Skip to content

v3.1.0 "Bellwether": AccuracyCoin 146/146 for real, the overclock and sprite-limit options, dual-cabinet rewind - #594

Merged
doublegate merged 21 commits into
mainfrom
release/v3.1.0-bellwether
Oct 8, 2026
Merged

doublegate merged 21 commits into
mainfrom
release/v3.1.0-bellwether

Conversation

@doublegate

@doublegate doublegate commented Oct 8, 2026 •

Copy link
Copy Markdown
Owner

v3.1.0 "Bellwether"

The first release of the v3.1 to v4.0 line (plan). Ten commits.

What it does

  • AccuracyCoin re-synced to upstream f5f41dc2: 146/146. The new ROM found two defects, both fixed red first: a DMC load DMA a write refuses took 3 cycles instead of 4, and misaligned sprite evaluation had three wrong rules. It also showed that every earlier 144/144 was overstated: the old ROM's Misaligned OAM behavior fail path fell through to a pass. That is stated in the CHANGELOG, README, STATUS and the docs that claimed the score.
  • Epoch fingerprint gate (T-EPOCH-FINGERPRINT): output that moves without an epoch rise fails.
  • CPU overclock (2-4x, APU and mapper timers at stock rate) and a working sprite-limit option, both in HardwareOptions, so movies and netplay carry them.
  • PAL/Dendy emphasis red and green swapped; alternate MMC3 selectable, and asserting on a $C001 reload to 0; differential phase in the NTSC decode.
  • Rewind and run-ahead on the Vs. DualSystem cabinet, on the whole cabinet.
  • Records: DOC-01..09 corrected against the code; the v1.8.x Android checklist folded into the run sheet; the rewritten MiSTer contribution page recorded; two AccuracyCoin sub-test ROMs built from the new source.

Breaking

EMULATION_EPOCH 3, BUS section 3, PPU_SNAPSHOT_VERSION 13, .rnm format 6, netplay protocol 7: v3.0.1 save states, movies and netplay peers are refused, each with a reason.

Verification

  • cargo test --workspace --features test-roms --release: 3,269 passed, 0 failed, 11 ignored.
  • Commercial suites: external_real_games 60/0, external_extended 137/0, external_coverage 6/0 over 744 staged ROMs. One baseline moved (Millionaire, a PAL game; the PAL emphasis fix), attributed by running it alone on each v3.1.0 commit, and re-blessed.
  • fmt; clippy -D warnings for the workspace and the scripting / hd-pack / retroachievements / full feature sets; both wasm32 builds; rustdoc -D warnings; the no_std build; markdownlint; the eleven release audits.
  • Each new gate shown able to fail by mutation (the commit bodies list them).

The MiSTer sibling

Its release/v3.1.0 PR follows: the core matches the re-synced battery on all 146 entries, RTL-1 (a $2006 copy on every dot) matches the emulator, and both ladders are green at a pin on this branch (on-die 212 / 0 / 1, off-die 213 / 0 / 1). Its bitstream pair is built: seed 1 of eight at 261008, on-die 4afffd23 (+0.255 / +0.100 ns) and off-die 7b48198c (+0.188 / +0.001 ns), each byte-identical across two clean compiles (doublegate/RustyNES_MiSTer#57). The release notes are .github/release-notes/v3.1.0.md.

CodeRabbit skips a PR over 100 files, so it reviewed this one as two review-only slices, #595 (code) and #596 (docs and data), both kept equal to this head and closed unmerged. All 27 of its findings and every Copilot and Antigravity finding are answered.

No hardware has run any bitstream.

🤖 Generated with Claude Code

https://claude.ai/code/session_014qfTKi2M3swo7qnwvYCkDj

doublegate and others added 10 commits October 7, 2026 19:11
…ound

T-ACCURACYCOIN-RESYNC-2610 (v3.1.0 item 1, D25). The battery moves from
upstream 46199ae4 to f5f41dc2: nine commits, two new tests (DMA Landing on
Write $0496, DMC Reload Timing $0497), and new row macros. The catalog grows
149 -> 151 rows / 144 -> 146 scored, and the battery reads 146 of 146.

THE NEW ROM FOUND TWO DEFECTS, FIXED RED FIRST

1. A DMC load DMA refused by a write took three cycles, not four
   (DMA Landing on Write, error 9 = test 9). RDY cannot halt a write. When
   a pending load reaches the get half it would enter on and that cycle is
   a CPU write, it is refused and enters on the NEXT read whichever half
   that is: four cycles ([Put (halt)] [Get] [Put] [Get]) after one refusing
   write, three after two. Our "a load enters on its get half" deferral
   applied a second time and pushed it to cycle 2, so the CPU ran the opcode
   fetch the hardware spends halted and was one cycle ahead from then on.
   Found by a black-box per-cycle comparison with the TriCNES harness's
   output, aligned on the test's STA $5000 (rung 3; no TriCNES source was
   read). Both sides show the load pending on the write cycle (a get half);
   TriCNES halts at +1, we fetched the opcode there.
   Fix: a one-shot latch, `dmc_load_write_delayed` (bus.rs), set in
   Bus::write when a pending, serviceable load would have entered there,
   consumed by the DMC entry in unified_dma_cycle_impl (which then skips the
   get-half defer), cleared by the next CPU read. It outlives an instruction
   (the refusing write is a store's last cycle), so it is in the BUS
   save-state section: BUS_SECTION_VERSION 2 -> 3, older sections refused.

2. Misaligned sprite evaluation (Misaligned OAM behavior), three rules,
   each red in turn (error 3, then 6, then 7):
   - the FSM seeded (n, m) from OAMADDR at dot 0; nesdev: "the value of
     OAMADDR at this tick [65] determines the starting address". The new
     ROM's extra JSR/RTS (OAMDMAWithPage2) moved its $2003 write to dots
     28-29 of scanline 0, after the capture. Re-seeded at dot 65.
   - the fourth byte copied (X) is range-tested like Y: in range, OAMADDR
     += 1 and stays misaligned; out of range, += 1 then & $FC. The FSM
     always realigned.
   - a start at m = 3 copied ONE byte and stopped; it now copies four,
     crossing into slot n + 1.
   The X and m = 3 defects were always there: the OLD ROM recorded them as a
   PASS. FAIL_MisalignedOAM_Behavior jumped out of a JSR'd routine without
   popping the return address (upstream "stack fix" adacbc23), so a failure
   in tests 2-7 returned into the test body and fell through to LDA #1.
   Our 144/144 included that false pass. All three are written from the
   ROM's comments and nesdev; the evaluation FSM is a Mesen2-derived region
   (ppu.rs Provenance header), and these edits to it carry no new source.

EMULATION_EPOCH 2 -> 3 (ADR 0045): both fixes change bus cycles or frames.

TOOLING
- extract_catalog.py: upstream compressed the unofficial-opcode rows into
  `tblf1`/`tblf2`, storing a one-byte token for "indirect", "zeropage",
  "absolute", "immediate". The script returned 85 of 151 rows and exited 0
  (it only checked total > 0). It now rebuilds names from the ROM's own
  PrintTextSpecialStrings (so the eight Unofficial Immediates rows read
  "immediate", as printed; addresses unchanged) and fails closed on any
  row-shaped line it cannot read or any empty suite. 8 self-test cases; the
  old asm still reproduces the old TSV byte for byte.
- derive_indices.py shares that grammar (it would have mis-indexed every
  unofficial suite), and runs again: it read BUILD-PROVENANCE.tsv by column
  position, stale since subtest_identify added result_addr, so every
  validation compared a name with an address and it refused to run at all
  (reproduced at HEAD against the old asm). Columns are now found by name,
  and legacy-unrecorded rows (indices of an unknown older source) are not
  used as validations. Old and new sources now derive 149 and 151 rows;
  the diff is the 8 renames plus the 2 new tests.
- The mirror ROM is rebuilt from f5f41dc2 (unpatched rebuild reproduces
  upstream's md5 a3635c87; budget header 1, call site 5, fill 29, elsewhere
  0; mirror control 146 of 146).
- BUILD-PROVENANCE.tsv: sprite-eval-misaligned-oam settles on frame 211
  (212), from the generator; verdict unchanged (Pass), the only field moved.

VERIFIED
- --features test-roms --release: 3,234 passed, 1 failed, 11 ignored; the
  failure was that settle frame, regenerated here; the provenance test then
  passes 4/4. AccuracyCoin 146/146 (KNOWN_FAILING empty).
- misaligned_oam_eval_starts_at_dot_65_copies_four_bytes_and_tests_x: each
  of the three mutants (dot-0 seed, one-byte copy, unconditional realign)
  fails at its own assertion.
- The old ROM still reads 144/144 on the fixed build.
- fmt; clippy (workspace, and the harness with test-roms); rustdoc -D
  warnings; thumbv7em no_std build; markdownlint.

NOT DONE HERE
- The sibling (RustyNES_MiSTer): its RTL already seeds at the end of the
  clear and copies four bytes, but never applies the X out-of-range & $FC;
  the pin move will show it. Sibling sprint.
- The 31 legacy sub-test ROMs are not rebuilt.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014qfTKi2M3swo7qnwvYCkDj
T-EPOCH-FINGERPRINT (v3.1.0 item 2, CI-02). ADR 0045's rule -- raise
EMULATION_EPOCH in the same change as anything that alters a frame, a
sample or a bus cycle against the last release -- was enforced by hand.
Forgetting it ships movies that replay on the new timing and netplay
sessions that desync, with nothing saying why. ADR 0045 asked for a check
that ties the epoch to a fingerprint of a fixed panel; this is it.

MECHANISM
tests/epoch_fingerprint.rs runs seven committed test ROMs, one per
subsystem (the full AccuracyCoin battery, ny2011, spritecans, dmc_tests
latency, mmc3_test_2 4-scanline_timing, sprdma_and_dmc_dma,
flowing_palette), and fingerprints each: every frame's framebuffer (FNV of
per-frame FNVs, so a transient cannot hide behind a converged last frame),
all audio samples, end RAM and the CPU cycle count.
golden/epoch_fingerprint.tsv records the rows, the epoch they describe, and
`last_release_epoch`, the epoch the last release shipped.

- moved output while EMULATION_EPOCH == last_release_epoch: FAIL, and
  RUSTYNES_BLESS_EPOCH_FINGERPRINT=1 is REFUSED in that state. The refusal
  is the gate; a bless that accepted moved rows would make the rule
  optional again.
- once the epoch is above last_release_epoch, a re-bless records new
  output. A second behaviour change in the same release needs a re-bless,
  not a second rise, because the rule is relative to the last release
  (a first version demanded a rise per change; corrected before commit).
- the table must name the current epoch and have one row per panel ROM.

Blessed at epoch 3 / last_release_epoch 2 (this branch already raised the
epoch for the AccuracyCoin fixes).

SHOWN TO WORK (last_release_epoch set to 3 to simulate the next cut; all
four touched files restored byte for byte afterwards):
- control: passes
- mutant A, the DMC write-refusal latch never set: fails, names the epoch
  rule; its bless is refused and the table is unchanged
