Skip to main content

rustynes_mappers/
m024_vrc6.rs

1//! Konami VRC6 (mappers 24 and 26) -- banking, the VRC IRQ counter, and the
2//! on-cart VRC6 audio expansion.
3//!
4//! VRC6 is a VRC4-family board plus a three-voice synthesizer: two pulse
5//! channels with a programmable duty threshold (and an "ignore duty" mode
6//! that holds the output at the volume level) and one 8-step sawtooth whose
7//! accumulator produces the ramp. All three run off the CPU clock. The
8//! synthesizer is gated behind the `mapper-audio` Cargo feature (default ON);
9//! with it off the register decoders still latch, so a save state written by
10//! an audio-enabled build still loads (ADR 0004).
11//!
12//! [`VRC6_MIX_SCALE`] is shared with the NSF expansion path
13//! (`nsf_expansion.rs`), which re-uses [`Vrc6Pulse`] / [`Vrc6Saw`] verbatim so
14//! an NSF tune and the cartridge produce bit-identical output.
15//!
16//! See `docs/mappers.md` and `docs/apu-2a03.md` §Expansion-audio levels.
17
18#![allow(
19    clippy::cast_possible_truncation,
20    clippy::cast_lossless,
21    clippy::missing_const_for_fn,
22    clippy::needless_pass_by_ref_mut,
23    clippy::manual_range_patterns,
24    clippy::match_same_arms,
25    clippy::struct_excessive_bools,
26    clippy::doc_markdown,
27    clippy::range_plus_one,
28    clippy::single_match_else,
29    clippy::bool_to_int_with_if,
30    clippy::unnested_or_patterns,
31    clippy::single_match,
32    clippy::doc_lazy_continuation,
33    clippy::too_long_first_doc_paragraph
34)]
35
36use crate::cartridge::Mirroring;
37use crate::mapper::{Mapper, MapperCaps, MapperError};
38use alloc::{boxed::Box, vec::Vec};
39use alloc::{format, vec};
40
41const PRG_BANK_8K: usize = 0x2000;
42const CHR_BANK_1K: usize = 0x0400;
43const CHR_BANK_8K: usize = 0x2000;
44const NAMETABLE_SIZE: usize = 0x0400;
45const NAMETABLE_SIZE_U16: u16 = 0x0400;
46
47/// Version byte this board writes in its mapper save-state section.
48///
49/// **v1** carried the banking, mirroring and IRQ registers and the 2 KiB
50/// nametable RAM. **v2** appended the expansion-audio tail
51/// ([`VRC6_AUDIO_TAIL_LEN`] bytes). Neither carried the 8 KiB PRG-RAM at
52/// `$6000-$7FFF` nor, on a CHR-RAM cartridge, the 8 KiB CHR-RAM, and the
53/// `.rns` container has no other section that carries cartridge RAM -- so
54/// every save-state load, rewind step, run-ahead frame and netplay rollback
55/// kept whatever RAM the running game held instead of the saved one (core
56/// audit v2.9.2 AUD-02). **v3** (v2.9.2) appends the PRG-RAM, then the
57/// CHR-RAM when present, after the audio tail. Since v2.9.8 (ADR 0042)
58/// `load_state` reads v3 only; a v1/v2 blob, which it used to load with the
59/// RAM untouched, is refused.
60const VRC6_SECTION_VERSION: u8 = 3;
61
62/// Power-on contents of the eight 1 KiB CHR bank registers
63/// (`$D000-$D003`, `$E000-$E003`): the identity layout, slot `i` -> bank `i`.
64///
65/// **This is an ASSUMPTION, not documented hardware behaviour.** The NESdev
66/// VRC6 page states no power-on or reset value for any VRC6 register, and the
67/// register file is plain latches whose contents at power-on are whatever the
68/// silicon settles to. Until v2.9.8 the board powered on with all eight slots
69/// on bank 0, which is equally undocumented. The identity layout was chosen
70/// (maintainer decision "Identity default", 2026-10-01) because one program
71/// depends on it and none is known to depend on anything else: *Pulsewave
72/// Invite* (2009, PD) never writes `$D000-$E003` and draws its postcard only
73/// when the first 8 KiB of CHR-ROM is mapped in order -- with every slot on
74/// bank 0 it shows a field of misplaced tiles. The other ten mapper 24/26
75/// dumps in the local corpus (the three licensed games, their translations
76/// and hacks, and three homebrew programs) render byte-identical boot frames
77/// under either layout -- checked frame by frame when this landed.
78///
79/// Applied at power-on only (construction, which is also what a power cycle
80/// does). `Mapper::reset` stays the default no-op: no reset input to the VRC6
81/// is documented, and a console soft reset leaves cartridge register latches
82/// alone on almost every board, so a game reset mid-play keeps its banks.
83///
84/// PRG is deliberately not touched: `$8000`/`$C000` still power on as 0. The
85/// reset vector lives in the fixed `$E000` bank, no documentation states a
86/// PRG power-on value, and no dump in the corpus gives evidence for one.
87const POWER_ON_CHR: [u8; 8] = [0, 1, 2, 3, 4, 5, 6, 7];
88
89/// Bytes of the v2 audio tail: `audio_ctrl` (1) + two pulses (7 each) + the
90/// sawtooth (8).
91const VRC6_AUDIO_TAIL_LEN: usize = 1 + 7 + 7 + 8;
92
93fn nametable_offset(addr: u16, mirroring: Mirroring) -> usize {
94    let table = (((addr - 0x2000) / NAMETABLE_SIZE_U16) & 0x03) as u8;
95    let local = (addr as usize) & (NAMETABLE_SIZE - 1);
96    let physical = mirroring.physical_bank(table);
97    physical * NAMETABLE_SIZE + local
98}
99
100/// Linear scale applied to the summed VRC6 channel output (see
101/// [`Vrc6::mix_audio`]).
102///
103/// Calibrated (v2.2.7 "Timbre II") so a single full-volume (15) VRC6 pulse
104/// reaches ~**1.0x** the amplitude of a single full-volume 2A03 pulse — the
105/// level the NESdev wiki ("at maximum volume, the pulse channels of the VRC6
106/// are roughly *equivalent* to the pulse channels of the 2A03"), the bbbradsmith
107/// `db_vrc6` decibel-comparison ROM's matched-level intent, and the wider
108/// reference field all corroborate: rustico, tetanes, and BizHawk each encode a
109/// VRC6 pulse == a 2A03 pulse *exactly*, and ares/higan/nestopia reach the same
110/// target via a `sum/61` normalization. Concretely, one pulse toggling 0↔15
111/// swings the mixer by `15 * 650 = 9750` raw units; divided by the bus's
112/// `/65536` external-audio normalization that is `0.1488`, matching the 2A03
113/// pulse's `pulse_table[15] ≈ 0.1488` — a ratio of `1.00`. The full
114/// three-channel peak stays in range: `(61 - 30) * 650 = 20150 < i16::MAX`, so a
115/// loud Akumajou-Densetsu / Madara passage never clips.
116///
117/// **History:** before v2.2.7 this was `979` (~1.506x the 2A03 pulse), which
118/// mirrored Mesen2's specifically *louder* mixer convention (Mesen2 weights VRC6
119/// at `output * 5` in `NesSoundMixer::GetOutputVolume`). A NESdev-forum reviewer
120/// flagged the VRC6 balance as too loud; a cross-check against the whole
121/// field of reference emulators (see the cross-reference in the v2.2.7 notes) confirmed
122/// Mesen2 is the loud outlier and the field/hardware consensus is 1.0x. Before
123/// v2.1.6 it was `256` (≈0.39x — ~11.7 dB too quiet). See `docs/apu-2a03.md`
124/// §Expansion-audio levels.
125///
126/// `pub(crate)` so the NSF-playback path (`crate::nsf_expansion::Vrc6Exp::mix`)
127/// references the SAME constant as the cartridge path — the two mixers can
128/// never drift apart, guaranteeing an NSF VRC6 tune stays level-matched to a
129/// VRC6 cartridge.
130pub(crate) const VRC6_MIX_SCALE: i16 = 650;
131
132/// VRC6 audio pulse channel state (`$9000-$9002` for pulse 1, `$A000-$A002`
133/// for pulse 2). Period is 12-bit, decrements every CPU cycle. On
134/// underflow, the duty index advances by 1 (mod 16). Output is volume when
135/// duty index <= duty-cycle threshold (or always-on when "ignore duty" mode
136/// is set); zero otherwise.
137#[derive(Clone, Default)]
138pub(crate) struct Vrc6Pulse {
139    /// Bits 0-3: volume (0..=15). Bits 4-6: duty (0..=7, sets the duty-cycle
140    /// threshold). Bit 7: ignore-duty (output always = volume).
141    pub(crate) ctrl: u8,
142    /// 12-bit period reload value.
143    pub(crate) period: u16,
144    /// Channel enable bit (from period-hi bit 7).
145    pub(crate) enabled: bool,
146    /// 12-bit countdown timer.
147    pub(crate) timer: u16,
148    /// 4-bit duty-cycle step (0..=15).
149    pub(crate) step: u8,
150}
151
152impl Vrc6Pulse {
153    /// Clock the timer one CPU cycle. When it underflows, advance the duty
154    /// step and reload from `period`.
155    pub(crate) fn clock(&mut self) {
156        if !self.enabled {
157            return;
158        }
159        if self.timer == 0 {
160            self.timer = self.period;
161            self.step = (self.step + 1) & 0x0F;
162        } else {
163            self.timer -= 1;
164        }
165    }
166
167    /// Current 4-bit unsigned output (0..=15). 0 when disabled.
168    pub(crate) fn output(&self) -> u8 {
169        if !self.enabled {
170            return 0;
171        }
172        let duty = (self.ctrl >> 4) & 0x07;
173        let ignore_duty = (self.ctrl & 0x80) != 0;
174        let volume = self.ctrl & 0x0F;
175        if ignore_duty || self.step <= duty {
176            volume
177        } else {
178            0
179        }
180    }
181}
182
183/// VRC6 audio sawtooth channel state (`$B000-$B002`). 6-bit accumulator
184/// adds an "accumulator rate" once per CPU cycle. Every 14th underflow,
185/// the high 5 bits of the accumulator are emitted (0..=31) and the
186/// accumulator resets.
187#[derive(Clone, Default)]
188pub(crate) struct Vrc6Saw {
189    /// 6-bit accumulator-rate value (bits 5-0 of `$B000`).
190    pub(crate) rate: u8,
191    /// 12-bit period reload value.
192    pub(crate) period: u16,
193    /// Channel enable bit (from period-hi bit 7).
194    pub(crate) enabled: bool,
195    /// 12-bit countdown timer.
196    pub(crate) timer: u16,
197    /// Internal step counter 0..=13 (every other increment "ticks the
198    /// accumulator"; 7 ticks per cycle = 14 steps).
199    pub(crate) step: u8,
200    /// 8-bit accumulator. Output = accumulator >> 3 (5-bit, 0..=31).
201    pub(crate) acc: u8,
202}
203
204impl Vrc6Saw {
205    pub(crate) fn clock(&mut self) {
206        if !self.enabled {
207            return;
208        }
209        if self.timer == 0 {
210            self.timer = self.period;
211            // Step 0..=13: every 2nd step (1, 3, 5, 7, 9, 11, 13) accumulates.
212            // Step 14 (== reset) zeros the accumulator and rolls step to 0.
213            self.step += 1;
214            if (self.step & 1) == 1 {
215                self.acc = self.acc.wrapping_add(self.rate);
216            }
217            if self.step >= 14 {
218                self.step = 0;
219                self.acc = 0;
220            }
221        } else {
222            self.timer -= 1;
223        }
224    }
225
226    /// 5-bit unsigned output (0..=31).
227    pub(crate) fn output(&self) -> u8 {
228        if !self.enabled {
229            return 0;
230        }
231        self.acc >> 3
232    }
233}
234
235/// VRC6 (Mappers 24 / 26).  Audio extension is implemented behind the
236/// `mapper-audio` Cargo feature (default ON).
237pub struct Vrc6 {
238    prg_rom: Box<[u8]>,
239    chr_rom: Box<[u8]>,
240    vram: Box<[u8]>,
241    chr_is_ram: bool,
242    prg_16: u8, // 16 KiB bank @ $8000-$BFFF
243    prg_8: u8,  // 8 KiB bank @ $C000-$DFFF
244    chr: [u8; 8],
245    mirroring: Mirroring,
246    /// 8 KiB WRAM at $6000-$7FFF (battery-backed on Konami carts).
247    /// T-60-003b (2026-05-17).
248    prg_ram: Box<[u8]>,
249    /// Mapper 24 = VRC6a (a0/a1 = bits 0/1).
250    /// Mapper 26 = VRC6b (a0/a1 = bits 1/0 — swapped).
251    swap_a01: bool,
252
253    irq_latch: u8,
254    irq_counter: u8,
255    irq_enabled: bool,
256    irq_enable_after_ack: bool,
257    irq_mode_scanline: bool,
258    irq_prescaler: i32,
259    irq_pending: bool,
260
261    // Audio extension state.
262    /// `$9003` global audio control. Bit 0 = halt-all; bits 1-2 = freq scale
263    /// shift (0 = ÷1, 1 = ÷16, 2 = ÷256 — implemented by left-shifting the
264    /// effective period). We keep the raw byte and inspect bits at clock time.
265    audio_ctrl: u8,
266    pulse1: Vrc6Pulse,
267    pulse2: Vrc6Pulse,
268    saw: Vrc6Saw,
269}
270
271#[cfg_attr(not(feature = "mapper-audio"), allow(dead_code))]
272impl Vrc6 {
273    /// Construct a new VRC6 mapper.
274    ///
275    /// # Errors
276    ///
277    /// Returns [`MapperError::Invalid`] on size mismatch.
278    pub fn new(
279        prg_rom: Box<[u8]>,
280        chr_rom: Box<[u8]>,
281        mapper_id: u16,
282        mirroring: Mirroring,
283    ) -> Result<Self, MapperError> {
284        if prg_rom.is_empty() || !prg_rom.len().is_multiple_of(PRG_BANK_8K) {
285            return Err(MapperError::Invalid(format!(
286                "VRC6 PRG-ROM size {} is not a non-zero multiple of 8 KiB",
287                prg_rom.len()
288            )));
289        }
290        let chr_is_ram = chr_rom.is_empty();
291        let chr: Box<[u8]> = if chr_is_ram {
292            vec![0u8; CHR_BANK_8K].into_boxed_slice()
293        } else if chr_rom.len().is_multiple_of(CHR_BANK_1K) {
294            chr_rom
295        } else {
296            return Err(MapperError::Invalid(format!(
297                "VRC6 CHR-ROM size {} is not a multiple of 1 KiB",
298                chr_rom.len()
299            )));
300        };
301        Ok(Self {
302            prg_rom,
303            chr_rom: chr,
304            vram: vec![0u8; 2 * NAMETABLE_SIZE].into_boxed_slice(),
305            chr_is_ram,
306            prg_16: 0,
307            prg_8: 0,
308            chr: POWER_ON_CHR,
309            mirroring,
310            // 8 KiB WRAM at $6000-$7FFF (T-60-003b).
311            prg_ram: vec![0u8; 8 * 1024].into_boxed_slice(),
312            swap_a01: mapper_id == 26,
313            irq_latch: 0,
314            irq_counter: 0,
315            irq_enabled: false,
316            irq_enable_after_ack: false,
317            irq_mode_scanline: false,
318            irq_prescaler: 341,
319            irq_pending: false,
320            audio_ctrl: 0,
321            pulse1: Vrc6Pulse::default(),
322            pulse2: Vrc6Pulse::default(),
323            saw: Vrc6Saw::default(),
324        })
325    }
326
327    /// Effective period for a pulse/saw channel, taking the global
328    /// `$9003` halt + frequency-scale bits into account.
329    fn effective_period_p(&self, p: &Vrc6Pulse) -> u16 {
330        let shift = match (self.audio_ctrl >> 1) & 0x03 {
331            0 => 0,
332            1 => 4,
333            _ => 8,
334        };
335        p.period >> shift
336    }
337
338    fn effective_period_s(&self) -> u16 {
339        let shift = match (self.audio_ctrl >> 1) & 0x03 {
340            0 => 0,
341            1 => 4,
342            _ => 8,
343        };
344        self.saw.period >> shift
345    }
346
347    /// Clock all three audio channels one CPU cycle. Called from
348    /// `notify_cpu_cycle` when the `mapper-audio` feature is on.
349    #[cfg(feature = "mapper-audio")]
350    fn clock_audio(&mut self) {
351        // $9003 bit 0 = halt-all. When set, channels do not advance.
352        if (self.audio_ctrl & 0x01) != 0 {
353            return;
354        }
355        // Apply the frequency-scale shift transiently by temporarily
356        // narrowing `period` for the channel clock. We don't mutate the
357        // stored period -- the shift is purely a read-time scaling.
358        let p1_period = self.effective_period_p(&self.pulse1);
359        let p2_period = self.effective_period_p(&self.pulse2);
360        let saw_period = self.effective_period_s();
361        let saved_p1 = self.pulse1.period;
362        let saved_p2 = self.pulse2.period;
363        let saved_saw = self.saw.period;
364        self.pulse1.period = p1_period;
365        self.pulse2.period = p2_period;
366        self.saw.period = saw_period;
367        self.pulse1.clock();
368        self.pulse2.clock();
369        self.saw.clock();
370        self.pulse1.period = saved_p1;
371        self.pulse2.period = saved_p2;
372        self.saw.period = saved_saw;
373    }
374
375    fn prg_offset(&self, addr: u16) -> usize {
376        let total_8k = (self.prg_rom.len() / PRG_BANK_8K).max(1);
377        let last1 = total_8k - 1;
378        match addr {
379            0x8000..=0xBFFF => {
380                let bank16 = (self.prg_16 as usize) & 0x0F;
381                let bank8 = (bank16 << 1) | (((addr & 0x2000) >> 13) as usize);
382                (bank8 % total_8k) * PRG_BANK_8K + (addr as usize & 0x1FFF)
383            }
384            0xC000..=0xDFFF => {
385                let bank8 = (self.prg_8 as usize) & 0x1F;
386                (bank8 % total_8k) * PRG_BANK_8K + (addr as usize & 0x1FFF)
387            }
388            0xE000..=0xFFFF => last1 * PRG_BANK_8K + (addr as usize & 0x1FFF),
389            _ => 0,
390        }
391    }
392
393    fn chr_offset(&self, addr: u16) -> usize {
394        let addr = (addr & 0x1FFF) as usize;
395        let total_1k = (self.chr_rom.len() / CHR_BANK_1K).max(1);
396        let slot = addr / CHR_BANK_1K;
397        let bank = (self.chr[slot] as usize) % total_1k;
398        bank * CHR_BANK_1K + (addr & (CHR_BANK_1K - 1))
399    }
400
401    fn clock_irq_counter(&mut self) {
402        if self.irq_counter == 0xFF {
403            self.irq_counter = self.irq_latch;
404            self.irq_pending = true;
405        } else {
406            self.irq_counter = self.irq_counter.wrapping_add(1);
407        }
408    }
409
410    fn decode_a(&self, addr: u16) -> u8 {
411        let a0 = (addr & 1) != 0;
412        let a1 = (addr & 2) != 0;
413        let (a0, a1) = if self.swap_a01 { (a1, a0) } else { (a0, a1) };
414        u8::from(a0) | (u8::from(a1) << 1)
415    }
416}
417
418impl Mapper for Vrc6 {
419    fn sram(&self) -> &[u8] {
420        &self.prg_ram
421    }
422    fn sram_mut(&mut self) -> &mut [u8] {
423        &mut self.prg_ram
424    }
425    // v2.8.0 Phase 4 — CPU-cycle hook + IRQ source + expansion audio
426    // (the audio hook only exists under the `mapper-audio` feature).
427    fn caps(&self) -> MapperCaps {
428        MapperCaps {
429            cpu_cycle_hook: true,
430            audio: cfg!(feature = "mapper-audio"),
431            frame_event_hook: false,
432            irq_source: true,
433        }
434    }
435
436    fn cpu_read(&mut self, addr: u16) -> u8 {
437        match addr {
438            // T-60-003b (2026-05-17): VRC6 carts (Akumajou Densetsu /
439            // Esper Dream 2 / Mouryou Senki Madara) include 8KB
440            // battery-backed WRAM at $6000-$7FFF. Pre-fix returned 0;
441            // Esper Dream 2 + Madara got stuck-at-uniform-gray
442            // validating save data, both bit-identical hash
443            // 89ee4c476c97a325 (the smoking-gun signal that pointed
444            // here per the recovery-session diagnostic at
445            // docs/audit/v1-closeout-progress-2026-05-17.md).
446            0x6000..=0x7FFF => self.prg_ram[(addr - 0x6000) as usize % self.prg_ram.len()],
447            0x8000..=0xFFFF => {
448                let off = self.prg_offset(addr);
449                self.prg_rom[off % self.prg_rom.len()]
450            }
451            _ => 0,
452        }
453    }
454
455    fn cpu_write(&mut self, addr: u16, value: u8) {
456        // T-60-003b (2026-05-17): WRAM at $6000-$7FFF (paired with the
457        // read fix above).
458        if (0x6000..=0x7FFF).contains(&addr) {
459            let len = self.prg_ram.len();
460            self.prg_ram[(addr - 0x6000) as usize % len] = value;
461            return;
462        }
463        let a = self.decode_a(addr);
464        match addr & 0xF000 {
465            0x8000 => self.prg_16 = value & 0x0F,
466            0x9000 => match a {
467                // $9000: Pulse 1 control (volume/duty/mode).
468                0 => self.pulse1.ctrl = value,
469                // $9001: Pulse 1 period low.
470                1 => {
471                    self.pulse1.period = (self.pulse1.period & 0x0F00) | u16::from(value);
472                }
473                // $9002: Pulse 1 period high + enable.
474                2 => {
475                    self.pulse1.period =
476                        (self.pulse1.period & 0x00FF) | (u16::from(value & 0x0F) << 8);
477                    self.pulse1.enabled = (value & 0x80) != 0;
478                    if !self.pulse1.enabled {
479                        self.pulse1.step = 0;
480                    }
481                }
482                // $9003: Global audio control (halt + freq scale).
483                _ => self.audio_ctrl = value,
484            },
485            0xA000 => match a {
486                // $A000: Pulse 2 control.
487                0 => self.pulse2.ctrl = value,
488                // $A001: Pulse 2 period low.
489                1 => {
490                    self.pulse2.period = (self.pulse2.period & 0x0F00) | u16::from(value);
491                }
492                // $A002: Pulse 2 period high + enable.
493                2 => {
494                    self.pulse2.period =
495                        (self.pulse2.period & 0x00FF) | (u16::from(value & 0x0F) << 8);
496                    self.pulse2.enabled = (value & 0x80) != 0;
497                    if !self.pulse2.enabled {
498                        self.pulse2.step = 0;
499                    }
500                }
501                _ => {}
502            },
503            0xB000 => match a {
504                // $B000: Sawtooth accumulator rate (6-bit).
505                0 => self.saw.rate = value & 0x3F,
506                // $B001: Sawtooth period low.
507                1 => {
508                    self.saw.period = (self.saw.period & 0x0F00) | u16::from(value);
509                }
510                // $B002: Sawtooth period high + enable.
511                2 => {
512                    self.saw.period = (self.saw.period & 0x00FF) | (u16::from(value & 0x0F) << 8);
513                    self.saw.enabled = (value & 0x80) != 0;
514                    if !self.saw.enabled {
515                        self.saw.step = 0;
516                        self.saw.acc = 0;
517                    }
518                }
519                _ => {
520                    // $B003: Mirroring + PPU/CPU mode.
521                    self.mirroring = match (value >> 2) & 0x03 {
522                        0 => Mirroring::Vertical,
523                        1 => Mirroring::Horizontal,
524                        2 => Mirroring::SingleScreenA,
525                        _ => Mirroring::SingleScreenB,
526                    };
527                }
528            },
529            0xC000 => self.prg_8 = value & 0x1F,
530            0xD000 => self.chr[a as usize] = value,
531            0xE000 => self.chr[(a + 4) as usize] = value,
532            0xF000 => match a {
533                0 => self.irq_latch = value,
534                1 => {
535                    self.irq_enable_after_ack = (value & 0x01) != 0;
536                    self.irq_enabled = (value & 0x02) != 0;
537                    self.irq_mode_scanline = (value & 0x04) == 0;
538                    if self.irq_enabled {
539                        self.irq_counter = self.irq_latch;
540                        self.irq_prescaler = 341;
541                    }
542                    self.irq_pending = false;
543                }
544                2 => {
545                    self.irq_pending = false;
546                    self.irq_enabled = self.irq_enable_after_ack;
547                }
548                _ => {}
549            },
550            _ => {}
551        }
552    }
553
554    fn ppu_read(&mut self, addr: u16) -> u8 {
555        let addr = addr & 0x3FFF;
556        match addr {
557            0x0000..=0x1FFF => {
558                let off = self.chr_offset(addr);
559                self.chr_rom[off % self.chr_rom.len()]
560            }
561            0x2000..=0x3EFF => self.vram[nametable_offset(addr, self.mirroring) % self.vram.len()],
562            _ => 0,
563        }
564    }
565
566    fn ppu_write(&mut self, addr: u16, value: u8) {
567        let addr = addr & 0x3FFF;
568        match addr {
569            0x0000..=0x1FFF => {
570                if self.chr_is_ram {
571                    let len = self.chr_rom.len();
572                    self.chr_rom[addr as usize % len] = value;
573                }
574            }
575            0x2000..=0x3EFF => {
576                let off = nametable_offset(addr, self.mirroring) % self.vram.len();
577                self.vram[off] = value;
578            }
579            _ => {}
580        }
581    }
582
583    fn notify_cpu_cycle(&mut self) {
584        // Audio runs every CPU cycle regardless of IRQ state.
585        #[cfg(feature = "mapper-audio")]
586        self.clock_audio();
587
588        if !self.irq_enabled {
589            return;
590        }
591        if self.irq_mode_scanline {
592            self.irq_prescaler -= 3;
593            if self.irq_prescaler <= 0 {
594                self.irq_prescaler += 341;
595                self.clock_irq_counter();
596            }
597        } else {
598            self.clock_irq_counter();
599        }
600    }
601
602    #[cfg(feature = "mapper-audio")]
603    fn mix_audio(&mut self) -> i32 {
604        // Three channels: pulse1 (4-bit, 0..=15), pulse2 (4-bit, 0..=15),
605        // sawtooth (5-bit, 0..=31). Sum is in 0..=61.
606        //
607        // Per nesdev "VRC6 audio": the three channels are summed digitally
608        // (a 6-bit DAC over two 4-bit pulses + the high 5 bits of the saw), so a
609        // linear sum is the canonical mix. The [`VRC6_MIX_SCALE`] = 650 factor
610        // makes a single full-volume pulse ~1.0x the 2A03 pulse (the NESdev /
611        // field / `db_vrc6` consensus level); the full three-channel peak
612        // `(61 - 30) * 650 = 20150` stays below `i16::MAX`.
613        let p1 = i16::from(self.pulse1.output());
614        let p2 = i16::from(self.pulse2.output());
615        let saw = i16::from(self.saw.output());
616        // Center at zero: subtract approx half the peak (~30), then scale by
617        // [`VRC6_MIX_SCALE`] for a hardware-accurate level vs the 2A03 pulse.
618        i32::from(((p1 + p2 + saw) - 30) * VRC6_MIX_SCALE)
619    }
620
621    fn irq_pending(&self) -> bool {
622        self.irq_pending
623    }
624
625    fn current_mirroring(&self) -> Mirroring {
626        self.mirroring
627    }
628
629    fn debug_info(&self) -> crate::mapper::MapperDebugInfo {
630        let mapper_id = if self.swap_a01 { 26 } else { 24 };
631        let mut info = crate::mapper::MapperDebugInfo {
632            mapper_id,
633            name: "VRC6".into(),
634            mirroring: crate::mapper::mirroring_name(self.current_mirroring()),
635            ..Default::default()
636        };
637        info.prg_banks
638            .push(("PRG16".into(), format!("{:#04x}", self.prg_16)));
639        info.prg_banks
640            .push(("PRG8".into(), format!("{:#04x}", self.prg_8)));
641        for (i, b) in self.chr.iter().enumerate() {
642            info.chr_banks
643                .push((format!("CHR{i}"), format!("{b:#04x}")));
644        }
645        info.irq_state
646            .push(("latch".into(), format!("{:#04x}", self.irq_latch)));
647        info.irq_state
648            .push(("counter".into(), format!("{:#04x}", self.irq_counter)));
649        info.irq_state
650            .push(("enabled".into(), format!("{}", self.irq_enabled)));
651        info.irq_state
652            .push(("pending".into(), format!("{}", self.irq_pending)));
653        info
654    }
655
656    fn save_state(&self) -> Vec<u8> {
657        // v2: appends audio state (audio_ctrl + 3 channels) at the end.
658        // Per ADR-0003: strictly additive; older readers ignore the tail.
659        // Channel layout per channel: ctrl(1) + period_lo(1) + period_hi(1)
660        //   + enabled(1) + timer_lo(1) + timer_hi(1) + step(1)
661        //   = 7 bytes for a pulse channel.
662        // Saw: rate(1) + period_lo(1) + period_hi(1) + enabled(1)
663        //   + timer_lo(1) + timer_hi(1) + step(1) + acc(1) = 8 bytes.
664        // Header: audio_ctrl(1).
665        // Total audio tail = 1 + 7 + 7 + 8 = 23 bytes.
666        // v3: appends the on-cart RAM after the audio tail; see
667        // `VRC6_SECTION_VERSION`.
668        let mut out =
669            Vec::with_capacity(48 + self.vram.len() + VRC6_AUDIO_TAIL_LEN + self.ram_block_len());
670        out.push(VRC6_SECTION_VERSION);
671        out.push(self.prg_16);
672        out.push(self.prg_8);
673        out.extend_from_slice(&self.chr);
674        out.push(self.mirroring as u8);
675        out.push(u8::from(self.swap_a01));
676        out.push(self.irq_latch);
677        out.push(self.irq_counter);
678        out.push(u8::from(self.irq_enabled));
679        out.push(u8::from(self.irq_enable_after_ack));
680        out.push(u8::from(self.irq_mode_scanline));
681        out.extend_from_slice(&self.irq_prescaler.to_le_bytes());
682        out.push(u8::from(self.irq_pending));
683        out.extend_from_slice(&self.vram);
684        // Audio tail (v2).
685        out.push(self.audio_ctrl);
686        Self::write_pulse(&mut out, &self.pulse1);
687        Self::write_pulse(&mut out, &self.pulse2);
688        Self::write_saw(&mut out, &self.saw);
689        // RAM tail (v3).
690        out.extend_from_slice(&self.prg_ram);
691        if self.chr_is_ram {
692            out.extend_from_slice(&self.chr_rom);
693        }
694        out
695    }
696
697    fn load_state(&mut self, data: &[u8]) -> Result<(), MapperError> {
698        let scalar_len = 1 + 1 + 1 + 8 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 4 + 1;
699        let core_expected = scalar_len + self.vram.len();
700        if data.len() < core_expected {
701            return Err(MapperError::WrongLength {
702                expected: core_expected,
703                got: data.len(),
704            });
705        }
706        let version = data[0];
707        // Only the current version is read (v2.9.8, ADR 0042). v1 (no audio
708        // tail) and v2 (no RAM tail, and a short audio tail tolerated) used
709        // to load with those parts defaulted or left as they were.
710        if version != VRC6_SECTION_VERSION {
711            return Err(MapperError::UnsupportedVersion(version));
712        }
713        // Strict: core + the full audio tail + the RAM tail, exactly.
714        // Validated before the first field is written.
715        let tail_off = core_expected;
716        let expected = tail_off + VRC6_AUDIO_TAIL_LEN + self.ram_block_len();
717        if data.len() != expected {
718            return Err(MapperError::WrongLength {
719                expected,
720                got: data.len(),
721            });
722        }
723        self.prg_16 = data[1];
724        self.prg_8 = data[2];
725        self.chr.copy_from_slice(&data[3..11]);
726        self.mirroring = match data[11] {
727            0 => Mirroring::Horizontal,
728            1 => Mirroring::Vertical,
729            2 => Mirroring::SingleScreenA,
730            3 => Mirroring::SingleScreenB,
731            4 => Mirroring::FourScreen,
732            5 => Mirroring::MapperControlled,
733            other => return Err(MapperError::Invalid(format!("mirroring {other}"))),
734        };
735        self.swap_a01 = data[12] != 0;
736        self.irq_latch = data[13];
737        self.irq_counter = data[14];
738        self.irq_enabled = data[15] != 0;
739        self.irq_enable_after_ack = data[16] != 0;
740        self.irq_mode_scanline = data[17] != 0;
741        self.irq_prescaler = i32::from_le_bytes(
742            data[18..22]
743                .try_into()
744                .map_err(|_| MapperError::Invalid("prescaler".into()))?,
745        );
746        self.irq_pending = data[22] != 0;
747        self.vram.copy_from_slice(&data[23..23 + self.vram.len()]);
748
749        // v2 tail: audio state.
750        self.audio_ctrl = data[tail_off];
751        Self::read_pulse(&data[tail_off + 1..tail_off + 8], &mut self.pulse1);
752        Self::read_pulse(&data[tail_off + 8..tail_off + 15], &mut self.pulse2);
753        Self::read_saw(&data[tail_off + 15..tail_off + 23], &mut self.saw);
754        // v3 RAM tail.
755        let ram_off = tail_off + VRC6_AUDIO_TAIL_LEN;
756        let (prg, chr) = data[ram_off..].split_at(self.prg_ram.len());
757        self.prg_ram.copy_from_slice(prg);
758        if self.chr_is_ram {
759            self.chr_rom.copy_from_slice(chr);
760        }
761        Ok(())
762    }
763}
764
765impl Vrc6 {
766    /// Bytes the v3 tail adds: the 8 KiB PRG-RAM, plus the 8 KiB CHR-RAM
767    /// when the cartridge has no CHR-ROM. Derived from the loaded ROM, so a
768    /// save and its load (same ROM, checked by the `.rns` hash tag) agree.
769    fn ram_block_len(&self) -> usize {
770        self.prg_ram.len()
771            + if self.chr_is_ram {
772                self.chr_rom.len()
773            } else {
774                0
775            }
776    }
777}
778
779impl Vrc6 {
780    fn write_pulse(out: &mut Vec<u8>, p: &Vrc6Pulse) {
781        out.push(p.ctrl);
782        out.extend_from_slice(&p.period.to_le_bytes());
783        out.push(u8::from(p.enabled));
784        out.extend_from_slice(&p.timer.to_le_bytes());
785        out.push(p.step);
786    }
787
788    fn write_saw(out: &mut Vec<u8>, s: &Vrc6Saw) {
789        out.push(s.rate);
790        out.extend_from_slice(&s.period.to_le_bytes());
791        out.push(u8::from(s.enabled));
792        out.extend_from_slice(&s.timer.to_le_bytes());
793        out.push(s.step);
794        out.push(s.acc);
795    }
796
797    fn read_pulse(src: &[u8], p: &mut Vrc6Pulse) {
798        p.ctrl = src[0];
799        p.period = u16::from_le_bytes([src[1], src[2]]);
800        p.enabled = src[3] != 0;
801        p.timer = u16::from_le_bytes([src[4], src[5]]);
802        p.step = src[6] & 0x0F;
803    }
804
805    fn read_saw(src: &[u8], s: &mut Vrc6Saw) {
806        s.rate = src[0] & 0x3F;
807        s.period = u16::from_le_bytes([src[1], src[2]]);
808        s.enabled = src[3] != 0;
809        s.timer = u16::from_le_bytes([src[4], src[5]]);
810        s.step = src[6];
811        s.acc = src[7];
812    }
813}
814
815#[cfg(test)]
816mod tests {
817    use super::*;
818
819    fn synth(banks_8k: usize) -> Box<[u8]> {
820        let mut v = vec![0u8; banks_8k * PRG_BANK_8K];
821        for b in 0..banks_8k {
822            v[b * PRG_BANK_8K] = b as u8;
823        }
824        v.into_boxed_slice()
825    }
826
827    fn synth_chr(banks_1k: usize) -> Box<[u8]> {
828        let mut v = vec![0u8; banks_1k * CHR_BANK_1K];
829        for b in 0..banks_1k {
830            v[b * CHR_BANK_1K] = b as u8;
831        }
832        v.into_boxed_slice()
833    }
834
835    #[test]
836    fn vrc6_audio_register_decoders_latch_state() {
837        let mut m = Vrc6::new(synth(8), synth_chr(8), 24, Mirroring::Vertical).unwrap();
838        // Pulse 1 ctrl = 0x8F (ignore-duty + volume 0xF).
839        m.cpu_write(0x9000, 0x8F);
840        // Pulse 1 period = 0x123 with enable bit.
841        m.cpu_write(0x9001, 0x23);
842        m.cpu_write(0x9002, 0x81); // bit 7 = enable, high nibble = 1.
843        assert!(m.pulse1.enabled);
844        assert_eq!(m.pulse1.period, 0x123);
845        assert_eq!(m.pulse1.ctrl, 0x8F);
846
847        // Pulse 2 similar.
848        m.cpu_write(0xA000, 0x07); // duty 0 -> threshold 0; volume 7.
849        m.cpu_write(0xA001, 0x40);
850        m.cpu_write(0xA002, 0x80); // enable, period high nibble 0.
851        assert!(m.pulse2.enabled);
852        assert_eq!(m.pulse2.period, 0x040);
853
854        // Sawtooth.
855        m.cpu_write(0xB000, 0x05); // rate = 5.
856        m.cpu_write(0xB001, 0x20);
857        m.cpu_write(0xB002, 0x80); // enable.
858        assert!(m.saw.enabled);
859        assert_eq!(m.saw.rate, 5);
860        assert_eq!(m.saw.period, 0x020);
861
862        // $B003 still drives mirroring.
863        m.cpu_write(0xB003, 0b0000_0100); // bits 3:2 = 01 -> Horizontal.
864        assert_eq!(m.current_mirroring(), Mirroring::Horizontal);
865    }
866
867    #[test]
868    fn vrc6_pulse_oscillator_steps_through_duty() {
869        let mut p = Vrc6Pulse {
870            ctrl: 0x4F, // duty = 0b100 (4) so output high while step <= 4.
871            period: 4,  // small, ticks fast.
872            enabled: true,
873            timer: 0,
874            step: 0,
875        };
876        // First clock: timer == 0 so we reload and bump step to 1.
877        let mut outputs = Vec::new();
878        for _ in 0..32 {
879            p.clock();
880            outputs.push(p.output());
881        }
882        // We expect a roughly 5/16 duty cycle pattern of volume(15) intervals
883        // separated by zero intervals. Sanity-check both poles appear.
884        assert!(outputs.contains(&0x0F));
885        assert!(outputs.contains(&0));
886    }
887
888    #[test]
889    fn vrc6_sawtooth_emits_ramp() {
890        let mut s = Vrc6Saw {
891            rate: 0x10,
892            period: 2,
893            enabled: true,
894            timer: 0,
895            step: 0,
896            acc: 0,
897        };
898        // Drive long enough to see at least one full 14-step ramp.
899        let mut sampled = Vec::new();
900        for _ in 0..60 {
901            s.clock();
902            sampled.push(s.output());
903        }
904        // Ramp should reach a peak greater than zero and eventually reset.
905        let peak = sampled.iter().copied().max().unwrap();
906        assert!(peak > 0, "saw must emit a non-zero peak");
907        // And it should hit zero (after step >= 14 reset).
908        assert!(sampled.contains(&0));
909    }
910
911    #[cfg(feature = "mapper-audio")]
912    #[test]
913    fn vrc6_mix_audio_is_nonzero_when_active() {
914        let mut m = Vrc6::new(synth(8), synth_chr(8), 24, Mirroring::Vertical).unwrap();
915        // Enable pulse 1 with max volume + ignore-duty mode -> output = 15.
916        m.cpu_write(0x9000, 0x8F);
917        m.cpu_write(0x9001, 0x10);
918        m.cpu_write(0x9002, 0x81);
919        // Tick once so the oscillator advances past the timer == 0 reload.
920        m.clock_audio();
921        let s = m.mix_audio();
922        // Centering subtracts ~30 from a 0..=61 sum, scales by 650 (v2.2.7).
923        // With only p1 = 15 contributing, s = (15 - 30) * 650 = -9750.
924        assert!(s < 0, "mix_audio with only p1 must be below center");
925    }
926
927    #[cfg(feature = "mapper-audio")]
928    #[test]
929    fn vrc6_mix_audio_silent_when_disabled() {
930        let m = Vrc6::new(synth(8), synth_chr(8), 24, Mirroring::Vertical).unwrap();
931        // All channels disabled -> outputs 0 -> sum 0 -> mix = (0 - 30) * 650.
932        // Confirm we land at the documented "center - offset" position.
933        let mut m = m;
934        let s = m.mix_audio();
935        assert_eq!(s, -19500);
936    }
937
938    #[test]
939    fn vrc6_save_state_round_trips_audio() {
940        let mut m = Vrc6::new(synth(8), synth_chr(8), 24, Mirroring::Vertical).unwrap();
941        m.cpu_write(0x9000, 0x8F);
942        m.cpu_write(0x9001, 0x12);
943        m.cpu_write(0x9002, 0x83);
944        m.cpu_write(0xB000, 0x07);
945        let blob = m.save_state();
946        assert_eq!(
947            blob[0], VRC6_SECTION_VERSION,
948            "save_state writes the current version"
949        );
950
951        let mut m2 = Vrc6::new(synth(8), synth_chr(8), 24, Mirroring::Vertical).unwrap();
952        m2.load_state(&blob).expect("round-trip");
953        assert_eq!(m2.pulse1.ctrl, 0x8F);
954        assert_eq!(m2.pulse1.period, 0x312);
955        assert!(m2.pulse1.enabled);
956        assert_eq!(m2.saw.rate, 0x07);
957    }
958
959    /// v2.9.8 (ADR 0042): only the current (v3) layout loads. A v1 blob (the
960    /// core only) and a v2 blob (core + audio, written through v2.9.1) used
961    /// to load with the missing parts defaulted or left as they were.
962    #[test]
963    fn vrc6_pre_v3_blobs_are_refused() {
964        let m = Vrc6::new(synth(8), synth_chr(8), 24, Mirroring::Vertical).unwrap();
965        let core_len = 23 + m.vram.len();
966        let mut v1 = m.save_state()[..core_len].to_vec();
967        v1[0] = 1;
968        let mut v2 = m.save_state()[..core_len + VRC6_AUDIO_TAIL_LEN].to_vec();
969        v2[0] = 2;
970        let mut m2 = Vrc6::new(synth(8), synth_chr(8), 24, Mirroring::Vertical).unwrap();
971        for (v, old) in [(1u8, &v1), (2, &v2)] {
972            assert!(matches!(
973                m2.load_state(old),
974                Err(MapperError::UnsupportedVersion(got)) if got == v
975            ));
976        }
977    }
978
979    /// Core audit v2.9.2 AUD-02: the section carries the 8 KiB PRG-RAM and,
980    /// on a CHR-RAM board, the 8 KiB CHR-RAM. The core-level pin is
981    /// `rustynes_core::nes::tests::every_board_snapshot_carries_cartridge_ram`.
982    #[test]
983    fn vrc6_save_state_carries_prg_ram_and_chr_ram() {
984        let mut m = Vrc6::new(synth(8), Box::new([]), 24, Mirroring::Vertical).unwrap();
985        m.cpu_write(0x6000, 0x5A);
986        m.cpu_write(0x7FFF, 0xA5);
987        m.ppu_write(0x0000, 0x11);
988        // Inside 1 KiB slot 0 (bank 0 at power-on), so the read path's
989        // banking and the write path agree whatever the registers hold.
990        m.ppu_write(0x03FF, 0x22);
991        let blob = m.save_state();
992        let mut m2 = Vrc6::new(synth(8), Box::new([]), 24, Mirroring::Vertical).unwrap();
993        m2.load_state(&blob).expect("round-trip");
994        assert_eq!(m2.cpu_read(0x6000), 0x5A);
995        assert_eq!(m2.cpu_read(0x7FFF), 0xA5);
996        assert_eq!(m2.ppu_read(0x0000), 0x11);
997        assert_eq!(m2.ppu_read(0x03FF), 0x22);
998    }
999
1000    /// ASSUMPTION pin (v2.9.8, maintainer decision "Identity default"): the
1001    /// eight 1 KiB CHR bank registers power on holding 0-7, so slot `i`
1002    /// (`$0000 + i * $400`) reads CHR bank `i` before the program writes
1003    /// `$D000-$E003`. NESdev documents no VRC6 power-on state; the evidence
1004    /// is *Pulsewave Invite* (PD), which never writes those registers and
1005    /// draws its postcard only under this layout. Checked for both pin
1006    /// orders (mapper 24 = VRC6a, mapper 26 = VRC6b): the power-on value is
1007    /// a property of the register file, not of the address decode.
1008    #[test]
1009    fn vrc6_chr_registers_power_on_as_identity() {
1010        for mapper_id in [24, 26] {
1011            let mut m = Vrc6::new(synth(8), synth_chr(32), mapper_id, Mirroring::Vertical).unwrap();
1012            for slot in 0..8u16 {
1013                assert_eq!(
1014                    m.ppu_read(slot * 0x0400),
1015                    slot as u8,
1016                    "mapper {mapper_id}: CHR slot {slot} must read bank {slot} at power-on"
1017                );
1018            }
1019        }
1020    }
1021
1022    /// The console RESET button does not restore the power-on CHR layout:
1023    /// the VRC6 has no reset input documented on NESdev, and like almost
1024    /// every board it keeps its registers across a soft reset
1025    /// (`Mapper::reset` stays the default no-op). Only a power cycle, which
1026    /// rebuilds the mapper through `Vrc6::new`, yields the identity layout.
1027    #[test]
1028    fn vrc6_soft_reset_keeps_chr_registers() {
1029        let mut m = Vrc6::new(synth(8), synth_chr(32), 24, Mirroring::Vertical).unwrap();
1030        m.cpu_write(0xD000, 0x1F);
1031        m.reset();
1032        assert_eq!(m.ppu_read(0x0000), 0x1F, "reset must not reload slot 0");
1033        assert_eq!(
1034            m.ppu_read(0x0400),
1035            1,
1036            "untouched slot keeps its power-on bank"
1037        );
1038    }
1039
1040    /// A v3 blob one byte short (inside the RAM tail) is rejected.
1041    #[test]
1042    fn vrc6_truncated_ram_tail_is_rejected() {
1043        let m = Vrc6::new(synth(8), synth_chr(8), 24, Mirroring::Vertical).unwrap();
1044        let blob = m.save_state();
1045        let mut m2 = Vrc6::new(synth(8), synth_chr(8), 24, Mirroring::Vertical).unwrap();
1046        let err = m2
1047            .load_state(&blob[..blob.len() - 1])
1048            .expect_err("a truncated v3 blob must be rejected");
1049        assert!(matches!(err, MapperError::WrongLength { .. }), "{err:?}");
1050    }
1051}