diff --git a/Dalamud.Boot/Dalamud.Boot.vcxproj b/Dalamud.Boot/Dalamud.Boot.vcxproj
index ea263d7f9..e0c82f25a 100644
--- a/Dalamud.Boot/Dalamud.Boot.vcxproj
+++ b/Dalamud.Boot/Dalamud.Boot.vcxproj
@@ -46,6 +46,7 @@
true
true
stdcpp20
+ stdc17
MultiThreadedDebug
pch.h
ProgramDatabase
@@ -66,6 +67,7 @@
_DEBUG;%(PreprocessorDefinitions)
Use
26812
+ false
false
@@ -185,4 +187,4 @@
-
\ No newline at end of file
+
diff --git a/Dalamud/Game/BaseAddressResolver.cs b/Dalamud/Game/BaseAddressResolver.cs
index 24e7dffe8..69736eafb 100644
--- a/Dalamud/Game/BaseAddressResolver.cs
+++ b/Dalamud/Game/BaseAddressResolver.cs
@@ -20,7 +20,7 @@ public abstract class BaseAddressResolver
///
/// Gets or sets a value indicating whether the resolver has successfully run or .
///
- protected bool IsResolved { get; set; }
+ public bool IsResolved { get; protected set; }
///
/// Setup the resolver, calling the appropriate method based on the process architecture,
diff --git a/Dalamud/Game/Internal/DXGI/SwapChainVtableResolver.cs b/Dalamud/Game/Internal/DXGI/SwapChainVtableResolver.cs
index 603324175..6df67409d 100644
--- a/Dalamud/Game/Internal/DXGI/SwapChainVtableResolver.cs
+++ b/Dalamud/Game/Internal/DXGI/SwapChainVtableResolver.cs
@@ -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;
///
public class SwapChainVtableResolver : BaseAddressResolver, ISwapChainAddressResolver
{
+ public static readonly int NumDxgiSwapChainMethods = Enum.GetValues(typeof(IDXGISwapChainVtbl)).Length;
+
///
public IntPtr Present { get; set; }
///
public IntPtr ResizeBuffers { get; set; }
+ ///
+ /// Gets or sets the pointer to DxgiSwapChain.
+ ///
+ public IntPtr DxgiSwapChain { get; set; }
+
///
/// Gets a value indicating whether or not ReShade is loaded/used.
///
@@ -31,28 +39,12 @@ public class SwapChainVtableResolver : BaseAddressResolver, ISwapChainAddressRes
///
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];
diff --git a/Dalamud/Hooking/ObjectVTableHook.cs b/Dalamud/Hooking/ObjectVTableHook.cs
new file mode 100644
index 000000000..4ace5602a
--- /dev/null
+++ b/Dalamud/Hooking/ObjectVTableHook.cs
@@ -0,0 +1,143 @@
+using System;
+using System.Runtime.InteropServices;
+
+namespace Dalamud.Hooking;
+
+///
+/// Manages a hook that works by replacing the vtable of target object.
+///
+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;
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// Address to vtable. Usually the address of the object itself.
+ /// Number of methods in this vtable.
+ 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);
+ }
+
+ ///
+ /// Finalizes an instance of the class.
+ ///
+ ~ObjectVTableHook() => this.ReleaseUnmanagedResources();
+
+ ///
+ /// Gets the span view of original vtable.
+ ///
+ private Span VtblOriginal => new(this.pVtblOriginal, this.numMethods);
+
+ ///
+ /// Gets the span view of overriden vtable.
+ ///
+ private Span VtblOverriden => new(this.pVtblOverriden, this.numMethods);
+
+ ///
+ /// Disables the hook.
+ ///
+ public void Disable() => *this.ppVtbl = this.pVtblOriginal;
+
+ ///
+ public void Dispose()
+ {
+ this.ReleaseUnmanagedResources();
+ GC.SuppressFinalize(this);
+ }
+
+ ///
+ /// Enables the hook.
+ ///
+ public void Enable() => *this.ppVtbl = this.pVtblOverriden;
+
+ ///
+ /// Gets the original method address of the given method index.
+ ///
+ /// The method index.
+ /// Address of the original method.
+ public nint GetOriginalMethodAddress(int methodIndex)
+ {
+ this.EnsureMethodIndex(methodIndex);
+ return this.pVtblOriginal[methodIndex];
+ }
+
+ ///
+ /// Gets the original method of the given method index, as a delegate of given type.
+ ///
+ /// The method index.
+ /// Type of delegate.
+ /// Delegate to the original method.
+ public T GetOriginalMethodDelegate(int methodIndex)
+ where T : Delegate
+ {
+ this.EnsureMethodIndex(methodIndex);
+ return Marshal.GetDelegateForFunctionPointer(this.pVtblOriginal[methodIndex]);
+ }
+
+ ///
+ /// Resets a method to the original function.
+ ///
+ /// The method index.
+ public void ResetVtableEntry(int methodIndex)
+ {
+ this.EnsureMethodIndex(methodIndex);
+ this.VtblOverriden[methodIndex] = this.pVtblOriginal[methodIndex];
+ this.detourDelegates[methodIndex] = null;
+ }
+
+ ///
+ /// Sets a method in vtable to the given address of function.
+ ///
+ /// The method index.
+ /// Address of the detour function.
+ /// Additional reference to keep in memory.
+ public void SetVtableEntry(int methodIndex, nint pfn, object? refkeep)
+ {
+ this.EnsureMethodIndex(methodIndex);
+ this.VtblOverriden[methodIndex] = pfn;
+ this.detourDelegates[methodIndex] = refkeep;
+ }
+
+ ///
+ /// Sets a method in vtable to the given delegate.
+ ///
+ /// The method index.
+ /// Detour delegate.
+ /// Type of delegate.
+ public void SetVtableEntry(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;
+ }
+ }
+}
diff --git a/Dalamud/Interface/Internal/InterfaceManager.cs b/Dalamud/Interface/Internal/InterfaceManager.cs
index 6bb45b325..4ceb51b62 100644
--- a/Dalamud/Interface/Internal/InterfaceManager.cs
+++ b/Dalamud/Interface/Internal/InterfaceManager.cs
@@ -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.Get();
+ [ServiceManager.ServiceDependency]
+ private readonly SigScanner sigScanner = Service.Get();
+
private readonly ManualResetEvent fontBuildSignal;
private readonly SwapChainVtableResolver address;
private readonly Hook dispatchMessageWHook;
@@ -67,8 +71,12 @@ internal class InterfaceManager : IDisposable, IServiceType
private Hook processMessageHook;
private RawDX11Scene? scene;
- private Hook? presentHook;
- private Hook? 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.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(this.address.Present);
+ this.resizeBuffersOriginal =
+ Marshal.GetDelegateForFunctionPointer(this.address.ResizeBuffers);
+
+ this.swapChainHook = new ObjectVTableHook(
+ this.address.DxgiSwapChain,
+ SwapChainVtableResolver.NumDxgiSwapChainMethods);
+ this.swapChainHook.SetVtableEntry(
+ (int)IDXGISwapChainVtbl.Present,
+ this.PresentDetour);
+ this.swapChainHook.SetVtableEntry(
+ (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.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.Get().WindowIsImmersive)
- this.SetImmersiveMode(true);
- }
- catch (Exception ex)
- {
- Log.Error(ex, "Could not enable immersive mode");
- }
-
- this.presentHook = Hook.FromAddress(this.address.Present, this.PresentDetour);
- this.resizeBuffersHook = Hook.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.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");
diff --git a/Dalamud/Utility/Util.cs b/Dalamud/Utility/Util.cs
index 038273eb6..cb60e60b8 100644
--- a/Dalamud/Utility/Util.cs
+++ b/Dalamud/Utility/Util.cs
@@ -546,6 +546,40 @@ public static class Util
/// If Windows 11 has been detected.
public static bool IsWindows11() => Environment.OSVersion.Version.Build >= 22000;
+ ///
+ /// Ensures that a pointer is not null, or throw a
+ ///
+ /// Pointer value.
+ /// Help text for exception.
+ /// Backing data type of the pointer.
+ /// The value, ensured to be not null.
+ public static unsafe T* NotNull(T* value, string what)
+ where T : unmanaged
+ {
+ if (value == null)
+ {
+ throw new NullReferenceException($"{what} is null.");
+ }
+
+ return value;
+ }
+
+ ///
+ /// Ensures that a pointer is not null, or throw a
+ ///
+ /// Pointer value.
+ /// Help text for exception.
+ /// The value, ensured to be not null.
+ public static unsafe void* NotNull(void* value, string what)
+ {
+ if (value == null)
+ {
+ throw new NullReferenceException($"{what} is null.");
+ }
+
+ return value;
+ }
+
///
/// Open a link in the default browser.
///