Describe address for debugging, change PresentDetour viewport handling (#1943)

* Describe memory address when printed in log/debug utilities

* PresentDetour: Compare against game's internal copy of IDXGISwapChain

* Handle ReShade on_present function signature properly
This commit is contained in:
srkizer 2024-07-19 04:28:25 +09:00 committed by GitHub
parent eed7abed12
commit 1109e64552
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
26 changed files with 445 additions and 591 deletions

View file

@ -4,6 +4,7 @@ using System.Collections.Generic;
using Dalamud.IoC;
using Dalamud.IoC.Internal;
using Dalamud.Plugin.Services;
using Dalamud.Utility;
using FFXIVClientStructs.FFXIV.Client.Game.UI;
using Serilog;
@ -28,7 +29,7 @@ internal sealed unsafe partial class AetheryteList : IServiceType, IAetheryteLis
[ServiceManager.ServiceConstructor]
private AetheryteList()
{
Log.Verbose($"Teleport address 0x{((nint)this.telepoInstance).ToInt64():X}");
Log.Verbose($"Teleport address {Util.DescribeAddress(this.telepoInstance)}");
}
/// <inheritdoc/>

View file

@ -6,6 +6,7 @@ using System.Runtime.InteropServices;
using Dalamud.IoC;
using Dalamud.IoC.Internal;
using Dalamud.Plugin.Services;
using Dalamud.Utility;
using Serilog;
@ -34,7 +35,7 @@ internal sealed partial class BuddyList : IServiceType, IBuddyList
{
this.address = this.clientState.AddressResolver;
Log.Verbose($"Buddy list address 0x{this.address.BuddyList.ToInt64():X}");
Log.Verbose($"Buddy list address {Util.DescribeAddress(this.address.BuddyList)}");
}
/// <inheritdoc/>

View file

@ -54,7 +54,7 @@ internal sealed class ClientState : IInternalDisposableService, IClientState
this.ClientLanguage = (ClientLanguage)dalamud.StartInfo.Language;
Log.Verbose($"SetupTerritoryType address 0x{this.address.SetupTerritoryType.ToInt64():X}");
Log.Verbose($"SetupTerritoryType address {Util.DescribeAddress(this.address.SetupTerritoryType)}");
this.setupTerritoryTypeHook = Hook<SetupTerritoryTypeDelegate>.FromAddress(this.address.SetupTerritoryType, this.SetupTerritoryTypeDetour);

View file

@ -4,6 +4,7 @@ using System.Collections.Generic;
using Dalamud.IoC;
using Dalamud.IoC.Internal;
using Dalamud.Plugin.Services;
using Dalamud.Utility;
using Serilog;
@ -26,7 +27,7 @@ internal sealed partial class FateTable : IServiceType, IFateTable
{
this.address = clientState.AddressResolver;
Log.Verbose($"Fate table address 0x{this.address.FateTablePtr.ToInt64():X}");
Log.Verbose($"Fate table address {Util.DescribeAddress(this.address.FateTablePtr)}");
}
/// <inheritdoc/>

View file

@ -4,6 +4,7 @@ using Dalamud.Hooking;
using Dalamud.IoC;
using Dalamud.IoC.Internal;
using Dalamud.Plugin.Services;
using Dalamud.Utility;
using ImGuiNET;
using Serilog;
@ -35,7 +36,7 @@ internal unsafe class GamepadState : IInternalDisposableService, IGamepadState
private GamepadState(ClientState clientState)
{
var resolver = clientState.AddressResolver;
Log.Verbose($"GamepadPoll address 0x{resolver.GamepadPoll.ToInt64():X}");
Log.Verbose($"GamepadPoll address {Util.DescribeAddress(resolver.GamepadPoll)}");
this.gamepadPoll = Hook<ControllerPoll>.FromAddress(resolver.GamepadPoll, this.GamepadPollDetour);
this.gamepadPoll?.Enable();
}

View file

@ -5,6 +5,7 @@ using Dalamud.Game.ClientState.JobGauge.Types;
using Dalamud.IoC;
using Dalamud.IoC.Internal;
using Dalamud.Plugin.Services;
using Dalamud.Utility;
using Serilog;
@ -27,7 +28,7 @@ internal class JobGauges : IServiceType, IJobGauges
{
this.Address = clientState.AddressResolver.JobGaugeData;
Log.Verbose($"JobGaugeData address 0x{this.Address.ToInt64():X}");
Log.Verbose($"JobGaugeData address {Util.DescribeAddress(this.Address)}");
}
/// <inheritdoc/>

View file

@ -5,6 +5,7 @@ using System.Runtime.InteropServices;
using Dalamud.IoC;
using Dalamud.IoC.Internal;
using Dalamud.Plugin.Services;
using Dalamud.Utility;
using Serilog;
@ -45,7 +46,7 @@ internal class KeyState : IServiceType, IKeyState
this.bufferBase = moduleBaseAddress + Marshal.ReadInt32(addressResolver.KeyboardState);
this.indexBase = moduleBaseAddress + Marshal.ReadInt32(addressResolver.KeyboardStateIndexArray);
Log.Verbose($"Keyboard state buffer address 0x{this.bufferBase.ToInt64():X}");
Log.Verbose($"Keyboard state buffer address {Util.DescribeAddress(this.bufferBase)}");
}
/// <inheritdoc/>

View file

@ -53,7 +53,7 @@ internal sealed partial class ObjectTable : IServiceType, IObjectTable
for (var i = 0; i < this.frameworkThreadEnumerators.Length; i++)
this.frameworkThreadEnumerators[i] = new(this, i);
Log.Verbose($"Object table address 0x{this.clientState.AddressResolver.ObjectTable.ToInt64():X}");
Log.Verbose($"Object table address {Util.DescribeAddress(this.clientState.AddressResolver.ObjectTable)}");
}
/// <inheritdoc/>

View file

