1#![allow(
22 clippy::cast_possible_truncation,
23 clippy::cast_lossless,
24 clippy::match_same_arms,
25 clippy::doc_markdown,
26 clippy::similar_names,
27 clippy::too_many_lines,
28 clippy::missing_const_for_fn,
29 clippy::struct_excessive_bools,
30 clippy::bool_to_int_with_if,
31 clippy::unreadable_literal
32)]
33
34use crate::cartridge::Mirroring;
35use crate::mapper::{Mapper, MapperCaps, MapperError};
36use alloc::{boxed::Box, format, vec, vec::Vec};
37
38const PRG_BANK_8K: usize = 0x2000;
39const PRG_BANK_16K: usize = 0x4000;
40const PRG_BANK_32K: usize = 0x8000;
41const CHR_BANK_2K: usize = 0x0800;
42const CHR_BANK_8K: usize = 0x2000;
43const NAMETABLE_SIZE: usize = 0x0400;
44const NAMETABLE_SIZE_U16: u16 = 0x0400;
45
46const SAVE_STATE_VERSION: u8 = 1;
47
48const fn nametable_offset(addr: u16, mirroring: Mirroring) -> usize {
53 let table = (((addr - 0x2000) / NAMETABLE_SIZE_U16) & 0x03) as u8;
54 let local = (addr as usize) & (NAMETABLE_SIZE - 1);
55 let physical = mirroring.physical_bank(table);
56 physical * NAMETABLE_SIZE + local
57}
58
59const fn mirroring_to_byte(m: Mirroring) -> u8 {
60 match m {
61 Mirroring::Horizontal => 0,
62 Mirroring::Vertical => 1,
63 Mirroring::SingleScreenA => 2,
64 Mirroring::SingleScreenB => 3,
65 Mirroring::FourScreen => 4,
66 Mirroring::MapperControlled => 5,
67 }
68}
69
70const fn byte_to_mirroring(b: u8, fallback: Mirroring) -> Mirroring {
71 match b {
72 0 => Mirroring::Horizontal,
73 1 => Mirroring::Vertical,
74 2 => Mirroring::SingleScreenA,
75 3 => Mirroring::SingleScreenB,
76 4 => Mirroring::FourScreen,
77 5 => Mirroring::MapperControlled,
78 _ => fallback,
79 }
80}
81
82fn check_prg(prg: &[u8], id: u16) -> Result<(), MapperError> {
84 if prg.is_empty() || !prg.len().is_multiple_of(PRG_BANK_8K) {
85 return Err(MapperError::Invalid(format!(
86 "mapper {id} PRG-ROM size {} is not a non-zero multiple of 8 KiB",
87 prg.len()
88 )));
89 }
90 Ok(())
91}
92
93#[derive(Debug, Clone, Copy, PartialEq, Eq)]
95pub enum SimpleBoard {
96 M164,
98 M261,
100 M289,
102 M320,
104 M336,
106 M349,
108 M286,
110}
111
112pub struct SimpleBmc {
114 board: SimpleBoard,
115 prg_rom: Box<[u8]>,
116 chr: Box<[u8]>,
117 chr_is_ram: bool,
118 vram: Box<[u8]>,
119 mirroring: Mirroring,
120 prg0: usize,
122 prg1: usize,
123 chr8: usize,
125 chr2: [usize; 4],
127 prg8: [usize; 4],
129 reg_inner: u8,
131 reg_outer: u8,
132 reg_mode: u8,
133 dip: u8,
134}
135
136impl SimpleBmc {
137 const SAVE_LEN: usize = 4 + 8 + 8 + 4 + 1;
138
139 fn new(
140 board: SimpleBoard,
141 prg_rom: Box<[u8]>,
142 chr_rom: Box<[u8]>,
143 mirroring: Mirroring,
144 id: u16,
145 ) -> Result<Self, MapperError> {
146 check_prg(&prg_rom, id)?;
147 let chr_is_ram = chr_rom.is_empty();
148 let chr: Box<[u8]> = if chr_is_ram {
149 vec![0u8; CHR_BANK_8K].into_boxed_slice()
150 } else {
151 chr_rom
152 };
153 let mut m = Self {
154 board,
155 prg_rom,
156 chr,
157 chr_is_ram,
158 vram: vec![0u8; 2 * NAMETABLE_SIZE].into_boxed_slice(),
159 mirroring,
160 prg0: 0,
161 prg1: 0,
162 chr8: 0,
163 chr2: [0; 4],
164 prg8: [0; 4],
165 reg_inner: 0,
166 reg_outer: 0,
167 reg_mode: 0,
168 dip: 0,
169 };
170 m.reset_banks();
171 Ok(m)
172 }
173
174 fn prg_count_16k(&self) -> usize {
175 (self.prg_rom.len() / PRG_BANK_16K).max(1)
176 }
177
178 fn prg_count_8k(&self) -> usize {
179 (self.prg_rom.len() / PRG_BANK_8K).max(1)
180 }
181
182 fn chr_count_8k(&self) -> usize {
183 (self.chr.len() / CHR_BANK_8K).max(1)
184 }
185
186 fn reset_banks(&mut self) {
188 let last16 = self.prg_count_16k() - 1;
189 match self.board {
190 SimpleBoard::M164 => {
191 self.reg_inner = 0x0F;
193 self.update_m164();
194 }
195 SimpleBoard::M286 => {
196 let last8 = self.prg_count_8k() - 1;
197 let last_chr2 = (self.chr.len() / CHR_BANK_2K).max(1) - 1;
198 self.prg8.fill(last8);
199 self.chr2.fill(last_chr2);
200 }
201 SimpleBoard::M320 => self.update_m320(),
202 SimpleBoard::M289 => self.update_m289(),
203 _ => {
204 self.prg0 = 0;
205 self.prg1 = last16;
206 }
207 }
208 }
209
210 fn update_m164(&mut self) {
211 let count32 = (self.prg_rom.len() / PRG_BANK_32K).max(1);
213 let bank32 = (self.reg_inner as usize) % count32;
214 self.prg0 = bank32 * 2;
215 self.prg1 = bank32 * 2 + 1;
216 }
217
218 fn update_m289(&mut self) {
219 let page = self.reg_outer as usize
220 | (if self.reg_mode & 0x04 != 0 {
221 0
222 } else {
223 self.reg_inner as usize
224 });
225 match self.reg_mode & 0x03 {
226 0 => {
227 self.prg0 = page;
228 self.prg1 = page;
229 }
230 1 => {
231 let b = page & 0xFE;
232 self.prg0 = b;
233 self.prg1 = b | 1;
234 }
235 2 => {
236 self.prg0 = page;
237 self.prg1 = self.reg_outer as usize | 7;
238 }
239 _ => {}
240 }
241 self.mirroring = if self.reg_mode & 0x08 != 0 {
242 Mirroring::Horizontal
243 } else {
244 Mirroring::Vertical
245 };
246 }
247
248 fn update_m320(&mut self) {
249 let outer = (self.reg_outer as usize) << 3;
250 if self.reg_mode != 0 {
251 self.prg0 = (self.reg_inner as usize & 0x07) | outer;
253 self.prg1 = 0x07 | outer;
254 } else {
255 self.prg0 = (self.reg_inner as usize) | outer;
257 self.prg1 = 0x0F | outer;
258 }
259 }
260
261 fn prg_byte(&self, slot16: usize, addr: u16) -> u8 {
262 let count = self.prg_count_16k();
263 let bank = slot16 % count;
264 self.prg_rom[bank * PRG_BANK_16K + (addr as usize & 0x3FFF)]
265 }
266}
267
268impl Mapper for SimpleBmc {
269 fn caps(&self) -> MapperCaps {
270 MapperCaps::NONE
271 }
272
273 fn cpu_read(&mut self, addr: u16) -> u8 {
274 if self.board == SimpleBoard::M286 {
275 if let 0x8000..=0xFFFF = addr {
276 let slot = (addr as usize - 0x8000) / PRG_BANK_8K;
277 let count = self.prg_count_8k();
278 let bank = self.prg8[slot] % count;
279 return self.prg_rom[bank * PRG_BANK_8K + (addr as usize & 0x1FFF)];
280 }
281 return 0;
282 }
283 match addr {
284 0x8000..=0xBFFF => self.prg_byte(self.prg0, addr),
285 0xC000..=0xFFFF => self.prg_byte(self.prg1, addr),
286 _ => 0,
287 }
288 }
289
290 fn cpu_write(&mut self, addr: u16, value: u8) {
291 match self.board {
292 SimpleBoard::M164 => {
293 if (0x5000..=0x5FFF).contains(&addr) {
294 match addr & 0x7300 {
295 0x5000 => self.reg_inner = (self.reg_inner & 0xF0) | (value & 0x0F),
296 0x5100 => self.reg_inner = (self.reg_inner & 0x0F) | ((value & 0x0F) << 4),
297 _ => {}
298 }
299 self.update_m164();
300 }
301 }
302 SimpleBoard::M261 => {
303 if addr >= 0x8000 {
304 let bank = ((addr >> 6) & 0xFFFE) as usize;
305 if addr & 0x40 != 0 {
306 self.prg0 = bank;
307 self.prg1 = bank | 1;
308 } else {
309 let b = bank | ((addr >> 5) & 0x01) as usize;
310 self.prg0 = b;
311 self.prg1 = b;
312 }
313 self.chr8 = (addr & 0x0F) as usize;
314 self.mirroring = if addr & 0x10 != 0 {
315 Mirroring::Horizontal
316 } else {
317 Mirroring::Vertical
318 };
319 }
320 }
321 SimpleBoard::M289 => {
322 if addr >= 0x8000 {
323 self.reg_inner = value & 0x07;
324 } else {
325 match addr & 0xE001 {
326 0x6000 => self.reg_mode = value & 0x0F,
327 0x6001 => self.reg_outer = value,
328 _ => {}
329 }
330 }
331 self.update_m289();
332 }
333 SimpleBoard::M320 => {
334 if addr >= 0x8000 {
335 self.reg_inner = value & 0x0F;
336 if addr & 0xFFE0 == 0xF0E0 {
337 self.reg_outer = (addr & 0x0F) as u8;
338 self.reg_mode = ((addr >> 4) & 0x01) as u8;
339 }
340 self.update_m320();
341 }
342 }
343 SimpleBoard::M336 => {
344 if addr >= 0x8000 {
345 let inner = value as usize & 0x07;
346 let outer = value as usize & 0x38;
347 self.prg0 = outer | inner;
348 self.prg1 = outer | 7;
349 }
350 }
351 SimpleBoard::M349 => {
352 if addr >= 0x8000 {
353 let a = addr as usize;
354 if a & 0x800 != 0 {
355 self.prg0 = (a & 0x1F) | (a & ((a & 0x40) >> 6));
356 self.prg1 = (a & 0x18) | 0x07;
357 } else if a & 0x40 != 0 {
358 self.prg0 = a & 0x1F;
359 self.prg1 = a & 0x1F;
360 } else {
361 let b = a & 0x1E;
362 self.prg0 = b;
363 self.prg1 = b | 1;
364 }
365 self.mirroring = if a & 0x80 != 0 {
366 Mirroring::Horizontal
367 } else {
368 Mirroring::Vertical
369 };
370 }
371 }
372 SimpleBoard::M286 => {
373 let bank = ((addr >> 10) & 0x03) as usize;
374 match addr & 0xF000 {
375 0x8000 => self.chr2[bank] = (addr & 0x1F) as usize,
376 0xA000 if addr & (1u16 << (self.dip + 4)) != 0 => {
377 self.prg8[bank] = (addr & 0x0F) as usize;
378 }
379 _ => {}
380 }
381 }
382 }
383 }
384
385 fn ppu_read(&mut self, addr: u16) -> u8 {
386 let addr = addr & 0x3FFF;
387 match addr {
388 0x0000..=0x1FFF => {
389 if self.chr_is_ram {
390 return self.chr[addr as usize & (CHR_BANK_8K - 1)];
391 }
392 if self.board == SimpleBoard::M286 {
393 let slot = (addr as usize) / CHR_BANK_2K;
394 let count = (self.chr.len() / CHR_BANK_2K).max(1);
395 let bank = self.chr2[slot] % count;
396 return self.chr[bank * CHR_BANK_2K + (addr as usize & (CHR_BANK_2K - 1))];
397 }
398 let bank = self.chr8 % self.chr_count_8k();
399 self.chr[bank * CHR_BANK_8K + (addr as usize & 0x1FFF)]
400 }
401 0x2000..=0x3EFF => self.vram[nametable_offset(addr, self.mirroring)],
402 _ => 0,
403 }
404 }
405
406 fn ppu_write(&mut self, addr: u16, value: u8) {
407 let addr = addr & 0x3FFF;
408 match addr {
409 0x0000..=0x1FFF if self.chr_is_ram => {
410 self.chr[addr as usize & (CHR_BANK_8K - 1)] = value;
411 }
412 0x2000..=0x3EFF => {
413 let off = nametable_offset(addr, self.mirroring);
414 self.vram[off] = value;
415 }
416 _ => {}
417 }
418 }
419
420 fn current_mirroring(&self) -> Mirroring {
421 self.mirroring
422 }
423
424 fn save_state(&self) -> Vec<u8> {
425 let chr_ram = if self.chr_is_ram { self.chr.len() } else { 0 };
426 let mut out = Vec::with_capacity(1 + Self::SAVE_LEN + self.vram.len() + chr_ram);
427 out.push(SAVE_STATE_VERSION);
428 out.extend_from_slice(&(self.prg0 as u32).to_le_bytes());
429 out.extend_from_slice(&(self.prg1 as u32).to_le_bytes());
430 out.extend_from_slice(&(self.chr8 as u32).to_le_bytes());
431 for c in &self.chr2 {
432 out.extend_from_slice(&(*c as u32).to_le_bytes());
433 }
434 for p in &self.prg8 {
435 out.extend_from_slice(&(*p as u32).to_le_bytes());
436 }
437 out.push(self.reg_inner);
438 out.push(self.reg_outer);
439 out.push(self.reg_mode);
440 out.push(self.dip);
441 out.push(mirroring_to_byte(self.mirroring));
442 out.extend_from_slice(&self.vram);
443 if self.chr_is_ram {
444 out.extend_from_slice(&self.chr);
445 }
446 out
447 }
448
449 fn load_state(&mut self, data: &[u8]) -> Result<(), MapperError> {
450 let chr_ram = if self.chr_is_ram { self.chr.len() } else { 0 };
451 let scratch = 1 + 12 + 16 + 16 + 4 + 1;
453 let expected = scratch + self.vram.len() + chr_ram;
454 if data.len() != expected {
455 return Err(MapperError::WrongLength {
456 expected,
457 got: data.len(),
458 });
459 }
460 if data[0] != SAVE_STATE_VERSION {
461 return Err(MapperError::UnsupportedVersion(data[0]));
462 }
463 let dip = data[scratch - 2];
470 if dip > 3 {
471 return Err(MapperError::Invalid(format!(
472 "mapper state DIP setting {dip} is not one of the board's four (0-3)"
473 )));
474 }
475 let rd = |c: usize| {
476 u32::from_le_bytes([data[c], data[c + 1], data[c + 2], data[c + 3]]) as usize
477 };
478 let mut c = 1;
479 self.prg0 = rd(c);
480 self.prg1 = rd(c + 4);
481 self.chr8 = rd(c + 8);
482 c += 12;
483 for s in &mut self.chr2 {
484 *s = rd(c);
485 c += 4;
486 }
487 for s in &mut self.prg8 {
488 *s = rd(c);
489 c += 4;
490 }
491 self.reg_inner = data[c];
492 self.reg_outer = data[c + 1];
493 self.reg_mode = data[c + 2];
494 self.dip = data[c + 3];
495 self.mirroring = byte_to_mirroring(data[c + 4], self.mirroring);
496 c += 5;
497 self.vram.copy_from_slice(&data[c..c + self.vram.len()]);
498 c += self.vram.len();
499 if self.chr_is_ram {
500 self.chr.copy_from_slice(&data[c..c + self.chr.len()]);
501 }
502 Ok(())
503 }
504}
505
506macro_rules! simple_ctor {
507 ($fn_name:ident, $board:expr, $id:expr, $doc:expr) => {
508 #[doc = $doc]
509 pub fn $fn_name(
513 prg_rom: Box<[u8]>,
514 chr_rom: Box<[u8]>,
515 mirroring: Mirroring,
516 ) -> Result<SimpleBmc, MapperError> {
517 SimpleBmc::new($board, prg_rom, chr_rom, mirroring, $id)
518 }
519 };
520}
521
522simple_ctor!(
523 new_m164,
524 SimpleBoard::M164,
525 164,
526 "Mapper 164 (Waixing Final Fantasy V, 32 KiB-PRG split register)."
527);
528simple_ctor!(
529 new_m261,
530 SimpleBoard::M261,
531 261,
532 "Mapper 261 (BMC-810544-C-A1 address-as-data multicart)."
533);
534simple_ctor!(
535 new_m289,
536 SimpleBoard::M289,
537 289,
538 "Mapper 289 (BMC-60311C NROM/UNROM multicart)."
539);
540simple_ctor!(
541 new_m320,
542 SimpleBoard::M320,
543 320,
544 "Mapper 320 (BMC-830425C-4391T UNROM/UOROM multicart)."
545);
546simple_ctor!(
547 new_m336,
548 SimpleBoard::M336,
549 336,
550 "Mapper 336 (BMC-K-3046 UNROM-style multicart)."
551);
552simple_ctor!(
553 new_m349,
554 SimpleBoard::M349,
555 349,
556 "Mapper 349 (BMC-G-146 NROM/UNROM/NROM-256 multicart)."
557);
558simple_ctor!(
559 new_m286,
560 SimpleBoard::M286,
561 286,
562 "Mapper 286 (Waixing BS-5 Olympic multicart)."
563);
564
565#[cfg(test)]
573#[allow(clippy::cast_possible_truncation)]
574mod tests {
575 use super::*;
576
577 fn synth_prg_8k(banks: usize) -> Box<[u8]> {
578 let mut v = vec![0xFFu8; banks * PRG_BANK_8K];
579 for b in 0..banks {
580 v[b * PRG_BANK_8K] = b as u8;
581 }
582 v.into_boxed_slice()
583 }
584
585 fn synth_prg_16k(banks: usize) -> Box<[u8]> {
586 let mut v = vec![0xFFu8; banks * PRG_BANK_16K];
587 for b in 0..banks {
588 v[b * PRG_BANK_16K] = b as u8;
589 }
590 v.into_boxed_slice()
591 }
592
593 fn synth_chr_8k(banks: usize) -> Box<[u8]> {
594 let mut v = vec![0u8; banks * CHR_BANK_8K];
595 for b in 0..banks {
596 v[b * CHR_BANK_8K] = b as u8;
597 }
598 v.into_boxed_slice()
599 }
600
601 fn synth_chr_2k(banks: usize) -> Box<[u8]> {
602 let mut v = vec![0u8; banks * CHR_BANK_2K];
603 for b in 0..banks {
604 v[b * CHR_BANK_2K] = b as u8;
605 }
606 v.into_boxed_slice()
607 }
608
609 #[test]
610 fn waixing164_split_prg_register() {
611 let mut m = new_m164(synth_prg_16k(16), synth_chr_8k(1), Mirroring::Vertical).unwrap();
612 m.cpu_write(0x5000, 0x02); m.cpu_write(0x5100, 0x00); assert_eq!(m.cpu_read(0x8000), 4);
616 assert_eq!(m.cpu_read(0xC000), 5);
617 }
618
619 #[test]
620 fn bmc_simple_save_state_round_trip() {
621 let mut m = new_m164(synth_prg_16k(16), Box::new([]), Mirroring::Vertical).unwrap();
622 m.cpu_write(0x5000, 0x03);
623 m.ppu_write(0x0007, 0x2B);
624 let blob = m.save_state();
625 let mut m2 = new_m164(synth_prg_16k(16), Box::new([]), Mirroring::Vertical).unwrap();
626 m2.load_state(&blob).unwrap();
627 assert_eq!(m2.cpu_read(0x8000), m.cpu_read(0x8000));
628 assert_eq!(m2.ppu_read(0x0007), 0x2B);
629 }
630
631 #[test]
632 fn bmc810544_address_decode() {
633 let mut m = new_m261(synth_prg_16k(32), synth_chr_8k(16), Mirroring::Vertical).unwrap();
634 m.cpu_write(0x8040 | (4 << 6), 0); m.cpu_write(0x8000 | (2 << 6) | 0x40, 0); let lo = m.cpu_read(0x8000);
639 let hi = m.cpu_read(0xC000);
640 assert_eq!(hi, lo.wrapping_add(1)); }
642
643 #[test]
644 fn bmc60311_nrom_mode() {
645 let mut m = new_m289(synth_prg_16k(8), synth_chr_8k(1), Mirroring::Vertical).unwrap();
646 m.cpu_write(0x6001, 0x02); m.cpu_write(0x6000, 0x00); m.cpu_write(0x8000, 0x01); assert_eq!(m.cpu_read(0x8000), 3);
650 assert_eq!(m.cpu_read(0xC000), 3); }
652
653 #[test]
654 fn bmc830425_unrom_mode() {
655 let mut m = new_m320(synth_prg_16k(32), synth_chr_8k(1), Mirroring::Vertical).unwrap();
656 m.cpu_write(0xF0E0 | 0x10 | 0x02, 0x03); assert_eq!(m.cpu_read(0x8000), 19);
659 assert_eq!(m.cpu_read(0xC000), 23);
660 }
661
662 #[test]
663 fn bmc_k3046_unrom() {
664 let mut m = new_m336(synth_prg_16k(16), synth_chr_8k(1), Mirroring::Vertical).unwrap();
665 m.cpu_write(0x8000, 0x0A); assert_eq!(m.cpu_read(0x8000), 10);
667 assert_eq!(m.cpu_read(0xC000), 15);
668 }
669
670 #[test]
671 fn bmc_g146_32k_mode() {
672 let mut m = new_m349(synth_prg_16k(32), synth_chr_8k(1), Mirroring::Vertical).unwrap();
673 m.cpu_write(0x8000 | 0x04, 0); assert_eq!(m.cpu_read(0x8000), 4);
676 assert_eq!(m.cpu_read(0xC000), 5);
677 }
678
679 #[test]
680 fn bs5_chr_bank_decode() {
681 let mut m = new_m286(synth_prg_8k(16), synth_chr_2k(16), Mirroring::Vertical).unwrap();
682 m.cpu_write(0x8000 | (1 << 10) | 0x05, 0); assert_eq!(m.ppu_read(0x0800), 5); }
686
687 #[test]
688 fn bs5_save_state_round_trip() {
689 let mut m = new_m286(synth_prg_8k(16), synth_chr_2k(16), Mirroring::Vertical).unwrap();
690 m.cpu_write(0x8000 | 0x03, 0);
691 let blob = m.save_state();
692 let mut m2 = new_m286(synth_prg_8k(16), synth_chr_2k(16), Mirroring::Vertical).unwrap();
693 m2.load_state(&blob).unwrap();
694 assert_eq!(m2.ppu_read(0x0000), m.ppu_read(0x0000));
695 }
696
697 #[test]
708 fn bs5_load_state_rejects_a_dip_the_board_cannot_have() {
709 const DIP: usize = 1 + 12 + 16 + 16 + 3;
710 let m = new_m286(synth_prg_8k(16), synth_chr_2k(16), Mirroring::Vertical).unwrap();
711 let good = m.save_state();
712 assert_eq!(good[DIP], 0, "fixture: the DIP defaults to setting #1");
713 for dip in [4u8, 12, 0x41, 0x80, 0xFF] {
714 let mut bad = good.clone();
715 bad[DIP] = dip;
716 let mut m2 = new_m286(synth_prg_8k(16), synth_chr_2k(16), Mirroring::Vertical).unwrap();
717 assert!(
718 matches!(m2.load_state(&bad), Err(MapperError::Invalid(_))),
719 "DIP {dip:#04x} must be rejected"
720 );
721 }
722 for dip in 0..=3u8 {
724 let mut ok = good.clone();
725 ok[DIP] = dip;
726 let mut m2 = new_m286(synth_prg_8k(16), synth_chr_2k(16), Mirroring::Vertical).unwrap();
727 m2.load_state(&ok).unwrap();
728 for a in 0xA000..=0xAFFFu16 {
729 m2.cpu_write(a, 0);
730 }
731 }
732 }
733}