1#![allow(
26 clippy::cast_possible_truncation,
27 clippy::cast_lossless,
28 clippy::match_same_arms,
29 clippy::doc_markdown,
30 clippy::similar_names,
31 clippy::too_many_lines,
32 clippy::missing_const_for_fn,
33 clippy::struct_excessive_bools,
34 clippy::bool_to_int_with_if,
35 clippy::unreadable_literal
36)]
37
38use crate::a12_filter::A12RiseFilter;
39use crate::cartridge::Mirroring;
40use crate::mapper::{Mapper, MapperCaps, MapperError};
41use alloc::{boxed::Box, format, vec, vec::Vec};
42
43const PRG_BANK_8K: usize = 0x2000;
44const CHR_BANK_1K: usize = 0x0400;
45const NAMETABLE_SIZE: usize = 0x0400;
46const NAMETABLE_SIZE_U16: u16 = 0x0400;
47
48const SAVE_STATE_VERSION: u8 = 3;
52
53const fn nametable_offset(addr: u16, mirroring: Mirroring) -> usize {
58 let table = (((addr - 0x2000) / NAMETABLE_SIZE_U16) & 0x03) as u8;
59 let local = (addr as usize) & (NAMETABLE_SIZE - 1);
60 let physical = mirroring.physical_bank(table);
61 physical * NAMETABLE_SIZE + local
62}
63
64const fn mirroring_to_byte(m: Mirroring) -> u8 {
65 match m {
66 Mirroring::Horizontal => 0,
67 Mirroring::Vertical => 1,
68 Mirroring::SingleScreenA => 2,
69 Mirroring::SingleScreenB => 3,
70 Mirroring::FourScreen => 4,
71 Mirroring::MapperControlled => 5,
72 }
73}
74
75const fn byte_to_mirroring(b: u8, fallback: Mirroring) -> Mirroring {
76 match b {
77 0 => Mirroring::Horizontal,
78 1 => Mirroring::Vertical,
79 2 => Mirroring::SingleScreenA,
80 3 => Mirroring::SingleScreenB,
81 4 => Mirroring::FourScreen,
82 5 => Mirroring::MapperControlled,
83 _ => fallback,
84 }
85}
86
87fn check_prg(prg: &[u8], id: u16) -> Result<(), MapperError> {
89 if prg.is_empty() || !prg.len().is_multiple_of(PRG_BANK_8K) {
90 return Err(MapperError::Invalid(format!(
91 "mapper {id} PRG-ROM size {} is not a non-zero multiple of 8 KiB",
92 prg.len()
93 )));
94 }
95 Ok(())
96}
97
98pub struct Fk23c {
113 prg_rom: Box<[u8]>,
114 chr: Box<[u8]>,
115 chr_is_ram: bool,
116 vram: Box<[u8]>,
117 wram: Box<[u8]>,
118 mirroring: Mirroring,
119 prg_count_8k: usize,
120 chr_count_1k: usize,
121 regs: [u8; 8],
123 bank_select: u8,
124 prg_mode: bool,
125 chr_mode: bool,
126 irq_counter: u8,
127 irq_latch: u8,
128 irq_reload: bool,
129 irq_enabled: bool,
130 irq_pending: bool,
131 a12: A12RiseFilter,
134 prg_banking_mode: u8,
136 outer_chr_64k: bool,
137 select_chr_ram: bool,
138 mmc3_chr_mode: bool,
139 cnrom_chr_mode: bool,
140 extended_mmc3: bool,
141 prg_base: u16,
142 chr_base: u8,
143 cnrom_chr_reg: u8,
144 submapper: u8,
148 ram_cfg: u8,
152 chr_ram: Box<[u8]>,
156}
157
158impl Fk23c {
159 const FS005_CHR_RAM_BANKS: usize = 8;
162
163 const SAVE_LEN: usize = 8 + 9 + 6 + 2 + 3 + 1;
166
167 fn new(
168 prg_rom: Box<[u8]>,
169 chr_rom: Box<[u8]>,
170 mirroring: Mirroring,
171 submapper: u8,
172 ) -> Result<Self, MapperError> {
173 check_prg(&prg_rom, 176)?;
174 let chr_is_ram = chr_rom.is_empty();
175 let chr: Box<[u8]> = if chr_is_ram {
176 vec![0u8; 0x40000].into_boxed_slice() } else {
178 if !chr_rom.len().is_multiple_of(CHR_BANK_1K) {
179 return Err(MapperError::Invalid(format!(
180 "mapper 176 CHR-ROM size {} is not a multiple of 1 KiB",
181 chr_rom.len()
182 )));
183 }
184 chr_rom
185 };
186 let prg_count_8k = prg_rom.len() / PRG_BANK_8K;
187 let chr_count_1k = (chr.len() / CHR_BANK_1K).max(1);
188 Ok(Self {
189 prg_rom,
190 chr,
191 chr_is_ram,
192 vram: vec![0u8; 2 * NAMETABLE_SIZE].into_boxed_slice(),
193 wram: vec![0u8; 0x8000].into_boxed_slice(),
194 mirroring,
195 prg_count_8k,
196 chr_count_1k,
197 regs: [0; 8],
198 bank_select: 0,
199 prg_mode: false,
200 chr_mode: false,
201 irq_counter: 0,
202 irq_latch: 0,
203 irq_reload: false,
204 irq_enabled: false,
205 irq_pending: false,
206 a12: A12RiseFilter::new(),
207 prg_banking_mode: 0,
208 outer_chr_64k: false,
209 select_chr_ram: false,
210 mmc3_chr_mode: true,
211 cnrom_chr_mode: false,
212 extended_mmc3: false,
213 prg_base: 0,
214 chr_base: 0,
215 cnrom_chr_reg: 0,
216 submapper,
217 ram_cfg: 0,
218 chr_ram: if submapper == 2 {
219 vec![0u8; Self::FS005_CHR_RAM_BANKS * CHR_BANK_1K].into_boxed_slice()
220 } else {
221 Box::default()
222 },
223 })
224 }
225
226 const fn is_fs005(&self) -> bool {
237 self.submapper == 2
238 }
239
240 const fn ram_cfg_enabled(&self) -> bool {
244 self.is_fs005() && (self.ram_cfg & 0x20) != 0
245 }
246
247 const fn outer_regs_enabled(&self) -> bool {
255 !self.ram_cfg_enabled() || (self.ram_cfg & 0x40) != 0
256 }
257
258 const fn wram_bank(&self) -> usize {
262 if self.ram_cfg_enabled() {
263 (self.ram_cfg & 0x03) as usize
264 } else {
265 0
266 }
267 }
268
269 const FS005_REG_WINDOW_OFF: usize = 2 * 0x2000 + 0x1000;
272
273 const fn chr_bank_is_ram(&self, bank: usize) -> bool {
276 self.ram_cfg_enabled() && (self.ram_cfg & 0x04) != 0 && bank < Self::FS005_CHR_RAM_BANKS
277 }
278
279 fn prg_bank_mmc3(&self, slot: usize) -> usize {
280 let last = self.prg_count_8k - 1;
281 let second_last = last.saturating_sub(1);
282 let r6 = self.regs[6] as usize;
283 let r7 = self.regs[7] as usize;
284 match (slot, self.prg_mode) {
285 (0, false) => r6,
286 (0, true) => second_last,
287 (1, _) => r7,
288 (2, false) => second_last,
289 (2, true) => r6,
290 (3, _) => last,
291 _ => 0,
292 }
293 }
294
295 fn resolve_prg(&self, slot: usize) -> usize {
296 let outer = (self.prg_base as usize) << 1;
297 let bank = match self.prg_banking_mode {
298 0..=2 => {
299 if self.extended_mmc3 {
300 self.prg_bank_mmc3(slot) | outer
301 } else {
302 let inner_mask = 0x3F >> self.prg_banking_mode;
303 let outer = outer & !inner_mask;
304 (self.prg_bank_mmc3(slot) & inner_mask) | outer
305 }
306 }
307 3 => {
308 (outer & !0x03) + slot
310 }
311 _ => {
312 ((self.prg_base as usize & 0xFFE) << 1 & !0x07) + slot
314 }
315 };
316 bank % self.prg_count_8k
317 }
318
319 fn chr_bank_mmc3(&self, slot: usize) -> usize {
320 let banks: [usize; 8] = if self.chr_mode {
321 [
322 self.regs[2] as usize,
323 self.regs[3] as usize,
324 self.regs[4] as usize,
325 self.regs[5] as usize,
326 self.regs[0] as usize & !1,
327 (self.regs[0] as usize & !1) | 1,
328 self.regs[1] as usize & !1,
329 (self.regs[1] as usize & !1) | 1,
330 ]
331 } else {
332 [
333 self.regs[0] as usize & !1,
334 (self.regs[0] as usize & !1) | 1,
335 self.regs[1] as usize & !1,
336 (self.regs[1] as usize & !1) | 1,
337 self.regs[2] as usize,
338 self.regs[3] as usize,
339 self.regs[4] as usize,
340 self.regs[5] as usize,
341 ]
342 };
343 banks[slot & 0x07]
344 }
345
346 fn resolve_chr(&self, slot: usize) -> usize {
347 let bank = if self.mmc3_chr_mode {
348 let outer = (self.chr_base as usize) << 3;
349 if self.extended_mmc3 {
350 self.chr_bank_mmc3(slot) | outer
351 } else {
352 let inner_mask = if self.outer_chr_64k { 0x7F } else { 0xFF };
353 let outer = outer & !inner_mask;
354 (self.chr_bank_mmc3(slot) & inner_mask) | outer
355 }
356 } else {
357 let inner_mask = if self.cnrom_chr_mode {
359 if self.outer_chr_64k { 1 } else { 3 }
360 } else {
361 0
362 };
363 (((self.cnrom_chr_reg as usize & inner_mask) | self.chr_base as usize) << 3) + slot
364 };
365 bank % self.chr_count_1k
366 }
367
368 fn write_5000(&mut self, addr: u16, value: u8) {
369 match addr & 0x03 {
370 0 => {
371 self.prg_banking_mode = value & 0x07;
372 self.outer_chr_64k = value & 0x10 != 0;
373 self.select_chr_ram = value & 0x20 != 0;
374 self.mmc3_chr_mode = value & 0x40 == 0;
375 if self.is_fs005() {
381 self.prg_base = (self.prg_base & !0x180)
382 | (((value as u16) & 0x80) << 1)
383 | (((value as u16) & 0x08) << 4);
384 }
385 }
386 1 => self.prg_base = (self.prg_base & !0x7F) | (value as u16 & 0x7F),
387 2 => {
388 if self.is_fs005() {
391 self.prg_base = (self.prg_base & !0xE00)
392 | (((value as u16) & 0x40) << 3)
393 | (((value as u16) & 0x80) << 3)
394 | (((value as u16) & 0x20) << 6);
395 }
396 self.chr_base = value;
397 self.cnrom_chr_reg = 0;
398 }
399 _ => {
400 self.extended_mmc3 = value & 0x02 != 0;
401 self.cnrom_chr_mode = value & 0x44 != 0;
402 }
403 }
404 }
405
406 fn write_mmc3(&mut self, addr: u16, value: u8) {
407 if self.cnrom_chr_mode && (addr <= 0x9FFF || addr >= 0xC000) {
408 self.cnrom_chr_reg = value & 0x03;
409 }
410 match addr & 0xE003 {
415 0x8000 => {
416 let mut reg = value & 0x0F;
420 if self.is_fs005() && (value & 0x40) != 0 && matches!(reg, 6 | 7) {
421 reg ^= 1;
422 }
423 self.bank_select = reg;
424 self.prg_mode = value & 0x40 != 0;
425 self.chr_mode = value & 0x80 != 0;
426 }
427 0x8001 => {
428 let idx = (self.bank_select & 0x07) as usize;
429 self.regs[idx] = value;
430 }
431 0xA000 => {
432 self.mirroring = match (value & 0x03, self.ram_cfg_enabled()) {
436 (2, true) => Mirroring::SingleScreenA,
437 (3, true) => Mirroring::SingleScreenB,
438 (v, _) if v & 0x01 == 0 => Mirroring::Vertical,
444 _ => Mirroring::Horizontal,
445 };
446 }
447 0xA001 if self.is_fs005() => {
448 self.ram_cfg = value;
449 self.mirroring = match self.mirroring {
460 Mirroring::SingleScreenA if !self.ram_cfg_enabled() => Mirroring::Vertical,
461 Mirroring::SingleScreenB if !self.ram_cfg_enabled() => Mirroring::Horizontal,
462 other => other,
463 };
464 }
465 0xC000 => self.irq_latch = value,
466 0xC001 => {
467 self.irq_counter = 0;
468 self.irq_reload = true;
469 }
470 0xE000 => {
471 self.irq_enabled = false;
472 self.irq_pending = false;
473 }
474 0xE001 => self.irq_enabled = true,
475 _ => {}
476 }
477 }
478}
479
480impl Mapper for Fk23c {
481 fn sram(&self) -> &[u8] {
488 &self.wram
489 }
490 fn sram_mut(&mut self) -> &mut [u8] {
491 &mut self.wram
492 }
493
494 fn caps(&self) -> MapperCaps {
495 MapperCaps {
496 cpu_cycle_hook: true,
499 audio: false,
500 frame_event_hook: false,
501 irq_source: true,
502 }
503 }
504
505 fn cpu_read(&mut self, addr: u16) -> u8 {
506 match addr {
507 0x5000..=0x5FFF if !self.outer_regs_enabled() => {
510 self.wram[Self::FS005_REG_WINDOW_OFF + (addr as usize & 0x0FFF)]
511 }
512 0x6000..=0x7FFF => self.wram[self.wram_bank() * 0x2000 + (addr as usize & 0x1FFF)],
513 0x8000..=0x9FFF => {
514 let b = self.resolve_prg(0);
515 self.prg_rom[b * PRG_BANK_8K + (addr as usize & 0x1FFF)]
516 }
517 0xA000..=0xBFFF => {
518 let b = self.resolve_prg(1);
519 self.prg_rom[b * PRG_BANK_8K + (addr as usize & 0x1FFF)]
520 }
521 0xC000..=0xDFFF => {
522 let b = self.resolve_prg(2);
523 self.prg_rom[b * PRG_BANK_8K + (addr as usize & 0x1FFF)]
524 }
525 0xE000..=0xFFFF => {
526 let b = self.resolve_prg(3);
527 self.prg_rom[b * PRG_BANK_8K + (addr as usize & 0x1FFF)]
528 }
529 _ => 0,
530 }
531 }
532
533 fn cpu_write(&mut self, addr: u16, value: u8) {
534 match addr {
535 0x5000..=0x5FFF => {
536 if self.outer_regs_enabled() {
537 self.write_5000(addr, value);
538 } else {
539 self.wram[Self::FS005_REG_WINDOW_OFF + (addr as usize & 0x0FFF)] = value;
540 }
541 }
542 0x6000..=0x7FFF => {
543 let off = self.wram_bank() * 0x2000 + (addr as usize & 0x1FFF);
544 self.wram[off] = value;
545 }
546 0x8000..=0xFFFF => self.write_mmc3(addr, value),
547 _ => {}
548 }
549 }
550
551 fn ppu_read(&mut self, addr: u16) -> u8 {
552 let addr = addr & 0x3FFF;
553 match addr {
554 0x0000..=0x1FFF => {
555 let slot = (addr as usize) / CHR_BANK_1K;
562 let b = self.resolve_chr(slot);
563 if self.chr_bank_is_ram(b) {
564 return self.chr_ram[b * CHR_BANK_1K + (addr as usize & 0x3FF)];
565 }
566 if self.chr_is_ram || self.select_chr_ram {
567 return self.chr[addr as usize & (self.chr.len() - 1)];
568 }
569 self.chr[b * CHR_BANK_1K + (addr as usize & 0x3FF)]
570 }
571 0x2000..=0x3EFF => self.vram[nametable_offset(addr, self.mirroring)],
572 _ => 0,
573 }
574 }
575
576 fn ppu_write(&mut self, addr: u16, value: u8) {
577 let addr = addr & 0x3FFF;
578 match addr {
579 0x0000..=0x1FFF if self.chr_is_ram => {
587 self.chr[addr as usize & (self.chr.len() - 1)] = value;
588 }
589 0x0000..=0x1FFF => {
593 let slot = (addr as usize) / CHR_BANK_1K;
594 let b = self.resolve_chr(slot);
595 if self.chr_bank_is_ram(b) {
596 self.chr_ram[b * CHR_BANK_1K + (addr as usize & 0x3FF)] = value;
597 }
598 }
599 0x2000..=0x3EFF => {
600 let off = nametable_offset(addr, self.mirroring);
601 self.vram[off] = value;
602 }
603 _ => {}
604 }
605 }
606
607 fn notify_cpu_cycle(&mut self) {
608 self.a12.tick();
609 }
610
611 fn notify_a12(&mut self, level: bool) {
612 if !self.a12.edge(level) {
615 return;
616 }
617 if self.irq_counter == 0 || self.irq_reload {
618 self.irq_counter = self.irq_latch;
619 self.irq_reload = false;
620 } else {
621 self.irq_counter = self.irq_counter.wrapping_sub(1);
622 }
623 if self.irq_counter == 0 && self.irq_enabled {
624 self.irq_pending = true;
625 }
626 }
627
628 fn irq_pending(&self) -> bool {
629 self.irq_pending
630 }
631
632 fn irq_acknowledge(&mut self) {
633 self.irq_pending = false;
634 }
635
636 fn current_mirroring(&self) -> Mirroring {
637 self.mirroring
638 }
639
640 fn save_state(&self) -> Vec<u8> {
641 let chr_ram = if self.chr_is_ram { self.chr.len() } else { 0 };
642 let mut out =
643 Vec::with_capacity(1 + Self::SAVE_LEN + self.vram.len() + self.wram.len() + chr_ram);
644 out.push(SAVE_STATE_VERSION);
645 out.extend_from_slice(&self.regs);
646 out.push(self.bank_select);
647 out.push(u8::from(self.prg_mode));
648 out.push(u8::from(self.chr_mode));
649 out.push(self.irq_counter);
650 out.push(self.irq_latch);
651 out.push(u8::from(self.irq_reload));
652 out.push(u8::from(self.irq_enabled));
653 out.push(u8::from(self.irq_pending));
654 out.push(self.a12.to_byte());
655 out.push(self.prg_banking_mode);
656 out.push(u8::from(self.outer_chr_64k));
657 out.push(u8::from(self.select_chr_ram));
658 out.push(u8::from(self.mmc3_chr_mode));
659 out.push(u8::from(self.cnrom_chr_mode));
660 out.push(u8::from(self.extended_mmc3));
661 out.extend_from_slice(&self.prg_base.to_le_bytes());
662 out.push(self.chr_base);
663 out.push(self.cnrom_chr_reg);
664 out.push(mirroring_to_byte(self.mirroring));
665 out.push(self.ram_cfg);
666 out.extend_from_slice(&self.vram);
667 out.extend_from_slice(&self.wram);
668 if self.chr_is_ram {
669 out.extend_from_slice(&self.chr);
670 }
671 out.extend_from_slice(&self.chr_ram);
673 out
674 }
675
676 fn load_state(&mut self, data: &[u8]) -> Result<(), MapperError> {
677 let chr_ram = if self.chr_is_ram { self.chr.len() } else { 0 };
678 match data.first() {
683 None => {
684 return Err(MapperError::WrongLength {
685 expected: 1,
686 got: 0,
687 });
688 }
689 Some(&v) if v != SAVE_STATE_VERSION => {
690 return Err(MapperError::UnsupportedVersion(v));
691 }
692 Some(_) => {}
693 }
694 let expected =
695 1 + Self::SAVE_LEN + self.vram.len() + self.wram.len() + chr_ram + self.chr_ram.len();
696 if data.len() != expected {
697 return Err(MapperError::WrongLength {
698 expected,
699 got: data.len(),
700 });
701 }
702 let mut c = 1;
703 self.regs.copy_from_slice(&data[c..c + 8]);
704 c += 8;
705 self.bank_select = data[c];
706 self.prg_mode = data[c + 1] != 0;
707 self.chr_mode = data[c + 2] != 0;
708 self.irq_counter = data[c + 3];
709 self.irq_latch = data[c + 4];
710 self.irq_reload = data[c + 5] != 0;
711 self.irq_enabled = data[c + 6] != 0;
712 self.irq_pending = data[c + 7] != 0;
713 self.a12 = A12RiseFilter::from_byte(data[c + 8]);
714 c += 9;
715 self.prg_banking_mode = data[c];
716 self.outer_chr_64k = data[c + 1] != 0;
717 self.select_chr_ram = data[c + 2] != 0;
718 self.mmc3_chr_mode = data[c + 3] != 0;
719 self.cnrom_chr_mode = data[c + 4] != 0;
720 self.extended_mmc3 = data[c + 5] != 0;
721 c += 6;
722 self.prg_base = u16::from_le_bytes([data[c], data[c + 1]]);
723 c += 2;
724 self.chr_base = data[c];
725 self.cnrom_chr_reg = data[c + 1];
726 self.mirroring = byte_to_mirroring(data[c + 2], self.mirroring);
727 c += 3;
728 self.ram_cfg = data[c];
729 c += 1;
730 self.vram.copy_from_slice(&data[c..c + self.vram.len()]);
731 c += self.vram.len();
732 self.wram.copy_from_slice(&data[c..c + self.wram.len()]);
733 c += self.wram.len();
734 if self.chr_is_ram {
735 self.chr.copy_from_slice(&data[c..c + self.chr.len()]);
736 c += self.chr.len();
737 }
738 let n = self.chr_ram.len();
739 self.chr_ram.copy_from_slice(&data[c..c + n]);
740 Ok(())
741 }
742}
743
744pub fn new_m176(
753 prg_rom: Box<[u8]>,
754 chr_rom: Box<[u8]>,
755 mirroring: Mirroring,
756 submapper: u8,
757) -> Result<Fk23c, MapperError> {
758 Fk23c::new(prg_rom, chr_rom, mirroring, submapper)
759}
760
761#[cfg(test)]
774#[allow(clippy::cast_possible_truncation)]
775mod tests {
776
777 #[test]
781 fn irq_counts_scanlines_not_raw_a12_pulses() {
782 let mut m = new_m176(synth_prg_8k(32), synth_chr_1k(64), Mirroring::Vertical, 0).unwrap();
783 assert_eq!(crate::a12_filter::irqs_over_scanlines(&mut m, 16), 2);
784 }
785
786 use super::*;
787
788 fn synth_prg_8k(banks: usize) -> Box<[u8]> {
789 let mut v = vec![0xFFu8; banks * PRG_BANK_8K];
790 for b in 0..banks {
791 v[b * PRG_BANK_8K] = b as u8;
792 }
793 v.into_boxed_slice()
794 }
795
796 fn synth_chr_1k(banks: usize) -> Box<[u8]> {
797 let mut v = vec![0u8; banks * CHR_BANK_1K];
798 for b in 0..banks {
799 v[b * CHR_BANK_1K] = b as u8;
800 }
801 v.into_boxed_slice()
802 }
803
804 #[test]
805 fn fk23c_truncated_save_state_rejected() {
806 let m = new_m176(synth_prg_8k(16), synth_chr_1k(32), Mirroring::Vertical, 0).unwrap();
807 let mut blob = m.save_state();
808 blob.pop();
809 let mut m2 = new_m176(synth_prg_8k(16), synth_chr_1k(32), Mirroring::Vertical, 0).unwrap();
810 assert!(m2.load_state(&blob).is_err());
811 }
812
813 #[test]
814 fn fk23c_mmc3_prg_and_a12_irq() {
815 let mut m = new_m176(synth_prg_8k(32), synth_chr_1k(64), Mirroring::Vertical, 0).unwrap();
816 m.cpu_write(0x8000, 0x06); m.cpu_write(0x8001, 5);
818 assert_eq!(m.cpu_read(0x8000), 5); assert_eq!(m.cpu_read(0xE000), 31); m.cpu_write(0xC000, 2); m.cpu_write(0xC001, 0); m.cpu_write(0xE001, 0); for _ in 0..3 {
825 m.notify_a12(false);
826 for _ in 0..3 {
828 m.notify_cpu_cycle();
829 }
830 m.notify_a12(true);
831 }
832 assert!(m.irq_pending());
833 m.cpu_write(0xE000, 0); assert!(!m.irq_pending());
835 }
836
837 #[test]
838 fn fk23c_outer_prg_base() {
839 let mut m = new_m176(synth_prg_8k(128), synth_chr_1k(64), Mirroring::Vertical, 0).unwrap();
840 m.cpu_write(0x5001, 0x08); m.cpu_write(0x8000, 0x06); m.cpu_write(0x8001, 0); let _ = m.cpu_read(0x8000);
847 assert!(m.cpu_read(0x8000) < 128);
848 }
849
850 #[test]
851 fn fk23c_save_state_round_trip() {
852 let mut m = new_m176(synth_prg_8k(32), Box::new([]), Mirroring::Vertical, 0).unwrap();
853 m.cpu_write(0x5000, 0x20); m.cpu_write(0x8000, 0x06);
855 m.cpu_write(0x8001, 7);
856 m.ppu_write(0x0040, 0x99);
857 m.cpu_write(0x6000, 0x5A); let blob = m.save_state();
859 let mut m2 = new_m176(synth_prg_8k(32), Box::new([]), Mirroring::Vertical, 0).unwrap();
860 m2.load_state(&blob).unwrap();
861 assert_eq!(m2.cpu_read(0x8000), m.cpu_read(0x8000));
862 assert_eq!(m2.ppu_read(0x0040), 0x99);
863 assert_eq!(m2.cpu_read(0x6000), 0x5A);
864 }
865
866 #[test]
867 fn fk23c_chr_rom_not_writable_via_select_chr_ram() {
868 let mut m = new_m176(synth_prg_8k(32), synth_chr_1k(64), Mirroring::Vertical, 0).unwrap();
872 m.cpu_write(0x5000, 0x20); let before = m.ppu_read(0x0010);
874 m.ppu_write(0x0010, before.wrapping_add(1));
875 assert_eq!(m.ppu_read(0x0010), before, "CHR-ROM must not be mutable");
876 }
877
878 fn fs005() -> Fk23c {
881 new_m176(synth_prg_8k(32), synth_chr_1k(256), Mirroring::Vertical, 2).unwrap()
882 }
883
884 #[test]
885 fn fs005_a001_bit5_switches_a001_from_mmc3_to_ram_config() {
886 let mut m = fs005();
887 m.cpu_write(0xA001, 0x02);
890 m.cpu_write(0x6000, 0xAA);
891 m.cpu_write(0xA001, 0x00);
892 assert_eq!(
893 m.cpu_read(0x6000),
894 0xAA,
895 "bank select must be ignored while $A001.5 is clear"
896 );
897
898 m.cpu_write(0xA001, 0x22);
901 assert_ne!(m.cpu_read(0x6000), 0xAA, "bank 2 must not alias bank 0");
902 m.cpu_write(0x6000, 0x55);
903 m.cpu_write(0xA001, 0x20); assert_eq!(m.cpu_read(0x6000), 0xAA);
905 m.cpu_write(0xA001, 0x22); assert_eq!(m.cpu_read(0x6000), 0x55);
907 }
908
909 #[test]
910 fn fs005_protection_sequence_round_trips_through_the_register_window() {
911 let mut m = fs005();
915 m.cpu_write(0xA001, 0xA1); m.cpu_write(0x5000, 0x11);
917 m.cpu_write(0x5010, 0x22);
918 m.cpu_write(0x5013, 0x33);
919 m.cpu_write(0xA001, 0xE2); assert_eq!(m.cpu_read(0x7000), 0x11);
922 assert_eq!(m.cpu_read(0x7010), 0x22);
923 assert_eq!(m.cpu_read(0x7013), 0x33);
924 }
925
926 #[test]
927 fn fs005_disabled_register_window_does_not_reach_the_mapper() {
928 let mut m = fs005();
932 m.cpu_write(0x8000, 0x06);
933 m.cpu_write(0x8001, 5);
934 let before = m.cpu_read(0x8000);
935
936 m.cpu_write(0xA001, 0xA1); m.cpu_write(0x5000, 0x04); assert_eq!(
939 m.cpu_read(0x8000),
940 before,
941 "a disabled window must not bank"
942 );
943
944 m.cpu_write(0xA001, 0xE0); m.cpu_write(0x5000, 0x04);
946 assert_ne!(m.cpu_read(0x8000), before, "an enabled window must bank");
947 }
948
949 #[test]
950 fn fs005_a000_selects_single_screen_only_while_ram_config_is_enabled() {
951 let mut m = fs005();
952 m.cpu_write(0xA000, 0x02);
953 assert_eq!(
959 m.current_mirroring(),
960 Mirroring::Vertical,
961 "single-screen is unwired until $A001.5 is set, so bit 0 decides"
962 );
963
964 m.cpu_write(0xA001, 0x20);
965 m.cpu_write(0xA000, 0x02);
966 assert_eq!(m.current_mirroring(), Mirroring::SingleScreenA);
967 m.cpu_write(0xA000, 0x03);
968 assert_eq!(m.current_mirroring(), Mirroring::SingleScreenB);
969 m.cpu_write(0xA000, 0x00);
970 assert_eq!(m.current_mirroring(), Mirroring::Vertical);
971
972 m.cpu_write(0xA000, 0x03);
975 m.cpu_write(0xA001, 0x00);
976 assert_eq!(m.current_mirroring(), Mirroring::Horizontal);
977 }
978
979 #[test]
980 fn fs005_swaps_bank_select_46_and_47_but_not_06_and_07() {
981 let mut m = fs005();
982 m.cpu_write(0x8000, 0x46);
984 m.cpu_write(0x8001, 9);
985 m.cpu_write(0x8000, 0x47);
986 m.cpu_write(0x8001, 4);
987 assert_eq!(m.cpu_read(0xA000), 9);
990 assert_eq!(m.cpu_read(0xC000), 4);
991
992 let mut m = fs005();
994 m.cpu_write(0x8000, 0x06);
995 m.cpu_write(0x8001, 9);
996 m.cpu_write(0x8000, 0x07);
997 m.cpu_write(0x8001, 4);
998 assert_eq!(m.cpu_read(0x8000), 9, "R6 at $8000 with PRG-invert clear");
999 assert_eq!(m.cpu_read(0xA000), 4, "R7 at $A000");
1000 }
1001
1002 #[test]
1003 fn fk23c_bank_select_46_47_is_not_swapped_on_submapper_0() {
1004 let mut m = new_m176(synth_prg_8k(32), synth_chr_1k(64), Mirroring::Vertical, 0).unwrap();
1006 m.cpu_write(0x8000, 0x46);
1007 m.cpu_write(0x8001, 9);
1008 assert_eq!(m.cpu_read(0xC000), 9, "R6 at $C000 with PRG-invert set");
1009 }
1010
1011 #[test]
1012 fn mmc3_registers_decode_with_mask_e003_so_9fff_does_nothing() {
1013 let mut m = fs005();
1014 m.cpu_write(0x8000, 0x06);
1015 m.cpu_write(0x8001, 5);
1016 let before = m.cpu_read(0x8000);
1017 m.cpu_write(0x9FFF, 0x1F);
1020 assert_eq!(m.cpu_read(0x8000), before);
1021 }
1022
1023 #[test]
1024 fn fs005_mixed_chr_makes_the_first_8k_writable_on_a_chr_rom_cart() {
1025 let mut m = fs005();
1026 m.cpu_write(0xA001, 0x24); let before = m.ppu_read(0x0010);
1029 m.ppu_write(0x0010, before.wrapping_add(1));
1030 assert_eq!(
1031 m.ppu_read(0x0010),
1032 before.wrapping_add(1),
1033 "the redirected banks are RAM and must accept writes"
1034 );
1035
1036 m.cpu_write(0xA001, 0x20);
1039 assert_eq!(m.ppu_read(0x0010), before);
1040 m.ppu_write(0x0010, before.wrapping_add(2));
1041 assert_eq!(m.ppu_read(0x0010), before, "CHR-ROM must stay immutable");
1042 }
1043
1044 #[test]
1045 fn fs005_save_state_round_trips_ram_config_and_chr_overlay() {
1046 let mut m = fs005();
1047 m.cpu_write(0xA001, 0x26); m.ppu_write(0x0010, 0x5A);
1049 m.cpu_write(0x6000, 0xC3);
1050 m.cpu_write(0x8000, 0x46);
1051 m.cpu_write(0x8001, 9);
1052 let blob = m.save_state();
1053
1054 let mut m2 = fs005();
1055 m2.load_state(&blob).unwrap();
1056 assert_eq!(m2.ppu_read(0x0010), 0x5A, "CHR-RAM overlay must round-trip");
1057 assert_eq!(m2.cpu_read(0x6000), 0xC3, "banked WRAM must round-trip");
1058 assert_eq!(m2.cpu_read(0xA000), m.cpu_read(0xA000));
1059 assert_eq!(m2.current_mirroring(), m.current_mirroring());
1060 }
1061
1062 #[test]
1063 fn fs005_state_blob_is_longer_than_the_fk23c_one() {
1064 let a = new_m176(synth_prg_8k(32), synth_chr_1k(256), Mirroring::Vertical, 0)
1067 .unwrap()
1068 .save_state()
1069 .len();
1070 let b = fs005().save_state().len();
1071 assert_eq!(b - a, Fk23c::FS005_CHR_RAM_BANKS * CHR_BANK_1K);
1072 }
1073
1074 #[test]
1075 fn a000_keeps_the_mmc3_bit0_decode_when_single_screen_is_not_armed() {
1076 for sub in [0u8, 2] {
1080 let mut m = new_m176(
1081 synth_prg_8k(32),
1082 synth_chr_1k(64),
1083 Mirroring::Horizontal,
1084 sub,
1085 )
1086 .unwrap();
1087 for v in [0x00u8, 0x02, 0x04, 0x06] {
1088 m.cpu_write(0xA000, v);
1089 assert_eq!(
1090 m.current_mirroring(),
1091 Mirroring::Vertical,
1092 "submapper {sub}: ${v:02X} is Vertical (bit 0 clear)"
1093 );
1094 }
1095 for v in [0x01u8, 0x03, 0x05, 0x07] {
1096 m.cpu_write(0xA000, v);
1097 assert_eq!(
1098 m.current_mirroring(),
1099 Mirroring::Horizontal,
1100 "submapper {sub}: ${v:02X} is Horizontal (bit 0 set)"
1101 );
1102 }
1103 }
1104 }
1105
1106 #[test]
1107 fn fs005_mixed_chr_overlay_outranks_the_flat_chr_window() {
1108 let mut m = fs005();
1112 m.cpu_write(0xA001, 0x24); m.cpu_write(0x5000, 0x20); m.ppu_write(0x0010, 0x77);
1115 assert_eq!(
1116 m.ppu_read(0x0010),
1117 0x77,
1118 "the overlay must answer the read, not the flat CHR window"
1119 );
1120 }
1121
1122 #[test]
1123 fn fk23c_refuses_v1_and_v2_states() {
1124 let mut m = new_m176(synth_prg_8k(32), synth_chr_1k(64), Mirroring::Vertical, 0).unwrap();
1128 m.cpu_write(0x8000, 0x06);
1129 m.cpu_write(0x8001, 5);
1130 let v2 = m.save_state();
1131
1132 let off = 1 + Fk23c::SAVE_LEN - 1;
1135 let mut v1 = Vec::with_capacity(v2.len() - 1);
1136 v1.extend_from_slice(&v2[..off]);
1137 v1.extend_from_slice(&v2[off + 1..]);
1138 v1[0] = 1;
1139
1140 let mut m2 = new_m176(synth_prg_8k(32), synth_chr_1k(64), Mirroring::Vertical, 0).unwrap();
1141 assert!(matches!(
1142 m2.load_state(&v1),
1143 Err(MapperError::UnsupportedVersion(1))
1144 ));
1145 let mut old_v2 = v2.clone();
1146 old_v2[0] = 2;
1147 assert!(matches!(
1148 m2.load_state(&old_v2),
1149 Err(MapperError::UnsupportedVersion(2))
1150 ));
1151 m2.load_state(&v2).expect("the current state loads");
1152 assert_eq!(m2.cpu_read(0x8000), 5, "bank state round-trips");
1153 }
1154
1155 #[test]
1156 fn fs005_disabling_ram_config_falls_back_per_the_mmc3_bit0_decode() {
1157 let mut m = fs005();
1161 m.cpu_write(0xA001, 0x20); m.cpu_write(0xA000, 0x02); assert_eq!(m.current_mirroring(), Mirroring::SingleScreenA);
1164 m.cpu_write(0xA001, 0x00); assert_eq!(
1166 m.current_mirroring(),
1167 Mirroring::Vertical,
1168 "A came from a bit-0-clear write, so it falls back to Vertical"
1169 );
1170
1171 let mut m = fs005();
1172 m.cpu_write(0xA001, 0x20);
1173 m.cpu_write(0xA000, 0x03); assert_eq!(m.current_mirroring(), Mirroring::SingleScreenB);
1175 m.cpu_write(0xA001, 0x00);
1176 assert_eq!(
1177 m.current_mirroring(),
1178 Mirroring::Horizontal,
1179 "B came from a bit-0-set write, so it falls back to Horizontal"
1180 );
1181 }
1182}