using System.Diagnostics; using System.Numerics; using System.Runtime.CompilerServices; 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 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 ClipEnabled; public SDL.Rect ClipRect; } public static RendererData GetRendererData() => ImGui.GetCurrentContext().Handle != null ? ImGuiUserData.Get(ImGui.GetIO().BackendRendererUserData)! : null!; public static SDL3Window? Parent; 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 = DelegateStorage.RendererCreateWindowDelegate.GetPtrForDelegate(); platformIO.RendererDestroyWindow = DelegateStorage.RendererDestroyWindowDelegate.GetPtrForDelegate(); platformIO.RendererRenderWindow = DelegateStorage.RendererRenderWindowDelegate.GetPtrForDelegate(); platformIO.RendererSwapBuffers = DelegateStorage.RendererSwapBuffersDelegate.GetPtrForDelegate(); return true; } public static void Dispose() { ImGuiIOPtr io = ImGui.GetIO(); ImGuiPlatformIOPtr platformIO = ImGui.GetPlatformIO(); if (io.BackendRendererName != null) { Marshal.FreeHGlobal((nint)io.BackendRendererName); } ImGuiUserData.Free(io.BackendRendererUserData); io.BackendRendererName = null; io.BackendRendererUserData = null; io.BackendFlags &= ~(ImGuiBackendFlags.RendererHasVtxOffset | ImGuiBackendFlags.RendererHasTextures | ImGuiBackendFlags.RendererHasViewports); platformIO.RendererTextureMaxWidth = 0; platformIO.RendererTextureMaxHeight = 0; platformIO.RendererRenderState = null; } public static void SetupRenderState(nint renderer) { // Clear out any viewports and cliprect set by the user // FIXME: Technically speaking there are lots of other things we could backup/setup/restore during our render process. SDL.SetRenderViewport(renderer, nint.Zero); SDL.SetRenderClipRect(renderer, nint.Zero); } public static void NewFrame() { RendererData bd = GetRendererData(); Debug.Assert(bd != null, "Context or backend not initialized! Did you call ImGuiSDL3Renderer.Init()?"); } 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++) { ImTextureDataPtr texture = drawData.Textures[i]; if (texture.Handle != null) { UpdateTexture(texture, renderer); } } // Backup SDL renderer state BackupSDLRendererState old = new BackupSDLRendererState { ClipEnabled = SDL.RenderClipEnabled(renderer) }; SDL.GetRenderViewport(renderer, out old.Viewport); SDL.GetRenderClipRect(renderer, out old.ClipRect); // Set up render state SetupRenderState(renderer); // 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 drawList = drawData.CmdLists[n]; for (int cmdIndex = 0; cmdIndex < drawList.CmdBuffer.Size; cmdIndex++) { ImDrawCmd cmd = drawList.CmdBuffer[cmdIndex]; if (cmd.UserCallback != null) { if (cmd.UserCallback == DelegateStorage.DrawCallbackResetRenderStateDelegate.GetPtrForDelegate()) { SetupRenderState(renderer); } else { // this is cursed af and idk if it even works :) ImDrawCmdPtr cmdPtr = new ImDrawCmdPtr { Handle = (ImDrawCmd*)Unsafe.AsPointer(ref cmd) }; ((delegate* unmanaged[Cdecl])cmd.UserCallback)(drawList, cmdPtr); } } else { // 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); // Convert ImGui vertices to SDL vertices 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 colR = (byte)((col >> 0) & 0xFF); byte colG = (byte)((col >> 8) & 0xFF); byte colB = (byte)((col >> 16) & 0xFF); byte colA = (byte)((col >> 24) & 0xFF); vertices[i] = new SDL.Vertex { Position = new SDL.FPoint { X = (srcVert.Pos.X - clipOffset.X) * renderScale.X, Y = (srcVert.Pos.Y - clipOffset.Y) * renderScale.Y }, Color = new SDL.FColor { R = colR / 255f, G = colG / 255f, B = colB / 255f, A = colA / 255f }, TexCoord = new SDL.FPoint { X = srcVert.Uv.X, Y = srcVert.Uv.Y } }; indices[i] = i; } // Get texture ImTextureID texId = cmd.GetTexID(); if (!SDL.RenderGeometry(renderer, texId, vertices, vertices.Length, indices, indices.Length)) { Program.Logger.Log($"Failed to render ImGui draw command: {SDL.GetError()}"); } } } } // Reset render state platformIo.RendererRenderState = null; // Restore renderer state SDL.SetRenderViewport(renderer, old.Viewport); 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++) { ImTextureRect 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); } } public static void CreateDeviceObjects() { } public static void DestroyDeviceObjects() { ImVector texures = ImGui.GetPlatformIO().Textures; for (int i = 0; i < texures.Size; i++) { ImTextureDataPtr texture = texures[i]; texture.Status = ImTextureStatus.WantDestroy; UpdateTexture(texture, GetRendererData().Renderer); } } // @formatter:off — disable formatter after this line public static class DelegateStorage { [UnmanagedFunctionPointer(CallingConvention.Cdecl)] internal delegate void DrawCallbackResetRenderStateFn(ImDrawListPtr drawList, ImDrawCmdPtr cmd); internal static readonly DrawCallbackResetRenderStateFn DrawCallbackResetRenderStateDelegate = DrawCallbackResetRenderState; private static void DrawCallbackResetRenderState(ImDrawListPtr drawList, ImDrawCmdPtr cmd) { } // Intentionally empty. Used as an identifier for rendering loop to call its code. Simpler to implement this way. [UnmanagedFunctionPointer(CallingConvention.Cdecl)] internal delegate void RendererCreateWindowFn(ImGuiViewportPtr viewport); internal static readonly RendererCreateWindowFn RendererCreateWindowDelegate = RendererCreateWindow; 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; } } [UnmanagedFunctionPointer(CallingConvention.Cdecl)] internal delegate void RendererDestroyWindowFn(ImGuiViewportPtr viewport); internal static readonly RendererDestroyWindowFn RendererDestroyWindowDelegate = RendererDestroyWindow; 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; } [UnmanagedFunctionPointer(CallingConvention.Cdecl)] internal delegate void RendererRenderWindowFn(ImGuiViewportPtr viewport); internal static readonly RendererRenderWindowFn RendererRenderWindowDelegate = RendererRenderWindow; 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); } [UnmanagedFunctionPointer(CallingConvention.Cdecl)] internal delegate void RendererSwapBuffersFn(ImGuiViewportPtr viewport); internal static readonly RendererSwapBuffersFn RendererSwapBuffersDelegate = RendererSwapBuffers; 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); } } // @formatter:on — enable formatter after this line }