add back DXGISwapChain::on_present hook as default for now

This commit is contained in:
Soreepeong 2024-07-25 18:05:21 +09:00
parent bd170ee74a
commit ebee2f151e
7 changed files with 379 additions and 172 deletions

View file

@ -54,6 +54,15 @@ internal unsafe class ObjectVTableHook : IDisposable
/// <summary>Gets the span view of overriden vtable.</summary> /// <summary>Gets the span view of overriden vtable.</summary>
public ReadOnlySpan<nint> OverridenVTableSpan => this.vtblOverriden.AsSpan(); public ReadOnlySpan<nint> OverridenVTableSpan => this.vtblOverriden.AsSpan();
/// <summary>Gets the address of the pointer to the vtable.</summary>
public nint Address => (nint)this.ppVtbl;
/// <summary>Gets the address of the original vtable.</summary>
public nint OriginalVTableAddress => (nint)this.pVtblOriginal;
/// <summary>Gets the address of the overriden vtable.</summary>
public nint OverridenVTableAddress => (nint)Unsafe.AsPointer(ref this.vtblOverriden[0]);
/// <summary>Disables the hook.</summary> /// <summary>Disables the hook.</summary>
public void Disable() public void Disable()
{ {

View file

@ -1,75 +1,142 @@
using System.Diagnostics; using System.Diagnostics;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using Dalamud.Utility; using Dalamud.Utility;
using TerraFX.Interop.DirectX;
using TerraFX.Interop.Windows;
namespace Dalamud.Interface.Internal; namespace Dalamud.Interface.Internal;
/// <summary> /// <summary>
/// This class manages interaction with the ImGui interface. /// This class manages interaction with the ImGui interface.
/// </summary> /// </summary>
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.
/// <summary>Delegate for <c>DXGISwapChain::on_present(UINT flags, const DXGI_PRESENT_PARAMETERS *params)</c> in
/// <c>dxgi_swapchain.cpp</c>.</summary>
/// <param name="swapChain">Pointer to an instance of <c>DXGISwapChain</c>, which happens to be an
/// <see cref="IDXGISwapChain"/>.</param>
/// <param name="flags">An integer value that contains swap-chain presentation options. These options are defined by
/// the <c>DXGI_PRESENT</c> constants.</param>
/// <param name="presentParams">Optional; DXGI present parameters.</param>
[UnmanagedFunctionPointer(CallingConvention.ThisCall)]
private delegate void ReShadeDxgiSwapChainPresentDelegate(
ReShadeDxgiSwapChain* swapChain,
uint flags,
DXGI_PRESENT_PARAMETERS* presentParams);
/// <summary>Delegate for <see cref="IDXGISwapChain.Present"/>.
/// <a href="https://learn.microsoft.com/en-us/windows/win32/api/dxgi/nf-dxgi-idxgiswapchain-present">Microsoft
/// Learn</a>.</summary>
/// <param name="swapChain">Pointer to an instance of <see cref="IDXGISwapChain"/>.</param>
/// <param name="syncInterval">An integer that specifies how to synchronize presentation of a frame with the
/// vertical blank.</param>
/// <param name="flags">An integer value that contains swap-chain presentation options. These options are defined by
/// the <c>DXGI_PRESENT</c> constants.</param>
/// <returns>A <see cref="HRESULT"/> representing the result of the operation.</returns>
[UnmanagedFunctionPointer(CallingConvention.ThisCall)]
private delegate int DxgiSwapChainPresentDelegate(IDXGISwapChain* swapChain, uint syncInterval, uint flags);
/// <summary>Detour function for <see cref="IDXGISwapChain.ResizeBuffers"/>.
/// <a href="https://learn.microsoft.com/en-us/windows/win32/api/dxgi/nf-dxgi-idxgiswapchain-resizebuffers">
/// Microsoft Learn</a>.</summary>
/// <param name="swapChain">Pointer to an instance of <see cref="IDXGISwapChain"/>.</param>
/// <param name="bufferCount">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 <see cref="DXGI.DXGI_MAX_SWAP_CHAIN_BUFFERS"/>. 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.</param>
/// <param name="width">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
/// <see cref="IDXGIFactory2.CreateSwapChainForComposition"/> method to create the swap chain for a composition
/// surface.</param>
/// <param name="height">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
/// <see cref="IDXGIFactory2.CreateSwapChainForComposition"/> method to create the swap chain for a composition
/// surface.</param>
/// <param name="newFormat">A DXGI_FORMAT-typed value for the new format of the back buffer. Set this value to
/// <see cref="DXGI_FORMAT.DXGI_FORMAT_UNKNOWN"/> 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.</param>
/// <param name="swapChainFlags">A combination of <see cref="DXGI_SWAP_CHAIN_FLAG"/>-typed values that are combined
/// by using a bitwise OR operation. The resulting value specifies options for swap-chain behavior.</param>
/// <returns>A <see cref="HRESULT"/> representing the result of the operation.</returns>
[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)) Debug.Assert(
return this.dxgiPresentHook!.Original(swapChain, syncInterval, presentFlags); 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?"); // Call this first to draw Dalamud over ReShade.
Debug.Assert(this.dalamudAtlas is not null, "dalamudAtlas should have been set already"); this.reShadeDxgiSwapChainPresentHook!.Original(swapChain, flags, presentParams);
if (this.scene == null) if (this.RenderDalamudCheckAndInitialize(swapChain->AsIDxgiSwapChain()) is { } activeScene)
this.InitScene(swapChain); this.RenderDalamudDraw(activeScene);
Debug.Assert(this.scene is not null, "InitScene did not set the scene field, but did not throw an exception."); // Upstream call to system IDXGISwapChain::Present will be called by ReShade.
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);
} }
private IntPtr AsHookResizeBuffersDetour( private int DxgiSwapChainPresentDetour(IDXGISwapChain* swapChain, uint syncInterval, uint flags)
IntPtr swapChain, uint bufferCount, uint width, uint height, uint newFormat, uint swapChainFlags) {
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)) 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 #if DEBUG
Log.Verbose( 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 #endif
this.ResizeBuffers?.InvokeSafely(); this.ResizeBuffers?.InvokeSafely();
this.scene?.OnPreResize(); this.scene?.OnPreResize();
var ret = this.resizeBuffersHook!.Original(swapChain, bufferCount, width, height, newFormat, swapChainFlags); var ret = this.dxgiSwapChainResizeBuffersHook!.Original(swapChain, bufferCount, width, height, newFormat, swapChainFlags);
if (ret.ToInt64() == 0x887A0001) if (ret == DXGI.DXGI_ERROR_INVALID_CALL)
{
Log.Error("invalid call to resizeBuffers"); Log.Error("invalid call to resizeBuffers");
}
this.scene?.OnPostResize((int)width, (int)height); this.scene?.OnPostResize((int)width, (int)height);
return ret; return ret;
} }
/// <summary>Represents <c>DXGISwapChain</c> in ReShade.</summary>
[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);
}
} }

