1#![allow(
29 clippy::cast_possible_truncation,
30 clippy::cast_lossless,
31 clippy::missing_const_for_fn,
32 clippy::doc_markdown,
33 clippy::struct_excessive_bools
34)]
35
36use crate::cartridge::Mirroring;
37use crate::mapper::{Mapper, MapperCaps, MapperError};
38use alloc::{boxed::Box, vec::Vec};
39use alloc::{format, vec};
40
41const PRG_BANK_8K: usize = 0x2000;
42const CHR_BANK_1K: usize = 0x0400;
43const NAMETABLE_SIZE: usize = 0x0400;
44const NAMETABLE_SIZE_U16: u16 = 0x0400;
45
46const SAVE_STATE_VERSION: u8 = 1;
47
48pub struct JalecoSs88006 {
50 prg_rom: Box<[u8]>,
51 chr: Box<[u8]>,
52 vram: Box<[u8]>,
53 prg_ram: Box<[u8]>,
54 chr_is_ram: bool,
55
56 prg_banks: [u8; 3],
58 chr_banks: [u8; 8],
60 mirroring: Mirroring,
61
62 prg_ram_enable: bool,
63 prg_ram_write: bool,
64
65 irq_reload: u16,
67 irq_counter: u16,
68 irq_enabled: bool,
69 irq_width: IrqWidth,
71 irq_pending: bool,
72}
73
74#[derive(Debug, Clone, Copy, PartialEq, Eq)]
76enum IrqWidth {
77 Bits16,
78 Bits12,
79 Bits8,
80 Bits4,
81}
82
83impl IrqWidth {
84 const fn from_ctrl(value: u8) -> Self {
85 if value & 0x08 != 0 {
87 Self::Bits4
88 } else if value & 0x04 != 0 {
89 Self::Bits8
90 } else if value & 0x02 != 0 {
91 Self::Bits12
92 } else {
93 Self::Bits16
94 }
95 }
96
97 const fn mask(self) -> u16 {
99 match self {
100 Self::Bits16 => 0xFFFF,
101 Self::Bits12 => 0x0FFF,
102 Self::Bits8 => 0x00FF,
103 Self::Bits4 => 0x000F,
104 }
105 }
106
107 fn to_byte(self) -> u8 {
108 match self {
109 Self::Bits16 => 0,
110 Self::Bits12 => 1,
111 Self::Bits8 => 2,
112 Self::Bits4 => 3,
113 }
114 }
115
116 const fn from_byte(b: u8) -> Self {
117 match b {
118 1 => Self::Bits12,
119 2 => Self::Bits8,
120 3 => Self::Bits4,
121 _ => Self::Bits16,
122 }
123 }
124}
125
126impl JalecoSs88006 {
127 pub fn new(
136 prg_rom: Box<[u8]>,
137 chr_rom: Box<[u8]>,
138 mirroring: Mirroring,
139 ) -> Result<Self, MapperError> {
140 if prg_rom.is_empty() || !prg_rom.len().is_multiple_of(PRG_BANK_8K) {
141 return Err(MapperError::Invalid(format!(
142 "SS88006 PRG-ROM size {} is not a non-zero multiple of 8 KiB",
143 prg_rom.len()
144 )));
145 }
146 let chr_is_ram = chr_rom.is_empty();
147 let chr: Box<[u8]> = if chr_is_ram {
148 vec![0u8; 8 * CHR_BANK_1K].into_boxed_slice()
149 } else if chr_rom.len().is_multiple_of(CHR_BANK_1K) {
150 chr_rom
151 } else {
152 return Err(MapperError::Invalid(format!(
153 "SS88006 CHR-ROM size {} is not a multiple of 1 KiB",
154 chr_rom.len()
155 )));
156 };
157 Ok(Self {
158 prg_rom,
159 chr,
160 vram: vec![0u8; 2 * NAMETABLE_SIZE].into_boxed_slice(),
161 prg_ram: vec![0u8; 8 * 1024].into_boxed_slice(),
162 chr_is_ram,
163 prg_banks: [0, 1, 2],
164 chr_banks: [0; 8],
165 mirroring,
166 prg_ram_enable: true,
167 prg_ram_write: true,
168 irq_reload: 0,
169 irq_counter: 0,
170 irq_enabled: false,
171 irq_width: IrqWidth::Bits16,
172 irq_pending: false,
173 })
174 }
175
176 const fn nametable_offset(&self, addr: u16) -> usize {
177 let table = (((addr - 0x2000) / NAMETABLE_SIZE_U16) & 0x03) as u8;
178 let local = (addr as usize) & (NAMETABLE_SIZE - 1);
179 let physical = self.mirroring.physical_bank(table);
180 physical * NAMETABLE_SIZE + local
181 }
182
183 fn prg_offset(&self, addr: u16) -> usize {
184 let total = (self.prg_rom.len() / PRG_BANK_8K).max(1);
185 let last = total - 1;
186 let bank = match addr & 0xE000 {
187 0x8000 => (self.prg_banks[0] as usize) % total,
188 0xA000 => (self.prg_banks[1] as usize) % total,
189 0xC000 => (self.prg_banks[2] as usize) % total,
190 0xE000 => last,
191 _ => 0,
192 };
193 bank * PRG_BANK_8K + (addr as usize & 0x1FFF)
194 }
195
196 fn chr_offset(&self, addr: u16) -> usize {
197 let slot = (addr as usize / CHR_BANK_1K) & 0x07;
198 let total = (self.chr.len() / CHR_BANK_1K).max(1);
199 let bank = (self.chr_banks[slot] as usize) % total;
200 bank * CHR_BANK_1K + (addr as usize & (CHR_BANK_1K - 1))
201 }
202
203 fn set_nibble(reg: &mut u8, high: bool, value: u8) {
204 let v = value & 0x0F;
205 if high {
206 *reg = (*reg & 0x0F) | (v << 4);
207 } else {
208 *reg = (*reg & 0xF0) | v;
209 }
210 }
211
212 fn set_reload_nibble(&mut self, nibble: u8, value: u8) {
215 let shift = (nibble & 0x03) * 4;
216 let v = (value as u16) & 0x0F;
217 self.irq_reload = (self.irq_reload & !(0x000F << shift)) | (v << shift);
218 }
219}
220
221impl Mapper for JalecoSs88006 {
222 fn sram(&self) -> &[u8] {
223 &self.prg_ram
224 }
225 fn sram_mut(&mut self) -> &mut [u8] {
226 &mut self.prg_ram
227 }
228 fn caps(&self) -> MapperCaps {
230 MapperCaps::CYCLE_IRQ
231 }
232
233 fn cpu_read(&mut self, addr: u16) -> u8 {
234 match addr {
235 0x6000..=0x7FFF => {
236 if self.prg_ram_enable {
237 self.prg_ram[(addr - 0x6000) as usize % self.prg_ram.len()]
238 } else {
239 0
240 }
241 }
242 0x8000..=0xFFFF => {
243 let off = self.prg_offset(addr);
244 self.prg_rom[off % self.prg_rom.len()]
245 }
246 _ => 0,
247 }
248 }
249
250 fn cpu_write(&mut self, addr: u16, value: u8) {
251 if (0x6000..=0x7FFF).contains(&addr) {
252 if self.prg_ram_enable && self.prg_ram_write {
253 let len = self.prg_ram.len();
254 self.prg_ram[(addr - 0x6000) as usize % len] = value;
255 }
256 return;
257 }
258 let reg = addr & 0xF003;
260 match reg {
261 0x8000 => Self::set_nibble(&mut self.prg_banks[0], false, value),
263 0x8001 => {
264 let cur = self.prg_banks[0];
265 self.prg_banks[0] = (cur & 0x0F) | ((value & 0x03) << 4);
266 }
267 0x8002 => Self::set_nibble(&mut self.prg_banks[1], false, value),
269 0x8003 => {
270 let cur = self.prg_banks[1];
271 self.prg_banks[1] = (cur & 0x0F) | ((value & 0x03) << 4);
272 }
273 0x9000 => Self::set_nibble(&mut self.prg_banks[2], false, value),
275 0x9001 => {
276 let cur = self.prg_banks[2];
277 self.prg_banks[2] = (cur & 0x0F) | ((value & 0x03) << 4);
278 }
279 0x9002 => {
281 self.prg_ram_enable = (value & 0x01) != 0;
282 self.prg_ram_write = (value & 0x02) != 0;
283 }
284 0xA000 => Self::set_nibble(&mut self.chr_banks[0], false, value),
286 0xA001 => Self::set_nibble(&mut self.chr_banks[0], true, value),
287 0xA002 => Self::set_nibble(&mut self.chr_banks[1], false, value),
288 0xA003 => Self::set_nibble(&mut self.chr_banks[1], true, value),
289 0xB000 => Self::set_nibble(&mut self.chr_banks[2], false, value),
290 0xB001 => Self::set_nibble(&mut self.chr_banks[2], true, value),
291 0xB002 => Self::set_nibble(&mut self.chr_banks[3], false, value),
292 0xB003 => Self::set_nibble(&mut self.chr_banks[3], true, value),
293 0xC000 => Self::set_nibble(&mut self.chr_banks[4], false, value),
294 0xC001 => Self::set_nibble(&mut self.chr_banks[4], true, value),
295 0xC002 => Self::set_nibble(&mut self.chr_banks[5], false, value),
296 0xC003 => Self::set_nibble(&mut self.chr_banks[5], true, value),
297 0xD000 => Self::set_nibble(&mut self.chr_banks[6], false, value),
298 0xD001 => Self::set_nibble(&mut self.chr_banks[6], true, value),
299 0xD002 => Self::set_nibble(&mut self.chr_banks[7], false, value),
300 0xD003 => Self::set_nibble(&mut self.chr_banks[7], true, value),
301 0xE000 => self.set_reload_nibble(0, value),
303 0xE001 => self.set_reload_nibble(1, value),
304 0xE002 => self.set_reload_nibble(2, value),
305 0xE003 => self.set_reload_nibble(3, value),
306 0xF000 => {
308 self.irq_counter = self.irq_reload;
309 self.irq_pending = false;
310 }
311 0xF001 => {
313 self.irq_enabled = (value & 0x01) != 0;
314 self.irq_width = IrqWidth::from_ctrl(value);
315 self.irq_pending = false;
316 }
317 0xF002 => {
319 self.mirroring = match value & 0x03 {
320 0 => Mirroring::Horizontal,
321 1 => Mirroring::Vertical,
322 2 => Mirroring::SingleScreenA,
323 _ => Mirroring::SingleScreenB,
324 };
325 }
326 _ => {}
328 }
329 }
330
331 fn ppu_read(&mut self, addr: u16) -> u8 {
332 let addr = addr & 0x3FFF;
333 match addr {
334 0x0000..=0x1FFF => {
335 let off = self.chr_offset(addr);
336 self.chr[off % self.chr.len()]
337 }
338 0x2000..=0x3EFF => self.vram[self.nametable_offset(addr)],
339 _ => 0,
340 }
341 }
342
343 fn ppu_write(&mut self, addr: u16, value: u8) {
344 let addr = addr & 0x3FFF;
345 match addr {
346 0x0000..=0x1FFF => {
347 if self.chr_is_ram {
348 let off = self.chr_offset(addr);
349 let len = self.chr.len();
350 self.chr[off % len] = value;
351 }
352 }
353 0x2000..=0x3EFF => {
354 let off = self.nametable_offset(addr);
355 self.vram[off] = value;
356 }
357 _ => {}
358 }
359 }
360
361 fn notify_cpu_cycle(&mut self) {
362 if !self.irq_enabled {
363 return;
364 }
365 let mask = self.irq_width.mask();
369 let active = self.irq_counter & mask;
370 if active == 0 {
371 self.irq_pending = true;
372 }
373 let new_active = active.wrapping_sub(1) & mask;
374 self.irq_counter = (self.irq_counter & !mask) | new_active;
375 }
376
377 fn irq_pending(&self) -> bool {
378 self.irq_pending
379 }
380
381 fn current_mirroring(&self) -> Mirroring {
382 self.mirroring
383 }
384
385 fn debug_info(&self) -> crate::mapper::MapperDebugInfo {
386 let mut info = crate::mapper::MapperDebugInfo {
387 mapper_id: 18,
388 name: "Jaleco SS88006 (18)".into(),
389 mirroring: crate::mapper::mirroring_name(self.mirroring),
390 ..Default::default()
391 };
392 for (i, b) in self.prg_banks.iter().enumerate() {
393 info.prg_banks
394 .push((format!("PRG{i}"), format!("{b:#04x}")));
395 }
396 for (i, b) in self.chr_banks.iter().enumerate() {
397 info.chr_banks
398 .push((format!("CHR{i}"), format!("{b:#04x}")));
399 }
400 info.irq_state
401 .push(("reload".into(), format!("{:#06x}", self.irq_reload)));
402 info.irq_state
403 .push(("counter".into(), format!("{:#06x}", self.irq_counter)));
404 info.irq_state
405 .push(("enabled".into(), format!("{}", self.irq_enabled)));
406 info.irq_state
407 .push(("pending".into(), format!("{}", self.irq_pending)));
408 info
409 }
410
411 fn save_state(&self) -> Vec<u8> {
412 let mut out = Vec::with_capacity(22 + self.vram.len() + self.prg_ram.len());
413 out.push(SAVE_STATE_VERSION);
414 out.extend_from_slice(&self.prg_banks);
415 out.extend_from_slice(&self.chr_banks);
416 out.push(self.mirroring as u8);
417 out.push(u8::from(self.prg_ram_enable));
418 out.push(u8::from(self.prg_ram_write));
419 out.extend_from_slice(&self.irq_reload.to_le_bytes());
420 out.extend_from_slice(&self.irq_counter.to_le_bytes());
421 out.push(u8::from(self.irq_enabled));
422 out.push(self.irq_width.to_byte());
423 out.push(u8::from(self.irq_pending));
424 out.extend_from_slice(&self.vram);
425 out.extend_from_slice(&self.prg_ram);
426 if self.chr_is_ram {
427 out.extend_from_slice(&self.chr);
428 }
429 out
430 }
431
432 fn load_state(&mut self, data: &[u8]) -> Result<(), MapperError> {
433 let need_chr = if self.chr_is_ram { self.chr.len() } else { 0 };
434 let expected = 22 + self.vram.len() + self.prg_ram.len() + need_chr;
435 if data.len() != expected {
436 return Err(MapperError::WrongLength {
437 expected,
438 got: data.len(),
439 });
440 }
441 if data[0] != SAVE_STATE_VERSION {
442 return Err(MapperError::UnsupportedVersion(data[0]));
443 }
444 self.prg_banks.copy_from_slice(&data[1..4]);
445 self.chr_banks.copy_from_slice(&data[4..12]);
446 self.mirroring = match data[12] {
447 0 => Mirroring::Horizontal,
448 1 => Mirroring::Vertical,
449 2 => Mirroring::SingleScreenA,
450 3 => Mirroring::SingleScreenB,
451 4 => Mirroring::FourScreen,
452 other => return Err(MapperError::Invalid(format!("mirroring {other}"))),
453 };
454 self.prg_ram_enable = data[13] != 0;
455 self.prg_ram_write = data[14] != 0;
456 self.irq_reload = u16::from_le_bytes([data[15], data[16]]);
457 self.irq_counter = u16::from_le_bytes([data[17], data[18]]);
458 let mut cursor = 19;
460 self.irq_enabled = data[cursor] != 0;
461 cursor += 1;
462 self.irq_width = IrqWidth::from_byte(data[cursor]);
463 cursor += 1;
464 self.irq_pending = data[cursor] != 0;
465 cursor += 1;
466 self.vram
467 .copy_from_slice(&data[cursor..cursor + self.vram.len()]);
468 cursor += self.vram.len();
469 self.prg_ram
470 .copy_from_slice(&data[cursor..cursor + self.prg_ram.len()]);
471 cursor += self.prg_ram.len();
472 if self.chr_is_ram {
473 self.chr
474 .copy_from_slice(&data[cursor..cursor + self.chr.len()]);
475 }
476 Ok(())
477 }
478}
479
480#[cfg(test)]
481#[allow(clippy::cast_possible_truncation)]
482mod tests {
483 use super::*;
484
485 fn synth_prg(banks_8k: usize) -> Box<[u8]> {
486 let mut v = vec![0u8; banks_8k * PRG_BANK_8K];
487 for b in 0..banks_8k {
488 v[b * PRG_BANK_8K] = b as u8;
489 }
490 v.into_boxed_slice()
491 }
492
493 fn synth_chr(banks_1k: usize) -> Box<[u8]> {
494 let mut v = vec![0u8; banks_1k * CHR_BANK_1K];
495 for b in 0..banks_1k {
496 v[b * CHR_BANK_1K] = b as u8;
497 }
498 v.into_boxed_slice()
499 }
500
501 #[test]
502 fn prg_defaults_and_fixed_last() {
503 let mut m = JalecoSs88006::new(synth_prg(16), synth_chr(32), Mirroring::Vertical).unwrap();
504 assert_eq!(m.cpu_read(0x8000), 0);
505 assert_eq!(m.cpu_read(0xA000), 1);
506 assert_eq!(m.cpu_read(0xC000), 2);
507 assert_eq!(m.cpu_read(0xE000), 15); }
509
510 #[test]
511 fn prg_bank_nibble_pair() {
512 let mut m = JalecoSs88006::new(synth_prg(16), synth_chr(32), Mirroring::Vertical).unwrap();
513 m.cpu_write(0x8000, 0x0A);
515 assert_eq!(m.cpu_read(0x8000), 0x0A);
516 m.cpu_write(0x8001, 0x01); assert_eq!(m.cpu_read(0x8000), 26 % 16);
519 }
520
521 #[test]
522 fn chr_bank_nibble_pair() {
523 let mut m = JalecoSs88006::new(synth_prg(16), synth_chr(32), Mirroring::Vertical).unwrap();
524 m.cpu_write(0xA000, 0x0B);
526 m.cpu_write(0xA001, 0x01);
527 assert_eq!(m.ppu_read(0x0000), 0x1B);
528 m.cpu_write(0xC000, 0x05);
530 assert_eq!(m.ppu_read(0x1000), 5);
531 }
532
533 #[test]
534 fn mirroring_control() {
535 let mut m = JalecoSs88006::new(synth_prg(16), synth_chr(32), Mirroring::Vertical).unwrap();
536 m.cpu_write(0xF002, 0); assert_eq!(m.current_mirroring(), Mirroring::Horizontal);
538 m.cpu_write(0xF002, 1); assert_eq!(m.current_mirroring(), Mirroring::Vertical);
540 m.cpu_write(0xF002, 3); assert_eq!(m.current_mirroring(), Mirroring::SingleScreenB);
542 }
543
544 #[test]
545 fn irq_reload_value_nibbles() {
546 let mut m = JalecoSs88006::new(synth_prg(16), synth_chr(32), Mirroring::Vertical).unwrap();
547 m.cpu_write(0xE000, 0x0A);
548 m.cpu_write(0xE001, 0x0B);
549 m.cpu_write(0xE002, 0x0C);
550 m.cpu_write(0xE003, 0x0D);
551 assert_eq!(m.irq_reload, 0xDCBA);
552 }
553
554 #[test]
555 fn irq_16bit_counts_down_to_borrow() {
556 let mut m = JalecoSs88006::new(synth_prg(16), synth_chr(32), Mirroring::Vertical).unwrap();
557 m.cpu_write(0xE000, 0x02);
559 m.cpu_write(0xF000, 0x00); assert_eq!(m.irq_counter, 2);
561 m.cpu_write(0xF001, 0x01); m.notify_cpu_cycle(); m.notify_cpu_cycle(); assert!(!m.irq_pending());
565 m.notify_cpu_cycle(); assert!(m.irq_pending());
567 }
568
569 #[test]
570 fn irq_width_8bit_ignores_high_bits() {
571 let mut m = JalecoSs88006::new(synth_prg(16), synth_chr(32), Mirroring::Vertical).unwrap();
572 m.cpu_write(0xE000, 0x02);
574 m.cpu_write(0xE001, 0x00);
575 m.cpu_write(0xE002, 0x01);
576 m.cpu_write(0xF000, 0x00);
577 m.cpu_write(0xF001, 0x01 | 0x04);
579 m.notify_cpu_cycle(); m.notify_cpu_cycle(); assert!(!m.irq_pending());
582 m.notify_cpu_cycle(); assert!(m.irq_pending());
584 assert_eq!(m.irq_counter & 0xFF00, 0x0100);
586 }
587
588 #[test]
589 fn irq_reload_acknowledges() {
590 let mut m = JalecoSs88006::new(synth_prg(16), synth_chr(32), Mirroring::Vertical).unwrap();
591 m.irq_pending = true;
592 m.cpu_write(0xF000, 0x00);
593 assert!(!m.irq_pending());
594 }
595
596 #[test]
597 fn save_state_round_trip() {
598 let mut m =
599 JalecoSs88006::new(synth_prg(16), synth_chr(32), Mirroring::Horizontal).unwrap();
600 m.cpu_write(0x8000, 0x05);
601 m.cpu_write(0xA000, 0x03);
602 m.cpu_write(0xE000, 0x07);
603 m.cpu_write(0xF000, 0x00);
604 m.cpu_write(0xF001, 0x05);
605 let blob = m.save_state();
606 let mut m2 =
607 JalecoSs88006::new(synth_prg(16), synth_chr(32), Mirroring::Horizontal).unwrap();
608 m2.load_state(&blob).unwrap();
609 assert_eq!(m.cpu_read(0x8000), m2.cpu_read(0x8000));
610 assert_eq!(m.ppu_read(0x0000), m2.ppu_read(0x0000));
611 assert_eq!(m.irq_counter, m2.irq_counter);
612 assert_eq!(m.irq_width, m2.irq_width);
613 }
614}