Deterministic Step Pipeline¶
This page describes the current per-tick contract shared by:
- live gameplay via
TickRunner - replay verification and replay info via
PlaybackDriver - replay playback mode
- world/runtime harnesses that step deterministic ticks directly
The core deterministic step lives in src/crimson/sim/step_pipeline.py.
Session orchestration lives in src/crimson/sim/sessions.py.
Canonical tick input lives in src/crimson/sim/input_providers.py.
Shared frame/apply helpers live in src/crimson/sim/frame_pump.py,
src/crimson/sim/driver/playback_pump.py, src/crimson/sim/batch_apply.py,
and src/crimson/sim/presentation_reactions.py.
Architecture Overview¶
flowchart TD
subgraph Sources["Tick Sources"]
Local["LocalInputProvider"]
Lan["LAN TickFrame"]
Replay["ReplayTick"]
end
subgraph Contract["Shared Tick Contract"]
Pull["InputProvider.pull_tick()"]
Tick["ResolvedTick"]
Runner["TickRunner"]
Driver["PlaybackDriver.step_tick()"]
Session["DeterministicSession.step_tick()"]
Result["TickResult"]
end
subgraph Apply["Shared Apply Layers"]
Metadata["apply_sim_metadata_*"]
Present["apply_presentation_outputs()"]
Reactions["build/apply post-apply reactions"]
end
subgraph Consumers["Outer Loops"]
Gameplay["BaseGameplayMode / LAN"]
World["WorldRuntime"]
ReplayMode["ReplayPlaybackMode"]
ReplayTools["verify / info / benchmark / render"]
end
Local --> Pull --> Tick --> Runner --> Session --> Result
Lan --> Pull
Replay --> Driver --> Session
Driver --> Result
Result --> Metadata --> Present --> Reactions
Gameplay --> Runner
World --> Runner
ReplayMode --> Driver
ReplayTools --> Driver
The important architectural point is that live, LAN, replay verification, and
replay playback no longer invent separate in-memory tick/result shapes. They
all converge on ResolvedTick -> DeterministicSession -> TickResult, then fan
out into shared apply layers plus mode- or tool-specific outer loops.
Tick Contract¶
At the runtime boundary, one deterministic tick is one ResolvedTick:
tick_indexdt_secondsinputscommands
Live/LAN paths receive it through InputProvider.pull_tick(...) -> TickSupply.
Replay paths synthesize the same shape inside PlaybackDriver.step_tick(...).
The deterministic result shape is shared too:
TickResult.source_tickcarries the canonicalResolvedTickTickResult.payloadcarriesDeterministicSessionTick- replay stepping additionally sets
TickResult.replay_tick_index
DeterministicSession.step_tick(...) produces a DeterministicSessionTick
whose step is a DeterministicStepResult with:
dt_sim: effective dt after deterministic scalingevents: deterministic sim eventspresentation: deterministic presentation commandspost_apply_sfx_keys: post-apply presentation reactions triggered by successful command effects- optional RNG trace data when replay trace mode is enabled
flowchart LR
ResolvedTick["ResolvedTick<br/>tick_index + dt + inputs + commands"]
Session["DeterministicSession.step_tick()"]
TickResult["TickResult"]
SessionTick["DeterministicSessionTick"]
Step["DeterministicStepResult"]
ResolvedTick --> Session --> TickResult
TickResult --> SessionTick
SessionTick --> Step
Shared Step / Apply Path¶
The runtime is now split into a consistent sequence:
- get a canonical
ResolvedTick - step
DeterministicSession - apply sim metadata to the runtime world
- apply presentation outputs
- apply post-apply reactions
- optionally record checkpoints, replay stats, or sync network state
Shared helpers own the bookkeeping around that sequence:
- live
TickRunnerframe advancement:src/crimson/sim/frame_pump.py - replay playback frame advancement:
src/crimson/sim/driver/playback_pump.py - sim metadata apply and presentation output apply:
src/crimson/sim/batch_apply.py - post-apply presentation reactions:
src/crimson/sim/presentation_reactions.py
This keeps live gameplay, replay playback, headless verification, and runtime harnesses close to the same deterministic contract even when their outer loops differ.
sequenceDiagram
participant Outer as Outer Loop
participant Source as Tick Source
participant Session as DeterministicSession
participant Apply as Shared Apply Layers
participant Hooks as Recording / Sync / Info
Outer->>Source: request next tick
Source-->>Outer: ResolvedTick
Outer->>Session: step_tick(timing, inputs, commands)
Session-->>Outer: TickResult
Outer->>Apply: apply_sim_metadata_*
Outer->>Apply: apply_presentation_outputs()
Outer->>Apply: build/apply post-apply reactions
Outer->>Hooks: optional checkpoints / replay info / LAN sync
Timer Ownership¶
Timer semantics are now explicit instead of implicit:
- survival and rush runtime timing comes from
DeterministicSession.elapsed_ms - quest progression/replay timing comes from
QuestSpawnState.spawn_timeline_ms - render/HUD animation caches use
SimWorldState.presentation_elapsed_ms
That separation is deliberate. The session or spawn state owns authoritative
runtime time; presentation_elapsed_ms is only a presentational cache for
world rendering and HUD animation.
Why This Matters¶
The old split between live gameplay, replay verification, and replay playback made parity drift easier: different tick shapes, duplicated loop bookkeeping, and mode-specific side effects wired in different places.
The current model is simpler:
- live/LAN/replay all step through the same
DeterministicSession - replay uses the same in-memory tick/result shapes as live
- mode-specific runtime state is authoritative in spawn/session state, not echoed through generic tick types
- presentation reactions are applied through one shared post-apply layer
That makes replay verification, checkpoint comparison, LAN parity, and diff investigation much easier to reason about.
Studyability Hook Topology¶
The deterministic step still routes selected behavior through explicit hook registries:
- perk world-step hooks:
- manifest:
src/crimson/perks/runtime/manifest.py - contracts:
src/crimson/perks/runtime/hook_types.py - bonus pickup presentation hooks:
- registry:
src/crimson/bonuses/pickup_fx.py - projectile decal presentation hooks:
- registry:
src/crimson/features/presentation/projectile_decals.py
This keeps WorldState.step and apply_world_presentation_step focused on orchestration.
RNG Policy¶
The deterministic pipeline uses one authoritative RNG stream:
- simulation + presentation RNG:
state.rng
WorldState.step, the deterministic session hooks, and replay verification all
consume that stream in a stable per-tick order.
RNG Trace Mode¶
Replay checkpoint verification exposes --trace-rng:
When enabled, checkpoints include presentation draw counters and replay-driver RNG trace rows that help localize divergence.
Replay Verify¶
replay verify runs the replay headlessly through PlaybackDriver and emits
the simulated run result: ticks, elapsed time, score, kills, weapon/shots
stats, and RNG state.
Header claimed-stat mismatches still return exit code 3.
Replay Info¶
replay info runs the same deterministic replay simulation and emits a
chronological event timeline sourced from collect_replay_info(driver, ...).
uv run crimson replay info replay.crd
uv run crimson replay info replay.crd --format json --json-out analysis/replay/info.json
The machine-readable payload is versioned (schema_version=2) and includes a
summary plus ordered timeline events.
Replay Benchmark And Render¶
replay benchmark and replay render also build on the same replay-driver contract.
uv run crimson replay benchmark replay.crd --runs 8 --warmup-runs 2
uv run crimson replay benchmark replay.crd --mode render --runs 8 --warmup-runs 2
uv run crimson replay render replay.crd
Headless benchmark uses the verify driver. Render benchmark and replay render use replay playback mode on top of the same deterministic replay stepping.
Replay Checkpoints Comparison¶
Replay checkpoints are compared by replaying through the same deterministic driver path and diffing checkpoint state, RNG marks, deaths, events, and score/kills metadata.
uv run crimson replay verify-checkpoints replay.crd
uv run crimson replay diff-checkpoints expected.crd.chk actual.crd.chk
This keeps checkpoint verification aligned with the same deterministic contract used by headless replay validation and replay playback.
Differential Testing Path¶
For original-game comparison, use unified trace (.cdt) tooling.
Frida capture host finalizes raw JSONL directly into .cdt.