Skip to main content

rustynes_mappers/
m071_camerica_bf9093.rs

1//! Camerica / Codemasters BF9093 and relatives (mapper 71).
2//!
3//! A UxROM-shaped board: a 16 KiB PRG bank selected at `$C000-$FFFF` with the
4//! last bank fixed, CHR-RAM, and no IRQ. The Fire Hawk variant additionally
5//! decodes a single-screen mirroring bit at `$9000-$9FFF`, which is why the
6//! mirroring write window is separated from the bank-select window rather
7//! than sharing one write-anywhere decode.
8//!
9//! See `docs/mappers.md` §Mapper coverage matrix.
10
11#![allow(
12    clippy::cast_possible_truncation,
13    clippy::cast_lossless,
14    clippy::missing_const_for_fn,
15    clippy::needless_pass_by_ref_mut,
16    clippy::manual_range_patterns,
17    clippy::match_same_arms,
18    clippy::too_many_arguments
19)]
20
21use crate::cartridge::Mirroring;
22use crate::mapper::{Mapper, MapperCaps, MapperError};
23use alloc::{boxed::Box, vec::Vec};
24use alloc::{format, vec};
25
26const PRG_BANK_16K: usize = 0x4000;
27const CHR_BANK_8K: usize = 0x2000;
28const NAMETABLE_SIZE: usize = 0x0400;
29const NAMETABLE_SIZE_U16: u16 = 0x0400;
30
31fn 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/// Camerica / Codemasters BF9093 (Mapper 71).
39pub struct Camerica {
40    prg_rom: Box<[u8]>,
41    chr_ram: Box<[u8]>,
42    vram: Box<[u8]>,
43    prg_bank: u8,
44    mirroring: Mirroring,
45    has_single_screen: bool,
46}
47
48impl Camerica {
49    /// Construct a new Camerica mapper.
50    ///
51    /// # Errors
52    ///
53    /// Returns [`MapperError::Invalid`] on size mismatch.
54    pub fn new(
55        prg_rom: Box<[u8]>,
56        mirroring: Mirroring,
57        has_single_screen: bool,
58    ) -> Result<Self, MapperError> {
59        if prg_rom.is_empty() || !prg_rom.len().is_multiple_of(PRG_BANK_16K) {
60            return Err(MapperError::Invalid(format!(
61                "Camerica PRG-ROM size {} is not a non-zero multiple of 16 KiB",
62                prg_rom.len()
63            )));
64        }
65        Ok(Self {
66            prg_rom,
67            chr_ram: vec![0u8; CHR_BANK_8K].into_boxed_slice(),
68            vram: vec![0u8; 2 * NAMETABLE_SIZE].into_boxed_slice(),
69            prg_bank: 0,
70            mirroring,
71            has_single_screen,
72        })
73    }
74}
75
76impl Mapper for Camerica {
77    // v2.8.0 Phase 4 — no per-cycle hooks (no IRQ, no audio): the bus
78    // skips all four per-CPU-cycle dispatches for this board.
79    fn caps(&self) -> MapperCaps {
80        MapperCaps::NONE
81    }
82
83    fn cpu_read(&mut self, addr: u16) -> u8 {
84        let total_16k = (self.prg_rom.len() / PRG_BANK_16K).max(1);
85        let last = total_16k - 1;
86        match addr {
87            0x8000..=0xBFFF => {
88                let bank = (self.prg_bank as usize) % total_16k;
89                self.prg_rom[(bank * PRG_BANK_16K + (addr as usize - 0x8000)) % self.prg_rom.len()]
90            }
91            0xC000..=0xFFFF => {
92                self.prg_rom[(last * PRG_BANK_16K + (addr as usize - 0xC000)) % self.prg_rom.len()]
93            }
94            _ => 0,
95        }
96    }
97
98    fn cpu_write(&mut self, addr: u16, value: u8) {
99        match addr {
100            0x9000..=0x9FFF if self.has_single_screen => {
101                self.mirroring = if value & 0x10 == 0 {
102                    Mirroring::SingleScreenA
103                } else {
104                    Mirroring::SingleScreenB
105                };
106            }
107            0xC000..=0xFFFF | 0x8000..=0xBFFF => self.prg_bank = value & 0x0F,
108            _ => {}
109        }
110    }
111
112    fn ppu_read(&mut self, addr: u16) -> u8 {
113        let addr = addr & 0x3FFF;
114        match addr {
115            0x0000..=0x1FFF => self.chr_ram[addr as usize % self.chr_ram.len()],
116            0x2000..=0x3EFF => self.vram[nametable_offset(addr, self.mirroring) % self.vram.len()],
117            _ => 0,
118        }
119    }
120
121    fn ppu_write(&mut self, addr: u16, value: u8) {
122        let addr = addr & 0x3FFF;
123        match addr {
124            0x0000..=0x1FFF => {
125                let len = self.chr_ram.len();
126                self.chr_ram[addr as usize % len] = value;
127            }
128            0x2000..=0x3EFF => {
129                let off = nametable_offset(addr, self.mirroring) % self.vram.len();
130                self.vram[off] = value;
131            }
132            _ => {}
133        }
134    }
135
136    fn current_mirroring(&self) -> Mirroring {
137        self.mirroring
138    }
139
140    fn save_state(&self) -> Vec<u8> {
141        let mut out = Vec::with_capacity(4 + self.vram.len() + self.chr_ram.len());
142        out.push(1u8);
143        out.push(self.prg_bank);
144        out.push(self.mirroring as u8);
145        out.push(u8::from(self.has_single_screen));
146        out.extend_from_slice(&self.chr_ram);
147        out.extend_from_slice(&self.vram);
148        out
149    }
150
151    fn load_state(&mut self, data: &[u8]) -> Result<(), MapperError> {
152        let expected = 4 + self.chr_ram.len() + self.vram.len();
153        if data.len() != expected {
154            return Err(MapperError::WrongLength {
155                expected,
156                got: data.len(),
157            });
158        }
159        if data[0] != 1 {
160            return Err(MapperError::UnsupportedVersion(data[0]));
161        }
162        self.prg_bank = data[1];
163        self.mirroring = match data[2] {
164            0 => Mirroring::Horizontal,
165            1 => Mirroring::Vertical,
166            2 => Mirroring::SingleScreenA,
167            3 => Mirroring::SingleScreenB,
168            4 => Mirroring::FourScreen,
169            5 => Mirroring::MapperControlled,
170            other => return Err(MapperError::Invalid(format!("mirroring {other}"))),
171        };
172        self.has_single_screen = data[3] != 0;
173        let mut cur = 4usize;
174        self.chr_ram
175            .copy_from_slice(&data[cur..cur + self.chr_ram.len()]);
176        cur += self.chr_ram.len();
177        self.vram.copy_from_slice(&data[cur..cur + self.vram.len()]);
178        Ok(())
179    }
180}
181
182#[cfg(test)]
183mod tests {
184    use super::*;
185
186    const PRG_BANK_8K: usize = 0x2000;
187
188    fn synth(banks_8k: usize) -> Box<[u8]> {
189        let mut v = vec![0u8; banks_8k * PRG_BANK_8K];
190        for b in 0..banks_8k {
191            v[b * PRG_BANK_8K] = b as u8;
192        }
193        v.into_boxed_slice()
194    }
195
196    #[test]
197    fn camerica_bank_swap() {
198        let mut m = Camerica::new(synth(8 * 2), Mirroring::Vertical, false).unwrap();
199        // Default: bank 0 at $8000.
200        assert_eq!(m.cpu_read(0x8000), 0);
201        m.cpu_write(0xC000, 5);
202        // Bank 5 (16K bank index, but we have 16K chunks — total_16k = 16).
203        // bank 5 at 16K offset. Let's just check it swaps from 0.
204        assert_ne!(m.cpu_read(0x8000), 0);
205    }
206}