Skip to main content

rustynes_mappers/
m107_magic_dragon107.rs

1//! Magic Dragon (mapper 107) -- Magicseries pirate board.
2//!
3//! One write-anywhere register in `$8000-$FFFF` carrying both bank fields:
4//! the low bit selects a 32 KiB PRG bank and the upper bits an 8 KiB CHR
5//! bank.
6//!
7//! A best-effort (Tier-2) board: register-decode correctness verified against
8//! the `GeraNES` reference emulator (cross-referenced, not copied)
9//! and the nesdev wiki, with no commercial-oracle ROM in the tree. Banking math
10//! is direct slice indexing and every bank select wraps with `% count`, so a
11//! register write can never index out of bounds -- required for the `#![no_std]`
12//! chip stack, which cannot afford a panic on a register access.
13//!
14//! See `tier.rs` (`MapperTier::BestEffort`), `docs/adr/0011-mapper-tiering.md`,
15//! and `docs/mappers.md` §Mapper coverage matrix.
16
17use crate::cartridge::Mirroring;
18use crate::mapper::{Mapper, MapperCaps, MapperError};
19use alloc::{boxed::Box, vec::Vec};
20use alloc::{format, vec};
21
22const PRG_BANK_32K: usize = 0x8000;
23const CHR_BANK_8K: usize = 0x2000;
24const NAMETABLE_SIZE: usize = 0x0400;
25const NAMETABLE_SIZE_U16: u16 = 0x0400;
26
27const SAVE_STATE_VERSION: u8 = 1;
28
29// ---------------------------------------------------------------------------
30// Shared nametable helper (mirrors the one in the other simple-mapper modules).
31// ---------------------------------------------------------------------------
32
33const fn nametable_offset(addr: u16, mirroring: Mirroring) -> usize {
34    let table = (((addr - 0x2000) / NAMETABLE_SIZE_U16) & 0x03) as u8;
35    let local = (addr as usize) & (NAMETABLE_SIZE - 1);
36    let physical = mirroring.physical_bank(table);
37    physical * NAMETABLE_SIZE + local
38}
39
40/// Mapper 107 (Magic Dragon).
41pub struct MagicDragon107 {
42    prg_rom: Box<[u8]>,
43    chr_rom: Box<[u8]>,
44    vram: Box<[u8]>,
45    prg_bank: u8,
46    chr_bank: u8,
47    mirroring: Mirroring,
48}
49
50impl MagicDragon107 {
51    /// Construct a new mapper 107 board.
52    ///
53    /// # Errors
54    ///
55    /// Returns [`MapperError::Invalid`] when PRG is not a non-zero multiple of
56    /// 32 KiB or CHR-ROM is empty / not a multiple of 8 KiB.
57    pub fn new(
58        prg_rom: Box<[u8]>,
59        chr_rom: Box<[u8]>,
60        mirroring: Mirroring,
61    ) -> Result<Self, MapperError> {
62        if prg_rom.is_empty() || !prg_rom.len().is_multiple_of(PRG_BANK_32K) {
63            return Err(MapperError::Invalid(format!(
64                "mapper 107 PRG-ROM size {} is not a non-zero multiple of 32 KiB",
65                prg_rom.len()
66            )));
67        }
68        if chr_rom.is_empty() || !chr_rom.len().is_multiple_of(CHR_BANK_8K) {
69            return Err(MapperError::Invalid(format!(
70                "mapper 107 CHR-ROM size {} is not a non-zero multiple of 8 KiB",
71                chr_rom.len()
72            )));
73        }
74        Ok(Self {
75            prg_rom,
76            chr_rom,
77            vram: vec![0u8; 2 * NAMETABLE_SIZE].into_boxed_slice(),
78            prg_bank: 0,
79            chr_bank: 0,
80            mirroring,
81        })
82    }
83}
84
85impl Mapper for MagicDragon107 {
86    fn caps(&self) -> MapperCaps {
87        MapperCaps::NONE
88    }
89
90    fn cpu_read(&mut self, addr: u16) -> u8 {
91        if (0x8000..=0xFFFF).contains(&addr) {
92            let count = (self.prg_rom.len() / PRG_BANK_32K).max(1);
93            let bank = (self.prg_bank as usize) % count;
94            self.prg_rom[bank * PRG_BANK_32K + (addr as usize - 0x8000)]
95        } else {
96            0
97        }
98    }
99
100    fn cpu_write(&mut self, addr: u16, value: u8) {
101        if (0x8000..=0xFFFF).contains(&addr) {
102            self.prg_bank = value >> 1;
103            self.chr_bank = value;
104        }
105    }
106
107    fn ppu_read(&mut self, addr: u16) -> u8 {
108        let addr = addr & 0x3FFF;
109        match addr {
110            0x0000..=0x1FFF => {
111                let count = (self.chr_rom.len() / CHR_BANK_8K).max(1);
112                let bank = (self.chr_bank as usize) % count;
113                self.chr_rom[bank * CHR_BANK_8K + addr as usize]
114            }
115            0x2000..=0x3EFF => self.vram[nametable_offset(addr, self.mirroring)],
116            _ => 0,
117        }
118    }
119
120    fn ppu_write(&mut self, addr: u16, value: u8) {
121        let addr = addr & 0x3FFF;
122        if let 0x2000..=0x3EFF = addr {
123            let off = nametable_offset(addr, self.mirroring);
124            self.vram[off] = value;
125        }
126    }
127
128    fn current_mirroring(&self) -> Mirroring {
129        self.mirroring
130    }
131
132    fn save_state(&self) -> Vec<u8> {
133        let mut out = Vec::with_capacity(3 + self.vram.len());
134        out.push(SAVE_STATE_VERSION);
135        out.push(self.prg_bank);
136        out.push(self.chr_bank);
137        out.extend_from_slice(&self.vram);
138        out
139    }
140
141    fn load_state(&mut self, data: &[u8]) -> Result<(), MapperError> {
142        let expected = 3 + self.vram.len();
143        if data.len() != expected {
144            return Err(MapperError::WrongLength {
145                expected,
146                got: data.len(),
147            });
148        }
149        if data[0] != SAVE_STATE_VERSION {
150            return Err(MapperError::UnsupportedVersion(data[0]));
151        }
152        self.prg_bank = data[1];
153        self.chr_bank = data[2];
154        self.vram.copy_from_slice(&data[3..3 + self.vram.len()]);
155        Ok(())
156    }
157}
158
159#[cfg(test)]
160#[allow(clippy::cast_possible_truncation)]
161mod tests {
162    use super::*;
163
164    fn synth_prg_32k(banks: usize) -> Box<[u8]> {
165        let mut v = vec![0xFFu8; banks * PRG_BANK_32K];
166        for b in 0..banks {
167            v[b * PRG_BANK_32K] = b as u8;
168        }
169        v.into_boxed_slice()
170    }
171
172    fn synth_chr_8k(banks: usize) -> Box<[u8]> {
173        let mut v = vec![0u8; banks * CHR_BANK_8K];
174        for b in 0..banks {
175            v[b * CHR_BANK_8K] = b as u8;
176        }
177        v.into_boxed_slice()
178    }
179
180    #[test]
181    fn m107_latch_selects_prg_and_chr() {
182        let mut m =
183            MagicDragon107::new(synth_prg_32k(4), synth_chr_8k(8), Mirroring::Vertical).unwrap();
184        // value 6: PRG = 6>>1 = 3; CHR = 6.
185        m.cpu_write(0x8000, 6);
186        assert_eq!(m.cpu_read(0x8000), 3);
187        assert_eq!(m.ppu_read(0x0000), 6);
188    }
189
190    #[test]
191    fn m107_save_state_round_trip() {
192        let mut m =
193            MagicDragon107::new(synth_prg_32k(4), synth_chr_8k(8), Mirroring::Vertical).unwrap();
194        m.cpu_write(0x8000, 4); // PRG 2, CHR 4
195        let blob = m.save_state();
196        let mut m2 =
197            MagicDragon107::new(synth_prg_32k(4), synth_chr_8k(8), Mirroring::Vertical).unwrap();
198        m2.load_state(&blob).unwrap();
199        assert_eq!(m2.cpu_read(0x8000), 2);
200        assert_eq!(m2.ppu_read(0x0000), 4);
201    }
202}