Skip to main content

rustynes_mappers/
m097_irem_tam_s1.rs

1//! Irem `TAM-S1`, Kaiketsu Yanchamaru (mapper 97).
2//!
3//! Inverts the normal `UxROM` arrangement: the *first* 16 KiB is fixed and the
4//! *second* switchable. That matters because the 6502 reset and interrupt
5//! vectors live at the top of the address space -- on this board they sit in
6//! the switchable half, so the bank in place at reset is load-bearing.
7//!
8//! A best-effort (Tier-2) board: register-decode correctness verified against
9//! the reference emulators (`Mesen2`, `GeraNES`) and the nesdev wiki, with no
10//! commercial-oracle ROM in the tree. Banking math is direct slice indexing and
11//! every bank select wraps with `% count`, so a register write can never index
12//! out of bounds -- required for the `#![no_std]` chip stack, which cannot
13//! afford a panic on a register access.
14//!
15//! See `tier.rs` (`MapperTier::BestEffort`), `docs/adr/0011-mapper-tiering.md`,
16//! and `docs/mappers.md` §Mapper coverage matrix.
17
18#![allow(
19    clippy::bool_to_int_with_if,
20    clippy::cast_lossless,
21    clippy::cast_possible_truncation,
22    clippy::doc_markdown,
23    clippy::match_same_arms,
24    clippy::missing_const_for_fn,
25    clippy::similar_names,
26    clippy::struct_excessive_bools,
27    clippy::too_many_lines,
28    clippy::unreadable_literal
29)]
30
31use crate::cartridge::Mirroring;
32use crate::mapper::{Mapper, MapperCaps, MapperError};
33use alloc::{boxed::Box, vec::Vec};
34use alloc::{format, vec};
35
36const PRG_BANK_16K: usize = 0x4000;
37const CHR_BANK_8K: usize = 0x2000;
38const NAMETABLE_SIZE: usize = 0x0400;
39const NAMETABLE_SIZE_U16: u16 = 0x0400;
40
41const SAVE_STATE_VERSION: u8 = 1;
42
43// ---------------------------------------------------------------------------
44// Shared nametable helper (mirrors the one in the other simple-mapper modules).
45// ---------------------------------------------------------------------------
46
47const fn nametable_offset(addr: u16, mirroring: Mirroring) -> usize {
48    let table = (((addr - 0x2000) / NAMETABLE_SIZE_U16) & 0x03) as u8;
49    let local = (addr as usize) & (NAMETABLE_SIZE - 1);
50    let physical = mirroring.physical_bank(table);
51    physical * NAMETABLE_SIZE + local
52}
53
54/// Mapper 97 (Irem `TAM-S1`, Kaiketsu Yanchamaru).
55pub struct Irem97 {
56    prg_rom: Box<[u8]>,
57    chr: Box<[u8]>,
58    vram: Box<[u8]>,
59    chr_is_ram: bool,
60    prg_bank: u8,
61    vertical_mirroring: bool,
62}
63
64impl Irem97 {
65    /// Construct a new mapper 97 board.
66    ///
67    /// # Errors
68    ///
69    /// Returns [`MapperError::Invalid`] when PRG is not a non-zero multiple of
70    /// 16 KiB, or CHR-ROM (when present) is not a multiple of 8 KiB.
71    pub fn new(
72        prg_rom: Box<[u8]>,
73        chr_rom: Box<[u8]>,
74        mirroring: Mirroring,
75    ) -> Result<Self, MapperError> {
76        if prg_rom.is_empty() || !prg_rom.len().is_multiple_of(PRG_BANK_16K) {
77            return Err(MapperError::Invalid(format!(
78                "mapper 97 PRG-ROM size {} is not a non-zero multiple of 16 KiB",
79                prg_rom.len()
80            )));
81        }
82        let chr_is_ram = chr_rom.is_empty();
83        let chr: Box<[u8]> = if chr_is_ram {
84            vec![0u8; CHR_BANK_8K].into_boxed_slice()
85        } else if chr_rom.len().is_multiple_of(CHR_BANK_8K) {
86            chr_rom
87        } else {
88            return Err(MapperError::Invalid(format!(
89                "mapper 97 CHR-ROM size {} is not a multiple of 8 KiB",
90                chr_rom.len()
91            )));
92        };
93        Ok(Self {
94            prg_rom,
95            chr,
96            vram: vec![0u8; 2 * NAMETABLE_SIZE].into_boxed_slice(),
97            chr_is_ram,
98            prg_bank: 0,
99            vertical_mirroring: mirroring == Mirroring::Vertical,
100        })
101    }
102
103    fn prg_count_16k(&self) -> usize {
104        (self.prg_rom.len() / PRG_BANK_16K).max(1)
105    }
106}
107
108impl Mapper for Irem97 {
109    fn caps(&self) -> MapperCaps {
110        MapperCaps::NONE
111    }
112
113    fn cpu_read(&mut self, addr: u16) -> u8 {
114        match addr {
115            // $8000-$BFFF fixed to the last 16 KiB bank.
116            0x8000..=0xBFFF => {
117                let last = self.prg_count_16k() - 1;
118                self.prg_rom[last * PRG_BANK_16K + (addr as usize - 0x8000)]
119            }
120            // $C000-$FFFF switchable.
121            0xC000..=0xFFFF => {
122                let bank = (self.prg_bank as usize) % self.prg_count_16k();
123                self.prg_rom[bank * PRG_BANK_16K + (addr as usize - 0xC000)]
124            }
125            _ => 0,
126        }
127    }
128
129    fn cpu_write(&mut self, addr: u16, value: u8) {
130        if (0x8000..=0xFFFF).contains(&addr) {
131            self.prg_bank = value & 0x1F;
132            self.vertical_mirroring = (value & 0x80) != 0;
133        }
134    }
135
136    fn ppu_read(&mut self, addr: u16) -> u8 {
137        let addr = addr & 0x3FFF;
138        match addr {
139            0x0000..=0x1FFF => self.chr[addr as usize],
140            0x2000..=0x3EFF => self.vram[nametable_offset(addr, self.current_mirroring())],
141            _ => 0,
142        }
143    }
144
145    fn ppu_write(&mut self, addr: u16, value: u8) {
146        let addr = addr & 0x3FFF;
147        match addr {
148            0x0000..=0x1FFF => {
149                if self.chr_is_ram {
150                    self.chr[addr as usize] = value;
151                }
152            }
153            0x2000..=0x3EFF => {
154                let off = nametable_offset(addr, self.current_mirroring());
155                self.vram[off] = value;
156            }
157            _ => {}
158        }
159    }
160
161    fn current_mirroring(&self) -> Mirroring {
162        if self.vertical_mirroring {
163            Mirroring::Vertical
164        } else {
165            Mirroring::Horizontal
166        }
167    }
168
169    fn save_state(&self) -> Vec<u8> {
170        let chr_extra = if self.chr_is_ram { self.chr.len() } else { 0 };
171        let mut out = Vec::with_capacity(3 + self.vram.len() + chr_extra);
172        out.push(SAVE_STATE_VERSION);
173        out.push(self.prg_bank);
174        out.push(u8::from(self.vertical_mirroring));
175        out.extend_from_slice(&self.vram);
176        if self.chr_is_ram {
177            out.extend_from_slice(&self.chr);
178        }
179        out
180    }
181
182    fn load_state(&mut self, data: &[u8]) -> Result<(), MapperError> {
183        let chr_extra = if self.chr_is_ram { self.chr.len() } else { 0 };
184        let expected = 3 + self.vram.len() + chr_extra;
185        if data.len() != expected {
186            return Err(MapperError::WrongLength {
187                expected,
188                got: data.len(),
189            });
190        }
191        if data[0] != SAVE_STATE_VERSION {
192            return Err(MapperError::UnsupportedVersion(data[0]));
193        }
194        self.prg_bank = data[1];
195        self.vertical_mirroring = data[2] != 0;
196        let mut cursor = 3;
197        self.vram
198            .copy_from_slice(&data[cursor..cursor + self.vram.len()]);
199        cursor += self.vram.len();
200        if self.chr_is_ram {
201            self.chr
202                .copy_from_slice(&data[cursor..cursor + self.chr.len()]);
203        }
204        Ok(())
205    }
206}
207
208#[cfg(test)]
209mod tests {
210    use super::*;
211
212    fn synth_prg_16k(banks: usize) -> Box<[u8]> {
213        let mut v = vec![0xFFu8; banks * PRG_BANK_16K];
214        for b in 0..banks {
215            v[b * PRG_BANK_16K] = b as u8;
216        }
217        v.into_boxed_slice()
218    }
219
220    fn synth_chr_8k(banks: usize) -> Box<[u8]> {
221        let mut v = vec![0u8; banks * CHR_BANK_8K];
222        for b in 0..banks {
223            v[b * CHR_BANK_8K] = b as u8;
224        }
225        v.into_boxed_slice()
226    }
227
228    #[test]
229    fn m97_fixed_first_switchable_second() {
230        let mut m = Irem97::new(synth_prg_16k(8), synth_chr_8k(1), Mirroring::Horizontal).unwrap();
231        // $8000-$BFFF fixed to last bank (7).
232        assert_eq!(m.cpu_read(0x8000), 7);
233        // Switch $C000 bank to 3, set vertical mirroring (bit 7).
234        m.cpu_write(0x8000, 0b1000_0011);
235        assert_eq!(m.cpu_read(0xC000), 3);
236        assert_eq!(m.current_mirroring(), Mirroring::Vertical);
237        // $8000 still fixed.
238        assert_eq!(m.cpu_read(0x8000), 7);
239    }
240}