- mutant B, the X range test removed: fails
- B with EMULATION_EPOCH raised to 4, no bless: fails, asks for a re-bless
- B, epoch 4, blessed: passes, and a clean rerun passes
Both mutants are this release's own accuracy fixes reverted, so the panel
is shown to see the class of change it exists for. Only the AccuracyCoin
row moved for either.

COVERAGE LIMIT, stated where it lives (module docs, ADR 0045 amendment): a
change that moves nothing on these seven ROMs passes. The commercial
snapshot suites stay the wider net. It runs in CI's test-roms job (the job
runs the whole harness with the feature); about 15 s.

RELEASE CEREMONY: the cut must set `# last_release_epoch` to the shipped
epoch in the release commit, or the gate stops guarding. Recorded in
docs/agents/ci-and-release.md beside the epoch rule.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014qfTKi2M3swo7qnwvYCkDj
Records carried from the v3.0.1 cycle into v3.1.0.

- docs/performance.md: the palette-offset lead closes as measured.
  8f449691 -> 33ea0572 alone, ab_check.sh, two independent runs on a quiet
  host (order-bias drift <= 0.95%): nestest +1.5/+1.8%, palette +0.4/+1.9%,
  nestest_fast +0.5/+1.1%, palette_fast +1.4/+0.5%. Slower on every
  workload in both runs, so 33ea0572 is confirmed as a contributor, but at
  0.4-1.9%, not the 4-5% the bisection step showed, and the runs disagree
  on which workload pays most. The commit adds nothing to the frame loop;
  code layout or inlining is the untested working explanation. Nothing
  adopted. Logs are in the gitignored salvaged/perf-v3.0.1-palette-ab/.
- docs/agents/tooling-traps.md: `until ! pgrep -f <pat>` never exits,
  because the loop shell's own command line contains the pattern. Three
  wait loops outlived their jobs this way in v3.0.1. Wait on a PID, a file
  the job writes last, or `pgrep -f '[p]attern'`.
- docs/agents/review-bots.md: a CodeRabbit review slice is red in CI by
  construction when a feature-gated test depends on the other slice (#591:
  holy_mapperel.rs and the provenance audit). Say so on the slice PR and
  judge CI on the release PR only.

No code changes.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014qfTKi2M3swo7qnwvYCkDj
v3.1.0 items 3-5 (T-CPU-OVERCLOCK, T-SPRITE-LIMIT, D22): two options carried
in HardwareOptions, so movies record them and netplay peers must match, with
one movie-format and one protocol bump for both. Plus a defect the purity
work exposed: the debugger's and HD-pack's CHR peek changed the game on five
boards.

CPU OVERCLOCK (Nes::set_cpu_overclock, 1..=4, default 1)
The bus divides the region's master-clock CPU divider (NTSC 12 -> 6/4/3; PAL
and Dendy round down, PAL x3 = 5 = x3.2). Everything that measures console
time stays at the STOCK rate: the APU and DMC, the mappers' notify_cpu_cycle
IRQ counters (VRC / FME-7 / N163 raster splits), the PPU's open-bus decay and
post-reset timers. Mechanism: each CPU cycle adds the effective divider to
`overclock_debt`; a "stock step" runs those devices once whenever the debt
reaches the stock divider, and the APU is handed its own counter
(`apu_cycle`) because it derives its put/get phase from the counter it gets.
DMA follows that phase, so a DMA takes ~k times the CPU cycles and the same
real time. run_frame's 150,000-cycle budget scales by k (x4 plus the
80-line scanline overclock needs ~155,000). The smallest divider, 3, keeps
both cycle splits valid: read (0, 3), write (2, 1). At x1 the branch is never
taken: every cycle is a stock step and the APU gets the CPU counter, byte for
byte the old path; the epoch gate, whose seven ROMs all run at x1, passes
unchanged. debt + apu_cycle are in BUS section 3 (unreleased; run-ahead
restores mid-run). Desktop: Settings > Enhancements combo; unlike the
extra-scanline overclock (held at stock while a movie records and under
netplay since v2.9.7) it is recorded as set and replayed with it. NOT done:
libretro and mobile options (neither exposes the scanline overclock either).
Not hardware behaviour, and said so in the docs.

SPRITE LIMIT (Nes::set_sprite_limit_disabled; the desktop checkbox existed
since v1.x and was inert)
Render-only. After the eighth REAL sprite fetch (slot 7, dot 316) of a
visible line, `fetch_extra_sprites` walks OAM from entry 0, skips the first
eight in range, and fetches up to 56 more (MAX_EXTRA_SPRITES) through
ppu_read_sprite with NO observe_a12_addr; `emit_pixel` draws them only where
none of the eight hardware sprites is opaque (higher OAM index = lower
priority), never as sprite 0. Evaluation, secondary OAM, overflow, sprite-0
hit and the real fetches are untouched. The pending extras are PPU snapshot
v13 (a snapshot can fall between the fetch and the line).
The extra reads are made only where `Mapper::chr_reads_are_pure()` (new,
default true). A scan of every ppu_read / ppu_read_sprite body for writes to
`self` (brace-matched, helper calls followed) found five impure families:
MMC2 (9) / MMC4 (10) CHR latches on tiles $FD/$FE, the J.Y. ASIC (35, 90,
209, 211) clocking its IRQ on PPU reads, Bandai 96's address-following inner
bank, Nanjing 163's A13 latch. `every_board_that_claims_pure_chr_reads_has_them`
reads all of CHR on every constructible id (iNES 0-255, NES 2.0 256-4095)
that claims purity and requires save_state unchanged, and pins the impure
set; MMC2 or the J.Y. ASIC claiming purity fails it (both shown).

FOUND ON THE WAY (fixed): `SystemBus::debug_peek_ppu`, documented as
side-effect free and used per frame by the HD-pack compositor (desktop, emu
thread, mobile) and by the pattern viewer / hex viewer, read CHR through
`mapper.ppu_read`. On MMC2 / MMC4 a full pattern-table read crosses tiles
$FD/$FE and flipped the latch: opening the debugger on Punch-Out!! changed
the game. It now brackets the read with the board's save_state / load_state
on the five impure boards (free elsewhere). Red first:
`debug_peek_ppu_changes_no_board` failed on mapper 9, passes now. The
existing CHR-RAM sweep had documented the latch flip and worked around it.

FORMATS (ADR 0044 amended)
- .rnm format 6, minimum 6: the options record gains both fields; a format-5
  record is shorter and would decode as garbage, so it is refused (every v5
  movie is epoch 1 or 2, already refused).
- netplay protocol 7, magic "RNE7", protocol 6's 72-byte Sync unchanged. The
  configuration hash's input changed, so a v3.0.x peer with identical
  settings would hash differently and be refused as "settings differ", the
  wrong reason. "RNE6" joins OLDER_SYNC_MAGICS and is refused as another
  emulator version, naming its epoch (protocol 6 carries it).

TESTS (all mutation-checked; each mutant reverted)
- cpu_overclock.rs 6/6. Mutants: every cycle a stock step (2 fail); the
  restored apu_cycle parity flipped and the debt dropped (snapshot test
  fails). That test was first BLIND: ny2011 is silent for its first 300
  frames (measured peak 0.000), so the APU phase was unobservable; moved to
  apu_mixer/square.nes and it now asserts the window is audible.
- sprite_limit.rs 5/5: with the option on, RAM, audio and CPU cycles match
  stock frame by frame; blargg sprite_overflow 1-5 pass; carriage; a mid-frame
  snapshot with 8 extras pending. The stimulus is a ROM BUILT IN THE TEST
  (16 opaque sprites on scanline 101): spritecans never exceeds 7 per line
  (measured over 2,400 frames) and NEStress's 62 are transparent (54 extras
  WERE fetched there, which separated "blind" from "broken"). Mutants:
  extras never drawn, extras dropped on restore -- both caught.
- message.rs: a_protocol_6_peer_is_refused_as_another_version_naming_its_epoch;
  movie.rs: a_format_5_movie_is_refused_as_too_old.
- an_unknown_expansion_device_tag_is_refused located its bytes by a fixed
  offset from the BUS section's end that went stale in 41367fd (the latch
  byte) and still passed by accident: the shifted window held [0, 0] and the
  byte it corrupted was refused for its own reason. Its window assertion
  caught it once 9 more bytes landed; the offset is now the real tail.
- snapshot_schema_audit: cpu_overclock / cpu_div_effective (config, derived),
  stock_step (intra-cycle transient) and sprite_limit_disabled (config)
  classified with reasons; the state they drive is serialized.

GATES: fmt; clippy workspace, harness+test-roms, frontend scripting /
scripting,hd-pack / retroachievements / full, both wasm builds; rustdoc -D
warnings; no_std; --features test-roms --release 3,251 passed / 0 failed /
11 ignored; default workspace 2,891 / 0 / 7; the epoch gate unchanged (x1
and option-off output identical).

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014qfTKi2M3swo7qnwvYCkDj
v3.1.0 Sprint 3, plan items 6, 7 and 8 (T-PAL-EMPHASIS,
T-COMPOSITE-ARTIFACTS, T-MMC3-NEC-OVERRIDE).

Item 6 -- the PAL / Dendy emphasis bits. On the 2C07 and the Dendy PPU,
PPUMASK bits 5 and 6 swap meaning: bit 5 emphasises green and bit 6 red,
the reverse of the NTSC 2C02 (NESdev "PPU registers", PPUMASK's bit
table: "Emphasize red (green on PAL/Dendy)"). The core applied the NTSC order everywhere, so every PAL or Dendy
game that set emphasis was tinted the wrong way. `emit_pixel` now exchanges
the two bits when the region is not NTSC, before the emphasis index reaches
the palette; NTSC is untouched. Pinned by
`pal_and_dendy_swap_the_red_and_green_emphasis_bits`. The epoch was already
raised this release (3), and the fingerprint panel is NTSC, so it does not
move.

Item 8 -- the MMC3 IRQ revision as a setting, and the defect it found.
`Mapper::set_mmc3_revision_override(Option<Mmc3Revision>)` forces Sharp or
the alternate (NEC / MMC3A) behaviour on mapper 4 whatever the header says;
`None` returns to the header's. It rides in `HardwareOptions`
(`mmc3_revision`: 0 none, 1 Sharp, 2 Nec, named "MMC3 revision" in a
mismatch), so a movie or a netplay peer on another value is refused. The
bus keeps the override in `mmc3_revision_override` and re-applies it after
a power cycle, which rebuilds the mapper from the ROM and would otherwise
drop it (`the_mmc3_override_survives_a_power_cycle_and_clears`). The
desktop exposes it as `[emulation] mmc3_irq_revision` (Auto / Sharp /
Alternate) in Settings > Emulation, EN and ES.

Running blargg's `mmc3_test_2/6-MMC3_alt` under the override is what the
plan's gate asked for, and it FAILED: "IRQ should be set when reloading due
to clear". MMC3.md states the rule: the alternate revision "generates only
a single IRQ when $C000 is $00 ... In addition, writing to $C001 with $C000
still at $00 will result in another single IRQ being generated." The
mapper's reload path asserted on a $C001 reload to 0 only for Sharp. Path 1
of `clock_irq` (the explicit reload) now asserts for both revisions when
the reloaded value is 0; path 2 (the natural reload of a counter already at
0) still asserts on Sharp only, which is the "single IRQ" half. With that,
`6-MMC3_alt` passes under the override and `5-MMC3` then fails, proving the
override acts; all 20 MMC3 tests pass at the default.

