sa1.rsannotatedsa1.rssource144 lines · 4.1 KB · raw
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}