Gododin Gododin Documentation

Example project

Rewind Lab

examples/rewind_lab/README.md

View original Markdown · Download · All examples

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
RL-3 windowed evidence: the pyramid_stack preset rendering through the Box2D-linked native image
Open image at full size

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:

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, 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=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 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). 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
RL-2 windowed evidence, from its recorded run: the pyramid_stack preset rendering through the Box2D-linked native image
Open image at full size

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.