Skip to content

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() and PrepareDrawData() 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 = .LOAD for 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=true
  • cmd_lists > 0, total_vtx > 0, total_idx > 0
  • atlas_built=true
  • font_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:

  1. Poll SDL events
  2. Forward every event to imgui_impl_sdl3.ProcessEvent
  3. Update app/sim/world/view
  4. Begin ImGui frame
  5. Build ImGui windows
  6. Acquire command buffer and swapchain
  7. Call imgui.Render()
  8. Fetch draw_data := imgui.GetDrawData()
  9. Call imgui_impl_sdlgpu3.PrepareDrawData(draw_data, cmd)
  10. Render world in its own pass
  11. Open a second UI pass on the swapchain with load_op = .LOAD
  12. Call imgui_impl_sdlgpu3.RenderDrawData(draw_data, cmd, ui_pass, nil)
  13. End UI pass
  14. 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 = nil
  • io.LogFilename = nil
  • imgui.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 = .LOAD
  • store_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.Valid
  • draw_data.CmdListsCount
  • draw_data.TotalVtxCount
  • draw_data.TotalIdxCount
  • io.Fonts.TexID != 0
  • FontAtlas_IsBuilt(io.Fonts)
  • swapchain size
  • ImGui DisplaySize and FramebufferScale

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.