using System; using System.Collections.Generic; using System.Numerics; using System.Reflection; using System.IO; using Veldrid; namespace ImGuiNET { /// /// A modified version of Veldrid.ImGui's ImGuiRenderer. /// Manages input for ImGui and handles rendering ImGui's DrawLists with Veldrid. /// public class ImGuiController : IDisposable { private GraphicsDevice _gd; private bool _frameBegun; // Veldrid objects private DeviceBuffer _vertexBuffer; private DeviceBuffer _indexBuffer; private DeviceBuffer _projMatrixBuffer; private Texture _fontTexture; private TextureView _fontTextureView; private Shader _vertexShader; private Shader _fragmentShader; private ResourceLayout _layout; private ResourceLayout _textureLayout; private Pipeline _pipeline; private ResourceSet _mainResourceSet; private ResourceSet _fontTextureResourceSet; private IntPtr _fontAtlasID = (IntPtr)1; private bool _controlDown; private bool _shiftDown; private bool _altDown; private int _windowWidth; private int _windowHeight; private Vector2 _scaleFactor = Vector2.One; // Image trackers private readonly Dictionary _setsByView = new Dictionary(); private readonly Dictionary _autoViewsByTexture = new Dictionary(); private readonly Dictionary _viewsById = new Dictionary(); private readonly List _ownedResources = new List(); private int _lastAssignedID = 100; /// /// Constructs a new ImGuiRenderer. /// public ImGuiController(GraphicsDevice gd, OutputDescription outputDescription, int width, int height) { _gd = gd; _windowWidth = width; _windowHeight = height; ImGui.GetIO().FontAtlas.AddDefaultFont(); CreateDeviceResources(gd, outputDescription); SetOpenTKKeyMappings(); SetPerFrameImGuiData(1f / 60f); ImGui.NewFrame(); _frameBegun = true; } public void WindowResized(int width, int height) { _windowWidth = width; _windowHeight = height; } public void DestroyDeviceObjects() { Dispose(); } public void CreateDeviceResources(GraphicsDevice gd, OutputDescription outputDescription) { _gd = gd; ResourceFactory factory = gd.ResourceFactory; _vertexBuffer = factory.CreateBuffer(new BufferDescription(10000, BufferUsage.VertexBuffer | BufferUsage.Dynamic)); _vertexBuffer.Name = "ImGui.NET Vertex Buffer"; _indexBuffer = factory.CreateBuffer(new BufferDescription(2000, BufferUsage.IndexBuffer | BufferUsage.Dynamic)); _indexBuffer.Name = "ImGui.NET Index Buffer"; RecreateFontDeviceTexture(gd); _projMatrixBuffer = factory.CreateBuffer(new BufferDescription(64, BufferUsage.UniformBuffer | BufferUsage.Dynamic)); _projMatrixBuffer.Name = "ImGui.NET Projection Buffer"; byte[] vertexShaderBytes = LoadEmbeddedShaderCode(gd.ResourceFactory, "imgui-vertex", ShaderStages.Vertex); byte[] fragmentShaderBytes = LoadEmbeddedShaderCode(gd.ResourceFactory, "imgui-frag", ShaderStages.Fragment); _vertexShader = factory.CreateShader(new ShaderDescription(ShaderStages.Vertex, vertexShaderBytes, "VS")); _fragmentShader = factory.CreateShader(new ShaderDescription(ShaderStages.Fragment, fragmentShaderBytes, "FS")); VertexLayoutDescription[] vertexLayouts = new VertexLayoutDescription[] { new VertexLayoutDescription( new VertexElementDescription("in_position", VertexElementSemantic.Position, VertexElementFormat.Float2), new VertexElementDescription("in_texCoord", VertexElementSemantic.TextureCoordinate, VertexElementFormat.Float2), new VertexElementDescription("in_color", VertexElementSemantic.Color, VertexElementFormat.Byte4_Norm)) }; _layout = factory.CreateResourceLayout(new ResourceLayoutDescription( new ResourceLayoutElementDescription("ProjectionMatrixBuffer", ResourceKind.UniformBuffer, ShaderStages.Vertex), new ResourceLayoutElementDescription("MainSampler", ResourceKind.Sampler, ShaderStages.Fragment))); _textureLayout = factory.CreateResourceLayout(new ResourceLayoutDescription( new ResourceLayoutElementDescription("MainTexture", ResourceKind.TextureReadOnly, ShaderStages.Fragment))); GraphicsPipelineDescription pd = new GraphicsPipelineDescription( BlendStateDescription.SingleAlphaBlend, new DepthStencilStateDescription(false, false, ComparisonKind.Always), new RasterizerStateDescription(FaceCullMode.None, PolygonFillMode.Solid, FrontFace.Clockwise, false, true), PrimitiveTopology.TriangleList, new ShaderSetDescription(vertexLayouts, new[] { _vertexShader, _fragmentShader }), new ResourceLayout[] { _layout, _textureLayout }, outputDescription); _pipeline = factory.CreateGraphicsPipeline(ref pd); _mainResourceSet = factory.CreateResourceSet(new ResourceSetDescription(_layout, _projMatrixBuffer, gd.PointSampler)); _fontTextureResourceSet = factory.CreateResourceSet(new ResourceSetDescription(_textureLayout, _fontTextureView)); } /// /// Gets or creates a handle for a texture to be drawn with ImGui. /// Pass the returned handle to Image() or ImageButton(). /// public IntPtr GetOrCreateImGuiBinding(ResourceFactory factory, TextureView textureView) { if (!_setsByView.TryGetValue(textureView, out ResourceSetInfo rsi)) { ResourceSet resourceSet = factory.CreateResourceSet(new ResourceSetDescription(_textureLayout, textureView)); rsi = new ResourceSetInfo(GetNextImGuiBindingID(), resourceSet); _setsByView.Add(textureView, rsi); _viewsById.Add(rsi.ImGuiBinding, rsi); _ownedResources.Add(resourceSet); } return rsi.ImGuiBinding; } private IntPtr GetNextImGuiBindingID() { int newID = _lastAssignedID++; return (IntPtr)newID; } /// /// Gets or creates a handle for a texture to be drawn with ImGui. /// Pass the returned handle to Image() or ImageButton(). /// public IntPtr GetOrCreateImGuiBinding(ResourceFactory factory, Texture texture) { if (!_autoViewsByTexture.TryGetValue(texture, out TextureView textureView)) { textureView = factory.CreateTextureView(texture); _autoViewsByTexture.Add(texture, textureView); _ownedResources.Add(textureView); } return GetOrCreateImGuiBinding(factory, textureView); } /// /// Retrieves the shader texture binding for the given helper handle. /// public ResourceSet GetImageResourceSet(IntPtr imGuiBinding) { if (!_viewsById.TryGetValue(imGuiBinding, out ResourceSetInfo tvi)) { throw new InvalidOperationException("No registered ImGui binding with id " + imGuiBinding.ToString()); } return tvi.ResourceSet; } public void ClearCachedImageResources() { foreach (IDisposable resource in _ownedResources) { resource.Dispose(); } _ownedResources.Clear(); _setsByView.Clear(); _viewsById.Clear(); _autoViewsByTexture.Clear(); _lastAssignedID = 100; } private byte[] LoadEmbeddedShaderCode(ResourceFactory factory, string name, ShaderStages stage) { switch (factory.BackendType) { case GraphicsBackend.Direct3D11: { string resourceName = name + ".hlsl.bytes"; return GetEmbeddedResourceBytes(resourceName); } case GraphicsBackend.OpenGL: { string resourceName = name + ".glsl"; return GetEmbeddedResourceBytes(resourceName); } case GraphicsBackend.Vulkan: { string resourceName = name + ".spv"; return GetEmbeddedResourceBytes(resourceName); } case GraphicsBackend.Metal: { string resourceName = name + ".metallib"; return GetEmbeddedResourceBytes(resourceName); } default: throw new NotImplementedException(); } } private byte[] GetEmbeddedResourceBytes(string resourceName) { Assembly assembly = typeof(ImGuiController).Assembly; using (Stream s = assembly.GetManifestResourceStream(resourceName)) { byte[] ret = new byte[s.Length]; s.Read(ret, 0, (int)s.Length); return ret; } } /// /// Recreates the device texture used to render text. /// public unsafe void RecreateFontDeviceTexture(GraphicsDevice gd) { IO io = ImGui.GetIO(); // Build FontTextureData textureData = io.FontAtlas.GetTexDataAsRGBA32(); // Store our identifier io.FontAtlas.SetTexID(_fontAtlasID); _fontTexture = gd.ResourceFactory.CreateTexture(TextureDescription.Texture2D( (uint)textureData.Width, (uint)textureData.Height, 1, 1, PixelFormat.R8_G8_B8_A8_UNorm, TextureUsage.Sampled)); _fontTexture.Name = "ImGui.NET Font Texture"; gd.UpdateTexture( _fontTexture, (IntPtr)textureData.Pixels, (uint)(textureData.BytesPerPixel * textureData.Width * textureData.Height), 0, 0, 0, (uint)textureData.Width, (uint)textureData.Height, 1, 0, 0); _fontTextureView = gd.ResourceFactory.CreateTextureView(_fontTexture); io.FontAtlas.ClearTexData(); } /// /// Renders the ImGui draw list data. /// This method requires a because it may create new DeviceBuffers if the size of vertex /// or index data has increased beyond the capacity of the existing buffers. /// A is needed to submit drawing and resource update commands. /// public unsafe void Render(GraphicsDevice gd, CommandList cl) { if (_frameBegun) { _frameBegun = false; ImGui.Render(); RenderImDrawData(ImGui.GetDrawData(), gd, cl); } } /// /// Updates ImGui input and IO configuration state. /// public void Update(float deltaSeconds, InputSnapshot snapshot) { if (_frameBegun) { ImGui.Render(); } SetPerFrameImGuiData(deltaSeconds); UpdateImGuiInput(snapshot); _frameBegun = true; ImGui.NewFrame(); } /// /// Sets per-frame data based on the associated window. /// This is called by Update(float). /// private unsafe void SetPerFrameImGuiData(float deltaSeconds) { IO io = ImGui.GetIO(); io.DisplaySize = new Vector2( _windowWidth / _scaleFactor.X, _windowHeight / _scaleFactor.Y); io.DisplayFramebufferScale = _scaleFactor; io.DeltaTime = deltaSeconds; // DeltaTime is in seconds. } private unsafe void UpdateImGuiInput(InputSnapshot snapshot) { IO io = ImGui.GetIO(); Vector2 mousePosition = snapshot.MousePosition; io.MousePosition = mousePosition; io.MouseDown[0] = snapshot.IsMouseDown(MouseButton.Left); io.MouseDown[1] = snapshot.IsMouseDown(MouseButton.Right); io.MouseDown[2] = snapshot.IsMouseDown(MouseButton.Middle); float delta = snapshot.WheelDelta; io.MouseWheel = delta; ImGui.GetIO().MouseWheel = delta; IReadOnlyList keyCharPresses = snapshot.KeyCharPresses; for (int i = 0; i < keyCharPresses.Count; i++) { char c = keyCharPresses[i]; ImGui.AddInputCharacter(c); } IReadOnlyList keyEvents = snapshot.KeyEvents; for (int i = 0; i < keyEvents.Count; i++) { KeyEvent keyEvent = keyEvents[i]; io.KeysDown[(int)keyEvent.Key] = keyEvent.Down; if (keyEvent.Key == Key.ControlLeft) { _controlDown = keyEvent.Down; } if (keyEvent.Key == Key.ShiftLeft) { _shiftDown = keyEvent.Down; } if (keyEvent.Key == Key.AltLeft) { _altDown = keyEvent.Down; } } io.CtrlPressed = _controlDown; io.AltPressed = _altDown; io.ShiftPressed = _shiftDown; } private static unsafe void SetOpenTKKeyMappings() { IO io = ImGui.GetIO(); io.KeyMap[GuiKey.Tab] = (int)Key.Tab; io.KeyMap[GuiKey.LeftArrow] = (int)Key.Left; io.KeyMap[GuiKey.RightArrow] = (int)Key.Right; io.KeyMap[GuiKey.UpArrow] = (int)Key.Up; io.KeyMap[GuiKey.DownArrow] = (int)Key.Down; io.KeyMap[GuiKey.PageUp] = (int)Key.PageUp; io.KeyMap[GuiKey.PageDown] = (int)Key.PageDown; io.KeyMap[GuiKey.Home] = (int)Key.Home; io.KeyMap[GuiKey.End] = (int)Key.End; io.KeyMap[GuiKey.Delete] = (int)Key.Delete; io.KeyMap[GuiKey.Backspace] = (int)Key.BackSpace; io.KeyMap[GuiKey.Enter] = (int)Key.Enter; io.KeyMap[GuiKey.Escape] = (int)Key.Escape; io.KeyMap[GuiKey.A] = (int)Key.A; io.KeyMap[GuiKey.C] = (int)Key.C; io.KeyMap[GuiKey.V] = (int)Key.V; io.KeyMap[GuiKey.X] = (int)Key.X; io.KeyMap[GuiKey.Y] = (int)Key.Y; io.KeyMap[GuiKey.Z] = (int)Key.Z; } private unsafe void RenderImDrawData(DrawData* draw_data, GraphicsDevice gd, CommandList cl) { uint vertexOffsetInVertices = 0; uint indexOffsetInElements = 0; if (draw_data->CmdListsCount == 0) { return; } uint totalVBSize = (uint)(draw_data->TotalVtxCount * sizeof(DrawVert)); if (totalVBSize > _vertexBuffer.SizeInBytes) { gd.DisposeWhenIdle(_vertexBuffer); _vertexBuffer = gd.ResourceFactory.CreateBuffer(new BufferDescription((uint)(totalVBSize * 1.5f), BufferUsage.VertexBuffer | BufferUsage.Dynamic)); } uint totalIBSize = (uint)(draw_data->TotalIdxCount * sizeof(ushort)); if (totalIBSize > _indexBuffer.SizeInBytes) { gd.DisposeWhenIdle(_indexBuffer); _indexBuffer = gd.ResourceFactory.CreateBuffer(new BufferDescription((uint)(totalIBSize * 1.5f), BufferUsage.IndexBuffer | BufferUsage.Dynamic)); } for (int i = 0; i < draw_data->CmdListsCount; i++) { NativeDrawList* cmd_list = draw_data->CmdLists[i]; cl.UpdateBuffer( _vertexBuffer, vertexOffsetInVertices * (uint)sizeof(DrawVert), (IntPtr)cmd_list->VtxBuffer.Data, (uint)(cmd_list->VtxBuffer.Size * sizeof(DrawVert))); cl.UpdateBuffer( _indexBuffer, indexOffsetInElements * (uint)sizeof(ushort), (IntPtr)cmd_list->IdxBuffer.Data, (uint)(cmd_list->IdxBuffer.Size * sizeof(ushort))); vertexOffsetInVertices += (uint)cmd_list->VtxBuffer.Size; indexOffsetInElements += (uint)cmd_list->IdxBuffer.Size; } // Setup orthographic projection matrix into our constant buffer { IO io = ImGui.GetIO(); Matrix4x4 mvp = Matrix4x4.CreateOrthographicOffCenter( 0f, io.DisplaySize.X, io.DisplaySize.Y, 0.0f, -1.0f, 1.0f); _gd.UpdateBuffer(_projMatrixBuffer, 0, ref mvp); } cl.SetVertexBuffer(0, _vertexBuffer); cl.SetIndexBuffer(_indexBuffer, IndexFormat.UInt16); cl.SetPipeline(_pipeline); cl.SetGraphicsResourceSet(0, _mainResourceSet); ImGui.ScaleClipRects(draw_data, ImGui.GetIO().DisplayFramebufferScale); // Render command lists int vtx_offset = 0; int idx_offset = 0; for (int n = 0; n < draw_data->CmdListsCount; n++) { NativeDrawList* cmd_list = draw_data->CmdLists[n]; for (int cmd_i = 0; cmd_i < cmd_list->CmdBuffer.Size; cmd_i++) { DrawCmd* pcmd = &(((DrawCmd*)cmd_list->CmdBuffer.Data)[cmd_i]); if (pcmd->UserCallback != IntPtr.Zero) { throw new NotImplementedException(); } else { if (pcmd->TextureId != IntPtr.Zero) { if (pcmd->TextureId == _fontAtlasID) { cl.SetGraphicsResourceSet(1, _fontTextureResourceSet); } else { cl.SetGraphicsResourceSet(1, GetImageResourceSet(pcmd->TextureId)); } } cl.SetScissorRect( 0, (uint)pcmd->ClipRect.X, (uint)pcmd->ClipRect.Y, (uint)(pcmd->ClipRect.Z - pcmd->ClipRect.X), (uint)(pcmd->ClipRect.W - pcmd->ClipRect.Y)); cl.DrawIndexed(pcmd->ElemCount, 1, (uint)idx_offset, vtx_offset, 0); } idx_offset += (int)pcmd->ElemCount; } vtx_offset += cmd_list->VtxBuffer.Size; } } /// /// Frees all graphics resources used by the renderer. /// public void Dispose() { _vertexBuffer.Dispose(); _indexBuffer.Dispose(); _projMatrixBuffer.Dispose(); _fontTexture.Dispose(); _fontTextureView.Dispose(); _vertexShader.Dispose(); _fragmentShader.Dispose(); _layout.Dispose(); _textureLayout.Dispose(); _pipeline.Dispose(); _mainResourceSet.Dispose(); foreach (IDisposable resource in _ownedResources) { resource.Dispose(); } } private struct ResourceSetInfo { public readonly IntPtr ImGuiBinding; public readonly ResourceSet ResourceSet; public ResourceSetInfo(IntPtr imGuiBinding, ResourceSet resourceSet) { ImGuiBinding = imGuiBinding; ResourceSet = resourceSet; } } } }