Implement ReShade addon interface

This commit is contained in:
Soreepeong 2024-07-22 05:42:18 +09:00
parent 42c728ec6c
commit 1f315be94e
7 changed files with 2140 additions and 244 deletions

View file

@ -0,0 +1,75 @@
using System.Diagnostics;
using Dalamud.Utility;
namespace Dalamud.Interface.Internal;
/// <summary>
/// This class manages interaction with the ImGui interface.
/// </summary>
internal partial class InterfaceManager
{
private IntPtr PresentDetour(IntPtr swapChain, uint syncInterval, uint presentFlags)
{
if (!SwapChainHelper.IsGameDeviceSwapChain(swapChain))
return this.dxgiPresentHook!.Original(swapChain, syncInterval, presentFlags);
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");
if (this.scene == null)
this.InitScene(swapChain);
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);
}
private IntPtr AsHookResizeBuffersDetour(
IntPtr swapChain, uint bufferCount, uint width, uint height, uint newFormat, uint swapChainFlags)
{
if (!SwapChainHelper.IsGameDeviceSwapChain(swapChain))
return this.resizeBuffersHook!.Original(swapChain, bufferCount, width, height, newFormat, swapChainFlags);
#if DEBUG
Log.Verbose(
$"Calling resizebuffers swap@{swapChain.ToInt64():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)
{
Log.Error("invalid call to resizeBuffers");
}
this.scene?.OnPostResize((int)width, (int)height);
return ret;
}
}

View file

@ -0,0 +1,86 @@
using System.Diagnostics;
using Dalamud.Utility;
using TerraFX.Interop.DirectX;
namespace Dalamud.Interface.Internal;
/// <summary>
/// This class manages interaction with the ImGui interface.
/// </summary>
internal partial class InterfaceManager
{
private unsafe void ReShadeAddonInterfaceOnDestroySwapChain(ref ReShadeAddonInterface.ApiObject swapchain)
{
var swapChain = swapchain.GetNative<IDXGISwapChain>();
if (this.scene?.SwapChain.NativePointer != (nint)swapChain)
return;
this.scene?.OnPreResize();
}
private unsafe void ReShadeAddonInterfaceOnInitSwapChain(ref ReShadeAddonInterface.ApiObject swapchain)
{
var swapChain = swapchain.GetNative<IDXGISwapChain>();
if (this.scene?.SwapChain.NativePointer != (nint)swapChain)
return;
DXGI_SWAP_CHAIN_DESC desc;
if (swapChain->GetDesc(&desc).FAILED)
return;
this.scene?.OnPostResize((int)desc.BufferDesc.Width, (int)desc.BufferDesc.Height);
}
private void ReShadeAddonInterfaceOnReShadeOverlay(ref ReShadeAddonInterface.ApiObject runtime)
{
var swapChain = runtime.GetNative();
if (this.scene == null)
this.InitScene(swapChain);
if (this.scene?.SwapChain.NativePointer != swapChain)
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(
nint swapChain,
uint bufferCount,
uint width,
uint height,
uint 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);
this.ResizeBuffers?.InvokeSafely();
return this.resizeBuffersHook!.Original(swapChain, bufferCount, width, height, newFormat, swapChainFlags);
}
}

View file

