v2.9.8 "Vanguard" -- v3.0.0's breaks landed early, every staged game looked at, and the database's corrections on every platform - #579
Conversation
The plan for v2.9.8 "Cadence" (to-dos/plans/v2.9.8-cadence-plan.md) and
the first item it closes. The maintainer decisions (2026-09-30):
- codename Cadence;
- S2 stays after v3.0.0;
- all three MiSTer menu options;
- libnx dropped with an explicit error.
libnx: the crate Makefile mapped `platform=libnx` to
aarch64-nintendo-switch-freestanding. That is a tier-3 target with no std,
while this wrapper needs std::fs (FDS BIOS, disk saves) and catch_unwind
(the panic-containment design). rustup ships no rust-std for it, and the
libretro buildbot has no Rust Switch template and never reads the Makefile
table, so nothing shipped from it. It is now a `$(error ...)` rather than a
deleted branch: deleting it would let `platform=libnx` fall through to the
native branch and emit a HOST library under the Switch's name. The same
branch's `EXT := a` routing is removed. Make needs the error line indented
with spaces; a tab made it a recipe line ("recipe commences before first
target").
Pinned by `platform_libnx_is_refused_not_built_for_the_host` in
libretro_makefile_audit: the make dry run must fail with the message.
Removing the error line fails it (CAUGHT), because the dry run then
succeeds as a host build.
Records:
- The line plan's S2 row carried v2.6.16 figures ("24/24" PRG, CHR "23
against a budget of 22"). Since v2.9.1 put CHR in its own bank, the worst
cases are CHR 18/22 and PRG 22/24, and the CHR gate's threshold was
always 31. The row is corrected and moved after v3.0.0.
- VERSION-PLAN's v2.9.8 row is updated.
- docs/libretro/architecture.md item 5 and the L-3.2 ledger row record the
drop.
- CHANGELOG [Unreleased] carries a Removed entry.
The second palette is 2C03 RGB, not palette_gen.rs: that generator's
Provenance header discloses a derived region, which ADR 0037 makes a black
box for HDL, and the plan records why.
Verified: all 12 audit tests pass; libretro_makefile_audit 6/6; fmt and
clippy; markdownlint on every changed document; `make -n platform=unix` and
`platform=windows ARCH=x86_64` recipes unchanged.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014qfTKi2M3swo7qnwvYCkDj
…ODEL)
Found because the MiSTer core's first palette-gate (v2.9.8) compared the
two projects' colours and found 374 of 1,024 emphasised entries different.
Neither side followed the hardware:
- this emulator dimmed the other two RGB channels to 13/16 per set bit,
compounding, so three bits gave an even 0.66 dim of everything;
- the core dimmed each channel once to 3/4.
NESdev's NTSC_video page documents one attenuator, shared by the three
bits, armed on the phases of colours $C, $4 and $8 ("6 out of 12 half-clocks
if one bit is set, 10 ... if two, all 12 if all three"), and never on
columns $E/$F. The maintainer asked for a better, documented model shared by
both before v3.0.0, in v2.9.8 since v2.9.9 is the release candidate.
emphasis.rs is the model, written from the page's prose and data:
- lidnariq's terminated plain and attenuated levels;
- the "Color Phases" diagram as (p + n) mod 12 in 1..=6, checked row by row
against the transcribed diagram;
- the attenuator phases;
- a twelve-sample decode with the burst on -U;
- the page's YUV-to-RGB matrix.
It is not from palette_gen.rs, a disclosed derived region, and not from
the page's illustrative C++, which is CC BY-SA example code. Only the CHANGE
is used: EMPHASIS_DELTA[(emph << 6) | colour] is the model's emphasised RGB
minus its plain RGB, added to the FBX base (or a loaded .pal) and clamped.
A difference, not a ratio, because the model's darkest colours decode at or
below black, where a ratio means nothing. With no emphasis every delta is
0, so un-emphasised frames are byte-identical.
The table is committed data (emphasis_delta_table.rs, generated by an
ignored test) because build_rgba_lut is a const fn and the decode needs
sin/cos. `the_committed_table_is_the_documented_model` recomputes all 512
entries with libm, so the two cannot drift. The model functions are
test-only in a non-test build, under a scoped dead_code allow saying so.
apply_emphasis (13/16) is deleted rather than deprecated: `palette` is a
private module and the function was never re-exported, so it was not public
API.
Tests: documented properties, each a unit test:
- the phase rule equals the diagram;
- $x6/$xA/$x2 decode red/green/blue;
- $xE/$xF and emphasis 0 are untouched;
- three bits darken with channels within 1 of each other;
- one bit tints toward its own channel.
Mutations, all CAUGHT, each by at least two tests:
- no $E/$F exemption;
- bit 5 on colour 4's phases;
- the burst on +U;
- attenuated levels equal to plain.
Goldens re-blessed, 13, each attributed:
- test-roms: visual_regression full_palette 60/180 and flowing_palette
60/180/300, m22 vrc2a chr banking, and the unreferenced-corpus pin for
ppu_palette.nes. A scratch probe counted emphasised pixels in the index
framebuffer at each pinned frame: 48,896, 49,664, 61,440 and 16,384 of
61,440.
- commercial (local, snapshots only): extended m112 / m221 / Felix the Cat,
and coverage Felix / Cobra Triangle / m112. In every one, only
fb_fnv1a64 changed; cycles and the audio hash are identical, so the
emulation is untouched and only emphasised colours moved.
- real_games 60/0 unchanged.
Verified:
- test-roms 3,067 passed before re-blessing, with the 7 failures all
attributed above (AccuracyCoin and nestest among the passes);
- fmt; workspace clippy -D warnings; rustdoc -D warnings;
- the no_std thumbv7em build.
Docs: ppu-2c02.md "Greyscale + emphasis" describes the model; ROADMAP gains
T-EMPHASIS-MODEL; CHANGELOG [Unreleased] Changed.
Raised, not changed: docs/ppu-2c02.md calls raw_signal.rs "the canonical
Bisqwit nes_ntsc / Mesen2 'raw palette' generator", but that file has no
Provenance header or record row. It was not read, and this model does not
use it.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014qfTKi2M3swo7qnwvYCkDj
docs/ppu-2c02.md has described crates/rustynes-ppu/src/raw_signal.rs since v2.1.9 as "the canonical Bisqwit nes_ntsc / Mesen2 'raw palette' generator", yet the file carried no `// Provenance:` header and no row in docs/originality-and-provenance.md section 1, so provenance_record_audit, which keys on headers, could not see it. Found while the v2.9.8 emphasis model was being written from NESdev (that model does not use this module, and the module was not read). The maintainer chose to attribute it, as for mapper 250: header, section 1 row, NOTICE. The description in the PPU spec is the evidence and is unchanged; nothing is judged about how much was taken. Verified: provenance_record_audit 2/2; raw_signal unit tests 6/6; markdownlint. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014qfTKi2M3swo7qnwvYCkDj
…em 4) v2.9.3 measured frame pacing in one configuration (Mailbox, 120 Hz, run-ahead 0) and named the three it left out. This measures them: eight 45 s captures of the 29fc5a39 release frontend on flowing_palette.nes, two per configuration, every one passing perf_log_check with present_discarded=0. - 60 Hz (display switched to 59.97 Hz with kscreen-doctor at the maintainer's approval, then restored): one present per vblank, p99 18.6-18.8 ms; Mailbox and Fifo are indistinguishable. - 120 Hz Fifo: p95 9.3-9.6 ms, as even as v2.9.3's Mailbox (10.4-11.4), so the present mode is not what evens pacing on this host. - Run-ahead 1: cost p95 doubles (3.1 -> 6.2 ms) and the worst produce interval grows 3-5 ms. Its present tail is NOT established: the two runs disagree (p95 11.92 vs 8.88 ms), and the record says so. Recorded as an observation, not investigated: at 59.97 Hz the frontend produced 59.9 fps, about 0.3% under NTSC, while every header reports pacing_active = wallclock (checked in all eight). Conditions stated with the numbers: one host, one ROM, KDE Wayland, and the off-die co-simulation ladder holding one of 20 threads (load 1.6-2.0, under v2.9.3's threshold of 2). The user config was backed up and restored byte for byte (md5 checked). Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014qfTKi2M3swo7qnwvYCkDj
agy's v2.9.7 round-3 nitpick on #577: `insertCoin()` passed a bare `2u` for the sub console. The bridge's `insert_coin` numbers acceptors 0/1 for a cabinet's main (left) console and 2/3 for the sub (right) one, and a single Vs. console has 0 and 1, so the button uses the first of each pair. That is now MAIN_CONSOLE_COIN / SUB_CONSOLE_COIN in a private companion, with the mapping in its doc comment. Behaviour is unchanged. The other two round-3 suggestions are declined in the v2.9.8 plan with reasons: `let _ = write!` is the workspace idiom (68 uses), and the battery-refusal reason already reaches the player through emu.rs. Verified: :app:testFossDebugUnitTest BUILD SUCCESSFUL. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014qfTKi2M3swo7qnwvYCkDj
…fix categorize
The maintainer noticed the v2.9.6/v2.9.7 mapper work had no screenshots.
Three separate causes, each fixed or stated here.
1. The v2.9.7 real-game fixes got .snap hashes but no PNGs. Captured with
the documented pipeline (scripts/coverage/README.md): external_coverage
with RUSTYNES_DUMP_FRAMES=1, narrowed by RUSTYNES_COVERAGE_FILTER to 15
ROMs. All six tests pass, so every hash matched; each PNG is the
harness's `final` frame, the frame its .snap pins. Added:
- six MC-ACC titles (mapper-004-3-MC-ACC);
- StarTropics II (mapper-004-1-MMC6);
- the DKC4 world map (mapper-211-JYCompany211);
- the mapper-221 1000-in-1;
- Time Diver Avenger (Unl).
Re-captured, because the fixes or the v2.9.8 emphasis model changed them:
- Time Diver - Avenger (Asia);
- Felix the Cat;
- Cobra Triangle;
- mapper 112's Chik Bik Ji Jin, updated in place under its existing
legacy name rather than duplicated.
Names follow CURATION-MANIFEST.md (standardized, GoodNES flags dropped).
Reviewed on a contact sheet: status bars and menus intact, the DKC4 map
whole, Time Diver's playfield correct.
2. The tier trees were stale. `coverage.py categorize` (ADR 0011: the
screenshot tree follows the mapper's tier) had not been re-run since
about 90 families were promoted to Curated, so they sat under
besteffort/. Re-run: 159 dirs under external/, besteffort/ empty but
for its README. That is accurate: none of today's 31 BestEffort families
has a committed capture.
3. Two defects in categorize itself, found running it:
- Its tier.rs parser read only the FIRST match arm after each `// Tier:`
marker. v2.9.6 added a second Curated arm, so GTROM (111) and the new
families read as "unclassified". `_parse_tier_arms` now reads every
`ids => Some(MapperTier::X)` arm with line comments stripped (their
prose holds numbers like 268/286); it returns 51 / 109 / 31, matching
tier.rs. The embedded fallback, which still listed 111 as BestEffort,
is regenerated from it.
- Merging a directory present in both trees moved file over file. That
replaced six 2026-08-16 external/ captures (Wampus, five mapper-150
Sachen titles) with 2026-06 duplicates from besteffort/. They are
restored from HEAD, and categorize now keeps both copies of such a
file and flags the pair. Checked on a scratch tree: the newer copy
kept, the unique file moved, the duplicate flagged.
Not done, and why: the 17 v2.9.6 families still have no screenshots,
because no dump of them exists on this machine (T-MAPPER-DUMPS, open).
screenshots/README.md now states the count (448 PNGs / 159 dirs), that
most PNGs predate the current core (the .snap hashes are the baseline),
and the current regeneration route. besteffort/README.md says why the
directory is empty.
Verified: ruff check passes; markdownlint passes; external_coverage
6/6 on the 15 filtered ROMs.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014qfTKi2M3swo7qnwvYCkDj
… would collide Staging the v2.9.6 families' real dumps (T-MAPPER-DUMPS, now possible with the library reachable again) hit two gaps in coverage.py. FAMILY had no board names for the 17 families, so `stage` would have made `mapper-012-m12/`-style directories, and those names become the screenshot folders and the external_coverage snapshot ids. Added from the board column of docs/mappers.md (GouderSL5020B, SMB-Tetris-NWC, GA23C, SpikeVBall-NWC, Waixing43-393, Cony-Yoko, JYCompany91 [matching 90/211], NES-EVENT, KashengA9711, BandaiLZ93D50, NanjingFC001, Waixing191, WaixingFS308, Waixing194, WaixingFS303, Action52, WaixingT9552). The diff is 17 additions and nothing else. _base_title, which keeps staged titles distinct, kept punctuation, so "Fatal Fury 2" and "Fatal Fury 2'" (a different dump) were both staged and then shared the snapshot id mapper_083_Cony_Yoko_Fatal_Fury_2_Unl, which the harness correctly refused (two different files claiming one baseline). It now collapses non-alphanumeric runs the way the snapshot id does; both names normalise to fatal_fury_2. Verified: ruff check passes; discover now offers one Fatal Fury 2. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014qfTKi2M3swo7qnwvYCkDj
NESdev INES_Mapper_153: "PPU $0000-$1FFF: 8 KiB unbanked CHR-RAM" and
"No CHR banking is available". The LZ93D50's PA12/PA13 inputs are grounded
on this board, so its $8000-$8003 registers drive the outer 256 KiB PRG
bank, and v2.9.6 correctly swallowed those writes (and $8004-$8007) instead
of storing them as CHR banks. But `chr_offset` still routed every pattern
fetch and CHR-RAM write through `chr_banks[slot]`, which on this board is
therefore all-zero forever: all eight 1 KiB windows resolved to bank 0,
the first 1 KiB of the 8 KiB RAM. Famicom Jump II's pattern tables
overwrote one another and its title screen rendered as vertical stripes.
`chr_offset` now returns `addr & $1FFF` for FcgVariant::Lz93d50Wram. The
other variants (mapper 16 submappers 0/4/5, mapper 159) keep the banked
path unchanged. The change is outside the file's Mesen2-derived EEPROM
region (the Provenance header is untouched).
Red first: m153_chr_ram_is_8k_with_no_aliasing_between_1k_windows writes
a distinct byte into each 1 KiB window (after writing $8000-$8007, which
must not reach CHR) and reads them back; it failed on the v2.9.6 code
("1 KiB window 0 aliased another window"), passes with the fix, and fails
again with the fix disabled (mutation caught). The existing
m153_chr_is_unbanked_ram test only touched address $0005, which aliasing
cannot break.
What this does NOT change: the dump still boots to a blank frame from a
fresh cartridge. Traced with debug-hooks access logging: with zero-filled
WRAM the game reads zeros from $6Bxx, bank-switches to bank 0 and returns
to $83F3, which is data; it BRKs into its halt loop at $C6F2. The NESdev
page documents exactly this ("When booting with WRAM filled with zeroes,
Famicom Jump II will freeze with a black screen. Simply soft-resetting
the console will then always run the game properly"). With WRAM pre-filled
$FF or pseudo-random, or after a soft reset, the title screen renders
correctly. Cartridge WRAM is zero at power-on core-wide by design; changing
that is a policy decision, not a board fix.
docs/mappers.md's 153 row and CHANGELOG [Unreleased] record both.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014qfTKi2M3swo7qnwvYCkDj
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014qfTKi2M3swo7qnwvYCkDj
The MMC3-based boards in mmc3_boards.rs resolved every PRG and CHR-ROM bank with `bank % banks`. That is correct only for power-of-two images. The MMC3 drives its fixed last PRG bank as all ones ($FF); on a 24-bank (192 KiB) image $FF % 24 = 15, and on a 20-bank (160 KiB) image $FF % 20 = 15, so $E000 mapped a bank with no reset vector (bank 15 of the Downtown translation holds RESET=$E000 over zero fill, bank 23 holds the real $FF85). The 192 KiB and 160 KiB mapper 191 translations (Downtown Nekketsu Monogatari, Downtown Special, Mighty Final Fight) therefore booted to a one-colour frame. Banks now go through `mirror_bank`, the inverse of the doubling algorithm in nesdev_wiki Non_power_of_two_ROM_size: the image is grown to the next power of two by repeatedly copying its last lowbit(size) banks, so 192 KiB reads as ABCC and 160 KiB grows 20 -> 24 -> 32. The function reduces modulo the grown size, then steps a bank lying in a copied region back onto its source; it allocates nothing and loops at most once per set bit of the bank count, so it is safe on the per-access path of the no_std chip stack. For power-of-two images it is exactly `bank % count`, so every power-of-two dump is unchanged. CHR-ROM (and whole-image CHR-RAM) offsets use the same mapping via `chr_rom_offset`, including `chr_phys`. Red first: `non_power_of_two_prg_mirrors_by_the_doubling_algorithm` (24- and 20-bank images: fixed banks land on 23/22 and 19/18, R6/R7 past the image fold per the page) and `non_power_of_two_chr_mirrors_by_the_doubling_algorithm` (384 KiB CHR on mapper 12) failed on the old code (15 vs 23; 0 vs 256) and pass now. Restoring the modulo makes both fail again (mutation caught). docs/mappers.md records the rule and three dumps from the same survey that are not the board they are labelled as (Q Boy is Sachen 8259 / mapper 141; Chaos World and San Guo Zhi 2 are Waixing FS005 / mapper 176 submapper 2; the Kasheng MK6 + Samurai Spirits 2-in-1 is mapper 291), with the register-write evidence, instead of forcing them. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014qfTKi2M3swo7qnwvYCkDj
Nintendo World Championships 1990 (mapper 105, NES-EVENT) booted to a one-colour frame forever. The mapper was not at fault. The cart's reset code at $FF00 waits two vblanks (~57,190 CPU cycles, so the power-on 4-step frame counter has already set the frame IRQ flag at 29,828), then runs STA $4017 (#$40), CLI, LDA #$FF. Its IRQ/BRK vector is $6010, in WRAM the game has not filled yet. The frame counter deferred the inhibit clear to the 3-4 cycle timer reset, so /IRQ was still high when LDA #imm polled three cycles after the write; the CPU vectored to $6010, read $00 (BRK), and re-entered the same vector for the rest of the run. Traced with step_instruction: pushed P = $A0 (B clear, a hardware IRQ), return address $FF1E, $4015 = $44 until cycle 57,196. The NESdev "APU Frame Counter" page states the two effects separately: bit 6 "If set, the frame interrupt flag is cleared", while only "the timer is reset" after 3 or 4 CPU cycles. FrameCounter::write now clears irq_flag, irq_line_active and any pending lazy $4015-read clear on the write itself; the delayed reset path still clears again, harmlessly. Whether a flag set inside the 29,828-29,830 window during a pending reset should also be suppressed is not settled by any source and is left unchanged (the test writes after the window, as the game does). Red first: write_4017_inhibit_drops_the_irq_on_the_write_cycle failed on the old code, passes with the fix, and fails again with the fix reverted (mutation caught). The game now renders its "Welcome to Nintendo World Championships 1990" title screen at 300 and 1,100 frames. No mapper code changed; the m105 row in docs/mappers.md is unaffected. Gates: cargo test --release --workspace --features test-roms --no-fail-fast 3,069 passed / 0 failed / 20 ignored, including AccuracyCoin, blargg apu 2005, apu_test, apu_reset, PAL APU and the IRQ trace fixture; cargo test -p rustynes-apu / -p rustynes-mappers; clippy -D warnings on both crates; cargo fmt --check. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014qfTKi2M3swo7qnwvYCkDj
T-MAPPER-DUMPS. v2.9.6 wrote 17 mapper families from their NESdev pages and shipped without a library dump for any of them. The dumps were staged from the maintainer's GoodNES set, booted through `external_coverage`, and each frame was looked at before its baseline was accepted. This commits 44 `.snap` baselines and the matching `final` frames as PNGs, the frame each `.snap` pins. What was NOT committed, and why: - Four dumps carry the wrong mapper in their header, recorded in docs/mappers.md by d0e89e3d rather than forced: Q Boy (mapper 141, not 191), Chaos World and San Guo Zhi 2 (176 submapper 2, not 74), and the Kasheng Super 2-in-1 (291, not 47). Their staged copies were moved out of the coverage tree to the gitignored salvaged/mislabelled-dumps/ (the originals remain in the GoodNES set), and their baselines were deleted. Each had booted blank, and a baseline of a blank frame on the wrong board pins nothing. - Famicom Jump II (153) still boots blank: with zero-filled WRAM the game waits, as the NESdev page documents, and runs after a soft reset. Whether the harness should model a battery fill or press reset is the maintainer's decision. - Famicom Yarou 54 (45) shows a blue screen: GA23C's register-2 power-on value is undocumented. Also the maintainer's decision. Four baselines were captured AFTER this branch's fixes, and the blank baselines accepted earlier in the session were replaced: Nintendo World Championships 1990 (the $4017 fix, e092c1aa) reaches its title, and the three mapper 191 Chinese translations (the non-power-of-two mirroring fix, d0e89e3d) reach gameplay. Each was viewed before re-blessing. `coverage.py categorize` put mappers 47, 121 and 191 in screenshots/besteffort/: v2.9.6 tiered them BestEffort, so that README's "this directory is empty" note was wrong and is corrected, as are the corpus counts (477 PNGs / 170 dirs in external/, 10 in besteffort/). Three committed PNGs were stale, recorded when the core rendered them as garbage, and are replaced with today's frames: Uchuu Keibitai SDF (MMC5), Mappy Kids (N163, fixed by v2.7.2's nametable hooks) and Cybernoid (Sunsoft-3). Their `.snap` baselines already matched the current core, so only the pictures were wrong. Pin Bot (Europe) is NOT among them: the current frame of that exact dump is still garbled, and is being investigated as a live defect. The screenshot READMEs named the harness crate `nes-test-harness`, renamed long ago; they now say `rustynes-test-harness`. Verification: a full external_coverage run with frame dumps over all 748 staged ROMs, a contact-sheet review of all 740 final frames, and the f900 / f1100 / final frames for every suspicious one. After the moves above, the targeted re-run of mappers 105 and 191 passes. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014qfTKi2M3swo7qnwvYCkDj
The two v2.9.6-family dumps left out of 4d2e29a1, settled by the maintainer
on 2026-10-01.
Famicom Jump II (mapper 153) joins KNOWN_BLANK, with a comment that sets it
apart from the other entries. It is not a defect: on zero-filled battery
WRAM the game waits for a reset (NESdev "INES Mapper 153"), and a soft reset
runs it. The harness powers every ROM on with zeroed cartridge RAM, so a
blank frame is the correct result. The rejected alternatives were a
harness-side soft reset for the board, and a non-zero WRAM power-on fill in
the core, which would change every battery board on no hardware evidence.
Famicom Yarou 54 (mapper 45) is ticketed as T-GA23C-POWERON, with a note in
docs/mappers.md's mapper 45 row. The maintainer chose "ticket and
KNOWN_BLANK", but the second half could not be done as asked: the blue frame
has enough colours to pass `frame_health`'s blank test, so a KNOWN_BLANK
entry would trip that list's own ratchet ("in KNOWN_BLANK but no longer
boots blank"). Its baseline therefore pins the blue frame, and the ticket and
the mappers.md row say so. The vendored NESdev page was re-read for this
commit: it says $6001 and a soft reset clear the outer registers, and gives
no value for them. The "register 2" detail in the ticket comes from an
earlier investigation and is labelled as such.
T-MAPPER-DUMPS in to-dos/ROADMAP.md now records the v2.9.8 outcome: 44
dumps pinned, three defects fixed red first, four mislabelled headers
recorded, and mappers 194 and 195 still without a dump.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014qfTKi2M3swo7qnwvYCkDj
Old ROM-management tools wrote signatures into iNES 1.0 header bytes 7-15, which the original iNES emulator ignored. The best known is "DiskDude!": byte 7 becomes 'D' = $44, whose high nibble reads as mapper bits 4-7 and adds 64 to the mapper number. `parse_header` trusted byte 7's high nibble unconditionally on the iNES 1.0 path, so any such image loaded on the wrong board. The NESdev "iNES" page gives the rule: "if the last 4 bytes are not all zero, and the header is not marked for NES 2.0 format, an emulator should either mask off the upper 4 bits of the mapper number or simply refuse to load the ROM." `parse_header` now masks. The mapper assembly moves into a `mapper_number` helper (parse_header crossed clippy's 100-line limit) that drops byte 7's high nibble when the header is not NES 2.0 and any of bytes 12-15 is non-zero. A clean iNES 1.0 header has zeros there, so a well-formed dump of mapper 16-255 is unaffected; NES 2.0 headers are exempt because bytes 12-15 carry real fields (region, Vs. type, misc ROMs, expansion device). Garbage confined to bytes 8-11 does not trigger it, matching the page's wording. Root cause of the v2.9.8 triage item: "Balloon Fight (J) [T+Rus_Mario Soft]" is a 16 KiB / 8 KiB NROM image whose tail reads "@iskDude!" (byte 7 = $40), so it loaded as mapper 64 (RAMBO-1) and the sky was filled with a repeated, wrongly banked tile. It now loads as NROM and plays with a black sky and clouds. Corpus sweep (every iNES 1.0 image under tests/roms with a non-zero byte 12-15): - Balloon Fight [T+Rus] 64 -> 0: fixed. - Asmik-kun Land [t1] and the Kyatto Ninden Teyandee hack, 244 -> 4 (MMC3): were in KNOWN_BLANK, now reach their title / story screens, so they leave the ratchet list. - Doraemon World 3 (Kiku hack), .nes and .zip, 72 -> 8 (FFE, not implemented): was blank as mapper 72, is now refused with UnsupportedMapper(8); moved from KNOWN_BLANK to UNSUPPORTED, its two coverage snapshots and its external_extended test + snapshot removed. - Adventures in the Magic Kingdom, Tom Sawyer, Barbie (65 -> 1), Cybernoid, Karate Kid (67 -> 3), Matendouji (244 -> 4): the per-game database already rewrites their mapper to a value below 16 before the core parses, so their snapshots are unchanged (re-run, no diff). - Fire Emblem (mapper 10) and flowing_palette (mapper 0) have a zero byte-7 high nibble: unchanged. Caveat recorded in docs/cartridge-format.md: the database rewrites bytes 6-7 but not the dirty tail, so a database mapper of 16 or more on such a dump would be masked too; none in the local corpus is. Verification: - red first: header::tests::ines1_garbage_tail_masks_the_mapper_high_nibble failed (left 64, right 0) before the fix, passes after. - mutations: disabling the mask -> FAILED (64 vs 0); dropping the !is_nes2 guard -> FAILED on the NES 2.0 case (0 vs 64). - cargo test -p rustynes-mappers: 929 + 6 passed. - external_coverage (filtered to every affected dump) and external_extended extended_m244: snapshots changed only for Balloon Fight [T+Rus], Asmik-kun Land [t1] (coverage + extended) and the Kyatto hack, each viewed after the change. - cargo fmt --check, cargo clippy --workspace --all-targets -D warnings. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014qfTKi2M3swo7qnwvYCkDj
The NESdev "RAMBO-1" page describes one clock of the IRQ counter as a
reload-or-decrement step followed by a single test: "If IRQ counter is
now 0 AND IRQs are enabled: trigger IRQ". `Rambo1::clock_irq` placed the
test inside the decrement branch only, so a reload that produced 0 --
either the reload forced by a `$C001` write or the reload of a counter
already at 0 -- never asserted. With a latch of 0 the documented counter
reloads 0 and raises the IRQ on every clock; ours stayed silent forever.
Skull & Crossbones (USA) (Unl) depends on exactly that. A register trace
of its first 400 frames (temporary instrumentation, not committed) shows
`$C000=00`, `$C001=00` (scanline mode), `$E001` written once per vblank,
393 times, and only two `$E000` writes: the game arms a zero-latch
scanline IRQ every frame and waits on its handler. The counter sat at 0
on every A12 clock and no IRQ ever fired, so the attract sequence showed
a black screen with a fragment of the "HINTS" page at f1100. With the
test moved after both branches the game runs its Tengen logo, its
title screen and the full hints page.
`clock_irq` now performs the reload or decrement and then returns
`counter == 0 && irq_enabled`. A non-zero latch behaves exactly as
before: a reload to a non-zero value cannot satisfy the test, and the
decrement-to-zero case is unchanged. The reload "+1 kick" (the page's
"ORed with 1") and the IRQ delay remain unmodelled, as the module
documentation already said; this change does not touch them.
Verification:
- red first: m064_rambo1::tests::zero_latch_reload_asserts_on_every_clock
failed ("reload to 0 asserts") on the old clock_irq and passes now;
that red run on the unmodified function is the mutation check.
- cargo test -p rustynes-mappers --lib m064: 10 passed (the existing
scanline / cpu-cycle / ack tests unchanged).
- external_coverage filtered to mapper-064 (6 dumps): only the Skull &
Crossbones snapshot changed; Hard Drivin', Road Runner, Shinobi,
Xybots and Balloon Fight [T+Rus] (now NROM) are byte-identical.
- cargo fmt --check; cargo clippy -p rustynes-mappers --all-targets.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014qfTKi2M3swo7qnwvYCkDj
NESdev "INES Mapper 019" documents $5800-$5FFF (read/write) as
`EHHH HHHH`: bit 7 is the IRQ enable (0: disabled, 1: enabled) and bits
6-0 are the high bits of the 15-bit up-counter. `Namco163` keeps the
enable in bit 15 of `irq_counter`, but its $5800 write handler masked
the value to 7 bits and then OR-ed in $8000 unconditionally, so every
write enabled the counter. The read handler masked bit 7 off, so the
enable never read back either.
Mechanism of the visible defect, in Digital Devil Story: Megami Tensei
II (Japan) (Rev A): the game drives raster bands from its IRQ handler
at $FE37 (bank 31). Each IRQ does INC $0348, and while $0348 < 10 loads
the next band's counter from a table into $5800/$5000 and the band's
four background CHR banks from $0349+X into $A000-$B800. After the last
band it takes the other branch, which writes $5800 = $00 and $5000 = $00
to stop the counter and restores $0349-$034C. With the enable forced
on, that "stop" restarted the counter from 0, so a stray IRQ arrived
every 32,768 CPU cycles (about 1.1 frames), incremented $0348 past the
band range for good and rewrote $A000-$B800 at an arbitrary scanline.
PPUCTRL was $90 (background at $1000), so the background was drawn
from whatever 1 KiB pages that stray write left, mostly page 0: the
repeated tile pattern behind the title logo and the intro text. Traced
with a temporary per-write log in the mapper and a CPU instruction
trace through `Nes::step_instruction` (neither committed).
The write now stores the whole byte in the high half, so bit 7 sets or
clears the enable; the read returns the high half including the enable.
The counter itself (counts while enabled, fires and stops at $7FFF,
$5000/$5800 writes acknowledge) is unchanged. The save-state layout is
unchanged: `irq_counter` was already a u16 carrying bit 15.
Verification:
- red first: m019_namco163::tests::namco163_5800_bit7_is_the_irq_enable
failed ("a disabled counter never fires") before the fix, passes after.
- mutations: restoring the forced `| 0x8000` on write -> FAILED ("a
disabled counter never fires"); restoring the `& 0x7F` on read ->
FAILED ("bit 7 reads back the enable").
- cargo test -p rustynes-mappers --lib m019: 27 passed.
- external_coverage filtered to mapper-019 (6 dumps): only the Megami
Tensei II snapshot changed; the title, the intro city skyline and the
"CAUTION FOR DEVIL BUSTERS" text screen now render cleanly. Battle
Fleet, both Famista, Final Lap and Mappy Kids are byte-identical, and
external_real_games' four Namco 163 tests pass unchanged.
- cargo fmt --check; cargo clippy -p rustynes-mappers --all-targets.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014qfTKi2M3swo7qnwvYCkDj
Root cause of two assigned live-render defects (Youkai Club, mapper 140, solid blue screen; the Sachen Lightgun Game 2 in 1, mapper 150, scattered garbage) and of eight more in the staged corpus: the boards were right, the dumps were right, and the load path replaced the right mapper with a wrong one before the core ever saw the header. Mechanism. The frontend (`App::apply_game_db_header_overrides`) and the coverage harness (`common::load_nes`, which mirrors it on purpose since v2.3.4) look the ROM's header-excluded CRC32 up in the vendored per-game table and write its region / mapper / submapper columns into the header with `apply_header_overrides`. That table was compiled for iNES 1.0 images and, for many boards, records a *compatible* mapper rather than the real one (140 as 66, 150 as 243, 152 as 70, 159 as 16, 48 as 33, 241 as 178, ...). The substitute is harmless only when it decodes the registers the game actually writes. Youkai Club writes its JF-11/14 bank register at $6000 (traced: 0x23 / 0x13 / 0x33 / 0x03 every frame); mapper 66 decodes $8000-$FFFF, so no bank ever switched and the game sat on a blue screen. With its own NES 2.0 header (mapper 140) it boots, shows its title and plays. Evidence. A scratch scan of the staged corpus listed every dump whose vendored row rewrites the header mapper or submapper: 41, of which 32 are NES 2.0 images. Each of the 32 was run on the harness's input schedule with and without the table: 20 identical, 2 differing only in a blinking prompt (Auto-Upturn, Pocket Zaurus), and 10 rendering wrong or blank under the table and correctly under their own header -- Youkai Club, Mississippi Satsujin Jiken (140), Lightgun 2 in 1 (150), Gegege no Kitarou 2 and Saint Seiya (152), Dragon Ball Z - Kyoushuu! Saiya Jin and both Magical Taruruuto-kun (159), Bakushou!! Jinsei Gekijou 3 (48), Fan Kong Jing Ying (241). None went the other way. Fix. New `rustynes_gamedb::load_time_entry(crc, header)`: a user-overlay entry is returned as written (a deliberate correction of this image); a vendored row has its mapper and submapper dropped when the header is NES 2.0. Region and mirroring are untouched (mirroring is applied separately and only to hardwired boards). iNES 1.0 corrections, the table's purpose (Seicross's mapper 185 submapper 4), still apply. Both load paths call it: the frontend's chokepoint and the harness's mirror of it, so the regression net keeps testing the program users run. Tests. `vendored_mapper_never_overrides_a_nes2_header` (rustynes-gamedb) uses the real vendored Youkai Club row (CRC 6BC65D7E, mapper 66) against a NES 2.0 mapper-140 header and an iNES 1.0 one. It failed before the fix (left Some(66), right None) and passes after; mutation (guard disabled with `&& false`) makes it fail again: CAUGHT. Snapshots re-blessed, each attributed to this change by the with/without A/B above and by viewing the new frames: external_coverage for Bakushou!! Jinsei Gekijou 3, Mississippi Satsujin Jiken, Youkai Club, Lightgun 2 in 1, Gegege no Kitarou 2 (Japan), Saint Seiya, Dragon Ball Z Kyoushuu, Magical Taruruuto-kun (Rev 1) and 2, Fan Kong Jing Ying. Five leave KNOWN_BLANK (the ratchet failed until removed): Bakushou 3, the three mapper-159 games and Fan Kong Jing Ying. The coverage sweep over mapper dirs 036/048/070/079/087/118/140/148/150/152/159/206/241 passes. Gates: cargo fmt --check; cargo clippy --workspace --all-targets -D warnings; clippy on rustynes-test-harness with commercial-roms,test-roms; cargo test -p rustynes-gamedb (13 + 2 pass); cargo test -p rustynes-frontend (646 pass); markdownlint on the two docs. Docs: docs/frontend.md (the per-game database paragraph claimed "mirroring only", stale since v2.3.4; it now states the header corrections and the NES 2.0 rule) and CHANGELOG [Unreleased] Fixed. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014qfTKi2M3swo7qnwvYCkDj
Mapper 78 covers two boards whose bank register bit 3 is wired
differently: Holy Diver routes it through a 74HC00 mux to give H/V
mirroring, Cosmo Carrier connects it straight to CIRAM A10 for
single-screen A/B (NESdev `INES_Mapper_078`). NES 2.0 names them with
submappers 3 and 1. An image without a submapper -- every iNES 1.0 dump,
and NES 2.0 submapper 0 -- was always built as Holy Diver, ignoring its
header.
The page states the convention for that case: "iNES1 ROM image headers
often set the 'alternative nametables' flag for Holy Diver and cleared it
for Cosmo Carrier", and notes that Nestopia 1.4.0 and FCEUX 2.1.5 default
to Cosmo Carrier's 1scA/1scB wiring. The dispatcher now follows it: a
named submapper (1 or 3) wins in both directions; otherwise the
four-screen / alternative-nametables bit (flags 6 bit 3, which the header
parser reports as `Mirroring::FourScreen`) selects Holy Diver when set
and Cosmo Carrier when clear.
What it does not fix, by design. The assigned dump "Uchuusen - Cosmo
Carrier (J) [!]" is an iNES 1.0 image with flags 6 = $E8, i.e. the bit
SET -- byte for byte the same flags as "Holy Diver (J) [!]". Under the
documented convention its header names Holy Diver, so it still runs on
the H/V wiring and still stalls on a blue screen with one dot. That is a
mislabelled header, not a board defect: "Uchuusen Cosmo Carrier
(Japan).zip" carries the identical PRG+CHR (body SHA-1 prefix 23d302b901
in both) with a NES 2.0 header, submapper 1, and renders the game (the
planet, the MISSILE/BEAM/SHIELD menu). A scratch copy of the [!] image
re-headed to NES 2.0 submapper 1 renders the same. No CRC special case
is added (no per-game overrides); the vendored game DB does not help
either -- its row for this CRC carries submapper 0.
Corpus effect, measured on the coverage harness schedule: none. Holy
Diver (both dumps) and the [!] Cosmo Carrier set the bit (Holy Diver,
unchanged); the NES 2.0 Cosmo Carrier is submapper 1 (unchanged); the
Portopia DvD translation (iNES, bit clear) moves to the Cosmo Carrier
wiring and its f300/f900/final frames are byte-identical either way
(md5 compared on scratch re-headed copies), so no snapshot changed. The
mapper-078 coverage sweep passes unmodified.
Test: `mapper_78_ines_header_selects_wiring_by_alt_nametables_bit`
(rustynes-mappers, lib.rs) parses synthetic mapper-78 images and checks
the mirroring after a bit-3 write: iNES clear -> 1scB, iNES set ->
Vertical, NES 2.0 sub 0 clear -> 1scB, sub 1 with the bit set -> 1scB,
sub 3 with it clear -> Vertical. Red before the fix (iNES clear gave
Vertical). Mutations: fallback arm back to Holy Diver -> CAUGHT; the
four-screen arm disabled -> CAUGHT ("iNES 1.0, flag set").
`v21_coverage_mappers::mapper_78_holy_diver_default_boots` is renamed
`mapper_78_no_submapper_default_boots`, since its synthetic image now
boots on the Cosmo Carrier wiring; it still passes.
Gates: cargo fmt --check; cargo clippy --workspace --all-targets -D
warnings; clippy on rustynes-test-harness with commercial-roms,test-roms;
cargo test -p rustynes-mappers (929 lib + integration pass);
v21_coverage_mappers mapper_78 (2 pass); markdownlint.
Docs: docs/mappers.md row 78, the module preamble, CHANGELOG
[Unreleased] Fixed.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014qfTKi2M3swo7qnwvYCkDj
The v2.9.8 live-render survey assigned five more blank or garbled boots. Two were fixed in the preceding commits (the game DB rewriting NES 2.0 headers; mapper 78's iNES fallback). The rest are not board defects, and are recorded here instead of forced, since no per-game override is added. 21-in-1 [p1][!] (header: 133, Sachen SA-72008). The 72008 is a $4100 latch with one PRG bit and two CHR bits (NESdev `INES_Mapper_133`), so it addresses 64 KiB PRG / 32 KiB CHR; the image has 128 KiB / 64 KiB. Traced with the event log, the game never writes $4100; its first mapper-space writes are $8001=$80, $F000=$80, $F020=$A2, $F001=$00 -- an address-encoded latch. Re-headed in scratch to each implemented address-latch multicart (57, 58, 62, 200-203, 212-214, 225-227, 229, 231, 242, ...), only 225 (ET-4310 / K-1010, `INES_Mapper_225`: banks, PRG mode and mirroring in the written address) boots to the "21 GAME" menu and holds it at f900; as 133 it is tile noise from the first frame. Verdict: mislabelled, the board is mapper 225. Zhan Guo Si Chuan Sheng (C&E) (Unl) (header: 132, TXC 22211). NESdev `INES_Mapper_132` names this image in its Notes: GoodNES sets it to 132, but it is a mapper hack with CHR rearranged for some emulators' mapper 132, which "only works on certain emulators' implementation of Mapper 132, not on the above implementation based on studying the circuit board"; the correct board is 173. Observed: the title and the map / player-select screen render correctly at f300, and in-game frames (after the harness's START taps start and pause a game) are black with one bar where the PAUSE text is drawn from the wrong CHR bank. Verdict: mislabelled; mapper 173 is not implemented, so no re-header was possible to confirm the rest. Uchuusen - Cosmo Carrier (J) [!]: cross-reference to the mapper-78 row (its header's alternative-nametables bit names Holy Diver). BB Car (Asia) (En) (Unl) (header: 152; the vendored game DB says 7): not a defect. The event log shows no mapper write at all in 120 frames, so the board does not matter; the "0123456789" screen with one sprite is the game's own. Its controller loop (disassembled at $9140: strobe $4016, read A and test it, four bare reads that discard B/Select/Start/Up, then tested D-pad reads) ignores START, which is all the harness taps. Tapping A on the same schedule reaches the race (track, cars, RN 07, score) by f900. The harness capture is what lands on the start screen; a capture override is the lead's call. Docs only. markdownlint passes. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014qfTKi2M3swo7qnwvYCkDj
Magic Floor (iNES mapper 218) has no CHR chip. The console's 2 KiB CIRAM is both the pattern table and the nametable, and the board ties CIRAM A10 directly to one PPU address line chosen by the iNES flags-6 bits (NESdev "INES Mapper 218"): $A1 CIRAM A10 = PPU A10 2 KiB CHR shared with two nametables $A0 CIRAM A10 = PPU A11 2 KiB CHR shared with two nametables $A8 CIRAM A10 = PPU A12 1 KiB per pattern table, one screen (bank 0) $A9 CIRAM A10 = PPU A13 1 KiB CHR (bank 0), one screen (bank 1) Root cause, two defects stacked: 1. `header::parse` folds bit 0 into `Mirroring::FourScreen` once bit 3 is set, and `MagicFloor218::new` mapped `FourScreen` to the A10 wiring. So both single-screen headers ran as A10. Both Magic Floor dumps in the local corpus carry flags 6 = $A9, so the game's nametable writes at $2000-$23FF landed in CIRAM bank 0, the same memory as its tiles at $0000-$03FF, and the bottom of the screen (and the tile shapes) filled with garbage. 2. Even when reached, the single-screen modes resolved to a constant bank for BOTH pattern and nametable space (ScreenA -> 0, ScreenB -> 1). The documented wiring makes the bank a function of the address line: under A12, pattern table 1 is bank 1 while the nametables are bank 0; under A13, all pattern space is bank 0 and the nametables bank 1. Fix: `MagicFloorMode::physical_bank` now takes the full PPU address and returns bit 10, 11, 12 or 13 of it, the literal wire, for both the CHR and nametable paths (the old 1 KiB block-index arithmetic for the two two-screen modes is the same function of A10 / A11, so those are unchanged). `parse` re-reads raw `bytes[6]` bit 0 when the four-screen bit is set, exactly as mapper 30 already does, and passes SingleScreenB ($A9, A13) or SingleScreenA ($A8, A12) to the existing constructor. No public signature changes. A caller that passes a bare `FourScreen` still gets the A10 wiring as before. Evidence and verification: - Red first: `m218_single_screen_a_wires_ciram_a10_to_ppu_a12`, `m218_single_screen_b_wires_ciram_a10_to_ppu_a13` (lib) and the new `tests/magic_floor_218.rs` (parse-level, all four header values) failed before the fix (2 of 3 parse tests, 2 of 5 m218 unit tests). - Mutations: dropping the raw-byte dispatch in `parse` fails 2 of the parse tests; returning the old constant banks for the single-screen modes fails both new unit tests. - Frames: both dumps now draw the board and "MAGIC FLOOR 0 OF 66 / 34 POINTS" score line at f900, f1100 and final (they drew garbage tiles). - Snapshots re-blessed, attributable to this change only: external_coverage__mapper_218_MagicFloor_..._PC10_Version_PD.snap and ..._PD_a1.snap. No other mapper uses this board. Provenance: the replaced function's doc said "Matches `GeraNES` `customMirroring`", a source cross-check of the kind docs/originality-and-provenance.md records under "GeraNES specifically". The rewrite dropped that line. It has been restored at the site as a dated note (provenance mentions are classified, never deleted; maintainer rule of 2026-09-22). The new code was written from the NESdev page alone. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014qfTKi2M3swo7qnwvYCkDj
NESdev "INES Mapper 226" (76-in-1 / Super 42-in-1 BMC) documents two
registers decoded with mask $8001:
$8000 [PMOP PPPP] bits 4-0 = PRG bits 4-0, bit 5 (O) = PRG mode
(0 = one 32 KiB bank, 1 = the same 16 KiB bank in
both halves), bit 6 (M) = mirroring (0 = H, 1 = V),
bit 7 (P) = PRG bit 5
$8001 [.... ...H] bit 0 = PRG bit 6
Root cause: `Multicart226` took PRG bits 5-0 from `reg0 & 0x3F` (so the
mode bit O became PRG bit 5), the mode from bit 6 (M), and mirroring from
bit 7 (P). The docs row quoted the right `[PMOP PPPP]` string but annotated
the wrong bits, and the old unit test encoded the same misreading. On the
2 MiB 76-in-1 the menu's first bank switch landed in the wrong bank, and
the screen stayed a field of one repeated tile at every capture point.
Super 42-in-1 happened to boot, but under bit-7 mirroring its menu scroll
showed the second page (games 12-22) with the header off-screen.
Fix: PRG = (reg1.0 << 6) | (reg0.7 << 5) | (reg0 & 0x1F); 16 KiB mode =
reg0 bit 5; vertical mirroring = reg0 bit 6. The page also says "The
multicart clears both registers on soft reset", so the board now
implements `Mapper::reset` (registers to 0: bank 0, 32 KiB mode,
horizontal), and the trait's doc list of boards that clear on reset names
it. Power-on is unchanged (the constructor already zeroes both).
Evidence and verification:
- Red first: `m226_reg0_layout_is_pmop_pppp` failed before the fix (left
34, right 35). The old `m226_two_regs_select_prg_and_mirror` encoded the
misreading ($43 as "16K, H"), and is rewritten to the page's layout
($23 = 16K, H, bank 3).
- Mutations, each reverted after: PRG bit 5 from reg0 bit 5 -> FAILS
(after strengthening the test with $23 and $83: the first draft used only
$A3, where bits 5 and 7 are both set, and that mutant SURVIVED); mode
from bit 6 -> FAILS (both tests); mirroring from bit 7 -> FAILS; reset
that keeps reg1 -> FAILS.
- Frames: 76-in-1 now shows "76 IN 1 GAME / PAGE.1-8 / 1990 TSANG HAI";
Super 42-in-1 now shows "SUPER 42 IN 1 / 22 GAMES / PUSH 'SEL' TO NEXT"
with games 1-10, at f900, f1100 and final.
- Snapshots re-blessed, attributable to this change only (the only two
mapper-226 dumps):
external_coverage__mapper_226_BMC_76in1_76_in_1_p1.snap,
external_coverage__mapper_226_BMC_76in1_Super_42_in_1_22_Games_p1.snap.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014qfTKi2M3swo7qnwvYCkDj
The Vs. System CPU board (MDS-0x, the same board in both cabinet types) carries 2 KiB of RAM at CPU $6000-$7FFF. NESdev "Vs. System" ($4016 write, bit 1) documents its arbitration on the primary CPU: "When 1: the primary CPU has access and the secondary CPU sees open bus. When 0: the secondary CPU has access and the primary CPU sees open bus." INES_Mapper_099 lists "CPU $6000-$7FFF: 2 KiB RAM, swappable between CPUs (open bus when not available)". Root cause: `VsSystem` provisioned that RAM only through the DualSystem wrapper (`enable_vs_dual_wram`). On a UniSystem cart `$6000-$7FFF` read 0 and every write was dropped. Vs. Super Mario Bros. clears $6600-$67FF, copies CHR data into $6674-$67FF with OUT1 high, and later reads its top score and state back from $66BA..; with the RAM missing it parks in its NMI loop with rendering effectively blank. Every $4016 value the game writes (2, 3, 6) keeps OUT1 set. Fix: a UniSystem `VsSystem` owns a 2 KiB `uni_wram` and an `out1` latch taken from bit 1 of every forwarded $4016 write. While `out1` is high the window reads and writes the RAM (mirrored every 2 KiB); while low, `cpu_read_unmapped` reports open bus and writes are dropped. The DualSystem path is unchanged (it keeps its shared, ungated copy; modelling OUT1 arbitration there is out of scope and would touch the four dual titles). Save state: UniSystem now emits layout v3 = v1 + OUT1 byte + 2 KiB RAM; v1 still loads (RAM cleared, OUT1 low, the power-on state); the dual v2 layout is untouched. Evidence and verification: - Red first: `unisystem_ram_follows_4016_bit1` failed before the fix (the window stayed unmapped with OUT1 high). - Mutations, each reverted: OUT1 taken from bit 2 -> FAILS; writes that ignore OUT1 -> FAILS; save state that drops the RAM -> FAILS. - Game: the corpus's `VS Super Mario Bros.nes` is headed mapper 3 with no Vs. flag (flags 6/7 = $31/$00), so it runs as CNROM and its coverage frame does not change. Its code writes $4016 37 times (CHR bank via bit 2) and the $4020 coin counter, and never writes a CNROM register, so its board is mapper 99. Run through a scratch probe with only header bytes 6/7 rewritten to $31/$61 (mapper 99, Vs.), PPU RP2C04-0004 and DIP $10 (the vs_db values for the title): before this change, a green backdrop with the CPU in its NMI loop; after, the SMB attract demo in correct colours. Recorded in docs/mappers.md, not forced. - No snapshot changed: the coverage suite over `vs-system`, `mapper-099` and `mapper-151` (every Vs. dump in the corpus, including the four DualSystem titles) matches its committed baselines. - `cargo test -p rustynes-mappers` and `-p rustynes-core` green. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014qfTKi2M3swo7qnwvYCkDj
NESdev "Vs. System" (Palette): "To determine which PPU is used, read the PPU type byte of the NES 2.0 header if available; otherwise, use the hash of the PRG and CHR ROM data." `vs_db` is that hash table. It carried a row for the patched `VS The Goonies Hack.nes` (RP2C04-0003, DSW0 $80) but none for the unpatched Konami dump `Goonies, The (VS).nes` (SHA-256 ff268fb3...), so the latter fell back to the parser's 2C03 default and drew its "VS." title red on a green backdrop. The two files differ only in 327 PRG bytes of code (six ranges at $80ec, $85aa, $f083, $f0bb, $f570-$f6ad); header, board and CHR are identical, so the PPU is the same chip. The new row carries the hack row's PPU and DIP default over; no new external source was consulted. This is palette selection by hash, the documented mechanism, not a boot override: the game already ran (it reached its ROM check and attract screen); only its colours were wrong. Records, no behaviour change: - docs/mappers.md (mapper 151 row), m151_konami_vs.rs and lib.rs no longer cite "GVS VS. TKO Boxing" as a mapper-151 example. Both corpus T.K.O. Boxing dumps (byte-identical, a6332035...) carry a mapper-151 header, but their reset handler writes $8000=6, $8001=8, $8000=7, $8001=9 (Namco 108 bank select / data) and nothing writes $9000-$F000; VRC1 decodes every $8000-$8FFF write as the $8000 PRG bank, so the boot goes nowhere and the frame stays grey. NESdev "Vs. System" names the Namco 108 (mapper 206) plus an extra protection IC for three third-party Vs. games; NES 2.0 encodes T.K.O. Boxing's as Vs. hardware type 2, which RustyNES does not model. Recorded as mis-labelled, not forced. Verification: - Red first: `goonies_original_dump_uses_the_2c04_0003_palette` failed before the row (lookup returned None). Mutation: the row with `Rp2C03` -> FAILS. `db_is_sorted_by_sha256` and `every_entry_is_findable` pass (the row sorts last, after fda84d8d...). - Frames: f900 shows the white-on-black "GOONIES / ROM 1 OK / ROM 2 OK" check; final shows the orange "VS." title, "HI 10000", "(c) KONAMI 1986" and "CREDIT 01" on black (was red on green). A 2000-frame probe shows the white-on-black Warner Bros. licence screen. - Snapshot re-blessed, attributable to this row only: external_coverage__mapper_151_KonamiVS_Goonies_The_VS_zip.snap. The other mapper-151 snapshots (Gradius, both T.K.O. Boxing, Silent Assault) are unchanged. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014qfTKi2M3swo7qnwvYCkDj
Two blank or garbled boots from the v2.9.8 commercial survey are not
board defects; this records why, so they are not re-investigated or
"fixed" by a per-game override.
Mapper 212, 999-in-1 [p1]: the menu text is right but every unused cell
shows the font's `0`. A nametable probe after 600 frames shows the menu
wrote only its text tiles; the rest of $2000-$23BF is tile $00. The reset
handler at $EF60 waits for one vblank ($FFA1: LDA $2002 / BPL), then
calls $F056, which sets PPUADDR to $2000 and writes 960 x $24 plus 64
attribute zeros. That $2006 write lands about 27,400 CPU cycles after
power-on, inside the NTSC warm-up in which PPUCTRL, PPUMASK, PPUSCROLL and
PPUADDR writes are ignored (~29,658 cycles; NESdev "PPU power up state",
modelled by `PpuRegion::post_reset_mask_cycles`). The fill therefore goes
to pattern space at v = 0. The same page notes that a Famicom's PPU leaves
reset about a frame before the CPU, which such carts rely on. Scratch
experiment (reverted, not committed): with the NTSC mask set to 0 the menu
draws on a clean background. Whether to model a Famicom power-on is a
console-model decision for the maintainer; the board matches
INES_Mapper_212 (address latch, $6000 D7 = !A4).
Mapper 233, Unknown Multi Cart w-Galaxian [p1]: INES_Mapper_233 says this
dump ("Unknown Multicart 1") "does *not* follow the description in this
doc at all": 32 NROM-128 games with no menu, CHR page = PRG page - 8,
per-game mirroring, and "might even be assigned the wrong mapper number".
No documented board fits, so none is guessed.
Docs only; no code or snapshot changes.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014qfTKi2M3swo7qnwvYCkDj
…ts board The artifact audit's fixes landed as 11 commits from three worktree agents, cherry-picked onto this branch. This commit closes the records around them. Screenshots. Every external_coverage baseline that those commits changed was viewed on the integrated tree: 20 frames, each now a real title, menu or play screen. Their final frames replace 10 committed PNGs (Bakushou 3, Balloon Fight [T], Lightgun 2-in-1, two Bandai 24C01 titles, both Magic Floor dumps, both mapper 226 multicarts, Kyatto Ninden) and add 11 for games the corpus had no picture of. For mapper 226 the fixed frames overwrite the existing file names rather than adding a renamed pair, so the corpus does not carry the garbled and fixed captures side by side. Corpus: 488 PNGs in 171 directories under external/, 10 in besteffort/. Famicom Jump II. Its first baseline (0a0e8e56) was taken while the vendored game database rewrote this dump to mapper 16. The database guard (9bc58595) stops that, so the full sweep re-ran it as 153. It is still blank, as its NESdev page documents, but the frame hash moved. A mismatched snapshot is reported as a failure before the blank test runs, so KNOWN_BLANK's ratchet also said the game "no longer boots blank", which was wrong. The baseline is re-taken on the correct board and the entry stays. KNOWN_BLANK's doc comment now gives the v2.9.8 count (60 -> 52: seven entries now render, the two Doraemon hacks moved to UNSUPPORTED as the mapper 8 images they are, and Famicom Jump II was added). The plan's outcome row lists every fix, mislabelled dump and non-defect. Verification: a full external_coverage sweep on the integrated tree, 744 ROMs, failed only on Famicom Jump II, for the reason above; after the re-take, that ROM passes. The test-roms suite will be re-run before the cut. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014qfTKi2M3swo7qnwvYCkDj
Maintainer decision 2026-10-01, "Identity default": the eight 1 KiB CHR
bank registers of the Konami VRC6 (mapper 24 = VRC6a, mapper 26 = VRC6b;
$D000-$D003 and $E000-$E003) now hold 0, 1, ..., 7 when the board is
constructed, so slot i ($0000 + i * $400) maps CHR bank i until the
program writes the register. Previously all eight started at 0, so every
1 KiB window showed the first 1 KiB of CHR.
This is an ASSUMPTION and is recorded as one. The NESdev VRC6 page
(fetched 2026-10-01) states no power-on or reset value for any VRC6
register; the old all-zero state was equally undocumented. The new value
is held in one named constant, `POWER_ON_CHR`, whose rustdoc carries the
reasoning and the evidence.
Evidence: "Pulsewave Invite (2009-08-xx) (PD)" never writes $D000-$E003
and expects the first 8 KiB of CHR-ROM mapped in order. Under the old
layout its boot frames were a scatter of misplaced tiles; under the
identity layout it draws its postcard ("This weekend in NYC we celebrate
the end of summer at Pulsewave!", the beach scene below). Its .snap
changes only in the f900/f1100 framebuffer hashes; cycles, audio sample
count and audio hash are unchanged, as expected for a CHR-only change.
Scope decisions:
- Power-on only. The value is applied in `Vrc6::new`, which is also what
a power cycle runs (the core rebuilds the mapper). `Mapper::reset`
stays the default no-op: NESdev documents no reset input to the VRC6,
and a console soft reset leaves cartridge register latches alone on
almost every board (the trait's own documentation lists the few that
clear something). A game reset mid-play therefore keeps its banks.
- PRG unchanged. $8000 (16 KiB) and $C000 (8 KiB) still power on as 0.
The decision covered CHR only, the reset vector lives in the fixed
$E000 bank, and no documentation or dump gives evidence for another
PRG value.
- Save states unaffected: the CHR registers were already serialised, so
a loaded state overrides the power-on value exactly as before.
- CHR-RAM VRC6 boards (none in the corpus) now see the 8 KiB RAM mapped
in order before the first register write, rather than eight aliases
of its first 1 KiB.
Tests (red first, in crates/rustynes-mappers/src/m024_vrc6.rs):
- vrc6_chr_registers_power_on_as_identity: for mappers 24 and 26, each
slot reads its bank number from a synthetic 32 KiB CHR-ROM whose
1 KiB banks are tagged. FAILED before the fix.
- vrc6_soft_reset_keeps_chr_registers: a written register and an
untouched slot both survive `reset()`. FAILED before the fix (the
untouched slot read bank 0).
Mutation checks:
- revert the initialiser to [0; 8]: both tests FAIL (caught).
- add a `reset()` that reloads POWER_ON_CHR: the soft-reset test FAILS
(caught).
Frame verification: every staged mapper 24/26 dump (11: Akumajou
Densetsu VC, the Castlevania III retranslation, Pulsewave Invite,
Rudeboy, Sun Dried Demo, SMB co-op hack, Esper Dream 2 x2, Madara x2,
Go-Go! Ice Challenge Ultimate!) was dumped with RUSTYNES_DUMP_FRAMES=1
before and after; `cmp` on all 33 PNGs: 30 byte-identical, the 3
differing are Pulsewave's f900/f1100/final. Only Pulsewave's snapshot
was re-blessed; the filtered coverage run then passes all 11.
Gates: cargo fmt --check; cargo clippy --workspace --all-targets
-D warnings; cargo test -p rustynes-mappers (938 lib tests + the
integration suites, 0 failed); AccuracyCoin 144/144 (RAM, 0 fail);
nestest golden log 0-diff; markdownlint on the two docs.
Docs: docs/mappers.md rows 24 and 26 note the assumption, the Pulsewave
evidence, the power-on-only scope and the untouched PRG. CHANGELOG
[Unreleased] lists it under Changed, not Fixed, because it swaps one
undocumented power-on value for another rather than correcting
documented hardware behaviour.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014qfTKi2M3swo7qnwvYCkDj
Maintainer decision 2026-10-01: an off-by-default Famicom console option
whose PPU leaves reset earlier than the NES's, with the default build
unchanged.
What the hardware does (NESdev "PPU power up state", vendored
nesdev_wiki/output/PPU_power_up_state.md, sections "Famicom" and the
front-/top-loader note):
- NES-001: the CPU and PPU share one reset line. After power-on and after
every Reset, writes to $2000/$2001/$2005/$2006 are ignored for about
29,658 CPU cycles (NTSC; 33,132 PAL), and Reset clears PPUCTRL, PPUMASK,
the scroll/address latch and the read buffer.
- Famicom: the PPU's /RESET is tied to 5 V and only the CPU's rides a
0.47 uF capacitor, so at power-on the PPU starts initialising
"approximately one frame before the CPU reset" (not measured, may vary).
One NTSC frame (~29,781 cycles) is longer than the 29,658-cycle window,
so the window has closed by the CPU's first instruction. The Reset
button resets only the CPU.
The 999-in-1 multicart (mapper 212) clears its nametable through
$2006/$2007 at about CPU cycle 27,400, inside the NES window. Both $2006
writes are dropped, the fill goes elsewhere, and the menu is drawn over a
nametable still holding tile $00 (the font's "0").
Mechanism:
- rustynes-ppu: Ppu::end_warmup() zeroes post_reset_mask_remaining, and
Ppu::warmup_cycles_remaining() reads it. New code, written from the wiki
text; it sits outside every region ppu.rs's Provenance header discloses
(sprite evaluation, OAM-data bus, OAM decay, ALE/octal latch, OAM
corruption), and nothing was copied from those regions.
- rustynes-core: ConsoleModel { Nes (default), Famicom }, re-exported at
the crate root. LockstepBus stores it as config (like ppu_die_revision):
power_cycle() ends the rebuilt PPU's warm-up under Famicom, reset() skips
Ppu::reset() under Famicom (APU, DMA and mapper reset as before), and
set_console_model(Famicom) ends a window in progress, which is how a
host applying its config straight after construction gets the Famicom
power-on. Selecting Nes never re-arms a closed window. Nes exposes
set_console_model / console_model.
- Not modelled, and documented: the PPU's frame position at power-on is
not advanced by a guessed "approximately one frame"; the NES-101
top-loader is not a separate model (its power-on lead is undocumented).
No per-game list and no header heuristic select it; NES 2.0 has no
console type that tells a Famicom from an NES.
- Save states: no format change. The model is config; the counter it acts
on is already in the PPU section. snapshot_schema_audit lists the new bus
field as config.
- Determinism: deterministic, but movies and netplay do not record it,
the same as the other hardware knobs (OAM decay, die revision, power-on
RAM). Recorded as a gap in docs/ppu-2c02.md, not closed here.
- Frontend: [emulation] famicom_console (default false), a checkbox under
Settings > Emulation > Accuracy with EN/ES strings, and
App::apply_console_model run on ROM load, at startup and on a Settings
change via its own SettingsApply::console_model flag. It is kept off the
fast_dotloop path because apply_ppu_hardware_config also re-runs
set_power_on_ram, which rewrites work RAM. "Reset to defaults" pushes it.
Power-cycle keeps the model inside the core.
Tests (red first):
- nes::tests::famicom_console_model_ppu_leaves_reset_before_the_cpu: a
ROM writes $2000=$90 and $2001=$01 once, as its first instructions. NES
model: both dropped. Famicom: both land; Reset keeps them and re-arms
nothing; NES Reset clears them and re-arms the window; the model
survives a power cycle. Failed against a store-only setter (warm-up
29,650 remaining).
- ppu::tests::end_warmup_lets_masked_registers_write_immediately.
- Mutations, each caught: setter without the power-on apply (nes.rs
assertion at the Famicom boot), unconditional ppu.reset() (Famicom Reset
assertion), power_cycle without the apply (power-cycle assertion),
end_warmup as a no-op (both tests).
- tests/famicom_console.rs (commercial-roms, local dump): 999-in-1 after
300 frames. Tile $00 in the first nametable: NES 388/960, Famicom 2/960;
the Famicom frame shows the menu on a clean background.
- config: famicom_console defaults off and round-trips; reset_advanced
pushes the flag.
Default unchanged: AccuracyCoin 144/144, nestest 0-diff; external_coverage
untouched (no re-bless) for mapper-212 (11 ROMs), mapper-000 (12),
mapper-001 (21), mapper-004 (33), mapper-019 (6). fmt, workspace clippy,
frontend clippy (scripting, scripting+hd-pack, retroachievements), both
wasm clippy gates, harness commercial-roms clippy, no_std thumbv7em build
and rustdoc -D warnings pass.
Docs: docs/ppu-2c02.md (Famicom console model section), docs/frontend.md
(Emulation tab), CHANGELOG [Unreleased] Added.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014qfTKi2M3swo7qnwvYCkDj
Found by the agent that added the Famicom console model (1d128692), while it
was deciding which apply path its own setting should take.
Mechanism. The Settings window's live-apply for `fast_dotloop` called
`apply_ppu_hardware_config`. That function pushes the whole `[emulation]`
PPU knob set: the PPU revision, the power-up palette, the power-on RAM fill,
and the fast-dot-path selector. Its comment called the re-push "idempotent".
It is not: `Nes::set_power_on_ram` -> `LockstepBus::set_power_on_ram`
immediately calls `apply_power_on_ram`, which fills the 2 KiB work RAM (and
the open-bus latch) with the power-on pattern, zeros by default. So flipping
a switch documented as frame-identical wiped the running game's RAM mid-play.
The three power-on knobs are only meaningful at load, power-cycle and
startup, which are the other three callers, and those stay as they were.
Fix. A new `apply_fast_dotloop` pushes the selector alone, and the live
branch calls it. The Famicom option already had its own path for the same
reason, and its doc comment said so.
Test. `live_settings_never_rerun_the_power_on_ram_fill` is a source-shape
gate, the same style as the wasm entry-point tests in this module, because
`App` needs a window and an emulator thread. It cuts its own test module out
of the source before searching (a file that reads itself must exclude the
part doing the reading; this module's first such test learnt that in
review). It asserts three things:
- the fast-dot-path branch calls `apply_fast_dotloop`;
- no `if settings.<knob> { ... }` live-apply branch reaches
`apply_ppu_hardware_config`;
- the helper never calls `set_power_on_ram`.
Mutation: putting the old call back in the branch makes it fail; with the
fix it passes.
Not done here: the same agent noted that on ROM load the frontend sets
`has_rom` before running the `apply_*` hooks, so the emulator thread could
run a frame before the power-on options apply. That needs a measurement
first, and is left as a follow-up.
Also in this commit: the Pulsewave Invite screenshot for 8a0ee92c (the VRC6
power-on banks), its final frame, the postcard.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014qfTKi2M3swo7qnwvYCkDj
…2.0's own
Maintainer decision (2026-10-01): "Honour DB region now" -- apply the game
database's region for iNES 1.0 headers in v2.9.8, audit the affected dumps'
frames, and correct any rows that are wrong.
Root cause
----------
`apply_header_overrides` wrote a PAL row into iNES 1.0 header byte 9 bit 0,
the TV-system flag. `parse_header` ignores that bit by design and parses every
iNES 1.0 image as NTSC, because raw dumps carry junk there ("DiskDude!" puts
's' = $73 in byte 9; on the staged corpus three pirate multicart images the
table does not list -- mapper 60's 4-in-1, 133's 21-in-1, 200's 7-in-1 -- have
$01 there in an otherwise clean header with nothing to confirm it). So no PAL
row reached any iNES 1.0 game, in the frontend or in the coverage harness.
Pin Bot (Europe), TQROM, uploads CHR-RAM for the length of the PAL vblank; at
NTSC the upload overran into rendering and garbled the title. Conversely, for
NES 2.0 headers the row DID rewrite byte 12, overriding a region the header
states.
Design
------
The region travels in the header bytes, through the existing chokepoint
(`load_time_entry` -> `apply_header_overrides`), because every load path
already runs it (desktop File menu and CLI, browser, coverage harness) and
everything that rebuilds later re-parses the corrected bytes (power cycle).
A constructor parameter would have had to be threaded through each of those
sites, which is how earlier corrections were lost three times. `parse_header`
is unchanged: raw byte 9 is still ignored.
* `load_time_entry`: a vendored row's region is withheld for a NES 2.0
header, alongside its mapper and submapper. An iNES 1.0 header takes it.
* `apply_header_overrides`, iNES 1.0 + PAL/Dendy: `ines1_as_nes2` builds the
NES 2.0 header of the SAME board -- the mapper the iNES 1.0 parse settled on
(after the dirty-tail rule) written cleanly, submapper 0, byte 9 = 0 (plain
byte-4/5 sizes), region in byte 12, bytes 13-15 zero -- and tries two RAM
statements in order: the iNES 1.0 heuristics written out (8 KiB PRG-RAM,
in the NVRAM nibble when byte 6 has the battery bit; 8 KiB CHR-RAM iff no
CHR-ROM), then no RAM at all. Each candidate is parsed and compared with the
iNES 1.0 parse by `same_board`: cartridge identity, mirroring, console type,
Vs. fields, battery, trainer, PRG/CHR, and the mapper's capability flags,
mirroring answers, sram/save_data lengths, serialized state and debug view.
The first match is used; if none matches the region is refused (byte 9 is
written as before, so the bytes and hash are unchanged). The second
candidate exists because NES 2.0 RAM sizes are taken at their word: the
v2.9.6 MMC3 multicart boards (12, 37, 45, ...) take their own default on
iNES 1.0 instead of the nominal 8 KiB, and an "8 KiB" promotion would have
put work RAM over Super Mario Bros. + Tetris + Nintendo World Cup's outer
bank register.
* Vs. System / PlayChoice-10 carts are never promoted: those cabinets exist
only in NTSC timing; a PAL row on one names the home release sharing its
PRG/CHR (PlayChoice-10 Baseball = Baseball (USA, Europe)). Mapper 99 needs
the explicit check, since both parses force it to the Vs. System.
* Multi-region rows: the vendored Region column holds only NTSC (2,305) and
PAL (377); no Dendy, no "multiple". The nine rows titled "(USA, Europe)" --
one image sold in both markets -- are the only multi-market titles, all PAL.
They now carry no region, so an iNES 1.0 copy stays at the NTSC default,
which is what a NES 2.0 "multiple region" header (Ice Climber's and
Gumshoe's staged copies) gives the same image. Dendy is accepted from the
user overlay and promoted like PAL.
No per-row correction was needed: every PAL row whose dump is staged was
confirmed or not refuted by its frames (below), so no correction layer was
added. (Note for the record: `game_database.txt` is not a verbatim copy -- the
Seicross row was edited in place by #127.)
Consequence: a promoted image hashes differently, so for an iNES 1.0 game
with a PAL row the frontend's save-state directory and `.sav` name change once.
Evidence (frames before/after, external_coverage, all looked at)
----------------------------------------------------------------
iNES 1.0, NTSC -> PAL (promoted):
Pin Bot (E) [!] garbled title -> correct PIN*BOT title FIXED
Sidewinder (Sachen) x2 frozen attract (3 identical frames) -> runs FIXED
High Speed (E) same title screen, PAL cycle counts
SMB + Tetris + NWC (E) (m37, no-RAM candidate) same, renders
Super Sports Challenge (m232) same team select, renders
Great Wall (m137) NTSC sat on EASY/HARD; PAL reaches gameplay, which
is garbled -- identically at NTSC, PAL and Dendy when
run long enough (coverage_smoke on NES 2.0 variants,
600..3200 frames): a pre-existing defect, not region
Silver Eagle, Dancing Blocks, Master Chu, Millionaire, Pyramid II,
Twin Eagle, Challenge of the Dragon x2, Mahjong World: render the same
NES 2.0, header region now kept:
Funblaster Pak (Australia) PAL header, was forced NTSC -> PAL
Fan Kong Jing Ying, Mei Guo Fu Hao (Asia) Dendy header, was NTSC -> Dendy
Ice Climber, Kid Icarus, Gumshoe, Auto-Upturn, Happy Pairs "multiple"
header, was forced PAL -> NTSC timing. All render correctly.
Unchanged: PlayChoice-10 Baseball (arcade, refused).
Verification
------------
Red first: `a_pal_row_reaches_the_core_on_an_ines1_header` failed (Ntsc vs
Pal) before the promotion; `a_nes2_header_keeps_its_own_region` failed
(Some(Pal)) before the NES 2.0 region drop. Mutations, each caught:
M1 promotion disabled -> a_pal_row..., header_overrides_rewrite...
M2 no-RAM candidate removed -> a_multicart_promotion..., sweep (refuses
12, 37, 45, ... at every layout)
M3 arcade refusal removed -> an_arcade_cart_is_never_promoted (m99)
M4 NES 2.0 region kept -> a_nes2_header_keeps_its_own_region
M5 multi-region rule removed -> a_multi_market_row_carries_no_region
`promotion_never_changes_the_board` sweeps mappers 0-255 x 5 layouts x
battery: no board refused.
Snapshots re-blessed (each attributed: cycle/audio counts move between the
NTSC 17.84M and PAL 19.92M per-600-frame figures, frames looked at):
external_coverage (24), external_extended m137/m138/m143/m145/m147/m150,
external_real_games Kid Icarus + Ice Climber. external_extended m218, m226 and
taito48 also fail on this branch with framebuffer-only diffs; none has a
region change (no DB row for m218/m226; Don Doko Don 2 is NES 2.0 NTSC with
an NTSC row), so they predate this commit and are left for their owners.
Gates: fmt, workspace clippy -D warnings, rustdoc, gamedb + frontend tests,
AccuracyCoin 144/144, nestest 0-diff, markdownlint.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014qfTKi2M3swo7qnwvYCkDj
…ibuted The agents that fixed mappers 218 and 226 and the game-database guard re-blessed the `external_coverage` baselines for their games, but not these `external_extended` ones. Those use a different input schedule and checkpoint (f600) for the same dumps. The region agent found all three failing on the branch and left them for their owners. In each, only the f600 framebuffer hash moved; cycles and audio are unchanged. - extended_m218_magic_floor_by_martin_korth: ccd695db, mapper 218's CIRAM wiring. Its coverage frame, viewed in 45765dec, is the board and score line. - extended_m226_76_in_1_p1: 12013e12 (cherry-picked from c64af128), mapper 226's `[PMOP PPPP]` register. Its coverage frame is the "76 IN 1 GAME" menu. - extended_taito48_don_doko_don_2: found by `git bisect run` from main (e3debc9) to 0b50244b, with a script that restores the old baseline and runs only this test. The first bad commit is e94724ee, the database NES 2.0 guard. The dump's header is NES 2.0, mapper 48 (bytes 6/7 = $00/$38), and the vendored database had been running it as another board. Its frames now show the Taito title, the overworld map and the title again, as they should. The whole external_extended suite passes: 137 / 0. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014qfTKi2M3swo7qnwvYCkDj
`SystemBus::power_cycle` rebuilds the PPU and the APU from `Ppu::new` /
`Apu::new`. It re-applied the settings the bus also stores (die revision,
power-up palette, Vs. RGB palette, the externally driven DMC), but every
setting held only on a chip silently reverted to its default:
PPU: the custom / generated (NTSC) palette, the extra-scanlines
overclock, the fast dot path selector, the OAM-decay switch
APU: the channel mask, the per-channel gain, the filter model
Only the desktop's Power Cycle re-pushed them (fully only since
410b333d). Android and iOS lost a loaded palette on every Power Cycle, and
`power_on_for_movie` re-applied the options a movie records but not the
palette, mask, gain or filter. The bug lived in the core, so every host
inherited it.
Mechanism. The bus now swaps the fresh chip in with `mem::replace` and
hands the old one to a new chip-side method that carries the settings:
- `Ppu::adopt_settings_from(&mut self, prev: &Self)` copies the custom
palette (and rebuilds the RGBA LUT to honour it), the overclock (through
`set_extra_scanlines`, which also zeroes the in-flight countdown, as a
fresh PPU has it) and the fast dot path, and re-enables OAM decay through
`set_oam_decay`, so the row ages are stamped from the rebuilt PPU's cycle
0 exactly as a host enabling it on a fresh console would. A new
`Ppu::custom_palette()` getter backs `Nes::custom_palette()`.
- `Apu::adopt_settings_from` copies the mask and gain and rebuilds the
filter through `set_filter_model`. The APU stores the built chain, not the
selected model, so `Apu` gains a plain `filter_model: FilterModel` field
(set in `set_filter_model`, read by nothing in synthesis) plus
`Apu::filter_model()` / `Nes::apu_filter_model()`. Nothing in blip.rs
changed: the BLEP region (Provenance: blip_buf) is untouched, and only
its existing `set_filter_model` is called.
The list of what a setting is lives next to the fields, so a new one is
carried where it is declared. `snapshot_schema_audit.rs` gains
`every_config_field_survives_a_power_cycle`: every PPU / APU field the
audit classifies as "config:" must be read as `prev.<field>` in
`adopt_settings_from`, or re-applied by a named call in
`SystemBus::power_cycle` (`active_palette`, `die_revision`,
`power_up_palette`, `dmc_driven_externally`). `filter_model` is added to the
APU's exclusion list as config (the chain it builds IS serialized).
The debug-hooks provenance stores (write attribution, pixel provenance,
audio provenance) are moved onto the new chips with the existing
take/put stash pair, so they stay ARMED and `Nes::power_cycle` empties
them, which is what its comment always claimed; before, they were dropped
with the old chips and that clear was a no-op. The state and fetch traces
(cosim capture buffers) are deliberately not carried.
Power-on fills are untouched: RAM and palette-RAM fills still re-apply on
a cycle exactly as before. Bus-held settings were never affected.
Host side, a pure refactor only: `power_on_for_movie` no longer captures
and re-sets the fast dot path by hand (the cycle keeps it); the frontend's
`configure_console` and `do_power_cycle` are unchanged in behaviour (they
still re-attach the expansion device and put the player's configuration
under a running movie's `held_options`), with their comments updated. The
frontend test `configure_console_restores_what_a_power_cycle_drops` had
asserted the old premise (that the cycle drops the chip settings); it now
asserts the cycle keeps them and drops only the expansion device.
Tests (nes.rs):
- `a_power_cycle_keeps_every_ppu_and_apu_setting`: a 7-row table, each
setting at a non-default value on a bare `Nes`, run a frame, cycle, read
back. Collects every lost row before failing.
- `a_power_cycled_console_with_settings_runs_as_a_fresh_one`: palette +
filter, cycled vs fresh nestest, 10 frames: framebuffer, audio and the
full snapshot equal. Gain and OAM decay are excluded with the measured
reason: a fresh console receives them only after `from_rom`'s reset
cycles, the cycled one has them in force for those cycles (measured:
gain applied to both after the cycle matches, carried through differs).
- `a_power_cycle_keeps_the_provenance_stores_armed` (debug-hooks).
Mutation checks: removing both `adopt_settings_from` calls and the two
provenance puts from `SystemBus::power_cycle` fails all three tests, the
table naming all seven settings; removing only
`self.fast_dotloop = prev.fast_dotloop;` fails the table on that row and
the audit on `fast_dotloop`. Restored, all pass.
Default output is unchanged: every carried value is a selector or an
output override, and at the defaults the carried state equals `new`'s.
Docs: ppu-2c02.md and apu-2a03.md gain a "settings across a power cycle"
note; frontend.md's Power Cycle and power-on movie paragraphs are
corrected; CHANGELOG [Unreleased] Fixed.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014qfTKi2M3swo7qnwvYCkDj
`Nes::mapper_id` returned `self.bus.mapper_debug_info().mapper_id`, the
mapper's DEBUGGER view. `Mapper::debug_info`'s default names mapper 0, and
only boards that override it name their own id, so UxROM (2), CNROM (3),
AxROM (7) and every other board without an override (a `grep -L "fn
debug_info"` over `m0*.rs` alone lists 20 files) reported mapper 0. The NSF
mapper has no override either, so an NSF reported 0 rather than the 31 its
synthetic `Cartridge` carries.
Consumers, all of which want the real id and none of which relied on the
0: the Lua `cart:mapper_id()` (mlua backend), the ROM-info panel (which
compares the active id against the game database's and so flagged a
mismatch on every such board), the mobile `RomInfo`, and
`Nes::expansion_audio_chip`, which used the same debug-view read. The
debugger's mapper panel reads `mapper_info()` directly and showed
"Mapper 0" for the same boards.
Fix:
- `Nes::mapper_id` reads `self.bus.cart.mapper_id` (now `const`): the id
after any load-time header correction, 20 for an FDS image, 31 for NSF.
- `SystemBus::mapper_debug_info` sets `info.mapper_id = cart.mapper_id`
alongside the cartridge metadata it already fills (submapper, tier,
sizes), so the debugger view agrees.
- `Nes::expansion_audio_chip` matches on `self.mapper_id()`. Checked: every
board that overrides `mix_audio` (MMC5 5, N163 19, VRC6 24/26, FDS 20,
5B 69, VRC7 85) already named its id in `debug_info`, and NSF falls to
the generic label under both 0 and 31, so no label changes.
- New `Nes::submapper()` reads the NES 2.0 submapper the same way.
Test: `mapper_id_reports_the_cartridge_mapper_on_every_board` builds NES
2.0 images for mappers 0, 2, 3, 7 and 4 with submappers and asserts
`mapper_id`, `submapper` and `mapper_info().mapper_id`. Red before the fix
("mapper 2: left 0, right 2"). Mutation: dropping the
`info.mapper_id = cart.mapper_id;` line fails the view assertion
("mapper 2 view"); restored, it passes. The NROM-based Lua test
(`cart_queries_report_metadata`, id=0) and the MMC3 probe (4) are
unaffected. No emulation path reads the id this changes.
Docs: scripting.md's `cart:mapper_id()` row; CHANGELOG [Unreleased] Fixed.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014qfTKi2M3swo7qnwvYCkDj
The desktop has two ROM load paths. `load_rom_from_path` (the menu's Open
ROM, drag-and-drop and File > Recent all call it) asked
`build_dual_cabinet` and installed a Vs. DualSystem image as the
two-console cabinet (`EmuCore::set_dual`). The startup path -- a ROM given
on the command line: `App::new` -> `resumed` -> `start_nes` ->
`finish_start_nes` -- never asked, and installed every image with
`set_nes`, so `rustynes balonfgt.nes` ran the main CPU alone and could not
complete the boot handshake with the sub. The browser's `install_nes_wasm`
asked, with its own copy of the FDS exclusion.
Mechanism. One decision, `App::cabinet_for_image(&self, nes, bytes,
sample_rate)`: NSF (native) and FDS images return `None` by their magic,
everything else goes to the existing `build_dual_cabinet` (which applies
the Vs. database, the game-database correction and `configure_console` to
both consoles). All three load paths call it:
- `load_rom_from_path`: replaces its inline NSF/FDS check + call.
- `install_nes_wasm`: replaces its inline FDS check + call.
- `finish_start_nes` (native): now takes `sample_rate` from `start_nes`
(all three call sites pass it), asks for a cabinet first, and mirrors the
menu path: a cabinet's consoles get no cheats (`raw_cheats` stays as it
was), `dual_mode` is set for the present branch, `present_fb_sub` is
cleared, and the cabinet is installed with `set_dual` in place of
`set_nes`. The probe console still supplies the frame duration and is
otherwise discarded, as on the menu path. The battery attach runs the
same way for both.
Test: there is no behavioural seam -- these functions need an event loop,
a window and `&App`, which no unit test constructs -- so, following the
existing `every_load_path_configures_the_console_before_installing_it`
pattern (production half only, test module cut off),
`every_load_path_installs_a_dual_system_cabinet` asserts that each of the
three load functions asks `self.cabinet_for_image(` before a `.set_dual(`,
that `open_rom_dialog` and `start_nes` still route through them, and that
`self.build_dual_cabinet(` is called exactly once in production code, from
`cabinet_for_image`. Red before the change ("load_rom_from_path: does not
ask `cabinet_for_image`"). Mutation: replacing the `finish_start_nes`
decision with `None` fails it ("finish_start_nes: does not ask
`cabinet_for_image`"); restored, it passes.
Not changed: the drag-and-drop and Recent paths already reached the
cabinet through `load_rom_from_path`; a cabinet is still excluded from
rewind, run-ahead, netplay, TAS and the debugger (ADR 0032).
Gates for this commit: both wasm clippy gates clean.
Docs: frontend.md's DualSystem paragraph names the one decision and the
three paths; CHANGELOG [Unreleased] Fixed.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Found while pinning the Vs. DualSystem power cycle against a fresh cabinet:
even a single `Nes` that had polled `$4016`/`$4017` did not equal a fresh
boot after `power_cycle`. The snapshots differed at byte 2133, the bus
section's `port_read_cycle: [u64; 2]` -- 20 for port 0 on the cycled
console, `u64::MAX` (never read) on the fresh one.
`port_read_cycle` holds the bus cycle of each port's last read, for the
v2.6.5 CLK-run model (`port_continues_run`: a read continues a run when
`cycle == last + 1`). `SystemBus::power_cycle` resets `self.cycle` to 0
and resets the controllers, the Four Score chain and the deferred strobe
write, but left these stamps from the old timeline. So the cycled state
depended on how long the console had run -- breaking the `power_cycle ==
fresh boot` equivalence the netplay determinism tests rely on (peers
power-cycle at session start from different states) -- and a stale stamp
could, in principle, match the new clock and make the first read of the new
boot count as continuing a run.
Fix: `self.port_read_cycle = [u64::MAX; 2];` in `power_cycle`, beside the
controller-write reset, matching `with_sample_rate`'s initial value. The
edit is in `power_cycle`, outside bus.rs's disclosed TriCNES regions (the
OAM-DMA register-window read and the DMA state).
Test: `a_power_cycle_after_controller_reads_is_a_fresh_boot` runs a
synthetic NROM looping `LDA $4016 / LDA $4017` for three frames (the
first `run_frame` on a fresh console ends after the 8 reset cycles, so one
frame does not reach the loop -- measured), asserts the premise that port
0 has a stamp, power-cycles and compares the full snapshot with a fresh
console's. Red before the fix ("a power-cycled console must equal a fresh
one"), green after; reverting the one line is the mutation.
No golden or test ROM power-cycles mid-run, so no recorded output changes.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Two defects, one in the core and one on the desktop.
Desktop. `App::do_power_cycle` acted on `emu.nes` only. A cabinet lives in
`emu.dual`, and `EmuCore::set_dual` empties `emu.nes`, so with a cabinet
loaded F3 and Emulation > Power Cycle did nothing to the emulator (the
timeline and lag tally were cleared, the consoles kept running).
Core. Cycling a cabinet's consoles one by one -- the only primitive there
was, and what the mobile bridge's fallback did -- is not a cabinet power
cycle. `Nes::power_cycle` rebuilds each mapper from the ROM, which drops the
wiring `VsDualSystem::from_pair` installed on it: the sub's second-half
PRG / upper-CHR banking (`set_vs_dual_sub`) and both consoles' shared 2 KiB
WRAM window (`enable_vs_dual_wram`). The sub then runs the MAIN program,
fails its `$4016` identity check, and the boot handshake never completes.
Fix:
- `VsDualSystem::power_cycle()` power-cycles both consoles, empties the
two scratch buffers, and re-runs the cabinet wiring. The wiring moved out
of `from_pair` into a private `wire()` that both call, so a power-cycled
cabinet is wired by the same code as a new one. The moved block's
comments, which cite Mesen2's `VsControlManager::Reset` and MAME's
`.share("nvram")` as behavioural references, are carried verbatim; this
file has no `// Provenance:` header and the change adds no derived code.
- `do_power_cycle`: when `emu.dual` holds a cabinet, call
`dual.power_cycle()`, then for each console (via `split_mut().into()`,
as `build_dual_cabinet` does) `configure_console(&self.config, console)`
and then a running movie's `held_options().apply(console)` -- the same
path and precedence as the single console above it. Cheats and debugger
telemetry have no cabinet counterpart (ADR 0032).
- Mobile: the `power_cycle` fallback (taken only if rebuilding the cabinet
from its ROM fails) now calls `VsDualSystem::power_cycle` instead of
cycling the consoles one by one; its rustdoc says so.
Tests:
- Core, `a_cabinet_power_cycle_is_a_fresh_cabinet` (vs_dualsystem_synth):
run the synthetic handshake cart, power-cycle, compare the RVSD snapshot
with a freshly built cabinet's, then run five frames and require all
five protocol markers on both consoles. Red with the naive
implementation (cycle main, cycle sub): "a power-cycled cabinet must
equal a freshly built one". It stayed red after the re-wiring until the
separate port-stamp fix (previous commit), which was the remaining
difference (byte 2133 of each console). Green now.
- Desktop, `the_power_cycle_cycles_and_configures_a_dual_system_cabinet`
(source shape, `do_power_cycle` needs `&mut App`): `dual.power_cycle();`
precedes `configure_console(&self.config, console)` precedes
`emu.movie.held_options()`, and no `main_mut()/sub_mut().power_cycle()`.
Red before ("a cabinet is not power-cycled"); mutation removing
`dual.power_cycle();` fails it the same way; restored, it passes.
Found, not changed: the mobile bridge's PRIMARY cabinet path rebuilds the
cabinet from its ROM, which drops battery-backed RAM (a Power Cycle has
kept it since v2.9.0 on a single console) and any loaded palette.
`VsDualSystem::power_cycle` would keep both; switching is a mobile-scope
decision, and the Swift / Kotlin sides are unverified on a device.
Gates for this commit: frontend clippy (native, both wasm targets) and
mobile clippy clean.
Docs: frontend.md Power Cycle paragraph; CHANGELOG [Unreleased] Fixed.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
A documentation pass over the root files and the docs the release changed,
done before the cut so the cut commit stays a version bump.
- CHANGELOG [Unreleased]:
- a "Breaking changes at a glance" lead, which says why the breaks ship as
v2.9.8 (maintainer: preparation for v3.0.0);
- two Added entries that were missing: the MiSTer core's new menu options,
and the real-game coverage work (44 dumps pinned, 748 frames reviewed).
- README:
- the Roadmap no longer says v3.0.0 will remove the 2.7.5 APIs; those breaks
landed in v2.9.8;
- the netplay and save-state highlights say what movies and peers now carry;
- the Cartridges list gains the every-platform header corrections, the
header-independent save identity, and the optional Famicom console model.
- The "Current release" paragraph is left to the cut, because
release_anchor_audit pins it to the Cargo version.
- AGENTS.md:
- It called v2.0.0 the project's "single designated breaking release" in
three places. It now records both breaks: v2.0.0, and v2.9.8 carrying
v3.0.0's breaks early. It notes that v3.0.0's notes must restate them.
- The docs/agents index counts were stale: measurement-discipline 20 -> 21,
tooling-traps 13 -> 19, dependencies 2 -> 3.
- docs/agents/tooling-traps.md, six v2.9.8 traps:
- a gitignore pattern with a trailing slash does not match a symlink;
- the shared, unconfigurable frame-dump directory;
- KNOWN_BLANK's ratchet misreporting after a hash change;
- zsh `set -- $var` not splitting;
- a 1Password outage blocking cherry-picks;
- telling parallel agents when the tip moves.
- docs/ppu-2c02.md still described the bus-side /NMI edge detector running in
`tick_one_cpu_cycle`, both removed by ADR 0042. It now states the current
path: the CPU samples `nmi_level` once per cycle and edge-detects it itself
(`ppu_vbl_nmi_07_nmi_on_timing` pins it). The history is kept as one
sentence.
- docs/pixel-provenance.md said the movie format "stays at 2", which read as
current. It is now past tense, with a pointer to format 3 (ADR 0044).
- .gitignore: `/tests/roms/external` without the trailing slash, so the
symlink each agent worktree creates is ignored. Verified with
`git check-ignore -v` on a real symlink in a scratch repo.
Checked and left alone: the hits on removed API names in historical records
(release notes, VERSION-PLAN rows, STATUS history, performance records) are
history and stay as written. docs/apu-2a03.md, docs/cartridge-format.md and
docs/frontend.md already described the removals correctly. markdownlint
passes on every edited file.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The game database's PAL region now reaches iNES 1.0 dumps (0b50244b). Two committed screenshots were taken while these games ran at NTSC timing: - Pin Bot (Europe).png showed the garbled title of the (E) dump, its CHR-RAM upload overrunning NTSC vblank. It now shows the clean Rare title. - Sidewinder (Sachen).png showed the game frozen. It now shows the title screen. Both are the final frame of the full external_coverage sweep on 734edd40, looked at before copying. Their .snap baselines were re-blessed, with the change attributed, in 0b50244b. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…paign Plan items 1-3: re-measure the leads that the pre-v2.9.1 ab_check.sh had "measured" with the reference binary on both sides. Eleven candidates were each rebuilt and measured twice with the fixed tool, every run started on a quiet host (one-minute load under 1.5). The full table, including the rejections, is in docs/performance.md. Three pass the evidence rule: reproduced, p < 0.05, consistent sign, a clean order-bias control, and a shipped `_fast` path moves. - D3, crates/rustynes-apu/src/apu.rs: the per-cycle mix compared six `f32`s against CHANNEL_GAIN_UNITY on every CPU cycle. A cached `gain_is_unity` flag, recomputed by `set_channel_gain` (the only writer of the gain), replaces the comparison. -3.1% to -4.5% on all four frame workloads, both runs; control drift at most 1.1%. - G3, crates/rustynes-ppu/src/ppu.rs: `tick_sprite_eval_per_dot` computed `next_line` and `sprite_height` on every dot, and they are dead on 149 of 341. They now sit in the `65..=256` arm, their only use. -1.0% to -3.1% on all four, both runs. This reverses a recorded verdict: v2.3.1 measured G3 as "no change" with the defective tool, and a comment forbade re-trying it. The comment now says what happened and cites the new measurement. The function is inside ppu.rs's disclosed Mesen2-derived sprite-evaluation region; this is a move of our own existing code within it, so the header and the provenance record are unchanged and no new expression is introduced. - D1, crates/rustynes-apu/src/apu.rs: `dmc_tick_end` read `self.dmc.bits_remaining()` twice in a row. It now reads it once. -1.2% on the shipped palette path, -2.5% to -3.0% on exact `nestest`, both runs. Shipped `nestest` does not move. A bug caught while applying D3, before it shipped. `Apu::adopt_settings_from`, the power-cycle carry added earlier in this release (05ceec50), assigned `channel_gain` directly. With D3 in place the fresh APU's cached `true` would have survived, and the mix would have ignored a non-unity gain after every Power Cycle. It now goes through `set_channel_gain`. `a_power_cycle_keeps_a_non_unity_gain_audible` checks two things: the flag, and that a power-cycled APU's samples equal a directly configured APU's. Mutation: putting the field copy back fails the test; restoring the fix passes it. `snapshot_schema_audit` classifies the new field as derived. Rejected: the palette read view (shipped `nestest_fast` +0.2* / -3.0 / +1.0 over three runs, mixed sign) and G2 `#[repr(C)]` (mixed sign). Ceilings with room for later correct candidates: G9, D4, D2. Verification: `cargo test --release --workspace --features test-roms,commercial-roms --no-fail-fast` gives 3,358 passed, 0 failed, 19 ignored (one more than before: the new test). That includes AccuracyCoin 144/144, nestest 0-diff, every golden and every commercial suite with the coverage sweep. No snapshot or golden changed. fmt and workspace clippy are clean. The three together are measured as one A/B next, against this commit's parent. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…s not add up docs/performance.md gains three v2.9.8 sections: - The /NMI edge detector's removal (63850aa7), measured on its own against its parent in two runs: -4.9% to -6.1% on three workloads and -1.4% on exact-path nestest. Run 2's order-bias control drifted at most 1.8%. This agrees with v2.9.1's ceiling probe and with ADR 0042's estimate. The section's older 'to be measured' sentence now points here. - The 11-candidate campaign. Each candidate was measured twice on a quiet host, correct candidates and ceilings tabulated, with the max control drift and a verdict per row. D3, G3 and D1 are adopted (599d5ade). The palette read view and G2 are rejected for mixed sign. The G9, D4 and D2 ceilings show room. G4, G5 and G6 are not interpretable, each for a stated reason. The three adopted together measure -1.8% to -4.3% on all four workloads, in two runs. Every figure was checked against the raw run logs; a first draft had misfiled G4's +8.9% candidate number as control drift. - The release end to end against v2.9.7. Only the shipped nestest_fast path is established (-5.5% to -8.7%, both runs). Run 1's gains on the other three workloads did not reproduce in run 2. Since two steps each measured clean gains on every workload, something else in the release offsets them there. It is recorded as found, not investigated, with the bisect to run in v2.9.9. No release-wide speed-up is claimed beyond the established path. The CHANGELOG gains a matching Changed entry. The v2.9.8 release notes' PERF placeholder becomes the same four plain statements. The significance markers in the new tables are escaped (markdownlint read them as emphasis). Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Moves the workspace, the excluded co-simulation crate and the libretro .info to 2.9.8. Every release anchor that names the current version is rewritten by scripts/release-automation/bump_release.py (dry run, then --apply), with a lead naming what the release did. Cargo.lock changes only its workspace version lines. Codename. The plan chose "Cadence", but v2.3.3 already owns that name. The maintainer chose "Vanguard" at the cut (2026-10-02), as v2.7.5 was once renamed away from "Ledger". The plan file is renamed to v2.9.8-vanguard-plan.md and records why. Every v2.9.8 mention is updated; v2.3.3's own mentions are untouched. The branch keeps its name, feat/v2.9.8-cadence. Hand edits the script asks for, by design: - VERSION-PLAN gains the v2.9.8 row (current), v2.9.7 is demoted, and v2.9.8 leaves "Planned next". - The to-dos/ROADMAP.md release chain gets its v2.9.8 entry, and v2.9.7 loses "the current release". - The root ROADMAP.md "Project Status" line is rewritten. The script had kept v2.9.7's description under v2.9.8's name, which is exactly the defect its own docstring describes. - docs/STATUS.md's top-line count moves to v2.9.8, and its ignored-test catalogue drops the 7 deleted scheduler pins: 14 ignored, 1 of them the emphasis table's generator. - to-dos/plans/README.md gains the v2.9.8 row, and the line plan's status moves. CHANGELOG: [Unreleased] became the dated [2.9.8] section, with a lead and a Verification section; an empty [Unreleased] stays above it. The rebase onto #578 (81c09c2, the iOS app fix) put that PR's [Unreleased] entries into this section, because #578 ships in v2.9.8. The section was rebuilt from the verified pre-rebase text plus those six entries. A paragraph-union resolver had left a stale intermediate sentence and broken blank lines across 25 conflict resolutions, so it was not trusted. The release notes (.github/release-notes/v2.9.8.md) open with the breaking changes, gain an "On iOS" section for #578, and carry the measured performance. The bitstream line is filled in at attach time, because the datecoded name and the md5 follow the merge date. Verification on this tree, after the rebase: - cargo test --release --workspace --features test-roms --no-fail-fast: 3,139 passed, 0 failed, 14 ignored (v2.9.7: 3,065 / 0 / 20). That includes the release audits (release_anchor_audit and the rest), AccuracyCoin 144/144 and nestest 0-diff. - With commercial-roms, before the rebase: 3,358 / 0 / 19, the full external_coverage sweep included. The rebase changed only #578's files (iOS Swift, CI workflows, an AccuracyCoin README rename, one test comment), verified by diffing the branch against its pre-rebase backup. - fmt, clippy for every feature set and both wasm builds, rustdoc, the no_std build and markdownlint were clean on the integrated tree. - MiSTer: ladders 175/0/1 on-die and 176/0/1 off-die; both builds re-swept, seed 2 kept, two clean compiles of each byte-identical. No hardware has run any bitstream. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
|
@coderabbitai review |
|
Important
This repository does not receive automatic reviews because it has fewer than 10 stars. ⚙️ Run configuration
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. Comment |
|
Antigravity review (Gemini via Ultra)This PR prepares the codebase for v3.0.0 by preemptively introducing its planned breaking changes to the public API, save/movie formats, and ROM identity logic, alongside comprehensive rendering bugfixes and new MiSTer core menu options. Blocking issues
Suggestions
Nitpicks
Automated first-pass review by Earlier review rounds (newest first)Round reviewed at 2026-10-03 09:47 UTCAntigravity review (Gemini via Ultra)This PR prepares for v3.0.0 by landing breaking API and on-disk format changes early, fixing rendering defects across the game library, and adding new display options to the MiSTer core menu. Blocking issues
Suggestions
Nitpicks
Automated first-pass review by Round reviewed at 2026-10-03 08:10 UTCAntigravity review (Gemini via Ultra)This PR implements breaking API and format changes ahead of the v3.0.0 release (renaming Blocking issues
Suggestions
Nitpicks
Automated first-pass review by Round reviewed at 2026-10-03 06:41 UTCAntigravity review (Gemini via Ultra)This PR introduces Blocking issues
Suggestions
Nitpicks
Automated first-pass review by |
There was a problem hiding this comment.
Copilot review overview
🔵 Needs a closer look
One or more issues must be addressed before approval.
Review effort: Balanced
Findings: None
What changed in this PR
Updates Vanguard’s documentation, release metadata, regression baselines, and API callers to support the early compatibility breaks and cross-platform database corrections.
Changes:
- Document API migration, configuration-aware netplay, database corrections, and provenance.
- Refresh visual/commercial regression baselines and remove unsupported-dump baselines.
- Update versions, adapt callers, and explicitly reject unsupported Switch builds.
| File | Description |
|---|---|
| to-dos/DEFERRED-AND-CARRYOVER-FEATURES.md | Updated as part of this pull request. |
| screenshots/mmc1_a12/README.md | Updated as part of this pull request. |
| screenshots/m22/README.md | Updated as part of this pull request. |
| screenshots/besteffort/README.md | Updated as part of this pull request. |
| NOTICE | Updated as part of this pull request. |
| docs/ppu-trace-tooling.md | Updated as part of this pull request. |
| docs/netplay-webrtc.md | Updated as part of this pull request. |
| docs/libretro/implementation_specifics.md | Updated as part of this pull request. |
| docs/ios.md | Updated as part of this pull request. |
| docs/benchmarks.md | Updated as part of this pull request. |
| docs/architecture.md | Updated as part of this pull request. |
| docs/android.md | Updated as part of this pull request. |
| docs/adr/0043-v3-is-the-api-major-and-a-release-candidate-core.md | Updated as part of this pull request. |
| crates/rustynes-test-harness/tests/v21_coverage_mappers.rs | Updated as part of this pull request. |
| crates/rustynes-test-harness/tests/terminus_control.rs | Updated as part of this pull request. |
| crates/rustynes-test-harness/tests/snapshots/visual_regression__full_palette_frame_60.snap | Updated as part of this pull request. |
| crates/rustynes-test-harness/tests/snapshots/visual_regression__full_palette_frame_180.snap | Updated as part of this pull request. |
| crates/rustynes-test-harness/tests/snapshots/visual_regression__flowing_palette_frame_60.snap | Updated as part of this pull request. |
| crates/rustynes-test-harness/tests/snapshots/visual_regression__flowing_palette_frame_300.snap | Updated as part of this pull request. |
| crates/rustynes-test-harness/tests/snapshots/visual_regression__flowing_palette_frame_180.snap | Updated as part of this pull request. |
| crates/rustynes-test-harness/tests/snapshots/m22__m22_vrc2a_chr_banking_0_127.snap | Updated as part of this pull request. |
| crates/rustynes-test-harness/tests/snapshots/external_real_games__external_nrom_ice_climber.snap | Updated as part of this pull request. |
| crates/rustynes-test-harness/tests/snapshots/external_real_games__external_mmc1_kid_icarus.snap | Updated as part of this pull request. |
| crates/rustynes-test-harness/tests/snapshots/external_extended__extended_taito48_don_doko_don_2.snap | Updated as part of this pull request. |
| crates/rustynes-test-harness/tests/snapshots/external_extended__extended_mmc3_felix_the_cat.snap | Updated as part of this pull request. |
| crates/rustynes-test-harness/tests/snapshots/external_extended__extended_m72_doraemon_world_3_by_kiku.snap | Updated as part of this pull request. |
| crates/rustynes-test-harness/tests/snapshots/external_extended__extended_m244_asmik_kun_land_t1.snap | Updated as part of this pull request. |
| crates/rustynes-test-harness/tests/snapshots/external_extended__extended_m226_76_in_1_p1.snap | Updated as part of this pull request. |
| crates/rustynes-test-harness/tests/snapshots/external_extended__extended_m221_1000_in_1.snap | Updated as part of this pull request. |
| crates/rustynes-test-harness/tests/snapshots/external_extended__extended_m218_magic_floor_by_martin_korth.snap | Updated as part of this pull request. |
| crates/rustynes-test-harness/tests/snapshots/external_extended__extended_m150_auto_upturn.snap | Updated as part of this pull request. |
| crates/rustynes-test-harness/tests/snapshots/external_extended__extended_m147_challenge_of_the_dragon.snap | Updated as part of this pull request. |
| crates/rustynes-test-harness/tests/snapshots/external_extended__extended_m145_sidewinder.snap | Updated as part of this pull request. |
| crates/rustynes-test-harness/tests/snapshots/external_extended__extended_m143_dancing_blocks.snap | Updated as part of this pull request. |
| crates/rustynes-test-harness/tests/snapshots/external_extended__extended_m138_silver_eagle.snap | Updated as part of this pull request. |
| crates/rustynes-test-harness/tests/snapshots/external_extended__extended_m137_great_wall_the.snap | Updated as part of this pull request. |
| crates/rustynes-test-harness/tests/snapshots/external_extended__extended_m112_chik_bik_ji_jin_saam_gwok_ji.snap | Updated as part of this pull request. |
| crates/rustynes-test-harness/tests/snapshots/external_coverage__mapper_249_WaixingT9552_Shui_Hu_Zhuan_Ch.snap | Updated as part of this pull request. |
| crates/rustynes-test-harness/tests/snapshots/external_coverage__mapper_249_WaixingT9552_San_Shi_Liu_Ji_Ch.snap | Updated as part of this pull request. |
| crates/rustynes-test-harness/tests/snapshots/external_coverage__mapper_249_WaixingT9552_Myth_Struggle_Ch.snap | Updated as part of this pull request. |
| crates/rustynes-test-harness/tests/snapshots/external_coverage__mapper_244_Decathlon_Asmik_kun_Land_J_t1.snap | Updated as part of this pull request. |
| crates/rustynes-test-harness/tests/snapshots/external_coverage__mapper_228_Action52_Cheetahmen_II_USA_Unl.snap | Updated as part of this pull request. |
| crates/rustynes-test-harness/tests/snapshots/external_coverage__mapper_228_Action52_Cheetah_Men_II_Active_Enterprises_p.snap | Updated as part of this pull request. |
| crates/rustynes-test-harness/tests/snapshots/external_coverage__mapper_228_Action52_Action_52_Active_Enterprises.snap | Updated as part of this pull request. |
| crates/rustynes-test-harness/tests/snapshots/external_coverage__mapper_226_BMC_76in1_Super_42_in_1_22_Games_p1.snap | Updated as part of this pull request. |
| crates/rustynes-test-harness/tests/snapshots/external_coverage__mapper_226_BMC_76in1_76_in_1_p1.snap | Updated as part of this pull request. |
| crates/rustynes-test-harness/tests/snapshots/external_coverage__mapper_218_MagicFloor_Magic_Floor_by_Martin_Korth_2012_PC10_Version_PD.snap | Updated as part of this pull request. |
| crates/rustynes-test-harness/tests/snapshots/external_coverage__mapper_218_MagicFloor_Magic_Floor_by_Martin_Korth_2012_PC10_Version_PD_a1.snap | Updated as part of this pull request. |
| crates/rustynes-test-harness/tests/snapshots/external_coverage__mapper_191_Waixing191_Mighty_Final_Fight_J_T_Chi_madcell.snap | Updated as part of this pull request. |
| crates/rustynes-test-harness/tests/snapshots/external_coverage__mapper_177_Hengedianzi_Mei_Guo_Fu_Hao_American_Man_Asia_Unl.snap | Updated as part of this pull request. |
| crates/rustynes-test-harness/tests/snapshots/external_coverage__mapper_163_NanjingFC001_Yu_Gi_Oh_NJ032_Ch.snap | Updated as part of this pull request. |
| crates/rustynes-test-harness/tests/snapshots/external_coverage__mapper_163_NanjingFC001_Qi_Guo_Da_Zhan_NJ029_Ch.snap | Updated as part of this pull request. |
| crates/rustynes-test-harness/tests/snapshots/external_coverage__mapper_163_NanjingFC001_Ne_Zha_Chuan_Qi_NJ036_Ch.snap | Updated as part of this pull request. |
| crates/rustynes-test-harness/tests/snapshots/external_coverage__mapper_163_NanjingFC001_Hu_Lu_Jin_Gang_NJ039_Ch.snap | Updated as part of this pull request. |
| crates/rustynes-test-harness/tests/snapshots/external_coverage__mapper_163_NanjingFC001_Han_Liu_Bang_NJ013_Ch.snap | Updated as part of this pull request. |
| crates/rustynes-test-harness/tests/snapshots/external_coverage__mapper_152_Bandai74161_Saint_Seiya_Ougon_Densetsu_Japan_zip.snap | Updated as part of this pull request. |
| crates/rustynes-test-harness/tests/snapshots/external_coverage__mapper_152_Bandai74161_Gegege_no_Kitarou_2_Youkai_Gundan_no_Chousen_Japan_zip.snap | Updated as part of this pull request. |
| crates/rustynes-test-harness/tests/snapshots/external_coverage__mapper_151_KonamiVS_Goonies_The_VS_zip.snap | Updated as part of this pull request. |
| crates/rustynes-test-harness/tests/snapshots/external_coverage__mapper_150_Sachen74LS374N_Lightgun_Game_2_in_1_Cosmocop_Cyber_Monster_Asia_Ja_Unl_zip.snap | Updated as part of this pull request. |
| crates/rustynes-test-harness/tests/snapshots/external_coverage__mapper_150_Sachen74LS374N_Happy_Pairs_Asia_Ja_PAL_Unl_zip.snap | Updated as part of this pull request. |
| crates/rustynes-test-harness/tests/snapshots/external_coverage__mapper_150_Sachen74LS374N_Auto_Upturn_Asia_Ja_PAL_Unl_zip.snap | Updated as part of this pull request. |
| crates/rustynes-test-harness/tests/snapshots/external_coverage__mapper_148_SachenSA0037_Mahjong_World_Sachen.snap | Updated as part of this pull request. |
| crates/rustynes-test-harness/tests/snapshots/external_coverage__mapper_147_Sachen3018_JV001_Challenge_of_the_Dragon_Sachen.snap | Updated as part of this pull request. |
| crates/rustynes-test-harness/tests/snapshots/external_coverage__mapper_147_Sachen3018_JV001_Challenge_of_the_Dragon_Sachen_zip.snap | Updated as part of this pull request. |
| crates/rustynes-test-harness/tests/snapshots/external_coverage__mapper_146_Sachen_NINA_Twin_Eagle_Sachen.snap | Updated as part of this pull request. |
| crates/rustynes-test-harness/tests/snapshots/external_coverage__mapper_146_Sachen_NINA_Pyramid_II_Sachen.snap | Updated as part of this pull request. |
| crates/rustynes-test-harness/tests/snapshots/external_coverage__mapper_146_Sachen_NINA_Millionaire_Sachen.snap | Updated as part of this pull request. |
| crates/rustynes-test-harness/tests/snapshots/external_coverage__mapper_145_SachenSA72007_Sidewinder_Sachen.snap | Updated as part of this pull request. |
| crates/rustynes-test-harness/tests/snapshots/external_coverage__mapper_145_SachenSA72007_Sidewinder_Sachen_zip.snap | Updated as part of this pull request. |
| crates/rustynes-test-harness/tests/snapshots/external_coverage__mapper_143_SachenTCA01_Dancing_Blocks_Sachen.snap | Updated as part of this pull request. |
| crates/rustynes-test-harness/tests/snapshots/external_coverage__mapper_140_Jaleco140_Youkai_Club_Japan_zip.snap | Updated as part of this pull request. |
| crates/rustynes-test-harness/tests/snapshots/external_coverage__mapper_138_Sachen8259B_Silver_Eagle_Sachen_zip.snap | Updated as part of this pull request. |
| crates/rustynes-test-harness/tests/snapshots/external_coverage__mapper_137_Sachen8259D_Great_Wall_The_Sachen.snap | Updated as part of this pull request. |
| crates/rustynes-test-harness/tests/snapshots/external_coverage__mapper_121_KashengA9711_Mortal_Kombat_6_Unl.snap | Updated as part of this pull request. |
| crates/rustynes-test-harness/tests/snapshots/external_coverage__mapper_119_TQROM_Pin_Bot_E.snap | Updated as part of this pull request. |
| crates/rustynes-test-harness/tests/snapshots/external_coverage__mapper_119_TQROM_High_Speed_E.snap | Updated as part of this pull request. |
| crates/rustynes-test-harness/tests/snapshots/external_coverage__mapper_113_NINA006_113_Funblaster_Pak_Australia_Unl_zip.snap | Updated as part of this pull request. |
| crates/rustynes-test-harness/tests/snapshots/external_coverage__mapper_105_NES_EVENT_Nintendo_World_Championships_1990_U.snap | Updated as part of this pull request. |
| crates/rustynes-test-harness/tests/snapshots/external_coverage__mapper_091_JYCompany91_Super_Mario_Kart_Rider_Unl.snap | Updated as part of this pull request. |
| crates/rustynes-test-harness/tests/snapshots/external_coverage__mapper_091_JYCompany91_Street_Fighter_III_18_Fighter_Unl.snap | Updated as part of this pull request. |
| crates/rustynes-test-harness/tests/snapshots/external_coverage__mapper_091_JYCompany91_Mortal_Kombat_II_Unl.snap | Updated as part of this pull request. |
| crates/rustynes-test-harness/tests/snapshots/external_coverage__mapper_091_JYCompany91_Mario_Rider_Unl.snap | Updated as part of this pull request. |
| crates/rustynes-test-harness/tests/snapshots/external_coverage__mapper_091_JYCompany91_Dragon_Ball_Z_Super_Butouden_2_Unl.snap | Updated as part of this pull request. |
| crates/rustynes-test-harness/tests/snapshots/external_coverage__mapper_083_Cony_Yoko_World_Heroes_2_Unl.snap | Updated as part of this pull request. |
| crates/rustynes-test-harness/tests/snapshots/external_coverage__mapper_083_Cony_Yoko_Street_Fighter_X_Turbo_40_Ch.snap | Updated as part of this pull request. |
| crates/rustynes-test-harness/tests/snapshots/external_coverage__mapper_083_Cony_Yoko_Street_Blaster_II_Pro_Unl.snap | Updated as part of this pull request. |
| crates/rustynes-test-harness/tests/snapshots/external_coverage__mapper_083_Cony_Yoko_Fatal_Fury_2_Unl.snap | Updated as part of this pull request. |
| crates/rustynes-test-harness/tests/snapshots/external_coverage__mapper_074_Waixing43_393_Young_Chivalry_Ch.snap | Updated as part of this pull request. |
| crates/rustynes-test-harness/tests/snapshots/external_coverage__mapper_074_Waixing43_393_Ji_Jia_Zhan_Shi_Ch.snap | Updated as part of this pull request. |
| crates/rustynes-test-harness/tests/snapshots/external_coverage__mapper_074_Waixing43_393_Feng_Yun_Ch.snap | Updated as part of this pull request. |
| crates/rustynes-test-harness/tests/snapshots/external_coverage__mapper_072_Jaleco72_Doraemon_World_3_by_Kiku_Doraemon_Hack_zip.snap | Updated as part of this pull request. |
| crates/rustynes-test-harness/tests/snapshots/external_coverage__mapper_066_GxROM_Gumshoe_zip.snap | Updated as part of this pull request. |
| crates/rustynes-test-harness/tests/snapshots/external_coverage__mapper_064_TengenRAMBO1_Balloon_Fight_J_T_Rus_Mario_Soft_zip.snap | Updated as part of this pull request. |
| crates/rustynes-test-harness/tests/snapshots/external_coverage__mapper_048_TaitoTC0690_Bakushou_Jinsei_Gekijou_3_Japan.snap | Updated as part of this pull request. |
| crates/rustynes-test-harness/tests/snapshots/external_coverage__mapper_045_GA23C_Super_3_in_1_p1.snap | Updated as part of this pull request. |
| crates/rustynes-test-harness/tests/snapshots/external_coverage__mapper_045_GA23C_Famicom_Yarou_54_Unl.snap | Updated as part of this pull request. |
| crates/rustynes-test-harness/tests/snapshots/external_coverage__mapper_024_VRC6_Pulsewave_Invite_2009_08_xx_PD_zip.snap | Updated as part of this pull request. |
| crates/rustynes-test-harness/tests/snapshots/external_coverage__mapper_019_Namco163_Digital_Devil_Story_Megami_Tensei_II_Japan_Rev_A_zip.snap | Updated as part of this pull request. |
| crates/rustynes-test-harness/tests/snapshots/external_coverage__mapper_012_GouderSL5020B_Dragon_Ball_Z_Super_Ch_f1.snap | Updated as part of this pull request. |
| crates/rustynes-test-harness/tests/snapshots/external_coverage__mapper_012_GouderSL5020B_Dragon_Ball_Z_5_Ch.snap | Updated as part of this pull request. |
| crates/rustynes-test-harness/tests/snapshots/external_coverage__mapper_007_AxROM_Cobra_Triangle.snap | Updated as part of this pull request. |
| crates/rustynes-test-harness/tests/snapshots/external_coverage__mapper_004_MMC3_Felix_the_Cat.snap | Updated as part of this pull request. |
| crates/rustynes-test-harness/tests/snapshots/external_coverage__mapper_001_MMC1_Kid_Icarus.snap | Updated as part of this pull request. |
| crates/rustynes-test-harness/tests/snapshots/external_coverage__mapper_000_NROM_Ice_Climber.snap | Updated as part of this pull request. |
| crates/rustynes-test-harness/tests/one_clock_invariants.rs | Updated as part of this pull request. |
| crates/rustynes-test-harness/tests/feature_flag_audit.rs | Updated as part of this pull request. |
| crates/rustynes-test-harness/tests/external_extended.rs | Updated as part of this pull request. |
| crates/rustynes-test-harness/tests/cpu_interrupts_v2.rs | Updated as part of this pull request. |
| crates/rustynes-test-harness/tests/cosim_manifest_audit.rs | Updated as part of this pull request. |
| crates/rustynes-test-harness/src/coverage.rs | Updated as part of this pull request. |
| crates/rustynes-test-harness/src/bin/coverage_smoke.rs | Updated as part of this pull request. |
| crates/rustynes-ppu/src/raw_signal.rs | Updated as part of this pull request. |
| crates/rustynes-ppu/src/provenance.rs | Updated as part of this pull request. |
| crates/rustynes-ppu/src/lib.rs | Updated as part of this pull request. |
| crates/rustynes-netplay/tests/relay_loopback.rs | Updated as part of this pull request. |
| crates/rustynes-netplay/tests/nat_loopback.rs | Updated as part of this pull request. |
| crates/rustynes-netplay/src/lib.rs | Updated as part of this pull request. |
| crates/rustynes-mappers/src/mapper.rs | Updated as part of this pull request. |
| crates/rustynes-mappers/src/m151_konami_vs.rs | Updated as part of this pull request. |
| crates/rustynes-mappers/src/m078_irem_jaleco78.rs | Updated as part of this pull request. |
| crates/rustynes-libretro/rustynes_libretro.info | Updated as part of this pull request. |
| crates/rustynes-libretro/Makefile | Updated as part of this pull request. |
| crates/rustynes-libretro/Cargo.toml | Updated as part of this pull request. |
| crates/rustynes-frontend/src/input_macros.rs | Updated as part of this pull request. |
| crates/rustynes-cosim/Cargo.toml | Updated as part of this pull request. |
| crates/rustynes-core/src/scheduler.rs | Updated as part of this pull request. |
| crates/rustynes-core/src/legacy_movie.rs | Updated as part of this pull request. |
| crates/rustynes-core/src/genie.rs | Updated as part of this pull request. |
| crates/rustynes-apu/src/lib.rs | Updated as part of this pull request. |
| crates/rustynes-apu/benches/apu_throughput.rs | Updated as part of this pull request. |
| Cargo.toml | Updated as part of this pull request. |
| android/app/src/main/java/com/doublegate/rustynes/MainActivity.kt | Updated as part of this pull request. |
| .gitignore | Updated as part of this pull request. |
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
|
Answers to the Antigravity review of 236ce4b: Blocking: breaking changes without a major bump. Declined. This is the maintainer's decision (2026-10-01): the breaks land in v2.9.8 as preparation for v3.0.0, and the number stays v2.9.x. That decision is recorded in three places. ADR 0042 and ADR 0043 carry dated amendments. The line plan's v3.0.0 section requires v3.0.0's release notes to restate every break. The CHANGELOG Suggestion: show the differing Nitpick: Nitpick: a |
CodeRabbit refused release PR #579: it has 180 reviewable files, and the limit is 100. The maintainer chose review-only slices: #580 carries the core crates (87 files) and #581 the rest (91). Both are byte copies of this branch at 236ce4b. This commit answers all nine of #580's findings, seven inline and two outside the diff. Every behaviour fix has a test that failed before the fix. A refused movie changed the running game (movie.rs). seek_to_start applied the movie's HardwareOptions before reaching the start point. `apply` refills work RAM and palette RAM, and installs the console model, revisions, Four Score and Game Genie codes. So a movie refused afterwards still left the player's game modified: an old start state (StartStateTooOld), a malformed one, or an undecodable code (OptionNotApplicable). The mutating half is now enter_start. Before calling it, seek_to_start captures the options and a snapshot. On error it applies the player's options and then restore_quiet()s the snapshot, in that order, because `apply` writes the fills and the restore must put the game's RAM back over them. Test: a_refused_start_leaves_the_player_ untouched drives both failure paths on a player with a non-default fill and the Four Score, run for a frame. It checks capture() and snapshot() for equality, and failed first on the old-start-state path. A mapper correction of 16+ was lost on a "DiskDude!" dump (gamedb, header.rs). v2.9.8's dirty-tail rule masks mapper bits 4-7 when an iNES 1.0 header has a non-zero byte in 12-15. The two writers that set a mapper on such a header left that tail in place. The game database wrote bytes 6-7 and nothing else. For the test's mapper-2 dump corrected to 66, byte 7 already held the needed 'D' nibble, so the writer saw NO change at all and returned false. With a PAL row, ines1_as_nes2 then copied the MASKED id into a clean NES 2.0 header, and the correction was gone for good. The header editor's preserving writer copies the tail back by design. Both now zero bytes 12-15 when the new mapper is 16 or more (the gamedb one before the region step). iNES 1.0 reads nothing else there. Tests: - a_mapper_correction_survives_a_dirty_tail: with no region and with PAL, checking the mapper and the region; - preserving_mapper_edit_reads_back_over_a_dirty_tail: an edit to 66 reads back, and an edit to 3 keeps the tail. Both failed first. $4017 inhibit (frame_counter.rs). v2.9.8 moved the flag clear onto the write (for Nintendo World Championships 1990) and left irq_inhibit to the 3-4 cycle timer reset. A write 1-3 cycles before step 29828 therefore let the OLD sequence raise the IRQ again inside the delay. That is an interrupt the program had just inhibited, and a model that contradicts itself. The write now sets irq_inhibit too. Evidence, stated plainly: no ROM reaches this window. The ROM suites pass under both models (checked by running them on the change: AccuracyCoin, apu_test, blargg 2005, apu_reset, PAL, the coincidence test and nes_blargg). The change rests on the wiki, which ties the flag clear to the inhibit bit, not to the reset. docs/apu-2a03.md is updated, and the line plan adds it to the sibling's v2.9.9 parity list. Test: write_4017_inhibit_holds_through_the_reset_ delay (lead 1-3, aligned and not). Deleting the new line makes it fail. Netplay half-sync (message.rs). Protocol 5 kept v4's Sync magic "RNES", and a v4 decoder reads the magic and the ROM hash and ignores the rest. So a v4 peer could take v5's longer Sync as a match and mark the session synced, while the v5 side refused v4's short reply and timed out. SYNC_MAGIC is now "RNE5" (0x524E_4535), which every version compares, and a Sync payload must be exactly 68 bytes. Test: a_v5_sync_is_not_a_v4_sync. The netplay suite passes (92 unit tests plus the loopback integration tests). Android netplay emphasis (rustynes-mobile, Netplay.kt). The netplay path turns the index framebuffer into pixels with its own LUT. That was a Kotlin copy of the base palette and of the old 13/16 emphasis rule, which the core replaced with the documented model in this release. The copy is gone. A new UniFFI function default_palette_argb() returns the core's build_rgba_lut(Composite2C02) packed 0xAARRGGBB as i32 (Kotlin Int), and NetplayPalette.ARGB reads it lazily. A Rust test pins every entry to the core LUT. The Kotlin was NOT compiled on this host. CI's NDK + UniFFI and Gradle jobs compile it. iOS netplay ignores emphasis entirely, a documented carry-over and not a stale copy, so it is unchanged. Smaller: - ApuSnapshotError::TrailingBytes(n) for a too-long blob, which used to report "truncated at offset N". The enum is already non_exhaustive; a mutation back to Truncated fails the test. - The movie format header said 3 bytes per frame; format 3 writes 5. - Nes::restore's doc gave a reason, the header-inclusive hash tag, that v2.9.8 removed. The doc now says so and keeps the reason that still holds. - m085_vrc7's save_state comment said v1 still loads; since v2.9.8 only v3 and v4 do. CHANGELOG [2.9.8] and the v2.9.8 release notes carry the user-visible fixes. Verified on this tree: - fmt; clippy -D warnings for the workspace, scripting+hd-pack, retroachievements, full, and both wasm32 builds; rustdoc -D warnings; the no_std thumbv7em build; - cargo test --release --workspace --features test-roms: 3,145 passed, 0 failed, 14 ignored (3,139 plus the six new tests); - markdownlint on the changed documents. Not verified here: the Kotlin compile (CI) and the commercial suite (re-run separately for the records). Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
CodeRabbit's review of slice #581 (the frontend, platforms, harness, CI and docs half of this branch) left 16 threads and 3 outside-diff comments. Six cite code that lives in the OTHER slice (#580), so this review read main's versions of it. They are answered with #579's code and need no change: - AGENTS.md's format numbers. The code has FORMAT_VERSION 3, MIN_FORMAT_VERSION 3, BUS_SECTION_VERSION 2, MOVIE_FORMAT_VERSION 3. - The header editor's "unmodelled" fields. Header models all six. - movie_ui's region/board check and its options-before-seek. Both are done in seek_to_start. - The mapper 78 default. The default is CosmoCarrier, pinned by mapper_78_ines_header_selects_wiring_by_alt_nametables_bit. The rest were real. Each behaviour fix below had a test that failed first. The history viewer exported across an options change. A movie carries one set of options, and export_from took the start anchor's while including every later frame. A Four Score or overclock toggled mid- session therefore replayed under the wrong machine. record_frame now captures HardwareOptions every frame and places an anchor on the frame they change, whatever the anchor period. export_from ends the clip at the first later anchor whose options differ. The capture allocates only for Game Genie codes, and only while the viewer records. Test an_export_stops_at_an_options_change: period 1,000, toggle at frame 25; the export from 0 has 25 frames, the export from 30 starts at the change with the new options. TAStudio replayed and exported differently from its movie. - seek and record_frame applied p1/p2 only, while movie playback uses FrameInput::apply_to (four ports). A four-player log played differently in the editor than in its export. Both now use apply_to. - to_movie captured options at export time. Every cached state descends from frame 0, so the editor now captures start_options with the frame-0 state and exports those. Tests: seek_and_record_drive_all_four_ports and an_export_carries_the_frame_zero_options. The .rnmproj project format still stores players 1-2 only (its documented 3-byte record); that is unchanged and stated in the reply. Power Cycle read the .pal file under the emu lock. configure_console calls palette_for_config, which reads a .pal from disk on native. do_power_cycle called it inside self.emu.lock(), twice for a cabinet. A stalled drive or network share would stall the emulation thread on the mutex. configure_console now wraps configure_console_with_palette, and do_power_cycle resolves the palette once, before the lock. The source-shape test also asserts that order and the single read. Moving the read back under the lock fails it (checked). agy round 2 on #579: the desktop's save-state and movie errors never reached the screen. The menu dispatch set "State loaded" whatever handle_load_state did, and the F4 hotkey set no status at all. A state refused for being from v2.9.7 or earlier, which the release notes say is "refused with a message saying so", therefore looked like a successful load, and its reason went only to stderr. The same was true of an old movie's "re-record" refusal (Movie::deserialize in handle_movie_play_toggle) and of a refused seek. handle_save_state and handle_load_state now return their outcome. One helper, report_state_result, puts it on the status line at all six native sites (menu, slot menu, hotkeys, script control, save-states panel). Script saves report failures only, so a per-frame script cannot bury the status line. The movie toggle shows its parse and seek refusals. handle_movie_play_toggle takes &mut self for it. The browser's saves are asynchronous and still only log, which is unchanged and stated in the CHANGELOG. There is no automated test for the toasts: App needs a window and a GPU; clippy for every feature set checks every call site. Docs that described the code wrongly: - ADR 0044 and the CHANGELOG said power-on recording keeps the overclock. start_recording_power_on zeroes the extra scanlines before capturing the options, so recording runs at stock timing and playback applies whatever overclock a movie stores. - pixel-provenance.md still said the verifier assumes a default profile and the format has none. It now matches main.rs's notice: options applied, the header used as found. - performance.md's "row §3.1 C above" points at the v2.9.1 table below. - accuracy-ledger.md: 4 + 1 + 5 + 2 + 1 = 13 ignored tests, not 16. - libretro-disposition.md's libnx row: CLOSED at v2.9.8 (see L-3.2). - to-dos/plans/README.md: v3.0.0 is the API major with an RC core under ADR 0043, and the hardware moves to v3.x. - CHANGELOG line 121 rewrapped. coverage.py: - load_tiers warns once when the parsed tiers differ from the fallback sets. An arm the regex cannot read (a guard, a range, `_ =>`) leaves a tier short but non-empty, which is accepted silently. Counting `Some(MapperTier::...)` occurrences cannot be the check: 14 of 19 are in tier.rs's tests. - categorize's dry run now applies the duplicate test, so it predicts the FLAGGED list and exit code of a real run. Commercial suite measured on 1a66470: 3,364 passed, 0 failed, 19 ignored (3,358 + the six tests of the previous commit). This confirms STATUS.md's 3,358 baseline; the release notes' 3,357 was the wrong one. The counts are recorded after the final run of this commit's tree. Verified so far: the affected frontend tests (47 passed); the three new tests failed first; the power-cycle ordering mutation is caught; categorize --dry-run runs; ruff and markdownlint are clean. The full gates follow before the counts commit. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
|
Answers to Antigravity round 2 (on 1a66470): Blocking: SemVer. Declined, as in round 1. The maintainer chose to land v3.0.0's breaks in v2.9.8 and keep the v2.9.x number. ADR 0042 and ADR 0043 record the decision, and v3.0.0's notes must restate every break. The crates are not published to crates.io (checked). Blocking: saves are not found after the identity change. Declined. This is a maintainer decision, taken with exactly this consequence in view: the maintainer chose header-excluded identity over a fallback, saying they did not mind breaking existing saves. That way a corrected header can never orphan a save again. The release notes open with it ("Older saves are not found for cartridge games") and the CHANGELOG lists it under the breaking changes. A rename-on-miss fallback would keep the old whole-file hash alive as a second identity indefinitely, which is the coupling this change removes. Suggestion: surface refused states and movies in the UI. Taken, and it was worse than you described. Fixed in 2fa43b8:
Nitpick: the Nitpick: the Famicom console model in the user manual. It is documented in |
|
Answers to Antigravity round 3 (on 2fa43b8):
|
Measured on 2fa43b8, the tree this commit documents, which differs from it only by these four count lines: - cargo test --release --workspace --features test-roms --no-fail-fast: 3,148 passed, 0 failed, 14 ignored (was 3,139; +9 tests from the two review rounds: six on 1a66470, three on 2fa43b8); - with commercial-roms too: 3,367 passed, 0 failed, 19 ignored. Corrects the release notes, which said 3,357. CodeRabbit flagged that it disagreed with STATUS.md and the CHANGELOG (3,358). The run on 1a66470 measured 3,364 = 3,358 + 6, so 3,358 was the right baseline and 3,357 the wrong figure. Also verified on 2fa43b8: fmt; clippy for the workspace, scripting + hd-pack, retroachievements, full, and both wasm32 builds; rustdoc -D warnings; the no_std build; the four release audits (anchor, state prose, notes render, contribution checklist); markdownlint. #579's CI is green on 2fa43b8, including test-roms, the Kotlin unit tests and the Gradle bundle, which also compiles the Android netplay palette change. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
|
Answers to Antigravity round 4 (on 6ee34b1). Nothing new here blocks, so there is no push:
|
v2.9.8 "Vanguard": the preparation release for v3.0.0
The ninth release of the v2.9.x line. It began as the performance and MiSTer-menu release. At the maintainer's direction it became the preparation release for v3.0.0:
The number stays v2.9.x by the maintainer's decision. ADR 0042 and ADR 0043 are amended accordingly, and v3.0.0's notes will restate every break.
Breaking changes
Nes::rom_sha256now hashes the bytes after the 16-byte header, so earlier cartridge saves, states, cheats and movies are not found.Nes::image_sha256is the whole-file hash, and the Vs. database is keyed by both..rnscontainer epoch 3 and BUS section 2 refuse every older state, and every legacy reader is removed..rnmformat 3 and the netplay handshake carry every emulation option (ADR 0044), and the Four Score's P3/P4.LockstepBusis nowSystemBus. The 2.7.5 deprecations,serialize_headerand the dead /NMI detector are removed.HeaderandFrameInputare#[non_exhaustive].Fixes from booting every one of 748 staged dumps
$4017inhibit), 153, 191 (non-power-of-two images), 218 and 226.Each fix has a test that failed first, and reverting the fix makes it fail again. Each changed baseline was looked at and attributed, one of them by bisect.
Every platform
Performance (two
ab_check.shruns per claim;docs/performance.md)nestest_fastpath is established (−5% to −9%). The palette workloads do not add up, and that is recorded as a v2.9.9 lead rather than claimed.MiSTer core (sibling PR)
palette-gate, the first gate to check the RTL's palette output.Review
CodeRabbit cannot review a PR this size: 180 reviewable files, against its limit of 100. It was run on two review-only slices of this branch instead, #580 (the core crates) and #581 (the rest). Both will be closed unmerged. 25 findings were real and are fixed here, each with a test that failed first:
$4017inhibit was let through inside the reset delay;.palfile under the emu lock;Six findings cited code in the other slice, and are answered with this branch's code. Antigravity's round 2 found the desktop reporting "State loaded" for a refused state; that is fixed. Its SemVer and save-identity blockers are the maintainer's decisions, and are declined with the reasons.
Verification
cargo test --release --workspace --features test-roms: 3,148 passed, 0 failed, 14 ignored (head before the counts commit, 2fa43b8).commercial-romstoo: 3,367 / 0 / 19, the full coverage sweep included.The iOS Swift and the device runs are not verified on this Linux host.
The merge waits for the maintainer.
🤖 Generated with Claude Code