Skip to main content

rustynes_mappers/
m021_vrc4.rs

1//! Konami VRC4 (mappers 21, 23, 25).
2//!
3//! Register-compatible with the VRC2 in `m022_vrc2.rs` but *pin-rewired* per
4//! board revision, which is why three mapper numbers describe one chip. That
5//! rewiring is isolated in [`vrc_a_bits`] (duplicated here rather than shared,
6//! matching how the crate treats its other small helpers).
7//!
8//! What VRC4 adds over VRC2 is the VRC IRQ counter -- the scanline/CPU-cycle
9//! counter shared with VRC3, VRC6 and VRC7 -- plus 8 KiB of PRG-RAM on the
10//! save-bearing Konami cartridges. No on-cart audio; see `m024_vrc6.rs` and
11//! `m085_vrc7.rs` for the boards that have it.
12//!
13//! See `docs/mappers.md` §Mapper coverage matrix.
14
15#![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
44/// Version byte this board writes in its mapper save-state section.
45///
46/// **v1** (through v2.9.1) carried the banking, mirroring and IRQ registers
47/// and the 2 KiB nametable RAM -- and nothing else. The 8 KiB PRG-RAM at
48/// `$6000-$7FFF` and, on a CHR-RAM cartridge, the 8 KiB CHR-RAM were left
49/// out, and the `.rns` container has no other section that carries
50/// cartridge RAM, so every save-state load, rewind step, run-ahead frame and
51/// netplay rollback kept whatever RAM the running game held instead of the
52/// saved one (core audit v2.9.2 AUD-02). **v2** appends the PRG-RAM, then the
53/// CHR-RAM when present. Since v2.9.8 (ADR 0042)
54/// `load_state` reads v2 only and refuses a v1 blob, which it used to load
55/// with the RAM left untouched.
56const 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
65/// Map a VRC2/4 register address to its (a0, a1) register-select pin pair.
66///
67/// Per the nesdev "VRC2 and VRC4" wiki, the iNES mapper number selects
68/// which CPU address lines are wired to the chip's A0/A1 register-select
69/// pins.  On real Konami boards the two candidate lines for each pin are
70/// physically tied together, so a write to *either* one drives the pin —
71/// the hardware ORs them.  Modelling that OR (rather than picking a single
72/// bit) is what makes submapper-0 iNES-1.0 ROMs decode correctly: e.g.
73/// mapper 23 games write CHR registers at both `$x002/$x003` (A1/A0) and
74/// `$x008/$x00C` (A3/A2), and a single-bit decoder collapses the latter
75/// set onto register 0.
76///
77/// Here `a0` is the chip's *high-nibble* select (register address +1) and
78/// `a1` is the *next-register* select (register address +2), matching how
79/// the callers consume the pair: `slot = a1 ? base+1 : base` and
80/// `low = !a0`.  Mapped to CPU address lines per mapper:
81///
82/// | Mapper | a0 (high) driven by | a1 (reg-sel) driven by |
83/// |--------|---------------------|------------------------|
84/// | 21     | A1, A6              | A2, A7                 |  (VRC4a/c)
85/// | 22     | A1                  | A0                     |  (VRC2a — A0/A1 SWAPPED)
86/// | 23     | A0, A2              | A1, A3                 |  (VRC4e/f, VRC2b)
87/// | 25     | A1, A3              | A0, A2                 |  (VRC4b/d, VRC2c — swapped)
88///
89/// VRC2a (mapper 22) and VRC2c (mapper 25) both wire the chip's A0 register
90/// pin to CPU A1 and A1 to CPU A0 (the swap); VRC2b (mapper 23) is straight.
91/// The v2.4.0 fix swapped 25 but left 22 straight, leaving TwinBee 3's BG
92/// tiles scrambled (the sprite slots happened to land right); v2.4.1 swaps 22.
93///
94/// Verified against the per-game register-write traces (Crisis Force /
95/// Akumajou = mapper 23 use offsets $0/$4/$8/$C; Wai Wai World 2 = mapper
96/// 21 use $0/$2/$4/$6; TwinBee 3 = mapper 22 and Goemon Gaiden = mapper 25
97/// use $0/$1/$2/$3).  NES 2.0 submappers, when present, pin a single line;
98/// OR-ing the candidate lines is a superset that decodes those correctly
99/// because a given ROM only toggles one of the board-tied lines.
100fn 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)), // VRC2a: A0/A1 SWAPPED (chip A0<-CPU A1)
105        25 => (bit(1) | bit(3), bit(0) | bit(2)), // VRC2c/VRC4b/d: swapped
106        // Mapper 23 (and any other VRC2/4 fallback).
107        _ => (bit(0) | bit(2), bit(1) | bit(3)),
108    }
109}
110
111/// VRC4 (and treats VRC2 hardware as a no-IRQ subset since the banking
112/// is identical).  IRQ counter is 8-bit, clocked per CPU cycle by
113/// default (mode bit selects scanline mode where it ticks every 114
114/// cycles).
115pub 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, // PRG mode: $9002 bit 1 swaps $8000/$C000.
123    chr: [u8; 8],
124    mirroring: Mirroring,
125    mapper_id: u16,
126    submapper: u8,
127    /// 8 KiB WRAM at $6000-$7FFF. T-60-003b (2026-05-17).
128    prg_ram: Box<[u8]>,
129
130    // IRQ counter state.
131    irq_latch: u8,
132    irq_counter: u8,
133    irq_enabled: bool,
134    irq_enable_after_ack: bool,
135    irq_mode_scanline: bool,
136    /// Sub-cycle prescaler for cycle mode (counts 0..341/3 and bumps
137    /// counter at zero — scanline-equivalent every 113.66 CPU cycles).
138    /// We approximate by counting 341 PPU dots per CPU-cycle group; per
139    /// `notify_cpu_cycle` we increment a CPU-cycle prescaler.
140    irq_prescaler: i32,
141    irq_pending: bool,
142}
143
144impl Vrc4 {
145    /// Construct a new VRC4 mapper.
146    ///
147    /// # Errors
148    ///
149    /// Returns [`MapperError::Invalid`] on size mismatch.
150    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            // 8 KiB WRAM at $6000-$7FFF (T-60-003b).
187            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    // v2.8.0 Phase 4 — CPU-cycle hook + IRQ source; no on-cart audio.
250    fn caps(&self) -> MapperCaps {
251        MapperCaps::CYCLE_IRQ
252    }
253
254    fn cpu_read(&mut self, addr: u16) -> u8 {
255        match addr {
256            // T-60-003b (2026-05-17): VRC4 carts (Konami's mid-life
257            // mappers — Ganbare Goemon 2, Wai Wai World, etc.) expose
258            // 8KB battery-backed WRAM at $6000-$7FFF. Pre-fix returned
259            // 0; games got stuck-at-uniform-gray validating save data.
260            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        // T-60-003b (2026-05-17): WRAM at $6000-$7FFF (paired with the
271        // read fix above).
272        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                    // Mirroring control.
283                    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                    // PRG mode swap.
292                    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                    // Control: bit 0 = enable_after_ack, bit 1 = enable now,
309                    // bit 2 = mode (1 = scanline mode).
310                    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                    // Acknowledge.
321                    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            // Tick prescaler at 341/3 PPU cycles per scanline = ~113.66
364            // CPU cycles.  Use 341 -= 3 each CPU cycle, reload at 0.
365            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        // --- v2 tail: the on-cart RAM (see `VRC4_SECTION_VERSION`) ---
432        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        // Only the current layout is read (v2.9.8, ADR 0042). A v1 blob, which
443        // stopped before the RAM block, is refused rather than loaded with the
444        // RAM left as it was.
445        if version != VRC4_SECTION_VERSION {
446            return Err(MapperError::UnsupportedVersion(version));
447        }
448        let ram_len = self.ram_block_len();
449        // The whole length is validated before the first field is written.
450        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    /// Bytes the v2 tail adds: the 8 KiB PRG-RAM, plus the 8 KiB CHR-RAM
494    /// when the cartridge has no CHR-ROM. Derived from the loaded ROM, so a
495    /// save and its load (same ROM, checked by the `.rns` hash tag) agree.
496    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        // VRC4a: a0_bit=1, a1_bit=2.  Control is at $F004 (a0=0, a1=1).
530        // Set latch low byte = 0xE.
531        m.cpu_write(0xF000, 0xE);
532        // Enable: bit 1 (enable now), mode=cycle (bit 2 = 0).
533        m.cpu_write(0xF004, 0x02);
534        // From counter=latch=0xE, ticks until 0xFF: 0xFF-0xE = 0xF1 ticks
535        // for the wrap, plus one to set pending.
536        for _ in 0..0xF1 + 1 {
537            m.notify_cpu_cycle();
538        }
539        assert!(m.irq_pending());
540    }
541
542    /// Core audit v2.9.2 AUD-02: the section carries the 8 KiB PRG-RAM and,
543    /// on a CHR-RAM board, the 8 KiB CHR-RAM. The core-level pin is
544    /// `rustynes_core::nes::tests::every_board_snapshot_carries_cartridge_ram`.
545    #[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        // Inside 1 KiB slot 0 (bank 0 at power-on), so the read path's
552        // banking and the write path agree whatever the registers hold.
553        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    /// v2.9.8 (ADR 0042): a v1 blob (no RAM tail, written through v2.9.1)
564    /// is refused. Until then it loaded and left the RAM as it was.
565    #[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    /// A v2 blob one byte short (inside the RAM tail) is rejected.
580    #[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}