Skip to main content

rustynes_mappers/
m240_cne_multicart.rs

1//! C&E multicart (mapper 240).
2//!
3//! One register carrying both the PRG and CHR bank fields, decoded in the
4//! `$4020-$5FFF` expansion window -- so it does not collide with the PRG-RAM
5//! window a bundled game may also be using.
6//!
7//! A discrete-logic board in the shape of the stock mappers (`NROM`, `CNROM`,
8//! `UxROM`, `GxROM`, `AxROM`): bank-select latch registers, no IRQ, no on-cart
9//! audio. Banking / mirroring semantics are cross-checked against the
10//! `GeraNES` reference emulator (cross-referenced, not copied)
11//! and the nesdev wiki, and validated by register-decode + save-state unit
12//! tests.
13//!
14//! See `docs/mappers.md` §Mapper coverage matrix.
15
16#![allow(
17    clippy::bool_to_int_with_if,
18    clippy::cast_lossless,
19    clippy::cast_possible_truncation,
20    clippy::doc_markdown,
21    clippy::match_same_arms,
22    clippy::missing_const_for_fn,
23    clippy::similar_names,
24    clippy::struct_excessive_bools,
25    clippy::too_many_lines,
26    clippy::unreadable_literal
27)]
28
29use crate::cartridge::Mirroring;
30use crate::mapper::{Mapper, MapperCaps, MapperError};
31use alloc::{boxed::Box, vec::Vec};
32use alloc::{format, vec};
33
34const PRG_BANK_32K: usize = 0x8000;
35const CHR_BANK_8K: usize = 0x2000;
36const NAMETABLE_SIZE: usize = 0x0400;
37const NAMETABLE_SIZE_U16: u16 = 0x0400;
38
39const SAVE_STATE_VERSION: u8 = 1;
40
41const fn nametable_offset(addr: u16, mirroring: Mirroring) -> usize {
42    let table = (((addr - 0x2000) / NAMETABLE_SIZE_U16) & 0x03) as u8;
43    let local = (addr as usize) & (NAMETABLE_SIZE - 1);
44    let physical = mirroring.physical_bank(table);
45    physical * NAMETABLE_SIZE + local
46}
47
48/// Mapper 240 (C&E multicart).
49pub struct Cne240 {
50    prg_rom: Box<[u8]>,
51    chr_rom: Box<[u8]>,
52    vram: Box<[u8]>,
53    prg_bank: u8,
54    chr_bank: u8,
55    mirroring: Mirroring,
56}
57
58impl Cne240 {
59    /// Construct a new mapper 240 board.
60    ///
61    /// # Errors
62    ///
63    /// Returns [`MapperError::Invalid`] when PRG is not a non-zero multiple of
64    /// 32 KiB or CHR-ROM is empty / not a multiple of 8 KiB.
65    pub fn new(
66        prg_rom: Box<[u8]>,
67        chr_rom: Box<[u8]>,
68        mirroring: Mirroring,
69    ) -> Result<Self, MapperError> {
70        if prg_rom.is_empty() || !prg_rom.len().is_multiple_of(PRG_BANK_32K) {
71            return Err(MapperError::Invalid(format!(
72                "mapper 240 PRG-ROM size {} is not a non-zero multiple of 32 KiB",
73                prg_rom.len()
74            )));
75        }
76        if chr_rom.is_empty() || !chr_rom.len().is_multiple_of(CHR_BANK_8K) {
77            return Err(MapperError::Invalid(format!(
78                "mapper 240 CHR-ROM size {} is not a non-zero multiple of 8 KiB",
79                chr_rom.len()
80            )));
81        }
82        Ok(Self {
83            prg_rom,
84            chr_rom,
85            vram: vec![0u8; 2 * NAMETABLE_SIZE].into_boxed_slice(),
86            prg_bank: 0,
87            chr_bank: 0,
88            mirroring,
89        })
90    }
91}
92
93impl Mapper for Cne240 {
94    fn caps(&self) -> MapperCaps {
95        MapperCaps::NONE
96    }
97
98    // The register window is write-only at $4020-$5FFF; reads there fall
99    // through to open bus, so the default `cpu_read_unmapped` is correct.
100
101    fn cpu_read(&mut self, addr: u16) -> u8 {
102        if (0x8000..=0xFFFF).contains(&addr) {
103            let count = (self.prg_rom.len() / PRG_BANK_32K).max(1);
104            let bank = (self.prg_bank as usize) % count;
105            self.prg_rom[bank * PRG_BANK_32K + (addr as usize - 0x8000)]
106        } else {
107            0
108        }
109    }
110
111    fn cpu_write(&mut self, addr: u16, value: u8) {
112        if (0x4020..=0x5FFF).contains(&addr) {
113            self.prg_bank = (value >> 4) & 0x0F;
114            self.chr_bank = value & 0x0F;
115        }
116    }
117
118    fn ppu_read(&mut self, addr: u16) -> u8 {
119        let addr = addr & 0x3FFF;
120        match addr {
121            0x0000..=0x1FFF => {
122                let count = (self.chr_rom.len() / CHR_BANK_8K).max(1);
123                let bank = (self.chr_bank as usize) % count;
124                self.chr_rom[bank * CHR_BANK_8K + addr as usize]
125            }
126            0x2000..=0x3EFF => self.vram[nametable_offset(addr, self.mirroring)],
127            _ => 0,
128        }
129    }
130
131    fn ppu_write(&mut self, addr: u16, value: u8) {
132        let addr = addr & 0x3FFF;
133        if let 0x2000..=0x3EFF = addr {
134            let off = nametable_offset(addr, self.mirroring);
135            self.vram[off] = value;
136        }
137    }
138
139    fn current_mirroring(&self) -> Mirroring {
140        self.mirroring
141    }
142
143    fn save_state(&self) -> Vec<u8> {
144        let mut out = Vec::with_capacity(3 + self.vram.len());
145        out.push(SAVE_STATE_VERSION);
146        out.push(self.prg_bank);
147        out.push(self.chr_bank);
148        out.extend_from_slice(&self.vram);
149        out
150    }
151
152    fn load_state(&mut self, data: &[u8]) -> Result<(), MapperError> {
153        let expected = 3 + self.vram.len();
154        if data.len() != expected {
155            return Err(MapperError::WrongLength {
156                expected,
157                got: data.len(),
158            });
159        }
160        if data[0] != SAVE_STATE_VERSION {
161            return Err(MapperError::UnsupportedVersion(data[0]));
162        }
163        self.prg_bank = data[1];
164        self.chr_bank = data[2];
165        self.vram.copy_from_slice(&data[3..3 + self.vram.len()]);
166        Ok(())
167    }
168}
169
170#[cfg(test)]
171mod tests {
172    use super::*;
173
174    fn synth_prg_32k(banks: usize) -> Box<[u8]> {
175        let mut v = vec![0xFFu8; banks * PRG_BANK_32K];
176        for b in 0..banks {
177            v[b * PRG_BANK_32K] = b as u8;
178        }
179        v.into_boxed_slice()
180    }
181
182    fn synth_chr_8k(banks: usize) -> Box<[u8]> {
183        let mut v = vec![0u8; banks * CHR_BANK_8K];
184        for b in 0..banks {
185            v[b * CHR_BANK_8K] = b as u8;
186        }
187        v.into_boxed_slice()
188    }
189
190    #[test]
191    fn m240_register_in_4020_5fff() {
192        let mut m = Cne240::new(synth_prg_32k(4), synth_chr_8k(16), Mirroring::Vertical).unwrap();
193        // value DDDD_PPPP: PRG = (v>>4)&0xF, CHR = v&0xF.
194        m.cpu_write(0x5000, 0b0011_1010); // PRG 3, CHR 10
195        assert_eq!(m.cpu_read(0x8000), 3);
196        assert_eq!(m.ppu_read(0x0000), 10);
197        // $4020 is the bottom of the register window.
198        m.cpu_write(0x4020, 0b0001_0101); // PRG 1, CHR 5
199        assert_eq!(m.cpu_read(0x8000), 1);
200        assert_eq!(m.ppu_read(0x0000), 5);
201        // Reads in the register window fall through to open bus.
202        assert!(m.cpu_read_unmapped(0x5000));
203    }
204}