mirror of
https://github.com/goatcorp/Dalamud.git
synced 2025-12-12 18:27:23 +01:00
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:
parent
eed7abed12
commit
1109e64552
26 changed files with 445 additions and 591 deletions
|
|
@ -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/>
|
||||
|
|
|
|||
|
|
@ -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/>
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
||||
|
|
|
|||
|
|
@ -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/>
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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/>
|
||||
|
|
|
|||
|
|
@ -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/>
|
||||
|
|
|
|||
|
|
@ -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/>
|
||||
|
|
|
|||
|
|
@ -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/>
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
}
|
||||
|
|
@ -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,
|
||||
}
|
||||
|
|
@ -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; }
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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));
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
193
Dalamud/Interface/Internal/SwapChainHelper.cs
Normal file
193
Dalamud/Interface/Internal/SwapChainHelper.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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"))
|
||||
|
|
|
|||
|
|
@ -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}"))
|
||||
|
|
|
|||
|
|
@ -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:");
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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})");
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue