# Rewind Lab

A physics playground with a rewind timeline: scrub to a recorded tick, change
playback speed, and freeze bodies mid-simulation. An Odin subsystem runs Box2D
and owns the fixed tick, world and replay history. Godot owns the scene, controls
and drawing; a native `Time_Lab` Node connects them. Impact audio and shake are
planned additions.

## Try it

- Drag the timeline to visit a recorded tick; release it to resume playback.
  While paused, use the arrow keys to step through the recording.
- Press **Space** to pause. **< speed** and **speed >** change playback speed
  from 0.25× to 4×.
- Left-click a body to freeze it, then click it again to unfreeze. Frozen bodies
  turn icy blue. Scrub back before that click to see the earlier state.
- Watch the HUD's tick, frozen-body count and checksum as the timeline changes.

![RL-3 windowed evidence: the pyramid_stack preset rendering through the Box2D-linked native image](../../docs/images/rewind-lab-rl3-windowed.png)

## Read the code

Follow [the Godot controls and frame driver](project/main.gd),
[the native TimeLab class](declarations/time_lab.odin), and
[the rewind subsystem](subsystem/rewind.odin). The subsystem is where the
simulation and recorded input history live; the class handles engine calls and
publishes the draw buffers. The [design document](../../docs/example-rewind-lab.md)
explains the intended example, and the development record below distinguishes
implemented behavior from later work.

## Build and run

Use a configured copy with its attached SDK; the separate
[project setup guide](../../docs/setup.md) covers that connection. This example
also needs its declared Box2D foreign library. From the example directory, run
`./test.sh vendor` to install the pinned library into the project’s `box2d/` directory,
then build and launch through the SDK:

```sh
/absolute/sdk/bin/gododin build --project-root /absolute/gododin/examples/rewind_lab --profile debug
/absolute/sdk/bin/gododin run --project-root /absolute/gododin/examples/rewind_lab
```

Ordinary launch opens the interactive playground. `./test.sh all` runs the
offline tests; `./test.sh run` drives the scripted windowed verification rather
than interactive play.

## Recorded development and evidence

