diff --git a/.gitmodules b/.gitmodules index dd184b54e4..4b7cf491a1 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,6 +1,3 @@ -[submodule "lib/ImGuiScene"] - path = lib/ImGuiScene - url = https://github.com/goatcorp/ImGuiScene [submodule "lib/FFXIVClientStructs"] path = lib/FFXIVClientStructs url = https://github.com/aers/FFXIVClientStructs @@ -10,3 +7,6 @@ [submodule "lib/TsudaKageyu-minhook"] path = lib/TsudaKageyu-minhook url = https://github.com/TsudaKageyu/minhook.git +[submodule "lib/ImGui.NET"] + path = lib/ImGui.NET + url = https://github.com/goatcorp/ImGui.NET diff --git a/Dalamud.Boot/DalamudStartInfo.cpp b/Dalamud.Boot/DalamudStartInfo.cpp index 985332966b..f342973b08 100644 --- a/Dalamud.Boot/DalamudStartInfo.cpp +++ b/Dalamud.Boot/DalamudStartInfo.cpp @@ -116,6 +116,7 @@ void from_json(const nlohmann::json& json, DalamudStartInfo& config) { config.NoLoadThirdPartyPlugins = json.value("NoLoadThirdPartyPlugins", config.NoLoadThirdPartyPlugins); config.BootLogPath = json.value("BootLogPath", config.BootLogPath); + config.BootDebugDirectX = json.value("BootDebugDirectX", config.BootDebugDirectX); config.BootShowConsole = json.value("BootShowConsole", config.BootShowConsole); config.BootDisableFallbackConsole = json.value("BootDisableFallbackConsole", config.BootDisableFallbackConsole); config.BootWaitMessageBox = json.value("BootWaitMessageBox", config.BootWaitMessageBox); diff --git a/Dalamud.Boot/DalamudStartInfo.h b/Dalamud.Boot/DalamudStartInfo.h index cc31ba2c59..d6524968ef 100644 --- a/Dalamud.Boot/DalamudStartInfo.h +++ b/Dalamud.Boot/DalamudStartInfo.h @@ -54,6 +54,7 @@ struct DalamudStartInfo { bool NoLoadThirdPartyPlugins; std::string BootLogPath; + bool BootDebugDirectX = false; bool BootShowConsole = false; bool BootDisableFallbackConsole = false; WaitMessageboxFlags BootWaitMessageBox = WaitMessageboxFlags::None; diff --git a/Dalamud.Boot/dllmain.cpp b/Dalamud.Boot/dllmain.cpp index 5c7c00b686..2c5f2e3d5c 100644 --- a/Dalamud.Boot/dllmain.cpp +++ b/Dalamud.Boot/dllmain.cpp @@ -1,6 +1,11 @@ #include "pch.h" +#include +#include +#include + #include "DalamudStartInfo.h" +#include "hooks.h" #include "logging.h" #include "utils.h" #include "veh.h" @@ -98,6 +103,69 @@ HRESULT WINAPI InitializeImpl(LPVOID lpParam, HANDLE hMainThreadContinue) { logging::W("Skipping fixes, as MinHook has failed to load."); } + if (g_startInfo.BootDebugDirectX) { + logging::I("Enabling DirectX Debugging."); + + const auto hD3D11 = GetModuleHandleW(L"d3d11.dll"); + const auto hDXGI = GetModuleHandleW(L"dxgi.dll"); + const auto pfnD3D11CreateDevice = static_cast( + hD3D11 ? static_cast(GetProcAddress(hD3D11, "D3D11CreateDevice")) : nullptr); + if (pfnD3D11CreateDevice) { + static hooks::direct_hook s_hookD3D11CreateDevice( + "d3d11.dll!D3D11CreateDevice", + pfnD3D11CreateDevice); + s_hookD3D11CreateDevice.set_detour([]( + IDXGIAdapter* pAdapter, + D3D_DRIVER_TYPE DriverType, + HMODULE Software, + UINT Flags, + const D3D_FEATURE_LEVEL* pFeatureLevels, + UINT FeatureLevels, + UINT SDKVersion, + ID3D11Device** ppDevice, + D3D_FEATURE_LEVEL* pFeatureLevel, + ID3D11DeviceContext** ppImmediateContext + ) -> HRESULT { + return s_hookD3D11CreateDevice.call_original( + pAdapter, + DriverType, + Software, + Flags | D3D11_CREATE_DEVICE_DEBUG, + pFeatureLevels, + FeatureLevels, + SDKVersion, + ppDevice, + pFeatureLevel, + ppImmediateContext); + }); + } else { + logging::W("Could not find d3d11!D3D11CreateDevice."); + } + + const auto pfnCreateDXGIFactory = static_cast( + hDXGI ? static_cast(GetProcAddress(hDXGI, "CreateDXGIFactory")) : nullptr); + const auto pfnCreateDXGIFactory1 = static_cast( + hDXGI ? static_cast(GetProcAddress(hDXGI, "CreateDXGIFactory1")) : nullptr); + static const auto pfnCreateDXGIFactory2 = static_cast( + hDXGI ? static_cast(GetProcAddress(hDXGI, "CreateDXGIFactory2")) : nullptr); + if (pfnCreateDXGIFactory2) { + static hooks::direct_hook s_hookCreateDXGIFactory( + "dxgi.dll!CreateDXGIFactory", + pfnCreateDXGIFactory); + static hooks::direct_hook s_hookCreateDXGIFactory1( + "dxgi.dll!CreateDXGIFactory1", + pfnCreateDXGIFactory1); + s_hookCreateDXGIFactory.set_detour([](REFIID riid, _COM_Outptr_ void **ppFactory) -> HRESULT { + return pfnCreateDXGIFactory2(DXGI_CREATE_FACTORY_DEBUG, riid, ppFactory); + }); + s_hookCreateDXGIFactory1.set_detour([](REFIID riid, _COM_Outptr_ void **ppFactory) -> HRESULT { + return pfnCreateDXGIFactory2(DXGI_CREATE_FACTORY_DEBUG, riid, ppFactory); + }); + } else { + logging::W("Could not find dxgi!CreateDXGIFactory2."); + } + } + if (g_startInfo.BootWaitDebugger) { logging::I("Waiting for debugger to attach..."); while (!IsDebuggerPresent()) diff --git a/Dalamud.Common/DalamudStartInfo.cs b/Dalamud.Common/DalamudStartInfo.cs index c3cc33a123..a6ff53f3e9 100644 --- a/Dalamud.Common/DalamudStartInfo.cs +++ b/Dalamud.Common/DalamudStartInfo.cs @@ -89,6 +89,11 @@ public DalamudStartInfo() /// public string? BootLogPath { get; set; } + /// + /// Gets or sets a value indicating whether to enable D3D11 and DXGI debugging if possible. + /// + public bool BootDebugDirectX { get; set; } + /// /// Gets or sets a value indicating whether a Boot console should be shown. /// diff --git a/Dalamud.CorePlugin/Dalamud.CorePlugin.csproj b/Dalamud.CorePlugin/Dalamud.CorePlugin.csproj index 626788a7a9..b9bc63cd1f 100644 --- a/Dalamud.CorePlugin/Dalamud.CorePlugin.csproj +++ b/Dalamud.CorePlugin/Dalamud.CorePlugin.csproj @@ -40,13 +40,7 @@ false - - false - - - false - - + false diff --git a/Dalamud.Injector/EntryPoint.cs b/Dalamud.Injector/EntryPoint.cs index f22c2923e1..cce40da768 100644 --- a/Dalamud.Injector/EntryPoint.cs +++ b/Dalamud.Injector/EntryPoint.cs @@ -89,6 +89,7 @@ public static int Main(int argc, IntPtr argvPtr) startInfo = ExtractAndInitializeStartInfoFromArguments(startInfo, args); // Remove already handled arguments + args.Remove("--debug-directx"); args.Remove("--console"); args.Remove("--msgbox1"); args.Remove("--msgbox2"); @@ -396,6 +397,7 @@ private static DalamudStartInfo ExtractAndInitializeStartInfoFromArguments(Dalam startInfo.LogName ??= string.Empty; // Set boot defaults + startInfo.BootDebugDirectX = args.Contains("--debug-directx"); startInfo.BootShowConsole = args.Contains("--console"); startInfo.BootEnableEtw = args.Contains("--etw"); startInfo.BootLogPath = GetLogPath(startInfo.LogPath, "dalamud.boot", startInfo.LogName); diff --git a/Dalamud.sln b/Dalamud.sln index 22cc59a8d9..d1d18b74c7 100644 --- a/Dalamud.sln +++ b/Dalamud.sln @@ -27,11 +27,7 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Dalamud.Test", "Dalamud.Tes EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Interface", "Interface", "{E15BDA6D-E881-4482-94BA-BE5527E917FF}" EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ImGui.NET-472", "lib\ImGuiScene\deps\ImGui.NET\src\ImGui.NET-472\ImGui.NET-472.csproj", "{0483026E-C6CE-4B1A-AA68-46544C08140B}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ImGuiScene", "lib\ImGuiScene\ImGuiScene\ImGuiScene.csproj", "{C0E7E797-4FBF-4F46-BC57-463F3719BA7A}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SDL2-CS", "lib\ImGuiScene\deps\SDL2-CS\SDL2-CS.csproj", "{2F7FF0A8-B619-4572-86C7-71E46FE22FB8}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ImGui.NET-472", "lib\ImGui.NET\src\ImGui.NET-472\ImGui.NET-472.csproj", "{0483026E-C6CE-4B1A-AA68-46544C08140B}" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Dalamud.CorePlugin", "Dalamud.CorePlugin\Dalamud.CorePlugin.csproj", "{4AFDB34A-7467-4D41-B067-53BC4101D9D0}" EndProject @@ -127,8 +123,6 @@ Global EndGlobalSection GlobalSection(NestedProjects) = preSolution {0483026E-C6CE-4B1A-AA68-46544C08140B} = {E15BDA6D-E881-4482-94BA-BE5527E917FF} - {C0E7E797-4FBF-4F46-BC57-463F3719BA7A} = {E15BDA6D-E881-4482-94BA-BE5527E917FF} - {2F7FF0A8-B619-4572-86C7-71E46FE22FB8} = {E15BDA6D-E881-4482-94BA-BE5527E917FF} {C9B87BD7-AF49-41C3-91F1-D550ADEB7833} = {E15BDA6D-E881-4482-94BA-BE5527E917FF} {3620414C-7DFC-423E-929F-310E19F5D930} = {E15BDA6D-E881-4482-94BA-BE5527E917FF} {A6AA1C3F-9470-4922-9D3F-D4549657AB22} = {E15BDA6D-E881-4482-94BA-BE5527E917FF} diff --git a/Dalamud/Configuration/Internal/DalamudConfiguration.cs b/Dalamud/Configuration/Internal/DalamudConfiguration.cs index 6bff5720fd..c7e0d173d9 100644 --- a/Dalamud/Configuration/Internal/DalamudConfiguration.cs +++ b/Dalamud/Configuration/Internal/DalamudConfiguration.cs @@ -464,6 +464,11 @@ public string EffectiveLanguage /// Gets or sets a value indicating whether to track texture allocation by plugins. public bool UseTexturePluginTracking { get; set; } + /// + /// Gets or sets a value indicating whether to use the DX12 renderer for testing purposes. + /// + public bool UseDx12Preview { get; set; } + /// /// Gets or sets the page of the plugin installer that is shown by default when opened. /// diff --git a/Dalamud/Dalamud.csproj b/Dalamud/Dalamud.csproj index f5f8f1c4fd..0ac48b6600 100644 --- a/Dalamud/Dalamud.csproj +++ b/Dalamud/Dalamud.csproj @@ -6,6 +6,7 @@ x64;AnyCPU 12.0 True + True @@ -79,10 +80,14 @@ + + + + all @@ -98,15 +103,18 @@ - - - + + + + + + diff --git a/Dalamud/Game/Internal/DXGI/SwapChainVtableResolver.cs b/Dalamud/Game/Internal/DXGI/SwapChainVtableResolver.cs index ba880d0e54..7343a7337e 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.ImGuiScene.Helpers; + using FFXIVClientStructs.FFXIV.Client.Graphics.Kernel; -using Serilog; + +using TerraFX.Interop.DirectX; +using TerraFX.Interop.Windows; namespace Dalamud.Game.Internal.DXGI; @@ -22,25 +22,18 @@ internal class SwapChainVtableResolver : BaseAddressResolver, ISwapChainAddressR /// public IntPtr ResizeBuffers { get; set; } - /// - /// Gets a value indicating whether or not ReShade is loaded/used. - /// - public bool IsReshade { get; private 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/ImGuiScene/Helpers/D3D11DeviceContextStateBackup.cs b/Dalamud/ImGuiScene/Helpers/D3D11DeviceContextStateBackup.cs new file mode 100644 index 0000000000..45f6aa0d72 --- /dev/null +++ b/Dalamud/ImGuiScene/Helpers/D3D11DeviceContextStateBackup.cs @@ -0,0 +1,655 @@ +using System.Runtime.InteropServices; + +using TerraFX.Interop.DirectX; +using TerraFX.Interop.Windows; + +namespace Dalamud.ImGuiScene.Helpers; + +/// +/// 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 = 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 = 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 = D3D11.D3D11_SIMULTANEOUS_RENDER_TARGET_COUNT; + private const int UavCountMax = 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 + ? D3D11.D3D11_1_UAV_SLOT_COUNT + : 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 = D3D11.D3D11_COMMONSHADER_CONSTANT_BUFFER_API_SLOT_COUNT; + private const int SamplerCount = D3D11.D3D11_COMMONSHADER_SAMPLER_SLOT_COUNT; + private const int ResourceCount = 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 = D3D11.D3D11_COMMONSHADER_CONSTANT_BUFFER_API_SLOT_COUNT; + private const int SamplerCount = D3D11.D3D11_COMMONSHADER_SAMPLER_SLOT_COUNT; + private const int ResourceCount = 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 = D3D11.D3D11_COMMONSHADER_CONSTANT_BUFFER_API_SLOT_COUNT; + private const int SamplerCount = D3D11.D3D11_COMMONSHADER_SAMPLER_SLOT_COUNT; + private const int ResourceCount = 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 = D3D11.D3D11_COMMONSHADER_CONSTANT_BUFFER_API_SLOT_COUNT; + private const int SamplerCount = D3D11.D3D11_COMMONSHADER_SAMPLER_SLOT_COUNT; + private const int ResourceCount = 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 = D3D11.D3D11_COMMONSHADER_CONSTANT_BUFFER_API_SLOT_COUNT; + private const int SamplerCount = D3D11.D3D11_COMMONSHADER_SAMPLER_SLOT_COUNT; + private const int ResourceCount = 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 = D3D11.D3D11_COMMONSHADER_CONSTANT_BUFFER_API_SLOT_COUNT; + private const int SamplerCount = D3D11.D3D11_COMMONSHADER_SAMPLER_SLOT_COUNT; + private const int ResourceCount = D3D11.D3D11_COMMONSHADER_INPUT_RESOURCE_SLOT_COUNT; + private const int InstanceCount = 256; // According to msdn + private const int UavCountMax = 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 + ? D3D11.D3D11_1_UAV_SLOT_COUNT + : 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/ImGuiScene/Helpers/D3D11RectangleDrawer.cs b/Dalamud/ImGuiScene/Helpers/D3D11RectangleDrawer.cs new file mode 100644 index 0000000000..4fc85b9d62 --- /dev/null +++ b/Dalamud/ImGuiScene/Helpers/D3D11RectangleDrawer.cs @@ -0,0 +1,364 @@ +using System.Buffers; +using System.Diagnostics.CodeAnalysis; +using System.Numerics; +using System.Reflection; + +using Dalamud.Utility; + +using ImGuiNET; + +using TerraFX.Interop.DirectX; +using TerraFX.Interop.Windows; + +namespace Dalamud.ImGuiScene.Helpers; + +/// +/// Helper class for drawing simple rectangles. +/// +[SuppressMessage( + "StyleCop.CSharp.LayoutRules", + "SA1519:Braces should not be omitted from multi-line child statement", + Justification = "Multiple fixed/using scopes")] +internal sealed unsafe class D3D11RectangleDrawer : IDisposable +{ + private ComPtr sampler; + private ComPtr vertexShader; + private ComPtr pixelShader; + private ComPtr inputLayout; + private ComPtr vertexConstantBuffer; + private ComPtr blendState; + private ComPtr blendStateForStrippingAlpha; + private ComPtr rasterizerState; + private ComPtr vertexBufferFill; + private ComPtr vertexBufferMutable; + private ComPtr indexBuffer; + + /// Finalizes an instance of the class. + ~D3D11RectangleDrawer() => this.Dispose(); + + /// + public void Dispose() + { + this.sampler.Reset(); + this.vertexShader.Reset(); + this.pixelShader.Reset(); + this.inputLayout.Reset(); + this.vertexConstantBuffer.Reset(); + this.blendState.Reset(); + this.blendStateForStrippingAlpha.Reset(); + this.rasterizerState.Reset(); + this.vertexBufferFill.Reset(); + this.vertexBufferMutable.Reset(); + this.indexBuffer.Reset(); + GC.SuppressFinalize(this); + } + + /// Sets up this instance of . + /// The device. + /// ID3D11Device. + public void Setup(T* device) where T : unmanaged, ID3D11Device.Interface + { + var assembly = Assembly.GetExecutingAssembly(); + + // Create the vertex shader + if (this.vertexShader.IsEmpty() || this.inputLayout.IsEmpty()) + { + using var stream = assembly.GetManifestResourceStream("imgui-vertex.hlsl.bytes")!; + var array = ArrayPool.Shared.Rent((int)stream.Length); + stream.ReadExactly(array, 0, (int)stream.Length); + fixed (byte* pArray = array) + fixed (ID3D11VertexShader** ppShader = &this.vertexShader.GetPinnableReference()) + fixed (ID3D11InputLayout** ppInputLayout = &this.inputLayout.GetPinnableReference()) + fixed (void* pszPosition = "POSITION"u8) + fixed (void* pszTexCoord = "TEXCOORD"u8) + fixed (void* pszColor = "COLOR"u8) + { + device->CreateVertexShader(pArray, (nuint)stream.Length, null, ppShader).ThrowOnError(); + + var ied = stackalloc D3D11_INPUT_ELEMENT_DESC[] + { + new() + { + SemanticName = (sbyte*)pszPosition, + Format = DXGI_FORMAT.DXGI_FORMAT_R32G32_FLOAT, + AlignedByteOffset = uint.MaxValue, + }, + new() + { + SemanticName = (sbyte*)pszTexCoord, + Format = DXGI_FORMAT.DXGI_FORMAT_R32G32_FLOAT, + AlignedByteOffset = uint.MaxValue, + }, + new() + { + SemanticName = (sbyte*)pszColor, + Format = DXGI_FORMAT.DXGI_FORMAT_R8G8B8A8_UNORM, + AlignedByteOffset = uint.MaxValue, + }, + }; + device->CreateInputLayout(ied, 3, pArray, (nuint)stream.Length, ppInputLayout).ThrowOnError(); + } + + ArrayPool.Shared.Return(array); + } + + // Create the constant buffer + if (this.vertexConstantBuffer.IsEmpty()) + { + var bufferDesc = new D3D11_BUFFER_DESC( + (uint)sizeof(Matrix4x4), + (uint)D3D11_BIND_FLAG.D3D11_BIND_CONSTANT_BUFFER, + D3D11_USAGE.D3D11_USAGE_IMMUTABLE); + var data = Matrix4x4.Identity; + var subr = new D3D11_SUBRESOURCE_DATA { pSysMem = &data }; + fixed (ID3D11Buffer** ppBuffer = &this.vertexConstantBuffer.GetPinnableReference()) + device->CreateBuffer(&bufferDesc, &subr, ppBuffer).ThrowOnError(); + } + + // Create the pixel shader + if (this.pixelShader.IsEmpty()) + { + using var stream = assembly.GetManifestResourceStream("imgui-frag.hlsl.bytes")!; + var array = ArrayPool.Shared.Rent((int)stream.Length); + stream.ReadExactly(array, 0, (int)stream.Length); + fixed (byte* pArray = array) + fixed (ID3D11PixelShader** ppShader = &this.pixelShader.GetPinnableReference()) + device->CreatePixelShader(pArray, (nuint)stream.Length, null, ppShader).ThrowOnError(); + + ArrayPool.Shared.Return(array); + } + + // Create the blending setup + if (this.blendState.IsEmpty()) + { + var blendStateDesc = new D3D11_BLEND_DESC + { + RenderTarget = + { + e0 = + { + BlendEnable = true, + SrcBlend = D3D11_BLEND.D3D11_BLEND_SRC_ALPHA, + DestBlend = D3D11_BLEND.D3D11_BLEND_INV_SRC_ALPHA, + BlendOp = D3D11_BLEND_OP.D3D11_BLEND_OP_ADD, + SrcBlendAlpha = D3D11_BLEND.D3D11_BLEND_INV_SRC_ALPHA, + DestBlendAlpha = D3D11_BLEND.D3D11_BLEND_ZERO, + BlendOpAlpha = D3D11_BLEND_OP.D3D11_BLEND_OP_ADD, + RenderTargetWriteMask = (byte)D3D11_COLOR_WRITE_ENABLE.D3D11_COLOR_WRITE_ENABLE_ALL, + }, + }, + }; + fixed (ID3D11BlendState** ppBlendState = &this.blendState.GetPinnableReference()) + device->CreateBlendState(&blendStateDesc, ppBlendState).ThrowOnError(); + } + + if (this.blendStateForStrippingAlpha.IsEmpty()) + { + var blendStateDesc = new D3D11_BLEND_DESC + { + RenderTarget = + { + e0 = + { + BlendEnable = true, + SrcBlend = D3D11_BLEND.D3D11_BLEND_ZERO, + DestBlend = D3D11_BLEND.D3D11_BLEND_ONE, + BlendOp = D3D11_BLEND_OP.D3D11_BLEND_OP_ADD, + SrcBlendAlpha = D3D11_BLEND.D3D11_BLEND_ONE, + DestBlendAlpha = D3D11_BLEND.D3D11_BLEND_ZERO, + BlendOpAlpha = D3D11_BLEND_OP.D3D11_BLEND_OP_ADD, + RenderTargetWriteMask = (byte)D3D11_COLOR_WRITE_ENABLE.D3D11_COLOR_WRITE_ENABLE_ALPHA, + }, + }, + }; + fixed (ID3D11BlendState** ppBlendState = &this.blendStateForStrippingAlpha.GetPinnableReference()) + device->CreateBlendState(&blendStateDesc, ppBlendState).ThrowOnError(); + } + + // Create the rasterizer state + if (this.rasterizerState.IsEmpty()) + { + var rasterizerDesc = new D3D11_RASTERIZER_DESC + { + FillMode = D3D11_FILL_MODE.D3D11_FILL_SOLID, + CullMode = D3D11_CULL_MODE.D3D11_CULL_NONE, + }; + fixed (ID3D11RasterizerState** ppRasterizerState = &this.rasterizerState.GetPinnableReference()) + device->CreateRasterizerState(&rasterizerDesc, ppRasterizerState).ThrowOnError(); + } + + // Create the font sampler + if (this.sampler.IsEmpty()) + { + var samplerDesc = new D3D11_SAMPLER_DESC( + D3D11_FILTER.D3D11_FILTER_MIN_MAG_MIP_LINEAR, + D3D11_TEXTURE_ADDRESS_MODE.D3D11_TEXTURE_ADDRESS_WRAP, + D3D11_TEXTURE_ADDRESS_MODE.D3D11_TEXTURE_ADDRESS_WRAP, + D3D11_TEXTURE_ADDRESS_MODE.D3D11_TEXTURE_ADDRESS_WRAP, + 0f, + 0, + D3D11_COMPARISON_FUNC.D3D11_COMPARISON_ALWAYS, + null, + 0, + 0); + fixed (ID3D11SamplerState** ppSampler = &this.sampler.GetPinnableReference()) + device->CreateSamplerState(&samplerDesc, ppSampler).ThrowOnError(); + } + + if (this.vertexBufferFill.IsEmpty()) + { + var data = stackalloc ImDrawVert[] + { + new() { col = uint.MaxValue, pos = new(-1, 1), uv = new(0, 0) }, + new() { col = uint.MaxValue, pos = new(-1, -1), uv = new(0, 1) }, + new() { col = uint.MaxValue, pos = new(1, 1), uv = new(1, 0) }, + new() { col = uint.MaxValue, pos = new(1, -1), uv = new(1, 1) }, + }; + var desc = new D3D11_BUFFER_DESC( + (uint)(sizeof(ImDrawVert) * 4), + (uint)D3D11_BIND_FLAG.D3D11_BIND_VERTEX_BUFFER, + D3D11_USAGE.D3D11_USAGE_IMMUTABLE); + var subr = new D3D11_SUBRESOURCE_DATA { pSysMem = data }; + var buffer = default(ID3D11Buffer*); + device->CreateBuffer(&desc, &subr, &buffer).ThrowOnError(); + this.vertexBufferFill.Attach(buffer); + } + + if (this.vertexBufferMutable.IsEmpty()) + { + var desc = new D3D11_BUFFER_DESC( + (uint)(sizeof(ImDrawVert) * 4), + (uint)D3D11_BIND_FLAG.D3D11_BIND_VERTEX_BUFFER, + D3D11_USAGE.D3D11_USAGE_DYNAMIC, + (uint)D3D11_CPU_ACCESS_FLAG.D3D11_CPU_ACCESS_WRITE); + var buffer = default(ID3D11Buffer*); + device->CreateBuffer(&desc, null, &buffer).ThrowOnError(); + this.vertexBufferMutable.Attach(buffer); + } + + if (this.indexBuffer.IsEmpty()) + { + var data = stackalloc ushort[] { 0, 1, 2, 1, 2, 3 }; + var desc = new D3D11_BUFFER_DESC( + sizeof(ushort) * 6, + (uint)D3D11_BIND_FLAG.D3D11_BIND_INDEX_BUFFER, + D3D11_USAGE.D3D11_USAGE_IMMUTABLE); + var subr = new D3D11_SUBRESOURCE_DATA { pSysMem = data }; + var buffer = default(ID3D11Buffer*); + device->CreateBuffer(&desc, &subr, &buffer).ThrowOnError(); + this.indexBuffer.Attach(buffer); + } + } + + /// Draws the given shader resource view to the current render target. + /// An instance of . + /// The render target view. + /// The left top coordinates relative to the size of the target texture. + /// The right bottom coordinates relative to the size of the target texture. + /// The shader resource view. + /// The left top coordinates relative to the size of the source texture. + /// The right bottom coordinates relative to the size of the source texture. + /// Whether to only copy the alpha channel. + /// This function does not throw. + public void Draw( + ID3D11DeviceContext* ctx, + ID3D11RenderTargetView* rtv, + Vector2 targetUv0, + Vector2 targetUv1, + ID3D11ShaderResourceView* srv, + Vector2 sourceUv0, + Vector2 sourceUv1, + bool copyAlphaOnly) + { + using var rtvRes = default(ComPtr); + rtv->GetResource(rtvRes.GetAddressOf()); + + using var rtvTex = default(ComPtr); + if (rtvRes.As(&rtvTex).FAILED) + return; + + D3D11_TEXTURE2D_DESC texDesc; + rtvTex.Get()->GetDesc(&texDesc); + + ID3D11Buffer* buffer; + if (sourceUv0 == Vector2.Zero && sourceUv1 == Vector2.One) + { + buffer = this.vertexBufferFill.Get(); + } + else + { + buffer = this.vertexBufferMutable.Get(); + var mapped = default(D3D11_MAPPED_SUBRESOURCE); + if (ctx->Map((ID3D11Resource*)buffer, 0, D3D11_MAP.D3D11_MAP_WRITE_DISCARD, 0u, &mapped).FAILED) + return; + _ = new Span(mapped.pData, 4) + { + [0] = new() + { + col = uint.MaxValue, + pos = new(-1 + (2 * targetUv0.X), 1 - (2 * targetUv0.Y)), + uv = sourceUv0, + }, + [1] = new() + { + col = uint.MaxValue, + pos = new(-1 + (2 * targetUv0.X), 1 - (2 * targetUv1.Y)), + uv = new(sourceUv0.X, sourceUv1.Y), + }, + [2] = new() + { + col = uint.MaxValue, + pos = new(-1 + (2 * targetUv1.X), 1 - (2 * targetUv0.Y)), + uv = new(sourceUv1.X, sourceUv0.Y), + }, + [3] = new() + { + col = uint.MaxValue, + pos = new(-1 + (2 * targetUv1.X), 1 - (2 * targetUv1.Y)), + uv = sourceUv1, + }, + }; + ctx->Unmap((ID3D11Resource*)buffer, 0u); + } + + var stride = (uint)sizeof(ImDrawVert); + var offset = 0u; + + ctx->IASetInputLayout(this.inputLayout); + ctx->IASetVertexBuffers(0, 1, &buffer, &stride, &offset); + ctx->IASetIndexBuffer(this.indexBuffer, DXGI_FORMAT.DXGI_FORMAT_R16_UINT, 0); + ctx->IASetPrimitiveTopology(D3D_PRIMITIVE_TOPOLOGY.D3D11_PRIMITIVE_TOPOLOGY_TRIANGLELIST); + + var viewport = new D3D11_VIEWPORT(0, 0, texDesc.Width, texDesc.Height); + ctx->RSSetState(this.rasterizerState); + ctx->RSSetViewports(1, &viewport); + + var blendColor = default(Vector4); + ctx->OMSetRenderTargets(1, &rtv, null); + if (copyAlphaOnly) + ctx->OMSetBlendState(this.blendStateForStrippingAlpha, (float*)&blendColor, 0xffffffff); + else + ctx->OMSetBlendState(this.blendState, (float*)&blendColor, 0xffffffff); + ctx->OMSetDepthStencilState(null, 0); + + ctx->VSSetShader(this.vertexShader.Get(), null, 0); + buffer = this.vertexConstantBuffer.Get(); + ctx->VSSetConstantBuffers(0, 1, &buffer); + + ctx->PSSetShader(this.pixelShader, null, 0); + var simp = this.sampler.Get(); + ctx->PSSetSamplers(0, 1, &simp); + ctx->PSSetShaderResources(0, 1, &srv); + + ctx->GSSetShader(null, null, 0); + ctx->HSSetShader(null, null, 0); + ctx->DSSetShader(null, null, 0); + ctx->CSSetShader(null, null, 0); + ctx->DrawIndexed(6, 0, 0); + + var ppn = default(ID3D11ShaderResourceView*); + ctx->PSSetShaderResources(0, 1, &ppn); + + ctx->OMSetRenderTargets(0, null, null); + } +} diff --git a/Dalamud/ImGuiScene/Helpers/FixedObjectPool.cs b/Dalamud/ImGuiScene/Helpers/FixedObjectPool.cs new file mode 100644 index 0000000000..7475b5b4ac --- /dev/null +++ b/Dalamud/ImGuiScene/Helpers/FixedObjectPool.cs @@ -0,0 +1,177 @@ +using System.Threading; + +namespace Dalamud.ImGuiScene.Helpers; + +/// +/// Pool of . +/// +/// The object type contained within. If it is an , objects will be disposed along with this pool. +internal class FixedObjectPool : IDisposable + where T : class +{ + private readonly Func creator; + private readonly bool noExtraAllocation; + private readonly T?[] objects; + private int allocated; + private bool disposed; + + /// + /// Initializes a new instance of the class. + /// + /// The creator. + /// The initial capacity. Non-positive number means . + /// If set to true, will wait if no pooled object is available. + public FixedObjectPool(Func creator, int capacity = 0, bool noExtraAllocation = false) + { + if (capacity <= 0) + capacity = Environment.ProcessorCount; + + this.creator = creator; + this.noExtraAllocation = noExtraAllocation; + this.objects = new T[capacity]; + } + + /// + /// Gets the capacity of this . + /// + public int Capacity => this.objects.Length; + + /// + public void Dispose() + { + if (this.disposed) + return; + + this.disposed = true; + foreach (ref var h in this.objects.AsSpan()) + { + if (Interlocked.Exchange(ref h, default) is { } obj) + (obj as IDisposable)?.Dispose(); + } + + GC.SuppressFinalize(this); + } + + /// + /// Tries to rent an object from this event pool, without allocating a new object nor waiting. + /// + /// A disposable, with the object accessible via a field. + /// The action to be executed before return. Must not throw. + /// True if successful. + public bool TryRent(out Returner returner, Action? beforeReturn = null) + { + ObjectDisposedException.ThrowIf(this.disposed, this); + + foreach (ref var h in this.objects.AsSpan()) + { + var @object = Interlocked.Exchange(ref h, default); + if (@object != default) + { + returner = new(@object, this, beforeReturn); + return true; + } + } + + for (var known = this.allocated; known < this.Capacity; known = this.allocated) + { + if (known == Interlocked.CompareExchange(ref this.allocated, known + 1, known)) + { + returner = new(this.creator(known), this, beforeReturn); + return true; + } + } + + returner = default; + return false; + } + + /// + /// Rents an object from this event pool, allocating a new object as necessary and if allowed. + /// + /// The action to be executed before return. Must not throw. + /// A disposable, with the object accessible via a field. + public Returner Rent(Action? beforeReturn = null) + { + if (this.TryRent(out var r, beforeReturn)) + return r; + + if (!this.noExtraAllocation) + return new(this.creator(-1), this, beforeReturn); + + while (true) + { + lock (this.objects) + Monitor.Wait(this.objects); + if (this.TryRent(out r, beforeReturn)) + return r; + } + } + + /// + /// Manually returns the object.
+ /// Warning: The beforeReturn parameter provided to will be ignored. + ///
+ /// The returning object. + public void Return(T @object) + { + if (!this.disposed) + { + foreach (ref var h in this.objects.AsSpan()) + { + @object = Interlocked.Exchange(ref h, @object); + if (@object == default) + { + lock (this.objects) + Monitor.Pulse(this.objects); + return; + } + } + } + + (@object as IDisposable)?.Dispose(); + } + + /// + /// A struct that must be disposed after use. + /// + public struct Returner : IDisposable + { + private FixedObjectPool? pool; + private Action? beforeReturn; + + /// + /// Initializes a new instance of the struct. + /// + /// The handle to contain. + /// The pool to return to. + /// Optional action to be executed before returning. + public Returner(T @object, FixedObjectPool pool, Action? beforeReturn) + { + this.O = @object; + this.pool = pool; + this.beforeReturn = beforeReturn; + } + + /// + /// Gets the handle. + /// + public T O { get; } + + public static implicit operator T(Returner p) => p.O; + + /// + public void Dispose() + { + try + { + this.beforeReturn?.Invoke(); + } + finally + { + this.beforeReturn = null; + this.pool?.Return(this.O); + this.pool = null; + } + } + } +} diff --git a/Dalamud/ImGuiScene/Helpers/ImGuiViewportHelpers.cs b/Dalamud/ImGuiScene/Helpers/ImGuiViewportHelpers.cs new file mode 100644 index 0000000000..ed40a0487d --- /dev/null +++ b/Dalamud/ImGuiScene/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.ImGuiScene.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/ImGuiScene/Helpers/ManagedComObject.cs b/Dalamud/ImGuiScene/Helpers/ManagedComObject.cs new file mode 100644 index 0000000000..392e06fce3 --- /dev/null +++ b/Dalamud/ImGuiScene/Helpers/ManagedComObject.cs @@ -0,0 +1,35 @@ +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +using TerraFX.Interop; +using TerraFX.Interop.Windows; + +namespace Dalamud.ImGuiScene.Helpers; + +/// +/// Wraps a managed class for use with . +/// +/// The contained type. +internal unsafe struct ManagedComObject : IUnknown.Interface + where T : ManagedComObjectBase, INativeGuid +{ + private nint vtbl; + + /// + public static Guid* NativeGuid => T.NativeGuid; + + /// + /// Gets the object. + /// + public T O => (T)(GCHandle.FromIntPtr(((nint*)Unsafe.AsPointer(ref this.vtbl))[1]).Target + ?? throw new ObjectDisposedException(nameof(T))); + + /// + public HRESULT QueryInterface(Guid* riid, void** ppvObject) => this.O.QueryInterface(riid, ppvObject); + + /// + public uint AddRef() => this.O.AddRef(); + + /// + public uint Release() => this.O.Release(); +} diff --git a/Dalamud/ImGuiScene/Helpers/ManagedComObjectBase.cs b/Dalamud/ImGuiScene/Helpers/ManagedComObjectBase.cs new file mode 100644 index 0000000000..1618e747ce --- /dev/null +++ b/Dalamud/ImGuiScene/Helpers/ManagedComObjectBase.cs @@ -0,0 +1,197 @@ +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; +using System.Threading; + +using TerraFX.Interop; +using TerraFX.Interop.Windows; + +namespace Dalamud.ImGuiScene.Helpers; + +/// +/// Base class for implementing COM objects. +/// +internal abstract unsafe class ManagedComObjectBase : IDisposable +{ + private nint[] vtblAndHandle; + private uint refCount; + + /// + /// Initializes a new instance of the class. + /// + /// Number of extra functions in the vtable, extending IUnknown. + protected ManagedComObjectBase(int numExtraFunctions = 0) + { + this.vtblAndHandle = GC.AllocateArray(5 + numExtraFunctions, true); + this.vtblAndHandle[0] = (nint)Unsafe.AsPointer(ref this.vtblAndHandle[2]); + this.vtblAndHandle[1] = GCHandle.ToIntPtr(GCHandle.Alloc(this)); + this.vtblAndHandle[2] = (nint)(delegate* unmanaged)&StaticQueryInterface; + this.vtblAndHandle[3] = (nint)(delegate* unmanaged)&StaticAddRef; + this.vtblAndHandle[4] = (nint)(delegate* unmanaged)&StaticRelease; + this.refCount = 1; + return; + + [UnmanagedCallersOnly] + static HRESULT StaticQueryInterface(nint punk, Guid* piid, void** ppvObject) => + AttachFromAddressOrNull(punk) is { } obj ? obj.QueryInterface(piid, ppvObject) : E.E_FAIL; + + [UnmanagedCallersOnly] + static uint StaticAddRef(nint punk) => AttachFromAddressOrNull(punk) is { } obj ? obj.AddRef() : 0; + + [UnmanagedCallersOnly] + static uint StaticRelease(nint punk) => AttachFromAddressOrNull(punk) is { } obj ? obj.Release() : 0; + } + + /// + /// Gets a value indicating whether this object has been fully released. + /// + public bool IsReleased => this.refCount == 0; + + /// + /// Gets the current reference count. + /// + public uint RefCount => this.refCount; + + /// + /// Gets the span of vtable. + /// + protected Span Vtbl => this.vtblAndHandle.AsSpan(2); + + /// + /// Converts the specified address to the instance of the class. + /// + /// The address. + /// The instance of the class. + public static ManagedComObjectBase AttachFromAddress(nint punk) => + AttachFromAddressOrNull(punk) ?? throw new InvalidCastException(); + + /// + /// Converts the specified address to the instance of the class. Returns null if it's impossible. + /// + /// The address. + /// The instance of the class. + public static ManagedComObjectBase? AttachFromAddressOrNull(nint punk) => + punk == 0 ? null : GCHandle.FromIntPtr(((nint*)punk)![1]).Target as ManagedComObjectBase; + + /// + /// Gets this class as a pointer of . + /// + /// The pointer. + public IUnknown* AsIUnknown() => (IUnknown*)Unsafe.AsPointer(ref this.vtblAndHandle[0]); + + /// + /// Gets the COM pointer of this class as a . + /// + /// The pointer. + public nint AsHandle() => (nint)Unsafe.AsPointer(ref this.vtblAndHandle[0]); + + /// + public HRESULT QueryInterface(Guid* piid, void** ppvObject) + { + if (ppvObject == null) + return E.E_POINTER; + + *ppvObject = null; + if (piid == null) + return E.E_INVALIDARG; + + if (*piid == IID.IID_IUnknown) + { + this.AddRef(); + *ppvObject = Unsafe.AsPointer(ref this.vtblAndHandle[0]); + return S.S_OK; + } + + return E.E_NOINTERFACE; + } + + /// + public uint AddRef() => Interlocked.Increment(ref this.refCount); + + /// + public uint Release() + { + var rc = Interlocked.Decrement(ref this.refCount); + if (rc == 0) + { + this.FinalRelease(); + GCHandle.FromIntPtr(this.vtblAndHandle[1]).Free(); + this.vtblAndHandle.AsSpan().Fill(unchecked((nint)0xCCCCCCCCCCCCCCCC)); + this.vtblAndHandle = null!; + } + + return rc; + } + + /// + /// Calls . + /// + public void Dispose() => this.Release(); + + /// + /// Attempt to cast this object as the given type, indicated with . + /// + /// The IID. + /// The casted object, or null if cast was not applicable. + protected virtual void* DynamicCast(in Guid iid) => null; + + /// + /// Called on the last release of this object. + /// + protected virtual void FinalRelease() + { + } +} + +/// +/// Base class for implementing COM objects. +/// +/// The implementor. +internal abstract unsafe class ManagedComObjectBase : ManagedComObjectBase, ICloneable + where T : ManagedComObjectBase, INativeGuid +{ + /// + /// Initializes a new instance of the class. + /// + /// Number of extra functions in the vtable, extending IUnknown. + protected ManagedComObjectBase(int numExtraFunctions = 0) + : base(numExtraFunctions) + { + } + + /// + /// Converts the specified address to the instance of the class. + /// + /// The address. + /// The instance of the class. + public static new T AttachFromAddress(nint punk) => AttachFromAddressOrNull(punk) ?? throw new InvalidCastException(); + + /// + /// Converts the specified address to the instance of the class. Returns null if it's impossible. + /// + /// The address. + /// The instance of the class. + public static new T? AttachFromAddressOrNull(nint punk) => + punk == 0 ? null : GCHandle.FromIntPtr(((nint*)punk)![1]).Target as T; + + /// + /// Gets this class as a pointer of . + /// + /// The pointer. + public ManagedComObject* AsComInterface() => (ManagedComObject*)this.AsHandle(); + + /// + /// Creates a new reference of this. + /// + /// This. + public T CloneRef() + { + this.AddRef(); + return (T)this; + } + + /// + /// Creates a new reference of this. + /// + /// This. + object ICloneable.Clone() => this.CloneRef(); +} diff --git a/Dalamud/ImGuiScene/Helpers/ReShadePeeler.cs b/Dalamud/ImGuiScene/Helpers/ReShadePeeler.cs new file mode 100644 index 0000000000..bb3f4540f2 --- /dev/null +++ b/Dalamud/ImGuiScene/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.ImGuiScene.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/ImGuiScene/Helpers/StructWrapper.cs b/Dalamud/ImGuiScene/Helpers/StructWrapper.cs new file mode 100644 index 0000000000..8260fa331e --- /dev/null +++ b/Dalamud/ImGuiScene/Helpers/StructWrapper.cs @@ -0,0 +1,38 @@ +namespace Dalamud.ImGuiScene.Helpers; + +/// +/// Wraps an unmanaged type as a reference type (class). +/// +/// The contained unmanaged type. +internal sealed class StructWrapper : IDisposable + where T : unmanaged +{ + private readonly T data; + + /// + /// Initializes a new instance of the class. + /// + /// The parameter. + public StructWrapper(T obj) => this.data = obj; + + /// + /// Finalizes an instance of the class. + /// + ~StructWrapper() => this.ReleaseUnmanagedResources(); + + /// + /// Gets the struct. + /// + public ref readonly T O => ref this.data; + + public static implicit operator T(StructWrapper t) => t.O; + + /// + public void Dispose() + { + this.ReleaseUnmanagedResources(); + GC.SuppressFinalize(this); + } + + private void ReleaseUnmanagedResources() => (this.data as IDisposable)?.Dispose(); +} diff --git a/Dalamud/ImGuiScene/Helpers/WicEasy.cs b/Dalamud/ImGuiScene/Helpers/WicEasy.cs new file mode 100644 index 0000000000..94f40296e4 --- /dev/null +++ b/Dalamud/ImGuiScene/Helpers/WicEasy.cs @@ -0,0 +1,188 @@ +using System.Diagnostics.CodeAnalysis; + +using Dalamud.Utility; + +using TerraFX.Interop.Windows; + +namespace Dalamud.ImGuiScene.Helpers; + +/// +/// Manager for containing some convenience methods. +/// +[SuppressMessage( + "StyleCop.CSharp.LayoutRules", + "SA1519:Braces should not be omitted from multi-line child statement", + Justification = "Multiple fixed/using scopes")] +internal sealed unsafe class WicEasy : IDisposable +{ + private ComPtr wicFactory; + + /// + /// Initializes a new instance of the class. + /// + public WicEasy() + { + try + { + fixed (Guid* clsid = &CLSID.CLSID_WICImagingFactory) + fixed (Guid* iid = &IID.IID_IWICImagingFactory) + fixed (IWICImagingFactory** pp = &this.wicFactory.GetPinnableReference()) + { + TerraFX.Interop.Windows.Windows.CoCreateInstance( + clsid, + null, + (uint)CLSCTX.CLSCTX_INPROC_SERVER, + iid, + (void**)pp).ThrowOnError(); + } + } + catch + { + this.ReleaseUnmanagedResources(); + throw; + } + } + + /// + /// Finalizes an instance of the class. + /// + ~WicEasy() => this.ReleaseUnmanagedResources(); + + /// + /// Gets the pointer to an instance of . + /// + public IWICImagingFactory* Factory => this.wicFactory; + + /// + public void Dispose() + { + this.ReleaseUnmanagedResources(); + GC.SuppressFinalize(this); + } + + /// + /// Creates an empty new instance of . + /// + /// The stream, wrapped in a smart pointer. + public ComPtr CreateStream() + { + var stream = default(ComPtr); + this.Factory->CreateStream(stream.GetAddressOf()).ThrowOnError(); + return stream; + } + + /// + public ComPtr CreateBitmapSource(IWICStream* stream) + { + // Note: IStream is an ancestor of IWICStream; pointer casting is well-defined. + return this.CreateBitmapSource((IStream*)stream); + } + + /// + /// Creates a new instance of from .
+ /// If the image contained within has multiple frames, the first frame is returned. + ///
+ /// The stream containing the image data. + /// The new bitmap source, wrapped in a smart pointer. + public ComPtr CreateBitmapSource(IStream* stream) + { + using var decoder = default(ComPtr); + this.Factory->CreateDecoderFromStream( + stream, + null, + WICDecodeOptions.WICDecodeMetadataCacheOnDemand, + decoder.GetAddressOf()).ThrowOnError(); + + var result = default(ComPtr); + // Note: IWICBitmapSource is an ancestor of IWICBitmapFrameDecode; pointer casting is well-defined. + decoder.Get()->GetFrame(0, (IWICBitmapFrameDecode**)result.GetAddressOf()).ThrowOnError(); + return result; + } + + /// + /// Converts into the pixel format . + /// + /// The bitmap source. + /// The new WIC pixel format. + /// The converted bitmap source, wrapped in a smart pointer. + public ComPtr ConvertPixelFormat(IWICBitmapSource* source, in Guid newPixelFormat) + { + using var converter = default(ComPtr); + this.Factory->CreateFormatConverter(converter.GetAddressOf()).ThrowOnError(); + fixed (Guid* format = &newPixelFormat) + { + converter.Get()->Initialize( + source, + format, + WICBitmapDitherType.WICBitmapDitherTypeNone, + null, + 0, + WICBitmapPaletteType.WICBitmapPaletteTypeMedianCut).ThrowOnError(); + } + + // Avoid increasing refcount; using a constructor of ComPtr will call AddRef. + var res = default(ComPtr); + // Note: IWICBitmapSource is an ancestor of IWICFormatConverter; pointer casting is well-defined. + res.Attach((IWICBitmapSource*)converter.Get()); + converter.Detach(); + return res; + } + + /// + /// Creates a new instance of from a . + /// + /// The bitmap source. + /// The new bitmap, wrapped in a smart pointer. + public ComPtr CreateBitmap(IWICBitmapSource* source) + { + uint width, height; + source->GetSize(&width, &height).ThrowOnError(); + var pixelFormat = source->GetPixelFormat(); + var bitmap = default(ComPtr); + this.Factory->CreateBitmap( + width, + height, + &pixelFormat, + WICBitmapCreateCacheOption.WICBitmapCacheOnDemand, + bitmap.GetAddressOf()).ThrowOnError(); + try + { + using var targetLock = bitmap.Get()->LockBits( + WICBitmapLockFlags.WICBitmapLockWrite, + out var pb, + out var nb, + out _); + uint stride; + targetLock.Get()->GetStride(&stride).ThrowOnError(); + source->CopyPixels(null, stride, nb, pb).ThrowOnError(); + return bitmap; + } + catch + { + bitmap.Reset(); + throw; + } + } + + /// + /// Gets the number of bits per pixel for the WIC pixel format. + /// + /// The WIC pixel format. + /// Number of bits (bpp). + public int GetBitsPerPixel(in Guid pixelFormatGuid) + { + using var cinfo = default(ComPtr); + fixed (Guid* guid = &pixelFormatGuid) + this.Factory->CreateComponentInfo(guid, cinfo.ReleaseAndGetAddressOf()).ThrowOnError(); + + using var pfinfo = default(ComPtr); + cinfo.As(&pfinfo).ThrowOnError(); + + uint bpp; + pfinfo.Get()->GetBitsPerPixel(&bpp).ThrowOnError(); + + return (int)bpp; + } + + private void ReleaseUnmanagedResources() => this.wicFactory.Reset(); +} diff --git a/Dalamud/ImGuiScene/Helpers/WicEasyExtensions.cs b/Dalamud/ImGuiScene/Helpers/WicEasyExtensions.cs new file mode 100644 index 0000000000..1c817f69d5 --- /dev/null +++ b/Dalamud/ImGuiScene/Helpers/WicEasyExtensions.cs @@ -0,0 +1,116 @@ +using System.Diagnostics.CodeAnalysis; +using System.Text; + +using Dalamud.Utility; + +using TerraFX.Interop.DirectX; +using TerraFX.Interop.Windows; + +namespace Dalamud.ImGuiScene.Helpers; + +/// +/// Helpers for . +/// +[SuppressMessage( + "StyleCop.CSharp.LayoutRules", + "SA1519:Braces should not be omitted from multi-line child statement", + Justification = "Multiple fixed/using scopes")] +internal static class WicEasyExtensions +{ + /// + /// Creates an instance of from a path to file. + /// + /// The file path. + /// wrapped in a smart pointer. + public static unsafe ComPtr CreateStreamFromFile(string path) + { + var cb = Encoding.Unicode.GetByteCount(path); + var buf = stackalloc byte[cb + 2]; + buf[cb] = buf[cb + 1] = 0; + Encoding.Unicode.GetBytes(path, new(buf, cb)); + + var stream = default(ComPtr); + TerraFX.Interop.Windows.Windows.SHCreateStreamOnFileW( + (ushort*)buf, + STGM.STGM_SHARE_DENY_WRITE, + stream.ReleaseAndGetAddressOf()).ThrowOnError(); + return stream; + } + + /// + /// Locks the bits of for its whole region. + /// + /// The pointer to an instance of . + /// The lock mode. + /// The pointer to data bytes. + /// The number of bytes available. + /// The span view of the data. + /// The handle to the lock, providing additional information on demand, wrapped in a smart pointer. Dispose after use. + public static unsafe ComPtr LockBits( + ref this IWICBitmap bitmap, + WICBitmapLockFlags mode, + out byte* pb, + out uint nb, + out Span fixedBytes) + { + var rc = default(WICRect); + bitmap.GetSize((uint*)&rc.Width, (uint*)&rc.Height).ThrowOnError(); + + var result = default(ComPtr); + bitmap.Lock(&rc, (uint)mode, result.GetAddressOf()); + try + { + uint nbc; + byte* pbc; + result.Get()->GetDataPointer(&nbc, &pbc).ThrowOnError(); + pb = pbc; + nb = nbc; + fixedBytes = new(pb, (int)nb); + } + catch + { + result.Reset(); + throw; + } + + return result; + } + + /// + /// Gets the corresponding from a containing a WIC pixel format. + /// + /// The WIC pixel format. + /// The corresponding , or if unavailable. + public static DXGI_FORMAT ToDxgiFormat(in this Guid fmt) => 0 switch + { + // See https://github.com/microsoft/DirectXTex/wiki/WIC-I-O-Functions#savetowicmemory-savetowicfile + _ when fmt == GUID.GUID_WICPixelFormat128bppRGBAFloat => DXGI_FORMAT.DXGI_FORMAT_R32G32B32A32_FLOAT, + _ when fmt == GUID.GUID_WICPixelFormat64bppRGBAHalf => DXGI_FORMAT.DXGI_FORMAT_R16G16B16A16_FLOAT, + _ when fmt == GUID.GUID_WICPixelFormat64bppRGBA => DXGI_FORMAT.DXGI_FORMAT_R16G16B16A16_UNORM, + _ when fmt == GUID.GUID_WICPixelFormat32bppRGBA1010102XR => DXGI_FORMAT.DXGI_FORMAT_R10G10B10_XR_BIAS_A2_UNORM, + _ when fmt == GUID.GUID_WICPixelFormat32bppRGBA1010102 => DXGI_FORMAT.DXGI_FORMAT_R10G10B10A2_UNORM, + _ when fmt == GUID.GUID_WICPixelFormat16bppBGRA5551 => DXGI_FORMAT.DXGI_FORMAT_B5G5R5A1_UNORM, + _ when fmt == GUID.GUID_WICPixelFormat16bppBGR565 => DXGI_FORMAT.DXGI_FORMAT_B5G6R5_UNORM, + _ when fmt == GUID.GUID_WICPixelFormat32bppGrayFloat => DXGI_FORMAT.DXGI_FORMAT_R32_FLOAT, + _ when fmt == GUID.GUID_WICPixelFormat16bppGrayHalf => DXGI_FORMAT.DXGI_FORMAT_R16_FLOAT, + _ when fmt == GUID.GUID_WICPixelFormat16bppGray => DXGI_FORMAT.DXGI_FORMAT_R16_UNORM, + _ when fmt == GUID.GUID_WICPixelFormat8bppGray => DXGI_FORMAT.DXGI_FORMAT_R8_UNORM, + _ when fmt == GUID.GUID_WICPixelFormat8bppAlpha => DXGI_FORMAT.DXGI_FORMAT_A8_UNORM, + _ when fmt == GUID.GUID_WICPixelFormat32bppRGBA => DXGI_FORMAT.DXGI_FORMAT_R8G8B8A8_UNORM, + _ when fmt == GUID.GUID_WICPixelFormat32bppBGRA => DXGI_FORMAT.DXGI_FORMAT_B8G8R8A8_UNORM, + _ when fmt == GUID.GUID_WICPixelFormat32bppBGR => DXGI_FORMAT.DXGI_FORMAT_B8G8R8X8_UNORM, + _ => DXGI_FORMAT.DXGI_FORMAT_UNKNOWN, + }; + + /// + /// Gets the pixel format of . + /// + /// The bitmap source. + /// The WIC pixel format. + public static unsafe Guid GetPixelFormat(ref this IWICBitmapSource source) + { + var pixelFormat = default(Guid); + source.GetPixelFormat(&pixelFormat).ThrowOnError(); + return pixelFormat; + } +} diff --git a/Dalamud/ImGuiScene/Helpers/Win32Handle.cs b/Dalamud/ImGuiScene/Helpers/Win32Handle.cs new file mode 100644 index 0000000000..4628ad9517 --- /dev/null +++ b/Dalamud/ImGuiScene/Helpers/Win32Handle.cs @@ -0,0 +1,87 @@ +using System.Diagnostics.CodeAnalysis; +using System.Runtime.InteropServices; + +using Dalamud.Utility; + +using TerraFX.Interop.DirectX; +using TerraFX.Interop.Windows; + +using Win32 = TerraFX.Interop.Windows.Windows; + +namespace Dalamud.ImGuiScene.Helpers; + +/// +/// Wraps a , that should be closed using .
+/// Wrapping anything else will result in an undefined behavior. +///
+[SuppressMessage( + "StyleCop.CSharp.LayoutRules", + "SA1519:Braces should not be omitted from multi-line child statement", + Justification = "Multiple fixed/using scopes")] +internal readonly struct Win32Handle : IDisposable +{ + /// + /// Initializes a new instance of the struct. + /// + /// The handle. + public Win32Handle(HANDLE handle) => this.Handle = handle; + + /// + /// Gets the handle. + /// + public HANDLE Handle { get; } + + public static implicit operator HANDLE(Win32Handle t) => t.Handle; + + /// + /// Creates a new instance of , by wrapping . + /// + /// The optional security attributes. + /// Whether the event is manual reset. + /// The initial state on whether the event is signaled. + /// The optional name of the pipe. + /// The new instance of containig a valid Win32 event handle. + public static unsafe Win32Handle CreateEvent( + in SECURITY_ATTRIBUTES securityAttributes = default, + bool manualReset = true, + bool initialState = false, + string? name = null) + { + fixed (SECURITY_ATTRIBUTES* psa = &securityAttributes) + fixed (char* pName = name) + { + var h = Win32.CreateEventW(psa, manualReset, initialState, (ushort*)pName); + if (h == default) + throw Marshal.GetExceptionForHR(Marshal.GetHRForLastWin32Error()) ?? new(); + return new(h); + } + } + + /// + /// Creates a new instance of , by wrapping . + /// + /// A pointer to an instance of . + /// The resource to share. + /// The optional security attributes. + /// The access. Currently the only valid value is . + /// The optional name of the shared object. + /// The type of resource. + /// The new instance of containig a valid shared DX12 resource handle. + public static unsafe Win32Handle CreateSharedHandle( + ID3D12Device* device, + TResource* resource, + in SECURITY_ATTRIBUTES securityAttributes = default, + uint access = Win32.GENERIC_ALL, + string? name = null) + where TResource : unmanaged, ID3D12DeviceChild.Interface + { + HANDLE handle; + fixed (SECURITY_ATTRIBUTES* psa = &securityAttributes) + fixed (char* pName = name) + device->CreateSharedHandle((ID3D12DeviceChild*)resource, psa, access, (ushort*)pName, &handle).ThrowOnError(); + return new(handle); + } + + /// + public void Dispose() => Win32.CloseHandle(this.Handle); +} diff --git a/Dalamud/ImGuiScene/IImGuiInputHandler.cs b/Dalamud/ImGuiScene/IImGuiInputHandler.cs new file mode 100644 index 0000000000..c86bcbeb52 --- /dev/null +++ b/Dalamud/ImGuiScene/IImGuiInputHandler.cs @@ -0,0 +1,31 @@ +namespace Dalamud.ImGuiScene; + +/// +/// A simple shared public interface that all ImGui input implementations follows. +/// +internal interface IImGuiInputHandler : IDisposable +{ + /// + /// Gets or sets a value indicating whether or not 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/ImGuiScene/IImGuiRenderer.cs b/Dalamud/ImGuiScene/IImGuiRenderer.cs new file mode 100644 index 0000000000..ffb9d1fef7 --- /dev/null +++ b/Dalamud/ImGuiScene/IImGuiRenderer.cs @@ -0,0 +1,159 @@ +using System.Numerics; +using System.Runtime.CompilerServices; +using System.Threading; + +using Dalamud.Interface.Textures; +using Dalamud.Interface.Textures.TextureWraps; +using Dalamud.Plugin.Internal.Types; + +using ImGuiNET; + +namespace Dalamud.ImGuiScene; + +/// +/// A simple shared public interface that all ImGui render implementations follows. +/// +internal interface IImGuiRenderer : IDisposable +{ + /// + /// Callback to be invoked on rendering, added via . + /// + /// The relevant draw data. + /// The relevant draw command. + public delegate void DrawCmdUserCallbackDelegate(ImDrawDataPtr drawData, ImDrawCmdPtr drawCmd); + + /// + /// Notifies that the window is about to be resized. + /// + void OnPreResize(); + + /// + /// Notifies that the window has been resized. + /// + /// The new window width. + /// The new window height. + void OnPostResize(int width, int height); + + /// + /// Marks the beginning of a new frame. + /// + void OnNewFrame(); + + /// + /// Renders the draw data. + /// + /// The draw data. + void RenderDrawData(ImDrawDataPtr drawData); + + /// + /// Sets the texture pipeline. The pipeline must be created from the concrete implementation of this interface.
+ /// The references of and are copied. + /// You may dispose after the call. + ///
+ /// The texture handle. + /// The pipeline handle to set, or null to clear. + void SetTexturePipeline(IDalamudTextureWrap texture, ITexturePipelineWrap? pipeline); + + /// + /// Creates a new reference of the pipeline registered for use with the given texture.
+ /// Dispose after use. + ///
+ /// The texture handle. + /// The previous pixel shader handle, or null if none. + ITexturePipelineWrap? GetTexturePipeline(IDalamudTextureWrap texture); + + /// + /// Adds a user callback handler. + /// + /// The delegate. + /// The value to use with . + nint AddDrawCmdUserCallback(DrawCmdUserCallbackDelegate @delegate); + + /// + /// Removes a user callback handler. + /// + /// The delegate. + void RemoveDrawCmdUserCallback(DrawCmdUserCallbackDelegate @delegate); + + /// + /// Load an image from a span of bytes of specified format. + /// + /// The data to load. + /// Texture specifications. + /// Whether to support reading from CPU, while disabling reading from GPU. + /// Whether to support writing from CPU, while disabling writing from GPU. + /// Whether to allow rendering to this texture. + /// Name for debugging. + /// A texture, ready to use in ImGui. + IDalamudTextureWrap CreateTexture2D( + ReadOnlySpan data, + RawImageSpecification specs, + bool cpuRead, + bool cpuWrite, + bool allowRenderTarget, + [CallerMemberName] string debugName = ""); + + /// Creates a texture from an ImGui viewport. + /// The arguments for creating a texture. + /// The owner plugin. + /// Name for debug display purposes. + /// The cancellation token. + /// The copied texture on success. Dispose after use. + /// + /// Use ImGui.GetMainViewport().ID to capture the game screen with Dalamud rendered. + /// This function may throw an exception. + /// + IDalamudTextureWrap CreateTextureFromImGuiViewport( + ImGuiViewportTextureArgs args, + LocalPlugin? ownerPlugin, + string? debugName = null, + CancellationToken cancellationToken = default); + + /// + /// Wraps an existing native texture. + /// + /// Handle to a native texture. + /// Wrapped object. + IDalamudTextureWrap WrapFromTextureResource(nint handle); + + /// + /// Gets the specification of a texture. + /// + /// The texture to obtain data about. + /// The specifications. + RawImageSpecification GetTextureSpecification(IDalamudTextureWrap texture); + + /// + /// Gets the raw texture data. + /// + /// Texture to obtain its raw data. + /// Raw image specifications. + /// Extracted data. + byte[] GetTextureData(IDalamudTextureWrap texture, out RawImageSpecification specification); + + /// + /// Gets the raw texture resource. + /// + /// Texture to obtain its underlying resource. + /// The underlying resource. + nint GetTextureResource(IDalamudTextureWrap texture); + + /// + /// Draws a texture onto another texture. + /// + /// Target texture. + /// Relative coordinates of the left-top point of the rectangle in the target texture. + /// Relative coordinates of the right-bottom point of the rectangle in the target texture. + /// Source texture. + /// Relative coordinates of the left-top point of the rectangle in the source texture. + /// Relative coordinates of the right-bottom point of the rectangle in the source texture. + /// Whether to only copy alpha values. + void DrawTextureToTexture( + IDalamudTextureWrap target, + Vector2 targetUv0, + Vector2 targetUv1, + IDalamudTextureWrap source, + Vector2 sourceUv0, + Vector2 sourceUv1, + bool copyAlphaOnly = false); +} diff --git a/Dalamud/ImGuiScene/IImGuiScene.cs b/Dalamud/ImGuiScene/IImGuiScene.cs new file mode 100644 index 0000000000..3931a726a3 --- /dev/null +++ b/Dalamud/ImGuiScene/IImGuiScene.cs @@ -0,0 +1,161 @@ +using System.Numerics; +using System.Runtime.CompilerServices; +using System.Threading; + +using Dalamud.Interface.Textures; +using Dalamud.Interface.Textures.TextureWraps; +using Dalamud.Plugin.Internal.Types; + +namespace Dalamud.ImGuiScene; + +/// +/// Backend for ImGui. +/// +internal interface IImGuiScene : 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 or not the cursor should be overridden with the ImGui cursor. + /// + public bool UpdateCursor { get; set; } + + /// + /// Gets or sets the path of ImGui configuration .ini file. + /// + public string? IniPath { get; set; } + + /// + /// Gets the device handle. + /// + public nint DeviceHandle { get; } + + /// + /// Perform a render cycle. + /// + void Render(); + + /// + /// Handle stuff before resizing happens. + /// + void OnPreResize(); + + /// + /// Handle stuff after resizing happens. + /// + /// The new width. + /// The new height. + void OnPostResize(int newWidth, int newHeight); + + /// + /// Invalidate fonts immediately. + /// + /// Call this while handling . + void InvalidateFonts(); + + /// + /// Check whether the current backend supports the given texture format for shader input. + /// + /// DXGI format to check. + /// Whether it is supported. + public bool SupportsTextureFormat(int format); + + /// + /// Check whether the current backend supports the given texture format for rendering to. + /// + /// DXGI format to check. + /// Whether it is supported. + public bool SupportsTextureFormatForRenderTarget(int format); + + /// + IDalamudTextureWrap CreateTexture2D( + ReadOnlySpan data, + RawImageSpecification specs, + bool cpuRead, + bool cpuWrite, + bool allowRenderTarget, + [CallerMemberName] string debugName = ""); + + /// + IDalamudTextureWrap CreateTextureFromImGuiViewport( + ImGuiViewportTextureArgs args, + LocalPlugin? ownerPlugin, + string? debugName = null, + CancellationToken cancellationToken = default); + + /// + IDalamudTextureWrap WrapFromTextureResource(nint handle); + + /// + RawImageSpecification GetTextureSpecification(IDalamudTextureWrap texture); + + /// + byte[] GetTextureData(IDalamudTextureWrap texture, out RawImageSpecification specification); + + /// + nint GetTextureResource(IDalamudTextureWrap texture); + + /// + void DrawTextureToTexture( + IDalamudTextureWrap target, + Vector2 targetUv0, + Vector2 targetUv1, + IDalamudTextureWrap source, + Vector2 sourceUv0, + Vector2 sourceUv1, + bool copyAlphaOnly = false); + + /// + void SetTexturePipeline(IDalamudTextureWrap textureHandle, ITexturePipelineWrap? pipelineHandle); + + /// + ITexturePipelineWrap? GetTexturePipeline(IDalamudTextureWrap textureHandle); + + /// + /// Determines if is owned by this. + /// + /// The cursor. + /// Whether it is the case. + public bool IsImGuiCursor(nint cursorHandle); + + /// + /// Determines if this instance of is rendering to . + /// + /// The present target handle. + /// Whether it is the case. + public bool IsAttachedToPresentationTarget(nint targetHandle); + + /// + /// Determines if the main viewport is full screen. + /// + /// Whether it is the case. + public bool IsMainViewportFullScreen(); +} diff --git a/Dalamud/ImGuiScene/ITexturePipelineWrap.cs b/Dalamud/ImGuiScene/ITexturePipelineWrap.cs new file mode 100644 index 0000000000..eb05a5fecc --- /dev/null +++ b/Dalamud/ImGuiScene/ITexturePipelineWrap.cs @@ -0,0 +1,18 @@ +namespace Dalamud.ImGuiScene; + +/// +/// Represents a handle to an immutable texture pipeline. +/// +public interface ITexturePipelineWrap : ICloneable, IDisposable +{ + /// + /// Gets a value indicating whether this instance of has been disposed. + /// + bool IsDisposed { get; } + + /// + new ITexturePipelineWrap Clone(); + + /// + object ICloneable.Clone() => this.Clone(); +} diff --git a/Dalamud/ImGuiScene/IWin32Scene.cs b/Dalamud/ImGuiScene/IWin32Scene.cs new file mode 100644 index 0000000000..95c070c1f6 --- /dev/null +++ b/Dalamud/ImGuiScene/IWin32Scene.cs @@ -0,0 +1,19 @@ +using TerraFX.Interop.Windows; + +namespace Dalamud.ImGuiScene; + +/// +/// with Win32 support. +/// +internal interface IWin32Scene : IImGuiScene +{ + /// + /// 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/ImGuiScene/Implementations/Dx11Renderer.TextureData.cs b/Dalamud/ImGuiScene/Implementations/Dx11Renderer.TextureData.cs new file mode 100644 index 0000000000..a5f58e7603 --- /dev/null +++ b/Dalamud/ImGuiScene/Implementations/Dx11Renderer.TextureData.cs @@ -0,0 +1,114 @@ +using System.Diagnostics.CodeAnalysis; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +using Dalamud.ImGuiScene.Helpers; +using Dalamud.Interface.Textures.TextureWraps; + +using TerraFX.Interop; +using TerraFX.Interop.DirectX; +using TerraFX.Interop.Windows; + +namespace Dalamud.ImGuiScene.Implementations; + +/// +/// Deals with rendering ImGui using DirectX 11. +/// See https://github.com/ocornut/imgui/blob/master/examples/imgui_impl_dx11.cpp for the original implementation. +/// +[SuppressMessage( + "StyleCop.CSharp.LayoutRules", + "SA1519:Braces should not be omitted from multi-line child statement", + Justification = "Multiple fixed/using scopes")] +internal unsafe partial class Dx11Renderer +{ + [Guid("72fe3f82-3ffc-4be9-b008-4aef7a942f55")] + private class TextureData : ManagedComObjectBase, INativeGuid + { + public static readonly Guid MyGuid = + new(0x72fe3f82, 0x3ffc, 0x4be9, 0xb0, 0x08, 0x4a, 0xef, 0x7a, 0x94, 0x2f, 0x55); + + private ComPtr tex2D; + private ComPtr srv; + private TexturePipeline? customPipeline; + + public TextureData(ID3D11Texture2D* tex2D, ID3D11ShaderResourceView* srv, int width, int height) + { + this.tex2D = new(tex2D); + this.srv = new(srv); + this.Width = width; + this.Height = height; + } + + public static Guid* NativeGuid => (Guid*)Unsafe.AsPointer(ref Unsafe.AsRef(in MyGuid)); + + public int Width { get; private init; } + + public int Height { get; private init; } + + public TexturePipeline? CustomPipeline + { + get => this.customPipeline; + set + { + if (value == this.customPipeline) + return; + this.customPipeline?.Dispose(); + this.customPipeline = value?.CloneRef(); + } + } + + public ID3D11Texture2D* Resource => this.tex2D; + + public ID3D11ShaderResourceView* ShaderResourceView => this.srv; + + protected override void* DynamicCast(in Guid iid) => + iid == MyGuid ? this.AsComInterface() : base.DynamicCast(iid); + + protected override void FinalRelease() + { + this.tex2D.Reset(); + this.srv.Reset(); + this.customPipeline?.Dispose(); + this.customPipeline = null; + } + } + + private class TextureWrap : IDalamudTextureWrap, ICloneable + { + private TextureData? data; + + private TextureWrap(TextureData data) => this.data = data; + + ~TextureWrap() => this.ReleaseUnmanagedResources(); + + public TextureData Data => this.data ?? throw new ObjectDisposedException(nameof(TextureWrap)); + + public bool IsDisposed => this.data is null; + + public nint ImGuiHandle => (nint)this.Data.AsComInterface(); + + public int Width => this.Data.Width; + + public int Height => this.Data.Height; + + public static TextureWrap TakeOwnership(TextureData data) => new(data); + + public static TextureWrap NewReference(TextureData data) => new(data.CloneRef()); + + public void Dispose() + { + this.ReleaseUnmanagedResources(); + GC.SuppressFinalize(this); + } + + public IDalamudTextureWrap Clone() => NewReference(this.Data); + + object ICloneable.Clone() => this.Clone(); + + private void ReleaseUnmanagedResources() + { + this.data?.Dispose(); + this.data = null; + } + } +} diff --git a/Dalamud/ImGuiScene/Implementations/Dx11Renderer.TexturePipeline.cs b/Dalamud/ImGuiScene/Implementations/Dx11Renderer.TexturePipeline.cs new file mode 100644 index 0000000000..d86c44c2dc --- /dev/null +++ b/Dalamud/ImGuiScene/Implementations/Dx11Renderer.TexturePipeline.cs @@ -0,0 +1,122 @@ +using System.Diagnostics.CodeAnalysis; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +using Dalamud.ImGuiScene.Helpers; +using Dalamud.Utility; + +using TerraFX.Interop; +using TerraFX.Interop.DirectX; +using TerraFX.Interop.Windows; + +namespace Dalamud.ImGuiScene.Implementations; + +/// +/// Deals with rendering ImGui using DirectX 11. +/// See https://github.com/ocornut/imgui/blob/master/examples/imgui_impl_dx11.cpp for the original implementation. +/// +[SuppressMessage( + "StyleCop.CSharp.LayoutRules", + "SA1519:Braces should not be omitted from multi-line child statement", + Justification = "Multiple fixed/using scopes")] +internal unsafe partial class Dx11Renderer +{ + [Guid("d3a0fe60-060a-49d6-8f6d-68e2ec5905c5")] + private class TexturePipeline : ManagedComObjectBase, INativeGuid + { + private static readonly Guid MyGuid = + new(0xd3a0fe60, 0x060a, 0x49d6, 0x8f, 0x6d, 0x68, 0xe2, 0xec, 0x59, 0x05, 0xc5); + + private ComPtr shader; + private ComPtr sampler; + + public TexturePipeline(ID3D11PixelShader* pixelShader, ID3D11SamplerState* samplerState) + { + this.shader = new(pixelShader); + this.sampler = new(samplerState); + } + + public static Guid* NativeGuid => (Guid*)Unsafe.AsPointer(ref Unsafe.AsRef(in MyGuid)); + + public static TexturePipeline From( + ID3D11Device* device, + ReadOnlySpan ps, + in D3D11_SAMPLER_DESC samplerDesc) + { + using var shader = default(ComPtr); + fixed (byte* pArray = ps) + device->CreatePixelShader(pArray, (nuint)ps.Length, null, shader.GetAddressOf()).ThrowOnError(); + + using var sampler = default(ComPtr); + fixed (D3D11_SAMPLER_DESC* pSamplerDesc = &samplerDesc) + device->CreateSamplerState(pSamplerDesc, sampler.GetAddressOf()).ThrowOnError(); + + return new(shader, sampler); + } + + public static TexturePipeline From( + ID3D11Device* device, + ReadOnlySpan ps) => From( + device, + ps, + new() + { + Filter = D3D11_FILTER.D3D11_FILTER_MIN_MAG_MIP_LINEAR, + AddressU = D3D11_TEXTURE_ADDRESS_MODE.D3D11_TEXTURE_ADDRESS_WRAP, + AddressV = D3D11_TEXTURE_ADDRESS_MODE.D3D11_TEXTURE_ADDRESS_WRAP, + AddressW = D3D11_TEXTURE_ADDRESS_MODE.D3D11_TEXTURE_ADDRESS_WRAP, + MipLODBias = 0, + MaxAnisotropy = 0, + ComparisonFunc = D3D11_COMPARISON_FUNC.D3D11_COMPARISON_ALWAYS, + MinLOD = 0, + MaxLOD = 0, + }); + + public void BindTo(ID3D11DeviceContext* ctx) + { + ctx->PSSetShader(this.shader, null, 0); + ctx->PSSetSamplers(0, 1, this.sampler.GetAddressOf()); + } + + protected override void* DynamicCast(in Guid guid) => guid == MyGuid ? this.AsComInterface() : null; + + protected override void FinalRelease() + { + this.shader.Reset(); + this.sampler.Reset(); + } + } + + private class TexturePipelineWrap : ITexturePipelineWrap + { + private TexturePipeline? data; + + private TexturePipelineWrap(TexturePipeline data) => this.data = data; + + ~TexturePipelineWrap() => this.ReleaseUnmanagedResources(); + + public TexturePipeline Data => this.data ?? throw new ObjectDisposedException(nameof(TextureWrap)); + + public bool IsDisposed => this.data is null; + + public static TexturePipelineWrap TakeOwnership(TexturePipeline data) => new(data); + + public static TexturePipelineWrap NewReference(TexturePipeline data) => new(data.CloneRef()); + + public void Dispose() + { + this.ReleaseUnmanagedResources(); + GC.SuppressFinalize(this); + } + + public ITexturePipelineWrap Clone() => NewReference(this.Data); + + object ICloneable.Clone() => this.Clone(); + + private void ReleaseUnmanagedResources() + { + this.data?.Dispose(); + this.data = null; + } + } +} diff --git a/Dalamud/ImGuiScene/Implementations/Dx11Renderer.ViewportHandler.cs b/Dalamud/ImGuiScene/Implementations/Dx11Renderer.ViewportHandler.cs new file mode 100644 index 0000000000..3d53f5621b --- /dev/null +++ b/Dalamud/ImGuiScene/Implementations/Dx11Renderer.ViewportHandler.cs @@ -0,0 +1,375 @@ +using System.Diagnostics.CodeAnalysis; +using System.Numerics; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +using Dalamud.ImGuiScene.Helpers; +using Dalamud.Utility; + +using ImGuiNET; + +using TerraFX.Interop; +using TerraFX.Interop.DirectX; +using TerraFX.Interop.Windows; + +using static TerraFX.Interop.Windows.Windows; + +namespace Dalamud.ImGuiScene.Implementations; + +/// +/// Deals with rendering ImGui using DirectX 11. +/// See https://github.com/ocornut/imgui/blob/master/examples/imgui_impl_dx11.cpp for the original implementation. +/// +[SuppressMessage( + "StyleCop.CSharp.LayoutRules", + "SA1519:Braces should not be omitted from multi-line child statement", + Justification = "Multiple fixed/using scopes")] +internal unsafe partial class Dx11Renderer +{ + private class ViewportHandler : IDisposable + { + private readonly Dx11Renderer renderer; + + [SuppressMessage("ReSharper", "NotAccessedField.Local", Justification = "Keeping reference alive")] + private readonly ImGuiViewportHelpers.CreateWindowDelegate cwd; + + public ViewportHandler(Dx11Renderer renderer) + { + this.renderer = renderer; + + var pio = ImGui.GetPlatformIO(); + pio.Renderer_CreateWindow = Marshal.GetFunctionPointerForDelegate(this.cwd = this.OnCreateWindow); + pio.Renderer_DestroyWindow = (nint)(delegate* unmanaged)&OnDestroyWindow; + pio.Renderer_SetWindowSize = (nint)(delegate* unmanaged)&OnSetWindowSize; + pio.Renderer_RenderWindow = (nint)(delegate* unmanaged)&OnRenderWindow; + pio.Renderer_SwapBuffers = (nint)(delegate* unmanaged)&OnSwapBuffers; + } + + ~ViewportHandler() => ReleaseUnmanagedResources(); + + public void Dispose() + { + ReleaseUnmanagedResources(); + GC.SuppressFinalize(this); + } + + private static void ReleaseUnmanagedResources() + { + var pio = ImGui.GetPlatformIO(); + pio.Renderer_CreateWindow = nint.Zero; + pio.Renderer_DestroyWindow = nint.Zero; + pio.Renderer_SetWindowSize = nint.Zero; + pio.Renderer_RenderWindow = nint.Zero; + pio.Renderer_SwapBuffers = nint.Zero; + } + + [UnmanagedCallersOnly] + private static void OnDestroyWindow(ImGuiViewportPtr viewport) + { + if (viewport.RendererUserData == nint.Zero) + return; + ViewportData.AttachFromAddress(viewport.RendererUserData).Release(); + viewport.RendererUserData = nint.Zero; + } + + [UnmanagedCallersOnly] + private static void OnSetWindowSize(ImGuiViewportPtr viewport, Vector2 size) => + ViewportData.AttachFromAddress(viewport.RendererUserData) + .ResizeBuffers((int)size.X, (int)size.Y, true); + + [UnmanagedCallersOnly] + private static void OnRenderWindow(ImGuiViewportPtr viewport, nint v) => + ViewportData.AttachFromAddress(viewport.RendererUserData) + .Draw(viewport.DrawData, true); + + [UnmanagedCallersOnly] + private static void OnSwapBuffers(ImGuiViewportPtr viewport, nint v) => + ViewportData.AttachFromAddress(viewport.RendererUserData) + .PresentIfSwapChainAvailable(); + + private void OnCreateWindow(ImGuiViewportPtr viewport) + { + // PlatformHandleRaw should always be a HWND, whereas PlatformHandle might be a higher-level handle (e.g. GLFWWindow*, SDL_Window*). + // Some backend will leave PlatformHandleRaw NULL, in which case we assume PlatformHandle will contain the HWND. + var hWnd = viewport.PlatformHandleRaw; + if (hWnd == 0) + hWnd = viewport.PlatformHandle; + try + { + viewport.RendererUserData = ViewportData.CreateDComposition(this.renderer, (HWND)hWnd).AsHandle(); + } + catch + { + viewport.RendererUserData = ViewportData.Create(this.renderer, (HWND)hWnd).AsHandle(); + } + } + } + + private class ViewportData : ManagedComObjectBase, INativeGuid + { + public static readonly Guid MyGuid = + new(0x98eaa0be, 0x9123, 0x4346, 0x94, 0x16, 0xe0, 0xd7, 0x54, 0xbe, 0x45, 0x8d); + + private readonly Dx11Renderer parent; + + private ComPtr swapChain; + private ComPtr renderTarget; + private ComPtr renderTargetView; + private ComPtr dcompVisual; + private ComPtr dcompTarget; + + private int width; + private int height; + + public ViewportData( + Dx11Renderer parent, + IDXGISwapChain* swapChain, + int width, + int height, + IDCompositionVisual* dcompVisual, + IDCompositionTarget* dcompTarget) + { + this.parent = parent; + this.swapChain = new(swapChain); + this.width = width; + this.height = height; + if (dcompVisual is not null) + this.dcompVisual = new(dcompVisual); + if (dcompTarget is not null) + this.dcompTarget = new(dcompTarget); + } + + public static Guid* NativeGuid => (Guid*)Unsafe.AsPointer(ref Unsafe.AsRef(in MyGuid)); + + public IDXGISwapChain* SwapChain => this.swapChain; + + private ID3D11Device* Device => this.parent.device.Get(); + + private DXGI_FORMAT RtvFormat => this.parent.rtvFormat; + + public static ViewportData Create( + Dx11Renderer renderer, + IDXGISwapChain* swapChain, + IDCompositionVisual* dcompVisual, + IDCompositionTarget* dcompTarget) + { + DXGI_SWAP_CHAIN_DESC desc; + swapChain->GetDesc(&desc).ThrowOnError(); + return new( + renderer, + swapChain, + (int)desc.BufferDesc.Width, + (int)desc.BufferDesc.Height, + dcompVisual, + dcompTarget); + } + + public static ViewportData CreateDComposition(Dx11Renderer renderer, HWND hWnd) + { + if (renderer.dcompDevice.IsEmpty()) + throw new NotSupportedException(); + + var mvsd = default(DXGI_SWAP_CHAIN_DESC); + renderer.mainViewport.SwapChain->GetDesc(&mvsd).ThrowOnError(); + + using var dxgiFactory = default(ComPtr); + fixed (Guid* piidFactory = &IID.IID_IDXGIFactory4) + { +#if DEBUG + DirectX.CreateDXGIFactory2( + DXGI.DXGI_CREATE_FACTORY_DEBUG, + piidFactory, + (void**)dxgiFactory.GetAddressOf()).ThrowOnError(); +#else + DirectX.CreateDXGIFactory1(piidFactory, (void**)dxgiFactory.GetAddressOf()).ThrowOnError(); +#endif + } + + RECT rc; + if (!GetWindowRect(hWnd, &rc) || rc.right == rc.left || rc.bottom == rc.top) + rc = new(0, 0, 4, 4); + + using var swapChain1 = default(ComPtr); + var sd1 = new DXGI_SWAP_CHAIN_DESC1 + { + Width = (uint)(rc.right - rc.left), + Height = (uint)(rc.bottom - rc.top), + Format = renderer.rtvFormat, + Stereo = false, + SampleDesc = new(1, 0), + BufferUsage = DXGI.DXGI_USAGE_RENDER_TARGET_OUTPUT, + BufferCount = Math.Max(2u, mvsd.BufferCount), + Scaling = DXGI_SCALING.DXGI_SCALING_STRETCH, + SwapEffect = DXGI_SWAP_EFFECT.DXGI_SWAP_EFFECT_FLIP_SEQUENTIAL, + AlphaMode = DXGI_ALPHA_MODE.DXGI_ALPHA_MODE_PREMULTIPLIED, + Flags = 0, + }; + dxgiFactory.Get()->CreateSwapChainForComposition( + (IUnknown*)renderer.device.Get(), + &sd1, + null, + swapChain1.GetAddressOf()).ThrowOnError(); + + if (ReShadePeeler.PeelSwapChain(&swapChain1)) + { + swapChain1.Get()->ResizeBuffers(sd1.BufferCount, sd1.Width, sd1.Height, sd1.Format, sd1.Flags) + .ThrowOnError(); + } + + using var dcTarget = default(ComPtr); + renderer.dcompDevice.Get()->CreateTargetForHwnd(hWnd, BOOL.TRUE, dcTarget.GetAddressOf()); + + using var dcVisual = default(ComPtr); + renderer.dcompDevice.Get()->CreateVisual(dcVisual.GetAddressOf()).ThrowOnError(); + + dcVisual.Get()->SetContent((IUnknown*)swapChain1.Get()).ThrowOnError(); + dcTarget.Get()->SetRoot(dcVisual).ThrowOnError(); + renderer.dcompDevice.Get()->Commit().ThrowOnError(); + + using var swapChain = default(ComPtr); + swapChain1.As(&swapChain).ThrowOnError(); + return Create(renderer, swapChain, dcVisual, dcTarget); + } + + public static ViewportData Create(Dx11Renderer renderer, HWND hWnd) + { + using var dxgiFactory = default(ComPtr); + fixed (Guid* piidFactory = &IID.IID_IDXGIFactory) + { +#if DEBUG + DirectX.CreateDXGIFactory2( + DXGI.DXGI_CREATE_FACTORY_DEBUG, + piidFactory, + (void**)dxgiFactory.GetAddressOf()).ThrowOnError(); +#else + DirectX.CreateDXGIFactory(piidFactory, (void**)dxgiFactory.GetAddressOf()).ThrowOnError(); +#endif + } + + // Create swapchain + using var swapChain = default(ComPtr); + var desc = new DXGI_SWAP_CHAIN_DESC + { + BufferDesc = + { + Format = DXGI_FORMAT.DXGI_FORMAT_R8G8B8A8_UNORM, + }, + SampleDesc = new(1, 0), + BufferUsage = DXGI.DXGI_USAGE_RENDER_TARGET_OUTPUT, + BufferCount = 1, + OutputWindow = hWnd, + Windowed = true, + SwapEffect = DXGI_SWAP_EFFECT.DXGI_SWAP_EFFECT_DISCARD, + }; + dxgiFactory.Get()->CreateSwapChain((IUnknown*)renderer.device.Get(), &desc, swapChain.GetAddressOf()) + .ThrowOnError(); + + if (ReShadePeeler.PeelSwapChain(&swapChain)) + { + swapChain.Get()->ResizeBuffers( + desc.BufferCount, + desc.BufferDesc.Width, + desc.BufferDesc.Height, + desc.BufferDesc.Format, + desc.Flags) + .ThrowOnError(); + } + + return Create(renderer, swapChain, null, null); + } + + public void Draw(ImDrawDataPtr drawData, bool clearRenderTarget) + { + if (this.width < 1 || this.height < 1) + return; + + this.EnsureRenderTarget(); + this.parent.RenderDrawDataInternal(this.renderTargetView, drawData, clearRenderTarget); + } + + public void PresentIfSwapChainAvailable() + { + if (this.width < 1 || this.height < 1) + return; + + if (!this.swapChain.IsEmpty()) + this.swapChain.Get()->Present(0, 0).ThrowOnError(); + } + + public void ResetBuffers() + { + this.renderTargetView.Reset(); + this.renderTarget.Reset(); + } + + public void ResizeBuffers(int newWidth, int newHeight, bool resizeSwapChain) + { + this.ResetBuffers(); + + this.width = newWidth; + this.height = newHeight; + if (this.width < 1 || this.height < 1) + return; + + if (resizeSwapChain && !this.swapChain.IsEmpty()) + { + DXGI_SWAP_CHAIN_DESC desc; + this.swapChain.Get()->GetDesc(&desc).ThrowOnError(); + this.swapChain.Get()->ResizeBuffers( + desc.BufferCount, + (uint)newWidth, + (uint)newHeight, + DXGI_FORMAT.DXGI_FORMAT_UNKNOWN, + desc.Flags).ThrowOnError(); + } + } + + protected override void FinalRelease() + { + this.ResetBuffers(); + this.dcompVisual.Reset(); + this.dcompTarget.Reset(); + this.swapChain.Reset(); + } + + private void EnsureRenderTarget() + { + if (!this.renderTarget.IsEmpty() && !this.renderTargetView.IsEmpty()) + return; + + this.ResetBuffers(); + + fixed (ID3D11Texture2D** pprt = &this.renderTarget.GetPinnableReference()) + fixed (ID3D11RenderTargetView** pprtv = &this.renderTargetView.GetPinnableReference()) + { + if (this.swapChain.IsEmpty()) + { + var desc = new D3D11_TEXTURE2D_DESC + { + Width = (uint)this.width, + Height = (uint)this.height, + MipLevels = 1, + ArraySize = 1, + Format = this.RtvFormat, + SampleDesc = new(1, 0), + Usage = D3D11_USAGE.D3D11_USAGE_DEFAULT, + BindFlags = (uint)D3D11_BIND_FLAG.D3D11_BIND_SHADER_RESOURCE, + CPUAccessFlags = 0, + MiscFlags = (uint)D3D11_RESOURCE_MISC_FLAG.D3D11_RESOURCE_MISC_SHARED_NTHANDLE, + }; + this.parent.device.Get()->CreateTexture2D(&desc, null, pprt).ThrowOnError(); + } + else + { + fixed (Guid* piid = &IID.IID_ID3D11Texture2D) + { + this.swapChain.Get()->GetBuffer(0u, piid, (void**)pprt) + .ThrowOnError(); + } + } + + this.parent.device.Get()->CreateRenderTargetView((ID3D11Resource*)*pprt, null, pprtv).ThrowOnError(); + } + } + } +} diff --git a/Dalamud/ImGuiScene/Implementations/Dx11Renderer.ViewportTextureWrap.cs b/Dalamud/ImGuiScene/Implementations/Dx11Renderer.ViewportTextureWrap.cs new file mode 100644 index 0000000000..8f5d8da9d2 --- /dev/null +++ b/Dalamud/ImGuiScene/Implementations/Dx11Renderer.ViewportTextureWrap.cs @@ -0,0 +1,287 @@ +using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; +using System.Numerics; +using System.Threading; + +using Dalamud.Game; +using Dalamud.Interface.Internal; +using Dalamud.Interface.Textures; +using Dalamud.Interface.Textures.Internal; +using Dalamud.Interface.Textures.TextureWraps; +using Dalamud.Plugin.Internal.Types; +using Dalamud.Storage.Assets; +using Dalamud.Utility; + +using ImGuiNET; + +using TerraFX.Interop.DirectX; +using TerraFX.Interop.Windows; + +using NotSupportedException = System.NotSupportedException; + +namespace Dalamud.ImGuiScene.Implementations; + +/// +/// Deals with rendering ImGui using DirectX 11. +/// See https://github.com/ocornut/imgui/blob/master/examples/imgui_impl_dx11.cpp for the original implementation. +/// +[SuppressMessage( + "StyleCop.CSharp.LayoutRules", + "SA1519:Braces should not be omitted from multi-line child statement", + Justification = "Multiple fixed/using scopes")] +internal unsafe partial class Dx11Renderer +{ + /// A texture wrap that takes its buffer from the frame buffer (of swap chain). + private sealed class ViewportTextureWrap : IDalamudTextureWrap, IDeferredDisposable + { + private readonly string? debugName; + private readonly Dx11Renderer renderer; + private readonly LocalPlugin? ownerPlugin; + private readonly CancellationToken cancellationToken; + + private ImGuiViewportTextureArgs args; + private D3D11_TEXTURE2D_DESC desc; + private ComPtr tex; + private ComPtr srv; + private ComPtr rtv; + + private bool disposed; + + /// Initializes a new instance of the class. + /// The renderer. + /// The arguments for creating a texture. + /// Name for debug display purposes. + /// The owner plugin. + /// The cancellation token. + public ViewportTextureWrap( + Dx11Renderer renderer, + ImGuiViewportTextureArgs args, + string? debugName, + LocalPlugin? ownerPlugin, + CancellationToken cancellationToken) + { + this.renderer = renderer; + this.args = args; + this.debugName = debugName; + this.ownerPlugin = ownerPlugin; + this.cancellationToken = cancellationToken; + } + + /// Finalizes an instance of the class. + ~ViewportTextureWrap() => this.Dispose(false); + + /// + public nint ImGuiHandle + { + get + { + var t = (nint)this.srv.Get(); + return t == nint.Zero ? Service.Get().Empty4X4.ImGuiHandle : t; + } + } + + /// + public int Width => (int)this.desc.Width; + + /// + public int Height => (int)this.desc.Height; + + /// Updates the texture from the source viewport. + public void Update() + { + if (this.cancellationToken.IsCancellationRequested || this.disposed) + return; + + ThreadSafety.AssertMainThread(); + + using var backBuffer = GetImGuiViewportBackBuffer(this.args.ViewportId); + D3D11_TEXTURE2D_DESC newDesc; + backBuffer.Get()->GetDesc(&newDesc); + + if (newDesc.SampleDesc.Count > 1) + throw new NotSupportedException("Multisampling is not expected"); + + using var device = default(ComPtr); + backBuffer.Get()->GetDevice(device.GetAddressOf()); + + using var context = default(ComPtr); + device.Get()->GetImmediateContext(context.GetAddressOf()); + + var copyBox = new D3D11_BOX + { + left = (uint)MathF.Round(newDesc.Width * this.args.Uv0.X), + top = (uint)MathF.Round(newDesc.Height * this.args.Uv0.Y), + right = (uint)MathF.Round(newDesc.Width * this.args.Uv1Effective.X), + bottom = (uint)MathF.Round(newDesc.Height * this.args.Uv1Effective.Y), + front = 0, + back = 1, + }; + + if (this.desc.Width != copyBox.right - copyBox.left + || this.desc.Height != copyBox.bottom - copyBox.top + || this.desc.Format != newDesc.Format) + { + var texDesc = new D3D11_TEXTURE2D_DESC + { + Width = copyBox.right - copyBox.left, + Height = copyBox.bottom - copyBox.top, + MipLevels = 1, + ArraySize = 1, + Format = newDesc.Format, + SampleDesc = new(1, 0), + Usage = D3D11_USAGE.D3D11_USAGE_DEFAULT, + BindFlags = (uint)(D3D11_BIND_FLAG.D3D11_BIND_SHADER_RESOURCE | + D3D11_BIND_FLAG.D3D11_BIND_RENDER_TARGET), + CPUAccessFlags = 0u, + MiscFlags = 0u, + }; + + using var texTemp = default(ComPtr); + device.Get()->CreateTexture2D(&texDesc, null, texTemp.GetAddressOf()).ThrowOnError(); + + var rtvDesc = new D3D11_RENDER_TARGET_VIEW_DESC( + texTemp, + D3D11_RTV_DIMENSION.D3D11_RTV_DIMENSION_TEXTURE2D); + using var rtvTemp = default(ComPtr); + device.Get()->CreateRenderTargetView( + (ID3D11Resource*)texTemp.Get(), + &rtvDesc, + rtvTemp.GetAddressOf()).ThrowOnError(); + + var srvDesc = new D3D11_SHADER_RESOURCE_VIEW_DESC( + texTemp, + D3D_SRV_DIMENSION.D3D11_SRV_DIMENSION_TEXTURE2D); + using var srvTemp = default(ComPtr); + device.Get()->CreateShaderResourceView( + (ID3D11Resource*)texTemp.Get(), + &srvDesc, + srvTemp.GetAddressOf()) + .ThrowOnError(); + + this.desc = texDesc; + srvTemp.Swap(ref this.srv); + rtvTemp.Swap(ref this.rtv); + texTemp.Swap(ref this.tex); + } + + // context.Get()->CopyResource((ID3D11Resource*)this.tex.Get(), (ID3D11Resource*)backBuffer.Get()); + context.Get()->CopySubresourceRegion( + (ID3D11Resource*)this.tex.Get(), + 0, + 0, + 0, + 0, + (ID3D11Resource*)backBuffer.Get(), + 0, + ©Box); + + if (!this.args.KeepTransparency) + { + var rtvLocal = this.rtv.Get(); + context.Get()->OMSetRenderTargets(1u, &rtvLocal, null); + this.renderer.rectangleDrawer.Draw( + this.renderer.context, + this.rtv, + Vector2.Zero, + Vector2.One, + TextureData.AttachFromAddress(Service.Get().White4X4.ImGuiHandle) + .ShaderResourceView, + Vector2.Zero, + Vector2.One, + true); + + var dummy = default(ID3D11RenderTargetView*); + context.Get()->OMSetRenderTargets(1u, &dummy, null); + } + + if (this.args.AutoUpdate) + this.QueueUpdate(); + } + + /// Queues a call to . + public void QueueUpdate() => + Service.Get().RunOnTick( + () => + { + if (this.args.TakeBeforeImGuiRender) + Service.Get().RunBeforeImGuiRender(this.Update); + else + Service.Get().RunAfterImGuiRender(this.Update); + }, + cancellationToken: this.cancellationToken); + + /// Queue the texture to be disposed once the frame ends. + public void Dispose() + { + this.Dispose(true); + GC.SuppressFinalize(this); + } + + /// Actually dispose the wrapped texture. + void IDeferredDisposable.RealDispose() + { + this.tex.Reset(); + this.srv.Reset(); + this.rtv.Reset(); + } + + private static ComPtr GetImGuiViewportBackBuffer(uint viewportId) + { + ThreadSafety.AssertMainThread(); + var viewports = ImGui.GetPlatformIO().Viewports; + var viewportIndex = 0; + for (; viewportIndex < viewports.Size; viewportIndex++) + { + if (viewports[viewportIndex].ID == viewportId) + break; + } + + if (viewportIndex >= viewports.Size) + { + throw new ArgumentOutOfRangeException( + nameof(viewportId), + viewportId, + "Could not find a viewport with the given ID."); + } + + var texture = default(ComPtr); + + Debug.Assert(viewports[0].ID == ImGui.GetMainViewport().ID, "ImGui has changed"); + if (viewportId == viewports[0].ID) + { + var device = FFXIVClientStructs.FFXIV.Client.Graphics.Kernel.Device.Instance(); + fixed (Guid* piid = &IID.IID_ID3D11Texture2D) + { + ((IDXGISwapChain*)device->SwapChain->DXGISwapChain)->GetBuffer( + 0, + piid, + (void**)texture.GetAddressOf()) + .ThrowOnError(); + } + } + else + { + // See: ImGui_Impl_DX11.ImGuiViewportDataDx11 + var rud = (nint*)viewports[viewportIndex].RendererUserData; + if (rud == null || rud[0] == nint.Zero || rud[1] == nint.Zero) + throw new InvalidOperationException(); + + using var resource = default(ComPtr); + ((ID3D11RenderTargetView*)rud[1])->GetResource(resource.GetAddressOf()); + resource.As(&texture).ThrowOnError(); + } + + return texture; + } + + private void Dispose(bool disposing) + { + this.disposed = true; + this.args.AutoUpdate = false; + if (disposing) + Service.GetNullable()?.EnqueueDeferredDispose(this); + else + ((IDeferredDisposable)this).RealDispose(); + } + } +} diff --git a/Dalamud/ImGuiScene/Implementations/Dx11Renderer.cs b/Dalamud/ImGuiScene/Implementations/Dx11Renderer.cs new file mode 100644 index 0000000000..4875d52e0a --- /dev/null +++ b/Dalamud/ImGuiScene/Implementations/Dx11Renderer.cs @@ -0,0 +1,862 @@ +using System.Buffers; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.Linq; +using System.Numerics; +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; +using System.Text; +using System.Threading; + +using Dalamud.ImGuiScene.Helpers; +using Dalamud.Interface.Textures; +using Dalamud.Interface.Textures.TextureWraps; +using Dalamud.Interface.Utility; +using Dalamud.Plugin.Internal.Types; +using Dalamud.Utility; +using Dalamud.Utility.TerraFxCom; + +using ImGuiNET; + +using TerraFX.Interop.DirectX; +using TerraFX.Interop.Windows; + +namespace Dalamud.ImGuiScene.Implementations; + +/// +/// Deals with rendering ImGui using DirectX 11. +/// See https://github.com/ocornut/imgui/blob/master/examples/imgui_impl_dx11.cpp for the original implementation. +/// +[SuppressMessage( + "StyleCop.CSharp.LayoutRules", + "SA1519:Braces should not be omitted from multi-line child statement", + Justification = "Multiple fixed/using scopes")] +internal unsafe partial class Dx11Renderer : IImGuiRenderer +{ + private readonly Dictionary userCallbacks = new(); + private readonly List fontTextures = new(); + private readonly D3D_FEATURE_LEVEL featureLevel; + private readonly ViewportHandler viewportHandler; + private readonly nint renderNamePtr; + private readonly DXGI_FORMAT rtvFormat; + private readonly ViewportData mainViewport; + private readonly D3D11RectangleDrawer rectangleDrawer; + + private bool releaseUnmanagedResourceCalled; + + private ComPtr device; + private ComPtr context; + private ComPtr vertexShader; + private ComPtr inputLayout; + private ComPtr vertexConstantBuffer; + private ComPtr blendState; + private ComPtr rasterizerState; + private ComPtr depthStencilState; + private ComPtr vertexBuffer; + private ComPtr indexBuffer; + private int vertexBufferSize; + private int indexBufferSize; + + private ComPtr dcompDevice; + + private TexturePipeline? defaultPipeline; + + /// + /// Initializes a new instance of the class. + /// + /// The swap chain. + /// A pointer to an instance of . + /// A pointer to an instance of . + public Dx11Renderer(IDXGISwapChain* swapChain, ID3D11Device* device, ID3D11DeviceContext* context) + { + var io = ImGui.GetIO(); + if (ImGui.GetIO().NativePtr->BackendRendererName is not null) + throw new InvalidOperationException("ImGui backend renderer seems to be have been already attached."); + try + { + DXGI_SWAP_CHAIN_DESC desc; + swapChain->GetDesc(&desc).ThrowOnError(); + this.rtvFormat = desc.BufferDesc.Format; + this.device = new(device); + this.context = new(context); + this.featureLevel = device->GetFeatureLevel(); + + io.BackendFlags |= ImGuiBackendFlags.RendererHasVtxOffset | ImGuiBackendFlags.RendererHasViewports; + + this.renderNamePtr = Marshal.StringToHGlobalAnsi("imgui_impl_dx11_c#"); + io.NativePtr->BackendRendererName = (byte*)this.renderNamePtr; + + if (io.ConfigFlags.HasFlag(ImGuiConfigFlags.ViewportsEnable)) + { + try + { + fixed (IDCompositionDevice** pp = &this.dcompDevice.GetPinnableReference()) + fixed (Guid* piidDCompositionDevice = &IID.IID_IDCompositionDevice) + DirectX.DCompositionCreateDevice(null, piidDCompositionDevice, (void**)pp).ThrowOnError(); + + ImGuiViewportHelpers.EnableViewportWindowBackgroundAlpha(); + } + catch + { + // don't care; not using DComposition then + } + + this.viewportHandler = new(this); + } + + this.mainViewport = ViewportData.Create(this, swapChain, null, null); + ImGui.GetPlatformIO().Viewports[0].RendererUserData = this.mainViewport.AsHandle(); + + this.rectangleDrawer = new(); + this.rectangleDrawer.Setup(this.device.Get()); + } + catch + { + this.rectangleDrawer?.Dispose(); + this.ReleaseUnmanagedResources(); + throw; + } + } + + /// + /// Finalizes an instance of the class. + /// + ~Dx11Renderer() => this.ReleaseUnmanagedResources(); + + /// + public void Dispose() + { + this.rectangleDrawer.Dispose(); + this.ReleaseUnmanagedResources(); + GC.SuppressFinalize(this); + } + + /// + public void OnNewFrame() + { + this.EnsureDeviceObjects(); + } + + /// + public void OnPreResize() => this.mainViewport.ResetBuffers(); + + /// + public void OnPostResize(int width, int height) => this.mainViewport.ResizeBuffers(width, height, false); + + /// + public void RenderDrawData(ImDrawDataPtr drawData) => + this.mainViewport.Draw(drawData, this.mainViewport.SwapChain == null); + + /// + /// Creates a new texture pipeline. + /// + /// The pixel shader data. + /// The sampler description. + /// The handle to the new texture pipeline. + public ITexturePipelineWrap CreateTexturePipeline(ReadOnlySpan ps, in D3D11_SAMPLER_DESC samplerDesc) => + TexturePipelineWrap.TakeOwnership(TexturePipeline.From(this.device, ps, samplerDesc)); + + /// + public ITexturePipelineWrap? GetTexturePipeline(IDalamudTextureWrap texture) => + TextureData.AttachFromAddress(texture.ImGuiHandle).CustomPipeline is { } cp + ? TexturePipelineWrap.NewReference(cp) + : null; + + /// + public void DrawTextureToTexture( + IDalamudTextureWrap target, + Vector2 targetUv0, + Vector2 targetUv1, + IDalamudTextureWrap source, + Vector2 sourceUv0, + Vector2 sourceUv1, + bool copyAlphaOnly = false) + { + var sourceSrv = TextureData.AttachFromAddress(source.ImGuiHandle).ShaderResourceView; + + using var targetResource = + new ComPtr((ID3D11Resource*)TextureData.AttachFromAddress(target.ImGuiHandle).Resource); + using var targetTex2D = default(ComPtr); + targetResource.As(&targetTex2D).ThrowOnError(); + + using var targetRtv = default(ComPtr); + var targetRtvDesc = new D3D11_RENDER_TARGET_VIEW_DESC( + targetTex2D, + D3D11_RTV_DIMENSION.D3D11_RTV_DIMENSION_TEXTURE2D); + this.device.Get()->CreateRenderTargetView(targetResource.Get(), &targetRtvDesc, targetRtv.GetAddressOf()) + .ThrowOnError(); + + this.rectangleDrawer.Draw( + this.context, + targetRtv, + targetUv0, + targetUv1, + sourceSrv, + sourceUv0, + sourceUv1, + copyAlphaOnly); + } + + /// + public byte[] GetTextureData(IDalamudTextureWrap texture, out RawImageSpecification specification) + { + var tex2D = TextureData.AttachFromAddress(texture.ImGuiHandle).Resource; + var desc = tex2D->GetDesc(); + + using var tmpTex = + (desc.CPUAccessFlags & (uint)D3D11_CPU_ACCESS_FLAG.D3D11_CPU_ACCESS_READ) == 0 + ? this.device.CreateTexture2D( + desc with + { + MipLevels = 1, + ArraySize = 1, + SampleDesc = new(1, 0), + Usage = D3D11_USAGE.D3D11_USAGE_STAGING, + BindFlags = 0u, + CPUAccessFlags = (uint)D3D11_CPU_ACCESS_FLAG.D3D11_CPU_ACCESS_READ, + MiscFlags = 0u, + }, + tex2D) + : default; + + var mapWhat = (ID3D11Resource*)(tmpTex.IsEmpty() ? tex2D : tmpTex.Get()); + + D3D11_MAPPED_SUBRESOURCE mapped; + this.context.Get()->Map(mapWhat, 0, D3D11_MAP.D3D11_MAP_READ, 0, &mapped).ThrowOnError(); + + try + { + specification = new(desc, mapped.RowPitch); + return new Span(mapped.pData, checked((int)mapped.DepthPitch)).ToArray(); + } + finally + { + this.context.Get()->Unmap(mapWhat, 0); + } + } + + /// + public void SetTexturePipeline(IDalamudTextureWrap texture, ITexturePipelineWrap? pipeline) => + TextureData.AttachFromAddress(texture.ImGuiHandle).CustomPipeline = pipeline switch + { + TexturePipelineWrap tpw => tpw.Data, + null => default, + _ => throw new ArgumentException("Not a compatible texture pipeline wrap.", nameof(pipeline)), + }; + + /// + public nint AddDrawCmdUserCallback(IImGuiRenderer.DrawCmdUserCallbackDelegate @delegate) + { + if (this.userCallbacks.FirstOrDefault(x => x.Value == @delegate).Key is not 0 and var key) + return key; + + key = Marshal.GetFunctionPointerForDelegate(@delegate); + this.userCallbacks.Add(key, @delegate); + return key; + } + + /// + public void RemoveDrawCmdUserCallback(IImGuiRenderer.DrawCmdUserCallbackDelegate @delegate) + { + foreach (var key in this.userCallbacks + .Where(x => x.Value == @delegate) + .Select(x => x.Key) + .ToArray()) + { + this.userCallbacks.Remove(key); + } + } + + /// + /// Rebuilds font texture. + /// + public void RebuildFontTexture() + { + foreach (var fontResourceView in this.fontTextures) + fontResourceView.Dispose(); + this.fontTextures.Clear(); + + this.CreateFontsTexture(); + } + + /// + public IDalamudTextureWrap CreateTexture2D( + ReadOnlySpan data, + RawImageSpecification specs, + bool cpuRead, + bool cpuWrite, + bool allowRenderTarget, + [CallerMemberName] string debugName = "") + { + if (cpuRead && cpuWrite) + throw new ArgumentException("cpuRead and cpuWrite cannot be set at the same time."); + + var cpuaf = default(D3D11_CPU_ACCESS_FLAG); + if (cpuRead) + cpuaf |= D3D11_CPU_ACCESS_FLAG.D3D11_CPU_ACCESS_READ; + if (cpuWrite) + cpuaf |= D3D11_CPU_ACCESS_FLAG.D3D11_CPU_ACCESS_WRITE; + + D3D11_USAGE usage; + if (cpuRead) + usage = D3D11_USAGE.D3D11_USAGE_STAGING; + else if (cpuWrite) + usage = D3D11_USAGE.D3D11_USAGE_DYNAMIC; + else + usage = D3D11_USAGE.D3D11_USAGE_DEFAULT; + + var texd = new D3D11_TEXTURE2D_DESC + { + Width = (uint)specs.Width, + Height = (uint)specs.Height, + MipLevels = 1, + ArraySize = 1, + Format = specs.Format, + SampleDesc = new(1, 0), + Usage = usage, + BindFlags = (uint)(D3D11_BIND_FLAG.D3D11_BIND_SHADER_RESOURCE | + (allowRenderTarget ? D3D11_BIND_FLAG.D3D11_BIND_RENDER_TARGET : 0)), + CPUAccessFlags = (uint)cpuaf, + MiscFlags = 0, + }; + using var texture = default(ComPtr); + if (data.IsEmpty) + { + Marshal.ThrowExceptionForHR(this.device.Get()->CreateTexture2D(&texd, null, texture.GetAddressOf())); + } + else + { + fixed (void* dataPtr = data) + { + var subrdata = new D3D11_SUBRESOURCE_DATA { pSysMem = dataPtr, SysMemPitch = (uint)specs.Pitch }; + Marshal.ThrowExceptionForHR( + this.device.Get()->CreateTexture2D(&texd, &subrdata, texture.GetAddressOf())); + } + } + + SetDebugName((ID3D11DeviceChild*)texture.Get(), $"Texture:{debugName}:Tex2D"); + + var viewd = new D3D11_SHADER_RESOURCE_VIEW_DESC + { + Format = texd.Format, + ViewDimension = D3D_SRV_DIMENSION.D3D11_SRV_DIMENSION_TEXTURE2D, + Texture2D = new() { MipLevels = texd.MipLevels }, + }; + using var view = default(ComPtr); + Marshal.ThrowExceptionForHR( + this.device.Get()->CreateShaderResourceView((ID3D11Resource*)texture.Get(), &viewd, view.GetAddressOf())); + + SetDebugName((ID3D11DeviceChild*)view.Get(), $"Texture:{debugName}:SRV"); + + return TextureWrap.TakeOwnership(new(texture.Get(), view, specs.Width, specs.Height)); + } + + /// + public IDalamudTextureWrap CreateTextureFromImGuiViewport( + ImGuiViewportTextureArgs args, + LocalPlugin? ownerPlugin, + string? debugName = null, + CancellationToken cancellationToken = default) + { + var t = new ViewportTextureWrap(this, args, debugName, ownerPlugin, cancellationToken); + try + { + t.Update(); + return t; + } + catch + { + t.Dispose(); + throw; + } + } + + /// + public IDalamudTextureWrap WrapFromTextureResource(nint handle) + { + var unk = new ComPtr((IUnknown*)handle); + using var tex2D = default(ComPtr); + using var srv = default(ComPtr); + using var rtv = default(ComPtr); + if (unk.As(&tex2D).SUCCEEDED) + { + var desc = new D3D11_SHADER_RESOURCE_VIEW_DESC( + tex2D, + D3D_SRV_DIMENSION.D3D11_SRV_DIMENSION_TEXTURE2D); + this.device.Get()->CreateShaderResourceView((ID3D11Resource*)tex2D.Get(), &desc, srv.GetAddressOf()) + .ThrowOnError(); + var texDesc = tex2D.Get()->GetDesc(); + return TextureWrap.TakeOwnership(new(tex2D, srv, (int)texDesc.Width, (int)texDesc.Height)); + } + + if (unk.As(&srv).SUCCEEDED) + { + using var res = default(ComPtr); + srv.Get()->GetResource(res.GetAddressOf()); + res.As(&tex2D).ThrowOnError(); + var texDesc = tex2D.Get()->GetDesc(); + return TextureWrap.TakeOwnership(new(tex2D, srv, (int)texDesc.Width, (int)texDesc.Height)); + } + + throw new NotSupportedException( + "Given object, assumed as IUnknown, failed to QueryInterface both as ID3D11Texture2D and ID3D11ShaderResourceView."); + } + + /// + public RawImageSpecification GetTextureSpecification(IDalamudTextureWrap texture) + { + var td = TextureData.AttachFromAddress(texture.ImGuiHandle); + + using var tex = default(ComPtr); + td.ShaderResourceView->GetResource((ID3D11Resource**)tex.GetAddressOf()); + + D3D11_TEXTURE2D_DESC desc; + tex.Get()->GetDesc(&desc); + + return new(desc); + } + + /// + public nint GetTextureResource(IDalamudTextureWrap texture) => + (nint)TextureData.AttachFromAddress(texture.ImGuiHandle).Resource; + + private static void SetDebugName(ID3D11DeviceChild* child, string name) + { + 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(); + } + + private void RenderDrawDataInternal( + ID3D11RenderTargetView* renderTargetView, + ImDrawDataPtr drawData, + bool clearRenderTarget) + { + // Avoid rendering when minimized + if (drawData.DisplaySize.X <= 0 || drawData.DisplaySize.Y <= 0) + return; + + using var oldState = new D3D11DeviceContextStateBackup(this.featureLevel, this.context.Get()); + + // Setup desired DX state + this.SetupRenderState(drawData); + + this.context.Get()->OMSetRenderTargets(1, &renderTargetView, null); + if (clearRenderTarget) + { + var color = default(Vector4); + this.context.Get()->ClearRenderTargetView(renderTargetView, (float*)&color); + } + + if (!drawData.Valid || drawData.CmdListsCount == 0) + return; + + var cmdLists = new Span(drawData.NativePtr->CmdLists, drawData.NativePtr->CmdListsCount); + + // Create and grow vertex/index buffers if needed + if (this.vertexBufferSize < drawData.TotalVtxCount) + this.vertexBuffer.Dispose(); + if (this.vertexBuffer.Get() is null) + { + this.vertexBufferSize = drawData.TotalVtxCount + 5000; + var desc = new D3D11_BUFFER_DESC( + (uint)(sizeof(ImDrawVert) * this.vertexBufferSize), + (uint)D3D11_BIND_FLAG.D3D11_BIND_VERTEX_BUFFER, + D3D11_USAGE.D3D11_USAGE_DYNAMIC, + (uint)D3D11_CPU_ACCESS_FLAG.D3D11_CPU_ACCESS_WRITE); + var buffer = default(ID3D11Buffer*); + this.device.Get()->CreateBuffer(&desc, null, &buffer).ThrowOnError(); + this.vertexBuffer.Attach(buffer); + } + + if (this.indexBufferSize < drawData.TotalIdxCount) + this.indexBuffer.Dispose(); + if (this.indexBuffer.Get() is null) + { + this.indexBufferSize = drawData.TotalIdxCount + 5000; + var desc = new D3D11_BUFFER_DESC( + (uint)(sizeof(ushort) * this.indexBufferSize), + (uint)D3D11_BIND_FLAG.D3D11_BIND_INDEX_BUFFER, + D3D11_USAGE.D3D11_USAGE_DYNAMIC, + (uint)D3D11_CPU_ACCESS_FLAG.D3D11_CPU_ACCESS_WRITE); + var buffer = default(ID3D11Buffer*); + this.device.Get()->CreateBuffer(&desc, null, &buffer).ThrowOnError(); + this.indexBuffer.Attach(buffer); + } + + // Upload vertex/index data into a single contiguous GPU buffer + try + { + var vertexData = default(D3D11_MAPPED_SUBRESOURCE); + var indexData = default(D3D11_MAPPED_SUBRESOURCE); + this.context.Get()->Map( + (ID3D11Resource*)this.vertexBuffer.Get(), + 0, + D3D11_MAP.D3D11_MAP_WRITE_DISCARD, + 0, + &vertexData).ThrowOnError(); + this.context.Get()->Map( + (ID3D11Resource*)this.indexBuffer.Get(), + 0, + D3D11_MAP.D3D11_MAP_WRITE_DISCARD, + 0, + &indexData).ThrowOnError(); + + var targetVertices = new Span(vertexData.pData, this.vertexBufferSize); + var targetIndices = new Span(indexData.pData, this.indexBufferSize); + foreach (ref var cmdList in cmdLists) + { + var vertices = new ImVectorWrapper(ref cmdList.NativePtr->VtxBuffer); + var indices = new ImVectorWrapper(ref cmdList.NativePtr->IdxBuffer); + + vertices.CopyTo(targetVertices); + indices.CopyTo(targetIndices); + + targetVertices = targetVertices[vertices.Length..]; + targetIndices = targetIndices[indices.Length..]; + } + } + finally + { + this.context.Get()->Unmap((ID3D11Resource*)this.vertexBuffer.Get(), 0); + this.context.Get()->Unmap((ID3D11Resource*)this.indexBuffer.Get(), 0); + } + + // Setup orthographic projection matrix into our constant buffer. + // Our visible imgui space lies from DisplayPos (LT) to DisplayPos+DisplaySize (RB). + // DisplayPos is (0,0) for single viewport apps. + try + { + var data = default(D3D11_MAPPED_SUBRESOURCE); + this.context.Get()->Map( + (ID3D11Resource*)this.vertexConstantBuffer.Get(), + 0, + D3D11_MAP.D3D11_MAP_WRITE_DISCARD, + 0, + &data).ThrowOnError(); + *(Matrix4x4*)data.pData = Matrix4x4.CreateOrthographicOffCenter( + drawData.DisplayPos.X, + drawData.DisplayPos.X + drawData.DisplaySize.X, + drawData.DisplayPos.Y + drawData.DisplaySize.Y, + drawData.DisplayPos.Y, + 1f, + 0f); + } + finally + { + this.context.Get()->Unmap((ID3D11Resource*)this.vertexConstantBuffer.Get(), 0); + } + + // Render command lists + // (Because we merged all buffers into a single one, we maintain our own offset into them) + var vertexOffset = 0; + var indexOffset = 0; + var clipOff = new Vector4(drawData.DisplayPos, drawData.DisplayPos.X, drawData.DisplayPos.Y); + foreach (ref var cmdList in cmdLists) + { + var cmds = new ImVectorWrapper(&cmdList.NativePtr->CmdBuffer); + foreach (ref var cmd in cmds.DataSpan) + { + var clipV4 = cmd.ClipRect - clipOff; + var clipRect = new RECT((int)clipV4.X, (int)clipV4.Y, (int)clipV4.Z, (int)clipV4.W); + + // Skip the draw if nothing would be visible + if (clipRect.left >= clipRect.right || clipRect.top >= clipRect.bottom) + continue; + + this.context.Get()->RSSetScissorRects(1, &clipRect); + + if (cmd.UserCallback == nint.Zero) + { + // Bind texture and draw + if (TextureData.AttachFromAddressOrNull(cmd.TextureId) is not { } textureData) + continue; + + (textureData.CustomPipeline ?? this.defaultPipeline!).BindTo(this.context); + + var srv = textureData.ShaderResourceView; + this.context.Get()->PSSetShaderResources(0, 1, &srv); + this.context.Get()->DrawIndexed( + cmd.ElemCount, + (uint)(cmd.IdxOffset + indexOffset), + (int)(cmd.VtxOffset + vertexOffset)); + } + else if (this.userCallbacks.TryGetValue(cmd.UserCallback, out var cb)) + { + // Use custom callback + cb(drawData, (ImDrawCmd*)Unsafe.AsPointer(ref cmd)); + } + } + + indexOffset += cmdList.IdxBuffer.Size; + vertexOffset += cmdList.VtxBuffer.Size; + } + } + + /// + /// Builds fonts as necessary, and uploads the built data onto the GPU.
+ /// No-op if it has already been done. + ///
+ private void CreateFontsTexture() + { + if (this.device.IsEmpty()) + throw new ObjectDisposedException(nameof(Dx11Renderer)); + + if (this.fontTextures.Any()) + return; + + var io = ImGui.GetIO(); + if (io.Fonts.Textures.Size == 0) + io.Fonts.Build(); + + for (int textureIndex = 0, textureCount = io.Fonts.Textures.Size; + textureIndex < textureCount; + textureIndex++) + { + // Build texture atlas + io.Fonts.GetTexDataAsRGBA32( + textureIndex, + out byte* fontPixels, + out var width, + out var height, + out var bytespp); + + var tex = this.CreateTexture2D( + new(fontPixels, width * height * bytespp), + new(width, height, DXGI_FORMAT.DXGI_FORMAT_R8G8B8A8_UNORM, width * bytespp), + false, + false, + false, + $"Font#{textureIndex}"); + io.Fonts.SetTexID(textureIndex, tex.ImGuiHandle); + this.fontTextures.Add(tex); + } + + io.Fonts.ClearTexData(); + } + + /// + /// Initializes the device context's render state to what we would use for rendering ImGui by default. + /// + /// The relevant ImGui draw data. + private void SetupRenderState(ImDrawDataPtr drawData) + { + var ctx = this.context.Get(); + ctx->IASetInputLayout(this.inputLayout); + var buffer = this.vertexBuffer.Get(); + var stride = (uint)sizeof(ImDrawVert); + var offset = 0u; + ctx->IASetVertexBuffers(0, 1, &buffer, &stride, &offset); + ctx->IASetIndexBuffer(this.indexBuffer, DXGI_FORMAT.DXGI_FORMAT_R16_UINT, 0); + ctx->IASetPrimitiveTopology(D3D_PRIMITIVE_TOPOLOGY.D3D11_PRIMITIVE_TOPOLOGY_TRIANGLELIST); + + var viewport = new D3D11_VIEWPORT(0, 0, drawData.DisplaySize.X, drawData.DisplaySize.Y); + ctx->RSSetState(this.rasterizerState); + ctx->RSSetViewports(1, &viewport); + + var blendColor = default(Vector4); + ctx->OMSetBlendState(this.blendState, (float*)&blendColor, 0xffffffff); + ctx->OMSetDepthStencilState(this.depthStencilState, 0); + + ctx->VSSetShader(this.vertexShader.Get(), null, 0); + buffer = this.vertexConstantBuffer.Get(); + ctx->VSSetConstantBuffers(0, 1, &buffer); + + // PS handled later + + ctx->GSSetShader(null, null, 0); + ctx->HSSetShader(null, null, 0); + ctx->DSSetShader(null, null, 0); + ctx->CSSetShader(null, null, 0); + } + + /// + /// Creates objects from the device as necessary.
+ /// No-op if objects already are built. + ///
+ private void EnsureDeviceObjects() + { + if (this.device.IsEmpty()) + throw new ObjectDisposedException(nameof(Dx11Renderer)); + + var assembly = Assembly.GetExecutingAssembly(); + + // Create the vertex shader + if (this.vertexShader.IsEmpty() || this.inputLayout.IsEmpty()) + { + using var stream = assembly.GetManifestResourceStream("imgui-vertex.hlsl.bytes")!; + var array = ArrayPool.Shared.Rent((int)stream.Length); + stream.ReadExactly(array, 0, (int)stream.Length); + fixed (byte* pArray = array) + fixed (ID3D11VertexShader** ppShader = &this.vertexShader.GetPinnableReference()) + fixed (ID3D11InputLayout** ppInputLayout = &this.inputLayout.GetPinnableReference()) + fixed (void* pszPosition = "POSITION"u8) + fixed (void* pszTexCoord = "TEXCOORD"u8) + fixed (void* pszColor = "COLOR"u8) + { + this.device.Get()->CreateVertexShader(pArray, (nuint)stream.Length, null, ppShader).ThrowOnError(); + + var ied = stackalloc D3D11_INPUT_ELEMENT_DESC[] + { + new() + { + SemanticName = (sbyte*)pszPosition, + Format = DXGI_FORMAT.DXGI_FORMAT_R32G32_FLOAT, + AlignedByteOffset = uint.MaxValue, + }, + new() + { + SemanticName = (sbyte*)pszTexCoord, + Format = DXGI_FORMAT.DXGI_FORMAT_R32G32_FLOAT, + AlignedByteOffset = uint.MaxValue, + }, + new() + { + SemanticName = (sbyte*)pszColor, + Format = DXGI_FORMAT.DXGI_FORMAT_R8G8B8A8_UNORM, + AlignedByteOffset = uint.MaxValue, + }, + }; + this.device.Get()->CreateInputLayout(ied, 3, pArray, (nuint)stream.Length, ppInputLayout) + .ThrowOnError(); + } + + ArrayPool.Shared.Return(array); + } + + // Create the constant buffer + if (this.vertexConstantBuffer.IsEmpty()) + { + var bufferDesc = new D3D11_BUFFER_DESC( + (uint)sizeof(Matrix4x4), + (uint)D3D11_BIND_FLAG.D3D11_BIND_CONSTANT_BUFFER, + D3D11_USAGE.D3D11_USAGE_DYNAMIC, + (uint)D3D11_CPU_ACCESS_FLAG.D3D11_CPU_ACCESS_WRITE); + fixed (ID3D11Buffer** ppBuffer = &this.vertexConstantBuffer.GetPinnableReference()) + this.device.Get()->CreateBuffer(&bufferDesc, null, ppBuffer).ThrowOnError(); + } + + // Create the default texture pipeline + if (this.defaultPipeline is null) + { + using var stream = assembly.GetManifestResourceStream("imgui-frag.hlsl.bytes")!; + var array = ArrayPool.Shared.Rent((int)stream.Length); + var psspan = array.AsSpan(0, (int)stream.Length); + stream.ReadExactly(psspan); + this.defaultPipeline = TexturePipeline.From(this.device, psspan); + ArrayPool.Shared.Return(array); + } + + // Create the blending setup + if (this.blendState.IsEmpty()) + { + var blendStateDesc = new D3D11_BLEND_DESC + { + RenderTarget = + { + e0 = + { + BlendEnable = true, + SrcBlend = D3D11_BLEND.D3D11_BLEND_SRC_ALPHA, + DestBlend = D3D11_BLEND.D3D11_BLEND_INV_SRC_ALPHA, + BlendOp = D3D11_BLEND_OP.D3D11_BLEND_OP_ADD, + SrcBlendAlpha = D3D11_BLEND.D3D11_BLEND_INV_DEST_ALPHA, + DestBlendAlpha = D3D11_BLEND.D3D11_BLEND_ONE, + BlendOpAlpha = D3D11_BLEND_OP.D3D11_BLEND_OP_ADD, + RenderTargetWriteMask = (byte)D3D11_COLOR_WRITE_ENABLE.D3D11_COLOR_WRITE_ENABLE_ALL, + }, + }, + }; + fixed (ID3D11BlendState** ppBlendState = &this.blendState.GetPinnableReference()) + this.device.Get()->CreateBlendState(&blendStateDesc, ppBlendState).ThrowOnError(); + } + + // Create the rasterizer state + if (this.rasterizerState.IsEmpty()) + { + var rasterizerDesc = new D3D11_RASTERIZER_DESC + { + FillMode = D3D11_FILL_MODE.D3D11_FILL_SOLID, + CullMode = D3D11_CULL_MODE.D3D11_CULL_NONE, + ScissorEnable = true, + DepthClipEnable = true, + }; + fixed (ID3D11RasterizerState** ppRasterizerState = &this.rasterizerState.GetPinnableReference()) + this.device.Get()->CreateRasterizerState(&rasterizerDesc, ppRasterizerState).ThrowOnError(); + } + + // Create the depth-stencil State + if (this.depthStencilState.IsEmpty()) + { + var dsDesc = new D3D11_DEPTH_STENCIL_DESC + { + DepthEnable = false, + DepthWriteMask = D3D11_DEPTH_WRITE_MASK.D3D11_DEPTH_WRITE_MASK_ALL, + DepthFunc = D3D11_COMPARISON_FUNC.D3D11_COMPARISON_ALWAYS, + StencilEnable = false, + StencilReadMask = byte.MaxValue, + StencilWriteMask = byte.MaxValue, + FrontFace = + { + StencilFailOp = D3D11_STENCIL_OP.D3D11_STENCIL_OP_KEEP, + StencilDepthFailOp = D3D11_STENCIL_OP.D3D11_STENCIL_OP_KEEP, + StencilPassOp = D3D11_STENCIL_OP.D3D11_STENCIL_OP_KEEP, + StencilFunc = D3D11_COMPARISON_FUNC.D3D11_COMPARISON_ALWAYS, + }, + BackFace = + { + StencilFailOp = D3D11_STENCIL_OP.D3D11_STENCIL_OP_KEEP, + StencilDepthFailOp = D3D11_STENCIL_OP.D3D11_STENCIL_OP_KEEP, + StencilPassOp = D3D11_STENCIL_OP.D3D11_STENCIL_OP_KEEP, + StencilFunc = D3D11_COMPARISON_FUNC.D3D11_COMPARISON_ALWAYS, + }, + }; + fixed (ID3D11DepthStencilState** ppDepthStencilState = &this.depthStencilState.GetPinnableReference()) + this.device.Get()->CreateDepthStencilState(&dsDesc, ppDepthStencilState).ThrowOnError(); + } + + this.CreateFontsTexture(); + } + + private void ReleaseUnmanagedResources() + { + if (this.releaseUnmanagedResourceCalled) + return; + this.releaseUnmanagedResourceCalled = true; + + this.mainViewport.Release(); + ImGui.GetPlatformIO().Viewports[0].RendererUserData = nint.Zero; + ImGui.DestroyPlatformWindows(); + + this.viewportHandler.Dispose(); + + var io = ImGui.GetIO(); + if (io.NativePtr->BackendRendererName == (void*)this.renderNamePtr) + io.NativePtr->BackendRendererName = null; + if (this.renderNamePtr != 0) + Marshal.FreeHGlobal(this.renderNamePtr); + + foreach (var fontResourceView in this.fontTextures) + fontResourceView.Dispose(); + + foreach (var i in Enumerable.Range(0, io.Fonts.Textures.Size)) + io.Fonts.SetTexID(i, nint.Zero); + + this.device.Reset(); + this.context.Reset(); + this.defaultPipeline?.Dispose(); + this.vertexShader.Reset(); + this.inputLayout.Reset(); + this.vertexConstantBuffer.Reset(); + this.blendState.Reset(); + this.rasterizerState.Reset(); + this.depthStencilState.Reset(); + this.vertexBuffer.Reset(); + this.indexBuffer.Reset(); + this.dcompDevice.Reset(); + } +} diff --git a/Dalamud/ImGuiScene/Implementations/Dx11Win32Scene.cs b/Dalamud/ImGuiScene/Implementations/Dx11Win32Scene.cs new file mode 100644 index 0000000000..1a069075fe --- /dev/null +++ b/Dalamud/ImGuiScene/Implementations/Dx11Win32Scene.cs @@ -0,0 +1,319 @@ +using System.Diagnostics.CodeAnalysis; +using System.Numerics; +using System.Runtime.CompilerServices; +using System.Threading; + +using Dalamud.ImGuiScene.Helpers; +using Dalamud.Interface.Textures; +using Dalamud.Interface.Textures.TextureWraps; +using Dalamud.Plugin.Internal.Types; +using Dalamud.Utility; + +using ImGuiNET; + +using ImGuizmoNET; + +using ImPlotNET; + +using TerraFX.Interop.DirectX; +using TerraFX.Interop.Windows; + +namespace Dalamud.ImGuiScene.Implementations; + +/// +/// 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 Dx11Win32Scene : IWin32Scene +{ + private readonly Dx11Renderer imguiRenderer; + private readonly Win32InputHandler imguiInput; + private readonly WicEasy wicEasy; + + 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 Dx11Win32Scene(IDXGISwapChain* swapChain) + { + this.wicEasy = new(); + 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. + /// + ~Dx11Win32Scene() => this.ReleaseUnmanagedResources(); + + /// + public event IImGuiScene.BuildUiDelegate? BuildUi; + + /// + public event IImGuiScene.NewInputFrameDelegate? NewInputFrame; + + /// + public event IImGuiScene.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; + } + + /// + /// 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(); + this.wicEasy.Dispose(); + 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 SupportsTextureFormat(int format) => + this.SupportsTextureFormat((DXGI_FORMAT)format); + + /// + public bool SupportsTextureFormatForRenderTarget(int format) => + this.SupportsTextureFormat( + (DXGI_FORMAT)format, + D3D11_FORMAT_SUPPORT.D3D11_FORMAT_SUPPORT_TEXTURE2D | + D3D11_FORMAT_SUPPORT.D3D11_FORMAT_SUPPORT_RENDER_TARGET); + + /// + public IDalamudTextureWrap CreateTexture2D( + ReadOnlySpan data, + RawImageSpecification specs, + bool cpuRead, + bool cpuWrite, + bool allowRenderTarget, + [CallerMemberName] string debugName = "") => + this.imguiRenderer.CreateTexture2D( + data, + specs, + cpuRead, + cpuWrite, + allowRenderTarget, + debugName); + + /// + public IDalamudTextureWrap CreateTextureFromImGuiViewport( + ImGuiViewportTextureArgs args, + LocalPlugin? ownerPlugin, + string? debugName = null, + CancellationToken cancellationToken = default) => + this.imguiRenderer.CreateTextureFromImGuiViewport(args, ownerPlugin, debugName, cancellationToken); + + /// + public IDalamudTextureWrap WrapFromTextureResource(nint handle) => + this.imguiRenderer.WrapFromTextureResource(handle); + + /// + public RawImageSpecification GetTextureSpecification(IDalamudTextureWrap texture) => + this.imguiRenderer.GetTextureSpecification(texture); + + /// + public IntPtr GetTextureResource(IDalamudTextureWrap texture) => this.imguiRenderer.GetTextureResource(texture); + + /// + public void DrawTextureToTexture( + IDalamudTextureWrap target, + Vector2 targetUv0, + Vector2 targetUv1, + IDalamudTextureWrap source, + Vector2 sourceUv0, + Vector2 sourceUv1, + bool copyAlphaOnly = false) => + this.imguiRenderer.DrawTextureToTexture( + target, + targetUv0, + targetUv1, + source, + sourceUv0, + sourceUv1, + copyAlphaOnly); + + /// + public ITexturePipelineWrap CreateTexturePipeline(ReadOnlySpan ps, in D3D11_SAMPLER_DESC samplerDesc) => + this.imguiRenderer.CreateTexturePipeline(ps, samplerDesc); + + /// + public void SetTexturePipeline(IDalamudTextureWrap textureHandle, ITexturePipelineWrap? pipelineHandle) => + this.imguiRenderer.SetTexturePipeline(textureHandle, pipelineHandle); + + /// + public ITexturePipelineWrap? GetTexturePipeline(IDalamudTextureWrap textureHandle) => + this.imguiRenderer.GetTexturePipeline(textureHandle); + + /// + public byte[] GetTextureData(IDalamudTextureWrap texture, out RawImageSpecification specification) => + this.imguiRenderer.GetTextureData(texture, out specification); + + /// + 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; + } + + /// + /// Determines whether the current D3D11 Device supports the given DXGI format. + /// + /// DXGI format to check. + /// Formats to test. All formats must be supported, if multiple are specified. + /// Whether it is supported. + public bool SupportsTextureFormat( + DXGI_FORMAT dxgiFormat, + D3D11_FORMAT_SUPPORT formatSupport = D3D11_FORMAT_SUPPORT.D3D11_FORMAT_SUPPORT_TEXTURE2D) + { + var flags = 0u; + if (this.Device->CheckFormatSupport(dxgiFormat, &flags).FAILED) + return false; + return (flags & (uint)formatSupport) == (uint)formatSupport; + } + + 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/ImGuiScene/Implementations/Dx12OnDx11Win32Scene.cs b/Dalamud/ImGuiScene/Implementations/Dx12OnDx11Win32Scene.cs new file mode 100644 index 0000000000..d9728e049c --- /dev/null +++ b/Dalamud/ImGuiScene/Implementations/Dx12OnDx11Win32Scene.cs @@ -0,0 +1,345 @@ +using System.Diagnostics.CodeAnalysis; +using System.Numerics; +using System.Runtime.CompilerServices; +using System.Threading; + +using Dalamud.ImGuiScene.Helpers; +using Dalamud.Interface.Textures; +using Dalamud.Interface.Textures.TextureWraps; +using Dalamud.Plugin.Internal.Types; +using Dalamud.Utility; + +using TerraFX.Interop.DirectX; +using TerraFX.Interop.Windows; + +namespace Dalamud.ImGuiScene.Implementations; + +/// +/// Renderer for testing DX12 implementation onto DX11 render target. +/// +[SuppressMessage( + "StyleCop.CSharp.LayoutRules", + "SA1519:Braces should not be omitted from multi-line child statement", + Justification = "Multiple fixed/using scopes")] +internal unsafe class Dx12OnDx11Win32Scene : IWin32Scene +{ + private readonly Dx12Win32Scene scene12; + private readonly ComPtr[] shaderResourceViewsD3D11; + private readonly D3D11RectangleDrawer drawsOneSquare; + private ComPtr swapChainPossiblyWrapped; + private ComPtr swapChain; + private ComPtr device11; + private ComPtr deviceContext; + private ComPtr rtv; + private ComPtr device12; + + private int targetWidth; + private int targetHeight; + + /// + /// Initializes a new instance of the class. + /// + /// The pointer to an instance of . + public Dx12OnDx11Win32Scene(IDXGISwapChain* swapChain) + { + try + { + this.swapChainPossiblyWrapped = new(swapChain); + this.swapChain = new(swapChain); + fixed (ComPtr* ppSwapChain = &this.swapChain) + ReShadePeeler.PeelSwapChain(ppSwapChain); + + fixed (Guid* guid = &IID.IID_ID3D11Device1) + fixed (ID3D11Device1** pp = &this.device11.GetPinnableReference()) + this.swapChain.Get()->GetDevice(guid, (void**)pp).ThrowOnError(); + + fixed (ID3D11DeviceContext** pp = &this.deviceContext.GetPinnableReference()) + this.device11.Get()->GetImmediateContext(pp); + + using var buffer = default(ComPtr); + fixed (Guid* guid = &IID.IID_ID3D11Resource) + this.swapChain.Get()->GetBuffer(0, guid, (void**)buffer.GetAddressOf()).ThrowOnError(); + + fixed (ID3D11RenderTargetView** pp = &this.rtv.GetPinnableReference()) + this.device11.Get()->CreateRenderTargetView(buffer.Get(), null, pp).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; + +#if DEBUG + fixed (Guid* piid = &IID.IID_ID3D12Debug) + { + using var debug = default(ComPtr); + DirectX.D3D12GetDebugInterface(piid, (void**)debug.GetAddressOf()); + debug.Get()->EnableDebugLayer(); + } +#endif + + fixed (Guid* piid = &IID.IID_ID3D12Device) + fixed (ID3D12Device** ppDevice = &this.device12.GetPinnableReference()) + { + DirectX.D3D12CreateDevice( + null, + D3D_FEATURE_LEVEL.D3D_FEATURE_LEVEL_11_0, + piid, + (void**)ppDevice).ThrowOnError(); + } + + this.drawsOneSquare = new(); + this.drawsOneSquare.Setup(this.device11.Get()); + this.scene12 = new(this.device12, this.WindowHandle, this.targetWidth, this.targetHeight); + this.shaderResourceViewsD3D11 = new ComPtr[this.scene12.Renderer.NumBackBuffers]; + } + catch + { + this.drawsOneSquare?.Dispose(); + this.ReleaseUnmanagedResources(); + throw; + } + } + + /// + /// Finalizes an instance of the class. + /// + ~Dx12OnDx11Win32Scene() => this.ReleaseUnmanagedResources(); + + /// + public event IImGuiScene.BuildUiDelegate? BuildUi + { + add => this.scene12.BuildUi += value; + remove => this.scene12.BuildUi -= value; + } + + /// + public event IImGuiScene.NewInputFrameDelegate? NewInputFrame + { + add => this.scene12.NewInputFrame += value; + remove => this.scene12.NewInputFrame -= value; + } + + /// + public event IImGuiScene.NewRenderFrameDelegate? NewRenderFrame + { + add => this.scene12.NewRenderFrame += value; + remove => this.scene12.NewRenderFrame -= value; + } + + /// + public bool UpdateCursor + { + get => this.scene12.UpdateCursor; + set => this.scene12.UpdateCursor = value; + } + + /// + public string? IniPath + { + get => this.scene12.IniPath; + set => this.scene12.IniPath = value; + } + + /// + /// Gets the pointer to an instance of . + /// + public IDXGISwapChain* SwapChain => this.swapChain; + + /// + /// Gets the pointer to an instance of . + /// + public ID3D11Device1* Device => this.device11; + + /// + /// Gets the pointer to an instance of , in . + /// + public nint DeviceHandle => (nint)this.device11.Get(); + + /// + /// Gets the window handle. + /// + public HWND WindowHandle { get; } + + /// + public void Dispose() + { + this.drawsOneSquare.Dispose(); + this.ReleaseUnmanagedResources(); + this.scene12.Dispose(); + GC.SuppressFinalize(this); + } + + /// + public nint? ProcessWndProcW(HWND hWnd, uint msg, WPARAM wParam, LPARAM lParam) => + this.scene12.ProcessWndProcW(hWnd, msg, wParam, lParam); + + /// + public void Render() + { + this.scene12.Render(); + + this.scene12.Renderer.GetCurrentMainViewportRenderTarget(out var resource, out var backBufferIndex); + ref var srv = ref this.shaderResourceViewsD3D11[backBufferIndex]; + if (srv.IsEmpty()) + { + using var sharedHandle = Win32Handle.CreateSharedHandle(this.device12, resource); + using var tex = default(ComPtr); + fixed (Guid* piid = &IID.IID_ID3D11Texture2D) + this.device11.Get()->OpenSharedResource1(sharedHandle, piid, (void**)tex.GetAddressOf()).ThrowOnError(); + + D3D11_TEXTURE2D_DESC desc; + tex.Get()->GetDesc(&desc); + + using var srvTemp = default(ComPtr); + var srvDesc = new D3D11_SHADER_RESOURCE_VIEW_DESC( + tex, + D3D_SRV_DIMENSION.D3D11_SRV_DIMENSION_TEXTURE2D); + this.device11.Get()->CreateShaderResourceView((ID3D11Resource*)tex.Get(), &srvDesc, srvTemp.GetAddressOf()) + .ThrowOnError(); + srv.Swap(&srvTemp); + } + + var prtv = this.rtv.Get(); + this.deviceContext.Get()->OMSetRenderTargets(1, &prtv, null); + this.drawsOneSquare.Draw( + this.deviceContext, + this.rtv.Get(), + Vector2.Zero, + Vector2.One, + srv, + Vector2.Zero, + Vector2.One, + false); + this.deviceContext.Get()->OMSetRenderTargets(0, null, null); + } + + /// + public void OnPreResize() + { + this.rtv.Reset(); + foreach (ref var s in this.shaderResourceViewsD3D11.AsSpan()) + s.Reset(); + this.scene12.OnPreResize(); + } + + /// + public void OnPostResize(int newWidth, int newHeight) + { + using var buffer = default(ComPtr); + fixed (Guid* guid = &IID.IID_ID3D11Resource) + this.SwapChain->GetBuffer(0, guid, (void**)buffer.ReleaseAndGetAddressOf()).ThrowOnError(); + + using var rtvTemp = default(ComPtr); + this.Device->CreateRenderTargetView(buffer.Get(), null, rtvTemp.GetAddressOf()).ThrowOnError(); + this.rtv.Swap(&rtvTemp); + + this.targetWidth = newWidth; + this.targetHeight = newHeight; + this.scene12.OnPostResize(newWidth, newHeight); + } + + /// + public void InvalidateFonts() => this.scene12.InvalidateFonts(); + + /// + public bool IsImGuiCursor(nint cursorHandle) => this.scene12.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; + } + + /// + public bool SupportsTextureFormat(int format) => this.scene12.SupportsTextureFormat(format); + + /// + public bool SupportsTextureFormatForRenderTarget(int format) => + this.scene12.SupportsTextureFormatForRenderTarget(format); + + /// + public IDalamudTextureWrap CreateTexture2D( + ReadOnlySpan data, + RawImageSpecification specs, + bool cpuRead, + bool cpuWrite, + bool allowRenderTarget, + [CallerMemberName] string debugName = "") => + this.scene12.CreateTexture2D( + data, + specs, + cpuRead, + cpuWrite, + allowRenderTarget, + debugName); + + /// + public IDalamudTextureWrap CreateTextureFromImGuiViewport( + ImGuiViewportTextureArgs args, + LocalPlugin? ownerPlugin, + string? debugName = null, + CancellationToken cancellationToken = default) => + this.scene12.CreateTextureFromImGuiViewport(args, ownerPlugin, debugName, cancellationToken); + + /// + public IDalamudTextureWrap WrapFromTextureResource(nint handle) => + this.scene12.WrapFromTextureResource(handle); + + /// + public RawImageSpecification GetTextureSpecification(IDalamudTextureWrap texture) => + this.scene12.GetTextureSpecification(texture); + + /// + public byte[] GetTextureData(IDalamudTextureWrap texture, out RawImageSpecification specification) => + this.scene12.GetTextureData(texture, out specification); + + /// + public IntPtr GetTextureResource(IDalamudTextureWrap texture) => this.scene12.GetTextureResource(texture); + + /// + public void DrawTextureToTexture( + IDalamudTextureWrap target, + Vector2 targetUv0, + Vector2 targetUv1, + IDalamudTextureWrap source, + Vector2 sourceUv0, + Vector2 sourceUv1, + bool copyAlphaOnly = false) => + this.scene12.DrawTextureToTexture(target, targetUv0, targetUv1, source, sourceUv0, sourceUv1, copyAlphaOnly); + + /// + public ITexturePipelineWrap CreateTexturePipeline( + ReadOnlySpan ps, + in D3D12_STATIC_SAMPLER_DESC samplerDesc, + [CallerMemberName] string debugName = "") + => this.scene12.CreateTexturePipeline(ps, samplerDesc, debugName); + + /// + public void SetTexturePipeline(IDalamudTextureWrap textureHandle, ITexturePipelineWrap? pipelineHandle) => + this.scene12.SetTexturePipeline(textureHandle, pipelineHandle); + + /// + public ITexturePipelineWrap? GetTexturePipeline(IDalamudTextureWrap textureHandle) => + this.scene12.GetTexturePipeline(textureHandle); + + private void ReleaseUnmanagedResources() + { + this.swapChain.Reset(); + this.device11.Reset(); + this.deviceContext.Reset(); + this.rtv.Reset(); + foreach (ref var s in this.shaderResourceViewsD3D11.AsSpan()) + s.Reset(); + this.device12.Reset(); + this.drawsOneSquare.Dispose(); + this.swapChainPossiblyWrapped.Dispose(); + } +} diff --git a/Dalamud/ImGuiScene/Implementations/Dx12Renderer.Heap.cs b/Dalamud/ImGuiScene/Implementations/Dx12Renderer.Heap.cs new file mode 100644 index 0000000000..ad08c2e440 --- /dev/null +++ b/Dalamud/ImGuiScene/Implementations/Dx12Renderer.Heap.cs @@ -0,0 +1,366 @@ +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.Runtime.CompilerServices; +using System.Threading; + +using Dalamud.ImGuiScene.Helpers; +using Dalamud.Utility; + +using TerraFX.Interop; +using TerraFX.Interop.DirectX; +using TerraFX.Interop.Windows; + +namespace Dalamud.ImGuiScene.Implementations; + +/// +/// Deals with rendering ImGui using DirectX 12. +/// See https://github.com/ocornut/imgui/blob/master/examples/imgui_impl_dx12.cpp for the original implementation. +/// +[SuppressMessage( + "StyleCop.CSharp.LayoutRules", + "SA1519:Braces should not be omitted from multi-line child statement", + Justification = "Multiple fixed/using scopes")] +internal unsafe partial class Dx12Renderer +{ + private sealed class AutoResourceHeap : IDisposable + { + private readonly List standardHeaps = new(); + private readonly List hugeHeaps = new(); + private readonly D3D12_HEAP_DESC heapDesc; + private readonly ulong minAlignment; + private readonly string heapDebugName; + private readonly long standardHeapRemovalGracePeriodTickCount; + private readonly long hugeHeapRemovalGracePeriodTickCount; + private int heapCounter; + private ComPtr device; + + public AutoResourceHeap( + ID3D12Device* device, + D3D12_HEAP_FLAGS heapFlags, + D3D12_HEAP_PROPERTIES heapProperties, + ulong standardHeapSize = 1024 * D3D12.D3D12_DEFAULT_RESOURCE_PLACEMENT_ALIGNMENT, + ulong heapAlignment = D3D12.D3D12_DEFAULT_RESOURCE_PLACEMENT_ALIGNMENT, + ulong minAllocationAlignment = D3D12.D3D12_SMALL_RESOURCE_PLACEMENT_ALIGNMENT, + long standardHeapRemovalGracePeriodTickCount = 5000, + long hugeHeapRemovalGracePeriodTickCount = 500, + [CallerMemberName] string debugName = "") + { + this.device = new(device); + this.minAlignment = minAllocationAlignment; + this.standardHeapRemovalGracePeriodTickCount = standardHeapRemovalGracePeriodTickCount; + this.hugeHeapRemovalGracePeriodTickCount = hugeHeapRemovalGracePeriodTickCount; + this.heapDebugName = debugName; + this.heapDesc = new() + { + SizeInBytes = standardHeapSize, + Properties = heapProperties, + Alignment = heapAlignment, + Flags = heapFlags, + }; + } + + ~AutoResourceHeap() => this.ReleaseUnmanagedResources(); + + public void Dispose() + { + this.ReleaseUnmanagedResources(); + GC.SuppressFinalize(this); + } + + public ComPtr CreateResource( + out D3D12_RESOURCE_DESC outDesc, + D3D12_RESOURCE_DESC desc, + D3D12_RESOURCE_STATES initialState = D3D12_RESOURCE_STATES.D3D12_RESOURCE_STATE_COMMON, + D3D12_CLEAR_VALUE* pOptimizedClearValue = null, + [CallerMemberName] string debugName = "") + { + desc.Alignment = D3D12.D3D12_SMALL_RESOURCE_PLACEMENT_ALIGNMENT; + var resAlloc = this.device.Get()->GetResourceAllocationInfo(0, 1, &desc); + if (resAlloc.SizeInBytes == ulong.MaxValue) + { + desc.Alignment = D3D12.D3D12_DEFAULT_RESOURCE_PLACEMENT_ALIGNMENT; + resAlloc = this.device.Get()->GetResourceAllocationInfo(0, 1, &desc); + if (resAlloc.SizeInBytes == ulong.MaxValue) + throw new InvalidOperationException($"{nameof(ID3D12Device.GetResourceAllocationInfo)}"); + } + + outDesc = desc; + + var useNormalHeap = resAlloc.SizeInBytes <= this.heapDesc.SizeInBytes; + var heaps = useNormalHeap ? this.standardHeaps : this.hugeHeaps; + + lock (heaps) + { + for (int i = 0, to = heaps.Count; i <= to; i++) + { + if (i == to) + { + var counter = Interlocked.Increment(ref this.heapCounter); + heaps.Add( + new( + this.device, + useNormalHeap + ? this.heapDesc + : this.heapDesc with { SizeInBytes = resAlloc.SizeInBytes }, + this.minAlignment, + useNormalHeap + ? $"{this.heapDebugName}#{counter}" + : $"{this.heapDebugName}#{counter}#Huge")); + } + + var r = heaps[i].CreatePlacedResource( + resAlloc, + desc, + initialState, + pOptimizedClearValue, + debugName); + if (!r.IsEmpty()) + return r; + } + } + + throw new OutOfMemoryException(); + } + + public void ClearEmptyHeaps() + { + for (var heapTypeIndex = 0; heapTypeIndex < 2; heapTypeIndex++) + { + var expireAt = Environment.TickCount64 - + (heapTypeIndex == 0 + ? this.standardHeapRemovalGracePeriodTickCount + : this.hugeHeapRemovalGracePeriodTickCount); + var heaps = heapTypeIndex == 0 ? this.standardHeaps : this.hugeHeaps; + lock (heaps) + { + for (var i = heaps.Count - 1; i >= 0; i--) + { + var heap = heaps[i]; + if (heap.RefCount > 1 || heap.LastNodeReleaseTickCount > expireAt) + continue; + + heap.Dispose(); + heaps.RemoveAt(i); + } + } + } + } + + private void ReleaseUnmanagedResources() + { + for (var heapTypeIndex = 0; heapTypeIndex < 2; heapTypeIndex++) + { + var heaps = heapTypeIndex == 0 ? this.standardHeaps : this.hugeHeaps; + lock (heaps) + { + foreach (var heap in heaps) + heap.Release(); + heaps.Clear(); + } + } + + this.device.Reset(); + } + + /// + /// Represents a heap. + /// + private class Heap : ManagedComObjectBase, INativeGuid + { + public static readonly Guid MyGuid = + new(0x2a5ba9e8, 0x22a5, 0x4b40, 0xb6, 0x88, 0xc2, 0x0f, 0xdf, 0x50, 0xbe, 0x8e); + + private const ulong OffsetShift = 0x100000000; + + private readonly string heapDebugName; + private readonly ulong minAlignment; + + private readonly Node head; + private readonly Node tail; + + private ComPtr device; + private ComPtr heap; + + public Heap( + ID3D12Device* device, + in D3D12_HEAP_DESC desc, + ulong minAlignment, + string debugName) + { + this.heapDebugName = debugName; + this.minAlignment = minAlignment; + this.LastNodeReleaseTickCount = Environment.TickCount64; + try + { + this.device = new(device); + + fixed (Guid* piid = &IID.IID_ID3D12Heap) + fixed (D3D12_HEAP_DESC* pDesc = &desc) + fixed (ID3D12Heap** ppHeap = &this.heap.GetPinnableReference()) + fixed (void* pName = $"{debugName} ({Util.FormatBytes((long)desc.SizeInBytes)})") + { + device->CreateHeap(pDesc, piid, (void**)ppHeap).ThrowOnError(); + this.heap.Get()->SetName((ushort*)pName).ThrowOnError(); + } + + this.head = new(null, 0, OffsetShift); + this.tail = new( + null, + OffsetShift + desc.SizeInBytes, + OffsetShift + desc.SizeInBytes); + this.head.InsertNext(this.tail); + } + catch + { + this.Release(); + throw; + } + } + + public static Guid* NativeGuid => (Guid*)Unsafe.AsPointer(ref Unsafe.AsRef(in MyGuid)); + + public object Lock { get; } = new(); + + public long LastNodeReleaseTickCount { get; set; } + + public ComPtr CreatePlacedResource( + in D3D12_RESOURCE_ALLOCATION_INFO resAlloc, + in D3D12_RESOURCE_DESC desc, + D3D12_RESOURCE_STATES initialState = D3D12_RESOURCE_STATES.D3D12_RESOURCE_STATE_COMMON, + D3D12_CLEAR_VALUE* pOptimizedClearValue = null, + [CallerMemberName] string debugName = "") + { + using var node = this.Allocate(resAlloc.Alignment, resAlloc.SizeInBytes); + if (node is null) + return default; + + using var texture = default(ComPtr); + fixed (Guid* piidResource = &IID.IID_ID3D12Resource) + fixed (Guid* piidHeap = &IID.IID_ID3D12Heap) + fixed (D3D12_RESOURCE_DESC* pDesc = &desc) + fixed (void* pNameResource = + $"{this.heapDebugName}[{debugName}] ({Util.FormatBytes((long)resAlloc.SizeInBytes)})") + { + this.device.Get()->CreatePlacedResource( + this.heap, + node.From - OffsetShift, + pDesc, + initialState, + pOptimizedClearValue, + piidResource, + (void**)texture.GetAddressOf()).ThrowOnError(); + + texture.Get()->SetName((ushort*)pNameResource).ThrowOnError(); + texture.Get()->SetPrivateDataInterface(Node.NativeGuid, node.AsIUnknown()).ThrowOnError(); + texture.Get()->SetPrivateDataInterface(piidHeap, (IUnknown*)this.heap.Get()).ThrowOnError(); + } + + return new(texture); + } + + protected override void* DynamicCast(in Guid iid) => + iid == MyGuid ? this.AsComInterface() : base.DynamicCast(iid); + + protected override void FinalRelease() + { + // Note: nodes between head and tail are owned by ID3D12Resource, with SetPrivateDataInterface. + this.head.Release(); + this.tail.Release(); + this.heap.Reset(); + this.device.Reset(); + } + + private Node? Allocate(ulong alignment, ulong length) + { + length = ((length + this.minAlignment) - 1) & ~(this.minAlignment - 1); + lock (this.Lock) + { + for (var n = this.head; n != this.tail; n = n.Next) + { + var from = n.To; + var to = n.Next.From; + var fromExpected = ((from + alignment) - 1) & ~(alignment - 1); + var toExpected = fromExpected + length; + if (toExpected > to) + continue; + + var o = new Node(this, fromExpected, toExpected); + try + { + n.InsertNext(o); + return o; + } + catch + { + o.Release(); + throw; + } + } + } + + return default; + } + } + + /// + /// Represents an allocated memory region in a . + /// + private class Node : ManagedComObjectBase, INativeGuid + { + public static readonly Guid NodeGuid = + new(0x2a5ba9e8, 0x22a5, 0x4b40, 0xb6, 0x88, 0xc2, 0x0f, 0xdf, 0x50, 0xbe, 0x8f); + + private readonly Heap? owner; + + public Node(Heap? owner, ulong from, ulong to) + { + this.owner = owner?.CloneRef(); + this.Prev = this; + this.Next = this; + this.From = from; + this.To = to; + } + + public static Guid* NativeGuid => (Guid*)Unsafe.AsPointer(ref Unsafe.AsRef(in NodeGuid)); + + public ulong From { get; } + + public ulong To { get; } + + public Node Prev { get; set; } + + public Node Next { get; set; } + + public void InsertNext(Node n) + { + n.Prev = this; + n.Next = this.Next; + this.Next.Prev = n; + this.Next = n; + if (n.Prev == this.Prev) + this.Prev = n; + } + + protected override void* DynamicCast(in Guid iid) => + iid == NodeGuid ? this.AsComInterface() : base.DynamicCast(iid); + + protected override void FinalRelease() + { + if (this.owner is null) + return; + + this.owner.LastNodeReleaseTickCount = Environment.TickCount64; + + lock (this.owner.Lock) + { + var prev = this.Prev; + var next = this.Next; + this.Next = next; + this.Prev = prev; + } + + this.owner.Dispose(); + } + } + } +} diff --git a/Dalamud/ImGuiScene/Implementations/Dx12Renderer.Queues.cs b/Dalamud/ImGuiScene/Implementations/Dx12Renderer.Queues.cs new file mode 100644 index 0000000000..2b52c7cfad --- /dev/null +++ b/Dalamud/ImGuiScene/Implementations/Dx12Renderer.Queues.cs @@ -0,0 +1,238 @@ +using System.Diagnostics.CodeAnalysis; +using System.Runtime.InteropServices; +using System.Threading; + +using Dalamud.ImGuiScene.Helpers; +using Dalamud.Utility; + +using TerraFX.Interop.DirectX; +using TerraFX.Interop.Windows; + +using Win32 = TerraFX.Interop.Windows.Windows; + +namespace Dalamud.ImGuiScene.Implementations; + +/// +/// Deals with rendering ImGui using DirectX 12. +/// See https://github.com/ocornut/imgui/blob/master/examples/imgui_impl_dx12.cpp for the original implementation. +/// +[SuppressMessage( + "StyleCop.CSharp.LayoutRules", + "SA1519:Braces should not be omitted from multi-line child statement", + Justification = "Multiple fixed/using scopes")] +internal unsafe partial class Dx12Renderer +{ + private sealed class GraphicsCommandListWrapper : IDisposable + { + private ComPtr allocator; + private ComPtr list; + + public GraphicsCommandListWrapper(ID3D12Device* device, D3D12_COMMAND_LIST_TYPE type, string debugName) + { + try + { + fixed (Guid* piid = &IID.IID_ID3D12CommandAllocator) + fixed (ID3D12CommandAllocator** pp = &this.allocator.GetPinnableReference()) + fixed (void* pName = $"{debugName}.{nameof(this.allocator)}") + { + device->CreateCommandAllocator(type, piid, (void**)pp).ThrowOnError(); + this.allocator.Get()->SetName((ushort*)pName).ThrowOnError(); + } + + fixed (Guid* piid = &IID.IID_ID3D12GraphicsCommandList) + fixed (ID3D12GraphicsCommandList** pp = &this.list.GetPinnableReference()) + fixed (void* pName = $"{debugName}.{nameof(this.list)}") + { + device->CreateCommandList(0, type, this.allocator, null, piid, (void**)pp).ThrowOnError(); + this.list.Get()->Close().ThrowOnError(); + this.list.Get()->SetName((ushort*)pName).ThrowOnError(); + } + } + catch + { + this.ReleaseUnmanagedResources(); + throw; + } + } + + ~GraphicsCommandListWrapper() => this.ReleaseUnmanagedResources(); + + public ID3D12GraphicsCommandList* CommandList => this.list.Get(); + + public CommandListCloser Record(out ID3D12GraphicsCommandList* commandList) + { + this.allocator.Get()->Reset().ThrowOnError(); + this.list.Get()->Reset(this.allocator, null).ThrowOnError(); + return new(commandList = this.list); + } + + public void Dispose() + { + this.ReleaseUnmanagedResources(); + GC.SuppressFinalize(this); + } + + private void ReleaseUnmanagedResources() + { + this.allocator.Reset(); + this.list.Reset(); + } + + public struct CommandListCloser : IDisposable + { + private ComPtr list; + + public CommandListCloser(ID3D12GraphicsCommandList* list) => this.list = new(list); + + public void Dispose() + { + if (!this.list.IsEmpty()) + this.list.Get()->Close(); + this.list.Reset(); + } + } + } + + private sealed class CommandQueueWrapper : IDisposable + { + private ComPtr queue; + private ComPtr fence; + private ulong fenceCounter; + + public CommandQueueWrapper(ID3D12Device* device, in D3D12_COMMAND_QUEUE_DESC desc, string debugName) + { + try + { + fixed (Guid* piid = &IID.IID_ID3D12CommandQueue) + fixed (ID3D12CommandQueue** pp = &this.queue.GetPinnableReference()) + fixed (D3D12_COMMAND_QUEUE_DESC* pDesc = &desc) + fixed (void* pName = $"{debugName}:{nameof(this.queue)}") + { + device->CreateCommandQueue(pDesc, piid, (void**)pp).ThrowOnError(); + this.queue.Get()->SetName((ushort*)pName).ThrowOnError(); + } + + fixed (Guid* piid = &IID.IID_ID3D12Fence) + fixed (ID3D12Fence** pp = &this.fence.GetPinnableReference()) + fixed (void* pName = $"{debugName}:{nameof(this.fence)}") + { + device->CreateFence(0, D3D12_FENCE_FLAGS.D3D12_FENCE_FLAG_NONE, piid, (void**)pp).ThrowOnError(); + this.fence.Get()->SetName((ushort*)pName).ThrowOnError(); + } + + this.fenceCounter = 0; + } + catch + { + this.ReleaseUnmanagedResources(); + throw; + } + } + + public CommandQueueWrapper(ID3D12Device* device, ID3D12CommandQueue* queue, string debugName) + { + try + { + this.queue = new(queue); + fixed (ComPtr* ppQueue = &this.queue) + ReShadePeeler.PeelD3D12CommandQueue(ppQueue); + fixed (void* pName = $"{debugName}:{nameof(this.queue)}") + this.queue.Get()->SetName((ushort*)pName).ThrowOnError(); + + fixed (Guid* piid = &IID.IID_ID3D12Fence) + fixed (ID3D12Fence** pp = &this.fence.GetPinnableReference()) + fixed (void* pName = $"{debugName}:{nameof(this.fence)}") + { + device->CreateFence(0, D3D12_FENCE_FLAGS.D3D12_FENCE_FLAG_NONE, piid, (void**)pp).ThrowOnError(); + this.fence.Get()->SetName((ushort*)pName).ThrowOnError(); + } + + this.fenceCounter = 0; + } + catch + { + this.ReleaseUnmanagedResources(); + throw; + } + } + + ~CommandQueueWrapper() => this.ReleaseUnmanagedResources(); + + public bool HasPendingWork => + !this.fence.IsEmpty() && this.fence.Get()->GetCompletedValue() != this.fenceCounter; + + public void Dispose() + { + this.ReleaseUnmanagedResources(); + GC.SuppressFinalize(this); + } + + public ulong Submit(GraphicsCommandListWrapper commandListWrapper) => + this.Submit(commandListWrapper.CommandList); + + public ulong Submit(T* command) + where T : unmanaged, ID3D12CommandList.Interface + { + var fenceValue = Interlocked.Increment(ref this.fenceCounter); + this.queue.Get()->ExecuteCommandLists(1, (ID3D12CommandList**)&command); + this.queue.Get()->Signal(this.fence, fenceValue).ThrowOnError(); + return fenceValue; + } + + public ulong Submit(T** commands, int count) + where T : unmanaged, ID3D12CommandList.Interface + { + if (count < 1) + return this.fenceCounter; + var fenceValue = Interlocked.Increment(ref this.fenceCounter); + this.queue.Get()->ExecuteCommandLists((uint)count, (ID3D12CommandList**)commands); + this.queue.Get()->Signal(this.fence, fenceValue).ThrowOnError(); + return fenceValue; + } + + public void Wait(HANDLE hEvent = default) => this.Wait(this.fenceCounter, hEvent); + + public void WaitMostRecent(HANDLE hEvent = default) + { + var fenceValue = Interlocked.Increment(ref this.fenceCounter); + this.queue.Get()->Signal(this.fence, fenceValue).ThrowOnError(); + this.Wait(fenceValue, hEvent); + } + + public void Wait(ulong fenceValue, HANDLE hEvent = default) + { + if (!this.HasPendingWork) + return; + + var closeHandleAfter = false; + if (hEvent == default) + { + hEvent = Win32.CreateEventW(null, true, false, null); + if (hEvent == default) + throw Marshal.GetExceptionForHR(Marshal.GetHRForLastWin32Error()) ?? new(); + closeHandleAfter = true; + } + + try + { + if (!Win32.ResetEvent(hEvent)) + throw Marshal.GetExceptionForHR(Marshal.GetHRForLastWin32Error()) ?? new(); + this.fence.Get()->SetEventOnCompletion(fenceValue, hEvent).ThrowOnError(); + if (Win32.WaitForSingleObject(hEvent, Win32.INFINITE) == WAIT.WAIT_FAILED) + throw Marshal.GetExceptionForHR(Marshal.GetHRForLastWin32Error()) ?? new(); + } + finally + { + if (closeHandleAfter) + Win32.CloseHandle(hEvent); + } + } + + private void ReleaseUnmanagedResources() + { + this.Wait(); + this.queue.Reset(); + this.fence.Reset(); + } + } +} diff --git a/Dalamud/ImGuiScene/Implementations/Dx12Renderer.TextureData.cs b/Dalamud/ImGuiScene/Implementations/Dx12Renderer.TextureData.cs new file mode 100644 index 0000000000..d2a8942d93 --- /dev/null +++ b/Dalamud/ImGuiScene/Implementations/Dx12Renderer.TextureData.cs @@ -0,0 +1,155 @@ +using System.Diagnostics.CodeAnalysis; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +using Dalamud.ImGuiScene.Helpers; +using Dalamud.Interface.Textures.TextureWraps; + +using TerraFX.Interop; +using TerraFX.Interop.DirectX; +using TerraFX.Interop.Windows; + +namespace Dalamud.ImGuiScene.Implementations; + +/// +/// Deals with rendering ImGui using DirectX 12. +/// See https://github.com/ocornut/imgui/blob/master/examples/imgui_impl_dx12.cpp for the original implementation. +/// +[SuppressMessage( + "StyleCop.CSharp.LayoutRules", + "SA1519:Braces should not be omitted from multi-line child statement", + Justification = "Multiple fixed/using scopes")] +internal unsafe partial class Dx12Renderer +{ + [Guid("f58175a6-37da-4daa-82fd-5993f6847643")] + private class TextureData : ManagedComObjectBase, INativeGuid + { + public static readonly Guid MyGuid = + new(0xf58175a6, 0x37da, 0x4daa, 0x82, 0xfd, 0x59, 0x93, 0xf6, 0x84, 0x76, 0x43); + + private ComPtr texture; + private ComPtr uploadBuffer; + private TexturePipeline? customPipeline; + + public TextureData( + DXGI_FORMAT format, + int width, + int height, + int uploadPitch, + ID3D12Resource* texture, + ID3D12Resource* uploadBuffer) + { + this.texture = new(texture); + this.uploadBuffer = new(uploadBuffer); + this.Format = format; + this.Width = width; + this.Height = height; + this.UploadPitch = uploadPitch; + } + + public static Guid* NativeGuid => (Guid*)Unsafe.AsPointer(ref Unsafe.AsRef(in MyGuid)); + + public DXGI_FORMAT Format { get; private init; } + + public int Width { get; private init; } + + public int Height { get; private init; } + + public int UploadPitch { get; private init; } + + public TexturePipeline? CustomPipeline + { + get => this.customPipeline; + set + { + if (value == this.customPipeline) + return; + this.customPipeline?.Dispose(); + this.customPipeline = value?.CloneRef(); + } + } + + public ID3D12Resource* Texture => this.texture; + + public void ClearUploadBuffer() => this.uploadBuffer.Reset(); + + public void WriteCopyCommand(ID3D12GraphicsCommandList* cmdList) + { + var srcLocation = new D3D12_TEXTURE_COPY_LOCATION + { + pResource = this.uploadBuffer, + Type = D3D12_TEXTURE_COPY_TYPE.D3D12_TEXTURE_COPY_TYPE_PLACED_FOOTPRINT, + PlacedFootprint = new() + { + Footprint = new() + { + Format = this.Format, + Width = (uint)this.Width, + Height = (uint)this.Height, + Depth = 1, + RowPitch = (uint)this.UploadPitch, + }, + }, + }; + + var dstLocation = new D3D12_TEXTURE_COPY_LOCATION + { + pResource = this.texture, + Type = D3D12_TEXTURE_COPY_TYPE.D3D12_TEXTURE_COPY_TYPE_SUBRESOURCE_INDEX, + SubresourceIndex = 0, + }; + + cmdList->CopyTextureRegion(&dstLocation, 0, 0, 0, &srcLocation, null); + } + + protected override void* DynamicCast(in Guid iid) => + iid == MyGuid ? this.AsComInterface() : base.DynamicCast(iid); + + protected override void FinalRelease() + { + this.texture.Reset(); + this.uploadBuffer.Reset(); + this.customPipeline?.Dispose(); + this.customPipeline = null; + } + } + + private class TextureWrap : IDalamudTextureWrap, ICloneable + { + private TextureData? data; + + private TextureWrap(TextureData data) => this.data = data; + + ~TextureWrap() => this.ReleaseUnmanagedResources(); + + public TextureData Data => this.data ?? throw new ObjectDisposedException(nameof(TextureWrap)); + + public bool IsDisposed => this.data is null; + + public nint ImGuiHandle => (nint)this.Data.AsComInterface(); + + public int Width => this.Data.Width; + + public int Height => this.Data.Height; + + public static TextureWrap TakeOwnership(TextureData data) => new(data); + + public static TextureWrap NewReference(TextureData data) => new(data.CloneRef()); + + public void Dispose() + { + this.ReleaseUnmanagedResources(); + GC.SuppressFinalize(this); + } + + public IDalamudTextureWrap Clone() => NewReference(this.Data); + + object ICloneable.Clone() => this.Clone(); + + private void ReleaseUnmanagedResources() + { + this.data?.Dispose(); + this.data = null; + } + } +} diff --git a/Dalamud/ImGuiScene/Implementations/Dx12Renderer.TextureManager.cs b/Dalamud/ImGuiScene/Implementations/Dx12Renderer.TextureManager.cs new file mode 100644 index 0000000000..b13255d7a2 --- /dev/null +++ b/Dalamud/ImGuiScene/Implementations/Dx12Renderer.TextureManager.cs @@ -0,0 +1,359 @@ +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.Runtime.InteropServices; + +using Dalamud.ImGuiScene.Helpers; +using Dalamud.Interface.Textures; +using Dalamud.Utility; + +using TerraFX.Interop.DirectX; +using TerraFX.Interop.Windows; + +namespace Dalamud.ImGuiScene.Implementations; + +/// +/// Deals with rendering ImGui using DirectX 12. +/// See https://github.com/ocornut/imgui/blob/master/examples/imgui_impl_dx12.cpp for the original implementation. +/// +[SuppressMessage( + "StyleCop.CSharp.LayoutRules", + "SA1519:Braces should not be omitted from multi-line child statement", + Justification = "Multiple fixed/using scopes")] +internal unsafe partial class Dx12Renderer +{ + private class TextureManager : IDisposable + { + private readonly FixedObjectPool> eventPool; + private readonly FixedObjectPool commandQueuePool; + private readonly FixedObjectPool commandListPool; + private readonly List.Returner> flushTempList; + + private readonly AutoResourceHeap uploadHeap; + private readonly AutoResourceHeap textureHeap; + + private readonly object uploadListLock = new(); + private List pending1 = new(); + private List pending2 = new(); + private List.Returner> activeQueues1; + private List.Returner> activeQueues2; + + private ComPtr device; + private bool disposed; + + public TextureManager( + ID3D12Device* device, + int queuePoolCapacity = 8, + ulong commonUploadHeapSize = 16 * 1048576, + ulong commonTextureHeapSize = 64 * 1048576) + { + if (queuePoolCapacity <= 0) + queuePoolCapacity = Environment.ProcessorCount; + + try + { + device->AddRef(); + this.device.Attach(device); + this.eventPool = new(_ => new(Win32Handle.CreateEvent()), queuePoolCapacity); + + this.uploadHeap = new( + device, + D3D12_HEAP_FLAGS.D3D12_HEAP_FLAG_CREATE_NOT_ZEROED | + D3D12_HEAP_FLAGS.D3D12_HEAP_FLAG_ALLOW_ONLY_BUFFERS, + new() { Type = D3D12_HEAP_TYPE.D3D12_HEAP_TYPE_UPLOAD }, + commonUploadHeapSize, + debugName: $"{nameof(TextureManager)}.{nameof(this.uploadHeap)}"); + + this.textureHeap = new( + device, + D3D12_HEAP_FLAGS.D3D12_HEAP_FLAG_CREATE_NOT_ZEROED | + D3D12_HEAP_FLAGS.D3D12_HEAP_FLAG_ALLOW_ONLY_NON_RT_DS_TEXTURES, + new() { Type = D3D12_HEAP_TYPE.D3D12_HEAP_TYPE_DEFAULT }, + commonTextureHeapSize, + debugName: $"{nameof(TextureManager)}.{nameof(this.textureHeap)}"); + + this.commandQueuePool = new( + i => new( + device, + new D3D12_COMMAND_QUEUE_DESC + { + Type = D3D12_COMMAND_LIST_TYPE.D3D12_COMMAND_LIST_TYPE_COPY, + Flags = D3D12_COMMAND_QUEUE_FLAGS.D3D12_COMMAND_QUEUE_FLAG_DISABLE_GPU_TIMEOUT, + }, + $"{nameof(TextureManager)}.{nameof(this.commandQueuePool)}[{i}]"), + queuePoolCapacity); + + this.commandListPool = new( + i => new( + device, + D3D12_COMMAND_LIST_TYPE.D3D12_COMMAND_LIST_TYPE_COPY, + $"{nameof(TextureManager)}.{nameof(this.commandListPool)}[{i}]"), + queuePoolCapacity); + + this.flushTempList = new(queuePoolCapacity); + this.activeQueues1 = new(queuePoolCapacity); + this.activeQueues2 = new(queuePoolCapacity); + } + catch + { + this.commandListPool?.Dispose(); + this.commandQueuePool?.Dispose(); + this.eventPool?.Dispose(); + this.uploadHeap?.Dispose(); + this.textureHeap?.Dispose(); + this.ReleaseUnmanagedResources(); + throw; + } + } + + ~TextureManager() => this.ReleaseUnmanagedResources(); + + public void Dispose() + { + if (this.disposed) + return; + this.disposed = true; + + this.FlushPendingTextureUploads(); + + lock (this.uploadListLock) + { + foreach (var x in this.activeQueues1) + { + x.O.Wait(); + x.Dispose(); + } + + foreach (ref var v in CollectionsMarshal.AsSpan(this.pending1)) + v.Dispose(); + + this.activeQueues1.Clear(); + this.pending1.Clear(); + } + + this.commandQueuePool.Dispose(); + this.commandListPool.Dispose(); + this.eventPool.Dispose(); + this.uploadHeap.Dispose(); + this.textureHeap.Dispose(); + this.ReleaseUnmanagedResources(); + GC.SuppressFinalize(this); + } + + public TextureWrap CreateTexture( + ReadOnlySpan data, + RawImageSpecification specs, + string debugName) + { + uint numRows; + int uploadPitch; + int uploadSize; + + // Create an empty texture of same specifications with the request + using var texture = default(ComPtr); + this.textureHeap.CreateResource( + out var resDesc, + new() + { + Dimension = D3D12_RESOURCE_DIMENSION.D3D12_RESOURCE_DIMENSION_TEXTURE2D, + Alignment = D3D12.D3D12_SMALL_RESOURCE_PLACEMENT_ALIGNMENT, + Width = (ulong)specs.Width, + Height = (uint)specs.Height, + DepthOrArraySize = 1, + MipLevels = 1, + Format = specs.Format, + SampleDesc = { Count = 1, Quality = 0 }, + Layout = D3D12_TEXTURE_LAYOUT.D3D12_TEXTURE_LAYOUT_UNKNOWN, + Flags = D3D12_RESOURCE_FLAGS.D3D12_RESOURCE_FLAG_NONE, + }, + debugName: debugName).Swap(&texture); + + ulong cbRow; + this.device.Get()->GetCopyableFootprints(&resDesc, 0, 1, 0, null, &numRows, &cbRow, null); + if (specs.Pitch == (int)cbRow) + { + uploadPitch = ((checked((int)cbRow) + D3D12.D3D12_TEXTURE_DATA_PITCH_ALIGNMENT) - 1) & + ~(D3D12.D3D12_TEXTURE_DATA_PITCH_ALIGNMENT - 1); + uploadSize = checked((int)(numRows * uploadPitch)); + } + else + { + throw new ArgumentException( + $"The provided pitch {specs.Pitch} does not match the calculated pitch of {cbRow}.", + nameof(specs)); + } + + // Upload texture to graphics system + using var uploadBuffer = default(ComPtr); + this.uploadHeap.CreateResource( + out resDesc, + new() + { + Dimension = D3D12_RESOURCE_DIMENSION.D3D12_RESOURCE_DIMENSION_BUFFER, + Alignment = 0, + Width = (ulong)uploadSize, + Height = 1, + DepthOrArraySize = 1, + MipLevels = 1, + Format = DXGI_FORMAT.DXGI_FORMAT_UNKNOWN, + SampleDesc = { Count = 1, Quality = 0 }, + Layout = D3D12_TEXTURE_LAYOUT.D3D12_TEXTURE_LAYOUT_ROW_MAJOR, + Flags = D3D12_RESOURCE_FLAGS.D3D12_RESOURCE_FLAG_NONE, + }, + D3D12_RESOURCE_STATES.D3D12_RESOURCE_STATE_GENERIC_READ, + debugName: debugName).Swap(&uploadBuffer); + + try + { + void* mapped; + var range = new D3D12_RANGE(0, (nuint)uploadSize); + uploadBuffer.Get()->Map(0, &range, &mapped).ThrowOnError(); + var source = data; + var target = new Span(mapped, uploadSize); + for (var y = 0; y < numRows; y++) + { + source[..specs.Pitch].CopyTo(target); + source = source[specs.Pitch..]; + target = target[uploadPitch..]; + } + } + finally + { + uploadBuffer.Get()->Unmap(0, null); + } + + using var texData = new TextureData( + specs.Format, + specs.Width, + specs.Height, + uploadPitch, + texture, + uploadBuffer); + + // deal with completed ones in the active queue, so that the following TryRent is more likely to succeed. + lock (this.uploadListLock) + { + this.activeQueues1.RemoveAll( + x => + { + if (x.O.HasPendingWork) + return false; + x.Dispose(); + return true; + }); + } + + if (this.commandListPool.TryRent(out var rentedList)) + { + FixedObjectPool.Returner rentedQueue = default; + var texDataPtrCopy = texData.CloneRef(); + var giveup = () => + { + texDataPtrCopy?.ClearUploadBuffer(); + texDataPtrCopy?.Dispose(); + texDataPtrCopy = null; + rentedList.Dispose(); + }; + try + { + rentedQueue = this.commandQueuePool.Rent(giveup); + using (rentedList.O.Record(out var cmdList)) + texDataPtrCopy.WriteCopyCommand(cmdList); + rentedQueue.O.Submit(rentedList.O); + } + catch + { + rentedQueue.Dispose(); + giveup.Invoke(); + throw; + } + + lock (this.uploadListLock) + this.activeQueues1.Add(rentedQueue); + } + else + { + lock (this.uploadListLock) + this.pending1.Add(texData.CloneRef()); + } + + return TextureWrap.NewReference(texData); + } + + public void FlushPendingTextureUploads() + { + // don't care about incoherent view of the internal size variable at this point; + // expectation is that the draw list is already complete, and the textures used + // are either already finished uploading, or in progress of doing so. + if (this.pending1.Count != 0) + { + lock (this.uploadListLock) + (this.pending2, this.pending1) = (this.pending1, this.pending2); + + using var queue = this.commandQueuePool.Rent( + () => + { + foreach (var x in this.flushTempList) + x.Dispose(); + this.flushTempList.Clear(); + }); + + var numIters = Math.Max(this.pending2.Count, this.commandListPool.Capacity); + this.flushTempList.Add(this.commandListPool.Rent()); + for (var i = 1; i < numIters && this.commandListPool.TryRent(out var r); i++) + this.flushTempList.Add(r); + + var commandLists = stackalloc ComPtr[this.flushTempList.Count]; + for (int i = 0, j = 0; i < this.flushTempList.Count; i++) + { + var listWrapper = this.flushTempList[i].O; + using (listWrapper.Record(out var cmdList)) + { + var jTo = (this.pending2.Count * (i + 1)) / this.flushTempList.Count; + for (; j < jTo; j++) + this.pending2[j].WriteCopyCommand(cmdList); + } + + commandLists[i] = listWrapper.CommandList; + } + + queue.O.Submit(commandLists->GetAddressOf(), this.flushTempList.Count); + using (var waiter = this.eventPool.Rent()) + queue.O.Wait(waiter.O.O); + + foreach (ref var v in CollectionsMarshal.AsSpan(this.pending2)) + { + v.ClearUploadBuffer(); + v.Dispose(); + } + + this.pending2.Clear(); + } + + // same reasoning with the above, but for uploads already in progress. + if (this.activeQueues1.Count != 0) + { + lock (this.uploadListLock) + (this.activeQueues2, this.activeQueues1) = (this.activeQueues1, this.activeQueues2); + + using (var waiter = this.eventPool.Rent()) + { + foreach (var x in this.activeQueues2) + { + x.O.Wait(waiter.O.O); + x.Dispose(); + } + } + + this.activeQueues2.Clear(); + } + + this.uploadHeap.ClearEmptyHeaps(); + this.textureHeap.ClearEmptyHeaps(); + } + + private void ReleaseUnmanagedResources() + { + this.device.Reset(); + } + } +} diff --git a/Dalamud/ImGuiScene/Implementations/Dx12Renderer.TexturePipeline.cs b/Dalamud/ImGuiScene/Implementations/Dx12Renderer.TexturePipeline.cs new file mode 100644 index 0000000000..d7ddf52f18 --- /dev/null +++ b/Dalamud/ImGuiScene/Implementations/Dx12Renderer.TexturePipeline.cs @@ -0,0 +1,317 @@ +using System.Diagnostics.CodeAnalysis; +using System.Numerics; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; +using System.Text; + +using Dalamud.ImGuiScene.Helpers; +using Dalamud.Utility; + +using TerraFX.Interop; +using TerraFX.Interop.DirectX; +using TerraFX.Interop.Windows; + +namespace Dalamud.ImGuiScene.Implementations; + +/// +/// Deals with rendering ImGui using DirectX 12. +/// See https://github.com/ocornut/imgui/blob/master/examples/imgui_impl_dx12.cpp for the original implementation. +/// +[SuppressMessage("Interoperability", "CA1416:Validate platform compatibility", Justification = "DX12")] +[SuppressMessage( + "StyleCop.CSharp.LayoutRules", + "SA1519:Braces should not be omitted from multi-line child statement", + Justification = "Multiple fixed/using scopes")] +internal unsafe partial class Dx12Renderer +{ + [Guid("a29f9ceb-f89f-4f35-bc1c-a5f7dc8af0ff")] + private sealed class TexturePipeline : ManagedComObjectBase, INativeGuid + { + public static readonly Guid MyGuid = + new(0xa29f9ceb, 0xf89f, 0x4f35, 0xbc, 0x1c, 0xa5, 0xf7, 0xdc, 0x8a, 0xf0, 0xff); + + private ComPtr rootSignature; + private ComPtr pipelineState; + + public TexturePipeline(ID3D12RootSignature* rootSignature, ID3D12PipelineState* pipelineState) + { + this.rootSignature = new(rootSignature); + this.pipelineState = new(pipelineState); + } + + public static Guid* NativeGuid => (Guid*)Unsafe.AsPointer(ref Unsafe.AsRef(in MyGuid)); + + public static TexturePipeline From( + ID3D12Device* device, + DXGI_FORMAT rtvFormat, + ReadOnlySpan vs, + ReadOnlySpan ps, + in D3D12_STATIC_SAMPLER_DESC samplerDesc, + string debugName) + { + var descRange = new D3D12_DESCRIPTOR_RANGE + { + RangeType = D3D12_DESCRIPTOR_RANGE_TYPE.D3D12_DESCRIPTOR_RANGE_TYPE_SRV, + NumDescriptors = 1, + BaseShaderRegister = 0, + RegisterSpace = 0, + OffsetInDescriptorsFromTableStart = 0, + }; + + var rootParams = stackalloc D3D12_ROOT_PARAMETER[] + { + new() + { + ParameterType = D3D12_ROOT_PARAMETER_TYPE.D3D12_ROOT_PARAMETER_TYPE_32BIT_CONSTANTS, + Constants = new() + { + ShaderRegister = 0, + RegisterSpace = 0, + Num32BitValues = (uint)(sizeof(Matrix4x4) / sizeof(float)), + }, + ShaderVisibility = D3D12_SHADER_VISIBILITY.D3D12_SHADER_VISIBILITY_VERTEX, + }, + new() + { + ParameterType = D3D12_ROOT_PARAMETER_TYPE.D3D12_ROOT_PARAMETER_TYPE_DESCRIPTOR_TABLE, + DescriptorTable = new() { NumDescriptorRanges = 1, pDescriptorRanges = &descRange }, + ShaderVisibility = D3D12_SHADER_VISIBILITY.D3D12_SHADER_VISIBILITY_PIXEL, + }, + }; + + using var rootSignature = default(ComPtr); + using (var successBlob = default(ComPtr)) + using (var errorBlob = default(ComPtr)) + { + fixed (D3D12_STATIC_SAMPLER_DESC* pSamplerDesc = &samplerDesc) + { + var signatureDesc = new D3D12_ROOT_SIGNATURE_DESC + { + NumParameters = 2, + pParameters = rootParams, + NumStaticSamplers = 1, + pStaticSamplers = pSamplerDesc, + Flags = + D3D12_ROOT_SIGNATURE_FLAGS.D3D12_ROOT_SIGNATURE_FLAG_ALLOW_INPUT_ASSEMBLER_INPUT_LAYOUT | + D3D12_ROOT_SIGNATURE_FLAGS.D3D12_ROOT_SIGNATURE_FLAG_DENY_HULL_SHADER_ROOT_ACCESS | + D3D12_ROOT_SIGNATURE_FLAGS.D3D12_ROOT_SIGNATURE_FLAG_DENY_DOMAIN_SHADER_ROOT_ACCESS | + D3D12_ROOT_SIGNATURE_FLAGS.D3D12_ROOT_SIGNATURE_FLAG_DENY_GEOMETRY_SHADER_ROOT_ACCESS, + }; + + var hr = DirectX.D3D12SerializeRootSignature( + &signatureDesc, + D3D_ROOT_SIGNATURE_VERSION.D3D_ROOT_SIGNATURE_VERSION_1, + successBlob.GetAddressOf(), + errorBlob.GetAddressOf()); + if (hr.FAILED) + { + var err = new Span( + errorBlob.Get()->GetBufferPointer(), + (int)errorBlob.Get()->GetBufferSize()); + throw new AggregateException(Encoding.UTF8.GetString(err), Marshal.GetExceptionForHR(hr)!); + } + } + + fixed (Guid* piid = &IID.IID_ID3D12RootSignature) + { + device->CreateRootSignature( + 0, + successBlob.Get()->GetBufferPointer(), + successBlob.Get()->GetBufferSize(), + piid, + (void**)rootSignature.GetAddressOf()).ThrowOnError(); + } + } + + fixed (void* pName = $"{debugName}:RootSignature") + rootSignature.Get()->SetName((ushort*)pName).ThrowOnError(); + + using var pipelineState = default(ComPtr); + fixed (void* pvs = vs) + fixed (void* pps = ps) + fixed (Guid* piidPipelineState = &IID.IID_ID3D12PipelineState) + fixed (void* pszPosition = "POSITION"u8) + fixed (void* pszTexCoord = "TEXCOORD"u8) + fixed (void* pszColor = "COLOR"u8) + { + var layout = stackalloc D3D12_INPUT_ELEMENT_DESC[] + { + new() + { + SemanticName = (sbyte*)pszPosition, + Format = DXGI_FORMAT.DXGI_FORMAT_R32G32_FLOAT, + AlignedByteOffset = uint.MaxValue, + InputSlotClass = D3D12_INPUT_CLASSIFICATION.D3D12_INPUT_CLASSIFICATION_PER_VERTEX_DATA, + }, + new() + { + SemanticName = (sbyte*)pszTexCoord, + Format = DXGI_FORMAT.DXGI_FORMAT_R32G32_FLOAT, + AlignedByteOffset = uint.MaxValue, + InputSlotClass = D3D12_INPUT_CLASSIFICATION.D3D12_INPUT_CLASSIFICATION_PER_VERTEX_DATA, + }, + new() + { + SemanticName = (sbyte*)pszColor, + Format = DXGI_FORMAT.DXGI_FORMAT_R8G8B8A8_UNORM, + AlignedByteOffset = uint.MaxValue, + InputSlotClass = D3D12_INPUT_CLASSIFICATION.D3D12_INPUT_CLASSIFICATION_PER_VERTEX_DATA, + }, + }; + var pipelineDesc = new D3D12_GRAPHICS_PIPELINE_STATE_DESC + { + NodeMask = 1, + PrimitiveTopologyType = D3D12_PRIMITIVE_TOPOLOGY_TYPE.D3D12_PRIMITIVE_TOPOLOGY_TYPE_TRIANGLE, + pRootSignature = rootSignature, + SampleMask = uint.MaxValue, + NumRenderTargets = 1, + RTVFormats = new() { e0 = rtvFormat }, + SampleDesc = new(1, 0), + Flags = D3D12_PIPELINE_STATE_FLAGS.D3D12_PIPELINE_STATE_FLAG_NONE, + VS = new(pvs, (nuint)vs.Length), + PS = new(pps, (nuint)ps.Length), + InputLayout = new() { pInputElementDescs = layout, NumElements = 3 }, + BlendState = new() + { + AlphaToCoverageEnable = false, + RenderTarget = new() + { + e0 = new() + { + BlendEnable = true, + SrcBlend = D3D12_BLEND.D3D12_BLEND_SRC_ALPHA, + DestBlend = D3D12_BLEND.D3D12_BLEND_INV_SRC_ALPHA, + BlendOp = D3D12_BLEND_OP.D3D12_BLEND_OP_ADD, + SrcBlendAlpha = D3D12_BLEND.D3D12_BLEND_INV_DEST_ALPHA, + DestBlendAlpha = D3D12_BLEND.D3D12_BLEND_ONE, + BlendOpAlpha = D3D12_BLEND_OP.D3D12_BLEND_OP_ADD, + RenderTargetWriteMask = (byte)D3D12_COLOR_WRITE_ENABLE.D3D12_COLOR_WRITE_ENABLE_ALL, + }, + }, + }, + RasterizerState = new() + { + FillMode = D3D12_FILL_MODE.D3D12_FILL_MODE_SOLID, + CullMode = D3D12_CULL_MODE.D3D12_CULL_MODE_NONE, + FrontCounterClockwise = false, + DepthBias = D3D12.D3D12_DEFAULT_DEPTH_BIAS, + DepthBiasClamp = D3D12.D3D12_DEFAULT_DEPTH_BIAS_CLAMP, + SlopeScaledDepthBias = D3D12.D3D12_DEFAULT_SLOPE_SCALED_DEPTH_BIAS, + DepthClipEnable = true, + MultisampleEnable = false, + AntialiasedLineEnable = false, + ForcedSampleCount = 0, + ConservativeRaster = + D3D12_CONSERVATIVE_RASTERIZATION_MODE.D3D12_CONSERVATIVE_RASTERIZATION_MODE_OFF, + }, + DepthStencilState = new() + { + DepthEnable = false, + DepthWriteMask = D3D12_DEPTH_WRITE_MASK.D3D12_DEPTH_WRITE_MASK_ALL, + DepthFunc = D3D12_COMPARISON_FUNC.D3D12_COMPARISON_FUNC_ALWAYS, + StencilEnable = false, + FrontFace = new() + { + StencilFailOp = D3D12_STENCIL_OP.D3D12_STENCIL_OP_KEEP, + StencilDepthFailOp = D3D12_STENCIL_OP.D3D12_STENCIL_OP_KEEP, + StencilPassOp = D3D12_STENCIL_OP.D3D12_STENCIL_OP_KEEP, + StencilFunc = D3D12_COMPARISON_FUNC.D3D12_COMPARISON_FUNC_ALWAYS, + }, + BackFace = new() + { + StencilFailOp = D3D12_STENCIL_OP.D3D12_STENCIL_OP_KEEP, + StencilDepthFailOp = D3D12_STENCIL_OP.D3D12_STENCIL_OP_KEEP, + StencilPassOp = D3D12_STENCIL_OP.D3D12_STENCIL_OP_KEEP, + StencilFunc = D3D12_COMPARISON_FUNC.D3D12_COMPARISON_FUNC_ALWAYS, + }, + }, + }; + device->CreateGraphicsPipelineState( + &pipelineDesc, + piidPipelineState, + (void**)pipelineState.GetAddressOf()).ThrowOnError(); + } + + fixed (void* pName = $"{debugName}:PipelineState") + pipelineState.Get()->SetName((ushort*)pName).ThrowOnError(); + + return new(rootSignature, pipelineState); + } + + public static TexturePipeline From( + ID3D12Device* device, + DXGI_FORMAT rtvFormat, + ReadOnlySpan vs, + ReadOnlySpan ps, + string debugName) => From( + device, + rtvFormat, + vs, + ps, + new() + { + Filter = D3D12_FILTER.D3D12_FILTER_MIN_MAG_MIP_LINEAR, + AddressU = D3D12_TEXTURE_ADDRESS_MODE.D3D12_TEXTURE_ADDRESS_MODE_WRAP, + AddressV = D3D12_TEXTURE_ADDRESS_MODE.D3D12_TEXTURE_ADDRESS_MODE_WRAP, + AddressW = D3D12_TEXTURE_ADDRESS_MODE.D3D12_TEXTURE_ADDRESS_MODE_WRAP, + MipLODBias = 0, + MaxAnisotropy = 0, + ComparisonFunc = D3D12_COMPARISON_FUNC.D3D12_COMPARISON_FUNC_ALWAYS, + BorderColor = D3D12_STATIC_BORDER_COLOR.D3D12_STATIC_BORDER_COLOR_TRANSPARENT_BLACK, + MinLOD = 0, + MaxLOD = 0, + ShaderRegister = 0, + RegisterSpace = 0, + ShaderVisibility = D3D12_SHADER_VISIBILITY.D3D12_SHADER_VISIBILITY_PIXEL, + }, + debugName); + + public void BindTo(ID3D12GraphicsCommandList* ctx) + { + ctx->SetPipelineState(this.pipelineState); + ctx->SetGraphicsRootSignature(this.rootSignature); + } + + protected override void* DynamicCast(in Guid iid) => + iid == MyGuid ? this.AsComInterface() : base.DynamicCast(iid); + + protected override void FinalRelease() + { + this.rootSignature.Reset(); + this.pipelineState.Reset(); + } + } + + private class TexturePipelineWrap : ITexturePipelineWrap + { + private TexturePipeline? data; + + private TexturePipelineWrap(TexturePipeline data) => this.data = data; + + ~TexturePipelineWrap() => this.ReleaseUnmanagedResources(); + + public TexturePipeline Data => this.data ?? throw new ObjectDisposedException(nameof(TextureWrap)); + + public bool IsDisposed => this.data is null; + + public static TexturePipelineWrap TakeOwnership(TexturePipeline data) => new(data); + + public static TexturePipelineWrap NewReference(TexturePipeline data) => new(data.CloneRef()); + + public void Dispose() + { + this.ReleaseUnmanagedResources(); + GC.SuppressFinalize(this); + } + + public ITexturePipelineWrap Clone() => NewReference(this.Data); + + object ICloneable.Clone() => this.Clone(); + + private void ReleaseUnmanagedResources() + { + this.data?.Dispose(); + this.data = null; + } + } +} diff --git a/Dalamud/ImGuiScene/Implementations/Dx12Renderer.ViewportHandler.cs b/Dalamud/ImGuiScene/Implementations/Dx12Renderer.ViewportHandler.cs new file mode 100644 index 0000000000..f704c9eeaf --- /dev/null +++ b/Dalamud/ImGuiScene/Implementations/Dx12Renderer.ViewportHandler.cs @@ -0,0 +1,899 @@ +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.Numerics; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +using Dalamud.ImGuiScene.Helpers; +using Dalamud.Interface.Utility; +using Dalamud.Utility; + +using ImGuiNET; + +using TerraFX.Interop; +using TerraFX.Interop.DirectX; +using TerraFX.Interop.Windows; + +using static TerraFX.Interop.Windows.Windows; + +namespace Dalamud.ImGuiScene.Implementations; + +/// +/// Deals with rendering ImGui using DirectX 12. +/// See https://github.com/ocornut/imgui/blob/master/examples/imgui_impl_dx12.cpp for the original implementation. +/// +[SuppressMessage( + "StyleCop.CSharp.LayoutRules", + "SA1519:Braces should not be omitted from multi-line child statement", + Justification = "Multiple fixed/using scopes")] +internal unsafe partial class Dx12Renderer +{ + private static readonly Vector4 ClearColor = default; + + /// + /// MULTI-VIEWPORT / PLATFORM INTERFACE SUPPORT + /// This is an _advanced_ and _optional_ feature, allowing the backend to create and handle multiple viewports simultaneously. + /// If you are new to dear imgui or creating a new binding for dear imgui, it is recommended that you completely ignore this section first.. + /// + private class ViewportHandler : IDisposable + { + private readonly Dx12Renderer renderer; + + [SuppressMessage("ReSharper", "NotAccessedField.Local", Justification = "Keeping reference alive")] + private readonly ImGuiViewportHelpers.CreateWindowDelegate cwd; + + public ViewportHandler(Dx12Renderer renderer) + { + this.renderer = renderer; + + var pio = ImGui.GetPlatformIO(); + pio.Renderer_CreateWindow = Marshal.GetFunctionPointerForDelegate(this.cwd = this.OnCreateWindow); + pio.Renderer_DestroyWindow = (nint)(delegate* unmanaged)&OnDestroyWindow; + pio.Renderer_SetWindowSize = (nint)(delegate* unmanaged)&OnSetWindowSize; + pio.Renderer_RenderWindow = (nint)(delegate* unmanaged)&OnRenderWindow; + pio.Renderer_SwapBuffers = (nint)(delegate* unmanaged)&OnSwapBuffers; + } + + ~ViewportHandler() => ReleaseUnmanagedResources(); + + public void Dispose() + { + ReleaseUnmanagedResources(); + GC.SuppressFinalize(this); + } + + private static void ReleaseUnmanagedResources() + { + var pio = ImGui.GetPlatformIO(); + pio.Renderer_CreateWindow = nint.Zero; + pio.Renderer_DestroyWindow = nint.Zero; + pio.Renderer_SetWindowSize = nint.Zero; + pio.Renderer_RenderWindow = nint.Zero; + pio.Renderer_SwapBuffers = nint.Zero; + } + + [UnmanagedCallersOnly] + private static void OnDestroyWindow(ImGuiViewportPtr viewport) + { + if (viewport.RendererUserData == nint.Zero) + return; + ViewportData.AttachFromAddress(viewport.RendererUserData).Release(); + viewport.RendererUserData = nint.Zero; + } + + [UnmanagedCallersOnly] + private static void OnSetWindowSize(ImGuiViewportPtr viewport, Vector2 size) => + ViewportData.AttachFromAddress(viewport.RendererUserData).ResizeBuffers((int)size.X, (int)size.Y, true); + + [UnmanagedCallersOnly] + private static void OnRenderWindow(ImGuiViewportPtr viewport, nint unused) => + ViewportData.AttachFromAddress(viewport.RendererUserData) + .Draw(viewport.DrawData, true); + + [UnmanagedCallersOnly] + private static void OnSwapBuffers(ImGuiViewportPtr viewport, nint v) + { + var data = ViewportData.AttachFromAddress(viewport.RendererUserData); + data.PresentIfSwapChainAvailable(); + data.WaitForPendingOperations(); + } + + private void OnCreateWindow(ImGuiViewportPtr viewport) + { + // PlatformHandleRaw should always be a HWND, whereas PlatformHandle might be a higher-level handle (e.g. GLFWWindow*, SDL_Window*). + // Some backend will leave PlatformHandleRaw NULL, in which case we assume PlatformHandle will contain the HWND. + var hWnd = viewport.PlatformHandleRaw; + if (hWnd == 0) + hWnd = viewport.PlatformHandle; + try + { + viewport.RendererUserData = + ViewportData.CreateDComposition(this.renderer, (HWND)hWnd, $"ID#{viewport.ID}").AsHandle(); + } + catch + { + viewport.RendererUserData = + ViewportData.Create(this.renderer, (HWND)hWnd, $"ID#{viewport.ID}").AsHandle(); + } + } + } + + /// + /// Helper structure we store in the void* RendererUserData field of each ImGuiViewport to easily retrieve our backend data. + /// Main viewport created by application will only use the Resources field. + /// Secondary viewports created by this backend will use all the fields (including Window fields.) + /// + private class ViewportData : ManagedComObjectBase, INativeGuid + { + public static readonly Guid MyGuid = + new(0xb4e47b38, 0x61fd, 0x4f4c, 0xb1, 0x49, 0x45, 0x47, 0x81, 0x19, 0x1c, 0x16); + + private readonly Dx12Renderer parent; + private readonly string debugName; + private readonly int numBackBuffers; + private readonly ViewportFrame?[]? frames; + private readonly CommandQueueWrapper? queue; + + private ComPtr rtvDescHeap; + private ComPtr swapChain; + private ComPtr dcompVisual; + private ComPtr dcompTarget; + + private int width; + private int height; + + private ViewportData( + Dx12Renderer parent, + IDXGISwapChain3* swapChain3, + CommandQueueWrapper queue, + int width, + int height, + int numBackBuffers, + string debugName, + IDCompositionVisual* dcompVisual, + IDCompositionTarget* dcompTarget) + { + try + { + // Set up basic information. + this.parent = parent; + this.width = width; + this.height = height; + this.numBackBuffers = numBackBuffers; + this.CurrentFrameIndex = this.numBackBuffers - 1; + this.debugName = debugName; + this.swapChain = swapChain3 != null ? new(swapChain3) : default; + this.queue = queue; + this.frames = new ViewportFrame[numBackBuffers]; + if (dcompVisual is not null) + this.dcompVisual = new(dcompVisual); + if (dcompTarget is not null) + this.dcompTarget = new(dcompTarget); + + // Create the descriptor heap to store the render targets. + fixed (Guid* piidDescHeap = &IID.IID_ID3D12DescriptorHeap) + fixed (void* pName = $"{nameof(ViewportData)}[{debugName}].{nameof(this.rtvDescHeap)}") + { + var desc = new D3D12_DESCRIPTOR_HEAP_DESC + { + Type = D3D12_DESCRIPTOR_HEAP_TYPE.D3D12_DESCRIPTOR_HEAP_TYPE_RTV, + NumDescriptors = (uint)numBackBuffers, + Flags = D3D12_DESCRIPTOR_HEAP_FLAGS.D3D12_DESCRIPTOR_HEAP_FLAG_NONE, + NodeMask = 1, + }; + + this.Device->CreateDescriptorHeap( + &desc, + piidDescHeap, + (void**)this.rtvDescHeap.GetAddressOf()).ThrowOnError(); + + this.rtvDescHeap.Get()->SetName((ushort*)pName).ThrowOnError(); + } + + // Create the frame buffers for each one of them. + var rtvDescriptorSize = this.Device->GetDescriptorHandleIncrementSize( + D3D12_DESCRIPTOR_HEAP_TYPE.D3D12_DESCRIPTOR_HEAP_TYPE_RTV); + var rtvHandle = this.rtvDescHeap.Get()->GetCPUDescriptorHandleForHeapStart(); + for (var i = 0; i < numBackBuffers; i++) + { + var frameDebugName = $"{this.debugName}.{nameof(this.frames)}[{i}]"; + this.frames[i] = new(this.Device, i, rtvHandle, frameDebugName); + rtvHandle.ptr += rtvDescriptorSize; + } + } + catch + { + this.Release(); + throw; + } + } + + public static Guid* NativeGuid => (Guid*)Unsafe.AsPointer(ref Unsafe.AsRef(in MyGuid)); + + public int CurrentFrameIndex { get; private set; } + + public ViewportFrame CurrentFrame => + this.frames?[this.CurrentFrameIndex] ?? throw new ObjectDisposedException(nameof(ViewportHandler)); + + public IDXGISwapChain3* SwapChain => this.swapChain; + + private ID3D12Device* Device => this.parent.device; + + private DXGI_FORMAT RtvFormat => this.parent.rtvFormat; + + public static ViewportData Create( + Dx12Renderer renderer, + IDXGISwapChain3* swapChain3, + ID3D12CommandQueue* commandQueue, + string debugName, + IDCompositionVisual* dcompVisual, + IDCompositionTarget* dcompTarget) + { + DXGI_SWAP_CHAIN_DESC desc; + swapChain3->GetDesc(&desc).ThrowOnError(); + return new( + renderer, + swapChain3, + new(renderer.device, commandQueue, $"{debugName}.{nameof(queue)}"), + (int)desc.BufferDesc.Width, + (int)desc.BufferDesc.Height, + (int)desc.BufferCount, + debugName, + dcompVisual, + dcompTarget); + } + + public static ViewportData CreateDComposition(Dx12Renderer renderer, HWND hWnd, string debugName) + { + if (renderer.dcompDevice.IsEmpty()) + throw new NotSupportedException(); + + // Create command queue. + using var queue = default(ComPtr); + fixed (Guid* piid = &IID.IID_ID3D12CommandQueue) + { + var queueDesc = new D3D12_COMMAND_QUEUE_DESC + { + Flags = D3D12_COMMAND_QUEUE_FLAGS.D3D12_COMMAND_QUEUE_FLAG_NONE, + Type = D3D12_COMMAND_LIST_TYPE.D3D12_COMMAND_LIST_TYPE_DIRECT, + }; + + renderer.device.Get()->CreateCommandQueue(&queueDesc, piid, (void**)queue.GetAddressOf()).ThrowOnError(); + } + + using var dxgiFactory = default(ComPtr); + fixed (Guid* piidFactory = &IID.IID_IDXGIFactory4) + { +#if DEBUG + DirectX.CreateDXGIFactory2( + DXGI.DXGI_CREATE_FACTORY_DEBUG, + piidFactory, + (void**)dxgiFactory.GetAddressOf()).ThrowOnError(); +#else + DirectX.CreateDXGIFactory1(piidFactory, (void**)dxgiFactory.GetAddressOf()).ThrowOnError(); +#endif + } + + RECT rc; + if (!GetWindowRect(hWnd, &rc) || rc.right == rc.left || rc.bottom == rc.top) + rc = new(0, 0, 4, 4); + + using var swapChain1 = default(ComPtr); + var sd1 = new DXGI_SWAP_CHAIN_DESC1 + { + Width = (uint)(rc.right - rc.left), + Height = (uint)(rc.bottom - rc.top), + Format = renderer.rtvFormat, + Stereo = false, + SampleDesc = new(1, 0), + BufferUsage = DXGI.DXGI_USAGE_RENDER_TARGET_OUTPUT, + BufferCount = (uint)renderer.NumBackBuffers, + Scaling = DXGI_SCALING.DXGI_SCALING_STRETCH, + SwapEffect = DXGI_SWAP_EFFECT.DXGI_SWAP_EFFECT_FLIP_SEQUENTIAL, + AlphaMode = DXGI_ALPHA_MODE.DXGI_ALPHA_MODE_PREMULTIPLIED, + Flags = 0, + }; + dxgiFactory.Get()->CreateSwapChainForComposition( + (IUnknown*)queue.Get(), + &sd1, + null, + swapChain1.GetAddressOf()).ThrowOnError(); + + if (ReShadePeeler.PeelSwapChain(&swapChain1)) + { + swapChain1.Get()->ResizeBuffers(sd1.BufferCount, sd1.Width, sd1.Height, sd1.Format, sd1.Flags) + .ThrowOnError(); + } + + using var dcTarget = default(ComPtr); + renderer.dcompDevice.Get()->CreateTargetForHwnd(hWnd, BOOL.TRUE, dcTarget.GetAddressOf()); + + using var dcVisual = default(ComPtr); + renderer.dcompDevice.Get()->CreateVisual(dcVisual.GetAddressOf()).ThrowOnError(); + + dcVisual.Get()->SetContent((IUnknown*)swapChain1.Get()).ThrowOnError(); + dcTarget.Get()->SetRoot(dcVisual).ThrowOnError(); + renderer.dcompDevice.Get()->Commit().ThrowOnError(); + + using var swapChain3 = default(ComPtr); + swapChain1.As(&swapChain3).ThrowOnError(); + return Create(renderer, swapChain3, queue, debugName, dcVisual, dcTarget); + } + + public static ViewportData Create( + Dx12Renderer renderer, + HWND hWnd, + string debugName) + { + // Create command queue. + using var queue = default(ComPtr); + fixed (Guid* piid = &IID.IID_ID3D12CommandQueue) + { + var queueDesc = new D3D12_COMMAND_QUEUE_DESC + { + Flags = D3D12_COMMAND_QUEUE_FLAGS.D3D12_COMMAND_QUEUE_FLAG_NONE, + Type = D3D12_COMMAND_LIST_TYPE.D3D12_COMMAND_LIST_TYPE_DIRECT, + }; + + renderer.device.Get()->CreateCommandQueue(&queueDesc, piid, (void**)queue.GetAddressOf()).ThrowOnError(); + } + + // Create swap chain. + using var swapChain3 = default(ComPtr); + fixed (Guid* piidFactory2 = &IID.IID_IDXGIFactory2) + fixed (Guid* piidSwapChain3 = &IID.IID_IDXGISwapChain3) + { + var sd1 = new DXGI_SWAP_CHAIN_DESC1 + { + Width = 0, + Height = 0, + Format = renderer.rtvFormat, + Stereo = false, + SampleDesc = new(1, 0), + BufferUsage = DXGI.DXGI_USAGE_RENDER_TARGET_OUTPUT, + BufferCount = (uint)renderer.NumBackBuffers, + Scaling = DXGI_SCALING.DXGI_SCALING_STRETCH, + SwapEffect = DXGI_SWAP_EFFECT.DXGI_SWAP_EFFECT_FLIP_DISCARD, + AlphaMode = DXGI_ALPHA_MODE.DXGI_ALPHA_MODE_UNSPECIFIED, + Flags = 0, + }; + + using var dxgiFactory2 = default(ComPtr); +#if DEBUG + DirectX.CreateDXGIFactory2( + DXGI.DXGI_CREATE_FACTORY_DEBUG, + piidFactory2, + (void**)dxgiFactory2.GetAddressOf()).ThrowOnError(); +#else + DirectX.CreateDXGIFactory1(piidFactory2, (void**)dxgiFactory2.GetAddressOf()).ThrowOnError(); +#endif + + using var swapChainTmp = default(ComPtr); + dxgiFactory2.Get()->CreateSwapChainForHwnd( + (IUnknown*)queue.Get(), + hWnd, + &sd1, + null, + null, + swapChainTmp.GetAddressOf()).ThrowOnError(); + + if (ReShadePeeler.PeelSwapChain(&swapChainTmp)) + { + swapChainTmp.Get()->ResizeBuffers(sd1.BufferCount, sd1.Width, sd1.Height, sd1.Format, sd1.Flags) + .ThrowOnError(); + } + + swapChainTmp.Get()->QueryInterface(piidSwapChain3, (void**)swapChain3.GetAddressOf()).ThrowOnError(); + } + + return Create(renderer, swapChain3, queue, debugName, null, null); + } + + public static ViewportData Create( + Dx12Renderer renderer, + int width, + int height, + string debugName) => + new( + renderer, + null, + new( + renderer.device, + new D3D12_COMMAND_QUEUE_DESC + { + Flags = D3D12_COMMAND_QUEUE_FLAGS.D3D12_COMMAND_QUEUE_FLAG_NONE, + Type = D3D12_COMMAND_LIST_TYPE.D3D12_COMMAND_LIST_TYPE_DIRECT, + }, + $"{debugName}.{nameof(queue)}"), + width, + height, + renderer.NumBackBuffers, + debugName, + null, + null); + + public void Draw(ImDrawDataPtr drawData, bool clearRenderTarget) + { + if (this.width < 1 || this.height < 1) + return; + + ObjectDisposedException.ThrowIf(this.frames is null || this.queue is null, nameof(ViewportHandler)); + + this.EnsureRenderTarget(); + + this.CurrentFrameIndex = + this.swapChain.IsEmpty() + ? (this.CurrentFrameIndex + 1) % this.numBackBuffers + : (int)this.swapChain.Get()->GetCurrentBackBufferIndex(); + var currentFrame = this.CurrentFrame; + + // Draw + using (currentFrame.Command.Record(out var cmdList)) + { + currentFrame.BeginRenderTarget(cmdList, clearRenderTarget); + this.parent.RenderDrawDataInternal(currentFrame, drawData, cmdList); + currentFrame.EndRenderTarget(cmdList); + } + + this.queue.Wait(); + this.queue.Submit(currentFrame.Command); + } + + public void PresentIfSwapChainAvailable() + { + if (this.width < 1 || this.height < 1) + return; + + if (!this.swapChain.IsEmpty()) + this.swapChain.Get()->Present(0, 0).ThrowOnError(); + } + + public void WaitForPendingOperations() + { + if (this.frames is null || this.queue is null) + return; + + this.queue.WaitMostRecent(); + } + + public void ResetBuffers() + { + ObjectDisposedException.ThrowIf(this.frames is null || this.queue is null, nameof(ViewportHandler)); + + this.WaitForPendingOperations(); + foreach (var f in this.frames) + f?.ResetRenderTarget(); + } + + public void ResizeBuffers(int newWidth, int newHeight, bool resizeSwapChain) + { + ObjectDisposedException.ThrowIf(this.frames is null, this); + + this.ResetBuffers(); + + this.width = newWidth; + this.height = newHeight; + if (this.width < 1 || this.height < 1) + return; + + if (!this.swapChain.IsEmpty() && resizeSwapChain) + { + DXGI_SWAP_CHAIN_DESC1 desc; + this.swapChain.Get()->GetDesc1(&desc).ThrowOnError(); + this.swapChain.Get()->ResizeBuffers( + desc.BufferCount, + (uint)newWidth, + (uint)newHeight, + DXGI_FORMAT.DXGI_FORMAT_UNKNOWN, + desc.Flags).ThrowOnError(); + } + } + + protected override void FinalRelease() + { + this.WaitForPendingOperations(); + + this.queue?.Dispose(); + foreach (var f in this.frames ?? Array.Empty()) + f?.Dispose(); + + this.rtvDescHeap.Reset(); + this.dcompVisual.Reset(); + this.dcompTarget.Reset(); + this.swapChain.Reset(); + } + + private void EnsureRenderTarget() + { + ObjectDisposedException.ThrowIf(this.frames is null, this); + + for (var i = 0; i < this.numBackBuffers; i++) + { + var frame = this.frames[i]; + ObjectDisposedException.ThrowIf(frame is null, this); + frame.EnsureRenderTarget(this.Device, this.swapChain, this.RtvFormat, this.width, this.height); + } + } + } + + private class ViewportFrame : IDisposable + { + private const int HeapDefaultCapacity = 1024; + + private readonly string debugName; + private readonly int bufferIndex; + private readonly D3D12_CPU_DESCRIPTOR_HANDLE renderTargetCpuDescriptor; + private readonly List> deferredReleasingHeaps = new(); + + // Buffers used for secondary viewports created by the multi-viewports systems. + private ComPtr renderTarget; + + // Buffers used during the rendering of a frame. + private ComPtr indexBuffer; + private ComPtr vertexBuffer; + private uint indexBufferSize; + private uint vertexBufferSize; + + private ComPtr heap; + private int heapLength; + private int heapCapacity; + + public ViewportFrame( + ID3D12Device* device, + int bufferIndex, + D3D12_CPU_DESCRIPTOR_HANDLE renderTargetCpuDescriptor, + string debugName) + { + this.debugName = debugName; + this.bufferIndex = bufferIndex; + this.renderTargetCpuDescriptor = renderTargetCpuDescriptor; + this.Command = new( + device, + D3D12_COMMAND_LIST_TYPE.D3D12_COMMAND_LIST_TYPE_DIRECT, + $"{debugName}.{nameof(this.Command)}"); + } + + ~ViewportFrame() => this.ReleaseUnmanagedResources(); + + public GraphicsCommandListWrapper Command { get; } + + public ID3D12Resource* RenderTarget => this.renderTarget.Get(); + + public ID3D12Resource* VertexBuffer => this.vertexBuffer; + + public int VertexBufferSize => (int)this.vertexBufferSize; + + public ID3D12Resource* IndexBuffer => this.indexBuffer; + + public int IndexBufferSize => (int)this.indexBufferSize; + + public void Dispose() + { + this.Command.Dispose(); + this.ReleaseUnmanagedResources(); + GC.SuppressFinalize(this); + } + + public void ResetRenderTarget() => this.renderTarget.Reset(); + + public void EnsureRenderTarget( + ID3D12Device* device, + IDXGISwapChain3* swapChain, + DXGI_FORMAT rtvFormat, + int width, + int height) + { + if (!this.renderTarget.IsEmpty()) + return; + + fixed (Guid* piidResource = &IID.IID_ID3D12Resource) + { + using var backBuffer = default(ComPtr); + if (swapChain is null) + { + var props = new D3D12_HEAP_PROPERTIES + { + Type = D3D12_HEAP_TYPE.D3D12_HEAP_TYPE_DEFAULT, + CPUPageProperty = D3D12_CPU_PAGE_PROPERTY.D3D12_CPU_PAGE_PROPERTY_UNKNOWN, + MemoryPoolPreference = D3D12_MEMORY_POOL.D3D12_MEMORY_POOL_UNKNOWN, + }; + var desc = new D3D12_RESOURCE_DESC + { + Dimension = D3D12_RESOURCE_DIMENSION.D3D12_RESOURCE_DIMENSION_TEXTURE2D, + Alignment = 0, + Width = (ulong)width, + Height = (uint)height, + DepthOrArraySize = 1, + MipLevels = 1, + Format = rtvFormat, + SampleDesc = { Count = 1, Quality = 0 }, + Layout = D3D12_TEXTURE_LAYOUT.D3D12_TEXTURE_LAYOUT_UNKNOWN, + Flags = D3D12_RESOURCE_FLAGS.D3D12_RESOURCE_FLAG_ALLOW_RENDER_TARGET | + D3D12_RESOURCE_FLAGS.D3D12_RESOURCE_FLAG_ALLOW_SIMULTANEOUS_ACCESS, + }; + var clearColor = ClearColor; + var clearValue = new D3D12_CLEAR_VALUE(rtvFormat, (float*)&clearColor); + device->CreateCommittedResource( + &props, + D3D12_HEAP_FLAGS.D3D12_HEAP_FLAG_SHARED, + &desc, + D3D12_RESOURCE_STATES.D3D12_RESOURCE_STATE_COMMON, + &clearValue, + piidResource, + (void**)backBuffer.GetAddressOf()).ThrowOnError(); + } + else + { + swapChain->GetBuffer( + (uint)this.bufferIndex, + piidResource, + (void**)backBuffer.GetAddressOf()).ThrowOnError(); + } + + fixed (void* pName = $"{this.debugName}.{nameof(this.renderTarget)}") + backBuffer.Get()->SetName((ushort*)pName).ThrowOnError(); + + device->CreateRenderTargetView(backBuffer, null, this.renderTargetCpuDescriptor); + this.renderTarget.Swap(&backBuffer); + } + } + + public void BeginRenderTarget(ID3D12GraphicsCommandList* cmdList, bool clearRenderTarget) + { + var barrier = new D3D12_RESOURCE_BARRIER + { + Type = D3D12_RESOURCE_BARRIER_TYPE.D3D12_RESOURCE_BARRIER_TYPE_TRANSITION, + Flags = D3D12_RESOURCE_BARRIER_FLAGS.D3D12_RESOURCE_BARRIER_FLAG_NONE, + Transition = new() + { + pResource = this.RenderTarget, + Subresource = D3D12.D3D12_RESOURCE_BARRIER_ALL_SUBRESOURCES, + StateBefore = D3D12_RESOURCE_STATES.D3D12_RESOURCE_STATE_PRESENT, + StateAfter = D3D12_RESOURCE_STATES.D3D12_RESOURCE_STATE_RENDER_TARGET, + }, + }; + cmdList->ResourceBarrier(1, &barrier); + + var rtcd = this.renderTargetCpuDescriptor; + cmdList->OMSetRenderTargets(1, &rtcd, false, null); + + if (clearRenderTarget) + { + var clearColor = ClearColor; + cmdList->ClearRenderTargetView(rtcd, (float*)&clearColor, 0, null); + } + } + + public void EndRenderTarget(ID3D12GraphicsCommandList* cmdList) + { + var barrier = new D3D12_RESOURCE_BARRIER + { + Type = D3D12_RESOURCE_BARRIER_TYPE.D3D12_RESOURCE_BARRIER_TYPE_TRANSITION, + Flags = D3D12_RESOURCE_BARRIER_FLAGS.D3D12_RESOURCE_BARRIER_FLAG_NONE, + Transition = new() + { + pResource = this.RenderTarget, + Subresource = D3D12.D3D12_RESOURCE_BARRIER_ALL_SUBRESOURCES, + StateBefore = D3D12_RESOURCE_STATES.D3D12_RESOURCE_STATE_RENDER_TARGET, + StateAfter = D3D12_RESOURCE_STATES.D3D12_RESOURCE_STATE_PRESENT, + }, + }; + cmdList->ResourceBarrier(1, &barrier); + } + + public void EnsureVertexBufferCapacity(ID3D12Device* device, int capacity) + { + if (this.vertexBufferSize >= capacity) + return; + + this.vertexBuffer.Reset(); + this.vertexBufferSize = (uint)(capacity + 5000); + fixed (Guid* piid = &IID.IID_ID3D12Resource) + { + var props = new D3D12_HEAP_PROPERTIES + { + Type = D3D12_HEAP_TYPE.D3D12_HEAP_TYPE_UPLOAD, + CPUPageProperty = D3D12_CPU_PAGE_PROPERTY.D3D12_CPU_PAGE_PROPERTY_UNKNOWN, + MemoryPoolPreference = D3D12_MEMORY_POOL.D3D12_MEMORY_POOL_UNKNOWN, + }; + var desc = new D3D12_RESOURCE_DESC + { + Dimension = D3D12_RESOURCE_DIMENSION.D3D12_RESOURCE_DIMENSION_BUFFER, + Width = (ulong)(this.vertexBufferSize * sizeof(ImDrawVert)), + Height = 1, + DepthOrArraySize = 1, + MipLevels = 1, + Format = DXGI_FORMAT.DXGI_FORMAT_UNKNOWN, + SampleDesc = new(1, 0), + Layout = D3D12_TEXTURE_LAYOUT.D3D12_TEXTURE_LAYOUT_ROW_MAJOR, + Flags = D3D12_RESOURCE_FLAGS.D3D12_RESOURCE_FLAG_NONE, + }; + device->CreateCommittedResource( + &props, + D3D12_HEAP_FLAGS.D3D12_HEAP_FLAG_NONE, + &desc, + D3D12_RESOURCE_STATES.D3D12_RESOURCE_STATE_GENERIC_READ, + null, + piid, + (void**)this.vertexBuffer.GetAddressOf()).ThrowOnError(); + + fixed (void* pName = $"{this.debugName}.{nameof(this.vertexBuffer)}") + this.vertexBuffer.Get()->SetName((ushort*)pName).ThrowOnError(); + } + } + + public void EnsureIndexBufferCapacity(ID3D12Device* device, int capacity) + { + if (this.indexBufferSize >= capacity) + return; + + this.indexBuffer.Reset(); + this.indexBufferSize = (uint)(capacity + 10000); + fixed (Guid* piid = &IID.IID_ID3D12Resource) + { + var props = new D3D12_HEAP_PROPERTIES + { + Type = D3D12_HEAP_TYPE.D3D12_HEAP_TYPE_UPLOAD, + CPUPageProperty = D3D12_CPU_PAGE_PROPERTY.D3D12_CPU_PAGE_PROPERTY_UNKNOWN, + MemoryPoolPreference = D3D12_MEMORY_POOL.D3D12_MEMORY_POOL_UNKNOWN, + }; + var desc = new D3D12_RESOURCE_DESC + { + Dimension = D3D12_RESOURCE_DIMENSION.D3D12_RESOURCE_DIMENSION_BUFFER, + Width = this.indexBufferSize * sizeof(ushort), + Height = 1, + DepthOrArraySize = 1, + MipLevels = 1, + Format = DXGI_FORMAT.DXGI_FORMAT_UNKNOWN, + SampleDesc = new(1, 0), + Layout = D3D12_TEXTURE_LAYOUT.D3D12_TEXTURE_LAYOUT_ROW_MAJOR, + Flags = D3D12_RESOURCE_FLAGS.D3D12_RESOURCE_FLAG_NONE, + }; + device->CreateCommittedResource( + &props, + D3D12_HEAP_FLAGS.D3D12_HEAP_FLAG_NONE, + &desc, + D3D12_RESOURCE_STATES.D3D12_RESOURCE_STATE_GENERIC_READ, + null, + piid, + (void**)this.indexBuffer.GetAddressOf()).ThrowOnError(); + + fixed (void* pName = $"{this.debugName}.{nameof(this.indexBuffer)}") + this.indexBuffer.Get()->SetName((ushort*)pName).ThrowOnError(); + } + } + + public void OverwriteVertexBuffer(Span cmdLists) + { + try + { + var range = default(D3D12_RANGE); // we don't care about what was in there before + void* tmp; + + this.VertexBuffer->Map(0, &range, &tmp).ThrowOnError(); + var targetVertices = new Span(tmp, this.VertexBufferSize); + + foreach (ref var cmdList in cmdLists) + { + var vertices = new ImVectorWrapper(ref cmdList.NativePtr->VtxBuffer); + vertices.CopyTo(targetVertices); + targetVertices = targetVertices[vertices.Length..]; + } + } + finally + { + this.VertexBuffer->Unmap(0, null); + } + } + + public void OverwriteIndexBuffer(Span cmdLists) + { + try + { + var range = default(D3D12_RANGE); // we don't care about what was in there before + void* tmp; + + this.IndexBuffer->Map(0, &range, &tmp).ThrowOnError(); + var targetIndices = new Span(tmp, this.IndexBufferSize); + + foreach (ref var cmdList in cmdLists) + { + var indices = new ImVectorWrapper(ref cmdList.NativePtr->IdxBuffer); + indices.CopyTo(targetIndices); + targetIndices = targetIndices[indices.Length..]; + } + } + finally + { + this.IndexBuffer->Unmap(0, null); + } + } + + public void BindIndexVertexBuffers(ID3D12GraphicsCommandList* cmdList) + { + // Bind shader and vertex buffers + var vbv = new D3D12_VERTEX_BUFFER_VIEW + { + BufferLocation = this.VertexBuffer->GetGPUVirtualAddress(), + SizeInBytes = (uint)(this.VertexBufferSize * sizeof(ImDrawVert)), + StrideInBytes = (uint)sizeof(ImDrawVert), + }; + cmdList->IASetVertexBuffers(0, 1, &vbv); + + var ibv = new D3D12_INDEX_BUFFER_VIEW + { + BufferLocation = this.IndexBuffer->GetGPUVirtualAddress(), + SizeInBytes = (uint)(this.IndexBufferSize * sizeof(ushort)), + Format = DXGI_FORMAT.DXGI_FORMAT_R16_UINT, + }; + cmdList->IASetIndexBuffer(&ibv); + cmdList->IASetPrimitiveTopology(D3D_PRIMITIVE_TOPOLOGY.D3D_PRIMITIVE_TOPOLOGY_TRIANGLELIST); + } + + public void ResetHeap() + { + this.heapLength = 0; + foreach (ref var x in CollectionsMarshal.AsSpan(this.deferredReleasingHeaps)) + x.Reset(); + this.deferredReleasingHeaps.Clear(); + } + + public void EnsureHeapCapacity(ID3D12Device* device, int capacity) + { + if (this.heapCapacity >= capacity) + return; + + if (!this.heap.IsEmpty()) + { + this.deferredReleasingHeaps.Add(this.heap); + this.heap.Detach(); + } + + var newCapacity = this.heapLength == 0 ? HeapDefaultCapacity : this.heapLength * 2; + var desc = new D3D12_DESCRIPTOR_HEAP_DESC + { + Type = D3D12_DESCRIPTOR_HEAP_TYPE.D3D12_DESCRIPTOR_HEAP_TYPE_CBV_SRV_UAV, + NumDescriptors = (uint)newCapacity, + Flags = D3D12_DESCRIPTOR_HEAP_FLAGS.D3D12_DESCRIPTOR_HEAP_FLAG_SHADER_VISIBLE, + }; + fixed (Guid* guid = &IID.IID_ID3D12DescriptorHeap) + fixed (ID3D12DescriptorHeap** ppHeap = &this.heap.GetPinnableReference()) + device->CreateDescriptorHeap(&desc, guid, (void**)ppHeap).ThrowOnError(); + + fixed (void* pName = $"{this.debugName}.{nameof(this.heap)}") + this.heap.Get()->SetName((ushort*)pName).ThrowOnError(); + this.heapCapacity = newCapacity; + } + + public void BindResourceUsingHeap( + ID3D12Device* device, + ID3D12GraphicsCommandList* cmdList, + ID3D12Resource* resource) + { + var entrySize = device->GetDescriptorHandleIncrementSize( + D3D12_DESCRIPTOR_HEAP_TYPE.D3D12_DESCRIPTOR_HEAP_TYPE_CBV_SRV_UAV); + this.EnsureHeapCapacity(device, this.heapLength + 1); + + var h = this.heap.Get(); + var cpuh = h->GetCPUDescriptorHandleForHeapStart(); + var gpuh = h->GetGPUDescriptorHandleForHeapStart(); + cpuh.ptr += (nuint)(entrySize * this.heapLength); + gpuh.ptr += (nuint)(entrySize * this.heapLength); + device->CreateShaderResourceView(resource, null, cpuh); + this.heapLength++; + + cmdList->SetDescriptorHeaps(1, &h); + cmdList->SetGraphicsRootDescriptorTable(1, gpuh); + } + + private void ReleaseUnmanagedResources() + { + this.ResetHeap(); + this.heap.Reset(); + this.indexBuffer.Reset(); + this.vertexBuffer.Reset(); + this.indexBufferSize = this.vertexBufferSize = 0u; + this.renderTarget.Reset(); + } + } +} diff --git a/Dalamud/ImGuiScene/Implementations/Dx12Renderer.cs b/Dalamud/ImGuiScene/Implementations/Dx12Renderer.cs new file mode 100644 index 0000000000..7543abbb77 --- /dev/null +++ b/Dalamud/ImGuiScene/Implementations/Dx12Renderer.cs @@ -0,0 +1,579 @@ +using System.Buffers; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.Linq; +using System.Numerics; +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; +using System.Threading; + +using Dalamud.ImGuiScene.Helpers; +using Dalamud.Interface.Textures; +using Dalamud.Interface.Textures.TextureWraps; +using Dalamud.Interface.Utility; +using Dalamud.Plugin.Internal.Types; +using Dalamud.Utility; + +using ImGuiNET; + +using TerraFX.Interop.DirectX; +using TerraFX.Interop.Windows; + +namespace Dalamud.ImGuiScene.Implementations; + +/// +/// Deals with rendering ImGui using DirectX 12. +/// See https://github.com/ocornut/imgui/blob/master/examples/imgui_impl_dx12.cpp for the original implementation. +/// +[SuppressMessage("Interoperability", "CA1416:Validate platform compatibility", Justification = "DX12")] +[SuppressMessage( + "StyleCop.CSharp.LayoutRules", + "SA1519:Braces should not be omitted from multi-line child statement", + Justification = "Multiple fixed/using scopes")] +internal unsafe partial class Dx12Renderer : IImGuiRenderer +{ + private readonly Dictionary userCallbacks = new(); + private readonly List fontTextures = new(); + + private readonly TextureManager textureManager; + private readonly ViewportHandler viewportHandler; + private readonly nint renderNamePtr; + + private readonly DXGI_FORMAT rtvFormat; + private readonly ViewportData mainViewport; + + private bool releaseUnmanagedResourceCalled; + + private ComPtr device; + private ComPtr dcompDevice; + + private TexturePipeline? defaultPipeline; + + /// + /// Initializes a new instance of the class, + /// from existing swap chain, device, and command queue. + /// + /// The swap chain. + /// The device. + /// The command queue. + public Dx12Renderer(IDXGISwapChain3* swapChain, ID3D12Device* device, ID3D12CommandQueue* commandQueue) + { + if (swapChain is null) + throw new NullReferenceException($"{nameof(swapChain)} cannot be null."); + if (device is null) + throw new NullReferenceException($"{nameof(device)} cannot be null."); + if (commandQueue is null) + throw new NullReferenceException($"{nameof(commandQueue)} cannot be null."); + + using var mySwapChain = new ComPtr(swapChain); + using var myDevice = new ComPtr(device); + using var myCommandQueue = new ComPtr(commandQueue); + ReShadePeeler.PeelSwapChain(&mySwapChain); + ReShadePeeler.PeelD3D12Device(&myDevice); + ReShadePeeler.PeelD3D12CommandQueue(&myCommandQueue); + + var io = ImGui.GetIO(); + if (ImGui.GetIO().NativePtr->BackendRendererName is not null) + throw new InvalidOperationException("ImGui backend renderer seems to be have been already attached."); + + DXGI_SWAP_CHAIN_DESC desc; + mySwapChain.Get()->GetDesc(&desc).ThrowOnError(); + this.NumBackBuffers = (int)desc.BufferCount; + this.rtvFormat = desc.BufferDesc.Format; + myDevice.Swap(ref this.device); + + io.BackendFlags |= ImGuiBackendFlags.RendererHasVtxOffset | ImGuiBackendFlags.RendererHasViewports; + + this.renderNamePtr = Marshal.StringToHGlobalAnsi("imgui_impl_dx12_c#"); + io.NativePtr->BackendRendererName = (byte*)this.renderNamePtr; + + try + { + if (io.ConfigFlags.HasFlag(ImGuiConfigFlags.ViewportsEnable)) + { + try + { + fixed (IDCompositionDevice** pp = &this.dcompDevice.GetPinnableReference()) + fixed (Guid* piidDCompositionDevice = &IID.IID_IDCompositionDevice) + DirectX.DCompositionCreateDevice(null, piidDCompositionDevice, (void**)pp).ThrowOnError(); + + ImGuiViewportHelpers.EnableViewportWindowBackgroundAlpha(); + } + catch + { + // don't care; not using DComposition then + } + + this.viewportHandler = new(this); + } + + this.mainViewport = ViewportData.Create( + this, + mySwapChain, + myCommandQueue, + nameof(this.mainViewport), + null, + null); + ImGui.GetPlatformIO().Viewports[0].RendererUserData = this.mainViewport.AsHandle(); + this.textureManager = new(this.device); + } + catch + { + this.textureManager?.Dispose(); + this.viewportHandler?.Dispose(); + this.ReleaseUnmanagedResources(); + throw; + } + } + + /// + /// Initializes a new instance of the class, + /// without using any swap buffer, for offscreen rendering. + /// + /// The device. + /// The format of render target. + /// Number of back buffers. + /// The width. + /// The height of render target. + public Dx12Renderer(ID3D12Device* device, DXGI_FORMAT rtvFormat, int numBackBuffers, int width, int height) + { + if (device is null) + throw new NullReferenceException($"{nameof(device)} cannot be null."); + if (rtvFormat == DXGI_FORMAT.DXGI_FORMAT_UNKNOWN) + throw new ArgumentOutOfRangeException(nameof(rtvFormat), rtvFormat, "Cannot be unknown."); + if (numBackBuffers is < 2 or > 16) + throw new ArgumentOutOfRangeException(nameof(numBackBuffers), numBackBuffers, "Must be between 2 and 16."); + if (width <= 0) + throw new ArgumentOutOfRangeException(nameof(width), width, "Must be a positive number."); + if (height <= 0) + throw new ArgumentOutOfRangeException(nameof(height), height, "Must be a positive number."); + + using var myDevice = new ComPtr(device); + ReShadePeeler.PeelD3D12Device(&myDevice); + + var io = ImGui.GetIO(); + if (ImGui.GetIO().NativePtr->BackendRendererName is not null) + throw new InvalidOperationException("ImGui backend renderer seems to be have been already attached."); + + this.NumBackBuffers = numBackBuffers; + this.rtvFormat = rtvFormat; + myDevice.Swap(ref this.device); + + io.BackendFlags |= ImGuiBackendFlags.RendererHasVtxOffset | ImGuiBackendFlags.RendererHasViewports; + + this.renderNamePtr = Marshal.StringToHGlobalAnsi("imgui_impl_dx12_c#"); + io.NativePtr->BackendRendererName = (byte*)this.renderNamePtr; + + try + { + if (io.ConfigFlags.HasFlag(ImGuiConfigFlags.ViewportsEnable)) + { + try + { + fixed (IDCompositionDevice** pp = &this.dcompDevice.GetPinnableReference()) + fixed (Guid* piidDCompositionDevice = &IID.IID_IDCompositionDevice) + DirectX.DCompositionCreateDevice(null, piidDCompositionDevice, (void**)pp).ThrowOnError(); + + ImGuiViewportHelpers.EnableViewportWindowBackgroundAlpha(); + } + catch + { + // don't care; not using DComposition then + } + + this.viewportHandler = new(this); + } + + this.mainViewport = ViewportData.Create(this, width, height, nameof(this.mainViewport)); + ImGui.GetPlatformIO().Viewports[0].RendererUserData = this.mainViewport.AsHandle(); + this.textureManager = new(this.device); + } + catch + { + this.textureManager?.Dispose(); + this.viewportHandler?.Dispose(); + this.ReleaseUnmanagedResources(); + throw; + } + } + + /// + /// Finalizes an instance of the class. + /// + ~Dx12Renderer() => this.ReleaseUnmanagedResources(); + + /// + /// Gets the number of back buffers. + /// + public int NumBackBuffers { get; } + + /// + public void Dispose() + { + this.ReleaseUnmanagedResources(); + foreach (var t in this.fontTextures) + t.Dispose(); + this.textureManager.Dispose(); + this.viewportHandler.Dispose(); + GC.SuppressFinalize(this); + } + + /// + public void OnNewFrame() + { + this.EnsureDeviceObjects(); + } + + /// + public void OnPreResize() => this.mainViewport.ResetBuffers(); + + /// + public void OnPostResize(int width, int height) => this.mainViewport.ResizeBuffers(width, height, false); + + /// + public void RenderDrawData(ImDrawDataPtr drawData) + { + var noSwapChain = this.mainViewport.SwapChain == null; + this.textureManager.FlushPendingTextureUploads(); + this.mainViewport.Draw(drawData, noSwapChain); + if (noSwapChain) + this.mainViewport.WaitForPendingOperations(); + } + + /// + /// Gets the current main render target, and the index of it, out of back buffers. + /// + /// The resource. Reference count is not increased. Do not release. + /// The back buffer index. + public void GetCurrentMainViewportRenderTarget(out ID3D12Resource* resource, out int backBufferIndex) + { + resource = this.mainViewport.CurrentFrame.RenderTarget; + backBufferIndex = this.mainViewport.CurrentFrameIndex; + } + + /// + /// Creates a new texture pipeline. + /// + /// The pixel shader data. + /// The sampler description. + /// Name for debugging. + /// The handle to the new texture pipeline. + public ITexturePipelineWrap CreateTexturePipeline( + ReadOnlySpan ps, + in D3D12_STATIC_SAMPLER_DESC samplerDesc, + [CallerMemberName] string debugName = "") + { + var assembly = Assembly.GetExecutingAssembly(); + using var streamVs = assembly.GetManifestResourceStream("imgui-vertex.hlsl.bytes")!; + var vs = ArrayPool.Shared.Rent((int)streamVs.Length); + try + { + streamVs.ReadExactly(vs, 0, (int)streamVs.Length); + return TexturePipelineWrap.TakeOwnership( + TexturePipeline.From( + this.device, + this.rtvFormat, + vs.AsSpan(0, (int)streamVs.Length), + ps, + samplerDesc, + debugName)); + } + finally + { + ArrayPool.Shared.Return(vs); + } + } + + /// + public ITexturePipelineWrap? GetTexturePipeline(IDalamudTextureWrap texture) => + TextureData.AttachFromAddress(texture.ImGuiHandle).CustomPipeline is { } cp + ? TexturePipelineWrap.NewReference(cp) + : null; + + /// + public void SetTexturePipeline(IDalamudTextureWrap texture, ITexturePipelineWrap? pipeline) => + TextureData.AttachFromAddress(texture.ImGuiHandle).CustomPipeline = pipeline switch + { + TexturePipelineWrap tpw => tpw.Data, + null => default, + _ => throw new ArgumentException("Not a compatible texture pipeline wrap.", nameof(pipeline)), + }; + + /// + public nint AddDrawCmdUserCallback(IImGuiRenderer.DrawCmdUserCallbackDelegate @delegate) + { + if (this.userCallbacks.FirstOrDefault(x => x.Value == @delegate).Key is not 0 and var key) + return key; + + key = Marshal.GetFunctionPointerForDelegate(@delegate); + this.userCallbacks.Add(key, @delegate); + return key; + } + + /// + public void RemoveDrawCmdUserCallback(IImGuiRenderer.DrawCmdUserCallbackDelegate @delegate) + { + foreach (var key in this.userCallbacks + .Where(x => x.Value == @delegate) + .Select(x => x.Key) + .ToArray()) + { + this.userCallbacks.Remove(key); + } + } + + /// + /// Rebuilds font texture. + /// + public void RebuildFontTexture() + { + foreach (var fontResourceView in this.fontTextures) + fontResourceView.Dispose(); + this.fontTextures.Clear(); + + this.CreateFontsTexture(); + } + + /// + public IDalamudTextureWrap CreateTexture2D( + ReadOnlySpan data, + RawImageSpecification specs, + bool cpuRead, bool cpuWrite, + bool allowRenderTarget, + [CallerMemberName] string debugName = "") + { + try + { + return this.textureManager.CreateTexture(data, specs, debugName); + } + catch (COMException e) when (e.HResult == unchecked((int)0x887a0005)) + { + throw new AggregateException( + Marshal.GetExceptionForHR(this.device.Get()->GetDeviceRemovedReason()) ?? new(), + e); + } + } + + /// + public IDalamudTextureWrap CreateTextureFromImGuiViewport( + ImGuiViewportTextureArgs args, + LocalPlugin? ownerPlugin, + string? debugName = null, + CancellationToken cancellationToken = default) => + throw new NotImplementedException(); + + /// + public IDalamudTextureWrap WrapFromTextureResource(IntPtr handle) => throw new NotImplementedException(); + + /// + public RawImageSpecification GetTextureSpecification(IDalamudTextureWrap texture) + { + var td = TextureData.AttachFromAddress(texture.ImGuiHandle); + return new(td.Width, td.Height, (int)td.Format); + } + + /// + public byte[] GetTextureData(IDalamudTextureWrap texture, out RawImageSpecification specification) => + throw new NotImplementedException(); + + /// + public nint GetTextureResource(IDalamudTextureWrap texture) => + (nint)TextureData.AttachFromAddress(texture.ImGuiHandle).Texture; + + /// + public void DrawTextureToTexture( + IDalamudTextureWrap target, + Vector2 targetUv0, + Vector2 targetUv1, + IDalamudTextureWrap source, + Vector2 sourceUv0, + Vector2 sourceUv1, + bool copyAlphaOnly = false) => + throw new NotImplementedException(); + + private void RenderDrawDataInternal(ViewportFrame frameData, ImDrawDataPtr drawData, ID3D12GraphicsCommandList* ctx) + { + // Avoid rendering when minimized + if (drawData.DisplaySize.X <= 0 || drawData.DisplaySize.Y <= 0) + return; + + if (!drawData.Valid || drawData.CmdListsCount == 0) + return; + + var cmdLists = new Span(drawData.NativePtr->CmdLists, drawData.NativePtr->CmdListsCount); + + frameData.EnsureVertexBufferCapacity(this.device, drawData.TotalVtxCount); + frameData.EnsureIndexBufferCapacity(this.device, drawData.TotalIdxCount); + frameData.OverwriteVertexBuffer(cmdLists); + frameData.OverwriteIndexBuffer(cmdLists); + frameData.BindIndexVertexBuffers(ctx); + + // Setup viewport + var viewport = new D3D12_VIEWPORT + { + Width = drawData.DisplaySize.X, + Height = drawData.DisplaySize.Y, + MinDepth = 0f, + MaxDepth = 1f, + TopLeftX = 0f, + TopLeftY = 0f, + }; + ctx->RSSetViewports(1, &viewport); + + // Setup blend factor + var blendFactor = default(Vector4); + ctx->OMSetBlendFactor((float*)&blendFactor); + + // Setup orthographic projection matrix into our constant buffer. + // Our visible imgui space lies from DisplayPos (LT) to DisplayPos+DisplaySize (RB). + // DisplayPos is (0,0) for single viewport apps. + var projMtx = Matrix4x4.CreateOrthographicOffCenter( + drawData.DisplayPos.X, + drawData.DisplayPos.X + drawData.DisplaySize.X, + drawData.DisplayPos.Y + drawData.DisplaySize.Y, + drawData.DisplayPos.Y, + 1f, + 0f); + + // Ensure that heap is of sufficient size. + // We're overshooting it; a texture may be bound to the same heap multiple times. + frameData.ResetHeap(); + var ensuringHeapSize = 0; + foreach (ref var cmdList in cmdLists) + ensuringHeapSize += cmdList.CmdBuffer.Size; + frameData.EnsureHeapCapacity(this.device, ensuringHeapSize); + + // Render command lists + // (Because we merged all buffers into a single one, we maintain our own offset into them) + var vertexOffset = 0; + var indexOffset = 0; + var clipOff = new Vector4(drawData.DisplayPos, drawData.DisplayPos.X, drawData.DisplayPos.Y); + foreach (ref var cmdList in cmdLists) + { + var cmds = new ImVectorWrapper(&cmdList.NativePtr->CmdBuffer); + foreach (ref var cmd in cmds.DataSpan) + { + var clipV4 = cmd.ClipRect - clipOff; + var clipRect = new RECT((int)clipV4.X, (int)clipV4.Y, (int)clipV4.Z, (int)clipV4.W); + + // Skip the draw if nothing would be visible + if (clipRect.left >= clipRect.right || clipRect.top >= clipRect.bottom) + continue; + + ctx->RSSetScissorRects(1, &clipRect); + + if (cmd.UserCallback == nint.Zero) + { + // Bind texture and draw + var ptcd = TextureData.AttachFromAddress(cmd.TextureId); + + (ptcd.CustomPipeline ?? this.defaultPipeline!).BindTo(ctx); + + ctx->SetGraphicsRoot32BitConstants(0, 16, &projMtx, 0); + frameData.BindResourceUsingHeap(this.device, ctx, ptcd.Texture); + ctx->DrawIndexedInstanced( + cmd.ElemCount, + 1, + (uint)(cmd.IdxOffset + indexOffset), + (int)(cmd.VtxOffset + vertexOffset), + 0); + } + else if (this.userCallbacks.TryGetValue(cmd.UserCallback, out var cb)) + { + // Use custom callback + cb(drawData, (ImDrawCmd*)Unsafe.AsPointer(ref cmd)); + } + } + + indexOffset += cmdList.IdxBuffer.Size; + vertexOffset += cmdList.VtxBuffer.Size; + } + } + + /// + /// Builds fonts as necessary, and uploads the built data onto the GPU.
+ /// No-op if it has already been done. + ///
+ private void CreateFontsTexture() + { + if (this.fontTextures.Any()) + return; + + var io = ImGui.GetIO(); + if (io.Fonts.Textures.Size == 0) + io.Fonts.Build(); + + for (int textureIndex = 0, textureCount = io.Fonts.Textures.Size; + textureIndex < textureCount; + textureIndex++) + { + // Build texture atlas + io.Fonts.GetTexDataAsRGBA32( + textureIndex, + out byte* fontPixels, + out var width, + out var height, + out var bytespp); + + var tex = this.CreateTexture2D( + new(fontPixels, width * height * bytespp), + new(width, height, DXGI_FORMAT.DXGI_FORMAT_R8G8B8A8_UNORM, width * bytespp), + false, + false, + false, + $"Font#{textureIndex}"); + io.Fonts.SetTexID(textureIndex, tex.ImGuiHandle); + this.fontTextures.Add(tex); + } + + io.Fonts.ClearTexData(); + } + + private void EnsureDeviceObjects() + { + if (this.defaultPipeline is null) + { + var assembly = Assembly.GetExecutingAssembly(); + using var streamVs = assembly.GetManifestResourceStream("imgui-vertex.hlsl.bytes")!; + using var streamPs = assembly.GetManifestResourceStream("imgui-frag.hlsl.bytes")!; + var vs = ArrayPool.Shared.Rent((int)streamVs.Length); + var ps = ArrayPool.Shared.Rent((int)streamPs.Length); + streamVs.ReadExactly(vs, 0, (int)streamVs.Length); + streamPs.ReadExactly(ps, 0, (int)streamPs.Length); + this.defaultPipeline = TexturePipeline.From( + this.device, + this.rtvFormat, + vs.AsSpan(0, (int)streamVs.Length), + ps.AsSpan(0, (int)streamPs.Length), + nameof(this.defaultPipeline)); + ArrayPool.Shared.Return(vs); + ArrayPool.Shared.Return(ps); + } + + this.CreateFontsTexture(); + } + + private void ReleaseUnmanagedResources() + { + if (this.releaseUnmanagedResourceCalled) + return; + this.releaseUnmanagedResourceCalled = true; + + this.mainViewport.Release(); + ImGui.GetPlatformIO().Viewports[0].RendererUserData = nint.Zero; + ImGui.DestroyPlatformWindows(); + + var io = ImGui.GetIO(); + if (io.NativePtr->BackendRendererName == (void*)this.renderNamePtr) + io.NativePtr->BackendRendererName = null; + if (this.renderNamePtr != 0) + Marshal.FreeHGlobal(this.renderNamePtr); + + foreach (var i in Enumerable.Range(0, io.Fonts.Textures.Size)) + io.Fonts.SetTexID(i, nint.Zero); + + this.device.Reset(); + this.dcompDevice.Reset(); + } +} diff --git a/Dalamud/ImGuiScene/Implementations/Dx12Win32Scene.cs b/Dalamud/ImGuiScene/Implementations/Dx12Win32Scene.cs new file mode 100644 index 0000000000..edc0a5918d --- /dev/null +++ b/Dalamud/ImGuiScene/Implementations/Dx12Win32Scene.cs @@ -0,0 +1,366 @@ +using System.Diagnostics.CodeAnalysis; +using System.Numerics; +using System.Runtime.CompilerServices; +using System.Threading; + +using Dalamud.ImGuiScene.Helpers; +using Dalamud.Interface.Textures; +using Dalamud.Interface.Textures.TextureWraps; +using Dalamud.Plugin.Internal.Types; +using Dalamud.Utility; + +using ImGuiNET; + +using ImGuizmoNET; + +using ImPlotNET; + +using TerraFX.Interop.DirectX; +using TerraFX.Interop.Windows; + +namespace Dalamud.ImGuiScene.Implementations; + +/// +/// Backend for ImGui, using and . +/// +[SuppressMessage("Interoperability", "CA1416:Validate platform compatibility", Justification = "DX12")] +[SuppressMessage( + "StyleCop.CSharp.LayoutRules", + "SA1519:Braces should not be omitted from multi-line child statement", + Justification = "Multiple fixed/using scopes")] +internal sealed unsafe class Dx12Win32Scene : IWin32Scene +{ + private readonly Dx12Renderer imguiRenderer; + private readonly Win32InputHandler imguiInput; + private readonly WicEasy wicEasy; + + private ComPtr swapChainPossiblyWrapped; + private ComPtr swapChain; + private ComPtr device; + + private int targetWidth; + private int targetHeight; + + /// + /// Initializes a new instance of the class. + /// + /// The pointer to an instance of . The reference is copied. + /// The pointer to an instance of . The reference is copied. + /// The pointer to an instance of . The reference is copied. + public Dx12Win32Scene(IDXGISwapChain3* swapChain, ID3D12CommandQueue* commandQueue, ID3D12Device* device) + { + if (device is null || swapChain is null) + throw new NullReferenceException(); + + this.wicEasy = new(); + try + { + this.device = new(device); + this.swapChainPossiblyWrapped = new(swapChain); + this.swapChain = new(swapChain); + fixed (ComPtr* ppSwapChain = &this.swapChain) + ReShadePeeler.PeelSwapChain(ppSwapChain); + fixed (ComPtr* ppDevice = &this.device) + ReShadePeeler.PeelD3D12Device(ppDevice); + + var desc = default(DXGI_SWAP_CHAIN_DESC); + swapChain->GetDesc(&desc).ThrowOnError(); + this.targetWidth = (int)desc.BufferDesc.Width; + this.targetHeight = (int)desc.BufferDesc.Height; + this.WindowHandlePtr = desc.OutputWindow; + + var ctx = ImGui.CreateContext(); + ImGuizmo.SetImGuiContext(ctx); + ImPlot.SetImGuiContext(ctx); + ImPlot.CreateContext(); + + ImGui.GetIO().ConfigFlags |= ImGuiConfigFlags.DockingEnable | ImGuiConfigFlags.ViewportsEnable; + + this.imguiRenderer = new(swapChain, device, commandQueue); + this.imguiInput = new(this.WindowHandlePtr); + } + catch + { + this.Dispose(); + throw; + } + } + + /// + /// Initializes a new instance of the class. + /// + /// The pointer to an instance of . The reference is copied. + /// The window handle for input processing. + /// Initial target width. + /// Initial target height. + public Dx12Win32Scene(ID3D12Device* device, HWND hwnd, int targetWidth, int targetHeight) + { + if (device is null) + throw new NullReferenceException(); + + this.wicEasy = new(); + try + { + this.device = new(device); + fixed (ComPtr* ppDevice = &this.device) + ReShadePeeler.PeelD3D12Device(ppDevice); + + this.targetWidth = targetWidth; + this.targetHeight = targetHeight; + this.WindowHandlePtr = hwnd; + + var ctx = ImGui.CreateContext(); + ImGuizmo.SetImGuiContext(ctx); + ImPlot.SetImGuiContext(ctx); + ImPlot.CreateContext(); + + ImGui.GetIO().ConfigFlags |= ImGuiConfigFlags.DockingEnable | ImGuiConfigFlags.ViewportsEnable; + + this.imguiRenderer = new(device, DXGI_FORMAT.DXGI_FORMAT_B8G8R8A8_UNORM, 2, targetWidth, targetHeight); + this.imguiInput = new(this.WindowHandlePtr); + } + catch + { + this.wicEasy.Dispose(); + this.ReleaseUnmanagedResources(); + throw; + } + } + + /// + /// Finalizes an instance of the class. + /// + ~Dx12Win32Scene() => this.ReleaseUnmanagedResources(); + + /// + public event IImGuiScene.BuildUiDelegate? BuildUi; + + /// + public event IImGuiScene.NewInputFrameDelegate? NewInputFrame; + + /// + public event IImGuiScene.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; + } + + /// + /// Gets the pointer to an instance of . + /// + public IDXGISwapChain3* SwapChain => this.swapChain; + + /// + /// Gets the pointer to an instance of . + /// + public ID3D12Device* Device => this.device; + + /// + /// Gets the pointer to an instance of , in . + /// + public nint DeviceHandle => (nint)this.device.Get(); + + /// + /// Gets the window handle. + /// + public HWND WindowHandlePtr { get; private set; } + + /// + /// Gets the input handler. + /// + public Win32InputHandler InputHandler => this.imguiInput; + + /// + /// Gets the renderer. + /// + public Dx12Renderer Renderer => this.imguiRenderer; + + /// + public void Dispose() + { + this.wicEasy.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 SupportsTextureFormat(int format) => + this.SupportsTextureFormat((DXGI_FORMAT)format); + + /// + public bool SupportsTextureFormatForRenderTarget(int format) => + this.SupportsTextureFormat( + (DXGI_FORMAT)format, + D3D12_FORMAT_SUPPORT1.D3D12_FORMAT_SUPPORT1_TEXTURE2D | + D3D12_FORMAT_SUPPORT1.D3D12_FORMAT_SUPPORT1_RENDER_TARGET); + + /// + public IDalamudTextureWrap CreateTexture2D( + ReadOnlySpan data, + RawImageSpecification specs, + bool cpuRead, + bool cpuWrite, + bool allowRenderTarget, + [CallerMemberName] string debugName = "") => + this.imguiRenderer.CreateTexture2D( + data, + specs, + false, + false, + allowRenderTarget, + debugName); + + /// + public IDalamudTextureWrap CreateTextureFromImGuiViewport( + ImGuiViewportTextureArgs args, + LocalPlugin? ownerPlugin, + string? debugName = null, + CancellationToken cancellationToken = default) => + this.imguiRenderer.CreateTextureFromImGuiViewport(args, ownerPlugin, debugName, cancellationToken); + + /// + public IDalamudTextureWrap WrapFromTextureResource(nint handle) => + this.imguiRenderer.WrapFromTextureResource(handle); + + /// + public RawImageSpecification GetTextureSpecification(IDalamudTextureWrap texture) => + this.imguiRenderer.GetTextureSpecification(texture); + + /// + public byte[] GetTextureData(IDalamudTextureWrap texture, out RawImageSpecification specification) => + this.imguiRenderer.GetTextureData(texture, out specification); + + /// + public IntPtr GetTextureResource(IDalamudTextureWrap texture) => this.imguiRenderer.GetTextureResource(texture); + + /// + public void DrawTextureToTexture( + IDalamudTextureWrap target, + Vector2 targetUv0, + Vector2 targetUv1, + IDalamudTextureWrap source, + Vector2 sourceUv0, + Vector2 sourceUv1, + bool copyAlphaOnly = false) + { + throw new NotImplementedException(); + } + + /// + public ITexturePipelineWrap CreateTexturePipeline( + ReadOnlySpan ps, + in D3D12_STATIC_SAMPLER_DESC samplerDesc, + [CallerMemberName] string debugName = "") + => this.imguiRenderer.CreateTexturePipeline(ps, samplerDesc, debugName); + + /// + public void SetTexturePipeline(IDalamudTextureWrap textureHandle, ITexturePipelineWrap? pipelineHandle) => + this.imguiRenderer.SetTexturePipeline(textureHandle, pipelineHandle); + + /// + public ITexturePipelineWrap? GetTexturePipeline(IDalamudTextureWrap textureHandle) => + this.imguiRenderer.GetTexturePipeline(textureHandle); + + /// + 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; + } + + /// + /// Determines whether the current D3D12 Device supports the given DXGI format. + /// + /// DXGI format to check. + /// First format to test. + /// Second format to test. + /// Whether it is supported. + public bool SupportsTextureFormat( + DXGI_FORMAT dxgiFormat, + D3D12_FORMAT_SUPPORT1 formatSupport1 = D3D12_FORMAT_SUPPORT1.D3D12_FORMAT_SUPPORT1_TEXTURE2D, + D3D12_FORMAT_SUPPORT2 formatSupport2 = D3D12_FORMAT_SUPPORT2.D3D12_FORMAT_SUPPORT2_NONE) + { + var data = new D3D12_FEATURE_DATA_FORMAT_SUPPORT { Format = dxgiFormat }; + if (this.Device->CheckFeatureSupport( + D3D12_FEATURE.D3D12_FEATURE_FORMAT_SUPPORT, + &data, + (uint)sizeof(D3D12_FEATURE_DATA_FORMAT_SUPPORT)).FAILED) + return false; + + return (data.Support1 & formatSupport1) == formatSupport1 + && (data.Support2 & formatSupport2) == formatSupport2; + } + + private void ReleaseUnmanagedResources() + { + if (this.device.IsEmpty()) + return; + + this.imguiRenderer.Dispose(); + this.imguiInput.Dispose(); + + ImGui.DestroyContext(); + + this.swapChain.Dispose(); + this.device.Dispose(); + this.swapChainPossiblyWrapped.Dispose(); + } +} diff --git a/Dalamud/ImGuiScene/Implementations/Win32InputHandler.StaticLookupFunctions.cs b/Dalamud/ImGuiScene/Implementations/Win32InputHandler.StaticLookupFunctions.cs new file mode 100644 index 0000000000..fab4da2d87 --- /dev/null +++ b/Dalamud/ImGuiScene/Implementations/Win32InputHandler.StaticLookupFunctions.cs @@ -0,0 +1,311 @@ +using System.Runtime.CompilerServices; + +using ImGuiNET; + +using TerraFX.Interop.Windows; + +using Win32 = TerraFX.Interop.Windows.Windows; + +namespace Dalamud.ImGuiScene.Implementations; + +/// +/// 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) => (Win32.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 => + Win32.GET_XBUTTON_WPARAM(wParam) == Win32.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/ImGuiScene/Implementations/Win32InputHandler.cs b/Dalamud/ImGuiScene/Implementations/Win32InputHandler.cs new file mode 100644 index 0000000000..b381eb5141 --- /dev/null +++ b/Dalamud/ImGuiScene/Implementations/Win32InputHandler.cs @@ -0,0 +1,1089 @@ +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.ImGuiScene.Helpers.ImGuiViewportHelpers; + +using Win32 = TerraFX.Interop.Windows.Windows; + +namespace Dalamud.ImGuiScene.Implementations; + +/// +/// 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 ImGuiMouseCursor _oldCursor = ImGuiMouseCursor.None; + 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] = Win32.LoadCursorW(default, IDC.IDC_ARROW); + this.cursors[(int)ImGuiMouseCursor.TextInput] = Win32.LoadCursorW(default, IDC.IDC_IBEAM); + this.cursors[(int)ImGuiMouseCursor.ResizeAll] = Win32.LoadCursorW(default, IDC.IDC_SIZEALL); + this.cursors[(int)ImGuiMouseCursor.ResizeEW] = Win32.LoadCursorW(default, IDC.IDC_SIZEWE); + this.cursors[(int)ImGuiMouseCursor.ResizeNS] = Win32.LoadCursorW(default, IDC.IDC_SIZENS); + this.cursors[(int)ImGuiMouseCursor.ResizeNESW] = Win32.LoadCursorW(default, IDC.IDC_SIZENESW); + this.cursors[(int)ImGuiMouseCursor.ResizeNWSE] = Win32.LoadCursorW(default, IDC.IDC_SIZENWSE); + this.cursors[(int)ImGuiMouseCursor.Hand] = Win32.LoadCursorW(default, IDC.IDC_HAND); + this.cursors[(int)ImGuiMouseCursor.NotAllowed] = Win32.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() && Win32.GetCapture() == nint.Zero) + Win32.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() && Win32.GetCapture() == hWndCurrent) + Win32.ReleaseCapture(); + + io.MouseDown[button] = false; + this.imguiMouseIsDown[button] = false; + return default(LRESULT); + } + + break; + } + + case WM.WM_MOUSEWHEEL: + if (io.WantCaptureMouse) + { + io.MouseWheel += Win32.GET_WHEEL_DELTA_WPARAM(wParam) / (float)Win32.WHEEL_DELTA; + return default(LRESULT); + } + + break; + case WM.WM_MOUSEHWHEEL: + if (io.WantCaptureMouse) + { + io.MouseWheelH += Win32.GET_WHEEL_DELTA_WPARAM(wParam) / (float)Win32.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 (Win32.LOWORD(lParam) == Win32.HTCLIENT && this.UpdateMouseCursor()) + { + // this message returns 1 to block further processing + // because consistency is no fun + return 1; + } + } + + break; + // TODO: Decode why IME is miserable + // case WM.WM_IME_NOTIFY: + // return HandleImeMessage(hWnd, (long) wParam, (long) lParam); + 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) + { + Win32.SetCursorPos((int)io.MousePos.X, (int)io.MousePos.Y); + } + + if (Win32.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; + Win32.ClientToScreen(this.hWnd, &pt); + Win32.SetCursorPos(pt.x, pt.y); + } + + if (Win32.GetCursorPos(&pt) && Win32.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; + } + } + } + + // TODO This is kind of unnecessary unless we REALLY want viewport hovered support + // It seems to mess with the mouse and get it stuck a lot. Do not know why + // private void UpdateMousePos() { + // ImGuiIOPtr io = ImGui.GetIO(); + // + // // Set OS mouse position if requested (rarely used, only when ImGuiConfigFlags_NavEnableSetMousePos is enabled by user) + // // (When multi-viewports are enabled, all imgui positions are same as OS positions) + // if (io.WantSetMousePos) { + // POINT pos = new POINT() {x = (int) io.MousePos.X, y = (int) io.MousePos.Y}; + // if ((io.ConfigFlags & ImGuiConfigFlags.ViewportsEnable) == 0) + // Win32.ClientToScreen(_hWnd, ref pos); + // Win32.SetCursorPos(pos.x, pos.y); + // } + // + // io.MousePos = new Vector2(float.NegativeInfinity, float.NegativeInfinity); + // io.MouseHoveredViewport = 0; + // + // // Set imgui mouse position + // if (!Win32.GetCursorPos(out POINT mouseScreenPos)) + // return; + // nint focusedHwnd = Win32.GetForegroundWindow(); + // if (focusedHwnd != nint.Zero) { + // if (Win32.IsChild(focusedHwnd, _hWnd)) + // focusedHwnd = _hWnd; + // if ((io.ConfigFlags & ImGuiConfigFlags.ViewportsEnable) == ImGuiConfigFlags.ViewportsEnable) { + // // Multi-viewport mode: mouse position in OS absolute coordinates (io.MousePos is (0,0) when the mouse is on the upper-left of the primary monitor) + // // This is the position you can get with GetCursorPos(). In theory adding viewport->Pos is also the reverse operation of doing ScreenToClient(). + // ImGuiViewportPtr viewport = ImGui.FindViewportByPlatformHandle(focusedHwnd); + // unsafe { + // if (viewport.NativePtr != null) + // io.MousePos = new Vector2(mouseScreenPos.x, mouseScreenPos.y); + // } + // } else { + // // Single viewport mode: mouse position in client window coordinates (io.MousePos is (0,0) when the mouse is on the upper-left corner of the app window.) + // // This is the position you can get with GetCursorPos() + ScreenToClient() or from WM_MOUSEMOVE. + // if (focusedHwnd == _hWnd) { + // POINT mouseClientPos = mouseScreenPos; + // Win32.ScreenToClient(focusedHwnd, ref mouseClientPos); + // io.MousePos = new Vector2(mouseClientPos.x, mouseClientPos.y); + // } + // } + // } + // + // // (Optional) When using multiple viewports: set io.MouseHoveredViewport to the viewport the OS mouse cursor is hovering. + // // Important: this information is not easy to provide and many high-level windowing library won't be able to provide it correctly, because + // // - This is _ignoring_ viewports with the ImGuiViewportFlags_NoInputs flag (pass-through windows). + // // - This is _regardless_ of whether another viewport is focused or being dragged from. + // // If ImGuiBackendFlags_HasMouseHoveredViewport is not set by the backend, imgui will ignore this field and infer the information by relying on the + // // rectangles and last focused time of every viewports it knows about. It will be unaware of foreign windows that may be sitting between or over your windows. + // nint hovered_hwnd = Win32.WindowFromPoint(mouseScreenPos); + // if (hovered_hwnd != nint.Zero) { + // ImGuiViewportPtr viewport = ImGui.FindViewportByPlatformHandle(focusedHwnd); + // unsafe { + // if (viewport.NativePtr != null) + // if ((viewport.Flags & ImGuiViewportFlags.NoInputs) == 0 + // ) // FIXME: We still get our NoInputs window with WM_NCHITTEST/HTTRANSPARENT code when decorated? + // io.MouseHoveredViewport = viewport.ID; + // } + // } + // } + + 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) + Win32.SetCursor(default); + else + Win32.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 = Win32.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); + } + } + } + + // TODO: Decode why IME is miserable + // private int HandleImeMessage(nint hWnd, long wParam, long lParam) { + // + // int result = -1; + // // if (io.WantCaptureKeyboard) + // result = (int) Win32.DefWindowProc(hWnd, WM.WM_IME_NOTIFY, (nint) wParam, (nint) lParam); + // System.Diagnostics.Debug.WriteLine($"ime command {(Win32.ImeCommand) wParam} result {result}"); + // + // return result; + // } + + /// + /// 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 Win32.DefWindowProcW(hWndCurrent, msg, wParam, lParam); + + var viewport = ImGui.FindViewportByPlatformHandle(hWndCurrent); + if (viewport.NativePtr == null) + return Win32.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 + Win32.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 + Win32.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 Win32.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 delegateReferences = new(); + + private Win32InputHandler input; + private nint classNamePtr; + + private bool wantUpdateMonitors = true; + + public ViewportHandler(Win32InputHandler input) + { + this.input = input; + this.classNamePtr = Marshal.StringToHGlobalUni("ImGui Platform"); + + var pio = ImGui.GetPlatformIO(); + pio.Platform_CreateWindow = this.RegisterFunctionPointer(this.OnCreateWindow); + pio.Platform_DestroyWindow = this.RegisterFunctionPointer(this.OnDestroyWindow); + pio.Platform_ShowWindow = this.RegisterFunctionPointer(this.OnShowWindow); + pio.Platform_SetWindowPos = this.RegisterFunctionPointer(this.OnSetWindowPos); + pio.Platform_GetWindowPos = this.RegisterFunctionPointer(this.OnGetWindowPos); + pio.Platform_SetWindowSize = this.RegisterFunctionPointer(this.OnSetWindowSize); + pio.Platform_GetWindowSize = this.RegisterFunctionPointer(this.OnGetWindowSize); + pio.Platform_SetWindowFocus = this.RegisterFunctionPointer(this.OnSetWindowFocus); + pio.Platform_GetWindowFocus = this.RegisterFunctionPointer(this.OnGetWindowFocus); + pio.Platform_GetWindowMinimized = + this.RegisterFunctionPointer(this.OnGetWindowMinimized); + pio.Platform_SetWindowTitle = this.RegisterFunctionPointer(this.OnSetWindowTitle); + pio.Platform_SetWindowAlpha = this.RegisterFunctionPointer(this.OnSetWindowAlpha); + pio.Platform_UpdateWindow = this.RegisterFunctionPointer(this.OnUpdateWindow); + // pio.Platform_SetImeInputPos = this.RegisterFunctionPointer(this.OnSetImeInputPos); + // pio.Platform_GetWindowDpiScale = this.RegisterFunctionPointer(this.OnGetWindowDpiScale); + // pio.Platform_ChangedViewport = this.RegisterFunctionPointer(this.OnChangedViewport); + + var wcex = new WNDCLASSEXW + { + cbSize = (uint)sizeof(WNDCLASSEXW), + style = CS.CS_HREDRAW | CS.CS_VREDRAW, + hInstance = Win32.GetModuleHandleW(null), + hbrBackground = (HBRUSH)(1 + COLOR.COLOR_BACKGROUND), + lpfnWndProc = (delegate* unmanaged)Marshal + .GetFunctionPointerForDelegate(this.input.wndProcDelegate), + lpszClassName = (ushort*)this.classNamePtr, + }; + + if (Win32.RegisterClassExW(&wcex) == 0) + throw Marshal.GetExceptionForHR(Marshal.GetHRForLastWin32Error()) ?? new("RegisterClassEx Fail"); + + // Register main window handle (which is owned by the main application, not by us) + // This is mostly for simplicity and consistency, so that our code (e.g. mouse handling etc.) can use same logic for main and secondary viewports. + var mainViewport = ImGui.GetMainViewport(); + + var data = (ImGuiViewportDataWin32*)Marshal.AllocHGlobal(Marshal.SizeOf()); + mainViewport.PlatformUserData = (nint)data; + data->Hwnd = this.input.hWnd; + data->HwndOwned = false; + mainViewport.PlatformHandle = this.input.hWnd; + } + + public void Dispose() + { + if (this.input is null) + return; + + var pio = ImGui.GetPlatformIO(); + + if (ImGui.GetPlatformIO().NativePtr->Monitors.Data != 0) + { + // We allocated the platform monitor data in OnUpdateMonitors ourselves, + // so we have to free it ourselves to ImGui doesn't try to, or else it will crash + Marshal.FreeHGlobal(ImGui.GetPlatformIO().NativePtr->Monitors.Data); + ImGui.GetPlatformIO().NativePtr->Monitors = default; + } + + if (this.classNamePtr != 0) + { + Win32.UnregisterClassW((ushort*)this.classNamePtr, Win32.GetModuleHandleW(null)); + Marshal.FreeHGlobal(this.classNamePtr); + this.classNamePtr = 0; + } + + pio.Platform_CreateWindow = nint.Zero; + pio.Platform_DestroyWindow = nint.Zero; + pio.Platform_ShowWindow = nint.Zero; + pio.Platform_SetWindowPos = nint.Zero; + pio.Platform_GetWindowPos = nint.Zero; + pio.Platform_SetWindowSize = nint.Zero; + pio.Platform_GetWindowSize = nint.Zero; + pio.Platform_SetWindowFocus = nint.Zero; + pio.Platform_GetWindowFocus = nint.Zero; + pio.Platform_GetWindowMinimized = nint.Zero; + pio.Platform_SetWindowTitle = nint.Zero; + pio.Platform_SetWindowAlpha = nint.Zero; + pio.Platform_UpdateWindow = nint.Zero; + // pio.Platform_SetImeInputPos = nint.Zero; + // pio.Platform_GetWindowDpiScale = nint.Zero; + // pio.Platform_ChangedViewport = nint.Zero; + + this.input = null!; + } + + public void UpdateMonitors() + { + if (!this.wantUpdateMonitors || this.input is null) + return; + + this.wantUpdateMonitors = false; + + // Set up platformIO monitor structures + // Here we use a manual ImVector overload, free the existing monitor data, + // and allocate our own, as we are responsible for telling ImGui about monitors + var pio = ImGui.GetPlatformIO(); + var numMonitors = Win32.GetSystemMetrics(SM.SM_CMONITORS); + var data = Marshal.AllocHGlobal(Marshal.SizeOf() * numMonitors); + if (pio.NativePtr->Monitors.Data != 0) + Marshal.FreeHGlobal(pio.NativePtr->Monitors.Data); + pio.NativePtr->Monitors = new(numMonitors, numMonitors, data); + + // ImGuiPlatformIOPtr platformIO = ImGui.GetPlatformIO(); + // Marshal.FreeHGlobal(platformIO.NativePtr->Monitors.Data); + // int numMonitors = Win32.GetSystemMetrics(Win32.SystemMetric.SM_CMONITORS); + // nint data = Marshal.AllocHGlobal(Marshal.SizeOf() * numMonitors); + // platformIO.NativePtr->Monitors = new ImVector(numMonitors, numMonitors, data); + + var monitorIndex = -1; + var enumfn = new MonitorEnumProcDelegate( + (hMonitor, _, _, _) => + { + monitorIndex++; + var info = new MONITORINFO { cbSize = (uint)sizeof(MONITORINFO) }; + if (!Win32.GetMonitorInfoW(hMonitor, &info)) + return true; + + var monitorLt = new Vector2(info.rcMonitor.left, info.rcMonitor.top); + var monitorRb = new Vector2(info.rcMonitor.right, info.rcMonitor.bottom); + var workLt = new Vector2(info.rcWork.left, info.rcWork.top); + var workRb = new Vector2(info.rcWork.right, info.rcWork.bottom); + // Give ImGui the info for this display + + var imMonitor = ImGui.GetPlatformIO().Monitors[monitorIndex]; + imMonitor.MainPos = monitorLt; + imMonitor.MainSize = monitorRb - monitorLt; + imMonitor.WorkPos = workLt; + imMonitor.WorkSize = workRb - workLt; + imMonitor.DpiScale = 1f; + return true; + }); + Win32.EnumDisplayMonitors( + default, + null, + (delegate* unmanaged)Marshal.GetFunctionPointerForDelegate(enumfn), + default); + } + + private nint RegisterFunctionPointer(T obj) + { + this.delegateReferences.Add(obj); + return Marshal.GetFunctionPointerForDelegate(obj); + } + + private void OnCreateWindow(ImGuiViewportPtr viewport) + { + var data = (ImGuiViewportDataWin32*)Marshal.AllocHGlobal(Marshal.SizeOf()); + viewport.PlatformUserData = (nint)data; + viewport.Flags = + ImGuiViewportFlags.NoTaskBarIcon | + ImGuiViewportFlags.NoFocusOnClick | + ImGuiViewportFlags.NoFocusOnAppearing | + viewport.Flags; + ViewportFlagsToWin32Styles(viewport.Flags, out data->DwStyle, out data->DwExStyle); + + var parentWindow = default(HWND); + if (viewport.ParentViewportId != 0) + { + var parentViewport = ImGui.FindViewportByID(viewport.ParentViewportId); + parentWindow = (HWND)parentViewport.PlatformHandle; + } + + // Create window + var rect = new RECT + { + left = (int)viewport.Pos.X, + top = (int)viewport.Pos.Y, + right = (int)(viewport.Pos.X + viewport.Size.X), + bottom = (int)(viewport.Pos.Y + viewport.Size.Y), + }; + Win32.AdjustWindowRectEx(&rect, (uint)data->DwStyle, false, (uint)data->DwExStyle); + + fixed (char* pwszWindowTitle = "Untitled") + { + data->Hwnd = Win32.CreateWindowExW( + (uint)data->DwExStyle, + (ushort*)this.classNamePtr, + (ushort*)pwszWindowTitle, + (uint)data->DwStyle, + rect.left, + rect.top, + rect.right - rect.left, + rect.bottom - rect.top, + parentWindow, + default, + Win32.GetModuleHandleW(null), + default); + } + + data->HwndOwned = true; + viewport.PlatformRequestResize = false; + viewport.PlatformHandle = viewport.PlatformHandleRaw = data->Hwnd; + } + + private void OnDestroyWindow(ImGuiViewportPtr viewport) + { + // This is also called on the main viewport for some reason, and we never set that viewport's PlatformUserData + if (viewport.PlatformUserData == nint.Zero) return; + + var data = (ImGuiViewportDataWin32*)viewport.PlatformUserData; + + if (Win32.GetCapture() == data->Hwnd) + { + // Transfer capture so if we started dragging from a window that later disappears, we'll still receive the MOUSEUP event. + Win32.ReleaseCapture(); + Win32.SetCapture(this.input.hWnd); + } + + if (data->Hwnd != nint.Zero && data->HwndOwned) + { + var result = Win32.DestroyWindow(data->Hwnd); + if (result == false && Win32.GetLastError() == ERROR.ERROR_ACCESS_DENIED) + { + // We are disposing, and we're doing it from a different thread because of course we are + // Just send the window the close message + Win32.PostMessageW(data->Hwnd, WM.WM_CLOSE, default, default); + } + } + + data->Hwnd = default; + Marshal.FreeHGlobal(viewport.PlatformUserData); + viewport.PlatformUserData = viewport.PlatformHandle = nint.Zero; + } + + private void OnShowWindow(ImGuiViewportPtr viewport) + { + var data = (ImGuiViewportDataWin32*)viewport.PlatformUserData; + + if (viewport.Flags.HasFlag(ImGuiViewportFlags.NoFocusOnAppearing)) + Win32.ShowWindow(data->Hwnd, SW.SW_SHOWNA); + else + Win32.ShowWindow(data->Hwnd, SW.SW_SHOW); + } + + private void OnUpdateWindow(ImGuiViewportPtr viewport) + { + // (Optional) Update Win32 style if it changed _after_ creation. + // Generally they won't change unless configuration flags are changed, but advanced uses (such as manually rewriting viewport flags) make this useful. + var data = (ImGuiViewportDataWin32*)viewport.PlatformUserData; + + viewport.Flags = + ImGuiViewportFlags.NoTaskBarIcon | + ImGuiViewportFlags.NoFocusOnClick | + ImGuiViewportFlags.NoFocusOnAppearing | + viewport.Flags; + ViewportFlagsToWin32Styles(viewport.Flags, out var newStyle, out var newExStyle); + + // Only reapply the flags that have been changed from our point of view (as other flags are being modified by Windows) + if (data->DwStyle != newStyle || data->DwExStyle != newExStyle) + { + // (Optional) Update TopMost state if it changed _after_ creation + var topMostChanged = (data->DwExStyle & WS.WS_EX_TOPMOST) != + (newExStyle & WS.WS_EX_TOPMOST); + + var insertAfter = default(HWND); + if (topMostChanged) + { + insertAfter = viewport.Flags.HasFlag(ImGuiViewportFlags.TopMost) + ? HWND.HWND_TOPMOST + : HWND.HWND_NOTOPMOST; + } + + var swpFlag = topMostChanged ? 0 : SWP.SWP_NOZORDER; + + // Apply flags and position (since it is affected by flags) + data->DwStyle = newStyle; + data->DwExStyle = newExStyle; + + _ = Win32.SetWindowLongW(data->Hwnd, GWL.GWL_STYLE, data->DwStyle); + _ = Win32.SetWindowLongW(data->Hwnd, GWL.GWL_EXSTYLE, data->DwExStyle); + + // Create window + var rect = new RECT + { + left = (int)viewport.Pos.X, + top = (int)viewport.Pos.Y, + right = (int)(viewport.Pos.X + viewport.Size.X), + bottom = (int)(viewport.Pos.Y + viewport.Size.Y), + }; + Win32.AdjustWindowRectEx(&rect, (uint)data->DwStyle, false, (uint)data->DwExStyle); + Win32.SetWindowPos( + data->Hwnd, + insertAfter, + rect.left, + rect.top, + rect.right - rect.left, + rect.bottom - rect.top, + (uint)(swpFlag | SWP.SWP_NOACTIVATE | SWP.SWP_FRAMECHANGED)); + + // This is necessary when we alter the style + Win32.ShowWindow(data->Hwnd, SW.SW_SHOWNA); + viewport.PlatformRequestMove = viewport.PlatformRequestResize = true; + } + } + + private Vector2* OnGetWindowPos(Vector2* returnStorage, ImGuiViewportPtr viewport) + { + var data = (ImGuiViewportDataWin32*)viewport.PlatformUserData; + var pt = new POINT { x = 0, y = 0 }; + Win32.ClientToScreen(data->Hwnd, &pt); + returnStorage->X = pt.x; + returnStorage->Y = pt.y; + return returnStorage; + } + + private void OnSetWindowPos(ImGuiViewportPtr viewport, Vector2 pos) + { + var data = (ImGuiViewportDataWin32*)viewport.PlatformUserData; + var rect = new RECT((int)pos.X, (int)pos.Y, (int)pos.X, (int)pos.Y); + Win32.AdjustWindowRectEx(&rect, (uint)data->DwStyle, false, (uint)data->DwExStyle); + Win32.SetWindowPos( + data->Hwnd, + default, + rect.left, + rect.top, + 0, + 0, + SWP.SWP_NOZORDER | + SWP.SWP_NOSIZE | + SWP.SWP_NOACTIVATE); + } + + private Vector2* OnGetWindowSize(Vector2* returnStorage, ImGuiViewportPtr viewport) + { + var data = (ImGuiViewportDataWin32*)viewport.PlatformUserData; + RECT rect; + Win32.GetClientRect(data->Hwnd, &rect); + returnStorage->X = rect.right - rect.left; + returnStorage->Y = rect.bottom - rect.top; + return returnStorage; + } + + private void OnSetWindowSize(ImGuiViewportPtr viewport, Vector2 size) + { + var data = (ImGuiViewportDataWin32*)viewport.PlatformUserData; + + var rect = new RECT(0, 0, (int)size.X, (int)size.Y); + Win32.AdjustWindowRectEx(&rect, (uint)data->DwStyle, false, (uint)data->DwExStyle); + Win32.SetWindowPos( + data->Hwnd, + default, + 0, + 0, + rect.right - rect.left, + rect.bottom - rect.top, + SWP.SWP_NOZORDER | + SWP.SWP_NOMOVE | + SWP.SWP_NOACTIVATE); + } + + private void OnSetWindowFocus(ImGuiViewportPtr viewport) + { + var data = (ImGuiViewportDataWin32*)viewport.PlatformUserData; + + Win32.BringWindowToTop(data->Hwnd); + Win32.SetForegroundWindow(data->Hwnd); + Win32.SetFocus(data->Hwnd); + } + + private bool OnGetWindowFocus(ImGuiViewportPtr viewport) + { + var data = (ImGuiViewportDataWin32*)viewport.PlatformUserData; + return Win32.GetForegroundWindow() == data->Hwnd; + } + + private bool OnGetWindowMinimized(ImGuiViewportPtr viewport) + { + var data = (ImGuiViewportDataWin32*)viewport.PlatformUserData; + return Win32.IsIconic(data->Hwnd); + } + + private void OnSetWindowTitle(ImGuiViewportPtr viewport, string title) + { + var data = (ImGuiViewportDataWin32*)viewport.PlatformUserData; + fixed (char* pwszTitle = title) + Win32.SetWindowTextW(data->Hwnd, (ushort*)pwszTitle); + } + + private void OnSetWindowAlpha(ImGuiViewportPtr viewport, float alpha) + { + var data = (ImGuiViewportDataWin32*)viewport.PlatformUserData; + var style = Win32.GetWindowLongW(data->Hwnd, GWL.GWL_EXSTYLE); + + alpha = Math.Clamp(alpha, 0f, 1f); + if (alpha < 1.0f) + { + style |= WS.WS_EX_LAYERED; + _ = Win32.SetWindowLongW(data->Hwnd, GWL.GWL_EXSTYLE, style); + } + else + { + style &= ~WS.WS_EX_LAYERED; + _ = Win32.SetWindowLongW(data->Hwnd, GWL.GWL_EXSTYLE, style); + } + + _ = Win32.SetLayeredWindowAttributes(data->Hwnd, 0, (byte)(255 * alpha), LWA.LWA_ALPHA); + } + + // TODO: Decode why IME is miserable + // private void OnSetImeInputPos(ImGuiViewportPtr viewport, Vector2 pos) { + // Win32.COMPOSITIONFORM cs = new Win32.COMPOSITIONFORM( + // 0x20, + // new Win32.POINT( + // (int) (pos.X - viewport.Pos.X), + // (int) (pos.Y - viewport.Pos.Y)), + // new Win32.RECT(0, 0, 0, 0) + // ); + // var hwnd = viewport.PlatformHandle; + // if (hwnd != nint.Zero) { + // var himc = Win32.ImmGetContext(hwnd); + // if (himc != nint.Zero) { + // Win32.ImmSetCompositionWindow(himc, ref cs); + // Win32.ImmReleaseContext(hwnd, himc); + // } + // } + // } + + // Helper structure we store in the void* RenderUserData field of each ImGuiViewport to easily retrieve our backend data-> + private struct ImGuiViewportDataWin32 + { + public HWND Hwnd; + public bool HwndOwned; + public int DwStyle; + public int DwExStyle; + } + } +} diff --git a/Dalamud/ImGuiScene/Shaders/imgui-frag.hlsl.bytes b/Dalamud/ImGuiScene/Shaders/imgui-frag.hlsl.bytes new file mode 100644 index 0000000000..2a9fbf5a36 Binary files /dev/null and b/Dalamud/ImGuiScene/Shaders/imgui-frag.hlsl.bytes differ diff --git a/Dalamud/ImGuiScene/Shaders/imgui-vertex.hlsl.bytes b/Dalamud/ImGuiScene/Shaders/imgui-vertex.hlsl.bytes new file mode 100644 index 0000000000..572a045382 Binary files /dev/null and b/Dalamud/ImGuiScene/Shaders/imgui-vertex.hlsl.bytes differ diff --git a/Dalamud/Interface/Internal/DalamudInterface.cs b/Dalamud/Interface/Internal/DalamudInterface.cs index 63243473de..a7c8f0044c 100644 --- a/Dalamud/Interface/Internal/DalamudInterface.cs +++ b/Dalamud/Interface/Internal/DalamudInterface.cs @@ -819,6 +819,12 @@ private void DrawDevMenu() this.OpenBranchSwitcher(); } + if (ImGui.MenuItem("Use DX12 on DX11 (experimental, restart required)", null, this.configuration.UseDx12Preview)) + { + this.configuration.UseDx12Preview = !this.configuration.UseDx12Preview; + this.configuration.QueueSave(); + } + ImGui.MenuItem(Util.AssemblyVersion, false); ImGui.MenuItem(this.dalamud.StartInfo.GameVersion?.ToString() ?? "Unknown version", false); ImGui.MenuItem($"D: {Util.GetGitHash()}[{Util.GetGitCommitCount()}] CS: {Util.GetGitHashClientStructs()}[{FFXIVClientStructs.ThisAssembly.Git.Commits}]", false); @@ -894,6 +900,7 @@ private void DrawDevMenu() if (ImGui.MenuItem("Show dev bar info", null, this.configuration.ShowDevBarInfo)) { this.configuration.ShowDevBarInfo = !this.configuration.ShowDevBarInfo; + this.configuration.QueueSave(); } ImGui.EndMenu(); diff --git a/Dalamud/Interface/Internal/ImGuiClipboardFunctionProvider.cs b/Dalamud/Interface/Internal/ImGuiClipboardFunctionProvider.cs index 9fa21a31bb..1f9e17bf58 100644 --- a/Dalamud/Interface/Internal/ImGuiClipboardFunctionProvider.cs +++ b/Dalamud/Interface/Internal/ImGuiClipboardFunctionProvider.cs @@ -62,7 +62,7 @@ private ImGuiClipboardFunctionProvider(InterfaceManager.InterfaceManagerWithScen io.SetClipboardTextFn = (nint)(delegate* unmanaged)&StaticSetClipboardTextImpl; io.GetClipboardTextFn = (nint)(delegate* unmanaged)&StaticGetClipboardTextImpl; - this.clipboardData = new(0); + this.clipboardData = new(0, null); return; [UnmanagedCallersOnly] diff --git a/Dalamud/Interface/Internal/InterfaceManager.cs b/Dalamud/Interface/Internal/InterfaceManager.cs index 26b5c8ce27..15a9e5eaaa 100644 --- a/Dalamud/Interface/Internal/InterfaceManager.cs +++ b/Dalamud/Interface/Internal/InterfaceManager.cs @@ -15,6 +15,8 @@ using Dalamud.Game.Internal.DXGI; using Dalamud.Hooking; using Dalamud.Hooking.WndProcHook; +using Dalamud.ImGuiScene; +using Dalamud.ImGuiScene.Implementations; using Dalamud.Interface.ImGuiNotification.Internal; using Dalamud.Interface.Internal.ManagedAsserts; using Dalamud.Interface.ManagedFontAtlas; @@ -28,12 +30,10 @@ using ImGuiNET; -using ImGuiScene; +using TerraFX.Interop.DirectX; +using TerraFX.Interop.Windows; -using PInvoke; - -using SharpDX; -using SharpDX.DXGI; +using static TerraFX.Interop.Windows.Windows; // general dev notes, here because it's easiest @@ -66,10 +66,13 @@ internal class InterfaceManager : IInternalDisposableService public const float DefaultFontSizePx = (DefaultFontSizePt * 4.0f) / 3.0f; private static readonly ModuleLog Log = new("INTERFACE"); - + private readonly ConcurrentBag deferredDisposeTextures = new(); private readonly ConcurrentBag deferredDisposeDisposables = new(); + [ServiceManager.ServiceDependency] + private readonly DalamudConfiguration dalamudConfiguration = Service.Get(); + [ServiceManager.ServiceDependency] private readonly WndProcHookManager wndProcHookManager = Service.Get(); @@ -80,7 +83,8 @@ internal class InterfaceManager : IInternalDisposableService private readonly ConcurrentQueue runAfterImGuiRender = new(); private readonly SwapChainVtableResolver address = new(); - private RawDX11Scene? scene; + private IWin32Scene? scene; + // private Dx12OnDx11Win32Scene? scene; private Hook? setCursorHook; private Hook? presentHook; @@ -92,26 +96,27 @@ internal class InterfaceManager : IInternalDisposableService // can't access imgui IO before first present call private bool lastWantCapture = false; private bool isOverrideGameCursor = true; - private IntPtr gameWindowHandle; + private HWND gameWindowHandle; [ServiceManager.ServiceConstructor] private InterfaceManager() { } - [UnmanagedFunctionPointer(CallingConvention.ThisCall)] - private delegate IntPtr PresentDelegate(IntPtr swapChain, uint syncInterval, uint presentFlags); + [UnmanagedFunctionPointer(CallingConvention.StdCall)] + private unsafe delegate HRESULT PresentDelegate(IDXGISwapChain* swapChain, uint syncInterval, uint presentFlags); - [UnmanagedFunctionPointer(CallingConvention.ThisCall)] - private delegate IntPtr ResizeBuffersDelegate(IntPtr swapChain, uint bufferCount, uint width, uint height, uint newFormat, uint swapChainFlags); + [UnmanagedFunctionPointer(CallingConvention.StdCall)] + private unsafe delegate HRESULT ResizeBuffersDelegate( + IDXGISwapChain* swapChain, uint bufferCount, uint width, uint height, uint newFormat, uint swapChainFlags); [UnmanagedFunctionPointer(CallingConvention.StdCall)] - private delegate IntPtr SetCursorDelegate(IntPtr hCursor); + private delegate HCURSOR SetCursorDelegate(HCURSOR hCursor); /// /// This event gets called each frame to facilitate ImGui drawing. /// - public event RawDX11Scene.BuildUIDelegate? Draw; + public event IImGuiScene.BuildUiDelegate? Draw; /// /// This event gets called when ResizeBuffers is called. @@ -179,17 +184,17 @@ private InterfaceManager() /// /// Gets the DX11 scene. /// - public RawDX11Scene? Scene => this.scene; + public IImGuiScene? Scene => this.scene; /// - /// Gets the D3D11 device instance. + /// Gets the device instance, or 0 if unavailable. /// - public SharpDX.Direct3D11.Device? Device => this.scene?.Device; + public nint Device => this.scene?.DeviceHandle ?? 0; /// - /// Gets the address handle to the main process window. + /// Gets the handle to the main process window, or 0 if unavailable. /// - public IntPtr WindowHandlePtr => this.scene?.WindowHandlePtr ?? IntPtr.Zero; + public nint WindowHandlePtr => this.GameWindowHandle; /// /// Gets or sets a value indicating whether or not the game's cursor should be overridden with the ImGui cursor. @@ -222,20 +227,24 @@ public bool OverrideGameCursor /// /// Gets a value indicating the native handle of the game main window. /// - public IntPtr GameWindowHandle + public unsafe HWND GameWindowHandle { get { if (this.gameWindowHandle == 0) { - nint gwh = 0; - while ((gwh = NativeFunctions.FindWindowEx(0, gwh, "FFXIVGAME", 0)) != 0) + var gwh = default(HWND); + fixed (char* pClass = "FFXIVGAME") { - _ = User32.GetWindowThreadProcessId(gwh, out var pid); - if (pid == Environment.ProcessId && User32.IsWindowVisible(gwh)) + while ((gwh = FindWindowExW(default, gwh, (ushort*)pClass, default)) != default) { - this.gameWindowHandle = gwh; - break; + uint pid; + _ = GetWindowThreadProcessId(gwh, &pid); + if (pid == Environment.ProcessId && IsWindowVisible(gwh)) + { + this.gameWindowHandle = gwh; + break; + } } } } @@ -420,27 +429,27 @@ public Task RunAfterImGuiRender(Func func) /// Get video memory information. /// /// The currently used video memory, or null if not available. - public (long Used, long Available)? GetD3dMemoryInfo() + public unsafe (long Used, long Available)? GetD3dMemoryInfo() { - if (this.Device == null) + if (this.Device == default) return null; - try - { - var dxgiDev = this.Device.QueryInterfaceOrNull(); - var dxgiAdapter = dxgiDev?.Adapter.QueryInterfaceOrNull(); - if (dxgiAdapter == null) - return null; + using var device = default(ComPtr); + using var adapter = default(ComPtr); + using var adapter4 = default(ComPtr); - var memInfo = dxgiAdapter.QueryVideoMemoryInfo(0, MemorySegmentGroup.Local); - return (memInfo.CurrentUsage, memInfo.CurrentReservation); - } - catch - { - // ignored - } + if (new ComPtr((IUnknown*)this.Device).As(&device).FAILED) + return null; - return null; + if (device.Get()->GetAdapter(adapter.GetAddressOf()).FAILED) + return null; + + if (adapter.As(&adapter4).FAILED) + return null; + + var vmi = default(DXGI_QUERY_VIDEO_MEMORY_INFO); + adapter4.Get()->QueryVideoMemoryInfo(0, DXGI_MEMORY_SEGMENT_GROUP.DXGI_MEMORY_SEGMENT_GROUP_LOCAL, &vmi); + return ((long)vmi.CurrentUsage, (long)vmi.CurrentReservation); } /// @@ -449,23 +458,23 @@ public Task RunAfterImGuiRender(Func func) /// public void ClearStacks() { - this.scene?.ClearStacksOnContext(); + ImGuiHelpers.ClearStacksOnContext(); } /// /// Toggle Windows 11 immersive mode on the game window. /// /// Value. - internal void SetImmersiveMode(bool enabled) + internal unsafe void SetImmersiveMode(bool enabled) { if (this.GameWindowHandle == 0) throw new InvalidOperationException("Game window is not yet ready."); - var value = enabled ? 1 : 0; - ((Result)NativeFunctions.DwmSetWindowAttribute( - this.GameWindowHandle, - NativeFunctions.DWMWINDOWATTRIBUTE.DWMWA_USE_IMMERSIVE_DARK_MODE, - ref value, - sizeof(int))).CheckError(); + var value = enabled ? 1u : 0u; + DwmSetWindowAttribute( + this.GameWindowHandle, + (uint)DWMWINDOWATTRIBUTE.DWMWA_USE_IMMERSIVE_DARK_MODE, + &value, + sizeof(int)).ThrowOnError(); } private static InterfaceManager WhenFontsReady() @@ -480,7 +489,7 @@ private static InterfaceManager WhenFontsReady() } [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static void RenderImGui(RawDX11Scene scene) + private static void RenderImGui(IImGuiScene scene) { var conf = Service.Get(); @@ -488,7 +497,7 @@ private static void RenderImGui(RawDX11Scene scene) ImGuiHelpers.NewFrame(); // Enable viewports if there are no issues. - if (conf.IsDisableViewport || scene.SwapChain.IsFullScreen || ImGui.GetPlatformIO().Monitors.Size == 1) + if (conf.IsDisableViewport || scene.IsMainViewportFullScreen() || ImGui.GetPlatformIO().Monitors.Size == 1) ImGui.GetIO().ConfigFlags &= ~ImGuiConfigFlags.ViewportsEnable; else ImGui.GetIO().ConfigFlags |= ImGuiConfigFlags.ViewportsEnable; @@ -496,27 +505,37 @@ private static void RenderImGui(RawDX11Scene scene) scene.Render(); } - private void InitScene(IntPtr swapChain) + private unsafe void InitScene(IDXGISwapChain* swapChain) { - RawDX11Scene newScene; + IWin32Scene newWin32Scene; using (Timings.Start("IM Scene Init")) { try { - newScene = new RawDX11Scene(swapChain); + newWin32Scene = this.dalamudConfiguration.UseDx12Preview + ? new Dx12OnDx11Win32Scene(swapChain) + : new Dx11Win32Scene(swapChain); } catch (DllNotFoundException ex) { Service.ProvideException(ex); Log.Error(ex, "Could not load ImGui dependencies."); - var res = User32.MessageBox( - IntPtr.Zero, - "Dalamud plugins require the Microsoft Visual C++ Redistributable to be installed.\nPlease install the runtime from the official Microsoft website or disable Dalamud.\n\nDo you want to download the redistributable now?", - "Dalamud Error", - User32.MessageBoxOptions.MB_YESNO | User32.MessageBoxOptions.MB_TOPMOST | User32.MessageBoxOptions.MB_ICONERROR); + int res; + fixed (char* msg = + "Dalamud plugins require the Microsoft Visual C++ Redistributable to be installed.\nPlease install the runtime from the official Microsoft website or disable Dalamud.\n\nDo you want to download the redistributable now?") + { + fixed (char* title = "Dalamud Error") + { + res = MessageBoxW( + default, + (ushort*)msg, + (ushort*)title, + MB.MB_YESNO | MB.MB_TOPMOST | MB.MB_ICONERROR); + } + } - if (res == User32.MessageBoxResult.IDYES) + if (res == IDYES) { var psi = new ProcessStartInfo { @@ -533,16 +552,20 @@ private void InitScene(IntPtr swapChain) } var startInfo = Service.Get().StartInfo; - var configuration = Service.Get(); + var configuration = this.dalamudConfiguration; - var iniFileInfo = new FileInfo(Path.Combine(Path.GetDirectoryName(startInfo.ConfigurationPath)!, "dalamudUI.ini")); + var iniFileInfo = new FileInfo( + Path.Combine(Path.GetDirectoryName(startInfo.ConfigurationPath)!, "dalamudUI.ini")); try { if (iniFileInfo.Length > 1200000) { Log.Warning("dalamudUI.ini was over 1mb, deleting"); - iniFileInfo.CopyTo(Path.Combine(iniFileInfo.DirectoryName!, $"dalamudUI-{DateTimeOffset.Now.ToUnixTimeSeconds()}.ini")); + iniFileInfo.CopyTo( + Path.Combine( + iniFileInfo.DirectoryName!, + $"dalamudUI-{DateTimeOffset.Now.ToUnixTimeSeconds()}.ini")); iniFileInfo.Delete(); } } @@ -551,16 +574,18 @@ private void InitScene(IntPtr swapChain) Log.Error(ex, "Could not delete dalamudUI.ini"); } - newScene.UpdateCursor = this.isOverrideGameCursor; - newScene.ImGuiIniPath = iniFileInfo.FullName; - newScene.OnBuildUI += this.Display; - newScene.OnNewInputFrame += this.OnNewInputFrame; + newWin32Scene.UpdateCursor = this.isOverrideGameCursor; + newWin32Scene.IniPath = iniFileInfo.FullName; + newWin32Scene.BuildUi += this.Display; + newWin32Scene.NewInputFrame += this.OnNewInputFrame; StyleModel.TransferOldModels(); - if (configuration.SavedStyles == null || configuration.SavedStyles.All(x => x.Name != StyleModelV1.DalamudStandard.Name)) + if (configuration.SavedStyles == null || + configuration.SavedStyles.All(x => x.Name != StyleModelV1.DalamudStandard.Name)) { - configuration.SavedStyles = new List { StyleModelV1.DalamudStandard, StyleModelV1.DalamudClassic }; + configuration.SavedStyles = new List + { StyleModelV1.DalamudStandard, StyleModelV1.DalamudClassic }; configuration.ChosenStyle = StyleModelV1.DalamudStandard.Name; } else if (configuration.SavedStyles.Count == 1) @@ -616,15 +641,15 @@ private void InitScene(IntPtr swapChain) Log.Information("[IM] Scene & ImGui setup OK!"); } - this.scene = newScene; + this.scene = newWin32Scene; Service.Provide(new(this)); this.wndProcHookManager.PreWndProc += this.WndProcHookManagerOnPreWndProc; } - private unsafe void WndProcHookManagerOnPreWndProc(WndProcEventArgs args) + private void WndProcHookManagerOnPreWndProc(WndProcEventArgs args) { - var r = this.scene?.ProcessWndProcW(args.Hwnd, (User32.WindowMessage)args.Message, args.WParam, args.LParam); + var r = this.scene?.ProcessWndProcW(args.Hwnd, args.Message, args.WParam, args.LParam); if (r is not null) args.SuppressWithValue(r.Value); } @@ -633,22 +658,24 @@ private unsafe void WndProcHookManagerOnPreWndProc(WndProcEventArgs args) * 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 unsafe HRESULT PresentDetour(IDXGISwapChain* swapChain, uint syncInterval, uint presentFlags) { 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 (this.scene != null && swapChain != this.scene.SwapChain.NativePointer) - return this.presentHook!.Original(swapChain, syncInterval, presentFlags); - if (this.scene == null) + if (this.scene is null) + { this.InitScene(swapChain); + if (this.scene is null) + throw new InvalidOperationException("InitScene did not set this.scene?"); + } - Debug.Assert(this.scene is not null, "InitScene did not set the scene field, but did not throw an exception."); + if (!this.scene.IsAttachedToPresentationTarget((nint)swapChain)) + return this.presentHook!.Original(swapChain, syncInterval, presentFlags); - if (!this.dalamudAtlas!.HasBuiltAtlas) + // Do not do anything yet if no font atlas has been built yet. + if (this.dalamudAtlas?.HasBuiltAtlas is not true) { - if (this.dalamudAtlas.BuildTask.Exception != null) + 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"); @@ -658,23 +685,11 @@ private IntPtr PresentDetour(IntPtr swapChain, uint syncInterval, uint presentFl return this.presentHook!.Original(swapChain, syncInterval, presentFlags); } + // Only count Present calls for the main viewport (game window). this.CumulativePresentCalls++; - this.IsMainThreadInPresent = true; - - while (this.runBeforeImGuiRender.TryDequeue(out var action)) - action.InvokeSafely(); - - if (this.address.IsReshade) - { - var pRes = this.presentHook!.Original(swapChain, syncInterval, presentFlags); - - RenderImGui(this.scene!); - this.PostImGuiRender(); - this.IsMainThreadInPresent = false; - - return pRes; - } + this.IsMainThreadInPresent = true; + this.PreImGuiRender(); RenderImGui(this.scene!); this.PostImGuiRender(); this.IsMainThreadInPresent = false; @@ -682,6 +697,12 @@ private IntPtr PresentDetour(IntPtr swapChain, uint syncInterval, uint presentFl return this.presentHook!.Original(swapChain, syncInterval, presentFlags); } + private void PreImGuiRender() + { + while (this.runBeforeImGuiRender.TryDequeue(out var action)) + action.InvokeSafely(); + } + private void PostImGuiRender() { while (this.runAfterImGuiRender.TryDequeue(out var action)) @@ -711,7 +732,7 @@ private void PostImGuiRender() [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) { @@ -731,13 +752,14 @@ private void ContinueConstruction( GlyphMaxAdvanceX = DefaultFontSizePx, }))); this.IconFontFixedWidthHandle = (FontHandle)this.dalamudAtlas.NewDelegateFontHandle( - e => e.OnPreBuild(tk => tk.AddDalamudAssetFont( - DalamudAsset.FontAwesomeFreeSolid, - new() - { - SizePx = Service.Get().DefaultFontSpec.SizePx, - GlyphRanges = new ushort[] { 0x20, 0x20, 0x00 }, - }))); + e => e.OnPreBuild( + tk => tk.AddDalamudAssetFont( + DalamudAsset.FontAwesomeFreeSolid, + new() + { + SizePx = Service.Get().DefaultFontSpec.SizePx, + GlyphRanges = new ushort[] { 0x20, 0x20, 0x00 }, + }))); this.MonoFontHandle = (FontHandle)this.dalamudAtlas.NewDelegateFontHandle( e => e.OnPreBuild( tk => tk.AddDalamudAssetFont( @@ -769,10 +791,7 @@ private void ContinueConstruction( () => { // Update the ImGui default font. - unsafe - { - ImGui.GetIO().NativePtr->FontDefault = fontLocked.ImFont; - } + ImGui.GetIO().NativePtr->FontDefault = fontLocked.ImFont; // Update the reference to the resources of the default font. this.defaultFontResourceLock?.Dispose(); @@ -783,7 +802,7 @@ private void ContinueConstruction( }); }; } - + // This will wait for scene on its own. We just wait for this.dalamudAtlas.BuildTask in this.InitScene. _ = this.dalamudAtlas.BuildFontsAsync(); @@ -799,9 +818,16 @@ private void ContinueConstruction( Log.Error(ex, "Could not enable immersive mode"); } - this.setCursorHook = Hook.FromImport(null, "user32.dll", "SetCursor", 0, this.SetCursorDetour); + this.setCursorHook = Hook.FromImport( + null, + "user32.dll", + "SetCursor", + 0, + this.SetCursorDetour); this.presentHook = Hook.FromAddress(this.address.Present, this.PresentDetour); - this.resizeBuffersHook = Hook.FromAddress(this.address.ResizeBuffers, this.ResizeBuffersDetour); + this.resizeBuffersHook = Hook.FromAddress( + this.address.ResizeBuffers, + this.ResizeBuffersDetour); Log.Verbose("===== S W A P C H A I N ====="); Log.Verbose($"Present address 0x{this.presentHook!.Address.ToInt64():X}"); @@ -812,39 +838,38 @@ private void ContinueConstruction( this.resizeBuffersHook.Enable(); } - private IntPtr ResizeBuffersDetour(IntPtr swapChain, uint bufferCount, uint width, uint height, uint newFormat, uint swapChainFlags) + private unsafe HRESULT ResizeBuffersDetour( + IDXGISwapChain* swapChain, uint bufferCount, uint width, uint height, uint newFormat, uint swapChainFlags) { #if DEBUG - Log.Verbose($"Calling resizebuffers swap@{swapChain.ToInt64():X}{bufferCount} {width} {height} {newFormat} {swapChainFlags}"); + Log.Verbose( + $"Calling resizebuffers swap@{(nint)swapChain: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) + // We have to ensure we're working with the main swapchain, as other viewports might be resizing as well. + if (this.scene?.IsAttachedToPresentationTarget((nint)swapChain) is not true) return this.resizeBuffersHook!.Original(swapChain, bufferCount, width, height, newFormat, swapChainFlags); this.scene?.OnPreResize(); var ret = this.resizeBuffersHook!.Original(swapChain, bufferCount, width, height, newFormat, swapChainFlags); - if (ret.ToInt64() == 0x887A0001) - { + if (ret == DXGI.DXGI_ERROR_INVALID_CALL) Log.Error("invalid call to resizeBuffers"); - } this.scene?.OnPostResize((int)width, (int)height); return ret; } - private IntPtr SetCursorDetour(IntPtr hCursor) + private HCURSOR SetCursorDetour(HCURSOR hCursor) { if (this.lastWantCapture && (!this.scene?.IsImGuiCursor(hCursor) ?? false) && this.OverrideGameCursor) - return IntPtr.Zero; + return default; return this.setCursorHook?.IsDisposed is not false - ? User32.SetCursor(new(hCursor, false)).DangerousGetHandle() + ? SetCursor(hCursor) : this.setCursorHook.Original(hCursor); } diff --git a/Dalamud/Interface/Internal/Windows/ConsoleWindow.cs b/Dalamud/Interface/Internal/Windows/ConsoleWindow.cs index 8f7c0e36c8..49e9527407 100644 --- a/Dalamud/Interface/Internal/Windows/ConsoleWindow.cs +++ b/Dalamud/Interface/Internal/Windows/ConsoleWindow.cs @@ -94,6 +94,7 @@ public ConsoleWindow(DalamudConfiguration configuration) this.autoScroll = configuration.LogAutoScroll; this.autoOpen = configuration.LogOpenAtStartup; + this.newLogEntries = new(); SerilogEventSink.Instance.LogLine += this.OnLogLine; Service.GetAsync().ContinueWith(r => r.Result.Update += this.FrameworkOnUpdate); @@ -114,7 +115,6 @@ public ConsoleWindow(DalamudConfiguration configuration) this.logLinesLimit = configuration.LogLinesLimit; var limit = Math.Max(LogLinesMinimum, this.logLinesLimit); - this.newLogEntries = new(); this.logText = new(limit); this.filteredLogEntries = new(limit); diff --git a/Dalamud/Interface/Internal/Windows/Data/Widgets/TexWidget.cs b/Dalamud/Interface/Internal/Windows/Data/Widgets/TexWidget.cs index 0d2b744b4b..2ed13c27b8 100644 --- a/Dalamud/Interface/Internal/Windows/Data/Widgets/TexWidget.cs +++ b/Dalamud/Interface/Internal/Windows/Data/Widgets/TexWidget.cs @@ -22,6 +22,7 @@ using ImGuiNET; using TerraFX.Interop.DirectX; +using TerraFX.Interop.Windows; using TextureManager = Dalamud.Interface.Textures.Internal.TextureManager; @@ -32,24 +33,8 @@ namespace Dalamud.Interface.Internal.Windows.Data.Widgets; /// internal class TexWidget : IDataWindowWidget { - // TODO: move tracking implementation to PluginStats where applicable, - // and show stats over there instead of TexWidget. - private static readonly Dictionary< - DrawBlameTableColumnUserId, - Func> DrawBlameTableColumnColumnComparers = new() - { - [DrawBlameTableColumnUserId.Plugins] = static x => string.Join(", ", x.OwnerPlugins.Select(y => y.Name)), - [DrawBlameTableColumnUserId.Name] = static x => x.Name, - [DrawBlameTableColumnUserId.Size] = static x => x.RawSpecs.EstimatedBytes, - [DrawBlameTableColumnUserId.Format] = static x => x.Format, - [DrawBlameTableColumnUserId.Width] = static x => x.Width, - [DrawBlameTableColumnUserId.Height] = static x => x.Height, - [DrawBlameTableColumnUserId.NativeAddress] = static x => x.ResourceAddress, - }; - private readonly List addedTextures = new(); - private string allLoadedTexturesTableName = "##table"; private string iconId = "18"; private bool hiRes = true; private bool hq = false; @@ -73,19 +58,6 @@ private static readonly Dictionary< private DXGI_FORMAT[]? supportedRenderTargetFormats; private int renderTargetChoiceInt; - private enum DrawBlameTableColumnUserId - { - NativeAddress, - Actions, - Name, - Width, - Height, - Format, - Size, - Plugins, - ColumnCount, - } - /// public string[]? CommandShortcuts { get; init; } = { "tex", "texture" }; @@ -98,7 +70,6 @@ private enum DrawBlameTableColumnUserId /// public void Load() { - this.allLoadedTexturesTableName = "##table" + Environment.TickCount64; this.addedTextures.AggregateToDisposable().Dispose(); this.addedTextures.Clear(); this.inputTexPath = "ui/loadingimage/-nowloading_base25_hr1.tex"; @@ -141,17 +112,6 @@ public void Draw() conf.QueueSave(); } - var allBlames = this.textureManager.BlameTracker; - lock (allBlames) - { - ImGui.PushID("blames"); - var sizeSum = allBlames.Sum(static x => Math.Max(0, x.RawSpecs.EstimatedBytes)); - if (ImGui.CollapsingHeader( - $"All Loaded Textures: {allBlames.Count:n0} ({Util.FormatBytes(sizeSum)})###header")) - this.DrawBlame(allBlames); - ImGui.PopID(); - } - ImGui.PushID("loadedGameTextures"); if (ImGui.CollapsingHeader( $"Loaded Game Textures: {this.textureManager.Shared.ForDebugGamePathTextures.Count:n0}###header")) @@ -290,17 +250,11 @@ this.textureModificationArgs with { if (t.GetTexture(this.textureManager) is { } source) { - var psrv = (ID3D11ShaderResourceView*)source.ImGuiHandle; - var rcsrv = psrv->AddRef() - 1; - psrv->Release(); - - var pres = default(ID3D11Resource*); - psrv->GetResource(&pres); - var rcres = pres->AddRef() - 1; - pres->Release(); - pres->Release(); + var punk = (IUnknown*)Service.Get().Scene.GetTextureResource(source); + var rc = punk->AddRef() - 1; + punk->Release(); - ImGui.TextUnformatted($"RC: Resource({rcres})/View({rcsrv})"); + ImGui.TextUnformatted($"RC: Resource({rc})"); ImGui.TextUnformatted(source.ToString()); } else @@ -337,174 +291,6 @@ this.textureModificationArgs with runLater?.Invoke(); } - private unsafe void DrawBlame(List allBlames) - { - var im = Service.Get(); - - var shouldSortAgain = ImGui.Button("Sort again"); - - ImGui.SameLine(); - if (ImGui.Button("Reset Columns")) - this.allLoadedTexturesTableName = "##table" + Environment.TickCount64; - - if (!ImGui.BeginTable( - this.allLoadedTexturesTableName, - (int)DrawBlameTableColumnUserId.ColumnCount, - ImGuiTableFlags.Sortable | ImGuiTableFlags.SortTristate | ImGuiTableFlags.SortMulti | - ImGuiTableFlags.Reorderable | ImGuiTableFlags.Resizable | ImGuiTableFlags.NoBordersInBodyUntilResize | - ImGuiTableFlags.NoSavedSettings)) - return; - - const int numIcons = 1; - float iconWidths; - using (im.IconFontHandle?.Push()) - iconWidths = ImGui.CalcTextSize(FontAwesomeIcon.Save.ToIconString()).X; - - ImGui.TableSetupScrollFreeze(0, 1); - ImGui.TableSetupColumn( - "Address", - ImGuiTableColumnFlags.WidthFixed, - ImGui.CalcTextSize("0x7F0000000000").X, - (uint)DrawBlameTableColumnUserId.NativeAddress); - ImGui.TableSetupColumn( - "Actions", - ImGuiTableColumnFlags.WidthFixed | ImGuiTableColumnFlags.NoSort, - iconWidths + - (ImGui.GetStyle().FramePadding.X * 2 * numIcons) + - (ImGui.GetStyle().ItemSpacing.X * 1 * numIcons), - (uint)DrawBlameTableColumnUserId.Actions); - ImGui.TableSetupColumn( - "Name", - ImGuiTableColumnFlags.WidthStretch, - 0f, - (uint)DrawBlameTableColumnUserId.Name); - ImGui.TableSetupColumn( - "Width", - ImGuiTableColumnFlags.WidthFixed, - ImGui.CalcTextSize("000000").X, - (uint)DrawBlameTableColumnUserId.Width); - ImGui.TableSetupColumn( - "Height", - ImGuiTableColumnFlags.WidthFixed, - ImGui.CalcTextSize("000000").X, - (uint)DrawBlameTableColumnUserId.Height); - ImGui.TableSetupColumn( - "Format", - ImGuiTableColumnFlags.WidthFixed, - ImGui.CalcTextSize("R32G32B32A32_TYPELESS").X, - (uint)DrawBlameTableColumnUserId.Format); - ImGui.TableSetupColumn( - "Size", - ImGuiTableColumnFlags.WidthFixed, - ImGui.CalcTextSize("123.45 MB").X, - (uint)DrawBlameTableColumnUserId.Size); - ImGui.TableSetupColumn( - "Plugins", - ImGuiTableColumnFlags.WidthFixed, - ImGui.CalcTextSize("Aaaaaaaaaa Aaaaaaaaaa Aaaaaaaaaa").X, - (uint)DrawBlameTableColumnUserId.Plugins); - ImGui.TableHeadersRow(); - - var sortSpecs = ImGui.TableGetSortSpecs(); - if (sortSpecs.NativePtr is not null && (sortSpecs.SpecsDirty || shouldSortAgain)) - { - allBlames.Sort( - static (a, b) => - { - var sortSpecs = ImGui.TableGetSortSpecs(); - var specs = new Span(sortSpecs.NativePtr->Specs, sortSpecs.SpecsCount); - Span sorted = stackalloc bool[(int)DrawBlameTableColumnUserId.ColumnCount]; - foreach (ref var spec in specs) - { - if (!DrawBlameTableColumnColumnComparers.TryGetValue( - (DrawBlameTableColumnUserId)spec.ColumnUserID, - out var comparableGetter)) - continue; - sorted[(int)spec.ColumnUserID] = true; - var ac = comparableGetter(a); - var bc = comparableGetter(b); - var c = ac.CompareTo(bc); - if (c != 0) - return spec.SortDirection == ImGuiSortDirection.Ascending ? c : -c; - } - - foreach (var (col, comparableGetter) in DrawBlameTableColumnColumnComparers) - { - if (sorted[(int)col]) - continue; - var ac = comparableGetter(a); - var bc = comparableGetter(b); - var c = ac.CompareTo(bc); - if (c != 0) - return c; - } - - return 0; - }); - sortSpecs.SpecsDirty = false; - } - - var clipper = new ImGuiListClipperPtr(ImGuiNative.ImGuiListClipper_ImGuiListClipper()); - clipper.Begin(allBlames.Count); - - while (clipper.Step()) - { - for (var i = clipper.DisplayStart; i < clipper.DisplayEnd; i++) - { - var wrap = allBlames[i]; - ImGui.TableNextRow(); - ImGui.PushID(i); - - ImGui.TableNextColumn(); - ImGui.AlignTextToFramePadding(); - this.TextCopiable($"0x{wrap.ResourceAddress:X}", true, true); - - ImGui.TableNextColumn(); - if (ImGuiComponents.IconButton(FontAwesomeIcon.Save)) - { - _ = Service.Get().ShowTextureSaveMenuAsync( - this.DisplayName, - $"{wrap.ImGuiHandle:X16}", - Task.FromResult(wrap.CreateWrapSharingLowLevelResource())); - } - - if (ImGui.IsItemHovered()) - { - ImGui.BeginTooltip(); - ImGui.Image(wrap.ImGuiHandle, wrap.Size); - ImGui.EndTooltip(); - } - - ImGui.TableNextColumn(); - this.TextCopiable(wrap.Name, false, true); - - ImGui.TableNextColumn(); - this.TextCopiable($"{wrap.Width:n0}", true, true); - - ImGui.TableNextColumn(); - this.TextCopiable($"{wrap.Height:n0}", true, true); - - ImGui.TableNextColumn(); - this.TextCopiable(Enum.GetName(wrap.Format)?[12..] ?? wrap.Format.ToString(), false, true); - - ImGui.TableNextColumn(); - var bytes = wrap.RawSpecs.EstimatedBytes; - this.TextCopiable(bytes < 0 ? "?" : $"{bytes:n0}", true, true); - - ImGui.TableNextColumn(); - lock (wrap.OwnerPlugins) - this.TextCopiable(string.Join(", ", wrap.OwnerPlugins.Select(static x => x.Name)), false, true); - - ImGui.PopID(); - } - } - - clipper.Destroy(); - ImGui.EndTable(); - - ImGuiHelpers.ScaledDummy(10); - } - private unsafe void DrawLoadedTextures(ICollection textures) { var im = Service.Get(); diff --git a/Dalamud/Interface/ManagedFontAtlas/Internals/FontAtlasFactory.BuildToolkit.cs b/Dalamud/Interface/ManagedFontAtlas/Internals/FontAtlasFactory.BuildToolkit.cs index 955b108927..9dad90fe60 100644 --- a/Dalamud/Interface/ManagedFontAtlas/Internals/FontAtlasFactory.BuildToolkit.cs +++ b/Dalamud/Interface/ManagedFontAtlas/Internals/FontAtlasFactory.BuildToolkit.cs @@ -17,8 +17,6 @@ using ImGuiNET; -using SharpDX.DXGI; - using TerraFX.Interop.DirectX; namespace Dalamud.Interface.ManagedFontAtlas.Internals; @@ -710,7 +708,6 @@ public unsafe void UploadTextures() RawImageSpecification.Rgba32(width, height), new(texture.TexPixelsRGBA32, width * height * 4), name); - this.factory.TextureManager.Blame(wrap, this.data.Owner?.OwnerPlugin); this.data.AddExistingTexture(wrap); texture.TexID = wrap.ImGuiHandle; } @@ -752,11 +749,12 @@ public unsafe void UploadTextures() new( width, height, - (int)(use4 ? Format.B4G4R4A4_UNorm : Format.B8G8R8A8_UNorm), + use4 + ? (int)DXGI_FORMAT.DXGI_FORMAT_B4G4R4A4_UNORM + : (int)DXGI_FORMAT.DXGI_FORMAT_R8G8B8A8_UNORM, width * bpp), buf, name); - this.factory.TextureManager.Blame(wrap, this.data.Owner?.OwnerPlugin); this.data.AddExistingTexture(wrap); texture.TexID = wrap.ImGuiHandle; continue; diff --git a/Dalamud/Interface/ManagedFontAtlas/Internals/FontAtlasFactory.Implementation.cs b/Dalamud/Interface/ManagedFontAtlas/Internals/FontAtlasFactory.Implementation.cs index ef92ffd659..029584e4da 100644 --- a/Dalamud/Interface/ManagedFontAtlas/Internals/FontAtlasFactory.Implementation.cs +++ b/Dalamud/Interface/ManagedFontAtlas/Internals/FontAtlasFactory.Implementation.cs @@ -318,8 +318,8 @@ public DalamudFontAtlas( if (this.disposed) return; - r.Result.OnNewRenderFrame += this.ImGuiSceneOnNewRenderFrame; - this.disposables.Add(() => r.Result.OnNewRenderFrame -= this.ImGuiSceneOnNewRenderFrame); + r.Result.NewRenderFrame += this.ImGuiSceneOnNewRenderFrame; + this.disposables.Add(() => r.Result.NewRenderFrame -= this.ImGuiSceneOnNewRenderFrame); } if (this.AutoRebuildMode == FontAtlasAutoRebuildMode.OnNewFrame) diff --git a/Dalamud/Interface/ManagedFontAtlas/Internals/FontAtlasFactory.cs b/Dalamud/Interface/ManagedFontAtlas/Internals/FontAtlasFactory.cs index c084d88e27..c6a81b8675 100644 --- a/Dalamud/Interface/ManagedFontAtlas/Internals/FontAtlasFactory.cs +++ b/Dalamud/Interface/ManagedFontAtlas/Internals/FontAtlasFactory.cs @@ -8,6 +8,7 @@ using Dalamud.Configuration.Internal; using Dalamud.Data; using Dalamud.Game; +using Dalamud.ImGuiScene; using Dalamud.Interface.FontIdentifier; using Dalamud.Interface.GameFonts; using Dalamud.Interface.Internal; @@ -19,8 +20,6 @@ using ImGuiNET; -using ImGuiScene; - using Lumina.Data.Files; using TerraFX.Interop.DirectX; @@ -151,9 +150,9 @@ private FontAtlasFactory( public TextureManager TextureManager => Service.Get(); /// - /// Gets the async task for inside . + /// Gets the async task for inside . /// - public Task SceneTask { get; } + public Task SceneTask { get; } /// /// Gets the default glyph ranges (glyph ranges of ). diff --git a/Dalamud/Interface/Textures/Internal/SharedImmediateTextures/FileSystemSharedImmediateTexture.cs b/Dalamud/Interface/Textures/Internal/SharedImmediateTextures/FileSystemSharedImmediateTexture.cs index 7e5882b515..84cda80827 100644 --- a/Dalamud/Interface/Textures/Internal/SharedImmediateTextures/FileSystemSharedImmediateTexture.cs +++ b/Dalamud/Interface/Textures/Internal/SharedImmediateTextures/FileSystemSharedImmediateTexture.cs @@ -33,8 +33,7 @@ public override string ToString() => protected override async Task CreateTextureAsync(CancellationToken cancellationToken) { var tm = await Service.GetAsync(); - var wrap = await tm.NoThrottleCreateFromFileAsync(this.path, cancellationToken); - tm.BlameSetName(wrap, this.ToString()); + var wrap = await tm.NoThrottleCreateFromFileAsync(this.path, this.ToString(), cancellationToken); return wrap; } } diff --git a/Dalamud/Interface/Textures/Internal/SharedImmediateTextures/GamePathSharedImmediateTexture.cs b/Dalamud/Interface/Textures/Internal/SharedImmediateTextures/GamePathSharedImmediateTexture.cs index 6b3d135882..2bd47ada03 100644 --- a/Dalamud/Interface/Textures/Internal/SharedImmediateTextures/GamePathSharedImmediateTexture.cs +++ b/Dalamud/Interface/Textures/Internal/SharedImmediateTextures/GamePathSharedImmediateTexture.cs @@ -3,7 +3,6 @@ using System.Threading.Tasks; using Dalamud.Data; -using Dalamud.Interface.Internal; using Dalamud.Interface.Textures.TextureWraps; using Lumina.Data.Files; @@ -39,8 +38,7 @@ protected override async Task CreateTextureAsync(Cancellati if (dm.GetFile(substPath) is not { } file) throw new FileNotFoundException(); cancellationToken.ThrowIfCancellationRequested(); - var wrap = tm.NoThrottleCreateFromTexFile(file); - tm.BlameSetName(wrap, this.ToString()); + var wrap = tm.NoThrottleCreateFromTexFile(file, this.ToString()); return wrap; } } diff --git a/Dalamud/Interface/Textures/Internal/SharedImmediateTextures/ManifestResourceSharedImmediateTexture.cs b/Dalamud/Interface/Textures/Internal/SharedImmediateTextures/ManifestResourceSharedImmediateTexture.cs index c95a9b0ad5..8485424c7d 100644 --- a/Dalamud/Interface/Textures/Internal/SharedImmediateTextures/ManifestResourceSharedImmediateTexture.cs +++ b/Dalamud/Interface/Textures/Internal/SharedImmediateTextures/ManifestResourceSharedImmediateTexture.cs @@ -3,7 +3,6 @@ using System.Threading; using System.Threading.Tasks; -using Dalamud.Interface.Internal; using Dalamud.Interface.Textures.TextureWraps; namespace Dalamud.Interface.Textures.Internal.SharedImmediateTextures; @@ -43,8 +42,10 @@ protected override async Task CreateTextureAsync(Cancellati var tm = await Service.GetAsync(); var ms = new MemoryStream(stream.CanSeek ? checked((int)stream.Length) : 0); await stream.CopyToAsync(ms, cancellationToken); - var wrap = tm.NoThrottleCreateFromImage(ms.GetBuffer().AsMemory(0, checked((int)ms.Length)), cancellationToken); - tm.BlameSetName(wrap, this.ToString()); + var wrap = tm.NoThrottleCreateFromImage( + ms.GetBuffer().AsMemory(0, checked((int)ms.Length)), + this.ToString(), + cancellationToken); return wrap; } } diff --git a/Dalamud/Interface/Textures/Internal/SharedImmediateTextures/SharedImmediateTexture.cs b/Dalamud/Interface/Textures/Internal/SharedImmediateTextures/SharedImmediateTexture.cs index c71d83fe86..03de2044de 100644 --- a/Dalamud/Interface/Textures/Internal/SharedImmediateTextures/SharedImmediateTexture.cs +++ b/Dalamud/Interface/Textures/Internal/SharedImmediateTextures/SharedImmediateTexture.cs @@ -1,6 +1,5 @@ using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; -using System.Linq; using System.Runtime.CompilerServices; using System.Threading; using System.Threading.Tasks; @@ -246,16 +245,7 @@ public void AddOwnerPlugin(LocalPlugin plugin) lock (this.ownerPlugins) { if (!this.ownerPlugins.Contains(plugin)) - { this.ownerPlugins.Add(plugin); - this.UnderlyingWrap?.ContinueWith( - r => - { - if (r.IsCompletedSuccessfully) - Service.Get().Blame(r.Result, plugin); - }, - default(CancellationToken)); - } } } @@ -272,31 +262,13 @@ protected void ClearUnderlyingWrap() /// Attempts to restore the reference to this texture. protected void LoadUnderlyingWrap() { - int addLen; lock (this.ownerPlugins) { this.UnderlyingWrap = Service.Get().DynamicPriorityTextureLoader.LoadAsync( this, this.CreateTextureAsync, this.LoadCancellationToken); - - addLen = this.ownerPlugins.Count; } - - if (addLen == 0) - return; - this.UnderlyingWrap.ContinueWith( - r => - { - if (!r.IsCompletedSuccessfully) - return; - lock (this.ownerPlugins) - { - foreach (var op in this.ownerPlugins.Take(addLen)) - Service.Get().Blame(r.Result, op); - } - }, - default(CancellationToken)); } /// Creates the texture immediately. diff --git a/Dalamud/Interface/Textures/Internal/TextureManager.BlameTracker.cs b/Dalamud/Interface/Textures/Internal/TextureManager.BlameTracker.cs deleted file mode 100644 index ffdc17d586..0000000000 --- a/Dalamud/Interface/Textures/Internal/TextureManager.BlameTracker.cs +++ /dev/null @@ -1,431 +0,0 @@ -using System.Collections.Generic; -using System.Runtime.CompilerServices; -using System.Runtime.InteropServices; - -using Dalamud.Interface.Internal; -using Dalamud.Interface.Textures.TextureWraps; -using Dalamud.Plugin.Internal.Types; -using Dalamud.Plugin.Services; -using Dalamud.Storage.Assets; -using Dalamud.Utility; - -using TerraFX.Interop; -using TerraFX.Interop.DirectX; -using TerraFX.Interop.Windows; - -namespace Dalamud.Interface.Textures.Internal; - -/// Service responsible for loading and disposing ImGui texture wraps. -internal sealed partial class TextureManager -{ - /// A wrapper for underlying texture2D resources. - public interface IBlameableDalamudTextureWrap : IDalamudTextureWrap - { - /// Gets the address of the native resource. - public nint ResourceAddress { get; } - - /// Gets the name of the underlying resource of this texture wrap. - public string Name { get; } - - /// Gets the format of the texture. - public DXGI_FORMAT Format { get; } - - /// Gets the list of owner plugins. - public List OwnerPlugins { get; } - - /// Gets the raw image specification. - public RawImageSpecification RawSpecs { get; } - - /// Tests whether the tag and the underlying resource are released or should be released. - /// true if there are no more remaining references to this instance. - bool TestIsReleasedOrShouldRelease(); - } - - /// Gets the list containing all the loaded textures from plugins. - /// Returned value must be used inside a lock. - public List BlameTracker { get; } = new(); - - /// Gets the blame for a texture wrap. - /// The texture wrap. - /// The blame, if it exists. - public unsafe IBlameableDalamudTextureWrap? GetBlame(IDalamudTextureWrap textureWrap) - { - using var wrapAux = new WrapAux(textureWrap, true); - return BlameTag.Get(wrapAux.ResPtr); - } - - /// Puts a plugin on blame for a texture. - /// The texture. - /// The plugin. - /// Same . - public unsafe IDalamudTextureWrap Blame(IDalamudTextureWrap textureWrap, LocalPlugin? ownerPlugin) - { - if (!this.dalamudConfiguration.UseTexturePluginTracking) - return textureWrap; - - try - { - if (textureWrap.ImGuiHandle == nint.Zero) - return textureWrap; - } - catch (ObjectDisposedException) - { - return textureWrap; - } - - using var wrapAux = new WrapAux(textureWrap, true); - var blame = BlameTag.GetOrCreate(wrapAux.ResPtr, out var isNew); - - if (ownerPlugin is not null) - { - lock (blame.OwnerPlugins) - blame.OwnerPlugins.Add(ownerPlugin); - } - - if (isNew) - { - lock (this.BlameTracker) - this.BlameTracker.Add(blame); - } - - return textureWrap; - } - - /// Sets the blame name for a texture. - /// The texture. - /// The name. - /// Same . - public unsafe IDalamudTextureWrap BlameSetName(IDalamudTextureWrap textureWrap, string name) - { - if (!this.dalamudConfiguration.UseTexturePluginTracking) - return textureWrap; - - try - { - if (textureWrap.ImGuiHandle == nint.Zero) - return textureWrap; - } - catch (ObjectDisposedException) - { - return textureWrap; - } - - using var wrapAux = new WrapAux(textureWrap, true); - var blame = BlameTag.GetOrCreate(wrapAux.ResPtr, out var isNew); - blame.Name = name.Length <= 1024 ? name : $"{name[..1024]}..."; - - if (isNew) - { - lock (this.BlameTracker) - this.BlameTracker.Add(blame); - } - - return textureWrap; - } - - private void BlameTrackerUpdate(IFramework unused) - { - lock (this.BlameTracker) - { - for (var i = 0; i < this.BlameTracker.Count;) - { - var entry = this.BlameTracker[i]; - if (entry.TestIsReleasedOrShouldRelease()) - { - this.BlameTracker[i] = this.BlameTracker[^1]; - this.BlameTracker.RemoveAt(this.BlameTracker.Count - 1); - } - else - { - ++i; - } - } - } - } - - /// A COM object that works by tagging itself to a DirectX resource. When the resource destructs, it will - /// also release our instance of the tag, letting us know that it is no longer being used, and can be evicted from - /// our tracker. - [Guid("2c3809e4-4f22-4c50-abde-4f22e5120875")] - private sealed unsafe class BlameTag : IUnknown.Interface, IRefCountable, IBlameableDalamudTextureWrap - { - private static readonly Guid MyGuid = typeof(BlameTag).GUID; - - private readonly nint[] comObject; - private readonly IUnknown.Vtbl vtbl; - private readonly D3D11_TEXTURE2D_DESC desc; - - private ID3D11Texture2D* tex2D; - private GCHandle gchThis; - private GCHandle gchComObject; - private GCHandle gchVtbl; - private int refCount; - - private ComPtr srvDebugPreview; - private long srvDebugPreviewExpiryTick; - - private BlameTag(IUnknown* trackWhat) - { - try - { - fixed (Guid* piid = &IID.IID_ID3D11Texture2D) - fixed (ID3D11Texture2D** ppTex2D = &this.tex2D) - trackWhat->QueryInterface(piid, (void**)ppTex2D).ThrowOnError(); - - fixed (D3D11_TEXTURE2D_DESC* pDesc = &this.desc) - this.tex2D->GetDesc(pDesc); - - this.comObject = new nint[2]; - - this.vtbl.QueryInterface = &QueryInterfaceStatic; - this.vtbl.AddRef = &AddRefStatic; - this.vtbl.Release = &ReleaseStatic; - - this.gchThis = GCHandle.Alloc(this); - this.gchVtbl = GCHandle.Alloc(this.vtbl, GCHandleType.Pinned); - this.gchComObject = GCHandle.Alloc(this.comObject, GCHandleType.Pinned); - this.comObject[0] = this.gchVtbl.AddrOfPinnedObject(); - this.comObject[1] = GCHandle.ToIntPtr(this.gchThis); - this.refCount = 1; - } - catch - { - this.refCount = 0; - if (this.gchComObject.IsAllocated) - this.gchComObject.Free(); - if (this.gchVtbl.IsAllocated) - this.gchVtbl.Free(); - if (this.gchThis.IsAllocated) - this.gchThis.Free(); - this.tex2D->Release(); - throw; - } - - try - { - fixed (Guid* pMyGuid = &MyGuid) - this.tex2D->SetPrivateDataInterface(pMyGuid, this).ThrowOnError(); - } - finally - { - // We don't own this. - this.tex2D->Release(); - - // If the try block above failed, then we will dispose ourselves right away. - // Otherwise, we are transferring our ownership to the device child tagging system. - this.Release(); - } - - return; - - [UnmanagedCallersOnly] - static int QueryInterfaceStatic(IUnknown* pThis, Guid* riid, void** ppvObject) => - ToManagedObject(pThis)?.QueryInterface(riid, ppvObject) ?? E.E_UNEXPECTED; - - [UnmanagedCallersOnly] - static uint AddRefStatic(IUnknown* pThis) => (uint)(ToManagedObject(pThis)?.AddRef() ?? 0); - - [UnmanagedCallersOnly] - static uint ReleaseStatic(IUnknown* pThis) => (uint)(ToManagedObject(pThis)?.Release() ?? 0); - } - - /// - public static Guid* NativeGuid => (Guid*)Unsafe.AsPointer(ref Unsafe.AsRef(in MyGuid)); - - /// - public List OwnerPlugins { get; } = new(); - - /// - public nint ResourceAddress => (nint)this.tex2D; - - /// - public string Name { get; set; } = ""; - - /// - public DXGI_FORMAT Format => this.desc.Format; - - /// - public RawImageSpecification RawSpecs => new( - (int)this.desc.Width, - (int)this.desc.Height, - (int)this.desc.Format, - 0); - - /// - public IntPtr ImGuiHandle - { - get - { - if (this.refCount == 0) - return Service.Get().Empty4X4.ImGuiHandle; - - this.srvDebugPreviewExpiryTick = Environment.TickCount64 + 1000; - if (!this.srvDebugPreview.IsEmpty()) - return (nint)this.srvDebugPreview.Get(); - var srvDesc = new D3D11_SHADER_RESOURCE_VIEW_DESC( - this.tex2D, - D3D_SRV_DIMENSION.D3D11_SRV_DIMENSION_TEXTURE2D); - - using var device = default(ComPtr); - this.tex2D->GetDevice(device.GetAddressOf()); - - using var srv = default(ComPtr); - if (device.Get()->CreateShaderResourceView((ID3D11Resource*)this.tex2D, &srvDesc, srv.GetAddressOf()) - .FAILED) - return Service.Get().Empty4X4.ImGuiHandle; - - srv.Swap(ref this.srvDebugPreview); - return (nint)this.srvDebugPreview.Get(); - } - } - - /// - public int Width => (int)this.desc.Width; - - /// - public int Height => (int)this.desc.Height; - - public static implicit operator IUnknown*(BlameTag bt) => (IUnknown*)bt.gchComObject.AddrOfPinnedObject(); - - /// Gets or creates an instance of for the given resource. - /// The COM object to track. - /// true if the tracker is new. - /// A COM object type. - /// A new instance of . - public static BlameTag GetOrCreate(T* trackWhat, out bool isNew) where T : unmanaged, IUnknown.Interface - { - if (Get(trackWhat) is { } v) - { - isNew = false; - return v; - } - - isNew = true; - return new((IUnknown*)trackWhat); - } - - /// Gets an existing instance of for the given resource. - /// The COM object to track. - /// A COM object type. - /// An existing instance of . - public static BlameTag? Get(T* trackWhat) where T : unmanaged, IUnknown.Interface - { - using var deviceChild = default(ComPtr); - fixed (Guid* piid = &IID.IID_ID3D11DeviceChild) - trackWhat->QueryInterface(piid, (void**)deviceChild.GetAddressOf()).ThrowOnError(); - - fixed (Guid* pMyGuid = &MyGuid) - { - var dataSize = (uint)sizeof(nint); - IUnknown* existingTag; - if (deviceChild.Get()->GetPrivateData(pMyGuid, &dataSize, &existingTag).SUCCEEDED) - { - if (ToManagedObject(existingTag) is { } existingTagInstance) - { - existingTagInstance.Release(); - return existingTagInstance; - } - } - } - - return null; - } - - /// - public bool TestIsReleasedOrShouldRelease() - { - if (this.srvDebugPreviewExpiryTick <= Environment.TickCount64) - this.srvDebugPreview.Reset(); - - return this.refCount == 0; - } - - /// - public HRESULT QueryInterface(Guid* riid, void** ppvObject) - { - if (ppvObject == null) - return E.E_POINTER; - - if (*riid == IID.IID_IUnknown || - *riid == MyGuid) - { - try - { - this.AddRef(); - } - catch - { - return E.E_FAIL; - } - - *ppvObject = (IUnknown*)this; - return S.S_OK; - } - - *ppvObject = null; - return E.E_NOINTERFACE; - } - - /// - public int AddRef() => IRefCountable.AlterRefCount(1, ref this.refCount, out var newRefCount) switch - { - IRefCountable.RefCountResult.StillAlive => newRefCount, - IRefCountable.RefCountResult.AlreadyDisposed => throw new ObjectDisposedException(nameof(BlameTag)), - IRefCountable.RefCountResult.FinalRelease => throw new InvalidOperationException(), - _ => throw new InvalidOperationException(), - }; - - /// - public int Release() - { - switch (IRefCountable.AlterRefCount(-1, ref this.refCount, out var newRefCount)) - { - case IRefCountable.RefCountResult.StillAlive: - return newRefCount; - - case IRefCountable.RefCountResult.FinalRelease: - this.gchThis.Free(); - this.gchComObject.Free(); - this.gchVtbl.Free(); - return newRefCount; - - case IRefCountable.RefCountResult.AlreadyDisposed: - throw new ObjectDisposedException(nameof(BlameTag)); - - default: - throw new InvalidOperationException(); - } - } - - /// - uint IUnknown.Interface.AddRef() - { - try - { - return (uint)this.AddRef(); - } - catch - { - return 0; - } - } - - /// - uint IUnknown.Interface.Release() - { - this.srvDebugPreviewExpiryTick = 0; - try - { - return (uint)this.Release(); - } - catch - { - return 0; - } - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static BlameTag? ToManagedObject(void* pThis) => - GCHandle.FromIntPtr(((nint*)pThis)[1]).Target as BlameTag; - } -} diff --git a/Dalamud/Interface/Textures/Internal/TextureManager.Drawer.cs b/Dalamud/Interface/Textures/Internal/TextureManager.Drawer.cs deleted file mode 100644 index 7fb79311ad..0000000000 --- a/Dalamud/Interface/Textures/Internal/TextureManager.Drawer.cs +++ /dev/null @@ -1,398 +0,0 @@ -using System.Buffers; -using System.Diagnostics.CodeAnalysis; -using System.Numerics; - -using Dalamud.Storage.Assets; -using Dalamud.Utility; - -using ImGuiNET; - -using TerraFX.Interop.DirectX; -using TerraFX.Interop.Windows; - -namespace Dalamud.Interface.Textures.Internal; - -/// Service responsible for loading and disposing ImGui texture wraps. -internal sealed partial class TextureManager -{ - private SimpleDrawerImpl? simpleDrawer; - - /// A class for drawing simple stuff. - [SuppressMessage( - "StyleCop.CSharp.LayoutRules", - "SA1519:Braces should not be omitted from multi-line child statement", - Justification = "Multiple fixed blocks")] - internal sealed unsafe class SimpleDrawerImpl : IDisposable - { - private ComPtr sampler; - private ComPtr vertexShader; - private ComPtr pixelShader; - private ComPtr inputLayout; - private ComPtr vertexConstantBuffer; - private ComPtr blendState; - private ComPtr blendStateForStrippingAlpha; - private ComPtr rasterizerState; - private ComPtr vertexBufferFill; - private ComPtr vertexBufferMutable; - private ComPtr indexBuffer; - - /// Finalizes an instance of the class. - ~SimpleDrawerImpl() => this.Dispose(); - - /// - public void Dispose() - { - this.sampler.Reset(); - this.vertexShader.Reset(); - this.pixelShader.Reset(); - this.inputLayout.Reset(); - this.vertexConstantBuffer.Reset(); - this.blendState.Reset(); - this.blendStateForStrippingAlpha.Reset(); - this.rasterizerState.Reset(); - this.vertexBufferFill.Reset(); - this.vertexBufferMutable.Reset(); - this.indexBuffer.Reset(); - GC.SuppressFinalize(this); - } - - /// Sets up this instance of . - /// The device. - public void Setup(ID3D11Device* device) - { - var assembly = typeof(ImGuiScene.ImGui_Impl_DX11).Assembly; - - // Create the vertex shader - if (this.vertexShader.IsEmpty() || this.inputLayout.IsEmpty()) - { - this.vertexShader.Reset(); - this.inputLayout.Reset(); - - using var stream = assembly.GetManifestResourceStream("imgui-vertex.hlsl.bytes")!; - var array = ArrayPool.Shared.Rent((int)stream.Length); - stream.ReadExactly(array, 0, (int)stream.Length); - fixed (byte* pArray = array) - fixed (ID3D11VertexShader** ppShader = &this.vertexShader.GetPinnableReference()) - fixed (ID3D11InputLayout** ppInputLayout = &this.inputLayout.GetPinnableReference()) - fixed (void* pszPosition = "POSITION"u8) - fixed (void* pszTexCoord = "TEXCOORD"u8) - fixed (void* pszColor = "COLOR"u8) - { - device->CreateVertexShader(pArray, (nuint)stream.Length, null, ppShader).ThrowOnError(); - - var ied = stackalloc D3D11_INPUT_ELEMENT_DESC[] - { - new() - { - SemanticName = (sbyte*)pszPosition, - Format = DXGI_FORMAT.DXGI_FORMAT_R32G32_FLOAT, - AlignedByteOffset = uint.MaxValue, - }, - new() - { - SemanticName = (sbyte*)pszTexCoord, - Format = DXGI_FORMAT.DXGI_FORMAT_R32G32_FLOAT, - AlignedByteOffset = uint.MaxValue, - }, - new() - { - SemanticName = (sbyte*)pszColor, - Format = DXGI_FORMAT.DXGI_FORMAT_R8G8B8A8_UNORM, - AlignedByteOffset = uint.MaxValue, - }, - }; - device->CreateInputLayout(ied, 3, pArray, (nuint)stream.Length, ppInputLayout).ThrowOnError(); - } - - ArrayPool.Shared.Return(array); - } - - // Create the constant buffer - if (this.vertexConstantBuffer.IsEmpty()) - { - var bufferDesc = new D3D11_BUFFER_DESC( - (uint)sizeof(Matrix4x4), - (uint)D3D11_BIND_FLAG.D3D11_BIND_CONSTANT_BUFFER, - D3D11_USAGE.D3D11_USAGE_IMMUTABLE); - var data = Matrix4x4.Identity; - var subr = new D3D11_SUBRESOURCE_DATA { pSysMem = &data }; - fixed (ID3D11Buffer** ppBuffer = &this.vertexConstantBuffer.GetPinnableReference()) - device->CreateBuffer(&bufferDesc, &subr, ppBuffer).ThrowOnError(); - } - - // Create the pixel shader - if (this.pixelShader.IsEmpty()) - { - using var stream = assembly.GetManifestResourceStream("imgui-frag.hlsl.bytes")!; - var array = ArrayPool.Shared.Rent((int)stream.Length); - stream.ReadExactly(array, 0, (int)stream.Length); - fixed (byte* pArray = array) - fixed (ID3D11PixelShader** ppShader = &this.pixelShader.GetPinnableReference()) - device->CreatePixelShader(pArray, (nuint)stream.Length, null, ppShader).ThrowOnError(); - - ArrayPool.Shared.Return(array); - } - - // Create the blending setup - if (this.blendState.IsEmpty()) - { - var blendStateDesc = new D3D11_BLEND_DESC - { - RenderTarget = - { - e0 = - { - BlendEnable = true, - SrcBlend = D3D11_BLEND.D3D11_BLEND_SRC_ALPHA, - DestBlend = D3D11_BLEND.D3D11_BLEND_INV_SRC_ALPHA, - BlendOp = D3D11_BLEND_OP.D3D11_BLEND_OP_ADD, - SrcBlendAlpha = D3D11_BLEND.D3D11_BLEND_INV_DEST_ALPHA, - DestBlendAlpha = D3D11_BLEND.D3D11_BLEND_ONE, - BlendOpAlpha = D3D11_BLEND_OP.D3D11_BLEND_OP_ADD, - RenderTargetWriteMask = (byte)D3D11_COLOR_WRITE_ENABLE.D3D11_COLOR_WRITE_ENABLE_ALL, - }, - }, - }; - fixed (ID3D11BlendState** ppBlendState = &this.blendState.GetPinnableReference()) - device->CreateBlendState(&blendStateDesc, ppBlendState).ThrowOnError(); - } - - if (this.blendStateForStrippingAlpha.IsEmpty()) - { - var blendStateDesc = new D3D11_BLEND_DESC - { - RenderTarget = - { - e0 = - { - BlendEnable = true, - SrcBlend = D3D11_BLEND.D3D11_BLEND_ZERO, - DestBlend = D3D11_BLEND.D3D11_BLEND_ONE, - BlendOp = D3D11_BLEND_OP.D3D11_BLEND_OP_ADD, - SrcBlendAlpha = D3D11_BLEND.D3D11_BLEND_ONE, - DestBlendAlpha = D3D11_BLEND.D3D11_BLEND_ZERO, - BlendOpAlpha = D3D11_BLEND_OP.D3D11_BLEND_OP_ADD, - RenderTargetWriteMask = (byte)D3D11_COLOR_WRITE_ENABLE.D3D11_COLOR_WRITE_ENABLE_ALPHA, - }, - }, - }; - fixed (ID3D11BlendState** ppBlendState = &this.blendStateForStrippingAlpha.GetPinnableReference()) - device->CreateBlendState(&blendStateDesc, ppBlendState).ThrowOnError(); - } - - // Create the rasterizer state - if (this.rasterizerState.IsEmpty()) - { - var rasterizerDesc = new D3D11_RASTERIZER_DESC - { - FillMode = D3D11_FILL_MODE.D3D11_FILL_SOLID, - CullMode = D3D11_CULL_MODE.D3D11_CULL_NONE, - }; - fixed (ID3D11RasterizerState** ppRasterizerState = &this.rasterizerState.GetPinnableReference()) - device->CreateRasterizerState(&rasterizerDesc, ppRasterizerState).ThrowOnError(); - } - - // Create the font sampler - if (this.sampler.IsEmpty()) - { - var samplerDesc = new D3D11_SAMPLER_DESC( - D3D11_FILTER.D3D11_FILTER_MIN_MAG_MIP_LINEAR, - D3D11_TEXTURE_ADDRESS_MODE.D3D11_TEXTURE_ADDRESS_WRAP, - D3D11_TEXTURE_ADDRESS_MODE.D3D11_TEXTURE_ADDRESS_WRAP, - D3D11_TEXTURE_ADDRESS_MODE.D3D11_TEXTURE_ADDRESS_WRAP, - 0f, - 0, - D3D11_COMPARISON_FUNC.D3D11_COMPARISON_ALWAYS, - null, - 0, - 0); - fixed (ID3D11SamplerState** ppSampler = &this.sampler.GetPinnableReference()) - device->CreateSamplerState(&samplerDesc, ppSampler).ThrowOnError(); - } - - if (this.vertexBufferFill.IsEmpty()) - { - var data = stackalloc ImDrawVert[] - { - new() { col = uint.MaxValue, pos = new(-1, 1), uv = new(0, 0) }, - new() { col = uint.MaxValue, pos = new(-1, -1), uv = new(0, 1) }, - new() { col = uint.MaxValue, pos = new(1, 1), uv = new(1, 0) }, - new() { col = uint.MaxValue, pos = new(1, -1), uv = new(1, 1) }, - }; - var desc = new D3D11_BUFFER_DESC( - (uint)(sizeof(ImDrawVert) * 4), - (uint)D3D11_BIND_FLAG.D3D11_BIND_VERTEX_BUFFER, - D3D11_USAGE.D3D11_USAGE_IMMUTABLE); - var subr = new D3D11_SUBRESOURCE_DATA { pSysMem = data }; - var buffer = default(ID3D11Buffer*); - device->CreateBuffer(&desc, &subr, &buffer).ThrowOnError(); - this.vertexBufferFill.Attach(buffer); - } - - if (this.vertexBufferMutable.IsEmpty()) - { - var desc = new D3D11_BUFFER_DESC( - (uint)(sizeof(ImDrawVert) * 4), - (uint)D3D11_BIND_FLAG.D3D11_BIND_VERTEX_BUFFER, - D3D11_USAGE.D3D11_USAGE_DYNAMIC, - (uint)D3D11_CPU_ACCESS_FLAG.D3D11_CPU_ACCESS_WRITE); - var buffer = default(ID3D11Buffer*); - device->CreateBuffer(&desc, null, &buffer).ThrowOnError(); - this.vertexBufferMutable.Attach(buffer); - } - - if (this.indexBuffer.IsEmpty()) - { - var data = stackalloc ushort[] { 0, 1, 2, 1, 2, 3 }; - var desc = new D3D11_BUFFER_DESC( - sizeof(ushort) * 6, - (uint)D3D11_BIND_FLAG.D3D11_BIND_INDEX_BUFFER, - D3D11_USAGE.D3D11_USAGE_IMMUTABLE); - var subr = new D3D11_SUBRESOURCE_DATA { pSysMem = data }; - var buffer = default(ID3D11Buffer*); - device->CreateBuffer(&desc, &subr, &buffer).ThrowOnError(); - this.indexBuffer.Attach(buffer); - } - } - - /// Draws the given shader resource view to the current render target. - /// An instance of . - /// The shader resource view. - /// The left top coordinates relative to the size of the source texture. - /// The right bottom coordinates relative to the size of the source texture. - /// This function does not throw. - public void Draw( - ID3D11DeviceContext* ctx, - ID3D11ShaderResourceView* srv, - Vector2 uv0, - Vector2 uv1) - { - using var rtv = default(ComPtr); - ctx->OMGetRenderTargets(1, rtv.GetAddressOf(), null); - if (rtv.IsEmpty()) - return; - - using var rtvRes = default(ComPtr); - rtv.Get()->GetResource(rtvRes.GetAddressOf()); - - using var rtvTex = default(ComPtr); - if (rtvRes.As(&rtvTex).FAILED) - return; - - D3D11_TEXTURE2D_DESC texDesc; - rtvTex.Get()->GetDesc(&texDesc); - - ID3D11Buffer* buffer; - if (uv0 == Vector2.Zero && uv1 == Vector2.One) - { - buffer = this.vertexBufferFill.Get(); - } - else - { - buffer = this.vertexBufferMutable.Get(); - var mapped = default(D3D11_MAPPED_SUBRESOURCE); - if (ctx->Map((ID3D11Resource*)buffer, 0, D3D11_MAP.D3D11_MAP_WRITE_DISCARD, 0u, &mapped).FAILED) - return; - _ = new Span(mapped.pData, 4) - { - [0] = new() { col = uint.MaxValue, pos = new(-1, 1), uv = uv0 }, - [1] = new() { col = uint.MaxValue, pos = new(-1, -1), uv = new(uv0.X, uv1.Y) }, - [2] = new() { col = uint.MaxValue, pos = new(1, 1), uv = new(uv1.X, uv0.Y) }, - [3] = new() { col = uint.MaxValue, pos = new(1, -1), uv = uv1 }, - }; - ctx->Unmap((ID3D11Resource*)buffer, 0u); - } - - var stride = (uint)sizeof(ImDrawVert); - var offset = 0u; - - ctx->IASetInputLayout(this.inputLayout); - ctx->IASetVertexBuffers(0, 1, &buffer, &stride, &offset); - ctx->IASetIndexBuffer(this.indexBuffer, DXGI_FORMAT.DXGI_FORMAT_R16_UINT, 0); - ctx->IASetPrimitiveTopology(D3D_PRIMITIVE_TOPOLOGY.D3D11_PRIMITIVE_TOPOLOGY_TRIANGLELIST); - - var viewport = new D3D11_VIEWPORT(0, 0, texDesc.Width, texDesc.Height); - ctx->RSSetState(this.rasterizerState); - ctx->RSSetViewports(1, &viewport); - - var blendColor = default(Vector4); - ctx->OMSetBlendState(this.blendState, (float*)&blendColor, 0xffffffff); - ctx->OMSetDepthStencilState(null, 0); - - ctx->VSSetShader(this.vertexShader.Get(), null, 0); - buffer = this.vertexConstantBuffer.Get(); - ctx->VSSetConstantBuffers(0, 1, &buffer); - - ctx->PSSetShader(this.pixelShader, null, 0); - var simp = this.sampler.Get(); - ctx->PSSetSamplers(0, 1, &simp); - ctx->PSSetShaderResources(0, 1, &srv); - - ctx->GSSetShader(null, null, 0); - ctx->HSSetShader(null, null, 0); - ctx->DSSetShader(null, null, 0); - ctx->CSSetShader(null, null, 0); - ctx->DrawIndexed(6, 0, 0); - - var ppn = default(ID3D11ShaderResourceView*); - ctx->PSSetShaderResources(0, 1, &ppn); - } - - /// Fills alpha channel to 1.0 from the current render target. - /// An instance of . - /// This function does not throw. - public void StripAlpha(ID3D11DeviceContext* ctx) - { - using var rtv = default(ComPtr); - ctx->OMGetRenderTargets(1, rtv.GetAddressOf(), null); - if (rtv.IsEmpty()) - return; - - using var rtvRes = default(ComPtr); - rtv.Get()->GetResource(rtvRes.GetAddressOf()); - - using var rtvTex = default(ComPtr); - if (rtvRes.As(&rtvTex).FAILED) - return; - - D3D11_TEXTURE2D_DESC texDesc; - rtvTex.Get()->GetDesc(&texDesc); - - var buffer = this.vertexBufferFill.Get(); - var stride = (uint)sizeof(ImDrawVert); - var offset = 0u; - - ctx->IASetInputLayout(this.inputLayout); - ctx->IASetVertexBuffers(0, 1, &buffer, &stride, &offset); - ctx->IASetIndexBuffer(this.indexBuffer, DXGI_FORMAT.DXGI_FORMAT_R16_UINT, 0); - ctx->IASetPrimitiveTopology(D3D_PRIMITIVE_TOPOLOGY.D3D11_PRIMITIVE_TOPOLOGY_TRIANGLELIST); - - var viewport = new D3D11_VIEWPORT(0, 0, texDesc.Width, texDesc.Height); - ctx->RSSetState(this.rasterizerState); - ctx->RSSetViewports(1, &viewport); - - var blendColor = default(Vector4); - ctx->OMSetBlendState(this.blendStateForStrippingAlpha, (float*)&blendColor, 0xffffffff); - ctx->OMSetDepthStencilState(null, 0); - - ctx->VSSetShader(this.vertexShader.Get(), null, 0); - buffer = this.vertexConstantBuffer.Get(); - ctx->VSSetConstantBuffers(0, 1, &buffer); - - ctx->PSSetShader(this.pixelShader, null, 0); - var simp = this.sampler.Get(); - ctx->PSSetSamplers(0, 1, &simp); - var ppn = (ID3D11ShaderResourceView*)Service.Get().White4X4.ImGuiHandle; - ctx->PSSetShaderResources(0, 1, &ppn); - - ctx->GSSetShader(null, null, 0); - ctx->HSSetShader(null, null, 0); - ctx->DSSetShader(null, null, 0); - ctx->CSSetShader(null, null, 0); - ctx->DrawIndexed(6, 0, 0); - - ppn = default; - ctx->PSSetShaderResources(0, 1, &ppn); - } - } -} diff --git a/Dalamud/Interface/Textures/Internal/TextureManager.FromExistingTexture.cs b/Dalamud/Interface/Textures/Internal/TextureManager.FromExistingTexture.cs index a744114e8d..3101bf3163 100644 --- a/Dalamud/Interface/Textures/Internal/TextureManager.FromExistingTexture.cs +++ b/Dalamud/Interface/Textures/Internal/TextureManager.FromExistingTexture.cs @@ -1,17 +1,13 @@ -using System.Runtime.CompilerServices; +using System.Numerics; using System.Threading; using System.Threading.Tasks; -using Dalamud.Interface.Internal; using Dalamud.Interface.Textures.TextureWraps; -using Dalamud.Interface.Textures.TextureWraps.Internal; using Dalamud.Plugin.Internal.Types; using Dalamud.Plugin.Services; -using Dalamud.Utility; -using Dalamud.Utility.TerraFxCom; +using Dalamud.Storage.Assets; using TerraFX.Interop.DirectX; -using TerraFX.Interop.Windows; namespace Dalamud.Interface.Textures.Internal; @@ -23,7 +19,7 @@ bool ITextureProvider.IsDxgiFormatSupportedForCreateFromExistingTextureAsync(int this.IsDxgiFormatSupportedForCreateFromExistingTextureAsync((DXGI_FORMAT)dxgiFormat); /// - public unsafe bool IsDxgiFormatSupportedForCreateFromExistingTextureAsync(DXGI_FORMAT dxgiFormat) + public bool IsDxgiFormatSupportedForCreateFromExistingTextureAsync(DXGI_FORMAT dxgiFormat) { switch (dxgiFormat) { @@ -37,14 +33,10 @@ public unsafe bool IsDxgiFormatSupportedForCreateFromExistingTextureAsync(DXGI_F return false; } - D3D11_FORMAT_SUPPORT supported; - if (this.device.Get()->CheckFormatSupport(dxgiFormat, (uint*)&supported).FAILED) - return false; - - const D3D11_FORMAT_SUPPORT required = - D3D11_FORMAT_SUPPORT.D3D11_FORMAT_SUPPORT_TEXTURE2D - | D3D11_FORMAT_SUPPORT.D3D11_FORMAT_SUPPORT_RENDER_TARGET; - return (supported & required) == required; + if (this.interfaceManager.Scene is not { } scene) + throw new InvalidOperationException("Not yet ready."); + return scene.SupportsTextureFormat((int)dxgiFormat) && + scene.SupportsTextureFormatForRenderTarget((int)dxgiFormat); } /// @@ -54,34 +46,13 @@ public Task CreateFromExistingTextureAsync( bool leaveWrapOpen = false, string? debugName = null, CancellationToken cancellationToken = default) => - this.DynamicPriorityTextureLoader.LoadAsync( + this.DynamicPriorityTextureLoader.LoadAsync( null, - async _ => - { - // leaveWrapOpen is taken care from calling LoadTextureAsync - using var wrapAux = new WrapAux(wrap, true); - using var tex = await this.NoThrottleCreateFromExistingTextureAsync(wrapAux, args); - - unsafe - { - using var srv = this.device.CreateShaderResourceView( - tex, - new(tex.Get(), D3D_SRV_DIMENSION.D3D11_SRV_DIMENSION_TEXTURE2D)); - - var desc = tex.GetDesc(); - - var outWrap = new UnknownTextureWrap( - (IUnknown*)srv.Get(), - (int)desc.Width, - (int)desc.Height, - true); - this.BlameSetName( - outWrap, - debugName ?? - $"{nameof(this.CreateFromExistingTextureAsync)}({wrap}, {args})"); - return outWrap; - } - }, + _ => this.NoThrottleCreateFromExistingTextureAsync( + wrap, + args, + debugName ?? + $"{nameof(this.CreateFromExistingTextureAsync)}({wrap}, {args})"), cancellationToken, leaveWrapOpen ? null : wrap); @@ -97,13 +68,9 @@ public Task CreateFromImGuiViewportAsync( ImGuiViewportTextureArgs args, LocalPlugin? ownerPlugin, string? debugName = null, - CancellationToken cancellationToken = default) - { - args.ThrowOnInvalidValues(); - var t = new ViewportTextureWrap(args, debugName, ownerPlugin, cancellationToken); - t.QueueUpdate(); - return t.FirstUpdateTask; - } + CancellationToken cancellationToken = default) => + this.interfaceManager.RunAfterImGuiRender( + () => this.Scene.CreateTextureFromImGuiViewport(args, ownerPlugin, debugName, cancellationToken)); /// public async Task<(RawImageSpecification Specification, byte[] RawData)> GetRawImageAsync( @@ -112,230 +79,83 @@ public Task CreateFromImGuiViewportAsync( bool leaveWrapOpen = false, CancellationToken cancellationToken = default) { - using var wrapAux = new WrapAux(wrap, leaveWrapOpen); - return await this.GetRawImageAsync(wrapAux, args, cancellationToken); + using var wrapCloser = leaveWrapOpen ? null : wrap; + return await this.GetRawImageAsync(wrap, args, cancellationToken); } private async Task<(RawImageSpecification Specification, byte[] RawData)> GetRawImageAsync( - WrapAux wrapAux, + IDalamudTextureWrap wrap, TextureModificationArgs args = default, CancellationToken cancellationToken = default) { using var tex2D = - args.IsCompleteSourceCopy(wrapAux.Desc) - ? wrapAux.NewTexRef() - : await this.NoThrottleCreateFromExistingTextureAsync(wrapAux, args); + args.IsCompleteSourceCopy(this.Scene.GetTextureSpecification(wrap)) + ? wrap.CreateWrapSharingLowLevelResource() + : await this.NoThrottleCreateFromExistingTextureAsync(wrap, args); cancellationToken.ThrowIfCancellationRequested(); // ID3D11DeviceContext is not a threadsafe resource, and it must be used from the UI thread. - return await this.RunDuringPresent(() => ExtractMappedResource(tex2D, cancellationToken)); - - static unsafe (RawImageSpecification Specification, byte[] RawData) ExtractMappedResource( - ComPtr tex2D, - CancellationToken cancellationToken) - { - cancellationToken.ThrowIfCancellationRequested(); - - var desc = tex2D.GetDesc(); - - using var device = default(ComPtr); - tex2D.Get()->GetDevice(device.GetAddressOf()); - using var context = default(ComPtr); - device.Get()->GetImmediateContext(context.GetAddressOf()); - - using var tmpTex = - (desc.CPUAccessFlags & (uint)D3D11_CPU_ACCESS_FLAG.D3D11_CPU_ACCESS_READ) == 0 - ? device.CreateTexture2D( - desc with - { - MipLevels = 1, - ArraySize = 1, - SampleDesc = new(1, 0), - Usage = D3D11_USAGE.D3D11_USAGE_STAGING, - BindFlags = 0u, - CPUAccessFlags = (uint)D3D11_CPU_ACCESS_FLAG.D3D11_CPU_ACCESS_READ, - MiscFlags = 0u, - }, - tex2D) - : default; - cancellationToken.ThrowIfCancellationRequested(); - - var mapWhat = (ID3D11Resource*)(tmpTex.IsEmpty() ? tex2D.Get() : tmpTex.Get()); - - D3D11_MAPPED_SUBRESOURCE mapped; - context.Get()->Map(mapWhat, 0, D3D11_MAP.D3D11_MAP_READ, 0, &mapped).ThrowOnError(); - - try - { - var specs = new RawImageSpecification(desc, mapped.RowPitch); - var bytes = new Span(mapped.pData, checked((int)mapped.DepthPitch)).ToArray(); - return (specs, bytes); - } - finally - { - context.Get()->Unmap(mapWhat, 0); - } - } + return await this.RunDuringPresent( + () => + { + var data = this.Scene.GetTextureData(tex2D, out var specs); + return (specs, data); + }); } - private async Task> NoThrottleCreateFromExistingTextureAsync( - WrapAux wrapAux, - TextureModificationArgs args) + private async Task NoThrottleCreateFromExistingTextureAsync( + IDalamudTextureWrap source, + TextureModificationArgs args, + string? debugName = null) { args.ThrowOnInvalidValues(); + var sourceSpecs = this.Scene.GetTextureSpecification(source); if (args.Format == DXGI_FORMAT.DXGI_FORMAT_UNKNOWN) - args = args with { Format = wrapAux.Desc.Format }; + args = args with { Format = sourceSpecs.Format }; if (args.NewWidth == 0) - args = args with { NewWidth = (int)MathF.Round((args.Uv1Effective.X - args.Uv0.X) * wrapAux.Desc.Width) }; + args = args with { NewWidth = (int)MathF.Round((args.Uv1Effective.X - args.Uv0.X) * sourceSpecs.Width) }; if (args.NewHeight == 0) - args = args with { NewHeight = (int)MathF.Round((args.Uv1Effective.Y - args.Uv0.Y) * wrapAux.Desc.Height) }; - - using var tex2DCopyTemp = - this.device.CreateTexture2D( - new() + args = args with { NewHeight = (int)MathF.Round((args.Uv1Effective.Y - args.Uv0.Y) * sourceSpecs.Height) }; + + var tex2DCopyTemp = this.Scene.CreateTexture2D( + default, + new(args.NewWidth, args.NewHeight, args.Format), + false, + false, + true, + debugName ?? $"{nameof(this.NoThrottleCreateFromExistingTextureAsync)}({args})"); + try + { + var dam = await Service.GetAsync(); + await this.RunDuringPresent( + () => { - Width = (uint)args.NewWidth, - Height = (uint)args.NewHeight, - MipLevels = 1, - ArraySize = 1, - Format = args.Format, - SampleDesc = new(1, 0), - Usage = D3D11_USAGE.D3D11_USAGE_DEFAULT, - BindFlags = (uint)(D3D11_BIND_FLAG.D3D11_BIND_SHADER_RESOURCE | - D3D11_BIND_FLAG.D3D11_BIND_RENDER_TARGET), - CPUAccessFlags = 0u, - MiscFlags = 0u, + this.Scene.DrawTextureToTexture( + tex2DCopyTemp, + Vector2.Zero, + Vector2.One, + source, + args.Uv0, + args.Uv1Effective); + if (args.MakeOpaque) + this.Scene.DrawTextureToTexture( + tex2DCopyTemp, + Vector2.Zero, + Vector2.One, + dam.White4X4, + args.Uv0, + args.Uv1Effective, + true); }); - await this.RunDuringPresent(() => DrawSourceTextureToTarget(wrapAux, args, this.SimpleDrawer, tex2DCopyTemp)); - - return new(tex2DCopyTemp); - - static unsafe void DrawSourceTextureToTarget( - WrapAux wrapAux, - TextureModificationArgs args, - SimpleDrawerImpl simpleDrawer, - ComPtr tex2DCopyTemp) - { - using var rtvCopyTemp = default(ComPtr); - var rtvCopyTempDesc = new D3D11_RENDER_TARGET_VIEW_DESC( - tex2DCopyTemp, - D3D11_RTV_DIMENSION.D3D11_RTV_DIMENSION_TEXTURE2D); - wrapAux.DevPtr->CreateRenderTargetView( - (ID3D11Resource*)tex2DCopyTemp.Get(), - &rtvCopyTempDesc, - rtvCopyTemp.GetAddressOf()) - .ThrowOnError(); - - wrapAux.CtxPtr->OMSetRenderTargets(1u, rtvCopyTemp.GetAddressOf(), null); - simpleDrawer.Draw(wrapAux.CtxPtr, wrapAux.SrvPtr, args.Uv0, args.Uv1Effective); - if (args.MakeOpaque) - simpleDrawer.StripAlpha(wrapAux.CtxPtr); - - var dummy = default(ID3D11RenderTargetView*); - wrapAux.CtxPtr->OMSetRenderTargets(1u, &dummy, null); - } - } - - /// Auxiliary data from . - private unsafe struct WrapAux : IDisposable - { - public readonly D3D11_TEXTURE2D_DESC Desc; - - private IDalamudTextureWrap? wrapToClose; - - private ComPtr srv; - private ComPtr res; - private ComPtr tex; - private ComPtr device; - private ComPtr context; - - public WrapAux(IDalamudTextureWrap wrap, bool leaveWrapOpen) - { - this.wrapToClose = leaveWrapOpen ? null : wrap; - - using var unk = new ComPtr((IUnknown*)wrap.ImGuiHandle); - - using var srvTemp = default(ComPtr); - unk.As(&srvTemp).ThrowOnError(); - - using var resTemp = default(ComPtr); - srvTemp.Get()->GetResource(resTemp.GetAddressOf()); - - using var texTemp = default(ComPtr); - resTemp.As(&texTemp).ThrowOnError(); - - using var deviceTemp = default(ComPtr); - texTemp.Get()->GetDevice(deviceTemp.GetAddressOf()); - - using var contextTemp = default(ComPtr); - deviceTemp.Get()->GetImmediateContext(contextTemp.GetAddressOf()); - - fixed (D3D11_TEXTURE2D_DESC* pDesc = &this.Desc) - texTemp.Get()->GetDesc(pDesc); - - srvTemp.Swap(ref this.srv); - resTemp.Swap(ref this.res); - texTemp.Swap(ref this.tex); - deviceTemp.Swap(ref this.device); - contextTemp.Swap(ref this.context); - } - - public ID3D11ShaderResourceView* SrvPtr - { - [MethodImpl(MethodImplOptions.AggressiveInlining)] - get => this.srv.Get(); - } - - public ID3D11Resource* ResPtr - { - [MethodImpl(MethodImplOptions.AggressiveInlining)] - get => this.res.Get(); - } - - public ID3D11Texture2D* TexPtr - { - [MethodImpl(MethodImplOptions.AggressiveInlining)] - get => this.tex.Get(); - } - - public ID3D11Device* DevPtr - { - [MethodImpl(MethodImplOptions.AggressiveInlining)] - get => this.device.Get(); - } - - public ID3D11DeviceContext* CtxPtr - { - [MethodImpl(MethodImplOptions.AggressiveInlining)] - get => this.context.Get(); + return tex2DCopyTemp; } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public ComPtr NewSrvRef() => new(this.srv); - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public ComPtr NewResRef() => new(this.res); - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public ComPtr NewTexRef() => new(this.tex); - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public ComPtr NewDevRef() => new(this.device); - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public ComPtr NewCtxRef() => new(this.context); - - public void Dispose() + catch { - this.srv.Reset(); - this.res.Reset(); - this.tex.Reset(); - this.device.Reset(); - this.context.Reset(); - Interlocked.Exchange(ref this.wrapToClose, null)?.Dispose(); + tex2DCopyTemp.Dispose(); + throw; } } } diff --git a/Dalamud/Interface/Textures/Internal/TextureManager.Wic.cs b/Dalamud/Interface/Textures/Internal/TextureManager.Wic.cs index 245a2a9acd..eba1819622 100644 --- a/Dalamud/Interface/Textures/Internal/TextureManager.Wic.cs +++ b/Dalamud/Interface/Textures/Internal/TextureManager.Wic.cs @@ -6,7 +6,6 @@ using System.Threading; using System.Threading.Tasks; -using Dalamud.Interface.Internal; using Dalamud.Interface.Textures.Internal.SharedImmediateTextures; using Dalamud.Interface.Textures.TextureWraps; using Dalamud.Plugin.Services; @@ -39,20 +38,18 @@ public async Task SaveToStreamAsync( { try { - if (wrap is null) - throw new NullReferenceException($"{nameof(wrap)} cannot be null."); - if (stream is null) - throw new NullReferenceException($"{nameof(stream)} cannot be null."); + ArgumentNullException.ThrowIfNull(wrap); + ArgumentNullException.ThrowIfNull(stream); using var istream = ManagedIStream.Create(stream, true); - using var wrapAux = new WrapAux(wrap, true); + var sourceFormat = this.Scene.GetTextureSpecification(wrap).Format; var dxgiFormat = - WicManager.GetCorrespondingWicPixelFormat(wrapAux.Desc.Format, out _, out _) - ? wrapAux.Desc.Format + WicManager.GetCorrespondingWicPixelFormat(sourceFormat, out _, out _) + ? sourceFormat : DXGI_FORMAT.DXGI_FORMAT_B8G8R8A8_UNORM; - var (specs, bytes) = await this.GetRawImageAsync(wrapAux, new() { Format = dxgiFormat }, cancellationToken) + var (specs, bytes) = await this.GetRawImageAsync(wrap, new() { Format = dxgiFormat }, cancellationToken) .ConfigureAwait(false); await Task.Run( @@ -85,12 +82,9 @@ public async Task SaveToFileAsync( { try { - if (wrap is null) - throw new NullReferenceException($"{nameof(wrap)} cannot be null."); - if (path is null) - throw new NullReferenceException($"{nameof(path)} cannot be null."); + ArgumentNullException.ThrowIfNull(wrap); + ArgumentNullException.ThrowIfNull(path); - using var wrapAux = new WrapAux(wrap, true); var pathTemp = Util.GetTempFileNameForFileReplacement(path); var trashfire = new List(); try @@ -101,13 +95,14 @@ public async Task SaveToFileAsync( FileAccess.Write, FileShare.None)) { + var sourceFormat = this.Scene.GetTextureSpecification(wrap).Format; var dxgiFormat = - WicManager.GetCorrespondingWicPixelFormat(wrapAux.Desc.Format, out _, out _) - ? wrapAux.Desc.Format + WicManager.GetCorrespondingWicPixelFormat(sourceFormat, out _, out _) + ? sourceFormat : DXGI_FORMAT.DXGI_FORMAT_B8G8R8A8_UNORM; var (specs, bytes) = await this.GetRawImageAsync( - wrapAux, + wrap, new() { Format = dxgiFormat }, cancellationToken).ConfigureAwait(false); @@ -167,10 +162,12 @@ IEnumerable ITextureReadbackProvider.GetSupportedImageEncoderI /// Creates a texture from the given bytes of an image file. Skips the load throttler; intended to be used /// from implementation of s. /// The data. + /// Name for debugging. /// The cancellation token. /// The loaded texture. internal IDalamudTextureWrap NoThrottleCreateFromImage( ReadOnlyMemory bytes, + string? debugName = null, CancellationToken cancellationToken = default) { ObjectDisposedException.ThrowIf(this.disposing, this); @@ -180,13 +177,13 @@ internal IDalamudTextureWrap NoThrottleCreateFromImage( { using var handle = bytes.Pin(); using var stream = this.Wic.CreateIStreamViewOfMemory(handle, bytes.Length); - return this.Wic.NoThrottleCreateFromWicStream(stream, cancellationToken); + return this.Wic.NoThrottleCreateFromWicStream(stream, debugName, cancellationToken); } catch (Exception e1) { try { - return this.NoThrottleCreateFromTexFile(bytes.Span); + return this.NoThrottleCreateFromTexFile(bytes.Span, debugName); } catch (Exception e2) { @@ -197,11 +194,13 @@ internal IDalamudTextureWrap NoThrottleCreateFromImage( /// Creates a texture from the given path to an image file. Skips the load throttler; intended to be used /// from implementation of s. - /// The path of the file.. + /// The path of the file. + /// Name for debugging. /// The cancellation token. /// The loaded texture. internal async Task NoThrottleCreateFromFileAsync( string path, + string? debugName = null, CancellationToken cancellationToken = default) { ObjectDisposedException.ThrowIf(this.disposing, this); @@ -214,7 +213,10 @@ internal async Task NoThrottleCreateFromFileAsync( FileMode.Open, FileAccess.Read, FileShare.Read); - return this.Wic.NoThrottleCreateFromWicStream(stream, cancellationToken); + return this.Wic.NoThrottleCreateFromWicStream( + stream, + debugName ?? $"{nameof(this.NoThrottleCreateFromFileAsync)}({path})", + cancellationToken); } catch (Exception e1) { @@ -372,10 +374,12 @@ public unsafe ComPtr CreateIStreamViewOfMemory(MemoryHandle handle, int /// Creates a new instance of from a . /// The stream that will NOT be closed after. + /// Name for debugging. /// The cancellation token. /// The newly loaded texture. public unsafe IDalamudTextureWrap NoThrottleCreateFromWicStream( ComPtr stream, + string? debugName = null, CancellationToken cancellationToken = default) { cancellationToken.ThrowIfCancellationRequested(); @@ -451,7 +455,8 @@ public unsafe IDalamudTextureWrap NoThrottleCreateFromWicStream( bitmapSource.Get()->CopyPixels(null, stride, cbBufferSize, pbData).ThrowOnError(); return this.textureManager.NoThrottleCreateFromRaw( new(rcLock.Width, rcLock.Height, (int)dxgiFormat, (int)stride), - new(pbData, (int)cbBufferSize)); + new(pbData, (int)cbBufferSize), + debugName); } /// Gets the supported bitmap codecs. diff --git a/Dalamud/Interface/Textures/Internal/TextureManager.cs b/Dalamud/Interface/Textures/Internal/TextureManager.cs index c9ee5d20e2..2a9c7f073b 100644 --- a/Dalamud/Interface/Textures/Internal/TextureManager.cs +++ b/Dalamud/Interface/Textures/Internal/TextureManager.cs @@ -3,23 +3,20 @@ using System.Threading; using System.Threading.Tasks; -using Dalamud.Configuration.Internal; using Dalamud.Data; using Dalamud.Game; +using Dalamud.ImGuiScene; using Dalamud.Interface.Internal; using Dalamud.Interface.Textures.Internal.SharedImmediateTextures; using Dalamud.Interface.Textures.TextureWraps; -using Dalamud.Interface.Textures.TextureWraps.Internal; using Dalamud.Logging.Internal; using Dalamud.Plugin.Services; using Dalamud.Utility; -using Dalamud.Utility.TerraFxCom; using Lumina.Data; using Lumina.Data.Files; using TerraFX.Interop.DirectX; -using TerraFX.Interop.Windows; namespace Dalamud.Interface.Textures.Internal; @@ -36,9 +33,6 @@ internal sealed partial class TextureManager [ServiceManager.ServiceDependency] private readonly Dalamud dalamud = Service.Get(); - [ServiceManager.ServiceDependency] - private readonly DalamudConfiguration dalamudConfiguration = Service.Get(); - [ServiceManager.ServiceDependency] private readonly DataManager dataManager = Service.Get(); @@ -52,27 +46,18 @@ internal sealed partial class TextureManager private SharedTextureManager? sharedTextureManager; private WicManager? wicManager; private bool disposing; - private ComPtr device; [ServiceManager.ServiceConstructor] - private unsafe TextureManager(InterfaceManager.InterfaceManagerWithScene withScene) + private TextureManager(InterfaceManager.InterfaceManagerWithScene withScene) { using var failsafe = new DisposeSafety.ScopedFinalizer(); - failsafe.Add(this.device = new((ID3D11Device*)withScene.Manager.Device!.NativePointer)); failsafe.Add(this.dynamicPriorityTextureLoader = new(Math.Max(1, Environment.ProcessorCount - 1))); failsafe.Add(this.sharedTextureManager = new(this)); failsafe.Add(this.wicManager = new(this)); - failsafe.Add(this.simpleDrawer = new()); - this.framework.Update += this.BlameTrackerUpdate; - failsafe.Add(() => this.framework.Update -= this.BlameTrackerUpdate); - this.simpleDrawer.Setup(this.device.Get()); failsafe.Cancel(); } - /// Finalizes an instance of the class. - ~TextureManager() => this.ReleaseUnmanagedResources(); - /// Gets the dynamic-priority queue texture loader. public DynamicPriorityQueueLoader DynamicPriorityTextureLoader { @@ -80,13 +65,6 @@ public DynamicPriorityQueueLoader DynamicPriorityTextureLoader get => this.dynamicPriorityTextureLoader ?? throw new ObjectDisposedException(nameof(TextureManager)); } - /// Gets a simpler drawer. - public SimpleDrawerImpl SimpleDrawer - { - [MethodImpl(MethodImplOptions.AggressiveInlining)] - get => this.simpleDrawer ?? throw new ObjectDisposedException(nameof(TextureManager)); - } - /// Gets the shared texture manager. public SharedTextureManager Shared { @@ -101,6 +79,9 @@ public WicManager Wic get => this.wicManager ?? throw new ObjectDisposedException(nameof(TextureManager)); } + /// Gets the active instance of . + internal IImGuiScene Scene => this.interfaceManager.Scene ?? throw new InvalidOperationException("Not yet ready."); + /// void IInternalDisposableService.DisposeService() { @@ -110,11 +91,8 @@ void IInternalDisposableService.DisposeService() this.disposing = true; Interlocked.Exchange(ref this.dynamicPriorityTextureLoader, null)?.Dispose(); - Interlocked.Exchange(ref this.simpleDrawer, null)?.Dispose(); Interlocked.Exchange(ref this.sharedTextureManager, null)?.Dispose(); Interlocked.Exchange(ref this.wicManager, null)?.Dispose(); - this.ReleaseUnmanagedResources(); - GC.SuppressFinalize(this); } /// @@ -126,10 +104,10 @@ public Task CreateFromImageAsync( null, ct => Task.Run( () => - this.BlameSetName( - this.NoThrottleCreateFromImage(bytes.ToArray(), ct), - debugName ?? - $"{nameof(this.CreateFromImageAsync)}({bytes.Length:n0}b)"), + this.NoThrottleCreateFromImage( + bytes.ToArray(), + debugName ?? $"{nameof(this.CreateFromImageAsync)}({bytes.Length:n0}b)", + ct), ct), cancellationToken); @@ -145,10 +123,10 @@ public Task CreateFromImageAsync( { await using var ms = stream.CanSeek ? new MemoryStream((int)stream.Length) : new(); await stream.CopyToAsync(ms, ct).ConfigureAwait(false); - return this.BlameSetName( - this.NoThrottleCreateFromImage(ms.GetBuffer(), ct), - debugName ?? - $"{nameof(this.CreateFromImageAsync)}(stream)"); + return this.NoThrottleCreateFromImage( + ms.GetBuffer(), + debugName ?? $"{nameof(this.CreateFromImageAsync)}(stream)", + ct); }, cancellationToken, leaveOpen ? null : stream); @@ -159,8 +137,9 @@ public IDalamudTextureWrap CreateFromRaw( RawImageSpecification specs, ReadOnlySpan bytes, string? debugName = null) => - this.BlameSetName( - this.NoThrottleCreateFromRaw(specs, bytes), + this.NoThrottleCreateFromRaw( + specs, + bytes, debugName ?? $"{nameof(this.CreateFromRaw)}({specs}, {bytes.Length:n0})"); /// @@ -172,10 +151,10 @@ public Task CreateFromRawAsync( this.DynamicPriorityTextureLoader.LoadAsync( null, _ => Task.FromResult( - this.BlameSetName( - this.NoThrottleCreateFromRaw(specs, bytes.Span), - debugName ?? - $"{nameof(this.CreateFromRawAsync)}({specs}, {bytes.Length:n0})")), + this.NoThrottleCreateFromRaw( + specs, + bytes.Span, + debugName ?? $"{nameof(this.CreateFromRawAsync)}({specs}, {bytes.Length:n0})")), cancellationToken); /// @@ -191,19 +170,17 @@ public Task CreateFromRawAsync( { await using var ms = stream.CanSeek ? new MemoryStream((int)stream.Length) : new(); await stream.CopyToAsync(ms, ct).ConfigureAwait(false); - return this.BlameSetName( - this.NoThrottleCreateFromRaw(specs, ms.GetBuffer().AsSpan(0, (int)ms.Length)), - debugName ?? - $"{nameof(this.CreateFromRawAsync)}({specs}, stream)"); + return this.NoThrottleCreateFromRaw( + specs, + ms.GetBuffer().AsSpan(0, (int)ms.Length), + debugName ?? $"{nameof(this.CreateFromRawAsync)}({specs}, stream)"); }, cancellationToken, leaveOpen ? null : stream); /// public IDalamudTextureWrap CreateFromTexFile(TexFile file) => - this.BlameSetName( - this.CreateFromTexFileAsync(file).Result, - $"{nameof(this.CreateFromTexFile)}({nameof(file)})"); + this.CreateFromTexFileAsync(file, $"{nameof(this.CreateFromTexFile)}({nameof(file)})").Result; /// public Task CreateFromTexFileAsync( @@ -214,8 +191,8 @@ public Task CreateFromTexFileAsync( return this.DynamicPriorityTextureLoader.LoadAsync( null, _ => Task.FromResult( - this.BlameSetName( - this.NoThrottleCreateFromTexFile(file), + this.NoThrottleCreateFromTexFile( + file, debugName ?? $"{nameof(this.CreateFromTexFile)}({ForceNullable(file.FilePath)?.Path})")), cancellationToken); @@ -223,112 +200,50 @@ public Task CreateFromTexFileAsync( } /// - public unsafe IDalamudTextureWrap CreateEmpty( + public IDalamudTextureWrap CreateEmpty( RawImageSpecification specs, bool cpuRead, bool cpuWrite, - string? debugName = null) - { - if (cpuRead && cpuWrite) - throw new ArgumentException("cpuRead and cpuWrite cannot be set at the same time."); - - var cpuaf = default(D3D11_CPU_ACCESS_FLAG); - if (cpuRead) - cpuaf |= D3D11_CPU_ACCESS_FLAG.D3D11_CPU_ACCESS_READ; - if (cpuWrite) - cpuaf |= D3D11_CPU_ACCESS_FLAG.D3D11_CPU_ACCESS_WRITE; - - D3D11_USAGE usage; - if (cpuRead) - usage = D3D11_USAGE.D3D11_USAGE_STAGING; - else if (cpuWrite) - usage = D3D11_USAGE.D3D11_USAGE_DYNAMIC; - else - usage = D3D11_USAGE.D3D11_USAGE_DEFAULT; - - using var texture = this.device.CreateTexture2D( - new() - { - Width = (uint)specs.Width, - Height = (uint)specs.Height, - MipLevels = 1, - ArraySize = 1, - Format = specs.Format, - SampleDesc = new(1, 0), - Usage = usage, - BindFlags = (uint)D3D11_BIND_FLAG.D3D11_BIND_SHADER_RESOURCE, - CPUAccessFlags = (uint)cpuaf, - MiscFlags = 0, - }); - using var view = this.device.CreateShaderResourceView( - texture, - new(texture, D3D_SRV_DIMENSION.D3D11_SRV_DIMENSION_TEXTURE2D)); - - var wrap = new UnknownTextureWrap((IUnknown*)view.Get(), specs.Width, specs.Height, true); - this.BlameSetName(wrap, debugName ?? $"{nameof(this.CreateEmpty)}({specs})"); - return wrap; - } + string? debugName = null) => + this.Scene.CreateTexture2D( + default, + specs, + cpuRead, + cpuWrite, + true, + debugName ?? $"{nameof(this.CreateEmpty)}({specs})"); /// bool ITextureProvider.IsDxgiFormatSupported(int dxgiFormat) => this.IsDxgiFormatSupported((DXGI_FORMAT)dxgiFormat); /// - public unsafe bool IsDxgiFormatSupported(DXGI_FORMAT dxgiFormat) + public bool IsDxgiFormatSupported(DXGI_FORMAT dxgiFormat) { - D3D11_FORMAT_SUPPORT supported; - if (this.device.Get()->CheckFormatSupport(dxgiFormat, (uint*)&supported).FAILED) - return false; - - const D3D11_FORMAT_SUPPORT required = D3D11_FORMAT_SUPPORT.D3D11_FORMAT_SUPPORT_TEXTURE2D; - return (supported & required) == required; + if (this.interfaceManager.Scene is not { } scene) + throw new InvalidOperationException("Not yet ready."); + return scene.SupportsTextureFormat((int)dxgiFormat); } /// - internal unsafe IDalamudTextureWrap NoThrottleCreateFromRaw( + internal IDalamudTextureWrap NoThrottleCreateFromRaw( RawImageSpecification specs, - ReadOnlySpan bytes) - { - var texd = new D3D11_TEXTURE2D_DESC - { - Width = (uint)specs.Width, - Height = (uint)specs.Height, - MipLevels = 1, - ArraySize = 1, - Format = specs.Format, - SampleDesc = new(1, 0), - Usage = D3D11_USAGE.D3D11_USAGE_IMMUTABLE, - BindFlags = (uint)D3D11_BIND_FLAG.D3D11_BIND_SHADER_RESOURCE, - CPUAccessFlags = 0, - MiscFlags = 0, - }; - using var texture = default(ComPtr); - fixed (void* dataPtr = bytes) - { - var subrdata = new D3D11_SUBRESOURCE_DATA { pSysMem = dataPtr, SysMemPitch = (uint)specs.Pitch }; - this.device.Get()->CreateTexture2D(&texd, &subrdata, texture.GetAddressOf()).ThrowOnError(); - } - - var viewDesc = new D3D11_SHADER_RESOURCE_VIEW_DESC - { - Format = texd.Format, - ViewDimension = D3D_SRV_DIMENSION.D3D11_SRV_DIMENSION_TEXTURE2D, - Texture2D = new() { MipLevels = texd.MipLevels }, - }; - using var view = default(ComPtr); - this.device.Get()->CreateShaderResourceView((ID3D11Resource*)texture.Get(), &viewDesc, view.GetAddressOf()) - .ThrowOnError(); - - var wrap = new UnknownTextureWrap((IUnknown*)view.Get(), specs.Width, specs.Height, true); - this.BlameSetName(wrap, $"{nameof(this.NoThrottleCreateFromRaw)}({specs}, {bytes.Length:n0})"); - return wrap; - } + ReadOnlySpan bytes, + string? debugName = null) => + this.Scene.CreateTexture2D( + bytes, + specs, + false, + false, + false, + debugName ?? $"{nameof(this.NoThrottleCreateFromRaw)}({specs}, {bytes.Length:n0})"); /// Creates a texture from the given . Skips the load throttler; intended to be used /// from implementation of s. /// The data. + /// Name for debugging. /// The loaded texture. - internal IDalamudTextureWrap NoThrottleCreateFromTexFile(TexFile file) + internal IDalamudTextureWrap NoThrottleCreateFromTexFile(TexFile file, string? debugName = null) { ObjectDisposedException.ThrowIf(this.disposing, this); @@ -341,8 +256,10 @@ internal IDalamudTextureWrap NoThrottleCreateFromTexFile(TexFile file) buffer = buffer.Filter(0, 0, TexFile.TextureFormat.B8G8R8A8); } - var wrap = this.NoThrottleCreateFromRaw(new(buffer.Width, buffer.Height, dxgiFormat), buffer.RawData); - this.BlameSetName(wrap, $"{nameof(this.NoThrottleCreateFromTexFile)}({ForceNullable(file.FilePath).Path})"); + var wrap = this.NoThrottleCreateFromRaw( + new(buffer.Width, buffer.Height, dxgiFormat), + buffer.RawData, + debugName ?? $"{nameof(this.NoThrottleCreateFromTexFile)}({ForceNullable(file.FilePath).Path})"); return wrap; static T? ForceNullable(T s) => s; @@ -351,8 +268,9 @@ internal IDalamudTextureWrap NoThrottleCreateFromTexFile(TexFile file) /// Creates a texture from the given , trying to interpret it as a /// . /// The file bytes. + /// Name for debugging. /// The loaded texture. - internal IDalamudTextureWrap NoThrottleCreateFromTexFile(ReadOnlySpan fileBytes) + internal IDalamudTextureWrap NoThrottleCreateFromTexFile(ReadOnlySpan fileBytes, string? debugName = null) { ObjectDisposedException.ThrowIf(this.disposing, this); @@ -363,19 +281,17 @@ internal IDalamudTextureWrap NoThrottleCreateFromTexFile(ReadOnlySpan file var tf = new TexFile(); typeof(TexFile).GetProperty(nameof(tf.Data))!.GetSetMethod(true)!.Invoke( tf, - new object?[] { bytesArray }); + [bytesArray]); typeof(TexFile).GetProperty(nameof(tf.Reader))!.GetSetMethod(true)!.Invoke( tf, - new object?[] { new LuminaBinaryReader(bytesArray) }); + [new LuminaBinaryReader(bytesArray)]); // Note: FileInfo and FilePath are not used from TexFile; skip it. - var wrap = this.NoThrottleCreateFromTexFile(tf); - this.BlameSetName(wrap, $"{nameof(this.NoThrottleCreateFromTexFile)}({fileBytes.Length:n0})"); - return wrap; + return this.NoThrottleCreateFromTexFile( + tf, + debugName ?? $"{nameof(this.NoThrottleCreateFromTexFile)}({fileBytes.Length:n0})"); } - private void ReleaseUnmanagedResources() => this.device.Reset(); - /// Runs the given action in IDXGISwapChain.Present immediately or waiting as needed. /// The action to run. // Not sure why this and the below can't be unconditional RunOnFrameworkThread diff --git a/Dalamud/Interface/Textures/Internal/TextureManagerPluginScoped.cs b/Dalamud/Interface/Textures/Internal/TextureManagerPluginScoped.cs index 27f97168eb..d32d917649 100644 --- a/Dalamud/Interface/Textures/Internal/TextureManagerPluginScoped.cs +++ b/Dalamud/Interface/Textures/Internal/TextureManagerPluginScoped.cs @@ -143,7 +143,6 @@ public IDalamudTextureWrap CreateEmpty( { var manager = this.ManagerOrThrow; var textureWrap = manager.CreateEmpty(specs, cpuRead, cpuWrite, debugName); - manager.Blame(textureWrap, this.plugin); return textureWrap; } @@ -162,7 +161,6 @@ public async Task CreateFromExistingTextureAsync( leaveWrapOpen, debugName, cancellationToken); - manager.Blame(textureWrap, this.plugin); return textureWrap; } @@ -174,7 +172,6 @@ public async Task CreateFromImGuiViewportAsync( { var manager = await this.ManagerTask; var textureWrap = await manager.CreateFromImGuiViewportAsync(args, this.plugin, debugName, cancellationToken); - manager.Blame(textureWrap, this.plugin); return textureWrap; } @@ -186,7 +183,6 @@ public async Task CreateFromImageAsync( { var manager = await this.ManagerTask; var textureWrap = await manager.CreateFromImageAsync(bytes, debugName, cancellationToken); - manager.Blame(textureWrap, this.plugin); return textureWrap; } @@ -199,7 +195,6 @@ public async Task CreateFromImageAsync( { var manager = await this.ManagerTask; var textureWrap = await manager.CreateFromImageAsync(stream, leaveOpen, debugName, cancellationToken); - manager.Blame(textureWrap, this.plugin); return textureWrap; } @@ -211,7 +206,6 @@ public IDalamudTextureWrap CreateFromRaw( { var manager = this.ManagerOrThrow; var textureWrap = manager.CreateFromRaw(specs, bytes, debugName); - manager.Blame(textureWrap, this.plugin); return textureWrap; } @@ -224,7 +218,6 @@ public async Task CreateFromRawAsync( { var manager = await this.ManagerTask; var textureWrap = await manager.CreateFromRawAsync(specs, bytes, debugName, cancellationToken); - manager.Blame(textureWrap, this.plugin); return textureWrap; } @@ -238,7 +231,6 @@ public async Task CreateFromRawAsync( { var manager = await this.ManagerTask; var textureWrap = await manager.CreateFromRawAsync(specs, stream, leaveOpen, debugName, cancellationToken); - manager.Blame(textureWrap, this.plugin); return textureWrap; } @@ -247,7 +239,6 @@ public IDalamudTextureWrap CreateFromTexFile(TexFile file) { var manager = this.ManagerOrThrow; var textureWrap = manager.CreateFromTexFile(file); - manager.Blame(textureWrap, this.plugin); return textureWrap; } @@ -259,7 +250,6 @@ public async Task CreateFromTexFileAsync( { var manager = await this.ManagerTask; var textureWrap = await manager.CreateFromTexFileAsync(file, debugName, cancellationToken); - manager.Blame(textureWrap, this.plugin); return textureWrap; } diff --git a/Dalamud/Interface/Textures/RawImageSpecification.cs b/Dalamud/Interface/Textures/RawImageSpecification.cs index 478d28dc21..84b48a524c 100644 --- a/Dalamud/Interface/Textures/RawImageSpecification.cs +++ b/Dalamud/Interface/Textures/RawImageSpecification.cs @@ -31,6 +31,12 @@ public RawImageSpecification(int width, int height, int dxgiFormat, int pitch = this.DxgiFormat = dxgiFormat; } + /// + internal RawImageSpecification(int width, int height, DXGI_FORMAT dxgiFormat, int pitch = -1) + : this(width, height, (int)dxgiFormat, pitch) + { + } + /// Initializes a new instance of the struct. /// The source texture description. internal RawImageSpecification(in D3D11_TEXTURE2D_DESC desc) @@ -38,6 +44,13 @@ internal RawImageSpecification(in D3D11_TEXTURE2D_DESC desc) { } + /// Initializes a new instance of the struct. + /// The source texture description. + internal RawImageSpecification(in D3D12_RESOURCE_DESC desc) + : this((int)desc.Width, (int)desc.Height, (int)desc.Format) + { + } + /// Initializes a new instance of the struct. /// The source texture description. /// The pitch of the raw image in bytes. diff --git a/Dalamud/Interface/Textures/TextureModificationArgs.cs b/Dalamud/Interface/Textures/TextureModificationArgs.cs index abccca6b50..426c481cef 100644 --- a/Dalamud/Interface/Textures/TextureModificationArgs.cs +++ b/Dalamud/Interface/Textures/TextureModificationArgs.cs @@ -85,16 +85,16 @@ public override string ToString() /// Test if this instance of does not instruct to change the /// underlying data of a texture. - /// The texture description to test against. + /// The texture description to test against. /// true if this instance of does not instruct to /// change the underlying data of a texture. - internal bool IsCompleteSourceCopy(in D3D11_TEXTURE2D_DESC desc) => + internal bool IsCompleteSourceCopy(in RawImageSpecification sourceSpec) => this.Uv0 == Vector2.Zero && this.Uv1 == Vector2.One - && (this.NewWidth == 0 || this.NewWidth == desc.Width) - && (this.NewHeight == 0 || this.NewHeight == desc.Height) + && (this.NewWidth == 0 || this.NewWidth == sourceSpec.Width) + && (this.NewHeight == 0 || this.NewHeight == sourceSpec.Height) && !this.MakeOpaque - && (this.Format == DXGI_FORMAT.DXGI_FORMAT_UNKNOWN || this.Format == desc.Format); + && (this.Format == DXGI_FORMAT.DXGI_FORMAT_UNKNOWN || this.Format == sourceSpec.Format); /// Checks the properties and throws an exception if values are invalid. internal void ThrowOnInvalidValues() diff --git a/Dalamud/Interface/Textures/TextureWraps/ForwardingTextureWrap.cs b/Dalamud/Interface/Textures/TextureWraps/ForwardingTextureWrap.cs index 8b0516e03c..51f9f49d5a 100644 --- a/Dalamud/Interface/Textures/TextureWraps/ForwardingTextureWrap.cs +++ b/Dalamud/Interface/Textures/TextureWraps/ForwardingTextureWrap.cs @@ -2,7 +2,6 @@ using System.Numerics; using System.Runtime.CompilerServices; -using Dalamud.Interface.Internal; using Dalamud.Interface.Textures.TextureWraps.Internal; using TerraFX.Interop.Windows; diff --git a/Dalamud/Interface/Textures/TextureWraps/Internal/DisposeSuppressingTextureWrap.cs b/Dalamud/Interface/Textures/TextureWraps/Internal/DisposeSuppressingTextureWrap.cs index 0dd5c9f253..b418dab052 100644 --- a/Dalamud/Interface/Textures/TextureWraps/Internal/DisposeSuppressingTextureWrap.cs +++ b/Dalamud/Interface/Textures/TextureWraps/Internal/DisposeSuppressingTextureWrap.cs @@ -1,5 +1,3 @@ -using Dalamud.Interface.Internal; - namespace Dalamud.Interface.Textures.TextureWraps.Internal; /// A texture wrap that ignores calls. diff --git a/Dalamud/Interface/Textures/TextureWraps/Internal/UnknownTextureWrap.cs b/Dalamud/Interface/Textures/TextureWraps/Internal/UnknownTextureWrap.cs index ec23d7d031..c5c6dd308d 100644 --- a/Dalamud/Interface/Textures/TextureWraps/Internal/UnknownTextureWrap.cs +++ b/Dalamud/Interface/Textures/TextureWraps/Internal/UnknownTextureWrap.cs @@ -1,7 +1,6 @@ using System.Threading; using Dalamud.Interface.Internal; -using Dalamud.Interface.Textures.Internal; using Dalamud.Utility; using TerraFX.Interop.Windows; @@ -52,8 +51,7 @@ public void Dispose() } /// - public override string ToString() => - $"{nameof(UnknownTextureWrap)}({Service.GetNullable()?.GetBlame(this)?.Name ?? $"{this.imGuiHandle:X}"})"; + public override string ToString() => $"{nameof(UnknownTextureWrap)}({this.imGuiHandle:X})"; /// Actually dispose the wrapped texture. void IDeferredDisposable.RealDispose() diff --git a/Dalamud/Interface/Textures/TextureWraps/Internal/ViewportTextureWrap.cs b/Dalamud/Interface/Textures/TextureWraps/Internal/ViewportTextureWrap.cs deleted file mode 100644 index ad3188925b..0000000000 --- a/Dalamud/Interface/Textures/TextureWraps/Internal/ViewportTextureWrap.cs +++ /dev/null @@ -1,276 +0,0 @@ -using System.Diagnostics; -using System.Threading; -using System.Threading.Tasks; - -using Dalamud.Game; -using Dalamud.Interface.Internal; -using Dalamud.Interface.Textures.Internal; -using Dalamud.Plugin.Internal.Types; -using Dalamud.Storage.Assets; -using Dalamud.Utility; - -using ImGuiNET; - -using TerraFX.Interop.DirectX; -using TerraFX.Interop.Windows; - -using NotSupportedException = System.NotSupportedException; - -namespace Dalamud.Interface.Textures.TextureWraps.Internal; - -/// A texture wrap that takes its buffer from the frame buffer (of swap chain). -internal sealed class ViewportTextureWrap : IDalamudTextureWrap, IDeferredDisposable -{ - private readonly string? debugName; - private readonly LocalPlugin? ownerPlugin; - private readonly CancellationToken cancellationToken; - private readonly TaskCompletionSource firstUpdateTaskCompletionSource = new(); - - private ImGuiViewportTextureArgs args; - private D3D11_TEXTURE2D_DESC desc; - private ComPtr tex; - private ComPtr srv; - private ComPtr rtv; - - private bool disposed; - - /// Initializes a new instance of the class. - /// The arguments for creating a texture. - /// Name for debug display purposes. - /// The owner plugin. - /// The cancellation token. - public ViewportTextureWrap( - ImGuiViewportTextureArgs args, string? debugName, LocalPlugin? ownerPlugin, CancellationToken cancellationToken) - { - this.args = args; - this.debugName = debugName; - this.ownerPlugin = ownerPlugin; - this.cancellationToken = cancellationToken; - } - - /// Finalizes an instance of the class. - ~ViewportTextureWrap() => this.Dispose(false); - - /// - public unsafe nint ImGuiHandle - { - get - { - var t = (nint)this.srv.Get(); - return t == nint.Zero ? Service.Get().Empty4X4.ImGuiHandle : t; - } - } - - /// - public int Width => (int)this.desc.Width; - - /// - public int Height => (int)this.desc.Height; - - /// Gets the task representing the first call. - public Task FirstUpdateTask => this.firstUpdateTaskCompletionSource.Task; - - /// Updates the texture from the source viewport. - public unsafe void Update() - { - if (this.cancellationToken.IsCancellationRequested || this.disposed) - { - this.firstUpdateTaskCompletionSource.TrySetCanceled(); - return; - } - - try - { - ThreadSafety.AssertMainThread(); - - using var backBuffer = GetImGuiViewportBackBuffer(this.args.ViewportId); - D3D11_TEXTURE2D_DESC newDesc; - backBuffer.Get()->GetDesc(&newDesc); - - if (newDesc.SampleDesc.Count > 1) - throw new NotSupportedException("Multisampling is not expected"); - - using var device = default(ComPtr); - backBuffer.Get()->GetDevice(device.GetAddressOf()); - - using var context = default(ComPtr); - device.Get()->GetImmediateContext(context.GetAddressOf()); - - var copyBox = new D3D11_BOX - { - left = (uint)MathF.Round(newDesc.Width * this.args.Uv0.X), - top = (uint)MathF.Round(newDesc.Height * this.args.Uv0.Y), - right = (uint)MathF.Round(newDesc.Width * this.args.Uv1Effective.X), - bottom = (uint)MathF.Round(newDesc.Height * this.args.Uv1Effective.Y), - front = 0, - back = 1, - }; - - if (this.desc.Width != copyBox.right - copyBox.left - || this.desc.Height != copyBox.bottom - copyBox.top - || this.desc.Format != newDesc.Format) - { - var texDesc = new D3D11_TEXTURE2D_DESC - { - Width = copyBox.right - copyBox.left, - Height = copyBox.bottom - copyBox.top, - MipLevels = 1, - ArraySize = 1, - Format = newDesc.Format, - SampleDesc = new(1, 0), - Usage = D3D11_USAGE.D3D11_USAGE_DEFAULT, - BindFlags = (uint)(D3D11_BIND_FLAG.D3D11_BIND_SHADER_RESOURCE | - D3D11_BIND_FLAG.D3D11_BIND_RENDER_TARGET), - CPUAccessFlags = 0u, - MiscFlags = 0u, - }; - - using var texTemp = default(ComPtr); - device.Get()->CreateTexture2D(&texDesc, null, texTemp.GetAddressOf()).ThrowOnError(); - - var rtvDesc = new D3D11_RENDER_TARGET_VIEW_DESC( - texTemp, - D3D11_RTV_DIMENSION.D3D11_RTV_DIMENSION_TEXTURE2D); - using var rtvTemp = default(ComPtr); - device.Get()->CreateRenderTargetView( - (ID3D11Resource*)texTemp.Get(), - &rtvDesc, - rtvTemp.GetAddressOf()).ThrowOnError(); - - var srvDesc = new D3D11_SHADER_RESOURCE_VIEW_DESC( - texTemp, - D3D_SRV_DIMENSION.D3D11_SRV_DIMENSION_TEXTURE2D); - using var srvTemp = default(ComPtr); - device.Get()->CreateShaderResourceView( - (ID3D11Resource*)texTemp.Get(), - &srvDesc, - srvTemp.GetAddressOf()) - .ThrowOnError(); - - this.desc = texDesc; - srvTemp.Swap(ref this.srv); - rtvTemp.Swap(ref this.rtv); - texTemp.Swap(ref this.tex); - - Service.Get().Blame(this, this.ownerPlugin); - Service.Get().BlameSetName( - this, - this.debugName ?? $"{nameof(ViewportTextureWrap)}({this.args})"); - } - - // context.Get()->CopyResource((ID3D11Resource*)this.tex.Get(), (ID3D11Resource*)backBuffer.Get()); - context.Get()->CopySubresourceRegion( - (ID3D11Resource*)this.tex.Get(), - 0, - 0, - 0, - 0, - (ID3D11Resource*)backBuffer.Get(), - 0, - ©Box); - - if (!this.args.KeepTransparency) - { - var rtvLocal = this.rtv.Get(); - context.Get()->OMSetRenderTargets(1u, &rtvLocal, null); - Service.Get().SimpleDrawer.StripAlpha(context.Get()); - - var dummy = default(ID3D11RenderTargetView*); - context.Get()->OMSetRenderTargets(1u, &dummy, null); - } - - this.firstUpdateTaskCompletionSource.TrySetResult(this); - } - catch (Exception e) - { - this.firstUpdateTaskCompletionSource.TrySetException(e); - } - - if (this.args.AutoUpdate) - this.QueueUpdate(); - } - - /// Queues a call to . - public void QueueUpdate() => - Service.Get().RunOnTick( - () => - { - if (this.args.TakeBeforeImGuiRender) - Service.Get().RunBeforeImGuiRender(this.Update); - else - Service.Get().RunAfterImGuiRender(this.Update); - }, - cancellationToken: this.cancellationToken); - - /// Queue the texture to be disposed once the frame ends. - public void Dispose() - { - this.Dispose(true); - GC.SuppressFinalize(this); - } - - /// Actually dispose the wrapped texture. - void IDeferredDisposable.RealDispose() - { - _ = this.FirstUpdateTask.Exception; - this.tex.Reset(); - this.srv.Reset(); - this.rtv.Reset(); - } - - private static unsafe ComPtr GetImGuiViewportBackBuffer(uint viewportId) - { - ThreadSafety.AssertMainThread(); - var viewports = ImGui.GetPlatformIO().Viewports; - var viewportIndex = 0; - for (; viewportIndex < viewports.Size; viewportIndex++) - { - if (viewports[viewportIndex].ID == viewportId) - break; - } - - if (viewportIndex >= viewports.Size) - { - throw new ArgumentOutOfRangeException( - nameof(viewportId), - viewportId, - "Could not find a viewport with the given ID."); - } - - var texture = default(ComPtr); - - Debug.Assert(viewports[0].ID == ImGui.GetMainViewport().ID, "ImGui has changed"); - if (viewportId == viewports[0].ID) - { - var device = FFXIVClientStructs.FFXIV.Client.Graphics.Kernel.Device.Instance(); - fixed (Guid* piid = &IID.IID_ID3D11Texture2D) - { - ((IDXGISwapChain*)device->SwapChain->DXGISwapChain)->GetBuffer(0, piid, (void**)texture.GetAddressOf()) - .ThrowOnError(); - } - } - else - { - // See: ImGui_Impl_DX11.ImGuiViewportDataDx11 - var rud = (nint*)viewports[viewportIndex].RendererUserData; - if (rud == null || rud[0] == nint.Zero || rud[1] == nint.Zero) - throw new InvalidOperationException(); - - using var resource = default(ComPtr); - ((ID3D11RenderTargetView*)rud[1])->GetResource(resource.GetAddressOf()); - resource.As(&texture).ThrowOnError(); - } - - return texture; - } - - private void Dispose(bool disposing) - { - this.disposed = true; - this.args.AutoUpdate = false; - if (disposing) - Service.GetNullable()?.EnqueueDeferredDispose(this); - else - ((IDeferredDisposable)this).RealDispose(); - } -} diff --git a/Dalamud/Interface/UiBuilder.cs b/Dalamud/Interface/UiBuilder.cs index 1413f33471..c9a04d81bb 100644 --- a/Dalamud/Interface/UiBuilder.cs +++ b/Dalamud/Interface/UiBuilder.cs @@ -19,8 +19,6 @@ using Serilog; -using SharpDX.Direct3D11; - namespace Dalamud.Interface; /// @@ -120,7 +118,13 @@ public interface IUiBuilder /// /// Gets the game's active Direct3D device. /// - Device Device { get; } + [Obsolete($"Use your own library of choice, wrapping ${nameof(DevicePtr)}.", true)] + SharpDX.Direct3D11.Device Device { get; } + + /// + /// Gets the game's active Direct3D device. + /// + public nint DevicePtr { get; } /// /// Gets the game's main window handle. @@ -415,10 +419,11 @@ internal UiBuilder(LocalPlugin plugin, string namespaceName) this.InterfaceManagerWithScene?.MonoFontHandle ?? throw new InvalidOperationException("Scene is not yet ready."))); - /// - /// Gets the game's active Direct3D device. - /// - public Device Device => this.InterfaceManagerWithScene!.Device!; + /// + public SharpDX.Direct3D11.Device Device => new(this.DevicePtr); + + /// + public nint DevicePtr => this.InterfaceManagerWithScene.Device; /// /// Gets the game's main window handle. diff --git a/Dalamud/Interface/UldWrapper.cs b/Dalamud/Interface/UldWrapper.cs index 85a2d83447..f45657a40b 100644 --- a/Dalamud/Interface/UldWrapper.cs +++ b/Dalamud/Interface/UldWrapper.cs @@ -7,6 +7,7 @@ using Dalamud.Interface.Textures.Internal; using Dalamud.Interface.Textures.TextureWraps; using Dalamud.Utility; + using Lumina.Data.Files; using Lumina.Data.Parsing.Uld; @@ -40,7 +41,7 @@ public bool Valid /// Load a part of a multi-icon sheet as a texture. /// The path of the requested texture. /// The index of the desired icon. - /// A TextureWrap containing the requested part if it exists and null otherwise. + /// A containing the requested part if it exists and null otherwise. public IDalamudTextureWrap? LoadTexturePart(string texturePath, int part) { if (!this.Valid) diff --git a/Dalamud/Interface/Utility/ImGuiHelpers.cs b/Dalamud/Interface/Utility/ImGuiHelpers.cs index 885f730676..13133bb430 100644 --- a/Dalamud/Interface/Utility/ImGuiHelpers.cs +++ b/Dalamud/Interface/Utility/ImGuiHelpers.cs @@ -9,19 +9,20 @@ using Dalamud.Configuration.Internal; using Dalamud.Game.ClientState.Keys; +using Dalamud.ImGuiScene; +using Dalamud.ImGuiScene.Implementations; using Dalamud.Interface.ManagedFontAtlas; using Dalamud.Interface.ManagedFontAtlas.Internals; using Dalamud.Interface.Utility.Raii; using ImGuiNET; -using ImGuiScene; namespace Dalamud.Interface.Utility; /// /// Class containing various helper methods for use with ImGui inside Dalamud. /// -public static class ImGuiHelpers +public static partial class ImGuiHelpers { /// /// Gets the main viewport. @@ -371,7 +372,7 @@ public static unsafe void CopyGlyphsAcrossFonts( /// The ImGuiKey that corresponds to this VirtualKey, or ImGuiKey.None otherwise. public static ImGuiKey VirtualKeyToImGuiKey(VirtualKey key) { - return ImGui_Input_Impl_Direct.VirtualKeyToImGuiKey((int)key); + return Win32InputHandler.VirtualKeyToImGuiKey((int)key); } /// @@ -381,7 +382,7 @@ public static ImGuiKey VirtualKeyToImGuiKey(VirtualKey key) /// The VirtualKey that corresponds to this ImGuiKey, or VirtualKey.NO_KEY otherwise. public static VirtualKey ImGuiKeyToVirtualKey(ImGuiKey key) { - return (VirtualKey)ImGui_Input_Impl_Direct.ImGuiKeyToVirtualKey(key); + return (VirtualKey)Win32InputHandler.ImGuiKeyToVirtualKey(key); } /// @@ -585,6 +586,12 @@ internal static unsafe int FindViewportId(nint hwnd) return -1; } + /// + /// Clears the stack in the current ImGui context. + /// + [LibraryImport("cimgui", EntryPoint = "igCustom_ClearStacks")] + internal static partial void ClearStacksOnContext(); + /// /// Attempts to validate that is valid. /// diff --git a/Dalamud/Interface/Utility/ImVectorWrapper.cs b/Dalamud/Interface/Utility/ImVectorWrapper.cs index f350a64368..ecdd849248 100644 --- a/Dalamud/Interface/Utility/ImVectorWrapper.cs +++ b/Dalamud/Interface/Utility/ImVectorWrapper.cs @@ -3,6 +3,7 @@ using System.Diagnostics; using System.Linq; using System.Numerics; +using System.Runtime.CompilerServices; using ImGuiNET; @@ -42,6 +43,25 @@ public ImVectorWrapper( this.HasOwnership = ownership; } + /// + /// Initializes a new instance of the struct.
+ /// If is set to true, you must call after use, + /// and the underlying memory for must have been allocated using + /// . Otherwise, it will crash. + ///
+ /// The underlying vector. + /// The destroyer function to call on item removal. + /// Whether this wrapper owns the vector. + public ImVectorWrapper( + ref ImVector vector, + ImGuiNativeDestroyDelegate? destroyer = null, + bool ownership = false) + { + this.vector = (ImVector*)Unsafe.AsPointer(ref vector); + this.destroyer = destroyer; + this.HasOwnership = ownership; + } + /// /// Initializes a new instance of the struct.
/// You must call after use. @@ -291,6 +311,20 @@ public void CopyTo(T[] array, int arrayIndex) Buffer.MemoryCopy(this.DataUnsafe, p, this.LengthUnsafe * sizeof(T), this.LengthUnsafe * sizeof(T)); } + /// + public void CopyTo(Span array) + { + if (array.Length < this.LengthUnsafe) + { + throw new ArgumentException( + "The number of elements in the source ImVectorWrapper is greater than the available space of the destination span.", + nameof(array)); + } + + fixed (void* p = array) + Buffer.MemoryCopy(this.DataUnsafe, p, this.LengthUnsafe * sizeof(T), this.LengthUnsafe * sizeof(T)); + } + /// /// Ensures that the capacity of this list is at least the specified .
/// On growth, the new capacity exactly matches . diff --git a/Dalamud/Plugin/Services/ITextureProvider.cs b/Dalamud/Plugin/Services/ITextureProvider.cs index d75899bd4a..6e8713181b 100644 --- a/Dalamud/Plugin/Services/ITextureProvider.cs +++ b/Dalamud/Plugin/Services/ITextureProvider.cs @@ -5,7 +5,6 @@ using System.Threading; using System.Threading.Tasks; -using Dalamud.Interface.Internal; using Dalamud.Interface.Internal.Windows.Data.Widgets; using Dalamud.Interface.Textures; using Dalamud.Interface.Textures.TextureWraps; diff --git a/Dalamud/Utility/TerraFxCom/TerraFxD3D11Extensions.cs b/Dalamud/Utility/TerraFxCom/TerraFxD3D11Extensions.cs index 967c1eb1b2..b1d4ef79b8 100644 --- a/Dalamud/Utility/TerraFxCom/TerraFxD3D11Extensions.cs +++ b/Dalamud/Utility/TerraFxCom/TerraFxD3D11Extensions.cs @@ -62,4 +62,14 @@ public static unsafe D3D11_TEXTURE2D_DESC GetDesc(this ComPtr t texture.Get()->GetDesc(&desc); return desc; } + + /// Gets the descriptor for a . + /// Texture. + /// Texture descriptor. + public static unsafe D3D11_TEXTURE2D_DESC GetDesc(ref this ID3D11Texture2D texture) + { + var desc = default(D3D11_TEXTURE2D_DESC); + texture.GetDesc(&desc); + return desc; + } } diff --git a/Dalamud/Utility/TexFileExtensions.cs b/Dalamud/Utility/TexFileExtensions.cs index ec8e10b3c2..c157e76f57 100644 --- a/Dalamud/Utility/TexFileExtensions.cs +++ b/Dalamud/Utility/TexFileExtensions.cs @@ -1,10 +1,12 @@ using System.Runtime.CompilerServices; +using Dalamud.ImGuiScene; using Dalamud.Memory; -using ImGuiScene; using Lumina.Data.Files; +using TerraFX.Interop.DirectX; + namespace Dalamud.Utility; /// @@ -13,7 +15,9 @@ namespace Dalamud.Utility; public static class TexFileExtensions { /// - /// Returns the image data formatted for . + /// Returns the image data formatted for , + /// using .
+ /// Consider using with . ///
/// The TexFile to format. /// The formatted image data. diff --git a/Dalamud/Utility/Util.cs b/Dalamud/Utility/Util.cs index a9be332447..890a1c68b8 100644 --- a/Dalamud/Utility/Util.cs +++ b/Dalamud/Utility/Util.cs @@ -52,6 +52,15 @@ public static class Util public static string AssemblyVersion { get; } = Assembly.GetAssembly(typeof(ChatHandlers)).GetName().Version.ToString(); + /// + /// Determines if the given COM interface wrapper is empty. + /// + /// The object. + /// The interface. + /// Whether it is empty. + public static unsafe bool IsEmpty(this ComPtr t) + where T : unmanaged, IUnknown.Interface => t.Get() is null; + /// /// Check two byte arrays for equality. /// @@ -682,6 +691,25 @@ internal static string GetRandomName() return names.ElementAt(rng.Next(0, names.Count() - 1)).Singular.RawString; } + /// + /// Debug purpose: show a message box so that you can attach a debugger. + /// + /// The reason. + internal static unsafe void ShowMessageBoxAsBreakpoint(string reason = "") + { + fixed (void* p = reason) + { + fixed (void* title = "Dalamud") + { + _ = TerraFX.Interop.Windows.Windows.MessageBoxW( + default, + (ushort*)p, + (ushort*)title, + MB.MB_OK); + } + } + } + /// /// Throws a corresponding exception if is true. /// diff --git a/Dalamud/Utility/VectorExtensions.cs b/Dalamud/Utility/VectorExtensions.cs index f617c8420f..f1bc65322c 100644 --- a/Dalamud/Utility/VectorExtensions.cs +++ b/Dalamud/Utility/VectorExtensions.cs @@ -5,6 +5,7 @@ namespace Dalamud.Utility; /// /// Extension methods for System.Numerics.VectorN and SharpDX.VectorN. /// +[Obsolete("Use your own vector extensions. You might as well copy this file.", true)] public static class VectorExtensions { /// diff --git a/lib/ImGui.NET b/lib/ImGui.NET new file mode 160000 index 0000000000..b104b3520d --- /dev/null +++ b/lib/ImGui.NET @@ -0,0 +1 @@ +Subproject commit b104b3520d500366a6ee34c743c20ccf112d7a6e diff --git a/lib/ImGuiScene b/lib/ImGuiScene deleted file mode 160000 index 2f37349ffd..0000000000 --- a/lib/ImGuiScene +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 2f37349ffd778561a1103a650683116c43edc86c diff --git a/targets/Dalamud.Plugin.targets b/targets/Dalamud.Plugin.targets index dc5bec410f..0c7cb9c071 100644 --- a/targets/Dalamud.Plugin.targets +++ b/targets/Dalamud.Plugin.targets @@ -19,7 +19,6 @@ -