1use crate::cartridge::Mirroring;
13use crate::mapper::{Mapper, MapperCaps, MapperError};
14use alloc::{boxed::Box, vec::Vec};
15use alloc::{format, vec};
16
17pub const NROM_PRG_RAM_SIZE: usize = 0x2000;
21
22pub const NROM_CHR_RAM_SIZE: usize = 0x2000;
24
25const PRG_BANK_16K: usize = 0x4000;
26const PRG_BANK_32K: usize = 0x8000;
27const NAMETABLE_SIZE: usize = 0x0400;
28const NAMETABLE_SIZE_U16: u16 = 0x0400;
29
30const SAVE_STATE_VERSION: u8 = 1;
31
32pub struct Nrom {
39 prg_rom: Box<[u8]>,
40 chr: Box<[u8]>,
41 prg_ram: Box<[u8]>,
42 vram: Box<[u8]>,
47 chr_is_ram: bool,
48 mirroring: Mirroring,
49}
50
51impl Nrom {
52 pub fn new(
62 prg_rom: Box<[u8]>,
63 chr_rom: Box<[u8]>,
64 mirroring: Mirroring,
65 ) -> Result<Self, MapperError> {
66 if prg_rom.len() != PRG_BANK_16K && prg_rom.len() != PRG_BANK_32K {
67 return Err(MapperError::Invalid(format!(
68 "NROM expects 16 KiB or 32 KiB PRG-ROM, got {} bytes",
69 prg_rom.len()
70 )));
71 }
72
73 let chr_is_ram = chr_rom.is_empty();
74 let chr: Box<[u8]> = if chr_is_ram {
75 vec![0u8; NROM_CHR_RAM_SIZE].into_boxed_slice()
76 } else if chr_rom.len() == NROM_CHR_RAM_SIZE {
77 chr_rom
78 } else {
79 return Err(MapperError::Invalid(format!(
80 "NROM expects 8 KiB CHR-ROM, got {} bytes",
81 chr_rom.len()
82 )));
83 };
84
85 Ok(Self {
86 prg_rom,
87 chr,
88 prg_ram: vec![0u8; NROM_PRG_RAM_SIZE].into_boxed_slice(),
89 vram: vec![0u8; 2 * NAMETABLE_SIZE].into_boxed_slice(),
90 chr_is_ram,
91 mirroring,
92 })
93 }
94
95 const fn nametable_offset(&self, addr: u16) -> usize {
98 let table = ((addr - 0x2000) / NAMETABLE_SIZE_U16) & 0x03;
100 let local = (addr as usize) & (NAMETABLE_SIZE - 1);
101 let physical = match self.mirroring {
102 Mirroring::Horizontal => match table {
103 0 | 1 => 0,
104 _ => 1,
105 },
106 Mirroring::Vertical => match table {
107 0 | 2 => 0,
108 _ => 1,
109 },
110 Mirroring::SingleScreenA => 0,
111 Mirroring::SingleScreenB => 1,
112 Mirroring::FourScreen | Mirroring::MapperControlled => match table {
116 0 | 2 => 0,
117 _ => 1,
118 },
119 };
120 physical * NAMETABLE_SIZE + local
121 }
122}
123
124impl Mapper for Nrom {
125 fn sram(&self) -> &[u8] {
126 &self.prg_ram
127 }
128 fn sram_mut(&mut self) -> &mut [u8] {
129 &mut self.prg_ram
130 }
131 fn caps(&self) -> MapperCaps {
134 MapperCaps::NONE
135 }
136
137 fn cpu_read(&mut self, addr: u16) -> u8 {
138 match addr {
139 0x6000..=0x7FFF => self.prg_ram[(addr - 0x6000) as usize],
140 0x8000..=0xFFFF => {
141 let idx = (addr - 0x8000) as usize;
142 if self.prg_rom.len() == PRG_BANK_16K {
143 self.prg_rom[idx & (PRG_BANK_16K - 1)]
145 } else {
146 self.prg_rom[idx]
147 }
148 }
149 _ => 0,
152 }
153 }
154
155 fn cpu_write(&mut self, addr: u16, value: u8) {
156 if let 0x6000..=0x7FFF = addr {
157 self.prg_ram[(addr - 0x6000) as usize] = value;
158 }
159 }
161
162 fn chr_phys(&self, addr: u16) -> Option<u32> {
163 if self.chr_is_ram {
166 None
167 } else {
168 Some(u32::from(addr & 0x1FFF))
169 }
170 }
171
172 fn ppu_read(&mut self, addr: u16) -> u8 {
173 let addr = addr & 0x3FFF;
174 match addr {
175 0x0000..=0x1FFF => self.chr[addr as usize],
176 0x2000..=0x3EFF => {
177 let off = self.nametable_offset(addr);
178 self.vram[off]
179 }
180 _ => 0,
184 }
185 }
186
187 fn ppu_write(&mut self, addr: u16, value: u8) {
188 let addr = addr & 0x3FFF;
189 match addr {
190 0x0000..=0x1FFF => {
191 if self.chr_is_ram {
192 self.chr[addr as usize] = value;
193 }
194 }
196 0x2000..=0x3EFF => {
197 let off = self.nametable_offset(addr);
198 self.vram[off] = value;
199 }
200 _ => {}
201 }
202 }
203
204 fn current_mirroring(&self) -> Mirroring {
205 self.mirroring
206 }
207
208 fn has_hardwired_mirroring(&self) -> bool {
210 true
211 }
212
213 fn save_state(&self) -> Vec<u8> {
214 let mut out = Vec::with_capacity(1 + self.prg_ram.len() + self.vram.len() + self.chr.len());
215 out.push(SAVE_STATE_VERSION);
216 out.extend_from_slice(&self.prg_ram);
217 out.extend_from_slice(&self.vram);
218 if self.chr_is_ram {
219 out.extend_from_slice(&self.chr);
220 }
221 out
222 }
223
224 fn load_state(&mut self, data: &[u8]) -> Result<(), MapperError> {
225 let need_chr = if self.chr_is_ram { self.chr.len() } else { 0 };
226 let expected = 1 + self.prg_ram.len() + self.vram.len() + need_chr;
227 if data.len() != expected {
228 return Err(MapperError::WrongLength {
229 expected,
230 got: data.len(),
231 });
232 }
233 if data[0] != SAVE_STATE_VERSION {
234 return Err(MapperError::UnsupportedVersion(data[0]));
235 }
236 let mut cursor = 1;
237 self.prg_ram
238 .copy_from_slice(&data[cursor..cursor + self.prg_ram.len()]);
239 cursor += self.prg_ram.len();
240 self.vram
241 .copy_from_slice(&data[cursor..cursor + self.vram.len()]);
242 cursor += self.vram.len();
243 if self.chr_is_ram {
244 self.chr
245 .copy_from_slice(&data[cursor..cursor + self.chr.len()]);
246 }
247 Ok(())
248 }
249}
250
251#[cfg(test)]
252#[allow(clippy::cast_possible_truncation)]
253mod tests {
254 use super::*;
255
256 fn synth_prg_16k() -> Box<[u8]> {
257 (0..PRG_BANK_16K)
259 .map(|i| (i & 0xFF) as u8)
260 .collect::<Vec<_>>()
261 .into_boxed_slice()
262 }
263
264 fn synth_prg_32k() -> Box<[u8]> {
265 (0..PRG_BANK_32K)
266 .map(|i| ((i >> 4) & 0xFF) as u8)
267 .collect::<Vec<_>>()
268 .into_boxed_slice()
269 }
270
271 fn synth_chr_8k() -> Box<[u8]> {
272 (0..NROM_CHR_RAM_SIZE)
273 .map(|i| ((i ^ 0xA5) & 0xFF) as u8)
274 .collect::<Vec<_>>()
275 .into_boxed_slice()
276 }
277
278 #[test]
279 fn nrom_16k_prg_mirrors_across_full_window() {
280 let mut m = Nrom::new(synth_prg_16k(), synth_chr_8k(), Mirroring::Horizontal).unwrap();
281 for off in [0u16, 1, 0xFF, 0x1234, 0x3FFF] {
283 let lo = m.cpu_read(0x8000 + off);
284 let hi = m.cpu_read(0xC000 + off);
285 assert_eq!(lo, hi, "mirror differs at offset {off:#06x}");
286 assert_eq!(lo, (off & 0xFF) as u8);
287 }
288 }
289
290 #[test]
291 fn nrom_32k_prg_does_not_mirror() {
292 let mut m = Nrom::new(synth_prg_32k(), synth_chr_8k(), Mirroring::Vertical).unwrap();
293 for off in 0u16..0x8000u16 {
294 let want = ((off as usize >> 4) & 0xFF) as u8;
295 assert_eq!(m.cpu_read(0x8000 + off), want);
296 }
297 }
298
299 #[test]
300 fn nrom_chr_rom_writes_ignored() {
301 let mut m = Nrom::new(synth_prg_16k(), synth_chr_8k(), Mirroring::Horizontal).unwrap();
302 let before = m.ppu_read(0x0123);
303 m.ppu_write(0x0123, before.wrapping_add(1));
304 assert_eq!(m.ppu_read(0x0123), before);
305 }
306
307 #[test]
308 fn nrom_chr_ram_writes_round_trip() {
309 let mut m = Nrom::new(
310 synth_prg_16k(),
311 Vec::new().into_boxed_slice(),
312 Mirroring::Horizontal,
313 )
314 .unwrap();
315 for addr in [0x0000u16, 0x07FF, 0x1234, 0x1FFF] {
316 m.ppu_write(addr, addr as u8);
317 }
318 for addr in [0x0000u16, 0x07FF, 0x1234, 0x1FFF] {
319 assert_eq!(m.ppu_read(addr), addr as u8);
320 }
321 }
322
323 #[test]
324 fn nrom_prg_ram_round_trip() {
325 let mut m = Nrom::new(synth_prg_16k(), synth_chr_8k(), Mirroring::Horizontal).unwrap();
326 m.cpu_write(0x6000, 0xAB);
327 m.cpu_write(0x6FFF, 0xCD);
328 m.cpu_write(0x7FFF, 0xEF);
329 assert_eq!(m.cpu_read(0x6000), 0xAB);
330 assert_eq!(m.cpu_read(0x6FFF), 0xCD);
331 assert_eq!(m.cpu_read(0x7FFF), 0xEF);
332 }
333
334 #[test]
335 fn nrom_horizontal_mirroring() {
336 let mut m = Nrom::new(synth_prg_16k(), synth_chr_8k(), Mirroring::Horizontal).unwrap();
337 m.ppu_write(0x2000, 0x11);
339 assert_eq!(m.ppu_read(0x2400), 0x11);
340 m.ppu_write(0x2800, 0x22);
342 assert_eq!(m.ppu_read(0x2C00), 0x22);
343 assert_ne!(m.ppu_read(0x2000), m.ppu_read(0x2800));
345 }
346
347 #[test]
348 fn nrom_vertical_mirroring() {
349 let mut m = Nrom::new(synth_prg_16k(), synth_chr_8k(), Mirroring::Vertical).unwrap();
350 m.ppu_write(0x2000, 0x33);
351 assert_eq!(m.ppu_read(0x2800), 0x33);
352 m.ppu_write(0x2400, 0x44);
353 assert_eq!(m.ppu_read(0x2C00), 0x44);
354 assert_ne!(m.ppu_read(0x2000), m.ppu_read(0x2400));
355 }
356
357 #[test]
358 fn nrom_save_state_round_trip_chr_ram() {
359 let mut m = Nrom::new(
360 synth_prg_16k(),
361 Vec::new().into_boxed_slice(),
362 Mirroring::Horizontal,
363 )
364 .unwrap();
365 m.cpu_write(0x6010, 0xBE);
366 m.ppu_write(0x0010, 0xEF);
367 m.ppu_write(0x2010, 0xCA);
368 let blob = m.save_state();
369
370 let mut m2 = Nrom::new(
371 synth_prg_16k(),
372 Vec::new().into_boxed_slice(),
373 Mirroring::Horizontal,
374 )
375 .unwrap();
376 m2.load_state(&blob).unwrap();
377 assert_eq!(m2.cpu_read(0x6010), 0xBE);
378 assert_eq!(m2.ppu_read(0x0010), 0xEF);
379 assert_eq!(m2.ppu_read(0x2010), 0xCA);
380 }
381
382 #[test]
383 fn nrom_rejects_bad_prg_size() {
384 let prg = vec![0u8; 0x2000].into_boxed_slice();
385 assert!(Nrom::new(prg, synth_chr_8k(), Mirroring::Horizontal).is_err());
386 }
387}