Skip to main content

rustynes_mappers/
m066_gxrom.rs

1//! `GxROM` (iNES mapper 66) implementation.
2//!
3//! `GxROM` (`GNROM`, `MHROM`) selects a 32 KiB PRG bank from bits 5-4 of
4//! the bank-select write and an 8 KiB CHR-ROM bank from bits 1-0. Bus
5//! conflicts are present (per `docs/mappers.md` §Bus conflicts).
6//! Mirroring is fixed by the iNES header; no IRQ.
7
8use crate::cartridge::Mirroring;
9use crate::mapper::{Mapper, MapperCaps, MapperError};
10use alloc::{boxed::Box, vec::Vec};
11use alloc::{format, vec};
12
13const PRG_BANK_32K: usize = 0x8000;
14const CHR_BANK_8K: usize = 0x2000;
15const NAMETABLE_SIZE: usize = 0x0400;
16const NAMETABLE_SIZE_U16: u16 = 0x0400;
17
18const SAVE_STATE_VERSION: u8 = 1;
19
20/// `GxROM` mapper.
21pub struct GxRom {
22    prg_rom: Box<[u8]>,
23    chr_rom: Box<[u8]>,
24    vram: Box<[u8]>,
25    prg_bank: u8,
26    chr_bank: u8,
27    mirroring: Mirroring,
28}
29
30impl GxRom {
31    /// Construct a new `GxROM` mapper.
32    ///
33    /// # Errors
34    ///
35    /// Returns [`MapperError::Invalid`] when sizes don't match.
36    pub fn new(
37        prg_rom: Box<[u8]>,
38        chr_rom: Box<[u8]>,
39        mirroring: Mirroring,
40    ) -> Result<Self, MapperError> {
41        if prg_rom.is_empty() || !prg_rom.len().is_multiple_of(PRG_BANK_32K) {
42            return Err(MapperError::Invalid(format!(
43                "GxROM PRG-ROM size {} is not a non-zero multiple of 32 KiB",
44                prg_rom.len()
45            )));
46        }
47        if chr_rom.is_empty() || !chr_rom.len().is_multiple_of(CHR_BANK_8K) {
48            return Err(MapperError::Invalid(format!(
49                "GxROM CHR-ROM size {} is not a non-zero multiple of 8 KiB",
50                chr_rom.len()
51            )));
52        }
53        Ok(Self {
54            prg_rom,
55            chr_rom,
56            vram: vec![0u8; 2 * NAMETABLE_SIZE].into_boxed_slice(),
57            prg_bank: 0,
58            chr_bank: 0,
59            mirroring,
60        })
61    }
62
63    const fn nametable_offset(&self, addr: u16) -> usize {
64        let table = (((addr - 0x2000) / NAMETABLE_SIZE_U16) & 0x03) as u8;
65        let local = (addr as usize) & (NAMETABLE_SIZE - 1);
66        let physical = self.mirroring.physical_bank(table);
67        physical * NAMETABLE_SIZE + local
68    }
69
70    fn read_prg(&self, addr: u16) -> u8 {
71        let bank_count = (self.prg_rom.len() / PRG_BANK_32K).max(1);
72        let bank = (self.prg_bank as usize) % bank_count;
73        let off = (addr - 0x8000) as usize;
74        self.prg_rom[bank * PRG_BANK_32K + off]
75    }
76}
77
78impl Mapper for GxRom {
79    // v2.8.0 Phase 4 — no per-cycle hooks (no IRQ, no audio): the bus
80    // skips all four per-CPU-cycle dispatches for this board.
81    fn caps(&self) -> MapperCaps {
82        MapperCaps::NONE
83    }
84
85    fn cpu_read(&mut self, addr: u16) -> u8 {
86        if (0x8000..=0xFFFF).contains(&addr) {
87            self.read_prg(addr)
88        } else {
89            0
90        }
91    }
92
93    fn cpu_write(&mut self, addr: u16, value: u8) {
94        if (0x8000..=0xFFFF).contains(&addr) {
95            // Bus conflict.
96            let prg_byte = self.read_prg(addr);
97            let effective = value & prg_byte;
98            self.prg_bank = (effective >> 4) & 0x03;
99            self.chr_bank = effective & 0x03;
100        }
101    }
102
103    fn ppu_read(&mut self, addr: u16) -> u8 {
104        let addr = addr & 0x3FFF;
105        match addr {
106            0x0000..=0x1FFF => {
107                let bank_count = (self.chr_rom.len() / CHR_BANK_8K).max(1);
108                let bank = (self.chr_bank as usize) % bank_count;
109                self.chr_rom[bank * CHR_BANK_8K + (addr as usize)]
110            }
111            0x2000..=0x3EFF => self.vram[self.nametable_offset(addr)],
112            _ => 0,
113        }
114    }
115
116    fn ppu_write(&mut self, addr: u16, value: u8) {
117        let addr = addr & 0x3FFF;
118        if let 0x2000..=0x3EFF = addr {
119            let off = self.nametable_offset(addr);
120            self.vram[off] = value;
121        }
122    }
123
124    fn current_mirroring(&self) -> Mirroring {
125        self.mirroring
126    }
127
128    // GxROM has fixed solder-pad mirroring — a game-DB header correction is valid.
129    fn has_hardwired_mirroring(&self) -> bool {
130        true
131    }
132
133    fn save_state(&self) -> Vec<u8> {
134        let mut out = Vec::with_capacity(3 + self.vram.len());
135        out.push(SAVE_STATE_VERSION);
136        out.push(self.prg_bank);
137        out.push(self.chr_bank);
138        out.extend_from_slice(&self.vram);
139        out
140    }
141
142    fn load_state(&mut self, data: &[u8]) -> Result<(), MapperError> {
143        let expected = 3 + self.vram.len();
144        if data.len() != expected {
145            return Err(MapperError::WrongLength {
146                expected,
147                got: data.len(),
148            });
149        }
150        if data[0] != SAVE_STATE_VERSION {
151            return Err(MapperError::UnsupportedVersion(data[0]));
152        }
153        self.prg_bank = data[1];
154        self.chr_bank = data[2];
155        self.vram.copy_from_slice(&data[3..3 + self.vram.len()]);
156        Ok(())
157    }
158}
159
160#[cfg(test)]
161#[allow(clippy::cast_possible_truncation)]
162mod tests {
163    use super::*;
164
165    fn synth_prg(banks: usize) -> Box<[u8]> {
166        let mut v = vec![0u8; banks * PRG_BANK_32K];
167        for b in 0..banks {
168            for o in 0..PRG_BANK_32K {
169                v[b * PRG_BANK_32K + o] = if o == 0 { b as u8 } else { 0xFF };
170            }
171        }
172        v.into_boxed_slice()
173    }
174
175    fn synth_chr(banks: usize) -> Box<[u8]> {
176        let mut v = vec![0u8; banks * CHR_BANK_8K];
177        for b in 0..banks {
178            v[b * CHR_BANK_8K] = b as u8;
179        }
180        v.into_boxed_slice()
181    }
182
183    #[test]
184    fn gxrom_bank_select_no_conflict() {
185        // PRG byte 0xFF, AND with 0x33 = 0x33 -> prg_bank=3, chr_bank=3.
186        let mut m = GxRom::new(synth_prg(4), synth_chr(4), Mirroring::Vertical).unwrap();
187        // First write goes to a $8001+ address whose PRG byte is 0xFF (we
188        // initialize all but the first byte of each bank to 0xFF).
189        m.cpu_write(0x8001, 0x33);
190        assert_eq!(m.cpu_read(0x8000), 3); // bank 3 marker
191        assert_eq!(m.ppu_read(0x0000), 3);
192    }
193
194    #[test]
195    fn gxrom_bus_conflict_at_offset_zero() {
196        // At offset 0 of bank 0, byte = 0x00. ANDing kills the write.
197        let mut m = GxRom::new(synth_prg(4), synth_chr(4), Mirroring::Vertical).unwrap();
198        m.cpu_write(0x8000, 0xFF);
199        assert_eq!(m.cpu_read(0x8000), 0); // unchanged
200    }
201}