1#![allow(
39 clippy::cast_possible_truncation,
40 clippy::cast_lossless,
41 clippy::missing_const_for_fn,
42 clippy::doc_markdown,
43 clippy::doc_lazy_continuation
44)]
45
46use crate::cartridge::Mirroring;
47use crate::mapper::{Mapper, MapperCaps, MapperError};
48use alloc::{boxed::Box, vec::Vec};
49use alloc::{format, vec};
50
51const PRG_BANK_8K: usize = 0x2000;
52const CHR_BANK_1K: usize = 0x0400;
53const NAMETABLE_SIZE: usize = 0x0400;
54const NAMETABLE_SIZE_U16: u16 = 0x0400;
55
56const SAVE_STATE_VERSION: u8 = 2;
62
63#[derive(Debug, Clone, Copy, PartialEq, Eq)]
65pub enum Namco118Board {
66 Dxrom,
68 M88,
71 M154,
77}
78
79pub struct Namco118 {
81 prg_rom: Box<[u8]>,
82 chr: Box<[u8]>,
83 vram: Box<[u8]>,
84 chr_is_ram: bool,
85
86 regs: [u8; 8],
88 bank_select: u8,
89
90 mirroring: Mirroring,
91 fixed_4screen: bool,
92 board: Namco118Board,
93}
94
95impl Namco118 {
96 pub fn new(
106 prg_rom: Box<[u8]>,
107 chr_rom: Box<[u8]>,
108 mirroring: Mirroring,
109 board: Namco118Board,
110 ) -> Result<Self, MapperError> {
111 if prg_rom.is_empty() || !prg_rom.len().is_multiple_of(PRG_BANK_8K) {
112 return Err(MapperError::Invalid(format!(
113 "Namco-118 PRG-ROM size {} is not a non-zero multiple of 8 KiB",
114 prg_rom.len()
115 )));
116 }
117 let chr_is_ram = chr_rom.is_empty();
118 let chr: Box<[u8]> = if chr_is_ram {
119 vec![0u8; 8 * CHR_BANK_1K].into_boxed_slice()
120 } else if chr_rom.len().is_multiple_of(CHR_BANK_1K) {
121 chr_rom
122 } else {
123 return Err(MapperError::Invalid(format!(
124 "Namco-118 CHR-ROM size {} is not a multiple of 1 KiB",
125 chr_rom.len()
126 )));
127 };
128 let fixed_4screen = matches!(mirroring, Mirroring::FourScreen);
129 let vram_size = if fixed_4screen {
130 4 * NAMETABLE_SIZE
131 } else {
132 2 * NAMETABLE_SIZE
133 };
134 Ok(Self {
135 prg_rom,
136 chr,
137 vram: vec![0u8; vram_size].into_boxed_slice(),
138 chr_is_ram,
139 regs: [0; 8],
140 bank_select: 0,
141 mirroring,
142 fixed_4screen,
143 board,
144 })
145 }
146
147 fn prg_offset(&self, addr: u16) -> usize {
149 let total = (self.prg_rom.len() / PRG_BANK_8K).max(1);
150 let last = total - 1;
151 let second_last = total.saturating_sub(2);
152 let bank = match addr & 0xE000 {
153 0x8000 => (self.regs[6] as usize) % total,
154 0xA000 => (self.regs[7] as usize) % total,
155 0xC000 => second_last,
156 0xE000 => last,
157 _ => 0,
158 };
159 bank * PRG_BANK_8K + (addr as usize & 0x1FFF)
160 }
161
162 fn chr_offset(&self, addr: u16) -> usize {
167 let addr = (addr & 0x1FFF) as usize;
168 let total_1k = (self.chr.len() / CHR_BANK_1K).max(1);
169 let slot = addr / CHR_BANK_1K;
170 let bank_1k: usize = match slot {
171 0 => (self.regs[0] as usize) & !1,
173 1 => ((self.regs[0] as usize) & !1) | 1,
174 2 => (self.regs[1] as usize) & !1,
176 3 => ((self.regs[1] as usize) & !1) | 1,
177 4 => self.m88_high(self.regs[2] as usize),
179 5 => self.m88_high(self.regs[3] as usize),
180 6 => self.m88_high(self.regs[4] as usize),
181 7 => self.m88_high(self.regs[5] as usize),
182 _ => 0,
183 };
184 let bank = bank_1k % total_1k;
185 bank * CHR_BANK_1K + (addr & (CHR_BANK_1K - 1))
186 }
187
188 fn m88_high(&self, reg: usize) -> usize {
191 match self.board {
192 Namco118Board::M88 | Namco118Board::M154 => (reg & 0x3F) | 0x40,
194 Namco118Board::Dxrom => reg,
195 }
196 }
197
198 fn nametable_offset(&self, addr: u16) -> usize {
199 let table = (((addr - 0x2000) / NAMETABLE_SIZE_U16) & 0x03) as u8;
200 let local = (addr as usize) & (NAMETABLE_SIZE - 1);
201 if self.fixed_4screen {
202 (table as usize) * NAMETABLE_SIZE + local
203 } else {
204 let physical = self.mirroring.physical_bank(table);
205 physical * NAMETABLE_SIZE + local
206 }
207 }
208}
209
210impl Mapper for Namco118 {
211 fn caps(&self) -> MapperCaps {
214 MapperCaps::NONE
215 }
216
217 fn cpu_read(&mut self, addr: u16) -> u8 {
218 match addr {
219 0x8000..=0xFFFF => {
220 let off = self.prg_offset(addr);
221 self.prg_rom[off % self.prg_rom.len()]
222 }
223 _ => 0,
224 }
225 }
226
227 fn cpu_write(&mut self, addr: u16, value: u8) {
228 if let 0x8000..=0xFFFF = addr {
229 if self.board == Namco118Board::M154 {
237 self.mirroring = if value & 0x40 == 0 {
238 Mirroring::SingleScreenA
239 } else {
240 Mirroring::SingleScreenB
241 };
242 }
243 if addr & 1 == 0 {
246 self.bank_select = value & 0x07;
247 } else {
248 self.regs[(self.bank_select & 0x07) as usize] = value;
249 }
250 }
251 }
252
253 fn ppu_read(&mut self, addr: u16) -> u8 {
254 let addr = addr & 0x3FFF;
255 match addr {
256 0x0000..=0x1FFF => {
257 let off = self.chr_offset(addr);
258 self.chr[off % self.chr.len()]
259 }
260 0x2000..=0x3EFF => self.vram[self.nametable_offset(addr) % self.vram.len()],
261 _ => 0,
262 }
263 }
264
265 fn ppu_write(&mut self, addr: u16, value: u8) {
266 let addr = addr & 0x3FFF;
267 match addr {
268 0x0000..=0x1FFF => {
269 if self.chr_is_ram {
270 let off = self.chr_offset(addr);
271 let len = self.chr.len();
272 self.chr[off % len] = value;
273 }
274 }
275 0x2000..=0x3EFF => {
276 let off = self.nametable_offset(addr) % self.vram.len();
277 self.vram[off] = value;
278 }
279 _ => {}
280 }
281 }
282
283 fn nametable_address(&self, addr: u16) -> u16 {
284 let off = self.nametable_offset(addr);
285 u16::try_from(off & 0x07FF).unwrap_or(0)
286 }
287
288 fn current_mirroring(&self) -> Mirroring {
289 self.mirroring
290 }
291
292 fn debug_info(&self) -> crate::mapper::MapperDebugInfo {
293 let id = match self.board {
294 Namco118Board::M88 => 88,
295 Namco118Board::M154 => 154,
296 Namco118Board::Dxrom => 206,
297 };
298 let mut info = crate::mapper::MapperDebugInfo {
299 mapper_id: id,
300 name: match self.board {
301 Namco118Board::M88 => "Namco 118 (88)".into(),
302 Namco118Board::M154 => "NAMCOT-3453 (154)".into(),
303 Namco118Board::Dxrom => "DxROM / Namco 118 (206)".into(),
304 },
305 mirroring: crate::mapper::mirroring_name(self.mirroring),
306 ..Default::default()
307 };
308 info.prg_banks
309 .push(("R6".into(), format!("{:#04x}", self.regs[6])));
310 info.prg_banks
311 .push(("R7".into(), format!("{:#04x}", self.regs[7])));
312 for i in 0..6 {
313 info.chr_banks
314 .push((format!("R{i}"), format!("{:#04x}", self.regs[i])));
315 }
316 info
317 }
318
319 fn save_state(&self) -> Vec<u8> {
320 let mut out = Vec::with_capacity(
321 11 + self.vram.len() + if self.chr_is_ram { self.chr.len() } else { 0 },
322 );
323 out.push(SAVE_STATE_VERSION);
324 out.extend_from_slice(&self.regs);
325 out.push(self.bank_select);
326 out.push(match self.mirroring {
327 Mirroring::Horizontal => 0,
328 Mirroring::Vertical => 1,
329 Mirroring::SingleScreenA => 2,
330 Mirroring::SingleScreenB => 3,
331 Mirroring::FourScreen => 4,
332 Mirroring::MapperControlled => 5,
333 });
334 out.extend_from_slice(&self.vram);
335 if self.chr_is_ram {
336 out.extend_from_slice(&self.chr);
337 }
338 out
339 }
340
341 fn load_state(&mut self, data: &[u8]) -> Result<(), MapperError> {
342 let need_chr = if self.chr_is_ram { self.chr.len() } else { 0 };
343 match data.first() {
348 None => {
349 return Err(MapperError::WrongLength {
350 expected: 1,
351 got: 0,
352 });
353 }
354 Some(&v) if v != SAVE_STATE_VERSION => {
355 return Err(MapperError::UnsupportedVersion(v));
356 }
357 Some(_) => {}
358 }
359 let expected = 11 + self.vram.len() + need_chr;
360 if data.len() != expected {
361 return Err(MapperError::WrongLength {
362 expected,
363 got: data.len(),
364 });
365 }
366 self.regs.copy_from_slice(&data[1..9]);
367 self.bank_select = data[9];
368 self.mirroring = match data[10] {
369 0 => Mirroring::Horizontal,
370 1 => Mirroring::Vertical,
371 2 => Mirroring::SingleScreenA,
372 3 => Mirroring::SingleScreenB,
373 4 => Mirroring::FourScreen,
374 5 => Mirroring::MapperControlled,
375 _ => self.mirroring,
376 };
377 let mut cursor = 11;
378 self.vram
379 .copy_from_slice(&data[cursor..cursor + self.vram.len()]);
380 cursor += self.vram.len();
381 if self.chr_is_ram {
382 self.chr
383 .copy_from_slice(&data[cursor..cursor + self.chr.len()]);
384 }
385 Ok(())
386 }
387}
388
389#[cfg(test)]
390#[allow(clippy::cast_possible_truncation)]
391mod tests {
392 use super::*;
393
394 fn synth_prg(banks_8k: usize) -> Box<[u8]> {
395 let mut v = vec![0u8; banks_8k * PRG_BANK_8K];
396 for b in 0..banks_8k {
397 v[b * PRG_BANK_8K] = b as u8;
398 }
399 v.into_boxed_slice()
400 }
401
402 fn synth_chr(banks_1k: usize) -> Box<[u8]> {
403 let mut v = vec![0u8; banks_1k * CHR_BANK_1K];
404 for b in 0..banks_1k {
405 v[b * CHR_BANK_1K] = b as u8;
406 }
407 v.into_boxed_slice()
408 }
409
410 #[test]
411 fn prg_last_two_banks_fixed() {
412 let mut m = Namco118::new(
414 synth_prg(8),
415 synth_chr(64),
416 Mirroring::Vertical,
417 Namco118Board::Dxrom,
418 )
419 .unwrap();
420 assert_eq!(m.cpu_read(0xC000), 6); assert_eq!(m.cpu_read(0xE000), 7); }
423
424 #[test]
425 fn prg_bank_select_r6_r7() {
426 let mut m = Namco118::new(
427 synth_prg(8),
428 synth_chr(64),
429 Mirroring::Vertical,
430 Namco118Board::Dxrom,
431 )
432 .unwrap();
433 m.cpu_write(0x8000, 6);
435 m.cpu_write(0x8001, 3);
436 assert_eq!(m.cpu_read(0x8000), 3);
437 m.cpu_write(0x8000, 7);
439 m.cpu_write(0x8001, 5);
440 assert_eq!(m.cpu_read(0xA000), 5);
441 assert_eq!(m.cpu_read(0xE000), 7);
443 }
444
445 #[test]
446 fn chr_fixed_layout_2k_then_1k() {
447 let mut m = Namco118::new(
448 synth_prg(8),
449 synth_chr(64),
450 Mirroring::Vertical,
451 Namco118Board::Dxrom,
452 )
453 .unwrap();
454 m.cpu_write(0x8000, 0);
456 m.cpu_write(0x8001, 4);
457 assert_eq!(m.ppu_read(0x0000), 4);
458 assert_eq!(m.ppu_read(0x0400), 5); m.cpu_write(0x8000, 2);
461 m.cpu_write(0x8001, 9);
462 assert_eq!(m.ppu_read(0x1000), 9);
463 }
464
465 #[test]
466 fn m88_right_table_uses_second_chr_half() {
467 let mut m = Namco118::new(
469 synth_prg(8),
470 synth_chr(128),
471 Mirroring::Vertical,
472 Namco118Board::M88,
473 )
474 .unwrap();
475 m.cpu_write(0x8000, 2);
477 m.cpu_write(0x8001, 5);
478 assert_eq!(m.ppu_read(0x1000), 69);
479 m.cpu_write(0x8000, 0);
481 m.cpu_write(0x8001, 2);
482 assert_eq!(m.ppu_read(0x0000), 2);
483 }
484
485 #[test]
486 fn no_irq() {
487 let mut m = Namco118::new(
488 synth_prg(8),
489 synth_chr(64),
490 Mirroring::Vertical,
491 Namco118Board::Dxrom,
492 )
493 .unwrap();
494 for _ in 0..1000 {
496 m.notify_a12(true);
497 m.notify_a12(false);
498 m.notify_cpu_cycle();
499 }
500 assert!(!m.irq_pending());
501 }
502
503 #[test]
504 fn save_state_round_trip() {
505 let mut m = Namco118::new(
506 synth_prg(8),
507 synth_chr(64),
508 Mirroring::Horizontal,
509 Namco118Board::Dxrom,
510 )
511 .unwrap();
512 m.cpu_write(0x8000, 6);
513 m.cpu_write(0x8001, 2);
514 m.cpu_write(0x8000, 0);
515 m.cpu_write(0x8001, 8);
516 let blob = m.save_state();
517 let mut m2 = Namco118::new(
518 synth_prg(8),
519 synth_chr(64),
520 Mirroring::Horizontal,
521 Namco118Board::Dxrom,
522 )
523 .unwrap();
524 m2.load_state(&blob).unwrap();
525 assert_eq!(m.cpu_read(0x8000), m2.cpu_read(0x8000));
526 assert_eq!(m.ppu_read(0x0000), m2.ppu_read(0x0000));
527 }
528
529 fn m154() -> Namco118 {
532 Namco118::new(
533 synth_prg(8),
534 synth_chr(128),
535 Mirroring::Vertical,
536 Namco118Board::M154,
537 )
538 .unwrap()
539 }
540
541 #[test]
542 fn m154_bit6_selects_the_one_screen_page() {
543 let mut m = m154();
544 m.cpu_write(0x8000, 0x00);
545 assert_eq!(m.current_mirroring(), Mirroring::SingleScreenA);
546 m.cpu_write(0x8000, 0x40);
547 assert_eq!(m.current_mirroring(), Mirroring::SingleScreenB);
548 }
549
550 #[test]
551 fn m154_decodes_the_nametable_bit_across_the_whole_8000_ffff_range() {
552 for addr in [0x8001u16, 0x9FFF, 0xA000, 0xC001, 0xFFFF] {
557 let mut m = m154();
558 m.cpu_write(0x8000, 0x00);
559 assert_eq!(m.current_mirroring(), Mirroring::SingleScreenA);
560 m.cpu_write(addr, 0x40);
561 assert_eq!(
562 m.current_mirroring(),
563 Mirroring::SingleScreenB,
564 "bit 6 must be decoded at ${addr:04X}"
565 );
566 }
567 }
568
569 #[test]
570 fn m154_keeps_the_mapper_88_chr_split() {
571 let mut a = m154();
574 let mut b = Namco118::new(
575 synth_prg(8),
576 synth_chr(128),
577 Mirroring::Vertical,
578 Namco118Board::M88,
579 )
580 .unwrap();
581 for m in [&mut a, &mut b] {
582 m.cpu_write(0x8000, 0x02); m.cpu_write(0x8001, 0x05);
584 }
585 assert_eq!(a.ppu_read(0x1000), b.ppu_read(0x1000));
586 assert_eq!(a.ppu_read(0x1000), (0x05u8 & 0x3F) | 0x40);
587 }
588
589 #[test]
590 fn m154_bank_writes_still_work_while_setting_mirroring() {
591 let mut m = m154();
594 m.cpu_write(0x8000, 0x06); m.cpu_write(0x8001, 0x43); assert_eq!(
597 m.cpu_read(0x8000),
598 3,
599 "the bank register still took the value"
600 );
601 assert_eq!(m.current_mirroring(), Mirroring::SingleScreenB);
602 }
603
604 #[test]
605 fn m154_mirroring_survives_a_save_state() {
606 let mut m = m154();
609 m.cpu_write(0x8000, 0x40);
610 assert_eq!(m.current_mirroring(), Mirroring::SingleScreenB);
611 let blob = m.save_state();
612
613 let mut m2 = m154();
614 m2.cpu_write(0x8000, 0x00);
615 assert_eq!(m2.current_mirroring(), Mirroring::SingleScreenA);
616 m2.load_state(&blob).unwrap();
617 assert_eq!(m2.current_mirroring(), Mirroring::SingleScreenB);
618 }
619
620 #[test]
621 fn m88_ignores_bit6_so_it_is_unchanged() {
622 let mut m = Namco118::new(
623 synth_prg(8),
624 synth_chr(128),
625 Mirroring::Vertical,
626 Namco118Board::M88,
627 )
628 .unwrap();
629 m.cpu_write(0x8000, 0x40);
630 assert_eq!(m.current_mirroring(), Mirroring::Vertical);
631 }
632
633 #[test]
634 fn m88_refuses_a_v1_state_that_predates_the_mirroring_byte() {
635 let mut m = Namco118::new(
638 synth_prg(8),
639 synth_chr(64),
640 Mirroring::Vertical,
641 Namco118Board::M88,
642 )
643 .unwrap();
644 m.cpu_write(0x8000, 0x06);
645 m.cpu_write(0x8001, 3);
646 let v2 = m.save_state();
647
648 let mut v1 = Vec::with_capacity(v2.len() - 1);
650 v1.extend_from_slice(&v2[..10]);
651 v1.extend_from_slice(&v2[11..]);
652 v1[0] = 1;
653
654 let mut m2 = Namco118::new(
655 synth_prg(8),
656 synth_chr(64),
657 Mirroring::Vertical,
658 Namco118Board::M88,
659 )
660 .unwrap();
661 assert!(matches!(
662 m2.load_state(&v1),
663 Err(MapperError::UnsupportedVersion(1))
664 ));
665 let mut mismatched = v2.clone();
667 mismatched[0] = 1;
668 assert!(m2.load_state(&mismatched).is_err());
669 assert!(matches!(
671 m2.load_state(&v2[..v2.len() - 1]),
672 Err(MapperError::WrongLength { .. })
673 ));
674 m2.load_state(&v2).expect("the current blob loads");
675 assert_eq!(m2.cpu_read(0x8000), 3, "bank state round-trips");
676 }
677}