From 18d9d136fb831b84fc9c5421b4628685b6bc9eb2 Mon Sep 17 00:00:00 2001 From: Soreepeong Date: Sun, 28 May 2023 23:10:52 +0900 Subject: [PATCH] Use vtable replacement as swapchain hook method --- Dalamud.Boot/Dalamud.Boot.vcxproj | 4 +- Dalamud/Game/BaseAddressResolver.cs | 2 +- .../Internal/DXGI/SwapChainVtableResolver.cs | 34 ++-- Dalamud/Hooking/ObjectVTableHook.cs | 143 ++++++++++++++ .../Interface/Internal/InterfaceManager.cs | 183 +++++++++++------- Dalamud/Utility/Util.cs | 34 ++++ 6 files changed, 310 insertions(+), 90 deletions(-) create mode 100644 Dalamud/Hooking/ObjectVTableHook.cs 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. ///