// ImGuiController with docking and viewport support for MoonWorks/Refresh. // Based on the example im ImGui.NET and MoonWorksDearImGuiScaffold. using ImGuiNET; using Nerfed.Runtime.Graphics; using System.Numerics; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; namespace Nerfed.Runtime.Gui; internal class Controller : IDisposable { public event Action OnGui; private readonly string shaderContentPath = Path.Combine(System.AppContext.BaseDirectory, "Assets", "Shaders"); private readonly GraphicsDevice graphicsDevice; private readonly Window mainWindow; private readonly Color clearColor; private readonly Platform_CreateWindow createWindow; private readonly Platform_DestroyWindow destroyWindow; private readonly Platform_GetWindowPos getWindowPos; private readonly Platform_ShowWindow showWindow; private readonly Platform_SetWindowPos setWindowPos; private readonly Platform_SetWindowSize setWindowSize; private readonly Platform_GetWindowSize getWindowSize; private readonly Platform_SetWindowFocus setWindowFocus; private readonly Platform_GetWindowFocus getWindowFocus; private readonly Platform_GetWindowMinimized getWindowMinimized; private readonly Platform_SetWindowTitle setWindowTitle; private readonly ResourceUploader resourceUploader; private readonly GraphicsPipeline imGuiPipeline; private readonly Shader imGuiVertexShader; private readonly Shader imGuiFragmentShader; private readonly Sampler imGuiSampler; private readonly TextureStorage textureStorage = new TextureStorage(); private readonly Dictionary windows = new Dictionary(16); private Texture fontTexture = null; private uint vertexCount = 0; private uint indexCount = 0; private Graphics.Buffer imGuiVertexBuffer = null; private Graphics.Buffer imGuiIndexBuffer = null; private bool frameBegun = false; public Controller(GraphicsDevice graphicsDevice, Window mainWindow, Color clearColor, ImGuiConfigFlags configFlags = ImGuiConfigFlags.NavEnableKeyboard | ImGuiConfigFlags.DockingEnable | ImGuiConfigFlags.ViewportsEnable) { this.mainWindow = mainWindow; this.graphicsDevice = graphicsDevice; this.clearColor = clearColor; resourceUploader = new ResourceUploader(graphicsDevice); ImGui.CreateContext(); ImGuiIOPtr io = ImGui.GetIO(); io.DisplaySize = new Vector2(mainWindow.Width, mainWindow.Height); io.DisplayFramebufferScale = Vector2.One; ShaderCreateInfo vertexCreateInfo = new ShaderCreateInfo { ShaderStage = ShaderStage.Vertex, ShaderFormat = ShaderFormat.SPIRV, UniformBufferCount = 1, }; imGuiVertexShader = new Shader(graphicsDevice, Path.Combine(shaderContentPath, "imgui-vertex.spv"), "main", in vertexCreateInfo); ShaderCreateInfo fragCreateInfo = new ShaderCreateInfo { ShaderStage = ShaderStage.Fragment, ShaderFormat = ShaderFormat.SPIRV, SamplerCount = 1, }; imGuiFragmentShader = new Shader(graphicsDevice, Path.Combine(shaderContentPath, "imgui-frag.spv"), "main", in fragCreateInfo); imGuiSampler = new Sampler(graphicsDevice, SamplerCreateInfo.LinearClamp); imGuiPipeline = new GraphicsPipeline( graphicsDevice, new GraphicsPipelineCreateInfo { AttachmentInfo = new GraphicsPipelineAttachmentInfo( new ColorAttachmentDescription( mainWindow.SwapchainFormat, ColorAttachmentBlendState.NonPremultiplied ) ), DepthStencilState = DepthStencilState.Disable, MultisampleState = MultisampleState.None, PrimitiveType = PrimitiveType.TriangleList, RasterizerState = RasterizerState.CW_CullNone, VertexInputState = VertexInputState.CreateSingleBinding(), VertexShader = imGuiVertexShader, FragmentShader = imGuiFragmentShader, } ); BuildFontAtlas(); io.ConfigFlags = configFlags; //io.MouseDrawCursor = true; if (!OperatingSystem.IsWindows()) { io.SetClipboardTextFn = Clipboard.SetFnPtr; io.GetClipboardTextFn = Clipboard.GetFnPtr; } ImGuiPlatformIOPtr platformIO = ImGui.GetPlatformIO(); ImGuiViewportPtr mainViewport = platformIO.Viewports[0]; mainViewport.PlatformHandle = mainWindow.Handle; GCHandle handle = GCHandle.Alloc(mainWindow); mainViewport.PlatformUserData = (IntPtr)handle; windows.Add(mainWindow, handle); unsafe { createWindow = CreateWindow; destroyWindow = DestroyWindow; getWindowPos = GetWindowPos; showWindow = ShowWindow; setWindowPos = SetWindowPos; setWindowSize = SetWindowSize; getWindowSize = GetWindowSize; setWindowFocus = SetWindowFocus; getWindowFocus = GetWindowFocus; getWindowMinimized = GetWindowMinimized; setWindowTitle = SetWindowTitle; platformIO.Platform_CreateWindow = Marshal.GetFunctionPointerForDelegate(createWindow); platformIO.Platform_DestroyWindow = Marshal.GetFunctionPointerForDelegate(destroyWindow); platformIO.Platform_ShowWindow = Marshal.GetFunctionPointerForDelegate(showWindow); platformIO.Platform_SetWindowPos = Marshal.GetFunctionPointerForDelegate(setWindowPos); platformIO.Platform_SetWindowSize = Marshal.GetFunctionPointerForDelegate(setWindowSize); platformIO.Platform_SetWindowFocus = Marshal.GetFunctionPointerForDelegate(setWindowFocus); platformIO.Platform_GetWindowFocus = Marshal.GetFunctionPointerForDelegate(getWindowFocus); platformIO.Platform_GetWindowMinimized = Marshal.GetFunctionPointerForDelegate(getWindowMinimized); platformIO.Platform_SetWindowTitle = Marshal.GetFunctionPointerForDelegate(setWindowTitle); ImGuiNative.ImGuiPlatformIO_Set_Platform_GetWindowPos(platformIO.NativePtr, Marshal.GetFunctionPointerForDelegate(getWindowPos)); ImGuiNative.ImGuiPlatformIO_Set_Platform_GetWindowSize(platformIO.NativePtr, Marshal.GetFunctionPointerForDelegate(getWindowSize)); } //io.BackendFlags |= ImGuiBackendFlags.HasMouseCursors; io.BackendFlags |= ImGuiBackendFlags.HasSetMousePos; io.BackendFlags |= ImGuiBackendFlags.PlatformHasViewports; io.BackendFlags |= ImGuiBackendFlags.RendererHasViewports; } public void Update(float deltaTime) { if (frameBegun) { ImGui.Render(); ImGui.UpdatePlatformWindows(); } UpdatePerFrameImGuiData(deltaTime); UpdateInput(); UpdateMonitors(); frameBegun = true; ImGui.NewFrame(); OnGui?.Invoke(); { // Debug ImGuiIOPtr io = ImGui.GetIO(); ImGui.Text($"mouse pos: {io.MousePos}"); } ImGui.EndFrame(); } private void UpdatePerFrameImGuiData(float deltaSeconds) { ImGuiIOPtr io = ImGui.GetIO(); io.DisplaySize = new Vector2(mainWindow.Width, mainWindow.Height); io.DisplayFramebufferScale = new Vector2(1, 1); io.DeltaTime = deltaSeconds; // DeltaTime is in seconds. } private void UpdateInput() { ImGuiIOPtr io = ImGui.GetIO(); if ((ImGui.GetIO().ConfigFlags & ImGuiConfigFlags.ViewportsEnable) != 0) { // For viewports we use the global mouse position. _ = SDL2.SDL.SDL_GetGlobalMouseState(out int x, out int y); io.MousePos = new Vector2(x, y); } else { // Without viewports we need to use the relative position. //_ = SDL2.SDL.SDL_GetMouseState(out int x, out int y); io.MousePos = Mouse.Position; } io.MouseDown[0] = Mouse.IsButtonDown(MouseButton.Left); io.MouseDown[1] = Mouse.IsButtonDown(MouseButton.Right); io.MouseDown[2] = Mouse.IsButtonDown(MouseButton.Middle); io.MouseWheel = Mouse.GetWheel(); io.AddKeyEvent(ImGuiKey.A, Keyboard.IsKeyDown(Key.A)); io.AddKeyEvent(ImGuiKey.Z, Keyboard.IsKeyDown(Key.Z)); io.AddKeyEvent(ImGuiKey.Y, Keyboard.IsKeyDown(Key.Y)); io.AddKeyEvent(ImGuiKey.X, Keyboard.IsKeyDown(Key.X)); io.AddKeyEvent(ImGuiKey.C, Keyboard.IsKeyDown(Key.C)); io.AddKeyEvent(ImGuiKey.V, Keyboard.IsKeyDown(Key.V)); io.AddKeyEvent(ImGuiKey.Tab, Keyboard.IsKeyDown(Key.Tab)); io.AddKeyEvent(ImGuiKey.LeftArrow, Keyboard.IsKeyDown(Key.Left)); io.AddKeyEvent(ImGuiKey.RightArrow, Keyboard.IsKeyDown(Key.Right)); io.AddKeyEvent(ImGuiKey.UpArrow, Keyboard.IsKeyDown(Key.Up)); io.AddKeyEvent(ImGuiKey.DownArrow, Keyboard.IsKeyDown(Key.Down)); io.AddKeyEvent(ImGuiKey.Enter, Keyboard.IsKeyDown(Key.Enter)); io.AddKeyEvent(ImGuiKey.Escape, Keyboard.IsKeyDown(Key.Escape)); io.AddKeyEvent(ImGuiKey.Delete, Keyboard.IsKeyDown(Key.Delete)); io.AddKeyEvent(ImGuiKey.Backspace, Keyboard.IsKeyDown(Key.Backspace)); io.AddKeyEvent(ImGuiKey.Home, Keyboard.IsKeyDown(Key.Home)); io.AddKeyEvent(ImGuiKey.End, Keyboard.IsKeyDown(Key.End)); io.AddKeyEvent(ImGuiKey.PageDown, Keyboard.IsKeyDown(Key.PageDown)); io.AddKeyEvent(ImGuiKey.PageUp, Keyboard.IsKeyDown(Key.PageUp)); io.AddKeyEvent(ImGuiKey.Insert, Keyboard.IsKeyDown(Key.Insert)); io.AddKeyEvent(ImGuiKey.ModCtrl, Keyboard.IsKeyDown(Key.LeftControl) || Keyboard.IsKeyDown(Key.RightControl)); io.AddKeyEvent(ImGuiKey.ModShift, Keyboard.IsKeyDown(Key.LeftShift) || Keyboard.IsKeyDown(Key.RightShift)); io.AddKeyEvent(ImGuiKey.ModAlt, Keyboard.IsKeyDown(Key.LeftAlt) || Keyboard.IsKeyDown(Key.RightAlt)); io.AddKeyEvent(ImGuiKey.ModSuper, Keyboard.IsKeyDown(Key.LeftSuper) || Keyboard.IsKeyDown(Key.RightSuper)); ReadOnlySpan input = Keyboard.GetTextInput(); if (!input.IsEmpty) { foreach (char c in input) { if (c == '\t') { break; } io.AddInputCharacter(c); } } } private unsafe void UpdateMonitors() { ImGuiPlatformIOPtr platformIO = ImGui.GetPlatformIO(); Marshal.FreeHGlobal(platformIO.NativePtr->Monitors.Data); int videoDisplayCount = SDL2.SDL.SDL_GetNumVideoDisplays(); IntPtr data = Marshal.AllocHGlobal(Unsafe.SizeOf() * videoDisplayCount); platformIO.NativePtr->Monitors = new ImVector(videoDisplayCount, videoDisplayCount, data); for (int i = 0; i < videoDisplayCount; i++) { _ = SDL2.SDL.SDL_GetDisplayUsableBounds(i, out SDL2.SDL.SDL_Rect usableBounds); _ = SDL2.SDL.SDL_GetDisplayBounds(i, out SDL2.SDL.SDL_Rect bounds); _ = SDL2.SDL.SDL_GetDisplayDPI(i, out float ddpi, out float hdpi, out float vdpi); ImGuiPlatformMonitorPtr monitor = platformIO.Monitors[i]; float standardDpi = 96f; // Standard DPI typically used monitor.DpiScale = hdpi / standardDpi; monitor.MainPos = new Vector2(bounds.x, bounds.y); monitor.MainSize = new Vector2(bounds.w, bounds.h); monitor.WorkPos = new Vector2(usableBounds.x, usableBounds.y); monitor.WorkSize = new Vector2(usableBounds.w, usableBounds.h); } } public void Render() { if (!frameBegun) { return; } frameBegun = false; if ((ImGui.GetIO().ConfigFlags & ImGuiConfigFlags.ViewportsEnable) != 0) { ImGui.Render(); ImGui.UpdatePlatformWindows(); ImGuiPlatformIOPtr platformIO = ImGui.GetPlatformIO(); for (int i = 0; i < platformIO.Viewports.Size; i++) { ImGuiViewportPtr vp = platformIO.Viewports[i]; Window window = (Window)GCHandle.FromIntPtr(vp.PlatformUserData).Target; if (!window.Claimed) { continue; } UpdateImGuiBuffers(vp.DrawData); CommandBuffer commandBuffer = graphicsDevice.AcquireCommandBuffer(); Texture swapchainTexture = commandBuffer.AcquireSwapchainTexture(window); if (swapchainTexture != null) { RenderCommandLists(commandBuffer, swapchainTexture, vp.DrawData); graphicsDevice.Submit(commandBuffer); graphicsDevice.Wait(); } } } else { ImGui.Render(); if (!mainWindow.Claimed) { return; } ImDrawDataPtr drawDataPtr = ImGui.GetDrawData(); UpdateImGuiBuffers(drawDataPtr); CommandBuffer commandBuffer = graphicsDevice.AcquireCommandBuffer(); Texture swapchainTexture = commandBuffer.AcquireSwapchainTexture(mainWindow); if (swapchainTexture != null) { RenderCommandLists(commandBuffer, swapchainTexture, drawDataPtr); graphicsDevice.Submit(commandBuffer); graphicsDevice.Wait(); } } } private unsafe void UpdateImGuiBuffers(ImDrawDataPtr drawDataPtr) { if (drawDataPtr.TotalVtxCount == 0 || drawDataPtr.CmdListsCount == 0) { return; } if (drawDataPtr.TotalVtxCount > vertexCount) { imGuiVertexBuffer?.Dispose(); vertexCount = (uint)(drawDataPtr.TotalVtxCount * 1.5f); imGuiVertexBuffer = Graphics.Buffer.Create( graphicsDevice, BufferUsageFlags.Vertex, vertexCount ); } if (drawDataPtr.TotalIdxCount > indexCount) { imGuiIndexBuffer?.Dispose(); indexCount = (uint)(drawDataPtr.TotalIdxCount * 1.5f); imGuiIndexBuffer = Graphics.Buffer.Create( graphicsDevice, BufferUsageFlags.Index, indexCount ); } uint vertexOffset = 0; uint indexOffset = 0; for (int n = 0; n < drawDataPtr.CmdListsCount; n++) { ImDrawListPtr cmdList = drawDataPtr.CmdLists[n]; resourceUploader.SetBufferData( imGuiVertexBuffer, vertexOffset, new Span(cmdList.VtxBuffer.Data.ToPointer(), cmdList.VtxBuffer.Size), n == 0 ); resourceUploader.SetBufferData( imGuiIndexBuffer, indexOffset, new Span(cmdList.IdxBuffer.Data.ToPointer(), cmdList.IdxBuffer.Size), n == 0 ); vertexOffset += (uint)cmdList.VtxBuffer.Size; indexOffset += (uint)cmdList.IdxBuffer.Size; } resourceUploader.Upload(); } private void RenderCommandLists(CommandBuffer commandBuffer, Texture renderTexture, ImDrawDataPtr drawDataPtr) { Vector2 pos = drawDataPtr.DisplayPos; RenderPass renderPass = commandBuffer.BeginRenderPass( new ColorAttachmentInfo(renderTexture, false, clearColor) ); renderPass.BindGraphicsPipeline(imGuiPipeline); // It is possible that the buffers are null (for example nothing is in our main windows viewport, then we exixt early but still clear it). if (imGuiVertexBuffer == null || imGuiIndexBuffer == null) { commandBuffer.EndRenderPass(renderPass); return; } Matrix4x4 projectionMatrix = Matrix4x4.CreateOrthographicOffCenter( pos.X, pos.X + drawDataPtr.DisplaySize.X, pos.Y + drawDataPtr.DisplaySize.Y, pos.Y, -1.0f, 1.0f ); TransformVertexUniform vertexUniform = new TransformVertexUniform(projectionMatrix); renderPass.BindVertexBuffer(imGuiVertexBuffer); renderPass.BindIndexBuffer(imGuiIndexBuffer, IndexElementSize.Sixteen); commandBuffer.PushVertexUniformData(in vertexUniform); uint vertexOffset = 0; uint indexOffset = 0; for (int n = 0; n < drawDataPtr.CmdListsCount; n++) { ImDrawListPtr cmdList = drawDataPtr.CmdLists[n]; for (int cmdIndex = 0; cmdIndex < cmdList.CmdBuffer.Size; cmdIndex++) { ImDrawCmdPtr drawCmd = cmdList.CmdBuffer[cmdIndex]; Texture texture = textureStorage.GetTexture(drawCmd.TextureId); if (texture == null) { Log.Error("Texture or drawCmd.TextureId became null. Fit it!"); continue; } renderPass.BindFragmentSampler(new TextureSamplerBinding(texture, imGuiSampler)); float width = drawCmd.ClipRect.Z - (int)drawCmd.ClipRect.X; float height = drawCmd.ClipRect.W - (int)drawCmd.ClipRect.Y; if (width <= 0 || height <= 0) { continue; } renderPass.SetScissor( new Rect( (int)drawCmd.ClipRect.X - (int)pos.X, (int)drawCmd.ClipRect.Y - (int)pos.Y, (int)drawCmd.ClipRect.Z - (int)drawCmd.ClipRect.X, (int)drawCmd.ClipRect.W - (int)drawCmd.ClipRect.Y ) ); renderPass.DrawIndexedPrimitives(vertexOffset, indexOffset, drawCmd.ElemCount / 3); indexOffset += drawCmd.ElemCount; } vertexOffset += (uint)cmdList.VtxBuffer.Size; } commandBuffer.EndRenderPass(renderPass); } private unsafe void BuildFontAtlas() { ResourceUploader resourceUploader = new ResourceUploader(graphicsDevice); ImGuiIOPtr io = ImGui.GetIO(); io.Fonts.GetTexDataAsRGBA32( out nint pixelData, out int width, out int height, out int bytesPerPixel ); Texture fontTexture = resourceUploader.CreateTexture2D( new Span((void*)pixelData, width * height * bytesPerPixel), (uint)width, (uint)height ); resourceUploader.Upload(); resourceUploader.Dispose(); io.Fonts.SetTexID(fontTexture.Handle); io.Fonts.ClearTexData(); textureStorage.Add(fontTexture); // <-- The fontTexture seems to get lost after some time (CG?). this.fontTexture = fontTexture; // <-- So we also keep a reference to make sure it doesn't happen. } #region Window private void CreateWindow(ImGuiViewportPtr vp) { WindowCreateInfo info = new WindowCreateInfo("Window Title", (uint)vp.Pos.X, (uint)vp.Pos.Y, ScreenMode.Windowed); Window window = new Window(graphicsDevice, info); graphicsDevice.ClaimWindow(window, SwapchainComposition.SDR, PresentMode.Immediate); // What PresentMode do we need? GCHandle handle = GCHandle.Alloc(window); vp.PlatformUserData = (IntPtr)handle; windows.Add(window, handle); } private void DestroyWindow(ImGuiViewportPtr vp) { if (vp.PlatformUserData == IntPtr.Zero) return; Window window = (Window)GCHandle.FromIntPtr(vp.PlatformUserData).Target; graphicsDevice.UnclaimWindow(window); if (windows.TryGetValue(window, out GCHandle handle)) { handle.Free(); windows.Remove(window); } graphicsDevice.Wait(); window.Dispose(); } private void ShowWindow(ImGuiViewportPtr vp) { if (vp.PlatformUserData == IntPtr.Zero) return; Window window = (Window)GCHandle.FromIntPtr(vp.PlatformUserData).Target; SDL2.SDL.SDL_ShowWindow(window.Handle); } private unsafe void GetWindowPos(ImGuiViewportPtr vp, Vector2* outPos) { if (vp.PlatformUserData == IntPtr.Zero) return; Window window = (Window)GCHandle.FromIntPtr(vp.PlatformUserData).Target; SDL2.SDL.SDL_GetWindowPosition(window.Handle, out int x, out int y); *outPos = new Vector2(x, y); } private void SetWindowPos(ImGuiViewportPtr vp, Vector2 pos) { if (vp.PlatformUserData == IntPtr.Zero) return; Window window = (Window)GCHandle.FromIntPtr(vp.PlatformUserData).Target; SDL2.SDL.SDL_SetWindowPosition(window.Handle, (int)pos.X, (int)pos.Y); } private void SetWindowSize(ImGuiViewportPtr vp, Vector2 size) { if (vp.PlatformUserData == IntPtr.Zero) return; Window window = (Window)GCHandle.FromIntPtr(vp.PlatformUserData).Target; SDL2.SDL.SDL_SetWindowSize(window.Handle, (int)size.X, (int)size.Y); } private unsafe void GetWindowSize(ImGuiViewportPtr vp, Vector2* outSize) { if (vp.PlatformUserData == IntPtr.Zero) return; Window window = (Window)GCHandle.FromIntPtr(vp.PlatformUserData).Target; SDL2.SDL.SDL_GetWindowSize(window.Handle, out int w, out int h); *outSize = new Vector2(w, h); } private void SetWindowFocus(ImGuiViewportPtr vp) { if (vp.PlatformUserData == IntPtr.Zero) return; Window window = (Window)GCHandle.FromIntPtr(vp.PlatformUserData).Target; //SDL2.SDL.SDL_SetWindowInputFocus(window.Handle); SDL2.SDL.SDL_RaiseWindow(window.Handle); } private byte GetWindowFocus(ImGuiViewportPtr vp) { if (vp.PlatformUserData == IntPtr.Zero) return (byte)0; Window window = (Window)GCHandle.FromIntPtr(vp.PlatformUserData).Target; SDL2.SDL.SDL_WindowFlags flags = (SDL2.SDL.SDL_WindowFlags)SDL2.SDL.SDL_GetWindowFlags(window.Handle); return (flags & SDL2.SDL.SDL_WindowFlags.SDL_WINDOW_INPUT_FOCUS) != 0 ? (byte)1 : (byte)0; } private byte GetWindowMinimized(ImGuiViewportPtr vp) { if (vp.PlatformUserData == IntPtr.Zero) return (byte)0; Window window = (Window)GCHandle.FromIntPtr(vp.PlatformUserData).Target; SDL2.SDL.SDL_WindowFlags flags = (SDL2.SDL.SDL_WindowFlags)SDL2.SDL.SDL_GetWindowFlags(window.Handle); return (flags & SDL2.SDL.SDL_WindowFlags.SDL_WINDOW_MINIMIZED) != 0 ? (byte)1 : (byte)0; } private unsafe void SetWindowTitle(ImGuiViewportPtr vp, IntPtr title) { if (vp.PlatformUserData == IntPtr.Zero) return; Window window = (Window)GCHandle.FromIntPtr(vp.PlatformUserData).Target; byte* titlePtr = (byte*)title; int count = 0; while (titlePtr[count] != 0) { count += 1; } SDL2.SDL.SDL_SetWindowTitle(window.Handle, System.Text.Encoding.ASCII.GetString(titlePtr, count)); } #endregion public void Dispose() { fontTexture?.Dispose(); imGuiVertexBuffer?.Dispose(); imGuiIndexBuffer?.Dispose(); imGuiFragmentShader?.Dispose(); imGuiVertexShader?.Dispose(); imGuiPipeline?.Dispose(); imGuiSampler?.Dispose(); resourceUploader?.Dispose(); foreach (KeyValuePair window in windows) { if (window.Key == mainWindow) continue; graphicsDevice.UnclaimWindow(window.Key); window.Key.Dispose(); window.Value.Free(); } windows.Clear(); } }