Render Overview

SafiEngine's renderer is a thin wrapper around the SDL_gpu API introduced in SDL 3. SDL_gpu is Khronos-style, explicit, and portable — the same code runs on Metal (macOS/iOS), Vulkan (Linux/Windows/Android), and D3D12 (Windows) without #ifdefs.

Frame lifecycle

The engine's render system (engine/src/render/render_system.c, runs on EcsOnStore) owns the whole frame. SDL_gpu forbids nested passes, so any copy-pass upload (UI vertex data, gizmo vertices) has to happen between begin_frame and begin_main_pass — hence the two-phase shape:

safi_renderer_begin_frame(r)       // command buffer + swapchain
  debug_ui_begin_frame              // MicroUI widgets
  debug_ui_prepare                  // MicroUI vertex upload (copy pass)
  gizmo_system_upload               // gizmo vertex upload (copy pass)

safi_renderer_begin_main_pass(r)   // opens color + depth pass
  <query (SafiGlobalTransform, SafiMeshRenderer), draw each lit model>
  gizmo_system_draw                 // line-list overlay inside main pass
  debug_ui_render                   // MicroUI on top
safi_renderer_end_main_pass(r)

safi_renderer_end_frame(r)         // submits

The active camera is discovered by a (SafiCamera, SafiActiveCamera) query; in Edit mode the editor camera holds the tag, in Play mode whichever entity user code attached it to. Gizmos enqueued from any frame phase (see Gizmo Draw List) are drained in the step above, then the queue clears.

Only one render pass per frame today. Additional passes (shadow maps, post-processing) are a future evolution.

:::warning WIP — Render architecture The following are planned but not yet implemented:

  • Render extraction — gameplay ECS state is read directly at draw time; no extracted render-world exists
  • Draw queue / sorting — draw calls are issued inline, not batched or sorted
  • Transparent pass — no separate pass for transparent/alpha-blended geometry
  • Pipeline cache — each material creates and owns one pipeline directly; no reuse strategy :::

Shaders: HLSL → SPIR-V + MSL

Shaders are authored once in HLSL and compiled at build time into SPIR-V (for Vulkan) and MSL (for Metal) via glslangValidator + spirv-cross. At runtime, safi_shader_load inspects SDL_GetGPUShaderFormats() and picks the matching artifact — callers only pass a logical shader name like "unlit".

See cmake/SafiShaders.cmake for the build pipeline and Shaders for the HLSL binding layout.

Coordinate conventions

AxisMeaning
+Xright
+Yup
-Zforward
Wind.counter-clockwise = front (SDL_GPU_FRONTFACE_COUNTER_CLOCKWISE)
DepthSDL_GPU_COMPAREOP_LESS, clear = 1.0

These match cglm's glm_lookat / glm_perspective defaults, so you can hand camera matrices straight to the shader without sign flips.

See also