rustynes_mappers/
m184_sunsoft1.rs1#![allow(clippy::cast_possible_truncation, clippy::doc_markdown)]
21
22use crate::cartridge::Mirroring;
23use crate::mapper::{Mapper, MapperCaps, MapperError};
24use alloc::{boxed::Box, vec::Vec};
25use alloc::{format, vec};
26
27const PRG_BANK_16K: usize = 0x4000;
28const PRG_BANK_32K: usize = 0x8000;
29const CHR_BANK_4K: usize = 0x1000;
30const NAMETABLE_SIZE: usize = 0x0400;
31const NAMETABLE_SIZE_U16: u16 = 0x0400;
32
33const SAVE_STATE_VERSION: u8 = 1;
34
35pub struct Sunsoft1 {
37 prg_rom: Box<[u8]>,
38 chr_rom: Box<[u8]>,
39 vram: Box<[u8]>,
40 chr_bank: [u8; 2],
42 mirroring: Mirroring,
43}
44
45impl Sunsoft1 {
46 pub fn new(
53 prg_rom: Box<[u8]>,
54 chr_rom: Box<[u8]>,
55 mirroring: Mirroring,
56 ) -> Result<Self, MapperError> {
57 if prg_rom.len() != PRG_BANK_16K && prg_rom.len() != PRG_BANK_32K {
58 return Err(MapperError::Invalid(format!(
59 "Sunsoft-1 expects 16 or 32 KiB PRG, got {} bytes",
60 prg_rom.len()
61 )));
62 }
63 if chr_rom.is_empty() || !chr_rom.len().is_multiple_of(CHR_BANK_4K) {
64 return Err(MapperError::Invalid(format!(
65 "Sunsoft-1 expects non-empty CHR-ROM in 4 KiB units, got {} bytes",
66 chr_rom.len()
67 )));
68 }
69 Ok(Self {
70 prg_rom,
71 chr_rom,
72 vram: vec![0u8; 2 * NAMETABLE_SIZE].into_boxed_slice(),
73 chr_bank: [0, 0],
74 mirroring,
75 })
76 }
77
78 const fn nametable_offset(&self, addr: u16) -> usize {
79 let table = (((addr - 0x2000) / NAMETABLE_SIZE_U16) & 0x03) as u8;
80 let local = (addr as usize) & (NAMETABLE_SIZE - 1);
81 let physical = self.mirroring.physical_bank(table);
82 physical * NAMETABLE_SIZE + local
83 }
84
85 fn read_prg(&self, addr: u16) -> u8 {
86 let off = (addr - 0x8000) as usize;
87 if self.prg_rom.len() == PRG_BANK_16K {
88 self.prg_rom[off & (PRG_BANK_16K - 1)]
89 } else {
90 self.prg_rom[off]
91 }
92 }
93
94 fn chr_offset(&self, addr: u16) -> usize {
95 let len = self.chr_rom.len().max(1);
96 let half = (addr >> 12) & 0x01; let bank = (self.chr_bank[half as usize] as usize) * CHR_BANK_4K;
98 (bank + (addr as usize & (CHR_BANK_4K - 1))) % len
99 }
100}
101
102impl Mapper for Sunsoft1 {
103 fn has_hardwired_mirroring(&self) -> bool {
106 true
107 }
108
109 fn caps(&self) -> MapperCaps {
112 MapperCaps::NONE
113 }
114
115 fn cpu_read(&mut self, addr: u16) -> u8 {
116 if (0x8000..=0xFFFF).contains(&addr) {
117 self.read_prg(addr)
118 } else {
119 0
120 }
121 }
122
123 fn cpu_write(&mut self, addr: u16, value: u8) {
124 if (0x6000..=0x7FFF).contains(&addr) {
125 self.chr_bank[0] = value & 0x07;
126 self.chr_bank[1] = (value >> 4) & 0x07;
127 }
128 }
129
130 fn ppu_read(&mut self, addr: u16) -> u8 {
131 let addr = addr & 0x3FFF;
132 match addr {
133 0x0000..=0x1FFF => self.chr_rom[self.chr_offset(addr)],
134 0x2000..=0x3EFF => self.vram[self.nametable_offset(addr)],
135 _ => 0,
136 }
137 }
138
139 fn ppu_write(&mut self, addr: u16, value: u8) {
140 let addr = addr & 0x3FFF;
141 if let 0x2000..=0x3EFF = addr {
142 let off = self.nametable_offset(addr);
143 self.vram[off] = value;
144 }
145 }
147
148 fn current_mirroring(&self) -> Mirroring {
149 self.mirroring
150 }
151
152 fn debug_info(&self) -> crate::mapper::MapperDebugInfo {
153 let mut info = crate::mapper::MapperDebugInfo {
154 mapper_id: 184,
155 name: "Sunsoft-1 (184)".into(),
156 mirroring: crate::mapper::mirroring_name(self.mirroring),
157 ..Default::default()
158 };
159 info.chr_banks
160 .push(("CHR4k@0".into(), format!("{:#04x}", self.chr_bank[0])));
161 info.chr_banks
162 .push(("CHR4k@1".into(), format!("{:#04x}", self.chr_bank[1])));
163 info
164 }
165
166 fn save_state(&self) -> Vec<u8> {
167 let mut out = Vec::with_capacity(3 + self.vram.len());
168 out.push(SAVE_STATE_VERSION);
169 out.extend_from_slice(&self.chr_bank);
170 out.extend_from_slice(&self.vram);
171 out
172 }
173
174 fn load_state(&mut self, data: &[u8]) -> Result<(), MapperError> {
175 let expected = 3 + self.vram.len();
176 if data.len() != expected {
177 return Err(MapperError::WrongLength {
178 expected,
179 got: data.len(),
180 });
181 }
182 if data[0] != SAVE_STATE_VERSION {
183 return Err(MapperError::UnsupportedVersion(data[0]));
184 }
185 self.chr_bank.copy_from_slice(&data[1..3]);
186 self.vram.copy_from_slice(&data[3..3 + self.vram.len()]);
187 Ok(())
188 }
189}
190
191#[cfg(test)]
192#[allow(clippy::cast_possible_truncation)]
193mod tests {
194 use super::*;
195
196 fn synth_prg(bytes: usize) -> Box<[u8]> {
197 let mut v = vec![0u8; bytes];
198 for (i, b) in v.iter_mut().enumerate() {
199 *b = (i & 0xFF) as u8;
200 }
201 v.into_boxed_slice()
202 }
203
204 fn synth_chr_4k(banks: usize) -> Box<[u8]> {
205 let mut v = vec![0u8; banks * CHR_BANK_4K];
206 for b in 0..banks {
207 v[b * CHR_BANK_4K] = b as u8;
208 }
209 v.into_boxed_slice()
210 }
211
212 #[test]
213 fn two_4k_chr_banks() {
214 let mut m = Sunsoft1::new(
215 synth_prg(PRG_BANK_32K),
216 synth_chr_4k(8),
217 Mirroring::Vertical,
218 )
219 .unwrap();
220 m.cpu_write(0x6000, 0b0101_0011);
222 assert_eq!(m.ppu_read(0x0000), 3);
223 assert_eq!(m.ppu_read(0x1000), 5);
224 }
225
226 #[test]
227 fn write_outside_window_ignored() {
228 let mut m = Sunsoft1::new(
229 synth_prg(PRG_BANK_32K),
230 synth_chr_4k(8),
231 Mirroring::Vertical,
232 )
233 .unwrap();
234 m.cpu_write(0x6000, 0b0010_0001); assert_eq!(m.ppu_read(0x0000), 1);
236 assert_eq!(m.ppu_read(0x1000), 2);
237 m.cpu_write(0x8000, 0b0111_0111); assert_eq!(m.ppu_read(0x0000), 1);
239 assert_eq!(m.ppu_read(0x1000), 2);
240 }
241
242 #[test]
243 fn prg_16k_mirrors() {
244 let mut m = Sunsoft1::new(
245 synth_prg(PRG_BANK_16K),
246 synth_chr_4k(4),
247 Mirroring::Vertical,
248 )
249 .unwrap();
250 assert_eq!(m.cpu_read(0x8000), m.cpu_read(0xC000));
251 }
252
253 #[test]
254 fn save_state_round_trip() {
255 let mut m = Sunsoft1::new(
256 synth_prg(PRG_BANK_32K),
257 synth_chr_4k(8),
258 Mirroring::Vertical,
259 )
260 .unwrap();
261 m.cpu_write(0x6000, 0b0110_0010);
262 let blob = m.save_state();
263 let mut m2 = Sunsoft1::new(
264 synth_prg(PRG_BANK_32K),
265 synth_chr_4k(8),
266 Mirroring::Vertical,
267 )
268 .unwrap();
269 m2.load_state(&blob).unwrap();
270 assert_eq!(m.ppu_read(0x0000), m2.ppu_read(0x0000));
271 assert_eq!(m.ppu_read(0x1000), m2.ppu_read(0x1000));
272 }
273}