1#![allow(
22 clippy::cast_possible_truncation,
23 clippy::cast_lossless,
24 clippy::match_same_arms,
25 clippy::doc_markdown,
26 clippy::similar_names,
27 clippy::too_many_lines,
28 clippy::missing_const_for_fn,
29 clippy::struct_excessive_bools,
30 clippy::bool_to_int_with_if,
31 clippy::unreadable_literal
32)]
33
34use crate::a12_filter::A12RiseFilter;
35use crate::cartridge::Mirroring;
36use crate::mapper::{Mapper, MapperCaps, MapperError};
37use alloc::{boxed::Box, format, vec, vec::Vec};
38
39const PRG_BANK_8K: usize = 0x2000;
40const CHR_BANK_8K: usize = 0x2000;
41const NAMETABLE_SIZE: usize = 0x0400;
42const NAMETABLE_SIZE_U16: u16 = 0x0400;
43
44const SAVE_STATE_VERSION: u8 = 2;
45
46const fn nametable_offset(addr: u16, mirroring: Mirroring) -> usize {
51 let table = (((addr - 0x2000) / NAMETABLE_SIZE_U16) & 0x03) as u8;
52 let local = (addr as usize) & (NAMETABLE_SIZE - 1);
53 let physical = mirroring.physical_bank(table);
54 physical * NAMETABLE_SIZE + local
55}
56
57const fn mirroring_to_byte(m: Mirroring) -> u8 {
58 match m {
59 Mirroring::Horizontal => 0,
60 Mirroring::Vertical => 1,
61 Mirroring::SingleScreenA => 2,
62 Mirroring::SingleScreenB => 3,
63 Mirroring::FourScreen => 4,
64 Mirroring::MapperControlled => 5,
65 }
66}
67
68const fn byte_to_mirroring(b: u8, fallback: Mirroring) -> Mirroring {
69 match b {
70 0 => Mirroring::Horizontal,
71 1 => Mirroring::Vertical,
72 2 => Mirroring::SingleScreenA,
73 3 => Mirroring::SingleScreenB,
74 4 => Mirroring::FourScreen,
75 5 => Mirroring::MapperControlled,
76 _ => fallback,
77 }
78}
79
80fn check_prg(prg: &[u8], id: u16) -> Result<(), MapperError> {
82 if prg.is_empty() || !prg.len().is_multiple_of(PRG_BANK_8K) {
83 return Err(MapperError::Invalid(format!(
84 "mapper {id} PRG-ROM size {} is not a non-zero multiple of 8 KiB",
85 prg.len()
86 )));
87 }
88 Ok(())
89}
90
91pub struct Sachen9602 {
93 prg_rom: Box<[u8]>,
94 chr: Box<[u8]>,
95 vram: Box<[u8]>,
96 mirroring: Mirroring,
97 prg_count_8k: usize,
98 regs: [u8; 8],
99 bank_select: u8,
100 prg_mode: bool,
101 chr_mode: bool,
102 irq_counter: u8,
103 irq_latch: u8,
104 irq_reload: bool,
105 irq_enabled: bool,
106 irq_pending: bool,
107 a12: A12RiseFilter,
110 outer: u8,
112}
113
114impl Sachen9602 {
115 const SAVE_LEN: usize = 8 + 9 + 1 + 1;
116
117 fn new(prg_rom: Box<[u8]>, mirroring: Mirroring) -> Result<Self, MapperError> {
118 check_prg(&prg_rom, 513)?;
119 let prg_count_8k = prg_rom.len() / PRG_BANK_8K;
120 Ok(Self {
121 prg_rom,
122 chr: vec![0u8; CHR_BANK_8K].into_boxed_slice(), vram: vec![0u8; 2 * NAMETABLE_SIZE].into_boxed_slice(),
124 mirroring,
125 prg_count_8k,
126 regs: [0; 8],
127 bank_select: 0,
128 prg_mode: false,
129 chr_mode: false,
130 irq_counter: 0,
131 irq_latch: 0,
132 irq_reload: false,
133 irq_enabled: false,
134 irq_pending: false,
135 a12: A12RiseFilter::new(),
136 outer: 0,
137 })
138 }
139
140 fn resolve_prg(&self, slot: usize) -> usize {
141 let outer = (self.outer as usize) << 6;
142 let bank = match (slot, self.prg_mode) {
144 (1, _) => (self.regs[7] as usize & 0x3F) | outer,
145 (0, false) => (self.regs[6] as usize & 0x3F) | outer,
146 (0, true) => 0x3E | outer,
147 (2, false) => 0x3E | outer,
148 (2, true) => (self.regs[6] as usize & 0x3F) | outer,
149 (3, _) => 0x3F | outer,
150 _ => 0,
151 };
152 bank % self.prg_count_8k
153 }
154
155 fn write_mmc3(&mut self, addr: u16, value: u8) {
156 match addr & 0xE001 {
157 0x8000 => {
158 self.bank_select = value & 0x07;
159 self.prg_mode = value & 0x40 != 0;
160 self.chr_mode = value & 0x80 != 0;
161 }
162 0x8001 => {
163 if (self.bank_select & 0x07) < 6 {
164 self.outer = value >> 6;
165 }
166 let idx = (self.bank_select & 0x07) as usize;
167 self.regs[idx] = value & 0x3F;
168 }
169 0xA000 => {
170 self.mirroring = if value & 0x01 == 0 {
171 Mirroring::Vertical
172 } else {
173 Mirroring::Horizontal
174 };
175 }
176 0xC000 => self.irq_latch = value,
177 0xC001 => {
178 self.irq_counter = 0;
179 self.irq_reload = true;
180 }
181 0xE000 => {
182 self.irq_enabled = false;
183 self.irq_pending = false;
184 }
185 0xE001 => self.irq_enabled = true,
186 _ => {}
187 }
188 }
189}
190
191impl Mapper for Sachen9602 {
192 fn caps(&self) -> MapperCaps {
193 MapperCaps {
194 cpu_cycle_hook: true,
197 audio: false,
198 frame_event_hook: false,
199 irq_source: true,
200 }
201 }
202
203 fn cpu_read(&mut self, addr: u16) -> u8 {
204 match addr {
205 0x8000..=0x9FFF => {
206 let b = self.resolve_prg(0);
207 self.prg_rom[b * PRG_BANK_8K + (addr as usize & 0x1FFF)]
208 }
209 0xA000..=0xBFFF => {
210 let b = self.resolve_prg(1);
211 self.prg_rom[b * PRG_BANK_8K + (addr as usize & 0x1FFF)]
212 }
213 0xC000..=0xDFFF => {
214 let b = self.resolve_prg(2);
215 self.prg_rom[b * PRG_BANK_8K + (addr as usize & 0x1FFF)]
216 }
217 0xE000..=0xFFFF => {
218 let b = self.resolve_prg(3);
219 self.prg_rom[b * PRG_BANK_8K + (addr as usize & 0x1FFF)]
220 }
221 _ => 0,
222 }
223 }
224
225 fn cpu_write(&mut self, addr: u16, value: u8) {
226 if (0x8000..=0xFFFF).contains(&addr) {
227 self.write_mmc3(addr, value);
228 }
229 }
230
231 fn ppu_read(&mut self, addr: u16) -> u8 {
232 let addr = addr & 0x3FFF;
233 match addr {
234 0x0000..=0x1FFF => self.chr[addr as usize & (CHR_BANK_8K - 1)],
235 0x2000..=0x3EFF => self.vram[nametable_offset(addr, self.mirroring)],
236 _ => 0,
237 }
238 }
239
240 fn ppu_write(&mut self, addr: u16, value: u8) {
241 let addr = addr & 0x3FFF;
242 match addr {
243 0x0000..=0x1FFF => self.chr[addr as usize & (CHR_BANK_8K - 1)] = value,
244 0x2000..=0x3EFF => {
245 let off = nametable_offset(addr, self.mirroring);
246 self.vram[off] = value;
247 }
248 _ => {}
249 }
250 }
251
252 fn notify_cpu_cycle(&mut self) {
253 self.a12.tick();
254 }
255
256 fn notify_a12(&mut self, level: bool) {
257 if !self.a12.edge(level) {
260 return;
261 }
262 if self.irq_counter == 0 || self.irq_reload {
263 self.irq_counter = self.irq_latch;
264 self.irq_reload = false;
265 } else {
266 self.irq_counter = self.irq_counter.wrapping_sub(1);
267 }
268 if self.irq_counter == 0 && self.irq_enabled {
269 self.irq_pending = true;
270 }
271 }
272
273 fn irq_pending(&self) -> bool {
274 self.irq_pending
275 }
276
277 fn irq_acknowledge(&mut self) {
278 self.irq_pending = false;
279 }
280
281 fn current_mirroring(&self) -> Mirroring {
282 self.mirroring
283 }
284
285 fn save_state(&self) -> Vec<u8> {
286 let mut out = Vec::with_capacity(1 + Self::SAVE_LEN + self.vram.len() + self.chr.len());
287 out.push(SAVE_STATE_VERSION);
288 out.extend_from_slice(&self.regs);
289 out.push(self.bank_select);
290 out.push(u8::from(self.prg_mode));
291 out.push(u8::from(self.chr_mode));
292 out.push(self.irq_counter);
293 out.push(self.irq_latch);
294 out.push(u8::from(self.irq_reload));
295 out.push(u8::from(self.irq_enabled));
296 out.push(u8::from(self.irq_pending));
297 out.push(self.a12.to_byte());
298 out.push(self.outer);
299 out.push(mirroring_to_byte(self.mirroring));
300 out.extend_from_slice(&self.vram);
301 out.extend_from_slice(&self.chr);
302 out
303 }
304
305 fn load_state(&mut self, data: &[u8]) -> Result<(), MapperError> {
306 let expected = 1 + Self::SAVE_LEN + self.vram.len() + self.chr.len();
307 if data.len() != expected {
308 return Err(MapperError::WrongLength {
309 expected,
310 got: data.len(),
311 });
312 }
313 let version = data[0];
317 if version != SAVE_STATE_VERSION {
318 return Err(MapperError::UnsupportedVersion(version));
319 }
320 let mut c = 1;
321 self.regs.copy_from_slice(&data[c..c + 8]);
322 c += 8;
323 self.bank_select = data[c];
324 self.prg_mode = data[c + 1] != 0;
325 self.chr_mode = data[c + 2] != 0;
326 self.irq_counter = data[c + 3];
327 self.irq_latch = data[c + 4];
328 self.irq_reload = data[c + 5] != 0;
329 self.irq_enabled = data[c + 6] != 0;
330 self.irq_pending = data[c + 7] != 0;
331 self.a12 = A12RiseFilter::from_byte(data[c + 8]);
332 c += 9;
333 self.outer = data[c];
334 self.mirroring = byte_to_mirroring(data[c + 1], self.mirroring);
335 c += 2;
336 self.vram.copy_from_slice(&data[c..c + self.vram.len()]);
337 c += self.vram.len();
338 self.chr.copy_from_slice(&data[c..c + self.chr.len()]);
339 Ok(())
340 }
341}
342
343#[allow(clippy::boxed_local)]
350pub fn new_m513(
351 prg_rom: Box<[u8]>,
352 _chr_rom: Box<[u8]>,
353 mirroring: Mirroring,
354) -> Result<Sachen9602, MapperError> {
355 Sachen9602::new(prg_rom, mirroring)
356}
357
358#[cfg(test)]
366#[allow(clippy::cast_possible_truncation)]
367mod tests {
368
369 #[test]
373 fn irq_counts_scanlines_not_raw_a12_pulses() {
374 let mut m = new_m513(synth_prg_8k(64), Box::new([]), Mirroring::Vertical).unwrap();
375 assert_eq!(crate::a12_filter::irqs_over_scanlines(&mut m, 16), 2);
376 }
377
378 use super::*;
379
380 fn synth_prg_8k(banks: usize) -> Box<[u8]> {
381 let mut v = vec![0xFFu8; banks * PRG_BANK_8K];
382 for b in 0..banks {
383 v[b * PRG_BANK_8K] = b as u8;
384 }
385 v.into_boxed_slice()
386 }
387
388 #[test]
389 fn sachen9602_prg_outer_bank() {
390 let mut m = new_m513(synth_prg_8k(128), Box::new([]), Mirroring::Vertical).unwrap();
391 m.cpu_write(0x8000, 0x06); m.cpu_write(0x8001, 0xC5); m.cpu_write(0x8000, 0x00);
395 m.cpu_write(0x8001, 0xC0); let v = m.cpu_read(0x8000);
397 assert!((v as usize) < 128);
398 }
399
400 #[test]
401 fn sachen9602_save_state_round_trip() {
402 let mut m = new_m513(synth_prg_8k(64), Box::new([]), Mirroring::Vertical).unwrap();
403 m.cpu_write(0x8000, 0x00);
404 m.cpu_write(0x8001, 0x45);
405 m.ppu_write(0x0001, 0x77);
406 let blob = m.save_state();
407 let mut m2 = new_m513(synth_prg_8k(64), Box::new([]), Mirroring::Vertical).unwrap();
408 m2.load_state(&blob).unwrap();
409 assert_eq!(m2.cpu_read(0x8000), m.cpu_read(0x8000));
410 assert_eq!(m2.ppu_read(0x0001), 0x77);
411 }
412}