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}