Skip to main content

rustynes_mappers/
mmc3_boards.rs

1// SPDX-License-Identifier: GPL-3.0-or-later
2//! MMC3-based boards written from their NESdev pages (v2.9.6 "Roster"):
3//! mappers 12, 37, 45, 47, 74, 121, 191, 192, 194, 195 and 249, and mapper 4
4//! submapper 5 (the T9552 address scrambler).
5//!
6//! Every board here is a stock MMC3 (or a clone that behaves as one) with
7//! something between the MMC3's bank outputs and the memories: an outer bank
8//! register, a CHR-RAM overlay, or a protection latch that overrides PRG
9//! banks. The MMC3 itself is the project's own [`Mmc3`] (`m004_mmc3.rs`),
10//! used as a register file and IRQ counter through
11//! [`Mmc3::register_core`]; this module owns the ROM and resolves each access
12//! from the raw bank outputs ([`Mmc3::prg_bank_raw`], [`Mmc3::chr_bank_1k`]).
13//! That keeps the IRQ timing work of `m004_mmc3.rs` (the A12 filter, the
14//! Sharp / NEC revisions) shared by every board, and it leaves the mapper 4
15//! path itself untouched.
16//!
17//! **Provenance.** Each board is implemented from the vendored NESdev page
18//! named in its [`Board`] variant (`nesdev_wiki/output/INES_Mapper_NNN.md`)
19//! and from `MMC3.md`. No reference-emulator source was read. This is
20//! deliberately a separate module from `mmc3_clones.rs`, whose MMC3 variants
21//! carry a Mesen2 derivation record (`docs/originality-and-provenance.md`
22//! §1). Mixing independently written boards into that file would blur which
23//! regions that record covers.
24//!
25//! **Bank arithmetic.** Every bank number is reduced modulo the size of the
26//! memory it indexes before use, so no register value can index out of bounds.
27//! This is required for the `#![no_std]` chip stack, which cannot afford a
28//! panic on a register write.
29
30#![allow(
31    clippy::cast_possible_truncation,
32    clippy::cast_lossless,
33    clippy::missing_const_for_fn,
34    clippy::doc_markdown,
35    clippy::match_same_arms
36)]
37
38use crate::cartridge::Mirroring;
39use crate::m004_mmc3::{Mmc3, Mmc3Revision};
40use crate::mapper::{Mapper, MapperCaps, MapperDebugInfo, MapperError};
41use alloc::string::ToString;
42use alloc::{boxed::Box, format, vec, vec::Vec};
43
44const PRG_BANK_8K: usize = 0x2000;
45const CHR_BANK_1K: usize = 0x0400;
46const WRAM_8K: usize = 0x2000;
47
48/// Save-state layout version for [`Mmc3Board`].
49const SAVE_STATE_VERSION: u8 = 1;
50
51/// Which board an [`Mmc3Board`] models.
52#[derive(Debug, Clone, Copy, PartialEq, Eq)]
53pub enum Board {
54    /// Mapper 12 submapper 0, Gouder SL-5020B (`INES_Mapper_012.md`): an
55    /// MMC3A plus a GAL register at `$4100` (mask `$E100`) supplying CHR A18
56    /// separately for each pattern table.
57    M12,
58    /// Mapper 37, "Super Mario Bros. + Tetris + Nintendo World Cup"
59    /// (`INES_Mapper_037.md`): a 74HC161 in the MMC3's PRG-RAM window.
60    M37,
61    /// Mapper 45, GA23C (`INES_Mapper_045.md`): four outer registers written
62    /// in turn at `$6000`, a lock bit, and a `$6001` reset.
63    M45,
64    /// Mapper 47, "Super Spike V'Ball + Nintendo World Cup"
65    /// (`INES_Mapper_047.md`): one block bit in the PRG-RAM window.
66    M47,
67    /// Mapper 74, Waixing 43-393 (`INES_Mapper_074.md`): CHR banks 8 and 9
68    /// are 2 KiB of CHR-RAM.
69    M74,
70    /// Mapper 121, Kasheng A9711 / A9713 (`INES_Mapper_121.md`): a protection
71    /// array at `$5000` and bit-reversed PRG overrides at `$8001` / `$8003`.
72    M121,
73    /// Mapper 191 (`INES_Mapper_191.md`): CHR bank bit 7 selects 2 KiB of
74    /// CHR-RAM.
75    M191,
76    /// Mapper 192, Waixing FS308 (`INES_Mapper_192.md`): CHR banks 8-11 are
77    /// 4 KiB of CHR-RAM.
78    M192,
79    /// Mapper 194 (`INES_Mapper_194.md`): CHR banks 0 and 1 are 2 KiB of
80    /// CHR-RAM.
81    M194,
82    /// Mapper 195, Waixing FS303 (`INES_Mapper_195.md`): which CHR banks are
83    /// RAM is chosen by the bank a PPU write lands on.
84    M195,
85    /// Mapper 4 submapper 5 (`T9552.md`): Waixing's T9552 scrambles PRG
86    /// A14-A17 and CHR A12-A17 by the pattern written to `$5000`. The file
87    /// stores the banks in the `$5000 = $02` order, the true one.
88    M4T9552,
89    /// Mapper 249 (`INES_Mapper_249.md` → `T9552.md`): the same board, with
90    /// the file stored in the `$5000 = $00` order.
91    M249,
92}
93
94impl Board {
95    const fn id(self) -> u16 {
96        match self {
97            Self::M12 => 12,
98            Self::M37 => 37,
99            Self::M45 => 45,
100            Self::M47 => 47,
101            Self::M74 => 74,
102            Self::M121 => 121,
103            Self::M191 => 191,
104            Self::M192 => 192,
105            Self::M194 => 194,
106            Self::M195 => 195,
107            Self::M4T9552 => 4,
108            Self::M249 => 249,
109        }
110    }
111
112    const fn name(self) -> &'static str {
113        match self {
114            Self::M12 => "SL-5020B (12)",
115            Self::M37 => "SMB+Tetris+NWC (37)",
116            Self::M45 => "GA23C (45)",
117            Self::M47 => "Spike V'Ball+NWC (47)",
118            Self::M74 => "Waixing 43-393 (74)",
119            Self::M121 => "Kasheng A9711/A9713 (121)",
120            Self::M191 => "Waixing MMC3 CHR-RAM (191)",
121            Self::M192 => "Waixing FS308 (192)",
122            Self::M194 => "Waixing MMC3 CHR-RAM (194)",
123            Self::M195 => "Waixing FS303 (195)",
124            Self::M4T9552 => "MMC3 + T9552 (4.5)",
125            Self::M249 => "Waixing T9552 (249)",
126        }
127    }
128
129    /// Default CHR-RAM overlay size for the mixed ROM/RAM boards.
130    const fn overlay_bytes(self) -> usize {
131        match self {
132            Self::M74 | Self::M191 | Self::M194 => 2 * CHR_BANK_1K,
133            Self::M192 => 4 * CHR_BANK_1K,
134            // FS303 mounts 32 KiB, of which CHR A10-A12 reach 8 KiB.
135            Self::M195 => 8 * CHR_BANK_1K,
136            _ => 0,
137        }
138    }
139
140    /// The Waixing boards are MMC3 boards with the usual 8 KiB of work RAM
141    /// (their games save to it); the multicarts put a register there instead.
142    const fn default_wram(self) -> usize {
143        match self {
144            Self::M74
145            | Self::M191
146            | Self::M192
147            | Self::M194
148            | Self::M195
149            | Self::M4T9552
150            | Self::M249 => WRAM_8K,
151            _ => 0,
152        }
153    }
154}
155
156/// T9552 PRG patterns (`T9552.md`): column `$5000 & 3`, row r = the ROM
157/// line (A14-A17) that MMC3 output line r's position maps to. Values 4-7 use
158/// the same columns as 0-3.
159const T9552_PRG: [[u8; 4]; 4] = [
160    [16, 17, 15, 14],
161    [17, 16, 14, 15],
162    [14, 15, 16, 17],
163    [15, 14, 17, 16],
164];
165
166/// T9552 CHR patterns: column `$5000 & 7`, rows over CHR A12-A17.
167const T9552_CHR: [[u8; 6]; 8] = [
168    [15, 12, 16, 17, 14, 13],
169    [14, 15, 13, 12, 17, 16],
170    [12, 13, 14, 15, 16, 17],
171    [16, 14, 12, 13, 17, 15],
172    [15, 13, 17, 16, 12, 14],
173    [14, 12, 15, 16, 17, 13],
174    [13, 16, 14, 15, 12, 17],
175    [12, 15, 16, 17, 13, 14],
176];
177
178/// Apply a T9552 pattern to the address lines `first..first+N` of `bank`
179/// (bank bit 0 is line `lsb_line`). Following the page: for each MMC3 line
180/// that is set, find its row in the current column, and take the line named
181/// in the same row of the file's column.
182fn t9552_scramble<const N: usize>(
183    bank: usize,
184    lsb_line: u8,
185    table: &[[u8; N]],
186    current: usize,
187    file: usize,
188) -> usize {
189    // Column 2 is the identity, so it lists the scrambled lines in order.
190    // Fixed-size loops over the table, no allocation: this runs on every PRG
191    // and CHR access.
192    let mut out = bank;
193    for &line in &table[2] {
194        out &= !(1 << (line - lsb_line));
195    }
196    for &line in &table[2] {
197        if bank & (1 << (line - lsb_line)) != 0 {
198            let row = table[current].iter().position(|&l| l == line).unwrap_or(0);
199            out |= 1 << (table[file][row] - lsb_line);
200        }
201    }
202    out
203}
204
205/// Map a bank number onto an image of `count` banks that may not be a power
206/// of two (`nesdev_wiki/output/Non_power_of_two_ROM_size.md`).
207///
208/// The page's doubling algorithm grows such an image to the next power of
209/// two by repeatedly copying its last `lowbit(size)` banks onto its end: a
210/// 24-bank (192 KiB) image becomes `ABCC` (banks 24-31 repeat 16-23), a
211/// 20-bank (160 KiB) one grows 20 -> 24 -> 32. Plain `bank % count` is wrong
212/// for those images: it sends the MMC3's all-ones fixed bank (`$FF`) to bank
213/// 15 of 24 instead of 23, so `$E000` holds no reset vector and the game
214/// never starts (the 192 KiB and 160 KiB translations on mapper 191).
215///
216/// This is the inverse of the doubling, computed without building the grown
217/// image: reduce `bank` modulo the grown size, then, while it lies in a copied
218/// region `[s, s + lowbit(s))`, step it back onto the region's source. For a
219/// power-of-two `count` it is exactly `bank % count`. No allocation, a few
220/// iterations at most (one per set bit of `count`): this runs on every PRG and
221/// CHR-ROM access.
222fn mirror_bank(bank: usize, count: usize) -> usize {
223    if count == 0 {
224        return 0;
225    }
226    let mut bank = bank & (count.next_power_of_two() - 1);
227    while bank >= count {
228        // Find the doubling stage whose copied region holds `bank`.
229        // `isolate_lowest_one` is `lowbit(size)` (`size & size.wrapping_neg()`,
230        // spelled that way until v3.0.1 moved these crates to Rust 1.99).
231        let mut size = count;
232        while size + size.isolate_lowest_one() <= bank {
233            size += size.isolate_lowest_one();
234        }
235        bank -= size.isolate_lowest_one();
236    }
237    bank
238}
239
240/// Where a pattern-table access lands.
241enum Chr {
242    Rom(usize),
243    Ram(usize),
244}
245
246/// Mapper 195's power-on CHR-RAM selection (`$80`: banks `$28-$2B`).
247const M195_POWER_ON_MODE: u8 = 0x80;
248
249/// Mapper 45's outer registers at power-on, after a soft reset and after a
250/// `$6001` write (T-GA23C-POWERON). The page gives no value: it says only that
251/// `$6001` resets them "as a soft reset would". Register 2's 4-bit CHR-AND
252/// field is `$F`, which `chr_target` decodes to the 8-bit mask `$FF`: every
253/// MMC3 CHR bank bit passes. It is `$F` because two *Famicom Yarou* menus draw
254/// with CHR banks 0-7 before their first outer-register write, which needs a
255/// mask of at least three bits. A black-box trace of both dumps showed no
256/// `$5000-$7FFF` access before rendering. The maintainer chose `$F` over the
257/// bare minimum (2026-10-05), matching the PRG-AND's inverted encoding, where 0
258/// is the full window. PRG-OR, PRG-AND and CHR-OR stay 0, so the power-on PRG
259/// window is unchanged.
260const M45_RESET_REGS: [u8; 4] = [0x00, 0x00, 0x0F, 0x00];
261
262/// The four-entry protection array mapper 121 returns at `$5000-$5FFF`.
263const M121_PROTECTION: [u8; 4] = [0x83, 0x83, 0x42, 0x00];
264
265/// An MMC3 board with a board-specific layer between the MMC3's bank outputs
266/// and the memories. See the module docs.
267pub struct Mmc3Board {
268    board: Board,
269    core: Mmc3,
270    prg_rom: Box<[u8]>,
271    /// CHR-ROM, or 8 KiB of CHR-RAM when the image has none.
272    chr: Box<[u8]>,
273    chr_is_ram: bool,
274    /// The CHR-RAM overlay of the mixed ROM/RAM boards (74/191/192/194/195).
275    chr_ram: Box<[u8]>,
276    /// Work RAM at `$6000-$7FFF` (mapper 195: `$5000-$5FFF`), when present.
277    wram: Box<[u8]>,
278    /// Board registers. Meaning per board:
279    /// - 12: `regs[0]` = the `$4100` CHR A18 register;
280    /// - 37 / 47: `regs[0]` = the outer latch;
281    /// - 45: `regs[0..4]` = the four outer registers;
282    /// - 121: `regs[0]` = `$5180` outer bit, `regs[1]` = `$8001` latch,
283    ///   `regs[2]` = `$8003` index, `regs[3]` = CHR A18 mode;
284    /// - 195: `regs[0]` = the CHR-RAM selection.
285    regs: [u8; 4],
286    /// 45: which outer register the next `$6000` write reaches.
287    /// 121: the protection-array index.
288    index: u8,
289    /// 45: the outer registers are locked (`$6000` #3 bit 6).
290    locked: bool,
291    /// 121: PRG overrides for `$A000`, `$C000`, `$E000` (`None` = MMC3).
292    prg_override: [Option<u8>; 3],
293    /// 121: which override slot later `$8001` writes keep updating.
294    sticky: Option<u8>,
295    /// 45: the menu DIP switch position (0-7).
296    dip: u8,
297}
298
299impl Mmc3Board {
300    /// Construct a board.
301    ///
302    /// `wram_bytes` is the header's PRG-RAM size, or 0 to take the board's
303    /// default (8 KiB on the Waixing boards, none on the others).
304    /// `chr_ram_bytes` overrides the CHR-RAM overlay size on the mixed
305    /// ROM/RAM boards (NES 2.0 carries it), or 0 for the page's default.
306    ///
307    /// # Errors
308    ///
309    /// [`MapperError::Invalid`] when PRG is not a non-zero multiple of 8 KiB
310    /// or CHR is not a multiple of 1 KiB.
311    pub fn new(
312        board: Board,
313        prg_rom: Box<[u8]>,
314        chr_rom: Box<[u8]>,
315        mirroring: Mirroring,
316        wram_bytes: usize,
317        chr_ram_bytes: usize,
318    ) -> Result<Self, MapperError> {
319        let id = board.id();
320        if prg_rom.is_empty() || !prg_rom.len().is_multiple_of(PRG_BANK_8K) {
321            return Err(MapperError::Invalid(format!(
322                "mapper {id} PRG-ROM size {} is not a non-zero multiple of 8 KiB",
323                prg_rom.len()
324            )));
325        }
326        if !chr_rom.len().is_multiple_of(CHR_BANK_1K) {
327            return Err(MapperError::Invalid(format!(
328                "mapper {id} CHR-ROM size {} is not a multiple of 1 KiB",
329                chr_rom.len()
330            )));
331        }
332        let chr_is_ram = chr_rom.is_empty();
333        let chr = if chr_is_ram {
334            vec![0u8; 8 * CHR_BANK_1K].into_boxed_slice()
335        } else {
336            chr_rom
337        };
338        let overlay = if board.overlay_bytes() == 0 {
339            0
340        } else if chr_ram_bytes >= CHR_BANK_1K {
341            chr_ram_bytes
342        } else {
343            board.overlay_bytes()
344        };
345        let wram = if wram_bytes > 0 {
346            wram_bytes
347        } else {
348            board.default_wram()
349        };
350        // Mapper 195's optional RAM is 4 KiB at `$5000-$5FFF`.
351        let wram = if board == Board::M195 && wram_bytes == 0 {
352            0
353        } else if board == Board::M195 {
354            0x1000
355        } else {
356            wram
357        };
358        // Mapper 12 is an MMC3A: the alternate ("NEC") IRQ behaviour, which
359        // `Dragon Ball Z 5` needs (`INES_Mapper_012.md`, `MMC3.md` "IRQ
360        // Specifics").
361        let revision = if board == Board::M12 {
362            Mmc3Revision::Nec
363        } else {
364            Mmc3Revision::Sharp
365        };
366        let mut this = Self {
367            board,
368            core: Mmc3::register_core(mirroring, revision),
369            prg_rom,
370            chr,
371            chr_is_ram,
372            chr_ram: vec![0u8; overlay].into_boxed_slice(),
373            wram: vec![0u8; wram].into_boxed_slice(),
374            regs: if board == Board::M45 {
375                M45_RESET_REGS
376            } else {
377                [0; 4]
378            },
379            index: 0,
380            locked: false,
381            prg_override: [None; 3],
382            sticky: None,
383            dip: 0,
384        };
385        if board == Board::M195 {
386            this.regs[0] = M195_POWER_ON_MODE;
387        }
388        Ok(this)
389    }
390
391    /// Set mapper 45's menu DIP switch (0-7). The Super New Year Cart 15-in-1
392    /// reads it to pick one of eight menus.
393    pub fn set_dip(&mut self, dip: u8) {
394        self.dip = dip & 0x07;
395    }
396
397    /// The column of the T9552 tables the file's bank order follows.
398    fn t9552_file_column(&self) -> usize {
399        if self.board == Board::M249 { 0 } else { 2 }
400    }
401
402    /// Mapper 121's A9713 board carries 512 KiB of PRG and an outer bank.
403    fn is_a9713(&self) -> bool {
404        self.prg_rom.len() > 256 * 1024
405    }
406
407    /// The 8 KiB PRG bank for `addr` (`$8000-$FFFF`), before the modulo.
408    fn prg_bank(&self, addr: u16) -> usize {
409        let raw = self.core.prg_bank_raw(addr) as usize;
410        let r = |i: usize| self.regs[i] as usize;
411        match self.board {
412            Board::M12 | Board::M74 | Board::M191 | Board::M192 | Board::M194 | Board::M195 => raw,
413            Board::M4T9552 | Board::M249 => t9552_scramble(
414                raw,
415                13,
416                &T9552_PRG,
417                usize::from(self.regs[0] & 0x03),
418                self.t9552_file_column(),
419            ),
420            Board::M37 => {
421                // PRG A16 = Q0·Q1 + Q2·M16, PRG A17 = Q2 (the page's NAND
422                // equations), over the MMC3's A13-A15.
423                let q = r(0);
424                let (q0, q1, q2) = (q & 1, (q >> 1) & 1, (q >> 2) & 1);
425                let m16 = (raw >> 3) & 1;
426                let a16 = (q0 & q1) | (q2 & m16);
427                (raw & 0x07) | (a16 << 3) | (q2 << 4)
428            }
429            Board::M45 => {
430                // PRG A13-A18: MMC3 AND the inverted PRG-AND, OR the PRG-OR;
431                // A19-A20 from register 1's top bits, A21-A22 from
432                // register 2's.
433                let and = !r(3) & 0x3F;
434                ((raw & and) | (r(1) & 0x3F)) | (r(1) & 0xC0) | ((r(2) & 0xC0) << 2)
435            }
436            Board::M47 => (raw & 0x0F) | ((r(0) & 1) << 4),
437            Board::M121 => {
438                let slot = match addr & 0xE000 {
439                    0xA000 => Some(0),
440                    0xC000 => Some(1),
441                    0xE000 => Some(2),
442                    _ => None,
443                };
444                let inner = slot
445                    .and_then(|s| self.prg_override[s])
446                    .map_or(raw & 0x1F, |b| b as usize & 0x1F);
447                if self.is_a9713() {
448                    inner | ((r(0) & 1) << 5)
449                } else {
450                    inner
451                }
452            }
453        }
454    }
455
456    /// Resolve a pattern-table address to CHR-ROM or the CHR-RAM overlay.
457    fn chr_target(&self, addr: u16) -> Chr {
458        let addr = addr & 0x1FFF;
459        let raw = self.core.chr_bank_1k(addr);
460        let within = addr as usize & (CHR_BANK_1K - 1);
461        let r = |i: usize| self.regs[i] as usize;
462        let rom = |bank: usize| Chr::Rom(bank * CHR_BANK_1K + within);
463        let ram = |bank: usize| Chr::Ram(bank * CHR_BANK_1K + within);
464        match self.board {
465            Board::M12 => {
466                let a18 = if addr & 0x1000 == 0 {
467                    r(0) & 1
468                } else {
469                    (r(0) >> 4) & 1
470                };
471                rom((raw & 0xFF) | (a18 << 8))
472            }
473            Board::M37 => rom((raw & 0x7F) | (((r(0) >> 2) & 1) << 7)),
474            // T-GA23C-CHRRAM: CHR-RAM is addressed straight from PPU
475            // A10-A12, bypassing every CHR bank. The mapper 45 page is
476            // silent on CHR-RAM; mapper 372's page, the GA23C with a
477            // ROM/RAM switch, documents its RAM as "unbanked".
478            Board::M45 if self.chr_is_ram => Chr::Rom(usize::from(addr & 0x1FFF)),
479            Board::M45 => {
480                let c = r(2) & 0x0F;
481                let mask = if c >= 7 { 0xFF >> (15 - c) } else { 0 };
482                rom((raw & mask) | r(0) | ((r(2) & 0xF0) << 4))
483            }
484            Board::M47 => rom((raw & 0x7F) | ((r(0) & 1) << 7)),
485            Board::M121 => {
486                if self.is_a9713() {
487                    rom((raw & 0xFF) | ((r(0) & 1) << 8))
488                } else {
489                    // A9711: CHR A18 follows PPU A12, inverted in mode 0.
490                    let a12 = usize::from(addr & 0x1000 != 0);
491                    let a18 = if r(3) & 0x80 != 0 { a12 } else { a12 ^ 1 };
492                    rom((raw & 0xFF) | (a18 << 8))
493                }
494            }
495            Board::M4T9552 | Board::M249 => rom(t9552_scramble(
496                raw,
497                10,
498                &T9552_CHR,
499                usize::from(self.regs[0] & 0x07),
500                self.t9552_file_column(),
501            )),
502            Board::M74 if matches!(raw, 8 | 9) => ram(raw & 1),
503            Board::M191 if raw & 0x80 != 0 => ram(raw & 1),
504            Board::M192 if matches!(raw, 8..=11) => ram(raw & 3),
505            Board::M194 if raw <= 1 => ram(raw & 1),
506            Board::M195 if m195_ram_banks(self.regs[0]).is_some_and(|b| b.contains(&raw)) => {
507                // CHR A10-A12 reach the RAM; A13 is not connected.
508                ram(raw & 7)
509            }
510            _ => rom(raw),
511        }
512    }
513
514    /// A CHR-ROM (or whole-image CHR-RAM) byte offset reduced onto the
515    /// image, mirroring a non-power-of-two size per [`mirror_bank`].
516    fn chr_rom_offset(&self, off: usize) -> usize {
517        mirror_bank(off / CHR_BANK_1K, self.chr.len() / CHR_BANK_1K) * CHR_BANK_1K
518            + (off & (CHR_BANK_1K - 1))
519    }
520
521    fn read_chr(&self, addr: u16) -> u8 {
522        match self.chr_target(addr) {
523            Chr::Rom(off) => self.chr[self.chr_rom_offset(off)],
524            Chr::Ram(off) => self.chr_ram[off % self.chr_ram.len()],
525        }
526    }
527
528    /// Mapper 121's `$8003` index write, and a `$8001` write while the index
529    /// is one that keeps following the latch.
530    fn m121_apply(&mut self) {
531        let v = reverse_low_six(self.regs[1]);
532        match self.regs[2] & 0x3F {
533            0x26 => {
534                self.prg_override[2] = Some(v);
535                self.sticky = Some(2);
536            }
537            0x28 => {
538                self.prg_override[1] = Some(v);
539                self.sticky = Some(1);
540            }
541            0x2A => {
542                self.prg_override[0] = Some(v);
543                self.sticky = Some(0);
544            }
545            0x2C => {
546                if v != 0 {
547                    self.prg_override[2] = Some(v);
548                }
549                self.sticky = None;
550            }
551            0x2F => self.sticky = None,
552            0x20 | 0x29 | 0x2B | 0x3C | 0x3F => {
553                self.prg_override[2] = Some(v);
554                self.sticky = None;
555            }
556            _ => {
557                self.prg_override = [None; 3];
558                self.sticky = None;
559            }
560        }
561    }
562
563    fn write_high(&mut self, addr: u16, value: u8) {
564        if self.board == Board::M121 {
565            match addr & 0xE003 {
566                0x8000 | 0x8002 => {
567                    if !self.is_a9713() {
568                        self.regs[3] = value;
569                    }
570                    self.core.cpu_write(addr, value);
571                }
572                0x8001 => {
573                    self.regs[1] = value;
574                    if let Some(slot) = self.sticky {
575                        self.prg_override[slot as usize] = Some(reverse_low_six(value));
576                    }
577                    self.core.cpu_write(addr, value);
578                }
579                0x8003 => {
580                    self.regs[2] = value;
581                    self.m121_apply();
582                    // It also reaches the MMC3's `$8000`.
583                    self.core.cpu_write(0x8000, value);
584                }
585                _ => self.core.cpu_write(addr, value),
586            }
587            return;
588        }
589        self.core.cpu_write(addr, value);
590    }
591}
592
593/// Mapper 195: the CHR banks the selection `mode` maps to RAM, per the
594/// table on `INES_Mapper_195.md`. Bit 4 set, or `$CA`, means none.
595fn m195_ram_banks(mode: u8) -> Option<core::ops::RangeInclusive<usize>> {
596    if mode & 0x10 != 0 {
597        return None;
598    }
599    match mode & 0xCA {
600        0x80 => Some(0x28..=0x2B),
601        0x82 => Some(0x00..=0x03),
602        0x88 => Some(0x4C..=0x4F),
603        0x8A => Some(0x64..=0x67),
604        0xC0 => Some(0x46..=0x47),
605        0xC2 => Some(0x7C..=0x7D),
606        0xC8 => Some(0x0A..=0x0B),
607        _ => None,
608    }
609}
610
611/// Mapper 121's latch transform: the low six bits reversed, the top two kept.
612const fn reverse_low_six(v: u8) -> u8 {
613    (v & 0xC0) | ((v & 0x3F).reverse_bits() >> 2)
614}
615
616impl Mapper for Mmc3Board {
617    fn sram(&self) -> &[u8] {
618        &self.wram
619    }
620
621    fn sram_mut(&mut self) -> &mut [u8] {
622        &mut self.wram
623    }
624
625    fn caps(&self) -> MapperCaps {
626        MapperCaps::CYCLE_IRQ
627    }
628
629    fn reset(&mut self) {
630        match self.board {
631            // The latch is cleared by the CIC reset line (`INES_Mapper_037.md`).
632            Board::M37 => self.regs[0] = 0,
633            // "resets the outer bank registers as a soft reset would".
634            Board::M45 => {
635                self.regs = M45_RESET_REGS;
636                self.index = 0;
637                self.locked = false;
638            }
639            _ => {}
640        }
641    }
642
643    fn cpu_read_unmapped(&self, addr: u16) -> bool {
644        match (self.board, addr) {
645            (Board::M12, 0x4020..=0x5FFF) => addr & 0xE100 != 0x4100,
646            (Board::M45 | Board::M121, 0x5000..=0x5FFF) => false,
647            (Board::M195, 0x5000..=0x5FFF) => self.wram.is_empty(),
648            (Board::M195, 0x6000..=0x7FFF) => true,
649            (_, 0x4020..=0x5FFF) => true,
650            (_, 0x6000..=0x7FFF) => self.wram.is_empty() || !self.core.prg_ram_enabled(),
651            _ => false,
652        }
653    }
654
655    fn cpu_read_driven_mask(&self, addr: u16) -> u8 {
656        match (self.board, addr) {
657            // The language bit (12) and the DIP bit (45) drive D0 only.
658            (Board::M12, _) => 0x01,
659            (Board::M45, 0x5000..=0x5FFF) => 0x01,
660            _ => 0xFF,
661        }
662    }
663
664    fn cpu_read(&mut self, addr: u16) -> u8 {
665        match addr {
666            0x8000..=0xFFFF => {
667                let bank = mirror_bank(self.prg_bank(addr), self.prg_rom.len() / PRG_BANK_8K);
668                self.prg_rom[bank * PRG_BANK_8K + (addr as usize & 0x1FFF)]
669            }
670            // Dragon Ball Z 5's language bit. The page says every known copy
671            // is hard-wired to Chinese but does not say which level that is;
672            // 0 is an assumption recorded in `docs/mappers.md`.
673            0x4020..=0x5FFF if self.board == Board::M12 => 0,
674            0x5000..=0x5FFF if self.board == Board::M45 => {
675                // `0101 AAAA AAAA ....`: A(4+n) reads 1 when the switch is at n.
676                u8::from((addr >> (4 + self.dip)) & 1 != 0)
677            }
678            0x5000..=0x5FFF if self.board == Board::M121 => {
679                M121_PROTECTION[usize::from(self.index & 3)]
680            }
681            0x5000..=0x5FFF if self.board == Board::M195 && !self.wram.is_empty() => {
682                self.wram[usize::from(addr & 0x0FFF) % self.wram.len()]
683            }
684            0x6000..=0x7FFF
685                if self.board != Board::M195
686                    && !self.wram.is_empty()
687                    && self.core.prg_ram_enabled() =>
688            {
689                self.wram[usize::from(addr - 0x6000) % self.wram.len()]
690            }
691            _ => 0,
692        }
693    }
694
695    fn cpu_write(&mut self, addr: u16, value: u8) {
696        match (self.board, addr) {
697            (_, 0x8000..=0xFFFF) => self.write_high(addr, value),
698            (Board::M12, 0x4020..=0x5FFF) if addr & 0xE100 == 0x4100 => self.regs[0] = value,
699            (Board::M37, 0x6000..=0x7FFF) if self.core.prg_ram_writable() => {
700                self.regs[0] = value & 0x07;
701            }
702            (Board::M47, 0x6000..=0x7FFF) if self.core.prg_ram_writable() => {
703                self.regs[0] = value & 0x01;
704            }
705            (Board::M45, 0x6000..=0x7FFF) => {
706                // The outer registers overlay WRAM and ignore the MMC3's
707                // PRG-RAM bits; the WRAM itself still obeys them.
708                match addr & 0xF001 {
709                    0x6000 if !self.locked => {
710                        self.regs[usize::from(self.index)] = value;
711                        if self.index == 3 && value & 0x40 != 0 {
712                            self.locked = true;
713                        }
714                        self.index = (self.index + 1) & 3;
715                    }
716                    0x6001 => {
717                        self.regs = M45_RESET_REGS;
718                        self.index = 0;
719                        self.locked = false;
720                    }
721                    _ => {}
722                }
723                if !self.wram.is_empty() && self.core.prg_ram_writable() {
724                    let len = self.wram.len();
725                    self.wram[usize::from(addr - 0x6000) % len] = value;
726                }
727            }
728            (Board::M4T9552 | Board::M249, 0x5000..=0x5FFF) => self.regs[0] = value,
729            (Board::M121, 0x5000..=0x5FFF) => {
730                self.index = value & 0x03;
731                if addr & 0xF180 == 0x5180 && self.is_a9713() {
732                    self.regs[0] = value >> 7;
733                }
734            }
735            (Board::M195, 0x5000..=0x5FFF) if !self.wram.is_empty() => {
736                let len = self.wram.len();
737                self.wram[usize::from(addr & 0x0FFF) % len] = value;
738            }
739            (Board::M195, _) => {}
740            (_, 0x6000..=0x7FFF) if !self.wram.is_empty() && self.core.prg_ram_writable() => {
741                let len = self.wram.len();
742                self.wram[usize::from(addr - 0x6000) % len] = value;
743            }
744            _ => {}
745        }
746    }
747
748    fn chr_phys(&self, addr: u16) -> Option<u32> {
749        match self.chr_target(addr) {
750            Chr::Rom(off) if !self.chr_is_ram => u32::try_from(self.chr_rom_offset(off)).ok(),
751            _ => None,
752        }
753    }
754
755    fn ppu_read(&mut self, addr: u16) -> u8 {
756        let addr = addr & 0x3FFF;
757        if addr < 0x2000 {
758            self.read_chr(addr)
759        } else {
760            self.core.ppu_read(addr)
761        }
762    }
763
764    fn ppu_write(&mut self, addr: u16, value: u8) {
765        let addr = addr & 0x3FFF;
766        if addr >= 0x2000 {
767            self.core.ppu_write(addr, value);
768            return;
769        }
770        match self.chr_target(addr) {
771            Chr::Ram(off) => {
772                let len = self.chr_ram.len();
773                self.chr_ram[off % len] = value;
774            }
775            Chr::Rom(off) if self.chr_is_ram => {
776                let off = self.chr_rom_offset(off);
777                self.chr[off] = value;
778            }
779            Chr::Rom(_) => {
780                // FS303: a write to a bank mapped to ROM selects which banks
781                // are RAM, from that bank's number (`INES_Mapper_195.md`;
782                // bit 7 "must be 1").
783                if self.board == Board::M195 {
784                    let bank = self.core.chr_bank_1k(addr);
785                    if bank & 0x80 != 0 {
786                        self.regs[0] = bank as u8;
787                    }
788                }
789            }
790        }
791    }
792
793    fn nametable_address(&self, addr: u16) -> u16 {
794        self.core.nametable_address(addr)
795    }
796
797    fn current_mirroring(&self) -> Mirroring {
798        self.core.current_mirroring()
799    }
800
801    fn notify_a12(&mut self, level: bool) {
802        self.core.notify_a12(level);
803    }
804
805    fn notify_a12_at_sub_dot(&mut self, level: bool, sub_dot: u8) {
806        self.core.notify_a12_at_sub_dot(level, sub_dot);
807    }
808
809    fn notify_cpu_cycle(&mut self) {
810        self.core.notify_cpu_cycle();
811    }
812
813    fn irq_pending(&self) -> bool {
814        self.core.irq_pending()
815    }
816
817    fn irq_acknowledge(&mut self) {
818        self.core.irq_acknowledge();
819    }
820
821    fn debug_info(&self) -> MapperDebugInfo {
822        let mut info = self.core.debug_info();
823        info.mapper_id = self.board.id();
824        info.name = self.board.name().to_string();
825        for (i, r) in self.regs.iter().enumerate() {
826            info.extra.push((format!("board{i}"), format!("{r:#04x}")));
827        }
828        if !self.chr_ram.is_empty() {
829            for slot in 0u16..8 {
830                let is_ram = matches!(self.chr_target(slot * 0x400), Chr::Ram(_));
831                info.chr_banks.push((
832                    format!("slot{slot}"),
833                    if is_ram { "RAM" } else { "ROM" }.to_string(),
834                ));
835            }
836        }
837        info
838    }
839
840    fn save_state(&self) -> Vec<u8> {
841        let core = self.core.save_state();
842        let chr = if self.chr_is_ram { self.chr.len() } else { 0 };
843        let mut out = Vec::with_capacity(16 + core.len() + chr + self.chr_ram.len());
844        out.push(SAVE_STATE_VERSION);
845        out.push(self.board.id() as u8);
846        out.extend_from_slice(&self.regs);
847        out.push(self.index);
848        out.push(u8::from(self.locked));
849        for o in self.prg_override {
850            out.push(u8::from(o.is_some()));
851            out.push(o.unwrap_or(0));
852        }
853        out.push(self.sticky.unwrap_or(0xFF));
854        out.push(self.dip);
855        out.extend_from_slice(&(core.len() as u32).to_le_bytes());
856        out.extend_from_slice(&core);
857        if self.chr_is_ram {
858            out.extend_from_slice(&self.chr);
859        }
860        out.extend_from_slice(&self.chr_ram);
861        out.extend_from_slice(&self.wram);
862        out
863    }
864
865    fn load_state(&mut self, data: &[u8]) -> Result<(), MapperError> {
866        const HEAD: usize = 2 + 4 + 2 + 6 + 2 + 4;
867        if data.len() < HEAD {
868            return Err(MapperError::WrongLength {
869                expected: HEAD,
870                got: data.len(),
871            });
872        }
873        if data[0] != SAVE_STATE_VERSION {
874            return Err(MapperError::UnsupportedVersion(data[0]));
875        }
876        if u16::from(data[1]) != self.board.id() & 0xFF {
877            return Err(MapperError::Invalid(format!(
878                "state is for mapper {}, this board is mapper {}",
879                data[1],
880                self.board.id()
881            )));
882        }
883        let core_len = u32::from_le_bytes([data[16], data[17], data[18], data[19]]) as usize;
884        let chr = if self.chr_is_ram { self.chr.len() } else { 0 };
885        // Checked: `core_len` comes from the blob, and on a 32-bit target
886        // (`usize` = u32) the plain sum wraps, passing the length check and
887        // panicking when the core section is sliced.
888        let expected = HEAD
889            .checked_add(core_len)
890            .and_then(|n| n.checked_add(chr + self.chr_ram.len() + self.wram.len()));
891        if expected != Some(data.len()) {
892            return Err(MapperError::WrongLength {
893                expected: expected.unwrap_or(usize::MAX),
894                got: data.len(),
895            });
896        }
897        let sticky = match data[14] {
898            0xFF => None,
899            s @ 0..=2 => Some(s),
900            s => {
901                return Err(MapperError::Invalid(format!(
902                    "override slot {s} out of range"
903                )));
904            }
905        };
906        let mut cur = HEAD;
907        self.core.load_state(&data[cur..cur + core_len])?;
908        cur += core_len;
909        self.regs.copy_from_slice(&data[2..6]);
910        self.index = data[6] & 3;
911        self.locked = data[7] != 0;
912        for (i, o) in self.prg_override.iter_mut().enumerate() {
913            *o = (data[8 + 2 * i] != 0).then_some(data[9 + 2 * i]);
914        }
915        self.sticky = sticky;
916        self.dip = data[15] & 7;
917        if self.chr_is_ram {
918            self.chr.copy_from_slice(&data[cur..cur + chr]);
919            cur += chr;
920        }
921        let n = self.chr_ram.len();
922        self.chr_ram.copy_from_slice(&data[cur..cur + n]);
923        cur += n;
924        self.wram.copy_from_slice(&data[cur..]);
925        Ok(())
926    }
927}
928
929#[cfg(test)]
930#[path = "mmc3_boards_tests.rs"]
931mod tests;