Skip to main content

rustynes_mappers/
m007_axrom.rs

1//! `AxROM` (iNES mapper 7) implementation.
2//!
3//! `AxROM` (`AMROM`, `ANROM`, `AOROM`, ...) switches a single 32 KiB PRG bank
4//! across `$8000-$FFFF`. Bit 4 of the bank-select write toggles between
5//! single-screen-A and single-screen-B mirroring. CHR is always RAM (8 KiB)
6//! on stock `AxROM`. No IRQ, no bus conflicts on most variants (`AOROM`
7//! technically has them; `AMROM` / `ANROM` do not — we omit conflicts
8//! since cleanly-written titles don't depend on them and modeling-them-
9//! without-cause makes the much more common `AMROM` / `ANROM` ROMs
10//! misbehave on certain edges).
11
12use crate::cartridge::Mirroring;
13use crate::mapper::{Mapper, MapperCaps, MapperError};
14use alloc::{boxed::Box, vec::Vec};
15use alloc::{format, vec};
16
17const PRG_BANK_32K: usize = 0x8000;
18const CHR_BANK_8K: usize = 0x2000;
19const NAMETABLE_SIZE: usize = 0x0400;
20
21const SAVE_STATE_VERSION: u8 = 1;
22
23/// `AxROM` mapper.
24pub struct AxRom {
25    prg_rom: Box<[u8]>,
26    chr: Box<[u8]>,
27    vram: Box<[u8]>,
28    chr_is_ram: bool,
29    bank: u8,
30    mirroring_b: bool,
31}
32
33impl AxRom {
34    /// Construct a new `AxROM` mapper.
35    ///
36    /// # Errors
37    ///
38    /// Returns [`MapperError::Invalid`] when sizes don't match.
39    pub fn new(prg_rom: Box<[u8]>, chr_rom: Box<[u8]>) -> Result<Self, MapperError> {
40        if prg_rom.is_empty() || !prg_rom.len().is_multiple_of(PRG_BANK_32K) {
41            return Err(MapperError::Invalid(format!(
42                "AxROM PRG-ROM size {} is not a non-zero multiple of 32 KiB",
43                prg_rom.len()
44            )));
45        }
46        let chr_is_ram = chr_rom.is_empty();
47        let chr: Box<[u8]> = if chr_is_ram {
48            vec![0u8; CHR_BANK_8K].into_boxed_slice()
49        } else if chr_rom.len() == CHR_BANK_8K {
50            chr_rom
51        } else {
52            return Err(MapperError::Invalid(format!(
53                "AxROM expects 8 KiB CHR (RAM or ROM); got {} bytes",
54                chr_rom.len()
55            )));
56        };
57        Ok(Self {
58            prg_rom,
59            chr,
60            vram: vec![0u8; 2 * NAMETABLE_SIZE].into_boxed_slice(),
61            chr_is_ram,
62            bank: 0,
63            mirroring_b: false,
64        })
65    }
66
67    fn nametable_offset(&self, addr: u16) -> usize {
68        let local = (addr as usize) & (NAMETABLE_SIZE - 1);
69        let physical = usize::from(self.mirroring_b);
70        physical * NAMETABLE_SIZE + local
71    }
72}
73
74impl Mapper for AxRom {
75    // v2.8.0 Phase 4 — no per-cycle hooks (no IRQ, no audio): the bus
76    // skips all four per-CPU-cycle dispatches for this board.
77    fn caps(&self) -> MapperCaps {
78        MapperCaps::NONE
79    }
80
81    fn cpu_read(&mut self, addr: u16) -> u8 {
82        if (0x8000..=0xFFFF).contains(&addr) {
83            let bank_count = (self.prg_rom.len() / PRG_BANK_32K).max(1);
84            let bank = (self.bank as usize) % bank_count;
85            let off = (addr - 0x8000) as usize;
86            self.prg_rom[bank * PRG_BANK_32K + off]
87        } else {
88            0
89        }
90    }
91
92    fn cpu_write(&mut self, addr: u16, value: u8) {
93        if (0x8000..=0xFFFF).contains(&addr) {
94            self.bank = value & 0x07;
95            self.mirroring_b = (value & 0x10) != 0;
96        }
97    }
98
99    fn chr_phys(&self, addr: u16) -> Option<u32> {
100        // AxROM CHR is unbanked; offset == address. Usually CHR-RAM (-> None).
101        if self.chr_is_ram {
102            None
103        } else {
104            Some(u32::from(addr & 0x1FFF))
105        }
106    }
107
108    fn ppu_read(&mut self, addr: u16) -> u8 {
109        let addr = addr & 0x3FFF;
110        match addr {
111            0x0000..=0x1FFF => self.chr[addr as usize],
112            0x2000..=0x3EFF => self.vram[self.nametable_offset(addr)],
113            _ => 0,
114        }
115    }
116
117    fn ppu_write(&mut self, addr: u16, value: u8) {
118        let addr = addr & 0x3FFF;
119        match addr {
120            0x0000..=0x1FFF => {
121                if self.chr_is_ram {
122                    self.chr[addr as usize] = value;
123                }
124            }
125            0x2000..=0x3EFF => {
126                let off = self.nametable_offset(addr);
127                self.vram[off] = value;
128            }
129            _ => {}
130        }
131    }
132
133    fn current_mirroring(&self) -> Mirroring {
134        if self.mirroring_b {
135            Mirroring::SingleScreenB
136        } else {
137            Mirroring::SingleScreenA
138        }
139    }
140
141    fn save_state(&self) -> Vec<u8> {
142        let mut out = Vec::with_capacity(
143            3 + self.vram.len() + if self.chr_is_ram { self.chr.len() } else { 0 },
144        );
145        out.push(SAVE_STATE_VERSION);
146        out.push(self.bank);
147        out.push(u8::from(self.mirroring_b));
148        out.extend_from_slice(&self.vram);
149        if self.chr_is_ram {
150            out.extend_from_slice(&self.chr);
151        }
152        out
153    }
154
155    fn load_state(&mut self, data: &[u8]) -> Result<(), MapperError> {
156        let need_chr = if self.chr_is_ram { self.chr.len() } else { 0 };
157        let expected = 3 + self.vram.len() + need_chr;
158        if data.len() != expected {
159            return Err(MapperError::WrongLength {
160                expected,
161                got: data.len(),
162            });
163        }
164        if data[0] != SAVE_STATE_VERSION {
165            return Err(MapperError::UnsupportedVersion(data[0]));
166        }
167        self.bank = data[1];
168        self.mirroring_b = data[2] != 0;
169        let mut cursor = 3;
170        self.vram
171            .copy_from_slice(&data[cursor..cursor + self.vram.len()]);
172        cursor += self.vram.len();
173        if self.chr_is_ram {
174            self.chr
175                .copy_from_slice(&data[cursor..cursor + self.chr.len()]);
176        }
177        Ok(())
178    }
179}
180
181#[cfg(test)]
182#[allow(clippy::cast_possible_truncation)]
183mod tests {
184    use super::*;
185
186    fn synth_prg(banks: usize) -> Box<[u8]> {
187        let mut v = vec![0u8; banks * PRG_BANK_32K];
188        for b in 0..banks {
189            v[b * PRG_BANK_32K] = b as u8;
190        }
191        v.into_boxed_slice()
192    }
193
194    #[test]
195    fn axrom_default_bank_zero() {
196        let mut m = AxRom::new(synth_prg(4), Box::new([])).unwrap();
197        assert_eq!(m.cpu_read(0x8000), 0);
198    }
199
200    #[test]
201    fn axrom_bank_select_and_mirroring() {
202        let mut m = AxRom::new(synth_prg(4), Box::new([])).unwrap();
203        m.cpu_write(0x8000, 0b0001_0010); // bank 2, mirror B
204        assert_eq!(m.cpu_read(0x8000), 2);
205        assert_eq!(m.current_mirroring(), Mirroring::SingleScreenB);
206        m.cpu_write(0x8000, 0b0000_0001); // bank 1, mirror A
207        assert_eq!(m.cpu_read(0x8000), 1);
208        assert_eq!(m.current_mirroring(), Mirroring::SingleScreenA);
209    }
210
211    #[test]
212    fn axrom_chr_ram_round_trip() {
213        let mut m = AxRom::new(synth_prg(2), Box::new([])).unwrap();
214        m.ppu_write(0x0123, 0xAB);
215        assert_eq!(m.ppu_read(0x0123), 0xAB);
216    }
217}