rustynes_mappers/
m066_gxrom.rs1use 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
20pub 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 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 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 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 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 let mut m = GxRom::new(synth_prg(4), synth_chr(4), Mirroring::Vertical).unwrap();
187 m.cpu_write(0x8001, 0x33);
190 assert_eq!(m.cpu_read(0x8000), 3); assert_eq!(m.ppu_read(0x0000), 3);
192 }
193
194 #[test]
195 fn gxrom_bus_conflict_at_offset_zero() {
196 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); }
201}