DOTS//CORE local · not synced

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.

cache line · 64 B fetched 48 B useful · 75% of the bandwidth you paid for one render frame containing three fixed steps: the outer clock is variable, the inner quanta are identical, and the leftover remainder carries forward — the step never changes size, only the count

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.

m07.l04