Skip to main content

rustynes_mappers/
m246_fong_shen_bang246.rs

1//! Fong Shen Bang / Feng Shen Bang (mapper 246).
2//!
3//! Four bank-select registers in the `$6000-$67FF` window -- two PRG, two
4//! CHR -- with battery-backed PRG-RAM sharing the same `$6000` region above
5//! the register window. The split matters: a write below `$6800` is a
6//! register, a write above it is save RAM.
7//!
8//! A best-effort (Tier-2) board: register-decode correctness verified against
9//! the `GeraNES` reference emulator (cross-referenced, not copied)
10//! and the nesdev wiki, with no commercial-oracle ROM in the tree. Banking math
11//! is direct slice indexing and every bank select wraps with `% count`, so a
12//! register write can never index out of bounds -- required for the `#![no_std]`
13//! chip stack, which cannot afford a panic on a register access.
14//!
15//! See `tier.rs` (`MapperTier::BestEffort`), `docs/adr/0011-mapper-tiering.md`,
16//! and `docs/mappers.md` §Mapper coverage matrix.
17
18use crate::cartridge::Mirroring;
19use crate::mapper::{Mapper, MapperCaps, MapperError};
20use alloc::{boxed::Box, vec::Vec};
21use alloc::{format, vec};
22
23const PRG_BANK_8K: usize = 0x2000;
24const CHR_BANK_2K: usize = 0x0800;
25const NAMETABLE_SIZE: usize = 0x0400;
26const NAMETABLE_SIZE_U16: u16 = 0x0400;
27
28const SAVE_STATE_VERSION: u8 = 1;
29
30// ---------------------------------------------------------------------------
31// Shared nametable helper (mirrors the one in the other simple-mapper modules).
32// ---------------------------------------------------------------------------
33
34const fn nametable_offset(addr: u16, mirroring: Mirroring) -> usize {
35    let table = (((addr - 0x2000) / NAMETABLE_SIZE_U16) & 0x03) as u8;
36    let local = (addr as usize) & (NAMETABLE_SIZE - 1);
37    let physical = mirroring.physical_bank(table);
38    physical * NAMETABLE_SIZE + local
39}
40
41/// Mapper 246 (`Fong Shen Bang` / G0151-1).
42pub struct FongShenBang246 {
43    prg_rom: Box<[u8]>,
44    chr_rom: Box<[u8]>,
45    vram: Box<[u8]>,
46    /// 2 KiB battery-backed PRG-RAM at $6800-$6FFF.
47    prg_ram: Box<[u8]>,
48    prg_banks: [u8; 4],
49    chr_banks: [u8; 4],
50    mirroring: Mirroring,
51}
52
53impl FongShenBang246 {
54    /// Construct a new mapper 246 board.
55    ///
56    /// # Errors
57    ///
58    /// Returns [`MapperError::Invalid`] when PRG is not a non-zero multiple of
59    /// 8 KiB or CHR-ROM is empty / not a multiple of 2 KiB.
60    pub fn new(
61        prg_rom: Box<[u8]>,
62        chr_rom: Box<[u8]>,
63        mirroring: Mirroring,
64    ) -> Result<Self, MapperError> {
65        if prg_rom.is_empty() || !prg_rom.len().is_multiple_of(PRG_BANK_8K) {
66            return Err(MapperError::Invalid(format!(
67                "mapper 246 PRG-ROM size {} is not a non-zero multiple of 8 KiB",
68                prg_rom.len()
69            )));
70        }
71        if chr_rom.is_empty() || !chr_rom.len().is_multiple_of(CHR_BANK_2K) {
72            return Err(MapperError::Invalid(format!(
73                "mapper 246 CHR-ROM size {} is not a non-zero multiple of 2 KiB",
74                chr_rom.len()
75            )));
76        }
77        // Power-on (per the nesdev wiki): the $6000-$6002 PRG regs are 0, but
78        // $6003 (the $E000-$FFFF slot) initializes to 0xFF — so the reset vector
79        // at $FFFC resolves into the last PRG bank, where the boot code lives.
80        let prg_banks = [0, 0, 0, 0xFF];
81        Ok(Self {
82            prg_rom,
83            chr_rom,
84            vram: vec![0u8; 2 * NAMETABLE_SIZE].into_boxed_slice(),
85            prg_ram: vec![0u8; 0x0800].into_boxed_slice(),
86            prg_banks,
87            chr_banks: [0, 0, 0, 0],
88            mirroring,
89        })
90    }
91
92    fn prg_byte(&self, slot: usize, addr: u16) -> u8 {
93        let count = (self.prg_rom.len() / PRG_BANK_8K).max(1);
94        let mut bank = self.prg_banks[slot] as usize;
95        // $E000-$FFFF hardware quirk: reads from $FFE4-$FFE7, $FFEC-$FFEF,
96        // $FFF4-$FFF7, and $FFFC-$FFFF force PRG A17 high (bank bit 4 of an 8 KiB
97        // index). The interrupt/reset vectors live in that forced region.
98        if slot == 3 {
99            let low = addr & 0x001F;
100            let in_window = (0xFFE4..=0xFFFF).contains(&addr)
101                && matches!(low, 0x04..=0x07 | 0x0C..=0x0F | 0x14..=0x17 | 0x1C..=0x1F);
102            if in_window {
103                bank |= 0x10;
104            }
105        }
106        let bank = bank % count;
107        self.prg_rom[bank * PRG_BANK_8K + (addr as usize & 0x1FFF)]
108    }
109}
110
111impl Mapper for FongShenBang246 {
112    fn sram(&self) -> &[u8] {
113        &self.prg_ram
114    }
115    fn sram_mut(&mut self) -> &mut [u8] {
116        &mut self.prg_ram
117    }
118    fn caps(&self) -> MapperCaps {
119        MapperCaps::NONE
120    }
121
122    // Only the dead sub-ranges below the PRG window are open bus: $4020-$67FF
123    // (the write-only register file at $6000-$67FF + the $4020-$5FFF gap) and
124    // the $7000-$7FFF mirror gap. The 2 KiB PRG-RAM at $6800-$6FFF and the PRG
125    // ROM at $8000-$FFFF are mapped (matching the trait default of "$6000-$FFFF
126    // is mapped" but carving out the register/gap holes).
127    fn cpu_read_unmapped(&self, addr: u16) -> bool {
128        (0x4020..=0x67FF).contains(&addr) || (0x7000..=0x7FFF).contains(&addr)
129    }
130
131    fn cpu_read(&mut self, addr: u16) -> u8 {
132        match addr {
133            0x6800..=0x6FFF => self.prg_ram[(addr - 0x6800) as usize],
134            0x8000..=0x9FFF => self.prg_byte(0, addr),
135            0xA000..=0xBFFF => self.prg_byte(1, addr),
136            0xC000..=0xDFFF => self.prg_byte(2, addr),
137            0xE000..=0xFFFF => self.prg_byte(3, addr),
138            _ => 0,
139        }
140    }
141
142    fn cpu_write(&mut self, addr: u16, value: u8) {
143        match addr {
144            0x6000..=0x6003 => self.prg_banks[(addr & 0x03) as usize] = value,
145            0x6004..=0x6007 => self.chr_banks[(addr & 0x03) as usize] = value,
146            0x6800..=0x6FFF => self.prg_ram[(addr - 0x6800) as usize] = value,
147            _ => {}
148        }
149    }
150
151    fn ppu_read(&mut self, addr: u16) -> u8 {
152        let addr = addr & 0x3FFF;
153        match addr {
154            0x0000..=0x1FFF => {
155                let slot = (addr >> 11) as usize & 0x03;
156                let count = (self.chr_rom.len() / CHR_BANK_2K).max(1);
157                let bank = (self.chr_banks[slot] as usize) % count;
158                self.chr_rom[bank * CHR_BANK_2K + (addr as usize & 0x07FF)]
159            }
160            0x2000..=0x3EFF => self.vram[nametable_offset(addr, self.mirroring)],
161            _ => 0,
162        }
163    }
164
165    fn ppu_write(&mut self, addr: u16, value: u8) {
166        let addr = addr & 0x3FFF;
167        if let 0x2000..=0x3EFF = addr {
168            let off = nametable_offset(addr, self.mirroring);
169            self.vram[off] = value;
170        }
171    }
172
173    fn current_mirroring(&self) -> Mirroring {
174        self.mirroring
175    }
176
177    fn save_state(&self) -> Vec<u8> {
178        let mut out = Vec::with_capacity(1 + 4 + 4 + self.prg_ram.len() + self.vram.len());
179        out.push(SAVE_STATE_VERSION);
180        out.extend_from_slice(&self.prg_banks);
181        out.extend_from_slice(&self.chr_banks);
182        out.extend_from_slice(&self.prg_ram);
183        out.extend_from_slice(&self.vram);
184        out
185    }
186
187    fn load_state(&mut self, data: &[u8]) -> Result<(), MapperError> {
188        let expected = 1 + 4 + 4 + self.prg_ram.len() + self.vram.len();
189        if data.len() != expected {
190            return Err(MapperError::WrongLength {
191                expected,
192                got: data.len(),
193            });
194        }
195        if data[0] != SAVE_STATE_VERSION {
196            return Err(MapperError::UnsupportedVersion(data[0]));
197        }
198        self.prg_banks.copy_from_slice(&data[1..5]);
199        self.chr_banks.copy_from_slice(&data[5..9]);
200        let mut cursor = 9;
201        self.prg_ram
202            .copy_from_slice(&data[cursor..cursor + self.prg_ram.len()]);
203        cursor += self.prg_ram.len();
204        self.vram
205            .copy_from_slice(&data[cursor..cursor + self.vram.len()]);
206        Ok(())
207    }
208}
209
210#[cfg(test)]
211#[allow(clippy::cast_possible_truncation)]
212mod tests {
213    use super::*;
214
215    fn synth_prg_8k(banks: usize) -> Box<[u8]> {
216        let mut v = vec![0xFFu8; banks * PRG_BANK_8K];
217        for b in 0..banks {
218            v[b * PRG_BANK_8K] = b as u8;
219        }
220        v.into_boxed_slice()
221    }
222
223    fn synth_chr_2k(banks: usize) -> Box<[u8]> {
224        let mut v = vec![0u8; banks * CHR_BANK_2K];
225        for b in 0..banks {
226            v[b * CHR_BANK_2K] = b as u8;
227        }
228        v.into_boxed_slice()
229    }
230
231    #[test]
232    fn m246_register_banking_and_prg_ram() {
233        let mut m =
234            FongShenBang246::new(synth_prg_8k(8), synth_chr_2k(8), Mirroring::Vertical).unwrap();
235        // $6000 -> PRG $8000 = bank 3.
236        m.cpu_write(0x6000, 3);
237        assert_eq!(m.cpu_read(0x8000), 3);
238        // $6004 -> CHR slot 0 = bank 5.
239        m.cpu_write(0x6004, 5);
240        assert_eq!(m.ppu_read(0x0000), 5);
241        // PRG-RAM round-trips at $6800.
242        m.cpu_write(0x6800, 0xC4);
243        assert_eq!(m.cpu_read(0x6800), 0xC4);
244    }
245
246    #[test]
247    fn m246_save_state_round_trip() {
248        let mut m =
249            FongShenBang246::new(synth_prg_8k(8), synth_chr_2k(8), Mirroring::Vertical).unwrap();
250        m.cpu_write(0x6001, 4); // PRG $A000 = bank 4
251        m.cpu_write(0x6007, 6); // CHR slot 3 = bank 6
252        m.cpu_write(0x6900, 0x9D); // PRG-RAM at $6800-$6FFF
253        let blob = m.save_state();
254        let mut m2 =
255            FongShenBang246::new(synth_prg_8k(8), synth_chr_2k(8), Mirroring::Vertical).unwrap();
256        m2.load_state(&blob).unwrap();
257        assert_eq!(m2.cpu_read(0xA000), 4);
258        assert_eq!(m2.ppu_read(0x1800), 6);
259        assert_eq!(m2.cpu_read(0x6900), 0x9D);
260    }
261}