Skip to main content

rustysnes_cart/coproc/
dsp1.rs

1//! The DSP-1 board — the µPD77C25 wired into a LoROM/HiROM cartridge.
2//!
3//! DSP-1 (NEC µPD77C25, the `uPD7725` revision of the shared engine) is the Mode-7 3D-math
4//! coprocessor in 15+ titles (Super Mario Kart, Pilotwings, Super Bases Loaded 2). It exposes
5//! exactly two memory-mapped ports — the data register (DR) and the status register (SR) — at a
6//! board-dependent bus window. There is no canonical per-game window table; this board picks the
7//! de-facto window from the map mode + ROM size, the heuristic snes9x/bsnes use when no cartridge
8//! database is present, which coincides with every ares DSP-1 board definition:
9//!
10//! | Map mode / size        | DSP window (banks : addr)        | DR / SR split            |
11//! |------------------------|----------------------------------|--------------------------|
12//! | HiROM                  | `$00–$1F,$80–$9F : $6000–$7FFF`   | DR `$6xxx`, SR `$7xxx`   |
13//! | LoROM, ROM ≤ 1 MiB     | `$30–$3F,$B0–$BF : $8000–$FFFF`   | DR `$8–$Bxxx`, SR `$C–$F`|
14//! | LoROM, ROM > 1 MiB     | `$60–$6F,$E0–$EF : $0000–$7FFF`   | DR `$0–$3xxx`, SR `$4–$7`|
15//!
16//! ROM and SRAM decode is delegated to the wrapped base board; only the DSP window is
17//! intercepted. The board is functional only once the user supplies the `dsp1.rom` / `dsp1b.rom`
18//! firmware dump (`docs/adr/0003` — a chip-ROM-dump coprocessor is never silently degraded).
19
20// Chip-name jargon (µPD77C25, DSP-1, …) is not Rust code.
21#![allow(clippy::doc_markdown)]
22
23use alloc::boxed::Box;
24
25use rustysnes_savestate::{SaveReader, SaveStateError, SaveWriter};
26
27use crate::board::{Board, Coprocessor, MappedAddr};
28use crate::coproc::upd77c25::{Revision, Upd77c25};
29use crate::header::MapMode;
30
31/// Which bus window the DSP-1 DR/SR ports occupy, selected from the cart's map mode + size.
32#[derive(Debug, Clone, Copy, PartialEq, Eq)]
33enum DspWindow {
34    /// HiROM: `$00–$1F,$80–$9F : $6000–$7FFF`; SR when `addr & 0x1000`.
35    HiRom,
36    /// LoROM ≤ 1 MiB: `$30–$3F,$B0–$BF : $8000–$FFFF`; SR when `addr & 0x4000`.
37    LoRomSmall,
38    /// LoROM > 1 MiB: `$60–$6F,$E0–$EF : $0000–$7FFF`; SR when `addr & 0x4000`.
39    LoRomLarge,
40}
41
42impl DspWindow {
43    /// Pick the window for a DSP-1 cart of `map_mode` with `rom_len` bytes of ROM.
44    const fn select(map_mode: MapMode, rom_len: usize) -> Self {
45        match map_mode {
46            MapMode::HiRom | MapMode::ExHiRom => Self::HiRom,
47            MapMode::LoRom | MapMode::ExLoRom if rom_len > 0x10_0000 => Self::LoRomLarge,
48            MapMode::LoRom | MapMode::ExLoRom => Self::LoRomSmall,
49        }
50    }
51
52    /// Classify a 24-bit CPU address: `Some(true)` = SR port, `Some(false)` = DR port, `None` =
53    /// not in the DSP window (delegate to the base board).
54    fn classify(self, addr24: u32) -> Option<bool> {
55        let bank = (addr24 >> 16) & 0x7F;
56        let addr = addr24 & 0xFFFF;
57        match self {
58            Self::HiRom => ((0x00..=0x1F).contains(&bank) && (0x6000..=0x7FFF).contains(&addr))
59                .then_some(addr & 0x1000 != 0),
60            Self::LoRomSmall => {
61                ((0x30..=0x3F).contains(&bank) && addr >= 0x8000).then_some(addr & 0x4000 != 0)
62            }
63            Self::LoRomLarge => {
64                ((0x60..=0x6F).contains(&bank) && addr < 0x8000).then_some(addr & 0x4000 != 0)
65            }
66        }
67    }
68}
69
70/// A LoROM/HiROM cartridge carrying a DSP-1 (µPD77C25).
71pub struct Dsp1Board {
72    inner: Box<dyn Board>,
73    dsp: Upd77c25,
74    window: DspWindow,
75    name: &'static str,
76}
77
78impl core::fmt::Debug for Dsp1Board {
79    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
80        f.debug_struct("Dsp1Board")
81            .field("name", &self.name)
82            .field("inner", &self.inner.name())
83            .field("window", &self.window)
84            .field("firmware_loaded", &self.dsp.firmware_loaded())
85            .finish()
86    }
87}
88
89impl Dsp1Board {
90    /// Wrap a base board (`inner`, a LoROM or HiROM board over the cart's ROM/SRAM) with a
91    /// DSP-1, selecting the bus window from `map_mode` + `rom_len`. The DSP is inert until
92    /// [`Dsp1Board::load_firmware`] supplies the chip dump.
93    #[must_use]
94    pub fn new(inner: Box<dyn Board>, map_mode: MapMode, rom_len: usize) -> Self {
95        let window = DspWindow::select(map_mode, rom_len);
96        let name = match window {
97            DspWindow::HiRom => "HiROM+DSP-1",
98            DspWindow::LoRomSmall | DspWindow::LoRomLarge => "LoROM+DSP-1",
99        };
100        Self {
101            inner,
102            dsp: Upd77c25::new(Revision::Upd7725),
103            window,
104            name,
105        }
106    }
107
108    /// Whether the DSP-1 firmware has been supplied (the chip is functional).
109    #[must_use]
110    pub const fn firmware_loaded(&self) -> bool {
111        self.dsp.firmware_loaded()
112    }
113}
114
115impl Board for Dsp1Board {
116    fn name(&self) -> &'static str {
117        self.name
118    }
119
120    fn coprocessor(&self) -> Coprocessor {
121        Coprocessor::Dsp
122    }
123
124    fn map(&self, addr24: u32) -> MappedAddr {
125        if self.window.classify(addr24).is_some() {
126            MappedAddr::Coprocessor
127        } else {
128            self.inner.map(addr24)
129        }
130    }
131
132    fn read24(&mut self, addr24: u32) -> u8 {
133        match self.window.classify(addr24) {
134            Some(true) => self.dsp.read_sr(),
135            Some(false) => self.dsp.read_dr(),
136            None => self.inner.read24(addr24),
137        }
138    }
139
140    fn write24(&mut self, addr24: u32, val: u8) {
141        match self.window.classify(addr24) {
142            Some(true) => self.dsp.write_sr(val),
143            Some(false) => self.dsp.write_dr(val),
144            None => self.inner.write24(addr24, val),
145        }
146    }
147
148    fn rom(&self) -> &[u8] {
149        self.inner.rom()
150    }
151
152    fn sram(&self) -> &[u8] {
153        self.inner.sram()
154    }
155
156    fn sram_mut(&mut self) -> &mut [u8] {
157        self.inner.sram_mut()
158    }
159
160    fn load_firmware(&mut self, bytes: &[u8]) -> bool {
161        self.dsp.load_firmware(bytes)
162    }
163
164    fn coprocessor_host_accesses(&self) -> u64 {
165        self.dsp.host_accesses()
166    }
167
168    // `window` is fixed at construction (derived from map mode + ROM size, never mutated), so it
169    // needs no save-state entry — only the engine's own mutable register/RAM state does.
170    fn save_state(&self, w: &mut SaveWriter) {
171        self.dsp.save_state(w);
172        self.inner.save_state(w);
173    }
174
175    fn load_state(&mut self, r: &mut SaveReader) -> Result<(), SaveStateError> {
176        self.dsp.load_state(r)?;
177        self.inner.load_state(r)
178    }
179}
180
181#[cfg(test)]
182mod tests {
183    use super::*;
184    use crate::board::LoRom;
185    use alloc::vec;
186
187    fn dsp_lorom_small() -> Dsp1Board {
188        let inner = Box::new(LoRom::new(
189            vec![0u8; 0x8_0000].into_boxed_slice(),
190            vec![].into_boxed_slice(),
191        ));
192        Dsp1Board::new(inner, MapMode::LoRom, 0x8_0000)
193    }
194
195    #[test]
196    fn window_split_lorom_small() {
197        let w = DspWindow::LoRomSmall;
198        assert_eq!(w.classify(0x30_8000), Some(false)); // DR
199        assert_eq!(w.classify(0x30_C000), Some(true)); // SR
200        assert_eq!(w.classify(0xB0_8000), Some(false)); // mirror DR
201        assert_eq!(w.classify(0x00_8000), None); // ROM, not DSP
202    }
203
204    #[test]
205    fn window_split_hirom_and_large() {
206        assert_eq!(DspWindow::HiRom.classify(0x00_6000), Some(false));
207        assert_eq!(DspWindow::HiRom.classify(0x00_7000), Some(true));
208        assert_eq!(DspWindow::LoRomLarge.classify(0x60_0000), Some(false));
209        assert_eq!(DspWindow::LoRomLarge.classify(0x60_4000), Some(true));
210    }
211
212    #[test]
213    fn inert_without_firmware() {
214        let mut b = dsp_lorom_small();
215        assert!(!b.firmware_loaded());
216        // SR/DR read as open-bus-ish zero until firmware is supplied.
217        assert_eq!(b.read24(0x30_C000), 0);
218        b.write24(0x30_8000, 0x42);
219        assert_eq!(b.read24(0x30_8000), 0);
220    }
221
222    #[test]
223    fn rejects_short_firmware() {
224        let mut b = dsp_lorom_small();
225        assert!(!b.load_firmware(&[0u8; 16]));
226        assert!(!b.firmware_loaded());
227    }
228
229    #[test]
230    fn engine_state_round_trips_through_save_state() {
231        let mut b = dsp_lorom_small();
232        // write_dp/read_dp are no-ops until firmware is loaded (matches real hardware — the
233        // register file is meaningless without a program); an all-zero dump is enough (only its
234        // LENGTH is validated) to unlock the data-RAM host port for this test.
235        assert!(b.load_firmware(&[0u8; 8192]));
236        b.dsp.write_dp(0x10, 0xAB);
237        b.dsp.write_dp(0x11, 0xCD);
238        let before = b.dsp.data_ram_word(0x10 >> 1);
239        assert_ne!(
240            before, 0,
241            "the write above should have produced non-zero data-RAM content"
242        );
243
244        let mut w = SaveWriter::new();
245        b.save_state(&mut w);
246        let bytes = w.into_bytes();
247
248        let mut fresh = dsp_lorom_small();
249        assert!(fresh.load_firmware(&[0u8; 8192]));
250        let mut r = SaveReader::new(&bytes);
251        fresh.load_state(&mut r).unwrap();
252
253        assert_eq!(fresh.dsp.data_ram_word(0x10 >> 1), before);
254        assert_eq!(r.remaining(), 0);
255    }
256}