Skip to main content

rustynes_mappers/
m070_bandai74.rs

1//! Bandai discrete (iNES mapper 70) implementation.
2//!
3//! A UxROM-like discrete board: a single `$8000-$FFFF` write register
4//! `[PPPP CCCC]` selects a 16 KiB switchable PRG bank at `$8000-$BFFF`
5//! (bits 4-7) and an 8 KiB CHR bank at `$0000-$1FFF` (bits 0-3). The last
6//! 16 KiB PRG bank is fixed at `$C000-$FFFF`. Mirroring is fixed from the
7//! iNES header (the 1-screen mapper-controlled variant is mapper 152). No
8//! IRQ.
9//!
10//! The real board has bus conflicts; the project models discrete boards
11//! without bus-conflict emulation (matching UxROM / GxROM), which is safe
12//! for the licensed library because the games write the correct value.
13//!
14//! See `docs/mappers.md` §Mapper coverage matrix and
15//! `nesdev_wiki/INES_Mapper_070.xhtml`.
16
17#![allow(clippy::cast_possible_truncation, clippy::doc_markdown)]
18
19use crate::cartridge::Mirroring;
20use crate::mapper::{Mapper, MapperCaps, MapperError};
21use alloc::{boxed::Box, vec::Vec};
22use alloc::{format, vec};
23
24const PRG_BANK_16K: usize = 0x4000;
25const CHR_BANK_8K: usize = 0x2000;
26const NAMETABLE_SIZE: usize = 0x0400;
27const NAMETABLE_SIZE_U16: u16 = 0x0400;
28
29const SAVE_STATE_VERSION: u8 = 1;
30
31/// Bandai discrete mapper (iNES mapper 70).
32pub struct Bandai74 {
33    prg_rom: Box<[u8]>,
34    chr: Box<[u8]>,
35    vram: Box<[u8]>,
36    chr_is_ram: bool,
37    prg_bank: u8,
38    chr_bank: u8,
39    mirroring: Mirroring,
40}
41
42impl Bandai74 {
43    /// Construct a new Bandai-70 mapper.
44    ///
45    /// `prg_rom` must be a non-zero multiple of 16 KiB. CHR-RAM is selected
46    /// when `chr_rom` is empty; otherwise CHR-ROM length must be a multiple
47    /// of 8 KiB.
48    ///
49    /// # Errors
50    ///
51    /// Returns [`MapperError::Invalid`] when sizes don't match the
52    /// constraints.
53    pub fn new(
54        prg_rom: Box<[u8]>,
55        chr_rom: Box<[u8]>,
56        mirroring: Mirroring,
57    ) -> Result<Self, MapperError> {
58        if prg_rom.is_empty() || !prg_rom.len().is_multiple_of(PRG_BANK_16K) {
59            return Err(MapperError::Invalid(format!(
60                "Bandai-70 PRG-ROM size {} is not a non-zero multiple of 16 KiB",
61                prg_rom.len()
62            )));
63        }
64        let chr_is_ram = chr_rom.is_empty();
65        let chr: Box<[u8]> = if chr_is_ram {
66            vec![0u8; CHR_BANK_8K].into_boxed_slice()
67        } else if chr_rom.len().is_multiple_of(CHR_BANK_8K) {
68            chr_rom
69        } else {
70            return Err(MapperError::Invalid(format!(
71                "Bandai-70 expects an 8 KiB multiple of CHR; got {} bytes",
72                chr_rom.len()
73            )));
74        };
75        Ok(Self {
76            prg_rom,
77            chr,
78            vram: vec![0u8; 2 * NAMETABLE_SIZE].into_boxed_slice(),
79            chr_is_ram,
80            prg_bank: 0,
81            chr_bank: 0,
82            mirroring,
83        })
84    }
85
86    const fn nametable_offset(&self, addr: u16) -> usize {
87        let table = (((addr - 0x2000) / NAMETABLE_SIZE_U16) & 0x03) as u8;
88        let local = (addr as usize) & (NAMETABLE_SIZE - 1);
89        let physical = self.mirroring.physical_bank(table);
90        physical * NAMETABLE_SIZE + local
91    }
92
93    fn chr_offset(&self, addr: u16) -> usize {
94        let bank_count = (self.chr.len() / CHR_BANK_8K).max(1);
95        let bank = (self.chr_bank as usize) % bank_count;
96        bank * CHR_BANK_8K + (addr as usize & (CHR_BANK_8K - 1))
97    }
98}
99
100impl Mapper for Bandai74 {
101    /// Fixed mirroring (v2.7.2, core audit §5.6): `nesdev_wiki/INES_Mapper_070`
102    /// gives mapper 70 no mirroring control, the 1-screen variant being mapper
103    /// 152. Except for a FOUR-SCREEN header: that is how the 152 variant was
104    /// marked before 152 existed ("alternative nametables" bit), so such a
105    /// dump is not known to be fixed and keeps the safe `false`.
106    fn has_hardwired_mirroring(&self) -> bool {
107        !matches!(self.mirroring, Mirroring::FourScreen)
108    }
109
110    // v2.8.0 Phase 4 — no per-cycle hooks (no IRQ, no audio): the bus
111    // skips all four per-CPU-cycle dispatches for this board.
112    fn caps(&self) -> MapperCaps {
113        MapperCaps::NONE
114    }
115
116    fn cpu_read(&mut self, addr: u16) -> u8 {
117        let bank_count = (self.prg_rom.len() / PRG_BANK_16K).max(1);
118        match addr {
119            0x8000..=0xBFFF => {
120                let bank = (self.prg_bank as usize) % bank_count;
121                let off = (addr - 0x8000) as usize;
122                self.prg_rom[bank * PRG_BANK_16K + off]
123            }
124            0xC000..=0xFFFF => {
125                let last = bank_count - 1;
126                let off = (addr - 0xC000) as usize;
127                self.prg_rom[last * PRG_BANK_16K + off]
128            }
129            _ => 0,
130        }
131    }
132
133    fn cpu_write(&mut self, addr: u16, value: u8) {
134        if let 0x8000..=0xFFFF = addr {
135            // [PPPP CCCC]: bits 4-7 = 16K PRG bank, bits 0-3 = 8K CHR bank.
136            self.prg_bank = (value >> 4) & 0x0F;
137            self.chr_bank = value & 0x0F;
138        }
139    }
140
141    fn ppu_read(&mut self, addr: u16) -> u8 {
142        let addr = addr & 0x3FFF;
143        match addr {
144            0x0000..=0x1FFF => self.chr[self.chr_offset(addr)],
145            0x2000..=0x3EFF => self.vram[self.nametable_offset(addr)],
146            _ => 0,
147        }
148    }
149
150    fn ppu_write(&mut self, addr: u16, value: u8) {
151        let addr = addr & 0x3FFF;
152        match addr {
153            0x0000..=0x1FFF => {
154                if self.chr_is_ram {
155                    let off = self.chr_offset(addr);
156                    self.chr[off] = value;
157                }
158            }
159            0x2000..=0x3EFF => {
160                let off = self.nametable_offset(addr);
161                self.vram[off] = value;
162            }
163            _ => {}
164        }
165    }
166
167    fn current_mirroring(&self) -> Mirroring {
168        self.mirroring
169    }
170
171    fn debug_info(&self) -> crate::mapper::MapperDebugInfo {
172        let mut info = crate::mapper::MapperDebugInfo {
173            mapper_id: 70,
174            name: "Bandai discrete (70)".into(),
175            mirroring: crate::mapper::mirroring_name(self.mirroring),
176            ..Default::default()
177        };
178        info.prg_banks
179            .push(("PRG".into(), format!("{:#04x}", self.prg_bank)));
180        info.chr_banks
181            .push(("CHR".into(), format!("{:#04x}", self.chr_bank)));
182        info
183    }
184
185    fn save_state(&self) -> Vec<u8> {
186        let mut out = Vec::with_capacity(
187            3 + self.vram.len() + if self.chr_is_ram { self.chr.len() } else { 0 },
188        );
189        out.push(SAVE_STATE_VERSION);
190        out.push(self.prg_bank);
191        out.push(self.chr_bank);
192        out.extend_from_slice(&self.vram);
193        if self.chr_is_ram {
194            out.extend_from_slice(&self.chr);
195        }
196        out
197    }
198
199    fn load_state(&mut self, data: &[u8]) -> Result<(), MapperError> {
200        let need_chr = if self.chr_is_ram { self.chr.len() } else { 0 };
201        let expected = 3 + self.vram.len() + need_chr;
202        if data.len() != expected {
203            return Err(MapperError::WrongLength {
204                expected,
205                got: data.len(),
206            });
207        }
208        if data[0] != SAVE_STATE_VERSION {
209            return Err(MapperError::UnsupportedVersion(data[0]));
210        }
211        self.prg_bank = data[1];
212        self.chr_bank = data[2];
213        let mut cursor = 3;
214        self.vram
215            .copy_from_slice(&data[cursor..cursor + self.vram.len()]);
216        cursor += self.vram.len();
217        if self.chr_is_ram {
218            self.chr
219                .copy_from_slice(&data[cursor..cursor + self.chr.len()]);
220        }
221        Ok(())
222    }
223}
224
225#[cfg(test)]
226#[allow(clippy::cast_possible_truncation)]
227mod tests {
228    use super::*;
229
230    fn synth_prg(banks: usize) -> Box<[u8]> {
231        let mut v = vec![0u8; banks * PRG_BANK_16K];
232        for b in 0..banks {
233            v[b * PRG_BANK_16K] = b as u8;
234        }
235        v.into_boxed_slice()
236    }
237
238    fn synth_chr(banks_8k: usize) -> Box<[u8]> {
239        let mut v = vec![0u8; banks_8k * CHR_BANK_8K];
240        for b in 0..banks_8k {
241            v[b * CHR_BANK_8K] = b as u8;
242        }
243        v.into_boxed_slice()
244    }
245
246    #[test]
247    fn defaults_first_prg_last_fixed() {
248        let mut m = Bandai74::new(synth_prg(8), synth_chr(4), Mirroring::Vertical).unwrap();
249        assert_eq!(m.cpu_read(0x8000), 0);
250        assert_eq!(m.cpu_read(0xC000), 7); // last 16K fixed
251        assert_eq!(m.ppu_read(0x0000), 0);
252    }
253
254    #[test]
255    fn prg_and_chr_bank_select() {
256        let mut m = Bandai74::new(synth_prg(8), synth_chr(4), Mirroring::Vertical).unwrap();
257        // [PPPP CCCC]: PRG=5 (bits 4-7), CHR=3 (bits 0-3) -> value 0x53.
258        m.cpu_write(0x8000, 0x53);
259        assert_eq!(m.cpu_read(0x8000), 5);
260        assert_eq!(m.cpu_read(0xC000), 7); // fixed unchanged
261        assert_eq!(m.ppu_read(0x0000), 3);
262    }
263
264    #[test]
265    fn chr_ram_round_trip() {
266        let mut m = Bandai74::new(synth_prg(2), Box::new([]), Mirroring::Horizontal).unwrap();
267        m.ppu_write(0x0010, 0xCD);
268        assert_eq!(m.ppu_read(0x0010), 0xCD);
269    }
270
271    #[test]
272    fn save_state_round_trip() {
273        let mut m = Bandai74::new(synth_prg(4), synth_chr(2), Mirroring::Vertical).unwrap();
274        m.cpu_write(0x8000, 0x21);
275        let blob = m.save_state();
276        let mut m2 = Bandai74::new(synth_prg(4), synth_chr(2), Mirroring::Vertical).unwrap();
277        m2.load_state(&blob).unwrap();
278        assert_eq!(m.cpu_read(0x8000), m2.cpu_read(0x8000));
279        assert_eq!(m.ppu_read(0x0000), m2.ppu_read(0x0000));
280    }
281}