Skip to main content

rustynes_mappers/
m079_ave_nina03_06.rs

1//! AVE `NINA-03` / `NINA-06` (mapper 79).
2//!
3//! Decodes its bank-select register in the `$4100-$5FFF` expansion window
4//! rather than the usual `$8000-$FFFF` -- specifically on address line A8, so
5//! the write only lands at addresses with that bit set. Decoding down there
6//! avoids bus conflicts without any gating logic, since no ROM drives the bus
7//! in that range.
8//!
9//! `NINA-006` (mapper 113) is the multicart relative, in
10//! `m113_ave_nina006.rs`; `NINA-001` is an unrelated board on mapper 34, in
11//! `m034_bnrom_nina001.rs`.
12//!
13//! A discrete-logic board in the shape of the stock mappers (`NROM`, `CNROM`,
14//! `UxROM`, `GxROM`, `AxROM`): bank-select latch registers, no IRQ, no on-cart
15//! audio. Banking / mirroring semantics are cross-checked against the
16//! `GeraNES` reference emulator (cross-referenced, not copied)
17//! and the nesdev wiki, and validated by register-decode + save-state unit
18//! tests.
19//!
20//! See `docs/mappers.md` §Mapper coverage matrix.
21
22#![allow(
23    clippy::bool_to_int_with_if,
24    clippy::cast_lossless,
25    clippy::cast_possible_truncation,
26    clippy::doc_markdown,
27    clippy::match_same_arms,
28    clippy::missing_const_for_fn,
29    clippy::similar_names,
30    clippy::struct_excessive_bools,
31    clippy::too_many_lines,
32    clippy::unreadable_literal
33)]
34
35use crate::cartridge::Mirroring;
36use crate::mapper::{Mapper, MapperCaps, MapperError};
37use alloc::{boxed::Box, vec::Vec};
38use alloc::{format, vec};
39
40const PRG_BANK_32K: usize = 0x8000;
41const CHR_BANK_8K: usize = 0x2000;
42const NAMETABLE_SIZE: usize = 0x0400;
43const NAMETABLE_SIZE_U16: u16 = 0x0400;
44
45const SAVE_STATE_VERSION: u8 = 1;
46
47const fn nametable_offset(addr: u16, mirroring: Mirroring) -> usize {
48    let table = (((addr - 0x2000) / NAMETABLE_SIZE_U16) & 0x03) as u8;
49    let local = (addr as usize) & (NAMETABLE_SIZE - 1);
50    let physical = mirroring.physical_bank(table);
51    physical * NAMETABLE_SIZE + local
52}
53
54/// Mapper 79 (AVE `NINA-03`/`NINA-06`).
55pub struct Nina0379 {
56    prg_rom: Box<[u8]>,
57    chr: Box<[u8]>,
58    vram: Box<[u8]>,
59    chr_is_ram: bool,
60    prg_bank: u8,
61    chr_bank: u8,
62    mirroring: Mirroring,
63}
64
65impl Nina0379 {
66    /// Construct a new mapper 79 board.
67    ///
68    /// # Errors
69    ///
70    /// Returns [`MapperError::Invalid`] when PRG is not a non-zero multiple of
71    /// 32 KiB, or CHR-ROM (when present) is not a multiple of 8 KiB.
72    pub fn new(
73        prg_rom: Box<[u8]>,
74        chr_rom: Box<[u8]>,
75        mirroring: Mirroring,
76    ) -> Result<Self, MapperError> {
77        if prg_rom.is_empty() || !prg_rom.len().is_multiple_of(PRG_BANK_32K) {
78            return Err(MapperError::Invalid(format!(
79                "mapper 79 PRG-ROM size {} is not a non-zero multiple of 32 KiB",
80                prg_rom.len()
81            )));
82        }
83        let chr_is_ram = chr_rom.is_empty();
84        let chr: Box<[u8]> = if chr_is_ram {
85            vec![0u8; CHR_BANK_8K].into_boxed_slice()
86        } else if chr_rom.len().is_multiple_of(CHR_BANK_8K) {
87            chr_rom
88        } else {
89            return Err(MapperError::Invalid(format!(
90                "mapper 79 CHR-ROM size {} is not a multiple of 8 KiB",
91                chr_rom.len()
92            )));
93        };
94        Ok(Self {
95            prg_rom,
96            chr,
97            vram: vec![0u8; 2 * NAMETABLE_SIZE].into_boxed_slice(),
98            chr_is_ram,
99            prg_bank: 0,
100            chr_bank: 0,
101            mirroring,
102        })
103    }
104
105    fn chr_offset(&self, addr: u16) -> usize {
106        let count = (self.chr.len() / CHR_BANK_8K).max(1);
107        let bank = (self.chr_bank as usize) % count;
108        bank * CHR_BANK_8K + addr as usize
109    }
110}
111
112impl Mapper for Nina0379 {
113    /// Fixed mirroring (v2.7.2, core audit §5.6): `nesdev_wiki/NINA_003_006: "Fixed H or V, controlled by solder pads"`, so a
114    /// per-game database correction of a wrong header bit is safe here.
115    fn has_hardwired_mirroring(&self) -> bool {
116        true
117    }
118
119    fn caps(&self) -> MapperCaps {
120        MapperCaps::NONE
121    }
122
123    // The register window lives in $4100-$5FFF; reads there are open bus
124    // (write-only registers), so the default `cpu_read_unmapped` is correct.
125
126    fn cpu_read(&mut self, addr: u16) -> u8 {
127        if (0x8000..=0xFFFF).contains(&addr) {
128            let count = (self.prg_rom.len() / PRG_BANK_32K).max(1);
129            let bank = (self.prg_bank as usize) % count;
130            self.prg_rom[bank * PRG_BANK_32K + (addr as usize - 0x8000)]
131        } else {
132            0
133        }
134    }
135
136    fn cpu_write(&mut self, addr: u16, value: u8) {
137        // $4100-$5FFF, decoded on A8.
138        if (0x4100..=0x5FFF).contains(&addr) && (addr & 0x0100) != 0 {
139            self.chr_bank = value & 0x07;
140            self.prg_bank = (value >> 3) & 0x01;
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[nametable_offset(addr, self.mirroring)],
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 = nametable_offset(addr, self.mirroring);
164                self.vram[off] = value;
165            }
166            _ => {}
167        }
168    }
169
170    fn current_mirroring(&self) -> Mirroring {
171        self.mirroring
172    }
173
174    fn save_state(&self) -> Vec<u8> {
175        let chr_extra = if self.chr_is_ram { self.chr.len() } else { 0 };
176        let mut out = Vec::with_capacity(3 + self.vram.len() + chr_extra);
177        out.push(SAVE_STATE_VERSION);
178        out.push(self.prg_bank);
179        out.push(self.chr_bank);
180        out.extend_from_slice(&self.vram);
181        if self.chr_is_ram {
182            out.extend_from_slice(&self.chr);
183        }
184        out
185    }
186
187    fn load_state(&mut self, data: &[u8]) -> Result<(), MapperError> {
188        let chr_extra = if self.chr_is_ram { self.chr.len() } else { 0 };
189        let expected = 3 + self.vram.len() + chr_extra;
190        if data.len() != expected {
191            return Err(MapperError::WrongLength {
192                expected,
193                got: data.len(),
194            });
195        }
196        if data[0] != SAVE_STATE_VERSION {
197            return Err(MapperError::UnsupportedVersion(data[0]));
198        }
199        self.prg_bank = data[1];
200        self.chr_bank = data[2];
201        let mut cursor = 3;
202        self.vram
203            .copy_from_slice(&data[cursor..cursor + self.vram.len()]);
204        cursor += self.vram.len();
205        if self.chr_is_ram {
206            self.chr
207                .copy_from_slice(&data[cursor..cursor + self.chr.len()]);
208        }
209        Ok(())
210    }
211}
212
213#[cfg(test)]
214mod tests {
215    use super::*;
216
217    fn synth_prg_32k(banks: usize) -> Box<[u8]> {
218        let mut v = vec![0xFFu8; banks * PRG_BANK_32K];
219        for b in 0..banks {
220            v[b * PRG_BANK_32K] = b as u8;
221        }
222        v.into_boxed_slice()
223    }
224
225    fn synth_chr_8k(banks: usize) -> Box<[u8]> {
226        let mut v = vec![0u8; banks * CHR_BANK_8K];
227        for b in 0..banks {
228            v[b * CHR_BANK_8K] = b as u8;
229        }
230        v.into_boxed_slice()
231    }
232
233    #[test]
234    fn m79_register_decodes_on_a8() {
235        let mut m =
236            Nina0379::new(synth_prg_32k(2), synth_chr_8k(8), Mirroring::Horizontal).unwrap();
237        // $4100 has A8 set: value 0b0000_1101 -> CHR = 0b101 = 5, PRG = bit3 = 1.
238        m.cpu_write(0x4100, 0b0000_1101);
239        assert_eq!(m.cpu_read(0x8000), 1);
240        assert_eq!(m.ppu_read(0x0000), 5);
241    }
242
243    #[test]
244    fn m79_no_decode_without_a8() {
245        let mut m =
246            Nina0379::new(synth_prg_32k(2), synth_chr_8k(8), Mirroring::Horizontal).unwrap();
247        // $4000-class addresses are not in $4100-$5FFF; and an in-range addr
248        // with A8 clear ($4200) must NOT latch.
249        m.cpu_write(0x4200, 0b0000_1111);
250        assert_eq!(m.cpu_read(0x8000), 0);
251        assert_eq!(m.ppu_read(0x0000), 0);
252    }
253}