> ## Documentation index
> Fetch the complete documentation index at: https://rustynes-git-release-v3-6-0-aaf8d1.docs7.io/llms.txt
> Ask it with GET https://rustynes-git-release-v3-6-0-aaf8d1.docs7.io/ask?q=<keywords or question>. Short keywords rank best.
> Use these to discover all available pages before exploring further.

# Mappers

# Mappers and cartridge

**References:** `ref-docs/research-report.md` §Cartridge format and mappers;
`ref-docs/nesdev-wiki-technical-report.md` §Common Mapper Families; Nesdev
[Mapper](https://www.nesdev.org/wiki/Mapper),
[Bus conflict](https://www.nesdev.org/wiki/Bus_conflict),
[MMC1](https://www.nesdev.org/wiki/MMC1),
[MMC3](https://www.nesdev.org/wiki/MMC3), and
[MMC5](https://www.nesdev.org/wiki/MMC5).

## Purpose

Implement the cartridge subsystem in `crates/rustynes-mappers`: a `Mapper` trait, a `Cartridge` struct that owns the ROM/RAM banks and a boxed `dyn Mapper`, and concrete implementations of the top ~25 mappers (covering >95% of the licensed library).

## Interfaces

```rust
pub trait Mapper: Send {
    fn cpu_read(&mut self, addr: u16) -> u8;          // $4020-$FFFF
    fn cpu_write(&mut self, addr: u16, value: u8);
    fn ppu_read(&mut self, addr: u16) -> u8;          // $0000-$3FFF (CHR + nametable)
    fn ppu_write(&mut self, addr: u16, value: u8);

    fn notify_a12(&mut self, level: bool) {}          // for MMC3/MMC5
    fn notify_cpu_cycle(&mut self) {}                 // for CPU-cycle IRQ counters (VRC, FME-7)
    fn irq_pending(&self) -> bool { false }
    fn irq_acknowledge(&mut self) {}

    fn mix_audio(&mut self) -> i16 { 0 }              // VRC6/7, MMC5, Sunsoft 5B, Namco 163, FDS

    fn save_state(&self) -> Vec<u8>;
    fn load_state(&mut self, data: &[u8]) -> Result<(), MapperError>;

    // v3.1.0 (`T-SPRITE-LIMIT`): `false` when a CHR read changes the board.
    fn chr_reads_are_pure(&self) -> bool { true }

    // v3.5.0: every PPU read (address the PPU drives), for boards that watch
    // the PPU bus. Delivered only when `snoops_ppu_reads` is true (MMC5 only;
    // the bus caches the answer and skips the dispatch for every other board).
    fn snoops_ppu_reads(&self) -> bool { false }
    fn notify_ppu_read(&mut self, addr: u16) {}
}

// `#[non_exhaustive]` since v3.0.0: outside `rustynes-mappers`, build one with
// `rustynes_mappers::parse` or `Cartridge::synthetic`, never a struct literal.
#[non_exhaustive]
pub struct Cartridge {
    pub prg_rom: Box<[u8]>,
    pub chr_rom: Box<[u8]>,          // empty if cart uses CHR-RAM
    pub mapper_id: u16,
    pub submapper: u8,
    pub mirroring: Mirroring,
    pub region: Region,
    pub console_type: ConsoleType,
    pub vs_ppu_type: VsPpuType,
    pub vs_dual_system: bool,
    pub prg_ram_size: u32,
    pub chr_ram_size: u32,
    pub has_battery: bool,
    pub has_trainer: bool,
    pub is_nes2: bool,
    pub nametable_wiring_bits: u8,   // the header's raw nametable bits (v2.9.9)
}

pub enum Mirroring { Horizontal, Vertical, SingleScreenA, SingleScreenB, FourScreen, MapperControlled }
```

**`chr_reads_are_pure` (v3.1.0).** The PPU's "disable sprite limit" option
makes extra, display-only pattern reads, and only on boards that report
`true`. Five report `false` because a CHR read changes them: MMC2 (9) and
MMC4 (10) switch a CHR latch on tiles `$FD` / `$FE`, the J.Y. ASIC (35, 90,
209, 211) clocks an IRQ counter on PPU reads and mapper 209 latches CHR,
Bandai 96 follows the last PPU address for its inner CHR bank, and Nanjing 163
latches PPU A13. **A new board whose `ppu_read` writes `self` must override
it.** `every_board_that_claims_pure_chr_reads_has_them` (in `mapper.rs`) reads
all of CHR on every constructible mapper id that claims purity and fails if
`save_state` moved; it also pins the impure set, so the list here and the code
cannot drift apart. The set came from a scan of every `ppu_read` body for
writes to `self`.

`rustynes_mappers::parse(&[u8]) -> Result<(Cartridge, Box<dyn Mapper>), RomError>`
parses an iNES or NES 2.0 file (see `cartridge-format.md`), constructs the
appropriate concrete mapper, and returns the metadata header together with
the boxed mapper as a **tuple**. The tuple shape is the pragmatic one: the
`Cartridge` is cheap-to-clone metadata (region, mirroring, mapper id, ROM
slices) that the bus, save-state path, and debugger all want to inspect
without disturbing the mapper, while the `Box<dyn Mapper>` is the live
mutable state. Keeping them as separate ownership roots lets the bus own
the mapper exclusively while metadata can be passed by `&` or cheaply
cloned for diagnostics.

## State

The `Cartridge` owns immutable PRG-ROM and CHR-ROM banks plus mutable PRG-RAM and CHR-RAM. The mapper holds:

- Bank-select registers (one per banking dimension).
- Mirroring control state (if mapper-controlled).
- IRQ counter state (latch, counter, enable, pending).
- For audio mappers: extra channel timers, DAC values, frame counter sub-state.

The mapper does *not* directly own the ROM bytes — it receives a reference to the cart-owned arrays via constructor. This avoids duplication and keeps the `dyn Mapper` boxed type small.

**Battery saves go through `sram()` / `sram_mut()`, and the default is empty.**
`Nes::sram()` is the one place a host can read battery memory from, and the
**libretro core** is what persists it today: it registers `sram_mut()` as
`RETRO_MEMORY_SAVE_RAM`, which RetroArch writes to the game's `.srm` file
(`crates/rustynes-libretro/src/lib.rs`). A board that holds save memory anywhere
else — its own `wram` field, an on-chip RAM, a serial EEPROM, a wrapped inner
mapper — must override the pair, or RetroArch saves an empty file. v2.7.1 found
six boards that did not (Bandai FCG's EEPROM, Taito X1-005's 128 bytes, TxSROM
and TQROM not forwarding to their MMC3, Multicart 15, and BMC-FK23C).

**Every host persists it now.** Libretro hands `sram()` to RetroArch (`.srm`);
the desktop keeps `<data_dir>/battery/<rom_sha256>.sav` since v2.7.3 (FE-01);
Android and iOS keep a `.sav` per ROM hash since v2.7.4 (MOB-05 / AND-09),
through the bridge's `battery_ram` / `load_battery_ram`. All of them persist
only a cartridge whose header sets the battery bit (`Nes::has_battery`), since
NROM, MMC1 and MMC3 expose work RAM whatever the header says. Before those
releases a game's in-cartridge save on the desktop and the phones survived
only inside a save state (the v2.7.1 finding).

**Since v2.9.6 the hosts persist `save_data()`, which is `sram()` except on a
self-flashable board.** GTROM (111) and a flashable UNROM 512 (30) save by
rewriting their PRG flash, have no RAM at `$6000`, and return the flash image
from `Mapper::save_data`. Every host moved to it: the desktop `.sav`, the mobile
bridge, libretro's `SAVE_RAM`, a power cycle (which keeps it), and a power-on
movie, which calls `clear_save_data`, the ROM as loaded on a flash board.
`sram()` stays the `$6000` RAM: libretro's memory map, which RetroAchievements
reads, and the open-bus rule both depend on that meaning, so it was not stretched
to cover a 512 KiB ROM. `parse` raises the battery flag for these two boards,
because GTROM headers do not set it.

`crates/rustynes-mappers/tests/battery_sram_exposed.rs` builds every NES 2.0
mapper number (0-4095) with a battery and 8 KiB of PRG-NVRAM, both with CHR-ROM
and with CHR-RAM, and fails any board whose `$6000-$7FFF` holds CPU writes while
`sram()` stays empty or unchanged — so a new mapper that forgets the override
fails by default. Boards whose save memory has no CPU window (the FCG EEPROM) or
needs an unlock sequence (X1-005) have their own tests in the same file. The
loop can only check boards whose RAM a blind write sweep reaches — 43 of 296
images at v2.7.1 — and prints the rest; RAM gated behind a board-specific enable
is not checked by it.

**A save state carries cartridge RAM only through the mapper's own blob.**
The `.rns` container has no SRAM section: the `MAP` section is the mapper's
`save_state` bytes and nothing else, so a board that leaves its PRG-RAM or CHR-RAM out of
that blob loses it on every save-state load, rewind step, run-ahead frame and
netplay rollback — the restored machine keeps the RAM the running game held.
Until v2.9.2 the Konami VRC2, VRC4, VRC6 and VRC7 boards (mappers 21-26 and 85)
did exactly that (core audit v2.9.2 AUD-02; VRC7's own version comment claimed
its v1 blob carried PRG-RAM, when it carried only the enable bit). They now
append the 8 KiB PRG-RAM, then the 8 KiB CHR-RAM on a board without CHR-ROM,
after every older field: VRC2 and VRC4 write section v2, VRC6 v3 (after its
audio tail), and VRC7 v3 without `mapper-audio` / v4 with it (after the
synthesizer tail, so the version still says whether that tail is present).
Every older version loaded and left the RAM as it was until v2.9.8, which
refuses them (see "Save-state versions" below).

A sweep of every mapper id with the same shape of test, run while triaging
AUD-02, found the same omission on boards beyond the VRC2/4/6/7 set it had
fixed, VRC1 among them: PRG-RAM on
**10** (MMC4), and CHR-RAM on **9**, **10**, **11**, **19**, **34**, **69**,
**75** and **151**. v2.9.2 fixes them the same way, a versioned tail after every
older field: MMC2 (9), MMC4 (10), Color Dreams (11), mapper 34 and VRC1 (75,
and 151, which forwards its section to the VRC1 core) write section v2, FME-7
(69) v3 (after its 5B audio tail) and Namco 163 (19) v4 (after its v3
`chr_ram_disable` / `ciram_owned` bytes). Every older version loaded and left
the RAM as it was until v2.9.8, which refuses them; a blob of the wrong length
is refused before any field is written.

The sweep is now a standing test,
`every_board_snapshot_carries_cartridge_ram` in
`crates/rustynes-core/src/nes.rs`, which replaces the two VRC-only pins. It
walks every mapper id 0-4095 that `parse` builds (NES 2.0 under all 16
submappers, and iNES 1.0 for 0-255), each with and without CHR-ROM, snapshots
a whole `Nes`, scribbles the RAM with its complement and restores. PRG-RAM is
checked through `sram()` on the whole buffer, or through the `$6000-$7FFF`
window when a board keeps RAM there without exposing it; CHR-RAM through PPU
`$0000-$1FFF`. It also asserts the boards above stay among those checked, so a
regression in the probes cannot pass by checking nothing. Two limits: RAM
behind a board-specific enable that `sram()` does not expose is not reached,
and CHR-RAM beyond the 8 KiB mapped at power-on is checked only on its visible
part.

**A board with nothing at `$6000-$7FFF` floats there (v2.7.2).** The CPU bus
keeps an open-bus latch, and a mapper reports an undriven address through
`cpu_read_unmapped`. The trait default now treats `$6000-$7FFF` as unmapped
exactly when `sram()` is empty. Before v2.7.2 the default treated all of
`$6000-$FFFF` as mapped, and 205 board variants read a made-up `$00` there. A
board with ROM or readable registers in that window but no save RAM must
override the hook for it: mappers 40, 42, 50, 212, 238, 305 and 306 do.
`crates/rustynes-mappers/tests/prg_ram_window_open_bus.rs` checks both
directions for every mapper number. Comparing the commercial-ROM suite before and after this
rule found five boards whose documented RAM the model lacked (156, 177, 227's
FW-01 variant, 241, 245); v2.7.2 gave them their 8 KiB
(`tests/documented_wram.rs`). A read that drives only some data bits,
like Sachen's 3-bit registers, reports the rest through `cpu_read_driven_mask`,
and the bus keeps its floating value on them.

### Save-state versions

**Since v2.9.8 every mapper's `load_state` reads its current layout only**
(ADR 0042). Before then most boards that grew their blob kept reading the
older layouts: a lower version byte, or, where the version did not move, a
shorter length, loaded with the new fields at a default or the RAM left as it
was. All of those readers are gone. An older version byte is
`MapperError::UnsupportedVersion`, a short or long blob `MapperError::WrongLength`.
The boards that had them: MMC1, MMC3, MMC5, MMC2, MMC4, Color Dreams, Bandai
FCG, Namco 163, VRC2, VRC4, VRC6, VRC7, VRC1, mapper 34, FME-7, Namco 118, Vs.
System (layout 1), FK23C, COOLBOY, Sachen 9602, the MMC3 clones, mappers 156,
177 and 241, the mapper-15 and FW-01 multicarts, FDS and NSF (a v1 blob on an
expansion-audio NSF). Two layouts per board survive where both are current:
VRC7 writes v3 without `mapper-audio` and v4 with it, mapper 99 writes v2 with
the `DualSystem` shared RAM and v3 without, and an NSF writes v1 or v2 by its
expansion chips. A `.rns` file from v2.9.7 or earlier never reaches these
readers: its container is refused at the header.

## Behavior

### Banking pattern

Every mapper resolves a CPU/PPU address into a `(bank_index, offset)` pair on every access, then indexes into the cart-owned ROM/RAM. This is hot code — inline aggressively.

A board's CHR bank outputs drive the address lines of whatever chip sits on the CHR bus, so on a CHR-RAM image a PPU **write** resolves through the same banking as a PPU read. Until v3.3.0 seven boards read CHR-RAM banked and wrote it at `addr % len` (VRC2 and VRC4 on mappers 21-23 and 25, VRC6 on 24 and 26, Namco 163 on 19, FME-7 on 69, VRC1 on 75 and 151, and NINA-001 on 34 submapper 1), so a game could not read back the tiles it uploaded (`T-VRC4-CHRRAM-BANKING`, `EMULATION_EPOCH` 5). `crates/rustynes-mappers/tests/chr_ram_read_back.rs` holds every board to it: on every board and submapper that takes a CHR-RAM image, a byte written at a pattern-table address reads straight back at power-on, and after random register writes no write may change some other pattern byte while leaving its own unchanged. The boards whose pattern tables are not RAM at power-on are exempt by name: FFE mapper 6 submappers 4-7 and 12-15 and mapper 8 (write-protected) and TQROM (every bank on CHR-ROM).

### IRQ counter mechanisms (the four families)

1. **None** — NROM, UxROM, AxROM, MMC1, CNROM, MMC2/4, BNROM, GxROM, Color Dreams, CPROM. No IRQs.
2. **PPU A12 edge counter** — MMC3 (and clones, e.g., Namco 108 derivatives). Notify on every A12 transition; filter is *internal to the mapper*. Per `ref-docs/research-report.md` §MMC3, the filter requires A12 to remain low for 3 falling edges of M2 before the next rising edge counts.
3. **PPU scanline count** — MMC5. Detects scanlines from the PPU's reads, delivered by `Mapper::notify_ppu_read` (v3.5.0): three consecutive reads of one `$2000-$2FFF` address (a line's two dummy nametable reads and the next line's first nametable fetch), then the next read, the attribute fetch at PPU cycle 4, is the detection (`nesdev_wiki/MMC5.xhtml`, "Scanline Detection and Scanline IRQ"). The first detection of a frame (visible line 0) sets "in frame" and the counter to 0; each later one increments the counter, so visible line N reads N, and `$5203` = N raises the flag at line N, dot 4. "In frame" clears when three CPU cycles pass with no PPU read (early on line 240, or when rendering stops), on a `$2001` write with bits 3-4 clear, and on a `$FFFA` / `$FFFB` read (which also acknowledges the IRQ and resets the counter). With rendering off nothing is read, so nothing counts. **Changed in v3.5.0:** until then the counter ticked from `notify_scanline_start` at dot 0 of the pre-render line and of every visible line, rendering or not, so line N read N + 1 and a `$5203` = N IRQ rose at dot 0 of line N - 1, a whole line early (the MiSTer sibling's ledger 3.100; `EMULATION_EPOCH` 6). Where the page's prose, pseudocode and state diagram disagree, and what is not modelled, is in the rustdoc of `Mmc5` (`crates/rustynes-mappers/src/m005_mmc5.rs`, "Scanline detection").
4. **CPU cycle counter** — VRC2/4/6/7, Sunsoft FME-7, Namco 163, plus the BestEffort M2-counter pirate boards (NTDEC 2722 / mapper 40, Nitra / mapper 250). Tick on every CPU cycle (`notify_cpu_cycle`).

### Mirroring

Most mappers expose a register that selects horizontal, vertical, single-screen A, or single-screen B. The PPU's nametable fetch consults `cart.ppu_read(addr)` for `$2000-$3FFF`; the mapper applies mirroring. Four-screen mode requires extra cart-side VRAM (typically 2 KB) on top of the console's 2 KB.

**Per-game mirroring override (v1.1.0 beta.1, T-110-B4).** The bus carries an optional `nt_mirroring_override: Option<Mirroring>` (set via `Nes::set_mirroring_override`). When `Some`, the bus's `$2000-$3EFF` nametable translation uses it instead of the mapper's `nametable_address` — a load-time correction for ROMs with a wrong iNES mirroring flag, supplied by the frontend's CRC32-keyed game database (the `rustynes_frontend::game_db` module). It does **not** affect mapper-supplied VRAM (`nametable_fetch`, e.g. four-screen), is `None` by default (byte-identical; the core test suites never set it), and is persisted in the save-state. The database itself (its format, CRC keys and the load-time rules for when an entry may apply) is documented in the `rustynes-gamedb` crate docs (`crates/rustynes-gamedb/src/lib.rs`); the CHANGELOG has the rollout.

### Bus conflicts

Some early mappers (CNROM, AxROM, GxROM, Color Dreams) do not buffer writes to the bank-select range, so the value written collides with the value being *read* from PRG at the same address. Implementations should AND the written value with the PRG byte at that address (per NESdev wiki bus-conflict rules). Affects rare but real test cases.

Bus conflict behavior is board-specific. ASIC mappers usually disable PRG ROM
outputs during writes; many discrete mappers do not. NES 2.0 submappers can
distinguish some conflict-free homebrew or modified boards from original
conflict-prone boards. The mapper implementation should therefore decide
conflicts from mapper/submapper/board metadata, not only from mapper number.

### FDS medium model (Mapper 20, v2.2.0 "Capstone")

The Famicom Disk System RAM adapter (`crates/rustynes-mappers/src/fds.rs`) models
the disk **medium** as a synthesized byte-stream wire image, not just the raw
`.fds` payload:

- **Wire image**: each inserted side is expanded into the hardware wire format the
  RP2C33 controller scans — a lead-in gap (`$00` run), a `$80` start mark, the
  block bytes, and a **CRC-16/KERMIT** (reflected poly `0x8408`) per block, with
  inter-block gaps between them. Reads stream this wire image; the controller's
  gap-skip hides the gap + start mark from the CPU (first byte delivered is the
  block's first real byte).
- **Per-block CRC-16 on write**: when the BIOS writes a block payload, the write
  path mirrors the byte into the raw side **and re-emits that block's CRC-16**
  over the updated payload (`resynth_block_crc`), modelling the controller's
  continuous CRC generator so the medium stays self-consistent (a stricter loader
  checking `$4030.D4`, and the synthetic oracle below, see a valid block).
- **Continuous analog head-seek model** (opt-in, default-off — `set_analog_head_seek`):
  the belt-driven head's motor-restart rewind time is proportional to the
  distance it had travelled from the disk-start gap (a constant belt velocity,
  `HEAD_SEEK_BYTES_PER_CYCLE`, plus a fixed `HEAD_SEEK_SETTLE_CYCLES` settle),
  clamped to a cold `MOTOR_SPIN_UP_CYCLES` spin-up. This replaces the flat
  `HEAD_RESEEK_CYCLES` not-ready window with a position-dependent seek. **With the
  model disabled (the default) a non-writing `.fds` run is byte-identical** to
  prior releases; the model state round-trips the **v4** save-state tail.
- **Synthetic write-verify oracle** (`medium_write_verify`): a BIOS-free walk of
  the wire image asserting every block's CRC-16 and gap/mark framing round-trips.
  This is the **CI-verifiable** half of the medium model; the real-BIOS write-CRC
  path needs a copyright FDS BIOS and is exercised only from a local, gitignored
  dump. See `docs/accuracy-ledger.md` for the CI-verifiable vs local-only split.

All FDS timing is deterministic cycle arithmetic (no wall-clock / analog jitter),
so the determinism contract and save-state round-trip hold.

**The save-state disk tail sizes itself with checked arithmetic (v2.9.2).** The
v3/v4 tail carries its own side count as a `u32`, and the loader validates the
blob's exact length as `count * 65500` plus the fixed fields. On a 64-bit host
that product cannot overflow, so a hostile count only fails the length check. On
the 32-bit targets RustyNES ships (`wasm32`, `armv7`, `i686`) it wrapped: a count
of `2^30` wraps the side region to zero bytes, so a crafted blob with no side
data passed the check and the restore loop sliced past its end, a panic in every
profile (core audit v2.9.2 AUD-01). The product and sum are now checked and a
count that overflows `usize` is rejected; no separate cap is needed, because a
count that fits must still match the blob's own length exactly. Pinned by
`fds::tests::load_state_rejects_a_side_count_whose_length_wraps_on_32_bit`,
which is red only when the mapper tests run on a 32-bit target — the host suite
and the host-run `save_state` fuzz target could not see this.

## Mapper coverage matrix (Phase 4 status)

Sorted by number of commercial titles using each mapper.

| iNES | Submapper | Name | Phase | Audio | IRQ | Status | Notes |
|------|-----------|------|-------|-------|-----|--------|-------|
| 0 | — | NROM | 1 | — | — | landed (Phase 1) | 247 titles. Trivial; no banking. |
| 1 | 1-5 | MMC1 (SUROM, SXROM, etc.) | 2 | — | — | landed (Phase 2) | Serial 5-write protocol; consecutive-write bug. On boards with at most 8 KiB of CHR (v2.7.2, from `nesdev_wiki/MMC1.xhtml`), the CHR bank register's bit 4 selects the 256 KiB PRG half for the whole window, fixed bank included (SUROM / SXROM), and bits 3-2 select the 8 KiB PRG-RAM bank (SOROM: bit 3; SXROM: bit 3 = A14, bit 2 = A13). In 4 KiB CHR mode the driving register is the one the last CHR fetch selected. SNROM's bit-4 RAM enable applies only to <= 256 KiB PRG with <= 8 KiB RAM. holy_mapperel `M1_P512K_CR8K_S8K` / `_S32K` pass `0000`. SZROM is not modelled. |
| 2 | 0-2 | UxROM | 2 | — | — | landed (Phase 2) | UNROM, UOROM, etc. CHR-RAM only. |
| 3 | 0-2 | CNROM | 2 | — | — | landed (Phase 2) | Bus conflict required. |
| 4 | 0-3 | MMC3 (and MMC6, sub 1) | 4 | — | A12 | landed (Phase 4 / S1) | Sharp vs NEC IRQ revision; default Sharp. v3.1.0: `Nes::set_mmc3_revision_override` selects the alternate (NEC) revision for any mapper-4 ROM, and its `$C001` reload to 0 now asserts as documented, so `mmc3_test_2/6-MMC3_alt` passes under it. The IRQ line is raised at the first per-cycle hook after the A12 rise (v2.9.9, pitfall 2). `mmc3_test/5-MMC3` and `mmc3_test_2/5-MMC3` pass; both `4-scanline_timing` ROMs pass all 13 sub-tests since T-MMC3-BG-A12, a PPU change (`docs/ppu-2c02.md` pitfall 4); they failed at sub-test 9 from v2.9.9. |
| 5 | — | MMC5 | 4 | yes (landed) | scanline | v0+v1 landed (Phase 4 / S4) | Banking + scanline IRQ + ExRAM modes 10/11 + multiplier (v0). Fill mode (`$5106`/`$5107`), dual sprite/BG CHR registers used for sprite tile fetches, ExGrafix per-tile attribute + CHR override (mode 01) (v1). Vertical split-screen (`$5200-$5202`) via `bg_split_state` (Castlevania III J status bar) and the MMC5 audio extension (two pulse + 7-bit PCM, `$5000-$5015`, behind the default-on `mapper-audio` feature) landed. PRG-RAM banking (v2.7.2, `nesdev_wiki/MMC5.xhtml` §"PRG-RAM configurations"): `$5113` and RAM-mode `$5114-$5116` page the RAM by the bank value's low three bits over the wiki's 64 KiB "compatible superset for all games", because PRG-RAM sizes in headers are unreliable (*L'Empereur*'s NES 2.0 dump under-declares its ETROM board); a 16 KiB window takes A13 from the CPU. `sram()` is the battery-backed part of the header's declared RAM: all of it, except ETROM's 16 KiB, where only the first chip is saved. |
| 7 | 0-2 | AxROM | 2 | — | — | landed (Phase 2) | Single-screen mirroring control. |
| 9 | — | MMC2 | 4 | — | — | landed (Phase 4 / S2) | Punch-Out; latched CHR per fetch ($FD/$FE). |
| 10 | — | MMC4 | 4 | — | — | landed (Phase 4 / S2) | Like MMC2 with full PRG banking. |
| 11 | — | Color Dreams | 4 | — | — | landed (Phase 4 / S2) | Unlicensed; bus conflict. |
| 13 | — | CPROM | 4 | — | — | landed (Phase 4 / S2) | Videomation. |
| 19 | — | Namco 163 | 4 | yes (landed) | CPU | banking+IRQ+audio landed (Phase 4 / S3 + Track C2 / Phase 2.2) | Mappy-Land, King of Kings, Final Lap, Rolling Thunder, Megami Tensei II.  1-8 wavetable channels playing 4-bit wavetables from 128 B mapper-internal sound RAM.  Address-port at `$F800-$FFFF` (bit 7 = auto-increment, bits 6-0 = 7-bit RAM address) + data-port at `$4800-$4FFF`; per-channel registers at the top of internal RAM (channel 8 at `$78-$7F`, channel 1 at `$40-$47`); 18-bit frequency + 24-bit phase + 6-bit wave-length + nibble-addressed wave start address + 4-bit volume per channel; `$E000` bit 6 = audio-disable.  Gated behind the `mapper-audio` cargo feature. Nametables and CIRAM-as-CHR (v2.7.2, `nesdev_wiki/INES_Mapper_019.xhtml`): `$C000/$C800/$D000/$D800` select each nametable quadrant (`< $E0` a read-only 1 KiB CHR-ROM page, `>= $E0` CIRAM A/B by the low bit), powering on as the header's layout; CHR values `>= $E0` map CIRAM as CHR-RAM unless `$E800` bit 6 (`$0000-$0FFF`) or bit 7 (`$1000-$1FFF`) disables it. Save state v3 carries the `$E800` bits; an older blob loads as both halves disabled, the pre-v2.7.2 behaviour. IRQ: a 15-bit up-counter at `$5000` (low) / `$5800` (`EHHH HHHH`, bit 7 = enable), clocked every CPU cycle while enabled, firing and stopping at `$7FFF`; a `$5800` write with bit 7 clear disables it, and the enable reads back in bit 7 (v2.9.8 -- before then every `$5800` write enabled the counter, which broke *Megami Tensei II*'s raster bands). |
| 21 | 1, 2 | VRC4a / VRC4c | 4 | — | CPU | landed (Phase 4 / S3) | Konami; Wai Wai World. |
| 22 | — | VRC2a | 4 | — | — | landed (Phase 4 / S3) | Konami. |
| 23 | 1-3 | VRC4e / VRC4f / VRC2b | 4 | — | CPU | landed (Phase 4 / S3) | Konami. Submapper 3 (VRC2b) is built as the VRC2 from v3.2.0: no IRQ, and H/V mirroring only (the VRC2 ignores bit 1). |
| 24 | — | VRC6a | 4 | yes (landed) | CPU | banking+IRQ+audio landed (Phase 4 / S3 + Track C2) | Akumajou Densetsu.  3 extra audio channels (2 pulse + 1 sawtooth) gated behind the `mapper-audio` cargo feature.  **Power-on CHR = identity (ASSUMPTION, v2.9.8):** the eight 1 KiB CHR registers (`$D000-$E003`) start at 0-7. NESdev documents no VRC6 power-on state; *Pulsewave Invite* (PD) never writes them and draws its postcard only under this layout (all-zero, the old value, scattered its tiles). The other ten staged mapper 24/26 dumps boot to byte-identical frames either way. Power-on only: `Mapper::reset` keeps the registers, as a soft reset does on almost every board. PRG registers still start at 0 (no documentation or evidence for another value). Applies to mapper 26 as well. |
| 25 | 1-3 | VRC4b / VRC4d / VRC2c | 4 | — | CPU | landed (Phase 4 / S3) | Konami. Submapper 3 (VRC2c) is built as the VRC2 from v3.2.0, as for mapper 23. |
| 26 | — | VRC6b | 4 | yes (landed) | CPU | banking+IRQ+audio landed (Phase 4 / S3 + Track C2) | Madara, Esper Dream 2.  Same channels as VRC6a; A0/A1 swap.  Same identity power-on CHR assumption as mapper 24. |
| 34 | 0-2 | BNROM / NINA-001 | 4 | — | — | landed (Phase 4 / S2) | Submapper 1 selects NINA-001. |
| 66 | — | GxROM | 2 | — | — | landed (Phase 2) | Bus conflict. |
| 69 | — | Sunsoft FME-7 | 4 | yes (5B, landed) | CPU | banking+IRQ+audio landed (Phase 4 / S3 + Track C2 / Phase 2.1) | Gimmick!  Sunsoft 5B = YM2149F clone: 3 squares + 32-step envelope generator + 17-bit LFSR noise.  Two-write protocol via `$C000-$DFFF` (address latch) and `$E000-$FFFF` (data); audio gated behind the `mapper-audio` cargo feature. |
| 71 | 0, 1 | Camerica BF9093 / BF9097 | 4 | — | — | landed (Phase 4 / S2) | Codemasters titles. The bank select answers only at `$C000-$FFFF` (from v3.3.0, `T-MAPPER71-DECODE`; it took `$8000-$BFFF` too before, which neither the NESdev page nor Kevin Horton's Camerica notes allow). Submapper 1 (*Fire Hawk*, BF9097) has the single-screen register at `$8000-$9FFF` from power-on; from v3.2.0 submapper 0 (every iNES 1.0 image) keeps its header mirroring until the first `$9000-$9FFF` write, then switches to it, per the NESdev page's compatibility advice (only *Fire Hawk* writes there). Other submappers ignore it. |
| 75 | — | VRC1 | 4 | — | — | landed (Phase 4 / S2) | Konami. |
| 85 | — | VRC7 | 5 | yes (FM, landed) | CPU | banking+IRQ+**OPLL FM audio landed** | Lagrange Point (JP). Banking + CPU-cycle IRQ identical to VRC6's. OPLL FM via the clean-room `emu2413` port (`crates/rustynes-apu/src/opll.rs`, MIT; ADR-0006 supersedes ADR-0004). |

### First long-tail batch (14 families, 25 → 39)

Spec-implemented from the nesdev wiki with register/IRQ/nametable unit tests +
boot-smoke (no redistributable behavioral fixtures exist for these boards).

| iNES | Submapper | Name | Audio | IRQ | Notes |
|------|-----------|------|-------|-----|-------|
| 16 / 159 | 0,4,5 | Bandai FCG | — | CPU | DBZ, Famicom Jump II. +minimal I2C EEPROM (24C02/24C01). The Datach is mapper 157 (v3.2.0, below). |
| 18 | — | Jaleco SS88006 | — (ADPCM decoded-not-emulated) | CPU | Goemon Gaiden, Doropie. Nibble-paired banking; selectable-width IRQ. |
| 64 | — | Tengen RAMBO-1 | — | A12 + CPU | Klax, Skull & Crossbones. Dual-mode IRQ (reuses MMC3 A12 filter). The zero test follows a reload as well as a decrement, so a latch of 0 asserts on every clock (v2.9.8; *Skull & Crossbones* depends on it). The reload "+1 kick" and the IRQ delay are not modelled. |
| 65 | — | Irem H3001 | — | CPU | Daiku no Gen-san, Spartan X 2. 16-bit reload-latch down-counter. |
| 67 | — | Sunsoft-3 | — | CPU | Fantasy Zone 2. 16-bit write-twice-latch IRQ. |
| 68 | — | Sunsoft-4 | — | — | After Burner, Maharaja. CHR-ROM-as-nametable. |
| 70 | — | Bandai discrete | — | — | Kamen Rider Club, Family Trainer. UxROM-like. |
| 73 | — | Konami VRC3 | — | CPU | Salamander. Simplest VRC; 8K CHR-RAM. |
| 78 | 1,3 | Holy Diver / Cosmo Carrier | — | — | Submapper-selected mirroring: 3 = Holy Diver (H/V), 1 = Cosmo Carrier (1scA/1scB). Without one (iNES 1.0, or NES 2.0 submapper 0) the header's alternative-nametables bit decides, per `INES_Mapper_078`: set = Holy Diver, clear = Cosmo Carrier (v2.9.8; it was always Holy Diver). The GoodNES *Uchuusen - Cosmo Carrier (J) [!]* sets the bit, so its header names Holy Diver and it stalls on a blue screen; the NES 2.0 image of the same PRG/CHR (submapper 1) plays. |
| 88 / 206 | — | Namco 118 / DxROM | — | — | Dragon Spirit, Quinty, Family Circuit. MMC3 banking subset. |
| 118 | — | TxSROM / TLSROM | — | A12 | Armadillo, NES Play Action Football. MMC3 + per-slot NT mirroring. |
| 119 | — | TQROM | — | A12 | Pin\*Bot, High Speed. MMC3 + mixed CHR (64K CHR-ROM + 8K CHR-RAM; bank bit 6 = RAM select). |
| 210 | 1,2 | Namco 175 / 340 | — | — | Famista variants. Submapper-split banking/mirroring; no IRQ. |

### Second long-tail batch (4 families, 39 → 43)

Spec-implemented from the nesdev wiki with register/bank unit tests. Mapper 99
is the headline: it is the robust, mapper-driven Vs. System signal (no licensed
home game uses it), so detecting it forces `ConsoleType::VsSystem` + the 2C03
RGB PPU at parse time — finally enabling in-game RGB-PPU verification of the
RGB device.

| iNES | Submapper | Name | Audio | IRQ | Notes |
|------|-----------|------|-------|-----|-------|
| 33 | — | Taito TC0190 / TC0350 | — | — | Don Doko Don, Power Blazer. 2x8K PRG + 2x2K + 4x1K CHR; software mirroring. (mapper 48 = TC0690, the +A12-IRQ variant.) |
| 93 | — | Sunsoft-3R | — | — | Shanghai, Fantasy Zone. UxROM-like: PRG bits 4-6 + CHR-RAM-enable bit 0; 8K CHR-RAM. |
| 99 | — | **Nintendo Vs. System** | — | — | **Vs. Excitebike, Vs. Clu Clu Land.** Fixed PRG (8/16/32K) + 8K CHR bank from `$4016` bit 2. Forces Vs. System + 2C03 RGB PPU (mapper-driven, immune to the byte-7 trap). Since v2.9.8 a UniSystem cart also has the board's 2 KiB RAM at `$6000-$7FFF` (mirrored), which the CPU sees while the last `$4016` write had bit 1 (`OUT1`) set and as open bus otherwise (nesdev "Vs. System", `$4016` write; save-state layout v3). *Vs. Super Mario Bros.* keeps its state there and never left its first frame without it. The DualSystem path keeps its shared copy and does not gate on `OUT1`. The circulating `VS Super Mario Bros.nes` dump is headed mapper 3 with no Vs. flag, so it runs as CNROM and still cannot boot: its code banks CHR through `$4016` bit 2 and writes the `$4020` coin counter, so its board is mapper 99 (recorded, not forced). |
| 152 | — | Bandai 74161/161 (1-screen) | — | — | Arkanoid II, Pocket Zaurus. UxROM-like (PRG bits 4-6, CHR bits 0-3) + bit-7 software 1-screen select. |

### Third long-tail batch (5 families, 43 → 48)

Five more spec-implemented licensed boards (nesdev wiki), each with register/bank
unit tests and a commercial-ROM visual survey via `coverage_smoke`. Mapper 48 is
the headline of the batch: an MMC3-style A12 scanline IRQ grafted onto the
TC0190 banking shape.

| iNES | Submapper | Name | Audio | IRQ | Notes |
|------|-----------|------|-------|-----|-------|
| 32 | 1 = Major League | Irem G-101 | — | — | Image Fight, Major League, Kaiketsu Yancha Maru 2, Magical Pop's. 2x8K PRG with a `$9000` swap-mode bit + 8x1K CHR + software H/V mirroring. Submapper 1 (Major League) hard-wires single-screen A and ignores the `$9000` mirroring bit. |
| 48 | — | Taito TC0690 | — | **A12 scanline** | Don Doko Don 2, Flintstones 2, Jetsons, Bakushou!! Jinsei Gekijou 3. TC0190 banking + an MMC3-style A12 IRQ (`$C000` latch = `value ^ 0xFF`, `$C001` reload, `$C002`/`$C003` enable/disable) + `$E000` bit-6 mirroring. The TC0690 has a 1-CPU-cycle IRQ-assert delay vs MMC3 that is **not** modelled exactly (the MMC3 A12 counter is used as-is — close enough for every licensed game). |
| 87 | — | Jaleco/Konami CNROM-style | — | — | Argus, Choplifter, The Goonies, City Connection. Fixed PRG + one bit-swapped 8K CHR-bank register in the `$6000-$7FFF` window: `bank = ((v >> 1) & 1) \| ((v << 1) & 2)`. |
| 89 | — | Sunsoft-2 (Sunsoft-3 board) | — | — | Tenka no Goikenban: Mito Koumon. One `$8000-$FFFF` register: PRG bits 4-6, CHR `((v>>3)&1)<<3 \| (v&7)` (bit 3 = A16), bit 7 = one-screen A/B select; last 16K PRG fixed. Note: **Mito Koumon (the only iNES mapper-89 dump on hand) renders blank** — see `docs/compatibility.md`. |
| 184 | — | Sunsoft-1 | — | — | Atlantis no Nazo, The Wing of Madoola, Kid Niki. Fixed PRG + two 4K CHR banks from one `$6000-$7FFF` register: bits 0-2 @ `$0000`, bits 4-6 @ `$1000`. |

### Fourth long-tail batch — Taito X1 + arcade RGB (3 families, 48 → 51)

Three more spec-implemented licensed boards (nesdev wiki), each with register/bank
unit tests and a commercial-ROM visual survey via `coverage_smoke`. This batch
pairs with the **clean-byte arcade detection** in `rustynes-mappers::parse` (see
`docs/compatibility.md` §PlayChoice-10): a clean iNES-1.0 dump whose byte 7 is
**exactly** `0x01` (Vs.) or `0x02` (PC10) is routed through the 2C03 RGB PPU, and
mapper 151 joins mapper 99 as a mapper-driven Vs. signal.

| iNES | Submapper | Name | Audio | IRQ | Notes |
|------|-----------|------|-------|-----|-------|
| 80 | — | Taito X1-005 | — | — | Kyonshiizu 2, Kyoto Ryuu no Tera Satsujin Jiken. A `$7EF0-$7EFF` register window: two 2K CHR banks (`value & 0xFE`, each driving a pair of adjacent 1K slots) + four 1K CHR banks, **three** switchable 8K PRG banks (`$7EFA`→`$8000`, `$7EFC`→`$A000`, `$7EFE`→`$C000`; only `$E000` is fixed to the last bank), `$7EF6` bit-0 mirroring (0 = Horizontal, 1 = Vertical), plus an on-cart 128-byte battery RAM at `$7F00-$7FFF` enabled only after writing `$A3` to **both** `$7EF8` and `$7EF9`. No IRQ. Kyonshiizu 2 renders its title screen (visually verified) — the earlier blank boot was a missing `$7EFE` `$C000` PRG register that stranded the reset bank (also: the `$7EF6` polarity was inverted). |
| 82 | — | Taito X1-017 | — | (decoded, unused) | Kyuukyoku Harikiri Koushien / Stadium III. Like the X1-005 plus a **CHR A12-inversion mode bit** (`$7EF6` bit 1 swaps the 2K/1K CHR halves between `$0000-$0FFF` and `$1000-$1FFF` — the non-linear X1-017 quirk), **value-shifted** registers (2K CHR banks `value >> 1`; PRG banks `$7EFA-$7EFC` `value >> 2`, ≤128K addressable), and three independently-protected 8K PRG-RAM sub-regions (`$7EF7`=`$CA`, `$7EF8`=`$69`, `$7EF9`=`$84`). The IRQ surface (`$7EFD-$7EFF`) is decoded but never clocked (the licensed games do not use it). `$7EF6` bit 0: 0 = Horizontal, 1 = Vertical. |
| 151 | — | **Konami VS (VRC1 on Vs.)** | — | — | **Vs. Gradius, Vs. The Goonies.** Konami VRC1 silicon (banking byte-identical to mapper 75: three 8K PRG banks `$8000`/`$A000`/`$C000` + fixed last; two 4K CHR windows with `$9000`-driven MSB bits; `$9000` bit 0 = H/V) on a Vs. board. Like mapper 99 it forces `ConsoleType::VsSystem` + the 2C03 RGB PPU (mapper-driven, immune to the byte-7 trap). Verified in-game via Vs. Gradius / Vs. The Goonies (both mapper 151). The unpatched Goonies dump (`Goonies, The (VS).nes`) gained its own `vs_db` row in v2.9.8 (RP2C04-0003, DSW0 `$80`, as the hack row); before it drew its title on the 2C03 palette. **Mislabelled, recorded not forced:** the circulating *Vs. T.K.O. Boxing* dumps (`GVS VS. TKO Boxing.nes` and `VS. TKO Boxing (VS) [!].nes`, byte-identical, SHA-256 `a6332035…`) carry a mapper-151 header, but their code drives a Namco 108: the reset handler writes `$8000 = 6, $8001 = 8, $8000 = 7, $8001 = 9`, and nothing writes `$9000`-`$F000`. VRC1 decodes `$8001` as another PRG write, so the boot goes nowhere (a grey frame). NESdev "Vs. System" lists the Namco 108 (mapper 206) with 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. The patched `VS TKO Boxing Hack.nes` (mapper 4 header) plays. |

Mapper coverage was staged across Phases 1-4 (the matrix above) and extended
across the engine lineage in the long-tail batches above (all shipping in
RustyNES v1.0.0); see `to-dos/ROADMAP.md`. FDS audio shipped as the last
expansion-audio integration.

#### Expansion-audio levels (v2.1.6 "Expansion Audio")

The expansion synth cores (VRC6/VRC7/MMC5/N163/5B, above) are correct in
*shape*; v2.1.6 calibrated their **absolute level** vs the 2A03 pulse to the
bbbradsmith `db_*` decibel-comparison ROMs / Mesen2 mixer weights, verified by
the `crates/rustynes-test-harness/tests/audio_expansion.rs` `level_db_*` oracle.
Corrected scales (`mix_audio`): **VRC6** `256 → 979` (`VRC6_MIX_SCALE`, ≈1.51×),
**MMC5** `256/16 → 650/40` (≈1.0×, "equivalent to APU"), **Namco 163**
`64 → 261` (`NAMCO163_MIX_SCALE`, ≈6.0× for 1-channel mode — no reference
attenuates N163, ours was ~12 dB too quiet). The N163 change is bit-shared with
the NSF path (`nsf_expansion.rs`). Two levels remain documented gaps: the
**Sunsoft 5B** absolute level (log DAC shape is exact, but the full vol-15 /
3-tone range overflows the `i16` `mix_audio` contract — needs a wider mix path)
and the **VRC7** FM level (OPLL implemented + patch ROM verified canonical, but
the pseudo-sine level is patch-dependent, not oracle-pinned). Full detail +
targets: `docs/apu-2a03.md` §Expansion-audio levels and
`docs/accuracy-ledger.md`.

### Fifth long-tail batch — v1.2.0 curated (9 families, 51 → 60)

Discrete-logic boards, each with register-decode unit tests. They now live in
per-board modules (`m038_bitcorp38.rs`, `m041_caltron41.rs`, `ave_nina.rs`,
`jaleco_discrete.rs`, `m232_camerica_bf9096.rs`, `cne240.rs`, `m241_bxrom241.rs`). All are **Tier-1 Curated** (see "Mapper accuracy tiering" below).

| iNES | Name | Audio | IRQ | Notes |
|------|------|-------|-----|-------|
| 38 | Bit Corp UNL-PCI556 | — | — | Crime Busters. PRG/CHR latch at `$7000-$7FFF`. |
| 41 | Caltron 6-in-1 | — | — | Outer register `$6000-$67FF` (PRG/mirroring/CHR-hi); inner CHR-low at `$8000-$FFFF` with a bus conflict, gated by the outer enable bit. |
| 79 | AVE NINA-03/06 | — | — | PRG+CHR bank via `$4100-$5FFF` (the `$4100`/`$5000` address mask). |
| 86 | Jaleco JF-13 | — | — | PRG/CHR latch in the `$6000-$7FFF` window. |
| 113 | NINA-006 / MB-91 | — | — | Like 79 plus a register-controlled mirroring bit (no header mirroring). |
| 140 | Jaleco JF-11/14 | — | — | PRG/CHR latch in the `$6000-$7FFF` window. |
| 232 | Camerica Quattro (BF9096) | — | — | Two-level (outer block + inner) 16 KiB PRG banking. |
| 240 | C&E multicart | — | — | PRG/CHR via `$4020-$5FFF` (write-only). |
| 241 | BxROM-like (pirate) | — | — | 32 KiB PRG bank via `$8000-$FFFF`; CHR-RAM. |

### Sixth long-tail batch — v1.2.0 best-effort sweep (27 families, 60 → 87)

The aggressive Tier-2 sweep, ported from the GeraNES / Mesen2 references (14 +
13 boards). Mostly multicart / Sachen
/ discrete boards with no redistributable test fixture; **register-decode
unit-tested only and not accuracy-gated** (see the tiering note below).

| first wave | second wave |
|---|---|
| 15 (K-1029 multicart), 36 (TXC 01-22000), 39 (Subor BNROM-like), 61, 62 (multicart), 72 / 92 (Jaleco JF-17/19), 77 (Irem, 4-screen CHR-RAM), 96 (Bandai Oeka Kids, PPU-bus CHR latch), 97 (Irem TAM-S1), 132 (TXC 22211), 133 / 145 / 146 (Sachen) | 147 (Sachen 3018), 148 / 149 (Sachen), 150 (Sachen SA-015, readable protection + custom mirroring), 180 (Nichibutsu UNROM-inverted), 185 (CNROM CHR-disable protection), 200 / 201 / 202 / 203 / 212 / 213 / 214 (multicart) |

**Mapper 212, *999-in-1* (v2.9.8 survey): not a board defect.** Its menu
shows `0` glyphs around the list because the nametable is never cleared. The
reset handler waits for one vblank, then sets `PPUADDR` and writes 960 `$24`
tiles; that `$2006` write lands about 27,400 CPU cycles after power-on, inside
the documented NTSC warm-up in which `PPUCTRL`/`PPUMASK`/`PPUSCROLL`/`PPUADDR`
writes are ignored (~29,658 cycles, NESdev "PPU power up state"), so the fill
goes to pattern space and the nametable keeps tile `$00`, the font's `0`. The
same page says a Famicom's PPU leaves reset about one frame before its CPU,
which such pirate carts assume; with the warm-up removed in a scratch run the
menu draws on a clean background. RustyNES emulates the front-loader here;
whether to model a Famicom power-on is a console-model decision, not a mapper
fix. The board itself matches `INES_Mapper_212`.

### Seventh long-tail batch — v1.3.0 "Bedrock" best-effort sweep (14 families, 87 → 101)

The v1.3.0 Workstream D1 Tier-2 sweep, ported from the GeraNES reference.
Simple discrete / homebrew / multicart boards with no IRQ, no
on-cart audio, and no per-cycle / A12 hook (`MapperCaps::NONE`); **register-decode
unit-tested only and not accuracy-gated** (see the tiering note below).

| iNES | Submapper | Name | Audio | IRQ | Status | Notes |
|------|-----------|------|-------|-----|--------|-------|
| 29 | — | Sealie RET-CUFROM | — | — | landed (v1.3.0 / S8) | Homebrew. 16K PRG (data bits 4-2) + 8K CHR-RAM bank (data bits 1-0); fixed last PRG bank at `$C000`. |
| 31 | — | INL NSF-style (2A03 Puritans) | — | — | landed (v1.3.0 / S8) | Eight 4K PRG slots latched at `$5FF8-$5FFF`; CHR-RAM; power-on fixes the `$F000` slot to the last bank. |
| 58 | — | Multicart | — | — | landed (v1.3.0 / S8) | Address-decoded PRG (16/32K mode) + CHR + mirroring bit; data byte ignored. |
| 60 | — | Reset-based 4-in-1 multicart | — | — | landed (v1.3.0 / S8) | Power-on bank only modelled (reset-latch game selection is host-driven, not exercised in the no_std core). |
| 94 | — | UN1ROM (Senjou no Ookami) | — | — | landed (v1.3.0 / S8) | 16K PRG bank (data bits 4-2, bus conflict) + fixed last bank at `$C000`; CHR-RAM. |
| 101 | — | Jaleco JF-10 CHR latch | — | — | landed (v1.3.0 / S8) | Fixed 32K PRG; 8K CHR bank latched via a write to the `$6000-$7FFF` window. |
| 107 | — | Magic Dragon | — | — | landed (v1.3.0 / S8) | One `$8000-$FFFF` latch: 32K PRG = data>>1, 8K CHR = data. |
| 111 | — | GTROM / Cheapocabra | — | — | landed (v1.3.0 / S8); rewritten and promoted to Curated v2.9.6 | Homebrew. 32K PRG + 16K CHR-RAM (two 8K banks) + two 8 KiB nametable pages, four-screen. See the v2.9.6 section below for the register window, bonus RAM and self-flashing. Since v2.9.0 (re-audit NC-02) a save state whose PRG, CHR or nametable bank is one the register cannot produce is refused; before, it loaded and the next fetch panicked. |
| 143 | — | Sachen TCA01 | — | — | landed (v1.3.0 / S8) | NROM-128 (mirrored) + a simple protection read at `$4020-$5FFF` returning `(~addr & 0x3F) \| 0x40`. |
| 177 | — | Hengedianzi | — | — | landed (v1.3.0 / S8) | 32K PRG + mirroring bit (bit 5) from one `$8000-$FFFF` latch; CHR-RAM. |
| 179 | — | Hengedianzi variant | — | — | landed (v1.3.0 / S8) | 32K PRG via `$5000-$5FFF` (data>>1) + mirroring bit (bit 0) via `$8000-$FFFF`; CHR-RAM. |
| 218 | — | Magic Floor | — | — | landed (v1.3.0 / S8); single-screen wirings fixed v2.9.8 | No PRG/CHR-ROM banking; the pattern tables and nametables are both the console's 2 KiB CIRAM. The header wires CIRAM A10 to one PPU address line (NESdev `INES_Mapper_218`): flags 6 `$A1` = A10, `$A0` = A11, `$A8` = A12 (1 KiB per pattern table, one screen in bank 0), `$A9` = A13 (all pattern space bank 0, the nametable bank 1). `parse` re-reads raw bit 0 when bit 3 is set, because the generic parser folds it into `FourScreen`; before v2.9.8 both single-screen wirings fell back to A10, and *Magic Floor* (`$A9`) drew its nametable over its own tiles. |
| 231 | — | 20-in-1 multicart | — | — | landed (v1.3.0 / S8) | Address-decoded dual 16K PRG banks + a mirroring bit; CHR-RAM. |
| 234 | — | Maxi 15 / BNROM-like multicart | — | — | landed (v1.3.0 / S8) | Two latch regs (`$FF80-$FF9F` / `$FFE8-$FFF8`) selecting 32K PRG + 8K CHR in NINA-style or CNROM-style sub-mode. |

### Eighth long-tail batch — v1.4.0 "Fidelity" best-effort sweep (12 families, 101 → 113)

The v1.4.0 Workstream G Tier-2 sweep, ported from the
concretely-documented nesdev decode tables (and the `Mesen2` / `GeraNES`
reference implementations). Simple discrete / homebrew / multicart boards with
no IRQ, no on-cart audio, and no per-cycle / A12 hook (`MapperCaps::NONE`);
**register-decode unit-tested only and not accuracy-gated** (see the tiering
note below).

| iNES | Submapper | Name | Audio | IRQ | Status | Notes |
|------|-----------|------|-------|-----|--------|-------|
| 28 | — | Action 53 homebrew multicart | — | — | landed (v1.4.0 / S9); rewritten v2.9.3 | `$5000-$5FFF` selects one of four registers (`$00` CHR bank, `$01` inner PRG, `$80` mode, `$81` outer PRG), written through `$8000-$FFFF` with no bus conflicts. PRG follows the wiki's 12-row mode × outer-size table (32 KiB, UNROM #180, UNROM #2; 32-256 KiB outer), the fixed UNROM half resolved as 32 KiB. 32 KiB CHR-RAM in four 8 KiB banks; D4 of a `$00`/`$01` write selects the 1-screen page. Powers on with the last 16 KiB at `$C000`. |
| 30 | — | UNROM-512 | — | — | landed (v1.4.0 / S9) | Homebrew. Latch `[N CC P PPPP]`: 16K PRG (bits 0-4) + 8K CHR-RAM/ROM (bits 5-6) + nametable bit (bit 7); fixed last bank at `$C000`. Bus-conflict / flash wiring keyed off submapper + battery (sub 0 w/o battery or sub 2 = bus conflicts on `$8000-$FFFF`; sub 0 w/ battery or sub 1/3/4 = no conflicts, latch only on `$C000-$FFFF`, `$8000-$BFFF` = flash window). Since v2.9.6 the flash window reaches a modelled SST39SF040 and the four-screen board maps the last 8 KiB of CHR-RAM over `$2000-$3EFF` (see the v2.9.6 section below). |
| 63 | — | NTDEC 0324 (Powerful 250-in-1) | — | — | landed (v1.4.0 / S9) | Address-decoded multicart: 16/32K PRG bank + mirroring bit; CHR-RAM. |
| 76 | — | NAMCOT-3446 (Namco 109) | — | — | landed (v1.4.0 / S9) | MMC3-style `$8000`/`$8001` register pairs select two 8K PRG banks (fixed last two) + four 2K CHR banks; header-fixed mirroring. |
| 174 | — | NTDEC 5-in-1 | — | — | landed (v1.4.0 / S9) | Address-decoded 16/32K PRG bank + 8K CHR bank + mirroring bit. |
| 225 | — | ColorDreams 72-in-1 | — | — | landed (v1.4.0 / S9) | Address-decoded `A~[.HMO PPPP PPCC CCCC]`: CHR A0-A5, PRG A6-A11, mode A12 (16/32K), mirror A13, high bit A14; plus a `$5800-$5FFF` 4-nibble scratch-RAM block. |
| 226 | — | 76-in-1 BMC | — | — | landed (v1.4.0 / S9) | Two `$8000-$FFFF` regs (even/odd): reg0 `[PMOP PPPP]` (bits 4-0 = PRG bits 4-0, bit 5 mode 0=32K/1=16K, bit 6 mirror 0=H/1=V, bit 7 = PRG bit 5), reg1 bit0 = PRG bit 6; CHR-RAM; RESET clears both registers. Before v2.9.8 bits 5-7 were read as PRG bit 5 / mode / mirroring, so *76-in-1* drew one repeated tile and *Super 42-in-1* opened on its second page under the wrong mirroring. |
| 227 | — | 1200-in-1 BMC | — | — | landed (v1.4.0 / S9) | Address-decoded 16/32K PRG + fixed-high-bank mode + mirroring bit; CHR-RAM. |
| 229 | — | 31-in-1 BMC | — | — | landed (v1.4.0 / S9) | Address-decoded: low bits zero = fixed NROM-32 menu bank, else a 16K bank pair + 8K CHR + mirroring bit. |
| 233 | — | 42-in-1 reset-based BMC | — | — | landed (v1.4.0 / S9) | DATA-driven `[MMOP PPPP]` (4-bit page, bit5 mode 0=16K/1=32K, bits6-7 mirroring); the reset-selected outer block is host-driven (fixed power-on `0`); CHR-RAM. **Not implementable from documentation (v2.9.8 survey):** `Unknown Multi Cart w-Galaxian [p1].nes` (512 KiB PRG + 256 KiB CHR) is solid blue at every capture point. It is the "Unknown Multicart 1" that `INES_Mapper_233` itself says "does *not* follow the description in this doc at all": 32 NROM-128 games, no menu, each game's CHR page eight below its PRG page, mirroring varying per game, "might even be assigned the wrong mapper number". No documented board fits it, so none is guessed. |
| 242 | — | Waixing 43-in-1 (Wai Xing Zhan Shi) | — | — | landed (v1.4.0 / S9) | `$8000-$FFFF` address-decoded 32K PRG (inner = A2-A4, outer = A5-A6) + mirror bit (A1); 8K work-RAM at `$6000-$7FFF`; CHR-RAM. |
| 246 | — | Fong Shen Bang / G0151-1 | — | — | landed (v1.4.0 / S9) | Four `$6000-$6003` PRG (8K) + four `$6004-$6007` CHR (2K) banking regs; 2K PRG-RAM at `$6800-$6FFF`; `$6003` powers on to `$FF` and `$FFE4-$FFFF`-family reads force PRG A17 high; CHR-ROM, header-fixed mirroring. |

These were boot-smoked against real unlicensed / pirate / multicart dumps (10 of
the 12 families have a library dump; 28 + 174 do not and are register-decode +
save-state tested only).

**Mapper 28 was wrong until v2.9.3.** It shifted the outer bank instead of
masking it, masked the inner bank to one bit, fixed the wrong half in both
UNROM modes, ignored the CHR bank register and the D4 mirroring write, and
powered on with bank 1 at `$C000` (a review thread on #97 reported the banking
half). It is now pinned by a unit test that checks every mode value, outer and
inner bank against the wiki table transcribed row for row, and by
`tests/roms/nes-test-roms/other/test28.nes`: the board passes that ROM's
power-on check, which it used to fail, and then stops at its check 2 exactly
where Mesen and Nestopia do (compared black-box, screen output only). The boot-smoke caught a shared `cpu_read_unmapped`
inversion (it had also been latent in the pre-existing m132 + m143) that
open-bused the whole PRG window so the board never booted, plus several decode
errors (m225/m226/m233/m242/m246) — all corrected. 7 of the 10 staged dumps now
render a real screen headless; the other 3 (30/63/233) boot + run real menu code
but are input-/reset-gated. See `screenshots/besteffort/README.md` for the full
matrix and the per-mapper fix log.

### Ninth long-tail batch — v1.5.0 "Lens" best-effort sweep (10 families, 113 → 123)

The v1.5.0 Workstream F Tier-2 sweep, ported from the
concretely-documented nesdev decode tables (and the `Mesen2` / `GeraNES` /
`puNES` reference implementations). Small pirate / unlicensed / multicart
boards; eight are hook-free (`MapperCaps::NONE`) and two carried a simple
CPU-cycle (M2) IRQ (`MapperCaps::CYCLE_IRQ`, m40 + m250 — no A12 hook). m250's
was corrected in v2.9.7 to the MMC3 scanline counter on A12 (its row below);
m40 keeps the M2 counter its page describes.
**Register-decode + save-state unit-tested only and not accuracy-gated** (see
the tiering note below).

| iNES | Submapper | Name | Audio | IRQ | Status | Notes |
|------|-----------|------|-------|-----|--------|-------|
| 40 | — | NTDEC 2722 (*SMB2J* pirate) | — | M2 cycle | landed (v1.5.0 / S10) | Fixed PRG layout (`$8000`/`$A000`/`$E000` = banks 4/5/7) with one switchable 8K window at `$C000`, selected by an `$E000` write (bits 0-2); 12-bit M2 IRQ that arms on `$A000`, asserts at 4096, and disables/acks on `$8000`; CHR-RAM. |
| 81 | — | NTDEC Super Gun | — | — | landed (v1.5.0 / S10) | CNROM-like single `$8000-$FFFF` register: 16K PRG (bits 2-3, `$C000` half fixed last) + 8K CHR (bits 0-1); header mirroring. |
| 95 | — | NAMCOT-3425 (*Dragon Buster*) | — | — | landed (v1.5.0 / S10) | MMC3-subset `$8000`/`$8001` register port (no A12 IRQ); CHR reg-0 bit 5 drives one-screen mirroring select; CHR-ROM. |
| 112 | — | NTDEC ASDER / Huang-1 | — | — | landed (v1.5.0 / S10) | Indexed `$8000`(idx)/`$A000`(data) port (no A12 IRQ) for two 8K PRG + 2K/1K CHR slots; `$E000` bit 0 = mirroring; `$C000`/`$E000` PRG fixed last two. |
| 137 | — | Sachen 8259D | — | — | landed (v1.5.0 / S10) | `$4100`(cmd)/`$4101`(data) protection board: 32K fixed PRG select (cmd 5) + four 2K CHR banks (cmds 0-3) + CHR outer (cmd 4) + mirroring (cmd 7). |
| 156 | — | DIS23C01 DAOU (Open Corp) | — | — | landed (v1.5.0 / S10; decode corrected in the coverage pass) | CHR-nibble registers `$C000-$C00F` decode the 1K slot as `(addr&0x03)+(addr>=0xC008?4:0)` with bit 2 selecting the high/low nibble array; `$C010` = 16K PRG; `$C014` = H/V mirroring from a single-screen-A power-on (Mesen2 `DaouInfosys`). |
| 162 | — | Waixing FS304 (*San Guo Zhi II*) | — | — | landed (v1.5.0 / S10; decode corrected in the coverage pass) | PRG bank composed from individual A15-A20 bits across `$5000`/`$5100`/`$5200` with a `$5300` mode selector (NESdev table; reset boots 32K bank #2); 8K battery PRG-RAM at `$6000-$7FFF`; 8K CHR-RAM; header mirroring. |
| 178 | — | Waixing educational series (FS305) | — | — | landed (v1.5.0 / S10; decode corrected in the coverage pass) | `$4800` bit 0 = mirroring, bits 1-2 = PRG mode (NROM-256/BNROM, UNROM, NROM-128, UNROM-variant); 16K bank = `(reg2<<3)\|(reg1&0x07)`; 8K work-RAM at `$6000`; CHR-RAM. |
| 244 | — | Decathlon (Mega Soft) | — | — | landed (v1.5.0 / S10; decode corrected in the coverage pass) | Data-decoded multicart: the written DATA byte selects through two scramble LUTs with bit 3 choosing CHR (`LUT_CHR[(v>>4)&7][v&7]`) vs PRG (`LUT_PRG[(v>>4)&3][v&3]`); CHR-ROM, header mirroring (Mesen2/puNES). |
| 250 | — | Nitra (*Time Diver Avenger*) | — | PPU A12 (MMC3) | landed (v1.5.0 / S10; decode corrected in the coverage pass; IRQ corrected v2.9.7) | MMC3-register-compatible, but the register data is carried in address bits A0-A7 and the even/odd line in **A10** (`addr & 0x0400`, Mesen2 `MMC3_250`); MMC3 banking subset + the MMC3 scanline IRQ counter (A12 through MMC3's filter). Until v2.9.7 the IRQ was an M2-clocked 8-bit reload counter, which the page does not support ("a regular MMC3 chip connected in [a] different way"); *Time Diver Avenger*'s splits landed at arbitrary points and its playfield drew from the wrong CHR banks (`T-COMMERCIAL-GARBLE`); CHR-ROM. |

These are register-decode + save-state unit-tested only (no redistributable
fixture is committed), and structurally excluded from the AccuracyCoin / oracle
gate by the BestEffort tier classifier.

### Per-mapper screenshot-coverage decode pass

A boot-coverage pass (the auto-discovering `external_coverage` harness +
per-mapper commercial dumps) surfaced a cluster of BestEffort boards that booted
to a blank/few-colour frame. Cross-checking each against puNES / Mesen2 / the
NESdev wiki corrected real decode bugs in **m143, m147, m150, m156, m162, m177,
m178, m185, m227, m233, m244, m250** (details in `CHANGELOG.md`); each now renders a
real screen for the staged dumps. All are BestEffort (off the AccuracyCoin
oracle), so AccuracyCoin holds 139/141 (the two newest upstream PPU tests are known gaps) and the `mapper_tier_honesty`
gate stays green. A handful of titles remain blank and are documented
follow-ups: 4 of the 5 m162 FS304 RPGs need the `$5000.7` CHR auto-switch (the
core decode is proven by *The Mummy* rendering); m036 TXC needs a proper TXC-
chip port (`$4000-$4FFF & 0x200` register window) rather than the flat decode;
m040 / m063 / m111 / m202; and 2 m227 pirate hacks need the m227-hack `$6000`
WRAM. Vs. System DualSystem games (Balloon Fight / Mahjong / Tennis / Wrecking
Crew) stay blank by design on this single-system core.

A later boot-coverage pass cleared three more blanks. **m030 UNROM-512** (Wampus,
PROTO DERE .NES) booted blank because the board unconditionally applied bus
conflicts; the self-flashing carts set the iNES battery bit, which on submapper
0 means *no* bus conflicts (and the banking latch responds only to
`$C000-$FFFF`, with `$8000-$BFFF` the flash window) — both now render gameplay.
**m080 Taito X1-005** (Kyonshiizu 2) was missing the `$7EFE` `$C000` PRG
register (only two of three switchable PRG banks were modelled), stranding the
reset bank; with all three banks it renders its title screen. **m185 Seicross**
(CRC `0F05FF0A`) is a CHR-disable copy-protection title that loops forever unless
CHR reads back as *disabled* for its protection latch (`$21`); its GoodNES dump
is iNES-1.0 mapper 185 submapper 0, but it is really submapper 4 (enabled iff
the latch low bits are `0`). The fix is a frontend per-game DB submapper
correction (`game_database.txt`, applied by `apply_header_overrides`, which now
promotes an iNES-1.0 header to NES 2.0 when a non-zero submapper override is
set) — the mapper's existing submapper-4 rule already matches FCEUX `Sync181` /
BizHawk's Seicross special-case, so the core is untouched. The `external_coverage`
boot-smoke used to feed raw bytes to `Nes::from_rom` and bypass the frontend DB,
so Seicross captured blank there — a harness limitation, not a decode bug. **As of
v2.3.4 the harness applies the database** (extracted into the `rustynes-gamedb`
crate so both consumers share one copy), and Seicross renders.

The three converted Waixing `(Wxn)` dumps staged under `mapper-030-` are Waixing
FS005 (iNES mapper 176 submapper 2) misdetected as mapper 30 by GoodNES. **As of
v2.3.4 they are routed by the loader**, on a rule that needs no database entry
because the header refutes itself: UNROM-512 is a CHR-RAM-only board, so a
mapper-30 image declaring CHR-ROM cannot be describing the board it claims. Two of
the three boot; `Chu Liu Xiang` renders no tiles and remains open.

#### Mapper 15 — K-1029 / K-1030P multicart

Two deliberate departures from the bare board, both v2.3.4, because the staged
corpus is dominated by single-game hacks rather than the multicarts the board
shipped as:

| | behaviour | why |
| --- | --- | --- |
| CPU `$6000-$7FFF` | 8 KiB PRG-RAM | The K-1029 has none. The mapper-15 hacks of `Doraemon`, `Dragon Ball`, `Z Gundam` and others are conversions of games that expect work RAM there, and several set the battery flag. Without it they read open bus and hang. Decoded *before* the bank latch, so it cannot shadow a register. |
| PPU `$0000-$1FFF` | CHR-RAM writable in every mode | The board is CHR-RAM-only; write-protecting it in some banking modes was modelling a restriction the hardware does not have, and it blanked four ROMs. |

The PRG-RAM is serialized, so it round-trips a save-state. Its arrival lengthened
the state blob without a version bump, so until v2.9.8 `load_state` accepted
**both** lengths and cleared the RAM on the shorter one. Since v2.9.8 (ADR 0042)
the shorter, pre-v2.3.4 length is `MapperError::WrongLength` (named `Truncated`
before v3.0.0).

#### Mapper 154 — NAMCOT-3453

Mapper 88 plus a single one-screen nametable bit, and nothing else. Implemented as
a third `Namco118Board` variant rather than a new type, because that is what the
hardware is: the CHR A16/A12 split, the bank registers, and the fixed PRG banks
are all mapper 88's.

| | |
| --- | --- |
| `$8000-$FFFF` bit 6 | nametable select — 0 = one-screen page A, 1 = page B |
| everything else | identical to mapper 88, including the CHR A16 ↔ PPU A12 split |

The bit is decoded across the **whole** `$8000-$FFFF` range, which is the one
thing easy to get wrong: it shares a byte with the bank-select register, but that
register is only decoded at `$8000-$9FFF` on the associated Namco 108. So the
nametable bit is read on every write in the range — odd addresses carrying bank
*data* included — and a test pins each of `$8001`, `$9FFF`, `$A000`, `$C001`,
`$FFFF`.

This makes mirroring **mutable state** on a board family where it had been
constant, so `SAVE_STATE_VERSION` moves to 2 to carry it. A v1 blob never held a
mirroring byte and would have restored the wrong CIRAM page; it is refused since
v2.9.8 (ADR 0042).

Used by exactly one game, *Devil Man*, whose dump is headered mapper 88 and
corrected to 154 by the per-game database.

#### Mapper 243 — Sachen SA-020A

The **same eight-register ASIC** as mapper 150, on the PCB it was designed for.
NESdev records this under mapper 243's Errata: the SA-150 board "connects the
same ASIC differently, changing the meaning of the CHR-bank-related register
bits". So this is a `Sa020aBoard` variant of the existing type, not a new one —
the `$4100`/`$4101` index/data protocol, the three-bit register file, the
readback, and the four mirroring modes are all shared, and only the bank decode
branches.

| register | SA-020A (243) | SA-150 (150) |
| --- | --- | --- |
| PRG 32 KiB bank | `R5[1:0]` (A16..A15) | `R5 \| R2[0]` |
| CHR 8 KiB bank | `R2[0]` (A13) ∥ `R4[0]` (A14) ∥ `R6[1:0]` (A16..A15) | `R2[0]<<3 \| R4[0]<<2 \| R6[1:0]` |

Note that both boards use the same three registers — R2 is the CHR **LSB** on the
SA-020A and the **MSB** on the SA-150. That inversion is the whole reason the two
need separate mapper numbers, and a test asserts one register setting lands on
different banks for the two boards.

Mirroring comes from `R7[2:1]`, including selector 0's S0-S0-S0-S1 layout (three
nametables sharing CIRAM page 0, only the fourth distinct) which the existing
`resolve_nametable` already implemented for mapper 150.

Used by exactly one game, 美女拳 *Honey Peach* (SA-006), headered mapper 150 and
corrected to 243 by the per-game database. Both maxima are reachable: 128 KiB PRG
(4 × 32 KiB from two bits) and 128 KiB CHR (16 × 8 KiB from four bits).

#### Mapper 176 submapper 2 — WAIXING-FS005/FS006

Mapper 176 is the 8025 enhanced-MMC3 ASIC, whose variants are **incompatible** and
split by NES 2.0 submapper. `m176_bmc_fk23c.rs` covers submapper 0/1 (BMC-FK23C)
and, since v2.3.4, submapper 2:

| area | FS005 behaviour |
| --- | --- |
| `$A001` | RAM Configuration Register once bit 5 is set; behaves as MMC3 `$A001` until then |
| `$A001.0-1` | 8 KiB WRAM bank at `$6000-$7FFF` (the board carries 32 KiB, not 8) |
| `$A001.2` | first 8 KiB of CHR space becomes CHR-RAM (mapper-195-like mixed memory) |
| `$A001.6` | clear ⇒ `$5000-$5FFF` is **not** a register window; it reads/writes the second 4 KiB of WRAM bank 2 |
| `$A000` | two mirroring bits; single-screen A/B only while the RAM Configuration Register is enabled |
| `$8000` | bank-select values `$46`/`$47` swap registers 6 and 7 — but `$06`/`$07` do not |
| `$5xx0.3/7`, `$5xx2.5/6/7` | PRG A21, A22, A25, A23, A24 |
| decode mask | `$E003`, not the stock MMC3 `$E001`, so `$9FFF` does not alias `$8001` |

The register-window disable is the whole mechanism behind the documented Waixing
copy-protection sequence: the game parks `$5000-$5FFF` over WRAM, stashes three
bytes there, re-enables the registers, and reads the same bytes back through
`$6000-$7FFF` with WRAM bank 2 selected.

Implemented from the NESdev wiki `INES_Mapper_176` page. Unlike the FK23C banking
transforms in the same file — a disclosed Mesen2 derivation, see `NOTICE` and
`docs/originality-and-provenance.md` §1 — no reference-emulator source was
consulted for any of the FS005 code.

**v2.9.8 survey: staged dumps that are not the board their header names.**
Recorded rather than forced, because the board code is right for the board the
header names and no per-game override is added:

- `21-in-1 [p1][!]` (labelled 133, Sachen SA-72008). 128 KiB PRG and 64 KiB
  CHR, more than the 72008's `$4100` latch can address (one PRG bit, two CHR
  bits, `INES_Mapper_133`), and it never writes `$4100`: its first writes go to
  `$8001`, `$F000`, `$F020` and `$F001`, an address-encoded latch. Re-headed in
  scratch as mapper 225 (ET-4310 / K-1010, `INES_Mapper_225`: banks and
  mirroring in the address of a `$8000-$FFFF` write) it boots to its "21 GAME"
  menu; as 133 it is tile noise.
- *Zhan Guo Si Chuan Sheng (C&E) (Unl)* (labelled 132, TXC 22211). The
  `INES_Mapper_132` page names this exact image: GoodNES sets it to 132, but it
  is a mapper hack with the CHR banks rearranged for some emulators' mapper 132,
  "not on the above implementation based on studying the circuit board"; the
  correct board is mapper 173. Here its title and map screen render, and in-game
  screens draw from the wrong CHR bank. Mapper 173 is not implemented.
- *Uchuusen - Cosmo Carrier (J) [!]* (mapper 78): see the row in the first
  long-tail table; its header's nametable bit names Holy Diver.

*BB Car (Asia) (En) (Unl)* (mapper 152) is not a defect: it never writes a
mapper register, and the "0123456789" screen it shows is its own. Its
controller loop reads the Start bit and discards it, so the harness's START taps
never leave that screen; an A press starts the race.

### Tenth batch — v2.9.6 "Roster" (17 families, 174 → 191)

Every family here is written from its vendored NESdev page, named in the table;
no reference-emulator source was read. The MMC3-based boards live in
`mmc3_boards.rs`, a new module that wraps the project's own `Mmc3` as a register
file and IRQ counter. They are kept out of `mmc3_clones.rs` on purpose: that
file carries a Mesen2 derivation record for its MMC3 variants, and mixing
independently written boards into it would blur which regions the record covers.

Tiers follow the maintainer's rule for this release (2026-09-30). A family is
**Curated** when its page gives exact register masks: ADR 0011's "precise decode
spec", backed by register-decode unit tests and a synthetic CC0 boot fixture in
`crates/rustynes-test-harness/tests/roster_boards.rs`. It is **BestEffort** when
the page gives only Disch's notes, or masks marked "probably".

| iNES | Sub | Board (page) | Tier | IRQ | Notes |
|------|-----|--------------|------|-----|-------|
| 12 | 0 | Gouder SL-5020B (`INES_Mapper_012`) | Curated | MMC3A (the alternate / NEC behaviour) | `$4100` mask `$E100`: CHR A18 per pattern table (bit 0 for PPU A12=0, bit 4 for A12=1), outside the ASIC so unaffected by `$8000` bit 7. The *Dragon Ball Z 5* language bit read at the same address returns 0 on D0: the page says every known copy is hard-wired to Chinese but not which level that is, so this is an assumption. Submapper 1 (the Magic Card 4M extraction) is a different device and is not supported. |
| 37 | — | SMB + Tetris + NWC (`INES_Mapper_037`) | Curated | MMC3 | The 74HC161 at `$6000-$7FFF`, written only while the MMC3's `$A001` allows a PRG-RAM write; PRG A16 = Q0·Q1 + Q2·M16, A17 = CHR A17 = Q2 (the page's NAND equations). Write-only (open bus). The CIC reset clears it (`Mapper::reset`). |
| 45 | — | GA23C (`INES_Mapper_045`) | Curated | MMC3 | Four outer registers written in turn at `$6000` (mask `$F001`): CHR-OR, PRG-OR, CHR-AND + high bits, PRG-AND (inverted) + lock. `$6001` resets and unlocks, as does a soft reset. `$5000-$5FFF` reads the menu DIP switch on D0. WRAM only when a NES 2.0 header declares it. The page gives no power-on or reset value for the outer registers. Power-on, soft reset and `$6001` set `[$00, $00, $0F, $00]`, so CHR-AND passes every MMC3 CHR bit (T-GA23C-POWERON, maintainer 2026-10-05). The evidence: two *Famicom Yarou* menus (54 and Vol.5) draw with CHR banks 0-7 before their first outer-register write, which needs CHR-AND at `$A` or more. CHR-RAM is unbanked: addressed straight from PPU A10-A12, past the MMC3 banks and the outer CHR registers (T-GA23C-CHRRAM, fixed v3.0.1). The page is silent on CHR-RAM; mapper 372, the GA23C with a ROM/RAM switch, documents its RAM as unbanked, and *Famicom Yarou Vol.1* uploads all 8 KiB with every CHR register at 0 and then draws with R0-R5 = 0, 2, 4, 5, 6, 7. |
| 47 | — | Spike V'Ball + NWC (`INES_Mapper_047`) | BestEffort | MMC3 | One block bit in the PRG-RAM window, gated like mapper 37. The page is Disch's notes only. The Kasheng *2-in-1 (Mortal Kombat 6, Samurai Spirits)* is often labelled 47 (512 KiB CHR, `$6000` written with `$C0`/`$60` while WRAM is disabled); it is NES 2.0 mapper 291 (`NES_2_0_Mapper_291`), which this project does not implement. |
| 74 | — | Waixing 43-393 (`INES_Mapper_074`) | Curated | MMC3 | CHR banks 8 and 9 are 2 KiB of CHR-RAM. 8 KiB work RAM. A dump that writes `$A001` with bit 5 set (`$EC`/`$ED`), `$5FF3`, or MMC3 registers 8-11 is a Waixing FS005 re-release, mapper 176 submapper 2 (`INES_Mapper_176`), not this board. |
| 83 | 0/1/2 | Cony / Yoko (`INES_Mapper_083`) | Curated | 16-bit M2, up or down | Three PRG modes, `$6000` ROM (subs 0/1) or 32 KiB banked WRAM (sub 2), 1 KiB / 2 KiB / outer-banked CHR. On iNES the submapper follows the page's CHR-size heuristic. The DIP and scratch-RAM masks are "probably" on the page and are decoded inside `$5000-$5FFF` only. |
| 91 | 0 | JY830623C / YY840238C (`INES_Mapper_091`) | Curated | 64 unfiltered PPU A12 rises | 2 KiB CHR x4, 8 KiB PRG x2 + fixed 16 KiB, outer bank from the `$8000-$9FFF` write address. |
| 91 | 1 | EJ-006-1 | BestEffort | M2, down by 5 every 4th cycle | The page does not say when it asserts; this board asserts on the decrement that would go below zero, then stops until `$7007`. |
| 105 | — | NES-EVENT (`NES_EVENT`) | Curated | 30-bit M2 timer | An embedded `Mmc1` runs the serial port. PRG locked to the first 32 KiB until `$A000` I goes 0 then 1; `O` picks the chip; the timer fires at `$20000000 \| DIP<<25`, default the tournament setting (DIP C, `NWC_TOURNAMENT_DIP`). Reset relocks. |
| 121 | — | Kasheng A9711 / A9713 (`INES_Mapper_121`) | BestEffort | MMC3 | Protection array at `$5000`, the bit-reversed `$8001` latch and `$8003` index overrides, CHR A18 from PPU A12 (A9711) or a `$5180` outer bank (A9713, told apart by 512 KiB PRG). The page's masks are "probably". |
| 153 | — | Bandai LZ93D50 + WRAM (`INES_Mapper_153`) | Curated | LZ93D50 | In `m016_bandai_fcg.rs` (outside its EEPROM region): `$8000-$8003` bit 0 is the outer 256 KiB PRG bank, `$800D` bit 5 the WRAM enable; 8 KiB of unbanked CHR-RAM, addressed directly and never through the CHR bank registers (in v2.9.6 and v2.9.7 all eight 1 KiB windows aliased the first 1 KiB, which striped *Famicom Jump II*'s title); the WRAM is the battery save. Per the page, the game itself freezes on a black screen when it boots with zero-filled WRAM (the default fresh-cartridge state here) and runs after a soft reset. |
| 163 | — | Nanjing FC-001 (`INES_Mapper_163`) | Curated | — | 32 KiB PRG from `$5000`/`$5200` with the mode register's D0/D1 swap (not on 1 MiB boards) and the boot-in-bank-3 rule; feedback register at `$5100`/`$5500`; the automatic CHR-RAM switch latches PPU A9 on each rise of A13, modelled as a nametable access that follows a pattern access. |
| 191 | — | (`INES_Mapper_191`) | BestEffort | MMC3 | CHR bank bit 7 selects 2 KiB CHR-RAM. Disch's notes. The 192 KiB / 160 KiB translations need the non-power-of-two mirroring below. *Q Boy* (Sachen, CHR-RAM) is often labelled 191 but writes only `$4100`/`$4101`: it is a Sachen 8259 board, mapper 141 (`Sachen_8259`), and does not boot as 191. |
| 192 | — | Waixing FS308 (`INES_Mapper_192`) | Curated | MMC3 | CHR banks 8-11 are 4 KiB of CHR-RAM. |
| 194 | — | (`INES_Mapper_194`) | BestEffort | MMC3 | CHR banks 0 and 1 are 2 KiB of CHR-RAM. Disch's notes. |
| 195 | — | Waixing FS303 (`INES_Mapper_195`) | Curated | MMC3 | A PPU write to a bank mapped to ROM selects which banks are RAM, from that bank's number (the page's eight-row table; power-on `$80`). CHR A10-A12 reach the RAM, so `$80` and `$82` share it. The optional 4 KiB at `$5000` appears when a NES 2.0 header declares PRG-RAM. |
| 228 | — | Action 52 / Cheetahmen II (`INES_Mapper_228`) | Curated | — | The register latches the write ADDRESS (mirroring, chip, page, size) and data (CHR low bits). On the 1.5 MiB image chip 3 is the third 512 KiB and chip 2 is open bus. Reset clears it. |
| 249 | — | Waixing T9552 (`T9552`) | Curated | MMC3 | `$5000` selects a PRG A14-A17 / CHR A12-A17 scrambling pattern; the file is stored in the `$5000=$00` order. Pinned to the page's worked example. |

**Non-power-of-two ROM sizes.** Every board in `mmc3_boards.rs` reduces a PRG
or CHR-ROM bank onto the image by the doubling algorithm of
`Non_power_of_two_ROM_size` (the smaller ROM is mirrored up to the next power of
two: 192 KiB reads as `ABCC`, 160 KiB grows 20 -> 24 -> 32 banks), not by a plain
modulo. A modulo sent the MMC3's all-ones fixed bank to bank 15 of a 24-bank
image, which holds no reset vector, so the 192 KiB and 160 KiB mapper 191
translations booted to a blank frame. For power-of-two images the two are
identical.

**Mapper 4, corrected.** The NES 2.0 submappers of mapper 4 were mis-assigned:
submapper 1 was read as "NEC" and 4 as Sharp. `NES_2_0_submappers` defines 1 as
the **MMC6**, 2 as MMC3C with hard-wired mirroring, 3 as Acclaim's **MC-ACC**, 4
as the **NEC** MMC3 and 5 as the **T9552** scrambler. All five are now modelled:
the MMC6's 1 KiB of internal RAM with its per-half read and write enables
(`MMC6.md`), `$A000` ignored on submapper 2, the MC-ACC's falling-edge A12 counter
behind a /8 prescaler, NEC on 4, and T9552 on 5 (the file in the `$02` order). An
iNES 1.0 StarTropics stays a plain MMC3, as the submapper page advises. The
`Mmc3Revision` names were also wrong: MMC3A is the *alternate* behaviour, not
Sharp. MC-ACC (4.3) was BestEffort at v2.9.6, because its prescaler reset and
phase come from a forum measurement the MMC3 page links, not from the page.

**MC-ACC, promoted to Curated (v2.9.7).** Six Acclaim titles with NES 2.0
submapper-3 headers were run headless with scripted input and compared against
the same dumps forced to submapper 0. The dumps are local and gitignored: Alien
3, Terminator 2, The Incredible Crash Dummies, WWF King of the Ring, WWF
WrestleMania: Steel Cage Challenge and T&C Surf Designs. On the first run,
v2.9.6's model broke four of them: HUDs missing, garbled title text, and Alien 3
black. The cause was not the model but the PPU. It reported one A12 edge per
scanline, and the MC-ACC's /8 prescaler expects the hardware's eight
(`docs/ppu-2c02.md`, dots 257-320). With the PPU fixed, all six boot cleanly.
The four that use the raster split draw it correctly only under MC-ACC: their
status bars, portraits and title cards appear, and forced to submapper 0 they
corrupt. The other two draw identically under either model. The same PPU fix
brings mapper 91 submapper 0 ("64 unfiltered rises") and the J.Y. ASIC's A12
mode ("unfiltered, eight per scanline") to their documented rates; both had
been eight times slow.

**MMC3-core boards with their own counter.** `mmc3_clones`, 176 (FK23C), 268
(CoolBoy) and 513 (Sachen 9602) clocked their IRQ counter on every A12 rise,
which was right only while the PPU delivered one rise per line. Since v2.9.7
they apply MMC3's filter (`a12_filter.rs`: a rise counts after three CPU cycles
low), and their save states pack the filter into the old `last_a12` byte
(format versions bumped; older states load with the byte read as the level).

**GTROM (111), promoted to Curated.** Three documented behaviours were missing:

- the latch decodes `/ROMSEL`, A14 and A12 high, which is `$5000-$5FFF` and
  `$7000-$7FFF` only, and a read there latches the value floating on the bus
  (`Mapper::notify_floating_read`);
- each nametable page is 8 KiB covering `$2000-$3EFF` unmirrored, so
  `$3000-$3EFF` is bonus RAM (`Mapper::nametable_unfolded`);
- PRG is an SST39SF040 that games rewrite to save.

The flash model is `sst39sf040.rs`, written from the datasheet's command table:
byte program, 4 KiB sector erase, chip erase and software ID. Programming only
clears bits. Operation time is not modelled, so every command completes within
its write. The flashed image is the board's battery save. It is
exposed through a new `Mapper::save_data` seam rather than `sram()`, which keeps
meaning "the RAM in the `$6000` window", the thing the open-bus rule, the
libretro memory map and RetroAchievements rely on. `parse` marks the cartridge
battery-backed. The desktop `.sav`, the mobile bridge and libretro's `SAVE_RAM`
all persist `save_data()`, a power cycle keeps it, and a power-on movie resets
it with `clear_save_data`, which on a flash board restores the ROM as loaded
rather than zero-filling it. A save state carries only the 4 KiB sectors that
differ from the ROM. The desktop keys `.sav` files by the ROM's hash, so a
flash image is never applied to a different ROM. RetroArch keys saves by content
name, so a renamed or updated ROM can load an older image there.

UNROM 512 (30) uses the same model on its flashable wiring, which needs the
flash window AND CHR-RAM. The CHR-ROM images headered as mapper 30 are Waixing
FS005 `.WXN` conversions (`UNROM_512.md`). Their MMC3-style register writes
reached the flash in this release's first draft and rewrote 21,602 bytes of
*Shui Hu Zhuan*; they are never flashed. The four-screen board now maps the last
8 KiB of its 32 KiB CHR-RAM over `$2000-$3EFF`, where it used to approximate
that as single-screen.

The promotion is backed by the CC0 fixture in `roster_boards.rs`: the register
window, the floating-read latch, bonus RAM and a byte program through the real
bus, plus the battery flag. The old tier note named "*Ninja Ryukenden*" as
GTROM's only dump. That image is a different board, an MMC1 variant with
CHR-ROM that the page describes only as "non-serialized", and it is not
supported.

**Mutation proof.** 31 mutants, each reverting one documented behaviour above
(an equation, a mask, a lock, an IRQ direction, a dispatch, a hook call, a save
path), were all CAUGHT by the unit tests or `roster_boards.rs`. Two needed a new
test first: the bus's floating-read call, which no mapper-level test can reach,
and the PPU's unfolded `$3xxx` path. The record is in the v2.9.6 plan.

### Mapper 157 — Bandai Datach Joint ROM System (v3.2.0)

Written from `nesdev_wiki/INES_Mapper_157.xhtml` and the pages it links
(`Bandai_FCG_board`, `Bandai_Datach_pinout`, `INES_Mapper_016`). No other
emulator's source was read; the page's link to FCEUX's barcode code was
deliberately not followed. Curated (D23, `docs/compatibility.md`).

| iNES | Sub | Board (page) | Tier | IRQ | Notes |
|------|-----|--------------|------|-----|-------|
| 157 | — | Datach main unit + LZ93D50 game cartridge (`INES_Mapper_157`) | Curated | LZ93D50 (latched 16-bit M2) | A `FcgVariant::Datach` in `m016_bandai_fcg.rs`, built by `BandaiFcg::new_datach`, outside the file's derived EEPROM region except for one opt-in flag (below). Registers mask `$800F` in `$8000-$FFFF` only. `$8008` PRG bank at `$8000` (last bank fixed at `$C000`), `$8009` mirroring, `$800A-$800C` IRQ: as mapper 16 submapper 5. 8 KiB of unbanked CHR-RAM; no CHR banking. `$8000-$8003` bit 3 is the external X24C01's SCL (CHR A13 OUT); `$8004-$8007` do nothing (PA12/PA13 grounded). `$800D` bit 5 is the internal 24C02's SCL, bit 6 both chips' SDA. `$6000-$7FFF` reads: bit 4 the two EEPROMs' data out ANDed, bit 3 the barcode reader. |

**The two EEPROMs.** The main unit's 256-byte 24C02 is always present. The
game cartridge's 128-byte X24C01 exists only when the NES 2.0 header declares
PRG-NVRAM (byte 10 `$10`, *Battle Rush*); it is reached through the existing
X24C01 state machine, on its own clock.

**The battery save holds both (v3.4.0, `T-DATACH-EXT-EEPROM-BATTERY`).** The
board keeps every EEPROM's cells in one image (`BandaiFcg::nv`; the chip
models hold only their protocol state and are handed their slice), and that
image is `sram()` / `save_data()`, so every host persists it through its one
battery file with no host-specific layout code. The layout is fixed and
documented:

| Offset | Bytes | Chip |
|---|---|---|
| `$000-$0FF` | 256 | the main unit's 24C02 |
| `$100-$17F` | 128 | the cartridge's X24C01 (*Battle Rush* only) |

A Datach game without the cartridge chip saves the 256 bytes it always did.
The size is fixed by the header, so a host sees one stable length per ROM,
which libretro's `RETRO_MEMORY_SAVE_RAM` needs. Until v3.4.0 the battery
save was the 24C02 alone and *Battle Rush*'s cartridge-side save was lost at
every power-off (it was kept only in save states).

- **A v3.3.0 save still loads.** *Battle Rush*'s v3.3.0 `.sav` is the 24C02's
  256 bytes. It is the prefix of today's image, so it loads into the 24C02,
  and the cartridge chip is set to its erased state (`$FF`), which is what a
  v3.3.0 session started it at. The board says which lengths it takes
  (`Mapper::accepts_save_len`: its save size, plus 256 on this one board) and
  loads them (`Mapper::load_save_data`); the desktop, web and mobile hosts
  call those instead of comparing lengths themselves, so the rule lives in
  the board. Any other length is refused and the file left untouched, as on
  every board. The next write after a change stores the full 384 bytes.
  libretro needs nothing: RetroArch copies a shorter `.srm` into the start of
  the buffer (`content_load_ram_file` in its `save.c`), which is the 24C02.
- **Why one image and not two files.** The page asks emulators to save each
  EEPROM to its own file, the main unit's shared among all Datach games, as
  the hardware shares it. That needs a second, per-console store on all four
  hosts (desktop, browser, libretro, Android / iOS) and a key that is not the
  ROM's hash; RetroArch exposes one save buffer per content. One image with a
  fixed layout persists both chips everywhere today, and splitting it later
  is a slice at `$100`. The main unit's EEPROM is therefore still saved per
  game, not shared between Datach games (`T-DATACH-SHARED-MAIN-UNIT`, open).
- **Movies and netplay start where they did.** A power-on movie and a netplay
  session clear the save data (`clear_save_data`). The 24C02 still clears to
  `$00` (the trait default it always used); the cartridge chip clears to its
  erased `$FF`, which is what it held at a v3.3.0 movie start (it was not
  save data then, and the power cycle before the clear rebuilt it blank).
  So no movie or netplay frame changes and `EMULATION_EPOCH` does not move.
  That the 24C02 clears to `$00` rather than an EEPROM's erased `$FF` is
  v3.3.0 behaviour, kept on purpose: changing it moves every Bandai EEPROM
  movie and needs an epoch of its own.
- **A Power Cycle now keeps the cartridge chip** on *Battle Rush*, as it
  keeps every battery save (`Nes::power_cycle`, v2.9.0); in v3.3.0 the
  rebuild erased it. A console keeps an EEPROM across power-off. Power Cycle
  is refused during a movie or netplay session, so no replay sees it.
- **Save states are byte-identical to v3.3.0.** The blob still writes the
  main chip's 256 bytes where it always did and the cartridge chip's 128 in
  the mapper-157 tail; `eeprom_save_state_bytes_are_unchanged_since_v3_3_0`
  pins the blobs of mappers 16, 159 and 157 (with and without the cartridge
  chip) to hashes taken from the v3.3.0 code before the change.

**The 24C02 random read (`keeps_word_address`).** A 24C02 random read is a
dummy write of the word address, a repeated START, and a read-mode device
select; the read starts at the address just written. The existing model
cleared the address on every START, so every random read began at `$00`.
*Ultraman Club* reads its file table that way: with the address cleared it
reformatted the EEPROM at every boot and stopped on "no free block"; *Dragon
Ball Z* and *SD Gundam Wars* listed garbage save files. Mapper 157's 24C02
keeps the address across a START, written from that protocol description;
with it all three reach their titles and *SD Gundam Wars* lists four empty
files. Keeping the address is a property of the 24C02, so v3.2.0 applies it to
every board that carries one: mapper 16's LZ93D50 + 24C02 too
(`m016_24c02_random_read_starts_at_the_written_address`, which read `$FF`
from the cleared address before). No `external_coverage` baseline of the six
staged mapper-16 dumps, nor of 157 or 159, moved; it falls under v3.2.0's
`EMULATION_EPOCH` 4 all the same. The X24C01 (mapper 159, and the Datach's
external chip) is unchanged: it takes its word address after every START, so
clearing it there is unobservable.

**Power-on EEPROM contents.** `$FF` throughout. Every game detects that as
unformatted and formats it on first boot (*Ultraman Club* writes `BA` then
its tables, *Dragon Ball Z* `BA` and `BB`).

**The barcode reader** (`crates/rustynes-mappers/src/datach_barcode.rs`). The
page says only that bit 3 returns "a sequence of light and dark periods" as a
card is swiped. The core takes a 13- or 8-digit JAN / EAN code
(`Nes::scan_barcode`, validated at the boundary, wrong check digit included,
as `BarcodeScanError::Malformed`), draws its symbol from the published
EAN-13 / EAN-8 tables (95 / 67 modules), and presents it on bit 3 one module
per 1,000 CPU cycles, between 128 light modules of card margin on each side.
Bit 3 reads 1 over light and 0 over a bar and when no card is passing. The
polarity, the idle level, the module time and the margin are not documented
anywhere found; they were measured black-box against the games:

| Choice | Measurement |
|---|---|
| light = 1, bar = 0 | With the opposite polarity no game reacted to any code at any speed. |
| idle = 0 (dark) | Idle-light left *Battle Rush*'s title showing one of its three menu items and ignoring A. |
| 1,000 cycles / module | Accepted windows (128-module margins): *Dragon Ball Z* Karin's room 200-3,000, *SD Gundam Wars* Barcode Lab 200-3,000, *J.League* team select 400-8,000. |
| 128-module margins | With 32, *Dragon Ball Z* needed 700 or more cycles per module. |

A swipe is therefore about 0.2 s of emulated time, advanced from the mapper's
M2 hook (no wall clock), and its in-flight position is in the mapper's save
state. What the games did with a scan (pinned in `external_extended.rs`):
*Dragon Ball Z* Karin's room turns 4006381333931 into Dende and 96385074 into
Android 18 (HP 96000 / BP 86750 / DP 12750); *SD Gundam Wars*' Barcode Lab
turns 4006381333931 into Rafflesia and 96385074 into the RX-78 Gundam;
*J.League* answers an arbitrary code with "not a team card", having decoded
it. *Ultraman Club*, *Yu Yu Hakusho* and *Battle Rush* boot and play, but a
screen that reads a card was not reached in testing, so the reader is not
verified on them.

**Save state.** The Bandai FCG blob keeps version 2. Mapper 157 appends a
tail after the fields every variant writes: the external SCL level, the
X24C01's 128 bytes when present, and the reader (module count, position,
cycles left, the modules bit-packed). No earlier blob of mapper 157 exists, so
nothing else's layout changed and no version moved; `PPU_SNAPSHOT_VERSION` and
`EMULATION_EPOCH` are untouched, because no frame of any game that loaded
before this change is different.

**Movies and netplay.** A scan is an input that changes emulation. Since
v3.5.0 a `.rnm` movie records it (format 7, `ExtraInput::barcode`, on the
frame the scan preceded: `Nes::barcode_this_frame` holds it until the next
frame starts) and playback swipes it again; netplay still does not carry it. A
host must refuse a scan during movie playback or a netplay session, rather than
let it desynchronise; the desktop does (`session_policy`'s `BarcodeScan`, the
reason shown in the menu, the window and the status line), and allows it while
a movie records. The libretro core, the mobile apps and Lua expose no scan
yet.

**Open bus.** The six undriven bits of a `$6000-$7FFF` read are open bus on
the board and read 0 here, as on mapper 16: the bus consults a partial
driven-bit mask only for `$4020-$5FFF`.

**Mutation proof.** Six mutants, each caught by the unit tests: an L-code
entry, a first-digit parity row, the bit-3 polarity, the external SCL bit,
the 24C02 `keeps_word_address` flag, and the EEPROMs' AND. The polarity and
the EEPROM flag were also caught by the game-level snapshots.

### v3.2.0 — MMC3-derived and multi-mode boards (`mmc3_boards_ext.rs`, `m116_somari.rs`)

Written from the vendored NESdev pages named in the table; no
reference-emulator source was read. The boards live in a new module,
`mmc3_boards_ext.rs`, because their board logic sits in FRONT of the MMC3 (a
register address and index scramble) or rewires the PPU address lines the MMC3
sees, where the `mmc3_boards.rs` boards only post-process its bank outputs. The
MMC3 is still the project's own `Mmc3` used as a register file and IRQ counter.
A family is **Curated** on a D23 record (`docs/compatibility.md`, "Curated
evidence from local dumps"): a local dump that boots to its title, pinned by a
boot snapshot in `external_extended.rs` and a committed screenshot.

| iNES | Sub | Board (page) | Tier | IRQ | Notes |
|------|-----|--------------|------|-----|-------|
| 114 | 0/1 | SuperGame / Hosenkan MMC3 scramble (`INES_Mapper_114`) | Curated | MMC3A (alternate) | Register addresses and `$8000` indices permuted per the page's two tables (submapper 1 is Boogerman's pattern). `$6000` (`M.S. BBBb`) is an NROM override of both PRG halves, NROM-256 when S is set; `$6001` bit 0 is CHR A18. Both answer whatever `$A001` says ("not connected to the MMC3 clone's WRAM interface"). The page gives their mask as "probably `$E001`"; that mask is used. GoodNES labels *Boogerman* (Sugar Softec) mapper 215, the assignment the 215 page says was withdrawn; it never writes `$5007` and boots only as 114.1, so its D23 record uses a copy with a NES 2.0 114.1 header (`Nes::rom_sha256` excludes the header, so the identity is the dump's). |
| 182 | — | (`INES_Mapper_182` → `INES_Mapper_114`) | Curated | as 114 | A duplicate of 114: the 182 page is a redirect, and the 114 page says Hosenkan's 182 games write 0 to `$6000`/`$6001` once and never again. Dispatched as 114 submapper 0; the full 114 model is kept rather than the "no `$600x`" simplification, since the zero writes leave it in the MMC3's own banking. |
| 116 | — | SOMARI-P, Huang-1 / Huang-2 (`INES_Mapper_116`) | Curated | MMC3's, in MMC3 mode only | In `m116_somari.rs`, not the MMC3 module: a supervisor at `$4100` (mask `$E100`) selects a simulated VRC2b (0), MMC3 (1) or MMC1 (2, 3), and bit 2 is CHR A18 in every mode. Each chip keeps its own registers (the project's `Mmc3` and `Mmc1` as register files, the VRC2 written here from `VRC2_and_VRC4`); only the selected one takes `$8000-$FFFF` writes and drives banks, mirroring and the IRQ line. VRC2 PRG / mirroring / PRG answer in all of `$8xxx` / `$9xxx` / `$Axxx`; its CHR selects power up at `$FF`. Switching INTO MMC1 mode empties the serial shift register (new `Mmc1::reset_shift_register`, which leaves `control` alone). Huang-2 (MMC1 PRG written one bit left) is picked by the page's heuristic, PRG and CHR both 128 KiB. Not on the page, chosen here: power-on in VRC2 mode (the page's note that *Somari* relies on the VRC2's power-up CHR value implies it); deselected chips keep receiving A12 and CPU-cycle clocks. Not modelled: the VRC2 `$6000` microwire latch and submapper 3 (one sentence on the page, no registers). All four staged dumps (Somari, Kart Fighter, Garō Densetsu Special, A.V. Girl Fighting) cycle the supervisor through all three chips while initialising and then run in MMC3 mode, so the boot snapshots do not reach the VRC2 and MMC1 bank details, which are unit-tested only. *Samurai Spirits (Rex Soft)* and *Chuugoku Taitei (Sachen)* are labelled 116 by GoodNES; `INES_Mapper_014` names the former (the 8-character Rex Soft version) as mapper 14, and neither was tried here. |
| 165 | — | MMC3 + MMC2 latch (`INES_Mapper_165`) | Curated | MMC3 (Sharp) | The page is Disch's notes only, so three readings are this model's. (1) A 4 KiB CHR page is the MMC3 register over 4 (`$0000`: R0 / R1, `$1000`: R2 / R4 by latch), page 0 being the 4 KiB CHR-RAM and page n reading ROM at n x 4 KiB; `$8000` bit 7 has no effect. (2) The latches are the MMC2's (`$0FD8` / `$0FE8` exactly; `$1FD8-$1FDF` / `$1FE8-$1FEF`), switching after the read. The notes' remark that the game reads "`$xFD0` or `$xFE0` via `$2007`" is not taken as a trigger: triggering on all of `$xFD0-$xFDF` drew a stray bar before every intro line on the Fire Emblem translation, the MMC2 triggers draw it clean. (3) 8 KiB of work RAM by default ("SRAM enabling ... as on your vanilla MMC3"): without it *Fire Emblem Gaiden (Ch)* draws its title as garbage. CHR reads are impure (`chr_reads_are_pure` is false). Curated on its D23 records under this release's rule; under v2.9.6's page-only rule a Disch-notes page would have been BestEffort. |
| 197 | 0-3 | MMC3 with 2 / 4 KiB CHR banks (`INES_Mapper_197`) | Curated | MMC3 | Modelled as the page's WIRING, not its bank table: the MMC3 sees PA10 = PPU A11, PA11 = GND (sub 0, 3), Vcc (sub 1) or PPU A11 (sub 2), PA12 = PPU A12; its CHR outputs A10-A17 drive ROM A11-A18 and PPU A10 drives ROM A10. The page's tables for submappers 0-2 follow (unit-tested row by row), and `$8000` bit 7, which the page does not mention, still swaps the pattern tables as the MMC3 would. Submapper 3's `$6000-$7FFF` register (`S..P`: PRG A17 = P when S is set) is on the WRAM interface, so it is taken only while `$A001` allows a write. PRG, mirroring and IRQ are mapper 4's (Sharp). iNES 1.0 images are wired as submapper 0; the page's submapper 1 / 2 games are identified by PRG CRC32 there, which belongs in the per-game database, and no dump of either is staged. |
| 215 | 0/1 | Sugar Softec UNL-8237 / UNL-8237A (`INES_Mapper_215`) | Curated | MMC3 (Sharp; the page names no revision) | `$5007` (mask `$F007`) selects one of eight scramble patterns (0, 2, 5, 6, 7 are the identity; 1, 3, 4 per the page's tables). `$5000` `MCS. BBBb` is 114's NROM override plus `C`, the 128 KiB mode that replaces PRG A17 / CHR A17 with `$5001` bits 4 / 5; `C` is applied after the NROM override, since the page describes it as a substitution on the A17 line both drive (the order is not stated). `$5001` is the outer bank (sub 0: PRG A18-A19 = bits 0-1, CHR A18-A19 = bits 2-3; sub 1: PRG A18/A19/A20 = bits 0/1/3, CHR A18-A20 = bits 1-3), power-up `$xF` (taken as `$0F`) and restored on a console reset; `$5000` and `$5007` power up `$00`. No WRAM unless a NES 2.0 header declares it. The page's index tables were checked black-box: pattern 1 (the *Super 808* menu, then *Aladdin* selected from it) and pattern 4 (*Mortal Kombat 3 Extra 60*) both run, and pattern 1's index table is the inverse of 114 submapper 0's, as printed. The "alternative outer bank registers in the `$6xxx` range" some multicarts carry are not modelled. |

### v3.2.0 batch: TXC, Tengen, Taito, Waixing, reset multicarts, FFE

Written from the vendored NESdev page named in each row; no reference-emulator
source was read. A family is **Curated** when its page gives exact register
masks AND a local dump boots to a correct title or gameplay screen, recorded
under D23 in `docs/compatibility.md` (title, header-excluded SHA-256, frames,
committed screenshot) and pinned by a byte-identity boot snapshot in
`external_extended.rs`. Otherwise it is **BestEffort**, with the reason here.

| iNES | Sub | Board (page) | Tier | IRQ | Notes |
|------|-----|--------------|------|-----|-------|
| 172 | — | Super Mega P-4070, JV001 (`INES_Mapper_172`, `INES_Mapper_136`) | Curated | — | `m172_m173_txc.rs`. The JV001's Input / Register / Output with CPU D0-D5 wired in reverse; Mode and Invert from CPU D5. `$8000-$FFFF` latches Output (CHR bank = Output & 3 in chip order) and sets mirroring from Invert (H when clear, V when set); the header's mirroring holds until then. PRG fixed 32 KiB. `$4100` reads drive D0-D5 only. *1991 Du Ma Racing* passes its protection check and plays. |
| 173 | — | Idea-Tek ET-xx, 05-00002-010 (`INES_Mapper_173`) | Curated | — | Same module. P/R/Output 3 bits, S/C/V 1 bit; `$4100` reads `[xxxx SRRR]` with S XOR V, D4-D7 open bus. CHR bank = (Output & 1) \| (~V << 1), V read live. Hard-wired mirroring. *Xiao Ma Li*'s 8 KiB EPROM with `/PGM` on pin 27 is not modelled beyond wrapping (the page does not say what a disabled CHR read returns). The TXC *Mahjong Block* (CRC32 `0ACFC3CD`), commonly headered 173, is a mapper 132 board per both pages: as 173 it draws garbage, as the page predicts. |
| 158 | — | Tengen 800037 (`INES_Mapper_158`, `RAMBO_1` Variants) | Curated | RAMBO-1 (A12 / CPU) | `Rambo1::new_m158` in `m064_rambo1.rs`: the RAMBO-1 with CHR A17 on CIRAM A10, as TLSROM is to MMC3. Nametable *n* takes bit 7 of the 1 KiB bank in slot *n* of `$0000-$0FFF`, after the C and K bits (R0/R1 in 2 KiB mode, R0/R8/R1/R9 with K, R2-R5 under C). `$A000` is not connected. Mapper 64's path is unchanged. The NES 2.0 *Alien Syndrome (USA) (Unl)* is the dump; the GoodNES `(Tengen) [!]` image has the same body headered as 118, and the game database lists that CRC as 118 (its NES 2.0 guard leaves the 158 header alone). |
| 207 | — | Taito X1-005, CHR A17 mirroring (`INES_Mapper_207`) | Curated | — | `TaitoX1005::new_m207` in `m080_taito_x1_005.rs`: the X1-005 with CHR A17 on CIRAM A10, so bit 7 of `$7EF0` pages nametables `$2000`/`$2400` and bit 7 of `$7EF1` pages `$2800`/`$2C00` (1scA / 1scB / H). `$7EF6-$7EF7` are ignored. Everything else is mapper 80's, whose path is unchanged. Evidence is the English translation, which carries a 207 header; the original Japanese GoodNES `[!]` is headered 80, and the game database lists its CRC as 80 too, so it does not reach this board. The boot snapshot plays into stage 1 (`Held`: the menu ignores one-frame taps). |
| 252 | — | Waixing VRC4 clone (`INES_Mapper_252`) | Curated | VRC4 (scanline / CPU) | `m252_waixing_vrc4.rs`: the project's `Vrc4` wired as mapper 23 submapper 2 (VRC4e: chip A0 = CPU A2, A1 = CPU A3) with its 8 KiB PRG-RAM and IRQ, plus 2 KiB of CHR-RAM: a 1 KiB slot whose bank register holds exactly 6 or 7 reads and writes RAM instead of CHR-ROM. The page names only the page numbers; a whole-value match is the plain reading, and *San Guo Zhi* rules out a 3-bit match (it would turn the portraits' ROM pages 14 and 23 into RAM). Booting it found the VRC4 IRQ mode-bit inversion described below. Mapper 253's older model in `waixing.rs` is not reused. |
| 6 | 0-7 | FFE Magic Card 1M/2M extraction (`INES_Mapper_006`, `Super_Magic_Card`) | Curated | card counter (M2 / PA12), FDS adapter timer | `ffe_super_magic_card.rs`: a model of the Super Magic Card in Play mode (512 KiB PRG DRAM, 256 KiB CHR-RAM, 32 KiB WRAM, 4 KiB scratch RAM; the image loaded at the start of each). The submapper is the initial latch mode (iNES 1.0 = 1, UN1ROM+CHRSW); all eight latch modes, the 2M and 4M modes, 1 KiB CHR, CHR-RAM nametables, MMC4 mode, the `$42Fx` write-enable, the card's 16-bit up-counter and the FDS adapter's 149 1/3-cycle disk IRQ (`$4025` bit 7, `$4024` acknowledge) are modelled. The trainer loads at `$7000`, and a power-on reset runs `JSR $7003` then the game's vector through a six-byte overlay at `$7200`. GUI and pass-through modes are not modelled. The mirroring-type encoding `A0 << 1 \| D4` is the only one consistent with the page's two initial values. *Contra (J) [hFFE]* draws stage 1 with a black background under its header's horizontal mirroring and correctly under vertical: the game never writes `$42Fx`, so the header decides, and this dump's header bit looks wrong; it is not staged. *Kaiketsu Yanchamaru (J) [hFFE]* (no trainer) boots to a grey screen: its reset vector in every PRG page points at data, so it needs a set-up the dump does not carry; not staged. |
| 8 | — | FFE GNROM-mode extraction (`INES_Mapper_008`) | Curated | as 6 | "A synonym of Mapper 6 submapper 4": the same card in latch mode 4 (32 KiB PRG `D5-D4`, 8 KiB CHR `D1-D0`, CHR write-protected). The two *Doraemon World 3* hack images in `mapper-072-Jaleco72/`, whose masked dirty header is mapper 8, now boot and leave `external_coverage`'s `UNSUPPORTED` list. |
| 17 | 0-3 | FFE Super Magic Card extraction (`INES_Mapper_017`) | Curated | as 6 | The same card starting in 4M mode with `$4504-$4507` on the image's last four 8 KiB banks and 1 KiB CHR enabled (`$4500 = $47`). The submapper is the trainer's load address (`$7000`, `$5D00`, `$5E00`, `$5F00`), and a power-on reset jumps to it instead of the vector. `$4510-$4517` start at 0-7 (the page gives no value). |
| 235 | — | "Golden Game" 150-in-1 / 260-in-1 (`INES_Mapper_235`) | Curated | — | `m235_golden_game.rs`. Address latch `1xMP RNBB xxxA AAAA`: 32 KiB page A within the 1 MiB socket B; R = 1 maps half P of it twice; M picks CIRAM A10 from PPU A11 / A10 (H / V) and N forces page A (1scA), per the page's 74'153 description. RESET and power-on clear the register ("All eleven bits ... cleared on reset"). Sockets: a 4 MiB image fills all four; a 2 MiB image is sockets 0 and 2 ("00b = ROM 0, 10b = ROM 1"); 1 MiB or less is socket 0; an empty socket is open bus. The GoodNES `260-in-1 [p1][!]` header has iNES byte 4 = 0 for its 4 MiB, which iNES cannot express, so `parse` reads no PRG; it is staged with a NES 2.0 header (PRG MSB 1) and its body unchanged. |
| 230 | — | 22-in-1, reset-switched (`INES_Mapper_230`, Disch's notes) | BestEffort (the power-on mode and the reset behaviour are the model's choices; `docs/compatibility.md`) | — | `m230_reset_multicart.rs`. Chip 0 (first 128 KiB, *Contra*): `[.... .PPP]` 16 KiB at `$8000`, page 7 fixed at `$C000`, always vertical. Chip 1 (next 512 KiB): `[.MOP PPPP]`, O = 0 one 32 KiB window at P >> 1, O = 1 page P twice, M = 0 horizontal. The console RESET (`Mapper::reset`) toggles the chip and clears the register. The notes leave the power-on mode and the register across a toggle open: this board powers on in *Contra* and clears the register, and the dump behaves that way (title at power-on, menu after RESET). BestEffort under D23 (ADR 0011, maintainer 2026-10-09): the notes do not state the power-on mode or what RESET does to the register, and the model's choices were never tested against an alternative. |

**VRC4 IRQ mode bit, corrected (v3.2.0).** `m021_vrc4.rs` (mappers 21, 23,
25) read bit 2 of the IRQ control register the wrong way round: `VRC_IRQ`
gives "IRQ Mode (1 = cycle mode, 0 = scanline mode)", and VRC6 and VRC7 had it
right. A game that asked for a scanline split got an IRQ that counted CPU
cycles, roughly 114 times too fast. Found through mapper 252, whose *San Guo
Zhi* sets latch `$52` in scanline mode for its text box and drew the box from
the wrong CHR banks. With the bit corrected, nine staged VRC4 dumps render
differently: *Bio Miracle Bokutte Upa* gains its status bar, the *Wai Wai World
2* map its logo, *Gradius II* reaches "PLAYER 1 START", and *Crisis Force*
loses a black band at the top of the playfield. The `external_coverage` and
`external_real_games` snapshots for them were re-blessed in the same change;
this moves commercial-ROM output, so the release must raise
`EMULATION_EPOCH` (ADR 0045).

### Mapper accuracy tiering (v1.2.0)

Every supported family is classified `Core` / `Curated` / `BestEffort` by
`rustynes-mappers::mapper_tier(id, submapper)` — an **honesty marker** (runtime
behaviour is identical) that keeps the accuracy claim precise as long-tail
coverage grows. `Core` (the original 51) and `Curated` are gated by the
AccuracyCoin / commercial-ROM oracle suites; `BestEffort` (reference-ported
boards with no redistributable fixture, register-decode unit-tested only) is
**excluded** from that gate. The invariant — no `BestEffort` mapper backs an
oracle ROM — is enforced at the classifier level (`BestEffort` is structurally
never accuracy-gated; the three tier id-sets are disjoint) and by the curated
construction of the byte-oracle corpus. See `docs/adr/0011-mapper-tiering.md`.
Current split: **208 families** — 51 Core + 130 Curated (**181 accuracy-gated**) +
27 BestEffort (v2.3.4 added 154 and 243; v2.9.6 added 17 families and promoted
111; v3.2.0 added 17 families, 230 of them BestEffort, and promoted 47, 121,
154, 191 and 243 on local-dump evidence, maintainer decision D23; see above). Submapper-level exceptions: 91.1 and 176.2 are BestEffort, 4.3
(MC-ACC) is Curated (BestEffort until v2.9.7), and 12.1 is unsupported. The **v2.1.0 "Fathom" F3** batch promoted **86** previously-
BestEffort families to Curated: each has a **cleanly-booting** staged
commercial-ROM dump (57 already in `tests/roms/external/` + 29 sourced from
GoodNES v3.23b) wired into a byte-identity boot-snapshot oracle in
`external_extended.rs` (ADR 0011), taking accuracy-gated coverage from 60 → 146.
The remaining **27** BestEffort families (`tier.rs`, `BEST_EFFORT_IDS`) stay
register-decode and save-state unit-tested only — the 16 NES 2.0 high-id boards
(268/286/289/290/299/301/303/305/306/312/320/336/348/349/366/513, which GoodNES
v3.23b's iNES-1.0 headers cannot encode); 8 boards with no matching cart
(29/39/81/104/174/179/238/261); 50, whose only dump (the SMB2j FDS-conversion)
jams at boot; and 2 notes-only boards whose unstated choices were never tested
against an alternative (194, and 230 since v3.2.0). 111 left the list at v2.9.6
(its "Ninja Ryukenden" dump turned out to be a different board; GTROM is
Curated). To keep that long tail from silently rotting,
the **v2.1.0 "Fathom" F3.1 CI boot-smoke sweep**
(`crates/rustynes-test-harness/tests/v21_best_effort_sweep.rs`) exercises the
full parse -> construct -> dispatch -> run-loop integration for **every**
`BestEffort` family: it derives the set live from the `mapper_tier` classifier
(so future promotions in/out are covered automatically), builds a synthetic
minimal iNES / NES 2.0 image for each, and runs ~60 headless frames asserting no
panic and a well-formed framebuffer. It gates *integration robustness only*
(register/bank/tick decode never panics or divides by zero), promotes nothing,
and adds no oracle claim — accuracy stays defined by the Core/Curated gate. The
two NTDEC boards **81**/**174** validate a non-zero-multiple-of-8-KiB CHR-ROM and
honestly reject a CHR-RAM header with a typed `RomError` (not a panic), so the
sweep hands them CHR-ROM geometry. The mapper *implementations* described below
(reference-ported across v1.2.0-v1.8.9) are unchanged by the promotion — only the
tier marker moved. The
v1.6.0 batch ports MMC3-clone variants
(44/49/52/115/134/189/205/238/245/348/366, on a shared MMC3-style core with an
A12 falling-edge IRQ + per-board outer-bank transform), the Sachen 8259 A/B/C
2 KiB-CHR variants (141/138/139 — siblings of the existing 8259D mapper 137),
and discrete unlicensed / FDS-conversion / multicart boards
(42/50 with CPU-cycle IRQs, 46/51/57/104/120/290/301 hook-free). Mapper 35 is
the J.Y. Company single-game "extended" board folded into `m035_jy_asic.rs` (same
silicon as 209). The v1.7.0 batch ports the next reusable-ASIC
BMC/pirate cores: the Waixing **FK23C** 8/16 Mbit BMC (176, `$5000` config +
MMC3 surface + A12 IRQ), **COOLBOY / MINDKIDS** (268, MMC3 + four `$6000` outer
registers), Sachen **9602** (513, MMC3 + PRG-A19/A20 outer) and **3011** (136,
the TXC protection accumulator driving an 8 KiB CHR select), Waixing **164**
(split `$5000`/`$5100` PRG), **253** (*Dragon Ball Z* VRC4-clone, per-1 KiB CHR
regs + CHR-RAM escape + scaled CPU-cycle IRQ) and **286** (BS-5 DIP-gated
multicart), the **Kaiser** FDS-conversion family (56/142 KS202/KS7032 with an
up-counting M2 IRQ, 303 KS7017 with a down-counting M2 IRQ + read-ack, and the
window boards 305/306/312), and the BMC multicarts **261**/**289**/**320**/
**336**/**349**. All are BestEffort: register-decode + save-state round-trip
unit-tested, outside the AccuracyCoin / oracle gate.

**KS7032 (142) has no work RAM at `$6000` (v2.9.5).** `nesdev_wiki/INES_Mapper_142`
gives that window an 8 KiB switchable PRG-ROM bank, selected by bank-select value
4. The model had read a zero work RAM there until an undocumented register set a
ROM flag, and it now always serves the bank. KS202 (56) keeps the RAM its own
page documents. Core ledger F-09.

**MMC3-clone A12/IRQ timing oracle (Fathom F3.3).** The eleven
`Mmc3CloneMapper` boards (44/49/52/115/134/189/205/238/245/348/366) all route
their `$8000`-`$FFFF` register space — including the IRQ ports
`$C000`/`$C001`/`$E000`/`$E001` — into a single shared `Mmc3Clone` core, so the
A12-clocked scanline IRQ is board-independent by construction.
`crates/rustynes-test-harness/tests/mmc3_clone_a12.rs` is a chip-level oracle
(no ROM files, deterministic, runs in the default `cargo test`) that pins that
timing as **additive evidence** — behind the existing `Curated` classification of
the eight Curated members (44/49/52/115/134/189/205/245) and, since the three
high-id boards 238/348/366 are `BestEffort`, giving those real IRQ-timing
evidence too. It does **not** move any tier. The centerpiece drives every
clone board and a reference plain `Mmc3` (Sharp / rev A) through the identical
canonical rendering-scanline A12 sequence and asserts the clone reproduces the
reference's per-scanline IRQ-assert bitmap **bit-for-bit**: the first assertion
lands on rising edge `latch + 1` (an initial `$C001` reload consumes edge 0,
then `latch` decrements reach zero) and re-asserts every `latch + 1` scanlines
once acknowledged. The suite additionally pins the `$E001`/`$E000` enable/ack
gate, the `$C001` reload periodicity, and the A12 **edge filter** (holding A12
high across consecutive reads clocks the counter exactly once — no
double-clock). The reference `Mmc3` *is* the oracle, so any board whose shared
core drifted from MMC3's scanline timing would fail; the non-zero latches used
throughout keep the comparison on the mechanism both cores agree on
unconditionally (the Sharp/NEC reload-to-zero sub-cadence and the ~3-M2-cycle
too-close-edges hardware sub-filter are revision/accuracy nuances outside the
Curated clone's remit).

The v1.8.9 "Backlog" beta.6 batch ports four more well-documented
NTDEC / TXC / discrete-BMC multicart cores, none with an IRQ
(`MapperCaps::NONE`): **NTDEC TC-112** (193, *Fighting Hero* — a `$6000-$7FFF`
four-register surface with one switchable 8 KiB PRG window over three fixed and
three 2 KiB CHR selects), the discrete **BMC 2-in-1** (204, an address-decoded
NROM/UNROM multicart), **NTDEC N625092** (221, a `$8000` mode register + `$C000`
inner-PRG register with NROM / UNROM / NROM-256 sub-modes), and **TXC/BMC-11160**
(299, one value-decoded register selecting a 32 KiB PRG bank + an 8 KiB CHR
bank + mirroring). All four are BestEffort: register-decode + save-state
round-trip unit-tested, outside the AccuracyCoin / oracle gate. The same pass
also widened
the UNIF board-name table (`unif.rs`) — wiring well-known board aliases (e.g.
`11160`→299, `N625092`→221, `COOLBOY`→268, `FK23C`→176, the Kaiser `KS70xx`
family, `BS-5`→286, `SA-9602B`→513, `NTBROM`→68, `SL1ROM`→1, `TEROM`→4) to the
families RustyNES already implements, with board-resolution unit tests for each.

### NSF player (synthetic mapper, v1.1.0)

NSF chiptune files are not cartridges — they have no iNES header and no PPU
program. They are played through a synthetic `NsfMapper` (`nsf.rs`), built by the
dedicated `Nes::from_nsf` path (not `parse`). The mapper serves the program image
(`$8000-$FFFF`, with `$5FF8-$5FFF` 4 KiB bank-switching; those registers are
write-only and read as open bus, per the NSF spec's readable-address list),
8 KiB WRAM at `$6000`,
and a tiny hand-assembled 6502 **driver** at `$5100` (`$5000` until v3.6.0, which put its NMI entry on the MMC5 status port `$5015`, so `play` never ran on an MMC5 NSF); the reset/NMI/IRQ vectors
(`$FFFA-$FFFF`) are overridden to point at the driver. Reset runs `init` for the
selected song. At the standard 60 Hz the driver first sets the frame counter to
4-step mode with the frame IRQ inhibited (`$40` to `$4017`, before `init`, as
nesdev's "NSF" page describes a player doing), runs `init`, waits two vblanks on
`$2002` to clear the PPU's post-reset warm-up, then enables vblank NMI, and the
ordinary 60 Hz NMI calls `play` each frame. (Until v3.6.0 it enabled the NMI at
once, inside the warm-up window v2.9.8 had modelled, so the write was dropped
and `play` never ran from v2.9.8 to v3.5.x; `nsf_standard_rate_calls_play_every_frame`
pins the fix.) at a **non-standard rate** (a PAL 50 Hz tune or any
custom µs divider from the header speed word, on the NTSC console) it instead
disables the APU frame IRQ and arms a mapper **cycle-timer** that raises a
`$5FF1`-acked IRQ every `period` CPU cycles, whose handler calls `play`. Because
this reuses the normal lockstep `run_frame`, the APU produces audio identically to
a cartridge and the determinism contract is untouched. The `Mapper` trait carries
three default-no-op `nsf_*` hooks (song count / current / set) so the bus + `Nes`
can drive track selection without downcasting. Scope: base 2A03 + expansion-chip
audio, NTSC / PAL / custom play rates, and both the classic `NESM` and the extended
chunked `NSFE` containers; the FDS-style `$5FF6/$5FF7` RAM banking remains deferred.

## Edge cases and gotchas

1. **MMC1 consecutive-write bug.** Writes on adjacent CPU cycles after the first are ignored — but only the **data** (bit 0): the bit-7 reset is never ignored (`nesdev_wiki/MMC1.xhtml`, "Consecutive-cycle writes"). *Bill & Ted's Excellent Adventure* needs the data half (an `INC` on `$FF` writes a reset, then a `$00` that must be dropped); *Shinsenden* needs the reset half (it sets bit 7 on an `RRA abs,X`'s second write and crashes if that reset is dropped). Until v2.8.2 the oracle filtered the reset too; the MiSTer RTL did not, and was right (RTL audit R-3.5a). Pinned by `a_reset_on_the_cycle_after_a_write_is_never_ignored` and `a_data_write_on_the_cycle_after_a_reset_is_ignored`.
2. **MMC3 IRQ revision differences.** The Sharp MMC3B / MMC3C (the default) asserts whenever a clock leaves the counter at 0, so a latch of `$00` fires every scanline; the MMC3A and non-Sharp MMC3B (the alternate revision, `Mmc3Revision::Nec`) assert on a 1 -> 0 decrement and on a `$C001` reload to 0, but not when a counter already at 0 reloads 0 by itself. *Star Trek: 25th Anniversary* requires the Sharp behaviour. NES 2.0 submapper 4 selects the alternate revision (submapper 1 is the MMC6); an iNES 1.0 dump uses the default unless `Nes::set_mmc3_revision_override` forces one (v3.1.0). Until the v3.1.0 review this item called Sharp "MMC3A" and put MMC3B on submapper 1, both wrong.

   **The counter rule and the IRQ's deferred output (v2.9.9, T-ORACLE-001).** On each filtered A12 rise the counter follows the NESdev MMC3 page exactly: if the reload flag (set by a `$C001` write) is set or the counter is zero, it reloads from `$C000`'s latch and the flag clears; otherwise it decrements. The IRQ then asserts if the counter is zero and IRQs are enabled. On the default chip that is after any of the three paths; on the alternate chip it is only after a decrement. The IRQ line is raised **at the first per-cycle hook after the rise that asserts it**: `notify_a12` sets `irq_assert_pending_next_cycle`, and the next `notify_cpu_cycle` raises the line. Which CPU cycle that is comes from the bus's order: `Cpu::start_cycle` catches the PPU up to the access and then calls `SystemBus::cpu_clock`, which calls the hook, so a rise caught up before the access is raised in its own cycle and one caught up after it (`end_cycle`) from the next. That is the same split the removed `mmc3-m2-phase-irq` feature made from phase data (this paragraph said "one CPU cycle after the rise" for every rise until the #583 review; ADR 0002's 2026-10-05 correction). An `$E000` write in between cancels the assertion still in flight, as it is the same line. Which rises clock the counter is unchanged; only when the line is seen moves. Until v2.9.9 a `$C001` reload asserted only if the write had cleared a non-zero counter (a latch the page does not have), and the IRQ was seen on the cycle of the rise. That latch made `4-scanline_timing` sub-test 2 pass by raising the IRQ a scanline late, which is what failed sub-test 3. With the page's rule and the deferred output, both `4-scanline_timing` ROMs (`mmc3_test`, `mmc3_test_2`) first fail at sub-test 9 instead of 3, and `mmc3_test/5-MMC3` passes; with the PPU's A12 stream corrected (T-MMC3-BG-A12, `docs/ppu-2c02.md` pitfall 4: the background's fetches at the page's dot 324, and a visible line's dot 0 driving the background CHR address except where the odd-frame skip replaced it) they pass all 13 sub-tests. The delay replaced v2.0.0's `mmc3-m2-phase-irq` feature, which deferred only rises seen in the M2-high half of a cycle; it passed the same ROMs because it makes, in effect, the same split. This form takes that split from the order of the catch-up and the per-cycle hook, with no phase data from the bus, so it is kept and the feature is removed. The MMC3 save-state section is version 4 (it carries the in-flight flag); version 3 is refused. ADR 0002 keeps the history of the earlier attempts.
3. **MMC2/MMC4 latch on tile fetch.** PPU calls a "tile fetched" notification with the tile address; mapper switches CHR bank if tile == `$FD` or `$FE`. Used for Punch-Out's character animations.
4. **MMC5 CHR bank sets.** Two register sets: A (`$5120-$5127`) and B (`$5128-$512B`). The PPU's sprite tile fetch path calls `PpuBus::ppu_read_sprite`, which `SystemBus` forwards to `Mapper::ppu_read_sprite`; MMC5 resolves it through the A set. Every other CHR access (background fetches, `$2007`) goes through `ppu_read` / `ppu_write`, and the MMC5 picks the set itself, from its own decode of the PPU's `$2000` (8x16 sprites) and `$2001` (render enables), delivered by `Mapper::notify_ppu_register_write` at the undecoded address, since the chip ignores mirrors. Per the NESdev MMC5 page and the hardware tests it cites: in 8x8 mode only the A set is used, for everything; in 8x16 mode background fetches use the B set while rendering, and `$2007` uses the set written last (reset to A by selecting 8x8), except that `$2007` reads use the A set while extended attributes are on. Which registers drive which window follows `$5101` exactly as the page's table gives, and a register value indexes banks of the selected size. **Changed in v2.9.9 (T-MMC5-8X8-SET):** before it, 8x8 mode drew backgrounds from the B set, sprites read the A set as 1 KiB banks whatever `$5101` said, and the 8, 4 and 2 KiB modes indexed 1 KiB banks and read B registers the table does not use. **The split and extended-attribute overrides (v3.5.1, T-MMC5-LATCH-VBLANK):** the split's `$5202` bank and an ExGrafix tile's 4 KiB bank replace the set's bank only for an access inside the same rendering-fetch predicate the B-set choice uses (the background-fetch window with `$2001` rendering on), because the page describes both as substitutions to background fetches while the PPU renders. Outside it a `$2007` CHR access uses the set above, whatever tile was latched last. Until v3.5.1 the latched bank applied whenever it was set, so a `$2007` read in vertical blank read the last rendered tile's bank. The split's nametable and attribute substitution (`nametable_fetch` reading ExRAM) is gated by the same predicate: outside it a `$2007` nametable read follows `$5105`; until v3.5.1 it returned the split's ExRAM byte (found by the MiSTer sibling's `mapper5latch490` stimulus).
5. **VRC2/4 mapping confusion.** Different VRC2/4 variants share iNES mapper IDs but route registers differently. NES 2.0 submappers disambiguate.
6. **Namco 163 N163 audio enable bit.** Disabled by default; ROM must set it. Some ROMs forget; default-on causes glitches in those.
7. **Bus conflict timing.** The bus-conflict-AND happens at the time of the write; emulators that compute the conflict after the bank-switch read get it wrong.
8. **PRG-RAM enable bit.** Some mappers (MMC1, MMC3) have a PRG-RAM enable that, when clear, causes reads to return open bus and writes to be ignored. Required for *Low G Man* music to play correctly under MMC3.
9. **NES 2.0 submapper routing.** VRC2/VRC4, MMC3 revision, BNROM/NINA,
   bus-conflict variants, and some multicarts require submapper-specific
   dispatch. Treat an iNES fallback as a compatibility guess, not proof of
   board identity.
10. **Expansion audio mix levels.** VRC6, Sunsoft 5B, Namco 163, MMC5, VRC7,
    and FDS audio use different cartridge output paths and board-dependent
    levels. Keep mapper audio behind explicit state and tests so future PAL,
    Famicom adapter, or front-loader mix options can be added without changing
    mapper banking behavior.
11. **A PRG window larger than the PRG-ROM mirrors it** (v2.9.0, re-audit
    NC-01). Mappers 46, 57, 58, 61, 62, 202 and 212 accept an image smaller
    than their 32 KiB window (16 KiB, or an odd count of 16 KiB banks for 202
    and 212), and the window's index ran past the end: a panic on the first
    fetch from `$C000-$FFFF` in 32 KiB mode. The index is now taken modulo the
    ROM length, as MMC3 always did, which is what a smaller part does on
    hardware with its missing address lines. It is the identity for every
    image whose window fits. Pinned by `tests/undersized_prg.rs`. Validate a
    board's reads against the ROM length, not against the bank count: a count
    clamped with `.max(1)` is still a count of banks that do not exist.
12. **`load_state` must refuse any value the board's own registers cannot
    produce** (v2.9.0, re-audit NC-02 / NC-06 / NC-07). A field a fetch or
    clock uses unmasked, restored raw, passes the load and fails on the next
    tick, after the restore's rollback can help. Three were found: GTROM
    (111) banks (an index panic), the BS-5 (286) DIP setting, which must be
    one of its four (a shift overflow in `1 << (dip + 4)`), and the FDS audio
    cycle prescaler, which must be 0-15 (an add overflow). Validate before
    assigning, and return `MapperError::Invalid`.

## Test plan

- **`holy_diver_battery_test`** / **`holy_mapperel`** (tepples): detects mappers and verifies bank reachability for each PRG/CHR bank. Wired into CI as the **mapper bank-reachability + IRQ regression net** (`crates/rustynes-test-harness/tests/holy_mapperel.rs`, gated on `--features test-roms`): the 19 committed zlib-licensed ROMs (`tests/roms/holy_mapperel/`) are each driven to their settled result screen and pinned by an `insta` framebuffer-hash snapshot, so a silent mapper-detection / bank-layout / RAM-sizing / IRQ regression flips exactly that ROM's hash and fails loudly. The net promotes nothing; it pins the honest current result, including the documented MMC1/FME-7 WRAM-disable residual (see `docs/accuracy-ledger.md`).
- **`mmc3_test_2`** (5 sub-ROMs): MMC3 IRQ behavior including the Sharp/NEC distinction and edge cases.
- **`mmc3_irq_tests`** (blargg): MMC3 IRQ timing.
- **`vrc24test`** (AWJ): all VRC2/4 variants.
- **`AccuracyCoin`** (100thCoin): single-cartridge accuracy battery covering many mappers.
- **Per-mapper boot test**: golden-master framebuffer for the first 60 frames of attract mode of one freely-distributable ROM per mapper.
- **NES 2.0 header corpus**: mapper/submapper fixtures for revision-sensitive
  boards, including MMC3A/B/C, VRC2/VRC4 address wiring, BNROM/NINA, and
  bus-conflict-free board variants.

## Open questions

Re-checked against the code at v3.1.0 (records item DOC-03): the first two
and the fourth are answered, and are kept with their answers rather than
deleted.

- **MMC3 default revision** when iNES (no submapper) is detected. *Answered:*
  the default is Sharp (the "normal" IRQ behaviour); NES 2.0 submapper 4 selects
  the alternate one (MMC3A and the non-Sharp MMC3B), and since v3.1.0 any
  mapper-4 game can be forced either way (`Nes::set_mmc3_revision_override`,
  desktop `[emulation] mmc3_irq_revision`, `T-MMC3-NEC-OVERRIDE`). This line
  used to call the Sharp default "MMC3A", which is the other revision.
- **Mapper #5 (MMC5) audio scope.** *Answered:* the two extra pulses and the
  raw PCM channel landed behind `mapper-audio` (Track C2 / Phase 2.3; the audio
  table in `docs/compatibility.md`).
- **VRC7 FM audio.** YM2413-derived; only Lagrange Point uses it commercially. Banking + IRQ landed in Track C2 / Phase 2.4 (mapper 85; same `mapper-audio` feature flag as VRC6 / Sunsoft 5B / Namco 163 / MMC5). **The FM synthesizer landed** via a clean-room pure-Rust port of `emu2413 v1.5.9` (MIT) at `crates/rustynes-apu/src/opll.rs`; ADR 0006 (`docs/adr/0006-vrc7-audio-landed.md`) supersedes the ADR 0004 deferral. *Lagrange Point* plays with in-game audio (mixed via the `mapper-audio` slot).
- **Pirate / multicart mappers.** *Answered by policy:* none were in the initial
  scope; many are in now (208 families, `docs/STATUS.md`), each admitted under
  the long-tail policy in `docs/compatibility.md` (demand, a fixture or a
  specific NESdev page, and NES 2.0 detection).
