1//! SPC7110 coprocessor, a data decompression chip (optionally with an Epson RTC-4513 real-time clock chip)
2//!
3//! Used by Tengai Makyou Zero, Momotarou Dentetsu Happy, and Super Power League 4
4
5mod decompressor;
6mod registers;
7mod rtc;
8
9use crate::common::{Rom, impl_take_set_rom};
10use crate::spc7110::decompressor::Spc7110Decompressor;
11use crate::spc7110::registers::Registers;
12use crate::spc7110::rtc::Rtc4513;
13use bincode::{Decode, Encode};
14use jgenesis_common::frontend::SaveWriter;
15use jgenesis_common::num::{U16Ext, U24Ext};
16use jgenesis_proc_macros::PartialClone;
17
18// All 3 SPC7110 game images have a 1MB program ROM followed by a data ROM
19const DATA_ROM_START: usize = 0x100000;
20
21// All 3 SPC7110 games have 8KB of SRAM
22const SRAM_LEN: usize = 8 * 1024;
23
24type Sram = [u8; SRAM_LEN];
25
26#[derive(Debug, Clone, PartialClone, Encode, Decode)]
27pub struct Spc7110 {
28    #[partial_clone(default)]
29    rom: Rom,
30    sram: Box<Sram>,
31    registers: Registers,
32    decompressor: Spc7110Decompressor,
33    rtc: Option<Rtc4513>,
34}
35
36impl Spc7110 {
37    #[allow(clippy::boxed_local)]
38    #[allow(clippy::missing_panics_doc)]
39    #[must_use]
40    pub fn new<S: SaveWriter>(
41        rom: Box<[u8]>,
42        initial_sram: Box<[u8]>,
43        save_writer: &mut S,
44    ) -> Self {
45        let sram = if initial_sram.len() == SRAM_LEN {
46            initial_sram.try_into().unwrap()
47        } else {
48            vec![0; SRAM_LEN].into_boxed_slice().try_into().unwrap()
49        };
50
51        // Chipset byte of $F9 indicates SPC7110 + RTC-4513 (only used by Tengai Makyou Zero)
52        let has_rtc = rom[0xFFD6] == 0xF9;
53        let rtc =
54            has_rtc.then(|| save_writer.load_serialized("rtc").ok().unwrap_or_else(Rtc4513::new));
55
56        log::info!("SPC7110 has RTC-4513: {}", rtc.is_some());
57
58        Self {
59            rom: Rom(rom),
60            sram,
61            registers: Registers::new(),
62            decompressor: Spc7110Decompressor::new(),
63            rtc,
64        }
65    }
66
67    #[inline]
68    #[must_use]
69    pub fn read(&mut self, address: u32) -> Option<u8> {
70        let bank = (address >> 16) & 0xFF;
71        let offset = address & 0xFFFF;
72        match (bank, offset) {
73            (0x00..=0x3F | 0x80..=0xBF, 0x4800..=0x4842) => {
74                // SPC7110 internal registers
75                self.read_register(address)
76            }
77            (0x00..=0x0F | 0x80..=0x8F, 0x8000..=0xFFFF) | (0xC0..=0xCF, _) => {
78                // Program ROM (1MB)
79                let rom_addr = address & 0x0FFFFF;
80                Some(self.rom[rom_addr as usize])
81            }
82            (0x40..=0x4F, _) => {
83                // Expansion ROM (1MB) if applicable, otherwise mirror program ROM
84                // This is needed for the fan translated version of Tengai Makyou Zero which stores
85                // an additional 1MB of ROM at the end of the 7MB file and expects it to be mapped
86                // to banks $40-$4F
87                Some(if self.rom.len() >= 0x700000 {
88                    self.rom[0x600000 | (address & 0xFFFFF) as usize]
89                } else {
90                    self.rom[(address & 0xFFFFF) as usize]
91                })
92            }
93            (0xD0..=0xDF, _) => {
94                // Data ROM bank D
95                let rom_addr = (address & 0x0FFFFF) | (u32::from(self.registers.rom_bank_d) << 20);
96                self.rom[DATA_ROM_START..].get(rom_addr as usize).copied()
97            }
98            (0xE0..=0xEF, _) => {
99                // Data ROM bank E
100                let rom_addr = (address & 0x0FFFFF) | (u32::from(self.registers.rom_bank_e) << 20);
101                self.rom[DATA_ROM_START..].get(rom_addr as usize).copied()
102            }
103            (0xF0..=0xFF, _) => {
104                // Data ROM bank F
105                let rom_addr = (address & 0x0FFFFF) | (u32::from(self.registers.rom_bank_f) << 20);
106                self.rom[DATA_ROM_START..].get(rom_addr as usize).copied()
107            }
108            (0x00..=0x3F | 0x80..=0xBF, 0x6000..=0x7FFF) => {
109                // SRAM (always 8KB)
110                self.registers.sram_enabled.then(|| {
111                    let sram_addr = address & 0x1FFF;
112                    self.sram[sram_addr as usize]
113                })
114            }
115            (0x50, _) => {
116                // Alternate mapping for internal register $4800 (read next decompressed byte)
117                Some(self.decompressor.next_byte(&self.rom[DATA_ROM_START..]))
118            }
119            _ => None,
120        }
121    }
122
123    #[inline]
124    pub fn write(&mut self, address: u32, value: u8) {
125        let bank = (address >> 16) & 0xFF;
126        let offset = address & 0xFFFF;
127        match (bank, offset) {
128            (0x00..=0x3F | 0x80..=0xBF, 0x4800..=0x4842) => {
129                // SPC7110 internal registers
130                self.write_register(address, value);
131            }
132            (0x00..=0x3F | 0x80..=0xBF, 0x6000..=0x7FFF) if self.registers.sram_enabled => {
133                // SRAM (always 8KB)
134                let sram_addr = address & 0x1FFF;
135                self.sram[sram_addr as usize] = value;
136            }
137            _ => {}
138        }
139    }
140
141    impl_take_set_rom!(rom);
142
143    #[inline]
144    #[must_use]
145    pub fn sram(&self) -> &[u8] {
146        self.sram.as_ref()
147    }
148
149    #[inline]
150    #[must_use]
151    pub fn rtc(&self) -> Option<&Rtc4513> {
152        self.rtc.as_ref()
153    }
154
155    #[allow(clippy::match_same_arms)]
156    fn read_register(&mut self, address: u32) -> Option<u8> {
157        log::trace!("SPC7110 register read {:04X}", address & 0xFFFF);
158
159        match address & 0xFFFF {
160            0x4800 => Some(self.decompressor.next_byte(&self.rom[DATA_ROM_START..])),
161            0x4801 => Some(self.decompressor.rom_directory_base.low_byte()),
162            0x4802 => Some(self.decompressor.rom_directory_base.mid_byte()),
163            0x4803 => Some(self.decompressor.rom_directory_base.high_byte()),
164            0x4804 => Some(self.decompressor.rom_directory_index),
165            0x4805 => Some(self.decompressor.target_offset.lsb()),
166            0x4806 => Some(self.decompressor.target_offset.msb()),
167            // $4807: Unknown functionality, reportedly DMA channel?
168            0x4807 => Some(0x00),
169            // $4808: Unknown functionality, reportedly "C r/w option"?
170            0x4808 => Some(0x00),
171            0x480B => Some(self.decompressor.read_mode()),
172            0x480C => Some(self.decompressor.read_status()),
173            0x4809 => Some(self.decompressor.length_counter.lsb()),
174            0x480A => Some(self.decompressor.length_counter.msb()),
175            0x4810 => Some(self.registers.read_direct_data_rom_r4810(&self.rom[DATA_ROM_START..])),
176            0x4811 => Some(self.registers.direct_data_rom_base.low_byte()),
177            0x4812 => Some(self.registers.direct_data_rom_base.mid_byte()),
178            0x4813 => Some(self.registers.direct_data_rom_base.high_byte()),
179            0x4814 => Some(self.registers.direct_data_rom_offset.lsb()),
180            0x4815 => Some(self.registers.direct_data_rom_offset.msb()),
181            0x4816 => Some(self.registers.direct_data_rom_step.lsb()),
182            0x4817 => Some(self.registers.direct_data_rom_step.msb()),
183            // $4818: Direct data ROM mode; reading from this register has unknown functionality
184            0x4818 => Some(0x00),
185            0x481A => Some(self.registers.read_direct_data_rom_r481a(&self.rom[DATA_ROM_START..])),
186            0x4820..=0x4823 => Some(self.registers.read_dividend(address)),
187            0x4824 => Some(self.registers.math.multiplier.lsb()),
188            0x4825 => Some(self.registers.math.multiplier.msb()),
189            0x4826 => Some(self.registers.math.divisor.lsb()),
190            0x4827 => Some(self.registers.math.divisor.msb()),
191            0x4828..=0x482B => Some(self.registers.read_math_result(address)),
192            0x482C => Some(self.registers.math.remainder.lsb()),
193            0x482D => Some(self.registers.math.remainder.msb()),
194            // $482E: Reportedly either "reset" or signed/unsigned toggle; games seem to only write $00
195            0x482E => Some(0x00),
196            // $482F: Multiplication/division unit status; hardcoded to 0 (ready)
197            0x482F => Some(0x00),
198            0x4830 => Some(self.registers.read_sram_enabled()),
199            0x4831 => Some(self.registers.rom_bank_d),
200            0x4832 => Some(self.registers.rom_bank_e),
201            0x4833 => Some(self.registers.rom_bank_f),
202            0x4834 => Some(self.registers.sram_bank),
203            0x4840 => self.rtc.as_ref().map(Rtc4513::read_chip_select),
204            0x4841 => self.rtc.as_mut().map(Rtc4513::read_data_port),
205            0x4842 => self.rtc.is_some().then_some(rtc::STATUS_BYTE),
206            _ => None,
207        }
208    }
209
210    #[allow(clippy::match_same_arms)]
211    fn write_register(&mut self, address: u32, value: u8) {
212        log::trace!("SPC7110 register write {:04X} {value:02X}", address & 0xFFFF);
213
214        match address & 0xFFFF {
215            0x4801 => self.decompressor.write_rom_directory_base_low(value),
216            0x4802 => self.decompressor.write_rom_directory_base_mid(value),
217            0x4803 => self.decompressor.write_rom_directory_base_high(value),
218            0x4804 => self.decompressor.rom_directory_index = value,
219            0x4805 => self.decompressor.write_target_offset_low(value),
220            0x4806 => {
221                self.decompressor.write_target_offset_high(value, &self.rom[DATA_ROM_START..]);
222            }
223            // $4807: Unknown functionality, reportedly DMA channel?
224            0x4807 => {}
225            // $4808: Unknown functionality, reportedly "C r/w option"?
226            0x4808 => {}
227            0x4809 => self.decompressor.write_length_counter_low(value),
228            0x480A => self.decompressor.write_length_counter_high(value),
229            0x480B => self.decompressor.write_mode(value),
230            0x4811 => self.registers.write_direct_data_rom_base_low(value),
231            0x4812 => self.registers.write_direct_data_rom_base_mid(value),
232            0x4813 => self.registers.write_direct_data_rom_base_high(value),
233            0x4814 => self.registers.write_direct_data_rom_offset_low(value),
234            0x4815 => self.registers.write_direct_data_rom_offset_high(value),
235            0x4816 => self.registers.write_direct_data_rom_step_low(value),
236            0x4817 => self.registers.write_direct_data_rom_step_high(value),
237            0x4818 => self.registers.write_direct_data_rom_mode(value),
238            0x4820..=0x4823 => self.registers.write_dividend(address, value),
239            0x4824 => self.registers.write_multiplier_low(value),
240            0x4825 => self.registers.write_multiplier_high(value),
241            0x4826 => self.registers.write_divisor_low(value),
242            0x4827 => self.registers.write_divisor_high(value),
243            0x482E => self.registers.write_math_mode(value),
244            0x4830 => self.registers.write_sram_enabled(value),
245            0x4831 => self.registers.rom_bank_d = value,
246            0x4832 => self.registers.rom_bank_e = value,
247            0x4833 => self.registers.rom_bank_f = value,
248            0x4834 => self.registers.sram_bank = value,
249            0x4840 => {
250                if let Some(rtc) = &mut self.rtc {
251                    rtc.write_chip_select(value);
252                }
253            }
254            0x4841 => {
255                if let Some(rtc) = &mut self.rtc {
256                    rtc.write_data_port(value);
257                }
258            }
259            _ => {}
260        }
261    }
262}