Skip to main content

rustynes_mappers/
m050_fds_conv_smb2j.rs

1//! Mapper 50 -- Famicom Disk System to cartridge conversion (Super Mario
2//! Bros. 2 / SMB2j pirate).
3//!
4//! Like mapper 42 (`m042_fds_conv_bio_miracle.rs`) it substitutes a
5//! free-running CPU-cycle IRQ counter for the disk BIOS timer the game expects.
6//! It additionally *scrambles* the bank bits of its `$8000` window -- the
7//! written value's bits are permuted before becoming a bank number, a copy
8//! protection measure rather than a technical necessity, and the detail a naive
9//! port gets wrong.
10//!
11//! A best-effort (Tier-2) board: register-decode correctness verified against
12//! the reference emulators (`Mesen2`, `GeraNES`) and the nesdev wiki, with no
13//! commercial-oracle ROM in the tree. Banking math is direct slice indexing and
14//! every bank select wraps with `% count`, so a register write can never index
15//! out of bounds -- required for the `#![no_std]` chip stack, which cannot
16//! afford a panic on a register access.
17//!
18//! See `tier.rs` (`MapperTier::BestEffort`), `docs/adr/0011-mapper-tiering.md`,
19//! and `docs/mappers.md` §Mapper coverage matrix.
20
21#![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)]
32
33use crate::cartridge::Mirroring;
34use crate::mapper::{Mapper, MapperCaps, MapperError};
35use alloc::{boxed::Box, format, vec, vec::Vec};
36
37const PRG_BANK_8K: usize = 0x2000;
38const CHR_BANK_8K: usize = 0x2000;
39const NAMETABLE_SIZE: usize = 0x0400;
40const NAMETABLE_SIZE_U16: u16 = 0x0400;
41
42const SAVE_STATE_VERSION: u8 = 1;
43
44// ---------------------------------------------------------------------------
45// Shared nametable helper (mirrors the one in the other simple-mapper modules).
46// ---------------------------------------------------------------------------
47
48const fn nametable_offset(addr: u16, mirroring: Mirroring) -> usize {
49    let table = (((addr - 0x2000) / NAMETABLE_SIZE_U16) & 0x03) as u8;
50    let local = (addr as usize) & (NAMETABLE_SIZE - 1);
51    let physical = mirroring.physical_bank(table);
52    physical * NAMETABLE_SIZE + local
53}
54
55// ===========================================================================
56// Mmc3Clone — reusable MMC3-style core for the clone boards.
57//
58// The MMC3 register protocol (NES 2.0 mapper 4):
59//   $8000 even : bank-select (low 3 bits = R index, bit 6 = PRG mode,
60//                bit 7 = CHR mode).
61//   $8001 odd  : bank-data (the value loaded into the selected R register).
62//   $A000 even : mirroring (bit 0: 0 = vertical, 1 = horizontal).
63//   $C000 even : IRQ latch (reload value).
64//   $C001 odd  : IRQ reload (force a reload on the next A12 rise).
65//   $E000 even : IRQ disable + acknowledge.
66//   $E001 odd  : IRQ enable.
67//
68// The A12 IRQ counter clocks on every PPU A12 rising edge: if the counter is 0
69// or a reload is pending, it reloads from the latch; otherwise it decrements.
70// After the update, if the counter is 0 and IRQs are enabled, the IRQ asserts.
71// ===========================================================================
72
73/// Mapper 50 (Alibaba / *SMB2J* alternate FDS-to-cart conversion).
74pub struct Mapper50 {
75    prg_rom: Box<[u8]>,
76    chr_ram: Box<[u8]>,
77    vram: Box<[u8]>,
78    switch_bank: u8,
79    irq_enabled: bool,
80    irq_counter: u16,
81    irq_pending: bool,
82    mirroring: Mirroring,
83}
84
85impl Mapper50 {
86    /// Construct a mapper 50 board.
87    ///
88    /// # Errors
89    /// [`MapperError::Invalid`] when PRG is not a non-zero multiple of 8 KiB.
90    pub fn new(
91        prg_rom: Box<[u8]>,
92        _chr_rom: &[u8],
93        mirroring: Mirroring,
94    ) -> Result<Self, MapperError> {
95        if prg_rom.is_empty() || !prg_rom.len().is_multiple_of(PRG_BANK_8K) {
96            return Err(MapperError::Invalid(format!(
97                "mapper 50 PRG-ROM size {} is not a non-zero multiple of 8 KiB",
98                prg_rom.len()
99            )));
100        }
101        Ok(Self {
102            prg_rom,
103            chr_ram: vec![0u8; CHR_BANK_8K].into_boxed_slice(),
104            vram: vec![0u8; 2 * NAMETABLE_SIZE].into_boxed_slice(),
105            switch_bank: 0,
106            irq_enabled: false,
107            irq_counter: 0,
108            irq_pending: false,
109            mirroring,
110        })
111    }
112
113    fn prg_8k(&self, bank: usize, addr: u16) -> u8 {
114        let count = (self.prg_rom.len() / PRG_BANK_8K).max(1);
115        let bank = bank % count;
116        self.prg_rom[bank * PRG_BANK_8K + (addr as usize & 0x1FFF)]
117    }
118}
119
120impl Mapper for Mapper50 {
121    fn caps(&self) -> MapperCaps {
122        MapperCaps::CYCLE_IRQ
123    }
124
125    fn cpu_read(&mut self, addr: u16) -> u8 {
126        match addr {
127            0x6000..=0x7FFF => self.prg_8k(15, addr),
128            0x8000..=0x9FFF => self.prg_8k(8, addr),
129            0xA000..=0xBFFF => self.prg_8k(9, addr),
130            0xC000..=0xDFFF => self.prg_8k(self.switch_bank as usize, addr),
131            0xE000..=0xFFFF => self.prg_8k(11, addr),
132            _ => 0,
133        }
134    }
135
136    fn cpu_read_unmapped(&self, addr: u16) -> bool {
137        (0x4020..=0x5FFF).contains(&addr)
138    }
139
140    fn cpu_write(&mut self, addr: u16, value: u8) {
141        match addr & 0x4120 {
142            0x4020 => {
143                self.switch_bank = (value & 0x08) | ((value & 0x01) << 2) | ((value & 0x06) >> 1);
144            }
145            0x4120 => {
146                // Both enable and disable (re)start the counter from 0 and
147                // clear any pending line; only the enable flag itself differs.
148                // On IRQ-enable this means a fresh enable after a prior fire
149                // counts a full period rather than tripping on a stale counter
150                // / latched IRQ.
151                self.irq_enabled = value & 0x01 != 0;
152                self.irq_pending = false;
153                self.irq_counter = 0;
154            }
155            _ => {}
156        }
157    }
158
159    fn ppu_read(&mut self, addr: u16) -> u8 {
160        let addr = addr & 0x3FFF;
161        match addr {
162            0x0000..=0x1FFF => self.chr_ram[addr as usize],
163            0x2000..=0x3EFF => self.vram[nametable_offset(addr, self.mirroring)],
164            _ => 0,
165        }
166    }
167
168    fn ppu_write(&mut self, addr: u16, value: u8) {
169        let addr = addr & 0x3FFF;
170        match addr {
171            0x0000..=0x1FFF => self.chr_ram[addr as usize] = value,
172            0x2000..=0x3EFF => {
173                let off = nametable_offset(addr, self.mirroring);
174                self.vram[off] = value;
175            }
176            _ => {}
177        }
178    }
179
180    fn notify_cpu_cycle(&mut self) {
181        if !self.irq_enabled {
182            return;
183        }
184        self.irq_counter += 1;
185        if self.irq_counter == 0x1000 {
186            self.irq_pending = true;
187            self.irq_enabled = false;
188        }
189    }
190
191    fn irq_pending(&self) -> bool {
192        self.irq_pending
193    }
194
195    fn irq_acknowledge(&mut self) {
196        self.irq_pending = false;
197    }
198
199    fn current_mirroring(&self) -> Mirroring {
200        self.mirroring
201    }
202
203    fn save_state(&self) -> Vec<u8> {
204        let mut out = Vec::with_capacity(6 + self.vram.len() + self.chr_ram.len());
205        out.push(SAVE_STATE_VERSION);
206        out.push(self.switch_bank);
207        out.push(u8::from(self.irq_enabled));
208        out.push((self.irq_counter & 0xFF) as u8);
209        out.push((self.irq_counter >> 8) as u8);
210        out.push(u8::from(self.irq_pending));
211        out.extend_from_slice(&self.vram);
212        out.extend_from_slice(&self.chr_ram);
213        out
214    }
215
216    fn load_state(&mut self, data: &[u8]) -> Result<(), MapperError> {
217        let expected = 6 + self.vram.len() + self.chr_ram.len();
218        if data.len() != expected {
219            return Err(MapperError::WrongLength {
220                expected,
221                got: data.len(),
222            });
223        }
224        if data[0] != SAVE_STATE_VERSION {
225            return Err(MapperError::UnsupportedVersion(data[0]));
226        }
227        self.switch_bank = data[1];
228        self.irq_enabled = data[2] != 0;
229        self.irq_counter = u16::from(data[3]) | (u16::from(data[4]) << 8);
230        self.irq_pending = data[5] != 0;
231        let mut cursor = 6;
232        self.vram
233            .copy_from_slice(&data[cursor..cursor + self.vram.len()]);
234        cursor += self.vram.len();
235        self.chr_ram
236            .copy_from_slice(&data[cursor..cursor + self.chr_ram.len()]);
237        Ok(())
238    }
239}
240
241// ===========================================================================
242// DiscreteMapper — small hook-free single/dual-register multicart boards
243// (46/51/57/104/120/290/301). 32/16 KiB PRG window + 8 KiB CHR-ROM/RAM.
244// ===========================================================================
245
246#[cfg(test)]
247mod tests {
248    use super::*;
249
250    fn synth_prg_8k(banks: usize) -> Box<[u8]> {
251        let mut v = vec![0xFFu8; banks * PRG_BANK_8K];
252        for b in 0..banks {
253            v[b * PRG_BANK_8K] = b as u8;
254        }
255        v.into_boxed_slice()
256    }
257
258    #[test]
259    fn m50_fixed_layout_and_scrambled_switch() {
260        let mut m = Mapper50::new(synth_prg_8k(16), &[], Mirroring::Vertical).unwrap();
261        assert_eq!(m.cpu_read(0x6000), 15);
262        assert_eq!(m.cpu_read(0x8000), 8);
263        assert_eq!(m.cpu_read(0xA000), 9);
264        assert_eq!(m.cpu_read(0xE000), 11);
265        // value 0x05 -> (0x05&8)|((0x05&1)<<2)|((0x05&6)>>1) = 0 | 4 | 2 = 6.
266        m.cpu_write(0x4020, 0x05);
267        assert_eq!(m.cpu_read(0xC000), 6);
268    }
269
270    #[test]
271    fn m50_irq_fires_once_then_disables() {
272        let mut m = Mapper50::new(synth_prg_8k(16), &[], Mirroring::Vertical).unwrap();
273        m.cpu_write(0x4120, 0x01); // enable
274        for _ in 0..0x1000 {
275            m.notify_cpu_cycle();
276        }
277        assert!(m.irq_pending());
278        m.cpu_write(0x4120, 0x00); // disable + ack
279        assert!(!m.irq_pending());
280    }
281
282    #[test]
283    fn m50_save_state_round_trip() {
284        let mut m = Mapper50::new(synth_prg_8k(16), &[], Mirroring::Vertical).unwrap();
285        m.cpu_write(0x4020, 0x05);
286        m.cpu_write(0x4120, 0x01);
287        m.notify_cpu_cycle();
288        m.ppu_write(0x0007, 0x33);
289        let blob = m.save_state();
290        let mut m2 = Mapper50::new(synth_prg_8k(16), &[], Mirroring::Vertical).unwrap();
291        m2.load_state(&blob).unwrap();
292        assert_eq!(m2.cpu_read(0xC000), 6);
293        assert_eq!(m2.ppu_read(0x0007), 0x33);
294    }
295}