Merge pull request #1955 from Soreepeong/reshade-addon

Implement ReShade addon interface
This commit is contained in:
goat 2024-07-23 19:32:17 +02:00 committed by GitHub
commit 22eea4b0de
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
17 changed files with 3044 additions and 309 deletions

View file

@ -8,6 +8,8 @@ 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;
using Dalamud.Plugin.Internal.AutoUpdate;
@ -441,6 +443,12 @@ internal sealed class DalamudConfiguration : IInternalDisposableService
/// </summary>
public bool WindowIsImmersive { get; set; } = false;
/// <summary>Gets or sets the mode specifying how to handle ReShade.</summary>
public ReShadeHandlingMode ReShadeHandlingMode { get; set; } = ReShadeHandlingMode.ReShadeAddon;
/// <summary>Gets or sets the swap chain hook mode.</summary>
public SwapChainHelper.HookMode SwapChainHookMode { get; set; } = SwapChainHelper.HookMode.ByteCode;
/// <summary>
/// Gets or sets hitch threshold for game network up in milliseconds.
/// </summary>

View file

@ -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;
/// <summary>Manages a hook that works by replacing the vtable of target object.</summary>
internal unsafe class ObjectVTableHook : IDisposable
{
private readonly nint** ppVtbl;
private readonly int numMethods;
private readonly nint* pVtblOriginal;
private readonly nint[] vtblOverriden;
/// <summary>Extra data for overriden vtable entries, primarily for keeping references to delegates that are used
/// with <see cref="Marshal.GetFunctionPointerForDelegate"/>.</summary>
private readonly object?[] vtblOverridenTag;
private bool released;
/// <summary>Initializes a new instance of the <see cref="ObjectVTableHook"/> class.</summary>
/// <param name="ppVtbl">Address to vtable. Usually the address of the object itself.</param>
/// <param name="numMethods">Number of methods in this vtable.</param>
public ObjectVTableHook(nint ppVtbl, int numMethods)
{
this.ppVtbl = (nint**)ppVtbl;
this.numMethods = numMethods;
this.vtblOverridenTag = new object?[numMethods];
this.pVtblOriginal = *this.ppVtbl;
this.vtblOverriden = GC.AllocateArray<nint>(numMethods, true);
this.OriginalVTableSpan.CopyTo(this.vtblOverriden);
}
/// <summary>Initializes a new instance of the <see cref="ObjectVTableHook"/> class.</summary>
/// <param name="ppVtbl">Address to vtable. Usually the address of the object itself.</param>
/// <param name="numMethods">Number of methods in this vtable.</param>
public ObjectVTableHook(void* ppVtbl, int numMethods)
: this((nint)ppVtbl, numMethods)
{
}
/// <summary>Finalizes an instance of the <see cref="ObjectVTableHook"/> class.</summary>
~ObjectVTableHook() => this.ReleaseUnmanagedResources();
/// <summary>Gets the span view of original vtable.</summary>
public ReadOnlySpan<nint> OriginalVTableSpan => new(this.pVtblOriginal, this.numMethods);
/// <summary>Gets the span view of overriden vtable.</summary>
public ReadOnlySpan<nint> OverridenVTableSpan => this.vtblOverriden.AsSpan();
/// <summary>Disables the hook.</summary>
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;
}
/// <inheritdoc />
public void Dispose()
{
this.ReleaseUnmanagedResources();
GC.SuppressFinalize(this);
}
/// <summary>Enables the hook.</summary>
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]);
}
/// <summary>Gets the original method address of the given method index.</summary>
/// <param name="methodIndex">Index of the method.</param>
/// <returns>Address of the original method.</returns>
public nint GetOriginalMethodAddress(int methodIndex)
{
this.EnsureMethodIndex(methodIndex);
return this.pVtblOriginal[methodIndex];
}
/// <summary>Gets the original method of the given method index, as a delegate of given type.</summary>
/// <param name="methodIndex">Index of the method.</param>
/// <typeparam name="T">Type of delegate.</typeparam>
/// <returns>Delegate to the original method.</returns>
public T GetOriginalMethodDelegate<T>(int methodIndex)
where T : Delegate
{
this.EnsureMethodIndex(methodIndex);
return Marshal.GetDelegateForFunctionPointer<T>(this.pVtblOriginal[methodIndex]);
}
/// <summary>Resets a method to the original function.</summary>
/// <param name="methodIndex">Index of the method.</param>
public void ResetVtableEntry(int methodIndex)
{
this.EnsureMethodIndex(methodIndex);
this.vtblOverriden[methodIndex] = this.pVtblOriginal[methodIndex];
this.vtblOverridenTag[methodIndex] = null;
}
/// <summary>Sets a method in vtable to the given address of function.</summary>
/// <param name="methodIndex">Index of the method.</param>
/// <param name="pfn">Address of the detour function.</param>
/// <param name="refkeep">Additional reference to keep in memory.</param>
public void SetVtableEntry(int methodIndex, nint pfn, object? refkeep)
{
this.EnsureMethodIndex(methodIndex);
this.vtblOverriden[methodIndex] = pfn;
this.vtblOverridenTag[methodIndex] = refkeep;
}
/// <summary>Sets a method in vtable to the given delegate.</summary>
/// <param name="methodIndex">Index of the method.</param>
/// <param name="detourDelegate">Detour delegate.</param>
/// <typeparam name="T">Type of delegate.</typeparam>
public void SetVtableEntry<T>(int methodIndex, T detourDelegate)
where T : Delegate =>
this.SetVtableEntry(methodIndex, Marshal.GetFunctionPointerForDelegate(detourDelegate), detourDelegate);
/// <summary>Sets a method in vtable to the given delegate.</summary>
/// <param name="methodIndex">Index of the method.</param>
/// <param name="detourDelegate">Detour delegate.</param>
/// <param name="originalMethodDelegate">Original method delegate.</param>
/// <typeparam name="T">Type of delegate.</typeparam>
public void SetVtableEntry<T>(int methodIndex, T detourDelegate, out T originalMethodDelegate)
where T : Delegate
{
originalMethodDelegate = this.GetOriginalMethodDelegate<T>(methodIndex);
this.SetVtableEntry(methodIndex, Marshal.GetFunctionPointerForDelegate(detourDelegate), detourDelegate);
}
/// <summary>Creates a new instance of <see cref="Hook{T}"/> that manages one entry in the vtable hook.</summary>
/// <param name="methodIndex">Index of the method.</param>
/// <param name="detourDelegate">Detour delegate.</param>
/// <typeparam name="T">Type of delegate.</typeparam>
/// <returns>A new instance of <see cref="Hook{T}"/>.</returns>
/// <remarks>Even if a single hook is enabled, without <see cref="Enable"/>, the hook will remain disabled.
/// </remarks>
public Hook<T> CreateHook<T>(int methodIndex, T detourDelegate) where T : Delegate =>
new SingleHook<T>(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<T>(ObjectVTableHook hook, int methodIndex, T detourDelegate)
: Hook<T>((nint)hook.ppVtbl)
where T : Delegate
{
/// <inheritdoc/>
public override T Original { get; } = hook.GetOriginalMethodDelegate<T>(methodIndex);
/// <inheritdoc/>
public override bool IsEnabled =>
hook.OriginalVTableSpan[methodIndex] != hook.OverridenVTableSpan[methodIndex];
/// <inheritdoc/>
public override string BackendName => nameof(ObjectVTableHook);
/// <inheritdoc/>
public override void Enable() => hook.SetVtableEntry(methodIndex, detourDelegate);
/// <inheritdoc/>
public override void Disable() => hook.ResetVtableEntry(methodIndex);
}
}
/// <summary>Typed version of <see cref="ObjectVTableHook"/>.</summary>
/// <typeparam name="TVTable">VTable struct.</typeparam>
internal unsafe class ObjectVTableHook<TVTable> : ObjectVTableHook
where TVTable : unmanaged
{
private static readonly string[] Fields =
typeof(TVTable).GetFields(BindingFlags.Instance | BindingFlags.Public).Select(x => x.Name).ToArray();
/// <summary>Initializes a new instance of the <see cref="ObjectVTableHook{TVTable}"/> class.</summary>
/// <param name="ppVtbl">Address to vtable. Usually the address of the object itself.</param>
public ObjectVTableHook(void* ppVtbl)
: base(ppVtbl, Fields.Length)
{
}
/// <summary>Gets the original vtable.</summary>
public ref readonly TVTable OriginalVTable => ref MemoryMarshal.Cast<nint, TVTable>(this.OriginalVTableSpan)[0];
/// <summary>Gets the overriden vtable.</summary>
public ref readonly TVTable OverridenVTable => ref MemoryMarshal.Cast<nint, TVTable>(this.OverridenVTableSpan)[0];
/// <summary>Gets the index of the method by method name.</summary>
/// <param name="methodName">Name of the method.</param>
/// <returns>Index of the method.</returns>
public int GetMethodIndex(string methodName) => Fields.IndexOf(methodName);
/// <summary>Gets the original method address of the given method index.</summary>
/// <param name="methodName">Name of the method.</param>
/// <returns>Address of the original method.</returns>
public nint GetOriginalMethodAddress(string methodName) =>
this.GetOriginalMethodAddress(this.GetMethodIndex(methodName));
/// <summary>Gets the original method of the given method index, as a delegate of given type.</summary>
/// <param name="methodName">Name of the method.</param>
/// <typeparam name="T">Type of delegate.</typeparam>
/// <returns>Delegate to the original method.</returns>
public T GetOriginalMethodDelegate<T>(string methodName)
where T : Delegate
=> this.GetOriginalMethodDelegate<T>(this.GetMethodIndex(methodName));
/// <summary>Resets a method to the original function.</summary>
/// <param name="methodName">Name of the method.</param>
public void ResetVtableEntry(string methodName)
=> this.ResetVtableEntry(this.GetMethodIndex(methodName));
/// <summary>Sets a method in vtable to the given address of function.</summary>
/// <param name="methodName">Name of the method.</param>
/// <param name="pfn">Address of the detour function.</param>
/// <param name="refkeep">Additional reference to keep in memory.</param>
public void SetVtableEntry(string methodName, nint pfn, object? refkeep)
=> this.SetVtableEntry(this.GetMethodIndex(methodName), pfn, refkeep);
/// <summary>Sets a method in vtable to the given delegate.</summary>
/// <param name="methodName">Name of the method.</param>
/// <param name="detourDelegate">Detour delegate.</param>
/// <typeparam name="T">Type of delegate.</typeparam>
public void SetVtableEntry<T>(string methodName, T detourDelegate)
where T : Delegate =>
this.SetVtableEntry(
this.GetMethodIndex(methodName),
Marshal.GetFunctionPointerForDelegate(detourDelegate),
detourDelegate);
/// <summary>Sets a method in vtable to the given delegate.</summary>
/// <param name="methodName">Name of the method.</param>
/// <param name="detourDelegate">Detour delegate.</param>
/// <param name="originalMethodDelegate">Original method delegate.</param>
/// <typeparam name="T">Type of delegate.</typeparam>
public void SetVtableEntry<T>(string methodName, T detourDelegate, out T originalMethodDelegate)
where T : Delegate
=> this.SetVtableEntry(this.GetMethodIndex(methodName), detourDelegate, out originalMethodDelegate);
/// <summary>Creates a new instance of <see cref="Hook{T}"/> that manages one entry in the vtable hook.</summary>
/// <param name="methodName">Name of the method.</param>
/// <param name="detourDelegate">Detour delegate.</param>
/// <typeparam name="T">Type of delegate.</typeparam>
/// <returns>A new instance of <see cref="Hook{T}"/>.</returns>
/// <remarks>Even if a single hook is enabled, without <see cref="ObjectVTableHook.Enable"/>, the hook will remain
/// disabled.</remarks>
public Hook<T> CreateHook<T>(string methodName, T detourDelegate) where T : Delegate =>
this.CreateHook(this.GetMethodIndex(methodName), detourDelegate);
}

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 ReShadeHandling.ReShadeAddonInterface.ApiObject swapchain)
{
var swapChain = swapchain.GetNative<IDXGISwapChain>();
if (this.scene?.SwapChain.NativePointer != (nint)swapChain)
return;
this.scene?.OnPreResize();
}
private unsafe void ReShadeAddonInterfaceOnInitSwapChain(ref ReShadeHandling.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 ReShadeHandling.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

@ -8,14 +8,19 @@ using System.Runtime.InteropServices;
using System.Threading;
using System.Threading.Tasks;
using CheapLoc;
using Dalamud.Configuration.Internal;
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;
using Dalamud.Interface.ImGuiNotification.Internal;
using Dalamud.Interface.Internal.ManagedAsserts;
using Dalamud.Interface.Internal.ReShadeHandling;
using Dalamud.Interface.ManagedFontAtlas;
using Dalamud.Interface.ManagedFontAtlas.Internals;
using Dalamud.Interface.Style;
@ -29,10 +34,12 @@ using ImGuiNET;
using ImGuiScene;
using JetBrains.Annotations;
using PInvoke;
using SharpDX;
using SharpDX.DXGI;
using TerraFX.Interop.DirectX;
using TerraFX.Interop.Windows;
// general dev notes, here because it's easiest
@ -52,7 +59,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.
@ -70,11 +77,19 @@ internal class InterfaceManager : IInternalDisposableService
private readonly ConcurrentBag<IDisposable> deferredDisposeDisposables = new();
[ServiceManager.ServiceDependency]
private readonly WndProcHookManager wndProcHookManager = Service<WndProcHookManager>.Get();
private readonly DalamudConfiguration dalamudConfiguration = Service<DalamudConfiguration>.Get();
[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();
[ServiceManager.ServiceDependency]
private readonly WndProcHookManager wndProcHookManager = Service<WndProcHookManager>.Get();
private readonly ConcurrentQueue<Action> runBeforeImGuiRender = new();
private readonly ConcurrentQueue<Action> runAfterImGuiRender = new();
@ -82,8 +97,9 @@ internal class InterfaceManager : IInternalDisposableService
private Hook<SetCursorDelegate>? setCursorHook;
private Hook<DxgiPresentDelegate>? dxgiPresentHook;
private Hook<ReshadeOnPresentDelegate>? reshadeOnPresentHook;
private Hook<ResizeBuffersDelegate>? resizeBuffersHook;
private ObjectVTableHook<IDXGISwapChain.Vtbl<IDXGISwapChain>>? swapChainHook;
private ReShadeAddonInterface? reShadeAddonInterface;
private IFontAtlas? dalamudAtlas;
private ILockedImFont? defaultFontResourceLock;
@ -101,9 +117,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 +312,9 @@ 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.swapChainHook, null)?.Dispose();
Interlocked.Exchange(ref this.reShadeAddonInterface, null)?.Dispose();
}
}
@ -431,11 +445,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 +478,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 +646,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 +733,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 +749,9 @@ internal class InterfaceManager : IInternalDisposableService
_ = this.dalamudAtlas.BuildFontsAsync();
SwapChainHelper.BusyWaitForGameDeviceSwapChain();
SwapChainHelper.DetectReShade();
var swapChainDesc = default(DXGI_SWAP_CHAIN_DESC);
if (SwapChainHelper.GameDeviceSwapChain->GetDesc(&swapChainDesc).SUCCEEDED)
this.gameWindowHandle = swapChainDesc.OutputWindow;
try
{
@ -838,53 +771,104 @@ internal class InterfaceManager : IInternalDisposableService
0,
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 (ReShadeAddonInterface.ReShadeHasSignature)
{
Log.Warning("Signed ReShade binary detected.");
Service<NotificationManager>
.GetAsync()
.ContinueWith(
nmt => nmt.Result.AddNotification(
new()
{
MinimizedText = Loc.Localize(
"ReShadeNoAddonSupportNotificationMinimizedText",
"Wrong ReShade installation"),
Content = Loc.Localize(
"ReShadeNoAddonSupportNotificationContent",
"Your installation of ReShade does not have full addon support, and may not work with Dalamud and/or the game.\n" +
"Download and install ReShade with full addon-support."),
Type = NotificationType.Warning,
InitialDuration = TimeSpan.MaxValue,
ShowIndeterminateIfNoExpiry = false,
}));
}
if (SwapChainHelper.ReshadeOnPresent is null)
Log.Verbose("===== S W A P C H A I N =====");
if (this.dalamudConfiguration.ReShadeHandlingMode == ReShadeHandlingMode.UnwrapReShade)
{
var addr = (nint)SwapChainHelper.GameDeviceSwapChainVtbl->Present;
this.dxgiPresentHook = Hook<DxgiPresentDelegate>.FromAddress(addr, this.PresentDetour);
Log.Verbose($"ReShade::DXGISwapChain::on_present address {Util.DescribeAddress(addr)}");
if (SwapChainHelper.UnwrapReShade())
Log.Verbose("Unwrapped ReShade.");
}
else
ResizeBuffersDelegate? resizeBuffersDelegate = null;
DxgiPresentDelegate? dxgiPresentDelegate = null;
if (this.dalamudConfiguration.ReShadeHandlingMode == ReShadeHandlingMode.ReShadeAddon)
{
var addr = (nint)SwapChainHelper.ReshadeOnPresent;
this.reshadeOnPresentHook = Hook<ReshadeOnPresentDelegate>.FromAddress(addr, this.ReshadeOnPresentDetour);
Log.Verbose($"IDXGISwapChain::Present address {Util.DescribeAddress(addr)}");
if (ReShadeAddonInterface.TryRegisterAddon(out this.reShadeAddonInterface))
{
resizeBuffersDelegate = this.AsReShadeAddonResizeBuffersDetour;
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;
}
}
if (resizeBuffersDelegate is null)
{
resizeBuffersDelegate = this.AsHookResizeBuffersDetour;
dxgiPresentDelegate = this.PresentDetour;
}
switch (this.dalamudConfiguration.SwapChainHookMode)
{
case SwapChainHelper.HookMode.ByteCode:
default:
{
this.resizeBuffersHook = Hook<ResizeBuffersDelegate>.FromAddress(
(nint)SwapChainHelper.GameDeviceSwapChainVtbl->ResizeBuffers,
resizeBuffersDelegate);
if (dxgiPresentDelegate is not null)
{
this.dxgiPresentHook = Hook<DxgiPresentDelegate>.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.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.dxgiPresentHook?.Enable();
this.swapChainHook?.Enable();
}
private IntPtr SetCursorDetour(IntPtr hCursor)

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,90 @@
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.ReShadeHandling;
/// <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;
fixed (void* pwszFile = m.FileName)
fixed (Guid* pguid = &WINTRUST_ACTION_GENERIC_VERIFY_V2)
{
var wtfi = new WINTRUST_FILE_INFO
{
cbStruct = (uint)sizeof(WINTRUST_FILE_INFO),
pcwszFilePath = (ushort*)pwszFile,
hFile = default,
pgKnownSubject = null,
};
var wtd = new WINTRUST_DATA
{
cbStruct = (uint)sizeof(WINTRUST_DATA),
pPolicyCallbackData = null,
pSIPClientData = null,
dwUIChoice = WTD.WTD_UI_NONE,
fdwRevocationChecks = WTD.WTD_REVOKE_NONE,
dwUnionChoice = WTD.WTD_STATEACTION_VERIFY,
hWVTStateData = default,
pwszURLReference = null,
dwUIContext = 0,
pFile = &wtfi,
};
ReShadeHasSignature = WinVerifyTrust(default, pguid, &wtd) != TRUST.TRUST_E_NOSIGNATURE;
}
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; }
/// <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>
public static bool ReShadeHasSignature { 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,178 @@
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.ReShadeHandling;
/// <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.");
// https://github.com/crosire/reshade/commit/eaaa2a2c5adf5749ad17b358305da3f2d0f6baf4
// TODO: when ReShade gets a proper release with this commit, make this hook optional
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="ReShadeAddonInterface.AddonEvent.ReShadeOverlay"/>.</summary>
/// <param name="effectRuntime">Reference to the ReShade runtime.</param>
public delegate void ReShadeOverlayDelegate(ref ApiObject effectRuntime);
/// <summary>Delegate for <see cref="ReShadeAddonInterface.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="ReShadeAddonInterface.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="ReShadeAddonInterface.AddonEvent.ReShadeOverlay"/>.</summary>
public event ReShadeOverlayDelegate? ReShadeOverlay;
/// <summary>Called on <see cref="ReShadeAddonInterface.AddonEvent.InitSwapChain"/>.</summary>
public event ReShadeInitSwapChain? InitSwapChain;
/// <summary>Called on <see cref="ReShadeAddonInterface.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

@ -0,0 +1,14 @@
namespace Dalamud.Interface.Internal.ReShadeHandling;
/// <summary>Available handling modes for working with ReShade.</summary>
internal enum ReShadeHandlingMode
{
/// <summary>Register as a ReShade addon, and draw on reshade_overlay event.</summary>
ReShadeAddon,
/// <summary>Unwraps ReShade from the swap chain obtained from the game.</summary>
UnwrapReShade,
/// <summary>Do not do anything special about it. ReShade will process Dalamud rendered stuff.</summary>
None = -1,
}

View file

@ -0,0 +1,194 @@
using System.Diagnostics;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using TerraFX.Interop.Windows;
using static TerraFX.Interop.Windows.Windows;
namespace Dalamud.Interface.Internal.ReShadeHandling;
/// <summary>Unwraps IUnknown wrapped by ReShade.</summary>
internal static unsafe class ReShadeUnwrapper
{
/// <summary>Unwraps <typeparamref name="T"/> if it is wrapped by ReShade.</summary>
/// <param name="comptr">[inout] The COM pointer to an instance of <typeparamref name="T"/>.</param>
/// <typeparam name="T">A COM type that is or extends <see cref="IUnknown"/>.</typeparam>
/// <returns><c>true</c> if peeled.</returns>
public static bool Unwrap<T>(ComPtr<T>* comptr)
where T : unmanaged, IUnknown.Interface
{
if (typeof(T).GetNestedType("Vtbl`1") is not { } vtblType)
return false;
nint vtblSize = vtblType.GetFields().Length * sizeof(nint);
var changed = false;
while (comptr->Get() != null && IsReShadedComObject(comptr->Get()))
{
// Expectation: the pointer to the underlying object should come early after the overriden vtable.
for (nint i = sizeof(nint); i <= 0x20; i += sizeof(nint))
{
var ppObjectBehind = (nint)comptr->Get() + i;
// Is the thing directly pointed from the address an actual something in the memory?
if (!IsValidReadableMemoryAddress(ppObjectBehind, 8))
continue;
var pObjectBehind = *(nint*)ppObjectBehind;
// Is the address of vtable readable?
if (!IsValidReadableMemoryAddress(pObjectBehind, sizeof(nint)))
continue;
var pObjectBehindVtbl = *(nint*)pObjectBehind;
// Is the vtable itself readable?
if (!IsValidReadableMemoryAddress(pObjectBehindVtbl, vtblSize))
continue;
// Are individual functions in vtable executable?
var valid = true;
for (var j = 0; valid && j < vtblSize; j += sizeof(nint))
valid &= IsValidExecutableMemoryAddress(*(nint*)(pObjectBehindVtbl + j), 1);
if (!valid)
continue;
// Interpret the object as an IUnknown.
// Note that `using` is not used, and `Attach` is used. We do not alter the reference count yet.
var punk = default(ComPtr<IUnknown>);
punk.Attach((IUnknown*)pObjectBehind);
// Is the IUnknown object also the type we want?
using var comptr2 = default(ComPtr<T>);
if (punk.As(&comptr2).FAILED)
continue;
comptr2.Swap(comptr);
changed = true;
break;
}
if (!changed)
break;
}
return changed;
}
private static bool BelongsInReShadeDll(nint ptr)
{
foreach (ProcessModule processModule in Process.GetCurrentProcess().Modules)
{
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;
return true;
}
return false;
static bool HasProcExported(ProcessModule m, ReadOnlySpan<byte> name)
{
fixed (byte* p = name)
return GetProcAddress((HMODULE)m.BaseAddress, (sbyte*)p) != 0;
}
}
private static bool IsReShadedComObject<T>(T* obj)
where T : unmanaged, IUnknown.Interface
{
if (!IsValidReadableMemoryAddress((nint)obj, sizeof(nint)))
return false;
try
{
var vtbl = (nint**)Marshal.ReadIntPtr((nint)obj);
if (!IsValidReadableMemoryAddress((nint)vtbl, sizeof(nint) * 3))
return false;
for (var i = 0; i < 3; i++)
{
var pfn = Marshal.ReadIntPtr((nint)(vtbl + i));
if (!IsValidExecutableMemoryAddress(pfn, 1))
return false;
if (!BelongsInReShadeDll(pfn))
return false;
}
return true;
}
catch
{
return false;
}
}
private static bool IsValidReadableMemoryAddress(nint p, nint size)
{
while (size > 0)
{
if (!IsValidUserspaceMemoryAddress(p))
return false;
MEMORY_BASIC_INFORMATION mbi;
if (VirtualQuery((void*)p, &mbi, (nuint)sizeof(MEMORY_BASIC_INFORMATION)) == 0)
return false;
if (mbi is not
{
State: MEM.MEM_COMMIT,
Protect: PAGE.PAGE_READONLY or PAGE.PAGE_READWRITE or PAGE.PAGE_EXECUTE_READ
or PAGE.PAGE_EXECUTE_READWRITE,
})
return false;
var regionSize = (nint)((mbi.RegionSize + 0xFFFUL) & ~0x1000UL);
var checkedSize = ((nint)mbi.BaseAddress + regionSize) - p;
size -= checkedSize;
p += checkedSize;
}
return true;
}
private static bool IsValidExecutableMemoryAddress(nint p, nint size)
{
while (size > 0)
{
if (!IsValidUserspaceMemoryAddress(p))
return false;
MEMORY_BASIC_INFORMATION mbi;
if (VirtualQuery((void*)p, &mbi, (nuint)sizeof(MEMORY_BASIC_INFORMATION)) == 0)
return false;
if (mbi is not
{
State: MEM.MEM_COMMIT,
Protect: PAGE.PAGE_EXECUTE or PAGE.PAGE_EXECUTE_READ or PAGE.PAGE_EXECUTE_READWRITE
or PAGE.PAGE_EXECUTE_WRITECOPY,
})
return false;
var regionSize = (nint)((mbi.RegionSize + 0xFFFUL) & ~0x1000UL);
var checkedSize = ((nint)mbi.BaseAddress + regionSize) - p;
size -= checkedSize;
p += checkedSize;
}
return true;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static bool IsValidUserspaceMemoryAddress(nint p)
{
// https://learn.microsoft.com/en-us/windows-hardware/drivers/gettingstarted/virtual-address-spaces
// A 64-bit process on 64-bit Windows has a virtual address space within the 128-terabyte range
// 0x000'00000000 through 0x7FFF'FFFFFFFF.
return p >= 0x10000 && p <= unchecked((nint)0x7FFF_FFFFFFFFUL);
}
}

View file

@ -1,13 +1,9 @@
using System.Diagnostics;
using System.Threading;
using Dalamud.Game;
using Dalamud.Utility;
using Dalamud.Interface.Internal.ReShadeHandling;
using FFXIVClientStructs.FFXIV.Client.Graphics.Kernel;
using Serilog;
using TerraFX.Interop.DirectX;
using TerraFX.Interop.Windows;
@ -16,11 +12,17 @@ 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; }
private static IDXGISwapChain* foundGameDeviceSwapChain;
/// <summary>Describes how to hook <see cref="IDXGISwapChain"/> methods.</summary>
public enum HookMode
{
/// <summary>Hooks by rewriting the native bytecode.</summary>
ByteCode,
/// <summary>Hooks by providing an alternative vtable.</summary>
VTable,
}
/// <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>
@ -28,6 +30,9 @@ internal static unsafe class SwapChainHelper
{
get
{
if (foundGameDeviceSwapChain is not null)
return foundGameDeviceSwapChain;
var kernelDev = Device.Instance();
if (kernelDev == null)
return null;
@ -41,7 +46,7 @@ internal static unsafe class SwapChainHelper
if (swapChain->BackBuffer == null)
return null;
return (IDXGISwapChain*)swapChain->DXGISwapChain;
return foundGameDeviceSwapChain = (IDXGISwapChain*)swapChain->DXGISwapChain;
}
}
@ -93,101 +98,17 @@ internal static unsafe class SwapChainHelper
Thread.Yield();
}
/// <summary>Detects ReShade and populate <see cref="ReshadeOnPresent"/>.</summary>
public static void DetectReShade()
/// <summary>
/// Make <see cref="GameDeviceSwapChain"/> store address of unwrapped swap chain, if it was wrapped with ReShade.
/// </summary>
/// <returns><c>true</c> if it was wrapped with ReShade.</returns>
public static bool UnwrapReShade()
{
var modules = Process.GetCurrentProcess().Modules;
foreach (ProcessModule processModule in modules)
{
if (!processModule.FileName.EndsWith("game\\dxgi.dll", StringComparison.InvariantCultureIgnoreCase))
continue;
using var swapChain = new ComPtr<IDXGISwapChain>(GameDeviceSwapChain);
if (!ReShadeUnwrapper.Unwrap(&swapChain))
return false;
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;
}
}
foundGameDeviceSwapChain = swapChain.Get();
return true;
}
}

