1#![allow(
37 clippy::cast_possible_truncation,
38 clippy::cast_lossless,
39 clippy::missing_const_for_fn,
40 clippy::struct_excessive_bools,
41 clippy::doc_markdown
42)]
43
44use crate::cartridge::Mirroring;
45use crate::mapper::{Mapper, MapperCaps, MapperError};
46use alloc::{boxed::Box, vec::Vec};
47use alloc::{format, vec};
48
49const PRG_BANK_16K: usize = 0x4000;
50const CHR_BANK_2K: usize = 0x0800;
51const NAMETABLE_SIZE: usize = 0x0400;
52const NAMETABLE_SIZE_U16: u16 = 0x0400;
53
54const SAVE_STATE_VERSION: u8 = 1;
55
56pub struct Sunsoft3 {
58 prg_rom: Box<[u8]>,
59 chr: Box<[u8]>,
60 vram: Box<[u8]>,
61 chr_is_ram: bool,
62
63 prg_bank: u8,
64 chr_banks: [u8; 4],
65 mirroring: Mirroring,
66
67 irq_counter: u16,
68 irq_enabled: bool,
69 irq_pending: bool,
70 irq_write_low_next: bool,
72}
73
74impl Sunsoft3 {
75 pub fn new(
85 prg_rom: Box<[u8]>,
86 chr_rom: Box<[u8]>,
87 mirroring: Mirroring,
88 ) -> Result<Self, MapperError> {
89 if prg_rom.is_empty() || !prg_rom.len().is_multiple_of(PRG_BANK_16K) {
90 return Err(MapperError::Invalid(format!(
91 "Sunsoft-3 PRG-ROM size {} is not a non-zero multiple of 16 KiB",
92 prg_rom.len()
93 )));
94 }
95 let chr_is_ram = chr_rom.is_empty();
96 let chr: Box<[u8]> = if chr_is_ram {
97 vec![0u8; 4 * CHR_BANK_2K].into_boxed_slice()
98 } else if chr_rom.len().is_multiple_of(CHR_BANK_2K) {
99 chr_rom
100 } else {
101 return Err(MapperError::Invalid(format!(
102 "Sunsoft-3 CHR-ROM size {} is not a multiple of 2 KiB",
103 chr_rom.len()
104 )));
105 };
106 Ok(Self {
107 prg_rom,
108 chr,
109 vram: vec![0u8; 2 * NAMETABLE_SIZE].into_boxed_slice(),
110 chr_is_ram,
111 prg_bank: 0,
112 chr_banks: [0; 4],
113 mirroring,
114 irq_counter: 0,
115 irq_enabled: false,
116 irq_pending: false,
117 irq_write_low_next: false,
118 })
119 }
120
121 fn prg_offset(&self, addr: u16) -> usize {
122 let total = (self.prg_rom.len() / PRG_BANK_16K).max(1);
123 let last = total - 1;
124 let bank = match addr {
125 0x8000..=0xBFFF => (self.prg_bank as usize) % total,
126 _ => last, };
128 bank * PRG_BANK_16K + (addr as usize & 0x3FFF)
129 }
130
131 fn chr_offset(&self, addr: u16) -> usize {
132 let addr = (addr & 0x1FFF) as usize;
133 let total_2k = (self.chr.len() / CHR_BANK_2K).max(1);
134 let slot = addr / CHR_BANK_2K;
135 let bank = (self.chr_banks[slot] as usize) % total_2k;
136 bank * CHR_BANK_2K + (addr & (CHR_BANK_2K - 1))
137 }
138
139 fn nametable_offset(&self, addr: u16) -> usize {
140 let table = (((addr - 0x2000) / NAMETABLE_SIZE_U16) & 0x03) as u8;
141 let local = (addr as usize) & (NAMETABLE_SIZE - 1);
142 let physical = self.mirroring.physical_bank(table);
143 physical * NAMETABLE_SIZE + local
144 }
145}
146
147impl Mapper for Sunsoft3 {
148 fn caps(&self) -> MapperCaps {
150 MapperCaps::CYCLE_IRQ
151 }
152
153 fn cpu_read(&mut self, addr: u16) -> u8 {
154 match addr {
155 0x8000..=0xFFFF => {
156 let off = self.prg_offset(addr);
157 self.prg_rom[off % self.prg_rom.len()]
158 }
159 _ => 0,
160 }
161 }
162
163 fn cpu_write(&mut self, addr: u16, value: u8) {
164 match addr & 0xF800 {
167 0x8000 => {
168 self.irq_pending = false;
170 }
171 0x8800 => self.chr_banks[0] = value,
172 0x9800 => self.chr_banks[1] = value,
173 0xA800 => self.chr_banks[2] = value,
174 0xB800 => self.chr_banks[3] = value,
175 0xC800 => {
176 if self.irq_write_low_next {
179 self.irq_counter = (self.irq_counter & 0xFF00) | (value as u16);
180 } else {
181 self.irq_counter = (self.irq_counter & 0x00FF) | ((value as u16) << 8);
182 }
183 self.irq_write_low_next = !self.irq_write_low_next;
184 }
185 0xD800 => {
186 self.irq_enabled = (value & 0x10) != 0;
187 self.irq_write_low_next = false;
189 }
190 0xE800 => {
191 self.mirroring = match value & 0x03 {
192 0 => Mirroring::Vertical,
193 1 => Mirroring::Horizontal,
194 2 => Mirroring::SingleScreenA,
195 _ => Mirroring::SingleScreenB,
196 };
197 }
198 0xF800 => self.prg_bank = value & 0x0F,
199 _ => {}
200 }
201 }
202
203 fn ppu_read(&mut self, addr: u16) -> u8 {
204 let addr = addr & 0x3FFF;
205 match addr {
206 0x0000..=0x1FFF => {
207 let off = self.chr_offset(addr);
208 self.chr[off % self.chr.len()]
209 }
210 0x2000..=0x3EFF => self.vram[self.nametable_offset(addr) % self.vram.len()],
211 _ => 0,
212 }
213 }
214
215 fn ppu_write(&mut self, addr: u16, value: u8) {
216 let addr = addr & 0x3FFF;
217 match addr {
218 0x0000..=0x1FFF => {
219 if self.chr_is_ram {
220 let off = self.chr_offset(addr);
221 let len = self.chr.len();
222 self.chr[off % len] = value;
223 }
224 }
225 0x2000..=0x3EFF => {
226 let off = self.nametable_offset(addr) % self.vram.len();
227 self.vram[off] = value;
228 }
229 _ => {}
230 }
231 }
232
233 fn notify_cpu_cycle(&mut self) {
234 if !self.irq_enabled {
235 return;
236 }
237 if self.irq_counter == 0 {
238 self.irq_counter = 0xFFFF;
240 self.irq_pending = true;
241 self.irq_enabled = false;
242 } else {
243 self.irq_counter -= 1;
244 }
245 }
246
247 fn irq_pending(&self) -> bool {
248 self.irq_pending
249 }
250
251 fn current_mirroring(&self) -> Mirroring {
252 self.mirroring
253 }
254
255 fn debug_info(&self) -> crate::mapper::MapperDebugInfo {
256 let mut info = crate::mapper::MapperDebugInfo {
257 mapper_id: 67,
258 name: "Sunsoft-3 (67)".into(),
259 mirroring: crate::mapper::mirroring_name(self.mirroring),
260 ..Default::default()
261 };
262 info.prg_banks
263 .push(("PRG".into(), format!("{:#04x}", self.prg_bank)));
264 for (i, b) in self.chr_banks.iter().enumerate() {
265 info.chr_banks.push((format!("C{i}"), format!("{b:#04x}")));
266 }
267 info.irq_state
268 .push(("counter".into(), format!("{:#06x}", self.irq_counter)));
269 info.irq_state
270 .push(("enabled".into(), format!("{}", self.irq_enabled)));
271 info.irq_state
272 .push(("pending".into(), format!("{}", self.irq_pending)));
273 info
274 }
275
276 fn save_state(&self) -> Vec<u8> {
277 let mut out = Vec::with_capacity(
278 16 + self.vram.len() + if self.chr_is_ram { self.chr.len() } else { 0 },
279 );
280 out.push(SAVE_STATE_VERSION);
281 out.push(self.prg_bank);
282 out.extend_from_slice(&self.chr_banks);
283 out.push(self.mirroring as u8);
284 out.extend_from_slice(&self.irq_counter.to_le_bytes());
285 out.push(u8::from(self.irq_enabled));
286 out.push(u8::from(self.irq_pending));
287 out.push(u8::from(self.irq_write_low_next));
288 out.extend_from_slice(&self.vram);
289 if self.chr_is_ram {
290 out.extend_from_slice(&self.chr);
291 }
292 out
293 }
294
295 fn load_state(&mut self, data: &[u8]) -> Result<(), MapperError> {
296 let chr_part = if self.chr_is_ram { self.chr.len() } else { 0 };
297 let scalar_len = 1 + 1 + 4 + 1 + 2 + 1 + 1 + 1;
299 let expected = scalar_len + self.vram.len() + chr_part;
300 if data.len() != expected {
301 return Err(MapperError::WrongLength {
302 expected,
303 got: data.len(),
304 });
305 }
306 if data[0] != SAVE_STATE_VERSION {
307 return Err(MapperError::UnsupportedVersion(data[0]));
308 }
309 let mut c = 1usize;
310 self.prg_bank = data[c];
311 c += 1;
312 self.chr_banks.copy_from_slice(&data[c..c + 4]);
313 c += 4;
314 self.mirroring = match data[c] {
315 0 => Mirroring::Horizontal,
316 1 => Mirroring::Vertical,
317 2 => Mirroring::SingleScreenA,
318 3 => Mirroring::SingleScreenB,
319 4 => Mirroring::FourScreen,
320 5 => Mirroring::MapperControlled,
321 other => return Err(MapperError::Invalid(format!("mirroring {other}"))),
322 };
323 c += 1;
324 self.irq_counter = u16::from_le_bytes([data[c], data[c + 1]]);
325 c += 2;
326 self.irq_enabled = data[c] != 0;
327 c += 1;
328 self.irq_pending = data[c] != 0;
329 c += 1;
330 self.irq_write_low_next = data[c] != 0;
331 c += 1;
332 self.vram.copy_from_slice(&data[c..c + self.vram.len()]);
333 c += self.vram.len();
334 if self.chr_is_ram {
335 self.chr.copy_from_slice(&data[c..c + self.chr.len()]);
336 }
337 Ok(())
338 }
339}
340
341#[cfg(test)]
342#[allow(clippy::cast_possible_truncation)]
343mod tests {
344 use super::*;
345
346 fn synth_prg(banks_16k: usize) -> Box<[u8]> {
347 let mut v = vec![0u8; banks_16k * PRG_BANK_16K];
348 for b in 0..banks_16k {
349 v[b * PRG_BANK_16K] = b as u8;
350 }
351 v.into_boxed_slice()
352 }
353
354 fn synth_chr(banks_2k: usize) -> Box<[u8]> {
355 let mut v = vec![0u8; banks_2k * CHR_BANK_2K];
356 for b in 0..banks_2k {
357 v[b * CHR_BANK_2K] = b as u8;
358 }
359 v.into_boxed_slice()
360 }
361
362 fn fresh() -> Sunsoft3 {
363 Sunsoft3::new(synth_prg(8), synth_chr(16), Mirroring::Vertical).unwrap()
364 }
365
366 #[test]
367 fn prg_bank_select_and_fixed_last() {
368 let mut m = fresh();
369 assert_eq!(m.cpu_read(0x8000), 0);
370 assert_eq!(m.cpu_read(0xC000), 7); m.cpu_write(0xF800, 5);
372 assert_eq!(m.cpu_read(0x8000), 5);
373 assert_eq!(m.cpu_read(0xC000), 7);
374 }
375
376 #[test]
377 fn chr_four_2k_banks() {
378 let mut m = fresh();
379 m.cpu_write(0x8800, 3); m.cpu_write(0xA800, 9); assert_eq!(m.ppu_read(0x0000), 3);
382 assert_eq!(m.ppu_read(0x1000), 9);
383 }
384
385 #[test]
386 fn mirroring_select() {
387 let mut m = fresh();
388 m.cpu_write(0xE800, 0);
389 assert_eq!(m.current_mirroring(), Mirroring::Vertical);
390 m.cpu_write(0xE800, 1);
391 assert_eq!(m.current_mirroring(), Mirroring::Horizontal);
392 m.cpu_write(0xE800, 2);
393 assert_eq!(m.current_mirroring(), Mirroring::SingleScreenA);
394 m.cpu_write(0xE800, 3);
395 assert_eq!(m.current_mirroring(), Mirroring::SingleScreenB);
396 }
397
398 #[test]
399 fn irq_write_twice_loads_counter_high_then_low() {
400 let mut m = fresh();
401 m.cpu_write(0xD800, 0x00); m.cpu_write(0xC800, 0x12); m.cpu_write(0xC800, 0x34); assert_eq!(m.irq_counter, 0x1234);
405 }
406
407 #[test]
408 fn irq_counts_down_wraps_and_pauses() {
409 let mut m = fresh();
410 m.cpu_write(0xD800, 0x00); m.cpu_write(0xC800, 0x00); m.cpu_write(0xC800, 0x02); m.cpu_write(0xD800, 0x10); m.notify_cpu_cycle(); m.notify_cpu_cycle(); assert!(!m.irq_pending());
417 m.notify_cpu_cycle(); assert!(m.irq_pending());
419 assert!(!m.irq_enabled, "wrap pauses the counter");
420 assert_eq!(m.irq_counter, 0xFFFF);
421 }
422
423 #[test]
424 fn d800_does_not_ack_8000_does() {
425 let mut m = fresh();
426 m.irq_pending = true;
427 m.cpu_write(0xD800, 0x00); assert!(m.irq_pending());
429 m.cpu_write(0x8000, 0x00); assert!(!m.irq_pending());
431 }
432
433 #[test]
434 fn save_state_round_trip() {
435 let mut m = fresh();
436 m.cpu_write(0xF800, 3);
437 m.cpu_write(0x9800, 7);
438 m.cpu_write(0xD800, 0x00);
439 m.cpu_write(0xC800, 0xAB);
440 m.cpu_write(0xC800, 0xCD);
441 m.cpu_write(0xD800, 0x10);
442 m.ppu_write(0x2000, 0x44);
443 let blob = m.save_state();
444 let mut m2 = fresh();
445 m2.load_state(&blob).unwrap();
446 assert_eq!(m.cpu_read(0x8000), m2.cpu_read(0x8000));
447 assert_eq!(m.ppu_read(0x0800), m2.ppu_read(0x0800));
448 assert_eq!(m.irq_counter, m2.irq_counter);
449 assert_eq!(m.ppu_read(0x2000), m2.ppu_read(0x2000));
450 }
451}