rustynes_mappers/
m241_bxrom241.rs1use crate::cartridge::Mirroring;
17use crate::mapper::{Mapper, MapperCaps, MapperError};
18use alloc::{boxed::Box, vec::Vec};
19use alloc::{format, vec};
20
21const PRG_BANK_32K: usize = 0x8000;
22const CHR_BANK_8K: usize = 0x2000;
23const NAMETABLE_SIZE: usize = 0x0400;
24const NAMETABLE_SIZE_U16: u16 = 0x0400;
25
26const SAVE_STATE_VERSION: u8 = 2;
29const WRAM_SIZE: usize = 0x2000;
30
31const fn nametable_offset(addr: u16, mirroring: Mirroring) -> usize {
32 let table = (((addr - 0x2000) / NAMETABLE_SIZE_U16) & 0x03) as u8;
33 let local = (addr as usize) & (NAMETABLE_SIZE - 1);
34 let physical = mirroring.physical_bank(table);
35 physical * NAMETABLE_SIZE + local
36}
37
38pub struct Bxrom241 {
40 prg_rom: Box<[u8]>,
41 chr: Box<[u8]>,
42 vram: Box<[u8]>,
43 chr_is_ram: bool,
44 prg_bank: u8,
45 mirroring: Mirroring,
46 wram: Box<[u8]>,
49 battery: bool,
51}
52
53impl Bxrom241 {
54 pub fn new(
61 prg_rom: Box<[u8]>,
62 chr_rom: Box<[u8]>,
63 mirroring: Mirroring,
64 ) -> Result<Self, MapperError> {
65 if prg_rom.is_empty() || !prg_rom.len().is_multiple_of(PRG_BANK_32K) {
66 return Err(MapperError::Invalid(format!(
67 "mapper 241 PRG-ROM size {} is not a non-zero multiple of 32 KiB",
68 prg_rom.len()
69 )));
70 }
71 let chr_is_ram = chr_rom.is_empty();
72 let chr: Box<[u8]> = if chr_is_ram {
73 vec![0u8; CHR_BANK_8K].into_boxed_slice()
74 } else if chr_rom.len() == CHR_BANK_8K {
75 chr_rom
76 } else {
77 return Err(MapperError::Invalid(format!(
78 "mapper 241 expects 8 KiB CHR (RAM or ROM); got {} bytes",
79 chr_rom.len()
80 )));
81 };
82 Ok(Self {
83 prg_rom,
84 chr,
85 vram: vec![0u8; 2 * NAMETABLE_SIZE].into_boxed_slice(),
86 chr_is_ram,
87 prg_bank: 0,
88 mirroring,
89 wram: vec![0u8; WRAM_SIZE].into_boxed_slice(),
90 battery: false,
91 })
92 }
93
94 #[must_use]
96 pub const fn with_battery(mut self, battery: bool) -> Self {
97 self.battery = battery;
98 self
99 }
100}
101
102impl Mapper for Bxrom241 {
103 fn caps(&self) -> MapperCaps {
104 MapperCaps::NONE
105 }
106
107 fn sram(&self) -> &[u8] {
108 if self.battery { &self.wram } else { &[] }
109 }
110 fn sram_mut(&mut self) -> &mut [u8] {
111 if self.battery {
112 &mut self.wram
113 } else {
114 &mut []
115 }
116 }
117
118 fn cpu_read_unmapped(&self, addr: u16) -> bool {
121 (0x4020..=0x5FFF).contains(&addr)
122 }
123
124 fn cpu_read(&mut self, addr: u16) -> u8 {
125 if (0x6000..=0x7FFF).contains(&addr) {
126 return self.wram[usize::from(addr - 0x6000)];
127 }
128 if (0x8000..=0xFFFF).contains(&addr) {
129 let count = (self.prg_rom.len() / PRG_BANK_32K).max(1);
130 let bank = (self.prg_bank as usize) % count;
131 self.prg_rom[bank * PRG_BANK_32K + (addr as usize - 0x8000)]
132 } else {
133 0
134 }
135 }
136
137 fn cpu_write(&mut self, addr: u16, value: u8) {
138 if (0x6000..=0x7FFF).contains(&addr) {
139 self.wram[usize::from(addr - 0x6000)] = value;
140 return;
141 }
142 if (0x8000..=0xFFFF).contains(&addr) {
143 self.prg_bank = value;
144 }
145 }
146
147 fn ppu_read(&mut self, addr: u16) -> u8 {
148 let addr = addr & 0x3FFF;
149 match addr {
150 0x0000..=0x1FFF => self.chr[addr as usize],
151 0x2000..=0x3EFF => self.vram[nametable_offset(addr, self.mirroring)],
152 _ => 0,
153 }
154 }
155
156 fn ppu_write(&mut self, addr: u16, value: u8) {
157 let addr = addr & 0x3FFF;
158 match addr {
159 0x0000..=0x1FFF => {
160 if self.chr_is_ram {
161 self.chr[addr as usize] = value;
162 }
163 }
164 0x2000..=0x3EFF => {
165 let off = nametable_offset(addr, self.mirroring);
166 self.vram[off] = value;
167 }
168 _ => {}
169 }
170 }
171
172 fn current_mirroring(&self) -> Mirroring {
173 self.mirroring
174 }
175
176 fn save_state(&self) -> Vec<u8> {
177 let chr_extra = if self.chr_is_ram { self.chr.len() } else { 0 };
178 let mut out = Vec::with_capacity(2 + self.vram.len() + chr_extra);
179 out.push(SAVE_STATE_VERSION);
180 out.push(self.prg_bank);
181 out.extend_from_slice(&self.vram);
182 if self.chr_is_ram {
183 out.extend_from_slice(&self.chr);
184 }
185 out.extend_from_slice(&self.wram);
186 out
187 }
188
189 fn load_state(&mut self, data: &[u8]) -> Result<(), MapperError> {
190 let chr_extra = if self.chr_is_ram { self.chr.len() } else { 0 };
191 let version = *data.first().ok_or(MapperError::WrongLength {
192 expected: 1,
193 got: 0,
194 })?;
195 if version != SAVE_STATE_VERSION {
196 return Err(MapperError::UnsupportedVersion(version));
197 }
198 let wram_len = self.wram.len();
199 let expected = 2 + self.vram.len() + chr_extra + wram_len;
200 if data.len() != expected {
201 return Err(MapperError::WrongLength {
202 expected,
203 got: data.len(),
204 });
205 }
206 self.prg_bank = data[1];
207 let mut cursor = 2;
208 self.vram
209 .copy_from_slice(&data[cursor..cursor + self.vram.len()]);
210 cursor += self.vram.len();
211 if self.chr_is_ram {
212 self.chr
213 .copy_from_slice(&data[cursor..cursor + self.chr.len()]);
214 cursor += self.chr.len();
215 }
216 self.wram.copy_from_slice(&data[cursor..cursor + wram_len]);
217 Ok(())
218 }
219}
220
221#[cfg(test)]
222#[allow(clippy::cast_possible_truncation)]
223mod tests {
224 use super::*;
225
226 fn synth_prg_32k(banks: usize) -> Box<[u8]> {
227 let mut v = vec![0xFFu8; banks * PRG_BANK_32K];
228 for b in 0..banks {
229 v[b * PRG_BANK_32K] = b as u8;
230 }
231 v.into_boxed_slice()
232 }
233
234 #[test]
235 fn m241_full_byte_selects_32k_prg() {
236 let mut m = Bxrom241::new(synth_prg_32k(8), Box::new([]), Mirroring::Vertical).unwrap();
237 m.cpu_write(0x8000, 5);
238 assert_eq!(m.cpu_read(0x8000), 5);
239 m.cpu_write(0xFFFF, 3);
242 assert_eq!(m.cpu_read(0x8000), 3);
243 }
244
245 #[test]
246 fn m241_chr_ram_round_trip() {
247 let mut m = Bxrom241::new(synth_prg_32k(2), Box::new([]), Mirroring::Vertical).unwrap();
248 m.ppu_write(0x0010, 0xAB);
249 assert_eq!(m.ppu_read(0x0010), 0xAB);
250 }
251}