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 { private sealed class TextureState { public ImTextureDataPtr Source; public readonly Dictionary RendererTextures = new(); } 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 RendererSetWindowSizeFn(ImGuiViewportPtr viewport, Vector2 size); private static readonly RendererSetWindowSizeFn RendererSetWindowSizeDelegate = RendererSetWindowSize; [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) { 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.RendererSetWindowSize = (void*)Marshal.GetFunctionPointerForDelegate(RendererSetWindowSizeDelegate); 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) { Program.Logger.Log(null); 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, (string?)null); vd.RendererOwned = true; } Program.Logger.Log(null); } private static void RendererDestroyWindow(ImGuiViewportPtr viewport) { Program.Logger.Log(null); 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; Program.Logger.Log(null); } private static void RendererSetWindowSize(ImGuiViewportPtr viewport, Vector2 size) { // SDL renderer windows track size through the platform window callback. Program.Logger.Log(null); } private static void RendererRenderWindow(ImGuiViewportPtr viewport) { Program.Logger.Log(null); ImGuiSDL3Platform.ViewPortData? vd = ImGuiUserData.Get(viewport.PlatformUserData); if (vd == null || vd.Renderer == nint.Zero) { return; } RenderDrawData(viewport.DrawData, vd.Renderer); Program.Logger.Log(null); } private static void RendererSwapBuffers(ImGuiViewportPtr viewport) { Program.Logger.Log(null); ImGuiSDL3Platform.ViewPortData? vd = ImGuiUserData.Get(viewport.PlatformUserData); if (vd == null || vd.Renderer == nint.Zero) return; SDL.RenderPresent(vd.Renderer); Program.Logger.Log(null); } 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 var oldViewport); old.Viewport = oldViewport; SDL.GetRenderClipRect(renderer, out var oldClipRect); old.ClipRect = oldClipRect; // Set up render state SDL.SetRenderViewport(renderer, 0); 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 } 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); // Get texture nint texId = ResolveTextureId(cmd.GetTexID(), renderer); // Convert ImGui vertices to SDL vertices if (!RenderDrawCommand(cmdList, cmd, renderer, texId, renderScale)) { 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 nint ResolveTextureId(ImTextureID texId, nint renderer) { if (texId == ImTextureID.Null) return nint.Zero; TextureState? state = ImGuiUserData.Get((void*)(nint)texId); if (state == null) return (nint)texId; if (!state.RendererTextures.TryGetValue(renderer, out nint rendererTexture) || rendererTexture == nint.Zero) { rendererTexture = CreateRendererTexture(state, renderer); state.RendererTextures[renderer] = rendererTexture; } return rendererTexture; } private static nint CreateRendererTexture(TextureState state, nint renderer) { ImTextureDataPtr tex = state.Source; // We keep ARGB8888 here because this project already relies on that upload path. nint sdlTexture = SDL.CreateTexture(renderer, SDL.PixelFormat.ARGB8888, SDL.TextureAccess.Static, tex.Width, tex.Height); if (sdlTexture == nint.Zero) return nint.Zero; SDL.UpdateTexture(sdlTexture, nint.Zero, (nint)tex.GetPixels(), tex.GetPitch()); SDL.SetTextureBlendMode(sdlTexture, SDL.BlendMode.Blend); SDL.SetTextureScaleMode(sdlTexture, SDL.ScaleMode.Linear); return sdlTexture; } private static void UploadRendererTexture(ImTextureDataPtr tex, nint renderer, nint sdlTexture) { if (tex.Status == ImTextureStatus.WantUpdates) { for (int i = 0; i < tex.Updates.Size; i++) { var r = tex.Updates[i]; SDL.Rect rect = new SDL.Rect { X = r.X, Y = r.Y, W = r.W, H = r.H }; SDL.UpdateTexture(sdlTexture, rect, (nint)tex.GetPixelsAt(r.X, r.Y), tex.GetPitch()); } } else { SDL.UpdateTexture(sdlTexture, nint.Zero, (nint)tex.GetPixels(), tex.GetPitch()); } } private static void UpdateTexture(ImTextureDataPtr tex, nint renderer) { TextureState? state = null; if ((nint)tex.BackendUserData != nint.Zero) state = ImGuiUserData.Get(tex.BackendUserData); if (state == null) { state = new TextureState(); tex.BackendUserData = ImGuiUserData.Store(state); } state.Source = tex; tex.SetTexID((nint)tex.BackendUserData); if (tex.Status == ImTextureStatus.WantDestroy) { foreach (nint rendererTexture in state.RendererTextures.Values) { if (rendererTexture != nint.Zero) SDL.DestroyTexture(rendererTexture); } state.RendererTextures.Clear(); ImGuiUserData.Free(tex.BackendUserData); tex.BackendUserData = (void*)nint.Zero; tex.SetTexID(ImTextureID.Null); tex.SetStatus(ImTextureStatus.Destroyed); return; } bool hasRendererTexture = state.RendererTextures.TryGetValue(renderer, out nint sdlTexture) && sdlTexture != nint.Zero; if (!hasRendererTexture) { sdlTexture = CreateRendererTexture(state, renderer); state.RendererTextures[renderer] = sdlTexture; } if (sdlTexture != nint.Zero && (tex.Status == ImTextureStatus.WantCreate || tex.Status == ImTextureStatus.WantUpdates || !hasRendererTexture)) { UploadRendererTexture(tex, renderer, sdlTexture); tex.SetStatus(ImTextureStatus.Ok); } } private static bool RenderDrawCommand(ImDrawListPtr drawList, ImDrawCmd cmd, nint renderer, nint texId, Vector2 scale) { 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 * scale.X, Y = srcVert.Pos.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, nint.Zero); } } }