Skip to main content

rustynes_mappers/
m073_vrc3.rs

1//! Konami VRC3 (iNES mapper 73) implementation.
2//!
3//! The simplest Konami VRC: used only by Salamander (JP). It has:
4//!
5//! - 8 KiB optional PRG-RAM at `$6000-$7FFF`.
6//! - A 16 KiB switchable PRG bank at `$8000-$BFFF` (selected by `$F000`).
7//! - A 16 KiB PRG bank fixed to the last bank at `$C000-$FFFF`.
8//! - 8 KiB CHR-RAM (no CHR banking).
9//! - Fixed H/V mirroring from the iNES header (solder pads).
10//! - A 16-bit CPU-cycle IRQ counter (no scanline mode, no CHR banking).
11//!
12//! # IRQ counter (`nesdev_wiki/VRC3.xhtml`)
13//!
14//! The 16-bit latch is written nibble-at-a-time across `$8000-$BFFF`:
15//! `$8xxx` = bits 0-3, `$9xxx` = bits 4-7, `$Axxx` = bits 8-11, `$Bxxx` =
16//! bits 12-15. `$Cxxx` is IRQ control `[.... .MEA]` (M = 8-bit mode, E =
17//! enable, A = enable-on-acknowledge). `$Dxxx` acknowledges and copies A
18//! into E.
19//!
20//! When enabled, the counter increments every CPU cycle. On overflow from
21//! `$FFFF` (or `$FF` in 8-bit mode), an IRQ is asserted and the counter is
22//! reloaded from the latch (8-bit mode reloads only the low 8 bits). Writing
23//! `$C000` with E set reloads all 16 bits regardless of mode.
24//!
25//! Reuses the VRC CPU-cycle IRQ family pattern (`m021_vrc4.rs`).
26
27#![allow(
28    clippy::cast_possible_truncation,
29    clippy::cast_lossless,
30    clippy::missing_const_for_fn,
31    clippy::struct_excessive_bools,
32    clippy::doc_markdown
33)]
34
35use crate::cartridge::Mirroring;
36use crate::mapper::{Mapper, MapperCaps, MapperError};
37use alloc::{boxed::Box, vec::Vec};
38use alloc::{format, vec};
39
40const PRG_BANK_16K: usize = 0x4000;
41const CHR_RAM_8K: usize = 0x2000;
42const NAMETABLE_SIZE: usize = 0x0400;
43const NAMETABLE_SIZE_U16: u16 = 0x0400;
44
45const SAVE_STATE_VERSION: u8 = 1;
46
47/// Konami VRC3 mapper (iNES mapper 73).
48pub struct Vrc3 {
49    prg_rom: Box<[u8]>,
50    chr_ram: Box<[u8]>,
51    vram: Box<[u8]>,
52    prg_ram: Box<[u8]>,
53    prg_bank: u8,
54    mirroring: Mirroring,
55
56    // 16-bit IRQ latch + counter.
57    irq_latch: u16,
58    irq_counter: u16,
59    irq_enabled: bool,
60    irq_enable_after_ack: bool,
61    irq_mode_8bit: bool,
62    irq_pending: bool,
63}
64
65impl Vrc3 {
66    /// Construct a new VRC3 mapper.
67    ///
68    /// `prg_rom` must be a non-zero multiple of 16 KiB. CHR is always 8 KiB
69    /// of RAM (the board has no CHR-ROM); any supplied CHR-ROM is ignored.
70    ///
71    /// # Errors
72    ///
73    /// Returns [`MapperError::Invalid`] on PRG size mismatch.
74    pub fn new(prg_rom: Box<[u8]>, mirroring: Mirroring) -> Result<Self, MapperError> {
75        if prg_rom.is_empty() || !prg_rom.len().is_multiple_of(PRG_BANK_16K) {
76            return Err(MapperError::Invalid(format!(
77                "VRC3 PRG-ROM size {} is not a non-zero multiple of 16 KiB",
78                prg_rom.len()
79            )));
80        }
81        Ok(Self {
82            prg_rom,
83            chr_ram: vec![0u8; CHR_RAM_8K].into_boxed_slice(),
84            vram: vec![0u8; 2 * NAMETABLE_SIZE].into_boxed_slice(),
85            prg_ram: vec![0u8; 8 * 1024].into_boxed_slice(),
86            prg_bank: 0,
87            mirroring,
88            irq_latch: 0,
89            irq_counter: 0,
90            irq_enabled: false,
91            irq_enable_after_ack: false,
92            irq_mode_8bit: false,
93            irq_pending: false,
94        })
95    }
96
97    const fn nametable_offset(&self, addr: u16) -> usize {
98        let table = (((addr - 0x2000) / NAMETABLE_SIZE_U16) & 0x03) as u8;
99        let local = (addr as usize) & (NAMETABLE_SIZE - 1);
100        let physical = self.mirroring.physical_bank(table);
101        physical * NAMETABLE_SIZE + local
102    }
103}
104
105impl Mapper for Vrc3 {
106    fn sram(&self) -> &[u8] {
107        &self.prg_ram
108    }
109    fn sram_mut(&mut self) -> &mut [u8] {
110        &mut self.prg_ram
111    }
112    // v2.8.0 Phase 4 — CPU-cycle hook + IRQ source; no on-cart audio.
113    fn caps(&self) -> MapperCaps {
114        MapperCaps::CYCLE_IRQ
115    }
116
117    fn cpu_read(&mut self, addr: u16) -> u8 {
118        match addr {
119            0x6000..=0x7FFF => self.prg_ram[(addr - 0x6000) as usize % self.prg_ram.len()],
120            0x8000..=0xBFFF => {
121                let total = (self.prg_rom.len() / PRG_BANK_16K).max(1);
122                let bank = (self.prg_bank as usize) % total;
123                self.prg_rom[bank * PRG_BANK_16K + (addr - 0x8000) as usize]
124            }
125            0xC000..=0xFFFF => {
126                let total = (self.prg_rom.len() / PRG_BANK_16K).max(1);
127                let last = total - 1;
128                self.prg_rom[last * PRG_BANK_16K + (addr - 0xC000) as usize]
129            }
130            _ => 0,
131        }
132    }
133
134    fn cpu_write(&mut self, addr: u16, value: u8) {
135        match addr {
136            0x6000..=0x7FFF => {
137                let len = self.prg_ram.len();
138                self.prg_ram[(addr - 0x6000) as usize % len] = value;
139            }
140            // IRQ latch nibbles.
141            0x8000..=0x8FFF => {
142                self.irq_latch = (self.irq_latch & 0xFFF0) | (value as u16 & 0x0F);
143            }
144            0x9000..=0x9FFF => {
145                self.irq_latch = (self.irq_latch & 0xFF0F) | ((value as u16 & 0x0F) << 4);
146            }
147            0xA000..=0xAFFF => {
148                self.irq_latch = (self.irq_latch & 0xF0FF) | ((value as u16 & 0x0F) << 8);
149            }
150            0xB000..=0xBFFF => {
151                self.irq_latch = (self.irq_latch & 0x0FFF) | ((value as u16 & 0x0F) << 12);
152            }
153            // IRQ control.
154            0xC000..=0xCFFF => {
155                self.irq_enable_after_ack = (value & 0x01) != 0;
156                self.irq_enabled = (value & 0x02) != 0;
157                self.irq_mode_8bit = (value & 0x04) != 0;
158                self.irq_pending = false;
159                if self.irq_enabled {
160                    // Reload all 16 bits regardless of mode.
161                    self.irq_counter = self.irq_latch;
162                }
163            }
164            // IRQ acknowledge.
165            0xD000..=0xDFFF => {
166                self.irq_pending = false;
167                self.irq_enabled = self.irq_enable_after_ack;
168            }
169            // PRG bank select.
170            0xF000..=0xFFFF => {
171                self.prg_bank = value & 0x07;
172            }
173            _ => {}
174        }
175    }
176
177    fn ppu_read(&mut self, addr: u16) -> u8 {
178        let addr = addr & 0x3FFF;
179        match addr {
180            0x0000..=0x1FFF => self.chr_ram[addr as usize],
181            0x2000..=0x3EFF => self.vram[self.nametable_offset(addr)],
182            _ => 0,
183        }
184    }
185
186    fn ppu_write(&mut self, addr: u16, value: u8) {
187        let addr = addr & 0x3FFF;
188        match addr {
189            0x0000..=0x1FFF => self.chr_ram[addr as usize] = value,
190            0x2000..=0x3EFF => {
191                let off = self.nametable_offset(addr);
192                self.vram[off] = value;
193            }
194            _ => {}
195        }
196    }
197
198    fn notify_cpu_cycle(&mut self) {
199        if !self.irq_enabled {
200            return;
201        }
202        if self.irq_mode_8bit {
203            // Only the low 8 bits count; on overflow from $FF reload low 8.
204            let lo = (self.irq_counter & 0x00FF) as u8;
205            if lo == 0xFF {
206                self.irq_counter = (self.irq_counter & 0xFF00) | (self.irq_latch & 0x00FF);
207                self.irq_pending = true;
208            } else {
209                self.irq_counter = (self.irq_counter & 0xFF00) | ((lo as u16) + 1);
210            }
211        } else if self.irq_counter == 0xFFFF {
212            self.irq_counter = self.irq_latch;
213            self.irq_pending = true;
214        } else {
215            self.irq_counter = self.irq_counter.wrapping_add(1);
216        }
217    }
218
219    fn irq_pending(&self) -> bool {
220        self.irq_pending
221    }
222
223    fn current_mirroring(&self) -> Mirroring {
224        self.mirroring
225    }
226
227    fn debug_info(&self) -> crate::mapper::MapperDebugInfo {
228        let mut info = crate::mapper::MapperDebugInfo {
229            mapper_id: 73,
230            name: "Konami VRC3 (73)".into(),
231            mirroring: crate::mapper::mirroring_name(self.mirroring),
232            ..Default::default()
233        };
234        info.prg_banks
235            .push(("PRG".into(), format!("{:#04x}", self.prg_bank)));
236        info.irq_state
237            .push(("latch".into(), format!("{:#06x}", self.irq_latch)));
238        info.irq_state
239            .push(("counter".into(), format!("{:#06x}", self.irq_counter)));
240        info.irq_state
241            .push(("enabled".into(), format!("{}", self.irq_enabled)));
242        info.irq_state
243            .push(("8bit".into(), format!("{}", self.irq_mode_8bit)));
244        info.irq_state
245            .push(("pending".into(), format!("{}", self.irq_pending)));
246        info
247    }
248
249    fn save_state(&self) -> Vec<u8> {
250        let mut out =
251            Vec::with_capacity(12 + self.vram.len() + self.chr_ram.len() + self.prg_ram.len());
252        out.push(SAVE_STATE_VERSION);
253        out.push(self.prg_bank);
254        out.push(self.mirroring as u8);
255        out.extend_from_slice(&self.irq_latch.to_le_bytes());
256        out.extend_from_slice(&self.irq_counter.to_le_bytes());
257        out.push(u8::from(self.irq_enabled));
258        out.push(u8::from(self.irq_enable_after_ack));
259        out.push(u8::from(self.irq_mode_8bit));
260        out.push(u8::from(self.irq_pending));
261        out.push(0); // reserved padding -> 12-byte header
262        out.extend_from_slice(&self.vram);
263        out.extend_from_slice(&self.chr_ram);
264        out.extend_from_slice(&self.prg_ram);
265        out
266    }
267
268    fn load_state(&mut self, data: &[u8]) -> Result<(), MapperError> {
269        let expected = 12 + self.vram.len() + self.chr_ram.len() + self.prg_ram.len();
270        if data.len() != expected {
271            return Err(MapperError::WrongLength {
272                expected,
273                got: data.len(),
274            });
275        }
276        if data[0] != SAVE_STATE_VERSION {
277            return Err(MapperError::UnsupportedVersion(data[0]));
278        }
279        self.prg_bank = data[1];
280        self.mirroring = match data[2] {
281            0 => Mirroring::Horizontal,
282            1 => Mirroring::Vertical,
283            2 => Mirroring::SingleScreenA,
284            3 => Mirroring::SingleScreenB,
285            4 => Mirroring::FourScreen,
286            other => return Err(MapperError::Invalid(format!("mirroring {other}"))),
287        };
288        self.irq_latch = u16::from_le_bytes([data[3], data[4]]);
289        self.irq_counter = u16::from_le_bytes([data[5], data[6]]);
290        self.irq_enabled = data[7] != 0;
291        self.irq_enable_after_ack = data[8] != 0;
292        self.irq_mode_8bit = data[9] != 0;
293        self.irq_pending = data[10] != 0;
294        // data[11] is reserved padding for alignment with the 12-byte header.
295        let mut cursor = 12;
296        self.vram
297            .copy_from_slice(&data[cursor..cursor + self.vram.len()]);
298        cursor += self.vram.len();
299        self.chr_ram
300            .copy_from_slice(&data[cursor..cursor + self.chr_ram.len()]);
301        cursor += self.chr_ram.len();
302        self.prg_ram
303            .copy_from_slice(&data[cursor..cursor + self.prg_ram.len()]);
304        Ok(())
305    }
306}
307
308#[cfg(test)]
309#[allow(clippy::cast_possible_truncation)]
310mod tests {
311    use super::*;
312
313    fn synth_prg(banks_16k: usize) -> Box<[u8]> {
314        let mut v = vec![0u8; banks_16k * PRG_BANK_16K];
315        for b in 0..banks_16k {
316            v[b * PRG_BANK_16K] = b as u8;
317        }
318        v.into_boxed_slice()
319    }
320
321    #[test]
322    fn prg_bank_select_and_fixed_last() {
323        let mut m = Vrc3::new(synth_prg(8), Mirroring::Vertical).unwrap();
324        assert_eq!(m.cpu_read(0x8000), 0);
325        assert_eq!(m.cpu_read(0xC000), 7);
326        m.cpu_write(0xF000, 5);
327        assert_eq!(m.cpu_read(0x8000), 5);
328        assert_eq!(m.cpu_read(0xC000), 7);
329    }
330
331    #[test]
332    fn irq_latch_nibble_assembly() {
333        let mut m = Vrc3::new(synth_prg(8), Mirroring::Vertical).unwrap();
334        m.cpu_write(0x8000, 0x0A); // bits 0-3
335        m.cpu_write(0x9000, 0x0B); // bits 4-7
336        m.cpu_write(0xA000, 0x0C); // bits 8-11
337        m.cpu_write(0xB000, 0x0D); // bits 12-15
338        assert_eq!(m.irq_latch, 0xDCBA);
339    }
340
341    #[test]
342    fn irq_16bit_fires_after_reload_count() {
343        let mut m = Vrc3::new(synth_prg(8), Mirroring::Vertical).unwrap();
344        // Latch = 0xFFFE: from reload, two increments reach 0xFFFF then overflow.
345        m.cpu_write(0x8000, 0x0E);
346        m.cpu_write(0x9000, 0x0F);
347        m.cpu_write(0xA000, 0x0F);
348        m.cpu_write(0xB000, 0x0F);
349        assert_eq!(m.irq_latch, 0xFFFE);
350        // Enable (bit 1) + reload all 16 bits.
351        m.cpu_write(0xC000, 0x02);
352        assert_eq!(m.irq_counter, 0xFFFE);
353        m.notify_cpu_cycle(); // 0xFFFE -> 0xFFFF
354        assert!(!m.irq_pending());
355        m.notify_cpu_cycle(); // 0xFFFF -> overflow -> IRQ + reload
356        assert!(m.irq_pending());
357        assert_eq!(m.irq_counter, 0xFFFE);
358    }
359
360    #[test]
361    fn irq_8bit_mode_only_low_byte() {
362        let mut m = Vrc3::new(synth_prg(8), Mirroring::Vertical).unwrap();
363        m.cpu_write(0x8000, 0x0E); // latch low nibble
364        m.cpu_write(0x9000, 0x0F); // latch bits 4-7 -> low byte = 0xFE
365        // Enable + 8-bit mode (bit 1 | bit 2).
366        m.cpu_write(0xC000, 0x06);
367        assert_eq!(m.irq_counter & 0xFF, 0xFE);
368        m.notify_cpu_cycle(); // 0xFE -> 0xFF
369        assert!(!m.irq_pending());
370        m.notify_cpu_cycle(); // 0xFF -> overflow -> IRQ
371        assert!(m.irq_pending());
372    }
373
374    #[test]
375    fn irq_acknowledge_moves_a_into_e() {
376        let mut m = Vrc3::new(synth_prg(8), Mirroring::Vertical).unwrap();
377        // Enable now (E=1) and set enable-after-ack (A=1).
378        m.cpu_write(0xC000, 0x03);
379        // Force an IRQ by writing $D000 should clear pending; E := A (1).
380        m.irq_pending = true;
381        m.cpu_write(0xD000, 0x00);
382        assert!(!m.irq_pending());
383        assert!(m.irq_enabled);
384    }
385
386    #[test]
387    fn disabled_counter_does_not_increment() {
388        let mut m = Vrc3::new(synth_prg(8), Mirroring::Vertical).unwrap();
389        for _ in 0..10_000 {
390            m.notify_cpu_cycle();
391        }
392        assert!(!m.irq_pending());
393        assert_eq!(m.irq_counter, 0);
394    }
395
396    #[test]
397    fn save_state_round_trip() {
398        let mut m = Vrc3::new(synth_prg(8), Mirroring::Vertical).unwrap();
399        m.cpu_write(0xF000, 3);
400        m.cpu_write(0x8000, 0x05);
401        m.cpu_write(0xC000, 0x02);
402        m.ppu_write(0x0040, 0x99);
403        let blob = m.save_state();
404        let mut m2 = Vrc3::new(synth_prg(8), Mirroring::Vertical).unwrap();
405        m2.load_state(&blob).unwrap();
406        assert_eq!(m.cpu_read(0x8000), m2.cpu_read(0x8000));
407        assert_eq!(m.ppu_read(0x0040), m2.ppu_read(0x0040));
408        assert_eq!(m.irq_counter, m2.irq_counter);
409    }
410}