diff --git a/Dalamud/Configuration/Internal/DalamudConfiguration.cs b/Dalamud/Configuration/Internal/DalamudConfiguration.cs
index 9d54f4562..d5f1299fd 100644
--- a/Dalamud/Configuration/Internal/DalamudConfiguration.cs
+++ b/Dalamud/Configuration/Internal/DalamudConfiguration.cs
@@ -8,6 +8,7 @@ using System.Runtime.InteropServices;
using Dalamud.Game.Text;
using Dalamud.Interface;
using Dalamud.Interface.FontIdentifier;
+using Dalamud.Interface.Internal;
using Dalamud.Interface.Internal.ReShadeHandling;
using Dalamud.Interface.Style;
using Dalamud.IoC.Internal;
@@ -445,6 +446,9 @@ internal sealed class DalamudConfiguration : IInternalDisposableService
/// Gets or sets the mode specifying how to handle ReShade.
public ReShadeHandlingMode ReShadeHandlingMode { get; set; } = ReShadeHandlingMode.ReShadeAddon;
+ /// Gets or sets the swap chain hook mode.
+ public SwapChainHelper.HookMode SwapChainHookMode { get; set; } = SwapChainHelper.HookMode.ByteCode;
+
///
/// Gets or sets hitch threshold for game network up in milliseconds.
///
diff --git a/Dalamud/Hooking/Internal/ObjectVTableHook.cs b/Dalamud/Hooking/Internal/ObjectVTableHook.cs
new file mode 100644
index 000000000..b4500bb5f
--- /dev/null
+++ b/Dalamud/Hooking/Internal/ObjectVTableHook.cs
@@ -0,0 +1,286 @@
+using System.Linq;
+using System.Reflection;
+using System.Runtime.CompilerServices;
+using System.Runtime.InteropServices;
+
+using Dalamud.Utility;
+
+using Serilog;
+
+namespace Dalamud.Hooking.Internal;
+
+/// Manages a hook that works by replacing the vtable of target object.
+internal unsafe class ObjectVTableHook : IDisposable
+{
+ private readonly nint** ppVtbl;
+ private readonly int numMethods;
+
+ private readonly nint* pVtblOriginal;
+ private readonly nint[] vtblOverriden;
+
+ /// Extra data for overriden vtable entries, primarily for keeping references to delegates that are used
+ /// with .
+ private readonly object?[] vtblOverridenTag;
+
+ private bool released;
+
+ /// Initializes a new instance of the class.
+ /// Address to vtable. Usually the address of the object itself.
+ /// Number of methods in this vtable.
+ public ObjectVTableHook(nint ppVtbl, int numMethods)
+ {
+ this.ppVtbl = (nint**)ppVtbl;
+ this.numMethods = numMethods;
+ this.vtblOverridenTag = new object?[numMethods];
+ this.pVtblOriginal = *this.ppVtbl;
+ this.vtblOverriden = GC.AllocateArray(numMethods, true);
+ this.OriginalVTableSpan.CopyTo(this.vtblOverriden);
+ }
+
+ /// Initializes a new instance of the class.
+ /// Address to vtable. Usually the address of the object itself.
+ /// Number of methods in this vtable.
+ public ObjectVTableHook(void* ppVtbl, int numMethods)
+ : this((nint)ppVtbl, numMethods)
+ {
+ }
+
+ /// Finalizes an instance of the class.
+ ~ObjectVTableHook() => this.ReleaseUnmanagedResources();
+
+ /// Gets the span view of original vtable.
+ public ReadOnlySpan OriginalVTableSpan => new(this.pVtblOriginal, this.numMethods);
+
+ /// Gets the span view of overriden vtable.
+ public ReadOnlySpan OverridenVTableSpan => this.vtblOverriden.AsSpan();
+
+ /// Disables the hook.
+ public void Disable()
+ {
+ // already disabled
+ if (*this.ppVtbl == this.pVtblOriginal)
+ return;
+
+ if (*this.ppVtbl != Unsafe.AsPointer(ref this.vtblOverriden[0]))
+ {
+ Log.Warning(
+ "[{who}]: the object was hooked by something else; disabling may result in a crash.",
+ this.GetType().Name);
+ }
+
+ *this.ppVtbl = this.pVtblOriginal;
+ }
+
+ ///
+ public void Dispose()
+ {
+ this.ReleaseUnmanagedResources();
+ GC.SuppressFinalize(this);
+ }
+
+ /// Enables the hook.
+ public void Enable()
+ {
+ // already enabled
+ if (*this.ppVtbl == Unsafe.AsPointer(ref this.vtblOverriden[0]))
+ return;
+
+ if (*this.ppVtbl != this.pVtblOriginal)
+ {
+ Log.Warning(
+ "[{who}]: the object was hooked by something else; enabling may result in a crash.",
+ this.GetType().Name);
+ }
+
+ *this.ppVtbl = (nint*)Unsafe.AsPointer(ref this.vtblOverriden[0]);
+ }
+
+ /// Gets the original method address of the given method index.
+ /// Index of the method.
+ /// Address of the original method.
+ public nint GetOriginalMethodAddress(int methodIndex)
+ {
+ this.EnsureMethodIndex(methodIndex);
+ return this.pVtblOriginal[methodIndex];
+ }
+
+ /// Gets the original method of the given method index, as a delegate of given type.
+ /// Index of the method.
+ /// Type of delegate.
+ /// Delegate to the original method.
+ public T GetOriginalMethodDelegate(int methodIndex)
+ where T : Delegate
+ {
+ this.EnsureMethodIndex(methodIndex);
+ return Marshal.GetDelegateForFunctionPointer(this.pVtblOriginal[methodIndex]);
+ }
+
+ /// Resets a method to the original function.
+ /// Index of the method.
+ public void ResetVtableEntry(int methodIndex)
+ {
+ this.EnsureMethodIndex(methodIndex);
+ this.vtblOverriden[methodIndex] = this.pVtblOriginal[methodIndex];
+ this.vtblOverridenTag[methodIndex] = null;
+ }
+
+ /// Sets a method in vtable to the given address of function.
+ /// Index of the method.
+ /// Address of the detour function.
+ /// Additional reference to keep in memory.
+ public void SetVtableEntry(int methodIndex, nint pfn, object? refkeep)
+ {
+ this.EnsureMethodIndex(methodIndex);
+ this.vtblOverriden[methodIndex] = pfn;
+ this.vtblOverridenTag[methodIndex] = refkeep;
+ }
+
+ /// Sets a method in vtable to the given delegate.
+ /// Index of the method.
+ /// Detour delegate.
+ /// Type of delegate.
+ public void SetVtableEntry(int methodIndex, T detourDelegate)
+ where T : Delegate =>
+ this.SetVtableEntry(methodIndex, Marshal.GetFunctionPointerForDelegate(detourDelegate), detourDelegate);
+
+ /// Sets a method in vtable to the given delegate.
+ /// Index of the method.
+ /// Detour delegate.
+ /// Original method delegate.
+ /// Type of delegate.
+ public void SetVtableEntry(int methodIndex, T detourDelegate, out T originalMethodDelegate)
+ where T : Delegate
+ {
+ originalMethodDelegate = this.GetOriginalMethodDelegate(methodIndex);
+ this.SetVtableEntry(methodIndex, Marshal.GetFunctionPointerForDelegate(detourDelegate), detourDelegate);
+ }
+
+ /// Creates a new instance of that manages one entry in the vtable hook.
+ /// Index of the method.
+ /// Detour delegate.
+ /// Type of delegate.
+ /// A new instance of .
+ /// Even if a single hook is enabled, without , the hook will remain disabled.
+ ///
+ public Hook CreateHook(int methodIndex, T detourDelegate) where T : Delegate =>
+ new SingleHook(this, methodIndex, detourDelegate);
+
+ private void EnsureMethodIndex(int methodIndex)
+ {
+ ArgumentOutOfRangeException.ThrowIfNegative(methodIndex);
+ ArgumentOutOfRangeException.ThrowIfGreaterThanOrEqual(methodIndex, this.numMethods);
+ }
+
+ private void ReleaseUnmanagedResources()
+ {
+ if (!this.released)
+ {
+ this.Disable();
+ this.released = true;
+ }
+ }
+
+ private sealed class SingleHook(ObjectVTableHook hook, int methodIndex, T detourDelegate)
+ : Hook((nint)hook.ppVtbl)
+ where T : Delegate
+ {
+ ///
+ public override T Original { get; } = hook.GetOriginalMethodDelegate(methodIndex);
+
+ ///
+ public override bool IsEnabled =>
+ hook.OriginalVTableSpan[methodIndex] != hook.OverridenVTableSpan[methodIndex];
+
+ ///
+ public override string BackendName => nameof(ObjectVTableHook);
+
+ ///
+ public override void Enable() => hook.SetVtableEntry(methodIndex, detourDelegate);
+
+ ///
+ public override void Disable() => hook.ResetVtableEntry(methodIndex);
+ }
+}
+
+/// Typed version of .
+/// VTable struct.
+internal unsafe class ObjectVTableHook : ObjectVTableHook
+ where TVTable : unmanaged
+{
+ private static readonly string[] Fields =
+ typeof(TVTable).GetFields(BindingFlags.Instance | BindingFlags.Public).Select(x => x.Name).ToArray();
+
+ /// Initializes a new instance of the class.
+ /// Address to vtable. Usually the address of the object itself.
+ public ObjectVTableHook(void* ppVtbl)
+ : base(ppVtbl, Fields.Length)
+ {
+ }
+
+ /// Gets the original vtable.
+ public ref readonly TVTable OriginalVTable => ref MemoryMarshal.Cast(this.OriginalVTableSpan)[0];
+
+ /// Gets the overriden vtable.
+ public ref readonly TVTable OverridenVTable => ref MemoryMarshal.Cast(this.OverridenVTableSpan)[0];
+
+ /// Gets the index of the method by method name.
+ /// Name of the method.
+ /// Index of the method.
+ public int GetMethodIndex(string methodName) => Fields.IndexOf(methodName);
+
+ /// Gets the original method address of the given method index.
+ /// Name of the method.
+ /// Address of the original method.
+ public nint GetOriginalMethodAddress(string methodName) =>
+ this.GetOriginalMethodAddress(this.GetMethodIndex(methodName));
+
+ /// Gets the original method of the given method index, as a delegate of given type.
+ /// Name of the method.
+ /// Type of delegate.
+ /// Delegate to the original method.
+ public T GetOriginalMethodDelegate(string methodName)
+ where T : Delegate
+ => this.GetOriginalMethodDelegate(this.GetMethodIndex(methodName));
+
+ /// Resets a method to the original function.
+ /// Name of the method.
+ public void ResetVtableEntry(string methodName)
+ => this.ResetVtableEntry(this.GetMethodIndex(methodName));
+
+ /// Sets a method in vtable to the given address of function.
+ /// Name of the method.
+ /// Address of the detour function.
+ /// Additional reference to keep in memory.
+ public void SetVtableEntry(string methodName, nint pfn, object? refkeep)
+ => this.SetVtableEntry(this.GetMethodIndex(methodName), pfn, refkeep);
+
+ /// Sets a method in vtable to the given delegate.
+ /// Name of the method.
+ /// Detour delegate.
+ /// Type of delegate.
+ public void SetVtableEntry(string methodName, T detourDelegate)
+ where T : Delegate =>
+ this.SetVtableEntry(
+ this.GetMethodIndex(methodName),
+ Marshal.GetFunctionPointerForDelegate(detourDelegate),
+ detourDelegate);
+
+ /// Sets a method in vtable to the given delegate.
+ /// Name of the method.
+ /// Detour delegate.
+ /// Original method delegate.
+ /// Type of delegate.
+ public void SetVtableEntry(string methodName, T detourDelegate, out T originalMethodDelegate)
+ where T : Delegate
+ => this.SetVtableEntry(this.GetMethodIndex(methodName), detourDelegate, out originalMethodDelegate);
+
+ /// Creates a new instance of that manages one entry in the vtable hook.
+ /// Name of the method.
+ /// Detour delegate.
+ /// Type of delegate.
+ /// A new instance of .
+ /// Even if a single hook is enabled, without , the hook will remain
+ /// disabled.
+ public Hook CreateHook(string methodName, T detourDelegate) where T : Delegate =>
+ this.CreateHook(this.GetMethodIndex(methodName), detourDelegate);
+}
diff --git a/Dalamud/Interface/Internal/InterfaceManager.cs b/Dalamud/Interface/Internal/InterfaceManager.cs
index b7c2f8765..4941ea46c 100644
--- a/Dalamud/Interface/Internal/InterfaceManager.cs
+++ b/Dalamud/Interface/Internal/InterfaceManager.cs
@@ -35,6 +35,7 @@ using JetBrains.Annotations;
using PInvoke;
+using TerraFX.Interop.DirectX;
using TerraFX.Interop.Windows;
// general dev notes, here because it's easiest
@@ -94,6 +95,7 @@ internal partial class InterfaceManager : IInternalDisposableService
private Hook? setCursorHook;
private Hook? dxgiPresentHook;
private Hook? resizeBuffersHook;
+ private ObjectVTableHook>? swapChainHook;
private ReShadeAddonInterface? reShadeAddonInterface;
private IFontAtlas? dalamudAtlas;
@@ -308,6 +310,7 @@ internal partial class InterfaceManager : IInternalDisposableService
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.reShadeAddonInterface, null)?.Dispose();
}
}
@@ -769,12 +772,13 @@ internal partial class InterfaceManager : IInternalDisposableService
Log.Verbose("Unwrapped ReShade.");
}
+ ResizeBuffersDelegate resizeBuffersDelegate;
+ DxgiPresentDelegate? dxgiPresentDelegate;
if (this.dalamudConfiguration.ReShadeHandlingMode == ReShadeHandlingMode.ReShadeAddon &&
ReShadeAddonInterface.TryRegisterAddon(out this.reShadeAddonInterface))
{
- this.resizeBuffersHook = Hook.FromAddress(
- (nint)SwapChainHelper.GameDeviceSwapChainVtbl->ResizeBuffers,
- this.AsReShadeAddonResizeBuffersDetour);
+ resizeBuffersDelegate = this.AsReShadeAddonResizeBuffersDetour;
+ dxgiPresentDelegate = null;
Log.Verbose(
"Registered as a ReShade({name}: 0x{addr:X}) addon.",
@@ -786,21 +790,55 @@ internal partial class InterfaceManager : IInternalDisposableService
}
else
{
- this.resizeBuffersHook = Hook.FromAddress(
- (nint)SwapChainHelper.GameDeviceSwapChainVtbl->ResizeBuffers,
- this.AsHookResizeBuffersDetour);
-
- this.dxgiPresentHook = Hook.FromAddress(
- (nint)SwapChainHelper.GameDeviceSwapChainVtbl->Present,
- this.PresentDetour);
+ resizeBuffersDelegate = this.AsHookResizeBuffersDetour;
+ dxgiPresentDelegate = this.PresentDetour;
}
- Log.Verbose($"IDXGISwapChain::ResizeBuffers address: {Util.DescribeAddress(this.resizeBuffersHook.Address)}");
+ switch (this.dalamudConfiguration.SwapChainHookMode)
+ {
+ case SwapChainHelper.HookMode.ByteCode:
+ default:
+ {
+ this.resizeBuffersHook = Hook.FromAddress(
+ (nint)SwapChainHelper.GameDeviceSwapChainVtbl->ResizeBuffers,
+ resizeBuffersDelegate);
+
+ if (dxgiPresentDelegate is not null)
+ {
+ this.dxgiPresentHook = Hook.FromAddress(
+ (nint)SwapChainHelper.GameDeviceSwapChainVtbl->Present,
+ dxgiPresentDelegate);
+ }
+
+ break;
+ }
+
+ case SwapChainHelper.HookMode.VTable:
+ {
+ this.swapChainHook = new(SwapChainHelper.GameDeviceSwapChain);
+ this.resizeBuffersHook = this.swapChainHook.CreateHook(
+ nameof(IDXGISwapChain.ResizeBuffers),
+ resizeBuffersDelegate);
+
+ if (dxgiPresentDelegate is not null)
+ {
+ this.dxgiPresentHook = this.swapChainHook.CreateHook(
+ nameof(IDXGISwapChain.Present),
+ dxgiPresentDelegate);
+ }
+
+ break;
+ }
+ }
+
+ Log.Verbose(
+ $"IDXGISwapChain::ResizeBuffers address: {Util.DescribeAddress(this.resizeBuffersHook.Address)}");
Log.Verbose($"IDXGISwapChain::Present address: {Util.DescribeAddress(this.dxgiPresentHook?.Address ?? 0)}");
this.setCursorHook.Enable();
- this.dxgiPresentHook?.Enable();
this.resizeBuffersHook.Enable();
+ this.dxgiPresentHook?.Enable();
+ this.swapChainHook?.Enable();
}
private IntPtr SetCursorDetour(IntPtr hCursor)
diff --git a/Dalamud/Interface/Internal/ReShadeHandling/ReShadeUnwrapper.cs b/Dalamud/Interface/Internal/ReShadeHandling/ReShadeUnwrapper.cs
index 3682b03c0..f1210425d 100644
--- a/Dalamud/Interface/Internal/ReShadeHandling/ReShadeUnwrapper.cs
+++ b/Dalamud/Interface/Internal/ReShadeHandling/ReShadeUnwrapper.cs
@@ -78,28 +78,24 @@ internal static unsafe class ReShadeUnwrapper
{
foreach (ProcessModule processModule in Process.GetCurrentProcess().Modules)
{
- if (ptr < processModule.BaseAddress || ptr >= processModule.BaseAddress + processModule.ModuleMemorySize)
+ if (ptr < processModule.BaseAddress ||
+ ptr >= processModule.BaseAddress + processModule.ModuleMemorySize ||
+ !HasProcExported(processModule, "ReShadeRegisterAddon"u8) ||
+ !HasProcExported(processModule, "ReShadeUnregisterAddon"u8) ||
+ !HasProcExported(processModule, "ReShadeRegisterEvent"u8) ||
+ !HasProcExported(processModule, "ReShadeUnregisterEvent"u8))
continue;
- fixed (byte* pfn0 = "ReShadeRegisterAddon"u8)
- fixed (byte* pfn1 = "ReShadeUnregisterAddon"u8)
- fixed (byte* pfn2 = "ReShadeRegisterEvent"u8)
- fixed (byte* pfn3 = "ReShadeUnregisterEvent"u8)
- {
- if (GetProcAddress((HMODULE)processModule.BaseAddress, (sbyte*)pfn0) == 0)
- continue;
- if (GetProcAddress((HMODULE)processModule.BaseAddress, (sbyte*)pfn1) == 0)
- continue;
- if (GetProcAddress((HMODULE)processModule.BaseAddress, (sbyte*)pfn2) == 0)
- continue;
- if (GetProcAddress((HMODULE)processModule.BaseAddress, (sbyte*)pfn3) == 0)
- continue;
- }
-
return true;
}
return false;
+
+ static bool HasProcExported(ProcessModule m, ReadOnlySpan name)
+ {
+ fixed (byte* p = name)
+ return GetProcAddress((HMODULE)m.BaseAddress, (sbyte*)p) != 0;
+ }
}
private static bool IsReShadedComObject(T* obj)
diff --git a/Dalamud/Interface/Internal/SwapChainHelper.cs b/Dalamud/Interface/Internal/SwapChainHelper.cs
index 051e348e0..4a336ee9f 100644
--- a/Dalamud/Interface/Internal/SwapChainHelper.cs
+++ b/Dalamud/Interface/Internal/SwapChainHelper.cs
@@ -14,6 +14,16 @@ internal static unsafe class SwapChainHelper
{
private static IDXGISwapChain* foundGameDeviceSwapChain;
+ /// Describes how to hook methods.
+ public enum HookMode
+ {
+ /// Hooks by rewriting the native bytecode.
+ ByteCode,
+
+ /// Hooks by providing an alternative vtable.
+ VTable,
+ }
+
/// Gets the game's active instance of IDXGISwapChain that is initialized.
/// Address of the game's instance of IDXGISwapChain, or null if not available (yet.)
public static IDXGISwapChain* GameDeviceSwapChain
diff --git a/Dalamud/Interface/Internal/Windows/Settings/Tabs/SettingsTabExperimental.cs b/Dalamud/Interface/Internal/Windows/Settings/Tabs/SettingsTabExperimental.cs
index c51f465f9..faefe418c 100644
--- a/Dalamud/Interface/Internal/Windows/Settings/Tabs/SettingsTabExperimental.cs
+++ b/Dalamud/Interface/Internal/Windows/Settings/Tabs/SettingsTabExperimental.cs
@@ -98,7 +98,7 @@ public class SettingsTabExperimental : SettingsTab
{
ReShadeHandlingMode.ReShadeAddon => Loc.Localize(
"DalamudSettingsReShadeHandlingModeReShadeAddonDescription",
- "Dalamud will register itself as a ReShade addon. Most compatibility is expected, but multi-monitor window option won't work too well."),
+ "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.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."),
@@ -109,6 +109,17 @@ public class SettingsTabExperimental : SettingsTab
},
},
+ new GapSettingsEntry(5, true),
+
+ new EnumSettingsEntry(
+ Loc.Localize("DalamudSettingsSwapChainHookMode", "Swap chain hooking mode"),
+ Loc.Localize(
+ "DalamudSettingsSwapChainHookModeHint",
+ "Depending on addons aside from Dalamud you use, you may have to use different options for Dalamud and other addons to cooperate.\nRestart is required for changes to take effect."),
+ c => c.SwapChainHookMode,
+ (v, c) => c.SwapChainHookMode = v,
+ fallbackValue: SwapChainHelper.HookMode.ByteCode),
+
/* Disabling profiles after they've been enabled doesn't make much sense, at least not if the user has already created profiles.
new GapSettingsEntry(5, true),