diff --git a/.gitmodules b/.gitmodules index 227653d48..4aa969fda 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,6 +7,9 @@ [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.git [submodule "lib/cimgui"] path = lib/cimgui url = https://github.com/goatcorp/gc-cimgui diff --git a/Dalamud.Boot/DalamudStartInfo.cpp b/Dalamud.Boot/DalamudStartInfo.cpp index 35ad42212..5be8f97d0 100644 --- a/Dalamud.Boot/DalamudStartInfo.cpp +++ b/Dalamud.Boot/DalamudStartInfo.cpp @@ -117,6 +117,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 05ff7b23e..0eeaddeed 100644 --- a/Dalamud.Boot/DalamudStartInfo.h +++ b/Dalamud.Boot/DalamudStartInfo.h @@ -55,6 +55,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 b72063855..5e73962ec 100644 --- a/Dalamud.Boot/dllmain.cpp +++ b/Dalamud.Boot/dllmain.cpp @@ -1,6 +1,10 @@ #include "pch.h" +#include +#include + #include "DalamudStartInfo.h" +#include "hooks.h" #include "logging.h" #include "utils.h" #include "veh.h" @@ -90,6 +94,69 @@ HRESULT WINAPI InitializeImpl(LPVOID lpParam, HANDLE hMainThreadContinue) { if ((g_startInfo.BootWaitMessageBox & DalamudStartInfo::WaitMessageboxFlags::BeforeInitialize) != DalamudStartInfo::WaitMessageboxFlags::None) MessageBoxW(nullptr, L"Press OK to continue (BeforeInitialize)", L"Dalamud Boot", MB_OK); + 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_PREVENT_ALTERING_LAYER_SETTINGS_FROM_REGISTRY) | 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 (minHookLoaded) { logging::I("Applying fixes..."); xivfixes::apply_all(true); diff --git a/Dalamud.Common/DalamudStartInfo.cs b/Dalamud.Common/DalamudStartInfo.cs index 680dce217..a0d7f8b0b 100644 --- a/Dalamud.Common/DalamudStartInfo.cs +++ b/Dalamud.Common/DalamudStartInfo.cs @@ -101,6 +101,11 @@ public record DalamudStartInfo /// public bool BootShowConsole { 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 the fallback console should be shown, if needed. /// diff --git a/Dalamud.CorePlugin/Dalamud.CorePlugin.csproj b/Dalamud.CorePlugin/Dalamud.CorePlugin.csproj index f2c8cbc5a..f51622c7e 100644 --- a/Dalamud.CorePlugin/Dalamud.CorePlugin.csproj +++ b/Dalamud.CorePlugin/Dalamud.CorePlugin.csproj @@ -38,13 +38,7 @@ false - - false - - - false - - + false diff --git a/Dalamud.Injector/EntryPoint.cs b/Dalamud.Injector/EntryPoint.cs index b526beb1c..97cb1fd9e 100644 --- a/Dalamud.Injector/EntryPoint.cs +++ b/Dalamud.Injector/EntryPoint.cs @@ -91,6 +91,7 @@ namespace Dalamud.Injector startInfo = ExtractAndInitializeStartInfoFromArguments(startInfo, args); // Remove already handled arguments + args.Remove("--debug-directx"); args.Remove("--console"); args.Remove("--msgbox1"); args.Remove("--msgbox2"); @@ -464,6 +465,7 @@ namespace Dalamud.Injector 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 5b1eb9d30..65f43952f 100644 --- a/Dalamud.sln +++ b/Dalamud.sln @@ -32,12 +32,6 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Dalamud.Test", "Dalamud.Tes EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Dependencies", "Dependencies", "{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}" -EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Dalamud.CorePlugin", "Dalamud.CorePlugin\Dalamud.CorePlugin.csproj", "{4AFDB34A-7467-4D41-B067-53BC4101D9D0}" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "FFXIVClientStructs", "lib\FFXIVClientStructs\FFXIVClientStructs\FFXIVClientStructs.csproj", "{C9B87BD7-AF49-41C3-91F1-D550ADEB7833}" @@ -68,6 +62,8 @@ Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "cimplot", "external\cimplot EndProject Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "cimguizmo", "external\cimguizmo\cimguizmo.vcxproj", "{F258347D-31BE-4605-98CE-40E43BDF6F9D}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ImGui.NET-472", "lib\ImGui.NET\src\ImGui.NET-472\ImGui.NET-472.csproj", "{A0A3C0AC-18D9-4C74-8CFC-14E53512846D}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -98,18 +94,6 @@ Global {C8004563-1806-4329-844F-0EF6274291FC}.Debug|Any CPU.Build.0 = Debug|x64 {C8004563-1806-4329-844F-0EF6274291FC}.Release|Any CPU.ActiveCfg = Release|x64 {C8004563-1806-4329-844F-0EF6274291FC}.Release|Any CPU.Build.0 = Release|x64 - {0483026E-C6CE-4B1A-AA68-46544C08140B}.Debug|Any CPU.ActiveCfg = Debug|x64 - {0483026E-C6CE-4B1A-AA68-46544C08140B}.Debug|Any CPU.Build.0 = Debug|x64 - {0483026E-C6CE-4B1A-AA68-46544C08140B}.Release|Any CPU.ActiveCfg = Release|x64 - {0483026E-C6CE-4B1A-AA68-46544C08140B}.Release|Any CPU.Build.0 = Release|x64 - {C0E7E797-4FBF-4F46-BC57-463F3719BA7A}.Debug|Any CPU.ActiveCfg = Debug|x64 - {C0E7E797-4FBF-4F46-BC57-463F3719BA7A}.Debug|Any CPU.Build.0 = Debug|x64 - {C0E7E797-4FBF-4F46-BC57-463F3719BA7A}.Release|Any CPU.ActiveCfg = Release|x64 - {C0E7E797-4FBF-4F46-BC57-463F3719BA7A}.Release|Any CPU.Build.0 = Release|x64 - {2F7FF0A8-B619-4572-86C7-71E46FE22FB8}.Debug|Any CPU.ActiveCfg = Debug|x64 - {2F7FF0A8-B619-4572-86C7-71E46FE22FB8}.Debug|Any CPU.Build.0 = Debug|x64 - {2F7FF0A8-B619-4572-86C7-71E46FE22FB8}.Release|Any CPU.ActiveCfg = Release|x64 - {2F7FF0A8-B619-4572-86C7-71E46FE22FB8}.Release|Any CPU.Build.0 = Release|x64 {4AFDB34A-7467-4D41-B067-53BC4101D9D0}.Debug|Any CPU.ActiveCfg = Debug|x64 {4AFDB34A-7467-4D41-B067-53BC4101D9D0}.Debug|Any CPU.Build.0 = Debug|x64 {4AFDB34A-7467-4D41-B067-53BC4101D9D0}.Release|Any CPU.ActiveCfg = Release|x64 @@ -154,6 +138,10 @@ Global {F258347D-31BE-4605-98CE-40E43BDF6F9D}.Debug|Any CPU.Build.0 = Debug|x64 {F258347D-31BE-4605-98CE-40E43BDF6F9D}.Release|Any CPU.ActiveCfg = Release|x64 {F258347D-31BE-4605-98CE-40E43BDF6F9D}.Release|Any CPU.Build.0 = Release|x64 + {A0A3C0AC-18D9-4C74-8CFC-14E53512846D}.Debug|Any CPU.ActiveCfg = Debug|x64 + {A0A3C0AC-18D9-4C74-8CFC-14E53512846D}.Debug|Any CPU.Build.0 = Debug|x64 + {A0A3C0AC-18D9-4C74-8CFC-14E53512846D}.Release|Any CPU.ActiveCfg = Release|x64 + {A0A3C0AC-18D9-4C74-8CFC-14E53512846D}.Release|Any CPU.Build.0 = Release|x64 EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -161,9 +149,6 @@ Global GlobalSection(NestedProjects) = preSolution {5B832F73-5F54-4ADC-870F-D0095EF72C9A} = {19775C83-7117-4A5F-AA00-18889F46A490} {8874326B-E755-4D13-90B4-59AB263A3E6B} = {19775C83-7117-4A5F-AA00-18889F46A490} - {0483026E-C6CE-4B1A-AA68-46544C08140B} = {DBE5345E-6594-4A59-B183-1C3D5592269D} - {C0E7E797-4FBF-4F46-BC57-463F3719BA7A} = {DBE5345E-6594-4A59-B183-1C3D5592269D} - {2F7FF0A8-B619-4572-86C7-71E46FE22FB8} = {DBE5345E-6594-4A59-B183-1C3D5592269D} {4AFDB34A-7467-4D41-B067-53BC4101D9D0} = {8F079208-C227-4D96-9427-2BEBE0003944} {C9B87BD7-AF49-41C3-91F1-D550ADEB7833} = {8BBACF2D-7AB8-4610-A115-0E363D35C291} {E0D51896-604F-4B40-8CFE-51941607B3A1} = {8BBACF2D-7AB8-4610-A115-0E363D35C291} @@ -175,6 +160,7 @@ Global {8BBACF2D-7AB8-4610-A115-0E363D35C291} = {E15BDA6D-E881-4482-94BA-BE5527E917FF} {76CAA246-C405-4A8C-B0AE-F4A0EF3D4E16} = {DBE5345E-6594-4A59-B183-1C3D5592269D} {F258347D-31BE-4605-98CE-40E43BDF6F9D} = {DBE5345E-6594-4A59-B183-1C3D5592269D} + {A0A3C0AC-18D9-4C74-8CFC-14E53512846D} = {DBE5345E-6594-4A59-B183-1C3D5592269D} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {79B65AC9-C940-410E-AB61-7EA7E12C7599} diff --git a/Dalamud/Dalamud.csproj b/Dalamud/Dalamud.csproj index 55847cf46..210ada844 100644 --- a/Dalamud/Dalamud.csproj +++ b/Dalamud/Dalamud.csproj @@ -70,6 +70,12 @@ all + + + + + + @@ -87,13 +93,19 @@ + + + imgui-frag.hlsl.bytes + + + imgui-vertex.hlsl.bytes + + + - - - diff --git a/Dalamud/Interface/DragDrop/DragDropManager.cs b/Dalamud/Interface/DragDrop/DragDropManager.cs index c9f0f9b80..f3acd704d 100644 --- a/Dalamud/Interface/DragDrop/DragDropManager.cs +++ b/Dalamud/Interface/DragDrop/DragDropManager.cs @@ -32,7 +32,7 @@ internal partial class DragDropManager : IInternalDisposableService, IDragDropMa Service.GetAsync() .ContinueWith(t => { - this.windowHandlePtr = t.Result.Manager.WindowHandlePtr; + this.windowHandlePtr = t.Result.Manager.GameWindowHandle; this.Enable(); }); } diff --git a/Dalamud/Interface/ImGuiBackend/Dx11Win32Backend.cs b/Dalamud/Interface/ImGuiBackend/Dx11Win32Backend.cs new file mode 100644 index 000000000..1346de439 --- /dev/null +++ b/Dalamud/Interface/ImGuiBackend/Dx11Win32Backend.cs @@ -0,0 +1,247 @@ +using System.Diagnostics.CodeAnalysis; + +using Dalamud.Interface.ImGuiBackend.Helpers; +using Dalamud.Interface.ImGuiBackend.InputHandler; +using Dalamud.Interface.ImGuiBackend.Renderers; +using Dalamud.Utility; + +using ImGuiNET; + +using ImGuizmoNET; + +using ImPlotNET; + +using TerraFX.Interop.DirectX; +using TerraFX.Interop.Windows; + +namespace Dalamud.Interface.ImGuiBackend; + +/// +/// Backend for ImGui, using and . +/// +[SuppressMessage( + "StyleCop.CSharp.LayoutRules", + "SA1519:Braces should not be omitted from multi-line child statement", + Justification = "Multiple fixed/using scopes")] +internal sealed unsafe class Dx11Win32Backend : IWin32Backend +{ + private readonly Dx11Renderer imguiRenderer; + private readonly Win32InputHandler imguiInput; + + private ComPtr swapChainPossiblyWrapped; + private ComPtr swapChain; + private ComPtr device; + private ComPtr deviceContext; + + private int targetWidth; + private int targetHeight; + + /// + /// Initializes a new instance of the class. + /// + /// The pointer to an instance of . The reference is copied. + public Dx11Win32Backend(IDXGISwapChain* swapChain) + { + try + { + this.swapChainPossiblyWrapped = new(swapChain); + this.swapChain = new(swapChain); + fixed (ComPtr* ppSwapChain = &this.swapChain) + ReShadePeeler.PeelSwapChain(ppSwapChain); + + fixed (Guid* guid = &IID.IID_ID3D11Device) + fixed (ID3D11Device** pp = &this.device.GetPinnableReference()) + this.swapChain.Get()->GetDevice(guid, (void**)pp).ThrowOnError(); + + fixed (ID3D11DeviceContext** pp = &this.deviceContext.GetPinnableReference()) + this.device.Get()->GetImmediateContext(pp); + + using var buffer = default(ComPtr); + fixed (Guid* guid = &IID.IID_ID3D11Resource) + this.swapChain.Get()->GetBuffer(0, guid, (void**)buffer.GetAddressOf()).ThrowOnError(); + + var desc = default(DXGI_SWAP_CHAIN_DESC); + this.swapChain.Get()->GetDesc(&desc).ThrowOnError(); + this.targetWidth = (int)desc.BufferDesc.Width; + this.targetHeight = (int)desc.BufferDesc.Height; + this.WindowHandle = desc.OutputWindow; + + var ctx = ImGui.CreateContext(); + ImGuizmo.SetImGuiContext(ctx); + ImPlot.SetImGuiContext(ctx); + ImPlot.CreateContext(); + + ImGui.GetIO().ConfigFlags |= ImGuiConfigFlags.DockingEnable | ImGuiConfigFlags.ViewportsEnable; + + this.imguiRenderer = new(this.SwapChain, this.Device, this.DeviceContext); + this.imguiInput = new(this.WindowHandle); + } + catch + { + this.Dispose(); + throw; + } + } + + /// + /// Finalizes an instance of the class. + /// + ~Dx11Win32Backend() => this.ReleaseUnmanagedResources(); + + /// + public event IImGuiBackend.BuildUiDelegate? BuildUi; + + /// + public event IImGuiBackend.NewInputFrameDelegate? NewInputFrame; + + /// + public event IImGuiBackend.NewRenderFrameDelegate? NewRenderFrame; + + /// + public bool UpdateCursor + { + get => this.imguiInput.UpdateCursor; + set => this.imguiInput.UpdateCursor = value; + } + + /// + public string? IniPath + { + get => this.imguiInput.IniPath; + set => this.imguiInput.IniPath = value; + } + + /// + public IImGuiInputHandler InputHandler => this.imguiInput; + + /// + public IImGuiRenderer Renderer => this.imguiRenderer; + + /// + /// Gets the pointer to an instance of . + /// + public IDXGISwapChain* SwapChain => this.swapChain; + + /// + /// Gets the pointer to an instance of . + /// + public ID3D11Device* Device => this.device; + + /// + /// Gets the pointer to an instance of , in . + /// + public nint DeviceHandle => (nint)this.device.Get(); + + /// + /// Gets the pointer to an instance of . + /// + public ID3D11DeviceContext* DeviceContext => this.deviceContext; + + /// + /// Gets the window handle. + /// + public HWND WindowHandle { get; } + + /// + public void Dispose() + { + this.ReleaseUnmanagedResources(); + GC.SuppressFinalize(this); + } + + /// + public nint? ProcessWndProcW(HWND hWnd, uint msg, WPARAM wParam, LPARAM lParam) => + this.imguiInput.ProcessWndProcW(hWnd, msg, wParam, lParam); + + /// + public void Render() + { + this.imguiRenderer.OnNewFrame(); + this.NewRenderFrame?.Invoke(); + this.imguiInput.NewFrame(this.targetWidth, this.targetHeight); + this.NewInputFrame?.Invoke(); + + ImGui.NewFrame(); + ImGuizmo.BeginFrame(); + + this.BuildUi?.Invoke(); + + ImGui.Render(); + + this.imguiRenderer.RenderDrawData(ImGui.GetDrawData()); + + ImGui.UpdatePlatformWindows(); + ImGui.RenderPlatformWindowsDefault(); + } + + /// + public void OnPreResize() => this.imguiRenderer.OnPreResize(); + + /// + public void OnPostResize(int newWidth, int newHeight) + { + this.imguiRenderer.OnPostResize(newWidth, newHeight); + this.targetWidth = newWidth; + this.targetHeight = newHeight; + } + + /// + public void InvalidateFonts() => this.imguiRenderer.RebuildFontTexture(); + + /// + public bool IsImGuiCursor(nint cursorHandle) => this.imguiInput.IsImGuiCursor(cursorHandle); + + /// + public bool IsAttachedToPresentationTarget(nint targetHandle) => + AreIUnknownEqual(this.swapChain.Get(), (IUnknown*)targetHandle) + || AreIUnknownEqual(this.swapChainPossiblyWrapped.Get(), (IUnknown*)targetHandle); + + /// + public bool IsMainViewportFullScreen() + { + BOOL fullscreen; + this.swapChain.Get()->GetFullscreenState(&fullscreen, null); + return fullscreen; + } + + private static bool AreIUnknownEqual(T1* punk1, T2* punk2) + where T1 : unmanaged, IUnknown.Interface + where T2 : unmanaged, IUnknown.Interface + { + // https://learn.microsoft.com/en-us/windows/win32/api/unknwn/nf-unknwn-iunknown-queryinterface(refiid_void) + // For any given COM object (also known as a COM component), a specific query for the IUnknown interface on any + // of the object's interfaces must always return the same pointer value. + + if (punk1 is null || punk2 is null) + return false; + + fixed (Guid* iid = &IID.IID_IUnknown) + { + using var u1 = default(ComPtr); + if (punk1->QueryInterface(iid, (void**)u1.GetAddressOf()).FAILED) + return false; + + using var u2 = default(ComPtr); + if (punk2->QueryInterface(iid, (void**)u2.GetAddressOf()).FAILED) + return false; + + return u1.Get() == u2.Get(); + } + } + + private void ReleaseUnmanagedResources() + { + if (this.device.IsEmpty()) + return; + + this.imguiRenderer.Dispose(); + this.imguiInput.Dispose(); + + ImGui.DestroyContext(); + + this.swapChain.Dispose(); + this.deviceContext.Dispose(); + this.device.Dispose(); + this.swapChainPossiblyWrapped.Dispose(); + } +} diff --git a/Dalamud/Interface/ImGuiBackend/Helpers/D3D11/D3D11DeviceContextStateBackup.cs b/Dalamud/Interface/ImGuiBackend/Helpers/D3D11/D3D11DeviceContextStateBackup.cs new file mode 100644 index 000000000..df1087ce3 --- /dev/null +++ b/Dalamud/Interface/ImGuiBackend/Helpers/D3D11/D3D11DeviceContextStateBackup.cs @@ -0,0 +1,655 @@ +using System.Runtime.InteropServices; + +using TerraFX.Interop.DirectX; +using TerraFX.Interop.Windows; + +namespace Dalamud.Interface.ImGuiBackend.Helpers.D3D11; + +/// +/// Captures states of a . +/// +internal unsafe struct D3D11DeviceContextStateBackup : IDisposable +{ + private InputAssemblerState inputAssemblerState; + private RasterizerState rasterizerState; + private OutputMergerState outputMergerState; + private VertexShaderState vertexShaderState; + private HullShaderState hullShaderState; + private DomainShaderState domainShaderState; + private GeometryShaderState geometryShaderState; + private PixelShaderState pixelShaderState; + private ComputeShaderState computeShaderState; + + /// + /// Initializes a new instance of the struct, + /// by capturing all states of a . + /// + /// The feature level. + /// The device context. + public D3D11DeviceContextStateBackup(D3D_FEATURE_LEVEL featureLevel, ID3D11DeviceContext* ctx) + { + this.inputAssemblerState = InputAssemblerState.From(ctx); + this.rasterizerState = RasterizerState.From(ctx); + this.outputMergerState = OutputMergerState.From(featureLevel, ctx); + this.vertexShaderState = VertexShaderState.From(ctx); + this.hullShaderState = HullShaderState.From(ctx); + this.domainShaderState = DomainShaderState.From(ctx); + this.geometryShaderState = GeometryShaderState.From(ctx); + this.pixelShaderState = PixelShaderState.From(ctx); + this.computeShaderState = ComputeShaderState.From(featureLevel, ctx); + } + + /// + public void Dispose() + { + this.inputAssemblerState.Dispose(); + this.rasterizerState.Dispose(); + this.outputMergerState.Dispose(); + this.vertexShaderState.Dispose(); + this.hullShaderState.Dispose(); + this.domainShaderState.Dispose(); + this.geometryShaderState.Dispose(); + this.pixelShaderState.Dispose(); + this.computeShaderState.Dispose(); + } + + /// + /// Captures Input Assembler states of a . + /// + [StructLayout(LayoutKind.Sequential)] + public struct InputAssemblerState : IDisposable + { + private const int BufferCount = TerraFX.Interop.DirectX.D3D11.D3D11_IA_VERTEX_INPUT_RESOURCE_SLOT_COUNT; + + private ComPtr context; + private ComPtr layout; + private ComPtr indexBuffer; + private DXGI_FORMAT indexFormat; + private uint indexOffset; + private D3D_PRIMITIVE_TOPOLOGY topology; + private fixed ulong buffers[BufferCount]; + private fixed uint strides[BufferCount]; + private fixed uint offsets[BufferCount]; + + /// + /// Creates a new instance of from . + /// + /// The device context. + /// The captured state. + public static InputAssemblerState From(ID3D11DeviceContext* ctx) + { + var state = default(InputAssemblerState); + state.context.Attach(ctx); + ctx->AddRef(); + ctx->IAGetInputLayout(state.layout.GetAddressOf()); + ctx->IAGetPrimitiveTopology(&state.topology); + ctx->IAGetIndexBuffer(state.indexBuffer.GetAddressOf(), &state.indexFormat, &state.indexOffset); + ctx->IAGetVertexBuffers(0, BufferCount, (ID3D11Buffer**)state.buffers, state.strides, state.offsets); + return state; + } + + /// + public void Dispose() + { + var ctx = this.context.Get(); + if (ctx is null) + return; + + fixed (InputAssemblerState* pThis = &this) + { + ctx->IASetInputLayout(pThis->layout); + ctx->IASetPrimitiveTopology(pThis->topology); + ctx->IASetIndexBuffer(pThis->indexBuffer, pThis->indexFormat, pThis->indexOffset); + ctx->IASetVertexBuffers(0, BufferCount, (ID3D11Buffer**)pThis->buffers, pThis->strides, pThis->offsets); + + pThis->context.Dispose(); + pThis->layout.Dispose(); + pThis->indexBuffer.Dispose(); + foreach (ref var b in new Span>(pThis->buffers, BufferCount)) + b.Dispose(); + } + } + } + + /// + /// Captures Rasterizer states of a . + /// + [StructLayout(LayoutKind.Sequential)] + public struct RasterizerState : IDisposable + { + private const int Count = TerraFX.Interop.DirectX.D3D11.D3D11_VIEWPORT_AND_SCISSORRECT_MAX_INDEX; + + private ComPtr context; + private ComPtr state; + private fixed byte viewports[24 * Count]; + private fixed ulong scissorRects[16 * Count]; + + /// + /// Creates a new instance of from . + /// + /// The device context. + /// The captured state. + public static RasterizerState From(ID3D11DeviceContext* ctx) + { + var state = default(RasterizerState); + state.context.Attach(ctx); + ctx->AddRef(); + ctx->RSGetState(state.state.GetAddressOf()); + uint n = Count; + ctx->RSGetViewports(&n, (D3D11_VIEWPORT*)state.viewports); + n = Count; + ctx->RSGetScissorRects(&n, (RECT*)state.scissorRects); + return state; + } + + /// + public void Dispose() + { + var ctx = this.context.Get(); + if (ctx is null) + return; + + fixed (RasterizerState* pThis = &this) + { + ctx->RSSetState(pThis->state); + ctx->RSSetViewports(Count, (D3D11_VIEWPORT*)pThis->viewports); + ctx->RSSetScissorRects(Count, (RECT*)pThis->scissorRects); + + pThis->context.Dispose(); + pThis->state.Dispose(); + } + } + } + + /// + /// Captures Output Merger states of a . + /// + [StructLayout(LayoutKind.Sequential)] + public struct OutputMergerState : IDisposable + { + private const int RtvCount = TerraFX.Interop.DirectX.D3D11.D3D11_SIMULTANEOUS_RENDER_TARGET_COUNT; + private const int UavCountMax = TerraFX.Interop.DirectX.D3D11.D3D11_1_UAV_SLOT_COUNT; + + private ComPtr context; + private ComPtr blendState; + private fixed float blendFactor[4]; + private uint sampleMask; + private uint stencilRef; + private ComPtr depthStencilState; + private fixed ulong rtvs[RtvCount]; // ID3D11RenderTargetView*[RtvCount] + private ComPtr dsv; + private fixed ulong uavs[UavCountMax]; // ID3D11UnorderedAccessView*[UavCount] + private int uavCount; + + /// + /// Creates a new instance of from . + /// + /// The feature level. + /// The device context. + /// The captured state. + public static OutputMergerState From(D3D_FEATURE_LEVEL featureLevel, ID3D11DeviceContext* ctx) + { + var state = default(OutputMergerState); + state.uavCount = featureLevel >= D3D_FEATURE_LEVEL.D3D_FEATURE_LEVEL_11_1 + ? TerraFX.Interop.DirectX.D3D11.D3D11_1_UAV_SLOT_COUNT + : TerraFX.Interop.DirectX.D3D11.D3D11_PS_CS_UAV_REGISTER_COUNT; + state.context.Attach(ctx); + ctx->AddRef(); + ctx->OMGetBlendState(state.blendState.GetAddressOf(), state.blendFactor, &state.sampleMask); + ctx->OMGetDepthStencilState(state.depthStencilState.GetAddressOf(), &state.stencilRef); + ctx->OMGetRenderTargetsAndUnorderedAccessViews( + RtvCount, + (ID3D11RenderTargetView**)state.rtvs, + state.dsv.GetAddressOf(), + 0, + (uint)state.uavCount, + (ID3D11UnorderedAccessView**)state.uavs); + return state; + } + + /// + public void Dispose() + { + var ctx = this.context.Get(); + if (ctx is null) + return; + + fixed (OutputMergerState* pThis = &this) + { + ctx->OMSetBlendState(pThis->blendState, pThis->blendFactor, pThis->sampleMask); + ctx->OMSetDepthStencilState(pThis->depthStencilState, pThis->stencilRef); + var rtvc = (uint)RtvCount; + while (rtvc > 0 && pThis->rtvs[rtvc - 1] == 0) + rtvc--; + + var uavlb = rtvc; + while (uavlb < this.uavCount && pThis->uavs[uavlb] == 0) + uavlb++; + + var uavc = (uint)this.uavCount; + while (uavc > uavlb && pThis->uavs[uavc - 1] == 0) + uavlb--; + uavc -= uavlb; + + ctx->OMSetRenderTargetsAndUnorderedAccessViews( + rtvc, + (ID3D11RenderTargetView**)pThis->rtvs, + pThis->dsv, + uavc == 0 ? 0 : uavlb, + uavc, + uavc == 0 ? null : (ID3D11UnorderedAccessView**)pThis->uavs, + null); + + this.context.Reset(); + this.blendState.Reset(); + this.depthStencilState.Reset(); + this.dsv.Reset(); + foreach (ref var b in new Span>(pThis->rtvs, RtvCount)) + b.Dispose(); + foreach (ref var b in new Span>(pThis->uavs, this.uavCount)) + b.Dispose(); + } + } + } + + /// + /// Captures Vertex Shader states of a . + /// + [StructLayout(LayoutKind.Sequential)] + public struct VertexShaderState : IDisposable + { + private const int BufferCount = TerraFX.Interop.DirectX.D3D11.D3D11_COMMONSHADER_CONSTANT_BUFFER_API_SLOT_COUNT; + private const int SamplerCount = TerraFX.Interop.DirectX.D3D11.D3D11_COMMONSHADER_SAMPLER_SLOT_COUNT; + private const int ResourceCount = TerraFX.Interop.DirectX.D3D11.D3D11_COMMONSHADER_INPUT_RESOURCE_SLOT_COUNT; + private const int ClassInstanceCount = 256; // According to msdn + + private ComPtr context; + private ComPtr shader; + private fixed ulong insts[ClassInstanceCount]; + private fixed ulong buffers[BufferCount]; + private fixed ulong samplers[SamplerCount]; + private fixed ulong resources[ResourceCount]; + private uint instCount; + + /// + /// Creates a new instance of from . + /// + /// The device context. + /// The captured state. + public static VertexShaderState From(ID3D11DeviceContext* ctx) + { + var state = default(VertexShaderState); + state.context.Attach(ctx); + ctx->AddRef(); + state.instCount = ClassInstanceCount; + ctx->VSGetShader(state.shader.GetAddressOf(), (ID3D11ClassInstance**)state.insts, &state.instCount); + ctx->VSGetConstantBuffers(0, BufferCount, (ID3D11Buffer**)state.buffers); + ctx->VSGetSamplers(0, SamplerCount, (ID3D11SamplerState**)state.samplers); + ctx->VSGetShaderResources(0, ResourceCount, (ID3D11ShaderResourceView**)state.resources); + return state; + } + + /// + public void Dispose() + { + var ctx = this.context.Get(); + if (ctx is null) + return; + + fixed (VertexShaderState* pThis = &this) + { + ctx->VSSetShader(pThis->shader, (ID3D11ClassInstance**)pThis->insts, pThis->instCount); + ctx->VSSetConstantBuffers(0, BufferCount, (ID3D11Buffer**)pThis->buffers); + ctx->VSSetSamplers(0, SamplerCount, (ID3D11SamplerState**)pThis->samplers); + ctx->VSSetShaderResources(0, ResourceCount, (ID3D11ShaderResourceView**)pThis->resources); + + foreach (ref var b in new Span>(pThis->buffers, BufferCount)) + b.Dispose(); + foreach (ref var b in new Span>(pThis->samplers, SamplerCount)) + b.Dispose(); + foreach (ref var b in new Span>(pThis->resources, ResourceCount)) + b.Dispose(); + foreach (ref var b in new Span>(pThis->insts, (int)pThis->instCount)) + b.Dispose(); + pThis->context.Dispose(); + pThis->shader.Dispose(); + } + } + } + + /// + /// Captures Hull Shader states of a . + /// + [StructLayout(LayoutKind.Sequential)] + public struct HullShaderState : IDisposable + { + private const int BufferCount = TerraFX.Interop.DirectX.D3D11.D3D11_COMMONSHADER_CONSTANT_BUFFER_API_SLOT_COUNT; + private const int SamplerCount = TerraFX.Interop.DirectX.D3D11.D3D11_COMMONSHADER_SAMPLER_SLOT_COUNT; + private const int ResourceCount = TerraFX.Interop.DirectX.D3D11.D3D11_COMMONSHADER_INPUT_RESOURCE_SLOT_COUNT; + private const int ClassInstanceCount = 256; // According to msdn + + private ComPtr context; + private ComPtr shader; + private fixed ulong insts[ClassInstanceCount]; + private fixed ulong buffers[BufferCount]; + private fixed ulong samplers[SamplerCount]; + private fixed ulong resources[ResourceCount]; + private uint instCount; + + /// + /// Creates a new instance of from . + /// + /// The device context. + /// The captured state. + public static HullShaderState From(ID3D11DeviceContext* ctx) + { + var state = default(HullShaderState); + state.context.Attach(ctx); + ctx->AddRef(); + state.instCount = ClassInstanceCount; + ctx->HSGetShader(state.shader.GetAddressOf(), (ID3D11ClassInstance**)state.insts, &state.instCount); + ctx->HSGetConstantBuffers(0, BufferCount, (ID3D11Buffer**)state.buffers); + ctx->HSGetSamplers(0, SamplerCount, (ID3D11SamplerState**)state.samplers); + ctx->HSGetShaderResources(0, ResourceCount, (ID3D11ShaderResourceView**)state.resources); + return state; + } + + /// + public void Dispose() + { + var ctx = this.context.Get(); + if (ctx is null) + return; + + fixed (HullShaderState* pThis = &this) + { + ctx->HSSetShader(pThis->shader, (ID3D11ClassInstance**)pThis->insts, pThis->instCount); + ctx->HSSetConstantBuffers(0, BufferCount, (ID3D11Buffer**)pThis->buffers); + ctx->HSSetSamplers(0, SamplerCount, (ID3D11SamplerState**)pThis->samplers); + ctx->HSSetShaderResources(0, ResourceCount, (ID3D11ShaderResourceView**)pThis->resources); + + foreach (ref var b in new Span>(pThis->buffers, BufferCount)) + b.Dispose(); + foreach (ref var b in new Span>(pThis->samplers, SamplerCount)) + b.Dispose(); + foreach (ref var b in new Span>(pThis->resources, ResourceCount)) + b.Dispose(); + foreach (ref var b in new Span>(pThis->insts, (int)pThis->instCount)) + b.Dispose(); + pThis->context.Dispose(); + pThis->shader.Dispose(); + } + } + } + + /// + /// Captures Domain Shader states of a . + /// + [StructLayout(LayoutKind.Sequential)] + public struct DomainShaderState : IDisposable + { + private const int BufferCount = TerraFX.Interop.DirectX.D3D11.D3D11_COMMONSHADER_CONSTANT_BUFFER_API_SLOT_COUNT; + private const int SamplerCount = TerraFX.Interop.DirectX.D3D11.D3D11_COMMONSHADER_SAMPLER_SLOT_COUNT; + private const int ResourceCount = TerraFX.Interop.DirectX.D3D11.D3D11_COMMONSHADER_INPUT_RESOURCE_SLOT_COUNT; + private const int ClassInstanceCount = 256; // According to msdn + + private ComPtr context; + private ComPtr shader; + private fixed ulong insts[ClassInstanceCount]; + private fixed ulong buffers[BufferCount]; + private fixed ulong samplers[SamplerCount]; + private fixed ulong resources[ResourceCount]; + private uint instCount; + + /// + /// Creates a new instance of from . + /// + /// The device context. + /// The captured state. + public static DomainShaderState From(ID3D11DeviceContext* ctx) + { + var state = default(DomainShaderState); + state.context.Attach(ctx); + ctx->AddRef(); + state.instCount = ClassInstanceCount; + ctx->DSGetShader(state.shader.GetAddressOf(), (ID3D11ClassInstance**)state.insts, &state.instCount); + ctx->DSGetConstantBuffers(0, BufferCount, (ID3D11Buffer**)state.buffers); + ctx->DSGetSamplers(0, SamplerCount, (ID3D11SamplerState**)state.samplers); + ctx->DSGetShaderResources(0, ResourceCount, (ID3D11ShaderResourceView**)state.resources); + return state; + } + + /// + public void Dispose() + { + var ctx = this.context.Get(); + if (ctx is null) + return; + + fixed (DomainShaderState* pThis = &this) + { + ctx->DSSetShader(pThis->shader, (ID3D11ClassInstance**)pThis->insts, pThis->instCount); + ctx->DSSetConstantBuffers(0, BufferCount, (ID3D11Buffer**)pThis->buffers); + ctx->DSSetSamplers(0, SamplerCount, (ID3D11SamplerState**)pThis->samplers); + ctx->DSSetShaderResources(0, ResourceCount, (ID3D11ShaderResourceView**)pThis->resources); + + foreach (ref var b in new Span>(pThis->buffers, BufferCount)) + b.Dispose(); + foreach (ref var b in new Span>(pThis->samplers, SamplerCount)) + b.Dispose(); + foreach (ref var b in new Span>(pThis->resources, ResourceCount)) + b.Dispose(); + foreach (ref var b in new Span>(pThis->insts, (int)pThis->instCount)) + b.Dispose(); + pThis->context.Dispose(); + pThis->shader.Dispose(); + } + } + } + + /// + /// Captures Geometry Shader states of a . + /// + [StructLayout(LayoutKind.Sequential)] + public struct GeometryShaderState : IDisposable + { + private const int BufferCount = TerraFX.Interop.DirectX.D3D11.D3D11_COMMONSHADER_CONSTANT_BUFFER_API_SLOT_COUNT; + private const int SamplerCount = TerraFX.Interop.DirectX.D3D11.D3D11_COMMONSHADER_SAMPLER_SLOT_COUNT; + private const int ResourceCount = TerraFX.Interop.DirectX.D3D11.D3D11_COMMONSHADER_INPUT_RESOURCE_SLOT_COUNT; + private const int ClassInstanceCount = 256; // According to msdn + + private ComPtr context; + private ComPtr shader; + private fixed ulong insts[ClassInstanceCount]; + private fixed ulong buffers[BufferCount]; + private fixed ulong samplers[SamplerCount]; + private fixed ulong resources[ResourceCount]; + private uint instCount; + + /// + /// Creates a new instance of from . + /// + /// The device context. + /// The captured state. + public static GeometryShaderState From(ID3D11DeviceContext* ctx) + { + var state = default(GeometryShaderState); + state.context.Attach(ctx); + ctx->AddRef(); + state.instCount = ClassInstanceCount; + ctx->GSGetShader(state.shader.GetAddressOf(), (ID3D11ClassInstance**)state.insts, &state.instCount); + ctx->GSGetConstantBuffers(0, BufferCount, (ID3D11Buffer**)state.buffers); + ctx->GSGetSamplers(0, SamplerCount, (ID3D11SamplerState**)state.samplers); + ctx->GSGetShaderResources(0, ResourceCount, (ID3D11ShaderResourceView**)state.resources); + return state; + } + + /// + public void Dispose() + { + var ctx = this.context.Get(); + if (ctx is null) + return; + + fixed (GeometryShaderState* pThis = &this) + { + ctx->GSSetShader(pThis->shader, (ID3D11ClassInstance**)pThis->insts, pThis->instCount); + ctx->GSSetConstantBuffers(0, BufferCount, (ID3D11Buffer**)pThis->buffers); + ctx->GSSetSamplers(0, SamplerCount, (ID3D11SamplerState**)pThis->samplers); + ctx->GSSetShaderResources(0, ResourceCount, (ID3D11ShaderResourceView**)pThis->resources); + + foreach (ref var b in new Span>(pThis->buffers, BufferCount)) + b.Dispose(); + foreach (ref var b in new Span>(pThis->samplers, SamplerCount)) + b.Dispose(); + foreach (ref var b in new Span>(pThis->resources, ResourceCount)) + b.Dispose(); + foreach (ref var b in new Span>(pThis->insts, (int)pThis->instCount)) + b.Dispose(); + pThis->context.Dispose(); + pThis->shader.Dispose(); + } + } + } + + /// + /// Captures Pixel Shader states of a . + /// + [StructLayout(LayoutKind.Sequential)] + public struct PixelShaderState : IDisposable + { + private const int BufferCount = TerraFX.Interop.DirectX.D3D11.D3D11_COMMONSHADER_CONSTANT_BUFFER_API_SLOT_COUNT; + private const int SamplerCount = TerraFX.Interop.DirectX.D3D11.D3D11_COMMONSHADER_SAMPLER_SLOT_COUNT; + private const int ResourceCount = TerraFX.Interop.DirectX.D3D11.D3D11_COMMONSHADER_INPUT_RESOURCE_SLOT_COUNT; + private const int ClassInstanceCount = 256; // According to msdn + + private ComPtr context; + private ComPtr shader; + private fixed ulong insts[ClassInstanceCount]; + private fixed ulong buffers[BufferCount]; + private fixed ulong samplers[SamplerCount]; + private fixed ulong resources[ResourceCount]; + private uint instCount; + + /// + /// Creates a new instance of from . + /// + /// The device context. + /// The captured state. + public static PixelShaderState From(ID3D11DeviceContext* ctx) + { + var state = default(PixelShaderState); + state.context.Attach(ctx); + ctx->AddRef(); + state.instCount = ClassInstanceCount; + ctx->PSGetShader(state.shader.GetAddressOf(), (ID3D11ClassInstance**)state.insts, &state.instCount); + ctx->PSGetConstantBuffers(0, BufferCount, (ID3D11Buffer**)state.buffers); + ctx->PSGetSamplers(0, SamplerCount, (ID3D11SamplerState**)state.samplers); + ctx->PSGetShaderResources(0, ResourceCount, (ID3D11ShaderResourceView**)state.resources); + return state; + } + + /// + public void Dispose() + { + var ctx = this.context.Get(); + if (ctx is null) + return; + + fixed (PixelShaderState* pThis = &this) + { + ctx->PSSetShader(pThis->shader, (ID3D11ClassInstance**)pThis->insts, pThis->instCount); + ctx->PSSetConstantBuffers(0, BufferCount, (ID3D11Buffer**)pThis->buffers); + ctx->PSSetSamplers(0, SamplerCount, (ID3D11SamplerState**)pThis->samplers); + ctx->PSSetShaderResources(0, ResourceCount, (ID3D11ShaderResourceView**)pThis->resources); + + foreach (ref var b in new Span>(pThis->buffers, BufferCount)) + b.Dispose(); + foreach (ref var b in new Span>(pThis->samplers, SamplerCount)) + b.Dispose(); + foreach (ref var b in new Span>(pThis->resources, ResourceCount)) + b.Dispose(); + foreach (ref var b in new Span>(pThis->insts, (int)pThis->instCount)) + b.Dispose(); + pThis->context.Dispose(); + pThis->shader.Dispose(); + } + } + } + + /// + /// Captures Compute Shader states of a . + /// + [StructLayout(LayoutKind.Sequential)] + public struct ComputeShaderState : IDisposable + { + private const int BufferCount = TerraFX.Interop.DirectX.D3D11.D3D11_COMMONSHADER_CONSTANT_BUFFER_API_SLOT_COUNT; + private const int SamplerCount = TerraFX.Interop.DirectX.D3D11.D3D11_COMMONSHADER_SAMPLER_SLOT_COUNT; + private const int ResourceCount = TerraFX.Interop.DirectX.D3D11.D3D11_COMMONSHADER_INPUT_RESOURCE_SLOT_COUNT; + private const int InstanceCount = 256; // According to msdn + private const int UavCountMax = TerraFX.Interop.DirectX.D3D11.D3D11_1_UAV_SLOT_COUNT; + + private ComPtr context; + private ComPtr shader; + private fixed ulong insts[InstanceCount]; // ID3D11ClassInstance*[BufferCount] + private fixed ulong buffers[BufferCount]; // ID3D11Buffer*[BufferCount] + private fixed ulong samplers[SamplerCount]; // ID3D11SamplerState*[SamplerCount] + private fixed ulong resources[ResourceCount]; // ID3D11ShaderResourceView*[ResourceCount] + private fixed ulong uavs[UavCountMax]; // ID3D11UnorderedAccessView*[UavCountMax] + private uint instCount; + private int uavCount; + + /// + /// Creates a new instance of from . + /// + /// The feature level. + /// The device context. + /// The captured state. + public static ComputeShaderState From(D3D_FEATURE_LEVEL featureLevel, ID3D11DeviceContext* ctx) + { + var state = default(ComputeShaderState); + state.uavCount = featureLevel >= D3D_FEATURE_LEVEL.D3D_FEATURE_LEVEL_11_1 + ? TerraFX.Interop.DirectX.D3D11.D3D11_1_UAV_SLOT_COUNT + : TerraFX.Interop.DirectX.D3D11.D3D11_PS_CS_UAV_REGISTER_COUNT; + state.context.Attach(ctx); + ctx->AddRef(); + state.instCount = InstanceCount; + ctx->CSGetShader(state.shader.GetAddressOf(), (ID3D11ClassInstance**)state.insts, &state.instCount); + ctx->CSGetConstantBuffers(0, BufferCount, (ID3D11Buffer**)state.buffers); + ctx->CSGetSamplers(0, SamplerCount, (ID3D11SamplerState**)state.samplers); + ctx->CSGetShaderResources(0, ResourceCount, (ID3D11ShaderResourceView**)state.resources); + ctx->CSGetUnorderedAccessViews(0, (uint)state.uavCount, (ID3D11UnorderedAccessView**)state.uavs); + return state; + } + + /// + public void Dispose() + { + var ctx = this.context.Get(); + if (ctx is null) + return; + + fixed (ComputeShaderState* pThis = &this) + { + ctx->CSSetShader(pThis->shader, (ID3D11ClassInstance**)pThis->insts, pThis->instCount); + ctx->CSSetConstantBuffers(0, BufferCount, (ID3D11Buffer**)pThis->buffers); + ctx->CSSetSamplers(0, SamplerCount, (ID3D11SamplerState**)pThis->samplers); + ctx->CSSetShaderResources(0, ResourceCount, (ID3D11ShaderResourceView**)pThis->resources); + ctx->CSSetUnorderedAccessViews(0, (uint)this.uavCount, (ID3D11UnorderedAccessView**)pThis->uavs, null); + + foreach (ref var b in new Span>(pThis->buffers, BufferCount)) + b.Dispose(); + foreach (ref var b in new Span>(pThis->samplers, SamplerCount)) + b.Dispose(); + foreach (ref var b in new Span>(pThis->resources, ResourceCount)) + b.Dispose(); + foreach (ref var b in new Span>(pThis->insts, (int)pThis->instCount)) + b.Dispose(); + foreach (ref var b in new Span>(pThis->uavs, this.uavCount)) + b.Dispose(); + pThis->context.Dispose(); + pThis->shader.Dispose(); + } + } + } +} diff --git a/Dalamud/Interface/ImGuiBackend/Helpers/D3D11/Extensions.cs b/Dalamud/Interface/ImGuiBackend/Helpers/D3D11/Extensions.cs new file mode 100644 index 000000000..4d81cb5fc --- /dev/null +++ b/Dalamud/Interface/ImGuiBackend/Helpers/D3D11/Extensions.cs @@ -0,0 +1,26 @@ +using System.Text; + +using Dalamud.Utility; + +using TerraFX.Interop.DirectX; + +namespace Dalamud.Interface.ImGuiBackend.Helpers.D3D11; + +/// Utility extension methods for D3D11 objects. +internal static class Extensions +{ + /// Sets the name for debugging. + /// D3D11 object. + /// Debug name. + /// Object type. + public static unsafe void SetDebugName(ref this T child, string name) + where T : unmanaged, ID3D11DeviceChild.Interface + { + var len = Encoding.UTF8.GetByteCount(name); + var buf = stackalloc byte[len + 1]; + Encoding.UTF8.GetBytes(name, new(buf, len + 1)); + buf[len] = 0; + fixed (Guid* pId = &DirectX.WKPDID_D3DDebugObjectName) + child.SetPrivateData(pId, (uint)(len + 1), buf).ThrowOnError(); + } +} diff --git a/Dalamud/Interface/ImGuiBackend/Helpers/ImGuiViewportHelpers.cs b/Dalamud/Interface/ImGuiBackend/Helpers/ImGuiViewportHelpers.cs new file mode 100644 index 000000000..c951eab1b --- /dev/null +++ b/Dalamud/Interface/ImGuiBackend/Helpers/ImGuiViewportHelpers.cs @@ -0,0 +1,165 @@ +using System.Diagnostics; +using System.Linq; +using System.Numerics; +using System.Runtime.InteropServices; + +using ImGuiNET; + +using TerraFX.Interop.Windows; + +using static TerraFX.Interop.Windows.Windows; + +namespace Dalamud.Interface.ImGuiBackend.Helpers; + +/// +/// Helpers for using ImGui Viewports. +/// +internal static class ImGuiViewportHelpers +{ + /// + /// Delegate to be called when a window should be created. + /// + /// An instance of . + public delegate void CreateWindowDelegate(ImGuiViewportPtr viewport); + + /// + /// Delegate to be called when a window should be destroyed. + /// + /// An instance of . + public delegate void DestroyWindowDelegate(ImGuiViewportPtr viewport); + + /// + /// Delegate to be called when a window should be resized. + /// + /// An instance of . + /// Size of the new window. + public delegate void SetWindowSizeDelegate(ImGuiViewportPtr viewport, Vector2 size); + + /// + /// Delegate to be called when a window should be rendered. + /// + /// An instance of . + /// Custom user-provided argument from . + public delegate void RenderWindowDelegate(ImGuiViewportPtr viewport, nint v); + + /// + /// Delegate to be called when buffers for the window should be swapped. + /// + /// An instance of . + /// Custom user-provided argument from . + public delegate void SwapBuffersDelegate(ImGuiViewportPtr viewport, nint v); + + /// + /// Delegate to be called when the window should be showed. + /// + /// An instance of . + public delegate void ShowWindowDelegate(ImGuiViewportPtr viewport); + + /// + /// Delegate to be called when the window should be updated. + /// + /// An instance of . + public delegate void UpdateWindowDelegate(ImGuiViewportPtr viewport); + + /// + /// Delegate to be called when the window position is queried. + /// + /// The return value storage. + /// An instance of . + /// Same value with . + public unsafe delegate Vector2* GetWindowPosDelegate(Vector2* returnStorage, ImGuiViewportPtr viewport); + + /// + /// Delegate to be called when the window should be moved. + /// + /// An instance of . + /// The new position. + public delegate void SetWindowPosDelegate(ImGuiViewportPtr viewport, Vector2 pos); + + /// + /// Delegate to be called when the window size is queried. + /// + /// The return value storage. + /// An instance of . + /// Same value with . + public unsafe delegate Vector2* GetWindowSizeDelegate(Vector2* returnStorage, ImGuiViewportPtr viewport); + + /// + /// Delegate to be called when the window should be given focus. + /// + /// An instance of . + public delegate void SetWindowFocusDelegate(ImGuiViewportPtr viewport); + + /// + /// Delegate to be called when the window is focused. + /// + /// An instance of . + /// Whether the window is focused. + public delegate bool GetWindowFocusDelegate(ImGuiViewportPtr viewport); + + /// + /// Delegate to be called when whether the window is minimized is queried. + /// + /// An instance of . + /// Whether the window is minimized. + public delegate bool GetWindowMinimizedDelegate(ImGuiViewportPtr viewport); + + /// + /// Delegate to be called when the window title should be changed. + /// + /// An instance of . + /// The new title. + public delegate void SetWindowTitleDelegate(ImGuiViewportPtr viewport, string title); + + /// + /// Delegate to be called when the window alpha should be changed. + /// + /// An instance of . + /// The new alpha. + public delegate void SetWindowAlphaDelegate(ImGuiViewportPtr viewport, float alpha); + + /// + /// Delegate to be called when the IME input position should be changed. + /// + /// An instance of . + /// The new position. + public delegate void SetImeInputPosDelegate(ImGuiViewportPtr viewport, Vector2 pos); + + /// + /// Delegate to be called when the window's DPI scale value is queried. + /// + /// An instance of . + /// The DPI scale. + public delegate float GetWindowDpiScaleDelegate(ImGuiViewportPtr viewport); + + /// + /// Delegate to be called when viewport is changed. + /// + /// An instance of . + public delegate void ChangedViewportDelegate(ImGuiViewportPtr viewport); + + /// + /// Disables ImGui from disabling alpha for Viewport window backgrounds. + /// + public static unsafe void EnableViewportWindowBackgroundAlpha() + { + // TODO: patch imgui.cpp:6126, which disables background transparency for extra viewport windows + var offset = 0x00007FFB6ADA632C - 0x00007FFB6AD60000; + offset += Process.GetCurrentProcess().Modules.Cast().First(x => x.ModuleName == "cimgui.dll") + .BaseAddress; + var b = (byte*)offset; + uint old; + if (!VirtualProtect(b, 1, PAGE.PAGE_EXECUTE_READWRITE, &old)) + { + throw Marshal.GetExceptionForHR(Marshal.GetHRForLastWin32Error()) + ?? throw new InvalidOperationException($"{nameof(VirtualProtect)} failed."); + } + + *b = 0xEB; + if (!VirtualProtect(b, 1, old, &old)) + { + throw Marshal.GetExceptionForHR(Marshal.GetHRForLastWin32Error()) + ?? throw new InvalidOperationException($"{nameof(VirtualProtect)} failed."); + } + } +} diff --git a/Dalamud/Interface/ImGuiBackend/Helpers/ReShadePeeler.cs b/Dalamud/Interface/ImGuiBackend/Helpers/ReShadePeeler.cs new file mode 100644 index 000000000..824ba382a --- /dev/null +++ b/Dalamud/Interface/ImGuiBackend/Helpers/ReShadePeeler.cs @@ -0,0 +1,230 @@ +using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +using TerraFX.Interop.DirectX; +using TerraFX.Interop.Windows; + +using static TerraFX.Interop.Windows.Windows; + +namespace Dalamud.Interface.ImGuiBackend.Helpers; + +/// +/// Peels ReShade off stuff. +/// +[SuppressMessage( + "StyleCop.CSharp.LayoutRules", + "SA1519:Braces should not be omitted from multi-line child statement", + Justification = "Multiple fixed blocks")] +internal static unsafe class ReShadePeeler +{ + /// + /// Peels if it is wrapped by ReShade. + /// + /// [inout] The COM pointer to an instance of . + /// A COM type that is or extends . + /// true if peeled. + public static bool PeelSwapChain(ComPtr* comptr) + where T : unmanaged, IDXGISwapChain.Interface => + PeelIUnknown(comptr, sizeof(IDXGISwapChain.Vtbl)); + + private static bool PeelIUnknown(ComPtr* comptr, nint vtblSize) + where T : unmanaged, IUnknown.Interface + { + var changed = false; + while (comptr->Get() != null && IsReShadedComObject(comptr->Get())) + { + // Expectation: the pointer to the underlying object should come early after the overriden vtable. + for (nint i = 8; i <= 0x20; i += 8) + { + var ppObjectBehind = (nint)comptr->Get() + i; + + // Is the thing directly pointed from the address an actual something in the memory? + if (!IsValidReadableMemoryAddress(ppObjectBehind, 8)) + continue; + + var pObjectBehind = *(nint*)ppObjectBehind; + + // Is the address of vtable readable? + if (!IsValidReadableMemoryAddress(pObjectBehind, sizeof(nint))) + continue; + var pObjectBehindVtbl = *(nint*)pObjectBehind; + + // Is the vtable itself readable? + if (!IsValidReadableMemoryAddress(pObjectBehindVtbl, vtblSize)) + continue; + + // Are individual functions in vtable executable? + var valid = true; + for (var j = 0; valid && j < vtblSize; j += sizeof(nint)) + valid &= IsValidExecutableMemoryAddress(*(nint*)(pObjectBehindVtbl + j), 1); + if (!valid) + continue; + + // Interpret the object as an IUnknown. + // Note that `using` is not used, and `Attach` is used. We do not alter the reference count yet. + var punk = default(ComPtr); + punk.Attach((IUnknown*)pObjectBehind); + + // Is the IUnknown object also the type we want? + using var comptr2 = default(ComPtr); + if (punk.As(&comptr2).FAILED) + continue; + + comptr2.Swap(comptr); + changed = true; + break; + } + + if (!changed) + break; + } + + return changed; + } + + private static bool BelongsInReShadeDll(nint ptr) + { + foreach (ProcessModule processModule in Process.GetCurrentProcess().Modules) + { + if (ptr < processModule.BaseAddress) + 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 + { + if (!IsValidReadableMemoryAddress((nint)obj, sizeof(nint))) + return false; + + try + { + var vtbl = (nint**)Marshal.ReadIntPtr((nint)obj); + if (!IsValidReadableMemoryAddress((nint)vtbl, sizeof(nint) * 3)) + return false; + + for (var i = 0; i < 3; i++) + { + var pfn = Marshal.ReadIntPtr((nint)(vtbl + i)); + if (!IsValidExecutableMemoryAddress(pfn, 1)) + return false; + if (!BelongsInReShadeDll(pfn)) + return false; + } + + return true; + } + catch + { + return false; + } + } + + private static bool IsValidReadableMemoryAddress(nint p, nint size) + { + while (size > 0) + { + if (!IsValidUserspaceMemoryAddress(p)) + return false; + + MEMORY_BASIC_INFORMATION mbi; + if (VirtualQuery((void*)p, &mbi, (nuint)sizeof(MEMORY_BASIC_INFORMATION)) == 0) + return false; + + if (mbi is not + { + State: MEM.MEM_COMMIT, + Protect: PAGE.PAGE_READONLY or PAGE.PAGE_READWRITE or PAGE.PAGE_EXECUTE_READ + or PAGE.PAGE_EXECUTE_READWRITE, + }) + return false; + + var regionSize = (nint)((mbi.RegionSize + 0xFFFUL) & ~0x1000UL); + var checkedSize = ((nint)mbi.BaseAddress + regionSize) - p; + size -= checkedSize; + p += checkedSize; + } + + return true; + } + + private static bool IsValidExecutableMemoryAddress(nint p, nint size) + { + while (size > 0) + { + if (!IsValidUserspaceMemoryAddress(p)) + return false; + + MEMORY_BASIC_INFORMATION mbi; + if (VirtualQuery((void*)p, &mbi, (nuint)sizeof(MEMORY_BASIC_INFORMATION)) == 0) + return false; + + if (mbi is not + { + State: MEM.MEM_COMMIT, + Protect: PAGE.PAGE_EXECUTE or PAGE.PAGE_EXECUTE_READ or PAGE.PAGE_EXECUTE_READWRITE + or PAGE.PAGE_EXECUTE_WRITECOPY, + }) + return false; + + var regionSize = (nint)((mbi.RegionSize + 0xFFFUL) & ~0x1000UL); + var checkedSize = ((nint)mbi.BaseAddress + regionSize) - p; + size -= checkedSize; + p += checkedSize; + } + + return true; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static bool IsValidUserspaceMemoryAddress(nint p) + { + // https://learn.microsoft.com/en-us/windows-hardware/drivers/gettingstarted/virtual-address-spaces + // A 64-bit process on 64-bit Windows has a virtual address space within the 128-terabyte range + // 0x000'00000000 through 0x7FFF'FFFFFFFF. + return p >= 0x10000 && p <= unchecked((nint)0x7FFF_FFFFFFFFUL); + } +} diff --git a/Dalamud/Interface/ImGuiBackend/IImGuiBackend.cs b/Dalamud/Interface/ImGuiBackend/IImGuiBackend.cs new file mode 100644 index 000000000..9248b4b5a --- /dev/null +++ b/Dalamud/Interface/ImGuiBackend/IImGuiBackend.cs @@ -0,0 +1,72 @@ +using Dalamud.Interface.ImGuiBackend.InputHandler; +using Dalamud.Interface.ImGuiBackend.Renderers; + +namespace Dalamud.Interface.ImGuiBackend; + +/// Backend for ImGui. +internal interface IImGuiBackend : IDisposable +{ + /// Delegate to be called when ImGui should be used to layout now. + public delegate void BuildUiDelegate(); + + /// Delegate to be called on new input frame. + public delegate void NewInputFrameDelegate(); + + /// Delegaet to be called on new render frame. + public delegate void NewRenderFrameDelegate(); + + /// User methods invoked every ImGui frame to construct custom UIs. + event BuildUiDelegate? BuildUi; + + /// User methods invoked every ImGui frame on handling inputs. + event NewInputFrameDelegate? NewInputFrame; + + /// User methods invoked every ImGui frame on handling renders. + event NewRenderFrameDelegate? NewRenderFrame; + + /// Gets or sets a value indicating whether the cursor should be overridden with the ImGui cursor. + /// + bool UpdateCursor { get; set; } + + /// Gets or sets the path of ImGui configuration .ini file. + string? IniPath { get; set; } + + /// Gets the device handle. + nint DeviceHandle { get; } + + /// Gets the input handler. + IImGuiInputHandler InputHandler { get; } + + /// Gets the renderer. + IImGuiRenderer Renderer { get; } + + /// Performs a render cycle. + void Render(); + + /// Handles stuff before resizing happens. + void OnPreResize(); + + /// Handles stuff after resizing happens. + /// The new width. + /// The new height. + void OnPostResize(int newWidth, int newHeight); + + /// Invalidates fonts immediately. + /// Call this while handling . + void InvalidateFonts(); + + /// Determines if is owned by this. + /// The cursor. + /// Whether it is the case. + bool IsImGuiCursor(nint cursorHandle); + + /// Determines if this instance of is rendering to + /// . + /// The present target handle. + /// Whether it is the case. + bool IsAttachedToPresentationTarget(nint targetHandle); + + /// Determines if the main viewport is full screen. + /// Whether it is the case. + bool IsMainViewportFullScreen(); +} diff --git a/Dalamud/Interface/ImGuiBackend/IWin32Backend.cs b/Dalamud/Interface/ImGuiBackend/IWin32Backend.cs new file mode 100644 index 000000000..0b158dfbc --- /dev/null +++ b/Dalamud/Interface/ImGuiBackend/IWin32Backend.cs @@ -0,0 +1,15 @@ +using TerraFX.Interop.Windows; + +namespace Dalamud.Interface.ImGuiBackend; + +/// with Win32 support. +internal interface IWin32Backend : IImGuiBackend +{ + /// Processes window messages. + /// Handle of the window. + /// Type of window message. + /// wParam. + /// lParam. + /// Return value. + public nint? ProcessWndProcW(HWND hWnd, uint msg, WPARAM wParam, LPARAM lParam); +} diff --git a/Dalamud/Interface/ImGuiBackend/InputHandler/IImGuiInputHandler.cs b/Dalamud/Interface/ImGuiBackend/InputHandler/IImGuiInputHandler.cs new file mode 100644 index 000000000..e6da2281e --- /dev/null +++ b/Dalamud/Interface/ImGuiBackend/InputHandler/IImGuiInputHandler.cs @@ -0,0 +1,22 @@ +namespace Dalamud.Interface.ImGuiBackend.InputHandler; + +/// A simple shared public interface that all ImGui input implementations follows. +internal interface IImGuiInputHandler : IDisposable +{ + /// Gets or sets a value indicating whether the cursor should be overridden with the ImGui cursor. + /// + public bool UpdateCursor { get; set; } + + /// Gets or sets the path of ImGui configuration .ini file. + string? IniPath { get; set; } + + /// Determines if is owned by this. + /// The cursor. + /// Whether it is the case. + public bool IsImGuiCursor(nint cursorHandle); + + /// Marks the beginning of a new frame. + /// The width of the new frame. + /// The height of the new frame. + void NewFrame(int width, int height); +} diff --git a/Dalamud/Interface/ImGuiBackend/InputHandler/Win32InputHandler.StaticLookupFunctions.cs b/Dalamud/Interface/ImGuiBackend/InputHandler/Win32InputHandler.StaticLookupFunctions.cs new file mode 100644 index 000000000..7732f0867 --- /dev/null +++ b/Dalamud/Interface/ImGuiBackend/InputHandler/Win32InputHandler.StaticLookupFunctions.cs @@ -0,0 +1,309 @@ +using System.Runtime.CompilerServices; + +using ImGuiNET; + +using TerraFX.Interop.Windows; + +namespace Dalamud.Interface.ImGuiBackend.InputHandler; + +/// +/// An implementation of , using Win32 APIs. +/// +internal sealed partial class Win32InputHandler +{ + /// + /// Maps a to . + /// + /// The virtual key. + /// The corresponding . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static ImGuiKey VirtualKeyToImGuiKey(int key) => key switch + { + VK.VK_TAB => ImGuiKey.Tab, + VK.VK_LEFT => ImGuiKey.LeftArrow, + VK.VK_RIGHT => ImGuiKey.RightArrow, + VK.VK_UP => ImGuiKey.UpArrow, + VK.VK_DOWN => ImGuiKey.DownArrow, + VK.VK_PRIOR => ImGuiKey.PageUp, + VK.VK_NEXT => ImGuiKey.PageDown, + VK.VK_HOME => ImGuiKey.Home, + VK.VK_END => ImGuiKey.End, + VK.VK_INSERT => ImGuiKey.Insert, + VK.VK_DELETE => ImGuiKey.Delete, + VK.VK_BACK => ImGuiKey.Backspace, + VK.VK_SPACE => ImGuiKey.Space, + VK.VK_RETURN => ImGuiKey.Enter, + VK.VK_ESCAPE => ImGuiKey.Escape, + VK.VK_OEM_7 => ImGuiKey.Apostrophe, + VK.VK_OEM_COMMA => ImGuiKey.Comma, + VK.VK_OEM_MINUS => ImGuiKey.Minus, + VK.VK_OEM_PERIOD => ImGuiKey.Period, + VK.VK_OEM_2 => ImGuiKey.Slash, + VK.VK_OEM_1 => ImGuiKey.Semicolon, + VK.VK_OEM_PLUS => ImGuiKey.Equal, + VK.VK_OEM_4 => ImGuiKey.LeftBracket, + VK.VK_OEM_5 => ImGuiKey.Backslash, + VK.VK_OEM_6 => ImGuiKey.RightBracket, + VK.VK_OEM_3 => ImGuiKey.GraveAccent, + VK.VK_CAPITAL => ImGuiKey.CapsLock, + VK.VK_SCROLL => ImGuiKey.ScrollLock, + VK.VK_NUMLOCK => ImGuiKey.NumLock, + VK.VK_SNAPSHOT => ImGuiKey.PrintScreen, + VK.VK_PAUSE => ImGuiKey.Pause, + VK.VK_NUMPAD0 => ImGuiKey.Keypad0, + VK.VK_NUMPAD1 => ImGuiKey.Keypad1, + VK.VK_NUMPAD2 => ImGuiKey.Keypad2, + VK.VK_NUMPAD3 => ImGuiKey.Keypad3, + VK.VK_NUMPAD4 => ImGuiKey.Keypad4, + VK.VK_NUMPAD5 => ImGuiKey.Keypad5, + VK.VK_NUMPAD6 => ImGuiKey.Keypad6, + VK.VK_NUMPAD7 => ImGuiKey.Keypad7, + VK.VK_NUMPAD8 => ImGuiKey.Keypad8, + VK.VK_NUMPAD9 => ImGuiKey.Keypad9, + VK.VK_DECIMAL => ImGuiKey.KeypadDecimal, + VK.VK_DIVIDE => ImGuiKey.KeypadDivide, + VK.VK_MULTIPLY => ImGuiKey.KeypadMultiply, + VK.VK_SUBTRACT => ImGuiKey.KeypadSubtract, + VK.VK_ADD => ImGuiKey.KeypadAdd, + VK.VK_RETURN + 256 => ImGuiKey.KeypadEnter, + VK.VK_LSHIFT => ImGuiKey.LeftShift, + VK.VK_LCONTROL => ImGuiKey.LeftCtrl, + VK.VK_LMENU => ImGuiKey.LeftAlt, + VK.VK_LWIN => ImGuiKey.LeftSuper, + VK.VK_RSHIFT => ImGuiKey.RightShift, + VK.VK_RCONTROL => ImGuiKey.RightCtrl, + VK.VK_RMENU => ImGuiKey.RightAlt, + VK.VK_RWIN => ImGuiKey.RightSuper, + VK.VK_APPS => ImGuiKey.Menu, + '0' => ImGuiKey._0, + '1' => ImGuiKey._1, + '2' => ImGuiKey._2, + '3' => ImGuiKey._3, + '4' => ImGuiKey._4, + '5' => ImGuiKey._5, + '6' => ImGuiKey._6, + '7' => ImGuiKey._7, + '8' => ImGuiKey._8, + '9' => ImGuiKey._9, + 'A' => ImGuiKey.A, + 'B' => ImGuiKey.B, + 'C' => ImGuiKey.C, + 'D' => ImGuiKey.D, + 'E' => ImGuiKey.E, + 'F' => ImGuiKey.F, + 'G' => ImGuiKey.G, + 'H' => ImGuiKey.H, + 'I' => ImGuiKey.I, + 'J' => ImGuiKey.J, + 'K' => ImGuiKey.K, + 'L' => ImGuiKey.L, + 'M' => ImGuiKey.M, + 'N' => ImGuiKey.N, + 'O' => ImGuiKey.O, + 'P' => ImGuiKey.P, + 'Q' => ImGuiKey.Q, + 'R' => ImGuiKey.R, + 'S' => ImGuiKey.S, + 'T' => ImGuiKey.T, + 'U' => ImGuiKey.U, + 'V' => ImGuiKey.V, + 'W' => ImGuiKey.W, + 'X' => ImGuiKey.X, + 'Y' => ImGuiKey.Y, + 'Z' => ImGuiKey.Z, + VK.VK_F1 => ImGuiKey.F1, + VK.VK_F2 => ImGuiKey.F2, + VK.VK_F3 => ImGuiKey.F3, + VK.VK_F4 => ImGuiKey.F4, + VK.VK_F5 => ImGuiKey.F5, + VK.VK_F6 => ImGuiKey.F6, + VK.VK_F7 => ImGuiKey.F7, + VK.VK_F8 => ImGuiKey.F8, + VK.VK_F9 => ImGuiKey.F9, + VK.VK_F10 => ImGuiKey.F10, + VK.VK_F11 => ImGuiKey.F11, + VK.VK_F12 => ImGuiKey.F12, + _ => ImGuiKey.None, + }; + + /// + /// Maps a to . + /// + /// The ImGui key. + /// The corresponding . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static int ImGuiKeyToVirtualKey(ImGuiKey key) => key switch + { + ImGuiKey.Tab => VK.VK_TAB, + ImGuiKey.LeftArrow => VK.VK_LEFT, + ImGuiKey.RightArrow => VK.VK_RIGHT, + ImGuiKey.UpArrow => VK.VK_UP, + ImGuiKey.DownArrow => VK.VK_DOWN, + ImGuiKey.PageUp => VK.VK_PRIOR, + ImGuiKey.PageDown => VK.VK_NEXT, + ImGuiKey.Home => VK.VK_HOME, + ImGuiKey.End => VK.VK_END, + ImGuiKey.Insert => VK.VK_INSERT, + ImGuiKey.Delete => VK.VK_DELETE, + ImGuiKey.Backspace => VK.VK_BACK, + ImGuiKey.Space => VK.VK_SPACE, + ImGuiKey.Enter => VK.VK_RETURN, + ImGuiKey.Escape => VK.VK_ESCAPE, + ImGuiKey.Apostrophe => VK.VK_OEM_7, + ImGuiKey.Comma => VK.VK_OEM_COMMA, + ImGuiKey.Minus => VK.VK_OEM_MINUS, + ImGuiKey.Period => VK.VK_OEM_PERIOD, + ImGuiKey.Slash => VK.VK_OEM_2, + ImGuiKey.Semicolon => VK.VK_OEM_1, + ImGuiKey.Equal => VK.VK_OEM_PLUS, + ImGuiKey.LeftBracket => VK.VK_OEM_4, + ImGuiKey.Backslash => VK.VK_OEM_5, + ImGuiKey.RightBracket => VK.VK_OEM_6, + ImGuiKey.GraveAccent => VK.VK_OEM_3, + ImGuiKey.CapsLock => VK.VK_CAPITAL, + ImGuiKey.ScrollLock => VK.VK_SCROLL, + ImGuiKey.NumLock => VK.VK_NUMLOCK, + ImGuiKey.PrintScreen => VK.VK_SNAPSHOT, + ImGuiKey.Pause => VK.VK_PAUSE, + ImGuiKey.Keypad0 => VK.VK_NUMPAD0, + ImGuiKey.Keypad1 => VK.VK_NUMPAD1, + ImGuiKey.Keypad2 => VK.VK_NUMPAD2, + ImGuiKey.Keypad3 => VK.VK_NUMPAD3, + ImGuiKey.Keypad4 => VK.VK_NUMPAD4, + ImGuiKey.Keypad5 => VK.VK_NUMPAD5, + ImGuiKey.Keypad6 => VK.VK_NUMPAD6, + ImGuiKey.Keypad7 => VK.VK_NUMPAD7, + ImGuiKey.Keypad8 => VK.VK_NUMPAD8, + ImGuiKey.Keypad9 => VK.VK_NUMPAD9, + ImGuiKey.KeypadDecimal => VK.VK_DECIMAL, + ImGuiKey.KeypadDivide => VK.VK_DIVIDE, + ImGuiKey.KeypadMultiply => VK.VK_MULTIPLY, + ImGuiKey.KeypadSubtract => VK.VK_SUBTRACT, + ImGuiKey.KeypadAdd => VK.VK_ADD, + ImGuiKey.KeypadEnter => VK.VK_RETURN + 256, + ImGuiKey.LeftShift => VK.VK_LSHIFT, + ImGuiKey.LeftCtrl => VK.VK_LCONTROL, + ImGuiKey.LeftAlt => VK.VK_LMENU, + ImGuiKey.LeftSuper => VK.VK_LWIN, + ImGuiKey.RightShift => VK.VK_RSHIFT, + ImGuiKey.RightCtrl => VK.VK_RCONTROL, + ImGuiKey.RightAlt => VK.VK_RMENU, + ImGuiKey.RightSuper => VK.VK_RWIN, + ImGuiKey.Menu => VK.VK_APPS, + ImGuiKey._0 => '0', + ImGuiKey._1 => '1', + ImGuiKey._2 => '2', + ImGuiKey._3 => '3', + ImGuiKey._4 => '4', + ImGuiKey._5 => '5', + ImGuiKey._6 => '6', + ImGuiKey._7 => '7', + ImGuiKey._8 => '8', + ImGuiKey._9 => '9', + ImGuiKey.A => 'A', + ImGuiKey.B => 'B', + ImGuiKey.C => 'C', + ImGuiKey.D => 'D', + ImGuiKey.E => 'E', + ImGuiKey.F => 'F', + ImGuiKey.G => 'G', + ImGuiKey.H => 'H', + ImGuiKey.I => 'I', + ImGuiKey.J => 'J', + ImGuiKey.K => 'K', + ImGuiKey.L => 'L', + ImGuiKey.M => 'M', + ImGuiKey.N => 'N', + ImGuiKey.O => 'O', + ImGuiKey.P => 'P', + ImGuiKey.Q => 'Q', + ImGuiKey.R => 'R', + ImGuiKey.S => 'S', + ImGuiKey.T => 'T', + ImGuiKey.U => 'U', + ImGuiKey.V => 'V', + ImGuiKey.W => 'W', + ImGuiKey.X => 'X', + ImGuiKey.Y => 'Y', + ImGuiKey.Z => 'Z', + ImGuiKey.F1 => VK.VK_F1, + ImGuiKey.F2 => VK.VK_F2, + ImGuiKey.F3 => VK.VK_F3, + ImGuiKey.F4 => VK.VK_F4, + ImGuiKey.F5 => VK.VK_F5, + ImGuiKey.F6 => VK.VK_F6, + ImGuiKey.F7 => VK.VK_F7, + ImGuiKey.F8 => VK.VK_F8, + ImGuiKey.F9 => VK.VK_F9, + ImGuiKey.F10 => VK.VK_F10, + ImGuiKey.F11 => VK.VK_F11, + ImGuiKey.F12 => VK.VK_F12, + _ => 0, + }; + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static bool IsGamepadKey(ImGuiKey key) => (int)key is >= 617 and <= 640; + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static bool IsModKey(ImGuiKey key) => + key is ImGuiKey.LeftShift + or ImGuiKey.RightShift + or ImGuiKey.ModShift + or ImGuiKey.LeftCtrl + or ImGuiKey.ModCtrl + or ImGuiKey.LeftAlt + or ImGuiKey.RightAlt + or ImGuiKey.ModAlt; + + private static void AddKeyEvent(ImGuiKey key, bool down, int nativeKeycode, int nativeScancode = -1) + { + var io = ImGui.GetIO(); + io.AddKeyEvent(key, down); + io.SetKeyEventNativeData(key, nativeKeycode, nativeScancode); + } + + private static void UpdateKeyModifiers() + { + var io = ImGui.GetIO(); + io.AddKeyEvent(ImGuiKey.ModCtrl, IsVkDown(VK.VK_CONTROL)); + io.AddKeyEvent(ImGuiKey.ModShift, IsVkDown(VK.VK_SHIFT)); + io.AddKeyEvent(ImGuiKey.ModAlt, IsVkDown(VK.VK_MENU)); + io.AddKeyEvent(ImGuiKey.ModSuper, IsVkDown(VK.VK_APPS)); + } + + private static void UpAllKeys() + { + var io = ImGui.GetIO(); + for (var i = (int)ImGuiKey.NamedKey_BEGIN; i < (int)ImGuiKey.NamedKey_END; i++) + io.AddKeyEvent((ImGuiKey)i, false); + } + + private static void UpAllMouseButton() + { + var io = ImGui.GetIO(); + for (var i = 0; i < io.MouseDown.Count; i++) + io.MouseDown[i] = false; + } + + private static bool IsVkDown(int key) => (TerraFX.Interop.Windows.Windows.GetKeyState(key) & 0x8000) != 0; + + private static int GetButton(uint msg, WPARAM wParam) => msg switch + { + WM.WM_LBUTTONUP or WM.WM_LBUTTONDOWN or WM.WM_LBUTTONDBLCLK => 0, + WM.WM_RBUTTONUP or WM.WM_RBUTTONDOWN or WM.WM_RBUTTONDBLCLK => 1, + WM.WM_MBUTTONUP or WM.WM_MBUTTONDOWN or WM.WM_MBUTTONDBLCLK => 2, + WM.WM_XBUTTONUP or WM.WM_XBUTTONDOWN or WM.WM_XBUTTONDBLCLK => + TerraFX.Interop.Windows.Windows.GET_XBUTTON_WPARAM(wParam) == TerraFX.Interop.Windows.Windows.XBUTTON1 ? 3 : 4, + _ => 0, + }; + + private static void ViewportFlagsToWin32Styles(ImGuiViewportFlags flags, out int style, out int exStyle) + { + style = (int)(flags.HasFlag(ImGuiViewportFlags.NoDecoration) ? WS.WS_POPUP : WS.WS_OVERLAPPEDWINDOW); + exStyle = + (int)(flags.HasFlag(ImGuiViewportFlags.NoTaskBarIcon) ? WS.WS_EX_TOOLWINDOW : (uint)WS.WS_EX_APPWINDOW); + exStyle |= WS.WS_EX_NOREDIRECTIONBITMAP; + if (flags.HasFlag(ImGuiViewportFlags.TopMost)) + exStyle |= WS.WS_EX_TOPMOST; + } +} diff --git a/Dalamud/Interface/ImGuiBackend/InputHandler/Win32InputHandler.cs b/Dalamud/Interface/ImGuiBackend/InputHandler/Win32InputHandler.cs new file mode 100644 index 000000000..18fd82651 --- /dev/null +++ b/Dalamud/Interface/ImGuiBackend/InputHandler/Win32InputHandler.cs @@ -0,0 +1,1016 @@ +using System.Collections.Generic; +using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; +using System.Linq; +using System.Numerics; +using System.Runtime.InteropServices; +using System.Text; + +using ImGuiNET; + +using TerraFX.Interop.Windows; + +using static Dalamud.Interface.ImGuiBackend.Helpers.ImGuiViewportHelpers; + +using static TerraFX.Interop.Windows.Windows; + +using ERROR = TerraFX.Interop.Windows.ERROR; + +namespace Dalamud.Interface.ImGuiBackend.InputHandler; + +/// +/// An implementation of , using Win32 APIs.
+/// Largely a port of https://github.com/ocornut/imgui/blob/master/examples/imgui_impl_win32.cpp, +/// though some changes and wndproc hooking. +///
+[SuppressMessage( + "StyleCop.CSharp.LayoutRules", + "SA1519:Braces should not be omitted from multi-line child statement", + Justification = "Multiple fixed/using scopes")] +internal sealed unsafe partial class Win32InputHandler : IImGuiInputHandler +{ + private readonly HWND hWnd; + private readonly HCURSOR[] cursors; + + private readonly WndProcDelegate wndProcDelegate; + private readonly bool[] imguiMouseIsDown; + private readonly nint platformNamePtr; + + private ViewportHandler viewportHandler; + + private long lastTime; + + private nint iniPathPtr; + + private bool disposedValue; + + /// + /// Initializes a new instance of the class. + /// + /// The window handle. + public Win32InputHandler(HWND hWnd) + { + var io = ImGui.GetIO(); + if (ImGui.GetIO().NativePtr->BackendPlatformName is not null) + throw new InvalidOperationException("ImGui backend platform seems to be have been already attached."); + + this.hWnd = hWnd; + + // hook wndproc + // have to hold onto the delegate to keep it in memory for unmanaged code + this.wndProcDelegate = this.WndProcDetour; + + io.BackendFlags |= ImGuiBackendFlags.HasMouseCursors | + ImGuiBackendFlags.HasSetMousePos | + ImGuiBackendFlags.RendererHasViewports | + ImGuiBackendFlags.PlatformHasViewports; + + this.platformNamePtr = Marshal.StringToHGlobalAnsi("imgui_impl_win32_c#"); + io.NativePtr->BackendPlatformName = (byte*)this.platformNamePtr; + + var mainViewport = ImGui.GetMainViewport(); + mainViewport.PlatformHandle = mainViewport.PlatformHandleRaw = hWnd; + if (io.ConfigFlags.HasFlag(ImGuiConfigFlags.ViewportsEnable)) + this.viewportHandler = new(this); + + this.imguiMouseIsDown = new bool[5]; + + this.cursors = new HCURSOR[9]; + this.cursors[(int)ImGuiMouseCursor.Arrow] = LoadCursorW(default, IDC.IDC_ARROW); + this.cursors[(int)ImGuiMouseCursor.TextInput] = LoadCursorW(default, IDC.IDC_IBEAM); + this.cursors[(int)ImGuiMouseCursor.ResizeAll] = LoadCursorW(default, IDC.IDC_SIZEALL); + this.cursors[(int)ImGuiMouseCursor.ResizeEW] = LoadCursorW(default, IDC.IDC_SIZEWE); + this.cursors[(int)ImGuiMouseCursor.ResizeNS] = LoadCursorW(default, IDC.IDC_SIZENS); + this.cursors[(int)ImGuiMouseCursor.ResizeNESW] = LoadCursorW(default, IDC.IDC_SIZENESW); + this.cursors[(int)ImGuiMouseCursor.ResizeNWSE] = LoadCursorW(default, IDC.IDC_SIZENWSE); + this.cursors[(int)ImGuiMouseCursor.Hand] = LoadCursorW(default, IDC.IDC_HAND); + this.cursors[(int)ImGuiMouseCursor.NotAllowed] = LoadCursorW(default, IDC.IDC_NO); + } + + /// + /// Finalizes an instance of the class. + /// + ~Win32InputHandler() => this.ReleaseUnmanagedResources(); + + private delegate LRESULT WndProcDelegate(HWND hWnd, uint uMsg, WPARAM wparam, LPARAM lparam); + + private delegate BOOL MonitorEnumProcDelegate(HMONITOR monitor, HDC hdc, RECT* rect, LPARAM lparam); + + /// + public bool UpdateCursor { get; set; } = true; + + /// + public string? IniPath + { + get + { + var ptr = (byte*)this.iniPathPtr; + if (ptr is null) + return string.Empty; + var len = 0; + while (ptr![len] != 0) + len++; + return Encoding.UTF8.GetString(ptr, len); + } + + set + { + if (this.iniPathPtr != 0) + Marshal.FreeHGlobal(this.iniPathPtr); + if (!string.IsNullOrEmpty(value)) + { + var e = Encoding.UTF8.GetByteCount(value); + var newAlloc = Marshal.AllocHGlobal(e + 2); + try + { + var span = new Span((void*)newAlloc, e + 2); + span[^1] = span[^2] = 0; + Encoding.UTF8.GetBytes(value, span); + } + catch + { + Marshal.FreeHGlobal(newAlloc); + throw; + } + + this.iniPathPtr = newAlloc; + } + + ImGui.GetIO().NativePtr->IniFilename = (byte*)this.iniPathPtr; + } + } + + /// + public void Dispose() + { + this.ReleaseUnmanagedResources(); + GC.SuppressFinalize(this); + } + + /// + public bool IsImGuiCursor(nint hCursor) => this.cursors.Contains((HCURSOR)hCursor); + + /// + public void NewFrame(int targetWidth, int targetHeight) + { + var io = ImGui.GetIO(); + + io.DisplaySize.X = targetWidth; + io.DisplaySize.Y = targetHeight; + io.DisplayFramebufferScale.X = 1f; + io.DisplayFramebufferScale.Y = 1f; + + var frequency = Stopwatch.Frequency; + var currentTime = Stopwatch.GetTimestamp(); + io.DeltaTime = this.lastTime > 0 ? (float)((double)(currentTime - this.lastTime) / frequency) : 1f / 60; + this.lastTime = currentTime; + + this.viewportHandler.UpdateMonitors(); + + this.UpdateMousePos(); + + this.ProcessKeyEventsWorkarounds(); + + // TODO: need to figure out some way to unify all this + // The bottom case works(?) if the caller hooks SetCursor, but otherwise causes fps issues + // The top case more or less works if we use ImGui's software cursor (and ideally hide the + // game's hardware cursor) + // It would be nice if hooking WM_SETCURSOR worked as it 'should' so that external hooking + // wasn't necessary + + // this is what imgui's example does, but it doesn't seem to work for us + // this could be a timing issue.. or their logic could just be wrong for many applications + // var cursor = io.MouseDrawCursor ? ImGuiMouseCursor.None : ImGui.GetMouseCursor(); + // if (_oldCursor != cursor) + // { + // _oldCursor = cursor; + // UpdateMouseCursor(); + // } + + // hacky attempt to make cursors work how I think they 'should' + if ((io.WantCaptureMouse || io.MouseDrawCursor) && this.UpdateCursor) + { + this.UpdateMouseCursor(); + } + + // Similar issue seen with overlapping mouse clicks + // eg, right click and hold on imgui window, drag off, left click and hold + // release right click, release left click -> right click was 'stuck' and imgui + // would become unresponsive + if (!io.WantCaptureMouse) + { + for (var i = 0; i < io.MouseDown.Count; i++) + { + io.MouseDown[i] = false; + } + } + } + + /// + /// Processes window messages. Supports both WndProcA and WndProcW. + /// + /// Handle of the window. + /// Type of window message. + /// wParam. + /// lParam. + /// Return value, if not doing further processing. + public LRESULT? ProcessWndProcW(HWND hWndCurrent, uint msg, WPARAM wParam, LPARAM lParam) + { + if (ImGui.GetCurrentContext() == nint.Zero) + return null; + + var io = ImGui.GetIO(); + + switch (msg) + { + case WM.WM_LBUTTONDOWN: + case WM.WM_LBUTTONDBLCLK: + case WM.WM_RBUTTONDOWN: + case WM.WM_RBUTTONDBLCLK: + case WM.WM_MBUTTONDOWN: + case WM.WM_MBUTTONDBLCLK: + case WM.WM_XBUTTONDOWN: + case WM.WM_XBUTTONDBLCLK: + { + var button = GetButton(msg, wParam); + if (io.WantCaptureMouse) + { + if (!ImGui.IsAnyMouseDown() && GetCapture() == nint.Zero) + SetCapture(hWndCurrent); + + io.MouseDown[button] = true; + this.imguiMouseIsDown[button] = true; + return default(LRESULT); + } + + break; + } + + case WM.WM_LBUTTONUP: + case WM.WM_RBUTTONUP: + case WM.WM_MBUTTONUP: + case WM.WM_XBUTTONUP: + { + var button = GetButton(msg, wParam); + if (io.WantCaptureMouse && this.imguiMouseIsDown[button]) + { + if (!ImGui.IsAnyMouseDown() && GetCapture() == hWndCurrent) + ReleaseCapture(); + + io.MouseDown[button] = false; + this.imguiMouseIsDown[button] = false; + return default(LRESULT); + } + + break; + } + + case WM.WM_MOUSEWHEEL: + if (io.WantCaptureMouse) + { + io.MouseWheel += GET_WHEEL_DELTA_WPARAM(wParam) / (float)WHEEL_DELTA; + return default(LRESULT); + } + + break; + case WM.WM_MOUSEHWHEEL: + if (io.WantCaptureMouse) + { + io.MouseWheelH += GET_WHEEL_DELTA_WPARAM(wParam) / (float)WHEEL_DELTA; + return default(LRESULT); + } + + break; + case WM.WM_KEYDOWN: + case WM.WM_SYSKEYDOWN: + case WM.WM_KEYUP: + case WM.WM_SYSKEYUP: + { + var isKeyDown = msg is WM.WM_KEYDOWN or WM.WM_SYSKEYDOWN; + if ((int)wParam >= 256) + break; + + // Submit modifiers + UpdateKeyModifiers(); + + // Obtain virtual key code + // (keypad enter doesn't have its own... VK_RETURN with KF_EXTENDED flag means keypad enter, see IM_VK_KEYPAD_ENTER definition for details, it is mapped to ImGuiKey.KeyPadEnter.) + var vk = (int)wParam; + if (vk == VK.VK_RETURN && ((int)lParam & (256 << 16)) > 0) + vk = VK.VK_RETURN + 256; + + // Submit key event + var key = VirtualKeyToImGuiKey(vk); + var scancode = ((int)lParam & 0xff0000) >> 16; + if (key != ImGuiKey.None && io.WantTextInput) + { + AddKeyEvent(key, isKeyDown, vk, scancode); + return nint.Zero; + } + + switch (vk) + { + // Submit individual left/right modifier events + case VK.VK_SHIFT: + // Important: Shift keys tend to get stuck when pressed together, missing key-up events are corrected in OnProcessKeyEventsWorkarounds() + if (IsVkDown(VK.VK_LSHIFT) == isKeyDown) + AddKeyEvent(ImGuiKey.LeftShift, isKeyDown, VK.VK_LSHIFT, scancode); + + if (IsVkDown(VK.VK_RSHIFT) == isKeyDown) + AddKeyEvent(ImGuiKey.RightShift, isKeyDown, VK.VK_RSHIFT, scancode); + + break; + + case VK.VK_CONTROL: + if (IsVkDown(VK.VK_LCONTROL) == isKeyDown) + AddKeyEvent(ImGuiKey.LeftCtrl, isKeyDown, VK.VK_LCONTROL, scancode); + + if (IsVkDown(VK.VK_RCONTROL) == isKeyDown) + AddKeyEvent(ImGuiKey.RightCtrl, isKeyDown, VK.VK_RCONTROL, scancode); + + break; + + case VK.VK_MENU: + if (IsVkDown(VK.VK_LMENU) == isKeyDown) + AddKeyEvent(ImGuiKey.LeftAlt, isKeyDown, VK.VK_LMENU, scancode); + + if (IsVkDown(VK.VK_RMENU) == isKeyDown) + AddKeyEvent(ImGuiKey.RightAlt, isKeyDown, VK.VK_RMENU, scancode); + + break; + } + + break; + } + + case WM.WM_CHAR: + if (io.WantTextInput) + { + io.AddInputCharacter((uint)wParam); + return nint.Zero; + } + + break; + + // this never seemed to work reasonably, but I'll leave it for now + case WM.WM_SETCURSOR: + if (io.WantCaptureMouse) + { + if (LOWORD(lParam) == HTCLIENT && this.UpdateMouseCursor()) + { + // this message returns 1 to block further processing + // because consistency is no fun + return 1; + } + } + + break; + + case WM.WM_DISPLAYCHANGE: + this.viewportHandler.UpdateMonitors(); + break; + } + + return null; + } + + private void UpdateMousePos() + { + var io = ImGui.GetIO(); + var pt = default(POINT); + + // Depending on if Viewports are enabled, we have to change how we process + // the cursor position. If viewports are enabled, we pass the absolute cursor + // position to ImGui. Otherwise, we use the old method of passing client-local + // mouse position to ImGui. + if (io.ConfigFlags.HasFlag(ImGuiConfigFlags.ViewportsEnable)) + { + if (io.WantSetMousePos) + { + SetCursorPos((int)io.MousePos.X, (int)io.MousePos.Y); + } + + if (GetCursorPos(&pt)) + { + io.MousePos.X = pt.x; + io.MousePos.Y = pt.y; + } + else + { + io.MousePos.X = float.MinValue; + io.MousePos.Y = float.MinValue; + } + } + else + { + if (io.WantSetMousePos) + { + pt.x = (int)io.MousePos.X; + pt.y = (int)io.MousePos.Y; + ClientToScreen(this.hWnd, &pt); + SetCursorPos(pt.x, pt.y); + } + + if (GetCursorPos(&pt) && ScreenToClient(this.hWnd, &pt)) + { + io.MousePos.X = pt.x; + io.MousePos.Y = pt.y; + } + else + { + io.MousePos.X = float.MinValue; + io.MousePos.Y = float.MinValue; + } + } + } + + private bool UpdateMouseCursor() + { + var io = ImGui.GetIO(); + if (io.ConfigFlags.HasFlag(ImGuiConfigFlags.NoMouseCursorChange)) + return false; + + var cur = ImGui.GetMouseCursor(); + if (cur == ImGuiMouseCursor.None || io.MouseDrawCursor) + SetCursor(default); + else + SetCursor(this.cursors[(int)cur]); + + return true; + } + + private void ProcessKeyEventsWorkarounds() + { + // Left & right Shift keys: when both are pressed together, Windows tend to not generate the WM_KEYUP event for the first released one. + if (ImGui.IsKeyDown(ImGuiKey.LeftShift) && !IsVkDown(VK.VK_LSHIFT)) + AddKeyEvent(ImGuiKey.LeftShift, false, VK.VK_LSHIFT); + if (ImGui.IsKeyDown(ImGuiKey.RightShift) && !IsVkDown(VK.VK_RSHIFT)) + AddKeyEvent(ImGuiKey.RightShift, false, VK.VK_RSHIFT); + + // Sometimes WM_KEYUP for Win key is not passed down to the app (e.g. for Win+V on some setups, according to GLFW). + if (ImGui.IsKeyDown(ImGuiKey.LeftSuper) && !IsVkDown(VK.VK_LWIN)) + AddKeyEvent(ImGuiKey.LeftSuper, false, VK.VK_LWIN); + if (ImGui.IsKeyDown(ImGuiKey.RightSuper) && !IsVkDown(VK.VK_RWIN)) + AddKeyEvent(ImGuiKey.RightSuper, false, VK.VK_RWIN); + + // From ImGui's FAQ: + // Note: Text input widget releases focus on "Return KeyDown", so the subsequent "Return KeyUp" event + // that your application receive will typically have io.WantCaptureKeyboard == false. Depending on your + // application logic it may or not be inconvenient. + // + // With how the local wndproc works, this causes the key up event to be missed when exiting ImGui text entry + // (eg, from hitting enter or escape. There may be other ways as well) + // This then causes the key to appear 'stuck' down, which breaks subsequent attempts to use the input field. + // This is something of a brute force fix that basically makes key up events irrelevant + // Holding a key will send repeated key down events and (re)set these where appropriate, so this should be ok. + var io = ImGui.GetIO(); + if (!io.WantTextInput) + { + // See: https://github.com/goatcorp/ImGuiScene/pull/13 + // > GetForegroundWindow from winuser.h is a surprisingly expensive function. + var isForeground = GetForegroundWindow() == this.hWnd; + for (var i = (int)ImGuiKey.NamedKey_BEGIN; i < (int)ImGuiKey.NamedKey_END; i++) + { + // Skip raising modifier keys if the game is focused. + // This allows us to raise the keys when one is held and the window becomes unfocused, + // but if we do not skip them, they will only be held down every 4th frame or so. + if (isForeground && (IsGamepadKey((ImGuiKey)i) || IsModKey((ImGuiKey)i))) + continue; + io.AddKeyEvent((ImGuiKey)i, false); + } + } + } + + /// + /// This WndProc is called for ImGuiScene windows. WndProc for main window will be called back from somewhere else. + /// + private LRESULT WndProcDetour(HWND hWndCurrent, uint msg, WPARAM wParam, LPARAM lParam) + { + // Attempt to process the result of this window message + // We will return the result here if we consider the message handled + var processResult = this.ProcessWndProcW(hWndCurrent, msg, wParam, lParam); + + if (processResult != null) return processResult.Value; + + // The message wasn't handled, but it's a platform window + // So we have to handle some messages ourselves + // BUT we might have disposed the context, so check that + if (ImGui.GetCurrentContext() == nint.Zero) + return DefWindowProcW(hWndCurrent, msg, wParam, lParam); + + var viewport = ImGui.FindViewportByPlatformHandle(hWndCurrent); + if (viewport.NativePtr == null) + return DefWindowProcW(hWndCurrent, msg, wParam, lParam); + + switch (msg) + { + case WM.WM_CLOSE: + viewport.PlatformRequestClose = true; + return 0; + case WM.WM_MOVE: + viewport.PlatformRequestMove = true; + return 0; + case WM.WM_SIZE: + viewport.PlatformRequestResize = true; + return 0; + case WM.WM_MOUSEACTIVATE: + // We never want our platform windows to be active, or else Windows will think we + // want messages dispatched with its hWnd. We don't. The only way to activate a platform + // window is via clicking, it does not appear on the taskbar or alt-tab, so we just + // brute force behavior here. + + // Make the game the foreground window. This prevents ImGui windows from becoming + // choppy when users have the "limit FPS" option enabled in-game + SetForegroundWindow(this.hWnd); + + // Also set the window capture to the main window, as focus will not cause + // future messages to be dispatched to the main window unless it is receiving capture + SetCapture(this.hWnd); + + // We still want to return MA_NOACTIVATE + // https://docs.microsoft.com/en-us/windows/win32/inputdev/wm-mouseactivate + return 0x3; + case WM.WM_NCHITTEST: + // Let mouse pass-through the window. This will allow the backend to set io.MouseHoveredViewport properly (which is OPTIONAL). + // The ImGuiViewportFlags_NoInputs flag is set while dragging a viewport, as want to detect the window behind the one we are dragging. + // If you cannot easily access those viewport flags from your windowing/event code: you may manually synchronize its state e.g. in + // your main loop after calling UpdatePlatformWindows(). Iterate all viewports/platform windows and pass the flag to your windowing system. + if (viewport.Flags.HasFlag(ImGuiViewportFlags.NoInputs)) + { + // https://docs.microsoft.com/en-us/windows/win32/inputdev/wm-nchittest + return -1; + } + + break; + } + + return DefWindowProcW(hWndCurrent, msg, wParam, lParam); + } + + private void ReleaseUnmanagedResources() + { + if (this.disposedValue) + return; + + this.viewportHandler.Dispose(); + + this.cursors.AsSpan().Clear(); + + if (ImGui.GetIO().NativePtr->BackendPlatformName == (void*)this.platformNamePtr) + ImGui.GetIO().NativePtr->BackendPlatformName = null; + if (this.platformNamePtr != nint.Zero) + Marshal.FreeHGlobal(this.platformNamePtr); + + if (this.iniPathPtr != nint.Zero) + { + ImGui.GetIO().NativePtr->IniFilename = null; + Marshal.FreeHGlobal(this.iniPathPtr); + this.iniPathPtr = nint.Zero; + } + + this.disposedValue = true; + } + + private struct ViewportHandler : IDisposable + { + [SuppressMessage("ReSharper", "CollectionNeverQueried.Local", Justification = "Keeping references alive")] + private readonly List 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 = GetModuleHandleW(null), + hbrBackground = (HBRUSH)(1 + COLOR.COLOR_BACKGROUND), + lpfnWndProc = (delegate* unmanaged)Marshal + .GetFunctionPointerForDelegate(this.input.wndProcDelegate), + lpszClassName = (ushort*)this.classNamePtr, + }; + + if (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) + { + UnregisterClassW((ushort*)this.classNamePtr, 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 = 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 = GetSystemMetrics(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 (!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; + }); + 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), + }; + AdjustWindowRectEx(&rect, (uint)data->DwStyle, false, (uint)data->DwExStyle); + + fixed (char* pwszWindowTitle = "Untitled") + { + data->Hwnd = 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, + 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 (GetCapture() == data->Hwnd) + { + // Transfer capture so if we started dragging from a window that later disappears, we'll still receive the MOUSEUP event. + ReleaseCapture(); + SetCapture(this.input.hWnd); + } + + if (data->Hwnd != nint.Zero && data->HwndOwned) + { + var result = DestroyWindow(data->Hwnd); + if (result == false && 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 + 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)) + ShowWindow(data->Hwnd, SW.SW_SHOWNA); + else + 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; + + _ = SetWindowLongW(data->Hwnd, GWL.GWL_STYLE, data->DwStyle); + _ = 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), + }; + AdjustWindowRectEx(&rect, (uint)data->DwStyle, false, (uint)data->DwExStyle); + 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 + 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 }; + 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); + AdjustWindowRectEx(&rect, (uint)data->DwStyle, false, (uint)data->DwExStyle); + 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; + 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); + AdjustWindowRectEx(&rect, (uint)data->DwStyle, false, (uint)data->DwExStyle); + 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; + + BringWindowToTop(data->Hwnd); + SetForegroundWindow(data->Hwnd); + SetFocus(data->Hwnd); + } + + private bool OnGetWindowFocus(ImGuiViewportPtr viewport) + { + var data = (ImGuiViewportDataWin32*)viewport.PlatformUserData; + return GetForegroundWindow() == data->Hwnd; + } + + private bool OnGetWindowMinimized(ImGuiViewportPtr viewport) + { + var data = (ImGuiViewportDataWin32*)viewport.PlatformUserData; + return IsIconic(data->Hwnd); + } + + private void OnSetWindowTitle(ImGuiViewportPtr viewport, string title) + { + var data = (ImGuiViewportDataWin32*)viewport.PlatformUserData; + fixed (char* pwszTitle = title) + SetWindowTextW(data->Hwnd, (ushort*)pwszTitle); + } + + private void OnSetWindowAlpha(ImGuiViewportPtr viewport, float alpha) + { + var data = (ImGuiViewportDataWin32*)viewport.PlatformUserData; + var style = GetWindowLongW(data->Hwnd, GWL.GWL_EXSTYLE); + + alpha = Math.Clamp(alpha, 0f, 1f); + if (alpha < 1.0f) + { + style |= WS.WS_EX_LAYERED; + _ = SetWindowLongW(data->Hwnd, GWL.GWL_EXSTYLE, style); + } + else + { + style &= ~WS.WS_EX_LAYERED; + _ = SetWindowLongW(data->Hwnd, GWL.GWL_EXSTYLE, style); + } + + _ = SetLayeredWindowAttributes(data->Hwnd, 0, (byte)(255 * alpha), LWA.LWA_ALPHA); + } + + // TODO: Decode why IME is miserable + // private void OnSetImeInputPos(ImGuiViewportPtr viewport, Vector2 pos) { + // COMPOSITIONFORM cs = new COMPOSITIONFORM( + // 0x20, + // new POINT( + // (int) (pos.X - viewport.Pos.X), + // (int) (pos.Y - viewport.Pos.Y)), + // new RECT(0, 0, 0, 0) + // ); + // var hwnd = viewport.PlatformHandle; + // if (hwnd != nint.Zero) { + // var himc = ImmGetContext(hwnd); + // if (himc != nint.Zero) { + // ImmSetCompositionWindow(himc, ref cs); + // 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/Interface/ImGuiBackend/Renderers/Dx11Renderer.ViewportHandler.cs b/Dalamud/Interface/ImGuiBackend/Renderers/Dx11Renderer.ViewportHandler.cs new file mode 100644 index 000000000..7fcbe432c --- /dev/null +++ b/Dalamud/Interface/ImGuiBackend/Renderers/Dx11Renderer.ViewportHandler.cs @@ -0,0 +1,374 @@ +using System.Diagnostics.CodeAnalysis; +using System.Numerics; +using System.Runtime.InteropServices; + +using Dalamud.Interface.ImGuiBackend.Helpers; +using Dalamud.Utility; + +using ImGuiNET; + +using TerraFX.Interop.DirectX; +using TerraFX.Interop.Windows; + +using static TerraFX.Interop.Windows.Windows; + +namespace Dalamud.Interface.ImGuiBackend.Renderers; + +/// +/// 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.Attach(viewport.RendererUserData).Dispose(); + viewport.RendererUserData = nint.Zero; + } + + [UnmanagedCallersOnly] + private static void OnSetWindowSize(ImGuiViewportPtr viewport, Vector2 size) => + ViewportData.Attach(viewport.RendererUserData).ResizeBuffers((int)size.X, (int)size.Y, true); + + [UnmanagedCallersOnly] + private static void OnRenderWindow(ImGuiViewportPtr viewport, nint v) => + ViewportData.Attach(viewport.RendererUserData).Draw(viewport.DrawData, true); + + [UnmanagedCallersOnly] + private static void OnSwapBuffers(ImGuiViewportPtr viewport, nint v) => + ViewportData.Attach(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).Handle; + } + catch + { + viewport.RendererUserData = ViewportData.Create(this.renderer, (HWND)hWnd).Handle; + } + } + } + + private sealed class ViewportData : IDisposable + { + private readonly Dx11Renderer parent; + + private GCHandle selfGcHandle; + 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); + this.selfGcHandle = GCHandle.Alloc(this); + } + + public IDXGISwapChain* SwapChain => this.swapChain; + + public nint Handle => GCHandle.ToIntPtr(this.selfGcHandle); + + private DXGI_FORMAT RtvFormat => this.parent.rtvFormat; + + public static ViewportData Attach(nint handle) => + (ViewportData)GCHandle.FromIntPtr(handle).Target ?? throw new InvalidOperationException(); + + 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 Dispose() + { + if (!this.selfGcHandle.IsAllocated) + return; + + this.ResetBuffers(); + this.dcompVisual.Reset(); + this.dcompTarget.Reset(); + this.swapChain.Reset(); + this.selfGcHandle.Free(); + } + + 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(); + } + } + + 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/Interface/ImGuiBackend/Renderers/Dx11Renderer.cs b/Dalamud/Interface/ImGuiBackend/Renderers/Dx11Renderer.cs new file mode 100644 index 000000000..685389058 --- /dev/null +++ b/Dalamud/Interface/ImGuiBackend/Renderers/Dx11Renderer.cs @@ -0,0 +1,664 @@ +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 Dalamud.Interface.ImGuiBackend.Helpers; +using Dalamud.Interface.ImGuiBackend.Helpers.D3D11; +using Dalamud.Interface.Textures; +using Dalamud.Interface.Textures.TextureWraps; +using Dalamud.Interface.Textures.TextureWraps.Internal; +using Dalamud.Interface.Utility; +using Dalamud.Utility; + +using ImGuiNET; + +using TerraFX.Interop.DirectX; +using TerraFX.Interop.Windows; + +namespace Dalamud.Interface.ImGuiBackend.Renderers; + +/// +/// 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 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 bool releaseUnmanagedResourceCalled; + + private ComPtr device; + private ComPtr context; + private ComPtr vertexShader; + private ComPtr pixelShader; + private ComPtr sampler; + 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; + + /// + /// 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.Handle; + } + catch + { + this.ReleaseUnmanagedResources(); + throw; + } + } + + /// + /// Finalizes an instance of the class. + /// + ~Dx11Renderer() => this.ReleaseUnmanagedResources(); + + /// + public void 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); + + /// + /// 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 texDesc = 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(&texDesc, 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(&texDesc, &subrdata, texture.GetAddressOf())); + } + } + + texture.Get()->SetDebugName($"Texture:{debugName}:SRV"); + + using var srvTemp = default(ComPtr); + var srvDesc = new D3D11_SHADER_RESOURCE_VIEW_DESC( + texture, + D3D_SRV_DIMENSION.D3D11_SRV_DIMENSION_TEXTURE2D); + this.device.Get()->CreateShaderResourceView((ID3D11Resource*)texture.Get(), &srvDesc, srvTemp.GetAddressOf()) + .ThrowOnError(); + srvTemp.Get()->SetDebugName($"Texture:{debugName}:SRV"); + + return new UnknownTextureWrap((IUnknown*)srvTemp.Get(), specs.Width, specs.Height, true); + } + + 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(&cmdList.NativePtr->VtxBuffer); + var indices = new ImVectorWrapper(&cmdList.NativePtr->IdxBuffer); + + vertices.DataSpan.CopyTo(targetVertices); + indices.DataSpan.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 + var srv = (ID3D11ShaderResourceView*)cmd.TextureId; + this.context.Get()->PSSetShader(this.pixelShader, null, 0); + this.context.Get()->PSSetSamplers(0, 1, this.sampler.GetAddressOf()); + this.context.Get()->PSSetShaderResources(0, 1, &srv); + this.context.Get()->DrawIndexed( + cmd.ElemCount, + (uint)(cmd.IdxOffset + indexOffset), + (int)(cmd.VtxOffset + vertexOffset)); + } + } + + 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, (int)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 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()) + this.device.Get()->CreatePixelShader(pArray, (nuint)stream.Length, null, ppShader).ThrowOnError(); + ArrayPool.Shared.Return(array); + } + + // Create the sampler state + if (this.sampler.IsEmpty()) + { + var samplerDesc = new D3D11_SAMPLER_DESC + { + 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, + }; + + fixed (ID3D11SamplerState** ppSampler = &this.sampler.GetPinnableReference()) + this.device.Get()->CreateSamplerState(&samplerDesc, ppSampler).ThrowOnError(); + } + + // 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 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.Dispose(); + 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.vertexShader.Reset(); + this.pixelShader.Reset(); + this.sampler.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/Interface/ImGuiBackend/Renderers/IImGuiRenderer.cs b/Dalamud/Interface/ImGuiBackend/Renderers/IImGuiRenderer.cs new file mode 100644 index 000000000..dc9229dc6 --- /dev/null +++ b/Dalamud/Interface/ImGuiBackend/Renderers/IImGuiRenderer.cs @@ -0,0 +1,43 @@ +using System.Runtime.CompilerServices; + +using Dalamud.Interface.Textures; +using Dalamud.Interface.Textures.TextureWraps; + +using ImGuiNET; + +namespace Dalamud.Interface.ImGuiBackend.Renderers; + +/// A simple shared public interface that all ImGui render implementations follow. +internal interface IImGuiRenderer : IDisposable +{ + /// 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 = ""); + + /// 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); +} diff --git a/Dalamud/Interface/ImGuiBackend/Renderers/imgui-frag.hlsl.bytes b/Dalamud/Interface/ImGuiBackend/Renderers/imgui-frag.hlsl.bytes new file mode 100644 index 000000000..2a9fbf5a3 Binary files /dev/null and b/Dalamud/Interface/ImGuiBackend/Renderers/imgui-frag.hlsl.bytes differ diff --git a/Dalamud/Interface/ImGuiBackend/Renderers/imgui-vertex.hlsl.bytes b/Dalamud/Interface/ImGuiBackend/Renderers/imgui-vertex.hlsl.bytes new file mode 100644 index 000000000..572a04538 Binary files /dev/null and b/Dalamud/Interface/ImGuiBackend/Renderers/imgui-vertex.hlsl.bytes differ diff --git a/Dalamud/Interface/Internal/InterfaceManager.AsHook.cs b/Dalamud/Interface/Internal/InterfaceManager.AsHook.cs index 3ad25f97d..05af78949 100644 --- a/Dalamud/Interface/Internal/InterfaceManager.AsHook.cs +++ b/Dalamud/Interface/Internal/InterfaceManager.AsHook.cs @@ -119,13 +119,13 @@ internal unsafe partial class InterfaceManager this.ResizeBuffers?.InvokeSafely(); - this.scene?.OnPreResize(); + this.backend?.OnPreResize(); var ret = this.dxgiSwapChainResizeBuffersHook!.Original(swapChain, bufferCount, width, height, newFormat, swapChainFlags); if (ret == DXGI.DXGI_ERROR_INVALID_CALL) Log.Error("invalid call to resizeBuffers"); - this.scene?.OnPostResize((int)width, (int)height); + this.backend?.OnPostResize((int)width, (int)height); return ret; } diff --git a/Dalamud/Interface/Internal/InterfaceManager.AsReShadeAddon.cs b/Dalamud/Interface/Internal/InterfaceManager.AsReShadeAddon.cs index 73c0a4d15..cd4b0a418 100644 --- a/Dalamud/Interface/Internal/InterfaceManager.AsReShadeAddon.cs +++ b/Dalamud/Interface/Internal/InterfaceManager.AsReShadeAddon.cs @@ -14,23 +14,23 @@ internal unsafe partial class InterfaceManager private void ReShadeAddonInterfaceOnDestroySwapChain(ref ReShadeAddonInterface.ApiObject swapChain) { var swapChainNative = swapChain.GetNative(); - if (this.scene?.SwapChain.NativePointer != (nint)swapChainNative) + if (this.backend?.IsAttachedToPresentationTarget((nint)swapChainNative) is not true) return; - this.scene?.OnPreResize(); + this.backend?.OnPreResize(); } private void ReShadeAddonInterfaceOnInitSwapChain(ref ReShadeAddonInterface.ApiObject swapChain) { var swapChainNative = swapChain.GetNative(); - if (this.scene?.SwapChain.NativePointer != (nint)swapChainNative) + if (this.backend?.IsAttachedToPresentationTarget((nint)swapChainNative) is not true) return; DXGI_SWAP_CHAIN_DESC desc; if (swapChainNative->GetDesc(&desc).FAILED) return; - this.scene?.OnPostResize((int)desc.BufferDesc.Width, (int)desc.BufferDesc.Height); + this.backend?.OnPostResize((int)desc.BufferDesc.Width, (int)desc.BufferDesc.Height); } private void ReShadeAddonInterfaceOnPresent( @@ -42,16 +42,16 @@ internal unsafe partial class InterfaceManager { var swapChainNative = swapChain.GetNative(); - if (this.RenderDalamudCheckAndInitialize(swapChainNative, 0) is { } activeScene) - this.RenderDalamudDraw(activeScene); + if (this.RenderDalamudCheckAndInitialize(swapChainNative, 0) is { } activebackend) + this.RenderDalamudDraw(activebackend); } private void ReShadeAddonInterfaceOnReShadeOverlay(ref ReShadeAddonInterface.ApiObject runtime) { var swapChainNative = runtime.GetNative(); - if (this.RenderDalamudCheckAndInitialize(swapChainNative, 0) is { } activeScene) - this.RenderDalamudDraw(activeScene); + if (this.RenderDalamudCheckAndInitialize(swapChainNative, 0) is { } activebackend) + this.RenderDalamudDraw(activebackend); } private int AsReShadeAddonDxgiSwapChainResizeBuffersDetour( diff --git a/Dalamud/Interface/Internal/InterfaceManager.cs b/Dalamud/Interface/Internal/InterfaceManager.cs index ed27a1043..5aeaf925f 100644 --- a/Dalamud/Interface/Internal/InterfaceManager.cs +++ b/Dalamud/Interface/Internal/InterfaceManager.cs @@ -17,6 +17,7 @@ using Dalamud.Game.ClientState.Keys; using Dalamud.Hooking; using Dalamud.Hooking.Internal; using Dalamud.Hooking.WndProcHook; +using Dalamud.Interface.ImGuiBackend; using Dalamud.Interface.ImGuiNotification; using Dalamud.Interface.ImGuiNotification.Internal; using Dalamud.Interface.Internal.Asserts; @@ -38,15 +39,13 @@ using FFXIVClientStructs.FFXIV.Client.Graphics.Environment; using ImGuiNET; -using ImGuiScene; - using JetBrains.Annotations; -using PInvoke; - using TerraFX.Interop.DirectX; using TerraFX.Interop.Windows; +using static TerraFX.Interop.Windows.Windows; + // general dev notes, here because it's easiest /* @@ -102,7 +101,7 @@ internal partial class InterfaceManager : IInternalDisposableService private readonly AssertHandler assertHandler = new(); - private RawDX11Scene? scene; + private IWin32Backend? backend; private Hook? setCursorHook; private Hook? reShadeDxgiSwapChainPresentHook; @@ -115,9 +114,9 @@ internal partial class InterfaceManager : IInternalDisposableService private ILockedImFont? defaultFontResourceLock; // can't access imgui IO before first present call - private bool lastWantCapture = false; + private HWND gameWindowHandle; + private bool lastWantCapture; private bool isOverrideGameCursor = true; - private IntPtr gameWindowHandle; [ServiceManager.ServiceConstructor] private InterfaceManager() @@ -126,12 +125,12 @@ internal partial class InterfaceManager : IInternalDisposableService } [UnmanagedFunctionPointer(CallingConvention.StdCall)] - private delegate IntPtr SetCursorDelegate(IntPtr hCursor); + private delegate nint SetCursorDelegate(nint hCursor); /// /// This event gets called each frame to facilitate ImGui drawing. /// - public event RawDX11Scene.BuildUIDelegate? Draw; + public event IImGuiBackend.BuildUiDelegate? Draw; /// /// This event gets called when ResizeBuffers is called. @@ -199,36 +198,26 @@ internal partial class InterfaceManager : IInternalDisposableService /// /// Gets the DX11 scene. /// - public RawDX11Scene? Scene => this.scene; - - /// - /// Gets the D3D11 device instance. - /// - public SharpDX.Direct3D11.Device? Device => this.scene?.Device; - - /// - /// Gets the address handle to the main process window. - /// - public IntPtr WindowHandlePtr => this.scene?.WindowHandlePtr ?? IntPtr.Zero; + public IImGuiBackend? Backend => this.backend; /// /// Gets or sets a value indicating whether or not the game's cursor should be overridden with the ImGui cursor. /// public bool OverrideGameCursor { - get => this.scene?.UpdateCursor ?? this.isOverrideGameCursor; + get => this.backend?.UpdateCursor ?? this.isOverrideGameCursor; set { this.isOverrideGameCursor = value; - if (this.scene != null) - this.scene.UpdateCursor = value; + if (this.backend != null) + this.backend.UpdateCursor = value; } } /// /// Gets a value indicating whether the Dalamud interface ready to use. /// - public bool IsReady => this.scene != null; + public bool IsReady => this.backend != null; /// /// Gets or sets a value indicating whether or not Draw events should be dispatched. @@ -242,20 +231,24 @@ internal partial class InterfaceManager : IInternalDisposableService /// /// 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; + } } } } @@ -324,7 +317,7 @@ internal partial class InterfaceManager : IInternalDisposableService this.IconFontHandle = null; Interlocked.Exchange(ref this.dalamudAtlas, null)?.Dispose(); - Interlocked.Exchange(ref this.scene, null)?.Dispose(); + Interlocked.Exchange(ref this.backend, null)?.Dispose(); return; @@ -459,27 +452,27 @@ internal partial class InterfaceManager : IInternalDisposableService /// 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.backend?.DeviceHandle is 0 or null) 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, SharpDX.DXGI.MemorySegmentGroup.Local); - return (memInfo.CurrentUsage, memInfo.CurrentReservation); - } - catch - { - // ignored - } + if (new ComPtr((IUnknown*)this.backend.DeviceHandle).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); } /// @@ -488,23 +481,23 @@ internal partial class InterfaceManager : IInternalDisposableService /// 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; - ((HRESULT)NativeFunctions.DwmSetWindowAttribute( - this.GameWindowHandle, - NativeFunctions.DWMWINDOWATTRIBUTE.DWMWA_USE_IMMERSIVE_DARK_MODE, - ref value, - sizeof(int))).ThrowOnError(); + var value = enabled ? 1u : 0u; + DwmSetWindowAttribute( + this.GameWindowHandle, + (uint)DWMWINDOWATTRIBUTE.DWMWA_USE_IMMERSIVE_DARK_MODE, + &value, + sizeof(int)).ThrowOnError(); } private static InterfaceManager WhenFontsReady() @@ -535,9 +528,9 @@ internal partial class InterfaceManager : IInternalDisposableService /// and initializes ImGui for drawing. /// The swap chain to test and initialize ImGui with if conditions are met. /// Flags passed to . - /// An initialized instance of , or null if + /// An initialized instance of , or null if /// is not the main swap chain. - private unsafe RawDX11Scene? RenderDalamudCheckAndInitialize(IDXGISwapChain* swapChain, uint flags) + private unsafe IImGuiBackend? RenderDalamudCheckAndInitialize(IDXGISwapChain* swapChain, uint flags) { // Quoting ReShade dxgi_swapchain.cpp DXGISwapChain::on_present: // > Some D3D11 games test presentation for timing and composition purposes @@ -550,7 +543,7 @@ internal partial class InterfaceManager : IInternalDisposableService Debug.Assert(this.dalamudAtlas is not null, "dalamudAtlas should have been set already"); - var activeScene = this.scene ?? this.InitScene(swapChain); + var activeBackend = this.backend ?? this.InitBackend(swapChain); if (!this.dalamudAtlas!.HasBuiltAtlas) { @@ -564,12 +557,12 @@ internal partial class InterfaceManager : IInternalDisposableService return null; } - return activeScene; + return activeBackend; } /// Draws Dalamud to the given scene representing the ImGui context. - /// The scene to draw to. - private void RenderDalamudDraw(RawDX11Scene activeScene) + /// The scene to draw to. + private void RenderDalamudDraw(IImGuiBackend activeBackend) { this.CumulativePresentCalls++; this.IsMainThreadInPresent = true; @@ -582,7 +575,7 @@ internal partial class InterfaceManager : IInternalDisposableService // Enable viewports if there are no issues. var viewportsEnable = this.dalamudConfiguration.IsDisableViewport || - activeScene.SwapChain.IsFullScreen || + activeBackend.IsMainViewportFullScreen() || ImGui.GetPlatformIO().Monitors.Size == 1; if (viewportsEnable) ImGui.GetIO().ConfigFlags &= ~ImGuiConfigFlags.ViewportsEnable; @@ -590,41 +583,48 @@ internal partial class InterfaceManager : IInternalDisposableService ImGui.GetIO().ConfigFlags |= ImGuiConfigFlags.ViewportsEnable; // Call drawing functions, which in turn will call Draw event. - activeScene.Render(); + activeBackend.Render(); this.PostImGuiRender(); this.IsMainThreadInPresent = false; } - private unsafe RawDX11Scene InitScene(IDXGISwapChain* swapChain) + private unsafe IImGuiBackend InitBackend(IDXGISwapChain* swapChain) { - RawDX11Scene newScene; + IWin32Backend newBackend; using (Timings.Start("IM Scene Init")) { try { + newBackend = new Dx11Win32Backend(swapChain); this.assertHandler.Setup(); - newScene = new RawDX11Scene((nint)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); - - if (res == User32.MessageBoxResult.IDYES) + fixed (void* lpText = + "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?") { - var psi = new ProcessStartInfo + fixed (void* lpCaption = "Dalamud Error") { - FileName = "https://aka.ms/vs/16/release/vc_redist.x64.exe", - UseShellExecute = true, - }; - Process.Start(psi); + var res = MessageBoxW( + default, + (ushort*)lpText, + (ushort*)lpCaption, + MB.MB_YESNO | MB.MB_TOPMOST | MB.MB_ICONERROR); + + if (res == IDYES) + { + var psi = new ProcessStartInfo + { + FileName = "https://aka.ms/vs/16/release/vc_redist.x64.exe", + UseShellExecute = true, + }; + Process.Start(psi); + } + } } Environment.Exit(-1); @@ -636,7 +636,8 @@ internal partial class InterfaceManager : IInternalDisposableService var startInfo = Service.Get().StartInfo; var configuration = Service.Get(); - 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 { @@ -659,16 +660,18 @@ internal partial class InterfaceManager : IInternalDisposableService Log.Error(ex, "Could not delete dalamudUI.ini"); } - newScene.UpdateCursor = this.isOverrideGameCursor; - newScene.ImGuiIniPath = iniFileInfo.FullName; - newScene.OnBuildUI += this.Display; - newScene.OnNewInputFrame += this.OnNewInputFrame; + newBackend.UpdateCursor = this.isOverrideGameCursor; + newBackend.IniPath = iniFileInfo.FullName; + newBackend.BuildUi += this.Display; + newBackend.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) @@ -724,16 +727,16 @@ internal partial class InterfaceManager : IInternalDisposableService Log.Information("[IM] Scene & ImGui setup OK!"); } - this.scene = newScene; + this.backend = newBackend; Service.Provide(new(this)); this.wndProcHookManager.PreWndProc += this.WndProcHookManagerOnPreWndProc; - return newScene; + return newBackend; } - 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.backend?.ProcessWndProcW(args.Hwnd, args.Message, args.WParam, args.LParam); if (r is not null) args.SuppressWithValue(r.Value); } @@ -790,7 +793,7 @@ internal partial class InterfaceManager : IInternalDisposableService new() { SizePx = Service.Get().DefaultFontSpec.SizePx, - GlyphRanges = new ushort[] { 0x20, 0x20, 0x00 }, + GlyphRanges = [0x20, 0x20, 0x00], }))); this.MonoFontHandle = (FontHandle)this.dalamudAtlas.NewDelegateFontHandle( e => e.OnPreBuild( @@ -1055,13 +1058,13 @@ internal partial class InterfaceManager : IInternalDisposableService this.dxgiSwapChainHook?.Enable(); } - private IntPtr SetCursorDetour(IntPtr hCursor) + private nint SetCursorDetour(nint hCursor) { - if (this.lastWantCapture && (!this.scene?.IsImGuiCursor(hCursor) ?? false) && this.OverrideGameCursor) - return IntPtr.Zero; + if (this.lastWantCapture && (!this.backend?.IsImGuiCursor(hCursor) ?? false) && this.OverrideGameCursor) + return default; return this.setCursorHook?.IsDisposed is not false - ? User32.SetCursor(new(hCursor, false)).DangerousGetHandle() + ? SetCursor((HCURSOR)hCursor) : this.setCursorHook.Original(hCursor); } diff --git a/Dalamud/Interface/ManagedFontAtlas/Internals/FontAtlasFactory.Implementation.cs b/Dalamud/Interface/ManagedFontAtlas/Internals/FontAtlasFactory.Implementation.cs index 61ac00faf..853556239 100644 --- a/Dalamud/Interface/ManagedFontAtlas/Internals/FontAtlasFactory.Implementation.cs +++ b/Dalamud/Interface/ManagedFontAtlas/Internals/FontAtlasFactory.Implementation.cs @@ -9,7 +9,6 @@ using System.Threading; using System.Threading.Tasks; using Dalamud.Interface.GameFonts; -using Dalamud.Interface.Internal; using Dalamud.Interface.Textures.TextureWraps; using Dalamud.Interface.Utility; using Dalamud.Logging.Internal; @@ -310,7 +309,7 @@ internal sealed partial class FontAtlasFactory throw; } - this.factory.SceneTask.ContinueWith( + this.factory.BackendTask.ContinueWith( r => { lock (this.syncRoot) @@ -318,8 +317,8 @@ internal sealed partial class FontAtlasFactory 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) @@ -735,7 +734,7 @@ internal sealed partial class FontAtlasFactory foreach (var font in toolkit.Fonts) toolkit.BuildLookupTable(font); - if (this.factory.SceneTask is { IsCompleted: false } sceneTask) + if (this.factory.BackendTask is { IsCompleted: false } backendTask) { Log.Verbose( "[{name}:{functionname}] 0x{ptr:X}: await SceneTask (at {sw}ms)", @@ -743,7 +742,7 @@ internal sealed partial class FontAtlasFactory nameof(this.RebuildFontsPrivateReal), atlasPtr, sw.ElapsedMilliseconds); - await sceneTask.ConfigureAwait(!isAsync); + await backendTask.ConfigureAwait(!isAsync); } #if VeryVerboseLog diff --git a/Dalamud/Interface/ManagedFontAtlas/Internals/FontAtlasFactory.cs b/Dalamud/Interface/ManagedFontAtlas/Internals/FontAtlasFactory.cs index c084d88e2..84d59b234 100644 --- a/Dalamud/Interface/ManagedFontAtlas/Internals/FontAtlasFactory.cs +++ b/Dalamud/Interface/ManagedFontAtlas/Internals/FontAtlasFactory.cs @@ -10,6 +10,7 @@ using Dalamud.Data; using Dalamud.Game; using Dalamud.Interface.FontIdentifier; using Dalamud.Interface.GameFonts; +using Dalamud.Interface.ImGuiBackend; using Dalamud.Interface.Internal; using Dalamud.Interface.Textures.Internal; using Dalamud.Interface.Textures.TextureWraps; @@ -19,8 +20,6 @@ using Dalamud.Utility; using ImGuiNET; -using ImGuiScene; - using Lumina.Data.Files; using TerraFX.Interop.DirectX; @@ -52,9 +51,9 @@ internal sealed partial class FontAtlasFactory this.Framework = framework; this.InterfaceManager = interfaceManager; this.dalamudAssetManager = dalamudAssetManager; - this.SceneTask = Service + this.BackendTask = Service .GetAsync() - .ContinueWith(r => r.Result.Manager.Scene); + .ContinueWith(r => r.Result.Manager.Backend); var gffasInfo = Enum.GetValues() .Select( @@ -141,7 +140,7 @@ internal sealed partial class FontAtlasFactory /// /// Gets the service instance of .
- /// may not yet be available. + /// may not yet be available. ///
public InterfaceManager InterfaceManager { get; } @@ -151,9 +150,9 @@ internal sealed partial class FontAtlasFactory public TextureManager TextureManager => Service.Get(); /// - /// Gets the async task for inside . + /// Gets the async task for inside . /// - public Task SceneTask { get; } + public Task BackendTask { get; } /// /// Gets the default glyph ranges (glyph ranges of ). diff --git a/Dalamud/Interface/Textures/Internal/TextureManager.Drawer.cs b/Dalamud/Interface/Textures/Internal/TextureManager.Drawer.cs index 7fb79311a..d1b017197 100644 --- a/Dalamud/Interface/Textures/Internal/TextureManager.Drawer.cs +++ b/Dalamud/Interface/Textures/Internal/TextureManager.Drawer.cs @@ -60,7 +60,7 @@ internal sealed partial class TextureManager /// The device. public void Setup(ID3D11Device* device) { - var assembly = typeof(ImGuiScene.ImGui_Impl_DX11).Assembly; + var assembly = typeof(SimpleDrawerImpl).Assembly; // Create the vertex shader if (this.vertexShader.IsEmpty() || this.inputLayout.IsEmpty()) diff --git a/Dalamud/Interface/Textures/Internal/TextureManager.cs b/Dalamud/Interface/Textures/Internal/TextureManager.cs index c9ee5d20e..e608956c2 100644 --- a/Dalamud/Interface/Textures/Internal/TextureManager.cs +++ b/Dalamud/Interface/Textures/Internal/TextureManager.cs @@ -58,7 +58,7 @@ internal sealed partial class TextureManager private unsafe TextureManager(InterfaceManager.InterfaceManagerWithScene withScene) { using var failsafe = new DisposeSafety.ScopedFinalizer(); - failsafe.Add(this.device = new((ID3D11Device*)withScene.Manager.Device!.NativePointer)); + failsafe.Add(this.device = new((ID3D11Device*)withScene.Manager.Backend!.DeviceHandle)); failsafe.Add(this.dynamicPriorityTextureLoader = new(Math.Max(1, Environment.ProcessorCount - 1))); failsafe.Add(this.sharedTextureManager = new(this)); failsafe.Add(this.wicManager = new(this)); diff --git a/Dalamud/Interface/UiBuilder.cs b/Dalamud/Interface/UiBuilder.cs index 2fc8c83b1..801253003 100644 --- a/Dalamud/Interface/UiBuilder.cs +++ b/Dalamud/Interface/UiBuilder.cs @@ -18,8 +18,6 @@ using ImGuiNET; using Serilog; -using SharpDX.Direct3D11; - namespace Dalamud.Interface; /// @@ -119,12 +117,22 @@ public interface IUiBuilder /// /// Gets the game's active Direct3D device. /// - Device Device { get; } + // TODO: Remove it on API11/APIXI, and remove SharpDX/PInvoke/etc. dependency from Dalamud. + [Obsolete($"Use {nameof(DeviceHandle)} and wrap it using DirectX wrapper library of your choice.")] + SharpDX.Direct3D11.Device Device { get; } - /// - /// Gets the game's main window handle. - /// - IntPtr WindowHandlePtr { get; } + /// Gets the game's active Direct3D device. + /// Pointer to the instance of IUnknown that the game is using and should be containing an ID3D11Device, + /// or 0 if it is not available yet. + /// Use + /// + /// QueryInterface with IID of IID_ID3D11Device if you want to ensure that the interface type contained + /// within is indeed an instance of ID3D11Device. + nint DeviceHandle { get; } + + /// Gets the game's main window handle. + /// HWND of the main game window, or 0 if it is not available yet. + nint WindowHandlePtr { get; } /// /// Gets or sets a value indicating whether this plugin should hide its UI automatically when the game's UI is hidden. @@ -258,6 +266,8 @@ public sealed class UiBuilder : IDisposable, IUiBuilder private IFontHandle? monoFontHandle; private IFontHandle? iconFontFixedWidthHandle; + private SharpDX.Direct3D11.Device? sdxDevice; + /// /// Initializes a new instance of the class and registers it. /// You do not have to call this manually. @@ -414,15 +424,17 @@ public sealed class UiBuilder : IDisposable, IUiBuilder this.InterfaceManagerWithScene?.MonoFontHandle ?? throw new InvalidOperationException("Scene is not yet ready."))); - /// - /// Gets the game's active Direct3D device. - /// - public Device Device => this.InterfaceManagerWithScene!.Device!; + /// + // TODO: Remove it on API11/APIXI, and remove SharpDX/PInvoke/etc. dependency from Dalamud. + [Obsolete($"Use {nameof(DeviceHandle)} and wrap it using DirectX wrapper library of your choice.")] + public SharpDX.Direct3D11.Device Device => + this.sdxDevice ??= new(this.InterfaceManagerWithScene!.Backend!.DeviceHandle); - /// - /// Gets the game's main window handle. - /// - public IntPtr WindowHandlePtr => this.InterfaceManagerWithScene!.WindowHandlePtr; + /// + public nint DeviceHandle => this.InterfaceManagerWithScene?.Backend?.DeviceHandle ?? 0; + + /// + public nint WindowHandlePtr => this.InterfaceManagerWithScene is { } imws ? imws.GameWindowHandle : 0; /// /// Gets or sets a value indicating whether this plugin should hide its UI automatically when the game's UI is hidden. diff --git a/Dalamud/Interface/Utility/ImGuiHelpers.cs b/Dalamud/Interface/Utility/ImGuiHelpers.cs index d9056fec4..37237438e 100644 --- a/Dalamud/Interface/Utility/ImGuiHelpers.cs +++ b/Dalamud/Interface/Utility/ImGuiHelpers.cs @@ -10,6 +10,7 @@ using System.Text.Unicode; using Dalamud.Configuration.Internal; using Dalamud.Game.ClientState.Keys; using Dalamud.Game.Text.SeStringHandling.Payloads; +using Dalamud.Interface.ImGuiBackend.InputHandler; using Dalamud.Interface.ImGuiSeStringRenderer; using Dalamud.Interface.ImGuiSeStringRenderer.Internal; using Dalamud.Interface.ManagedFontAtlas; @@ -17,14 +18,15 @@ using Dalamud.Interface.ManagedFontAtlas.Internals; using Dalamud.Interface.Utility.Raii; using ImGuiNET; -using ImGuiScene; + +using VirtualKey = Dalamud.Game.ClientState.Keys.VirtualKey; 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. @@ -111,7 +113,7 @@ public static class ImGuiHelpers /// /// The size of the indent. public static void ScaledIndent(float size) => ImGui.Indent(size * GlobalScale); - + /// /// Use a relative ImGui.SameLine() from your current cursor position, scaled by the Dalamud global scale. /// @@ -286,7 +288,7 @@ public static class ImGuiHelpers foreach (ref var kp in new Span((void*)font->KerningPairs.Data, font->KerningPairs.Size)) kp.AdvanceXAdjustment = rounder(kp.AdvanceXAdjustment * scale); - + foreach (ref var fkp in new Span((void*)font->FrequentKerningPairs.Data, font->FrequentKerningPairs.Size)) fkp = rounder(fkp * scale); } @@ -425,7 +427,7 @@ public static class ImGuiHelpers /// 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); } /// @@ -435,7 +437,7 @@ public static class ImGuiHelpers /// 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); } /// @@ -535,7 +537,7 @@ public static class ImGuiHelpers builder.BuildRanges(out var vec); return new ReadOnlySpan((void*)vec.Data, vec.Size).ToArray(); } - + /// public static ushort[] CreateImGuiRangesFrom(params UnicodeRange[] ranges) => CreateImGuiRangesFrom((IEnumerable)ranges); @@ -618,7 +620,7 @@ public static class ImGuiHelpers ImGuiNative.ImGuiInputTextCallbackData_InsertChars(data, 0, pBuf, pBuf + len); ImGuiNative.ImGuiInputTextCallbackData_SelectAll(data); } - + /// /// Finds the corresponding ImGui viewport ID for the given window handle. /// @@ -639,6 +641,12 @@ public static class ImGuiHelpers 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/Utility/TexFileExtensions.cs b/Dalamud/Utility/TexFileExtensions.cs index ec8e10b3c..9d6f152c5 100644 --- a/Dalamud/Utility/TexFileExtensions.cs +++ b/Dalamud/Utility/TexFileExtensions.cs @@ -1,10 +1,12 @@ using System.Runtime.CompilerServices; +using Dalamud.Interface.ImGuiBackend.Renderers; 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/lib/ImGui.NET b/lib/ImGui.NET new file mode 160000 index 000000000..98304cfd0 --- /dev/null +++ b/lib/ImGui.NET @@ -0,0 +1 @@ +Subproject commit 98304cfd0bf86cf176732d60eb5dba6fc351f737 diff --git a/lib/ImGuiScene b/lib/ImGuiScene deleted file mode 160000 index d336b20a8..000000000 --- a/lib/ImGuiScene +++ /dev/null @@ -1 +0,0 @@ -Subproject commit d336b20a85ea48723a98681b18bdfe14a56a3403