Skip to main content

rustynes_mappers/
sachen_8259.rs

1//! Sachen 8259 ASIC (mapper 137 and the wider 8259 A/B/C/D family).
2//!
3//! Sachen's reusable bank-select ASIC, and a step up from the company's
4//! discrete boards in `sachen_discrete.rs`: instead of a single latch it
5//! implements an address-then-data register protocol through the
6//! `$4100-$5FFF` window, with eight internal registers. The four die
7//! revisions differ only in how the CHR bank bits are shuffled on the way
8//! out, which is why the variants share one implementation and differ by a
9//! bit-permutation rather than by separate decode paths.
10//!
11//! A best-effort (Tier-2) board: register-decode correctness verified against
12//! the `GeraNES` reference emulator (cross-referenced, not copied)
13//! and the nesdev wiki, with no commercial-oracle ROM in the tree. Banking math
14//! is direct slice indexing and every bank select wraps with `% count`, so a
15//! register write can never index out of bounds -- required for the `#![no_std]`
16//! chip stack, which cannot afford a panic on a register access.
17//!
18//! See `tier.rs` (`MapperTier::BestEffort`), `docs/adr/0011-mapper-tiering.md`,
19//! and `docs/mappers.md` §Mapper coverage matrix.
20
21#![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
47// ---------------------------------------------------------------------------
48// Shared nametable helper (mirrors the one in the other simple-mapper modules).
49// ---------------------------------------------------------------------------
50
51const 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
81/// Mapper 137 (Sachen 8259D).
82pub 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    /// Construct a new mapper 137 board.
95    ///
96    /// # Errors
97    ///
98    /// Returns [`MapperError::Invalid`] when PRG is empty / not a multiple of
99    /// 32 KiB or CHR-ROM is empty / not a multiple of 2 KiB.
100    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        // v2.7.2 (core audit §5.5): with no save RAM, nothing drives
154        // `$6000-$7FFF` and it floats; see `Mapper::cpu_read_unmapped`.
155        (matches!(addr, 0x6000..=0x7FFF) && self.sram().is_empty()) || {
156            // $4100/$4101 are write-only registers; the rest of $4020-$5FFF is open
157            // bus. $8000-$FFFF is mapped PRG.
158            (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/// Which Sachen 8259 variant (CHR shift + OR constants).
235#[derive(Debug, Clone, Copy, PartialEq, Eq)]
236pub enum Sachen8259Variant {
237    /// 8259A (mapper 141): shift 1, CHR-OR [1, 0, 1].
238    A,
239    /// 8259B (mapper 138): shift 0, CHR-OR [0, 0, 0].
240    B,
241    /// 8259C (mapper 139): shift 2, CHR-OR [1, 2, 3].
242    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
262/// Sachen 8259 A/B/C (mappers 141 / 138 / 139). 32 KiB PRG + 2 KiB CHR banks.
263pub 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    /// Construct a Sachen 8259 A/B/C board.
278    ///
279    /// # Errors
280    /// [`MapperError::Invalid`] on a bad PRG/CHR size.
281    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    /// Resolve the 2 KiB CHR bank for slot 0..=3.
331    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        // cmd 5 -> PRG 32 KiB bank 2.
479        m.cpu_write(0x4100, 5);
480        m.cpu_write(0x4101, 2);
481        assert_eq!(m.cpu_read(0x8000), 2);
482        // cmd 0 -> CHR slot 0 = bank 3.
483        m.cpu_write(0x4100, 0);
484        m.cpu_write(0x4101, 3);
485        assert_eq!(m.ppu_read(0x0000), 3);
486        // cmd 7 -> horizontal mirroring.
487        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); // reg7 = 2 -> mirroring bits (2>>1)&3 == 1 -> horizontal.
522        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); // shift 0.
546        assert_eq!(a.ppu_read(0x0000), 2); // shift 1.
547    }
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}