rustynes_mappers/
m093_sunsoft3r.rs1#![allow(clippy::cast_possible_truncation, clippy::doc_markdown)]
25
26use crate::cartridge::Mirroring;
27use crate::mapper::{Mapper, MapperCaps, MapperError};
28use alloc::{boxed::Box, vec::Vec};
29use alloc::{format, vec};
30
31const PRG_BANK_16K: usize = 0x4000;
32const CHR_BANK_8K: usize = 0x2000;
33const NAMETABLE_SIZE: usize = 0x0400;
34const NAMETABLE_SIZE_U16: u16 = 0x0400;
35
36const SAVE_STATE_VERSION: u8 = 1;
37
38pub struct Sunsoft3r {
40 prg_rom: Box<[u8]>,
41 chr_ram: Box<[u8]>,
42 vram: Box<[u8]>,
43 prg_bank: u8,
44 chr_ram_enabled: bool,
45 mirroring: Mirroring,
46}
47
48impl Sunsoft3r {
49 pub fn new(prg_rom: Box<[u8]>, mirroring: Mirroring) -> Result<Self, MapperError> {
58 if prg_rom.is_empty() || !prg_rom.len().is_multiple_of(PRG_BANK_16K) {
59 return Err(MapperError::Invalid(format!(
60 "Sunsoft-3R PRG-ROM size {} is not a non-zero multiple of 16 KiB",
61 prg_rom.len()
62 )));
63 }
64 Ok(Self {
65 prg_rom,
66 chr_ram: vec![0u8; CHR_BANK_8K].into_boxed_slice(),
67 vram: vec![0u8; 2 * NAMETABLE_SIZE].into_boxed_slice(),
68 prg_bank: 0,
69 chr_ram_enabled: true,
70 mirroring,
71 })
72 }
73
74 fn read_prg(&self, addr: u16) -> u8 {
77 let bank_count = (self.prg_rom.len() / PRG_BANK_16K).max(1);
78 match addr {
79 0x8000..=0xBFFF => {
80 let bank = (self.prg_bank as usize) % bank_count;
81 let off = (addr - 0x8000) as usize;
82 self.prg_rom[bank * PRG_BANK_16K + off]
83 }
84 0xC000..=0xFFFF => {
85 let last = bank_count - 1;
87 let off = (addr - 0xC000) as usize;
88 self.prg_rom[last * PRG_BANK_16K + off]
89 }
90 _ => 0,
91 }
92 }
93
94 const fn nametable_offset(&self, addr: u16) -> usize {
95 let table = (((addr - 0x2000) / NAMETABLE_SIZE_U16) & 0x03) as u8;
96 let local = (addr as usize) & (NAMETABLE_SIZE - 1);
97 let physical = self.mirroring.physical_bank(table);
98 physical * NAMETABLE_SIZE + local
99 }
100}
101
102impl Mapper for Sunsoft3r {
103 fn caps(&self) -> MapperCaps {
106 MapperCaps::NONE
107 }
108
109 fn cpu_read(&mut self, addr: u16) -> u8 {
110 self.read_prg(addr)
111 }
112
113 fn cpu_write(&mut self, addr: u16, value: u8) {
114 if let 0x8000..=0xFFFF = addr {
115 let value = value & self.read_prg(addr);
128 self.prg_bank = (value >> 4) & 0x07;
130 self.chr_ram_enabled = (value & 0x01) != 0;
131 }
132 }
133
134 fn ppu_read(&mut self, addr: u16) -> u8 {
135 let addr = addr & 0x3FFF;
136 match addr {
137 0x0000..=0x1FFF => {
138 if self.chr_ram_enabled {
139 self.chr_ram[addr as usize]
140 } else {
141 0
143 }
144 }
145 0x2000..=0x3EFF => self.vram[self.nametable_offset(addr)],
146 _ => 0,
147 }
148 }
149
150 fn ppu_write(&mut self, addr: u16, value: u8) {
151 let addr = addr & 0x3FFF;
152 match addr {
153 0x0000..=0x1FFF => {
154 if self.chr_ram_enabled {
155 self.chr_ram[addr as usize] = value;
156 }
157 }
158 0x2000..=0x3EFF => {
159 let off = self.nametable_offset(addr);
160 self.vram[off] = value;
161 }
162 _ => {}
163 }
164 }
165
166 fn current_mirroring(&self) -> Mirroring {
167 self.mirroring
168 }
169
170 fn debug_info(&self) -> crate::mapper::MapperDebugInfo {
171 let mut info = crate::mapper::MapperDebugInfo {
172 mapper_id: 93,
173 name: "Sunsoft-3R (93)".into(),
174 mirroring: crate::mapper::mirroring_name(self.mirroring),
175 ..Default::default()
176 };
177 info.prg_banks
178 .push(("PRG".into(), format!("{:#04x}", self.prg_bank)));
179 info.extra.push((
180 "CHR-RAM".into(),
181 if self.chr_ram_enabled {
182 "enabled".into()
183 } else {
184 "disabled".into()
185 },
186 ));
187 info
188 }
189
190 fn save_state(&self) -> Vec<u8> {
191 let mut out = Vec::with_capacity(3 + self.vram.len() + self.chr_ram.len());
192 out.push(SAVE_STATE_VERSION);
193 out.push(self.prg_bank);
194 out.push(u8::from(self.chr_ram_enabled));
195 out.extend_from_slice(&self.vram);
196 out.extend_from_slice(&self.chr_ram);
197 out
198 }
199
200 fn load_state(&mut self, data: &[u8]) -> Result<(), MapperError> {
201 let expected = 3 + self.vram.len() + self.chr_ram.len();
202 if data.len() != expected {
203 return Err(MapperError::WrongLength {
204 expected,
205 got: data.len(),
206 });
207 }
208 if data[0] != SAVE_STATE_VERSION {
209 return Err(MapperError::UnsupportedVersion(data[0]));
210 }
211 self.prg_bank = data[1];
212 self.chr_ram_enabled = data[2] != 0;
213 let mut cursor = 3;
214 self.vram
215 .copy_from_slice(&data[cursor..cursor + self.vram.len()]);
216 cursor += self.vram.len();
217 self.chr_ram
218 .copy_from_slice(&data[cursor..cursor + self.chr_ram.len()]);
219 Ok(())
220 }
221}
222
223#[cfg(test)]
224#[allow(clippy::cast_possible_truncation)]
225mod tests {
226 use super::*;
227
228 fn synth_prg(banks: usize) -> Box<[u8]> {
238 let mut v = vec![0xFFu8; banks * PRG_BANK_16K];
239 for b in 0..banks {
240 v[b * PRG_BANK_16K] = b as u8;
241 }
242 v.into_boxed_slice()
243 }
244
245 #[test]
246 fn defaults_first_prg_last_fixed() {
247 let mut m = Sunsoft3r::new(synth_prg(8), Mirroring::Vertical).unwrap();
248 assert_eq!(m.cpu_read(0x8000), 0);
249 assert_eq!(m.cpu_read(0xC000), 7); }
251
252 #[test]
253 fn prg_bank_select_bits_4_to_6() {
254 let mut m = Sunsoft3r::new(synth_prg(8), Mirroring::Vertical).unwrap();
255 m.cpu_write(0x8001, 0b0101_0001);
257 assert_eq!(m.cpu_read(0x8000), 5);
258 assert_eq!(m.cpu_read(0xC000), 7); }
260
261 #[test]
262 fn chr_ram_enable_gates_access() {
263 let mut m = Sunsoft3r::new(synth_prg(2), Mirroring::Horizontal).unwrap();
264 m.ppu_write(0x0010, 0xCD);
266 assert_eq!(m.ppu_read(0x0010), 0xCD);
267 m.cpu_write(0x8001, 0x00);
269 m.ppu_write(0x0020, 0xEE);
270 assert_eq!(m.ppu_read(0x0020), 0);
271 m.cpu_write(0x8001, 0x01);
273 assert_eq!(m.ppu_read(0x0010), 0xCD);
274 }
275
276 #[test]
277 fn save_state_round_trip() {
278 let mut m = Sunsoft3r::new(synth_prg(4), Mirroring::Vertical).unwrap();
279 m.cpu_write(0x8001, 0b0010_0001);
280 m.ppu_write(0x0001, 0x77);
281 let blob = m.save_state();
282 let mut m2 = Sunsoft3r::new(synth_prg(4), Mirroring::Vertical).unwrap();
283 m2.load_state(&blob).unwrap();
284 assert_eq!(m.cpu_read(0x8000), m2.cpu_read(0x8000));
285 assert_eq!(m.ppu_read(0x0001), m2.ppu_read(0x0001));
286 }
287}