rustynes_mappers/
m007_axrom.rs1use 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
23pub 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 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 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 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); assert_eq!(m.cpu_read(0x8000), 2);
205 assert_eq!(m.current_mirroring(), Mirroring::SingleScreenB);
206 m.cpu_write(0x8000, 0b0000_0001); 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}