From efc557e3ad4b6bd5a0d0dc06d0afc4db4860cc76 Mon Sep 17 00:00:00 2001 From: DoubleGate Date: Wed, 19 Aug 2026 18:37:04 -0400 Subject: [PATCH] docs(movie): disclose that a movie records two ports, under the claim that says four Found in a sweep of the project's remaining TODO markers. One of the two real ones -- `movie_interop.rs`'s "carry P3/P4 once `FrameInput` grows beyond two ports" -- turns out to describe a live, silent gap in the NATIVE recording path, not just the `.fm2` importer it sits in. `FrameInput` models ports 0 and 1. `MovieRecorder::capture` therefore reads `nes.buttons(0)` and `nes.buttons(1)` and nothing else, while the core carries four: `EmuCore::latch` calls `set_buttons(2)` and `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 is a different run. Nothing said so. Worse, the Replay panel's topology grid printed "Four Score (P1..P4)" -- true of the console, false of the movie -- which is an over-claim at exactly the moment a user decides to hit Record. 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. Disclosed at all three levels rather than fixed, and the distinction is deliberate. Widening `FrameInput` is a `.rnm` format EPOCH change (ADR 0028), not an additive one, so it is a release-boundary decision rather than a remediation. The `.fm2` importer already takes this exact position for this exact reason -- it keeps pads 1 and 2, drops 3 and 4, and preserves the `fourscore` flag "so the caller is not silently misled". The native path simply never got the same treatment. * `MovieRecorder::capture` says what it records and what it drops. * The Replay panel prints the caveat directly UNDER the topology row that makes the claim, not in a separate place a reader may not reach. * `docs/creator-tools.md` records it as a documented limit with the reason it is one. `rustynes-core` changes, but comment-only: the diff contains no non-comment line, so AccuracyCoin and nestest are untouched by construction and that is checkable from the diff rather than asserted. `no_std` cross-compile, workspace clippy, `full`, wasm32 and rustdoc all clean. --- crates/rustynes-core/src/movie.rs | 21 +++++++++++++++++++ .../src/debugger/replay_panel.rs | 20 ++++++++++++++++++ docs/creator-tools.md | 14 +++++++++++++ 3 files changed, 55 insertions(+) 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,