Skip to main content

rustynes_mappers/
mmc3_clones.rs

1// SPDX-License-Identifier: GPL-3.0-or-later
2//
3// Provenance: several MMC3-clone boards (e.g. Waixing Mapper 253 and the `InvertPrgBits` transform) are derived from Mesen2 (GPL-3.0-or-later), `Waixing/Mapper253.h` and the MMC3-variant sources. See docs/originality-and-provenance.md (Section 1)
4// and NOTICE for the complete, audited derivation record.
5//! MMC3-clone ASICs: mappers 44, 49, 52, 115, 134, 189, 205, 238, 245, 348,
6//! 366 and relatives.
7//!
8//! Unlicensed manufacturers cloned the MMC3 more than any other Nintendo
9//! ASIC, because it was the cheapest way to run existing MMC3 games off a
10//! multicart. The clones keep the MMC3 register protocol and its A12-driven
11//! scanline IRQ counter *exactly*, and add an outer bank register that
12//! selects which 128/256/512 KiB "cartridge" the inner MMC3 sees.
13//!
14//! That is why this is one implementation with a board discriminant
15//! ([`CloneBoard`]) rather than eleven: the shared [`Mmc3Clone`] core carries
16//! the real MMC3 behaviour, and each board contributes only its outer-register
17//! decode. Getting the IRQ timing right once benefits all of them.
18//!
19//! The genuine Nintendo MMC3 is in `m004_mmc3.rs`.
20//!
21//! A best-effort (Tier-2) board: register-decode correctness verified against
22//! the reference emulators (`Mesen2`, `GeraNES`) and the nesdev wiki, with no
23//! commercial-oracle ROM in the tree. Banking math is direct slice indexing and
24//! every bank select wraps with `% count`, so a register write can never index
25//! out of bounds -- required for the `#![no_std]` chip stack, which cannot
26//! afford a panic on a register access.
27//!
28//! See `tier.rs` (`MapperTier::BestEffort`), `docs/adr/0011-mapper-tiering.md`,
29//! and `docs/mappers.md` §Mapper coverage matrix.
30
31#![allow(
32    clippy::cast_possible_truncation,
33    clippy::cast_lossless,
34    clippy::match_same_arms,
35    clippy::doc_markdown,
36    clippy::similar_names,
37    clippy::too_many_lines,
38    clippy::missing_const_for_fn,
39    clippy::struct_excessive_bools,
40    clippy::bool_to_int_with_if
41)]
42
43use crate::a12_filter::A12RiseFilter;
44use crate::cartridge::Mirroring;
45use crate::mapper::{Mapper, MapperCaps, MapperError};
46use alloc::{boxed::Box, format, vec, vec::Vec};
47
48const PRG_BANK_8K: usize = 0x2000;
49const CHR_BANK_1K: usize = 0x0400;
50const NAMETABLE_SIZE: usize = 0x0400;
51const NAMETABLE_SIZE_U16: u16 = 0x0400;
52
53/// v2 (v2.7.2) appends the board's WRAM (mapper 245 only today); v3 (v2.9.7)
54/// packs the A12 filter into the old `last_a12` byte. Only v3 loads since
55/// v2.9.8 (ADR 0042); v1 used to load with the WRAM zeroed and v1/v2 with a
56/// bare A12 level.
57const SAVE_STATE_VERSION: u8 = 3;
58
59// ---------------------------------------------------------------------------
60// Shared nametable helper (mirrors the one in the other simple-mapper modules).
61// ---------------------------------------------------------------------------
62
63const fn nametable_offset(addr: u16, mirroring: Mirroring) -> usize {
64    let table = (((addr - 0x2000) / NAMETABLE_SIZE_U16) & 0x03) as u8;
65    let local = (addr as usize) & (NAMETABLE_SIZE - 1);
66    let physical = mirroring.physical_bank(table);
67    physical * NAMETABLE_SIZE + local
68}
69
70const fn mirroring_to_byte(m: Mirroring) -> u8 {
71    match m {
72        Mirroring::Horizontal => 0,
73        Mirroring::Vertical => 1,
74        Mirroring::SingleScreenA => 2,
75        Mirroring::SingleScreenB => 3,
76        Mirroring::FourScreen => 4,
77        Mirroring::MapperControlled => 5,
78    }
79}
80
81const fn byte_to_mirroring(b: u8, fallback: Mirroring) -> Mirroring {
82    match b {
83        0 => Mirroring::Horizontal,
84        1 => Mirroring::Vertical,
85        2 => Mirroring::SingleScreenA,
86        3 => Mirroring::SingleScreenB,
87        4 => Mirroring::FourScreen,
88        5 => Mirroring::MapperControlled,
89        _ => fallback,
90    }
91}
92
93// ===========================================================================
94// Mmc3Clone — reusable MMC3-style core for the clone boards.
95//
96// The MMC3 register protocol (NES 2.0 mapper 4):
97//   $8000 even : bank-select (low 3 bits = R index, bit 6 = PRG mode,
98//                bit 7 = CHR mode).
99//   $8001 odd  : bank-data (the value loaded into the selected R register).
100//   $A000 even : mirroring (bit 0: 0 = vertical, 1 = horizontal).
101//   $C000 even : IRQ latch (reload value).
102//   $C001 odd  : IRQ reload (force a reload on the next A12 rise).
103//   $E000 even : IRQ disable + acknowledge.
104//   $E001 odd  : IRQ enable.
105//
106// The A12 IRQ counter clocks on every PPU A12 rising edge: if the counter is 0
107// or a reload is pending, it reloads from the latch; otherwise it decrements.
108// After the update, if the counter is 0 and IRQs are enabled, the IRQ asserts.
109// ===========================================================================
110
111/// A reusable MMC3-style banking + A12-IRQ core for the clone boards.
112struct Mmc3Clone {
113    regs: [u8; 8],
114    bank_select: u8,
115    prg_mode: bool,
116    chr_mode: bool,
117    mirroring: Mirroring,
118    irq_counter: u8,
119    irq_latch: u8,
120    irq_reload: bool,
121    irq_enabled: bool,
122    irq_pending: bool,
123    /// v2.9.7 — MMC3's A12 rise filter (see `a12_filter`): the counter
124    /// clocks once per scanline on the eight-pulse stream the PPU reports.
125    a12: A12RiseFilter,
126    prg_count_8k: usize,
127    chr_count_1k: usize,
128}
129
130impl Mmc3Clone {
131    const SAVE_LEN: usize = 8 + 10;
132
133    fn new(prg_count_8k: usize, chr_count_1k: usize, mirroring: Mirroring) -> Self {
134        Self {
135            regs: [0; 8],
136            bank_select: 0,
137            prg_mode: false,
138            chr_mode: false,
139            mirroring,
140            irq_counter: 0,
141            irq_latch: 0,
142            irq_reload: false,
143            irq_enabled: false,
144            irq_pending: false,
145            a12: A12RiseFilter::new(),
146            prg_count_8k: prg_count_8k.max(1),
147            chr_count_1k: chr_count_1k.max(1),
148        }
149    }
150
151    /// Handle a write to the `$8000-$FFFF` MMC3 register space.
152    fn write_register(&mut self, addr: u16, value: u8) {
153        match addr & 0xE001 {
154            0x8000 => {
155                self.bank_select = value & 0x07;
156                self.prg_mode = value & 0x40 != 0;
157                self.chr_mode = value & 0x80 != 0;
158            }
159            0x8001 => {
160                let idx = (self.bank_select & 0x07) as usize;
161                self.regs[idx] = value;
162            }
163            0xA000 => {
164                self.mirroring = if value & 0x01 == 0 {
165                    Mirroring::Vertical
166                } else {
167                    Mirroring::Horizontal
168                };
169            }
170            0xC000 => self.irq_latch = value,
171            0xC001 => {
172                self.irq_counter = 0;
173                self.irq_reload = true;
174            }
175            0xE000 => {
176                self.irq_enabled = false;
177                self.irq_pending = false;
178            }
179            0xE001 => self.irq_enabled = true,
180            _ => {}
181        }
182    }
183
184    /// The base 8 KiB PRG bank for CPU slot 0..=3 ($8000/$A000/$C000/$E000),
185    /// before any wrapper outer-bank transform. Mirrors the MMC3 PRG layout.
186    fn prg_bank(&self, slot: usize) -> usize {
187        let last = self.prg_count_8k - 1;
188        let second_last = last.saturating_sub(1);
189        let r6 = self.regs[6] as usize;
190        let r7 = self.regs[7] as usize;
191        match (slot, self.prg_mode) {
192            (0, false) => r6,
193            (0, true) => second_last,
194            (1, _) => r7,
195            (2, false) => second_last,
196            (2, true) => r6,
197            (3, _) => last,
198            _ => 0,
199        }
200    }
201
202    /// The base 1 KiB CHR bank for PPU 1 KiB slot 0..=7, before any wrapper
203    /// outer-bank transform. Mirrors the MMC3 CHR layout (2 KiB R0/R1 +
204    /// 1 KiB R2-R5, swapped by `chr_mode`).
205    fn chr_bank(&self, slot: usize) -> usize {
206        let banks: [usize; 8] = if self.chr_mode {
207            [
208                self.regs[2] as usize,
209                self.regs[3] as usize,
210                self.regs[4] as usize,
211                self.regs[5] as usize,
212                self.regs[0] as usize & !1,
213                (self.regs[0] as usize & !1) | 1,
214                self.regs[1] as usize & !1,
215                (self.regs[1] as usize & !1) | 1,
216            ]
217        } else {
218            [
219                self.regs[0] as usize & !1,
220                (self.regs[0] as usize & !1) | 1,
221                self.regs[1] as usize & !1,
222                (self.regs[1] as usize & !1) | 1,
223                self.regs[2] as usize,
224                self.regs[3] as usize,
225                self.regs[4] as usize,
226                self.regs[5] as usize,
227            ]
228        };
229        banks[slot & 0x07]
230    }
231
232    /// Clock the A12 IRQ counter on a PPU A12 transition.
233    fn notify_a12(&mut self, level: bool) {
234        // v2.9.7 — MMC3's filter; see `a12_filter` for why a bare
235        // rising-edge test is no longer enough.
236        if !self.a12.edge(level) {
237            return;
238        }
239        if self.irq_counter == 0 || self.irq_reload {
240            self.irq_counter = self.irq_latch;
241            self.irq_reload = false;
242        } else {
243            self.irq_counter = self.irq_counter.wrapping_sub(1);
244        }
245        if self.irq_counter == 0 && self.irq_enabled {
246            self.irq_pending = true;
247        }
248    }
249
250    fn save(&self, out: &mut Vec<u8>) {
251        out.extend_from_slice(&self.regs);
252        out.push(self.bank_select);
253        out.push(u8::from(self.prg_mode));
254        out.push(u8::from(self.chr_mode));
255        out.push(mirroring_to_byte(self.mirroring));
256        out.push(self.irq_counter);
257        out.push(self.irq_latch);
258        out.push(u8::from(self.irq_reload));
259        out.push(u8::from(self.irq_enabled));
260        out.push(u8::from(self.irq_pending));
261        out.push(self.a12.to_byte());
262    }
263
264    fn load(&mut self, data: &[u8]) {
265        self.regs.copy_from_slice(&data[0..8]);
266        self.bank_select = data[8];
267        self.prg_mode = data[9] != 0;
268        self.chr_mode = data[10] != 0;
269        self.mirroring = byte_to_mirroring(data[11], self.mirroring);
270        self.irq_counter = data[12];
271        self.irq_latch = data[13];
272        self.irq_reload = data[14] != 0;
273        self.irq_enabled = data[15] != 0;
274        self.irq_pending = data[16] != 0;
275        self.a12 = A12RiseFilter::from_byte(data[17]);
276    }
277}
278
279/// Which clone board's outer-bank transform [`Mmc3CloneMapper`] applies.
280#[derive(Debug, Clone, Copy, PartialEq, Eq)]
281enum CloneBoard {
282    /// Mapper 44 — 7-block selector via `$A001`.
283    M44,
284    /// Mapper 49 — `$6000` outer block-select with a simplified-PRG mode bit.
285    M49,
286    /// Mapper 52 — `$6000` outer-block / PRG+CHR-size selector.
287    M52,
288    /// Mapper 115 — `$5000`/`$4100` PRG-override + CHR outer-256K register.
289    M115,
290    /// Mapper 134 — `$6001` PRG (bit 1) + CHR (bit 5) 256 KiB outer bank.
291    M134,
292    /// Mapper 189 — `$4120-$7FFF` 32 KiB PRG select (overrides MMC3 PRG).
293    M189,
294    /// Mapper 205 — `$6000` 2-bit block-select (PRG/CHR outer window).
295    M205,
296    /// Mapper 238 — `$4020-$7FFF` security register (read-back LUT).
297    M238,
298    /// Mapper 245 — `$8001` R0 bit 1 -> PRG 256 KiB outer; CHR-RAM 4K/4K swap.
299    M245,
300    /// Mapper 348 — `$6800` outer-bank register (BMC-830118C).
301    M348,
302    /// Mapper 366 — `$6000-$7FFF` block-select (BMC-GN-45).
303    M366,
304}
305
306// ===========================================================================
307// Mmc3CloneMapper — wraps `Mmc3Clone` + a `CloneBoard` outer transform.
308// ===========================================================================
309
310/// An MMC3-clone board: the shared MMC3-style core plus a board-specific
311/// outer-bank register and PRG/CHR transform.
312pub struct Mmc3CloneMapper {
313    board: CloneBoard,
314    core: Mmc3Clone,
315    prg_rom: Box<[u8]>,
316    chr: Box<[u8]>,
317    chr_is_ram: bool,
318    vram: Box<[u8]>,
319    /// Board-specific outer register (semantics per `CloneBoard`).
320    outer: u8,
321    /// A second board register where needed (115 CHR-hi / protection read).
322    outer2: u8,
323    /// Battery-backed WRAM at `$6000-$7FFF`, on boards that have it. Only
324    /// mapper 245 today: "an MMC3 clone with 8 KiB of battery-backed WRAM"
325    /// (`nesdev_wiki/INES_Mapper_245.xhtml`); empty for the others. The other
326    /// clones that document WRAM (mapper 52's 7-in-1) put board registers in
327    /// the same window, which needs its own per-board decode (core ledger).
328    wram: Box<[u8]>,
329}
330
331impl Mmc3CloneMapper {
332    fn new(
333        board: CloneBoard,
334        prg_rom: Box<[u8]>,
335        chr_rom: Box<[u8]>,
336        mirroring: Mirroring,
337        mapper_id: u16,
338    ) -> Result<Self, MapperError> {
339        if prg_rom.is_empty() || !prg_rom.len().is_multiple_of(PRG_BANK_8K) {
340            return Err(MapperError::Invalid(format!(
341                "mapper {mapper_id} PRG-ROM size {} is not a non-zero multiple of 8 KiB",
342                prg_rom.len()
343            )));
344        }
345        let chr_is_ram = chr_rom.is_empty();
346        let chr: Box<[u8]> = if chr_is_ram {
347            vec![0u8; 0x8000].into_boxed_slice() // 32 KiB CHR-RAM (245 needs >8K).
348        } else {
349            if !chr_rom.len().is_multiple_of(CHR_BANK_1K) {
350                return Err(MapperError::Invalid(format!(
351                    "mapper {mapper_id} CHR-ROM size {} is not a multiple of 1 KiB",
352                    chr_rom.len()
353                )));
354            }
355            chr_rom
356        };
357        let prg_count_8k = prg_rom.len() / PRG_BANK_8K;
358        let chr_count_1k = (chr.len() / CHR_BANK_1K).max(1);
359        Ok(Self {
360            board,
361            core: Mmc3Clone::new(prg_count_8k, chr_count_1k, mirroring),
362            prg_rom,
363            chr,
364            chr_is_ram,
365            vram: vec![0u8; 2 * NAMETABLE_SIZE].into_boxed_slice(),
366            outer: 0,
367            outer2: 0,
368            wram: if matches!(board, CloneBoard::M245) {
369                vec![0u8; 0x2000].into_boxed_slice()
370            } else {
371                Box::new([])
372            },
373        })
374    }
375
376    /// Resolve the final 8 KiB PRG bank for a CPU slot after the board outer
377    /// transform.
378    fn resolve_prg(&self, slot: usize) -> usize {
379        let base = self.core.prg_bank(slot);
380        let count = self.core.prg_count_8k;
381        let bank = match self.board {
382            CloneBoard::M44 => {
383                let block = (self.outer & 0x07).min(6) as usize;
384                let mask = if block <= 5 { 0x0F } else { 0x1F };
385                (base & mask) | (block * 0x10)
386            }
387            CloneBoard::M49 => {
388                let block = ((self.outer >> 6) & 0x03) as usize;
389                if self.outer & 0x01 != 0 {
390                    (base & 0x0F) | (block * 0x10)
391                } else {
392                    ((self.outer >> 4) & 0x03) as usize * 4 + slot
393                }
394            }
395            CloneBoard::M52 => {
396                if self.outer & 0x08 != 0 {
397                    (base & 0x0F) | ((self.outer as usize & 0x07) << 4)
398                } else {
399                    (base & 0x1F) | ((self.outer as usize & 0x06) << 4)
400                }
401            }
402            CloneBoard::M115 => {
403                if self.outer & 0x80 != 0 {
404                    if self.outer & 0x20 != 0 {
405                        ((self.outer as usize & 0x0F) >> 1) * 4 + slot
406                    } else {
407                        let b16 = (self.outer as usize & 0x0F) * 2;
408                        b16 + (slot & 0x01)
409                    }
410                } else {
411                    base
412                }
413            }
414            CloneBoard::M134 => (base & 0x1F) | ((self.outer as usize & 0x02) << 4),
415            CloneBoard::M189 => {
416                let page = ((self.outer as usize) | (self.outer as usize >> 4)) & 0x07;
417                page * 4 + slot
418            }
419            CloneBoard::M205 => {
420                let block = self.outer as usize & 0x03;
421                let mask = if block <= 1 { 0x1F } else { 0x0F };
422                (base & mask) | (block * 0x10)
423            }
424            CloneBoard::M238 => base,
425            CloneBoard::M245 => {
426                let or = if self.core.regs[0] & 0x02 != 0 {
427                    0x40
428                } else {
429                    0
430                };
431                (base & 0x3F) | or
432            }
433            CloneBoard::M348 => (base & 0x0F) | ((self.outer as usize & 0x0C) << 2),
434            CloneBoard::M366 => (base & 0x0F) | (self.outer as usize & 0x30),
435        };
436        bank % count
437    }
438
439    /// Resolve the final 1 KiB CHR bank for a PPU slot after the board outer
440    /// transform.
441    fn resolve_chr(&self, slot: usize) -> usize {
442        let base = self.core.chr_bank(slot);
443        let count = self.core.chr_count_1k;
444        let bank = match self.board {
445            CloneBoard::M44 => {
446                let block = (self.outer & 0x07).min(6) as usize;
447                let mask = if block <= 5 { 0x7F } else { 0xFF };
448                (base & mask) | (block * 0x80)
449            }
450            CloneBoard::M49 => {
451                let block = ((self.outer >> 6) & 0x03) as usize;
452                (base & 0x7F) | (block * 0x80)
453            }
454            CloneBoard::M52 => {
455                if self.outer & 0x40 != 0 {
456                    (base & 0x7F)
457                        | (((self.outer as usize & 0x04) | ((self.outer as usize >> 4) & 0x03))
458                            << 7)
459                } else {
460                    (base & 0xFF)
461                        | (((self.outer as usize & 0x04) | ((self.outer as usize >> 4) & 0x02))
462                            << 7)
463                }
464            }
465            CloneBoard::M115 => base | ((self.outer2 as usize & 0x01) << 8),
466            CloneBoard::M134 => (base & 0xFF) | ((self.outer as usize & 0x20) << 3),
467            CloneBoard::M189 => base,
468            CloneBoard::M205 => {
469                let block = self.outer as usize & 0x03;
470                if block >= 2 {
471                    (base & 0x7F) | 0x100
472                } else {
473                    base | if block == 1 { 0x80 } else { 0 }
474                }
475            }
476            CloneBoard::M238 => base,
477            CloneBoard::M245 => base, // CHR-RAM; handled in ppu_read.
478            CloneBoard::M348 => (base & 0x7F) | ((self.outer as usize & 0x0C) << 5),
479            CloneBoard::M366 => (base & 0x7F) | ((self.outer as usize & 0x30) << 3),
480        };
481        bank % count
482    }
483}
484
485impl Mapper for Mmc3CloneMapper {
486    // Mapper 245's WRAM is battery-backed; empty on the other clones.
487    fn sram(&self) -> &[u8] {
488        &self.wram
489    }
490    fn sram_mut(&mut self) -> &mut [u8] {
491        &mut self.wram
492    }
493
494    fn caps(&self) -> MapperCaps {
495        MapperCaps {
496            // v2.9.7: the A12 filter's clock (`notify_cpu_cycle`); the bus
497            // calls it only on boards that declare this.
498            cpu_cycle_hook: true,
499            audio: false,
500            frame_event_hook: false,
501            irq_source: true,
502        }
503    }
504
505    fn cpu_read(&mut self, addr: u16) -> u8 {
506        match addr {
507            0x8000..=0x9FFF => {
508                let bank = self.resolve_prg(0);
509                self.prg_rom[bank * PRG_BANK_8K + (addr as usize & 0x1FFF)]
510            }
511            0xA000..=0xBFFF => {
512                let bank = self.resolve_prg(1);
513                self.prg_rom[bank * PRG_BANK_8K + (addr as usize & 0x1FFF)]
514            }
515            0xC000..=0xDFFF => {
516                let bank = self.resolve_prg(2);
517                self.prg_rom[bank * PRG_BANK_8K + (addr as usize & 0x1FFF)]
518            }
519            0xE000..=0xFFFF => {
520                let bank = self.resolve_prg(3);
521                self.prg_rom[bank * PRG_BANK_8K + (addr as usize & 0x1FFF)]
522            }
523            // 115 protection read-back at $5000-$5FFF.
524            0x5000..=0x5FFF if matches!(self.board, CloneBoard::M115) => self.outer2,
525            // 238 security read-back at $4020-$7FFF.
526            0x4020..=0x7FFF if matches!(self.board, CloneBoard::M238) => self.outer2,
527            0x6000..=0x7FFF if !self.wram.is_empty() => self.wram[usize::from(addr - 0x6000)],
528            _ => 0,
529        }
530    }
531
532    fn cpu_read_unmapped(&self, addr: u16) -> bool {
533        match self.board {
534            // v2.7.2 (core audit §5.5): with no save RAM, `$6000-$7FFF`
535            // floats; see `Mapper::cpu_read_unmapped`. Not for M238, whose
536            // security register answers across the whole window.
537            CloneBoard::M115 => {
538                (0x4020..=0x4FFF).contains(&addr)
539                    || (matches!(addr, 0x6000..=0x7FFF) && self.sram().is_empty())
540            }
541            CloneBoard::M238 => false, // $4020-$7FFF is all mapped (security reg).
542            _ => {
543                (0x4020..=0x5FFF).contains(&addr)
544                    || (matches!(addr, 0x6000..=0x7FFF) && self.sram().is_empty())
545            }
546        }
547    }
548
549    fn cpu_write(&mut self, addr: u16, value: u8) {
550        match self.board {
551            CloneBoard::M115 => match addr {
552                0x5080 => self.outer2 = value,
553                0x4100..=0x7FFF => {
554                    if addr & 0x01 == 0 {
555                        self.outer = value; // PRG override reg.
556                    } else {
557                        self.outer2 = value; // CHR-hi reg (bit 0 used).
558                    }
559                }
560                0x8000..=0xFFFF => self.core.write_register(addr, value),
561                _ => {}
562            },
563            CloneBoard::M134 => {
564                if addr == 0x6001 {
565                    self.outer = value;
566                } else if (0x8000..=0xFFFF).contains(&addr) {
567                    self.core.write_register(addr, value);
568                }
569            }
570            CloneBoard::M189 => {
571                if (0x4120..=0x7FFF).contains(&addr) {
572                    self.outer = value;
573                } else if (0x8000..=0xFFFF).contains(&addr) {
574                    self.core.write_register(addr, value);
575                }
576            }
577            CloneBoard::M238 => {
578                if (0x4020..=0x7FFF).contains(&addr) {
579                    const LUT: [u8; 4] = [0x00, 0x02, 0x02, 0x03];
580                    self.outer2 = LUT[(value & 0x03) as usize];
581                } else if (0x8000..=0xFFFF).contains(&addr) {
582                    self.core.write_register(addr, value);
583                }
584            }
585            CloneBoard::M44 => {
586                if (0x8000..=0xFFFF).contains(&addr) {
587                    if addr & 0xE001 == 0xA001 {
588                        self.outer = value & 0x07;
589                    }
590                    self.core.write_register(addr, value);
591                }
592            }
593            CloneBoard::M348 => {
594                if (0x6800..=0x68FF).contains(&addr) {
595                    self.outer = value;
596                } else if (0x8000..=0xFFFF).contains(&addr) {
597                    self.core.write_register(addr, value);
598                }
599            }
600            CloneBoard::M366 => {
601                if (0x6000..=0x7FFF).contains(&addr) {
602                    self.outer = if addr < 0x7000 {
603                        (addr as u8) & 0x30
604                    } else {
605                        value & 0x30
606                    };
607                } else if (0x8000..=0xFFFF).contains(&addr) {
608                    self.core.write_register(addr, value);
609                }
610            }
611            CloneBoard::M49 | CloneBoard::M52 | CloneBoard::M205 => {
612                if (0x6000..=0x7FFF).contains(&addr) {
613                    self.outer = value;
614                } else if (0x8000..=0xFFFF).contains(&addr) {
615                    self.core.write_register(addr, value);
616                }
617            }
618            CloneBoard::M245 => {
619                if (0x6000..=0x7FFF).contains(&addr) {
620                    self.wram[usize::from(addr - 0x6000)] = value;
621                } else if (0x8000..=0xFFFF).contains(&addr) {
622                    self.core.write_register(addr, value);
623                }
624            }
625        }
626    }
627
628    fn ppu_read(&mut self, addr: u16) -> u8 {
629        let addr = addr & 0x3FFF;
630        match addr {
631            0x0000..=0x1FFF => {
632                if self.chr_is_ram {
633                    if matches!(self.board, CloneBoard::M245) {
634                        let half = if self.core.chr_mode { 0x1000 } else { 0 };
635                        let off = (half ^ (addr as usize & 0x1FFF)) & (self.chr.len() - 1);
636                        return self.chr[off];
637                    }
638                    return self.chr[addr as usize & (self.chr.len() - 1)];
639                }
640                let slot = (addr as usize) / CHR_BANK_1K;
641                let bank = self.resolve_chr(slot);
642                self.chr[bank * CHR_BANK_1K + (addr as usize & 0x3FF)]
643            }
644            0x2000..=0x3EFF => self.vram[nametable_offset(addr, self.core.mirroring)],
645            _ => 0,
646        }
647    }
648
649    fn ppu_write(&mut self, addr: u16, value: u8) {
650        let addr = addr & 0x3FFF;
651        match addr {
652            0x0000..=0x1FFF if self.chr_is_ram => {
653                if matches!(self.board, CloneBoard::M245) {
654                    let half = if self.core.chr_mode { 0x1000 } else { 0 };
655                    let off = (half ^ (addr as usize & 0x1FFF)) & (self.chr.len() - 1);
656                    self.chr[off] = value;
657                } else {
658                    let off = addr as usize & (self.chr.len() - 1);
659                    self.chr[off] = value;
660                }
661            }
662            0x2000..=0x3EFF => {
663                let off = nametable_offset(addr, self.core.mirroring);
664                self.vram[off] = value;
665            }
666            _ => {}
667        }
668    }
669
670    fn notify_a12(&mut self, level: bool) {
671        self.core.notify_a12(level);
672    }
673
674    fn notify_cpu_cycle(&mut self) {
675        self.core.a12.tick();
676    }
677
678    fn irq_pending(&self) -> bool {
679        self.core.irq_pending
680    }
681
682    fn irq_acknowledge(&mut self) {
683        self.core.irq_pending = false;
684    }
685
686    fn current_mirroring(&self) -> Mirroring {
687        self.core.mirroring
688    }
689
690    fn save_state(&self) -> Vec<u8> {
691        let chr_ram = if self.chr_is_ram { self.chr.len() } else { 0 };
692        let mut out = Vec::with_capacity(3 + Mmc3Clone::SAVE_LEN + self.vram.len() + chr_ram);
693        out.push(SAVE_STATE_VERSION);
694        out.push(self.outer);
695        out.push(self.outer2);
696        self.core.save(&mut out);
697        out.extend_from_slice(&self.vram);
698        if self.chr_is_ram {
699            out.extend_from_slice(&self.chr);
700        }
701        out.extend_from_slice(&self.wram);
702        out
703    }
704
705    fn load_state(&mut self, data: &[u8]) -> Result<(), MapperError> {
706        let chr_ram = if self.chr_is_ram { self.chr.len() } else { 0 };
707        let version = *data.first().ok_or(MapperError::WrongLength {
708            expected: 1,
709            got: 0,
710        })?;
711        if version != SAVE_STATE_VERSION {
712            return Err(MapperError::UnsupportedVersion(version));
713        }
714        let wram_len = self.wram.len();
715        let expected = 3 + Mmc3Clone::SAVE_LEN + self.vram.len() + chr_ram + wram_len;
716        if data.len() != expected {
717            return Err(MapperError::WrongLength {
718                expected,
719                got: data.len(),
720            });
721        }
722        self.outer = data[1];
723        self.outer2 = data[2];
724        let mut cursor = 3;
725        self.core.load(&data[cursor..cursor + Mmc3Clone::SAVE_LEN]);
726        cursor += Mmc3Clone::SAVE_LEN;
727        self.vram
728            .copy_from_slice(&data[cursor..cursor + self.vram.len()]);
729        cursor += self.vram.len();
730        if self.chr_is_ram {
731            self.chr
732                .copy_from_slice(&data[cursor..cursor + self.chr.len()]);
733            cursor += self.chr.len();
734        }
735        self.wram.copy_from_slice(&data[cursor..cursor + wram_len]);
736        Ok(())
737    }
738}
739
740macro_rules! clone_ctor {
741    ($fn_name:ident, $board:expr, $id:expr, $doc:expr) => {
742        #[doc = $doc]
743        ///
744        /// # Errors
745        /// [`MapperError::Invalid`] on a bad PRG/CHR size.
746        pub fn $fn_name(
747            prg_rom: Box<[u8]>,
748            chr_rom: Box<[u8]>,
749            mirroring: Mirroring,
750        ) -> Result<Mmc3CloneMapper, MapperError> {
751            Mmc3CloneMapper::new($board, prg_rom, chr_rom, mirroring, $id)
752        }
753    };
754}
755
756clone_ctor!(
757    new_m44,
758    CloneBoard::M44,
759    44,
760    "Mapper 44 (BMC SuperBig 7-in-1 MMC3 multicart)."
761);
762clone_ctor!(
763    new_m49,
764    CloneBoard::M49,
765    49,
766    "Mapper 49 (BMC 4-in-1 MMC3 multicart)."
767);
768clone_ctor!(
769    new_m52,
770    CloneBoard::M52,
771    52,
772    "Mapper 52 (BMC Mario 7-in-1 MMC3 multicart)."
773);
774clone_ctor!(
775    new_m115,
776    CloneBoard::M115,
777    115,
778    "Mapper 115 (Kasheng SFC-02B/-03/-004 MMC3 clone)."
779);
780clone_ctor!(
781    new_m134,
782    CloneBoard::M134,
783    134,
784    "Mapper 134 (T4A54A / WX-KB4K MMC3-clone multicart)."
785);
786clone_ctor!(
787    new_m189,
788    CloneBoard::M189,
789    189,
790    "Mapper 189 (TXC 32 KiB-PRG MMC3 clone)."
791);
792clone_ctor!(
793    new_m205,
794    CloneBoard::M205,
795    205,
796    "Mapper 205 (BMC 3-in-1 / 15-in-1 MMC3 multicart)."
797);
798clone_ctor!(
799    new_m238,
800    CloneBoard::M238,
801    238,
802    "Mapper 238 (MMC3 clone + $4020-$7FFF security LUT)."
803);
804clone_ctor!(
805    new_m245,
806    CloneBoard::M245,
807    245,
808    "Mapper 245 (Waixing MMC3 clone, CHR-RAM PRG-256K outer)."
809);
810clone_ctor!(
811    new_m348,
812    CloneBoard::M348,
813    348,
814    "Mapper 348 (BMC-830118C MMC3 multicart)."
815);
816clone_ctor!(
817    new_m366,
818    CloneBoard::M366,
819    366,
820    "Mapper 366 (BMC-GN-45 MMC3 multicart)."
821);
822
823// ===========================================================================
824// Sachen 8259 (A/B/C) — the 2 KiB-CHR variants of the protection ASIC.
825//
826// $4100 (addr & 0xC101 == 0x4100) : command — selects internal reg 0..=7.
827// $4101 (addr & 0xC101 == 0x4101) : data    — writes the selected reg (& 0x07).
828// 32 KiB fixed PRG ($8000), four 2 KiB CHR banks. The variants differ only by a
829// CHR left-shift and three per-slot OR constants:
830//   8259A: shift 1, chrOr [1,0,1]   (mapper 141)
831//   8259B: shift 0, chrOr [0,0,0]   (mapper 138)
832//   8259C: shift 2, chrOr [1,2,3]   (mapper 139)
833// reg7 bits 1-2 select mirroring (reg7 bit 0 = "simple mode" override).
834// reg5 selects the 32 KiB PRG bank; reg4 supplies the CHR high bits.
835// Register map per the NESdev wiki Sachen 8259 (mappers 138/139/141)
836// documentation; the implementation is derived from Mesen2's `Sachen/Sachen8259.h`
837// (GPL-3.0-or-later). See NOTICE + docs/originality-and-provenance.md §1.
838// ===========================================================================
839
840#[cfg(test)]
841#[allow(clippy::cast_possible_truncation)]
842mod tests {
843
844    /// v2.9.7 — the IRQ counter clocks once per scanline on the A12 stream the
845    /// PPU now reports (eight pulses per line), through MMC3's filter. Before,
846    /// it clocked on every rise and depended on the PPU delivering only one.
847    #[test]
848    fn irq_counts_scanlines_not_raw_a12_pulses() {
849        let mut m = new_m245(synth_prg_8k(16), Box::new([]), Mirroring::Vertical).unwrap();
850        assert_eq!(crate::a12_filter::irqs_over_scanlines(&mut m, 16), 2);
851    }
852
853    use super::*;
854
855    fn synth_prg_8k(banks: usize) -> Box<[u8]> {
856        let mut v = vec![0xFFu8; banks * PRG_BANK_8K];
857        for b in 0..banks {
858            v[b * PRG_BANK_8K] = b as u8;
859        }
860        v.into_boxed_slice()
861    }
862
863    fn synth_chr_1k(banks: usize) -> Box<[u8]> {
864        let mut v = vec![0u8; banks * CHR_BANK_1K];
865        for b in 0..banks {
866            v[b * CHR_BANK_1K] = b as u8;
867        }
868        v.into_boxed_slice()
869    }
870
871    #[test]
872    fn mmc3_clone_prg_layout_and_a12_irq() {
873        let mut m = new_m245(synth_prg_8k(16), Box::new([]), Mirroring::Vertical).unwrap();
874        m.cpu_write(0x8000, 0x06); // bank-select R6
875        m.cpu_write(0x8001, 5);
876        m.cpu_write(0x8000, 0x07); // bank-select R7
877        m.cpu_write(0x8001, 6);
878        assert_eq!(m.cpu_read(0x8000), 5); // R6 @ $8000
879        assert_eq!(m.cpu_read(0xA000), 6); // R7 @ $A000
880        assert_eq!(m.cpu_read(0xE000), 15); // last @ $E000
881
882        m.cpu_write(0xC000, 2); // latch
883        m.cpu_write(0xC001, 0); // reload
884        m.cpu_write(0xE001, 0); // enable
885        assert!(!m.irq_pending());
886        for _ in 0..3 {
887            m.notify_a12(false);
888            // v2.9.7: three CPU cycles low, as MMC3's A12 filter requires.
889            for _ in 0..3 {
890                m.notify_cpu_cycle();
891            }
892            m.notify_a12(true);
893        }
894        assert!(m.irq_pending());
895        m.cpu_write(0xE000, 0); // disable + ack
896        assert!(!m.irq_pending());
897    }
898
899    #[test]
900    fn m245_prg_outer_bank_from_reg0_bit1() {
901        let mut m = new_m245(synth_prg_8k(128), Box::new([]), Mirroring::Vertical).unwrap();
902        // m245: the PRG-A18 outer bit is R0 bit 1 (select reg 0, write value).
903        m.cpu_write(0x8000, 0x00); // select R0
904        m.cpu_write(0x8001, 0x02); // R0 bit 1 set -> PRG OR 0x40
905        m.cpu_write(0x8000, 0x06); // select R6
906        m.cpu_write(0x8001, 5); // R6 = 5
907        // R6 base 5 -> (5 & 0x3F) | 0x40 = 69.
908        assert_eq!(m.cpu_read(0x8000), 69);
909    }
910
911    #[test]
912    fn m115_chr_outer_and_protection_read() {
913        let mut m = new_m115(synth_prg_8k(32), synth_chr_1k(512), Mirroring::Vertical).unwrap();
914        m.cpu_write(0x4101, 0x01); // CHR-hi reg bit 0 -> +0x100.
915        assert_eq!(m.ppu_read(0x0000), 0); // bank 256 % 512 -> stored index 0.
916        m.cpu_write(0x5080, 0xAB);
917        assert_eq!(m.cpu_read(0x5000), 0xAB);
918    }
919
920    #[test]
921    fn m189_prg_32k_select_overrides_mmc3() {
922        let mut m = new_m189(synth_prg_8k(32), synth_chr_1k(64), Mirroring::Vertical).unwrap();
923        m.cpu_write(0x4120, 0x33); // (3|3) = 3 -> page 3 -> bank 12.
924        assert_eq!(m.cpu_read(0x8000), 12);
925        assert_eq!(m.cpu_read(0xA000), 13);
926    }
927
928    #[test]
929    fn mmc3_clone_save_state_round_trip() {
930        let mut m = new_m115(synth_prg_8k(32), synth_chr_1k(256), Mirroring::Vertical).unwrap();
931        m.cpu_write(0x8000, 0x06);
932        m.cpu_write(0x8001, 4);
933        m.cpu_write(0x4101, 0x01);
934        m.cpu_write(0xC000, 7);
935        m.ppu_write(0x2005, 0x5A);
936        let blob = m.save_state();
937        let mut m2 = new_m115(synth_prg_8k(32), synth_chr_1k(256), Mirroring::Vertical).unwrap();
938        m2.load_state(&blob).unwrap();
939        assert_eq!(m2.cpu_read(0x8000), m.cpu_read(0x8000));
940        assert_eq!(m2.ppu_read(0x2005), 0x5A);
941    }
942
943    #[test]
944    fn m245_chr_ram_round_trip() {
945        let mut m = new_m245(synth_prg_8k(16), Box::new([]), Mirroring::Vertical).unwrap();
946        m.ppu_write(0x0010, 0x42);
947        let blob = m.save_state();
948        let mut m2 = new_m245(synth_prg_8k(16), Box::new([]), Mirroring::Vertical).unwrap();
949        m2.load_state(&blob).unwrap();
950        assert_eq!(m2.ppu_read(0x0010), 0x42);
951    }
952}