Skip to main content

rustynes_mappers/
m013_cprom.rs

1//! CPROM (mapper 13) -- Nintendo discrete board with banked CHR-RAM.
2//!
3//! Unusual in that the switchable half is *RAM*, not ROM: PRG is a fixed
4//! 32 KiB with no banking at all, while the upper 4 KiB of the 16 KiB
5//! CHR-RAM is selected by the low two bits of a write-anywhere register.
6//! Used by Videomation.
7//!
8//! See `docs/mappers.md` §Mapper coverage matrix.
9
10#![allow(
11    clippy::cast_possible_truncation,
12    clippy::cast_lossless,
13    clippy::missing_const_for_fn,
14    clippy::needless_pass_by_ref_mut,
15    clippy::manual_range_patterns,
16    clippy::match_same_arms,
17    clippy::too_many_arguments
18)]
19
20use crate::cartridge::Mirroring;
21use crate::mapper::{Mapper, MapperCaps, MapperError};
22use alloc::{boxed::Box, vec::Vec};
23use alloc::{format, vec};
24
25const CHR_BANK_4K: usize = 0x1000;
26const NAMETABLE_SIZE: usize = 0x0400;
27const NAMETABLE_SIZE_U16: u16 = 0x0400;
28
29fn nametable_offset(addr: u16, mirroring: Mirroring) -> usize {
30    let table = (((addr - 0x2000) / NAMETABLE_SIZE_U16) & 0x03) as u8;
31    let local = (addr as usize) & (NAMETABLE_SIZE - 1);
32    let physical = mirroring.physical_bank(table);
33    physical * NAMETABLE_SIZE + local
34}
35
36/// CPROM (Mapper 13).
37pub struct Cprom {
38    prg_rom: Box<[u8]>,
39    chr_ram: Box<[u8]>, // 16 KiB total: 4 banks of 4 KiB.
40    vram: Box<[u8]>,
41    chr_bank: u8,
42    mirroring: Mirroring,
43}
44
45impl Cprom {
46    /// Construct a new CPROM mapper (NES Time Lord uses this).
47    ///
48    /// # Errors
49    ///
50    /// Returns [`MapperError::Invalid`] on size mismatch.
51    pub fn new(prg_rom: Box<[u8]>, mirroring: Mirroring) -> Result<Self, MapperError> {
52        if prg_rom.len() != 32 * 1024 {
53            return Err(MapperError::Invalid(format!(
54                "CPROM expects 32 KiB PRG, got {} bytes",
55                prg_rom.len()
56            )));
57        }
58        Ok(Self {
59            prg_rom,
60            chr_ram: vec![0u8; 16 * 1024].into_boxed_slice(),
61            vram: vec![0u8; 2 * NAMETABLE_SIZE].into_boxed_slice(),
62            chr_bank: 0,
63            mirroring,
64        })
65    }
66}
67
68impl Mapper for Cprom {
69    /// Fixed mirroring (v2.7.2, core audit §5.6): `nesdev_wiki/CPROM: "Nametable mirroring: Vertical"`, so a
70    /// per-game database correction of a wrong header bit is safe here.
71    fn has_hardwired_mirroring(&self) -> bool {
72        true
73    }
74
75    // v2.8.0 Phase 4 — no per-cycle hooks (no IRQ, no audio): the bus
76    // skips all four per-CPU-cycle dispatches for this board.
77    fn caps(&self) -> MapperCaps {
78        MapperCaps::NONE
79    }
80
81    fn cpu_read(&mut self, addr: u16) -> u8 {
82        if addr < 0x8000 {
83            return 0;
84        }
85        self.prg_rom[(addr as usize - 0x8000) % self.prg_rom.len()]
86    }
87
88    fn cpu_write(&mut self, addr: u16, value: u8) {
89        if addr >= 0x8000 {
90            self.chr_bank = value & 0x03;
91        }
92    }
93
94    fn ppu_read(&mut self, addr: u16) -> u8 {
95        let addr = addr & 0x3FFF;
96        match addr {
97            0x0000..=0x0FFF => self.chr_ram[addr as usize],
98            0x1000..=0x1FFF => {
99                let bank = (self.chr_bank as usize) & 0x03;
100                let off = bank * CHR_BANK_4K + (addr as usize - 0x1000);
101                self.chr_ram[off % self.chr_ram.len()]
102            }
103            0x2000..=0x3EFF => self.vram[nametable_offset(addr, self.mirroring) % self.vram.len()],
104            _ => 0,
105        }
106    }
107
108    fn ppu_write(&mut self, addr: u16, value: u8) {
109        let addr = addr & 0x3FFF;
110        match addr {
111            0x0000..=0x0FFF => self.chr_ram[addr as usize] = value,
112            0x1000..=0x1FFF => {
113                let bank = (self.chr_bank as usize) & 0x03;
114                let off = (bank * CHR_BANK_4K + (addr as usize - 0x1000)) % self.chr_ram.len();
115                self.chr_ram[off] = value;
116            }
117            0x2000..=0x3EFF => {
118                let off = nametable_offset(addr, self.mirroring) % self.vram.len();
119                self.vram[off] = value;
120            }
121            _ => {}
122        }
123    }
124
125    fn current_mirroring(&self) -> Mirroring {
126        self.mirroring
127    }
128
129    fn save_state(&self) -> Vec<u8> {
130        let mut out = Vec::with_capacity(2 + self.chr_ram.len() + self.vram.len());
131        out.push(1u8);
132        out.push(self.chr_bank);
133        out.extend_from_slice(&self.chr_ram);
134        out.extend_from_slice(&self.vram);
135        out
136    }
137
138    fn load_state(&mut self, data: &[u8]) -> Result<(), MapperError> {
139        let expected = 2 + self.chr_ram.len() + self.vram.len();
140        if data.len() != expected {
141            return Err(MapperError::WrongLength {
142                expected,
143                got: data.len(),
144            });
145        }
146        if data[0] != 1 {
147            return Err(MapperError::UnsupportedVersion(data[0]));
148        }
149        self.chr_bank = data[1];
150        self.chr_ram
151            .copy_from_slice(&data[2..2 + self.chr_ram.len()]);
152        let off = 2 + self.chr_ram.len();
153        self.vram.copy_from_slice(&data[off..off + self.vram.len()]);
154        Ok(())
155    }
156}
157
158#[cfg(test)]
159mod tests {
160    use super::*;
161
162    #[test]
163    fn cprom_chr_bank_select() {
164        let mut m =
165            Cprom::new(vec![0u8; 32 * 1024].into_boxed_slice(), Mirroring::Vertical).unwrap();
166        m.ppu_write(0x1000, 0xAA); // bank 0
167        m.cpu_write(0x8000, 1);
168        m.ppu_write(0x1000, 0xBB); // bank 1
169        m.cpu_write(0x8000, 0);
170        assert_eq!(m.ppu_read(0x1000), 0xAA);
171        m.cpu_write(0x8000, 1);
172        assert_eq!(m.ppu_read(0x1000), 0xBB);
173    }
174}