1#![allow(clippy::doc_markdown)]
32
33use alloc::boxed::Box;
34
35use rustysnes_savestate::{SaveReader, SaveStateError, SaveWriter};
36
37use crate::board::{Board, Coprocessor, MappedAddr};
38use crate::coproc::upd77c25::{Revision, Upd77c25};
39
40#[derive(Debug, Clone, Copy, PartialEq, Eq)]
42pub enum Variant {
43 Dsp2,
45 Dsp4,
47 St010,
49}
50
51impl Variant {
52 #[must_use]
55 pub fn detect(title_upper: &str) -> Option<Self> {
56 if title_upper.contains("DUNGEON MASTER") {
57 Some(Self::Dsp2)
58 } else if title_upper.contains("TOP GEAR 3000") {
59 Some(Self::Dsp4)
60 } else if title_upper.contains("F1 ROC") {
61 Some(Self::St010)
62 } else {
63 None
64 }
65 }
66
67 const fn revision(self) -> Revision {
68 match self {
69 Self::Dsp2 | Self::Dsp4 => Revision::Upd7725,
70 Self::St010 => Revision::Upd96050,
71 }
72 }
73
74 const fn reg_banks(self) -> (u8, u8, u8, u8) {
76 match self {
77 Self::Dsp2 => (0x20, 0x3F, 0xA0, 0xBF),
78 Self::Dsp4 => (0x30, 0x3F, 0xB0, 0xBF),
79 Self::St010 => (0x60, 0x67, 0xE0, 0xE7),
80 }
81 }
82
83 const fn dp_banks(self) -> Option<(u8, u8, u8, u8)> {
86 match self {
87 Self::St010 => Some((0x68, 0x6F, 0xE8, 0xEF)),
88 Self::Dsp2 | Self::Dsp4 => None,
89 }
90 }
91
92 #[must_use]
94 pub const fn firmware_name(self) -> &'static str {
95 match self {
96 Self::Dsp2 => "dsp2.rom",
97 Self::Dsp4 => "dsp4.rom",
98 Self::St010 => "st010.rom",
99 }
100 }
101}
102
103fn in_bank(bank: u32, lo: u8, hi: u8, mlo: u8, mhi: u8) -> bool {
104 (u32::from(lo)..=u32::from(hi)).contains(&bank)
105 || (u32::from(mlo)..=u32::from(mhi)).contains(&bank)
106}
107
108enum Hit {
110 Dr,
111 Sr,
112 Dp(u16),
113}
114
115fn classify(variant: Variant, addr24: u32) -> Option<Hit> {
116 let bank = (addr24 >> 16) & 0xFF;
117 let addr = addr24 & 0xFFFF;
118
119 let (lo, hi, mlo, mhi) = variant.reg_banks();
120 if in_bank(bank, lo, hi, mlo, mhi) && addr >= 0x8000 {
121 return Some(if variant == Variant::Dsp4 {
129 if addr & 0x3FFF < 0x2000 {
130 Hit::Dr
131 } else {
132 Hit::Sr
133 }
134 } else if addr & 1 != 0 {
135 Hit::Sr
136 } else {
137 Hit::Dr
138 });
139 }
140 if variant == Variant::St010 && in_bank(bank, lo, hi, mlo, mhi) && addr <= 0x3FFF {
142 return Some(if addr & 1 != 0 { Hit::Sr } else { Hit::Dr });
143 }
144 if let Some((dlo, dhi, dmlo, dmhi)) = variant.dp_banks()
145 && in_bank(bank, dlo, dhi, dmlo, dmhi)
146 && addr <= 0x7FFF
147 {
148 return Some(Hit::Dp(addr as u16));
149 }
150 None
151}
152
153pub struct NecDspVariantBoard {
155 inner: Box<dyn Board>,
156 dsp: Upd77c25,
157 variant: Variant,
158}
159
160impl core::fmt::Debug for NecDspVariantBoard {
161 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
162 f.debug_struct("NecDspVariantBoard")
163 .field("variant", &self.variant)
164 .field("inner", &self.inner.name())
165 .field("firmware_loaded", &self.dsp.firmware_loaded())
166 .finish()
167 }
168}
169
170impl NecDspVariantBoard {
171 #[must_use]
174 pub fn new(inner: Box<dyn Board>, variant: Variant) -> Self {
175 Self {
176 inner,
177 dsp: Upd77c25::new(variant.revision()),
178 variant,
179 }
180 }
181}
182
183impl Board for NecDspVariantBoard {
184 fn name(&self) -> &'static str {
185 match self.variant {
186 Variant::Dsp2 => "LoROM+DSP-2",
187 Variant::Dsp4 => "LoROM+DSP-4",
188 Variant::St010 => "LoROM+ST010",
189 }
190 }
191
192 fn coprocessor(&self) -> Coprocessor {
193 Coprocessor::Dsp
194 }
195
196 fn map(&self, addr24: u32) -> MappedAddr {
197 if classify(self.variant, addr24).is_some() {
198 MappedAddr::Coprocessor
199 } else {
200 self.inner.map(addr24)
201 }
202 }
203
204 fn read24(&mut self, addr24: u32) -> u8 {
205 match classify(self.variant, addr24) {
206 Some(Hit::Dr) => self.dsp.read_dr(),
207 Some(Hit::Sr) => self.dsp.read_sr(),
208 Some(Hit::Dp(a)) => self.dsp.read_dp(a),
209 None => self.inner.read24(addr24),
210 }
211 }
212
213 fn write24(&mut self, addr24: u32, val: u8) {
214 match classify(self.variant, addr24) {
215 Some(Hit::Dr) => self.dsp.write_dr(val),
216 Some(Hit::Sr) => self.dsp.write_sr(val),
217 Some(Hit::Dp(a)) => self.dsp.write_dp(a, val),
218 None => self.inner.write24(addr24, val),
219 }
220 }
221
222 fn rom(&self) -> &[u8] {
223 self.inner.rom()
224 }
225
226 fn sram(&self) -> &[u8] {
227 self.inner.sram()
228 }
229
230 fn sram_mut(&mut self) -> &mut [u8] {
231 self.inner.sram_mut()
232 }
233
234 fn load_firmware(&mut self, bytes: &[u8]) -> bool {
235 self.dsp.load_firmware(bytes)
236 }
237
238 fn coprocessor_host_accesses(&self) -> u64 {
239 self.dsp.host_accesses()
240 }
241
242 fn firmware_hint(&self) -> Option<&'static str> {
243 Some(self.variant.firmware_name())
244 }
245
246 fn save_state(&self, w: &mut SaveWriter) {
249 self.dsp.save_state(w);
250 self.inner.save_state(w);
251 }
252
253 fn load_state(&mut self, r: &mut SaveReader) -> Result<(), SaveStateError> {
254 self.dsp.load_state(r)?;
255 self.inner.load_state(r)
256 }
257}
258
259#[cfg(test)]
260mod tests {
261 use super::*;
262 use crate::board::LoRom;
263 use alloc::vec;
264
265 fn board(variant: Variant) -> NecDspVariantBoard {
266 let inner = Box::new(LoRom::new(
267 vec![0u8; 0x8_0000].into_boxed_slice(),
268 vec![].into_boxed_slice(),
269 ));
270 NecDspVariantBoard::new(inner, variant)
271 }
272
273 #[test]
274 fn detect_by_title() {
275 assert_eq!(
276 Variant::detect("DUNGEON MASTER "),
277 Some(Variant::Dsp2)
278 );
279 assert_eq!(
280 Variant::detect("TOP GEAR 3000 "),
281 Some(Variant::Dsp4)
282 );
283 assert_eq!(
284 Variant::detect("F1 ROC II "),
285 Some(Variant::St010)
286 );
287 assert_eq!(Variant::detect("SUPER MARIO KART "), None);
288 }
289
290 #[test]
291 fn dsp2_window_split() {
292 let b = board(Variant::Dsp2);
293 assert!(matches!(classify(b.variant, 0x20_8000), Some(Hit::Dr)));
294 assert!(matches!(classify(b.variant, 0x20_8001), Some(Hit::Sr)));
295 assert!(matches!(classify(b.variant, 0xA0_8000), Some(Hit::Dr))); assert!(classify(b.variant, 0x00_8000).is_none()); }
298
299 #[test]
300 fn dsp4_window_uses_half_boundary_split_not_bit0() {
301 let b = board(Variant::Dsp4);
306 assert!(matches!(classify(b.variant, 0x30_8000), Some(Hit::Dr)));
307 assert!(matches!(classify(b.variant, 0x30_8001), Some(Hit::Dr)));
308 assert!(matches!(classify(b.variant, 0x30_9FFF), Some(Hit::Dr)));
309 assert!(matches!(classify(b.variant, 0x30_A000), Some(Hit::Sr)));
310 assert!(matches!(classify(b.variant, 0x30_BFFF), Some(Hit::Sr)));
311 assert!(matches!(classify(b.variant, 0x30_C000), Some(Hit::Dr)));
313 assert!(matches!(classify(b.variant, 0x30_E000), Some(Hit::Sr)));
314 }
315
316 #[test]
317 fn st010_register_and_dp_windows() {
318 let b = board(Variant::St010);
319 assert!(matches!(classify(b.variant, 0x60_0000), Some(Hit::Dr)));
320 assert!(matches!(classify(b.variant, 0x60_0001), Some(Hit::Sr)));
321 assert!(matches!(classify(b.variant, 0xE0_0000), Some(Hit::Dr))); assert!(matches!(classify(b.variant, 0x68_0000), Some(Hit::Dp(0)))); assert!(matches!(
324 classify(b.variant, 0xE8_0010),
325 Some(Hit::Dp(0x10))
326 ));
327 assert!(classify(b.variant, 0x00_8000).is_none()); }
329
330 #[test]
331 fn inert_without_firmware() {
332 let mut b = board(Variant::Dsp2);
333 assert!(!b.dsp.firmware_loaded());
334 assert_eq!(b.read24(0x20_8000), 0);
335 }
336
337 #[test]
338 fn engine_state_round_trips_through_save_state() {
339 let mut b = board(Variant::Dsp2); assert!(b.load_firmware(&[0u8; 8192]));
341 b.dsp.write_dp(0x20, 0x5A);
342 let before = b.dsp.data_ram_word(0x20 >> 1);
343 assert_ne!(before, 0);
344
345 let mut w = SaveWriter::new();
346 b.save_state(&mut w);
347 let bytes = w.into_bytes();
348
349 let mut fresh = board(Variant::Dsp2);
350 assert!(fresh.load_firmware(&[0u8; 8192]));
351 let mut r = SaveReader::new(&bytes);
352 fresh.load_state(&mut r).unwrap();
353
354 assert_eq!(fresh.dsp.data_ram_word(0x20 >> 1), before);
355 assert_eq!(r.remaining(), 0);
356 }
357}