Scheduler & Stages
SafiEngine runs four flecs pipelines per frame, in a fixed order. Each pipeline selects systems by the phase they depend on, and two of them are gated by the current editor mode.
The engine does not use the default flecs pipeline. It builds four custom pipelines in safi_ecs_create, each matching a specific set of phases, and safi_app_tick calls ecs_run_pipeline on them in order. The fixed and game pipelines are skipped entirely when SafiEditorState.mode != PLAY.
Assigning a system to a stage
Systems opt into a stage by depending on any phase that pipeline matches.
Variable-rate
Runs once per frame at wall-clock dt. Use this for input-driven logic, camera smoothing, gameplay that should feel smooth regardless of frame rate, and (when step 2 lands) transform propagation on EcsPostUpdate.
Fixed-rate
Runs N times per frame at SafiTime.fixed_delta (default 1/60 s). Use this for physics and any deterministic simulation where stable dt matters. SafiFixedUpdate is intentionally not chained to any default flecs phase, so it is invisible to the variable pipeline — it only runs from the fixed-step accumulator loop. Skipped when editor mode is not PLAY.
Gameplay (variable-rate, pausable)
Runs once per frame at wall-clock dt, but only when SafiEditorState.mode == PLAY. Use this for user gameplay systems that should freeze in Edit mode (player movement, AI, scripted events). Engine-owned variable systems — input pumping, camera updates, transform propagation — stay on EcsOnUpdate so the Inspector and viewport remain responsive while the game is paused.
Render
Runs last in the frame, after both the variable and fixed stages have settled. Use this for draw systems.
Fixed-timestep accumulator
Each frame the engine adds dt to an accumulator and then runs SafiFixedUpdate for as long as the accumulator has a full step in it, capped at SafiApp.fixed_max_steps per frame to prevent the spiral-of-death:
If the cap is hit during Play, the leftover time is discarded rather than carried into the next frame. SafiTime.fixed_overshoot exposes the surviving remainder for interpolation.
Editor mode gate
The fixed and game pipelines are gated on SafiEditorState.mode. The full tick:
Toggle the mode at runtime via safi_editor_set_mode (see Editor State). The gltf_viewer demo binds F1 to Edit ↔ Play so you can watch the falling cube freeze and resume.
Pipeline accessors
Exposed for tests and for apps that want to step individual stages manually. Normal code doesn't touch these — safi_app_tick handles them.
:::warning WIP — scheduling gaps
- Startup stage — no one-shot startup system registration API; initialization runs in
main()beforesafi_app_run - Higher-level ordering API — only raw flecs
ecs_dependsonphase ordering; no explicit before/after system dependencies :::
See also
SafiApp— owns the main loopSafiTime— variable and fixed clocks- Editor State — the mode flag that gates the fixed + game pipelines
- ECS Overview