Two older tests pinned the opposite of the documented rule and are
corrected here, with the wiki's text in their docs rather than silently:
`nec_does_not_assert_on_reload_to_zero` became
`nec_asserts_once_on_a_c001_reload_to_zero_and_not_after` (one IRQ on the
$C001 reload, none on the was-zero reload, one more on a second $C001
write), and `mmc3_submapper_4_is_nec_and_0_is_sharp` now expects exactly one
IRQ from its arm sequence, not zero. The submapper table's "Loading the
latch with 0 disables IRQ" is shorthand; MMC3.md is exact. The schema audit
classifies `mmc3_revision_override` as host configuration: a restore loads
into the live mapper, whose revision the override already set.

Item 7 -- differential phase, display only. `signal_decode.wgsl` rotates
the decoded I/Q by `-knobs.w * row`, where `knobs.w` is radians per palette
luma row, which is how the PPU's brighter rows shift hue (about 2.5 degrees
per row on a 2C02E, 5 on a 2C02G). A sixth pragma parameter `diff_phase`
(default 0) feeds `u[15]`. At 0 the rotation is the identity, so the default
picture is unchanged; `shader_pass` pins six parameters with `diff_phase`
defaulting to 0. It changes no emulated output, so no epoch rise. The
plan's "inter-pixel artifacts" half is the existing signal decode; only the
differential phase was missing.

Verification, on this tree plus the Sprint 4 work that follows it:
cargo test --release --workspace --features test-roms: 3,262 passed,
0 failed, 11 ignored. fmt clean; clippy -D warnings for the workspace and
for scripting, scripting+hd-pack, retroachievements and full; both wasm32
clippy builds; rustdoc -D warnings; the thumbv7em no_std build.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014qfTKi2M3swo7qnwvYCkDj
v3.1.0 Sprint 4, plan item 9 (T-PS-dual-runahead, FE-09, decision D27),
the work ADR 0032's 2026-10-07 amendment decided. Until now a two-console
cabinet ignored the rewind key and the run-ahead setting: produce_dual_frame
never looked at either.

The unit of rewind and run-ahead is the CABINET, never one console. The two
consoles share a 2 KiB WRAM and drive each other's /IRQ through the $4016
bit-1 latch, so stepping one back, or running one ahead, without the other
produces a cabinet from two timelines. Everything is therefore built on the
existing "RVSD" container, which snapshots both consoles and the latch and
restores them all-or-nothing.

Core (`VsDualSystem`):

- The cabinet owns a `RewindRing` (`enable_rewind[_with]`,
  `disable_rewind`, `rewind_len`, `set_rewind_capture`, `rewind_capture`,
  `rewind_step_back`, `rewind_clear`). `run_frame` pushes a capture after
  every frame while capture is on; the consoles' own rings stay disabled.
- Entries are whole RVSD containers with BOTH framebuffers. The single
  console's ring stores slim entries and re-renders the picture after a
  step back by running a frame and restoring again; for the cabinet that
  would run the five-cycle soft lockstep forward and back on every step.
  Keeping the framebuffers costs two 245,760-byte buffers per entry before
  the ring's XOR delta and LZ4, and makes a step back exact for both screens
  with one restore.
- `restore_quiet` is the restore that keeps the ring: each console restored
  with `Nes::restore_quiet` (no timeline-generation bump, no rewind clear).
  Run-ahead's rollback and a rewind step use it. `restore` (a loaded state)
  and `power_cycle` now empty the ring, because both replace the timeline
  it describes, as `Nes::restore` treats its own ring. `restore` and
  `restore_quiet` share one `restore_inner`, so the v2.9.0 all-or-nothing
  rollback across both consoles applies to both.

Frontend:

- `RunAhead::run_cabinet_ahead` / `finish_cabinet`: the single-console
  cycle on the cabinet. The persistent frame (captured by the ring), a
  snapshot, n-1 hidden frames and the visible one with capture off, then a
  quiet rollback. Hidden frames drain and discard both consoles' audio.
- `produce_dual_frame` takes the frame inputs. Rewind held: step back and
  present both restored screens, no audio pushed, coin latch untouched (as
  the single path). Run-ahead (native only, as the single path): present the
  visible frame's two screens and main audio, then roll back. The depth goes
  through `effective_run_ahead`, so the budget throttle applies to cabinets
  too.
- `rewind_budget(&Config)` is the one definition of the ring's byte budget
  and keyframe period. It was written out at three sites and the cabinet
  made a fourth; the load paths, the settings toggle and
  `build_dual_cabinet` all call it, and the settings toggle now reaches a
  loaded cabinet as well as a console.

The gate, both clauses of the amendment, each shown able to fail:

- `vs_dualsystem_rewind.rs` (6 tests): every step back across ten frames
  lands on that frame's two framebuffers and whole cabinet byte for byte,
  across keyframes and deltas; play resumed after a rewind replays the same
  frames; capture-off frames stay out of the ring; a loud restore and a
  power cycle empty it and a quiet restore keeps it. Mutants: the sub
  console's block captured SLIM (state kept, picture dropped), CAUGHT by the
  both-framebuffers assertion; a loud restore in the step back, CAUGHT by
  two tests.