@ -6,6 +6,7 @@ using System.Runtime.InteropServices;
using Dalamud.IoC;
using Dalamud.IoC.Internal;
using Dalamud.Plugin.Services;
using Dalamud.Utility;
using Serilog;
@ -34,7 +35,7 @@ internal sealed unsafe partial class PartyList : IServiceType, IPartyList
{
this.address = this.clientState.AddressResolver;
Log.Verbose($"Group manager address 0x{this.address.GroupManager.ToInt64():X}");
Log.Verbose($"Group manager address {Util.DescribeAddress(this.address.GroupManager)}");
}
/// <inheritdoc/>

View file

@ -55,11 +55,11 @@ internal sealed unsafe class GameGui : IInternalDisposableService, IGameGui
this.address.Setup(sigScanner);
Log.Verbose("===== G A M E G U I =====");
Log.Verbose($"GameGuiManager address 0x{this.address.BaseAddress.ToInt64():X}");
Log.Verbose($"SetGlobalBgm address 0x{this.address.SetGlobalBgm.ToInt64():X}");
Log.Verbose($"HandleItemHover address 0x{this.address.HandleItemHover.ToInt64():X}");
Log.Verbose($"HandleItemOut address 0x{this.address.HandleItemOut.ToInt64():X}");
Log.Verbose($"HandleImm address 0x{this.address.HandleImm.ToInt64():X}");
Log.Verbose($"GameGuiManager address {Util.DescribeAddress(this.address.BaseAddress)}");
Log.Verbose($"SetGlobalBgm address {Util.DescribeAddress(this.address.SetGlobalBgm)}");
Log.Verbose($"HandleItemHover address {Util.DescribeAddress(this.address.HandleItemHover)}");
Log.Verbose($"HandleItemOut address {Util.DescribeAddress(this.address.HandleItemOut)}");
Log.Verbose($"HandleImm address {Util.DescribeAddress(this.address.HandleImm)}");
this.setGlobalBgmHook = Hook<SetGlobalBgmDelegate>.FromAddress(this.address.SetGlobalBgm, this.HandleSetGlobalBgmDetour);

View file

