m07 · Advanced Patterns · reading · 9 min
System groups and the fixed step — giving determinism a place to live
The conflict between two clocks
The last lesson ended on a requirement: a deterministic simulation needs a fixed timestep. This lesson is about what that costs structurally, because it forces a genuine conflict into the frame.
Rendering wants a variable rate. Draw as often as the hardware allows — 144 Hz on a good monitor, 40 on a struggling one, whatever the frame took. The display should show the freshest possible state.
Simulation wants a fixed rate. Every step must advance exactly the same amount of time, or the same inputs produce different results on different machines and determinism is gone.
These are incompatible. You cannot run one loop at both rates, so the frame
splits: a variable-rate outer loop that renders, containing a fixed-rate inner
loop that simulates. That’s FixedStepSimulationSystemGroup — not a
convenience, but the structural consequence of wanting determinism and smooth
rendering simultaneously.
Why catch-up rather than scaling
Given a frame that took 33 ms and a simulation step of 16.67 ms, there are two ways to reconcile them, and only one preserves determinism.
Scale the step. Run one simulation step with dt = 33 ms. Simple, no loop.
And it destroys the property entirely — the step size is now a function of frame
rate, so a machine at 30 fps computes different physics than one at 60. This is
precisely the variable-delta problem the fixed step existed to eliminate.
Loop until caught up. Run two steps of 16.67 ms, carrying the 0.33 ms remainder into the next frame. Every step is identical in size, so every step is reproducible. The simulation’s clock advances in fixed quanta regardless of how the render clock behaves.
The second is forced. Determinism requires that the step never varies, which means the only free variable is how many steps you take.
The accumulator carries the remainder because throwing it away would make the simulation clock drift from wall time. Over minutes, a discarded few milliseconds per frame becomes seconds of desync between what the simulation thinks happened and what the player experienced.
The spiral of death
Catch-up has a failure mode, and it’s inherent rather than a bug — it falls out of the design and every fixed-step system must handle it.
Suppose simulation steps get expensive: a big physics pile-up, a spawn burst. Each frame now needs more steps than it can afford. So the frame takes longer. So next frame there’s more accumulated time to catch up on. So it needs more steps. So it takes even longer.
That’s a positive feedback loop, and it doesn’t recover on its own — the simulation falls progressively further behind, the frame rate collapses toward zero, and the game hangs while technically still running.
The fix is a maximum step count per frame. When the accumulator demands more steps than the cap allows, the extra time is discarded and the simulation formally runs slow — game time advances slower than wall time.
That is a deliberate, correct choice, and it’s worth seeing why. The alternatives are: hang forever (spiral), or vary the step size (lose determinism). Running in slow motion is the only option that keeps the simulation both responsive and reproducible. When overloaded, a fixed-step simulation trades time fidelity to preserve determinism — it would rather be slow and correct than fast and divergent.
Where things belong
The group structure now decodes completely, and each placement is derivable from what the group guarantees rather than from convention.
FixedStepSimulationSystemGroup — anything that must be deterministic or
that integrates over time. Physics, movement, gameplay state machines,
networked simulation. Runs zero or more times per frame with an identical step.
SimulationSystemGroup (variable) — logic that should run once per frame
and doesn’t integrate. Input sampling, AI decisions that don’t need fixed
timing, anything cosmetic.
PresentationSystemGroup — rendering, and crucially interpolation. The
simulation is at a discrete tick that rarely aligns with the display moment;
presentation interpolates between the last two simulated states so motion looks
smooth despite being computed in quanta.
That last point closes a loop from Module 6. Interpolating between transform
states is exactly why LocalTransform stores decomposed TRS with a quaternion
rather than a matrix — because presentation blends between simulation ticks
every frame, and blending matrices produces garbage. The storage decision in M6
and the frame structure here are the same requirement seen from two ends.
What this buys you
You can now explain why a fixed step forces a catch-up loop rather than a scaled delta, why the spiral of death is inherent and what bounding it costs, and why presentation interpolation is the same requirement that shaped Module 6’s transform storage. Next, the lab: you will make a simulation diverge, then fix it.