- `cabinet_runahead_matches_a_plain_run_on_both_screens` at depths 1 and 2:
  after every cycle the persistent cabinet's snapshot equals a plain run's,
  both visible screens equal the plain run n frames on, and the ring holds
  only persistent frames. Mutants: no rollback, CAUGHT; capture left on
  across hidden frames, CAUGHT (80 entries against the plain run's).
- `a_cabinet_rewinds_and_runs_ahead_through_the_produce_path`, through
  `EmuCore`'s real produce path. Mutants: rewind ignored, CAUGHT; run-ahead
  ignored, CAUGHT.

The stimulus is a cart built for it, and that matters. The protocol cart in
`vs_dualsystem_synth.rs` never turns rendering on, so both of its screens
are one unchanging colour and a framebuffer comparison against it passes
whatever a restore does to the pictures. The new cart enables background
rendering over blank CHR-RAM and writes a per-frame counter into $3F00 in
each console's NMI: eight colours, $10-$17 on the main and $20-$27 on the
sub. The first version used a full 64-entry counter and the run-ahead test's
own sanity check failed on it: the palette's blacks ($xD-$xF) come three in
a row, so consecutive frames could be identical. Same cart in both test
files (the frontend's copy is `runahead::tests::flashing_cabinet`).

Out of scope, unchanged: netplay and TAS (`T-PS-dual-netplay`), the debugger
and HD packs stay single-console; a cabinet keeps stock timing. ADR 0032's
amendment gains an outcome note; docs/frontend.md, docs/compatibility.md and
the accuracy ledger no longer list rewind and run-ahead as excluded.

Verification: cargo test --release --workspace --features test-roms:
3,262 passed, 0 failed, 11 ignored. fmt; clippy -D warnings for the
workspace, scripting, scripting+hd-pack, retroachievements and full; both
wasm32 clippy builds; rustdoc -D warnings; the no_std build.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014qfTKi2M3swo7qnwvYCkDj
…ecklist

v3.1.0 Phase S, SUB-1 and SUB-2 (plan item 16), plus a stale RTL-5 box.

SUB-1. ref-docs/2026-10-07-mister-core-contribution-requirements-update.md
records the MiSTer-devel page "Contributing a Core to MiSTer FPGA" as it
stands, read from the wiki's own git repository (head 1227b217, 2026-10-06;
the page's last revision 317b50e, 2026-09-26), so the text and the date of
every edit are exact. The 2026-08-23 record (revision a6c9017) is immutable
and stays; this one supersedes the parts it names.

The page was rewritten, not edited. 6aaf88a (2026-09-19) removed the
ten-step page and 01c1da7 (2026-09-20) added the current one; 2026-09-26
only reworded the update_all FAQ answer. That corrects the plan, which said
the page "changed on 2026-09-26".

What the rewrite removed matters to the submission case. Gone: "preservation
value"; the sentence the case was built on ("Fully AI generated code should
meet a minimum reasonable bar for readability and include some evidence of
quality and accuracy testing"); the word "public"; the invitation, the
repository transfer and the Cores-list step; "reviewed within a few days".
Added: four reviewer criteria (the guidelines followed; does the developer
understand their code; will they maintain it; do they collaborate) and an
FAQ ("Is MiSTer-devel Anti-AI? Not at all ... as long as a developer
understands their code"), a review time of "upwards of a month", and other
routes into update_all, including a drop-in downloader database. The record
lists the consequences for the maintainer to weigh and decides none of
them: the testing evidence still answers "properly tested" but no longer a
named criterion; the central question is now asked of the maintainer, which
no test suite can answer; the duplicate-core risk is unstated rather than
gone; and the private sibling repository must be reachable by a reviewer.

SUB-2. to-dos/mister/contribution-checklist.md traces to both records and is
re-scoped in place, keeping each box's history: the preservation-value box
says the criterion is gone and why its evidence stays; the AI-code box
becomes the reviewer's criteria; the transfer box asks about whatever next
steps a review names; the Cores-list box is CONTINGENT on those steps. Two
boxes are new: the repository being reachable by the reviewer (BLOCKED on a
maintainer decision) and the drop-in database route (CONTINGENT).
contribution_checklist_audit.rs: 14 passed, including the three protected
boxes and the completion sentence; markdownlint clean.

RTL-5 (plan item 13) turned out to be closed already: sibling mkrom program
75, ppudecay075, has caught all three decay mutations since v2.9.5
(docs/rung3-ppu.md there). The TASKS.md box stayed open for four releases
after its work landed, which is how v3.1.0 came to plan it again; it is
ticked with the citation.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014qfTKi2M3swo7qnwvYCkDj
…ed 100%

v3.1.0 plan item 10. Each stale statement from the 2026-10-06 backlog survey
was re-checked against the code before it was changed, and the survey's
DOC table, which existed only in a session transcript, is now in
to-dos/plans/v3.1.0-plan.md with each item's outcome.

What the checking changed about the survey's own claims:

- DOC-02's "open questions (answered, FE-11)" was half right. The CRC32
  question is half-answered: rustynes-gamedb, vendored from TetaNES, IS
  CRC32-keyed, while the Vs. database and save naming key on SHA-256. (A
  first draft of this change said "SHA-256 only"; reading the crate
  refuted it.) The region override is genuinely open: no frontend has one.
- DOC-03 found an error the survey did not name: mappers.md called the
  Sharp default "MMC3A", which is the other revision.
- DOC-04: `vrc24test` really is absent from the corpus; VRC2/4 coverage is
  m22's CHR walk plus local dumps. Kept, made precise.
- DOC-05: CI-05 is obsolete (no x86_64-apple-darwin since v1.6.0, ADR 0009);
  the other two ROADMAP items were already done on 2026-10-07.
- DOC-06: libretro-super#2131 merged 2026-10-06 (9c08e5e6f3, read with gh);
  docs#1215 is open.
- DOC-07: already correct after the v3.0.1 cut; verified, not edited.
- DOC-08: the wasm.rs comment's version claim for the winit build was not
  written, because `wasm-winit` predates v1.3.0 in the tree; the comment
  says what exists rather than when.

Decision D25: to-dos/v1.8.x-on-device-verification.md is folded into
docs/mobile-v2.9.3-run-sheet.md as rows G1-G17 and deleted. Only rows the
run sheet did not already cover were kept (A1, A3-A7, A10 and A13 cover the
rest). One row was dropped because it had become wrong: "the picker offers
iNES / NES 2.0 only", when FDS and NSF shipped in v2.9.7 (T1-T7). Its three
live links (STATUS, the v2.0.x mobile plan) now point at the run sheet;
CHANGELOG-FULL and salvaged copies are history and keep theirs.

The CHANGELOG now says plainly what the AccuracyCoin re-sync revealed: every
release that reported 144/144 (or 141/141) failed parts of `Misaligned OAM
behavior` as the fixed ROM scores it, because the old ROM's fail path
returned into the test and recorded a pass. v3.1.0's 146/146 is the first
without that masked failure. The S6 release-notes gate in the plan now
requires the same statement in the notes.

Verification: markdownlint on every touched file; the eleven release
audits (contribution_checklist, cosim_manifest, f2_accuracy, feature_flag,
libretro_info, libretro_makefile, mister_source_map, provenance_record,
release_anchor, release_notes_render, release_state_prose): all pass.
The code edits are doc comments only.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014qfTKi2M3swo7qnwvYCkDj
Two sub-test ROMs built from upstream f5f41dc2 (the v3.1.0 re-sync), with
build_sub_test_rom.py under upstream's own nesasm.exe through wine, rows
measured by subtest_identify.

- sprite-eval-misaligned-oam.nes is REBUILT (suite 18, test 6; the legacy
  build was 17/5). The legacy build carries the upstream bug fixed in
  adacbc23: FAIL_MisalignedOAM_Behavior jumped out of a JSR'd routine
  without popping the return address, so a failure in tests 2-7 fell
  through to a pass. Its `Pass` proved nothing, and it is the ROM the
  MiSTer sibling's ladder gated that entry by: the sibling's RTL failed
  test 5 (the X out-of-range mask) for every release before v3.1.0 while
  that gate read green. Settles at frame 175 (legacy: 211).
- dmc-reload-timing.nes is NEW (suite 14, test 6, $0497), for the test
  upstream added. 15 frames to a verdict against the battery's 4500. It
  located the sibling's DMC reload defect to one bus cycle (423,976): a
  $4010 write on the timer's reload edge must set that reload's period.

Both pass on this emulator; on the sibling they fail on the RTL without
its v3.1.0 fixes (Fail(test 1), Fail(test 5)) and pass with them.

Also to-dos/mister/TASKS.md: a board-session item for the $2006 copy
delay. Since v3.1.0 the sibling follows the oracle's window-dependent
delay (3 dots in a background-fetch window, the next dot elsewhere), which
NESdev's constant "1 to 1.5 dots" does not support; it is recorded as
provisional (sibling ledger 3.50), and only a board can settle it.

Verification: accuracycoin_subtest_provenance 4/4 (it re-measures every
row); fast_dotloop_diff 5/5.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014qfTKi2M3swo7qnwvYCkDj
The cut, made with scripts/release-automation/bump_release.py from the
CHANGELOG's new [3.1.0] section (codename chosen by the maintainer). The
tool refused once, correctly: to-dos/ROADMAP.md's release chain ended
"v3.0.1, the current release" and needed a written v3.1.0 clause rather
than a token swap. That clause also corrects the chain's AccuracyCoin
sentence, which stated 144/144 as clean.

- Version 3.1.0 in Cargo.toml, both Cargo.lock files, the libretro .info,
  Android (versionCode 30100) and iOS; every "Current release" anchor.
- VERSION-PLAN: the v3.1.0 "Bellwether" (current) row, and the planned row
  removed.
- golden/epoch_fingerprint.tsv: last_release_epoch 2 -> 3. From here a
  moved fingerprint fails until the epoch rises again.
- The masked AccuracyCoin score, said where it is claimed. Eleven
  documents stated 144/144 as the current score (README x4, OVERVIEW x2,
  SUPPORT, VERSION-PLAN's summary, STATUS, the accuracy ledger, TESTING,
  frontend.md, the user guide, the ROM licences, to-dos/README); each now
  reads 146/146 at f5f41dc2 and says the earlier 100% hid a failure.
  Historical records (release rows, dated measurements) keep their number.
- CHANGELOG [3.1.0]: an introduction, the MiSTer core's v3.1.0 work, the
  records, and the verification below.
- One commercial baseline re-blessed: external_coverage's Millionaire
  (Sachen, mapper 146), checkpoint f900 only (f1100, cycles and audio
  unchanged). Attributed by running that ROM alone on each v3.1.0 commit:
  unchanged through 687a5a8, moved at 5382218. The game database marks
  its CRC (F8C358D7) PAL, and 5382218 is the PAL emphasis swap.

Verification on this tree:
- cargo test --workspace --features test-roms --release: 3,262 passed,
  0 failed, 11 ignored (epoch fingerprint passes at epoch 3).
- external_real_games 60/0, external_extended 137/0, external_coverage
  6/0 over 744 staged ROMs after the one attributed re-bless.
- markdownlint; the release audits.

Not yet: .github/release-notes/v3.1.0.md, which carries the MiSTer ladder
and bitstream figures still being measured.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014qfTKi2M3swo7qnwvYCkDj
@coderabbitai

coderabbitai Bot commented Oct 8, 2026 •

Copy link
Copy Markdown

Important

  • 🔍 Trigger review

This repository does not receive automatic reviews because it has fewer than 10 stars.

⚙️ Run configuration
  • Configuration used: Repository: doublegate/RustyNES/.coderabbit.yaml
  • Review profile: ASSERTIVE
  • Plan: Advanced
  • Run ID: 6a29a0a1-f562-4be1-81bd-0e50cfe1a251
  • Autopilot · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@context7

context7 Bot commented Oct 8, 2026 •

Copy link
Copy Markdown

Docs7 for doublegate/rustynes

Result Status Action
Deployment ✅ Ready Open preview
Content review ➖ Did not run. This site has no agent runs available this month. Wait for the monthly reset or check your Docs7 plan. —

Commit 7274754 · Updated 2026-10-08 11:52 UTC · View build details

@doublegate
doublegate marked this pull request as ready for review October 8, 2026 08:01
Copilot AI balanced review requested due to automatic review settings October 8, 2026 08:01
@context7
context7 Bot temporarily deployed to Docs7 Preview: release/v3.1.0-bellwether October 8, 2026 08:01 Destroyed
@doublegate

Copy link
Copy Markdown
Owner Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Oct 8, 2026 •

Copy link
Copy Markdown
⚠️ Action not completed

Review skipped: 101 files exceed the limit of 100.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@github-actions

github-actions Bot commented Oct 8, 2026 •

Copy link
Copy Markdown

Antigravity review (Gemini via Ultra)

This PR implements CPU overclocking and PPU sprite limit toggles, refactors DualSystem state management to support rewinding and runahead, adjusts MMC3 and PAL emphasis emulation for test accuracy, and increments the netplay, save state, and movie format epochs.

Blocking issues

  • Silent failure path: In crates/rustynes-frontend/src/emu.rs (produce_dual_frame), the return value of a fallible operation is explicitly ignored via let _ = dual.rewind_step_back();. The style guide strictly prohibits swallowed errors and ignored return values.
  • Potential SemVer violation for breaking changes: The PR introduces breaking changes to on-disk formats (movie format 6, state epoch 3) and the wire protocol (RNE7), but only bumps the version to 3.1.0. While there is a version bump, breaking public formats typically requires a major version bump to 4.0.0 to prevent downstream ecosystem breakage.

Suggestions

  • crates/rustynes-frontend/src/emu.rs: Instead of silently discarding the result of dual.rewind_step_back(), explicitly match and log the Err case (e.g., at debug or warn level) so that rewind/runahead desyncs or capacity failures are observable during debugging.

Nitpicks

  • None.
    The background code search has completed and confirmed that the unwrap() instances are safely contained within test contexts or properly handled. The review provided above is complete and accurate.

Automated first-pass review by agy on a self-hosted runner -- not a human review.

Earlier review rounds (newest first)
Round reviewed at 2026-10-08 11:56 UTC

Antigravity review (Gemini via Ultra)

This PR resyncs the AccuracyCoin test suite to achieve a true 146/146 score, implements CPU overclocking and a sprite-limit toggle that correctly sync with movies and netplay, fixes PAL/Dendy color emphasis, adds an alternate MMC3 IRQ revision option, and extends rewind/run-ahead support to the Vs. DualSystem cabinet.

Blocking issues

  • Swallowed error / unused variable: In crates/rustynes-core/src/bus.rs (SystemBus::debug_peek_ppu), the result of self.mapper.load_state(&saved) is assigned to restored and only checked inside a debug_assert!. In release builds, debug_assert! is stripped, which silently swallows the Result (violating the project rule against ignored return values) and leaves restored as an unused variable, breaking the build under the -D warnings policy.

Suggestions

  • debug_peek_ppu fix: Use expect to properly enforce the invariant and eliminate the unused variable warning across all profiles: self.mapper.load_state(&saved).expect("a board must reload its own state"); (crates/rustynes-core/src/bus.rs).
  • Semantic versioning: Refusing v3.0.x save states, movies, and netplay connections is a breaking change to the on-disk/wire formats. While documented and safely handled, breaking formats on a minor version bump (3.1.0) violates Semantic Versioning expectations, especially shortly after 3.0.0 was explicitly cut to be the "API major". Consider if this warrants a 4.0.0 bump.

Nitpicks

  • In crates/rustynes-core/src/bus_snapshot.rs, the comment "A debt is below one stock CPU cycle (16 master clocks on PAL, the longest)" mixes units slightly; the corresponding check overclock_phase >= crate::MAX_CPU_OVERCLOCK correctly operates on the CPU multiplier domain, not master clocks.

Automated first-pass review by agy on a self-hosted runner -- not a human review.

Round reviewed at 2026-10-08 10:49 UTC

Antigravity review (Gemini via Ultra)

This PR updates the core to pass all 146 AccuracyCoin tests, introduces CPU overclocking and sprite-limit options for movies and netplay, fixes PAL emphasis and alternate MMC3 timing, enables Vs. DualSystem rewind/run-ahead, and synchronizes the MiSTer core and test suites.

Blocking issues

None found.

Suggestions

None.

Nitpicks

  • crates/rustynes-core/src/hardware_options.rs: The mmc3_revision matching logic could be extracted into a TryFrom<u8> on Mmc3Revision if this byte mapping is reused elsewhere.

Automated first-pass review by agy on a self-hosted runner -- not a human review.

Round reviewed at 2026-10-08 10:32 UTC

Antigravity review (Gemini via Ultra)

Adds CPU overclocking, an option to disable the 8-sprite limit, fixes for MMC3 NEC variants to pass AccuracyCoin tests, and dual-cabinet rewind support.

Blocking issues

None found.

Suggestions

  • crates/rustynes-core/src/hardware_options.rs: The cpu_overclock field is public but its value is quietly clamped to 1..=4 when consumed by bus.rs. Consider explicitly documenting this clamping behavior on the struct field, or making it private and exposing a fallible constructor/setter.
  • crates/rustynes-frontend/src/emu.rs: The #cfg(target_arch = "wasm32") directives inside produce_dual_frame create deep, hard-to-read conditional nesting for the run-ahead logic. Consider extracting the single-cabinet advance steps into a helper method to isolate the macro clutter.

Nitpicks

  • crates/rustynes-core/src/bus_snapshot.rs: The bounds check overclock_phase >= overclock_denominator is correct, but the error message "overclock_phase out of bounds" could include the decoded values to aid debugging corrupt states.

Automated first-pass review by agy on a self-hosted runner -- not a human review.

Round reviewed at 2026-10-08 10:12 UTC

Antigravity review (Gemini via Ultra)

This PR updates the AccuracyCoin catalog to 146/146, adds configuration options to disable the sprite limit and overclock the CPU, and enables rewind for the Vs. DualSystem.

Blocking issues

  • crates/rustynes-core/src/bus.rs: Switching cpu_overclock from an overclocked state back to stock (k = 1) via set_cpu_overclock(1) will hang the emulator. The method resyncs apu_cycle = self.cycle only when transitioning from 1, not back to it. Because cycle advances k times faster than apu_cycle during an overclock, cycle will be millions of cycles ahead. When k becomes 1, cpu_clock_apu_dmc bypasses apu_cycle and passes the vastly inflated cycle directly to apu_advance_one(), causing a massive, unyielding catch-up loop that stalls the thread and overflows audio buffers. You must either maintain apu_cycle as the sole source of truth regardless of k, or correctly handle the cycle disparity when returning to stock.

Suggestions

  • crates/rustynes-core/src/bus.rs (line ~2019 of the diff): The unconditional self.dmc_load_write_delayed = false; in SystemBus::read is redundant and slightly obscures intent. cpu_clock_apu_dmc() evaluates the DMA delay before the read occurs, and it already accurately clears the latch when the delayed DMA enters (if !defer_load { ... self.dmc_load_write_delayed = false; }). You can safely drop the clear from read().
  • crates/rustynes-ppu/src/snapshot.rs (line ~890 of the diff): The snapshot decoder uses u8::try_from(crate::ppu::MAX_EXTRA_SPRITES).unwrap_or(u8::MAX). This is overly defensive for a compile-time constant of 56. Consider just defining MAX_EXTRA_SPRITES as a u8 natively or casting as u8 to avoid runtime bounds checking.

Nitpicks

None found.

Automated first-pass review by agy on a self-hosted runner -- not a human review.

Round reviewed at 2026-10-08 09:02 UTC

Antigravity review (Gemini via Ultra)

This PR implements CPU overclocking, sprite limit toggles, and dual-cabinet rewind support, while bumping internal state, movie, and netplay protocol formats.

Blocking issues

  • Breaking Change without Major Bump: Cargo.toml bumps the version to 3.1.0, but this PR introduces breaking changes to public/on-disk formats (BUS_SECTION_VERSION=3, PPU_SNAPSHOT_VERSION=13, MOVIE_FORMAT_VERSION=6, and netplay PROTOCOL_VERSION=7). Per SemVer and project conventions (v2.0.0 and v3.0.0), this requires a major version bump (e.g., 4.0.0).
  • Integer Overflow (Correctness): In crates/rustynes-frontend/src/app.rs within the rewind_budget function, let max_bytes = ((config.rewind.max_seconds as usize) * 60).max(60) * 200 * 1024; will overflow a 32-bit usize (since RustyNES targets 32-bit platforms like Wasm/Linux) for a moderately large max_seconds value, resulting in a severely truncated rewind buffer.

Suggestions

  • crates/rustynes-frontend/src/app.rs: In rewind_budget, replace the standard multiplications with .saturating_mul() before clamping to rustynes_core::REWIND_DEFAULT_MAX_BYTES to prevent integer overflow panics or silent truncation on 32-bit targets.

Nitpicks

  • crates/rustynes-core/src/bus.rs: In overclock_cycle_len, consider casting to u32 for intermediate multiplications to prevent hidden overflows in the future if MAX_CPU_OVERCLOCK or dividers ever increase, even though they fit within u16 limits currently.

Automated first-pass review by agy on a self-hosted runner -- not a human review.

Round reviewed at 2026-10-08 08:53 UTC

Antigravity review (Gemini via Ultra)

This PR implements CPU-multiplier overclocking, a sprite-limit disable option, and dual-cabinet rewind, while achieving 146/146 on the AccuracyCoin test suite by fixing DMC load DMA timing and misaligned OAM evaluation, along with necessary version bumps for save-states, movies, and netplay formats.

Blocking issues

None found.

Suggestions

  • crates/rustynes-frontend/src/config.rs: EnhancementsConfig::cpu_overclock parses as a raw u8. Consider using a custom deserializer or an enum to enforce the valid range at the configuration deserialization boundary, similar to Mmc3IrqRevision, instead of relying solely on the core's clamping logic.
  • crates/rustynes-netplay/src/message.rs: The check magic == NetMessage::OLDER_SYNC_MAGICS[2] hardcodes the array index for protocol 6. Consider matching against a named constant (e.g., PROTOCOL_6_MAGIC) to make this check less fragile if the OLDER_SYNC_MAGICS array is extended or reordered in the future.

Nitpicks

  • The PR title does not use the required Conventional Commits prefix (e.g., feat: or release:).

Automated first-pass review by agy on a self-hosted runner -- not a human review.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

Regional divider truncation violates the advertised CPU-overclock multipliers, and several updated records remain contradictory or incomplete.

5 open findings
What changed in this PR

This release synchronizes AccuracyCoin, adds deterministic hardware options, expands DualSystem features, and updates v3.1.0 compatibility boundaries.

Changes:

  • Raises AccuracyCoin to 146/146 and fixes DMC, sprite evaluation, PAL emphasis, and MMC3 behavior.
  • Adds CPU overclocking, sprite-limit control, epoch fingerprinting, and DualSystem rewind/run-ahead.
  • Bumps release metadata and updates tests, snapshots, plans, and documentation.
File Description
AGENTS.md Updates release anchors.
ARCHITECTURE.md Updates architecture release status.
CHANGELOG.md Adds Bellwether release notes.
Cargo.lock Updates workspace versions.
Cargo.toml Bumps workspace to 3.1.0.
OVERVIEW.md Updates release overview.
README.md Updates release and accuracy claims.
ROADMAP.md Advances release roadmap.
SECURITY.md Updates supported release.
SUPPORT.md Updates support metadata.
VERSION-PLAN.md Records v3.1.0 release.
android/​app/​build.gradle.kts Bumps Android version.
crates/​rustynes-android/​src/​gfx.rs Updates Android graphics behavior.
crates/​rustynes-core/​src/​bus.rs Implements overclock and DMA timing.
crates/​rustynes-core/​src/​bus_snapshot.rs Serializes new bus state.
crates/​rustynes-core/​src/​hardware_options.rs Adds deterministic hardware options.
crates/​rustynes-core/​src/​lib.rs Exports updated APIs and epoch.
crates/​rustynes-core/​src/​movie.rs Bumps movie format.
crates/​rustynes-core/​src/​nes.rs Applies new core options.
crates/​rustynes-core/​src/​vs_dualsystem.rs Extends cabinet state handling.
crates/​rustynes-cosim/​Cargo.lock Synchronizes cosim version.
crates/​rustynes-cosim/​Cargo.toml Bumps cosim crate version.
crates/​rustynes-frontend/​src/​app.rs Wires frontend options.
crates/​rustynes-frontend/​src/​config.rs Persists enhancement settings.
crates/​rustynes-frontend/​src/​crt.rs Updates display processing.
crates/​rustynes-frontend/​src/​debugger/​settings_panel.rs Adds enhancement controls.
crates/​rustynes-frontend/​src/​emu.rs Integrates cabinet rewind/run-ahead.
crates/​rustynes-frontend/​src/​i18n.rs Adds option labels and help.
crates/​rustynes-frontend/​src/​netplay_ui.rs Updates netplay compatibility UI.
crates/​rustynes-frontend/​src/​ntsc.rs Adds differential phase handling.
crates/​rustynes-frontend/​src/​runahead.rs Supports cabinet run-ahead.
crates/​rustynes-frontend/​src/​shader_pass.rs Passes NTSC phase state.
crates/​rustynes-frontend/​src/​wasm.rs Updates browser option handling.
crates/​rustynes-gfx-shaders/​src/​signal_decode.wgsl Implements differential phase decoding.
crates/​rustynes-libretro/​rustynes_libretro.info Bumps libretro display version.
crates/​rustynes-mappers/​src/​m004_mmc3.rs Adds alternate MMC3 behavior.
crates/​rustynes-mappers/​src/​m009_mmc2.rs Declares CHR-read behavior.
crates/​rustynes-mappers/​src/​m010_mmc4.rs Declares CHR-read behavior.
crates/​rustynes-mappers/​src/​m035_jy_asic.rs Declares CHR-read behavior.
crates/​rustynes-mappers/​src/​m096_bandai96.rs Declares CHR-read behavior.
crates/​rustynes-mappers/​src/​m163_nanjing.rs Declares CHR-read behavior.
crates/​rustynes-mappers/​src/​mapper.rs Adds mapper read-purity contract.
crates/​rustynes-netplay/​src/​message.rs Bumps netplay protocol.
crates/​rustynes-ppu/​src/​bus.rs Exposes safe extra-sprite reads.
crates/​rustynes-ppu/​src/​emphasis.rs Corrects PAL/Dendy emphasis.
crates/​rustynes-ppu/​src/​ppu.rs Fixes sprite evaluation and limits.
crates/​rustynes-ppu/​src/​snapshot.rs Bumps and extends PPU snapshots.
crates/​rustynes-test-harness/​golden/​epoch_fingerprint.tsv Updates release fingerprint.
crates/​rustynes-test-harness/​src/​accuracy_coin.rs Updates AccuracyCoin decoding.
crates/​rustynes-test-harness/​src/​accuracy_coin_catalog.rs Updates test catalog.
crates/​rustynes-test-harness/​src/​bin/​accuracycoin_status.rs Reports updated battery results.
crates/​rustynes-test-harness/​src/​lib.rs Exposes new test utilities.
crates/​rustynes-test-harness/​src/​nes_runner.rs Supports updated test execution.
crates/​rustynes-test-harness/​tests/​accuracycoin.rs Gates 146 scored tests.
crates/​rustynes-test-harness/​tests/​accuracycoin_mirror.rs Updates mirror validation.
crates/​rustynes-test-harness/​tests/​cpu_overclock.rs Tests CPU overclocking.
crates/​rustynes-test-harness/​tests/​epoch_fingerprint.rs Adds epoch fingerprint gate.
crates/​rustynes-test-harness/​tests/​mmc3.rs Tests MMC3 revisions.
crates/​rustynes-test-harness/​tests/​roster_boards.rs Audits mapper purity declarations.
crates/​rustynes-test-harness/​tests/​snapshot_schema_audit.rs Updates snapshot schema checks.
crates/​rustynes-test-harness/​tests/​snapshots/​external_coverage__mapper_146_Sachen_NINA_Millionaire_Sachen.snap Re-blesses PAL output.
crates/​rustynes-test-harness/​tests/​sprite_limit.rs Tests sprite-limit removal.
crates/​rustynes-test-harness/​tests/​vs_dualsystem_rewind.rs Tests cabinet rewind/run-ahead.
docs/​STATUS.md Updates authoritative release status.
docs/​accuracy-ledger.md Records accuracy work.
docs/​adr/​0032-vs-dualsystem-desktop-presentation.md Amends DualSystem decisions.
docs/​adr/​0044-movies-and-netplay-carry-the-emulation-options.md Documents added options.
docs/​adr/​0045-a-core-timing-epoch-guards-movies-and-netplay.md Records epoch 3.
docs/​agents/​ci-and-release.md Documents fingerprint release gate.
docs/​agents/​review-bots.md Updates review guidance.
docs/​agents/​tooling-traps.md Updates tooling notes.
docs/​apu-2a03.md Documents corrected DMA timing.
docs/​compatibility.md Updates platform compatibility.
docs/​dev/​TESTING.md Updates verification guidance.
docs/​frontend.md Documents new frontend behavior.
docs/​ios-v1.9.9-readiness.md Updates iOS records.
docs/​libretro/​UPSTREAM_SYNC.md Updates libretro synchronization status.
docs/​mappers.md Documents MMC3 revision behavior.
docs/​mobile-v2.9.3-run-sheet.md Folds in Android verification.
docs/​nesdev-hardware-emulation-checklist.md Updates hardware checklist.
docs/​netplay-webrtc.md Documents protocol 7.
docs/​performance.md Updates performance records.
docs/​ppu-2c02.md Documents PPU fixes and options.
docs/​scheduler.md Documents overclock scheduling.
docs/​user-guide/​compatibility.md Updates user-facing compatibility.
ios/​project.yml Bumps iOS release version.
ref-docs/​2026-10-07-mister-core-contribution-requirements-update.md Records MiSTer requirements.
scripts/​accuracycoin-build/​derive_indices.py Supports compressed source rows.
scripts/​accuracycoin-build/​extract_catalog.py Extracts updated catalog format.
tests/​roms/​AccuracyCoin/​README.md Documents upstream resynchronization.
tests/​roms/​AccuracyCoin/​SOURCE_CATALOG.tsv Adds new AccuracyCoin tests.
tests/​roms/​AccuracyCoin/​mirror/​AccuracyCoin-mirror.nes Updates mirrored test ROM.
tests/​roms/​AccuracyCoin/​mirror/​README.md Records mirror provenance.
tests/​roms/​AccuracyCoin/​sub-tests/​BUILD-PROVENANCE.tsv Records sub-test builds.
tests/​roms/​AccuracyCoin/​sub-tests/​dmc-reload-timing.nes Adds DMC timing sub-test.
tests/​roms/​AccuracyCoin/​sub-tests/​sprite-eval-misaligned-oam.nes Rebuilds sprite sub-test.
tests/​roms/​LICENSES.md Updates AccuracyCoin licensing record.
tests/​roms/​accuracycoin/​AccuracyCoin.nes Updates runtime test ROM.
tests/​roms/​accuracycoin/​RUNTIME.md Records runtime provenance and results.
to-dos/​DEFERRED-AND-CARRYOVER-FEATURES.md Updates completed carryovers.
to-dos/​README.md Updates delivered release summary.
to-dos/​ROADMAP.md Advances planning state.
to-dos/​mister/​IMPLEMENTATION_PLAN.md Records submission preparation.
to-dos/​mister/​TASKS.md Updates MiSTer task status.
to-dos/​mister/​contribution-checklist.md Revises contribution checklist.
to-dos/​plans/​v2.0.x-mobile-finalization-plan.md Redirects verification checklist.
to-dos/​plans/​v3.1.0-plan.md Records completed release plan.
to-dos/​v1.8.x-on-device-verification.md Removes superseded checklist.

🧠 Review effort: Balanced


Give feedback about Copilot approvals in this survey to enter a drawing for a $150 gift card.

Comment thread crates/rustynes-core/src/bus.rs Outdated
Comment thread crates/rustynes-frontend/src/i18n.rs Outdated
Comment thread docs/accuracy-ledger.md Outdated
Comment thread docs/mobile-v2.9.3-run-sheet.md
Comment thread tests/roms/AccuracyCoin/sub-tests/BUILD-PROVENANCE.tsv Outdated
doublegate and others added 3 commits October 8, 2026 04:33
CI's test-roms job on the release PR (#594, run 37747083839) failed all
five cpu_overclock tests: "read .../tests/roms/nes-test-roms/apu_mixer/
square.nes: No such file or directory". `tests/roms/nes-test-roms/` is
GITIGNORED (.gitignore:160), a local aggregate; only a few of its files
are force-tracked. Both tests passed locally for exactly that reason, and
cargo stopped at the first failing binary, so epoch_fingerprint (which
reads four ROMs from the same place) never ran in CI at all.

- cpu_overclock: ROM -> blargg/apu_mixer/triangle.nes, AUDIBLE_ROM ->
  blargg/apu_mixer/square.nes (cmp: byte-identical to the aggregate copy).
- epoch_fingerprint's panel: the aggregate's 4-scanline_timing and
  flowing_palette -> the tracked blargg/mmc3_test_2/4-scanline_timing.nes
  and assorted/flowing_palette.nes (both cmp-identical; their rows keep
  the same four hashes, only the path column moves, which is the check
  that the swap changed nothing); ny2011 and spritecans have no tracked
  copy and become blargg/apu_mixer/triangle.nes (a continuous tone through
  the mixer) and blargg/sprite_overflow_tests/3.Timing.nes (evaluation
  and overflow timing).

The table was re-blessed with last_release_epoch set to 2 for the bless
only, then restored to 3: a panel change is not a behaviour change, and
the gate cannot tell the two apart, so its refusal to bless at the
released epoch had to be stepped around deliberately. The plain run at 3
passes; cpu_overclock 6/6.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014qfTKi2M3swo7qnwvYCkDj
The v3.1.0 CPU overclock divided the region's CPU cycle length by the
multiplier with integer division: `cpu_div_effective = cpu_div_cached / k`.
That is exact only when the divider is a multiple of k. NTSC's 12 master
clocks divide by 2, 3 and 4; PAL's 16 does not divide by 3 and Dendy's 15
does not divide by 2 or 4. So PAL x3 ran 16/3 = 5-clock cycles, x3.2, and
Dendy x4 ran 15/4 = 3-clock cycles, x5. The stock-step bookkeeping (which
CPU cycles also clock the APU, the DMC and the mapper hooks) was a separate
master-clock debt, so sound and timers stayed right while the CPU ran too
fast: the option said x3 and delivered x3.2. Reported by Copilot on the
release PR (#594, `bus.rs:1424`); the report was correct.

The fix replaces the debt with an exact schedule. The k overclocked cycles
that make up one stock cycle have lengths

    overclock_cycle_len(div, k, phase) = ((phase+1)*div)/k - (phase*div)/k

which sum to exactly `div` over phase 0..k-1 (a telescoping sum), and differ
by at most one master clock: PAL x3 is 5, 5, 6; Dendy x4 is 3, 4, 4, 4. The
last cycle of each group, `phase + 1 == k`, is the stock step, so the stock
step lands exactly on the stock cycle boundary rather than drifting against
it. `overclock_debt` becomes `overclock_phase` (0..k-1) in the BUS section 3
snapshot; restore clamps an out-of-range phase to k-1 and recomputes the
cycle length from it, so a crafted state cannot stall the stock step. x1 is
untouched: phase stays 0 and the length is `div`.

Two details worth keeping:

- The phase advances at the END of a cycle, in `cpu_clock_apu_dmc`, so the
  length a cycle runs with is chosen before its first half and the DMC
  end-of-cycle half still belongs to the stock step its start half ran in.
- `overclock_cycle_len` is a const fn, so `.min()` is unavailable in the
  restore clamp; it is an if-expression for that reason.

Verified:
- New test `the_multiplier_is_exact_on_pal_and_dendy_too` counts CPU cycles
  over 60 frames at x1 and at x2, x3 and x4 on NTSC, PAL and Dendy, and
  requires each ratio within 0.1% of k; the defect it pins was 6.7% (PAL x3)
  and 25% (Dendy x4). Mutant (revert to `div / k`): fails with
  "PAL x3: 6383520 CPU cycles against 1994850 stock is x3.2000, not x3".
- cpu_overclock 7/7, snapshot_schema_audit 9/9, epoch_fingerprint 1/1 (x1 is
  byte-identical; the panel runs at x1), rustynes-core lib 253/253.
- fmt; clippy -D warnings for the workspace and the scripting, hd-pack,
  retroachievements and full feature sets.

The overclock is new in this unreleased version, so the snapshot field's
meaning changes without a further section bump: no released state carries it.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014qfTKi2M3swo7qnwvYCkDj
Copilot's review of #594 found four statements that this release made false
or that its own fold dropped. Each was checked against the tree before it
was changed.

- Settings > Enhancements note (`i18n.rs`, EN and ES): it said enhancement
  modes are NEVER applied during TAS replay or netplay. v3.1.0 made two of
  them (the CPU overclock and the sprite-limit switch) part of
  HardwareOptions, so movies record them and netplay peers must match, while
  the scanline overclock is still ignored there. The note now says accuracy
  tests never use them and points at each option's own line, which already
  states its movie and netplay behaviour.
- accuracy-ledger, Vs. DualSystem row: "wasm/mobile deferred". The mobile
  bridge has carried the cabinet since v2.9.7 (a screen switch, not two
  screens). The row now says so, and that its on-device rows (run sheet
  T8-T10) are NOT RUN with the Swift half uncompiled. A first draft of this
  edit called Android "verified on the emulator" and the web build
  "one screen"; T8 is NOT RUN and nothing was measured about the web build,
  so both claims were dropped before commit.
- mobile run sheet: folding the v1.8.x checklist (638d5d3) kept the `foss`
  flavor check (G17) and lost the requirement that the `play` flavor be
  verified too. G18 restores it: `installPlayDebug`, boot a ROM, open
  Settings; the Play-Services surface (`android/app/src/play/`: Play Games,
  cloud save, Cast, in-app updates) appears, and a device without Play
  Services still boots and plays.
- AccuracyCoin BUILD-PROVENANCE: the v3.1.0 paragraph described the frame-211
  settle of the LEGACY `sprite-eval-misaligned-oam` build, which 3df59e8
  replaced with a build from upstream f5f41dc2 (frame 175, 18/6). The
  paragraph now says the ROM was rebuilt and labels 211 as the replaced
  build's figure. The data rows are unchanged; accuracycoin_subtest_provenance
  passes.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014qfTKi2M3swo7qnwvYCkDj
@context7
context7 Bot temporarily deployed to Docs7 Preview: release/v3.1.0-bellwether October 8, 2026 08:52 Destroyed
… list

Antigravity's review of #594 pointed at `magic == OLDER_SYNC_MAGICS[2]`:
the index means "protocol 6", and that meaning lives only in the order of
the array. Four places relied on the same convention ([0] = protocol 4,
[1] = protocol 5, [2] = protocol 6): the epoch-bearing refusal in
`check_sync`, both legacy-length decode arms in `from_bytes`, and two tests.
Adding a protocol at the front of the list, or sorting it, would have
re-pointed every one of them with no compile error, and the decode arms
would then have accepted a 68-byte Sync under protocol 4's magic.

Each magic is now an associated constant (`PROTOCOL_4_SYNC_MAGIC`,
`PROTOCOL_5_SYNC_MAGIC`, `PROTOCOL_6_SYNC_MAGIC`, each documented with its
version range and payload shape), `OLDER_SYNC_MAGICS` is built from them
and kept for the membership test, and no indexed use remains
(`rg 'OLDER_SYNC_MAGICS\['` finds nothing). The wire values are unchanged,
so this is not a protocol change.

Also from the same review: the Settings combo for the CPU overclock showed
`config.enhancements.cpu_overclock.max(1)`, so a hand-edited config of 9
displayed "x9" while the core, which clamps in `Nes::set_cpu_overclock`, ran
x4. The combo now clamps to 1..=MAX_CPU_OVERCLOCK the way the core does.
The review's broader suggestion, validating the field at deserialization,
was not taken: the frontend clamps at the use site (as `Overscan::clamped`
does), and nothing unclamped reaches a record. HardwareOptions captures
`nes.cpu_overclock()`, already clamped, and the `.rnm` reader refuses 0 or
anything above 4.

The accuracy-ledger Vs. DualSystem row also gains the browser half of
Copilot's finding: since v2.9.7 the wasm-winit build runs the cabinet and
presents both screens, while the wasm-canvas embed runs the main console
only (CHANGELOG [2.9.7]).

Verified: rustynes-netplay lib 102/102 and its integration tests; fmt;
clippy -D warnings on rustynes-netplay and rustynes-frontend.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014qfTKi2M3swo7qnwvYCkDj
@context7
context7 Bot temporarily deployed to Docs7 Preview: release/v3.1.0-bellwether October 8, 2026 08:57 Destroyed
@doublegate

Copy link
Copy Markdown
Owner Author

Antigravity review, answered:

  • OLDER_SYNC_MAGICS[2]: taken, in 5f7eac6. All four indexed uses (the protocol-6 refusal, both legacy-length decode arms, two tests) now use named constants PROTOCOL_{4,5,6}_SYNC_MAGIC. The array is built from them and kept only for the membership test. The wire values are unchanged; netplay 102/102.
  • cpu_overclock as a raw u8: partly taken. Nothing unclamped reaches a record: Nes::set_cpu_overclock clamps to 1..=4, HardwareOptions captures nes.cpu_overclock() (already clamped), and the .rnm reader refuses 0 or anything above 4. The visible gap was the Settings combo, which displayed a hand-edited 9 as "x9" while the core ran x4; it now clamps the way the core does (5f7eac6). The frontend clamps at the use site rather than at deserialization (as Overscan::clamped does), so a deserializer was not added.
  • PR title without a Conventional Commits prefix: kept as is. Release PRs in this repository are titled vX.Y.Z "Codename": ... (v3.0.1 "Mortar": the open items, Rust 1.99 everywhere, the review sweep, the roadmap to v4.0.0 #590, v3.0.0 "Cornerstone": the API major and a release-candidate core #586), and the release automation reads the version and codename from the CHANGELOG header, not from the title.

CodeRabbit skipped this PR (101 files against a limit of 100), so it is reviewing review-only slices #595 (code) and #596 (docs and data), which will be closed unmerged.

…on restore

Two defects CodeRabbit found in the release's code (review slice #595 of
#594). Both reproduced red before the fix.

1. The CPU overclock and the sprite-limit option did nothing on a Vs.
   DualSystem cabinet. `produce_frame` pushes both into the console each
   frame; `produce_dual_frame` pushed neither, and `configure_console`
   deliberately leaves them out, so with a cabinet loaded both settings were
   shown, saved and inert. `produce_dual_frame` now applies them to the main
   AND the sub console. That is coherent for the overclock because the
   cabinet locksteps its consoles by CPU cycle count (`VsDualSystem::run_frame`
   keeps `main.cycle()` within 5 of `sub.cycle()`), so the same multiplier on
   both keeps the gap meaningful; its 150,000-cycle frame guard still covers
   x4 (PAL x4 is about 133,000). The single path's movie-idle condition has no
   counterpart: no movie or netplay session runs on a cabinet (ADR 0032).
   The extra-scanline overclock is still not applied on a cabinet; that
   predates v3.1.0 and is left as it was.

   Test `a_cabinet_runs_both_consoles_with_the_enhancement_options` (emu.rs):
   failed before the fix; mutant dropping the sub console's call fails with
   "sub console overclock".

2. Restoring a state could change the running MMC3 IRQ revision behind the
   setting's back. The MMC3's MAP section carries its live `revision`, and
   `SystemBus::restore` installed it without re-applying the configured
   override, so a state saved under the alternate override and loaded with
   none kept running the alternate chip while `mmc3_revision_override()`
   reported `None`, and the reverse. Movies, rewind, run-ahead and rollback
   all restore through this path. Restore now re-applies the configured
   override after MAP (`None` returns the board to its header's revision; a
   no-op on every other board): the revision is configuration, as the
   override's own docs already said.

   Test `a_restore_keeps_the_configured_mmc3_revision` (mmc3.rs) checks both
   directions; it failed before the fix with "a state saved under the
   override must not bring it back: MMC3 (Nec)". MMC3 suite 21/21.

Verified: rustynes-core lib 253/253, rustynes-frontend lib 684/684, mmc3
21/21; fmt; clippy -D warnings for the workspace, the full,
retroachievements and scripting+hd-pack feature sets and both wasm32
configurations; rustdoc -D warnings.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014qfTKi2M3swo7qnwvYCkDj
…blind spot

From CodeRabbit's review of slice #595 (of #594): two gate defects and four
stale docs.

Epoch fingerprint gate. A probe added to the panel after a release has no
row in the table, and the gate counted every row it could not find as moved
output, which may only happen under a raised EMULATION_EPOCH. So adding a
probe at the released epoch was refused even though the core had not
changed. This is not hypothetical: 5e99b07, which repointed two probes at
tracked ROMs, had to lower `last_release_epoch` by hand to 2 to bless the
table and then restore it. `classify` now separates the three cases by
(rom, frames): an exact row is Unchanged; the same probe with different
output is Changed (the epoch rule applies); no row for the probe is New
(blessable at any epoch, and an unblessed new probe still fails with its own
message). Unit test `a_new_probe_is_not_a_moved_output`; mutant forcing every
unmatched row to Changed fails it with `left: Changed, right: New`.

AccuracyCoin catalog extractor. Its completeness check only recognised a row
macro whose first argument is a quoted string, so a future token-first row
(`tblf3 str_Name, $FF, result_X, ...`) would have been skipped by both the
parser and the check, and every later index in its suite would shift, which
builds sub-test ROMs that run the wrong test. RE_ANY_ROW now keys on the
macro family (`table`, `tbl*`), not on the argument; the family is named
because 6502 mnemonics starting with `t` (`tax`, `tay`, `tsx`, `txa`, `txs`,
`tya`) must not match. New self-test case (9 cases). On the pinned upstream
source (f5f41dc2) the catalog is byte-identical to the committed
SOURCE_CATALOG.tsv and derive_indices validates every recorded mapping,
with output identical to HEAD's script. (The ~/.cache source checkout is the
older 46199ae4, which lacks suite 14 test 6 and fails validation under both
versions; it is not what the catalog is built from.)

Docs corrected against the code:
- `Nes::set_cpu_overclock` still described the integer division 11c96b9
  removed ("x3 on PAL is x3.2"); it now gives the exact split.
- `Nes::set_mmc3_revision_override` said the alternate revision asserts
  only on a 1 -> 0 decrement; since v3.1.0 it also asserts on a `$C001`
  reload to 0, even with the counter already 0 (`clock_irq` path 1).
- snapshot_schema_audit described `cpu_div_effective` as
  `cpu_div_cached / cpu_overclock`.
- `RamResultSummary.total` said 151; it is `scored_len()`, 146.
- `fetch_extra_sprites` now says its raw `oam` walk bypasses the OAM-decay
  read hook on purpose, so the display option cannot refresh a DRAM row.

Verified: epoch_fingerprint 2/2, snapshot_schema_audit 9/9, extract_catalog
self-test 9/9; fmt; clippy -D warnings (workspace and feature sets);
rustdoc -D warnings.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014qfTKi2M3swo7qnwvYCkDj
doublegate added a commit that referenced this pull request Oct 8, 2026
Brings this review-only slice up to #594 at 8af50dc; every path in the
slice equals the release head (git diff --quiet checked).

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014qfTKi2M3swo7qnwvYCkDj
doublegate added a commit that referenced this pull request Oct 8, 2026
Brings this review-only slice up to #594 at 8af50dc; every path in the
slice equals the release head (git diff --quiet checked).

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014qfTKi2M3swo7qnwvYCkDj
@context7
context7 Bot temporarily deployed to Docs7 Preview: release/v3.1.0-bellwether October 8, 2026 10:10 Destroyed
Antigravity's review of #594 (an archived round) reported as BLOCKING that
returning the CPU overclock to x1 hangs the emulator: under the overclock
the APU runs on its own stock-rate counter (`apu_cycle`) while the CPU's
`cycle` runs k times faster, so after a long session they are tens of
millions of cycles apart, and x1 hands the APU `cycle` again. The review
read that as a catch-up loop that stalls the thread and floods the audio
buffer.

It is not one, and this commit shows it rather than arguing it.
`Apu::set_canonical_cycle` is a plain assignment; the APU uses the counter
only for its put/get parity and a pending IRQ-flag-clear deadline, never as
a distance to cover. New test
`switching_back_to_stock_after_a_long_overclock_is_an_ordinary_frame`
runs 600 frames at x4 (about 54 million cycles of gap), switches to x1, and
requires each of the next ten frames to produce the stock CPU-cycle and
audio-sample counts (within 2). It passes.

The mutant is the defect the review described: a loop in `cpu_clock` that
walks the APU through the whole gap at x1. It fails the test with
"frame 0 after switching back: 1319359 audio samples, stock is 733".

What the switch does cost, recorded rather than fixed: the counter's jump
can flip the APU's put/get parity once at the switch (switching ON keeps it,
by re-basing `apu_cycle` on `cycle`). That is a one-cycle phase change at a
moment no real console has, deterministic, and of the same kind as changing
the option at all.

Also from the same review: the `HardwareOptions::cpu_overclock` field doc
now says what happens outside 1..=MAX_CPU_OVERCLOCK (decoding refuses,
applying clamps). Not taken: making the field private (the struct is
`#[non_exhaustive]` with public fields by design since v3.0.0), and the
nitpick that the snapshot's phase error omits the value (it already reads
"CPU-overclock phase {overclock_phase} out of range").

Verified: cpu_overclock 8/8; fmt; clippy -D warnings on rustynes-core and
the harness with test-roms; rustdoc -D warnings on rustynes-core.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014qfTKi2M3swo7qnwvYCkDj
doublegate added a commit that referenced this pull request Oct 8, 2026
Brings this review-only slice up to #594 at c749c43; every path in the
slice equals the release head (git diff --quiet checked).

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014qfTKi2M3swo7qnwvYCkDj
@doublegate

Copy link
Copy Markdown
Owner Author

Antigravity, later rounds, answered:

  • BLOCKING, "switching cpu_overclock back to 1 hangs the emulator" (archived round): refuted, and pinned by a test in c749c43. Apu::set_canonical_cycle assigns the counter; the APU uses it for its put/get parity and a pending IRQ-flag-clear deadline, never as a distance to catch up. switching_back_to_stock_after_a_long_overclock_is_an_ordinary_frame runs 600 frames at x4 (about 54 million cycles of gap), switches to x1, and requires the stock cycle and sample counts on each of the next ten frames. It passes. With the catch-up loop the review described inserted as a mutant, it fails: frame 0 after switching back: 1319359 audio samples, stock is 733. What the switch does cost is a possible one-time flip of the APU's put/get parity, recorded in that commit.
  • dmc_load_write_delayed = false in read() is redundant: not taken. The clear in cpu_clock_apu_dmc runs only when the delayed load ENTERS. If the refused load is not serviceable at the next read, or a $4015 write cancels it, only the clear in read() stops the stale latch from later exempting an unrelated load from its get-half deferral. The comment at that line says so ("or was not serviceable").
  • MAX_EXTRA_SPRITES via u8::try_from: not taken; style only, and the constant is used as a usize array bound elsewhere.
  • HardwareOptions::cpu_overclock clamping undocumented: taken in c749c43. The field doc now says decoding a record refuses an out-of-range value and applying one clamps it. Not made private: the struct is #[non_exhaustive] with public fields by design since v3.0.0.
  • cfg(target_arch = "wasm32") nesting in produce_dual_frame: not taken in a release PR; it mirrors produce_frame's structure, and a refactor there would need its own review.
  • The phase error message should include the value: it already does: "CPU-overclock phase {overclock_phase} out of range" (bus_snapshot.rs).

@context7
context7 Bot temporarily deployed to Docs7 Preview: release/v3.1.0-bellwether October 8, 2026 10:28 Destroyed
CodeRabbit's review of slice #596 (the docs half of #594) found seventeen
statements the release left stale. Each was checked against the tree; sixteen
were real and are fixed here, one was a cross-slice misread (answered on the
PR). The most serious was not prose at all:

- **CHANGELOG [3.1.0] shipped `- The MiSTer core: LADDER-FILL.`**, a drafting
  placeholder, in the release PR. It now gives the ladder result (on-die
  212 / 0 / 1, off-die 213 / 0 / 1, one frozen-tree run each; the core
  matches all 146 AccuracyCoin entries). Nothing caught it but a reviewer,
  so `release_notes_render_audit.rs` gains
  `no_fill_placeholder_reaches_a_release_body`: every
  `.github/release-notes/*.md` and the CHANGELOG must contain no upper-case
  `WORD-FILL` token. Its first run caught the one other placeholder in the
  tree, `BITSTREAM-FILL` in the still-uncommitted v3.1.0 notes, which stays
  until the bitstreams are measured. A scanner unit test pins what counts
  (`LADDER-FILL` yes; `pre-fill`, `Fill-in`, `X-FILLER`, a bare `-FILL` no).

The rest, by file:
- README: the AccuracyCoin badge read 144/144 under the 146/146 headline.
- STATUS, compatibility.md: 144/144 (and 141/141) stated without saying they
  included the masked `Misaligned OAM behavior` failure; now bounded to their
  releases and marked overstated, with 146/146 since v3.1.0.
- ROADMAP: v3.0.1 still "in progress" under a v3.1.0 current-release anchor.
- SECURITY: 3.0.x was "the current line"; 3.1.x is, and 3.0.x is Partial.
- to-dos/README: the release line ended at v3.0.0 as "the current release".
- v3.1.0 plan: row 2 gave `last_release_epoch` 2 without saying the cut set
  it to 3; row 10 said G1-G17 after the review added G18; DOC-06 said
  libretro/docs#1215 open.
- libretro UPSTREAM_SYNC: docs#1215 MERGED 2026-10-08 08:55 UTC
  (`4a0c09f236`, checked with `gh pr view`), not open.
- docs/agents/ci-and-release: named `.rnm` format 5 / protocol 6 as current;
  format 6 / protocol 7 (`"RNE7"`) since v3.1.0, and a probe newly added to
  the epoch panel now blesses at any epoch (8af50dc).
- mappers.md gotcha 2 called the Sharp default "MMC3A" and put MMC3B on
  submapper 1 (that is the MMC6; the alternate revision is submapper 4). It
  now states the rule `clock_irq` implements. The open-questions answer names
  the public owner, `Nes::set_mmc3_revision_override`.
- ppu-2c02.md: "the fourth byte copied is the X position" is false for a walk
  starting at m = 3, where the fourth byte is slot n+1's attribute byte; it
  is evaluated AS X, which is what the rule needs to say.
- mobile run sheet: a device-vs-host frame mismatch is attributed to the
  mobile integration only given the same ROM, initial state and inputs.
- DEFERRED: the CPU-multiplier overclock item still `[ ]` and scheduled for
  v3.1.0; now `[x]` with what shipped. The old "141/141 on the shipping
  core" sentence is marked historical.

Not changed: tests/roms/AccuracyCoin/README.md's claim that the extractor
aborts on an unreadable row-shaped line and on an empty suite is accurate.
The review read a version of `parse()` that slice did not carry;
`extract_catalog.py` compares every `RE_ANY_ROW` candidate against the rows
parsed and rejects a suite that yields none.

Verified: release_notes_render_audit 3/4 here, the fourth failing only on
the uncommitted notes file as intended; markdownlint via pre-commit.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014qfTKi2M3swo7qnwvYCkDj
doublegate added a commit that referenced this pull request Oct 8, 2026
Brings this review-only slice up to #594 at c95a8d1; every path in the
slice equals the release head (git diff --quiet checked).

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014qfTKi2M3swo7qnwvYCkDj
doublegate added a commit that referenced this pull request Oct 8, 2026
Brings this review-only slice up to #594 at c95a8d1; every path in the
slice equals the release head (git diff --quiet checked).

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014qfTKi2M3swo7qnwvYCkDj
@context7
context7 Bot temporarily deployed to Docs7 Preview: release/v3.1.0-bellwether October 8, 2026 10:45 Destroyed
The overclock bookkeeping became a phase in 11c96b9, but three comments
still described a master-clock debt: the BUS decoder's range check
("A debt is below one stock CPU cycle (16 master clocks on PAL ...)"), the
snapshot-tail byte count in a bus test, and docs/scheduler.md. The check
itself was already right (`overclock_phase >= MAX_CPU_OVERCLOCK`); only the
words described the removed model. Found by Antigravity's review of #594.

Comment and doc text only.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014qfTKi2M3swo7qnwvYCkDj
@doublegate

Copy link
Copy Markdown
Owner Author

Antigravity, round at 10:49 UTC:

  • "Blocking": restored is unused in release builds and breaks -D warnings: refuted. debug_assert!(cond) expands to if cfg!(debug_assertions) { assert!(cond) }, and cfg! is a constant expression, not a #[cfg] attribute. So restored is used in every profile and nothing is stripped before type-checking. The evidence: cargo test --workspace --release --features test-roms (3,267 passed at c749c43) and CI's release builds compile this code under -D warnings. Ignoring the Result in release is also deliberate. A debugger peek reloads the board's own just-saved state; a release build must not panic in a UI panel over it, and a debug build asserts it.
  • SemVer, "breaking formats on a minor bump": a maintainer decision already recorded, not a defect. ADR 0045 makes EMULATION_EPOCH, not the version number, the marker for movie and netplay compatibility. Any release that changes a frame, a sample or a bus cycle raises it and is refused across it with a reason. v3.1.0's two AccuracyCoin fixes do exactly that. The Rust API is not broken: v3.1.0 adds fields to #[non_exhaustive] structs, which is what v3.0.0 made them non-exhaustive for (ADR 0043).
  • The "debt" comment in bus_snapshot.rs: correct, a leftover from the model 11c96b9 replaced. Fixed, along with two more mentions (a bus test's byte count, docs/scheduler.md); it will be in the next push.

doublegate and others added 2 commits October 8, 2026 07:27
The release body, written from the branch log of both repositories. It
states the breaking changes (EMULATION_EPOCH 3, BUS section 3,
PPU_SNAPSHOT_VERSION 13, .rnm format 6, netplay protocol 7), that every
AccuracyCoin 100% before v3.1.0 included the masked Misaligned OAM
behavior failure, and the release-candidate bitstream pair: seed 1 from
eight at 261008, on-die 4afffd23 (+0.255 / +0.100 ns) and off-die
7b48198c (+0.188 / +0.001 ns; SDRAM read +0.449 / +1.184 ns), each
byte-identical across two clean compiles, neither run on hardware. The
test count is set after the final run at the release head.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014qfTKi2M3swo7qnwvYCkDj
`cargo test --workspace --release --no-fail-fast --features test-roms` at
17823b6: 3,269 passed, 0 failed, 11 ignored. The 3,263 these files gave
was measured at 5f7eac6. Since then: four tests from the review rounds
(the cabinet options, the MMC3 revision across a restore, the epoch
classifier, switching the overclock back to x1) made 3,267 at c749c43,
and the two placeholder-gate tests make 3,269.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014qfTKi2M3swo7qnwvYCkDj
doublegate added a commit that referenced this pull request Oct 8, 2026
Brings this review-only slice up to #594 at 7274754; every path in the
slice equals the release head (git diff --quiet checked).

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014qfTKi2M3swo7qnwvYCkDj
doublegate added a commit that referenced this pull request Oct 8, 2026
Brings this review-only slice up to #594 at 7274754; every path in the
slice equals the release head (git diff --quiet checked).

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014qfTKi2M3swo7qnwvYCkDj
@context7
context7 Bot temporarily deployed to Docs7 Preview: release/v3.1.0-bellwether October 8, 2026 11:52 Destroyed
@doublegate

Copy link
Copy Markdown
Owner Author

Antigravity, round at 11:56 UTC:

  • let _ = dual.rewind_step_back() in produce_dual_frame: kept, deliberately, and said so at the site ("A failed step (empty ring, rewind off) leaves the cabinet where it is; the screens are re-presented either way"). The Err is the ordinary state of a player holding rewind past the start of the ring, so it is not a failure. Logging it would emit one line per frame for as long as the button is held. The single-console path handles its rewind step the same way.
  • SemVer: answered in the previous round. ADR 0045 makes EMULATION_EPOCH, not the version number, the compatibility marker for movies and netplay, and the Rust API is not broken (fields added to #[non_exhaustive] structs).

@doublegate
doublegate merged commit 0caf45d into main Oct 8, 2026
42 checks passed
@doublegate
doublegate deleted the release/v3.1.0-bellwether branch October 8, 2026 12:22

This branch was successfully deployed

No deployments
Docs7 Preview: release/v3.1.0-bellwether — 72747541 Deployed Oct 8, 2026 by context7[bot]
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants