CDT trace format¶
CDT is Crimson's owned debug-trace container. It is used by
crimson dbg record|health|diff|bisect|focus|tick|entity|query; it is not an
original Crimsonland asset format.
This document specifies the only supported contract implemented by
crimson-re/src/crimson_re/dbg/schema.py and crimson-re/src/crimson_re/dbg/trace.py.
The Python port is the only producer: crimson dbg record
(crimson-re/src/crimson_re/dbg/record.py) records a trace while it plays a
.crd replay, so CDT localizes regressions between two port revisions.
Original-code behavior is checked by the
native execution oracle
and the recovered core gate.
Versioning¶
| Artifact | Current version | Authority |
|---|---|---|
| CDT container | 2 | crimson-re/src/crimson_re/dbg/schema.py |
| CDT payload schema | 21 | crimson-re/src/crimson_re/dbg/schema.py |
| CRD replay | 34 | src/crimson/game_version.py |
scripts/check_docs.py keeps this table in step with the sources. Traces are
throwaway debugging data: a trace reader requires exactly the CDT versions and
does not translate, normalize, or salvage an older recording. Re-record a trace
when a version changes. dbg record reads replays as the
replay format allows.
File layout¶
TRACE_MAGIC:b"crimson_debug_trace_v2\n"<u32le trace_format_version>- one
METAchunk - one or more
TICKchunks - one
FOTRchunk - trailer
<8-byte magic, u64le footer_offset>
The trailer magic is b"CDTFTR2\n".
Chunks are adjacent in the declared order: unindexed bytes, padding, extra
chunks, and bytes between the footer and trailer are rejected.
Chunk envelope¶
Each chunk has header struct <4siiIIIQ> followed by its payload:
kind:META,TICK, orFOTRstart_tick: signed 32-bit tick boundend_tick: signed 32-bit tick boundflags: exactlyCHUNK_FLAG_MSGPACKcompressed_len: stored payload lengthuncompressed_len: stored payload lengthchecksum64: little-endian blake2b-64 of the payload
The legacy length field names remain in the fixed envelope, but CDT v2 payloads are raw msgpack. The two lengths are equal and the zstd flag is not accepted.
Payload types are:
META->TraceMetaTICK->TickBlockFOTR->TraceFooter
TraceFooter indexes every tick block and records the total count plus first
and last tick. Its offset in the trailer allows direct lookup without scanning
the whole trace.
Metadata¶
TraceMeta contains:
- the exact container and schema versions
- creation time
- typed producer identity
- typed source identity and replay fingerprint
- declared tick range
- optional captured game status
TraceMeta.status carries the run's unlock indices and weapon usage counts with
every other status field zero. Unknown fields are rejected. The declared tick range must exactly match the rows and footer written to disk.
Tick blocks¶
A TickBlock contains ordered TickRecord rows. Every row contains:
tick_indexelapsed_msdt_ms_i32mode_idchannels
Tick indices must be non-negative, strictly increasing, and unique. A CDT may represent a selected window and therefore need not start at zero. The health report exposes gaps so a user can decide whether the selected evidence is suitable for a comparison.
elapsed_ms is the Quest simulation spawn timeline in Quest mode, including
time scaling and completion pauses. Other modes use the summed unscaled replay
clock. dt_ms_i32 always remains the unscaled frame delta. Outside Quest mode,
canonical quest stage fields are 0.0; stale native globals belong in evidence.
Channel contract¶
Every tick requires all six channels:
| Channel | Payload |
|---|---|
replay_step |
ReplayStepSnapshot |
checkpoint |
ReplayCheckpoint |
sim_state |
SimStateSnapshot |
entity_samples |
EntitySamplesSnapshot |
rng_stream |
list[RngStreamRow] |
timing_samples |
list[TimingSampleRow] |
replay_step¶
The replay step is the authoritative driving evidence for the tick:
dt: finite, non-negative f32 frame deltainputs: a non-empty row per player containingmove_x,move_y,aim_x,aim_y, and uint32flagsprelude: ordered native frame-RNG advances and perk operations applied before simulationpostlude: perk-menu generation applied after simulation while tick RNG tracing remains activecommands: replay commands (perk and Typ-o) applied as part of the tick
Replays step a fixed 60 Hz schedule, so their traces report
dt = float32(1/60) and empty prelude and postlude. Input bit 17,
fire_bullets_key_down, records the native G-key Fire Bullets shortcut;
playback honors it only with preserve_bugs enabled.
Checkpoint and simulation player counts must equal the input count.
checkpoint¶
The checkpoint tick and elapsed time must equal their enclosing TickRecord.
Effective hit and pickup counts are cross-producer fields. The non-equivalent
audio count, detailed death rows, and SFX/hit heads are zero or empty in CDT.
Replay-only checkpoint sidecars may retain their fully typed rows. This makes the shared
channel strictly comparable without producer masks.
It carries the compact deterministic state used for fast divergence detection, as a checkpoint sidecar does
(replay run start); its rng_callers_crc32 covers the call sites
rng_stream lists.
sim_state¶
The gameplay mode must equal the enclosing mode. Each player row includes position and gameplay state plus the movement fields needed to explain input integration:
headingmove_speedmove_phaseaimaim_heading
entity_samples¶
Creature, projectile, secondary-projectile, and bonus samples use stable UIDs.
UIDs must be unique within each entity kind for a tick, allowing dbg entity
to follow slot reuse without confusing two lifetimes. Generations advance on
successful allocations, including an active projectile slot being overwritten
or a slot being retired and reused between snapshots. Failed allocations to
native sentinels do not advance a real slot. Replay pools start a new epoch at
reset. Generation zero denotes a directly seeded implementation state without
an observed allocation. Generation overflow is an error, never UID reuse.
rng_stream¶
Each row contains:
- one-based
tick_call_index value_15state_before_u32state_after_u32- optional static
caller
Every row must be a valid CRT LCG transition, value_15 must derive from the
after-state, and consecutive rows must form one contiguous chain. A gap is an
incomplete recording, not replay input.
The caller is diagnostic attribution. Equal values and states with different callers produce a caller-attribution diagnostic, not an RNG behavior mismatch.
timing_samples¶
Every tick has a non-empty timing set with exactly one gpur_enter row. Its
frame_dt_f32 equals replay_step.dt, its frame_dt_ms_i32 equals the
enclosing dt_ms_i32, and its mode_fn identifies
gameplay_update_and_render.
Diff contract¶
Run dbg health on both traces before interpreting a diff. Health validates the
tick records, reports tick spans and gaps, counts rows per channel, and exits
nonzero when the selected window is not ready for comparison.
dbg diff compares ticks in deterministic channel order:
replay_stepcheckpointrng_streamsim_stateentity_samplestiming_samples
The JSON report exposes:
mismatch: the earliest divergent tick and all behavioral channel mismatches at that tickchannel_first_mismatches: the first behavioral mismatch for every channel across the selected rangechannel_first_diagnostics: the first non-behavioral diagnostic for every channel, including RNG caller-only differences
Strict field mismatches name the path and include expected/actual values. Finite float mismatches additionally include numeric delta, expected and actual f32 hex encodings, and f32 ULP distance.
Use dbg bisect to bound the earliest bad tick and dbg focus, dbg tick,
dbg entity, or dbg query to inspect the relevant channel and entity history.