Skip to main content

rustynes_mappers/
m042_fds_conv_bio_miracle.rs

1//! Mapper 42 -- Famicom Disk System to cartridge conversion (Bio Miracle
2//! Bokutte Upa, Mario Baby).
3//!
4//! Running an FDS title from a cartridge is harder than it sounds: the game
5//! expects RAM where a cartridge has ROM, and it expects the disk BIOS's timer
6//! IRQ to be ticking. So this board does two unusual things at once -- it maps
7//! an 8 KiB PRG window a plain multicart would not need, and it carries a
8//! free-running CPU-cycle IRQ counter purely to stand in for the BIOS timer the
9//! game is polling.
10//!
11//! The other conversion board is mapper 50, in
12//! `m050_fds_conv_smb2j.rs`. The real FDS is emulated in `fds.rs`.
13//!
14//! A best-effort (Tier-2) board: register-decode correctness verified against
15//! the reference emulators (`Mesen2`, `GeraNES`) and the nesdev wiki, with no
16//! commercial-oracle ROM in the tree. Banking math is direct slice indexing and
17//! every bank select wraps with `% count`, so a register write can never index
18//! out of bounds -- required for the `#![no_std]` chip stack, which cannot
19//! afford a panic on a register access.
20//!
21//! See `tier.rs` (`MapperTier::BestEffort`), `docs/adr/0011-mapper-tiering.md`,
22//! and `docs/mappers.md` §Mapper coverage matrix.
23
24#![allow(
25    clippy::cast_possible_truncation,
26    clippy::cast_lossless,
27    clippy::match_same_arms,
28    clippy::doc_markdown,
29    clippy::similar_names,
30    clippy::too_many_lines,
31    clippy::missing_const_for_fn,
32    clippy::struct_excessive_bools,
33    clippy::bool_to_int_with_if
34)]
35
36use crate::cartridge::Mirroring;
37use crate::mapper::{Mapper, MapperCaps, MapperError};
38use alloc::{boxed::Box, format, vec, vec::Vec};
39
40const PRG_BANK_8K: usize = 0x2000;
41const CHR_BANK_8K: usize = 0x2000;
42const NAMETABLE_SIZE: usize = 0x0400;
43const NAMETABLE_SIZE_U16: u16 = 0x0400;
44
45const SAVE_STATE_VERSION: u8 = 1;
46
47// ---------------------------------------------------------------------------
48// Shared nametable helper (mirrors the one in the other simple-mapper modules).
49// ---------------------------------------------------------------------------
50
51const fn nametable_offset(addr: u16, mirroring: Mirroring) -> usize {
52    let table = (((addr - 0x2000) / NAMETABLE_SIZE_U16) & 0x03) as u8;
53    let local = (addr as usize) & (NAMETABLE_SIZE - 1);
54    let physical = mirroring.physical_bank(table);
55    physical * NAMETABLE_SIZE + local
56}
57
58const fn mirroring_to_byte(m: Mirroring) -> u8 {
59    match m {
60        Mirroring::Horizontal => 0,
61        Mirroring::Vertical => 1,
62        Mirroring::SingleScreenA => 2,
63        Mirroring::SingleScreenB => 3,
64        Mirroring::FourScreen => 4,
65        Mirroring::MapperControlled => 5,
66    }
67}
68
69const fn byte_to_mirroring(b: u8, fallback: Mirroring) -> Mirroring {
70    match b {
71        0 => Mirroring::Horizontal,
72        1 => Mirroring::Vertical,
73        2 => Mirroring::SingleScreenA,
74        3 => Mirroring::SingleScreenB,
75        4 => Mirroring::FourScreen,
76        5 => Mirroring::MapperControlled,
77        _ => fallback,
78    }
79}
80
81// ===========================================================================
82// Mmc3Clone — reusable MMC3-style core for the clone boards.
83//
84// The MMC3 register protocol (NES 2.0 mapper 4):
85//   $8000 even : bank-select (low 3 bits = R index, bit 6 = PRG mode,
86//                bit 7 = CHR mode).
87//   $8001 odd  : bank-data (the value loaded into the selected R register).
88//   $A000 even : mirroring (bit 0: 0 = vertical, 1 = horizontal).
89//   $C000 even : IRQ latch (reload value).
90//   $C001 odd  : IRQ reload (force a reload on the next A12 rise).
91//   $E000 even : IRQ disable + acknowledge.
92//   $E001 odd  : IRQ enable.
93//
94// The A12 IRQ counter clocks on every PPU A12 rising edge: if the counter is 0
95// or a reload is pending, it reloads from the latch; otherwise it decrements.
96// After the update, if the counter is 0 and IRQs are enabled, the IRQ asserts.
97// ===========================================================================
98
99/// Mapper 42 (FDS-to-cart conversion: *Mario Baby* / *Ai Senshi Nicol*).
100pub struct Mapper42 {
101    prg_rom: Box<[u8]>,
102    chr: Box<[u8]>,
103    chr_is_ram: bool,
104    vram: Box<[u8]>,
105    prg_ram_bank: u8,
106    chr_bank: u8,
107    mirroring: Mirroring,
108    irq_counter: u16,
109    irq_enabled: bool,
110    irq_pending: bool,
111}
112
113impl Mapper42 {
114    /// Construct a mapper 42 board.
115    ///
116    /// # Errors
117    /// [`MapperError::Invalid`] on a bad PRG size.
118    pub fn new(
119        prg_rom: Box<[u8]>,
120        chr_rom: Box<[u8]>,
121        mirroring: Mirroring,
122    ) -> Result<Self, MapperError> {
123        if prg_rom.is_empty() || !prg_rom.len().is_multiple_of(PRG_BANK_8K) {
124            return Err(MapperError::Invalid(format!(
125                "mapper 42 PRG-ROM size {} is not a non-zero multiple of 8 KiB",
126                prg_rom.len()
127            )));
128        }
129        let chr_is_ram = chr_rom.is_empty();
130        let chr: Box<[u8]> = if chr_is_ram {
131            vec![0u8; CHR_BANK_8K].into_boxed_slice()
132        } else {
133            chr_rom
134        };
135        Ok(Self {
136            prg_rom,
137            chr,
138            chr_is_ram,
139            vram: vec![0u8; 2 * NAMETABLE_SIZE].into_boxed_slice(),
140            prg_ram_bank: 0,
141            chr_bank: 0,
142            mirroring,
143            irq_counter: 0,
144            irq_enabled: false,
145            irq_pending: false,
146        })
147    }
148
149    fn prg_8k(&self, bank: usize, addr: u16) -> u8 {
150        let count = (self.prg_rom.len() / PRG_BANK_8K).max(1);
151        let bank = bank % count;
152        self.prg_rom[bank * PRG_BANK_8K + (addr as usize & 0x1FFF)]
153    }
154}
155
156impl Mapper for Mapper42 {
157    fn caps(&self) -> MapperCaps {
158        MapperCaps::CYCLE_IRQ
159    }
160
161    fn cpu_read(&mut self, addr: u16) -> u8 {
162        let count = (self.prg_rom.len() / PRG_BANK_8K).max(1);
163        match addr {
164            0x6000..=0x7FFF => self.prg_8k(self.prg_ram_bank as usize, addr),
165            0x8000..=0x9FFF => self.prg_8k(count.saturating_sub(4), addr),
166            0xA000..=0xBFFF => self.prg_8k(count.saturating_sub(3), addr),
167            0xC000..=0xDFFF => self.prg_8k(count.saturating_sub(2), addr),
168            0xE000..=0xFFFF => self.prg_8k(count.saturating_sub(1), addr),
169            _ => 0,
170        }
171    }
172
173    fn cpu_read_unmapped(&self, addr: u16) -> bool {
174        (0x4020..=0x5FFF).contains(&addr)
175    }
176
177    fn cpu_write(&mut self, addr: u16, value: u8) {
178        match addr & 0xE003 {
179            0x8000 => self.chr_bank = value & 0x0F,
180            0xE000 => self.prg_ram_bank = value & 0x0F,
181            0xE001 => {
182                self.mirroring = if value & 0x08 != 0 {
183                    Mirroring::Horizontal
184                } else {
185                    Mirroring::Vertical
186                };
187            }
188            0xE002 => {
189                self.irq_enabled = value & 0x02 != 0;
190                if !self.irq_enabled {
191                    self.irq_pending = false;
192                    self.irq_counter = 0;
193                }
194            }
195            _ => {}
196        }
197    }
198
199    fn ppu_read(&mut self, addr: u16) -> u8 {
200        let addr = addr & 0x3FFF;
201        match addr {
202            0x0000..=0x1FFF => {
203                if self.chr_is_ram {
204                    return self.chr[addr as usize & (self.chr.len() - 1)];
205                }
206                let count = (self.chr.len() / CHR_BANK_8K).max(1);
207                let bank = (self.chr_bank as usize) % count;
208                self.chr[bank * CHR_BANK_8K + (addr as usize & 0x1FFF)]
209            }
210            0x2000..=0x3EFF => self.vram[nametable_offset(addr, self.mirroring)],
211            _ => 0,
212        }
213    }
214
215    fn ppu_write(&mut self, addr: u16, value: u8) {
216        let addr = addr & 0x3FFF;
217        match addr {
218            0x0000..=0x1FFF if self.chr_is_ram => {
219                let off = addr as usize & (self.chr.len() - 1);
220                self.chr[off] = value;
221            }
222            0x2000..=0x3EFF => {
223                let off = nametable_offset(addr, self.mirroring);
224                self.vram[off] = value;
225            }
226            _ => {}
227        }
228    }
229
230    fn notify_cpu_cycle(&mut self) {
231        if !self.irq_enabled {
232            return;
233        }
234        self.irq_counter += 1;
235        if self.irq_counter >= 0x8000 {
236            self.irq_counter -= 0x8000;
237        }
238        self.irq_pending = self.irq_counter >= 0x6000;
239    }
240
241    fn irq_pending(&self) -> bool {
242        self.irq_pending
243    }
244
245    fn irq_acknowledge(&mut self) {
246        self.irq_pending = false;
247    }
248
249    fn current_mirroring(&self) -> Mirroring {
250        self.mirroring
251    }
252
253    fn save_state(&self) -> Vec<u8> {
254        let chr_ram = if self.chr_is_ram { self.chr.len() } else { 0 };
255        let mut out = Vec::with_capacity(8 + self.vram.len() + chr_ram);
256        out.push(SAVE_STATE_VERSION);
257        out.push(self.prg_ram_bank);
258        out.push(self.chr_bank);
259        out.push(mirroring_to_byte(self.mirroring));
260        out.push((self.irq_counter & 0xFF) as u8);
261        out.push((self.irq_counter >> 8) as u8);
262        out.push(u8::from(self.irq_enabled));
263        out.push(u8::from(self.irq_pending));
264        out.extend_from_slice(&self.vram);
265        if self.chr_is_ram {
266            out.extend_from_slice(&self.chr);
267        }
268        out
269    }
270
271    fn load_state(&mut self, data: &[u8]) -> Result<(), MapperError> {
272        let chr_ram = if self.chr_is_ram { self.chr.len() } else { 0 };
273        let expected = 8 + self.vram.len() + chr_ram;
274        if data.len() != expected {
275            return Err(MapperError::WrongLength {
276                expected,
277                got: data.len(),
278            });
279        }
280        if data[0] != SAVE_STATE_VERSION {
281            return Err(MapperError::UnsupportedVersion(data[0]));
282        }
283        self.prg_ram_bank = data[1];
284        self.chr_bank = data[2];
285        self.mirroring = byte_to_mirroring(data[3], self.mirroring);
286        self.irq_counter = u16::from(data[4]) | (u16::from(data[5]) << 8);
287        self.irq_enabled = data[6] != 0;
288        self.irq_pending = data[7] != 0;
289        let mut cursor = 8;
290        self.vram
291            .copy_from_slice(&data[cursor..cursor + self.vram.len()]);
292        cursor += self.vram.len();
293        if self.chr_is_ram {
294            self.chr
295                .copy_from_slice(&data[cursor..cursor + self.chr.len()]);
296        }
297        Ok(())
298    }
299}
300
301#[cfg(test)]
302mod tests {
303    use super::*;
304
305    fn synth_prg_8k(banks: usize) -> Box<[u8]> {
306        let mut v = vec![0xFFu8; banks * PRG_BANK_8K];
307        for b in 0..banks {
308            v[b * PRG_BANK_8K] = b as u8;
309        }
310        v.into_boxed_slice()
311    }
312
313    fn synth_chr_8k(banks: usize) -> Box<[u8]> {
314        let mut v = vec![0u8; banks * CHR_BANK_8K];
315        for b in 0..banks {
316            v[b * CHR_BANK_8K] = b as u8;
317        }
318        v.into_boxed_slice()
319    }
320
321    #[test]
322    fn m42_fixed_tail_and_switchable_window() {
323        let mut m = Mapper42::new(synth_prg_8k(8), synth_chr_8k(2), Mirroring::Vertical).unwrap();
324        assert_eq!(m.cpu_read(0x8000), 4);
325        assert_eq!(m.cpu_read(0xA000), 5);
326        assert_eq!(m.cpu_read(0xC000), 6);
327        assert_eq!(m.cpu_read(0xE000), 7);
328        m.cpu_write(0xE000, 3);
329        assert_eq!(m.cpu_read(0x6000), 3);
330        m.cpu_write(0x8000, 1);
331        assert_eq!(m.ppu_read(0x0000), 1);
332    }
333
334    #[test]
335    fn m42_irq_window() {
336        let mut m = Mapper42::new(synth_prg_8k(8), synth_chr_8k(1), Mirroring::Vertical).unwrap();
337        m.cpu_write(0xE002, 0x02); // enable
338        let mut fired = false;
339        for _ in 0..0x8000 {
340            m.notify_cpu_cycle();
341            if m.irq_pending() {
342                fired = true;
343                break;
344            }
345        }
346        assert!(fired);
347        m.cpu_write(0xE002, 0x00); // disable + clear
348        assert!(!m.irq_pending());
349    }
350
351    #[test]
352    fn m42_save_state_round_trip() {
353        let mut m = Mapper42::new(synth_prg_8k(8), synth_chr_8k(2), Mirroring::Vertical).unwrap();
354        m.cpu_write(0xE000, 2);
355        m.cpu_write(0x8000, 1);
356        m.cpu_write(0xE002, 0x02);
357        m.notify_cpu_cycle();
358        let blob = m.save_state();
359        let mut m2 = Mapper42::new(synth_prg_8k(8), synth_chr_8k(2), Mirroring::Vertical).unwrap();
360        m2.load_state(&blob).unwrap();
361        assert_eq!(m2.cpu_read(0x6000), 2);
362        assert_eq!(m2.ppu_read(0x0000), 1);
363    }
364}