**Status: RL-0 through RL-3 implemented (2026-09-16), RL-3 evidence
recorded.** The subsystem, its tests, the offline eval, the authored
`Time_Lab` class, the generated bridge, and the windowed engine run all
pass on the installed `gododin-phase16-2026-09-16.1` SDK — the first
consumer of [ADR 0064](../../docs/decisions/0064-declared-foreign-library-linking.md),
the declared foreign-library admission this example's measured boundary
produced. `./test.sh all` owns the offline tiers and `./test.sh run` owns
the windowed gate against the accepted publication. RL-3 delivered the
interactive time controls: timeline scrub, speed dial, pause, freeze gun,
and the HUD counters. RL-4 onward (contact events, reload journey) are not
started. Numbers below are receipt evidence only where a named workload
line is quoted. The 2026-09-26 documentation audit confirms that status; the
[DevX plan](../../docs/plans/2026-09-26-devx-and-integration.md#dx-6--finish-the-already-planned-examples)
orders RL-4/5 without reopening RL-0–RL-3 or making RL-6 required.

The windowed evidence lines, from `./test.sh run` (RL-3's gate):

```
GODODIN_REWIND_LAB_SCRUB target=60 restore_steps=60 digest_match=true
GODODIN_REWIND_LAB_HUD_PASS native=120 hud=120 match=true
GODODIN_REWIND_LAB_INTERLEAVE window_match=false
GODODIN_REWIND_LAB_FREEZE_PASS slot=0 digest_match=true frozen_ok=true
GODODIN_REWIND_LAB_READBACK_PASS meshes=3 stride=12 bitwise=true
GODODIN_REWIND_LAB_PASS workload=rewind-lab preset=pyramid_stack frames=420 bodies=48 verified=3 rewind=60 scrub=true restore_steps=60 freeze=true hud=true submissions=853
GODODIN_REWIND_LAB_TEARDOWN_PASS staging_live=false
GODODIN_HOST_PASS target=4.8.0 method_variant_calls=4232 method_ptrcalls=0 virtual_calls=0 registrations=0 instances=0 owned_foreign_values=0 references=0 active_callback_scopes=0 host_allocations=0 image_allocations=0
```

RL-2's gate (target=120, the same-tick scrub) was superseded by the lines
above; its recorded run stands as history in the RL-2 section below.

## RL-0: the Box2D dependency, as actually shipped

The design doc's premise is superseded twice by observation at the pinned
toolchain. The doc expected the Odin distribution to ship Box2D with wasm
objects only; what the pinned Odin commit
(`8412dc37aa91def0c2fa90f89eade29056b4e608`, `dev-2026-08`, a clean
checkout) actually ships in `vendor/box2d/lib/` is the full set of native
static archives, `box2d_darwin_arm64.a` included,
sha256 `b4c373851bb8975641339f99d047a9c5705e19c0156a3ea087fb218b2827fe8f`.

The RL-0 decision is a **vendored-collection install into the project
tree**, not an archive build and not a compiler-root install.
[`vendor_box2d.sh`](vendor_box2d.sh) copies `vendor/box2d` from a clean
checkout of the pinned Odin commit into `box2d/` beside this README
(git-ignored) — the bindings verbatim plus only `lib/box2d_darwin_arm64.a`
— verifying at each step that the checkout HEAD equals the pinned commit,
the working tree is clean, and the archive matches the digest above before
and after the copy. It is idempotent and reinstalls over any older-shaped
installation. The subsystem imports it as the plain relative package
`../box2d`; `gododin.toml` admits `box2d` as a second subsystem root and
declares the archive under `[project].foreign_libraries`
([ADR 0064](../../docs/decisions/0064-declared-foreign-library-linking.md)),
so every staged project-input snapshot carries the archive at its
project-relative path, the link stage links it into the project image, and
the artifact manifest records its digest. Upstream's compile-time `#exists`
guard observes a truthful tree everywhere, so **the bindings install
verbatim**; the single remaining recorded deviation is the archive trim to
this example's platform.

Box2D remains an example dependency, never a Gododin product dependency.

## The RL-2 boundary finding, and its resolution

RL-2's authored class and project were written first against the then-
installed SDK, and the pipeline got as far as `check` (`GODODIN_CHECK_PASS`,
1 class, 13 operations). `build` failed in the linking stage with undefined
`_b2*` symbols, and the reason was structural, measured in the installed
SDK's own sources: every compiler stage consumes byte-verified `.odin`-only
source snapshots, the link stage ran a fixed `ld -dylib … project-image.o
-lSystem` command with no foreign-library admission, and the staged managed
compiler publication is manifest-sealed, so no vendor-tree addition could
carry the archive either. No fixture or phase in the repository linked
foreign C code; the general case — any game needing a C library — had no
supported path into a project image, an axis godot-rust covers via cargo
build scripts.

That finding became [ADR 0064](../../docs/decisions/0064-declared-foreign-library-linking.md),
implemented across the generator's input schema and staged snapshots, the
coordinator's source capture, the build owner's plan forwarding, and the
stage tool's link command, and shipped in the
`gododin-phase16-2026-09-16.1` SDK this example now runs on (packed from
the dev5-lineage tree; the lock reseat and SDK facts follow the version
policy). With the admission in place the example builds, runs, and verifies
in the engine window; nothing about the example's own composition changed
except declaring the archive.

### The corrected render-layout facts

The render layout went through one recorded correction. An earlier draft
was written from the upstream Godot *master* sources and packed 2D
instances at stride 10 with two float slots carrying f16 color halves; that
is master's GL shape, not this binary's. Measured against the pinned
4.8-dev5 binary (a 10-float-row submission is rejected with a stride error;
a 12-float submission is accepted and read back at length `extent * 12`),
the dev5 GL compatibility renderer stores one instance as **12 floats** —
the 2D transform 8 plus four color floats, each color quantized through the
engine's half-float type (a truncating `make_half_float` whose subnormal
range collapses to zero) and stored back as plain f32. The subsystem's
`render_layout.odin` records the layout and the engine-exact converter; its
writers reproduce the stored form for the bitwise readback, which is what
`Time_Lab.verify` proves (`meshes=3 stride=12 bitwise=true` above).

## RL-1: the `rewind` subsystem

`subsystem/` imports `vendor:box2d` and Odin core only. One owned
allocation per world (the `World` value embeds the command log, the digest
ring, and every table); Box2D's storage never flows through Odin
allocators. The authored `Config` mirrors the planned `Time_Lab`
properties: `preset`, `seed`, `gravity`, `restitution_scale`, `sub_steps`,
`window_ticks`, `body_cap` — all admitted through `valid_config` bounds.

- **Fixed tick.** One tick is one `World_Step(world, 1/60, sub_steps)`.
  `advance` consumes the tick's commands, steps, and appends the running
  digest to the ring. Wall-clock time never enters.
- **Input log.** Per-tick commands: `fling`, `freeze`, `unfreeze`,
  `spawn_ball`, `switch_preset`, `clear_spawned`. Appends below the
  playhead are refused; an append at or below the log's last tick cuts the
  branch there (rewind-then-act semantics). A preset switch starts a new
  epoch: the log clears (commands addressed to the old epoch's slots do
  not survive it) and restores before the switch tick are refused.
- **Determinism.** Warm starting and sleeping are disabled on every world
  the package creates — they are solver state a rebuild does not
  reconstruct (see below). Seeded single LCG stream; f32 throughout;
  array-only iteration; single-threaded solving, task callbacks unused.
- **Digest and oracle.** `state_digest` FNV-1a-hashes the roster identity
  plus every dynamic slot's full solver state (position, rotation as the
  solver's own `(c, s)` pair — never a lossy angle — linear and angular
  velocity). `oracle_restore` recomputes a target tick in a scratch world
  built independently at the epoch and compares elementwise, bitwise.

