Skip to main content

rustynes_mappers/
homebrew_boards.rs

1//! Modern homebrew flash boards: `INL`-NSF (mapper 31), Magic Floor
2//! (mapper 218), `RET-CUFROM` (mapper 29), and `GTROM` (mapper 111).
3//!
4//! Unlike the pirate boards elsewhere in this crate, these were designed
5//! *after* the console, by homebrew developers who could pick any mapping they
6//! liked -- so they optimise for what a modern toolchain wants rather than for
7//! 1980s discrete-logic cost. Mapper 31 exposes eight independently-latched
8//! 4 KiB PRG slots (chosen so an NSF player can page music banks freely);
9//! Magic Floor uses no CHR memory at all, serving pattern *and* nametable
10//! fetches out of the console's own CIRAM; `GTROM` banks its own nametable
11//! alongside PRG and CHR so a game can double-buffer whole screens.
12//!
13//! A best-effort (Tier-2) board: register-decode correctness verified against
14//! the `GeraNES` reference emulator (cross-referenced, not copied)
15//! and the nesdev wiki, with no commercial-oracle ROM in the tree. Banking math
16//! is direct slice indexing and every bank select wraps with `% count`, so a
17//! register write can never index out of bounds -- required for the `#![no_std]`
18//! chip stack, which cannot afford a panic on a register access.
19//!
20//! See `tier.rs` (`MapperTier::BestEffort`), `docs/adr/0011-mapper-tiering.md`,
21//! and `docs/mappers.md` §Mapper coverage matrix.
22
23#![allow(
24    clippy::bool_to_int_with_if,
25    clippy::cast_lossless,
26    clippy::cast_possible_truncation,
27    clippy::doc_markdown,
28    clippy::match_same_arms,
29    clippy::missing_const_for_fn,
30    clippy::similar_names,
31    clippy::struct_excessive_bools,
32    clippy::too_many_lines,
33    clippy::unreadable_literal
34)]
35
36use crate::cartridge::Mirroring;
37use crate::mapper::{Mapper, MapperCaps, MapperError};
38use crate::sst39sf040::{
39    Sst39sf040, decode_sector_diff, encode_sector_diff, sector_bitmap_len, sector_diff_len,
40};
41use alloc::{boxed::Box, vec::Vec};
42use alloc::{format, vec};
43
44const PRG_BANK_4K: usize = 0x1000;
45
46/// The exact length of a flash board's save state: `fixed` bytes, then the
47/// sector diff (`sst39sf040.rs`). A diff whose bitmap is cut short is
48/// `WrongLength`, reporting the length the bitmap alone needs. A bitmap bit past
49/// the end of the chip is one `encode_sector_diff` never writes, so `Invalid`
50/// (`docs/mappers.md` gotcha 12). The caller has already checked
51/// `data.len() >= fixed`.
52fn flash_state_len(
53    mapper: u16,
54    fixed: usize,
55    flash_len: usize,
56    data: &[u8],
57) -> Result<usize, MapperError> {
58    let tail = &data[fixed..];
59    let bitmap = sector_bitmap_len(flash_len);
60    if tail.len() < bitmap {
61        return Err(MapperError::WrongLength {
62            expected: fixed + bitmap,
63            got: data.len(),
64        });
65    }
66    sector_diff_len(flash_len, tail)
67        .map(|n| fixed + n)
68        .ok_or_else(|| {
69            MapperError::Invalid(format!(
70                "mapper {mapper} flash bitmap marks a sector past the {flash_len}-byte chip"
71            ))
72        })
73}
74const PRG_BANK_16K: usize = 0x4000;
75const PRG_BANK_32K: usize = 0x8000;
76const CHR_BANK_8K: usize = 0x2000;
77const NAMETABLE_SIZE: usize = 0x0400;
78const NAMETABLE_SIZE_U16: u16 = 0x0400;
79
80const SAVE_STATE_VERSION: u8 = 1;
81
82// ---------------------------------------------------------------------------
83// Shared nametable helper (mirrors the one in the other simple-mapper modules).
84// ---------------------------------------------------------------------------
85
86const fn nametable_offset(addr: u16, mirroring: Mirroring) -> usize {
87    let table = (((addr - 0x2000) / NAMETABLE_SIZE_U16) & 0x03) as u8;
88    let local = (addr as usize) & (NAMETABLE_SIZE - 1);
89    let physical = mirroring.physical_bank(table);
90    physical * NAMETABLE_SIZE + local
91}
92
93// ===========================================================================
94// Mapper 31 — INL / NSF-style 4 KiB-banked board ("2A03 Puritans").
95//
96// Eight 4 KiB PRG slots ($8000/$9000/.../$F000), each latched by a write to
97// $5FF8-$5FFF (the low three address bits pick the slot). Power-on fixes the
98// last slot ($F000) to the final 4 KiB bank (0xFF & mask). CHR is 8 KiB RAM.
99// Mirroring header-fixed; no IRQ.
100// ===========================================================================
101
102/// Mapper 31 (`INL`-NSF-style 4 KiB-banked board).
103pub struct Inl31 {
104    prg_rom: Box<[u8]>,
105    chr_ram: Box<[u8]>,
106    vram: Box<[u8]>,
107    prg_slots: [u8; 8],
108    mirroring: Mirroring,
109}
110
111impl Inl31 {
112    /// Construct a new mapper 31 board.
113    ///
114    /// # Errors
115    ///
116    /// Returns [`MapperError::Invalid`] when PRG is not a non-zero multiple of
117    /// 4 KiB.
118    #[allow(clippy::cast_possible_truncation)]
119    pub fn new(
120        prg_rom: Box<[u8]>,
121        _chr_rom: &[u8],
122        mirroring: Mirroring,
123    ) -> Result<Self, MapperError> {
124        if prg_rom.is_empty() || !prg_rom.len().is_multiple_of(PRG_BANK_4K) {
125            return Err(MapperError::Invalid(format!(
126                "mapper 31 PRG-ROM size {} is not a non-zero multiple of 4 KiB",
127                prg_rom.len()
128            )));
129        }
130        // The last 4 KiB bank index is bounded by the slot register width; the
131        // truncation is benign (bank selects wrap by `% count` anyway).
132        let last = ((prg_rom.len() / PRG_BANK_4K).max(1) - 1) as u8;
133        let mut prg_slots = [0u8; 8];
134        prg_slots[7] = last;
135        Ok(Self {
136            prg_rom,
137            chr_ram: vec![0u8; CHR_BANK_8K].into_boxed_slice(),
138            vram: vec![0u8; 2 * NAMETABLE_SIZE].into_boxed_slice(),
139            prg_slots,
140            mirroring,
141        })
142    }
143}
144
145impl Mapper for Inl31 {
146    fn caps(&self) -> MapperCaps {
147        MapperCaps::NONE
148    }
149
150    // The latch window lives at $5FF8-$5FFF (write-only); reads there fall
151    // through to open bus, so the default `cpu_read_unmapped` is correct.
152
153    fn cpu_read(&mut self, addr: u16) -> u8 {
154        if (0x8000..=0xFFFF).contains(&addr) {
155            let count = (self.prg_rom.len() / PRG_BANK_4K).max(1);
156            let slot = ((addr >> 12) & 0x07) as usize;
157            let bank = (self.prg_slots[slot] as usize) % count;
158            self.prg_rom[bank * PRG_BANK_4K + (addr as usize & 0x0FFF)]
159        } else {
160            0
161        }
162    }
163
164    fn cpu_write(&mut self, addr: u16, value: u8) {
165        if (0x5FF8..=0x5FFF).contains(&addr) {
166            self.prg_slots[(addr & 0x07) as usize] = value;
167        }
168    }
169
170    fn ppu_read(&mut self, addr: u16) -> u8 {
171        let addr = addr & 0x3FFF;
172        match addr {
173            0x0000..=0x1FFF => self.chr_ram[addr as usize],
174            0x2000..=0x3EFF => self.vram[nametable_offset(addr, self.mirroring)],
175            _ => 0,
176        }
177    }
178
179    fn ppu_write(&mut self, addr: u16, value: u8) {
180        let addr = addr & 0x3FFF;
181        match addr {
182            0x0000..=0x1FFF => self.chr_ram[addr as usize] = value,
183            0x2000..=0x3EFF => {
184                let off = nametable_offset(addr, self.mirroring);
185                self.vram[off] = value;
186            }
187            _ => {}
188        }
189    }
190
191    fn current_mirroring(&self) -> Mirroring {
192        self.mirroring
193    }
194
195    fn save_state(&self) -> Vec<u8> {
196        let mut out = Vec::with_capacity(1 + 8 + self.vram.len() + self.chr_ram.len());
197        out.push(SAVE_STATE_VERSION);
198        out.extend_from_slice(&self.prg_slots);
199        out.extend_from_slice(&self.vram);
200        out.extend_from_slice(&self.chr_ram);
201        out
202    }
203
204    fn load_state(&mut self, data: &[u8]) -> Result<(), MapperError> {
205        let expected = 1 + 8 + self.vram.len() + self.chr_ram.len();
206        if data.len() != expected {
207            return Err(MapperError::WrongLength {
208                expected,
209                got: data.len(),
210            });
211        }
212        if data[0] != SAVE_STATE_VERSION {
213            return Err(MapperError::UnsupportedVersion(data[0]));
214        }
215        self.prg_slots.copy_from_slice(&data[1..9]);
216        let mut cursor = 9;
217        self.vram
218            .copy_from_slice(&data[cursor..cursor + self.vram.len()]);
219        cursor += self.vram.len();
220        self.chr_ram
221            .copy_from_slice(&data[cursor..cursor + self.chr_ram.len()]);
222        Ok(())
223    }
224}
225
226/// Custom mirroring/CHR-source mode for mapper 218 ("Magic Floor").
227#[derive(Clone, Copy, PartialEq, Eq)]
228enum MagicFloorMode {
229    Vertical,
230    Horizontal,
231    ScreenA,
232    ScreenB,
233}
234
235impl MagicFloorMode {
236    /// Resolve a PPU address (pattern or nametable) to its physical CIRAM
237    /// 1 KiB bank (0 or 1).
238    ///
239    /// The board wires CIRAM A10 straight to one PPU address line, chosen by
240    /// the header (NESdev "INES Mapper 218"): A10 (`$A1`), A11 (`$A0`), A12
241    /// (`$A8`) or A13 (`$A9`). Taking the bit from the full address, rather
242    /// than from a 1 KiB block index, is what makes the two single-screen
243    /// wirings come out right: under A12, pattern table 1 (`$1000-$1FFF`) is
244    /// bank 1 while the nametables (`$2xxx`, A12 = 0) are bank 0; under A13,
245    /// all pattern space (A13 = 0) is bank 0 and the nametables are bank 1.
246    ///
247    /// Provenance note, kept rather than deleted (maintainer rule, 2026-09-22:
248    /// provenance mentions are classified, never removed). Until v2.9.7 this
249    /// function took a 1 KiB block index and its doc said "Matches `GeraNES`
250    /// `customMirroring`", a cross-check against that emulator's source of the
251    /// kind `docs/originality-and-provenance.md` describes under "GeraNES
252    /// specifically". That version was wrong for both single-screen wirings.
253    /// v2.9.8 rewrote it from the NESdev page alone; no reference source was
254    /// opened for the rewrite.
255    const fn physical_bank(self, addr: u16) -> usize {
256        let line = match self {
257            Self::Vertical => 10,
258            Self::Horizontal => 11,
259            Self::ScreenA => 12,
260            Self::ScreenB => 13,
261        };
262        ((addr >> line) & 0x01) as usize
263    }
264}
265
266/// Mapper 218 ("Magic Floor").
267pub struct MagicFloor218 {
268    prg_rom: Box<[u8]>,
269    /// 2 KiB CIRAM serving both the pattern table and nametables.
270    ciram: Box<[u8]>,
271    mode: MagicFloorMode,
272}
273
274impl MagicFloor218 {
275    /// Construct a new mapper 218 board.
276    ///
277    /// # Errors
278    ///
279    /// Returns [`MapperError::Invalid`] when PRG is not a non-zero multiple of
280    /// 16 KiB. Any supplied CHR-ROM is rejected (the board has none). Real Magic
281    /// Floor dumps are 16 KiB (NROM-128-style, mirrored across the 32 KiB CPU
282    /// window); a 32 KiB image is also accepted.
283    pub fn new(
284        prg_rom: Box<[u8]>,
285        chr_rom: &[u8],
286        mirroring: Mirroring,
287    ) -> Result<Self, MapperError> {
288        if prg_rom.is_empty() || !prg_rom.len().is_multiple_of(PRG_BANK_16K) {
289            return Err(MapperError::Invalid(format!(
290                "mapper 218 PRG-ROM size {} is not a non-zero multiple of 16 KiB",
291                prg_rom.len()
292            )));
293        }
294        if !chr_rom.is_empty() {
295            return Err(MapperError::Invalid(format!(
296                "mapper 218 has no CHR-ROM (CIRAM is used as CHR); got {} bytes",
297                chr_rom.len()
298            )));
299        }
300        // The four wirings come from the header's flags-6 bits 0 and 3. The
301        // generic parser drops bit 0 once bit 3 is set, so `parse` decodes the
302        // raw byte and passes `SingleScreenA` for `$A8` (CIRAM A10 = PPU A12)
303        // and `SingleScreenB` for `$A9` (A13). A bare `FourScreen` (a caller
304        // without the raw byte) falls back to the A10 wiring.
305        let mode = match mirroring {
306            Mirroring::Vertical | Mirroring::FourScreen => MagicFloorMode::Vertical,
307            Mirroring::SingleScreenA => MagicFloorMode::ScreenA,
308            Mirroring::SingleScreenB => MagicFloorMode::ScreenB,
309            Mirroring::Horizontal | Mirroring::MapperControlled => MagicFloorMode::Horizontal,
310        };
311        Ok(Self {
312            prg_rom,
313            ciram: vec![0u8; 2 * NAMETABLE_SIZE].into_boxed_slice(),
314            mode,
315        })
316    }
317
318    /// Map a $0000-$1FFF pattern-table address into the 2 KiB CIRAM: the low
319    /// ten bits are the offset, and the wired PPU line supplies CIRAM A10.
320    const fn chr_offset(&self, addr: u16) -> usize {
321        let local = (addr as usize) & (NAMETABLE_SIZE - 1);
322        self.mode.physical_bank(addr) * NAMETABLE_SIZE + local
323    }
324
325    /// Map a $2000-$3EFF nametable address into the 2 KiB CIRAM, by the same
326    /// wiring as [`Self::chr_offset`].
327    const fn nt_offset(&self, addr: u16) -> usize {
328        let local = (addr as usize) & (NAMETABLE_SIZE - 1);
329        self.mode.physical_bank(addr) * NAMETABLE_SIZE + local
330    }
331}
332
333impl Mapper for MagicFloor218 {
334    fn caps(&self) -> MapperCaps {
335        MapperCaps::NONE
336    }
337
338    fn cpu_read(&mut self, addr: u16) -> u8 {
339        if (0x8000..=0xFFFF).contains(&addr) {
340            // Mirror the PRG across the 32 KiB window: a 16 KiB image
341            // (NROM-128-style) repeats, a 32 KiB image maps 1:1.
342            self.prg_rom[(addr as usize - 0x8000) % self.prg_rom.len()]
343        } else {
344            0
345        }
346    }
347
348    fn cpu_write(&mut self, _addr: u16, _value: u8) {}
349
350    fn ppu_read(&mut self, addr: u16) -> u8 {
351        let addr = addr & 0x3FFF;
352        match addr {
353            0x0000..=0x1FFF => self.ciram[self.chr_offset(addr)],
354            0x2000..=0x3EFF => self.ciram[self.nt_offset(addr)],
355            _ => 0,
356        }
357    }
358
359    fn ppu_write(&mut self, addr: u16, value: u8) {
360        let addr = addr & 0x3FFF;
361        match addr {
362            0x0000..=0x1FFF => {
363                let off = self.chr_offset(addr);
364                self.ciram[off] = value;
365            }
366            0x2000..=0x3EFF => {
367                let off = self.nt_offset(addr);
368                self.ciram[off] = value;
369            }
370            _ => {}
371        }
372    }
373
374    fn nametable_fetch(&mut self, addr: u16) -> Option<u8> {
375        Some(self.ciram[self.nt_offset(addr)])
376    }
377
378    fn nametable_write(&mut self, addr: u16, value: u8) -> bool {
379        let off = self.nt_offset(addr);
380        self.ciram[off] = value;
381        true
382    }
383
384    fn current_mirroring(&self) -> Mirroring {
385        match self.mode {
386            MagicFloorMode::Vertical => Mirroring::Vertical,
387            MagicFloorMode::Horizontal => Mirroring::Horizontal,
388            MagicFloorMode::ScreenA => Mirroring::SingleScreenA,
389            MagicFloorMode::ScreenB => Mirroring::SingleScreenB,
390        }
391    }
392
393    fn save_state(&self) -> Vec<u8> {
394        let mut out = Vec::with_capacity(1 + self.ciram.len());
395        out.push(SAVE_STATE_VERSION);
396        out.extend_from_slice(&self.ciram);
397        out
398    }
399
400    fn load_state(&mut self, data: &[u8]) -> Result<(), MapperError> {
401        let expected = 1 + self.ciram.len();
402        if data.len() != expected {
403            return Err(MapperError::WrongLength {
404                expected,
405                got: data.len(),
406            });
407        }
408        if data[0] != SAVE_STATE_VERSION {
409            return Err(MapperError::UnsupportedVersion(data[0]));
410        }
411        self.ciram.copy_from_slice(&data[1..=self.ciram.len()]);
412        Ok(())
413    }
414}
415
416// ===========================================================================
417// Mapper 29 — Sealie RET-CUFROM homebrew.
418//
419// $8000-$FFFF latch: CHR (8 KiB RAM) bank = data & 0x03; PRG (16 KiB) bank =
420// (data >> 2) & 0x07. $8000 reads the selected 16 KiB bank; $C000 is fixed to
421// the last 16 KiB bank. CHR is 8 KiB RAM (32 KiB on the board, but the visible
422// window is 8 KiB selected by the 2-bit CHR bank). Mirroring header-fixed.
423// ===========================================================================
424
425/// Mapper 29 (Sealie `RET-CUFROM`).
426pub struct Cufrom29 {
427    prg_rom: Box<[u8]>,
428    /// 32 KiB CHR-RAM (four 8 KiB banks).
429    chr_ram: Box<[u8]>,
430    vram: Box<[u8]>,
431    prg_bank: u8,
432    chr_bank: u8,
433    mirroring: Mirroring,
434}
435
436impl Cufrom29 {
437    /// Construct a new mapper 29 board.
438    ///
439    /// # Errors
440    ///
441    /// Returns [`MapperError::Invalid`] when PRG is not a non-zero multiple of
442    /// 16 KiB.
443    pub fn new(
444        prg_rom: Box<[u8]>,
445        _chr_rom: &[u8],
446        mirroring: Mirroring,
447    ) -> Result<Self, MapperError> {
448        if prg_rom.is_empty() || !prg_rom.len().is_multiple_of(PRG_BANK_16K) {
449            return Err(MapperError::Invalid(format!(
450                "mapper 29 PRG-ROM size {} is not a non-zero multiple of 16 KiB",
451                prg_rom.len()
452            )));
453        }
454        Ok(Self {
455            prg_rom,
456            chr_ram: vec![0u8; 4 * CHR_BANK_8K].into_boxed_slice(),
457            vram: vec![0u8; 2 * NAMETABLE_SIZE].into_boxed_slice(),
458            prg_bank: 0,
459            chr_bank: 0,
460            mirroring,
461        })
462    }
463
464    fn chr_offset(&self, addr: u16) -> usize {
465        let count = (self.chr_ram.len() / CHR_BANK_8K).max(1);
466        let bank = (self.chr_bank as usize) % count;
467        bank * CHR_BANK_8K + (addr as usize & 0x1FFF)
468    }
469}
470
471impl Mapper for Cufrom29 {
472    fn caps(&self) -> MapperCaps {
473        MapperCaps::NONE
474    }
475
476    fn cpu_read(&mut self, addr: u16) -> u8 {
477        match addr {
478            0x8000..=0xBFFF => {
479                let count = (self.prg_rom.len() / PRG_BANK_16K).max(1);
480                let bank = (self.prg_bank as usize) % count;
481                self.prg_rom[bank * PRG_BANK_16K + (addr as usize & 0x3FFF)]
482            }
483            0xC000..=0xFFFF => {
484                let last = (self.prg_rom.len() / PRG_BANK_16K).max(1) - 1;
485                self.prg_rom[last * PRG_BANK_16K + (addr as usize & 0x3FFF)]
486            }
487            _ => 0,
488        }
489    }
490
491    fn cpu_write(&mut self, addr: u16, value: u8) {
492        if (0x8000..=0xFFFF).contains(&addr) {
493            self.chr_bank = value & 0x03;
494            self.prg_bank = (value >> 2) & 0x07;
495        }
496    }
497
498    fn ppu_read(&mut self, addr: u16) -> u8 {
499        let addr = addr & 0x3FFF;
500        match addr {
501            0x0000..=0x1FFF => self.chr_ram[self.chr_offset(addr)],
502            0x2000..=0x3EFF => self.vram[nametable_offset(addr, self.mirroring)],
503            _ => 0,
504        }
505    }
506
507    fn ppu_write(&mut self, addr: u16, value: u8) {
508        let addr = addr & 0x3FFF;
509        match addr {
510            0x0000..=0x1FFF => {
511                let off = self.chr_offset(addr);
512                self.chr_ram[off] = value;
513            }
514            0x2000..=0x3EFF => {
515                let off = nametable_offset(addr, self.mirroring);
516                self.vram[off] = value;
517            }
518            _ => {}
519        }
520    }
521
522    fn current_mirroring(&self) -> Mirroring {
523        self.mirroring
524    }
525
526    fn save_state(&self) -> Vec<u8> {
527        let mut out = Vec::with_capacity(3 + self.vram.len() + self.chr_ram.len());
528        out.push(SAVE_STATE_VERSION);
529        out.push(self.prg_bank);
530        out.push(self.chr_bank);
531        out.extend_from_slice(&self.vram);
532        out.extend_from_slice(&self.chr_ram);
533        out
534    }
535
536    fn load_state(&mut self, data: &[u8]) -> Result<(), MapperError> {
537        let expected = 3 + self.vram.len() + self.chr_ram.len();
538        if data.len() != expected {
539            return Err(MapperError::WrongLength {
540                expected,
541                got: data.len(),
542            });
543        }
544        if data[0] != SAVE_STATE_VERSION {
545            return Err(MapperError::UnsupportedVersion(data[0]));
546        }
547        self.prg_bank = data[1];
548        self.chr_bank = data[2];
549        let mut cursor = 3;
550        self.vram
551            .copy_from_slice(&data[cursor..cursor + self.vram.len()]);
552        cursor += self.vram.len();
553        self.chr_ram
554            .copy_from_slice(&data[cursor..cursor + self.chr_ram.len()]);
555        Ok(())
556    }
557}
558
559/// Mapper 111 (`GTROM` / Cheapocabra), written from
560/// `nesdev_wiki/output/GTROM.md` (v2.9.6 "Roster": the register window, bonus
561/// RAM and self-flashing were added and the board promoted to Curated).
562///
563/// - **Register** (`GRNC PPPP`): 32 KiB PRG bank, 8 KiB CHR-RAM bank, 8 KiB
564///   nametable page, and two LEDs. The latch clocks when `/ROMSEL`, A14 and
565///   A12 are all high, which is `$5000-$5FFF` and `$7000-$7FFF` and nowhere
566///   else; `$6000-$6FFF` is not decoded. A read there latches too, with the
567///   value floating on the bus ("reading from the register effectively
568///   writes the value of open bus"), which is what
569///   [`Mapper::notify_floating_read`] exists for.
570/// - **PPU RAM** is one 32 KiB chip. The pattern tables use one of its first
571///   two 8 KiB pages, and PPU `$2000-$3EFF` one of its last two, unmirrored.
572///   Each nametable page therefore holds the four nametables plus almost
573///   4 KiB of bonus RAM at `$3000-$3EFF`. The console's CIRAM is disabled.
574/// - **PRG** is an SST39SF040 (`sst39sf040.rs`). Writes to `$8000-$FFFF` are
575///   its commands; command addresses are A14-A0, so `5555h` is CPU `$D555`
576///   and `2AAAh` is `$AAAA` in any bank. The flashed image is the board's
577///   battery save ([`Mapper::save_data`]; `sram()` stays empty, since no
578///   RAM sits at `$6000`), and a save state carries only the
579///   sectors that differ from the ROM.
580///
581/// The LEDs have no emulated effect. Their bits are kept in the register so a
582/// debugger shows them.
583pub struct Gtrom111 {
584    /// The flash contents: PRG-ROM as loaded, plus whatever was flashed.
585    flash: Box<[u8]>,
586    /// The PRG-ROM as loaded, for the sector diff in save states.
587    original: Box<[u8]>,
588    chip: Sst39sf040,
589    /// 16 KiB of pattern-table RAM: two 8 KiB pages.
590    chr_ram: Box<[u8]>,
591    /// 16 KiB of nametable RAM: two 8 KiB pages covering `$2000-$3FFF`.
592    nt_ram: Box<[u8]>,
593    /// The last value latched (`GRNC PPPP`).
594    reg: u8,
595    prg_bank: u8,
596    chr_bank: u8,
597    nt_bank: u8,
598}
599
600/// GTROM save-state layout version. v2 (v2.9.6) grew the nametable pages to
601/// 8 KiB and added the register, the flash state and the flashed sectors.
602const GTROM_STATE_VERSION: u8 = 2;
603
604impl Gtrom111 {
605    /// Construct a new mapper 111 board.
606    ///
607    /// # Errors
608    ///
609    /// Returns [`MapperError::Invalid`] when PRG is not a non-zero multiple of
610    /// 32 KiB.
611    pub fn new(prg_rom: Box<[u8]>, _chr_rom: &[u8]) -> Result<Self, MapperError> {
612        if prg_rom.is_empty() || !prg_rom.len().is_multiple_of(PRG_BANK_32K) {
613            return Err(MapperError::Invalid(format!(
614                "mapper 111 PRG-ROM size {} is not a non-zero multiple of 32 KiB",
615                prg_rom.len()
616            )));
617        }
618        Ok(Self {
619            original: prg_rom.clone(),
620            flash: prg_rom,
621            chip: Sst39sf040::new(),
622            chr_ram: vec![0u8; 2 * CHR_BANK_8K].into_boxed_slice(),
623            nt_ram: vec![0u8; 2 * CHR_BANK_8K].into_boxed_slice(),
624            reg: 0,
625            prg_bank: 0,
626            chr_bank: 0,
627            nt_bank: 0,
628        })
629    }
630
631    #[allow(clippy::cast_possible_truncation)]
632    fn update_register(&mut self, value: u8) {
633        let count = (self.flash.len() / PRG_BANK_32K).max(1);
634        self.reg = value;
635        // `(value & 0x0F) % count` < 16, so the cast cannot truncate.
636        self.prg_bank = ((value & 0x0F) as usize % count) as u8;
637        self.chr_bank = (value >> 4) & 0x01;
638        self.nt_bank = (value >> 5) & 0x01;
639    }
640
641    /// The latch decodes `/ROMSEL` high, A14 high, A12 high.
642    const fn is_register(addr: u16) -> bool {
643        matches!(addr, 0x5000..=0x5FFF | 0x7000..=0x7FFF)
644    }
645
646    const fn chr_offset(&self, addr: u16) -> usize {
647        (self.chr_bank as usize) * CHR_BANK_8K + (addr as usize & 0x1FFF)
648    }
649
650    /// `$2000-$3EFF`, unmirrored within the selected 8 KiB page.
651    const fn nt_offset(&self, addr: u16) -> usize {
652        (self.nt_bank as usize) * CHR_BANK_8K + (addr as usize & 0x1FFF)
653    }
654
655    fn chip_addr(&self, addr: u16) -> usize {
656        (self.prg_bank as usize) * PRG_BANK_32K + (addr as usize & 0x7FFF)
657    }
658}
659
660impl Mapper for Gtrom111 {
661    fn caps(&self) -> MapperCaps {
662        MapperCaps::NONE
663    }
664
665    /// The flash image: what a self-flashing GTROM game saves to. There is
666    /// no RAM at `$6000`, so `sram()` stays empty.
667    fn save_data(&self) -> &[u8] {
668        &self.flash
669    }
670
671    fn save_data_mut(&mut self) -> &mut [u8] {
672        &mut self.flash
673    }
674
675    fn clear_save_data(&mut self) {
676        self.flash.copy_from_slice(&self.original);
677    }
678
679    /// The register is write-only and nothing else lives below `$8000`.
680    fn cpu_read_unmapped(&self, addr: u16) -> bool {
681        addr < 0x8000
682    }
683
684    fn notify_floating_read(&mut self, addr: u16, value: u8) {
685        if Self::is_register(addr) {
686            self.update_register(value);
687        }
688    }
689
690    fn cpu_read(&mut self, addr: u16) -> u8 {
691        if addr >= 0x8000 {
692            let a = self.chip_addr(addr);
693            self.chip.id_read(a).unwrap_or(self.flash[a])
694        } else {
695            0
696        }
697    }
698
699    fn cpu_write(&mut self, addr: u16, value: u8) {
700        if Self::is_register(addr) {
701            self.update_register(value);
702        } else if addr >= 0x8000 {
703            let a = self.chip_addr(addr);
704            self.chip.write(&mut self.flash, a, value);
705        }
706    }
707
708    fn ppu_read(&mut self, addr: u16) -> u8 {
709        let addr = addr & 0x3FFF;
710        match addr {
711            0x0000..=0x1FFF => self.chr_ram[self.chr_offset(addr)],
712            0x2000..=0x3EFF => self.nt_ram[self.nt_offset(addr)],
713            _ => 0,
714        }
715    }
716
717    fn ppu_write(&mut self, addr: u16, value: u8) {
718        let addr = addr & 0x3FFF;
719        match addr {
720            0x0000..=0x1FFF => {
721                let off = self.chr_offset(addr);
722                self.chr_ram[off] = value;
723            }
724            0x2000..=0x3EFF => {
725                let off = self.nt_offset(addr);
726                self.nt_ram[off] = value;
727            }
728            _ => {}
729        }
730    }
731
732    fn nametable_unfolded(&self) -> bool {
733        true
734    }
735
736    fn nametable_fetch(&mut self, addr: u16) -> Option<u8> {
737        Some(self.nt_ram[self.nt_offset(addr)])
738    }
739
740    fn nametable_write(&mut self, addr: u16, value: u8) -> bool {
741        let off = self.nt_offset(addr);
742        self.nt_ram[off] = value;
743        true
744    }
745
746    fn current_mirroring(&self) -> Mirroring {
747        Mirroring::FourScreen
748    }
749
750    fn debug_info(&self) -> crate::mapper::MapperDebugInfo {
751        let mut info = crate::mapper::MapperDebugInfo {
752            mapper_id: 111,
753            name: "GTROM (111)".into(),
754            mirroring: crate::mapper::mirroring_name(Mirroring::FourScreen),
755            ..Default::default()
756        };
757        info.prg_banks
758            .push(("32K".into(), format!("{:#04x}", self.prg_bank)));
759        info.chr_banks
760            .push(("8K".into(), format!("{}", self.chr_bank)));
761        info.extra
762            .push(("nt page".into(), format!("{}", self.nt_bank)));
763        info.extra.push((
764            "LEDs".into(),
765            format!(
766                "red {} green {}",
767                if self.reg & 0x40 == 0 { "on" } else { "off" },
768                if self.reg & 0x80 == 0 { "on" } else { "off" }
769            ),
770        ));
771        info
772    }
773
774    fn save_state(&self) -> Vec<u8> {
775        let mut out = Vec::with_capacity(8 + self.chr_ram.len() + self.nt_ram.len());
776        out.push(GTROM_STATE_VERSION);
777        out.push(self.prg_bank);
778        out.push(self.chr_bank);
779        out.push(self.nt_bank);
780        out.push(self.reg);
781        out.extend_from_slice(&self.chip.to_bytes());
782        out.extend_from_slice(&self.chr_ram);
783        out.extend_from_slice(&self.nt_ram);
784        encode_sector_diff(&self.flash, &self.original, &mut out);
785        out
786    }
787
788    fn load_state(&mut self, data: &[u8]) -> Result<(), MapperError> {
789        let fixed = 7 + self.chr_ram.len() + self.nt_ram.len();
790        if data.len() < fixed {
791            return Err(MapperError::WrongLength {
792                expected: fixed,
793                got: data.len(),
794            });
795        }
796        if data[0] != GTROM_STATE_VERSION {
797            return Err(MapperError::UnsupportedVersion(data[0]));
798        }
799        // v2.9.0 (re-audit NC-02): validate before assigning anything. The
800        // fetch paths index with these banks unmasked (`cpu_read`,
801        // `chr_offset`, `nt_offset`), so a corrupt value loaded cleanly and
802        // panicked on the next fetch, after the restore had returned `Ok`.
803        // These are exactly the values `update_register` can produce: a PRG
804        // bank below the 32 KiB bank count, and 0 or 1 for the CHR-RAM and
805        // nametable banks.
806        let prg_banks = self.flash.len() / PRG_BANK_32K;
807        let (prg_bank, chr_bank, nt_bank) = (data[1], data[2], data[3]);
808        if usize::from(prg_bank) >= prg_banks || chr_bank > 1 || nt_bank > 1 {
809            return Err(MapperError::Invalid(format!(
810                "mapper 111 state banks PRG {prg_bank} / CHR {chr_bank} / NT {nt_bank} \
811                 exceed the board ({prg_banks} PRG banks, 2 CHR, 2 NT)"
812            )));
813        }
814        let chip = Sst39sf040::from_bytes([data[5], data[6]]).ok_or_else(|| {
815            MapperError::Invalid(format!(
816                "mapper 111 flash state {:#04x} {:#04x} is not one the chip produces",
817                data[5], data[6]
818            ))
819        })?;
820        let expected = flash_state_len(111, fixed, self.flash.len(), data)?;
821        if data.len() != expected {
822            return Err(MapperError::WrongLength {
823                expected,
824                got: data.len(),
825            });
826        }
827        // v2.9.9 (libretro re-audit NL-15): straight into the live flash.
828        // `decode_sector_diff` checks the whole diff before its first write
829        // and leaves the buffer untouched when it refuses, so the scratch
830        // copy this used to decode into (512 KiB on the largest board, on
831        // every restore, which run-ahead makes every frame) bought nothing.
832        decode_sector_diff(&mut self.flash, &self.original, &data[fixed..])
833            .ok_or_else(|| MapperError::Invalid("mapper 111 flash diff".into()))?;
834        self.prg_bank = prg_bank;
835        self.chr_bank = chr_bank;
836        self.nt_bank = nt_bank;
837        self.reg = data[4];
838        self.chip = chip;
839        let mut cursor = 7;
840        self.chr_ram
841            .copy_from_slice(&data[cursor..cursor + self.chr_ram.len()]);
842        cursor += self.chr_ram.len();
843        self.nt_ram
844            .copy_from_slice(&data[cursor..cursor + self.nt_ram.len()]);
845        Ok(())
846    }
847}
848
849/// Mapper 28 (Action 53 homebrew multicart).
850///
851/// Implemented from the NESdev wiki "Action 53 mapper" page (vendored at
852/// `nesdev_wiki/output/Action_53_mapper.md`) and pinned to Damian Yerrick's
853/// `test28` ROM (`tests/roms/nes-test-roms/other/test28.nes`).
854///
855/// Four registers are selected through `$5000-$5FFF` (bit 7 = supervisor, bit
856/// 0 = register) and written through `$8000-$FFFF`, with no bus conflicts:
857///
858/// * `$00` CHR bank: bits 0-1 pick one of four 8 KiB banks of the 32 KiB
859///   CHR RAM.
860/// * `$01` inner PRG bank: bits 0-3.
861/// * `$80` mode: bits 0-1 mirroring (0/1 = 1-screen lower/upper, 2 =
862///   vertical, 3 = horizontal), bits 2-3 PRG mode, bits 4-5 outer bank size
863///   (32/64/128/256 KiB).
864/// * `$81` outer PRG bank: all 8 bits.
865///
866/// While mirroring is 1-screen, D4 of a write to `$00` or `$01` replaces
867/// mirroring bit 0 (AxROM's single-screen select); in V/H it is ignored.
868///
869/// PRG resolution follows the wiki's 12-row table: the "o" bits of the 16 KiB
870/// bank number come from the top of the outer register and the "i" bits from
871/// the bottom of the inner register, the number of inner bits growing with the
872/// outer bank size. The fixed half of the UNROM-style modes (`$8000` in mode
873/// 2, `$C000` in mode 3) is resolved as if the size were 32 KiB, so all outer
874/// bits pass straight through. Power-on maps the last 16 KiB at `$C000`; reset
875/// leaves the mapper untouched.
876///
877/// Until v2.9.3 this board shifted the outer bank left instead of masking its
878/// low bits, masked the inner bank to one bit, fixed the wrong half in modes 2
879/// and 3, ignored the CHR bank register and the D4 mirroring write, and
880/// powered on at bank 1 in `$C000` -- so `test28` failed its first check. The
881/// review thread on #97 reported the banking half.
882pub struct Action53M28 {
883    prg_rom: Box<[u8]>,
884    chr_ram: Box<[u8]>,
885    vram: Box<[u8]>,
886    reg_select: u8,
887    chr_reg: u8,
888    inner_prg: u8,
889    mode: u8,
890    outer_prg: u8,
891}
892
893/// Mapper 28's own save-state section version. Version 2 (v2.9.3) carries the
894/// full 32 KiB of CHR RAM; version 1 carried 8 KiB and still loads, into bank 0.
895const M28_STATE_VERSION: u8 = 2;
896/// CHR RAM on an Action 53 board: four 8 KiB banks.
897const M28_CHR_RAM: usize = 4 * CHR_BANK_8K;
898
899impl Action53M28 {
900    /// Construct a new mapper 28 board.
901    ///
902    /// # Errors
903    ///
904    /// Returns [`MapperError::Invalid`] when PRG is not a non-zero multiple of
905    /// 16 KiB.
906    pub fn new(
907        prg_rom: Box<[u8]>,
908        _chr_rom: &[u8],
909        _mirroring: Mirroring,
910    ) -> Result<Self, MapperError> {
911        if prg_rom.is_empty() || !prg_rom.len().is_multiple_of(PRG_BANK_16K) {
912            return Err(MapperError::Invalid(format!(
913                "mapper 28 PRG-ROM size {} is not a non-zero multiple of 16 KiB",
914                prg_rom.len()
915            )));
916        }
917        Ok(Self {
918            prg_rom,
919            chr_ram: vec![0u8; M28_CHR_RAM].into_boxed_slice(),
920            vram: vec![0u8; 2 * NAMETABLE_SIZE].into_boxed_slice(),
921            reg_select: 0,
922            chr_reg: 0,
923            inner_prg: 0,
924            // Power-on: the wiki specifies only that the last 16 KiB sits at
925            // $C000. Mode 0 (32 KiB, size 32 KiB) with every outer bit set
926            // resolves $C000 to bank `...1_1111_1111`, i.e. the last bank of
927            // any power-of-two ROM once reduced modulo the bank count, and
928            // $8000 to the one before it.
929            mode: 0,
930            outer_prg: 0xFF,
931        })
932    }
933
934    /// Resolve the 16 KiB PRG bank serving a CPU address in $8000-$FFFF.
935    ///
936    /// The bank number is `outer << 1 | half`, with its low `size + 1` bits
937    /// replaced by inner-bank bits (the wiki table's "i" positions). For the
938    /// 32 KiB modes the replaced field is `inner << 1 | half`; for the
939    /// switchable half of the UNROM-style modes it is `inner` itself; the
940    /// fixed half keeps `outer << 1 | half` untouched (resolved as size 0).
941    fn prg_bank_for(&self, addr: u16) -> usize {
942        let count16 = (self.prg_rom.len() / PRG_BANK_16K).max(1);
943        let size = u32::from((self.mode >> 4) & 0x03);
944        let prg_mode = (self.mode >> 2) & 0x03;
945        let high = addr >= 0xC000;
946        let half = usize::from(high);
947        let inner = usize::from(self.inner_prg & 0x0F);
948        let outer_bits = (usize::from(self.outer_prg) << 1) | half;
949        // `size + 1` low bits of the bank number come from the inner register.
950        let mask = (1usize << (size + 1)) - 1;
951        let bank = match prg_mode {
952            // BNROM / AOROM: one 32 KiB bank.
953            0 | 1 => (outer_bits & !mask) | (((inner << 1) | half) & mask),
954            // UNROM #180: $8000 fixed (resolved as 32 KiB), $C000 switchable.
955            // UNROM #2:   $8000 switchable, $C000 fixed (resolved as 32 KiB).
956            2 | 3 => {
957                let fixed = if prg_mode == 2 { !high } else { high };
958                if fixed {
959                    outer_bits
960                } else {
961                    (outer_bits & !mask) | (inner & mask)
962                }
963            }
964            _ => unreachable!("PRG mode is two bits"),
965        };
966        bank % count16
967    }
968
969    /// Offset into the 32 KiB CHR RAM for a pattern-table address.
970    fn chr_offset(&self, addr: u16) -> usize {
971        usize::from(self.chr_reg & 0x03) * CHR_BANK_8K + usize::from(addr & 0x1FFF)
972    }
973
974    /// Apply a write's D4 to mirroring bit 0 while the mode is 1-screen.
975    const fn latch_one_screen(&mut self, value: u8) {
976        if self.mode & 0x02 == 0 {
977            self.mode = (self.mode & !0x01) | ((value >> 4) & 0x01);
978        }
979    }
980}
981
982impl Mapper for Action53M28 {
983    fn caps(&self) -> MapperCaps {
984        MapperCaps::NONE
985    }
986
987    fn cpu_read(&mut self, addr: u16) -> u8 {
988        if (0x8000..=0xFFFF).contains(&addr) {
989            let bank = self.prg_bank_for(addr);
990            self.prg_rom[bank * PRG_BANK_16K + (addr as usize & 0x3FFF)]
991        } else {
992            0
993        }
994    }
995
996    fn cpu_write(&mut self, addr: u16, value: u8) {
997        match addr {
998            0x5000..=0x5FFF => self.reg_select = value & 0x81,
999            0x8000..=0xFFFF => match self.reg_select {
1000                0x00 => {
1001                    self.chr_reg = value & 0x03;
1002                    self.latch_one_screen(value);
1003                }
1004                0x01 => {
1005                    self.inner_prg = value & 0x0F;
1006                    self.latch_one_screen(value);
1007                }
1008                0x80 => self.mode = value & 0x3F,
1009                _ => self.outer_prg = value,
1010            },
1011            _ => {}
1012        }
1013    }
1014
1015    fn ppu_read(&mut self, addr: u16) -> u8 {
1016        let addr = addr & 0x3FFF;
1017        match addr {
1018            0x0000..=0x1FFF => self.chr_ram[self.chr_offset(addr)],
1019            0x2000..=0x3EFF => self.vram[nametable_offset(addr, self.current_mirroring())],
1020            _ => 0,
1021        }
1022    }
1023
1024    fn ppu_write(&mut self, addr: u16, value: u8) {
1025        let addr = addr & 0x3FFF;
1026        match addr {
1027            0x0000..=0x1FFF => {
1028                let off = self.chr_offset(addr);
1029                self.chr_ram[off] = value;
1030            }
1031            0x2000..=0x3EFF => {
1032                let off = nametable_offset(addr, self.current_mirroring());
1033                self.vram[off] = value;
1034            }
1035            _ => {}
1036        }
1037    }
1038
1039    fn current_mirroring(&self) -> Mirroring {
1040        match self.mode & 0x03 {
1041            0 => Mirroring::SingleScreenA,
1042            1 => Mirroring::SingleScreenB,
1043            2 => Mirroring::Vertical,
1044            _ => Mirroring::Horizontal,
1045        }
1046    }
1047
1048    fn save_state(&self) -> Vec<u8> {
1049        let mut out = Vec::with_capacity(6 + self.vram.len() + self.chr_ram.len());
1050        out.push(M28_STATE_VERSION);
1051        out.push(self.reg_select);
1052        out.push(self.chr_reg);
1053        out.push(self.inner_prg);
1054        out.push(self.mode);
1055        out.push(self.outer_prg);
1056        out.extend_from_slice(&self.vram);
1057        out.extend_from_slice(&self.chr_ram);
1058        out
1059    }
1060
1061    fn load_state(&mut self, data: &[u8]) -> Result<(), MapperError> {
1062        // Version 1 (before v2.9.3) carried 8 KiB of CHR RAM; it restores into
1063        // bank 0 with the other three banks cleared. Its register bytes mean
1064        // the same thing, so only the CHR length differs.
1065        let version = *data.first().ok_or(MapperError::WrongLength {
1066            expected: 1,
1067            got: 0,
1068        })?;
1069        let chr_len = match version {
1070            M28_STATE_VERSION => self.chr_ram.len(),
1071            1 => CHR_BANK_8K,
1072            v => return Err(MapperError::UnsupportedVersion(v)),
1073        };
1074        let expected = 6 + self.vram.len() + chr_len;
1075        if data.len() != expected {
1076            return Err(MapperError::WrongLength {
1077                expected,
1078                got: data.len(),
1079            });
1080        }
1081        self.reg_select = data[1] & 0x81;
1082        self.chr_reg = data[2] & 0x03;
1083        self.inner_prg = data[3] & 0x0F;
1084        self.mode = data[4] & 0x3F;
1085        self.outer_prg = data[5];
1086        let mut cursor = 6;
1087        self.vram
1088            .copy_from_slice(&data[cursor..cursor + self.vram.len()]);
1089        cursor += self.vram.len();
1090        self.chr_ram.fill(0);
1091        self.chr_ram[..chr_len].copy_from_slice(&data[cursor..cursor + chr_len]);
1092        Ok(())
1093    }
1094}
1095
1096// ===========================================================================
1097// Mapper 30 — UNROM-512 (RetroUSB / InfiniteNESLives / Broke Studio).
1098//
1099// A single latch register decodes as `[N CC P PPPP]`: bits 0-4 = 16 KiB PRG
1100// bank at $8000, bits 5-6 = 8 KiB CHR-RAM bank, bit 7 = nametable select
1101// (only when the cart is wired for software-controlled mirroring). $C000 is
1102// fixed to the last 16 KiB bank. CHR is 32 KiB RAM (carts with no CHR-ROM)
1103// or, for the converted Waixing `.WXN` dumps, CHR-ROM. No IRQ.
1104//
1105// Submapper / battery semantics (NESdev "UNROM 512", verified against the
1106// Mesen2 `UnRom512` board):
1107//
1108//   * Submapper 0 *without* the battery bit, or submapper 2: the latch
1109//     responds to the whole $8000-$FFFF range and the board has BUS CONFLICTS
1110//     (the written value is ANDed with the PRG byte at that address).
1111//   * Submapper 0 *with* the battery bit, or submappers 1/3/4: NO bus
1112//     conflicts; the latch responds only to $C000-$FFFF (A14 high) and
1113//     $8000-$BFFF is the flash-write window: a write there reaches the
1114//     SST39SF040 at bank * 16 KiB + (addr & $3FFF), so `$9555` in bank 1 is its
1115//     `5555h` and `$AAAA` in bank 0 its `2AAAh` (`UNROM_512.md`). Since
1116//     v2.9.6 the chip is modelled (`sst39sf040.rs`) and the flashed image is
1117//     the battery save, where before v2.9.6 the write was dropped.
1118//
1119// The battery bit, not a save-RAM presence, is what selects the no-bus-conflict
1120// wiring on iNES (submapper 0). Self-flashing homebrew such as *Wampus* and the
1121// *PROTO DERE .NES* beta set it; applying bus conflicts to those carts ANDs the
1122// boot-time bank-switch value with ROM and jumps the CPU into garbage (a solid
1123// backdrop frame). See `docs/mappers.md`.
1124//
1125// Nametable arrangement bits in iNES byte 6 (`%....N..M`, N = bit 3 = the
1126// four-screen flag, M = bit 0). UNROM-512 uses the *standard* iNES byte-6
1127// convention (no inversion) — verified against Mesen2 `UnRom512::InitMapper`,
1128// which decodes `Byte6 & 0x09`:
1129//
1130//   * `00` (N=0,M=0) -> Horizontal mirroring (the wiki's "vertical arrangement").
1131//   * `01` (N=0,M=1) -> Vertical mirroring   (the wiki's "horizontal arrangement").
1132//   * `10` (N=1,M=0) -> 1-screen, software-switchable A/B via latch bit 7.
1133//   * `11` (N=1,M=1) -> 4-screen, cartridge VRAM (last 8 KiB of CHR-RAM; latch
1134//     bit 7 is inert for mirroring here, per Mesen2).
1135//
1136// The wiki phrases the M bit in *arrangement* terms ("vertical arrangement" =
1137// horizontal mirroring); this codebase's `Mirroring` enum is in *mirroring*
1138// terms, so M=1 -> `Mirroring::Vertical`. That matches both Mesen2 and the
1139// generic header parser (`header.rs`: `byte6 bit0 -> Vertical`). The raw flags
1140// are still threaded through the constructor so the 1-screen / 4-screen N=1
1141// wirings (which the generic parser collapses) can be reconstructed precisely.
1142// ===========================================================================
1143
1144/// Per-board nametable wiring resolved from the iNES header for mapper 30.
1145#[derive(Clone, Copy, PartialEq, Eq)]
1146enum M30Nametable {
1147    /// Hard-wired horizontal mirroring.
1148    Horizontal,
1149    /// Hard-wired vertical mirroring.
1150    Vertical,
1151    /// Submapper 3: latch bit 7 selects horizontal vs vertical mirroring at
1152    /// runtime (Mesen2 `UnRom512`: `value & 0x80 ? Vertical : Horizontal`).
1153    SwitchableHv,
1154    /// Software-switchable single-screen (latch bit 7 picks A/B).
1155    OneScreen,
1156    /// Four-screen, cartridge VRAM (the `InfiniteNESLives` board): the last
1157    /// 8 KiB of the 32 KiB CHR-RAM is mapped to PPU `$2000-$3EFF`, the four
1158    /// nametables at `$2000-$2FFF` and independent RAM at `$3000-$3EFF`
1159    /// (`UNROM_512.md`, "InfiniteNESLives 4-screen board"). Until v2.9.6 this
1160    /// was approximated as single-screen.
1161    FourScreen,
1162}
1163
1164/// UNROM 512 save-state layout version. v2 (v2.9.6) appends the flash
1165/// chip's command state and, on a flashable board, the flashed sectors.
1166const M30_STATE_VERSION: u8 = 2;
1167
1168/// Mapper 30 (`UNROM-512`).
1169///
1170/// The four booleans mirror distinct iNES-header-derived wirings (CHR-ROM vs
1171/// RAM, the latch nametable bit, bus-conflict presence, and the flash-window
1172/// banking mode), so they don't fold into an enum without losing fidelity.
1173#[allow(clippy::struct_excessive_bools)]
1174pub struct Unrom512M30 {
1175    /// PRG: the SST39SF040's contents on a flashable board.
1176    prg_rom: Box<[u8]>,
1177    /// The PRG as loaded, for the save state's sector diff (flashable only).
1178    original: Box<[u8]>,
1179    chip: Sst39sf040,
1180    /// CHR storage: 32 KiB RAM by default, or CHR-ROM for `.WXN` conversions.
1181    chr: Box<[u8]>,
1182    /// True when `chr` is read-only ROM (no PPU writes land).
1183    chr_is_rom: bool,
1184    vram: Box<[u8]>,
1185    prg_bank: u8,
1186    chr_bank: u8,
1187    /// Latch bit 7 (software nametable select), only meaningful for the
1188    /// 1-screen / 4-screen wirings.
1189    nt_bit: bool,
1190    nametable: M30Nametable,
1191    /// True when the board has bus conflicts (submapper 0 w/o battery, or 2).
1192    bus_conflicts: bool,
1193    /// True when the banking latch responds only to $C000-$FFFF and
1194    /// $8000-$BFFF is the flash window (submapper 0 w/ battery, or 1/3/4).
1195    flash_window: bool,
1196}
1197
1198impl Unrom512M30 {
1199    /// Construct a new mapper 30 board.
1200    ///
1201    /// `four_screen` is iNES byte-6 bit 3, `vertical` is byte-6 bit 0 (the raw
1202    /// flags, before the generic parser's standard-convention mapping). The
1203    /// `submapper` and `has_battery` flags select the bus-conflict / flash
1204    /// wiring per the nesdev-wiki `UNROM 512` submapper table.
1205    ///
1206    /// # Errors
1207    ///
1208    /// Returns [`MapperError::Invalid`] when PRG is not a non-zero multiple of
1209    /// 16 KiB.
1210    pub fn new(
1211        prg_rom: Box<[u8]>,
1212        chr_rom: &[u8],
1213        four_screen: bool,
1214        vertical: bool,
1215        submapper: u8,
1216        has_battery: bool,
1217    ) -> Result<Self, MapperError> {
1218        if prg_rom.is_empty() || !prg_rom.len().is_multiple_of(PRG_BANK_16K) {
1219            return Err(MapperError::Invalid(format!(
1220                "mapper 30 PRG-ROM size {} is not a non-zero multiple of 16 KiB",
1221                prg_rom.len()
1222            )));
1223        }
1224
1225        // Nametable wiring. Submapper 3 = runtime mapper-controlled H/V select
1226        // (latch bit 7); power-on default is Vertical (matching Mesen2).
1227        // Otherwise the byte-6 N/M bits select the four configurations.
1228        let nametable = if submapper == 3 {
1229            M30Nametable::SwitchableHv
1230        } else if four_screen && vertical {
1231            M30Nametable::FourScreen
1232        } else if four_screen {
1233            M30Nametable::OneScreen
1234        } else if vertical {
1235            M30Nametable::Vertical
1236        } else {
1237            M30Nametable::Horizontal
1238        };
1239
1240        // Bus conflicts / flash wiring per submapper + battery bit.
1241        let bus_conflicts = (submapper == 0 && !has_battery) || submapper == 2;
1242        let flash_window = (submapper == 0 && has_battery) || matches!(submapper, 1 | 3 | 4);
1243
1244        // CHR: prefer CHR-ROM when the dump carries it (e.g. the converted
1245        // `.WXN` Waixing carts); otherwise the standard 32 KiB CHR-RAM.
1246        let (chr, chr_is_rom) = if chr_rom.is_empty() {
1247            (vec![0u8; 4 * CHR_BANK_8K].into_boxed_slice(), false)
1248        } else {
1249            (chr_rom.to_vec().into_boxed_slice(), true)
1250        };
1251
1252        // Power-on `nt_bit`: for submapper 3 the board defaults to Vertical
1253        // (Mesen2 `UnRom512::InitMapper`), and `current_mirroring()` maps a set
1254        // bit to Vertical, so seed it `true` to match that default before the
1255        // first latch write. For every other wiring the bit only matters for
1256        // the single-screen case, whose A/B default is `false` (ScreenA).
1257        let nt_bit = nametable == M30Nametable::SwitchableHv;
1258
1259        Ok(Self {
1260            original: if flash_window && !chr_is_rom {
1261                prg_rom.clone()
1262            } else {
1263                Box::new([])
1264            },
1265            prg_rom,
1266            chip: Sst39sf040::new(),
1267            chr,
1268            chr_is_rom,
1269            vram: vec![0u8; 2 * NAMETABLE_SIZE].into_boxed_slice(),
1270            prg_bank: 0,
1271            chr_bank: 0,
1272            nt_bit,
1273            nametable,
1274            bus_conflicts,
1275            flash_window,
1276        })
1277    }
1278
1279    fn read_prg(&self, bank: usize, addr: u16) -> u8 {
1280        let a = self.prg_chip_addr(bank, addr);
1281        self.chip.id_read(a).unwrap_or(self.prg_rom[a])
1282    }
1283
1284    /// Whether writes to `$8000-$BFFF` reach a flash chip. The flashable
1285    /// wiring needs the flash window AND CHR-RAM: UNROM 512 carries CHR-RAM,
1286    /// while the CHR-ROM images headered as mapper 30 are Waixing FS005 `.WXN`
1287    /// conversions (`UNROM_512.md`), whose MMC3-style register writes would
1288    /// otherwise program the "ROM". Measured on *Shui Hu Zhuan*: 7,012 such
1289    /// writes in 1,200 frames rewrote 21,602 bytes of it before this check.
1290    const fn flashable(&self) -> bool {
1291        self.flash_window && !self.chr_is_rom
1292    }
1293
1294    fn prg_chip_addr(&self, bank: usize, addr: u16) -> usize {
1295        let count = (self.prg_rom.len() / PRG_BANK_16K).max(1);
1296        (bank % count) * PRG_BANK_16K + (addr as usize & 0x3FFF)
1297    }
1298
1299    /// The four-screen board's nametable RAM, the last 8 KiB of CHR-RAM,
1300    /// when the board has the full 32 KiB it is defined for.
1301    fn four_screen_offset(&self, addr: u16) -> Option<usize> {
1302        (self.nametable == M30Nametable::FourScreen
1303            && !self.chr_is_rom
1304            && self.chr.len() == 4 * CHR_BANK_8K)
1305            .then(|| 3 * CHR_BANK_8K + (addr as usize & 0x1FFF))
1306    }
1307
1308    fn chr_offset(&self, addr: u16) -> usize {
1309        let count = (self.chr.len() / CHR_BANK_8K).max(1);
1310        let bank = (self.chr_bank as usize) % count;
1311        bank * CHR_BANK_8K + (addr as usize & 0x1FFF)
1312    }
1313
1314    /// Apply a write to the banking latch (already known to target the latch).
1315    fn write_latch(&mut self, addr: u16, value: u8) {
1316        let effective = if self.bus_conflicts {
1317            // Bus conflict: AND with the PRG byte actually driving the bus at the
1318            // write address. The switchable bank serves $8000-$BFFF; the FIXED
1319            // last 16 KiB bank serves $C000-$FFFF, so a write there conflicts
1320            // with the fixed bank, not the currently-selected low bank (matches
1321            // Mesen2's address-based `BaseMapper` conflict resolution).
1322            let conflict_bank = if addr >= 0xC000 {
1323                (self.prg_rom.len() / PRG_BANK_16K).max(1) - 1
1324            } else {
1325                self.prg_bank as usize
1326            };
1327            value & self.read_prg(conflict_bank, addr)
1328        } else {
1329            value
1330        };
1331        self.prg_bank = effective & 0x1F;
1332        self.chr_bank = (effective >> 5) & 0x03;
1333        self.nt_bit = (effective & 0x80) != 0;
1334    }
1335}
1336
1337impl Mapper for Unrom512M30 {
1338    fn caps(&self) -> MapperCaps {
1339        MapperCaps::NONE
1340    }
1341
1342    fn cpu_read(&mut self, addr: u16) -> u8 {
1343        match addr {
1344            0x8000..=0xBFFF => self.read_prg(self.prg_bank as usize, addr),
1345            0xC000..=0xFFFF => {
1346                let last = (self.prg_rom.len() / PRG_BANK_16K).max(1) - 1;
1347                self.read_prg(last, addr)
1348            }
1349            _ => 0,
1350        }
1351    }
1352
1353    /// The flash image on a flashable board, its save; nothing otherwise.
1354    /// There is never RAM at `$6000`, so `sram()` stays empty.
1355    fn save_data(&self) -> &[u8] {
1356        if self.flashable() { &self.prg_rom } else { &[] }
1357    }
1358
1359    fn save_data_mut(&mut self) -> &mut [u8] {
1360        if self.flashable() {
1361            &mut self.prg_rom
1362        } else {
1363            &mut []
1364        }
1365    }
1366
1367    fn clear_save_data(&mut self) {
1368        if self.flashable() {
1369            self.prg_rom.copy_from_slice(&self.original);
1370        }
1371    }
1372
1373    /// Nothing drives `$4020-$7FFF` on any wiring.
1374    fn cpu_read_unmapped(&self, addr: u16) -> bool {
1375        addr < 0x8000
1376    }
1377
1378    fn nametable_unfolded(&self) -> bool {
1379        self.four_screen_offset(0x2000).is_some()
1380    }
1381
1382    fn nametable_fetch(&mut self, addr: u16) -> Option<u8> {
1383        self.four_screen_offset(addr).map(|off| self.chr[off])
1384    }
1385
1386    fn nametable_write(&mut self, addr: u16, value: u8) -> bool {
1387        match self.four_screen_offset(addr) {
1388            Some(off) => {
1389                self.chr[off] = value;
1390                true
1391            }
1392            None => false,
1393        }
1394    }
1395
1396    fn cpu_write(&mut self, addr: u16, value: u8) {
1397        if !(0x8000..=0xFFFF).contains(&addr) {
1398            return;
1399        }
1400        if self.flash_window {
1401            // No-bus-conflict wiring: the banking latch lives at $C000-$FFFF;
1402            // $8000-$BFFF writes reach the SST39SF040 in the selected bank.
1403            if addr >= 0xC000 {
1404                self.write_latch(addr, value);
1405            } else if self.flashable() {
1406                let a = self.prg_chip_addr(self.prg_bank as usize, addr);
1407                self.chip.write(&mut self.prg_rom, a, value);
1408            }
1409        } else {
1410            // Submapper 0 w/o battery or submapper 2: the latch responds to the
1411            // whole $8000-$FFFF range, with bus conflicts.
1412            self.write_latch(addr, value);
1413        }
1414    }
1415
1416    fn ppu_read(&mut self, addr: u16) -> u8 {
1417        let addr = addr & 0x3FFF;
1418        match addr {
1419            0x0000..=0x1FFF => self.chr[self.chr_offset(addr)],
1420            0x2000..=0x3EFF => self.vram[nametable_offset(addr, self.current_mirroring())],
1421            _ => 0,
1422        }
1423    }
1424
1425    fn ppu_write(&mut self, addr: u16, value: u8) {
1426        let addr = addr & 0x3FFF;
1427        match addr {
1428            0x0000..=0x1FFF => {
1429                if !self.chr_is_rom {
1430                    let off = self.chr_offset(addr);
1431                    self.chr[off] = value;
1432                }
1433            }
1434            0x2000..=0x3EFF => {
1435                let off = nametable_offset(addr, self.current_mirroring());
1436                self.vram[off] = value;
1437            }
1438            _ => {}
1439        }
1440    }
1441
1442    fn current_mirroring(&self) -> Mirroring {
1443        match self.nametable {
1444            M30Nametable::Horizontal => Mirroring::Horizontal,
1445            M30Nametable::Vertical => Mirroring::Vertical,
1446            // Submapper 3: latch bit 7 picks vertical (set) vs horizontal
1447            // (clear) at runtime (Mesen2 `value & 0x80 ? Vertical : Horizontal`).
1448            M30Nametable::SwitchableHv => {
1449                if self.nt_bit {
1450                    Mirroring::Vertical
1451                } else {
1452                    Mirroring::Horizontal
1453                }
1454            }
1455            // The four-screen board with its 32 KiB of CHR-RAM owns the
1456            // nametables outright.
1457            M30Nametable::FourScreen if self.four_screen_offset(0x2000).is_some() => {
1458                Mirroring::FourScreen
1459            }
1460            // Software-switchable single-screen: latch bit 7 selects which CIRAM
1461            // half (A10=0 lower, A10=1 upper). A four-screen header on a board
1462            // without 32 KiB of CHR-RAM, which the page leaves undefined, keeps
1463            // this single-screen base.
1464            M30Nametable::OneScreen | M30Nametable::FourScreen => {
1465                if self.nt_bit {
1466                    Mirroring::SingleScreenB
1467                } else {
1468                    Mirroring::SingleScreenA
1469                }
1470            }
1471        }
1472    }
1473
1474    fn save_state(&self) -> Vec<u8> {
1475        let chr_len = if self.chr_is_rom { 0 } else { self.chr.len() };
1476        let mut out = Vec::with_capacity(6 + self.vram.len() + chr_len);
1477        out.push(M30_STATE_VERSION);
1478        out.push(self.prg_bank);
1479        out.push(self.chr_bank);
1480        out.push(u8::from(self.nt_bit));
1481        out.extend_from_slice(&self.vram);
1482        // CHR-ROM is immutable; only persist CHR-RAM contents.
1483        if !self.chr_is_rom {
1484            out.extend_from_slice(&self.chr);
1485        }
1486        out.extend_from_slice(&self.chip.to_bytes());
1487        if self.flashable() {
1488            encode_sector_diff(&self.prg_rom, &self.original, &mut out);
1489        }
1490        out
1491    }
1492
1493    fn load_state(&mut self, data: &[u8]) -> Result<(), MapperError> {
1494        let chr_len = if self.chr_is_rom { 0 } else { self.chr.len() };
1495        let fixed = 6 + self.vram.len() + chr_len;
1496        if data.len() < fixed {
1497            return Err(MapperError::WrongLength {
1498                expected: fixed,
1499                got: data.len(),
1500            });
1501        }
1502        if data[0] != M30_STATE_VERSION {
1503            return Err(MapperError::UnsupportedVersion(data[0]));
1504        }
1505        // Validate everything before assigning anything: a refused state
1506        // leaves the board, and above all its flash, as it was.
1507        let (s0, s1) = (data[fixed - 2], data[fixed - 1]);
1508        let chip = Sst39sf040::from_bytes([s0, s1]).ok_or_else(|| {
1509            MapperError::Invalid(format!(
1510                "mapper 30 flash state {s0:#04x} {s1:#04x} is not one the chip produces"
1511            ))
1512        })?;
1513        let expected = if self.flashable() {
1514            flash_state_len(30, fixed, self.prg_rom.len(), data)?
1515        } else {
1516            fixed
1517        };
1518        if data.len() != expected {
1519            return Err(MapperError::WrongLength {
1520                expected,
1521                got: data.len(),
1522            });
1523        }
1524        if self.flashable() {
1525            // v2.9.9 NL-15: decoded in place, as for mapper 111 above;
1526            // a refused diff leaves the flash as it was.
1527            decode_sector_diff(&mut self.prg_rom, &self.original, &data[fixed..])
1528                .ok_or_else(|| MapperError::Invalid("mapper 30 flash diff".into()))?;
1529        }
1530        self.chip = chip;
1531        // Mask the register indices to their live-invariant widths so a
1532        // corrupted / hand-edited save-state can't seed an out-of-range value
1533        // (mirrors the write-latch masks; same defensive treatment as the
1534        // JY-ASIC `chr_latch` clamp in `m035_jy_asic.rs`). The read paths already
1535        // wrap with `% count`, so this is belt-and-suspenders, not a panic fix.
1536        self.prg_bank = data[1] & 0x1F;
1537        self.chr_bank = data[2] & 0x03;
1538        self.nt_bit = data[3] != 0;
1539        let mut cursor = 4;
1540        self.vram
1541            .copy_from_slice(&data[cursor..cursor + self.vram.len()]);
1542        cursor += self.vram.len();
1543        if !self.chr_is_rom {
1544            self.chr
1545                .copy_from_slice(&data[cursor..cursor + self.chr.len()]);
1546        }
1547        Ok(())
1548    }
1549}
1550
1551#[cfg(test)]
1552#[allow(clippy::cast_possible_truncation)]
1553mod tests {
1554    use super::*;
1555
1556    fn synth_prg_32k(banks: usize) -> Box<[u8]> {
1557        let mut v = vec![0xFFu8; banks * PRG_BANK_32K];
1558        for b in 0..banks {
1559            v[b * PRG_BANK_32K] = b as u8;
1560        }
1561        v.into_boxed_slice()
1562    }
1563
1564    fn synth_prg_16k(banks: usize) -> Box<[u8]> {
1565        let mut v = vec![0xFFu8; banks * PRG_BANK_16K];
1566        for b in 0..banks {
1567            v[b * PRG_BANK_16K] = b as u8;
1568        }
1569        v.into_boxed_slice()
1570    }
1571
1572    fn synth_prg_4k(banks: usize) -> Box<[u8]> {
1573        let mut v = vec![0xFFu8; banks * PRG_BANK_4K];
1574        for b in 0..banks {
1575            v[b * PRG_BANK_4K] = b as u8;
1576        }
1577        v.into_boxed_slice()
1578    }
1579
1580    #[test]
1581    fn m31_slots_latch_per_window() {
1582        let mut m = Inl31::new(synth_prg_4k(8), &[], Mirroring::Vertical).unwrap();
1583        // Slot 0 ($8000) <- bank 3; slot 7 ($F000) <- bank 5.
1584        m.cpu_write(0x5FF8, 3);
1585        m.cpu_write(0x5FFF, 5);
1586        assert_eq!(m.cpu_read(0x8000), 3);
1587        assert_eq!(m.cpu_read(0xF000), 5);
1588        // Untouched slot 1 ($9000) stays at power-on 0.
1589        assert_eq!(m.cpu_read(0x9000), 0);
1590    }
1591
1592    #[test]
1593    fn m31_save_state_round_trip() {
1594        let mut m = Inl31::new(synth_prg_4k(8), &[], Mirroring::Vertical).unwrap();
1595        m.cpu_write(0x5FF8, 2);
1596        m.ppu_write(0x0001, 0xCD);
1597        let blob = m.save_state();
1598        let mut m2 = Inl31::new(synth_prg_4k(8), &[], Mirroring::Vertical).unwrap();
1599        m2.load_state(&blob).unwrap();
1600        assert_eq!(m2.cpu_read(0x8000), 2);
1601        assert_eq!(m2.ppu_read(0x0001), 0xCD);
1602    }
1603
1604    #[test]
1605    fn m218_ciram_serves_chr_and_nametable() {
1606        let mut m = MagicFloor218::new(synth_prg_32k(1), &[], Mirroring::Vertical).unwrap();
1607        // Vertical: pattern block 0 -> physical bank 0; block 1 -> bank 1.
1608        // Write CHR at $0000 (block 0) and a nametable at $2400 (table 1).
1609        m.ppu_write(0x0000, 0x11);
1610        m.ppu_write(0x2400, 0x22);
1611        // $2400 = table 1 -> physical bank 1; $0400 = pattern block 1 -> bank 1.
1612        assert_eq!(m.ppu_read(0x0400), 0x22);
1613        // $2000 = table 0 -> bank 0 = the CHR byte written at $0000.
1614        assert_eq!(m.ppu_read(0x2000), 0x11);
1615        assert_eq!(m.current_mirroring(), Mirroring::Vertical);
1616    }
1617
1618    #[test]
1619    fn m218_single_screen_a_wires_ciram_a10_to_ppu_a12() {
1620        // NESdev "INES Mapper 218", flags 6 = $A8: CIRAM A10 = PPU A12.
1621        // Pattern table 0 ($0000-$0FFF) is CIRAM bank 0, pattern table 1
1622        // ($1000-$1FFF) is bank 1, and every nametable ($2xxx, A12 = 0) is
1623        // bank 0 -- so "swappable via PPUCTRL" pattern tables, one screen.
1624        let mut m = MagicFloor218::new(synth_prg_32k(1), &[], Mirroring::SingleScreenA).unwrap();
1625        m.ppu_write(0x0000, 0x11);
1626        m.ppu_write(0x1000, 0x22);
1627        // A10 and A11 do not reach CIRAM A10: $0400 / $0C00 alias $0000.
1628        assert_eq!(m.ppu_read(0x0400), 0x11);
1629        assert_eq!(m.ppu_read(0x0C00), 0x11);
1630        assert_eq!(m.ppu_read(0x1400), 0x22);
1631        // Nametables sit in bank 0 alongside pattern table 0.
1632        assert_eq!(m.ppu_read(0x2000), 0x11);
1633        assert_eq!(m.ppu_read(0x2C00), 0x11);
1634        assert_eq!(m.nametable_fetch(0x2400), Some(0x11));
1635    }
1636
1637    #[test]
1638    fn m218_single_screen_b_wires_ciram_a10_to_ppu_a13() {
1639        // NESdev "INES Mapper 218", flags 6 = $A9: CIRAM A10 = PPU A13.
1640        // Every pattern address ($0000-$1FFF, A13 = 0) is CIRAM bank 0 and
1641        // every nametable ($2xxx, A13 = 1) is bank 1: 1 KiB (64 tiles) of
1642        // CHR-RAM that the nametable never overwrites.
1643        let mut m = MagicFloor218::new(synth_prg_32k(1), &[], Mirroring::SingleScreenB).unwrap();
1644        m.ppu_write(0x0000, 0x11);
1645        m.ppu_write(0x2000, 0x33);
1646        assert_eq!(m.ppu_read(0x0000), 0x11);
1647        assert_eq!(m.ppu_read(0x1000), 0x11);
1648        assert_eq!(m.ppu_read(0x1C00), 0x11);
1649        assert_eq!(m.ppu_read(0x2C00), 0x33);
1650        assert_eq!(m.nametable_fetch(0x2400), Some(0x33));
1651    }
1652
1653    #[test]
1654    fn m218_accepts_16k_prg_and_mirrors_it() {
1655        // Real Magic Floor dumps are 16 KiB (NROM-128-style). The board must
1656        // accept them and mirror PRG across the full 32 KiB CPU window.
1657        let mut prg = synth_prg_16k(1);
1658        prg[0] = 0xAB; // marker at the start of the 16 KiB image
1659        let mut m = MagicFloor218::new(prg, &[], Mirroring::Horizontal).unwrap();
1660        // $8000 and the mirror at $C000 both read the same byte.
1661        assert_eq!(m.cpu_read(0x8000), 0xAB);
1662        assert_eq!(m.cpu_read(0xC000), 0xAB);
1663    }
1664
1665    #[test]
1666    fn m218_save_state_round_trip() {
1667        let mut m = MagicFloor218::new(synth_prg_32k(1), &[], Mirroring::Horizontal).unwrap();
1668        m.ppu_write(0x0005, 0x42);
1669        let blob = m.save_state();
1670        let mut m2 = MagicFloor218::new(synth_prg_32k(1), &[], Mirroring::Horizontal).unwrap();
1671        m2.load_state(&blob).unwrap();
1672        assert_eq!(m2.ppu_read(0x0005), 0x42);
1673    }
1674
1675    #[test]
1676    fn m29_latch_selects_prg_and_chr_bank() {
1677        let mut m = Cufrom29::new(synth_prg_16k(8), &[], Mirroring::Vertical).unwrap();
1678        // value: CHR = data&3, PRG = (data>>2)&7. 0b0001_0110 = 0x16:
1679        //   CHR = 0b10 = 2, PRG = 0b101 = 5.
1680        m.cpu_write(0x8000, 0b0001_0110);
1681        assert_eq!(m.cpu_read(0x8000), 5);
1682        // $C000 is fixed to the last 16 KiB bank (7).
1683        assert_eq!(m.cpu_read(0xC000), 7);
1684        // CHR-RAM round-trip in the selected (bank 2) window.
1685        m.ppu_write(0x0003, 0x77);
1686        assert_eq!(m.ppu_read(0x0003), 0x77);
1687    }
1688
1689    #[test]
1690    fn m29_save_state_round_trip() {
1691        let mut m = Cufrom29::new(synth_prg_16k(8), &[], Mirroring::Vertical).unwrap();
1692        m.cpu_write(0x8000, 0b0000_1101); // CHR 1, PRG 3
1693        m.ppu_write(0x0007, 0x55);
1694        let blob = m.save_state();
1695        let mut m2 = Cufrom29::new(synth_prg_16k(8), &[], Mirroring::Vertical).unwrap();
1696        m2.load_state(&blob).unwrap();
1697        assert_eq!(m2.cpu_read(0x8000), 3);
1698        assert_eq!(m2.ppu_read(0x0007), 0x55);
1699    }
1700
1701    #[test]
1702    fn m111_register_selects_prg_chr_nt() {
1703        let mut m = Gtrom111::new(synth_prg_32k(8), &[]).unwrap();
1704        // value 0b0011_0101 (0x35): PRG = 5; CHR = (v>>4)&1 = 1; NT = (v>>5)&1 = 1.
1705        m.cpu_write(0x5000, 0b0011_0101);
1706        assert_eq!(m.cpu_read(0x8000), 5);
1707        // CHR bank 1 round-trip.
1708        m.ppu_write(0x0000, 0x88);
1709        assert_eq!(m.ppu_read(0x0000), 0x88);
1710        // Nametable bank 1 round-trip via the fetch hook.
1711        assert!(m.nametable_write(0x2000, 0x99));
1712        assert_eq!(m.nametable_fetch(0x2000), Some(0x99));
1713        assert_eq!(m.current_mirroring(), Mirroring::FourScreen);
1714        // Switching nt bank to 0 hides the byte written under bank 1.
1715        m.cpu_write(0x5000, 0x00);
1716        assert_eq!(m.nametable_fetch(0x2000), Some(0x00));
1717    }
1718
1719    #[test]
1720    fn m111_save_state_round_trip() {
1721        let mut m = Gtrom111::new(synth_prg_32k(8), &[]).unwrap();
1722        m.cpu_write(0x5000, 0b0011_0011); // PRG 3, CHR 1, NT 1
1723        m.ppu_write(0x0001, 0xAA);
1724        m.nametable_write(0x2001, 0xBB);
1725        let blob = m.save_state();
1726        let mut m2 = Gtrom111::new(synth_prg_32k(8), &[]).unwrap();
1727        m2.load_state(&blob).unwrap();
1728        assert_eq!(m2.cpu_read(0x8000), 3);
1729        assert_eq!(m2.ppu_read(0x0001), 0xAA);
1730        assert_eq!(m2.nametable_fetch(0x2001), Some(0xBB));
1731    }
1732
1733    /// v2.9.6: the latch decodes `/ROMSEL`, A14 and A12 high: `$5000` and
1734    /// `$7000` pages only (`GTROM.md`, "Hardware Teardown").
1735    #[test]
1736    fn m111_register_window_is_5000_and_7000_only() {
1737        let mut m = Gtrom111::new(synth_prg_32k(8), &[]).unwrap();
1738        m.cpu_write(0x6000, 0x03);
1739        assert_eq!(m.cpu_read(0x8000), 0, "$6000 is not decoded");
1740        m.cpu_write(0x4FFF, 0x03);
1741        assert_eq!(m.cpu_read(0x8000), 0, "$4FFF is not decoded");
1742        m.cpu_write(0x7ABC, 0x03);
1743        assert_eq!(m.cpu_read(0x8000), 3);
1744        m.cpu_write(0x5FFF, 0x04);
1745        assert_eq!(m.cpu_read(0x8000), 4);
1746        assert!(m.cpu_read_unmapped(0x5000), "the register is write-only");
1747    }
1748
1749    /// "reading from the register effectively writes the value of open bus".
1750    #[test]
1751    fn m111_a_read_latches_the_floating_value() {
1752        let mut m = Gtrom111::new(synth_prg_32k(8), &[]).unwrap();
1753        m.notify_floating_read(0x5000, 0x26);
1754        assert_eq!(m.cpu_read(0x8000), 6);
1755        m.notify_floating_read(0x6000, 0x01);
1756        assert_eq!(m.cpu_read(0x8000), 6, "outside the window: no latch");
1757    }
1758
1759    /// PPU `$3000-$3EFF` is RAM of its own, per nametable page.
1760    #[test]
1761    fn m111_bonus_ram_at_3000_is_not_a_mirror() {
1762        let mut m = Gtrom111::new(synth_prg_32k(8), &[]).unwrap();
1763        assert!(m.nametable_unfolded());
1764        m.nametable_write(0x2123, 0x11);
1765        m.nametable_write(0x3123, 0x22);
1766        assert_eq!(m.nametable_fetch(0x2123), Some(0x11));
1767        assert_eq!(m.nametable_fetch(0x3123), Some(0x22));
1768        m.cpu_write(0x5000, 0x20); // the other nametable page
1769        assert_eq!(m.nametable_fetch(0x3123), Some(0x00));
1770        m.nametable_write(0x3EFF, 0x33);
1771        m.cpu_write(0x5000, 0x00);
1772        assert_eq!(m.nametable_fetch(0x3123), Some(0x22));
1773        assert_eq!(m.nametable_fetch(0x3EFF), Some(0x00));
1774    }
1775
1776    /// Self-flashing: `5555h` is `$D555` and `2AAAh` is `$AAAA` in any bank.
1777    #[test]
1778    fn m111_flash_program_and_erase_through_the_cpu_window() {
1779        let mut m = Gtrom111::new(synth_prg_32k(16), &[]).unwrap();
1780        m.cpu_write(0x5000, 0x07);
1781        for (a, v) in [
1782            (0xD555u16, 0xAAu8),
1783            (0xAAAA, 0x55),
1784            (0xD555, 0xA0),
1785            (0x9000, 0x3C),
1786        ] {
1787            m.cpu_write(a, v);
1788        }
1789        assert_eq!(m.cpu_read(0x9000), 0x3C, "programmed");
1790        assert_eq!(
1791            m.save_data()[7 * PRG_BANK_32K + 0x1000],
1792            0x3C,
1793            "save_data() is the flash"
1794        );
1795        assert!(m.sram().is_empty(), "GTROM has no RAM at $6000");
1796        m.cpu_write(0x5000, 0x02);
1797        assert_eq!(m.cpu_read(0x9000), 0xFF, "another bank is untouched");
1798        m.cpu_write(0x5000, 0x07);
1799        for (a, v) in [
1800            (0xD555u16, 0xAAu8),
1801            (0xAAAA, 0x55),
1802            (0xD555, 0x80),
1803            (0xD555, 0xAA),
1804            (0xAAAA, 0x55),
1805            (0x9800, 0x30),
1806        ] {
1807            m.cpu_write(a, v);
1808        }
1809        assert_eq!(m.cpu_read(0x9000), 0xFF, "the 4 KiB sector is erased");
1810        assert_eq!(m.cpu_read(0x8000), 0x07, "the neighbouring sector is not");
1811    }
1812
1813    #[test]
1814    fn m111_state_carries_only_flashed_sectors() {
1815        let mut m = Gtrom111::new(synth_prg_32k(16), &[]).unwrap();
1816        let clean = m.save_state().len();
1817        for (a, v) in [
1818            (0xD555u16, 0xAAu8),
1819            (0xAAAA, 0x55),
1820            (0xD555, 0xA0),
1821            (0xC001, 0x00),
1822        ] {
1823            m.cpu_write(a, v);
1824        }
1825        m.nametable_write(0x3456, 0x78);
1826        let blob = m.save_state();
1827        assert_eq!(blob.len(), clean + 0x1000, "one flashed sector");
1828        let mut m2 = Gtrom111::new(synth_prg_32k(16), &[]).unwrap();
1829        m2.load_state(&blob).unwrap();
1830        assert_eq!(m2.cpu_read(0xC001), 0x00);
1831        assert_eq!(m2.nametable_fetch(0x3456), Some(0x78));
1832        assert_eq!(m2.save_state(), blob);
1833        // A clean state restores the loaded ROM over a flashed one.
1834        let fresh = Gtrom111::new(synth_prg_32k(16), &[]).unwrap().save_state();
1835        m2.load_state(&fresh).unwrap();
1836        assert_eq!(m2.cpu_read(0xC001), 0xFF);
1837    }
1838
1839    /// v2.9.0 re-audit NC-02: a restored bank the board cannot hold is
1840    /// rejected, not stored. `load_state` assigned `prg_bank`, `chr_bank` and
1841    /// `nt_bank` raw from the blob and the fetch paths use them unmasked, so a
1842    /// corrupt `.rns` (or RetroArch `.state`) for a GTROM game loaded cleanly
1843    /// and panicked on the next CPU fetch from `$8000` — too late for the
1844    /// restore's rollback to help. `update_register` can only produce a PRG
1845    /// bank below the 32 KiB bank count and 0/1 for the other two.
1846    #[test]
1847    fn m111_load_state_rejects_banks_the_board_cannot_hold() {
1848        let mut m = Gtrom111::new(synth_prg_32k(8), &[]).unwrap();
1849        m.cpu_write(0x5000, 0b0011_0011); // PRG 3, CHR 1, NT 1
1850        let good = m.save_state();
1851        for (byte, value) in [(1, 8), (1, 0xFF), (2, 2), (2, 0xFF), (3, 2), (3, 0xFF)] {
1852            let mut bad = good.clone();
1853            bad[byte] = value;
1854            let mut m2 = Gtrom111::new(synth_prg_32k(8), &[]).unwrap();
1855            assert!(
1856                matches!(m2.load_state(&bad), Err(MapperError::Invalid(_))),
1857                "byte {byte} = {value:#04x} must be rejected"
1858            );
1859        }
1860        // Every value the register can produce still loads.
1861        for (byte, max) in [(1, 7), (2, 1), (3, 1)] {
1862            for value in 0..=max {
1863                let mut ok = good.clone();
1864                ok[byte] = value;
1865                let mut m2 = Gtrom111::new(synth_prg_32k(8), &[]).unwrap();
1866                m2.load_state(&ok).unwrap();
1867                let _ = m2.cpu_read(0xFFFF);
1868                let _ = m2.ppu_read(0x1FFF);
1869                let _ = m2.nametable_fetch(0x2FFF);
1870            }
1871        }
1872    }
1873
1874    /// The NESdev "Action 53 mapper" table (A22-A14 output per mode value and
1875    /// outer bank size), transcribed row for row as data. `o` = outer-bank bit
1876    /// taken from the TOP of `$81`, `i` = inner-bank bit taken from the
1877    /// BOTTOM of `$01`, `0`/`1` = a literal (CPU A14 in the 32 KiB modes).
1878    const M28_WIKI_TABLE: [(u8, &str, &str); 12] = [
1879        (0x00, "oooooooo0", "oooooooo1"),
1880        (0x08, "oooooooo0", "ooooooooi"),
1881        (0x0C, "ooooooooi", "oooooooo1"),
1882        (0x10, "oooooooi0", "oooooooi1"),
1883        (0x18, "oooooooo0", "oooooooii"),
1884        (0x1C, "oooooooii", "oooooooo1"),
1885        (0x20, "ooooooii0", "ooooooii1"),
1886        (0x28, "oooooooo0", "ooooooiii"),
1887        (0x2C, "ooooooiii", "oooooooo1"),
1888        (0x30, "oooooiii0", "oooooiii1"),
1889        (0x38, "oooooooo0", "oooooiiii"),
1890        (0x3C, "oooooiiii", "oooooooo1"),
1891    ];
1892
1893    /// Expand one table pattern into a 9-bit bank number.
1894    fn m28_expected(pattern: &str, outer: u8, inner: u8) -> usize {
1895        let os = pattern.bytes().filter(|&c| c == b'o').count();
1896        let is = pattern.bytes().filter(|&c| c == b'i').count();
1897        // "o"s are the topmost outer bits, "i"s the bottommost inner bits.
1898        let mut o_bits = (0..os).map(|k| (outer >> (7 - k)) & 1);
1899        let mut i_bits = (0..is).rev().map(|k| (inner >> k) & 1);
1900        pattern.bytes().fold(0usize, |acc, c| {
1901            let bit = match c {
1902                b'o' => o_bits.next().unwrap(),
1903                b'i' => i_bits.next().unwrap(),
1904                b'0' => 0,
1905                _ => 1,
1906            };
1907            (acc << 1) | usize::from(bit)
1908        })
1909    }
1910
1911    #[test]
1912    fn m28_prg_banking_matches_every_row_of_the_wiki_table() {
1913        // A 512-bank (8 MiB) image, so the modulo in `prg_bank_for` never
1914        // wraps and all nine output bits (A22-A14) are compared.
1915        let mut m = Action53M28::new(synth_prg_16k(512), &[], Mirroring::Vertical).unwrap();
1916        for (mode_base, lo, hi) in M28_WIKI_TABLE {
1917            // Each row covers a range of mode values; the table's row value
1918            // plus every mirroring setting (and, for the 32 KiB rows, both
1919            // PRG-mode encodings 0 and 1) must resolve identically.
1920            let variants: &[u8] = if mode_base & 0x0C == 0 {
1921                &[0x0, 0x1, 0x2, 0x3, 0x4, 0x5, 0x6, 0x7]
1922            } else {
1923                &[0x0, 0x1, 0x2, 0x3]
1924            };
1925            for &v in variants {
1926                m.mode = mode_base | v;
1927                for outer in 0..=255u8 {
1928                    for inner in 0..16u8 {
1929                        m.outer_prg = outer;
1930                        m.inner_prg = inner;
1931                        assert_eq!(
1932                            m.prg_bank_for(0x8000),
1933                            m28_expected(lo, outer, inner),
1934                            "mode ${:02X} outer ${outer:02X} inner {inner} at $8000",
1935                            m.mode
1936                        );
1937                        assert_eq!(
1938                            m.prg_bank_for(0xC000),
1939                            m28_expected(hi, outer, inner),
1940                            "mode ${:02X} outer ${outer:02X} inner {inner} at $C000",
1941                            m.mode
1942                        );
1943                    }
1944                }
1945            }
1946        }
1947    }
1948
1949    #[test]
1950    fn m28_powers_on_with_the_last_bank_at_c000() {
1951        // test28's first check ("DOES NOT POWER ON WITH LAST BANK IN
1952        // $C000-$FFFF"), which the pre-v2.9.3 board failed.
1953        let mut m = Action53M28::new(synth_prg_16k(32), &[], Mirroring::Vertical).unwrap();
1954        assert_eq!(m.cpu_read(0xC000), 31);
1955    }
1956
1957    #[test]
1958    fn m28_chr_register_banks_32k_of_chr_ram() {
1959        let mut m = Action53M28::new(synth_prg_16k(8), &[], Mirroring::Vertical).unwrap();
1960        for bank in 0..4u8 {
1961            m.cpu_write(0x5000, 0x00);
1962            m.cpu_write(0x8000, bank);
1963            m.ppu_write(0x0123, 0xA0 | bank);
1964        }
1965        for bank in 0..4u8 {
1966            m.cpu_write(0x5000, 0x00);
1967            m.cpu_write(0x8000, bank);
1968            assert_eq!(m.ppu_read(0x0123), 0xA0 | bank, "CHR bank {bank}");
1969        }
1970    }
1971
1972    #[test]
1973    fn m28_d4_selects_the_single_screen_only_in_one_screen_modes() {
1974        let mut m = Action53M28::new(synth_prg_16k(8), &[], Mirroring::Vertical).unwrap();
1975        m.cpu_write(0x5000, 0x80);
1976        m.cpu_write(0x8000, 0x00); // 1-screen lower
1977        m.cpu_write(0x5000, 0x01);
1978        m.cpu_write(0x8000, 0x10); // inner write with D4 set
1979        assert_eq!(m.current_mirroring(), Mirroring::SingleScreenB);
1980        m.cpu_write(0x5000, 0x00);
1981        m.cpu_write(0x8000, 0x00); // CHR write with D4 clear
1982        assert_eq!(m.current_mirroring(), Mirroring::SingleScreenA);
1983        // Vertical: D4 is ignored.
1984        m.cpu_write(0x5000, 0x80);
1985        m.cpu_write(0x8000, 0x02);
1986        m.cpu_write(0x5000, 0x01);
1987        m.cpu_write(0x8000, 0x10);
1988        assert_eq!(m.current_mirroring(), Mirroring::Vertical);
1989    }
1990
1991    #[test]
1992    fn m28_loads_a_version_1_state_with_8k_of_chr() {
1993        let mut m = Action53M28::new(synth_prg_16k(8), &[], Mirroring::Vertical).unwrap();
1994        let mut v1 = vec![1u8, 0x81, 0x00, 0x03, 0x0E, 0x02];
1995        v1.extend(core::iter::repeat_n(0u8, 2 * NAMETABLE_SIZE));
1996        let mut chr = vec![0u8; CHR_BANK_8K];
1997        chr[7] = 0x5A;
1998        v1.extend_from_slice(&chr);
1999        m.load_state(&v1).unwrap();
2000        assert_eq!(m.ppu_read(0x0007), 0x5A);
2001        assert_eq!(m.mode, 0x0E);
2002        assert_eq!(m.outer_prg, 0x02);
2003    }
2004
2005    #[test]
2006    fn m28_save_state_round_trip() {
2007        let mut m = Action53M28::new(synth_prg_16k(8), &[], Mirroring::Vertical).unwrap();
2008        // Set NROM-128 mode (mode bits 2-3 = 3, mirroring bits 0-1 = 2).
2009        m.cpu_write(0x5000, 0x80);
2010        m.cpu_write(0x8000, 0x0E);
2011        // Set outer = 1.
2012        m.cpu_write(0x5000, 0x81);
2013        m.cpu_write(0x8000, 0x01);
2014        m.ppu_write(0x0007, 0x5A);
2015        let resolved = m.cpu_read(0x8000);
2016        let blob = m.save_state();
2017        let mut m2 = Action53M28::new(synth_prg_16k(8), &[], Mirroring::Vertical).unwrap();
2018        m2.load_state(&blob).unwrap();
2019        assert_eq!(m2.ppu_read(0x0007), 0x5A);
2020        assert_eq!(m2.cpu_read(0x8000), resolved);
2021        assert_eq!(m2.current_mirroring(), Mirroring::Vertical);
2022    }
2023
2024    #[test]
2025    fn m30_latch_selects_prg_chr_and_fixed_high() {
2026        // Submapper 0 without battery -> bus conflicts on $8000-$FFFF.
2027        let mut m = Unrom512M30::new(synth_prg_16k(8), &[], false, true, 0, false).unwrap();
2028        // PRG bits 0-4 = 3, CHR bits 5-6 = 1. value = 0b0010_0011 = 0x23.
2029        // Offset 1 (no marker, 0xFF) -> bus conflict harmless.
2030        m.cpu_write(0x8001, 0x23);
2031        assert_eq!(m.cpu_read(0x8000), 3);
2032        // $C000 fixed to last (7).
2033        assert_eq!(m.cpu_read(0xC000), 7);
2034        // CHR bank 1.
2035        m.ppu_write(0x0000, 0xEE);
2036        assert_eq!(m.ppu_read(0x0000), 0xEE);
2037    }
2038
2039    #[test]
2040    fn m30_battery_cart_no_bus_conflict_high_window_only() {
2041        // Submapper 0 WITH battery (e.g. Wampus / PROTO DERE): no bus conflicts;
2042        // the banking latch responds only to $C000-$FFFF, and $8000-$BFFF is
2043        // the (un-modelled) flash window that must NOT bank-switch.
2044        let mut m = Unrom512M30::new(synth_prg_16k(8), &[], false, true, 0, true).unwrap();
2045        // A write to the flash window leaves the bank untouched (still 0).
2046        m.cpu_write(0x8000, 0x05);
2047        assert_eq!(m.cpu_read(0x8000), 0);
2048        // A write to $C000-$FFFF switches the bank with NO bus-conflict AND.
2049        // Bank 5 even though the PRG byte read there (the bank index) differs.
2050        m.cpu_write(0xC000, 0x05);
2051        assert_eq!(m.cpu_read(0x8000), 5);
2052    }
2053
2054    /// v2.9.6: the flashable wiring programs the SST39SF040 with the
2055    /// wiki's own sequence (`UNROM_512.md`, "Write a byte").
2056    #[test]
2057    fn m30_flashable_board_programs_and_erases_the_chip() {
2058        let mut m = Unrom512M30::new(synth_prg_16k(16), &[], false, true, 1, false).unwrap();
2059        assert_eq!(
2060            m.save_data().len(),
2061            16 * PRG_BANK_16K,
2062            "the flash is the save"
2063        );
2064        assert!(m.sram().is_empty(), "no RAM at $6000");
2065        for (bank, a, v) in [
2066            (1u8, 0x9555u16, 0xAAu8),
2067            (0, 0xAAAA, 0x55),
2068            (1, 0x9555, 0xA0),
2069            (5, 0x8123, 0x42),
2070        ] {
2071            m.cpu_write(0xC000, bank);
2072            m.cpu_write(a, v);
2073        }
2074        m.cpu_write(0xC000, 5);
2075        assert_eq!(m.cpu_read(0x8123), 0x42);
2076        let blob = m.save_state();
2077        let mut m2 = Unrom512M30::new(synth_prg_16k(16), &[], false, true, 1, false).unwrap();
2078        m2.load_state(&blob).unwrap();
2079        assert_eq!(
2080            m2.cpu_read(0x8123),
2081            0x42,
2082            "the flashed sector is in the state"
2083        );
2084        // Erase the sector again ("Erase 4KB Flash Sector").
2085        for (bank, a, v) in [
2086            (1u8, 0x9555u16, 0xAAu8),
2087            (0, 0xAAAA, 0x55),
2088            (1, 0x9555, 0x80),
2089            (1, 0x9555, 0xAA),
2090            (0, 0xAAAA, 0x55),
2091            (5, 0x8000, 0x30),
2092        ] {
2093            m.cpu_write(0xC000, bank);
2094            m.cpu_write(a, v);
2095        }
2096        m.cpu_write(0xC000, 5);
2097        assert_eq!(m.cpu_read(0x8123), 0xFF);
2098    }
2099
2100    /// The reset a power-on movie performs (`power_on_for_movie` ->
2101    /// `clear_save_data`): a flashed UNROM 512 goes back to the image as
2102    /// loaded, byte for byte. Neither zeros (a ROM with no program in it) nor
2103    /// the flashed image (a movie that replays differently with a save) is
2104    /// right. GTROM's half is pinned end to end in `roster_boards.rs`.
2105    #[test]
2106    fn m30_clear_save_data_restores_the_image_as_loaded() {
2107        let fresh = Unrom512M30::new(synth_prg_16k(16), &[], false, true, 1, false).unwrap();
2108        let mut m = Unrom512M30::new(synth_prg_16k(16), &[], false, true, 1, false).unwrap();
2109        for (bank, a, v) in [
2110            (1u8, 0x9555u16, 0xAAu8),
2111            (0, 0xAAAA, 0x55),
2112            (1, 0x9555, 0xA0),
2113            (5, 0x8123, 0x00),
2114        ] {
2115            m.cpu_write(0xC000, bank);
2116            m.cpu_write(a, v);
2117        }
2118        assert_ne!(
2119            m.save_data(),
2120            fresh.save_data(),
2121            "the program changed the flash"
2122        );
2123        m.clear_save_data();
2124        assert_eq!(
2125            m.save_data(),
2126            fresh.save_data(),
2127            "back to the image as loaded"
2128        );
2129    }
2130
2131    /// The flash chip's two state bytes take only the values `to_bytes` writes
2132    /// (`docs/mappers.md` gotcha 12): steps 0-6 and a 0/1 ID-mode flag. Both
2133    /// boards refuse anything else before assigning a field.
2134    #[test]
2135    fn flash_state_bytes_the_chip_cannot_produce_are_refused() {
2136        let gt = Gtrom111::new(synth_prg_32k(8), &[]).unwrap();
2137        let good = gt.save_state();
2138        for (i, v) in [(5usize, 7u8), (5, 0xFF), (6, 2)] {
2139            let mut blob = good.clone();
2140            blob[i] = v;
2141            let mut m = Gtrom111::new(synth_prg_32k(8), &[]).unwrap();
2142            assert!(
2143                matches!(m.load_state(&blob), Err(MapperError::Invalid(_))),
2144                "GTROM byte {i} = {v:#x}"
2145            );
2146            assert_eq!(m.save_state(), good, "GTROM: nothing assigned");
2147        }
2148        let m30 = Unrom512M30::new(synth_prg_16k(16), &[], false, true, 1, false).unwrap();
2149        let good = m30.save_state();
2150        let fixed = 6 + m30.vram.len() + m30.chr.len();
2151        for (i, v) in [(fixed - 2, 7u8), (fixed - 1, 2)] {
2152            let mut blob = good.clone();
2153            blob[i] = v;
2154            let mut m = Unrom512M30::new(synth_prg_16k(16), &[], false, true, 1, false).unwrap();
2155            assert!(
2156                matches!(m.load_state(&blob), Err(MapperError::Invalid(_))),
2157                "UNROM 512 byte {i} = {v:#x}"
2158            );
2159            assert_eq!(m.save_state(), good, "UNROM 512: nothing assigned");
2160        }
2161    }
2162
2163    /// A cut-short flash section reports the length the state really needs.
2164    /// It used to report `fixed + 1`, whatever the bitmap said.
2165    #[test]
2166    fn m111_truncated_state_reports_its_exact_length() {
2167        let mut m = Gtrom111::new(synth_prg_32k(8), &[]).unwrap();
2168        for (a, v) in [
2169            (0xD555u16, 0xAAu8),
2170            (0xAAAA, 0x55),
2171            (0xD555, 0xA0),
2172            (0x8100, 0x42),
2173        ] {
2174            m.cpu_write(a, v);
2175        }
2176        let blob = m.save_state();
2177        let err = Gtrom111::new(synth_prg_32k(8), &[])
2178            .unwrap()
2179            .load_state(&blob[..blob.len() - 100])
2180            .unwrap_err();
2181        assert!(
2182            matches!(err, MapperError::WrongLength { expected, .. } if expected == blob.len()),
2183            "{err:?}"
2184        );
2185    }
2186
2187    /// A state refused for trailing bytes must leave the flash as it was. The
2188    /// decoded image used to be copied in before the length check.
2189    #[test]
2190    fn m30_refused_state_leaves_the_flash_untouched() {
2191        let mut a = Unrom512M30::new(synth_prg_16k(16), &[], false, true, 1, false).unwrap();
2192        for (bank, addr, v) in [
2193            (1u8, 0x9555u16, 0xAAu8),
2194            (0, 0xAAAA, 0x55),
2195            (1, 0x9555, 0xA0),
2196            (5, 0x8123, 0x00),
2197        ] {
2198            a.cpu_write(0xC000, bank);
2199            a.cpu_write(addr, v);
2200        }
2201        let mut blob = a.save_state();
2202        blob.push(0);
2203        let mut b = Unrom512M30::new(synth_prg_16k(16), &[], false, true, 1, false).unwrap();
2204        let before = b.save_data().to_vec();
2205        assert!(b.load_state(&blob).is_err());
2206        assert_eq!(b.save_data(), &before[..], "the flash is unchanged");
2207    }
2208
2209    /// A CHR-ROM image headered as mapper 30 is a Waixing FS005 `.WXN`
2210    /// conversion, not UNROM 512 (`UNROM_512.md`). Its register writes must not
2211    /// program a "flash": that rewrote 21,602 bytes of *Shui Hu Zhuan*.
2212    #[test]
2213    fn m30_chr_rom_image_is_never_flashed() {
2214        let chr = vec![0u8; 0x8000];
2215        let mut m = Unrom512M30::new(synth_prg_16k(16), &chr, false, false, 0, true).unwrap();
2216        assert!(m.save_data().is_empty(), "no flash save on a CHR-ROM image");
2217        for (a, v) in [
2218            (0x9555u16, 0xAAu8),
2219            (0xAAAA, 0x55),
2220            (0x9555, 0xA0),
2221            (0x8123, 0x00),
2222        ] {
2223            m.cpu_write(0xC000, 1);
2224            m.cpu_write(a, v);
2225        }
2226        m.cpu_write(0xC000, 0);
2227        assert_eq!(m.cpu_read(0x8123), 0xFF, "the ROM is untouched");
2228    }
2229
2230    #[test]
2231    fn m30_non_flashable_board_has_no_flash() {
2232        let mut m = Unrom512M30::new(synth_prg_16k(16), &[], false, true, 0, false).unwrap();
2233        assert_eq!(m.save_data(), []);
2234        m.cpu_write(0x9555, 0xAA);
2235        assert!(m.cpu_read_unmapped(0x6000));
2236    }
2237
2238    /// The four-screen board: the last 8 KiB of the 32 KiB CHR-RAM is PPU
2239    /// `$2000-$3EFF`, `$3000-$3EFF` included.
2240    #[test]
2241    fn m30_four_screen_uses_the_last_chr_ram_bank() {
2242        let mut m = Unrom512M30::new(synth_prg_16k(16), &[], true, true, 0, true).unwrap();
2243        assert_eq!(m.current_mirroring(), Mirroring::FourScreen);
2244        assert!(m.nametable_unfolded());
2245        assert!(m.nametable_write(0x2C00, 0x44));
2246        assert!(m.nametable_write(0x3C00, 0x55));
2247        assert_eq!(m.nametable_fetch(0x2C00), Some(0x44));
2248        assert_eq!(m.nametable_fetch(0x3C00), Some(0x55));
2249        // The same bytes seen as pattern data in CHR bank 3.
2250        m.cpu_write(0xC000, 0x60);
2251        assert_eq!(m.ppu_read(0x0C00), 0x44);
2252        assert_eq!(m.ppu_read(0x1C00), 0x55);
2253    }
2254
2255    #[test]
2256    fn m30_save_state_round_trip() {
2257        let mut m = Unrom512M30::new(synth_prg_16k(8), &[], false, true, 0, false).unwrap();
2258        m.cpu_write(0x8001, 0x45);
2259        m.ppu_write(0x0003, 0x77);
2260        let blob = m.save_state();
2261        let mut m2 = Unrom512M30::new(synth_prg_16k(8), &[], false, true, 0, false).unwrap();
2262        m2.load_state(&blob).unwrap();
2263        assert_eq!(m2.cpu_read(0x8000), m.cpu_read(0x8000));
2264        assert_eq!(m2.ppu_read(0x0003), 0x77);
2265    }
2266
2267    #[test]
2268    fn m30_header_mirroring_matches_mesen2() {
2269        // byte6 N/M decode, mirroring vocabulary (Mesen2 `UnRom512`):
2270        //   00 (four_screen=0, vertical=0) -> Horizontal mirroring
2271        //   01 (four_screen=0, vertical=1) -> Vertical mirroring
2272        // No latch write needed; this is the hard-wired arrangement.
2273        let m_h = Unrom512M30::new(synth_prg_16k(2), &[], false, false, 0, false).unwrap();
2274        assert_eq!(m_h.current_mirroring(), Mirroring::Horizontal);
2275        let m_v = Unrom512M30::new(synth_prg_16k(2), &[], false, true, 0, false).unwrap();
2276        assert_eq!(m_v.current_mirroring(), Mirroring::Vertical);
2277    }
2278
2279    #[test]
2280    fn m30_submapper3_runtime_hv_switch() {
2281        // Submapper 3: latch bit 7 flips H/V at runtime; power-on default is
2282        // Vertical (Mesen2). No bus conflicts (flash wiring), latch at $C000+.
2283        let mut m = Unrom512M30::new(synth_prg_16k(8), &[], false, false, 3, false).unwrap();
2284        assert_eq!(
2285            m.current_mirroring(),
2286            Mirroring::Vertical,
2287            "power-on default"
2288        );
2289        // Clear bit 7 -> Horizontal.
2290        m.cpu_write(0xC000, 0x00);
2291        assert_eq!(m.current_mirroring(), Mirroring::Horizontal);
2292        // Set bit 7 -> Vertical.
2293        m.cpu_write(0xC000, 0x80);
2294        assert_eq!(m.current_mirroring(), Mirroring::Vertical);
2295    }
2296
2297    #[test]
2298    fn m30_bus_conflict_high_window_uses_fixed_bank() {
2299        // Bus-conflict cart (submapper 0, no battery): the latch responds across
2300        // the whole $8000-$FFFF. A $C000-$FFFF write ANDs against the FIXED last
2301        // bank's byte, NOT the currently-selected low bank. In an 8-bank
2302        // `synth_prg_16k` ROM, bank b holds `b` at offset 0 and `0xFF` elsewhere.
2303        let mut m = Unrom512M30::new(synth_prg_16k(8), &[], false, true, 0, false).unwrap();
2304        // Seed the low bank to 2 via a write at offset 1 (current low bank 0,
2305        // byte 1 = 0xFF, so the value passes through unmasked).
2306        m.cpu_write(0x8001, 0x02);
2307        assert_eq!(m.cpu_read(0x8000), 2);
2308        // Write 0x1F at $C000 (offset 0). The AND source is the FIXED bank 7
2309        // (byte 0 = 0x07): 0x1F & 0x07 = 0x07 -> low bank becomes 7. The old
2310        // (buggy) behaviour would source the now-bank-2 low window (byte 0 =
2311        // 0x02): 0x1F & 0x02 = 0x02 -> bank 2. Asserting 7 proves the fix.
2312        m.cpu_write(0xC000, 0x1F);
2313        assert_eq!(m.cpu_read(0x8000), 7);
2314    }
2315}