1#![allow(
77 clippy::cast_possible_truncation,
78 clippy::cast_lossless,
79 clippy::match_same_arms,
80 clippy::doc_markdown,
81 clippy::struct_excessive_bools,
82 clippy::similar_names,
83 clippy::missing_const_for_fn,
84 clippy::too_many_lines
85)]
86
87use crate::cartridge::Mirroring;
88use crate::mapper::{Mapper, MapperCaps, MapperError};
89use alloc::{boxed::Box, format, vec, vec::Vec};
90
91const PRG_BANK_8K: usize = 0x2000;
92const CHR_BANK_1K: usize = 0x0400;
93const NAMETABLE_SIZE: usize = 0x0400;
94const NAMETABLE_SIZE_U16: u16 = 0x0400;
95
96const SAVE_STATE_VERSION: u8 = 1;
97
98#[derive(Debug, Clone, Copy, PartialEq, Eq)]
101pub enum JyBoard {
102 M90,
104 M209,
106 M211,
108 M35,
113}
114
115impl JyBoard {
116 const fn mapper_id(self) -> u16 {
117 match self {
118 Self::M90 => 90,
119 Self::M209 => 209,
120 Self::M211 => 211,
121 Self::M35 => 35,
122 }
123 }
124}
125
126#[derive(Debug, Clone, Copy, PartialEq, Eq)]
128enum IrqSource {
129 CpuClock,
131 PpuA12Rise,
133 PpuRead,
135 CpuWrite,
137}
138
139impl IrqSource {
140 const fn from_bits(bits: u8) -> Self {
141 match bits & 0x03 {
142 0 => Self::CpuClock,
143 1 => Self::PpuA12Rise,
144 2 => Self::PpuRead,
145 _ => Self::CpuWrite,
146 }
147 }
148
149 const fn to_bits(self) -> u8 {
150 match self {
151 Self::CpuClock => 0,
152 Self::PpuA12Rise => 1,
153 Self::PpuRead => 2,
154 Self::CpuWrite => 3,
155 }
156 }
157}
158
159pub struct JyAsic {
161 board: JyBoard,
162 prg_rom: Box<[u8]>,
163 chr: Box<[u8]>,
164 chr_is_ram: bool,
165
166 prg_regs: [u8; 4], chr_low_regs: [u8; 8], chr_high_regs: [u8; 8], nt_low_regs: [u8; 4], nt_high_regs: [u8; 4], chr_latch: [u8; 2], prg_mode: u8, enable_prg_at_6000: bool, chr_mode: u8, advanced_nt_control: bool, nt_global: bool, mirroring_reg: u8, extended_mirroring: bool, nt_ram_select_bit: u8, chr_block_mode: bool, chr_block: u8, mirror_chr: bool, irq_enabled: bool,
192 irq_source: IrqSource,
193 irq_count_direction: u8, irq_small_prescaler: bool,
195 irq_prescaler: u8,
196 irq_counter: u8,
197 irq_xor: u8,
198 irq_funky_reg: u8, irq_pending: bool,
200
201 mul1: u8,
203 mul2: u8,
204 test_reg: u8,
205
206 last_ppu_addr: u16,
207}
208
209impl JyAsic {
210 pub fn new(
220 prg_rom: Box<[u8]>,
221 chr_rom: Box<[u8]>,
222 mirroring: Mirroring,
223 board: JyBoard,
224 ) -> Result<Self, MapperError> {
225 if prg_rom.is_empty() || !prg_rom.len().is_multiple_of(PRG_BANK_8K) {
226 return Err(MapperError::Invalid(format!(
227 "JY ASIC PRG-ROM size {} is not a non-zero multiple of 8 KiB",
228 prg_rom.len()
229 )));
230 }
231 let chr_is_ram = chr_rom.is_empty();
232 let chr: Box<[u8]> = if chr_is_ram {
233 vec![0u8; 256 * CHR_BANK_1K].into_boxed_slice()
235 } else if chr_rom.len().is_multiple_of(CHR_BANK_1K) {
236 chr_rom
237 } else {
238 return Err(MapperError::Invalid(format!(
239 "JY ASIC CHR-ROM size {} is not a multiple of 1 KiB",
240 chr_rom.len()
241 )));
242 };
243 let mirroring_reg = match mirroring {
245 Mirroring::Horizontal => 1,
246 Mirroring::SingleScreenA => 2,
247 Mirroring::SingleScreenB => 3,
248 _ => 0,
250 };
251 Ok(Self {
252 board,
253 prg_rom,
254 chr,
255 chr_is_ram,
256 prg_regs: [0; 4],
257 chr_low_regs: [0; 8],
258 chr_high_regs: [0; 8],
259 nt_low_regs: [0; 4],
260 nt_high_regs: [0; 4],
261 chr_latch: [0, 4],
264 prg_mode: 0,
265 enable_prg_at_6000: false,
266 chr_mode: 0,
267 advanced_nt_control: false,
268 nt_global: false,
269 mirroring_reg,
270 extended_mirroring: false,
271 nt_ram_select_bit: 0,
272 chr_block_mode: false,
273 chr_block: 0,
274 mirror_chr: false,
275 irq_enabled: false,
276 irq_source: IrqSource::CpuClock,
277 irq_count_direction: 0,
278 irq_small_prescaler: false,
279 irq_prescaler: 0,
280 irq_counter: 0,
281 irq_xor: 0,
282 irq_funky_reg: 0,
283 irq_pending: false,
284 mul1: 0,
285 mul2: 0,
286 test_reg: 0,
287 last_ppu_addr: 0,
288 })
289 }
290
291 const fn nt_control_active(&self) -> bool {
296 match self.board {
297 JyBoard::M211 => true,
298 JyBoard::M90 => false,
299 JyBoard::M209 | JyBoard::M35 => self.advanced_nt_control || self.extended_mirroring,
300 }
301 }
302
303 const fn invert_prg_bits(reg: u8, invert: bool) -> u8 {
319 if invert {
320 (reg & 0x01) << 6
321 | (reg & 0x02) << 4
322 | (reg & 0x04) << 2
323 | (reg & 0x10) >> 2
324 | (reg & 0x20) >> 4
325 | (reg & 0x40) >> 6
326 } else {
327 reg
328 }
329 }
330
331 fn prg_bank_8k(&self, window: usize) -> usize {
335 let invert = (self.prg_mode & 0x03) == 0x03;
336 let r: [usize; 4] = [
337 Self::invert_prg_bits(self.prg_regs[0], invert) as usize,
338 Self::invert_prg_bits(self.prg_regs[1], invert) as usize,
339 Self::invert_prg_bits(self.prg_regs[2], invert) as usize,
340 Self::invert_prg_bits(self.prg_regs[3], invert) as usize,
341 ];
342 let last_switchable = (self.prg_mode & 0x04) != 0;
343 match self.prg_mode & 0x03 {
344 0 => {
346 let base = if last_switchable { r[3] * 4 } else { 0x3C };
347 base + window
348 }
349 1 => {
351 if window < 2 {
352 (r[1] << 1) + window
353 } else {
354 let base = if last_switchable { r[3] << 1 } else { 0x3E };
355 base + (window - 2)
356 }
357 }
358 _ => match window {
361 0 => r[0],
362 1 => r[1],
363 2 => r[2],
364 _ => {
365 if last_switchable {
366 r[3]
367 } else {
368 0x3F
369 }
370 }
371 },
372 }
373 }
374
375 fn prg_bank_6000(&self) -> usize {
378 let invert = (self.prg_mode & 0x03) == 0x03;
379 let r3 = Self::invert_prg_bits(self.prg_regs[3], invert) as usize;
380 match self.prg_mode & 0x03 {
381 0 => r3 * 4 + 3,
382 1 => (r3 << 1) | 1,
383 _ => r3,
384 }
385 }
386
387 fn prg_offset(&self, addr: u16) -> usize {
388 let total = (self.prg_rom.len() / PRG_BANK_8K).max(1);
389 let bank = if (0x6000..0x8000).contains(&addr) {
390 self.prg_bank_6000()
391 } else {
392 let window = ((addr >> 13) & 0x03) as usize; self.prg_bank_8k(window)
394 };
395 (bank % total) * PRG_BANK_8K + (addr as usize & 0x1FFF)
396 }
397
398 fn chr_reg(&self, index: usize) -> u32 {
401 let index = if self.chr_mode >= 2 && self.mirror_chr && (index == 2 || index == 3) {
404 index - 2
405 } else {
406 index
407 };
408 if self.chr_block_mode {
409 let (mask, shift): (u32, u32) = match self.chr_mode {
410 0 => (0x1F, 5),
411 1 => (0x3F, 6),
412 2 => (0x7F, 7),
413 _ => (0xFF, 8),
414 };
415 (self.chr_low_regs[index] as u32 & mask) | ((self.chr_block as u32) << shift)
416 } else {
417 self.chr_low_regs[index] as u32 | ((self.chr_high_regs[index] as u32) << 8)
418 }
419 }
420
421 fn chr_offset(&self, addr: u16) -> usize {
423 let addr = (addr & 0x1FFF) as usize;
424 let total_1k = (self.chr.len() / CHR_BANK_1K).max(1);
425 let slot_1k = addr / CHR_BANK_1K; let bank_1k: usize = match self.chr_mode {
427 0 => (self.chr_reg(0) as usize) * 8 + slot_1k,
429 1 => {
431 let reg = if addr < 0x1000 {
432 self.chr_latch[0] as usize
433 } else {
434 self.chr_latch[1] as usize
435 };
436 (self.chr_reg(reg) as usize) * 4 + (slot_1k & 0x03)
437 }
438 2 => {
440 let reg = (slot_1k & !1) & 0x07;
441 (self.chr_reg(reg) as usize) * 2 + (slot_1k & 0x01)
442 }
443 _ => self.chr_reg(slot_1k) as usize,
445 };
446 (bank_1k % total_1k) * CHR_BANK_1K + (addr & (CHR_BANK_1K - 1))
447 }
448
449 fn nametable_offset(&self, addr: u16) -> usize {
450 let table = (((addr - 0x2000) / NAMETABLE_SIZE_U16) & 0x03) as u8;
451 let local = (addr as usize) & (NAMETABLE_SIZE - 1);
452 let physical = self.ciram_bank_for(table);
453 physical * NAMETABLE_SIZE + local
454 }
455
456 const fn ciram_bank_for(&self, table: u8) -> usize {
466 if self.nt_control_active() {
467 (self.nt_low_regs[table as usize & 0x03] & 0x01) as usize
468 } else {
469 Self::physical_bank_for(table, self.mirroring_reg)
470 }
471 }
472
473 const fn physical_bank_for(table: u8, mirroring_reg: u8) -> usize {
477 match mirroring_reg & 0x03 {
478 0 => table as usize & 1,
480 1 => (table >> 1) as usize & 1,
482 2 => 0,
484 _ => 1,
486 }
487 }
488
489 fn update_chr_latch_209(&mut self, addr: u16) {
492 if !matches!(self.board, JyBoard::M209 | JyBoard::M35) {
493 return;
494 }
495 match addr & 0x2FF8 {
496 0x0FD8 | 0x0FE8 => {
497 let idx = (addr >> 12) as usize & 0x01;
499 self.chr_latch[idx] = ((addr >> 4) as u8) & (((addr >> 10) as u8 & 0x04) | 0x02);
500 }
501 _ => {}
502 }
503 }
504
505 fn nt_index_is_rom(&self, nt_index: usize) -> bool {
509 if self.extended_mirroring {
510 return false;
512 }
513 if !self.advanced_nt_control && self.board != JyBoard::M211 {
514 return false;
515 }
516 if self.nt_global {
517 true
519 } else {
520 (self.nt_low_regs[nt_index] & 0x80) != (self.nt_ram_select_bit & 0x80)
522 }
523 }
524
525 fn nt_rom_byte(&self, addr: u16) -> u8 {
527 let nt_index = ((addr & 0x0FFF) / NAMETABLE_SIZE_U16) as usize & 0x03;
528 let page =
529 self.nt_low_regs[nt_index] as usize | ((self.nt_high_regs[nt_index] as usize) << 8);
530 let off = page * NAMETABLE_SIZE + (addr as usize & 0x3FF);
531 if off < self.chr.len() {
532 self.chr[off]
533 } else {
534 0
535 }
536 }
537
538 fn tick_irq(&mut self) {
540 if self.irq_count_direction != 0x01 && self.irq_count_direction != 0x02 {
541 return; }
543 let mask: u8 = if self.irq_small_prescaler { 0x07 } else { 0xFF };
544 let mut prescaler = self.irq_prescaler & mask;
545 let mut clock_counter = false;
546 if self.irq_count_direction == 0x01 {
547 prescaler = prescaler.wrapping_add(1);
548 if (prescaler & mask) == 0 {
549 clock_counter = true;
550 }
551 } else {
552 prescaler = prescaler.wrapping_sub(1);
553 if (prescaler & mask) == mask {
554 clock_counter = true;
555 }
556 }
557 self.irq_prescaler = (self.irq_prescaler & !mask) | (prescaler & mask);
558
559 if clock_counter {
560 if self.irq_count_direction == 0x01 {
561 self.irq_counter = self.irq_counter.wrapping_add(1);
562 if self.irq_counter == 0 && self.irq_enabled {
563 self.irq_pending = true;
564 }
565 } else {
566 self.irq_counter = self.irq_counter.wrapping_sub(1);
567 if self.irq_counter == 0xFF && self.irq_enabled {
568 self.irq_pending = true;
569 }
570 }
571 }
572 }
573
574 fn irq_disable(&mut self) {
576 self.irq_enabled = false;
577 self.irq_pending = false;
578 self.irq_prescaler = 0;
579 }
580}
581
582impl Mapper for JyAsic {
583 fn chr_reads_are_pure(&self) -> bool {
586 false
587 }
588
589 fn caps(&self) -> MapperCaps {
591 MapperCaps::CYCLE_IRQ
592 }
593
594 fn cpu_read(&mut self, addr: u16) -> u8 {
595 match addr {
596 0x5000..=0x5FFF => match addr & 0xF803 {
597 0x5000 => 0,
599 0x5800 => (self.mul1 as u16 * self.mul2 as u16) as u8,
600 0x5801 => ((self.mul1 as u16 * self.mul2 as u16) >> 8) as u8,
601 0x5803 => self.test_reg,
602 _ => 0,
603 },
604 0x6000..=0x7FFF if self.enable_prg_at_6000 => {
605 let off = self.prg_offset(addr);
606 self.prg_rom[off % self.prg_rom.len()]
607 }
608 0x8000..=0xFFFF => {
609 let off = self.prg_offset(addr);
610 self.prg_rom[off % self.prg_rom.len()]
611 }
612 _ => 0,
613 }
614 }
615
616 fn cpu_read_unmapped(&self, addr: u16) -> bool {
617 match addr {
618 0x5000..=0x5FFF => false,
620 0x6000..=0x7FFF => !self.enable_prg_at_6000,
622 _ => (0x4020..0x5000).contains(&addr),
624 }
625 }
626
627 fn cpu_write(&mut self, addr: u16, value: u8) {
628 if addr < 0x8000 {
629 match addr & 0xF803 {
630 0x5800 => self.mul1 = value,
631 0x5801 => self.mul2 = value,
632 0x5803 => self.test_reg = value,
633 _ => {}
634 }
635 } else {
636 match addr & 0xF007 {
648 0x8000..=0x8007 => self.prg_regs[(addr & 0x03) as usize] = value & 0x7F,
652 0x9000..=0x9007 => self.chr_low_regs[(addr & 0x07) as usize] = value,
654 0xA000..=0xA007 => self.chr_high_regs[(addr & 0x07) as usize] = value,
656 0xB000..=0xB003 => self.nt_low_regs[(addr & 0x03) as usize] = value,
658 0xB004..=0xB007 => self.nt_high_regs[(addr & 0x03) as usize] = value,
660 0xC000 => {
662 if value & 0x01 != 0 {
663 self.irq_enabled = true;
664 } else {
665 self.irq_disable();
666 }
667 }
668 0xC001 => {
670 self.irq_count_direction = (value >> 6) & 0x03;
671 self.irq_small_prescaler = (value & 0x04) != 0;
672 self.irq_source = IrqSource::from_bits(value);
673 }
674 0xC002 => self.irq_disable(),
676 0xC003 => self.irq_enabled = true,
678 0xC004 => self.irq_prescaler = value ^ self.irq_xor,
680 0xC005 => self.irq_counter = value ^ self.irq_xor,
682 0xC006 => self.irq_xor = value,
684 0xC007 => self.irq_funky_reg = value,
686 0xD000 => {
688 self.prg_mode = value & 0x07;
689 self.chr_mode = (value >> 3) & 0x03;
690 self.advanced_nt_control = (value & 0x20) != 0;
691 self.nt_global = (value & 0x40) != 0;
692 self.enable_prg_at_6000 = (value & 0x80) != 0;
693 }
694 0xD001 => {
696 self.mirroring_reg = value & 0x03;
697 self.extended_mirroring = (value & 0x08) != 0;
698 }
699 0xD002 => self.nt_ram_select_bit = value & 0x80,
701 0xD003 => {
703 self.mirror_chr = (value & 0x80) != 0;
704 self.chr_block_mode = (value & 0x20) == 0;
705 self.chr_block = ((value & 0x18) >> 2) | (value & 0x01);
706 }
707 _ => {}
708 }
709 }
710 if self.irq_source == IrqSource::CpuWrite {
712 self.tick_irq();
713 }
714 }
715
716 fn ppu_read(&mut self, addr: u16) -> u8 {
717 let addr = addr & 0x1FFF;
722 if self.irq_source == IrqSource::PpuRead {
724 self.tick_irq();
725 }
726 self.update_chr_latch_209(addr);
727 let off = self.chr_offset(addr);
728 self.chr[off % self.chr.len()]
729 }
730
731 fn ppu_write(&mut self, addr: u16, value: u8) {
732 let addr = addr & 0x1FFF;
735 if self.chr_is_ram {
736 let off = self.chr_offset(addr);
737 let len = self.chr.len();
738 self.chr[off % len] = value;
739 }
740 }
741
742 fn nametable_fetch(&mut self, addr: u16) -> Option<u8> {
743 let masked = addr & 0x2FFF;
747 let nt_index = ((masked - 0x2000) / NAMETABLE_SIZE_U16) as usize & 0x03;
748 if self.nt_control_active() && self.nt_index_is_rom(nt_index) {
749 Some(self.nt_rom_byte(masked))
750 } else {
751 None
752 }
753 }
754
755 fn nametable_write(&mut self, addr: u16, _value: u8) -> bool {
756 let masked = addr & 0x2FFF;
760 let nt_index = ((masked - 0x2000) / NAMETABLE_SIZE_U16) as usize & 0x03;
761 self.nt_control_active() && self.nt_index_is_rom(nt_index)
762 }
763
764 fn notify_a12(&mut self, level: bool) {
765 if self.irq_source == IrqSource::PpuA12Rise && level && (self.last_ppu_addr & 0x1000) == 0 {
767 self.tick_irq();
768 }
769 self.last_ppu_addr = if level { 0x1000 } else { 0x0000 };
770 }
771
772 fn notify_cpu_cycle(&mut self) {
773 if self.irq_source == IrqSource::CpuClock {
774 self.tick_irq();
775 }
776 }
777
778 fn irq_pending(&self) -> bool {
779 self.irq_pending
780 }
781
782 fn current_mirroring(&self) -> Mirroring {
783 match self.mirroring_reg & 0x03 {
786 0 => Mirroring::Vertical,
787 1 => Mirroring::Horizontal,
788 2 => Mirroring::SingleScreenA,
789 _ => Mirroring::SingleScreenB,
790 }
791 }
792
793 fn nametable_address(&self, addr: u16) -> u16 {
794 (self.nametable_offset(addr) & 0x07FF) as u16
795 }
796
797 fn debug_info(&self) -> crate::mapper::MapperDebugInfo {
798 let mut info = crate::mapper::MapperDebugInfo {
799 mapper_id: self.board.mapper_id(),
800 name: match self.board {
801 JyBoard::M90 => "J.Y. Company ASIC (90)".into(),
802 JyBoard::M209 => "J.Y. Company ASIC (209)".into(),
803 JyBoard::M211 => "J.Y. Company ASIC (211)".into(),
804 JyBoard::M35 => "J.Y. Company ASIC (35)".into(),
805 },
806 mirroring: crate::mapper::mirroring_name(self.current_mirroring()),
807 ..Default::default()
808 };
809 for (i, b) in self.prg_regs.iter().enumerate() {
810 info.prg_banks.push((format!("P{i}"), format!("{b:#04x}")));
811 }
812 info.prg_banks
813 .push(("mode".into(), format!("{:#04x}", self.prg_mode)));
814 for i in 0..8 {
815 info.chr_banks
816 .push((format!("C{i}"), format!("{:#06x}", self.chr_reg(i))));
817 }
818 info.chr_banks
819 .push(("mode".into(), format!("{}", self.chr_mode)));
820 info.irq_state
821 .push(("source".into(), format!("{:?}", self.irq_source)));
822 info.irq_state
823 .push(("prescaler".into(), format!("{:#04x}", self.irq_prescaler)));
824 info.irq_state
825 .push(("counter".into(), format!("{:#04x}", self.irq_counter)));
826 info.irq_state
827 .push(("enabled".into(), format!("{}", self.irq_enabled)));
828 info.irq_state
829 .push(("pending".into(), format!("{}", self.irq_pending)));
830 info.extra
831 .push(("ntROM".into(), format!("{}", self.nt_control_active())));
832 info
833 }
834
835 fn save_state(&self) -> Vec<u8> {
836 let mut out = Vec::with_capacity(64 + if self.chr_is_ram { self.chr.len() } else { 0 });
837 out.push(SAVE_STATE_VERSION);
838 out.extend_from_slice(&self.prg_regs);
839 out.extend_from_slice(&self.chr_low_regs);
840 out.extend_from_slice(&self.chr_high_regs);
841 out.extend_from_slice(&self.nt_low_regs);
842 out.extend_from_slice(&self.nt_high_regs);
843 out.extend_from_slice(&self.chr_latch);
844 out.push(self.prg_mode);
845 out.push(u8::from(self.enable_prg_at_6000));
846 out.push(self.chr_mode);
847 out.push(u8::from(self.advanced_nt_control));
848 out.push(u8::from(self.nt_global));
849 out.push(self.mirroring_reg);
850 out.push(u8::from(self.extended_mirroring));
851 out.push(self.nt_ram_select_bit);
852 out.push(u8::from(self.chr_block_mode));
853 out.push(self.chr_block);
854 out.push(u8::from(self.mirror_chr));
855 out.push(u8::from(self.irq_enabled));
856 out.push(self.irq_source.to_bits());
857 out.push(self.irq_count_direction);
858 out.push(u8::from(self.irq_small_prescaler));
859 out.push(self.irq_prescaler);
860 out.push(self.irq_counter);
861 out.push(self.irq_xor);
862 out.push(self.irq_funky_reg);
863 out.push(u8::from(self.irq_pending));
864 out.push(self.mul1);
865 out.push(self.mul2);
866 out.push(self.test_reg);
867 out.extend_from_slice(&self.last_ppu_addr.to_le_bytes());
868 if self.chr_is_ram {
869 out.extend_from_slice(&self.chr);
870 }
871 out
872 }
873
874 fn load_state(&mut self, data: &[u8]) -> Result<(), MapperError> {
875 let chr_part = if self.chr_is_ram { self.chr.len() } else { 0 };
876 let scalar_len = 1 + 4 + 8 + 8 + 4 + 4 + 2 + 23 + 2;
879 let expected = scalar_len + chr_part;
880 if data.len() != expected {
881 return Err(MapperError::WrongLength {
882 expected,
883 got: data.len(),
884 });
885 }
886 if data[0] != SAVE_STATE_VERSION {
887 return Err(MapperError::UnsupportedVersion(data[0]));
888 }
889 let mut c = 1usize;
890 let mut take = |n: usize| {
891 let s = &data[c..c + n];
892 c += n;
893 s
894 };
895 self.prg_regs.copy_from_slice(take(4));
896 self.chr_low_regs.copy_from_slice(take(8));
897 self.chr_high_regs.copy_from_slice(take(8));
898 self.nt_low_regs.copy_from_slice(take(4));
899 self.nt_high_regs.copy_from_slice(take(4));
900 self.chr_latch.copy_from_slice(take(2));
901 self.chr_latch[0] &= 0x07;
906 self.chr_latch[1] &= 0x07;
907 self.prg_mode = take(1)[0];
908 self.enable_prg_at_6000 = take(1)[0] != 0;
909 self.chr_mode = take(1)[0];
910 self.advanced_nt_control = take(1)[0] != 0;
911 self.nt_global = take(1)[0] != 0;
912 self.mirroring_reg = take(1)[0];
913 self.extended_mirroring = take(1)[0] != 0;
914 self.nt_ram_select_bit = take(1)[0];
915 self.chr_block_mode = take(1)[0] != 0;
916 self.chr_block = take(1)[0];
917 self.mirror_chr = take(1)[0] != 0;
918 self.irq_enabled = take(1)[0] != 0;
919 self.irq_source = IrqSource::from_bits(take(1)[0]);
920 self.irq_count_direction = take(1)[0];
921 self.irq_small_prescaler = take(1)[0] != 0;
922 self.irq_prescaler = take(1)[0];
923 self.irq_counter = take(1)[0];
924 self.irq_xor = take(1)[0];
925 self.irq_funky_reg = take(1)[0];
926 self.irq_pending = take(1)[0] != 0;
927 self.mul1 = take(1)[0];
928 self.mul2 = take(1)[0];
929 self.test_reg = take(1)[0];
930 let la = take(2);
931 self.last_ppu_addr = u16::from_le_bytes([la[0], la[1]]);
932 if self.chr_is_ram {
933 let n = self.chr.len();
934 self.chr.copy_from_slice(take(n));
935 }
936 Ok(())
937 }
938}
939
940#[cfg(test)]
941#[allow(clippy::cast_possible_truncation)]
942mod tests {
943 use super::*;
944
945 fn synth_prg(banks_8k: usize) -> Box<[u8]> {
946 let mut v = vec![0u8; banks_8k * PRG_BANK_8K];
947 for b in 0..banks_8k {
948 v[b * PRG_BANK_8K] = b as u8;
949 }
950 v.into_boxed_slice()
951 }
952
953 fn synth_chr(banks_1k: usize) -> Box<[u8]> {
954 let mut v = vec![0u8; banks_1k * CHR_BANK_1K];
955 for b in 0..banks_1k {
956 v[b * CHR_BANK_1K] = b as u8;
957 }
958 v.into_boxed_slice()
959 }
960
961 fn fresh(board: JyBoard) -> JyAsic {
962 JyAsic::new(synth_prg(64), synth_chr(256), Mirroring::Vertical, board).unwrap()
965 }
966
967 #[test]
968 fn prg_mode2_8k_banks_select_each_window() {
969 let mut m = fresh(JyBoard::M209);
970 m.cpu_write(0xD000, 0x02); m.cpu_write(0x8000, 5);
972 m.cpu_write(0x8001, 6);
973 m.cpu_write(0x8002, 7);
974 assert_eq!(m.cpu_read(0x8000), 5);
975 assert_eq!(m.cpu_read(0xA000), 6);
976 assert_eq!(m.cpu_read(0xC000), 7);
977 assert_eq!(m.cpu_read(0xE000), 0x3F);
979 }
980
981 #[test]
982 fn prg_mode2_last_bank_switchable() {
983 let mut m = fresh(JyBoard::M209);
984 m.cpu_write(0xD000, 0x06); m.cpu_write(0x8003, 9);
986 assert_eq!(m.cpu_read(0xE000), 9);
987 }
988
989 #[test]
990 fn prg_mode0_32k_uses_fixed_3c_window() {
991 let mut m = fresh(JyBoard::M209);
992 m.cpu_write(0xD000, 0x00); assert_eq!(m.cpu_read(0x8000), 0x3C);
994 assert_eq!(m.cpu_read(0xA000), 0x3D);
995 assert_eq!(m.cpu_read(0xC000), 0x3E);
996 assert_eq!(m.cpu_read(0xE000), 0x3F);
997 }
998
999 #[test]
1000 fn prg_mode1_16k_low_and_high_windows() {
1001 let mut m = fresh(JyBoard::M209);
1002 m.cpu_write(0xD000, 0x01); m.cpu_write(0x8001, 2); assert_eq!(m.cpu_read(0x8000), 4);
1005 assert_eq!(m.cpu_read(0xA000), 5);
1006 assert_eq!(m.cpu_read(0xC000), 0x3E);
1007 assert_eq!(m.cpu_read(0xE000), 0x3F);
1008 }
1009
1010 #[test]
1011 fn prg_bits_reversed_in_mode3() {
1012 let mut m = fresh(JyBoard::M209);
1013 m.cpu_write(0xD000, 0x03); m.cpu_write(0x8000, 0x01);
1016 assert_eq!(m.cpu_read(0x8000), 0x40 % 64);
1017 }
1018
1019 #[test]
1020 fn prg_mode3_bit3_matches_mesen2() {
1021 assert_eq!(JyAsic::invert_prg_bits(0x08, true), 0x00);
1026 assert_eq!(JyAsic::invert_prg_bits(0x01, true), 0x40); assert_eq!(JyAsic::invert_prg_bits(0x40, true), 0x01); assert_eq!(JyAsic::invert_prg_bits(0x02, true), 0x20); assert_eq!(JyAsic::invert_prg_bits(0x04, true), 0x10); assert_eq!(JyAsic::invert_prg_bits(0x09, true), 0x40);
1034 assert_eq!(JyAsic::invert_prg_bits(0x7F, false), 0x7F);
1036 }
1037
1038 #[test]
1039 fn prg_register_decode_ignores_a2_accepts_a11() {
1040 let mut m = fresh(JyBoard::M209);
1044 m.cpu_write(0xD000, 0x02); m.cpu_write(0x8004, 5); assert_eq!(m.cpu_read(0x8000), 5, "$8004 must alias the $8000 register");
1047 m.cpu_write(0x8800, 9);
1049 assert_eq!(
1050 m.cpu_read(0x8000),
1051 9,
1052 "$8800 must still write the $8000 reg"
1053 );
1054 }
1055
1056 #[test]
1057 fn prg_at_6000_when_enabled() {
1058 let mut m = fresh(JyBoard::M209);
1059 m.cpu_write(0xD000, 0x82); m.cpu_write(0x8003, 4);
1061 assert!(!m.cpu_read_unmapped(0x6000));
1062 assert_eq!(m.cpu_read(0x6000), 4);
1063 }
1064
1065 #[test]
1066 fn chr_8k_mode() {
1067 let mut m = fresh(JyBoard::M209);
1068 m.cpu_write(0xD000, 0x00); m.cpu_write(0x9000, 3); assert_eq!(m.ppu_read(0x0000), 24);
1071 assert_eq!(m.ppu_read(0x0400), 25);
1072 }
1073
1074 #[test]
1075 fn chr_1k_mode_each_slot() {
1076 let mut m = fresh(JyBoard::M209);
1077 m.cpu_write(0xD000, 0x18); m.cpu_write(0x9000, 10);
1079 m.cpu_write(0x9007, 20);
1080 assert_eq!(m.ppu_read(0x0000), 10);
1081 assert_eq!(m.ppu_read(0x1C00), 20);
1082 }
1083
1084 #[test]
1085 fn chr_high_byte_extends_bank() {
1086 let mut m = fresh(JyBoard::M209);
1087 m.cpu_write(0xD000, 0x18); m.cpu_write(0x9000, 0x05);
1089 m.cpu_write(0xA000, 0x01); assert_eq!(m.ppu_read(0x0000), (0x105 % 256) as u8);
1091 }
1092
1093 #[test]
1094 fn chr_block_mode_outer_bank() {
1095 let mut m = fresh(JyBoard::M209);
1096 m.cpu_write(0xD000, 0x18); m.cpu_write(0xD003, 0x01); m.cpu_write(0x9000, 0x00);
1100 assert_eq!(m.ppu_read(0x0000), 0);
1102 }
1103
1104 #[test]
1105 fn mirroring_register_select() {
1106 let mut m = fresh(JyBoard::M90);
1107 m.cpu_write(0xD001, 0x00);
1108 assert_eq!(m.current_mirroring(), Mirroring::Vertical);
1109 m.cpu_write(0xD001, 0x01);
1110 assert_eq!(m.current_mirroring(), Mirroring::Horizontal);
1111 m.cpu_write(0xD001, 0x02);
1112 assert_eq!(m.current_mirroring(), Mirroring::SingleScreenA);
1113 m.cpu_write(0xD001, 0x03);
1114 assert_eq!(m.current_mirroring(), Mirroring::SingleScreenB);
1115 }
1116
1117 #[test]
1118 fn mapper90_inhibits_rom_nametables() {
1119 let mut m = fresh(JyBoard::M90);
1120 m.cpu_write(0xD000, 0x60); assert!(!m.nt_control_active());
1123 }
1124
1125 #[test]
1126 fn mapper211_forces_rom_nametables() {
1127 let m = fresh(JyBoard::M211);
1128 assert!(m.nt_control_active());
1130 }
1131
1132 #[test]
1133 fn mapper209_rom_nametable_global_reads_chr() {
1134 let mut m = fresh(JyBoard::M209);
1135 m.cpu_write(0xD000, 0x60); m.cpu_write(0xB000, 7); assert_eq!(m.nametable_fetch(0x2000), Some(7));
1141 }
1142
1143 #[test]
1144 fn mapper209_ciram_nametable_returns_none() {
1145 let mut m = fresh(JyBoard::M209);
1148 assert_eq!(m.nametable_fetch(0x2000), None);
1149 }
1150
1151 #[test]
1152 fn extended_mirroring_selects_per_nt_ciram_page() {
1153 let mut m = fresh(JyBoard::M209);
1157 m.cpu_write(0xD001, 0x08); assert!(m.nt_control_active());
1159 for table in 0..4u16 {
1161 let addr = 0x2000 + table * 0x400;
1162 assert_eq!(
1163 m.nametable_address(addr) & 0x0400,
1164 0,
1165 "table {table} page 0"
1166 );
1167 }
1168 m.cpu_write(0xB001, 0x01);
1170 m.cpu_write(0xB003, 0x01);
1171 assert_eq!(m.nametable_address(0x2000) & 0x0400, 0x000); assert_eq!(m.nametable_address(0x2400) & 0x0400, 0x400); assert_eq!(m.nametable_address(0x2800) & 0x0400, 0x000); assert_eq!(m.nametable_address(0x2C00) & 0x0400, 0x400); }
1176
1177 #[test]
1178 fn extended_mirroring_off_uses_mm_register() {
1179 let mut m = fresh(JyBoard::M209);
1183 m.cpu_write(0xD001, 0x00); assert!(!m.nt_control_active());
1185 assert_eq!(m.nametable_address(0x2000) & 0x0400, 0x000); assert_eq!(m.nametable_address(0x2400) & 0x0400, 0x400); assert_eq!(m.nametable_address(0x2800) & 0x0400, 0x000); assert_eq!(m.nametable_address(0x2C00) & 0x0400, 0x400); }
1190
1191 #[test]
1192 fn rom_nametable_write_is_absorbed() {
1193 let mut m = fresh(JyBoard::M209);
1196 m.cpu_write(0xD000, 0x60); assert!(m.nametable_write(0x2000, 0x42)); let mut m2 = fresh(JyBoard::M209); assert!(!m2.nametable_write(0x2000, 0x42)); }
1201
1202 #[test]
1203 fn irq_cpu_clock_increment_wraps_to_assert() {
1204 let mut m = fresh(JyBoard::M209);
1205 m.cpu_write(0xC006, 0x00); m.cpu_write(0xC005, 0xFF); m.cpu_write(0xC004, 0xFF); m.cpu_write(0xC001, 0x40);
1211 m.cpu_write(0xC000, 0x01); m.notify_cpu_cycle();
1214 assert!(m.irq_pending());
1215 }
1216
1217 #[test]
1218 fn irq_decrement_underflow_asserts() {
1219 let mut m = fresh(JyBoard::M209);
1220 m.cpu_write(0xC006, 0x00);
1221 m.cpu_write(0xC005, 0x00); m.cpu_write(0xC004, 0x00); m.cpu_write(0xC001, 0x80); m.cpu_write(0xC000, 0x01); m.notify_cpu_cycle();
1227 assert!(m.irq_pending());
1228 }
1229
1230 #[test]
1231 fn irq_small_prescaler_mask() {
1232 let mut m = fresh(JyBoard::M209);
1233 m.cpu_write(0xC006, 0x00);
1234 m.cpu_write(0xC005, 0xFF); m.cpu_write(0xC004, 0x07); m.cpu_write(0xC001, 0x44);
1238 m.cpu_write(0xC000, 0x01);
1239 m.notify_cpu_cycle(); assert!(m.irq_pending());
1241 }
1242
1243 #[test]
1244 fn irq_xor_applied_to_counter_and_prescaler() {
1245 let mut m = fresh(JyBoard::M209);
1246 m.cpu_write(0xC006, 0x0F); m.cpu_write(0xC005, 0xF0); m.cpu_write(0xC001, 0x40); m.cpu_write(0xC004, 0xF0); m.cpu_write(0xC000, 0x01);
1251 m.notify_cpu_cycle();
1252 assert!(m.irq_pending());
1253 }
1254
1255 #[test]
1256 fn irq_disable_acknowledges_and_resets_prescaler() {
1257 let mut m = fresh(JyBoard::M209);
1258 m.irq_pending = true;
1259 m.irq_prescaler = 0x42;
1260 m.cpu_write(0xC000, 0x00); assert!(!m.irq_pending());
1262 assert_eq!(m.irq_prescaler, 0);
1263 m.irq_pending = true;
1264 m.cpu_write(0xC002, 0x00); assert!(!m.irq_pending());
1266 }
1267
1268 #[test]
1269 fn irq_source_cpu_write_ticks_on_write() {
1270 let mut m = fresh(JyBoard::M209);
1271 m.cpu_write(0xC006, 0x00);
1272 m.cpu_write(0xC001, 0x43);
1277 m.cpu_write(0xC000, 0x01); let before = m.irq_prescaler;
1279 m.cpu_write(0x5803, 0x00); assert_eq!(m.irq_prescaler, before.wrapping_add(1));
1281 }
1282
1283 #[test]
1284 fn irq_source_cpu_clock_does_not_tick_on_write() {
1285 let mut m = fresh(JyBoard::M209);
1286 m.cpu_write(0xC006, 0x00);
1287 m.cpu_write(0xC001, 0x40); m.cpu_write(0xC000, 0x01);
1289 let before = m.irq_prescaler;
1290 m.cpu_write(0x5803, 0x00); assert_eq!(m.irq_prescaler, before);
1292 }
1293
1294 #[test]
1295 fn irq_a12_rise_source_ticks_on_rising_edge() {
1296 let mut m = fresh(JyBoard::M209);
1297 m.cpu_write(0xC006, 0x00);
1298 m.cpu_write(0xC005, 0xFF);
1299 m.cpu_write(0xC004, 0xFF);
1300 m.cpu_write(0xC001, 0x41); m.cpu_write(0xC000, 0x01);
1302 m.notify_a12(false); assert!(!m.irq_pending());
1304 m.notify_a12(true); assert!(m.irq_pending());
1306 }
1307
1308 #[test]
1309 fn mapper209_mmc4_latch_switches_chr() {
1310 let mut m = fresh(JyBoard::M209);
1311 m.cpu_write(0xD000, 0x08); m.ppu_read(0x0FE8);
1314 assert_eq!(m.chr_latch[0], 2);
1317 }
1318
1319 #[test]
1320 fn mapper90_no_mmc4_latch() {
1321 let mut m = fresh(JyBoard::M90);
1322 let before = m.chr_latch;
1323 m.ppu_read(0x0FE8);
1324 assert_eq!(m.chr_latch, before); }
1326
1327 #[test]
1328 fn multiplier_register() {
1329 let mut m = fresh(JyBoard::M209);
1330 m.cpu_write(0x5800, 6);
1331 m.cpu_write(0x5801, 7);
1332 assert_eq!(m.cpu_read(0x5800), 42); assert_eq!(m.cpu_read(0x5801), 0); m.cpu_write(0x5800, 0xFF);
1335 m.cpu_write(0x5801, 0xFF);
1336 assert_eq!(m.cpu_read(0x5800), 0x01); assert_eq!(m.cpu_read(0x5801), 0xFE); }
1339
1340 #[test]
1341 fn test_register_read_write() {
1342 let mut m = fresh(JyBoard::M209);
1343 m.cpu_write(0x5803, 0x5A);
1344 assert_eq!(m.cpu_read(0x5803), 0x5A);
1345 }
1346
1347 #[test]
1348 fn save_state_round_trip() {
1349 for board in [JyBoard::M90, JyBoard::M209, JyBoard::M211] {
1350 let mut m = fresh(board);
1351 m.cpu_write(0xD000, 0x9A); m.cpu_write(0x8000, 3);
1353 m.cpu_write(0x8003, 9);
1354 m.cpu_write(0x9000, 0x11);
1355 m.cpu_write(0xA000, 0x01);
1356 m.cpu_write(0xB000, 4);
1357 m.cpu_write(0xB004, 1);
1358 m.cpu_write(0xD001, 0x09); m.cpu_write(0xD003, 0xA1);
1360 m.cpu_write(0xC006, 0x0F);
1361 m.cpu_write(0xC005, 0x12);
1362 m.cpu_write(0xC004, 0x34);
1363 m.cpu_write(0xC001, 0x41);
1364 m.cpu_write(0xC000, 0x01);
1365 m.ppu_read(0x0FE8); let blob = m.save_state();
1368 let mut m2 = fresh(board);
1369 m2.load_state(&blob).unwrap();
1370
1371 assert_eq!(m.cpu_read(0x8000), m2.cpu_read(0x8000));
1372 assert_eq!(m.cpu_read(0xE000), m2.cpu_read(0xE000));
1373 assert_eq!(m.ppu_read(0x0000), m2.ppu_read(0x0000));
1374 assert_eq!(m.chr_latch, m2.chr_latch);
1375 assert_eq!(m.irq_counter, m2.irq_counter);
1376 assert_eq!(m.irq_prescaler, m2.irq_prescaler);
1377 assert_eq!(m.current_mirroring(), m2.current_mirroring());
1378 assert_eq!(m.test_reg, m2.test_reg);
1379 }
1380 }
1381
1382 #[test]
1383 fn save_state_rejects_bad_version() {
1384 let mut m = fresh(JyBoard::M209);
1385 let mut blob = m.save_state();
1386 blob[0] = 0xFF;
1387 assert!(matches!(
1388 m.load_state(&blob),
1389 Err(MapperError::UnsupportedVersion(0xFF))
1390 ));
1391 }
1392
1393 #[test]
1394 fn save_state_rejects_truncated() {
1395 let mut m = fresh(JyBoard::M209);
1396 let blob = m.save_state();
1397 assert!(matches!(
1398 m.load_state(&blob[..blob.len() - 1]),
1399 Err(MapperError::WrongLength { .. })
1400 ));
1401 }
1402
1403 #[test]
1404 fn load_state_clamps_chr_latch() {
1405 let mut m = fresh(JyBoard::M209);
1410 let mut blob = m.save_state();
1411 let latch_off = 1 + 4 + 8 + 8 + 4 + 4;
1414 blob[latch_off] = 0xFF;
1415 blob[latch_off + 1] = 0xFE;
1416 m.load_state(&blob).unwrap();
1417 assert!(m.chr_latch[0] < 8);
1418 assert!(m.chr_latch[1] < 8);
1419 assert_eq!(m.chr_latch[0], 0x07);
1420 assert_eq!(m.chr_latch[1], 0x06);
1421 m.cpu_write(0xD000, 0x08); let _ = m.ppu_read(0x0000);
1424 let _ = m.ppu_read(0x1000);
1425 }
1426}