1#![allow(
24 clippy::bool_to_int_with_if,
25 clippy::cast_lossless,
26 clippy::cast_possible_truncation,
27 clippy::doc_markdown,
28 clippy::match_same_arms,
29 clippy::missing_const_for_fn,
30 clippy::similar_names,
31 clippy::struct_excessive_bools,
32 clippy::too_many_lines,
33 clippy::unreadable_literal
34)]
35
36use crate::cartridge::Mirroring;
37use crate::mapper::{Mapper, MapperCaps, MapperError};
38use crate::sst39sf040::{
39 Sst39sf040, decode_sector_diff, encode_sector_diff, sector_bitmap_len, sector_diff_len,
40};
41use alloc::{boxed::Box, vec::Vec};
42use alloc::{format, vec};
43
44const PRG_BANK_4K: usize = 0x1000;
45
46fn flash_state_len(
53 mapper: u16,
54 fixed: usize,
55 flash_len: usize,
56 data: &[u8],
57) -> Result<usize, MapperError> {
58 let tail = &data[fixed..];
59 let bitmap = sector_bitmap_len(flash_len);
60 if tail.len() < bitmap {
61 return Err(MapperError::WrongLength {
62 expected: fixed + bitmap,
63 got: data.len(),
64 });
65 }
66 sector_diff_len(flash_len, tail)
67 .map(|n| fixed + n)
68 .ok_or_else(|| {
69 MapperError::Invalid(format!(
70 "mapper {mapper} flash bitmap marks a sector past the {flash_len}-byte chip"
71 ))
72 })
73}
74const PRG_BANK_16K: usize = 0x4000;
75const PRG_BANK_32K: usize = 0x8000;
76const CHR_BANK_8K: usize = 0x2000;
77const NAMETABLE_SIZE: usize = 0x0400;
78const NAMETABLE_SIZE_U16: u16 = 0x0400;
79
80const SAVE_STATE_VERSION: u8 = 1;
81
82const fn nametable_offset(addr: u16, mirroring: Mirroring) -> usize {
87 let table = (((addr - 0x2000) / NAMETABLE_SIZE_U16) & 0x03) as u8;
88 let local = (addr as usize) & (NAMETABLE_SIZE - 1);
89 let physical = mirroring.physical_bank(table);
90 physical * NAMETABLE_SIZE + local
91}
92
93pub struct Inl31 {
104 prg_rom: Box<[u8]>,
105 chr_ram: Box<[u8]>,
106 vram: Box<[u8]>,
107 prg_slots: [u8; 8],
108 mirroring: Mirroring,
109}
110
111impl Inl31 {
112 #[allow(clippy::cast_possible_truncation)]
119 pub fn new(
120 prg_rom: Box<[u8]>,
121 _chr_rom: &[u8],
122 mirroring: Mirroring,
123 ) -> Result<Self, MapperError> {
124 if prg_rom.is_empty() || !prg_rom.len().is_multiple_of(PRG_BANK_4K) {
125 return Err(MapperError::Invalid(format!(
126 "mapper 31 PRG-ROM size {} is not a non-zero multiple of 4 KiB",
127 prg_rom.len()
128 )));
129 }
130 let last = ((prg_rom.len() / PRG_BANK_4K).max(1) - 1) as u8;
133 let mut prg_slots = [0u8; 8];
134 prg_slots[7] = last;
135 Ok(Self {
136 prg_rom,
137 chr_ram: vec![0u8; CHR_BANK_8K].into_boxed_slice(),
138 vram: vec![0u8; 2 * NAMETABLE_SIZE].into_boxed_slice(),
139 prg_slots,
140 mirroring,
141 })
142 }
143}
144
145impl Mapper for Inl31 {
146 fn caps(&self) -> MapperCaps {
147 MapperCaps::NONE
148 }
149
150 fn cpu_read(&mut self, addr: u16) -> u8 {
154 if (0x8000..=0xFFFF).contains(&addr) {
155 let count = (self.prg_rom.len() / PRG_BANK_4K).max(1);
156 let slot = ((addr >> 12) & 0x07) as usize;
157 let bank = (self.prg_slots[slot] as usize) % count;
158 self.prg_rom[bank * PRG_BANK_4K + (addr as usize & 0x0FFF)]
159 } else {
160 0
161 }
162 }
163
164 fn cpu_write(&mut self, addr: u16, value: u8) {
165 if (0x5FF8..=0x5FFF).contains(&addr) {
166 self.prg_slots[(addr & 0x07) as usize] = value;
167 }
168 }
169
170 fn ppu_read(&mut self, addr: u16) -> u8 {
171 let addr = addr & 0x3FFF;
172 match addr {
173 0x0000..=0x1FFF => self.chr_ram[addr as usize],
174 0x2000..=0x3EFF => self.vram[nametable_offset(addr, self.mirroring)],
175 _ => 0,
176 }
177 }
178
179 fn ppu_write(&mut self, addr: u16, value: u8) {
180 let addr = addr & 0x3FFF;
181 match addr {
182 0x0000..=0x1FFF => self.chr_ram[addr as usize] = value,
183 0x2000..=0x3EFF => {
184 let off = nametable_offset(addr, self.mirroring);
185 self.vram[off] = value;
186 }
187 _ => {}
188 }
189 }
190
191 fn current_mirroring(&self) -> Mirroring {
192 self.mirroring
193 }
194
195 fn save_state(&self) -> Vec<u8> {
196 let mut out = Vec::with_capacity(1 + 8 + self.vram.len() + self.chr_ram.len());
197 out.push(SAVE_STATE_VERSION);
198 out.extend_from_slice(&self.prg_slots);
199 out.extend_from_slice(&self.vram);
200 out.extend_from_slice(&self.chr_ram);
201 out
202 }
203
204 fn load_state(&mut self, data: &[u8]) -> Result<(), MapperError> {
205 let expected = 1 + 8 + self.vram.len() + self.chr_ram.len();
206 if data.len() != expected {
207 return Err(MapperError::WrongLength {
208 expected,
209 got: data.len(),
210 });
211 }
212 if data[0] != SAVE_STATE_VERSION {
213 return Err(MapperError::UnsupportedVersion(data[0]));
214 }
215 self.prg_slots.copy_from_slice(&data[1..9]);
216 let mut cursor = 9;
217 self.vram
218 .copy_from_slice(&data[cursor..cursor + self.vram.len()]);
219 cursor += self.vram.len();
220 self.chr_ram
221 .copy_from_slice(&data[cursor..cursor + self.chr_ram.len()]);
222 Ok(())
223 }
224}
225
226#[derive(Clone, Copy, PartialEq, Eq)]
228enum MagicFloorMode {
229 Vertical,
230 Horizontal,
231 ScreenA,
232 ScreenB,
233}
234
235impl MagicFloorMode {
236 const fn physical_bank(self, addr: u16) -> usize {
256 let line = match self {
257 Self::Vertical => 10,
258 Self::Horizontal => 11,
259 Self::ScreenA => 12,
260 Self::ScreenB => 13,
261 };
262 ((addr >> line) & 0x01) as usize
263 }
264}
265
266pub struct MagicFloor218 {
268 prg_rom: Box<[u8]>,
269 ciram: Box<[u8]>,
271 mode: MagicFloorMode,
272}
273
274impl MagicFloor218 {
275 pub fn new(
284 prg_rom: Box<[u8]>,
285 chr_rom: &[u8],
286 mirroring: Mirroring,
287 ) -> Result<Self, MapperError> {
288 if prg_rom.is_empty() || !prg_rom.len().is_multiple_of(PRG_BANK_16K) {
289 return Err(MapperError::Invalid(format!(
290 "mapper 218 PRG-ROM size {} is not a non-zero multiple of 16 KiB",
291 prg_rom.len()
292 )));
293 }
294 if !chr_rom.is_empty() {
295 return Err(MapperError::Invalid(format!(
296 "mapper 218 has no CHR-ROM (CIRAM is used as CHR); got {} bytes",
297 chr_rom.len()
298 )));
299 }
300 let mode = match mirroring {
306 Mirroring::Vertical | Mirroring::FourScreen => MagicFloorMode::Vertical,
307 Mirroring::SingleScreenA => MagicFloorMode::ScreenA,
308 Mirroring::SingleScreenB => MagicFloorMode::ScreenB,
309 Mirroring::Horizontal | Mirroring::MapperControlled => MagicFloorMode::Horizontal,
310 };
311 Ok(Self {
312 prg_rom,
313 ciram: vec![0u8; 2 * NAMETABLE_SIZE].into_boxed_slice(),
314 mode,
315 })
316 }
317
318 const fn chr_offset(&self, addr: u16) -> usize {
321 let local = (addr as usize) & (NAMETABLE_SIZE - 1);
322 self.mode.physical_bank(addr) * NAMETABLE_SIZE + local
323 }
324
325 const fn nt_offset(&self, addr: u16) -> usize {
328 let local = (addr as usize) & (NAMETABLE_SIZE - 1);
329 self.mode.physical_bank(addr) * NAMETABLE_SIZE + local
330 }
331}
332
333impl Mapper for MagicFloor218 {
334 fn caps(&self) -> MapperCaps {
335 MapperCaps::NONE
336 }
337
338 fn cpu_read(&mut self, addr: u16) -> u8 {
339 if (0x8000..=0xFFFF).contains(&addr) {
340 self.prg_rom[(addr as usize - 0x8000) % self.prg_rom.len()]
343 } else {
344 0
345 }
346 }
347
348 fn cpu_write(&mut self, _addr: u16, _value: u8) {}
349
350 fn ppu_read(&mut self, addr: u16) -> u8 {
351 let addr = addr & 0x3FFF;
352 match addr {
353 0x0000..=0x1FFF => self.ciram[self.chr_offset(addr)],
354 0x2000..=0x3EFF => self.ciram[self.nt_offset(addr)],
355 _ => 0,
356 }
357 }
358
359 fn ppu_write(&mut self, addr: u16, value: u8) {
360 let addr = addr & 0x3FFF;
361 match addr {
362 0x0000..=0x1FFF => {
363 let off = self.chr_offset(addr);
364 self.ciram[off] = value;
365 }
366 0x2000..=0x3EFF => {
367 let off = self.nt_offset(addr);
368 self.ciram[off] = value;
369 }
370 _ => {}
371 }
372 }
373
374 fn nametable_fetch(&mut self, addr: u16) -> Option<u8> {
375 Some(self.ciram[self.nt_offset(addr)])
376 }
377
378 fn nametable_write(&mut self, addr: u16, value: u8) -> bool {
379 let off = self.nt_offset(addr);
380 self.ciram[off] = value;
381 true
382 }
383
384 fn current_mirroring(&self) -> Mirroring {
385 match self.mode {
386 MagicFloorMode::Vertical => Mirroring::Vertical,
387 MagicFloorMode::Horizontal => Mirroring::Horizontal,
388 MagicFloorMode::ScreenA => Mirroring::SingleScreenA,
389 MagicFloorMode::ScreenB => Mirroring::SingleScreenB,
390 }
391 }
392
393 fn save_state(&self) -> Vec<u8> {
394 let mut out = Vec::with_capacity(1 + self.ciram.len());
395 out.push(SAVE_STATE_VERSION);
396 out.extend_from_slice(&self.ciram);
397 out
398 }
399
400 fn load_state(&mut self, data: &[u8]) -> Result<(), MapperError> {
401 let expected = 1 + self.ciram.len();
402 if data.len() != expected {
403 return Err(MapperError::WrongLength {
404 expected,
405 got: data.len(),
406 });
407 }
408 if data[0] != SAVE_STATE_VERSION {
409 return Err(MapperError::UnsupportedVersion(data[0]));
410 }
411 self.ciram.copy_from_slice(&data[1..=self.ciram.len()]);
412 Ok(())
413 }
414}
415
416pub struct Cufrom29 {
427 prg_rom: Box<[u8]>,
428 chr_ram: Box<[u8]>,
430 vram: Box<[u8]>,
431 prg_bank: u8,
432 chr_bank: u8,
433 mirroring: Mirroring,
434}
435
436impl Cufrom29 {
437 pub fn new(
444 prg_rom: Box<[u8]>,
445 _chr_rom: &[u8],
446 mirroring: Mirroring,
447 ) -> Result<Self, MapperError> {
448 if prg_rom.is_empty() || !prg_rom.len().is_multiple_of(PRG_BANK_16K) {
449 return Err(MapperError::Invalid(format!(
450 "mapper 29 PRG-ROM size {} is not a non-zero multiple of 16 KiB",
451 prg_rom.len()
452 )));
453 }
454 Ok(Self {
455 prg_rom,
456 chr_ram: vec![0u8; 4 * CHR_BANK_8K].into_boxed_slice(),
457 vram: vec![0u8; 2 * NAMETABLE_SIZE].into_boxed_slice(),
458 prg_bank: 0,
459 chr_bank: 0,
460 mirroring,
461 })
462 }
463
464 fn chr_offset(&self, addr: u16) -> usize {
465 let count = (self.chr_ram.len() / CHR_BANK_8K).max(1);
466 let bank = (self.chr_bank as usize) % count;
467 bank * CHR_BANK_8K + (addr as usize & 0x1FFF)
468 }
469}
470
471impl Mapper for Cufrom29 {
472 fn caps(&self) -> MapperCaps {
473 MapperCaps::NONE
474 }
475
476 fn cpu_read(&mut self, addr: u16) -> u8 {
477 match addr {
478 0x8000..=0xBFFF => {
479 let count = (self.prg_rom.len() / PRG_BANK_16K).max(1);
480 let bank = (self.prg_bank as usize) % count;
481 self.prg_rom[bank * PRG_BANK_16K + (addr as usize & 0x3FFF)]
482 }
483 0xC000..=0xFFFF => {
484 let last = (self.prg_rom.len() / PRG_BANK_16K).max(1) - 1;
485 self.prg_rom[last * PRG_BANK_16K + (addr as usize & 0x3FFF)]
486 }
487 _ => 0,
488 }
489 }
490
491 fn cpu_write(&mut self, addr: u16, value: u8) {
492 if (0x8000..=0xFFFF).contains(&addr) {
493 self.chr_bank = value & 0x03;
494 self.prg_bank = (value >> 2) & 0x07;
495 }
496 }
497
498 fn ppu_read(&mut self, addr: u16) -> u8 {
499 let addr = addr & 0x3FFF;
500 match addr {
501 0x0000..=0x1FFF => self.chr_ram[self.chr_offset(addr)],
502 0x2000..=0x3EFF => self.vram[nametable_offset(addr, self.mirroring)],
503 _ => 0,
504 }
505 }
506
507 fn ppu_write(&mut self, addr: u16, value: u8) {
508 let addr = addr & 0x3FFF;
509 match addr {
510 0x0000..=0x1FFF => {
511 let off = self.chr_offset(addr);
512 self.chr_ram[off] = value;
513 }
514 0x2000..=0x3EFF => {
515 let off = nametable_offset(addr, self.mirroring);
516 self.vram[off] = value;
517 }
518 _ => {}
519 }
520 }
521
522 fn current_mirroring(&self) -> Mirroring {
523 self.mirroring
524 }
525
526 fn save_state(&self) -> Vec<u8> {
527 let mut out = Vec::with_capacity(3 + self.vram.len() + self.chr_ram.len());
528 out.push(SAVE_STATE_VERSION);
529 out.push(self.prg_bank);
530 out.push(self.chr_bank);
531 out.extend_from_slice(&self.vram);
532 out.extend_from_slice(&self.chr_ram);
533 out
534 }
535
536 fn load_state(&mut self, data: &[u8]) -> Result<(), MapperError> {
537 let expected = 3 + self.vram.len() + self.chr_ram.len();
538 if data.len() != expected {
539 return Err(MapperError::WrongLength {
540 expected,
541 got: data.len(),
542 });
543 }
544 if data[0] != SAVE_STATE_VERSION {
545 return Err(MapperError::UnsupportedVersion(data[0]));
546 }
547 self.prg_bank = data[1];
548 self.chr_bank = data[2];
549 let mut cursor = 3;
550 self.vram
551 .copy_from_slice(&data[cursor..cursor + self.vram.len()]);
552 cursor += self.vram.len();
553 self.chr_ram
554 .copy_from_slice(&data[cursor..cursor + self.chr_ram.len()]);
555 Ok(())
556 }
557}
558
559pub struct Gtrom111 {
584 flash: Box<[u8]>,
586 original: Box<[u8]>,
588 chip: Sst39sf040,
589 chr_ram: Box<[u8]>,
591 nt_ram: Box<[u8]>,
593 reg: u8,
595 prg_bank: u8,
596 chr_bank: u8,
597 nt_bank: u8,
598}
599
600const GTROM_STATE_VERSION: u8 = 2;
603
604impl Gtrom111 {
605 pub fn new(prg_rom: Box<[u8]>, _chr_rom: &[u8]) -> Result<Self, MapperError> {
612 if prg_rom.is_empty() || !prg_rom.len().is_multiple_of(PRG_BANK_32K) {
613 return Err(MapperError::Invalid(format!(
614 "mapper 111 PRG-ROM size {} is not a non-zero multiple of 32 KiB",
615 prg_rom.len()
616 )));
617 }
618 Ok(Self {
619 original: prg_rom.clone(),
620 flash: prg_rom,
621 chip: Sst39sf040::new(),
622 chr_ram: vec![0u8; 2 * CHR_BANK_8K].into_boxed_slice(),
623 nt_ram: vec![0u8; 2 * CHR_BANK_8K].into_boxed_slice(),
624 reg: 0,
625 prg_bank: 0,
626 chr_bank: 0,
627 nt_bank: 0,
628 })
629 }
630
631 #[allow(clippy::cast_possible_truncation)]
632 fn update_register(&mut self, value: u8) {
633 let count = (self.flash.len() / PRG_BANK_32K).max(1);
634 self.reg = value;
635 self.prg_bank = ((value & 0x0F) as usize % count) as u8;
637 self.chr_bank = (value >> 4) & 0x01;
638 self.nt_bank = (value >> 5) & 0x01;
639 }
640
641 const fn is_register(addr: u16) -> bool {
643 matches!(addr, 0x5000..=0x5FFF | 0x7000..=0x7FFF)
644 }
645
646 const fn chr_offset(&self, addr: u16) -> usize {
647 (self.chr_bank as usize) * CHR_BANK_8K + (addr as usize & 0x1FFF)
648 }
649
650 const fn nt_offset(&self, addr: u16) -> usize {
652 (self.nt_bank as usize) * CHR_BANK_8K + (addr as usize & 0x1FFF)
653 }
654
655 fn chip_addr(&self, addr: u16) -> usize {
656 (self.prg_bank as usize) * PRG_BANK_32K + (addr as usize & 0x7FFF)
657 }
658}
659
660impl Mapper for Gtrom111 {
661 fn caps(&self) -> MapperCaps {
662 MapperCaps::NONE
663 }
664
665 fn save_data(&self) -> &[u8] {
668 &self.flash
669 }
670
671 fn save_data_mut(&mut self) -> &mut [u8] {
672 &mut self.flash
673 }
674
675 fn clear_save_data(&mut self) {
676 self.flash.copy_from_slice(&self.original);
677 }
678
679 fn cpu_read_unmapped(&self, addr: u16) -> bool {
681 addr < 0x8000
682 }
683
684 fn notify_floating_read(&mut self, addr: u16, value: u8) {
685 if Self::is_register(addr) {
686 self.update_register(value);
687 }
688 }
689
690 fn cpu_read(&mut self, addr: u16) -> u8 {
691 if addr >= 0x8000 {
692 let a = self.chip_addr(addr);
693 self.chip.id_read(a).unwrap_or(self.flash[a])
694 } else {
695 0
696 }
697 }
698
699 fn cpu_write(&mut self, addr: u16, value: u8) {
700 if Self::is_register(addr) {
701 self.update_register(value);
702 } else if addr >= 0x8000 {
703 let a = self.chip_addr(addr);
704 self.chip.write(&mut self.flash, a, value);
705 }
706 }
707
708 fn ppu_read(&mut self, addr: u16) -> u8 {
709 let addr = addr & 0x3FFF;
710 match addr {
711 0x0000..=0x1FFF => self.chr_ram[self.chr_offset(addr)],
712 0x2000..=0x3EFF => self.nt_ram[self.nt_offset(addr)],
713 _ => 0,
714 }
715 }
716
717 fn ppu_write(&mut self, addr: u16, value: u8) {
718 let addr = addr & 0x3FFF;
719 match addr {
720 0x0000..=0x1FFF => {
721 let off = self.chr_offset(addr);
722 self.chr_ram[off] = value;
723 }
724 0x2000..=0x3EFF => {
725 let off = self.nt_offset(addr);
726 self.nt_ram[off] = value;
727 }
728 _ => {}
729 }
730 }
731
732 fn nametable_unfolded(&self) -> bool {
733 true
734 }
735
736 fn nametable_fetch(&mut self, addr: u16) -> Option<u8> {
737 Some(self.nt_ram[self.nt_offset(addr)])
738 }
739
740 fn nametable_write(&mut self, addr: u16, value: u8) -> bool {
741 let off = self.nt_offset(addr);
742 self.nt_ram[off] = value;
743 true
744 }
745
746 fn current_mirroring(&self) -> Mirroring {
747 Mirroring::FourScreen
748 }
749
750 fn debug_info(&self) -> crate::mapper::MapperDebugInfo {
751 let mut info = crate::mapper::MapperDebugInfo {
752 mapper_id: 111,
753 name: "GTROM (111)".into(),
754 mirroring: crate::mapper::mirroring_name(Mirroring::FourScreen),
755 ..Default::default()
756 };
757 info.prg_banks
758 .push(("32K".into(), format!("{:#04x}", self.prg_bank)));
759 info.chr_banks
760 .push(("8K".into(), format!("{}", self.chr_bank)));
761 info.extra
762 .push(("nt page".into(), format!("{}", self.nt_bank)));
763 info.extra.push((
764 "LEDs".into(),
765 format!(
766 "red {} green {}",
767 if self.reg & 0x40 == 0 { "on" } else { "off" },
768 if self.reg & 0x80 == 0 { "on" } else { "off" }
769 ),
770 ));
771 info
772 }
773
774 fn save_state(&self) -> Vec<u8> {
775 let mut out = Vec::with_capacity(8 + self.chr_ram.len() + self.nt_ram.len());
776 out.push(GTROM_STATE_VERSION);
777 out.push(self.prg_bank);
778 out.push(self.chr_bank);
779 out.push(self.nt_bank);
780 out.push(self.reg);
781 out.extend_from_slice(&self.chip.to_bytes());
782 out.extend_from_slice(&self.chr_ram);
783 out.extend_from_slice(&self.nt_ram);
784 encode_sector_diff(&self.flash, &self.original, &mut out);
785 out
786 }
787
788 fn load_state(&mut self, data: &[u8]) -> Result<(), MapperError> {
789 let fixed = 7 + self.chr_ram.len() + self.nt_ram.len();
790 if data.len() < fixed {
791 return Err(MapperError::WrongLength {
792 expected: fixed,
793 got: data.len(),
794 });
795 }
796 if data[0] != GTROM_STATE_VERSION {
797 return Err(MapperError::UnsupportedVersion(data[0]));
798 }
799 let prg_banks = self.flash.len() / PRG_BANK_32K;
807 let (prg_bank, chr_bank, nt_bank) = (data[1], data[2], data[3]);
808 if usize::from(prg_bank) >= prg_banks || chr_bank > 1 || nt_bank > 1 {
809 return Err(MapperError::Invalid(format!(
810 "mapper 111 state banks PRG {prg_bank} / CHR {chr_bank} / NT {nt_bank} \
811 exceed the board ({prg_banks} PRG banks, 2 CHR, 2 NT)"
812 )));
813 }
814 let chip = Sst39sf040::from_bytes([data[5], data[6]]).ok_or_else(|| {
815 MapperError::Invalid(format!(
816 "mapper 111 flash state {:#04x} {:#04x} is not one the chip produces",
817 data[5], data[6]
818 ))
819 })?;
820 let expected = flash_state_len(111, fixed, self.flash.len(), data)?;
821 if data.len() != expected {
822 return Err(MapperError::WrongLength {
823 expected,
824 got: data.len(),
825 });
826 }
827 decode_sector_diff(&mut self.flash, &self.original, &data[fixed..])
833 .ok_or_else(|| MapperError::Invalid("mapper 111 flash diff".into()))?;
834 self.prg_bank = prg_bank;
835 self.chr_bank = chr_bank;
836 self.nt_bank = nt_bank;
837 self.reg = data[4];
838 self.chip = chip;
839 let mut cursor = 7;
840 self.chr_ram
841 .copy_from_slice(&data[cursor..cursor + self.chr_ram.len()]);
842 cursor += self.chr_ram.len();
843 self.nt_ram
844 .copy_from_slice(&data[cursor..cursor + self.nt_ram.len()]);
845 Ok(())
846 }
847}
848
849pub struct Action53M28 {
883 prg_rom: Box<[u8]>,
884 chr_ram: Box<[u8]>,
885 vram: Box<[u8]>,
886 reg_select: u8,
887 chr_reg: u8,
888 inner_prg: u8,
889 mode: u8,
890 outer_prg: u8,
891}
892
893const M28_STATE_VERSION: u8 = 2;
896const M28_CHR_RAM: usize = 4 * CHR_BANK_8K;
898
899impl Action53M28 {
900 pub fn new(
907 prg_rom: Box<[u8]>,
908 _chr_rom: &[u8],
909 _mirroring: Mirroring,
910 ) -> Result<Self, MapperError> {
911 if prg_rom.is_empty() || !prg_rom.len().is_multiple_of(PRG_BANK_16K) {
912 return Err(MapperError::Invalid(format!(
913 "mapper 28 PRG-ROM size {} is not a non-zero multiple of 16 KiB",
914 prg_rom.len()
915 )));
916 }
917 Ok(Self {
918 prg_rom,
919 chr_ram: vec![0u8; M28_CHR_RAM].into_boxed_slice(),
920 vram: vec![0u8; 2 * NAMETABLE_SIZE].into_boxed_slice(),
921 reg_select: 0,
922 chr_reg: 0,
923 inner_prg: 0,
924 mode: 0,
930 outer_prg: 0xFF,
931 })
932 }
933
934 fn prg_bank_for(&self, addr: u16) -> usize {
942 let count16 = (self.prg_rom.len() / PRG_BANK_16K).max(1);
943 let size = u32::from((self.mode >> 4) & 0x03);
944 let prg_mode = (self.mode >> 2) & 0x03;
945 let high = addr >= 0xC000;
946 let half = usize::from(high);
947 let inner = usize::from(self.inner_prg & 0x0F);
948 let outer_bits = (usize::from(self.outer_prg) << 1) | half;
949 let mask = (1usize << (size + 1)) - 1;
951 let bank = match prg_mode {
952 0 | 1 => (outer_bits & !mask) | (((inner << 1) | half) & mask),
954 2 | 3 => {
957 let fixed = if prg_mode == 2 { !high } else { high };
958 if fixed {
959 outer_bits
960 } else {
961 (outer_bits & !mask) | (inner & mask)
962 }
963 }
964 _ => unreachable!("PRG mode is two bits"),
965 };
966 bank % count16
967 }
968
969 fn chr_offset(&self, addr: u16) -> usize {
971 usize::from(self.chr_reg & 0x03) * CHR_BANK_8K + usize::from(addr & 0x1FFF)
972 }
973
974 const fn latch_one_screen(&mut self, value: u8) {
976 if self.mode & 0x02 == 0 {
977 self.mode = (self.mode & !0x01) | ((value >> 4) & 0x01);
978 }
979 }
980}
981
982impl Mapper for Action53M28 {
983 fn caps(&self) -> MapperCaps {
984 MapperCaps::NONE
985 }
986
987 fn cpu_read(&mut self, addr: u16) -> u8 {
988 if (0x8000..=0xFFFF).contains(&addr) {
989 let bank = self.prg_bank_for(addr);
990 self.prg_rom[bank * PRG_BANK_16K + (addr as usize & 0x3FFF)]
991 } else {
992 0
993 }
994 }
995
996 fn cpu_write(&mut self, addr: u16, value: u8) {
997 match addr {
998 0x5000..=0x5FFF => self.reg_select = value & 0x81,
999 0x8000..=0xFFFF => match self.reg_select {
1000 0x00 => {
1001 self.chr_reg = value & 0x03;
1002 self.latch_one_screen(value);
1003 }
1004 0x01 => {
1005 self.inner_prg = value & 0x0F;
1006 self.latch_one_screen(value);
1007 }
1008 0x80 => self.mode = value & 0x3F,
1009 _ => self.outer_prg = value,
1010 },
1011 _ => {}
1012 }
1013 }
1014
1015 fn ppu_read(&mut self, addr: u16) -> u8 {
1016 let addr = addr & 0x3FFF;
1017 match addr {
1018 0x0000..=0x1FFF => self.chr_ram[self.chr_offset(addr)],
1019 0x2000..=0x3EFF => self.vram[nametable_offset(addr, self.current_mirroring())],
1020 _ => 0,
1021 }
1022 }
1023
1024 fn ppu_write(&mut self, addr: u16, value: u8) {
1025 let addr = addr & 0x3FFF;
1026 match addr {
1027 0x0000..=0x1FFF => {
1028 let off = self.chr_offset(addr);
1029 self.chr_ram[off] = value;
1030 }
1031 0x2000..=0x3EFF => {
1032 let off = nametable_offset(addr, self.current_mirroring());
1033 self.vram[off] = value;
1034 }
1035 _ => {}
1036 }
1037 }
1038
1039 fn current_mirroring(&self) -> Mirroring {
1040 match self.mode & 0x03 {
1041 0 => Mirroring::SingleScreenA,
1042 1 => Mirroring::SingleScreenB,
1043 2 => Mirroring::Vertical,
1044 _ => Mirroring::Horizontal,
1045 }
1046 }
1047
1048 fn save_state(&self) -> Vec<u8> {
1049 let mut out = Vec::with_capacity(6 + self.vram.len() + self.chr_ram.len());
1050 out.push(M28_STATE_VERSION);
1051 out.push(self.reg_select);
1052 out.push(self.chr_reg);
1053 out.push(self.inner_prg);
1054 out.push(self.mode);
1055 out.push(self.outer_prg);
1056 out.extend_from_slice(&self.vram);
1057 out.extend_from_slice(&self.chr_ram);
1058 out
1059 }
1060
1061 fn load_state(&mut self, data: &[u8]) -> Result<(), MapperError> {
1062 let version = *data.first().ok_or(MapperError::WrongLength {
1066 expected: 1,
1067 got: 0,
1068 })?;
1069 let chr_len = match version {
1070 M28_STATE_VERSION => self.chr_ram.len(),
1071 1 => CHR_BANK_8K,
1072 v => return Err(MapperError::UnsupportedVersion(v)),
1073 };
1074 let expected = 6 + self.vram.len() + chr_len;
1075 if data.len() != expected {
1076 return Err(MapperError::WrongLength {
1077 expected,
1078 got: data.len(),
1079 });
1080 }
1081 self.reg_select = data[1] & 0x81;
1082 self.chr_reg = data[2] & 0x03;
1083 self.inner_prg = data[3] & 0x0F;
1084 self.mode = data[4] & 0x3F;
1085 self.outer_prg = data[5];
1086 let mut cursor = 6;
1087 self.vram
1088 .copy_from_slice(&data[cursor..cursor + self.vram.len()]);
1089 cursor += self.vram.len();
1090 self.chr_ram.fill(0);
1091 self.chr_ram[..chr_len].copy_from_slice(&data[cursor..cursor + chr_len]);
1092 Ok(())
1093 }
1094}
1095
1096#[derive(Clone, Copy, PartialEq, Eq)]
1146enum M30Nametable {
1147 Horizontal,
1149 Vertical,
1151 SwitchableHv,
1154 OneScreen,
1156 FourScreen,
1162}
1163
1164const M30_STATE_VERSION: u8 = 2;
1167
1168#[allow(clippy::struct_excessive_bools)]
1174pub struct Unrom512M30 {
1175 prg_rom: Box<[u8]>,
1177 original: Box<[u8]>,
1179 chip: Sst39sf040,
1180 chr: Box<[u8]>,
1182 chr_is_rom: bool,
1184 vram: Box<[u8]>,
1185 prg_bank: u8,
1186 chr_bank: u8,
1187 nt_bit: bool,
1190 nametable: M30Nametable,
1191 bus_conflicts: bool,
1193 flash_window: bool,
1196}
1197
1198impl Unrom512M30 {
1199 pub fn new(
1211 prg_rom: Box<[u8]>,
1212 chr_rom: &[u8],
1213 four_screen: bool,
1214 vertical: bool,
1215 submapper: u8,
1216 has_battery: bool,
1217 ) -> Result<Self, MapperError> {
1218 if prg_rom.is_empty() || !prg_rom.len().is_multiple_of(PRG_BANK_16K) {
1219 return Err(MapperError::Invalid(format!(
1220 "mapper 30 PRG-ROM size {} is not a non-zero multiple of 16 KiB",
1221 prg_rom.len()
1222 )));
1223 }
1224
1225 let nametable = if submapper == 3 {
1229 M30Nametable::SwitchableHv
1230 } else if four_screen && vertical {
1231 M30Nametable::FourScreen
1232 } else if four_screen {
1233 M30Nametable::OneScreen
1234 } else if vertical {
1235 M30Nametable::Vertical
1236 } else {
1237 M30Nametable::Horizontal
1238 };
1239
1240 let bus_conflicts = (submapper == 0 && !has_battery) || submapper == 2;
1242 let flash_window = (submapper == 0 && has_battery) || matches!(submapper, 1 | 3 | 4);
1243
1244 let (chr, chr_is_rom) = if chr_rom.is_empty() {
1247 (vec![0u8; 4 * CHR_BANK_8K].into_boxed_slice(), false)
1248 } else {
1249 (chr_rom.to_vec().into_boxed_slice(), true)
1250 };
1251
1252 let nt_bit = nametable == M30Nametable::SwitchableHv;
1258
1259 Ok(Self {
1260 original: if flash_window && !chr_is_rom {
1261 prg_rom.clone()
1262 } else {
1263 Box::new([])
1264 },
1265 prg_rom,
1266 chip: Sst39sf040::new(),
1267 chr,
1268 chr_is_rom,
1269 vram: vec![0u8; 2 * NAMETABLE_SIZE].into_boxed_slice(),
1270 prg_bank: 0,
1271 chr_bank: 0,
1272 nt_bit,
1273 nametable,
1274 bus_conflicts,
1275 flash_window,
1276 })
1277 }
1278
1279 fn read_prg(&self, bank: usize, addr: u16) -> u8 {
1280 let a = self.prg_chip_addr(bank, addr);
1281 self.chip.id_read(a).unwrap_or(self.prg_rom[a])
1282 }
1283
1284 const fn flashable(&self) -> bool {
1291 self.flash_window && !self.chr_is_rom
1292 }
1293
1294 fn prg_chip_addr(&self, bank: usize, addr: u16) -> usize {
1295 let count = (self.prg_rom.len() / PRG_BANK_16K).max(1);
1296 (bank % count) * PRG_BANK_16K + (addr as usize & 0x3FFF)
1297 }
1298
1299 fn four_screen_offset(&self, addr: u16) -> Option<usize> {
1302 (self.nametable == M30Nametable::FourScreen
1303 && !self.chr_is_rom
1304 && self.chr.len() == 4 * CHR_BANK_8K)
1305 .then(|| 3 * CHR_BANK_8K + (addr as usize & 0x1FFF))
1306 }
1307
1308 fn chr_offset(&self, addr: u16) -> usize {
1309 let count = (self.chr.len() / CHR_BANK_8K).max(1);
1310 let bank = (self.chr_bank as usize) % count;
1311 bank * CHR_BANK_8K + (addr as usize & 0x1FFF)
1312 }
1313
1314 fn write_latch(&mut self, addr: u16, value: u8) {
1316 let effective = if self.bus_conflicts {
1317 let conflict_bank = if addr >= 0xC000 {
1323 (self.prg_rom.len() / PRG_BANK_16K).max(1) - 1
1324 } else {
1325 self.prg_bank as usize
1326 };
1327 value & self.read_prg(conflict_bank, addr)
1328 } else {
1329 value
1330 };
1331 self.prg_bank = effective & 0x1F;
1332 self.chr_bank = (effective >> 5) & 0x03;
1333 self.nt_bit = (effective & 0x80) != 0;
1334 }
1335}
1336
1337impl Mapper for Unrom512M30 {
1338 fn caps(&self) -> MapperCaps {
1339 MapperCaps::NONE
1340 }
1341
1342 fn cpu_read(&mut self, addr: u16) -> u8 {
1343 match addr {
1344 0x8000..=0xBFFF => self.read_prg(self.prg_bank as usize, addr),
1345 0xC000..=0xFFFF => {
1346 let last = (self.prg_rom.len() / PRG_BANK_16K).max(1) - 1;
1347 self.read_prg(last, addr)
1348 }
1349 _ => 0,
1350 }
1351 }
1352
1353 fn save_data(&self) -> &[u8] {
1356 if self.flashable() { &self.prg_rom } else { &[] }
1357 }
1358
1359 fn save_data_mut(&mut self) -> &mut [u8] {
1360 if self.flashable() {
1361 &mut self.prg_rom
1362 } else {
1363 &mut []
1364 }
1365 }
1366
1367 fn clear_save_data(&mut self) {
1368 if self.flashable() {
1369 self.prg_rom.copy_from_slice(&self.original);
1370 }
1371 }
1372
1373 fn cpu_read_unmapped(&self, addr: u16) -> bool {
1375 addr < 0x8000
1376 }
1377
1378 fn nametable_unfolded(&self) -> bool {
1379 self.four_screen_offset(0x2000).is_some()
1380 }
1381
1382 fn nametable_fetch(&mut self, addr: u16) -> Option<u8> {
1383 self.four_screen_offset(addr).map(|off| self.chr[off])
1384 }
1385
1386 fn nametable_write(&mut self, addr: u16, value: u8) -> bool {
1387 match self.four_screen_offset(addr) {
1388 Some(off) => {
1389 self.chr[off] = value;
1390 true
1391 }
1392 None => false,
1393 }
1394 }
1395
1396 fn cpu_write(&mut self, addr: u16, value: u8) {
1397 if !(0x8000..=0xFFFF).contains(&addr) {
1398 return;
1399 }
1400 if self.flash_window {
1401 if addr >= 0xC000 {
1404 self.write_latch(addr, value);
1405 } else if self.flashable() {
1406 let a = self.prg_chip_addr(self.prg_bank as usize, addr);
1407 self.chip.write(&mut self.prg_rom, a, value);
1408 }
1409 } else {
1410 self.write_latch(addr, value);
1413 }
1414 }
1415
1416 fn ppu_read(&mut self, addr: u16) -> u8 {
1417 let addr = addr & 0x3FFF;
1418 match addr {
1419 0x0000..=0x1FFF => self.chr[self.chr_offset(addr)],
1420 0x2000..=0x3EFF => self.vram[nametable_offset(addr, self.current_mirroring())],
1421 _ => 0,
1422 }
1423 }
1424
1425 fn ppu_write(&mut self, addr: u16, value: u8) {
1426 let addr = addr & 0x3FFF;
1427 match addr {
1428 0x0000..=0x1FFF => {
1429 if !self.chr_is_rom {
1430 let off = self.chr_offset(addr);
1431 self.chr[off] = value;
1432 }
1433 }
1434 0x2000..=0x3EFF => {
1435 let off = nametable_offset(addr, self.current_mirroring());
1436 self.vram[off] = value;
1437 }
1438 _ => {}
1439 }
1440 }
1441
1442 fn current_mirroring(&self) -> Mirroring {
1443 match self.nametable {
1444 M30Nametable::Horizontal => Mirroring::Horizontal,
1445 M30Nametable::Vertical => Mirroring::Vertical,
1446 M30Nametable::SwitchableHv => {
1449 if self.nt_bit {
1450 Mirroring::Vertical
1451 } else {
1452 Mirroring::Horizontal
1453 }
1454 }
1455 M30Nametable::FourScreen if self.four_screen_offset(0x2000).is_some() => {
1458 Mirroring::FourScreen
1459 }
1460 M30Nametable::OneScreen | M30Nametable::FourScreen => {
1465 if self.nt_bit {
1466 Mirroring::SingleScreenB
1467 } else {
1468 Mirroring::SingleScreenA
1469 }
1470 }
1471 }
1472 }
1473
1474 fn save_state(&self) -> Vec<u8> {
1475 let chr_len = if self.chr_is_rom { 0 } else { self.chr.len() };
1476 let mut out = Vec::with_capacity(6 + self.vram.len() + chr_len);
1477 out.push(M30_STATE_VERSION);
1478 out.push(self.prg_bank);
1479 out.push(self.chr_bank);
1480 out.push(u8::from(self.nt_bit));
1481 out.extend_from_slice(&self.vram);
1482 if !self.chr_is_rom {
1484 out.extend_from_slice(&self.chr);
1485 }
1486 out.extend_from_slice(&self.chip.to_bytes());
1487 if self.flashable() {
1488 encode_sector_diff(&self.prg_rom, &self.original, &mut out);
1489 }
1490 out
1491 }
1492
1493 fn load_state(&mut self, data: &[u8]) -> Result<(), MapperError> {
1494 let chr_len = if self.chr_is_rom { 0 } else { self.chr.len() };
1495 let fixed = 6 + self.vram.len() + chr_len;
1496 if data.len() < fixed {
1497 return Err(MapperError::WrongLength {
1498 expected: fixed,
1499 got: data.len(),
1500 });
1501 }
1502 if data[0] != M30_STATE_VERSION {
1503 return Err(MapperError::UnsupportedVersion(data[0]));
1504 }
1505 let (s0, s1) = (data[fixed - 2], data[fixed - 1]);
1508 let chip = Sst39sf040::from_bytes([s0, s1]).ok_or_else(|| {
1509 MapperError::Invalid(format!(
1510 "mapper 30 flash state {s0:#04x} {s1:#04x} is not one the chip produces"
1511 ))
1512 })?;
1513 let expected = if self.flashable() {
1514 flash_state_len(30, fixed, self.prg_rom.len(), data)?
1515 } else {
1516 fixed
1517 };
1518 if data.len() != expected {
1519 return Err(MapperError::WrongLength {
1520 expected,
1521 got: data.len(),
1522 });
1523 }
1524 if self.flashable() {
1525 decode_sector_diff(&mut self.prg_rom, &self.original, &data[fixed..])
1528 .ok_or_else(|| MapperError::Invalid("mapper 30 flash diff".into()))?;
1529 }
1530 self.chip = chip;
1531 self.prg_bank = data[1] & 0x1F;
1537 self.chr_bank = data[2] & 0x03;
1538 self.nt_bit = data[3] != 0;
1539 let mut cursor = 4;
1540 self.vram
1541 .copy_from_slice(&data[cursor..cursor + self.vram.len()]);
1542 cursor += self.vram.len();
1543 if !self.chr_is_rom {
1544 self.chr
1545 .copy_from_slice(&data[cursor..cursor + self.chr.len()]);
1546 }
1547 Ok(())
1548 }
1549}
1550
1551#[cfg(test)]
1552#[allow(clippy::cast_possible_truncation)]
1553mod tests {
1554 use super::*;
1555
1556 fn synth_prg_32k(banks: usize) -> Box<[u8]> {
1557 let mut v = vec![0xFFu8; banks * PRG_BANK_32K];
1558 for b in 0..banks {
1559 v[b * PRG_BANK_32K] = b as u8;
1560 }
1561 v.into_boxed_slice()
1562 }
1563
1564 fn synth_prg_16k(banks: usize) -> Box<[u8]> {
1565 let mut v = vec![0xFFu8; banks * PRG_BANK_16K];
1566 for b in 0..banks {
1567 v[b * PRG_BANK_16K] = b as u8;
1568 }
1569 v.into_boxed_slice()
1570 }
1571
1572 fn synth_prg_4k(banks: usize) -> Box<[u8]> {
1573 let mut v = vec![0xFFu8; banks * PRG_BANK_4K];
1574 for b in 0..banks {
1575 v[b * PRG_BANK_4K] = b as u8;
1576 }
1577 v.into_boxed_slice()
1578 }
1579
1580 #[test]
1581 fn m31_slots_latch_per_window() {
1582 let mut m = Inl31::new(synth_prg_4k(8), &[], Mirroring::Vertical).unwrap();
1583 m.cpu_write(0x5FF8, 3);
1585 m.cpu_write(0x5FFF, 5);
1586 assert_eq!(m.cpu_read(0x8000), 3);
1587 assert_eq!(m.cpu_read(0xF000), 5);
1588 assert_eq!(m.cpu_read(0x9000), 0);
1590 }
1591
1592 #[test]
1593 fn m31_save_state_round_trip() {
1594 let mut m = Inl31::new(synth_prg_4k(8), &[], Mirroring::Vertical).unwrap();
1595 m.cpu_write(0x5FF8, 2);
1596 m.ppu_write(0x0001, 0xCD);
1597 let blob = m.save_state();
1598 let mut m2 = Inl31::new(synth_prg_4k(8), &[], Mirroring::Vertical).unwrap();
1599 m2.load_state(&blob).unwrap();
1600 assert_eq!(m2.cpu_read(0x8000), 2);
1601 assert_eq!(m2.ppu_read(0x0001), 0xCD);
1602 }
1603
1604 #[test]
1605 fn m218_ciram_serves_chr_and_nametable() {
1606 let mut m = MagicFloor218::new(synth_prg_32k(1), &[], Mirroring::Vertical).unwrap();
1607 m.ppu_write(0x0000, 0x11);
1610 m.ppu_write(0x2400, 0x22);
1611 assert_eq!(m.ppu_read(0x0400), 0x22);
1613 assert_eq!(m.ppu_read(0x2000), 0x11);
1615 assert_eq!(m.current_mirroring(), Mirroring::Vertical);
1616 }
1617
1618 #[test]
1619 fn m218_single_screen_a_wires_ciram_a10_to_ppu_a12() {
1620 let mut m = MagicFloor218::new(synth_prg_32k(1), &[], Mirroring::SingleScreenA).unwrap();
1625 m.ppu_write(0x0000, 0x11);
1626 m.ppu_write(0x1000, 0x22);
1627 assert_eq!(m.ppu_read(0x0400), 0x11);
1629 assert_eq!(m.ppu_read(0x0C00), 0x11);
1630 assert_eq!(m.ppu_read(0x1400), 0x22);
1631 assert_eq!(m.ppu_read(0x2000), 0x11);
1633 assert_eq!(m.ppu_read(0x2C00), 0x11);
1634 assert_eq!(m.nametable_fetch(0x2400), Some(0x11));
1635 }
1636
1637 #[test]
1638 fn m218_single_screen_b_wires_ciram_a10_to_ppu_a13() {
1639 let mut m = MagicFloor218::new(synth_prg_32k(1), &[], Mirroring::SingleScreenB).unwrap();
1644 m.ppu_write(0x0000, 0x11);
1645 m.ppu_write(0x2000, 0x33);
1646 assert_eq!(m.ppu_read(0x0000), 0x11);
1647 assert_eq!(m.ppu_read(0x1000), 0x11);
1648 assert_eq!(m.ppu_read(0x1C00), 0x11);
1649 assert_eq!(m.ppu_read(0x2C00), 0x33);
1650 assert_eq!(m.nametable_fetch(0x2400), Some(0x33));
1651 }
1652
1653 #[test]
1654 fn m218_accepts_16k_prg_and_mirrors_it() {
1655 let mut prg = synth_prg_16k(1);
1658 prg[0] = 0xAB; let mut m = MagicFloor218::new(prg, &[], Mirroring::Horizontal).unwrap();
1660 assert_eq!(m.cpu_read(0x8000), 0xAB);
1662 assert_eq!(m.cpu_read(0xC000), 0xAB);
1663 }
1664
1665 #[test]
1666 fn m218_save_state_round_trip() {
1667 let mut m = MagicFloor218::new(synth_prg_32k(1), &[], Mirroring::Horizontal).unwrap();
1668 m.ppu_write(0x0005, 0x42);
1669 let blob = m.save_state();
1670 let mut m2 = MagicFloor218::new(synth_prg_32k(1), &[], Mirroring::Horizontal).unwrap();
1671 m2.load_state(&blob).unwrap();
1672 assert_eq!(m2.ppu_read(0x0005), 0x42);
1673 }
1674
1675 #[test]
1676 fn m29_latch_selects_prg_and_chr_bank() {
1677 let mut m = Cufrom29::new(synth_prg_16k(8), &[], Mirroring::Vertical).unwrap();
1678 m.cpu_write(0x8000, 0b0001_0110);
1681 assert_eq!(m.cpu_read(0x8000), 5);
1682 assert_eq!(m.cpu_read(0xC000), 7);
1684 m.ppu_write(0x0003, 0x77);
1686 assert_eq!(m.ppu_read(0x0003), 0x77);
1687 }
1688
1689 #[test]
1690 fn m29_save_state_round_trip() {
1691 let mut m = Cufrom29::new(synth_prg_16k(8), &[], Mirroring::Vertical).unwrap();
1692 m.cpu_write(0x8000, 0b0000_1101); m.ppu_write(0x0007, 0x55);
1694 let blob = m.save_state();
1695 let mut m2 = Cufrom29::new(synth_prg_16k(8), &[], Mirroring::Vertical).unwrap();
1696 m2.load_state(&blob).unwrap();
1697 assert_eq!(m2.cpu_read(0x8000), 3);
1698 assert_eq!(m2.ppu_read(0x0007), 0x55);
1699 }
1700
1701 #[test]
1702 fn m111_register_selects_prg_chr_nt() {
1703 let mut m = Gtrom111::new(synth_prg_32k(8), &[]).unwrap();
1704 m.cpu_write(0x5000, 0b0011_0101);
1706 assert_eq!(m.cpu_read(0x8000), 5);
1707 m.ppu_write(0x0000, 0x88);
1709 assert_eq!(m.ppu_read(0x0000), 0x88);
1710 assert!(m.nametable_write(0x2000, 0x99));
1712 assert_eq!(m.nametable_fetch(0x2000), Some(0x99));
1713 assert_eq!(m.current_mirroring(), Mirroring::FourScreen);
1714 m.cpu_write(0x5000, 0x00);
1716 assert_eq!(m.nametable_fetch(0x2000), Some(0x00));
1717 }
1718
1719 #[test]
1720 fn m111_save_state_round_trip() {
1721 let mut m = Gtrom111::new(synth_prg_32k(8), &[]).unwrap();
1722 m.cpu_write(0x5000, 0b0011_0011); m.ppu_write(0x0001, 0xAA);
1724 m.nametable_write(0x2001, 0xBB);
1725 let blob = m.save_state();
1726 let mut m2 = Gtrom111::new(synth_prg_32k(8), &[]).unwrap();
1727 m2.load_state(&blob).unwrap();
1728 assert_eq!(m2.cpu_read(0x8000), 3);
1729 assert_eq!(m2.ppu_read(0x0001), 0xAA);
1730 assert_eq!(m2.nametable_fetch(0x2001), Some(0xBB));
1731 }
1732
1733 #[test]
1736 fn m111_register_window_is_5000_and_7000_only() {
1737 let mut m = Gtrom111::new(synth_prg_32k(8), &[]).unwrap();
1738 m.cpu_write(0x6000, 0x03);
1739 assert_eq!(m.cpu_read(0x8000), 0, "$6000 is not decoded");
1740 m.cpu_write(0x4FFF, 0x03);
1741 assert_eq!(m.cpu_read(0x8000), 0, "$4FFF is not decoded");
1742 m.cpu_write(0x7ABC, 0x03);
1743 assert_eq!(m.cpu_read(0x8000), 3);
1744 m.cpu_write(0x5FFF, 0x04);
1745 assert_eq!(m.cpu_read(0x8000), 4);
1746 assert!(m.cpu_read_unmapped(0x5000), "the register is write-only");
1747 }
1748
1749 #[test]
1751 fn m111_a_read_latches_the_floating_value() {
1752 let mut m = Gtrom111::new(synth_prg_32k(8), &[]).unwrap();
1753 m.notify_floating_read(0x5000, 0x26);
1754 assert_eq!(m.cpu_read(0x8000), 6);
1755 m.notify_floating_read(0x6000, 0x01);
1756 assert_eq!(m.cpu_read(0x8000), 6, "outside the window: no latch");
1757 }
1758
1759 #[test]
1761 fn m111_bonus_ram_at_3000_is_not_a_mirror() {
1762 let mut m = Gtrom111::new(synth_prg_32k(8), &[]).unwrap();
1763 assert!(m.nametable_unfolded());
1764 m.nametable_write(0x2123, 0x11);
1765 m.nametable_write(0x3123, 0x22);
1766 assert_eq!(m.nametable_fetch(0x2123), Some(0x11));
1767 assert_eq!(m.nametable_fetch(0x3123), Some(0x22));
1768 m.cpu_write(0x5000, 0x20); assert_eq!(m.nametable_fetch(0x3123), Some(0x00));
1770 m.nametable_write(0x3EFF, 0x33);
1771 m.cpu_write(0x5000, 0x00);
1772 assert_eq!(m.nametable_fetch(0x3123), Some(0x22));
1773 assert_eq!(m.nametable_fetch(0x3EFF), Some(0x00));
1774 }
1775
1776 #[test]
1778 fn m111_flash_program_and_erase_through_the_cpu_window() {
1779 let mut m = Gtrom111::new(synth_prg_32k(16), &[]).unwrap();
1780 m.cpu_write(0x5000, 0x07);
1781 for (a, v) in [
1782 (0xD555u16, 0xAAu8),
1783 (0xAAAA, 0x55),
1784 (0xD555, 0xA0),
1785 (0x9000, 0x3C),
1786 ] {
1787 m.cpu_write(a, v);
1788 }
1789 assert_eq!(m.cpu_read(0x9000), 0x3C, "programmed");
1790 assert_eq!(
1791 m.save_data()[7 * PRG_BANK_32K + 0x1000],
1792 0x3C,
1793 "save_data() is the flash"
1794 );
1795 assert!(m.sram().is_empty(), "GTROM has no RAM at $6000");
1796 m.cpu_write(0x5000, 0x02);
1797 assert_eq!(m.cpu_read(0x9000), 0xFF, "another bank is untouched");
1798 m.cpu_write(0x5000, 0x07);
1799 for (a, v) in [
1800 (0xD555u16, 0xAAu8),
1801 (0xAAAA, 0x55),
1802 (0xD555, 0x80),
1803 (0xD555, 0xAA),
1804 (0xAAAA, 0x55),
1805 (0x9800, 0x30),
1806 ] {
1807 m.cpu_write(a, v);
1808 }
1809 assert_eq!(m.cpu_read(0x9000), 0xFF, "the 4 KiB sector is erased");
1810 assert_eq!(m.cpu_read(0x8000), 0x07, "the neighbouring sector is not");
1811 }
1812
1813 #[test]
1814 fn m111_state_carries_only_flashed_sectors() {
1815 let mut m = Gtrom111::new(synth_prg_32k(16), &[]).unwrap();
1816 let clean = m.save_state().len();
1817 for (a, v) in [
1818 (0xD555u16, 0xAAu8),
1819 (0xAAAA, 0x55),
1820 (0xD555, 0xA0),
1821 (0xC001, 0x00),
1822 ] {
1823 m.cpu_write(a, v);
1824 }
1825 m.nametable_write(0x3456, 0x78);
1826 let blob = m.save_state();
1827 assert_eq!(blob.len(), clean + 0x1000, "one flashed sector");
1828 let mut m2 = Gtrom111::new(synth_prg_32k(16), &[]).unwrap();
1829 m2.load_state(&blob).unwrap();
1830 assert_eq!(m2.cpu_read(0xC001), 0x00);
1831 assert_eq!(m2.nametable_fetch(0x3456), Some(0x78));
1832 assert_eq!(m2.save_state(), blob);
1833 let fresh = Gtrom111::new(synth_prg_32k(16), &[]).unwrap().save_state();
1835 m2.load_state(&fresh).unwrap();
1836 assert_eq!(m2.cpu_read(0xC001), 0xFF);
1837 }
1838
1839 #[test]
1847 fn m111_load_state_rejects_banks_the_board_cannot_hold() {
1848 let mut m = Gtrom111::new(synth_prg_32k(8), &[]).unwrap();
1849 m.cpu_write(0x5000, 0b0011_0011); let good = m.save_state();
1851 for (byte, value) in [(1, 8), (1, 0xFF), (2, 2), (2, 0xFF), (3, 2), (3, 0xFF)] {
1852 let mut bad = good.clone();
1853 bad[byte] = value;
1854 let mut m2 = Gtrom111::new(synth_prg_32k(8), &[]).unwrap();
1855 assert!(
1856 matches!(m2.load_state(&bad), Err(MapperError::Invalid(_))),
1857 "byte {byte} = {value:#04x} must be rejected"
1858 );
1859 }
1860 for (byte, max) in [(1, 7), (2, 1), (3, 1)] {
1862 for value in 0..=max {
1863 let mut ok = good.clone();
1864 ok[byte] = value;
1865 let mut m2 = Gtrom111::new(synth_prg_32k(8), &[]).unwrap();
1866 m2.load_state(&ok).unwrap();
1867 let _ = m2.cpu_read(0xFFFF);
1868 let _ = m2.ppu_read(0x1FFF);
1869 let _ = m2.nametable_fetch(0x2FFF);
1870 }
1871 }
1872 }
1873
1874 const M28_WIKI_TABLE: [(u8, &str, &str); 12] = [
1879 (0x00, "oooooooo0", "oooooooo1"),
1880 (0x08, "oooooooo0", "ooooooooi"),
1881 (0x0C, "ooooooooi", "oooooooo1"),
1882 (0x10, "oooooooi0", "oooooooi1"),
1883 (0x18, "oooooooo0", "oooooooii"),
1884 (0x1C, "oooooooii", "oooooooo1"),
1885 (0x20, "ooooooii0", "ooooooii1"),
1886 (0x28, "oooooooo0", "ooooooiii"),
1887 (0x2C, "ooooooiii", "oooooooo1"),
1888 (0x30, "oooooiii0", "oooooiii1"),
1889 (0x38, "oooooooo0", "oooooiiii"),
1890 (0x3C, "oooooiiii", "oooooooo1"),
1891 ];
1892
1893 fn m28_expected(pattern: &str, outer: u8, inner: u8) -> usize {
1895 let os = pattern.bytes().filter(|&c| c == b'o').count();
1896 let is = pattern.bytes().filter(|&c| c == b'i').count();
1897 let mut o_bits = (0..os).map(|k| (outer >> (7 - k)) & 1);
1899 let mut i_bits = (0..is).rev().map(|k| (inner >> k) & 1);
1900 pattern.bytes().fold(0usize, |acc, c| {
1901 let bit = match c {
1902 b'o' => o_bits.next().unwrap(),
1903 b'i' => i_bits.next().unwrap(),
1904 b'0' => 0,
1905 _ => 1,
1906 };
1907 (acc << 1) | usize::from(bit)
1908 })
1909 }
1910
1911 #[test]
1912 fn m28_prg_banking_matches_every_row_of_the_wiki_table() {
1913 let mut m = Action53M28::new(synth_prg_16k(512), &[], Mirroring::Vertical).unwrap();
1916 for (mode_base, lo, hi) in M28_WIKI_TABLE {
1917 let variants: &[u8] = if mode_base & 0x0C == 0 {
1921 &[0x0, 0x1, 0x2, 0x3, 0x4, 0x5, 0x6, 0x7]
1922 } else {
1923 &[0x0, 0x1, 0x2, 0x3]
1924 };
1925 for &v in variants {
1926 m.mode = mode_base | v;
1927 for outer in 0..=255u8 {
1928 for inner in 0..16u8 {
1929 m.outer_prg = outer;
1930 m.inner_prg = inner;
1931 assert_eq!(
1932 m.prg_bank_for(0x8000),
1933 m28_expected(lo, outer, inner),
1934 "mode ${:02X} outer ${outer:02X} inner {inner} at $8000",
1935 m.mode
1936 );
1937 assert_eq!(
1938 m.prg_bank_for(0xC000),
1939 m28_expected(hi, outer, inner),
1940 "mode ${:02X} outer ${outer:02X} inner {inner} at $C000",
1941 m.mode
1942 );
1943 }
1944 }
1945 }
1946 }
1947 }
1948
1949 #[test]
1950 fn m28_powers_on_with_the_last_bank_at_c000() {
1951 let mut m = Action53M28::new(synth_prg_16k(32), &[], Mirroring::Vertical).unwrap();
1954 assert_eq!(m.cpu_read(0xC000), 31);
1955 }
1956
1957 #[test]
1958 fn m28_chr_register_banks_32k_of_chr_ram() {
1959 let mut m = Action53M28::new(synth_prg_16k(8), &[], Mirroring::Vertical).unwrap();
1960 for bank in 0..4u8 {
1961 m.cpu_write(0x5000, 0x00);
1962 m.cpu_write(0x8000, bank);
1963 m.ppu_write(0x0123, 0xA0 | bank);
1964 }
1965 for bank in 0..4u8 {
1966 m.cpu_write(0x5000, 0x00);
1967 m.cpu_write(0x8000, bank);
1968 assert_eq!(m.ppu_read(0x0123), 0xA0 | bank, "CHR bank {bank}");
1969 }
1970 }
1971
1972 #[test]
1973 fn m28_d4_selects_the_single_screen_only_in_one_screen_modes() {
1974 let mut m = Action53M28::new(synth_prg_16k(8), &[], Mirroring::Vertical).unwrap();
1975 m.cpu_write(0x5000, 0x80);
1976 m.cpu_write(0x8000, 0x00); m.cpu_write(0x5000, 0x01);
1978 m.cpu_write(0x8000, 0x10); assert_eq!(m.current_mirroring(), Mirroring::SingleScreenB);
1980 m.cpu_write(0x5000, 0x00);
1981 m.cpu_write(0x8000, 0x00); assert_eq!(m.current_mirroring(), Mirroring::SingleScreenA);
1983 m.cpu_write(0x5000, 0x80);
1985 m.cpu_write(0x8000, 0x02);
1986 m.cpu_write(0x5000, 0x01);
1987 m.cpu_write(0x8000, 0x10);
1988 assert_eq!(m.current_mirroring(), Mirroring::Vertical);
1989 }
1990
1991 #[test]
1992 fn m28_loads_a_version_1_state_with_8k_of_chr() {
1993 let mut m = Action53M28::new(synth_prg_16k(8), &[], Mirroring::Vertical).unwrap();
1994 let mut v1 = vec![1u8, 0x81, 0x00, 0x03, 0x0E, 0x02];
1995 v1.extend(core::iter::repeat_n(0u8, 2 * NAMETABLE_SIZE));
1996 let mut chr = vec![0u8; CHR_BANK_8K];
1997 chr[7] = 0x5A;
1998 v1.extend_from_slice(&chr);
1999 m.load_state(&v1).unwrap();
2000 assert_eq!(m.ppu_read(0x0007), 0x5A);
2001 assert_eq!(m.mode, 0x0E);
2002 assert_eq!(m.outer_prg, 0x02);
2003 }
2004
2005 #[test]
2006 fn m28_save_state_round_trip() {
2007 let mut m = Action53M28::new(synth_prg_16k(8), &[], Mirroring::Vertical).unwrap();
2008 m.cpu_write(0x5000, 0x80);
2010 m.cpu_write(0x8000, 0x0E);
2011 m.cpu_write(0x5000, 0x81);
2013 m.cpu_write(0x8000, 0x01);
2014 m.ppu_write(0x0007, 0x5A);
2015 let resolved = m.cpu_read(0x8000);
2016 let blob = m.save_state();
2017 let mut m2 = Action53M28::new(synth_prg_16k(8), &[], Mirroring::Vertical).unwrap();
2018 m2.load_state(&blob).unwrap();
2019 assert_eq!(m2.ppu_read(0x0007), 0x5A);
2020 assert_eq!(m2.cpu_read(0x8000), resolved);
2021 assert_eq!(m2.current_mirroring(), Mirroring::Vertical);
2022 }
2023
2024 #[test]
2025 fn m30_latch_selects_prg_chr_and_fixed_high() {
2026 let mut m = Unrom512M30::new(synth_prg_16k(8), &[], false, true, 0, false).unwrap();
2028 m.cpu_write(0x8001, 0x23);
2031 assert_eq!(m.cpu_read(0x8000), 3);
2032 assert_eq!(m.cpu_read(0xC000), 7);
2034 m.ppu_write(0x0000, 0xEE);
2036 assert_eq!(m.ppu_read(0x0000), 0xEE);
2037 }
2038
2039 #[test]
2040 fn m30_battery_cart_no_bus_conflict_high_window_only() {
2041 let mut m = Unrom512M30::new(synth_prg_16k(8), &[], false, true, 0, true).unwrap();
2045 m.cpu_write(0x8000, 0x05);
2047 assert_eq!(m.cpu_read(0x8000), 0);
2048 m.cpu_write(0xC000, 0x05);
2051 assert_eq!(m.cpu_read(0x8000), 5);
2052 }
2053
2054 #[test]
2057 fn m30_flashable_board_programs_and_erases_the_chip() {
2058 let mut m = Unrom512M30::new(synth_prg_16k(16), &[], false, true, 1, false).unwrap();
2059 assert_eq!(
2060 m.save_data().len(),
2061 16 * PRG_BANK_16K,
2062 "the flash is the save"
2063 );
2064 assert!(m.sram().is_empty(), "no RAM at $6000");
2065 for (bank, a, v) in [
2066 (1u8, 0x9555u16, 0xAAu8),
2067 (0, 0xAAAA, 0x55),
2068 (1, 0x9555, 0xA0),
2069 (5, 0x8123, 0x42),
2070 ] {
2071 m.cpu_write(0xC000, bank);
2072 m.cpu_write(a, v);
2073 }
2074 m.cpu_write(0xC000, 5);
2075 assert_eq!(m.cpu_read(0x8123), 0x42);
2076 let blob = m.save_state();
2077 let mut m2 = Unrom512M30::new(synth_prg_16k(16), &[], false, true, 1, false).unwrap();
2078 m2.load_state(&blob).unwrap();
2079 assert_eq!(
2080 m2.cpu_read(0x8123),
2081 0x42,
2082 "the flashed sector is in the state"
2083 );
2084 for (bank, a, v) in [
2086 (1u8, 0x9555u16, 0xAAu8),
2087 (0, 0xAAAA, 0x55),
2088 (1, 0x9555, 0x80),
2089 (1, 0x9555, 0xAA),
2090 (0, 0xAAAA, 0x55),
2091 (5, 0x8000, 0x30),
2092 ] {
2093 m.cpu_write(0xC000, bank);
2094 m.cpu_write(a, v);
2095 }
2096 m.cpu_write(0xC000, 5);
2097 assert_eq!(m.cpu_read(0x8123), 0xFF);
2098 }
2099
2100 #[test]
2106 fn m30_clear_save_data_restores_the_image_as_loaded() {
2107 let fresh = Unrom512M30::new(synth_prg_16k(16), &[], false, true, 1, false).unwrap();
2108 let mut m = Unrom512M30::new(synth_prg_16k(16), &[], false, true, 1, false).unwrap();
2109 for (bank, a, v) in [
2110 (1u8, 0x9555u16, 0xAAu8),
2111 (0, 0xAAAA, 0x55),
2112 (1, 0x9555, 0xA0),
2113 (5, 0x8123, 0x00),
2114 ] {
2115 m.cpu_write(0xC000, bank);
2116 m.cpu_write(a, v);
2117 }
2118 assert_ne!(
2119 m.save_data(),
2120 fresh.save_data(),
2121 "the program changed the flash"
2122 );
2123 m.clear_save_data();
2124 assert_eq!(
2125 m.save_data(),
2126 fresh.save_data(),
2127 "back to the image as loaded"
2128 );
2129 }
2130
2131 #[test]
2135 fn flash_state_bytes_the_chip_cannot_produce_are_refused() {
2136 let gt = Gtrom111::new(synth_prg_32k(8), &[]).unwrap();
2137 let good = gt.save_state();
2138 for (i, v) in [(5usize, 7u8), (5, 0xFF), (6, 2)] {
2139 let mut blob = good.clone();
2140 blob[i] = v;
2141 let mut m = Gtrom111::new(synth_prg_32k(8), &[]).unwrap();
2142 assert!(
2143 matches!(m.load_state(&blob), Err(MapperError::Invalid(_))),
2144 "GTROM byte {i} = {v:#x}"
2145 );
2146 assert_eq!(m.save_state(), good, "GTROM: nothing assigned");
2147 }
2148 let m30 = Unrom512M30::new(synth_prg_16k(16), &[], false, true, 1, false).unwrap();
2149 let good = m30.save_state();
2150 let fixed = 6 + m30.vram.len() + m30.chr.len();
2151 for (i, v) in [(fixed - 2, 7u8), (fixed - 1, 2)] {
2152 let mut blob = good.clone();
2153 blob[i] = v;
2154 let mut m = Unrom512M30::new(synth_prg_16k(16), &[], false, true, 1, false).unwrap();
2155 assert!(
2156 matches!(m.load_state(&blob), Err(MapperError::Invalid(_))),
2157 "UNROM 512 byte {i} = {v:#x}"
2158 );
2159 assert_eq!(m.save_state(), good, "UNROM 512: nothing assigned");
2160 }
2161 }
2162
2163 #[test]
2166 fn m111_truncated_state_reports_its_exact_length() {
2167 let mut m = Gtrom111::new(synth_prg_32k(8), &[]).unwrap();
2168 for (a, v) in [
2169 (0xD555u16, 0xAAu8),
2170 (0xAAAA, 0x55),
2171 (0xD555, 0xA0),
2172 (0x8100, 0x42),
2173 ] {
2174 m.cpu_write(a, v);
2175 }
2176 let blob = m.save_state();
2177 let err = Gtrom111::new(synth_prg_32k(8), &[])
2178 .unwrap()
2179 .load_state(&blob[..blob.len() - 100])
2180 .unwrap_err();
2181 assert!(
2182 matches!(err, MapperError::WrongLength { expected, .. } if expected == blob.len()),
2183 "{err:?}"
2184 );
2185 }
2186
2187 #[test]
2190 fn m30_refused_state_leaves_the_flash_untouched() {
2191 let mut a = Unrom512M30::new(synth_prg_16k(16), &[], false, true, 1, false).unwrap();
2192 for (bank, addr, v) in [
2193 (1u8, 0x9555u16, 0xAAu8),
2194 (0, 0xAAAA, 0x55),
2195 (1, 0x9555, 0xA0),
2196 (5, 0x8123, 0x00),
2197 ] {
2198 a.cpu_write(0xC000, bank);
2199 a.cpu_write(addr, v);
2200 }
2201 let mut blob = a.save_state();
2202 blob.push(0);
2203 let mut b = Unrom512M30::new(synth_prg_16k(16), &[], false, true, 1, false).unwrap();
2204 let before = b.save_data().to_vec();
2205 assert!(b.load_state(&blob).is_err());
2206 assert_eq!(b.save_data(), &before[..], "the flash is unchanged");
2207 }
2208
2209 #[test]
2213 fn m30_chr_rom_image_is_never_flashed() {
2214 let chr = vec![0u8; 0x8000];
2215 let mut m = Unrom512M30::new(synth_prg_16k(16), &chr, false, false, 0, true).unwrap();
2216 assert!(m.save_data().is_empty(), "no flash save on a CHR-ROM image");
2217 for (a, v) in [
2218 (0x9555u16, 0xAAu8),
2219 (0xAAAA, 0x55),
2220 (0x9555, 0xA0),
2221 (0x8123, 0x00),
2222 ] {
2223 m.cpu_write(0xC000, 1);
2224 m.cpu_write(a, v);
2225 }
2226 m.cpu_write(0xC000, 0);
2227 assert_eq!(m.cpu_read(0x8123), 0xFF, "the ROM is untouched");
2228 }
2229
2230 #[test]
2231 fn m30_non_flashable_board_has_no_flash() {
2232 let mut m = Unrom512M30::new(synth_prg_16k(16), &[], false, true, 0, false).unwrap();
2233 assert_eq!(m.save_data(), []);
2234 m.cpu_write(0x9555, 0xAA);
2235 assert!(m.cpu_read_unmapped(0x6000));
2236 }
2237
2238 #[test]
2241 fn m30_four_screen_uses_the_last_chr_ram_bank() {
2242 let mut m = Unrom512M30::new(synth_prg_16k(16), &[], true, true, 0, true).unwrap();
2243 assert_eq!(m.current_mirroring(), Mirroring::FourScreen);
2244 assert!(m.nametable_unfolded());
2245 assert!(m.nametable_write(0x2C00, 0x44));
2246 assert!(m.nametable_write(0x3C00, 0x55));
2247 assert_eq!(m.nametable_fetch(0x2C00), Some(0x44));
2248 assert_eq!(m.nametable_fetch(0x3C00), Some(0x55));
2249 m.cpu_write(0xC000, 0x60);
2251 assert_eq!(m.ppu_read(0x0C00), 0x44);
2252 assert_eq!(m.ppu_read(0x1C00), 0x55);
2253 }
2254
2255 #[test]
2256 fn m30_save_state_round_trip() {
2257 let mut m = Unrom512M30::new(synth_prg_16k(8), &[], false, true, 0, false).unwrap();
2258 m.cpu_write(0x8001, 0x45);
2259 m.ppu_write(0x0003, 0x77);
2260 let blob = m.save_state();
2261 let mut m2 = Unrom512M30::new(synth_prg_16k(8), &[], false, true, 0, false).unwrap();
2262 m2.load_state(&blob).unwrap();
2263 assert_eq!(m2.cpu_read(0x8000), m.cpu_read(0x8000));
2264 assert_eq!(m2.ppu_read(0x0003), 0x77);
2265 }
2266
2267 #[test]
2268 fn m30_header_mirroring_matches_mesen2() {
2269 let m_h = Unrom512M30::new(synth_prg_16k(2), &[], false, false, 0, false).unwrap();
2274 assert_eq!(m_h.current_mirroring(), Mirroring::Horizontal);
2275 let m_v = Unrom512M30::new(synth_prg_16k(2), &[], false, true, 0, false).unwrap();
2276 assert_eq!(m_v.current_mirroring(), Mirroring::Vertical);
2277 }
2278
2279 #[test]
2280 fn m30_submapper3_runtime_hv_switch() {
2281 let mut m = Unrom512M30::new(synth_prg_16k(8), &[], false, false, 3, false).unwrap();
2284 assert_eq!(
2285 m.current_mirroring(),
2286 Mirroring::Vertical,
2287 "power-on default"
2288 );
2289 m.cpu_write(0xC000, 0x00);
2291 assert_eq!(m.current_mirroring(), Mirroring::Horizontal);
2292 m.cpu_write(0xC000, 0x80);
2294 assert_eq!(m.current_mirroring(), Mirroring::Vertical);
2295 }
2296
2297 #[test]
2298 fn m30_bus_conflict_high_window_uses_fixed_bank() {
2299 let mut m = Unrom512M30::new(synth_prg_16k(8), &[], false, true, 0, false).unwrap();
2304 m.cpu_write(0x8001, 0x02);
2307 assert_eq!(m.cpu_read(0x8000), 2);
2308 m.cpu_write(0xC000, 0x1F);
2313 assert_eq!(m.cpu_read(0x8000), 7);
2314 }
2315}