1#![allow(
16 clippy::cast_possible_truncation,
17 clippy::cast_lossless,
18 clippy::missing_const_for_fn,
19 clippy::needless_pass_by_ref_mut,
20 clippy::manual_range_patterns,
21 clippy::match_same_arms,
22 clippy::too_many_arguments
23)]
24
25use crate::cartridge::Mirroring;
26use crate::mapper::{Mapper, MapperCaps, MapperError};
27use alloc::{boxed::Box, vec::Vec};
28use alloc::{format, vec};
29
30const CHR_BANK_4K: usize = 0x1000;
31const CHR_BANK_8K: usize = 0x2000;
32const NAMETABLE_SIZE: usize = 0x0400;
33const NAMETABLE_SIZE_U16: u16 = 0x0400;
34
35const M34_SECTION_VERSION: u8 = 2;
49
50fn nametable_offset(addr: u16, mirroring: Mirroring) -> usize {
51 let table = (((addr - 0x2000) / NAMETABLE_SIZE_U16) & 0x03) as u8;
52 let local = (addr as usize) & (NAMETABLE_SIZE - 1);
53 let physical = mirroring.physical_bank(table);
54 physical * NAMETABLE_SIZE + local
55}
56
57#[derive(Debug, Clone, Copy)]
59pub enum M34Variant {
60 Bnrom,
62 Nina001,
64}
65
66pub struct M34 {
68 prg_rom: Box<[u8]>,
69 chr: Box<[u8]>,
70 chr_is_ram: bool,
71 vram: Box<[u8]>,
72 prg_ram: Box<[u8]>,
73 prg_bank: u8,
74 chr_bank_lo: u8,
75 chr_bank_hi: u8,
76 variant: M34Variant,
77 mirroring: Mirroring,
78}
79
80impl M34 {
81 pub fn new(
87 prg_rom: Box<[u8]>,
88 chr_rom: Box<[u8]>,
89 mirroring: Mirroring,
90 variant: M34Variant,
91 ) -> Result<Self, MapperError> {
92 if prg_rom.is_empty() || !prg_rom.len().is_multiple_of(32 * 1024) {
93 return Err(MapperError::Invalid(format!(
94 "Mapper 34 PRG-ROM size {} is not a non-zero multiple of 32 KiB",
95 prg_rom.len()
96 )));
97 }
98 let chr_is_ram = chr_rom.is_empty();
99 let chr: Box<[u8]> = if chr_is_ram {
100 vec![0u8; CHR_BANK_8K].into_boxed_slice()
101 } else if chr_rom.len().is_multiple_of(CHR_BANK_4K) {
102 chr_rom
103 } else {
104 return Err(MapperError::Invalid(format!(
105 "Mapper 34 CHR-ROM size {} is not a multiple of 4 KiB",
106 chr_rom.len()
107 )));
108 };
109 Ok(Self {
110 prg_rom,
111 chr,
112 chr_is_ram,
113 vram: vec![0u8; 2 * NAMETABLE_SIZE].into_boxed_slice(),
114 prg_ram: vec![0u8; 8 * 1024].into_boxed_slice(),
115 prg_bank: 0,
116 chr_bank_lo: 0,
117 chr_bank_hi: 0,
118 variant,
119 mirroring,
120 })
121 }
122}
123
124impl Mapper for M34 {
125 fn has_hardwired_mirroring(&self) -> bool {
128 true
129 }
130
131 fn sram(&self) -> &[u8] {
132 &self.prg_ram
133 }
134 fn sram_mut(&mut self) -> &mut [u8] {
135 &mut self.prg_ram
136 }
137 fn caps(&self) -> MapperCaps {
140 MapperCaps::NONE
141 }
142
143 fn cpu_read(&mut self, addr: u16) -> u8 {
144 match addr {
145 0x6000..=0x7FFF => self.prg_ram[(addr - 0x6000) as usize % self.prg_ram.len()],
146 0x8000..=0xFFFF => {
147 let total_32k = (self.prg_rom.len() / (32 * 1024)).max(1);
148 let bank = (self.prg_bank as usize) % total_32k;
149 self.prg_rom[(bank * 32 * 1024 + (addr as usize - 0x8000)) % self.prg_rom.len()]
150 }
151 _ => 0,
152 }
153 }
154
155 fn cpu_write(&mut self, addr: u16, value: u8) {
156 match (self.variant, addr) {
157 (M34Variant::Nina001, 0x7FFD) => self.prg_bank = value & 0x01,
158 (M34Variant::Nina001, 0x7FFE) => self.chr_bank_lo = value & 0x0F,
159 (M34Variant::Nina001, 0x7FFF) => self.chr_bank_hi = value & 0x0F,
160 (_, 0x6000..=0x7FFF) => {
161 let off = (addr - 0x6000) as usize % self.prg_ram.len();
162 self.prg_ram[off] = value;
163 }
164 (M34Variant::Bnrom, 0x8000..=0xFFFF) => self.prg_bank = value,
165 _ => {}
166 }
167 }
168
169 fn ppu_read(&mut self, addr: u16) -> u8 {
170 let addr = addr & 0x3FFF;
171 match (addr, self.variant) {
172 (0x0000..=0x0FFF, M34Variant::Nina001) => {
173 let total_4k = (self.chr.len() / CHR_BANK_4K).max(1);
174 let bank = (self.chr_bank_lo as usize) % total_4k;
175 self.chr[(bank * CHR_BANK_4K + addr as usize) % self.chr.len()]
176 }
177 (0x1000..=0x1FFF, M34Variant::Nina001) => {
178 let total_4k = (self.chr.len() / CHR_BANK_4K).max(1);
179 let bank = (self.chr_bank_hi as usize) % total_4k;
180 self.chr[(bank * CHR_BANK_4K + (addr as usize - 0x1000)) % self.chr.len()]
181 }
182 (0x0000..=0x1FFF, _) => self.chr[addr as usize % self.chr.len()],
183 (0x2000..=0x3EFF, _) => {
184 self.vram[nametable_offset(addr, self.mirroring) % self.vram.len()]
185 }
186 _ => 0,
187 }
188 }
189
190 fn ppu_write(&mut self, addr: u16, value: u8) {
191 let addr = addr & 0x3FFF;
192 match addr {
193 0x0000..=0x1FFF => {
194 if self.chr_is_ram {
195 let len = self.chr.len();
196 self.chr[addr as usize % len] = value;
197 }
198 }
199 0x2000..=0x3EFF => {
200 let off = nametable_offset(addr, self.mirroring) % self.vram.len();
201 self.vram[off] = value;
202 }
203 _ => {}
204 }
205 }
206
207 fn current_mirroring(&self) -> Mirroring {
208 self.mirroring
209 }
210
211 fn save_state(&self) -> Vec<u8> {
212 let mut out =
213 Vec::with_capacity(8 + self.prg_ram.len() + self.vram.len() + self.ram_block_len());
214 out.push(M34_SECTION_VERSION);
215 out.push(self.prg_bank);
216 out.push(self.chr_bank_lo);
217 out.push(self.chr_bank_hi);
218 out.push(match self.variant {
219 M34Variant::Bnrom => 0,
220 M34Variant::Nina001 => 1,
221 });
222 out.extend_from_slice(&self.prg_ram);
223 out.extend_from_slice(&self.vram);
224 if self.chr_is_ram {
226 out.extend_from_slice(&self.chr);
227 }
228 out
229 }
230
231 fn load_state(&mut self, data: &[u8]) -> Result<(), MapperError> {
232 let version = data.first().copied().unwrap_or(0);
233 if version != M34_SECTION_VERSION {
237 return Err(MapperError::UnsupportedVersion(version));
238 }
239 let ram_len = self.ram_block_len();
240 let core_len = 5 + self.prg_ram.len() + self.vram.len();
242 let expected = core_len + ram_len;
243 if data.len() != expected {
244 return Err(MapperError::WrongLength {
245 expected,
246 got: data.len(),
247 });
248 }
249 self.prg_bank = data[1];
250 self.chr_bank_lo = data[2];
251 self.chr_bank_hi = data[3];
252 self.variant = match data[4] {
253 0 => M34Variant::Bnrom,
254 1 => M34Variant::Nina001,
255 other => return Err(MapperError::Invalid(format!("variant {other}"))),
256 };
257 let mut cur = 5usize;
258 self.prg_ram
259 .copy_from_slice(&data[cur..cur + self.prg_ram.len()]);
260 cur += self.prg_ram.len();
261 self.vram.copy_from_slice(&data[cur..core_len]);
262 if self.chr_is_ram {
263 self.chr.copy_from_slice(&data[core_len..]);
264 }
265 Ok(())
266 }
267}
268
269impl M34 {
270 fn ram_block_len(&self) -> usize {
274 if self.chr_is_ram { self.chr.len() } else { 0 }
275 }
276}
277
278#[cfg(test)]
279mod tests {
280 use super::*;
281
282 const PRG_BANK_8K: usize = 0x2000;
283
284 fn synth(banks_8k: usize) -> Box<[u8]> {
285 let mut v = vec![0u8; banks_8k * PRG_BANK_8K];
286 for b in 0..banks_8k {
287 v[b * PRG_BANK_8K] = b as u8;
288 }
289 v.into_boxed_slice()
290 }
291
292 fn synth_chr_4k(banks: usize) -> Box<[u8]> {
293 let mut v = vec![0u8; banks * CHR_BANK_4K];
294 for b in 0..banks {
295 v[b * CHR_BANK_4K] = b as u8;
296 }
297 v.into_boxed_slice()
298 }
299
300 #[test]
301 fn m34_bnrom_swap() {
302 let mut m = M34::new(
303 synth(8),
304 Box::new([]),
305 Mirroring::Vertical,
306 M34Variant::Bnrom,
307 )
308 .unwrap();
309 assert_eq!(m.cpu_read(0x8000), 0);
311 m.cpu_write(0x8000, 1);
313 assert_eq!(m.cpu_read(0x8000), 4);
315 }
316
317 #[test]
318 fn m34_nina001_variant_register_layout() {
319 let mut m = M34::new(
323 synth(8),
324 synth_chr_4k(8),
325 Mirroring::Vertical,
326 M34Variant::Nina001,
327 )
328 .unwrap();
329 m.cpu_write(0x7FFD, 1);
331 assert_eq!(m.cpu_read(0x8000), 4, "NINA-001 PRG bank selects via $7FFD");
332 m.cpu_write(0x8000, 0);
334 assert_eq!(m.cpu_read(0x8000), 4, "$8000 write is ignored on NINA-001");
335 m.cpu_write(0x7FFE, 2);
337 assert_eq!(m.ppu_read(0x0000), 2, "NINA-001 CHR lo bank via $7FFE");
338 m.cpu_write(0x7FFF, 3);
339 assert_eq!(m.ppu_read(0x1000), 3, "NINA-001 CHR hi bank via $7FFF");
340 }
341
342 #[test]
346 fn m34_save_state_carries_chr_ram() {
347 let mut m = M34::new(
348 synth(8),
349 Box::new([]),
350 Mirroring::Vertical,
351 M34Variant::Bnrom,
352 )
353 .unwrap();
354 m.chr[0x0000] = 0x11;
355 m.chr[0x1FFF] = 0x22;
356 let blob = m.save_state();
357 let mut m2 = M34::new(
358 synth(8),
359 Box::new([]),
360 Mirroring::Vertical,
361 M34Variant::Bnrom,
362 )
363 .unwrap();
364 m2.load_state(&blob).expect("round-trip");
365 assert_eq!(m2.chr[0x0000], 0x11);
366 assert_eq!(m2.chr[0x1FFF], 0x22);
367 }
368
369 #[test]
372 fn m34_v1_blob_is_refused() {
373 let mut m = M34::new(
374 synth(8),
375 Box::new([]),
376 Mirroring::Vertical,
377 M34Variant::Bnrom,
378 )
379 .unwrap();
380 m.prg_bank = 1;
381 let core_len = 5 + m.prg_ram.len() + m.vram.len();
382 let mut v1 = m.save_state()[..core_len].to_vec();
383 v1[0] = 1;
384 let mut m2 = M34::new(
385 synth(8),
386 Box::new([]),
387 Mirroring::Vertical,
388 M34Variant::Bnrom,
389 )
390 .unwrap();
391 assert!(matches!(
392 m2.load_state(&v1),
393 Err(MapperError::UnsupportedVersion(1))
394 ));
395 }
396
397 #[test]
399 fn m34_truncated_chr_ram_tail_is_rejected() {
400 let m = M34::new(
401 synth(8),
402 Box::new([]),
403 Mirroring::Vertical,
404 M34Variant::Bnrom,
405 )
406 .unwrap();
407 let blob = m.save_state();
408 let mut m2 = M34::new(
409 synth(8),
410 Box::new([]),
411 Mirroring::Vertical,
412 M34Variant::Bnrom,
413 )
414 .unwrap();
415 let err = m2
416 .load_state(&blob[..blob.len() - 1])
417 .expect_err("a truncated v2 blob must be rejected");
418 assert!(matches!(err, MapperError::WrongLength { .. }), "{err:?}");
419 }
420}