Skip to main content

rustynes_mappers/
m241_bxrom241.rs

1//! `BxROM`-like pirate board (mapper 241), e.g. the Mortal Kombat bootlegs.
2//!
3//! The whole written byte selects a 32 KiB PRG bank -- no masking, no bus
4//! conflict -- with CHR-RAM and fixed mirroring. Effectively BNROM with a
5//! wider bank field; see `m034_bnrom_nina001.rs` for BNROM proper.
6//!
7//! A discrete-logic board in the shape of the stock mappers (`NROM`, `CNROM`,
8//! `UxROM`, `GxROM`, `AxROM`): bank-select latch registers, no IRQ, no on-cart
9//! audio. Banking / mirroring semantics are cross-checked against the
10//! `GeraNES` reference emulator (cross-referenced, not copied)
11//! and the nesdev wiki, and validated by register-decode + save-state unit
12//! tests.
13//!
14//! See `docs/mappers.md` §Mapper coverage matrix.
15
16use crate::cartridge::Mirroring;
17use crate::mapper::{Mapper, MapperCaps, MapperError};
18use alloc::{boxed::Box, vec::Vec};
19use alloc::{format, vec};
20
21const PRG_BANK_32K: usize = 0x8000;
22const CHR_BANK_8K: usize = 0x2000;
23const NAMETABLE_SIZE: usize = 0x0400;
24const NAMETABLE_SIZE_U16: u16 = 0x0400;
25
26/// v2 (v2.7.2) appends the 8 KiB WRAM. Only v2 loads since v2.9.8 (ADR 0042);
27/// a v1 blob used to load with it zeroed.
28const SAVE_STATE_VERSION: u8 = 2;
29const WRAM_SIZE: usize = 0x2000;
30
31const fn nametable_offset(addr: u16, mirroring: Mirroring) -> usize {
32    let table = (((addr - 0x2000) / NAMETABLE_SIZE_U16) & 0x03) as u8;
33    let local = (addr as usize) & (NAMETABLE_SIZE - 1);
34    let physical = mirroring.physical_bank(table);
35    physical * NAMETABLE_SIZE + local
36}
37
38/// Mapper 241 (`BxROM`-like pirate board).
39pub struct Bxrom241 {
40    prg_rom: Box<[u8]>,
41    chr: Box<[u8]>,
42    vram: Box<[u8]>,
43    chr_is_ram: bool,
44    prg_bank: u8,
45    mirroring: Mirroring,
46    /// "8 KiB of WRAM at CPU $6000-$7FFF that can be battery-backed"
47    /// (`nesdev_wiki/INES_Mapper_241.xhtml`). Absent before v2.7.2.
48    wram: Box<[u8]>,
49    /// Whether the header marks it battery-backed (then it is the save).
50    battery: bool,
51}
52
53impl Bxrom241 {
54    /// Construct a new mapper 241 board.
55    ///
56    /// # Errors
57    ///
58    /// Returns [`MapperError::Invalid`] when PRG is not a non-zero multiple of
59    /// 32 KiB, or CHR-ROM (when present) is not 8 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_32K) {
66            return Err(MapperError::Invalid(format!(
67                "mapper 241 PRG-ROM size {} is not a non-zero multiple of 32 KiB",
68                prg_rom.len()
69            )));
70        }
71        let chr_is_ram = chr_rom.is_empty();
72        let chr: Box<[u8]> = if chr_is_ram {
73            vec![0u8; CHR_BANK_8K].into_boxed_slice()
74        } else if chr_rom.len() == CHR_BANK_8K {
75            chr_rom
76        } else {
77            return Err(MapperError::Invalid(format!(
78                "mapper 241 expects 8 KiB CHR (RAM or ROM); got {} bytes",
79                chr_rom.len()
80            )));
81        };
82        Ok(Self {
83            prg_rom,
84            chr,
85            vram: vec![0u8; 2 * NAMETABLE_SIZE].into_boxed_slice(),
86            chr_is_ram,
87            prg_bank: 0,
88            mirroring,
89            wram: vec![0u8; WRAM_SIZE].into_boxed_slice(),
90            battery: false,
91        })
92    }
93
94    /// Mark the WRAM battery-backed (from the header), making it the save.
95    #[must_use]
96    pub const fn with_battery(mut self, battery: bool) -> Self {
97        self.battery = battery;
98        self
99    }
100}
101
102impl Mapper for Bxrom241 {
103    fn caps(&self) -> MapperCaps {
104        MapperCaps::NONE
105    }
106
107    fn sram(&self) -> &[u8] {
108        if self.battery { &self.wram } else { &[] }
109    }
110    fn sram_mut(&mut self) -> &mut [u8] {
111        if self.battery {
112            &mut self.wram
113        } else {
114            &mut []
115        }
116    }
117
118    /// The WRAM is always present, so `$6000-$7FFF` stays mapped whether or
119    /// not it is battery-backed.
120    fn cpu_read_unmapped(&self, addr: u16) -> bool {
121        (0x4020..=0x5FFF).contains(&addr)
122    }
123
124    fn cpu_read(&mut self, addr: u16) -> u8 {
125        if (0x6000..=0x7FFF).contains(&addr) {
126            return self.wram[usize::from(addr - 0x6000)];
127        }
128        if (0x8000..=0xFFFF).contains(&addr) {
129            let count = (self.prg_rom.len() / PRG_BANK_32K).max(1);
130            let bank = (self.prg_bank as usize) % count;
131            self.prg_rom[bank * PRG_BANK_32K + (addr as usize - 0x8000)]
132        } else {
133            0
134        }
135    }
136
137    fn cpu_write(&mut self, addr: u16, value: u8) {
138        if (0x6000..=0x7FFF).contains(&addr) {
139            self.wram[usize::from(addr - 0x6000)] = value;
140            return;
141        }
142        if (0x8000..=0xFFFF).contains(&addr) {
143            self.prg_bank = value;
144        }
145    }
146
147    fn ppu_read(&mut self, addr: u16) -> u8 {
148        let addr = addr & 0x3FFF;
149        match addr {
150            0x0000..=0x1FFF => self.chr[addr as usize],
151            0x2000..=0x3EFF => self.vram[nametable_offset(addr, self.mirroring)],
152            _ => 0,
153        }
154    }
155
156    fn ppu_write(&mut self, addr: u16, value: u8) {
157        let addr = addr & 0x3FFF;
158        match addr {
159            0x0000..=0x1FFF => {
160                if self.chr_is_ram {
161                    self.chr[addr as usize] = value;
162                }
163            }
164            0x2000..=0x3EFF => {
165                let off = nametable_offset(addr, self.mirroring);
166                self.vram[off] = value;
167            }
168            _ => {}
169        }
170    }
171
172    fn current_mirroring(&self) -> Mirroring {
173        self.mirroring
174    }
175
176    fn save_state(&self) -> Vec<u8> {
177        let chr_extra = if self.chr_is_ram { self.chr.len() } else { 0 };
178        let mut out = Vec::with_capacity(2 + self.vram.len() + chr_extra);
179        out.push(SAVE_STATE_VERSION);
180        out.push(self.prg_bank);
181        out.extend_from_slice(&self.vram);
182        if self.chr_is_ram {
183            out.extend_from_slice(&self.chr);
184        }
185        out.extend_from_slice(&self.wram);
186        out
187    }
188
189    fn load_state(&mut self, data: &[u8]) -> Result<(), MapperError> {
190        let chr_extra = if self.chr_is_ram { self.chr.len() } else { 0 };
191        let version = *data.first().ok_or(MapperError::WrongLength {
192            expected: 1,
193            got: 0,
194        })?;
195        if version != SAVE_STATE_VERSION {
196            return Err(MapperError::UnsupportedVersion(version));
197        }
198        let wram_len = self.wram.len();
199        let expected = 2 + self.vram.len() + chr_extra + wram_len;
200        if data.len() != expected {
201            return Err(MapperError::WrongLength {
202                expected,
203                got: data.len(),
204            });
205        }
206        self.prg_bank = data[1];
207        let mut cursor = 2;
208        self.vram
209            .copy_from_slice(&data[cursor..cursor + self.vram.len()]);
210        cursor += self.vram.len();
211        if self.chr_is_ram {
212            self.chr
213                .copy_from_slice(&data[cursor..cursor + self.chr.len()]);
214            cursor += self.chr.len();
215        }
216        self.wram.copy_from_slice(&data[cursor..cursor + wram_len]);
217        Ok(())
218    }
219}
220
221#[cfg(test)]
222#[allow(clippy::cast_possible_truncation)]
223mod tests {
224    use super::*;
225
226    fn synth_prg_32k(banks: usize) -> Box<[u8]> {
227        let mut v = vec![0xFFu8; banks * PRG_BANK_32K];
228        for b in 0..banks {
229            v[b * PRG_BANK_32K] = b as u8;
230        }
231        v.into_boxed_slice()
232    }
233
234    #[test]
235    fn m241_full_byte_selects_32k_prg() {
236        let mut m = Bxrom241::new(synth_prg_32k(8), Box::new([]), Mirroring::Vertical).unwrap();
237        m.cpu_write(0x8000, 5);
238        assert_eq!(m.cpu_read(0x8000), 5);
239        // No bus conflict: the written value sticks even though offset 0 of the
240        // landing bank is not 0xFF.
241        m.cpu_write(0xFFFF, 3);
242        assert_eq!(m.cpu_read(0x8000), 3);
243    }
244
245    #[test]
246    fn m241_chr_ram_round_trip() {
247        let mut m = Bxrom241::new(synth_prg_32k(2), Box::new([]), Mirroring::Vertical).unwrap();
248        m.ppu_write(0x0010, 0xAB);
249        assert_eq!(m.ppu_read(0x0010), 0xAB);
250    }
251}