@ -1,5 +1,7 @@
using System.Collections.Generic;
using Dalamud.Utility;
#if !DEBUG
using Dalamud.Configuration.Internal;
#endif
@ -30,7 +32,7 @@ internal sealed class AntiDebug : IInternalDisposableService
this.debugCheckAddress = IntPtr.Zero;
}
Log.Verbose($"Debug check address 0x{this.debugCheckAddress.ToInt64():X}");
Log.Verbose($"Debug check address {Util.DescribeAddress(this.debugCheckAddress)}");
if (!this.IsEnabled)
{
@ -65,7 +67,7 @@ internal sealed class AntiDebug : IInternalDisposableService
this.original = new byte[this.nop.Length];
if (this.debugCheckAddress != IntPtr.Zero && !this.IsEnabled)
{
Log.Information($"Overwriting debug check at 0x{this.debugCheckAddress.ToInt64():X}");
Log.Information($"Overwriting debug check at {Util.DescribeAddress(this.debugCheckAddress)}");
SafeMemory.ReadBytes(this.debugCheckAddress, this.nop.Length, out this.original);
SafeMemory.WriteBytes(this.debugCheckAddress, this.nop);
}
@ -87,7 +89,7 @@ internal sealed class AntiDebug : IInternalDisposableService
if (this.debugCheckAddress != IntPtr.Zero && this.original != null)
{
Log.Information($"Reverting debug check at 0x{this.debugCheckAddress.ToInt64():X}");
Log.Information($"Reverting debug check at {Util.DescribeAddress(this.debugCheckAddress)}");
SafeMemory.WriteBytes(this.debugCheckAddress, this.original);
}
else

View file

@ -1,226 +0,0 @@
namespace Dalamud.Game.Internal.DXGI.Definitions;
/// <summary>
/// Contains a full list of ID3D11Device functions to be used as an indexer into the DirectX Virtual Function Table entries.
/// </summary>
internal enum ID3D11DeviceVtbl
{
// IUnknown
/// <summary>
/// IUnknown::QueryInterface method (unknwn.h).
/// </summary>
QueryInterface = 0,
/// <summary>
/// IUnknown::AddRef method (unknwn.h).
/// </summary>
AddRef = 1,
/// <summary>
/// IUnknown::Release method (unknwn.h).
/// </summary>
Release = 2,
// ID3D11Device
/// <summary>
/// ID3D11Device::CreateBuffer method (d3d11.h).
/// </summary>
CreateBuffer = 3,
/// <summary>
/// ID3D11Device::CreateTexture1D method (d3d11.h).
/// </summary>
CreateTexture1D = 4,
/// <summary>
/// ID3D11Device::CreateTexture2D method (d3d11.h).
/// </summary>
CreateTexture2D = 5,
/// <summary>
/// ID3D11Device::CreateTexture3D method (d3d11.h).
/// </summary>
CreateTexture3D = 6,
/// <summary>
/// ID3D11Device::CreateShaderResourceView method (d3d11.h).
/// </summary>
CreateShaderResourceView = 7,
/// <summary>
/// ID3D11Device::CreateUnorderedAccessView method (d3d11.h).
/// </summary>
CreateUnorderedAccessView = 8,
/// <summary>
/// ID3D11Device::CreateRenderTargetView method (d3d11.h).
/// </summary>
CreateRenderTargetView = 9,
/// <summary>
/// ID3D11Device::CreateDepthStencilView method (d3d11.h).
/// </summary>
CreateDepthStencilView = 10,
/// <summary>
/// ID3D11Device::CreateInputLayout method (d3d11.h).
/// </summary>
CreateInputLayout = 11,
/// <summary>
/// ID3D11Device::CreateVertexShader method (d3d11.h).
/// </summary>
CreateVertexShader = 12,
/// <summary>
/// ID3D11Device::CreateGeometryShader method (d3d11.h).
/// </summary>
CreateGeometryShader = 13,
/// <summary>
/// ID3D11Device::CreateGeometryShaderWithStreamOutput method (d3d11.h).
/// </summary>
CreateGeometryShaderWithStreamOutput = 14,
/// <summary>
/// ID3D11Device::CreatePixelShader method (d3d11.h).
/// </summary>
CreatePixelShader = 15,
/// <summary>
/// ID3D11Device::CreateHullShader method (d3d11.h).
/// </summary>
CreateHullShader = 16,
/// <summary>
/// ID3D11Device::CreateDomainShader method (d3d11.h).
/// </summary>
CreateDomainShader = 17,
/// <summary>
/// ID3D11Device::CreateComputeShader method (d3d11.h).
/// </summary>
CreateComputeShader = 18,
/// <summary>
/// ID3D11Device::CreateClassLinkage method (d3d11.h).
/// </summary>
CreateClassLinkage = 19,
/// <summary>
/// ID3D11Device::CreateBlendState method (d3d11.h).
/// </summary>
CreateBlendState = 20,
/// <summary>
/// ID3D11Device::CreateDepthStencilState method (d3d11.h).
/// </summary>
CreateDepthStencilState = 21,
/// <summary>
/// ID3D11Device::CreateRasterizerState method (d3d11.h).
/// </summary>
CreateRasterizerState = 22,
/// <summary>
/// ID3D11Device::CreateSamplerState method (d3d11.h).
/// </summary>
CreateSamplerState = 23,
/// <summary>
/// ID3D11Device::CreateQuery method (d3d11.h).
/// </summary>
CreateQuery = 24,
/// <summary>
/// ID3D11Device::CreatePredicate method (d3d11.h).
/// </summary>
CreatePredicate = 25,
/// <summary>
/// ID3D11Device::CreateCounter method (d3d11.h).
/// </summary>
CreateCounter = 26,
/// <summary>
/// ID3D11Device::CreateDeferredContext method (d3d11.h).
/// </summary>
CreateDeferredContext = 27,
/// <summary>
/// ID3D11Device::OpenSharedResource method (d3d11.h).
/// </summary>
OpenSharedResource = 28,
/// <summary>
/// ID3D11Device::CheckFormatSupport method (d3d11.h).
/// </summary>
CheckFormatSupport = 29,
/// <summary>
/// ID3D11Device::CheckMultisampleQualityLevels method (d3d11.h).
/// </summary>
CheckMultisampleQualityLevels = 30,
/// <summary>
/// ID3D11Device::CheckCounterInfo method (d3d11.h).
/// </summary>
CheckCounterInfo = 31,
/// <summary>
/// ID3D11Device::CheckCounter method (d3d11.h).
/// </summary>
CheckCounter = 32,
/// <summary>
/// ID3D11Device::CheckFeatureSupport method (d3d11.h).
/// </summary>
CheckFeatureSupport = 33,
/// <summary>
/// ID3D11Device::GetPrivateData method (d3d11.h).
/// </summary>
GetPrivateData = 34,
/// <summary>
/// ID3D11Device::SetPrivateData method (d3d11.h).
/// </summary>
SetPrivateData = 35,
/// <summary>
/// ID3D11Device::SetPrivateDataInterface method (d3d11.h).
/// </summary>
SetPrivateDataInterface = 36,
/// <summary>
/// ID3D11Device::GetFeatureLevel method (d3d11.h).
/// </summary>
GetFeatureLevel = 37,
/// <summary>
/// ID3D11Device::GetCreationFlags method (d3d11.h).
/// </summary>
GetCreationFlags = 38,
/// <summary>
/// ID3D11Device::GetDeviceRemovedReason method (d3d11.h).
/// </summary>
GetDeviceRemovedReason = 39,
/// <summary>
/// ID3D11Device::GetImmediateContext method (d3d11.h).
/// </summary>
GetImmediateContext = 40,
/// <summary>
/// ID3D11Device::SetExceptionMode method (d3d11.h).
/// </summary>
SetExceptionMode = 41,
/// <summary>
/// ID3D11Device::GetExceptionMode method (d3d11.h).
/// </summary>
GetExceptionMode = 42,
}

View file

@ -1,106 +0,0 @@
namespace Dalamud.Game.Internal.DXGI.Definitions;
/// <summary>
/// Contains a full list of IDXGISwapChain functions to be used as an indexer into the SwapChain Virtual Function Table
/// entries.
/// </summary>
internal enum IDXGISwapChainVtbl
{
// IUnknown
/// <summary>
/// IUnknown::QueryInterface method (unknwn.h).
/// </summary>
QueryInterface = 0,
/// <summary>
/// IUnknown::AddRef method (unknwn.h).
/// </summary>
AddRef = 1,
/// <summary>
/// IUnknown::Release method (unknwn.h).
/// </summary>
Release = 2,
// IDXGIObject
/// <summary>
/// IDXGIObject::SetPrivateData method (dxgi.h).
/// </summary>
SetPrivateData = 3,
/// <summary>
/// IDXGIObject::SetPrivateDataInterface method (dxgi.h).
/// </summary>
SetPrivateDataInterface = 4,
/// <summary>
/// IDXGIObject::GetPrivateData method (dxgi.h).
/// </summary>
GetPrivateData = 5,
/// <summary>
/// IDXGIObject::GetParent method (dxgi.h).
/// </summary>
GetParent = 6,
// IDXGIDeviceSubObject
/// <summary>
/// IDXGIDeviceSubObject::GetDevice method (dxgi.h).
/// </summary>
GetDevice = 7,
// IDXGISwapChain
/// <summary>
/// IDXGISwapChain::Present method (dxgi.h).
/// </summary>
Present = 8,
/// <summary>
/// IUnknIDXGISwapChainown::GetBuffer method (dxgi.h).
/// </summary>
GetBuffer = 9,
/// <summary>
/// IDXGISwapChain::SetFullscreenState method (dxgi.h).
/// </summary>
SetFullscreenState = 10,
/// <summary>
/// IDXGISwapChain::GetFullscreenState method (dxgi.h).
/// </summary>
GetFullscreenState = 11,
/// <summary>
/// IDXGISwapChain::GetDesc method (dxgi.h).
/// </summary>
GetDesc = 12,
/// <summary>
/// IDXGISwapChain::ResizeBuffers method (dxgi.h).
/// </summary>
ResizeBuffers = 13,
/// <summary>
/// IDXGISwapChain::ResizeTarget method (dxgi.h).
/// </summary>
ResizeTarget = 14,
/// <summary>
/// IDXGISwapChain::GetContainingOutput method (dxgi.h).
/// </summary>
GetContainingOutput = 15,
/// <summary>
/// IDXGISwapChain::GetFrameStatistics method (dxgi.h).
/// </summary>
GetFrameStatistics = 16,
/// <summary>
/// IDXGISwapChain::GetLastPresentCount method (dxgi.h).
/// </summary>
GetLastPresentCount = 17,
}

View file

@ -1,17 +0,0 @@
namespace Dalamud.Game.Internal.DXGI;
/// <summary>
/// An interface binding for the address resolvers that attempt to find native D3D11 methods.
/// </summary>
public interface ISwapChainAddressResolver
{
/// <summary>
/// Gets or sets the address of the native D3D11.Present method.
/// </summary>
IntPtr Present { get; set; }
/// <summary>
/// Gets or sets the address of the native D3D11.ResizeBuffers method.
/// </summary>
IntPtr ResizeBuffers { get; set; }
}

View file

@ -1,169 +0,0 @@
using System.Collections.Generic;
using System.Diagnostics;
using System.Runtime.InteropServices;
using Dalamud.Game.Internal.DXGI.Definitions;
using FFXIVClientStructs.FFXIV.Client.Graphics.Kernel;
using Serilog;
namespace Dalamud.Game.Internal.DXGI;
/// <summary>
/// This class attempts to determine the D3D11 SwapChain vtable addresses via instantiating a new form and inspecting it.
/// </summary>
/// <remarks>
/// If the normal signature based method of resolution fails, this is the backup.
/// </remarks>
internal class SwapChainVtableResolver : BaseAddressResolver, ISwapChainAddressResolver
{
/// <inheritdoc/>
public IntPtr Present { get; set; }
/// <inheritdoc/>
public IntPtr ResizeBuffers { get; set; }
/// <summary>
/// Gets a value indicating whether or not ReShade is loaded/used.
/// </summary>
public bool IsReshade { get; private set; }
/// <inheritdoc/>
protected override unsafe void Setup64Bit(ISigScanner sig)
{
Device* kernelDev;
SwapChain* swapChain;
void* dxgiSwapChain;
while (true)
{
kernelDev = Device.Instance();
if (kernelDev == null)
continue;
swapChain = kernelDev->SwapChain;
if (swapChain == null)
continue;
dxgiSwapChain = swapChain->DXGISwapChain;
if (dxgiSwapChain == null)
continue;
break;
}
var scVtbl = GetVTblAddresses(new IntPtr(dxgiSwapChain), Enum.GetValues(typeof(IDXGISwapChainVtbl)).Length);
this.Present = scVtbl[(int)IDXGISwapChainVtbl.Present];
var modules = Process.GetCurrentProcess().Modules;
foreach (ProcessModule processModule in modules)
{
if (processModule.FileName == null || !processModule.FileName.EndsWith("game\\dxgi.dll"))
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 = IntPtr.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 == IntPtr.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 == IntPtr.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",
reShadeDxgiPresent.ToString("X"));
if (reShadeDxgiPresent != IntPtr.Zero)
{
this.Present = reShadeDxgiPresent;
this.IsReshade = true;
}
break;
}
catch (Exception e)
{
Log.Error(e, "Failed to get ReShade version info");
break;
}
}
this.ResizeBuffers = scVtbl[(int)IDXGISwapChainVtbl.ResizeBuffers];
}
private static List<IntPtr> GetVTblAddresses(IntPtr pointer, int numberOfMethods)
{
return GetVTblAddresses(pointer, 0, numberOfMethods);
}
private static List<IntPtr> GetVTblAddresses(IntPtr pointer, int startIndex, int numberOfMethods)
{
var vtblAddresses = new List<IntPtr>();
var vTable = Marshal.ReadIntPtr(pointer);
for (var i = startIndex; i < startIndex + numberOfMethods; i++)
vtblAddresses.Add(Marshal.ReadIntPtr(vTable, i * IntPtr.Size)); // using IntPtr.Size allows us to support both 32 and 64-bit processes
return vtblAddresses;
}
}