View file

@ -11,9 +11,9 @@ namespace Dalamud.Interface.Internal;
/// <summary> /// <summary>
/// This class manages interaction with the ImGui interface. /// This class manages interaction with the ImGui interface.
/// </summary> /// </summary>
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<IDXGISwapChain>(); var swapChainNative = swapChain.GetNative<IDXGISwapChain>();
if (this.scene?.SwapChain.NativePointer != (nint)swapChainNative) if (this.scene?.SwapChain.NativePointer != (nint)swapChainNative)
@ -22,7 +22,7 @@ internal partial class InterfaceManager
this.scene?.OnPreResize(); this.scene?.OnPreResize();
} }
private unsafe void ReShadeAddonInterfaceOnInitSwapChain(ref ReShadeAddonInterface.ApiObject swapChain) private void ReShadeAddonInterfaceOnInitSwapChain(ref ReShadeAddonInterface.ApiObject swapChain)
{ {
var swapChainNative = swapChain.GetNative<IDXGISwapChain>(); var swapChainNative = swapChain.GetNative<IDXGISwapChain>();
if (this.scene?.SwapChain.NativePointer != (nint)swapChainNative) if (this.scene?.SwapChain.NativePointer != (nint)swapChainNative)
@ -42,52 +42,25 @@ internal partial class InterfaceManager
ReadOnlySpan<RECT> destRect, ReadOnlySpan<RECT> destRect,
ReadOnlySpan<RECT> dirtyRects) ReadOnlySpan<RECT> dirtyRects)
{ {
var swapChainNative = swapChain.GetNative(); var swapChainNative = swapChain.GetNative<IDXGISwapChain>();
if (this.scene == null) if (this.RenderDalamudCheckAndInitialize(swapChainNative) is { } activeScene)
this.InitScene(swapChainNative); this.RenderDalamudDraw(activeScene);
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;
} }
private nint AsReShadeAddonResizeBuffersDetour( private int AsReShadeAddonDxgiSwapChainResizeBuffersDetour(
nint swapChain, IDXGISwapChain* swapChain,
uint bufferCount, uint bufferCount,
uint width, uint width,
uint height, uint height,
uint newFormat, DXGI_FORMAT newFormat,
uint swapChainFlags) uint swapChainFlags)
{ {
// Hooked vtbl instead of registering ReShade event. This check is correct. // Hooked vtbl instead of registering ReShade event. This check is correct.
if (!SwapChainHelper.IsGameDeviceSwapChain(swapChain)) 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(); this.ResizeBuffers?.InvokeSafely();
return this.resizeBuffersHook!.Original(swapChain, bufferCount, width, height, newFormat, swapChainFlags); return this.dxgiSwapChainResizeBuffersHook!.Original(swapChain, bufferCount, width, height, newFormat, swapChainFlags);
} }
} }

