Dear ImGui + Odin + SDL3 GPU Integration Notes
This note records the working integration pattern for Dear ImGui with Odin bindings and SDL3 GPU.
It exists because the failure mode is deceptive:
- ImGui windows can appear as empty shells
- text and buttons can fail to show
- font atlases can be built correctly
- draw data can be non-zero
- and the bug can still be frame-composition, not fonts
Working Conclusion
The reliable fix was:
- initialize ImGui normally with the SDL3 and SDLGPU3 backends
- build ImGui windows during the normal UI frame
- call
Render()andPrepareDrawData()before render passes - render the 3D world in its own pass
- render ImGui in a separate final UI pass on the swapchain
- use
load_op = .LOADfor that UI pass - use no depth target for that UI pass
This was the change that made text and widgets appear correctly.
Symptoms We Saw
The broken integration showed these symptoms:
- ImGui window chrome/panels appeared
- text and buttons did not appear
simulation_begin=truecmd_lists > 0,total_vtx > 0,total_idx > 0atlas_built=truefont_tex_ready=true
That combination is important. It means:
- this is not primarily a glyph-range problem
- this is not primarily a font-atlas creation problem
- this is not primarily "no ImGui geometry submitted"
The content existed. The composition path was wrong.
Root Cause
The main issue was rendering ImGui inside the same pass as the world draw.
Even though the SDLGPU3 backend could be initialized and produce draw data, rendering widgets inside the scene pass was not reliable in this setup. A separate final UI pass fixed it.
This matters more than several earlier suspicions:
- not glyph ranges
- not default font absence
- not just high-DPI mismatch
- not just scissor in isolation
The decisive fix was pass ownership and frame composition.
Correct Frame Order
The working frame order is:
- Poll SDL events
- Forward every event to
imgui_impl_sdl3.ProcessEvent - Update app/sim/world/view
- Begin ImGui frame
- Build ImGui windows
- Acquire command buffer and swapchain
- Call
imgui.Render() - Fetch
draw_data := imgui.GetDrawData() - Call
imgui_impl_sdlgpu3.PrepareDrawData(draw_data, cmd) - Render world in its own pass
- Open a second UI pass on the swapchain with
load_op = .LOAD - Call
imgui_impl_sdlgpu3.RenderDrawData(draw_data, cmd, ui_pass, nil) - End UI pass
- Submit command buffer
Minimal Backend Rules
These rules matter for the SDLGPU3 backend:
PrepareDrawData()is mandatory before rendering ImGui- it must happen before opening the render pass used for ImGui
- do not assume same-pass world+ImGui composition is safe
- prefer a final overlay pass for ImGui
Initialization Pattern
The initialization pattern that worked reliably was:
imgui.CHECKVERSION()ctx := imgui.CreateContext()imgui.SetCurrentContext(ctx)imgui.StyleColorsDark()io := imgui.GetIO()io.IniFilename = nilio.LogFilename = nilimgui.FontAtlas_AddFontDefault(io.Fonts, nil)imgui_impl_sdl3.InitForSDLGPU(window)imgui_impl_sdlgpu3.Init(&InitInfo{ Device = gpu, ColorTargetFormat = swapchain_format, MSAASamples = ._1 })
Important:
- explicitly set the ImGui context before init/frame/build/render operations
- disable ini/log files if you want deterministic startup while debugging
- explicitly add the default font so the setup is unambiguous
UI Pass Shape
The final UI pass should look like:
- target: swapchain texture
load_op = .LOADstore_op = .STORE- no depth target
This pass should be treated as a pure overlay composition pass.
Explicit viewport/scissor reset may still be a reasonable safety measure in some projects, but it was not the essential fix here.
Debugging Lessons
These diagnostics were useful:
- whether
Begin(...)returned true draw_data.Validdraw_data.CmdListsCountdraw_data.TotalVtxCountdraw_data.TotalIdxCountio.Fonts.TexID != 0FontAtlas_IsBuilt(io.Fonts)- swapchain size
- ImGui
DisplaySizeandFramebufferScale
Interpretation:
- zero draw data means UI content is not being submitted
- built atlas + ready texture + nonzero vertices means the issue is likely later in composition
- visible panel chrome with missing widgets does not automatically mean a font problem
What Did Not Ultimately Solve It
These were useful to test but were not the real fix:
- forcing default font alone
- forcing full-screen scissor alone
- toggling high-DPI alone
- forcing window size/position/collapsed state alone
Those checks helped narrow the problem, but the lasting fix was the separate final UI pass.
Practical Rule For Future Projects
For Odin + Dear ImGui + SDL3 GPU:
- treat ImGui as a final overlay renderer
- do not couple it tightly to the scene pass
- prepare draw data before passes
- render it in a dedicated final UI pass
If ImGui shows empty window shells with no widget text, check frame composition before blaming fonts.