From faf61477c717afbd72012945cdb124cd1d266d57 Mon Sep 17 00:00:00 2001 From: Soreepeong Date: Fri, 12 Jul 2024 16:17:33 +0900 Subject: [PATCH] Move ImGuiScene into Dalamud project --- .gitmodules | 6 +- Dalamud.CorePlugin/Dalamud.CorePlugin.csproj | 8 +- Dalamud.sln | 16 +- Dalamud/Dalamud.csproj | 18 +- .../Interface/ImGuiScene/D3DTextureWrap.cs | 60 + Dalamud/Interface/ImGuiScene/FodyWeavers.xml | 12 + Dalamud/Interface/ImGuiScene/FodyWeavers.xsd | 141 ++ .../Interface/ImGuiScene/FramerateLimit.cs | 75 + .../Interface/ImGuiScene/ImGui_Impl/Custom.cs | 10 + .../ImGui_Impl/Input/IImGuiInputHandler.cs | 8 + .../Input/ImGui_Input_Impl_Direct.cs | 1398 +++++++++++++++++ .../ImGui_Impl/Renderers/IImGuiRenderer.cs | 14 + .../ImGui_Impl/Renderers/ImGui_Impl_DX11.cs | 859 ++++++++++ Dalamud/Interface/ImGuiScene/RawDX11Scene.cs | 368 +++++ .../ImGuiScene/Win32 Utils/Constants.cs | 515 ++++++ .../ImGuiScene/Win32 Utils/MemUtil.cs | 15 + .../Interface/ImGuiScene/Win32 Utils/Win32.cs | 184 +++ .../Interface/ImGuiScene/costura64/stbi.dll | Bin 0 -> 90112 bytes .../resources/shaders/imgui-frag.glsl | 12 + .../resources/shaders/imgui-frag.hlsl.bytes | Bin 0 -> 740 bytes .../resources/shaders/imgui-vertex.glsl | 17 + .../resources/shaders/imgui-vertex.hlsl.bytes | Bin 0 -> 996 bytes Dalamud/Interface/Utility/ImGuiHelpers.cs | 2 + lib/ImGui.NET | 1 + lib/ImGuiScene | 1 - 25 files changed, 3711 insertions(+), 29 deletions(-) create mode 100644 Dalamud/Interface/ImGuiScene/D3DTextureWrap.cs create mode 100644 Dalamud/Interface/ImGuiScene/FodyWeavers.xml create mode 100644 Dalamud/Interface/ImGuiScene/FodyWeavers.xsd create mode 100644 Dalamud/Interface/ImGuiScene/FramerateLimit.cs create mode 100644 Dalamud/Interface/ImGuiScene/ImGui_Impl/Custom.cs create mode 100644 Dalamud/Interface/ImGuiScene/ImGui_Impl/Input/IImGuiInputHandler.cs create mode 100644 Dalamud/Interface/ImGuiScene/ImGui_Impl/Input/ImGui_Input_Impl_Direct.cs create mode 100644 Dalamud/Interface/ImGuiScene/ImGui_Impl/Renderers/IImGuiRenderer.cs create mode 100644 Dalamud/Interface/ImGuiScene/ImGui_Impl/Renderers/ImGui_Impl_DX11.cs create mode 100644 Dalamud/Interface/ImGuiScene/RawDX11Scene.cs create mode 100644 Dalamud/Interface/ImGuiScene/Win32 Utils/Constants.cs create mode 100644 Dalamud/Interface/ImGuiScene/Win32 Utils/MemUtil.cs create mode 100644 Dalamud/Interface/ImGuiScene/Win32 Utils/Win32.cs create mode 100644 Dalamud/Interface/ImGuiScene/costura64/stbi.dll create mode 100644 Dalamud/Interface/ImGuiScene/resources/shaders/imgui-frag.glsl create mode 100644 Dalamud/Interface/ImGuiScene/resources/shaders/imgui-frag.hlsl.bytes create mode 100644 Dalamud/Interface/ImGuiScene/resources/shaders/imgui-vertex.glsl create mode 100644 Dalamud/Interface/ImGuiScene/resources/shaders/imgui-vertex.hlsl.bytes create mode 160000 lib/ImGui.NET delete mode 160000 lib/ImGuiScene diff --git a/.gitmodules b/.gitmodules index dd184b54e..d379480d9 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,6 +1,3 @@ -[submodule "lib/ImGuiScene"] - path = lib/ImGuiScene - url = https://github.com/goatcorp/ImGuiScene [submodule "lib/FFXIVClientStructs"] path = lib/FFXIVClientStructs url = https://github.com/aers/FFXIVClientStructs @@ -10,3 +7,6 @@ [submodule "lib/TsudaKageyu-minhook"] path = lib/TsudaKageyu-minhook url = https://github.com/TsudaKageyu/minhook.git +[submodule "lib/ImGui.NET"] + path = lib/ImGui.NET + url = https://github.com/goatcorp/ImGui.NET.git diff --git a/Dalamud.CorePlugin/Dalamud.CorePlugin.csproj b/Dalamud.CorePlugin/Dalamud.CorePlugin.csproj index 626788a7a..b9bc63cd1 100644 --- a/Dalamud.CorePlugin/Dalamud.CorePlugin.csproj +++ b/Dalamud.CorePlugin/Dalamud.CorePlugin.csproj @@ -40,13 +40,7 @@ false - - false - - - false - - + false diff --git a/Dalamud.sln b/Dalamud.sln index 22cc59a8d..67c15dd2d 100644 --- a/Dalamud.sln +++ b/Dalamud.sln @@ -27,11 +27,7 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Dalamud.Test", "Dalamud.Tes EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Interface", "Interface", "{E15BDA6D-E881-4482-94BA-BE5527E917FF}" EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ImGui.NET-472", "lib\ImGuiScene\deps\ImGui.NET\src\ImGui.NET-472\ImGui.NET-472.csproj", "{0483026E-C6CE-4B1A-AA68-46544C08140B}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ImGuiScene", "lib\ImGuiScene\ImGuiScene\ImGuiScene.csproj", "{C0E7E797-4FBF-4F46-BC57-463F3719BA7A}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SDL2-CS", "lib\ImGuiScene\deps\SDL2-CS\SDL2-CS.csproj", "{2F7FF0A8-B619-4572-86C7-71E46FE22FB8}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ImGui.NET-472", "lib\ImGui.NET\src\ImGui.NET-472\ImGui.NET-472.csproj", "{0483026E-C6CE-4B1A-AA68-46544C08140B}" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Dalamud.CorePlugin", "Dalamud.CorePlugin\Dalamud.CorePlugin.csproj", "{4AFDB34A-7467-4D41-B067-53BC4101D9D0}" EndProject @@ -81,14 +77,6 @@ Global {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 @@ -127,8 +115,6 @@ Global EndGlobalSection GlobalSection(NestedProjects) = preSolution {0483026E-C6CE-4B1A-AA68-46544C08140B} = {E15BDA6D-E881-4482-94BA-BE5527E917FF} - {C0E7E797-4FBF-4F46-BC57-463F3719BA7A} = {E15BDA6D-E881-4482-94BA-BE5527E917FF} - {2F7FF0A8-B619-4572-86C7-71E46FE22FB8} = {E15BDA6D-E881-4482-94BA-BE5527E917FF} {C9B87BD7-AF49-41C3-91F1-D550ADEB7833} = {E15BDA6D-E881-4482-94BA-BE5527E917FF} {3620414C-7DFC-423E-929F-310E19F5D930} = {E15BDA6D-E881-4482-94BA-BE5527E917FF} {A6AA1C3F-9470-4922-9D3F-D4549657AB22} = {E15BDA6D-E881-4482-94BA-BE5527E917FF} diff --git a/Dalamud/Dalamud.csproj b/Dalamud/Dalamud.csproj index 2b575f617..775345d33 100644 --- a/Dalamud/Dalamud.csproj +++ b/Dalamud/Dalamud.csproj @@ -79,11 +79,18 @@ + + + + + + + all runtime; build; native; contentfiles; analyzers; buildtransitive @@ -95,12 +102,17 @@ + + + + + + + - - - + diff --git a/Dalamud/Interface/ImGuiScene/D3DTextureWrap.cs b/Dalamud/Interface/ImGuiScene/D3DTextureWrap.cs new file mode 100644 index 000000000..b621dbbd4 --- /dev/null +++ b/Dalamud/Interface/ImGuiScene/D3DTextureWrap.cs @@ -0,0 +1,60 @@ +using Dalamud.Interface.Textures.TextureWraps; + +using SharpDX.Direct3D11; + +/// +/// DX11 Implementation of . +/// Provides a simple wrapped view of the disposeable resource as well as the handle for ImGui. +/// +public class D3DTextureWrap : IDalamudTextureWrap +{ + // hold onto this directly for easier dispose etc and in case we need it later + private ShaderResourceView _resourceView = null; + + public int Width { get; } + public int Height { get; } + public IntPtr ImGuiHandle => (_resourceView == null) ? IntPtr.Zero : _resourceView.NativePointer; + + public D3DTextureWrap(ShaderResourceView texView, int width, int height) + { + _resourceView = texView; + Width = width; + Height = height; + } + + #region IDisposable Support + private bool disposedValue = false; // To detect redundant calls + + protected virtual void Dispose(bool disposing) + { + if (!disposedValue) + { + if (disposing) + { + // TODO: dispose managed state (managed objects). + } + + // TODO: free unmanaged resources (unmanaged objects) and override a finalizer below. + // TODO: set large fields to null. + + _resourceView?.Dispose(); + _resourceView = null; + + disposedValue = true; + } + } + + ~D3DTextureWrap() + { + // Do not change this code. Put cleanup code in Dispose(bool disposing) above. + Dispose(false); + } + + public void Dispose() + { + // Do not change this code. Put cleanup code in Dispose(bool disposing) above. + Dispose(true); + GC.SuppressFinalize(this); + } + #endregion +} diff --git a/Dalamud/Interface/ImGuiScene/FodyWeavers.xml b/Dalamud/Interface/ImGuiScene/FodyWeavers.xml new file mode 100644 index 000000000..68a407a25 --- /dev/null +++ b/Dalamud/Interface/ImGuiScene/FodyWeavers.xml @@ -0,0 +1,12 @@ + + + + + SDL2-CS + ImGui.NET + + + stbi + + + \ No newline at end of file diff --git a/Dalamud/Interface/ImGuiScene/FodyWeavers.xsd b/Dalamud/Interface/ImGuiScene/FodyWeavers.xsd new file mode 100644 index 000000000..af5be0b4e --- /dev/null +++ b/Dalamud/Interface/ImGuiScene/FodyWeavers.xsd @@ -0,0 +1,141 @@ + + + + + + + + + + + + A list of assembly names to exclude from the default action of "embed all Copy Local references", delimited with line breaks + + + + + A list of assembly names to include from the default action of "embed all Copy Local references", delimited with line breaks. + + + + + A list of runtime assembly names to exclude from the default action of "embed all Copy Local references", delimited with line breaks + + + + + A list of runtime assembly names to include from the default action of "embed all Copy Local references", delimited with line breaks. + + + + + A list of unmanaged 32 bit assembly names to include, delimited with line breaks. + + + + + A list of unmanaged 64 bit assembly names to include, delimited with line breaks. + + + + + The order of preloaded assemblies, delimited with line breaks. + + + + + + This will copy embedded files to disk before loading them into memory. This is helpful for some scenarios that expected an assembly to be loaded from a physical file. + + + + + Controls if .pdbs for reference assemblies are also embedded. + + + + + Controls if runtime assemblies are also embedded. + + + + + Controls whether the runtime assemblies are embedded with their full path or only with their assembly name. + + + + + Embedded assemblies are compressed by default, and uncompressed when they are loaded. You can turn compression off with this option. + + + + + As part of Costura, embedded assemblies are no longer included as part of the build. This cleanup can be turned off. + + + + + Costura by default will load as part of the module initialization. This flag disables that behavior. Make sure you call CosturaUtility.Initialize() somewhere in your code. + + + + + Costura will by default use assemblies with a name like 'resources.dll' as a satellite resource and prepend the output path. This flag disables that behavior. + + + + + A list of assembly names to exclude from the default action of "embed all Copy Local references", delimited with | + + + + + A list of assembly names to include from the default action of "embed all Copy Local references", delimited with |. + + + + + A list of runtime assembly names to exclude from the default action of "embed all Copy Local references", delimited with | + + + + + A list of runtime assembly names to include from the default action of "embed all Copy Local references", delimited with |. + + + + + A list of unmanaged 32 bit assembly names to include, delimited with |. + + + + + A list of unmanaged 64 bit assembly names to include, delimited with |. + + + + + The order of preloaded assemblies, delimited with |. + + + + + + + + 'true' to run assembly verification (PEVerify) on the target assembly after all weavers have been executed. + + + + + A comma-separated list of error codes that can be safely ignored in assembly verification. + + + + + 'false' to turn off automatic generation of the XML Schema file. + + + + + \ No newline at end of file diff --git a/Dalamud/Interface/ImGuiScene/FramerateLimit.cs b/Dalamud/Interface/ImGuiScene/FramerateLimit.cs new file mode 100644 index 000000000..06227f6d7 --- /dev/null +++ b/Dalamud/Interface/ImGuiScene/FramerateLimit.cs @@ -0,0 +1,75 @@ +namespace ImGuiScene +{ + /// + /// Simple encapsulation of framerate limiting behavior, allowing for fully unbounded (no control), + /// Vsync-enabled (sync to monitor refresh), or a specified fixed framerate (vsync disabled, hard time cap) + /// + public class FramerateLimit + { + /// + /// The different methods of limiting framerate. + /// + public enum LimitType + { + /// + /// No limiting at all. + /// + Unbounded, + /// + /// Vsync enabled. Render presentation will be synced to display refresh rate. + /// + Vsync, + /// + /// Restrict rendering to a fixed (maximum) number of frames per second. + /// This will disable vsync regardless of the fps value. + /// + FixedFPS + } + + /// + /// Which type of framerate limiting to apply. + /// + public LimitType Type { get; } + + private readonly int _fps; + /// + /// The current FPS limit. Only valid with . + /// + public int FPS + { + get + { + if (Type != LimitType.FixedFPS) + throw new InvalidOperationException(); + + return _fps; + } + } + + /// + /// Creates a new framerate limit description. + /// + /// Which type of limiting to apply. + /// Used only with , the target frames per second to restrict rendering to. + public FramerateLimit(LimitType limitType, int targetFps = 0) + { + if (limitType == LimitType.FixedFPS && targetFps <= 0) + { + limitType = LimitType.Unbounded; + } + + Type = limitType; + _fps = targetFps; + } + + public override string ToString() + { + var str = Type.ToString(); + if (Type == LimitType.FixedFPS) + { + str += $" ({FPS})"; + } + return str; + } + } +} diff --git a/Dalamud/Interface/ImGuiScene/ImGui_Impl/Custom.cs b/Dalamud/Interface/ImGuiScene/ImGui_Impl/Custom.cs new file mode 100644 index 000000000..68b47315e --- /dev/null +++ b/Dalamud/Interface/ImGuiScene/ImGui_Impl/Custom.cs @@ -0,0 +1,10 @@ +using System.Runtime.InteropServices; + +namespace ImGuiScene.ImGui_Impl { + + // Custom cimgui functions we use for utility purposes + internal static class Custom { + [DllImport("cimgui", CallingConvention = CallingConvention.Cdecl)] + public static extern void igCustom_ClearStacks(); + } +} diff --git a/Dalamud/Interface/ImGuiScene/ImGui_Impl/Input/IImGuiInputHandler.cs b/Dalamud/Interface/ImGuiScene/ImGui_Impl/Input/IImGuiInputHandler.cs new file mode 100644 index 000000000..7c2d3cfbe --- /dev/null +++ b/Dalamud/Interface/ImGuiScene/ImGui_Impl/Input/IImGuiInputHandler.cs @@ -0,0 +1,8 @@ +namespace ImGuiScene +{ + public interface IImGuiInputHandler : IDisposable + { + void NewFrame(int width, int height); + void SetIniPath(string path); + } +} diff --git a/Dalamud/Interface/ImGuiScene/ImGui_Impl/Input/ImGui_Input_Impl_Direct.cs b/Dalamud/Interface/ImGuiScene/ImGui_Impl/Input/ImGui_Input_Impl_Direct.cs new file mode 100644 index 000000000..3b7fbba3b --- /dev/null +++ b/Dalamud/Interface/ImGuiScene/ImGui_Impl/Input/ImGui_Input_Impl_Direct.cs @@ -0,0 +1,1398 @@ +using ImGuiNET; + +using System.Diagnostics; +using System.Linq; +using System.Numerics; +using System.Runtime.InteropServices; +using PInvoke; + +namespace ImGuiScene +{ + // largely a port of https://github.com/ocornut/imgui/blob/master/examples/imgui_impl_win32.cpp, though some changes + // and wndproc hooking + public unsafe class ImGui_Input_Impl_Direct : IImGuiInputHandler + { + private long _lastTime; + + private IntPtr _platformNamePtr; + private IntPtr _iniPathPtr; + private IntPtr _classNamePtr; + private IntPtr _hWnd; + + private User32.WndProc _wndProcDelegate; + private bool[] _imguiMouseIsDown; + + // private ImGuiMouseCursor _oldCursor = ImGuiMouseCursor.None; + private IntPtr[] _cursors; + + public bool UpdateCursor { get; set; } = true; + + public unsafe ImGui_Input_Impl_Direct(IntPtr hWnd) + { + _hWnd = hWnd; + + // hook wndproc + // have to hold onto the delegate to keep it in memory for unmanaged code + _wndProcDelegate = WndProcDetour; + + var io = ImGui.GetIO(); + + io.BackendFlags |= ImGuiBackendFlags.HasMouseCursors | + ImGuiBackendFlags.HasSetMousePos | + ImGuiBackendFlags.RendererHasViewports | + ImGuiBackendFlags.PlatformHasViewports; + + _platformNamePtr = Marshal.StringToHGlobalAnsi("imgui_impl_win32_c#"); + io.NativePtr->BackendPlatformName = (byte*)_platformNamePtr.ToPointer(); + + ImGuiViewportPtr mainViewport = ImGui.GetMainViewport(); + mainViewport.PlatformHandle = mainViewport.PlatformHandleRaw = hWnd; + if (io.ConfigFlags.HasFlag(ImGuiConfigFlags.ViewportsEnable)) + ImGui_ImplWin32_InitPlatformInterface(); + + _imguiMouseIsDown = new bool[5]; + + _cursors = new IntPtr[9]; + _cursors[(int)ImGuiMouseCursor.Arrow] = Win32.LoadCursor(IntPtr.Zero, Cursor.IDC_ARROW); + _cursors[(int)ImGuiMouseCursor.TextInput] = Win32.LoadCursor(IntPtr.Zero, Cursor.IDC_IBEAM); + _cursors[(int)ImGuiMouseCursor.ResizeAll] = Win32.LoadCursor(IntPtr.Zero, Cursor.IDC_SIZEALL); + _cursors[(int)ImGuiMouseCursor.ResizeEW] = Win32.LoadCursor(IntPtr.Zero, Cursor.IDC_SIZEWE); + _cursors[(int)ImGuiMouseCursor.ResizeNS] = Win32.LoadCursor(IntPtr.Zero, Cursor.IDC_SIZENS); + _cursors[(int)ImGuiMouseCursor.ResizeNESW] = Win32.LoadCursor(IntPtr.Zero, Cursor.IDC_SIZENESW); + _cursors[(int)ImGuiMouseCursor.ResizeNWSE] = Win32.LoadCursor(IntPtr.Zero, Cursor.IDC_SIZENWSE); + _cursors[(int)ImGuiMouseCursor.Hand] = Win32.LoadCursor(IntPtr.Zero, Cursor.IDC_HAND); + _cursors[(int)ImGuiMouseCursor.NotAllowed] = Win32.LoadCursor(IntPtr.Zero, Cursor.IDC_NO); + } + + public bool IsImGuiCursor(IntPtr hCursor) + { + return _cursors?.Contains(hCursor) ?? false; + } + + 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 = _lastTime > 0 ? (float)((double)(currentTime - _lastTime) / frequency) : 1f / 60; + _lastTime = currentTime; + + UpdateMousePos(); + + 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) && UpdateCursor) + { + 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 (int i = 0; i < io.MouseDown.Count; i++) + { + io.MouseDown[i] = false; + } + } + } + + public void SetIniPath(string iniPath) + { + // TODO: error/messaging when trying to set after first render? + if (iniPath != null) + { + if (_iniPathPtr != IntPtr.Zero) + { + Marshal.FreeHGlobal(_iniPathPtr); + } + + _iniPathPtr = Marshal.StringToHGlobalAnsi(iniPath); + unsafe + { + ImGui.GetIO().NativePtr->IniFilename = (byte*)_iniPathPtr.ToPointer(); + } + } + } + + private void UpdateMousePos() + { + var io = ImGui.GetIO(); + + // Depending on if Viewports are enabled, we have to change how we process + // the cursor position. If viewports are enabled, we pass the absolute cursor + // position to ImGui. Otherwise, we use the old method of passing client-local + // mouse position to ImGui. + if (io.ConfigFlags.HasFlag(ImGuiConfigFlags.ViewportsEnable)) + { + if (io.WantSetMousePos) + { + Win32.SetCursorPos((int)io.MousePos.X, (int)io.MousePos.Y); + } + + if (Win32.GetCursorPos(out Win32.POINT 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) + { + var pos = new Win32.POINT { X = (int)io.MousePos.X, Y = (int)io.MousePos.Y }; + Win32.ClientToScreen(_hWnd, ref pos); + Win32.SetCursorPos(pos.X, pos.Y); + } + + if (Win32.GetCursorPos(out Win32.POINT pt) && Win32.ScreenToClient(_hWnd, ref pt)) + { + io.MousePos.X = pt.X; + io.MousePos.Y = pt.Y; + } + else + { + io.MousePos.X = float.MinValue; + io.MousePos.Y = float.MinValue; + } + } + } + + // TODO This is kind of unnecessary unless we REALLY want viewport hovered support + // It seems to mess with the mouse and get it stuck a lot. Do not know why + // private void UpdateMousePos() { + // ImGuiIOPtr io = ImGui.GetIO(); + // + // // Set OS mouse position if requested (rarely used, only when ImGuiConfigFlags_NavEnableSetMousePos is enabled by user) + // // (When multi-viewports are enabled, all imgui positions are same as OS positions) + // if (io.WantSetMousePos) { + // POINT pos = new POINT() {x = (int) io.MousePos.X, y = (int) io.MousePos.Y}; + // if ((io.ConfigFlags & ImGuiConfigFlags.ViewportsEnable) == 0) + // User32.ClientToScreen(_hWnd, ref pos); + // User32.SetCursorPos(pos.x, pos.y); + // } + // + // io.MousePos = new Vector2(float.NegativeInfinity, float.NegativeInfinity); + // io.MouseHoveredViewport = 0; + // + // // Set imgui mouse position + // if (!User32.GetCursorPos(out POINT mouseScreenPos)) + // return; + // IntPtr focusedHwnd = User32.GetForegroundWindow(); + // if (focusedHwnd != IntPtr.Zero) { + // if (User32.IsChild(focusedHwnd, _hWnd)) + // focusedHwnd = _hWnd; + // if ((io.ConfigFlags & ImGuiConfigFlags.ViewportsEnable) == ImGuiConfigFlags.ViewportsEnable) { + // // Multi-viewport mode: mouse position in OS absolute coordinates (io.MousePos is (0,0) when the mouse is on the upper-left of the primary monitor) + // // This is the position you can get with GetCursorPos(). In theory adding viewport->Pos is also the reverse operation of doing ScreenToClient(). + // ImGuiViewportPtr viewport = ImGui.FindViewportByPlatformHandle(focusedHwnd); + // unsafe { + // if (viewport.NativePtr != null) + // io.MousePos = new Vector2(mouseScreenPos.x, mouseScreenPos.y); + // } + // } else { + // // Single viewport mode: mouse position in client window coordinates (io.MousePos is (0,0) when the mouse is on the upper-left corner of the app window.) + // // This is the position you can get with GetCursorPos() + ScreenToClient() or from WM_MOUSEMOVE. + // if (focusedHwnd == _hWnd) { + // POINT mouseClientPos = mouseScreenPos; + // User32.ScreenToClient(focusedHwnd, ref mouseClientPos); + // io.MousePos = new Vector2(mouseClientPos.x, mouseClientPos.y); + // } + // } + // } + // + // // (Optional) When using multiple viewports: set io.MouseHoveredViewport to the viewport the OS mouse cursor is hovering. + // // Important: this information is not easy to provide and many high-level windowing library won't be able to provide it correctly, because + // // - This is _ignoring_ viewports with the ImGuiViewportFlags_NoInputs flag (pass-through windows). + // // - This is _regardless_ of whether another viewport is focused or being dragged from. + // // If ImGuiBackendFlags_HasMouseHoveredViewport is not set by the backend, imgui will ignore this field and infer the information by relying on the + // // rectangles and last focused time of every viewports it knows about. It will be unaware of foreign windows that may be sitting between or over your windows. + // IntPtr hovered_hwnd = User32.WindowFromPoint(mouseScreenPos); + // if (hovered_hwnd != IntPtr.Zero) { + // ImGuiViewportPtr viewport = ImGui.FindViewportByPlatformHandle(focusedHwnd); + // unsafe { + // if (viewport.NativePtr != null) + // if ((viewport.Flags & ImGuiViewportFlags.NoInputs) == 0 + // ) // FIXME: We still get our NoInputs window with WM_NCHITTEST/HTTRANSPARENT code when decorated? + // io.MouseHoveredViewport = viewport.ID; + // } + // } + // } + + private bool UpdateMouseCursor() + { + var io = ImGui.GetIO(); + if (io.ConfigFlags.HasFlag(ImGuiConfigFlags.NoMouseCursorChange)) + return false; + + var cur = ImGui.GetMouseCursor(); + if (cur == ImGuiMouseCursor.None || io.MouseDrawCursor) + Win32.SetCursor(IntPtr.Zero); + else + Win32.SetCursor(_cursors[(int)cur]); + + return true; + } + + /// + /// 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 unsafe IntPtr? ProcessWndProcW(IntPtr hWnd, User32.WindowMessage msg, void* wParam, void* lParam) + { + if (ImGui.GetCurrentContext() != IntPtr.Zero) + { + var io = ImGui.GetIO(); + + switch (msg) + { + case User32.WindowMessage.WM_LBUTTONDOWN: + case User32.WindowMessage.WM_LBUTTONDBLCLK: + case User32.WindowMessage.WM_RBUTTONDOWN: + case User32.WindowMessage.WM_RBUTTONDBLCLK: + case User32.WindowMessage.WM_MBUTTONDOWN: + case User32.WindowMessage.WM_MBUTTONDBLCLK: + case User32.WindowMessage.WM_XBUTTONDOWN: + case User32.WindowMessage.WM_XBUTTONDBLCLK: { + var button = GetButton(msg, (ulong)wParam); + if (io.WantCaptureMouse) + { + if (!ImGui.IsAnyMouseDown() && Win32.GetCapture() == IntPtr.Zero) + Win32.SetCapture(hWnd); + + io.MouseDown[button] = true; + this._imguiMouseIsDown[button] = true; + return IntPtr.Zero; + } + break; + } + case User32.WindowMessage.WM_LBUTTONUP: + case User32.WindowMessage.WM_RBUTTONUP: + case User32.WindowMessage.WM_MBUTTONUP: + case User32.WindowMessage.WM_XBUTTONUP: { + var button = GetButton(msg, (ulong)wParam); + if (io.WantCaptureMouse && this._imguiMouseIsDown[button]) + { + if (!ImGui.IsAnyMouseDown() && Win32.GetCapture() == hWnd) + Win32.ReleaseCapture(); + + io.MouseDown[button] = false; + this._imguiMouseIsDown[button] = false; + return IntPtr.Zero; + } + break; + } + case User32.WindowMessage.WM_MOUSEWHEEL: + if (io.WantCaptureMouse) + { + io.MouseWheel += (float)Win32.GET_WHEEL_DELTA_WPARAM((ulong)wParam) / + (float)Win32Constants.WHEEL_DELTA; + return IntPtr.Zero; + } + + break; + case User32.WindowMessage.WM_MOUSEHWHEEL: + if (io.WantCaptureMouse) + { + io.MouseWheelH += (float)Win32.GET_WHEEL_DELTA_WPARAM((ulong)wParam) / + (float)Win32Constants.WHEEL_DELTA; + return IntPtr.Zero; + } + + break; + case User32.WindowMessage.WM_KEYDOWN: + case User32.WindowMessage.WM_SYSKEYDOWN: + case User32.WindowMessage.WM_KEYUP: + case User32.WindowMessage.WM_SYSKEYUP: + bool isKeyDown = (msg == User32.WindowMessage.WM_KEYDOWN || msg == User32.WindowMessage.WM_SYSKEYDOWN); + if ((int)wParam < 256) + { + // 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 = (VirtualKey)(int)wParam; + if (((int)wParam == (int)VirtualKey.Return) && ((int)lParam & (256 << 16)) > 0) + vk = (VirtualKey.Return + 256); + + // Submit key event + var key = VirtualKeyToImGuiKey((int)vk); + var scancode = ((int)lParam & 0xff0000) >> 16; + if (key != ImGuiKey.None && io.WantTextInput) { + AddKeyEvent(key, isKeyDown, vk, scancode); + return IntPtr.Zero; + } + + // Submit individual left/right modifier events + if (vk == VirtualKey.Shift) + { + // Important: Shift keys tend to get stuck when pressed together, missing key-up events are corrected in ImGui_ImplWin32_ProcessKeyEventsWorkarounds() + if (IsVkDown(VirtualKey.LeftShift) == isKeyDown) { AddKeyEvent(ImGuiKey.LeftShift, isKeyDown, VirtualKey.LeftShift, scancode); } + if (IsVkDown(VirtualKey.RightShift) == isKeyDown) { AddKeyEvent(ImGuiKey.RightShift, isKeyDown, VirtualKey.RightShift, scancode); } + } + else if (vk == VirtualKey.Control) + { + if (IsVkDown(VirtualKey.LeftControl) == isKeyDown) { AddKeyEvent(ImGuiKey.LeftCtrl, isKeyDown, VirtualKey.LeftControl, scancode); } + if (IsVkDown(VirtualKey.RightControl) == isKeyDown) { AddKeyEvent(ImGuiKey.RightCtrl, isKeyDown, VirtualKey.RightControl, scancode); } + } + else if (vk == VirtualKey.Menu) + { + if (IsVkDown(VirtualKey.LeftMenu) == isKeyDown) { AddKeyEvent(ImGuiKey.LeftAlt, isKeyDown, VirtualKey.LeftMenu, scancode); } + if (IsVkDown(VirtualKey.RightMenu) == isKeyDown) { AddKeyEvent(ImGuiKey.RightAlt, isKeyDown, VirtualKey.RightMenu, scancode); } + } + } + break; + case User32.WindowMessage.WM_CHAR: + if (io.WantTextInput) + { + io.AddInputCharacter((uint)wParam); + return IntPtr.Zero; + } + break; + // this never seemed to work reasonably, but I'll leave it for now + case User32.WindowMessage.WM_SETCURSOR: + if (io.WantCaptureMouse) + { + if (Win32.LOWORD((ulong)lParam) == Win32Constants.HTCLIENT && UpdateMouseCursor()) + { + // this message returns 1 to block further processing + // because consistency is no fun + return (IntPtr)1; + } + } + break; + // TODO: Decode why IME is miserable + // case User32.WindowMessage.WM_IME_NOTIFY: + // return HandleImeMessage(hWnd, (long) wParam, (long) lParam); + default: + break; + } + } + + // We did not produce a result - return -1 + return null; + } + + private int GetButton(User32.WindowMessage msg, ulong wParam) { + switch (msg) + { + case User32.WindowMessage.WM_LBUTTONUP: + case User32.WindowMessage.WM_LBUTTONDOWN: + case User32.WindowMessage.WM_LBUTTONDBLCLK: + return 0; + case User32.WindowMessage.WM_RBUTTONUP: + case User32.WindowMessage.WM_RBUTTONDOWN: + case User32.WindowMessage.WM_RBUTTONDBLCLK: + return 1; + case User32.WindowMessage.WM_MBUTTONUP: + case User32.WindowMessage.WM_MBUTTONDOWN: + case User32.WindowMessage.WM_MBUTTONDBLCLK: + return 2; + case User32.WindowMessage.WM_XBUTTONUP: + case User32.WindowMessage.WM_XBUTTONDOWN: + case User32.WindowMessage.WM_XBUTTONDBLCLK: + return Win32.GET_XBUTTON_WPARAM(wParam) == Win32Constants.XBUTTON1 ? 3 : 4; + default: + return 0; + } + } + + 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(VirtualKey.LeftShift)) + AddKeyEvent(ImGuiKey.LeftShift, false, VirtualKey.LeftShift); + if (ImGui.IsKeyDown(ImGuiKey.RightShift) && !IsVkDown(VirtualKey.RightShift)) + AddKeyEvent(ImGuiKey.RightShift, false, VirtualKey.RightShift); + + // 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(VirtualKey.LeftWindows)) + AddKeyEvent(ImGuiKey.LeftSuper, false, VirtualKey.LeftWindows); + if (ImGui.IsKeyDown(ImGuiKey.RightSuper) && !IsVkDown(VirtualKey.RightWindows)) + AddKeyEvent(ImGuiKey.RightSuper, false, VirtualKey.RightWindows); + + // 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) + { + for (int 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 (User32.GetForegroundWindow() == this._hWnd && + (IsGamepadKey((ImGuiKey) i) || + IsModKey((ImGuiKey) i))) + continue; + io.AddKeyEvent((ImGuiKey) i, false); + } + } + } + + private static void AddKeyEvent(ImGuiKey key, bool down, VirtualKey nativeKeycode, int nativeScancode = -1) { + var io = ImGui.GetIO(); + io.AddKeyEvent(key, down); + io.SetKeyEventNativeData(key, (int)nativeKeycode, nativeScancode); + } + + static void UpdateKeyModifiers() + { + var io = ImGui.GetIO(); + io.AddKeyEvent(ImGuiKey.ModCtrl, IsVkDown(VirtualKey.Control)); + io.AddKeyEvent(ImGuiKey.ModShift, IsVkDown(VirtualKey.Shift)); + io.AddKeyEvent(ImGuiKey.ModAlt, IsVkDown(VirtualKey.Menu)); + io.AddKeyEvent(ImGuiKey.ModSuper, IsVkDown(VirtualKey.Application)); + } + + private static bool IsVkDown(VirtualKey key) { + return (Win32.GetKeyState(key) & 0x8000) != 0; + } + + // TODO: Decode why IME is miserable + // private int HandleImeMessage(IntPtr hWnd, long wParam, long lParam) { + // + // int result = -1; + // // if (io.WantCaptureKeyboard) + // result = (int) User32.DefWindowProc(hWnd, User32.WindowMessage.WM_IME_NOTIFY, (IntPtr) wParam, (IntPtr) lParam); + // System.Diagnostics.Debug.WriteLine($"ime command {(Win32.ImeCommand) wParam} result {result}"); + // + // return result; + // } + + /// + /// This WndProc is called for ImGuiScene windows. WndProc for main window will be called back from somewhere else. + /// + private unsafe IntPtr WndProcDetour(IntPtr hWnd, User32.WindowMessage msg, void* wParam, void* lParam) + { + // Attempt to process the result of this window message + // We will return the result here if we consider the message handled + var processResult = ProcessWndProcW(hWnd, 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() == IntPtr.Zero) + return User32.DefWindowProc(hWnd, msg, (IntPtr)wParam, (IntPtr)lParam); + ImGuiViewportPtr viewport = ImGui.FindViewportByPlatformHandle(hWnd); + + if (viewport.NativePtr != null) + { + switch (msg) + { + case User32.WindowMessage.WM_CLOSE: + viewport.PlatformRequestClose = true; + return IntPtr.Zero; + case User32.WindowMessage.WM_MOVE: + viewport.PlatformRequestMove = true; + break; + case User32.WindowMessage.WM_SIZE: + viewport.PlatformRequestResize = true; + break; + case User32.WindowMessage.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 + User32.SetForegroundWindow(_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 + User32.SetCapture(_hWnd); + + // We still want to return MA_NOACTIVATE + // https://docs.microsoft.com/en-us/windows/win32/inputdev/wm-mouseactivate + return (IntPtr)0x3; + case User32.WindowMessage.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 (IntPtr)uint.MaxValue; + } + break; + } + } + + return User32.DefWindowProc(hWnd, msg, (IntPtr)wParam, (IntPtr)lParam); + } + + private static void UpAllKeys() { + var io = ImGui.GetIO(); + for (int 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 (int i = 0; i < io.MouseDown.Count; i++) + io.MouseDown[i] = false; + } + + #region large switch statements + // Map VK_xxx to ImGuiKey.xxx. + public static ImGuiKey VirtualKeyToImGuiKey(int key) { + return (VirtualKey)key switch { + VirtualKey.Tab => ImGuiKey.Tab, + VirtualKey.Left => ImGuiKey.LeftArrow, + VirtualKey.Right => ImGuiKey.RightArrow, + VirtualKey.Up => ImGuiKey.UpArrow, + VirtualKey.Down => ImGuiKey.DownArrow, + VirtualKey.Prior => ImGuiKey.PageUp, + VirtualKey.Next => ImGuiKey.PageDown, + VirtualKey.Home => ImGuiKey.Home, + VirtualKey.End => ImGuiKey.End, + VirtualKey.Insert => ImGuiKey.Insert, + VirtualKey.Delete => ImGuiKey.Delete, + VirtualKey.Back => ImGuiKey.Backspace, + VirtualKey.Space => ImGuiKey.Space, + VirtualKey.Return => ImGuiKey.Enter, + VirtualKey.Escape => ImGuiKey.Escape, + VirtualKey.OEM7 => ImGuiKey.Apostrophe, + VirtualKey.OEMComma => ImGuiKey.Comma, + VirtualKey.OEMMinus => ImGuiKey.Minus, + VirtualKey.OEMPeriod => ImGuiKey.Period, + VirtualKey.OEM2 => ImGuiKey.Slash, + VirtualKey.OEM1 => ImGuiKey.Semicolon, + VirtualKey.OEMPlus => ImGuiKey.Equal, + VirtualKey.OEM4 => ImGuiKey.LeftBracket, + VirtualKey.OEM5 => ImGuiKey.Backslash, + VirtualKey.OEM6 => ImGuiKey.RightBracket, + VirtualKey.OEM3 => ImGuiKey.GraveAccent, + VirtualKey.CapsLock => ImGuiKey.CapsLock, + VirtualKey.ScrollLock => ImGuiKey.ScrollLock, + VirtualKey.NumLock => ImGuiKey.NumLock, + VirtualKey.Snapshot => ImGuiKey.PrintScreen, + VirtualKey.Pause => ImGuiKey.Pause, + VirtualKey.Numpad0 => ImGuiKey.Keypad0, + VirtualKey.Numpad1 => ImGuiKey.Keypad1, + VirtualKey.Numpad2 => ImGuiKey.Keypad2, + VirtualKey.Numpad3 => ImGuiKey.Keypad3, + VirtualKey.Numpad4 => ImGuiKey.Keypad4, + VirtualKey.Numpad5 => ImGuiKey.Keypad5, + VirtualKey.Numpad6 => ImGuiKey.Keypad6, + VirtualKey.Numpad7 => ImGuiKey.Keypad7, + VirtualKey.Numpad8 => ImGuiKey.Keypad8, + VirtualKey.Numpad9 => ImGuiKey.Keypad9, + VirtualKey.Decimal => ImGuiKey.KeypadDecimal, + VirtualKey.Divide => ImGuiKey.KeypadDivide, + VirtualKey.Multiply => ImGuiKey.KeypadMultiply, + VirtualKey.Subtract => ImGuiKey.KeypadSubtract, + VirtualKey.Add => ImGuiKey.KeypadAdd, + (VirtualKey.Return + 256) => ImGuiKey.KeypadEnter, + VirtualKey.LeftShift => ImGuiKey.LeftShift, + VirtualKey.LeftControl => ImGuiKey.LeftCtrl, + VirtualKey.LeftMenu => ImGuiKey.LeftAlt, + VirtualKey.LeftWindows => ImGuiKey.LeftSuper, + VirtualKey.RightShift => ImGuiKey.RightShift, + VirtualKey.RightControl => ImGuiKey.RightCtrl, + VirtualKey.RightMenu => ImGuiKey.RightAlt, + VirtualKey.RightWindows => ImGuiKey.RightSuper, + VirtualKey.Application => ImGuiKey.Menu, + VirtualKey.N0 => ImGuiKey._0, + VirtualKey.N1 => ImGuiKey._1, + VirtualKey.N2 => ImGuiKey._2, + VirtualKey.N3 => ImGuiKey._3, + VirtualKey.N4 => ImGuiKey._4, + VirtualKey.N5 => ImGuiKey._5, + VirtualKey.N6 => ImGuiKey._6, + VirtualKey.N7 => ImGuiKey._7, + VirtualKey.N8 => ImGuiKey._8, + VirtualKey.N9 => ImGuiKey._9, + VirtualKey.A => ImGuiKey.A, + VirtualKey.B => ImGuiKey.B, + VirtualKey.C => ImGuiKey.C, + VirtualKey.D => ImGuiKey.D, + VirtualKey.E => ImGuiKey.E, + VirtualKey.F => ImGuiKey.F, + VirtualKey.G => ImGuiKey.G, + VirtualKey.H => ImGuiKey.H, + VirtualKey.I => ImGuiKey.I, + VirtualKey.J => ImGuiKey.J, + VirtualKey.K => ImGuiKey.K, + VirtualKey.L => ImGuiKey.L, + VirtualKey.M => ImGuiKey.M, + VirtualKey.N => ImGuiKey.N, + VirtualKey.O => ImGuiKey.O, + VirtualKey.P => ImGuiKey.P, + VirtualKey.Q => ImGuiKey.Q, + VirtualKey.R => ImGuiKey.R, + VirtualKey.S => ImGuiKey.S, + VirtualKey.T => ImGuiKey.T, + VirtualKey.U => ImGuiKey.U, + VirtualKey.V => ImGuiKey.V, + VirtualKey.W => ImGuiKey.W, + VirtualKey.X => ImGuiKey.X, + VirtualKey.Y => ImGuiKey.Y, + VirtualKey.Z => ImGuiKey.Z, + VirtualKey.F1 => ImGuiKey.F1, + VirtualKey.F2 => ImGuiKey.F2, + VirtualKey.F3 => ImGuiKey.F3, + VirtualKey.F4 => ImGuiKey.F4, + VirtualKey.F5 => ImGuiKey.F5, + VirtualKey.F6 => ImGuiKey.F6, + VirtualKey.F7 => ImGuiKey.F7, + VirtualKey.F8 => ImGuiKey.F8, + VirtualKey.F9 => ImGuiKey.F9, + VirtualKey.F10 => ImGuiKey.F10, + VirtualKey.F11 => ImGuiKey.F11, + VirtualKey.F12 => ImGuiKey.F12, + _ => ImGuiKey.None + }; + } + + // Map ImGuiKey.xxx to VK_xxx. + public static int ImGuiKeyToVirtualKey(ImGuiKey key) { + VirtualKey vk = key switch { + ImGuiKey.Tab => VirtualKey.Tab, + ImGuiKey.LeftArrow => VirtualKey.Left, + ImGuiKey.RightArrow => VirtualKey.Right, + ImGuiKey.UpArrow => VirtualKey.Up, + ImGuiKey.DownArrow => VirtualKey.Down, + ImGuiKey.PageUp => VirtualKey.Prior, + ImGuiKey.PageDown => VirtualKey.Next, + ImGuiKey.Home => VirtualKey.Home, + ImGuiKey.End => VirtualKey.End, + ImGuiKey.Insert => VirtualKey.Insert, + ImGuiKey.Delete => VirtualKey.Delete, + ImGuiKey.Backspace => VirtualKey.Back, + ImGuiKey.Space => VirtualKey.Space, + ImGuiKey.Enter => VirtualKey.Return, + ImGuiKey.Escape => VirtualKey.Escape, + ImGuiKey.Apostrophe => VirtualKey.OEM7, + ImGuiKey.Comma => VirtualKey.OEMComma, + ImGuiKey.Minus => VirtualKey.OEMMinus, + ImGuiKey.Period => VirtualKey.OEMPeriod, + ImGuiKey.Slash => VirtualKey.OEM2, + ImGuiKey.Semicolon => VirtualKey.OEM1, + ImGuiKey.Equal => VirtualKey.OEMPlus, + ImGuiKey.LeftBracket => VirtualKey.OEM4, + ImGuiKey.Backslash => VirtualKey.OEM5, + ImGuiKey.RightBracket => VirtualKey.OEM6, + ImGuiKey.GraveAccent => VirtualKey.OEM3, + ImGuiKey.CapsLock => VirtualKey.CapsLock, + ImGuiKey.ScrollLock => VirtualKey.ScrollLock, + ImGuiKey.NumLock => VirtualKey.NumLock, + ImGuiKey.PrintScreen => VirtualKey.Snapshot, + ImGuiKey.Pause => VirtualKey.Pause, + ImGuiKey.Keypad0 => VirtualKey.Numpad0, + ImGuiKey.Keypad1 => VirtualKey.Numpad1, + ImGuiKey.Keypad2 => VirtualKey.Numpad2, + ImGuiKey.Keypad3 => VirtualKey.Numpad3, + ImGuiKey.Keypad4 => VirtualKey.Numpad4, + ImGuiKey.Keypad5 => VirtualKey.Numpad5, + ImGuiKey.Keypad6 => VirtualKey.Numpad6, + ImGuiKey.Keypad7 => VirtualKey.Numpad7, + ImGuiKey.Keypad8 => VirtualKey.Numpad8, + ImGuiKey.Keypad9 => VirtualKey.Numpad9, + ImGuiKey.KeypadDecimal => VirtualKey.Decimal, + ImGuiKey.KeypadDivide => VirtualKey.Divide, + ImGuiKey.KeypadMultiply => VirtualKey.Multiply, + ImGuiKey.KeypadSubtract => VirtualKey.Subtract, + ImGuiKey.KeypadAdd => VirtualKey.Add, + ImGuiKey.KeypadEnter => (VirtualKey.Return + 256), + ImGuiKey.LeftShift => VirtualKey.LeftShift, + ImGuiKey.LeftCtrl => VirtualKey.LeftControl, + ImGuiKey.LeftAlt => VirtualKey.LeftMenu, + ImGuiKey.LeftSuper => VirtualKey.LeftWindows, + ImGuiKey.RightShift => VirtualKey.RightShift, + ImGuiKey.RightCtrl => VirtualKey.RightControl, + ImGuiKey.RightAlt => VirtualKey.RightMenu, + ImGuiKey.RightSuper => VirtualKey.RightWindows, + ImGuiKey.Menu => VirtualKey.Application, + ImGuiKey._0 => VirtualKey.N0, + ImGuiKey._1 => VirtualKey.N1, + ImGuiKey._2 => VirtualKey.N2, + ImGuiKey._3 => VirtualKey.N3, + ImGuiKey._4 => VirtualKey.N4, + ImGuiKey._5 => VirtualKey.N5, + ImGuiKey._6 => VirtualKey.N6, + ImGuiKey._7 => VirtualKey.N7, + ImGuiKey._8 => VirtualKey.N8, + ImGuiKey._9 => VirtualKey.N9, + ImGuiKey.A => VirtualKey.A, + ImGuiKey.B => VirtualKey.B, + ImGuiKey.C => VirtualKey.C, + ImGuiKey.D => VirtualKey.D, + ImGuiKey.E => VirtualKey.E, + ImGuiKey.F => VirtualKey.F, + ImGuiKey.G => VirtualKey.G, + ImGuiKey.H => VirtualKey.H, + ImGuiKey.I => VirtualKey.I, + ImGuiKey.J => VirtualKey.J, + ImGuiKey.K => VirtualKey.K, + ImGuiKey.L => VirtualKey.L, + ImGuiKey.M => VirtualKey.M, + ImGuiKey.N => VirtualKey.N, + ImGuiKey.O => VirtualKey.O, + ImGuiKey.P => VirtualKey.P, + ImGuiKey.Q => VirtualKey.Q, + ImGuiKey.R => VirtualKey.R, + ImGuiKey.S => VirtualKey.S, + ImGuiKey.T => VirtualKey.T, + ImGuiKey.U => VirtualKey.U, + ImGuiKey.V => VirtualKey.V, + ImGuiKey.W => VirtualKey.W, + ImGuiKey.X => VirtualKey.X, + ImGuiKey.Y => VirtualKey.Y, + ImGuiKey.Z => VirtualKey.Z, + ImGuiKey.F1 => VirtualKey.F1, + ImGuiKey.F2 => VirtualKey.F2, + ImGuiKey.F3 => VirtualKey.F3, + ImGuiKey.F4 => VirtualKey.F4, + ImGuiKey.F5 => VirtualKey.F5, + ImGuiKey.F6 => VirtualKey.F6, + ImGuiKey.F7 => VirtualKey.F7, + ImGuiKey.F8 => VirtualKey.F8, + ImGuiKey.F9 => VirtualKey.F9, + ImGuiKey.F10 => VirtualKey.F10, + ImGuiKey.F11 => VirtualKey.F11, + ImGuiKey.F12 => VirtualKey.F12, + _ => 0 + }; + + return (int)vk; + } + + private static bool IsGamepadKey(ImGuiKey key) { + return (int) key is >= 617 and <= 640; + } + + private static bool IsModKey(ImGuiKey key) { + return 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; + } + + #endregion + + #region IDisposable Support + + private bool disposedValue = false; // To detect redundant calls + + protected virtual void Dispose(bool disposing) + { + if (!disposedValue) + { + if (disposing) + { + // TODO: dispose managed state (managed objects). + _cursors = null; + } + + // TODO: free unmanaged resources (unmanaged objects) and override a finalizer below. + // TODO: set large fields to null. + + if (_platformNamePtr != IntPtr.Zero) + { + unsafe + { + ImGui.GetIO().NativePtr->BackendPlatformName = null; + } + + Marshal.FreeHGlobal(_platformNamePtr); + _platformNamePtr = IntPtr.Zero; + } + + if (_iniPathPtr != IntPtr.Zero) + { + unsafe + { + ImGui.GetIO().NativePtr->IniFilename = null; + } + + Marshal.FreeHGlobal(_iniPathPtr); + _iniPathPtr = IntPtr.Zero; + } + + ImGui_ImplWin32_ShutdownPlatformInterface(); + + disposedValue = true; + } + } + + ~ImGui_Input_Impl_Direct() + { + // Do not change this code. Put cleanup code in Dispose(bool disposing) above. + Dispose(false); + } + + // This code added to correctly implement the disposable pattern. + public void Dispose() + { + // Do not change this code. Put cleanup code in Dispose(bool disposing) above. + Dispose(true); + GC.SuppressFinalize(this); + } + + #endregion + + // Viewport support + private CreateWindowDelegate createWindow; + private DestroyWindowDelegate destroyWindow; + private ShowWindowDelegate showWindow; + private SetWindowPosDelegate setWindowPos; + private GetWindowPosDelegate getWindowPos; + private SetWindowSizeDelegate setWindowSize; + private GetWindowSizeDelegate getWindowSize; + private SetWindowFocusDelegate setWindowFocus; + private GetWindowFocusDelegate getWindowFocus; + private GetWindowMinimizedDelegate getWindowMinimized; + private SetWindowTitleDelegate setWindowTitle; + private SetWindowAlphaDelegate setWindowAlpha; + private UpdateWindowDelegate updateWindow; + // private SetImeInputPosDelegate setImeInputPos; + // private GetWindowDpiScaleDelegate getWindowDpiScale; + // private ChangedViewportDelegate changedViewport; + + private delegate void CreateWindowDelegate(ImGuiViewportPtr viewport); + + private delegate void DestroyWindowDelegate(ImGuiViewportPtr viewport); + + private delegate void ShowWindowDelegate(ImGuiViewportPtr viewport); + + private delegate void UpdateWindowDelegate(ImGuiViewportPtr viewport); + + private delegate Vector2* GetWindowPosDelegate(IntPtr unk, ImGuiViewportPtr viewport); + + private delegate void SetWindowPosDelegate(ImGuiViewportPtr viewport, Vector2 pos); + + private delegate Vector2* GetWindowSizeDelegate(IntPtr unk, ImGuiViewportPtr viewport); + + private delegate void SetWindowSizeDelegate(ImGuiViewportPtr viewport, Vector2 size); + + private delegate void SetWindowFocusDelegate(ImGuiViewportPtr viewport); + + private delegate bool GetWindowFocusDelegate(ImGuiViewportPtr viewport); + + private delegate byte GetWindowMinimizedDelegate(ImGuiViewportPtr viewport); + + private delegate void SetWindowTitleDelegate(ImGuiViewportPtr viewport, string title); + + private delegate void SetWindowAlphaDelegate(ImGuiViewportPtr viewport, float alpha); + + private delegate void SetImeInputPosDelegate(ImGuiViewportPtr viewport, Vector2 pos); + + private delegate float GetWindowDpiScaleDelegate(ImGuiViewportPtr viewport); + + private delegate void ChangedViewportDelegate(ImGuiViewportPtr viewport); + + // private bool wantUpdateMonitors = false; + + private void ImGui_ImplWin32_UpdateMonitors() + { + // 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 + ImGuiPlatformIOPtr platformIO = ImGui.GetPlatformIO(); + int numMonitors = User32.GetSystemMetrics(User32.SystemMetric.SM_CMONITORS); + IntPtr data = Marshal.AllocHGlobal(Marshal.SizeOf() * numMonitors); + platformIO.NativePtr->Monitors = new ImVector(numMonitors, numMonitors, data); + + // ImGuiPlatformIOPtr platformIO = ImGui.GetPlatformIO(); + // Marshal.FreeHGlobal(platformIO.NativePtr->Monitors.Data); + // int numMonitors = User32.GetSystemMetrics(User32.SystemMetric.SM_CMONITORS); + // IntPtr data = Marshal.AllocHGlobal(Marshal.SizeOf() * numMonitors); + // platformIO.NativePtr->Monitors = new ImVector(numMonitors, numMonitors, data); + + // Store an iterator for the enumeration function + int* iterator = (int*)Marshal.AllocHGlobal(sizeof(int)); + *iterator = 0; + + User32.EnumDisplayMonitors(IntPtr.Zero, IntPtr.Zero, ImGui_ImplWin32_UpdateMonitors_EnumFunc, + new IntPtr(iterator)); + // this.wantUpdateMonitors = false; + } + + private bool ImGui_ImplWin32_UpdateMonitors_EnumFunc(IntPtr nativeMonitor, IntPtr hdc, RECT* LPRECT, + void* LPARAM) + { + // Get and increment iterator + int monitorIndex = *(int*)LPARAM; + *(int*)LPARAM = *(int*)LPARAM + 1; + + User32.MONITORINFO info = new User32.MONITORINFO(); + info.cbSize = Marshal.SizeOf(info); + if (!User32.GetMonitorInfo(nativeMonitor, ref info)) + return true; + + // Give ImGui the info for this display + ImGuiPlatformMonitorPtr imMonitor = ImGui.GetPlatformIO().Monitors[monitorIndex]; + imMonitor.MainPos = new Vector2(info.rcMonitor.left, info.rcMonitor.top); + imMonitor.MainSize = new Vector2(info.rcMonitor.right - info.rcMonitor.left, + info.rcMonitor.bottom - info.rcMonitor.top); + imMonitor.WorkPos = new Vector2(info.rcWork.left, info.rcWork.top); + imMonitor.WorkSize = + new Vector2(info.rcWork.right - info.rcWork.left, info.rcWork.bottom - info.rcWork.top); + imMonitor.DpiScale = 1f; + + return true; + } + + // Helper structure we store in the void* RenderUserData field of each ImGuiViewport to easily retrieve our backend data-> + private struct ImGuiViewportDataWin32 + { + public IntPtr Hwnd; + public bool HwndOwned; + public User32.WindowStyles DwStyle; + public User32.WindowStylesEx DwExStyle; + } + + private void ImGui_ImplWin32_GetWin32StyleFromViewportFlags(ImGuiViewportFlags flags, + ref User32.WindowStyles outStyle, + ref User32.WindowStylesEx outExStyle) + { + if (flags.HasFlag(ImGuiViewportFlags.NoDecoration)) + outStyle = User32.WindowStyles.WS_POPUP; + else + outStyle = User32.WindowStyles.WS_OVERLAPPEDWINDOW; + + if (flags.HasFlag(ImGuiViewportFlags.NoTaskBarIcon)) + outExStyle = User32.WindowStylesEx.WS_EX_TOOLWINDOW; + else + outExStyle = User32.WindowStylesEx.WS_EX_APPWINDOW; + + if (flags.HasFlag(ImGuiViewportFlags.TopMost)) + outExStyle |= User32.WindowStylesEx.WS_EX_TOPMOST; + } + + private void ImGui_ImplWin32_CreateWindow(ImGuiViewportPtr viewport) + { + var data = (ImGuiViewportDataWin32*)Marshal.AllocHGlobal(Marshal.SizeOf()); + viewport.PlatformUserData = (IntPtr)data; + viewport.Flags = + ( + ImGuiViewportFlags.NoTaskBarIcon | + ImGuiViewportFlags.NoFocusOnClick | + ImGuiViewportFlags.NoRendererClear | + ImGuiViewportFlags.NoFocusOnAppearing | + viewport.Flags + ); + ImGui_ImplWin32_GetWin32StyleFromViewportFlags(viewport.Flags, ref data->DwStyle, ref data->DwExStyle); + + IntPtr parentWindow = IntPtr.Zero; + if (viewport.ParentViewportId != 0) + { + ImGuiViewportPtr parentViewport = ImGui.FindViewportByID(viewport.ParentViewportId); + parentWindow = parentViewport.PlatformHandle; + } + + // Create window + var rect = MemUtil.Allocate(); + rect->left = (int)viewport.Pos.X; + rect->top = (int)viewport.Pos.Y; + rect->right = (int)(viewport.Pos.X + viewport.Size.X); + rect->bottom = (int)(viewport.Pos.Y + viewport.Size.Y); + User32.AdjustWindowRectEx(rect, data->DwStyle, false, data->DwExStyle); + + data->Hwnd = User32.CreateWindowEx( + data->DwExStyle, "ImGui Platform", "Untitled", data->DwStyle, + rect->left, rect->top, rect->right - rect->left, rect->bottom - rect->top, + parentWindow, IntPtr.Zero, Kernel32.GetModuleHandle(null), + IntPtr.Zero); + + // User32.GetWindowThreadProcessId(data->Hwnd, out var windowProcessId); + // var currentThreadId = Kernel32.GetCurrentThreadId(); + // var currentProcessId = Kernel32.GetCurrentProcessId(); + + // Allow transparent windows + // TODO: Eventually... + ImGui_ImplWin32_EnableAlphaCompositing(data->Hwnd); + + data->HwndOwned = true; + viewport.PlatformRequestResize = false; + viewport.PlatformHandle = viewport.PlatformHandleRaw = data->Hwnd; + Marshal.FreeHGlobal((IntPtr)rect); + } + + private void ImGui_ImplWin32_DestroyWindow(ImGuiViewportPtr viewport) + { + // This is also called on the main viewport for some reason, and we never set that viewport's PlatformUserData + if (viewport.PlatformUserData == IntPtr.Zero) return; + + var data = (ImGuiViewportDataWin32*)viewport.PlatformUserData; + + if (Win32.GetCapture() == data->Hwnd) + { + // Transfer capture so if we started dragging from a window that later disappears, we'll still receive the MOUSEUP event. + User32.ReleaseCapture(); + User32.SetCapture(_hWnd); + } + + if (data->Hwnd != IntPtr.Zero && data->HwndOwned) + { + var result = User32.DestroyWindow(data->Hwnd); + + if (result == false && Kernel32.GetLastError() == Win32ErrorCode.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 + User32.PostMessage(data->Hwnd, User32.WindowMessage.WM_CLOSE, IntPtr.Zero, IntPtr.Zero); + } + } + + data->Hwnd = IntPtr.Zero; + Marshal.FreeHGlobal(viewport.PlatformUserData); + viewport.PlatformUserData = viewport.PlatformHandle = IntPtr.Zero; + } + + private void ImGui_ImplWin32_ShowWindow(ImGuiViewportPtr viewport) + { + var data = (ImGuiViewportDataWin32*)viewport.PlatformUserData; + + if (viewport.Flags.HasFlag(ImGuiViewportFlags.NoFocusOnAppearing)) + User32.ShowWindow(data->Hwnd, User32.WindowShowStyle.SW_SHOWNA); + else + User32.ShowWindow(data->Hwnd, User32.WindowShowStyle.SW_SHOW); + } + + private void ImGui_ImplWin32_UpdateWindow(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.NoRendererClear | + ImGuiViewportFlags.NoFocusOnAppearing | + viewport.Flags + ); + User32.WindowStyles newStyle = 0; + User32.WindowStylesEx newExStyle = 0; + ImGui_ImplWin32_GetWin32StyleFromViewportFlags(viewport.Flags, ref newStyle, ref 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 + bool topMostChanged = (data->DwExStyle & User32.WindowStylesEx.WS_EX_TOPMOST) != + (newExStyle & User32.WindowStylesEx.WS_EX_TOPMOST); + + IntPtr insertAfter = IntPtr.Zero; + if (topMostChanged) + { + if (viewport.Flags.HasFlag(ImGuiViewportFlags.TopMost)) + insertAfter = User32.SpecialWindowHandles.HWND_TOPMOST; + else + insertAfter = User32.SpecialWindowHandles.HWND_NOTOPMOST; + } + + User32.SetWindowPosFlags swpFlag = topMostChanged ? 0 : User32.SetWindowPosFlags.SWP_NOZORDER; + + // Apply flags and position (since it is affected by flags) + data->DwStyle = newStyle; + data->DwExStyle = newExStyle; + + User32.SetWindowLong(data->Hwnd, User32.WindowLongIndexFlags.GWL_STYLE, + (User32.SetWindowLongFlags)data->DwStyle); + User32.SetWindowLong(data->Hwnd, User32.WindowLongIndexFlags.GWL_EXSTYLE, + (User32.SetWindowLongFlags)data->DwExStyle); + + // Create window + var rect = MemUtil.Allocate(); + rect->left = (int)viewport.Pos.X; + rect->top = (int)viewport.Pos.Y; + rect->right = (int)(viewport.Pos.X + viewport.Size.X); + rect->bottom = (int)(viewport.Pos.Y + viewport.Size.Y); + User32.AdjustWindowRectEx(rect, data->DwStyle, false, data->DwExStyle); + User32.SetWindowPos(data->Hwnd, insertAfter, + rect->left, rect->top, rect->right - rect->left, rect->bottom - rect->top, + swpFlag | + User32.SetWindowPosFlags.SWP_NOACTIVATE | + User32.SetWindowPosFlags.SWP_FRAMECHANGED); + + // This is necessary when we alter the style + User32.ShowWindow(data->Hwnd, User32.WindowShowStyle.SW_SHOWNA); + viewport.PlatformRequestMove = viewport.PlatformRequestResize = true; + Marshal.FreeHGlobal((IntPtr)rect); + } + } + + private Vector2* ImGui_ImplWin32_GetWindowPos(IntPtr unk, ImGuiViewportPtr viewport) + { + var data = (ImGuiViewportDataWin32*)viewport.PlatformUserData; + var vec2 = MemUtil.Allocate(); + + POINT pt = new POINT { x = 0, y = 0 }; + User32.ClientToScreen(data->Hwnd, ref pt); + vec2->X = pt.x; + vec2->Y = pt.y; + + return vec2; + } + + private void ImGui_ImplWin32_SetWindowPos(ImGuiViewportPtr viewport, Vector2 pos) + { + var data = (ImGuiViewportDataWin32*)viewport.PlatformUserData; + + var rect = MemUtil.Allocate(); + rect->left = (int)pos.X; + rect->top = (int)pos.Y; + rect->right = (int)pos.X; + rect->bottom = (int)pos.Y; + + User32.AdjustWindowRectEx(rect, data->DwStyle, false, data->DwExStyle); + User32.SetWindowPos(data->Hwnd, IntPtr.Zero, + rect->left, rect->top, 0, 0, + User32.SetWindowPosFlags.SWP_NOZORDER | + User32.SetWindowPosFlags.SWP_NOSIZE | + User32.SetWindowPosFlags.SWP_NOACTIVATE); + Marshal.FreeHGlobal((IntPtr)rect); + } + + private Vector2* ImGui_ImplWin32_GetWindowSize(IntPtr size, ImGuiViewportPtr viewport) + { + var data = (ImGuiViewportDataWin32*)viewport.PlatformUserData; + var vec2 = MemUtil.Allocate(); + + User32.GetClientRect(data->Hwnd, out var rect); + vec2->X = rect.right - rect.left; + vec2->Y = rect.bottom - rect.top; + + return vec2; + } + + private void ImGui_ImplWin32_SetWindowSize(ImGuiViewportPtr viewport, Vector2 size) + { + var data = (ImGuiViewportDataWin32*)viewport.PlatformUserData; + + var rect = MemUtil.Allocate(); + rect->left = 0; + rect->top = 0; + rect->right = (int)size.X; + rect->bottom = (int)size.Y; + + User32.AdjustWindowRectEx(rect, data->DwStyle, false, data->DwExStyle); + User32.SetWindowPos(data->Hwnd, IntPtr.Zero, + 0, 0, rect->right - rect->left, rect->bottom - rect->top, + User32.SetWindowPosFlags.SWP_NOZORDER | + User32.SetWindowPosFlags.SWP_NOMOVE | + User32.SetWindowPosFlags.SWP_NOACTIVATE); + Marshal.FreeHGlobal((IntPtr) rect); + } + + private void ImGui_ImplWin32_SetWindowFocus(ImGuiViewportPtr viewport) + { + var data = (ImGuiViewportDataWin32*)viewport.PlatformUserData; + + Win32.BringWindowToTop(data->Hwnd); + User32.SetForegroundWindow(data->Hwnd); + Win32.SetFocus(data->Hwnd); + } + + private bool ImGui_ImplWin32_GetWindowFocus(ImGuiViewportPtr viewport) + { + var data = (ImGuiViewportDataWin32*)viewport.PlatformUserData; + return User32.GetForegroundWindow() == data->Hwnd; + } + + private byte ImGui_ImplWin32_GetWindowMinimized(ImGuiViewportPtr viewport) + { + var data = (ImGuiViewportDataWin32*)viewport.PlatformUserData; + return (byte)(User32.IsIconic(data->Hwnd) ? 1 : 0); + } + + private void ImGui_ImplWin32_SetWindowTitle(ImGuiViewportPtr viewport, string title) + { + var data = (ImGuiViewportDataWin32*)viewport.PlatformUserData; + User32.SetWindowText(data->Hwnd, title); + } + + private void ImGui_ImplWin32_SetWindowAlpha(ImGuiViewportPtr viewport, float alpha) + { + var data = (ImGuiViewportDataWin32*)viewport.PlatformUserData; + + if (alpha < 1.0f) + { + User32.WindowStylesEx gwl = + (User32.WindowStylesEx)User32.GetWindowLong(data->Hwnd, User32.WindowLongIndexFlags.GWL_EXSTYLE); + User32.WindowStylesEx style = gwl | User32.WindowStylesEx.WS_EX_LAYERED; + User32.SetWindowLong(data->Hwnd, User32.WindowLongIndexFlags.GWL_EXSTYLE, + (User32.SetWindowLongFlags)style); + Win32.SetLayeredWindowAttributes(data->Hwnd, 0, (byte)(255 * alpha), 0x2); //0x2 = LWA_ALPHA + } + else + { + User32.WindowStylesEx gwl = + (User32.WindowStylesEx)User32.GetWindowLong(data->Hwnd, User32.WindowLongIndexFlags.GWL_EXSTYLE); + User32.WindowStylesEx style = gwl & ~User32.WindowStylesEx.WS_EX_LAYERED; + User32.SetWindowLong(data->Hwnd, User32.WindowLongIndexFlags.GWL_EXSTYLE, + (User32.SetWindowLongFlags)style); + } + } + + // TODO: Decode why IME is miserable + // private void ImGui_ImplWin32_SetImeInputPos(ImGuiViewportPtr viewport, Vector2 pos) { + // Win32.COMPOSITIONFORM cs = new Win32.COMPOSITIONFORM( + // 0x20, + // new Win32.POINT( + // (int) (pos.X - viewport.Pos.X), + // (int) (pos.Y - viewport.Pos.Y)), + // new Win32.RECT(0, 0, 0, 0) + // ); + // var hwnd = viewport.PlatformHandle; + // if (hwnd != IntPtr.Zero) { + // var himc = Win32.ImmGetContext(hwnd); + // if (himc != IntPtr.Zero) { + // Win32.ImmSetCompositionWindow(himc, ref cs); + // Win32.ImmReleaseContext(hwnd, himc); + // } + // } + // } + + // TODO Alpha when it's no longer forced + private void ImGui_ImplWin32_EnableAlphaCompositing(IntPtr hwnd) + { + Win32.DwmIsCompositionEnabled(out bool composition); + + if (!composition) return; + + if (DwmApi.DwmGetColorizationColor(out uint color, out bool opaque) == HResult.Code.S_OK && !opaque) + { + DwmApi.DWM_BLURBEHIND bb = new DwmApi.DWM_BLURBEHIND(); + bb.Enable = true; + bb.dwFlags = DwmApi.DWM_BLURBEHINDFlags.DWM_BB_ENABLE; + bb.hRgnBlur = IntPtr.Zero; + DwmApi.DwmEnableBlurBehindWindow(hwnd, bb); + } + } + + private void ImGui_ImplWin32_InitPlatformInterface() + { + _classNamePtr = Marshal.StringToHGlobalUni("ImGui Platform"); + + User32.WNDCLASSEX wcex = new User32.WNDCLASSEX(); + wcex.cbSize = Marshal.SizeOf(wcex); + wcex.style = User32.ClassStyles.CS_HREDRAW | User32.ClassStyles.CS_VREDRAW; + wcex.cbClsExtra = 0; + wcex.cbWndExtra = 0; + wcex.hInstance = Kernel32.GetModuleHandle(null); + wcex.hIcon = IntPtr.Zero; + wcex.hCursor = IntPtr.Zero; + wcex.hbrBackground = new IntPtr(2); // COLOR_BACKGROUND is 1, so... + wcex.lpfnWndProc = _wndProcDelegate; + unsafe + { + wcex.lpszMenuName = null; + wcex.lpszClassName = (char*)_classNamePtr; + } + + wcex.hIconSm = IntPtr.Zero; + User32.RegisterClassEx(ref wcex); + + ImGui_ImplWin32_UpdateMonitors(); + + this.createWindow = ImGui_ImplWin32_CreateWindow; + this.destroyWindow = ImGui_ImplWin32_DestroyWindow; + this.showWindow = ImGui_ImplWin32_ShowWindow; + this.setWindowPos = ImGui_ImplWin32_SetWindowPos; + this.getWindowPos = ImGui_ImplWin32_GetWindowPos; + this.setWindowSize = ImGui_ImplWin32_SetWindowSize; + this.getWindowSize = ImGui_ImplWin32_GetWindowSize; + this.setWindowFocus = ImGui_ImplWin32_SetWindowFocus; + this.getWindowFocus = ImGui_ImplWin32_GetWindowFocus; + this.getWindowMinimized = ImGui_ImplWin32_GetWindowMinimized; + this.setWindowTitle = ImGui_ImplWin32_SetWindowTitle; + this.setWindowAlpha = ImGui_ImplWin32_SetWindowAlpha; + this.updateWindow = ImGui_ImplWin32_UpdateWindow; + // this.setImeInputPos = ImGui_ImplWin32_SetImeInputPos; + + // Register platform interface (will be coupled with a renderer interface) + ImGuiPlatformIOPtr io = ImGui.GetPlatformIO(); + io.Platform_CreateWindow = Marshal.GetFunctionPointerForDelegate(this.createWindow); + io.Platform_DestroyWindow = Marshal.GetFunctionPointerForDelegate(this.destroyWindow); + io.Platform_ShowWindow = Marshal.GetFunctionPointerForDelegate(this.showWindow); + io.Platform_SetWindowPos = Marshal.GetFunctionPointerForDelegate(this.setWindowPos); + io.Platform_GetWindowPos = Marshal.GetFunctionPointerForDelegate(this.getWindowPos); + io.Platform_SetWindowSize = Marshal.GetFunctionPointerForDelegate(this.setWindowSize); + io.Platform_GetWindowSize = Marshal.GetFunctionPointerForDelegate(this.getWindowSize); + io.Platform_SetWindowFocus = Marshal.GetFunctionPointerForDelegate(this.setWindowFocus); + io.Platform_GetWindowFocus = Marshal.GetFunctionPointerForDelegate(this.getWindowFocus); + io.Platform_GetWindowMinimized = Marshal.GetFunctionPointerForDelegate(this.getWindowMinimized); + io.Platform_SetWindowTitle = Marshal.GetFunctionPointerForDelegate(this.setWindowTitle); + io.Platform_SetWindowAlpha = Marshal.GetFunctionPointerForDelegate(this.setWindowAlpha); + io.Platform_UpdateWindow = Marshal.GetFunctionPointerForDelegate(this.updateWindow); + // io.Platform_SetImeInputPos = Marshal.GetFunctionPointerForDelegate(this.setImeInputPos); + + // 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. + ImGuiViewportPtr mainViewport = ImGui.GetMainViewport(); + + var data = (ImGuiViewportDataWin32*)Marshal.AllocHGlobal(Marshal.SizeOf()); + mainViewport.PlatformUserData = (IntPtr)data; + data->Hwnd = _hWnd; + data->HwndOwned = false; + mainViewport.PlatformHandle = _hWnd; + } + + private void ImGui_ImplWin32_ShutdownPlatformInterface() + { + Marshal.FreeHGlobal(_classNamePtr); + + // We allocated the platform monitor data in ImGui_ImplWin32_UpdateMonitors 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 = new ImVector(0, 0, IntPtr.Zero); + + User32.UnregisterClass("ImGui Platform", Kernel32.GetModuleHandle(null)); + } + } +} diff --git a/Dalamud/Interface/ImGuiScene/ImGui_Impl/Renderers/IImGuiRenderer.cs b/Dalamud/Interface/ImGuiScene/ImGui_Impl/Renderers/IImGuiRenderer.cs new file mode 100644 index 000000000..72036b7bc --- /dev/null +++ b/Dalamud/Interface/ImGuiScene/ImGui_Impl/Renderers/IImGuiRenderer.cs @@ -0,0 +1,14 @@ +namespace ImGuiScene +{ + /// + /// A simple shared public interface that all ImGui render implementations follow. + /// + public interface IImGuiRenderer + { + // FIXME - probably a better way to do this than params object[] ! + void Init(params object[] initParams); + void Shutdown(); + void NewFrame(); + void RenderDrawData(ImGuiNET.ImDrawDataPtr drawData); + } +} diff --git a/Dalamud/Interface/ImGuiScene/ImGui_Impl/Renderers/ImGui_Impl_DX11.cs b/Dalamud/Interface/ImGuiScene/ImGui_Impl/Renderers/ImGui_Impl_DX11.cs new file mode 100644 index 000000000..cd63c27e1 --- /dev/null +++ b/Dalamud/Interface/ImGuiScene/ImGui_Impl/Renderers/ImGui_Impl_DX11.cs @@ -0,0 +1,859 @@ +using ImGuiNET; +using SharpDX; +using SharpDX.Direct3D; +using SharpDX.Direct3D11; +using SharpDX.DXGI; +using SharpDX.Mathematics.Interop; + +using System.Collections.Generic; +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; +using Buffer = SharpDX.Direct3D11.Buffer; +using Device = SharpDX.Direct3D11.Device; +using MapFlags = SharpDX.Direct3D11.MapFlags; + +namespace ImGuiScene +{ + /// + /// Currently undocumented because it is a horrible mess. + /// A near-direct port of https://github.com/ocornut/imgui/blob/master/examples/imgui_impl_dx11.cpp + /// State backup follows the general layout of imgui's sample (which is a mess), but has been rather + /// expanded to cover the vast majority of render state, following the example here + /// https://github.com/GPUOpen-LibrariesAndSDKs/CrossfireAPI11/blob/master/amd_lib/src/AMD_SaveRestoreState.cpp + /// Would be nice to organize it better, but it seems to work + /// + public unsafe class ImGui_Impl_DX11 : IImGuiRenderer + { + private IntPtr _renderNamePtr; + private Device _device; + private DeviceContext _deviceContext; + private List _fontResourceViews = new(); + private SamplerState _fontSampler; + private VertexShader _vertexShader; + private PixelShader _pixelShader; + private InputLayout _inputLayout; + private Buffer _vertexConstantBuffer; + private BlendState _blendState; + private RasterizerState _rasterizerState; + private DepthStencilState _depthStencilState; + private Buffer _vertexBuffer; + private Buffer _indexBuffer; + private int _vertexBufferSize; + private int _indexBufferSize; + private VertexBufferBinding _vertexBinding; + // so we don't make a temporary object every frame + private RawColor4 _blendColor = new RawColor4(0, 0, 0, 0); + + // TODO: I'll clean this up better later + private class StateBackup : IDisposable + { + private DeviceContext deviceContext; + + // IA + public InputLayout InputLayout; + public PrimitiveTopology PrimitiveTopology; + public Buffer IndexBuffer; + public Format IndexBufferFormat; + public int IndexBufferOffset; + public Buffer[] VertexBuffers; + public int[] VertexBufferStrides; + public int[] VertexBufferOffsets; + + // RS + public RasterizerState RS; + public Rectangle[] ScissorRects; + public RawViewportF[] Viewports; + + // OM + public BlendState BlendState; + public RawColor4 BlendFactor; + public int SampleMask; + public DepthStencilState DepthStencilState; + public int DepthStencilRef; + public DepthStencilView DepthStencilView; + public RenderTargetView[] RenderTargetViews; + + // VS + public VertexShader VS; + public Buffer[] VSConstantBuffers; + public SamplerState[] VSSamplers; + public ShaderResourceView[] VSResourceViews; + + // HS + public HullShader HS; + public Buffer[] HSConstantBuffers; + public SamplerState[] HSSamplers; + public ShaderResourceView[] HSResourceViews; + + // DS + public DomainShader DS; + public Buffer[] DSConstantBuffers; + public SamplerState[] DSSamplers; + public ShaderResourceView[] DSResourceViews; + + // GS + public GeometryShader GS; + public Buffer[] GSConstantBuffers; + public SamplerState[] GSSamplers; + public ShaderResourceView[] GSResourceViews; + + // PS + public PixelShader PS; + public Buffer[] PSConstantBuffers; + public SamplerState[] PSSamplers; + public ShaderResourceView[] PSResourceViews; + + public ComputeShader CS; + public Buffer[] CSConstantBuffers; + public SamplerState[] CSSamplers; + public ShaderResourceView[] CSResourceViews; + public UnorderedAccessView[] CSUAVs; + + private bool disposedValue = false; // To detect redundant calls + + public StateBackup(DeviceContext deviceContext) + { + this.deviceContext = deviceContext; + + this.ScissorRects = new Rectangle[16]; // I couldn't find D3D11_VIEWPORT_AND_SCISSORRECT_OBJECT_COUNT_PER_PIPELINE as a SharpDX enum + this.Viewports = new RawViewportF[16]; + this.VertexBuffers = new Buffer[InputAssemblerStage.VertexInputResourceSlotCount]; + this.VertexBufferStrides = new int[InputAssemblerStage.VertexInputResourceSlotCount]; + this.VertexBufferOffsets = new int[InputAssemblerStage.VertexInputResourceSlotCount]; + + // IA + this.InputLayout = deviceContext.InputAssembler.InputLayout; + this.deviceContext.InputAssembler.GetIndexBuffer(out this.IndexBuffer, out this.IndexBufferFormat, out this.IndexBufferOffset); + this.PrimitiveTopology = this.deviceContext.InputAssembler.PrimitiveTopology; + this.deviceContext.InputAssembler.GetVertexBuffers(0, InputAssemblerStage.VertexInputResourceSlotCount, this.VertexBuffers, this.VertexBufferStrides, this.VertexBufferOffsets); + + // RS + this.RS = this.deviceContext.Rasterizer.State; + this.deviceContext.Rasterizer.GetScissorRectangles(this.ScissorRects); + this.deviceContext.Rasterizer.GetViewports(this.Viewports); + + // OM + this.BlendState = this.deviceContext.OutputMerger.GetBlendState(out this.BlendFactor, out this.SampleMask); + this.DepthStencilState = this.deviceContext.OutputMerger.GetDepthStencilState(out this.DepthStencilRef); + this.RenderTargetViews = this.deviceContext.OutputMerger.GetRenderTargets(OutputMergerStage.SimultaneousRenderTargetCount, out this.DepthStencilView); + + // VS + this.VS = this.deviceContext.VertexShader.Get(); + this.VSSamplers = this.deviceContext.VertexShader.GetSamplers(0, CommonShaderStage.SamplerSlotCount); + this.VSConstantBuffers = this.deviceContext.VertexShader.GetConstantBuffers(0, CommonShaderStage.ConstantBufferApiSlotCount); + this.VSResourceViews = this.deviceContext.VertexShader.GetShaderResources(0, CommonShaderStage.InputResourceSlotCount); + + // HS + this.HS = this.deviceContext.HullShader.Get(); + this.HSSamplers = this.deviceContext.HullShader.GetSamplers(0, CommonShaderStage.SamplerSlotCount); + this.HSConstantBuffers = this.deviceContext.HullShader.GetConstantBuffers(0, CommonShaderStage.ConstantBufferApiSlotCount); + this.HSResourceViews = this.deviceContext.HullShader.GetShaderResources(0, CommonShaderStage.InputResourceSlotCount); + + // DS + this.DS = this.deviceContext.DomainShader.Get(); + this.DSSamplers = this.deviceContext.DomainShader.GetSamplers(0, CommonShaderStage.SamplerSlotCount); + this.DSConstantBuffers = this.deviceContext.DomainShader.GetConstantBuffers(0, CommonShaderStage.ConstantBufferApiSlotCount); + this.DSResourceViews = this.deviceContext.DomainShader.GetShaderResources(0, CommonShaderStage.InputResourceSlotCount); + + // GS + this.GS = this.deviceContext.GeometryShader.Get(); + this.GSSamplers = this.deviceContext.GeometryShader.GetSamplers(0, CommonShaderStage.SamplerSlotCount); + this.GSConstantBuffers = this.deviceContext.GeometryShader.GetConstantBuffers(0, CommonShaderStage.ConstantBufferApiSlotCount); + this.GSResourceViews = this.deviceContext.GeometryShader.GetShaderResources(0, CommonShaderStage.InputResourceSlotCount); + + // PS + this.PS = this.deviceContext.PixelShader.Get(); + this.PSSamplers = this.deviceContext.PixelShader.GetSamplers(0, CommonShaderStage.SamplerSlotCount); + this.PSConstantBuffers = this.deviceContext.PixelShader.GetConstantBuffers(0, CommonShaderStage.ConstantBufferApiSlotCount); + this.PSResourceViews = this.deviceContext.PixelShader.GetShaderResources(0, CommonShaderStage.InputResourceSlotCount); + + // CS + this.CS = this.deviceContext.ComputeShader.Get(); + this.CSSamplers = this.deviceContext.ComputeShader.GetSamplers(0, CommonShaderStage.SamplerSlotCount); + this.CSConstantBuffers = this.deviceContext.ComputeShader.GetConstantBuffers(0, CommonShaderStage.ConstantBufferApiSlotCount); + this.CSResourceViews = this.deviceContext.ComputeShader.GetShaderResources(0, CommonShaderStage.InputResourceSlotCount); + this.CSUAVs = this.deviceContext.ComputeShader.GetUnorderedAccessViews(0, ComputeShaderStage.UnorderedAccessViewSlotCount); // should be register count and not slot, but the value is correct + } + + protected virtual void Dispose(bool disposing) + { + if (!disposedValue) + { + if (disposing) + { + // TODO: dispose managed state (managed objects). + } + + // TODO: free unmanaged resources (unmanaged objects) and override a finalizer below. + // TODO: set large fields to null. + + // IA + this.deviceContext.InputAssembler.InputLayout = this.InputLayout; + this.deviceContext.InputAssembler.SetIndexBuffer(this.IndexBuffer, this.IndexBufferFormat, this.IndexBufferOffset); + this.deviceContext.InputAssembler.PrimitiveTopology = this.PrimitiveTopology; + this.deviceContext.InputAssembler.SetVertexBuffers(0, this.VertexBuffers, this.VertexBufferStrides, this.VertexBufferOffsets); + + // RS + this.deviceContext.Rasterizer.State = this.RS; + this.deviceContext.Rasterizer.SetScissorRectangles(this.ScissorRects); + this.deviceContext.Rasterizer.SetViewports(this.Viewports, this.Viewports.Length); + + // OM + this.deviceContext.OutputMerger.SetBlendState(this.BlendState, this.BlendFactor, this.SampleMask); + this.deviceContext.OutputMerger.SetDepthStencilState(this.DepthStencilState, this.DepthStencilRef); + this.deviceContext.OutputMerger.SetRenderTargets(this.DepthStencilView, this.RenderTargetViews); + + // VS + this.deviceContext.VertexShader.Set(this.VS); + this.deviceContext.VertexShader.SetSamplers(0, this.VSSamplers); + this.deviceContext.VertexShader.SetConstantBuffers(0, this.VSConstantBuffers); + this.deviceContext.VertexShader.SetShaderResources(0, this.VSResourceViews); + + // HS + this.deviceContext.HullShader.Set(this.HS); + this.deviceContext.HullShader.SetSamplers(0, this.HSSamplers); + this.deviceContext.HullShader.SetConstantBuffers(0, this.HSConstantBuffers); + this.deviceContext.HullShader.SetShaderResources(0, this.HSResourceViews); + + // DS + this.deviceContext.DomainShader.Set(this.DS); + this.deviceContext.DomainShader.SetSamplers(0, this.DSSamplers); + this.deviceContext.DomainShader.SetConstantBuffers(0, this.DSConstantBuffers); + this.deviceContext.DomainShader.SetShaderResources(0, this.DSResourceViews); + + // GS + this.deviceContext.GeometryShader.Set(this.GS); + this.deviceContext.GeometryShader.SetSamplers(0, this.GSSamplers); + this.deviceContext.GeometryShader.SetConstantBuffers(0, this.GSConstantBuffers); + this.deviceContext.GeometryShader.SetShaderResources(0, this.GSResourceViews); + + // PS + this.deviceContext.PixelShader.Set(this.PS); + this.deviceContext.PixelShader.SetSamplers(0, this.PSSamplers); + this.deviceContext.PixelShader.SetConstantBuffers(0, this.PSConstantBuffers); + this.deviceContext.PixelShader.SetShaderResources(0, this.PSResourceViews); + + // CS + this.deviceContext.ComputeShader.Set(this.CS); + this.deviceContext.ComputeShader.SetSamplers(0, this.CSSamplers); + this.deviceContext.ComputeShader.SetConstantBuffers(0, this.CSConstantBuffers); + this.deviceContext.ComputeShader.SetShaderResources(0, this.CSResourceViews); + this.deviceContext.ComputeShader.SetUnorderedAccessViews(0, this.CSUAVs); + + // force free these references immediately, or they hang around too long and calls + // to swapchain->ResizeBuffers() will fail due to outstanding references + // We could force free other things too, but nothing else should cause errors + // and these should get gc'd and disposed eventually + foreach (var rtv in this.RenderTargetViews) + { + rtv?.Dispose(); + } + + this.RenderTargetViews = null; + this.DepthStencilView = null; + this.VSResourceViews = null; + this.HSResourceViews = null; + this.DSResourceViews = null; + this.GSResourceViews = null; + this.PSResourceViews = null; + this.CSResourceViews = null; + + disposedValue = true; + } + } + + ~StateBackup() + { + // Do not change this code. Put cleanup code in Dispose(bool disposing) above. + Dispose(false); + } + + // This code added to correctly implement the disposable pattern. + public void Dispose() + { + // Do not change this code. Put cleanup code in Dispose(bool disposing) above. + Dispose(true); + GC.SuppressFinalize(this); + } + } + + public void SetupRenderState(ImDrawDataPtr drawData) + { + // Setup viewport + _deviceContext.Rasterizer.SetViewport(0, 0, drawData.DisplaySize.X, drawData.DisplaySize.Y); + + // Setup shader and vertex buffers + _deviceContext.InputAssembler.InputLayout = _inputLayout; + _vertexBinding.Buffer = _vertexBuffer; + _deviceContext.InputAssembler.SetVertexBuffers(0, _vertexBinding); + _deviceContext.InputAssembler.SetIndexBuffer(_indexBuffer, Format.R16_UInt, 0); + _deviceContext.InputAssembler.PrimitiveTopology = PrimitiveTopology.TriangleList; + _deviceContext.VertexShader.Set(_vertexShader); + _deviceContext.VertexShader.SetConstantBuffer(0, _vertexConstantBuffer); + _deviceContext.PixelShader.Set(_pixelShader); + _deviceContext.PixelShader.SetSampler(0, _fontSampler); + _deviceContext.GeometryShader.Set(null); + _deviceContext.HullShader.Set(null); + _deviceContext.DomainShader.Set(null); + _deviceContext.ComputeShader.Set(null); + + // Setup blend state + _deviceContext.OutputMerger.BlendState = _blendState; + _deviceContext.OutputMerger.BlendFactor = _blendColor; + _deviceContext.OutputMerger.DepthStencilState = _depthStencilState; + _deviceContext.Rasterizer.State = _rasterizerState; + } + + public void RenderDrawData(ImDrawDataPtr drawData) + { + // Avoid rendering when minimized + if (drawData.DisplaySize.X <= 0 || drawData.DisplaySize.Y <= 0) + { + return; + } + + if (!drawData.Valid || drawData.CmdListsCount == 0) + { + return; + } + + // drawData.ScaleClipRects(ImGui.GetIO().DisplayFramebufferScale); + + // Create and grow vertex/index buffers if needed + if (_vertexBuffer == null || _vertexBufferSize < drawData.TotalVtxCount) + { + _vertexBuffer?.Dispose(); + _vertexBufferSize = drawData.TotalVtxCount + 5000; + + _vertexBuffer = new Buffer(_device, new BufferDescription + { + Usage = ResourceUsage.Dynamic, + SizeInBytes = Unsafe.SizeOf() * _vertexBufferSize, + BindFlags = BindFlags.VertexBuffer, + CpuAccessFlags = CpuAccessFlags.Write, + OptionFlags = ResourceOptionFlags.None + }); + + // (Re)make this here rather than every frame + _vertexBinding = new VertexBufferBinding + { + Buffer = _vertexBuffer, + Stride = Unsafe.SizeOf(), + Offset = 0 + }; + } + + if (_indexBuffer == null || _indexBufferSize < drawData.TotalIdxCount) + { + _indexBuffer?.Dispose(); + _indexBufferSize = drawData.TotalIdxCount + 10000; + + _indexBuffer = new Buffer(_device, new BufferDescription + { + Usage = ResourceUsage.Dynamic, + SizeInBytes = sizeof(ushort) * _indexBufferSize, // ImGui.NET doesn't provide an ImDrawIdx, but their sample uses ushort + BindFlags = BindFlags.IndexBuffer, + CpuAccessFlags = CpuAccessFlags.Write + }); + } + + // Upload vertex/index data into a single contiguous GPU buffer + int vertexOffset = 0, indexOffset = 0; + var vertexData = _deviceContext.MapSubresource(_vertexBuffer, 0, MapMode.WriteDiscard, MapFlags.None).DataPointer; + var indexData = _deviceContext.MapSubresource(_indexBuffer, 0, MapMode.WriteDiscard, MapFlags.None).DataPointer; + + for (int n = 0; n < drawData.CmdListsCount; n++) + { + var cmdList = drawData.CmdListsRange[n]; + unsafe + { + System.Buffer.MemoryCopy(cmdList.VtxBuffer.Data.ToPointer(), + (ImDrawVert*)vertexData + vertexOffset, + Unsafe.SizeOf() * _vertexBufferSize, + Unsafe.SizeOf() * cmdList.VtxBuffer.Size); + + System.Buffer.MemoryCopy(cmdList.IdxBuffer.Data.ToPointer(), + (ushort*)indexData + indexOffset, + sizeof(ushort) * _indexBufferSize, + sizeof(ushort) * cmdList.IdxBuffer.Size); + + vertexOffset += cmdList.VtxBuffer.Size; + indexOffset += cmdList.IdxBuffer.Size; + } + } + _deviceContext.UnmapSubresource(_vertexBuffer, 0); + _deviceContext.UnmapSubresource(_indexBuffer, 0); + + // Setup orthographic projection matrix into our constant buffer + // Our visible imgui space lies from drawData.DisplayPos (top left) to drawData.DisplayPos+drawData.DisplaySize (bottom right). DisplayPos is (0,0) for single viewport apps. + var L = drawData.DisplayPos.X; + var R = drawData.DisplayPos.X + drawData.DisplaySize.X; + var T = drawData.DisplayPos.Y; + var B = drawData.DisplayPos.Y + drawData.DisplaySize.Y; + var mvp = new float[] + { + 2f/(R-L), 0, 0, 0, + 0, 2f/(T-B), 0, 0, + 0, 0, 0.5f, 0, + (R+L)/(L-R), (T+B)/(B-T), 0.5f, 1f + }; + + var constantBuffer = _deviceContext.MapSubresource(_vertexConstantBuffer, 0, MapMode.WriteDiscard, MapFlags.None).DataPointer; + unsafe + { + fixed (void* mvpPtr = mvp) + { + System.Buffer.MemoryCopy(mvpPtr, constantBuffer.ToPointer(), 16 * sizeof(float), 16 * sizeof(float)); + } + } + _deviceContext.UnmapSubresource(_vertexConstantBuffer, 0); + + var oldState = new StateBackup(_deviceContext); + + // Setup desired DX state + SetupRenderState(drawData); + + // Render command lists + // (Because we merged all buffers into a single one, we maintain our own offset into them) + vertexOffset = 0; + indexOffset = 0; + var clipOff = drawData.DisplayPos; + for (int n = 0; n < drawData.CmdListsCount; n++) + { + var cmdList = drawData.CmdListsRange[n]; + for (int cmd = 0; cmd < cmdList.CmdBuffer.Size; cmd++) + { + var pcmd = cmdList.CmdBuffer[cmd]; + if (pcmd.UserCallback != IntPtr.Zero) + { + // TODO + throw new NotImplementedException(); + } + else + { + // Apply scissor/clipping rectangle + _deviceContext.Rasterizer.SetScissorRectangle((int)(pcmd.ClipRect.X - clipOff.X), (int)(pcmd.ClipRect.Y - clipOff.Y), (int)(pcmd.ClipRect.Z - clipOff.X), (int)(pcmd.ClipRect.W - clipOff.Y)); + + // Bind texture, Draw + // TODO: might be nice to store samplers for loaded textures so that we can look them up and apply them here + // rather than just always using the font sampler + var textureSrv = ShaderResourceView.FromPointer(pcmd.TextureId); + _deviceContext.PixelShader.SetShaderResource(0, textureSrv); + _deviceContext.DrawIndexed((int)pcmd.ElemCount, (int)(pcmd.IdxOffset + indexOffset), (int)(pcmd.VtxOffset + vertexOffset)); + } + } + + indexOffset += cmdList.IdxBuffer.Size; + vertexOffset += cmdList.VtxBuffer.Size; + } + + oldState.Dispose(); // restores the previous state + oldState = null; + } + + public void CreateFontsTexture() + { + 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 IntPtr fontPixels, out int fontWidth, out int fontHeight, + out int fontBytesPerPixel); + + // Upload texture to graphics system + var texDesc = new Texture2DDescription { + Width = fontWidth, + Height = fontHeight, + MipLevels = 1, + ArraySize = 1, + Format = Format.R8G8B8A8_UNorm, + SampleDescription = new SampleDescription(1, 0), + Usage = ResourceUsage.Immutable, + BindFlags = BindFlags.ShaderResource, + CpuAccessFlags = CpuAccessFlags.None, + OptionFlags = ResourceOptionFlags.None + }; + + using var fontTexture = new Texture2D( + _device, texDesc, new DataRectangle(fontPixels, fontWidth * fontBytesPerPixel)); + + // Create texture view + var fontResourceView = new ShaderResourceView(_device, fontTexture, new ShaderResourceViewDescription { + Format = texDesc.Format, + Dimension = ShaderResourceViewDimension.Texture2D, + Texture2D = { MipLevels = texDesc.MipLevels } + }); + + // Store our identifier + _fontResourceViews.Add(fontResourceView); + io.Fonts.SetTexID(textureIndex, fontResourceView.NativePointer); + } + + io.Fonts.ClearTexData(); + + // Create texture sampler + _fontSampler = new SamplerState(_device, new SamplerStateDescription + { + Filter = Filter.MinMagMipLinear, + AddressU = TextureAddressMode.Wrap, + AddressV = TextureAddressMode.Wrap, + AddressW = TextureAddressMode.Wrap, + MipLodBias = 0, + ComparisonFunction = Comparison.Always, + MinimumLod = 0, + MaximumLod = 0 + }); + } + + public bool CreateDeviceObjects() + { + if (_device == null) + { + return false; + } + + if (_fontSampler != null) + { + InvalidateDeviceObjects(); + } + + // Create the vertex shader + byte[] shaderData; + + var assembly = Assembly.GetExecutingAssembly(); + using (var stream = assembly.GetManifestResourceStream("imgui-vertex.hlsl.bytes")) + { + shaderData = new byte[stream.Length]; + stream.Read(shaderData, 0, shaderData.Length); + } + + _vertexShader = new VertexShader(_device, shaderData); + + // Create the input layout + _inputLayout = new InputLayout(_device, shaderData, new[] + { + new InputElement("POSITION", 0, Format.R32G32_Float, 0), + new InputElement("TEXCOORD", 0, Format.R32G32_Float, 0), + new InputElement("COLOR", 0, Format.R8G8B8A8_UNorm, 0) + }); + + // Create the constant buffer + _vertexConstantBuffer = new Buffer(_device, new BufferDescription + { + Usage = ResourceUsage.Dynamic, + BindFlags = BindFlags.ConstantBuffer, + CpuAccessFlags = CpuAccessFlags.Write, + OptionFlags = ResourceOptionFlags.None, + SizeInBytes = 16 * sizeof(float) + }); + + // Create the pixel shader + using (var stream = assembly.GetManifestResourceStream("imgui-frag.hlsl.bytes")) + { + shaderData = new byte[stream.Length]; + stream.Read(shaderData, 0, shaderData.Length); + } + + _pixelShader = new PixelShader(_device, shaderData); + + // Create the blending setup + // ...of course this was setup in a way that can't be done inline + var blendStateDesc = new BlendStateDescription(); + blendStateDesc.AlphaToCoverageEnable = false; + blendStateDesc.RenderTarget[0].IsBlendEnabled = true; + blendStateDesc.RenderTarget[0].SourceBlend = BlendOption.SourceAlpha; + blendStateDesc.RenderTarget[0].DestinationBlend = BlendOption.InverseSourceAlpha; + blendStateDesc.RenderTarget[0].BlendOperation = BlendOperation.Add; + blendStateDesc.RenderTarget[0].SourceAlphaBlend = BlendOption.InverseSourceAlpha; + blendStateDesc.RenderTarget[0].DestinationAlphaBlend = BlendOption.Zero; + blendStateDesc.RenderTarget[0].AlphaBlendOperation = BlendOperation.Add; + blendStateDesc.RenderTarget[0].RenderTargetWriteMask = ColorWriteMaskFlags.All; + _blendState = new BlendState(_device, blendStateDesc); + + // Create the rasterizer state + _rasterizerState = new RasterizerState(_device, new RasterizerStateDescription + { + FillMode = FillMode.Solid, + CullMode = CullMode.None, + IsScissorEnabled = true, + IsDepthClipEnabled = true + }); + + // Create the depth-stencil State + _depthStencilState = new DepthStencilState(_device, new DepthStencilStateDescription + { + IsDepthEnabled = false, + DepthWriteMask = DepthWriteMask.All, + DepthComparison = Comparison.Always, + IsStencilEnabled = false, + FrontFace = + { + FailOperation = StencilOperation.Keep, + DepthFailOperation = StencilOperation.Keep, + PassOperation = StencilOperation.Keep, + Comparison = Comparison.Always + }, + BackFace = + { + FailOperation = StencilOperation.Keep, + DepthFailOperation = StencilOperation.Keep, + PassOperation = StencilOperation.Keep, + Comparison = Comparison.Always + } + }); + + CreateFontsTexture(); + + return true; + } + + // Added to support dynamic rebuilding of the font texture + // for adding fonts after initialization time + public void RebuildFontTexture() + { + _fontSampler?.Dispose(); + foreach (var fontResourceView in this._fontResourceViews) + fontResourceView.Dispose(); + this._fontResourceViews.Clear(); + + CreateFontsTexture(); + } + + public void InvalidateDeviceObjects() + { + if (_device == null) + { + return; + } + + _fontSampler?.Dispose(); + _fontSampler = null; + + foreach (var fontResourceView in this._fontResourceViews) + fontResourceView.Dispose(); + this._fontResourceViews.Clear(); + for (int textureIndex = 0, textureCount = ImGui.GetIO().Fonts.Textures.Size; + textureIndex < textureCount; + textureIndex++) + ImGui.GetIO().Fonts.SetTexID(textureIndex, IntPtr.Zero); + + _indexBuffer?.Dispose(); + _indexBuffer = null; + + _vertexBuffer?.Dispose(); + _vertexBuffer = null; + + _blendState?.Dispose(); + _blendState = null; + + _depthStencilState?.Dispose(); + _depthStencilState = null; + + _rasterizerState?.Dispose(); + _rasterizerState = null; + + _pixelShader?.Dispose(); + _pixelShader = null; + + _vertexConstantBuffer?.Dispose(); + _vertexConstantBuffer = null; + + _inputLayout?.Dispose(); + _inputLayout = null; + + _vertexShader?.Dispose(); + _vertexShader = null; + } + + public void Init(params object[] initParams) + { + ImGui.GetIO().BackendFlags = ImGui.GetIO().BackendFlags | ImGuiBackendFlags.RendererHasVtxOffset | ImGuiBackendFlags.RendererHasViewports; + + // BackendRendererName is readonly (and null) in ImGui.NET for some reason, but we can hack it via its internal pointer + _renderNamePtr = Marshal.StringToHGlobalAnsi("imgui_impl_dx11_c#"); + unsafe + { + ImGui.GetIO().NativePtr->BackendRendererName = (byte*)_renderNamePtr.ToPointer(); + } + + _device = (Device)initParams[0]; + _deviceContext = (DeviceContext)initParams[1]; + + InitPlatformInterface(); + + // SharpDX also doesn't allow reference managment + } + + public void Shutdown() + { + ShutdownPlatformInterface(); + InvalidateDeviceObjects(); + + // we don't own these, so no Dispose() + _device = null; + _deviceContext = null; + + if (_renderNamePtr != IntPtr.Zero) + { + Marshal.FreeHGlobal(_renderNamePtr); + _renderNamePtr = IntPtr.Zero; + } + } + + public void NewFrame() + { + if (_fontSampler == null) + { + CreateDeviceObjects(); + } + } + + /** Viewport support **/ + private struct ImGuiViewportDataDx11 + { + public IntPtr SwapChain; + public IntPtr View; + } + + // Viewport interface + private delegate void CreateWindowDelegate(ImGuiViewportPtr ptr); + private delegate void DestroyWindowDelegate(ImGuiViewportPtr ptr); + private delegate void SetWindowSizeDelegate(ImGuiViewportPtr ptr, Vector2 size); + private delegate void RenderWindowDelegate(ImGuiViewportPtr ptr, IntPtr v); + private delegate void SwapBuffersDelegate(ImGuiViewportPtr ptr, IntPtr v); + + private CreateWindowDelegate _createWindow; + private DestroyWindowDelegate _destroyWindow; + private SetWindowSizeDelegate _setWindowSize; + private RenderWindowDelegate _renderWindow; + private SwapBuffersDelegate _swapBuffers; + + private void InitPlatformInterface() + { + ImGuiPlatformIOPtr ptr = ImGui.GetPlatformIO(); + _createWindow = CreateWindow; + _destroyWindow = DestroyWindow; + _setWindowSize = SetWindowSize; + _renderWindow = RenderWindow; + _swapBuffers = SwapBuffers; + + ptr.Renderer_CreateWindow = Marshal.GetFunctionPointerForDelegate(_createWindow); + ptr.Renderer_DestroyWindow = Marshal.GetFunctionPointerForDelegate(_destroyWindow); + ptr.Renderer_SetWindowSize = Marshal.GetFunctionPointerForDelegate(_setWindowSize); + ptr.Renderer_RenderWindow = Marshal.GetFunctionPointerForDelegate(_renderWindow); + ptr.Renderer_SwapBuffers = Marshal.GetFunctionPointerForDelegate(_swapBuffers); + } + + private void ShutdownPlatformInterface() + { + ImGui.DestroyPlatformWindows(); + } + + // Viewport functions + public void CreateWindow(ImGuiViewportPtr viewport) + { + var data = (ImGuiViewportDataDx11*) Marshal.AllocHGlobal(Marshal.SizeOf()); + + // 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. + IntPtr hWnd = viewport.PlatformHandleRaw; + if (hWnd == IntPtr.Zero) + hWnd = viewport.PlatformHandle; + + // Create swapchain + SwapChainDescription desc = new SwapChainDescription + { + ModeDescription = new ModeDescription + { + Width = 0, + Height = 0, + Format = Format.R8G8B8A8_UNorm, + RefreshRate = new Rational(0, 0) + }, + SampleDescription = new SampleDescription + { + Count = 1, + Quality = 0 + }, + Usage = Usage.RenderTargetOutput, + BufferCount = 1, + OutputHandle = hWnd, + IsWindowed = true, + SwapEffect = SwapEffect.Discard, + Flags = SwapChainFlags.None + }; + + data->SwapChain = CreateSwapChain(desc); + + // Create the render target view + using (var backbuffer = new SwapChain(data->SwapChain).GetBackBuffer(0)) + data->View = new RenderTargetView(_device, backbuffer).NativePointer; + + viewport.RendererUserData = (IntPtr) data; + } + + private IntPtr CreateSwapChain(SwapChainDescription desc) { + + // Create a swapchain using the existing game hardware (I think) + using (var dxgi = _device.QueryInterface()) + using (var adapter = dxgi.Adapter) + using (var factory = adapter.GetParent()) + { + return new SwapChain(factory, _device, desc).NativePointer; + } + } + + public void DestroyWindow(ImGuiViewportPtr viewport) + { + // This is also called on the main viewport for some reason, and we never set that viewport's RendererUserData + if (viewport.RendererUserData == IntPtr.Zero) return; + + var data = (ImGuiViewportDataDx11*) viewport.RendererUserData; + + new SwapChain(data->SwapChain).Dispose(); + new RenderTargetView(data->View).Dispose(); + data->SwapChain = IntPtr.Zero; + data->View = IntPtr.Zero; + + Marshal.FreeHGlobal(viewport.RendererUserData); + viewport.RendererUserData = IntPtr.Zero; + } + + public void SetWindowSize(ImGuiViewportPtr viewport, Vector2 size) + { + var data = (ImGuiViewportDataDx11*)viewport.RendererUserData; + + // Delete our existing view + new RenderTargetView(data->View).Dispose(); + var tmpSwap = new SwapChain(data->SwapChain); + + // Resize buffers and recreate view + tmpSwap.ResizeBuffers(1, (int)size.X, (int)size.Y, Format.Unknown, SwapChainFlags.None); + using (var backbuffer = tmpSwap.GetBackBuffer(0)) + data->View = new RenderTargetView(_device, backbuffer).NativePointer; + } + + public void RenderWindow(ImGuiViewportPtr viewport, IntPtr v) + { + var data = (ImGuiViewportDataDx11*)viewport.RendererUserData; + + var tmpRtv = new RenderTargetView(data->View); + this._deviceContext.OutputMerger.SetTargets(tmpRtv); + if ((viewport.Flags & ImGuiViewportFlags.NoRendererClear) != ImGuiViewportFlags.NoRendererClear) + this._deviceContext.ClearRenderTargetView(tmpRtv, new RawColor4(0f, 0f, 0f, 1f)); + RenderDrawData(viewport.DrawData); + } + + public void SwapBuffers(ImGuiViewportPtr viewport, IntPtr v) + { + var data = (ImGuiViewportDataDx11*)viewport.RendererUserData; + new SwapChain(data->SwapChain).Present(0, PresentFlags.None); + } + } +} diff --git a/Dalamud/Interface/ImGuiScene/RawDX11Scene.cs b/Dalamud/Interface/ImGuiScene/RawDX11Scene.cs new file mode 100644 index 000000000..299349d51 --- /dev/null +++ b/Dalamud/Interface/ImGuiScene/RawDX11Scene.cs @@ -0,0 +1,368 @@ +using ImGuiNET; +using PInvoke; +using SharpDX; +using SharpDX.Direct3D; +using SharpDX.Direct3D11; +using SharpDX.DXGI; +using StbiSharp; + +using System.IO; + +using Dalamud.Interface.Textures.TextureWraps; + +using ImGuiScene.ImGui_Impl; +using ImGuizmoNET; +using ImPlotNET; +using Device = SharpDX.Direct3D11.Device; + +namespace ImGuiScene +{ + // This class will likely eventually be unified a bit more with other scenes, but for + // now it should be directly useable + public sealed class RawDX11Scene : IDisposable + { + public Device Device { get; private set; } + public IntPtr WindowHandlePtr { get; private set; } + public SwapChain SwapChain { get; private set; } + + public bool UpdateCursor + { + get => this.imguiInput.UpdateCursor; + set => this.imguiInput.UpdateCursor = value; + } + + private DeviceContext deviceContext; + private RenderTargetView rtv; + + private int targetWidth; + private int targetHeight; + + private ImGui_Impl_DX11 imguiRenderer; + private ImGui_Input_Impl_Direct imguiInput; + + public delegate void BuildUIDelegate(); + public delegate void NewInputFrameDelegate(); + public delegate void NewRenderFrameDelegate(); + + /// + /// User methods invoked every ImGui frame to construct custom UIs. + /// + public BuildUIDelegate OnBuildUI; + + public NewInputFrameDelegate OnNewInputFrame; + public NewRenderFrameDelegate OnNewRenderFrame; + + private string imguiIniPath = null; + public string ImGuiIniPath + { + get { return imguiIniPath; } + set + { + imguiIniPath = value; + imguiInput.SetIniPath(imguiIniPath); + } + } + + public RawDX11Scene(IntPtr nativeSwapChain) + { + this.SwapChain = new SwapChain(nativeSwapChain); + this.Device = SwapChain.GetDevice(); + + Initialize(); + } + + // This ctor will work fine, but it's only usefulness over using just the swapchain version + // is that this one will allow you to pass a different device than the swapchain.GetDevice() would + // return. This is mostly useful for render debugging, where the real d3ddevice is hooked and + // where we would like all our work to be done on that hooked device. + // Because we generally will get the swapchain from the internal present() call, we are getting + // the real d3d swapchain and not a hooked version, so GetDevice() will correspondingly return + // the read device and not a hooked verison. + // By passing in the hooked version explicitly here, we can mostly play nice with debug tools + public RawDX11Scene(IntPtr nativeDevice, IntPtr nativeSwapChain) + { + this.Device = new Device(nativeDevice); + this.SwapChain = new SwapChain(nativeSwapChain); + + Initialize(); + } + + private void Initialize() + { + this.deviceContext = this.Device.ImmediateContext; + + using (var backbuffer = this.SwapChain.GetBackBuffer(0)) + { + this.rtv = new RenderTargetView(this.Device, backbuffer); + } + + // could also do things with GetClientRect() for WindowHandlePtr, not sure if that is necessary + this.targetWidth = this.SwapChain.Description.ModeDescription.Width; + this.targetHeight = this.SwapChain.Description.ModeDescription.Height; + + this.WindowHandlePtr = this.SwapChain.Description.OutputHandle; + + InitializeImGui(); + } + + private void InitializeImGui() + { + this.imguiRenderer = new ImGui_Impl_DX11(); + + var ctx = ImGui.CreateContext(); + ImGuizmo.SetImGuiContext(ctx); + ImPlot.SetImGuiContext(ctx); + ImPlot.CreateContext(); + + ImGui.GetIO().ConfigFlags |= ImGuiConfigFlags.DockingEnable | ImGuiConfigFlags.ViewportsEnable; + + this.imguiRenderer.Init(this.Device, this.deviceContext); + this.imguiInput = new ImGui_Input_Impl_Direct(WindowHandlePtr); + } + + /// + /// Processes window messages. + /// + /// Handle of the window. + /// Type of window message. + /// wParam. + /// lParam. + /// Return value. + public unsafe IntPtr? ProcessWndProcW(IntPtr hWnd, User32.WindowMessage msg, void* wParam, void* lParam) { + return this.imguiInput.ProcessWndProcW(hWnd, msg, wParam, lParam); + } + + public void Render() + { + this.deviceContext.OutputMerger.SetRenderTargets(this.rtv); + + this.imguiRenderer.NewFrame(); + this.OnNewRenderFrame?.Invoke(); + this.imguiInput.NewFrame(targetWidth, targetHeight); + this.OnNewInputFrame?.Invoke(); + + ImGui.NewFrame(); + ImGuizmo.BeginFrame(); + + OnBuildUI?.Invoke(); + + ImGui.Render(); + + this.imguiRenderer.RenderDrawData(ImGui.GetDrawData()); + this.deviceContext.OutputMerger.SetRenderTargets((RenderTargetView)null); + ImGui.UpdatePlatformWindows(); + ImGui.RenderPlatformWindowsDefault(); + } + + public void OnPreResize() + { + this.deviceContext.OutputMerger.SetRenderTargets((RenderTargetView)null); + + this.rtv?.Dispose(); + this.rtv = null; + } + + public void OnPostResize(int newWidth, int newHeight) + { + using (var backbuffer = this.SwapChain.GetBackBuffer(0)) + { + this.rtv = new RenderTargetView(this.Device, backbuffer); + } + + this.targetWidth = newWidth; + this.targetHeight = newHeight; + } + + // It is pretty much required that this is called from a handler attached + // to OnNewRenderFrame + public void InvalidateFonts() + { + this.imguiRenderer.RebuildFontTexture(); + } + + // It is pretty much required that this is called from a handler attached + // to OnNewRenderFrame + public void ClearStacksOnContext() { + Custom.igCustom_ClearStacks(); + } + + public bool IsImGuiCursor(IntPtr hCursor) + { + return this.imguiInput.IsImGuiCursor(hCursor); + } + + public IDalamudTextureWrap LoadImage(string path) + { + using (var fs = new FileStream(path, FileMode.Open, FileAccess.Read)) + using (var ms = new MemoryStream()) + { + fs.CopyTo(ms); + var image = Stbi.LoadFromMemory(ms, 4); + return LoadImage_Internal(image); + } + } + + public IDalamudTextureWrap LoadImage(byte[] imageBytes) + { + using (var ms = new MemoryStream(imageBytes, 0, imageBytes.Length, false, true)) + { + var image = Stbi.LoadFromMemory(ms, 4); + return LoadImage_Internal(image); + } + } + + public unsafe IDalamudTextureWrap LoadImageRaw(byte[] imageData, int width, int height, int numChannels = 4) + { + // StbiSharp doesn't expose a constructor, even just to wrap existing data, which means + // short of something awful like below, or creating another wrapper layer, we can't avoid + // adding divergent code paths into CreateTexture + //var mock = new { Width = width, Height = height, NumChannels = numChannels, Data = imageData }; + //var image = Unsafe.As(mock); + //return LoadImage_Internal(image); + + fixed (void* pixelData = imageData) + { + return CreateTexture(pixelData, width, height, numChannels); + } + } + + private unsafe IDalamudTextureWrap LoadImage_Internal(StbiImage image) + { + fixed (void* pixelData = image.Data) + { + return CreateTexture(pixelData, image.Width, image.Height, image.NumChannels); + } + } + + private unsafe IDalamudTextureWrap CreateTexture(void* pixelData, int width, int height, int bytesPerPixel) + { + ShaderResourceView resView = null; + + var texDesc = new Texture2DDescription + { + Width = width, + Height = height, + MipLevels = 1, + ArraySize = 1, + Format = Format.R8G8B8A8_UNorm, + SampleDescription = new SampleDescription(1, 0), + Usage = ResourceUsage.Immutable, + BindFlags = BindFlags.ShaderResource, + CpuAccessFlags = CpuAccessFlags.None, + OptionFlags = ResourceOptionFlags.None + }; + + using (var texture = new Texture2D(this.Device, texDesc, new DataRectangle(new IntPtr(pixelData), width * bytesPerPixel))) + { + resView = new ShaderResourceView(this.Device, texture, new ShaderResourceViewDescription + { + Format = texDesc.Format, + Dimension = ShaderResourceViewDimension.Texture2D, + Texture2D = { MipLevels = texDesc.MipLevels } + }); + } + + // no sampler for now because the ImGui implementation we copied doesn't allow for changing it + + return new D3DTextureWrap(resView, width, height); + } + + public byte[] CaptureScreenshot() + { + using (var backBuffer = this.SwapChain.GetBackBuffer(0)) + { + Texture2DDescription desc = backBuffer.Description; + desc.CpuAccessFlags = CpuAccessFlags.Read; + desc.Usage = ResourceUsage.Staging; + desc.OptionFlags = ResourceOptionFlags.None; + desc.BindFlags = BindFlags.None; + + using (var tex = new Texture2D(this.Device, desc)) + { + this.deviceContext.CopyResource(backBuffer, tex); + using (var surf = tex.QueryInterface()) + { + var map = surf.Map(SharpDX.DXGI.MapFlags.Read, out DataStream dataStream); + var pixelData = new byte[surf.Description.Width * surf.Description.Height * surf.Description.Format.SizeOfInBytes()]; + var dataCounter = 0; + + while (dataCounter < pixelData.Length) + { + //var curPixel = dataStream.Read(); + var x = dataStream.Read(); + var y = dataStream.Read(); + var z = dataStream.Read(); + var w = dataStream.Read(); + + pixelData[dataCounter++] = z; + pixelData[dataCounter++] = y; + pixelData[dataCounter++] = x; + pixelData[dataCounter++] = w; + } + + // TODO: test this on a thread + //var gch = GCHandle.Alloc(pixelData, GCHandleType.Pinned); + //using (var bitmap = new Bitmap(surf.Description.Width, surf.Description.Height, map.Pitch, PixelFormat.Format32bppRgb, gch.AddrOfPinnedObject())) + //{ + // bitmap.Save(path); + //} + //gch.Free(); + + surf.Unmap(); + dataStream.Dispose(); + + return pixelData; + } + } + } + } + + #region IDisposable Support + private bool disposedValue = false; // To detect redundant calls + + private void Dispose(bool disposing) + { + if (!disposedValue) + { + if (disposing) + { + // TODO: dispose managed state (managed objects). + } + + this.imguiRenderer?.Shutdown(); + this.imguiInput?.Dispose(); + + ImGui.DestroyContext(); + + this.rtv.Dispose(); + + // Not actually sure how sharpdx does ref management, but hopefully they + // addref when we create our wrappers, so this should just release that count + + // Originally it was thought these lines were needed because it was assumed that SharpDX does + // proper refcounting to handle disposing, but disposing these would cause the game to crash + // on resizing after unloading Dalamud + // this.SwapChain?.Dispose(); + // this.deviceContext?.Dispose(); + // this.Device?.Dispose(); + + disposedValue = true; + } + } + + ~RawDX11Scene() + { + // Do not change this code. Put cleanup code in Dispose(bool disposing) above. + Dispose(false); + } + + // This code added to correctly implement the disposable pattern. + public void Dispose() + { + // Do not change this code. Put cleanup code in Dispose(bool disposing) above. + Dispose(true); + GC.SuppressFinalize(this); + } + #endregion + } +} diff --git a/Dalamud/Interface/ImGuiScene/Win32 Utils/Constants.cs b/Dalamud/Interface/ImGuiScene/Win32 Utils/Constants.cs new file mode 100644 index 000000000..31d9075fc --- /dev/null +++ b/Dalamud/Interface/ImGuiScene/Win32 Utils/Constants.cs @@ -0,0 +1,515 @@ +namespace ImGuiScene +{ + // Assorted other win32 constants that are seemingly just #defined + // Not an enum like the others because the types vary and there are overlapping values + internal static class Win32Constants + { + public const short HTTRANSPARENT = -1; + public const short HTCLIENT = 1; + + public const short XBUTTON1 = 1; + + public const int WHEEL_DELTA = 120; + + public const int CURSOR_SHOWING = 1; + public const int CURSOR_SUPPRESSED = 2; + } + + internal enum WindowLongType : int + { + GWL_EXSTYLE = -20, + GWL_HINSTANCE = -6, + GWL_HWNDPARENT = -8, + GWL_ID = -12, + GWL_STYLE = -16, + GWL_USERDATA = -21, + GWL_WNDPROC = -4 + } + + internal enum Cursor : int + { + IDC_ARROW = 32512, + IDC_IBEAM = 32513, + IDC_WAIT = 32514, + IDC_CROSS = 32515, + IDC_UPARROW = 32516, + IDC_SIZE = 32640, + IDC_ICON = 32641, + IDC_SIZENWSE = 32642, + IDC_SIZENESW = 32643, + IDC_SIZEWE = 32644, + IDC_SIZENS = 32645, + IDC_SIZEALL = 32646, + IDC_NO = 32648, + IDC_HAND = 32649, + IDC_APPSTARTING = 32650, + IDC_HELP = 32651 + } + + // taken from https://www.pinvoke.net/default.aspx/Enums/VirtualKeys.html + internal enum VirtualKey : int + { + LeftButton = 0x01, + RightButton = 0x02, + Cancel = 0x03, + MiddleButton = 0x04, + ExtraButton1 = 0x05, + ExtraButton2 = 0x06, + Back = 0x08, + Tab = 0x09, + Clear = 0x0C, + Return = 0x0D, + Shift = 0x10, + Control = 0x11, + Menu = 0x12, + Pause = 0x13, + CapsLock = 0x14, + Kana = 0x15, + Hangeul = 0x15, + Hangul = 0x15, + Junja = 0x17, + Final = 0x18, + Hanja = 0x19, + Kanji = 0x19, + Escape = 0x1B, + Convert = 0x1C, + NonConvert = 0x1D, + Accept = 0x1E, + ModeChange = 0x1F, + Space = 0x20, + Prior = 0x21, + Next = 0x22, + End = 0x23, + Home = 0x24, + Left = 0x25, + Up = 0x26, + Right = 0x27, + Down = 0x28, + Select = 0x29, + Print = 0x2A, + Execute = 0x2B, + Snapshot = 0x2C, + Insert = 0x2D, + Delete = 0x2E, + Help = 0x2F, + N0 = 0x30, + N1 = 0x31, + N2 = 0x32, + N3 = 0x33, + N4 = 0x34, + N5 = 0x35, + N6 = 0x36, + N7 = 0x37, + N8 = 0x38, + N9 = 0x39, + A = 0x41, + B = 0x42, + C = 0x43, + D = 0x44, + E = 0x45, + F = 0x46, + G = 0x47, + H = 0x48, + I = 0x49, + J = 0x4A, + K = 0x4B, + L = 0x4C, + M = 0x4D, + N = 0x4E, + O = 0x4F, + P = 0x50, + Q = 0x51, + R = 0x52, + S = 0x53, + T = 0x54, + U = 0x55, + V = 0x56, + W = 0x57, + X = 0x58, + Y = 0x59, + Z = 0x5A, + LeftWindows = 0x5B, + RightWindows = 0x5C, + Application = 0x5D, + Sleep = 0x5F, + Numpad0 = 0x60, + Numpad1 = 0x61, + Numpad2 = 0x62, + Numpad3 = 0x63, + Numpad4 = 0x64, + Numpad5 = 0x65, + Numpad6 = 0x66, + Numpad7 = 0x67, + Numpad8 = 0x68, + Numpad9 = 0x69, + Multiply = 0x6A, + Add = 0x6B, + Separator = 0x6C, + Subtract = 0x6D, + Decimal = 0x6E, + Divide = 0x6F, + F1 = 0x70, + F2 = 0x71, + F3 = 0x72, + F4 = 0x73, + F5 = 0x74, + F6 = 0x75, + F7 = 0x76, + F8 = 0x77, + F9 = 0x78, + F10 = 0x79, + F11 = 0x7A, + F12 = 0x7B, + F13 = 0x7C, + F14 = 0x7D, + F15 = 0x7E, + F16 = 0x7F, + F17 = 0x80, + F18 = 0x81, + F19 = 0x82, + F20 = 0x83, + F21 = 0x84, + F22 = 0x85, + F23 = 0x86, + F24 = 0x87, + NumLock = 0x90, + ScrollLock = 0x91, + NEC_Equal = 0x92, + Fujitsu_Jisho = 0x92, + Fujitsu_Masshou = 0x93, + Fujitsu_Touroku = 0x94, + Fujitsu_Loya = 0x95, + Fujitsu_Roya = 0x96, + LeftShift = 0xA0, + RightShift = 0xA1, + LeftControl = 0xA2, + RightControl = 0xA3, + LeftMenu = 0xA4, + RightMenu = 0xA5, + BrowserBack = 0xA6, + BrowserForward = 0xA7, + BrowserRefresh = 0xA8, + BrowserStop = 0xA9, + BrowserSearch = 0xAA, + BrowserFavorites = 0xAB, + BrowserHome = 0xAC, + VolumeMute = 0xAD, + VolumeDown = 0xAE, + VolumeUp = 0xAF, + MediaNextTrack = 0xB0, + MediaPrevTrack = 0xB1, + MediaStop = 0xB2, + MediaPlayPause = 0xB3, + LaunchMail = 0xB4, + LaunchMediaSelect = 0xB5, + LaunchApplication1 = 0xB6, + LaunchApplication2 = 0xB7, + OEM1 = 0xBA, + OEMPlus = 0xBB, + OEMComma = 0xBC, + OEMMinus = 0xBD, + OEMPeriod = 0xBE, + OEM2 = 0xBF, + OEM3 = 0xC0, + OEM4 = 0xDB, + OEM5 = 0xDC, + OEM6 = 0xDD, + OEM7 = 0xDE, + OEM8 = 0xDF, + OEMAX = 0xE1, + OEM102 = 0xE2, + ICOHelp = 0xE3, + ICO00 = 0xE4, + ProcessKey = 0xE5, + ICOClear = 0xE6, + Packet = 0xE7, + OEMReset = 0xE9, + OEMJump = 0xEA, + OEMPA1 = 0xEB, + OEMPA2 = 0xEC, + OEMPA3 = 0xED, + OEMWSCtrl = 0xEE, + OEMCUSel = 0xEF, + OEMATTN = 0xF0, + OEMFinish = 0xF1, + OEMCopy = 0xF2, + OEMAuto = 0xF3, + OEMENLW = 0xF4, + OEMBackTab = 0xF5, + ATTN = 0xF6, + CRSel = 0xF7, + EXSel = 0xF8, + EREOF = 0xF9, + Play = 0xFA, + Zoom = 0xFB, + Noname = 0xFC, + PA1 = 0xFD, + OEMClear = 0xFE + } + + // taken from https://gist.github.com/amgine/2395987 + // may be missing some but likely has anything we'd ever need + internal enum WindowsMessage + { + WM_NULL = 0x0000, + WM_CREATE = 0x0001, + WM_DESTROY = 0x0002, + WM_MOVE = 0x0003, + WM_SIZE = 0x0005, + WM_ACTIVATE = 0x0006, + WM_SETFOCUS = 0x0007, + WM_KILLFOCUS = 0x0008, + WM_ENABLE = 0x000A, + WM_SETREDRAW = 0x000B, + WM_SETTEXT = 0x000C, + WM_GETTEXT = 0x000D, + WM_GETTEXTLENGTH = 0x000E, + WM_PAINT = 0x000F, + WM_CLOSE = 0x0010, + WM_QUERYENDSESSION = 0x0011, + WM_QUERYOPEN = 0x0013, + WM_ENDSESSION = 0x0016, + WM_QUIT = 0x0012, + WM_ERASEBKGND = 0x0014, + WM_SYSCOLORCHANGE = 0x0015, + WM_SHOWWINDOW = 0x0018, + WM_WININICHANGE = 0x001A, + WM_SETTINGCHANGE = WM_WININICHANGE, + WM_DEVMODECHANGE = 0x001B, + WM_ACTIVATEAPP = 0x001C, + WM_FONTCHANGE = 0x001D, + WM_TIMECHANGE = 0x001E, + WM_CANCELMODE = 0x001F, + WM_SETCURSOR = 0x0020, + WM_MOUSEACTIVATE = 0x0021, + WM_CHILDACTIVATE = 0x0022, + WM_QUEUESYNC = 0x0023, + WM_GETMINMAXINFO = 0x0024, + WM_PAINTICON = 0x0026, + WM_ICONERASEBKGND = 0x0027, + WM_NEXTDLGCTL = 0x0028, + WM_SPOOLERSTATUS = 0x002A, + WM_DRAWITEM = 0x002B, + WM_MEASUREITEM = 0x002C, + WM_DELETEITEM = 0x002D, + WM_VKEYTOITEM = 0x002E, + WM_CHARTOITEM = 0x002F, + WM_SETFONT = 0x0030, + WM_GETFONT = 0x0031, + WM_SETHOTKEY = 0x0032, + WM_GETHOTKEY = 0x0033, + WM_QUERYDRAGICON = 0x0037, + WM_COMPAREITEM = 0x0039, + WM_GETOBJECT = 0x003D, + WM_COMPACTING = 0x0041, + WM_COMMNOTIFY = 0x0044, + WM_WINDOWPOSCHANGING = 0x0046, + WM_WINDOWPOSCHANGED = 0x0047, + WM_POWER = 0x0048, + WM_COPYDATA = 0x004A, + WM_CANCELJOURNAL = 0x004B, + WM_NOTIFY = 0x004E, + WM_INPUTLANGCHANGEREQUEST = 0x0050, + WM_INPUTLANGCHANGE = 0x0051, + WM_TCARD = 0x0052, + WM_HELP = 0x0053, + WM_USERCHANGED = 0x0054, + WM_NOTIFYFORMAT = 0x0055, + WM_CONTEXTMENU = 0x007B, + WM_STYLECHANGING = 0x007C, + WM_STYLECHANGED = 0x007D, + WM_DISPLAYCHANGE = 0x007E, + WM_GETICON = 0x007F, + WM_SETICON = 0x0080, + WM_NCCREATE = 0x0081, + WM_NCDESTROY = 0x0082, + WM_NCCALCSIZE = 0x0083, + WM_NCHITTEST = 0x0084, + WM_NCPAINT = 0x0085, + WM_NCACTIVATE = 0x0086, + WM_GETDLGCODE = 0x0087, + WM_SYNCPAINT = 0x0088, + + WM_NCMOUSEMOVE = 0x00A0, + WM_NCLBUTTONDOWN = 0x00A1, + WM_NCLBUTTONUP = 0x00A2, + WM_NCLBUTTONDBLCLK = 0x00A3, + WM_NCRBUTTONDOWN = 0x00A4, + WM_NCRBUTTONUP = 0x00A5, + WM_NCRBUTTONDBLCLK = 0x00A6, + WM_NCMBUTTONDOWN = 0x00A7, + WM_NCMBUTTONUP = 0x00A8, + WM_NCMBUTTONDBLCLK = 0x00A9, + WM_NCXBUTTONDOWN = 0x00AB, + WM_NCXBUTTONUP = 0x00AC, + WM_NCXBUTTONDBLCLK = 0x00AD, + + WM_INPUT_DEVICE_CHANGE = 0x00FE, + WM_INPUT = 0x00FF, + + WM_KEYFIRST = 0x0100, + WM_KEYDOWN = 0x0100, + WM_KEYUP = 0x0101, + WM_CHAR = 0x0102, + WM_DEADCHAR = 0x0103, + WM_SYSKEYDOWN = 0x0104, + WM_SYSKEYUP = 0x0105, + WM_SYSCHAR = 0x0106, + WM_SYSDEADCHAR = 0x0107, + WM_UNICHAR = 0x0109, + WM_KEYLAST = 0x0109, + + WM_IME_STARTCOMPOSITION = 0x010D, + WM_IME_ENDCOMPOSITION = 0x010E, + WM_IME_COMPOSITION = 0x010F, + WM_IME_KEYLAST = 0x010F, + + WM_INITDIALOG = 0x0110, + WM_COMMAND = 0x0111, + WM_SYSCOMMAND = 0x0112, + WM_TIMER = 0x0113, + WM_HSCROLL = 0x0114, + WM_VSCROLL = 0x0115, + WM_INITMENU = 0x0116, + WM_INITMENUPOPUP = 0x0117, + WM_MENUSELECT = 0x011F, + WM_MENUCHAR = 0x0120, + WM_ENTERIDLE = 0x0121, + WM_MENURBUTTONUP = 0x0122, + WM_MENUDRAG = 0x0123, + WM_MENUGETOBJECT = 0x0124, + WM_UNINITMENUPOPUP = 0x0125, + WM_MENUCOMMAND = 0x0126, + + WM_CHANGEUISTATE = 0x0127, + WM_UPDATEUISTATE = 0x0128, + WM_QUERYUISTATE = 0x0129, + + WM_CTLCOLORMSGBOX = 0x0132, + WM_CTLCOLOREDIT = 0x0133, + WM_CTLCOLORLISTBOX = 0x0134, + WM_CTLCOLORBTN = 0x0135, + WM_CTLCOLORDLG = 0x0136, + WM_CTLCOLORSCROLLBAR = 0x0137, + WM_CTLCOLORSTATIC = 0x0138, + MN_GETHMENU = 0x01E1, + + WM_MOUSEFIRST = 0x0200, + WM_MOUSEMOVE = 0x0200, + WM_LBUTTONDOWN = 0x0201, + WM_LBUTTONUP = 0x0202, + WM_LBUTTONDBLCLK = 0x0203, + WM_RBUTTONDOWN = 0x0204, + WM_RBUTTONUP = 0x0205, + WM_RBUTTONDBLCLK = 0x0206, + WM_MBUTTONDOWN = 0x0207, + WM_MBUTTONUP = 0x0208, + WM_MBUTTONDBLCLK = 0x0209, + WM_MOUSEWHEEL = 0x020A, + WM_XBUTTONDOWN = 0x020B, + WM_XBUTTONUP = 0x020C, + WM_XBUTTONDBLCLK = 0x020D, + WM_MOUSEHWHEEL = 0x020E, + + WM_PARENTNOTIFY = 0x0210, + WM_ENTERMENULOOP = 0x0211, + WM_EXITMENULOOP = 0x0212, + + WM_NEXTMENU = 0x0213, + WM_SIZING = 0x0214, + WM_CAPTURECHANGED = 0x0215, + WM_MOVING = 0x0216, + + WM_POWERBROADCAST = 0x0218, + + WM_DEVICECHANGE = 0x0219, + + WM_MDICREATE = 0x0220, + WM_MDIDESTROY = 0x0221, + WM_MDIACTIVATE = 0x0222, + WM_MDIRESTORE = 0x0223, + WM_MDINEXT = 0x0224, + WM_MDIMAXIMIZE = 0x0225, + WM_MDITILE = 0x0226, + WM_MDICASCADE = 0x0227, + WM_MDIICONARRANGE = 0x0228, + WM_MDIGETACTIVE = 0x0229, + + WM_MDISETMENU = 0x0230, + WM_ENTERSIZEMOVE = 0x0231, + WM_EXITSIZEMOVE = 0x0232, + WM_DROPFILES = 0x0233, + WM_MDIREFRESHMENU = 0x0234, + + WM_IME_SETCONTEXT = 0x0281, + WM_IME_NOTIFY = 0x0282, + WM_IME_CONTROL = 0x0283, + WM_IME_COMPOSITIONFULL = 0x0284, + WM_IME_SELECT = 0x0285, + WM_IME_CHAR = 0x0286, + WM_IME_REQUEST = 0x0288, + WM_IME_KEYDOWN = 0x0290, + WM_IME_KEYUP = 0x0291, + + WM_MOUSEHOVER = 0x02A1, + WM_MOUSELEAVE = 0x02A3, + WM_NCMOUSEHOVER = 0x02A0, + WM_NCMOUSELEAVE = 0x02A2, + + WM_WTSSESSION_CHANGE = 0x02B1, + + WM_TABLET_FIRST = 0x02c0, + WM_TABLET_LAST = 0x02df, + + WM_CUT = 0x0300, + WM_COPY = 0x0301, + WM_PASTE = 0x0302, + WM_CLEAR = 0x0303, + WM_UNDO = 0x0304, + WM_RENDERFORMAT = 0x0305, + WM_RENDERALLFORMATS = 0x0306, + WM_DESTROYCLIPBOARD = 0x0307, + WM_DRAWCLIPBOARD = 0x0308, + WM_PAINTCLIPBOARD = 0x0309, + WM_VSCROLLCLIPBOARD = 0x030A, + WM_SIZECLIPBOARD = 0x030B, + WM_ASKCBFORMATNAME = 0x030C, + WM_CHANGECBCHAIN = 0x030D, + WM_HSCROLLCLIPBOARD = 0x030E, + WM_QUERYNEWPALETTE = 0x030F, + WM_PALETTEISCHANGING = 0x0310, + WM_PALETTECHANGED = 0x0311, + WM_HOTKEY = 0x0312, + + WM_PRINT = 0x0317, + WM_PRINTCLIENT = 0x0318, + + WM_APPCOMMAND = 0x0319, + + WM_THEMECHANGED = 0x031A, + + WM_CLIPBOARDUPDATE = 0x031D, + + WM_DWMCOMPOSITIONCHANGED = 0x031E, + WM_DWMNCRENDERINGCHANGED = 0x031F, + WM_DWMCOLORIZATIONCOLORCHANGED = 0x0320, + WM_DWMWINDOWMAXIMIZEDCHANGE = 0x0321, + + WM_GETTITLEBARINFOEX = 0x033F, + + WM_HANDHELDFIRST = 0x0358, + WM_HANDHELDLAST = 0x035F, + + WM_AFXFIRST = 0x0360, + WM_AFXLAST = 0x037F, + + WM_PENWINFIRST = 0x0380, + WM_PENWINLAST = 0x038F, + + WM_APP = 0x8000, + + WM_USER = 0x0400, + + WM_REFLECT = WM_USER + 0x1C00, + } +} diff --git a/Dalamud/Interface/ImGuiScene/Win32 Utils/MemUtil.cs b/Dalamud/Interface/ImGuiScene/Win32 Utils/MemUtil.cs new file mode 100644 index 000000000..d87730f5b --- /dev/null +++ b/Dalamud/Interface/ImGuiScene/Win32 Utils/MemUtil.cs @@ -0,0 +1,15 @@ +using System.Runtime.InteropServices; + +namespace ImGuiScene +{ + public static unsafe class MemUtil + { + public static T* Allocate() where T : unmanaged { + return (T*)Marshal.AllocHGlobal(Marshal.SizeOf()); + } + + public static void Free(this IntPtr obj) { + Marshal.FreeHGlobal(obj); + } + } +} diff --git a/Dalamud/Interface/ImGuiScene/Win32 Utils/Win32.cs b/Dalamud/Interface/ImGuiScene/Win32 Utils/Win32.cs new file mode 100644 index 000000000..d35162cb5 --- /dev/null +++ b/Dalamud/Interface/ImGuiScene/Win32 Utils/Win32.cs @@ -0,0 +1,184 @@ +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +namespace ImGuiScene +{ + // Even though we are importing PInvoke stuff from Nuget, we still need this class + // for some APIs that do not seem to be exposed in any way through those packages. + // In the future, we may be able to use https://github.com/microsoft/cswin32 + internal class Win32 + { + public enum ImeCommand + { + IMN_CLOSESTATUSWINDOW = 0x0001, + IMN_OPENSTATUSWINDOW = 0x0002, + IMN_CHANGECANDIDATE = 0x0003, + IMN_CLOSECANDIDATE = 0x0004, + IMN_OPENCANDIDATE = 0x0005, + IMN_SETCONVERSIONMODE = 0x0006, + IMN_SETSENTENCEMODE = 0x0007, + IMN_SETOPENSTATUS = 0x0008, + IMN_SETCANDIDATEPOS = 0x0009, + IMN_SETCOMPOSITIONFONT = 0x000A, + IMN_SETCOMPOSITIONWINDOW = 0x000B, + IMN_SETSTATUSWINDOWPOS = 0x000C, + IMN_GUIDELINE = 0x000D, + IMN_PRIVATE = 0x000E + } + + + [StructLayout(LayoutKind.Sequential)] + public struct POINT + { + public int X; + public int Y; + + public POINT(int X, int Y) + { + this.X = X; + this.Y = Y; + } + } + + [StructLayout(LayoutKind.Sequential)] + public struct CURSORINFO + { + public Int32 cbSize; + public Int32 flags; + public IntPtr hCursor; + public POINT ptScreenPos; + } + + [StructLayout(LayoutKind.Sequential)] + public struct RECT + { + public int left; + public int top; + public int right; + public int bottom; + + public RECT(int left, int top, int right, int bottom) + { + this.left = left; + this.top = top; + this.right = right; + this.bottom = bottom; + } + } + + [StructLayout(LayoutKind.Sequential)] + public struct COMPOSITIONFORM + { + public uint dwStyle; + public POINT ptCurrentPos; + public RECT rcArea; + + public COMPOSITIONFORM(uint dwStyle, POINT ptCurrentPos, RECT rcArea) + { + this.dwStyle = dwStyle; + this.ptCurrentPos = ptCurrentPos; + this.rcArea = rcArea; + } + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static ushort HIWORD(ulong val) + { + // #define HIWORD(l) ((WORD)((((DWORD_PTR)(l)) >> 16) & 0xffff)) + return (ushort)((val >> 16) & 0xFFFF); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static ushort LOWORD(ulong val) + { + // #define LOWORD(l) ((WORD)(((DWORD_PTR)(l)) & 0xffff)) + return (ushort)(val & 0xFFFF); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static ushort GET_XBUTTON_WPARAM(ulong val) + { + // #define GET_XBUTTON_WPARAM(wParam) (HIWORD(wParam)) + return HIWORD(val); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static short GET_WHEEL_DELTA_WPARAM(ulong val) + { + // #define GET_WHEEL_DELTA_WPARAM(wParam) ((short)HIWORD(wParam)) + return (short)HIWORD(val); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static int GET_X_LPARAM(ulong val) + { + // #define GET_X_LPARAM(lp) ((int)(short)LOWORD(lp)) + return (int)(short)LOWORD(val); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static int GET_Y_LPARAM(ulong val) + { + // #define GET_Y_LPARAM(lp) ((int)(short)HIWORD(lp)) + return (int)(short)HIWORD(val); + } + + [DllImport("dwmapi.dll")] + public static extern int DwmIsCompositionEnabled(out bool enabled); + [DllImport("user32.dll", SetLastError = true)] + public static extern bool BringWindowToTop(IntPtr hWnd); + [DllImport("user32.dll", SetLastError = true)] + public static extern IntPtr SetFocus(IntPtr hWnd); + [DllImport("user32.dll")] + public static extern bool SetLayeredWindowAttributes(IntPtr hwnd, uint crKey, byte bAlpha, uint dwFlags); + // [DllImport("Imm32.dll", SetLastError=true)] + // public static extern IntPtr ImmGetContext(IntPtr hWnd); + // [DllImport("Imm32.dll", SetLastError=true)] + // public static extern bool ImmSetCompositionWindow(IntPtr hImc, ref COMPOSITIONFORM lpCompForm); + // [DllImport("Imm32.dll", SetLastError=true)] + // public static extern bool ImmReleaseContext(IntPtr hWnd, IntPtr hImc); + + + [DllImport("user32.dll")] + public static extern bool GetCursorPos(out POINT lpPoint); + [DllImport("user32.dll")] + public static extern bool SetCursorPos(int x, int y); + [DllImport("user32.dll")] + public static extern bool ScreenToClient(IntPtr hWnd, ref POINT lpPoint); + [DllImport("user32.dll")] + public static extern bool ClientToScreen(IntPtr hWnd, ref POINT lpPoint); + [DllImport("user32.dll")] + public static extern IntPtr GetCapture(); + [DllImport("user32.dll")] + public static extern IntPtr SetCapture(IntPtr hWnd); + [DllImport("user32.dll")] + public static extern bool ReleaseCapture(); + [DllImport("user32.dll")] + public static extern short GetKeyState(VirtualKey nVirtKey); + [DllImport("user32.dll")] + public static extern IntPtr GetCursor(); + [DllImport("user32.dll")] + public static extern IntPtr SetCursor(IntPtr handle); + [DllImport("user32.dll")] + public static extern IntPtr LoadCursor(IntPtr hInstance, Cursor lpCursorName); + [DllImport("user32.dll")] + public static extern int ShowCursor(bool bShow); + [DllImport("user32.dll", EntryPoint = "SetWindowLongPtrW", SetLastError = true)] + public static extern IntPtr SetWindowLongPtr(IntPtr hWnd, WindowLongType nIndex, IntPtr dwNewLong); + [DllImport("user32.dll", EntryPoint = "CallWindowProcW")] + public static extern long CallWindowProc(IntPtr lpPrevWndFunc, IntPtr hWnd, uint Msg, ulong wParam, long lParam); + + [DllImport("user32.dll", EntryPoint = "GetCursorInfo")] + private static extern bool GetCursorInfo_Internal(ref CURSORINFO pci); + + public static bool GetCursorInfo(out CURSORINFO pci) + { + pci = new CURSORINFO + { + cbSize = Marshal.SizeOf(typeof(CURSORINFO)) + }; + + return GetCursorInfo_Internal(ref pci); + } + } +} diff --git a/Dalamud/Interface/ImGuiScene/costura64/stbi.dll b/Dalamud/Interface/ImGuiScene/costura64/stbi.dll new file mode 100644 index 0000000000000000000000000000000000000000..d36a199bc5b676b3adf5546c54a194f8ab9db080 GIT binary patch literal 90112 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 literal 0 HcmV?d00001 diff --git a/Dalamud/Interface/ImGuiScene/resources/shaders/imgui-frag.glsl b/Dalamud/Interface/ImGuiScene/resources/shaders/imgui-frag.glsl new file mode 100644 index 000000000..2703e717a --- /dev/null +++ b/Dalamud/Interface/ImGuiScene/resources/shaders/imgui-frag.glsl @@ -0,0 +1,12 @@ +#version 150 + +uniform sampler2D Texture; + +in vec2 Frag_UV; +in vec4 Frag_Color; +out vec4 Out_Color; + +void main() +{ + Out_Color = Frag_Color * texture(Texture, Frag_UV.st); +} \ No newline at end of file diff --git a/Dalamud/Interface/ImGuiScene/resources/shaders/imgui-frag.hlsl.bytes b/Dalamud/Interface/ImGuiScene/resources/shaders/imgui-frag.hlsl.bytes new file mode 100644 index 0000000000000000000000000000000000000000..2a9fbf5a36bd803ed3dd9c596e2428b2d2daabee GIT binary patch literal 740 zcma)3Jxjw-6uq?;i>S>abSfDfDu@^nbPz%FVGFe>Nh(1Etr}~kH6>OM6wys^b(Zc~ zoSdBWPdJOepp$}7&rKSzL&XcH@4R#Gd*}2;vDf6c$3^co^Y#5+^&dtP7f&%F`WPZg zU|j${fjY!Ckb`b0s`d;mDoC`kz;y}41J?fo3u4>? zd@pAh2D&ILL2n$Z=AN|7N~2kKyntKIdF#w`Xrs38xu@=7OUxS6Vy<8oMDwU};CQ0! zHkvhzi_+YzM0s<)SjRn_AqkWKF7}RcrNFmY>=o+qbx9N)p7A`sPYPuyWuysFb8-#2rr-Nq2-<2O7`Qf!MYcLnT- z>@=RI8S-)&xde)L1R?NXJ`c{>9GvNh;oK48at4*??uF=DE>BX2!k*Tf*RH~SSsrFy{N~PwP!bCkGjfNqH zMDY2|u8>%&ZUz34UkS}ocg)P>yqxLnmbS}*Y5Dty)fz}_Vmu~UFTK2rdOVjzfjpp% z8qEM%Q&h$H=G~}oe$jcS-d^-^ylf%sEK4C(v>eAxisj@SR~*c%@htFh-dn#`FC72$ z?yO~=`cLx={8-+~#`{P$>3wj+%cP1CG{~dN0($I4r49Fq*k>jp5MTo<#ryi+V$C^| zya}E6+Cgd4R5i?%nM1639GrD{&vt6lxMEXO{Yt;`dM5L6kuIIrI%Mf=?P2$*GmxTs xuGVxt-)0j3XuXF$tahNL_p#BB^XBl3USXkd&KUqT8aexD)4GoT^1SnZ*efKJYZL$g literal 0 HcmV?d00001 diff --git a/Dalamud/Interface/Utility/ImGuiHelpers.cs b/Dalamud/Interface/Utility/ImGuiHelpers.cs index 885f73067..b09b29411 100644 --- a/Dalamud/Interface/Utility/ImGuiHelpers.cs +++ b/Dalamud/Interface/Utility/ImGuiHelpers.cs @@ -16,6 +16,8 @@ using Dalamud.Interface.Utility.Raii; using ImGuiNET; using ImGuiScene; +using VirtualKey = Dalamud.Game.ClientState.Keys.VirtualKey; + namespace Dalamud.Interface.Utility; /// diff --git a/lib/ImGui.NET b/lib/ImGui.NET new file mode 160000 index 000000000..b104b3520 --- /dev/null +++ b/lib/ImGui.NET @@ -0,0 +1 @@ +Subproject commit b104b3520d500366a6ee34c743c20ccf112d7a6e diff --git a/lib/ImGuiScene b/lib/ImGuiScene deleted file mode 160000 index 2f37349ff..000000000 --- a/lib/ImGuiScene +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 2f37349ffd778561a1103a650683116c43edc86c