View file

@ -3,8 +3,8 @@ using System.Collections.Generic;
using System.Diagnostics; using System.Diagnostics;
using System.IO; using System.IO;
using System.Linq; using System.Linq;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices; using System.Runtime.InteropServices;
using System.Text;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
@ -97,9 +97,10 @@ internal partial class InterfaceManager : IInternalDisposableService
private RawDX11Scene? scene; private RawDX11Scene? scene;
private Hook<SetCursorDelegate>? setCursorHook; private Hook<SetCursorDelegate>? setCursorHook;
private Hook<DxgiPresentDelegate>? dxgiPresentHook; private Hook<ReShadeDxgiSwapChainPresentDelegate>? reShadeDxgiSwapChainPresentHook;
private Hook<ResizeBuffersDelegate>? resizeBuffersHook; private Hook<DxgiSwapChainPresentDelegate>? dxgiSwapChainPresentHook;
private ObjectVTableHook<IDXGISwapChain.Vtbl<IDXGISwapChain>>? swapChainHook; private Hook<ResizeBuffersDelegate>? dxgiSwapChainResizeBuffersHook;
private ObjectVTableHook<IDXGISwapChain4.Vtbl<IDXGISwapChain4>>? dxgiSwapChainHook;
private ReShadeAddonInterface? reShadeAddonInterface; private ReShadeAddonInterface? reShadeAddonInterface;
private IFontAtlas? dalamudAtlas; 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)] [UnmanagedFunctionPointer(CallingConvention.StdCall)]
private delegate IntPtr SetCursorDelegate(IntPtr hCursor); private delegate IntPtr SetCursorDelegate(IntPtr hCursor);
@ -231,7 +226,7 @@ internal partial class InterfaceManager : IInternalDisposableService
/// </summary> /// </summary>
public bool IsDispatchingEvents { get; set; } = true; public bool IsDispatchingEvents { get; set; } = true;
/// <summary>Gets a value indicating whether the main thread is executing <see cref="PresentDetour"/>.</summary> /// <summary>Gets a value indicating whether the main thread is executing <see cref="DxgiSwapChainPresentDetour"/>.</summary>
/// <remarks>This still will be <c>true</c> even when queried off the main thread.</remarks> /// <remarks>This still will be <c>true</c> even when queried off the main thread.</remarks>
public bool IsMainThreadInPresent { get; private set; } public bool IsMainThreadInPresent { get; private set; }
@ -265,7 +260,7 @@ internal partial class InterfaceManager : IInternalDisposableService
/// </summary> /// </summary>
public Task FontBuildTask => WhenFontsReady().dalamudAtlas!.BuildTask; public Task FontBuildTask => WhenFontsReady().dalamudAtlas!.BuildTask;
/// <summary>Gets the number of calls to <see cref="PresentDetour"/> so far.</summary> /// <summary>Gets the number of calls to <see cref="DxgiSwapChainPresentDetour"/> so far.</summary>
/// <remarks> /// <remarks>
/// The value increases even when Dalamud is hidden via &quot;/xlui hide&quot;. /// The value increases even when Dalamud is hidden via &quot;/xlui hide&quot;.
/// <see cref="DalamudInterface.FrameCount"/> does not. /// <see cref="DalamudInterface.FrameCount"/> does not.
@ -312,9 +307,10 @@ internal partial class InterfaceManager : IInternalDisposableService
{ {
this.wndProcHookManager.PreWndProc -= this.WndProcHookManagerOnPreWndProc; this.wndProcHookManager.PreWndProc -= this.WndProcHookManagerOnPreWndProc;
Interlocked.Exchange(ref this.setCursorHook, null)?.Dispose(); Interlocked.Exchange(ref this.setCursorHook, null)?.Dispose();
Interlocked.Exchange(ref this.dxgiPresentHook, null)?.Dispose(); Interlocked.Exchange(ref this.dxgiSwapChainPresentHook, null)?.Dispose();
Interlocked.Exchange(ref this.resizeBuffersHook, null)?.Dispose(); Interlocked.Exchange(ref this.reShadeDxgiSwapChainPresentHook, null)?.Dispose();
Interlocked.Exchange(ref this.swapChainHook, null)?.Dispose(); Interlocked.Exchange(ref this.dxgiSwapChainResizeBuffersHook, null)?.Dispose();
Interlocked.Exchange(ref this.dxgiSwapChainHook, null)?.Dispose();
Interlocked.Exchange(ref this.reShadeAddonInterface, null)?.Dispose(); Interlocked.Exchange(ref this.reShadeAddonInterface, null)?.Dispose();
} }
} }
@ -497,31 +493,72 @@ internal partial class InterfaceManager : IInternalDisposableService
return im; return im;
} }
[MethodImpl(MethodImplOptions.AggressiveInlining)] /// <summary>Checks if the provided swap chain is the target that Dalamud should draw its interface onto,
private static void RenderImGui(RawDX11Scene scene) /// and initializes ImGui for drawing.</summary>
/// <param name="swapChain">The swap chain to test and initialize ImGui with if conditions are met.</param>
/// <returns>An initialized instance of <see cref="RawDX11Scene"/>, or <c>null</c> if <paramref name="swapChain"/>
/// is not the main swap chain.</returns>
private unsafe RawDX11Scene? RenderDalamudCheckAndInitialize(IDXGISwapChain* swapChain)
{ {
var conf = Service<DalamudConfiguration>.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;
}
/// <summary>Draws Dalamud to the given scene representing the ImGui context.</summary>
/// <param name="activeScene">The scene to draw to.</param>
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. // Process information needed by ImGuiHelpers each frame.
ImGuiHelpers.NewFrame(); ImGuiHelpers.NewFrame();
// Enable viewports if there are no issues. // 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; ImGui.GetIO().ConfigFlags &= ~ImGuiConfigFlags.ViewportsEnable;
else else
ImGui.GetIO().ConfigFlags |= ImGuiConfigFlags.ViewportsEnable; 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; RawDX11Scene newScene;
using (Timings.Start("IM Scene Init")) using (Timings.Start("IM Scene Init"))
{ {
try try
{ {
newScene = new RawDX11Scene(swapChain); newScene = new RawDX11Scene((nint)swapChain);
} }
catch (DllNotFoundException ex) catch (DllNotFoundException ex)
{ {
@ -547,7 +584,7 @@ internal partial class InterfaceManager : IInternalDisposableService
Environment.Exit(-1); Environment.Exit(-1);
// Doesn't reach here, but to make the compiler not complain // Doesn't reach here, but to make the compiler not complain
return; throw new InvalidOperationException();
} }
var startInfo = Service<Dalamud>.Get().StartInfo; var startInfo = Service<Dalamud>.Get().StartInfo;
@ -638,6 +675,7 @@ internal partial class InterfaceManager : IInternalDisposableService
Service<InterfaceManagerWithScene>.Provide(new(this)); Service<InterfaceManagerWithScene>.Provide(new(this));
this.wndProcHookManager.PreWndProc += this.WndProcHookManagerOnPreWndProc; this.wndProcHookManager.PreWndProc += this.WndProcHookManagerOnPreWndProc;
return newScene;
} }
private unsafe void WndProcHookManagerOnPreWndProc(WndProcEventArgs args) 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 ====="); Log.Information("===== S W A P C H A I N =====");
if (this.dalamudConfiguration.ReShadeHandlingMode == ReShadeHandlingMode.UnwrapReShade) var sb = new StringBuilder();
foreach (var m in ReShadeAddonInterface.AllReShadeModules)
{ {
if (SwapChainHelper.UnwrapReShade()) sb.Clear();
Log.Information("Unwrapped ReShade"); 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; if (ReShadeAddonInterface.AllReShadeModules.Length > 1)
DxgiPresentDelegate? dxgiPresentDelegate = null; Log.Warning("Multiple ReShade dlls are detected.");
if (this.dalamudConfiguration.ReShadeHandlingMode == ReShadeHandlingMode.ReShadeAddon)
{
if (ReShadeAddonInterface.TryRegisterAddon(out this.reShadeAddonInterface))
{
resizeBuffersDelegate = this.AsReShadeAddonResizeBuffersDetour;
Log.Information( ResizeBuffersDelegate dxgiSwapChainResizeBuffersDelegate;
"Registered as a ReShade({Name}: 0x{Addr:X}) addon", ReShadeDxgiSwapChainPresentDelegate? reShadeDxgiSwapChainPresentDelegate = null;
ReShadeAddonInterface.ReShadeModule!.FileName, DxgiSwapChainPresentDelegate? dxgiSwapChainPresentDelegate = null;
ReShadeAddonInterface.ReShadeModule!.BaseAddress); 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.InitSwapChain += this.ReShadeAddonInterfaceOnInitSwapChain;
this.reShadeAddonInterface.DestroySwapChain += this.ReShadeAddonInterfaceOnDestroySwapChain; this.reShadeAddonInterface.DestroySwapChain += this.ReShadeAddonInterfaceOnDestroySwapChain;
this.reShadeAddonInterface.Present += this.ReShadeAddonInterfaceOnPresent; this.reShadeAddonInterface.Present += this.ReShadeAddonInterfaceOnPresent;
}
else
{
Log.Information("Could not register as ReShade addon");
}
}
if (resizeBuffersDelegate is null) dxgiSwapChainResizeBuffersDelegate = this.AsReShadeAddonDxgiSwapChainResizeBuffersDetour;
{ break;
resizeBuffersDelegate = this.AsHookResizeBuffersDetour;
dxgiPresentDelegate = this.PresentDetour; // 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) switch (this.dalamudConfiguration.SwapChainHookMode)
@ -846,16 +929,31 @@ internal partial class InterfaceManager : IInternalDisposableService
default: default:
{ {
Log.Information("Hooking using bytecode..."); Log.Information("Hooking using bytecode...");
this.resizeBuffersHook = Hook<ResizeBuffersDelegate>.FromAddress( this.dxgiSwapChainResizeBuffersHook = Hook<ResizeBuffersDelegate>.FromAddress(
(nint)SwapChainHelper.GameDeviceSwapChainVtbl->ResizeBuffers, (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<DxgiPresentDelegate>.FromAddress( this.dxgiSwapChainPresentHook = Hook<DxgiSwapChainPresentDelegate>.FromAddress(
(nint)SwapChainHelper.GameDeviceSwapChainVtbl->Present, (nint)SwapChainHelper.GameDeviceSwapChainVtbl->Present,
dxgiPresentDelegate); dxgiSwapChainPresentDelegate);
Log.Information("Hooked present using bytecode"); Log.Information(
"Hooked IDXGISwapChain::Present using bytecode: {addr}",
Util.DescribeAddress(this.dxgiSwapChainPresentHook.Address));
}
if (reShadeDxgiSwapChainPresentDelegate is not null && pfnReShadeDxgiSwapChainPresent != 0)
{
this.reShadeDxgiSwapChainPresentHook = Hook<ReShadeDxgiSwapChainPresentDelegate>.FromAddress(
pfnReShadeDxgiSwapChainPresent,
reShadeDxgiSwapChainPresentDelegate);
Log.Information(
"Hooked ReShade::DXGISwapChain::on_present using bytecode: {addr}",
Util.DescribeAddress(this.reShadeDxgiSwapChainPresentHook.Address));
} }
break; break;
@ -864,30 +962,38 @@ internal partial class InterfaceManager : IInternalDisposableService
case SwapChainHelper.HookMode.VTable: case SwapChainHelper.HookMode.VTable:
{ {
Log.Information("Hooking using VTable..."); Log.Information("Hooking using VTable...");
this.swapChainHook = new(SwapChainHelper.GameDeviceSwapChain); this.dxgiSwapChainHook = new(SwapChainHelper.GameDeviceSwapChain);
this.resizeBuffersHook = this.swapChainHook.CreateHook( this.dxgiSwapChainResizeBuffersHook = this.dxgiSwapChainHook.CreateHook(
nameof(IDXGISwapChain.ResizeBuffers), 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), nameof(IDXGISwapChain.Present),
dxgiPresentDelegate); dxgiSwapChainPresentDelegate);
Log.Information("Hooked present using VTable"); 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; 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.setCursorHook.Enable();
this.resizeBuffersHook.Enable(); this.reShadeDxgiSwapChainPresentHook?.Enable();
this.dxgiPresentHook?.Enable(); this.dxgiSwapChainResizeBuffersHook.Enable();
this.swapChainHook?.Enable(); this.dxgiSwapChainPresentHook?.Enable();
this.dxgiSwapChainHook?.Enable();
} }
private IntPtr SetCursorDetour(IntPtr hCursor) private IntPtr SetCursorDetour(IntPtr hCursor)

View file

@ -1,3 +1,5 @@
using System.Collections.Generic;
using System.Collections.Immutable;
using System.ComponentModel; using System.ComponentModel;
using System.Diagnostics; using System.Diagnostics;
using System.Diagnostics.CodeAnalysis; using System.Diagnostics.CodeAnalysis;
@ -24,6 +26,7 @@ internal sealed unsafe partial class ReShadeAddonInterface
static ReShadeAddonInterface() static ReShadeAddonInterface()
{ {
var modules = new List<ProcessModule>();
foreach (var m in Process.GetCurrentProcess().Modules.Cast<ProcessModule>()) foreach (var m in Process.GetCurrentProcess().Modules.Cast<ProcessModule>())
{ {
ExportsStruct e; ExportsStruct e;
@ -33,27 +36,31 @@ internal sealed unsafe partial class ReShadeAddonInterface
!GetProcAddressInto(m, nameof(e.ReShadeUnregisterEvent), &e.ReShadeUnregisterEvent)) !GetProcAddressInto(m, nameof(e.ReShadeUnregisterEvent), &e.ReShadeUnregisterEvent))
continue; continue;
try modules.Add(m);
if (modules.Count == 1)
{ {
var signerName = GetSignatureSignerNameWithoutVerification(m.FileName); try
ReShadeIsSignedByReShade = signerName == "ReShade"; {
Log.Information( var signerName = GetSignatureSignerNameWithoutVerification(m.FileName);
"ReShade DLL is signed by {signerName}. {vn}={v}", ReShadeIsSignedByReShade = signerName == "ReShade";
signerName, Log.Information(
nameof(ReShadeIsSignedByReShade), "ReShade DLL is signed by {signerName}. {vn}={v}",
ReShadeIsSignedByReShade); signerName,
} nameof(ReShadeIsSignedByReShade),
catch (Exception ex) ReShadeIsSignedByReShade);
{ }
Log.Information(ex, "ReShade DLL did not had a valid signature."); catch (Exception ex)
} {
Log.Information(ex, "ReShade DLL did not had a valid signature.");
}
ReShadeModule = m; ReShadeModule = m;
Exports = e; Exports = e;
}
return;
} }
AllReShadeModules = [..modules];
return; return;
bool GetProcAddressInto(ProcessModule m, ReadOnlySpan<char> name, void* res) bool GetProcAddressInto(ProcessModule m, ReadOnlySpan<char> name, void* res)
@ -68,10 +75,30 @@ internal sealed unsafe partial class ReShadeAddonInterface
/// <summary>Gets the active ReShade module.</summary> /// <summary>Gets the active ReShade module.</summary>
public static ProcessModule? ReShadeModule { get; private set; } public static ProcessModule? ReShadeModule { get; private set; }
/// <summary>Gets all the detected ReShade modules.</summary>
public static ImmutableArray<ProcessModule> AllReShadeModules { get; private set; }
/// <summary>Gets a value indicating whether the loaded ReShade has signatures.</summary> /// <summary>Gets a value indicating whether the loaded ReShade has signatures.</summary>
/// <remarks>ReShade without addon support is signed, but may not pass signature verification.</remarks> /// <remarks>ReShade without addon support is signed, but may not pass signature verification.</remarks>
public static bool ReShadeIsSignedByReShade { get; private set; } public static bool ReShadeIsSignedByReShade { get; private set; }
/// <summary>Finds the address of <c>DXGISwapChain::on_present</c> in <see cref="ReShadeModule"/>.</summary>
/// <returns>Address of the function, or <c>0</c> if not found.</returns>
public static nint FindReShadeDxgiSwapChainOnPresent()
{
if (ReShadeModule is not { } rsm)
return 0;
var m = new ReadOnlySpan<byte>((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;
}
/// <summary>Gets the name of the signer of a file that has a certificate embedded within, without verifying if the /// <summary>Gets the name of the signer of a file that has a certificate embedded within, without verifying if the
/// file has a valid signature.</summary> /// file has a valid signature.</summary>
/// <param name="path">Path to the file.</param> /// <param name="path">Path to the file.</param>

View file

@ -3,11 +3,18 @@ namespace Dalamud.Interface.Internal.ReShadeHandling;
/// <summary>Available handling modes for working with ReShade.</summary> /// <summary>Available handling modes for working with ReShade.</summary>
internal enum ReShadeHandlingMode internal enum ReShadeHandlingMode
{ {
/// <summary>Use the default method, whatever it is for the current Dalamud version.</summary>
Default = 0,
/// <summary>Unwrap ReShade from the swap chain obtained from the game.</summary>
UnwrapReShade,
/// <summary>Register as a ReShade addon, and draw on reshade_overlay event.</summary> /// <summary>Register as a ReShade addon, and draw on reshade_overlay event.</summary>
ReShadeAddon, ReShadeAddon,
/// <summary>Unwraps ReShade from the swap chain obtained from the game.</summary> /// <summary>Hook <c>DXGISwapChain::on_present(UINT flags, const DXGI_PRESENT_PARAMETERS *params)</c> in
UnwrapReShade, /// <c>dxgi_swapchain.cpp</c>.</summary>
HookReShadeDxgiSwapChainOnPresent,
/// <summary>Do not do anything special about it. ReShade will process Dalamud rendered stuff.</summary> /// <summary>Do not do anything special about it. ReShade will process Dalamud rendered stuff.</summary>
None = -1, None = -1,

View file

@ -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."), "You may try different options to work around problems you may encounter.\nRestart is required for changes to take effect."),
c => c.ReShadeHandlingMode, c => c.ReShadeHandlingMode,
(v, c) => c.ReShadeHandlingMode = v, (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 FriendlyEnumNameGetter = x => x switch
{ {
ReShadeHandlingMode.ReShadeAddon => Loc.Localize( ReShadeHandlingMode.Default => Loc.Localize(
"DalamudSettingsReShadeHandlingModeReShadeAddon", "DalamudSettingsReShadeHandlingModeDefault",
"ReShade addon"), "Default"),
ReShadeHandlingMode.UnwrapReShade => Loc.Localize( ReShadeHandlingMode.UnwrapReShade => Loc.Localize(
"DalamudSettingsReShadeHandlingModeUnwrapReShade", "DalamudSettingsReShadeHandlingModeUnwrapReShade",
"Unwrap ReShade"), "Unwrap ReShade"),
ReShadeHandlingMode.ReShadeAddon => Loc.Localize(
"DalamudSettingsReShadeHandlingModeReShadeAddon",
"ReShade addon"),
ReShadeHandlingMode.HookReShadeDxgiSwapChainOnPresent => Loc.Localize(
"DalamudSettingsReShadeHandlingModeHookReShadeDxgiSwapChainOnPresent",
"Hook ReShade DXGISwapChain::OnPresent"),
ReShadeHandlingMode.None => Loc.Localize( ReShadeHandlingMode.None => Loc.Localize(
"DalamudSettingsReShadeHandlingModeNone", "DalamudSettingsReShadeHandlingModeNone",
"Do not handle"), "Do not handle"),
@ -96,12 +108,18 @@ public class SettingsTabExperimental : SettingsTab
}, },
FriendlyEnumDescriptionGetter = x => x switch FriendlyEnumDescriptionGetter = x => x switch
{ {
ReShadeHandlingMode.ReShadeAddon => Loc.Localize( ReShadeHandlingMode.Default => Loc.Localize(
"DalamudSettingsReShadeHandlingModeReShadeAddonDescription", "DalamudSettingsReShadeHandlingModeDefaultDescription",
"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."), "Dalamud will use the developer-recommend settings. If nothing's wrong, keeping this option is recommended."),
ReShadeHandlingMode.UnwrapReShade => Loc.Localize( ReShadeHandlingMode.UnwrapReShade => Loc.Localize(
"DalamudSettingsReShadeHandlingModeUnwrapReShadeDescription", "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."), "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( ReShadeHandlingMode.None => Loc.Localize(
"DalamudSettingsReShadeHandlingModeNoneDescription", "DalamudSettingsReShadeHandlingModeNoneDescription",
"No special handling will be done for ReShade. Dalamud will be under the effect of ReShade postprocessing."), "No special handling will be done for ReShade. Dalamud will be under the effect of ReShade postprocessing."),