1#![allow(
43 clippy::cast_possible_truncation,
44 clippy::cast_lossless,
45 clippy::missing_const_for_fn,
46 clippy::struct_excessive_bools,
47 clippy::doc_markdown
48)]
49
50use crate::cartridge::Mirroring;
51use crate::mapper::{Mapper, MapperCaps, MapperError};
52use alloc::{boxed::Box, vec::Vec};
53use alloc::{format, vec};
54
55const PRG_BANK_16K: usize = 0x4000;
56const CHR_BANK_2K: usize = 0x0800;
57const CHR_BANK_1K: usize = 0x0400;
58const NAMETABLE_SIZE: usize = 0x0400;
59const NAMETABLE_SIZE_U16: u16 = 0x0400;
60
61const SAVE_STATE_VERSION: u8 = 1;
62
63pub struct Sunsoft4 {
65 prg_rom: Box<[u8]>,
66 chr: Box<[u8]>,
67 prg_ram: Box<[u8]>,
68 vram: Box<[u8]>,
69 chr_is_ram: bool,
70
71 prg_bank: u8,
72 prg_ram_enabled: bool,
73 chr_banks: [u8; 4], nt_banks: [u8; 2], mirroring: Mirroring,
76 nt_rom_mode: bool, }
78
79impl Sunsoft4 {
80 pub fn new(
91 prg_rom: Box<[u8]>,
92 chr_rom: Box<[u8]>,
93 mirroring: Mirroring,
94 ) -> Result<Self, MapperError> {
95 if prg_rom.is_empty() || !prg_rom.len().is_multiple_of(PRG_BANK_16K) {
96 return Err(MapperError::Invalid(format!(
97 "Sunsoft-4 PRG-ROM size {} is not a non-zero multiple of 16 KiB",
98 prg_rom.len()
99 )));
100 }
101 let chr_is_ram = chr_rom.is_empty();
102 let chr: Box<[u8]> = if chr_is_ram {
103 vec![0u8; 4 * CHR_BANK_2K].into_boxed_slice()
104 } else if chr_rom.len().is_multiple_of(CHR_BANK_1K) {
105 chr_rom
106 } else {
107 return Err(MapperError::Invalid(format!(
108 "Sunsoft-4 CHR-ROM size {} is not a multiple of 1 KiB",
109 chr_rom.len()
110 )));
111 };
112 Ok(Self {
113 prg_rom,
114 chr,
115 prg_ram: vec![0u8; 8 * 1024].into_boxed_slice(),
116 vram: vec![0u8; 2 * NAMETABLE_SIZE].into_boxed_slice(),
117 chr_is_ram,
118 prg_bank: 0,
119 prg_ram_enabled: false,
120 chr_banks: [0; 4],
121 nt_banks: [0; 2],
122 mirroring,
123 nt_rom_mode: false,
124 })
125 }
126
127 fn prg_offset(&self, addr: u16) -> usize {
128 let total = (self.prg_rom.len() / PRG_BANK_16K).max(1);
129 let last = total - 1;
130 let bank = match addr {
131 0x8000..=0xBFFF => (self.prg_bank as usize) % total,
132 _ => last,
133 };
134 bank * PRG_BANK_16K + (addr as usize & 0x3FFF)
135 }
136
137 fn chr_offset(&self, addr: u16) -> usize {
138 let addr = (addr & 0x1FFF) as usize;
139 let total_2k = (self.chr.len() / CHR_BANK_2K).max(1);
140 let slot = addr / CHR_BANK_2K;
141 let bank = (self.chr_banks[slot] as usize) % total_2k;
142 bank * CHR_BANK_2K + (addr & (CHR_BANK_2K - 1))
143 }
144
145 fn nt_physical(&self, table: u8) -> usize {
148 self.mirroring.physical_bank(table)
149 }
150
151 fn nametable_offset(&self, addr: u16) -> usize {
152 let table = (((addr - 0x2000) / NAMETABLE_SIZE_U16) & 0x03) as u8;
153 let local = (addr as usize) & (NAMETABLE_SIZE - 1);
154 self.nt_physical(table) * NAMETABLE_SIZE + local
155 }
156
157 fn chr_nt_byte(&self, addr: u16) -> u8 {
160 let table = (((addr - 0x2000) / NAMETABLE_SIZE_U16) & 0x03) as u8;
161 let local = (addr as usize) & (NAMETABLE_SIZE - 1);
162 let reg = self.nt_banks[self.nt_physical(table)] as usize;
164 let bank = (reg & 0x7F) | 0x80;
166 let total_1k = (self.chr.len() / CHR_BANK_1K).max(1);
167 let off = (bank % total_1k) * CHR_BANK_1K + local;
168 self.chr[off % self.chr.len()]
169 }
170}
171
172impl Mapper for Sunsoft4 {
173 fn sram(&self) -> &[u8] {
174 &self.prg_ram
175 }
176 fn sram_mut(&mut self) -> &mut [u8] {
177 &mut self.prg_ram
178 }
179 fn caps(&self) -> MapperCaps {
182 MapperCaps::NONE
183 }
184
185 fn cpu_read(&mut self, addr: u16) -> u8 {
186 match addr {
187 0x6000..=0x7FFF => {
188 if self.prg_ram_enabled {
189 self.prg_ram[(addr - 0x6000) as usize % self.prg_ram.len()]
190 } else {
191 0
192 }
193 }
194 0x8000..=0xFFFF => {
195 let off = self.prg_offset(addr);
196 self.prg_rom[off % self.prg_rom.len()]
197 }
198 _ => 0,
199 }
200 }
201
202 fn cpu_write(&mut self, addr: u16, value: u8) {
203 match addr {
204 0x6000..=0x7FFF => {
205 if self.prg_ram_enabled {
206 let off = (addr - 0x6000) as usize % self.prg_ram.len();
207 self.prg_ram[off] = value;
208 }
209 }
210 0x8000..=0x8FFF => self.chr_banks[0] = value,
212 0x9000..=0x9FFF => self.chr_banks[1] = value,
213 0xA000..=0xAFFF => self.chr_banks[2] = value,
214 0xB000..=0xBFFF => self.chr_banks[3] = value,
215 0xC000..=0xCFFF => self.nt_banks[0] = value,
216 0xD000..=0xDFFF => self.nt_banks[1] = value,
217 0xE000..=0xEFFF => {
218 self.mirroring = match value & 0x03 {
219 0 => Mirroring::Vertical,
220 1 => Mirroring::Horizontal,
221 2 => Mirroring::SingleScreenA,
222 _ => Mirroring::SingleScreenB,
223 };
224 self.nt_rom_mode = (value & 0x10) != 0;
225 }
226 0xF000..=0xFFFF => {
227 self.prg_bank = value & 0x0F;
228 self.prg_ram_enabled = (value & 0x10) != 0;
229 }
230 _ => {}
231 }
232 }
233
234 fn ppu_read(&mut self, addr: u16) -> u8 {
235 let addr = addr & 0x3FFF;
236 match addr {
237 0x0000..=0x1FFF => {
238 let off = self.chr_offset(addr);
239 self.chr[off % self.chr.len()]
240 }
241 0x2000..=0x3EFF => {
242 if self.nt_rom_mode {
246 self.chr_nt_byte(addr | 0x2000)
247 } else {
248 self.vram[self.nametable_offset(addr) % self.vram.len()]
249 }
250 }
251 _ => 0,
252 }
253 }
254
255 fn ppu_write(&mut self, addr: u16, value: u8) {
256 let addr = addr & 0x3FFF;
257 match addr {
258 0x0000..=0x1FFF => {
259 if self.chr_is_ram {
260 let off = self.chr_offset(addr);
261 let len = self.chr.len();
262 self.chr[off % len] = value;
263 }
264 }
265 0x2000..=0x3EFF if !self.nt_rom_mode => {
267 let off = self.nametable_offset(addr) % self.vram.len();
268 self.vram[off] = value;
269 }
270 _ => {}
271 }
272 }
273
274 fn nametable_fetch(&mut self, addr: u16) -> Option<u8> {
275 if self.nt_rom_mode && (0x2000..=0x2FFF).contains(&(addr & 0x3FFF)) {
276 Some(self.chr_nt_byte(addr & 0x2FFF))
277 } else {
278 None
279 }
280 }
281
282 fn nametable_write(&mut self, _addr: u16, _value: u8) -> bool {
283 self.nt_rom_mode
287 }
288
289 fn nametable_address(&self, addr: u16) -> u16 {
290 let off = self.nametable_offset(addr);
291 u16::try_from(off & 0x07FF).unwrap_or(0)
292 }
293
294 fn current_mirroring(&self) -> Mirroring {
295 self.mirroring
296 }
297
298 fn debug_info(&self) -> crate::mapper::MapperDebugInfo {
299 let mut info = crate::mapper::MapperDebugInfo {
300 mapper_id: 68,
301 name: "Sunsoft-4 (68)".into(),
302 mirroring: crate::mapper::mirroring_name(self.mirroring),
303 ..Default::default()
304 };
305 info.prg_banks
306 .push(("PRG".into(), format!("{:#04x}", self.prg_bank)));
307 info.prg_banks
308 .push(("ram_en".into(), format!("{}", self.prg_ram_enabled)));
309 for (i, b) in self.chr_banks.iter().enumerate() {
310 info.chr_banks.push((format!("C{i}"), format!("{b:#04x}")));
311 }
312 info.extra
313 .push(("ntROM".into(), format!("{}", self.nt_rom_mode)));
314 info.extra
315 .push(("NT0".into(), format!("{:#04x}", self.nt_banks[0])));
316 info.extra
317 .push(("NT1".into(), format!("{:#04x}", self.nt_banks[1])));
318 info
319 }
320
321 fn save_state(&self) -> Vec<u8> {
322 let mut out = Vec::with_capacity(
323 16 + self.prg_ram.len()
324 + self.vram.len()
325 + if self.chr_is_ram { self.chr.len() } else { 0 },
326 );
327 out.push(SAVE_STATE_VERSION);
328 out.push(self.prg_bank);
329 out.push(u8::from(self.prg_ram_enabled));
330 out.extend_from_slice(&self.chr_banks);
331 out.extend_from_slice(&self.nt_banks);
332 out.push(self.mirroring as u8);
333 out.push(u8::from(self.nt_rom_mode));
334 out.extend_from_slice(&self.prg_ram);
335 out.extend_from_slice(&self.vram);
336 if self.chr_is_ram {
337 out.extend_from_slice(&self.chr);
338 }
339 out
340 }
341
342 fn load_state(&mut self, data: &[u8]) -> Result<(), MapperError> {
343 let chr_part = if self.chr_is_ram { self.chr.len() } else { 0 };
344 let scalar_len = 1 + 1 + 1 + 4 + 2 + 1 + 1;
346 let expected = scalar_len + self.prg_ram.len() + self.vram.len() + chr_part;
347 if data.len() != expected {
348 return Err(MapperError::WrongLength {
349 expected,
350 got: data.len(),
351 });
352 }
353 if data[0] != SAVE_STATE_VERSION {
354 return Err(MapperError::UnsupportedVersion(data[0]));
355 }
356 let mut c = 1usize;
357 self.prg_bank = data[c];
358 c += 1;
359 self.prg_ram_enabled = data[c] != 0;
360 c += 1;
361 self.chr_banks.copy_from_slice(&data[c..c + 4]);
362 c += 4;
363 self.nt_banks.copy_from_slice(&data[c..c + 2]);
364 c += 2;
365 self.mirroring = match data[c] {
366 0 => Mirroring::Horizontal,
367 1 => Mirroring::Vertical,
368 2 => Mirroring::SingleScreenA,
369 3 => Mirroring::SingleScreenB,
370 4 => Mirroring::FourScreen,
371 5 => Mirroring::MapperControlled,
372 other => return Err(MapperError::Invalid(format!("mirroring {other}"))),
373 };
374 c += 1;
375 self.nt_rom_mode = data[c] != 0;
376 c += 1;
377 self.prg_ram
378 .copy_from_slice(&data[c..c + self.prg_ram.len()]);
379 c += self.prg_ram.len();
380 self.vram.copy_from_slice(&data[c..c + self.vram.len()]);
381 c += self.vram.len();
382 if self.chr_is_ram {
383 self.chr.copy_from_slice(&data[c..c + self.chr.len()]);
384 }
385 Ok(())
386 }
387}
388
389#[cfg(test)]
390#[allow(clippy::cast_possible_truncation)]
391mod tests {
392 use super::*;
393
394 fn synth_prg(banks_16k: usize) -> Box<[u8]> {
395 let mut v = vec![0u8; banks_16k * PRG_BANK_16K];
396 for b in 0..banks_16k {
397 v[b * PRG_BANK_16K] = b as u8;
398 }
399 v.into_boxed_slice()
400 }
401
402 fn synth_chr(banks_1k: usize) -> Box<[u8]> {
405 let mut v = vec![0u8; banks_1k * CHR_BANK_1K];
406 for b in 0..banks_1k {
407 v[b * CHR_BANK_1K] = b as u8;
408 }
409 v.into_boxed_slice()
410 }
411
412 fn fresh() -> Sunsoft4 {
413 Sunsoft4::new(synth_prg(8), synth_chr(256), Mirroring::Vertical).unwrap()
414 }
415
416 #[test]
417 fn prg_bank_select_and_fixed_last() {
418 let mut m = fresh();
419 assert_eq!(m.cpu_read(0x8000), 0);
420 assert_eq!(m.cpu_read(0xC000), 7);
421 m.cpu_write(0xF000, 5);
422 assert_eq!(m.cpu_read(0x8000), 5);
423 assert_eq!(m.cpu_read(0xC000), 7);
424 }
425
426 #[test]
427 fn chr_four_2k_pattern_banks() {
428 let mut m = fresh();
429 m.cpu_write(0x8000, 3); assert_eq!(m.ppu_read(0x0000), 6);
432 m.cpu_write(0xA000, 9); assert_eq!(m.ppu_read(0x1000), 18);
434 }
435
436 #[test]
437 fn prg_ram_gated_by_f000_bit4() {
438 let mut m = fresh();
439 m.cpu_write(0x6000, 0xAB);
441 assert_eq!(m.cpu_read(0x6000), 0);
442 m.cpu_write(0xF000, 0x10);
444 m.cpu_write(0x6000, 0xCD);
445 assert_eq!(m.cpu_read(0x6000), 0xCD);
446 }
447
448 #[test]
449 fn nametable_chr_rom_mode_serves_chr_bytes() {
450 let mut m = fresh();
451 m.cpu_write(0xC000, 0x05); m.cpu_write(0xD000, 0x06); m.cpu_write(0xE000, 0x10);
456 assert_eq!(m.nametable_fetch(0x2000), Some(133));
458 assert_eq!(m.nametable_fetch(0x2400), Some(134));
460 }
461
462 #[test]
463 fn nametable_ciram_mode_returns_none_from_fetch() {
464 let mut m = fresh();
465 m.cpu_write(0xE000, 0x00);
468 assert_eq!(m.nametable_fetch(0x2000), None);
469 assert!(!m.nametable_write(0x2000, 0));
470 }
471
472 #[test]
473 fn rom_nametable_mode_suppresses_ciram_write() {
474 let mut m = fresh();
475 m.cpu_write(0xE000, 0x10); assert!(m.nametable_write(0x2000, 0x42));
477 }
478
479 #[test]
480 fn save_state_round_trip() {
481 let mut m = fresh();
482 m.cpu_write(0xF000, 0x10 | 3);
483 m.cpu_write(0x9000, 7);
484 m.cpu_write(0xC000, 0x12);
485 m.cpu_write(0xE000, 0x11);
486 m.cpu_write(0x6000, 0x99);
487 let blob = m.save_state();
488 let mut m2 = fresh();
489 m2.load_state(&blob).unwrap();
490 assert_eq!(m.cpu_read(0x8000), m2.cpu_read(0x8000));
491 assert_eq!(m.cpu_read(0x6000), m2.cpu_read(0x6000));
492 assert_eq!(m.nametable_fetch(0x2000), m2.nametable_fetch(0x2000));
493 assert_eq!(m.current_mirroring(), m2.current_mirroring());
494 }
495}