View file

@ -1,8 +1,10 @@
using System.Diagnostics.CodeAnalysis;
using CheapLoc;
using Dalamud.Configuration.Internal;
using Dalamud.Interface.Colors;
using Dalamud.Interface.Internal.ReShadeHandling;
using Dalamud.Interface.Internal.Windows.PluginInstaller;
using Dalamud.Interface.Internal.Windows.Settings.Widgets;
using Dalamud.Interface.Utility;
@ -11,28 +13,39 @@ using Dalamud.Utility;
namespace Dalamud.Interface.Internal.Windows.Settings.Tabs;
[SuppressMessage("StyleCop.CSharp.DocumentationRules", "SA1600:Elements should be documented", Justification = "Internals")]
[SuppressMessage(
"StyleCop.CSharp.DocumentationRules",
"SA1600:Elements should be documented",
Justification = "Internals")]
public class SettingsTabExperimental : SettingsTab
{
public override SettingsEntry[] Entries { get; } =
{
[
new SettingsEntry<bool>(
Loc.Localize("DalamudSettingsPluginTest", "Get plugin testing builds"),
string.Format(
Loc.Localize("DalamudSettingsPluginTestHint", "Receive testing prereleases for selected plugins.\nTo opt-in to testing builds for a plugin, you have to right click it in the \"{0}\" tab of the plugin installer and select \"{1}\"."),
Loc.Localize(
"DalamudSettingsPluginTestHint",
"Receive testing prereleases for selected plugins.\nTo opt-in to testing builds for a plugin, you have to right click it in the \"{0}\" tab of the plugin installer and select \"{1}\"."),
PluginCategoryManager.Locs.Group_Installed,
PluginInstallerWindow.Locs.PluginContext_TestingOptIn),
c => c.DoPluginTest,
(v, c) => c.DoPluginTest = v),
new HintSettingsEntry(
Loc.Localize("DalamudSettingsPluginTestWarning", "Testing plugins may contain bugs or crash your game. Please only enable this if you are aware of the risks."),
Loc.Localize(
"DalamudSettingsPluginTestWarning",
"Testing plugins may contain bugs or crash your game. Please only enable this if you are aware of the risks."),
ImGuiColors.DalamudRed),
new GapSettingsEntry(5),
new SettingsEntry<bool>(
Loc.Localize("DalamudSettingEnablePluginUIAdditionalOptions", "Add a button to the title bar of plugin windows to open additional options"),
Loc.Localize("DalamudSettingEnablePluginUIAdditionalOptionsHint", "This will allow you to pin certain plugin windows, make them clickthrough or adjust their opacity.\nThis may not be supported by all of your plugins. Contact the plugin author if you want them to support this feature."),
Loc.Localize(
"DalamudSettingEnablePluginUIAdditionalOptions",
"Add a button to the title bar of plugin windows to open additional options"),
Loc.Localize(
"DalamudSettingEnablePluginUIAdditionalOptionsHint",
"This will allow you to pin certain plugin windows, make them clickthrough or adjust their opacity.\nThis may not be supported by all of your plugins. Contact the plugin author if you want them to support this feature."),
c => c.EnablePluginUiAdditionalOptions,
(v, c) => c.EnablePluginUiAdditionalOptions = v),
@ -40,7 +53,9 @@ public class SettingsTabExperimental : SettingsTab
new ButtonSettingsEntry(
Loc.Localize("DalamudSettingsClearHidden", "Clear hidden plugins"),
Loc.Localize("DalamudSettingsClearHiddenHint", "Restore plugins you have previously hidden from the plugin installer."),
Loc.Localize(
"DalamudSettingsClearHiddenHint",
"Restore plugins you have previously hidden from the plugin installer."),
() =>
{
Service<DalamudConfiguration>.Get().HiddenPluginInternalName.Clear();
@ -55,6 +70,56 @@ public class SettingsTabExperimental : SettingsTab
new ThirdRepoSettingsEntry(),
new GapSettingsEntry(5, true),
new EnumSettingsEntry<ReShadeHandlingMode>(
Loc.Localize("DalamudSettingsReShadeHandlingMode", "ReShade handling mode"),
Loc.Localize(
"DalamudSettingsReShadeHandlingModeHint",
"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)
{
FriendlyEnumNameGetter = x => x switch
{
ReShadeHandlingMode.ReShadeAddon => Loc.Localize(
"DalamudSettingsReShadeHandlingModeReShadeAddon",
"ReShade addon"),
ReShadeHandlingMode.UnwrapReShade => Loc.Localize(
"DalamudSettingsReShadeHandlingModeUnwrapReShade",
"Unwrap ReShade"),
ReShadeHandlingMode.None => Loc.Localize(
"DalamudSettingsReShadeHandlingModeNone",
"Do not handle"),
_ => "<invalid>",
},
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.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.None => Loc.Localize(
"DalamudSettingsReShadeHandlingModeNoneDescription",
"No special handling will be done for ReShade. Dalamud will be under the effect of ReShade postprocessing."),
_ => "<invalid>",
},
},
new GapSettingsEntry(5, true),
new EnumSettingsEntry<SwapChainHelper.HookMode>(
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),
@ -64,7 +129,7 @@ public class SettingsTabExperimental : SettingsTab
c => c.ProfilesEnabled,
(v, c) => c.ProfilesEnabled = v),
*/
};
];
public override string Title => Loc.Localize("DalamudSettingsExperimental", "Experimental");
@ -72,7 +137,9 @@ public class SettingsTabExperimental : SettingsTab
{
base.Draw();
ImGuiHelpers.SafeTextColoredWrapped(ImGuiColors.DalamudGrey, "Total memory used by Dalamud & Plugins: " + Util.FormatBytes(GC.GetTotalMemory(false)));
ImGuiHelpers.SafeTextColoredWrapped(
ImGuiColors.DalamudGrey,
"Total memory used by Dalamud & Plugins: " + Util.FormatBytes(GC.GetTotalMemory(false)));
ImGuiHelpers.ScaledDummy(15);
}
}

View file

@ -15,7 +15,7 @@ public class SettingsTabGeneral : SettingsTab
new GapSettingsEntry(5),
new SettingsEntry<XivChatType>(
new EnumSettingsEntry<XivChatType>(
Loc.Localize("DalamudSettingsChannel", "Dalamud Chat Channel"),
Loc.Localize("DalamudSettingsChannelHint", "Select the chat channel that is to be used for general Dalamud messages."),
c => c.GeneralChatType,

View file

@ -0,0 +1,175 @@
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using Dalamud.Configuration.Internal;
using Dalamud.Interface.Colors;
using Dalamud.Interface.Utility;
using Dalamud.Interface.Utility.Raii;
using ImGuiNET;
namespace Dalamud.Interface.Internal.Windows.Settings.Widgets;
[SuppressMessage("StyleCop.CSharp.DocumentationRules", "SA1600:Elements should be documented", Justification = "Internals")]
internal sealed class EnumSettingsEntry<T> : SettingsEntry
where T : struct, Enum
{
private readonly LoadSettingDelegate load;
private readonly SaveSettingDelegate save;
private readonly Action<T>? change;
private readonly T fallbackValue;
private T valueBacking;
public EnumSettingsEntry(
string name,
string description,
LoadSettingDelegate load,
SaveSettingDelegate save,
Action<T>? change = null,
Func<T, string?>? warning = null,
Func<T, string?>? validity = null,
Func<bool>? visibility = null,
T fallbackValue = default)
{
this.load = load;
this.save = save;
this.change = change;
this.Name = name;
this.Description = description;
this.CheckWarning = warning;
this.CheckValidity = validity;
this.CheckVisibility = visibility;
this.fallbackValue = fallbackValue;
}
public delegate T LoadSettingDelegate(DalamudConfiguration config);
public delegate void SaveSettingDelegate(T value, DalamudConfiguration config);
public T Value
{
get => this.valueBacking;
set
{
if (Equals(value, this.valueBacking))
return;
this.valueBacking = value;
this.change?.Invoke(value);
}
}
public string Description { get; }
public Action<EnumSettingsEntry<T>>? CustomDraw { get; init; }
public Func<T, string?>? CheckValidity { get; init; }
public Func<T, string?>? CheckWarning { get; init; }
public Func<bool>? CheckVisibility { get; init; }
public Func<T, string> FriendlyEnumNameGetter { get; init; } = x => x.ToString();
public Func<T, string> FriendlyEnumDescriptionGetter { get; init; } = _ => string.Empty;
public override bool IsVisible => this.CheckVisibility?.Invoke() ?? true;
public override void Draw()
{
Debug.Assert(this.Name != null, "this.Name != null");
if (this.CustomDraw is not null)
{
this.CustomDraw.Invoke(this);
}
else
{
ImGuiHelpers.SafeTextWrapped(this.Name);
var idx = this.valueBacking;
var values = Enum.GetValues<T>();
if (!values.Contains(idx))
{
idx = Enum.IsDefined(this.fallbackValue)
? this.fallbackValue
: throw new InvalidOperationException("No fallback value for enum");
this.valueBacking = idx;
}
if (ImGui.BeginCombo($"###{this.Id.ToString()}", this.FriendlyEnumNameGetter(idx)))
{
foreach (var value in values)
{
if (ImGui.Selectable(this.FriendlyEnumNameGetter(value), idx.Equals(value)))
{
this.valueBacking = value;
}
}
ImGui.EndCombo();
}
}
using (ImRaii.PushColor(ImGuiCol.Text, ImGuiColors.DalamudGrey))
{
var desc = this.FriendlyEnumDescriptionGetter(this.valueBacking);
if (!string.IsNullOrWhiteSpace(desc))
{
ImGuiHelpers.SafeTextWrapped(desc);
ImGuiHelpers.ScaledDummy(2);
}
ImGuiHelpers.SafeTextWrapped(this.Description);
}
if (this.CheckValidity != null)
{
var validityMsg = this.CheckValidity.Invoke(this.Value);
this.IsValid = string.IsNullOrEmpty(validityMsg);
if (!this.IsValid)
{
using (ImRaii.PushColor(ImGuiCol.Text, ImGuiColors.DalamudRed))
{
ImGui.Text(validityMsg);
}
}
}
else
{
this.IsValid = true;
}
var warningMessage = this.CheckWarning?.Invoke(this.Value);
if (warningMessage != null)
{
using (ImRaii.PushColor(ImGuiCol.Text, ImGuiColors.DalamudRed))
{
ImGui.Text(warningMessage);
}
}
}
public override void Load()
{
this.valueBacking = this.load(Service<DalamudConfiguration>.Get());
if (this.CheckValidity != null)
{
this.IsValid = this.CheckValidity(this.Value) == null;
}
else
{
this.IsValid = true;
}
}
public override void Save() => this.save(this.Value, Service<DalamudConfiguration>.Get());
}

View file

@ -1,15 +1,13 @@
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.IO;
using System.Linq;
using System.Numerics;
using Dalamud.Configuration.Internal;
using Dalamud.Interface.Colors;
using Dalamud.Interface.Utility;
using Dalamud.Interface.Utility.Raii;
using Dalamud.Utility;
using ImGuiNET;
namespace Dalamud.Interface.Internal.Windows.Settings.Widgets;
@ -22,7 +20,6 @@ internal sealed class SettingsEntry<T> : SettingsEntry
private readonly Action<T?>? change;
private object? valueBacking;
private object? fallbackValue;
public SettingsEntry(
string name,
@ -32,8 +29,7 @@ internal sealed class SettingsEntry<T> : SettingsEntry
Action<T?>? change = null,
Func<T?, string?>? warning = null,
Func<T?, string?>? validity = null,
Func<bool>? visibility = null,
object? fallbackValue = null)
Func<bool>? visibility = null)
{
this.load = load;
this.save = save;
@ -43,8 +39,6 @@ internal sealed class SettingsEntry<T> : SettingsEntry
this.CheckWarning = warning;
this.CheckValidity = validity;
this.CheckVisibility = visibility;
this.fallbackValue = fallbackValue;
}
public delegate T? LoadSettingDelegate(DalamudConfiguration config);
@ -118,34 +112,6 @@ internal sealed class SettingsEntry<T> : SettingsEntry
this.change?.Invoke(this.Value);
}
}
else if (type.IsEnum)
{
ImGuiHelpers.SafeTextWrapped(this.Name);
var idx = (Enum)(this.valueBacking ?? 0);
var values = Enum.GetValues(type);
var descriptions =
values.Cast<Enum>().ToDictionary(x => x, x => x.GetAttribute<SettingsAnnotationAttribute>() ?? new SettingsAnnotationAttribute(x.ToString(), string.Empty));
if (!descriptions.ContainsKey(idx))
{
idx = (Enum)this.fallbackValue ?? throw new Exception("No fallback value for enum");
this.valueBacking = idx;
}
if (ImGui.BeginCombo($"###{this.Id.ToString()}", descriptions[idx].FriendlyName))
{
foreach (Enum value in values)
{
if (ImGui.Selectable(descriptions[value].FriendlyName, idx.Equals(value)))
{
this.valueBacking = value;
}
}
ImGui.EndCombo();
}
}
using (ImRaii.PushColor(ImGuiCol.Text, ImGuiColors.DalamudGrey))
{
@ -197,18 +163,3 @@ internal sealed class SettingsEntry<T> : SettingsEntry
public override void Save() => this.save(this.Value, Service<DalamudConfiguration>.Get());
}
[SuppressMessage("StyleCop.CSharp.DocumentationRules", "SA1600:Elements should be documented", Justification = "Internals")]
[AttributeUsage(AttributeTargets.Field)]
internal class SettingsAnnotationAttribute : Attribute
{
public SettingsAnnotationAttribute(string friendlyName, string description)
{
this.FriendlyName = friendlyName;
this.Description = description;
}
public string FriendlyName { get; set; }
public string Description { get; set; }
}

View file

@ -8,7 +8,7 @@ namespace Dalamud.Support;
/// <summary>Tracks the loaded process modules.</summary>
internal static unsafe partial class CurrentProcessModules
{
private static Process? process;
private static ProcessModuleCollection? moduleCollection;
/// <summary>Gets all the loaded modules, up to date.</summary>
public static ProcessModuleCollection ModuleCollection
@ -19,13 +19,13 @@ internal static unsafe partial class CurrentProcessModules
if (t != 0)
{
t = 0;
process = null;
moduleCollection = null;
Log.Verbose("{what}: Fetching fresh copy of current process modules.", nameof(CurrentProcessModules));
}
try
{
return (process ??= Process.GetCurrentProcess()).Modules;
return moduleCollection ??= Process.GetCurrentProcess().Modules;
}
catch (Exception e)
{

View file

@ -269,7 +269,7 @@ public static class Util
{
if ((mbi.Protect & (1 << i)) == 0)
continue;
if (c++ == 0)
if (c++ != 0)
sb.Append(" | ");
sb.Append(PageProtectionFlagNames[i]);
}