Skip to main content

rustynes_mappers/
m151_konami_vs.rs

1//! Konami VS (iNES mapper 151) implementation.
2//!
3//! Mapper 151 is **Konami's VRC1 silicon mounted on a Nintendo Vs. System
4//! arcade board** (Vs. Gradius / Vs. The Goonies; the T.K.O. Boxing dumps that
5//! carry this header drive a Namco 108 instead, see `docs/mappers.md`). The bank-switching
6//! behaviour is byte-for-byte VRC1 (mapper 75): three switchable 8 KiB PRG
7//! banks at `$8000`/`$A000`/`$C000` with a fixed last bank, two 4 KiB CHR
8//! windows, a CHR-MSB bit per window, and `$9000`-bit-0 H/V mirroring control.
9//!
10//! The only thing that distinguishes mapper 151 from mapper 75 is the *console*
11//! it runs on: the Vs. board replaces the 2C02 composite PPU with a 2C03 RGB
12//! PPU. That platform detail is applied in [`crate::parse`] (which forces
13//! [`crate::ConsoleType::VsSystem`] + the 2C03 RGB PPU for a mapper-151 cart,
14//! exactly as it does for mapper 99). This wrapper exists so mapper 151 is a
15//! first-class, dispatch-visible mapper family that reuses the proven VRC1
16//! core rather than re-implementing it.
17//!
18//! See `docs/mappers.md` §Mapper coverage matrix.
19
20#![allow(clippy::doc_markdown)]
21
22use crate::cartridge::Mirroring;
23use crate::m075_vrc1::Vrc1;
24use crate::mapper::{Mapper, MapperCaps, MapperDebugInfo, MapperError};
25use alloc::{boxed::Box, vec::Vec};
26
27/// Konami VS (Mapper 151) — VRC1 on Vs. System hardware.
28pub struct KonamiVs {
29    inner: Vrc1,
30}
31
32impl KonamiVs {
33    /// Construct a new Konami VS (mapper 151) mapper.
34    ///
35    /// # Errors
36    ///
37    /// Returns [`MapperError::Invalid`] on a PRG / CHR size mismatch (forwarded
38    /// from the VRC1 core).
39    pub fn new(
40        prg_rom: Box<[u8]>,
41        chr_rom: Box<[u8]>,
42        mirroring: Mirroring,
43    ) -> Result<Self, MapperError> {
44        Ok(Self {
45            inner: Vrc1::new(prg_rom, chr_rom, mirroring)?,
46        })
47    }
48}
49
50impl Mapper for KonamiVs {
51    // v2.8.0 Phase 4 — no per-cycle hooks (no IRQ, no audio): the bus
52    // skips all four per-CPU-cycle dispatches for this board.
53    fn caps(&self) -> MapperCaps {
54        MapperCaps::NONE
55    }
56
57    fn cpu_read(&mut self, addr: u16) -> u8 {
58        self.inner.cpu_read(addr)
59    }
60
61    fn cpu_write(&mut self, addr: u16, value: u8) {
62        self.inner.cpu_write(addr, value);
63    }
64
65    fn ppu_read(&mut self, addr: u16) -> u8 {
66        self.inner.ppu_read(addr)
67    }
68
69    fn ppu_write(&mut self, addr: u16, value: u8) {
70        self.inner.ppu_write(addr, value);
71    }
72
73    fn current_mirroring(&self) -> Mirroring {
74        self.inner.current_mirroring()
75    }
76
77    fn save_state(&self) -> Vec<u8> {
78        self.inner.save_state()
79    }
80
81    fn load_state(&mut self, data: &[u8]) -> Result<(), MapperError> {
82        self.inner.load_state(data)
83    }
84
85    fn debug_info(&self) -> MapperDebugInfo {
86        let mut info = self.inner.debug_info();
87        info.mapper_id = 151;
88        info.name = "Konami VS / VRC1 (151)".into();
89        info
90    }
91}
92
93#[cfg(test)]
94#[allow(clippy::cast_possible_truncation)]
95mod tests {
96    use super::*;
97    use alloc::vec;
98
99    const PRG_BANK_8K: usize = 0x2000;
100    const CHR_BANK_4K: usize = 0x1000;
101
102    fn synth(banks_8k: usize) -> Box<[u8]> {
103        let mut v = vec![0u8; banks_8k * PRG_BANK_8K];
104        for b in 0..banks_8k {
105            v[b * PRG_BANK_8K] = b as u8;
106        }
107        v.into_boxed_slice()
108    }
109
110    fn synth_chr_4k(banks: usize) -> Box<[u8]> {
111        let mut v = vec![0u8; banks * CHR_BANK_4K];
112        for b in 0..banks {
113            v[b * CHR_BANK_4K] = b as u8;
114        }
115        v.into_boxed_slice()
116    }
117
118    #[test]
119    fn delegates_vrc1_banking() {
120        let mut m = KonamiVs::new(synth(8), synth_chr_4k(2), Mirroring::Vertical).unwrap();
121        m.cpu_write(0x8000, 3);
122        assert_eq!(m.cpu_read(0x8000), 3);
123        // $E000 is the fixed last bank.
124        assert_eq!(m.cpu_read(0xE000), 7);
125    }
126
127    #[test]
128    fn debug_info_reports_mapper_151() {
129        let m = KonamiVs::new(synth(8), synth_chr_4k(2), Mirroring::Vertical).unwrap();
130        assert_eq!(m.debug_info().mapper_id, 151);
131    }
132
133    #[test]
134    fn save_state_round_trip() {
135        let mut m = KonamiVs::new(synth(8), synth_chr_4k(2), Mirroring::Vertical).unwrap();
136        m.cpu_write(0x8000, 5);
137        m.cpu_write(0x9000, 0x01); // mirroring -> Horizontal
138        let blob = m.save_state();
139        let mut m2 = KonamiVs::new(synth(8), synth_chr_4k(2), Mirroring::Vertical).unwrap();
140        m2.load_state(&blob).unwrap();
141        assert_eq!(m.cpu_read(0x8000), m2.cpu_read(0x8000));
142        assert_eq!(m.current_mirroring(), m2.current_mirroring());
143    }
144
145    /// v2.9.2 cartridge-RAM sweep: mapper 151 forwards its section to the
146    /// VRC1 core, so a board with no CHR-ROM carries its CHR-RAM through the
147    /// wrapper too. The whole-machine pin is
148    /// `rustynes_core::nes::tests::every_board_snapshot_carries_cartridge_ram`.
149    #[test]
150    fn save_state_carries_chr_ram() {
151        let mut m = KonamiVs::new(synth(8), Box::new([]), Mirroring::Vertical).unwrap();
152        m.ppu_write(0x0000, 0x11);
153        m.ppu_write(0x0FFF, 0x22);
154        let blob = m.save_state();
155        let mut m2 = KonamiVs::new(synth(8), Box::new([]), Mirroring::Vertical).unwrap();
156        m2.load_state(&blob).expect("round-trip");
157        assert_eq!(m2.ppu_read(0x0000), 0x11);
158        assert_eq!(m2.ppu_read(0x0FFF), 0x22);
159    }
160}