1#![allow(
22 clippy::bool_to_int_with_if,
23 clippy::cast_lossless,
24 clippy::cast_possible_truncation,
25 clippy::doc_markdown,
26 clippy::match_same_arms,
27 clippy::missing_const_for_fn,
28 clippy::similar_names,
29 clippy::struct_excessive_bools,
30 clippy::too_many_lines,
31 clippy::unreadable_literal
32)]
33
34use crate::cartridge::Mirroring;
35use crate::mapper::{Mapper, MapperCaps, MapperError};
36use alloc::{boxed::Box, vec::Vec};
37use alloc::{format, vec};
38
39const PRG_BANK_32K: usize = 0x8000;
40const CHR_BANK_2K: usize = 0x0800;
41const CHR_BANK_8K: usize = 0x2000;
42const NAMETABLE_SIZE: usize = 0x0400;
43const NAMETABLE_SIZE_U16: u16 = 0x0400;
44
45const SAVE_STATE_VERSION: u8 = 1;
46
47const fn nametable_offset(addr: u16, mirroring: Mirroring) -> usize {
52 let table = (((addr - 0x2000) / NAMETABLE_SIZE_U16) & 0x03) as u8;
53 let local = (addr as usize) & (NAMETABLE_SIZE - 1);
54 let physical = mirroring.physical_bank(table);
55 physical * NAMETABLE_SIZE + local
56}
57
58const fn byte_to_mirroring(b: u8, fallback: Mirroring) -> Mirroring {
59 match b {
60 0 => Mirroring::Horizontal,
61 1 => Mirroring::Vertical,
62 2 => Mirroring::SingleScreenA,
63 3 => Mirroring::SingleScreenB,
64 4 => Mirroring::FourScreen,
65 5 => Mirroring::MapperControlled,
66 _ => fallback,
67 }
68}
69
70const fn mirroring_to_byte(m: Mirroring) -> u8 {
71 match m {
72 Mirroring::Horizontal => 0,
73 Mirroring::Vertical => 1,
74 Mirroring::SingleScreenA => 2,
75 Mirroring::SingleScreenB => 3,
76 Mirroring::FourScreen => 4,
77 Mirroring::MapperControlled => 5,
78 }
79}
80
81pub struct Sachen8259M137 {
83 prg_rom: Box<[u8]>,
84 chr_rom: Box<[u8]>,
85 vram: Box<[u8]>,
86 cmd: u8,
87 chr_banks: [u8; 4],
88 chr_outer: u8,
89 prg_bank: u8,
90 horizontal_mirroring: bool,
91}
92
93impl Sachen8259M137 {
94 pub fn new(
101 prg_rom: Box<[u8]>,
102 chr_rom: Box<[u8]>,
103 mirroring: Mirroring,
104 ) -> Result<Self, MapperError> {
105 if prg_rom.is_empty() || !prg_rom.len().is_multiple_of(PRG_BANK_32K) {
106 return Err(MapperError::Invalid(format!(
107 "mapper 137 PRG-ROM size {} is not a non-zero multiple of 32 KiB",
108 prg_rom.len()
109 )));
110 }
111 if chr_rom.is_empty() || !chr_rom.len().is_multiple_of(CHR_BANK_2K) {
112 return Err(MapperError::Invalid(format!(
113 "mapper 137 CHR-ROM size {} is not a non-zero multiple of 2 KiB",
114 chr_rom.len()
115 )));
116 }
117 Ok(Self {
118 prg_rom,
119 chr_rom,
120 vram: vec![0u8; 2 * NAMETABLE_SIZE].into_boxed_slice(),
121 cmd: 0,
122 chr_banks: [0; 4],
123 chr_outer: 0,
124 prg_bank: 0,
125 horizontal_mirroring: mirroring == Mirroring::Horizontal,
126 })
127 }
128
129 fn read_chr(&self, addr: u16) -> u8 {
130 let count2k = (self.chr_rom.len() / CHR_BANK_2K).max(1);
131 let slot = (addr as usize >> 11) & 0x03;
132 let bank = (self.chr_banks[slot] as usize | ((self.chr_outer as usize) << 4)) % count2k;
133 self.chr_rom[bank * CHR_BANK_2K + (addr as usize & 0x07FF)]
134 }
135}
136
137impl Mapper for Sachen8259M137 {
138 fn caps(&self) -> MapperCaps {
139 MapperCaps::NONE
140 }
141
142 fn cpu_read(&mut self, addr: u16) -> u8 {
143 if (0x8000..=0xFFFF).contains(&addr) {
144 let count = (self.prg_rom.len() / PRG_BANK_32K).max(1);
145 let bank = (self.prg_bank as usize) % count;
146 self.prg_rom[bank * PRG_BANK_32K + (addr as usize & 0x7FFF)]
147 } else {
148 0
149 }
150 }
151
152 fn cpu_read_unmapped(&self, addr: u16) -> bool {
153 (matches!(addr, 0x6000..=0x7FFF) && self.sram().is_empty()) || {
156 (0x4020..=0x5FFF).contains(&addr)
159 }
160 }
161
162 fn cpu_write(&mut self, addr: u16, value: u8) {
163 match addr {
164 0x4100 => self.cmd = value & 0x07,
165 0x4101 => match self.cmd {
166 0..=3 => self.chr_banks[self.cmd as usize] = value & 0x07,
167 4 => self.chr_outer = value & 0x07,
168 5 => self.prg_bank = value & 0x07,
169 7 => self.horizontal_mirroring = (value & 0x01) != 0,
170 _ => {}
171 },
172 _ => {}
173 }
174 }
175
176 fn ppu_read(&mut self, addr: u16) -> u8 {
177 let addr = addr & 0x3FFF;
178 match addr {
179 0x0000..=0x1FFF => self.read_chr(addr),
180 0x2000..=0x3EFF => self.vram[nametable_offset(addr, self.current_mirroring())],
181 _ => 0,
182 }
183 }
184
185 fn ppu_write(&mut self, addr: u16, value: u8) {
186 let addr = addr & 0x3FFF;
187 if (0x2000..=0x3EFF).contains(&addr) {
188 let off = nametable_offset(addr, self.current_mirroring());
189 self.vram[off] = value;
190 }
191 }
192
193 fn current_mirroring(&self) -> Mirroring {
194 if self.horizontal_mirroring {
195 Mirroring::Horizontal
196 } else {
197 Mirroring::Vertical
198 }
199 }
200
201 fn save_state(&self) -> Vec<u8> {
202 let mut out = Vec::with_capacity(9 + self.vram.len());
203 out.push(SAVE_STATE_VERSION);
204 out.push(self.cmd);
205 out.extend_from_slice(&self.chr_banks);
206 out.push(self.chr_outer);
207 out.push(self.prg_bank);
208 out.push(u8::from(self.horizontal_mirroring));
209 out.extend_from_slice(&self.vram);
210 out
211 }
212
213 fn load_state(&mut self, data: &[u8]) -> Result<(), MapperError> {
214 let expected = 9 + self.vram.len();
215 if data.len() != expected {
216 return Err(MapperError::WrongLength {
217 expected,
218 got: data.len(),
219 });
220 }
221 if data[0] != SAVE_STATE_VERSION {
222 return Err(MapperError::UnsupportedVersion(data[0]));
223 }
224 self.cmd = data[1];
225 self.chr_banks.copy_from_slice(&data[2..6]);
226 self.chr_outer = data[6];
227 self.prg_bank = data[7];
228 self.horizontal_mirroring = data[8] != 0;
229 self.vram.copy_from_slice(&data[9..9 + self.vram.len()]);
230 Ok(())
231 }
232}
233
234#[derive(Debug, Clone, Copy, PartialEq, Eq)]
236pub enum Sachen8259Variant {
237 A,
239 B,
241 C,
243}
244
245impl Sachen8259Variant {
246 const fn shift(self) -> u8 {
247 match self {
248 Self::A => 1,
249 Self::B => 0,
250 Self::C => 2,
251 }
252 }
253 const fn chr_or(self) -> [usize; 3] {
254 match self {
255 Self::A => [1, 0, 1],
256 Self::B => [0, 0, 0],
257 Self::C => [1, 2, 3],
258 }
259 }
260}
261
262pub struct Sachen8259 {
264 variant: Sachen8259Variant,
265 prg_rom: Box<[u8]>,
266 chr: Box<[u8]>,
267 chr_is_ram: bool,
268 vram: Box<[u8]>,
269 regs: [u8; 8],
270 current_reg: u8,
271 mirroring: Mirroring,
272}
273
274const CHR_2K: usize = 0x0800;
275
276impl Sachen8259 {
277 pub fn new(
282 variant: Sachen8259Variant,
283 prg_rom: Box<[u8]>,
284 chr_rom: Box<[u8]>,
285 mirroring: Mirroring,
286 ) -> Result<Self, MapperError> {
287 if prg_rom.is_empty() || !prg_rom.len().is_multiple_of(PRG_BANK_32K) {
288 return Err(MapperError::Invalid(format!(
289 "Sachen 8259 PRG-ROM size {} is not a non-zero multiple of 32 KiB",
290 prg_rom.len()
291 )));
292 }
293 let chr_is_ram = chr_rom.is_empty();
294 let chr: Box<[u8]> = if chr_is_ram {
295 vec![0u8; CHR_BANK_8K].into_boxed_slice()
296 } else {
297 if !chr_rom.len().is_multiple_of(CHR_2K) {
298 return Err(MapperError::Invalid(format!(
299 "Sachen 8259 CHR-ROM size {} is not a multiple of 2 KiB",
300 chr_rom.len()
301 )));
302 }
303 chr_rom
304 };
305 Ok(Self {
306 variant,
307 prg_rom,
308 chr,
309 chr_is_ram,
310 vram: vec![0u8; 4 * NAMETABLE_SIZE].into_boxed_slice(),
311 regs: [0; 8],
312 current_reg: 0,
313 mirroring,
314 })
315 }
316
317 fn update_mirroring(&mut self) {
318 let simple = self.regs[7] & 0x01 == 0x01;
319 self.mirroring = match (self.regs[7] >> 1) & 0x03 {
320 0 => Mirroring::Vertical,
321 1 => Mirroring::Horizontal,
322 2 => Mirroring::SingleScreenB,
323 _ => Mirroring::SingleScreenA,
324 };
325 if simple {
326 self.mirroring = Mirroring::Vertical;
327 }
328 }
329
330 fn chr_bank(&self, slot: usize) -> usize {
332 let simple = self.regs[7] & 0x01 == 0x01;
333 let shift = self.variant.shift();
334 let chr_or = self.variant.chr_or();
335 let chr_high = (self.regs[4] as usize) << 3;
336 match slot {
337 0 => (chr_high | self.regs[0] as usize) << shift,
338 1 => ((chr_high | self.regs[if simple { 0 } else { 1 }] as usize) << shift) | chr_or[0],
339 2 => ((chr_high | self.regs[if simple { 0 } else { 2 }] as usize) << shift) | chr_or[1],
340 _ => ((chr_high | self.regs[if simple { 0 } else { 3 }] as usize) << shift) | chr_or[2],
341 }
342 }
343}
344
345impl Mapper for Sachen8259 {
346 fn caps(&self) -> MapperCaps {
347 MapperCaps::NONE
348 }
349
350 fn cpu_read(&mut self, addr: u16) -> u8 {
351 match addr {
352 0x8000..=0xFFFF => {
353 let count = (self.prg_rom.len() / PRG_BANK_32K).max(1);
354 let bank = (self.regs[5] as usize) % count;
355 self.prg_rom[bank * PRG_BANK_32K + (addr as usize & 0x7FFF)]
356 }
357 _ => 0,
358 }
359 }
360
361 fn cpu_read_unmapped(&self, addr: u16) -> bool {
362 (0x4020..=0x7FFF).contains(&addr)
363 }
364
365 fn cpu_write(&mut self, addr: u16, value: u8) {
366 match addr & 0xC101 {
367 0x4100 => self.current_reg = value & 0x07,
368 0x4101 => {
369 self.regs[(self.current_reg & 0x07) as usize] = value & 0x07;
370 self.update_mirroring();
371 }
372 _ => {}
373 }
374 }
375
376 fn ppu_read(&mut self, addr: u16) -> u8 {
377 let addr = addr & 0x3FFF;
378 match addr {
379 0x0000..=0x1FFF => {
380 if self.chr_is_ram {
381 return self.chr[addr as usize & (self.chr.len() - 1)];
382 }
383 let slot = (addr as usize) / CHR_2K;
384 let count = (self.chr.len() / CHR_2K).max(1);
385 let bank = self.chr_bank(slot) % count;
386 self.chr[bank * CHR_2K + (addr as usize & (CHR_2K - 1))]
387 }
388 0x2000..=0x3EFF => self.vram[nametable_offset(addr, self.mirroring)],
389 _ => 0,
390 }
391 }
392
393 fn ppu_write(&mut self, addr: u16, value: u8) {
394 let addr = addr & 0x3FFF;
395 match addr {
396 0x0000..=0x1FFF if self.chr_is_ram => {
397 let off = addr as usize & (self.chr.len() - 1);
398 self.chr[off] = value;
399 }
400 0x2000..=0x3EFF => {
401 let off = nametable_offset(addr, self.mirroring);
402 self.vram[off] = value;
403 }
404 _ => {}
405 }
406 }
407
408 fn current_mirroring(&self) -> Mirroring {
409 self.mirroring
410 }
411
412 fn save_state(&self) -> Vec<u8> {
413 let chr_ram = if self.chr_is_ram { self.chr.len() } else { 0 };
414 let mut out = Vec::with_capacity(11 + self.vram.len() + chr_ram);
415 out.push(SAVE_STATE_VERSION);
416 out.push(self.current_reg);
417 out.extend_from_slice(&self.regs);
418 out.push(mirroring_to_byte(self.mirroring));
419 out.extend_from_slice(&self.vram);
420 if self.chr_is_ram {
421 out.extend_from_slice(&self.chr);
422 }
423 out
424 }
425
426 fn load_state(&mut self, data: &[u8]) -> Result<(), MapperError> {
427 let chr_ram = if self.chr_is_ram { self.chr.len() } else { 0 };
428 let expected = 11 + self.vram.len() + chr_ram;
429 if data.len() != expected {
430 return Err(MapperError::WrongLength {
431 expected,
432 got: data.len(),
433 });
434 }
435 if data[0] != SAVE_STATE_VERSION {
436 return Err(MapperError::UnsupportedVersion(data[0]));
437 }
438 self.current_reg = data[1];
439 self.regs.copy_from_slice(&data[2..10]);
440 self.mirroring = byte_to_mirroring(data[10], self.mirroring);
441 let mut cursor = 11;
442 self.vram
443 .copy_from_slice(&data[cursor..cursor + self.vram.len()]);
444 cursor += self.vram.len();
445 if self.chr_is_ram {
446 self.chr
447 .copy_from_slice(&data[cursor..cursor + self.chr.len()]);
448 }
449 Ok(())
450 }
451}
452
453#[cfg(test)]
454#[allow(clippy::cast_possible_truncation)]
455mod tests {
456 use super::*;
457
458 fn synth_prg_32k(banks: usize) -> Box<[u8]> {
459 let mut v = vec![0xFFu8; banks * PRG_BANK_32K];
460 for b in 0..banks {
461 v[b * PRG_BANK_32K] = b as u8;
462 }
463 v.into_boxed_slice()
464 }
465
466 fn synth_chr_2k(banks: usize) -> Box<[u8]> {
467 let mut v = vec![0u8; banks * CHR_BANK_2K];
468 for b in 0..banks {
469 v[b * CHR_BANK_2K] = b as u8;
470 }
471 v.into_boxed_slice()
472 }
473
474 #[test]
475 fn m137_command_data_chr_and_prg() {
476 let mut m =
477 Sachen8259M137::new(synth_prg_32k(4), synth_chr_2k(16), Mirroring::Vertical).unwrap();
478 m.cpu_write(0x4100, 5);
480 m.cpu_write(0x4101, 2);
481 assert_eq!(m.cpu_read(0x8000), 2);
482 m.cpu_write(0x4100, 0);
484 m.cpu_write(0x4101, 3);
485 assert_eq!(m.ppu_read(0x0000), 3);
486 m.cpu_write(0x4100, 7);
488 m.cpu_write(0x4101, 1);
489 assert_eq!(m.current_mirroring(), Mirroring::Horizontal);
490 }
491
492 #[test]
493 fn m137_save_state_round_trip() {
494 let mut m =
495 Sachen8259M137::new(synth_prg_32k(4), synth_chr_2k(16), Mirroring::Vertical).unwrap();
496 m.cpu_write(0x4100, 5);
497 m.cpu_write(0x4101, 1);
498 m.cpu_write(0x4100, 0);
499 m.cpu_write(0x4101, 2);
500 let blob = m.save_state();
501 let mut m2 =
502 Sachen8259M137::new(synth_prg_32k(4), synth_chr_2k(16), Mirroring::Vertical).unwrap();
503 m2.load_state(&blob).unwrap();
504 assert_eq!(m2.cpu_read(0x8000), m.cpu_read(0x8000));
505 assert_eq!(m2.ppu_read(0x0000), m.ppu_read(0x0000));
506 }
507
508 #[test]
509 fn sachen8259_prg_and_reg_protocol() {
510 let mut m = Sachen8259::new(
511 Sachen8259Variant::B,
512 synth_prg_32k(4),
513 synth_chr_2k(16),
514 Mirroring::Vertical,
515 )
516 .unwrap();
517 m.cpu_write(0x4100, 5);
518 m.cpu_write(0x4101, 2);
519 assert_eq!(m.cpu_read(0x8000), 2);
520 m.cpu_write(0x4100, 7);
521 m.cpu_write(0x4101, 2); assert_eq!(m.current_mirroring(), Mirroring::Horizontal);
523 }
524
525 #[test]
526 fn sachen8259_variants_differ_by_shift() {
527 let mut b = Sachen8259::new(
528 Sachen8259Variant::B,
529 synth_prg_32k(2),
530 synth_chr_2k(16),
531 Mirroring::Vertical,
532 )
533 .unwrap();
534 b.cpu_write(0x4100, 0);
535 b.cpu_write(0x4101, 1);
536 let mut a = Sachen8259::new(
537 Sachen8259Variant::A,
538 synth_prg_32k(2),
539 synth_chr_2k(16),
540 Mirroring::Vertical,
541 )
542 .unwrap();
543 a.cpu_write(0x4100, 0);
544 a.cpu_write(0x4101, 1);
545 assert_eq!(b.ppu_read(0x0000), 1); assert_eq!(a.ppu_read(0x0000), 2); }
548
549 #[test]
550 fn sachen8259_save_state_round_trip() {
551 let mut m = Sachen8259::new(
552 Sachen8259Variant::C,
553 synth_prg_32k(4),
554 synth_chr_2k(32),
555 Mirroring::Vertical,
556 )
557 .unwrap();
558 m.cpu_write(0x4100, 5);
559 m.cpu_write(0x4101, 3);
560 m.cpu_write(0x4100, 4);
561 m.cpu_write(0x4101, 1);
562 let blob = m.save_state();
563 let mut m2 = Sachen8259::new(
564 Sachen8259Variant::C,
565 synth_prg_32k(4),
566 synth_chr_2k(32),
567 Mirroring::Vertical,
568 )
569 .unwrap();
570 m2.load_state(&blob).unwrap();
571 assert_eq!(m2.cpu_read(0x8000), m.cpu_read(0x8000));
572 assert_eq!(m2.ppu_read(0x0000), m.ppu_read(0x0000));
573 }
574}