Skip to main content

rustynes_mappers/
m113_ave_nina006.rs

1//! AVE `NINA-006` / `MB-91` multicart (mapper 113).
2//!
3//! Mapper 79 (`m079_ave_nina03_06.rs`) plus a register-controlled mirroring
4//! bit and a wider CHR field -- the additions a multicart needs, since each
5//! bundled game may want different mirroring and the combined CHR is larger
6//! than any single title's. Same A8 decode in the `$4100-$5FFF` window.
7//!
8//! A discrete-logic board in the shape of the stock mappers (`NROM`, `CNROM`,
9//! `UxROM`, `GxROM`, `AxROM`): bank-select latch registers, no IRQ, no on-cart
10//! audio. Banking / mirroring semantics are cross-checked against the
11//! `GeraNES` reference emulator (cross-referenced, not copied)
12//! and the nesdev wiki, and validated by register-decode + save-state unit
13//! tests.
14//!
15//! See `docs/mappers.md` §Mapper coverage matrix.
16
17#![allow(
18    clippy::bool_to_int_with_if,
19    clippy::cast_lossless,
20    clippy::cast_possible_truncation,
21    clippy::doc_markdown,
22    clippy::match_same_arms,
23    clippy::missing_const_for_fn,
24    clippy::similar_names,
25    clippy::struct_excessive_bools,
26    clippy::too_many_lines,
27    clippy::unreadable_literal
28)]
29
30use crate::cartridge::Mirroring;
31use crate::mapper::{Mapper, MapperCaps, MapperError};
32use alloc::{boxed::Box, vec::Vec};
33use alloc::{format, vec};
34
35const PRG_BANK_32K: usize = 0x8000;
36const CHR_BANK_8K: usize = 0x2000;
37const NAMETABLE_SIZE: usize = 0x0400;
38const NAMETABLE_SIZE_U16: u16 = 0x0400;
39
40const SAVE_STATE_VERSION: u8 = 1;
41
42const fn nametable_offset(addr: u16, mirroring: Mirroring) -> usize {
43    let table = (((addr - 0x2000) / NAMETABLE_SIZE_U16) & 0x03) as u8;
44    let local = (addr as usize) & (NAMETABLE_SIZE - 1);
45    let physical = mirroring.physical_bank(table);
46    physical * NAMETABLE_SIZE + local
47}
48
49/// Mapper 113 (`NINA-006`/`MB-91` multicart).
50pub struct Nina006M113 {
51    prg_rom: Box<[u8]>,
52    chr: Box<[u8]>,
53    vram: Box<[u8]>,
54    chr_is_ram: bool,
55    prg_bank: u8,
56    chr_bank: u8,
57    vertical_mirroring: bool,
58}
59
60impl Nina006M113 {
61    /// Construct a new mapper 113 board.
62    ///
63    /// # Errors
64    ///
65    /// Returns [`MapperError::Invalid`] when PRG is not a non-zero multiple of
66    /// 32 KiB, or CHR-ROM (when present) is not a multiple of 8 KiB.
67    pub fn new(prg_rom: Box<[u8]>, chr_rom: Box<[u8]>) -> Result<Self, MapperError> {
68        if prg_rom.is_empty() || !prg_rom.len().is_multiple_of(PRG_BANK_32K) {
69            return Err(MapperError::Invalid(format!(
70                "mapper 113 PRG-ROM size {} is not a non-zero multiple of 32 KiB",
71                prg_rom.len()
72            )));
73        }
74        let chr_is_ram = chr_rom.is_empty();
75        let chr: Box<[u8]> = if chr_is_ram {
76            vec![0u8; CHR_BANK_8K].into_boxed_slice()
77        } else if chr_rom.len().is_multiple_of(CHR_BANK_8K) {
78            chr_rom
79        } else {
80            return Err(MapperError::Invalid(format!(
81                "mapper 113 CHR-ROM size {} is not a multiple of 8 KiB",
82                chr_rom.len()
83            )));
84        };
85        Ok(Self {
86            prg_rom,
87            chr,
88            vram: vec![0u8; 2 * NAMETABLE_SIZE].into_boxed_slice(),
89            chr_is_ram,
90            prg_bank: 0,
91            chr_bank: 0,
92            vertical_mirroring: false,
93        })
94    }
95
96    fn chr_offset(&self, addr: u16) -> usize {
97        let count = (self.chr.len() / CHR_BANK_8K).max(1);
98        let bank = (self.chr_bank as usize) % count;
99        bank * CHR_BANK_8K + addr as usize
100    }
101}
102
103impl Mapper for Nina006M113 {
104    fn caps(&self) -> MapperCaps {
105        MapperCaps::NONE
106    }
107
108    fn cpu_read(&mut self, addr: u16) -> u8 {
109        if (0x8000..=0xFFFF).contains(&addr) {
110            let count = (self.prg_rom.len() / PRG_BANK_32K).max(1);
111            let bank = (self.prg_bank as usize) % count;
112            self.prg_rom[bank * PRG_BANK_32K + (addr as usize - 0x8000)]
113        } else {
114            0
115        }
116    }
117
118    fn cpu_write(&mut self, addr: u16, value: u8) {
119        if (0x4100..=0x5FFF).contains(&addr) && (addr & 0x0100) != 0 {
120            self.prg_bank = (value >> 3) & 0x07;
121            self.chr_bank = (value & 0x07) | ((value >> 3) & 0x08);
122            self.vertical_mirroring = (value & 0x80) != 0;
123        }
124    }
125
126    fn ppu_read(&mut self, addr: u16) -> u8 {
127        let addr = addr & 0x3FFF;
128        match addr {
129            0x0000..=0x1FFF => self.chr[self.chr_offset(addr)],
130            0x2000..=0x3EFF => self.vram[nametable_offset(addr, self.current_mirroring())],
131            _ => 0,
132        }
133    }
134
135    fn ppu_write(&mut self, addr: u16, value: u8) {
136        let addr = addr & 0x3FFF;
137        match addr {
138            0x0000..=0x1FFF => {
139                if self.chr_is_ram {
140                    let off = self.chr_offset(addr);
141                    self.chr[off] = value;
142                }
143            }
144            0x2000..=0x3EFF => {
145                let off = nametable_offset(addr, self.current_mirroring());
146                self.vram[off] = value;
147            }
148            _ => {}
149        }
150    }
151
152    fn current_mirroring(&self) -> Mirroring {
153        if self.vertical_mirroring {
154            Mirroring::Vertical
155        } else {
156            Mirroring::Horizontal
157        }
158    }
159
160    fn save_state(&self) -> Vec<u8> {
161        let chr_extra = if self.chr_is_ram { self.chr.len() } else { 0 };
162        let mut out = Vec::with_capacity(4 + self.vram.len() + chr_extra);
163        out.push(SAVE_STATE_VERSION);
164        out.push(self.prg_bank);
165        out.push(self.chr_bank);
166        out.push(u8::from(self.vertical_mirroring));
167        out.extend_from_slice(&self.vram);
168        if self.chr_is_ram {
169            out.extend_from_slice(&self.chr);
170        }
171        out
172    }
173
174    fn load_state(&mut self, data: &[u8]) -> Result<(), MapperError> {
175        let chr_extra = if self.chr_is_ram { self.chr.len() } else { 0 };
176        let expected = 4 + self.vram.len() + chr_extra;
177        if data.len() != expected {
178            return Err(MapperError::WrongLength {
179                expected,
180                got: data.len(),
181            });
182        }
183        if data[0] != SAVE_STATE_VERSION {
184            return Err(MapperError::UnsupportedVersion(data[0]));
185        }
186        self.prg_bank = data[1];
187        self.chr_bank = data[2];
188        self.vertical_mirroring = data[3] != 0;
189        let mut cursor = 4;
190        self.vram
191            .copy_from_slice(&data[cursor..cursor + self.vram.len()]);
192        cursor += self.vram.len();
193        if self.chr_is_ram {
194            self.chr
195                .copy_from_slice(&data[cursor..cursor + self.chr.len()]);
196        }
197        Ok(())
198    }
199}
200
201#[cfg(test)]
202mod tests {
203    use super::*;
204
205    fn synth_prg_32k(banks: usize) -> Box<[u8]> {
206        let mut v = vec![0xFFu8; banks * PRG_BANK_32K];
207        for b in 0..banks {
208            v[b * PRG_BANK_32K] = b as u8;
209        }
210        v.into_boxed_slice()
211    }
212
213    fn synth_chr_8k(banks: usize) -> Box<[u8]> {
214        let mut v = vec![0u8; banks * CHR_BANK_8K];
215        for b in 0..banks {
216            v[b * CHR_BANK_8K] = b as u8;
217        }
218        v.into_boxed_slice()
219    }
220
221    #[test]
222    fn m113_decodes_prg_chr_and_mirroring() {
223        let mut m = Nina006M113::new(synth_prg_32k(4), synth_chr_8k(16)).unwrap();
224        // value 0b1100_0010 (0xC2):
225        //   PRG = (v>>3)&7 = (24)&7 = 0
226        //   CHR = (v&7) | ((v>>3)&8) = 2 | (24 & 8 = 8) = 10
227        //   mirroring bit7 set -> vertical.
228        m.cpu_write(0x4100, 0b1100_0010);
229        assert_eq!(m.cpu_read(0x8000), 0);
230        assert_eq!(m.ppu_read(0x0000), 10);
231        assert_eq!(m.current_mirroring(), Mirroring::Vertical);
232        // Clear bit 7 -> horizontal.
233        m.cpu_write(0x4100, 0b0000_0001);
234        assert_eq!(m.current_mirroring(), Mirroring::Horizontal);
235    }
236}