Skip to main content

rustynes_mappers/
m152_bandai152.rs

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