Record a session and play it back, from a Machine menu or --record - #40
Merged
Conversation
A movie is not a video and holds no picture of the machine at all. The console is deterministic -- nothing in it reads a clock or a random number -- so a session is where it started, one button mask per finished frame, and the frames Reset was pressed at, and playing that back reproduces the run byte for byte rather than approximately. Nine hundred frames of Super Mario Bros. comes to 119 bytes, and the save state at the end of a replay is `cmp`-identical to the one at the end of the recording. The headline is that a rewind is not in it. Rewinding while recording truncates the log rather than appending to it, so a movie holds the timeline that was finally played and a replay never re-enacts the revert. That is exact rather than a convenient approximation, and it composes out of something already proved: `RewindTests.rewindingGoesBackToTheFrameItLeft` shows a rewound machine *is* the machine that never went forward, so there is nothing lost by truncating to match. A rewind that goes back past the recording's own start re-anchors instead, since the log no longer describes where the machine is; a loaded save state re-anchors for the same reason. One rule for every jump. Recording from power on carries no state at all, which is what makes a movie of a whole playthrough something to hand to anybody with the same ROM. Anything else puts a save state in the file to start from, because there is otherwise nothing to say where the beginning was -- and that includes `--sram-in`, since a movie has no way to carry a battery either. The anchor is a whole `SaveState` file nested inside the gzipped body rather than unpacked into it. That double-gzips a few kilobytes and buys the thing worth having: there is exactly one tested way of putting a machine back, and a movie uses it. The Game Genie codes ride inside the movie and are put back on replay, and they have to. A cheated cartridge is byte for byte an honest one, so `cart.sha256` cannot tell the two apart and nothing else in the file could say so -- the same argument `--genie` already makes about save states, turned into a chunk. Both front ends refuse to change the codes while a recording is running, because a file whose header names one set and whose frames were played against another cannot be replayed and would not admit it. `.mnm` follows `SaveState`'s discipline exactly: big endian, a fixed header outside the compression so a file can be labelled or refused without inflating it, length-prefixed chunks a later version may add to and this one steps over, a version bumped only when something already there changes meaning, and everything the file can be wrong about checked in `Movie.read` before there is a machine to touch. `Movie` and `MovieRecorder` are deliberately not reachable from `NES`, for the reason `Rewind` is not: `SaveStateCompletenessTests` walks everything the console can reach and scrambles every array it finds, so a growing log hanging off a chip would be shredded. They belong to whoever is driving the machine, which is also the honest place for them. The desktop has an asymmetry to answer. Key events reach the controller mid-frame on the event dispatch thread, and a press that landed half way through a frame would be written down as belonging to a frame it was only half of. So while a movie is being recorded or played the pad is latched exactly once a frame on the emulation thread, guarded on a frame boundary so a frame resumed after a breakpoint is not re-latched in flight. When neither is happening the immediate path is untouched, because that is the one a player feels. A replay mutes the keyboard entirely -- a bumped key must not leak into somebody else's session -- except for rewind, which is how you take the game back. `MovieRecorder.rewound` takes frames rather than rewind steps, and the difference is not cosmetic: `Rewind.rewind` answers in states, the window keeps one state every *other* frame, and passing the wrong one of the two leaves the movie holding twice the frames the machine actually went back over. `EmulatorRunnerTests.rewindingWhileRecordingDropsTheFramesItTookBack` pins the invariant that catches it -- a movie holds exactly the frames between its anchor and where the machine stands, however many were played twice on the way. `run.record` and `run.replay` join `run.state`, `run.region`, `run.hacks` and `run.genie` in what has to be checked before diffing two reports, always present with explicit nulls so two documents line up key for key. Two honesty fixes came with them. `startedFromPowerOn` is false for a replay of an anchored take, which is no more a power-on run than a `--load-state` one is. And `framesWithInput` walks the movie when replaying rather than the schedule `--play` refused, since answering 0 for a run that pressed something on every frame would be the number in that document most likely to be believed. `--play` refuses `--record`, `--input`, `--input-file`, `--reset-at`, `--genie`, `--load-state`, `--sram-in` and `--interactive` one at a time, naming the flag that was typed. Each is a second answer to a question the movie has already answered, and a run that quietly took one of them would not be the recorded session at all -- and would look exactly like one that worked. It defaults `--frames` to the movie's own length, since running past the end with nothing held is how to see what a game does when the player stops playing. No new dependencies, no chip gains a field, and `VERSION` stays 1 everywhere. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
A movie holds no picture of the machine at all: because the console is deterministic, a session is where it started, one button mask per finished frame, and the frames Reset was pressed at — nine hundred frames of Super Mario Bros. comes to 119 bytes, and the save state at the end of a replay is
cmp-identical to the one at the end of the recording. Rewinding while recording truncates the log rather than appending to it, so a movie holds the timeline that was finally played and a replay never re-enacts the revert; a rewind past the recording's own start, or a loaded save state, re-anchors instead. Recording from power on carries no state at all, anything else embeds one to begin from, and the Game Genie codes ride inside the file — they have to, since a cheated cartridge is byte for byte an honest one andcart.sha256cannot tell them apart. Both front ends are wired:--record/--playandrecord start/record stopin the REPL, Machine > Record Movie… / Play Movie… in the window, where the pad moves to a once-a-frame latch on the emulation thread so a mid-frame press cannot be written down as belonging to a frame it was only half of. NewMovieTests,EmulatorRunnerTestsand headless round-trip tests cover it,run.record/run.replayjoin the report's comparability checklist, and there are no new dependencies and no new fields on any chip.🤖 Generated with Claude Code