View file

@ -38,8 +38,8 @@ internal sealed class GameNetwork : IInternalDisposableService, IGameNetwork
this.address.Setup(sigScanner);
Log.Verbose("===== G A M E N E T W O R K =====");
Log.Verbose($"ProcessZonePacketDown address 0x{this.address.ProcessZonePacketDown.ToInt64():X}");
Log.Verbose($"ProcessZonePacketUp address 0x{this.address.ProcessZonePacketUp.ToInt64():X}");
Log.Verbose($"ProcessZonePacketDown address {Util.DescribeAddress(this.address.ProcessZonePacketDown)}");
Log.Verbose($"ProcessZonePacketUp address {Util.DescribeAddress(this.address.ProcessZonePacketUp)}");
this.processZonePacketDownHook = Hook<ProcessZonePacketDownDelegate>.FromAddress(this.address.ProcessZonePacketDown, this.ProcessZonePacketDownDetour);
this.processZonePacketUpHook = Hook<ProcessZonePacketUpDelegate>.FromAddress(this.address.ProcessZonePacketUp, this.ProcessZonePacketUpDetour);

View file

@ -5,6 +5,7 @@ using System.Runtime.InteropServices;
using Dalamud.Logging.Internal;
using Dalamud.Memory;
using Dalamud.Utility;
using Iced.Intel;
@ -69,7 +70,7 @@ internal class HookManager : IInternalDisposableService
/// <returns>A new Unhooker instance.</returns>
public static Unhooker RegisterUnhooker(IntPtr address, int minBytes, int maxBytes)
{
Log.Verbose($"Registering hook at 0x{address.ToInt64():X} (minBytes=0x{minBytes:X}, maxBytes=0x{maxBytes:X})");
Log.Verbose($"Registering hook at {Util.DescribeAddress(address)} (minBytes=0x{minBytes:X}, maxBytes=0x{maxBytes:X})");
return Unhookers.GetOrAdd(address, _ => new Unhooker(address, minBytes, maxBytes));
}

