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}