rustynes_mappers/
m002_uxrom.rs1#![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
36pub 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 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 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 let bank_count = (self.prg_rom.len() / PRG_BANK_16K).max(1);
128 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 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 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); 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}