diff --git a/crates/rustynes-core/src/movie.rs b/crates/rustynes-core/src/movie.rs index 2a1cde0b..7386e979 100644 --- a/crates/rustynes-core/src/movie.rs +++ b/crates/rustynes-core/src/movie.rs @@ -800,6 +800,27 @@ impl MovieRecorder { /// frame *before* [`Nes::run_frame`], after the frontend has applied its /// `set_buttons` calls — this captures exactly the inputs the upcoming /// frame consumes. + /// + /// # Two ports only, including under a Four Score + /// + /// [`FrameInput`] models ports 0 and 1, so this reads `nes.buttons(0)` and + /// `nes.buttons(1)` and **nothing else**. The core itself carries four — + /// the frontend calls `set_buttons(2)` / `set_buttons(3)` whenever the Four + /// Score adapter is active — so recording a four-player session captures + /// half of what drove it, and replaying that movie diverges from the run it + /// came from. + /// + /// Stated here rather than left to be discovered, because the failure is + /// silent at record time: nothing about a `.rnm` says which ports it could + /// not hold, and the divergence only appears on playback. The frontend's + /// Replay panel says so where the topology is displayed, and + /// `Movie::verify`'s attestation catches it after the fact. + /// + /// Widening [`FrameInput`] is a `.rnm` format epoch change (ADR 0028), not + /// an additive one, which is why this is a documented limit rather than a + /// fix. The `.fm2` importer already takes the same position for the same + /// reason — it keeps pads 1 and 2, drops 3 and 4, and preserves the + /// `fourscore` flag so the caller is not silently misled. pub fn capture(&mut self, nes: &Nes) { self.frames.push(FrameInput { p1: nes.buttons(0), diff --git a/crates/rustynes-frontend/src/debugger/replay_panel.rs b/crates/rustynes-frontend/src/debugger/replay_panel.rs index 99134e2e..5193fe91 100644 --- a/crates/rustynes-frontend/src/debugger/replay_panel.rs +++ b/crates/rustynes-frontend/src/debugger/replay_panel.rs @@ -177,6 +177,26 @@ pub fn show( ui.end_row(); }); + // The adapter row above says "Four Score (P1..P4)", which is true of + // the CONSOLE and not of the movie. `FrameInput` holds two ports, so + // `MovieRecorder::capture` records P1 and P2 and drops P3 and P4 -- + // a four-player recording replays as a different run. + // + // Surfaced right under the claim it qualifies, because the failure is + // silent at record time: nothing about a `.rnm` says which ports it + // could not hold, and the divergence only shows up on playback. + if info.four_score { + ui.colored_label( + ui.visuals().warn_fg_color, + "Movies record P1 + P2 only — P3/P4 input is NOT captured.", + ) + .on_hover_text( + "The `.rnm` format stores two ports. A recording made with \ + the Four Score active will not replay the same run. \ + Widening it is a movie-format epoch change (ADR 0028).", + ); + } + ui.separator(); // --- Controls --- diff --git a/docs/creator-tools.md b/docs/creator-tools.md index fdc4b8ee..414405e9 100644 --- a/docs/creator-tools.md +++ b/docs/creator-tools.md @@ -44,6 +44,20 @@ what makes movie replay exact — see [`testing-strategy.md`](testing-strategy.m for the determinism contract. The `.rnm` deserializer is hardened against malformed input (bounded allocations; fuzz-tested, see `fuzz/`). +**Movies record two ports.** `FrameInput` models P1 and P2, so a recording made +with the **Four Score** adapter active captures half of what drove the run and +replays as a different one. The console carries four ports and the emulator +drives all four; the movie format does not. This is a documented limit rather +than a defect being worked around: widening `FrameInput` is a `.rnm` format epoch +change (ADR 0028), not an additive one, and the `.fm2` importer takes the same +position — it keeps pads 1 and 2, drops 3 and 4, and preserves the `fourscore` +flag so the caller is not misled. + +The failure is silent at record time, so it is surfaced where the claim is made: +the Replay panel prints the caveat directly under the "Four Score (P1..P4)" +topology row, and `rustynes verify` catches a divergent replay after the fact +because its attestation folds in the video the run produced, not just the input. + ## Scripting (Lua) An embedded Lua engine exposes an emulation API (memory peek/poke, frame hooks,