Skip to main content

rustynes_mappers/
m002_uxrom.rs

1//! Nintendo `UxROM` (mappers 2, 71-like) -- 16 KiB PRG banking with a fixed
2//! high bank.
3//!
4//! The archetypal simple discrete board: one write-anywhere register selects a
5//! 16 KiB bank at `$8000-$BFFF` while `$C000-$FFFF` stays pinned to the last
6//! bank, so the interrupt vectors are always reachable. CHR is RAM.
7//!
8//! The `UN1ROM` variant (mapper 94) is in `m094_un1rom.rs`.
9//!
10//! See `docs/mappers.md` §Mapper coverage matrix.
11
12#![allow(
13    clippy::bool_to_int_with_if,
14    clippy::cast_lossless,
15    clippy::cast_possible_truncation,
16    clippy::doc_markdown,
17    clippy::match_same_arms,
18    clippy::missing_const_for_fn,
19    clippy::similar_names,
20    clippy::struct_excessive_bools,
21    clippy::too_many_lines
22)]
23
24use crate::cartridge::Mirroring;
25use crate::mapper::{Mapper, MapperCaps, MapperError};
26use alloc::{boxed::Box, vec::Vec};
27use alloc::{format, vec};
28
29const PRG_BANK_16K: usize = 0x4000;
30const CHR_BANK_8K: usize = 0x2000;
31const NAMETABLE_SIZE: usize = 0x0400;
32const NAMETABLE_SIZE_U16: u16 = 0x0400;
33
34const SAVE_STATE_VERSION: u8 = 1;
35
36/// `UxROM` mapper.
37pub struct UxRom {
38    prg_rom: Box<[u8]>,
39    chr: Box<[u8]>,
40    vram: Box<[u8]>,
41    chr_is_ram: bool,
42    bank: u8,
43    mirroring: Mirroring,
44}
45
46impl UxRom {
47    /// Construct a new `UxROM` mapper.
48    ///
49    /// `prg_rom` must be a non-zero multiple of 16 KiB. CHR-RAM is selected
50    /// when `chr_rom` is empty; otherwise CHR-ROM length must be 8 KiB.
51    ///
52    /// # Errors
53    ///
54    /// Returns [`MapperError::Invalid`] when sizes don't match the
55    /// constraints.
56    pub fn new(
57        prg_rom: Box<[u8]>,
58        chr_rom: Box<[u8]>,
59        mirroring: Mirroring,
60    ) -> Result<Self, MapperError> {
61        if prg_rom.is_empty() || !prg_rom.len().is_multiple_of(PRG_BANK_16K) {
62            return Err(MapperError::Invalid(format!(
63                "UxROM PRG-ROM size {} is not a non-zero multiple of 16 KiB",
64                prg_rom.len()
65            )));
66        }
67        let chr_is_ram = chr_rom.is_empty();
68        let chr: Box<[u8]> = if chr_is_ram {
69            vec![0u8; CHR_BANK_8K].into_boxed_slice()
70        } else if chr_rom.len() == CHR_BANK_8K {
71            chr_rom
72        } else {
73            return Err(MapperError::Invalid(format!(
74                "UxROM expects 8 KiB CHR (RAM or ROM); got {} bytes",
75                chr_rom.len()
76            )));
77        };
78        Ok(Self {
79            prg_rom,
80            chr,
81            vram: vec![0u8; 2 * NAMETABLE_SIZE].into_boxed_slice(),
82            chr_is_ram,
83            bank: 0,
84            mirroring,
85        })
86    }
87
88    const fn nametable_offset(&self, addr: u16) -> usize {
89        let table = (((addr - 0x2000) / NAMETABLE_SIZE_U16) & 0x03) as u8;
90        let local = (addr as usize) & (NAMETABLE_SIZE - 1);
91        let physical = self.mirroring.physical_bank(table);
92        physical * NAMETABLE_SIZE + local
93    }
94}
95
96impl Mapper for UxRom {
97    // v2.8.0 Phase 4 — no per-cycle hooks (no IRQ, no audio): the bus
98    // skips all four per-CPU-cycle dispatches for this board.
99    fn caps(&self) -> MapperCaps {
100        MapperCaps::NONE
101    }
102
103    fn cpu_read(&mut self, addr: u16) -> u8 {
104        match addr {
105            0x8000..=0xBFFF => {
106                let bank_count = (self.prg_rom.len() / PRG_BANK_16K).max(1);
107                let bank = (self.bank as usize) % bank_count;
108                let off = (addr - 0x8000) as usize;
109                self.prg_rom[bank * PRG_BANK_16K + off]
110            }
111            0xC000..=0xFFFF => {
112                let bank_count = (self.prg_rom.len() / PRG_BANK_16K).max(1);
113                let last = bank_count - 1;
114                let off = (addr - 0xC000) as usize;
115                self.prg_rom[last * PRG_BANK_16K + off]
116            }
117            _ => 0,
118        }
119    }
120
121    fn cpu_write(&mut self, addr: u16, value: u8) {
122        if let 0x8000..=0xFFFF = addr {
123            // Standard UxROM uses the low 4 bits (16-bank max). UOROM uses
124            // 5 bits (32-bank max). Submapper 2 specifies this exactly,
125            // but for our purposes masking to a power of two derived from
126            // the PRG length is robust.
127            let bank_count = (self.prg_rom.len() / PRG_BANK_16K).max(1);
128            // Find bit width that fits bank_count; saturate at 8 bits since
129            // the bank register is u8.
130            let mask = u8::try_from((bank_count - 1) | 0x0F).unwrap_or(u8::MAX);
131            self.bank = value & mask;
132        }
133    }
134
135    fn chr_phys(&self, addr: u16) -> Option<u32> {
136        // UxROM CHR is unbanked; offset == address. Usually CHR-RAM (-> None).
137        if self.chr_is_ram {
138            None
139        } else {
140            Some(u32::from(addr & 0x1FFF))
141        }
142    }
143
144    fn ppu_read(&mut self, addr: u16) -> u8 {
145        let addr = addr & 0x3FFF;
146        match addr {
147            0x0000..=0x1FFF => self.chr[addr as usize],
148            0x2000..=0x3EFF => self.vram[self.nametable_offset(addr)],
149            _ => 0,
150        }
151    }
152
153    fn ppu_write(&mut self, addr: u16, value: u8) {
154        let addr = addr & 0x3FFF;
155        match addr {
156            0x0000..=0x1FFF => {
157                if self.chr_is_ram {
158                    self.chr[addr as usize] = value;
159                }
160            }
161            0x2000..=0x3EFF => {
162                let off = self.nametable_offset(addr);
163                self.vram[off] = value;
164            }
165            _ => {}
166        }
167    }
168
169    fn current_mirroring(&self) -> Mirroring {
170        self.mirroring
171    }
172
173    // UxROM has fixed solder-pad mirroring — a game-DB header correction is valid.
174    fn has_hardwired_mirroring(&self) -> bool {
175        true
176    }
177
178    fn save_state(&self) -> Vec<u8> {
179        let mut out = Vec::with_capacity(
180            2 + self.vram.len() + if self.chr_is_ram { self.chr.len() } else { 0 },
181        );
182        out.push(SAVE_STATE_VERSION);
183        out.push(self.bank);
184        out.extend_from_slice(&self.vram);
185        if self.chr_is_ram {
186            out.extend_from_slice(&self.chr);
187        }
188        out
189    }
190
191    fn load_state(&mut self, data: &[u8]) -> Result<(), MapperError> {
192        let need_chr = if self.chr_is_ram { self.chr.len() } else { 0 };
193        let expected = 2 + self.vram.len() + need_chr;
194        if data.len() != expected {
195            return Err(MapperError::WrongLength {
196                expected,
197                got: data.len(),
198            });
199        }
200        if data[0] != SAVE_STATE_VERSION {
201            return Err(MapperError::UnsupportedVersion(data[0]));
202        }
203        self.bank = data[1];
204        let mut cursor = 2;
205        self.vram
206            .copy_from_slice(&data[cursor..cursor + self.vram.len()]);
207        cursor += self.vram.len();
208        if self.chr_is_ram {
209            self.chr
210                .copy_from_slice(&data[cursor..cursor + self.chr.len()]);
211        }
212        Ok(())
213    }
214}
215
216#[cfg(test)]
217mod tests {
218    use super::*;
219
220    fn synth_prg(banks: usize) -> Box<[u8]> {
221        let mut v = vec![0u8; banks * PRG_BANK_16K];
222        for b in 0..banks {
223            v[b * PRG_BANK_16K] = b as u8;
224        }
225        v.into_boxed_slice()
226    }
227
228    #[test]
229    fn uxrom_default_first_bank_zero_last_bank_fixed() {
230        let mut m = UxRom::new(synth_prg(8), Box::new([]), Mirroring::Vertical).unwrap();
231        assert_eq!(m.cpu_read(0x8000), 0);
232        assert_eq!(m.cpu_read(0xC000), 7);
233    }
234
235    #[test]
236    fn uxrom_bank_select_switches_first_window() {
237        let mut m = UxRom::new(synth_prg(8), Box::new([]), Mirroring::Vertical).unwrap();
238        m.cpu_write(0x8000, 5);
239        assert_eq!(m.cpu_read(0x8000), 5);
240        assert_eq!(m.cpu_read(0xC000), 7); // last fixed
241        m.cpu_write(0xFFFF, 3);
242        assert_eq!(m.cpu_read(0x8000), 3);
243    }
244
245    #[test]
246    fn uxrom_chr_ram_round_trip() {
247        let mut m = UxRom::new(synth_prg(2), Box::new([]), Mirroring::Vertical).unwrap();
248        m.ppu_write(0x0010, 0xAB);
249        assert_eq!(m.ppu_read(0x0010), 0xAB);
250    }
251}