Closed
Labels
architectureEngine architecture / cross-cutting designEngine architecture / cross-cutting designfeatureNew feature or requestNew feature or request
Description
Problem
The gameplay simulation is neither deterministic nor on a unified fixed timestep:
- Only Jolt 3D physics has a fixed-step accumulator. Box2D and all gameplay systems (scripts, animation, AI, particles, navigation) advance on raw variable frame delta — see the variable-delta path in
Core/Application.cppRun()(std::min(rawDelta, s_MaxTimestep) * m_TimeScale). - Global RNG is seeded from system time (
RandomUtils::GetGlobalRandom()/GetTimeBasedSeed()inMath/FastRandom.h) with no fixed-seed injection.
Consequence: gameplay behavior is frame-rate dependent and runs aren't reproducible.
The kicker
You already built Networking/P2P/RollbackManager + lockstep P2P, which require a deterministic simulation (any float divergence or RNG difference desyncs). That feature is effectively dead weight until determinism exists. Fixing this also unlocks replays and rock-steady physics.
Proposed approach
- Wrap the whole gameplay update in a fixed-timestep accumulator. The headless
RunHeadless()path already does this at a fixed tick — generalize it to the windowed loop. Render at display rate; interpolate visual state between ticks. - Step Box2D and gameplay systems on the fixed dt, not the frame dt.
- Add a
SetRandomSeed(u64)entry point and route gameplay RNG through a seeded deterministic stream (PCG/Xoshiro already exist — just stop time-seeding). - Audit gameplay systems for other nondeterminism (container iteration order feeding sim, fast-math).
Acceptance criteria
- Two runs with the same seed + same inputs produce identical gameplay state.
- Gameplay/physics behavior is independent of frame rate.
RollbackManagerP2P stays in sync across peers in a smoke test.
Tier A — architecture. Highest-ROI: it makes the already-built rollback netcode actually usable.
Score
capability: 13
craft: 2
stability: 13
decay: 5
effort: 8
confidence: 0.8
learning: 3
fun: 3
kano: table-stakes
blocked_by: []
blocks: []
Rated per issue-scoring · score = confidence × (capability + craft + stability + decay) / effort, derived by the picker.
Reacted by coderabbitai
Metadata
Metadata
Assignees
Labels
architectureEngine architecture / cross-cutting designEngine architecture / cross-cutting designfeatureNew feature or requestNew feature or request
Activity
coderabbitai commentedon Jun 28, 2026
🔗 Related PRs
#84 - Feature/rng [merged]
#305 - ci(physics): retry flaky PhysicsLayerFilteringTest + harden JoltJobSy… [merged]
#311 - feat(physics): breakable 3D joints (break force / break torque) [merged]
#400 - fix(net,task): isfinite-guard untrusted float ingestion (network snap… [merged]
🧪 Issue enrichment is currently in open beta.
To disable automatic issue enrichment, add the following to your
.coderabbit.yaml:💬 Have feedback or questions? Drop into our discord!
drsnuggles8 commentedon Jun 30, 2026
First shippable slice landed — deterministic fixed-timestep loop + seeded RNG
Branch
feature/deterministic-fixed-timestep(commits080dc573,69aa9357).What changed
Scene::OnUpdateRuntimeFixed(frameTs, fixedDt)accumulates the raw frame delta and advances the gameplay simulation in fixedfixedDtsteps (N catch-up steps, clamped against a spiral of death), rendering once per displayed frame. Windowed hosts (editor Play,OloRuntime) use it; the headless server and the test/Functional harness keep the exact-single-stepOnUpdateRuntimecontract they rely on.OnUpdateRuntimewas split intoUpdateStreaming/SimulateRuntimeStep/RenderRuntimeso both paths share one sim step.RandomUtils::SetGlobalSeed(u64)+Application::SetRandomSeed/GetRandomSeed, re-seeded atScene::OnRuntimeStartso every Play / runtime launch replays from a known seed (fixed default → deterministic by default). The stream is deterministic-by-design, not cryptographically secure — competitive anti-prediction is a separate server-authority concern, intentionally out of scope.Scene::GetSimulationTick()— the addressable tick index that rollback/replay keys off.Acceptance criteria
FixedTimestepDeterminismTest.SeededRngReplaysIdenticallyFromASeed+ the existingDeterministicReplayProducesSameStateTest.FixedTimestepDeterminismTest.GameplayIsFrameRateIndependent(three frame pacings → identical fixed-step count and final state).RollbackManagerP2P stays in sync across peers in a smoke test —P2PRollbackDeterminismTest.RollbackResimReconstructsIdenticalCrossPeerState(an out-of-order input forces a rollback +EntitySnapshotrestore + re-sim; the rolled-back peer ends bit-for-bit identical to the straight-through peer).Full GoogleTest suite green (3827 passed, 0 failed, 1 unrelated skip).
Deferred follow-ups (leaving this epic open to track)
HeadlessTickRate).RollbackManagercross-peer sync proof over the real transport (this slice covers the deterministic-reconstruction smoke test).Incidental: pre-existing renderer bug fixed
While validating the full suite, root-caused and fixed a latent bug surfaced by test ordering (not part of this feature):
OpenGLTextureCubemap::GetFaceData/OpenGLTexture2D::GetDatacheckedglGetError()after their readback without draining a pre-existing error first, so an error leaked by an earlier render was misattributed as a readback failure (a spurious all-black cubemap inProceduralSkyBakeTest, reproducing only under full-suite ordering). Fixed in69aa9357, with a regression test that leaks a GL error before a cubemap readback.fix: address xhigh code-review findings on fixed-timestep + RNG (#452)
refactor(scene): share one physics step; reset sim clock in Simulate …
drsnuggles8 commentedon Jul 1, 2026
Delivered by #484 (merged 2026-07-01); worktree removed. Added a canonical fixed-timestep gameplay update path (gameplay systems + Box2D stepped on fixed dt, render at display rate) and a configurable runtime random seed (
SetRandomSeed) applied at runtime start, plus functional coverage for fixed-timestep determinism, seeded-RNG replay, and strengthened rollback-determinism. Meets all three acceptance criteria. Flagging for closure.drsnuggles8 commentedon Jul 9, 2026
Follow-up: the particle/sub-emitter determinism gap this issue flagged (particle jitter rode the thread-local, time-seeded RNG stream and was not reproducible off the game thread) is now closed by #588. Each
ParticleSystemowns a per-system RNG seeded fromrunSeed × entity UUID, so emission replays identically regardless of thread placement or how many other consumers touched the shared global stream. This also enabled the worker-dispatchedParticlesCPUsplit (#576 item 2).