Skip to main content

rustynes_mappers/
sachen_discrete.rs

1// SPDX-License-Identifier: GPL-3.0-or-later
2//
3// Provenance: the discrete Sachen / Txc boards are derived from Mesen2 (GPL-3.0-or-later), `Sachen/Sachen8259.h` / `Txc/TxcChip.h`. See docs/originality-and-provenance.md (Section 1)
4// and NOTICE for the complete, audited derivation record.
5//! Sachen discrete boards addressed in the `$4100-$5FFF` expansion window:
6//! mappers 133, 145 and 146.
7//!
8//! Sachen's unlicensed boards consistently decode on address line A8 rather
9//! than in the `$8000-$FFFF` ROM window -- a way of avoiding bus conflicts
10//! without gating logic, since nothing else drives the bus down there. The
11//! three differ only in which bits of the written byte become the CHR select:
12//! mapper 145 takes bit 7 alone, and mapper 146 is electrically the same board
13//! as AVE's `NINA-03` (mapper 79), which is why its decode mirrors
14//! `ave_nina.rs`.
15//!
16//! Sachen's later 8259 ASIC family is a different design; see
17//! `sachen_8259.rs`.
18//!
19//! A best-effort (Tier-2) board: register-decode correctness verified against
20//! the `GeraNES` reference emulator (cross-referenced, not copied)
21//! and the nesdev wiki, with no commercial-oracle ROM in the tree. Banking math
22//! is direct slice indexing and every bank select wraps with `% count`, so a
23//! register write can never index out of bounds -- required for the `#![no_std]`
24//! chip stack, which cannot afford a panic on a register access.
25//!
26//! See `tier.rs` (`MapperTier::BestEffort`), `docs/adr/0011-mapper-tiering.md`,
27//! and `docs/mappers.md` §Mapper coverage matrix.
28
29#![allow(
30    clippy::bool_to_int_with_if,
31    clippy::cast_lossless,
32    clippy::cast_possible_truncation,
33    clippy::doc_markdown,
34    clippy::match_same_arms,
35    clippy::missing_const_for_fn,
36    clippy::similar_names,
37    clippy::struct_excessive_bools,
38    clippy::too_many_lines,
39    clippy::unreadable_literal
40)]
41
42use crate::cartridge::Mirroring;
43use crate::mapper::{Mapper, MapperCaps, MapperError};
44use alloc::{boxed::Box, vec::Vec};
45use alloc::{format, vec};
46
47const PRG_BANK_16K: usize = 0x4000;
48const PRG_BANK_32K: usize = 0x8000;
49const CHR_BANK_8K: usize = 0x2000;
50const NAMETABLE_SIZE: usize = 0x0400;
51const NAMETABLE_SIZE_U16: u16 = 0x0400;
52
53const SAVE_STATE_VERSION: u8 = 1;
54
55// ---------------------------------------------------------------------------
56// Shared nametable helper (mirrors the one in the other simple-mapper modules).
57// ---------------------------------------------------------------------------
58
59const fn nametable_offset(addr: u16, mirroring: Mirroring) -> usize {
60    let table = (((addr - 0x2000) / NAMETABLE_SIZE_U16) & 0x03) as u8;
61    let local = (addr as usize) & (NAMETABLE_SIZE - 1);
62    let physical = mirroring.physical_bank(table);
63    physical * NAMETABLE_SIZE + local
64}
65
66/// Mapper 133 (Sachen 3009).
67pub struct Sachen133 {
68    prg_rom: Box<[u8]>,
69    chr_rom: Box<[u8]>,
70    vram: Box<[u8]>,
71    prg_bank: u8,
72    chr_bank: u8,
73    mirroring: Mirroring,
74}
75
76impl Sachen133 {
77    /// Construct a new mapper 133 board.
78    ///
79    /// # Errors
80    ///
81    /// Returns [`MapperError::Invalid`] when PRG is not a non-zero multiple of
82    /// 32 KiB or CHR-ROM is empty / not a multiple of 8 KiB.
83    pub fn new(
84        prg_rom: Box<[u8]>,
85        chr_rom: Box<[u8]>,
86        mirroring: Mirroring,
87    ) -> Result<Self, MapperError> {
88        if prg_rom.is_empty() || !prg_rom.len().is_multiple_of(PRG_BANK_32K) {
89            return Err(MapperError::Invalid(format!(
90                "mapper 133 PRG-ROM size {} is not a non-zero multiple of 32 KiB",
91                prg_rom.len()
92            )));
93        }
94        if chr_rom.is_empty() || !chr_rom.len().is_multiple_of(CHR_BANK_8K) {
95            return Err(MapperError::Invalid(format!(
96                "mapper 133 CHR-ROM size {} is not a non-zero multiple of 8 KiB",
97                chr_rom.len()
98            )));
99        }
100        Ok(Self {
101            prg_rom,
102            chr_rom,
103            vram: vec![0u8; 2 * NAMETABLE_SIZE].into_boxed_slice(),
104            prg_bank: 0,
105            chr_bank: 0,
106            mirroring,
107        })
108    }
109}
110
111impl Mapper for Sachen133 {
112    fn caps(&self) -> MapperCaps {
113        MapperCaps::NONE
114    }
115
116    fn cpu_read(&mut self, addr: u16) -> u8 {
117        if (0x8000..=0xFFFF).contains(&addr) {
118            let count = (self.prg_rom.len() / PRG_BANK_32K).max(1);
119            let bank = (self.prg_bank as usize) % count;
120            self.prg_rom[bank * PRG_BANK_32K + (addr as usize - 0x8000)]
121        } else {
122            0
123        }
124    }
125
126    fn cpu_write(&mut self, addr: u16, value: u8) {
127        if (0x4100..=0x5FFF).contains(&addr) && (addr & 0x0100) != 0 {
128            self.prg_bank = (value >> 2) & 0x01;
129            self.chr_bank = value & 0x03;
130        }
131    }
132
133    fn ppu_read(&mut self, addr: u16) -> u8 {
134        let addr = addr & 0x3FFF;
135        match addr {
136            0x0000..=0x1FFF => {
137                let count = (self.chr_rom.len() / CHR_BANK_8K).max(1);
138                let bank = (self.chr_bank as usize) % count;
139                self.chr_rom[bank * CHR_BANK_8K + addr as usize]
140            }
141            0x2000..=0x3EFF => self.vram[nametable_offset(addr, self.mirroring)],
142            _ => 0,
143        }
144    }
145
146    fn ppu_write(&mut self, addr: u16, value: u8) {
147        let addr = addr & 0x3FFF;
148        if let 0x2000..=0x3EFF = addr {
149            let off = nametable_offset(addr, self.mirroring);
150            self.vram[off] = value;
151        }
152    }
153
154    fn current_mirroring(&self) -> Mirroring {
155        self.mirroring
156    }
157
158    fn save_state(&self) -> Vec<u8> {
159        let mut out = Vec::with_capacity(3 + self.vram.len());
160        out.push(SAVE_STATE_VERSION);
161        out.push(self.prg_bank);
162        out.push(self.chr_bank);
163        out.extend_from_slice(&self.vram);
164        out
165    }
166
167    fn load_state(&mut self, data: &[u8]) -> Result<(), MapperError> {
168        let expected = 3 + self.vram.len();
169        if data.len() != expected {
170            return Err(MapperError::WrongLength {
171                expected,
172                got: data.len(),
173            });
174        }
175        if data[0] != SAVE_STATE_VERSION {
176            return Err(MapperError::UnsupportedVersion(data[0]));
177        }
178        self.prg_bank = data[1];
179        self.chr_bank = data[2];
180        self.vram.copy_from_slice(&data[3..3 + self.vram.len()]);
181        Ok(())
182    }
183}
184
185// ===========================================================================
186// Mapper 145 — Sachen SA-72007.
187//
188// A single CHR-bank bit (the high data bit) is decoded when the address
189// satisfies (absolute & 0x4100) == 0x4100, in BOTH the $4100 register window
190// and the $6000 save-RAM window:
191//   CHR (8 KiB) = (value >> 7) & 0x01
192// PRG is a fixed 32 KiB (bank 0). Mirroring header-fixed; no IRQ.
193// ===========================================================================
194
195/// Mapper 145 (Sachen `SA-72007`).
196pub struct Sachen145 {
197    prg_rom: Box<[u8]>,
198    chr_rom: Box<[u8]>,
199    vram: Box<[u8]>,
200    chr_bank: u8,
201    mirroring: Mirroring,
202}
203
204impl Sachen145 {
205    /// Construct a new mapper 145 board.
206    ///
207    /// # Errors
208    ///
209    /// Returns [`MapperError::Invalid`] when PRG is empty / not a multiple of
210    /// 16 KiB or CHR-ROM is empty / not a multiple of 8 KiB.
211    pub fn new(
212        prg_rom: Box<[u8]>,
213        chr_rom: Box<[u8]>,
214        mirroring: Mirroring,
215    ) -> Result<Self, MapperError> {
216        // Real SA-72007 dumps (e.g. "Sidewinder") are 16 KiB PRG / NROM-128-style
217        // — the fixed bank is simply mirrored across the 32 KiB CPU window. Accept
218        // any non-zero 16 KiB multiple (16 KiB mirrors; 32 KiB maps 1:1).
219        if prg_rom.is_empty() || !prg_rom.len().is_multiple_of(PRG_BANK_16K) {
220            return Err(MapperError::Invalid(format!(
221                "mapper 145 PRG-ROM size {} is not a non-zero multiple of 16 KiB",
222                prg_rom.len()
223            )));
224        }
225        if chr_rom.is_empty() || !chr_rom.len().is_multiple_of(CHR_BANK_8K) {
226            return Err(MapperError::Invalid(format!(
227                "mapper 145 CHR-ROM size {} is not a non-zero multiple of 8 KiB",
228                chr_rom.len()
229            )));
230        }
231        Ok(Self {
232            prg_rom,
233            chr_rom,
234            vram: vec![0u8; 2 * NAMETABLE_SIZE].into_boxed_slice(),
235            chr_bank: 0,
236            mirroring,
237        })
238    }
239}
240
241impl Mapper for Sachen145 {
242    fn caps(&self) -> MapperCaps {
243        MapperCaps::NONE
244    }
245
246    fn cpu_read(&mut self, addr: u16) -> u8 {
247        if (0x8000..=0xFFFF).contains(&addr) {
248            // Fixed bank 0, mirrored across the 32 KiB window for sub-32 KiB PRG.
249            self.prg_rom[(addr as usize - 0x8000) % self.prg_rom.len()]
250        } else {
251            0
252        }
253    }
254
255    fn cpu_write(&mut self, addr: u16, value: u8) {
256        // CHR bank decoded when (addr & 0x4100) == 0x4100 in both the register
257        // ($4100-$5FFF) and save-RAM ($6000-$7FFF) windows.
258        if (0x4100..=0x7FFF).contains(&addr) && (addr & 0x4100) == 0x4100 {
259            self.chr_bank = (value >> 7) & 0x01;
260        }
261    }
262
263    fn ppu_read(&mut self, addr: u16) -> u8 {
264        let addr = addr & 0x3FFF;
265        match addr {
266            0x0000..=0x1FFF => {
267                let count = (self.chr_rom.len() / CHR_BANK_8K).max(1);
268                let bank = (self.chr_bank as usize) % count;
269                self.chr_rom[bank * CHR_BANK_8K + addr as usize]
270            }
271            0x2000..=0x3EFF => self.vram[nametable_offset(addr, self.mirroring)],
272            _ => 0,
273        }
274    }
275
276    fn ppu_write(&mut self, addr: u16, value: u8) {
277        let addr = addr & 0x3FFF;
278        if let 0x2000..=0x3EFF = addr {
279            let off = nametable_offset(addr, self.mirroring);
280            self.vram[off] = value;
281        }
282    }
283
284    fn current_mirroring(&self) -> Mirroring {
285        self.mirroring
286    }
287
288    fn save_state(&self) -> Vec<u8> {
289        let mut out = Vec::with_capacity(2 + self.vram.len());
290        out.push(SAVE_STATE_VERSION);
291        out.push(self.chr_bank);
292        out.extend_from_slice(&self.vram);
293        out
294    }
295
296    fn load_state(&mut self, data: &[u8]) -> Result<(), MapperError> {
297        let expected = 2 + self.vram.len();
298        if data.len() != expected {
299            return Err(MapperError::WrongLength {
300                expected,
301                got: data.len(),
302            });
303        }
304        if data[0] != SAVE_STATE_VERSION {
305            return Err(MapperError::UnsupportedVersion(data[0]));
306        }
307        self.chr_bank = data[1];
308        self.vram.copy_from_slice(&data[2..2 + self.vram.len()]);
309        Ok(())
310    }
311}
312
313// ===========================================================================
314// Mapper 146 — Sachen (mapper-79-equivalent behaviour).
315//
316// Identical decode to NINA-03 (mapper 79) but Sachen wired the register into
317// the $4100-$5FFF window decoded on A8 AND aliased into the $6000-$7FFF
318// save-RAM window (offset by $2000). The byte selects:
319//   PRG (32 KiB) = (value >> 3) & 0x01
320//   CHR (8 KiB)  =  value       & 0x07
321// Mirroring header-fixed; no IRQ.
322// ===========================================================================
323
324/// Mapper 146 (Sachen, `NINA-03`/mapper-79-equivalent behaviour).
325pub struct Sachen146 {
326    prg_rom: Box<[u8]>,
327    chr_rom: Box<[u8]>,
328    vram: Box<[u8]>,
329    prg_bank: u8,
330    chr_bank: u8,
331    mirroring: Mirroring,
332}
333
334impl Sachen146 {
335    /// Construct a new mapper 146 board.
336    ///
337    /// # Errors
338    ///
339    /// Returns [`MapperError::Invalid`] when PRG is not a non-zero multiple of
340    /// 32 KiB or CHR-ROM is empty / not a multiple of 8 KiB.
341    pub fn new(
342        prg_rom: Box<[u8]>,
343        chr_rom: Box<[u8]>,
344        mirroring: Mirroring,
345    ) -> Result<Self, MapperError> {
346        if prg_rom.is_empty() || !prg_rom.len().is_multiple_of(PRG_BANK_32K) {
347            return Err(MapperError::Invalid(format!(
348                "mapper 146 PRG-ROM size {} is not a non-zero multiple of 32 KiB",
349                prg_rom.len()
350            )));
351        }
352        if chr_rom.is_empty() || !chr_rom.len().is_multiple_of(CHR_BANK_8K) {
353            return Err(MapperError::Invalid(format!(
354                "mapper 146 CHR-ROM size {} is not a non-zero multiple of 8 KiB",
355                chr_rom.len()
356            )));
357        }
358        Ok(Self {
359            prg_rom,
360            chr_rom,
361            vram: vec![0u8; 2 * NAMETABLE_SIZE].into_boxed_slice(),
362            prg_bank: 0,
363            chr_bank: 0,
364            mirroring,
365        })
366    }
367
368    const fn apply(&mut self, value: u8) {
369        self.prg_bank = (value >> 3) & 0x01;
370        self.chr_bank = value & 0x07;
371    }
372}
373
374impl Mapper for Sachen146 {
375    fn caps(&self) -> MapperCaps {
376        MapperCaps::NONE
377    }
378
379    fn cpu_read(&mut self, addr: u16) -> u8 {
380        if (0x8000..=0xFFFF).contains(&addr) {
381            let count = (self.prg_rom.len() / PRG_BANK_32K).max(1);
382            let bank = (self.prg_bank as usize) % count;
383            self.prg_rom[bank * PRG_BANK_32K + (addr as usize - 0x8000)]
384        } else {
385            0
386        }
387    }
388
389    fn cpu_write(&mut self, addr: u16, value: u8) {
390        // $4100-$5FFF on A8, and the $6000-$7FFF save-RAM alias.
391        if ((0x4100..=0x5FFF).contains(&addr) && (addr & 0x0100) != 0)
392            || (0x6000..=0x7FFF).contains(&addr)
393        {
394            self.apply(value);
395        }
396    }
397
398    fn ppu_read(&mut self, addr: u16) -> u8 {
399        let addr = addr & 0x3FFF;
400        match addr {
401            0x0000..=0x1FFF => {
402                let count = (self.chr_rom.len() / CHR_BANK_8K).max(1);
403                let bank = (self.chr_bank as usize) % count;
404                self.chr_rom[bank * CHR_BANK_8K + addr as usize]
405            }
406            0x2000..=0x3EFF => self.vram[nametable_offset(addr, self.mirroring)],
407            _ => 0,
408        }
409    }
410
411    fn ppu_write(&mut self, addr: u16, value: u8) {
412        let addr = addr & 0x3FFF;
413        if let 0x2000..=0x3EFF = addr {
414            let off = nametable_offset(addr, self.mirroring);
415            self.vram[off] = value;
416        }
417    }
418
419    fn current_mirroring(&self) -> Mirroring {
420        self.mirroring
421    }
422
423    fn save_state(&self) -> Vec<u8> {
424        let mut out = Vec::with_capacity(3 + self.vram.len());
425        out.push(SAVE_STATE_VERSION);
426        out.push(self.prg_bank);
427        out.push(self.chr_bank);
428        out.extend_from_slice(&self.vram);
429        out
430    }
431
432    fn load_state(&mut self, data: &[u8]) -> Result<(), MapperError> {
433        let expected = 3 + self.vram.len();
434        if data.len() != expected {
435            return Err(MapperError::WrongLength {
436                expected,
437                got: data.len(),
438            });
439        }
440        if data[0] != SAVE_STATE_VERSION {
441            return Err(MapperError::UnsupportedVersion(data[0]));
442        }
443        self.prg_bank = data[1];
444        self.chr_bank = data[2];
445        self.vram.copy_from_slice(&data[3..3 + self.vram.len()]);
446        Ok(())
447    }
448}
449
450/// The TXC JV001 scrambling-accumulator chip (mapper 147). Distinct from the
451/// non-JV001 `TxcChip` in `txc.rs` (different register/output bit positions).
452/// The JV001 pre/post-scramble is a fixed hardware bit-permutation. Provenance:
453/// derived from puNES's `JV001.c` / `mapper_147.c` (GPL-2.0-or-later) — the
454/// bit-permutation is also documented in the nesdev wiki mapper-147 board notes.
455/// See NOTICE and docs/originality-and-provenance.md (Section 1).
456#[derive(Clone, Copy)]
457struct Jv001Chip {
458    accumulator: u8,
459    inverter: u8,
460    staging: u8,
461    output: u8,
462    increase: bool,
463    /// Full 0x00/0xFF mask (NOT a bool) — puNES stores `0xFF * (value & 1)` and
464    /// hard-resets it to 0xFF, so the very first handshake read inverts.
465    invert: u8,
466}
467
468impl Default for Jv001Chip {
469    fn default() -> Self {
470        Self {
471            accumulator: 0,
472            inverter: 0,
473            staging: 0,
474            output: 0,
475            increase: false,
476            // Hard-reset state (puNES init_JV001): invert latched high.
477            invert: 0xFF,
478        }
479    }
480}
481
482impl Jv001Chip {
483    /// The value the chip returns on a $4100 read (the protection handshake).
484    /// puNES: `((inverter ^ invert) & 0xF0) | (accumulator & 0x0F)`.
485    const fn read(self) -> u8 {
486        ((self.inverter ^ self.invert) & 0xF0) | (self.accumulator & 0x0F)
487    }
488
489    /// `absolute` is the full CPU address; `value` the written byte (already
490    /// mapper-147-pre-scrambled by the caller). Mirrors puNES
491    /// `extcl_cpu_wr_mem_JV001`.
492    const fn write(&mut self, absolute: u16, value: u8) {
493        if absolute < 0x8000 {
494            match absolute & 0x0103 {
495                0x0100 => {
496                    self.accumulator = if self.increase {
497                        self.accumulator.wrapping_add(1)
498                    } else {
499                        (self.accumulator & 0xF0) | ((self.staging ^ self.invert) & 0x0F)
500                    };
501                }
502                0x0101 => self.invert = if value & 0x01 != 0 { 0xFF } else { 0x00 },
503                0x0102 => {
504                    self.staging = value & 0x0F;
505                    self.inverter = value & 0xF0;
506                }
507                0x0103 => self.increase = (value & 0x01) != 0,
508                _ => {}
509            }
510        } else {
511            // A $8000-$FFFF access refreshes the bank-output latch.
512            self.output = (self.inverter & 0xF0) | (self.accumulator & 0x0F);
513        }
514    }
515}
516
517/// Mapper 147 (Sachen 3018 / TXC `JV001`).
518pub struct Sachen3018M147 {
519    prg_rom: Box<[u8]>,
520    chr_rom: Box<[u8]>,
521    vram: Box<[u8]>,
522    chr_is_ram: bool,
523    jv001: Jv001Chip,
524    mirroring: Mirroring,
525}
526
527impl Sachen3018M147 {
528    /// Construct a new mapper 147 board.
529    ///
530    /// # Errors
531    ///
532    /// Returns [`MapperError::Invalid`] when PRG is not a non-zero multiple of
533    /// 32 KiB, or CHR-ROM (when present) is not a multiple of 8 KiB.
534    pub fn new(
535        prg_rom: Box<[u8]>,
536        chr_rom: Box<[u8]>,
537        mirroring: Mirroring,
538    ) -> Result<Self, MapperError> {
539        if prg_rom.is_empty() || !prg_rom.len().is_multiple_of(PRG_BANK_32K) {
540            return Err(MapperError::Invalid(format!(
541                "mapper 147 PRG-ROM size {} is not a non-zero multiple of 32 KiB",
542                prg_rom.len()
543            )));
544        }
545        let chr_is_ram = chr_rom.is_empty();
546        let chr_rom: Box<[u8]> = if chr_is_ram {
547            vec![0u8; CHR_BANK_8K].into_boxed_slice()
548        } else if chr_rom.len().is_multiple_of(CHR_BANK_8K) {
549            chr_rom
550        } else {
551            return Err(MapperError::Invalid(format!(
552                "mapper 147 CHR-ROM size {} is not a multiple of 8 KiB",
553                chr_rom.len()
554            )));
555        };
556        Ok(Self {
557            prg_rom,
558            chr_rom,
559            vram: vec![0u8; 2 * NAMETABLE_SIZE].into_boxed_slice(),
560            chr_is_ram,
561            jv001: Jv001Chip::default(),
562            mirroring,
563        })
564    }
565
566    /// PRG 32 KiB bank from the chip output latch (puNES `prg_fix_jv001_147`).
567    const fn prg_bank(&self) -> usize {
568        (((self.jv001.output & 0x20) >> 4) | (self.jv001.output & 0x01)) as usize
569    }
570
571    /// CHR 8 KiB bank from the chip output latch (puNES `chr_fix_jv001_147`).
572    const fn chr_bank(&self) -> usize {
573        ((self.jv001.output & 0x1E) >> 1) as usize
574    }
575
576    fn read_prg(&self, addr: u16) -> u8 {
577        let count = (self.prg_rom.len() / PRG_BANK_32K).max(1);
578        let bank = self.prg_bank() % count;
579        self.prg_rom[bank * PRG_BANK_32K + (addr as usize - 0x8000)]
580    }
581
582    fn chr_offset(&self, addr: u16) -> usize {
583        let count = (self.chr_rom.len() / CHR_BANK_8K).max(1);
584        let bank = self.chr_bank() % count;
585        bank * CHR_BANK_8K + addr as usize
586    }
587}
588
589impl Mapper for Sachen3018M147 {
590    fn caps(&self) -> MapperCaps {
591        MapperCaps::NONE
592    }
593
594    fn cpu_read_unmapped(&self, addr: u16) -> bool {
595        // v2.7.2 (core audit §5.5): with no save RAM, nothing drives
596        // `$6000-$7FFF` and it floats; see `Mapper::cpu_read_unmapped`.
597        (matches!(addr, 0x6000..=0x7FFF) && self.sram().is_empty()) || {
598            // The JV001 protection register answers reads at $4100 (decoded on
599            // A0/A1 == 0). Everything else in $4020-$5FFF is open bus.
600            !((0x4100..=0x5FFF).contains(&addr) && (addr & 0x0103) == 0x0100)
601                && (0x4020..=0x5FFF).contains(&addr)
602        }
603    }
604
605    fn cpu_read(&mut self, addr: u16) -> u8 {
606        match addr {
607            // JV001 protection handshake read. The mapper-147 board post-
608            // scrambles the chip read: ((v & 0x3F) << 2) | ((v & 0xC0) >> 6)
609            // (puNES extcl_cpu_rd_mem_147).
610            0x4100..=0x5FFF if (addr & 0x0103) == 0x0100 => {
611                let v = self.jv001.read();
612                ((v & 0x3F) << 2) | ((v & 0xC0) >> 6)
613            }
614            0x8000..=0xFFFF => self.read_prg(addr),
615            _ => 0,
616        }
617    }
618
619    fn cpu_write(&mut self, addr: u16, value: u8) {
620        // The mapper-147 board pre-scrambles every write to the JV001:
621        // ((value & 0x03) << 6) | ((value & 0xFC) >> 2) (puNES
622        // extcl_cpu_wr_mem_147).
623        let scramble = |v: u8| ((v & 0x03) << 6) | ((v & 0xFC) >> 2);
624        match addr {
625            0x4100..=0x5FFF => self.jv001.write(addr, scramble(value)),
626            0x8000..=0xFFFF => {
627                // Bus conflict in the PRG window; the write refreshes the latch.
628                let effective = value & self.read_prg(addr);
629                self.jv001.write(addr, scramble(effective));
630            }
631            _ => {}
632        }
633    }
634
635    fn ppu_read(&mut self, addr: u16) -> u8 {
636        let addr = addr & 0x3FFF;
637        match addr {
638            0x0000..=0x1FFF => self.chr_rom[self.chr_offset(addr)],
639            0x2000..=0x3EFF => self.vram[nametable_offset(addr, self.mirroring)],
640            _ => 0,
641        }
642    }
643
644    fn ppu_write(&mut self, addr: u16, value: u8) {
645        let addr = addr & 0x3FFF;
646        match addr {
647            0x0000..=0x1FFF => {
648                if self.chr_is_ram {
649                    let off = self.chr_offset(addr);
650                    self.chr_rom[off] = value;
651                }
652            }
653            0x2000..=0x3EFF => {
654                let off = nametable_offset(addr, self.mirroring);
655                self.vram[off] = value;
656            }
657            _ => {}
658        }
659    }
660
661    fn current_mirroring(&self) -> Mirroring {
662        self.mirroring
663    }
664
665    fn save_state(&self) -> Vec<u8> {
666        let chr_extra = if self.chr_is_ram {
667            self.chr_rom.len()
668        } else {
669            0
670        };
671        // 6 JV001 fields (accumulator, inverter, staging, output, increase, invert).
672        let mut out = Vec::with_capacity(7 + self.vram.len() + chr_extra);
673        out.push(SAVE_STATE_VERSION);
674        out.push(self.jv001.accumulator);
675        out.push(self.jv001.inverter);
676        out.push(self.jv001.staging);
677        out.push(self.jv001.output);
678        out.push(u8::from(self.jv001.increase));
679        out.push(self.jv001.invert); // full 0x00/0xFF mask
680        out.extend_from_slice(&self.vram);
681        if self.chr_is_ram {
682            out.extend_from_slice(&self.chr_rom);
683        }
684        out
685    }
686
687    fn load_state(&mut self, data: &[u8]) -> Result<(), MapperError> {
688        let chr_extra = if self.chr_is_ram {
689            self.chr_rom.len()
690        } else {
691            0
692        };
693        let expected = 7 + self.vram.len() + chr_extra;
694        if data.len() != expected {
695            return Err(MapperError::WrongLength {
696                expected,
697                got: data.len(),
698            });
699        }
700        if data[0] != SAVE_STATE_VERSION {
701            return Err(MapperError::UnsupportedVersion(data[0]));
702        }
703        self.jv001.accumulator = data[1];
704        self.jv001.inverter = data[2];
705        self.jv001.staging = data[3];
706        self.jv001.output = data[4];
707        self.jv001.increase = data[5] != 0;
708        self.jv001.invert = data[6]; // full 0x00/0xFF mask
709        let mut cursor = 7;
710        self.vram
711            .copy_from_slice(&data[cursor..cursor + self.vram.len()]);
712        cursor += self.vram.len();
713        if self.chr_is_ram {
714            self.chr_rom
715                .copy_from_slice(&data[cursor..cursor + self.chr_rom.len()]);
716        }
717        Ok(())
718    }
719}
720
721// ===========================================================================
722// Mapper 148 — Sachen SA-008-A / Tengen 800008.
723//
724// The mapper-79 bit layout (`.... PCCC`: CHR = bits 0-2, PRG = bit 3) moved
725// into the $8000-$FFFF window, introducing bus conflicts:
726//   PRG (32 KiB) = (value >> 3) & 0x01
727//   CHR (8 KiB)  = value & 0x07
728// Mirroring header-fixed; no IRQ.
729// ===========================================================================
730
731/// Mapper 148 (Sachen `SA-008-A` / Tengen 800008).
732pub struct Sachen148 {
733    prg_rom: Box<[u8]>,
734    chr_rom: Box<[u8]>,
735    vram: Box<[u8]>,
736    chr_is_ram: bool,
737    prg_bank: u8,
738    chr_bank: u8,
739    mirroring: Mirroring,
740}
741
742impl Sachen148 {
743    /// Construct a new mapper 148 board.
744    ///
745    /// # Errors
746    ///
747    /// Returns [`MapperError::Invalid`] when PRG is not a non-zero multiple of
748    /// 32 KiB, or CHR-ROM (when present) is not a multiple of 8 KiB.
749    pub fn new(
750        prg_rom: Box<[u8]>,
751        chr_rom: Box<[u8]>,
752        mirroring: Mirroring,
753    ) -> Result<Self, MapperError> {
754        if prg_rom.is_empty() || !prg_rom.len().is_multiple_of(PRG_BANK_32K) {
755            return Err(MapperError::Invalid(format!(
756                "mapper 148 PRG-ROM size {} is not a non-zero multiple of 32 KiB",
757                prg_rom.len()
758            )));
759        }
760        let chr_is_ram = chr_rom.is_empty();
761        let chr_rom: Box<[u8]> = if chr_is_ram {
762            vec![0u8; CHR_BANK_8K].into_boxed_slice()
763        } else if chr_rom.len().is_multiple_of(CHR_BANK_8K) {
764            chr_rom
765        } else {
766            return Err(MapperError::Invalid(format!(
767                "mapper 148 CHR-ROM size {} is not a multiple of 8 KiB",
768                chr_rom.len()
769            )));
770        };
771        Ok(Self {
772            prg_rom,
773            chr_rom,
774            vram: vec![0u8; 2 * NAMETABLE_SIZE].into_boxed_slice(),
775            chr_is_ram,
776            prg_bank: 0,
777            chr_bank: 0,
778            mirroring,
779        })
780    }
781
782    fn read_prg(&self, addr: u16) -> u8 {
783        let count = (self.prg_rom.len() / PRG_BANK_32K).max(1);
784        let bank = (self.prg_bank as usize) % count;
785        self.prg_rom[bank * PRG_BANK_32K + (addr as usize - 0x8000)]
786    }
787
788    fn chr_offset(&self, addr: u16) -> usize {
789        let count = (self.chr_rom.len() / CHR_BANK_8K).max(1);
790        let bank = (self.chr_bank as usize) % count;
791        bank * CHR_BANK_8K + addr as usize
792    }
793}
794
795impl Mapper for Sachen148 {
796    fn caps(&self) -> MapperCaps {
797        MapperCaps::NONE
798    }
799
800    fn cpu_read(&mut self, addr: u16) -> u8 {
801        if (0x8000..=0xFFFF).contains(&addr) {
802            self.read_prg(addr)
803        } else {
804            0
805        }
806    }
807
808    fn cpu_write(&mut self, addr: u16, value: u8) {
809        if (0x8000..=0xFFFF).contains(&addr) {
810            // Bus conflict.
811            let effective = value & self.read_prg(addr);
812            self.prg_bank = (effective >> 3) & 0x01;
813            self.chr_bank = effective & 0x07;
814        }
815    }
816
817    fn ppu_read(&mut self, addr: u16) -> u8 {
818        let addr = addr & 0x3FFF;
819        match addr {
820            0x0000..=0x1FFF => self.chr_rom[self.chr_offset(addr)],
821            0x2000..=0x3EFF => self.vram[nametable_offset(addr, self.mirroring)],
822            _ => 0,
823        }
824    }
825
826    fn ppu_write(&mut self, addr: u16, value: u8) {
827        let addr = addr & 0x3FFF;
828        match addr {
829            0x0000..=0x1FFF => {
830                if self.chr_is_ram {
831                    let off = self.chr_offset(addr);
832                    self.chr_rom[off] = value;
833                }
834            }
835            0x2000..=0x3EFF => {
836                let off = nametable_offset(addr, self.mirroring);
837                self.vram[off] = value;
838            }
839            _ => {}
840        }
841    }
842
843    fn current_mirroring(&self) -> Mirroring {
844        self.mirroring
845    }
846
847    fn save_state(&self) -> Vec<u8> {
848        let chr_extra = if self.chr_is_ram {
849            self.chr_rom.len()
850        } else {
851            0
852        };
853        let mut out = Vec::with_capacity(3 + self.vram.len() + chr_extra);
854        out.push(SAVE_STATE_VERSION);
855        out.push(self.prg_bank);
856        out.push(self.chr_bank);
857        out.extend_from_slice(&self.vram);
858        if self.chr_is_ram {
859            out.extend_from_slice(&self.chr_rom);
860        }
861        out
862    }
863
864    fn load_state(&mut self, data: &[u8]) -> Result<(), MapperError> {
865        let chr_extra = if self.chr_is_ram {
866            self.chr_rom.len()
867        } else {
868            0
869        };
870        let expected = 3 + self.vram.len() + chr_extra;
871        if data.len() != expected {
872            return Err(MapperError::WrongLength {
873                expected,
874                got: data.len(),
875            });
876        }
877        if data[0] != SAVE_STATE_VERSION {
878            return Err(MapperError::UnsupportedVersion(data[0]));
879        }
880        self.prg_bank = data[1];
881        self.chr_bank = data[2];
882        let mut cursor = 3;
883        self.vram
884            .copy_from_slice(&data[cursor..cursor + self.vram.len()]);
885        cursor += self.vram.len();
886        if self.chr_is_ram {
887            self.chr_rom
888                .copy_from_slice(&data[cursor..cursor + self.chr_rom.len()]);
889        }
890        Ok(())
891    }
892}
893
894// ===========================================================================
895// Mapper 149 — Sachen SA-0036.
896//
897// CNROM-like: fixed 32 KiB PRG, switchable 8 KiB CHR. The CHR bank is a single
898// bit in bit 7 of the value written to $8000-$FFFF, with bus conflicts:
899//   CHR (8 KiB) = (value >> 7) & 0x01
900// Mirroring header-fixed; no IRQ.
901// ===========================================================================
902
903/// Mapper 149 (Sachen `SA-0036`).
904pub struct Sachen149 {
905    prg_rom: Box<[u8]>,
906    chr_rom: Box<[u8]>,
907    vram: Box<[u8]>,
908    chr_bank: u8,
909    mirroring: Mirroring,
910}
911
912impl Sachen149 {
913    /// Construct a new mapper 149 board.
914    ///
915    /// # Errors
916    ///
917    /// Returns [`MapperError::Invalid`] when PRG is not a non-zero multiple of
918    /// 32 KiB or CHR-ROM is empty / not a multiple of 8 KiB.
919    pub fn new(
920        prg_rom: Box<[u8]>,
921        chr_rom: Box<[u8]>,
922        mirroring: Mirroring,
923    ) -> Result<Self, MapperError> {
924        if prg_rom.is_empty() || !prg_rom.len().is_multiple_of(PRG_BANK_32K) {
925            return Err(MapperError::Invalid(format!(
926                "mapper 149 PRG-ROM size {} is not a non-zero multiple of 32 KiB",
927                prg_rom.len()
928            )));
929        }
930        if chr_rom.is_empty() || !chr_rom.len().is_multiple_of(CHR_BANK_8K) {
931            return Err(MapperError::Invalid(format!(
932                "mapper 149 CHR-ROM size {} is not a non-zero multiple of 8 KiB",
933                chr_rom.len()
934            )));
935        }
936        Ok(Self {
937            prg_rom,
938            chr_rom,
939            vram: vec![0u8; 2 * NAMETABLE_SIZE].into_boxed_slice(),
940            chr_bank: 0,
941            mirroring,
942        })
943    }
944
945    fn read_prg(&self, addr: u16) -> u8 {
946        // Fixed first 32 KiB bank.
947        self.prg_rom[addr as usize - 0x8000]
948    }
949}
950
951impl Mapper for Sachen149 {
952    fn caps(&self) -> MapperCaps {
953        MapperCaps::NONE
954    }
955
956    fn cpu_read(&mut self, addr: u16) -> u8 {
957        if (0x8000..=0xFFFF).contains(&addr) {
958            self.read_prg(addr)
959        } else {
960            0
961        }
962    }
963
964    fn cpu_write(&mut self, addr: u16, value: u8) {
965        if (0x8000..=0xFFFF).contains(&addr) {
966            // Bus conflict.
967            let effective = value & self.read_prg(addr);
968            self.chr_bank = (effective >> 7) & 0x01;
969        }
970    }
971
972    fn ppu_read(&mut self, addr: u16) -> u8 {
973        let addr = addr & 0x3FFF;
974        match addr {
975            0x0000..=0x1FFF => {
976                let count = (self.chr_rom.len() / CHR_BANK_8K).max(1);
977                let bank = (self.chr_bank as usize) % count;
978                self.chr_rom[bank * CHR_BANK_8K + addr as usize]
979            }
980            0x2000..=0x3EFF => self.vram[nametable_offset(addr, self.mirroring)],
981            _ => 0,
982        }
983    }
984
985    fn ppu_write(&mut self, addr: u16, value: u8) {
986        let addr = addr & 0x3FFF;
987        if let 0x2000..=0x3EFF = addr {
988            let off = nametable_offset(addr, self.mirroring);
989            self.vram[off] = value;
990        }
991    }
992
993    fn current_mirroring(&self) -> Mirroring {
994        self.mirroring
995    }
996
997    fn save_state(&self) -> Vec<u8> {
998        let mut out = Vec::with_capacity(2 + self.vram.len());
999        out.push(SAVE_STATE_VERSION);
1000        out.push(self.chr_bank);
1001        out.extend_from_slice(&self.vram);
1002        out
1003    }
1004
1005    fn load_state(&mut self, data: &[u8]) -> Result<(), MapperError> {
1006        let expected = 2 + self.vram.len();
1007        if data.len() != expected {
1008            return Err(MapperError::WrongLength {
1009                expected,
1010                got: data.len(),
1011            });
1012        }
1013        if data[0] != SAVE_STATE_VERSION {
1014            return Err(MapperError::UnsupportedVersion(data[0]));
1015        }
1016        self.chr_bank = data[1];
1017        self.vram.copy_from_slice(&data[2..2 + self.vram.len()]);
1018        Ok(())
1019    }
1020}
1021
1022// ===========================================================================
1023// Mapper 150 — Sachen SA-015 / SA-630 (UNL-Sachen-74LS374N).
1024//
1025// An eight-register ASIC at $4100 (register index, write) / $4101
1026// (register data, read+write). Both decode on the $C101 mask: A8 selects
1027// index ($4100) vs. data ($4101). Each register holds 3 bits and is fully
1028// readable (Shogi Gakuen checks this as protection). Banking is derived from
1029// the registers:
1030//   PRG (32 KiB) = reg[5] & 0x03
1031//   CHR (8 KiB)  = ((reg[4] & 0x01) << 2) | (reg[6] & 0x03)
1032//   mirroring (reg[7] >> 1) & 0x03:
1033//       0: custom S0-S0-S0-S1 (lower-right unique)
1034//       1: Horizontal
1035//       2: Vertical
1036//       3: Single-screen A
1037// Reads at $4101 return (open_bus & 0xF8) | (reg[index] & 0x07): the ASIC
1038// drives D2-D0 only. `cpu_read_driven_mask` reports that, and the bus keeps
1039// its floating value on D7-D3 (v2.7.2, core audit §4.5; previously those bits
1040// read as 0 and the bus latched the zeros).
1041// Writes are also accepted via the $6000-$7FFF mirror (addr | 0x1000). No IRQ.
1042// ===========================================================================
1043
1044/// Mapper 150 (Sachen `SA-015`/`SA-630`, `UNL-Sachen-74LS374N`).
1045/// Which PCB the SA-020A ASIC is wired into.
1046///
1047/// The chip is the same eight-register part in both cases -- the NESdev wiki
1048/// records this explicitly under mapper 243's Errata: "The SA-150 PCB, denoted
1049/// by INES Mapper 150, connects the same ASIC differently, changing the meaning
1050/// of the CHR-bank-related register bits." Only the bank decode differs; the
1051/// register file, the $4100/$4101 index/data protocol, the readback, and the
1052/// four mirroring modes are shared.
1053#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1054pub enum Sa020aBoard {
1055    /// SA-150 PCB (iNES mapper 150).
1056    Sa150,
1057    /// SA-020A PCB (iNES mapper 243) -- Honey Peach / Mei Nu Quan (SA-006).
1058    Sa020a,
1059}
1060
1061impl Sa020aBoard {
1062    /// The iNES mapper number this PCB is denoted by (used in error text).
1063    const fn ines_id(self) -> u16 {
1064        match self {
1065            Self::Sa150 => 150,
1066            Self::Sa020a => 243,
1067        }
1068    }
1069}
1070
1071/// Sachen SA-020A eight-register ASIC (iNES mappers **150 and 243**).
1072///
1073/// A fake-"74LS374N"-marked part with eight three-bit registers reached through
1074/// an index/data pair at `$4100`/`$4101` (mask `$C101`), driving one switchable
1075/// 32 KiB PRG bank, one switchable 8 KiB CHR bank, and four mirroring modes --
1076/// one of which is the non-standard S0-S0-S0-S1 layout.
1077///
1078/// The type is named for mapper 150 for historical reasons but backs both PCBs
1079/// the ASIC shipped on; see [`Sa020aBoard`]. Construct via [`Sachen150::new`]
1080/// for the SA-150 or [`Sachen150::new_on_board`] to pick explicitly.
1081pub struct Sachen150 {
1082    prg_rom: Box<[u8]>,
1083    chr_rom: Box<[u8]>,
1084    vram: Box<[u8]>,
1085    chr_is_ram: bool,
1086    current_register: u8,
1087    reg: [u8; 8],
1088    board: Sa020aBoard,
1089}
1090
1091impl Sachen150 {
1092    /// Construct a new mapper 150 board.
1093    ///
1094    /// # Errors
1095    ///
1096    /// Returns [`MapperError::Invalid`] when PRG is not a non-zero multiple of
1097    /// 32 KiB, or CHR-ROM (when present) is not a multiple of 8 KiB.
1098    pub fn new(prg_rom: Box<[u8]>, chr_rom: Box<[u8]>) -> Result<Self, MapperError> {
1099        Self::new_on_board(prg_rom, chr_rom, Sa020aBoard::Sa150)
1100    }
1101
1102    /// Construct the SA-020A ASIC on an explicit PCB.
1103    ///
1104    /// # Errors
1105    ///
1106    /// Returns [`MapperError::Invalid`] when PRG is not a non-zero multiple of
1107    /// 32 KiB, or CHR-ROM (when present) is not a multiple of 8 KiB.
1108    pub fn new_on_board(
1109        prg_rom: Box<[u8]>,
1110        chr_rom: Box<[u8]>,
1111        board: Sa020aBoard,
1112    ) -> Result<Self, MapperError> {
1113        if prg_rom.is_empty() || !prg_rom.len().is_multiple_of(PRG_BANK_32K) {
1114            return Err(MapperError::Invalid(format!(
1115                "mapper {} PRG-ROM size {} is not a non-zero multiple of 32 KiB",
1116                board.ines_id(),
1117                prg_rom.len()
1118            )));
1119        }
1120        let chr_is_ram = chr_rom.is_empty();
1121        let chr_rom: Box<[u8]> = if chr_is_ram {
1122            vec![0u8; CHR_BANK_8K].into_boxed_slice()
1123        } else if chr_rom.len().is_multiple_of(CHR_BANK_8K) {
1124            chr_rom
1125        } else {
1126            return Err(MapperError::Invalid(format!(
1127                "mapper {} CHR-ROM size {} is not a multiple of 8 KiB",
1128                board.ines_id(),
1129                chr_rom.len()
1130            )));
1131        };
1132        Ok(Self {
1133            prg_rom,
1134            chr_rom,
1135            vram: vec![0u8; 2 * NAMETABLE_SIZE].into_boxed_slice(),
1136            chr_is_ram,
1137            current_register: 0,
1138            reg: [0u8; 8],
1139            board,
1140        })
1141    }
1142
1143    // puNES prg_fix_150 / chr_fix_150 (the non-243 branch):
1144    //   PRG 32 KiB = reg[5] | (reg[2] & 0x01)
1145    //   CHR  8 KiB = (reg[2] << 3) | ((reg[4] & 0x01) << 2) | (reg[6] & 0x03)
1146    // The old decode masked PRG to reg[5] & 0x03 (dropping reg[2].0) and omitted
1147    // the reg[2]<<3 CHR term, so both banks resolved wrong -> blank/garbled.
1148    const fn prg_bank(&self) -> u8 {
1149        match self.board {
1150            Sa020aBoard::Sa150 => self.reg[5] | (self.reg[2] & 0x01),
1151            // Mapper 243: R5 supplies PRG A16..A15 and nothing else does, so the
1152            // 32 KiB bank is just those two bits -- 4 banks, the documented
1153            // 128 KiB maximum. R2 does NOT contribute here; that is one half of
1154            // what "connects the same ASIC differently" means.
1155            Sa020aBoard::Sa020a => self.reg[5] & 0x03,
1156        }
1157    }
1158
1159    const fn chr_bank(&self) -> u8 {
1160        match self.board {
1161            Sa020aBoard::Sa150 => {
1162                ((self.reg[2] & 0x01) << 3) | ((self.reg[4] & 0x01) << 2) | (self.reg[6] & 0x03)
1163            }
1164            // Mapper 243 assembles the 8 KiB CHR bank from, low to high:
1165            // A13 = R2.0, A14 = R4.0, A15 = R6.0, A16 = R6.1. Four bits, 16
1166            // banks, the documented 128 KiB maximum. Note this is the SAME three
1167            // registers as the SA-150 above but at inverted significance -- R2
1168            // is the LSB here and the MSB there, which is exactly why a ROM for
1169            // one board renders garbage on the other.
1170            Sa020aBoard::Sa020a => {
1171                (self.reg[2] & 0x01) | ((self.reg[4] & 0x01) << 1) | ((self.reg[6] & 0x03) << 2)
1172            }
1173        }
1174    }
1175
1176    /// Mirroring selector value `(reg[7] >> 1) & 0x03`.
1177    const fn mirror_sel(&self) -> u8 {
1178        (self.reg[7] >> 1) & 0x03
1179    }
1180
1181    const fn write_register(&mut self, addr: u16, value: u8) {
1182        match addr & 0x0101 {
1183            0x0100 => self.current_register = value & 0x07,
1184            0x0101 => self.reg[(self.current_register & 0x07) as usize] = value & 0x07,
1185            _ => {}
1186        }
1187    }
1188
1189    fn read_prg(&self, addr: u16) -> u8 {
1190        let count = (self.prg_rom.len() / PRG_BANK_32K).max(1);
1191        let bank = (self.prg_bank() as usize) % count;
1192        self.prg_rom[bank * PRG_BANK_32K + (addr as usize - 0x8000)]
1193    }
1194
1195    fn chr_offset(&self, addr: u16) -> usize {
1196        let count = (self.chr_rom.len() / CHR_BANK_8K).max(1);
1197        let bank = (self.chr_bank() as usize) % count;
1198        bank * CHR_BANK_8K + addr as usize
1199    }
1200}
1201
1202impl Mapper for Sachen150 {
1203    fn caps(&self) -> MapperCaps {
1204        MapperCaps::NONE
1205    }
1206
1207    fn cpu_read_unmapped(&self, addr: u16) -> bool {
1208        // v2.7.2 (core audit §5.5): with no save RAM, nothing drives
1209        // `$6000-$7FFF` and it floats; see `Mapper::cpu_read_unmapped`.
1210        (matches!(addr, 0x6000..=0x7FFF) && self.sram().is_empty()) || {
1211            // $4100-$5FFF has the readable protection register at $4101 (decoded
1212            // on A8); $4020-$40FF and $4200+ without A8 are open bus.
1213            (0x4020..=0x5FFF).contains(&addr) && (addr & 0x0101) != 0x0101
1214        }
1215    }
1216
1217    /// The data register drives D2-D0 only; D7-D3 float
1218    /// (`nesdev_wiki/INES_Mapper_150.xhtml`: "Register data ... .... .RRR").
1219    fn cpu_read_driven_mask(&self, addr: u16) -> u8 {
1220        if (0x4100..=0x5FFF).contains(&addr) && (addr & 0x0101) == 0x0101 {
1221            0x07
1222        } else {
1223            0xFF
1224        }
1225    }
1226
1227    fn cpu_read(&mut self, addr: u16) -> u8 {
1228        match addr {
1229            0x4100..=0x5FFF if (addr & 0x0101) == 0x0101 => {
1230                // D7-D3 are undriven; see `cpu_read_driven_mask`.
1231                self.reg[(self.current_register & 0x07) as usize] & 0x07
1232            }
1233            0x8000..=0xFFFF => self.read_prg(addr),
1234            _ => 0,
1235        }
1236    }
1237
1238    fn cpu_write(&mut self, addr: u16, value: u8) {
1239        match addr {
1240            0x4100..=0x5FFF => self.write_register(addr, value),
1241            // $6000-$7FFF mirror: the ASIC sees these as register writes at
1242            // (addr + 0x1000) per the SaveRAM-mapped register path.
1243            0x6000..=0x7FFF => self.write_register(addr.wrapping_add(0x1000), value),
1244            _ => {}
1245        }
1246    }
1247
1248    fn ppu_read(&mut self, addr: u16) -> u8 {
1249        let addr = addr & 0x3FFF;
1250        match addr {
1251            0x0000..=0x1FFF => self.chr_rom[self.chr_offset(addr)],
1252            0x2000..=0x3EFF => self.vram[self.resolve_nametable(addr)],
1253            _ => 0,
1254        }
1255    }
1256
1257    fn ppu_write(&mut self, addr: u16, value: u8) {
1258        let addr = addr & 0x3FFF;
1259        match addr {
1260            0x0000..=0x1FFF => {
1261                if self.chr_is_ram {
1262                    let off = self.chr_offset(addr);
1263                    self.chr_rom[off] = value;
1264                }
1265            }
1266            0x2000..=0x3EFF => {
1267                let off = self.resolve_nametable(addr);
1268                self.vram[off] = value;
1269            }
1270            _ => {}
1271        }
1272    }
1273
1274    fn current_mirroring(&self) -> Mirroring {
1275        match self.mirror_sel() {
1276            1 => Mirroring::Horizontal,
1277            2 => Mirroring::Vertical,
1278            3 => Mirroring::SingleScreenA,
1279            // 0 = custom S0-S0-S0-S1; report as MapperControlled (the PPU
1280            // routes through our resolve_nametable for that case).
1281            _ => Mirroring::MapperControlled,
1282        }
1283    }
1284
1285    #[allow(clippy::cast_possible_truncation)]
1286    fn nametable_address(&self, addr: u16) -> u16 {
1287        // CIRAM offset is always < 0x800, so the truncation is a no-op.
1288        self.resolve_nametable(addr) as u16
1289    }
1290
1291    fn save_state(&self) -> Vec<u8> {
1292        let chr_extra = if self.chr_is_ram {
1293            self.chr_rom.len()
1294        } else {
1295            0
1296        };
1297        let mut out = Vec::with_capacity(2 + 8 + self.vram.len() + chr_extra);
1298        out.push(SAVE_STATE_VERSION);
1299        out.push(self.current_register);
1300        out.extend_from_slice(&self.reg);
1301        out.extend_from_slice(&self.vram);
1302        if self.chr_is_ram {
1303            out.extend_from_slice(&self.chr_rom);
1304        }
1305        out
1306    }
1307
1308    fn load_state(&mut self, data: &[u8]) -> Result<(), MapperError> {
1309        let chr_extra = if self.chr_is_ram {
1310            self.chr_rom.len()
1311        } else {
1312            0
1313        };
1314        let expected = 2 + 8 + self.vram.len() + chr_extra;
1315        if data.len() != expected {
1316            return Err(MapperError::WrongLength {
1317                expected,
1318                got: data.len(),
1319            });
1320        }
1321        if data[0] != SAVE_STATE_VERSION {
1322            return Err(MapperError::UnsupportedVersion(data[0]));
1323        }
1324        self.current_register = data[1];
1325        let mut cursor = 2;
1326        self.reg.copy_from_slice(&data[cursor..cursor + 8]);
1327        cursor += 8;
1328        self.vram
1329            .copy_from_slice(&data[cursor..cursor + self.vram.len()]);
1330        cursor += self.vram.len();
1331        if self.chr_is_ram {
1332            self.chr_rom
1333                .copy_from_slice(&data[cursor..cursor + self.chr_rom.len()]);
1334        }
1335        Ok(())
1336    }
1337}
1338
1339impl Sachen150 {
1340    /// Resolve a nametable address to a CIRAM offset (`0..0x800`), applying the
1341    /// custom S0-S0-S0-S1 layout for mirroring selector 0 and the standard
1342    /// layouts otherwise.
1343    const fn resolve_nametable(&self, addr: u16) -> usize {
1344        let local = (addr as usize) & (NAMETABLE_SIZE - 1);
1345        match self.mirror_sel() {
1346            // Custom S0-S0-S0-S1: tables 0/1/2 -> bank 0, table 3 -> bank 1.
1347            0 => {
1348                let table = (((addr - 0x2000) / NAMETABLE_SIZE_U16) & 0x03) as usize;
1349                let physical = if table == 3 { 1 } else { 0 };
1350                physical * NAMETABLE_SIZE + local
1351            }
1352            1 => nametable_offset(addr, Mirroring::Horizontal),
1353            3 => nametable_offset(addr, Mirroring::SingleScreenA),
1354            // selector 2 (vertical) and any stray value default to vertical.
1355            _ => nametable_offset(addr, Mirroring::Vertical),
1356        }
1357    }
1358}
1359
1360/// Mapper 143 (Sachen `TCA01`).
1361pub struct SachenTca01M143 {
1362    prg_rom: Box<[u8]>,
1363    chr_rom: Box<[u8]>,
1364    vram: Box<[u8]>,
1365    mirroring: Mirroring,
1366}
1367
1368impl SachenTca01M143 {
1369    /// Construct a new mapper 143 board.
1370    ///
1371    /// # Errors
1372    ///
1373    /// Returns [`MapperError::Invalid`] when PRG is not a non-zero multiple of
1374    /// 16 KiB or CHR-ROM is empty / not a multiple of 8 KiB.
1375    pub fn new(
1376        prg_rom: Box<[u8]>,
1377        chr_rom: Box<[u8]>,
1378        mirroring: Mirroring,
1379    ) -> Result<Self, MapperError> {
1380        if prg_rom.is_empty() || !prg_rom.len().is_multiple_of(PRG_BANK_16K) {
1381            return Err(MapperError::Invalid(format!(
1382                "mapper 143 PRG-ROM size {} is not a non-zero multiple of 16 KiB",
1383                prg_rom.len()
1384            )));
1385        }
1386        if chr_rom.is_empty() || !chr_rom.len().is_multiple_of(CHR_BANK_8K) {
1387            return Err(MapperError::Invalid(format!(
1388                "mapper 143 CHR-ROM size {} is not a non-zero multiple of 8 KiB",
1389                chr_rom.len()
1390            )));
1391        }
1392        Ok(Self {
1393            prg_rom,
1394            chr_rom,
1395            vram: vec![0u8; 2 * NAMETABLE_SIZE].into_boxed_slice(),
1396            mirroring,
1397        })
1398    }
1399
1400    /// $8000-$BFFF -> first 16 KiB bank; $C000-$FFFF -> second 16 KiB bank
1401    /// (which equals the first on a 16 KiB image, after the modulo wrap).
1402    fn read_prg(&self, addr: u16) -> u8 {
1403        let count = (self.prg_rom.len() / PRG_BANK_16K).max(1);
1404        let bank = if addr < 0xC000 { 0 } else { 1 % count };
1405        self.prg_rom[bank * PRG_BANK_16K + (addr as usize & 0x3FFF)]
1406    }
1407}
1408
1409impl Mapper for SachenTca01M143 {
1410    fn caps(&self) -> MapperCaps {
1411        MapperCaps::NONE
1412    }
1413
1414    // The protection register answers reads across the whole $4020-$5FFF
1415    // window (mapped). $8000-$FFFF PRG-ROM stays mapped (the trait default) —
1416    // a `!(...)` here would wrongly open-bus the program ROM + reset vector, so
1417    // the board never boots. There is no open-bus hole to carve out, so this
1418    // returns false for everything the board answers. v2.7.2 (core audit
1419    // §5.5): except `$6000-$7FFF`, where the board drives nothing, so it floats.
1420    fn cpu_read_unmapped(&self, addr: u16) -> bool {
1421        matches!(addr, 0x6000..=0x7FFF) && self.sram().is_empty()
1422    }
1423
1424    /// The protection read drives D5-D0 only; D7-D6 float (v2.7.2: the bus
1425    /// now keeps them from its latch, where the read used to approximate them
1426    /// from the address high byte).
1427    fn cpu_read_driven_mask(&self, addr: u16) -> u8 {
1428        if (0x4100..=0x5FFF).contains(&addr) && addr & 0x0100 != 0 {
1429            0x3F
1430        } else {
1431            0xFF
1432        }
1433    }
1434
1435    fn cpu_read(&mut self, addr: u16) -> u8 {
1436        match addr {
1437            // Sachen TCA01 protection (puNES extcl_cpu_rd_mem_143): the chip
1438            // only answers when A8 is set, returning (~addr & 0x3F) in the low
1439            // 6 bits and leaving the top 2 bits as open bus. The old decode
1440            // answered across the WHOLE window with a hardcoded bit 6, so the
1441            // game's `(~addr & 0x3F)` protection compare failed -> blank boot.
1442            // The high 2 open-bus bits are approximated from the address high
1443            // byte (the most-recently-driven bus value in this read).
1444            0x4100..=0x5FFF if addr & 0x0100 != 0 => {
1445                ((!addr & 0x3F) as u8) | ((addr >> 8) as u8 & 0xC0)
1446            }
1447            0x4100..=0x5FFF if addr >= 0x5000 => 0xFF,
1448            0x8000..=0xFFFF => self.read_prg(addr),
1449            _ => 0,
1450        }
1451    }
1452
1453    fn cpu_write(&mut self, _addr: u16, _value: u8) {}
1454
1455    fn ppu_read(&mut self, addr: u16) -> u8 {
1456        let addr = addr & 0x3FFF;
1457        match addr {
1458            0x0000..=0x1FFF => {
1459                let count = (self.chr_rom.len() / CHR_BANK_8K).max(1);
1460                let bank = 0 % count;
1461                self.chr_rom[bank * CHR_BANK_8K + addr as usize]
1462            }
1463            0x2000..=0x3EFF => self.vram[nametable_offset(addr, self.mirroring)],
1464            _ => 0,
1465        }
1466    }
1467
1468    fn ppu_write(&mut self, addr: u16, value: u8) {
1469        let addr = addr & 0x3FFF;
1470        if let 0x2000..=0x3EFF = addr {
1471            let off = nametable_offset(addr, self.mirroring);
1472            self.vram[off] = value;
1473        }
1474    }
1475
1476    fn current_mirroring(&self) -> Mirroring {
1477        self.mirroring
1478    }
1479
1480    fn save_state(&self) -> Vec<u8> {
1481        let mut out = Vec::with_capacity(1 + self.vram.len());
1482        out.push(SAVE_STATE_VERSION);
1483        out.extend_from_slice(&self.vram);
1484        out
1485    }
1486
1487    fn load_state(&mut self, data: &[u8]) -> Result<(), MapperError> {
1488        let expected = 1 + self.vram.len();
1489        if data.len() != expected {
1490            return Err(MapperError::WrongLength {
1491                expected,
1492                got: data.len(),
1493            });
1494        }
1495        if data[0] != SAVE_STATE_VERSION {
1496            return Err(MapperError::UnsupportedVersion(data[0]));
1497        }
1498        self.vram.copy_from_slice(&data[1..=self.vram.len()]);
1499        Ok(())
1500    }
1501}
1502
1503#[cfg(test)]
1504#[allow(clippy::cast_possible_truncation)]
1505mod tests {
1506    use super::*;
1507    /// 16 KiB-banked PRG: byte 0 of each 16 KiB bank holds the bank index.
1508    fn synth_prg_16k(banks: usize) -> Box<[u8]> {
1509        let mut v = vec![0xFFu8; banks * PRG_BANK_16K];
1510        for b in 0..banks {
1511            v[b * PRG_BANK_16K] = b as u8;
1512        }
1513        v.into_boxed_slice()
1514    }
1515
1516    fn synth_prg_32k(banks: usize) -> Box<[u8]> {
1517        let mut v = vec![0xFFu8; banks * PRG_BANK_32K];
1518        for b in 0..banks {
1519            v[b * PRG_BANK_32K] = b as u8;
1520        }
1521        v.into_boxed_slice()
1522    }
1523
1524    fn synth_chr_8k(banks: usize) -> Box<[u8]> {
1525        let mut v = vec![0u8; banks * CHR_BANK_8K];
1526        for b in 0..banks {
1527            v[b * CHR_BANK_8K] = b as u8;
1528        }
1529        v.into_boxed_slice()
1530    }
1531
1532    #[test]
1533    fn m133_register_on_a8() {
1534        let mut m = Sachen133::new(synth_prg_32k(2), synth_chr_8k(4), Mirroring::Vertical).unwrap();
1535        // value: PRG = (v>>2)&1, CHR = v&3. 0b0000_0111 -> PRG 1, CHR 3.
1536        m.cpu_write(0x4100, 0b0000_0111);
1537        assert_eq!(m.cpu_read(0x8000), 1);
1538        assert_eq!(m.ppu_read(0x0000), 3);
1539        // A8 clear -> no latch.
1540        m.cpu_write(0x4200, 0b0000_0000);
1541        assert_eq!(m.cpu_read(0x8000), 1);
1542    }
1543
1544    #[test]
1545    fn m145_chr_from_data_bit7() {
1546        let mut m = Sachen145::new(synth_prg_32k(1), synth_chr_8k(2), Mirroring::Vertical).unwrap();
1547        // Default CHR bank 0.
1548        assert_eq!(m.ppu_read(0x0000), 0);
1549        // (addr & 0x4100) == 0x4100 -> $4100 qualifies. Bit 7 set -> CHR 1.
1550        m.cpu_write(0x4100, 0x80);
1551        assert_eq!(m.ppu_read(0x0000), 1);
1552        // Also decoded in the $6000 save-RAM window ($6100 has 0x4100 bits).
1553        m.cpu_write(0x6100, 0x00);
1554        assert_eq!(m.ppu_read(0x0000), 0);
1555        // PRG is fixed 32 KiB bank 0.
1556        assert_eq!(m.cpu_read(0x8000), 0);
1557    }
1558
1559    #[test]
1560    fn m146_like_nina03() {
1561        let mut m = Sachen146::new(synth_prg_32k(2), synth_chr_8k(8), Mirroring::Vertical).unwrap();
1562        // value: PRG = (v>>3)&1, CHR = v&7. 0b0000_1101 -> PRG 1, CHR 5.
1563        m.cpu_write(0x4100, 0b0000_1101);
1564        assert_eq!(m.cpu_read(0x8000), 1);
1565        assert_eq!(m.ppu_read(0x0000), 5);
1566        // Save-RAM alias also latches.
1567        m.cpu_write(0x6000, 0b0000_0010); // PRG 0, CHR 2
1568        assert_eq!(m.cpu_read(0x8000), 0);
1569        assert_eq!(m.ppu_read(0x0000), 2);
1570    }
1571
1572    #[test]
1573    fn m147_jv001_protection_read_and_bank_decode() {
1574        // JV001 scramble derived from puNES `JV001.c` (GPL-2.0-or-later; also
1575        // documented in the nesdev wiki mapper-147 board notes). The board pre-scrambles
1576        // writes ((v&3)<<6)|((v&0xFC)>>2) and post-scrambles reads
1577        // ((v&0x3F)<<2)|((v&0xC0)>>6); the chip resets with invert=0xFF.
1578        let mut m =
1579            Sachen3018M147::new(synth_prg_32k(4), synth_chr_8k(8), Mirroring::Vertical).unwrap();
1580        // Disable inversion so the handshake is a clean staging->accumulator
1581        // copy: write 1 to $4101 (scrambled 0x01 -> 0x40, bit 0 == 0 -> invert
1582        // off). puNES: invert = 0xFF * (value & 1); 0x40 & 1 == 0 -> 0x00.
1583        m.cpu_write(0x4101, 0x01);
1584        // $4102 <- value V: staging = scramble(V)&0x0F, inverter = scramble(V)&0xF0.
1585        // Pick V = 0x14: scramble = ((0x14&3)<<6)|((0x14&0xFC)>>2) = 0|0x05 = 0x05
1586        //   -> staging 0x05, inverter 0x00.
1587        m.cpu_write(0x4102, 0x14);
1588        // $4100 latch (increase off, invert off): accumulator =
1589        //   (0 & 0xF0) | ((staging ^ 0) & 0x0F) = 0x05.
1590        m.cpu_write(0x4100, 0x00);
1591        // Handshake read: chip = ((inverter ^ invert) & 0xF0) | (acc & 0x0F)
1592        //   = (0x00 & 0xF0) | 0x05 = 0x05; board post-scramble = 0x05<<2 = 0x14.
1593        assert_eq!(m.cpu_read(0x4100), 0x14);
1594        // Refresh the bank-output latch (a $8000+ access): output =
1595        //   (inverter & 0xF0) | (acc & 0x0F) = 0x05.
1596        // PRG = ((out&0x20)>>4)|(out&1) = 0|1 = 1; CHR = (out&0x1E)>>1 = (4)>>1 = 2.
1597        m.cpu_write(0x8000, 0xFF); // bus conflict with PRG byte 0 (==0) -> 0
1598        // The $8000 write refreshes output from acc/inverter (0x05), not the
1599        // ANDed data; bank decode below reflects that.
1600        assert_eq!(m.cpu_read(0x8000), 1); // PRG bank 1 of 4 -> byte 0 = 1
1601        assert_eq!(m.ppu_read(0x0000), 2); // CHR bank 2 of 8 -> byte 0 = 2
1602    }
1603
1604    #[test]
1605    fn m147_save_state_round_trip() {
1606        let mut m =
1607            Sachen3018M147::new(synth_prg_32k(4), synth_chr_8k(8), Mirroring::Vertical).unwrap();
1608        m.cpu_write(0x4101, 0x01);
1609        m.cpu_write(0x4102, 0x14);
1610        m.cpu_write(0x4100, 0x00);
1611        m.cpu_write(0x8000, 0x00); // refresh output latch
1612        let prg = m.cpu_read(0x8000);
1613        let chr = m.ppu_read(0x0000);
1614        let blob = m.save_state();
1615        let mut m2 =
1616            Sachen3018M147::new(synth_prg_32k(4), synth_chr_8k(8), Mirroring::Vertical).unwrap();
1617        m2.load_state(&blob).unwrap();
1618        assert_eq!(m2.cpu_read(0x8000), prg);
1619        assert_eq!(m2.ppu_read(0x0000), chr);
1620    }
1621
1622    #[test]
1623    fn m148_latch_selects_prg_and_chr_with_conflict() {
1624        // PRG all-0xFF except offset 0, so the in-window write sees 0xFF.
1625        let mut m =
1626            Sachen148::new(synth_prg_32k(2), synth_chr_8k(8), Mirroring::Horizontal).unwrap();
1627        // value .... PCCC: PRG = bit3, CHR = bits 0-2.
1628        // Write at $8001 (PRG byte 0xFF -> no masking): 0b0000_1101 -> PRG 1, CHR 5.
1629        m.cpu_write(0x8001, 0b0000_1101);
1630        assert_eq!(m.cpu_read(0x8000), 1);
1631        assert_eq!(m.ppu_read(0x0000), 5);
1632    }
1633
1634    #[test]
1635    fn m149_chr_bit_in_bit7() {
1636        let mut m = Sachen149::new(synth_prg_32k(1), synth_chr_8k(2), Mirroring::Vertical).unwrap();
1637        // Write at $8001 (PRG byte 0xFF -> no masking): bit7 set -> CHR 1.
1638        m.cpu_write(0x8001, 0x80);
1639        assert_eq!(m.ppu_read(0x0000), 1);
1640        // PRG is fixed bank 0.
1641        assert_eq!(m.cpu_read(0x8000), 0);
1642        // bit7 clear -> CHR 0.
1643        m.cpu_write(0x8001, 0x00);
1644        assert_eq!(m.ppu_read(0x0000), 0);
1645    }
1646
1647    #[test]
1648    fn m150_register_protocol_and_banking() {
1649        let mut m = Sachen150::new(synth_prg_32k(4), synth_chr_8k(8)).unwrap();
1650        // Select register 5 (PRG), write value 2 -> PRG bank 2.
1651        m.cpu_write(0x4100, 5); // index
1652        m.cpu_write(0x4101, 2); // data
1653        assert_eq!(m.cpu_read(0x8000), 2);
1654        // Register 6 = CHR low 2 bits; register 4 bit0 = CHR bit2.
1655        // Set reg6 = 0b01, reg4 = 1 -> CHR = (1<<2)|1 = 5.
1656        m.cpu_write(0x4100, 6);
1657        m.cpu_write(0x4101, 0b001);
1658        m.cpu_write(0x4100, 4);
1659        m.cpu_write(0x4101, 1);
1660        assert_eq!(m.ppu_read(0x0000), 5);
1661        // Registers are readable (protection).
1662        m.cpu_write(0x4100, 6);
1663        assert_eq!(m.cpu_read(0x4101), 0b001);
1664    }
1665
1666    #[test]
1667    fn m150_mirroring_modes() {
1668        let mut m = Sachen150::new(synth_prg_32k(1), synth_chr_8k(1)).unwrap();
1669        // reg7 mirroring sel = (reg7 >> 1) & 3.
1670        m.cpu_write(0x4100, 7);
1671        m.cpu_write(0x4101, 1 << 1); // sel 1 -> horizontal
1672        assert_eq!(m.current_mirroring(), Mirroring::Horizontal);
1673        m.cpu_write(0x4101, 2 << 1); // sel 2 -> vertical
1674        assert_eq!(m.current_mirroring(), Mirroring::Vertical);
1675        m.cpu_write(0x4101, 3 << 1); // sel 3 -> single-screen A
1676        assert_eq!(m.current_mirroring(), Mirroring::SingleScreenA);
1677        // sel 0 -> custom S0-S0-S0-S1; table 3 maps to bank 1.
1678        m.cpu_write(0x4101, 0);
1679        assert_eq!(m.current_mirroring(), Mirroring::MapperControlled);
1680        // table 0 ($2000) -> bank 0; table 3 ($2C00) -> bank 1.
1681        m.ppu_write(0x2000, 0xAA);
1682        m.ppu_write(0x2C00, 0xBB);
1683        assert_eq!(m.ppu_read(0x2000), 0xAA);
1684        assert_eq!(m.ppu_read(0x2C00), 0xBB);
1685        // table 1 ($2400) shares bank 0 with table 0 in this custom mode.
1686        assert_eq!(m.ppu_read(0x2400), 0xAA);
1687    }
1688
1689    #[test]
1690    fn m143_protection_read_and_nrom() {
1691        let mut m =
1692            SachenTca01M143::new(synth_prg_16k(1), synth_chr_8k(1), Mirroring::Vertical).unwrap();
1693        // Protection answers only with A8 set: low 6 bits = ~addr & 0x3F, top
1694        // 2 bits = open bus (approximated from the address high byte). puNES.
1695        let addr = 0x4100u16;
1696        assert_eq!(
1697            m.cpu_read(addr),
1698            ((!addr & 0x3F) as u8) | ((addr >> 8) as u8 & 0xC0)
1699        );
1700        // A8 clear, addr >= $5000 -> $FF.
1701        assert_eq!(m.cpu_read(0x5000), 0xFF);
1702        assert!(!m.cpu_read_unmapped(0x4100));
1703        // NROM-128: $8000 and $C000 read the same (only) 16 KiB bank (index 0).
1704        assert_eq!(m.cpu_read(0x8000), 0);
1705        assert_eq!(m.cpu_read(0xC000), 0);
1706    }
1707
1708    #[test]
1709    fn m143_save_state_round_trip() {
1710        let mut m =
1711            SachenTca01M143::new(synth_prg_16k(1), synth_chr_8k(1), Mirroring::Vertical).unwrap();
1712        m.ppu_write(0x2000, 0x33);
1713        let blob = m.save_state();
1714        let mut m2 =
1715            SachenTca01M143::new(synth_prg_16k(1), synth_chr_8k(1), Mirroring::Vertical).unwrap();
1716        m2.load_state(&blob).unwrap();
1717        assert_eq!(m2.ppu_read(0x2000), 0x33);
1718    }
1719
1720    // ---- mapper 243 (Sachen SA-020A) ---------------------------------------
1721
1722    /// 128 KiB PRG (4 x 32 KiB) + 128 KiB CHR (16 x 8 KiB) -- the documented
1723    /// maxima, so every bank bit is reachable.
1724    fn m243() -> Sachen150 {
1725        Sachen150::new_on_board(synth_prg_32k(4), synth_chr_8k(16), Sa020aBoard::Sa020a).unwrap()
1726    }
1727
1728    /// Write `value` to register `index` through the $4100/$4101 index/data pair.
1729    fn set_reg(m: &mut Sachen150, index: u8, value: u8) {
1730        m.cpu_write(0x4100, index);
1731        m.cpu_write(0x4101, value);
1732    }
1733
1734    #[test]
1735    fn m243_prg_bank_is_r5_alone() {
1736        // R5 supplies PRG A16..A15 and nothing else contributes -- unlike the
1737        // SA-150 wiring, where R2 bit 0 also feeds PRG.
1738        let mut m = m243();
1739        for bank in 0..4u8 {
1740            set_reg(&mut m, 5, bank);
1741            assert_eq!(
1742                m.cpu_read(0x8000),
1743                bank,
1744                "R5={bank} selects 32 KiB bank {bank}"
1745            );
1746        }
1747        // R2 must NOT disturb PRG on this board.
1748        set_reg(&mut m, 5, 2);
1749        set_reg(&mut m, 2, 1);
1750        assert_eq!(m.cpu_read(0x8000), 2, "R2 does not feed PRG on the SA-020A");
1751    }
1752
1753    #[test]
1754    fn m243_chr_bank_assembles_a13_a14_a15_a16() {
1755        // A13 = R2.0 (LSB), A14 = R4.0, A15 = R6.0, A16 = R6.1.
1756        let cases = [
1757            (0u8, 0u8, 0u8, 0u8),
1758            (1, 0, 0, 1),
1759            (0, 1, 0, 2),
1760            (1, 1, 0, 3),
1761            (0, 0, 1, 4),
1762            (1, 0, 1, 5),
1763            (0, 0, 2, 8),
1764            (1, 1, 3, 15),
1765        ];
1766        for (r2, r4, r6, expect) in cases {
1767            let mut m = m243();
1768            set_reg(&mut m, 2, r2);
1769            set_reg(&mut m, 4, r4);
1770            set_reg(&mut m, 6, r6);
1771            assert_eq!(
1772                m.ppu_read(0x0000),
1773                expect,
1774                "R2={r2} R4={r4} R6={r6} should select CHR bank {expect}"
1775            );
1776        }
1777    }
1778
1779    #[test]
1780    fn m243_and_m150_decode_the_same_registers_differently() {
1781        // The whole reason 243 exists as a separate mapper: same ASIC, inverted
1782        // bank-bit significance. R2 is the CHR LSB on the SA-020A and the MSB on
1783        // the SA-150, so one setting must land on different banks.
1784        let mut a = m243();
1785        let mut b = Sachen150::new(synth_prg_32k(4), synth_chr_8k(16)).unwrap();
1786        for m in [&mut a, &mut b] {
1787            set_reg(m, 2, 1);
1788        }
1789        assert_eq!(a.ppu_read(0x0000), 1, "SA-020A: R2 is CHR A13");
1790        assert_eq!(b.ppu_read(0x0000), 8, "SA-150: R2 is the CHR MSB");
1791        assert_ne!(a.ppu_read(0x0000), b.ppu_read(0x0000));
1792    }
1793
1794    #[test]
1795    fn m243_mirroring_modes_including_the_vertically_flipped_l() {
1796        let mut m = m243();
1797        // Selector lives in R7 bits 2:1.
1798        set_reg(&mut m, 7, 1 << 1);
1799        assert_eq!(m.current_mirroring(), Mirroring::Horizontal);
1800        set_reg(&mut m, 7, 2 << 1);
1801        assert_eq!(m.current_mirroring(), Mirroring::Vertical);
1802        set_reg(&mut m, 7, 3 << 1);
1803        assert_eq!(m.current_mirroring(), Mirroring::SingleScreenA);
1804
1805        // Selector 0 is the S0-S0-S0-S1 layout: the first three nametables share
1806        // CIRAM page 0 and only the fourth is distinct.
1807        set_reg(&mut m, 7, 0);
1808        assert_eq!(m.current_mirroring(), Mirroring::MapperControlled);
1809        m.ppu_write(0x2000, 0xA1);
1810        m.ppu_write(0x2C00, 0xB2);
1811        assert_eq!(m.ppu_read(0x2000), 0xA1);
1812        assert_eq!(m.ppu_read(0x2400), 0xA1, "table 1 shares page 0");
1813        assert_eq!(m.ppu_read(0x2800), 0xA1, "table 2 shares page 0");
1814        assert_eq!(m.ppu_read(0x2C00), 0xB2, "table 3 is the unique page");
1815    }
1816
1817    #[test]
1818    fn m243_registers_read_back_and_round_trip_through_a_save_state() {
1819        let mut m = m243();
1820        set_reg(&mut m, 6, 3);
1821        assert_eq!(
1822            m.cpu_read(0x4101),
1823            3,
1824            "the data port reads the selected register"
1825        );
1826        set_reg(&mut m, 2, 1);
1827        set_reg(&mut m, 7, 2 << 1);
1828        let blob = m.save_state();
1829
1830        let mut m2 = m243();
1831        m2.load_state(&blob).unwrap();
1832        assert_eq!(m2.ppu_read(0x0000), m.ppu_read(0x0000));
1833        assert_eq!(m2.current_mirroring(), Mirroring::Vertical);
1834    }
1835}