Skip to main content

rustynes_mappers/
m513_sachen_9602.rs

1// SPDX-License-Identifier: GPL-3.0-or-later
2//
3// Provenance: the Sachen 9602 board is derived from Mesen2 (GPL-3.0-or-later), `Sachen/Sachen9602.h`. See docs/originality-and-provenance.md (Section 1)
4// and NOTICE for the complete, audited derivation record.
5//! Sachen `9602` (mapper 513).
6//!
7//! An MMC3-derived Sachen ASIC with an outer PRG bank register, later and
8//! more capable than the 8259 family in `sachen_8259.rs` and the discrete
9//! boards in `sachen_discrete.rs`.
10//!
11//! A best-effort (Tier-2) board: register-decode correctness verified against
12//! the reference emulators (`Mesen2`, `GeraNES`) and the nesdev wiki, with no
13//! commercial-oracle ROM in the tree. Banking math is direct slice indexing and
14//! every bank select wraps with `% count`, so a register write can never index
15//! out of bounds -- required for the `#![no_std]` chip stack, which cannot
16//! 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::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
46// ---------------------------------------------------------------------------
47// Shared nametable + mirroring helpers (mirror the other simple-mapper modules).
48// ---------------------------------------------------------------------------
49
50const 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
80/// Validate a PRG-ROM image is a non-zero multiple of 8 KiB.
81fn 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
91/// Sachen 9602 MMC3-clone (mapper 513).
92pub 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    /// v2.9.7 — MMC3's A12 rise filter (see `a12_filter`): the counter
108    /// clocks once per scanline on the eight-pulse stream the PPU reports.
109    a12: A12RiseFilter,
110    /// PRG outer bank (high two bits, << 6).
111    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(), // 8 KiB CHR-RAM.
123            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        // m9602 fixes the two top banks to $3E/$3F (within the outer bank).
143        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            // v2.9.7: the A12 filter's clock (`notify_cpu_cycle`); the bus
195            // calls it only on boards that declare this.
196            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        // v2.9.7 — MMC3's filter; see `a12_filter` for why a bare
258        // rising-edge test is no longer enough.
259        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        // v2 (v2.9.7) packs the A12 filter into the old `last_a12` byte. A v1
314        // state, whose byte is the bare level, is refused since v2.9.8 (ADR
315        // 0042); it used to load as such.
316        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/// Mapper 513 (Sachen 9602 MMC3-clone).
344///
345/// # Errors
346/// [`MapperError::Invalid`] on a bad PRG size.
347// The `_chr_rom: Box<[u8]>` keeps the factory signature uniform with the
348// dispatch site even though the 9602 is always CHR-RAM.
349#[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// ===========================================================================
359// TxcChip — the TXC protection accumulator (shared by Sachen 3011 / m136).
360// The non-JV001 variant (mask 0x07), register map per the NESdev wiki TXC /
361// mapper-136 documentation; the implementation is derived from Mesen2's
362// `Txc/TxcChip.h` (GPL-3.0-or-later). See NOTICE + docs/originality-and-provenance.md §1.
363// ===========================================================================
364
365#[cfg(test)]
366#[allow(clippy::cast_possible_truncation)]
367mod tests {
368
369    /// v2.9.7 — the IRQ counter clocks once per scanline on the A12 stream the
370    /// PPU now reports (eight pulses per line), through MMC3's filter. Before,
371    /// it clocked on every rise and depended on the PPU delivering only one.
372    #[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); // select R6 (< 6 is false; 6 captures? <6 only)
392        m.cpu_write(0x8001, 0xC5); // value>>6 = 3 only if reg<6; R6 is not <6.
393        // Use R0 (<6) to set the outer bank.
394        m.cpu_write(0x8000, 0x00);
395        m.cpu_write(0x8001, 0xC0); // outer = 3 -> <<6 = 192
396        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}