### The measured finding that reshaped restore: Box2D is path-dependent

The design doc specified anchors — solver-state checkpoints every
`anchor_interval` ticks — with restore = rebuild + install nearest anchor +
replay. That was implemented first, and it does **not** reproduce history:
with warm starting and sleeping already disabled, a world rebuilt and
teleported into mid-history state still diverges from one that stepped
there — the first post-restore step already differs in the fourth decimal.
The engine's broadphase tree, pair ordering, and manifold history evolve
as a world ages, so the step is not a pure function of
(transforms, velocities, types).

What is a pure function is the path from a rebuild: a world rebuilt from
the spec and replayed to tick T is bitwise identical to any other world
that reached T the same way — including the un-rewound timeline. Restore
therefore means **rebuild from the epoch and replay every tick**. The
anchor machinery and the `anchor_interval` property were removed; the
design doc's anchor-spacing slider and restored-state-cache follow-ups are
what re-enters if scrubbing cost ever demands them. This is the example's
recorded deviation from the design, and it is the RL-1 measurement the doc
asked for.

### Offline evidence

`./test.sh` runs the offline surface: `vendor` (install/verify the
collection), `subsystem` (11 guarded tests: preset well-formedness,
two-world determinism, rewind-vs-continuous chain equality, oracle after
restore, freeze parking bitwise, spawn cap and clear, window refusals,
branch-cut semantics, seed control, epoch switch, and zero warm
allocations with full teardown retirement). The test runner is pinned to
one thread (`ODIN_TEST_THREADS=1`): Box2D keeps global world state, so
concurrent stepping of separate test worlds is unsafe — the example's own
threading rule is single-world, single-thread.

