1#![allow(
16 clippy::cast_possible_truncation,
17 clippy::cast_lossless,
18 clippy::missing_const_for_fn,
19 clippy::needless_pass_by_ref_mut,
20 clippy::manual_range_patterns,
21 clippy::match_same_arms,
22 clippy::struct_excessive_bools,
23 clippy::doc_markdown,
24 clippy::range_plus_one,
25 clippy::single_match_else,
26 clippy::bool_to_int_with_if,
27 clippy::unnested_or_patterns,
28 clippy::single_match,
29 clippy::doc_lazy_continuation,
30 clippy::too_long_first_doc_paragraph
31)]
32
33use crate::cartridge::Mirroring;
34use crate::mapper::{Mapper, MapperCaps, MapperError};
35use alloc::{boxed::Box, vec::Vec};
36use alloc::{format, vec};
37
38const PRG_BANK_8K: usize = 0x2000;
39const CHR_BANK_1K: usize = 0x0400;
40const CHR_BANK_8K: usize = 0x2000;
41const NAMETABLE_SIZE: usize = 0x0400;
42const NAMETABLE_SIZE_U16: u16 = 0x0400;
43
44const VRC4_SECTION_VERSION: u8 = 2;
57
58fn nametable_offset(addr: u16, mirroring: Mirroring) -> usize {
59 let table = (((addr - 0x2000) / NAMETABLE_SIZE_U16) & 0x03) as u8;
60 let local = (addr as usize) & (NAMETABLE_SIZE - 1);
61 let physical = mirroring.physical_bank(table);
62 physical * NAMETABLE_SIZE + local
63}
64
65fn vrc_a_bits(mapper_id: u16, _submapper: u8, addr: u16) -> (bool, bool) {
101 let bit = |n: u16| (addr >> n) & 1 != 0;
102 match mapper_id {
103 21 => (bit(1) | bit(6), bit(2) | bit(7)),
104 22 => (bit(1), bit(0)), 25 => (bit(1) | bit(3), bit(0) | bit(2)), _ => (bit(0) | bit(2), bit(1) | bit(3)),
108 }
109}
110
111pub struct Vrc4 {
116 prg_rom: Box<[u8]>,
117 chr_rom: Box<[u8]>,
118 vram: Box<[u8]>,
119 chr_is_ram: bool,
120 prg_lo: u8,
121 prg_mid: u8,
122 prg_swap: bool, chr: [u8; 8],
124 mirroring: Mirroring,
125 mapper_id: u16,
126 submapper: u8,
127 prg_ram: Box<[u8]>,
129
130 irq_latch: u8,
132 irq_counter: u8,
133 irq_enabled: bool,
134 irq_enable_after_ack: bool,
135 irq_mode_scanline: bool,
136 irq_prescaler: i32,
141 irq_pending: bool,
142}
143
144impl Vrc4 {
145 pub fn new(
151 prg_rom: Box<[u8]>,
152 chr_rom: Box<[u8]>,
153 mapper_id: u16,
154 submapper: u8,
155 mirroring: Mirroring,
156 ) -> Result<Self, MapperError> {
157 if prg_rom.is_empty() || !prg_rom.len().is_multiple_of(PRG_BANK_8K) {
158 return Err(MapperError::Invalid(format!(
159 "VRC4 PRG-ROM size {} is not a non-zero multiple of 8 KiB",
160 prg_rom.len()
161 )));
162 }
163 let chr_is_ram = chr_rom.is_empty();
164 let chr: Box<[u8]> = if chr_is_ram {
165 vec![0u8; CHR_BANK_8K].into_boxed_slice()
166 } else if chr_rom.len().is_multiple_of(CHR_BANK_1K) {
167 chr_rom
168 } else {
169 return Err(MapperError::Invalid(format!(
170 "VRC4 CHR-ROM size {} is not a multiple of 1 KiB",
171 chr_rom.len()
172 )));
173 };
174 Ok(Self {
175 prg_rom,
176 chr_rom: chr,
177 vram: vec![0u8; 2 * NAMETABLE_SIZE].into_boxed_slice(),
178 chr_is_ram,
179 prg_lo: 0,
180 prg_mid: 1,
181 prg_swap: false,
182 chr: [0; 8],
183 mirroring,
184 mapper_id,
185 submapper,
186 prg_ram: vec![0u8; 8 * 1024].into_boxed_slice(),
188 irq_latch: 0,
189 irq_counter: 0,
190 irq_enabled: false,
191 irq_enable_after_ack: false,
192 irq_mode_scanline: false,
193 irq_prescaler: 341,
194 irq_pending: false,
195 })
196 }
197
198 fn prg_offset(&self, addr: u16) -> usize {
199 let total_8k = (self.prg_rom.len() / PRG_BANK_8K).max(1);
200 let last1 = total_8k - 1;
201 let last2 = total_8k.saturating_sub(2);
202 let bank = match (addr & 0xE000, self.prg_swap) {
203 (0x8000, false) => (self.prg_lo as usize) % total_8k,
204 (0x8000, true) => last2,
205 (0xA000, _) => (self.prg_mid as usize) % total_8k,
206 (0xC000, false) => last2,
207 (0xC000, true) => (self.prg_lo as usize) % total_8k,
208 (0xE000, _) => last1,
209 _ => 0,
210 };
211 bank * PRG_BANK_8K + (addr as usize & 0x1FFF)
212 }
213
214 fn chr_offset(&self, addr: u16) -> usize {
215 let addr = (addr & 0x1FFF) as usize;
216 let total_1k = (self.chr_rom.len() / CHR_BANK_1K).max(1);
217 let slot = addr / CHR_BANK_1K;
218 let bank = (self.chr[slot] as usize) % total_1k;
219 bank * CHR_BANK_1K + (addr & (CHR_BANK_1K - 1))
220 }
221
222 fn write_chr_reg(&mut self, slot: usize, low: bool, value: u8) {
223 let cur = self.chr[slot];
224 let v = if low {
225 (cur & 0xF0) | (value & 0x0F)
226 } else {
227 (cur & 0x0F) | ((value & 0x1F) << 4)
228 };
229 self.chr[slot] = v;
230 }
231
232 fn clock_irq_counter(&mut self) {
233 if self.irq_counter == 0xFF {
234 self.irq_counter = self.irq_latch;
235 self.irq_pending = true;
236 } else {
237 self.irq_counter = self.irq_counter.wrapping_add(1);
238 }
239 }
240}
241
242impl Mapper for Vrc4 {
243 fn sram(&self) -> &[u8] {
244 &self.prg_ram
245 }
246 fn sram_mut(&mut self) -> &mut [u8] {
247 &mut self.prg_ram
248 }
249 fn caps(&self) -> MapperCaps {
251 MapperCaps::CYCLE_IRQ
252 }
253
254 fn cpu_read(&mut self, addr: u16) -> u8 {
255 match addr {
256 0x6000..=0x7FFF => self.prg_ram[(addr - 0x6000) as usize % self.prg_ram.len()],
261 0x8000..=0xFFFF => {
262 let off = self.prg_offset(addr);
263 self.prg_rom[off % self.prg_rom.len()]
264 }
265 _ => 0,
266 }
267 }
268
269 fn cpu_write(&mut self, addr: u16, value: u8) {
270 if (0x6000..=0x7FFF).contains(&addr) {
273 let len = self.prg_ram.len();
274 self.prg_ram[(addr - 0x6000) as usize % len] = value;
275 return;
276 }
277 let (a0, a1) = vrc_a_bits(self.mapper_id, self.submapper, addr);
278 match addr & 0xF000 {
279 0x8000 => self.prg_lo = value & 0x1F,
280 0x9000 => match (a0, a1) {
281 (false, false) | (false, true) => {
282 self.mirroring = match value & 0x03 {
284 0 => Mirroring::Vertical,
285 1 => Mirroring::Horizontal,
286 2 => Mirroring::SingleScreenA,
287 _ => Mirroring::SingleScreenB,
288 };
289 }
290 (true, false) | (true, true) => {
291 self.prg_swap = (value & 0x02) != 0;
293 }
294 },
295 0xA000 => self.prg_mid = value & 0x1F,
296 0xB000 => self.write_chr_reg(if a1 { 1 } else { 0 }, !a0, value),
297 0xC000 => self.write_chr_reg(if a1 { 3 } else { 2 }, !a0, value),
298 0xD000 => self.write_chr_reg(if a1 { 5 } else { 4 }, !a0, value),
299 0xE000 => self.write_chr_reg(if a1 { 7 } else { 6 }, !a0, value),
300 0xF000 => match (a0, a1) {
301 (false, false) => {
302 self.irq_latch = (self.irq_latch & 0xF0) | (value & 0x0F);
303 }
304 (true, false) => {
305 self.irq_latch = (self.irq_latch & 0x0F) | ((value & 0x0F) << 4);
306 }
307 (false, true) => {
308 self.irq_enable_after_ack = (value & 0x01) != 0;
311 self.irq_enabled = (value & 0x02) != 0;
312 self.irq_mode_scanline = (value & 0x04) != 0;
313 self.irq_pending = false;
314 if self.irq_enabled {
315 self.irq_counter = self.irq_latch;
316 self.irq_prescaler = 341;
317 }
318 }
319 (true, true) => {
320 self.irq_pending = false;
322 self.irq_enabled = self.irq_enable_after_ack;
323 }
324 },
325 _ => {}
326 }
327 }
328
329 fn ppu_read(&mut self, addr: u16) -> u8 {
330 let addr = addr & 0x3FFF;
331 match addr {
332 0x0000..=0x1FFF => {
333 let off = self.chr_offset(addr);
334 self.chr_rom[off % self.chr_rom.len()]
335 }
336 0x2000..=0x3EFF => self.vram[nametable_offset(addr, self.mirroring) % self.vram.len()],
337 _ => 0,
338 }
339 }
340
341 fn ppu_write(&mut self, addr: u16, value: u8) {
342 let addr = addr & 0x3FFF;
343 match addr {
344 0x0000..=0x1FFF => {
345 if self.chr_is_ram {
346 let len = self.chr_rom.len();
347 self.chr_rom[addr as usize % len] = value;
348 }
349 }
350 0x2000..=0x3EFF => {
351 let off = nametable_offset(addr, self.mirroring) % self.vram.len();
352 self.vram[off] = value;
353 }
354 _ => {}
355 }
356 }
357
358 fn notify_cpu_cycle(&mut self) {
359 if !self.irq_enabled {
360 return;
361 }
362 if self.irq_mode_scanline {
363 self.irq_prescaler -= 3;
366 if self.irq_prescaler <= 0 {
367 self.irq_prescaler += 341;
368 self.clock_irq_counter();
369 }
370 } else {
371 self.clock_irq_counter();
372 }
373 }
374
375 fn irq_pending(&self) -> bool {
376 self.irq_pending
377 }
378
379 fn current_mirroring(&self) -> Mirroring {
380 self.mirroring
381 }
382
383 fn debug_info(&self) -> crate::mapper::MapperDebugInfo {
384 let mut info = crate::mapper::MapperDebugInfo {
385 mapper_id: self.mapper_id,
386 name: format!("VRC4 (sub {})", self.submapper),
387 mirroring: crate::mapper::mirroring_name(self.current_mirroring()),
388 ..Default::default()
389 };
390 info.prg_banks
391 .push(("PRG_lo".into(), format!("{:#04x}", self.prg_lo)));
392 info.prg_banks
393 .push(("PRG_mid".into(), format!("{:#04x}", self.prg_mid)));
394 info.prg_banks
395 .push(("swap".into(), format!("{}", self.prg_swap)));
396 for (i, b) in self.chr.iter().enumerate() {
397 info.chr_banks
398 .push((format!("CHR{i}"), format!("{b:#04x}")));
399 }
400 info.irq_state
401 .push(("latch".into(), format!("{:#04x}", self.irq_latch)));
402 info.irq_state
403 .push(("counter".into(), format!("{:#04x}", self.irq_counter)));
404 info.irq_state
405 .push(("enabled".into(), format!("{}", self.irq_enabled)));
406 info.irq_state.push((
407 "scanline_mode".into(),
408 format!("{}", self.irq_mode_scanline),
409 ));
410 info.irq_state
411 .push(("pending".into(), format!("{}", self.irq_pending)));
412 info
413 }
414
415 fn save_state(&self) -> Vec<u8> {
416 let mut out = Vec::with_capacity(40 + self.vram.len() + self.ram_block_len());
417 out.push(VRC4_SECTION_VERSION);
418 out.push(self.prg_lo);
419 out.push(self.prg_mid);
420 out.push(u8::from(self.prg_swap));
421 out.extend_from_slice(&self.chr);
422 out.push(self.mirroring as u8);
423 out.push(self.irq_latch);
424 out.push(self.irq_counter);
425 out.push(u8::from(self.irq_enabled));
426 out.push(u8::from(self.irq_enable_after_ack));
427 out.push(u8::from(self.irq_mode_scanline));
428 out.extend_from_slice(&self.irq_prescaler.to_le_bytes());
429 out.push(u8::from(self.irq_pending));
430 out.extend_from_slice(&self.vram);
431 out.extend_from_slice(&self.prg_ram);
433 if self.chr_is_ram {
434 out.extend_from_slice(&self.chr_rom);
435 }
436 out
437 }
438
439 fn load_state(&mut self, data: &[u8]) -> Result<(), MapperError> {
440 let scalar_len = 1 + 1 + 1 + 1 + 8 + 1 + 1 + 1 + 1 + 1 + 1 + 4 + 1;
441 let version = data.first().copied().unwrap_or(0);
442 if version != VRC4_SECTION_VERSION {
446 return Err(MapperError::UnsupportedVersion(version));
447 }
448 let ram_len = self.ram_block_len();
449 let core_len = scalar_len + self.vram.len();
451 let expected = core_len + ram_len;
452 if data.len() != expected {
453 return Err(MapperError::WrongLength {
454 expected,
455 got: data.len(),
456 });
457 }
458 self.prg_lo = data[1];
459 self.prg_mid = data[2];
460 self.prg_swap = data[3] != 0;
461 self.chr.copy_from_slice(&data[4..12]);
462 self.mirroring = match data[12] {
463 0 => Mirroring::Horizontal,
464 1 => Mirroring::Vertical,
465 2 => Mirroring::SingleScreenA,
466 3 => Mirroring::SingleScreenB,
467 4 => Mirroring::FourScreen,
468 5 => Mirroring::MapperControlled,
469 other => return Err(MapperError::Invalid(format!("mirroring {other}"))),
470 };
471 self.irq_latch = data[13];
472 self.irq_counter = data[14];
473 self.irq_enabled = data[15] != 0;
474 self.irq_enable_after_ack = data[16] != 0;
475 self.irq_mode_scanline = data[17] != 0;
476 self.irq_prescaler = i32::from_le_bytes(
477 data[18..22]
478 .try_into()
479 .map_err(|_| MapperError::Invalid("prescaler".into()))?,
480 );
481 self.irq_pending = data[22] != 0;
482 self.vram.copy_from_slice(&data[23..core_len]);
483 let (prg, chr) = data[core_len..].split_at(self.prg_ram.len());
484 self.prg_ram.copy_from_slice(prg);
485 if self.chr_is_ram {
486 self.chr_rom.copy_from_slice(chr);
487 }
488 Ok(())
489 }
490}
491
492impl Vrc4 {
493 fn ram_block_len(&self) -> usize {
497 self.prg_ram.len()
498 + if self.chr_is_ram {
499 self.chr_rom.len()
500 } else {
501 0
502 }
503 }
504}
505
506#[cfg(test)]
507mod tests {
508 use super::*;
509
510 fn synth(banks_8k: usize) -> Box<[u8]> {
511 let mut v = vec![0u8; banks_8k * PRG_BANK_8K];
512 for b in 0..banks_8k {
513 v[b * PRG_BANK_8K] = b as u8;
514 }
515 v.into_boxed_slice()
516 }
517
518 fn synth_chr(banks_1k: usize) -> Box<[u8]> {
519 let mut v = vec![0u8; banks_1k * CHR_BANK_1K];
520 for b in 0..banks_1k {
521 v[b * CHR_BANK_1K] = b as u8;
522 }
523 v.into_boxed_slice()
524 }
525
526 #[test]
527 fn vrc4_irq_counter_pending() {
528 let mut m = Vrc4::new(synth(8), synth_chr(8), 21, 1, Mirroring::Vertical).unwrap();
529 m.cpu_write(0xF000, 0xE);
532 m.cpu_write(0xF004, 0x02);
534 for _ in 0..0xF1 + 1 {
537 m.notify_cpu_cycle();
538 }
539 assert!(m.irq_pending());
540 }
541
542 #[test]
546 fn vrc4_save_state_carries_prg_ram_and_chr_ram() {
547 let mut m = Vrc4::new(synth(8), Box::new([]), 21, 0, Mirroring::Vertical).unwrap();
548 m.cpu_write(0x6000, 0x5A);
549 m.cpu_write(0x7FFF, 0xA5);
550 m.ppu_write(0x0000, 0x11);
551 m.ppu_write(0x03FF, 0x22);
554 let blob = m.save_state();
555 let mut m2 = Vrc4::new(synth(8), Box::new([]), 21, 0, Mirroring::Vertical).unwrap();
556 m2.load_state(&blob).expect("round-trip");
557 assert_eq!(m2.cpu_read(0x6000), 0x5A);
558 assert_eq!(m2.cpu_read(0x7FFF), 0xA5);
559 assert_eq!(m2.ppu_read(0x0000), 0x11);
560 assert_eq!(m2.ppu_read(0x03FF), 0x22);
561 }
562
563 #[test]
566 fn vrc4_v1_blob_is_refused() {
567 let mut m = Vrc4::new(synth(8), synth_chr(8), 21, 0, Mirroring::Vertical).unwrap();
568 m.cpu_write(0x8000, 3);
569 let core_len = 23 + m.vram.len();
570 let mut v1 = m.save_state()[..core_len].to_vec();
571 v1[0] = 1;
572 let mut m2 = Vrc4::new(synth(8), synth_chr(8), 21, 0, Mirroring::Vertical).unwrap();
573 assert!(matches!(
574 m2.load_state(&v1),
575 Err(MapperError::UnsupportedVersion(1))
576 ));
577 }
578
579 #[test]
581 fn vrc4_truncated_ram_tail_is_rejected() {
582 let m = Vrc4::new(synth(8), synth_chr(8), 21, 0, Mirroring::Vertical).unwrap();
583 let blob = m.save_state();
584 let mut m2 = Vrc4::new(synth(8), synth_chr(8), 21, 0, Mirroring::Vertical).unwrap();
585 let err = m2
586 .load_state(&blob[..blob.len() - 1])
587 .expect_err("a truncated v2 blob must be rejected");
588 assert!(matches!(err, MapperError::WrongLength { .. }), "{err:?}");
589 }
590}