View file

@ -1,4 +1,5 @@
using Dalamud.Memory;
using Dalamud.Utility;
namespace Dalamud.Hooking.Internal;
@ -63,7 +64,7 @@ public class Unhooker
var len = this.trimmed ? this.originalBytes.Length : int.Max(this.GetNaiveHookLength(), this.minBytes);
if (len > 0)
{
HookManager.Log.Verbose($"Reverting hook at 0x{this.address.ToInt64():X} ({len} bytes, trimmed={this.trimmed})");
HookManager.Log.Verbose($"Reverting hook at {Util.DescribeAddress(this.address)} ({len} bytes, trimmed={this.trimmed})");
MemoryHelper.ChangePermission(this.address, len, MemoryProtection.ExecuteReadWrite, out var oldPermissions);
MemoryHelper.WriteRaw(this.address, this.originalBytes[..len]);
MemoryHelper.ChangePermission(this.address, len, oldPermissions);

View file

@ -12,7 +12,6 @@ using Dalamud.Configuration.Internal;
using Dalamud.Game;
using Dalamud.Game.ClientState.GamePad;
using Dalamud.Game.ClientState.Keys;
using Dalamud.Game.Internal.DXGI;
using Dalamud.Hooking;
using Dalamud.Hooking.WndProcHook;
using Dalamud.Interface.ImGuiNotification.Internal;
@ -79,11 +78,11 @@ internal class InterfaceManager : IInternalDisposableService
private readonly ConcurrentQueue<Action> runBeforeImGuiRender = new();
private readonly ConcurrentQueue<Action> runAfterImGuiRender = new();
private readonly SwapChainVtableResolver address = new();
private RawDX11Scene? scene;
private Hook<SetCursorDelegate>? setCursorHook;
private Hook<PresentDelegate>? presentHook;
private Hook<DxgiPresentDelegate>? dxgiPresentHook;
private Hook<ReshadeOnPresentDelegate>? reshadeOnPresentHook;
private Hook<ResizeBuffersDelegate>? resizeBuffersHook;
private IFontAtlas? dalamudAtlas;
@ -100,7 +99,10 @@ internal class InterfaceManager : IInternalDisposableService
}
[UnmanagedFunctionPointer(CallingConvention.ThisCall)]
private delegate IntPtr PresentDelegate(IntPtr swapChain, uint syncInterval, uint presentFlags);
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);
@ -296,7 +298,8 @@ internal class InterfaceManager : IInternalDisposableService
{
this.wndProcHookManager.PreWndProc -= this.WndProcHookManagerOnPreWndProc;
Interlocked.Exchange(ref this.setCursorHook, null)?.Dispose();
Interlocked.Exchange(ref this.presentHook, null)?.Dispose();
Interlocked.Exchange(ref this.dxgiPresentHook, null)?.Dispose();
Interlocked.Exchange(ref this.reshadeOnPresentHook, null)?.Dispose();
Interlocked.Exchange(ref this.resizeBuffersHook, null)?.Dispose();
}
}
@ -629,17 +632,16 @@ internal class InterfaceManager : IInternalDisposableService
args.SuppressWithValue(r.Value);
}
/*
* NOTE(goat): When hooking ReShade DXGISwapChain::runtime_present, this is missing the syncInterval arg.
* Seems to work fine regardless, I guess, so whatever.
*/
private IntPtr PresentDetour(IntPtr swapChain, uint syncInterval, uint presentFlags)
private void ReshadeOnPresentDetour(nint swapChain, uint flags, nint presentParams)
{
Debug.Assert(this.presentHook 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 (!SwapChainHelper.IsGameDeviceSwapChain(swapChain))
{
this.reshadeOnPresentHook!.Original(swapChain, flags, presentParams);
return;
}
if (this.scene != null && swapChain != this.scene.SwapChain.NativePointer)
return this.presentHook!.Original(swapChain, syncInterval, presentFlags);
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);
@ -655,7 +657,8 @@ internal class InterfaceManager : IInternalDisposableService
Util.Fatal("Failed to initialize Dalamud base fonts.\nPlease report this error.", "Dalamud");
}
return this.presentHook!.Original(swapChain, syncInterval, presentFlags);
this.reshadeOnPresentHook!.Original(swapChain, flags, presentParams);
return;
}
this.CumulativePresentCalls++;
@ -664,22 +667,49 @@ internal class InterfaceManager : IInternalDisposableService
while (this.runBeforeImGuiRender.TryDequeue(out var action))
action.InvokeSafely();
if (this.address.IsReshade)
{
var pRes = this.presentHook!.Original(swapChain, syncInterval, presentFlags);
this.reshadeOnPresentHook!.Original(swapChain, flags, presentParams);
RenderImGui(this.scene!);
this.PostImGuiRender();
this.IsMainThreadInPresent = false;
return pRes;
}
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.presentHook!.Original(swapChain, syncInterval, presentFlags);
return this.dxgiPresentHook!.Original(swapChain, syncInterval, presentFlags);
}
private void PostImGuiRender()
@ -711,7 +741,7 @@ internal class InterfaceManager : IInternalDisposableService
[ServiceManager.CallWhenServicesReady(
"InterfaceManager accepts event registration and stuff even when the game window is not ready.")]
private void ContinueConstruction(
private unsafe void ContinueConstruction(
TargetSigScanner sigScanner,
FontAtlasFactory fontAtlasFactory)
{
@ -787,8 +817,6 @@ internal class InterfaceManager : IInternalDisposableService
// This will wait for scene on its own. We just wait for this.dalamudAtlas.BuildTask in this.InitScene.
_ = this.dalamudAtlas.BuildFontsAsync();
this.address.Setup(sigScanner);
try
{
if (Service<DalamudConfiguration>.Get().WindowIsImmersive)
@ -799,32 +827,52 @@ internal class InterfaceManager : IInternalDisposableService
Log.Error(ex, "Could not enable immersive mode");
}
this.setCursorHook = Hook<SetCursorDelegate>.FromImport(null, "user32.dll", "SetCursor", 0, this.SetCursorDetour);
this.presentHook = Hook<PresentDelegate>.FromAddress(this.address.Present, this.PresentDetour);
this.resizeBuffersHook = Hook<ResizeBuffersDelegate>.FromAddress(this.address.ResizeBuffers, this.ResizeBuffersDetour);
this.setCursorHook = Hook<SetCursorDelegate>.FromImport(
null,
"user32.dll",
"SetCursor",
0,
this.SetCursorDetour);
SwapChainHelper.BusyWaitForGameDeviceSwapChain();
SwapChainHelper.DetectReShade();
Log.Verbose("===== S W A P C H A I N =====");
Log.Verbose($"Present address 0x{this.presentHook!.Address.ToInt64():X}");
Log.Verbose($"ResizeBuffers address 0x{this.resizeBuffersHook!.Address.ToInt64():X}");
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)
{
var addr = (nint)SwapChainHelper.GameDeviceSwapChainVtbl->Present;
this.dxgiPresentHook = Hook<DxgiPresentDelegate>.FromAddress(addr, this.PresentDetour);
Log.Verbose($"ReShade::DXGISwapChain::on_present address {Util.DescribeAddress(addr)}");
}
else
{
var addr = (nint)SwapChainHelper.ReshadeOnPresent;
this.reshadeOnPresentHook = Hook<ReshadeOnPresentDelegate>.FromAddress(addr, this.ReshadeOnPresentDetour);
Log.Verbose($"IDXGISwapChain::Present address {Util.DescribeAddress(addr)}");
}
this.setCursorHook.Enable();
this.presentHook.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();
// We have to ensure we're working with the main swapchain,
// as viewports might be resizing as well
if (this.scene == null || swapChain != this.scene.SwapChain.NativePointer)
return this.resizeBuffersHook!.Original(swapChain, bufferCount, width, height, newFormat, swapChainFlags);
this.scene?.OnPreResize();
var ret = this.resizeBuffersHook!.Original(swapChain, bufferCount, width, height, newFormat, swapChainFlags);

View file

@ -0,0 +1,193 @@
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;
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
{
get
{
var kernelDev = Device.Instance();
if (kernelDev == null)
return null;
var swapChain = kernelDev->SwapChain;
if (swapChain == null)
return null;
// BackBuffer should be something from IDXGISwapChain->GetBuffer, which means that IDXGISwapChain itself
// must have been fully initialized.
if (swapChain->BackBuffer == null)
return null;
return (IDXGISwapChain*)swapChain->DXGISwapChain;
}
}
/// <summary>Gets the vtable of <see cref="GameDeviceSwapChain"/>.</summary>
public static IDXGISwapChain.Vtbl<IDXGISwapChain>* GameDeviceSwapChainVtbl
{
get
{
var s = GameDeviceSwapChain;
return (IDXGISwapChain.Vtbl<IDXGISwapChain>*)(s is null ? null : s->lpVtbl);
}
}
/// <inheritdoc cref="IsGameDeviceSwapChain{T}"/>
public static bool IsGameDeviceSwapChain(nint punk) => IsGameDeviceSwapChain((IUnknown*)punk);
/// <summary>Determines if the given instance of IUnknown is the game device's swap chain.</summary>
/// <param name="punk">Object to check.</param>
/// <typeparam name="T">Type of the object to check.</typeparam>
/// <returns><c>true</c> if the object is the game's swap chain.</returns>
public static bool IsGameDeviceSwapChain<T>(T* punk) where T : unmanaged, IUnknown.Interface
{
// https://learn.microsoft.com/en-us/windows/win32/api/unknwn/nf-unknwn-iunknown-queryinterface(refiid_void)
// For any given COM object (also known as a COM component), a specific query for the IUnknown interface on any
// of the object's interfaces must always return the same pointer value.
var gdsc = GameDeviceSwapChain;
if (gdsc is null || punk is null)
return false;
fixed (Guid* iid = &IID.IID_IUnknown)
{
using var u1 = default(ComPtr<IUnknown>);
if (gdsc->QueryInterface(iid, (void**)u1.GetAddressOf()).FAILED)
return false;
using var u2 = default(ComPtr<IUnknown>);
if (punk->QueryInterface(iid, (void**)u2.GetAddressOf()).FAILED)
return false;
return u1.Get() == u2.Get();
}
}
/// <summary>Wait for the game to have finished initializing the IDXGISwapChain.</summary>
public static void BusyWaitForGameDeviceSwapChain()
{
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;
}
}
}
}

