using System.Numerics; using System.Runtime.InteropServices; using ImGuiNET; 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 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() != nint.Zero ? ImGuiUserData.Get(ImGui.GetIO().BackendRendererUserData)! : null!; private static nint _fontTexture = nint.Zero; public static bool Init(nint renderer) { ImGuiIOPtr io = ImGui.GetIO(); RendererData bd = new RendererData(); io.BackendRendererUserData = ImGuiUserData.Store(bd); io.BackendFlags |= ImGuiBackendFlags.RendererHasVtxOffset; // We can honor the ImDrawCmd.VtxOffset field, allowing for large meshes. io.BackendFlags |= ImGuiBackendFlags.RendererHasViewports; bd.Renderer = renderer; ImGuiPlatformIOPtr platformIO = ImGui.GetPlatformIO(); platformIO.Renderer_CreateWindow = Marshal.GetFunctionPointerForDelegate(RendererCreateWindowDelegate); platformIO.Renderer_DestroyWindow = Marshal.GetFunctionPointerForDelegate(RendererDestroyWindowDelegate); platformIO.Renderer_SetWindowSize = Marshal.GetFunctionPointerForDelegate(RendererSetWindowSizeDelegate); platformIO.Renderer_RenderWindow = Marshal.GetFunctionPointerForDelegate(RendererRenderWindowDelegate); platformIO.Renderer_SwapBuffers = Marshal.GetFunctionPointerForDelegate(RendererSwapBuffersDelegate); return true; } public static void Dispose() { var io = ImGui.GetIO(); var platformIO = ImGui.GetPlatformIO(); DestroyDeviceObjects(); io.BackendRendererUserData = nint.Zero; io.BackendFlags &= ~ImGuiBackendFlags.RendererHasVtxOffset; platformIO.Renderer_RenderState = nint.Zero; platformIO.Renderer_CreateWindow = nint.Zero; platformIO.Renderer_DestroyWindow = nint.Zero; platformIO.Renderer_SetWindowSize = nint.Zero; platformIO.Renderer_RenderWindow = nint.Zero; platformIO.Renderer_SwapBuffers = nint.Zero; } public static void NewFrame() { if (_fontTexture == nint.Zero) { CreateDeviceObjects(); } } 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.NativePtr == 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; // 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.Renderer_RenderState = 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++) { ImDrawCmdPtr cmd = cmdList.CmdBuffer[cmdIndex]; if (cmd.UserCallback != nint.Zero) { 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)) { Console.WriteLine($"Failed to render ImGui draw command: {SDL.GetError()}"); } } } // Reset render state platformIo.Renderer_RenderState = nint.Zero; // 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 bool RenderDrawCommand(ImDrawListPtr drawList, ImDrawCmdPtr 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); ImDrawVertPtr 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() { ImGuiIOPtr io = ImGui.GetIO(); var data = Data; io.Fonts.AddFontDefault(); // TODO: Load custom fonts from "Fonts" directory //string fontsPath = Path.Combine(Plugin.AssemblyDirectory, "Fonts"); //if (Path.Exists(fontsPath)) //{ // string[] fonts = Directory.GetFiles(fontsPath, "*.ttf"); // if (fonts.Length > 0) // { // foreach (string font in fonts) // { // io.Fonts.AddFontFromFileTTF(font, 20f); // } // } //} // Build texture atlas io.Fonts.GetTexDataAsRGBA32(out byte* pixels, out int width, out int height); // Create surface from pixel data nint surface = SDL.CreateSurfaceFrom(width, height, SDL.PixelFormat.RGBA8888, (nint)pixels, width * 4); if (surface == nint.Zero) { SDL.LogError(SDL.LogCategory.Application, $"Failed to create font surface: {SDL.GetError()}"); return; } // Create texture _fontTexture = SDL.CreateTextureFromSurface(data.Renderer, surface); if (_fontTexture == nint.Zero) { SDL.LogError(SDL.LogCategory.Application, $"Failed to create font texture: {SDL.GetError()}"); return; } // Update texture directly without converting pixel format if (!SDL.UpdateTexture(_fontTexture, nint.Zero, (nint)pixels, width * 4)) { SDL.LogError(SDL.LogCategory.Application, $"Failed to update font texture: {SDL.GetError()}"); return; } // Ensure proper blending for font rendering SDL.SetTextureBlendMode(_fontTexture, SDL.BlendMode.Blend); // Use nearest neighbor filtering for crisp font rendering at small sizes SDL.SetTextureScaleMode(_fontTexture, SDL.ScaleMode.Linear); // Store our identifier io.Fonts.SetTexID(_fontTexture); SDL.DestroySurface(surface); io.Fonts.ClearTexData(); } public static void DestroyDeviceObjects() { } }