diff --git a/Dalamud/Hooking/Internal/ObjectVTableHook.cs b/Dalamud/Hooking/Internal/ObjectVTableHook.cs index b4500bb5f..8b2f24de2 100644 --- a/Dalamud/Hooking/Internal/ObjectVTableHook.cs +++ b/Dalamud/Hooking/Internal/ObjectVTableHook.cs @@ -54,6 +54,15 @@ internal unsafe class ObjectVTableHook : IDisposable /// Gets the span view of overriden vtable. public ReadOnlySpan OverridenVTableSpan => this.vtblOverriden.AsSpan(); + /// Gets the address of the pointer to the vtable. + public nint Address => (nint)this.ppVtbl; + + /// Gets the address of the original vtable. + public nint OriginalVTableAddress => (nint)this.pVtblOriginal; + + /// Gets the address of the overriden vtable. + public nint OverridenVTableAddress => (nint)Unsafe.AsPointer(ref this.vtblOverriden[0]); + /// Disables the hook. public void Disable() { diff --git a/Dalamud/Interface/Internal/InterfaceManager.AsHook.cs b/Dalamud/Interface/Internal/InterfaceManager.AsHook.cs index b2afb970f..3dc8b30cd 100644 --- a/Dalamud/Interface/Internal/InterfaceManager.AsHook.cs +++ b/Dalamud/Interface/Internal/InterfaceManager.AsHook.cs @@ -1,75 +1,142 @@ using System.Diagnostics; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; using Dalamud.Utility; +using TerraFX.Interop.DirectX; +using TerraFX.Interop.Windows; + namespace Dalamud.Interface.Internal; /// /// This class manages interaction with the ImGui interface. /// -internal partial class InterfaceManager +internal unsafe partial class InterfaceManager { - private IntPtr PresentDetour(IntPtr swapChain, uint syncInterval, uint presentFlags) + // NOTE: Do not use HRESULT as return value type. It appears that .NET marshaller thinks HRESULT needs to be still + // treated as a type that does not fit into RAX. + + /// Delegate for DXGISwapChain::on_present(UINT flags, const DXGI_PRESENT_PARAMETERS *params) in + /// dxgi_swapchain.cpp. + /// Pointer to an instance of DXGISwapChain, which happens to be an + /// . + /// An integer value that contains swap-chain presentation options. These options are defined by + /// the DXGI_PRESENT constants. + /// Optional; DXGI present parameters. + [UnmanagedFunctionPointer(CallingConvention.ThisCall)] + private delegate void ReShadeDxgiSwapChainPresentDelegate( + ReShadeDxgiSwapChain* swapChain, + uint flags, + DXGI_PRESENT_PARAMETERS* presentParams); + + /// Delegate for . + /// Microsoft + /// Learn. + /// Pointer to an instance of . + /// An integer that specifies how to synchronize presentation of a frame with the + /// vertical blank. + /// An integer value that contains swap-chain presentation options. These options are defined by + /// the DXGI_PRESENT constants. + /// A representing the result of the operation. + [UnmanagedFunctionPointer(CallingConvention.ThisCall)] + private delegate int DxgiSwapChainPresentDelegate(IDXGISwapChain* swapChain, uint syncInterval, uint flags); + + /// Detour function for . + /// + /// Microsoft Learn. + /// Pointer to an instance of . + /// The number of buffers in the swap chain (including all back and front buffers). + /// This number can be different from the number of buffers with which you created the swap chain. This number + /// can't be greater than . Set this number to zero to preserve the + /// existing number of buffers in the swap chain. You can't specify less than two buffers for the flip presentation + /// model. + /// The new width of the back buffer. If you specify zero, DXGI will use the width of the client + /// area of the target window. You can't specify the width as zero if you called the + /// method to create the swap chain for a composition + /// surface. + /// The new height of the back buffer. If you specify zero, DXGI will use the height of the + /// client area of the target window. You can't specify the height as zero if you called the + /// method to create the swap chain for a composition + /// surface. + /// A DXGI_FORMAT-typed value for the new format of the back buffer. Set this value to + /// to preserve the existing format of the back buffer. The flip + /// presentation model supports a more restricted set of formats than the bit-block transfer (bitblt) model. + /// A combination of -typed values that are combined + /// by using a bitwise OR operation. The resulting value specifies options for swap-chain behavior. + /// A representing the result of the operation. + [UnmanagedFunctionPointer(CallingConvention.ThisCall)] + private delegate int ResizeBuffersDelegate( + IDXGISwapChain* swapChain, + uint bufferCount, + uint width, + uint height, + DXGI_FORMAT newFormat, + uint swapChainFlags); + + private void ReShadeDxgiSwapChainOnPresentDetour( + ReShadeDxgiSwapChain* swapChain, + uint flags, + DXGI_PRESENT_PARAMETERS* presentParams) { - if (!SwapChainHelper.IsGameDeviceSwapChain(swapChain)) - return this.dxgiPresentHook!.Original(swapChain, syncInterval, presentFlags); + Debug.Assert( + this.reShadeDxgiSwapChainPresentHook is not null, + "this.reShadeDxgiSwapChainPresentHook is not null"); - Debug.Assert(this.dxgiPresentHook is not null, "How did PresentDetour get called when presentHook is null?"); - Debug.Assert(this.dalamudAtlas is not null, "dalamudAtlas should have been set already"); + // Call this first to draw Dalamud over ReShade. + this.reShadeDxgiSwapChainPresentHook!.Original(swapChain, flags, presentParams); - if (this.scene == null) - this.InitScene(swapChain); + if (this.RenderDalamudCheckAndInitialize(swapChain->AsIDxgiSwapChain()) is { } activeScene) + this.RenderDalamudDraw(activeScene); - Debug.Assert(this.scene is not null, "InitScene did not set the scene field, but did not throw an exception."); - - if (!this.dalamudAtlas!.HasBuiltAtlas) - { - if (this.dalamudAtlas.BuildTask.Exception != null) - { - // TODO: Can we do something more user-friendly here? Unload instead? - Log.Error(this.dalamudAtlas.BuildTask.Exception, "Failed to initialize Dalamud base fonts"); - Util.Fatal("Failed to initialize Dalamud base fonts.\nPlease report this error.", "Dalamud"); - } - - return this.dxgiPresentHook!.Original(swapChain, syncInterval, presentFlags); - } - - this.CumulativePresentCalls++; - this.IsMainThreadInPresent = true; - - while (this.runBeforeImGuiRender.TryDequeue(out var action)) - action.InvokeSafely(); - - RenderImGui(this.scene!); - this.PostImGuiRender(); - this.IsMainThreadInPresent = false; - - return this.dxgiPresentHook!.Original(swapChain, syncInterval, presentFlags); + // Upstream call to system IDXGISwapChain::Present will be called by ReShade. } - private IntPtr AsHookResizeBuffersDetour( - IntPtr swapChain, uint bufferCount, uint width, uint height, uint newFormat, uint swapChainFlags) + private int DxgiSwapChainPresentDetour(IDXGISwapChain* swapChain, uint syncInterval, uint flags) + { + Debug.Assert(this.dxgiSwapChainPresentHook is not null, "this.dxgiSwapChainPresentHook is not null"); + + if (this.RenderDalamudCheckAndInitialize(swapChain) is { } activeScene) + this.RenderDalamudDraw(activeScene); + + return this.dxgiSwapChainPresentHook!.Original(swapChain, syncInterval, flags); + } + + private int AsHookDxgiSwapChainResizeBuffersDetour( + IDXGISwapChain* swapChain, + uint bufferCount, + uint width, + uint height, + DXGI_FORMAT newFormat, + uint swapChainFlags) { if (!SwapChainHelper.IsGameDeviceSwapChain(swapChain)) - return this.resizeBuffersHook!.Original(swapChain, bufferCount, width, height, newFormat, swapChainFlags); + return this.dxgiSwapChainResizeBuffersHook!.Original(swapChain, bufferCount, width, height, newFormat, swapChainFlags); #if DEBUG Log.Verbose( - $"Calling resizebuffers swap@{swapChain.ToInt64():X}{bufferCount} {width} {height} {newFormat} {swapChainFlags}"); + $"Calling resizebuffers swap@{(nint)swapChain:X}{bufferCount} {width} {height} {newFormat} {swapChainFlags}"); #endif this.ResizeBuffers?.InvokeSafely(); this.scene?.OnPreResize(); - var ret = this.resizeBuffersHook!.Original(swapChain, bufferCount, width, height, newFormat, swapChainFlags); - if (ret.ToInt64() == 0x887A0001) - { + var ret = this.dxgiSwapChainResizeBuffersHook!.Original(swapChain, bufferCount, width, height, newFormat, swapChainFlags); + if (ret == DXGI.DXGI_ERROR_INVALID_CALL) Log.Error("invalid call to resizeBuffers"); - } this.scene?.OnPostResize((int)width, (int)height); return ret; } + + /// Represents DXGISwapChain in ReShade. + [StructLayout(LayoutKind.Sequential)] + private struct ReShadeDxgiSwapChain + { + // DXGISwapChain only implements IDXGISwapChain4. The only vtable should be that. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public IDXGISwapChain* AsIDxgiSwapChain() => (IDXGISwapChain*)Unsafe.AsPointer(ref this); + } } diff --git a/Dalamud/Interface/Internal/InterfaceManager.AsReShadeAddon.cs b/Dalamud/Interface/Internal/InterfaceManager.AsReShadeAddon.cs index 486a3874b..817a935ee 100644 --- a/Dalamud/Interface/Internal/InterfaceManager.AsReShadeAddon.cs +++ b/Dalamud/Interface/Internal/InterfaceManager.AsReShadeAddon.cs @@ -11,9 +11,9 @@ namespace Dalamud.Interface.Internal; /// /// This class manages interaction with the ImGui interface. /// -internal partial class InterfaceManager +internal unsafe partial class InterfaceManager { - private unsafe void ReShadeAddonInterfaceOnDestroySwapChain(ref ReShadeAddonInterface.ApiObject swapChain) + private void ReShadeAddonInterfaceOnDestroySwapChain(ref ReShadeAddonInterface.ApiObject swapChain) { var swapChainNative = swapChain.GetNative(); if (this.scene?.SwapChain.NativePointer != (nint)swapChainNative) @@ -22,7 +22,7 @@ internal partial class InterfaceManager this.scene?.OnPreResize(); } - private unsafe void ReShadeAddonInterfaceOnInitSwapChain(ref ReShadeAddonInterface.ApiObject swapChain) + private void ReShadeAddonInterfaceOnInitSwapChain(ref ReShadeAddonInterface.ApiObject swapChain) { var swapChainNative = swapChain.GetNative(); if (this.scene?.SwapChain.NativePointer != (nint)swapChainNative) @@ -42,52 +42,25 @@ internal partial class InterfaceManager ReadOnlySpan destRect, ReadOnlySpan dirtyRects) { - var swapChainNative = swapChain.GetNative(); + var swapChainNative = swapChain.GetNative(); - if (this.scene == null) - this.InitScene(swapChainNative); - - if (this.scene?.SwapChain.NativePointer != swapChainNative) - return; - - Debug.Assert(this.dalamudAtlas is not null, "this.dalamudAtlas is not null"); - - if (!this.dalamudAtlas!.HasBuiltAtlas) - { - if (this.dalamudAtlas.BuildTask.Exception != null) - { - // TODO: Can we do something more user-friendly here? Unload instead? - Log.Error(this.dalamudAtlas.BuildTask.Exception, "Failed to initialize Dalamud base fonts"); - Util.Fatal("Failed to initialize Dalamud base fonts.\nPlease report this error.", "Dalamud"); - } - - return; - } - - this.CumulativePresentCalls++; - this.IsMainThreadInPresent = true; - - while (this.runBeforeImGuiRender.TryDequeue(out var action)) - action.InvokeSafely(); - - RenderImGui(this.scene!); - this.PostImGuiRender(); - this.IsMainThreadInPresent = false; + if (this.RenderDalamudCheckAndInitialize(swapChainNative) is { } activeScene) + this.RenderDalamudDraw(activeScene); } - private nint AsReShadeAddonResizeBuffersDetour( - nint swapChain, + private int AsReShadeAddonDxgiSwapChainResizeBuffersDetour( + IDXGISwapChain* swapChain, uint bufferCount, uint width, uint height, - uint newFormat, + DXGI_FORMAT newFormat, uint swapChainFlags) { // Hooked vtbl instead of registering ReShade event. This check is correct. if (!SwapChainHelper.IsGameDeviceSwapChain(swapChain)) - return this.resizeBuffersHook!.Original(swapChain, bufferCount, width, height, newFormat, swapChainFlags); + return this.dxgiSwapChainResizeBuffersHook!.Original(swapChain, bufferCount, width, height, newFormat, swapChainFlags); this.ResizeBuffers?.InvokeSafely(); - return this.resizeBuffersHook!.Original(swapChain, bufferCount, width, height, newFormat, swapChainFlags); + return this.dxgiSwapChainResizeBuffersHook!.Original(swapChain, bufferCount, width, height, newFormat, swapChainFlags); } } diff --git a/Dalamud/Interface/Internal/InterfaceManager.cs b/Dalamud/Interface/Internal/InterfaceManager.cs index 0491fed1e..45cba5c76 100644 --- a/Dalamud/Interface/Internal/InterfaceManager.cs +++ b/Dalamud/Interface/Internal/InterfaceManager.cs @@ -3,8 +3,8 @@ using System.Collections.Generic; using System.Diagnostics; using System.IO; using System.Linq; -using System.Runtime.CompilerServices; using System.Runtime.InteropServices; +using System.Text; using System.Threading; using System.Threading.Tasks; @@ -97,9 +97,10 @@ internal partial class InterfaceManager : IInternalDisposableService private RawDX11Scene? scene; private Hook? setCursorHook; - private Hook? dxgiPresentHook; - private Hook? resizeBuffersHook; - private ObjectVTableHook>? swapChainHook; + private Hook? reShadeDxgiSwapChainPresentHook; + private Hook? dxgiSwapChainPresentHook; + private Hook? dxgiSwapChainResizeBuffersHook; + private ObjectVTableHook>? dxgiSwapChainHook; private ReShadeAddonInterface? reShadeAddonInterface; private IFontAtlas? dalamudAtlas; @@ -115,12 +116,6 @@ internal partial class InterfaceManager : IInternalDisposableService { } - [UnmanagedFunctionPointer(CallingConvention.ThisCall)] - private delegate IntPtr DxgiPresentDelegate(IntPtr swapChain, uint syncInterval, uint presentFlags); - - [UnmanagedFunctionPointer(CallingConvention.ThisCall)] - private delegate IntPtr ResizeBuffersDelegate(IntPtr swapChain, uint bufferCount, uint width, uint height, uint newFormat, uint swapChainFlags); - [UnmanagedFunctionPointer(CallingConvention.StdCall)] private delegate IntPtr SetCursorDelegate(IntPtr hCursor); @@ -231,7 +226,7 @@ internal partial class InterfaceManager : IInternalDisposableService /// public bool IsDispatchingEvents { get; set; } = true; - /// Gets a value indicating whether the main thread is executing . + /// Gets a value indicating whether the main thread is executing . /// This still will be true even when queried off the main thread. public bool IsMainThreadInPresent { get; private set; } @@ -265,7 +260,7 @@ internal partial class InterfaceManager : IInternalDisposableService /// public Task FontBuildTask => WhenFontsReady().dalamudAtlas!.BuildTask; - /// Gets the number of calls to so far. + /// Gets the number of calls to so far. /// /// The value increases even when Dalamud is hidden via "/xlui hide". /// does not. @@ -312,9 +307,10 @@ internal partial class InterfaceManager : IInternalDisposableService { this.wndProcHookManager.PreWndProc -= this.WndProcHookManagerOnPreWndProc; Interlocked.Exchange(ref this.setCursorHook, null)?.Dispose(); - Interlocked.Exchange(ref this.dxgiPresentHook, null)?.Dispose(); - Interlocked.Exchange(ref this.resizeBuffersHook, null)?.Dispose(); - Interlocked.Exchange(ref this.swapChainHook, null)?.Dispose(); + Interlocked.Exchange(ref this.dxgiSwapChainPresentHook, null)?.Dispose(); + Interlocked.Exchange(ref this.reShadeDxgiSwapChainPresentHook, null)?.Dispose(); + Interlocked.Exchange(ref this.dxgiSwapChainResizeBuffersHook, null)?.Dispose(); + Interlocked.Exchange(ref this.dxgiSwapChainHook, null)?.Dispose(); Interlocked.Exchange(ref this.reShadeAddonInterface, null)?.Dispose(); } } @@ -497,31 +493,72 @@ internal partial class InterfaceManager : IInternalDisposableService return im; } - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static void RenderImGui(RawDX11Scene scene) + /// Checks if the provided swap chain is the target that Dalamud should draw its interface onto, + /// and initializes ImGui for drawing. + /// The swap chain to test and initialize ImGui with if conditions are met. + /// An initialized instance of , or null if + /// is not the main swap chain. + private unsafe RawDX11Scene? RenderDalamudCheckAndInitialize(IDXGISwapChain* swapChain) { - var conf = Service.Get(); + if (!SwapChainHelper.IsGameDeviceSwapChain(swapChain)) + return null; + + Debug.Assert(this.dalamudAtlas is not null, "dalamudAtlas should have been set already"); + + var activeScene = this.scene ?? this.InitScene(swapChain); + + if (!this.dalamudAtlas!.HasBuiltAtlas) + { + if (this.dalamudAtlas.BuildTask.Exception != null) + { + // TODO: Can we do something more user-friendly here? Unload instead? + Log.Error(this.dalamudAtlas.BuildTask.Exception, "Failed to initialize Dalamud base fonts"); + Util.Fatal("Failed to initialize Dalamud base fonts.\nPlease report this error.", "Dalamud"); + } + + return null; + } + + return activeScene; + } + + /// Draws Dalamud to the given scene representing the ImGui context. + /// The scene to draw to. + private void RenderDalamudDraw(RawDX11Scene activeScene) + { + this.CumulativePresentCalls++; + this.IsMainThreadInPresent = true; + + while (this.runBeforeImGuiRender.TryDequeue(out var action)) + action.InvokeSafely(); // Process information needed by ImGuiHelpers each frame. ImGuiHelpers.NewFrame(); // Enable viewports if there are no issues. - if (conf.IsDisableViewport || scene.SwapChain.IsFullScreen || ImGui.GetPlatformIO().Monitors.Size == 1) + var viewportsEnable = this.dalamudConfiguration.IsDisableViewport || + activeScene.SwapChain.IsFullScreen || + ImGui.GetPlatformIO().Monitors.Size == 1; + if (viewportsEnable) ImGui.GetIO().ConfigFlags &= ~ImGuiConfigFlags.ViewportsEnable; else ImGui.GetIO().ConfigFlags |= ImGuiConfigFlags.ViewportsEnable; - scene.Render(); + // Call drawing functions, which in turn will call Draw event. + activeScene.Render(); + + this.PostImGuiRender(); + this.IsMainThreadInPresent = false; } - private void InitScene(IntPtr swapChain) + private unsafe RawDX11Scene InitScene(IDXGISwapChain* swapChain) { RawDX11Scene newScene; using (Timings.Start("IM Scene Init")) { try { - newScene = new RawDX11Scene(swapChain); + newScene = new RawDX11Scene((nint)swapChain); } catch (DllNotFoundException ex) { @@ -547,7 +584,7 @@ internal partial class InterfaceManager : IInternalDisposableService Environment.Exit(-1); // Doesn't reach here, but to make the compiler not complain - return; + throw new InvalidOperationException(); } var startInfo = Service.Get().StartInfo; @@ -638,6 +675,7 @@ internal partial class InterfaceManager : IInternalDisposableService Service.Provide(new(this)); this.wndProcHookManager.PreWndProc += this.WndProcHookManagerOnPreWndProc; + return newScene; } private unsafe void WndProcHookManagerOnPreWndProc(WndProcEventArgs args) @@ -805,39 +843,84 @@ internal partial class InterfaceManager : IInternalDisposableService }); } - Log.Verbose("===== S W A P C H A I N ====="); - if (this.dalamudConfiguration.ReShadeHandlingMode == ReShadeHandlingMode.UnwrapReShade) + Log.Information("===== S W A P C H A I N ====="); + var sb = new StringBuilder(); + foreach (var m in ReShadeAddonInterface.AllReShadeModules) { - if (SwapChainHelper.UnwrapReShade()) - Log.Information("Unwrapped ReShade"); + sb.Clear(); + sb.Append("ReShade detected: "); + sb.Append(m.FileName).Append('('); + sb.Append(m.FileVersionInfo.OriginalFilename); + sb.Append("; ").Append(m.FileVersionInfo.ProductName); + sb.Append("; ").Append(m.FileVersionInfo.ProductVersion); + sb.Append("; ").Append(m.FileVersionInfo.FileDescription); + sb.Append("; ").Append(m.FileVersionInfo.FileVersion); + sb.Append($"@ 0x{m.BaseAddress:X}"); + if (!ReferenceEquals(m, ReShadeAddonInterface.ReShadeModule)) + sb.Append(" [ignored by Dalamud]"); + Log.Information(sb.ToString()); } - ResizeBuffersDelegate? resizeBuffersDelegate = null; - DxgiPresentDelegate? dxgiPresentDelegate = null; - if (this.dalamudConfiguration.ReShadeHandlingMode == ReShadeHandlingMode.ReShadeAddon) - { - if (ReShadeAddonInterface.TryRegisterAddon(out this.reShadeAddonInterface)) - { - resizeBuffersDelegate = this.AsReShadeAddonResizeBuffersDetour; + if (ReShadeAddonInterface.AllReShadeModules.Length > 1) + Log.Warning("Multiple ReShade dlls are detected."); - Log.Information( - "Registered as a ReShade({Name}: 0x{Addr:X}) addon", - ReShadeAddonInterface.ReShadeModule!.FileName, - ReShadeAddonInterface.ReShadeModule!.BaseAddress); + ResizeBuffersDelegate dxgiSwapChainResizeBuffersDelegate; + ReShadeDxgiSwapChainPresentDelegate? reShadeDxgiSwapChainPresentDelegate = null; + DxgiSwapChainPresentDelegate? dxgiSwapChainPresentDelegate = null; + nint pfnReShadeDxgiSwapChainPresent = 0; + switch (this.dalamudConfiguration.ReShadeHandlingMode) + { + // This is the only mode honored when SwapChainHookMode is set to VTable. + case ReShadeHandlingMode.UnwrapReShade when ReShadeAddonInterface.ReShadeModule is not null: + if (SwapChainHelper.UnwrapReShade()) + Log.Information("Unwrapped ReShade"); + else + Log.Warning("Could not unwrap ReShade"); + goto default; + + // Do no special ReShade handling. + // If ReShade is not found or SwapChainHookMode is set to VTable, also do nothing special. + case ReShadeHandlingMode.None: + case var _ when ReShadeAddonInterface.ReShadeModule is null: + case var _ when this.dalamudConfiguration.SwapChainHookMode == SwapChainHelper.HookMode.VTable: + default: + dxgiSwapChainResizeBuffersDelegate = this.AsHookDxgiSwapChainResizeBuffersDetour; + dxgiSwapChainPresentDelegate = this.DxgiSwapChainPresentDetour; + break; + + // Register Dalamud as a ReShade addon. + case ReShadeHandlingMode.ReShadeAddon: + if (!ReShadeAddonInterface.TryRegisterAddon(out this.reShadeAddonInterface)) + { + Log.Warning("Could not register as ReShade addon"); + goto default; + } + + Log.Information("Registered as a ReShade addon"); this.reShadeAddonInterface.InitSwapChain += this.ReShadeAddonInterfaceOnInitSwapChain; this.reShadeAddonInterface.DestroySwapChain += this.ReShadeAddonInterfaceOnDestroySwapChain; this.reShadeAddonInterface.Present += this.ReShadeAddonInterfaceOnPresent; - } - else - { - Log.Information("Could not register as ReShade addon"); - } - } - if (resizeBuffersDelegate is null) - { - resizeBuffersDelegate = this.AsHookResizeBuffersDetour; - dxgiPresentDelegate = this.PresentDetour; + dxgiSwapChainResizeBuffersDelegate = this.AsReShadeAddonDxgiSwapChainResizeBuffersDetour; + break; + + // Hook ReShade's DXGISwapChain::on_present. This is the legacy and the default option. + case ReShadeHandlingMode.Default: + case ReShadeHandlingMode.HookReShadeDxgiSwapChainOnPresent: + pfnReShadeDxgiSwapChainPresent = ReShadeAddonInterface.FindReShadeDxgiSwapChainOnPresent(); + + if (pfnReShadeDxgiSwapChainPresent == 0) + { + Log.Warning("ReShade::DXGISwapChain::on_present could not be found"); + goto default; + } + + Log.Information( + "Found ReShade::DXGISwapChain::on_present at {addr}", + Util.DescribeAddress(pfnReShadeDxgiSwapChainPresent)); + reShadeDxgiSwapChainPresentDelegate = this.ReShadeDxgiSwapChainOnPresentDetour; + dxgiSwapChainResizeBuffersDelegate = this.AsHookDxgiSwapChainResizeBuffersDetour; + break; } switch (this.dalamudConfiguration.SwapChainHookMode) @@ -846,16 +929,31 @@ internal partial class InterfaceManager : IInternalDisposableService default: { Log.Information("Hooking using bytecode..."); - this.resizeBuffersHook = Hook.FromAddress( + this.dxgiSwapChainResizeBuffersHook = Hook.FromAddress( (nint)SwapChainHelper.GameDeviceSwapChainVtbl->ResizeBuffers, - resizeBuffersDelegate); + dxgiSwapChainResizeBuffersDelegate); + Log.Information( + "Hooked IDXGISwapChain::ResizeBuffers using bytecode: {addr}", + Util.DescribeAddress(this.dxgiSwapChainResizeBuffersHook.Address)); - if (dxgiPresentDelegate is not null) + if (dxgiSwapChainPresentDelegate is not null) { - this.dxgiPresentHook = Hook.FromAddress( + this.dxgiSwapChainPresentHook = Hook.FromAddress( (nint)SwapChainHelper.GameDeviceSwapChainVtbl->Present, - dxgiPresentDelegate); - Log.Information("Hooked present using bytecode"); + dxgiSwapChainPresentDelegate); + Log.Information( + "Hooked IDXGISwapChain::Present using bytecode: {addr}", + Util.DescribeAddress(this.dxgiSwapChainPresentHook.Address)); + } + + if (reShadeDxgiSwapChainPresentDelegate is not null && pfnReShadeDxgiSwapChainPresent != 0) + { + this.reShadeDxgiSwapChainPresentHook = Hook.FromAddress( + pfnReShadeDxgiSwapChainPresent, + reShadeDxgiSwapChainPresentDelegate); + Log.Information( + "Hooked ReShade::DXGISwapChain::on_present using bytecode: {addr}", + Util.DescribeAddress(this.reShadeDxgiSwapChainPresentHook.Address)); } break; @@ -864,30 +962,38 @@ internal partial class InterfaceManager : IInternalDisposableService case SwapChainHelper.HookMode.VTable: { Log.Information("Hooking using VTable..."); - this.swapChainHook = new(SwapChainHelper.GameDeviceSwapChain); - this.resizeBuffersHook = this.swapChainHook.CreateHook( + this.dxgiSwapChainHook = new(SwapChainHelper.GameDeviceSwapChain); + this.dxgiSwapChainResizeBuffersHook = this.dxgiSwapChainHook.CreateHook( nameof(IDXGISwapChain.ResizeBuffers), - resizeBuffersDelegate); + dxgiSwapChainResizeBuffersDelegate); + Log.Information( + "Hooked IDXGISwapChain::ResizeBuffers using VTable: {addr}", + Util.DescribeAddress(this.dxgiSwapChainResizeBuffersHook.Address)); - if (dxgiPresentDelegate is not null) + if (dxgiSwapChainPresentDelegate is not null) { - this.dxgiPresentHook = this.swapChainHook.CreateHook( + this.dxgiSwapChainPresentHook = this.dxgiSwapChainHook.CreateHook( nameof(IDXGISwapChain.Present), - dxgiPresentDelegate); - Log.Information("Hooked present using VTable"); + dxgiSwapChainPresentDelegate); + Log.Information( + "Hooked IDXGISwapChain::Present using VTable: {addr}", + Util.DescribeAddress(this.dxgiSwapChainPresentHook.Address)); } + Log.Information( + "Detouring vtable at {addr}: {prev} to {new}", + Util.DescribeAddress(this.dxgiSwapChainHook.Address), + Util.DescribeAddress(this.dxgiSwapChainHook.OriginalVTableAddress), + Util.DescribeAddress(this.dxgiSwapChainHook.OverridenVTableAddress)); break; } } - Log.Information($"IDXGISwapChain::ResizeBuffers address: {Util.DescribeAddress(this.resizeBuffersHook.Address)}"); - Log.Information($"IDXGISwapChain::Present address: {Util.DescribeAddress(this.dxgiPresentHook?.Address ?? 0)}"); - this.setCursorHook.Enable(); - this.resizeBuffersHook.Enable(); - this.dxgiPresentHook?.Enable(); - this.swapChainHook?.Enable(); + this.reShadeDxgiSwapChainPresentHook?.Enable(); + this.dxgiSwapChainResizeBuffersHook.Enable(); + this.dxgiSwapChainPresentHook?.Enable(); + this.dxgiSwapChainHook?.Enable(); } private IntPtr SetCursorDetour(IntPtr hCursor) diff --git a/Dalamud/Interface/Internal/ReShadeHandling/ReShadeAddonInterface.Exports.cs b/Dalamud/Interface/Internal/ReShadeHandling/ReShadeAddonInterface.Exports.cs index b3add07e7..d8d210076 100644 --- a/Dalamud/Interface/Internal/ReShadeHandling/ReShadeAddonInterface.Exports.cs +++ b/Dalamud/Interface/Internal/ReShadeHandling/ReShadeAddonInterface.Exports.cs @@ -1,3 +1,5 @@ +using System.Collections.Generic; +using System.Collections.Immutable; using System.ComponentModel; using System.Diagnostics; using System.Diagnostics.CodeAnalysis; @@ -24,6 +26,7 @@ internal sealed unsafe partial class ReShadeAddonInterface static ReShadeAddonInterface() { + var modules = new List(); foreach (var m in Process.GetCurrentProcess().Modules.Cast()) { ExportsStruct e; @@ -33,27 +36,31 @@ internal sealed unsafe partial class ReShadeAddonInterface !GetProcAddressInto(m, nameof(e.ReShadeUnregisterEvent), &e.ReShadeUnregisterEvent)) continue; - try + modules.Add(m); + if (modules.Count == 1) { - var signerName = GetSignatureSignerNameWithoutVerification(m.FileName); - ReShadeIsSignedByReShade = signerName == "ReShade"; - Log.Information( - "ReShade DLL is signed by {signerName}. {vn}={v}", - signerName, - nameof(ReShadeIsSignedByReShade), - ReShadeIsSignedByReShade); - } - catch (Exception ex) - { - Log.Information(ex, "ReShade DLL did not had a valid signature."); - } + try + { + var signerName = GetSignatureSignerNameWithoutVerification(m.FileName); + ReShadeIsSignedByReShade = signerName == "ReShade"; + Log.Information( + "ReShade DLL is signed by {signerName}. {vn}={v}", + signerName, + nameof(ReShadeIsSignedByReShade), + ReShadeIsSignedByReShade); + } + catch (Exception ex) + { + Log.Information(ex, "ReShade DLL did not had a valid signature."); + } - ReShadeModule = m; - Exports = e; - - return; + ReShadeModule = m; + Exports = e; + } } + AllReShadeModules = [..modules]; + return; bool GetProcAddressInto(ProcessModule m, ReadOnlySpan name, void* res) @@ -68,10 +75,30 @@ internal sealed unsafe partial class ReShadeAddonInterface /// Gets the active ReShade module. public static ProcessModule? ReShadeModule { get; private set; } + /// Gets all the detected ReShade modules. + public static ImmutableArray AllReShadeModules { get; private set; } + /// Gets a value indicating whether the loaded ReShade has signatures. /// ReShade without addon support is signed, but may not pass signature verification. public static bool ReShadeIsSignedByReShade { get; private set; } + /// Finds the address of DXGISwapChain::on_present in . + /// Address of the function, or 0 if not found. + public static nint FindReShadeDxgiSwapChainOnPresent() + { + if (ReShadeModule is not { } rsm) + return 0; + + var m = new ReadOnlySpan((void*)rsm.BaseAddress, rsm.ModuleMemorySize); + + // Signature validated against 5.0.0 to 6.2.0 + var i = m.IndexOf(new byte[] { 0xCC, 0xF6, 0xC2, 0x01, 0x0F, 0x85 }); + if (i == -1) + return 0; + + return rsm.BaseAddress + i + 1; + } + /// Gets the name of the signer of a file that has a certificate embedded within, without verifying if the /// file has a valid signature. /// Path to the file. diff --git a/Dalamud/Interface/Internal/ReShadeHandling/ReShadeHandlingMode.cs b/Dalamud/Interface/Internal/ReShadeHandling/ReShadeHandlingMode.cs index e4561ca46..6ffba3878 100644 --- a/Dalamud/Interface/Internal/ReShadeHandling/ReShadeHandlingMode.cs +++ b/Dalamud/Interface/Internal/ReShadeHandling/ReShadeHandlingMode.cs @@ -3,11 +3,18 @@ namespace Dalamud.Interface.Internal.ReShadeHandling; /// Available handling modes for working with ReShade. internal enum ReShadeHandlingMode { + /// Use the default method, whatever it is for the current Dalamud version. + Default = 0, + + /// Unwrap ReShade from the swap chain obtained from the game. + UnwrapReShade, + /// Register as a ReShade addon, and draw on reshade_overlay event. ReShadeAddon, - /// Unwraps ReShade from the swap chain obtained from the game. - UnwrapReShade, + /// Hook DXGISwapChain::on_present(UINT flags, const DXGI_PRESENT_PARAMETERS *params) in + /// dxgi_swapchain.cpp. + HookReShadeDxgiSwapChainOnPresent, /// Do not do anything special about it. ReShade will process Dalamud rendered stuff. None = -1, diff --git a/Dalamud/Interface/Internal/Windows/Settings/Tabs/SettingsTabExperimental.cs b/Dalamud/Interface/Internal/Windows/Settings/Tabs/SettingsTabExperimental.cs index 3f6ec783e..6da89004b 100644 --- a/Dalamud/Interface/Internal/Windows/Settings/Tabs/SettingsTabExperimental.cs +++ b/Dalamud/Interface/Internal/Windows/Settings/Tabs/SettingsTabExperimental.cs @@ -79,16 +79,28 @@ public class SettingsTabExperimental : SettingsTab "You may try different options to work around problems you may encounter.\nRestart is required for changes to take effect."), c => c.ReShadeHandlingMode, (v, c) => c.ReShadeHandlingMode = v, - fallbackValue: ReShadeHandlingMode.ReShadeAddon) + fallbackValue: ReShadeHandlingMode.ReShadeAddon, + warning: static rshm => + rshm is ReShadeHandlingMode.UnwrapReShade or ReShadeHandlingMode.None + ? null + : Loc.Localize( + "DalamudSettingsReShadeHandlingModeIgnoredVTableHookMode", + "Current option will be ignored and no special ReShade handling will be done, because SwapChain vtable hook mode is set.")) { FriendlyEnumNameGetter = x => x switch { - ReShadeHandlingMode.ReShadeAddon => Loc.Localize( - "DalamudSettingsReShadeHandlingModeReShadeAddon", - "ReShade addon"), + ReShadeHandlingMode.Default => Loc.Localize( + "DalamudSettingsReShadeHandlingModeDefault", + "Default"), ReShadeHandlingMode.UnwrapReShade => Loc.Localize( "DalamudSettingsReShadeHandlingModeUnwrapReShade", "Unwrap ReShade"), + ReShadeHandlingMode.ReShadeAddon => Loc.Localize( + "DalamudSettingsReShadeHandlingModeReShadeAddon", + "ReShade addon"), + ReShadeHandlingMode.HookReShadeDxgiSwapChainOnPresent => Loc.Localize( + "DalamudSettingsReShadeHandlingModeHookReShadeDxgiSwapChainOnPresent", + "Hook ReShade DXGISwapChain::OnPresent"), ReShadeHandlingMode.None => Loc.Localize( "DalamudSettingsReShadeHandlingModeNone", "Do not handle"), @@ -96,12 +108,18 @@ public class SettingsTabExperimental : SettingsTab }, FriendlyEnumDescriptionGetter = x => x switch { - ReShadeHandlingMode.ReShadeAddon => Loc.Localize( - "DalamudSettingsReShadeHandlingModeReShadeAddonDescription", - "Dalamud will register itself as a ReShade addon. Most compatibility is expected, but multi-monitor window option will require reloading ReShade every time a new window is opened, or even may not work at all."), + ReShadeHandlingMode.Default => Loc.Localize( + "DalamudSettingsReShadeHandlingModeDefaultDescription", + "Dalamud will use the developer-recommend settings. If nothing's wrong, keeping this option is recommended."), ReShadeHandlingMode.UnwrapReShade => Loc.Localize( "DalamudSettingsReShadeHandlingModeUnwrapReShadeDescription", "Dalamud will exclude itself from all ReShade handling. Multi-monitor windows should work fine with this mode, but it may not be supported and crash in future ReShade versions."), + ReShadeHandlingMode.ReShadeAddon => Loc.Localize( + "DalamudSettingsReShadeHandlingModeReShadeAddonDescription", + "Dalamud will register itself as a ReShade addon. Multi-monitor window option will require reloading ReShade every time a new window is opened, or even may not work at all."), + ReShadeHandlingMode.HookReShadeDxgiSwapChainOnPresent => Loc.Localize( + "DalamudSettingsReShadeHandlingModeHookReShadeDxgiSwapChainOnPresentDescription", + "Dalamud will use an unsupported method of detouring an internal ReShade function. Multi-monitor window option will require reloading ReShade every time a new window is opened, or even may not work at all."), ReShadeHandlingMode.None => Loc.Localize( "DalamudSettingsReShadeHandlingModeNoneDescription", "No special handling will be done for ReShade. Dalamud will be under the effect of ReShade postprocessing."),