1//! SA-1 coprocessor, a second 65C816 CPU clocked at 10.74 MHz 2//! 3//! Used by ~35 games including Kirby Super Star, Kirby's Dream Land 3, Super Mario RPG 4 5mod bus; 6mod mmc; 7mod registers; 8mod timer; 9 10use crate::common; 11use crate::common::{Rom, impl_take_set_rom}; 12use crate::sa1::bus::Sa1Bus; 13use crate::sa1::mmc::Sa1Mmc; 14use crate::sa1::registers::{DmaState, Sa1Registers}; 15use crate::sa1::timer::Sa1Timer; 16use bincode::{Decode, Encode}; 17use jgenesis_common::frontend::TimingMode; 18use jgenesis_proc_macros::PartialClone; 19use std::cmp; 20use wdc65816_emu::core::Wdc65816; 21 22const IRAM_LEN: usize = 2 * 1024; 23 24type Iram = [u8; IRAM_LEN]; 25 26macro_rules! new_sa1_bus { 27 ($self:expr) => { 28 Sa1Bus { 29 rom: &$self.rom, 30 iram: &mut $self.iram, 31 bwram: &mut $self.bwram, 32 mmc: &mut $self.mmc, 33 registers: &mut $self.registers, 34 timer: &mut $self.timer, 35 bwram_wait_cycles: 0, 36 } 37 }; 38} 39 40#[derive(Debug, Clone, Encode, Decode, PartialClone)] 41pub struct Sa1 { 42 #[partial_clone(default)] 43 rom: Rom, 44 iram: Box<Iram>, 45 bwram: Box<[u8]>, 46 cpu: Wdc65816, 47 bwram_wait_cycles: u64, 48 mmc: Sa1Mmc, 49 registers: Sa1Registers, 50 timer: Sa1Timer, 51} 52 53impl Sa1 { 54 #[allow(clippy::missing_panics_doc)] 55 #[must_use] 56 pub fn new(rom: Box<[u8]>, mut sram: Box<[u8]>, timing_mode: TimingMode) -> Self { 57 if sram.is_empty() { 58 log::info!("SA-1 cartridge with no BW-RAM makes no sense; allocating 64KB of BW-RAM"); 59 sram = vec![0; 64 * 1024].into_boxed_slice(); 60 } 61 62 Self { 63 rom: Rom(rom), 64 iram: vec![0; IRAM_LEN].into_boxed_slice().try_into().unwrap(), 65 bwram: sram, 66 cpu: Wdc65816::new(), 67 bwram_wait_cycles: 0, 68 mmc: Sa1Mmc::new(), 69 registers: Sa1Registers::new(), 70 timer: Sa1Timer::new(timing_mode), 71 } 72 } 73 74 impl_take_set_rom!(rom); 75 76 #[inline] 77 #[must_use] 78 pub fn has_battery(&self) -> bool { 79 // Most SA-1 games have battery backup, but Dragon Ball Z: Hyper Dimension does not 80 // This is indicated by a chipset byte of $34 instead of $32 or $35 81 let chipset_byte = self.rom[common::LOROM_CHIPSET_BYTE_ADDRESS]; 82 !self.bwram.is_empty() && (chipset_byte == 0x32 || chipset_byte == 0x35) 83 } 84 85 #[inline] 86 #[must_use] 87 pub fn sram(&self) -> Option<&[u8]> { 88 (!self.bwram.is_empty()).then_some(self.bwram.as_ref()) 89 } 90 91 #[inline] 92 /// # Panics 93 /// 94 /// This method will panic if `master_cycles_elapsed` is not a multiple of 2. 95 pub fn tick(&mut self, master_cycles_elapsed: u64) { 96 assert_eq!(master_cycles_elapsed % 2, 0); 97 98 // 10.74 MHz clock, SNES mclk / 2 99 let sa1_cycles = master_cycles_elapsed / 2; 100 101 let spent_wait_cycles = cmp::min(sa1_cycles, self.bwram_wait_cycles); 102 let cpu_cycles = sa1_cycles - spent_wait_cycles; 103 self.bwram_wait_cycles -= spent_wait_cycles; 104 105 if !self.registers.cpu_halted() { 106 let mut bus = new_sa1_bus!(self); 107 for _ in 0..cpu_cycles { 108 self.cpu.tick(&mut bus); 109 } 110 self.bwram_wait_cycles += bus.bwram_wait_cycles; 111 } 112 113 if self.registers.dma_state != DmaState::Idle { 114 for _ in 0..sa1_cycles { 115 self.registers.tick_dma(&self.mmc, &self.rom, &mut self.iram, &mut self.bwram); 116 } 117 } 118 119 for _ in 0..sa1_cycles { 120 self.timer.tick(); 121 } 122 } 123 124 #[inline] 125 #[must_use] 126 pub fn snes_irq(&self) -> bool { 127 (self.registers.snes_irq_from_sa1_enabled && self.registers.snes_irq_from_sa1) 128 || (self.registers.snes_irq_from_dma_enabled && self.registers.character_conversion_irq) 129 } 130 131 pub fn reset(&mut self) { 132 self.registers.reset(&mut self.timer, &mut self.mmc); 133 } 134 135 #[inline] 136 pub fn notify_dma_start(&mut self, source_address: u32) { 137 self.registers.notify_snes_dma_start(source_address); 138 } 139 140 #[inline] 141 pub fn notify_dma_end(&mut self) { 142 self.registers.notify_snes_dma_end(); 143 } 144}