Skip to content

Instantly share code, notes, and snippets.

@marpe
Created November 13, 2022 12:59
Show Gist options
  • Save marpe/fb8c78b6fe9dbad8b99275a729d22165 to your computer and use it in GitHub Desktop.
Save marpe/fb8c78b6fe9dbad8b99275a729d22165 to your computer and use it in GitHub Desktop.
Moonworks ImGui renderer
#pragma warning disable CS8618
using System.Runtime.InteropServices;
using ImGuiNET;
using MyGame.Graphics;
using SDL2;
namespace MyGame.TWImGui;
public enum ImGuiFont
{
    Tiny,
    TinyBold,
    Small,
    SmallBold,
    Medium,
    MediumBold,
    Default,
}
public class ImGuiRenderer
{
    #region PlatformDelegates
    private delegate void Platform_SetWindowAlpha(ImGuiViewportPtr vp, float alpha);
    private Platform_CreateWindow _createWindow;
    private Platform_DestroyWindow _destroyWindow;
    private Platform_GetWindowPos _getWindowPos;
    private Platform_SetWindowPos _setWindowPos;
    private Platform_SetWindowSize _setWindowSize;
    private Platform_GetWindowSize _getWindowSize;
    private Platform_ShowWindow _showWindow;
    private Platform_SetWindowFocus _setWindowFocus;
    private Platform_GetWindowFocus _getWindowFocus;
    private Platform_GetWindowMinimized _getWindowMinimized;
    private Platform_SetWindowTitle _setWindowTitle;
    private Platform_SetWindowAlpha _setWindowAlpha;
    #endregion
    private readonly Dictionary<IntPtr, Texture> _textures = new();
    private readonly Dictionary<ImGuiFont, ImFontPtr> _fonts = new();
    private readonly Num.Vector2 _scaleFactor = Num.Vector2.One;
    private int _textureIdCounter;
    private IntPtr? _fontAtlasTextureId;
    private MoonWorks.Graphics.Buffer? _indexBuffer;
    private uint _indexBufferSize;
    private MoonWorks.Graphics.Buffer? _vertexBuffer;
    private uint _vertexBufferSize;
    public bool IsDisposed { get; private set; }
    private readonly Dictionary<ImGuiMouseCursor, IntPtr> _mouseCursors = new();
    private ImGuiMouseCursor _lastCursor = ImGuiMouseCursor.None;
    private readonly MyGameMain _game;
    private readonly Sampler _sampler;
    private GraphicsPipeline _pipeline;
    private Texture _renderTarget;
    public Texture RenderTarget => _renderTarget;
    public ColorAttachmentBlendState BlendState { get; private set; }
    public ImGuiRenderer(MyGameMain game)
    {
        _game = game;
        var context = ImGui.CreateContext();
        ImGui.SetCurrentContext(context);
        SetupInput();
        var io = ImGui.GetIO();
        io.ConfigFlags |= ImGuiConfigFlags.DockingEnable;
        io.ConfigFlags |= ImGuiConfigFlags.ViewportsEnable;
        io.ConfigFlags |= ImGuiConfigFlags.NavEnableKeyboard;
        io.BackendFlags |= ImGuiBackendFlags.HasMouseCursors;
        io.BackendFlags |= ImGuiBackendFlags.RendererHasVtxOffset;
        _mouseCursors.Add(ImGuiMouseCursor.Arrow, SDL.SDL_CreateSystemCursor(SDL.SDL_SystemCursor.SDL_SYSTEM_CURSOR_ARROW));
        _mouseCursors.Add(ImGuiMouseCursor.TextInput, SDL.SDL_CreateSystemCursor(SDL.SDL_SystemCursor.SDL_SYSTEM_CURSOR_IBEAM));
        _mouseCursors.Add(ImGuiMouseCursor.ResizeAll, SDL.SDL_CreateSystemCursor(SDL.SDL_SystemCursor.SDL_SYSTEM_CURSOR_SIZEALL));
        _mouseCursors.Add(ImGuiMouseCursor.ResizeNS, SDL.SDL_CreateSystemCursor(SDL.SDL_SystemCursor.SDL_SYSTEM_CURSOR_SIZENS));
        _mouseCursors.Add(ImGuiMouseCursor.ResizeEW, SDL.SDL_CreateSystemCursor(SDL.SDL_SystemCursor.SDL_SYSTEM_CURSOR_SIZEWE));
        _mouseCursors.Add(ImGuiMouseCursor.ResizeNESW, SDL.SDL_CreateSystemCursor(SDL.SDL_SystemCursor.SDL_SYSTEM_CURSOR_SIZENESW));
        _mouseCursors.Add(ImGuiMouseCursor.ResizeNWSE, SDL.SDL_CreateSystemCursor(SDL.SDL_SystemCursor.SDL_SYSTEM_CURSOR_SIZENWSE));
        _mouseCursors.Add(ImGuiMouseCursor.Hand, SDL.SDL_CreateSystemCursor(SDL.SDL_SystemCursor.SDL_SYSTEM_CURSOR_HAND));
        _mouseCursors.Add(ImGuiMouseCursor.NotAllowed, SDL.SDL_CreateSystemCursor(SDL.SDL_SystemCursor.SDL_SYSTEM_CURSOR_NO));
        _sampler = new Sampler(game.GraphicsDevice, SamplerCreateInfo.LinearClamp);
        BlendState = new ColorAttachmentBlendState()
        {
            BlendEnable = true,
            AlphaBlendOp = BlendOp.Add,
            ColorBlendOp = BlendOp.Add,
            ColorWriteMask = ColorComponentFlags.RGBA,
            SourceColorBlendFactor = BlendFactor.SourceAlpha,
            SourceAlphaBlendFactor = BlendFactor.One,
            DestinationColorBlendFactor = BlendFactor.OneMinusSourceAlpha,
            DestinationAlphaBlendFactor = BlendFactor.OneMinusSourceAlpha
        };
        _pipeline = SetupPipeline(game.GraphicsDevice, BlendState);
        var windowSize = game.MainWindow.Size;
        _renderTarget = Texture.CreateTexture2D(game.GraphicsDevice, (uint)windowSize.X, (uint)windowSize.Y, TextureFormat.B8G8R8A8,
            TextureUsageFlags.Sampler | TextureUsageFlags.ColorTarget);
        
        BuildFontAtlas();
        SetupMultiViewport(_game.MainWindow);
    }
    #region Setup
    private unsafe void SetupMultiViewport(Window mainWindow)
    {
        var platformIO = ImGui.GetPlatformIO();
        var mainViewport = ImGui.GetMainViewport();
        mainWindow.WindowEvent += HandleWindowEvent;
        mainViewport.PlatformHandle = mainWindow.Handle;
        var gcHandle = GCHandle.Alloc(mainWindow);
        mainViewport.PlatformUserData = (IntPtr)gcHandle;
        SDL.SDL_SysWMinfo info = new();
        SDL.SDL_VERSION(out info.version);
        if (SDL.SDL_bool.SDL_TRUE == SDL.SDL_GetWindowWMInfo(mainWindow.Handle, ref info))
        {
            mainViewport.PlatformHandleRaw = info.info.win.window;
        }
        _createWindow = CreateWindow;
        _destroyWindow = DestroyWindow;
        _getWindowPos = GetWindowPos;
        _setWindowPos = SetWindowPos;
        _setWindowSize = SetWindowSize;
        _getWindowSize = GetWindowSize;
        _showWindow = ShowWindow;
        _setWindowFocus = SetWindowFocus;
        _getWindowFocus = GetWindowFocus;
        _getWindowMinimized = GetWindowMinimized;
        _setWindowTitle = SetWindowTitle;
        _setWindowAlpha = SetWindowAlpha;
        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);
        platformIO.Platform_SetWindowAlpha = Marshal.GetFunctionPointerForDelegate(_setWindowAlpha);
        // don't know why these are required to be set by using the NativePtr directly.
        // but setting them like the others results in the callback being called with vp->PlatformUserData uninitialized.
        // see: https://github.com/mellinoe/ImGui.NET/blob/75f493683873af6ac9f57e5dba851546345fd2a5/src/ImGui.NET.SampleProgram/ImGuiController.cs#L113
        ImGuiNative.ImGuiPlatformIO_Set_Platform_GetWindowPos(platformIO.NativePtr, Marshal.GetFunctionPointerForDelegate(_getWindowPos));
        ImGuiNative.ImGuiPlatformIO_Set_Platform_GetWindowSize(platformIO.NativePtr, Marshal.GetFunctionPointerForDelegate(_getWindowSize));
        var io = ImGui.GetIO();
        io.ConfigFlags |= ImGuiConfigFlags.ViewportsEnable;
        io.ConfigDockingTransparentPayload = true;
        // io.ConfigViewportsNoAutoMerge = true;
        io.NativePtr->BackendPlatformName = (byte*)new FixedAsciiString("MoonWorks.SDL").DataPtr;
        io.BackendFlags |= ImGuiBackendFlags.HasMouseCursors;
        io.BackendFlags |= ImGuiBackendFlags.HasSetMousePos;
        io.BackendFlags |= ImGuiBackendFlags.PlatformHasViewports;
        io.BackendFlags |= ImGuiBackendFlags.RendererHasViewports;
        io.ConfigViewportsNoDecoration = true;
        // io.BackendFlags |= ImGuiBackendFlags.HasMouseHoveredViewport;
        SDL.SDL_SetHint(SDL.SDL_HINT_MOUSE_FOCUS_CLICKTHROUGH, "1");
        // SDL.SDL_SetHint(SDL.SDL_HINT_MOUSE_AUTO_CAPTURE, "0");
        UpdateMonitors();
    }
    private static GraphicsPipeline SetupPipeline(GraphicsDevice graphicsDevice, ColorAttachmentBlendState blendState)
    {
        var vertexShader = new ShaderModule(graphicsDevice, ContentPaths.Shaders.imgui.sprite_vert_spv);
        var fragmentShader =
            new ShaderModule(graphicsDevice, ContentPaths.Shaders.imgui.sprite_frag_spv);
        var myVertexBindings = new VertexBinding[]
        {
            VertexBinding.Create<PositionTextureColorVertex>()
        };
        var myVertexAttributes = new VertexAttribute[]
        {
            VertexAttribute.Create<PositionTextureColorVertex>(nameof(PositionTextureColorVertex.Position), 0),
            VertexAttribute.Create<PositionTextureColorVertex>(nameof(PositionTextureColorVertex.TexCoord), 1),
            VertexAttribute.Create<PositionTextureColorVertex>(nameof(PositionTextureColorVertex.Color), 2),
        };
        var myVertexInputState = new VertexInputState
        {
            VertexBindings = myVertexBindings,
            VertexAttributes = myVertexAttributes
        };
        var pipelineCreateInfo = new GraphicsPipelineCreateInfo
        {
            AttachmentInfo = new GraphicsPipelineAttachmentInfo(
                new ColorAttachmentDescription(TextureFormat.B8G8R8A8, blendState)
            ),
            DepthStencilState = DepthStencilState.Disable,
            VertexShaderInfo = GraphicsShaderInfo.Create<Matrix4x4>(vertexShader, "main", 0),
            FragmentShaderInfo = GraphicsShaderInfo.Create(fragmentShader, "main", 1),
            MultisampleState = MultisampleState.None,
            RasterizerState = RasterizerState.CCW_CullNone,
            PrimitiveType = PrimitiveType.TriangleList,
            VertexInputState = myVertexInputState,
        };
        return new GraphicsPipeline(
            graphicsDevice,
            pipelineCreateInfo
        );
    }
    private unsafe void BuildFontAtlas()
    {
        var sw = Stopwatch.StartNew();
        var io = ImGui.GetIO();
        var fa6IconRanges = stackalloc ushort[] { FontAwesome6.IconMin, FontAwesome6.IconMax, 0 };
        var fa6FontPath = Path.Combine(MyGameMain.ContentRoot, "fonts", FontAwesome6.FontIconFileName);
        ImFontPtr CreateFont(string fontPath, int fontSize, int iconFontSize)
        {
            var fontPtr = io.Fonts.AddFontFromFileTTF(fontPath, fontSize);
            var fontConfig = ImGuiNative.ImFontConfig_ImFontConfig();
            var fontConfigPtr = new ImFontConfigPtr(fontConfig);
            fontConfigPtr.MergeMode = true;
            fontConfigPtr.PixelSnapH = true;
            fontConfigPtr.GlyphMinAdvanceX = iconFontSize;
            fontConfigPtr.RasterizerMultiply = 1.5f;
            io.Fonts.AddFontFromFileTTF(fa6FontPath, iconFontSize, fontConfigPtr, (IntPtr)fa6IconRanges);
            fontConfigPtr.Destroy();
            return fontPtr;
        }
        var fontPath = ContentPaths.fonts.Roboto_Regular_ttf;
        var fontPathBold = ContentPaths.fonts.Roboto_Bold_ttf;
        foreach (var font in _fonts)
        {
            font.Value.Destroy();
        }
        _fonts.Clear();
        _fonts[ImGuiFont.Medium] = CreateFont(fontPath, 16, 14);
        /*_fonts[ImGuiFont.Default] = ImGui.GetIO().Fonts.AddFontDefault();
        _fonts[ImGuiFont.MediumBold] = CreateFont(fontPathBold, 16, 14);
        _fonts[ImGuiFont.Small] = CreateFont(fontPath, 14, 12);
        _fonts[ImGuiFont.SmallBold] = CreateFont(fontPathBold, 14, 12);
        _fonts[ImGuiFont.Tiny] = CreateFont(fontPath, 12, 12);
        _fonts[ImGuiFont.TinyBold] = CreateFont(fontPathBold, 12, 12);*/
        io.Fonts.GetTexDataAsRGBA32(out byte* pixelData, out var width, out var height, out var bytesPerPixel);
        var pixels = new byte[width * height * bytesPerPixel];
        Marshal.Copy(new IntPtr(pixelData), pixels, 0, pixels.Length);
        var fontAtlasTexture = Texture.CreateTexture2D(_game.GraphicsDevice, (uint)width, (uint)height, TextureFormat.R8G8B8A8,
            TextureUsageFlags.Sampler);
        var commandBuffer = _game.GraphicsDevice.AcquireCommandBuffer();
        commandBuffer.SetTextureData(fontAtlasTexture, pixels);
        _game.GraphicsDevice.Submit(commandBuffer);
        if (_fontAtlasTextureId.HasValue)
        {
            UnbindTexture(_fontAtlasTextureId.Value);
        }
        _fontAtlasTextureId = BindTexture(fontAtlasTexture);
        io.Fonts.SetTexID(_fontAtlasTextureId.Value);
        io.Fonts.ClearTexData();
        // io.NativePtr->FontDefault = _fonts[ImGuiFont.Default];
        Logger.LogInfo($"Build ImGui fonts in {sw.ElapsedMilliseconds} ms");
    }
    private void SetupInput()
    {
        var io = ImGui.GetIO();
        io.KeyMap[(int)ImGuiKey.Tab] = (int)KeyCode.Tab;
        io.KeyMap[(int)ImGuiKey.LeftArrow] = (int)KeyCode.Left;
        io.KeyMap[(int)ImGuiKey.RightArrow] = (int)KeyCode.Right;
        io.KeyMap[(int)ImGuiKey.UpArrow] = (int)KeyCode.Up;
        io.KeyMap[(int)ImGuiKey.DownArrow] = (int)KeyCode.Down;
        io.KeyMap[(int)ImGuiKey.PageUp] = (int)KeyCode.PageUp;
        io.KeyMap[(int)ImGuiKey.PageDown] = (int)KeyCode.PageDown;
        io.KeyMap[(int)ImGuiKey.Home] = (int)KeyCode.Home;
        io.KeyMap[(int)ImGuiKey.End] = (int)KeyCode.End;
        io.KeyMap[(int)ImGuiKey.Delete] = (int)KeyCode.Delete;
        io.KeyMap[(int)ImGuiKey.Space] = (int)KeyCode.Space;
        io.KeyMap[(int)ImGuiKey.Backspace] = (int)KeyCode.Backspace;
        io.KeyMap[(int)ImGuiKey.Enter] = (int)KeyCode.Return;
        io.KeyMap[(int)ImGuiKey.Escape] = (int)KeyCode.Escape;
        io.KeyMap[(int)ImGuiKey.A] = (int)KeyCode.A;
        io.KeyMap[(int)ImGuiKey.C] = (int)KeyCode.C;
        io.KeyMap[(int)ImGuiKey.V] = (int)KeyCode.V;
        io.KeyMap[(int)ImGuiKey.X] = (int)KeyCode.X;
        io.KeyMap[(int)ImGuiKey.Y] = (int)KeyCode.Y;
        io.KeyMap[(int)ImGuiKey.Z] = (int)KeyCode.Z;
    }
    #endregion
    #region Dispose
    public void Dispose()
    {
        Dispose(true);
        GC.SuppressFinalize(this);
    }
    protected virtual void Dispose(bool isDisposing)
    {
        if (IsDisposed)
            return;
        if (isDisposing)
        {
            ImGui.DestroyPlatformWindows();
            foreach (var texture in _textures)
            {
                texture.Value.Dispose();
            }
            _textures.Clear();
            foreach (var font in _fonts)
            {
                font.Value.Destroy();
            }
            _fonts.Clear();
            foreach (var cursor in _mouseCursors)
            {
                SDL.SDL_FreeCursor(cursor.Value);
            }
            _mouseCursors.Clear();
            _renderTarget.Dispose();
            _vertexBuffer?.Dispose();
            _indexBuffer?.Dispose();
            _sampler.Dispose();
            _pipeline.Dispose();
        }
        IsDisposed = true;
    }
    #endregion
    #region Rendering
    public void Begin()
    {
        if (IsDisposed)
            throw new ObjectDisposedException(nameof(ImGuiRenderer));
        ImGui.NewFrame();
    }
    public void End()
    {
        if (IsDisposed)
            throw new ObjectDisposedException(nameof(ImGuiRenderer));
        ImGui.Render();
        var windowSize = _game.MainWindow.Size;
        // SDL.SDL_Vulkan_GetDrawableSize(_game.MainWindow.Handle, out var width, out var height);
        TextureUtils.EnsureTextureSize(ref _renderTarget, _game.GraphicsDevice, (uint)windowSize.X, (uint)windowSize.Y);
        var commandBuffer = _game.GraphicsDevice.AcquireCommandBuffer();
        Render(commandBuffer, _renderTarget, ImGui.GetDrawData());
        _game.GraphicsDevice.Submit(commandBuffer);
        // Update and Render additional Platform Windows
        var io = ImGui.GetIO();
        if ((io.ConfigFlags & ImGuiConfigFlags.ViewportsEnable) != 0)
        {
            ImGui.UpdatePlatformWindows();
            var platformIO = ImGui.GetPlatformIO();
            for (var i = 1; i < platformIO.Viewports.Size; i++)
            {
                var vp = platformIO.Viewports[i];
                if ((vp.Flags & ImGuiViewportFlags.Minimized) != 0)
                    continue;
                var window = vp.Window();
                var windowCommandBuffer = _game.GraphicsDevice.AcquireCommandBuffer();
                var windowTexture = windowCommandBuffer.AcquireSwapchainTexture(window);
                if (windowTexture == null)
                {
                    Logger.LogError("Couldn't acquire swapchain texture");
                    continue;
                }
                Render(windowCommandBuffer, windowTexture, vp.DrawData);
                _game.GraphicsDevice.Submit(windowCommandBuffer);
            }
        }
    }
    private void Render(CommandBuffer commandBuffer, Texture swapchainTexture, ImDrawDataPtr drawData)
    {
        UpdateBuffers(commandBuffer, drawData);
        commandBuffer.BeginRenderPass(
            new ColorAttachmentInfo(swapchainTexture, Color.Transparent)
        );
        RenderDrawData(commandBuffer, drawData);
        commandBuffer.EndRenderPass();
    }
    private void UpdateBuffers(CommandBuffer commandBuffer, ImDrawDataPtr drawData)
    {
        var totalVtxBufferSize =
            (uint)(drawData.TotalVtxCount * Unsafe.SizeOf<PositionTextureColorVertex>()); // Unsafe.SizeOf<ImDrawVert>());
        if (totalVtxBufferSize > _vertexBufferSize)
        {
            _vertexBuffer?.Dispose();
            _vertexBufferSize = (uint)(drawData.TotalVtxCount * Unsafe.SizeOf<PositionTextureColorVertex>());
            _vertexBuffer = new MoonWorks.Graphics.Buffer(_game.GraphicsDevice, BufferUsageFlags.Vertex, _vertexBufferSize);
        }
        var totalIdxBufferSize = (uint)(drawData.TotalIdxCount * sizeof(ushort));
        if (totalIdxBufferSize > _indexBufferSize)
        {
            _indexBuffer?.Dispose();
            _indexBufferSize = (uint)(drawData.TotalIdxCount * sizeof(ushort));
            _indexBuffer = new MoonWorks.Graphics.Buffer(_game.GraphicsDevice, BufferUsageFlags.Index, _indexBufferSize);
        }
        var vtxOffset = 0u;
        var idxOffset = 0u;
        var vtxStride = Unsafe.SizeOf<PositionTextureColorVertex>();
        var idxStride = sizeof(ushort);
        for (var n = 0; n < drawData.CmdListsCount; n++)
        {
            var cmdList = drawData.CmdListsRange[n];
            var imVtxBufferSize = (uint)(cmdList.VtxBuffer.Size * vtxStride);
            var imIdxBufferSize = (uint)(cmdList.IdxBuffer.Size * idxStride);
            commandBuffer.SetBufferData(_vertexBuffer, cmdList.VtxBuffer.Data, vtxOffset, imVtxBufferSize);
            commandBuffer.SetBufferData(_indexBuffer, cmdList.IdxBuffer.Data, idxOffset, imIdxBufferSize);
            vtxOffset += imVtxBufferSize;
            idxOffset += imIdxBufferSize;
        }
    }
    private void RenderDrawData(CommandBuffer commandBuffer, ImDrawDataPtr drawData)
    {
        if (drawData.CmdListsCount == 0)
            return;
        commandBuffer.BindGraphicsPipeline(_pipeline);
        commandBuffer.SetViewport(new Viewport
        {
            X = 0,
            Y = 0,
            W = Math.Max(1, drawData.DisplaySize.X),
            H = Math.Max(1, drawData.DisplaySize.Y),
            MaxDepth = 1,
            MinDepth = 0
        });
        var viewProjectionMatrix = Matrix4x4.CreateOrthographicOffCenter(
            drawData.DisplayPos.X,
            drawData.DisplayPos.X + drawData.DisplaySize.X,
            drawData.DisplayPos.Y + drawData.DisplaySize.Y,
            drawData.DisplayPos.Y,
            -1f,
            1f
        );
        var vtxUniformsOffset = commandBuffer.PushVertexShaderUniforms(viewProjectionMatrix);
        commandBuffer.BindVertexBuffers(_vertexBuffer);
        commandBuffer.BindIndexBuffer(_indexBuffer, IndexElementSize.Sixteen);
        var vtxOffset = 0u;
        var idxOffset = 0u;
        // Will project scissor/clipping rectangles into framebuffer space
        var clipOffset = drawData.DisplayPos; // (0,0) unless using multi-viewports
        var clipScale = drawData.FramebufferScale; // (1,1) unless using retina display which are often (2,2)
        for (var n = 0; n < drawData.CmdListsCount; n++)
        {
            var cmdList = drawData.CmdListsRange[n];
            for (var cmdi = 0; cmdi < cmdList.CmdBuffer.Size; cmdi++)
            {
                var drawCmd = cmdList.CmdBuffer[cmdi];
                if (!_textures.ContainsKey(drawCmd.TextureId))
                {
                    throw new InvalidOperationException(
                        $"Could not find a texture with id '{drawCmd.TextureId}', check your bindings"
                    );
                }
                var textureSamplerBindings = new TextureSamplerBinding(_textures[drawCmd.TextureId], _sampler);
                commandBuffer.BindFragmentSamplers(textureSamplerBindings);
                // Project scissor/clipping rectangles into framebuffer space
                var clipMin = new Vector2(
                    (drawCmd.ClipRect.X - clipOffset.X) * clipScale.X,
                    (drawCmd.ClipRect.Y - clipOffset.Y) * clipScale.Y
                );
                var clipMax = new Vector2(
                    (drawCmd.ClipRect.Z - clipOffset.X) * clipScale.X,
                    (drawCmd.ClipRect.W - clipOffset.Y) * clipScale.Y
                );
                // Clamp to viewport as vkCmdSetScissor() won't accept values that are off bounds
                clipMin.X = Math.Max(0, clipMin.X);
                clipMin.Y = Math.Max(0, clipMin.Y);
                clipMax.X = Math.Min(drawData.DisplaySize.X, clipMax.X);
                clipMax.Y = Math.Min(drawData.DisplaySize.Y, clipMax.Y);
                if (clipMax.X <= clipMin.X || clipMax.Y <= clipMin.Y)
                    continue;
                // Apply scissor/clipping rectangle
                var scissor = new Rect()
                {
                    X = (int)clipMin.X,
                    Y = (int)clipMin.Y,
                    W = (int)(clipMax.X - clipMin.X),
                    H = (int)(clipMax.Y - clipMin.Y)
                };
                commandBuffer.SetScissor(scissor);
                commandBuffer.DrawIndexedPrimitives(
                    vtxOffset + drawCmd.VtxOffset,
                    idxOffset + drawCmd.IdxOffset,
                    drawCmd.ElemCount / 3,
                    vtxUniformsOffset,
                    0
                );
            }
            vtxOffset += (uint)cmdList.VtxBuffer.Size;
            idxOffset += (uint)cmdList.IdxBuffer.Size;
        }
    }
    #endregion
    #region Update
    
    public void Update(float deltaTimeInSeconds, in InputState inputState)
    {
        if (IsDisposed)
            throw new ObjectDisposedException(nameof(ImGuiRenderer));
        var io = ImGui.GetIO();
        var mainWindowSize = _game.MainWindow.Size;
        io.DisplaySize = new Num.Vector2(
            mainWindowSize.X / _scaleFactor.X,
            mainWindowSize.Y / _scaleFactor.Y
        );
        io.DisplayFramebufferScale = _scaleFactor;
        io.DeltaTime = deltaTimeInSeconds;
        UpdateInput(inputState);
        UpdateMouseCursor();
        UpdateMonitors();
    }
    private bool HandleWindowEvent(Window window, SDL.SDL_Event evt)
    {
        switch (evt.window.windowEvent)
        {
            case SDL.SDL_WindowEventID.SDL_WINDOWEVENT_ENTER:
                break;
            case SDL.SDL_WindowEventID.SDL_WINDOWEVENT_LEAVE:
                break;
            case SDL.SDL_WindowEventID.SDL_WINDOWEVENT_FOCUS_GAINED:
                ImGui.GetIO().AddFocusEvent(true);
                break;
            case SDL.SDL_WindowEventID.SDL_WINDOWEVENT_FOCUS_LOST:
                ImGui.GetIO().AddFocusEvent(false);
                break;
            case SDL.SDL_WindowEventID.SDL_WINDOWEVENT_RESIZED:
            {
                var viewport = ImGui.FindViewportByPlatformHandle(window.Handle);
                viewport.PlatformRequestResize = true;
            }
                break;
            case SDL.SDL_WindowEventID.SDL_WINDOWEVENT_MOVED:
            {
                var viewport = ImGui.FindViewportByPlatformHandle(window.Handle);
                viewport.PlatformRequestMove = true;
            }
                break;
            case SDL.SDL_WindowEventID.SDL_WINDOWEVENT_CLOSE:
            {
                var viewport = ImGui.FindViewportByPlatformHandle(window.Handle);
                viewport.PlatformRequestClose = true;
                if (window == _game.MainWindow)
                {
                    _game.Quit();
                }
            }
                break;
            default:
                // Logger.LogWarn($"Unhandled window event: {evt.window.windowEvent}");
                break;
        }
        return true;
    }
    private unsafe void UpdateMonitors()
    {
        var platformIO = ImGui.GetPlatformIO();
        Marshal.FreeHGlobal(platformIO.NativePtr->Monitors.Data);
        var numMonitors = SDL.SDL_GetNumVideoDisplays();
        var data = Marshal.AllocHGlobal(Unsafe.SizeOf<ImGuiPlatformMonitor>() * numMonitors);
        platformIO.NativePtr->Monitors = new ImVector(numMonitors, numMonitors, data);
        for (var i = 0; i < numMonitors; i++)
        {
            var result = SDL.SDL_GetDisplayUsableBounds(i, out var r);
            if (result < 0)
            {
                Logger.LogError($"SDL_GetDisplayUsableBounds failed: {SDL.SDL_GetError()}");
            }
            var monitor = platformIO.Monitors[i];
            monitor.DpiScale = 1f;
            monitor.MainPos = new Num.Vector2(r.x, r.y);
            monitor.MainSize = new Num.Vector2(r.w, r.h);
            monitor.WorkPos = new Num.Vector2(r.x, r.y);
            monitor.WorkSize = new Num.Vector2(r.w, r.h);
        }
    }
    private void UpdateMouseCursor()
    {
        var io = ImGui.GetIO();
        if ((io.ConfigFlags & ImGuiConfigFlags.NoMouseCursorChange) != 0)
        {
            return;
        }
        var cursor = ImGui.GetMouseCursor();
        if (_lastCursor == cursor)
        {
            return;
        }
        if (io.MouseDrawCursor || cursor == ImGuiMouseCursor.None)
        {
            SDL.SDL_ShowCursor((int)SDL.SDL_bool.SDL_FALSE);
        }
        else
        {
            SDL.SDL_ShowCursor((int)SDL.SDL_bool.SDL_TRUE);
            SDL.SDL_SetCursor(_mouseCursors[cursor]);
        }
        _lastCursor = cursor;
    }
    private void UpdateInput(in InputState input)
    {
        var io = ImGui.GetIO();
        for (var i = 0; i < io.KeysDown.Count; i++)
        {
            if (!Enum.IsDefined((KeyCode)i))
                continue;
            io.KeysDown[i] = InputState.IsKeyDown(input, (KeyCode)i);
        }
        io.KeyShift = InputState.IsAnyKeyDown(input, InputHandler.ShiftKeys);
        io.KeyCtrl = InputState.IsAnyKeyDown(input, InputHandler.ControlKeys);
        io.KeyAlt = InputState.IsAnyKeyDown(input, InputHandler.AltKeys);
        io.KeySuper = InputState.IsAnyKeyDown(input, InputHandler.MetaKeys);
        io.MousePos = new Num.Vector2(input.GlobalMousePosition.X, input.GlobalMousePosition.Y);
        // io.MousePos = new Num.Vector2(_game.Inputs.Mouse.X, _game.Inputs.Mouse.Y);
        io.MouseDown[0] = InputState.IsMouseButtonDown(input, MouseButtonCode.Left);
        io.MouseDown[1] = InputState.IsMouseButtonDown(input, MouseButtonCode.Right);
        io.MouseDown[2] = InputState.IsMouseButtonDown(input, MouseButtonCode.Middle);
        io.MouseWheel = input.MouseWheelDelta;
        for (var i = 0; i < input.NumTextInputChars; i++)
        {
            var c = input.TextInput[i];
            io.AddInputCharacter(c);
        }
    }
    #endregion
    #region Getters/Setters
    public void SetBlendState(ColorAttachmentBlendState blendState)
    {
        _pipeline.Dispose();
        BlendState = blendState;
        _pipeline = SetupPipeline(_game.GraphicsDevice, blendState);
    }
    public ImFontPtr GetFont(ImGuiFont font)
    {
        if (IsDisposed)
            throw new ObjectDisposedException(nameof(ImGuiRenderer));
        return _fonts[font];
    }
    public IntPtr BindTexture(Texture texture)
    {
        if (IsDisposed)
            throw new ObjectDisposedException(nameof(ImGuiRenderer));
        var id = new IntPtr(++_textureIdCounter);
        _textures.Add(id, texture);
        return id;
    }
    public void UnbindTexture(IntPtr textureId)
    {
        if (IsDisposed)
            throw new ObjectDisposedException(nameof(ImGuiRenderer));
        _textures.Remove(textureId);
    }
    #endregion
    #region PlatformWindowCallbacks
    private void CreateWindow(ImGuiViewportPtr vp)
    {
        var flags = SDL.SDL_WindowFlags.SDL_WINDOW_HIDDEN | SDL.SDL_WindowFlags.SDL_WINDOW_VULKAN;
        if ((vp.Flags & ImGuiViewportFlags.NoTaskBarIcon) != 0)
            flags |= SDL.SDL_WindowFlags.SDL_WINDOW_SKIP_TASKBAR;
        if ((vp.Flags & ImGuiViewportFlags.NoDecoration) != 0)
            flags |= SDL.SDL_WindowFlags.SDL_WINDOW_BORDERLESS;
        else
            flags |= SDL.SDL_WindowFlags.SDL_WINDOW_RESIZABLE;
        if ((vp.Flags & ImGuiViewportFlags.TopMost) != 0)
            flags |= SDL.SDL_WindowFlags.SDL_WINDOW_ALWAYS_ON_TOP;
        var windowCreateInfo = new WindowCreateInfo
        {
            WindowWidth = (uint)vp.Size.X,
            WindowHeight = (uint)vp.Size.Y,
            WindowTitle = "No Title Yet",
            ScreenMode = ScreenMode.Windowed,
            SystemResizable = true
        };
        var window = new Window(windowCreateInfo, flags);
        window.WindowEvent += HandleWindowEvent;
        window.SetWindowPosition((int)vp.Pos.X, (int)vp.Pos.Y);
        // claim window calls SDL_Vulkan_CreateSurface
        if (!_game.GraphicsDevice.ClaimWindow(window, window.PresentMode))
        {
            throw new SystemException("Could not claim window!");
        }
        var gcHandle = GCHandle.Alloc(window);
        vp.PlatformHandle = window.Handle;
        vp.PlatformUserData = (IntPtr)gcHandle;
        SDL.SDL_SysWMinfo info = new();
        SDL.SDL_VERSION(out info.version);
        if (SDL.SDL_bool.SDL_TRUE == SDL.SDL_GetWindowWMInfo(window.Handle, ref info))
        {
            vp.PlatformHandleRaw = info.info.win.window;
        }
    }
    private void DestroyWindow(ImGuiViewportPtr vp)
    {
        var gcHandle = GCHandle.FromIntPtr(vp.PlatformUserData);
        if (gcHandle.Target != null)
        {
            var window = (Window)gcHandle.Target;
            var title = SDL.SDL_GetWindowTitle(window.Handle);
            Logger.LogInfo($"Destroying window: {title}");
            window.WindowEvent -= HandleWindowEvent;
            if (window.Claimed)
                _game.GraphicsDevice.UnclaimWindow(window);
            if (!window.IsDisposed)
                window.Dispose();
        }
        gcHandle.Free();
        vp.PlatformUserData = IntPtr.Zero;
        vp.PlatformHandle = IntPtr.Zero;
        vp.PlatformHandleRaw = IntPtr.Zero;
    }
    private unsafe void GetWindowPos(ImGuiViewportPtr vp, Num.Vector2* outPos)
    {
        var window = vp.Window();
        SDL.SDL_GetWindowPosition(window.Handle, out var x, out var y);
        var pos = new Num.Vector2(x, y);
        *outPos = pos;
    }
    private void SetWindowPos(ImGuiViewportPtr vp, Num.Vector2 pos)
    {
        var window = vp.Window();
        SDL.SDL_SetWindowPosition(window.Handle, (int)pos.X, (int)pos.Y);
    }
    private void SetWindowSize(ImGuiViewportPtr vp, Num.Vector2 size)
    {
        var window = vp.Window();
        window.SetWindowSize((uint)size.X, (uint)size.Y);
    }
    private unsafe void GetWindowSize(ImGuiViewportPtr vp, Num.Vector2* outSize)
    {
        var window = vp.Window();
        var windowSize = window.Size;
        var size = new Num.Vector2(windowSize.X, windowSize.Y);
        *outSize = size;
    }
    private void ShowWindow(ImGuiViewportPtr vp)
    {
        var window = vp.Window();
        SDL.SDL_ShowWindow(window.Handle);
    }
    private void SetWindowFocus(ImGuiViewportPtr vp)
    {
        var window = vp.Window();
        SDL.SDL_RaiseWindow(window.Handle);
    }
    private byte GetWindowFocus(ImGuiViewportPtr vp)
    {
        var window = vp.Window();
        var flags = (SDL.SDL_WindowFlags)SDL.SDL_GetWindowFlags(window.Handle);
        return (flags & SDL.SDL_WindowFlags.SDL_WINDOW_INPUT_FOCUS) != 0 ? (byte)1 : (byte)0;
    }
    private byte GetWindowMinimized(ImGuiViewportPtr vp)
    {
        var window = vp.Window();
        return window.IsMinimized ? (byte)1 : (byte)0;
    }
    private unsafe void SetWindowTitle(ImGuiViewportPtr vp, IntPtr title)
    {
        var window = vp.Window();
        var titlePtr = (byte*)title;
        var count = 0;
        while (titlePtr[count] != 0)
        {
            count += 1;
        }
        var titleStr = Encoding.ASCII.GetString(titlePtr, count);
        SDL.SDL_SetWindowTitle(window.Handle, titleStr);
        Logger.LogInfo($"Created window: {titleStr}");
    }
    private void SetWindowAlpha(ImGuiViewportPtr vp, float alpha)
    {
        var window = vp.Window();
        SDL.SDL_SetWindowOpacity(window.Handle, alpha);
    }
    #endregion
}
public sealed class FixedAsciiString : IDisposable
{
    public IntPtr DataPtr { get; }
    public unsafe FixedAsciiString(string s)
    {
        var byteCount = Encoding.ASCII.GetByteCount(s);
        DataPtr = Marshal.AllocHGlobal(byteCount + 1);
        fixed (char* sPtr = s)
        {
            var end = Encoding.ASCII.GetBytes(sPtr, s.Length, (byte*)DataPtr, byteCount);
            ((byte*)DataPtr)[end] = 0;
        }
    }
    public void Dispose()
    {
        Marshal.FreeHGlobal(DataPtr);
    }
}
public static class ImGuiViewportPtrExt
{
    public static Window Window(this ImGuiViewportPtr vp)
    {
        return (Window)(GCHandle.FromIntPtr(vp.PlatformUserData).Target ?? throw new InvalidOperationException("UserData was null"));
    }
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment