jevsnes.git / packages / replay / README.md
README.mdpreviewREADME.mdsource108 lines · 5.7 KB · raw
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/) →