mirror of
https://github.com/goatcorp/Dalamud.git
synced 2025-12-26 18:39:20 +01:00
Use vtable replacement as swapchain hook method
This commit is contained in:
parent
509e33bf27
commit
18d9d136fb
6 changed files with 310 additions and 90 deletions
|
|
@ -46,6 +46,7 @@
|
|||
<SDLCheck>true</SDLCheck>
|
||||
<ConformanceMode>true</ConformanceMode>
|
||||
<LanguageStandard>stdcpp20</LanguageStandard>
|
||||
<LanguageStandard_C>stdc17</LanguageStandard_C>
|
||||
<RuntimeLibrary>MultiThreadedDebug</RuntimeLibrary>
|
||||
<PrecompiledHeaderFile>pch.h</PrecompiledHeaderFile>
|
||||
<DebugInformationFormat>ProgramDatabase</DebugInformationFormat>
|
||||
|
|
@ -66,6 +67,7 @@
|
|||
<PreprocessorDefinitions>_DEBUG;%(PreprocessorDefinitions)</PreprocessorDefinitions>
|
||||
<PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">Use</PrecompiledHeader>
|
||||
<DisableSpecificWarnings Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">26812</DisableSpecificWarnings>
|
||||
<BuildStlModules Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">false</BuildStlModules>
|
||||
</ClCompile>
|
||||
<Link>
|
||||
<EnableCOMDATFolding>false</EnableCOMDATFolding>
|
||||
|
|
@ -185,4 +187,4 @@
|
|||
<Delete Files="$(OutDir)$(TargetName).lib" />
|
||||
<Delete Files="$(OutDir)$(TargetName).exp" />
|
||||
</Target>
|
||||
</Project>
|
||||
</Project>
|
||||
|
|
|
|||
|
|
@ -20,7 +20,7 @@ public abstract class BaseAddressResolver
|
|||
/// <summary>
|
||||
/// Gets or sets a value indicating whether the resolver has successfully run <see cref="Setup32Bit(SigScanner)"/> or <see cref="Setup64Bit(SigScanner)"/>.
|
||||
/// </summary>
|
||||
protected bool IsResolved { get; set; }
|
||||
public bool IsResolved { get; protected set; }
|
||||
|
||||
/// <summary>
|
||||
/// Setup the resolver, calling the appropriate method based on the process architecture,
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ using System.Diagnostics;
|
|||
using System.Runtime.InteropServices;
|
||||
|
||||
using Dalamud.Game.Internal.DXGI.Definitions;
|
||||
using Dalamud.Utility;
|
||||
using FFXIVClientStructs.FFXIV.Client.Graphics.Kernel;
|
||||
using Serilog;
|
||||
|
||||
|
|
@ -17,12 +18,19 @@ namespace Dalamud.Game.Internal.DXGI;
|
|||
/// </remarks>
|
||||
public class SwapChainVtableResolver : BaseAddressResolver, ISwapChainAddressResolver
|
||||
{
|
||||
public static readonly int NumDxgiSwapChainMethods = Enum.GetValues(typeof(IDXGISwapChainVtbl)).Length;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public IntPtr Present { get; set; }
|
||||
|
||||
/// <inheritdoc/>
|
||||
public IntPtr ResizeBuffers { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the pointer to DxgiSwapChain.
|
||||
/// </summary>
|
||||
public IntPtr DxgiSwapChain { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets a value indicating whether or not ReShade is loaded/used.
|
||||
/// </summary>
|
||||
|
|
@ -31,28 +39,12 @@ public class SwapChainVtableResolver : BaseAddressResolver, ISwapChainAddressRes
|
|||
/// <inheritdoc/>
|
||||
protected override unsafe void Setup64Bit(SigScanner sig)
|
||||
{
|
||||
Device* kernelDev;
|
||||
SwapChain* swapChain;
|
||||
void* dxgiSwapChain;
|
||||
var kernelDev = Util.NotNull(Device.Instance(), "Device.Instance()");
|
||||
var swapChain = Util.NotNull(kernelDev->SwapChain, "KernelDevice->SwapChain");
|
||||
var dxgiSwapChain = Util.NotNull(swapChain->DXGISwapChain, "SwapChain->DXGISwapChain");
|
||||
|
||||
while (true)
|
||||
{
|
||||
kernelDev = Device.Instance();
|
||||
if (kernelDev == null)
|
||||
continue;
|
||||
|
||||
swapChain = kernelDev->SwapChain;
|
||||
if (swapChain == null)
|
||||
continue;
|
||||
|
||||
dxgiSwapChain = swapChain->DXGISwapChain;
|
||||
if (dxgiSwapChain == null)
|
||||
continue;
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
var scVtbl = GetVTblAddresses(new IntPtr(dxgiSwapChain), Enum.GetValues(typeof(IDXGISwapChainVtbl)).Length);
|
||||
this.DxgiSwapChain = (nint)dxgiSwapChain;
|
||||
var scVtbl = GetVTblAddresses(this.DxgiSwapChain, NumDxgiSwapChainMethods);
|
||||
|
||||
this.Present = scVtbl[(int)IDXGISwapChainVtbl.Present];
|
||||
|
||||
|
|
|
|||
143
Dalamud/Hooking/ObjectVTableHook.cs
Normal file
143
Dalamud/Hooking/ObjectVTableHook.cs
Normal file
|
|
@ -0,0 +1,143 @@
|
|||
using System;
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
namespace Dalamud.Hooking;
|
||||
|
||||
/// <summary>
|
||||
/// Manages a hook that works by replacing the vtable of target object.
|
||||
/// </summary>
|
||||
public sealed unsafe class ObjectVTableHook : IDisposable
|
||||
{
|
||||
private readonly nint** ppVtbl;
|
||||
private readonly int numMethods;
|
||||
|
||||
private readonly nint* pVtblOriginal;
|
||||
private readonly nint* pVtblOverriden;
|
||||
|
||||
private readonly object?[] detourDelegates;
|
||||
|
||||
private bool released;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="ObjectVTableHook"/> class.
|
||||
/// </summary>
|
||||
/// <param name="ppVtbl">Address to vtable. Usually the address of the object itself.</param>
|
||||
/// <param name="numMethods">Number of methods in this vtable.</param>
|
||||
public ObjectVTableHook(nint ppVtbl, int numMethods)
|
||||
{
|
||||
this.ppVtbl = (nint**)ppVtbl;
|
||||
this.numMethods = numMethods;
|
||||
this.detourDelegates = new object?[numMethods];
|
||||
this.pVtblOriginal = *this.ppVtbl;
|
||||
this.pVtblOverriden = (nint*)Marshal.AllocHGlobal(sizeof(void*) * numMethods);
|
||||
this.VtblOriginal.CopyTo(this.VtblOverriden);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Finalizes an instance of the <see cref="ObjectVTableHook"/> class.
|
||||
/// </summary>
|
||||
~ObjectVTableHook() => this.ReleaseUnmanagedResources();
|
||||
|
||||
/// <summary>
|
||||
/// Gets the span view of original vtable.
|
||||
/// </summary>
|
||||
private Span<nint> VtblOriginal => new(this.pVtblOriginal, this.numMethods);
|
||||
|
||||
/// <summary>
|
||||
/// Gets the span view of overriden vtable.
|
||||
/// </summary>
|
||||
private Span<nint> VtblOverriden => new(this.pVtblOverriden, this.numMethods);
|
||||
|
||||
/// <summary>
|
||||
/// Disables the hook.
|
||||
/// </summary>
|
||||
public void Disable() => *this.ppVtbl = this.pVtblOriginal;
|
||||
|
||||
/// <inheritdoc />
|
||||
public void Dispose()
|
||||
{
|
||||
this.ReleaseUnmanagedResources();
|
||||
GC.SuppressFinalize(this);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Enables the hook.
|
||||
/// </summary>
|
||||
public void Enable() => *this.ppVtbl = this.pVtblOverriden;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the original method address of the given method index.
|
||||
/// </summary>
|
||||
/// <param name="methodIndex">The method index.</param>
|
||||
/// <returns>Address of the original method.</returns>
|
||||
public nint GetOriginalMethodAddress(int methodIndex)
|
||||
{
|
||||
this.EnsureMethodIndex(methodIndex);
|
||||
return this.pVtblOriginal[methodIndex];
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the original method of the given method index, as a delegate of given type.
|
||||
/// </summary>
|
||||
/// <param name="methodIndex">The method index.</param>
|
||||
/// <typeparam name="T">Type of delegate.</typeparam>
|
||||
/// <returns>Delegate to the original method.</returns>
|
||||
public T GetOriginalMethodDelegate<T>(int methodIndex)
|
||||
where T : Delegate
|
||||
{
|
||||
this.EnsureMethodIndex(methodIndex);
|
||||
return Marshal.GetDelegateForFunctionPointer<T>(this.pVtblOriginal[methodIndex]);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Resets a method to the original function.
|
||||
/// </summary>
|
||||
/// <param name="methodIndex">The method index.</param>
|
||||
public void ResetVtableEntry(int methodIndex)
|
||||
{
|
||||
this.EnsureMethodIndex(methodIndex);
|
||||
this.VtblOverriden[methodIndex] = this.pVtblOriginal[methodIndex];
|
||||
this.detourDelegates[methodIndex] = null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sets a method in vtable to the given address of function.
|
||||
/// </summary>
|
||||
/// <param name="methodIndex">The method index.</param>
|
||||
/// <param name="pfn">Address of the detour function.</param>
|
||||
/// <param name="refkeep">Additional reference to keep in memory.</param>
|
||||
public void SetVtableEntry(int methodIndex, nint pfn, object? refkeep)
|
||||
{
|
||||
this.EnsureMethodIndex(methodIndex);
|
||||
this.VtblOverriden[methodIndex] = pfn;
|
||||
this.detourDelegates[methodIndex] = refkeep;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sets a method in vtable to the given delegate.
|
||||
/// </summary>
|
||||
/// <param name="methodIndex">The method index.</param>
|
||||
/// <param name="detourDelegate">Detour delegate.</param>
|
||||
/// <typeparam name="T">Type of delegate.</typeparam>
|
||||
public void SetVtableEntry<T>(int methodIndex, T detourDelegate)
|
||||
where T : Delegate =>
|
||||
this.SetVtableEntry(methodIndex, Marshal.GetFunctionPointerForDelegate(detourDelegate), detourDelegate);
|
||||
|
||||
private void EnsureMethodIndex(int methodIndex)
|
||||
{
|
||||
if (methodIndex < 0 || methodIndex >= this.numMethods)
|
||||
{
|
||||
throw new ArgumentOutOfRangeException(nameof(methodIndex), methodIndex, null);
|
||||
}
|
||||
}
|
||||
|
||||
private void ReleaseUnmanagedResources()
|
||||
{
|
||||
if (!this.released)
|
||||
{
|
||||
this.Disable();
|
||||
Marshal.FreeHGlobal((nint)this.pVtblOverriden);
|
||||
this.released = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -14,6 +14,7 @@ using Dalamud.Game.ClientState.GamePad;
|
|||
using Dalamud.Game.ClientState.Keys;
|
||||
using Dalamud.Game.Gui.Internal;
|
||||
using Dalamud.Game.Internal.DXGI;
|
||||
using Dalamud.Game.Internal.DXGI.Definitions;
|
||||
using Dalamud.Hooking;
|
||||
using Dalamud.Interface.GameFonts;
|
||||
using Dalamud.Interface.Internal.ManagedAsserts;
|
||||
|
|
@ -60,6 +61,9 @@ internal class InterfaceManager : IDisposable, IServiceType
|
|||
[ServiceManager.ServiceDependency]
|
||||
private readonly Framework framework = Service<Framework>.Get();
|
||||
|
||||
[ServiceManager.ServiceDependency]
|
||||
private readonly SigScanner sigScanner = Service<SigScanner>.Get();
|
||||
|
||||
private readonly ManualResetEvent fontBuildSignal;
|
||||
private readonly SwapChainVtableResolver address;
|
||||
private readonly Hook<DispatchMessageWDelegate> dispatchMessageWHook;
|
||||
|
|
@ -67,8 +71,12 @@ internal class InterfaceManager : IDisposable, IServiceType
|
|||
private Hook<ProcessMessageDelegate> processMessageHook;
|
||||
private RawDX11Scene? scene;
|
||||
|
||||
private Hook<PresentDelegate>? presentHook;
|
||||
private Hook<ResizeBuffersDelegate>? resizeBuffersHook;
|
||||
private ObjectVTableHook? swapChainHook;
|
||||
|
||||
// Use these instead of querying for functions inside the above,
|
||||
// since we behave differently if ReShade or stuff are detected.
|
||||
private PresentDelegate presentOriginal;
|
||||
private ResizeBuffersDelegate resizeBuffersOriginal;
|
||||
|
||||
// can't access imgui IO before first present call
|
||||
private bool lastWantCapture = false;
|
||||
|
|
@ -84,8 +92,9 @@ internal class InterfaceManager : IDisposable, IServiceType
|
|||
null, "user32.dll", "SetCursor", 0, this.SetCursorDetour);
|
||||
|
||||
this.fontBuildSignal = new ManualResetEvent(false);
|
||||
|
||||
this.address = new SwapChainVtableResolver();
|
||||
|
||||
this.QueueHookResolution();
|
||||
}
|
||||
|
||||
[UnmanagedFunctionPointer(CallingConvention.ThisCall)]
|
||||
|
|
@ -220,13 +229,13 @@ internal class InterfaceManager : IDisposable, IServiceType
|
|||
this.framework.RunOnFrameworkThread(() =>
|
||||
{
|
||||
this.setCursorHook.Dispose();
|
||||
this.presentHook?.Dispose();
|
||||
this.resizeBuffersHook?.Dispose();
|
||||
this.dispatchMessageWHook.Dispose();
|
||||
this.processMessageHook?.Dispose();
|
||||
}).Wait();
|
||||
|
||||
this.scene?.Dispose();
|
||||
|
||||
this.swapChainHook?.Dispose();
|
||||
}
|
||||
|
||||
#nullable enable
|
||||
|
|
@ -598,22 +607,22 @@ internal class InterfaceManager : IDisposable, IServiceType
|
|||
*/
|
||||
private IntPtr PresentDetour(IntPtr swapChain, uint syncInterval, uint presentFlags)
|
||||
{
|
||||
if (this.scene != null && swapChain != this.scene.SwapChain.NativePointer)
|
||||
return this.presentHook!.Original(swapChain, syncInterval, presentFlags);
|
||||
|
||||
if (this.scene == null)
|
||||
this.InitScene(swapChain);
|
||||
|
||||
if (this.address.IsReshade)
|
||||
{
|
||||
var pRes = this.presentHook.Original(swapChain, syncInterval, presentFlags);
|
||||
|
||||
this.RenderImGui();
|
||||
|
||||
return pRes;
|
||||
this.InitScene(swapChain);
|
||||
}
|
||||
|
||||
this.RenderImGui();
|
||||
nint res;
|
||||
if (this.address.IsReshade)
|
||||
{
|
||||
res = this.presentOriginal(swapChain, syncInterval, presentFlags);
|
||||
this.RenderImGui();
|
||||
}
|
||||
else
|
||||
{
|
||||
this.RenderImGui();
|
||||
res = this.presentOriginal(swapChain, syncInterval, presentFlags);
|
||||
}
|
||||
|
||||
if (this.deferredDisposeTextures.Count > 0)
|
||||
{
|
||||
|
|
@ -626,7 +635,95 @@ internal class InterfaceManager : IDisposable, IServiceType
|
|||
this.deferredDisposeTextures.Clear();
|
||||
}
|
||||
|
||||
return this.presentHook.Original(swapChain, syncInterval, presentFlags);
|
||||
return res;
|
||||
}
|
||||
|
||||
private void QueueHookResolution()
|
||||
{
|
||||
if (this.GameWindowHandle != 0 && this.address.IsResolved)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
this.framework.RunOnFrameworkThread(() =>
|
||||
{
|
||||
if (this.GameWindowHandle == 0)
|
||||
{
|
||||
while ((this.GameWindowHandle = NativeFunctions.FindWindowEx(IntPtr.Zero, this.GameWindowHandle, "FFXIVGAME", IntPtr.Zero)) != IntPtr.Zero)
|
||||
{
|
||||
_ = User32.GetWindowThreadProcessId(this.GameWindowHandle, out var pid);
|
||||
|
||||
if (pid == Environment.ProcessId && User32.IsWindowVisible(this.GameWindowHandle))
|
||||
{
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (this.GameWindowHandle == 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
if (Service<DalamudConfiguration>.Get().WindowIsImmersive)
|
||||
{
|
||||
this.SetImmersiveMode(true);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Log.Error(ex, "Could not enable immersive mode");
|
||||
}
|
||||
|
||||
if (!this.address.IsResolved)
|
||||
{
|
||||
try
|
||||
{
|
||||
this.address.Setup();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Log.Error(ex, "Could not resolve addresses and set up hooks; trying again later");
|
||||
return;
|
||||
}
|
||||
|
||||
Log.Information("Resolver setup complete");
|
||||
|
||||
Log.Information("===== S W A P C H A I N =====");
|
||||
Log.Information($"Is ReShade: {this.address.IsReshade}");
|
||||
Log.Information($"Present address 0x{this.address.Present.ToInt64():X}");
|
||||
Log.Information($"ResizeBuffers address 0x{this.address.ResizeBuffers.ToInt64():X}");
|
||||
|
||||
this.presentOriginal =
|
||||
Marshal.GetDelegateForFunctionPointer<PresentDelegate>(this.address.Present);
|
||||
this.resizeBuffersOriginal =
|
||||
Marshal.GetDelegateForFunctionPointer<ResizeBuffersDelegate>(this.address.ResizeBuffers);
|
||||
|
||||
this.swapChainHook = new ObjectVTableHook(
|
||||
this.address.DxgiSwapChain,
|
||||
SwapChainVtableResolver.NumDxgiSwapChainMethods);
|
||||
this.swapChainHook.SetVtableEntry<PresentDelegate>(
|
||||
(int)IDXGISwapChainVtbl.Present,
|
||||
this.PresentDetour);
|
||||
this.swapChainHook.SetVtableEntry<ResizeBuffersDelegate>(
|
||||
(int)IDXGISwapChainVtbl.ResizeBuffers,
|
||||
this.ResizeBuffersDetour);
|
||||
this.swapChainHook.Enable();
|
||||
Log.Information("Present and ResizeBuffers hooked");
|
||||
|
||||
var wndProcAddress = this.sigScanner.ScanText("E8 ?? ?? ?? ?? 80 7C 24 ?? ?? 74 ?? B8");
|
||||
Log.Information($"WndProc address 0x{wndProcAddress.ToInt64():X}");
|
||||
this.processMessageHook =
|
||||
Hook<ProcessMessageDelegate>.FromAddress(wndProcAddress, this.ProcessMessageDetour);
|
||||
|
||||
this.setCursorHook.Enable();
|
||||
this.dispatchMessageWHook.Enable();
|
||||
this.processMessageHook.Enable();
|
||||
Log.Information("Hooks enabled");
|
||||
}
|
||||
}).ContinueWith(_ => this.QueueHookResolution());
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
|
|
@ -991,49 +1088,6 @@ internal class InterfaceManager : IDisposable, IServiceType
|
|||
}
|
||||
}
|
||||
|
||||
[ServiceManager.CallWhenServicesReady]
|
||||
private void ContinueConstruction(SigScanner sigScanner, Framework framework)
|
||||
{
|
||||
this.address.Setup(sigScanner);
|
||||
framework.RunOnFrameworkThread(() =>
|
||||
{
|
||||
while ((this.GameWindowHandle = NativeFunctions.FindWindowEx(IntPtr.Zero, this.GameWindowHandle, "FFXIVGAME", IntPtr.Zero)) != IntPtr.Zero)
|
||||
{
|
||||
_ = User32.GetWindowThreadProcessId(this.GameWindowHandle, out var pid);
|
||||
|
||||
if (pid == Environment.ProcessId && User32.IsWindowVisible(this.GameWindowHandle))
|
||||
break;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
if (Service<DalamudConfiguration>.Get().WindowIsImmersive)
|
||||
this.SetImmersiveMode(true);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Log.Error(ex, "Could not enable immersive mode");
|
||||
}
|
||||
|
||||
this.presentHook = Hook<PresentDelegate>.FromAddress(this.address.Present, this.PresentDetour);
|
||||
this.resizeBuffersHook = Hook<ResizeBuffersDelegate>.FromAddress(this.address.ResizeBuffers, this.ResizeBuffersDetour);
|
||||
|
||||
Log.Verbose("===== S W A P C H A I N =====");
|
||||
Log.Verbose($"Present address 0x{this.presentHook!.Address.ToInt64():X}");
|
||||
Log.Verbose($"ResizeBuffers address 0x{this.resizeBuffersHook!.Address.ToInt64():X}");
|
||||
|
||||
var wndProcAddress = sigScanner.ScanText("E8 ?? ?? ?? ?? 80 7C 24 ?? ?? 74 ?? B8");
|
||||
Log.Verbose($"WndProc address 0x{wndProcAddress.ToInt64():X}");
|
||||
this.processMessageHook = Hook<ProcessMessageDelegate>.FromAddress(wndProcAddress, this.ProcessMessageDetour);
|
||||
|
||||
this.setCursorHook.Enable();
|
||||
this.presentHook.Enable();
|
||||
this.resizeBuffersHook.Enable();
|
||||
this.dispatchMessageWHook.Enable();
|
||||
this.processMessageHook.Enable();
|
||||
});
|
||||
}
|
||||
|
||||
// This is intended to only be called as a handler attached to scene.OnNewRenderFrame
|
||||
private void RebuildFontsInternal()
|
||||
{
|
||||
|
|
@ -1078,14 +1132,9 @@ internal class InterfaceManager : IDisposable, IServiceType
|
|||
|
||||
this.ResizeBuffers?.InvokeSafely();
|
||||
|
||||
// We have to ensure we're working with the main swapchain,
|
||||
// as viewports might be resizing as well
|
||||
if (this.scene == null || swapChain != this.scene.SwapChain.NativePointer)
|
||||
return this.resizeBuffersHook!.Original(swapChain, bufferCount, width, height, newFormat, swapChainFlags);
|
||||
|
||||
this.scene?.OnPreResize();
|
||||
|
||||
var ret = this.resizeBuffersHook!.Original(swapChain, bufferCount, width, height, newFormat, swapChainFlags);
|
||||
var ret = this.resizeBuffersOriginal(swapChain, bufferCount, width, height, newFormat, swapChainFlags);
|
||||
if (ret.ToInt64() == 0x887A0001)
|
||||
{
|
||||
Log.Error("invalid call to resizeBuffers");
|
||||
|
|
|
|||
|
|
@ -546,6 +546,40 @@ public static class Util
|
|||
/// <returns>If Windows 11 has been detected.</returns>
|
||||
public static bool IsWindows11() => Environment.OSVersion.Version.Build >= 22000;
|
||||
|
||||
/// <summary>
|
||||
/// Ensures that a pointer is not null, or throw a <see cref="NullReferenceException" />
|
||||
/// </summary>
|
||||
/// <param name="value">Pointer value.</param>
|
||||
/// <param name="what">Help text for exception.</param>
|
||||
/// <typeparam name="T">Backing data type of the pointer.</typeparam>
|
||||
/// <returns>The value, ensured to be not null.</returns>
|
||||
public static unsafe T* NotNull<T>(T* value, string what)
|
||||
where T : unmanaged
|
||||
{
|
||||
if (value == null)
|
||||
{
|
||||
throw new NullReferenceException($"{what} is null.");
|
||||
}
|
||||
|
||||
return value;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Ensures that a pointer is not null, or throw a <see cref="NullReferenceException" />
|
||||
/// </summary>
|
||||
/// <param name="value">Pointer value.</param>
|
||||
/// <param name="what">Help text for exception.</param>
|
||||
/// <returns>The value, ensured to be not null.</returns>
|
||||
public static unsafe void* NotNull(void* value, string what)
|
||||
{
|
||||
if (value == null)
|
||||
{
|
||||
throw new NullReferenceException($"{what} is null.");
|
||||
}
|
||||
|
||||
return value;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Open a link in the default browser.
|
||||
/// </summary>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue