RustyNES v2.3.4 — "Ledger" (mapper coverage + the load path the harness could not see)
RustyNES v2.3.4 "Ledger" is the coverage release: three new boards, a regression net that can finally see what users see, and the bug that discovery immediately exposed — the per-game database had been destroying correct mapper numbers on every Sachen cartridge since v1.2.0, making twelve staged ROMs refuse to load at all.
This release does touch the emulation core — five mappers and the tier classifier — so the accuracy contract was verified rather than asserted by construction: AccuracyCoin holds at exactly 141/141, nestest is 0-diff.
A regression net that tested the wrong program
The coverage harness loaded ROMs with a bare Nes::from_rom. The frontend rewrites the iNES header first, from the CRC32-keyed per-game database. So every fix delivered through that database was invisible to the net: the ROM booted correctly for users while the sweep reported it blank, and no amount of staring at the mapper would have explained it.
Seicross is the case that proves it. PR #127 repaired it with a database entry setting mapper 185 submapper 4, and the staged dump is plain iNES with no submapper field — so under the old path it loaded as submapper 0 and hung in its copy-protection loop forever. The database moved into its own rustynes-gamedb crate that both consumers share, and Seicross now draws full gameplay.
Putting the harness on the real load path is what surfaced everything below.
The database was overwriting correct headers
The vendored TetaNES table uses 0 in its Mapper column as the default for rows nobody filled in, with no separate empty marker:
80D63472, PAL, 0, 0, 2, 1, 0, false, Horizontal, "Sidewinder (Asia) (PAL) (Unl).nes"
Sidewinder is a Sachen SA-72007 — mapper 145, correctly headered. apply_header_overrides read that 0 as "force NROM", cleared both mapper nibbles, and NROM's own size check then rejected the image: "NROM expects 8 KiB CHR-ROM". The error names the wrong board, which is why it never pointed back at the database.
| staged ROMs with the mapper destroyed | 12 |
| distinct Sachen boards affected | 133, 143, 145, 146, 147, 148, 149, 150 — all of them |
| present since | v1.2.0 |
| reached users, not just CI | yes — every ROM load calls this |
A 0 in that column is now "unspecified". It cannot lose a real correction: on a header already reading 0 the override was a no-op, and on any other ROM it could only ever destroy data. The 11 legitimate non-zero overrides are untouched.
Second time this vendored table has force-applied a field it should not have — the first was the mirroring column freezing Wizards & Warriors (ADR 0031) — and fixed the same way: refuse to apply an override indistinguishable from "no data".
Three boards, breadth 172 → 174
- Mapper 176 submapper 2 — WAIXING-FS005. The 8025's submappers are incompatible boards, and only the FK23C half existed. Adds the
$A001RAM Configuration Register (32 KiB banked WRAM, the$5000-$5FFFregister-window disable the Waixing copy-protection is built on, and a mapper-195-like mixed CHR-ROM/CHR-RAM mode), two-bit$A000mirroring, the$46/$47bank-select swap — which does not apply to$06/$07— PRG A21–A25, and the board's documented$E003decode mask so$9FFFno longer aliases$8001. - Mapper 154 — NAMCOT-3453. Mapper 88 plus a one-screen nametable bit, decoded across the whole
$8000-$FFFFrange rather than only the bank-select window. Used by one game, Devil Man. - Mapper 243 — Sachen SA-020A. Mapper 150's ASIC on the PCB it was designed for, which NESdev states under its Errata. Same three registers at inverted significance — R2 is the CHR LSB here and the MSB on the SA-150 — which is exactly why they need separate numbers. Used by one game, 美女拳 Honey Peach.
Detection for FS005 is a loader rule, not a database entry, because the header refutes itself: UNROM-512 is CHR-RAM-only, so a mapper-30 image declaring CHR-ROM cannot be that board. Genuine mapper-30 images are untouched.
All three implemented from the NESdev wiki. Unlike the FK23C banking transforms in the same file — a disclosed Mesen2 derivation — no reference-emulator source was consulted for any of the new code.
Also fixed
| Bandai FCG (16) | the I²C EEPROM address counter is a u8 advanced as (addr + 1) & 0xFF — the mask was written to express a ring, but the add traps on the wrap. A debug-build panic on a real ROM. |
| CLI launches skipped every header override | rustynes <rom> applied only the post-construction corrections, so a ROM needing a mapper/submapper fix worked from the File menu and hung from the command line. Both paths now share one helper. |
| Mapper 15 | 8 KiB compatibility PRG-RAM the single-game hacks expect, and CHR-RAM writable in every mode. Four ROMs blank → rendering. |
| Save-state back-compat | mappers 15, 88 and 176 would have rejected their own pre-v2.3.4 states. All three accept the legacy length; length and version must now agree, so a truncation cannot be reinterpreted as an older format. |
| #360 | the movie_ui recording tests never called after_frame, so v2.3.2's replay attestation was exercised by no test. Now verified end to end against a fresh Nes, both the matching and diverging case. |
Snapshot identity, twice
Two distinct collision classes made ROMs permanently unfixable — each wrote the same baseline with a different rom= line, so exactly one of every pair mismatched on every run and blessing one broke the other. First: a title staged both loose and archived shared a stem (54 ids). Second, which the extension suffix cannot close: Magic Dragon (Unl).nes and Magic Dragon _Unl_.nes are both .nes and normalise identically (8 ids, all byte-identical duplicates).
Collisions are now loud at discovery — byte-identical ones collapse, differing ones abort naming the paths. The corpus is 691 files, 691 ids, and 283 baselines orphaned by the rename were removed while 82 belonging to ROMs not staged locally were deliberately kept.
What this release does not claim
- The APU workstream was not delivered. It was scoped for v2.3.4 and is carried to v2.3.5 unstarted: the blocking step, an
apu_throughputbench, was never built, so there is no measurement to adopt or reject. At 18.7% of frame time it remains the largest core cost never examined. Chu Liu Xiangis not fixed. Two of the three FS005 dumps boot; it went blank → a flat grey frame. It is alone in never writing$A000, which makes the power-on mirroring value the leading hypothesis — deliberately not applied, because wrong mirroring produces a scrambled screen, not an empty one.- 66 blank boots and 12 load rejections remain, catalogued with their classes: 5 were unimplemented mappers (now 0), 6 are header/size mismatches awaiting a per-board decision, 1 is a corrupt dump.
Corrections to this project's own published numbers
Three figures carried into this release were wrong and are corrected in-tree: "~60 boot panics" conflated snapshot mismatches with panics (really 42 + 6 + one real defect); "6 load rejections" came from a filtered run (37 corpus-wide); and a risk check reported as covering the database's mapper changes missed the twelve ROMs that fail to load rather than boot blank.
Full detail is in CHANGELOG.md and to-dos/DEFERRED-AND-CARRYOVER-FEATURES.md §7, which records every residual with its measured numbers rather than prose.