m07 · Advanced Patterns · reading · 10 min
Determinism — why the same inputs must give the same frame
The property, stated precisely
A simulation is deterministic if running it twice from the same starting state, with the same inputs, produces byte-identical results — every frame, every entity, every float.
That sounds like a nicety. It is in fact the load-bearing property behind three things you will want, and one of them is the module this course is building toward:
Replay. Record inputs, not state. A ten-minute match is a few kilobytes of button presses rather than gigabytes of snapshots — but only if replaying those inputs reconstructs the match exactly.
Rollback. Predict ahead, discover you were wrong, rewind, re-simulate with corrected inputs. This is the heart of Netcode’s prediction, and it works only if re-simulating produces the same result the original run would have.
Lockstep networking. Send only inputs across the network; every client simulates identically. Bandwidth becomes independent of world size. One divergent float and the clients silently drift into different games.
Notice what all three have in common: they re-run the simulation and require the re-run to match. Determinism isn’t about being tidy. It’s the thing that makes re-running meaningful.
Why it’s harder than it looks
The naive expectation is that computers are deterministic by nature — same code, same input, same output. For a single-threaded integer program, roughly true. For a parallel floating-point simulation, false in several specific ways, and it’s worth knowing exactly which.
Floating-point addition is not associative. (a + b) + c does not always
equal a + (b + c), because each addition rounds. The difference is one unit
in the last place — vanishingly small, and completely fatal, because the
simulation feeds the result back into the next frame and the error compounds.
Two machines that disagree by one ULP on frame 1 disagree visibly by frame 400.
This is the reason parallel accumulation is the classic determinism killer. Sum a million values across eight cores and the order of combination depends on which core finished first, which depends on cache state, other processes, and luck. Run it twice, get two different sums. Both are “correct” to within floating-point tolerance, and the simulation diverges anyway.
Iteration order.
What DOTS actually gives you
Here is the honest picture, and it’s more nuanced than “DOTS is deterministic.”
Burst is deterministic across identical hardware and settings. Given the
same target and the same compilation, the same instructions execute in the same
order producing bit-identical results. Unity.Mathematics is specified to be
deterministic in a way UnityEngine.Mathf is not, which is one more reason for
the Module 3 rule about which math types to use.
Single-threaded system execution is deterministic, because ordering is declared (Module 2), not incidental. Same declared order, same execution, same result.
Parallel jobs are deterministic only if the work is order-independent. And here the Module 1 property comes back around with new significance: if each entity’s result depends only on its own data, then the order entities are processed in cannot affect the outcome. Order-independence isn’t just what makes parallelism safe — it’s what makes parallelism deterministic.
That’s the key insight and it reframes everything. The same property that lets a job parallelize is what lets it stay deterministic. A job that writes only per-entity results is deterministic no matter how the scheduler distributes it. A job that accumulates into shared state is not, because the accumulation order varies.
Getting determinism, in practice
The recipe follows from the failure modes, and it is a discipline rather than a setting.
Use a fixed timestep. Variable delta time makes the simulation a function
of frame rate, so two machines running at different speeds compute different
physics. Simulation runs on a fixed step; rendering interpolates for smoothness.
This is why FixedStepSimulationSystemGroup exists as a separate group from the
ordinary simulation group.
Keep parallel work order-independent. Per-entity results are free. If you genuinely need a reduction, either do it single-threaded, or use a deterministic reduction that combines in a fixed order regardless of completion order.
Make ordering explicit. Declared system order (Module 2), declared command buffer playback points, and where iteration order matters, sort by a stable key like entity index rather than relying on whatever the chunk gives you.
Use Unity.Mathematics. Module 3’s argument was vectorization; the
determinism argument is separate and equally binding.
Seed your randomness explicitly. A Random seeded from the clock is
nondeterministic by construction. Seed from something derived from simulation
state — tick number, entity index — so the same tick always produces the same
sequence.
What this buys you
You can now explain why determinism is a pipeline property rather than a setting, why floating-point non-associativity makes parallel accumulation the classic failure, why order-independence is simultaneously what makes jobs parallel and what makes them deterministic, and why thread-safe does not imply deterministic. Next: system groups and the fixed timestep — the machinery that gives determinism a place to live in the frame.