diff --git a/Dalamud/Dalamud.csproj b/Dalamud/Dalamud.csproj
index 16266beeba..1389ba68ee 100644
--- a/Dalamud/Dalamud.csproj
+++ b/Dalamud/Dalamud.csproj
@@ -90,7 +90,6 @@
-
all
runtime; build; native; contentfiles; analyzers; buildtransitive
@@ -103,11 +102,12 @@
-
-
-
-
-
+
+ imgui-frag.hlsl.bytes
+
+
+ imgui-vertex.hlsl.bytes
+
diff --git a/Dalamud/Game/Internal/DXGI/SwapChainVtableResolver.cs b/Dalamud/Game/Internal/DXGI/SwapChainVtableResolver.cs
index ba880d0e54..c9ccb876e9 100644
--- a/Dalamud/Game/Internal/DXGI/SwapChainVtableResolver.cs
+++ b/Dalamud/Game/Internal/DXGI/SwapChainVtableResolver.cs
@@ -1,10 +1,10 @@
-using System.Collections.Generic;
-using System.Diagnostics;
-using System.Runtime.InteropServices;
-
using Dalamud.Game.Internal.DXGI.Definitions;
+using Dalamud.Interface.ImGuiBackend.Helpers;
+
using FFXIVClientStructs.FFXIV.Client.Graphics.Kernel;
-using Serilog;
+
+using TerraFX.Interop.DirectX;
+using TerraFX.Interop.Windows;
namespace Dalamud.Game.Internal.DXGI;
@@ -17,30 +17,23 @@ namespace Dalamud.Game.Internal.DXGI;
internal class SwapChainVtableResolver : BaseAddressResolver, ISwapChainAddressResolver
{
///
- public IntPtr Present { get; set; }
+ public nint Present { get; set; }
///
- public IntPtr ResizeBuffers { get; set; }
-
- ///
- /// Gets a value indicating whether or not ReShade is loaded/used.
- ///
- public bool IsReshade { get; private set; }
+ public nint ResizeBuffers { get; set; }
///
protected override unsafe void Setup64Bit(ISigScanner sig)
{
- Device* kernelDev;
- SwapChain* swapChain;
void* dxgiSwapChain;
while (true)
{
- kernelDev = Device.Instance();
+ var kernelDev = Device.Instance();
if (kernelDev == null)
continue;
- swapChain = kernelDev->SwapChain;
+ var swapChain = kernelDev->SwapChain;
if (swapChain == null)
continue;
@@ -51,119 +44,10 @@ protected override unsafe void Setup64Bit(ISigScanner sig)
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 GetVTblAddresses(IntPtr pointer, int numberOfMethods)
- {
- return GetVTblAddresses(pointer, 0, numberOfMethods);
- }
-
- private static List GetVTblAddresses(IntPtr pointer, int startIndex, int numberOfMethods)
- {
- var vtblAddresses = new List();
- 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
+ using var sc = new ComPtr((IDXGISwapChain*)dxgiSwapChain);
+ ReShadePeeler.PeelSwapChain(&sc);
- return vtblAddresses;
+ this.Present = (nint)sc.Get()->lpVtbl[(int)IDXGISwapChainVtbl.Present];
+ this.ResizeBuffers = (nint)sc.Get()->lpVtbl[(int)IDXGISwapChainVtbl.ResizeBuffers];
}
}
diff --git a/Dalamud/Interface/DragDrop/DragDropManager.cs b/Dalamud/Interface/DragDrop/DragDropManager.cs
index c9f0f9b80c..f3acd704d9 100644
--- a/Dalamud/Interface/DragDrop/DragDropManager.cs
+++ b/Dalamud/Interface/DragDrop/DragDropManager.cs
@@ -32,7 +32,7 @@ private DragDropManager()
Service.GetAsync()
.ContinueWith(t =>
{
- this.windowHandlePtr = t.Result.Manager.WindowHandlePtr;
+ this.windowHandlePtr = t.Result.Manager.GameWindowHandle;
this.Enable();
});
}
diff --git a/Dalamud/Interface/ImGuiBackend/Dx11Win32Backend.cs b/Dalamud/Interface/ImGuiBackend/Dx11Win32Backend.cs
new file mode 100644
index 0000000000..a8a84b20d2
--- /dev/null
+++ b/Dalamud/Interface/ImGuiBackend/Dx11Win32Backend.cs
@@ -0,0 +1,222 @@
+using System.Diagnostics.CodeAnalysis;
+
+using Dalamud.Interface.ImGuiBackend.Helpers;
+using Dalamud.Interface.ImGuiBackend.InputHandler;
+using Dalamud.Interface.ImGuiBackend.Renderers;
+using Dalamud.Utility;
+
+using ImGuiNET;
+
+using ImGuizmoNET;
+
+using ImPlotNET;
+
+using TerraFX.Interop.DirectX;
+using TerraFX.Interop.Windows;
+
+namespace Dalamud.Interface.ImGuiBackend;
+
+///
+/// Backend for ImGui, using and .
+///
+[SuppressMessage(
+ "StyleCop.CSharp.LayoutRules",
+ "SA1519:Braces should not be omitted from multi-line child statement",
+ Justification = "Multiple fixed/using scopes")]
+internal sealed unsafe class Dx11Win32Backend : IWin32Backend
+{
+ private readonly Dx11Renderer imguiRenderer;
+ private readonly Win32InputHandler imguiInput;
+
+ private ComPtr swapChainPossiblyWrapped;
+ private ComPtr swapChain;
+ private ComPtr device;
+ private ComPtr deviceContext;
+
+ private int targetWidth;
+ private int targetHeight;
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The pointer to an instance of . The reference is copied.
+ public Dx11Win32Backend(IDXGISwapChain* swapChain)
+ {
+ try
+ {
+ this.swapChainPossiblyWrapped = new(swapChain);
+ this.swapChain = new(swapChain);
+ fixed (ComPtr* ppSwapChain = &this.swapChain)
+ ReShadePeeler.PeelSwapChain(ppSwapChain);
+
+ fixed (Guid* guid = &IID.IID_ID3D11Device)
+ fixed (ID3D11Device** pp = &this.device.GetPinnableReference())
+ this.swapChain.Get()->GetDevice(guid, (void**)pp).ThrowOnError();
+
+ fixed (ID3D11DeviceContext** pp = &this.deviceContext.GetPinnableReference())
+ this.device.Get()->GetImmediateContext(pp);
+
+ using var buffer = default(ComPtr);
+ fixed (Guid* guid = &IID.IID_ID3D11Resource)
+ this.swapChain.Get()->GetBuffer(0, guid, (void**)buffer.GetAddressOf()).ThrowOnError();
+
+ var desc = default(DXGI_SWAP_CHAIN_DESC);
+ this.swapChain.Get()->GetDesc(&desc).ThrowOnError();
+ this.targetWidth = (int)desc.BufferDesc.Width;
+ this.targetHeight = (int)desc.BufferDesc.Height;
+ this.WindowHandle = desc.OutputWindow;
+
+ var ctx = ImGui.CreateContext();
+ ImGuizmo.SetImGuiContext(ctx);
+ ImPlot.SetImGuiContext(ctx);
+ ImPlot.CreateContext();
+
+ ImGui.GetIO().ConfigFlags |= ImGuiConfigFlags.DockingEnable | ImGuiConfigFlags.ViewportsEnable;
+
+ this.imguiRenderer = new(this.SwapChain, this.Device, this.DeviceContext);
+ this.imguiInput = new(this.WindowHandle);
+ }
+ catch
+ {
+ this.Dispose();
+ throw;
+ }
+ }
+
+ ///
+ /// Finalizes an instance of the class.
+ ///
+ ~Dx11Win32Backend() => this.ReleaseUnmanagedResources();
+
+ ///
+ public event IImGuiBackend.BuildUiDelegate? BuildUi;
+
+ ///
+ public event IImGuiBackend.NewInputFrameDelegate? NewInputFrame;
+
+ ///
+ public event IImGuiBackend.NewRenderFrameDelegate? NewRenderFrame;
+
+ ///
+ public bool UpdateCursor
+ {
+ get => this.imguiInput.UpdateCursor;
+ set => this.imguiInput.UpdateCursor = value;
+ }
+
+ ///
+ public string? IniPath
+ {
+ get => this.imguiInput.IniPath;
+ set => this.imguiInput.IniPath = value;
+ }
+
+ ///
+ public IImGuiInputHandler InputHandler => this.imguiInput;
+
+ ///
+ public IImGuiRenderer Renderer => this.imguiRenderer;
+
+ ///
+ /// Gets the pointer to an instance of .
+ ///
+ public IDXGISwapChain* SwapChain => this.swapChain;
+
+ ///
+ /// Gets the pointer to an instance of .
+ ///
+ public ID3D11Device* Device => this.device;
+
+ ///
+ /// Gets the pointer to an instance of , in .
+ ///
+ public nint DeviceHandle => (nint)this.device.Get();
+
+ ///
+ /// Gets the pointer to an instance of .
+ ///
+ public ID3D11DeviceContext* DeviceContext => this.deviceContext;
+
+ ///
+ /// Gets the window handle.
+ ///
+ public HWND WindowHandle { get; }
+
+ ///
+ public void Dispose()
+ {
+ this.ReleaseUnmanagedResources();
+ GC.SuppressFinalize(this);
+ }
+
+ ///
+ public nint? ProcessWndProcW(HWND hWnd, uint msg, WPARAM wParam, LPARAM lParam) =>
+ this.imguiInput.ProcessWndProcW(hWnd, msg, wParam, lParam);
+
+ ///
+ public void Render()
+ {
+ this.imguiRenderer.OnNewFrame();
+ this.NewRenderFrame?.Invoke();
+ this.imguiInput.NewFrame(this.targetWidth, this.targetHeight);
+ this.NewInputFrame?.Invoke();
+
+ ImGui.NewFrame();
+ ImGuizmo.BeginFrame();
+
+ this.BuildUi?.Invoke();
+
+ ImGui.Render();
+
+ this.imguiRenderer.RenderDrawData(ImGui.GetDrawData());
+
+ ImGui.UpdatePlatformWindows();
+ ImGui.RenderPlatformWindowsDefault();
+ }
+
+ ///
+ public void OnPreResize() => this.imguiRenderer.OnPreResize();
+
+ ///
+ public void OnPostResize(int newWidth, int newHeight)
+ {
+ this.imguiRenderer.OnPostResize(newWidth, newHeight);
+ this.targetWidth = newWidth;
+ this.targetHeight = newHeight;
+ }
+
+ ///
+ public void InvalidateFonts() => this.imguiRenderer.RebuildFontTexture();
+
+ ///
+ public bool IsImGuiCursor(nint cursorHandle) => this.imguiInput.IsImGuiCursor(cursorHandle);
+
+ ///
+ public bool IsAttachedToPresentationTarget(nint targetHandle) =>
+ this.swapChain.Get() == (void*)targetHandle
+ || this.swapChainPossiblyWrapped.Get() == (void*)targetHandle;
+
+ ///
+ public bool IsMainViewportFullScreen()
+ {
+ BOOL fullscreen;
+ this.swapChain.Get()->GetFullscreenState(&fullscreen, null);
+ return fullscreen;
+ }
+
+ private void ReleaseUnmanagedResources()
+ {
+ if (this.device.IsEmpty())
+ return;
+
+ this.imguiRenderer.Dispose();
+ this.imguiInput.Dispose();
+
+ ImGui.DestroyContext();
+
+ this.swapChain.Dispose();
+ this.deviceContext.Dispose();
+ this.device.Dispose();
+ this.swapChainPossiblyWrapped.Dispose();
+ }
+}
diff --git a/Dalamud/Interface/ImGuiBackend/Helpers/D3D11/D3D11DeviceContextStateBackup.cs b/Dalamud/Interface/ImGuiBackend/Helpers/D3D11/D3D11DeviceContextStateBackup.cs
new file mode 100644
index 0000000000..df1087ce3c
--- /dev/null
+++ b/Dalamud/Interface/ImGuiBackend/Helpers/D3D11/D3D11DeviceContextStateBackup.cs
@@ -0,0 +1,655 @@
+using System.Runtime.InteropServices;
+
+using TerraFX.Interop.DirectX;
+using TerraFX.Interop.Windows;
+
+namespace Dalamud.Interface.ImGuiBackend.Helpers.D3D11;
+
+///
+/// Captures states of a .
+///
+internal unsafe struct D3D11DeviceContextStateBackup : IDisposable
+{
+ private InputAssemblerState inputAssemblerState;
+ private RasterizerState rasterizerState;
+ private OutputMergerState outputMergerState;
+ private VertexShaderState vertexShaderState;
+ private HullShaderState hullShaderState;
+ private DomainShaderState domainShaderState;
+ private GeometryShaderState geometryShaderState;
+ private PixelShaderState pixelShaderState;
+ private ComputeShaderState computeShaderState;
+
+ ///
+ /// Initializes a new instance of the struct,
+ /// by capturing all states of a .
+ ///
+ /// The feature level.
+ /// The device context.
+ public D3D11DeviceContextStateBackup(D3D_FEATURE_LEVEL featureLevel, ID3D11DeviceContext* ctx)
+ {
+ this.inputAssemblerState = InputAssemblerState.From(ctx);
+ this.rasterizerState = RasterizerState.From(ctx);
+ this.outputMergerState = OutputMergerState.From(featureLevel, ctx);
+ this.vertexShaderState = VertexShaderState.From(ctx);
+ this.hullShaderState = HullShaderState.From(ctx);
+ this.domainShaderState = DomainShaderState.From(ctx);
+ this.geometryShaderState = GeometryShaderState.From(ctx);
+ this.pixelShaderState = PixelShaderState.From(ctx);
+ this.computeShaderState = ComputeShaderState.From(featureLevel, ctx);
+ }
+
+ ///
+ public void Dispose()
+ {
+ this.inputAssemblerState.Dispose();
+ this.rasterizerState.Dispose();
+ this.outputMergerState.Dispose();
+ this.vertexShaderState.Dispose();
+ this.hullShaderState.Dispose();
+ this.domainShaderState.Dispose();
+ this.geometryShaderState.Dispose();
+ this.pixelShaderState.Dispose();
+ this.computeShaderState.Dispose();
+ }
+
+ ///
+ /// Captures Input Assembler states of a .
+ ///
+ [StructLayout(LayoutKind.Sequential)]
+ public struct InputAssemblerState : IDisposable
+ {
+ private const int BufferCount = TerraFX.Interop.DirectX.D3D11.D3D11_IA_VERTEX_INPUT_RESOURCE_SLOT_COUNT;
+
+ private ComPtr context;
+ private ComPtr layout;
+ private ComPtr indexBuffer;
+ private DXGI_FORMAT indexFormat;
+ private uint indexOffset;
+ private D3D_PRIMITIVE_TOPOLOGY topology;
+ private fixed ulong buffers[BufferCount];
+ private fixed uint strides[BufferCount];
+ private fixed uint offsets[BufferCount];
+
+ ///
+ /// Creates a new instance of from .
+ ///
+ /// The device context.
+ /// The captured state.
+ public static InputAssemblerState From(ID3D11DeviceContext* ctx)
+ {
+ var state = default(InputAssemblerState);
+ state.context.Attach(ctx);
+ ctx->AddRef();
+ ctx->IAGetInputLayout(state.layout.GetAddressOf());
+ ctx->IAGetPrimitiveTopology(&state.topology);
+ ctx->IAGetIndexBuffer(state.indexBuffer.GetAddressOf(), &state.indexFormat, &state.indexOffset);
+ ctx->IAGetVertexBuffers(0, BufferCount, (ID3D11Buffer**)state.buffers, state.strides, state.offsets);
+ return state;
+ }
+
+ ///
+ public void Dispose()
+ {
+ var ctx = this.context.Get();
+ if (ctx is null)
+ return;
+
+ fixed (InputAssemblerState* pThis = &this)
+ {
+ ctx->IASetInputLayout(pThis->layout);
+ ctx->IASetPrimitiveTopology(pThis->topology);
+ ctx->IASetIndexBuffer(pThis->indexBuffer, pThis->indexFormat, pThis->indexOffset);
+ ctx->IASetVertexBuffers(0, BufferCount, (ID3D11Buffer**)pThis->buffers, pThis->strides, pThis->offsets);
+
+ pThis->context.Dispose();
+ pThis->layout.Dispose();
+ pThis->indexBuffer.Dispose();
+ foreach (ref var b in new Span>(pThis->buffers, BufferCount))
+ b.Dispose();
+ }
+ }
+ }
+
+ ///
+ /// Captures Rasterizer states of a .
+ ///
+ [StructLayout(LayoutKind.Sequential)]
+ public struct RasterizerState : IDisposable
+ {
+ private const int Count = TerraFX.Interop.DirectX.D3D11.D3D11_VIEWPORT_AND_SCISSORRECT_MAX_INDEX;
+
+ private ComPtr context;
+ private ComPtr state;
+ private fixed byte viewports[24 * Count];
+ private fixed ulong scissorRects[16 * Count];
+
+ ///
+ /// Creates a new instance of from .
+ ///
+ /// The device context.
+ /// The captured state.
+ public static RasterizerState From(ID3D11DeviceContext* ctx)
+ {
+ var state = default(RasterizerState);
+ state.context.Attach(ctx);
+ ctx->AddRef();
+ ctx->RSGetState(state.state.GetAddressOf());
+ uint n = Count;
+ ctx->RSGetViewports(&n, (D3D11_VIEWPORT*)state.viewports);
+ n = Count;
+ ctx->RSGetScissorRects(&n, (RECT*)state.scissorRects);
+ return state;
+ }
+
+ ///
+ public void Dispose()
+ {
+ var ctx = this.context.Get();
+ if (ctx is null)
+ return;
+
+ fixed (RasterizerState* pThis = &this)
+ {
+ ctx->RSSetState(pThis->state);
+ ctx->RSSetViewports(Count, (D3D11_VIEWPORT*)pThis->viewports);
+ ctx->RSSetScissorRects(Count, (RECT*)pThis->scissorRects);
+
+ pThis->context.Dispose();
+ pThis->state.Dispose();
+ }
+ }
+ }
+
+ ///
+ /// Captures Output Merger states of a .
+ ///
+ [StructLayout(LayoutKind.Sequential)]
+ public struct OutputMergerState : IDisposable
+ {
+ private const int RtvCount = TerraFX.Interop.DirectX.D3D11.D3D11_SIMULTANEOUS_RENDER_TARGET_COUNT;
+ private const int UavCountMax = TerraFX.Interop.DirectX.D3D11.D3D11_1_UAV_SLOT_COUNT;
+
+ private ComPtr context;
+ private ComPtr blendState;
+ private fixed float blendFactor[4];
+ private uint sampleMask;
+ private uint stencilRef;
+ private ComPtr depthStencilState;
+ private fixed ulong rtvs[RtvCount]; // ID3D11RenderTargetView*[RtvCount]
+ private ComPtr dsv;
+ private fixed ulong uavs[UavCountMax]; // ID3D11UnorderedAccessView*[UavCount]
+ private int uavCount;
+
+ ///
+ /// Creates a new instance of from .
+ ///
+ /// The feature level.
+ /// The device context.
+ /// The captured state.
+ public static OutputMergerState From(D3D_FEATURE_LEVEL featureLevel, ID3D11DeviceContext* ctx)
+ {
+ var state = default(OutputMergerState);
+ state.uavCount = featureLevel >= D3D_FEATURE_LEVEL.D3D_FEATURE_LEVEL_11_1
+ ? TerraFX.Interop.DirectX.D3D11.D3D11_1_UAV_SLOT_COUNT
+ : TerraFX.Interop.DirectX.D3D11.D3D11_PS_CS_UAV_REGISTER_COUNT;
+ state.context.Attach(ctx);
+ ctx->AddRef();
+ ctx->OMGetBlendState(state.blendState.GetAddressOf(), state.blendFactor, &state.sampleMask);
+ ctx->OMGetDepthStencilState(state.depthStencilState.GetAddressOf(), &state.stencilRef);
+ ctx->OMGetRenderTargetsAndUnorderedAccessViews(
+ RtvCount,
+ (ID3D11RenderTargetView**)state.rtvs,
+ state.dsv.GetAddressOf(),
+ 0,
+ (uint)state.uavCount,
+ (ID3D11UnorderedAccessView**)state.uavs);
+ return state;
+ }
+
+ ///
+ public void Dispose()
+ {
+ var ctx = this.context.Get();
+ if (ctx is null)
+ return;
+
+ fixed (OutputMergerState* pThis = &this)
+ {
+ ctx->OMSetBlendState(pThis->blendState, pThis->blendFactor, pThis->sampleMask);
+ ctx->OMSetDepthStencilState(pThis->depthStencilState, pThis->stencilRef);
+ var rtvc = (uint)RtvCount;
+ while (rtvc > 0 && pThis->rtvs[rtvc - 1] == 0)
+ rtvc--;
+
+ var uavlb = rtvc;
+ while (uavlb < this.uavCount && pThis->uavs[uavlb] == 0)
+ uavlb++;
+
+ var uavc = (uint)this.uavCount;
+ while (uavc > uavlb && pThis->uavs[uavc - 1] == 0)
+ uavlb--;
+ uavc -= uavlb;
+
+ ctx->OMSetRenderTargetsAndUnorderedAccessViews(
+ rtvc,
+ (ID3D11RenderTargetView**)pThis->rtvs,
+ pThis->dsv,
+ uavc == 0 ? 0 : uavlb,
+ uavc,
+ uavc == 0 ? null : (ID3D11UnorderedAccessView**)pThis->uavs,
+ null);
+
+ this.context.Reset();
+ this.blendState.Reset();
+ this.depthStencilState.Reset();
+ this.dsv.Reset();
+ foreach (ref var b in new Span>(pThis->rtvs, RtvCount))
+ b.Dispose();
+ foreach (ref var b in new Span>(pThis->uavs, this.uavCount))
+ b.Dispose();
+ }
+ }
+ }
+
+ ///
+ /// Captures Vertex Shader states of a .
+ ///
+ [StructLayout(LayoutKind.Sequential)]
+ public struct VertexShaderState : IDisposable
+ {
+ private const int BufferCount = TerraFX.Interop.DirectX.D3D11.D3D11_COMMONSHADER_CONSTANT_BUFFER_API_SLOT_COUNT;
+ private const int SamplerCount = TerraFX.Interop.DirectX.D3D11.D3D11_COMMONSHADER_SAMPLER_SLOT_COUNT;
+ private const int ResourceCount = TerraFX.Interop.DirectX.D3D11.D3D11_COMMONSHADER_INPUT_RESOURCE_SLOT_COUNT;
+ private const int ClassInstanceCount = 256; // According to msdn
+
+ private ComPtr context;
+ private ComPtr shader;
+ private fixed ulong insts[ClassInstanceCount];
+ private fixed ulong buffers[BufferCount];
+ private fixed ulong samplers[SamplerCount];
+ private fixed ulong resources[ResourceCount];
+ private uint instCount;
+
+ ///
+ /// Creates a new instance of from .
+ ///
+ /// The device context.
+ /// The captured state.
+ public static VertexShaderState From(ID3D11DeviceContext* ctx)
+ {
+ var state = default(VertexShaderState);
+ state.context.Attach(ctx);
+ ctx->AddRef();
+ state.instCount = ClassInstanceCount;
+ ctx->VSGetShader(state.shader.GetAddressOf(), (ID3D11ClassInstance**)state.insts, &state.instCount);
+ ctx->VSGetConstantBuffers(0, BufferCount, (ID3D11Buffer**)state.buffers);
+ ctx->VSGetSamplers(0, SamplerCount, (ID3D11SamplerState**)state.samplers);
+ ctx->VSGetShaderResources(0, ResourceCount, (ID3D11ShaderResourceView**)state.resources);
+ return state;
+ }
+
+ ///
+ public void Dispose()
+ {
+ var ctx = this.context.Get();
+ if (ctx is null)
+ return;
+
+ fixed (VertexShaderState* pThis = &this)
+ {
+ ctx->VSSetShader(pThis->shader, (ID3D11ClassInstance**)pThis->insts, pThis->instCount);
+ ctx->VSSetConstantBuffers(0, BufferCount, (ID3D11Buffer**)pThis->buffers);
+ ctx->VSSetSamplers(0, SamplerCount, (ID3D11SamplerState**)pThis->samplers);
+ ctx->VSSetShaderResources(0, ResourceCount, (ID3D11ShaderResourceView**)pThis->resources);
+
+ foreach (ref var b in new Span>(pThis->buffers, BufferCount))
+ b.Dispose();
+ foreach (ref var b in new Span>(pThis->samplers, SamplerCount))
+ b.Dispose();
+ foreach (ref var b in new Span>(pThis->resources, ResourceCount))
+ b.Dispose();
+ foreach (ref var b in new Span>(pThis->insts, (int)pThis->instCount))
+ b.Dispose();
+ pThis->context.Dispose();
+ pThis->shader.Dispose();
+ }
+ }
+ }
+
+ ///
+ /// Captures Hull Shader states of a .
+ ///
+ [StructLayout(LayoutKind.Sequential)]
+ public struct HullShaderState : IDisposable
+ {
+ private const int BufferCount = TerraFX.Interop.DirectX.D3D11.D3D11_COMMONSHADER_CONSTANT_BUFFER_API_SLOT_COUNT;
+ private const int SamplerCount = TerraFX.Interop.DirectX.D3D11.D3D11_COMMONSHADER_SAMPLER_SLOT_COUNT;
+ private const int ResourceCount = TerraFX.Interop.DirectX.D3D11.D3D11_COMMONSHADER_INPUT_RESOURCE_SLOT_COUNT;
+ private const int ClassInstanceCount = 256; // According to msdn
+
+ private ComPtr context;
+ private ComPtr shader;
+ private fixed ulong insts[ClassInstanceCount];
+ private fixed ulong buffers[BufferCount];
+ private fixed ulong samplers[SamplerCount];
+ private fixed ulong resources[ResourceCount];
+ private uint instCount;
+
+ ///
+ /// Creates a new instance of from .
+ ///
+ /// The device context.
+ /// The captured state.
+ public static HullShaderState From(ID3D11DeviceContext* ctx)
+ {
+ var state = default(HullShaderState);
+ state.context.Attach(ctx);
+ ctx->AddRef();
+ state.instCount = ClassInstanceCount;
+ ctx->HSGetShader(state.shader.GetAddressOf(), (ID3D11ClassInstance**)state.insts, &state.instCount);
+ ctx->HSGetConstantBuffers(0, BufferCount, (ID3D11Buffer**)state.buffers);
+ ctx->HSGetSamplers(0, SamplerCount, (ID3D11SamplerState**)state.samplers);
+ ctx->HSGetShaderResources(0, ResourceCount, (ID3D11ShaderResourceView**)state.resources);
+ return state;
+ }
+
+ ///
+ public void Dispose()
+ {
+ var ctx = this.context.Get();
+ if (ctx is null)
+ return;
+
+ fixed (HullShaderState* pThis = &this)
+ {
+ ctx->HSSetShader(pThis->shader, (ID3D11ClassInstance**)pThis->insts, pThis->instCount);
+ ctx->HSSetConstantBuffers(0, BufferCount, (ID3D11Buffer**)pThis->buffers);
+ ctx->HSSetSamplers(0, SamplerCount, (ID3D11SamplerState**)pThis->samplers);
+ ctx->HSSetShaderResources(0, ResourceCount, (ID3D11ShaderResourceView**)pThis->resources);
+
+ foreach (ref var b in new Span>(pThis->buffers, BufferCount))
+ b.Dispose();
+ foreach (ref var b in new Span>(pThis->samplers, SamplerCount))
+ b.Dispose();
+ foreach (ref var b in new Span>(pThis->resources, ResourceCount))
+ b.Dispose();
+ foreach (ref var b in new Span>(pThis->insts, (int)pThis->instCount))
+ b.Dispose();
+ pThis->context.Dispose();
+ pThis->shader.Dispose();
+ }
+ }
+ }
+
+ ///
+ /// Captures Domain Shader states of a .
+ ///
+ [StructLayout(LayoutKind.Sequential)]
+ public struct DomainShaderState : IDisposable
+ {
+ private const int BufferCount = TerraFX.Interop.DirectX.D3D11.D3D11_COMMONSHADER_CONSTANT_BUFFER_API_SLOT_COUNT;
+ private const int SamplerCount = TerraFX.Interop.DirectX.D3D11.D3D11_COMMONSHADER_SAMPLER_SLOT_COUNT;
+ private const int ResourceCount = TerraFX.Interop.DirectX.D3D11.D3D11_COMMONSHADER_INPUT_RESOURCE_SLOT_COUNT;
+ private const int ClassInstanceCount = 256; // According to msdn
+
+ private ComPtr context;
+ private ComPtr shader;
+ private fixed ulong insts[ClassInstanceCount];
+ private fixed ulong buffers[BufferCount];
+ private fixed ulong samplers[SamplerCount];
+ private fixed ulong resources[ResourceCount];
+ private uint instCount;
+
+ ///
+ /// Creates a new instance of from .
+ ///
+ /// The device context.
+ /// The captured state.
+ public static DomainShaderState From(ID3D11DeviceContext* ctx)
+ {
+ var state = default(DomainShaderState);
+ state.context.Attach(ctx);
+ ctx->AddRef();
+ state.instCount = ClassInstanceCount;
+ ctx->DSGetShader(state.shader.GetAddressOf(), (ID3D11ClassInstance**)state.insts, &state.instCount);
+ ctx->DSGetConstantBuffers(0, BufferCount, (ID3D11Buffer**)state.buffers);
+ ctx->DSGetSamplers(0, SamplerCount, (ID3D11SamplerState**)state.samplers);
+ ctx->DSGetShaderResources(0, ResourceCount, (ID3D11ShaderResourceView**)state.resources);
+ return state;
+ }
+
+ ///
+ public void Dispose()
+ {
+ var ctx = this.context.Get();
+ if (ctx is null)
+ return;
+
+ fixed (DomainShaderState* pThis = &this)
+ {
+ ctx->DSSetShader(pThis->shader, (ID3D11ClassInstance**)pThis->insts, pThis->instCount);
+ ctx->DSSetConstantBuffers(0, BufferCount, (ID3D11Buffer**)pThis->buffers);
+ ctx->DSSetSamplers(0, SamplerCount, (ID3D11SamplerState**)pThis->samplers);
+ ctx->DSSetShaderResources(0, ResourceCount, (ID3D11ShaderResourceView**)pThis->resources);
+
+ foreach (ref var b in new Span>(pThis->buffers, BufferCount))
+ b.Dispose();
+ foreach (ref var b in new Span>(pThis->samplers, SamplerCount))
+ b.Dispose();
+ foreach (ref var b in new Span>(pThis->resources, ResourceCount))
+ b.Dispose();
+ foreach (ref var b in new Span>(pThis->insts, (int)pThis->instCount))
+ b.Dispose();
+ pThis->context.Dispose();
+ pThis->shader.Dispose();
+ }
+ }
+ }
+
+ ///
+ /// Captures Geometry Shader states of a .
+ ///
+ [StructLayout(LayoutKind.Sequential)]
+ public struct GeometryShaderState : IDisposable
+ {
+ private const int BufferCount = TerraFX.Interop.DirectX.D3D11.D3D11_COMMONSHADER_CONSTANT_BUFFER_API_SLOT_COUNT;
+ private const int SamplerCount = TerraFX.Interop.DirectX.D3D11.D3D11_COMMONSHADER_SAMPLER_SLOT_COUNT;
+ private const int ResourceCount = TerraFX.Interop.DirectX.D3D11.D3D11_COMMONSHADER_INPUT_RESOURCE_SLOT_COUNT;
+ private const int ClassInstanceCount = 256; // According to msdn
+
+ private ComPtr context;
+ private ComPtr shader;
+ private fixed ulong insts[ClassInstanceCount];
+ private fixed ulong buffers[BufferCount];
+ private fixed ulong samplers[SamplerCount];
+ private fixed ulong resources[ResourceCount];
+ private uint instCount;
+
+ ///
+ /// Creates a new instance of from .
+ ///
+ /// The device context.
+ /// The captured state.
+ public static GeometryShaderState From(ID3D11DeviceContext* ctx)
+ {
+ var state = default(GeometryShaderState);
+ state.context.Attach(ctx);
+ ctx->AddRef();
+ state.instCount = ClassInstanceCount;
+ ctx->GSGetShader(state.shader.GetAddressOf(), (ID3D11ClassInstance**)state.insts, &state.instCount);
+ ctx->GSGetConstantBuffers(0, BufferCount, (ID3D11Buffer**)state.buffers);
+ ctx->GSGetSamplers(0, SamplerCount, (ID3D11SamplerState**)state.samplers);
+ ctx->GSGetShaderResources(0, ResourceCount, (ID3D11ShaderResourceView**)state.resources);
+ return state;
+ }
+
+ ///
+ public void Dispose()
+ {
+ var ctx = this.context.Get();
+ if (ctx is null)
+ return;
+
+ fixed (GeometryShaderState* pThis = &this)
+ {
+ ctx->GSSetShader(pThis->shader, (ID3D11ClassInstance**)pThis->insts, pThis->instCount);
+ ctx->GSSetConstantBuffers(0, BufferCount, (ID3D11Buffer**)pThis->buffers);
+ ctx->GSSetSamplers(0, SamplerCount, (ID3D11SamplerState**)pThis->samplers);
+ ctx->GSSetShaderResources(0, ResourceCount, (ID3D11ShaderResourceView**)pThis->resources);
+
+ foreach (ref var b in new Span>(pThis->buffers, BufferCount))
+ b.Dispose();
+ foreach (ref var b in new Span>(pThis->samplers, SamplerCount))
+ b.Dispose();
+ foreach (ref var b in new Span>(pThis->resources, ResourceCount))
+ b.Dispose();
+ foreach (ref var b in new Span>(pThis->insts, (int)pThis->instCount))
+ b.Dispose();
+ pThis->context.Dispose();
+ pThis->shader.Dispose();
+ }
+ }
+ }
+
+ ///
+ /// Captures Pixel Shader states of a .
+ ///
+ [StructLayout(LayoutKind.Sequential)]
+ public struct PixelShaderState : IDisposable
+ {
+ private const int BufferCount = TerraFX.Interop.DirectX.D3D11.D3D11_COMMONSHADER_CONSTANT_BUFFER_API_SLOT_COUNT;
+ private const int SamplerCount = TerraFX.Interop.DirectX.D3D11.D3D11_COMMONSHADER_SAMPLER_SLOT_COUNT;
+ private const int ResourceCount = TerraFX.Interop.DirectX.D3D11.D3D11_COMMONSHADER_INPUT_RESOURCE_SLOT_COUNT;
+ private const int ClassInstanceCount = 256; // According to msdn
+
+ private ComPtr context;
+ private ComPtr shader;
+ private fixed ulong insts[ClassInstanceCount];
+ private fixed ulong buffers[BufferCount];
+ private fixed ulong samplers[SamplerCount];
+ private fixed ulong resources[ResourceCount];
+ private uint instCount;
+
+ ///
+ /// Creates a new instance of from .
+ ///
+ /// The device context.
+ /// The captured state.
+ public static PixelShaderState From(ID3D11DeviceContext* ctx)
+ {
+ var state = default(PixelShaderState);
+ state.context.Attach(ctx);
+ ctx->AddRef();
+ state.instCount = ClassInstanceCount;
+ ctx->PSGetShader(state.shader.GetAddressOf(), (ID3D11ClassInstance**)state.insts, &state.instCount);
+ ctx->PSGetConstantBuffers(0, BufferCount, (ID3D11Buffer**)state.buffers);
+ ctx->PSGetSamplers(0, SamplerCount, (ID3D11SamplerState**)state.samplers);
+ ctx->PSGetShaderResources(0, ResourceCount, (ID3D11ShaderResourceView**)state.resources);
+ return state;
+ }
+
+ ///
+ public void Dispose()
+ {
+ var ctx = this.context.Get();
+ if (ctx is null)
+ return;
+
+ fixed (PixelShaderState* pThis = &this)
+ {
+ ctx->PSSetShader(pThis->shader, (ID3D11ClassInstance**)pThis->insts, pThis->instCount);
+ ctx->PSSetConstantBuffers(0, BufferCount, (ID3D11Buffer**)pThis->buffers);
+ ctx->PSSetSamplers(0, SamplerCount, (ID3D11SamplerState**)pThis->samplers);
+ ctx->PSSetShaderResources(0, ResourceCount, (ID3D11ShaderResourceView**)pThis->resources);
+
+ foreach (ref var b in new Span>(pThis->buffers, BufferCount))
+ b.Dispose();
+ foreach (ref var b in new Span>(pThis->samplers, SamplerCount))
+ b.Dispose();
+ foreach (ref var b in new Span>(pThis->resources, ResourceCount))
+ b.Dispose();
+ foreach (ref var b in new Span>(pThis->insts, (int)pThis->instCount))
+ b.Dispose();
+ pThis->context.Dispose();
+ pThis->shader.Dispose();
+ }
+ }
+ }
+
+ ///
+ /// Captures Compute Shader states of a .
+ ///
+ [StructLayout(LayoutKind.Sequential)]
+ public struct ComputeShaderState : IDisposable
+ {
+ private const int BufferCount = TerraFX.Interop.DirectX.D3D11.D3D11_COMMONSHADER_CONSTANT_BUFFER_API_SLOT_COUNT;
+ private const int SamplerCount = TerraFX.Interop.DirectX.D3D11.D3D11_COMMONSHADER_SAMPLER_SLOT_COUNT;
+ private const int ResourceCount = TerraFX.Interop.DirectX.D3D11.D3D11_COMMONSHADER_INPUT_RESOURCE_SLOT_COUNT;
+ private const int InstanceCount = 256; // According to msdn
+ private const int UavCountMax = TerraFX.Interop.DirectX.D3D11.D3D11_1_UAV_SLOT_COUNT;
+
+ private ComPtr context;
+ private ComPtr shader;
+ private fixed ulong insts[InstanceCount]; // ID3D11ClassInstance*[BufferCount]
+ private fixed ulong buffers[BufferCount]; // ID3D11Buffer*[BufferCount]
+ private fixed ulong samplers[SamplerCount]; // ID3D11SamplerState*[SamplerCount]
+ private fixed ulong resources[ResourceCount]; // ID3D11ShaderResourceView*[ResourceCount]
+ private fixed ulong uavs[UavCountMax]; // ID3D11UnorderedAccessView*[UavCountMax]
+ private uint instCount;
+ private int uavCount;
+
+ ///
+ /// Creates a new instance of from .
+ ///
+ /// The feature level.
+ /// The device context.
+ /// The captured state.
+ public static ComputeShaderState From(D3D_FEATURE_LEVEL featureLevel, ID3D11DeviceContext* ctx)
+ {
+ var state = default(ComputeShaderState);
+ state.uavCount = featureLevel >= D3D_FEATURE_LEVEL.D3D_FEATURE_LEVEL_11_1
+ ? TerraFX.Interop.DirectX.D3D11.D3D11_1_UAV_SLOT_COUNT
+ : TerraFX.Interop.DirectX.D3D11.D3D11_PS_CS_UAV_REGISTER_COUNT;
+ state.context.Attach(ctx);
+ ctx->AddRef();
+ state.instCount = InstanceCount;
+ ctx->CSGetShader(state.shader.GetAddressOf(), (ID3D11ClassInstance**)state.insts, &state.instCount);
+ ctx->CSGetConstantBuffers(0, BufferCount, (ID3D11Buffer**)state.buffers);
+ ctx->CSGetSamplers(0, SamplerCount, (ID3D11SamplerState**)state.samplers);
+ ctx->CSGetShaderResources(0, ResourceCount, (ID3D11ShaderResourceView**)state.resources);
+ ctx->CSGetUnorderedAccessViews(0, (uint)state.uavCount, (ID3D11UnorderedAccessView**)state.uavs);
+ return state;
+ }
+
+ ///
+ public void Dispose()
+ {
+ var ctx = this.context.Get();
+ if (ctx is null)
+ return;
+
+ fixed (ComputeShaderState* pThis = &this)
+ {
+ ctx->CSSetShader(pThis->shader, (ID3D11ClassInstance**)pThis->insts, pThis->instCount);
+ ctx->CSSetConstantBuffers(0, BufferCount, (ID3D11Buffer**)pThis->buffers);
+ ctx->CSSetSamplers(0, SamplerCount, (ID3D11SamplerState**)pThis->samplers);
+ ctx->CSSetShaderResources(0, ResourceCount, (ID3D11ShaderResourceView**)pThis->resources);
+ ctx->CSSetUnorderedAccessViews(0, (uint)this.uavCount, (ID3D11UnorderedAccessView**)pThis->uavs, null);
+
+ foreach (ref var b in new Span>(pThis->buffers, BufferCount))
+ b.Dispose();
+ foreach (ref var b in new Span>(pThis->samplers, SamplerCount))
+ b.Dispose();
+ foreach (ref var b in new Span>(pThis->resources, ResourceCount))
+ b.Dispose();
+ foreach (ref var b in new Span>(pThis->insts, (int)pThis->instCount))
+ b.Dispose();
+ foreach (ref var b in new Span>(pThis->uavs, this.uavCount))
+ b.Dispose();
+ pThis->context.Dispose();
+ pThis->shader.Dispose();
+ }
+ }
+ }
+}
diff --git a/Dalamud/Interface/ImGuiBackend/Helpers/D3D11/Extensions.cs b/Dalamud/Interface/ImGuiBackend/Helpers/D3D11/Extensions.cs
new file mode 100644
index 0000000000..4d81cb5fc4
--- /dev/null
+++ b/Dalamud/Interface/ImGuiBackend/Helpers/D3D11/Extensions.cs
@@ -0,0 +1,26 @@
+using System.Text;
+
+using Dalamud.Utility;
+
+using TerraFX.Interop.DirectX;
+
+namespace Dalamud.Interface.ImGuiBackend.Helpers.D3D11;
+
+/// Utility extension methods for D3D11 objects.
+internal static class Extensions
+{
+ /// Sets the name for debugging.
+ /// D3D11 object.
+ /// Debug name.
+ /// Object type.
+ public static unsafe void SetDebugName(ref this T child, string name)
+ where T : unmanaged, ID3D11DeviceChild.Interface
+ {
+ var len = Encoding.UTF8.GetByteCount(name);
+ var buf = stackalloc byte[len + 1];
+ Encoding.UTF8.GetBytes(name, new(buf, len + 1));
+ buf[len] = 0;
+ fixed (Guid* pId = &DirectX.WKPDID_D3DDebugObjectName)
+ child.SetPrivateData(pId, (uint)(len + 1), buf).ThrowOnError();
+ }
+}
diff --git a/Dalamud/Interface/ImGuiBackend/Helpers/ImGuiViewportHelpers.cs b/Dalamud/Interface/ImGuiBackend/Helpers/ImGuiViewportHelpers.cs
new file mode 100644
index 0000000000..c951eab1be
--- /dev/null
+++ b/Dalamud/Interface/ImGuiBackend/Helpers/ImGuiViewportHelpers.cs
@@ -0,0 +1,165 @@
+using System.Diagnostics;
+using System.Linq;
+using System.Numerics;
+using System.Runtime.InteropServices;
+
+using ImGuiNET;
+
+using TerraFX.Interop.Windows;
+
+using static TerraFX.Interop.Windows.Windows;
+
+namespace Dalamud.Interface.ImGuiBackend.Helpers;
+
+///
+/// Helpers for using ImGui Viewports.
+///
+internal static class ImGuiViewportHelpers
+{
+ ///
+ /// Delegate to be called when a window should be created.
+ ///
+ /// An instance of .
+ public delegate void CreateWindowDelegate(ImGuiViewportPtr viewport);
+
+ ///
+ /// Delegate to be called when a window should be destroyed.
+ ///
+ /// An instance of .
+ public delegate void DestroyWindowDelegate(ImGuiViewportPtr viewport);
+
+ ///
+ /// Delegate to be called when a window should be resized.
+ ///
+ /// An instance of .
+ /// Size of the new window.
+ public delegate void SetWindowSizeDelegate(ImGuiViewportPtr viewport, Vector2 size);
+
+ ///
+ /// Delegate to be called when a window should be rendered.
+ ///
+ /// An instance of .
+ /// Custom user-provided argument from .
+ public delegate void RenderWindowDelegate(ImGuiViewportPtr viewport, nint v);
+
+ ///
+ /// Delegate to be called when buffers for the window should be swapped.
+ ///
+ /// An instance of .
+ /// Custom user-provided argument from .
+ public delegate void SwapBuffersDelegate(ImGuiViewportPtr viewport, nint v);
+
+ ///
+ /// Delegate to be called when the window should be showed.
+ ///
+ /// An instance of .
+ public delegate void ShowWindowDelegate(ImGuiViewportPtr viewport);
+
+ ///
+ /// Delegate to be called when the window should be updated.
+ ///
+ /// An instance of .
+ public delegate void UpdateWindowDelegate(ImGuiViewportPtr viewport);
+
+ ///
+ /// Delegate to be called when the window position is queried.
+ ///
+ /// The return value storage.
+ /// An instance of .
+ /// Same value with .
+ public unsafe delegate Vector2* GetWindowPosDelegate(Vector2* returnStorage, ImGuiViewportPtr viewport);
+
+ ///
+ /// Delegate to be called when the window should be moved.
+ ///
+ /// An instance of .
+ /// The new position.
+ public delegate void SetWindowPosDelegate(ImGuiViewportPtr viewport, Vector2 pos);
+
+ ///
+ /// Delegate to be called when the window size is queried.
+ ///
+ /// The return value storage.
+ /// An instance of .
+ /// Same value with .
+ public unsafe delegate Vector2* GetWindowSizeDelegate(Vector2* returnStorage, ImGuiViewportPtr viewport);
+
+ ///
+ /// Delegate to be called when the window should be given focus.
+ ///
+ /// An instance of .
+ public delegate void SetWindowFocusDelegate(ImGuiViewportPtr viewport);
+
+ ///
+ /// Delegate to be called when the window is focused.
+ ///
+ /// An instance of .
+ /// Whether the window is focused.
+ public delegate bool GetWindowFocusDelegate(ImGuiViewportPtr viewport);
+
+ ///
+ /// Delegate to be called when whether the window is minimized is queried.
+ ///
+ /// An instance of .
+ /// Whether the window is minimized.
+ public delegate bool GetWindowMinimizedDelegate(ImGuiViewportPtr viewport);
+
+ ///
+ /// Delegate to be called when the window title should be changed.
+ ///
+ /// An instance of .
+ /// The new title.
+ public delegate void SetWindowTitleDelegate(ImGuiViewportPtr viewport, string title);
+
+ ///
+ /// Delegate to be called when the window alpha should be changed.
+ ///
+ /// An instance of .
+ /// The new alpha.
+ public delegate void SetWindowAlphaDelegate(ImGuiViewportPtr viewport, float alpha);
+
+ ///
+ /// Delegate to be called when the IME input position should be changed.
+ ///
+ /// An instance of .
+ /// The new position.
+ public delegate void SetImeInputPosDelegate(ImGuiViewportPtr viewport, Vector2 pos);
+
+ ///
+ /// Delegate to be called when the window's DPI scale value is queried.
+ ///
+ /// An instance of .
+ /// The DPI scale.
+ public delegate float GetWindowDpiScaleDelegate(ImGuiViewportPtr viewport);
+
+ ///
+ /// Delegate to be called when viewport is changed.
+ ///
+ /// An instance of .
+ public delegate void ChangedViewportDelegate(ImGuiViewportPtr viewport);
+
+ ///
+ /// Disables ImGui from disabling alpha for Viewport window backgrounds.
+ ///
+ public static unsafe void EnableViewportWindowBackgroundAlpha()
+ {
+ // TODO: patch imgui.cpp:6126, which disables background transparency for extra viewport windows
+ var offset = 0x00007FFB6ADA632C - 0x00007FFB6AD60000;
+ offset += Process.GetCurrentProcess().Modules.Cast().First(x => x.ModuleName == "cimgui.dll")
+ .BaseAddress;
+ var b = (byte*)offset;
+ uint old;
+ if (!VirtualProtect(b, 1, PAGE.PAGE_EXECUTE_READWRITE, &old))
+ {
+ throw Marshal.GetExceptionForHR(Marshal.GetHRForLastWin32Error())
+ ?? throw new InvalidOperationException($"{nameof(VirtualProtect)} failed.");
+ }
+
+ *b = 0xEB;
+ if (!VirtualProtect(b, 1, old, &old))
+ {
+ throw Marshal.GetExceptionForHR(Marshal.GetHRForLastWin32Error())
+ ?? throw new InvalidOperationException($"{nameof(VirtualProtect)} failed.");
+ }
+ }
+}
diff --git a/Dalamud/Interface/ImGuiBackend/Helpers/ReShadePeeler.cs b/Dalamud/Interface/ImGuiBackend/Helpers/ReShadePeeler.cs
new file mode 100644
index 0000000000..c165840249
--- /dev/null
+++ b/Dalamud/Interface/ImGuiBackend/Helpers/ReShadePeeler.cs
@@ -0,0 +1,134 @@
+using System.Diagnostics;
+using System.Diagnostics.CodeAnalysis;
+using System.Runtime.InteropServices;
+
+using TerraFX.Interop.DirectX;
+using TerraFX.Interop.Windows;
+
+using static TerraFX.Interop.Windows.Windows;
+
+namespace Dalamud.Interface.ImGuiBackend.Helpers;
+
+///
+/// Peels ReShade off stuff.
+///
+[SuppressMessage(
+ "StyleCop.CSharp.LayoutRules",
+ "SA1519:Braces should not be omitted from multi-line child statement",
+ Justification = "Multiple fixed blocks")]
+internal static unsafe class ReShadePeeler
+{
+ ///
+ /// Peels if it is wrapped by ReShade.
+ ///
+ /// [inout] The COM pointer to an instance of .
+ /// A COM type that is or extends .
+ /// true if peeled.
+ public static bool PeelSwapChain(ComPtr* comptr)
+ where T : unmanaged, IDXGISwapChain.Interface =>
+ PeelIUnknown(comptr, 0x10);
+
+ ///
+ /// Peels if it is wrapped by ReShade.
+ ///
+ /// [inout] The COM pointer to an instance of .
+ /// A COM type that is or extends .
+ /// true if peeled.
+ public static bool PeelD3D12Device(ComPtr* comptr)
+ where T : unmanaged, ID3D12Device.Interface =>
+ PeelIUnknown(comptr, 0x10);
+
+ ///
+ /// Peels if it is wrapped by ReShade.
+ ///
+ /// [inout] The COM pointer to an instance of .
+ /// A COM type that is or extends .
+ /// true if peeled.
+ public static bool PeelD3D12CommandQueue(ComPtr* comptr)
+ where T : unmanaged, ID3D12CommandQueue.Interface =>
+ PeelIUnknown(comptr, 0x10);
+
+ private static bool PeelIUnknown(ComPtr* comptr, nint offset)
+ where T : unmanaged, IUnknown.Interface
+ {
+ if (comptr->Get() == null || !IsReShadedComObject(comptr->Get()))
+ return false;
+
+ var punk = new ComPtr(*(IUnknown**)((nint)comptr->Get() + offset));
+ using var comptr2 = default(ComPtr);
+ if (punk.As(&comptr2).FAILED)
+ return false;
+ comptr2.Swap(comptr);
+ return true;
+ }
+
+ private static bool BelongsInReShadeDll(nint ptr)
+ {
+ foreach (ProcessModule processModule in Process.GetCurrentProcess().Modules)
+ {
+ if (ptr < processModule.BaseAddress)
+ continue;
+
+ var dosh = (IMAGE_DOS_HEADER*)processModule.BaseAddress;
+ var nth = (IMAGE_NT_HEADERS64*)(processModule.BaseAddress + dosh->e_lfanew);
+ if (ptr >= processModule.BaseAddress + nth->OptionalHeader.SizeOfImage)
+ continue;
+
+ fixed (byte* pfn0 = "CreateDXGIFactory"u8)
+ fixed (byte* pfn1 = "D2D1CreateDevice"u8)
+ fixed (byte* pfn2 = "D3D10CreateDevice"u8)
+ fixed (byte* pfn3 = "D3D11CreateDevice"u8)
+ fixed (byte* pfn4 = "D3D12CreateDevice"u8)
+ fixed (byte* pfn5 = "glBegin"u8)
+ fixed (byte* pfn6 = "vkCreateDevice"u8)
+ {
+ if (GetProcAddress((HMODULE)dosh, (sbyte*)pfn0) == 0)
+ continue;
+ if (GetProcAddress((HMODULE)dosh, (sbyte*)pfn1) == 0)
+ continue;
+ if (GetProcAddress((HMODULE)dosh, (sbyte*)pfn2) == 0)
+ continue;
+ if (GetProcAddress((HMODULE)dosh, (sbyte*)pfn3) == 0)
+ continue;
+ if (GetProcAddress((HMODULE)dosh, (sbyte*)pfn4) == 0)
+ continue;
+ if (GetProcAddress((HMODULE)dosh, (sbyte*)pfn5) == 0)
+ continue;
+ if (GetProcAddress((HMODULE)dosh, (sbyte*)pfn6) == 0)
+ continue;
+ }
+
+ var fileInfo = FileVersionInfo.GetVersionInfo(processModule.FileName);
+
+ if (fileInfo.FileDescription == null)
+ continue;
+
+ if (!fileInfo.FileDescription.Contains("GShade") && !fileInfo.FileDescription.Contains("ReShade"))
+ continue;
+
+ return true;
+ }
+
+ return false;
+ }
+
+ private static bool IsReShadedComObject(T* obj)
+ where T : unmanaged, IUnknown.Interface
+ {
+ try
+ {
+ var vtbl = (nint**)Marshal.ReadIntPtr((nint)obj);
+ for (var i = 0; i < 3; i++)
+ {
+ if (!BelongsInReShadeDll(Marshal.ReadIntPtr((nint)(vtbl + i))))
+ return false;
+ }
+
+ return true;
+ }
+ catch
+ {
+ return false;
+ }
+ }
+}
diff --git a/Dalamud/Interface/ImGuiBackend/IImGuiBackend.cs b/Dalamud/Interface/ImGuiBackend/IImGuiBackend.cs
new file mode 100644
index 0000000000..9248b4b5a4
--- /dev/null
+++ b/Dalamud/Interface/ImGuiBackend/IImGuiBackend.cs
@@ -0,0 +1,72 @@
+using Dalamud.Interface.ImGuiBackend.InputHandler;
+using Dalamud.Interface.ImGuiBackend.Renderers;
+
+namespace Dalamud.Interface.ImGuiBackend;
+
+/// Backend for ImGui.
+internal interface IImGuiBackend : IDisposable
+{
+ /// Delegate to be called when ImGui should be used to layout now.
+ public delegate void BuildUiDelegate();
+
+ /// Delegate to be called on new input frame.
+ public delegate void NewInputFrameDelegate();
+
+ /// Delegaet to be called on new render frame.
+ public delegate void NewRenderFrameDelegate();
+
+ /// User methods invoked every ImGui frame to construct custom UIs.
+ event BuildUiDelegate? BuildUi;
+
+ /// User methods invoked every ImGui frame on handling inputs.
+ event NewInputFrameDelegate? NewInputFrame;
+
+ /// User methods invoked every ImGui frame on handling renders.
+ event NewRenderFrameDelegate? NewRenderFrame;
+
+ /// Gets or sets a value indicating whether the cursor should be overridden with the ImGui cursor.
+ ///
+ bool UpdateCursor { get; set; }
+
+ /// Gets or sets the path of ImGui configuration .ini file.
+ string? IniPath { get; set; }
+
+ /// Gets the device handle.
+ nint DeviceHandle { get; }
+
+ /// Gets the input handler.
+ IImGuiInputHandler InputHandler { get; }
+
+ /// Gets the renderer.
+ IImGuiRenderer Renderer { get; }
+
+ /// Performs a render cycle.
+ void Render();
+
+ /// Handles stuff before resizing happens.
+ void OnPreResize();
+
+ /// Handles stuff after resizing happens.
+ /// The new width.
+ /// The new height.
+ void OnPostResize(int newWidth, int newHeight);
+
+ /// Invalidates fonts immediately.
+ /// Call this while handling .
+ void InvalidateFonts();
+
+ /// Determines if is owned by this.
+ /// The cursor.
+ /// Whether it is the case.
+ bool IsImGuiCursor(nint cursorHandle);
+
+ /// Determines if this instance of is rendering to
+ /// .
+ /// The present target handle.
+ /// Whether it is the case.
+ bool IsAttachedToPresentationTarget(nint targetHandle);
+
+ /// Determines if the main viewport is full screen.
+ /// Whether it is the case.
+ bool IsMainViewportFullScreen();
+}
diff --git a/Dalamud/Interface/ImGuiBackend/IWin32Backend.cs b/Dalamud/Interface/ImGuiBackend/IWin32Backend.cs
new file mode 100644
index 0000000000..0b158dfbc2
--- /dev/null
+++ b/Dalamud/Interface/ImGuiBackend/IWin32Backend.cs
@@ -0,0 +1,15 @@
+using TerraFX.Interop.Windows;
+
+namespace Dalamud.Interface.ImGuiBackend;
+
+/// with Win32 support.
+internal interface IWin32Backend : IImGuiBackend
+{
+ /// Processes window messages.
+ /// Handle of the window.
+ /// Type of window message.
+ /// wParam.
+ /// lParam.
+ /// Return value.
+ public nint? ProcessWndProcW(HWND hWnd, uint msg, WPARAM wParam, LPARAM lParam);
+}
diff --git a/Dalamud/Interface/ImGuiBackend/InputHandler/IImGuiInputHandler.cs b/Dalamud/Interface/ImGuiBackend/InputHandler/IImGuiInputHandler.cs
new file mode 100644
index 0000000000..e6da2281e0
--- /dev/null
+++ b/Dalamud/Interface/ImGuiBackend/InputHandler/IImGuiInputHandler.cs
@@ -0,0 +1,22 @@
+namespace Dalamud.Interface.ImGuiBackend.InputHandler;
+
+/// A simple shared public interface that all ImGui input implementations follows.
+internal interface IImGuiInputHandler : IDisposable
+{
+ /// Gets or sets a value indicating whether the cursor should be overridden with the ImGui cursor.
+ ///
+ public bool UpdateCursor { get; set; }
+
+ /// Gets or sets the path of ImGui configuration .ini file.
+ string? IniPath { get; set; }
+
+ /// Determines if is owned by this.
+ /// The cursor.
+ /// Whether it is the case.
+ public bool IsImGuiCursor(nint cursorHandle);
+
+ /// Marks the beginning of a new frame.
+ /// The width of the new frame.
+ /// The height of the new frame.
+ void NewFrame(int width, int height);
+}
diff --git a/Dalamud/Interface/ImGuiBackend/InputHandler/Win32InputHandler.StaticLookupFunctions.cs b/Dalamud/Interface/ImGuiBackend/InputHandler/Win32InputHandler.StaticLookupFunctions.cs
new file mode 100644
index 0000000000..7732f0867d
--- /dev/null
+++ b/Dalamud/Interface/ImGuiBackend/InputHandler/Win32InputHandler.StaticLookupFunctions.cs
@@ -0,0 +1,309 @@
+using System.Runtime.CompilerServices;
+
+using ImGuiNET;
+
+using TerraFX.Interop.Windows;
+
+namespace Dalamud.Interface.ImGuiBackend.InputHandler;
+
+///
+/// An implementation of , using Win32 APIs.
+///
+internal sealed partial class Win32InputHandler
+{
+ ///
+ /// Maps a to .
+ ///
+ /// The virtual key.
+ /// The corresponding .
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static ImGuiKey VirtualKeyToImGuiKey(int key) => key switch
+ {
+ VK.VK_TAB => ImGuiKey.Tab,
+ VK.VK_LEFT => ImGuiKey.LeftArrow,
+ VK.VK_RIGHT => ImGuiKey.RightArrow,
+ VK.VK_UP => ImGuiKey.UpArrow,
+ VK.VK_DOWN => ImGuiKey.DownArrow,
+ VK.VK_PRIOR => ImGuiKey.PageUp,
+ VK.VK_NEXT => ImGuiKey.PageDown,
+ VK.VK_HOME => ImGuiKey.Home,
+ VK.VK_END => ImGuiKey.End,
+ VK.VK_INSERT => ImGuiKey.Insert,
+ VK.VK_DELETE => ImGuiKey.Delete,
+ VK.VK_BACK => ImGuiKey.Backspace,
+ VK.VK_SPACE => ImGuiKey.Space,
+ VK.VK_RETURN => ImGuiKey.Enter,
+ VK.VK_ESCAPE => ImGuiKey.Escape,
+ VK.VK_OEM_7 => ImGuiKey.Apostrophe,
+ VK.VK_OEM_COMMA => ImGuiKey.Comma,
+ VK.VK_OEM_MINUS => ImGuiKey.Minus,
+ VK.VK_OEM_PERIOD => ImGuiKey.Period,
+ VK.VK_OEM_2 => ImGuiKey.Slash,
+ VK.VK_OEM_1 => ImGuiKey.Semicolon,
+ VK.VK_OEM_PLUS => ImGuiKey.Equal,
+ VK.VK_OEM_4 => ImGuiKey.LeftBracket,
+ VK.VK_OEM_5 => ImGuiKey.Backslash,
+ VK.VK_OEM_6 => ImGuiKey.RightBracket,
+ VK.VK_OEM_3 => ImGuiKey.GraveAccent,
+ VK.VK_CAPITAL => ImGuiKey.CapsLock,
+ VK.VK_SCROLL => ImGuiKey.ScrollLock,
+ VK.VK_NUMLOCK => ImGuiKey.NumLock,
+ VK.VK_SNAPSHOT => ImGuiKey.PrintScreen,
+ VK.VK_PAUSE => ImGuiKey.Pause,
+ VK.VK_NUMPAD0 => ImGuiKey.Keypad0,
+ VK.VK_NUMPAD1 => ImGuiKey.Keypad1,
+ VK.VK_NUMPAD2 => ImGuiKey.Keypad2,
+ VK.VK_NUMPAD3 => ImGuiKey.Keypad3,
+ VK.VK_NUMPAD4 => ImGuiKey.Keypad4,
+ VK.VK_NUMPAD5 => ImGuiKey.Keypad5,
+ VK.VK_NUMPAD6 => ImGuiKey.Keypad6,
+ VK.VK_NUMPAD7 => ImGuiKey.Keypad7,
+ VK.VK_NUMPAD8 => ImGuiKey.Keypad8,
+ VK.VK_NUMPAD9 => ImGuiKey.Keypad9,
+ VK.VK_DECIMAL => ImGuiKey.KeypadDecimal,
+ VK.VK_DIVIDE => ImGuiKey.KeypadDivide,
+ VK.VK_MULTIPLY => ImGuiKey.KeypadMultiply,
+ VK.VK_SUBTRACT => ImGuiKey.KeypadSubtract,
+ VK.VK_ADD => ImGuiKey.KeypadAdd,
+ VK.VK_RETURN + 256 => ImGuiKey.KeypadEnter,
+ VK.VK_LSHIFT => ImGuiKey.LeftShift,
+ VK.VK_LCONTROL => ImGuiKey.LeftCtrl,
+ VK.VK_LMENU => ImGuiKey.LeftAlt,
+ VK.VK_LWIN => ImGuiKey.LeftSuper,
+ VK.VK_RSHIFT => ImGuiKey.RightShift,
+ VK.VK_RCONTROL => ImGuiKey.RightCtrl,
+ VK.VK_RMENU => ImGuiKey.RightAlt,
+ VK.VK_RWIN => ImGuiKey.RightSuper,
+ VK.VK_APPS => ImGuiKey.Menu,
+ '0' => ImGuiKey._0,
+ '1' => ImGuiKey._1,
+ '2' => ImGuiKey._2,
+ '3' => ImGuiKey._3,
+ '4' => ImGuiKey._4,
+ '5' => ImGuiKey._5,
+ '6' => ImGuiKey._6,
+ '7' => ImGuiKey._7,
+ '8' => ImGuiKey._8,
+ '9' => ImGuiKey._9,
+ 'A' => ImGuiKey.A,
+ 'B' => ImGuiKey.B,
+ 'C' => ImGuiKey.C,
+ 'D' => ImGuiKey.D,
+ 'E' => ImGuiKey.E,
+ 'F' => ImGuiKey.F,
+ 'G' => ImGuiKey.G,
+ 'H' => ImGuiKey.H,
+ 'I' => ImGuiKey.I,
+ 'J' => ImGuiKey.J,
+ 'K' => ImGuiKey.K,
+ 'L' => ImGuiKey.L,
+ 'M' => ImGuiKey.M,
+ 'N' => ImGuiKey.N,
+ 'O' => ImGuiKey.O,
+ 'P' => ImGuiKey.P,
+ 'Q' => ImGuiKey.Q,
+ 'R' => ImGuiKey.R,
+ 'S' => ImGuiKey.S,
+ 'T' => ImGuiKey.T,
+ 'U' => ImGuiKey.U,
+ 'V' => ImGuiKey.V,
+ 'W' => ImGuiKey.W,
+ 'X' => ImGuiKey.X,
+ 'Y' => ImGuiKey.Y,
+ 'Z' => ImGuiKey.Z,
+ VK.VK_F1 => ImGuiKey.F1,
+ VK.VK_F2 => ImGuiKey.F2,
+ VK.VK_F3 => ImGuiKey.F3,
+ VK.VK_F4 => ImGuiKey.F4,
+ VK.VK_F5 => ImGuiKey.F5,
+ VK.VK_F6 => ImGuiKey.F6,
+ VK.VK_F7 => ImGuiKey.F7,
+ VK.VK_F8 => ImGuiKey.F8,
+ VK.VK_F9 => ImGuiKey.F9,
+ VK.VK_F10 => ImGuiKey.F10,
+ VK.VK_F11 => ImGuiKey.F11,
+ VK.VK_F12 => ImGuiKey.F12,
+ _ => ImGuiKey.None,
+ };
+
+ ///
+ /// Maps a to .
+ ///
+ /// The ImGui key.
+ /// The corresponding .
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static int ImGuiKeyToVirtualKey(ImGuiKey key) => key switch
+ {
+ ImGuiKey.Tab => VK.VK_TAB,
+ ImGuiKey.LeftArrow => VK.VK_LEFT,
+ ImGuiKey.RightArrow => VK.VK_RIGHT,
+ ImGuiKey.UpArrow => VK.VK_UP,
+ ImGuiKey.DownArrow => VK.VK_DOWN,
+ ImGuiKey.PageUp => VK.VK_PRIOR,
+ ImGuiKey.PageDown => VK.VK_NEXT,
+ ImGuiKey.Home => VK.VK_HOME,
+ ImGuiKey.End => VK.VK_END,
+ ImGuiKey.Insert => VK.VK_INSERT,
+ ImGuiKey.Delete => VK.VK_DELETE,
+ ImGuiKey.Backspace => VK.VK_BACK,
+ ImGuiKey.Space => VK.VK_SPACE,
+ ImGuiKey.Enter => VK.VK_RETURN,
+ ImGuiKey.Escape => VK.VK_ESCAPE,
+ ImGuiKey.Apostrophe => VK.VK_OEM_7,
+ ImGuiKey.Comma => VK.VK_OEM_COMMA,
+ ImGuiKey.Minus => VK.VK_OEM_MINUS,
+ ImGuiKey.Period => VK.VK_OEM_PERIOD,
+ ImGuiKey.Slash => VK.VK_OEM_2,
+ ImGuiKey.Semicolon => VK.VK_OEM_1,
+ ImGuiKey.Equal => VK.VK_OEM_PLUS,
+ ImGuiKey.LeftBracket => VK.VK_OEM_4,
+ ImGuiKey.Backslash => VK.VK_OEM_5,
+ ImGuiKey.RightBracket => VK.VK_OEM_6,
+ ImGuiKey.GraveAccent => VK.VK_OEM_3,
+ ImGuiKey.CapsLock => VK.VK_CAPITAL,
+ ImGuiKey.ScrollLock => VK.VK_SCROLL,
+ ImGuiKey.NumLock => VK.VK_NUMLOCK,
+ ImGuiKey.PrintScreen => VK.VK_SNAPSHOT,
+ ImGuiKey.Pause => VK.VK_PAUSE,
+ ImGuiKey.Keypad0 => VK.VK_NUMPAD0,
+ ImGuiKey.Keypad1 => VK.VK_NUMPAD1,
+ ImGuiKey.Keypad2 => VK.VK_NUMPAD2,
+ ImGuiKey.Keypad3 => VK.VK_NUMPAD3,
+ ImGuiKey.Keypad4 => VK.VK_NUMPAD4,
+ ImGuiKey.Keypad5 => VK.VK_NUMPAD5,
+ ImGuiKey.Keypad6 => VK.VK_NUMPAD6,
+ ImGuiKey.Keypad7 => VK.VK_NUMPAD7,
+ ImGuiKey.Keypad8 => VK.VK_NUMPAD8,
+ ImGuiKey.Keypad9 => VK.VK_NUMPAD9,
+ ImGuiKey.KeypadDecimal => VK.VK_DECIMAL,
+ ImGuiKey.KeypadDivide => VK.VK_DIVIDE,
+ ImGuiKey.KeypadMultiply => VK.VK_MULTIPLY,
+ ImGuiKey.KeypadSubtract => VK.VK_SUBTRACT,
+ ImGuiKey.KeypadAdd => VK.VK_ADD,
+ ImGuiKey.KeypadEnter => VK.VK_RETURN + 256,
+ ImGuiKey.LeftShift => VK.VK_LSHIFT,
+ ImGuiKey.LeftCtrl => VK.VK_LCONTROL,
+ ImGuiKey.LeftAlt => VK.VK_LMENU,
+ ImGuiKey.LeftSuper => VK.VK_LWIN,
+ ImGuiKey.RightShift => VK.VK_RSHIFT,
+ ImGuiKey.RightCtrl => VK.VK_RCONTROL,
+ ImGuiKey.RightAlt => VK.VK_RMENU,
+ ImGuiKey.RightSuper => VK.VK_RWIN,
+ ImGuiKey.Menu => VK.VK_APPS,
+ ImGuiKey._0 => '0',
+ ImGuiKey._1 => '1',
+ ImGuiKey._2 => '2',
+ ImGuiKey._3 => '3',
+ ImGuiKey._4 => '4',
+ ImGuiKey._5 => '5',
+ ImGuiKey._6 => '6',
+ ImGuiKey._7 => '7',
+ ImGuiKey._8 => '8',
+ ImGuiKey._9 => '9',
+ ImGuiKey.A => 'A',
+ ImGuiKey.B => 'B',
+ ImGuiKey.C => 'C',
+ ImGuiKey.D => 'D',
+ ImGuiKey.E => 'E',
+ ImGuiKey.F => 'F',
+ ImGuiKey.G => 'G',
+ ImGuiKey.H => 'H',
+ ImGuiKey.I => 'I',
+ ImGuiKey.J => 'J',
+ ImGuiKey.K => 'K',
+ ImGuiKey.L => 'L',
+ ImGuiKey.M => 'M',
+ ImGuiKey.N => 'N',
+ ImGuiKey.O => 'O',
+ ImGuiKey.P => 'P',
+ ImGuiKey.Q => 'Q',
+ ImGuiKey.R => 'R',
+ ImGuiKey.S => 'S',
+ ImGuiKey.T => 'T',
+ ImGuiKey.U => 'U',
+ ImGuiKey.V => 'V',
+ ImGuiKey.W => 'W',
+ ImGuiKey.X => 'X',
+ ImGuiKey.Y => 'Y',
+ ImGuiKey.Z => 'Z',
+ ImGuiKey.F1 => VK.VK_F1,
+ ImGuiKey.F2 => VK.VK_F2,
+ ImGuiKey.F3 => VK.VK_F3,
+ ImGuiKey.F4 => VK.VK_F4,
+ ImGuiKey.F5 => VK.VK_F5,
+ ImGuiKey.F6 => VK.VK_F6,
+ ImGuiKey.F7 => VK.VK_F7,
+ ImGuiKey.F8 => VK.VK_F8,
+ ImGuiKey.F9 => VK.VK_F9,
+ ImGuiKey.F10 => VK.VK_F10,
+ ImGuiKey.F11 => VK.VK_F11,
+ ImGuiKey.F12 => VK.VK_F12,
+ _ => 0,
+ };
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ private static bool IsGamepadKey(ImGuiKey key) => (int)key is >= 617 and <= 640;
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ private static bool IsModKey(ImGuiKey key) =>
+ key is ImGuiKey.LeftShift
+ or ImGuiKey.RightShift
+ or ImGuiKey.ModShift
+ or ImGuiKey.LeftCtrl
+ or ImGuiKey.ModCtrl
+ or ImGuiKey.LeftAlt
+ or ImGuiKey.RightAlt
+ or ImGuiKey.ModAlt;
+
+ private static void AddKeyEvent(ImGuiKey key, bool down, int nativeKeycode, int nativeScancode = -1)
+ {
+ var io = ImGui.GetIO();
+ io.AddKeyEvent(key, down);
+ io.SetKeyEventNativeData(key, nativeKeycode, nativeScancode);
+ }
+
+ private static void UpdateKeyModifiers()
+ {
+ var io = ImGui.GetIO();
+ io.AddKeyEvent(ImGuiKey.ModCtrl, IsVkDown(VK.VK_CONTROL));
+ io.AddKeyEvent(ImGuiKey.ModShift, IsVkDown(VK.VK_SHIFT));
+ io.AddKeyEvent(ImGuiKey.ModAlt, IsVkDown(VK.VK_MENU));
+ io.AddKeyEvent(ImGuiKey.ModSuper, IsVkDown(VK.VK_APPS));
+ }
+
+ private static void UpAllKeys()
+ {
+ var io = ImGui.GetIO();
+ for (var i = (int)ImGuiKey.NamedKey_BEGIN; i < (int)ImGuiKey.NamedKey_END; i++)
+ io.AddKeyEvent((ImGuiKey)i, false);
+ }
+
+ private static void UpAllMouseButton()
+ {
+ var io = ImGui.GetIO();
+ for (var i = 0; i < io.MouseDown.Count; i++)
+ io.MouseDown[i] = false;
+ }
+
+ private static bool IsVkDown(int key) => (TerraFX.Interop.Windows.Windows.GetKeyState(key) & 0x8000) != 0;
+
+ private static int GetButton(uint msg, WPARAM wParam) => msg switch
+ {
+ WM.WM_LBUTTONUP or WM.WM_LBUTTONDOWN or WM.WM_LBUTTONDBLCLK => 0,
+ WM.WM_RBUTTONUP or WM.WM_RBUTTONDOWN or WM.WM_RBUTTONDBLCLK => 1,
+ WM.WM_MBUTTONUP or WM.WM_MBUTTONDOWN or WM.WM_MBUTTONDBLCLK => 2,
+ WM.WM_XBUTTONUP or WM.WM_XBUTTONDOWN or WM.WM_XBUTTONDBLCLK =>
+ TerraFX.Interop.Windows.Windows.GET_XBUTTON_WPARAM(wParam) == TerraFX.Interop.Windows.Windows.XBUTTON1 ? 3 : 4,
+ _ => 0,
+ };
+
+ private static void ViewportFlagsToWin32Styles(ImGuiViewportFlags flags, out int style, out int exStyle)
+ {
+ style = (int)(flags.HasFlag(ImGuiViewportFlags.NoDecoration) ? WS.WS_POPUP : WS.WS_OVERLAPPEDWINDOW);
+ exStyle =
+ (int)(flags.HasFlag(ImGuiViewportFlags.NoTaskBarIcon) ? WS.WS_EX_TOOLWINDOW : (uint)WS.WS_EX_APPWINDOW);
+ exStyle |= WS.WS_EX_NOREDIRECTIONBITMAP;
+ if (flags.HasFlag(ImGuiViewportFlags.TopMost))
+ exStyle |= WS.WS_EX_TOPMOST;
+ }
+}
diff --git a/Dalamud/Interface/ImGuiBackend/InputHandler/Win32InputHandler.cs b/Dalamud/Interface/ImGuiBackend/InputHandler/Win32InputHandler.cs
new file mode 100644
index 0000000000..18fd826511
--- /dev/null
+++ b/Dalamud/Interface/ImGuiBackend/InputHandler/Win32InputHandler.cs
@@ -0,0 +1,1016 @@
+using System.Collections.Generic;
+using System.Diagnostics;
+using System.Diagnostics.CodeAnalysis;
+using System.Linq;
+using System.Numerics;
+using System.Runtime.InteropServices;
+using System.Text;
+
+using ImGuiNET;
+
+using TerraFX.Interop.Windows;
+
+using static Dalamud.Interface.ImGuiBackend.Helpers.ImGuiViewportHelpers;
+
+using static TerraFX.Interop.Windows.Windows;
+
+using ERROR = TerraFX.Interop.Windows.ERROR;
+
+namespace Dalamud.Interface.ImGuiBackend.InputHandler;
+
+///
+/// An implementation of , using Win32 APIs.
+/// Largely a port of https://github.com/ocornut/imgui/blob/master/examples/imgui_impl_win32.cpp,
+/// though some changes and wndproc hooking.
+///
+[SuppressMessage(
+ "StyleCop.CSharp.LayoutRules",
+ "SA1519:Braces should not be omitted from multi-line child statement",
+ Justification = "Multiple fixed/using scopes")]
+internal sealed unsafe partial class Win32InputHandler : IImGuiInputHandler
+{
+ private readonly HWND hWnd;
+ private readonly HCURSOR[] cursors;
+
+ private readonly WndProcDelegate wndProcDelegate;
+ private readonly bool[] imguiMouseIsDown;
+ private readonly nint platformNamePtr;
+
+ private ViewportHandler viewportHandler;
+
+ private long lastTime;
+
+ private nint iniPathPtr;
+
+ private bool disposedValue;
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The window handle.
+ public Win32InputHandler(HWND hWnd)
+ {
+ var io = ImGui.GetIO();
+ if (ImGui.GetIO().NativePtr->BackendPlatformName is not null)
+ throw new InvalidOperationException("ImGui backend platform seems to be have been already attached.");
+
+ this.hWnd = hWnd;
+
+ // hook wndproc
+ // have to hold onto the delegate to keep it in memory for unmanaged code
+ this.wndProcDelegate = this.WndProcDetour;
+
+ io.BackendFlags |= ImGuiBackendFlags.HasMouseCursors |
+ ImGuiBackendFlags.HasSetMousePos |
+ ImGuiBackendFlags.RendererHasViewports |
+ ImGuiBackendFlags.PlatformHasViewports;
+
+ this.platformNamePtr = Marshal.StringToHGlobalAnsi("imgui_impl_win32_c#");
+ io.NativePtr->BackendPlatformName = (byte*)this.platformNamePtr;
+
+ var mainViewport = ImGui.GetMainViewport();
+ mainViewport.PlatformHandle = mainViewport.PlatformHandleRaw = hWnd;
+ if (io.ConfigFlags.HasFlag(ImGuiConfigFlags.ViewportsEnable))
+ this.viewportHandler = new(this);
+
+ this.imguiMouseIsDown = new bool[5];
+
+ this.cursors = new HCURSOR[9];
+ this.cursors[(int)ImGuiMouseCursor.Arrow] = LoadCursorW(default, IDC.IDC_ARROW);
+ this.cursors[(int)ImGuiMouseCursor.TextInput] = LoadCursorW(default, IDC.IDC_IBEAM);
+ this.cursors[(int)ImGuiMouseCursor.ResizeAll] = LoadCursorW(default, IDC.IDC_SIZEALL);
+ this.cursors[(int)ImGuiMouseCursor.ResizeEW] = LoadCursorW(default, IDC.IDC_SIZEWE);
+ this.cursors[(int)ImGuiMouseCursor.ResizeNS] = LoadCursorW(default, IDC.IDC_SIZENS);
+ this.cursors[(int)ImGuiMouseCursor.ResizeNESW] = LoadCursorW(default, IDC.IDC_SIZENESW);
+ this.cursors[(int)ImGuiMouseCursor.ResizeNWSE] = LoadCursorW(default, IDC.IDC_SIZENWSE);
+ this.cursors[(int)ImGuiMouseCursor.Hand] = LoadCursorW(default, IDC.IDC_HAND);
+ this.cursors[(int)ImGuiMouseCursor.NotAllowed] = LoadCursorW(default, IDC.IDC_NO);
+ }
+
+ ///
+ /// Finalizes an instance of the class.
+ ///
+ ~Win32InputHandler() => this.ReleaseUnmanagedResources();
+
+ private delegate LRESULT WndProcDelegate(HWND hWnd, uint uMsg, WPARAM wparam, LPARAM lparam);
+
+ private delegate BOOL MonitorEnumProcDelegate(HMONITOR monitor, HDC hdc, RECT* rect, LPARAM lparam);
+
+ ///
+ public bool UpdateCursor { get; set; } = true;
+
+ ///
+ public string? IniPath
+ {
+ get
+ {
+ var ptr = (byte*)this.iniPathPtr;
+ if (ptr is null)
+ return string.Empty;
+ var len = 0;
+ while (ptr![len] != 0)
+ len++;
+ return Encoding.UTF8.GetString(ptr, len);
+ }
+
+ set
+ {
+ if (this.iniPathPtr != 0)
+ Marshal.FreeHGlobal(this.iniPathPtr);
+ if (!string.IsNullOrEmpty(value))
+ {
+ var e = Encoding.UTF8.GetByteCount(value);
+ var newAlloc = Marshal.AllocHGlobal(e + 2);
+ try
+ {
+ var span = new Span((void*)newAlloc, e + 2);
+ span[^1] = span[^2] = 0;
+ Encoding.UTF8.GetBytes(value, span);
+ }
+ catch
+ {
+ Marshal.FreeHGlobal(newAlloc);
+ throw;
+ }
+
+ this.iniPathPtr = newAlloc;
+ }
+
+ ImGui.GetIO().NativePtr->IniFilename = (byte*)this.iniPathPtr;
+ }
+ }
+
+ ///
+ public void Dispose()
+ {
+ this.ReleaseUnmanagedResources();
+ GC.SuppressFinalize(this);
+ }
+
+ ///
+ public bool IsImGuiCursor(nint hCursor) => this.cursors.Contains((HCURSOR)hCursor);
+
+ ///
+ public void NewFrame(int targetWidth, int targetHeight)
+ {
+ var io = ImGui.GetIO();
+
+ io.DisplaySize.X = targetWidth;
+ io.DisplaySize.Y = targetHeight;
+ io.DisplayFramebufferScale.X = 1f;
+ io.DisplayFramebufferScale.Y = 1f;
+
+ var frequency = Stopwatch.Frequency;
+ var currentTime = Stopwatch.GetTimestamp();
+ io.DeltaTime = this.lastTime > 0 ? (float)((double)(currentTime - this.lastTime) / frequency) : 1f / 60;
+ this.lastTime = currentTime;
+
+ this.viewportHandler.UpdateMonitors();
+
+ this.UpdateMousePos();
+
+ this.ProcessKeyEventsWorkarounds();
+
+ // TODO: need to figure out some way to unify all this
+ // The bottom case works(?) if the caller hooks SetCursor, but otherwise causes fps issues
+ // The top case more or less works if we use ImGui's software cursor (and ideally hide the
+ // game's hardware cursor)
+ // It would be nice if hooking WM_SETCURSOR worked as it 'should' so that external hooking
+ // wasn't necessary
+
+ // this is what imgui's example does, but it doesn't seem to work for us
+ // this could be a timing issue.. or their logic could just be wrong for many applications
+ // var cursor = io.MouseDrawCursor ? ImGuiMouseCursor.None : ImGui.GetMouseCursor();
+ // if (_oldCursor != cursor)
+ // {
+ // _oldCursor = cursor;
+ // UpdateMouseCursor();
+ // }
+
+ // hacky attempt to make cursors work how I think they 'should'
+ if ((io.WantCaptureMouse || io.MouseDrawCursor) && this.UpdateCursor)
+ {
+ this.UpdateMouseCursor();
+ }
+
+ // Similar issue seen with overlapping mouse clicks
+ // eg, right click and hold on imgui window, drag off, left click and hold
+ // release right click, release left click -> right click was 'stuck' and imgui
+ // would become unresponsive
+ if (!io.WantCaptureMouse)
+ {
+ for (var i = 0; i < io.MouseDown.Count; i++)
+ {
+ io.MouseDown[i] = false;
+ }
+ }
+ }
+
+ ///
+ /// Processes window messages. Supports both WndProcA and WndProcW.
+ ///
+ /// Handle of the window.
+ /// Type of window message.
+ /// wParam.
+ /// lParam.
+ /// Return value, if not doing further processing.
+ public LRESULT? ProcessWndProcW(HWND hWndCurrent, uint msg, WPARAM wParam, LPARAM lParam)
+ {
+ if (ImGui.GetCurrentContext() == nint.Zero)
+ return null;
+
+ var io = ImGui.GetIO();
+
+ switch (msg)
+ {
+ case WM.WM_LBUTTONDOWN:
+ case WM.WM_LBUTTONDBLCLK:
+ case WM.WM_RBUTTONDOWN:
+ case WM.WM_RBUTTONDBLCLK:
+ case WM.WM_MBUTTONDOWN:
+ case WM.WM_MBUTTONDBLCLK:
+ case WM.WM_XBUTTONDOWN:
+ case WM.WM_XBUTTONDBLCLK:
+ {
+ var button = GetButton(msg, wParam);
+ if (io.WantCaptureMouse)
+ {
+ if (!ImGui.IsAnyMouseDown() && GetCapture() == nint.Zero)
+ SetCapture(hWndCurrent);
+
+ io.MouseDown[button] = true;
+ this.imguiMouseIsDown[button] = true;
+ return default(LRESULT);
+ }
+
+ break;
+ }
+
+ case WM.WM_LBUTTONUP:
+ case WM.WM_RBUTTONUP:
+ case WM.WM_MBUTTONUP:
+ case WM.WM_XBUTTONUP:
+ {
+ var button = GetButton(msg, wParam);
+ if (io.WantCaptureMouse && this.imguiMouseIsDown[button])
+ {
+ if (!ImGui.IsAnyMouseDown() && GetCapture() == hWndCurrent)
+ ReleaseCapture();
+
+ io.MouseDown[button] = false;
+ this.imguiMouseIsDown[button] = false;
+ return default(LRESULT);
+ }
+
+ break;
+ }
+
+ case WM.WM_MOUSEWHEEL:
+ if (io.WantCaptureMouse)
+ {
+ io.MouseWheel += GET_WHEEL_DELTA_WPARAM(wParam) / (float)WHEEL_DELTA;
+ return default(LRESULT);
+ }
+
+ break;
+ case WM.WM_MOUSEHWHEEL:
+ if (io.WantCaptureMouse)
+ {
+ io.MouseWheelH += GET_WHEEL_DELTA_WPARAM(wParam) / (float)WHEEL_DELTA;
+ return default(LRESULT);
+ }
+
+ break;
+ case WM.WM_KEYDOWN:
+ case WM.WM_SYSKEYDOWN:
+ case WM.WM_KEYUP:
+ case WM.WM_SYSKEYUP:
+ {
+ var isKeyDown = msg is WM.WM_KEYDOWN or WM.WM_SYSKEYDOWN;
+ if ((int)wParam >= 256)
+ break;
+
+ // Submit modifiers
+ UpdateKeyModifiers();
+
+ // Obtain virtual key code
+ // (keypad enter doesn't have its own... VK_RETURN with KF_EXTENDED flag means keypad enter, see IM_VK_KEYPAD_ENTER definition for details, it is mapped to ImGuiKey.KeyPadEnter.)
+ var vk = (int)wParam;
+ if (vk == VK.VK_RETURN && ((int)lParam & (256 << 16)) > 0)
+ vk = VK.VK_RETURN + 256;
+
+ // Submit key event
+ var key = VirtualKeyToImGuiKey(vk);
+ var scancode = ((int)lParam & 0xff0000) >> 16;
+ if (key != ImGuiKey.None && io.WantTextInput)
+ {
+ AddKeyEvent(key, isKeyDown, vk, scancode);
+ return nint.Zero;
+ }
+
+ switch (vk)
+ {
+ // Submit individual left/right modifier events
+ case VK.VK_SHIFT:
+ // Important: Shift keys tend to get stuck when pressed together, missing key-up events are corrected in OnProcessKeyEventsWorkarounds()
+ if (IsVkDown(VK.VK_LSHIFT) == isKeyDown)
+ AddKeyEvent(ImGuiKey.LeftShift, isKeyDown, VK.VK_LSHIFT, scancode);
+
+ if (IsVkDown(VK.VK_RSHIFT) == isKeyDown)
+ AddKeyEvent(ImGuiKey.RightShift, isKeyDown, VK.VK_RSHIFT, scancode);
+
+ break;
+
+ case VK.VK_CONTROL:
+ if (IsVkDown(VK.VK_LCONTROL) == isKeyDown)
+ AddKeyEvent(ImGuiKey.LeftCtrl, isKeyDown, VK.VK_LCONTROL, scancode);
+
+ if (IsVkDown(VK.VK_RCONTROL) == isKeyDown)
+ AddKeyEvent(ImGuiKey.RightCtrl, isKeyDown, VK.VK_RCONTROL, scancode);
+
+ break;
+
+ case VK.VK_MENU:
+ if (IsVkDown(VK.VK_LMENU) == isKeyDown)
+ AddKeyEvent(ImGuiKey.LeftAlt, isKeyDown, VK.VK_LMENU, scancode);
+
+ if (IsVkDown(VK.VK_RMENU) == isKeyDown)
+ AddKeyEvent(ImGuiKey.RightAlt, isKeyDown, VK.VK_RMENU, scancode);
+
+ break;
+ }
+
+ break;
+ }
+
+ case WM.WM_CHAR:
+ if (io.WantTextInput)
+ {
+ io.AddInputCharacter((uint)wParam);
+ return nint.Zero;
+ }
+
+ break;
+
+ // this never seemed to work reasonably, but I'll leave it for now
+ case WM.WM_SETCURSOR:
+ if (io.WantCaptureMouse)
+ {
+ if (LOWORD(lParam) == HTCLIENT && this.UpdateMouseCursor())
+ {
+ // this message returns 1 to block further processing
+ // because consistency is no fun
+ return 1;
+ }
+ }
+
+ break;
+
+ case WM.WM_DISPLAYCHANGE:
+ this.viewportHandler.UpdateMonitors();
+ break;
+ }
+
+ return null;
+ }
+
+ private void UpdateMousePos()
+ {
+ var io = ImGui.GetIO();
+ var pt = default(POINT);
+
+ // Depending on if Viewports are enabled, we have to change how we process
+ // the cursor position. If viewports are enabled, we pass the absolute cursor
+ // position to ImGui. Otherwise, we use the old method of passing client-local
+ // mouse position to ImGui.
+ if (io.ConfigFlags.HasFlag(ImGuiConfigFlags.ViewportsEnable))
+ {
+ if (io.WantSetMousePos)
+ {
+ SetCursorPos((int)io.MousePos.X, (int)io.MousePos.Y);
+ }
+
+ if (GetCursorPos(&pt))
+ {
+ io.MousePos.X = pt.x;
+ io.MousePos.Y = pt.y;
+ }
+ else
+ {
+ io.MousePos.X = float.MinValue;
+ io.MousePos.Y = float.MinValue;
+ }
+ }
+ else
+ {
+ if (io.WantSetMousePos)
+ {
+ pt.x = (int)io.MousePos.X;
+ pt.y = (int)io.MousePos.Y;
+ ClientToScreen(this.hWnd, &pt);
+ SetCursorPos(pt.x, pt.y);
+ }
+
+ if (GetCursorPos(&pt) && ScreenToClient(this.hWnd, &pt))
+ {
+ io.MousePos.X = pt.x;
+ io.MousePos.Y = pt.y;
+ }
+ else
+ {
+ io.MousePos.X = float.MinValue;
+ io.MousePos.Y = float.MinValue;
+ }
+ }
+ }
+
+ private bool UpdateMouseCursor()
+ {
+ var io = ImGui.GetIO();
+ if (io.ConfigFlags.HasFlag(ImGuiConfigFlags.NoMouseCursorChange))
+ return false;
+
+ var cur = ImGui.GetMouseCursor();
+ if (cur == ImGuiMouseCursor.None || io.MouseDrawCursor)
+ SetCursor(default);
+ else
+ SetCursor(this.cursors[(int)cur]);
+
+ return true;
+ }
+
+ private void ProcessKeyEventsWorkarounds()
+ {
+ // Left & right Shift keys: when both are pressed together, Windows tend to not generate the WM_KEYUP event for the first released one.
+ if (ImGui.IsKeyDown(ImGuiKey.LeftShift) && !IsVkDown(VK.VK_LSHIFT))
+ AddKeyEvent(ImGuiKey.LeftShift, false, VK.VK_LSHIFT);
+ if (ImGui.IsKeyDown(ImGuiKey.RightShift) && !IsVkDown(VK.VK_RSHIFT))
+ AddKeyEvent(ImGuiKey.RightShift, false, VK.VK_RSHIFT);
+
+ // Sometimes WM_KEYUP for Win key is not passed down to the app (e.g. for Win+V on some setups, according to GLFW).
+ if (ImGui.IsKeyDown(ImGuiKey.LeftSuper) && !IsVkDown(VK.VK_LWIN))
+ AddKeyEvent(ImGuiKey.LeftSuper, false, VK.VK_LWIN);
+ if (ImGui.IsKeyDown(ImGuiKey.RightSuper) && !IsVkDown(VK.VK_RWIN))
+ AddKeyEvent(ImGuiKey.RightSuper, false, VK.VK_RWIN);
+
+ // From ImGui's FAQ:
+ // Note: Text input widget releases focus on "Return KeyDown", so the subsequent "Return KeyUp" event
+ // that your application receive will typically have io.WantCaptureKeyboard == false. Depending on your
+ // application logic it may or not be inconvenient.
+ //
+ // With how the local wndproc works, this causes the key up event to be missed when exiting ImGui text entry
+ // (eg, from hitting enter or escape. There may be other ways as well)
+ // This then causes the key to appear 'stuck' down, which breaks subsequent attempts to use the input field.
+ // This is something of a brute force fix that basically makes key up events irrelevant
+ // Holding a key will send repeated key down events and (re)set these where appropriate, so this should be ok.
+ var io = ImGui.GetIO();
+ if (!io.WantTextInput)
+ {
+ // See: https://github.com/goatcorp/ImGuiScene/pull/13
+ // > GetForegroundWindow from winuser.h is a surprisingly expensive function.
+ var isForeground = GetForegroundWindow() == this.hWnd;
+ for (var i = (int)ImGuiKey.NamedKey_BEGIN; i < (int)ImGuiKey.NamedKey_END; i++)
+ {
+ // Skip raising modifier keys if the game is focused.
+ // This allows us to raise the keys when one is held and the window becomes unfocused,
+ // but if we do not skip them, they will only be held down every 4th frame or so.
+ if (isForeground && (IsGamepadKey((ImGuiKey)i) || IsModKey((ImGuiKey)i)))
+ continue;
+ io.AddKeyEvent((ImGuiKey)i, false);
+ }
+ }
+ }
+
+ ///
+ /// This WndProc is called for ImGuiScene windows. WndProc for main window will be called back from somewhere else.
+ ///
+ private LRESULT WndProcDetour(HWND hWndCurrent, uint msg, WPARAM wParam, LPARAM lParam)
+ {
+ // Attempt to process the result of this window message
+ // We will return the result here if we consider the message handled
+ var processResult = this.ProcessWndProcW(hWndCurrent, msg, wParam, lParam);
+
+ if (processResult != null) return processResult.Value;
+
+ // The message wasn't handled, but it's a platform window
+ // So we have to handle some messages ourselves
+ // BUT we might have disposed the context, so check that
+ if (ImGui.GetCurrentContext() == nint.Zero)
+ return DefWindowProcW(hWndCurrent, msg, wParam, lParam);
+
+ var viewport = ImGui.FindViewportByPlatformHandle(hWndCurrent);
+ if (viewport.NativePtr == null)
+ return DefWindowProcW(hWndCurrent, msg, wParam, lParam);
+
+ switch (msg)
+ {
+ case WM.WM_CLOSE:
+ viewport.PlatformRequestClose = true;
+ return 0;
+ case WM.WM_MOVE:
+ viewport.PlatformRequestMove = true;
+ return 0;
+ case WM.WM_SIZE:
+ viewport.PlatformRequestResize = true;
+ return 0;
+ case WM.WM_MOUSEACTIVATE:
+ // We never want our platform windows to be active, or else Windows will think we
+ // want messages dispatched with its hWnd. We don't. The only way to activate a platform
+ // window is via clicking, it does not appear on the taskbar or alt-tab, so we just
+ // brute force behavior here.
+
+ // Make the game the foreground window. This prevents ImGui windows from becoming
+ // choppy when users have the "limit FPS" option enabled in-game
+ SetForegroundWindow(this.hWnd);
+
+ // Also set the window capture to the main window, as focus will not cause
+ // future messages to be dispatched to the main window unless it is receiving capture
+ SetCapture(this.hWnd);
+
+ // We still want to return MA_NOACTIVATE
+ // https://docs.microsoft.com/en-us/windows/win32/inputdev/wm-mouseactivate
+ return 0x3;
+ case WM.WM_NCHITTEST:
+ // Let mouse pass-through the window. This will allow the backend to set io.MouseHoveredViewport properly (which is OPTIONAL).
+ // The ImGuiViewportFlags_NoInputs flag is set while dragging a viewport, as want to detect the window behind the one we are dragging.
+ // If you cannot easily access those viewport flags from your windowing/event code: you may manually synchronize its state e.g. in
+ // your main loop after calling UpdatePlatformWindows(). Iterate all viewports/platform windows and pass the flag to your windowing system.
+ if (viewport.Flags.HasFlag(ImGuiViewportFlags.NoInputs))
+ {
+ // https://docs.microsoft.com/en-us/windows/win32/inputdev/wm-nchittest
+ return -1;
+ }
+
+ break;
+ }
+
+ return DefWindowProcW(hWndCurrent, msg, wParam, lParam);
+ }
+
+ private void ReleaseUnmanagedResources()
+ {
+ if (this.disposedValue)
+ return;
+
+ this.viewportHandler.Dispose();
+
+ this.cursors.AsSpan().Clear();
+
+ if (ImGui.GetIO().NativePtr->BackendPlatformName == (void*)this.platformNamePtr)
+ ImGui.GetIO().NativePtr->BackendPlatformName = null;
+ if (this.platformNamePtr != nint.Zero)
+ Marshal.FreeHGlobal(this.platformNamePtr);
+
+ if (this.iniPathPtr != nint.Zero)
+ {
+ ImGui.GetIO().NativePtr->IniFilename = null;
+ Marshal.FreeHGlobal(this.iniPathPtr);
+ this.iniPathPtr = nint.Zero;
+ }
+
+ this.disposedValue = true;
+ }
+
+ private struct ViewportHandler : IDisposable
+ {
+ [SuppressMessage("ReSharper", "CollectionNeverQueried.Local", Justification = "Keeping references alive")]
+ private readonly List