1#![allow(
15 clippy::cast_possible_truncation,
16 clippy::cast_lossless,
17 clippy::missing_const_for_fn,
18 clippy::needless_pass_by_ref_mut,
19 clippy::manual_range_patterns,
20 clippy::match_same_arms,
21 clippy::too_many_arguments
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_8K: usize = 0x2000;
30const CHR_BANK_4K: usize = 0x1000;
31const CHR_BANK_8K: usize = 0x2000;
32const NAMETABLE_SIZE: usize = 0x0400;
33const NAMETABLE_SIZE_U16: u16 = 0x0400;
34
35const VRC1_SECTION_VERSION: u8 = 2;
49
50fn nametable_offset(addr: u16, mirroring: Mirroring) -> usize {
51 let table = (((addr - 0x2000) / NAMETABLE_SIZE_U16) & 0x03) as u8;
52 let local = (addr as usize) & (NAMETABLE_SIZE - 1);
53 let physical = mirroring.physical_bank(table);
54 physical * NAMETABLE_SIZE + local
55}
56
57pub struct Vrc1 {
59 prg_rom: Box<[u8]>,
60 chr_rom: Box<[u8]>,
61 vram: Box<[u8]>,
62 chr_is_ram: bool,
63 prg_banks: [u8; 3], chr_lo: u8,
65 chr_hi: u8,
66 chr_lo_msb: u8,
67 chr_hi_msb: u8,
68 mirroring: Mirroring,
69}
70
71impl Vrc1 {
72 pub fn new(
78 prg_rom: Box<[u8]>,
79 chr_rom: Box<[u8]>,
80 mirroring: Mirroring,
81 ) -> Result<Self, MapperError> {
82 if prg_rom.is_empty() || !prg_rom.len().is_multiple_of(PRG_BANK_8K) {
83 return Err(MapperError::Invalid(format!(
84 "VRC1 PRG-ROM size {} is not a non-zero multiple of 8 KiB",
85 prg_rom.len()
86 )));
87 }
88 let chr_is_ram = chr_rom.is_empty();
89 let chr: Box<[u8]> = if chr_is_ram {
90 vec![0u8; CHR_BANK_8K].into_boxed_slice()
91 } else if chr_rom.len().is_multiple_of(CHR_BANK_4K) {
92 chr_rom
93 } else {
94 return Err(MapperError::Invalid(format!(
95 "VRC1 CHR-ROM size {} is not a multiple of 4 KiB",
96 chr_rom.len()
97 )));
98 };
99 Ok(Self {
100 prg_rom,
101 chr_rom: chr,
102 vram: vec![0u8; 2 * NAMETABLE_SIZE].into_boxed_slice(),
103 chr_is_ram,
104 prg_banks: [0, 1, 2],
105 chr_lo: 0,
106 chr_hi: 0,
107 chr_lo_msb: 0,
108 chr_hi_msb: 0,
109 mirroring,
110 })
111 }
112}
113
114impl Mapper for Vrc1 {
115 fn caps(&self) -> MapperCaps {
118 MapperCaps::NONE
119 }
120
121 fn cpu_read(&mut self, addr: u16) -> u8 {
122 let total_8k = (self.prg_rom.len() / PRG_BANK_8K).max(1);
123 let last = total_8k - 1;
124 let bank = match addr & 0xE000 {
125 0x8000 => (self.prg_banks[0] as usize) % total_8k,
126 0xA000 => (self.prg_banks[1] as usize) % total_8k,
127 0xC000 => (self.prg_banks[2] as usize) % total_8k,
128 0xE000 => last,
129 _ => return 0,
130 };
131 self.prg_rom[(bank * PRG_BANK_8K + (addr as usize & 0x1FFF)) % self.prg_rom.len()]
132 }
133
134 fn cpu_write(&mut self, addr: u16, value: u8) {
135 match addr & 0xF000 {
136 0x8000 => self.prg_banks[0] = value & 0x0F,
137 0x9000 => {
138 self.mirroring = if value & 1 == 0 {
140 Mirroring::Vertical
141 } else {
142 Mirroring::Horizontal
143 };
144 self.chr_lo_msb = (value >> 1) & 1;
145 self.chr_hi_msb = (value >> 2) & 1;
146 }
147 0xA000 => self.prg_banks[1] = value & 0x0F,
148 0xC000 => self.prg_banks[2] = value & 0x0F,
149 0xE000 => self.chr_lo = value & 0x0F,
150 0xF000 => self.chr_hi = value & 0x0F,
151 _ => {}
152 }
153 }
154
155 fn ppu_read(&mut self, addr: u16) -> u8 {
156 let addr = addr & 0x3FFF;
157 match addr {
158 0x0000..=0x0FFF => {
159 let total_4k = (self.chr_rom.len() / CHR_BANK_4K).max(1);
160 let bank = (((self.chr_lo_msb as usize) << 4) | (self.chr_lo as usize)) % total_4k;
161 self.chr_rom[(bank * CHR_BANK_4K + addr as usize) % self.chr_rom.len()]
162 }
163 0x1000..=0x1FFF => {
164 let total_4k = (self.chr_rom.len() / CHR_BANK_4K).max(1);
165 let bank = (((self.chr_hi_msb as usize) << 4) | (self.chr_hi as usize)) % total_4k;
166 self.chr_rom[(bank * CHR_BANK_4K + (addr as usize - 0x1000)) % self.chr_rom.len()]
167 }
168 0x2000..=0x3EFF => self.vram[nametable_offset(addr, self.mirroring) % self.vram.len()],
169 _ => 0,
170 }
171 }
172
173 fn ppu_write(&mut self, addr: u16, value: u8) {
174 let addr = addr & 0x3FFF;
175 match addr {
176 0x0000..=0x1FFF => {
177 if self.chr_is_ram {
178 let len = self.chr_rom.len();
179 self.chr_rom[addr as usize % len] = value;
180 }
181 }
182 0x2000..=0x3EFF => {
183 let off = nametable_offset(addr, self.mirroring) % self.vram.len();
184 self.vram[off] = value;
185 }
186 _ => {}
187 }
188 }
189
190 fn current_mirroring(&self) -> Mirroring {
191 self.mirroring
192 }
193
194 fn save_state(&self) -> Vec<u8> {
195 let mut out = Vec::with_capacity(16 + self.vram.len() + self.ram_block_len());
196 out.push(VRC1_SECTION_VERSION);
197 out.extend_from_slice(&self.prg_banks);
198 out.push(self.chr_lo);
199 out.push(self.chr_hi);
200 out.push(self.chr_lo_msb);
201 out.push(self.chr_hi_msb);
202 out.push(self.mirroring as u8);
203 out.extend_from_slice(&self.vram);
204 if self.chr_is_ram {
206 out.extend_from_slice(&self.chr_rom);
207 }
208 out
209 }
210
211 fn load_state(&mut self, data: &[u8]) -> Result<(), MapperError> {
212 let version = data.first().copied().unwrap_or(0);
213 if version != VRC1_SECTION_VERSION {
217 return Err(MapperError::UnsupportedVersion(version));
218 }
219 let ram_len = self.ram_block_len();
220 let core_len = 9 + self.vram.len();
222 let expected = core_len + ram_len;
223 if data.len() != expected {
224 return Err(MapperError::WrongLength {
225 expected,
226 got: data.len(),
227 });
228 }
229 self.prg_banks.copy_from_slice(&data[1..4]);
230 self.chr_lo = data[4];
231 self.chr_hi = data[5];
232 self.chr_lo_msb = data[6];
233 self.chr_hi_msb = data[7];
234 self.mirroring = match data[8] {
235 0 => Mirroring::Horizontal,
236 1 => Mirroring::Vertical,
237 2 => Mirroring::SingleScreenA,
238 3 => Mirroring::SingleScreenB,
239 4 => Mirroring::FourScreen,
240 5 => Mirroring::MapperControlled,
241 other => return Err(MapperError::Invalid(format!("mirroring {other}"))),
242 };
243 self.vram.copy_from_slice(&data[9..core_len]);
244 if self.chr_is_ram {
245 self.chr_rom.copy_from_slice(&data[core_len..]);
246 }
247 Ok(())
248 }
249}
250
251impl Vrc1 {
252 fn ram_block_len(&self) -> usize {
256 if self.chr_is_ram {
257 self.chr_rom.len()
258 } else {
259 0
260 }
261 }
262}
263
264#[cfg(test)]
265mod tests {
266 use super::*;
267
268 fn synth(banks_8k: usize) -> Box<[u8]> {
269 let mut v = vec![0u8; banks_8k * PRG_BANK_8K];
270 for b in 0..banks_8k {
271 v[b * PRG_BANK_8K] = b as u8;
272 }
273 v.into_boxed_slice()
274 }
275
276 fn synth_chr_4k(banks: usize) -> Box<[u8]> {
277 let mut v = vec![0u8; banks * CHR_BANK_4K];
278 for b in 0..banks {
279 v[b * CHR_BANK_4K] = b as u8;
280 }
281 v.into_boxed_slice()
282 }
283
284 #[test]
285 fn vrc1_basic_banking() {
286 let mut m = Vrc1::new(synth(8), synth_chr_4k(2), Mirroring::Vertical).unwrap();
287 m.cpu_write(0x8000, 3);
288 assert_eq!(m.cpu_read(0x8000), 3);
289 assert_eq!(m.cpu_read(0xE000), 7);
291 }
292
293 #[test]
297 fn vrc1_save_state_carries_chr_ram() {
298 let mut m = Vrc1::new(synth(8), Box::new([]), Mirroring::Vertical).unwrap();
299 m.chr_rom[0x0000] = 0x11;
300 m.chr_rom[0x1FFF] = 0x22;
301 let blob = m.save_state();
302 let mut m2 = Vrc1::new(synth(8), Box::new([]), Mirroring::Vertical).unwrap();
303 m2.load_state(&blob).expect("round-trip");
304 assert_eq!(m2.chr_rom[0x0000], 0x11);
305 assert_eq!(m2.chr_rom[0x1FFF], 0x22);
306 }
307
308 #[test]
311 fn vrc1_v1_blob_is_refused() {
312 let mut m = Vrc1::new(synth(8), Box::new([]), Mirroring::Vertical).unwrap();
313 m.cpu_write(0x8000, 3);
314 let core_len = 9 + m.vram.len();
315 let mut v1 = m.save_state()[..core_len].to_vec();
316 v1[0] = 1;
317 let mut m2 = Vrc1::new(synth(8), Box::new([]), Mirroring::Vertical).unwrap();
318 assert!(matches!(
319 m2.load_state(&v1),
320 Err(MapperError::UnsupportedVersion(1))
321 ));
322 }
323
324 #[test]
326 fn vrc1_truncated_chr_ram_tail_is_rejected() {
327 let m = Vrc1::new(synth(8), Box::new([]), Mirroring::Vertical).unwrap();
328 let blob = m.save_state();
329 let mut m2 = Vrc1::new(synth(8), Box::new([]), Mirroring::Vertical).unwrap();
330 let err = m2
331 .load_state(&blob[..blob.len() - 1])
332 .expect_err("a truncated v2 blob must be rejected");
333 assert!(matches!(err, MapperError::WrongLength { .. }), "{err:?}");
334 }
335}