Use vtable replacement as swapchain hook method

This commit is contained in:
Soreepeong 2023-05-28 23:10:52 +09:00
parent 509e33bf27
commit 18d9d136fb
6 changed files with 310 additions and 90 deletions

View file

@ -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>

View file

@ -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,

View file

@ -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];

View 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;
}
}
}

View file

@ -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");

View file

@ -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>