Skip to main content

rustynes_mappers/
m019_namco163.rs

1// SPDX-License-Identifier: GPL-3.0-or-later
2//
3// Provenance: the Namco 163 output level (the `* 20` weight against the 2A03 pulse DAC behind `NAMCO163_MIX_SCALE`) is derived from Mesen2 (GPL-3.0-or-later), `NesSoundMixer::GetOutputVolume`. See docs/originality-and-provenance.md (Section 1) and NOTICE. Classified v2.9.9 (core re-audit NC-17, maintainer's decision 2026-10-04): the in-source citations below record the derivation and are kept as written.
4
5//! Namco 163 (mappers 19 and 210) -- banking, the CPU-cycle IRQ counter, and
6//! the on-cart Namco 163 wavetable synthesizer.
7//!
8//! The 163 carries 128 bytes of internal RAM that serve double duty: they
9//! hold the channel register file *and* the wavetable samples themselves,
10//! packed two 4-bit samples per byte. One to eight channels play from that
11//! shared RAM, time-multiplexed -- so enabling more channels does not make
12//! the cart louder, it divides the same output among more voices, which is
13//! why the mix divides by the active channel count.
14//!
15//! Audio is gated behind the `mapper-audio` Cargo feature (default ON); with
16//! it off the register decoders still latch (writes land in the internal RAM
17//! and the address-port auto-increment still advances) so save states remain
18//! portable across feature configurations (ADR 0004). [`Namco163Audio`] is
19//! re-used verbatim by the NSF expansion path (`nsf_expansion.rs`).
20//!
21//! [`NAMCO163_MIX_SCALE`] was recalibrated in v2.1.6 (the previous value was
22//! ~12 dB too quiet). See `docs/apu-2a03.md` §Expansion-audio levels.
23
24#![allow(
25    clippy::cast_possible_truncation,
26    clippy::cast_lossless,
27    clippy::missing_const_for_fn,
28    clippy::needless_pass_by_ref_mut,
29    clippy::manual_range_patterns,
30    clippy::match_same_arms,
31    clippy::struct_excessive_bools,
32    clippy::doc_markdown,
33    clippy::range_plus_one,
34    clippy::single_match_else,
35    clippy::bool_to_int_with_if,
36    clippy::unnested_or_patterns,
37    clippy::single_match,
38    clippy::doc_lazy_continuation,
39    clippy::too_long_first_doc_paragraph
40)]
41
42use crate::cartridge::Mirroring;
43use crate::mapper::{Mapper, MapperCaps, MapperError};
44use alloc::{boxed::Box, vec::Vec};
45use alloc::{format, vec};
46
47const PRG_BANK_8K: usize = 0x2000;
48const CHR_BANK_1K: usize = 0x0400;
49const CHR_BANK_8K: usize = 0x2000;
50const NAMETABLE_SIZE: usize = 0x0400;
51
52/// Version byte this board writes in its mapper save-state section.
53///
54/// **v1** carried the banking, mirroring and IRQ registers, the 8 KiB PRG-RAM
55/// and the 2 KiB CIRAM copy. **v2** appended the sound-disable bit and the
56/// wavetable audio tail. **v3** (v2.7.2) appended `chr_ram_disable` and
57/// `ciram_owned`. None of them carried the 8 KiB CHR-RAM of a cartridge with
58/// no CHR-ROM, and the `.rns` container has no other section that carries
59/// cartridge RAM -- so every save-state load, rewind step, run-ahead frame
60/// and netplay rollback kept the running game's CHR-RAM instead of the saved
61/// one (the v2.9.2 cartridge-RAM sweep; the same omission core audit AUD-02
62/// found on the Konami VRC boards). **v4** (v2.9.2) appends that CHR-RAM
63/// after the v3 tail. Since v2.9.8 (ADR 0042) `load_state` reads v4 only; a
64/// v1-v3 blob, which it used to load with the CHR-RAM untouched, is refused.
65const N163_SECTION_VERSION: u8 = 4;
66
67/// Linear scale applied to the channel-count-averaged Namco 163 output (see
68/// [`Namco163::mix_audio`] via the audio struct's `mix`).
69///
70/// Calibrated so a single full-volume (nibble 0↔15, volume 15) N163 square in
71/// 1-channel mode reaches ~6.0x the amplitude of a single full-volume 2A03
72/// pulse — the level Mesen2 (RustyNES's accuracy bar) produces and that no
73/// reference emulator attenuates. Mesen2 `NesSoundMixer::GetOutputVolume`
74/// weights N163 at `output * 20` against the 2A03 pulse DAC of
75/// `95.88*5000/(8128/15+100) ≈ 746.9`; a full 0↔15 square has per-channel
76/// `(sample-8)*volume` swing `225` (from `(0-8)*15 = -120` to `(15-8)*15 =
77/// +105`) which, divided by 1 channel and weighted `*20`, is `4500` — a ratio
78/// of `4500 / 746.9 ≈ 6.03`. Our path is `((sum / n) * scale) / 65536`; for the
79/// same 1-channel full square the normalized swing is `225 * scale / 65536`,
80/// which against the 2A03 pulse's `pulse_table[15] ≈ 0.14882` equals
81/// `225 * 261 / 65536 / 0.14882 ≈ 6.02`. Peak stays representable: a single
82/// full-volume channel reaches `±120 * 261 = ±31320 < i16::MAX`, and the
83/// channel-count division keeps multi-voice sums bounded to the same envelope
84/// (each of `n` voices only drives `1/n` of the output). Before v2.1.6 this was
85/// `64` (≈1.48x — ~12 dB too quiet, an outlier no reference matched). See
86/// `docs/apu-2a03.md` §Expansion-audio levels.
87// Every item below is expansion-audio support: fully implemented and
88// exercised whenever `mapper-audio` is on (the default build is
89// dead-code-warning clean), but unreachable when the feature compiles the
90// audio subsystem out. `allow(dead_code)` ONLY in that configuration —
91// deliberately not `#[cfg]`, so the items still compile and any future
92// non-audio caller keeps working.
93#[cfg_attr(not(feature = "mapper-audio"), allow(dead_code))]
94pub(crate) const NAMCO163_MIX_SCALE: i32 = 261;
95
96/// Namco 163 on-cart wavetable synthesiser.
97///
98/// 1-8 simultaneous channels, each playing a 4-bit wavetable from the
99/// mapper-internal 128-byte sound RAM.  Wavetable data shares the same
100/// RAM as the per-channel register file: the wavetable pool conventionally
101/// sits at `$00-$3F` (128 nibble-samples), and channels claim 8-byte
102/// regions at the top of RAM, with channel 8 (the always-enabled channel)
103/// at `$78-$7F` and channel 1 (the lowest priority) at `$40-$47`.  When
104/// fewer than 8 channels are enabled, the unused channels' register
105/// regions are reusable as additional wavetable storage.
106///
107/// Register interface (per NESdev wiki, "Namco 163 audio"):
108///
109/// - `$F800-$FFFF` (write): **address port**.  Bit 7 = auto-increment
110///   flag; bits 6-0 = 7-bit address into the 128-byte internal RAM.
111/// - `$4800-$4FFF` (read/write): **data port**.  Reads/writes the byte
112///   at the latched address.  If the auto-increment flag is set, the
113///   latch advances by 1 after each access, *saturating at $7F* (per
114///   the wiki: "stopping at $7F" — does **not** wrap to $00).
115///
116/// Per-channel register layout (8 bytes each; here referenced for the
117/// channel at `$78-$7F` = channel 8, but every channel's 8-byte slot
118/// follows the same offsets):
119///
120/// | Offset | Bits   | Field                                           |
121/// |--------|--------|-------------------------------------------------|
122/// | +0     | 7-0    | Frequency low (bits 7-0 of 18-bit freq)         |
123/// | +1     | 7-0    | Phase low (bits 7-0 of 24-bit phase accumulator)|
124/// | +2     | 7-0    | Frequency mid (bits 15-8 of freq)               |
125/// | +3     | 7-0    | Phase mid (bits 15-8 of phase)                  |
126/// | +4     | 1-0    | Frequency high (bits 17-16 of freq)             |
127/// | +4     | 7-2    | Length encoding: waveform length = `256 - (reg & 0xFC)` 4-bit samples |
128/// | +5     | 7-0    | Phase high (bits 23-16 of phase)                |
129/// | +6     | 7-0    | Wave start address, in 4-bit samples (nibbles)  |
130/// | +7     | 3-0    | Linear volume (0..=15)                          |
131/// | +7     | 6-4    | (Channel 8's `$7F` only) `C` field: number of   |
132/// |        |        | enabled channels - 1 (so C=0 → 1 channel,       |
133/// |        |        | C=7 → all 8 channels)                           |
134///
135/// Update rate: each channel updates every 15 CPU cycles.  With `n`
136/// active channels, the chip cycles through them in round-robin, so
137/// per-channel update rate = `CPU_clock / (15 * n)`.  We model this as
138/// a 15-cycle prescaler that advances `tick_index` (mod `n`) and
139/// increments only that one channel's phase per tick.
140///
141/// Mixing: per channel, output = `(sample - 8) * volume`, where `sample`
142/// is the 4-bit nibble fetched from RAM at `(wave_addr + (phase >> 16))
143/// mod L`, `L` is the per-channel wave length, and the `-8` bias makes
144/// the output bipolar (range `-120..=+105`).  The chip itself does not
145/// mix — channels are output one-at-a-time — but in practice emulators
146/// sum the per-channel outputs and divide by the active channel count
147/// (the convention recommended by the wiki and what Mesen2/FCEUX both
148/// do).  The final i16 is scaled to match the headroom VRC6 leaves for
149/// the APU mixer.
150#[cfg(feature = "mapper-audio")]
151#[derive(Clone)]
152pub(crate) struct Namco163Audio {
153    /// 128-byte internal sound RAM.  Shared between wavetable samples
154    /// (`$00-$3F` conventionally) and per-channel register file
155    /// (`$40-$7F`).
156    ram: [u8; 128],
157    /// 7-bit address latch (the address the next data-port access
158    /// targets).
159    addr_latch: u8,
160    /// Auto-increment flag from the most recent `$F800-$FFFF` write.
161    /// When set, data-port accesses advance `addr_latch` (saturating at
162    /// `$7F` per the wiki).
163    auto_inc: bool,
164    /// Round-robin tick index: 0..=7.  Each 15-cycle tick advances the
165    /// phase of channel `7 - tick_index` (since channel 8, at `$78-$7F`,
166    /// is the *first* channel updated when only one channel is enabled).
167    tick_index: u8,
168    /// 15-cycle prescaler.  When it reaches 15, we update the next
169    /// channel and reset.
170    prescaler: u8,
171}
172
173// When the `mapper-audio` feature is OFF, the audio struct still exists
174// (so save-state round-trip and the register-decoder contract stay
175// identical between feature on/off builds) — but reduced to the bare
176// state required for those two paths.
177#[cfg(not(feature = "mapper-audio"))]
178#[derive(Clone)]
179pub(crate) struct Namco163Audio {
180    ram: [u8; 128],
181    addr_latch: u8,
182    auto_inc: bool,
183    tick_index: u8,
184    prescaler: u8,
185}
186
187impl Default for Namco163Audio {
188    fn default() -> Self {
189        Self {
190            ram: [0; 128],
191            addr_latch: 0,
192            auto_inc: false,
193            tick_index: 0,
194            prescaler: 0,
195        }
196    }
197}
198
199impl Namco163Audio {
200    /// Write to the address port (`$F800-$FFFF`).  Bit 7 = auto-increment;
201    /// bits 6-0 = 7-bit address into internal RAM.
202    pub(crate) fn write_addr_port(&mut self, value: u8) {
203        self.auto_inc = value & 0x80 != 0;
204        self.addr_latch = value & 0x7F;
205    }
206
207    /// Advance the address latch if auto-increment is enabled.  Per the
208    /// wiki, it saturates at `$7F` rather than wrapping back to `$00`.
209    fn step_addr(&mut self) {
210        if self.auto_inc && self.addr_latch < 0x7F {
211            self.addr_latch += 1;
212        }
213    }
214
215    /// Write to the data port (`$4800-$4FFF`).  Stores at the latched
216    /// address; advances the latch when auto-increment is set.
217    pub(crate) fn write_data_port(&mut self, value: u8) {
218        let idx = (self.addr_latch & 0x7F) as usize;
219        self.ram[idx] = value;
220        self.step_addr();
221    }
222
223    /// Read from the data port (`$4800-$4FFF`).  Returns the byte at the
224    /// latched address; advances the latch when auto-increment is set.
225    pub(crate) fn read_data_port(&mut self) -> u8 {
226        let idx = (self.addr_latch & 0x7F) as usize;
227        let v = self.ram[idx];
228        self.step_addr();
229        v
230    }
231
232    /// Active channel count, derived from bits 6-4 of register `$7F`
233    /// (`C` field): returns `C + 1` in the range `1..=8`.
234    #[cfg(feature = "mapper-audio")]
235    fn channel_count(&self) -> u8 {
236        ((self.ram[0x7F] >> 4) & 0x07) + 1
237    }
238
239    /// Compute the 18-bit frequency value for the channel whose 8-byte
240    /// register slot starts at `base` (i.e. `$78` for channel 8, `$70`
241    /// for channel 7, ..., `$40` for channel 1).
242    #[cfg(feature = "mapper-audio")]
243    fn channel_freq(&self, base: usize) -> u32 {
244        let lo = u32::from(self.ram[base]);
245        let mid = u32::from(self.ram[base + 2]);
246        let hi = u32::from(self.ram[base + 4] & 0x03);
247        lo | (mid << 8) | (hi << 16)
248    }
249
250    /// 24-bit phase accumulator for the channel at `base`.
251    #[cfg(feature = "mapper-audio")]
252    fn channel_phase(&self, base: usize) -> u32 {
253        let lo = u32::from(self.ram[base + 1]);
254        let mid = u32::from(self.ram[base + 3]);
255        let hi = u32::from(self.ram[base + 5]);
256        lo | (mid << 8) | (hi << 16)
257    }
258
259    /// Write back the 24-bit phase to the channel's three phase
260    /// registers.  Only bits 23..0 are retained (the value is naturally
261    /// 24-bit; we mask to be safe under wrap-around).
262    #[cfg(feature = "mapper-audio")]
263    fn set_channel_phase(&mut self, base: usize, phase: u32) {
264        let phase = phase & 0x00FF_FFFF;
265        self.ram[base + 1] = (phase & 0xFF) as u8;
266        self.ram[base + 3] = ((phase >> 8) & 0xFF) as u8;
267        self.ram[base + 5] = ((phase >> 16) & 0xFF) as u8;
268    }
269
270    /// Wave length L (in 4-bit samples) for the channel at `base`.
271    /// Per the wiki: `L = 256 - (reg[base+4] & 0xFC)`.
272    #[cfg(feature = "mapper-audio")]
273    fn channel_length(&self, base: usize) -> u32 {
274        256u32 - u32::from(self.ram[base + 4] & 0xFC)
275    }
276
277    /// Wave start address for the channel at `base` (in nibble units —
278    /// every step of `wave_addr` represents one 4-bit sample, so two
279    /// nibbles per RAM byte).
280    #[cfg(feature = "mapper-audio")]
281    fn channel_wave_addr(&self, base: usize) -> u32 {
282        u32::from(self.ram[base + 6])
283    }
284
285    /// 4-bit linear volume for the channel at `base`.
286    #[cfg(feature = "mapper-audio")]
287    fn channel_volume(&self, base: usize) -> u8 {
288        self.ram[base + 7] & 0x0F
289    }
290
291    /// Resolve the 4-bit nibble at `nibble_addr` in the wavetable pool.
292    /// Bit 0 of the address picks the high or low nibble of the
293    /// corresponding RAM byte: even = low nibble, odd = high nibble.
294    #[cfg(feature = "mapper-audio")]
295    fn fetch_nibble(&self, nibble_addr: u32) -> u8 {
296        let byte = self.ram[((nibble_addr >> 1) & 0x7F) as usize];
297        if nibble_addr & 1 == 0 {
298            byte & 0x0F
299        } else {
300            (byte >> 4) & 0x0F
301        }
302    }
303
304    /// Returns the register-file base address for the i-th enabled
305    /// channel (i = 0 is the always-enabled channel 8 at `$78-$7F`;
306    /// i = 1 is channel 7 at `$70-$77`; ...; i = 7 is channel 1 at
307    /// `$40-$47`).
308    #[cfg(feature = "mapper-audio")]
309    const fn channel_base(i: u8) -> usize {
310        // Channel 8 = $78, channel 7 = $70, ..., channel 1 = $40.
311        // base = 0x78 - i*8.
312        0x78 - (i as usize) * 8
313    }
314
315    /// Advance one CPU cycle.  Every 15 cycles, round-robin to the next
316    /// enabled channel and increment its phase by its 18-bit freq value.
317    /// When the phase exceeds `L * 65536`, wrap around — the integer
318    /// part of `phase >> 16` modulo `L` is the wavetable index.
319    #[cfg(feature = "mapper-audio")]
320    pub(crate) fn clock(&mut self) {
321        self.prescaler = self.prescaler.wrapping_add(1);
322        if self.prescaler < 15 {
323            return;
324        }
325        self.prescaler = 0;
326
327        let n = self.channel_count();
328        // Round-robin within the active set.  tick_index counts 0..n.
329        if self.tick_index >= n {
330            self.tick_index = 0;
331        }
332        let ch = self.tick_index;
333        self.tick_index = (self.tick_index + 1) % n;
334
335        let base = Self::channel_base(ch);
336        let freq = self.channel_freq(base);
337        let length = self.channel_length(base);
338        // Phase modulus is L * 2^16 (so that (phase >> 16) mod L stays
339        // in [0, L)).  Use 64-bit math to avoid 32-bit overflow when L
340        // is near 256 and freq is near 2^18.
341        let modulus = u64::from(length) << 16;
342        let mut phase = u64::from(self.channel_phase(base));
343        phase = phase.wrapping_add(u64::from(freq));
344        if modulus != 0 {
345            phase %= modulus;
346        }
347        self.set_channel_phase(base, phase as u32);
348    }
349
350    /// Per-channel output sample, bipolar: `(nibble - 8) * volume`,
351    /// range `-120..=+105`.
352    #[cfg(feature = "mapper-audio")]
353    fn channel_output(&self, ch: u8) -> i16 {
354        let base = Self::channel_base(ch);
355        let length = self.channel_length(base);
356        if length == 0 {
357            return 0;
358        }
359        let phase = self.channel_phase(base);
360        let wave_addr = self.channel_wave_addr(base);
361        let index = (phase >> 16) % length;
362        let nibble = self.fetch_nibble(wave_addr + index);
363        // -8 bias makes the output bipolar.
364        let signed = i16::from(nibble) - 8;
365        signed * i16::from(self.channel_volume(base))
366    }
367
368    /// Linear-summed audio output, scaled by [`NAMCO163_MIX_SCALE`] to the
369    /// hardware-accurate level (v2.1.6).  Per the wiki, channels are output
370    /// one-at-a-time on hardware; emulators (Mesen2, FCEUX) approximate the
371    /// mix by summing channel outputs and dividing by the number of active
372    /// channels.  We do the same, then scale by `261` so a single full-volume
373    /// bipolar channel reaches `±31,320` — just under `i16::MAX` and, through
374    /// the bus's `/65536` external contract, ~6.0x the 2A03 pulse peak (the
375    /// Mesen2 `*20`-weighted `db_n163` level; see [`NAMCO163_MIX_SCALE`]).
376    ///
377    /// NOTE: The channel-count division matches the reference emulators'
378    /// behaviour; the chip's real per-channel time-multiplexed output is
379    /// effectively the same average since each channel only drives the
380    /// output `1/n` of the time.  Before v2.1.6 the scale was `64` (~1.48x —
381    /// ~12 dB too quiet).
382    #[cfg(feature = "mapper-audio")]
383    pub(crate) fn mix(&self) -> i16 {
384        let n = self.channel_count();
385        if n == 0 {
386            return 0;
387        }
388        let mut sum: i32 = 0;
389        for ch in 0..n {
390            sum += i32::from(self.channel_output(ch));
391        }
392        // Per-channel range is -120..=+105; the channel-count-averaged sum has
393        // the same envelope.  Scale to the Mesen2 `db_n163` level.
394        ((sum / i32::from(n)) * NAMCO163_MIX_SCALE) as i16
395    }
396
397    /// Feature-off shim: the wavetable generator does not advance with
398    /// `mapper-audio` disabled.
399    ///
400    /// Mirrors the gated `clock` above so the shared NSF expansion router
401    /// (`nsf_expansion::NsfExpansion::clock`) can call it unconditionally, the
402    /// same arrangement `Sunsoft5BAudio` and `FdsAudio` already had. Its
403    /// absence broke `--no-default-features` outright: the router clocks every
404    /// present chip with no `cfg` of its own, so with the feature off this was
405    /// a hard `E0599` — the N163 was the one chip in the router missing the
406    /// shim, and `mix` alone was not enough.
407    #[cfg(not(feature = "mapper-audio"))]
408    #[allow(clippy::needless_pass_by_ref_mut, clippy::unused_self)]
409    pub(crate) fn clock(&mut self) {}
410
411    /// `mix_audio` shim for the no-audio build.
412    #[cfg(not(feature = "mapper-audio"))]
413    #[allow(clippy::unused_self)]
414    pub(crate) fn mix(&self) -> i16 {
415        0
416    }
417
418    /// Save-state tail layout (kept lock-step with `read_tail`):
419    ///   ram[128]      : 128
420    ///   addr_latch    : 1
421    ///   auto_inc      : 1 (bool)
422    ///   tick_index    : 1
423    ///   prescaler     : 1
424    ///   -- 132 bytes total --
425    fn write_tail(&self, out: &mut Vec<u8>) {
426        out.extend_from_slice(&self.ram);
427        out.push(self.addr_latch & 0x7F);
428        out.push(u8::from(self.auto_inc));
429        out.push(self.tick_index);
430        out.push(self.prescaler);
431    }
432
433    /// Tail size in bytes — see `write_tail`.
434    const TAIL_LEN: usize = 128 + 1 + 1 + 1 + 1;
435
436    fn read_tail(&mut self, src: &[u8]) -> Result<(), MapperError> {
437        if src.len() < Self::TAIL_LEN {
438            return Err(MapperError::WrongLength {
439                expected: Self::TAIL_LEN,
440                got: src.len(),
441            });
442        }
443        self.ram.copy_from_slice(&src[0..128]);
444        self.addr_latch = src[128] & 0x7F;
445        self.auto_inc = src[129] != 0;
446        self.tick_index = src[130];
447        self.prescaler = src[131];
448        Ok(())
449    }
450}
451
452/// Namco 163 (Mapper 19).  Banking + CPU-cycle IRQ + (gated behind
453/// `mapper-audio`) 1-8 channel wavetable audio.
454pub struct Namco163 {
455    prg_rom: Box<[u8]>,
456    chr_rom: Box<[u8]>,
457    chr_is_ram: bool,
458    prg_ram: Box<[u8]>,
459    /// The console's 2 KiB CIRAM, as this board sees it. N163 can map CIRAM
460    /// as CHR-RAM, which the PPU-owned copy cannot serve (a pattern fetch goes
461    /// to the mapper), so while `ciram_owned` is set this copy is the one both
462    /// nametable and pattern fetches read. The PPU's copy is still written in
463    /// parallel for nametable writes, so debugger views of it stay close.
464    vram: Box<[u8]>,
465    /// `vram` is authoritative. True from power-on, where both copies are
466    /// zero. False after restoring a pre-v2.7.2 save state: those never kept
467    /// `vram` in step with the PPU's CIRAM, so nametable fetches then stay
468    /// with the PPU copy (the old behaviour) and CIRAM-as-CHR reads `vram`.
469    ciram_owned: bool,
470    prg: [u8; 4], // 8 KiB banks: $8000, $A000, $C000, fixed $E000
471    chr: [u8; 8], // 1 KiB CHR banks
472    nta: [u8; 4], // 1 KiB NTA banks (CIRAM/CHR ROM swappable)
473    /// `$E800` bits 7-6, the CHR-RAM disables: bit 6 for pattern
474    /// `$0000-$0FFF`, bit 7 for `$1000-$1FFF`. Set = CHR values `$E0-$FF`
475    /// are CHR-ROM pages; clear = they map console CIRAM as CHR-RAM.
476    chr_ram_disable: u8,
477    mirroring: Mirroring,
478
479    irq_counter: u16,
480    irq_pending: bool,
481
482    /// Audio disable bit (`$E000-$E7FF` bit 6).  When set, the
483    /// N163 audio circuitry is silenced — both the per-channel clocks
484    /// stop advancing and `mix_audio` returns 0.  Cleared at power-on.
485    sound_disabled: bool,
486    /// Namco 163 on-cart wavetable audio state.  Live regardless of the
487    /// `mapper-audio` feature — the register decoders always latch into
488    /// `ram` and the address-port flag/latch (so save states stay
489    /// round-trippable across builds), but `clock()` / `mix()` are only
490    /// driven when the feature is on.
491    audio: Namco163Audio,
492}
493
494impl Namco163 {
495    /// Construct a new Namco 163 mapper.
496    ///
497    /// # Errors
498    ///
499    /// Returns [`MapperError::Invalid`] on size mismatch.
500    pub fn new(
501        prg_rom: Box<[u8]>,
502        chr_rom: Box<[u8]>,
503        mirroring: Mirroring,
504    ) -> Result<Self, MapperError> {
505        if prg_rom.is_empty() || !prg_rom.len().is_multiple_of(PRG_BANK_8K) {
506            return Err(MapperError::Invalid(format!(
507                "Namco163 PRG-ROM size {} is not a non-zero multiple of 8 KiB",
508                prg_rom.len()
509            )));
510        }
511        let chr_is_ram = chr_rom.is_empty();
512        let chr: Box<[u8]> = if chr_is_ram {
513            vec![0u8; CHR_BANK_8K].into_boxed_slice()
514        } else if chr_rom.len().is_multiple_of(CHR_BANK_1K) {
515            chr_rom
516        } else {
517            return Err(MapperError::Invalid(format!(
518                "Namco163 CHR-ROM size {} is not a multiple of 1 KiB",
519                chr_rom.len()
520            )));
521        };
522        Ok(Self {
523            prg_rom,
524            chr_rom: chr,
525            chr_is_ram,
526            prg_ram: vec![0u8; 8 * 1024].into_boxed_slice(),
527            vram: vec![0u8; 2 * NAMETABLE_SIZE].into_boxed_slice(),
528            ciram_owned: true,
529            prg: [0, 0, 0, 0],
530            chr: [0; 8],
531            // The nametable registers power on as the header's layout
532            // (`$E0` = CIRAM A, `$E1` = CIRAM B), so a game that never
533            // writes them keeps the mirroring it had before v2.7.2.
534            nta: Self::nta_for(mirroring),
535            // Both halves power on as CHR-ROM, the pre-v2.7.2 behaviour, until
536            // the game writes `$E800`.
537            chr_ram_disable: 0xC0,
538            mirroring,
539            irq_counter: 0,
540            irq_pending: false,
541            sound_disabled: false,
542            audio: Namco163Audio::default(),
543        })
544    }
545
546    fn prg_offset(&self, addr: u16) -> usize {
547        let total_8k = (self.prg_rom.len() / PRG_BANK_8K).max(1);
548        let last = total_8k - 1;
549        let bank = match addr & 0xE000 {
550            0x8000 => (self.prg[0] as usize) % total_8k,
551            0xA000 => (self.prg[1] as usize) % total_8k,
552            0xC000 => (self.prg[2] as usize) % total_8k,
553            0xE000 => last,
554            _ => 0,
555        };
556        bank * PRG_BANK_8K + (addr as usize & 0x1FFF)
557    }
558}
559
560impl Namco163 {
561    /// Nametable register values that reproduce a fixed layout.
562    const fn nta_for(mirroring: Mirroring) -> [u8; 4] {
563        match mirroring {
564            Mirroring::Horizontal => [0xE0, 0xE0, 0xE1, 0xE1],
565            Mirroring::SingleScreenA => [0xE0; 4],
566            Mirroring::SingleScreenB => [0xE1; 4],
567            // Vertical, and the layouts N163 cannot express, default vertical.
568            _ => [0xE0, 0xE1, 0xE0, 0xE1],
569        }
570    }
571
572    /// What a 1 KiB bank register value maps to (`nesdev_wiki/INES_Mapper_019.xhtml`):
573    /// `Ok(page)` is a CHR-ROM page, `Err(ciram_page)` is console CIRAM page
574    /// A (0) or B (1). `ciram_allowed` is false where `$E800` disables it.
575    fn resolve(&self, value: u8, ciram_allowed: bool) -> Result<usize, usize> {
576        if value >= 0xE0 && ciram_allowed {
577            Err(usize::from(value & 1))
578        } else {
579            let pages = (self.chr_rom.len() / CHR_BANK_1K).max(1);
580            Ok(usize::from(value) % pages)
581        }
582    }
583
584    /// Resolve a pattern-table access through its 1 KiB CHR register.
585    fn chr_target(&self, addr: u16) -> Result<usize, usize> {
586        let slot = usize::from(addr >> 10) & 7;
587        let disable_bit = if addr < 0x1000 { 0x40 } else { 0x80 };
588        let allowed = !self.chr_is_ram && self.chr_ram_disable & disable_bit == 0;
589        self.resolve(self.chr[slot], allowed)
590            .map(|page| page * CHR_BANK_1K + usize::from(addr) % CHR_BANK_1K)
591            .map_err(|ciram| ciram * NAMETABLE_SIZE + usize::from(addr) % CHR_BANK_1K)
592    }
593
594    /// Resolve a nametable access (`$2000-$3EFF`) through `$C000-$DFFF`,
595    /// which are always allowed to select CIRAM.
596    fn nt_target(&self, addr: u16) -> Result<usize, usize> {
597        let quadrant = usize::from((addr - 0x2000) >> 10) & 3;
598        self.resolve(self.nta[quadrant], true)
599            .map(|page| page * CHR_BANK_1K + usize::from(addr) % CHR_BANK_1K)
600            .map_err(|ciram| ciram * NAMETABLE_SIZE + usize::from(addr) % NAMETABLE_SIZE)
601    }
602}
603
604impl Mapper for Namco163 {
605    fn sram(&self) -> &[u8] {
606        &self.prg_ram
607    }
608    fn sram_mut(&mut self) -> &mut [u8] {
609        &mut self.prg_ram
610    }
611    // v2.8.0 Phase 4 — CPU-cycle hook + IRQ source + expansion audio
612    // (the audio hook only exists under the `mapper-audio` feature).
613    fn caps(&self) -> MapperCaps {
614        MapperCaps {
615            cpu_cycle_hook: true,
616            audio: cfg!(feature = "mapper-audio"),
617            frame_event_hook: false,
618            irq_source: true,
619        }
620    }
621
622    fn cpu_read_unmapped(&self, addr: u16) -> bool {
623        // v2.7.2 (core audit §5.5): with no save RAM, nothing drives
624        // `$6000-$7FFF` and it floats; see `Mapper::cpu_read_unmapped`.
625        (matches!(addr, 0x6000..=0x7FFF) && self.sram().is_empty()) || {
626            // Namco 163 maps `$4800-$4FFF` (sound data port) and
627            // `$5000-$5FFF` (IRQ counter low/high). The `$4020-$47FF`
628            // range is unmapped.
629            (0x4020..=0x47FF).contains(&addr)
630        }
631    }
632
633    fn cpu_read(&mut self, addr: u16) -> u8 {
634        match addr {
635            // Audio data port: reads the byte at the latched address in
636            // internal sound RAM, advancing the latch if auto-increment
637            // is set.  Decoder runs regardless of `mapper-audio`.
638            0x4800..=0x4FFF => self.audio.read_data_port(),
639            0x5000..=0x57FF => {
640                // IRQ counter low.
641                let v = (self.irq_counter & 0xFF) as u8;
642                self.irq_pending = false;
643                v
644            }
645            0x5800..=0x5FFF => {
646                // `EHHH HHHH`: the enable in bit 7, the counter's high bits
647                // below it (bit 15 of `irq_counter` holds the enable).
648                let v = (self.irq_counter >> 8) as u8;
649                self.irq_pending = false;
650                v
651            }
652            0x6000..=0x7FFF => self.prg_ram[(addr - 0x6000) as usize % self.prg_ram.len()],
653            0x8000..=0xFFFF => {
654                let off = self.prg_offset(addr);
655                self.prg_rom[off % self.prg_rom.len()]
656            }
657            _ => 0,
658        }
659    }
660
661    fn cpu_write(&mut self, addr: u16, value: u8) {
662        match addr {
663            // Audio data port: stores at the latched address in internal
664            // sound RAM, advancing the latch if auto-increment is set.
665            // Decoder runs regardless of `mapper-audio`.
666            0x4800..=0x4FFF => self.audio.write_data_port(value),
667            0x5000..=0x57FF => {
668                self.irq_counter = (self.irq_counter & 0xFF00) | u16::from(value);
669                self.irq_pending = false;
670            }
671            // `$5800` is `EHHH HHHH` (NESdev "INES Mapper 019"): bit 7 is
672            // the IRQ enable and bits 6-0 the counter's high bits. Bit 15 of
673            // `irq_counter` stores the enable, so the whole byte lands in the
674            // high half. Until v2.9.8 this forced the enable on, so a
675            // `$5800 = $00` meant to stop the counter restarted it instead
676            // (Megami Tensei II's raster bands).
677            0x5800..=0x5FFF => {
678                self.irq_counter = (self.irq_counter & 0x00FF) | (u16::from(value) << 8);
679                self.irq_pending = false;
680            }
681            0x6000..=0x7FFF => {
682                let off = (addr - 0x6000) as usize % self.prg_ram.len();
683                self.prg_ram[off] = value;
684            }
685            0x8000..=0xBFFF => {
686                let slot = ((addr - 0x8000) >> 11) as usize; // 4 banks: 8000,8800,9000,9800,A000,...
687                if slot < 8 {
688                    self.chr[slot] = value;
689                }
690            }
691            // `$C000`, `$C800`, `$D000`, `$D800`: the four nametable
692            // quadrants (v2.7.2, core audit IMP-11).
693            0xC000..=0xDFFF => {
694                self.nta[usize::from((addr - 0xC000) >> 11)] = value;
695            }
696            // $E000-$E7FF: PRG bank 0 select (bits 0-5) + audio-disable
697            // flag (bit 6).  When bit 6 is set, the N163 audio chip is
698            // silenced — see `mix_audio` / `notify_cpu_cycle`.
699            0xE000..=0xE7FF => {
700                self.prg[0] = value & 0x3F;
701                self.sound_disabled = value & 0x40 != 0;
702            }
703            // Bits 5-0 PRG at `$A000`; bits 7-6 the CHR-RAM disables.
704            0xE800..=0xEFFF => {
705                self.prg[1] = value & 0x3F;
706                self.chr_ram_disable = value & 0xC0;
707            }
708            0xF000..=0xF7FF => self.prg[2] = value & 0x3F,
709            // $F800-$FFFF: audio address port (bit 7 = auto-increment,
710            // bits 6-0 = 7-bit internal RAM address).  On real hardware
711            // this register also gates PRG-RAM writes via the upper
712            // nibble (`0100` enables writes), but no commercially-released
713            // Namco 163 cartridge uses that feature in a way that affects
714            // accuracy, so we model only the audio half here.  Decoder
715            // runs regardless of `mapper-audio`.
716            0xF800..=0xFFFF => self.audio.write_addr_port(value),
717            _ => {}
718        }
719    }
720
721    // The PPU fetches nametables through these three hooks, never through
722    // `ppu_read`/`ppu_write` (`Bus`, `PpuBusAdapter`). Without them the
723    // `$C000-$DFFF` CHR-ROM pages and CIRAM-as-CHR worked only when a test
724    // called `ppu_read($2000)` directly (PR #550 review).
725    fn nametable_fetch(&mut self, addr: u16) -> Option<u8> {
726        match self.nt_target(addr) {
727            Ok(off) => Some(self.chr_rom[off % self.chr_rom.len()]),
728            Err(ciram) => self.ciram_owned.then(|| self.vram[ciram]),
729        }
730    }
731
732    fn nametable_write(&mut self, addr: u16, value: u8) -> bool {
733        match self.nt_target(addr) {
734            // A CHR-ROM page is read-only: absorb the write.
735            Ok(_) => true,
736            // CIRAM: keep this board's copy, and let the PPU write its own at
737            // `nametable_address` so the two stay equal.
738            Err(ciram) => {
739                self.vram[ciram] = value;
740                false
741            }
742        }
743    }
744
745    #[allow(clippy::cast_possible_truncation)]
746    fn nametable_address(&self, addr: u16) -> u16 {
747        // The CIRAM offset the nametable registers select; `ciram < 0x800`, so
748        // the cast is exact. A CHR-ROM quadrant never reaches CIRAM, and its
749        // offset only matters to debugger views.
750        match self.nt_target(addr) {
751            Err(ciram) => ciram as u16,
752            Ok(_) => (addr.wrapping_sub(0x2000) >> 10 & 1) * 0x400 + (addr & 0x3FF),
753        }
754    }
755
756    fn ppu_read(&mut self, addr: u16) -> u8 {
757        let addr = addr & 0x3FFF;
758        match addr {
759            0x0000..=0x1FFF => match self.chr_target(addr) {
760                Ok(off) => self.chr_rom[off % self.chr_rom.len()],
761                Err(ciram) => self.vram[ciram],
762            },
763            0x2000..=0x3EFF => match self.nt_target(addr) {
764                Ok(off) => self.chr_rom[off % self.chr_rom.len()],
765                Err(ciram) => self.vram[ciram],
766            },
767            _ => 0,
768        }
769    }
770
771    fn ppu_write(&mut self, addr: u16, value: u8) {
772        let addr = addr & 0x3FFF;
773        match addr {
774            0x0000..=0x1FFF => {
775                if self.chr_is_ram {
776                    let len = self.chr_rom.len();
777                    self.chr_rom[addr as usize % len] = value;
778                } else if let Err(ciram) = self.chr_target(addr) {
779                    // CIRAM mapped as CHR-RAM is writable; CHR-ROM is not.
780                    self.vram[ciram] = value;
781                }
782            }
783            0x2000..=0x3EFF => {
784                // A CHR-ROM page used as a nametable is read-only.
785                if let Err(ciram) = self.nt_target(addr) {
786                    self.vram[ciram] = value;
787                }
788            }
789            _ => {}
790        }
791    }
792
793    fn notify_cpu_cycle(&mut self) {
794        // N163 audio runs every CPU cycle whenever the chip is not
795        // silenced via the $E000 sound-disable bit.  None of the
796        // 8 channel oscillators can be individually halted — only the
797        // active-channel count and per-channel volume gate their effect
798        // on the mix.
799        #[cfg(feature = "mapper-audio")]
800        if !self.sound_disabled {
801            self.audio.clock();
802        }
803
804        if self.irq_counter & 0x8000 != 0 {
805            let low = self.irq_counter & 0x7FFF;
806            if low == 0x7FFF {
807                self.irq_pending = true;
808            } else {
809                self.irq_counter = (self.irq_counter & 0x8000) | (low + 1);
810            }
811        }
812    }
813
814    #[cfg(feature = "mapper-audio")]
815    fn mix_audio(&mut self) -> i32 {
816        if self.sound_disabled {
817            return 0;
818        }
819        i32::from(self.audio.mix())
820    }
821
822    fn irq_pending(&self) -> bool {
823        self.irq_pending
824    }
825
826    /// The layout the nametable registers currently produce, when it is a
827    /// standard one; otherwise mapper-controlled (a CHR-ROM page, or a mix
828    /// no fixed layout describes). Nametable fetches go through
829    /// `nt_target` either way; this is what the debugger reports.
830    fn current_mirroring(&self) -> Mirroring {
831        match self.nta {
832            [0xE0, 0xE1, 0xE0, 0xE1] => Mirroring::Vertical,
833            [0xE0, 0xE0, 0xE1, 0xE1] => Mirroring::Horizontal,
834            [0xE0, 0xE0, 0xE0, 0xE0] => Mirroring::SingleScreenA,
835            [0xE1, 0xE1, 0xE1, 0xE1] => Mirroring::SingleScreenB,
836            _ => Mirroring::MapperControlled,
837        }
838    }
839
840    fn debug_info(&self) -> crate::mapper::MapperDebugInfo {
841        let mut info = crate::mapper::MapperDebugInfo {
842            mapper_id: 19,
843            name: "Namco 163".into(),
844            mirroring: crate::mapper::mirroring_name(self.current_mirroring()),
845            ..Default::default()
846        };
847        for (i, b) in self.prg.iter().enumerate() {
848            info.prg_banks
849                .push((format!("PRG{i}"), format!("{b:#04x}")));
850        }
851        for (i, b) in self.chr.iter().enumerate() {
852            info.chr_banks
853                .push((format!("CHR{i}"), format!("{b:#04x}")));
854        }
855        for (i, b) in self.nta.iter().enumerate() {
856            info.extra.push((format!("NTA{i}"), format!("{b:#04x}")));
857        }
858        info.irq_state
859            .push(("counter".into(), format!("{:#06x}", self.irq_counter)));
860        info.irq_state
861            .push(("pending".into(), format!("{}", self.irq_pending)));
862        info
863    }
864
865    fn save_state(&self) -> Vec<u8> {
866        // v2 (per ADR-0003): strictly additive tail — older v1 readers
867        // tolerate the additional bytes (we encode the audio at the end,
868        // so the core layout is byte-identical to v1).
869        // Audio tail layout:
870        //   sound_disabled : 1
871        //   audio block    : Namco163Audio::TAIL_LEN (132 bytes)
872        //   -- 133 bytes total --
873        let mut out = Vec::with_capacity(
874            32 + self.prg_ram.len()
875                + self.vram.len()
876                + 1
877                + Namco163Audio::TAIL_LEN
878                + 2
879                + self.chr_ram_tail_len(),
880        );
881        out.push(N163_SECTION_VERSION);
882        out.extend_from_slice(&self.prg);
883        out.extend_from_slice(&self.chr);
884        out.extend_from_slice(&self.nta);
885        out.push(self.mirroring as u8);
886        out.extend_from_slice(&self.irq_counter.to_le_bytes());
887        out.push(u8::from(self.irq_pending));
888        out.extend_from_slice(&self.prg_ram);
889        out.extend_from_slice(&self.vram);
890        // v2 audio tail.
891        out.push(u8::from(self.sound_disabled));
892        self.audio.write_tail(&mut out);
893        // v3 tail (v2.7.2).
894        out.push(self.chr_ram_disable);
895        out.push(u8::from(self.ciram_owned));
896        // v4 tail (v2.9.2): the cartridge CHR-RAM, if any; see
897        // `N163_SECTION_VERSION`.
898        if self.chr_is_ram {
899            out.extend_from_slice(&self.chr_rom);
900        }
901        out
902    }
903
904    fn load_state(&mut self, data: &[u8]) -> Result<(), MapperError> {
905        let scalar_len = 1 + 4 + 8 + 4 + 1 + 2 + 1;
906        let core_expected = scalar_len + self.prg_ram.len() + self.vram.len();
907        if data.len() < core_expected {
908            return Err(MapperError::WrongLength {
909                expected: core_expected,
910                got: data.len(),
911            });
912        }
913        let version = data[0];
914        // Only the current version is read (v2.9.8, ADR 0042). v1-v3 used to
915        // load with the audio, CIRAM-as-CHR and CHR-RAM tails defaulted or
916        // left as they were.
917        if version != N163_SECTION_VERSION {
918            return Err(MapperError::UnsupportedVersion(version));
919        }
920        // Strict: the core, the audio tail, the two CIRAM-as-CHR bytes and
921        // the CHR-RAM, exactly. Each tail's offset depends on every earlier
922        // field being present, so neither a short nor a long blob can be read
923        // safely.
924        let expected = core_expected + 1 + Namco163Audio::TAIL_LEN + 2 + self.chr_ram_tail_len();
925        if data.len() != expected {
926            return Err(MapperError::WrongLength {
927                expected,
928                got: data.len(),
929            });
930        }
931        self.prg.copy_from_slice(&data[1..5]);
932        self.chr.copy_from_slice(&data[5..13]);
933        self.nta.copy_from_slice(&data[13..17]);
934        self.mirroring = match data[17] {
935            0 => Mirroring::Horizontal,
936            1 => Mirroring::Vertical,
937            2 => Mirroring::SingleScreenA,
938            3 => Mirroring::SingleScreenB,
939            4 => Mirroring::FourScreen,
940            5 => Mirroring::MapperControlled,
941            other => return Err(MapperError::Invalid(format!("mirroring {other}"))),
942        };
943        self.irq_counter = u16::from_le_bytes(
944            data[18..20]
945                .try_into()
946                .map_err(|_| MapperError::Invalid("irq_counter".into()))?,
947        );
948        self.irq_pending = data[20] != 0;
949        let mut cur = 21usize;
950        self.prg_ram
951            .copy_from_slice(&data[cur..cur + self.prg_ram.len()]);
952        cur += self.prg_ram.len();
953        self.vram.copy_from_slice(&data[cur..cur + self.vram.len()]);
954        cur += self.vram.len();
955
956        // v2 tail: audio + sound-disable bit.
957        self.sound_disabled = data[cur] != 0;
958        cur += 1;
959        self.audio
960            .read_tail(&data[cur..cur + Namco163Audio::TAIL_LEN])?;
961        cur += Namco163Audio::TAIL_LEN;
962        // v3 tail (v2.7.2): CIRAM-as-CHR.
963        self.chr_ram_disable = data[cur] & 0xC0;
964        self.ciram_owned = data[cur + 1] != 0;
965        // v4 tail (v2.9.2): the cartridge CHR-RAM, exactly sized above.
966        if self.chr_is_ram {
967            self.chr_rom.copy_from_slice(&data[cur + 2..]);
968        }
969        Ok(())
970    }
971}
972
973impl Namco163 {
974    /// Bytes the v4 tail adds: the 8 KiB CHR-RAM when the cartridge has no
975    /// CHR-ROM, else nothing. Derived from the loaded ROM, so a save and its
976    /// load (same ROM, checked by the `.rns` hash tag) agree. Distinct from
977    /// CIRAM-as-CHR (`chr_ram_disable`), whose bytes are the `vram` field and
978    /// have travelled in the core since v1.
979    fn chr_ram_tail_len(&self) -> usize {
980        if self.chr_is_ram {
981            self.chr_rom.len()
982        } else {
983            0
984        }
985    }
986}
987
988#[cfg(test)]
989mod tests {
990    use super::*;
991
992    fn synth(banks_8k: usize) -> Box<[u8]> {
993        let mut v = vec![0u8; banks_8k * PRG_BANK_8K];
994        for b in 0..banks_8k {
995            v[b * PRG_BANK_8K] = b as u8;
996        }
997        v.into_boxed_slice()
998    }
999
1000    fn synth_chr(banks_1k: usize) -> Box<[u8]> {
1001        let mut v = vec![0u8; banks_1k * CHR_BANK_1K];
1002        for b in 0..banks_1k {
1003            v[b * CHR_BANK_1K] = b as u8;
1004        }
1005        v.into_boxed_slice()
1006    }
1007
1008    #[test]
1009    fn namco163_irq_counter() {
1010        let mut m = Namco163::new(synth(8), synth_chr(8), Mirroring::Vertical).unwrap();
1011        // Set counter low byte = 0xFFE, then high byte+enable.
1012        m.cpu_write(0x5000, 0xFE);
1013        m.cpu_write(0x5800, 0xFF); // sets bit 7 & 0x80 of high byte = enable.
1014        // Ticks until counter reaches 0x7FFF.
1015        for _ in 0..3 {
1016            m.notify_cpu_cycle();
1017        }
1018        assert!(m.irq_pending());
1019    }
1020
1021    #[test]
1022    fn namco163_5800_bit7_is_the_irq_enable() {
1023        // NESdev "INES Mapper 019", $5800-$5FFF (read/write) is `EHHH HHHH`:
1024        // bit 7 is the IRQ enable (0: disabled) and bits 6-0 the counter's
1025        // high bits. Until v2.9.8 every $5800 write enabled the counter, so a
1026        // game that disables its raster IRQ with $5800 = $00 kept counting
1027        // and took a spurious IRQ every 32,768 cycles. Megami Tensei II does
1028        // exactly that after its last raster band, and the stray IRQ rewrote
1029        // its background CHR banks mid-frame.
1030        let mut m = Namco163::new(synth(8), synth_chr(8), Mirroring::Vertical).unwrap();
1031        m.cpu_write(0x5000, 0xFE);
1032        m.cpu_write(0x5800, 0x7F); // counter $7FFE, enable clear
1033        for _ in 0..40_000 {
1034            m.notify_cpu_cycle();
1035        }
1036        assert!(!m.irq_pending(), "a disabled counter never fires");
1037        assert_eq!(
1038            m.cpu_read(0x5800),
1039            0x7F,
1040            "disabled: bit 7 reads 0, count held"
1041        );
1042        assert_eq!(
1043            m.cpu_read(0x5000),
1044            0xFE,
1045            "a disabled counter does not count"
1046        );
1047        m.cpu_write(0x5800, 0xFF); // same count, enable set
1048        assert_eq!(m.cpu_read(0x5800), 0xFF, "bit 7 reads back the enable");
1049        m.notify_cpu_cycle(); // $7FFE -> $7FFF
1050        m.notify_cpu_cycle(); // at $7FFF: fire
1051        assert!(m.irq_pending(), "an enabled counter fires at $7FFF");
1052    }
1053
1054    fn namco163_for_audio() -> Namco163 {
1055        Namco163::new(synth(8), synth_chr(8), Mirroring::Vertical).unwrap()
1056    }
1057
1058    fn n163_write_ram(m: &mut Namco163, addr: u8, auto_inc: bool, value: u8) {
1059        // $F800 = address port (bit 7 = auto-increment, bits 6-0 = addr).
1060        let port = (if auto_inc { 0x80 } else { 0x00 }) | (addr & 0x7F);
1061        m.cpu_write(0xF800, port);
1062        m.cpu_write(0x4800, value);
1063    }
1064
1065    #[test]
1066    fn namco163_address_port_latch_and_auto_increment() {
1067        let mut m = namco163_for_audio();
1068        // Without auto-increment: write 0x05 to addr, then 0x42 to data.
1069        // Latch should stay at 0x05.
1070        m.cpu_write(0xF800, 0x05);
1071        m.cpu_write(0x4800, 0x42);
1072        assert_eq!(m.audio.ram[0x05], 0x42);
1073        assert_eq!(m.audio.addr_latch, 0x05);
1074        assert!(!m.audio.auto_inc);
1075
1076        // Second write also lands at 0x05 (latch did not advance).
1077        m.cpu_write(0x4800, 0x99);
1078        assert_eq!(m.audio.ram[0x05], 0x99);
1079        assert_eq!(m.audio.addr_latch, 0x05);
1080
1081        // With auto-increment: write 0x80 | 0x05, then 0x55 → addr 0x05
1082        // gets 0x55 and latch advances to 0x06.
1083        m.cpu_write(0xF800, 0x80 | 0x05);
1084        m.cpu_write(0x4800, 0x55);
1085        assert_eq!(m.audio.ram[0x05], 0x55);
1086        assert_eq!(m.audio.addr_latch, 0x06);
1087        assert!(m.audio.auto_inc);
1088
1089        // Next data write lands at 0x06.
1090        m.cpu_write(0x4800, 0x66);
1091        assert_eq!(m.audio.ram[0x06], 0x66);
1092        assert_eq!(m.audio.addr_latch, 0x07);
1093    }
1094
1095    #[test]
1096    fn namco163_address_port_saturates_at_7f() {
1097        // Per the NESdev wiki: the auto-increment "stopping at $7F"
1098        // rather than wrapping.  Verify by walking the latch up to $7F
1099        // and then doing one more data access.
1100        let mut m = namco163_for_audio();
1101        m.cpu_write(0xF800, 0x80 | 0x7F);
1102        m.cpu_write(0x4800, 0xAA); // RAM[0x7F] = 0xAA, latch stays at 0x7F.
1103        assert_eq!(m.audio.ram[0x7F], 0xAA);
1104        assert_eq!(m.audio.addr_latch, 0x7F);
1105        // A second write also lands at 0x7F (saturation, not wrap).
1106        m.cpu_write(0x4800, 0xBB);
1107        assert_eq!(m.audio.ram[0x7F], 0xBB);
1108        assert_eq!(m.audio.addr_latch, 0x7F);
1109        assert_eq!(m.audio.ram[0x00], 0x00, "wrap to $00 must not happen");
1110    }
1111
1112    #[test]
1113    fn namco163_data_port_read_round_trip() {
1114        // Write 0xAB at addr 0x10 with auto-increment, then read it back.
1115        // Read also advances the latch.
1116        let mut m = namco163_for_audio();
1117        m.cpu_write(0xF800, 0x80 | 0x10);
1118        m.cpu_write(0x4800, 0xAB);
1119        // After the write, latch is at 0x11.
1120        // Re-target 0x10 for the read.
1121        m.cpu_write(0xF800, 0x80 | 0x10);
1122        assert_eq!(m.cpu_read(0x4800), 0xAB);
1123        assert_eq!(m.audio.addr_latch, 0x11);
1124    }
1125
1126    #[test]
1127    fn namco163_wavetable_nibble_unpacking() {
1128        // Byte 0xAB at RAM[0x10] → nibble 0x20 = 0xB (low), nibble 0x21
1129        // = 0xA (high).  Verifies the wavetable nibble-fetch helper.
1130        let mut m = namco163_for_audio();
1131        m.cpu_write(0xF800, 0x10);
1132        m.cpu_write(0x4800, 0xAB);
1133        assert_eq!(m.audio.ram[0x10], 0xAB);
1134        #[cfg(feature = "mapper-audio")]
1135        {
1136            assert_eq!(m.audio.fetch_nibble(0x20), 0x0B);
1137            assert_eq!(m.audio.fetch_nibble(0x21), 0x0A);
1138        }
1139    }
1140
1141    #[test]
1142    #[cfg(feature = "mapper-audio")]
1143    fn namco163_channel_count_selection() {
1144        // Bits 6-4 of register $7F encode "channel count - 1".
1145        // C=0 → 1 channel; C=7 → 8 channels.
1146        let mut m = namco163_for_audio();
1147        for c in 0u8..=7 {
1148            n163_write_ram(&mut m, 0x7F, false, c << 4);
1149            assert_eq!(
1150                m.audio.channel_count(),
1151                c + 1,
1152                "C={c} should map to {} channels",
1153                c + 1
1154            );
1155        }
1156    }
1157
1158    #[test]
1159    #[cfg(feature = "mapper-audio")]
1160    fn namco163_channel_frequency_assembly() {
1161        // Channel 8 lives at $78-$7F.  Write freq lo=$78, mid=$7A, hi=$7C.
1162        // hi register's bits 7-2 carry the wave length encoding, so we
1163        // pack length bits as well to exercise the mask.
1164        let mut m = namco163_for_audio();
1165        // Lo = 0x34, mid = 0x12, hi-bits = 0x02, length-bits = 0xFC
1166        // (length = 256 - 0xFC = 4).
1167        n163_write_ram(&mut m, 0x78, false, 0x34);
1168        n163_write_ram(&mut m, 0x7A, false, 0x12);
1169        n163_write_ram(&mut m, 0x7C, false, 0xFC | 0x02);
1170
1171        let freq = m.audio.channel_freq(0x78);
1172        assert_eq!(freq, 0x02_1234, "freq = hi<<16 | mid<<8 | lo");
1173        let length = m.audio.channel_length(0x78);
1174        assert_eq!(length, 4);
1175    }
1176
1177    #[test]
1178    #[cfg(feature = "mapper-audio")]
1179    fn namco163_single_channel_constant_output_then_bipolar_swing() {
1180        // Channel 0 (the always-enabled channel at $78-$7F) with a
1181        // constant wavetable of 0xFF (high nibble 0xF, low nibble 0xF)
1182        // and volume 15 should yield output = (15 - 8) * 15 = +105.
1183        // Length-1 waveform means the index never moves.
1184        let mut m = namco163_for_audio();
1185        // Wavetable byte 0x10 = 0xFF → nibble 0x20 = 0xF, 0x21 = 0xF.
1186        n163_write_ram(&mut m, 0x10, false, 0xFF);
1187        // Channel 8 (the always-enabled, highest-priority channel) regs.
1188        // Wave addr = 0x20 (the nibble we filled).
1189        // Length encoding: 256 - 0xFC = 4 (chosen to keep the test
1190        // robust to phase, since every cycle still reads 0xF).
1191        // Volume = 0x0F, channel-count field = 0 (single channel).
1192        n163_write_ram(&mut m, 0x7C, false, 0xFC); // length=4, freq-hi=0
1193        n163_write_ram(&mut m, 0x7E, false, 0x20); // wave_addr
1194        n163_write_ram(&mut m, 0x7F, false, 0x0F); // volume=15, C=0
1195
1196        let output = m.audio.channel_output(0);
1197        assert_eq!(output, (15 - 8) * 15, "+105 expected for nibble=15, vol=15");
1198        // Mix returns (sum / 1) * NAMCO163_MIX_SCALE = 105 * 261 = 27405
1199        // (v2.1.6 hardware-accurate 6.0x db_n163 level; was 105 * 64).
1200        assert_eq!(m.audio.mix(), 105 * NAMCO163_MIX_SCALE as i16);
1201
1202        // Now swap the wavetable to nibble 0 — output should swing
1203        // negative: (0 - 8) * 15 = -120.
1204        m.cpu_write(0xF800, 0x10);
1205        m.cpu_write(0x4800, 0x00);
1206        assert_eq!(m.audio.channel_output(0), (0 - 8) * 15);
1207        assert!(m.audio.mix() < 0, "negative samples must yield <0 mix");
1208    }
1209
1210    #[test]
1211    #[cfg(feature = "mapper-audio")]
1212    fn namco163_volume_zero_silences_channel() {
1213        // A channel with volume == 0 contributes 0 to the mix
1214        // regardless of the wavetable contents.
1215        let mut m = namco163_for_audio();
1216        n163_write_ram(&mut m, 0x10, false, 0xFF); // wavetable bytes
1217        n163_write_ram(&mut m, 0x7C, false, 0xFC); // length=4
1218        n163_write_ram(&mut m, 0x7E, false, 0x20); // wave_addr=0x20
1219        n163_write_ram(&mut m, 0x7F, false, 0x00); // vol=0, C=0
1220        assert_eq!(m.audio.channel_output(0), 0);
1221        assert_eq!(m.audio.mix(), 0);
1222    }
1223
1224    #[test]
1225    #[cfg(feature = "mapper-audio")]
1226    fn namco163_longwave_256_sample_wave_phase_wraps_and_reads_full_period() {
1227        // The `test_n163_longwave` accuracy criterion: long-period wavetables
1228        // (the case several emulators truncate). RustyNES uses the canonical
1229        // wave-length formula `L = 256 - (reg[base+4] & 0xFC)` and a 64-bit
1230        // phase accumulator wrapped at `L << 16`, so a full 256-sample wave and
1231        // a low frequency address the whole period without aliasing.
1232        let mut m = namco163_for_audio();
1233        // Fill 128 wave-RAM bytes = 256 nibbles with a ramp so every sample
1234        // index is distinguishable: nibble[i] = i & 0x0F.
1235        for byte in 0u8..0x80 {
1236            // low nibble = (2*byte)&0xF, high nibble = (2*byte+1)&0xF.
1237            let lo = (2 * byte) & 0x0F;
1238            let hi = (2 * byte + 1) & 0x0F;
1239            n163_write_ram(&mut m, byte, false, (hi << 4) | lo);
1240        }
1241        // Channel 8 ($78-$7F). N163 register layout (per Mesen `SoundReg`):
1242        // base+0 = freq lo, +2 = freq mid, +4 = freq hi (bits 0-1) + wave
1243        // length (bits 2-7), +6 = wave addr, +7 = volume. Set a frequency that
1244        // advances the phase by exactly one sample per clock update
1245        // (freq = 1<<16, i.e. freq-hi bit set) while keeping the wave length at
1246        // the max 256 (`256 - (reg & 0xFC)` with the length bits zero), then
1247        // step the wave across its full period and confirm every one of the
1248        // 256 sample indices is reached (no early wrap, no aliasing) — the
1249        // hallmark long-period behaviour.
1250        n163_write_ram(&mut m, 0x78, false, 0x00); // freq lo = 0
1251        n163_write_ram(&mut m, 0x7A, false, 0x00); // freq mid = 0
1252        n163_write_ram(&mut m, 0x7C, false, 0x01); // freq hi = 1 (-> 0x10000), length bits 0 -> L=256
1253        n163_write_ram(&mut m, 0x7E, false, 0x00); // wave_addr = 0
1254        n163_write_ram(&mut m, 0x7F, false, 0x0F); // volume=15, channel-count=0
1255        assert_eq!(
1256            m.audio.channel_length(0x78),
1257            256,
1258            "L must be 256, not truncated"
1259        );
1260        let mut seen = [false; 256];
1261        // N163 advances one channel every 15 CPU cycles; 256 samples * 15 = 3840
1262        // cycles cover the whole period, plus margin.
1263        for _ in 0..(256 * 15 + 15) {
1264            let idx = ((m.audio.channel_phase(0x78) >> 16) % 256) as usize;
1265            seen[idx] = true;
1266            m.audio.clock();
1267        }
1268        assert!(
1269            seen.iter().all(|&s| s),
1270            "long-period wave must reach every one of the 256 sample indices"
1271        );
1272    }
1273
1274    #[test]
1275    #[cfg(feature = "mapper-audio")]
1276    fn namco163_clock_advances_only_active_channel() {
1277        // Two-channel setup: C=1, so channels 8 and 7 (bases $78, $70)
1278        // are active.  Set freq=0x01_0000 on channel 8 (so each tick
1279        // advances phase by 1 << 16) and freq=0 on channel 7.  After
1280        // 30 CPU cycles (= 2 audio updates), phase[ch=8] should have
1281        // advanced exactly once (the round-robin alternates 8/7/8/7...).
1282        let mut m = namco163_for_audio();
1283        // Channel 8 freq = 0x01_0000 → hi=01, mid=00, lo=00.
1284        n163_write_ram(&mut m, 0x78, false, 0x00); // freq lo
1285        n163_write_ram(&mut m, 0x7A, false, 0x00); // freq mid
1286        // length=4 (256 - 0xFC), freq-hi=01.
1287        n163_write_ram(&mut m, 0x7C, false, 0xFC | 0x01);
1288        n163_write_ram(&mut m, 0x7F, false, 0x10); // C=1 → 2 channels
1289        // Channel 7 freq = 0.
1290        n163_write_ram(&mut m, 0x70, false, 0x00);
1291        n163_write_ram(&mut m, 0x72, false, 0x00);
1292        n163_write_ram(&mut m, 0x74, false, 0xFC);
1293
1294        // 15 cycles → channel 8 advances by 0x01_0000.
1295        for _ in 0..15 {
1296            m.notify_cpu_cycle();
1297        }
1298        let phase_ch8 = m.audio.channel_phase(0x78);
1299        // length=4, modulus = 4 << 16 = 0x40000, so 0x10000 stays.
1300        assert_eq!(phase_ch8, 0x0001_0000);
1301        let phase_ch7 = m.audio.channel_phase(0x70);
1302        assert_eq!(phase_ch7, 0, "ch7 must not advance on the first slot");
1303
1304        // Next 15 cycles → channel 7 advances (by 0, so still 0); ch8
1305        // unchanged.
1306        for _ in 0..15 {
1307            m.notify_cpu_cycle();
1308        }
1309        assert_eq!(m.audio.channel_phase(0x78), 0x0001_0000);
1310        assert_eq!(m.audio.channel_phase(0x70), 0);
1311    }
1312
1313    #[test]
1314    #[cfg(feature = "mapper-audio")]
1315    fn namco163_sound_disable_bit_silences_mix() {
1316        // $E000 bit 6 set → audio chip is silenced.  Even with a
1317        // non-zero wavetable and volume, mix_audio returns 0.
1318        let mut m = namco163_for_audio();
1319        n163_write_ram(&mut m, 0x10, false, 0xFF);
1320        n163_write_ram(&mut m, 0x7C, false, 0xFC);
1321        n163_write_ram(&mut m, 0x7E, false, 0x20);
1322        n163_write_ram(&mut m, 0x7F, false, 0x0F);
1323        assert_ne!(m.mix_audio(), 0);
1324        // Set sound-disable: $E000 with bit 6 = 1.  Bits 0-5 also write
1325        // PRG bank 0; we just need the bit 6.
1326        m.cpu_write(0xE000, 0x40);
1327        assert!(m.sound_disabled);
1328        assert_eq!(m.mix_audio(), 0);
1329        // Clearing it re-enables.
1330        m.cpu_write(0xE000, 0x00);
1331        assert!(!m.sound_disabled);
1332        assert_ne!(m.mix_audio(), 0);
1333    }
1334
1335    #[test]
1336    fn namco163_save_state_v2_round_trip() {
1337        // v2 → v2 round-trip preserves the full audio state.
1338        let mut donor = namco163_for_audio();
1339        n163_write_ram(&mut donor, 0x10, true, 0xAB);
1340        n163_write_ram(&mut donor, 0x7F, false, 0x35); // C=3 → 4 channels, vol=5
1341        donor.cpu_write(0xE000, 0x40); // sound disable
1342        let blob = donor.save_state();
1343        // v3 since v2.7.2 (the CHR-RAM-disable tail byte), v4 since v2.9.2
1344        // (the cartridge CHR-RAM tail); the audio tail it exercises is
1345        // unchanged from v2.
1346        assert_eq!(blob[0], N163_SECTION_VERSION, "current tag expected");
1347
1348        let mut target = namco163_for_audio();
1349        target.load_state(&blob).unwrap();
1350        assert_eq!(target.audio.ram[0x10], 0xAB);
1351        assert_eq!(target.audio.ram[0x7F], 0x35);
1352        assert!(target.sound_disabled);
1353        // addr_latch after the writes: $7F (we wrote $7F last,
1354        // auto_inc=false, so the latch stayed at $7F).
1355        assert_eq!(target.audio.addr_latch, 0x7F);
1356    }
1357
1358    #[test]
1359    fn namco163_mapper_audio_off_path_latches_state_but_stays_silent() {
1360        // Mirrors the Sunsoft 5B feature-off test: the register decoders
1361        // run regardless of `mapper-audio`, so writes still land in the
1362        // internal RAM and the address-port latch advances.  With the
1363        // feature off, `notify_cpu_cycle` does not advance any phase
1364        // counters and `mix_audio` returns 0.
1365        let mut m = namco163_for_audio();
1366        // Address-port write + data-port write contract — works with
1367        // the feature off, because the decoders are unconditional.
1368        m.cpu_write(0xF800, 0x80 | 0x05);
1369        m.cpu_write(0x4800, 0x42);
1370        assert_eq!(m.audio.ram[0x05], 0x42);
1371        assert_eq!(m.audio.addr_latch, 0x06);
1372        assert!(m.audio.auto_inc);
1373
1374        // Phase counters stay at zero whether or not we call clock()
1375        // (with the feature off, notify_cpu_cycle skips the clock; with
1376        // the feature on, we haven't touched the freq registers so the
1377        // phase still doesn't advance from the zero state).  Verify the
1378        // zero-init invariant directly.
1379        for _ in 0..256 {
1380            m.notify_cpu_cycle();
1381        }
1382        // Phase regs are at offsets +1/+3/+5 of each channel slot.
1383        for ch_base in (0x40..=0x78).step_by(8) {
1384            assert_eq!(m.audio.ram[ch_base + 1], 0, "phase lo @ {ch_base:#x}");
1385            assert_eq!(m.audio.ram[ch_base + 3], 0, "phase mid @ {ch_base:#x}");
1386            assert_eq!(m.audio.ram[ch_base + 5], 0, "phase hi @ {ch_base:#x}");
1387        }
1388    }
1389
1390    // ---- v2.7.2: nametable + CIRAM-as-CHR (core audit IMP-11, §5.2) --------
1391    //
1392    // From nesdev_wiki/INES_Mapper_019.xhtml §"CHR and NT Select": $C000,
1393    // $C800, $D000, $D800 select the four nametable quadrants; a value below
1394    // $E0 is a 1 KiB CHR-ROM page, $E0-$FF is console CIRAM (even = A, odd =
1395    // B). In $8000-$BFFF, $E0-$FF maps CIRAM as CHR-RAM unless $E800 bit 6
1396    // (pattern $0000-$0FFF) or bit 7 ($1000-$1FFF) is set.
1397
1398    fn n163(chr_1k: usize) -> Namco163 {
1399        Namco163::new(synth(8), synth_chr(chr_1k), Mirroring::Vertical).unwrap()
1400    }
1401
1402    #[test]
1403    fn nametable_registers_select_ciram_pages_per_quadrant() {
1404        let mut m = n163(0x100);
1405        // All four quadrants on CIRAM A, then write through $2000.
1406        for reg in [0xC000u16, 0xC800, 0xD000, 0xD800] {
1407            m.cpu_write(reg, 0xE0);
1408        }
1409        m.ppu_write(0x2000, 0x3C);
1410        assert_eq!(
1411            m.ppu_read(0x2C00),
1412            0x3C,
1413            "all four quadrants are the same page"
1414        );
1415        m.cpu_write(0xD800, 0xE1); // quadrant 3 -> CIRAM B
1416        assert_ne!(m.ppu_read(0x2C00), 0x3C, "quadrant 3 now reads page B");
1417        m.ppu_write(0x2C00, 0x4D);
1418        m.cpu_write(0xC000, 0xE1);
1419        assert_eq!(m.ppu_read(0x2000), 0x4D, "page B through quadrant 0");
1420    }
1421
1422    #[test]
1423    fn a_nametable_value_below_e0_is_a_read_only_chr_rom_page() {
1424        let mut m = n163(0x100);
1425        m.cpu_write(0xC800, 0x05); // quadrant 1 -> CHR-ROM page 5
1426        assert_eq!(m.ppu_read(0x2400), 5, "the page's first byte is its index");
1427        m.ppu_write(0x2400, 0x99);
1428        assert_eq!(m.ppu_read(0x2400), 5, "ROM is not written");
1429    }
1430
1431    #[test]
1432    fn power_on_nametables_follow_the_header_mirroring() {
1433        // The registers power on as the header's layout, so a game that
1434        // relied on header mirroring before v2.7.2 renders exactly as it did.
1435        let mut v = n163(0x100);
1436        v.ppu_write(0x2000, 0x11);
1437        assert_eq!(
1438            v.ppu_read(0x2800),
1439            0x11,
1440            "vertical: $2000 and $2800 share page A"
1441        );
1442        let mut h = Namco163::new(synth(8), synth_chr(0x100), Mirroring::Horizontal).unwrap();
1443        h.ppu_write(0x2000, 0x22);
1444        assert_eq!(
1445            h.ppu_read(0x2400),
1446            0x22,
1447            "horizontal: $2000 and $2400 share page A"
1448        );
1449    }
1450
1451    #[test]
1452    fn chr_values_e0_and_up_map_ciram_as_chr_ram_unless_e800_disables_it() {
1453        let mut m = n163(0x100);
1454        m.cpu_write(0xE800, 0x00); // CHR-RAM enabled for both pattern halves
1455        m.cpu_write(0xC000, 0xE0); // quadrant 0 = CIRAM A, to observe it
1456        m.cpu_write(0x8000, 0xE0); // pattern $0000-$03FF = CIRAM A
1457        m.ppu_write(0x0005, 0x6B);
1458        assert_eq!(m.ppu_read(0x0005), 0x6B, "writable as CHR-RAM");
1459        assert_eq!(m.ppu_read(0x2005), 0x6B, "and it IS the nametable page A");
1460        m.cpu_write(0xE800, 0x40); // bit 6 -> low half uses CHR-ROM for $E0-$FF
1461        assert_eq!(
1462            m.ppu_read(0x0000),
1463            0xE0,
1464            "CHR-ROM page $E0 (first byte = index)"
1465        );
1466        m.cpu_write(0xA000, 0xE1); // pattern $1000 = CIRAM B (bit 7 clear)
1467        m.ppu_write(0x1001, 0x7C);
1468        assert_eq!(m.ppu_read(0x1001), 0x7C);
1469        m.cpu_write(0xE800, 0xC0);
1470        assert_eq!(
1471            m.ppu_read(0x1000),
1472            0xE1,
1473            "bit 7 -> CHR-ROM for the high half"
1474        );
1475    }
1476
1477    #[test]
1478    fn e800_disable_bits_do_not_disturb_its_prg_bank() {
1479        let mut m = n163(0x100);
1480        m.cpu_write(0xE800, 0xC3);
1481        assert_eq!(m.cpu_read(0xA000), 3, "PRG page 3 at $A000");
1482    }
1483
1484    /// v2.9.8 (ADR 0042): only the current (v4) layout loads. A v2 blob
1485    /// (before v2.7.2's CIRAM-as-CHR) used to load with CHR-RAM disabled and
1486    /// its nametable layout rebuilt from the header, and a v3 blob (before
1487    /// v2.9.2's CHR-RAM tail) with the CHR-RAM left as it was.
1488    #[test]
1489    fn pre_v4_save_states_are_refused() {
1490        let m = Namco163::new(synth(8), Box::new([]), Mirroring::Vertical).unwrap();
1491        let blob = m.save_state();
1492        let mut v3 = blob.clone();
1493        v3.truncate(v3.len() - m.chr_rom.len());
1494        v3[0] = 3;
1495        let mut v2 = v3.clone();
1496        v2.truncate(v2.len() - 2);
1497        v2[0] = 2;
1498        let mut n = Namco163::new(synth(8), Box::new([]), Mirroring::Vertical).unwrap();
1499        for (v, old) in [(3u8, &v3), (2, &v2)] {
1500            assert!(matches!(
1501                n.load_state(old),
1502                Err(MapperError::UnsupportedVersion(got)) if got == v
1503            ));
1504        }
1505        n.load_state(&blob).expect("the current blob loads");
1506    }
1507
1508    #[test]
1509    fn a_truncated_v4_blob_is_refused_before_any_state_changes() {
1510        let mut m = n163(0x100);
1511        m.cpu_write(0xC000, 0x05);
1512        let blob = m.save_state();
1513        let mut n = n163(0x100);
1514        assert!(matches!(
1515            n.load_state(&blob[..blob.len() - 1]),
1516            Err(MapperError::WrongLength { .. })
1517        ));
1518        assert_eq!(n.nta, Namco163::nta_for(Mirroring::Vertical), "untouched");
1519        n.load_state(&blob).expect("the whole blob loads");
1520        assert_eq!(n.nta[0], 0x05);
1521    }
1522
1523    #[test]
1524    fn a_current_blob_keeps_ciram_ownership_as_saved() {
1525        let m = Namco163::new(synth(8), synth_chr(0x100), Mirroring::Horizontal).unwrap();
1526        let mut o = Namco163::new(synth(8), synth_chr(0x100), Mirroring::Horizontal).unwrap();
1527        o.load_state(&m.save_state())
1528            .expect("the current blob loads");
1529        assert_eq!(o.nametable_fetch(0x2000), Some(0), "CIRAM is the board's");
1530    }
1531
1532    /// v2.9.2 cartridge-RAM sweep: the section carries the 8 KiB CHR-RAM of a
1533    /// board with no CHR-ROM. The whole-machine pin is
1534    /// `rustynes_core::nes::tests::every_board_snapshot_carries_cartridge_ram`.
1535    #[test]
1536    fn n163_save_state_carries_chr_ram() {
1537        let mut m = Namco163::new(synth(8), Box::new([]), Mirroring::Vertical).unwrap();
1538        m.chr_rom[0x0000] = 0x11;
1539        m.chr_rom[0x1FFF] = 0x22;
1540        let blob = m.save_state();
1541        let mut n = Namco163::new(synth(8), Box::new([]), Mirroring::Vertical).unwrap();
1542        n.load_state(&blob).expect("round-trip");
1543        assert_eq!(n.chr_rom[0x0000], 0x11);
1544        assert_eq!(n.chr_rom[0x1FFF], 0x22);
1545    }
1546
1547    /// A v4 blob one byte short (inside the CHR-RAM tail) is rejected before
1548    /// any state changes.
1549    #[test]
1550    fn n163_truncated_chr_ram_tail_is_rejected() {
1551        let mut m = Namco163::new(synth(8), Box::new([]), Mirroring::Vertical).unwrap();
1552        m.cpu_write(0xE000, 0x03);
1553        let blob = m.save_state();
1554        let mut n = Namco163::new(synth(8), Box::new([]), Mirroring::Vertical).unwrap();
1555        let err = n
1556            .load_state(&blob[..blob.len() - 1])
1557            .expect_err("a truncated v4 blob must be rejected");
1558        assert!(matches!(err, MapperError::WrongLength { .. }), "{err:?}");
1559        assert_eq!(n.prg[0], 0, "untouched");
1560    }
1561}