View file

@ -50,7 +50,7 @@ internal unsafe class AddonWidget : IDataWindowWidget
var addon = (FFXIVClientStructs.FFXIV.Component.GUI.AtkUnitBase*)address;
var name = addon->NameString;
ImGui.TextUnformatted($"{name} - 0x{address.ToInt64():X}\n v:{addon->IsVisible} x:{addon->X} y:{addon->Y} s:{addon->Scale}, w:{addon->RootNode->Width}, h:{addon->RootNode->Height}");
ImGui.TextUnformatted($"{name} - {Util.DescribeAddress(address)}\n v:{addon->IsVisible} x:{addon->X} y:{addon->Y} s:{addon->Scale}, w:{addon->RootNode->Width}, h:{addon->RootNode->Height}");
if (ImGui.Button("Find Agent"))
{
@ -59,7 +59,7 @@ internal unsafe class AddonWidget : IDataWindowWidget
if (this.findAgentInterfacePtr != nint.Zero)
{
ImGui.TextUnformatted($"Agent: 0x{this.findAgentInterfacePtr.ToInt64():X}");
ImGui.TextUnformatted($"Agent: {Util.DescribeAddress(this.findAgentInterfacePtr)}");
ImGui.SameLine();
if (ImGui.Button("C"))

View file

@ -1,6 +1,7 @@
using System.Collections.Generic;
using Dalamud.Game;
using Dalamud.Utility;
using ImGuiNET;
@ -57,7 +58,7 @@ internal class AddressesWidget : IDataWindowWidget
foreach (var valueTuple in debugScannedValue.Value)
{
ImGui.TextUnformatted(
$" {valueTuple.ClassName} - 0x{valueTuple.Address.ToInt64():X}");
$" {valueTuple.ClassName} - {Util.DescribeAddress(valueTuple.Address)}");
ImGui.SameLine();
if (ImGui.Button($"C##{valueTuple.Address.ToInt64():X}"))

View file

@ -1,4 +1,5 @@
using Dalamud.Game.ClientState.Conditions;
using Dalamud.Utility;
using ImGuiNET;
@ -30,7 +31,7 @@ internal class ConditionWidget : IDataWindowWidget
var condition = Service<Condition>.Get();
#if DEBUG
ImGui.Text($"ptr: 0x{condition.Address.ToInt64():X}");
ImGui.Text($"ptr: {Util.DescribeAddress(condition.Address)}");
#endif
ImGui.Text("Current Conditions:");

View file

@ -1,4 +1,5 @@
using Dalamud.Game.ClientState.GamePad;
using Dalamud.Utility;
using ImGuiNET;
@ -29,14 +30,14 @@ internal class GamepadWidget : IDataWindowWidget
{
var gamepadState = Service<GamepadState>.Get();
ImGui.Text($"GamepadInput 0x{gamepadState.GamepadInputAddress.ToInt64():X}");
ImGui.Text($"GamepadInput {Util.DescribeAddress(gamepadState.GamepadInputAddress)}");
#if DEBUG
if (ImGui.IsItemHovered())
ImGui.SetMouseCursor(ImGuiMouseCursor.Hand);
if (ImGui.IsItemClicked())
ImGui.SetClipboardText($"0x{gamepadState.GamepadInputAddress.ToInt64():X}");
ImGui.SetClipboardText($"{Util.DescribeAddress(gamepadState.GamepadInputAddress)}");
#endif
this.DrawHelper(

View file

@ -5,6 +5,7 @@ using System.Text;
using Dalamud.Game.Text.SeStringHandling;
using Dalamud.Memory.Exceptions;
using Dalamud.Utility;
using FFXIVClientStructs.FFXIV.Client.System.Memory;
using FFXIVClientStructs.FFXIV.Client.System.String;
@ -787,11 +788,11 @@ public static unsafe class MemoryHelper
var result = VirtualProtect(memoryAddress, (nuint)length, newPermissions, out var oldPermissions);
if (!result)
throw new MemoryPermissionException($"Unable to change permissions at 0x{memoryAddress.ToInt64():X} of length {length} and permission {newPermissions} (result={result})");
throw new MemoryPermissionException($"Unable to change permissions at {Util.DescribeAddress(memoryAddress)} of length {length} and permission {newPermissions} (result={result})");
var last = Marshal.GetLastWin32Error();
if (last > 0)
throw new MemoryPermissionException($"Unable to change permissions at 0x{memoryAddress.ToInt64():X} of length {length} and permission {newPermissions} (error={last})");
throw new MemoryPermissionException($"Unable to change permissions at {Util.DescribeAddress(memoryAddress)} of length {length} and permission {newPermissions} (error={last})");
return oldPermissions;
}
@ -862,11 +863,11 @@ public static unsafe class MemoryHelper
var result = NativeFunctions.ReadProcessMemory((nint)0xFFFFFFFF, memoryAddress, value, length, out _);
if (!result)
throw new MemoryReadException($"Unable to read memory at 0x{memoryAddress.ToInt64():X} of length {length} (result={result})");
throw new MemoryReadException($"Unable to read memory at {Util.DescribeAddress(memoryAddress)} of length {length} (result={result})");
var last = Marshal.GetLastWin32Error();
if (last > 0)
throw new MemoryReadException($"Unable to read memory at 0x{memoryAddress.ToInt64():X} of length {length} (error={last})");
throw new MemoryReadException($"Unable to read memory at {Util.DescribeAddress(memoryAddress)} of length {length} (error={last})");
}
}
@ -884,11 +885,11 @@ public static unsafe class MemoryHelper
var result = NativeFunctions.WriteProcessMemory((nint)0xFFFFFFFF, memoryAddress, data, length, out _);
if (!result)
throw new MemoryWriteException($"Unable to write memory at 0x{memoryAddress.ToInt64():X} of length {length} (result={result})");
throw new MemoryWriteException($"Unable to write memory at {Util.DescribeAddress(memoryAddress)} of length {length} (result={result})");
var last = Marshal.GetLastWin32Error();
if (last > 0)
throw new MemoryWriteException($"Unable to write memory at 0x{memoryAddress.ToInt64():X} of length {length} (error={last})");
throw new MemoryWriteException($"Unable to write memory at {Util.DescribeAddress(memoryAddress)} of length {length} (error={last})");
}
}

