1# Chapter 6: replay, a VCR for a machine 2 3Watching a bot play is mostly waiting, and the interesting moment (Link 4walking into a wall for the ninth time) is always thirty seconds ago. This 5crate is the rewind button. While the bot plays, every emulated frame is 6recorded, and afterwards the window can pause, drag back to any frame 7already played, play it again, and even start a *fresh* bot from that frame 8as a new branch, without throwing away the future that was already there. 9 10The obvious way to rewind an emulator is to keep a snapshot of every frame. 11A snapshot is 1.3 MB, and there are sixty a second, so that is out. The 12classic trick (Doom demos, Quake, BizHawk's TAS tools) is to record only the 13*inputs*: the machine is deterministic, so the same inputs from the same 14start replay the same game. 15 16> **Aside: why the pad alone is not enough.** The bot cheats. It writes 17> infinite health, bombs and arrows into work RAM every frame, and those 18> writes are inputs too, as far as the game is concerned. So each frame 19> records the pad the console held, the human's pad, and a diff of the work 20> RAM the bot's tick wrote. Replaying is: apply the diff, hold the pad, run 21> the frame. 22 23Seeking is then: restore the nearest keyframe before the target, and replay 24the log forward from there, headless, never asking the bot anything. 25Keyframes are kept in three tiers by age, so the cost of a seek is bounded: 26 27| Age | A keyframe every | A click there seeks in about | 28| --- | --- | --- | 29| the last ~5 minutes | 2 s (120 frames) | half a second | 30| the last ~hour | 10 s | up to 3 s | 31| older | 30 s | up to 8 s | 32 33(Seek times measured by `apps/replay-probe`, chapter 14. The first hour of 34play takes about 76 MB on disk.) 35 36**The bot is the hard part.** The console can be rewound from a keyframe; 37the bot cannot, because its state is C globals that no snapshot holds. So 38"run the bot from here" starts a new process whose fresh bot is ticked 39through the *whole* logged history up to that frame, with every goal choice 40it makes answered from the log instead of from Jev. That is exact because 41the bot is a pure function of RAM, the pad, `rand()` and those picks, and 42the log fixes all four. 43 44```mermaid 45flowchart LR 46 start["root: frame 0"] --> at["frame 500: run the bot from here"] 47 at --> old["root goes on to frame 900, and is kept"] 48 at --> new["root-at-500-1: frames 501 onwards, live"] 49``` 50 51A branch's log holds only its own frames, after its fork point; the parent's 52are not copied, and `Tree::owner_of` says which branch in the chain recorded 53any given frame. 54 55> **Try it.** `nix develop -c buck2 test //packages/replay:test` runs the 56> codec, the tiers and their pruning, the torn-entry log, the branch tree, 57> the speed steps and the disk cap, with no ROM. To prove the core 58> deterministic on your own cartridge, run chapter 14. 59 60## For the people who maintain it 61 62### On disk 63 64``` 65run/zbanks-window/replay/<recording-id>/ 66 header.bin # RunHeader: the starting snapshot, the imported map, bot options 67 tree.json # the branch tree 68 branches/ 69 root/ 70 log.bin # FrameEntry values, one per emulated frame, length-prefixed 71 keyframes/ 72 000000000120.kf # a compressed console snapshot at frame 120 73 ... 74 root-at-500-1/ # "run the bot from here" at frame 500 75 log.bin # frames 501.. of THIS branch 76 keyframes/ 77 000000000500.kf # this branch's own keyframe AT its fork point 78 ... 79``` 80 81Recordings are capped at 1 GiB (`storage::DEFAULT_CAP_BYTES`): the oldest 82whole recordings go first, then the one being written is thinned to its 83coarse keyframes, never deleted. A window starting deletes recordings in an 84older `FORMAT_VERSION`, which it could not seek. 85 86### In this folder 87 88| Path | What | 89| --- | --- | 90| [src/lib.rs](src/lib.rs) | The modules, and the re-exports `FrameEntry`, `Pick`, `RunHeader`, `Tree`, `BranchMeta`. | 91| [src/format.rs](src/format.rs) | `FrameEntry` (what one recorded frame holds) and `Pick` (which goal-choice option was used); `diff_wram`/`apply_wram_diff`. | 92| [src/keyframe.rs](src/keyframe.rs) | The compression: XOR a fixed-width snapshot against its reference, then zstd (`ruzstd`, pure Rust). | 93| [src/keyframes.rs](src/keyframes.rs) | A branch's keyframe files in three tiers (`DENSE_FRAMES`, `MEDIUM_FRAMES`, `COARSE_FRAMES`) and `prune`. Each keyframe is a delta against one of its own tier or coarser. | 94| [src/log.rs](src/log.rs) | A branch's append-only frame log: length-prefixed, so a killed process's torn last entry is dropped rather than corrupting the read. | 95| [src/tree.rs](src/tree.rs) | The branch tree: fork, a branch's ancestry, and which branch owns an absolute frame. | 96| [src/header.rs](src/header.rs) | `RunHeader`, and `FORMAT_VERSION`. | 97| [src/recorder.rs](src/recorder.rs) | What a live window calls once per frame: append, keep a keyframe every `RECORD_EVERY` frames, prune aged-out tiers. | 98| [src/seek.rs](src/seek.rs) | The console at any recorded frame: nearest keyframe, then the log forward, headless. | 99| [src/rebuild.rs](src/rebuild.rs) | `Rebuilder`: catch a fresh bot up to a frame by re-running it over the log, a bounded slice per step, answering goal choices from the log. | 100| [src/storage.rs](src/storage.rs) | The disk cap, and deleting older formats. | 101| [src/speed.rs](src/speed.rs) | Playback speed steps (0.25x to 8x) and the frame-pacing maths. | 102| [BUCK](BUCK) | The `rust_library` and `//packages/replay:test`. | 103 104Native only, like `zbanks` (which it depends on) and `mcp`: it writes files. 105`seek.rs`, `rebuild.rs` and `storage.rs` were built only after 106`apps/replay-probe` proved the core deterministic (see CLAUDE.md). 107 108← Previous: [Chapter 5, decisions/](../decisions/) · Up: [packages](../) · Next: [Chapter 7, panels/](../panels/) →