Skip to main content

rustysnes_cart/coproc/
cx4.rs

1//! The CX4 board — the Hitachi HG51B169 wired into a LoROM cartridge (Mega Man X2, Mega Man X3).
2//!
3//! Clean-room port of ares' `HitachiDSP` board wrapper (ISC, `sfc/coprocessor/hitachidsp/`),
4//! `Mapping == 0` only (the scheme both local games use — `ares` board `SHVC-1DC0N-01`). Unlike
5//! DSP-1, the [`Hg51b`] program executes from the cart's own ROM (see that module's doc), so this
6//! board is functional the instant the cart loads for CODE; only the 3 KiB data-ROM constant
7//! table (`cx4.rom`) is a genuine external chip dump, and the chip stays inert (never silently
8//! degraded, `docs/adr/0003`) until it's supplied.
9//!
10//! Bus window (bank:addr, `$00-3F,$80-BF` only):
11//!
12//! | Region        | Target                                             |
13//! |---------------|-----------------------------------------------------|
14//! | `$8000-FFFF`  | cart ROM (delegated to the wrapped base board)      |
15//! | `$6000-6BFF`, `$7000-7BFF` | HG51B's 3 KiB data RAM ([`Hg51b::read_dram`]) |
16//! | `$6C00-6FFF`, `$7C00-7FFF` | HG51B's IO register block ([`Hg51b::read_io`]) |
17//! | `$70-77:0000-7FFF` | save RAM — falls through to the base board, whose own LoROM SRAM decode already covers this bank range (`docs/cart.md`'s LoROM SRAM table); no CX4-specific handling needed |
18//!
19//! Execution trigger: writing the cache program-counter register (`$7F4F`) while the chip is
20//! halted starts it, which then runs synchronously to its next halt (`Hg51b::run_until_halt`) —
21//! the same run-to-completion host-sync pattern this project's GSU/DSP-1 boards use.
22
23// Chip-name jargon (CX4, HG51B, ...) is not Rust code.
24#![allow(clippy::doc_markdown)]
25
26use alloc::boxed::Box;
27
28use rustysnes_savestate::{SaveReader, SaveStateError, SaveWriter};
29
30use crate::board::{Board, Coprocessor, MappedAddr};
31use crate::coproc::hg51b::{Hg51b, Hg51bBus};
32
33/// Bridges the HG51B core's chip-relative bus access to the wrapped base board's own (already
34/// correct) LoROM ROM/SRAM decode — the chip's `isROM`/`isRAM`/`read`/`write` hooks resolve to
35/// exactly the same address space the S-CPU sees, so there is no separate address math to
36/// re-derive here.
37struct Cx4Mem<'a> {
38    inner: &'a mut dyn Board,
39}
40
41impl Hg51bBus for Cx4Mem<'_> {
42    fn is_rom(&self, address: u32) -> bool {
43        matches!(self.inner.map(address), MappedAddr::Rom(_))
44    }
45
46    fn is_ram(&self, address: u32) -> bool {
47        matches!(self.inner.map(address), MappedAddr::Sram(_))
48    }
49
50    fn read(&mut self, address: u32) -> u8 {
51        self.inner.read24(address)
52    }
53
54    fn write(&mut self, address: u32, data: u8) {
55        self.inner.write24(address, data);
56    }
57}
58
59/// Classification of a bus address against the CX4 windows (see the module doc's table).
60enum Hit {
61    /// HG51B data RAM, at the given already-chip-relative offset.
62    Dram(u32),
63    /// HG51B IO register block, at the given already-folded `$7C00 | (addr & 0x3FF)` offset.
64    Io(u32),
65}
66
67const fn classify(addr24: u32) -> Option<Hit> {
68    // Exact ares `addressIO`/`addressDRAM` masks (`Mapping == 0`): bit 0x40_0000 folds bank
69    // `$80-BF` onto `$00-3F` (both are valid — the "high" mirror), so the bank restriction to
70    // `$00-3F,$80-BF` and the address-range check are the SAME combined bitmask test, not two
71    // separate ones.
72    let a = addr24 & 0xFF_FFFF;
73    if (a & 0x40_EC00) == 0x00_6C00 {
74        return Some(Hit::Io(0x7C00 | (a & 0x3FF)));
75    }
76    if (a & 0x40_E000) == 0x00_6000 && (a & 0x00_0C00) != 0x00_0C00 {
77        return Some(Hit::Dram(a & 0xFFF));
78    }
79    None
80}
81
82/// A LoROM cartridge carrying a CX4 (Hitachi HG51B169).
83pub struct Cx4Board {
84    inner: Box<dyn Board>,
85    hg51b: Hg51b,
86}
87
88impl core::fmt::Debug for Cx4Board {
89    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
90        f.debug_struct("Cx4Board")
91            .field("inner", &self.inner.name())
92            .field("hg51b", &self.hg51b)
93            .finish()
94    }
95}
96
97impl Cx4Board {
98    /// Wrap a base board (`inner`, the cart's LoROM ROM/SRAM decode) with a CX4. The chip's
99    /// program executes from `inner`'s own ROM immediately; the data-ROM constant table is inert
100    /// until [`Board::load_firmware`] supplies `cx4.rom`.
101    #[must_use]
102    pub fn new(inner: Box<dyn Board>) -> Self {
103        Self {
104            inner,
105            hg51b: Hg51b::new(),
106        }
107    }
108}
109
110impl Board for Cx4Board {
111    fn name(&self) -> &'static str {
112        "LoROM+CX4"
113    }
114
115    fn coprocessor(&self) -> Coprocessor {
116        Coprocessor::Cx4
117    }
118
119    fn map(&self, addr24: u32) -> MappedAddr {
120        if classify(addr24).is_some() {
121            MappedAddr::Coprocessor
122        } else {
123            self.inner.map(addr24)
124        }
125    }
126
127    fn read24(&mut self, addr24: u32) -> u8 {
128        match classify(addr24) {
129            Some(Hit::Dram(a)) => self.hg51b.read_dram(a),
130            Some(Hit::Io(a)) => self.hg51b.read_io(a),
131            None => self.inner.read24(addr24),
132        }
133    }
134
135    fn write24(&mut self, addr24: u32, val: u8) {
136        match classify(addr24) {
137            Some(Hit::Dram(a)) => self.hg51b.write_dram(a, val),
138            Some(Hit::Io(a)) => {
139                let mut mem = Cx4Mem {
140                    inner: &mut *self.inner,
141                };
142                self.hg51b.write_io(a, val, &mut mem);
143            }
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.hg51b.load_data_rom(bytes)
162    }
163
164    fn firmware_hint(&self) -> Option<&'static str> {
165        Some("cx4.rom")
166    }
167
168    fn irq_pending(&self) -> bool {
169        self.hg51b.irq_pending()
170    }
171
172    fn coprocessor_host_accesses(&self) -> u64 {
173        self.hg51b.instructions_run()
174    }
175
176    fn save_state(&self, w: &mut SaveWriter) {
177        self.hg51b.save_state(w);
178        self.inner.save_state(w);
179    }
180
181    fn load_state(&mut self, r: &mut SaveReader) -> Result<(), SaveStateError> {
182        self.hg51b.load_state(r)?;
183        self.inner.load_state(r)
184    }
185}
186
187#[cfg(test)]
188mod tests {
189    use super::*;
190    use crate::board::LoRom;
191    use alloc::vec;
192
193    fn board() -> Cx4Board {
194        let inner = Box::new(LoRom::new(
195            vec![0u8; 0x8_0000].into_boxed_slice(),
196            vec![].into_boxed_slice(),
197        ));
198        Cx4Board::new(inner)
199    }
200
201    #[test]
202    fn window_classify() {
203        assert!(matches!(classify(0x00_6000), Some(Hit::Dram(0))));
204        assert!(matches!(classify(0x00_6BFF), Some(Hit::Dram(0xBFF))));
205        assert!(classify(0x00_6C00).is_some()); // IO, not DRAM
206        assert!(matches!(classify(0x00_6C00), Some(Hit::Io(0x7C00))));
207        assert!(matches!(classify(0x00_7FEF), Some(Hit::Io(0x7FEF))));
208        assert!(classify(0x00_8000).is_none()); // ROM, not CX4
209        assert!(matches!(classify(0x80_6000), Some(Hit::Dram(0))));
210    }
211
212    #[test]
213    fn inert_without_data_rom() {
214        let mut b = board();
215        assert!(!b.hg51b.data_rom_loaded());
216        // Writing the pc trigger with no data ROM loaded must not attempt to run.
217        b.write24(0x00_7C4F, 0x00);
218        assert!(b.hg51b.data_rom_loaded().eq(&false));
219    }
220
221    #[test]
222    fn dram_roundtrip() {
223        let mut b = board();
224        b.write24(0x00_6000, 0x42);
225        assert_eq!(b.read24(0x00_6000), 0x42);
226    }
227
228    #[test]
229    fn engine_state_round_trips_through_save_state() {
230        let mut b = board();
231        b.write24(0x00_6000, 0x42);
232        b.write24(0x00_7F4D, 0x12); // cache.pb low byte (a register outside the data-ROM path)
233
234        let mut w = SaveWriter::new();
235        b.save_state(&mut w);
236        let bytes = w.into_bytes();
237
238        let mut fresh = board();
239        let mut r = SaveReader::new(&bytes);
240        fresh.load_state(&mut r).unwrap();
241
242        assert_eq!(fresh.read24(0x00_6000), 0x42);
243        assert_eq!(fresh.read24(0x00_7F4D), 0x12);
244        assert_eq!(r.remaining(), 0);
245    }
246}