Example project
Rewind Lab
examples/rewind_lab/README.md
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.

Read the code
Follow the Godot controls and frame driver, the native TimeLab class, and the rewind subsystem. The subsystem is where the simulation and recorded input history live; the class handles engine calls and publishes the draw buffers. The design document 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 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:
/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_labOrdinary 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,
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
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=0RL-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 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),
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,
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).advanceconsumes 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_digestFNV-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_restorerecomputes 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.4Reads: 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

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.
advancesteps the pure subsystem, then writes and publishes both moving meshes and issues exactly twomultimesh_set_buffercalls — the frame's whole engine cost on the staging path the bulk-transfer axis owns.rewind_torestores 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=trueabove;restore_stepsreports the rebuild-plus-replay cost the path-dependence finding bought). - The readback proof.
verifyrepacks each mesh's live solver state into the stored 12-float form and compares it bitwise againstmultimesh_get_buffer— all three meshes, zero tolerance (meshes=3 stride=12 bitwise=true). - Symmetric teardown.
shutdownretires 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
HSliderspans[window_base, tick](the exact boundrewind_toenforces: epoch or window base, whichever is later). Dragging pauses playback and drivesrewind_toper 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 livedigest, 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_stepstoken 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 throughatan2/MakeRotis lossy and would break bitwise restore. anchor_intervalis gone fromConfig(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.