1#![allow(
29 clippy::doc_markdown,
30 clippy::similar_names,
31 clippy::cast_possible_truncation
32)]
33
34use alloc::boxed::Box;
35use alloc::vec;
36
37use rustysnes_savestate::{SaveReader, SaveStateError, SaveWriter};
38
39use crate::board::{Board, Coprocessor, MappedAddr};
40use crate::header::MapMode;
41
42mod decompressor;
43use decompressor::Decompressor;
44
45#[derive(Debug, Clone, Copy, Default)]
47struct DmaSnoop {
48 address: u32,
49 size: u16,
50}
51
52pub struct Sdd1Board {
54 rom: Box<[u8]>,
55 sram: Box<[u8]>,
56 r4800: u8, r4801: u8, r4804: u8, r4805: u8, r4806: u8, r4807: u8, dma: [DmaSnoop; 8],
63 dma_ready: bool,
64 decompressor: Decompressor,
65}
66
67impl core::fmt::Debug for Sdd1Board {
68 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
69 f.debug_struct("Sdd1Board")
70 .field("rom_len", &self.rom.len())
71 .field("r4800", &self.r4800)
72 .field("r4801", &self.r4801)
73 .finish_non_exhaustive()
74 }
75}
76
77impl Sdd1Board {
78 #[must_use]
80 pub fn new(rom: Box<[u8]>, sram_size: usize) -> Self {
81 Self {
82 rom,
83 sram: vec![0u8; sram_size].into_boxed_slice(),
84 r4800: 0,
85 r4801: 0,
86 r4804: 0x00,
87 r4805: 0x01,
88 r4806: 0x02,
89 r4807: 0x03,
90 dma: [DmaSnoop::default(); 8],
91 dma_ready: false,
92 decompressor: Decompressor::new(),
93 }
94 }
95
96 fn mmc_read(&self, address: u32) -> u8 {
99 let bank_reg = match (address >> 20) & 0x3 {
100 0 => self.r4804,
101 1 => self.r4805,
102 2 => self.r4806,
103 _ => self.r4807,
104 };
105 let off = (u32::from(bank_reg & 0xF) << 20) | (address & 0x0F_FFFF);
106 self.rom
107 .get((off as usize) % self.rom.len().max(1))
108 .copied()
109 .unwrap_or(0)
110 }
111
112 fn mcu_read(&mut self, address: u32) -> u8 {
117 let a = address & 0xFF_FFFF;
118 if a & 0x40_0000 == 0 {
119 let mut a = a;
121 if a & 0x80_0000 == 0 && a & 0x20_0000 != 0 && self.r4805 & 0x80 != 0 {
122 a &= !0x20_0000; }
124 if a & 0x80_0000 != 0 && a & 0x20_0000 != 0 && self.r4807 & 0x80 != 0 {
125 a &= !0x20_0000; }
127 let off = ((a >> 16) & 0x3F) << 15 | (a & 0x7FFF);
128 return self
129 .rom
130 .get((off as usize) % self.rom.len().max(1))
131 .copied()
132 .unwrap_or(0);
133 }
134
135 if self.r4800 & self.r4801 != 0 {
137 for n in 0..8usize {
138 let armed = self.r4800 & (1 << n) != 0 && self.r4801 & (1 << n) != 0;
139 if armed && self.dma[n].address & 0xFF_FFFF == a {
140 let mmc_map = self.mmc_map();
141 if !self.dma_ready {
142 self.decompressor.init(a, &self.rom, &mmc_map);
143 self.dma_ready = true;
144 }
145 let data = self.decompressor.read(&self.rom, &mmc_map);
146 self.dma[n].size = self.dma[n].size.wrapping_sub(1);
147 if self.dma[n].size == 0 {
148 self.dma_ready = false;
149 self.r4801 &= !(1 << n);
150 }
151 return data;
152 }
153 }
154 }
155 self.mmc_read(a)
156 }
157
158 const fn mmc_map(&self) -> [u8; 4] {
159 [self.r4804, self.r4805, self.r4806, self.r4807]
160 }
161}
162
163impl Board for Sdd1Board {
164 fn name(&self) -> &'static str {
165 "ExHiROM+S-DD1"
166 }
167
168 fn coprocessor(&self) -> Coprocessor {
169 Coprocessor::SDd1
170 }
171
172 fn map(&self, addr24: u32) -> MappedAddr {
173 let a = addr24 & 0xFF_FFFF;
174 let bank = (a >> 16) & 0xFF;
175 let addr = a & 0xFFFF;
176 let in_ctrl = matches!(bank, 0x00..=0x3F | 0x80..=0xBF)
177 && matches!(addr, 0x4800 | 0x4801 | 0x4804..=0x4807);
178 if in_ctrl {
179 MappedAddr::Coprocessor
180 } else {
181 MappedAddr::Rom(a) }
183 }
184
185 fn read24(&mut self, addr24: u32) -> u8 {
186 let a = addr24 & 0xFF_FFFF;
187 let bank = (a >> 16) & 0xFF;
188 let addr = a & 0xFFFF;
189 if matches!(bank, 0x00..=0x3F | 0x80..=0xBF) {
190 match addr {
191 0x4800 => return self.r4800,
192 0x4801 => return self.r4801,
193 0x4804 => return self.r4804,
194 0x4805 => return self.r4805,
195 0x4806 => return self.r4806,
196 0x4807 => return self.r4807,
197 0x6000..=0x7FFF if !self.sram.is_empty() => {
198 let off = (((bank - if bank >= 0x80 { 0x80 } else { 0x00 }) as usize) * 0x2000
199 + (addr - 0x6000) as usize)
200 % self.sram.len();
201 return self.sram[off];
202 }
203 _ => {}
204 }
205 }
206 self.mcu_read(a)
207 }
208
209 fn write24(&mut self, addr24: u32, val: u8) {
210 let a = addr24 & 0xFF_FFFF;
211 let bank = (a >> 16) & 0xFF;
212 let addr = a & 0xFFFF;
213 if matches!(bank, 0x00..=0x3F | 0x80..=0xBF) {
214 match addr {
215 0x4800 => self.r4800 = val,
216 0x4801 => self.r4801 = val,
217 0x4804 => self.r4804 = val & 0x8F,
218 0x4805 => self.r4805 = val & 0x8F,
219 0x4806 => self.r4806 = val & 0x8F,
220 0x4807 => self.r4807 = val & 0x8F,
221 0x6000..=0x7FFF if !self.sram.is_empty() => {
222 let off = (((bank - if bank >= 0x80 { 0x80 } else { 0x00 }) as usize) * 0x2000
223 + (addr - 0x6000) as usize)
224 % self.sram.len();
225 self.sram[off] = val;
226 }
227 _ => {}
229 }
230 }
231 }
232
233 fn rom(&self) -> &[u8] {
234 &self.rom
235 }
236
237 fn sram(&self) -> &[u8] {
238 &self.sram
239 }
240
241 fn sram_mut(&mut self) -> &mut [u8] {
242 &mut self.sram
243 }
244
245 fn notify_dma_channel(&mut self, channel: usize, address: u32, count: u16) {
246 if let Some(c) = self.dma.get_mut(channel & 7) {
247 c.address = address;
248 c.size = count;
249 }
250 }
251
252 fn save_state(&self, w: &mut SaveWriter) {
257 w.section(*b"SDB1", |s| {
258 s.write_u8(self.r4800);
259 s.write_u8(self.r4801);
260 s.write_u8(self.r4804);
261 s.write_u8(self.r4805);
262 s.write_u8(self.r4806);
263 s.write_u8(self.r4807);
264 for c in &self.dma {
265 s.write_u32(c.address);
266 s.write_u16(c.size);
267 }
268 s.write_bool(self.dma_ready);
269 });
270 self.decompressor.save_state(w);
271 }
272
273 fn load_state(&mut self, r: &mut SaveReader) -> Result<(), SaveStateError> {
274 let mut s = r.expect_section(*b"SDB1")?;
275 self.r4800 = s.read_u8()?;
276 self.r4801 = s.read_u8()?;
277 self.r4804 = s.read_u8()? & 0x8F;
280 self.r4805 = s.read_u8()? & 0x8F;
281 self.r4806 = s.read_u8()? & 0x8F;
282 self.r4807 = s.read_u8()? & 0x8F;
283 for c in &mut self.dma {
284 c.address = s.read_u32()?;
285 c.size = s.read_u16()?;
286 }
287 self.dma_ready = s.read_bool()?;
288 if s.remaining() != 0 {
289 return Err(SaveStateError::Invalid(alloc::format!(
290 "SDB1 section has {} trailing byte(s)",
291 s.remaining()
292 )));
293 }
294 self.decompressor.load_state(r)
295 }
296}
297
298#[must_use]
300pub fn select(_map_mode: MapMode, rom: Box<[u8]>, sram_size: usize) -> Sdd1Board {
301 Sdd1Board::new(rom, sram_size)
302}
303
304#[cfg(test)]
305mod tests {
306 use super::*;
307
308 fn board() -> Sdd1Board {
309 Sdd1Board::new(vec![0u8; 0x40_0000].into_boxed_slice(), 0)
310 }
311
312 #[test]
313 fn control_register_roundtrip() {
314 let mut b = board();
315 b.write24(0x00_4800, 0xAB);
316 assert_eq!(b.read24(0x00_4800), 0xAB);
317 b.write24(0x00_4804, 0xFF); assert_eq!(b.read24(0x00_4804), 0x8F);
319 }
320
321 #[test]
322 fn mmc_bank_selects_rom_quarter() {
323 let mut b = board();
324 b.rom[0x10_0000] = 0x55; assert_eq!(b.mmc_read(0x10_0000), 0x55);
326 }
327
328 #[test]
329 fn dma_snoop_arms_and_disarms_a_channel() {
330 let mut b = board();
331 b.notify_dma_channel(0, 0xC0_0000, 4);
332 assert_eq!(b.dma[0].address, 0xC0_0000);
333 assert_eq!(b.dma[0].size, 4);
334 }
335
336 #[test]
337 fn registers_and_decompressor_round_trip_through_save_state() {
338 let mut rom = vec![0u8; 0x40_0000];
340 for (i, b) in rom.iter_mut().enumerate().take(64) {
341 *b = (i as u8).wrapping_mul(37).wrapping_add(11);
342 }
343 let mut b = Sdd1Board::new(rom.into_boxed_slice(), 0);
344 b.write24(0x00_4805, 0x77); b.notify_dma_channel(2, 0xC1_2345, 9);
346 let mmc_map = [0u8, 1, 2, 3];
347 b.decompressor.init(0, &b.rom, &mmc_map);
348 b.decompressor.read(&b.rom, &mmc_map); let mut w = SaveWriter::new();
351 b.save_state(&mut w);
352 let bytes = w.into_bytes();
353
354 let mut fresh = Sdd1Board::new(b.rom.clone(), 0);
355 let mut r = SaveReader::new(&bytes);
356 fresh.load_state(&mut r).unwrap();
357
358 assert_eq!(fresh.r4805, 0x77 & 0x8F);
359 assert_eq!(fresh.dma[2].address, 0xC1_2345);
360 assert_eq!(fresh.dma[2].size, 9);
361
362 let expected: alloc::vec::Vec<u8> = (0..8)
365 .map(|_| b.decompressor.read(&b.rom, &mmc_map))
366 .collect();
367 let actual: alloc::vec::Vec<u8> = (0..8)
368 .map(|_| fresh.decompressor.read(&fresh.rom, &mmc_map))
369 .collect();
370 assert_eq!(actual, expected);
371 assert_eq!(r.remaining(), 0);
372 }
373}