tickwise 0.2.0

Record, replay, and diff deterministic simulations
Documentation

Tickwise

Record, replay, and diff deterministic simulations.

Tickwise is an engine-agnostic recording, replay, and desync-debugging toolkit for deterministic multiplayer games, written in Rust. Determinism is a promise that must be verified every single tick, and Tickwise exists to make that vigilance cheap.

⚠️ Status: early development. Version 0.2.0 is on crates.io as tickwise and tickwise-cli and covers the full two-pass workflow: record, compare, replay, diff. The API and the recording format may change freely until 1.0.

Try it

cargo add tickwise --features serde    # the library, with the serde convenience layer
cargo install tickwise-cli             # the tickwise binary

With the serde feature, any Serialize state becomes a probe in a few lines:

use serde::Serialize;
use tickwise::serde_probe::SerdeProbe;
use tickwise::{Recorder, RecorderConfig};

#[derive(Serialize)]
struct Game { tick: u64, score: u64, positions: Vec<(f32, f32)> }

let mut game = Game { tick: 0, score: 0, positions: vec![(0.0, 0.0)] };
let mut rec = Recorder::create("session.rec", RecorderConfig::default())?;
for tick in 0..600 {
    let input = (1u8, 0u8);         // your own input type
    game.tick += 1;                 // your own simulation step
    rec.record_tick_typed(tick, &input, &SerdeProbe::new(&game))?;
}
rec.finish()?;

Performance-sensitive code implements the three-method DeterminismProbe trait by hand instead. Then the CLI takes over:

tickwise inspect session.rec        # what is in a recording
tickwise compare a.rec b.rec        # first divergent tick between two sessions
tickwise diff a.dump b.dump         # field-level differences at that tick

The problem

Deterministic simulation is the foundation of lockstep and rollback netcode. Every client runs the same simulation from the same inputs and must arrive at the same state. When that promise breaks, even by a single divergent bit, two players fork into different realities. This failure mode is called a desync, and it is uniquely expensive to debug for three reasons.

  1. The symptom appears far from the cause. A divergence at tick 4,021 typically surfaces to a human minutes later, as impossible gameplay or a checksum kick.
  2. Reproduction dominates the cost. Without recorded inputs and hashes, reproducing a desync locally is guesswork, and reproduction is most of the work.
  3. The tooling is always bespoke. Studios that ship deterministic multiplayer keep rebuilding the same three components privately: input and hash recording, first-divergence search, and structural state diff.

No open-source, engine-agnostic equivalent of that in-house tooling exists. Tickwise fills the gap. It records sessions cheaply, finds the first divergent tick in seconds, and reports the diverging subsystem and field.

How it works

Tickwise is an observer. It never runs your simulation. You drive your own game loop and call into the kit, which keeps it invasion-free and engine-agnostic. The analysis happens in two passes.

┌─ PASS 1 (always on, cheap) ───────────────────────────────┐
│ Client A plays → a.rec   (inputs + per-tick hashes)       │
│ Client B plays → b.rec                                    │
│                                                           │
│ $ tickwise compare a.rec b.rec                            │
│ → "First divergence: tick 4021 (light-hash mismatch,      │
│    confirmed by full hash at tick 4200)"                  │
└───────────────────────────────────────────────────────────┘
┌─ PASS 2 (targeted, on demand) ────────────────────────────┐
│ Replay a.rec in your own loop with                        │
│ dump_at_tick = 4021 → a.dump                              │
│ Same for b.rec → b.dump                                   │
│                                                           │
│ $ tickwise diff a.dump b.dump                             │
│ → "tick 4021: players[2].velocity.x                       │
│    A: 3.5  B: 3.5000001  (sub-epsilon float drift)"       │
│ → "tick 4021: projectiles.len  A: 14  B: 15 (structural)" │
└───────────────────────────────────────────────────────────┘

There is an even simpler entry point: the self-check. Play a session once, replay its recorded inputs through your simulation, record that too, and compare:

tickwise compare original.rec replayed.rec

If the verdict is anything but identical, your simulation is not deterministic, and you just found out before your players did.

CLI

Three commands in v1, no more:

tickwise compare a.rec b.rec   # first divergent tick + hash kind + summary
tickwise diff a.dump b.dump    # structural diff, float-classified, colored output
tickwise inspect session.rec   # metadata + statistics

The diff classifies rather than judges. Differences are reported as Structural, Exact, or SubEpsilonFloat, so both float-based and fixed-point simulations are first-class citizens.

How Tickwise compares

Photon Quantum GGRS SyncTest rr debugger In-house tools Tickwise
Open source ✗ commercial
Engine-agnostic ✗ Quantum only ✗ GGRS sessions only n/a ✗ project-specific
Recording format + offline compare ✓ replay files ✓ syscall level partial
Field-level state diff partial ✗ checksum only ✓ bespoke
Simulation-level semantics: ticks, game state

rr records execution at the syscall level. Tickwise records simulation at the tick level, which is the layer where "tick 4021, players[2].velocity.x diverged" is even expressible. GGRS users are especially welcome: Tickwise complements SyncTest with a persistent recording format, offline comparison, and structural diffs.

Roadmap

Milestone Content Definition of done Status
M0 Workspace skeleton, probe trait, reference simulation Refsim runs 10k ticks deterministically, CI green
M1 Recorder, .rec format, inspect Recording round-trip tests pass, 0.1.0 on crates.io
M2 compare for first divergence, chaos flags All chaos classes caught at the correct tick
M3 Replayer, dumps, diff, serde layer, GGRS integration Two-pass workflow end-to-end, 0.2.0 on crates.io
M4 Launch package: docs, examples, tutorial, benchmarks A stranger finds their first desync in 15 minutes, unaided in progress

Non-goals

Tickwise deliberately does not include:

  • ❌ Network or transport layer, netcode, or a rollback engine. GGRS and friends own that space.
  • ❌ Unity/C# FFI bridge in v1. It is the headline theme of v2, and the core API is designed for it.
  • ❌ Determinism linter or static analysis.
  • ❌ A fixed-point math library.
  • ❌ Engine plugins for Bevy or Godot. Open territory for the community, and the API makes them possible.
  • ❌ GUI or TUI visualizer, live monitoring.
  • ❌ Async API or tokio dependency. The core stays synchronous and allocation-conscious.

Contributing

Contributions are welcome. Start with CONTRIBUTING.md for the workflow and commit conventions, CODING_STANDARDS.md for the code rules, and CODE_OF_CONDUCT.md for community expectations. Security reports go through the process in SECURITY.md.

License

Licensed under the MIT License.