Skip to main content

rustynes_mappers/
m076_namcot3446.rs

1//! Namcot 3446 (mapper 76).
2//!
3//! A cut-down relative of the Namco 118 (`m088_namco118.rs`): the same register-pair
4//! protocol -- write a register index to the even address, then its value to
5//! the odd one -- but with a reduced bank layout and no IRQ. It is the shape
6//! Namco used for cheaper cartridges before the MMC3-class boards took over.
7//!
8//! Its sibling 3425 is in `m095_namcot3425.rs`.
9//!
10//! A best-effort (Tier-2) board: register-decode correctness verified against
11//! the reference emulators (`Mesen2`, `GeraNES`) and the nesdev wiki, with no
12//! commercial-oracle ROM in the tree. Banking math is direct slice indexing and
13//! every bank select wraps with `% count`, so a register write can never index
14//! out of bounds -- required for the `#![no_std]` chip stack, which cannot
15//! afford a panic on a register access.
16//!
17//! See `tier.rs` (`MapperTier::BestEffort`), `docs/adr/0011-mapper-tiering.md`,
18//! and `docs/mappers.md` §Mapper coverage matrix.
19
20#![allow(
21    clippy::bool_to_int_with_if,
22    clippy::cast_lossless,
23    clippy::cast_possible_truncation,
24    clippy::doc_markdown,
25    clippy::match_same_arms,
26    clippy::missing_const_for_fn,
27    clippy::similar_names,
28    clippy::struct_excessive_bools,
29    clippy::too_many_lines,
30    clippy::unreadable_literal
31)]
32
33use crate::cartridge::Mirroring;
34use crate::mapper::{Mapper, MapperCaps, MapperError};
35use alloc::{boxed::Box, vec::Vec};
36use alloc::{format, vec};
37
38const PRG_BANK_8K: usize = 0x2000;
39const CHR_BANK_2K: usize = 0x0800;
40const NAMETABLE_SIZE: usize = 0x0400;
41const NAMETABLE_SIZE_U16: u16 = 0x0400;
42
43const SAVE_STATE_VERSION: u8 = 1;
44
45// ---------------------------------------------------------------------------
46// Shared nametable helper (mirrors the one in the other simple-mapper modules).
47// ---------------------------------------------------------------------------
48
49const fn nametable_offset(addr: u16, mirroring: Mirroring) -> usize {
50    let table = (((addr - 0x2000) / NAMETABLE_SIZE_U16) & 0x03) as u8;
51    let local = (addr as usize) & (NAMETABLE_SIZE - 1);
52    let physical = mirroring.physical_bank(table);
53    physical * NAMETABLE_SIZE + local
54}
55
56/// Mapper 76 (`NAMCOT-3446`).
57pub struct Namcot3446M76 {
58    prg_rom: Box<[u8]>,
59    chr_rom: Box<[u8]>,
60    vram: Box<[u8]>,
61    reg_index: u8,
62    prg_banks: [u8; 2],
63    chr_banks: [u8; 4],
64    mirroring: Mirroring,
65}
66
67impl Namcot3446M76 {
68    /// Construct a new mapper 76 board.
69    ///
70    /// # Errors
71    ///
72    /// Returns [`MapperError::Invalid`] when PRG is not a non-zero multiple of
73    /// 8 KiB or CHR-ROM is empty / not a multiple of 2 KiB.
74    pub fn new(
75        prg_rom: Box<[u8]>,
76        chr_rom: Box<[u8]>,
77        mirroring: Mirroring,
78    ) -> Result<Self, MapperError> {
79        if prg_rom.is_empty() || !prg_rom.len().is_multiple_of(PRG_BANK_8K) {
80            return Err(MapperError::Invalid(format!(
81                "mapper 76 PRG-ROM size {} is not a non-zero multiple of 8 KiB",
82                prg_rom.len()
83            )));
84        }
85        if chr_rom.is_empty() || !chr_rom.len().is_multiple_of(CHR_BANK_2K) {
86            return Err(MapperError::Invalid(format!(
87                "mapper 76 CHR-ROM size {} is not a non-zero multiple of 2 KiB",
88                chr_rom.len()
89            )));
90        }
91        Ok(Self {
92            prg_rom,
93            chr_rom,
94            vram: vec![0u8; 2 * NAMETABLE_SIZE].into_boxed_slice(),
95            reg_index: 0,
96            prg_banks: [0, 1],
97            chr_banks: [0, 1, 2, 3],
98            mirroring,
99        })
100    }
101
102    fn prg_offset(&self, bank: usize, addr: u16) -> u8 {
103        let count = (self.prg_rom.len() / PRG_BANK_8K).max(1);
104        let bank = bank % count;
105        self.prg_rom[bank * PRG_BANK_8K + (addr as usize & 0x1FFF)]
106    }
107}
108
109impl Mapper for Namcot3446M76 {
110    fn caps(&self) -> MapperCaps {
111        MapperCaps::NONE
112    }
113
114    fn cpu_read(&mut self, addr: u16) -> u8 {
115        let last = (self.prg_rom.len() / PRG_BANK_8K).max(1) - 1;
116        match addr {
117            0x8000..=0x9FFF => self.prg_offset(self.prg_banks[0] as usize, addr),
118            0xA000..=0xBFFF => self.prg_offset(self.prg_banks[1] as usize, addr),
119            // `last - 1` would underflow on a single-8 KiB-bank ROM (`last == 0`);
120            // `prg_offset`'s modulo makes both forms identical for multi-bank ROMs.
121            0xC000..=0xDFFF => self.prg_offset(last.saturating_sub(1), addr),
122            0xE000..=0xFFFF => self.prg_offset(last, addr),
123            _ => 0,
124        }
125    }
126
127    fn cpu_write(&mut self, addr: u16, value: u8) {
128        match addr {
129            0x8000..=0x9FFF if (addr & 0x01) == 0 => self.reg_index = value & 0x07,
130            0x8000..=0x9FFF => match self.reg_index {
131                2 => self.chr_banks[0] = value,
132                3 => self.chr_banks[1] = value,
133                4 => self.chr_banks[2] = value,
134                5 => self.chr_banks[3] = value,
135                6 => self.prg_banks[0] = value,
136                7 => self.prg_banks[1] = value,
137                _ => {}
138            },
139            _ => {}
140        }
141    }
142
143    fn ppu_read(&mut self, addr: u16) -> u8 {
144        let addr = addr & 0x3FFF;
145        match addr {
146            0x0000..=0x1FFF => {
147                let slot = (addr >> 11) as usize & 0x03;
148                let count = (self.chr_rom.len() / CHR_BANK_2K).max(1);
149                let bank = (self.chr_banks[slot] as usize) % count;
150                self.chr_rom[bank * CHR_BANK_2K + (addr as usize & 0x07FF)]
151            }
152            0x2000..=0x3EFF => self.vram[nametable_offset(addr, self.mirroring)],
153            _ => 0,
154        }
155    }
156
157    fn ppu_write(&mut self, addr: u16, value: u8) {
158        let addr = addr & 0x3FFF;
159        if let 0x2000..=0x3EFF = addr {
160            let off = nametable_offset(addr, self.mirroring);
161            self.vram[off] = value;
162        }
163    }
164
165    fn current_mirroring(&self) -> Mirroring {
166        self.mirroring
167    }
168
169    fn save_state(&self) -> Vec<u8> {
170        let mut out = Vec::with_capacity(1 + 1 + 2 + 4 + self.vram.len());
171        out.push(SAVE_STATE_VERSION);
172        out.push(self.reg_index);
173        out.extend_from_slice(&self.prg_banks);
174        out.extend_from_slice(&self.chr_banks);
175        out.extend_from_slice(&self.vram);
176        out
177    }
178
179    fn load_state(&mut self, data: &[u8]) -> Result<(), MapperError> {
180        let expected = 1 + 1 + 2 + 4 + self.vram.len();
181        if data.len() != expected {
182            return Err(MapperError::WrongLength {
183                expected,
184                got: data.len(),
185            });
186        }
187        if data[0] != SAVE_STATE_VERSION {
188            return Err(MapperError::UnsupportedVersion(data[0]));
189        }
190        self.reg_index = data[1];
191        self.prg_banks.copy_from_slice(&data[2..4]);
192        self.chr_banks.copy_from_slice(&data[4..8]);
193        self.vram.copy_from_slice(&data[8..8 + self.vram.len()]);
194        Ok(())
195    }
196}
197
198#[cfg(test)]
199mod tests {
200    use super::*;
201
202    fn synth_prg_8k(banks: usize) -> Box<[u8]> {
203        let mut v = vec![0xFFu8; banks * PRG_BANK_8K];
204        for b in 0..banks {
205            v[b * PRG_BANK_8K] = b as u8;
206        }
207        v.into_boxed_slice()
208    }
209
210    fn synth_chr_2k(banks: usize) -> Box<[u8]> {
211        let mut v = vec![0u8; banks * CHR_BANK_2K];
212        for b in 0..banks {
213            v[b * CHR_BANK_2K] = b as u8;
214        }
215        v.into_boxed_slice()
216    }
217
218    #[test]
219    fn m76_register_pairs_select_banks() {
220        let mut m =
221            Namcot3446M76::new(synth_prg_8k(8), synth_chr_2k(8), Mirroring::Vertical).unwrap();
222        // index 6 -> PRG $8000 = bank 3.
223        m.cpu_write(0x8000, 6);
224        m.cpu_write(0x8001, 3);
225        assert_eq!(m.cpu_read(0x8000), 3);
226        // index 2 -> CHR slot 0 = bank 5.
227        m.cpu_write(0x8000, 2);
228        m.cpu_write(0x8001, 5);
229        assert_eq!(m.ppu_read(0x0000), 5);
230        // $C000/$E000 fixed to last two banks (6, 7).
231        assert_eq!(m.cpu_read(0xC000), 6);
232        assert_eq!(m.cpu_read(0xE000), 7);
233    }
234
235    #[test]
236    fn m76_save_state_round_trip() {
237        let mut m =
238            Namcot3446M76::new(synth_prg_8k(8), synth_chr_2k(8), Mirroring::Vertical).unwrap();
239        m.cpu_write(0x8000, 7);
240        m.cpu_write(0x8001, 4); // PRG $A000 = bank 4
241        let blob = m.save_state();
242        let mut m2 =
243            Namcot3446M76::new(synth_prg_8k(8), synth_chr_2k(8), Mirroring::Vertical).unwrap();
244        m2.load_state(&blob).unwrap();
245        assert_eq!(m2.cpu_read(0xA000), 4);
246    }
247}