Skip to main content

rustynes_mappers/
m077_irem_napoleon.rs

1//! Irem, Napoleon Senki (mapper 77).
2//!
3//! A one-register board whose interest is its memory layout rather than its
4//! logic: CHR banks at 2 KiB granularity, and four nametables of *real* VRAM
5//! wired on the cartridge. That makes its nametable handling four-screen
6//! rather than the usual mirrored pair -- the console's own 2 KiB of CIRAM is
7//! bypassed for the upper half.
8//!
9//! A best-effort (Tier-2) board: register-decode correctness verified against
10//! the reference emulators (`Mesen2`, `GeraNES`) and the nesdev wiki, with no
11//! commercial-oracle ROM in the tree. Banking math is direct slice indexing and
12//! every bank select wraps with `% count`, so a register write can never index
13//! out of bounds -- required for the `#![no_std]` chip stack, which cannot
14//! afford a panic on a register access.
15//!
16//! See `tier.rs` (`MapperTier::BestEffort`), `docs/adr/0011-mapper-tiering.md`,
17//! and `docs/mappers.md` §Mapper coverage matrix.
18
19#![allow(
20    clippy::bool_to_int_with_if,
21    clippy::cast_lossless,
22    clippy::cast_possible_truncation,
23    clippy::doc_markdown,
24    clippy::match_same_arms,
25    clippy::missing_const_for_fn,
26    clippy::similar_names,
27    clippy::struct_excessive_bools,
28    clippy::too_many_lines,
29    clippy::unreadable_literal
30)]
31
32use crate::cartridge::Mirroring;
33use crate::mapper::{Mapper, MapperCaps, MapperError};
34use alloc::{boxed::Box, vec::Vec};
35use alloc::{format, vec};
36
37const PRG_BANK_32K: usize = 0x8000;
38const CHR_BANK_2K: usize = 0x0800;
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
48/// Mapper 77 (Irem, Napoleon Senki).
49pub struct Irem77 {
50    prg_rom: Box<[u8]>,
51    chr_rom: Box<[u8]>,
52    /// CHR RAM for $0800-$1FFF (6 KiB).
53    chr_ram: Box<[u8]>,
54    /// 4 KiB on-cart nametable RAM (four 1 KiB screens).
55    nt_ram: Box<[u8]>,
56    prg_bank: u8,
57    chr_bank: u8,
58}
59
60impl Irem77 {
61    /// Construct a new mapper 77 board.
62    ///
63    /// # Errors
64    ///
65    /// Returns [`MapperError::Invalid`] when PRG is not a non-zero multiple of
66    /// 32 KiB or CHR-ROM is empty / not a multiple of 2 KiB.
67    pub fn new(prg_rom: Box<[u8]>, chr_rom: Box<[u8]>) -> Result<Self, MapperError> {
68        if prg_rom.is_empty() || !prg_rom.len().is_multiple_of(PRG_BANK_32K) {
69            return Err(MapperError::Invalid(format!(
70                "mapper 77 PRG-ROM size {} is not a non-zero multiple of 32 KiB",
71                prg_rom.len()
72            )));
73        }
74        if chr_rom.is_empty() || !chr_rom.len().is_multiple_of(CHR_BANK_2K) {
75            return Err(MapperError::Invalid(format!(
76                "mapper 77 CHR-ROM size {} is not a non-zero multiple of 2 KiB",
77                chr_rom.len()
78            )));
79        }
80        Ok(Self {
81            prg_rom,
82            chr_rom,
83            chr_ram: vec![0u8; 0x1800].into_boxed_slice(), // $0800-$1FFF = 6 KiB
84            nt_ram: vec![0u8; 4 * NAMETABLE_SIZE].into_boxed_slice(),
85            prg_bank: 0,
86            chr_bank: 0,
87        })
88    }
89
90    fn read_prg(&self, addr: u16) -> u8 {
91        let count = (self.prg_rom.len() / PRG_BANK_32K).max(1);
92        let bank = (self.prg_bank as usize) % count;
93        self.prg_rom[bank * PRG_BANK_32K + (addr as usize - 0x8000)]
94    }
95
96    /// Map a $2000-$3EFF nametable address to a 4 KiB on-cart RAM offset.
97    const fn nt_offset(addr: u16) -> usize {
98        let table = (((addr - 0x2000) / NAMETABLE_SIZE_U16) & 0x03) as usize;
99        let local = (addr as usize) & (NAMETABLE_SIZE - 1);
100        table * NAMETABLE_SIZE + local
101    }
102}
103
104impl Mapper for Irem77 {
105    fn caps(&self) -> MapperCaps {
106        MapperCaps::NONE
107    }
108
109    fn cpu_read(&mut self, addr: u16) -> u8 {
110        if (0x8000..=0xFFFF).contains(&addr) {
111            self.read_prg(addr)
112        } else {
113            0
114        }
115    }
116
117    fn cpu_write(&mut self, addr: u16, value: u8) {
118        if (0x8000..=0xFFFF).contains(&addr) {
119            // Bus conflict: AND with the underlying PRG byte.
120            let effective = value & self.read_prg(addr);
121            self.prg_bank = effective & 0x0F;
122            self.chr_bank = (effective >> 4) & 0x0F;
123        }
124    }
125
126    fn ppu_read(&mut self, addr: u16) -> u8 {
127        let addr = addr & 0x3FFF;
128        match addr {
129            0x0000..=0x07FF => {
130                // Bottom 2 KiB: switchable CHR-ROM bank.
131                let count = (self.chr_rom.len() / CHR_BANK_2K).max(1);
132                let bank = (self.chr_bank as usize) % count;
133                self.chr_rom[bank * CHR_BANK_2K + addr as usize]
134            }
135            0x0800..=0x1FFF => self.chr_ram[addr as usize - 0x0800],
136            0x2000..=0x3EFF => self.nt_ram[Self::nt_offset(addr)],
137            _ => 0,
138        }
139    }
140
141    fn ppu_write(&mut self, addr: u16, value: u8) {
142        let addr = addr & 0x3FFF;
143        // $0000-$07FF is CHR-ROM (read-only); everything else is RAM.
144        match addr {
145            0x0800..=0x1FFF => self.chr_ram[addr as usize - 0x0800] = value,
146            0x2000..=0x3EFF => self.nt_ram[Self::nt_offset(addr)] = value,
147            _ => {}
148        }
149    }
150
151    fn nametable_fetch(&mut self, addr: u16) -> Option<u8> {
152        // Consume the nametable read from on-cart 4-screen RAM.
153        Some(self.nt_ram[Self::nt_offset(addr)])
154    }
155
156    fn nametable_write(&mut self, addr: u16, value: u8) -> bool {
157        self.nt_ram[Self::nt_offset(addr)] = value;
158        true
159    }
160
161    fn current_mirroring(&self) -> Mirroring {
162        Mirroring::FourScreen
163    }
164
165    fn save_state(&self) -> Vec<u8> {
166        let mut out = Vec::with_capacity(3 + self.chr_ram.len() + self.nt_ram.len());
167        out.push(SAVE_STATE_VERSION);
168        out.push(self.prg_bank);
169        out.push(self.chr_bank);
170        out.extend_from_slice(&self.chr_ram);
171        out.extend_from_slice(&self.nt_ram);
172        out
173    }
174
175    fn load_state(&mut self, data: &[u8]) -> Result<(), MapperError> {
176        let expected = 3 + self.chr_ram.len() + self.nt_ram.len();
177        if data.len() != expected {
178            return Err(MapperError::WrongLength {
179                expected,
180                got: data.len(),
181            });
182        }
183        if data[0] != SAVE_STATE_VERSION {
184            return Err(MapperError::UnsupportedVersion(data[0]));
185        }
186        self.prg_bank = data[1];
187        self.chr_bank = data[2];
188        let mut cursor = 3;
189        self.chr_ram
190            .copy_from_slice(&data[cursor..cursor + self.chr_ram.len()]);
191        cursor += self.chr_ram.len();
192        self.nt_ram
193            .copy_from_slice(&data[cursor..cursor + self.nt_ram.len()]);
194        Ok(())
195    }
196}
197
198#[cfg(test)]
199mod tests {
200    use super::*;
201
202    fn synth_prg_32k(banks: usize) -> Box<[u8]> {
203        let mut v = vec![0xFFu8; banks * PRG_BANK_32K];
204        for b in 0..banks {
205            v[b * PRG_BANK_32K] = b as u8;
206        }
207        v.into_boxed_slice()
208    }
209
210    fn synth_chr_2k(banks: usize) -> Box<[u8]> {
211        let mut v = vec![0u8; banks * CHR_BANK_2K];
212        for b in 0..banks {
213            v[b * CHR_BANK_2K] = b as u8;
214        }
215        v.into_boxed_slice()
216    }
217
218    #[test]
219    fn m77_prg_and_2k_chr_with_bus_conflict() {
220        let mut m = Irem77::new(synth_prg_32k(4), synth_chr_2k(16)).unwrap();
221        // Write to $8001 (byte 0xFF, transparent). [CCCC PPPP] = 0b0011_0010.
222        // PRG = 2, CHR (2 KiB at $0000) = 3.
223        m.cpu_write(0x8001, 0b0011_0010);
224        assert_eq!(m.cpu_read(0x8000), 2);
225        assert_eq!(m.ppu_read(0x0000), 3);
226        assert_eq!(m.current_mirroring(), Mirroring::FourScreen);
227    }
228
229    #[test]
230    fn m77_chr_ram_and_four_screen_nt() {
231        let mut m = Irem77::new(synth_prg_32k(2), synth_chr_2k(8)).unwrap();
232        // $0800-$1FFF is CHR-RAM.
233        m.ppu_write(0x0800, 0xAB);
234        assert_eq!(m.ppu_read(0x0800), 0xAB);
235        // Four independent nametables in on-cart RAM via the hooks.
236        assert!(m.nametable_write(0x2000, 0x11));
237        assert!(m.nametable_write(0x2400, 0x22));
238        assert!(m.nametable_write(0x2800, 0x33));
239        assert!(m.nametable_write(0x2C00, 0x44));
240        assert_eq!(m.nametable_fetch(0x2000), Some(0x11));
241        assert_eq!(m.nametable_fetch(0x2400), Some(0x22));
242        assert_eq!(m.nametable_fetch(0x2800), Some(0x33));
243        assert_eq!(m.nametable_fetch(0x2C00), Some(0x44));
244    }
245}