On this page

Determinism: sources and measurements

The research behind the determinism skill and the C# example's Determinism/Replay.cs (IDeterministic, DeterminismCheck, DeterministicRandom), gathered on 2026-10-21. It draws on the .NET runtime source and issues, Microsoft and Godot documentation, Box2D, Jolt, GGPO and Factorio. Tests ran on Linux x86_64 (Intel Xeon 2.8 GHz with FMA, AVX2 and AVX-512F, 4 cores), .NET SDK 8.0.131 (runtime 8.0.31), Godot 4.7.2 mono and standard, and Godot 4.6.1 standard for one comparison. Marks: tested means run here, source means read in the code, doc means stated by the vendor or author, and unconfirmed means not verified.

Sources

Source What it gave
dotnet/runtime #64591, #64604 The JIT keeps fmul and fadd on Arm64: contraction is "not an IEEE 754 compliant optimization"; the runtime's native code builds with -ffp-contract=off
Red Hat .NET known issues (search summary) Different Math.Cos values on different platforms are "expected behavior"
System.Random, Random.cs No promise across major versions; seeded instances use the legacy generator
Erin Catto, Determinism (Box2D, 2024) Fixed-point is no longer required; FMA and trigonometry are the traps; test it in CI
Glenn Fiedler, Floating Point Determinism Same compiler and architecture make floats repeatable
Jolt, Architecture: Deterministic Simulation The conditions, and CROSS_PLATFORM_DETERMINISTIC
Godot: using Jolt Physics; Godot 4.7 modules/jolt_physics/SCsub, platform/*/detect.py Jolt is the default for new projects; built without the cross-platform option; C++ built with -ffp-contract=off or /fp:strict
GGPO Developer Guide SyncTest
Jonathan Blow, The Implementation of Rewind in Braid (GDC; notes) Record full state every frame; keyframes diffed against a default entity, frames against their keyframe; chunked forgetting: Determinism/Rewind.cs
Factorio FFF #55, FFF #47, forum A checksum every tick; diff the first diverging tick; sin differs between Windows and Linux
Bettner and Terrano, 1500 Archers on a 28.8 (GDC 2001) Out-of-sync errors were the hardest problem in lockstep
Backdash (MIT), godot-rapier-physics A GGPO-style C# library with a SyncTest session; a physics add-on with an optional cross-platform build
"Automated Testing and Instant Replays in Retro City Rampage" (GDC, about 2015; notes) Always-on input recording dumped on exit or assert, skip to the bug's frame, per-button run-length streams under zlib (1 to 8 KB), one-byte checksums, separate random streams, no engine callbacks in gameplay, a header version that switches old behaviour on: Determinism/InputLog.cs

.NET floating point (tested)

A 64-body float simulation (square roots, MathF.Sin and Cos, drag) for 20,000 steps, a double orbit with Math.Sin, Atan2, Pow, Exp and Tan, a FixedMath.NET simulation and an integer Q16.16 one, each hashed by the bits of its final state:

Configuration Result
Debug, Release Identical hashes for all four
DOTNET_TieredCompilation=0; DOTNET_TieredPGO=0; DOTNET_TC_QuickJit=0 Identical
DOTNET_EnableFMA=0, EnableAVX2=0, EnableAVX512F=0, EnableHWIntrinsic=0 Identical
Vector<float> sum of 10,007 values, AVX2 (8 lanes) and DOTNET_EnableAVX2=0 (4 lanes) 0x44E31562 and 0x44E31568: different
The same sum as a plain loop Different again (1816.6674 against 1816.6682)
a*b+c with 0.1f, 10f and -1f, against MathF.FusedMultiplyAdd 0 and 1.4901161E-08: the JIT does not fuse (also in double)

Other sources of nondeterminism in .NET (tested)

What Result
new Random(42) in two processes The same sequence (1434747710 302596119 269548474 ...) on runtime 8.0.31
HashCode.Combine(42, 1, 2, 3) in two processes 1215531979, then -295045105; string.GetHashCode() and tuples also change; 42.GetHashCode() does not
Dictionary after removes and adds New keys fill freed slots: order follows the add and remove history, not insertion
float.Parse("0.5") with LANG=de_DE.UTF-8; with fr_FR 5; FormatException (also inside Godot C#)

Microsoft: "you shouldn't assume that the same seed will result in the same pseudo-random sequence in different versions of .NET". The source keeps the legacy generator for seeded instances "to respect the same algorithm that's been used in the past". System.Random exposes no state to save or restore, which rules it out for rollback; Godot's RandomNumberGenerator (PCG32) has a saveable State. Parallel.For and ConcurrentDictionary order were not tested.

Godot physics (tested)

200 RigidBody3D (boxes and spheres) dropped on a floor for 300 physics frames, headless, hashed with SHA-256 over all transforms; 300 RigidBody2D for 2D.

Run GodotPhysics3D Jolt GodotPhysics2D
Three separate runs Identical Identical Identical
Pinned to one core; mono instead of standard; Godot 4.6.1 instead of 4.7.2; separate physics thread Identical Identical Not run
The same bodies added in reverse order Different Different Not run
One unrelated body added far away at frame 5 Different Identical Not run

Fixed-point and integers (tested, Release, x64)

Work float FixedMath.NET Fix64 FixedMathSharp Fixed64 Integer Q16.16
1.28 M body-steps (sqrt, 1/x, sin, cos, multiply-adds) 19 to 20 ms 1.19 to 1.41 s (60 to 74 times) Not run 12 to 17 ms
50 M multiply-adds 135 to 144 ms 374 to 494 ms (2.8 to 3.6 times) 624 to 800 ms (4.6 to 5.6 times) Not run
5 M Sin 17 to 33 ms 958 to 990 ms 748 to 914 ms Not run
5 M Sqrt 14 ms 863 to 924 ms 824 to 968 ms Not run

Replays and desync detection (doc)

Engine completion signals (tested)

A probe scene on Godot 4.7.2 standard, headless, started a one-second silent AudioStreamWAV, a one-second AnimationPlayer animation, a one-second Tween and a one-second SceneTreeTimer, and logged the physics tick of each signal, 20 runs per mode. Real time: finished on ticks 56 to 62, animation_finished on 60 or 61, the tween and the timer always on 60. With --fixed-fps 60: the sound had not finished after 200,000 ticks (audio follows the wall clock: after 100,000 ticks, 0.47 s after the engine started, 0.28 s of the sound had played), the animation always 61, tween and timer 60. Hence the skill's rule: gameplay counts its own ticks.

The C# example's toolkit

examples/csharp/src/Example.Core/Determinism/Replay.cs turns the above into code: IDeterministic (Save, Load, Step, Checksum), DeterminismCheck.Record (a checksum every interval ticks), Verify (the first checkpoint that differs), SyncTest (every tick, re-run the last depth ticks and compare; depth 7 in RollbackTests), Fnv1a (a hash that is the same in every process) and DeterministicRandom (xorshift64*, its whole state one ulong). DeterminismTests.cs runs a ball in 1/1000 px integer units for 3600 ticks: the replay matches all 61 checkpoints, an input changed at tick 137 is found at tick 180, SyncTest catches a shared System.Random kept outside the state, TheRandomSequenceIsFixedForever pins the generator's first values, and TheChecksumIgnoresTheProcess checks FNV-1a against its published vector for "a". InputLog.cs stores a session's input for playback (numbers in gdc/retro-city-rampage-replays.md).

Not confirmed