using System.Numerics; using System.Runtime.InteropServices; using Hexa.NET.ImGui; using SDL3; namespace SDL3_TestingSuite.SDL3; /// /// Implementation of ImGui for SDL3 Renderer backend. /// https://github.com/ocornut/imgui/blob/master/backends/imgui_impl_sdlrenderer3.h /// public unsafe static class ImGuiSDL3Renderer { public static SDL3Window Parent; public class RendererData { public nint Renderer; // Main viewport's renderer public ImVector ColorBuffer; // Render State public SDL.ScaleMode CurrentScaleMode; } private struct BackupSDLRendererState { public SDL.Rect Viewport; public bool ViewportEnabled; public bool ClipEnabled; public SDL.Rect ClipRect; } [UnmanagedFunctionPointer(CallingConvention.Cdecl)] private delegate void RendererCreateWindowFn(ImGuiViewportPtr viewport); private static readonly RendererCreateWindowFn RendererCreateWindowDelegate = RendererCreateWindow; [UnmanagedFunctionPointer(CallingConvention.Cdecl)] private delegate void RendererDestroyWindowFn(ImGuiViewportPtr viewport); private static readonly RendererDestroyWindowFn RendererDestroyWindowDelegate = RendererDestroyWindow; [UnmanagedFunctionPointer(CallingConvention.Cdecl)] private delegate void RendererRenderWindowFn(ImGuiViewportPtr viewport); private static readonly RendererRenderWindowFn RendererRenderWindowDelegate = RendererRenderWindow; [UnmanagedFunctionPointer(CallingConvention.Cdecl)] private delegate void RendererSwapBuffersFn(ImGuiViewportPtr viewport); private static readonly RendererSwapBuffersFn RendererSwapBuffersDelegate = RendererSwapBuffers; public static RendererData Data => ImGui.GetCurrentContext().Handle != null ? ImGuiUserData.Get(ImGui.GetIO().BackendRendererUserData)! : null!; public static bool Init(nint renderer, SDL3Window parentWindow = null) { Parent = parentWindow; ImGuiIOPtr io = ImGui.GetIO(); RendererData bd = new RendererData(); io.BackendRendererUserData = ImGuiUserData.Store(bd); io.BackendRendererName = (byte*)Marshal.StringToHGlobalAnsi("NepImGuiSDL3Renderer"); io.BackendFlags |= ImGuiBackendFlags.RendererHasVtxOffset; // We can honor the ImDrawCmd.VtxOffset field, allowing for large meshes. io.BackendFlags |= ImGuiBackendFlags.RendererHasTextures; // We can honor ImGuiPlatformIO.Textures[] requests during render. io.BackendFlags |= ImGuiBackendFlags.RendererHasViewports; bd.Renderer = renderer; ImGuiPlatformIOPtr platformIO = ImGui.GetPlatformIO(); platformIO.RendererCreateWindow = (void*)Marshal.GetFunctionPointerForDelegate(RendererCreateWindowDelegate); platformIO.RendererDestroyWindow = (void*)Marshal.GetFunctionPointerForDelegate(RendererDestroyWindowDelegate); platformIO.RendererRenderWindow = (void*)Marshal.GetFunctionPointerForDelegate(RendererRenderWindowDelegate); platformIO.RendererSwapBuffers = (void*)Marshal.GetFunctionPointerForDelegate(RendererSwapBuffersDelegate); return true; } public static void Dispose() { var io = ImGui.GetIO(); var platformIO = ImGui.GetPlatformIO(); DestroyDeviceObjects(); io.BackendRendererName = null; io.BackendRendererUserData = null; io.BackendFlags &= ~(ImGuiBackendFlags.RendererHasVtxOffset | ImGuiBackendFlags.RendererHasTextures); platformIO.RendererTextureMaxWidth = 0; platformIO.RendererTextureMaxHeight = 0; platformIO.RendererRenderState = null; platformIO.RendererCreateWindow = null; platformIO.RendererDestroyWindow = null; platformIO.RendererSetWindowSize = null; platformIO.RendererRenderWindow = null; platformIO.RendererSwapBuffers = null; } public static void NewFrame() { } private static void RendererCreateWindow(ImGuiViewportPtr viewport) { ImGuiSDL3Platform.ViewPortData? vd = ImGuiUserData.Get(viewport.PlatformUserData); if (vd == null || vd.Window == nint.Zero) return; if (vd.Renderer == nint.Zero) { vd.Renderer = SDL.CreateRenderer(vd.Window, null); vd.RendererOwned = true; } } private static void RendererDestroyWindow(ImGuiViewportPtr viewport) { ImGuiSDL3Platform.ViewPortData? vd = ImGuiUserData.Get(viewport.PlatformUserData); if (vd == null) return; if (vd.RendererOwned && vd.Renderer != nint.Zero) { SDL.DestroyRenderer(vd.Renderer); } vd.Renderer = nint.Zero; vd.RendererOwned = false; } private static void RendererRenderWindow(ImGuiViewportPtr viewport) { ImGuiSDL3Platform.ViewPortData? vd = ImGuiUserData.Get(viewport.PlatformUserData); if (vd == null || vd.Renderer == nint.Zero) { return; } if (Parent != null) { SDL.SetRenderDrawColorFloat(vd.Renderer, Parent.ClearColor.X, Parent.ClearColor.Y, Parent.ClearColor.Z, 0f); SDL.RenderClear(vd.Renderer); } RenderDrawData(viewport.DrawData, vd.Renderer); } private static void RendererSwapBuffers(ImGuiViewportPtr viewport) { ImGuiSDL3Platform.ViewPortData? vd = ImGuiUserData.Get(viewport.PlatformUserData); if (vd == null || vd.Renderer == nint.Zero) return; SDL.RenderPresent(vd.Renderer); } public static void RenderDrawData(ImDrawDataPtr drawData, nint renderer) { // Skip if no data to render if (drawData.Handle == null || drawData.CmdListsCount == 0) return; SDL.GetRenderScale(renderer, out float renderScaleX, out float renderScaleY); Vector2 renderScale = new Vector2(renderScaleX == 1.0f ? drawData.FramebufferScale.X : 1.0f, renderScaleY == 1.0f ? drawData.FramebufferScale.Y : 1.0f); int fbWidth = (int)(drawData.DisplaySize.X * renderScale.X); int fbHeight = (int)(drawData.DisplaySize.Y * renderScale.Y); if (fbWidth <= 0 || fbHeight <= 0) return; for (int i = 0; i < drawData.Textures.Size; i++) { var texture = drawData.Textures[i]; UpdateTexture(texture, renderer); } // Backup SDL renderer state BackupSDLRendererState old = new BackupSDLRendererState { ViewportEnabled = SDL.RenderViewportSet(renderer), ClipEnabled = SDL.RenderClipEnabled(renderer) }; SDL.GetRenderViewport(renderer, out old.Viewport); SDL.GetRenderClipRect(renderer, out old.ClipRect); // Set up render state SDL.SetRenderViewport(renderer, nint.Zero); SDL.SetRenderClipRect(renderer, nint.Zero); // Set render state in platform IO ImGuiPlatformIOPtr platformIo = ImGui.GetPlatformIO(); platformIo.RendererRenderState = (void*)renderer; Vector2 clipOffset = drawData.DisplayPos; // Render command lists for (int n = 0; n < drawData.CmdListsCount; n++) { ImDrawListPtr cmdList = drawData.CmdLists[n]; for (int cmdIndex = 0; cmdIndex < cmdList.CmdBuffer.Size; cmdIndex++) { ImDrawCmd cmd = cmdList.CmdBuffer[cmdIndex]; if (cmd.UserCallback != null) { continue; // User callback not implemented } // Apply clipping rectangle Vector4 clipRect = cmd.ClipRect; Vector2 clipMin = new Vector2((clipRect.X - clipOffset.X) * renderScale.X, (clipRect.Y - clipOffset.Y) * renderScale.Y); Vector2 clipMax = new Vector2((clipRect.Z - clipOffset.X) * renderScale.X, (clipRect.W - clipOffset.Y) * renderScale.Y); clipMin.X = Math.Max(0, clipMin.X); clipMin.Y = Math.Max(0, clipMin.Y); clipMax.X = Math.Min(fbWidth, clipMax.X); clipMax.Y = Math.Min(fbHeight, clipMax.Y); if (clipMax.X <= clipMin.X || clipMax.Y <= clipMin.Y) continue; SDL.Rect r = new SDL.Rect { X = (int)clipMin.X, Y = (int)clipMin.Y, W = (int)(clipMax.X - clipMin.X), H = (int)(clipMax.Y - clipMin.Y) }; SDL.SetRenderClipRect(renderer, r); // Get texture nint texId = cmd.GetTexID(); // Convert ImGui vertices to SDL vertices if (!RenderDrawCommand(cmdList, cmd, renderer, texId, renderScale, clipOffset)) { Console.WriteLine($"Failed to render ImGui draw command: {SDL.GetError()}"); } } } // Reset render state platformIo.RendererRenderState = null; // Restore renderer state SDL.SetRenderViewport(renderer, old.ViewportEnabled ? old.Viewport : new SDL.Rect()); SDL.SetRenderClipRect(renderer, old.ClipEnabled ? old.ClipRect : new SDL.Rect()); } private static void UpdateTexture(ImTextureDataPtr tex, nint renderer) { if (tex.Status == ImTextureStatus.WantDestroy) { if (tex.TexID != ImTextureID.Null) SDL.DestroyTexture(tex.TexID); tex.SetTexID(ImTextureID.Null); tex.SetStatus(ImTextureStatus.Destroyed); return; } bool needsCreate = tex.TexID == ImTextureID.Null || SDL.GetRendererFromTexture(tex.TexID) != renderer; if (tex.Status == ImTextureStatus.WantCreate || needsCreate) { if (tex.TexID != ImTextureID.Null) SDL.DestroyTexture(tex.TexID); // Create texture on the renderer that is actually drawing this viewport. // SDL textures are renderer-owned and cannot be shared across renderer instances. nint sdlTexture = SDL.CreateTexture(renderer, SDL.PixelFormat.ABGR8888, SDL.TextureAccess.Static, tex.Width, tex.Height); SDL.UpdateTexture(sdlTexture, nint.Zero, (nint)tex.GetPixels(), tex.GetPitch()); SDL.SetTextureBlendMode(sdlTexture, SDL.BlendMode.Blend); SDL.SetTextureScaleMode(sdlTexture, SDL.ScaleMode.Linear); tex.SetTexID(sdlTexture); tex.SetStatus(ImTextureStatus.Ok); return; } if (tex.Status == ImTextureStatus.WantUpdates) { // Update selected blocks. We only ever write to textures regions which have never been used before! // This backend choose to use tex.Updates[] but you can use tex.UpdateRect to upload a single region. nint sdlTexture = tex.TexID; for (int i = 0; i < tex.Updates.Size; i++) { var r = tex.Updates[i]; SDL.Rect sdlR = new SDL.Rect { X = r.X, Y = r.Y, W = r.W, H = r.H }; SDL.UpdateTexture(sdlTexture, sdlR, (nint)tex.GetPixelsAt(r.X, r.Y), tex.GetPitch()); } tex.SetStatus(ImTextureStatus.Ok); } } private static bool RenderDrawCommand(ImDrawListPtr drawList, ImDrawCmd cmd, nint renderer, nint texId, Vector2 scale, Vector2 displayPos) { uint indexOffset = cmd.IdxOffset; uint vertexOffset = cmd.VtxOffset; uint elemCount = cmd.ElemCount; SDL.Vertex[] vertices = new SDL.Vertex[elemCount]; int[] indices = new int[elemCount]; for (int i = 0; i < elemCount; i++) { ushort idx = drawList.IdxBuffer[(int)indexOffset + i]; int vertIdx = (int)(vertexOffset + idx); ImDrawVert srcVert = drawList.VtxBuffer[vertIdx]; uint col = srcVert.Col; byte r = (byte)((col >> 0) & 0xFF); byte g = (byte)((col >> 8) & 0xFF); byte b = (byte)((col >> 16) & 0xFF); byte a = (byte)((col >> 24) & 0xFF); vertices[i] = new SDL.Vertex { Position = new SDL.FPoint { X = (srcVert.Pos.X - displayPos.X) * scale.X, Y = (srcVert.Pos.Y - displayPos.Y) * scale.Y }, Color = new SDL.FColor { R = r / 255f, G = g / 255f, B = b / 255f, A = a / 255f }, TexCoord = new SDL.FPoint { X = srcVert.Uv.X, Y = srcVert.Uv.Y } }; indices[i] = i; } return SDL.RenderGeometry(renderer, texId, vertices, vertices.Length, indices, indices.Length); } public static void CreateDeviceObjects() { } public static void DestroyDeviceObjects() { var texures = ImGui.GetPlatformIO().Textures; for (int i = 0; i < texures.Size; i++) { var texture = texures[i]; texture.Status = ImTextureStatus.WantDestroy; UpdateTexture(texture, Data.Renderer); } } }