Debug UI

SafiEngine's debug overlay uses MicroUI — a tiny (~1100 SLOC), pure-C immediate-mode UI library. The engine ships its own SDL_gpu batched-quad backend (stb_truetype font atlas, vertex/index buffer batching, pipeline, scissor + draw) so the whole stack stays in C.

#include <safi/ui/debug_ui.h>

Lifecycle

The debug UI is opt-in via SafiAppDesc.enable_debug_ui. When enabled:

  1. safi_app_init calls safi_debug_ui_init — creates the MicroUI context, bakes a ProggyClean font atlas via stb_truetype into an SDL_GPUTexture, builds the UI graphics pipeline.
  2. Each frame, after safi_renderer_begin_frame and before safi_renderer_begin_main_pass:
    • safi_debug_ui_begin_frame(r) — calls mu_begin, opens the widget frame.
    • safi_debug_ui_draw_panels(r, world) — draws the built-in Scene and Inspector panels.
    • Optionally build additional custom widgets with mu_begin_window / mu_label / mu_button / ….
    • safi_debug_ui_prepare(r) — calls mu_end, batches all draw commands into vertex/index data, and uploads through a copy pass. Must happen before the main render pass opens; SDL_gpu forbids nested passes.
  3. After safi_renderer_begin_main_pass and any engine geometry, call safi_debug_ui_render(r) to record the batched draw commands into the active render pass.
  4. safi_app_shutdown calls safi_debug_ui_shutdown.

The engine's input system already forwards SDL events to MicroUI — you don't need to call anything extra for mouse, keyboard, or text input.

Built-in panels

The engine provides two ready-to-use panels via safi_debug_ui_draw_panels. No MicroUI include or widget code needed in your app.

Scene hierarchy

Positioned on the left side of the window. Displays every entity with a SafiName component as a collapsible tree that respects EcsChildOf hierarchy:

  • Root entities (no parent) appear at the top level.
  • Child entities appear indented under their parent with an arrow toggle.
  • Clicking the arrow (> / v) expands or collapses a parent's subtree.
  • Clicking the entity name selects it for the Inspector.
  • The selected entity is highlighted with a distinct button color.

Inspector

Positioned on the right side of the window. Displays the selected entity's name and all its stock components in collapsible sections (expanded by default):

ComponentFields shown
SafiTransformPosition (x/y/z), Rotation (euler degrees), Scale (x/y/z)
SafiCameraFOV (degrees), Near, Far, Target (x/y/z)
SafiMeshRendererModel assignment status (read-only), Visible (checkbox)
SafiSpinSpeed (rad/s), Axis (x/y/z)
SafiRigidBodyType (dropdown: Static / Dynamic / Kinematic), Mass, Friction, Restitution
SafiColliderShape (dropdown: Box / Sphere), HalfExtents (x/y/z) or Radius
All 5 light typesColor (r/g/b), Intensity, type-specific params

Property widgets:

  • Number fields — drag to scrub, double-click to type a value directly.
  • Vec3 rows — label + three number cells in a single row.
  • Checkboxes — bool fields (e.g. MeshRenderer.Visible).
  • Dropdowns — enum fields render as a button showing the current value; clicking drops a scrollable list below the button. Only one dropdown can be open at a time; clicking outside dismisses it.

Only components present on the selected entity are shown.

Enabling / disabling

The panels are gated by SafiAppDesc.enable_debug_ui:

SafiAppDesc desc = {
    .title           = "My Game",
    .width           = 1280,
    .height          = 720,
    .enable_debug_ui = true,   // set to false to disable all debug UI
};

At runtime, app.debug_ui_enabled can be toggled to show/hide the panels.

Functions

Framework

bool safi_debug_ui_init(SafiRenderer *r);
void safi_debug_ui_shutdown(SafiRenderer *r);
void safi_debug_ui_process_event(const void *sdl_event);  // called by the engine
void safi_debug_ui_begin_frame(SafiRenderer *r);
void safi_debug_ui_prepare(SafiRenderer *r);             // pre-pass, batches + uploads UI vbo/ibo
void safi_debug_ui_render(SafiRenderer *r);              // in-pass, records draws
mu_Context *safi_debug_ui_context(void);                 // the underlying MicroUI context
bool safi_debug_ui_wants_input(void);                    // true when a widget is focused

Scene & Inspector

void safi_debug_ui_draw_panels(SafiRenderer *r, ecs_world_t *world);

/* Selection lives on SafiEditorState — use these from anywhere: */
ecs_entity_t safi_editor_get_selected(const ecs_world_t *world);
void         safi_editor_set_selected(ecs_world_t *world, ecs_entity_t e);
FunctionDescription
safi_debug_ui_draw_panelsDraws the Scene hierarchy and Inspector windows. Call between begin_frame and prepare.
safi_editor_get_selectedReturns the entity currently shown in the Inspector.
safi_editor_set_selectedSets the entity shown in the Inspector. Typically called once at startup to pick a default.
Selection source of truth

The selected entity is stored on SafiEditorState.selected_entity, not inside the debug UI. The Scene panel writes it on click; the Inspector, the toolbar highlight, and the gizmo system all read from the same singleton. Use safi_editor_get_selected(world) / safi_editor_set_selected(world, e) from any system; see Editor State.

Example

main.c
#include <safi/safi.h>

static void render_system(ecs_iter_t *it) {
    SafiApp *app = (SafiApp *)it->ctx;
    SafiRenderer *r = &app->renderer;

    if (!safi_renderer_begin_frame(r)) return;

    if (app->debug_ui_enabled) {
        safi_debug_ui_begin_frame(r);
        safi_debug_ui_draw_panels(r, it->world);
        safi_debug_ui_prepare(r);
    }

    safi_renderer_begin_main_pass(r);
    // ... draw your scene ...
    if (app->debug_ui_enabled) safi_debug_ui_render(r);
    safi_renderer_end_main_pass(r);
    safi_renderer_end_frame(r);
}

Custom widgets

If you need additional MicroUI widgets beyond the built-in panels, include <microui.h> and add your own mu_begin_window / mu_end_window blocks between safi_debug_ui_begin_frame and safi_debug_ui_prepare:

#include <microui.h>

// in your render system, after safi_debug_ui_begin_frame:
mu_Context *ctx = safi_debug_ui_context();
if (mu_begin_window(ctx, "My Panel", mu_rect(10, 10, 200, 100))) {
    mu_layout_row(ctx, 1, (int[]){ -1 }, 0);
    mu_label(ctx, "Hello from MicroUI!");
    mu_end_window(ctx);
}

No special macros or feature flags needed — just include the header and call MicroUI functions directly.

Why MicroUI?

An earlier iteration used Nuklear. While functional, it was a large single-header (~18K SLOC) and its vertex-buffer conversion model (nk_convert) added complexity to the rendering backend.

MicroUI is:

  • ~1100 lines of C — trivially auditable
  • pure C99, no generator, no submodule, no C++
  • command-based rendering — emits simple draw commands (rect, text, icon, clip) that the backend batches into GPU-friendly vertex data
  • license-friendly (MIT)

Trade-off: MicroUI has a smaller widget catalogue than Nuklear or Dear ImGui. For a debug overlay this is fine — the engine extends it with custom property widgets (vec3 rows, double-click number editing) as needed.

See also

  • SafiRenderer — frame lifecycle (begin_framebegin_main_passend_main_passend_frame)
  • SafiName — entities need a SafiName component to appear in the Scene panel
  • Editor State — where the selected entity is actually stored
  • Editor Toolbar — Play/Pause/Stop + tool buttons drawn at the top of the viewport
  • MicroUI project — upstream source and documentation