Skip to main content

rustynes_mappers/
multicart_discrete.rs

1// SPDX-License-Identifier: GPL-3.0-or-later
2//
3// Provenance: discrete multicart boards (e.g. NTDEC Mapper 221, Txc Bmc11160) are derived from Mesen2 (GPL-3.0-or-later), `Ntdec/Mapper221.h` / `Txc/Bmc11160.h`. See docs/originality-and-provenance.md (Section 1)
4// and NOTICE for the complete, audited derivation record.
5//! Discrete-logic multicart boards addressed by their iNES mapper number:
6//! K-1029 / Contra Function 16 (mapper 15), and the 20-in-1 / Super 700-in-1
7//! style boards on mappers 61 and 62.
8//!
9//! What these share is that the *address written to* carries as much
10//! information as the byte written. Mapper 15 selects among four PRG banking
11//! modes from the low two address bits; mapper 61 picks 16 KiB-vs-32 KiB mode
12//! the same way; mapper 62 splits a CHR bank field across the address and the
13//! data. That is cheaper in discrete logic than decoding a wide register, and
14//! it is why these decode paths look address-driven rather than value-driven.
15//!
16//! A best-effort (Tier-2) board: register-decode correctness verified against
17//! the `GeraNES` reference emulator (cross-referenced, not copied)
18//! and the nesdev wiki, with no commercial-oracle ROM in the tree. Banking math
19//! is direct slice indexing and every bank select wraps with `% count`, so a
20//! register write can never index out of bounds -- required for the `#![no_std]`
21//! chip stack, which cannot afford a panic on a register access.
22//!
23//! See `tier.rs` (`MapperTier::BestEffort`), `docs/adr/0011-mapper-tiering.md`,
24//! and `docs/mappers.md` §Mapper coverage matrix.
25
26#![allow(
27    clippy::bool_to_int_with_if,
28    clippy::cast_lossless,
29    clippy::cast_possible_truncation,
30    clippy::doc_markdown,
31    clippy::match_same_arms,
32    clippy::missing_const_for_fn,
33    clippy::similar_names,
34    clippy::struct_excessive_bools,
35    clippy::too_many_lines,
36    clippy::unreadable_literal
37)]
38
39use crate::cartridge::Mirroring;
40use crate::mapper::{Mapper, MapperCaps, MapperError};
41use alloc::{boxed::Box, vec::Vec};
42use alloc::{format, vec};
43
44const PRG_BANK_16K: usize = 0x4000;
45const PRG_BANK_32K: usize = 0x8000;
46const PRG_BANK_8K: usize = 0x2000;
47const CHR_BANK_8K: usize = 0x2000;
48const NAMETABLE_SIZE: usize = 0x0400;
49const NAMETABLE_SIZE_U16: u16 = 0x0400;
50
51const SAVE_STATE_VERSION: u8 = 1;
52
53// ---------------------------------------------------------------------------
54// Shared nametable helper (mirrors the one in the other simple-mapper modules).
55// ---------------------------------------------------------------------------
56
57const fn nametable_offset(addr: u16, mirroring: Mirroring) -> usize {
58    let table = (((addr - 0x2000) / NAMETABLE_SIZE_U16) & 0x03) as u8;
59    let local = (addr as usize) & (NAMETABLE_SIZE - 1);
60    let physical = mirroring.physical_bank(table);
61    physical * NAMETABLE_SIZE + local
62}
63
64// ===========================================================================
65// Mapper 15 — K-1029 / 100-in-1 Contra Function 16.
66//
67// Single register decoded across $8000-$FFFF (data + low two address bits):
68//   addr bits 0-1 select the banking MODE; data holds the PRG bank, a CHR-RAM
69//   mirroring bit (bit 6) and a "half-bank" bit (bit 7).
70//     mode 0: 32 KiB at the 16 KiB granularity, second half = bank|1
71//     mode 1: 128 KiB? upper half forced to bank|7 (UNROM-like fixed top)
72//     mode 2: 8 KiB-granular ((bank<<1)|b) mirrored across the whole window
73//     mode 3: single 16 KiB bank mirrored across the whole window
74//   CHR is always 8 KiB RAM; CHR writes are protected in modes 0 and 3.
75//   mirroring: data bit 6 (1 = horizontal, 0 = vertical). No IRQ.
76// ===========================================================================
77
78const fn byte_to_mirroring(b: u8, fallback: Mirroring) -> Mirroring {
79    match b {
80        0 => Mirroring::Horizontal,
81        1 => Mirroring::Vertical,
82        2 => Mirroring::SingleScreenA,
83        3 => Mirroring::SingleScreenB,
84        4 => Mirroring::FourScreen,
85        5 => Mirroring::MapperControlled,
86        _ => fallback,
87    }
88}
89
90/// Validate a PRG-ROM image is a non-zero multiple of 8 KiB.
91fn check_prg(prg: &[u8], id: u16) -> Result<(), MapperError> {
92    if prg.is_empty() || !prg.len().is_multiple_of(PRG_BANK_8K) {
93        return Err(MapperError::Invalid(format!(
94            "mapper {id} PRG-ROM size {} is not a non-zero multiple of 8 KiB",
95            prg.len()
96        )));
97    }
98    Ok(())
99}
100
101/// Allocate the CHR slice, falling back to an 8 KiB CHR-RAM bank when the ROM
102/// ships no CHR-ROM. Returns `(chr, is_ram)`.
103fn chr_or_ram(chr_rom: Box<[u8]>) -> (Box<[u8]>, bool) {
104    if chr_rom.is_empty() {
105        (vec![0u8; CHR_BANK_8K].into_boxed_slice(), true)
106    } else {
107        (chr_rom, false)
108    }
109}
110
111const fn mirroring_to_byte(m: Mirroring) -> u8 {
112    match m {
113        Mirroring::Horizontal => 0,
114        Mirroring::Vertical => 1,
115        Mirroring::SingleScreenA => 2,
116        Mirroring::SingleScreenB => 3,
117        Mirroring::FourScreen => 4,
118        Mirroring::MapperControlled => 5,
119    }
120}
121
122/// Mapper 15 (K-1029 / 100-in-1 Contra Function 16 multicart).
123pub struct Multicart15 {
124    prg_rom: Box<[u8]>,
125    chr_ram: Box<[u8]>,
126    /// 8 KiB of PRG-RAM at CPU `$6000-$7FFF`.
127    ///
128    /// A real K-1029 PCB has none. It is here because the NESdev wiki's mapper
129    /// 15 page requires it: only two cartridges genuinely use this mapper, and
130    /// "all other known ROM files claiming to use mapper 15 have been identified
131    /// as being mapper hacks... to run these mapper-hacked games, emulators must
132    /// [...] provide 8 KiB of PRG-RAM at CPU $6000-$7FFF even though an actual
133    /// K-1029 PCB would not."
134    prg_ram: Box<[u8]>,
135    vram: Box<[u8]>,
136    mode: u8,
137    prg_bank: u8,
138    half: u8,
139    horizontal_mirroring: bool,
140}
141
142impl Multicart15 {
143    /// Construct a new mapper 15 board.
144    ///
145    /// # Errors
146    ///
147    /// Returns [`MapperError::Invalid`] when PRG is not a non-zero multiple of
148    /// 16 KiB. CHR is always 8 KiB RAM (any supplied CHR-ROM is rejected).
149    pub fn new(prg_rom: Box<[u8]>, chr_rom: &[u8]) -> Result<Self, MapperError> {
150        if prg_rom.is_empty() || !prg_rom.len().is_multiple_of(PRG_BANK_16K) {
151            return Err(MapperError::Invalid(format!(
152                "mapper 15 PRG-ROM size {} is not a non-zero multiple of 16 KiB",
153                prg_rom.len()
154            )));
155        }
156        if !chr_rom.is_empty() {
157            return Err(MapperError::Invalid(format!(
158                "mapper 15 uses 8 KiB CHR-RAM; got {} bytes of CHR-ROM",
159                chr_rom.len()
160            )));
161        }
162        Ok(Self {
163            prg_rom,
164            chr_ram: vec![0u8; CHR_BANK_8K].into_boxed_slice(),
165            prg_ram: vec![0u8; PRG_BANK_8K].into_boxed_slice(),
166            vram: vec![0u8; 2 * NAMETABLE_SIZE].into_boxed_slice(),
167            mode: 0,
168            prg_bank: 0,
169            half: 0,
170            horizontal_mirroring: false,
171        })
172    }
173
174    fn prg_count_16k(&self) -> usize {
175        (self.prg_rom.len() / PRG_BANK_16K).max(1)
176    }
177
178    fn read_16k(&self, bank: usize, off: usize) -> u8 {
179        let bank = bank % self.prg_count_16k();
180        self.prg_rom[bank * PRG_BANK_16K + off]
181    }
182
183    fn read_8k(&self, bank: usize, off: usize) -> u8 {
184        let count = (self.prg_rom.len() / PRG_BANK_8K).max(1);
185        let bank = bank % count;
186        self.prg_rom[bank * PRG_BANK_8K + off]
187    }
188}
189
190impl Mapper for Multicart15 {
191    // Battery save: the 8 KiB PRG-RAM at `$6000-$7FFF` (core audit 5.1e).
192    fn sram(&self) -> &[u8] {
193        &self.prg_ram
194    }
195    fn sram_mut(&mut self) -> &mut [u8] {
196        &mut self.prg_ram
197    }
198
199    fn caps(&self) -> MapperCaps {
200        MapperCaps::NONE
201    }
202
203    fn cpu_read(&mut self, addr: u16) -> u8 {
204        if (0x6000..=0x7FFF).contains(&addr) {
205            return self.prg_ram[(addr as usize) - 0x6000];
206        }
207        if !(0x8000..=0xFFFF).contains(&addr) {
208            return 0;
209        }
210        let win = (addr as usize) - 0x8000; // 0..0x8000 (the visible 32 KiB)
211        let bank = self.prg_bank as usize;
212        match self.mode {
213            0 => {
214                if win < PRG_BANK_16K {
215                    self.read_16k(bank, win)
216                } else {
217                    self.read_16k(bank | 1, win - PRG_BANK_16K)
218                }
219            }
220            1 => {
221                if win < PRG_BANK_16K {
222                    self.read_16k(bank, win)
223                } else {
224                    self.read_16k(bank | 7, win - PRG_BANK_16K)
225                }
226            }
227            2 => {
228                // 8 KiB-granular, mirrored across the whole window.
229                let off = win & (PRG_BANK_8K - 1);
230                self.read_8k((bank << 1) | (self.half as usize), off)
231            }
232            // mode 3: a single 16 KiB bank mirrored across the window.
233            _ => {
234                let off = win & (PRG_BANK_16K - 1);
235                self.read_16k(bank, off)
236            }
237        }
238    }
239
240    fn cpu_write(&mut self, addr: u16, value: u8) {
241        if (0x6000..=0x7FFF).contains(&addr) {
242            self.prg_ram[(addr as usize) - 0x6000] = value;
243            return;
244        }
245        if (0x8000..=0xFFFF).contains(&addr) {
246            self.mode = (addr & 0x03) as u8;
247            self.horizontal_mirroring = (value & 0x40) != 0;
248            self.prg_bank = value & 0x3F;
249            self.half = u8::from((value & 0x80) != 0);
250        }
251    }
252
253    fn ppu_read(&mut self, addr: u16) -> u8 {
254        let addr = addr & 0x3FFF;
255        match addr {
256            0x0000..=0x1FFF => self.chr_ram[addr as usize],
257            0x2000..=0x3EFF => self.vram[nametable_offset(addr, self.current_mirroring())],
258            _ => 0,
259        }
260    }
261
262    fn ppu_write(&mut self, addr: u16, value: u8) {
263        let addr = addr & 0x3FFF;
264        match addr {
265            0x0000..=0x1FFF => {
266                // NOT write-protected, deliberately, against the hardware.
267                //
268                // A real K-1029 write-protects CHR-RAM in PRG banking modes 0
269                // and 3 (NESdev `INES_Mapper_015`), and RustyNES enforced that
270                // faithfully. The NESdev page also says an emulator must not:
271                // only two cartridges genuinely use mapper 15, and "all other
272                // known ROM files claiming to use mapper 15 have been identified
273                // as being mapper hacks, usually from mappers 164 and 227. To
274                // run these mapper-hacked games, emulators must not enforce
275                // CHR-RAM write-protection even though an actual K-1029 PCB
276                // would".
277                //
278                // Measured, which is what settled it: of the 11 mapper-15 ROMs
279                // in the coverage corpus, the ONE that rendered was
280                // `100-in-1 Contra Function 16` -- a genuine K-1029 cart. The
281                // other ten are hacks (several literally tagged `[hM15]` in
282                // their filenames) and every one of them booted to a blank
283                // screen, because power-on mode is 0 and their tile uploads
284                // were being discarded. Emulating the board correctly is what
285                // broke them.
286                self.chr_ram[addr as usize] = value;
287            }
288            0x2000..=0x3EFF => {
289                let off = nametable_offset(addr, self.current_mirroring());
290                self.vram[off] = value;
291            }
292            _ => {}
293        }
294    }
295
296    fn current_mirroring(&self) -> Mirroring {
297        if self.horizontal_mirroring {
298            Mirroring::Horizontal
299        } else {
300            Mirroring::Vertical
301        }
302    }
303
304    fn save_state(&self) -> Vec<u8> {
305        // `prg_ram` is part of the state, not an afterthought: the mapper-15
306        // ROMs that need this RAM are the hacks, several of which set the
307        // battery flag, so a snapshot omitting it would silently drop save data
308        // across a state load. Adding mapper state without adding it to the
309        // snapshot is the exact schema gap v2.2.3 had to go back and fix.
310        let mut out =
311            Vec::with_capacity(5 + self.vram.len() + self.chr_ram.len() + self.prg_ram.len());
312        out.push(SAVE_STATE_VERSION);
313        out.push(self.mode);
314        out.push(self.prg_bank);
315        out.push(self.half);
316        out.push(u8::from(self.horizontal_mirroring));
317        out.extend_from_slice(&self.vram);
318        out.extend_from_slice(&self.chr_ram);
319        out.extend_from_slice(&self.prg_ram);
320        out
321    }
322
323    fn load_state(&mut self, data: &[u8]) -> Result<(), MapperError> {
324        // The PRG-RAM tail is new in v2.3.4 and shares the version byte, so a
325        // state written before it was told apart by length and loaded with the
326        // RAM cleared. Since v2.9.8 (ADR 0042) only the full layout loads; the
327        // short one is a truncation.
328        let with_ram = 5 + self.vram.len() + self.chr_ram.len() + self.prg_ram.len();
329        if data.len() != with_ram {
330            return Err(MapperError::WrongLength {
331                expected: with_ram,
332                got: data.len(),
333            });
334        }
335        if data[0] != SAVE_STATE_VERSION {
336            return Err(MapperError::UnsupportedVersion(data[0]));
337        }
338        self.mode = data[1];
339        self.prg_bank = data[2];
340        self.half = data[3];
341        self.horizontal_mirroring = data[4] != 0;
342        let mut cursor = 5;
343        self.vram
344            .copy_from_slice(&data[cursor..cursor + self.vram.len()]);
345        cursor += self.vram.len();
346        self.chr_ram
347            .copy_from_slice(&data[cursor..cursor + self.chr_ram.len()]);
348        cursor += self.chr_ram.len();
349        self.prg_ram
350            .copy_from_slice(&data[cursor..cursor + self.prg_ram.len()]);
351        Ok(())
352    }
353}
354
355/// Mapper 61 (0x80-style multicart).
356pub struct Multicart61 {
357    prg_rom: Box<[u8]>,
358    chr_ram: Box<[u8]>,
359    vram: Box<[u8]>,
360    prg_page: u8,
361    prg_16k_mode: bool,
362    horizontal_mirroring: bool,
363}
364
365impl Multicart61 {
366    /// Construct a new mapper 61 board.
367    ///
368    /// # Errors
369    ///
370    /// Returns [`MapperError::Invalid`] when PRG is not a non-zero multiple of
371    /// 16 KiB. CHR is always 8 KiB RAM (any supplied CHR-ROM is rejected).
372    pub fn new(prg_rom: Box<[u8]>, chr_rom: &[u8]) -> Result<Self, MapperError> {
373        if prg_rom.is_empty() || !prg_rom.len().is_multiple_of(PRG_BANK_16K) {
374            return Err(MapperError::Invalid(format!(
375                "mapper 61 PRG-ROM size {} is not a non-zero multiple of 16 KiB",
376                prg_rom.len()
377            )));
378        }
379        if !chr_rom.is_empty() {
380            return Err(MapperError::Invalid(format!(
381                "mapper 61 uses 8 KiB CHR-RAM; got {} bytes of CHR-ROM",
382                chr_rom.len()
383            )));
384        }
385        Ok(Self {
386            prg_rom,
387            chr_ram: vec![0u8; CHR_BANK_8K].into_boxed_slice(),
388            vram: vec![0u8; 2 * NAMETABLE_SIZE].into_boxed_slice(),
389            prg_page: 0,
390            prg_16k_mode: false,
391            horizontal_mirroring: false,
392        })
393    }
394}
395
396impl Mapper for Multicart61 {
397    fn caps(&self) -> MapperCaps {
398        MapperCaps::NONE
399    }
400
401    fn cpu_read(&mut self, addr: u16) -> u8 {
402        if !(0x8000..=0xFFFF).contains(&addr) {
403            return 0;
404        }
405        let win = (addr as usize) - 0x8000;
406        if self.prg_16k_mode {
407            let count = (self.prg_rom.len() / PRG_BANK_16K).max(1);
408            let bank = (self.prg_page as usize) % count;
409            let off = win & (PRG_BANK_16K - 1);
410            self.prg_rom[bank * PRG_BANK_16K + off]
411        } else {
412            let count = (self.prg_rom.len() / PRG_BANK_32K).max(1);
413            let bank = ((self.prg_page >> 1) as usize) % count;
414            // v2.9.0 (re-audit NC-01): modulo the ROM length. A 16 KiB image
415            // clamps `count` to 1, and bank 0's 32 KiB window then runs past
416            // the end; a smaller part in a larger window mirrors on hardware.
417            // The identity for every image whose window fits.
418            self.prg_rom[(bank * PRG_BANK_32K + win) % self.prg_rom.len()]
419        }
420    }
421
422    fn cpu_write(&mut self, addr: u16, _value: u8) {
423        if (0x8000..=0xFFFF).contains(&addr) {
424            let lo = (addr & 0x0F) as u8;
425            let hi = ((addr >> 5) & 0x01) as u8;
426            self.prg_page = (lo << 1) | hi;
427            self.prg_16k_mode = (addr & 0x10) != 0;
428            self.horizontal_mirroring = (addr & 0x80) != 0;
429        }
430    }
431
432    fn ppu_read(&mut self, addr: u16) -> u8 {
433        let addr = addr & 0x3FFF;
434        match addr {
435            0x0000..=0x1FFF => self.chr_ram[addr as usize],
436            0x2000..=0x3EFF => self.vram[nametable_offset(addr, self.current_mirroring())],
437            _ => 0,
438        }
439    }
440
441    fn ppu_write(&mut self, addr: u16, value: u8) {
442        let addr = addr & 0x3FFF;
443        match addr {
444            0x0000..=0x1FFF => self.chr_ram[addr as usize] = value,
445            0x2000..=0x3EFF => {
446                let off = nametable_offset(addr, self.current_mirroring());
447                self.vram[off] = value;
448            }
449            _ => {}
450        }
451    }
452
453    fn current_mirroring(&self) -> Mirroring {
454        if self.horizontal_mirroring {
455            Mirroring::Horizontal
456        } else {
457            Mirroring::Vertical
458        }
459    }
460
461    fn save_state(&self) -> Vec<u8> {
462        let mut out = Vec::with_capacity(4 + self.vram.len() + self.chr_ram.len());
463        out.push(SAVE_STATE_VERSION);
464        out.push(self.prg_page);
465        out.push(u8::from(self.prg_16k_mode));
466        out.push(u8::from(self.horizontal_mirroring));
467        out.extend_from_slice(&self.vram);
468        out.extend_from_slice(&self.chr_ram);
469        out
470    }
471
472    fn load_state(&mut self, data: &[u8]) -> Result<(), MapperError> {
473        let expected = 4 + self.vram.len() + self.chr_ram.len();
474        if data.len() != expected {
475            return Err(MapperError::WrongLength {
476                expected,
477                got: data.len(),
478            });
479        }
480        if data[0] != SAVE_STATE_VERSION {
481            return Err(MapperError::UnsupportedVersion(data[0]));
482        }
483        self.prg_page = data[1];
484        self.prg_16k_mode = data[2] != 0;
485        self.horizontal_mirroring = data[3] != 0;
486        let mut cursor = 4;
487        self.vram
488            .copy_from_slice(&data[cursor..cursor + self.vram.len()]);
489        cursor += self.vram.len();
490        self.chr_ram
491            .copy_from_slice(&data[cursor..cursor + self.chr_ram.len()]);
492        Ok(())
493    }
494}
495
496// ===========================================================================
497// Mapper 62 — multicart.
498//
499// Both the CPU address and the written byte feed the register ($8000-$FFFF).
500// With `A = addr` and `D = data`:
501//   prg_page          = ((A & 0x3F00) >> 8) | (A & 0x40)
502//   chr_bank (4-bit?) = ((A & 0x1F) << 2) | (D & 0x03)
503//   prg_16k_mode      =  (A & 0x20) != 0
504//   horizontal_mirror =  (A & 0x80) != 0
505// In 16 KiB mode the 16 KiB bank `prg_page` is mirrored across the window; in
506// 32 KiB mode bank `prg_page >> 1` is used. CHR is 8 KiB ROM banked. No IRQ.
507// ===========================================================================
508
509/// Mapper 62 (multicart).
510pub struct Multicart62 {
511    prg_rom: Box<[u8]>,
512    chr_rom: Box<[u8]>,
513    vram: Box<[u8]>,
514    prg_page: u8,
515    chr_bank: u8,
516    prg_16k_mode: bool,
517    horizontal_mirroring: bool,
518}
519
520impl Multicart62 {
521    /// Construct a new mapper 62 board.
522    ///
523    /// # Errors
524    ///
525    /// Returns [`MapperError::Invalid`] when PRG is not a non-zero multiple of
526    /// 16 KiB or CHR-ROM is empty / not a multiple of 8 KiB.
527    pub fn new(
528        prg_rom: Box<[u8]>,
529        chr_rom: Box<[u8]>,
530        mirroring: Mirroring,
531    ) -> Result<Self, MapperError> {
532        if prg_rom.is_empty() || !prg_rom.len().is_multiple_of(PRG_BANK_16K) {
533            return Err(MapperError::Invalid(format!(
534                "mapper 62 PRG-ROM size {} is not a non-zero multiple of 16 KiB",
535                prg_rom.len()
536            )));
537        }
538        if chr_rom.is_empty() || !chr_rom.len().is_multiple_of(CHR_BANK_8K) {
539            return Err(MapperError::Invalid(format!(
540                "mapper 62 CHR-ROM size {} is not a non-zero multiple of 8 KiB",
541                chr_rom.len()
542            )));
543        }
544        Ok(Self {
545            prg_rom,
546            chr_rom,
547            vram: vec![0u8; 2 * NAMETABLE_SIZE].into_boxed_slice(),
548            prg_page: 0,
549            chr_bank: 0,
550            // Seed from the header arrangement so the power-on default is sane.
551            prg_16k_mode: false,
552            horizontal_mirroring: mirroring == Mirroring::Horizontal,
553        })
554    }
555}
556
557impl Mapper for Multicart62 {
558    fn caps(&self) -> MapperCaps {
559        MapperCaps::NONE
560    }
561
562    fn cpu_read(&mut self, addr: u16) -> u8 {
563        if !(0x8000..=0xFFFF).contains(&addr) {
564            return 0;
565        }
566        let win = (addr as usize) - 0x8000;
567        if self.prg_16k_mode {
568            let count = (self.prg_rom.len() / PRG_BANK_16K).max(1);
569            let bank = (self.prg_page as usize) % count;
570            let off = win & (PRG_BANK_16K - 1);
571            self.prg_rom[bank * PRG_BANK_16K + off]
572        } else {
573            let count = (self.prg_rom.len() / PRG_BANK_32K).max(1);
574            let bank = ((self.prg_page >> 1) as usize) % count;
575            // v2.9.0 (re-audit NC-01): modulo the ROM length, as mapper 61.
576            self.prg_rom[(bank * PRG_BANK_32K + win) % self.prg_rom.len()]
577        }
578    }
579
580    fn cpu_write(&mut self, addr: u16, value: u8) {
581        if (0x8000..=0xFFFF).contains(&addr) {
582            let page_lo = ((addr & 0x3F00) >> 8) as u8;
583            let page_hi = (addr & 0x40) as u8;
584            self.prg_page = page_lo | page_hi;
585            let chr_hi = ((addr & 0x1F) as u8) << 2;
586            self.chr_bank = chr_hi | (value & 0x03);
587            self.prg_16k_mode = (addr & 0x20) != 0;
588            self.horizontal_mirroring = (addr & 0x80) != 0;
589        }
590    }
591
592    fn ppu_read(&mut self, addr: u16) -> u8 {
593        let addr = addr & 0x3FFF;
594        match addr {
595            0x0000..=0x1FFF => {
596                let count = (self.chr_rom.len() / CHR_BANK_8K).max(1);
597                let bank = (self.chr_bank as usize) % count;
598                self.chr_rom[bank * CHR_BANK_8K + addr as usize]
599            }
600            0x2000..=0x3EFF => self.vram[nametable_offset(addr, self.current_mirroring())],
601            _ => 0,
602        }
603    }
604
605    fn ppu_write(&mut self, addr: u16, value: u8) {
606        let addr = addr & 0x3FFF;
607        if let 0x2000..=0x3EFF = addr {
608            let off = nametable_offset(addr, self.current_mirroring());
609            self.vram[off] = value;
610        }
611    }
612
613    fn current_mirroring(&self) -> Mirroring {
614        if self.horizontal_mirroring {
615            Mirroring::Horizontal
616        } else {
617            Mirroring::Vertical
618        }
619    }
620
621    fn save_state(&self) -> Vec<u8> {
622        let mut out = Vec::with_capacity(5 + self.vram.len());
623        out.push(SAVE_STATE_VERSION);
624        out.push(self.prg_page);
625        out.push(self.chr_bank);
626        out.push(u8::from(self.prg_16k_mode));
627        out.push(u8::from(self.horizontal_mirroring));
628        out.extend_from_slice(&self.vram);
629        out
630    }
631
632    fn load_state(&mut self, data: &[u8]) -> Result<(), MapperError> {
633        let expected = 5 + self.vram.len();
634        if data.len() != expected {
635            return Err(MapperError::WrongLength {
636                expected,
637                got: data.len(),
638            });
639        }
640        if data[0] != SAVE_STATE_VERSION {
641            return Err(MapperError::UnsupportedVersion(data[0]));
642        }
643        self.prg_page = data[1];
644        self.chr_bank = data[2];
645        self.prg_16k_mode = data[3] != 0;
646        self.horizontal_mirroring = data[4] != 0;
647        self.vram.copy_from_slice(&data[5..5 + self.vram.len()]);
648        Ok(())
649    }
650}
651
652/// Mapper 200 (`MG109` NROM-128 multicart).
653pub struct Multicart200 {
654    prg_rom: Box<[u8]>,
655    chr: Box<[u8]>,
656    vram: Box<[u8]>,
657    chr_is_ram: bool,
658    bank: u8,
659    horizontal_mirroring: bool,
660}
661
662impl Multicart200 {
663    /// Construct a new mapper 200 board.
664    ///
665    /// # Errors
666    ///
667    /// Returns [`MapperError::Invalid`] when PRG is not a non-zero multiple of
668    /// 16 KiB, or CHR-ROM (when present) is not a multiple of 8 KiB.
669    pub fn new(
670        prg_rom: Box<[u8]>,
671        chr_rom: Box<[u8]>,
672        mirroring: Mirroring,
673    ) -> Result<Self, MapperError> {
674        if prg_rom.is_empty() || !prg_rom.len().is_multiple_of(PRG_BANK_16K) {
675            return Err(MapperError::Invalid(format!(
676                "mapper 200 PRG-ROM size {} is not a non-zero multiple of 16 KiB",
677                prg_rom.len()
678            )));
679        }
680        let chr_is_ram = chr_rom.is_empty();
681        let chr: Box<[u8]> = if chr_is_ram {
682            vec![0u8; CHR_BANK_8K].into_boxed_slice()
683        } else if chr_rom.len().is_multiple_of(CHR_BANK_8K) {
684            chr_rom
685        } else {
686            return Err(MapperError::Invalid(format!(
687                "mapper 200 CHR-ROM size {} is not a multiple of 8 KiB",
688                chr_rom.len()
689            )));
690        };
691        Ok(Self {
692            prg_rom,
693            chr,
694            vram: vec![0u8; 2 * NAMETABLE_SIZE].into_boxed_slice(),
695            chr_is_ram,
696            bank: 0,
697            horizontal_mirroring: mirroring == Mirroring::Horizontal,
698        })
699    }
700}
701
702impl Mapper for Multicart200 {
703    fn caps(&self) -> MapperCaps {
704        MapperCaps::NONE
705    }
706
707    fn cpu_read(&mut self, addr: u16) -> u8 {
708        if (0x8000..=0xFFFF).contains(&addr) {
709            // NROM-128: $8000-$BFFF mirrors $C000-$FFFF.
710            let count = (self.prg_rom.len() / PRG_BANK_16K).max(1);
711            let bank = (self.bank as usize) % count;
712            let off = (addr as usize - 0x8000) & (PRG_BANK_16K - 1);
713            self.prg_rom[bank * PRG_BANK_16K + off]
714        } else {
715            0
716        }
717    }
718
719    fn cpu_write(&mut self, addr: u16, _value: u8) {
720        if (0x8000..=0xFFFF).contains(&addr) {
721            self.bank = (addr & 0x07) as u8;
722            self.horizontal_mirroring = (addr & 0x08) != 0;
723        }
724    }
725
726    fn ppu_read(&mut self, addr: u16) -> u8 {
727        let addr = addr & 0x3FFF;
728        match addr {
729            0x0000..=0x1FFF => {
730                let count = (self.chr.len() / CHR_BANK_8K).max(1);
731                let bank = (self.bank as usize) % count;
732                self.chr[bank * CHR_BANK_8K + addr as usize]
733            }
734            0x2000..=0x3EFF => self.vram[nametable_offset(addr, self.current_mirroring())],
735            _ => 0,
736        }
737    }
738
739    fn ppu_write(&mut self, addr: u16, value: u8) {
740        let addr = addr & 0x3FFF;
741        match addr {
742            0x0000..=0x1FFF => {
743                if self.chr_is_ram {
744                    let count = (self.chr.len() / CHR_BANK_8K).max(1);
745                    let bank = (self.bank as usize) % count;
746                    self.chr[bank * CHR_BANK_8K + addr as usize] = value;
747                }
748            }
749            0x2000..=0x3EFF => {
750                let off = nametable_offset(addr, self.current_mirroring());
751                self.vram[off] = value;
752            }
753            _ => {}
754        }
755    }
756
757    fn current_mirroring(&self) -> Mirroring {
758        if self.horizontal_mirroring {
759            Mirroring::Horizontal
760        } else {
761            Mirroring::Vertical
762        }
763    }
764
765    fn save_state(&self) -> Vec<u8> {
766        let chr_extra = if self.chr_is_ram { self.chr.len() } else { 0 };
767        let mut out = Vec::with_capacity(3 + self.vram.len() + chr_extra);
768        out.push(SAVE_STATE_VERSION);
769        out.push(self.bank);
770        out.push(u8::from(self.horizontal_mirroring));
771        out.extend_from_slice(&self.vram);
772        if self.chr_is_ram {
773            out.extend_from_slice(&self.chr);
774        }
775        out
776    }
777
778    fn load_state(&mut self, data: &[u8]) -> Result<(), MapperError> {
779        let chr_extra = if self.chr_is_ram { self.chr.len() } else { 0 };
780        let expected = 3 + self.vram.len() + chr_extra;
781        if data.len() != expected {
782            return Err(MapperError::WrongLength {
783                expected,
784                got: data.len(),
785            });
786        }
787        if data[0] != SAVE_STATE_VERSION {
788            return Err(MapperError::UnsupportedVersion(data[0]));
789        }
790        self.bank = data[1];
791        self.horizontal_mirroring = data[2] != 0;
792        let mut cursor = 3;
793        self.vram
794            .copy_from_slice(&data[cursor..cursor + self.vram.len()]);
795        cursor += self.vram.len();
796        if self.chr_is_ram {
797            self.chr
798                .copy_from_slice(&data[cursor..cursor + self.chr.len()]);
799        }
800        Ok(())
801    }
802}
803
804// ===========================================================================
805// Mapper 201 — NROM-256 multicart (BNROM + CNROM overlaid, address-driven).
806//
807// Write $8000-$FFFF: the ADDRESS low byte selects one bank that drives both a
808// 32 KiB PRG bank and an 8 KiB CHR bank:
809//   PRG (32 KiB) = addr & 0x03   (masked to PRG bank count)
810//   CHR (8 KiB)  = addr & 0x07   (masked to CHR bank count)
811// (All known games use only the low 2 bits.) Mirroring header-fixed; no IRQ.
812// ===========================================================================
813
814/// Mapper 201 (NROM-256 multicart).
815pub struct Multicart201 {
816    prg_rom: Box<[u8]>,
817    chr_rom: Box<[u8]>,
818    vram: Box<[u8]>,
819    chr_is_ram: bool,
820    prg_bank: u8,
821    chr_bank: u8,
822    mirroring: Mirroring,
823}
824
825impl Multicart201 {
826    /// Construct a new mapper 201 board.
827    ///
828    /// # Errors
829    ///
830    /// Returns [`MapperError::Invalid`] when PRG is not a non-zero multiple of
831    /// 32 KiB, or CHR-ROM (when present) is not a multiple of 8 KiB.
832    pub fn new(
833        prg_rom: Box<[u8]>,
834        chr_rom: Box<[u8]>,
835        mirroring: Mirroring,
836    ) -> Result<Self, MapperError> {
837        if prg_rom.is_empty() || !prg_rom.len().is_multiple_of(PRG_BANK_32K) {
838            return Err(MapperError::Invalid(format!(
839                "mapper 201 PRG-ROM size {} is not a non-zero multiple of 32 KiB",
840                prg_rom.len()
841            )));
842        }
843        let chr_is_ram = chr_rom.is_empty();
844        let chr_rom: Box<[u8]> = if chr_is_ram {
845            vec![0u8; CHR_BANK_8K].into_boxed_slice()
846        } else if chr_rom.len().is_multiple_of(CHR_BANK_8K) {
847            chr_rom
848        } else {
849            return Err(MapperError::Invalid(format!(
850                "mapper 201 CHR-ROM size {} is not a multiple of 8 KiB",
851                chr_rom.len()
852            )));
853        };
854        Ok(Self {
855            prg_rom,
856            chr_rom,
857            vram: vec![0u8; 2 * NAMETABLE_SIZE].into_boxed_slice(),
858            chr_is_ram,
859            prg_bank: 0,
860            chr_bank: 0,
861            mirroring,
862        })
863    }
864
865    fn chr_offset(&self, addr: u16) -> usize {
866        let count = (self.chr_rom.len() / CHR_BANK_8K).max(1);
867        let bank = (self.chr_bank as usize) % count;
868        bank * CHR_BANK_8K + addr as usize
869    }
870}
871
872impl Mapper for Multicart201 {
873    fn caps(&self) -> MapperCaps {
874        MapperCaps::NONE
875    }
876
877    fn cpu_read(&mut self, addr: u16) -> u8 {
878        if (0x8000..=0xFFFF).contains(&addr) {
879            let count = (self.prg_rom.len() / PRG_BANK_32K).max(1);
880            let bank = (self.prg_bank as usize) % count;
881            self.prg_rom[bank * PRG_BANK_32K + (addr as usize - 0x8000)]
882        } else {
883            0
884        }
885    }
886
887    fn cpu_write(&mut self, addr: u16, _value: u8) {
888        if (0x8000..=0xFFFF).contains(&addr) {
889            self.prg_bank = (addr & 0x03) as u8;
890            self.chr_bank = (addr & 0x07) as u8;
891        }
892    }
893
894    fn ppu_read(&mut self, addr: u16) -> u8 {
895        let addr = addr & 0x3FFF;
896        match addr {
897            0x0000..=0x1FFF => self.chr_rom[self.chr_offset(addr)],
898            0x2000..=0x3EFF => self.vram[nametable_offset(addr, self.mirroring)],
899            _ => 0,
900        }
901    }
902
903    fn ppu_write(&mut self, addr: u16, value: u8) {
904        let addr = addr & 0x3FFF;
905        match addr {
906            0x0000..=0x1FFF => {
907                if self.chr_is_ram {
908                    let off = self.chr_offset(addr);
909                    self.chr_rom[off] = value;
910                }
911            }
912            0x2000..=0x3EFF => {
913                let off = nametable_offset(addr, self.mirroring);
914                self.vram[off] = value;
915            }
916            _ => {}
917        }
918    }
919
920    fn current_mirroring(&self) -> Mirroring {
921        self.mirroring
922    }
923
924    fn save_state(&self) -> Vec<u8> {
925        let chr_extra = if self.chr_is_ram {
926            self.chr_rom.len()
927        } else {
928            0
929        };
930        let mut out = Vec::with_capacity(3 + self.vram.len() + chr_extra);
931        out.push(SAVE_STATE_VERSION);
932        out.push(self.prg_bank);
933        out.push(self.chr_bank);
934        out.extend_from_slice(&self.vram);
935        if self.chr_is_ram {
936            out.extend_from_slice(&self.chr_rom);
937        }
938        out
939    }
940
941    fn load_state(&mut self, data: &[u8]) -> Result<(), MapperError> {
942        let chr_extra = if self.chr_is_ram {
943            self.chr_rom.len()
944        } else {
945            0
946        };
947        let expected = 3 + self.vram.len() + chr_extra;
948        if data.len() != expected {
949            return Err(MapperError::WrongLength {
950                expected,
951                got: data.len(),
952            });
953        }
954        if data[0] != SAVE_STATE_VERSION {
955            return Err(MapperError::UnsupportedVersion(data[0]));
956        }
957        self.prg_bank = data[1];
958        self.chr_bank = data[2];
959        let mut cursor = 3;
960        self.vram
961            .copy_from_slice(&data[cursor..cursor + self.vram.len()]);
962        cursor += self.vram.len();
963        if self.chr_is_ram {
964            self.chr_rom
965                .copy_from_slice(&data[cursor..cursor + self.chr_rom.len()]);
966        }
967        Ok(())
968    }
969}
970
971// ===========================================================================
972// Mapper 202 — 150-in-1 multicart (address latch).
973//
974// Write $8000-$FFFF, ADDRESS-driven (data ignored):
975//   A~[.... .... .... O..O]  (PRG mode bits, combine to form a 2-bit value)
976//   A~[.... .... .... RRRM]  (R = page register, M = mirroring)
977//   prg_mode_is_32k = (((addr >> 1) & 0x01) == 1 && (addr & 0x01) == 1)
978//                   i.e. the two "O" bits (addr bit 3 and addr bit 0) == 0b11
979//   page = (addr >> 1) & 0x07
980//   mirroring = addr & 0x01 (0: vertical, 1: horizontal)
981// In 16 KiB mode the page maps both halves; in 32 KiB mode (page>>1) selects a
982// 32 KiB bank. CHR (8 KiB) = page. Mirroring runtime; no IRQ.
983//
984// Per the nesdev wiki the "O" bits are addr bit 3 and addr bit 0; if both set,
985// 32 KiB mode. We follow the BizHawk/Disch convention used in the wiki note.
986// ===========================================================================
987
988/// Mapper 202 (150-in-1 multicart).
989pub struct Multicart202 {
990    prg_rom: Box<[u8]>,
991    chr_rom: Box<[u8]>,
992    vram: Box<[u8]>,
993    chr_is_ram: bool,
994    page: u8,
995    prg_32k_mode: bool,
996    horizontal_mirroring: bool,
997}
998
999impl Multicart202 {
1000    /// Construct a new mapper 202 board.
1001    ///
1002    /// # Errors
1003    ///
1004    /// Returns [`MapperError::Invalid`] when PRG is not a non-zero multiple of
1005    /// 16 KiB, or CHR-ROM (when present) is not a multiple of 8 KiB.
1006    pub fn new(
1007        prg_rom: Box<[u8]>,
1008        chr_rom: Box<[u8]>,
1009        mirroring: Mirroring,
1010    ) -> Result<Self, MapperError> {
1011        if prg_rom.is_empty() || !prg_rom.len().is_multiple_of(PRG_BANK_16K) {
1012            return Err(MapperError::Invalid(format!(
1013                "mapper 202 PRG-ROM size {} is not a non-zero multiple of 16 KiB",
1014                prg_rom.len()
1015            )));
1016        }
1017        let chr_is_ram = chr_rom.is_empty();
1018        let chr_rom: Box<[u8]> = if chr_is_ram {
1019            vec![0u8; CHR_BANK_8K].into_boxed_slice()
1020        } else if chr_rom.len().is_multiple_of(CHR_BANK_8K) {
1021            chr_rom
1022        } else {
1023            return Err(MapperError::Invalid(format!(
1024                "mapper 202 CHR-ROM size {} is not a multiple of 8 KiB",
1025                chr_rom.len()
1026            )));
1027        };
1028        Ok(Self {
1029            prg_rom,
1030            chr_rom,
1031            vram: vec![0u8; 2 * NAMETABLE_SIZE].into_boxed_slice(),
1032            chr_is_ram,
1033            page: 0,
1034            prg_32k_mode: false,
1035            horizontal_mirroring: mirroring == Mirroring::Horizontal,
1036        })
1037    }
1038
1039    fn chr_offset(&self, addr: u16) -> usize {
1040        let count = (self.chr_rom.len() / CHR_BANK_8K).max(1);
1041        let bank = (self.page as usize) % count;
1042        bank * CHR_BANK_8K + addr as usize
1043    }
1044}
1045
1046impl Mapper for Multicart202 {
1047    fn caps(&self) -> MapperCaps {
1048        MapperCaps::NONE
1049    }
1050
1051    fn cpu_read(&mut self, addr: u16) -> u8 {
1052        match addr {
1053            0x8000..=0xFFFF => {
1054                let count = (self.prg_rom.len() / PRG_BANK_16K).max(1);
1055                if self.prg_32k_mode {
1056                    // 32 KiB bank (page >> 1) spread across the whole window.
1057                    let lo16 = ((self.page >> 1) << 1) as usize % count;
1058                    let off = addr as usize - 0x8000;
1059                    // v2.9.0 (re-audit NC-01): modulo the ROM length. An odd
1060                    // 16 KiB bank count (e.g. 80 KiB: `lo16` 4 of 5) or a
1061                    // 16 KiB image puts the window's second half past the
1062                    // end; a smaller part mirrors on hardware. The identity
1063                    // for every image whose window fits.
1064                    self.prg_rom[(lo16 * PRG_BANK_16K + off) % self.prg_rom.len()]
1065                } else {
1066                    // 16 KiB mode: same page mirrored at $8000 and $C000.
1067                    let bank = (self.page as usize) % count;
1068                    let off = (addr as usize - 0x8000) & (PRG_BANK_16K - 1);
1069                    self.prg_rom[bank * PRG_BANK_16K + off]
1070                }
1071            }
1072            _ => 0,
1073        }
1074    }
1075
1076    fn cpu_write(&mut self, addr: u16, _value: u8) {
1077        if (0x8000..=0xFFFF).contains(&addr) {
1078            // "O" bits = addr bit 3 and addr bit 0; both set => 32 KiB mode.
1079            self.prg_32k_mode = (addr & 0x09) == 0x09;
1080            self.page = ((addr >> 1) & 0x07) as u8;
1081            self.horizontal_mirroring = (addr & 0x01) != 0;
1082        }
1083    }
1084
1085    fn ppu_read(&mut self, addr: u16) -> u8 {
1086        let addr = addr & 0x3FFF;
1087        match addr {
1088            0x0000..=0x1FFF => self.chr_rom[self.chr_offset(addr)],
1089            0x2000..=0x3EFF => self.vram[nametable_offset(addr, self.current_mirroring())],
1090            _ => 0,
1091        }
1092    }
1093
1094    fn ppu_write(&mut self, addr: u16, value: u8) {
1095        let addr = addr & 0x3FFF;
1096        match addr {
1097            0x0000..=0x1FFF => {
1098                if self.chr_is_ram {
1099                    let off = self.chr_offset(addr);
1100                    self.chr_rom[off] = value;
1101                }
1102            }
1103            0x2000..=0x3EFF => {
1104                let off = nametable_offset(addr, self.current_mirroring());
1105                self.vram[off] = value;
1106            }
1107            _ => {}
1108        }
1109    }
1110
1111    fn current_mirroring(&self) -> Mirroring {
1112        if self.horizontal_mirroring {
1113            Mirroring::Horizontal
1114        } else {
1115            Mirroring::Vertical
1116        }
1117    }
1118
1119    fn save_state(&self) -> Vec<u8> {
1120        let chr_extra = if self.chr_is_ram {
1121            self.chr_rom.len()
1122        } else {
1123            0
1124        };
1125        let mut out = Vec::with_capacity(4 + self.vram.len() + chr_extra);
1126        out.push(SAVE_STATE_VERSION);
1127        out.push(self.page);
1128        out.push(u8::from(self.prg_32k_mode));
1129        out.push(u8::from(self.horizontal_mirroring));
1130        out.extend_from_slice(&self.vram);
1131        if self.chr_is_ram {
1132            out.extend_from_slice(&self.chr_rom);
1133        }
1134        out
1135    }
1136
1137    fn load_state(&mut self, data: &[u8]) -> Result<(), MapperError> {
1138        let chr_extra = if self.chr_is_ram {
1139            self.chr_rom.len()
1140        } else {
1141            0
1142        };
1143        let expected = 4 + self.vram.len() + chr_extra;
1144        if data.len() != expected {
1145            return Err(MapperError::WrongLength {
1146                expected,
1147                got: data.len(),
1148            });
1149        }
1150        if data[0] != SAVE_STATE_VERSION {
1151            return Err(MapperError::UnsupportedVersion(data[0]));
1152        }
1153        self.page = data[1];
1154        self.prg_32k_mode = data[2] != 0;
1155        self.horizontal_mirroring = data[3] != 0;
1156        let mut cursor = 4;
1157        self.vram
1158            .copy_from_slice(&data[cursor..cursor + self.vram.len()]);
1159        cursor += self.vram.len();
1160        if self.chr_is_ram {
1161            self.chr_rom
1162                .copy_from_slice(&data[cursor..cursor + self.chr_rom.len()]);
1163        }
1164        Ok(())
1165    }
1166}
1167
1168// ===========================================================================
1169// Mapper 203 — 35-in-1 multicart (data latch).
1170//
1171// Write $8000-$FFFF, DATA-driven:
1172//   PPPP PPCC
1173//   PRG (16 KiB, mirrored at $8000 and $C000) = (data >> 2) & 0x3F
1174//   CHR (8 KiB)                               = data & 0x03
1175// Mirroring header-fixed; no IRQ.
1176// ===========================================================================
1177
1178/// Mapper 203 (35-in-1 multicart).
1179pub struct Multicart203 {
1180    prg_rom: Box<[u8]>,
1181    chr_rom: Box<[u8]>,
1182    vram: Box<[u8]>,
1183    chr_is_ram: bool,
1184    prg_bank: u8,
1185    chr_bank: u8,
1186    mirroring: Mirroring,
1187}
1188
1189impl Multicart203 {
1190    /// Construct a new mapper 203 board.
1191    ///
1192    /// # Errors
1193    ///
1194    /// Returns [`MapperError::Invalid`] when PRG is not a non-zero multiple of
1195    /// 16 KiB, or CHR-ROM (when present) is not a multiple of 8 KiB.
1196    pub fn new(
1197        prg_rom: Box<[u8]>,
1198        chr_rom: Box<[u8]>,
1199        mirroring: Mirroring,
1200    ) -> Result<Self, MapperError> {
1201        if prg_rom.is_empty() || !prg_rom.len().is_multiple_of(PRG_BANK_16K) {
1202            return Err(MapperError::Invalid(format!(
1203                "mapper 203 PRG-ROM size {} is not a non-zero multiple of 16 KiB",
1204                prg_rom.len()
1205            )));
1206        }
1207        let chr_is_ram = chr_rom.is_empty();
1208        let chr_rom: Box<[u8]> = if chr_is_ram {
1209            vec![0u8; CHR_BANK_8K].into_boxed_slice()
1210        } else if chr_rom.len().is_multiple_of(CHR_BANK_8K) {
1211            chr_rom
1212        } else {
1213            return Err(MapperError::Invalid(format!(
1214                "mapper 203 CHR-ROM size {} is not a multiple of 8 KiB",
1215                chr_rom.len()
1216            )));
1217        };
1218        Ok(Self {
1219            prg_rom,
1220            chr_rom,
1221            vram: vec![0u8; 2 * NAMETABLE_SIZE].into_boxed_slice(),
1222            chr_is_ram,
1223            prg_bank: 0,
1224            chr_bank: 0,
1225            mirroring,
1226        })
1227    }
1228
1229    fn chr_offset(&self, addr: u16) -> usize {
1230        let count = (self.chr_rom.len() / CHR_BANK_8K).max(1);
1231        let bank = (self.chr_bank as usize) % count;
1232        bank * CHR_BANK_8K + addr as usize
1233    }
1234}
1235
1236impl Mapper for Multicart203 {
1237    fn caps(&self) -> MapperCaps {
1238        MapperCaps::NONE
1239    }
1240
1241    fn cpu_read(&mut self, addr: u16) -> u8 {
1242        if (0x8000..=0xFFFF).contains(&addr) {
1243            // NROM-128: $8000-$BFFF mirrors $C000-$FFFF.
1244            let count = (self.prg_rom.len() / PRG_BANK_16K).max(1);
1245            let bank = (self.prg_bank as usize) % count;
1246            let off = (addr as usize - 0x8000) & (PRG_BANK_16K - 1);
1247            self.prg_rom[bank * PRG_BANK_16K + off]
1248        } else {
1249            0
1250        }
1251    }
1252
1253    fn cpu_write(&mut self, addr: u16, value: u8) {
1254        if (0x8000..=0xFFFF).contains(&addr) {
1255            self.prg_bank = (value >> 2) & 0x3F;
1256            self.chr_bank = value & 0x03;
1257        }
1258    }
1259
1260    fn ppu_read(&mut self, addr: u16) -> u8 {
1261        let addr = addr & 0x3FFF;
1262        match addr {
1263            0x0000..=0x1FFF => self.chr_rom[self.chr_offset(addr)],
1264            0x2000..=0x3EFF => self.vram[nametable_offset(addr, self.mirroring)],
1265            _ => 0,
1266        }
1267    }
1268
1269    fn ppu_write(&mut self, addr: u16, value: u8) {
1270        let addr = addr & 0x3FFF;
1271        match addr {
1272            0x0000..=0x1FFF => {
1273                if self.chr_is_ram {
1274                    let off = self.chr_offset(addr);
1275                    self.chr_rom[off] = value;
1276                }
1277            }
1278            0x2000..=0x3EFF => {
1279                let off = nametable_offset(addr, self.mirroring);
1280                self.vram[off] = value;
1281            }
1282            _ => {}
1283        }
1284    }
1285
1286    fn current_mirroring(&self) -> Mirroring {
1287        self.mirroring
1288    }
1289
1290    fn save_state(&self) -> Vec<u8> {
1291        let chr_extra = if self.chr_is_ram {
1292            self.chr_rom.len()
1293        } else {
1294            0
1295        };
1296        let mut out = Vec::with_capacity(3 + self.vram.len() + chr_extra);
1297        out.push(SAVE_STATE_VERSION);
1298        out.push(self.prg_bank);
1299        out.push(self.chr_bank);
1300        out.extend_from_slice(&self.vram);
1301        if self.chr_is_ram {
1302            out.extend_from_slice(&self.chr_rom);
1303        }
1304        out
1305    }
1306
1307    fn load_state(&mut self, data: &[u8]) -> Result<(), MapperError> {
1308        let chr_extra = if self.chr_is_ram {
1309            self.chr_rom.len()
1310        } else {
1311            0
1312        };
1313        let expected = 3 + self.vram.len() + chr_extra;
1314        if data.len() != expected {
1315            return Err(MapperError::WrongLength {
1316                expected,
1317                got: data.len(),
1318            });
1319        }
1320        if data[0] != SAVE_STATE_VERSION {
1321            return Err(MapperError::UnsupportedVersion(data[0]));
1322        }
1323        self.prg_bank = data[1];
1324        self.chr_bank = data[2];
1325        let mut cursor = 3;
1326        self.vram
1327            .copy_from_slice(&data[cursor..cursor + self.vram.len()]);
1328        cursor += self.vram.len();
1329        if self.chr_is_ram {
1330            self.chr_rom
1331                .copy_from_slice(&data[cursor..cursor + self.chr_rom.len()]);
1332        }
1333        Ok(())
1334    }
1335}
1336
1337// ===========================================================================
1338// Mapper 212 — BMC Super HiK 300-in-1 (address latch).
1339//
1340// Write $8000-$FFFF, ADDRESS-driven (data ignored):
1341//   A~[1o.. .... .... MBBb]
1342//   prg_32k_mode = bit 14 of addr ("o")
1343//   page (3-bit)  = addr & 0x07 (drives 16 KiB PRG, 32 KiB PRG and 8 KiB CHR)
1344//   mirroring = bit 3 of addr (0: vertical, 1: horizontal)
1345//   16 KiB mode: page maps both $8000 and $C000 windows
1346//   32 KiB mode: (page >> 1) selects a 32 KiB bank
1347//   CHR (8 KiB) = page (regardless of "o")
1348// Reads at $6000-$7FFF with (addr & 0x10) == 0 return bit 7 set (a protection
1349// signature). Mirroring runtime; no IRQ.
1350// ===========================================================================
1351
1352/// Mapper 212 (`BMC` Super `HiK` 300-in-1 multicart).
1353pub struct Multicart212 {
1354    prg_rom: Box<[u8]>,
1355    chr_rom: Box<[u8]>,
1356    vram: Box<[u8]>,
1357    chr_is_ram: bool,
1358    page: u8,
1359    prg_32k_mode: bool,
1360    horizontal_mirroring: bool,
1361}
1362
1363impl Multicart212 {
1364    /// Construct a new mapper 212 board.
1365    ///
1366    /// # Errors
1367    ///
1368    /// Returns [`MapperError::Invalid`] when PRG is not a non-zero multiple of
1369    /// 16 KiB, or CHR-ROM (when present) is not a multiple of 8 KiB.
1370    pub fn new(
1371        prg_rom: Box<[u8]>,
1372        chr_rom: Box<[u8]>,
1373        mirroring: Mirroring,
1374    ) -> Result<Self, MapperError> {
1375        if prg_rom.is_empty() || !prg_rom.len().is_multiple_of(PRG_BANK_16K) {
1376            return Err(MapperError::Invalid(format!(
1377                "mapper 212 PRG-ROM size {} is not a non-zero multiple of 16 KiB",
1378                prg_rom.len()
1379            )));
1380        }
1381        let chr_is_ram = chr_rom.is_empty();
1382        let chr_rom: Box<[u8]> = if chr_is_ram {
1383            vec![0u8; CHR_BANK_8K].into_boxed_slice()
1384        } else if chr_rom.len().is_multiple_of(CHR_BANK_8K) {
1385            chr_rom
1386        } else {
1387            return Err(MapperError::Invalid(format!(
1388                "mapper 212 CHR-ROM size {} is not a multiple of 8 KiB",
1389                chr_rom.len()
1390            )));
1391        };
1392        Ok(Self {
1393            prg_rom,
1394            chr_rom,
1395            vram: vec![0u8; 2 * NAMETABLE_SIZE].into_boxed_slice(),
1396            chr_is_ram,
1397            page: 0,
1398            prg_32k_mode: false,
1399            horizontal_mirroring: mirroring == Mirroring::Horizontal,
1400        })
1401    }
1402
1403    fn chr_offset(&self, addr: u16) -> usize {
1404        let count = (self.chr_rom.len() / CHR_BANK_8K).max(1);
1405        let bank = (self.page as usize) % count;
1406        bank * CHR_BANK_8K + addr as usize
1407    }
1408}
1409
1410impl Mapper for Multicart212 {
1411    fn caps(&self) -> MapperCaps {
1412        MapperCaps::NONE
1413    }
1414
1415    fn cpu_read_unmapped(&self, addr: u16) -> bool {
1416        // $6000-$7FFF carries the protection signature (mapped). $4020-$5FFF
1417        // is unmapped open bus, as for stock boards.
1418        (0x4020..=0x5FFF).contains(&addr)
1419    }
1420
1421    fn cpu_read(&mut self, addr: u16) -> u8 {
1422        match addr {
1423            0x6000..=0x7FFF => {
1424                // Protection: (addr & 0x10) == 0 reads $80; else open-bus-ish 0.
1425                if (addr & 0x0010) == 0 { 0x80 } else { 0x00 }
1426            }
1427            0x8000..=0xFFFF => {
1428                let count = (self.prg_rom.len() / PRG_BANK_16K).max(1);
1429                if self.prg_32k_mode {
1430                    let lo16 = ((self.page >> 1) << 1) as usize % count;
1431                    let off = addr as usize - 0x8000;
1432                    // v2.9.0 (re-audit NC-01): modulo the ROM length, as
1433                    // mapper 202 (48 and 80 KiB images reach past the end).
1434                    self.prg_rom[(lo16 * PRG_BANK_16K + off) % self.prg_rom.len()]
1435                } else {
1436                    let bank = (self.page as usize) % count;
1437                    let off = (addr as usize - 0x8000) & (PRG_BANK_16K - 1);
1438                    self.prg_rom[bank * PRG_BANK_16K + off]
1439                }
1440            }
1441            _ => 0,
1442        }
1443    }
1444
1445    fn cpu_write(&mut self, addr: u16, _value: u8) {
1446        if (0x8000..=0xFFFF).contains(&addr) {
1447            self.prg_32k_mode = (addr & 0x4000) != 0;
1448            self.page = (addr & 0x07) as u8;
1449            self.horizontal_mirroring = (addr & 0x0008) != 0;
1450        }
1451    }
1452
1453    fn ppu_read(&mut self, addr: u16) -> u8 {
1454        let addr = addr & 0x3FFF;
1455        match addr {
1456            0x0000..=0x1FFF => self.chr_rom[self.chr_offset(addr)],
1457            0x2000..=0x3EFF => self.vram[nametable_offset(addr, self.current_mirroring())],
1458            _ => 0,
1459        }
1460    }
1461
1462    fn ppu_write(&mut self, addr: u16, value: u8) {
1463        let addr = addr & 0x3FFF;
1464        match addr {
1465            0x0000..=0x1FFF => {
1466                if self.chr_is_ram {
1467                    let off = self.chr_offset(addr);
1468                    self.chr_rom[off] = value;
1469                }
1470            }
1471            0x2000..=0x3EFF => {
1472                let off = nametable_offset(addr, self.current_mirroring());
1473                self.vram[off] = value;
1474            }
1475            _ => {}
1476        }
1477    }
1478
1479    fn current_mirroring(&self) -> Mirroring {
1480        if self.horizontal_mirroring {
1481            Mirroring::Horizontal
1482        } else {
1483            Mirroring::Vertical
1484        }
1485    }
1486
1487    fn save_state(&self) -> Vec<u8> {
1488        let chr_extra = if self.chr_is_ram {
1489            self.chr_rom.len()
1490        } else {
1491            0
1492        };
1493        let mut out = Vec::with_capacity(4 + self.vram.len() + chr_extra);
1494        out.push(SAVE_STATE_VERSION);
1495        out.push(self.page);
1496        out.push(u8::from(self.prg_32k_mode));
1497        out.push(u8::from(self.horizontal_mirroring));
1498        out.extend_from_slice(&self.vram);
1499        if self.chr_is_ram {
1500            out.extend_from_slice(&self.chr_rom);
1501        }
1502        out
1503    }
1504
1505    fn load_state(&mut self, data: &[u8]) -> Result<(), MapperError> {
1506        let chr_extra = if self.chr_is_ram {
1507            self.chr_rom.len()
1508        } else {
1509            0
1510        };
1511        let expected = 4 + self.vram.len() + chr_extra;
1512        if data.len() != expected {
1513            return Err(MapperError::WrongLength {
1514                expected,
1515                got: data.len(),
1516            });
1517        }
1518        if data[0] != SAVE_STATE_VERSION {
1519            return Err(MapperError::UnsupportedVersion(data[0]));
1520        }
1521        self.page = data[1];
1522        self.prg_32k_mode = data[2] != 0;
1523        self.horizontal_mirroring = data[3] != 0;
1524        let mut cursor = 4;
1525        self.vram
1526            .copy_from_slice(&data[cursor..cursor + self.vram.len()]);
1527        cursor += self.vram.len();
1528        if self.chr_is_ram {
1529            self.chr_rom
1530                .copy_from_slice(&data[cursor..cursor + self.chr_rom.len()]);
1531        }
1532        Ok(())
1533    }
1534}
1535
1536// ===========================================================================
1537// Mapper 213 — 9999999-in-1 multicart (address latch; duplicate of 58).
1538//
1539// Write $8000-$FFFF, ADDRESS-driven (data ignored):
1540//   CHR (8 KiB)  = (addr >> 3) & 0x07
1541//   PRG (32 KiB) = (addr >> 1) & 0x03
1542// NROM-256-style mirroring (header-fixed). No IRQ.
1543// ===========================================================================
1544
1545/// Mapper 213 (9999999-in-1 multicart).
1546pub struct Multicart213 {
1547    prg_rom: Box<[u8]>,
1548    chr_rom: Box<[u8]>,
1549    vram: Box<[u8]>,
1550    chr_is_ram: bool,
1551    prg_bank: u8,
1552    chr_bank: u8,
1553    mirroring: Mirroring,
1554}
1555
1556impl Multicart213 {
1557    /// Construct a new mapper 213 board.
1558    ///
1559    /// # Errors
1560    ///
1561    /// Returns [`MapperError::Invalid`] when PRG is not a non-zero multiple of
1562    /// 32 KiB, or CHR-ROM (when present) is not a multiple of 8 KiB.
1563    pub fn new(
1564        prg_rom: Box<[u8]>,
1565        chr_rom: Box<[u8]>,
1566        mirroring: Mirroring,
1567    ) -> Result<Self, MapperError> {
1568        if prg_rom.is_empty() || !prg_rom.len().is_multiple_of(PRG_BANK_32K) {
1569            return Err(MapperError::Invalid(format!(
1570                "mapper 213 PRG-ROM size {} is not a non-zero multiple of 32 KiB",
1571                prg_rom.len()
1572            )));
1573        }
1574        let chr_is_ram = chr_rom.is_empty();
1575        let chr_rom: Box<[u8]> = if chr_is_ram {
1576            vec![0u8; CHR_BANK_8K].into_boxed_slice()
1577        } else if chr_rom.len().is_multiple_of(CHR_BANK_8K) {
1578            chr_rom
1579        } else {
1580            return Err(MapperError::Invalid(format!(
1581                "mapper 213 CHR-ROM size {} is not a multiple of 8 KiB",
1582                chr_rom.len()
1583            )));
1584        };
1585        Ok(Self {
1586            prg_rom,
1587            chr_rom,
1588            vram: vec![0u8; 2 * NAMETABLE_SIZE].into_boxed_slice(),
1589            chr_is_ram,
1590            prg_bank: 0,
1591            chr_bank: 0,
1592            mirroring,
1593        })
1594    }
1595
1596    fn chr_offset(&self, addr: u16) -> usize {
1597        let count = (self.chr_rom.len() / CHR_BANK_8K).max(1);
1598        let bank = (self.chr_bank as usize) % count;
1599        bank * CHR_BANK_8K + addr as usize
1600    }
1601}
1602
1603impl Mapper for Multicart213 {
1604    fn caps(&self) -> MapperCaps {
1605        MapperCaps::NONE
1606    }
1607
1608    fn cpu_read(&mut self, addr: u16) -> u8 {
1609        if (0x8000..=0xFFFF).contains(&addr) {
1610            let count = (self.prg_rom.len() / PRG_BANK_32K).max(1);
1611            let bank = (self.prg_bank as usize) % count;
1612            self.prg_rom[bank * PRG_BANK_32K + (addr as usize - 0x8000)]
1613        } else {
1614            0
1615        }
1616    }
1617
1618    fn cpu_write(&mut self, addr: u16, _value: u8) {
1619        if (0x8000..=0xFFFF).contains(&addr) {
1620            self.chr_bank = ((addr >> 3) & 0x07) as u8;
1621            self.prg_bank = ((addr >> 1) & 0x03) as u8;
1622        }
1623    }
1624
1625    fn ppu_read(&mut self, addr: u16) -> u8 {
1626        let addr = addr & 0x3FFF;
1627        match addr {
1628            0x0000..=0x1FFF => self.chr_rom[self.chr_offset(addr)],
1629            0x2000..=0x3EFF => self.vram[nametable_offset(addr, self.mirroring)],
1630            _ => 0,
1631        }
1632    }
1633
1634    fn ppu_write(&mut self, addr: u16, value: u8) {
1635        let addr = addr & 0x3FFF;
1636        match addr {
1637            0x0000..=0x1FFF => {
1638                if self.chr_is_ram {
1639                    let off = self.chr_offset(addr);
1640                    self.chr_rom[off] = value;
1641                }
1642            }
1643            0x2000..=0x3EFF => {
1644                let off = nametable_offset(addr, self.mirroring);
1645                self.vram[off] = value;
1646            }
1647            _ => {}
1648        }
1649    }
1650
1651    fn current_mirroring(&self) -> Mirroring {
1652        self.mirroring
1653    }
1654
1655    fn save_state(&self) -> Vec<u8> {
1656        let chr_extra = if self.chr_is_ram {
1657            self.chr_rom.len()
1658        } else {
1659            0
1660        };
1661        let mut out = Vec::with_capacity(3 + self.vram.len() + chr_extra);
1662        out.push(SAVE_STATE_VERSION);
1663        out.push(self.prg_bank);
1664        out.push(self.chr_bank);
1665        out.extend_from_slice(&self.vram);
1666        if self.chr_is_ram {
1667            out.extend_from_slice(&self.chr_rom);
1668        }
1669        out
1670    }
1671
1672    fn load_state(&mut self, data: &[u8]) -> Result<(), MapperError> {
1673        let chr_extra = if self.chr_is_ram {
1674            self.chr_rom.len()
1675        } else {
1676            0
1677        };
1678        let expected = 3 + self.vram.len() + chr_extra;
1679        if data.len() != expected {
1680            return Err(MapperError::WrongLength {
1681                expected,
1682                got: data.len(),
1683            });
1684        }
1685        if data[0] != SAVE_STATE_VERSION {
1686            return Err(MapperError::UnsupportedVersion(data[0]));
1687        }
1688        self.prg_bank = data[1];
1689        self.chr_bank = data[2];
1690        let mut cursor = 3;
1691        self.vram
1692            .copy_from_slice(&data[cursor..cursor + self.vram.len()]);
1693        cursor += self.vram.len();
1694        if self.chr_is_ram {
1695            self.chr_rom
1696                .copy_from_slice(&data[cursor..cursor + self.chr_rom.len()]);
1697        }
1698        Ok(())
1699    }
1700}
1701
1702// ===========================================================================
1703// Mapper 214 — Super Gun 20-in-1 multicart (address latch).
1704//
1705// Write $8000-$FFFF, ADDRESS-driven (data ignored):
1706//   CHR (8 KiB)  = addr & 0x03
1707//   PRG (16 KiB, mirrored at $8000 and $C000) = (addr >> 2) & 0x03
1708// Mirroring header-fixed; no IRQ.
1709// ===========================================================================
1710
1711/// Mapper 214 (Super Gun 20-in-1 multicart).
1712pub struct Multicart214 {
1713    prg_rom: Box<[u8]>,
1714    chr_rom: Box<[u8]>,
1715    vram: Box<[u8]>,
1716    chr_is_ram: bool,
1717    prg_bank: u8,
1718    chr_bank: u8,
1719    mirroring: Mirroring,
1720}
1721
1722impl Multicart214 {
1723    /// Construct a new mapper 214 board.
1724    ///
1725    /// # Errors
1726    ///
1727    /// Returns [`MapperError::Invalid`] when PRG is not a non-zero multiple of
1728    /// 16 KiB, or CHR-ROM (when present) is not a multiple of 8 KiB.
1729    pub fn new(
1730        prg_rom: Box<[u8]>,
1731        chr_rom: Box<[u8]>,
1732        mirroring: Mirroring,
1733    ) -> Result<Self, MapperError> {
1734        if prg_rom.is_empty() || !prg_rom.len().is_multiple_of(PRG_BANK_16K) {
1735            return Err(MapperError::Invalid(format!(
1736                "mapper 214 PRG-ROM size {} is not a non-zero multiple of 16 KiB",
1737                prg_rom.len()
1738            )));
1739        }
1740        let chr_is_ram = chr_rom.is_empty();
1741        let chr_rom: Box<[u8]> = if chr_is_ram {
1742            vec![0u8; CHR_BANK_8K].into_boxed_slice()
1743        } else if chr_rom.len().is_multiple_of(CHR_BANK_8K) {
1744            chr_rom
1745        } else {
1746            return Err(MapperError::Invalid(format!(
1747                "mapper 214 CHR-ROM size {} is not a multiple of 8 KiB",
1748                chr_rom.len()
1749            )));
1750        };
1751        Ok(Self {
1752            prg_rom,
1753            chr_rom,
1754            vram: vec![0u8; 2 * NAMETABLE_SIZE].into_boxed_slice(),
1755            chr_is_ram,
1756            prg_bank: 0,
1757            chr_bank: 0,
1758            mirroring,
1759        })
1760    }
1761
1762    fn chr_offset(&self, addr: u16) -> usize {
1763        let count = (self.chr_rom.len() / CHR_BANK_8K).max(1);
1764        let bank = (self.chr_bank as usize) % count;
1765        bank * CHR_BANK_8K + addr as usize
1766    }
1767}
1768
1769impl Mapper for Multicart214 {
1770    fn caps(&self) -> MapperCaps {
1771        MapperCaps::NONE
1772    }
1773
1774    fn cpu_read(&mut self, addr: u16) -> u8 {
1775        if (0x8000..=0xFFFF).contains(&addr) {
1776            // NROM-128: $8000-$BFFF mirrors $C000-$FFFF.
1777            let count = (self.prg_rom.len() / PRG_BANK_16K).max(1);
1778            let bank = (self.prg_bank as usize) % count;
1779            let off = (addr as usize - 0x8000) & (PRG_BANK_16K - 1);
1780            self.prg_rom[bank * PRG_BANK_16K + off]
1781        } else {
1782            0
1783        }
1784    }
1785
1786    fn cpu_write(&mut self, addr: u16, _value: u8) {
1787        if (0x8000..=0xFFFF).contains(&addr) {
1788            self.chr_bank = (addr & 0x03) as u8;
1789            self.prg_bank = ((addr >> 2) & 0x03) as u8;
1790        }
1791    }
1792
1793    fn ppu_read(&mut self, addr: u16) -> u8 {
1794        let addr = addr & 0x3FFF;
1795        match addr {
1796            0x0000..=0x1FFF => self.chr_rom[self.chr_offset(addr)],
1797            0x2000..=0x3EFF => self.vram[nametable_offset(addr, self.mirroring)],
1798            _ => 0,
1799        }
1800    }
1801
1802    fn ppu_write(&mut self, addr: u16, value: u8) {
1803        let addr = addr & 0x3FFF;
1804        match addr {
1805            0x0000..=0x1FFF => {
1806                if self.chr_is_ram {
1807                    let off = self.chr_offset(addr);
1808                    self.chr_rom[off] = value;
1809                }
1810            }
1811            0x2000..=0x3EFF => {
1812                let off = nametable_offset(addr, self.mirroring);
1813                self.vram[off] = value;
1814            }
1815            _ => {}
1816        }
1817    }
1818
1819    fn current_mirroring(&self) -> Mirroring {
1820        self.mirroring
1821    }
1822
1823    fn save_state(&self) -> Vec<u8> {
1824        let chr_extra = if self.chr_is_ram {
1825            self.chr_rom.len()
1826        } else {
1827            0
1828        };
1829        let mut out = Vec::with_capacity(3 + self.vram.len() + chr_extra);
1830        out.push(SAVE_STATE_VERSION);
1831        out.push(self.prg_bank);
1832        out.push(self.chr_bank);
1833        out.extend_from_slice(&self.vram);
1834        if self.chr_is_ram {
1835            out.extend_from_slice(&self.chr_rom);
1836        }
1837        out
1838    }
1839
1840    fn load_state(&mut self, data: &[u8]) -> Result<(), MapperError> {
1841        let chr_extra = if self.chr_is_ram {
1842            self.chr_rom.len()
1843        } else {
1844            0
1845        };
1846        let expected = 3 + self.vram.len() + chr_extra;
1847        if data.len() != expected {
1848            return Err(MapperError::WrongLength {
1849                expected,
1850                got: data.len(),
1851            });
1852        }
1853        if data[0] != SAVE_STATE_VERSION {
1854            return Err(MapperError::UnsupportedVersion(data[0]));
1855        }
1856        self.prg_bank = data[1];
1857        self.chr_bank = data[2];
1858        let mut cursor = 3;
1859        self.vram
1860            .copy_from_slice(&data[cursor..cursor + self.vram.len()]);
1861        cursor += self.vram.len();
1862        if self.chr_is_ram {
1863            self.chr_rom
1864                .copy_from_slice(&data[cursor..cursor + self.chr_rom.len()]);
1865        }
1866        Ok(())
1867    }
1868}
1869
1870/// Mapper 58 (multicart).
1871pub struct Multicart58 {
1872    prg_rom: Box<[u8]>,
1873    chr_rom: Box<[u8]>,
1874    vram: Box<[u8]>,
1875    prg_bank: u8,
1876    prg32_mode: bool,
1877    chr_bank: u8,
1878    horizontal_mirroring: bool,
1879}
1880
1881impl Multicart58 {
1882    /// Construct a new mapper 58 board.
1883    ///
1884    /// # Errors
1885    ///
1886    /// Returns [`MapperError::Invalid`] when PRG is not a non-zero multiple of
1887    /// 16 KiB or CHR-ROM is empty / not a multiple of 8 KiB.
1888    pub fn new(
1889        prg_rom: Box<[u8]>,
1890        chr_rom: Box<[u8]>,
1891        mirroring: Mirroring,
1892    ) -> Result<Self, MapperError> {
1893        if prg_rom.is_empty() || !prg_rom.len().is_multiple_of(PRG_BANK_16K) {
1894            return Err(MapperError::Invalid(format!(
1895                "mapper 58 PRG-ROM size {} is not a non-zero multiple of 16 KiB",
1896                prg_rom.len()
1897            )));
1898        }
1899        if chr_rom.is_empty() || !chr_rom.len().is_multiple_of(CHR_BANK_8K) {
1900            return Err(MapperError::Invalid(format!(
1901                "mapper 58 CHR-ROM size {} is not a non-zero multiple of 8 KiB",
1902                chr_rom.len()
1903            )));
1904        }
1905        let horizontal_mirroring = mirroring == Mirroring::Horizontal;
1906        Ok(Self {
1907            prg_rom,
1908            chr_rom,
1909            vram: vec![0u8; 2 * NAMETABLE_SIZE].into_boxed_slice(),
1910            prg_bank: 0,
1911            prg32_mode: false,
1912            chr_bank: 0,
1913            horizontal_mirroring,
1914        })
1915    }
1916}
1917
1918impl Mapper for Multicart58 {
1919    fn caps(&self) -> MapperCaps {
1920        MapperCaps::NONE
1921    }
1922
1923    fn cpu_read(&mut self, addr: u16) -> u8 {
1924        if !(0x8000..=0xFFFF).contains(&addr) {
1925            return 0;
1926        }
1927        if self.prg32_mode {
1928            let count = (self.prg_rom.len() / PRG_BANK_32K).max(1);
1929            let bank = (((self.prg_bank & 0x06) >> 1) as usize) % count;
1930            // v2.9.0 (re-audit NC-01): modulo the ROM length, so a 16 KiB
1931            // image mirrors in the 32 KiB window instead of indexing past it.
1932            self.prg_rom[(bank * PRG_BANK_32K + (addr as usize - 0x8000)) % self.prg_rom.len()]
1933        } else {
1934            let count = (self.prg_rom.len() / PRG_BANK_16K).max(1);
1935            let bank = (self.prg_bank as usize) % count;
1936            self.prg_rom[bank * PRG_BANK_16K + (addr as usize & 0x3FFF)]
1937        }
1938    }
1939
1940    fn cpu_write(&mut self, addr: u16, _value: u8) {
1941        if (0x8000..=0xFFFF).contains(&addr) {
1942            self.prg_bank = (addr & 0x07) as u8;
1943            self.prg32_mode = (addr & 0x40) == 0;
1944            self.chr_bank = ((addr >> 3) & 0x07) as u8;
1945            self.horizontal_mirroring = (addr & 0x80) != 0;
1946        }
1947    }
1948
1949    fn ppu_read(&mut self, addr: u16) -> u8 {
1950        let addr = addr & 0x3FFF;
1951        match addr {
1952            0x0000..=0x1FFF => {
1953                let count = (self.chr_rom.len() / CHR_BANK_8K).max(1);
1954                let bank = (self.chr_bank as usize) % count;
1955                self.chr_rom[bank * CHR_BANK_8K + addr as usize]
1956            }
1957            0x2000..=0x3EFF => self.vram[nametable_offset(addr, self.current_mirroring())],
1958            _ => 0,
1959        }
1960    }
1961
1962    fn ppu_write(&mut self, addr: u16, value: u8) {
1963        let addr = addr & 0x3FFF;
1964        if let 0x2000..=0x3EFF = addr {
1965            let off = nametable_offset(addr, self.current_mirroring());
1966            self.vram[off] = value;
1967        }
1968    }
1969
1970    fn current_mirroring(&self) -> Mirroring {
1971        if self.horizontal_mirroring {
1972            Mirroring::Horizontal
1973        } else {
1974            Mirroring::Vertical
1975        }
1976    }
1977
1978    fn save_state(&self) -> Vec<u8> {
1979        let mut out = Vec::with_capacity(5 + self.vram.len());
1980        out.push(SAVE_STATE_VERSION);
1981        out.push(self.prg_bank);
1982        out.push(u8::from(self.prg32_mode));
1983        out.push(self.chr_bank);
1984        out.push(u8::from(self.horizontal_mirroring));
1985        out.extend_from_slice(&self.vram);
1986        out
1987    }
1988
1989    fn load_state(&mut self, data: &[u8]) -> Result<(), MapperError> {
1990        let expected = 5 + self.vram.len();
1991        if data.len() != expected {
1992            return Err(MapperError::WrongLength {
1993                expected,
1994                got: data.len(),
1995            });
1996        }
1997        if data[0] != SAVE_STATE_VERSION {
1998            return Err(MapperError::UnsupportedVersion(data[0]));
1999        }
2000        self.prg_bank = data[1];
2001        self.prg32_mode = data[2] != 0;
2002        self.chr_bank = data[3];
2003        self.horizontal_mirroring = data[4] != 0;
2004        self.vram.copy_from_slice(&data[5..5 + self.vram.len()]);
2005        Ok(())
2006    }
2007}
2008
2009// ===========================================================================
2010// Mapper 60 — reset-based 4-in-1 multicart.
2011//
2012// The active game is chosen by a reset counter (0..=3) that increments on each
2013// console reset; the selected value drives a 16 KiB PRG bank + 8 KiB CHR bank
2014// in lockstep. Reset-latch behaviour is host-driven (the bus/frontend would
2015// have to pulse a reset hook we do not model in the no_std core), so we model
2016// only the power-on bank (counter = 0). No IRQ.
2017// ===========================================================================
2018
2019/// Mapper 60 (reset-based 4-in-1 multicart; power-on bank only).
2020pub struct Multicart60 {
2021    prg_rom: Box<[u8]>,
2022    chr_rom: Box<[u8]>,
2023    vram: Box<[u8]>,
2024    bank: u8,
2025    mirroring: Mirroring,
2026}
2027
2028impl Multicart60 {
2029    /// Construct a new mapper 60 board.
2030    ///
2031    /// # Errors
2032    ///
2033    /// Returns [`MapperError::Invalid`] when PRG is not a non-zero multiple of
2034    /// 16 KiB or CHR-ROM is empty / not a multiple of 8 KiB.
2035    pub fn new(
2036        prg_rom: Box<[u8]>,
2037        chr_rom: Box<[u8]>,
2038        mirroring: Mirroring,
2039    ) -> Result<Self, MapperError> {
2040        if prg_rom.is_empty() || !prg_rom.len().is_multiple_of(PRG_BANK_16K) {
2041            return Err(MapperError::Invalid(format!(
2042                "mapper 60 PRG-ROM size {} is not a non-zero multiple of 16 KiB",
2043                prg_rom.len()
2044            )));
2045        }
2046        if chr_rom.is_empty() || !chr_rom.len().is_multiple_of(CHR_BANK_8K) {
2047            return Err(MapperError::Invalid(format!(
2048                "mapper 60 CHR-ROM size {} is not a non-zero multiple of 8 KiB",
2049                chr_rom.len()
2050            )));
2051        }
2052        Ok(Self {
2053            prg_rom,
2054            chr_rom,
2055            vram: vec![0u8; 2 * NAMETABLE_SIZE].into_boxed_slice(),
2056            // Power-on selects game 0.
2057            bank: 0,
2058            mirroring,
2059        })
2060    }
2061}
2062
2063impl Mapper for Multicart60 {
2064    fn caps(&self) -> MapperCaps {
2065        MapperCaps::NONE
2066    }
2067
2068    fn cpu_read(&mut self, addr: u16) -> u8 {
2069        if (0x8000..=0xFFFF).contains(&addr) {
2070            // The reset counter drives a 16 KiB bank mirrored across the 32 KiB
2071            // window (NROM-128-per-game).
2072            let count = (self.prg_rom.len() / PRG_BANK_16K).max(1);
2073            let bank = (self.bank as usize) % count;
2074            self.prg_rom[bank * PRG_BANK_16K + (addr as usize & 0x3FFF)]
2075        } else {
2076            0
2077        }
2078    }
2079
2080    fn cpu_write(&mut self, _addr: u16, _value: u8) {}
2081
2082    fn ppu_read(&mut self, addr: u16) -> u8 {
2083        let addr = addr & 0x3FFF;
2084        match addr {
2085            0x0000..=0x1FFF => {
2086                let count = (self.chr_rom.len() / CHR_BANK_8K).max(1);
2087                let bank = (self.bank as usize) % count;
2088                self.chr_rom[bank * CHR_BANK_8K + addr as usize]
2089            }
2090            0x2000..=0x3EFF => self.vram[nametable_offset(addr, self.mirroring)],
2091            _ => 0,
2092        }
2093    }
2094
2095    fn ppu_write(&mut self, addr: u16, value: u8) {
2096        let addr = addr & 0x3FFF;
2097        if let 0x2000..=0x3EFF = addr {
2098            let off = nametable_offset(addr, self.mirroring);
2099            self.vram[off] = value;
2100        }
2101    }
2102
2103    fn current_mirroring(&self) -> Mirroring {
2104        self.mirroring
2105    }
2106
2107    fn save_state(&self) -> Vec<u8> {
2108        let mut out = Vec::with_capacity(2 + self.vram.len());
2109        out.push(SAVE_STATE_VERSION);
2110        out.push(self.bank);
2111        out.extend_from_slice(&self.vram);
2112        out
2113    }
2114
2115    fn load_state(&mut self, data: &[u8]) -> Result<(), MapperError> {
2116        let expected = 2 + self.vram.len();
2117        if data.len() != expected {
2118            return Err(MapperError::WrongLength {
2119                expected,
2120                got: data.len(),
2121            });
2122        }
2123        if data[0] != SAVE_STATE_VERSION {
2124            return Err(MapperError::UnsupportedVersion(data[0]));
2125        }
2126        self.bank = data[1];
2127        self.vram.copy_from_slice(&data[2..2 + self.vram.len()]);
2128        Ok(())
2129    }
2130}
2131
2132// ===========================================================================
2133// Mapper 231 — 20-in-1 multicart.
2134//
2135// Address-decoded register across $8000-$FFFF (data byte ignored). For the
2136// absolute address A:
2137//   prgBank = ((A >> 5) & 0x01) | (A & 0x1E)
2138//   $8000 16 KiB bank = prgBank & 0x1E
2139//   $C000 16 KiB bank = prgBank
2140//   mirroring = bit 7 of A (1 = horizontal, 0 = vertical)
2141// CHR is 8 KiB RAM. No IRQ.
2142// ===========================================================================
2143
2144/// Mapper 231 (20-in-1 multicart).
2145pub struct Multicart231 {
2146    prg_rom: Box<[u8]>,
2147    chr_ram: Box<[u8]>,
2148    vram: Box<[u8]>,
2149    prg_bank0: u8,
2150    prg_bank1: u8,
2151    horizontal_mirroring: bool,
2152}
2153
2154impl Multicart231 {
2155    /// Construct a new mapper 231 board.
2156    ///
2157    /// # Errors
2158    ///
2159    /// Returns [`MapperError::Invalid`] when PRG is not a non-zero multiple of
2160    /// 16 KiB.
2161    pub fn new(
2162        prg_rom: Box<[u8]>,
2163        _chr_rom: &[u8],
2164        mirroring: Mirroring,
2165    ) -> Result<Self, MapperError> {
2166        if prg_rom.is_empty() || !prg_rom.len().is_multiple_of(PRG_BANK_16K) {
2167            return Err(MapperError::Invalid(format!(
2168                "mapper 231 PRG-ROM size {} is not a non-zero multiple of 16 KiB",
2169                prg_rom.len()
2170            )));
2171        }
2172        let horizontal_mirroring = mirroring == Mirroring::Horizontal;
2173        Ok(Self {
2174            prg_rom,
2175            chr_ram: vec![0u8; CHR_BANK_8K].into_boxed_slice(),
2176            vram: vec![0u8; 2 * NAMETABLE_SIZE].into_boxed_slice(),
2177            prg_bank0: 0,
2178            prg_bank1: 0,
2179            horizontal_mirroring,
2180        })
2181    }
2182
2183    fn read_prg(&self, bank: u8, addr: u16) -> u8 {
2184        let count = (self.prg_rom.len() / PRG_BANK_16K).max(1);
2185        let bank = (bank as usize) % count;
2186        self.prg_rom[bank * PRG_BANK_16K + (addr as usize & 0x3FFF)]
2187    }
2188}
2189
2190impl Mapper for Multicart231 {
2191    fn caps(&self) -> MapperCaps {
2192        MapperCaps::NONE
2193    }
2194
2195    fn cpu_read(&mut self, addr: u16) -> u8 {
2196        match addr {
2197            0x8000..=0xBFFF => self.read_prg(self.prg_bank0, addr),
2198            0xC000..=0xFFFF => self.read_prg(self.prg_bank1, addr),
2199            _ => 0,
2200        }
2201    }
2202
2203    fn cpu_write(&mut self, addr: u16, _value: u8) {
2204        if (0x8000..=0xFFFF).contains(&addr) {
2205            let prg_bank = ((((addr >> 5) & 0x01) | (addr & 0x1E)) & 0xFF) as u8;
2206            self.prg_bank0 = prg_bank & 0x1E;
2207            self.prg_bank1 = prg_bank;
2208            self.horizontal_mirroring = (addr & 0x80) != 0;
2209        }
2210    }
2211
2212    fn ppu_read(&mut self, addr: u16) -> u8 {
2213        let addr = addr & 0x3FFF;
2214        match addr {
2215            0x0000..=0x1FFF => self.chr_ram[addr as usize],
2216            0x2000..=0x3EFF => self.vram[nametable_offset(addr, self.current_mirroring())],
2217            _ => 0,
2218        }
2219    }
2220
2221    fn ppu_write(&mut self, addr: u16, value: u8) {
2222        let addr = addr & 0x3FFF;
2223        match addr {
2224            0x0000..=0x1FFF => self.chr_ram[addr as usize] = value,
2225            0x2000..=0x3EFF => {
2226                let off = nametable_offset(addr, self.current_mirroring());
2227                self.vram[off] = value;
2228            }
2229            _ => {}
2230        }
2231    }
2232
2233    fn current_mirroring(&self) -> Mirroring {
2234        if self.horizontal_mirroring {
2235            Mirroring::Horizontal
2236        } else {
2237            Mirroring::Vertical
2238        }
2239    }
2240
2241    fn save_state(&self) -> Vec<u8> {
2242        let mut out = Vec::with_capacity(4 + self.vram.len() + self.chr_ram.len());
2243        out.push(SAVE_STATE_VERSION);
2244        out.push(self.prg_bank0);
2245        out.push(self.prg_bank1);
2246        out.push(u8::from(self.horizontal_mirroring));
2247        out.extend_from_slice(&self.vram);
2248        out.extend_from_slice(&self.chr_ram);
2249        out
2250    }
2251
2252    fn load_state(&mut self, data: &[u8]) -> Result<(), MapperError> {
2253        let expected = 4 + self.vram.len() + self.chr_ram.len();
2254        if data.len() != expected {
2255            return Err(MapperError::WrongLength {
2256                expected,
2257                got: data.len(),
2258            });
2259        }
2260        if data[0] != SAVE_STATE_VERSION {
2261            return Err(MapperError::UnsupportedVersion(data[0]));
2262        }
2263        self.prg_bank0 = data[1];
2264        self.prg_bank1 = data[2];
2265        self.horizontal_mirroring = data[3] != 0;
2266        let mut cursor = 4;
2267        self.vram
2268            .copy_from_slice(&data[cursor..cursor + self.vram.len()]);
2269        cursor += self.vram.len();
2270        self.chr_ram
2271            .copy_from_slice(&data[cursor..cursor + self.chr_ram.len()]);
2272        Ok(())
2273    }
2274}
2275
2276/// Mapper 234 (Maxi 15 / `BNROM`-like multicart).
2277pub struct Maxi15M234 {
2278    prg_rom: Box<[u8]>,
2279    chr_rom: Box<[u8]>,
2280    vram: Box<[u8]>,
2281    reg0: u8,
2282    reg1: u8,
2283}
2284
2285impl Maxi15M234 {
2286    /// Construct a new mapper 234 board.
2287    ///
2288    /// # Errors
2289    ///
2290    /// Returns [`MapperError::Invalid`] when PRG is not a non-zero multiple of
2291    /// 32 KiB or CHR-ROM is empty / not a multiple of 8 KiB.
2292    pub fn new(
2293        prg_rom: Box<[u8]>,
2294        chr_rom: Box<[u8]>,
2295        _mirroring: Mirroring,
2296    ) -> Result<Self, MapperError> {
2297        if prg_rom.is_empty() || !prg_rom.len().is_multiple_of(PRG_BANK_32K) {
2298            return Err(MapperError::Invalid(format!(
2299                "mapper 234 PRG-ROM size {} is not a non-zero multiple of 32 KiB",
2300                prg_rom.len()
2301            )));
2302        }
2303        if chr_rom.is_empty() || !chr_rom.len().is_multiple_of(CHR_BANK_8K) {
2304            return Err(MapperError::Invalid(format!(
2305                "mapper 234 CHR-ROM size {} is not a non-zero multiple of 8 KiB",
2306                chr_rom.len()
2307            )));
2308        }
2309        Ok(Self {
2310            prg_rom,
2311            chr_rom,
2312            vram: vec![0u8; 2 * NAMETABLE_SIZE].into_boxed_slice(),
2313            reg0: 0,
2314            reg1: 0,
2315        })
2316    }
2317
2318    const fn update_state(&mut self) {
2319        if (self.reg0 & 0x40) != 0 {
2320            // NINA-03 mode.
2321            self.reg1 &= 0x71;
2322        } else {
2323            // CNROM mode: only the low 2 bits of the reg1 CHR selector matter.
2324            self.reg1 &= 0x31;
2325        }
2326    }
2327
2328    fn latch_access(&mut self, addr: u16, value: u8) {
2329        if (0xFF80..=0xFF9F).contains(&addr) {
2330            // The reg0 latch only fires while its low 6 bits are still zero.
2331            if self.reg0.trailing_zeros() >= 6 {
2332                self.reg0 = value;
2333                self.update_state();
2334            }
2335        } else if (0xFFE8..=0xFFF8).contains(&addr) {
2336            self.reg1 = value & 0x71;
2337            self.update_state();
2338        }
2339    }
2340
2341    const fn prg_bank(&self) -> u8 {
2342        if (self.reg0 & 0x40) != 0 {
2343            (self.reg0 & 0x0E) | (self.reg1 & 0x01)
2344        } else {
2345            self.reg0 & 0x0F
2346        }
2347    }
2348
2349    const fn chr_bank(&self) -> u8 {
2350        if (self.reg0 & 0x40) != 0 {
2351            ((self.reg0 << 2) & 0x38) | ((self.reg1 >> 4) & 0x07)
2352        } else {
2353            ((self.reg0 << 2) & 0x3C) | ((self.reg1 >> 4) & 0x03)
2354        }
2355    }
2356}
2357
2358impl Mapper for Maxi15M234 {
2359    fn caps(&self) -> MapperCaps {
2360        MapperCaps::NONE
2361    }
2362
2363    fn cpu_read(&mut self, addr: u16) -> u8 {
2364        if (0x8000..=0xFFFF).contains(&addr) {
2365            let count = (self.prg_rom.len() / PRG_BANK_32K).max(1);
2366            let bank = (self.prg_bank() as usize) % count;
2367            let value = self.prg_rom[bank * PRG_BANK_32K + (addr as usize - 0x8000)];
2368            // The latch windows live in the readable PRG space; reading them
2369            // triggers the same latch as a write (with the data the CPU sees).
2370            self.latch_access(addr, value);
2371            value
2372        } else {
2373            0
2374        }
2375    }
2376
2377    fn cpu_write(&mut self, addr: u16, value: u8) {
2378        if (0x8000..=0xFFFF).contains(&addr) {
2379            self.latch_access(addr, value);
2380        }
2381    }
2382
2383    fn ppu_read(&mut self, addr: u16) -> u8 {
2384        let addr = addr & 0x3FFF;
2385        match addr {
2386            0x0000..=0x1FFF => {
2387                let count = (self.chr_rom.len() / CHR_BANK_8K).max(1);
2388                let bank = (self.chr_bank() as usize) % count;
2389                self.chr_rom[bank * CHR_BANK_8K + addr as usize]
2390            }
2391            0x2000..=0x3EFF => self.vram[nametable_offset(addr, self.current_mirroring())],
2392            _ => 0,
2393        }
2394    }
2395
2396    fn ppu_write(&mut self, addr: u16, value: u8) {
2397        let addr = addr & 0x3FFF;
2398        if let 0x2000..=0x3EFF = addr {
2399            let off = nametable_offset(addr, self.current_mirroring());
2400            self.vram[off] = value;
2401        }
2402    }
2403
2404    fn current_mirroring(&self) -> Mirroring {
2405        if (self.reg0 & 0x80) != 0 {
2406            Mirroring::Horizontal
2407        } else {
2408            Mirroring::Vertical
2409        }
2410    }
2411
2412    fn save_state(&self) -> Vec<u8> {
2413        let mut out = Vec::with_capacity(3 + self.vram.len());
2414        out.push(SAVE_STATE_VERSION);
2415        out.push(self.reg0);
2416        out.push(self.reg1);
2417        out.extend_from_slice(&self.vram);
2418        out
2419    }
2420
2421    fn load_state(&mut self, data: &[u8]) -> Result<(), MapperError> {
2422        let expected = 3 + self.vram.len();
2423        if data.len() != expected {
2424            return Err(MapperError::WrongLength {
2425                expected,
2426                got: data.len(),
2427            });
2428        }
2429        if data[0] != SAVE_STATE_VERSION {
2430            return Err(MapperError::UnsupportedVersion(data[0]));
2431        }
2432        self.reg0 = data[1];
2433        self.reg1 = data[2];
2434        self.vram.copy_from_slice(&data[3..3 + self.vram.len()]);
2435        Ok(())
2436    }
2437}
2438
2439/// Mapper 225 (`ColorDreams` `72-in-1`).
2440pub struct Multicart225 {
2441    prg_rom: Box<[u8]>,
2442    chr_rom: Box<[u8]>,
2443    vram: Box<[u8]>,
2444    /// $5800-$5FFF 4-nibble scratch RAM (4 bytes, mirrored).
2445    scratch: [u8; 4],
2446    prg_bank: u8,
2447    prg16_mode: bool,
2448    chr_bank: u8,
2449    horizontal_mirroring: bool,
2450}
2451
2452impl Multicart225 {
2453    /// Construct a new mapper 225 board.
2454    ///
2455    /// # Errors
2456    ///
2457    /// Returns [`MapperError::Invalid`] when PRG is not a non-zero multiple of
2458    /// 16 KiB or CHR-ROM is empty / not a multiple of 8 KiB.
2459    pub fn new(
2460        prg_rom: Box<[u8]>,
2461        chr_rom: Box<[u8]>,
2462        _mirroring: Mirroring,
2463    ) -> Result<Self, MapperError> {
2464        if prg_rom.is_empty() || !prg_rom.len().is_multiple_of(PRG_BANK_16K) {
2465            return Err(MapperError::Invalid(format!(
2466                "mapper 225 PRG-ROM size {} is not a non-zero multiple of 16 KiB",
2467                prg_rom.len()
2468            )));
2469        }
2470        if chr_rom.is_empty() || !chr_rom.len().is_multiple_of(CHR_BANK_8K) {
2471            return Err(MapperError::Invalid(format!(
2472                "mapper 225 CHR-ROM size {} is not a non-zero multiple of 8 KiB",
2473                chr_rom.len()
2474            )));
2475        }
2476        Ok(Self {
2477            prg_rom,
2478            chr_rom,
2479            vram: vec![0u8; 2 * NAMETABLE_SIZE].into_boxed_slice(),
2480            scratch: [0; 4],
2481            prg_bank: 0,
2482            prg16_mode: false,
2483            chr_bank: 0,
2484            horizontal_mirroring: false,
2485        })
2486    }
2487
2488    fn read_prg(&self, bank16: usize, addr: u16) -> u8 {
2489        let count = (self.prg_rom.len() / PRG_BANK_16K).max(1);
2490        let bank = bank16 % count;
2491        self.prg_rom[bank * PRG_BANK_16K + (addr as usize & 0x3FFF)]
2492    }
2493}
2494
2495impl Mapper for Multicart225 {
2496    fn caps(&self) -> MapperCaps {
2497        MapperCaps::NONE
2498    }
2499
2500    // The scratch RAM answers reads in $5800-$5FFF (mapped). The rest of
2501    // $4020-$57FF stays open bus. PRG-ROM is mapped at $8000-$FFFF only
2502    // (nesdev_wiki/INES_Mapper_225): `cpu_read` has no $6000-$7FFF arm and
2503    // nothing drives it, so it floats. (Until v2.7.2 this comment said
2504    // "$6000-$FFFF PRG is mapped", which the code never implemented.)
2505    fn cpu_read_unmapped(&self, addr: u16) -> bool {
2506        // v2.7.2 (core audit §5.5): with no save RAM, `$6000-$7FFF` floats.
2507        (0x4020..=0x57FF).contains(&addr)
2508            || (matches!(addr, 0x6000..=0x7FFF) && self.sram().is_empty())
2509    }
2510
2511    fn cpu_read(&mut self, addr: u16) -> u8 {
2512        match addr {
2513            0x5800..=0x5FFF => self.scratch[(addr & 0x03) as usize] & 0x0F,
2514            0x8000..=0xBFFF => {
2515                let base = if self.prg16_mode {
2516                    self.prg_bank as usize
2517                } else {
2518                    (self.prg_bank as usize) & !1
2519                };
2520                self.read_prg(base, addr)
2521            }
2522            0xC000..=0xFFFF => {
2523                let bank = if self.prg16_mode {
2524                    self.prg_bank as usize
2525                } else {
2526                    ((self.prg_bank as usize) & !1) | 1
2527                };
2528                self.read_prg(bank, addr)
2529            }
2530            _ => 0,
2531        }
2532    }
2533
2534    fn cpu_write(&mut self, addr: u16, value: u8) {
2535        match addr {
2536            0x5800..=0x5FFF => self.scratch[(addr & 0x03) as usize] = value & 0x0F,
2537            0x8000..=0xFFFF => {
2538                // nesdev iNES 225: the bank/mode are in the ADDRESS bits
2539                // A~[.HMO PPPP PPCC CCCC]: CHR = A0..A5 (6 bits), PRG = A6..A11
2540                // (6 bits), O (PRG mode) = A12 (1 = 16 KiB switchable,
2541                // 0 = 32 KiB), M (mirroring) = A13 (1 = horizontal), H (outer
2542                // high bit for both PRG and CHR) = A14.
2543                let high = ((addr >> 14) & 0x01) as u8;
2544                self.prg16_mode = (addr & 0x1000) != 0;
2545                self.prg_bank = (high << 6) | (((addr >> 6) & 0x3F) as u8);
2546                self.chr_bank = (high << 6) | ((addr & 0x3F) as u8);
2547                self.horizontal_mirroring = (addr & 0x2000) != 0;
2548            }
2549            _ => {}
2550        }
2551    }
2552
2553    fn ppu_read(&mut self, addr: u16) -> u8 {
2554        let addr = addr & 0x3FFF;
2555        match addr {
2556            0x0000..=0x1FFF => {
2557                let count = (self.chr_rom.len() / CHR_BANK_8K).max(1);
2558                let bank = (self.chr_bank as usize) % count;
2559                self.chr_rom[bank * CHR_BANK_8K + addr as usize]
2560            }
2561            0x2000..=0x3EFF => self.vram[nametable_offset(addr, self.current_mirroring())],
2562            _ => 0,
2563        }
2564    }
2565
2566    fn ppu_write(&mut self, addr: u16, value: u8) {
2567        let addr = addr & 0x3FFF;
2568        if let 0x2000..=0x3EFF = addr {
2569            let off = nametable_offset(addr, self.current_mirroring());
2570            self.vram[off] = value;
2571        }
2572    }
2573
2574    fn current_mirroring(&self) -> Mirroring {
2575        if self.horizontal_mirroring {
2576            Mirroring::Horizontal
2577        } else {
2578            Mirroring::Vertical
2579        }
2580    }
2581
2582    fn save_state(&self) -> Vec<u8> {
2583        let mut out = Vec::with_capacity(4 + 4 + self.vram.len());
2584        out.push(SAVE_STATE_VERSION);
2585        out.push(self.prg_bank);
2586        out.push(u8::from(self.prg16_mode));
2587        out.push(self.chr_bank);
2588        out.push(u8::from(self.horizontal_mirroring));
2589        out.extend_from_slice(&self.scratch);
2590        out.extend_from_slice(&self.vram);
2591        out
2592    }
2593
2594    fn load_state(&mut self, data: &[u8]) -> Result<(), MapperError> {
2595        let expected = 5 + 4 + self.vram.len();
2596        if data.len() != expected {
2597            return Err(MapperError::WrongLength {
2598                expected,
2599                got: data.len(),
2600            });
2601        }
2602        if data[0] != SAVE_STATE_VERSION {
2603            return Err(MapperError::UnsupportedVersion(data[0]));
2604        }
2605        self.prg_bank = data[1];
2606        self.prg16_mode = data[2] != 0;
2607        self.chr_bank = data[3];
2608        self.horizontal_mirroring = data[4] != 0;
2609        self.scratch.copy_from_slice(&data[5..9]);
2610        self.vram.copy_from_slice(&data[9..9 + self.vram.len()]);
2611        Ok(())
2612    }
2613}
2614
2615// ===========================================================================
2616// Mapper 226 — 76-in-1 BMC.
2617//
2618// Two latch registers across $8000-$FFFF (mask $8001: the low address bit
2619// selects reg0 vs reg1; NESdev `INES_Mapper_226`):
2620//   reg0 ($8000, even): [PMOP PPPP] -- bits 4-0 = PRG bits 4-0, bit 5 = PRG
2621//        mode (O: 0 = one 32 KiB bank, 1 = the same 16 KiB bank in both
2622//        halves), bit 6 = mirroring (M: 0 = horizontal, 1 = vertical), bit 7
2623//        = PRG bit 5.
2624//   reg1 ($8001, odd): bit 0 = PRG bit 6 (outer block).
2625// The 7-bit PRG register = (reg1.bit0 << 6) | (reg0.bit7 << 5) | (reg0 & 0x1F);
2626// in 32 KiB mode its low bit is ignored. CHR is 8 KiB RAM. No IRQ.
2627//
2628// Before v2.9.8 bits 5-7 were decoded as PRG bit 5 / mode / mirroring, which
2629// sent the 76-in-1 menu to the wrong bank and left it drawing one tile.
2630// ===========================================================================
2631
2632/// Mapper 226 (`76-in-1` BMC).
2633pub struct Multicart226 {
2634    prg_rom: Box<[u8]>,
2635    chr_ram: Box<[u8]>,
2636    vram: Box<[u8]>,
2637    reg0: u8,
2638    reg1: u8,
2639}
2640
2641impl Multicart226 {
2642    /// Construct a new mapper 226 board.
2643    ///
2644    /// # Errors
2645    ///
2646    /// Returns [`MapperError::Invalid`] when PRG is not a non-zero multiple of
2647    /// 16 KiB.
2648    pub fn new(
2649        prg_rom: Box<[u8]>,
2650        _chr_rom: &[u8],
2651        _mirroring: Mirroring,
2652    ) -> Result<Self, MapperError> {
2653        if prg_rom.is_empty() || !prg_rom.len().is_multiple_of(PRG_BANK_16K) {
2654            return Err(MapperError::Invalid(format!(
2655                "mapper 226 PRG-ROM size {} is not a non-zero multiple of 16 KiB",
2656                prg_rom.len()
2657            )));
2658        }
2659        Ok(Self {
2660            prg_rom,
2661            chr_ram: vec![0u8; CHR_BANK_8K].into_boxed_slice(),
2662            vram: vec![0u8; 2 * NAMETABLE_SIZE].into_boxed_slice(),
2663            reg0: 0,
2664            reg1: 0,
2665        })
2666    }
2667
2668    /// 7-bit 16 KiB PRG bank index: bits 4-0 from reg0 bits 4-0, bit 5 from
2669    /// reg0 bit 7, bit 6 from reg1 bit 0.
2670    const fn prg_bank(&self) -> usize {
2671        let low = (self.reg0 & 0x1F) as usize;
2672        let bit5 = ((self.reg0 >> 7) & 0x01) as usize;
2673        let high = (self.reg1 & 0x01) as usize;
2674        (high << 6) | (bit5 << 5) | low
2675    }
2676
2677    /// PRG mode: reg0 bit 5 set = the same 16 KiB bank in both halves; clear
2678    /// = one 32 KiB bank.
2679    const fn is_16k(&self) -> bool {
2680        (self.reg0 & 0x20) != 0
2681    }
2682
2683    fn read_prg(&self, bank16: usize, addr: u16) -> u8 {
2684        let count = (self.prg_rom.len() / PRG_BANK_16K).max(1);
2685        let bank = bank16 % count;
2686        self.prg_rom[bank * PRG_BANK_16K + (addr as usize & 0x3FFF)]
2687    }
2688}
2689
2690impl Mapper for Multicart226 {
2691    fn caps(&self) -> MapperCaps {
2692        MapperCaps::NONE
2693    }
2694
2695    fn cpu_read(&mut self, addr: u16) -> u8 {
2696        if (0x8000..=0xFFFF).contains(&addr) {
2697            let bank = self.prg_bank();
2698            if self.is_16k() {
2699                // Both 16 KiB halves map the same selected bank.
2700                self.read_prg(bank, addr)
2701            } else {
2702                // 32 KiB mode: the bank index addresses a 32 KiB page (its low
2703                // bit is ignored); the high half is +1.
2704                let base = bank & !1;
2705                let bank16 = base | usize::from(addr >= 0xC000);
2706                self.read_prg(bank16, addr)
2707            }
2708        } else {
2709            0
2710        }
2711    }
2712
2713    fn cpu_write(&mut self, addr: u16, value: u8) {
2714        match addr {
2715            0x8000..=0xFFFF if (addr & 0x01) == 0 => self.reg0 = value,
2716            0x8000..=0xFFFF => self.reg1 = value,
2717            _ => {}
2718        }
2719    }
2720
2721    fn ppu_read(&mut self, addr: u16) -> u8 {
2722        let addr = addr & 0x3FFF;
2723        match addr {
2724            0x0000..=0x1FFF => self.chr_ram[addr as usize],
2725            0x2000..=0x3EFF => self.vram[nametable_offset(addr, self.current_mirroring())],
2726            _ => 0,
2727        }
2728    }
2729
2730    fn ppu_write(&mut self, addr: u16, value: u8) {
2731        let addr = addr & 0x3FFF;
2732        match addr {
2733            0x0000..=0x1FFF => self.chr_ram[addr as usize] = value,
2734            0x2000..=0x3EFF => {
2735                let off = nametable_offset(addr, self.current_mirroring());
2736                self.vram[off] = value;
2737            }
2738            _ => {}
2739        }
2740    }
2741
2742    /// NESdev `INES_Mapper_226`: "The multicart clears both registers on soft
2743    /// reset", which is how the menu comes back on RESET.
2744    fn reset(&mut self) {
2745        self.reg0 = 0;
2746        self.reg1 = 0;
2747    }
2748
2749    fn current_mirroring(&self) -> Mirroring {
2750        // reg0 bit 6 (M): 0 = horizontal, 1 = vertical.
2751        if (self.reg0 & 0x40) != 0 {
2752            Mirroring::Vertical
2753        } else {
2754            Mirroring::Horizontal
2755        }
2756    }
2757
2758    fn save_state(&self) -> Vec<u8> {
2759        let mut out = Vec::with_capacity(3 + self.vram.len() + self.chr_ram.len());
2760        out.push(SAVE_STATE_VERSION);
2761        out.push(self.reg0);
2762        out.push(self.reg1);
2763        out.extend_from_slice(&self.vram);
2764        out.extend_from_slice(&self.chr_ram);
2765        out
2766    }
2767
2768    fn load_state(&mut self, data: &[u8]) -> Result<(), MapperError> {
2769        let expected = 3 + self.vram.len() + self.chr_ram.len();
2770        if data.len() != expected {
2771            return Err(MapperError::WrongLength {
2772                expected,
2773                got: data.len(),
2774            });
2775        }
2776        if data[0] != SAVE_STATE_VERSION {
2777            return Err(MapperError::UnsupportedVersion(data[0]));
2778        }
2779        self.reg0 = data[1];
2780        self.reg1 = data[2];
2781        let mut cursor = 3;
2782        self.vram
2783            .copy_from_slice(&data[cursor..cursor + self.vram.len()]);
2784        cursor += self.vram.len();
2785        self.chr_ram
2786            .copy_from_slice(&data[cursor..cursor + self.chr_ram.len()]);
2787        Ok(())
2788    }
2789}
2790
2791// ===========================================================================
2792// Mapper 227 — 1200-in-1 BMC.
2793//
2794// Address-decoded register across $8000-$FFFF (Mesen2 Mapper227). For the
2795// absolute write address A:
2796//   prg_bank = ((A >> 2) & 0x1F) | ((A & 0x100) >> 3)   (6-bit 16 KiB index)
2797//   s_flag   = (A & 0x01)        (set: restrict / half-select)
2798//   prg_mode = (A >> 7) & 0x01   (set: NROM modes; clear: UNROM-like)
2799//   l_flag   = (A >> 9) & 0x01   (set: fix $C000 to bank|0x07; clear: &0x38)
2800//   mirroring = (A & 0x02) -> 1 = horizontal, 0 = vertical
2801// The two $8000/$C000 16 KiB windows are then composed per the Mesen2 mode
2802// table. The old decode read bit 0 as a 32 KiB mode, mis-applied bit 7, and
2803// IGNORED bit 9, so the fixed $C000 window pointed at the wrong bank and the
2804// multicart menu never drew. CHR is 8 KiB RAM. No IRQ.
2805// ===========================================================================
2806
2807/// Mapper 227 (`1200-in-1` BMC).
2808#[allow(clippy::struct_excessive_bools)] // 4 independent decoded register flags
2809pub struct Multicart227 {
2810    prg_rom: Box<[u8]>,
2811    chr_ram: Box<[u8]>,
2812    vram: Box<[u8]>,
2813    prg_bank: u8,
2814    s_flag: bool,
2815    l_flag: bool,
2816    prg_mode: bool,
2817    horizontal_mirroring: bool,
2818    /// The FW-01 variant "adds 8 KiB battery-backed WRAM"
2819    /// (`nesdev_wiki/INES_Mapper_227.xhtml`); the 1200-in-1 multicarts have
2820    /// none. Allocated only when the header says battery (v2.7.2), so a
2821    /// multicart keeps an empty window, which floats.
2822    wram: Box<[u8]>,
2823}
2824
2825impl Multicart227 {
2826    /// Construct a new mapper 227 board.
2827    ///
2828    /// # Errors
2829    ///
2830    /// Returns [`MapperError::Invalid`] when PRG is not a non-zero multiple of
2831    /// 16 KiB.
2832    pub fn new(
2833        prg_rom: Box<[u8]>,
2834        _chr_rom: &[u8],
2835        _mirroring: Mirroring,
2836    ) -> Result<Self, MapperError> {
2837        if prg_rom.is_empty() || !prg_rom.len().is_multiple_of(PRG_BANK_16K) {
2838            return Err(MapperError::Invalid(format!(
2839                "mapper 227 PRG-ROM size {} is not a non-zero multiple of 16 KiB",
2840                prg_rom.len()
2841            )));
2842        }
2843        Ok(Self {
2844            prg_rom,
2845            chr_ram: vec![0u8; CHR_BANK_8K].into_boxed_slice(),
2846            vram: vec![0u8; 2 * NAMETABLE_SIZE].into_boxed_slice(),
2847            prg_bank: 0,
2848            s_flag: false,
2849            l_flag: false,
2850            prg_mode: false,
2851            horizontal_mirroring: false,
2852            wram: Box::new([]),
2853        })
2854    }
2855
2856    /// Give the board its battery-backed WRAM when the header says battery
2857    /// (the FW-01 variant).
2858    #[must_use]
2859    pub fn with_battery(mut self, battery: bool) -> Self {
2860        self.wram = if battery {
2861            vec![0u8; 0x2000].into_boxed_slice()
2862        } else {
2863            Box::new([])
2864        };
2865        self
2866    }
2867
2868    fn read_prg(&self, bank16: usize, addr: u16) -> u8 {
2869        let count = (self.prg_rom.len() / PRG_BANK_16K).max(1);
2870        let bank = bank16 % count;
2871        self.prg_rom[bank * PRG_BANK_16K + (addr as usize & 0x3FFF)]
2872    }
2873
2874    /// Compose the ($8000, $C000) 16 KiB bank pair from the decoded flags,
2875    /// matching Mesen2 `Mapper227::WriteRegister`.
2876    const fn prg_pages(&self) -> (usize, usize) {
2877        let b = self.prg_bank as usize;
2878        if self.prg_mode {
2879            if self.s_flag {
2880                (b & 0xFE, (b & 0xFE) | 1) // 32 KiB pair
2881            } else {
2882                (b, b) // NROM-128 (16 KiB mirrored)
2883            }
2884        } else {
2885            let lo = if self.s_flag { b & 0x3E } else { b };
2886            let hi = if self.l_flag { b | 0x07 } else { b & 0x38 };
2887            (lo, hi)
2888        }
2889    }
2890}
2891
2892impl Mapper for Multicart227 {
2893    fn caps(&self) -> MapperCaps {
2894        MapperCaps::NONE
2895    }
2896
2897    fn sram(&self) -> &[u8] {
2898        &self.wram
2899    }
2900    fn sram_mut(&mut self) -> &mut [u8] {
2901        &mut self.wram
2902    }
2903
2904    fn cpu_read(&mut self, addr: u16) -> u8 {
2905        let (p0, p1) = self.prg_pages();
2906        match addr {
2907            0x6000..=0x7FFF if !self.wram.is_empty() => self.wram[usize::from(addr - 0x6000)],
2908            0x8000..=0xBFFF => self.read_prg(p0, addr),
2909            0xC000..=0xFFFF => self.read_prg(p1, addr),
2910            _ => 0,
2911        }
2912    }
2913
2914    fn cpu_write(&mut self, addr: u16, value: u8) {
2915        if (0x6000..=0x7FFF).contains(&addr) && !self.wram.is_empty() {
2916            self.wram[usize::from(addr - 0x6000)] = value;
2917            return;
2918        }
2919        if (0x8000..=0xFFFF).contains(&addr) {
2920            let low = ((addr >> 2) & 0x1F) as u8;
2921            let high = ((addr & 0x100) >> 3) as u8; // bit 8 -> bit 5 (0x20)
2922            self.prg_bank = low | high;
2923            self.s_flag = (addr & 0x01) != 0;
2924            self.prg_mode = ((addr >> 7) & 0x01) != 0;
2925            self.l_flag = ((addr >> 9) & 0x01) != 0;
2926            self.horizontal_mirroring = (addr & 0x02) != 0;
2927        }
2928    }
2929
2930    fn ppu_read(&mut self, addr: u16) -> u8 {
2931        let addr = addr & 0x3FFF;
2932        match addr {
2933            0x0000..=0x1FFF => self.chr_ram[addr as usize],
2934            0x2000..=0x3EFF => self.vram[nametable_offset(addr, self.current_mirroring())],
2935            _ => 0,
2936        }
2937    }
2938
2939    fn ppu_write(&mut self, addr: u16, value: u8) {
2940        let addr = addr & 0x3FFF;
2941        match addr {
2942            0x0000..=0x1FFF => self.chr_ram[addr as usize] = value,
2943            0x2000..=0x3EFF => {
2944                let off = nametable_offset(addr, self.current_mirroring());
2945                self.vram[off] = value;
2946            }
2947            _ => {}
2948        }
2949    }
2950
2951    fn current_mirroring(&self) -> Mirroring {
2952        if self.horizontal_mirroring {
2953            Mirroring::Horizontal
2954        } else {
2955            Mirroring::Vertical
2956        }
2957    }
2958
2959    fn save_state(&self) -> Vec<u8> {
2960        let mut out = Vec::with_capacity(6 + self.vram.len() + self.chr_ram.len());
2961        out.push(SAVE_STATE_VERSION);
2962        out.push(self.prg_bank);
2963        out.push(u8::from(self.s_flag));
2964        out.push(u8::from(self.l_flag));
2965        out.push(u8::from(self.prg_mode));
2966        out.push(u8::from(self.horizontal_mirroring));
2967        out.extend_from_slice(&self.vram);
2968        out.extend_from_slice(&self.chr_ram);
2969        // v2.7.2: the FW-01 WRAM, when present, as a trailing block. Its
2970        // length is fixed by the header.
2971        out.extend_from_slice(&self.wram);
2972        out
2973    }
2974
2975    fn load_state(&mut self, data: &[u8]) -> Result<(), MapperError> {
2976        // This board's blob shares the file-wide `SAVE_STATE_VERSION`, so a
2977        // pre-v2.7.2 blob (no WRAM) was told apart by length and loaded with
2978        // WRAM zeroed. Since v2.9.8 (ADR 0042) only the full layout loads. A
2979        // board built without WRAM (no battery header) has `wram.len() == 0`.
2980        let expected = 6 + self.vram.len() + self.chr_ram.len() + self.wram.len();
2981        if data.len() != expected {
2982            return Err(MapperError::WrongLength {
2983                expected,
2984                got: data.len(),
2985            });
2986        }
2987        if data[0] != SAVE_STATE_VERSION {
2988            return Err(MapperError::UnsupportedVersion(data[0]));
2989        }
2990        self.prg_bank = data[1];
2991        self.s_flag = data[2] != 0;
2992        self.l_flag = data[3] != 0;
2993        self.prg_mode = data[4] != 0;
2994        self.horizontal_mirroring = data[5] != 0;
2995        let mut cursor = 6;
2996        self.vram
2997            .copy_from_slice(&data[cursor..cursor + self.vram.len()]);
2998        cursor += self.vram.len();
2999        self.chr_ram
3000            .copy_from_slice(&data[cursor..cursor + self.chr_ram.len()]);
3001        cursor += self.chr_ram.len();
3002        self.wram.copy_from_slice(&data[cursor..]);
3003        Ok(())
3004    }
3005}
3006
3007// ===========================================================================
3008// Mapper 229 — 31-in-1 BMC.
3009//
3010// Address-decoded register across $8000-$FFFF. For the absolute address A:
3011//   When (A & 0x1E) == 0: a fixed 32 KiB NROM bank 0 (the menu).
3012//   Otherwise: a 16 KiB PRG bank pair = (A & 0x1F) on both $8000 and $C000?
3013//   The documented decode: $8000 = (A & 0x1F), $C000 = (A & 0x1F) (16 KiB,
3014//   same bank), CHR (8 KiB) bank = A & 0x0F, mirroring = (A & 0x20).
3015// CHR is ROM. No IRQ.
3016// ===========================================================================
3017
3018/// Mapper 229 (`31-in-1` BMC).
3019pub struct Multicart229 {
3020    prg_rom: Box<[u8]>,
3021    chr_rom: Box<[u8]>,
3022    vram: Box<[u8]>,
3023    /// Latched absolute address bits (low 6) used by the decode.
3024    addr_latch: u8,
3025    chr_bank: u8,
3026    horizontal_mirroring: bool,
3027}
3028
3029impl Multicart229 {
3030    /// Construct a new mapper 229 board.
3031    ///
3032    /// # Errors
3033    ///
3034    /// Returns [`MapperError::Invalid`] when PRG is not a non-zero multiple of
3035    /// 16 KiB or CHR-ROM is empty / not a multiple of 8 KiB.
3036    pub fn new(
3037        prg_rom: Box<[u8]>,
3038        chr_rom: Box<[u8]>,
3039        _mirroring: Mirroring,
3040    ) -> Result<Self, MapperError> {
3041        if prg_rom.is_empty() || !prg_rom.len().is_multiple_of(PRG_BANK_16K) {
3042            return Err(MapperError::Invalid(format!(
3043                "mapper 229 PRG-ROM size {} is not a non-zero multiple of 16 KiB",
3044                prg_rom.len()
3045            )));
3046        }
3047        if chr_rom.is_empty() || !chr_rom.len().is_multiple_of(CHR_BANK_8K) {
3048            return Err(MapperError::Invalid(format!(
3049                "mapper 229 CHR-ROM size {} is not a non-zero multiple of 8 KiB",
3050                chr_rom.len()
3051            )));
3052        }
3053        Ok(Self {
3054            prg_rom,
3055            chr_rom,
3056            vram: vec![0u8; 2 * NAMETABLE_SIZE].into_boxed_slice(),
3057            addr_latch: 0,
3058            chr_bank: 0,
3059            horizontal_mirroring: false,
3060        })
3061    }
3062
3063    /// True when the latched address selects the fixed 32 KiB NROM menu bank.
3064    const fn is_menu(&self) -> bool {
3065        (self.addr_latch & 0x1E) == 0
3066    }
3067
3068    fn read_prg(&self, bank16: usize, addr: u16) -> u8 {
3069        let count = (self.prg_rom.len() / PRG_BANK_16K).max(1);
3070        let bank = bank16 % count;
3071        self.prg_rom[bank * PRG_BANK_16K + (addr as usize & 0x3FFF)]
3072    }
3073}
3074
3075impl Mapper for Multicart229 {
3076    fn caps(&self) -> MapperCaps {
3077        MapperCaps::NONE
3078    }
3079
3080    fn cpu_read(&mut self, addr: u16) -> u8 {
3081        if !(0x8000..=0xFFFF).contains(&addr) {
3082            return 0;
3083        }
3084        if self.is_menu() {
3085            // Fixed 32 KiB NROM bank 0.
3086            let bank16 = usize::from(addr >= 0xC000);
3087            self.read_prg(bank16, addr)
3088        } else {
3089            // 16 KiB bank from the latch, mirrored across both halves.
3090            let bank = (self.addr_latch & 0x1F) as usize;
3091            self.read_prg(bank, addr)
3092        }
3093    }
3094
3095    fn cpu_write(&mut self, addr: u16, _value: u8) {
3096        if (0x8000..=0xFFFF).contains(&addr) {
3097            self.addr_latch = (addr & 0x3F) as u8;
3098            self.chr_bank = (addr & 0x0F) as u8;
3099            self.horizontal_mirroring = (addr & 0x20) != 0;
3100        }
3101    }
3102
3103    fn ppu_read(&mut self, addr: u16) -> u8 {
3104        let addr = addr & 0x3FFF;
3105        match addr {
3106            0x0000..=0x1FFF => {
3107                let count = (self.chr_rom.len() / CHR_BANK_8K).max(1);
3108                let bank = (self.chr_bank as usize) % count;
3109                self.chr_rom[bank * CHR_BANK_8K + addr as usize]
3110            }
3111            0x2000..=0x3EFF => self.vram[nametable_offset(addr, self.current_mirroring())],
3112            _ => 0,
3113        }
3114    }
3115
3116    fn ppu_write(&mut self, addr: u16, value: u8) {
3117        let addr = addr & 0x3FFF;
3118        if let 0x2000..=0x3EFF = addr {
3119            let off = nametable_offset(addr, self.current_mirroring());
3120            self.vram[off] = value;
3121        }
3122    }
3123
3124    fn current_mirroring(&self) -> Mirroring {
3125        if self.horizontal_mirroring {
3126            Mirroring::Horizontal
3127        } else {
3128            Mirroring::Vertical
3129        }
3130    }
3131
3132    fn save_state(&self) -> Vec<u8> {
3133        let mut out = Vec::with_capacity(4 + self.vram.len());
3134        out.push(SAVE_STATE_VERSION);
3135        out.push(self.addr_latch);
3136        out.push(self.chr_bank);
3137        out.push(u8::from(self.horizontal_mirroring));
3138        out.extend_from_slice(&self.vram);
3139        out
3140    }
3141
3142    fn load_state(&mut self, data: &[u8]) -> Result<(), MapperError> {
3143        let expected = 4 + self.vram.len();
3144        if data.len() != expected {
3145            return Err(MapperError::WrongLength {
3146                expected,
3147                got: data.len(),
3148            });
3149        }
3150        if data[0] != SAVE_STATE_VERSION {
3151            return Err(MapperError::UnsupportedVersion(data[0]));
3152        }
3153        self.addr_latch = data[1];
3154        self.chr_bank = data[2];
3155        self.horizontal_mirroring = data[3] != 0;
3156        self.vram.copy_from_slice(&data[4..4 + self.vram.len()]);
3157        Ok(())
3158    }
3159}
3160
3161// ===========================================================================
3162// Mapper 233 — 42-in-1 reset-based BMC.
3163//
3164// Address-decoded register across $8000-$FFFF. For the absolute address A:
3165//   PRG: bits 0-4 select a 16 KiB bank; bit 5 picks 32/16 KiB mode.
3166//   mirroring: bits 6-7 -> 0 = one-screen A, 1 = one-screen B, 2 = vertical,
3167//              3 = horizontal.
3168// A reset toggles a separate "outer block" line that selects the upper or lower
3169// half of the ROM; that line is host-driven (the physical reset button), so we
3170// model it as a fixed power-on `0`. CHR is 8 KiB RAM. No IRQ.
3171// ===========================================================================
3172
3173/// Mapper 233 (`42-in-1` reset-based BMC).
3174pub struct Multicart233 {
3175    prg_rom: Box<[u8]>,
3176    chr_ram: Box<[u8]>,
3177    vram: Box<[u8]>,
3178    /// Reset-selected outer block (host-driven; fixed at power-on).
3179    outer_block: u8,
3180    prg_bank: u8,
3181    /// reg bit 5: set = 16 KiB mode (bank mirrored to both halves), clear =
3182    /// 32 KiB mode (the pair at bank>>1). puNES `prg_fix_233`.
3183    mode_16k: bool,
3184    mirror_mode: u8,
3185}
3186
3187impl Multicart233 {
3188    /// Construct a new mapper 233 board.
3189    ///
3190    /// # Errors
3191    ///
3192    /// Returns [`MapperError::Invalid`] when PRG is not a non-zero multiple of
3193    /// 16 KiB.
3194    pub fn new(
3195        prg_rom: Box<[u8]>,
3196        _chr_rom: &[u8],
3197        _mirroring: Mirroring,
3198    ) -> Result<Self, MapperError> {
3199        if prg_rom.is_empty() || !prg_rom.len().is_multiple_of(PRG_BANK_16K) {
3200            return Err(MapperError::Invalid(format!(
3201                "mapper 233 PRG-ROM size {} is not a non-zero multiple of 16 KiB",
3202                prg_rom.len()
3203            )));
3204        }
3205        Ok(Self {
3206            prg_rom,
3207            chr_ram: vec![0u8; CHR_BANK_8K].into_boxed_slice(),
3208            vram: vec![0u8; 2 * NAMETABLE_SIZE].into_boxed_slice(),
3209            outer_block: 0,
3210            prg_bank: 0,
3211            mode_16k: false,
3212            mirror_mode: 0,
3213        })
3214    }
3215
3216    fn read_prg(&self, bank16: usize, addr: u16) -> u8 {
3217        let count = (self.prg_rom.len() / PRG_BANK_16K).max(1);
3218        let bank = bank16 % count;
3219        self.prg_rom[bank * PRG_BANK_16K + (addr as usize & 0x3FFF)]
3220    }
3221}
3222
3223impl Mapper for Multicart233 {
3224    fn caps(&self) -> MapperCaps {
3225        MapperCaps::NONE
3226    }
3227
3228    fn cpu_read(&mut self, addr: u16) -> u8 {
3229        // puNES prg_fix_233: bank = (reg & 0x1F) | reset. reg bit 5 set =>
3230        // 16 KiB mode (the SAME bank mirrored to both halves); clear => 32 KiB
3231        // mode (the pair at bank>>1). The previous code had the mode bit
3232        // inverted (treating bit-5-set as 32 KiB), so the menu's expected bank
3233        // never mapped and the multicart booted blank.
3234        let bank16 = (self.prg_bank as usize) | ((self.outer_block as usize) << 5);
3235        match addr {
3236            0x8000..=0xBFFF => {
3237                let b = if self.mode_16k { bank16 } else { bank16 & !1 };
3238                self.read_prg(b, addr)
3239            }
3240            0xC000..=0xFFFF => {
3241                let b = if self.mode_16k {
3242                    bank16
3243                } else {
3244                    (bank16 & !1) | 1
3245                };
3246                self.read_prg(b, addr)
3247            }
3248            _ => 0,
3249        }
3250    }
3251
3252    fn cpu_write(&mut self, _addr: u16, value: u8) {
3253        // puNES iNES 233: a $8000-$FFFF write carries the full register in the
3254        // DATA byte. PPPPP (bits 0-4) = PRG page (combined with the reset outer
3255        // line), bit 5 = 16 KiB mode select (set = 16 KiB mirrored both halves,
3256        // clear = 32 KiB pair), MM (bits 6-7) = mirroring (0 = 1-screen A,
3257        // 1 = vertical, 2 = horizontal, 3 = 1-screen B).
3258        self.prg_bank = value & 0x1F;
3259        self.mode_16k = (value & 0x20) != 0;
3260        self.mirror_mode = (value >> 6) & 0x03;
3261    }
3262
3263    fn ppu_read(&mut self, addr: u16) -> u8 {
3264        let addr = addr & 0x3FFF;
3265        match addr {
3266            0x0000..=0x1FFF => self.chr_ram[addr as usize],
3267            0x2000..=0x3EFF => self.vram[nametable_offset(addr, self.current_mirroring())],
3268            _ => 0,
3269        }
3270    }
3271
3272    fn ppu_write(&mut self, addr: u16, value: u8) {
3273        let addr = addr & 0x3FFF;
3274        match addr {
3275            0x0000..=0x1FFF => self.chr_ram[addr as usize] = value,
3276            0x2000..=0x3EFF => {
3277                let off = nametable_offset(addr, self.current_mirroring());
3278                self.vram[off] = value;
3279            }
3280            _ => {}
3281        }
3282    }
3283
3284    fn current_mirroring(&self) -> Mirroring {
3285        match self.mirror_mode {
3286            0 => Mirroring::SingleScreenA,
3287            1 => Mirroring::Vertical,
3288            2 => Mirroring::Horizontal,
3289            _ => Mirroring::SingleScreenB,
3290        }
3291    }
3292
3293    fn save_state(&self) -> Vec<u8> {
3294        let mut out = Vec::with_capacity(5 + self.vram.len() + self.chr_ram.len());
3295        out.push(SAVE_STATE_VERSION);
3296        out.push(self.outer_block);
3297        out.push(self.prg_bank);
3298        out.push(u8::from(self.mode_16k));
3299        out.push(self.mirror_mode);
3300        out.extend_from_slice(&self.vram);
3301        out.extend_from_slice(&self.chr_ram);
3302        out
3303    }
3304
3305    fn load_state(&mut self, data: &[u8]) -> Result<(), MapperError> {
3306        let expected = 5 + self.vram.len() + self.chr_ram.len();
3307        if data.len() != expected {
3308            return Err(MapperError::WrongLength {
3309                expected,
3310                got: data.len(),
3311            });
3312        }
3313        if data[0] != SAVE_STATE_VERSION {
3314            return Err(MapperError::UnsupportedVersion(data[0]));
3315        }
3316        self.outer_block = data[1];
3317        self.prg_bank = data[2];
3318        self.mode_16k = data[3] != 0;
3319        // Mask to the valid 0..=3 range so a corrupt / hand-edited save state
3320        // can never produce an out-of-range mirroring mode (adopted from the
3321        // PR #116 Gemini review).
3322        self.mirror_mode = data[4] & 0x03;
3323        let mut cursor = 5;
3324        self.vram
3325            .copy_from_slice(&data[cursor..cursor + self.vram.len()]);
3326        cursor += self.vram.len();
3327        self.chr_ram
3328            .copy_from_slice(&data[cursor..cursor + self.chr_ram.len()]);
3329        Ok(())
3330    }
3331}
3332
3333/// Which discrete board the [`DiscreteMapper`] models.
3334#[derive(Debug, Clone, Copy, PartialEq, Eq)]
3335pub enum DiscreteBoard {
3336    /// Mapper 46 (Color Dreams "Rumble Station" 15-in-1).
3337    M46,
3338    /// Mapper 51 (BMC 11-in-1).
3339    M51,
3340    /// Mapper 57 (BMC GK 6-in-1).
3341    M57,
3342    /// Mapper 104 (Codemasters Golden Five / Pegasus 5-in-1).
3343    M104,
3344    /// Mapper 120 (Tobidase Daisakusen FDS-conversion protection).
3345    M120,
3346    /// Mapper 290 (NTDEC Asder BMC-NTD-03).
3347    M290,
3348    /// Mapper 301 (BMC-8157 address-as-data multicart).
3349    M301,
3350}
3351
3352/// A discrete unlicensed/multicart board with a simple register surface.
3353pub struct DiscreteMapper {
3354    board: DiscreteBoard,
3355    prg_rom: Box<[u8]>,
3356    chr: Box<[u8]>,
3357    chr_is_ram: bool,
3358    vram: Box<[u8]>,
3359    reg0: u8,
3360    reg1: u8,
3361    /// For 301 (address-as-data) the last-written address.
3362    last_addr: u16,
3363    mirroring: Mirroring,
3364}
3365
3366impl DiscreteMapper {
3367    fn new(
3368        board: DiscreteBoard,
3369        prg_rom: Box<[u8]>,
3370        chr_rom: Box<[u8]>,
3371        mirroring: Mirroring,
3372        id: u16,
3373    ) -> Result<Self, MapperError> {
3374        if prg_rom.is_empty() || !prg_rom.len().is_multiple_of(PRG_BANK_8K) {
3375            return Err(MapperError::Invalid(format!(
3376                "mapper {id} PRG-ROM size {} is not a non-zero multiple of 8 KiB",
3377                prg_rom.len()
3378            )));
3379        }
3380        let chr_is_ram = chr_rom.is_empty();
3381        let chr: Box<[u8]> = if chr_is_ram {
3382            vec![0u8; CHR_BANK_8K].into_boxed_slice()
3383        } else {
3384            chr_rom
3385        };
3386        Ok(Self {
3387            board,
3388            prg_rom,
3389            chr,
3390            chr_is_ram,
3391            vram: vec![0u8; 2 * NAMETABLE_SIZE].into_boxed_slice(),
3392            reg0: 0,
3393            reg1: 0,
3394            last_addr: 0,
3395            mirroring,
3396        })
3397    }
3398
3399    fn prg_16k(&self, bank: usize, addr: u16) -> u8 {
3400        let count = (self.prg_rom.len() / PRG_BANK_16K).max(1);
3401        let bank = bank % count;
3402        // v2.9.0 (review on #561): modulo the ROM length, as `prg_32k` does
3403        // since NC-01. The constructor accepts any multiple of 8 KiB, and an
3404        // 8 KiB image clamps `count` to 1 and leaves the window's upper half
3405        // past the end; an 8 KiB part mirrors there. The identity for every
3406        // image whose window fits.
3407        self.prg_rom[(bank * PRG_BANK_16K + (addr as usize & 0x3FFF)) % self.prg_rom.len()]
3408    }
3409
3410    fn prg_32k(&self, bank: usize, addr: u16) -> u8 {
3411        let count = (self.prg_rom.len() / PRG_BANK_32K).max(1);
3412        let bank = bank % count;
3413        // v2.9.0 (re-audit NC-01): modulo the ROM length. Mappers 46 and 57
3414        // accept a 16 KiB image, which clamps `count` to 1 and leaves bank 0's
3415        // window 16 KiB past the end; the part mirrors on hardware. The
3416        // identity for every image whose window fits.
3417        self.prg_rom[(bank * PRG_BANK_32K + (addr as usize & 0x7FFF)) % self.prg_rom.len()]
3418    }
3419
3420    fn prg_8k(&self, bank: usize, addr: u16) -> u8 {
3421        let count = (self.prg_rom.len() / PRG_BANK_8K).max(1);
3422        let bank = bank % count;
3423        self.prg_rom[bank * PRG_BANK_8K + (addr as usize & 0x1FFF)]
3424    }
3425
3426    fn chr_8k_bank(&self) -> usize {
3427        match self.board {
3428            DiscreteBoard::M46 => {
3429                ((self.reg0 as usize & 0xF0) >> 1) | ((self.reg1 as usize & 0x70) >> 4)
3430            }
3431            DiscreteBoard::M57 => {
3432                ((self.reg0 as usize & 0x40) >> 3) | ((self.reg0 | self.reg1) as usize & 0x07)
3433            }
3434            _ => 0,
3435        }
3436    }
3437}
3438
3439impl Mapper for DiscreteMapper {
3440    fn caps(&self) -> MapperCaps {
3441        MapperCaps::NONE
3442    }
3443
3444    fn cpu_read(&mut self, addr: u16) -> u8 {
3445        match self.board {
3446            DiscreteBoard::M46 => {
3447                if (0x8000..=0xFFFF).contains(&addr) {
3448                    let bank = ((self.reg0 as usize & 0x0F) << 1) | (self.reg1 as usize & 0x01);
3449                    self.prg_32k(bank, addr)
3450                } else {
3451                    0
3452                }
3453            }
3454            DiscreteBoard::M51 => {
3455                let bank4 = (self.reg0 as usize) << 2;
3456                match addr {
3457                    0x6000..=0x7FFF => {
3458                        let b = if self.reg1 & 0x01 != 0 {
3459                            0x23 | bank4
3460                        } else {
3461                            0x2F | bank4
3462                        };
3463                        self.prg_8k(b, addr)
3464                    }
3465                    0x8000..=0xBFFF if self.reg1 & 0x01 != 0 => self.prg_16k(bank4 >> 1, addr),
3466                    0xC000..=0xFFFF if self.reg1 & 0x01 != 0 => {
3467                        self.prg_16k((bank4 >> 1) | 1, addr)
3468                    }
3469                    0x8000..=0xBFFF => {
3470                        self.prg_16k((bank4 >> 1) | (self.reg1 as usize >> 1 & 0x01), addr)
3471                    }
3472                    0xC000..=0xFFFF => self.prg_16k((bank4 >> 1) | 0x07, addr),
3473                    _ => 0,
3474                }
3475            }
3476            DiscreteBoard::M57 => {
3477                if (0x8000..=0xFFFF).contains(&addr) {
3478                    if self.reg1 & 0x10 != 0 {
3479                        let b = ((self.reg1 as usize >> 5) & 0x06) >> 1;
3480                        self.prg_32k(b, addr)
3481                    } else {
3482                        let b16 = (self.reg1 as usize >> 5) & 0x07;
3483                        self.prg_16k(b16, addr)
3484                    }
3485                } else {
3486                    0
3487                }
3488            }
3489            DiscreteBoard::M104 => match addr {
3490                0x8000..=0xBFFF => self.prg_16k(self.reg0 as usize, addr),
3491                0xC000..=0xFFFF => {
3492                    let high = (self.reg1 as usize & 0x70) | 0x0F;
3493                    self.prg_16k(high, addr)
3494                }
3495                _ => 0,
3496            },
3497            DiscreteBoard::M120 => match addr {
3498                0x6000..=0x7FFF => self.prg_8k(self.reg0 as usize, addr),
3499                0x8000..=0xFFFF => {
3500                    let count = (self.prg_rom.len() / PRG_BANK_8K).max(1);
3501                    let slot = (addr as usize - 0x8000) / PRG_BANK_8K;
3502                    let base = count.saturating_sub(4);
3503                    self.prg_8k(base + slot, addr)
3504                }
3505                _ => 0,
3506            },
3507            DiscreteBoard::M290 => {
3508                if (0x8000..=0xFFFF).contains(&addr) {
3509                    self.prg_16k(self.reg0 as usize, addr)
3510                } else {
3511                    0
3512                }
3513            }
3514            DiscreteBoard::M301 => {
3515                if (0x8000..=0xFFFF).contains(&addr) {
3516                    let a = self.last_addr;
3517                    let inner = (a >> 2) & 0x07;
3518                    let outer128 = (a >> 5) & 0x03;
3519                    // A7 is the 256 KiB outer-bank select; without it any PRG
3520                    // image > 256 KiB can only reach its low half. Slot it
3521                    // between the 128 KiB (A5-A6) and 512 KiB (A8) selects.
3522                    let outer256 = (a >> 7) & 0x01;
3523                    let outer512 = (a >> 8) & 0x01;
3524                    let bank16 = (outer512 << 6) | (outer256 << 5) | (outer128 << 3) | inner;
3525                    self.prg_16k(bank16 as usize, addr)
3526                } else {
3527                    0
3528                }
3529            }
3530        }
3531    }
3532
3533    fn cpu_read_unmapped(&self, addr: u16) -> bool {
3534        match self.board {
3535            DiscreteBoard::M51 | DiscreteBoard::M120 => (0x4020..=0x5FFF).contains(&addr),
3536            _ => (0x4020..=0x7FFF).contains(&addr),
3537        }
3538    }
3539
3540    fn cpu_write(&mut self, addr: u16, value: u8) {
3541        match self.board {
3542            DiscreteBoard::M46 => {
3543                if addr < 0x8000 {
3544                    self.reg0 = value;
3545                } else {
3546                    self.reg1 = value;
3547                }
3548            }
3549            DiscreteBoard::M51 => match addr {
3550                0x6000..=0x7FFF => self.reg1 = ((value >> 3) & 0x02) | ((value >> 1) & 0x01),
3551                0xC000..=0xDFFF => {
3552                    self.reg0 = value & 0x0F;
3553                    self.reg1 = ((value >> 3) & 0x02) | (self.reg1 & 0x01);
3554                }
3555                0x8000..=0xFFFF => self.reg0 = value & 0x0F,
3556                _ => {}
3557            },
3558            DiscreteBoard::M57 => match addr & 0x8800 {
3559                0x8000 => self.reg0 = value,
3560                0x8800 => self.reg1 = value,
3561                _ => {}
3562            },
3563            DiscreteBoard::M104 => {
3564                if addr >= 0xC000 {
3565                    self.reg0 = (self.reg0 & 0xF0) | (value & 0x0F);
3566                } else if (0x8000..=0x9FFF).contains(&addr) && value & 0x08 != 0 {
3567                    self.reg0 = (self.reg0 & 0x0F) | ((value << 4) & 0x70);
3568                    self.reg1 = value;
3569                }
3570            }
3571            DiscreteBoard::M120 => {
3572                if addr == 0x41FF {
3573                    self.reg0 = value;
3574                }
3575            }
3576            DiscreteBoard::M290 => {
3577                if (0x8000..=0xFFFF).contains(&addr) {
3578                    let prg = ((addr >> 10) & 0x1E) as u8;
3579                    let chr = (((addr & 0x0300) >> 5) | (addr & 0x07)) as u8;
3580                    self.reg0 = if addr & 0x80 != 0 {
3581                        prg | ((addr >> 6) & 1) as u8
3582                    } else {
3583                        prg & 0xFE
3584                    };
3585                    self.reg1 = chr;
3586                    self.mirroring = if addr & 0x400 != 0 {
3587                        Mirroring::Horizontal
3588                    } else {
3589                        Mirroring::Vertical
3590                    };
3591                }
3592            }
3593            DiscreteBoard::M301 => {
3594                if (0x8000..=0xFFFF).contains(&addr) {
3595                    self.last_addr = addr;
3596                    self.mirroring = if addr & 0x02 != 0 {
3597                        Mirroring::Horizontal
3598                    } else {
3599                        Mirroring::Vertical
3600                    };
3601                }
3602            }
3603        }
3604    }
3605
3606    fn ppu_read(&mut self, addr: u16) -> u8 {
3607        let addr = addr & 0x3FFF;
3608        match addr {
3609            0x0000..=0x1FFF => {
3610                if self.chr_is_ram {
3611                    return self.chr[addr as usize & (self.chr.len() - 1)];
3612                }
3613                let count = (self.chr.len() / CHR_BANK_8K).max(1);
3614                let bank = self.chr_8k_bank() % count;
3615                self.chr[bank * CHR_BANK_8K + (addr as usize & 0x1FFF)]
3616            }
3617            0x2000..=0x3EFF => self.vram[nametable_offset(addr, self.mirroring)],
3618            _ => 0,
3619        }
3620    }
3621
3622    fn ppu_write(&mut self, addr: u16, value: u8) {
3623        let addr = addr & 0x3FFF;
3624        match addr {
3625            0x0000..=0x1FFF if self.chr_is_ram => {
3626                let off = addr as usize & (self.chr.len() - 1);
3627                self.chr[off] = value;
3628            }
3629            0x2000..=0x3EFF => {
3630                let off = nametable_offset(addr, self.mirroring);
3631                self.vram[off] = value;
3632            }
3633            _ => {}
3634        }
3635    }
3636
3637    fn current_mirroring(&self) -> Mirroring {
3638        self.mirroring
3639    }
3640
3641    fn save_state(&self) -> Vec<u8> {
3642        let chr_ram = if self.chr_is_ram { self.chr.len() } else { 0 };
3643        let mut out = Vec::with_capacity(6 + self.vram.len() + chr_ram);
3644        out.push(SAVE_STATE_VERSION);
3645        out.push(self.reg0);
3646        out.push(self.reg1);
3647        out.push((self.last_addr & 0xFF) as u8);
3648        out.push((self.last_addr >> 8) as u8);
3649        out.push(mirroring_to_byte(self.mirroring));
3650        out.extend_from_slice(&self.vram);
3651        if self.chr_is_ram {
3652            out.extend_from_slice(&self.chr);
3653        }
3654        out
3655    }
3656
3657    fn load_state(&mut self, data: &[u8]) -> Result<(), MapperError> {
3658        let chr_ram = if self.chr_is_ram { self.chr.len() } else { 0 };
3659        let expected = 6 + self.vram.len() + chr_ram;
3660        if data.len() != expected {
3661            return Err(MapperError::WrongLength {
3662                expected,
3663                got: data.len(),
3664            });
3665        }
3666        if data[0] != SAVE_STATE_VERSION {
3667            return Err(MapperError::UnsupportedVersion(data[0]));
3668        }
3669        self.reg0 = data[1];
3670        self.reg1 = data[2];
3671        self.last_addr = u16::from(data[3]) | (u16::from(data[4]) << 8);
3672        self.mirroring = byte_to_mirroring(data[5], self.mirroring);
3673        let mut cursor = 6;
3674        self.vram
3675            .copy_from_slice(&data[cursor..cursor + self.vram.len()]);
3676        cursor += self.vram.len();
3677        if self.chr_is_ram {
3678            self.chr
3679                .copy_from_slice(&data[cursor..cursor + self.chr.len()]);
3680        }
3681        Ok(())
3682    }
3683}
3684
3685macro_rules! discrete_ctor {
3686    ($fn_name:ident, $board:expr, $id:expr, $doc:expr) => {
3687        #[doc = $doc]
3688        ///
3689        /// # Errors
3690        /// [`MapperError::Invalid`] on a bad PRG/CHR size.
3691        pub fn $fn_name(
3692            prg_rom: Box<[u8]>,
3693            chr_rom: Box<[u8]>,
3694            mirroring: Mirroring,
3695        ) -> Result<DiscreteMapper, MapperError> {
3696            DiscreteMapper::new($board, prg_rom, chr_rom, mirroring, $id)
3697        }
3698    };
3699}
3700
3701discrete_ctor!(
3702    new_m46,
3703    DiscreteBoard::M46,
3704    46,
3705    "Mapper 46 (Color Dreams Rumble Station 15-in-1)."
3706);
3707discrete_ctor!(
3708    new_m51,
3709    DiscreteBoard::M51,
3710    51,
3711    "Mapper 51 (BMC 11-in-1 multicart)."
3712);
3713discrete_ctor!(
3714    new_m57,
3715    DiscreteBoard::M57,
3716    57,
3717    "Mapper 57 (BMC GK 6-in-1 multicart)."
3718);
3719discrete_ctor!(
3720    new_m104,
3721    DiscreteBoard::M104,
3722    104,
3723    "Mapper 104 (Codemasters Golden Five / Pegasus 5-in-1)."
3724);
3725discrete_ctor!(
3726    new_m120,
3727    DiscreteBoard::M120,
3728    120,
3729    "Mapper 120 (Tobidase Daisakusen FDS-conversion protection)."
3730);
3731discrete_ctor!(
3732    new_m290,
3733    DiscreteBoard::M290,
3734    290,
3735    "Mapper 290 (NTDEC Asder BMC-NTD-03)."
3736);
3737discrete_ctor!(
3738    new_m301,
3739    DiscreteBoard::M301,
3740    301,
3741    "Mapper 301 (BMC-8157 address-as-data multicart)."
3742);
3743
3744/// Discrete NROM/UNROM 2-in-1 BMC multicart (mapper 204).
3745pub struct Bmc204 {
3746    prg_rom: Box<[u8]>,
3747    chr: Box<[u8]>,
3748    chr_is_ram: bool,
3749    vram: Box<[u8]>,
3750    mirroring: Mirroring,
3751    /// 16 KiB PRG windows for $8000 and $C000.
3752    prg0: usize,
3753    prg1: usize,
3754    /// 8 KiB CHR window.
3755    chr8: usize,
3756}
3757
3758impl Bmc204 {
3759    fn new(
3760        prg_rom: Box<[u8]>,
3761        chr_rom: Box<[u8]>,
3762        mirroring: Mirroring,
3763    ) -> Result<Self, MapperError> {
3764        check_prg(&prg_rom, 204)?;
3765        if prg_rom.len() < PRG_BANK_16K {
3766            return Err(MapperError::Invalid(format!(
3767                "mapper 204 PRG-ROM size {} is smaller than one 16 KiB bank",
3768                prg_rom.len()
3769            )));
3770        }
3771        let (chr, chr_is_ram) = chr_or_ram(chr_rom);
3772        let mut m = Self {
3773            prg_rom,
3774            chr,
3775            chr_is_ram,
3776            vram: vec![0u8; 2 * NAMETABLE_SIZE].into_boxed_slice(),
3777            mirroring,
3778            prg0: 0,
3779            prg1: 0,
3780            chr8: 0,
3781        };
3782        // Power-on: WriteRegister(0x8000, 0).
3783        m.write_addr(0x8000);
3784        Ok(m)
3785    }
3786
3787    fn prg_count_16k(&self) -> usize {
3788        (self.prg_rom.len() / PRG_BANK_16K).max(1)
3789    }
3790
3791    fn chr_count_8k(&self) -> usize {
3792        (self.chr.len() / CHR_BANK_8K).max(1)
3793    }
3794
3795    fn write_addr(&mut self, addr: u16) {
3796        let bit_mask = (addr & 0x06) as usize;
3797        let page = bit_mask
3798            + if bit_mask == 0x06 {
3799                0
3800            } else {
3801                (addr & 0x01) as usize
3802            };
3803        self.prg0 = page;
3804        self.prg1 = bit_mask
3805            + if bit_mask == 0x06 {
3806                1
3807            } else {
3808                (addr & 0x01) as usize
3809            };
3810        self.chr8 = page;
3811        self.mirroring = if addr & 0x10 != 0 {
3812            Mirroring::Horizontal
3813        } else {
3814            Mirroring::Vertical
3815        };
3816    }
3817
3818    fn prg_byte(&self, slot16: usize, addr: u16) -> u8 {
3819        let count = self.prg_count_16k();
3820        let bank = slot16 % count;
3821        self.prg_rom[bank * PRG_BANK_16K + (addr as usize & 0x3FFF)]
3822    }
3823}
3824
3825impl Mapper for Bmc204 {
3826    fn caps(&self) -> MapperCaps {
3827        MapperCaps::NONE
3828    }
3829
3830    fn cpu_read(&mut self, addr: u16) -> u8 {
3831        match addr {
3832            0x8000..=0xBFFF => self.prg_byte(self.prg0, addr),
3833            0xC000..=0xFFFF => self.prg_byte(self.prg1, addr),
3834            _ => 0,
3835        }
3836    }
3837
3838    fn cpu_write(&mut self, addr: u16, _value: u8) {
3839        if addr >= 0x8000 {
3840            self.write_addr(addr);
3841        }
3842    }
3843
3844    fn ppu_read(&mut self, addr: u16) -> u8 {
3845        let addr = addr & 0x3FFF;
3846        match addr {
3847            0x0000..=0x1FFF => {
3848                if self.chr_is_ram {
3849                    return self.chr[addr as usize & (CHR_BANK_8K - 1)];
3850                }
3851                let bank = self.chr8 % self.chr_count_8k();
3852                self.chr[bank * CHR_BANK_8K + (addr as usize & 0x1FFF)]
3853            }
3854            0x2000..=0x3EFF => self.vram[nametable_offset(addr, self.mirroring)],
3855            _ => 0,
3856        }
3857    }
3858
3859    fn ppu_write(&mut self, addr: u16, value: u8) {
3860        let addr = addr & 0x3FFF;
3861        match addr {
3862            0x0000..=0x1FFF if self.chr_is_ram => {
3863                self.chr[addr as usize & (CHR_BANK_8K - 1)] = value;
3864            }
3865            0x2000..=0x3EFF => {
3866                let off = nametable_offset(addr, self.mirroring);
3867                self.vram[off] = value;
3868            }
3869            _ => {}
3870        }
3871    }
3872
3873    fn current_mirroring(&self) -> Mirroring {
3874        self.mirroring
3875    }
3876
3877    fn save_state(&self) -> Vec<u8> {
3878        let chr_ram = if self.chr_is_ram { self.chr.len() } else { 0 };
3879        let mut out = Vec::with_capacity(1 + 12 + 1 + self.vram.len() + chr_ram);
3880        out.push(SAVE_STATE_VERSION);
3881        out.extend_from_slice(&(self.prg0 as u32).to_le_bytes());
3882        out.extend_from_slice(&(self.prg1 as u32).to_le_bytes());
3883        out.extend_from_slice(&(self.chr8 as u32).to_le_bytes());
3884        out.push(mirroring_to_byte(self.mirroring));
3885        out.extend_from_slice(&self.vram);
3886        if self.chr_is_ram {
3887            out.extend_from_slice(&self.chr);
3888        }
3889        out
3890    }
3891
3892    fn load_state(&mut self, data: &[u8]) -> Result<(), MapperError> {
3893        let chr_ram = if self.chr_is_ram { self.chr.len() } else { 0 };
3894        let expected = 1 + 12 + 1 + self.vram.len() + chr_ram;
3895        if data.len() != expected {
3896            return Err(MapperError::WrongLength {
3897                expected,
3898                got: data.len(),
3899            });
3900        }
3901        if data[0] != SAVE_STATE_VERSION {
3902            return Err(MapperError::UnsupportedVersion(data[0]));
3903        }
3904        let rd = |c: usize| {
3905            u32::from_le_bytes([data[c], data[c + 1], data[c + 2], data[c + 3]]) as usize
3906        };
3907        self.prg0 = rd(1);
3908        self.prg1 = rd(5);
3909        self.chr8 = rd(9);
3910        let mut c = 13;
3911        self.mirroring = byte_to_mirroring(data[c], self.mirroring);
3912        c += 1;
3913        self.vram.copy_from_slice(&data[c..c + self.vram.len()]);
3914        c += self.vram.len();
3915        if self.chr_is_ram {
3916            self.chr.copy_from_slice(&data[c..c + self.chr.len()]);
3917        }
3918        Ok(())
3919    }
3920}
3921
3922/// Mapper 204 (discrete NROM/UNROM 2-in-1 BMC multicart).
3923///
3924/// # Errors
3925/// [`MapperError::Invalid`] on a bad PRG size.
3926pub fn new_m204(
3927    prg_rom: Box<[u8]>,
3928    chr_rom: Box<[u8]>,
3929    mirroring: Mirroring,
3930) -> Result<Bmc204, MapperError> {
3931    Bmc204::new(prg_rom, chr_rom, mirroring)
3932}
3933
3934// ===========================================================================
3935// NtdecN625092 (mapper 221) — NTDEC N625092 multicart.
3936//
3937// $8000 latches a 16-bit "mode" from the written address; $C000 latches the
3938// 3-bit inner PRG register. The outer bank is `(mode & 0xFC) >> 2`. When
3939// `mode & 0x02` the board is in UNROM-style mode (a switchable $8000 + a fixed
3940// $C000), with a NROM-256 sub-case when `mode & 0x0100`; otherwise both 16 KiB
3941// windows mirror the same NROM bank. `mode & 0x01` flips the mirroring. CHR is a
3942// single fixed 8 KiB window. Register map per the NESdev wiki mapper-299 /
3943// BMC-11160 documentation; the implementation is derived from Mesen2's
3944// `Txc/Bmc11160.h` (GPL-3.0-or-later). See NOTICE + docs/originality-and-provenance.md §1.
3945// ===========================================================================
3946
3947/// TXC/BMC-11160 multicart (mapper 299).
3948pub struct Bmc11160 {
3949    prg_rom: Box<[u8]>,
3950    chr: Box<[u8]>,
3951    chr_is_ram: bool,
3952    vram: Box<[u8]>,
3953    mirroring: Mirroring,
3954    /// 32 KiB PRG window.
3955    prg32: usize,
3956    /// 8 KiB CHR window.
3957    chr8: usize,
3958}
3959
3960impl Bmc11160 {
3961    fn new(
3962        prg_rom: Box<[u8]>,
3963        chr_rom: Box<[u8]>,
3964        mirroring: Mirroring,
3965    ) -> Result<Self, MapperError> {
3966        check_prg(&prg_rom, 299)?;
3967        if prg_rom.len() < PRG_BANK_32K {
3968            return Err(MapperError::Invalid(format!(
3969                "mapper 299 PRG-ROM size {} is smaller than one 32 KiB bank",
3970                prg_rom.len()
3971            )));
3972        }
3973        let (chr, chr_is_ram) = chr_or_ram(chr_rom);
3974        let mut m = Self {
3975            prg_rom,
3976            chr,
3977            chr_is_ram,
3978            vram: vec![0u8; 2 * NAMETABLE_SIZE].into_boxed_slice(),
3979            mirroring,
3980            prg32: 0,
3981            chr8: 0,
3982        };
3983        // Power-on (Reset): WriteRegister(0x8000, 0).
3984        m.write_reg(0);
3985        Ok(m)
3986    }
3987
3988    fn prg_count_32k(&self) -> usize {
3989        (self.prg_rom.len() / PRG_BANK_32K).max(1)
3990    }
3991
3992    fn chr_count_8k(&self) -> usize {
3993        (self.chr.len() / CHR_BANK_8K).max(1)
3994    }
3995
3996    fn write_reg(&mut self, value: u8) {
3997        let bank = ((value >> 4) & 0x07) as usize;
3998        self.prg32 = bank;
3999        self.chr8 = (bank << 2) | (value as usize & 0x03);
4000        self.mirroring = if value & 0x80 != 0 {
4001            Mirroring::Vertical
4002        } else {
4003            Mirroring::Horizontal
4004        };
4005    }
4006}
4007
4008impl Mapper for Bmc11160 {
4009    fn caps(&self) -> MapperCaps {
4010        MapperCaps::NONE
4011    }
4012
4013    fn cpu_read(&mut self, addr: u16) -> u8 {
4014        match addr {
4015            0x8000..=0xFFFF => {
4016                let count = self.prg_count_32k();
4017                let bank = self.prg32 % count;
4018                self.prg_rom[bank * PRG_BANK_32K + (addr as usize & 0x7FFF)]
4019            }
4020            _ => 0,
4021        }
4022    }
4023
4024    fn cpu_write(&mut self, addr: u16, value: u8) {
4025        if addr >= 0x8000 {
4026            self.write_reg(value);
4027        }
4028    }
4029
4030    fn ppu_read(&mut self, addr: u16) -> u8 {
4031        let addr = addr & 0x3FFF;
4032        match addr {
4033            0x0000..=0x1FFF => {
4034                if self.chr_is_ram {
4035                    return self.chr[addr as usize & (CHR_BANK_8K - 1)];
4036                }
4037                let bank = self.chr8 % self.chr_count_8k();
4038                self.chr[bank * CHR_BANK_8K + (addr as usize & 0x1FFF)]
4039            }
4040            0x2000..=0x3EFF => self.vram[nametable_offset(addr, self.mirroring)],
4041            _ => 0,
4042        }
4043    }
4044
4045    fn ppu_write(&mut self, addr: u16, value: u8) {
4046        let addr = addr & 0x3FFF;
4047        match addr {
4048            0x0000..=0x1FFF if self.chr_is_ram => {
4049                self.chr[addr as usize & (CHR_BANK_8K - 1)] = value;
4050            }
4051            0x2000..=0x3EFF => {
4052                let off = nametable_offset(addr, self.mirroring);
4053                self.vram[off] = value;
4054            }
4055            _ => {}
4056        }
4057    }
4058
4059    fn current_mirroring(&self) -> Mirroring {
4060        self.mirroring
4061    }
4062
4063    fn save_state(&self) -> Vec<u8> {
4064        let chr_ram = if self.chr_is_ram { self.chr.len() } else { 0 };
4065        let mut out = Vec::with_capacity(1 + 8 + 1 + self.vram.len() + chr_ram);
4066        out.push(SAVE_STATE_VERSION);
4067        out.extend_from_slice(&(self.prg32 as u32).to_le_bytes());
4068        out.extend_from_slice(&(self.chr8 as u32).to_le_bytes());
4069        out.push(mirroring_to_byte(self.mirroring));
4070        out.extend_from_slice(&self.vram);
4071        if self.chr_is_ram {
4072            out.extend_from_slice(&self.chr);
4073        }
4074        out
4075    }
4076
4077    fn load_state(&mut self, data: &[u8]) -> Result<(), MapperError> {
4078        let chr_ram = if self.chr_is_ram { self.chr.len() } else { 0 };
4079        let expected = 1 + 8 + 1 + self.vram.len() + chr_ram;
4080        if data.len() != expected {
4081            return Err(MapperError::WrongLength {
4082                expected,
4083                got: data.len(),
4084            });
4085        }
4086        if data[0] != SAVE_STATE_VERSION {
4087            return Err(MapperError::UnsupportedVersion(data[0]));
4088        }
4089        let rd = |c: usize| {
4090            u32::from_le_bytes([data[c], data[c + 1], data[c + 2], data[c + 3]]) as usize
4091        };
4092        self.prg32 = rd(1);
4093        self.chr8 = rd(5);
4094        let mut c = 9;
4095        self.mirroring = byte_to_mirroring(data[c], self.mirroring);
4096        c += 1;
4097        self.vram.copy_from_slice(&data[c..c + self.vram.len()]);
4098        c += self.vram.len();
4099        if self.chr_is_ram {
4100            self.chr.copy_from_slice(&data[c..c + self.chr.len()]);
4101        }
4102        Ok(())
4103    }
4104}
4105
4106/// Mapper 299 (TXC/BMC-11160 multicart).
4107///
4108/// # Errors
4109/// [`MapperError::Invalid`] on a bad PRG size.
4110pub fn new_m299(
4111    prg_rom: Box<[u8]>,
4112    chr_rom: Box<[u8]>,
4113    mirroring: Mirroring,
4114) -> Result<Bmc11160, MapperError> {
4115    Bmc11160::new(prg_rom, chr_rom, mirroring)
4116}
4117
4118#[cfg(test)]
4119#[allow(clippy::cast_possible_truncation)]
4120mod tests {
4121    use super::*;
4122    /// 8 KiB-banked PRG: byte 0 of each 8 KiB bank holds the bank index.
4123    fn synth_prg_8k(banks: usize) -> Box<[u8]> {
4124        let mut v = vec![0xFFu8; banks * PRG_BANK_8K];
4125        for b in 0..banks {
4126            v[b * PRG_BANK_8K] = b as u8;
4127        }
4128        v.into_boxed_slice()
4129    }
4130
4131    /// 32 KiB-banked PRG: byte 0 of each 32 KiB bank holds the bank index (all
4132    /// other offsets are transparent).
4133    fn synth_prg_32k(banks: usize) -> Box<[u8]> {
4134        let mut v = vec![0xFFu8; banks * PRG_BANK_32K];
4135        for b in 0..banks {
4136            v[b * PRG_BANK_32K] = b as u8;
4137        }
4138        v.into_boxed_slice()
4139    }
4140
4141    fn synth_prg_16k(banks: usize) -> Box<[u8]> {
4142        let mut v = vec![0xFFu8; banks * PRG_BANK_16K];
4143        for b in 0..banks {
4144            v[b * PRG_BANK_16K] = b as u8;
4145        }
4146        v.into_boxed_slice()
4147    }
4148
4149    fn synth_chr_8k(banks: usize) -> Box<[u8]> {
4150        let mut v = vec![0u8; banks * CHR_BANK_8K];
4151        for b in 0..banks {
4152            v[b * CHR_BANK_8K] = b as u8;
4153        }
4154        v.into_boxed_slice()
4155    }
4156
4157    #[test]
4158    fn m15_mode0_two_16k_halves() {
4159        // 8 16 KiB banks = 128 KiB.
4160        let mut m = Multicart15::new(synth_prg_16k(8), &[]).unwrap();
4161        // mode 0 ($8000), prg_bank = 2, mirroring bit clear (vertical).
4162        m.cpu_write(0x8000, 0b0000_0010);
4163        assert_eq!(m.cpu_read(0x8000), 2); // low half = bank 2
4164        assert_eq!(m.cpu_read(0xC000), 3); // high half = bank 2|1 = 3
4165        assert_eq!(m.current_mirroring(), Mirroring::Vertical);
4166    }
4167
4168    #[test]
4169    fn m15_mirroring_bit() {
4170        let mut m = Multicart15::new(synth_prg_16k(8), &[]).unwrap();
4171        m.cpu_write(0x8000, 0b0100_0000); // bit 6 = horizontal
4172        assert_eq!(m.current_mirroring(), Mirroring::Horizontal);
4173    }
4174
4175    #[test]
4176    fn m15_mode3_single_bank_mirrored() {
4177        let mut m = Multicart15::new(synth_prg_16k(8), &[]).unwrap();
4178        // mode 3 ($8003), prg_bank = 5.
4179        m.cpu_write(0x8003, 0b0000_0101);
4180        assert_eq!(m.cpu_read(0x8000), 5);
4181        assert_eq!(m.cpu_read(0xC000), 5); // 16 KiB mirrored across the window
4182    }
4183
4184    /// CHR-RAM stays writable in EVERY mode, deliberately against the hardware.
4185    ///
4186    /// This test previously asserted the opposite -- that modes 0 and 3
4187    /// discard CHR-RAM writes, which is what a real K-1029 does. The NESdev
4188    /// `INES_Mapper_015` page requires an emulator not to enforce it: only two
4189    /// cartridges genuinely use mapper 15, and every other mapper-15 ROM is a
4190    /// hack that expects writes to land. Power-on mode is 0, so enforcing the
4191    /// board cost ten of the eleven mapper-15 ROMs in the coverage corpus a
4192    /// blank screen -- Pokemon Gold and Crazy Climber among them, both of which
4193    /// now render.
4194    #[test]
4195    fn m15_chr_ram_is_writable_in_every_mode() {
4196        let mut m = Multicart15::new(synth_prg_16k(8), &[]).unwrap();
4197        for (mode_addr, value) in [
4198            (0x8000u16, 0xABu8),
4199            (0x8001, 0xCD),
4200            (0x8002, 0xEF),
4201            (0x8003, 0x42),
4202        ] {
4203            m.cpu_write(mode_addr, 0);
4204            m.ppu_write(0x0000, value);
4205            assert_eq!(
4206                m.ppu_read(0x0000),
4207                value,
4208                "mapper-15 hacks require CHR-RAM writes to land in mode {}",
4209                mode_addr & 0x03
4210            );
4211        }
4212    }
4213
4214    /// PRG-RAM survives a save-state round trip.
4215    ///
4216    /// Written because the field was added without one, and a snapshot that
4217    /// silently drops battery-backed RAM is the failure mode v2.2.3's
4218    /// field-vs-schema audit exists to catch.
4219    #[test]
4220    fn m15_prg_ram_round_trips_through_a_save_state() {
4221        let mut m = Multicart15::new(synth_prg_16k(8), &[]).unwrap();
4222        m.cpu_write(0x6000, 0x11);
4223        m.cpu_write(0x7FFF, 0x22);
4224        m.ppu_write(0x0000, 0x33);
4225        let blob = m.save_state();
4226
4227        let mut restored = Multicart15::new(synth_prg_16k(8), &[]).unwrap();
4228        restored.load_state(&blob).expect("same-shape state loads");
4229        assert_eq!(restored.cpu_read(0x6000), 0x11);
4230        assert_eq!(restored.cpu_read(0x7FFF), 0x22);
4231        assert_eq!(restored.ppu_read(0x0000), 0x33);
4232    }
4233
4234    /// The 8 KiB of PRG-RAM at `$6000-$7FFF` that a real K-1029 does not have,
4235    /// and that the NESdev page requires an emulator to provide for the hacks.
4236    #[test]
4237    fn m15_provides_prg_ram_the_board_lacks() {
4238        let mut m = Multicart15::new(synth_prg_16k(8), &[]).unwrap();
4239        m.cpu_write(0x6000, 0x5A);
4240        m.cpu_write(0x7FFF, 0xA5);
4241        assert_eq!(m.cpu_read(0x6000), 0x5A);
4242        assert_eq!(m.cpu_read(0x7FFF), 0xA5);
4243        // A write into the RAM window must NOT be decoded as the bank latch.
4244        let before = m.cpu_read(0x8000);
4245        m.cpu_write(0x6001, 0xFF);
4246        assert_eq!(
4247            m.cpu_read(0x8000),
4248            before,
4249            "the $6000-$7FFF window is RAM, not the mapper's address/data latch"
4250        );
4251    }
4252
4253    #[test]
4254    fn m61_16k_mode_address_decode() {
4255        // 16 16 KiB banks.
4256        let mut m = Multicart61::new(synth_prg_16k(16), &[]).unwrap();
4257        // Choose addr with A&0x0F = 3, A>>5&1 = 0 -> page = 6; A&0x10 set (16k);
4258        // A&0x80 set (horizontal). addr = 0x8000 | 0x10 | 0x80 | 0x03 = 0x8093.
4259        m.cpu_write(0x8093, 0x00);
4260        assert_eq!(m.cpu_read(0x8000), 6);
4261        assert_eq!(m.cpu_read(0xC000), 6); // 16 KiB mirrored
4262        assert_eq!(m.current_mirroring(), Mirroring::Horizontal);
4263    }
4264
4265    #[test]
4266    fn m61_32k_mode() {
4267        let mut m = Multicart61::new(synth_prg_16k(16), &[]).unwrap();
4268        // A&0x0F = 2, A>>5&1 = 0 -> page = 4; 32 KiB mode (A&0x10 clear).
4269        // 32 KiB bank = page>>1 = 2. addr = 0x8000 | 0x02 = 0x8002.
4270        m.cpu_write(0x8002, 0x00);
4271        // 32 KiB bank 2 = 16 KiB banks 4 and 5.
4272        assert_eq!(m.cpu_read(0x8000), 4);
4273        assert_eq!(m.cpu_read(0xC000), 5);
4274    }
4275
4276    #[test]
4277    fn m62_address_and_data_decode() {
4278        let mut m =
4279            Multicart62::new(synth_prg_16k(8), synth_chr_8k(256), Mirroring::Vertical).unwrap();
4280        // prg_page = ((A&0x3F00)>>8) | (A&0x40); pick A bits so page small.
4281        // A = 0x8000 | (0x01 << 8) | 0x20(16k mode) | 0x80(horiz) | 0x05(chr lo)
4282        //   prg_page = 0x01, 16k mode, horizontal, chr = (5<<2)|data&3.
4283        let addr = 0x8000 | (0x01 << 8) | 0x20 | 0x80 | 0x05;
4284        m.cpu_write(addr, 0x02); // data low 2 bits = 2
4285        assert_eq!(m.cpu_read(0x8000), 1); // 16k bank 1
4286        assert_eq!(m.current_mirroring(), Mirroring::Horizontal);
4287        // chr_bank = (5<<2)|2 = 22.
4288        assert_eq!(m.ppu_read(0x0000), 22);
4289    }
4290
4291    #[test]
4292    fn m15_save_state_round_trips_mirroring() {
4293        let mut m = Multicart15::new(synth_prg_16k(8), &[]).unwrap();
4294        m.cpu_write(0x8001, 0b0100_0101);
4295        let blob = m.save_state();
4296        let mut m2 = Multicart15::new(synth_prg_16k(8), &[]).unwrap();
4297        m2.load_state(&blob).unwrap();
4298        assert_eq!(m2.current_mirroring(), m.current_mirroring());
4299    }
4300
4301    #[test]
4302    fn m200_address_latch() {
4303        let mut m =
4304            Multicart200::new(synth_prg_16k(8), synth_chr_8k(8), Mirroring::Vertical).unwrap();
4305        // Write address with low bits = 3 and bit3 set (horizontal).
4306        m.cpu_write(0x8000 | 0x0B, 0x00); // 0x0B = 0b1011: bank 3, H bit set
4307        assert_eq!(m.cpu_read(0x8000), 3);
4308        // NROM-128: $8000 mirrors $C000.
4309        assert_eq!(m.cpu_read(0xC000), 3);
4310        assert_eq!(m.ppu_read(0x0000), 3);
4311        assert_eq!(m.current_mirroring(), Mirroring::Horizontal);
4312        // bit3 clear -> vertical.
4313        m.cpu_write(0x8000 | 0x02, 0x00);
4314        assert_eq!(m.current_mirroring(), Mirroring::Vertical);
4315        assert_eq!(m.cpu_read(0x8000), 2);
4316    }
4317
4318    #[test]
4319    fn m201_address_drives_prg_and_chr() {
4320        let mut m =
4321            Multicart201::new(synth_prg_32k(4), synth_chr_8k(8), Mirroring::Vertical).unwrap();
4322        // addr low 3 bits = 0b011: PRG = 3 & 3 = 3, CHR = 3.
4323        m.cpu_write(0x8000 | 0x03, 0x00);
4324        assert_eq!(m.cpu_read(0x8000), 3);
4325        assert_eq!(m.ppu_read(0x0000), 3);
4326        // addr low 3 bits = 0b101: PRG = 5 & 3 = 1, CHR = 5.
4327        m.cpu_write(0x8000 | 0x05, 0x00);
4328        assert_eq!(m.cpu_read(0x8000), 1);
4329        assert_eq!(m.ppu_read(0x0000), 5);
4330    }
4331
4332    #[test]
4333    fn m202_16k_mode() {
4334        let mut m =
4335            Multicart202::new(synth_prg_16k(8), synth_chr_8k(8), Mirroring::Vertical).unwrap();
4336        // page = (addr>>1)&7. Pick page 3 -> addr bits 3..1 = 0b011 -> addr = 0b0110.
4337        // O bits (bit3 and bit0): bit3 = 0, bit0 = 0 -> not both set -> 16k mode.
4338        // mirroring = addr bit0 = 0 -> vertical.
4339        m.cpu_write(0x8000 | 0b0110, 0x00);
4340        assert_eq!(m.cpu_read(0x8000), 3);
4341        assert_eq!(m.cpu_read(0xC000), 3); // mirrored in 16k mode
4342        assert_eq!(m.ppu_read(0x0000), 3);
4343        assert_eq!(m.current_mirroring(), Mirroring::Vertical);
4344    }
4345
4346    #[test]
4347    fn m202_32k_mode_and_mirroring() {
4348        let mut m =
4349            Multicart202::new(synth_prg_16k(8), synth_chr_8k(8), Mirroring::Vertical).unwrap();
4350        // Both O bits set: addr bit3 = 1 and bit0 = 1.
4351        // page = (addr>>1)&7. addr = 0b1001 | (page<<1). Pick page 2 -> page<<1 = 0b100.
4352        // addr = 0b1101 = 0x0D: bit3=1, bit0=1 -> 32k mode. page = (0xD>>1)&7 = 6&7 = 6.
4353        // Recompute to make page even/clear: choose addr = 0x09 (0b1001): page = (9>>1)&7 = 4.
4354        //   bit3=1, bit0=1 -> 32k. mirroring = bit0 = 1 -> horizontal.
4355        m.cpu_write(0x8000 | 0x09, 0x00);
4356        assert_eq!(m.current_mirroring(), Mirroring::Horizontal);
4357        // 32k bank = (page>>1)<<1 = (4>>1)<<1 = 4. Bank 4 at $8000, bank 5 at $C000.
4358        assert_eq!(m.cpu_read(0x8000), 4);
4359        assert_eq!(m.cpu_read(0xC000), 5);
4360    }
4361
4362    #[test]
4363    fn m203_data_latch() {
4364        let mut m =
4365            Multicart203::new(synth_prg_16k(8), synth_chr_8k(4), Mirroring::Vertical).unwrap();
4366        // value PPPPPPCC: PRG = value>>2, CHR = value&3.
4367        // 0b0000_1110 = 0x0E: PRG = 3, CHR = 2.
4368        m.cpu_write(0x8000, 0x0E);
4369        assert_eq!(m.cpu_read(0x8000), 3);
4370        assert_eq!(m.cpu_read(0xC000), 3); // mirrored
4371        assert_eq!(m.ppu_read(0x0000), 2);
4372    }
4373
4374    #[test]
4375    fn m212_16k_mode_and_protection_read() {
4376        let mut m =
4377            Multicart212::new(synth_prg_16k(8), synth_chr_8k(8), Mirroring::Vertical).unwrap();
4378        // 16k mode (bit14 clear). page = addr & 7 = 3, mirroring bit3 = 1 (H).
4379        m.cpu_write(0x8000 | 0x0B, 0x00); // 0b1011: page 3, H bit set
4380        assert_eq!(m.cpu_read(0x8000), 3);
4381        assert_eq!(m.cpu_read(0xC000), 3);
4382        assert_eq!(m.ppu_read(0x0000), 3);
4383        assert_eq!(m.current_mirroring(), Mirroring::Horizontal);
4384        // Protection read: $6000 (addr&0x10 == 0) -> bit7 set.
4385        assert_eq!(m.cpu_read(0x6000) & 0x80, 0x80);
4386        assert_eq!(m.cpu_read(0x6010), 0x00);
4387    }
4388
4389    #[test]
4390    fn m212_32k_mode() {
4391        let mut m =
4392            Multicart212::new(synth_prg_16k(8), synth_chr_8k(8), Mirroring::Vertical).unwrap();
4393        // bit14 set -> 32k mode. page = addr & 7 = 4. 32k bank = (4>>1)<<1 = 4.
4394        m.cpu_write(0xC000 | 0x04, 0x00); // 0xC004: bit14 set, page 4
4395        assert_eq!(m.cpu_read(0x8000), 4);
4396        assert_eq!(m.cpu_read(0xC000), 5);
4397    }
4398
4399    #[test]
4400    fn m213_address_latch() {
4401        let mut m =
4402            Multicart213::new(synth_prg_32k(4), synth_chr_8k(8), Mirroring::Vertical).unwrap();
4403        // CHR = (addr>>3)&7, PRG = (addr>>1)&3.
4404        // Pick CHR 5, PRG 2: addr bits: (5<<3)|(2<<1) = 0x28 | 0x04 = 0x2C.
4405        m.cpu_write(0x8000 | 0x2C, 0x00);
4406        assert_eq!(m.ppu_read(0x0000), 5);
4407        assert_eq!(m.cpu_read(0x8000), 2);
4408    }
4409
4410    #[test]
4411    fn m214_address_latch() {
4412        let mut m =
4413            Multicart214::new(synth_prg_16k(8), synth_chr_8k(4), Mirroring::Vertical).unwrap();
4414        // CHR = addr & 3, PRG = (addr>>2)&3.
4415        // Pick PRG 2, CHR 1: addr bits = (2<<2)|1 = 0x09.
4416        m.cpu_write(0x8000 | 0x09, 0x00);
4417        assert_eq!(m.cpu_read(0x8000), 2);
4418        assert_eq!(m.cpu_read(0xC000), 2); // mirrored
4419        assert_eq!(m.ppu_read(0x0000), 1);
4420    }
4421
4422    #[test]
4423    fn m58_address_decoded_banking() {
4424        let mut m =
4425            Multicart58::new(synth_prg_16k(8), synth_chr_8k(8), Mirroring::Vertical).unwrap();
4426        // 16 KiB mode (bit 6 set): A = $8000 | (1<<6) | (CHR=2<<3) | (PRG=3).
4427        let addr = 0x8000 | (1 << 6) | (0b010 << 3) | 0b011;
4428        m.cpu_write(addr, 0x00);
4429        assert_eq!(m.cpu_read(0x8000), 3); // 16 KiB bank 3
4430        assert_eq!(m.ppu_read(0x0000), 2); // CHR bank 2
4431        // 32 KiB mode (bit 6 clear): PRG bank = (A&6)>>1. A low bits = 0b110 = 6
4432        // -> 32 KiB bank (6&6)>>1 = 3.
4433        let addr32 = 0x8000 | 0b110;
4434        m.cpu_write(addr32, 0x00);
4435        // synth_prg_16k(8) = 4 32 KiB banks; bank 3 offset 0 holds index 6.
4436        assert_eq!(m.cpu_read(0x8000), 6);
4437    }
4438
4439    #[test]
4440    fn m58_save_state_round_trip() {
4441        let mut m =
4442            Multicart58::new(synth_prg_16k(8), synth_chr_8k(8), Mirroring::Vertical).unwrap();
4443        let addr = 0x8000 | (1 << 7) | (1 << 6) | (0b001 << 3) | 0b010;
4444        m.cpu_write(addr, 0x00);
4445        let blob = m.save_state();
4446        let mut m2 =
4447            Multicart58::new(synth_prg_16k(8), synth_chr_8k(8), Mirroring::Vertical).unwrap();
4448        m2.load_state(&blob).unwrap();
4449        assert_eq!(m2.cpu_read(0x8000), 2);
4450        assert_eq!(m2.ppu_read(0x0000), 1);
4451        assert_eq!(m2.current_mirroring(), Mirroring::Horizontal);
4452    }
4453
4454    #[test]
4455    fn m60_power_on_bank_zero() {
4456        let mut m =
4457            Multicart60::new(synth_prg_16k(4), synth_chr_8k(4), Mirroring::Vertical).unwrap();
4458        assert_eq!(m.cpu_read(0x8000), 0);
4459        assert_eq!(m.ppu_read(0x0000), 0);
4460        // Writes are ignored (reset-driven selection not modelled).
4461        m.cpu_write(0x8000, 0xFF);
4462        assert_eq!(m.cpu_read(0x8000), 0);
4463    }
4464
4465    #[test]
4466    fn m60_save_state_round_trip() {
4467        let mut m =
4468            Multicart60::new(synth_prg_16k(4), synth_chr_8k(4), Mirroring::Vertical).unwrap();
4469        m.ppu_write(0x2000, 0x44);
4470        let blob = m.save_state();
4471        let mut m2 =
4472            Multicart60::new(synth_prg_16k(4), synth_chr_8k(4), Mirroring::Vertical).unwrap();
4473        m2.load_state(&blob).unwrap();
4474        assert_eq!(m2.ppu_read(0x2000), 0x44);
4475    }
4476
4477    #[test]
4478    fn m231_dual_bank_address_decoded() {
4479        let mut m = Multicart231::new(synth_prg_16k(32), &[], Mirroring::Vertical).unwrap();
4480        // A bits: prgBank = ((A>>5)&1) | (A&0x1E). Pick A = $8000 | 0x14 (= 0b1_0100).
4481        // A&0x1E = 0x14 = 20; (A>>5)&1 = 0. prgBank = 20.
4482        // bank0 = 20 & 0x1E = 20; bank1 = 20.
4483        let addr = 0x8000 | 0x14;
4484        m.cpu_write(addr, 0x00);
4485        assert_eq!(m.cpu_read(0x8000), 20);
4486        assert_eq!(m.cpu_read(0xC000), 20);
4487        // Set bit 5 (0x20): contributes 1 to bank1's LSB; A&0x1E unchanged.
4488        let addr2 = 0x8000 | 0x20 | 0x14; // (A>>5)&1 = 1
4489        m.cpu_write(addr2, 0x00);
4490        // prgBank = 1 | 20 = 21; bank0 = 21 & 0x1E = 20; bank1 = 21.
4491        assert_eq!(m.cpu_read(0x8000), 20);
4492        assert_eq!(m.cpu_read(0xC000), 21);
4493    }
4494
4495    #[test]
4496    fn m231_save_state_round_trip() {
4497        let mut m = Multicart231::new(synth_prg_16k(32), &[], Mirroring::Vertical).unwrap();
4498        m.cpu_write(0x8000 | 0x80 | 0x04, 0x00); // horizontal, bank bits
4499        m.ppu_write(0x0008, 0x66);
4500        let blob = m.save_state();
4501        let mut m2 = Multicart231::new(synth_prg_16k(32), &[], Mirroring::Vertical).unwrap();
4502        m2.load_state(&blob).unwrap();
4503        assert_eq!(m2.current_mirroring(), Mirroring::Horizontal);
4504        assert_eq!(m2.ppu_read(0x0008), 0x66);
4505    }
4506
4507    #[test]
4508    fn m234_cnrom_mode_banks() {
4509        let mut m =
4510            Maxi15M234::new(synth_prg_32k(8), synth_chr_8k(16), Mirroring::Vertical).unwrap();
4511        // reg0 latch (CNROM mode, bit6 clear). Write $FF80 with value 0x05:
4512        //   reg0 = 0x05 -> prgBank = reg0 & 0x0F = 5.
4513        //   chrBank = ((reg0<<2)&0x3C) | ((reg1>>4)&3) = (0x14) | 0 = 20.
4514        m.cpu_write(0xFF80, 0x05);
4515        assert_eq!(m.cpu_read(0x8000), 5);
4516        // chrBank = ((0x05<<2)&0x3C) = 0x14 = 20; 16-bank ROM wraps to 4.
4517        assert_eq!(m.ppu_read(0x0000), 20 % 16);
4518        // reg1 sets CHR low bits via $FFE8 (value 0x10 -> (0x10>>4)&3 = 1).
4519        m.cpu_write(0xFFE8, 0x10);
4520        // chrBank = 0x14 | 0x01 = 0x15 = 21; wraps to 5.
4521        assert_eq!(m.ppu_read(0x0000), 0x15 % 16);
4522    }
4523
4524    #[test]
4525    fn m234_save_state_round_trip() {
4526        let mut m =
4527            Maxi15M234::new(synth_prg_32k(8), synth_chr_8k(16), Mirroring::Vertical).unwrap();
4528        m.cpu_write(0xFF80, 0x83); // reg0: horizontal (bit7), prg = 3
4529        m.cpu_write(0xFFE8, 0x20);
4530        let blob = m.save_state();
4531        let mut m2 =
4532            Maxi15M234::new(synth_prg_32k(8), synth_chr_8k(16), Mirroring::Vertical).unwrap();
4533        m2.load_state(&blob).unwrap();
4534        assert_eq!(m2.cpu_read(0x8000), 3);
4535        assert_eq!(m2.current_mirroring(), Mirroring::Horizontal);
4536    }
4537
4538    #[test]
4539    fn m225_address_decoded_and_scratch_ram() {
4540        let mut m =
4541            Multicart225::new(synth_prg_16k(16), synth_chr_8k(8), Mirroring::Vertical).unwrap();
4542        // nesdev decode A~[.HMO PPPP PPCC CCCC]: PRG = A6..A9, O(mode) = A10,
4543        // M(mirror) = A11, H = A14. A = 0x8080: PRG = (0x80>>6)&0xF = 2; O = 0 ->
4544        // 32K; M = 0 -> vertical; CHR = A&0x3F = 0.
4545        m.cpu_write(0x8080, 0);
4546        assert_eq!(m.cpu_read(0x8000), 2);
4547        assert_eq!(m.cpu_read(0xC000), 3);
4548        assert_eq!(m.current_mirroring(), Mirroring::Vertical);
4549        // Scratch RAM round-trips low nibble.
4550        m.cpu_write(0x5800, 0xA9);
4551        assert_eq!(m.cpu_read(0x5800), 0x09);
4552    }
4553
4554    #[test]
4555    fn m225_save_state_round_trip() {
4556        let mut m =
4557            Multicart225::new(synth_prg_16k(16), synth_chr_8k(8), Mirroring::Vertical).unwrap();
4558        m.cpu_write(0x9180, 0); // some bank
4559        m.cpu_write(0x5803, 0x05);
4560        let blob = m.save_state();
4561        let mut m2 =
4562            Multicart225::new(synth_prg_16k(16), synth_chr_8k(8), Mirroring::Vertical).unwrap();
4563        m2.load_state(&blob).unwrap();
4564        assert_eq!(m2.cpu_read(0x8000), m.cpu_read(0x8000));
4565        assert_eq!(m2.cpu_read(0x5803), 0x05);
4566    }
4567
4568    #[test]
4569    fn m226_two_regs_select_prg_and_mirror() {
4570        let mut m = Multicart226::new(synth_prg_16k(16), &[], Mirroring::Vertical).unwrap();
4571        // reg0 (even) [PMOP PPPP]: low bits = 3, O (bit 5) = 16K mode, M (bit
4572        // 6) = 0 = horizontal. value 0b0010_0011 = 0x23.
4573        m.cpu_write(0x8000, 0x23);
4574        // reg1 (odd): bit0 = 0.
4575        m.cpu_write(0x8001, 0x00);
4576        // 16K mode: bank 3 on both halves.
4577        assert_eq!(m.cpu_read(0x8000), 3);
4578        assert_eq!(m.cpu_read(0xC000), 3);
4579        assert_eq!(m.current_mirroring(), Mirroring::Horizontal);
4580    }
4581
4582    #[test]
4583    fn m226_reg0_layout_is_pmop_pppp() {
4584        // NESdev `INES_Mapper_226`: $8000 = [PMOP PPPP] -- bit 7 is PRG bit 5,
4585        // bit 6 mirroring (1 = vertical), bit 5 the PRG mode (1 = two 16 KiB
4586        // halves of the same bank), bits 4-0 PRG bits 4-0. $8001 bit 0 is
4587        // PRG bit 6.
4588        let mut m = Multicart226::new(synth_prg_16k(128), &[], Mirroring::Vertical).unwrap();
4589        // $A3 = P1 M0 O1 P00011: bank 32 + 3 = 35, 16 KiB mode, horizontal.
4590        m.cpu_write(0x8000, 0xA3);
4591        assert_eq!(m.cpu_read(0x8000), 35);
4592        assert_eq!(m.cpu_read(0xC000), 35);
4593        assert_eq!(m.current_mirroring(), Mirroring::Horizontal);
4594        // $23 = P0 M0 O1 P00011: the mode bit is not a bank bit -- bank 3.
4595        m.cpu_write(0x8000, 0x23);
4596        assert_eq!(m.cpu_read(0x8000), 3);
4597        // $83 = P1 M0 O0 P00011: bank 35 in 32 KiB mode = 34 / 35.
4598        m.cpu_write(0x8000, 0x83);
4599        assert_eq!(m.cpu_read(0x8000), 34);
4600        assert_eq!(m.cpu_read(0xC000), 35);
4601        assert_eq!(m.current_mirroring(), Mirroring::Horizontal);
4602        // $45 + $8001 = 1: bank 64 + 5 = 69, 32 KiB mode (68 / 69), vertical.
4603        m.cpu_write(0x8000, 0x45);
4604        m.cpu_write(0x8001, 0x01);
4605        assert_eq!(m.cpu_read(0x8000), 68);
4606        assert_eq!(m.cpu_read(0xC000), 69);
4607        assert_eq!(m.current_mirroring(), Mirroring::Vertical);
4608        // "The multicart clears both registers on soft reset": bank 0, 32 KiB.
4609        m.reset();
4610        assert_eq!(m.cpu_read(0x8000), 0);
4611        assert_eq!(m.cpu_read(0xC000), 1);
4612    }
4613
4614    #[test]
4615    fn m226_save_state_round_trip() {
4616        let mut m = Multicart226::new(synth_prg_16k(16), &[], Mirroring::Vertical).unwrap();
4617        m.cpu_write(0x8000, 0x85); // 32K mode, low = 5
4618        m.cpu_write(0x8001, 0x00);
4619        m.ppu_write(0x0001, 0x66);
4620        let blob = m.save_state();
4621        let mut m2 = Multicart226::new(synth_prg_16k(16), &[], Mirroring::Vertical).unwrap();
4622        m2.load_state(&blob).unwrap();
4623        assert_eq!(m2.cpu_read(0x8000), m.cpu_read(0x8000));
4624        assert_eq!(m2.ppu_read(0x0001), 0x66);
4625    }
4626
4627    #[test]
4628    fn m227_address_decoded_bank() {
4629        let mut m = Multicart227::new(synth_prg_16k(16), &[], Mirroring::Vertical).unwrap();
4630        // A = 0x8008: prg_bank = (0x8008>>2)&0x1F = 2; s=(A&1)=0, prg_mode=
4631        // (A>>7)&1=0, l=(A>>9)&1=0, mirror=(A&2)=0 -> V. UNROM-like, s=0,l=0:
4632        // $8000 = bank 2, $C000 = bank & 0x38 = 0.
4633        m.cpu_write(0x8008, 0);
4634        assert_eq!(m.cpu_read(0x8000), 2);
4635        assert_eq!(m.cpu_read(0xC000), 0);
4636        assert_eq!(m.current_mirroring(), Mirroring::Vertical);
4637        // l_flag set (A bit 9 = 0x200): $C000 fixed to bank | 0x07.
4638        // A = 0x8208: prg_bank still 2, l=1 -> $C000 = 2 | 7 = 7.
4639        m.cpu_write(0x8208, 0);
4640        assert_eq!(m.cpu_read(0x8000), 2);
4641        assert_eq!(m.cpu_read(0xC000), 7);
4642        // prg_mode set without s (A bit 7 = 0x80): NROM-128, both halves = bank.
4643        // A = 0x8088: prg_bank = (0x8088>>2)&0x1F = 2; prg_mode=1, s=0.
4644        m.cpu_write(0x8088, 0);
4645        assert_eq!(m.cpu_read(0x8000), 2);
4646        assert_eq!(m.cpu_read(0xC000), 2);
4647    }
4648
4649    #[test]
4650    fn m227_save_state_round_trip() {
4651        let mut m = Multicart227::new(synth_prg_16k(16), &[], Mirroring::Vertical).unwrap();
4652        m.cpu_write(0x808B, 0); // prg_mode + s (32K pair) + A&2 -> H
4653        m.ppu_write(0x0002, 0x33);
4654        let blob = m.save_state();
4655        let mut m2 = Multicart227::new(synth_prg_16k(16), &[], Mirroring::Vertical).unwrap();
4656        m2.load_state(&blob).unwrap();
4657        assert_eq!(m2.cpu_read(0x8000), m.cpu_read(0x8000));
4658        assert_eq!(m2.ppu_read(0x0002), 0x33);
4659    }
4660
4661    #[test]
4662    fn m229_menu_bank_and_game_bank() {
4663        let mut m =
4664            Multicart229::new(synth_prg_16k(16), synth_chr_8k(16), Mirroring::Vertical).unwrap();
4665        // A with low 5 bits zero -> menu (fixed NROM-32 bank 0).
4666        m.cpu_write(0x8000, 0);
4667        assert_eq!(m.cpu_read(0x8000), 0);
4668        assert_eq!(m.cpu_read(0xC000), 1);
4669        // A = 0x8003: latch = 3 (non-menu) -> 16K bank 3 on both halves.
4670        m.cpu_write(0x8003, 0);
4671        assert_eq!(m.cpu_read(0x8000), 3);
4672        assert_eq!(m.cpu_read(0xC000), 3);
4673    }
4674
4675    #[test]
4676    fn m229_save_state_round_trip() {
4677        let mut m =
4678            Multicart229::new(synth_prg_16k(16), synth_chr_8k(16), Mirroring::Vertical).unwrap();
4679        m.cpu_write(0x8025, 0); // latch with chr + mirror H
4680        let blob = m.save_state();
4681        let mut m2 =
4682            Multicart229::new(synth_prg_16k(16), synth_chr_8k(16), Mirroring::Vertical).unwrap();
4683        m2.load_state(&blob).unwrap();
4684        assert_eq!(m2.cpu_read(0x8000), m.cpu_read(0x8000));
4685        assert_eq!(m2.current_mirroring(), m.current_mirroring());
4686    }
4687
4688    #[test]
4689    fn m233_bank_and_mirror_modes() {
4690        let mut m = Multicart233::new(synth_prg_16k(16), &[], Mirroring::Vertical).unwrap();
4691        // puNES: bit 5 SET = 16 KiB mode (one bank mirrored to both halves),
4692        // bits 6-7 = mirroring. value 0xA5 = MM=10 (horizontal), bit5=1 (16K),
4693        // bank = 0x05.
4694        m.cpu_write(0x8000, 0xA5);
4695        assert_eq!(m.cpu_read(0x8000), 5);
4696        assert_eq!(m.cpu_read(0xC000), 5);
4697        assert_eq!(m.current_mirroring(), Mirroring::Horizontal);
4698        // bit 5 CLEAR = 32 KiB mode: the pair at (bank>>1)<<1. value 0x05 ->
4699        // bank 5, 32K pair = banks 4 ($8000) / 5 ($C000).
4700        m.cpu_write(0x8000, 0x05);
4701        assert_eq!(m.cpu_read(0x8000), 4);
4702        assert_eq!(m.cpu_read(0xC000), 5);
4703    }
4704
4705    #[test]
4706    fn m233_save_state_round_trip() {
4707        let mut m = Multicart233::new(synth_prg_16k(16), &[], Mirroring::Vertical).unwrap();
4708        m.cpu_write(0x8000, 0x26); // bit5=1 -> 16K mode, bank 6
4709        m.ppu_write(0x0004, 0x88);
4710        let blob = m.save_state();
4711        let mut m2 = Multicart233::new(synth_prg_16k(16), &[], Mirroring::Vertical).unwrap();
4712        m2.load_state(&blob).unwrap();
4713        assert_eq!(m2.cpu_read(0x8000), m.cpu_read(0x8000));
4714        assert_eq!(m2.ppu_read(0x0004), 0x88);
4715    }
4716
4717    #[test]
4718    fn m46_outer_inner_prg() {
4719        let mut m = new_m46(synth_prg_32k(8), synth_chr_8k(8), Mirroring::Vertical).unwrap();
4720        m.cpu_write(0x6000, 0x02); // outer.
4721        m.cpu_write(0x8000, 0x01); // inner.
4722        // bank = (2 << 1) | 1 = 5.
4723        assert_eq!(m.cpu_read(0x8000), 5);
4724    }
4725
4726    #[test]
4727    fn m104_golden_five_blocks() {
4728        let mut m = new_m104(synth_prg_16k(32), synth_chr_8k(1), Mirroring::Vertical).unwrap();
4729        m.cpu_write(0x8000, 0x08 | 0x02); // block bits -> reg0 high = 0x20.
4730        m.cpu_write(0xC000, 0x05);
4731        assert_eq!(m.cpu_read(0x8000), 37 % 32); // inner ((0x20)|5) = 37.
4732        assert_eq!(m.cpu_read(0xC000), 47 % 32); // high|0x0F = 47.
4733    }
4734
4735    #[test]
4736    fn m290_address_decoded_mirror() {
4737        let mut m = new_m290(synth_prg_16k(16), synth_chr_8k(16), Mirroring::Vertical).unwrap();
4738        m.cpu_write(0x8400, 0); // bit 0x400 -> horizontal.
4739        assert_eq!(m.current_mirroring(), Mirroring::Horizontal);
4740    }
4741
4742    #[test]
4743    fn m301_address_as_data_mirror() {
4744        let mut m = new_m301(synth_prg_16k(16), synth_chr_8k(1), Mirroring::Vertical).unwrap();
4745        m.cpu_write(0x8002, 0); // addr bit 1 -> horizontal.
4746        assert_eq!(m.current_mirroring(), Mirroring::Horizontal);
4747    }
4748
4749    #[test]
4750    fn discrete_save_state_round_trip() {
4751        let mut m = new_m51(synth_prg_8k(64), Box::new([]), Mirroring::Vertical).unwrap();
4752        m.cpu_write(0x6000, 0x10);
4753        m.cpu_write(0xC000, 0x05);
4754        m.ppu_write(0x0020, 0x7F);
4755        let blob = m.save_state();
4756        let mut m2 = new_m51(synth_prg_8k(64), Box::new([]), Mirroring::Vertical).unwrap();
4757        m2.load_state(&blob).unwrap();
4758        assert_eq!(m2.ppu_read(0x0020), 0x7F);
4759        assert_eq!(m2.cpu_read(0x8000), m.cpu_read(0x8000));
4760    }
4761
4762    fn prg(banks_8k: usize) -> Box<[u8]> {
4763        // Fill each 8 KiB bank with its index so bank routing is observable.
4764        let mut v = vec![0u8; banks_8k * PRG_BANK_8K];
4765        for (i, b) in v.chunks_mut(PRG_BANK_8K).enumerate() {
4766            b.fill(i as u8);
4767        }
4768        v.into_boxed_slice()
4769    }
4770
4771    fn chr(banks_8k: usize) -> Box<[u8]> {
4772        let mut v = vec![0u8; banks_8k * CHR_BANK_8K];
4773        for (i, b) in v.chunks_mut(CHR_BANK_8K).enumerate() {
4774            b.fill(i as u8);
4775        }
4776        v.into_boxed_slice()
4777    }
4778
4779    #[test]
4780    fn m299_load_state_rejects_truncated_and_bad_version() {
4781        let m = new_m299(prg(8), chr(8), Mirroring::Horizontal).unwrap();
4782        let mut s = m.save_state();
4783        // Truncate.
4784        let mut t = m.save_state();
4785        t.pop();
4786        let mut m2 = new_m299(prg(8), chr(8), Mirroring::Horizontal).unwrap();
4787        assert!(matches!(
4788            m2.load_state(&t),
4789            Err(MapperError::WrongLength { .. })
4790        ));
4791        // Bad version.
4792        s[0] = 0xFF;
4793        assert!(matches!(
4794            m2.load_state(&s),
4795            Err(MapperError::UnsupportedVersion(0xFF))
4796        ));
4797    }
4798
4799    #[test]
4800    fn m204_address_decode_selects_prg_and_chr() {
4801        let mut m = new_m204(prg(16), chr(8), Mirroring::Vertical).unwrap();
4802        // bitMask = addr&6 = 0; page = 0 + (addr&1) = 0.
4803        m.cpu_write(0x8000, 0);
4804        assert_eq!(m.cpu_read(0x8000), 0);
4805        assert_eq!(m.cpu_read(0xC000), 0); // prg1 = 0 + (addr&1) = 0
4806        // addr 0x8007: bitMask = 6 -> page = 6+0 = 6; prg1 = 6+1 = 7.
4807        m.cpu_write(0x8007, 0);
4808        assert_eq!(m.cpu_read(0x8000), 12, "16k page 6 -> 8k bank 12");
4809    }
4810
4811    #[test]
4812    fn m204_distinct_halves_in_bitmask6_mode() {
4813        let mut m = new_m204(prg(16), chr(8), Mirroring::Vertical).unwrap();
4814        m.cpu_write(0x8006, 0); // bitMask 6: prg0=6, prg1=7 (16 KiB pages)
4815        // 16 KiB page 6 => 8 KiB bank 12 at $8000.
4816        assert_eq!(m.cpu_read(0x8000), 12);
4817        // 16 KiB page 7 => 8 KiB bank 14 at $C000.
4818        assert_eq!(m.cpu_read(0xC000), 14);
4819        assert_eq!(m.current_mirroring(), Mirroring::Vertical);
4820    }
4821
4822    #[test]
4823    fn m204_mirroring_bit() {
4824        let mut m = new_m204(prg(4), chr(2), Mirroring::Vertical).unwrap();
4825        m.cpu_write(0x8010, 0);
4826        assert_eq!(m.current_mirroring(), Mirroring::Horizontal);
4827        m.cpu_write(0x8000, 0);
4828        assert_eq!(m.current_mirroring(), Mirroring::Vertical);
4829    }
4830
4831    #[test]
4832    fn m299_value_decode_selects_prg_chr_mirror() {
4833        let mut m = new_m299(prg(8 * 4), chr(32), Mirroring::Horizontal).unwrap();
4834        // 32 KiB PRG: 4 banks of 32 KiB (32 8-KiB banks / 4). value 0x10:
4835        // bank = (0x10>>4)&7 = 1; chr8 = (1<<2)|0 = 4; bit7 clear => horizontal.
4836        m.cpu_write(0x8000, 0x10);
4837        assert_eq!(m.cpu_read(0x8000), 4, "32k bank 1 -> 8k bank 4");
4838        assert_eq!(m.ppu_read(0x0000), 4, "chr 8k bank 4");
4839        assert_eq!(m.current_mirroring(), Mirroring::Horizontal);
4840    }
4841
4842    #[test]
4843    fn m299_chr_low_bits_and_mirror() {
4844        let mut m = new_m299(prg(8 * 2), chr(16), Mirroring::Horizontal).unwrap();
4845        // value 0x83: bank = 0; chr8 = (0<<2)|3 = 3; bit7 set => vertical.
4846        m.cpu_write(0xFFFF, 0x83);
4847        assert_eq!(m.cpu_read(0x8000), 0, "32k bank 0");
4848        assert_eq!(m.ppu_read(0x0000), 3, "chr 8k bank 3");
4849        assert_eq!(m.current_mirroring(), Mirroring::Vertical);
4850    }
4851
4852    #[test]
4853    fn m204_m299_save_load_round_trip() {
4854        // m204
4855        let mut a = new_m204(prg(16), chr(8), Mirroring::Vertical).unwrap();
4856        a.cpu_write(0x8006, 0);
4857        let s = a.save_state();
4858        let mut b = new_m204(prg(16), chr(8), Mirroring::Horizontal).unwrap();
4859        b.load_state(&s).unwrap();
4860        assert_eq!(a.cpu_read(0xC000), b.cpu_read(0xC000));
4861        assert_eq!(a.current_mirroring(), b.current_mirroring());
4862
4863        // m299
4864        let mut a = new_m299(prg(8 * 4), chr(32), Mirroring::Horizontal).unwrap();
4865        a.cpu_write(0x8000, 0x91);
4866        let s = a.save_state();
4867        let mut b = new_m299(prg(8 * 4), chr(32), Mirroring::Horizontal).unwrap();
4868        b.load_state(&s).unwrap();
4869        assert_eq!(a.cpu_read(0x8000), b.cpu_read(0x8000));
4870        assert_eq!(a.ppu_read(0x0000), b.ppu_read(0x0000));
4871        assert_eq!(a.current_mirroring(), b.current_mirroring());
4872    }
4873
4874    #[test]
4875    fn m204_m299_bad_prg_size_is_rejected() {
4876        // 100 bytes is not a multiple of 8 KiB.
4877        assert!(
4878            new_m204(
4879                vec![0u8; 100].into_boxed_slice(),
4880                chr(1),
4881                Mirroring::Vertical
4882            )
4883            .is_err()
4884        );
4885        assert!(
4886            new_m299(
4887                vec![0u8; 100].into_boxed_slice(),
4888                chr(1),
4889                Mirroring::Vertical
4890            )
4891            .is_err()
4892        );
4893    }
4894
4895    #[test]
4896    fn m15_refuses_a_pre_prg_ram_save_state() {
4897        // The PRG-RAM tail is new in v2.3.4 and carries the SAME version byte,
4898        // so an old slot is distinguishable only by length. It loaded with the
4899        // RAM cleared until v2.9.8 (ADR 0042), which reads the full layout only.
4900        let mut m = Multicart15::new(synth_prg_16k(8), &[]).unwrap();
4901        m.cpu_write(0x6000, 0x5A);
4902        let current = m.save_state();
4903
4904        // Legacy form: identical bytes minus the PRG-RAM tail.
4905        let legacy = current[..current.len() - 0x2000].to_vec();
4906        let mut m2 = Multicart15::new(synth_prg_16k(8), &[]).unwrap();
4907        m2.cpu_write(0x6000, 0xFF);
4908        assert!(matches!(
4909            m2.load_state(&legacy),
4910            Err(MapperError::WrongLength { .. })
4911        ));
4912
4913        // And the current form still round-trips its contents.
4914        let mut m3 = Multicart15::new(synth_prg_16k(8), &[]).unwrap();
4915        m3.load_state(&current).unwrap();
4916        assert_eq!(m3.cpu_read(0x6000), 0x5A);
4917
4918        // A length that is neither is still rejected.
4919        let mut bad = current.clone();
4920        bad.pop();
4921        assert!(m3.load_state(&bad).is_err());
4922    }
4923}