Skip to main content

rustynes_mappers/
m005_mmc5.rs

1//! MMC5 (iNES mapper 5) — v0 implementation.
2//!
3//! See `docs/mappers.md` §MMC5 and the nesdev wiki page
4//! <https://www.nesdev.org/wiki/MMC5>.
5//!
6//! # Scope (v0 + v1)
7//!
8//! Implemented:
9//! - Register layout `$5000-$5FFF` (banking, mirroring, ExRAM, IRQ).
10//! - 4 PRG banking modes (`$5100`): 32K / 16K+16K / 16K+8K+8K / 4x8K.
11//! - 4 CHR banking modes (`$5101`): 8K / 4K+4K / 4x2K / 8x1K.
12//! - Per-1KiB nametable mirroring control via `$5105` (NT_A / NT_B / ExRAM /
13//!   fill).
14//! - 8 KiB PRG-RAM (single bank) at `$6000-$7FFF` with the PRG-RAM protect
15//!   pair `$5102` / `$5103` (two-write unlock).
16//! - ExRAM 1 KiB at `$5C00-$5FFF` with mode select via `$5104`:
17//!   - Mode 00: extra nametable. ExRAM serves the byte directly via
18//!     `Mapper::nametable_fetch`; the PPU bypasses CIRAM for those tables.
19//!   - Mode 01 (ExGrafix): per-tile attribute + per-tile CHR bank via
20//!     `Mapper::peek_ex_attribute`; the PPU overrides the AT-derived
21//!     palette and the BG pattern fetch routes through the latched 4 KiB
22//!     bank.
23//!   - Mode 10: general-purpose ExRAM. CPU read/write to `$5C00-$5FFF`.
24//!   - Mode 11: read-only ExRAM (CPU writes ignored).
25//! - 4-byte fill mode (`$5105` per-NT selector 0b11). Nametable byte reads
26//!   in those tables return `$5106` (fill tile); attribute byte reads
27//!   return `$5107` low 2 bits replicated 4 ways.
28//! - Two CHR bank sets, A (`$5120-$5127`) and B (`$5128-$512B`). Sprite
29//!   fetches (`Mapper::ppu_read_sprite`) always use A. In 8x8 mode the B set
30//!   is ignored and A serves background fetches and `$2007` too; in 8x16
31//!   mode background fetches use B while rendering and `$2007` uses the set
32//!   written last. The chip learns the sprite size and render enables by
33//!   decoding the PPU's `$2000` / `$2001` itself
34//!   (`Mapper::notify_ppu_register_write`). `$5101` selects which registers
35//!   drive which window, and a value indexes banks of the selected size.
36//! - Scanline IRQ at PPU cycle 4 of each visible scanline. The scanline
37//!   counter ticks via `Mapper::notify_scanline_start`; the in-frame flag
38//!   is cleared on vertical blank (`Mapper::notify_vblank`).
39//! - 8x8 multiply unit at `$5205` / `$5206`.
40//!
41//! Implemented (continued):
42//! - Vertical split-screen mode (`$5200`-`$5202`). The PPU queries
43//!   [`Mapper::bg_split_state`] at each BG fetch-group boundary; when the
44//!   current tile column falls inside the alt region (determined by `$5200`
45//!   bit 6 + bits 4-0), the mapper supplies a synthesized NT/AT address
46//!   anchored at ExRAM (rather than the loopy-v derivation) plus the
47//!   `$5202` 4 KiB CHR bank and an alt fine-Y from `$5201` + the current
48//!   scanline. Castlevania III (J) uses this for its independently-scrolled
49//!   status bar.
50//!
51//! Implemented (continued, audio):
52//! - MMC5 audio extension at `$5000-$5015` (Track C2). Two pulse-wave
53//!   channels (`$5000-$5007`) modelled on the 2A03 pulse but with no
54//!   sweep unit; one raw 7-bit PCM channel via `$5010` (mode bit) +
55//!   `$5011` (sample). Used by Castlevania III: Dracula's Curse
56//!   (Japan / PAL) and Just Breed. Envelope and length-counter
57//!   sub-units share the 2A03 frame-counter cadence — the bus fans
58//!   the APU frame events out via [`crate::mapper::Mapper::notify_frame_event`].
59//!   Behind the `mapper-audio` Cargo feature (default ON); when off,
60//!   register decoders still latch state (save-state round-trip
61//!   preserved) but oscillators do not advance and `mix_audio`
62//!   returns silence.
63//!
64//! # Register layout (v0 cheat sheet)
65//!
66//! | Range            | Purpose                                                |
67//! |------------------|--------------------------------------------------------|
68//! | `$5000-$5015`    | Audio: 2 pulse + raw PCM (Track C2)                    |
69//! | `$5100`          | PRG mode (low 2 bits)                                  |
70//! | `$5101`          | CHR mode (low 2 bits)                                  |
71//! | `$5102`          | PRG-RAM protect 1: must equal `0b01`                   |
72//! | `$5103`          | PRG-RAM protect 2: must equal `0b10`                   |
73//! | `$5104`          | ExRAM mode (low 2 bits)                                |
74//! | `$5105`          | Nametable mapping (4 x 2 bits)                         |
75//! | `$5106`          | Fill-mode tile (active)                                |
76//! | `$5107`          | Fill-mode attribute (active)                           |
77//! | `$5113`          | PRG-RAM bank select @ `$6000-$7FFF`                    |
78//! | `$5114-$5117`    | PRG bank select 0..3 @ `$8000-$FFFF`                   |
79//! | `$5120-$5127`    | Sprite CHR banks (active — used for 8x16 sprite fetch)  |
80//! | `$5128-$512B`    | BG CHR banks (active)                                  |
81//! | `$5130`          | CHR-bank upper bits (active)                            |
82//! | `$5200-$5202`    | Vertical split-screen mode / scroll / CHR bank         |
83//! | `$5203`          | Scanline IRQ compare value                             |
84//! | `$5204`          | Scanline IRQ status (read) / enable (write bit 7)      |
85//! | `$5205-$5206`    | 8x8 multiplier (factor1 / factor2)                     |
86//! | `$5C00-$5FFF`    | ExRAM (1 KiB)                                          |
87//! | `$6000-$7FFF`    | PRG-RAM (banked via `$5113`)                           |
88//! | `$8000-$FFFF`    | PRG-ROM (banked per `$5100` mode)                      |
89
90#![allow(
91    clippy::cast_possible_truncation,
92    clippy::cast_lossless,
93    clippy::missing_const_for_fn,
94    clippy::struct_excessive_bools,
95    clippy::match_same_arms,
96    clippy::manual_range_patterns,
97    clippy::too_many_arguments,
98    clippy::useless_let_if_seq,
99    clippy::doc_markdown,
100    clippy::if_not_else,
101    clippy::nonminimal_bool,
102    clippy::cognitive_complexity
103)]
104
105use crate::cartridge::Mirroring;
106use crate::mapper::{
107    BgSplitState, ExAttribute, Mapper, MapperCaps, MapperError, MapperFrameEvents,
108};
109use alloc::{boxed::Box, vec::Vec};
110use alloc::{format, vec};
111
112const PRG_BANK_8K: usize = 0x2000;
113const PRG_BANK_16K: usize = 0x4000;
114const PRG_BANK_32K: usize = 0x8000;
115const CHR_BANK_1K: usize = 0x0400;
116const PRG_RAM_BANK: usize = 0x2000;
117const EXRAM_SIZE: usize = 0x0400;
118const NAMETABLE_SIZE: usize = 0x0400;
119
120/// v6 (v2.9.9): the `$2000` / `$2001` snoop (`ppu_sprites_8x16`,
121/// `ppu_rendering`) follows `last_chr_write_was_sprite`.
122const SAVE_STATE_VERSION: u8 = 6;
123
124/// The MMC5's two CHR bank-register sets.
125#[derive(Clone, Copy, Debug, PartialEq, Eq)]
126enum ChrSet {
127    /// `$5120-$5127`: sprites always; everything in 8x8 mode.
128    A,
129    /// `$5128-$512B`: background tiles in 8x16 mode.
130    B,
131}
132/// The PRG-RAM address space the MMC5 always presents: 64 KiB, the wiki's
133/// "compatible superset for all games" (see [`Mmc5::prg_ram_offset`]).
134const PRG_RAM_SUPERSET: usize = 0x1_0000;
135
136// ---------------------------------------------------------------------------
137// MMC5 audio mixer level (v2.1.6 "Expansion Audio")
138//
139// The single source of truth for the MMC5 expansion-audio DAC scale. Both the
140// cartridge path ([`Mmc5::mix_audio`]) and the NSF-playback path
141// (`crate::nsf_expansion::Mmc5Exp::mix`) reference these so the two can never
142// drift apart — an NSF MMC5 tune is guaranteed level-matched to an MMC5
143// cartridge. Per the nesdev wiki §"MMC5 audio" and Mesen2, the MMC5 pulses use
144// the SAME DAC/gain as the 2A03 pulses, so a full-volume MMC5 square is
145// ~equal in loudness to a full-volume 2A03 square (`db_mmc5` ≈ 1.0×; the
146// per-pulse scale ≈ `pulse_table[15] * 65536 / 15 ≈ 650`). The 7-bit raw PCM
147// keeps its documented ~half-gain relative to the pulses (`40 ≈ 650 / 16`).
148// See `docs/apu-2a03.md` §Expansion-audio levels.
149// ---------------------------------------------------------------------------
150
151/// Per-pulse linear scale for the two MMC5 square channels (0..=15 each).
152pub(crate) const MMC5_PULSE_SCALE: i16 = 650;
153/// Linear scale for the 7-bit raw PCM channel (0..=127) — ~half the pulse gain.
154pub(crate) const MMC5_PCM_SCALE: i16 = 40;
155/// DC bias subtracted to centre the AC signal on zero (half the maximum linear
156/// sum `(15+15) * PULSE + 127 * PCM`). Derived from the scales via
157/// `i16::midpoint` so it can never drift out of sync (`midpoint(30*650,
158/// 127*40) = 12290`). The APU mixer's downstream high-pass filters remove any
159/// residual DC regardless.
160pub(crate) const MMC5_MIX_BIAS: i16 = i16::midpoint(30 * MMC5_PULSE_SCALE, 127 * MMC5_PCM_SCALE);
161
162/// 32-entry length-counter lookup table (same as the 2A03 APU).
163/// Indexed by the top 5 bits of `$5003` / `$5007` writes.
164const LENGTH_TABLE: [u8; 32] = [
165    10, 254, 20, 2, 40, 4, 80, 6, 160, 8, 60, 10, 14, 12, 26, 14, 12, 16, 24, 18, 48, 20, 96, 22,
166    192, 24, 72, 26, 16, 28, 32, 30,
167];
168
169/// 4-duty x 8-step duty waveform table, identical to the 2A03 pulse channel.
170const DUTY_TABLE: [[u8; 8]; 4] = [
171    [0, 1, 0, 0, 0, 0, 0, 0], // 12.5%
172    [0, 1, 1, 0, 0, 0, 0, 0], // 25.0%
173    [0, 1, 1, 1, 1, 0, 0, 0], // 50.0%
174    [1, 0, 0, 1, 1, 1, 1, 1], // 25.0% negated
175];
176
177/// MMC5 audio extension state. See top-level module docs §"Implemented
178/// (continued, audio)" for protocol details.
179///
180/// Layout & semantics follow nesdev wiki "MMC5 audio":
181/// - `$5000-$5003`: Pulse 1 (control, unused, timer-lo, length+timer-hi).
182///   Identical to APU `$4000-$4003` but with NO sweep unit at `$5001`.
183/// - `$5004-$5007`: Pulse 2 (same shape).
184/// - `$5010`: PCM control (bit 0: 0 = write-mode / output PCM, 1 = read-mode
185///   / silenced; bit 7: IRQ enable — not modelled in v0, no IRQ source on
186///   our side).
187/// - `$5011`: PCM 8-bit raw sample (only the low 7 bits contribute to output
188///   per Mesen2 / nesdev: writing `$00` mutes the channel, which programs
189///   use as the canonical silence value).
190/// - `$5015`: Status. Bit 0 = pulse-1 length > 0; bit 1 = pulse-2 length > 0.
191///   On write, bits 0/1 enable/disable each pulse length counter (same
192///   contract as `$4015` for the 2A03 pulses; no DMC bit since there's no
193///   DMC).
194#[derive(Debug, Clone, Default)]
195pub(crate) struct Mmc5Audio {
196    pub(crate) pulse1: Mmc5Pulse,
197    pub(crate) pulse2: Mmc5Pulse,
198    /// `$5010` raw byte. Bit 0 = read-mode (silences PCM); bit 7 = PCM IRQ
199    /// enable (not modelled).
200    pub(crate) pcm_ctrl: u8,
201    /// `$5011` last write — 7-bit linear PCM level (low 7 bits used).
202    pub(crate) pcm_sample: u8,
203}
204
205/// MMC5 audio pulse channel. Same architecture as the 2A03 pulse (duty
206/// sequencer + 11-bit timer + envelope + length counter), but with NO
207/// sweep unit. Length and envelope tick on the APU frame-counter events
208/// fanned out via [`crate::mapper::Mapper::notify_frame_event`].
209#[derive(Debug, Clone, Default)]
210pub(crate) struct Mmc5Pulse {
211    /// Duty selection (bits 6-7 of `$5000` / `$5004`).
212    duty: u8,
213    /// Length-halt + envelope-loop bit (bit 5 of `$5000` / `$5004`).
214    halt: bool,
215    /// Envelope constant-volume flag (bit 4 of `$5000` / `$5004`).
216    envelope_constant: bool,
217    /// Envelope volume (constant) or decay period (decay rate) — bits 0-3
218    /// of `$5000` / `$5004`.
219    envelope_volume_or_period: u8,
220    /// 11-bit timer reload period (from `$5002`/`$5003` low+high writes).
221    timer_period: u16,
222    /// Internal countdown timer.
223    timer: u16,
224    /// 3-bit step into `DUTY_TABLE`.
225    step: u8,
226    /// Length counter (5-bit lookup -> 0..=254). 0 mutes the channel.
227    pub(crate) length: u8,
228    /// Length-counter channel-enable from `$5015` writes.
229    length_enabled: bool,
230    /// Envelope start flag (set on `$5003`/`$5007` write).
231    envelope_start: bool,
232    /// Envelope divider countdown.
233    envelope_divider: u8,
234    /// Envelope decay level (0..=15).
235    envelope_decay: u8,
236}
237
238impl Mmc5Pulse {
239    /// `$5000` / `$5004` write: duty + length-halt + envelope.
240    pub(crate) fn write_ctrl(&mut self, value: u8) {
241        self.duty = (value >> 6) & 0x03;
242        self.halt = (value & 0x20) != 0;
243        self.envelope_constant = (value & 0x10) != 0;
244        self.envelope_volume_or_period = value & 0x0F;
245    }
246
247    /// `$5002` / `$5006` write: timer-period low byte.
248    pub(crate) fn write_timer_lo(&mut self, value: u8) {
249        self.timer_period = (self.timer_period & 0xFF00) | u16::from(value);
250    }
251
252    /// `$5003` / `$5007` write: length-load + timer-period high 3 bits.
253    /// Also resets the duty step and primes the envelope.
254    pub(crate) fn write_timer_hi(&mut self, value: u8) {
255        self.timer_period = (self.timer_period & 0x00FF) | (u16::from(value & 0x07) << 8);
256        if self.length_enabled {
257            self.length = LENGTH_TABLE[(value >> 3) as usize];
258        }
259        self.step = 0;
260        self.envelope_start = true;
261    }
262
263    /// `$5015` write: per-channel length-enable. Clearing the bit forces
264    /// the length count to 0 (same contract as `$4015` for the 2A03).
265    pub(crate) fn set_length_enabled(&mut self, enabled: bool) {
266        self.length_enabled = enabled;
267        if !enabled {
268            self.length = 0;
269        }
270    }
271
272    /// One APU clock (every other CPU cycle) — advance the timer / duty
273    /// sequencer. Caller is responsible for the every-other-cycle gating.
274    pub(crate) fn clock_timer(&mut self) {
275        if self.timer == 0 {
276            self.timer = self.timer_period;
277            self.step = (self.step + 1) & 0x07;
278        } else {
279            self.timer -= 1;
280        }
281    }
282
283    /// Quarter-frame: clock envelope.
284    pub(crate) fn clock_envelope(&mut self) {
285        if self.envelope_start {
286            self.envelope_start = false;
287            self.envelope_decay = 15;
288            self.envelope_divider = self.envelope_volume_or_period;
289        } else if self.envelope_divider == 0 {
290            self.envelope_divider = self.envelope_volume_or_period;
291            if self.envelope_decay > 0 {
292                self.envelope_decay -= 1;
293            } else if self.halt {
294                self.envelope_decay = 15;
295            }
296        } else {
297            self.envelope_divider -= 1;
298        }
299    }
300
301    /// Half-frame: clock length counter (no sweep — MMC5 pulses have no
302    /// sweep unit).
303    pub(crate) fn clock_length(&mut self) {
304        if !self.halt && self.length > 0 {
305            self.length -= 1;
306        }
307    }
308
309    /// Effective envelope output volume (0..=15) — constant or decay.
310    fn envelope_output(&self) -> u8 {
311        if self.envelope_constant {
312            self.envelope_volume_or_period
313        } else {
314            self.envelope_decay
315        }
316    }
317
318    /// True iff the channel is currently muted: length 0, timer period < 8,
319    /// or duty waveform low. Matches the 2A03 pulse gating except for the
320    /// absent sweep mute.
321    fn muted(&self) -> bool {
322        self.length == 0 || self.timer_period < 8
323    }
324
325    /// Per-cycle 4-bit output (0..=15). 0 when muted.
326    pub(crate) fn output(&self) -> u8 {
327        if self.muted() || DUTY_TABLE[self.duty as usize][self.step as usize] == 0 {
328            0
329        } else {
330            self.envelope_output()
331        }
332    }
333}
334
335impl Mmc5Audio {
336    /// Encode the audio state to a save-state tail. Versioned by the
337    /// surrounding MMC5 save-state (see `SAVE_STATE_VERSION`). Layout per
338    /// pulse: ctrl-byte(1) + halt(1) + envelope-constant(1) +
339    /// envelope-volume(1) + timer_period(2) + timer(2) + step(1) +
340    /// length(1) + length_enabled(1) + envelope_start(1) +
341    /// envelope_divider(1) + envelope_decay(1) = 14 bytes.
342    /// Plus pcm_ctrl(1) + pcm_sample(1) = 2 bytes.
343    /// Total audio tail = 2 * 14 + 2 = 30 bytes.
344    const TAIL_LEN: usize = 30;
345
346    fn write_tail(&self, out: &mut Vec<u8>) {
347        Self::write_pulse(out, &self.pulse1);
348        Self::write_pulse(out, &self.pulse2);
349        out.push(self.pcm_ctrl);
350        out.push(self.pcm_sample);
351    }
352
353    fn write_pulse(out: &mut Vec<u8>, p: &Mmc5Pulse) {
354        // Re-emit the source register bytes for ctrl so a round-trip stays
355        // self-describing. We don't store the literal $5000 byte; instead
356        // we serialize the decoded fields directly (same shape as VRC6).
357        let ctrl = (p.duty << 6)
358            | (u8::from(p.halt) << 5)
359            | (u8::from(p.envelope_constant) << 4)
360            | (p.envelope_volume_or_period & 0x0F);
361        out.push(ctrl);
362        out.push(u8::from(p.halt));
363        out.push(u8::from(p.envelope_constant));
364        out.push(p.envelope_volume_or_period);
365        out.extend_from_slice(&p.timer_period.to_le_bytes());
366        out.extend_from_slice(&p.timer.to_le_bytes());
367        out.push(p.step);
368        out.push(p.length);
369        out.push(u8::from(p.length_enabled));
370        out.push(u8::from(p.envelope_start));
371        out.push(p.envelope_divider);
372        out.push(p.envelope_decay);
373    }
374
375    fn read_tail(&mut self, data: &[u8]) -> Result<(), MapperError> {
376        if data.len() != Self::TAIL_LEN {
377            return Err(MapperError::Invalid(format!(
378                "MMC5 audio tail expected {} bytes, got {}",
379                Self::TAIL_LEN,
380                data.len()
381            )));
382        }
383        Self::read_pulse(&data[0..14], &mut self.pulse1);
384        Self::read_pulse(&data[14..28], &mut self.pulse2);
385        self.pcm_ctrl = data[28];
386        self.pcm_sample = data[29];
387        Ok(())
388    }
389
390    fn read_pulse(data: &[u8], p: &mut Mmc5Pulse) {
391        // We re-derive the decoded fields from the ctrl byte to match the
392        // write side; the ctrl byte itself is informational. The next
393        // bytes carry the authoritative state.
394        let ctrl = data[0];
395        p.duty = (ctrl >> 6) & 0x03;
396        p.halt = data[1] != 0;
397        p.envelope_constant = data[2] != 0;
398        p.envelope_volume_or_period = data[3] & 0x0F;
399        p.timer_period = u16::from_le_bytes([data[4], data[5]]);
400        p.timer = u16::from_le_bytes([data[6], data[7]]);
401        p.step = data[8] & 0x07;
402        p.length = data[9];
403        p.length_enabled = data[10] != 0;
404        p.envelope_start = data[11] != 0;
405        p.envelope_divider = data[12];
406        p.envelope_decay = data[13] & 0x0F;
407    }
408}
409
410/// Per-1 KiB nametable source as decoded from `$5105`.
411#[derive(Debug, Clone, Copy, PartialEq, Eq)]
412enum NtSource {
413    /// CIRAM bank 0 (logical NT_A).
414    CiramA,
415    /// CIRAM bank 1 (logical NT_B).
416    CiramB,
417    /// On-cart ExRAM (only meaningful when `$5104` is mode 0 or 1).
418    ExRam,
419    /// Fill-mode: returns the `$5106` fill tile with the `$5107` attribute.
420    Fill,
421}
422
423impl NtSource {
424    const fn from_bits(b: u8) -> Self {
425        match b & 0x03 {
426            0 => Self::CiramA,
427            1 => Self::CiramB,
428            2 => Self::ExRam,
429            _ => Self::Fill,
430        }
431    }
432}
433
434/// `$5114-$5117` PRG bank slot. Bit 7 selects ROM (1) vs. RAM (0); the low
435/// 7 bits index 8 KiB pages within the selected medium.
436#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
437struct PrgSlot {
438    /// Raw register value (we keep this for save-state symmetry).
439    raw: u8,
440}
441
442impl PrgSlot {
443    const fn is_rom(self) -> bool {
444        // For `$5117` (the fixed-last slot) MMC5 forces ROM regardless of
445        // bit 7; the caller handles that. For the other slots the bit
446        // selects ROM (1) vs PRG-RAM (0).
447        (self.raw & 0x80) != 0
448    }
449    const fn page(self) -> usize {
450        (self.raw & 0x7F) as usize
451    }
452}
453
454/// MMC5 mapper (iNES mapper 5).
455pub struct Mmc5 {
456    // === ROM / RAM storage ===
457    prg_rom: Box<[u8]>,
458    chr: Box<[u8]>,
459    chr_is_ram: bool,
460    prg_ram: Box<[u8]>,
461    /// The rest of the 64 KiB superset beyond the header's declared
462    /// `prg_ram` (v2.7.2). Declared RAM stays in `prg_ram` -- that is where
463    /// the battery save lives and where older save states put it -- and this
464    /// holds the pages a header under-declared. Saved as the v5 tail.
465    prg_ram_extra: Box<[u8]>,
466    /// 2 KiB on-cart CIRAM (indexed via `nametable_address`).
467    /// Sized to the maximum the bus exposes (2 KiB) — extra nametables go
468    /// through ExRAM, not VRAM.
469    vram: Box<[u8]>,
470    /// 1 KiB ExRAM. Always allocated; access pattern depends on `$5104`.
471    exram: [u8; EXRAM_SIZE],
472
473    // === CPU bus state ===
474    /// `$5100` low 2 bits.
475    prg_mode: u8,
476    /// `$5101` low 2 bits.
477    chr_mode: u8,
478    /// `$5102` last write (low 2 bits), and `$5103` last write — both must
479    /// match the magic pair to unlock PRG-RAM writes.
480    prg_ram_protect_1: u8,
481    prg_ram_protect_2: u8,
482    /// `$5104` low 2 bits.
483    exram_mode: u8,
484    /// `$5105` raw byte (4 fields of 2 bits, low->high = NT0..NT3).
485    nametable_map: u8,
486    /// `$5106` fill-mode tile.
487    fill_tile: u8,
488    /// `$5107` fill-mode attribute (bottom 2 bits replicated).
489    fill_attr: u8,
490    /// `$5113` PRG-RAM bank (low 7 bits used; only one 8 KiB PRG-RAM bank
491    /// supported in v0 — high bits ignored).
492    prg_ram_bank: u8,
493    /// `$5114-$5117` PRG bank registers.
494    prg_banks: [PrgSlot; 4],
495    /// `$5128-$512B`, the "B" CHR bank set. 10-bit values: low 8 bits from
496    /// the register, high 2 bits from `$5130`. Used only while 8x16 sprites
497    /// are selected: for background fetches while rendering, and for `$2007`
498    /// when this set was written last. In 8x8 mode the MMC5 ignores it.
499    bg_chr_banks: [u16; 4],
500    /// `$5120-$5127`, the "A" CHR bank set. Sprite fetches always use it; in
501    /// 8x8 mode background fetches and `$2007` use it too. Which registers
502    /// drive which window depends on `$5101` (see [`Mmc5::chr_set_offset`]).
503    sprite_chr_banks: [u16; 8],
504    /// `$5130` upper 2 bits applied to the CHR bank registers (high bits of the
505    /// bank index).
506    chr_upper: u8,
507    /// Whether the A set (`$5120-$5127`) was written after the B set. In 8x16
508    /// mode `$2007` uses the set written last; selecting 8x8 sprites resets
509    /// it to the A set (loopy's hardware tests on the NESdev thread the MMC5
510    /// page cites: "Switching back to 8x16, it still uses $5120-27 (until
511    /// 5128-2B is written again)").
512    last_chr_write_was_sprite: bool,
513    /// `$2000` bit 5 as the MMC5 last saw it written. The chip decodes the
514    /// PPU's `$2000` itself (exactly `$2000`, not a mirror), which is how it
515    /// knows 8x16 sprites are selected (MMC5 page, "8x16 mode enable").
516    ppu_sprites_8x16: bool,
517    /// `$2001` bits 3-4 (show background / show sprites) as the MMC5 last saw
518    /// them written, at exactly `$2001`. "Only when Z is set and at least one
519    /// E bit is set does the MMC5 draw 8x16 sprites from eight independent
520    /// banks."
521    ppu_rendering: bool,
522    /// MMC5 ExGrafix per-tile CHR bank latch. Set by `peek_ex_attribute`
523    /// at NT-fetch time, consumed by the next BG `ppu_read` call(s).
524    /// 4 KiB bank index (combined with the in-tile 12-bit offset).
525    /// `None` outside ExGrafix mode 01 or before the first tile latch.
526    ex_chr_bank_latch: Option<u16>,
527
528    // === Vertical split-screen ($5200-$5202) ===
529    /// `$5200` bit 7 — split enable.
530    split_enable: bool,
531    /// `$5200` bit 6 — split side: `false` = alt region occupies tile
532    /// columns `< split_tile` (left); `true` = alt region occupies tile
533    /// columns `>= split_tile` (right).
534    split_side_right: bool,
535    /// `$5200` bits 4-0 — split tile column (0..=31).
536    split_tile: u8,
537    /// `$5201` — vertical scroll within the alt region (0..=239 useful).
538    split_v_scroll: u8,
539    /// `$5202` — 4 KiB CHR bank for the alt region's BG pattern fetches.
540    split_chr_bank: u8,
541    /// 4 KiB CHR bank latch for the current BG fetch group when split is
542    /// active. Set by `bg_split_state` at the NT-byte boundary; cleared on
543    /// any subsequent non-split fetch. Distinct from `ex_chr_bank_latch`
544    /// (split takes precedence when both would apply).
545    split_chr_bank_latch: Option<u8>,
546
547    // === Scanline IRQ state ===
548    /// `$5203` compare value.
549    irq_compare: u8,
550    /// True when `$5204` bit 7 is set.
551    irq_enabled: bool,
552    /// True when the IRQ "pending" latch is set (read at `$5204` bit 7,
553    /// cleared by reading `$5204`).
554    irq_pending: bool,
555    /// "In-frame" flag — set on the first rendered scanline after VBL,
556    /// cleared on VBL. Read at `$5204` bit 6.
557    in_frame: bool,
558    /// Internal scanline counter; ticks each rendered scanline.
559    scanline_counter: u8,
560
561    // === Multiplier ===
562    /// `$5205` factor 1.
563    mul_a: u8,
564    /// `$5206` factor 2.
565    mul_b: u8,
566
567    // === Mirroring (last-resort fallback for headers / save state) ===
568    /// MMC5 has fully runtime-controlled mirroring; this field is a derived
569    /// summary for `current_mirroring` (callers like the save-state loader
570    /// or debuggers).
571    current_mirroring_summary: Mirroring,
572
573    // === Audio extension ($5000-$5015) ===
574    /// 2 pulse channels + raw PCM. See [`Mmc5Audio`] docs.
575    audio: Mmc5Audio,
576    /// APU-phase toggle. MMC5 pulses (like the 2A03 pulses) clock their
577    /// timer / duty sequencer every other CPU cycle. We toggle this on
578    /// each `notify_cpu_cycle`.
579    audio_apu_phase: bool,
580}
581
582/// Where a CPU access lands in PRG-RAM terms (v2.7.2).
583#[derive(Debug, Clone, Copy, PartialEq, Eq)]
584enum RamTarget {
585    /// The address is not a PRG-RAM access in the current mapping.
586    NotRam,
587    /// A PRG-RAM access landing at this offset of the 64 KiB space.
588    At(usize),
589}
590
591impl Mmc5 {
592    /// Construct a new MMC5 mapper.
593    ///
594    /// `prg_rom` must be a non-zero multiple of 8 KiB. CHR-RAM is selected
595    /// when `chr_rom` is empty; otherwise CHR-ROM length must be a multiple
596    /// of 1 KiB. `prg_ram_bytes == 0` selects the default 8 KiB.
597    ///
598    /// # Errors
599    ///
600    /// Returns [`MapperError::Invalid`] on size mismatch.
601    pub fn new(
602        prg_rom: Box<[u8]>,
603        chr_rom: Box<[u8]>,
604        initial_mirroring: Mirroring,
605        prg_ram_bytes: usize,
606    ) -> Result<Self, MapperError> {
607        if prg_rom.is_empty() || !prg_rom.len().is_multiple_of(PRG_BANK_8K) {
608            return Err(MapperError::Invalid(format!(
609                "MMC5 PRG-ROM size {} is not a non-zero multiple of 8 KiB",
610                prg_rom.len()
611            )));
612        }
613        let chr_is_ram = chr_rom.is_empty();
614        let chr: Box<[u8]> = if chr_is_ram {
615            // Some carts use 8 KiB CHR-RAM with MMC5; allocate a default.
616            vec![0u8; 8 * CHR_BANK_1K].into_boxed_slice()
617        } else if chr_rom.len().is_multiple_of(CHR_BANK_1K) {
618            chr_rom
619        } else {
620            return Err(MapperError::Invalid(format!(
621                "MMC5 CHR-ROM size {} is not a multiple of 1 KiB",
622                chr_rom.len()
623            )));
624        };
625        let prg_ram_size = if prg_ram_bytes == 0 {
626            PRG_RAM_BANK
627        } else {
628            prg_ram_bytes
629        };
630        let total_prg_pages = prg_rom.len() / PRG_BANK_8K;
631        let last_page = (total_prg_pages.saturating_sub(1)) as u8;
632
633        // Power-on defaults: PRG mode 3 (4x8K), `$5117` -> last bank ROM,
634        // other slots zero. CHR mode 0 (single 8K bank).
635        let mut prg_banks = [PrgSlot::default(); 4];
636        prg_banks[3] = PrgSlot {
637            raw: 0x80 | (last_page & 0x7F),
638        };
639
640        Ok(Self {
641            prg_rom,
642            chr,
643            chr_is_ram,
644            prg_ram: vec![0u8; prg_ram_size].into_boxed_slice(),
645            prg_ram_extra: vec![0u8; PRG_RAM_SUPERSET.saturating_sub(prg_ram_size)]
646                .into_boxed_slice(),
647            vram: vec![0u8; 2 * NAMETABLE_SIZE].into_boxed_slice(),
648            exram: [0u8; EXRAM_SIZE],
649            prg_mode: 3,
650            chr_mode: 0,
651            prg_ram_protect_1: 0,
652            prg_ram_protect_2: 0,
653            exram_mode: 0,
654            // Default mirroring (vertical) is `0b01_00_01_00` = NT_B, NT_A,
655            // NT_B, NT_A — but real MMC5 power-on is undefined. Use a
656            // mirroring of "all NT_A" which is the most defensive default.
657            nametable_map: 0,
658            fill_tile: 0,
659            fill_attr: 0,
660            prg_ram_bank: 0,
661            prg_banks,
662            bg_chr_banks: [0; 4],
663            sprite_chr_banks: [0; 8],
664            chr_upper: 0,
665            // The PPU powers on with 8x8 sprites, which selects the A set.
666            last_chr_write_was_sprite: true,
667            ppu_sprites_8x16: false,
668            ppu_rendering: false,
669            ex_chr_bank_latch: None,
670            split_enable: false,
671            split_side_right: false,
672            split_tile: 0,
673            split_v_scroll: 0,
674            split_chr_bank: 0,
675            split_chr_bank_latch: None,
676            irq_compare: 0,
677            irq_enabled: false,
678            irq_pending: false,
679            in_frame: false,
680            scanline_counter: 0,
681            mul_a: 0,
682            mul_b: 0,
683            current_mirroring_summary: initial_mirroring,
684            audio: Mmc5Audio::default(),
685            audio_apu_phase: false,
686        })
687    }
688
689    /// True iff the PRG-RAM protect "magic pair" is currently unlocked.
690    /// Hardware: `$5102` low 2 bits == 0b10 AND `$5103` low 2 bits == 0b01.
691    /// Any other value locks the RAM; the typical unlock sequence is two
692    /// adjacent writes of the right values.
693    fn prg_ram_writable(&self) -> bool {
694        (self.prg_ram_protect_1 & 0x03) == 0b10 && (self.prg_ram_protect_2 & 0x03) == 0b01
695    }
696
697    /// Offset into the PRG-RAM space for an 8 KiB bank value: bank x 8 KiB,
698    /// over a 64 KiB space.
699    ///
700    /// The wiki (`nesdev_wiki/MMC5.xhtml` §"PRG-RAM configurations") gives
701    /// four commercial layouts -- 8 KiB (EKROM), 2 x 8 KiB (ETROM), 32 KiB
702    /// (EWROM), 2 x 32 KiB -- with bit 2 as a chip select and open bus where
703    /// no chip answers. It also says iNES headers are unreliable here and that
704    /// "no ExROM game is known to write PRG-RAM with one bank value and then
705    /// attempt to read back the same data with a different bank value, [so]
706    /// emulating the PRG-RAM as 64K at all times can be used as a compatible
707    /// superset for all games". v2.7.2 first implemented the exact table and
708    /// the commercial oracle caught the cost: *L'Empereur* is ETROM (the wiki's
709    /// board table) but its NES 2.0 dump declares only the 8 KiB battery chip,
710    /// so its work-RAM chip floated and the game stopped at its logo. The
711    /// superset gives every game the RAM it expects whatever its header says.
712    /// The battery save is still the header's declared part (see `sram`).
713    fn prg_ram_offset(&self, bank: u8, off_8k: usize) -> usize {
714        let total = self.prg_ram.len() + self.prg_ram_extra.len();
715        (usize::from(bank & 0x07) * PRG_RAM_BANK + (off_8k & (PRG_RAM_BANK - 1))) % total.max(1)
716    }
717
718    /// Read a byte of the 64 KiB PRG-RAM space.
719    fn ram_get(&self, idx: usize) -> u8 {
720        if idx < self.prg_ram.len() {
721            self.prg_ram[idx]
722        } else {
723            self.prg_ram_extra[idx - self.prg_ram.len()]
724        }
725    }
726
727    /// Write a byte of the 64 KiB PRG-RAM space.
728    fn ram_set(&mut self, idx: usize, value: u8) {
729        if idx < self.prg_ram.len() {
730            self.prg_ram[idx] = value;
731        } else {
732            let base = self.prg_ram.len();
733            self.prg_ram_extra[idx - base] = value;
734        }
735    }
736
737    /// The byte a PRG-RAM access reads (0 for a non-RAM target, which the
738    /// caller never reaches for RAM).
739    fn ram_read(&self, t: RamTarget) -> u8 {
740        match t {
741            RamTarget::At(idx) => self.ram_get(idx),
742            RamTarget::NotRam => 0,
743        }
744    }
745
746    /// What a CPU access at `addr` reaches in PRG-RAM terms.
747    fn prg_ram_target(&self, addr: u16) -> RamTarget {
748        match addr {
749            // `$5113` always maps RAM, 8 KiB, at `$6000-$7FFF`.
750            0x6000..=0x7FFF => {
751                RamTarget::At(self.prg_ram_offset(self.prg_ram_bank, usize::from(addr - 0x6000)))
752            }
753            0x8000..=0xFFFF => {
754                let (slot, slot_size, region_off) = self.prg_window_lookup(addr);
755                let raw = self.prg_banks[slot];
756                if slot == 3 || raw.is_rom() {
757                    return RamTarget::NotRam;
758                }
759                // In a 16 KiB window the register's bit 0 is ignored and CPU
760                // A13 drives PRG A13 (wiki §"PRG Bankswitching").
761                #[allow(clippy::cast_possible_truncation)] // page is 7 bits
762                let page = raw.page() as u8;
763                let bank = if slot_size == PRG_BANK_16K {
764                    (page & !1) | u8::from(region_off >= PRG_BANK_8K)
765                } else {
766                    page
767                };
768                RamTarget::At(self.prg_ram_offset(bank, region_off))
769            }
770            _ => RamTarget::NotRam,
771        }
772    }
773
774    /// Resolve a CPU PRG address (`$8000-$FFFF`) to either a ROM byte offset
775    /// or, for PRG-RAM mapped into the window (PRG modes that allow it),
776    /// a `(slot, offset)` indication. Returns `Some(byte)` if PRG-RAM was
777    /// hit; `None` if the caller should fall through to ROM.
778    fn read_prg_window(&self, addr: u16) -> u8 {
779        let (slot, slot_size, region_off) = self.prg_window_lookup(addr);
780        let raw = self.prg_banks[slot];
781        // `$5117` is forced ROM regardless of bit 7.
782        let force_rom = slot == 3;
783        if force_rom || raw.is_rom() {
784            // ROM path. The slot indexes 8 KiB pages, but in larger windows
785            // (16 K / 32 K) the low bits of `page` are masked to align.
786            let page = raw.page();
787            let (page, mask) = match slot_size {
788                PRG_BANK_8K => (page, !0usize),
789                PRG_BANK_16K => (page & !1, !0usize),
790                PRG_BANK_32K => (page & !3, !0usize),
791                _ => (page, !0usize),
792            };
793            let base = (page * PRG_BANK_8K) & mask;
794            let off = (base + region_off) % self.prg_rom.len();
795            self.prg_rom[off]
796        } else {
797            // PRG-RAM at this slot, paged over the 64 KiB superset, where
798            // every page answers. (The exact per-board table, whose chip-less
799            // cells were open bus, was replaced during v2.7.2.)
800            self.ram_read(self.prg_ram_target(addr))
801        }
802    }
803
804    /// Decode a CPU PRG address into `(slot_index, slot_size, offset_within_region)`
805    /// where `slot_index` is the index into `self.prg_banks` and
806    /// `slot_size` is the size of the *physical* bank window (8/16/32 K).
807    fn prg_window_lookup(&self, addr: u16) -> (usize, usize, usize) {
808        let off16 = (addr - 0x8000) as usize; // 0..0x8000
809        match self.prg_mode & 0x03 {
810            0 => {
811                // 32 K window driven by `$5117` (slot 3, but page bits & ~3).
812                (3, PRG_BANK_32K, off16)
813            }
814            1 => {
815                // 16 K + 16 K. `$5115` (slot 1) -> $8000-$BFFF (page & ~1);
816                // `$5117` (slot 3) -> $C000-$FFFF (page & ~1).
817                if off16 < PRG_BANK_16K {
818                    (1, PRG_BANK_16K, off16)
819                } else {
820                    (3, PRG_BANK_16K, off16 - PRG_BANK_16K)
821                }
822            }
823            2 => {
824                // 16 K + 8 K + 8 K. `$5115` (slot 1) -> $8000-$BFFF (page & ~1);
825                // `$5116` (slot 2) -> $C000-$DFFF; `$5117` (slot 3) -> $E000-$FFFF.
826                if off16 < PRG_BANK_16K {
827                    (1, PRG_BANK_16K, off16)
828                } else if off16 < PRG_BANK_16K + PRG_BANK_8K {
829                    (2, PRG_BANK_8K, off16 - PRG_BANK_16K)
830                } else {
831                    (3, PRG_BANK_8K, off16 - PRG_BANK_16K - PRG_BANK_8K)
832                }
833            }
834            _ => {
835                // Mode 3: 8 K x 4. `$5114` -> $8000; `$5115` -> $A000;
836                // `$5116` -> $C000; `$5117` -> $E000.
837                let slot = off16 / PRG_BANK_8K; // 0..=3
838                (slot, PRG_BANK_8K, off16 % PRG_BANK_8K)
839            }
840        }
841    }
842
843    /// Write into the PRG window. PRG-RAM writes honor the protect pair;
844    /// ROM writes are silently dropped.
845    fn write_prg_window(&mut self, addr: u16, value: u8) {
846        let (slot, _slot_size, _region_off) = self.prg_window_lookup(addr);
847        // `$5117` is always ROM; never writable.
848        if slot == 3 {
849            return;
850        }
851        let raw = self.prg_banks[slot];
852        if raw.is_rom() {
853            return;
854        }
855        if !self.prg_ram_writable() {
856            return;
857        }
858        if let RamTarget::At(off) = self.prg_ram_target(addr) {
859            self.ram_set(off, value);
860        }
861    }
862
863    /// Resolve a sprite pattern fetch. Sprites always use the A set
864    /// (`$5120-$5127`), in 8x8 and 8x16 mode alike, laid out by `$5101`
865    /// exactly as the MMC5 page's "CHR select $5120-$512B" table gives it.
866    ///
867    /// Until v2.9.9 this read the A registers as eight 1 KiB banks whatever
868    /// `$5101` said; a game in 4 KiB or 8 KiB mode drew its sprites from the
869    /// wrong place (T-MMC5-8X8-SET).
870    fn chr_offset_sprite(&self, addr: u16) -> usize {
871        self.chr_set_offset(addr, ChrSet::A)
872    }
873
874    /// The bank set a non-sprite CHR access uses: a background fetch while
875    /// rendering, or a `$2007` access.
876    ///
877    /// The MMC5 page and the hardware tests it cites (loopy, on the NESdev
878    /// thread its footnotes link) give:
879    ///
880    /// - **8x8 sprites:** "only registers $5120-$5127 are used. Registers
881    ///   $5128-$512B are completely ignored", for background tiles and
882    ///   `$2007` as well as sprites.
883    /// - **8x16 sprites, rendering:** background tiles use the B set
884    ///   (`$5128-$512B`).
885    /// - **8x16 sprites, `$2007`:** "the last set of registers written to";
886    ///   but with extended attributes on, `$2007` READS always use the A set
887    ///   (Sour's result on the same thread). `read` selects that case.
888    ///
889    /// The MMC5 tells a rendering fetch from a `$2007` access by counting
890    /// PPU reads since its last scanline detection. The model's equivalent is
891    /// its in-frame flag together with the snooped `$2001` render enables:
892    /// a fetch made while both say "rendering" is a background fetch.
893    fn non_sprite_set(&self, read: bool) -> ChrSet {
894        if !self.ppu_sprites_8x16 {
895            ChrSet::A
896        } else if self.in_frame && self.ppu_rendering {
897            ChrSet::B
898        } else if self.last_chr_write_was_sprite || (read && self.exram_mode & 0x03 == 1) {
899            ChrSet::A
900        } else {
901            ChrSet::B
902        }
903    }
904
905    /// Resolve `addr` (`$0000-$1FFF`) through one CHR bank set, per the
906    /// MMC5 page's table:
907    ///
908    /// | `$5101` | size  | A set (`$5120-$5127`)        | B set (`$5128-$512B`)                 |
909    /// |---------|-------|------------------------------|---------------------------------------|
910    /// | 0       | 8 KiB | `$5127`                      | `$512B`                               |
911    /// | 1       | 4 KiB | `$5123`, `$5127`             | `$512B` for both halves               |
912    /// | 2       | 2 KiB | `$5121`, `$5123`, `$5125`, `$5127` | `$5129`, `$512B`, repeated per 4 KiB |
913    /// | 3       | 1 KiB | `$5120`-`$5127`              | `$5128`-`$512B`, repeated per 4 KiB   |
914    ///
915    /// "The banks are always indexed by the currently selected size": a
916    /// register value N in 4 KiB mode selects the N-th 4 KiB bank, and its
917    /// low bits are not ignored. Before v2.9.9 the 8 KiB, 4 KiB and 2 KiB
918    /// modes masked the value and indexed 1 KiB banks, and modes 1 and 2
919    /// read B registers the table does not use.
920    fn chr_set_offset(&self, addr: u16, set: ChrSet) -> usize {
921        let a = (addr & 0x1FFF) as usize;
922        let (size, bank) = match (self.chr_mode & 0x03, set) {
923            (0, ChrSet::A) => (0x2000, self.sprite_chr_banks[7]),
924            (1, ChrSet::A) => (0x1000, self.sprite_chr_banks[(a >> 12) * 4 + 3]),
925            (2, ChrSet::A) => (0x0800, self.sprite_chr_banks[(a >> 11) * 2 + 1]),
926            (_, ChrSet::A) => (0x0400, self.sprite_chr_banks[a >> 10]),
927            (0, ChrSet::B) => (0x2000, self.bg_chr_banks[3]),
928            (1, ChrSet::B) => (0x1000, self.bg_chr_banks[3]),
929            (2, ChrSet::B) => (0x0800, self.bg_chr_banks[((a >> 11) & 1) * 2 + 1]),
930            (_, ChrSet::B) => (0x0400, self.bg_chr_banks[(a >> 10) & 0x03]),
931        };
932        // A register value wraps modulo the image's bank count. `Mmc5::new`
933        // accepts any multiple of 1 KiB, so that count need not be a power
934        // of two (24 KiB is three 8 KiB banks), and a mask would leave banks
935        // unreachable (#583 review). It counts a PARTIAL final bank too
936        // (`div_ceil`: 10 KiB in 8 KiB mode is two banks), whose missing
937        // tail the caller's `% len` wraps; floor division left that bank's
938        // bytes unreachable (#583 review, round 4). Every image that is a
939        // multiple of the bank size maps as before.
940        let banks = self.chr.len().div_ceil(size).max(1);
941        ((bank as usize) % banks) * size + (a & (size - 1))
942    }
943
944    /// The byte offset of `addr` within 4 KiB CHR bank `bank4k`, for the
945    /// split-screen and ExGrafix overrides, which both select 4 KiB banks.
946    /// The bank wraps modulo the image's 4 KiB bank count, a partial final
947    /// bank included, as [`Mmc5::chr_set_offset`] does for the register
948    /// sets; the two used to mask by the 1 KiB bank count, which reaches
949    /// every bank only when that count is a power of two. Offsets past the
950    /// image's end are wrapped `% len` by the caller.
951    fn chr_4k_offset(&self, bank4k: usize, addr: u16) -> usize {
952        let banks = self.chr.len().div_ceil(0x1000).max(1);
953        (bank4k % banks) * 0x1000 + (addr & 0x0FFF) as usize
954    }
955
956    /// Resolve a non-sprite PPU CHR address (`$0000-$1FFF`) to a byte offset
957    /// in `chr`: a background fetch or a `$2007` access, `read` telling a
958    /// `$2007` read from a write. The bank set is [`Mmc5::non_sprite_set`]'s.
959    ///
960    /// In MMC5 ExGrafix mode (`$5104` mode 01) the per-tile CHR bank
961    /// latched at the most recent NT-byte fetch overrides the standard
962    /// BG bank decoding; we apply that override here.
963    fn chr_offset(&self, addr: u16, read: bool) -> usize {
964        // Vertical split-screen override: take precedence over both
965        // ExGrafix and standard BG bank decoding. 4 KiB bank from $5202
966        // (latched at NT-fetch time by `bg_split_state`).
967        if let Some(bank4k) = self.split_chr_bank_latch {
968            return self.chr_4k_offset(bank4k as usize, addr);
969        }
970        // ExGrafix override: per-tile 4 KiB bank from the latch.
971        if let Some(bank4k) = self.ex_chr_bank_latch {
972            return self.chr_4k_offset(bank4k as usize, addr);
973        }
974
975        self.chr_set_offset(addr, self.non_sprite_set(read))
976    }
977
978    /// Decode the per-1KiB nametable source for logical table 0..=3.
979    fn nt_source(&self, table: u8) -> NtSource {
980        let bits = (self.nametable_map >> ((table & 0x03) * 2)) & 0x03;
981        NtSource::from_bits(bits)
982    }
983
984    /// Resolve a PPU nametable address `$2000-$3EFF` to either
985    /// (a) a CIRAM offset 0..0x800, or (b) an ExRAM offset, or (c) fill mode.
986    fn nt_resolve(&self, addr: u16) -> (NtSource, usize) {
987        let table = (((addr - 0x2000) / NAMETABLE_SIZE as u16) & 0x03) as u8;
988        let local = (addr as usize) & (NAMETABLE_SIZE - 1);
989        let src = self.nt_source(table);
990        (src, local)
991    }
992
993    /// True iff ExRAM is currently configured for use as a nametable
994    /// (mode 0 or 1 — extended attributes is also a "nametable-mapped"
995    /// mode for routing purposes; the per-tile-attribute interpretation
996    /// is what's deferred).
997    /// How much of `prg_ram` (the header's declared RAM) is battery-backed:
998    /// all of it, except the 16 KiB two-chip ETROM layout, where "games with
999    /// 16K PRG-RAM only battery-save the first 8K". The superset's extra pages
1000    /// are never part of the save, so save files keep the size they had.
1001    const fn battery_len(&self) -> usize {
1002        if self.prg_ram.len() == 0x4000 {
1003            PRG_RAM_BANK
1004        } else {
1005            self.prg_ram.len()
1006        }
1007    }
1008
1009    fn exram_is_nametable(&self) -> bool {
1010        matches!(self.exram_mode & 0x03, 0 | 1)
1011    }
1012}
1013
1014impl Mapper for Mmc5 {
1015    // The battery save. "Games with 16K PRG-RAM only battery-save the first
1016    // 8K" (nesdev MMC5): ETROM's second chip is volatile work RAM, so it is
1017    // not part of the save.
1018    fn sram(&self) -> &[u8] {
1019        let n = self.battery_len();
1020        &self.prg_ram[..n]
1021    }
1022    fn sram_mut(&mut self) -> &mut [u8] {
1023        let n = self.battery_len();
1024        &mut self.prg_ram[..n]
1025    }
1026    // v2.8.0 Phase 4 — MMC5: CPU-cycle hook + IRQ + frame-counter-
1027    // cadenced audio envelopes (+ expansion audio under `mapper-audio`).
1028    fn caps(&self) -> MapperCaps {
1029        MapperCaps {
1030            cpu_cycle_hook: true,
1031            audio: cfg!(feature = "mapper-audio"),
1032            frame_event_hook: true,
1033            irq_source: true,
1034        }
1035    }
1036
1037    fn cpu_read_unmapped(&self, addr: u16) -> bool {
1038        // v2.7.2's "no save RAM -> `$6000-$7FFF` floats" default does not apply:
1039        // the header RAM is at least 8 KiB (`prg_ram_size`), so `sram()` is never
1040        // empty. Nor does any PRG-RAM access float: over the 64 KiB superset
1041        // every page answers (`prg_ram_target` returns `At` for all of them).
1042        {
1043            // MMC5 maps almost the entire `$5000-$5FFF` window: audio at
1044            // `$5000-$5015`, ExGfx config at `$5100-$5107`, PRG bank regs
1045            // at `$5113-$5117`, CHR bank regs at `$5120-$512B`, upper-CHR
1046            // bits at `$5130`, multiplier at `$5205-$5206`, scanline IRQ
1047            // at `$5203-$5204`, split-screen at `$5200-$5207`, and ExRAM
1048            // at `$5C00-$5FFF`. The `$4020-$4FFF` range is not mapped
1049            // (per the default impl convention).
1050            (0x4020..=0x4FFF).contains(&addr)
1051        }
1052    }
1053
1054    fn cpu_read(&mut self, addr: u16) -> u8 {
1055        // v1.4.0 Workstream F (F2): PRG-ROM/RAM fetches at `$8000-$FFFF`
1056        // dominate `cpu_read` (every opcode + operand fetch on an MMC5 cart),
1057        // while the register/ExRAM arms only fire on explicit `$5xxx` accesses.
1058        // Short-circuit the hot case before the register-range match so the
1059        // common path is one compare, not a walk of the `$5xxx` decision tree.
1060        // Byte-identical to the `0x8000..=0xFFFF` match arm below.
1061        if addr >= 0x8000 {
1062            return self.read_prg_window(addr);
1063        }
1064        match addr {
1065            // Audio status (`$5015`): bit 0 = pulse-1 length > 0, bit 1 =
1066            // pulse-2 length > 0. No DMC bit (MMC5 has no DMC).
1067            0x5015 => {
1068                let mut v = 0u8;
1069                if self.audio.pulse1.length > 0 {
1070                    v |= 0x01;
1071                }
1072                if self.audio.pulse2.length > 0 {
1073                    v |= 0x02;
1074                }
1075                v
1076            }
1077
1078            // Other audio range registers are write-only on real hardware
1079            // ($5000-$5014 except $5011 in PCM read-mode — which would be
1080            // a CPU-side sample-delivery port we don't model in v0).
1081            // Falls through to the catch-all $5000-$5FFF "open bus = 0".
1082
1083            // Multiplier readback — most-significant byte and
1084            // least-significant byte of the 16-bit product.
1085            0x5205 => {
1086                let prod = u16::from(self.mul_a) * u16::from(self.mul_b);
1087                (prod & 0xFF) as u8
1088            }
1089            0x5206 => {
1090                let prod = u16::from(self.mul_a) * u16::from(self.mul_b);
1091                ((prod >> 8) & 0xFF) as u8
1092            }
1093
1094            // IRQ status (and ack on read).
1095            0x5204 => {
1096                let mut v = 0u8;
1097                if self.irq_pending {
1098                    v |= 0x80;
1099                }
1100                if self.in_frame {
1101                    v |= 0x40;
1102                }
1103                self.irq_pending = false;
1104                v
1105            }
1106
1107            // ExRAM CPU read window. Modes 10/11 are CPU-readable; modes
1108            // 00/01 are *also* CPU-readable per nesdev (writes are restricted
1109            // depending on rendering state, but reads always succeed).
1110            0x5C00..=0x5FFF => {
1111                let off = (addr - 0x5C00) as usize;
1112                self.exram[off]
1113            }
1114
1115            // Other registers in `$5000-$5FFF` are write-only on real
1116            // hardware — return 0 (open bus is approximated as zero;
1117            // the lockstep bus latches its own open-bus value).
1118            0x5000..=0x5FFF => 0,
1119
1120            // PRG-RAM at `$6000-$7FFF`: always 8 KiB, bank from `$5113`.
1121            0x6000..=0x7FFF => self.ram_read(self.prg_ram_target(addr)),
1122
1123            // PRG-ROM / PRG-RAM windowed by `$5114-$5117`.
1124            0x8000..=0xFFFF => self.read_prg_window(addr),
1125
1126            _ => 0,
1127        }
1128    }
1129
1130    #[allow(clippy::too_many_lines)]
1131    fn cpu_write(&mut self, addr: u16, value: u8) {
1132        match addr {
1133            // === Audio extension ($5000-$5015) ===
1134            // Pulse 1 (`$5000-$5003`). $5001 (sweep slot) is unused — MMC5
1135            // pulses have no sweep unit; write is silently absorbed so
1136            // round-tripping a memcpy of `$5000-$5003` stays no-op.
1137            0x5000 => self.audio.pulse1.write_ctrl(value),
1138            0x5001 => {} // no sweep unit on MMC5 pulse channels.
1139            0x5002 => self.audio.pulse1.write_timer_lo(value),
1140            0x5003 => self.audio.pulse1.write_timer_hi(value),
1141            // Pulse 2 (`$5004-$5007`).
1142            0x5004 => self.audio.pulse2.write_ctrl(value),
1143            0x5005 => {} // no sweep unit.
1144            0x5006 => self.audio.pulse2.write_timer_lo(value),
1145            0x5007 => self.audio.pulse2.write_timer_hi(value),
1146            // $5008-$500F: unused on real hardware (open bus). Absorb writes.
1147            0x5008..=0x500F => {}
1148            // $5010: PCM control. Bit 0 = mode select (0 = write/output;
1149            // 1 = read-mode/CPU-side sample delivery, which silences PCM
1150            // output). Bit 7 = PCM IRQ enable (not modelled — we have no
1151            // PCM-side IRQ source).
1152            0x5010 => {
1153                self.audio.pcm_ctrl = value;
1154            }
1155            // $5011: PCM data. In write-mode (`$5010` bit 0 = 0), the low
1156            // 7 bits drive the PCM channel output level. In read-mode the
1157            // write is ignored.
1158            0x5011 => {
1159                if (self.audio.pcm_ctrl & 0x01) == 0 {
1160                    self.audio.pcm_sample = value & 0x7F;
1161                }
1162            }
1163            // $5012-$5014: unused.
1164            0x5012..=0x5014 => {}
1165            // $5015: per-channel length-enable. Bit 0 -> pulse 1, bit 1 ->
1166            // pulse 2. Other bits ignored (no DMC).
1167            0x5015 => {
1168                self.audio.pulse1.set_length_enabled((value & 0x01) != 0);
1169                self.audio.pulse2.set_length_enabled((value & 0x02) != 0);
1170            }
1171
1172            // PRG mode.
1173            0x5100 => {
1174                self.prg_mode = value & 0x03;
1175            }
1176            // CHR mode.
1177            0x5101 => {
1178                self.chr_mode = value & 0x03;
1179            }
1180            // PRG-RAM protect (two-write magic pair).
1181            0x5102 => {
1182                self.prg_ram_protect_1 = value;
1183            }
1184            0x5103 => {
1185                self.prg_ram_protect_2 = value;
1186            }
1187            // ExRAM mode.
1188            0x5104 => {
1189                self.exram_mode = value & 0x03;
1190                // Switching modes invalidates any cached ExGrafix CHR
1191                // bank latch.
1192                self.ex_chr_bank_latch = None;
1193            }
1194            // Nametable mapping.
1195            0x5105 => {
1196                self.nametable_map = value;
1197                // Update mirroring summary for `current_mirroring`.
1198                self.current_mirroring_summary = nt_summary(value);
1199            }
1200            // Fill-mode tile.
1201            0x5106 => {
1202                self.fill_tile = value;
1203            }
1204            0x5107 => {
1205                self.fill_attr = value & 0x03;
1206            }
1207            // PRG-RAM bank select (v0: only the default single bank).
1208            0x5113 => {
1209                self.prg_ram_bank = value & 0x7F;
1210            }
1211            // PRG bank select 0..3 -> $5114..$5117.
1212            0x5114..=0x5117 => {
1213                let idx = (addr - 0x5114) as usize;
1214                self.prg_banks[idx] = PrgSlot { raw: value };
1215            }
1216            // Sprite CHR banks. Used for sprite tile fetches when 8x16
1217            // sprites are enabled.
1218            0x5120..=0x5127 => {
1219                let idx = (addr - 0x5120) as usize;
1220                self.sprite_chr_banks[idx] = u16::from(value) | (u16::from(self.chr_upper) << 8);
1221                self.last_chr_write_was_sprite = true;
1222            }
1223            // BG CHR banks. Always used for BG tile fetches.
1224            0x5128..=0x512B => {
1225                let idx = (addr - 0x5128) as usize;
1226                self.bg_chr_banks[idx] = u16::from(value) | (u16::from(self.chr_upper) << 8);
1227                self.last_chr_write_was_sprite = false;
1228            }
1229            // Upper CHR bank bits (applied to the $5120-$512B bank indices).
1230            0x5130 => {
1231                self.chr_upper = value & 0x03;
1232            }
1233            // Vertical split-screen mode / scroll / CHR bank.
1234            // $5200 (mode): bit 7 = enable, bit 6 = side (0 = left, 1 = right),
1235            //               bits 4-0 = split tile column (0..=31).
1236            0x5200 => {
1237                self.split_enable = (value & 0x80) != 0;
1238                self.split_side_right = (value & 0x40) != 0;
1239                self.split_tile = value & 0x1F;
1240            }
1241            // $5201: vertical scroll within the alt region.
1242            0x5201 => {
1243                self.split_v_scroll = value;
1244            }
1245            // $5202: 4 KiB CHR bank for the alt region's BG pattern fetches.
1246            0x5202 => {
1247                self.split_chr_bank = value;
1248            }
1249            // Scanline IRQ compare value.
1250            0x5203 => {
1251                self.irq_compare = value;
1252            }
1253            // Scanline IRQ enable.
1254            0x5204 => {
1255                self.irq_enabled = (value & 0x80) != 0;
1256            }
1257            // Multiplier inputs.
1258            0x5205 => {
1259                self.mul_a = value;
1260            }
1261            0x5206 => {
1262                self.mul_b = value;
1263            }
1264            // ExRAM CPU writes. Behavior depends on mode and rendering
1265            // state; v0 simplifies:
1266            //   Mode 00/01: writable always (nametable mode — real hardware
1267            //               only allows during rendering, but games tend
1268            //               to update during VBL too; we accept writes).
1269            //   Mode 10:    writable always (general RAM).
1270            //   Mode 11:    read-only (writes ignored).
1271            0x5C00..=0x5FFF => {
1272                let off = (addr - 0x5C00) as usize;
1273                if (self.exram_mode & 0x03) != 0b11 {
1274                    self.exram[off] = value;
1275                }
1276            }
1277            // PRG-RAM at `$6000-$7FFF`.
1278            0x6000..=0x7FFF => {
1279                if self.prg_ram_writable()
1280                    && let RamTarget::At(off) = self.prg_ram_target(addr)
1281                {
1282                    self.ram_set(off, value);
1283                }
1284            }
1285            // PRG window writes (PRG-RAM banks may be mapped here).
1286            0x8000..=0xFFFF => self.write_prg_window(addr, value),
1287
1288            _ => {}
1289        }
1290    }
1291
1292    fn ppu_read(&mut self, addr: u16) -> u8 {
1293        let addr = addr & 0x3FFF;
1294        match addr {
1295            0x0000..=0x1FFF => {
1296                let off = self.chr_offset(addr, true);
1297                let len = self.chr.len();
1298                self.chr[off % len]
1299            }
1300            0x2000..=0x3EFF => {
1301                // Fill / ExRAM / CIRAM is decided by `$5105`. The lockstep
1302                // bus's PPU calls `peek_nametable` first; this `ppu_read`
1303                // path is only used by the test bus and direct mapper
1304                // probes, so we still service the same logic here.
1305                let (src, local) = self.nt_resolve(addr);
1306                match src {
1307                    NtSource::CiramA => self.vram[local],
1308                    NtSource::CiramB => self.vram[NAMETABLE_SIZE + local],
1309                    NtSource::ExRam => {
1310                        if self.exram_is_nametable() {
1311                            self.exram[local]
1312                        } else {
1313                            0
1314                        }
1315                    }
1316                    NtSource::Fill => {
1317                        if local < 0x03C0 {
1318                            self.fill_tile
1319                        } else {
1320                            let a = self.fill_attr & 0x03;
1321                            (a << 6) | (a << 4) | (a << 2) | a
1322                        }
1323                    }
1324                }
1325            }
1326            _ => 0,
1327        }
1328    }
1329
1330    fn ppu_read_sprite(&mut self, addr: u16) -> u8 {
1331        let addr = addr & 0x1FFF;
1332        let off = self.chr_offset_sprite(addr);
1333        let len = self.chr.len();
1334        self.chr[off % len]
1335    }
1336
1337    fn ppu_write(&mut self, addr: u16, value: u8) {
1338        let addr = addr & 0x3FFF;
1339        match addr {
1340            0x0000..=0x1FFF => {
1341                if self.chr_is_ram {
1342                    let off = self.chr_offset(addr, false);
1343                    let len = self.chr.len();
1344                    self.chr[off % len] = value;
1345                }
1346            }
1347            0x2000..=0x3EFF => {
1348                let (src, local) = self.nt_resolve(addr);
1349                match src {
1350                    NtSource::CiramA => self.vram[local] = value,
1351                    NtSource::CiramB => self.vram[NAMETABLE_SIZE + local] = value,
1352                    NtSource::ExRam => {
1353                        if self.exram_is_nametable() {
1354                            self.exram[local] = value;
1355                        }
1356                    }
1357                    NtSource::Fill => {
1358                        // Fill mode is a read-only synthetic surface; ignore.
1359                    }
1360                }
1361            }
1362            _ => {}
1363        }
1364    }
1365
1366    fn nametable_address(&self, addr: u16) -> u16 {
1367        // CIRAM-bound mapping. For ExRAM and fill mode, the PPU consults
1368        // `nametable_fetch` first (returning a synthesized byte and
1369        // bypassing the CIRAM read entirely); this path is only used as a
1370        // fallback / for callers that have not adopted that hook.
1371        let (src, local) = self.nt_resolve(addr);
1372        let bank = match src {
1373            NtSource::CiramA | NtSource::ExRam | NtSource::Fill => 0,
1374            NtSource::CiramB => 1,
1375        };
1376        (bank * NAMETABLE_SIZE + local) as u16
1377    }
1378
1379    fn nametable_fetch(&mut self, addr: u16) -> Option<u8> {
1380        // Vertical split-screen: when the current BG fetch group has been
1381        // marked as inside the alt region (split CHR bank latched), the
1382        // NT and AT bytes come from ExRAM regardless of the $5105 mapping
1383        // for the synthesized $2000-$23FF address the PPU passes in.
1384        if self.split_chr_bank_latch.is_some() {
1385            let local = (addr as usize) & (NAMETABLE_SIZE - 1);
1386            return Some(self.exram[local & (EXRAM_SIZE - 1)]);
1387        }
1388        let (src, local) = self.nt_resolve(addr);
1389        match src {
1390            NtSource::CiramA | NtSource::CiramB => None,
1391            NtSource::ExRam => {
1392                // ExRAM-as-nametable: synthesize the byte directly from
1393                // ExRAM. In ExGrafix mode (mode 01) the byte is also used
1394                // as a per-tile attribute by `peek_ex_attribute`; here we
1395                // just return the raw byte for any nametable / AT read.
1396                if self.exram_is_nametable() {
1397                    Some(self.exram[local])
1398                } else {
1399                    // ExRAM not configured as nametable (modes 10/11) but
1400                    // `$5105` points there — return open-bus 0.
1401                    Some(0)
1402                }
1403            }
1404            NtSource::Fill => {
1405                // Fill mode: the 32x30 nametable region returns the fill
1406                // tile (`$5106`); the 64-byte attribute region returns the
1407                // 2-bit fill attribute (`$5107`) replicated 4 ways.
1408                if local < 0x03C0 {
1409                    Some(self.fill_tile)
1410                } else {
1411                    let a = self.fill_attr & 0x03;
1412                    Some((a << 6) | (a << 4) | (a << 2) | a)
1413                }
1414            }
1415        }
1416    }
1417
1418    fn nametable_write(&mut self, addr: u16, value: u8) -> bool {
1419        let (src, local) = self.nt_resolve(addr);
1420        match src {
1421            NtSource::CiramA | NtSource::CiramB => {
1422                // Defer to the PPU's CIRAM write.
1423                false
1424            }
1425            NtSource::ExRam => {
1426                if self.exram_is_nametable() {
1427                    self.exram[local] = value;
1428                }
1429                // Even when ExRAM-as-NT is not active for the current
1430                // mode, `$5105` pointing at ExRAM masks the CIRAM write —
1431                // real hardware drops it on the floor. We absorb it.
1432                true
1433            }
1434            NtSource::Fill => {
1435                // Fill mode: writes are dropped (read-only synthetic).
1436                true
1437            }
1438        }
1439    }
1440
1441    fn peek_ex_attribute(&mut self, v: u16) -> Option<ExAttribute> {
1442        // ExGrafix is `$5104` mode 01.
1443        if (self.exram_mode & 0x03) != 0b01 {
1444            // Clear the chr-bank latch so subsequent BG fetches use the
1445            // standard BG bank registers.
1446            self.ex_chr_bank_latch = None;
1447            return None;
1448        }
1449        // The current tile within the active nametable is encoded in the
1450        // low 12 bits of v: low 5 = coarse-X, next 5 = coarse-Y, next 2 =
1451        // nametable select. ExRAM is 1 KiB and indexed by the same 10-bit
1452        // tile coordinate (32 cols * 30 rows = 960; ExRAM is 1024 — we
1453        // mod by 1024 to avoid OOB on the unused 64 entries).
1454        let coarse_x = (v & 0x001F) as usize;
1455        let coarse_y = ((v >> 5) & 0x001F) as usize;
1456        let tile_idx = (coarse_y * 32 + coarse_x) & (EXRAM_SIZE - 1);
1457        let byte = self.exram[tile_idx];
1458        // Bits 7-6 = palette (2 bits).
1459        let palette = (byte >> 6) & 0x03;
1460        // Bits 5-0 = upper 6 bits of the CHR bank for this tile.
1461        // Combined with `$5130` upper 2 bits (low 2 bits of `chr_upper`)
1462        // shifted left by 6 to form an 8-bit raw bank — but per nesdev
1463        // the ExGrafix CHR bank is 4 KiB units, with `$5130` bits 1-0
1464        // as the topmost 2 of an 8-bit bank index.
1465        let bank_low6 = u16::from(byte & 0x3F);
1466        let bank_high2 = u16::from(self.chr_upper & 0x03) << 6;
1467        let bank4k = bank_low6 | bank_high2;
1468        // Latch internally so the BG pattern fetches in this tile use it.
1469        self.ex_chr_bank_latch = Some(bank4k);
1470        Some(ExAttribute {
1471            palette,
1472            chr_bank: bank4k,
1473        })
1474    }
1475
1476    fn bg_split_state(&mut self, scanline_y: u16, coarse_x: u16) -> Option<BgSplitState> {
1477        if !self.split_enable {
1478            // Drop any stale CHR latch from the previous tile group.
1479            self.split_chr_bank_latch = None;
1480            return None;
1481        }
1482        // Decide whether this tile column is in the alt region.
1483        // `split_side_right == false` (bit 6 = 0): alt region = columns < split_tile.
1484        // `split_side_right == true`              : alt region = columns >= split_tile.
1485        let cx = (coarse_x & 0x1F) as u8;
1486        let split_tile = self.split_tile & 0x1F;
1487        let in_alt = if self.split_side_right {
1488            cx >= split_tile
1489        } else {
1490            cx < split_tile
1491        };
1492        if !in_alt {
1493            self.split_chr_bank_latch = None;
1494            return None;
1495        }
1496
1497        // Compute the alt region's logical row from the current scanline +
1498        // $5201 vertical scroll. The alt region is a flat 32x30 nametable
1499        // backed by ExRAM (`$5C00-$5FBF` for NT bytes, `$5FC0-$5FFF` for
1500        // attributes). Scrolling wraps at 240.
1501        let y_in_region = (u16::from(self.split_v_scroll) + scanline_y) % 240;
1502        let coarse_y = (y_in_region / 8) & 0x1F;
1503        let fine_y = (y_in_region % 8) as u8;
1504
1505        // Synthesize an NT byte address inside the $2000-$23FF window — the
1506        // PPU's `peek_nametable` (calling `nametable_fetch`) will route this
1507        // to ExRAM (the alt region is *always* sourced from ExRAM, regardless
1508        // of $5105 — see nesdev MMC5 §"Vertical split mode").
1509        //
1510        // We anchor the address inside NT0 so the PPU's loopy-style decoding
1511        // (`coarse_y * 32 + coarse_x`) lands at the correct ExRAM index.
1512        // Our `nametable_fetch` below treats split-active addresses inside
1513        // NT0 specially.
1514        let nt_addr = 0x2000 | (coarse_y << 5) | u16::from(cx);
1515        let at_addr = 0x23C0 | ((coarse_y >> 2) << 3) | u16::from(cx >> 2);
1516
1517        // Latch the $5202 4 KiB CHR bank for the pattern fetches in this
1518        // 8-dot group.
1519        self.split_chr_bank_latch = Some(self.split_chr_bank);
1520
1521        Some(BgSplitState {
1522            nt_addr,
1523            at_addr,
1524            fine_y,
1525            chr_bank: self.split_chr_bank,
1526        })
1527    }
1528
1529    fn current_mirroring(&self) -> Mirroring {
1530        self.current_mirroring_summary
1531    }
1532
1533    fn notify_a12(&mut self, _level: bool) {
1534        // MMC5 does NOT use A12 for IRQ — it has its own scanline detector.
1535        // Intentionally empty.
1536    }
1537
1538    fn notify_cpu_cycle(&mut self) {
1539        // No CPU-cycle IRQ for MMC5. However, the audio extension's two
1540        // pulse channels tick their 11-bit timer / duty sequencer every
1541        // *other* CPU cycle — same as the 2A03 pulses. Envelope &
1542        // length-counter clocks arrive via `notify_frame_event` and are
1543        // handled separately.
1544        #[cfg(feature = "mapper-audio")]
1545        {
1546            self.audio_apu_phase = !self.audio_apu_phase;
1547            if self.audio_apu_phase {
1548                self.audio.pulse1.clock_timer();
1549                self.audio.pulse2.clock_timer();
1550            }
1551        }
1552    }
1553
1554    fn notify_frame_event(&mut self, events: MapperFrameEvents) {
1555        // MMC5 pulse channels share the 2A03 frame-counter cadence. Quarter
1556        // frame -> envelope clock; half frame -> length clock. No sweep.
1557        // Without the `mapper-audio` feature the channels do not advance,
1558        // but `length` still decrements (cheap, no-effect) since `output`
1559        // is gated independently — keeping this branchless under the
1560        // feature-OFF build is harmless. We still feature-gate explicitly
1561        // to make the audio surface a no-op under the off path.
1562        #[cfg(feature = "mapper-audio")]
1563        {
1564            if events.quarter {
1565                self.audio.pulse1.clock_envelope();
1566                self.audio.pulse2.clock_envelope();
1567            }
1568            if events.half {
1569                self.audio.pulse1.clock_length();
1570                self.audio.pulse2.clock_length();
1571            }
1572        }
1573        #[cfg(not(feature = "mapper-audio"))]
1574        {
1575            let _ = events;
1576        }
1577    }
1578
1579    #[cfg(feature = "mapper-audio")]
1580    fn mix_audio(&mut self) -> i32 {
1581        // Two pulse outputs (each 0..=15) plus one 7-bit PCM level.
1582        //
1583        // PCM is silenced when `$5010` bit 0 = 1 (read-mode) -- the chip
1584        // is then sourcing samples back to the CPU rather than outputting.
1585        let p1 = i16::from(self.audio.pulse1.output());
1586        let p2 = i16::from(self.audio.pulse2.output());
1587        let pcm = if (self.audio.pcm_ctrl & 0x01) == 0 {
1588            i16::from(self.audio.pcm_sample) // 0..=127
1589        } else {
1590            0
1591        };
1592        // v2.1.6 — hardware-accurate levels. The nesdev wiki §"MMC5 audio" and
1593        // Mesen2 both note the MMC5 pulses use the SAME DAC/gain as the 2A03
1594        // pulses, so a full-volume MMC5 square must be ~equal in loudness to a
1595        // full-volume 2A03 square. A single pulse toggling 0↔15 must therefore
1596        // swing the mixer by ~`0.1488 * 65536 ≈ 9755` raw units (the 2A03
1597        // `pulse_table[15]` amplitude after the bus's `/65536` normalization) —
1598        // i.e. a per-pulse scale of `9755/15 ≈ 650`. The 7-bit raw PCM keeps its
1599        // documented ~half-gain relative to the pulses (`40 ≈ 650/16`). Peak
1600        // stays in range: `(15+15)*650 + 127*40 = 24580 < i16::MAX`, so a Koei
1601        // PCM-voice + dual-pulse passage never clips. Before v2.1.6 this was
1602        // `256`/`16` (≈0.39x the 2A03 pulse — ~6.8 dB too quiet). Bias = half
1603        // the peak so the AC signal is centred (the bus HPF removes any residual
1604        // DC regardless). See `docs/mappers.md` §MMC5 audio. The scale/bias
1605        // constants are shared with the NSF path so they can't drift.
1606        let pulse_mix = (p1 + p2) * MMC5_PULSE_SCALE; // 0..=19500
1607        let pcm_mix = pcm * MMC5_PCM_SCALE; // 0..=5080
1608        i32::from((pulse_mix + pcm_mix) - MMC5_MIX_BIAS)
1609    }
1610
1611    fn notify_scanline_start(&mut self) {
1612        // First rendered scanline after VBL: enter "in-frame" state and
1613        // reset the scanline counter to 0.
1614        if !self.in_frame {
1615            self.in_frame = true;
1616            self.scanline_counter = 0;
1617            // The compare register can match scanline 0 too — handle below.
1618        } else {
1619            self.scanline_counter = self.scanline_counter.wrapping_add(1);
1620        }
1621        if self.scanline_counter == self.irq_compare && self.irq_compare != 0 {
1622            self.irq_pending = true;
1623        }
1624    }
1625
1626    fn notify_ppu_register_write(&mut self, addr: u16, value: u8) {
1627        match addr {
1628            0x2000 => {
1629                self.ppu_sprites_8x16 = value & 0x20 != 0;
1630                // "being in 8x8 resets what set of registers was written
1631                // last" (loopy's hardware result): 8x16 then uses the A set
1632                // for `$2007` until the B set is written again.
1633                if !self.ppu_sprites_8x16 {
1634                    self.last_chr_write_was_sprite = true;
1635                }
1636            }
1637            0x2001 => self.ppu_rendering = value & 0x18 != 0,
1638            _ => {}
1639        }
1640    }
1641
1642    fn notify_vblank(&mut self) {
1643        // Vertical blank: clear the in-frame flag (the next rendered line
1644        // re-enters "in-frame" via notify_scanline_start).
1645        self.in_frame = false;
1646    }
1647
1648    fn irq_pending(&self) -> bool {
1649        self.irq_pending && self.irq_enabled
1650    }
1651
1652    fn irq_acknowledge(&mut self) {
1653        // MMC5 acks via reading $5204; we don't ack here.
1654    }
1655
1656    fn debug_info(&self) -> crate::mapper::MapperDebugInfo {
1657        let mut info = crate::mapper::MapperDebugInfo {
1658            mapper_id: 5,
1659            name: "MMC5".into(),
1660            mirroring: crate::mapper::mirroring_name(self.current_mirroring()),
1661            ..Default::default()
1662        };
1663        info.prg_banks
1664            .push(("mode".into(), format!("{}", self.prg_mode)));
1665        for (i, slot) in self.prg_banks.iter().enumerate() {
1666            info.prg_banks
1667                .push((format!("$5114+{i}"), format!("{:#04x}", slot.raw)));
1668        }
1669        info.chr_banks
1670            .push(("mode".into(), format!("{}", self.chr_mode)));
1671        for (i, b) in self.bg_chr_banks.iter().enumerate() {
1672            info.chr_banks.push((format!("BG{i}"), format!("{b:#04x}")));
1673        }
1674        for (i, b) in self.sprite_chr_banks.iter().enumerate() {
1675            info.chr_banks.push((format!("SP{i}"), format!("{b:#04x}")));
1676        }
1677        info.irq_state
1678            .push(("compare".into(), format!("{:#04x}", self.irq_compare)));
1679        info.irq_state
1680            .push(("scanline".into(), format!("{:#04x}", self.scanline_counter)));
1681        info.irq_state
1682            .push(("enabled".into(), format!("{}", self.irq_enabled)));
1683        info.irq_state
1684            .push(("pending".into(), format!("{}", self.irq_pending)));
1685        info.irq_state
1686            .push(("in_frame".into(), format!("{}", self.in_frame)));
1687        info.extra
1688            .push(("nt_map".into(), format!("{:#04x}", self.nametable_map)));
1689        info.extra
1690            .push(("exram_mode".into(), format!("{}", self.exram_mode)));
1691        info.extra.push((
1692            "split".into(),
1693            format!(
1694                "en={} tile={} v={} chr={}",
1695                self.split_enable, self.split_tile, self.split_v_scroll, self.split_chr_bank
1696            ),
1697        ));
1698        info
1699    }
1700
1701    fn save_state(&self) -> Vec<u8> {
1702        let mut out = Vec::with_capacity(
1703            64 + self.prg_ram.len() + self.vram.len() + self.exram.len() + self.chr.len(),
1704        );
1705        out.push(SAVE_STATE_VERSION);
1706        out.push(self.prg_mode);
1707        out.push(self.chr_mode);
1708        out.push(self.prg_ram_protect_1);
1709        out.push(self.prg_ram_protect_2);
1710        out.push(self.exram_mode);
1711        out.push(self.nametable_map);
1712        out.push(self.fill_tile);
1713        out.push(self.fill_attr);
1714        out.push(self.prg_ram_bank);
1715        for slot in &self.prg_banks {
1716            out.push(slot.raw);
1717        }
1718        for &b in &self.bg_chr_banks {
1719            out.extend_from_slice(&b.to_le_bytes());
1720        }
1721        for &b in &self.sprite_chr_banks {
1722            out.extend_from_slice(&b.to_le_bytes());
1723        }
1724        out.push(self.chr_upper);
1725        out.push(u8::from(self.last_chr_write_was_sprite));
1726        out.push(u8::from(self.ppu_sprites_8x16));
1727        out.push(u8::from(self.ppu_rendering));
1728        // ExGrafix CHR bank latch: 1 byte tag (0/1 = absent/present)
1729        // followed by 2 bytes of bank value.
1730        if let Some(b) = self.ex_chr_bank_latch {
1731            out.push(1);
1732            out.extend_from_slice(&b.to_le_bytes());
1733        } else {
1734            out.push(0);
1735            out.extend_from_slice(&[0u8, 0u8]);
1736        }
1737        // Vertical split-screen state ($5200-$5202) — added in v3.
1738        // 6 bytes total: split_enable | split_side_right | split_tile |
1739        //                split_v_scroll | split_chr_bank | latch_tag(+1 byte value).
1740        out.push(u8::from(self.split_enable));
1741        out.push(u8::from(self.split_side_right));
1742        out.push(self.split_tile);
1743        out.push(self.split_v_scroll);
1744        out.push(self.split_chr_bank);
1745        if let Some(b) = self.split_chr_bank_latch {
1746            out.push(1);
1747            out.push(b);
1748        } else {
1749            out.push(0);
1750            out.push(0);
1751        }
1752        out.push(self.irq_compare);
1753        out.push(u8::from(self.irq_enabled));
1754        out.push(u8::from(self.irq_pending));
1755        out.push(u8::from(self.in_frame));
1756        out.push(self.scanline_counter);
1757        out.push(self.mul_a);
1758        out.push(self.mul_b);
1759        out.push(self.current_mirroring_summary as u8);
1760        out.extend_from_slice(&self.prg_ram);
1761        out.extend_from_slice(&self.vram);
1762        out.extend_from_slice(&self.exram);
1763        if self.chr_is_ram {
1764            out.extend_from_slice(&self.chr);
1765        }
1766        // v4 tail: audio extension state (30 bytes).
1767        out.push(u8::from(self.audio_apu_phase));
1768        self.audio.write_tail(&mut out);
1769        // v5 tail (v2.7.2): the superset's extra PRG-RAM pages.
1770        out.extend_from_slice(&self.prg_ram_extra);
1771        out
1772    }
1773
1774    #[allow(clippy::too_many_lines)]
1775    fn load_state(&mut self, data: &[u8]) -> Result<(), MapperError> {
1776        let chr_part = if self.chr_is_ram { self.chr.len() } else { 0 };
1777        // Scalar layout:
1778        //   1 (version) + 9 (prg_mode..prg_ram_bank) + 4 (prg_banks)
1779        //   + 8 (4 * 2 bytes BG) + 16 (8 * 2 bytes sprite)
1780        //   + 1 (chr_upper) + 1 (last_chr_write_was_sprite)
1781        //   + 2 (ppu_sprites_8x16, ppu_rendering; v6)
1782        //   + 1 (ex_chr_bank_latch tag) + 2 (ex_chr_bank_latch value)
1783        //   + 1 (irq_compare) + 1 (irq_enabled) + 1 (irq_pending)
1784        //   + 1 (in_frame) + 1 (scanline_counter) + 1 (mul_a) + 1 (mul_b)
1785        //   + 1 (mirroring_summary)
1786        // v3 adds 7 bytes for vertical split state:
1787        //   split_enable + split_side_right + split_tile + split_v_scroll
1788        //   + split_chr_bank + split_chr_bank_latch (tag + value)
1789        let scalar_len: usize =
1790            1 + 9 + 4 + 8 + 16 + 1 + 1 + 2 + 1 + 2 + 7 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1;
1791        let core_expected =
1792            scalar_len + self.prg_ram.len() + self.vram.len() + self.exram.len() + chr_part;
1793        // Only the current version is read (v2.9.8, ADR 0042): v3 (no audio
1794        // tail) and v4 (no superset pages) used to load with those parts at
1795        // their defaults. v5 (v2.9.8, no `$2000` / `$2001` snoop) is refused
1796        // too: a v5 state restored with 8x8 assumed would draw an 8x16 game
1797        // from the wrong bank set until its next `$2000` write.
1798        let version = if data.is_empty() { 0 } else { data[0] };
1799        if version != SAVE_STATE_VERSION {
1800            return Err(MapperError::UnsupportedVersion(version));
1801        }
1802        let expected = core_expected + 1 + Mmc5Audio::TAIL_LEN + self.prg_ram_extra.len();
1803        if data.len() != expected {
1804            return Err(MapperError::WrongLength {
1805                expected,
1806                got: data.len(),
1807            });
1808        }
1809        let mut cur = 1usize;
1810        self.prg_mode = data[cur];
1811        cur += 1;
1812        self.chr_mode = data[cur];
1813        cur += 1;
1814        self.prg_ram_protect_1 = data[cur];
1815        cur += 1;
1816        self.prg_ram_protect_2 = data[cur];
1817        cur += 1;
1818        self.exram_mode = data[cur];
1819        cur += 1;
1820        self.nametable_map = data[cur];
1821        cur += 1;
1822        self.fill_tile = data[cur];
1823        cur += 1;
1824        self.fill_attr = data[cur];
1825        cur += 1;
1826        self.prg_ram_bank = data[cur];
1827        cur += 1;
1828        for slot in &mut self.prg_banks {
1829            slot.raw = data[cur];
1830            cur += 1;
1831        }
1832        for b in &mut self.bg_chr_banks {
1833            *b = u16::from_le_bytes([data[cur], data[cur + 1]]);
1834            cur += 2;
1835        }
1836        for b in &mut self.sprite_chr_banks {
1837            *b = u16::from_le_bytes([data[cur], data[cur + 1]]);
1838            cur += 2;
1839        }
1840        self.chr_upper = data[cur];
1841        cur += 1;
1842        self.last_chr_write_was_sprite = data[cur] != 0;
1843        cur += 1;
1844        self.ppu_sprites_8x16 = data[cur] != 0;
1845        cur += 1;
1846        self.ppu_rendering = data[cur] != 0;
1847        cur += 1;
1848        // ExGrafix CHR bank latch: tag + 2-byte value.
1849        let tag = data[cur];
1850        cur += 1;
1851        let bank_lo = data[cur];
1852        let bank_hi = data[cur + 1];
1853        cur += 2;
1854        self.ex_chr_bank_latch = if tag != 0 {
1855            Some(u16::from_le_bytes([bank_lo, bank_hi]))
1856        } else {
1857            None
1858        };
1859        // v3: vertical split state.
1860        self.split_enable = data[cur] != 0;
1861        cur += 1;
1862        self.split_side_right = data[cur] != 0;
1863        cur += 1;
1864        self.split_tile = data[cur] & 0x1F;
1865        cur += 1;
1866        self.split_v_scroll = data[cur];
1867        cur += 1;
1868        self.split_chr_bank = data[cur];
1869        cur += 1;
1870        let split_tag = data[cur];
1871        cur += 1;
1872        let split_lat = data[cur];
1873        cur += 1;
1874        self.split_chr_bank_latch = if split_tag != 0 {
1875            Some(split_lat)
1876        } else {
1877            None
1878        };
1879        self.irq_compare = data[cur];
1880        cur += 1;
1881        self.irq_enabled = data[cur] != 0;
1882        cur += 1;
1883        self.irq_pending = data[cur] != 0;
1884        cur += 1;
1885        self.in_frame = data[cur] != 0;
1886        cur += 1;
1887        self.scanline_counter = data[cur];
1888        cur += 1;
1889        self.mul_a = data[cur];
1890        cur += 1;
1891        self.mul_b = data[cur];
1892        cur += 1;
1893        self.current_mirroring_summary = match data[cur] {
1894            0 => Mirroring::Horizontal,
1895            1 => Mirroring::Vertical,
1896            2 => Mirroring::SingleScreenA,
1897            3 => Mirroring::SingleScreenB,
1898            4 => Mirroring::FourScreen,
1899            5 => Mirroring::MapperControlled,
1900            other => {
1901                return Err(MapperError::Invalid(format!(
1902                    "unknown mirroring tag {other}"
1903                )));
1904            }
1905        };
1906        cur += 1;
1907        let prg_ram_len = self.prg_ram.len();
1908        self.prg_ram.copy_from_slice(&data[cur..cur + prg_ram_len]);
1909        cur += prg_ram_len;
1910        let vram_len = self.vram.len();
1911        self.vram.copy_from_slice(&data[cur..cur + vram_len]);
1912        cur += vram_len;
1913        let exram_len = self.exram.len();
1914        self.exram.copy_from_slice(&data[cur..cur + exram_len]);
1915        cur += exram_len;
1916        if self.chr_is_ram {
1917            let chr_len = self.chr.len();
1918            self.chr.copy_from_slice(&data[cur..cur + chr_len]);
1919            cur += chr_len;
1920        }
1921        // v4 tail: the audio extension.
1922        self.audio_apu_phase = data[cur] != 0;
1923        cur += 1;
1924        self.audio
1925            .read_tail(&data[cur..cur + Mmc5Audio::TAIL_LEN])?;
1926        cur += Mmc5Audio::TAIL_LEN;
1927        // v5 tail: the superset pages.
1928        let n = self.prg_ram_extra.len();
1929        self.prg_ram_extra.copy_from_slice(&data[cur..cur + n]);
1930        Ok(())
1931    }
1932}
1933
1934/// Decode `$5105` into a coarse `Mirroring` summary for `current_mirroring`.
1935/// MMC5 supports per-1KiB nametable mapping that `Mirroring` cannot fully
1936/// represent; we collapse common cases:
1937///   `0b01_00_01_00` (NT_A/B/A/B) -> Vertical
1938///   `0b00_00_01_01` (NT_A/A/B/B) -> Horizontal (well, the inverse of...)
1939///   `0b00_00_00_00` -> SingleScreenA
1940///   `0b01_01_01_01` -> SingleScreenB
1941/// For more exotic mappings we report `MapperControlled`.
1942fn nt_summary(byte: u8) -> Mirroring {
1943    match byte {
1944        0x44 /* 0b01_00_01_00 */ => Mirroring::Vertical,
1945        0x50 /* 0b01_01_00_00 */ => Mirroring::Horizontal,
1946        0x00 => Mirroring::SingleScreenA,
1947        0x55 /* 0b01_01_01_01 */ => Mirroring::SingleScreenB,
1948        _ => Mirroring::MapperControlled,
1949    }
1950}
1951
1952#[cfg(test)]
1953#[allow(clippy::cast_possible_truncation)]
1954mod tests {
1955    use super::*;
1956
1957    fn synth_prg(banks_8k: usize) -> Box<[u8]> {
1958        let mut v = vec![0u8; banks_8k * PRG_BANK_8K];
1959        for b in 0..banks_8k {
1960            // Mark the start of each 8 KiB page with its bank index so we
1961            // can verify banking math.
1962            v[b * PRG_BANK_8K] = b as u8;
1963            // Mark the last byte of each page with bank index XOR 0xFF.
1964            v[(b + 1) * PRG_BANK_8K - 1] = !(b as u8);
1965        }
1966        v.into_boxed_slice()
1967    }
1968
1969    fn synth_chr(banks_1k: usize) -> Box<[u8]> {
1970        let mut v = vec![0u8; banks_1k * CHR_BANK_1K];
1971        for b in 0..banks_1k {
1972            v[b * CHR_BANK_1K] = b as u8;
1973        }
1974        v.into_boxed_slice()
1975    }
1976
1977    fn fresh(prg_banks: usize, chr_banks: usize) -> Mmc5 {
1978        Mmc5::new(
1979            synth_prg(prg_banks),
1980            synth_chr(chr_banks),
1981            Mirroring::Vertical,
1982            0,
1983        )
1984        .unwrap()
1985    }
1986
1987    #[test]
1988    fn power_on_defaults_to_prg_mode_3_with_last_bank_at_e000() {
1989        let mut m = fresh(8, 8);
1990        // Default: PRG mode 3 (4x8K), $5117 -> last bank ROM. $E000 should
1991        // read bank 7's first byte (= 7).
1992        assert_eq!(m.cpu_read(0xE000), 7);
1993        // Last byte of $FFFF window:
1994        assert_eq!(m.cpu_read(0xFFFF), !7u8);
1995    }
1996
1997    #[test]
1998    fn prg_mode_0_uses_single_32k_bank() {
1999        let mut m = fresh(8, 8);
2000        // Mode 0: 32K driven by $5117 (page bits & ~3).
2001        m.cpu_write(0x5100, 0);
2002        // Set $5117 to bank 4 (which gets masked to 4 since 4 & ~3 == 4).
2003        m.cpu_write(0x5117, 0x80 | 4);
2004        // $8000 -> page 4, $A000 -> page 5, $C000 -> page 6, $E000 -> page 7.
2005        assert_eq!(m.cpu_read(0x8000), 4);
2006        assert_eq!(m.cpu_read(0xA000), 5);
2007        assert_eq!(m.cpu_read(0xC000), 6);
2008        assert_eq!(m.cpu_read(0xE000), 7);
2009    }
2010
2011    #[test]
2012    fn prg_mode_1_two_16k_banks() {
2013        let mut m = fresh(8, 8);
2014        m.cpu_write(0x5100, 1);
2015        // $5115 -> low 16K (page & ~1), $5117 -> high 16K (page & ~1).
2016        m.cpu_write(0x5115, 0x80 | 2); // page 2 -> 2 & ~1 = 2
2017        m.cpu_write(0x5117, 0x80 | 5); // page 5 -> 5 & ~1 = 4
2018        assert_eq!(m.cpu_read(0x8000), 2);
2019        assert_eq!(m.cpu_read(0xA000), 3);
2020        assert_eq!(m.cpu_read(0xC000), 4);
2021        assert_eq!(m.cpu_read(0xE000), 5);
2022    }
2023
2024    #[test]
2025    fn prg_mode_2_16k_plus_8k_plus_8k() {
2026        let mut m = fresh(8, 8);
2027        m.cpu_write(0x5100, 2);
2028        m.cpu_write(0x5115, 0x80 | 2); // 16K @ $8000
2029        m.cpu_write(0x5116, 0x80 | 5); // 8K @ $C000
2030        m.cpu_write(0x5117, 0x80 | 7); // 8K @ $E000
2031        assert_eq!(m.cpu_read(0x8000), 2);
2032        assert_eq!(m.cpu_read(0xA000), 3);
2033        assert_eq!(m.cpu_read(0xC000), 5);
2034        assert_eq!(m.cpu_read(0xE000), 7);
2035    }
2036
2037    #[test]
2038    fn prg_mode_3_four_8k_banks() {
2039        let mut m = fresh(8, 8);
2040        m.cpu_write(0x5100, 3);
2041        m.cpu_write(0x5114, 0x80 | 1);
2042        m.cpu_write(0x5115, 0x80 | 3);
2043        m.cpu_write(0x5116, 0x80 | 5);
2044        m.cpu_write(0x5117, 0x80 | 7);
2045        assert_eq!(m.cpu_read(0x8000), 1);
2046        assert_eq!(m.cpu_read(0xA000), 3);
2047        assert_eq!(m.cpu_read(0xC000), 5);
2048        assert_eq!(m.cpu_read(0xE000), 7);
2049    }
2050
2051    #[test]
2052    fn slot_5117_is_always_rom() {
2053        let mut m = fresh(8, 8);
2054        m.cpu_write(0x5100, 3);
2055        // Try to mark $5117 as RAM by clearing bit 7.
2056        m.cpu_write(0x5117, 7); // page 7, no bit-7 set ("RAM" bit)
2057        // Should still read ROM bank 7.
2058        assert_eq!(m.cpu_read(0xE000), 7);
2059    }
2060
2061    /// Put the MMC5 in 8x16 mode while rendering, the only state in which the
2062    /// B set (`$5128-$512B`) drives background fetches.
2063    fn rendering_8x16(m: &mut Mmc5) {
2064        m.notify_ppu_register_write(0x2000, 0x20);
2065        m.notify_ppu_register_write(0x2001, 0x18);
2066        m.notify_scanline_start();
2067    }
2068
2069    // The CHR tests below are written from the MMC5 page's "CHR select
2070    // $5120-$512B" table and its rule that "the banks are always indexed by
2071    // the currently selected size" (`nesdev_wiki/output/MMC5.md`). Before
2072    // v2.9.9 the model masked the value and indexed 1 KiB banks in the 8, 4
2073    // and 2 KiB modes, read B registers the table does not use, and used the
2074    // B set for background tiles in 8x8 mode (T-MMC5-8X8-SET).
2075
2076    #[test]
2077    fn chr_mode_0_indexes_8k_banks() {
2078        // 32 KiB of CHR = four 8 KiB banks; bank N starts at 1 KiB bank 8N.
2079        let mut m = fresh(8, 32);
2080        m.cpu_write(0x5101, 0);
2081        m.cpu_write(0x5127, 1); // A set, 8x8 mode: background too
2082        assert_eq!(m.ppu_read(0x0000), 8);
2083        assert_eq!(m.ppu_read(0x1C00), 15);
2084        assert_eq!(m.ppu_read_sprite(0x0400), 9);
2085        rendering_8x16(&mut m);
2086        m.cpu_write(0x512B, 3);
2087        assert_eq!(m.ppu_read(0x0000), 24);
2088        assert_eq!(m.ppu_read_sprite(0x0000), 8);
2089    }
2090
2091    #[test]
2092    fn a_chr_image_that_is_not_a_power_of_two_reaches_every_bank() {
2093        // `Mmc5::new` accepts any multiple of 1 KiB. 24 KiB is three 8 KiB
2094        // banks (1 KiB banks 0, 8 and 16); a power-of-two mask (`& 2`) could
2095        // never select bank 1 and folded bank 3 onto bank 1 (#583 review).
2096        let mut m = fresh(8, 24);
2097        m.cpu_write(0x5101, 0);
2098        for (reg, first_1k) in [(0u8, 0u8), (1, 8), (2, 16), (3, 0)] {
2099            m.cpu_write(0x5127, reg);
2100            assert_eq!(m.ppu_read(0x0000), first_1k, "8 KiB bank {reg}");
2101        }
2102        // The ExGrafix override indexes 4 KiB banks of the same image: six
2103        // of them, so 4 KiB bank 2 is 1 KiB bank 8 and bank 6 wraps to 0.
2104        m.cpu_write(0x5104, 1);
2105        for (bank4k, first_1k) in [(2u16, 8u8), (5, 20), (6, 0)] {
2106            m.ex_chr_bank_latch = Some(bank4k);
2107            assert_eq!(m.ppu_read(0x0000), first_1k, "ExGrafix 4 KiB bank {bank4k}");
2108        }
2109    }
2110
2111    #[test]
2112    fn a_partial_final_chr_bank_is_selectable() {
2113        // #583 review round 4 (CodeRabbit): a bank count by floor division
2114        // leaves a partial final bank unreachable. 10 KiB in 8 KiB mode is
2115        // one whole bank and 2 KiB of a second; register 1 must reach that
2116        // second bank's bytes (1 KiB bank 8), not wrap to bank 0. The
2117        // byte-offset wrap (`% len`) keeps the read in range.
2118        let mut m = fresh(8, 10);
2119        m.cpu_write(0x5101, 0);
2120        m.cpu_write(0x5127, 1);
2121        assert_eq!(m.ppu_read(0x0000), 8, "8 KiB bank 1 starts at 1 KiB bank 8");
2122        assert_eq!(m.ppu_read(0x0400), 9);
2123        // 6 KiB under the 4 KiB ExGrafix override: 4 KiB bank 1 is 1 KiB 4.
2124        let mut m = fresh(8, 6);
2125        m.cpu_write(0x5104, 1);
2126        m.ex_chr_bank_latch = Some(1);
2127        assert_eq!(m.ppu_read(0x0000), 4, "4 KiB bank 1 starts at 1 KiB bank 4");
2128    }
2129
2130    #[test]
2131    fn chr_mode_1_indexes_4k_banks() {
2132        let mut m = fresh(8, 32);
2133        m.cpu_write(0x5101, 1);
2134        m.cpu_write(0x5123, 1); // $0000-$0FFF -> 4 KiB bank 1 (1 KiB 4)
2135        m.cpu_write(0x5127, 6); // $1000-$1FFF -> 4 KiB bank 6 (1 KiB 24)
2136        m.cpu_write(0x5121, 7); // not used in 4 KiB mode
2137        assert_eq!(m.ppu_read(0x0000), 4);
2138        assert_eq!(m.ppu_read(0x0C00), 7);
2139        assert_eq!(m.ppu_read(0x1000), 24);
2140        assert_eq!(m.ppu_read_sprite(0x1400), 25);
2141        // B set: `$512B` drives both halves; `$5129` is unused.
2142        rendering_8x16(&mut m);
2143        m.cpu_write(0x5129, 5);
2144        m.cpu_write(0x512B, 2);
2145        assert_eq!(m.ppu_read(0x0000), 8);
2146        assert_eq!(m.ppu_read(0x1000), 8);
2147        assert_eq!(m.ppu_read_sprite(0x0000), 4);
2148    }
2149
2150    #[test]
2151    fn chr_mode_2_indexes_2k_banks() {
2152        let mut m = fresh(8, 32);
2153        m.cpu_write(0x5101, 2);
2154        m.cpu_write(0x5120, 15); // not used in 2 KiB mode
2155        m.cpu_write(0x5121, 1); // $0000-$07FF -> 1 KiB 2
2156        m.cpu_write(0x5123, 3); // $0800-$0FFF -> 1 KiB 6
2157        m.cpu_write(0x5125, 5); // $1000-$17FF -> 1 KiB 10
2158        m.cpu_write(0x5127, 7); // $1800-$1FFF -> 1 KiB 14
2159        assert_eq!(m.ppu_read(0x0000), 2);
2160        assert_eq!(m.ppu_read(0x0400), 3);
2161        assert_eq!(m.ppu_read(0x0800), 6);
2162        assert_eq!(m.ppu_read(0x1000), 10);
2163        assert_eq!(m.ppu_read_sprite(0x1800), 14);
2164        // B set: `$5129` for $0000-$07FF and $1000-$17FF, `$512B` for the
2165        // other two; `$5128` / `$512A` are unused.
2166        rendering_8x16(&mut m);
2167        m.cpu_write(0x5128, 15);
2168        m.cpu_write(0x5129, 4);
2169        m.cpu_write(0x512A, 15);
2170        m.cpu_write(0x512B, 9);
2171        assert_eq!(m.ppu_read(0x0000), 8);
2172        assert_eq!(m.ppu_read(0x0800), 18);
2173        assert_eq!(m.ppu_read(0x1000), 8);
2174        assert_eq!(m.ppu_read(0x1800), 18);
2175    }
2176
2177    #[test]
2178    fn chr_mode_3_eight_1k_banks() {
2179        let mut m = fresh(8, 8);
2180        m.cpu_write(0x5101, 3);
2181        for i in 0..8u8 {
2182            m.cpu_write(0x5120 + u16::from(i), 7 - i);
2183        }
2184        assert_eq!(m.ppu_read(0x0000), 7);
2185        assert_eq!(m.ppu_read(0x1C00), 0);
2186        // B set: four registers, the second 4 KiB repeating the first.
2187        rendering_8x16(&mut m);
2188        m.cpu_write(0x5128, 1);
2189        m.cpu_write(0x5129, 3);
2190        m.cpu_write(0x512A, 5);
2191        m.cpu_write(0x512B, 7);
2192        assert_eq!(m.ppu_read(0x0000), 1);
2193        assert_eq!(m.ppu_read(0x0400), 3);
2194        assert_eq!(m.ppu_read(0x0800), 5);
2195        assert_eq!(m.ppu_read(0x0C00), 7);
2196        assert_eq!(m.ppu_read(0x1000), 1);
2197        assert_eq!(m.ppu_read(0x1C00), 7);
2198    }
2199
2200    #[test]
2201    fn eight_by_eight_mode_ignores_the_b_set() {
2202        // "When using 8x8 sprites, only registers $5120-$5127 are used.
2203        // Registers $5128-$512B are completely ignored." That holds while
2204        // rendering as well as for `$2007`.
2205        let mut m = fresh(8, 16);
2206        m.cpu_write(0x5101, 3);
2207        for i in 0..8u8 {
2208            m.cpu_write(0x5120 + u16::from(i), 8 + i);
2209        }
2210        for i in 0..4u8 {
2211            m.cpu_write(0x5128 + u16::from(i), i);
2212        }
2213        m.notify_ppu_register_write(0x2000, 0x00);
2214        m.notify_ppu_register_write(0x2001, 0x18);
2215        m.notify_scanline_start();
2216        assert_eq!(m.ppu_read(0x0000), 8);
2217        assert_eq!(m.ppu_read(0x1C00), 15);
2218        assert_eq!(m.ppu_read_sprite(0x1000), 12);
2219        m.notify_vblank();
2220        assert_eq!(m.ppu_read(0x0400), 9);
2221    }
2222
2223    #[test]
2224    fn data_port_in_8x16_mode_uses_the_last_written_set() {
2225        let mut m = fresh(8, 16);
2226        m.cpu_write(0x5101, 3);
2227        m.notify_ppu_register_write(0x2000, 0x20);
2228        m.cpu_write(0x5120, 4); // A
2229        m.cpu_write(0x5128, 2); // B, written last
2230        assert_eq!(m.ppu_read(0x0000), 2);
2231        m.cpu_write(0x5120, 5); // A, written last
2232        assert_eq!(m.ppu_read(0x0000), 5);
2233        m.cpu_write(0x5128, 3);
2234        assert_eq!(m.ppu_read(0x0000), 3);
2235        // Rendering fetches use the B set regardless; `$2007` outside the
2236        // frame goes back to the set written last.
2237        m.cpu_write(0x5120, 6);
2238        rendering_8x16(&mut m);
2239        assert_eq!(m.ppu_read(0x0000), 3);
2240        m.notify_vblank();
2241        assert_eq!(m.ppu_read(0x0000), 6);
2242    }
2243
2244    #[test]
2245    fn forced_blank_mid_frame_returns_the_data_port_to_the_last_written_set() {
2246        // "The 'In Frame' flag is cleared when the PPU is no longer rendering"
2247        // (3 CPU cycles without a PPU read). The model has no read counter;
2248        // the snooped `$2001` enables stand in for it, so a `$2007` access
2249        // after `$2001 = 0` mid-frame is not taken for a background fetch.
2250        let mut m = fresh(8, 16);
2251        m.cpu_write(0x5101, 3);
2252        m.cpu_write(0x5128, 2);
2253        m.cpu_write(0x5120, 4); // A written last
2254        rendering_8x16(&mut m);
2255        assert_eq!(m.ppu_read(0x0000), 2);
2256        m.notify_ppu_register_write(0x2001, 0x00);
2257        assert_eq!(m.ppu_read(0x0000), 4);
2258    }
2259
2260    #[test]
2261    fn selecting_8x8_resets_the_last_written_set_to_a() {
2262        // loopy's hardware result: "Switching back to 8x16, it still uses
2263        // $5120-27 (until 5128-2B is written again)".
2264        let mut m = fresh(8, 16);
2265        m.cpu_write(0x5101, 3);
2266        m.notify_ppu_register_write(0x2000, 0x20);
2267        m.cpu_write(0x5120, 4);
2268        m.cpu_write(0x5128, 2);
2269        assert_eq!(m.ppu_read(0x0000), 2);
2270        m.notify_ppu_register_write(0x2000, 0x00);
2271        m.notify_ppu_register_write(0x2000, 0x20);
2272        assert_eq!(m.ppu_read(0x0000), 4);
2273    }
2274
2275    #[test]
2276    fn extended_attributes_in_8x16_mode_read_the_a_set() {
2277        // Sour's result: with 8x16 sprites and extended attributes, "$2007
2278        // reads always use $5120-5127, no matter which register was last
2279        // written to".
2280        let mut m = fresh(8, 16);
2281        m.cpu_write(0x5101, 3);
2282        m.cpu_write(0x5104, 1);
2283        m.notify_ppu_register_write(0x2000, 0x20);
2284        m.cpu_write(0x5120, 4);
2285        m.cpu_write(0x5128, 2);
2286        assert_eq!(m.ppu_read(0x0000), 4);
2287    }
2288
2289    #[test]
2290    fn only_the_exact_ppu_register_addresses_are_snooped() {
2291        // The MMC5 decodes `$2000` / `$2001` fully: a mirror write reaches the
2292        // PPU but not the MMC5 ("A game could write to a mirror of a PPU
2293        // register to get the MMC5 out of sync").
2294        let mut m = fresh(8, 16);
2295        m.cpu_write(0x5101, 3);
2296        m.cpu_write(0x5120, 4);
2297        m.cpu_write(0x5128, 2);
2298        m.notify_ppu_register_write(0x2008, 0x20);
2299        m.notify_ppu_register_write(0x2009, 0x18);
2300        m.notify_scanline_start();
2301        assert_eq!(m.ppu_read(0x0000), 4);
2302    }
2303
2304    #[test]
2305    fn eight_by_sixteen_sprites_use_the_a_set_and_background_the_b_set() {
2306        let mut m = fresh(8, 8);
2307        m.cpu_write(0x5101, 3);
2308        for i in 0..8u8 {
2309            m.cpu_write(0x5120 + u16::from(i), (i + 2) & 0x07);
2310        }
2311        for i in 0..4u8 {
2312            m.cpu_write(0x5128 + u16::from(i), 0);
2313        }
2314        rendering_8x16(&mut m);
2315        assert_eq!(m.ppu_read(0x0000), 0);
2316        assert_eq!(m.ppu_read_sprite(0x0000), 2);
2317        assert_eq!(m.ppu_read_sprite(0x0400), 3);
2318        assert_eq!(m.ppu_read_sprite(0x1C00), 1);
2319    }
2320
2321    #[test]
2322    fn ppu_snoop_survives_a_save_state_round_trip() {
2323        let mut m = fresh(8, 16);
2324        m.cpu_write(0x5101, 3);
2325        m.cpu_write(0x5120, 4);
2326        m.cpu_write(0x5128, 2);
2327        m.notify_ppu_register_write(0x2000, 0x20);
2328        m.notify_ppu_register_write(0x2001, 0x08);
2329        let state = m.save_state();
2330        let mut other = fresh(8, 16);
2331        other.load_state(&state).unwrap();
2332        assert!(other.ppu_sprites_8x16);
2333        assert!(other.ppu_rendering);
2334        assert!(!other.last_chr_write_was_sprite);
2335        assert_eq!(other.ppu_read(0x0000), 2);
2336    }
2337
2338    #[test]
2339    fn nametable_mapping_routes_per_1kib() {
2340        let mut m = fresh(8, 8);
2341        // 0b00_01_10_11 -> NT0=A, NT1=B, NT2=ExRAM, NT3=Fill.
2342        // Note: low 2 bits = NT0, etc.
2343        m.cpu_write(0x5105, 0b11_10_01_00);
2344        assert_eq!(m.nt_source(0), NtSource::CiramA);
2345        assert_eq!(m.nt_source(1), NtSource::CiramB);
2346        assert_eq!(m.nt_source(2), NtSource::ExRam);
2347        assert_eq!(m.nt_source(3), NtSource::Fill);
2348    }
2349
2350    #[test]
2351    fn exram_mode_10_general_ram_readback() {
2352        let mut m = fresh(8, 8);
2353        m.cpu_write(0x5104, 0b10);
2354        m.cpu_write(0x5C00, 0xAB);
2355        m.cpu_write(0x5DEF, 0xCD);
2356        assert_eq!(m.cpu_read(0x5C00), 0xAB);
2357        assert_eq!(m.cpu_read(0x5DEF), 0xCD);
2358    }
2359
2360    #[test]
2361    fn exram_mode_11_is_read_only() {
2362        let mut m = fresh(8, 8);
2363        // First populate via mode 10.
2364        m.cpu_write(0x5104, 0b10);
2365        m.cpu_write(0x5C00, 0x42);
2366        // Switch to read-only.
2367        m.cpu_write(0x5104, 0b11);
2368        m.cpu_write(0x5C00, 0xFF); // ignored
2369        assert_eq!(m.cpu_read(0x5C00), 0x42);
2370    }
2371
2372    #[test]
2373    fn exram_mode_00_used_as_nametable_via_5105() {
2374        let mut m = fresh(8, 8);
2375        // ExRAM mode = 00 (extra nametable).
2376        m.cpu_write(0x5104, 0b00);
2377        // Map NT0 to ExRAM.
2378        m.cpu_write(0x5105, 0b11_10_01_10); // NT0 = ExRAM (10)
2379        // Write through PPU bus to $2000 (NT0).
2380        m.ppu_write(0x2000, 0xAA);
2381        assert_eq!(m.ppu_read(0x2000), 0xAA);
2382        // ExRAM should reflect this too via CPU read.
2383        // Need to be in mode 10 to read CPU side cleanly — we left
2384        // ExRAM mode = 00, but CPU reads from $5C00-$5FFF still work.
2385        assert_eq!(m.cpu_read(0x5C00), 0xAA);
2386    }
2387
2388    #[test]
2389    fn prg_ram_protect_pair_must_be_unlocked() {
2390        let mut m = fresh(8, 8);
2391        // Default lock state -> writes to $6000 are dropped.
2392        m.cpu_write(0x6000, 0x11);
2393        assert_eq!(m.cpu_read(0x6000), 0x00);
2394        // Unlock: $5102 = 0x02, $5103 = 0x01.
2395        m.cpu_write(0x5102, 0x02);
2396        m.cpu_write(0x5103, 0x01);
2397        m.cpu_write(0x6000, 0x22);
2398        assert_eq!(m.cpu_read(0x6000), 0x22);
2399        // Re-lock by writing the wrong value to $5102.
2400        m.cpu_write(0x5102, 0x00);
2401        m.cpu_write(0x6000, 0x33);
2402        // Stays at 0x22.
2403        assert_eq!(m.cpu_read(0x6000), 0x22);
2404    }
2405
2406    #[test]
2407    fn multiplier_returns_8x8_to_16_product() {
2408        let mut m = fresh(8, 8);
2409        m.cpu_write(0x5205, 0x10);
2410        m.cpu_write(0x5206, 0x20);
2411        // 0x10 * 0x20 = 0x200; low byte = 0x00; high byte = 0x02.
2412        assert_eq!(m.cpu_read(0x5205), 0x00);
2413        assert_eq!(m.cpu_read(0x5206), 0x02);
2414
2415        m.cpu_write(0x5205, 0xFF);
2416        m.cpu_write(0x5206, 0xFF);
2417        // 0xFE01.
2418        assert_eq!(m.cpu_read(0x5205), 0x01);
2419        assert_eq!(m.cpu_read(0x5206), 0xFE);
2420    }
2421
2422    #[test]
2423    fn scanline_irq_enters_in_frame_and_increments() {
2424        let mut m = fresh(8, 8);
2425        // Compare value = 3, IRQ enabled.
2426        m.cpu_write(0x5203, 3);
2427        m.cpu_write(0x5204, 0x80);
2428        // Pretend the PPU starts scanlines.
2429        // First call -> in_frame=true, counter=0.
2430        m.notify_scanline_start();
2431        assert!(m.in_frame);
2432        assert_eq!(m.scanline_counter, 0);
2433        // Three more -> counter=3 -> match -> irq_pending.
2434        m.notify_scanline_start();
2435        m.notify_scanline_start();
2436        m.notify_scanline_start();
2437        assert_eq!(m.scanline_counter, 3);
2438        assert!(m.irq_pending);
2439        assert!(m.irq_pending());
2440    }
2441
2442    #[test]
2443    fn vblank_clears_in_frame_flag() {
2444        let mut m = fresh(8, 8);
2445        m.notify_scanline_start();
2446        assert!(m.in_frame);
2447        m.notify_vblank();
2448        assert!(!m.in_frame);
2449        // Next scanline_start re-enters in_frame and resets counter.
2450        m.notify_scanline_start();
2451        assert!(m.in_frame);
2452        assert_eq!(m.scanline_counter, 0);
2453    }
2454
2455    #[test]
2456    fn reading_5204_acks_pending_and_returns_status() {
2457        let mut m = fresh(8, 8);
2458        m.cpu_write(0x5203, 1);
2459        m.cpu_write(0x5204, 0x80);
2460        m.notify_scanline_start(); // counter=0; not match
2461        m.notify_scanline_start(); // counter=1; match
2462        assert!(m.irq_pending);
2463        let v = m.cpu_read(0x5204);
2464        assert!((v & 0x80) != 0); // status bit was set
2465        // Read should clear the pending latch.
2466        assert!(!m.irq_pending);
2467        assert!(!m.irq_pending());
2468    }
2469
2470    #[test]
2471    fn irq_disabled_no_assert_to_cpu() {
2472        let mut m = fresh(8, 8);
2473        m.cpu_write(0x5203, 1);
2474        // Don't write $5204 enable bit.
2475        m.notify_scanline_start();
2476        m.notify_scanline_start();
2477        // The internal latch may set, but irq_pending() (CPU-visible) is gated.
2478        assert!(!m.irq_pending());
2479    }
2480
2481    // ------------------------------------------------------------------
2482    // Feature tests: fill mode, dual CHR for sprites, ExGrafix.
2483    // ------------------------------------------------------------------
2484
2485    #[test]
2486    fn fill_mode_returns_fill_tile_in_nametable_region() {
2487        let mut m = fresh(8, 8);
2488        // Set NT0 -> Fill (0b11 in low 2 bits of $5105).
2489        m.cpu_write(0x5105, 0x03);
2490        m.cpu_write(0x5106, 0xAB);
2491        m.cpu_write(0x5107, 0x02);
2492        // Within the 32x30 NT byte region (offsets 0..0x3C0) -> fill tile.
2493        assert_eq!(m.nametable_fetch(0x2000), Some(0xAB));
2494        assert_eq!(m.nametable_fetch(0x2200), Some(0xAB));
2495        // Within the AT region (offset 0x3C0..0x400) -> 4-way replicated
2496        // 2-bit fill attr. attr=2 -> 0b10101010 = 0xAA.
2497        assert_eq!(m.nametable_fetch(0x23C0), Some(0xAA));
2498        assert_eq!(m.nametable_fetch(0x23FF), Some(0xAA));
2499    }
2500
2501    #[test]
2502    fn fill_mode_writes_are_dropped() {
2503        let mut m = fresh(8, 8);
2504        m.cpu_write(0x5105, 0x03); // NT0 -> Fill
2505        m.cpu_write(0x5106, 0x11);
2506        // Writes via the nametable hook are absorbed and dropped.
2507        let consumed = m.nametable_write(0x2000, 0xFF);
2508        assert!(consumed);
2509        // Read still returns the fill tile.
2510        assert_eq!(m.nametable_fetch(0x2000), Some(0x11));
2511    }
2512
2513    #[test]
2514    fn fill_mode_only_affects_selected_nametables() {
2515        let mut m = fresh(8, 8);
2516        // NT0 = Fill, NT1 = CIRAM_A, NT2 = CIRAM_B, NT3 = Fill.
2517        m.cpu_write(0x5105, 0b11_01_00_11);
2518        m.cpu_write(0x5106, 0x55);
2519        // NT1 and NT2 do not synthesize.
2520        assert_eq!(m.nametable_fetch(0x2400), None);
2521        assert_eq!(m.nametable_fetch(0x2800), None);
2522        // NT0 + NT3 do.
2523        assert_eq!(m.nametable_fetch(0x2000), Some(0x55));
2524        assert_eq!(m.nametable_fetch(0x2C00), Some(0x55));
2525    }
2526
2527    #[test]
2528    fn exram_nametable_fetch_returns_exram_byte() {
2529        let mut m = fresh(8, 8);
2530        // ExRAM mode 00 + NT0 -> ExRAM.
2531        m.cpu_write(0x5104, 0b00);
2532        m.cpu_write(0x5105, 0b11_10_01_10);
2533        // Stash a value in ExRAM.
2534        m.exram[0x10] = 0x77;
2535        assert_eq!(m.nametable_fetch(0x2010), Some(0x77));
2536    }
2537
2538    #[test]
2539    fn exram_nametable_write_routes_into_exram() {
2540        let mut m = fresh(8, 8);
2541        m.cpu_write(0x5104, 0b00);
2542        m.cpu_write(0x5105, 0b00_00_00_10); // NT0 = ExRAM
2543        let consumed = m.nametable_write(0x2042, 0x33);
2544        assert!(consumed);
2545        assert_eq!(m.exram[0x42], 0x33);
2546    }
2547
2548    #[test]
2549    fn ciram_nametable_writes_pass_through() {
2550        let mut m = fresh(8, 8);
2551        // Default $5105 = 0 -> all NT_A.
2552        // Writes should NOT be absorbed; the PPU is responsible for CIRAM.
2553        let consumed = m.nametable_write(0x2000, 0xCC);
2554        assert!(!consumed);
2555    }
2556
2557    #[test]
2558    fn ex_attribute_returns_none_outside_mode_01() {
2559        let mut m = fresh(8, 8);
2560        m.cpu_write(0x5104, 0b00); // not ExGrafix
2561        assert_eq!(m.peek_ex_attribute(0), None);
2562        m.cpu_write(0x5104, 0b10);
2563        assert_eq!(m.peek_ex_attribute(0), None);
2564    }
2565
2566    #[test]
2567    fn ex_attribute_decodes_palette_and_chr_bank_in_mode_01() {
2568        let mut m = fresh(8, 8);
2569        m.cpu_write(0x5104, 0b01); // ExGrafix
2570        // ExRAM byte for tile (coarse_x=2, coarse_y=3): index 3*32+2 = 98.
2571        m.exram[98] = 0b11_001010; // palette = 3, bank low = 0x0A
2572        // v: low 5 = coarse_x = 2; bits 5..9 = coarse_y = 3.
2573        let v = (3u16 << 5) | 2;
2574        let ex = m.peek_ex_attribute(v).unwrap();
2575        assert_eq!(ex.palette, 3);
2576        assert_eq!(ex.chr_bank, 0x0A);
2577        // The latch is now stored on the mapper.
2578        assert_eq!(m.ex_chr_bank_latch, Some(0x0A));
2579    }
2580
2581    #[test]
2582    fn ex_attribute_chr_override_routes_chr_fetch() {
2583        // CHR with 16 banks of 1K (4 banks of 4K).
2584        let mut m = fresh(8, 16);
2585        // Mark each bank's first byte uniquely (already done by synth_chr).
2586        m.cpu_write(0x5104, 0b01); // ExGrafix
2587        // Tile at coarse (0, 0): ExRAM[0] -> palette=0, bank low6=2 (4K bank 2).
2588        m.exram[0] = 0b00_000010;
2589        let v = 0u16;
2590        let _ = m.peek_ex_attribute(v); // sets ex_chr_bank_latch = Some(2)
2591        // BG fetch at $0000 -> chr_offset uses 4K bank 2 -> 1K bank 8 -> bank index 8.
2592        assert_eq!(m.ppu_read(0x0000), 8);
2593        // BG fetch at $0400 (1K offset 1024 within the 4K bank) -> 1K bank 9.
2594        assert_eq!(m.ppu_read(0x0400), 9);
2595    }
2596
2597    #[test]
2598    fn ex_attribute_clears_latch_when_leaving_mode() {
2599        let mut m = fresh(8, 16);
2600        m.cpu_write(0x5104, 0b01);
2601        m.exram[0] = 0b00_000011;
2602        let _ = m.peek_ex_attribute(0);
2603        assert_eq!(m.ex_chr_bank_latch, Some(3));
2604        // Leave mode -> next peek returns None and clears the latch.
2605        m.cpu_write(0x5104, 0b00);
2606        assert_eq!(m.ex_chr_bank_latch, None);
2607        assert_eq!(m.peek_ex_attribute(0), None);
2608    }
2609
2610    #[test]
2611    fn save_load_round_trip() {
2612        let mut m = fresh(8, 8);
2613        m.cpu_write(0x5100, 2);
2614        m.cpu_write(0x5114, 0x80 | 1);
2615        m.cpu_write(0x5115, 0x80 | 3);
2616        m.cpu_write(0x5116, 0x80 | 5);
2617        m.cpu_write(0x5117, 0x80 | 7);
2618        m.cpu_write(0x5101, 1);
2619        m.cpu_write(0x5128, 1);
2620        m.cpu_write(0x5129, 2);
2621        m.cpu_write(0x512A, 3);
2622        m.cpu_write(0x512B, 4);
2623        m.cpu_write(0x5104, 0b10);
2624        m.cpu_write(0x5C00, 0xDE);
2625        m.cpu_write(0x5203, 42);
2626        m.cpu_write(0x5204, 0x80);
2627        m.cpu_write(0x5205, 0x07);
2628        m.cpu_write(0x5206, 0x09);
2629
2630        let blob = m.save_state();
2631        let mut other = fresh(8, 8);
2632        other.load_state(&blob).unwrap();
2633
2634        assert_eq!(other.prg_mode, m.prg_mode);
2635        assert_eq!(other.chr_mode, m.chr_mode);
2636        assert_eq!(other.bg_chr_banks, m.bg_chr_banks);
2637        for i in 0..4 {
2638            assert_eq!(other.prg_banks[i].raw, m.prg_banks[i].raw);
2639        }
2640        assert_eq!(other.exram_mode, m.exram_mode);
2641        assert_eq!(other.exram[0x000], 0xDE);
2642        assert_eq!(other.irq_compare, 42);
2643        assert!(other.irq_enabled);
2644        assert_eq!(other.mul_a, 0x07);
2645        assert_eq!(other.mul_b, 0x09);
2646        // Multiplier readback.
2647        assert_eq!(other.cpu_read(0x5205), 0x3F);
2648        assert_eq!(other.cpu_read(0x5206), 0x00);
2649    }
2650
2651    // ------------------------------------------------------------------
2652    // Vertical split-screen ($5200-$5202).
2653    // ------------------------------------------------------------------
2654
2655    #[test]
2656    fn split_registers_default_to_disabled() {
2657        let mut m = fresh(8, 8);
2658        assert!(!m.split_enable);
2659        assert_eq!(m.split_tile, 0);
2660        assert_eq!(m.split_v_scroll, 0);
2661        assert_eq!(m.split_chr_bank, 0);
2662        // bg_split_state returns None when disabled.
2663        assert_eq!(m.bg_split_state(0, 0), None);
2664        assert_eq!(m.bg_split_state(100, 15), None);
2665    }
2666
2667    #[test]
2668    fn split_5200_decodes_enable_side_and_tile() {
2669        let mut m = fresh(8, 8);
2670        // Enable, left side (bit 6 = 0), split column 10.
2671        m.cpu_write(0x5200, 0x80 | 0x0A);
2672        assert!(m.split_enable);
2673        assert!(!m.split_side_right);
2674        assert_eq!(m.split_tile, 0x0A);
2675        // Enable, right side (bit 6 = 1), split column 16.
2676        m.cpu_write(0x5200, 0x80 | 0x40 | 0x10);
2677        assert!(m.split_enable);
2678        assert!(m.split_side_right);
2679        assert_eq!(m.split_tile, 0x10);
2680        // Disable (bit 7 = 0).
2681        m.cpu_write(0x5200, 0x00);
2682        assert!(!m.split_enable);
2683    }
2684
2685    #[test]
2686    fn split_5201_and_5202_store_raw_values() {
2687        let mut m = fresh(8, 8);
2688        m.cpu_write(0x5201, 0xC8); // = 200
2689        m.cpu_write(0x5202, 0x03);
2690        assert_eq!(m.split_v_scroll, 0xC8);
2691        assert_eq!(m.split_chr_bank, 0x03);
2692    }
2693
2694    #[test]
2695    fn split_left_side_alt_region_is_columns_below_split_tile() {
2696        let mut m = fresh(8, 8);
2697        // Enable + left side + split at tile 10.
2698        m.cpu_write(0x5200, 0x80 | 0x0A);
2699        m.cpu_write(0x5201, 0); // no v-scroll
2700        m.cpu_write(0x5202, 5);
2701        // Columns 0..=9 are alt; 10..=31 are main.
2702        assert!(m.bg_split_state(0, 0).is_some());
2703        assert!(m.bg_split_state(0, 9).is_some());
2704        assert!(m.bg_split_state(0, 10).is_none());
2705        assert!(m.bg_split_state(0, 31).is_none());
2706    }
2707
2708    #[test]
2709    fn split_right_side_alt_region_is_columns_at_or_above_split_tile() {
2710        let mut m = fresh(8, 8);
2711        // Enable + right side + split at tile 10.
2712        m.cpu_write(0x5200, 0x80 | 0x40 | 0x0A);
2713        // Columns 10..=31 are alt; 0..=9 are main.
2714        assert!(m.bg_split_state(0, 0).is_none());
2715        assert!(m.bg_split_state(0, 9).is_none());
2716        assert!(m.bg_split_state(0, 10).is_some());
2717        assert!(m.bg_split_state(0, 31).is_some());
2718    }
2719
2720    #[test]
2721    fn split_state_supplies_nt_at_addresses_and_fine_y() {
2722        let mut m = fresh(8, 8);
2723        // Enable + left side + split at tile 16.
2724        m.cpu_write(0x5200, 0x80 | 16);
2725        // V-scroll = 16 -> first alt scanline 0 lands at logical row 16,
2726        // i.e. coarse_y = 2, fine_y = 0.
2727        m.cpu_write(0x5201, 16);
2728        m.cpu_write(0x5202, 7);
2729        // Coarse-X = 5, scanline = 0.
2730        let s = m.bg_split_state(0, 5).unwrap();
2731        assert_eq!(s.chr_bank, 7);
2732        assert_eq!(s.fine_y, 0);
2733        // NT addr: NT0 + coarse_y=2, coarse_x=5 -> $2000 | (2 << 5) | 5 = $2045.
2734        assert_eq!(s.nt_addr, 0x2045);
2735        // AT addr: $23C0 | ((2 >> 2) << 3) | (5 >> 2) = $23C0 | 0 | 1 = $23C1.
2736        assert_eq!(s.at_addr, 0x23C1);
2737    }
2738
2739    #[test]
2740    fn split_v_scroll_wraps_at_240() {
2741        let mut m = fresh(8, 8);
2742        m.cpu_write(0x5200, 0x80); // enable, left, split = 0 (alt covers cols < 0 = none!)
2743        // Use split tile 31 so cx=0 is in alt.
2744        m.cpu_write(0x5200, 0x80 | 31);
2745        m.cpu_write(0x5201, 230);
2746        m.cpu_write(0x5202, 0);
2747        // Scanline 20 -> y_in_region = (230 + 20) % 240 = 10 -> coarse_y=1, fine_y=2.
2748        let s = m.bg_split_state(20, 0).unwrap();
2749        assert_eq!(s.fine_y, 2);
2750        // coarse_y = 1 -> NT addr = $2000 | (1 << 5) | 0 = $2020.
2751        assert_eq!(s.nt_addr, 0x2020);
2752    }
2753
2754    #[test]
2755    fn split_state_latches_chr_bank_for_subsequent_bg_fetch() {
2756        let mut m = fresh(8, 16);
2757        // ExGrafix off; split on, left, split=16, bank=2 (4 KiB).
2758        m.cpu_write(0x5200, 0x80 | 16);
2759        m.cpu_write(0x5202, 2);
2760        // Trigger split state at coarse_x=0 (alt region).
2761        let _ = m.bg_split_state(0, 0).unwrap();
2762        assert_eq!(m.split_chr_bank_latch, Some(2));
2763        // BG fetch at $0000 -> 4 KiB bank 2 -> 1 KiB bank 8 -> first byte = 8.
2764        assert_eq!(m.ppu_read(0x0000), 8);
2765        // BG fetch at $0400 -> 1 KiB bank 9.
2766        assert_eq!(m.ppu_read(0x0400), 9);
2767    }
2768
2769    #[test]
2770    fn split_state_clears_latch_outside_alt_region() {
2771        let mut m = fresh(8, 16);
2772        m.cpu_write(0x5200, 0x80 | 16); // left, split=16
2773        m.cpu_write(0x5202, 2);
2774        let _ = m.bg_split_state(0, 0); // alt
2775        assert!(m.split_chr_bank_latch.is_some());
2776        // Now a tile outside the alt region clears the latch.
2777        let _ = m.bg_split_state(0, 20);
2778        assert_eq!(m.split_chr_bank_latch, None);
2779    }
2780
2781    #[test]
2782    fn split_disable_drops_latch_and_returns_none() {
2783        let mut m = fresh(8, 8);
2784        m.cpu_write(0x5200, 0x80 | 16);
2785        m.cpu_write(0x5202, 1);
2786        let _ = m.bg_split_state(0, 0);
2787        assert!(m.split_chr_bank_latch.is_some());
2788        // Disable.
2789        m.cpu_write(0x5200, 0x00);
2790        assert!(!m.split_enable);
2791        let s = m.bg_split_state(0, 0);
2792        assert!(s.is_none());
2793        assert_eq!(m.split_chr_bank_latch, None);
2794    }
2795
2796    #[test]
2797    fn split_nametable_fetch_reads_exram_when_active() {
2798        let mut m = fresh(8, 8);
2799        m.cpu_write(0x5200, 0x80 | 31); // alt covers cols 0..=30
2800        m.cpu_write(0x5201, 16); // v-scroll 16 -> first alt scanline at row 16
2801        m.cpu_write(0x5202, 0);
2802        // Activate split for the tile at cx=5, scanline=0 (alt region).
2803        // y_in_region = (16 + 0) % 240 = 16; coarse_y = 2; NT addr =
2804        // $2000 | (2 << 5) | 5 = $2045 -> ExRAM index 0x45.
2805        let s = m.bg_split_state(0, 5).unwrap();
2806        assert_eq!(s.nt_addr, 0x2045);
2807        // Stash a byte at the ExRAM index the synthesized NT addr maps to.
2808        m.exram[0x45] = 0xC3;
2809        // The PPU would now call nametable_fetch with $2045; we should get
2810        // 0xC3 from ExRAM regardless of $5105.
2811        assert_eq!(m.nametable_fetch(0x2045), Some(0xC3));
2812    }
2813
2814    #[test]
2815    fn split_nametable_fetch_falls_back_to_normal_when_inactive() {
2816        let mut m = fresh(8, 8);
2817        // No split activation -> nametable_fetch follows $5105.
2818        // Default $5105=0 -> all NT_A -> nametable_fetch returns None.
2819        assert!(m.split_chr_bank_latch.is_none());
2820        assert_eq!(m.nametable_fetch(0x2000), None);
2821    }
2822
2823    #[test]
2824    fn split_save_load_round_trip_v3() {
2825        let mut m = fresh(8, 8);
2826        m.cpu_write(0x5200, 0x80 | 0x40 | 0x0C); // enable, right, tile=12
2827        m.cpu_write(0x5201, 137);
2828        m.cpu_write(0x5202, 6);
2829        // Drive a split fetch so the latch is set.
2830        let _ = m.bg_split_state(0, 20);
2831        assert!(m.split_chr_bank_latch.is_some());
2832
2833        let blob = m.save_state();
2834        // Version byte should be the current SAVE_STATE_VERSION (v4 after
2835        // the Track C2 MMC5 audio landing; was v3 when this test landed).
2836        assert_eq!(blob[0], SAVE_STATE_VERSION);
2837        let mut other = fresh(8, 8);
2838        other.load_state(&blob).unwrap();
2839        assert!(other.split_enable);
2840        assert!(other.split_side_right);
2841        assert_eq!(other.split_tile, 12);
2842        assert_eq!(other.split_v_scroll, 137);
2843        assert_eq!(other.split_chr_bank, 6);
2844        assert_eq!(other.split_chr_bank_latch, Some(6));
2845    }
2846
2847    #[test]
2848    fn split_takes_precedence_over_exgrafix() {
2849        let mut m = fresh(8, 16);
2850        // ExGrafix mode + populate ExRAM[0] = bank 3.
2851        m.cpu_write(0x5104, 0b01);
2852        m.exram[0] = 0b00_000011;
2853        // Also enable split, left, tile=16, bank=2.
2854        m.cpu_write(0x5200, 0x80 | 16);
2855        m.cpu_write(0x5202, 2);
2856        // Activate split (cx=0).
2857        let _ = m.bg_split_state(0, 0).unwrap();
2858        // Split CHR latch should win over the (otherwise-applied) ExGrafix latch.
2859        assert!(m.split_chr_bank_latch.is_some());
2860        // BG fetch at $0000 -> 4 KiB bank 2 -> 1 KiB bank 8.
2861        assert_eq!(m.ppu_read(0x0000), 8);
2862    }
2863
2864    // -----------------------------------------------------------------
2865    // MMC5 audio extension (Track C2) — $5000-$5015.
2866    // -----------------------------------------------------------------
2867
2868    #[test]
2869    fn audio_5000_write_decodes_duty_volume_halt() {
2870        let mut m = fresh(8, 8);
2871        // 0x9F = 1001_1111 -> duty=2 (bits 6-7), halt=0 (bit 5)... wait,
2872        // bit 5 is 0 in 0x9F. The task description claims halt=set; that
2873        // matches 0xBF or 0xB5. We test the canonical decoder: bit 5 set
2874        // means halt+loop. Use 0xBF: 1011_1111 -> duty=2, halt=1, const=1,
2875        // vol=15.
2876        m.cpu_write(0x5000, 0xBF);
2877        assert_eq!(m.audio.pulse1.duty, 2);
2878        assert!(m.audio.pulse1.halt);
2879        assert!(m.audio.pulse1.envelope_constant);
2880        assert_eq!(m.audio.pulse1.envelope_volume_or_period, 15);
2881    }
2882
2883    #[test]
2884    fn audio_5003_loads_length_counter_via_lookup() {
2885        let mut m = fresh(8, 8);
2886        // Enable pulse 1 length counter (per $5015 contract).
2887        m.cpu_write(0x5015, 0x01);
2888        // value = (idx << 3) | timer_hi_3bits. idx 4 -> 40.
2889        m.cpu_write(0x5003, 4 << 3);
2890        assert_eq!(m.audio.pulse1.length, 40);
2891        // Disabled channel does NOT load length.
2892        m.cpu_write(0x5015, 0x00);
2893        m.cpu_write(0x5003, 5 << 3); // index 5 -> 4
2894        assert_eq!(m.audio.pulse1.length, 0);
2895    }
2896
2897    #[test]
2898    fn audio_5015_status_reflects_length_and_write_enables() {
2899        let mut m = fresh(8, 8);
2900        m.cpu_write(0x5015, 0x03);
2901        // Load lengths via $5003 / $5007.
2902        m.cpu_write(0x5003, 4 << 3);
2903        m.cpu_write(0x5007, 4 << 3);
2904        // Both pulses now have length > 0.
2905        let s = m.cpu_read(0x5015);
2906        assert_eq!(s & 0x03, 0x03);
2907        // Disable pulse 2 -> length cleared, status drops bit 1.
2908        m.cpu_write(0x5015, 0x01);
2909        let s = m.cpu_read(0x5015);
2910        assert_eq!(s & 0x03, 0x01);
2911    }
2912
2913    #[test]
2914    fn audio_timer_period_assembled_from_5002_5003() {
2915        let mut m = fresh(8, 8);
2916        m.cpu_write(0x5015, 0x01);
2917        m.cpu_write(0x5002, 0xAB);
2918        // $5003 low 3 bits = timer high, top 5 bits = length idx.
2919        m.cpu_write(0x5003, (3u8 << 3) | 0x05);
2920        // 11-bit assembled period = 0x5AB.
2921        assert_eq!(m.audio.pulse1.timer_period, 0x05AB);
2922    }
2923
2924    #[test]
2925    #[cfg(feature = "mapper-audio")]
2926    fn audio_pulse_muted_when_timer_period_below_8() {
2927        let mut m = fresh(8, 8);
2928        // Enable pulse 1, constant volume 15, no halt.
2929        m.cpu_write(0x5015, 0x01);
2930        m.cpu_write(0x5000, 0b0001_1111); // duty=0, halt=0, const=1, vol=15
2931        m.cpu_write(0x5002, 0x07); // period 7 (< 8) -> muted
2932        m.cpu_write(0x5003, 4 << 3); // length nonzero
2933        // Advance enough CPU cycles to let the duty sequencer step many
2934        // times. With period < 8 the muted-rule keeps output at 0.
2935        for _ in 0..64 {
2936            m.notify_cpu_cycle();
2937        }
2938        // mix_audio biases to -12290 baseline (no audio active). The
2939        // pulse-1 muted check applies regardless of duty step.
2940        assert!(m.audio.pulse1.muted());
2941        // With both pulses muted and pcm=0, the bias is the only term.
2942        assert_eq!(m.mix_audio(), -12290);
2943    }
2944
2945    #[test]
2946    #[cfg(feature = "mapper-audio")]
2947    fn audio_pulse_timer_advances_step_every_period_plus_one_cycles() {
2948        let mut m = fresh(8, 8);
2949        m.cpu_write(0x5015, 0x01);
2950        // Pulse 1 with period 2 (the smallest non-muted), duty 2 (50%),
2951        // const vol 15.
2952        m.cpu_write(0x5000, 0b1001_1111);
2953        m.cpu_write(0x5002, 0x08); // timer-lo 8 -> period 8 (>= 8 = not muted)
2954        m.cpu_write(0x5003, 4 << 3);
2955        // notify_cpu_cycle clocks the timer every OTHER cycle (APU rate).
2956        // After 18 CPU cycles we expect ~ (18 / 2) / (period+1) = 1 step
2957        // increment from step 0 -> step 1 (with some integer rounding).
2958        let start = m.audio.pulse1.step;
2959        for _ in 0..18 {
2960            m.notify_cpu_cycle();
2961        }
2962        let after = m.audio.pulse1.step;
2963        // At least one duty step should have advanced.
2964        assert_ne!(start, after);
2965    }
2966
2967    #[test]
2968    #[cfg(feature = "mapper-audio")]
2969    fn audio_5011_pcm_write_latches_sample_and_mix_reflects_it() {
2970        let mut m = fresh(8, 8);
2971        // PCM in write-mode (default, $5010 bit 0 = 0). Sample 64 -> mix
2972        // contribution 64 * 40 = 2560 above the bias.
2973        m.cpu_write(0x5010, 0x00);
2974        m.cpu_write(0x5011, 64);
2975        assert_eq!(m.audio.pcm_sample, 64);
2976        // Pulses are silent (no length loaded). Expected mix = pcm_mix - bias.
2977        let mix = m.mix_audio();
2978        assert_eq!(mix, 64 * 40 - 12290);
2979    }
2980
2981    #[test]
2982    #[cfg(feature = "mapper-audio")]
2983    fn audio_5010_read_mode_silences_pcm() {
2984        let mut m = fresh(8, 8);
2985        m.cpu_write(0x5011, 100); // would mix to 100*16 above bias
2986        let mix_write_mode = m.mix_audio();
2987        // Switch to read-mode: PCM output is silenced.
2988        m.cpu_write(0x5010, 0x01);
2989        let mix_read_mode = m.mix_audio();
2990        // In read-mode the PCM contribution drops to 0, so mix returns to
2991        // the bias only. The delta should equal the pre-switch PCM
2992        // contribution.
2993        assert_ne!(mix_write_mode, mix_read_mode);
2994        // v2.1.6: bias is now -12290 (hardware-accurate 650/40 level scale).
2995        assert_eq!(mix_read_mode, -12290);
2996        // Also: writes to $5011 in read-mode are dropped.
2997        m.cpu_write(0x5011, 50);
2998        assert_eq!(m.audio.pcm_sample, 100);
2999    }
3000
3001    #[test]
3002    fn audio_save_load_v3_blob_is_refused() {
3003        // A v3 blob (pre-audio MMC5 save) used to load with the audio
3004        // extension silenced. v2.9.8 reads the current layout only (ADR
3005        // 0042). Build one from a current blob by stripping the v5 superset
3006        // tail and the v4 audio tail and rewriting the version byte.
3007        let m = fresh(8, 8);
3008        let mut blob = m.save_state();
3009        let tail_len = m.prg_ram_extra.len() + 1 + Mmc5Audio::TAIL_LEN;
3010        blob.truncate(blob.len() - tail_len);
3011        blob[0] = 3;
3012        let mut other = fresh(8, 8);
3013        assert!(matches!(
3014            other.load_state(&blob),
3015            Err(MapperError::UnsupportedVersion(3))
3016        ));
3017    }
3018
3019    #[test]
3020    fn audio_save_load_v4_round_trip_preserves_audio_state() {
3021        let mut m = fresh(8, 8);
3022        m.cpu_write(0x5015, 0x03);
3023        m.cpu_write(0x5000, 0xBF); // pulse1 ctrl
3024        m.cpu_write(0x5002, 0xCD);
3025        m.cpu_write(0x5003, (5u8 << 3) | 0x07);
3026        m.cpu_write(0x5004, 0x95);
3027        m.cpu_write(0x5006, 0x42);
3028        m.cpu_write(0x5007, (2u8 << 3) | 0x01);
3029        m.cpu_write(0x5011, 0x42);
3030        let blob = m.save_state();
3031        // First byte = current version.
3032        assert_eq!(blob[0], SAVE_STATE_VERSION);
3033
3034        let mut other = fresh(8, 8);
3035        other.load_state(&blob).unwrap();
3036        assert_eq!(other.audio.pulse1.duty, 2);
3037        assert_eq!(other.audio.pulse1.timer_period, 0x7CD);
3038        assert_eq!(other.audio.pulse2.duty, 2);
3039        assert_eq!(other.audio.pulse2.timer_period, 0x142);
3040        assert_eq!(other.audio.pcm_sample, 0x42);
3041    }
3042
3043    #[test]
3044    #[cfg(not(feature = "mapper-audio"))]
3045    fn audio_feature_off_latches_state_but_mixes_silent() {
3046        // With the feature off, the register decoders still latch state
3047        // (so save-state round-trip stays compatible across feature-flag
3048        // builds), but the oscillators do not advance and mix_audio
3049        // returns 0.
3050        let mut m = fresh(8, 8);
3051        m.cpu_write(0x5015, 0x03);
3052        m.cpu_write(0x5000, 0xBF);
3053        m.cpu_write(0x5011, 0x7F);
3054        // State latched.
3055        assert_eq!(m.audio.pulse1.duty, 2);
3056        assert!(m.audio.pulse1.envelope_constant);
3057        assert_eq!(m.audio.pcm_sample, 0x7F);
3058        // mix_audio: the default impl returns 0 under the feature-off
3059        // build (we do not provide a `mix_audio` override).
3060        assert_eq!(m.mix_audio(), 0);
3061        // Timer / step does not advance even under many notify_cpu_cycle calls.
3062        let s = m.audio.pulse1.step;
3063        for _ in 0..16 {
3064            m.notify_cpu_cycle();
3065        }
3066        assert_eq!(m.audio.pulse1.step, s);
3067    }
3068
3069    #[test]
3070    #[cfg(feature = "mapper-audio")]
3071    fn audio_frame_event_quarter_clocks_envelope_half_clocks_length() {
3072        let mut m = fresh(8, 8);
3073        // Enable pulse 1; set non-constant volume, period >= 8, length on.
3074        m.cpu_write(0x5015, 0x01);
3075        m.cpu_write(0x5000, 0b0000_1111); // duty=0, halt=0, const=0, period=15
3076        m.cpu_write(0x5002, 0x10);
3077        m.cpu_write(0x5003, 5u8 << 3); // length idx 5 -> 4
3078        let initial_length = m.audio.pulse1.length;
3079        assert!(initial_length > 0);
3080
3081        // Quarter-frame -> envelope clock.
3082        m.notify_frame_event(MapperFrameEvents {
3083            quarter: true,
3084            half: false,
3085        });
3086        // After a single quarter, the envelope_start latch is cleared and
3087        // decay is primed at 15. Length unchanged.
3088        assert!(!m.audio.pulse1.envelope_start);
3089        assert_eq!(m.audio.pulse1.envelope_decay, 15);
3090        assert_eq!(m.audio.pulse1.length, initial_length);
3091
3092        // Half-frame -> length clock (decrements by 1).
3093        m.notify_frame_event(MapperFrameEvents {
3094            quarter: false,
3095            half: true,
3096        });
3097        assert_eq!(m.audio.pulse1.length, initial_length - 1);
3098    }
3099
3100    // ---- v2.7.2: PRG-RAM banks (core audit §5.3) ---------------------------
3101    //
3102    // nesdev_wiki/MMC5.xhtml §"PRG-RAM configurations": the RAM is paged by
3103    // the low three bits of the bank value, and "emulating the PRG-RAM as 64K
3104    // at all times can be used as a compatible superset for all games". The
3105    // battery save stays the header's declared part.
3106
3107    fn with_ram(bytes: usize) -> Mmc5 {
3108        let mut m = Mmc5::new(synth_prg(8), synth_chr(8), Mirroring::Vertical, bytes).unwrap();
3109        m.cpu_write(0x5102, 0x02); // unlock the protect pair
3110        m.cpu_write(0x5103, 0x01);
3111        m
3112    }
3113
3114    /// Write a distinct tag through `$6000` with each bank value 0-7, then
3115    /// report what each bank value reads back.
3116    fn bank_readback(m: &mut Mmc5) -> [u8; 8] {
3117        for b in 0..8u8 {
3118            m.cpu_write(0x5113, b);
3119            m.cpu_write(0x6000, 0xB0 | b);
3120        }
3121        let mut out = [0; 8];
3122        for b in 0..8u8 {
3123            m.cpu_write(0x5113, b);
3124            assert!(!m.cpu_read_unmapped(0x6000), "bank {b}: RAM always answers");
3125            out[usize::from(b)] = m.cpu_read(0x6000);
3126        }
3127        out
3128    }
3129
3130    #[test]
3131    fn every_declared_size_pages_all_eight_bank_values() {
3132        for bytes in [0x2000, 0x4000, 0x8000, 0x1_0000] {
3133            let mut m = with_ram(bytes);
3134            let r = bank_readback(&mut m);
3135            for b in 0..8u8 {
3136                assert_eq!(r[usize::from(b)], 0xB0 | b, "{bytes:#x} bytes, bank {b}");
3137            }
3138        }
3139    }
3140
3141    #[test]
3142    fn an_under_declared_etrom_keeps_its_work_ram_chip() {
3143        // L'Empereur is ETROM (2 x 8 KiB; wiki board table), but its NES 2.0
3144        // dump declares only the 8 KiB battery chip. It keeps work data in
3145        // bank values 4-7; the exact-table model floated those and the game
3146        // stopped at its logo.
3147        let mut m = with_ram(0x2000);
3148        m.cpu_write(0x5113, 4);
3149        m.cpu_write(0x6123, 0x42);
3150        m.cpu_write(0x5113, 0);
3151        m.cpu_write(0x6123, 0x17);
3152        m.cpu_write(0x5113, 4);
3153        assert_eq!(m.cpu_read(0x6123), 0x42, "bank 4 is its own RAM");
3154        assert_eq!(
3155            m.sram().len(),
3156            0x2000,
3157            "the save is still the declared 8 KiB"
3158        );
3159        assert_eq!(m.sram()[0x123], 0x17, "and it is bank 0's page");
3160    }
3161
3162    #[test]
3163    fn the_save_is_the_battery_backed_part_of_the_declared_ram() {
3164        // "Games with 16K PRG-RAM only battery-save the first 8K."
3165        assert_eq!(with_ram(0x4000).sram().len(), 0x2000);
3166        assert_eq!(with_ram(0x8000).sram().len(), 0x8000);
3167        assert_eq!(with_ram(0x1_0000).sram().len(), 0x1_0000);
3168    }
3169
3170    #[test]
3171    fn superset_pages_survive_a_save_state_and_old_blobs_are_refused() {
3172        let mut m = with_ram(0x2000);
3173        m.cpu_write(0x5113, 6);
3174        m.cpu_write(0x6000, 0x66);
3175        let blob = m.save_state();
3176        assert_eq!(blob[0], SAVE_STATE_VERSION);
3177        let mut n = with_ram(0x2000);
3178        n.load_state(&blob).unwrap();
3179        n.cpu_write(0x5113, 6);
3180        assert_eq!(n.cpu_read(0x6000), 0x66, "the superset page round-trips");
3181        // A v4 blob (no superset tail) is refused since v2.9.8 (ADR 0042); it
3182        // used to load with those pages zeroed.
3183        let mut v4 = blob.clone();
3184        v4.truncate(blob.len() - n.prg_ram_extra.len());
3185        v4[0] = 4;
3186        let mut o = with_ram(0x2000);
3187        assert!(matches!(
3188            o.load_state(&v4),
3189            Err(MapperError::UnsupportedVersion(4))
3190        ));
3191    }
3192
3193    #[test]
3194    fn prg_ram_banked_into_8000_uses_its_own_register() {
3195        // "Uncharted Waters ... writes to PRG-RAM at one CPU address and
3196        // expects to read the same data back via a different CPU address."
3197        let mut m = with_ram(0x8000);
3198        m.cpu_write(0x5100, 0x03); // PRG mode 3: 4 x 8 KiB
3199        m.cpu_write(0x5113, 0);
3200        m.cpu_write(0x6000, 0x11);
3201        m.cpu_write(0x5113, 2);
3202        m.cpu_write(0x6000, 0x5E);
3203        m.cpu_write(0x5114, 0x02); // $8000 = RAM page 2 (bit 7 clear)
3204        assert_eq!(m.cpu_read(0x8000), 0x5E, "same page seen through $8000");
3205        m.cpu_write(0x5114, 0x00);
3206        assert_eq!(m.cpu_read(0x8000), 0x11, "and page 0 is a different page");
3207        m.cpu_write(0x5114, 0x02);
3208        m.cpu_write(0x8001, 0x6F);
3209        assert_eq!(m.cpu_read(0x6001), 0x6F, "and written back through it");
3210    }
3211
3212    #[test]
3213    fn prg_ram_in_a_16k_window_takes_a13_from_the_cpu() {
3214        // In a 16 KiB window, register bit 0 is ignored and CPU A13 drives
3215        // PRG A13: value $02 at $8000-$BFFF shows page 2 then page 3.
3216        let mut m = with_ram(0x8000);
3217        m.cpu_write(0x5100, 0x01); // PRG mode 1: 16 K + 16 K
3218        m.cpu_write(0x5113, 2);
3219        m.cpu_write(0x6000, 0x22);
3220        m.cpu_write(0x5113, 3);
3221        m.cpu_write(0x6000, 0x33);
3222        m.cpu_write(0x5115, 0x03); // RAM, low bit ignored
3223        assert_eq!(m.cpu_read(0x8000), 0x22);
3224        assert_eq!(m.cpu_read(0xA000), 0x33);
3225    }
3226}