1#![allow(clippy::cast_possible_truncation, clippy::doc_markdown)]
24
25use crate::cartridge::Mirroring;
26use crate::mapper::{Mapper, MapperCaps, MapperError};
27use alloc::{boxed::Box, vec::Vec};
28use alloc::{format, vec};
29
30const PRG_BANK_16K: usize = 0x4000;
31const CHR_BANK_8K: usize = 0x2000;
32const NAMETABLE_SIZE: usize = 0x0400;
33const NAMETABLE_SIZE_U16: u16 = 0x0400;
34
35const SAVE_STATE_VERSION: u8 = 1;
36
37pub struct Sunsoft2 {
39 prg_rom: Box<[u8]>,
40 chr: Box<[u8]>,
41 vram: Box<[u8]>,
42 chr_is_ram: bool,
43 prg_bank: u8,
44 chr_bank: u8,
45 mirroring: Mirroring,
46}
47
48impl Sunsoft2 {
49 pub fn new(
58 prg_rom: Box<[u8]>,
59 chr_rom: Box<[u8]>,
60 mirroring: Mirroring,
61 ) -> Result<Self, MapperError> {
62 if prg_rom.is_empty() || !prg_rom.len().is_multiple_of(PRG_BANK_16K) {
63 return Err(MapperError::Invalid(format!(
64 "Sunsoft-2 PRG-ROM size {} is not a non-zero multiple of 16 KiB",
65 prg_rom.len()
66 )));
67 }
68 let chr_is_ram = chr_rom.is_empty();
69 let chr: Box<[u8]> = if chr_is_ram {
70 vec![0u8; CHR_BANK_8K].into_boxed_slice()
71 } else if chr_rom.len().is_multiple_of(CHR_BANK_8K) {
72 chr_rom
73 } else {
74 return Err(MapperError::Invalid(format!(
75 "Sunsoft-2 expects an 8 KiB multiple of CHR; got {} bytes",
76 chr_rom.len()
77 )));
78 };
79 let _ = mirroring;
82 Ok(Self {
83 prg_rom,
84 chr,
85 vram: vec![0u8; 2 * NAMETABLE_SIZE].into_boxed_slice(),
86 chr_is_ram,
87 prg_bank: 0,
88 chr_bank: 0,
89 mirroring: Mirroring::SingleScreenA,
90 })
91 }
92
93 const fn nametable_offset(&self, addr: u16) -> usize {
94 let table = (((addr - 0x2000) / NAMETABLE_SIZE_U16) & 0x03) as u8;
95 let local = (addr as usize) & (NAMETABLE_SIZE - 1);
96 let physical = self.mirroring.physical_bank(table);
97 physical * NAMETABLE_SIZE + local
98 }
99
100 fn read_prg(&self, addr: u16) -> u8 {
101 let bank_count = (self.prg_rom.len() / PRG_BANK_16K).max(1);
102 let bank = if addr < 0xC000 {
103 (self.prg_bank as usize) % bank_count
104 } else {
105 bank_count - 1 };
107 let off = (addr as usize) & (PRG_BANK_16K - 1);
108 self.prg_rom[bank * PRG_BANK_16K + off]
109 }
110
111 fn chr_offset(&self, addr: u16) -> usize {
112 let len = self.chr.len().max(1);
113 let bank = (self.chr_bank as usize) * CHR_BANK_8K;
114 (bank + (addr as usize)) % len
115 }
116}
117
118impl Mapper for Sunsoft2 {
119 fn caps(&self) -> MapperCaps {
122 MapperCaps::NONE
123 }
124
125 fn cpu_read(&mut self, addr: u16) -> u8 {
126 if (0x8000..=0xFFFF).contains(&addr) {
127 self.read_prg(addr)
128 } else {
129 0
130 }
131 }
132
133 fn cpu_write(&mut self, addr: u16, value: u8) {
134 if (0x8000..=0xFFFF).contains(&addr) {
135 let value = value & self.read_prg(addr);
146 self.prg_bank = (value >> 4) & 0x07;
150 self.chr_bank = (((value >> 7) & 0x01) << 3) | (value & 0x07);
151 self.mirroring = if (value & 0x08) != 0 {
152 Mirroring::SingleScreenB
153 } else {
154 Mirroring::SingleScreenA
155 };
156 }
157 }
158
159 fn ppu_read(&mut self, addr: u16) -> u8 {
160 let addr = addr & 0x3FFF;
161 match addr {
162 0x0000..=0x1FFF => self.chr[self.chr_offset(addr)],
163 0x2000..=0x3EFF => self.vram[self.nametable_offset(addr)],
164 _ => 0,
165 }
166 }
167
168 fn ppu_write(&mut self, addr: u16, value: u8) {
169 let addr = addr & 0x3FFF;
170 match addr {
171 0x0000..=0x1FFF => {
172 if self.chr_is_ram {
173 let off = self.chr_offset(addr);
174 self.chr[off] = value;
175 }
176 }
177 0x2000..=0x3EFF => {
178 let off = self.nametable_offset(addr);
179 self.vram[off] = value;
180 }
181 _ => {}
182 }
183 }
184
185 fn current_mirroring(&self) -> Mirroring {
186 self.mirroring
187 }
188
189 fn debug_info(&self) -> crate::mapper::MapperDebugInfo {
190 let mut info = crate::mapper::MapperDebugInfo {
191 mapper_id: 89,
192 name: "Sunsoft-2 (89)".into(),
193 mirroring: crate::mapper::mirroring_name(self.mirroring),
194 ..Default::default()
195 };
196 info.prg_banks
197 .push(("PRG16k".into(), format!("{:#04x}", self.prg_bank)));
198 info.chr_banks
199 .push(("CHR8k".into(), format!("{:#04x}", self.chr_bank)));
200 info
201 }
202
203 fn save_state(&self) -> Vec<u8> {
204 let mut out = Vec::with_capacity(
205 4 + self.vram.len() + if self.chr_is_ram { self.chr.len() } else { 0 },
206 );
207 out.push(SAVE_STATE_VERSION);
208 out.push(self.prg_bank);
209 out.push(self.chr_bank);
210 out.push(match self.mirroring {
211 Mirroring::SingleScreenB => 1,
212 _ => 0,
213 });
214 out.extend_from_slice(&self.vram);
215 if self.chr_is_ram {
216 out.extend_from_slice(&self.chr);
217 }
218 out
219 }
220
221 fn load_state(&mut self, data: &[u8]) -> Result<(), MapperError> {
222 let need_chr = if self.chr_is_ram { self.chr.len() } else { 0 };
223 let expected = 4 + self.vram.len() + need_chr;
224 if data.len() != expected {
225 return Err(MapperError::WrongLength {
226 expected,
227 got: data.len(),
228 });
229 }
230 if data[0] != SAVE_STATE_VERSION {
231 return Err(MapperError::UnsupportedVersion(data[0]));
232 }
233 self.prg_bank = data[1];
234 self.chr_bank = data[2];
235 self.mirroring = if data[3] != 0 {
236 Mirroring::SingleScreenB
237 } else {
238 Mirroring::SingleScreenA
239 };
240 let mut cursor = 4;
241 self.vram
242 .copy_from_slice(&data[cursor..cursor + self.vram.len()]);
243 cursor += self.vram.len();
244 if self.chr_is_ram {
245 self.chr
246 .copy_from_slice(&data[cursor..cursor + self.chr.len()]);
247 }
248 Ok(())
249 }
250}
251
252#[cfg(test)]
253#[allow(clippy::cast_possible_truncation)]
254mod tests {
255 use super::*;
256
257 fn synth_prg(banks_16k: usize) -> Box<[u8]> {
261 let mut v = vec![0xFFu8; banks_16k * PRG_BANK_16K];
262 for b in 0..banks_16k {
263 v[b * PRG_BANK_16K] = b as u8;
264 }
265 v.into_boxed_slice()
266 }
267
268 fn synth_chr(banks: usize) -> Box<[u8]> {
269 let mut v = vec![0u8; banks * CHR_BANK_8K];
270 for b in 0..banks {
271 v[b * CHR_BANK_8K] = b as u8;
272 }
273 v.into_boxed_slice()
274 }
275
276 #[test]
277 fn prg_bank_and_fixed_tail() {
278 let mut m = Sunsoft2::new(synth_prg(8), synth_chr(4), Mirroring::Vertical).unwrap();
279 assert_eq!(m.cpu_read(0x8000), 0);
281 assert_eq!(m.cpu_read(0xC000), 7);
282 m.cpu_write(0x8001, 0x30);
285 assert_eq!(m.cpu_read(0x8000), 3);
286 assert_eq!(m.cpu_read(0xC000), 7);
287 }
288
289 #[test]
290 fn chr_bank_combines_a16() {
291 let mut m = Sunsoft2::new(synth_prg(2), synth_chr(16), Mirroring::Vertical).unwrap();
292 m.cpu_write(0x8001, 0b1000_0010);
295 assert_eq!(m.ppu_read(0x0000), 10);
296 m.cpu_write(0x8001, 0b0000_0101);
298 assert_eq!(m.ppu_read(0x0000), 5);
299 }
300
301 #[test]
302 fn one_screen_mirroring_select() {
303 let mut m = Sunsoft2::new(synth_prg(2), synth_chr(4), Mirroring::Vertical).unwrap();
304 assert_eq!(m.current_mirroring(), Mirroring::SingleScreenA);
305 m.cpu_write(0x8001, 0x08); assert_eq!(m.current_mirroring(), Mirroring::SingleScreenB);
307 m.cpu_write(0x8001, 0x00);
308 assert_eq!(m.current_mirroring(), Mirroring::SingleScreenA);
309 }
310
311 #[test]
312 fn bus_conflict_masks_write_with_prg_byte() {
313 let mut prg = vec![0xFFu8; 8 * PRG_BANK_16K];
318 for b in 0..8 {
319 prg[b * PRG_BANK_16K] = b as u8;
320 }
321 let mut m =
324 Sunsoft2::new(prg.into_boxed_slice(), synth_chr(16), Mirroring::Vertical).unwrap();
325 m.cpu_write(0x8000, 0xFF); assert_eq!(m.prg_bank, 0);
327 assert_eq!(m.chr_bank, 0);
328 assert_eq!(m.current_mirroring(), Mirroring::SingleScreenA);
329
330 let mut prg = vec![0xFFu8; 8 * PRG_BANK_16K];
334 prg[0x1000] = 0b0011_1000; let mut m =
336 Sunsoft2::new(prg.into_boxed_slice(), synth_chr(16), Mirroring::Vertical).unwrap();
337 m.cpu_write(0x9000, 0xFF);
338 assert_eq!(m.prg_bank, 3, "PRG bank decoded from masked value");
339 assert_eq!(m.current_mirroring(), Mirroring::SingleScreenB);
340 assert_eq!(m.chr_bank, 0);
341 }
342
343 #[test]
344 fn save_state_round_trip() {
345 let mut m = Sunsoft2::new(synth_prg(8), synth_chr(16), Mirroring::Vertical).unwrap();
346 m.cpu_write(0x8001, 0b1011_1010);
347 let blob = m.save_state();
348 let mut m2 = Sunsoft2::new(synth_prg(8), synth_chr(16), Mirroring::Vertical).unwrap();
349 m2.load_state(&blob).unwrap();
350 assert_eq!(m.cpu_read(0x8000), m2.cpu_read(0x8000));
351 assert_eq!(m.ppu_read(0x0000), m2.ppu_read(0x0000));
352 assert_eq!(m.current_mirroring(), m2.current_mirroring());
353 }
354}