`./test.sh eval` runs the scripted workload — 600 ticks of flings, spawns,
freezes, and paired unfreezes; scrub to tick 120 and replay; run-twice;
changed seed; oracles at ticks 120/173/401; restore-cost sampling. Latest
offline run (2026-09-16, development host: Apple M5 Max, macOS 26.6.2,
locked Odin `dev-2026-08:8412dc37a`, `-o:speed`):

```
GODODIN_REWIND_LAB_EVAL_PASS workload=rewind-lab-v1 preset=pyramid_stack seed=20260916
  ticks=600 restore_target=120 run_twice=true scrub_chain=true scrub_restore_steps=120
  oracle=true seed_control=true warm_allocations=0 warm_ticks=300
  restore_samples=40 restore_p50_ms=6.3 restore_p95_ms=9.2 restore_max_ms=9.5
  restore_avg_steps=354 advance_avg_usec=16.4
```

Reads: one warm tick of the 36-body pyramid costs ~16 µs, so the
canonical workload's restore (120 replayed steps) sits near ~2 ms plus one
rebuild — inside the design's proposed p95 ≤ 8 ms budget. Across the whole
sampled window (targets up to 600), restores cost p50 ~6 ms, p95 ~9 ms.
These are informal offline numbers from `eval`; the receipt that freezes
numbers belongs to RL-5's recorded engine runs.

## RL-2: the authored `Time_Lab` class, the bridge, and the windowed run

![RL-2 windowed evidence, from its recorded run: the pyramid_stack preset rendering through the Box2D-linked native image](../../docs/images/rewind-lab-rl2-windowed.png)

`declarations/time_lab.odin` is the ADR 0050 single-source class: one
`@(gododin_class="Node2D") Time_Lab` owning three server MultiMesh RIDs
taken from the scene's child `MultiMeshInstance2D` nodes, three retained
float32 staging owners, and one pure subsystem world. Every member is
method-mediated — no authored fields are exposed as properties, matching
every example's authoring form. The compiler-owned catalog and the
generated bridge come from `gododin generate`; the generated surface is
never edited.

- **Setup** validates the authored configuration, binds children 0/1/2
  (boxes, circles, statics) through the engine's own
  `multimesh_allocate_data` (2D transforms, colors on), creates the staging
  owners, builds the world, and submits statics plus the initial dynamics
  so the contraption is visible before the first tick. Named error codes
  per step; a failed bind leaves nothing retained.
- **One tick, two bulk calls.** `advance` steps the pure subsystem, then
  writes and publishes both moving meshes and issues exactly two
  `multimesh_set_buffer` calls — the frame's whole engine cost on the
  staging path the bulk-transfer axis owns. `rewind_to` restores an exact
  recorded tick and submits once.
- **The scrub proof.** The driver digests the live world at the warm tick,
  rewinds under the playhead, keeps advancing, and requires the digest to
  match bitwise when the playhead returns (`scrub digest_match=true`
  above; `restore_steps` reports the rebuild-plus-replay cost the
  path-dependence finding bought).
- **The readback proof.** `verify` repacks each mesh's live solver state
  into the stored 12-float form and compares it bitwise against
  `multimesh_get_buffer` — all three meshes, zero tolerance
  (`meshes=3 stride=12 bitwise=true`).
- **Symmetric teardown.** `shutdown` retires the staging owners and the
  world through the same facade that created them; the windowed run's
  Host line shows zero retained owners, scopes, and allocations, and the
  engine log carries no leak or error line.

`./test.sh run` stages the accepted publication into an isolated Godot
home, runs the eval windowed at 240 physics ticks per second, and gates on
every marker line above plus the PNG screenshot.

## RL-3: the interactive time controls

RL-3 adds seven authored operations (generated into the bridge by
`gododin generate`, now 20 operations) — `freeze`, `unfreeze`, `pick`,
`frozen`, `frozen_count`, `recorded_digest`, and `window_base` — and turns
the argument-driven driver into a playable scene:

