Skip to main content

rustynes_mappers/
m032_irem_g101.rs

1//! Irem G-101 (iNES mapper 32) implementation.
2//!
3//! An Irem ASIC board (Image Fight, Major League, Kaiketsu Yancha Maru 2,
4//! Magical Pop's, etc.). Two switchable 8 KiB PRG banks with a software-
5//! selectable swap mode, eight switchable 1 KiB CHR banks, and a software
6//! H/V mirroring control. There is **no IRQ**.
7//!
8//! Register map (nesdev `INES_Mapper_032.xhtml`):
9//!
10//! ```text
11//!   $8000-$8FFF [..PP PPPP]  PRG reg 0 (8 KiB) — to $8000 (mode 0) or $C000 (mode 1)
12//!   $9000-$9FFF [.... ..MP]  M = mirroring (0 = Vertical, 1 = Horizontal)
13//!                            P = PRG swap mode (0 = $8000 swappable, $C000 fixed;
14//!                                               1 = $C000 swappable, $8000 fixed)
15//!   $A000-$AFFF [..PP PPPP]  PRG reg 1 (8 KiB) — always to $A000
16//!   $B000-$B007 [CCCC CCCC]  CHR reg 0..7 (1 KiB each, $0000..$1C00)
17//! ```
18//!
19//! Slot layout:
20//!
21//! - Mode 0: `$8000` = PRG reg 0, `$A000` = PRG reg 1, `$C000` = fixed {-2},
22//!   `$E000` = fixed {-1}.
23//! - Mode 1: `$8000` = fixed {-2}, `$A000` = PRG reg 1, `$C000` = PRG reg 0,
24//!   `$E000` = fixed {-1}.
25//!
26//! **Submapper 1** (Major League) is the special case where the board hard-wires
27//! one-screen (single-screen A) mirroring and ignores the `$9000` mirroring bit.
28//! When the header carries submapper 1 (or, lacking a submapper, we detect the
29//! known Major League title via a forced flag from the dispatch) the mapper
30//! locks single-screen A.
31//!
32//! See `docs/mappers.md` §Mapper coverage matrix.
33
34#![allow(clippy::cast_possible_truncation, clippy::doc_markdown)]
35
36use crate::cartridge::Mirroring;
37use crate::mapper::{Mapper, MapperCaps, MapperError};
38use alloc::{boxed::Box, vec::Vec};
39use alloc::{format, vec};
40
41const PRG_BANK_8K: usize = 0x2000;
42const CHR_BANK_1K: usize = 0x0400;
43const NAMETABLE_SIZE: usize = 0x0400;
44const NAMETABLE_SIZE_U16: u16 = 0x0400;
45
46const SAVE_STATE_VERSION: u8 = 1;
47
48/// Irem G-101 mapper (iNES mapper 32).
49pub struct IremG101 {
50    prg_rom: Box<[u8]>,
51    chr: Box<[u8]>,
52    vram: Box<[u8]>,
53    chr_is_ram: bool,
54    /// PRG reg 0 ($8000 writes) and PRG reg 1 ($A000 writes).
55    prg_bank: [u8; 2],
56    /// Eight 1 KiB CHR banks ($B000-$B007).
57    chr_1k: [u8; 8],
58    /// PRG swap mode ($9000 bit 1): false = $8000 swappable, true = $C000.
59    prg_swap_mode: bool,
60    mirroring: Mirroring,
61    /// Submapper-1 boards (Major League) force single-screen A and ignore the
62    /// $9000 mirroring bit.
63    force_one_screen: bool,
64}
65
66impl IremG101 {
67    /// Construct a new Irem G-101 mapper.
68    ///
69    /// `prg_rom` must be a non-zero multiple of 8 KiB. CHR-RAM is selected when
70    /// `chr_rom` is empty; otherwise CHR-ROM length must be a multiple of 1 KiB.
71    /// `force_one_screen` locks single-screen A mirroring (submapper 1 / Major
72    /// League).
73    ///
74    /// # Errors
75    ///
76    /// Returns [`MapperError::Invalid`] when sizes don't match the constraints.
77    pub fn new(
78        prg_rom: Box<[u8]>,
79        chr_rom: Box<[u8]>,
80        mirroring: Mirroring,
81        force_one_screen: bool,
82    ) -> Result<Self, MapperError> {
83        if prg_rom.is_empty() || !prg_rom.len().is_multiple_of(PRG_BANK_8K) {
84            return Err(MapperError::Invalid(format!(
85                "Irem-G101 PRG-ROM size {} is not a non-zero multiple of 8 KiB",
86                prg_rom.len()
87            )));
88        }
89        let chr_is_ram = chr_rom.is_empty();
90        let chr: Box<[u8]> = if chr_is_ram {
91            vec![0u8; 8 * CHR_BANK_1K].into_boxed_slice()
92        } else if chr_rom.len().is_multiple_of(CHR_BANK_1K) {
93            chr_rom
94        } else {
95            return Err(MapperError::Invalid(format!(
96                "Irem-G101 expects a 1 KiB multiple of CHR; got {} bytes",
97                chr_rom.len()
98            )));
99        };
100        let mirroring = if force_one_screen {
101            Mirroring::SingleScreenA
102        } else {
103            mirroring
104        };
105        Ok(Self {
106            prg_rom,
107            chr,
108            vram: vec![0u8; 2 * NAMETABLE_SIZE].into_boxed_slice(),
109            chr_is_ram,
110            prg_bank: [0, 0],
111            chr_1k: [0, 0, 0, 0, 0, 0, 0, 0],
112            prg_swap_mode: false,
113            mirroring,
114            force_one_screen,
115        })
116    }
117
118    const fn nametable_offset(&self, addr: u16) -> usize {
119        let table = (((addr - 0x2000) / NAMETABLE_SIZE_U16) & 0x03) as u8;
120        let local = (addr as usize) & (NAMETABLE_SIZE - 1);
121        let physical = self.mirroring.physical_bank(table);
122        physical * NAMETABLE_SIZE + local
123    }
124
125    fn read_prg(&self, addr: u16) -> u8 {
126        let bank_count = (self.prg_rom.len() / PRG_BANK_8K).max(1);
127        // `saturating_sub`, not `-`: the constructor admits any non-zero 8 KiB
128        // multiple, so `bank_count == 1` is reachable from a crafted iNES image
129        // and a bare `bank_count - 2` underflows. Release builds survive only
130        // because the wrap happens to land back in range after `% bank_count`;
131        // overflow-checked builds panic outright on an untrusted-ROM read path.
132        // Same fixed-tail idiom as `m088_namco118.rs` / `m004_mmc3.rs`.
133        let fixed_second_last = bank_count.saturating_sub(2);
134        let slot = (addr >> 13) & 0x03; // 0=$8000,1=$A000,2=$C000,3=$E000
135        let bank = match slot {
136            0 => {
137                if self.prg_swap_mode {
138                    fixed_second_last // mode 1: $8000 fixed {-2}
139                } else {
140                    self.prg_bank[0] as usize // mode 0: $8000 swappable
141                }
142            }
143            1 => self.prg_bank[1] as usize, // $A000 always swappable
144            2 => {
145                if self.prg_swap_mode {
146                    self.prg_bank[0] as usize // mode 1: $C000 swappable
147                } else {
148                    fixed_second_last // mode 0: $C000 fixed {-2}
149                }
150            }
151            _ => bank_count - 1, // $E000 always fixed {-1}: `.max(1)` makes this safe
152        } % bank_count;
153        let off = (addr as usize) & (PRG_BANK_8K - 1);
154        self.prg_rom[bank * PRG_BANK_8K + off]
155    }
156
157    fn chr_offset(&self, addr: u16) -> usize {
158        let len = self.chr.len().max(1);
159        let idx = ((addr >> 10) & 0x07) as usize; // 0..=7 over $0000-$1FFF
160        let base = (self.chr_1k[idx] as usize) * CHR_BANK_1K;
161        (base + (addr as usize & (CHR_BANK_1K - 1))) % len
162    }
163}
164
165impl Mapper for IremG101 {
166    // v2.8.0 Phase 4 — no per-cycle hooks (no IRQ, no audio): the bus
167    // skips all four per-CPU-cycle dispatches for this board.
168    fn caps(&self) -> MapperCaps {
169        MapperCaps::NONE
170    }
171
172    fn cpu_read(&mut self, addr: u16) -> u8 {
173        if (0x8000..=0xFFFF).contains(&addr) {
174            self.read_prg(addr)
175        } else {
176            0
177        }
178    }
179
180    fn cpu_write(&mut self, addr: u16, value: u8) {
181        if !(0x8000..=0xFFFF).contains(&addr) {
182            return;
183        }
184        match addr & 0xF000 {
185            0x8000 => self.prg_bank[0] = value & 0x3F,
186            0x9000 => {
187                if !self.force_one_screen {
188                    self.mirroring = if (value & 0x01) != 0 {
189                        Mirroring::Horizontal
190                    } else {
191                        Mirroring::Vertical
192                    };
193                }
194                self.prg_swap_mode = (value & 0x02) != 0;
195            }
196            0xA000 => self.prg_bank[1] = value & 0x3F,
197            0xB000 => {
198                let idx = (addr & 0x07) as usize;
199                self.chr_1k[idx] = value;
200            }
201            _ => {}
202        }
203    }
204
205    fn ppu_read(&mut self, addr: u16) -> u8 {
206        let addr = addr & 0x3FFF;
207        match addr {
208            0x0000..=0x1FFF => self.chr[self.chr_offset(addr)],
209            0x2000..=0x3EFF => self.vram[self.nametable_offset(addr)],
210            _ => 0,
211        }
212    }
213
214    fn ppu_write(&mut self, addr: u16, value: u8) {
215        let addr = addr & 0x3FFF;
216        match addr {
217            0x0000..=0x1FFF => {
218                if self.chr_is_ram {
219                    let off = self.chr_offset(addr);
220                    self.chr[off] = value;
221                }
222            }
223            0x2000..=0x3EFF => {
224                let off = self.nametable_offset(addr);
225                self.vram[off] = value;
226            }
227            _ => {}
228        }
229    }
230
231    fn current_mirroring(&self) -> Mirroring {
232        self.mirroring
233    }
234
235    fn debug_info(&self) -> crate::mapper::MapperDebugInfo {
236        let mut info = crate::mapper::MapperDebugInfo {
237            mapper_id: 32,
238            name: "Irem G-101 (32)".into(),
239            mirroring: crate::mapper::mirroring_name(self.mirroring),
240            ..Default::default()
241        };
242        info.prg_banks
243            .push(("swap".into(), format!("{}", u8::from(self.prg_swap_mode))));
244        for (i, b) in self.prg_bank.iter().enumerate() {
245            info.prg_banks
246                .push((format!("PRG{i}"), format!("{b:#04x}")));
247        }
248        for (i, b) in self.chr_1k.iter().enumerate() {
249            info.chr_banks
250                .push((format!("CHR{i}"), format!("{b:#04x}")));
251        }
252        info
253    }
254
255    fn save_state(&self) -> Vec<u8> {
256        let mut out = Vec::with_capacity(
257            13 + self.vram.len() + if self.chr_is_ram { self.chr.len() } else { 0 },
258        );
259        out.push(SAVE_STATE_VERSION);
260        out.extend_from_slice(&self.prg_bank);
261        out.extend_from_slice(&self.chr_1k);
262        out.push(u8::from(self.prg_swap_mode));
263        out.push(match self.mirroring {
264            Mirroring::Horizontal => 1,
265            Mirroring::SingleScreenA => 2,
266            _ => 0,
267        });
268        out.extend_from_slice(&self.vram);
269        if self.chr_is_ram {
270            out.extend_from_slice(&self.chr);
271        }
272        out
273    }
274
275    fn load_state(&mut self, data: &[u8]) -> Result<(), MapperError> {
276        let need_chr = if self.chr_is_ram { self.chr.len() } else { 0 };
277        let expected = 13 + self.vram.len() + need_chr;
278        if data.len() != expected {
279            return Err(MapperError::WrongLength {
280                expected,
281                got: data.len(),
282            });
283        }
284        if data[0] != SAVE_STATE_VERSION {
285            return Err(MapperError::UnsupportedVersion(data[0]));
286        }
287        self.prg_bank.copy_from_slice(&data[1..3]);
288        self.chr_1k.copy_from_slice(&data[3..11]);
289        self.prg_swap_mode = data[11] != 0;
290        // Honor force_one_screen on restore.
291        self.mirroring = if self.force_one_screen {
292            Mirroring::SingleScreenA
293        } else {
294            match data[12] {
295                1 => Mirroring::Horizontal,
296                _ => Mirroring::Vertical,
297            }
298        };
299        let mut cursor = 13;
300        self.vram
301            .copy_from_slice(&data[cursor..cursor + self.vram.len()]);
302        cursor += self.vram.len();
303        if self.chr_is_ram {
304            self.chr
305                .copy_from_slice(&data[cursor..cursor + self.chr.len()]);
306        }
307        Ok(())
308    }
309}
310
311#[cfg(test)]
312#[allow(clippy::cast_possible_truncation)]
313mod tests {
314    use super::*;
315
316    fn synth_prg(banks_8k: usize) -> Box<[u8]> {
317        let mut v = vec![0u8; banks_8k * PRG_BANK_8K];
318        for b in 0..banks_8k {
319            v[b * PRG_BANK_8K] = b as u8;
320        }
321        v.into_boxed_slice()
322    }
323
324    fn synth_chr_1k(banks: usize) -> Box<[u8]> {
325        let mut v = vec![0u8; banks * CHR_BANK_1K];
326        for b in 0..banks {
327            v[b * CHR_BANK_1K] = b as u8;
328        }
329        v.into_boxed_slice()
330    }
331
332    #[test]
333    fn prg_mode_0_default_and_fixed_tail() {
334        let mut m =
335            IremG101::new(synth_prg(8), synth_chr_1k(8), Mirroring::Vertical, false).unwrap();
336        // Mode 0 default: $8000 = PRG0=0, $A000 = PRG1=0, $C000 = {-2}=6, $E000 = {-1}=7.
337        assert_eq!(m.cpu_read(0x8000), 0);
338        assert_eq!(m.cpu_read(0xA000), 0);
339        assert_eq!(m.cpu_read(0xC000), 6);
340        assert_eq!(m.cpu_read(0xE000), 7);
341        m.cpu_write(0x8000, 0x03);
342        m.cpu_write(0xA000, 0x05);
343        assert_eq!(m.cpu_read(0x8000), 3);
344        assert_eq!(m.cpu_read(0xA000), 5);
345        assert_eq!(m.cpu_read(0xC000), 6);
346        assert_eq!(m.cpu_read(0xE000), 7);
347    }
348
349    #[test]
350    fn prg_swap_mode_1_fixes_8000_swaps_c000() {
351        let mut m =
352            IremG101::new(synth_prg(8), synth_chr_1k(8), Mirroring::Vertical, false).unwrap();
353        m.cpu_write(0x8000, 0x03); // PRG reg 0 = 3
354        m.cpu_write(0x9000, 0x02); // swap mode 1
355        // Mode 1: $8000 = {-2}=6, $C000 = PRG reg 0 = 3, $E000 = {-1}=7.
356        assert_eq!(m.cpu_read(0x8000), 6);
357        assert_eq!(m.cpu_read(0xC000), 3);
358        assert_eq!(m.cpu_read(0xE000), 7);
359    }
360
361    #[test]
362    fn mirroring_bit_and_chr_banks() {
363        let mut m =
364            IremG101::new(synth_prg(4), synth_chr_1k(16), Mirroring::Vertical, false).unwrap();
365        m.cpu_write(0x9000, 0x01); // mirroring bit -> Horizontal
366        assert_eq!(m.current_mirroring(), Mirroring::Horizontal);
367        m.cpu_write(0x9000, 0x00); // -> Vertical
368        assert_eq!(m.current_mirroring(), Mirroring::Vertical);
369        // CHR reg 0 ($B000) selects 1 KiB bank at $0000.
370        m.cpu_write(0xB000, 0x09);
371        assert_eq!(m.ppu_read(0x0000), 9);
372        // CHR reg 7 ($B007) selects 1 KiB bank at $1C00.
373        m.cpu_write(0xB007, 0x0B);
374        assert_eq!(m.ppu_read(0x1C00), 11);
375    }
376
377    #[test]
378    fn force_one_screen_ignores_mirroring_bit() {
379        let mut m =
380            IremG101::new(synth_prg(4), synth_chr_1k(8), Mirroring::Vertical, true).unwrap();
381        assert_eq!(m.current_mirroring(), Mirroring::SingleScreenA);
382        m.cpu_write(0x9000, 0x01); // attempt to set Horizontal — ignored
383        assert_eq!(m.current_mirroring(), Mirroring::SingleScreenA);
384        // The PRG swap bit still takes effect even on a one-screen board.
385        assert!(!m.prg_swap_mode);
386        m.cpu_write(0x9000, 0x02);
387        assert!(m.prg_swap_mode);
388    }
389
390    #[test]
391    fn save_state_round_trip() {
392        let mut m =
393            IremG101::new(synth_prg(8), synth_chr_1k(16), Mirroring::Vertical, false).unwrap();
394        m.cpu_write(0x8000, 0x05);
395        m.cpu_write(0x9000, 0x03); // mode 1 + Horizontal
396        m.cpu_write(0xA000, 0x07);
397        m.cpu_write(0xB003, 0x0A);
398        let blob = m.save_state();
399        let mut m2 =
400            IremG101::new(synth_prg(8), synth_chr_1k(16), Mirroring::Vertical, false).unwrap();
401        m2.load_state(&blob).unwrap();
402        assert_eq!(m.cpu_read(0x8000), m2.cpu_read(0x8000));
403        assert_eq!(m.cpu_read(0xC000), m2.cpu_read(0xC000));
404        assert_eq!(m.ppu_read(0x0C00), m2.ppu_read(0x0C00));
405        assert_eq!(m.current_mirroring(), m2.current_mirroring());
406    }
407}