@ -13,6 +13,7 @@ using Dalamud.Game;
using Dalamud.Game.ClientState.GamePad;
using Dalamud.Game.ClientState.Keys;
using Dalamud.Hooking;
using Dalamud.Hooking.Internal;
using Dalamud.Hooking.WndProcHook;
using Dalamud.Interface.ImGuiNotification.Internal;
using Dalamud.Interface.Internal.ManagedAsserts;
@ -29,10 +30,11 @@ using ImGuiNET;
using ImGuiScene;
using JetBrains.Annotations;
using PInvoke;
using SharpDX;
using SharpDX.DXGI;
using TerraFX.Interop.Windows;
// general dev notes, here because it's easiest
@ -52,7 +54,7 @@ namespace Dalamud.Interface.Internal;
/// This class manages interaction with the ImGui interface.
/// </summary>
[ServiceManager.EarlyLoadedService]
internal class InterfaceManager : IInternalDisposableService
internal partial class InterfaceManager : IInternalDisposableService
{
/// <summary>
/// The default font size, in points.
@ -75,6 +77,11 @@ internal class InterfaceManager : IInternalDisposableService
[ServiceManager.ServiceDependency]
private readonly Framework framework = Service<Framework>.Get();
// ReShadeAddonInterface requires hooks to be alive to unregister itself.
[ServiceManager.ServiceDependency]
[UsedImplicitly]
private readonly HookManager hookManager = Service<HookManager>.Get();
private readonly ConcurrentQueue<Action> runBeforeImGuiRender = new();
private readonly ConcurrentQueue<Action> runAfterImGuiRender = new();
@ -82,8 +89,8 @@ internal class InterfaceManager : IInternalDisposableService
private Hook<SetCursorDelegate>? setCursorHook;
private Hook<DxgiPresentDelegate>? dxgiPresentHook;
private Hook<ReshadeOnPresentDelegate>? reshadeOnPresentHook;
private Hook<ResizeBuffersDelegate>? resizeBuffersHook;
private ReShadeAddonInterface? reShadeAddonInterface;
private IFontAtlas? dalamudAtlas;
private ILockedImFont? defaultFontResourceLock;
@ -101,9 +108,6 @@ internal class InterfaceManager : IInternalDisposableService
[UnmanagedFunctionPointer(CallingConvention.ThisCall)]
private delegate IntPtr DxgiPresentDelegate(IntPtr swapChain, uint syncInterval, uint presentFlags);
[UnmanagedFunctionPointer(CallingConvention.ThisCall)]
private delegate void ReshadeOnPresentDelegate(nint swapChain, uint flags, nint presentParams);
[UnmanagedFunctionPointer(CallingConvention.ThisCall)]
private delegate IntPtr ResizeBuffersDelegate(IntPtr swapChain, uint bufferCount, uint width, uint height, uint newFormat, uint swapChainFlags);
@ -299,8 +303,8 @@ internal 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.reshadeOnPresentHook, null)?.Dispose();
Interlocked.Exchange(ref this.resizeBuffersHook, null)?.Dispose();
Interlocked.Exchange(ref this.reShadeAddonInterface, null)?.Dispose();
}
}
@ -431,11 +435,11 @@ internal class InterfaceManager : IInternalDisposableService
try
{
var dxgiDev = this.Device.QueryInterfaceOrNull<SharpDX.DXGI.Device>();
var dxgiAdapter = dxgiDev?.Adapter.QueryInterfaceOrNull<Adapter4>();
var dxgiAdapter = dxgiDev?.Adapter.QueryInterfaceOrNull<SharpDX.DXGI.Adapter4>();
if (dxgiAdapter == null)
return null;
var memInfo = dxgiAdapter.QueryVideoMemoryInfo(0, MemorySegmentGroup.Local);
var memInfo = dxgiAdapter.QueryVideoMemoryInfo(0, SharpDX.DXGI.MemorySegmentGroup.Local);
return (memInfo.CurrentUsage, memInfo.CurrentReservation);
}
catch
@ -464,11 +468,11 @@ internal class InterfaceManager : IInternalDisposableService
if (this.GameWindowHandle == 0)
throw new InvalidOperationException("Game window is not yet ready.");
var value = enabled ? 1 : 0;
((Result)NativeFunctions.DwmSetWindowAttribute(
((HRESULT)NativeFunctions.DwmSetWindowAttribute(
this.GameWindowHandle,
NativeFunctions.DWMWINDOWATTRIBUTE.DWMWA_USE_IMMERSIVE_DARK_MODE,
ref value,
sizeof(int))).CheckError();
sizeof(int))).ThrowOnError();
}
private static InterfaceManager WhenFontsReady()
@ -632,86 +636,6 @@ internal class InterfaceManager : IInternalDisposableService
args.SuppressWithValue(r.Value);
}
private void ReshadeOnPresentDetour(nint swapChain, uint flags, nint presentParams)
{
if (!SwapChainHelper.IsGameDeviceSwapChain(swapChain))
{
this.reshadeOnPresentHook!.Original(swapChain, flags, presentParams);
return;
}
Debug.Assert(this.reshadeOnPresentHook is not null, "this.reshadeOnPresentHook is not null");
Debug.Assert(this.dalamudAtlas is not null, "this.dalamudAtlas is not null");
if (this.scene == null)
this.InitScene(swapChain);
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");
}
this.reshadeOnPresentHook!.Original(swapChain, flags, presentParams);
return;
}
this.CumulativePresentCalls++;
this.IsMainThreadInPresent = true;
while (this.runBeforeImGuiRender.TryDequeue(out var action))
action.InvokeSafely();
this.reshadeOnPresentHook!.Original(swapChain, flags, presentParams);
RenderImGui(this.scene!);
this.PostImGuiRender();
this.IsMainThreadInPresent = false;
}
private IntPtr PresentDetour(IntPtr swapChain, uint syncInterval, uint presentFlags)
{
if (!SwapChainHelper.IsGameDeviceSwapChain(swapChain))
return this.dxgiPresentHook!.Original(swapChain, syncInterval, presentFlags);
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");
if (this.scene == null)
this.InitScene(swapChain);
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);
}
private void PostImGuiRender()
{
while (this.runAfterImGuiRender.TryDequeue(out var action))
@ -799,10 +723,7 @@ internal class InterfaceManager : IInternalDisposableService
() =>
{
// Update the ImGui default font.
unsafe
{
ImGui.GetIO().NativePtr->FontDefault = fontLocked.ImFont;
}
ImGui.GetIO().NativePtr->FontDefault = fontLocked.ImFont;
// Update the reference to the resources of the default font.
this.defaultFontResourceLock?.Dispose();
@ -818,7 +739,6 @@ internal class InterfaceManager : IInternalDisposableService
_ = this.dalamudAtlas.BuildFontsAsync();
SwapChainHelper.BusyWaitForGameDeviceSwapChain();
SwapChainHelper.DetectReShade();
try
{
@ -839,52 +759,36 @@ internal class InterfaceManager : IInternalDisposableService
this.SetCursorDetour);
Log.Verbose("===== S W A P C H A I N =====");
this.resizeBuffersHook = Hook<ResizeBuffersDelegate>.FromAddress(
(nint)SwapChainHelper.GameDeviceSwapChainVtbl->ResizeBuffers,
this.ResizeBuffersDetour);
Log.Verbose($"ResizeBuffers address {Util.DescribeAddress(this.resizeBuffersHook!.Address)}");
if (SwapChainHelper.ReshadeOnPresent is null)
if (ReShadeAddonInterface.TryRegisterAddon(out this.reShadeAddonInterface))
{
var addr = (nint)SwapChainHelper.GameDeviceSwapChainVtbl->Present;
this.dxgiPresentHook = Hook<DxgiPresentDelegate>.FromAddress(addr, this.PresentDetour);
Log.Verbose($"ReShade::DXGISwapChain::on_present address {Util.DescribeAddress(addr)}");
this.resizeBuffersHook = Hook<ResizeBuffersDelegate>.FromAddress(
(nint)SwapChainHelper.GameDeviceSwapChainVtbl->ResizeBuffers,
this.AsReShadeAddonResizeBuffersDetour);
Log.Verbose($"ResizeBuffers address {Util.DescribeAddress(this.resizeBuffersHook!.Address)}");
Log.Verbose(
"Registered as a ReShade({name}: 0x{addr:X}) addon.",
ReShadeAddonInterface.ReShadeModule!.FileName,
ReShadeAddonInterface.ReShadeModule!.BaseAddress);
this.reShadeAddonInterface.InitSwapChain += this.ReShadeAddonInterfaceOnInitSwapChain;
this.reShadeAddonInterface.DestroySwapChain += this.ReShadeAddonInterfaceOnDestroySwapChain;
this.reShadeAddonInterface.ReShadeOverlay += this.ReShadeAddonInterfaceOnReShadeOverlay;
}
else
{
var addr = (nint)SwapChainHelper.ReshadeOnPresent;
this.reshadeOnPresentHook = Hook<ReshadeOnPresentDelegate>.FromAddress(addr, this.ReshadeOnPresentDetour);
this.resizeBuffersHook = Hook<ResizeBuffersDelegate>.FromAddress(
(nint)SwapChainHelper.GameDeviceSwapChainVtbl->ResizeBuffers,
this.AsHookResizeBuffersDetour);
Log.Verbose($"ResizeBuffers address {Util.DescribeAddress(this.resizeBuffersHook!.Address)}");
var addr = (nint)SwapChainHelper.GameDeviceSwapChainVtbl->Present;
this.dxgiPresentHook = Hook<DxgiPresentDelegate>.FromAddress(addr, this.PresentDetour);
Log.Verbose($"IDXGISwapChain::Present address {Util.DescribeAddress(addr)}");
}
this.setCursorHook.Enable();
this.dxgiPresentHook?.Enable();
this.reshadeOnPresentHook?.Enable();
this.resizeBuffersHook.Enable();
}
private IntPtr ResizeBuffersDetour(IntPtr swapChain, uint bufferCount, uint width, uint height, uint newFormat, uint swapChainFlags)
{
if (!SwapChainHelper.IsGameDeviceSwapChain(swapChain))
return this.resizeBuffersHook!.Original(swapChain, bufferCount, width, height, newFormat, swapChainFlags);
#if DEBUG
Log.Verbose($"Calling resizebuffers swap@{swapChain.ToInt64():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)
{
Log.Error("invalid call to resizeBuffers");
}
this.scene?.OnPostResize((int)width, (int)height);
return ret;
this.resizeBuffersHook?.Enable();
}
private IntPtr SetCursorDetour(IntPtr hCursor)

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,59 @@
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using System.Runtime.CompilerServices;
using System.Text;
using TerraFX.Interop.Windows;
using static TerraFX.Interop.Windows.Windows;
namespace Dalamud.Interface.Internal;
/// <summary>ReShade interface.</summary>
[SuppressMessage(
"StyleCop.CSharp.LayoutRules",
"SA1519:Braces should not be omitted from multi-line child statement",
Justification = "Multiple fixed blocks")]
internal sealed unsafe partial class ReShadeAddonInterface
{
private static readonly ExportsStruct Exports;
static ReShadeAddonInterface()
{
foreach (var m in Process.GetCurrentProcess().Modules.Cast<ProcessModule>())
{
ExportsStruct e;
if (!GetProcAddressInto(m, nameof(e.ReShadeRegisterAddon), &e.ReShadeRegisterAddon) ||
!GetProcAddressInto(m, nameof(e.ReShadeUnregisterAddon), &e.ReShadeUnregisterAddon) ||
!GetProcAddressInto(m, nameof(e.ReShadeRegisterEvent), &e.ReShadeRegisterEvent) ||
!GetProcAddressInto(m, nameof(e.ReShadeUnregisterEvent), &e.ReShadeUnregisterEvent))
continue;
ReShadeModule = m;
Exports = e;
return;
}
return;
bool GetProcAddressInto(ProcessModule m, ReadOnlySpan<char> name, void* res)
{
Span<byte> name8 = stackalloc byte[Encoding.UTF8.GetByteCount(name) + 1];
name8[Encoding.UTF8.GetBytes(name, name8)] = 0;
*(nint*)res = GetProcAddress((HMODULE)m.BaseAddress, (sbyte*)Unsafe.AsPointer(ref name8[0]));
return *(nint*)res != 0;
}
}
/// <summary>Gets the active ReShade module.</summary>
public static ProcessModule? ReShadeModule { get; private set; }
private struct ExportsStruct
{
public delegate* unmanaged<HMODULE, uint, bool> ReShadeRegisterAddon;
public delegate* unmanaged<HMODULE, void> ReShadeUnregisterAddon;
public delegate* unmanaged<AddonEvent, void*, void> ReShadeRegisterEvent;
public delegate* unmanaged<AddonEvent, void*, void> ReShadeUnregisterEvent;
}
}

View file

@ -0,0 +1,176 @@
using System.Diagnostics.CodeAnalysis;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using Dalamud.Hooking;
using JetBrains.Annotations;
using TerraFX.Interop.Windows;
using static TerraFX.Interop.Windows.Windows;
namespace Dalamud.Interface.Internal;
/// <summary>ReShade interface.</summary>
internal sealed unsafe partial class ReShadeAddonInterface : IDisposable
{
private const int ReShadeApiVersion = 12;
private readonly HMODULE hDalamudModule;
private readonly Hook<GetModuleHandleExWDelegate> addonModuleResolverHook;
private readonly DelegateStorage<ReShadeOverlayDelegate> reShadeOverlayDelegate;
private readonly DelegateStorage<ReShadeInitSwapChain> initSwapChainDelegate;
private readonly DelegateStorage<ReShadeDestroySwapChain> destroySwapChainDelegate;
private ReShadeAddonInterface()
{
this.hDalamudModule = (HMODULE)Marshal.GetHINSTANCE(typeof(ReShadeAddonInterface).Assembly.ManifestModule);
if (!Exports.ReShadeRegisterAddon(this.hDalamudModule, ReShadeApiVersion))
throw new InvalidOperationException("ReShadeRegisterAddon failure.");
this.addonModuleResolverHook = Hook<GetModuleHandleExWDelegate>.FromImport(
ReShadeModule!,
"kernel32.dll",
nameof(GetModuleHandleExW),
0,
this.GetModuleHandleExWDetour);
this.addonModuleResolverHook.Enable();
Exports.ReShadeRegisterEvent(
AddonEvent.ReShadeOverlay,
this.reShadeOverlayDelegate = new((ref ApiObject rt) => this.ReShadeOverlay?.Invoke(ref rt)));
Exports.ReShadeRegisterEvent(
AddonEvent.InitSwapChain,
this.initSwapChainDelegate = new((ref ApiObject rt) => this.InitSwapChain?.Invoke(ref rt)));
Exports.ReShadeRegisterEvent(
AddonEvent.DestroySwapChain,
this.destroySwapChainDelegate = new((ref ApiObject rt) => this.DestroySwapChain?.Invoke(ref rt)));
}
/// <summary>Finalizes an instance of the <see cref="ReShadeAddonInterface"/> class.</summary>
~ReShadeAddonInterface() => this.ReleaseUnmanagedResources();
/// <summary>Delegate for <see cref="AddonEvent.ReShadeOverlay"/>.</summary>
/// <param name="effectRuntime">Reference to the ReShade runtime.</param>
public delegate void ReShadeOverlayDelegate(ref ApiObject effectRuntime);
/// <summary>Delegate for <see cref="AddonEvent.InitSwapChain"/>.</summary>
/// <param name="swapChain">Reference to the ReShade SwapChain wrapper.</param>
public delegate void ReShadeInitSwapChain(ref ApiObject swapChain);
/// <summary>Delegate for <see cref="AddonEvent.DestroySwapChain"/>.</summary>
/// <param name="swapChain">Reference to the ReShade SwapChain wrapper.</param>
public delegate void ReShadeDestroySwapChain(ref ApiObject swapChain);
private delegate BOOL GetModuleHandleExWDelegate(uint dwFlags, ushort* lpModuleName, HMODULE* phModule);
/// <summary>Called on <see cref="AddonEvent.ReShadeOverlay"/>.</summary>
public event ReShadeOverlayDelegate? ReShadeOverlay;
/// <summary>Called on <see cref="AddonEvent.InitSwapChain"/>.</summary>
public event ReShadeInitSwapChain? InitSwapChain;
/// <summary>Called on <see cref="AddonEvent.DestroySwapChain"/>.</summary>
public event ReShadeDestroySwapChain? DestroySwapChain;
/// <summary>Registers Dalamud as a ReShade addon.</summary>
/// <param name="r">Initialized interface.</param>
/// <returns><c>true</c> on success.</returns>
public static bool TryRegisterAddon([NotNullWhen(true)] out ReShadeAddonInterface? r)
{
try
{
r = Exports.ReShadeRegisterAddon is null ? null : new();
return r is not null;
}
catch
{
r = null;
return false;
}
}
/// <inheritdoc/>
public void Dispose()
{
this.ReleaseUnmanagedResources();
GC.SuppressFinalize(this);
}
private void ReleaseUnmanagedResources()
{
Exports.ReShadeUnregisterEvent(AddonEvent.InitSwapChain, this.initSwapChainDelegate);
Exports.ReShadeUnregisterEvent(AddonEvent.DestroySwapChain, this.destroySwapChainDelegate);
Exports.ReShadeUnregisterEvent(AddonEvent.ReShadeOverlay, this.reShadeOverlayDelegate);
Exports.ReShadeUnregisterAddon(this.hDalamudModule);
this.addonModuleResolverHook.Disable();
this.addonModuleResolverHook.Dispose();
}
private BOOL GetModuleHandleExWDetour(uint dwFlags, ushort* lpModuleName, HMODULE* phModule)
{
if ((dwFlags & GET.GET_MODULE_HANDLE_EX_FLAG_FROM_ADDRESS) == 0)
return this.addonModuleResolverHook.Original(dwFlags, lpModuleName, phModule);
if ((dwFlags & GET.GET_MODULE_HANDLE_EX_FLAG_UNCHANGED_REFCOUNT) == 0)
return this.addonModuleResolverHook.Original(dwFlags, lpModuleName, phModule);
if (lpModuleName == this.initSwapChainDelegate ||
lpModuleName == this.destroySwapChainDelegate ||
lpModuleName == this.reShadeOverlayDelegate)
{
*phModule = this.hDalamudModule;
return BOOL.TRUE;
}
return this.addonModuleResolverHook.Original(dwFlags, lpModuleName, phModule);
}
/// <summary>ReShade effect runtime object.</summary>
[StructLayout(LayoutKind.Sequential)]
public struct ApiObject
{
/// <summary>The vtable.</summary>
public VTable* Vtbl;
/// <summary>Gets this object as a typed pointer.</summary>
/// <returns>Address of this instance.</returns>
/// <remarks>This call is invalid if this object is not already fixed.</remarks>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public ApiObject* AsPointer() => (ApiObject*)Unsafe.AsPointer(ref this);
/// <summary>Gets the native object.</summary>
/// <returns>The native object.</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public nint GetNative() => this.Vtbl->GetNative(this.AsPointer());
/// <inheritdoc cref="GetNative"/>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public T* GetNative<T>() where T : unmanaged => (T*)this.GetNative();
/// <summary>VTable of <see cref="ApiObject"/>.</summary>
[StructLayout(LayoutKind.Sequential)]
public struct VTable
{
/// <inheritdoc cref="ApiObject.GetNative"/>
public delegate* unmanaged<ApiObject*, nint> GetNative;
}
}
private readonly struct DelegateStorage<T> where T : Delegate
{
[UsedImplicitly]
public readonly T Delegate;
public readonly void* Address;
public DelegateStorage(T @delegate)
{
this.Delegate = @delegate;
this.Address = (void*)Marshal.GetFunctionPointerForDelegate(@delegate);
}
public static implicit operator void*(DelegateStorage<T> sto) => sto.Address;
}
}

View file

@ -1,13 +1,7 @@
using System.Diagnostics;
using System.Threading;
using Dalamud.Game;
using Dalamud.Utility;
using FFXIVClientStructs.FFXIV.Client.Graphics.Kernel;
using Serilog;
using TerraFX.Interop.DirectX;
using TerraFX.Interop.Windows;
@ -16,12 +10,6 @@ namespace Dalamud.Interface.Internal;
/// <summary>Helper for dealing with swap chains.</summary>
internal static unsafe class SwapChainHelper
{
/// <summary>
/// Gets the function pointer for ReShade's DXGISwapChain::on_present.
/// <a href="https://github.com/crosire/reshade/blob/59eeecd0c902129a168cd772a63c46c5254ff2c5/source/dxgi/dxgi_swapchain.hpp#L88">Source.</a>
/// </summary>
public static delegate* unmanaged<nint, uint, nint, void> ReshadeOnPresent { get; private set; }
/// <summary>Gets the game's active instance of IDXGISwapChain that is initialized.</summary>
/// <value>Address of the game's instance of IDXGISwapChain, or <c>null</c> if not available (yet.)</value>
public static IDXGISwapChain* GameDeviceSwapChain
@ -92,102 +80,4 @@ internal static unsafe class SwapChainHelper
while (GameDeviceSwapChain is null)
Thread.Yield();
}
/// <summary>Detects ReShade and populate <see cref="ReshadeOnPresent"/>.</summary>
public static void DetectReShade()
{
var modules = Process.GetCurrentProcess().Modules;
foreach (ProcessModule processModule in modules)
{
if (!processModule.FileName.EndsWith("game\\dxgi.dll", StringComparison.InvariantCultureIgnoreCase))
continue;
try
{
var fileInfo = FileVersionInfo.GetVersionInfo(processModule.FileName);
if (fileInfo.FileDescription == null)
break;
if (!fileInfo.FileDescription.Contains("GShade") && !fileInfo.FileDescription.Contains("ReShade"))
break;
// warning: these comments may no longer be accurate.
// reshade master@4232872 RVA
// var p = processModule.BaseAddress + 0x82C7E0; // DXGISwapChain::Present
// var p = processModule.BaseAddress + 0x82FAC0; // DXGISwapChain::runtime_present
// DXGISwapChain::handle_device_loss =>df DXGISwapChain::Present => DXGISwapChain::runtime_present
// 5.2+ - F6 C2 01 0F 85
// 6.0+ - F6 C2 01 0F 85 88
var scanner = new SigScanner(processModule);
var reShadeDxgiPresent = nint.Zero;
if (fileInfo.FileVersion?.StartsWith("6.") == true)
{
// No Addon
if (scanner.TryScanText("F6 C2 01 0F 85 A8", out reShadeDxgiPresent))
{
Log.Information("Hooking present for ReShade 6 No-Addon");
}
// Addon
else if (scanner.TryScanText("F6 C2 01 0F 85 88", out reShadeDxgiPresent))
{
Log.Information("Hooking present for ReShade 6 Addon");
}
// Fallback
else
{
Log.Error("Failed to get ReShade 6 DXGISwapChain::on_present offset!");
}
}
// Looks like this sig only works for GShade 4
if (reShadeDxgiPresent == nint.Zero && fileInfo.FileDescription?.Contains("GShade 4.") == true)
{
if (scanner.TryScanText("E8 ?? ?? ?? ?? 45 0F B6 5E ??", out reShadeDxgiPresent))
{
Log.Information("Hooking present for GShade 4");
}
else
{
Log.Error("Failed to find GShade 4 DXGISwapChain::on_present offset!");
}
}
if (reShadeDxgiPresent == nint.Zero)
{
if (scanner.TryScanText("F6 C2 01 0F 85", out reShadeDxgiPresent))
{
Log.Information("Hooking present for ReShade with fallback 5.X sig");
}
else
{
Log.Error("Failed to find ReShade DXGISwapChain::on_present offset with fallback sig!");
}
}
Log.Information(
"ReShade DLL: {FileName} ({Info} - {Version}) with DXGISwapChain::on_present at {Address}",
processModule.FileName,
fileInfo.FileDescription ?? "Unknown",
fileInfo.FileVersion ?? "Unknown",
Util.DescribeAddress(reShadeDxgiPresent));
if (reShadeDxgiPresent != nint.Zero)
{
ReshadeOnPresent = (delegate* unmanaged<nint, uint, nint, void>)reShadeDxgiPresent;
}
break;
}
catch (Exception e)
{
Log.Error(e, "Failed to get ReShade version info");
break;
}
}
}
}