rustynes_mappers/
m003_cnrom.rs1use crate::cartridge::Mirroring;
11use crate::mapper::{Mapper, MapperCaps, MapperError};
12use alloc::{boxed::Box, vec::Vec};
13use alloc::{format, vec};
14
15const PRG_BANK_16K: usize = 0x4000;
16const PRG_BANK_32K: usize = 0x8000;
17const CHR_BANK_8K: usize = 0x2000;
18const NAMETABLE_SIZE: usize = 0x0400;
19const NAMETABLE_SIZE_U16: u16 = 0x0400;
20
21const SAVE_STATE_VERSION: u8 = 1;
22
23pub struct CnRom {
25 prg_rom: Box<[u8]>,
26 chr_rom: Box<[u8]>,
27 vram: Box<[u8]>,
28 chr_bank: u8,
29 mirroring: Mirroring,
30}
31
32impl CnRom {
33 pub fn new(
40 prg_rom: Box<[u8]>,
41 chr_rom: Box<[u8]>,
42 mirroring: Mirroring,
43 ) -> Result<Self, MapperError> {
44 if prg_rom.len() != PRG_BANK_16K && prg_rom.len() != PRG_BANK_32K {
45 return Err(MapperError::Invalid(format!(
46 "CNROM expects 16 or 32 KiB PRG, got {} bytes",
47 prg_rom.len()
48 )));
49 }
50 if chr_rom.is_empty() || !chr_rom.len().is_multiple_of(CHR_BANK_8K) {
51 return Err(MapperError::Invalid(format!(
52 "CNROM expects non-empty CHR-ROM in 8 KiB units, got {} bytes",
53 chr_rom.len()
54 )));
55 }
56 Ok(Self {
57 prg_rom,
58 chr_rom,
59 vram: vec![0u8; 2 * NAMETABLE_SIZE].into_boxed_slice(),
60 chr_bank: 0,
61 mirroring,
62 })
63 }
64
65 const fn nametable_offset(&self, addr: u16) -> usize {
66 let table = (((addr - 0x2000) / NAMETABLE_SIZE_U16) & 0x03) as u8;
67 let local = (addr as usize) & (NAMETABLE_SIZE - 1);
68 let physical = self.mirroring.physical_bank(table);
69 physical * NAMETABLE_SIZE + local
70 }
71
72 fn read_prg(&self, addr: u16) -> u8 {
73 let off = (addr - 0x8000) as usize;
74 if self.prg_rom.len() == PRG_BANK_16K {
75 self.prg_rom[off & (PRG_BANK_16K - 1)]
76 } else {
77 self.prg_rom[off]
78 }
79 }
80}
81
82impl Mapper for CnRom {
83 fn caps(&self) -> MapperCaps {
86 MapperCaps::NONE
87 }
88
89 fn cpu_read(&mut self, addr: u16) -> u8 {
90 if (0x8000..=0xFFFF).contains(&addr) {
91 self.read_prg(addr)
92 } else {
93 0
94 }
95 }
96
97 fn cpu_write(&mut self, addr: u16, value: u8) {
98 if (0x8000..=0xFFFF).contains(&addr) {
99 let prg_byte = self.read_prg(addr);
102 let effective = value & prg_byte;
103 let bank_count = (self.chr_rom.len() / CHR_BANK_8K).max(1);
106 let mask = u8::try_from((bank_count - 1) | 0x03).unwrap_or(u8::MAX);
107 self.chr_bank = effective & mask;
108 }
109 }
110
111 fn chr_phys(&self, addr: u16) -> Option<u32> {
112 let bank_count = (self.chr_rom.len() / CHR_BANK_8K).max(1);
114 let bank = usize::from(self.chr_bank) % bank_count;
115 u32::try_from(bank * CHR_BANK_8K + usize::from(addr & 0x1FFF)).ok()
116 }
117
118 fn ppu_read(&mut self, addr: u16) -> u8 {
119 let addr = addr & 0x3FFF;
120 match addr {
121 0x0000..=0x1FFF => {
122 let bank_count = (self.chr_rom.len() / CHR_BANK_8K).max(1);
123 let bank = (self.chr_bank as usize) % bank_count;
124 self.chr_rom[bank * CHR_BANK_8K + (addr as usize)]
125 }
126 0x2000..=0x3EFF => self.vram[self.nametable_offset(addr)],
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 = self.nametable_offset(addr);
135 self.vram[off] = value;
136 }
137 }
139
140 fn current_mirroring(&self) -> Mirroring {
141 self.mirroring
142 }
143
144 fn has_hardwired_mirroring(&self) -> bool {
146 true
147 }
148
149 fn save_state(&self) -> Vec<u8> {
150 let mut out = Vec::with_capacity(2 + self.vram.len());
151 out.push(SAVE_STATE_VERSION);
152 out.push(self.chr_bank);
153 out.extend_from_slice(&self.vram);
154 out
155 }
156
157 fn load_state(&mut self, data: &[u8]) -> Result<(), MapperError> {
158 let expected = 2 + self.vram.len();
159 if data.len() != expected {
160 return Err(MapperError::WrongLength {
161 expected,
162 got: data.len(),
163 });
164 }
165 if data[0] != SAVE_STATE_VERSION {
166 return Err(MapperError::UnsupportedVersion(data[0]));
167 }
168 self.chr_bank = data[1];
169 self.vram.copy_from_slice(&data[2..2 + self.vram.len()]);
170 Ok(())
171 }
172}
173
174#[cfg(test)]
175#[allow(clippy::cast_possible_truncation)]
176mod tests {
177 use super::*;
178
179 fn synth_prg(bytes: usize, fill: u8) -> Box<[u8]> {
180 vec![fill; bytes].into_boxed_slice()
181 }
182
183 fn synth_chr(banks: usize) -> Box<[u8]> {
184 let mut v = vec![0u8; banks * CHR_BANK_8K];
185 for b in 0..banks {
186 v[b * CHR_BANK_8K] = b as u8;
187 }
188 v.into_boxed_slice()
189 }
190
191 #[test]
192 fn cnrom_chr_bank_select_with_no_conflict() {
193 let mut m = CnRom::new(
194 synth_prg(PRG_BANK_32K, 0xFF),
195 synth_chr(4),
196 Mirroring::Vertical,
197 )
198 .unwrap();
199 m.cpu_write(0x8000, 2);
201 assert_eq!(m.ppu_read(0x0000), 2);
202 }
203
204 #[test]
205 fn cnrom_bus_conflict_masks_write_with_prg_byte() {
206 let mut prg = vec![0u8; PRG_BANK_32K];
208 prg[0] = 0x01;
209 let mut m = CnRom::new(prg.into_boxed_slice(), synth_chr(4), Mirroring::Vertical).unwrap();
210 m.cpu_write(0x8000, 0x03);
211 assert_eq!(m.ppu_read(0x0000), 1);
212 }
213
214 #[test]
215 fn cnrom_save_state_round_trip() {
216 let mut m = CnRom::new(
217 synth_prg(PRG_BANK_32K, 0xFF),
218 synth_chr(4),
219 Mirroring::Vertical,
220 )
221 .unwrap();
222 m.cpu_write(0x8000, 3);
223 let blob = m.save_state();
224 let mut m2 = CnRom::new(
225 synth_prg(PRG_BANK_32K, 0xFF),
226 synth_chr(4),
227 Mirroring::Vertical,
228 )
229 .unwrap();
230 m2.load_state(&blob).unwrap();
231 assert_eq!(m2.ppu_read(0x0000), 3);
232 }
233}