rustysnes_cart/coproc/
obc1.rs1#![allow(clippy::doc_markdown)]
17
18use alloc::boxed::Box;
19use alloc::vec;
20
21use rustysnes_savestate::{SaveReader, SaveStateError, SaveWriter};
22
23use crate::board::{Board, Coprocessor, MappedAddr};
24
25const RAM_SIZE: usize = 0x2000;
28
29fn classify(addr24: u32) -> Option<u16> {
32 let bank = (addr24 >> 16) & 0xFF;
33 let addr = addr24 & 0xFFFF;
34 let in_main = matches!(bank, 0x00..=0x3F | 0x80..=0xBF) && (0x6000..=0x7FFF).contains(&addr);
35 let in_mirror = matches!(bank, 0x70 | 0x71 | 0xF0 | 0xF1)
36 && ((0x6000..=0x7FFF).contains(&addr) || addr >= 0xE000);
37 (in_main || in_mirror).then_some((addr & 0x1FFF) as u16)
38}
39
40#[derive(Debug, Clone, Copy, Default)]
42struct Status {
43 address: u16,
45 baseptr: u16,
47 shift: u16,
49}
50
51pub struct Obc1Board {
53 inner: Box<dyn Board>,
54 ram: Box<[u8]>,
55 status: Status,
56}
57
58impl core::fmt::Debug for Obc1Board {
59 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
60 f.debug_struct("Obc1Board")
61 .field("inner", &self.inner.name())
62 .field("ram_len", &self.ram.len())
63 .field("status", &self.status)
64 .finish()
65 }
66}
67
68impl Obc1Board {
69 #[must_use]
73 pub fn new(inner: Box<dyn Board>) -> Self {
74 let ram = vec![0u8; RAM_SIZE].into_boxed_slice();
75 let mut b = Self {
76 inner,
77 ram,
78 status: Status::default(),
79 };
80 b.power();
81 b
82 }
83
84 fn power(&mut self) {
87 self.status.baseptr = if self.ram_read(0x1FF5) & 1 != 0 {
88 0x1800
89 } else {
90 0x1C00
91 };
92 self.status.address = u16::from(self.ram_read(0x1FF6) & 0x7F);
93 self.status.shift = u16::from(self.ram_read(0x1FF6) & 3) << 1;
94 }
95
96 fn ram_read(&self, addr: u16) -> u8 {
97 self.ram[usize::from(addr & 0x1FFF)]
98 }
99
100 fn ram_write(&mut self, addr: u16, val: u8) {
101 self.ram[usize::from(addr & 0x1FFF)] = val;
102 }
103
104 fn read_register(&self, addr: u16) -> u8 {
105 let s = self.status;
106 match addr {
107 0x1FF0..=0x1FF3 => self.ram_read(s.baseptr + (s.address << 2) + (addr - 0x1FF0)),
108 0x1FF4 => self.ram_read(s.baseptr + (s.address >> 2) + 0x200),
109 _ => self.ram_read(addr),
110 }
111 }
112
113 fn write_register(&mut self, addr: u16, val: u8) {
114 let s = self.status;
115 match addr {
116 0x1FF0..=0x1FF3 => self.ram_write(s.baseptr + (s.address << 2) + (addr - 0x1FF0), val),
117 0x1FF4 => {
118 let slot = s.baseptr + (s.address >> 2) + 0x200;
119 let old = self.ram_read(slot);
120 let merged = (old & !(3 << s.shift)) | ((val & 3) << s.shift);
121 self.ram_write(slot, merged);
122 }
123 0x1FF5 => {
124 self.status.baseptr = if val & 1 != 0 { 0x1800 } else { 0x1C00 };
125 self.ram_write(addr, val);
126 }
127 0x1FF6 => {
128 self.status.address = u16::from(val & 0x7F);
129 self.status.shift = u16::from(val & 3) << 1;
130 self.ram_write(addr, val);
131 }
132 _ => self.ram_write(addr, val),
133 }
134 }
135}
136
137impl Board for Obc1Board {
138 fn name(&self) -> &'static str {
139 "LoROM+OBC1"
140 }
141
142 fn coprocessor(&self) -> Coprocessor {
143 Coprocessor::Obc1
144 }
145
146 fn map(&self, addr24: u32) -> MappedAddr {
147 if classify(addr24).is_some() {
148 MappedAddr::Coprocessor
149 } else {
150 self.inner.map(addr24)
151 }
152 }
153
154 fn read24(&mut self, addr24: u32) -> u8 {
155 match classify(addr24) {
156 Some(a) => self.read_register(a),
157 None => self.inner.read24(addr24),
158 }
159 }
160
161 fn write24(&mut self, addr24: u32, val: u8) {
162 if let Some(a) = classify(addr24) {
163 self.write_register(a, val);
164 } else {
165 self.inner.write24(addr24, val);
166 }
167 }
168
169 fn rom(&self) -> &[u8] {
170 self.inner.rom()
171 }
172
173 fn sram(&self) -> &[u8] {
177 &self.ram
178 }
179
180 fn sram_mut(&mut self) -> &mut [u8] {
181 &mut self.ram
182 }
183
184 fn save_state(&self, w: &mut SaveWriter) {
189 w.section(*b"OBC1", |s| {
190 s.write_u16(self.status.address);
191 s.write_u16(self.status.baseptr);
192 s.write_u16(self.status.shift);
193 });
194 self.inner.save_state(w);
195 }
196
197 fn load_state(&mut self, r: &mut SaveReader) -> Result<(), SaveStateError> {
198 let mut s = r.expect_section(*b"OBC1")?;
199 let address = s.read_u16()?;
200 let baseptr = s.read_u16()?;
201 let shift = s.read_u16()?;
202 if address > 0x7F || !matches!(baseptr, 0x1800 | 0x1C00) || !matches!(shift, 0 | 2 | 4 | 6)
209 {
210 return Err(SaveStateError::Invalid(alloc::format!(
211 "OBC1 cursor out of range: address={address:#x} baseptr={baseptr:#x} \
212 shift={shift}"
213 )));
214 }
215 if s.remaining() != 0 {
216 return Err(SaveStateError::Invalid(alloc::format!(
217 "OBC1 section has {} trailing byte(s)",
218 s.remaining()
219 )));
220 }
221 self.status.address = address;
222 self.status.baseptr = baseptr;
223 self.status.shift = shift;
224 self.inner.load_state(r)
225 }
226}
227
228#[cfg(test)]
229mod tests {
230 use super::*;
231 use crate::board::LoRom;
232 use alloc::vec;
233
234 fn board() -> Obc1Board {
235 let inner = Box::new(LoRom::new(
236 vec![0u8; 0x8_0000].into_boxed_slice(),
237 vec![].into_boxed_slice(),
238 ));
239 Obc1Board::new(inner)
240 }
241
242 #[test]
243 fn window_classify() {
244 assert_eq!(classify(0x00_6000), Some(0x0000));
245 assert_eq!(classify(0x3F_7FFF), Some(0x1FFF));
246 assert_eq!(classify(0x70_6000), Some(0x0000));
247 assert_eq!(classify(0xF1_E000), Some(0x0000));
248 assert_eq!(classify(0x00_8000), None); }
250
251 #[test]
252 fn slot_read_write_roundtrip() {
253 let mut b = board();
254 b.write24(0x00_7FF6, 5);
256 b.write24(0x00_7FF0, 0xAA);
257 b.write24(0x00_7FF1, 0xBB);
258 assert_eq!(b.read24(0x00_7FF0), 0xAA);
259 assert_eq!(b.read24(0x00_7FF1), 0xBB);
260 assert_eq!(b.ram[0x1C00 + (5 << 2)], 0xAA);
262 assert_eq!(b.ram[0x1C00 + (5 << 2) + 1], 0xBB);
263 }
264
265 #[test]
266 fn packed_slot_read_modify_write() {
267 let mut b = board();
268 b.write24(0x00_7FF6, 3); b.write24(0x00_7FF4, 0b11); let slot = 0x1C00 + 0x200; assert_eq!(b.ram[slot], 0b1100_0000);
272 }
273
274 #[test]
275 fn baseptr_toggle() {
276 let mut b = board();
277 b.write24(0x00_7FF5, 1);
278 assert_eq!(b.status.baseptr, 0x1800);
279 b.write24(0x00_7FF5, 0);
280 assert_eq!(b.status.baseptr, 0x1C00);
281 }
282
283 #[test]
284 fn cursor_round_trips_through_save_state() {
285 let mut b = board();
286 b.write24(0x00_7FF6, 5); b.write24(0x00_7FF5, 1); let mut w = SaveWriter::new();
290 b.save_state(&mut w);
291 let bytes = w.into_bytes();
292
293 let mut fresh = board();
294 let mut r = SaveReader::new(&bytes);
295 fresh.load_state(&mut r).unwrap();
296
297 assert_eq!(fresh.status.address, b.status.address);
298 assert_eq!(fresh.status.baseptr, b.status.baseptr);
299 assert_eq!(fresh.status.shift, b.status.shift);
300 assert_eq!(r.remaining(), 0);
301 }
302
303 #[test]
304 fn out_of_range_cursor_is_rejected_not_panicked_on() {
305 let mut w = SaveWriter::new();
308 w.section(*b"OBC1", |s| {
309 s.write_u16(0); s.write_u16(0x1C00); s.write_u16(8); });
313 let bytes = w.into_bytes();
314 let mut b = board();
315 let mut r = SaveReader::new(&bytes);
316 assert!(matches!(
317 b.load_state(&mut r),
318 Err(SaveStateError::Invalid(_))
319 ));
320 }
321
322 #[test]
323 fn trailing_bytes_in_section_are_rejected() {
324 let mut w = SaveWriter::new();
325 w.section(*b"OBC1", |s| {
326 s.write_u16(0);
327 s.write_u16(0x1C00);
328 s.write_u16(0);
329 s.write_u8(0xFF); });
331 let bytes = w.into_bytes();
332 let mut b = board();
333 let mut r = SaveReader::new(&bytes);
334 assert!(matches!(
335 b.load_state(&mut r),
336 Err(SaveStateError::Invalid(_))
337 ));
338 }
339}