- **Timeline scrub.** An `HSlider` spans `[window_base, tick]` (the exact
  bound `rewind_to` enforces: epoch or window base, whichever is later).
  Dragging pauses playback and drives `rewind_to` per slider tick; release
  resumes. Arrow keys step one recorded tick when paused.
- **Speed dial.** `< speed` / `speed >` cycle 0.25×–4×; the interactive
  loop accumulates fractional ticks per 60 Hz physics step, so 0.25× is
  one tick every four steps. Space pauses.
- **Freeze gun.** A left click resolves the world point through
  `pick` (nearest dynamic slot within 0.75 world units, ties to the lower
  slot) and toggles freeze/unfreeze for that slot. Frozen bodies read the
  ice tint through the staged layout, and the command is replayed history:
  scrubbing before it reproduces the unfrozen timeline exactly.
- **HUD counters.** `tick`, `speed`, `bodies`, `frozen`, `submissions`,
  `rebuilds`, `restore_steps`, and the live `digest`, updated per tick from
  the native getters.

The windowed gate proves each acceptance criterion in-engine:

- **Oracle equality under scripted scrubbing.** The eval scrubs to tick 60
  and requires the live digest at the target to equal both the pre-scrub
  state digest and the history ring's recorded digest — and the warm tick's
  digest to rematch after re-advancing (`SCRUB target=60 restore_steps=60
  digest_match=true`).
- **Freeze/unfreeze round-trip.** Offline, the subsystem test proves the
  parked velocity bits are restored bitwise and that a same-tick
  freeze/unfreeze pair preserves the whole digest chain through a rewind.
  In-engine, the eval rewinds to tick 120, replays the window with the
  pair appended at tick 120, and requires every digest in ticks 121–180 to
  come back bitwise identical to a same-shape control chain
  (`FREEZE_PASS slot=0 digest_match=true frozen_ok=true`); the frozen
  counters must round-trip through a real freeze and unfreeze.
- **HUD restore-cost counter matches the harness.** After the freeze
  proof's rewind (120 replayed steps from the epoch), the eval reparses the
  HUD label and requires its `restore_steps` token to equal the native
  telemetry (`HUD_PASS native=120 hud=120 match=true`).

**The measured RL-3 finding: interleaving classes.** Comparing chains
across engine-call interleavings is not meaningful past the settling
window. A replay chain (restore in one tight loop, then frame-stepped) and
a frames-interleaved chain carry bitwise-equal visible state through every
digest the settling window recorded — the scrub proofs at ticks 60 and 120
hold across that boundary — but their digest chains diverge afterward, at
tick 121 in this workload, and each class is individually deterministic
(the same values reproduce across runs). The gate therefore compares the
freeze pair against a same-shape control chain and records the cross-class
comparison as evidence, not as a pass/fail: `INTERLEAVE
window_match=false`. Offline, where there is only one interleaving class,
the same scenarios reproduce the whole chain exactly
(`plain_replay_reproduces_the_chain_in_the_eval_scenario`,
`double_rewind_reproduces_the_chain`).

## Other recorded deviations from the design doc

- **Domino preset is a row, not a spiral** — the arc version needs a
  curved ground segment, outside the spec's box/circle shapes. The enum
  name keeps the design's vocabulary.
- **Rotations are captured as `(c, s)`, never angles** — an angle round
  trip through `atan2`/`MakeRot` is lossy and would break bitwise restore.
- **`anchor_interval` is gone from `Config`** (see the path-dependence
  finding). RL-2's authored property list drops it with it, or reintroduces
  it only with the cache follow-up that gives it a job.

## Not implemented yet

RL-4 (contact events to an `impacted` signal, audio/shake, a run-twice
HUD proof, the GDScript baseline comparison), RL-5 (the editor reload
journey on this example, export receipt), and the RL-6 stretches
(PhysicsServer2D baseline, slow-field gadget, threaded solving). The
windowed gates of RL-5's receipt form arrive with that package; the
`./test.sh run` gate above is RL-3's own evidence tier.
