1#![allow(
32 clippy::cast_possible_truncation,
33 clippy::cast_lossless,
34 clippy::match_same_arms,
35 clippy::doc_markdown,
36 clippy::similar_names,
37 clippy::too_many_lines,
38 clippy::missing_const_for_fn,
39 clippy::struct_excessive_bools,
40 clippy::bool_to_int_with_if
41)]
42
43use crate::a12_filter::A12RiseFilter;
44use crate::cartridge::Mirroring;
45use crate::mapper::{Mapper, MapperCaps, MapperError};
46use alloc::{boxed::Box, format, vec, vec::Vec};
47
48const PRG_BANK_8K: usize = 0x2000;
49const CHR_BANK_1K: usize = 0x0400;
50const NAMETABLE_SIZE: usize = 0x0400;
51const NAMETABLE_SIZE_U16: u16 = 0x0400;
52
53const SAVE_STATE_VERSION: u8 = 3;
58
59const fn nametable_offset(addr: u16, mirroring: Mirroring) -> usize {
64 let table = (((addr - 0x2000) / NAMETABLE_SIZE_U16) & 0x03) as u8;
65 let local = (addr as usize) & (NAMETABLE_SIZE - 1);
66 let physical = mirroring.physical_bank(table);
67 physical * NAMETABLE_SIZE + local
68}
69
70const fn mirroring_to_byte(m: Mirroring) -> u8 {
71 match m {
72 Mirroring::Horizontal => 0,
73 Mirroring::Vertical => 1,
74 Mirroring::SingleScreenA => 2,
75 Mirroring::SingleScreenB => 3,
76 Mirroring::FourScreen => 4,
77 Mirroring::MapperControlled => 5,
78 }
79}
80
81const fn byte_to_mirroring(b: u8, fallback: Mirroring) -> Mirroring {
82 match b {
83 0 => Mirroring::Horizontal,
84 1 => Mirroring::Vertical,
85 2 => Mirroring::SingleScreenA,
86 3 => Mirroring::SingleScreenB,
87 4 => Mirroring::FourScreen,
88 5 => Mirroring::MapperControlled,
89 _ => fallback,
90 }
91}
92
93struct Mmc3Clone {
113 regs: [u8; 8],
114 bank_select: u8,
115 prg_mode: bool,
116 chr_mode: bool,
117 mirroring: Mirroring,
118 irq_counter: u8,
119 irq_latch: u8,
120 irq_reload: bool,
121 irq_enabled: bool,
122 irq_pending: bool,
123 a12: A12RiseFilter,
126 prg_count_8k: usize,
127 chr_count_1k: usize,
128}
129
130impl Mmc3Clone {
131 const SAVE_LEN: usize = 8 + 10;
132
133 fn new(prg_count_8k: usize, chr_count_1k: usize, mirroring: Mirroring) -> Self {
134 Self {
135 regs: [0; 8],
136 bank_select: 0,
137 prg_mode: false,
138 chr_mode: false,
139 mirroring,
140 irq_counter: 0,
141 irq_latch: 0,
142 irq_reload: false,
143 irq_enabled: false,
144 irq_pending: false,
145 a12: A12RiseFilter::new(),
146 prg_count_8k: prg_count_8k.max(1),
147 chr_count_1k: chr_count_1k.max(1),
148 }
149 }
150
151 fn write_register(&mut self, addr: u16, value: u8) {
153 match addr & 0xE001 {
154 0x8000 => {
155 self.bank_select = value & 0x07;
156 self.prg_mode = value & 0x40 != 0;
157 self.chr_mode = value & 0x80 != 0;
158 }
159 0x8001 => {
160 let idx = (self.bank_select & 0x07) as usize;
161 self.regs[idx] = value;
162 }
163 0xA000 => {
164 self.mirroring = if value & 0x01 == 0 {
165 Mirroring::Vertical
166 } else {
167 Mirroring::Horizontal
168 };
169 }
170 0xC000 => self.irq_latch = value,
171 0xC001 => {
172 self.irq_counter = 0;
173 self.irq_reload = true;
174 }
175 0xE000 => {
176 self.irq_enabled = false;
177 self.irq_pending = false;
178 }
179 0xE001 => self.irq_enabled = true,
180 _ => {}
181 }
182 }
183
184 fn prg_bank(&self, slot: usize) -> usize {
187 let last = self.prg_count_8k - 1;
188 let second_last = last.saturating_sub(1);
189 let r6 = self.regs[6] as usize;
190 let r7 = self.regs[7] as usize;
191 match (slot, self.prg_mode) {
192 (0, false) => r6,
193 (0, true) => second_last,
194 (1, _) => r7,
195 (2, false) => second_last,
196 (2, true) => r6,
197 (3, _) => last,
198 _ => 0,
199 }
200 }
201
202 fn chr_bank(&self, slot: usize) -> usize {
206 let banks: [usize; 8] = if self.chr_mode {
207 [
208 self.regs[2] as usize,
209 self.regs[3] as usize,
210 self.regs[4] as usize,
211 self.regs[5] as usize,
212 self.regs[0] as usize & !1,
213 (self.regs[0] as usize & !1) | 1,
214 self.regs[1] as usize & !1,
215 (self.regs[1] as usize & !1) | 1,
216 ]
217 } else {
218 [
219 self.regs[0] as usize & !1,
220 (self.regs[0] as usize & !1) | 1,
221 self.regs[1] as usize & !1,
222 (self.regs[1] as usize & !1) | 1,
223 self.regs[2] as usize,
224 self.regs[3] as usize,
225 self.regs[4] as usize,
226 self.regs[5] as usize,
227 ]
228 };
229 banks[slot & 0x07]
230 }
231
232 fn notify_a12(&mut self, level: bool) {
234 if !self.a12.edge(level) {
237 return;
238 }
239 if self.irq_counter == 0 || self.irq_reload {
240 self.irq_counter = self.irq_latch;
241 self.irq_reload = false;
242 } else {
243 self.irq_counter = self.irq_counter.wrapping_sub(1);
244 }
245 if self.irq_counter == 0 && self.irq_enabled {
246 self.irq_pending = true;
247 }
248 }
249
250 fn save(&self, out: &mut Vec<u8>) {
251 out.extend_from_slice(&self.regs);
252 out.push(self.bank_select);
253 out.push(u8::from(self.prg_mode));
254 out.push(u8::from(self.chr_mode));
255 out.push(mirroring_to_byte(self.mirroring));
256 out.push(self.irq_counter);
257 out.push(self.irq_latch);
258 out.push(u8::from(self.irq_reload));
259 out.push(u8::from(self.irq_enabled));
260 out.push(u8::from(self.irq_pending));
261 out.push(self.a12.to_byte());
262 }
263
264 fn load(&mut self, data: &[u8]) {
265 self.regs.copy_from_slice(&data[0..8]);
266 self.bank_select = data[8];
267 self.prg_mode = data[9] != 0;
268 self.chr_mode = data[10] != 0;
269 self.mirroring = byte_to_mirroring(data[11], self.mirroring);
270 self.irq_counter = data[12];
271 self.irq_latch = data[13];
272 self.irq_reload = data[14] != 0;
273 self.irq_enabled = data[15] != 0;
274 self.irq_pending = data[16] != 0;
275 self.a12 = A12RiseFilter::from_byte(data[17]);
276 }
277}
278
279#[derive(Debug, Clone, Copy, PartialEq, Eq)]
281enum CloneBoard {
282 M44,
284 M49,
286 M52,
288 M115,
290 M134,
292 M189,
294 M205,
296 M238,
298 M245,
300 M348,
302 M366,
304}
305
306pub struct Mmc3CloneMapper {
313 board: CloneBoard,
314 core: Mmc3Clone,
315 prg_rom: Box<[u8]>,
316 chr: Box<[u8]>,
317 chr_is_ram: bool,
318 vram: Box<[u8]>,
319 outer: u8,
321 outer2: u8,
323 wram: Box<[u8]>,
329}
330
331impl Mmc3CloneMapper {
332 fn new(
333 board: CloneBoard,
334 prg_rom: Box<[u8]>,
335 chr_rom: Box<[u8]>,
336 mirroring: Mirroring,
337 mapper_id: u16,
338 ) -> Result<Self, MapperError> {
339 if prg_rom.is_empty() || !prg_rom.len().is_multiple_of(PRG_BANK_8K) {
340 return Err(MapperError::Invalid(format!(
341 "mapper {mapper_id} PRG-ROM size {} is not a non-zero multiple of 8 KiB",
342 prg_rom.len()
343 )));
344 }
345 let chr_is_ram = chr_rom.is_empty();
346 let chr: Box<[u8]> = if chr_is_ram {
347 vec![0u8; 0x8000].into_boxed_slice() } else {
349 if !chr_rom.len().is_multiple_of(CHR_BANK_1K) {
350 return Err(MapperError::Invalid(format!(
351 "mapper {mapper_id} CHR-ROM size {} is not a multiple of 1 KiB",
352 chr_rom.len()
353 )));
354 }
355 chr_rom
356 };
357 let prg_count_8k = prg_rom.len() / PRG_BANK_8K;
358 let chr_count_1k = (chr.len() / CHR_BANK_1K).max(1);
359 Ok(Self {
360 board,
361 core: Mmc3Clone::new(prg_count_8k, chr_count_1k, mirroring),
362 prg_rom,
363 chr,
364 chr_is_ram,
365 vram: vec![0u8; 2 * NAMETABLE_SIZE].into_boxed_slice(),
366 outer: 0,
367 outer2: 0,
368 wram: if matches!(board, CloneBoard::M245) {
369 vec![0u8; 0x2000].into_boxed_slice()
370 } else {
371 Box::new([])
372 },
373 })
374 }
375
376 fn resolve_prg(&self, slot: usize) -> usize {
379 let base = self.core.prg_bank(slot);
380 let count = self.core.prg_count_8k;
381 let bank = match self.board {
382 CloneBoard::M44 => {
383 let block = (self.outer & 0x07).min(6) as usize;
384 let mask = if block <= 5 { 0x0F } else { 0x1F };
385 (base & mask) | (block * 0x10)
386 }
387 CloneBoard::M49 => {
388 let block = ((self.outer >> 6) & 0x03) as usize;
389 if self.outer & 0x01 != 0 {
390 (base & 0x0F) | (block * 0x10)
391 } else {
392 ((self.outer >> 4) & 0x03) as usize * 4 + slot
393 }
394 }
395 CloneBoard::M52 => {
396 if self.outer & 0x08 != 0 {
397 (base & 0x0F) | ((self.outer as usize & 0x07) << 4)
398 } else {
399 (base & 0x1F) | ((self.outer as usize & 0x06) << 4)
400 }
401 }
402 CloneBoard::M115 => {
403 if self.outer & 0x80 != 0 {
404 if self.outer & 0x20 != 0 {
405 ((self.outer as usize & 0x0F) >> 1) * 4 + slot
406 } else {
407 let b16 = (self.outer as usize & 0x0F) * 2;
408 b16 + (slot & 0x01)
409 }
410 } else {
411 base
412 }
413 }
414 CloneBoard::M134 => (base & 0x1F) | ((self.outer as usize & 0x02) << 4),
415 CloneBoard::M189 => {
416 let page = ((self.outer as usize) | (self.outer as usize >> 4)) & 0x07;
417 page * 4 + slot
418 }
419 CloneBoard::M205 => {
420 let block = self.outer as usize & 0x03;
421 let mask = if block <= 1 { 0x1F } else { 0x0F };
422 (base & mask) | (block * 0x10)
423 }
424 CloneBoard::M238 => base,
425 CloneBoard::M245 => {
426 let or = if self.core.regs[0] & 0x02 != 0 {
427 0x40
428 } else {
429 0
430 };
431 (base & 0x3F) | or
432 }
433 CloneBoard::M348 => (base & 0x0F) | ((self.outer as usize & 0x0C) << 2),
434 CloneBoard::M366 => (base & 0x0F) | (self.outer as usize & 0x30),
435 };
436 bank % count
437 }
438
439 fn resolve_chr(&self, slot: usize) -> usize {
442 let base = self.core.chr_bank(slot);
443 let count = self.core.chr_count_1k;
444 let bank = match self.board {
445 CloneBoard::M44 => {
446 let block = (self.outer & 0x07).min(6) as usize;
447 let mask = if block <= 5 { 0x7F } else { 0xFF };
448 (base & mask) | (block * 0x80)
449 }
450 CloneBoard::M49 => {
451 let block = ((self.outer >> 6) & 0x03) as usize;
452 (base & 0x7F) | (block * 0x80)
453 }
454 CloneBoard::M52 => {
455 if self.outer & 0x40 != 0 {
456 (base & 0x7F)
457 | (((self.outer as usize & 0x04) | ((self.outer as usize >> 4) & 0x03))
458 << 7)
459 } else {
460 (base & 0xFF)
461 | (((self.outer as usize & 0x04) | ((self.outer as usize >> 4) & 0x02))
462 << 7)
463 }
464 }
465 CloneBoard::M115 => base | ((self.outer2 as usize & 0x01) << 8),
466 CloneBoard::M134 => (base & 0xFF) | ((self.outer as usize & 0x20) << 3),
467 CloneBoard::M189 => base,
468 CloneBoard::M205 => {
469 let block = self.outer as usize & 0x03;
470 if block >= 2 {
471 (base & 0x7F) | 0x100
472 } else {
473 base | if block == 1 { 0x80 } else { 0 }
474 }
475 }
476 CloneBoard::M238 => base,
477 CloneBoard::M245 => base, CloneBoard::M348 => (base & 0x7F) | ((self.outer as usize & 0x0C) << 5),
479 CloneBoard::M366 => (base & 0x7F) | ((self.outer as usize & 0x30) << 3),
480 };
481 bank % count
482 }
483}
484
485impl Mapper for Mmc3CloneMapper {
486 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 0x8000..=0x9FFF => {
508 let bank = self.resolve_prg(0);
509 self.prg_rom[bank * PRG_BANK_8K + (addr as usize & 0x1FFF)]
510 }
511 0xA000..=0xBFFF => {
512 let bank = self.resolve_prg(1);
513 self.prg_rom[bank * PRG_BANK_8K + (addr as usize & 0x1FFF)]
514 }
515 0xC000..=0xDFFF => {
516 let bank = self.resolve_prg(2);
517 self.prg_rom[bank * PRG_BANK_8K + (addr as usize & 0x1FFF)]
518 }
519 0xE000..=0xFFFF => {
520 let bank = self.resolve_prg(3);
521 self.prg_rom[bank * PRG_BANK_8K + (addr as usize & 0x1FFF)]
522 }
523 0x5000..=0x5FFF if matches!(self.board, CloneBoard::M115) => self.outer2,
525 0x4020..=0x7FFF if matches!(self.board, CloneBoard::M238) => self.outer2,
527 0x6000..=0x7FFF if !self.wram.is_empty() => self.wram[usize::from(addr - 0x6000)],
528 _ => 0,
529 }
530 }
531
532 fn cpu_read_unmapped(&self, addr: u16) -> bool {
533 match self.board {
534 CloneBoard::M115 => {
538 (0x4020..=0x4FFF).contains(&addr)
539 || (matches!(addr, 0x6000..=0x7FFF) && self.sram().is_empty())
540 }
541 CloneBoard::M238 => false, _ => {
543 (0x4020..=0x5FFF).contains(&addr)
544 || (matches!(addr, 0x6000..=0x7FFF) && self.sram().is_empty())
545 }
546 }
547 }
548
549 fn cpu_write(&mut self, addr: u16, value: u8) {
550 match self.board {
551 CloneBoard::M115 => match addr {
552 0x5080 => self.outer2 = value,
553 0x4100..=0x7FFF => {
554 if addr & 0x01 == 0 {
555 self.outer = value; } else {
557 self.outer2 = value; }
559 }
560 0x8000..=0xFFFF => self.core.write_register(addr, value),
561 _ => {}
562 },
563 CloneBoard::M134 => {
564 if addr == 0x6001 {
565 self.outer = value;
566 } else if (0x8000..=0xFFFF).contains(&addr) {
567 self.core.write_register(addr, value);
568 }
569 }
570 CloneBoard::M189 => {
571 if (0x4120..=0x7FFF).contains(&addr) {
572 self.outer = value;
573 } else if (0x8000..=0xFFFF).contains(&addr) {
574 self.core.write_register(addr, value);
575 }
576 }
577 CloneBoard::M238 => {
578 if (0x4020..=0x7FFF).contains(&addr) {
579 const LUT: [u8; 4] = [0x00, 0x02, 0x02, 0x03];
580 self.outer2 = LUT[(value & 0x03) as usize];
581 } else if (0x8000..=0xFFFF).contains(&addr) {
582 self.core.write_register(addr, value);
583 }
584 }
585 CloneBoard::M44 => {
586 if (0x8000..=0xFFFF).contains(&addr) {
587 if addr & 0xE001 == 0xA001 {
588 self.outer = value & 0x07;
589 }
590 self.core.write_register(addr, value);
591 }
592 }
593 CloneBoard::M348 => {
594 if (0x6800..=0x68FF).contains(&addr) {
595 self.outer = value;
596 } else if (0x8000..=0xFFFF).contains(&addr) {
597 self.core.write_register(addr, value);
598 }
599 }
600 CloneBoard::M366 => {
601 if (0x6000..=0x7FFF).contains(&addr) {
602 self.outer = if addr < 0x7000 {
603 (addr as u8) & 0x30
604 } else {
605 value & 0x30
606 };
607 } else if (0x8000..=0xFFFF).contains(&addr) {
608 self.core.write_register(addr, value);
609 }
610 }
611 CloneBoard::M49 | CloneBoard::M52 | CloneBoard::M205 => {
612 if (0x6000..=0x7FFF).contains(&addr) {
613 self.outer = value;
614 } else if (0x8000..=0xFFFF).contains(&addr) {
615 self.core.write_register(addr, value);
616 }
617 }
618 CloneBoard::M245 => {
619 if (0x6000..=0x7FFF).contains(&addr) {
620 self.wram[usize::from(addr - 0x6000)] = value;
621 } else if (0x8000..=0xFFFF).contains(&addr) {
622 self.core.write_register(addr, value);
623 }
624 }
625 }
626 }
627
628 fn ppu_read(&mut self, addr: u16) -> u8 {
629 let addr = addr & 0x3FFF;
630 match addr {
631 0x0000..=0x1FFF => {
632 if self.chr_is_ram {
633 if matches!(self.board, CloneBoard::M245) {
634 let half = if self.core.chr_mode { 0x1000 } else { 0 };
635 let off = (half ^ (addr as usize & 0x1FFF)) & (self.chr.len() - 1);
636 return self.chr[off];
637 }
638 return self.chr[addr as usize & (self.chr.len() - 1)];
639 }
640 let slot = (addr as usize) / CHR_BANK_1K;
641 let bank = self.resolve_chr(slot);
642 self.chr[bank * CHR_BANK_1K + (addr as usize & 0x3FF)]
643 }
644 0x2000..=0x3EFF => self.vram[nametable_offset(addr, self.core.mirroring)],
645 _ => 0,
646 }
647 }
648
649 fn ppu_write(&mut self, addr: u16, value: u8) {
650 let addr = addr & 0x3FFF;
651 match addr {
652 0x0000..=0x1FFF if self.chr_is_ram => {
653 if matches!(self.board, CloneBoard::M245) {
654 let half = if self.core.chr_mode { 0x1000 } else { 0 };
655 let off = (half ^ (addr as usize & 0x1FFF)) & (self.chr.len() - 1);
656 self.chr[off] = value;
657 } else {
658 let off = addr as usize & (self.chr.len() - 1);
659 self.chr[off] = value;
660 }
661 }
662 0x2000..=0x3EFF => {
663 let off = nametable_offset(addr, self.core.mirroring);
664 self.vram[off] = value;
665 }
666 _ => {}
667 }
668 }
669
670 fn notify_a12(&mut self, level: bool) {
671 self.core.notify_a12(level);
672 }
673
674 fn notify_cpu_cycle(&mut self) {
675 self.core.a12.tick();
676 }
677
678 fn irq_pending(&self) -> bool {
679 self.core.irq_pending
680 }
681
682 fn irq_acknowledge(&mut self) {
683 self.core.irq_pending = false;
684 }
685
686 fn current_mirroring(&self) -> Mirroring {
687 self.core.mirroring
688 }
689
690 fn save_state(&self) -> Vec<u8> {
691 let chr_ram = if self.chr_is_ram { self.chr.len() } else { 0 };
692 let mut out = Vec::with_capacity(3 + Mmc3Clone::SAVE_LEN + self.vram.len() + chr_ram);
693 out.push(SAVE_STATE_VERSION);
694 out.push(self.outer);
695 out.push(self.outer2);
696 self.core.save(&mut out);
697 out.extend_from_slice(&self.vram);
698 if self.chr_is_ram {
699 out.extend_from_slice(&self.chr);
700 }
701 out.extend_from_slice(&self.wram);
702 out
703 }
704
705 fn load_state(&mut self, data: &[u8]) -> Result<(), MapperError> {
706 let chr_ram = if self.chr_is_ram { self.chr.len() } else { 0 };
707 let version = *data.first().ok_or(MapperError::WrongLength {
708 expected: 1,
709 got: 0,
710 })?;
711 if version != SAVE_STATE_VERSION {
712 return Err(MapperError::UnsupportedVersion(version));
713 }
714 let wram_len = self.wram.len();
715 let expected = 3 + Mmc3Clone::SAVE_LEN + self.vram.len() + chr_ram + wram_len;
716 if data.len() != expected {
717 return Err(MapperError::WrongLength {
718 expected,
719 got: data.len(),
720 });
721 }
722 self.outer = data[1];
723 self.outer2 = data[2];
724 let mut cursor = 3;
725 self.core.load(&data[cursor..cursor + Mmc3Clone::SAVE_LEN]);
726 cursor += Mmc3Clone::SAVE_LEN;
727 self.vram
728 .copy_from_slice(&data[cursor..cursor + self.vram.len()]);
729 cursor += self.vram.len();
730 if self.chr_is_ram {
731 self.chr
732 .copy_from_slice(&data[cursor..cursor + self.chr.len()]);
733 cursor += self.chr.len();
734 }
735 self.wram.copy_from_slice(&data[cursor..cursor + wram_len]);
736 Ok(())
737 }
738}
739
740macro_rules! clone_ctor {
741 ($fn_name:ident, $board:expr, $id:expr, $doc:expr) => {
742 #[doc = $doc]
743 pub fn $fn_name(
747 prg_rom: Box<[u8]>,
748 chr_rom: Box<[u8]>,
749 mirroring: Mirroring,
750 ) -> Result<Mmc3CloneMapper, MapperError> {
751 Mmc3CloneMapper::new($board, prg_rom, chr_rom, mirroring, $id)
752 }
753 };
754}
755
756clone_ctor!(
757 new_m44,
758 CloneBoard::M44,
759 44,
760 "Mapper 44 (BMC SuperBig 7-in-1 MMC3 multicart)."
761);
762clone_ctor!(
763 new_m49,
764 CloneBoard::M49,
765 49,
766 "Mapper 49 (BMC 4-in-1 MMC3 multicart)."
767);
768clone_ctor!(
769 new_m52,
770 CloneBoard::M52,
771 52,
772 "Mapper 52 (BMC Mario 7-in-1 MMC3 multicart)."
773);
774clone_ctor!(
775 new_m115,
776 CloneBoard::M115,
777 115,
778 "Mapper 115 (Kasheng SFC-02B/-03/-004 MMC3 clone)."
779);
780clone_ctor!(
781 new_m134,
782 CloneBoard::M134,
783 134,
784 "Mapper 134 (T4A54A / WX-KB4K MMC3-clone multicart)."
785);
786clone_ctor!(
787 new_m189,
788 CloneBoard::M189,
789 189,
790 "Mapper 189 (TXC 32 KiB-PRG MMC3 clone)."
791);
792clone_ctor!(
793 new_m205,
794 CloneBoard::M205,
795 205,
796 "Mapper 205 (BMC 3-in-1 / 15-in-1 MMC3 multicart)."
797);
798clone_ctor!(
799 new_m238,
800 CloneBoard::M238,
801 238,
802 "Mapper 238 (MMC3 clone + $4020-$7FFF security LUT)."
803);
804clone_ctor!(
805 new_m245,
806 CloneBoard::M245,
807 245,
808 "Mapper 245 (Waixing MMC3 clone, CHR-RAM PRG-256K outer)."
809);
810clone_ctor!(
811 new_m348,
812 CloneBoard::M348,
813 348,
814 "Mapper 348 (BMC-830118C MMC3 multicart)."
815);
816clone_ctor!(
817 new_m366,
818 CloneBoard::M366,
819 366,
820 "Mapper 366 (BMC-GN-45 MMC3 multicart)."
821);
822
823#[cfg(test)]
841#[allow(clippy::cast_possible_truncation)]
842mod tests {
843
844 #[test]
848 fn irq_counts_scanlines_not_raw_a12_pulses() {
849 let mut m = new_m245(synth_prg_8k(16), Box::new([]), Mirroring::Vertical).unwrap();
850 assert_eq!(crate::a12_filter::irqs_over_scanlines(&mut m, 16), 2);
851 }
852
853 use super::*;
854
855 fn synth_prg_8k(banks: usize) -> Box<[u8]> {
856 let mut v = vec![0xFFu8; banks * PRG_BANK_8K];
857 for b in 0..banks {
858 v[b * PRG_BANK_8K] = b as u8;
859 }
860 v.into_boxed_slice()
861 }
862
863 fn synth_chr_1k(banks: usize) -> Box<[u8]> {
864 let mut v = vec![0u8; banks * CHR_BANK_1K];
865 for b in 0..banks {
866 v[b * CHR_BANK_1K] = b as u8;
867 }
868 v.into_boxed_slice()
869 }
870
871 #[test]
872 fn mmc3_clone_prg_layout_and_a12_irq() {
873 let mut m = new_m245(synth_prg_8k(16), Box::new([]), Mirroring::Vertical).unwrap();
874 m.cpu_write(0x8000, 0x06); m.cpu_write(0x8001, 5);
876 m.cpu_write(0x8000, 0x07); m.cpu_write(0x8001, 6);
878 assert_eq!(m.cpu_read(0x8000), 5); assert_eq!(m.cpu_read(0xA000), 6); assert_eq!(m.cpu_read(0xE000), 15); m.cpu_write(0xC000, 2); m.cpu_write(0xC001, 0); m.cpu_write(0xE001, 0); assert!(!m.irq_pending());
886 for _ in 0..3 {
887 m.notify_a12(false);
888 for _ in 0..3 {
890 m.notify_cpu_cycle();
891 }
892 m.notify_a12(true);
893 }
894 assert!(m.irq_pending());
895 m.cpu_write(0xE000, 0); assert!(!m.irq_pending());
897 }
898
899 #[test]
900 fn m245_prg_outer_bank_from_reg0_bit1() {
901 let mut m = new_m245(synth_prg_8k(128), Box::new([]), Mirroring::Vertical).unwrap();
902 m.cpu_write(0x8000, 0x00); m.cpu_write(0x8001, 0x02); m.cpu_write(0x8000, 0x06); m.cpu_write(0x8001, 5); assert_eq!(m.cpu_read(0x8000), 69);
909 }
910
911 #[test]
912 fn m115_chr_outer_and_protection_read() {
913 let mut m = new_m115(synth_prg_8k(32), synth_chr_1k(512), Mirroring::Vertical).unwrap();
914 m.cpu_write(0x4101, 0x01); assert_eq!(m.ppu_read(0x0000), 0); m.cpu_write(0x5080, 0xAB);
917 assert_eq!(m.cpu_read(0x5000), 0xAB);
918 }
919
920 #[test]
921 fn m189_prg_32k_select_overrides_mmc3() {
922 let mut m = new_m189(synth_prg_8k(32), synth_chr_1k(64), Mirroring::Vertical).unwrap();
923 m.cpu_write(0x4120, 0x33); assert_eq!(m.cpu_read(0x8000), 12);
925 assert_eq!(m.cpu_read(0xA000), 13);
926 }
927
928 #[test]
929 fn mmc3_clone_save_state_round_trip() {
930 let mut m = new_m115(synth_prg_8k(32), synth_chr_1k(256), Mirroring::Vertical).unwrap();
931 m.cpu_write(0x8000, 0x06);
932 m.cpu_write(0x8001, 4);
933 m.cpu_write(0x4101, 0x01);
934 m.cpu_write(0xC000, 7);
935 m.ppu_write(0x2005, 0x5A);
936 let blob = m.save_state();
937 let mut m2 = new_m115(synth_prg_8k(32), synth_chr_1k(256), Mirroring::Vertical).unwrap();
938 m2.load_state(&blob).unwrap();
939 assert_eq!(m2.cpu_read(0x8000), m.cpu_read(0x8000));
940 assert_eq!(m2.ppu_read(0x2005), 0x5A);
941 }
942
943 #[test]
944 fn m245_chr_ram_round_trip() {
945 let mut m = new_m245(synth_prg_8k(16), Box::new([]), Mirroring::Vertical).unwrap();
946 m.ppu_write(0x0010, 0x42);
947 let blob = m.save_state();
948 let mut m2 = new_m245(synth_prg_8k(16), Box::new([]), Mirroring::Vertical).unwrap();
949 m2.load_state(&blob).unwrap();
950 assert_eq!(m2.ppu_read(0x0010), 0x42);
951 }
952}