Skip to main content

rustynes_mappers/
m095_namcot3425.rs

1//! Namcot 3425 (mapper 95).
2//!
3//! The same Namco register-pair protocol as the 3446 in
4//! `m076_namcot3446.rs`, with one addition worth knowing: a bit of the CHR
5//! bank number is routed to the nametable select, so the board drives
6//! single-screen mirroring from the *CHR banking registers* rather than from a
7//! dedicated mirroring register.
8//!
9//! A best-effort (Tier-2) board: register-decode correctness verified against
10//! the reference emulators (`Mesen2`, `GeraNES`) and the nesdev wiki, with no
11//! commercial-oracle ROM in the tree. Banking math is direct slice indexing and
12//! every bank select wraps with `% count`, so a register write can never index
13//! out of bounds -- required for the `#![no_std]` chip stack, which cannot
14//! afford a panic on a register access.
15//!
16//! See `tier.rs` (`MapperTier::BestEffort`), `docs/adr/0011-mapper-tiering.md`,
17//! and `docs/mappers.md` §Mapper coverage matrix.
18
19#![allow(
20    clippy::bool_to_int_with_if,
21    clippy::cast_lossless,
22    clippy::cast_possible_truncation,
23    clippy::doc_markdown,
24    clippy::match_same_arms,
25    clippy::missing_const_for_fn,
26    clippy::similar_names,
27    clippy::struct_excessive_bools,
28    clippy::too_many_lines,
29    clippy::unreadable_literal
30)]
31
32use crate::cartridge::Mirroring;
33use crate::mapper::{Mapper, MapperCaps, MapperError};
34use alloc::{boxed::Box, vec::Vec};
35use alloc::{format, vec};
36
37const PRG_BANK_8K: usize = 0x2000;
38const NAMETABLE_SIZE: usize = 0x0400;
39const NAMETABLE_SIZE_U16: u16 = 0x0400;
40
41const SAVE_STATE_VERSION: u8 = 1;
42
43// ---------------------------------------------------------------------------
44// Shared nametable helper (mirrors the one in the other simple-mapper modules).
45// ---------------------------------------------------------------------------
46
47const fn nametable_offset(addr: u16, mirroring: Mirroring) -> usize {
48    let table = (((addr - 0x2000) / NAMETABLE_SIZE_U16) & 0x03) as u8;
49    let local = (addr as usize) & (NAMETABLE_SIZE - 1);
50    let physical = mirroring.physical_bank(table);
51    physical * NAMETABLE_SIZE + local
52}
53
54const CHR_BANK_1K: usize = 0x0400;
55
56/// Mapper 95 (`NAMCOT-3425`, *Dragon Buster*).
57pub struct Namcot3425M95 {
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[0],chr[1] are 2 KiB selects; chr[2..6] are 1 KiB selects.
64    chr_regs: [u8; 6],
65    one_screen_b: bool,
66}
67
68impl Namcot3425M95 {
69    /// Construct a new mapper 95 board.
70    ///
71    /// # Errors
72    ///
73    /// Returns [`MapperError::Invalid`] when PRG is not a non-zero multiple of
74    /// 8 KiB or CHR-ROM is empty / not a multiple of 1 KiB.
75    pub fn new(
76        prg_rom: Box<[u8]>,
77        chr_rom: Box<[u8]>,
78        _mirroring: Mirroring,
79    ) -> Result<Self, MapperError> {
80        if prg_rom.is_empty() || !prg_rom.len().is_multiple_of(PRG_BANK_8K) {
81            return Err(MapperError::Invalid(format!(
82                "mapper 95 PRG-ROM size {} is not a non-zero multiple of 8 KiB",
83                prg_rom.len()
84            )));
85        }
86        if chr_rom.is_empty() || !chr_rom.len().is_multiple_of(CHR_BANK_1K) {
87            return Err(MapperError::Invalid(format!(
88                "mapper 95 CHR-ROM size {} is not a non-zero multiple of 1 KiB",
89                chr_rom.len()
90            )));
91        }
92        Ok(Self {
93            prg_rom,
94            chr_rom,
95            vram: vec![0u8; 2 * NAMETABLE_SIZE].into_boxed_slice(),
96            reg_index: 0,
97            prg_banks: [0, 1],
98            chr_regs: [0; 6],
99            one_screen_b: false,
100        })
101    }
102
103    fn read_prg(&self, bank: usize, addr: u16) -> u8 {
104        let count = (self.prg_rom.len() / PRG_BANK_8K).max(1);
105        let bank = bank % count;
106        self.prg_rom[bank * PRG_BANK_8K + (addr as usize & 0x1FFF)]
107    }
108
109    fn read_chr(&self, addr: u16) -> u8 {
110        let count1k = (self.chr_rom.len() / CHR_BANK_1K).max(1);
111        // Resolve the 1 KiB bank for this CHR address.
112        let bank1k = match addr {
113            0x0000..=0x07FF => (self.chr_regs[0] as usize & !1) + ((addr as usize >> 10) & 1),
114            0x0800..=0x0FFF => (self.chr_regs[1] as usize & !1) + ((addr as usize >> 10) & 1),
115            0x1000..=0x13FF => self.chr_regs[2] as usize,
116            0x1400..=0x17FF => self.chr_regs[3] as usize,
117            0x1800..=0x1BFF => self.chr_regs[4] as usize,
118            _ => self.chr_regs[5] as usize,
119        };
120        let bank = bank1k % count1k;
121        self.chr_rom[bank * CHR_BANK_1K + (addr as usize & 0x03FF)]
122    }
123}
124
125impl Mapper for Namcot3425M95 {
126    fn caps(&self) -> MapperCaps {
127        MapperCaps::NONE
128    }
129
130    fn cpu_read(&mut self, addr: u16) -> u8 {
131        let last = (self.prg_rom.len() / PRG_BANK_8K).max(1) - 1;
132        match addr {
133            0x8000..=0x9FFF => self.read_prg(self.prg_banks[0] as usize, addr),
134            0xA000..=0xBFFF => self.read_prg(self.prg_banks[1] as usize, addr),
135            0xC000..=0xDFFF => self.read_prg(last - 1, addr),
136            0xE000..=0xFFFF => self.read_prg(last, addr),
137            _ => 0,
138        }
139    }
140
141    fn cpu_write(&mut self, addr: u16, value: u8) {
142        match addr {
143            0x8000..=0x9FFF if addr & 1 == 0 => self.reg_index = value & 0x07,
144            0x8000..=0x9FFF => match self.reg_index {
145                0 => {
146                    self.chr_regs[0] = value & 0x3F;
147                    // CHR reg 0 bit 5 drives one-screen select on this board.
148                    self.one_screen_b = (value & 0x20) != 0;
149                }
150                1 => self.chr_regs[1] = value & 0x3F,
151                2..=5 => self.chr_regs[self.reg_index as usize] = value & 0x3F,
152                6 => self.prg_banks[0] = value,
153                _ => self.prg_banks[1] = value,
154            },
155            _ => {}
156        }
157    }
158
159    fn ppu_read(&mut self, addr: u16) -> u8 {
160        let addr = addr & 0x3FFF;
161        match addr {
162            0x0000..=0x1FFF => self.read_chr(addr),
163            0x2000..=0x3EFF => self.vram[nametable_offset(addr, self.current_mirroring())],
164            _ => 0,
165        }
166    }
167
168    fn ppu_write(&mut self, addr: u16, value: u8) {
169        let addr = addr & 0x3FFF;
170        if (0x2000..=0x3EFF).contains(&addr) {
171            let off = nametable_offset(addr, self.current_mirroring());
172            self.vram[off] = value;
173        }
174    }
175
176    fn current_mirroring(&self) -> Mirroring {
177        if self.one_screen_b {
178            Mirroring::SingleScreenB
179        } else {
180            Mirroring::SingleScreenA
181        }
182    }
183
184    fn save_state(&self) -> Vec<u8> {
185        let mut out = Vec::with_capacity(11 + self.vram.len());
186        out.push(SAVE_STATE_VERSION);
187        out.push(self.reg_index);
188        out.extend_from_slice(&self.prg_banks);
189        out.extend_from_slice(&self.chr_regs);
190        out.push(u8::from(self.one_screen_b));
191        out.extend_from_slice(&self.vram);
192        out
193    }
194
195    fn load_state(&mut self, data: &[u8]) -> Result<(), MapperError> {
196        let expected = 11 + self.vram.len();
197        if data.len() != expected {
198            return Err(MapperError::WrongLength {
199                expected,
200                got: data.len(),
201            });
202        }
203        if data[0] != SAVE_STATE_VERSION {
204            return Err(MapperError::UnsupportedVersion(data[0]));
205        }
206        self.reg_index = data[1];
207        self.prg_banks.copy_from_slice(&data[2..4]);
208        self.chr_regs.copy_from_slice(&data[4..10]);
209        self.one_screen_b = data[10] != 0;
210        self.vram.copy_from_slice(&data[11..11 + self.vram.len()]);
211        Ok(())
212    }
213}
214
215#[cfg(test)]
216mod tests {
217    use super::*;
218
219    fn synth_chr_1k(banks: usize) -> Box<[u8]> {
220        let mut v = vec![0u8; banks * CHR_BANK_1K];
221        for b in 0..banks {
222            v[b * CHR_BANK_1K] = b as u8;
223        }
224        v.into_boxed_slice()
225    }
226
227    fn synth_prg_8k(banks: usize) -> Box<[u8]> {
228        let mut v = vec![0xFFu8; banks * PRG_BANK_8K];
229        for b in 0..banks {
230            v[b * PRG_BANK_8K] = b as u8;
231        }
232        v.into_boxed_slice()
233    }
234
235    #[test]
236    fn m95_prg_select_and_one_screen() {
237        let mut m =
238            Namcot3425M95::new(synth_prg_8k(8), synth_chr_1k(16), Mirroring::Vertical).unwrap();
239        // PRG reg 6 -> $8000.
240        m.cpu_write(0x8000, 6);
241        m.cpu_write(0x8001, 3);
242        assert_eq!(m.cpu_read(0x8000), 3);
243        // CHR reg 0, value with bit 5 set -> one-screen B.
244        m.cpu_write(0x8000, 0);
245        m.cpu_write(0x8001, 0x20);
246        assert_eq!(m.current_mirroring(), Mirroring::SingleScreenB);
247        // $C000/$E000 fixed to last two (6,7).
248        assert_eq!(m.cpu_read(0xC000), 6);
249        assert_eq!(m.cpu_read(0xE000), 7);
250    }
251
252    #[test]
253    fn m95_save_state_round_trip() {
254        let mut m =
255            Namcot3425M95::new(synth_prg_8k(8), synth_chr_1k(16), Mirroring::Vertical).unwrap();
256        m.cpu_write(0x8000, 7);
257        m.cpu_write(0x8001, 4);
258        m.cpu_write(0x8000, 0);
259        m.cpu_write(0x8001, 0x20);
260        let blob = m.save_state();
261        let mut m2 =
262            Namcot3425M95::new(synth_prg_8k(8), synth_chr_1k(16), Mirroring::Vertical).unwrap();
263        m2.load_state(&blob).unwrap();
264        assert_eq!(m2.cpu_read(0xA000), m.cpu_read(0xA000));
265        assert_eq!(m2.current_mirroring(), Mirroring::SingleScreenB);
266    }
267}