View file

@ -19,7 +19,6 @@ using Dalamud.Game.ClientState.Objects.SubKinds;
using Dalamud.Game.ClientState.Objects.Types;
using Dalamud.Interface.Colors;
using Dalamud.Interface.Utility;
using ImGuiNET;
using Lumina.Excel.GeneratedSheets;
using Serilog;
@ -28,7 +27,8 @@ using Windows.Win32.Storage.FileSystem;
using Windows.Win32.System.Memory;
using Windows.Win32.System.Ole;
using HWND = Windows.Win32.Foundation.HWND;
using static TerraFX.Interop.Windows.Windows;
using Win32_PInvoke = Windows.Win32.PInvoke;
namespace Dalamud.Utility;
@ -38,6 +38,28 @@ namespace Dalamud.Utility;
/// </summary>
public static class Util
{
private static readonly string[] PageProtectionFlagNames = [
"PAGE_NOACCESS",
"PAGE_READONLY",
"PAGE_READWRITE",
"PAGE_WRITECOPY",
"PAGE_EXECUTE",
"PAGE_EXECUTE_READ",
"PAGE_EXECUTE_READWRITE",
"PAGE_EXECUTE_WRITECOPY",
"PAGE_GUARD",
"PAGE_NOCACHE",
"PAGE_WRITECOMBINE",
"PAGE_GRAPHICS_NOACCESS",
"PAGE_GRAPHICS_READONLY",
"PAGE_GRAPHICS_READWRITE",
"PAGE_GRAPHICS_EXECUTE",
"PAGE_GRAPHICS_EXECUTE_READ",
"PAGE_GRAPHICS_EXECUTE_READWRITE",
"PAGE_GRAPHICS_COHERENT",
"PAGE_GRAPHICS_NOCACHE",
];
private static readonly Type GenericSpanType = typeof(Span<>);
private static string? gitHashInternal;
private static int? gitCommitCountInternal;
@ -160,6 +182,100 @@ public static class Util
return gitHashClientStructsInternal;
}
/// <inheritdoc cref="DescribeAddress(nint)"/>
public static unsafe string DescribeAddress(void* p) => DescribeAddress((nint)p);
/// <summary>Describes a memory address relative to module, or allocation base.</summary>
/// <param name="p">Address.</param>
/// <returns>Address description.</returns>
public static unsafe string DescribeAddress(nint p)
{
Span<char> namebuf = stackalloc char[9];
var modules = Process.GetCurrentProcess().Modules;
for (var i = 0; i < modules.Count; i++)
{
if (p < modules[i].BaseAddress) continue;
var d = p - modules[i].BaseAddress;
if (d > modules[i].ModuleMemorySize) continue;
// Display module name without path, only if there exists exactly one module loaded in the memory.
var fileName = modules[i].ModuleName;
for (var j = 0; j < modules.Count; j++)
{
if (i == j)
continue;
if (!modules[j].ModuleName.Equals(fileName, StringComparison.InvariantCultureIgnoreCase))
continue;
fileName = modules[i].FileName;
break;
}
var dos = (IMAGE_DOS_HEADER*)modules[i].BaseAddress;
if (dos->e_magic != 0x5A4D)
return $"0x{p:X}({fileName}+0x{d:X}: ???)";
Span<IMAGE_SECTION_HEADER> sections;
switch (((IMAGE_NT_HEADERS32*)(modules[i].BaseAddress + dos->e_lfanew))->OptionalHeader.Magic)
{
case IMAGE.IMAGE_NT_OPTIONAL_HDR32_MAGIC:
{
var nth = (IMAGE_NT_HEADERS32*)(modules[i].BaseAddress + dos->e_lfanew);
if (d < dos->e_lfanew + sizeof(IMAGE_NT_HEADERS32)
+ (nth->FileHeader.NumberOfSections * sizeof(IMAGE_SECTION_HEADER)))
goto default;
sections = new(
(void*)(modules[i].BaseAddress + dos->e_lfanew + sizeof(IMAGE_NT_HEADERS32)),
nth->FileHeader.NumberOfSections);
break;
}
case IMAGE.IMAGE_NT_OPTIONAL_HDR64_MAGIC:
{
var nth = (IMAGE_NT_HEADERS64*)(modules[i].BaseAddress + dos->e_lfanew);
if (d < dos->e_lfanew + sizeof(IMAGE_NT_HEADERS64)
+ (nth->FileHeader.NumberOfSections * sizeof(IMAGE_SECTION_HEADER)))
goto default;
sections = new(
(void*)(modules[i].BaseAddress + dos->e_lfanew + sizeof(IMAGE_NT_HEADERS64)),
nth->FileHeader.NumberOfSections);
break;
}
default:
return $"0x{p:X}({fileName}+0x{d:X}: header)";
}
for (var j = 0; j < sections.Length; j++)
{
if (d >= sections[j].VirtualAddress && d < sections[j].VirtualAddress + sections[j].Misc.VirtualSize)
{
var d2 = d - sections[j].VirtualAddress;
var name8 = new Span<byte>((byte*)Unsafe.AsPointer(ref sections[j].Name[0]), 8).TrimEnd((byte)0);
return $"0x{p:X}({fileName}+0x{d:X}({namebuf[..Encoding.UTF8.GetChars(name8, namebuf)]}+0x{d2:X}))";
}
}
return $"0x{p:X}({fileName}+0x{d:X}: ???)";
}
MEMORY_BASIC_INFORMATION mbi;
if (VirtualQuery((void*)p, &mbi, (nuint)sizeof(MEMORY_BASIC_INFORMATION)) == 0)
return $"0x{p:X}(???)";
var sb = new StringBuilder();
sb.Append($"0x{p:X}(");
for (int i = 0, c = 0; i < PageProtectionFlagNames.Length; i++)
{
if ((mbi.Protect & (1 << i)) == 0)
continue;
if (c++ == 0)
sb.Append(" | ");
sb.Append(PageProtectionFlagNames[i]);
}
return sb.Append(')').ToString();
}
/// <summary>
/// Read memory from an offset and hexdump them via Serilog.
/// </summary>
@ -858,7 +974,7 @@ public static class Util
Win32_PInvoke.GlobalUnlock(hGlobal);
if (Win32_PInvoke.OpenClipboard(HWND.Null))
if (Win32_PInvoke.OpenClipboard(default))
{
Win32_PInvoke.SetClipboardData(
(uint)CLIPBOARD_FORMAT.CF_HDROP,