Skip to main content

rustynes_mappers/
m232_camerica_bf9096.rs

1//! Camerica / Codemasters `BF9096` (mapper 232) -- the Quattro multicarts.
2//!
3//! Two-level 16 KiB PRG banking: an outer block select and an inner bank
4//! select within that block, which is how a four-game cartridge presents each
5//! title as if it owned the whole address space.
6//!
7//! The single-game `BF9093` is a different ASIC on mapper 71; see
8//! `m071_camerica_bf9093.rs`.
9//!
10//! A discrete-logic board in the shape of the stock mappers (`NROM`, `CNROM`,
11//! `UxROM`, `GxROM`, `AxROM`): bank-select latch registers, no IRQ, no on-cart
12//! audio. Banking / mirroring semantics are cross-checked against the
13//! `GeraNES` reference emulator (cross-referenced, not copied)
14//! and the nesdev wiki, and validated by register-decode + save-state unit
15//! tests.
16//!
17//! See `docs/mappers.md` §Mapper coverage matrix.
18
19use crate::cartridge::Mirroring;
20use crate::mapper::{Mapper, MapperCaps, MapperError};
21use alloc::{boxed::Box, vec::Vec};
22use alloc::{format, vec};
23
24const PRG_BANK_16K: usize = 0x4000;
25const CHR_BANK_8K: usize = 0x2000;
26const NAMETABLE_SIZE: usize = 0x0400;
27const NAMETABLE_SIZE_U16: u16 = 0x0400;
28
29const SAVE_STATE_VERSION: u8 = 1;
30
31const fn nametable_offset(addr: u16, mirroring: Mirroring) -> usize {
32    let table = (((addr - 0x2000) / NAMETABLE_SIZE_U16) & 0x03) as u8;
33    let local = (addr as usize) & (NAMETABLE_SIZE - 1);
34    let physical = mirroring.physical_bank(table);
35    physical * NAMETABLE_SIZE + local
36}
37
38/// Mapper 232 (Camerica Quattro / `BF9096`).
39pub struct Camerica232 {
40    prg_rom: Box<[u8]>,
41    chr: Box<[u8]>,
42    vram: Box<[u8]>,
43    outer_block: u8,
44    inner_page: u8,
45    mirroring: Mirroring,
46}
47
48impl Camerica232 {
49    /// Construct a new mapper 232 board.
50    ///
51    /// # Errors
52    ///
53    /// Returns [`MapperError::Invalid`] when PRG is not a non-zero multiple of
54    /// 16 KiB. CHR is always 8 KiB RAM (any supplied CHR-ROM is rejected).
55    pub fn new(
56        prg_rom: Box<[u8]>,
57        chr_rom: Box<[u8]>,
58        mirroring: Mirroring,
59    ) -> Result<Self, MapperError> {
60        if prg_rom.is_empty() || !prg_rom.len().is_multiple_of(PRG_BANK_16K) {
61            return Err(MapperError::Invalid(format!(
62                "mapper 232 PRG-ROM size {} is not a non-zero multiple of 16 KiB",
63                prg_rom.len()
64            )));
65        }
66        let chr: Box<[u8]> = if chr_rom.is_empty() {
67            vec![0u8; CHR_BANK_8K].into_boxed_slice()
68        } else if chr_rom.len() == CHR_BANK_8K {
69            // Some dumps carry 8 KiB CHR-ROM; accept and use it read-only.
70            chr_rom
71        } else {
72            return Err(MapperError::Invalid(format!(
73                "mapper 232 expects 8 KiB CHR (RAM or ROM); got {} bytes",
74                chr_rom.len()
75            )));
76        };
77        Ok(Self {
78            prg_rom,
79            chr,
80            vram: vec![0u8; 2 * NAMETABLE_SIZE].into_boxed_slice(),
81            outer_block: 0,
82            inner_page: 0,
83            mirroring,
84        })
85    }
86
87    fn map_16k(&self, page_in_block: u8) -> usize {
88        let count = (self.prg_rom.len() / PRG_BANK_16K).max(1);
89        let bank = ((self.outer_block << 2) | (page_in_block & 0x03)) as usize;
90        bank % count
91    }
92}
93
94impl Mapper for Camerica232 {
95    fn caps(&self) -> MapperCaps {
96        MapperCaps::NONE
97    }
98
99    fn cpu_read(&mut self, addr: u16) -> u8 {
100        match addr {
101            0x8000..=0xBFFF => {
102                let bank = self.map_16k(self.inner_page);
103                self.prg_rom[bank * PRG_BANK_16K + (addr as usize - 0x8000)]
104            }
105            0xC000..=0xFFFF => {
106                let bank = self.map_16k(3);
107                self.prg_rom[bank * PRG_BANK_16K + (addr as usize - 0xC000)]
108            }
109            _ => 0,
110        }
111    }
112
113    fn cpu_write(&mut self, addr: u16, value: u8) {
114        match addr {
115            0x8000..=0xBFFF => self.outer_block = (value >> 3) & 0x03,
116            0xC000..=0xFFFF => self.inner_page = value & 0x03,
117            _ => {}
118        }
119    }
120
121    fn ppu_read(&mut self, addr: u16) -> u8 {
122        let addr = addr & 0x3FFF;
123        match addr {
124            0x0000..=0x1FFF => self.chr[addr as usize],
125            0x2000..=0x3EFF => self.vram[nametable_offset(addr, self.mirroring)],
126            _ => 0,
127        }
128    }
129
130    fn ppu_write(&mut self, addr: u16, value: u8) {
131        let addr = addr & 0x3FFF;
132        match addr {
133            0x0000..=0x1FFF => {
134                // CHR-RAM dumps allow writes; if CHR is the supplied 8 KiB
135                // image we still let the program scribble its 8 KiB window.
136                self.chr[addr as usize] = value;
137            }
138            0x2000..=0x3EFF => {
139                let off = nametable_offset(addr, self.mirroring);
140                self.vram[off] = value;
141            }
142            _ => {}
143        }
144    }
145
146    fn current_mirroring(&self) -> Mirroring {
147        self.mirroring
148    }
149
150    fn save_state(&self) -> Vec<u8> {
151        let mut out = Vec::with_capacity(3 + self.vram.len() + self.chr.len());
152        out.push(SAVE_STATE_VERSION);
153        out.push(self.outer_block);
154        out.push(self.inner_page);
155        out.extend_from_slice(&self.vram);
156        out.extend_from_slice(&self.chr);
157        out
158    }
159
160    fn load_state(&mut self, data: &[u8]) -> Result<(), MapperError> {
161        let expected = 3 + self.vram.len() + self.chr.len();
162        if data.len() != expected {
163            return Err(MapperError::WrongLength {
164                expected,
165                got: data.len(),
166            });
167        }
168        if data[0] != SAVE_STATE_VERSION {
169            return Err(MapperError::UnsupportedVersion(data[0]));
170        }
171        self.outer_block = data[1];
172        self.inner_page = data[2];
173        let mut cursor = 3;
174        self.vram
175            .copy_from_slice(&data[cursor..cursor + self.vram.len()]);
176        cursor += self.vram.len();
177        self.chr
178            .copy_from_slice(&data[cursor..cursor + self.chr.len()]);
179        Ok(())
180    }
181}
182
183#[cfg(test)]
184#[allow(clippy::cast_possible_truncation)]
185mod tests {
186    use super::*;
187
188    fn synth_prg_16k(banks: usize) -> Box<[u8]> {
189        let mut v = vec![0xFFu8; banks * PRG_BANK_16K];
190        for b in 0..banks {
191            v[b * PRG_BANK_16K] = b as u8;
192        }
193        v.into_boxed_slice()
194    }
195
196    #[test]
197    fn m232_two_level_banking() {
198        // 8 16 KiB banks = 2 blocks of 4 pages.
199        let mut m = Camerica232::new(synth_prg_16k(8), Box::new([]), Mirroring::Vertical).unwrap();
200        // Select outer block 1 ($8000 write, bits 4-3 = 0b01 << 3 = 0b1000).
201        m.cpu_write(0x8000, 0b0000_1000);
202        // Select inner page 2 ($C000 write, bits 1-0).
203        m.cpu_write(0xC000, 0b0000_0010);
204        // $8000-$BFFF reads bank (1<<2)|2 = 6.
205        assert_eq!(m.cpu_read(0x8000), 6);
206        // $C000-$FFFF is fixed to page 3 of block 1 -> bank (1<<2)|3 = 7.
207        assert_eq!(m.cpu_read(0xC000), 7);
208    }
209}