Skip to main content

rustynes_mappers/
ntdec.rs

1// SPDX-License-Identifier: GPL-3.0-or-later
2//
3// Provenance: the NTDEC boards are derived from Mesen2 (GPL-3.0-or-later), `Ntdec/NtdecTc112.h` (mapper 193), `Unlicensed/Mapper204.h` (mapper 204) and `Txc/Bmc11160.h` (mapper 299). See docs/originality-and-provenance.md (Section 1)
4// and NOTICE for the complete, audited derivation record.
5//! NTDEC boards decoded from the address bus: mappers 63 and 174.
6//!
7//! NTDEC's multicart designs consistently push the bank selection into the
8//! *address* of the write rather than its data -- the cartridge decodes which
9//! address in `$8000-$FFFF` was touched and banks accordingly. That costs
10//! nothing in discrete logic (the address lines are already there) and needs
11//! no data-bus buffer, which is why so many cheap multicarts work this way.
12//!
13//! NTDEC's later ASIC-based boards are in this module too as they are added;
14//! see also `sachen_8259.rs` for the comparable Sachen family.
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_8K: usize = 0x2000;
45const PRG_BANK_16K: usize = 0x4000;
46const CHR_BANK_2K: usize = 0x0800;
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
64const CHR_BANK_1K: usize = 0x0400;
65
66const fn byte_to_mirroring(b: u8, fallback: Mirroring) -> Mirroring {
67    match b {
68        0 => Mirroring::Horizontal,
69        1 => Mirroring::Vertical,
70        2 => Mirroring::SingleScreenA,
71        3 => Mirroring::SingleScreenB,
72        4 => Mirroring::FourScreen,
73        5 => Mirroring::MapperControlled,
74        _ => fallback,
75    }
76}
77
78const fn mirroring_to_byte(m: Mirroring) -> u8 {
79    match m {
80        Mirroring::Horizontal => 0,
81        Mirroring::Vertical => 1,
82        Mirroring::SingleScreenA => 2,
83        Mirroring::SingleScreenB => 3,
84        Mirroring::FourScreen => 4,
85        Mirroring::MapperControlled => 5,
86    }
87}
88
89/// Mapper 63 (NTDEC `0324` multicart).
90pub struct Ntdec63 {
91    prg_rom: Box<[u8]>,
92    chr_ram: Box<[u8]>,
93    vram: Box<[u8]>,
94    prg_bank: u8,
95    prg32_mode: bool,
96    horizontal_mirroring: bool,
97}
98
99impl Ntdec63 {
100    /// Construct a new mapper 63 board.
101    ///
102    /// # Errors
103    ///
104    /// Returns [`MapperError::Invalid`] when PRG is not a non-zero multiple of
105    /// 16 KiB.
106    pub fn new(
107        prg_rom: Box<[u8]>,
108        _chr_rom: &[u8],
109        _mirroring: Mirroring,
110    ) -> Result<Self, MapperError> {
111        if prg_rom.is_empty() || !prg_rom.len().is_multiple_of(PRG_BANK_16K) {
112            return Err(MapperError::Invalid(format!(
113                "mapper 63 PRG-ROM size {} is not a non-zero multiple of 16 KiB",
114                prg_rom.len()
115            )));
116        }
117        Ok(Self {
118            prg_rom,
119            chr_ram: vec![0u8; CHR_BANK_8K].into_boxed_slice(),
120            vram: vec![0u8; 2 * NAMETABLE_SIZE].into_boxed_slice(),
121            prg_bank: 0,
122            prg32_mode: true,
123            horizontal_mirroring: false,
124        })
125    }
126
127    fn read_prg(&self, bank16: usize, addr: u16) -> u8 {
128        let count = (self.prg_rom.len() / PRG_BANK_16K).max(1);
129        let bank = bank16 % count;
130        self.prg_rom[bank * PRG_BANK_16K + (addr as usize & 0x3FFF)]
131    }
132}
133
134impl Mapper for Ntdec63 {
135    fn caps(&self) -> MapperCaps {
136        MapperCaps::NONE
137    }
138
139    fn cpu_read(&mut self, addr: u16) -> u8 {
140        match addr {
141            0x8000..=0xBFFF => {
142                let base = (self.prg_bank as usize) & if self.prg32_mode { !1 } else { !0 };
143                self.read_prg(base, addr)
144            }
145            0xC000..=0xFFFF => {
146                let base = self.prg_bank as usize;
147                let bank = if self.prg32_mode {
148                    (base & !1) | 1
149                } else {
150                    base
151                };
152                self.read_prg(bank, addr)
153            }
154            _ => 0,
155        }
156    }
157
158    fn cpu_write(&mut self, addr: u16, _value: u8) {
159        if (0x8000..=0xFFFF).contains(&addr) {
160            self.prg_bank = ((addr >> 2) & 0x3F) as u8;
161            self.prg32_mode = (addr & 0x02) == 0;
162            self.horizontal_mirroring = (addr & 0x01) != 0;
163        }
164    }
165
166    fn ppu_read(&mut self, addr: u16) -> u8 {
167        let addr = addr & 0x3FFF;
168        match addr {
169            0x0000..=0x1FFF => self.chr_ram[addr as usize],
170            0x2000..=0x3EFF => self.vram[nametable_offset(addr, self.current_mirroring())],
171            _ => 0,
172        }
173    }
174
175    fn ppu_write(&mut self, addr: u16, value: u8) {
176        let addr = addr & 0x3FFF;
177        match addr {
178            0x0000..=0x1FFF => self.chr_ram[addr as usize] = value,
179            0x2000..=0x3EFF => {
180                let off = nametable_offset(addr, self.current_mirroring());
181                self.vram[off] = value;
182            }
183            _ => {}
184        }
185    }
186
187    fn current_mirroring(&self) -> Mirroring {
188        if self.horizontal_mirroring {
189            Mirroring::Horizontal
190        } else {
191            Mirroring::Vertical
192        }
193    }
194
195    fn save_state(&self) -> Vec<u8> {
196        let mut out = Vec::with_capacity(4 + self.vram.len() + self.chr_ram.len());
197        out.push(SAVE_STATE_VERSION);
198        out.push(self.prg_bank);
199        out.push(u8::from(self.prg32_mode));
200        out.push(u8::from(self.horizontal_mirroring));
201        out.extend_from_slice(&self.vram);
202        out.extend_from_slice(&self.chr_ram);
203        out
204    }
205
206    fn load_state(&mut self, data: &[u8]) -> Result<(), MapperError> {
207        let expected = 4 + self.vram.len() + self.chr_ram.len();
208        if data.len() != expected {
209            return Err(MapperError::WrongLength {
210                expected,
211                got: data.len(),
212            });
213        }
214        if data[0] != SAVE_STATE_VERSION {
215            return Err(MapperError::UnsupportedVersion(data[0]));
216        }
217        self.prg_bank = data[1];
218        self.prg32_mode = data[2] != 0;
219        self.horizontal_mirroring = data[3] != 0;
220        let mut cursor = 4;
221        self.vram
222            .copy_from_slice(&data[cursor..cursor + self.vram.len()]);
223        cursor += self.vram.len();
224        self.chr_ram
225            .copy_from_slice(&data[cursor..cursor + self.chr_ram.len()]);
226        Ok(())
227    }
228}
229
230/// Mapper 174 (NTDEC `5-in-1`).
231pub struct Ntdec174 {
232    prg_rom: Box<[u8]>,
233    chr_rom: Box<[u8]>,
234    vram: Box<[u8]>,
235    prg_bank: u8,
236    prg16_mode: bool,
237    chr_bank: u8,
238    horizontal_mirroring: bool,
239}
240
241impl Ntdec174 {
242    /// Construct a new mapper 174 board.
243    ///
244    /// # Errors
245    ///
246    /// Returns [`MapperError::Invalid`] when PRG is not a non-zero multiple of
247    /// 16 KiB or CHR-ROM is empty / not a multiple of 8 KiB.
248    pub fn new(
249        prg_rom: Box<[u8]>,
250        chr_rom: Box<[u8]>,
251        _mirroring: Mirroring,
252    ) -> Result<Self, MapperError> {
253        if prg_rom.is_empty() || !prg_rom.len().is_multiple_of(PRG_BANK_16K) {
254            return Err(MapperError::Invalid(format!(
255                "mapper 174 PRG-ROM size {} is not a non-zero multiple of 16 KiB",
256                prg_rom.len()
257            )));
258        }
259        if chr_rom.is_empty() || !chr_rom.len().is_multiple_of(CHR_BANK_8K) {
260            return Err(MapperError::Invalid(format!(
261                "mapper 174 CHR-ROM size {} is not a non-zero multiple of 8 KiB",
262                chr_rom.len()
263            )));
264        }
265        Ok(Self {
266            prg_rom,
267            chr_rom,
268            vram: vec![0u8; 2 * NAMETABLE_SIZE].into_boxed_slice(),
269            prg_bank: 0,
270            prg16_mode: false,
271            chr_bank: 0,
272            horizontal_mirroring: false,
273        })
274    }
275
276    fn read_prg(&self, bank16: usize, addr: u16) -> u8 {
277        let count = (self.prg_rom.len() / PRG_BANK_16K).max(1);
278        let bank = bank16 % count;
279        self.prg_rom[bank * PRG_BANK_16K + (addr as usize & 0x3FFF)]
280    }
281}
282
283impl Mapper for Ntdec174 {
284    fn caps(&self) -> MapperCaps {
285        MapperCaps::NONE
286    }
287
288    fn cpu_read(&mut self, addr: u16) -> u8 {
289        match addr {
290            0x8000..=0xBFFF => {
291                let base = if self.prg16_mode {
292                    self.prg_bank as usize
293                } else {
294                    (self.prg_bank as usize) & !1
295                };
296                self.read_prg(base, addr)
297            }
298            0xC000..=0xFFFF => {
299                let bank = if self.prg16_mode {
300                    self.prg_bank as usize
301                } else {
302                    ((self.prg_bank as usize) & !1) | 1
303                };
304                self.read_prg(bank, addr)
305            }
306            _ => 0,
307        }
308    }
309
310    fn cpu_write(&mut self, addr: u16, _value: u8) {
311        if (0x8000..=0xFFFF).contains(&addr) {
312            self.prg_bank = ((addr >> 4) & 0x07) as u8;
313            self.prg16_mode = (addr & 0x80) != 0;
314            self.chr_bank = ((addr >> 1) & 0x07) as u8;
315            self.horizontal_mirroring = (addr & 0x01) != 0;
316        }
317    }
318
319    fn ppu_read(&mut self, addr: u16) -> u8 {
320        let addr = addr & 0x3FFF;
321        match addr {
322            0x0000..=0x1FFF => {
323                let count = (self.chr_rom.len() / CHR_BANK_8K).max(1);
324                let bank = (self.chr_bank as usize) % count;
325                self.chr_rom[bank * CHR_BANK_8K + addr as usize]
326            }
327            0x2000..=0x3EFF => self.vram[nametable_offset(addr, self.current_mirroring())],
328            _ => 0,
329        }
330    }
331
332    fn ppu_write(&mut self, addr: u16, value: u8) {
333        let addr = addr & 0x3FFF;
334        if let 0x2000..=0x3EFF = addr {
335            let off = nametable_offset(addr, self.current_mirroring());
336            self.vram[off] = value;
337        }
338    }
339
340    fn current_mirroring(&self) -> Mirroring {
341        if self.horizontal_mirroring {
342            Mirroring::Horizontal
343        } else {
344            Mirroring::Vertical
345        }
346    }
347
348    fn save_state(&self) -> Vec<u8> {
349        let mut out = Vec::with_capacity(5 + self.vram.len());
350        out.push(SAVE_STATE_VERSION);
351        out.push(self.prg_bank);
352        out.push(u8::from(self.prg16_mode));
353        out.push(self.chr_bank);
354        out.push(u8::from(self.horizontal_mirroring));
355        out.extend_from_slice(&self.vram);
356        out
357    }
358
359    fn load_state(&mut self, data: &[u8]) -> Result<(), MapperError> {
360        let expected = 5 + self.vram.len();
361        if data.len() != expected {
362            return Err(MapperError::WrongLength {
363                expected,
364                got: data.len(),
365            });
366        }
367        if data[0] != SAVE_STATE_VERSION {
368            return Err(MapperError::UnsupportedVersion(data[0]));
369        }
370        self.prg_bank = data[1];
371        self.prg16_mode = data[2] != 0;
372        self.chr_bank = data[3];
373        self.horizontal_mirroring = data[4] != 0;
374        self.vram.copy_from_slice(&data[5..5 + self.vram.len()]);
375        Ok(())
376    }
377}
378
379/// Mapper 40 (NTDEC 2722, *SMB2J* pirate).
380pub struct Ntdec2722M40 {
381    prg_rom: Box<[u8]>,
382    chr_ram: Box<[u8]>,
383    vram: Box<[u8]>,
384    switch_bank: u8,
385    irq_enabled: bool,
386    irq_counter: u16,
387    irq_pending: bool,
388    mirroring: Mirroring,
389}
390
391impl Ntdec2722M40 {
392    /// Construct a new mapper 40 board.
393    ///
394    /// # Errors
395    ///
396    /// Returns [`MapperError::Invalid`] when PRG is not a non-zero multiple of
397    /// 8 KiB.
398    pub fn new(
399        prg_rom: Box<[u8]>,
400        _chr_rom: &[u8],
401        mirroring: Mirroring,
402    ) -> Result<Self, MapperError> {
403        if prg_rom.is_empty() || !prg_rom.len().is_multiple_of(PRG_BANK_8K) {
404            return Err(MapperError::Invalid(format!(
405                "mapper 40 PRG-ROM size {} is not a non-zero multiple of 8 KiB",
406                prg_rom.len()
407            )));
408        }
409        Ok(Self {
410            prg_rom,
411            chr_ram: vec![0u8; CHR_BANK_8K].into_boxed_slice(),
412            vram: vec![0u8; 2 * NAMETABLE_SIZE].into_boxed_slice(),
413            switch_bank: 0,
414            irq_enabled: false,
415            irq_counter: 0,
416            irq_pending: false,
417            mirroring,
418        })
419    }
420
421    fn read_prg(&self, bank: usize, addr: u16) -> u8 {
422        let count = (self.prg_rom.len() / PRG_BANK_8K).max(1);
423        let bank = bank % count;
424        self.prg_rom[bank * PRG_BANK_8K + (addr as usize & 0x1FFF)]
425    }
426}
427
428impl Mapper for Ntdec2722M40 {
429    fn caps(&self) -> MapperCaps {
430        MapperCaps::CYCLE_IRQ
431    }
432
433    fn cpu_read(&mut self, addr: u16) -> u8 {
434        match addr {
435            0x6000..=0x7FFF => self.read_prg(6, addr),
436            0x8000..=0x9FFF => self.read_prg(4, addr),
437            0xA000..=0xBFFF => self.read_prg(5, addr),
438            0xC000..=0xDFFF => self.read_prg(self.switch_bank as usize, addr),
439            0xE000..=0xFFFF => self.read_prg(7, addr),
440            _ => 0,
441        }
442    }
443
444    fn cpu_read_unmapped(&self, addr: u16) -> bool {
445        // $6000-$FFFF is mapped PRG; the $4020-$5FFF window is open bus.
446        (0x4020..=0x5FFF).contains(&addr)
447    }
448
449    fn cpu_write(&mut self, addr: u16, value: u8) {
450        match addr {
451            0x8000..=0x9FFF => {
452                // IRQ disable + acknowledge; counter held in reset.
453                self.irq_enabled = false;
454                self.irq_pending = false;
455                self.irq_counter = 0;
456            }
457            0xA000..=0xBFFF => self.irq_enabled = true,
458            0xE000..=0xFFFF => self.switch_bank = value & 0x07,
459            _ => {}
460        }
461    }
462
463    fn ppu_read(&mut self, addr: u16) -> u8 {
464        let addr = addr & 0x3FFF;
465        match addr {
466            0x0000..=0x1FFF => self.chr_ram[addr as usize],
467            0x2000..=0x3EFF => self.vram[nametable_offset(addr, self.mirroring)],
468            _ => 0,
469        }
470    }
471
472    fn ppu_write(&mut self, addr: u16, value: u8) {
473        let addr = addr & 0x3FFF;
474        match addr {
475            0x0000..=0x1FFF => self.chr_ram[addr as usize] = value,
476            0x2000..=0x3EFF => {
477                let off = nametable_offset(addr, self.mirroring);
478                self.vram[off] = value;
479            }
480            _ => {}
481        }
482    }
483
484    fn notify_cpu_cycle(&mut self) {
485        if !self.irq_enabled {
486            return;
487        }
488        // 12-bit M2 counter; asserts (and holds) at 4096.
489        if self.irq_counter >= 0x1000 {
490            self.irq_pending = true;
491        } else {
492            self.irq_counter += 1;
493            if self.irq_counter >= 0x1000 {
494                self.irq_pending = true;
495            }
496        }
497    }
498
499    fn irq_pending(&self) -> bool {
500        self.irq_pending
501    }
502
503    fn irq_acknowledge(&mut self) {
504        self.irq_pending = false;
505    }
506
507    fn current_mirroring(&self) -> Mirroring {
508        self.mirroring
509    }
510
511    fn save_state(&self) -> Vec<u8> {
512        let mut out = Vec::with_capacity(6 + self.vram.len() + self.chr_ram.len());
513        out.push(SAVE_STATE_VERSION);
514        out.push(self.switch_bank);
515        out.push(u8::from(self.irq_enabled));
516        out.push((self.irq_counter & 0xFF) as u8);
517        out.push((self.irq_counter >> 8) as u8);
518        out.push(u8::from(self.irq_pending));
519        out.extend_from_slice(&self.vram);
520        out.extend_from_slice(&self.chr_ram);
521        out
522    }
523
524    fn load_state(&mut self, data: &[u8]) -> Result<(), MapperError> {
525        let expected = 6 + self.vram.len() + self.chr_ram.len();
526        if data.len() != expected {
527            return Err(MapperError::WrongLength {
528                expected,
529                got: data.len(),
530            });
531        }
532        if data[0] != SAVE_STATE_VERSION {
533            return Err(MapperError::UnsupportedVersion(data[0]));
534        }
535        self.switch_bank = data[1];
536        self.irq_enabled = data[2] != 0;
537        self.irq_counter = u16::from(data[3]) | (u16::from(data[4]) << 8);
538        self.irq_pending = data[5] != 0;
539        let mut cursor = 6;
540        self.vram
541            .copy_from_slice(&data[cursor..cursor + self.vram.len()]);
542        cursor += self.vram.len();
543        self.chr_ram
544            .copy_from_slice(&data[cursor..cursor + self.chr_ram.len()]);
545        Ok(())
546    }
547}
548
549// ===========================================================================
550// Mapper 81 — NTDEC Super Gun (CNROM-like with a wider PRG select).
551//
552// A single $8000-$FFFF register; the written byte carries:
553//   bits 2-3 : 16 KiB PRG bank at $8000 (the $C000 half is fixed to the last).
554//   bits 0-1 : 8 KiB CHR bank.
555// Mirroring is header-fixed. No IRQ.
556// ===========================================================================
557
558/// Mapper 81 (NTDEC Super Gun).
559pub struct Ntdec81 {
560    prg_rom: Box<[u8]>,
561    chr_rom: Box<[u8]>,
562    vram: Box<[u8]>,
563    prg_bank: u8,
564    chr_bank: u8,
565    mirroring: Mirroring,
566}
567
568impl Ntdec81 {
569    /// Construct a new mapper 81 board.
570    ///
571    /// # Errors
572    ///
573    /// Returns [`MapperError::Invalid`] when PRG is not a non-zero multiple of
574    /// 16 KiB or CHR-ROM is empty / not a multiple of 8 KiB.
575    pub fn new(
576        prg_rom: Box<[u8]>,
577        chr_rom: Box<[u8]>,
578        mirroring: Mirroring,
579    ) -> Result<Self, MapperError> {
580        if prg_rom.is_empty() || !prg_rom.len().is_multiple_of(PRG_BANK_16K) {
581            return Err(MapperError::Invalid(format!(
582                "mapper 81 PRG-ROM size {} is not a non-zero multiple of 16 KiB",
583                prg_rom.len()
584            )));
585        }
586        if chr_rom.is_empty() || !chr_rom.len().is_multiple_of(CHR_BANK_8K) {
587            return Err(MapperError::Invalid(format!(
588                "mapper 81 CHR-ROM size {} is not a non-zero multiple of 8 KiB",
589                chr_rom.len()
590            )));
591        }
592        Ok(Self {
593            prg_rom,
594            chr_rom,
595            vram: vec![0u8; 2 * NAMETABLE_SIZE].into_boxed_slice(),
596            prg_bank: 0,
597            chr_bank: 0,
598            mirroring,
599        })
600    }
601}
602
603impl Mapper for Ntdec81 {
604    fn caps(&self) -> MapperCaps {
605        MapperCaps::NONE
606    }
607
608    fn cpu_read(&mut self, addr: u16) -> u8 {
609        let count = (self.prg_rom.len() / PRG_BANK_16K).max(1);
610        match addr {
611            0x8000..=0xBFFF => {
612                let bank = (self.prg_bank as usize) % count;
613                self.prg_rom[bank * PRG_BANK_16K + (addr as usize & 0x3FFF)]
614            }
615            0xC000..=0xFFFF => {
616                let last = count - 1;
617                self.prg_rom[last * PRG_BANK_16K + (addr as usize & 0x3FFF)]
618            }
619            _ => 0,
620        }
621    }
622
623    fn cpu_write(&mut self, addr: u16, value: u8) {
624        if (0x8000..=0xFFFF).contains(&addr) {
625            self.prg_bank = (value >> 2) & 0x03;
626            self.chr_bank = value & 0x03;
627        }
628    }
629
630    fn ppu_read(&mut self, addr: u16) -> u8 {
631        let addr = addr & 0x3FFF;
632        match addr {
633            0x0000..=0x1FFF => {
634                let count = (self.chr_rom.len() / CHR_BANK_8K).max(1);
635                let bank = (self.chr_bank as usize) % count;
636                self.chr_rom[bank * CHR_BANK_8K + (addr as usize & 0x1FFF)]
637            }
638            0x2000..=0x3EFF => self.vram[nametable_offset(addr, self.mirroring)],
639            _ => 0,
640        }
641    }
642
643    fn ppu_write(&mut self, addr: u16, value: u8) {
644        let addr = addr & 0x3FFF;
645        if (0x2000..=0x3EFF).contains(&addr) {
646            let off = nametable_offset(addr, self.mirroring);
647            self.vram[off] = value;
648        }
649    }
650
651    fn current_mirroring(&self) -> Mirroring {
652        self.mirroring
653    }
654
655    fn save_state(&self) -> Vec<u8> {
656        let mut out = Vec::with_capacity(3 + self.vram.len());
657        out.push(SAVE_STATE_VERSION);
658        out.push(self.prg_bank);
659        out.push(self.chr_bank);
660        out.extend_from_slice(&self.vram);
661        out
662    }
663
664    fn load_state(&mut self, data: &[u8]) -> Result<(), MapperError> {
665        let expected = 3 + self.vram.len();
666        if data.len() != expected {
667            return Err(MapperError::WrongLength {
668                expected,
669                got: data.len(),
670            });
671        }
672        if data[0] != SAVE_STATE_VERSION {
673            return Err(MapperError::UnsupportedVersion(data[0]));
674        }
675        self.prg_bank = data[1];
676        self.chr_bank = data[2];
677        self.vram.copy_from_slice(&data[3..3 + self.vram.len()]);
678        Ok(())
679    }
680}
681
682/// Mapper 112 (NTDEC ASDER / Huang-1).
683pub struct NtdecAsder112 {
684    prg_rom: Box<[u8]>,
685    chr_rom: Box<[u8]>,
686    vram: Box<[u8]>,
687    reg_index: u8,
688    prg_banks: [u8; 2],
689    // chr[0..2] = 2 KiB selects; chr[2..6] = 1 KiB selects.
690    chr_regs: [u8; 6],
691    horizontal_mirroring: bool,
692}
693
694impl NtdecAsder112 {
695    /// Construct a new mapper 112 board.
696    ///
697    /// # Errors
698    ///
699    /// Returns [`MapperError::Invalid`] when PRG is not a non-zero multiple of
700    /// 8 KiB or CHR-ROM is empty / not a multiple of 1 KiB.
701    pub fn new(
702        prg_rom: Box<[u8]>,
703        chr_rom: Box<[u8]>,
704        mirroring: Mirroring,
705    ) -> Result<Self, MapperError> {
706        if prg_rom.is_empty() || !prg_rom.len().is_multiple_of(PRG_BANK_8K) {
707            return Err(MapperError::Invalid(format!(
708                "mapper 112 PRG-ROM size {} is not a non-zero multiple of 8 KiB",
709                prg_rom.len()
710            )));
711        }
712        if chr_rom.is_empty() || !chr_rom.len().is_multiple_of(CHR_BANK_1K) {
713            return Err(MapperError::Invalid(format!(
714                "mapper 112 CHR-ROM size {} is not a non-zero multiple of 1 KiB",
715                chr_rom.len()
716            )));
717        }
718        Ok(Self {
719            prg_rom,
720            chr_rom,
721            vram: vec![0u8; 2 * NAMETABLE_SIZE].into_boxed_slice(),
722            reg_index: 0,
723            prg_banks: [0, 1],
724            chr_regs: [0; 6],
725            horizontal_mirroring: mirroring == Mirroring::Horizontal,
726        })
727    }
728
729    fn read_prg(&self, bank: usize, addr: u16) -> u8 {
730        let count = (self.prg_rom.len() / PRG_BANK_8K).max(1);
731        let bank = bank % count;
732        self.prg_rom[bank * PRG_BANK_8K + (addr as usize & 0x1FFF)]
733    }
734
735    fn read_chr(&self, addr: u16) -> u8 {
736        let count1k = (self.chr_rom.len() / CHR_BANK_1K).max(1);
737        let bank1k = match addr {
738            0x0000..=0x07FF => (self.chr_regs[0] as usize & !1) + ((addr as usize >> 10) & 1),
739            0x0800..=0x0FFF => (self.chr_regs[1] as usize & !1) + ((addr as usize >> 10) & 1),
740            0x1000..=0x13FF => self.chr_regs[2] as usize,
741            0x1400..=0x17FF => self.chr_regs[3] as usize,
742            0x1800..=0x1BFF => self.chr_regs[4] as usize,
743            _ => self.chr_regs[5] as usize,
744        };
745        let bank = bank1k % count1k;
746        self.chr_rom[bank * CHR_BANK_1K + (addr as usize & 0x03FF)]
747    }
748}
749
750impl Mapper for NtdecAsder112 {
751    fn caps(&self) -> MapperCaps {
752        MapperCaps::NONE
753    }
754
755    fn cpu_read(&mut self, addr: u16) -> u8 {
756        let last = (self.prg_rom.len() / PRG_BANK_8K).max(1) - 1;
757        match addr {
758            0x8000..=0x9FFF => self.read_prg(self.prg_banks[0] as usize, addr),
759            0xA000..=0xBFFF => self.read_prg(self.prg_banks[1] as usize, addr),
760            0xC000..=0xDFFF => self.read_prg(last - 1, addr),
761            0xE000..=0xFFFF => self.read_prg(last, addr),
762            _ => 0,
763        }
764    }
765
766    fn cpu_write(&mut self, addr: u16, value: u8) {
767        match addr & 0xE001 {
768            0x8000 => self.reg_index = value & 0x07,
769            0xA000 => match self.reg_index {
770                0 => self.prg_banks[0] = value,
771                1 => self.prg_banks[1] = value,
772                2 => self.chr_regs[0] = value,
773                3 => self.chr_regs[1] = value,
774                4 => self.chr_regs[2] = value,
775                5 => self.chr_regs[3] = value,
776                6 => self.chr_regs[4] = value,
777                _ => self.chr_regs[5] = value,
778            },
779            0xE000 => self.horizontal_mirroring = (value & 0x01) != 0,
780            _ => {}
781        }
782    }
783
784    fn ppu_read(&mut self, addr: u16) -> u8 {
785        let addr = addr & 0x3FFF;
786        match addr {
787            0x0000..=0x1FFF => self.read_chr(addr),
788            0x2000..=0x3EFF => self.vram[nametable_offset(addr, self.current_mirroring())],
789            _ => 0,
790        }
791    }
792
793    fn ppu_write(&mut self, addr: u16, value: u8) {
794        let addr = addr & 0x3FFF;
795        if (0x2000..=0x3EFF).contains(&addr) {
796            let off = nametable_offset(addr, self.current_mirroring());
797            self.vram[off] = value;
798        }
799    }
800
801    fn current_mirroring(&self) -> Mirroring {
802        if self.horizontal_mirroring {
803            Mirroring::Horizontal
804        } else {
805            Mirroring::Vertical
806        }
807    }
808
809    fn save_state(&self) -> Vec<u8> {
810        let mut out = Vec::with_capacity(11 + self.vram.len());
811        out.push(SAVE_STATE_VERSION);
812        out.push(self.reg_index);
813        out.extend_from_slice(&self.prg_banks);
814        out.extend_from_slice(&self.chr_regs);
815        out.push(u8::from(self.horizontal_mirroring));
816        out.extend_from_slice(&self.vram);
817        out
818    }
819
820    fn load_state(&mut self, data: &[u8]) -> Result<(), MapperError> {
821        let expected = 11 + self.vram.len();
822        if data.len() != expected {
823            return Err(MapperError::WrongLength {
824                expected,
825                got: data.len(),
826            });
827        }
828        if data[0] != SAVE_STATE_VERSION {
829            return Err(MapperError::UnsupportedVersion(data[0]));
830        }
831        self.reg_index = data[1];
832        self.prg_banks.copy_from_slice(&data[2..4]);
833        self.chr_regs.copy_from_slice(&data[4..10]);
834        self.horizontal_mirroring = data[10] != 0;
835        self.vram.copy_from_slice(&data[11..11 + self.vram.len()]);
836        Ok(())
837    }
838}
839
840/// Validate a PRG-ROM image is a non-zero multiple of 8 KiB.
841fn check_prg(prg: &[u8], id: u16) -> Result<(), MapperError> {
842    if prg.is_empty() || !prg.len().is_multiple_of(PRG_BANK_8K) {
843        return Err(MapperError::Invalid(format!(
844            "mapper {id} PRG-ROM size {} is not a non-zero multiple of 8 KiB",
845            prg.len()
846        )));
847    }
848    Ok(())
849}
850
851/// Allocate the CHR slice, falling back to an 8 KiB CHR-RAM bank when the ROM
852/// ships no CHR-ROM. Returns `(chr, is_ram)`.
853fn chr_or_ram(chr_rom: Box<[u8]>) -> (Box<[u8]>, bool) {
854    if chr_rom.is_empty() {
855        (vec![0u8; CHR_BANK_8K].into_boxed_slice(), true)
856    } else {
857        (chr_rom, false)
858    }
859}
860
861// ===========================================================================
862// NtdecTc112 (mapper 193) — NTDEC TC-112 (*Fighting Hero*).
863//
864// PRG: 8 KiB pages. The last three 8 KiB windows ($A000/$C000/$E000) are fixed
865// to the final three banks; $8000 is the one switchable window (register 3).
866// CHR: 2 KiB pages. Register 0 selects a paired 2 KiB window into the first two
867// slots ($0000 + $0800), register 1 the third ($1000), register 2 the fourth
868// ($1800). Registers live at $6000-$7FFF (addr & 3). Register map per the
869// NESdev wiki NTDEC TC-112 / mapper-193 documentation; the implementation is
870// derived from Mesen2's NTDEC mapper source (GPL-3.0-or-later).
871// See NOTICE + docs/originality-and-provenance.md §1.
872// ===========================================================================
873
874/// NTDEC TC-112 (mapper 193).
875pub struct NtdecTc112 {
876    prg_rom: Box<[u8]>,
877    chr: Box<[u8]>,
878    chr_is_ram: bool,
879    vram: Box<[u8]>,
880    mirroring: Mirroring,
881    /// Switchable 8 KiB PRG window for $8000.
882    prg0: usize,
883    /// 2 KiB CHR windows for $0000/$0800/$1000/$1800.
884    chr2: [usize; 4],
885}
886
887impl NtdecTc112 {
888    fn new(
889        prg_rom: Box<[u8]>,
890        chr_rom: Box<[u8]>,
891        mirroring: Mirroring,
892    ) -> Result<Self, MapperError> {
893        check_prg(&prg_rom, 193)?;
894        let (chr, chr_is_ram) = chr_or_ram(chr_rom);
895        Ok(Self {
896            prg_rom,
897            chr,
898            chr_is_ram,
899            vram: vec![0u8; 2 * NAMETABLE_SIZE].into_boxed_slice(),
900            mirroring,
901            prg0: 0,
902            chr2: [0; 4],
903        })
904    }
905
906    fn prg_count_8k(&self) -> usize {
907        (self.prg_rom.len() / PRG_BANK_8K).max(1)
908    }
909
910    fn chr_count_2k(&self) -> usize {
911        (self.chr.len() / CHR_BANK_2K).max(1)
912    }
913}
914
915impl Mapper for NtdecTc112 {
916    fn caps(&self) -> MapperCaps {
917        MapperCaps::NONE
918    }
919
920    fn cpu_read(&mut self, addr: u16) -> u8 {
921        match addr {
922            // The final three 8 KiB windows are fixed to the last three banks.
923            0x8000..=0xFFFF => {
924                let count = self.prg_count_8k();
925                let slot = (addr as usize - 0x8000) / PRG_BANK_8K;
926                let bank = match slot {
927                    0 => self.prg0 % count,
928                    // `saturating_sub` guards a malformed sub-4-bank PRG image
929                    // (the subtraction would otherwise underflow + panic).
930                    _ => count.saturating_sub(4 - slot) % count, // last-3 fixed window
931                };
932                self.prg_rom[bank * PRG_BANK_8K + (addr as usize & 0x1FFF)]
933            }
934            _ => 0,
935        }
936    }
937
938    fn cpu_write(&mut self, addr: u16, value: u8) {
939        if (0x6000..=0x7FFF).contains(&addr) {
940            match addr & 0x03 {
941                0 => {
942                    // Paired 2 KiB CHR select into slots 0 and 1.
943                    self.chr2[0] = (value >> 1) as usize;
944                    self.chr2[1] = (value >> 1) as usize + 1;
945                }
946                1 => self.chr2[2] = (value >> 1) as usize,
947                2 => self.chr2[3] = (value >> 1) as usize,
948                _ => self.prg0 = value as usize,
949            }
950        }
951    }
952
953    fn ppu_read(&mut self, addr: u16) -> u8 {
954        let addr = addr & 0x3FFF;
955        match addr {
956            0x0000..=0x1FFF => {
957                if self.chr_is_ram {
958                    return self.chr[addr as usize & (CHR_BANK_8K - 1)];
959                }
960                let slot = (addr as usize) / CHR_BANK_2K;
961                let count = self.chr_count_2k();
962                let bank = self.chr2[slot] % count;
963                self.chr[bank * CHR_BANK_2K + (addr as usize & (CHR_BANK_2K - 1))]
964            }
965            0x2000..=0x3EFF => self.vram[nametable_offset(addr, self.mirroring)],
966            _ => 0,
967        }
968    }
969
970    fn ppu_write(&mut self, addr: u16, value: u8) {
971        let addr = addr & 0x3FFF;
972        match addr {
973            0x0000..=0x1FFF if self.chr_is_ram => {
974                self.chr[addr as usize & (CHR_BANK_8K - 1)] = value;
975            }
976            0x2000..=0x3EFF => {
977                let off = nametable_offset(addr, self.mirroring);
978                self.vram[off] = value;
979            }
980            _ => {}
981        }
982    }
983
984    fn current_mirroring(&self) -> Mirroring {
985        self.mirroring
986    }
987
988    fn save_state(&self) -> Vec<u8> {
989        let chr_ram = if self.chr_is_ram { self.chr.len() } else { 0 };
990        let mut out = Vec::with_capacity(1 + 4 + 16 + 1 + self.vram.len() + chr_ram);
991        out.push(SAVE_STATE_VERSION);
992        out.extend_from_slice(&(self.prg0 as u32).to_le_bytes());
993        for c in &self.chr2 {
994            out.extend_from_slice(&(*c as u32).to_le_bytes());
995        }
996        out.push(mirroring_to_byte(self.mirroring));
997        out.extend_from_slice(&self.vram);
998        if self.chr_is_ram {
999            out.extend_from_slice(&self.chr);
1000        }
1001        out
1002    }
1003
1004    fn load_state(&mut self, data: &[u8]) -> Result<(), MapperError> {
1005        let chr_ram = if self.chr_is_ram { self.chr.len() } else { 0 };
1006        let expected = 1 + 4 + 16 + 1 + self.vram.len() + chr_ram;
1007        if data.len() != expected {
1008            return Err(MapperError::WrongLength {
1009                expected,
1010                got: data.len(),
1011            });
1012        }
1013        if data[0] != SAVE_STATE_VERSION {
1014            return Err(MapperError::UnsupportedVersion(data[0]));
1015        }
1016        let rd = |c: usize| {
1017            u32::from_le_bytes([data[c], data[c + 1], data[c + 2], data[c + 3]]) as usize
1018        };
1019        let mut c = 1;
1020        self.prg0 = rd(c);
1021        c += 4;
1022        for s in &mut self.chr2 {
1023            *s = rd(c);
1024            c += 4;
1025        }
1026        self.mirroring = byte_to_mirroring(data[c], self.mirroring);
1027        c += 1;
1028        self.vram.copy_from_slice(&data[c..c + self.vram.len()]);
1029        c += self.vram.len();
1030        if self.chr_is_ram {
1031            self.chr.copy_from_slice(&data[c..c + self.chr.len()]);
1032        }
1033        Ok(())
1034    }
1035}
1036
1037/// Mapper 193 (NTDEC TC-112, *Fighting Hero*).
1038///
1039/// # Errors
1040/// [`MapperError::Invalid`] on a bad PRG size.
1041pub fn new_m193(
1042    prg_rom: Box<[u8]>,
1043    chr_rom: Box<[u8]>,
1044    mirroring: Mirroring,
1045) -> Result<NtdecTc112, MapperError> {
1046    NtdecTc112::new(prg_rom, chr_rom, mirroring)
1047}
1048
1049// ===========================================================================
1050// Bmc204 (mapper 204) — discrete NROM/UNROM 2-in-1 BMC multicart.
1051//
1052// The written *address* low bits select the layout: `bitMask = addr & 0x06`
1053// gives the 16 KiB PRG block, and (when bitMask != 0x06) `addr & 1` picks the
1054// inner half. Both PRG windows ($8000 + $C000) and the 8 KiB CHR window track
1055// the decoded page; `addr & 0x10` flips the mirroring. Register map per the
1056// NESdev wiki mapper-204 documentation; the implementation is derived from
1057// Mesen2's NTDEC mapper source (GPL-3.0-or-later).
1058// See NOTICE + docs/originality-and-provenance.md §1.
1059// ===========================================================================
1060
1061/// NTDEC N625092 multicart (mapper 221).
1062pub struct NtdecN625092 {
1063    prg_rom: Box<[u8]>,
1064    chr: Box<[u8]>,
1065    chr_is_ram: bool,
1066    vram: Box<[u8]>,
1067    mirroring: Mirroring,
1068    mode: u16,
1069    prg_reg: u8,
1070    /// 16 KiB PRG windows for $8000 and $C000.
1071    prg0: usize,
1072    prg1: usize,
1073}
1074
1075impl NtdecN625092 {
1076    fn new(
1077        prg_rom: Box<[u8]>,
1078        chr_rom: Box<[u8]>,
1079        mirroring: Mirroring,
1080    ) -> Result<Self, MapperError> {
1081        check_prg(&prg_rom, 221)?;
1082        if prg_rom.len() < PRG_BANK_16K {
1083            return Err(MapperError::Invalid(format!(
1084                "mapper 221 PRG-ROM size {} is smaller than one 16 KiB bank",
1085                prg_rom.len()
1086            )));
1087        }
1088        let (chr, chr_is_ram) = chr_or_ram(chr_rom);
1089        let mut m = Self {
1090            prg_rom,
1091            chr,
1092            chr_is_ram,
1093            vram: vec![0u8; 2 * NAMETABLE_SIZE].into_boxed_slice(),
1094            mirroring,
1095            mode: 0,
1096            prg_reg: 0,
1097            prg0: 0,
1098            prg1: 0,
1099        };
1100        m.update_state();
1101        Ok(m)
1102    }
1103
1104    fn prg_count_16k(&self) -> usize {
1105        (self.prg_rom.len() / PRG_BANK_16K).max(1)
1106    }
1107
1108    fn update_state(&mut self) {
1109        let outer = ((self.mode & 0xFC) >> 2) as usize;
1110        let reg = self.prg_reg as usize;
1111        if self.mode & 0x02 != 0 {
1112            if self.mode & 0x0100 != 0 {
1113                // NROM-256 sub-case: switchable low + fixed (outer | 7) high.
1114                self.prg0 = outer | reg;
1115                self.prg1 = outer | 0x07;
1116            } else {
1117                // UNROM 2x16 KiB aligned pair (SelectPrgPage2x): the inner reg
1118                // (masked to even) selects a 32 KiB-aligned window.
1119                let b = outer | (reg & 0x06);
1120                self.prg0 = b;
1121                self.prg1 = b | 1;
1122            }
1123        } else {
1124            // NROM: both windows mirror the same bank.
1125            self.prg0 = outer | reg;
1126            self.prg1 = outer | reg;
1127        }
1128        self.mirroring = if self.mode & 0x01 != 0 {
1129            Mirroring::Horizontal
1130        } else {
1131            Mirroring::Vertical
1132        };
1133    }
1134
1135    fn prg_byte(&self, slot16: usize, addr: u16) -> u8 {
1136        let count = self.prg_count_16k();
1137        let bank = slot16 % count;
1138        self.prg_rom[bank * PRG_BANK_16K + (addr as usize & 0x3FFF)]
1139    }
1140}
1141
1142impl Mapper for NtdecN625092 {
1143    fn caps(&self) -> MapperCaps {
1144        MapperCaps::NONE
1145    }
1146
1147    fn cpu_read(&mut self, addr: u16) -> u8 {
1148        match addr {
1149            0x8000..=0xBFFF => self.prg_byte(self.prg0, addr),
1150            0xC000..=0xFFFF => self.prg_byte(self.prg1, addr),
1151            _ => 0,
1152        }
1153    }
1154
1155    fn cpu_write(&mut self, addr: u16, _value: u8) {
1156        match addr & 0xC000 {
1157            0x8000 => {
1158                self.mode = addr;
1159                self.update_state();
1160            }
1161            0xC000 => {
1162                self.prg_reg = (addr & 0x07) as u8;
1163                self.update_state();
1164            }
1165            _ => {}
1166        }
1167    }
1168
1169    fn ppu_read(&mut self, addr: u16) -> u8 {
1170        let addr = addr & 0x3FFF;
1171        match addr {
1172            0x0000..=0x1FFF => self.chr[addr as usize & (CHR_BANK_8K - 1)],
1173            0x2000..=0x3EFF => self.vram[nametable_offset(addr, self.mirroring)],
1174            _ => 0,
1175        }
1176    }
1177
1178    fn ppu_write(&mut self, addr: u16, value: u8) {
1179        let addr = addr & 0x3FFF;
1180        match addr {
1181            0x0000..=0x1FFF if self.chr_is_ram => {
1182                self.chr[addr as usize & (CHR_BANK_8K - 1)] = value;
1183            }
1184            0x2000..=0x3EFF => {
1185                let off = nametable_offset(addr, self.mirroring);
1186                self.vram[off] = value;
1187            }
1188            _ => {}
1189        }
1190    }
1191
1192    fn current_mirroring(&self) -> Mirroring {
1193        self.mirroring
1194    }
1195
1196    fn save_state(&self) -> Vec<u8> {
1197        let chr_ram = if self.chr_is_ram { self.chr.len() } else { 0 };
1198        let mut out = Vec::with_capacity(1 + 2 + 1 + 8 + 1 + self.vram.len() + chr_ram);
1199        out.push(SAVE_STATE_VERSION);
1200        out.extend_from_slice(&self.mode.to_le_bytes());
1201        out.push(self.prg_reg);
1202        out.extend_from_slice(&(self.prg0 as u32).to_le_bytes());
1203        out.extend_from_slice(&(self.prg1 as u32).to_le_bytes());
1204        out.push(mirroring_to_byte(self.mirroring));
1205        out.extend_from_slice(&self.vram);
1206        if self.chr_is_ram {
1207            out.extend_from_slice(&self.chr);
1208        }
1209        out
1210    }
1211
1212    fn load_state(&mut self, data: &[u8]) -> Result<(), MapperError> {
1213        let chr_ram = if self.chr_is_ram { self.chr.len() } else { 0 };
1214        let expected = 1 + 2 + 1 + 8 + 1 + self.vram.len() + chr_ram;
1215        if data.len() != expected {
1216            return Err(MapperError::WrongLength {
1217                expected,
1218                got: data.len(),
1219            });
1220        }
1221        if data[0] != SAVE_STATE_VERSION {
1222            return Err(MapperError::UnsupportedVersion(data[0]));
1223        }
1224        let rd = |c: usize| {
1225            u32::from_le_bytes([data[c], data[c + 1], data[c + 2], data[c + 3]]) as usize
1226        };
1227        self.mode = u16::from_le_bytes([data[1], data[2]]);
1228        self.prg_reg = data[3];
1229        self.prg0 = rd(4);
1230        self.prg1 = rd(8);
1231        let mut c = 12;
1232        self.mirroring = byte_to_mirroring(data[c], self.mirroring);
1233        c += 1;
1234        self.vram.copy_from_slice(&data[c..c + self.vram.len()]);
1235        c += self.vram.len();
1236        if self.chr_is_ram {
1237            self.chr.copy_from_slice(&data[c..c + self.chr.len()]);
1238        }
1239        Ok(())
1240    }
1241}
1242
1243/// Mapper 221 (NTDEC N625092 multicart).
1244///
1245/// # Errors
1246/// [`MapperError::Invalid`] on a bad PRG size.
1247pub fn new_m221(
1248    prg_rom: Box<[u8]>,
1249    chr_rom: Box<[u8]>,
1250    mirroring: Mirroring,
1251) -> Result<NtdecN625092, MapperError> {
1252    NtdecN625092::new(prg_rom, chr_rom, mirroring)
1253}
1254
1255// ===========================================================================
1256// Bmc11160 (mapper 299) — TXC/BMC-11160 multicart.
1257//
1258// One value-decoded $8000-$FFFF register: bits 4-6 select a 32 KiB PRG bank,
1259// the 8 KiB CHR bank is `(bank << 2) | (value & 0x03)`, and bit 7 flips the
1260// mirroring (set => vertical). Register map per the NESdev wiki mapper-299 /
1261// BMC-11160 documentation; the implementation is derived from Mesen2's
1262// `Txc/Bmc11160.h` (GPL-3.0-or-later). See NOTICE + docs/originality-and-provenance.md §1.
1263// ===========================================================================
1264
1265#[cfg(test)]
1266#[allow(clippy::cast_possible_truncation)]
1267mod tests {
1268    use super::*;
1269    /// 1 KiB-banked CHR: byte 0 of each 1 KiB bank holds the bank index.
1270    fn synth_chr_1k(banks: usize) -> Box<[u8]> {
1271        let mut v = vec![0u8; banks * CHR_BANK_1K];
1272        for b in 0..banks {
1273            v[b * CHR_BANK_1K] = b as u8;
1274        }
1275        v.into_boxed_slice()
1276    }
1277
1278    fn synth_prg_16k(banks: usize) -> Box<[u8]> {
1279        let mut v = vec![0xFFu8; banks * PRG_BANK_16K];
1280        for b in 0..banks {
1281            v[b * PRG_BANK_16K] = b as u8;
1282        }
1283        v.into_boxed_slice()
1284    }
1285
1286    fn synth_prg_8k(banks: usize) -> Box<[u8]> {
1287        let mut v = vec![0xFFu8; banks * PRG_BANK_8K];
1288        for b in 0..banks {
1289            v[b * PRG_BANK_8K] = b as u8;
1290        }
1291        v.into_boxed_slice()
1292    }
1293
1294    fn synth_chr_8k(banks: usize) -> Box<[u8]> {
1295        let mut v = vec![0u8; banks * CHR_BANK_8K];
1296        for b in 0..banks {
1297            v[b * CHR_BANK_8K] = b as u8;
1298        }
1299        v.into_boxed_slice()
1300    }
1301
1302    #[test]
1303    fn m63_address_decoded_bank_and_mode() {
1304        let mut m = Ntdec63::new(synth_prg_16k(16), &[], Mirroring::Vertical).unwrap();
1305        // 32 KiB mode: A & 2 == 0. bank = (A>>2)&0x3F. Choose A=0x8008 ->
1306        // (0x8008>>2)&0x3F = 0x02 -> bank 2; (A&2)==0 -> 32K; (A&1)==0 -> V.
1307        m.cpu_write(0x8008, 0);
1308        assert_eq!(m.cpu_read(0x8000), 2);
1309        assert_eq!(m.cpu_read(0xC000), 3); // 32K high half = bank|1
1310        assert_eq!(m.current_mirroring(), Mirroring::Vertical);
1311    }
1312
1313    #[test]
1314    fn m63_save_state_round_trip() {
1315        let mut m = Ntdec63::new(synth_prg_16k(16), &[], Mirroring::Vertical).unwrap();
1316        m.cpu_write(0x8009, 0); // A&1 == 1 -> horizontal
1317        m.ppu_write(0x0010, 0x12);
1318        let blob = m.save_state();
1319        let mut m2 = Ntdec63::new(synth_prg_16k(16), &[], Mirroring::Vertical).unwrap();
1320        m2.load_state(&blob).unwrap();
1321        assert_eq!(m2.current_mirroring(), Mirroring::Horizontal);
1322        assert_eq!(m2.ppu_read(0x0010), 0x12);
1323    }
1324
1325    #[test]
1326    fn m174_address_decoded_prg_chr_mirror() {
1327        let mut m = Ntdec174::new(synth_prg_16k(8), synth_chr_8k(8), Mirroring::Vertical).unwrap();
1328        // A = 0x8020: prg = (0x20>>4)&7 = 2; (A&0x80)==0 -> 32K; chr =
1329        // (0x20>>1)&7 = 0; (A&1)==0 -> V.
1330        m.cpu_write(0x8020, 0);
1331        assert_eq!(m.cpu_read(0x8000), 2);
1332        assert_eq!(m.cpu_read(0xC000), 3);
1333        assert_eq!(m.ppu_read(0x0000), 0);
1334        assert_eq!(m.current_mirroring(), Mirroring::Vertical);
1335    }
1336
1337    #[test]
1338    fn m174_save_state_round_trip() {
1339        let mut m = Ntdec174::new(synth_prg_16k(8), synth_chr_8k(8), Mirroring::Vertical).unwrap();
1340        m.cpu_write(0x80A3, 0); // 16K mode + chr select + horizontal
1341        let blob = m.save_state();
1342        let mut m2 = Ntdec174::new(synth_prg_16k(8), synth_chr_8k(8), Mirroring::Vertical).unwrap();
1343        m2.load_state(&blob).unwrap();
1344        assert_eq!(m2.cpu_read(0x8000), m.cpu_read(0x8000));
1345        assert_eq!(m2.current_mirroring(), m.current_mirroring());
1346    }
1347
1348    #[test]
1349    fn m40_fixed_layout_and_switchable_window() {
1350        let mut m = Ntdec2722M40::new(synth_prg_8k(8), &[], Mirroring::Vertical).unwrap();
1351        // Fixed banks.
1352        assert_eq!(m.cpu_read(0x8000), 4);
1353        assert_eq!(m.cpu_read(0xA000), 5);
1354        assert_eq!(m.cpu_read(0xE000), 7);
1355        // Switch $C000 to bank 3.
1356        m.cpu_write(0xE000, 3);
1357        assert_eq!(m.cpu_read(0xC000), 3);
1358    }
1359
1360    #[test]
1361    fn m40_irq_fires_after_enable() {
1362        let mut m = Ntdec2722M40::new(synth_prg_8k(8), &[], Mirroring::Vertical).unwrap();
1363        assert!(!m.irq_pending());
1364        m.cpu_write(0xA000, 0); // enable
1365        for _ in 0..0x1000 {
1366            m.notify_cpu_cycle();
1367        }
1368        assert!(m.irq_pending());
1369        m.cpu_write(0x8000, 0); // disable + ack
1370        assert!(!m.irq_pending());
1371    }
1372
1373    #[test]
1374    fn m40_save_state_round_trip() {
1375        let mut m = Ntdec2722M40::new(synth_prg_8k(8), &[], Mirroring::Vertical).unwrap();
1376        m.cpu_write(0xE000, 2);
1377        m.cpu_write(0xA000, 0);
1378        m.notify_cpu_cycle();
1379        m.ppu_write(0x0005, 0x9A);
1380        let blob = m.save_state();
1381        let mut m2 = Ntdec2722M40::new(synth_prg_8k(8), &[], Mirroring::Vertical).unwrap();
1382        m2.load_state(&blob).unwrap();
1383        assert_eq!(m2.cpu_read(0xC000), 2);
1384        assert_eq!(m2.ppu_read(0x0005), 0x9A);
1385    }
1386
1387    #[test]
1388    fn m81_prg_and_chr_select() {
1389        let mut m = Ntdec81::new(synth_prg_16k(8), synth_chr_8k(4), Mirroring::Vertical).unwrap();
1390        // PRG bits 2-3 = 2, CHR bits 0-1 = 1. value = (2<<2)|1 = 0x09.
1391        m.cpu_write(0x8000, 0x09);
1392        assert_eq!(m.cpu_read(0x8000), 2);
1393        // $C000 fixed to last (7).
1394        assert_eq!(m.cpu_read(0xC000), 7);
1395        assert_eq!(m.ppu_read(0x0000), 1);
1396    }
1397
1398    #[test]
1399    fn m81_save_state_round_trip() {
1400        let mut m = Ntdec81::new(synth_prg_16k(8), synth_chr_8k(4), Mirroring::Vertical).unwrap();
1401        m.cpu_write(0x8000, 0x06);
1402        let blob = m.save_state();
1403        let mut m2 = Ntdec81::new(synth_prg_16k(8), synth_chr_8k(4), Mirroring::Vertical).unwrap();
1404        m2.load_state(&blob).unwrap();
1405        assert_eq!(m2.cpu_read(0x8000), m.cpu_read(0x8000));
1406        assert_eq!(m2.ppu_read(0x0000), m.ppu_read(0x0000));
1407    }
1408
1409    #[test]
1410    fn m112_indexed_prg_chr_and_mirroring() {
1411        let mut m =
1412            NtdecAsder112::new(synth_prg_8k(8), synth_chr_1k(16), Mirroring::Vertical).unwrap();
1413        // Select reg 0 (PRG $8000) -> bank 3.
1414        m.cpu_write(0x8000, 0);
1415        m.cpu_write(0xA000, 3);
1416        assert_eq!(m.cpu_read(0x8000), 3);
1417        // Select reg 1 (PRG $A000) -> bank 2.
1418        m.cpu_write(0x8000, 1);
1419        m.cpu_write(0xA000, 2);
1420        assert_eq!(m.cpu_read(0xA000), 2);
1421        // Mirroring register.
1422        m.cpu_write(0xE000, 1);
1423        assert_eq!(m.current_mirroring(), Mirroring::Horizontal);
1424        // Fixed last two.
1425        assert_eq!(m.cpu_read(0xE000), 7);
1426    }
1427
1428    #[test]
1429    fn m112_save_state_round_trip() {
1430        let mut m =
1431            NtdecAsder112::new(synth_prg_8k(8), synth_chr_1k(16), Mirroring::Vertical).unwrap();
1432        m.cpu_write(0x8000, 0);
1433        m.cpu_write(0xA000, 5);
1434        m.cpu_write(0xE000, 1);
1435        let blob = m.save_state();
1436        let mut m2 =
1437            NtdecAsder112::new(synth_prg_8k(8), synth_chr_1k(16), Mirroring::Vertical).unwrap();
1438        m2.load_state(&blob).unwrap();
1439        assert_eq!(m2.cpu_read(0x8000), m.cpu_read(0x8000));
1440        assert_eq!(m2.current_mirroring(), Mirroring::Horizontal);
1441    }
1442
1443    fn prg(banks_8k: usize) -> Box<[u8]> {
1444        // Fill each 8 KiB bank with its index so bank routing is observable.
1445        let mut v = vec![0u8; banks_8k * PRG_BANK_8K];
1446        for (i, b) in v.chunks_mut(PRG_BANK_8K).enumerate() {
1447            b.fill(i as u8);
1448        }
1449        v.into_boxed_slice()
1450    }
1451
1452    fn chr(banks_8k: usize) -> Box<[u8]> {
1453        let mut v = vec![0u8; banks_8k * CHR_BANK_8K];
1454        for (i, b) in v.chunks_mut(CHR_BANK_8K).enumerate() {
1455            b.fill(i as u8);
1456        }
1457        v.into_boxed_slice()
1458    }
1459
1460    #[test]
1461    fn m193_last_three_prg_windows_are_fixed() {
1462        // 8 banks of 8 KiB. The last three 8 KiB windows must be fixed to the
1463        // last three banks (5,6,7) regardless of the switchable $8000 select.
1464        let mut m = new_m193(prg(8), chr(4), Mirroring::Vertical).unwrap();
1465        // $8000 defaults to bank 0.
1466        assert_eq!(m.cpu_read(0x8000), 0);
1467        assert_eq!(m.cpu_read(0xA000), 5, "$A000 fixed to last-3");
1468        assert_eq!(m.cpu_read(0xC000), 6, "$C000 fixed to last-3");
1469        assert_eq!(m.cpu_read(0xE000), 7, "$E000 fixed to last-3");
1470        // Register 3 selects the switchable $8000 8 KiB window.
1471        m.cpu_write(0x6003, 3);
1472        assert_eq!(m.cpu_read(0x8000), 3);
1473        // Fixed windows are unaffected.
1474        assert_eq!(m.cpu_read(0xE000), 7);
1475    }
1476
1477    #[test]
1478    fn m193_chr_registers_select_2k_windows() {
1479        let mut m = new_m193(prg(2), chr(4), Mirroring::Vertical).unwrap();
1480        // reg0: paired 2 KiB select into slots 0+1. value 4 => (4>>1)=2 / 3.
1481        m.cpu_write(0x6000, 4);
1482        // chr bank N (2 KiB) of a 4x8KiB image => 16 2-KiB banks; bank 2 lives in
1483        // 8 KiB CHR bank 0 (banks 0..3) so its byte == 0.
1484        assert_eq!(m.ppu_read(0x0000), 0); // slot0 -> 2k bank 2 -> 8k bank0
1485        // reg1 -> slot2 ($1000); reg2 -> slot3 ($1800).
1486        m.cpu_write(0x6001, 8); // (8>>1)=4 -> 2k bank4 -> 8k bank1
1487        assert_eq!(m.ppu_read(0x1000), 1);
1488        m.cpu_write(0x6002, 12); // (12>>1)=6 -> 2k bank6 -> 8k bank1
1489        assert_eq!(m.ppu_read(0x1800), 1);
1490    }
1491
1492    #[test]
1493    fn m193_chr_ram_when_no_chr_rom() {
1494        let mut m = new_m193(prg(2), Box::new([]), Mirroring::Vertical).unwrap();
1495        m.ppu_write(0x0123, 0xAB);
1496        assert_eq!(m.ppu_read(0x0123), 0xAB);
1497    }
1498
1499    #[test]
1500    fn m221_nrom_mode_mirrors_both_windows() {
1501        let mut m = new_m221(prg(16), chr(1), Mirroring::Vertical).unwrap();
1502        // mode = $8000 (mode&2 == 0 => NROM); outer = 0; prg_reg default 0.
1503        m.cpu_write(0x8000, 0);
1504        m.cpu_write(0xC003, 0); // inner reg = 3
1505        // NROM: both windows == outer|reg == 3.
1506        assert_eq!(m.cpu_read(0x8000), 6, "16k page 3 -> 8k bank 6");
1507        assert_eq!(m.cpu_read(0xC000), 6);
1508    }
1509
1510    #[test]
1511    fn m221_unrom_nrom256_subcase() {
1512        let mut m = new_m221(prg(16), chr(1), Mirroring::Vertical).unwrap();
1513        // mode bits: set bit1 (UNROM) and bit8 (NROM-256 sub-case).
1514        // addr = 0x8000 | 0x0102 = 0x8102.
1515        m.cpu_write(0x8102, 0);
1516        m.cpu_write(0xC002, 0); // inner reg = 2
1517        // outer = (0x0102 & 0xFC) >> 2 = 0x00. prg0 = 0|2 = 2; prg1 = 0|7 = 7.
1518        assert_eq!(m.cpu_read(0x8000), 4, "16k page 2 -> 8k bank 4");
1519        assert_eq!(m.cpu_read(0xC000), 14, "16k page 7 -> 8k bank 14");
1520    }
1521
1522    #[test]
1523    fn m221_mirroring_bit() {
1524        let mut m = new_m221(prg(4), chr(1), Mirroring::Horizontal).unwrap();
1525        m.cpu_write(0x8001, 0); // mode&1 set => horizontal
1526        assert_eq!(m.current_mirroring(), Mirroring::Horizontal);
1527        m.cpu_write(0x8000, 0); // mode&1 clear => vertical
1528        assert_eq!(m.current_mirroring(), Mirroring::Vertical);
1529    }
1530
1531    #[test]
1532    fn m193_m221_save_load_round_trip() {
1533        // m193
1534        let mut a = new_m193(prg(8), chr(4), Mirroring::Vertical).unwrap();
1535        a.cpu_write(0x6003, 5);
1536        a.cpu_write(0x6000, 6);
1537        let s = a.save_state();
1538        let mut b = new_m193(prg(8), chr(4), Mirroring::Vertical).unwrap();
1539        b.load_state(&s).unwrap();
1540        assert_eq!(a.cpu_read(0x8000), b.cpu_read(0x8000));
1541        assert_eq!(a.ppu_read(0x0000), b.ppu_read(0x0000));
1542
1543        // m221
1544        let mut a = new_m221(prg(16), chr(1), Mirroring::Vertical).unwrap();
1545        a.cpu_write(0x8102, 0);
1546        a.cpu_write(0xC002, 0);
1547        let s = a.save_state();
1548        let mut b = new_m221(prg(16), chr(1), Mirroring::Horizontal).unwrap();
1549        b.load_state(&s).unwrap();
1550        assert_eq!(a.cpu_read(0x8000), b.cpu_read(0x8000));
1551        assert_eq!(a.cpu_read(0xC000), b.cpu_read(0xC000));
1552    }
1553
1554    #[test]
1555    fn m193_m221_bad_prg_size_is_rejected() {
1556        // 100 bytes is not a multiple of 8 KiB.
1557        assert!(
1558            new_m193(
1559                vec![0u8; 100].into_boxed_slice(),
1560                chr(1),
1561                Mirroring::Vertical
1562            )
1563            .is_err()
1564        );
1565        assert!(
1566            new_m221(
1567                vec![0u8; 100].into_boxed_slice(),
1568                chr(1),
1569                Mirroring::Vertical
1570            )
1571            .is_err()
1572        );
1573    }
1574}