Skip to content

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

  1. TRACE_MAGIC: b"crimson_debug_trace_v2\n"
  2. <u32le trace_format_version>
  3. one META chunk
  4. one or more TICK chunks
  5. one FOTR chunk
  6. 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:

  1. kind: META, TICK, or FOTR
  2. start_tick: signed 32-bit tick bound
  3. end_tick: signed 32-bit tick bound
  4. flags: exactly CHUNK_FLAG_MSGPACK
  5. compressed_len: stored payload length
  6. uncompressed_len: stored payload length
  7. checksum64: 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 -> TraceMeta
  • TICK -> TickBlock
  • FOTR -> 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_index
  • elapsed_ms
  • dt_ms_i32
  • mode_id
  • channels

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 delta
  • inputs: a non-empty row per player containing move_x, move_y, aim_x, aim_y, and uint32 flags
  • prelude: ordered native frame-RNG advances and perk operations applied before simulation
  • postlude: perk-menu generation applied after simulation while tick RNG tracing remains active
  • commands: 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:

  • heading
  • move_speed
  • move_phase
  • aim
  • aim_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_15
  • state_before_u32
  • state_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:

  1. replay_step
  2. checkpoint
  3. rng_stream
  4. sim_state
  5. entity_samples
  6. timing_samples

The JSON report exposes:

  • mismatch: the earliest divergent tick and all behavioral channel mismatches at that tick
  • channel_first_mismatches: the first behavioral mismatch for every channel across the selected range
  • channel_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.