Skip to main content

rustynes_mappers/
m069_sunsoft_fme7.rs

1// SPDX-License-Identifier: GPL-3.0-or-later
2//
3// Provenance: the Sunsoft FME-7 / 5B audio detail is derived from Mesen2 (GPL-3.0-or-later), `NesSoundMixer::GetOutputVolume` (the 5B `* 15` output weight) over `Sunsoft5bAudio::_volumeLut`, and cross-referenced with Nestopia UE (GPL-2.0-or-later; no upstream file is recorded). See docs/originality-and-provenance.md (Section 1)
4// and NOTICE for the complete, audited derivation record.
5//! Sunsoft FME-7 (mapper 69) -- banking, the CPU-cycle IRQ counter, and the
6//! on-cart Sunsoft 5B audio chip.
7//!
8//! The FME-7 is the mapper ASIC; the 5B is the AY-3-8910-derivative sound
9//! chip packaged with it on the Japanese Gimmick! cartridge. This module owns
10//! both, because the 5B is addressed through the same `$C000`/`$E000`
11//! command/parameter port pair the mapper uses.
12//!
13//! The 5B is three square-wave tone channels, a shared 5-bit LFSR noise
14//! generator, and a shared envelope generator, mixed through a *logarithmic*
15//! DAC ([`SUNSOFT5B_LOG_VOL`]) rather than the linear one a naive port would
16//! use. Shape and absolute level are separately pinned: the step law by a
17//! unit test, the level by [`SUNSOFT5B_MIX_SCALE_NUM`] /
18//! [`SUNSOFT5B_MIX_SCALE_DEN`] against the `db_5b` oracle ROM.
19//!
20//! Audio is gated behind the `mapper-audio` Cargo feature (default ON); with
21//! it off the register decoders still latch and the oscillators freeze, so a
22//! save state written by an audio-enabled build still loads (ADR 0004).
23//! [`Sunsoft5BAudio`] is re-used verbatim by the NSF expansion path
24//! (`nsf_expansion.rs`).
25//!
26//! See `docs/mappers.md` and `docs/apu-2a03.md` §Expansion-audio levels.
27
28#![allow(
29    clippy::cast_possible_truncation,
30    clippy::cast_lossless,
31    clippy::missing_const_for_fn,
32    clippy::needless_pass_by_ref_mut,
33    clippy::manual_range_patterns,
34    clippy::match_same_arms,
35    clippy::struct_excessive_bools,
36    clippy::doc_markdown,
37    clippy::range_plus_one,
38    clippy::single_match_else,
39    clippy::bool_to_int_with_if,
40    clippy::unnested_or_patterns,
41    clippy::single_match,
42    clippy::doc_lazy_continuation,
43    clippy::too_long_first_doc_paragraph
44)]
45
46use crate::cartridge::Mirroring;
47use crate::mapper::{Mapper, MapperCaps, MapperError};
48use alloc::{boxed::Box, vec::Vec};
49use alloc::{format, vec};
50
51const PRG_BANK_8K: usize = 0x2000;
52const CHR_BANK_1K: usize = 0x0400;
53const CHR_BANK_8K: usize = 0x2000;
54const NAMETABLE_SIZE: usize = 0x0400;
55const NAMETABLE_SIZE_U16: u16 = 0x0400;
56
57/// Version byte this board writes in its mapper save-state section.
58///
59/// **v1** carried the command latch, the banking, PRG-RAM-enable, mirroring
60/// and IRQ registers, the 8 KiB PRG-RAM and the 2 KiB nametable RAM. **v2**
61/// appended the Sunsoft 5B audio tail ([`Sunsoft5BAudio::TAIL_LEN`] bytes).
62/// Neither carried the 8 KiB CHR-RAM of a cartridge with no CHR-ROM, and the
63/// `.rns` container has no other section that carries cartridge RAM -- so
64/// every save-state load, rewind step, run-ahead frame and netplay rollback
65/// kept the running game's CHR-RAM instead of the saved one (the v2.9.2
66/// cartridge-RAM sweep; the same omission core audit AUD-02 found on the
67/// Konami VRC boards). **v3** (v2.9.2) appends that CHR-RAM after the audio
68/// tail. Since v2.9.8 (ADR 0042) `load_state` reads v3 only; a v1/v2 blob,
69/// which it used to load with the CHR-RAM untouched, is refused.
70const FME7_SECTION_VERSION: u8 = 3;
71
72fn nametable_offset(addr: u16, mirroring: Mirroring) -> usize {
73    let table = (((addr - 0x2000) / NAMETABLE_SIZE_U16) & 0x03) as u8;
74    let local = (addr as usize) & (NAMETABLE_SIZE - 1);
75    let physical = mirroring.physical_bank(table);
76    physical * NAMETABLE_SIZE + local
77}
78
79/// 16-entry logarithmic volume DAC, ~3 dB per 4-bit step (= 1.5 dB per
80/// 5-bit step in the underlying chip).  Peak chosen so that three channels
81/// summed at maximum volume stay comfortably inside the `i16` headroom the
82/// APU mixer expects.
83///
84/// This table is the DAC **shape** only — each step is `1.1885^2 ≈ 1.4126x`,
85/// the +1.5 dB×2 logarithmic law; `LUT[12] = 668`, `LUT[15] = 1882`,
86/// cross-checked against Mesen2's `Sunsoft5bAudio::_volumeLut` `[63, 177]` and
87/// tetanes. The absolute mixer **level** lives in
88/// [`SUNSOFT5B_MIX_SCALE_NUM`], deliberately separate so each can be pinned by
89/// its own oracle: the shape by
90/// `sunsoft5b_volume_dac_follows_logarithmic_step_law` (a unit test on these
91/// ratios), the level by `level_db_5b` (the `db_5b` comparison ROM).
92///
93/// Our entries are a finer scaling of the same law than Mesen2's `uint8_t`
94/// table, which truncates hard at the bottom (its `LUT[1]` is `1`). Keeping the
95/// finer table preserves the step ratios that the unit test asserts.
96///
97/// HISTORY (v2.1.6 → v2.2.3): the absolute level used to be an explicit,
98/// documented gap — not because the value was unknown but because
99/// `Mapper::mix_audio` returned `i16` and the correct value does not fit. A1
100/// widened that return to `i32` and calibrated the level; see
101/// [`SUNSOFT5B_MIX_SCALE_NUM`] and `docs/accuracy-ledger.md`.
102///
103/// Per the NESdev "Sunsoft 5B audio" page, the chip's DAC has a **1.5 dB step
104/// on the 5-bit signal** — "some emulator implementations based on the
105/// AY-3-8910 instead treat it as a 4-bit signal with a 3 dB per step curve",
106/// which the wiki flags as the *approximation*, not the exact behavior.
107///
108/// A channel driven by its **fixed 4-bit volume** register genuinely steps
109/// 3 dB (it selects every other level of the 5-bit DAC), so this 16-entry
110/// table — the 4-bit projection — is exact for fixed-volume tones. A channel
111/// driven by the **envelope generator** produces the full 5-bit level and must
112/// use the 32-entry [`SUNSOFT5B_LOG_VOL32`] table for the true 1.5 dB/step
113/// resolution (v2.2.7 "Timbre II": before, the envelope was reduced to 4-bit
114/// via `e >> 1`, collapsing it onto this coarser 3 dB curve — the approximation
115/// the wiki names; nestopia and rustico use the exact 5-bit DAC, which this now
116/// matches). The odd entries of `SUNSOFT5B_LOG_VOL32` equal this table exactly.
117#[cfg_attr(not(feature = "mapper-audio"), allow(dead_code))]
118const SUNSOFT5B_LOG_VOL: [i32; 16] = [
119    0, 15, 21, 30, 42, 59, 84, 119, 168, 237, 335, 473, 668, 944, 1333, 1882,
120];
121
122/// Exact **5-bit, 1.5 dB/step** logarithmic DAC (v2.2.7 "Timbre II") — the
123/// NESdev-authoritative Sunsoft-5B envelope curve, matching nestopia / rustico.
124/// Indexed by the envelope generator's full 5-bit output (0..=31). Each step is
125/// `×1.1885` (= +1.5 dB); the finest quantization is the same 1882-scaled law as
126/// [`SUNSOFT5B_LOG_VOL`], so the two never drift. Per the wiki, envelope levels
127/// `e=0` and `e=1` both map to silence. The **odd** entries reproduce the 4-bit
128/// [`SUNSOFT5B_LOG_VOL`] table exactly (`LOG_VOL32[2v+1] == LOG_VOL[v]`), so a
129/// fixed-volume channel and an envelope channel resting on the same level agree
130/// to the bit — guaranteed by the `log_vol32_odd_entries_match_4bit` unit test.
131#[cfg_attr(not(feature = "mapper-audio"), allow(dead_code))]
132const SUNSOFT5B_LOG_VOL32: [i32; 32] = [
133    0, 0, 13, 15, 18, 21, 25, 30, 35, 42, 50, 59, 71, 84, 100, 119, 141, 168, 199, 237, 282, 335,
134    398, 473, 562, 668, 794, 944, 1122, 1333, 1584, 1882,
135];
136
137/// Mixed centering bias: subtracted from the scaled linear sum before emitting
138/// the i32 sample.  We use a *constant zero* — the APU mixer's chained
139/// high-pass filters (90 Hz / 440 Hz, see `rustynes-apu::mixer::OnePole`)
140/// remove any steady DC component downstream, and the 5B's linear sum
141/// can swing from 0 (all channels muted) up to ~104 k (three channels at
142/// peak volume + tone high, post-[`SUNSOFT5B_MIX_SCALE_NUM`]).  Keeping the
143/// constant named here makes a future numerical bias easy to add if
144/// AccuracyCoin's mixed-output tests ever ask for it.
145#[cfg_attr(not(feature = "mapper-audio"), allow(dead_code))]
146const SUNSOFT5B_DC_BIAS: i32 = 0;
147
148/// v2.2.3 (A1) — absolute mixer level for the 5B, as a rational
149/// `NUM / DEN = 2549 / 138 ≈ 18.471`.
150///
151/// [`SUNSOFT5B_LOG_VOL`] carries the DAC *shape* (the +1.5 dB x2 law); this
152/// carries the *level*, the same split `VRC6_MIX_SCALE` /
153/// `NAMCO163_MIX_SCALE` / the MMC5 `650/40` pair use. Separating them is what
154/// lets the shape stay pinned by its own unit test while the level is pinned
155/// by a ROM oracle.
156///
157/// **Target, derived from Mesen2 (the project's accuracy bar) rather than from
158/// our own prior numbers** (see this file's `Provenance` header and
159/// `docs/originality-and-provenance.md` section 1). v2.2.5 reworded this to
160/// "calibrated against Mesen2 ... as an oracle", which describes a black-box
161/// comparison and so understated a derivation; v3.0.1 restored it
162/// (maintainer, 2026-10-06). Using the standard
163/// blargg nonlinear-mixer approximation (nesdev "APU Mixer"), a full-volume
164/// 2A03 square is `(95.88 * 5000) / (8128/15 + 100) = 746.9` units, and the 5B
165/// is summed with weight `* 15` over the documented 5B log-DAC volume table
166/// (`= (uint8_t)1.1885^(2i)`, so `LUT[12] = 63`, `LUT[15] = 177`). The
167/// `db_5b` ROM compares a **volume-12** 5B square against that square:
168///
169/// ```text
170///   volume 12: 63 * 15 / 746.9 = 1.265x   <- the db_5b oracle target
171///   volume 15: 177 * 15 / 746.9 = 3.554x  <- full-scale, the i16 blocker
172/// ```
173///
174/// This independently reproduces the ~1.27x / ~3.56x figures the accuracy
175/// ledger recorded when the calibration was deferred. The NESdev wiki and the
176/// in-repo technical references describe the chip but pin no absolute level —
177/// expansion-audio levels are a mixer convention, not a hardware spec, which
178/// is why the reference emulator is the oracle here.
179///
180/// The scale itself is measured, not computed: with the shape table above and
181/// the bus's `/65536` contract, `db_5b` measured `0.0685x` before this change,
182/// so `1.2652 / 0.0685 = 18.471`. That is the same measure-then-fix method
183/// `NAMCO163_MIX_SCALE` used for its ~12 dB correction.
184///
185/// **This is why `Mapper::mix_audio` had to widen to `i32` first.** A
186/// volume-15 tone now reaches `1882 * 18.471 = 34,761` — already past
187/// `i16::MAX` for ONE channel — and three simultaneous full-volume tones
188/// (Gimmick!, Hebereke) reach ~104 k, 3.2x over. The level could not be
189/// corrected while the return type was `i16`; that, not the arithmetic, was
190/// the actual blocker.
191#[cfg_attr(not(feature = "mapper-audio"), allow(dead_code))]
192pub(crate) const SUNSOFT5B_MIX_SCALE_NUM: i32 = 2549;
193/// Denominator of [`SUNSOFT5B_MIX_SCALE_NUM`].
194#[cfg_attr(not(feature = "mapper-audio"), allow(dead_code))]
195pub(crate) const SUNSOFT5B_MIX_SCALE_DEN: i32 = 138;
196
197/// One of the 5B's three square-wave tone channels.
198///
199/// The chip toggles the output level every `16 * TP` CPU cycles (TP = the
200/// 12-bit period from registers `$00/$01` for channel A, etc.).  Per wiki,
201/// a `TP` of 0 behaves identically to `TP` of 1, so the divide path uses
202/// `max(TP, 1)` to avoid both a divide-by-zero and a degenerate "always
203/// toggling" case.  None of the 5B's generators can be halted — disabling
204/// a channel in the mixer only mutes its output, the internal counters
205/// keep running.
206#[derive(Clone, Default)]
207struct Sunsoft5BTone {
208    /// 12-bit reload period.
209    period: u16,
210    /// Internal half-period countdown in CPU clocks (counts down from
211    /// `16 * period`; on hitting 0 the level toggles and the counter
212    /// reloads).
213    counter: u32,
214    /// Current square-wave output level (0 or 1).
215    level: u8,
216}
217
218#[cfg_attr(not(feature = "mapper-audio"), allow(dead_code))]
219impl Sunsoft5BTone {
220    /// Effective half-period, in CPU clocks (`max(period, 1) * 16`).
221    fn half_period(&self) -> u32 {
222        u32::from(self.period.max(1)) * 16
223    }
224
225    /// One CPU cycle.  Counters always run, even when the channel is
226    /// muted by the mixer register.
227    fn clock(&mut self) {
228        if self.counter == 0 {
229            self.counter = self.half_period();
230            self.level ^= 1;
231        } else {
232            self.counter -= 1;
233        }
234    }
235}
236
237/// 17-bit LFSR noise generator with taps at bits 16 and 13 (per the AY-
238/// 3-8910 datasheet, as cited on the NESdev wiki).
239#[derive(Clone)]
240struct Sunsoft5BNoise {
241    /// 5-bit period reload (`$06`).
242    period: u8,
243    /// Half-period countdown in CPU clocks.
244    counter: u32,
245    /// 17-bit LFSR state; output is bit 0.
246    lfsr: u32,
247}
248
249impl Default for Sunsoft5BNoise {
250    fn default() -> Self {
251        // The AY's LFSR powers up with all bits set; if it ever reached 0
252        // it would lock up (no taps could ever flip a bit back in).
253        Self {
254            period: 0,
255            counter: 0,
256            lfsr: 0x1FFFF,
257        }
258    }
259}
260
261#[cfg_attr(not(feature = "mapper-audio"), allow(dead_code))]
262impl Sunsoft5BNoise {
263    fn half_period(&self) -> u32 {
264        u32::from(self.period.max(1)) * 16
265    }
266
267    fn clock(&mut self) {
268        if self.counter == 0 {
269            self.counter = self.half_period();
270            // 17-bit LFSR, taps at bits 16 and 13 (XOR).  Shift right,
271            // feed the XOR back into bit 16.
272            let fb = ((self.lfsr >> 16) ^ (self.lfsr >> 13)) & 1;
273            self.lfsr = (self.lfsr >> 1) | (fb << 16);
274            self.lfsr &= 0x1FFFF;
275        } else {
276            self.counter -= 1;
277        }
278    }
279
280    /// Current noise output bit (0 or 1).
281    fn level(&self) -> u8 {
282        (self.lfsr & 1) as u8
283    }
284}
285
286/// Envelope generator: 16-bit period, 32-step output, 10 distinct shapes.
287///
288/// Writing the shape register (`$0D`) **restarts** the envelope from its
289/// shape-determined starting position.  The wiki gives the shapes in
290/// terms of four bits `CAaH` (continue/attack/alternate/hold); we
291/// implement them as a small state machine — `attack` chooses the
292/// starting direction, `alternate` flips it after each ramp, `continue`
293/// gates whether to keep going past the first ramp, and `hold` freezes
294/// (with `attack XOR alternate` deciding the held value).
295#[derive(Clone, Default)]
296struct Sunsoft5BEnvelope {
297    /// 16-bit reload period.
298    period: u16,
299    /// Half-step countdown in CPU clocks (the wiki gives step frequency
300    /// `clock / (16 * period)`).
301    counter: u32,
302    /// Shape register value (`$0D`).  Only the low 4 bits matter.
303    shape: u8,
304    /// Current 5-bit envelope level (0..=31).
305    level: u8,
306    /// Internal direction: +1 for rising, -1 for falling.
307    rising: bool,
308    /// Set once the envelope has completed its first ramp and decided to
309    /// hold (per `continue=0` or `hold=1` after the first ramp/alternate).
310    holding: bool,
311}
312
313#[cfg_attr(not(feature = "mapper-audio"), allow(dead_code))]
314impl Sunsoft5BEnvelope {
315    /// Effective step interval in CPU clocks.
316    fn step_period(&self) -> u32 {
317        u32::from(self.period.max(1)) * 16
318    }
319
320    /// Write `$0D` — latches the shape AND restarts the envelope.
321    fn write_shape(&mut self, value: u8) {
322        self.shape = value & 0x0F;
323        // Attack bit (bit 2) sets the initial direction.  When attack=1,
324        // start at 0 going up; when attack=0, start at 31 going down.
325        let attack = (self.shape & 0x04) != 0;
326        self.rising = attack;
327        self.level = if attack { 0 } else { 31 };
328        self.counter = self.step_period();
329        self.holding = false;
330    }
331
332    /// One CPU cycle.  Runs forever (cannot be halted) but emits silence
333    /// while `holding == true` and `continue == 0`.
334    fn clock(&mut self) {
335        if self.counter == 0 {
336            self.counter = self.step_period();
337            self.step();
338        } else {
339            self.counter -= 1;
340        }
341    }
342
343    fn step(&mut self) {
344        if self.holding {
345            return;
346        }
347        if self.rising {
348            if self.level < 31 {
349                self.level += 1;
350                return;
351            }
352        } else if self.level > 0 {
353            self.level -= 1;
354            return;
355        }
356        // We reached the end of a ramp.  Decide what to do based on the
357        // four shape bits.  Per the wiki:
358        //   continue=0 (bit 3): the envelope holds at 0 regardless of the
359        //                       other bits after one ramp.
360        //   hold=1 (bit 0):     hold at the current value (possibly flipped
361        //                       by alternate).
362        //   alternate=1 (bit 1): reverse direction every ramp.
363        let cont = (self.shape & 0x08) != 0;
364        let alternate = (self.shape & 0x02) != 0;
365        let hold = (self.shape & 0x01) != 0;
366        if !cont {
367            self.level = 0;
368            self.holding = true;
369            return;
370        }
371        if hold {
372            if alternate {
373                // /\___ etc.: flip the final level once.
374                self.level = if self.rising { 0 } else { 31 };
375            }
376            self.holding = true;
377            return;
378        }
379        if alternate {
380            self.rising = !self.rising;
381        } else {
382            // Pure sawtooth: snap back to the starting level.
383            self.level = if self.rising { 0 } else { 31 };
384        }
385    }
386
387    /// Current 5-bit envelope output (0..=31).
388    const fn output(&self) -> u8 {
389        self.level
390    }
391}
392
393/// 5B audio chip state: 16-byte register file, 3 tone channels, noise
394/// generator, envelope generator, plus the address-latch byte that the
395/// `$C000-$DFFF` writes use to select the next `$E000-$FFFF` data target.
396#[derive(Clone, Default)]
397pub(crate) struct Sunsoft5BAudio {
398    /// Latched 4-bit register index from the most recent `$C000-$DFFF`
399    /// write.  Bits 7-4 of the high-byte are silently ignored (per the
400    /// NESdev wiki: writes with bits 7-4 nonzero are inhibited; we model
401    /// only the inhibit-on-high-bits case by masking to 4 bits, since no
402    /// known software relies on the high bits).
403    addr_latch: u8,
404    /// Raw 16-byte register file (mostly for save-state round-trip and
405    /// debug inspection — the live state lives in the channel structs).
406    regs: [u8; 16],
407    tone_a: Sunsoft5BTone,
408    tone_b: Sunsoft5BTone,
409    tone_c: Sunsoft5BTone,
410    noise: Sunsoft5BNoise,
411    envelope: Sunsoft5BEnvelope,
412}
413
414#[cfg_attr(not(feature = "mapper-audio"), allow(dead_code))]
415impl Sunsoft5BAudio {
416    /// Raw value of one of the 16 PSG registers (`$00-$0F`), for the debug
417    /// window. Read-only — no side effects, unlike a real `$E000` access.
418    pub(crate) fn reg(&self, idx: usize) -> u8 {
419        self.regs[idx & 0x0F]
420    }
421
422    /// Current 16-bit envelope period (`$0B` low | `$0C` high), for debug.
423    pub(crate) const fn envelope_period(&self) -> u16 {
424        self.envelope.period
425    }
426
427    /// Current 5-bit envelope output level (0..=31), for debug.
428    pub(crate) fn envelope_output(&self) -> u8 {
429        self.envelope.output()
430    }
431
432    pub(crate) fn write_addr(&mut self, value: u8) {
433        // Per the wiki, writes with the high nibble nonzero are inhibited.
434        // The simplest faithful model is to mask the latch to 4 bits and
435        // accept the next data write unconditionally — no known software
436        // depends on the inhibit path.
437        self.addr_latch = value & 0x0F;
438    }
439
440    pub(crate) fn write_data(&mut self, value: u8) {
441        let idx = self.addr_latch as usize;
442        self.regs[idx] = value;
443        match idx {
444            0x00 => self.tone_a.period = (self.tone_a.period & 0x0F00) | u16::from(value),
445            0x01 => {
446                self.tone_a.period = (self.tone_a.period & 0x00FF) | (u16::from(value & 0x0F) << 8);
447            }
448            0x02 => self.tone_b.period = (self.tone_b.period & 0x0F00) | u16::from(value),
449            0x03 => {
450                self.tone_b.period = (self.tone_b.period & 0x00FF) | (u16::from(value & 0x0F) << 8);
451            }
452            0x04 => self.tone_c.period = (self.tone_c.period & 0x0F00) | u16::from(value),
453            0x05 => {
454                self.tone_c.period = (self.tone_c.period & 0x00FF) | (u16::from(value & 0x0F) << 8);
455            }
456            0x06 => self.noise.period = value & 0x1F,
457            0x07 => { /* mixer; consulted live in `mix_audio`. */ }
458            0x08 | 0x09 | 0x0A => { /* per-channel volume; consulted live. */ }
459            0x0B => {
460                self.envelope.period = (self.envelope.period & 0xFF00) | u16::from(value);
461            }
462            0x0C => {
463                self.envelope.period = (self.envelope.period & 0x00FF) | (u16::from(value) << 8);
464            }
465            0x0D => self.envelope.write_shape(value),
466            // $0E/$0F = I/O ports A/B.  Unused on the NES (the cart never
467            // wires them out).  We latch the byte for save-state round-trip
468            // and otherwise ignore.
469            _ => {}
470        }
471    }
472
473    /// Mixer register: bits are `--CBAcca`, 0 = enable / 1 = disable.
474    /// Bits 5/3/1 are noise enables for channels C/B/A respectively;
475    /// bits 4/2/0 are tone enables for channels c/b/a (same lettering).
476    const fn tone_enabled(&self, ch: u8) -> bool {
477        let mixer = self.regs[0x07];
478        // 0 = enable, 1 = disable.  Tone bits = 0, 2, 4 for A/B/C.
479        (mixer >> (ch * 2)) & 1 == 0
480    }
481
482    const fn noise_enabled(&self, ch: u8) -> bool {
483        let mixer = self.regs[0x07];
484        // Noise bits = 1, 3, 5 for A/B/C.
485        (mixer >> (ch * 2 + 1)) & 1 == 0
486    }
487
488    /// Resolve channel `ch`'s DAC amplitude (0/1/2 for A/B/C), honoring the
489    /// per-channel envelope-mode bit.
490    ///
491    /// Fixed-volume mode indexes the 4-bit [`SUNSOFT5B_LOG_VOL`] table (exact
492    /// 3 dB/step for a 4-bit register). Envelope mode indexes the full 5-bit
493    /// [`SUNSOFT5B_LOG_VOL32`] table for the NESdev-exact **1.5 dB/step**
494    /// resolution (v2.2.7 "Timbre II"; previously the 5-bit envelope was
495    /// truncated to 4-bit via `>> 1`, i.e. the wiki-named 3 dB approximation).
496    /// `env=0`/`env=1` both map to silence; `env=31` is full scale.
497    fn amplitude(&self, ch: u8) -> i32 {
498        let reg = self.regs[0x08 + ch as usize];
499        if reg & 0x10 != 0 {
500            SUNSOFT5B_LOG_VOL32[self.envelope.output() as usize & 0x1F]
501        } else {
502            SUNSOFT5B_LOG_VOL[(reg & 0x0F) as usize]
503        }
504    }
505
506    /// Advance every internal generator by one CPU cycle.  Per the wiki,
507    /// "none of the various generators can be halted" — they run whenever
508    /// the chip is clocked, regardless of mixer/enable state.
509    #[cfg(feature = "mapper-audio")]
510    pub(crate) fn clock(&mut self) {
511        self.tone_a.clock();
512        self.tone_b.clock();
513        self.tone_c.clock();
514        self.noise.clock();
515        self.envelope.clock();
516    }
517
518    /// Linear-summed audio output, scaled to ~i16 with the same headroom
519    /// VRC6 leaves for the APU mixer.
520    #[cfg(feature = "mapper-audio")]
521    pub(crate) fn mix(&self) -> i32 {
522        let mut sum: i32 = 0;
523        for (ch, tone) in [&self.tone_a, &self.tone_b, &self.tone_c]
524            .iter()
525            .enumerate()
526        {
527            let ch = ch as u8;
528            // Per wiki: "If both bits are 1 [disable + disable], the
529            // channel outputs a constant signal at the specified volume.
530            // If both bits are 0, the result is the logical and of noise
531            // and tone."  Equivalent: emit when (tone_enabled => square
532            // high) AND (noise_enabled => noise high), defaulting either
533            // factor to "1" when its source is disabled.
534            let tone_factor = !self.tone_enabled(ch) || tone.level != 0;
535            let noise_factor = !self.noise_enabled(ch) || self.noise.level() != 0;
536            if tone_factor && noise_factor {
537                sum += self.amplitude(ch);
538            }
539        }
540        // Scale the shape table to the hardware-relative level (see
541        // `SUNSOFT5B_MIX_SCALE_NUM`), then centre on zero so the BLEP buffer
542        // doesn't see a steady DC offset for an idle
543        // (all-channels-on-with-fixed-volume) cartridge. No cast: v2.2.3
544        // widened `Mapper::mix_audio` to i32 precisely so the 5B's full
545        // three-channel swing (~104 k) is representable rather than clamped.
546        // The multiply precedes the divide so the integer division loses at
547        // most 1 part in ~12,000 on a volume-12 tone.
548        sum * SUNSOFT5B_MIX_SCALE_NUM / SUNSOFT5B_MIX_SCALE_DEN - SUNSOFT5B_DC_BIAS
549    }
550
551    /// Feature-off shim: the generators do not advance with `mapper-audio`
552    /// disabled (mirrors the gated path so the shared NSF expansion router
553    /// can clock unconditionally).
554    #[cfg(not(feature = "mapper-audio"))]
555    #[allow(clippy::needless_pass_by_ref_mut, clippy::unused_self)]
556    pub(crate) fn clock(&mut self) {}
557
558    /// Feature-off shim: silence when `mapper-audio` is disabled.
559    #[cfg(not(feature = "mapper-audio"))]
560    #[allow(clippy::unused_self)]
561    pub(crate) fn mix(&self) -> i32 {
562        0
563    }
564
565    /// Serialize the live audio state.  21-byte tail:
566    ///   addr_latch(1) + regs[16](16) + tone_a/b/c counter+level(3*5=15) +
567    ///   noise counter+lfsr(4+1+... wait that's bigger).
568    ///
569    /// Tail layout (kept in lock-step with `read_tail`):
570    ///   addr_latch         : 1
571    ///   regs               : 16
572    ///   tone_a.counter     : 4 (u32 LE)
573    ///   tone_a.level       : 1
574    ///   tone_b.counter     : 4
575    ///   tone_b.level       : 1
576    ///   tone_c.counter     : 4
577    ///   tone_c.level       : 1
578    ///   noise.counter      : 4
579    ///   noise.lfsr         : 4 (u32 LE, only low 17 bits used)
580    ///   envelope.counter   : 4
581    ///   envelope.level     : 1
582    ///   envelope.rising    : 1 (bool)
583    ///   envelope.holding   : 1 (bool)
584    ///   -- 51 bytes total --
585    /// (Channel period/shape state is reconstructible from `regs`; we
586    /// don't serialize the period/shape fields separately.)
587    fn write_tail(&self, out: &mut Vec<u8>) {
588        out.push(self.addr_latch);
589        out.extend_from_slice(&self.regs);
590        for t in [&self.tone_a, &self.tone_b, &self.tone_c] {
591            out.extend_from_slice(&t.counter.to_le_bytes());
592            out.push(t.level);
593        }
594        out.extend_from_slice(&self.noise.counter.to_le_bytes());
595        out.extend_from_slice(&self.noise.lfsr.to_le_bytes());
596        out.extend_from_slice(&self.envelope.counter.to_le_bytes());
597        out.push(self.envelope.level);
598        out.push(u8::from(self.envelope.rising));
599        out.push(u8::from(self.envelope.holding));
600    }
601
602    /// Tail size in bytes — see `write_tail`.
603    const TAIL_LEN: usize = 1 + 16 + 3 * 5 + 4 + 4 + 4 + 1 + 1 + 1;
604
605    fn read_tail(&mut self, src: &[u8]) -> Result<(), MapperError> {
606        if src.len() < Self::TAIL_LEN {
607            return Err(MapperError::WrongLength {
608                expected: Self::TAIL_LEN,
609                got: src.len(),
610            });
611        }
612        self.addr_latch = src[0] & 0x0F;
613        self.regs.copy_from_slice(&src[1..17]);
614        let mut cur = 17usize;
615        for t in [&mut self.tone_a, &mut self.tone_b, &mut self.tone_c] {
616            t.counter = u32::from_le_bytes([src[cur], src[cur + 1], src[cur + 2], src[cur + 3]]);
617            t.level = src[cur + 4] & 1;
618            cur += 5;
619        }
620        self.noise.counter =
621            u32::from_le_bytes([src[cur], src[cur + 1], src[cur + 2], src[cur + 3]]);
622        cur += 4;
623        self.noise.lfsr =
624            u32::from_le_bytes([src[cur], src[cur + 1], src[cur + 2], src[cur + 3]]) & 0x1FFFF;
625        if self.noise.lfsr == 0 {
626            // Guard against a lock-up (LFSR with all zeros has no way out).
627            self.noise.lfsr = 0x1FFFF;
628        }
629        cur += 4;
630        self.envelope.counter =
631            u32::from_le_bytes([src[cur], src[cur + 1], src[cur + 2], src[cur + 3]]);
632        cur += 4;
633        self.envelope.level = src[cur] & 0x1F;
634        self.envelope.rising = src[cur + 1] != 0;
635        self.envelope.holding = src[cur + 2] != 0;
636        // Reconstruct live period/shape state from the register file.
637        self.tone_a.period = u16::from(self.regs[0x00]) | (u16::from(self.regs[0x01] & 0x0F) << 8);
638        self.tone_b.period = u16::from(self.regs[0x02]) | (u16::from(self.regs[0x03] & 0x0F) << 8);
639        self.tone_c.period = u16::from(self.regs[0x04]) | (u16::from(self.regs[0x05] & 0x0F) << 8);
640        self.noise.period = self.regs[0x06] & 0x1F;
641        self.envelope.period = u16::from(self.regs[0x0B]) | (u16::from(self.regs[0x0C]) << 8);
642        self.envelope.shape = self.regs[0x0D] & 0x0F;
643        Ok(())
644    }
645}
646
647/// Sunsoft FME-7 (Mapper 69).  Bank-switching, CPU-cycle IRQ, and (gated
648/// behind `mapper-audio`) the on-cart Sunsoft 5B audio chip.
649pub struct Fme7 {
650    prg_rom: Box<[u8]>,
651    chr_rom: Box<[u8]>,
652    prg_ram: Box<[u8]>,
653    vram: Box<[u8]>,
654    chr_is_ram: bool,
655    cmd: u8,
656    chr: [u8; 8],
657    prg_banks: [u8; 4], // $6000, $8000, $A000, $C000 (E000 fixed)
658    prg_ram_enabled: bool,
659    prg_ram_select: bool,
660    mirroring: Mirroring,
661
662    irq_counter: u16,
663    irq_enabled: bool,
664    irq_counter_enabled: bool,
665    irq_pending: bool,
666
667    /// Sunsoft 5B audio extension state.  Live regardless of the
668    /// `mapper-audio` feature — the register decoders always latch into
669    /// `regs` (so save states stay round-trippable across builds), but
670    /// `clock()` / `mix()` are only called when the feature is on.
671    audio: Sunsoft5BAudio,
672}
673
674impl Fme7 {
675    /// Construct a new FME-7 mapper.
676    ///
677    /// # Errors
678    ///
679    /// Returns [`MapperError::Invalid`] on size mismatch.
680    pub fn new(
681        prg_rom: Box<[u8]>,
682        chr_rom: Box<[u8]>,
683        mirroring: Mirroring,
684    ) -> Result<Self, MapperError> {
685        if prg_rom.is_empty() || !prg_rom.len().is_multiple_of(PRG_BANK_8K) {
686            return Err(MapperError::Invalid(format!(
687                "FME-7 PRG-ROM size {} is not a non-zero multiple of 8 KiB",
688                prg_rom.len()
689            )));
690        }
691        let chr_is_ram = chr_rom.is_empty();
692        let chr: Box<[u8]> = if chr_is_ram {
693            vec![0u8; CHR_BANK_8K].into_boxed_slice()
694        } else if chr_rom.len().is_multiple_of(CHR_BANK_1K) {
695            chr_rom
696        } else {
697            return Err(MapperError::Invalid(format!(
698                "FME-7 CHR-ROM size {} is not a multiple of 1 KiB",
699                chr_rom.len()
700            )));
701        };
702        Ok(Self {
703            prg_rom,
704            chr_rom: chr,
705            prg_ram: vec![0u8; 8 * 1024].into_boxed_slice(),
706            vram: vec![0u8; 2 * NAMETABLE_SIZE].into_boxed_slice(),
707            chr_is_ram,
708            cmd: 0,
709            chr: [0; 8],
710            prg_banks: [0; 4],
711            prg_ram_enabled: false,
712            prg_ram_select: true,
713            mirroring,
714            irq_counter: 0,
715            irq_enabled: false,
716            irq_counter_enabled: false,
717            irq_pending: false,
718            audio: Sunsoft5BAudio::default(),
719        })
720    }
721
722    fn prg_8k(&self, idx: usize) -> usize {
723        let total_8k = (self.prg_rom.len() / PRG_BANK_8K).max(1);
724        (self.prg_banks[idx] as usize) % total_8k
725    }
726}
727
728impl Mapper for Fme7 {
729    fn sram(&self) -> &[u8] {
730        &self.prg_ram
731    }
732    fn sram_mut(&mut self) -> &mut [u8] {
733        &mut self.prg_ram
734    }
735    // v2.8.0 Phase 4 — CPU-cycle hook + IRQ source + expansion audio
736    // (the audio hook only exists under the `mapper-audio` feature).
737    fn caps(&self) -> MapperCaps {
738        MapperCaps {
739            cpu_cycle_hook: true,
740            audio: cfg!(feature = "mapper-audio"),
741            frame_event_hook: false,
742            irq_source: true,
743        }
744    }
745
746    /// A2: the RAM-selected-but-DISABLED `$6000-$7FFF` window floats.
747    ///
748    /// Command `$8` carries RAM-enable in bit 7 and RAM-select in bit 6. Three
749    /// of the four states already worked: both set maps PRG-RAM, and bit 6
750    /// clear maps a PRG-ROM bank (regardless of bit 7). The fourth --
751    /// **selected but not enabled** (bit 6 = 1, bit 7 = 0) -- drives *neither*
752    /// chip on real hardware: the RAM is addressed but its enable is deasserted,
753    /// and the ROM is deselected. The CPU databus is left floating, so the read
754    /// returns the open-bus latch.
755    ///
756    /// Previously this state fell through to the PRG-ROM bank, returning that
757    /// bank's tag byte. Holy Mapperel's `M69_*` WRAM sub-check requires the
758    /// floating read to be `>= 3` (a `$7F`-class open-bus byte); it read the
759    /// bank tag `1` instead and set `MAPTEST_WRAMEN`, which is the entire
760    /// `1000` WRAM nibble those two ROMs reported (`docs/accuracy-ledger.md`).
761    ///
762    /// Routed through `cpu_read_unmapped` rather than by returning a guessed
763    /// byte from `cpu_read`: that is the trait's existing contract for "not
764    /// wired to mapper-resident memory", and it makes the bus preserve the real
765    /// latch instead of clobbering it -- which is what open bus actually is.
766    fn cpu_read_unmapped(&self, addr: u16) -> bool {
767        if matches!(addr, 0x6000..=0x7FFF) {
768            return self.prg_ram_select && !self.prg_ram_enabled;
769        }
770        // Everything else keeps the stock behaviour: `$4020-$5FFF` unmapped
771        // (the FME-7 has no registers down there -- its IRQ control lives at
772        // `$8000`/`$A000`), `$8000-$FFFF` always mapped PRG-ROM.
773        addr < 0x6000
774    }
775
776    fn cpu_read(&mut self, addr: u16) -> u8 {
777        match addr {
778            0x6000..=0x7FFF => {
779                if self.prg_ram_select && self.prg_ram_enabled {
780                    return self.prg_ram[(addr - 0x6000) as usize % self.prg_ram.len()];
781                }
782                let bank = self.prg_8k(0);
783                self.prg_rom[(bank * PRG_BANK_8K + (addr as usize - 0x6000)) % self.prg_rom.len()]
784            }
785            0x8000..=0x9FFF => {
786                let off = self.prg_8k(1) * PRG_BANK_8K + (addr as usize - 0x8000);
787                self.prg_rom[off % self.prg_rom.len()]
788            }
789            0xA000..=0xBFFF => {
790                let off = self.prg_8k(2) * PRG_BANK_8K + (addr as usize - 0xA000);
791                self.prg_rom[off % self.prg_rom.len()]
792            }
793            0xC000..=0xDFFF => {
794                let off = self.prg_8k(3) * PRG_BANK_8K + (addr as usize - 0xC000);
795                self.prg_rom[off % self.prg_rom.len()]
796            }
797            0xE000..=0xFFFF => {
798                let total_8k = (self.prg_rom.len() / PRG_BANK_8K).max(1);
799                let last = total_8k - 1;
800                self.prg_rom[(last * PRG_BANK_8K + (addr as usize - 0xE000)) % self.prg_rom.len()]
801            }
802            _ => 0,
803        }
804    }
805
806    fn cpu_write(&mut self, addr: u16, value: u8) {
807        match addr {
808            0x6000..=0x7FFF => {
809                if self.prg_ram_select && self.prg_ram_enabled {
810                    let off = (addr - 0x6000) as usize % self.prg_ram.len();
811                    self.prg_ram[off] = value;
812                }
813            }
814            0x8000..=0x9FFF => self.cmd = value & 0x0F,
815            0xA000..=0xBFFF => match self.cmd {
816                0..=7 => self.chr[self.cmd as usize] = value,
817                8 => {
818                    self.prg_ram_enabled = (value & 0x80) != 0;
819                    self.prg_ram_select = (value & 0x40) != 0;
820                    self.prg_banks[0] = value & 0x3F;
821                }
822                9..=11 => self.prg_banks[(self.cmd - 8) as usize] = value & 0x3F,
823                12 => {
824                    self.mirroring = match value & 0x03 {
825                        0 => Mirroring::Vertical,
826                        1 => Mirroring::Horizontal,
827                        2 => Mirroring::SingleScreenA,
828                        _ => Mirroring::SingleScreenB,
829                    };
830                }
831                13 => {
832                    self.irq_enabled = (value & 0x01) != 0;
833                    self.irq_counter_enabled = (value & 0x80) != 0;
834                    self.irq_pending = false;
835                }
836                14 => self.irq_counter = (self.irq_counter & 0xFF00) | u16::from(value),
837                15 => self.irq_counter = (self.irq_counter & 0x00FF) | (u16::from(value) << 8),
838                _ => {}
839            },
840            // Sunsoft 5B audio: $C000-$DFFF latches the register address;
841            // $E000-$FFFF writes data to the latched register.  Mapper-audio
842            // OFF builds still latch state (so the save-state path is
843            // round-trippable) but never advance the oscillators.
844            0xC000..=0xDFFF => self.audio.write_addr(value),
845            0xE000..=0xFFFF => self.audio.write_data(value),
846            _ => {}
847        }
848    }
849
850    fn ppu_read(&mut self, addr: u16) -> u8 {
851        let addr = addr & 0x3FFF;
852        match addr {
853            0x0000..=0x1FFF => {
854                let total_1k = (self.chr_rom.len() / CHR_BANK_1K).max(1);
855                let slot = addr as usize / CHR_BANK_1K;
856                let bank = (self.chr[slot] as usize) % total_1k;
857                let off = bank * CHR_BANK_1K + (addr as usize & (CHR_BANK_1K - 1));
858                self.chr_rom[off % self.chr_rom.len()]
859            }
860            0x2000..=0x3EFF => self.vram[nametable_offset(addr, self.mirroring) % self.vram.len()],
861            _ => 0,
862        }
863    }
864
865    fn ppu_write(&mut self, addr: u16, value: u8) {
866        let addr = addr & 0x3FFF;
867        match addr {
868            0x0000..=0x1FFF => {
869                if self.chr_is_ram {
870                    let len = self.chr_rom.len();
871                    self.chr_rom[addr as usize % len] = value;
872                }
873            }
874            0x2000..=0x3EFF => {
875                let off = nametable_offset(addr, self.mirroring) % self.vram.len();
876                self.vram[off] = value;
877            }
878            _ => {}
879        }
880    }
881
882    fn notify_cpu_cycle(&mut self) {
883        // Sunsoft 5B audio runs every CPU cycle, regardless of IRQ state.
884        // None of the 5B's internal generators can be halted, so we always
885        // tick when the feature is on.
886        #[cfg(feature = "mapper-audio")]
887        self.audio.clock();
888
889        if self.irq_counter_enabled {
890            self.irq_counter = self.irq_counter.wrapping_sub(1);
891            if self.irq_counter == 0xFFFF && self.irq_enabled {
892                self.irq_pending = true;
893            }
894        }
895    }
896
897    #[cfg(feature = "mapper-audio")]
898    fn mix_audio(&mut self) -> i32 {
899        self.audio.mix()
900    }
901
902    fn irq_pending(&self) -> bool {
903        self.irq_pending
904    }
905
906    fn current_mirroring(&self) -> Mirroring {
907        self.mirroring
908    }
909
910    fn debug_info(&self) -> crate::mapper::MapperDebugInfo {
911        let mut info = crate::mapper::MapperDebugInfo {
912            mapper_id: 69,
913            name: "Sunsoft FME-7".into(),
914            mirroring: crate::mapper::mirroring_name(self.current_mirroring()),
915            ..Default::default()
916        };
917        for (i, b) in self.prg_banks.iter().enumerate() {
918            info.prg_banks
919                .push((format!("PRG{i}"), format!("{b:#04x}")));
920        }
921        for (i, b) in self.chr.iter().enumerate() {
922            info.chr_banks
923                .push((format!("CHR{i}"), format!("{b:#04x}")));
924        }
925        info.irq_state
926            .push(("counter".into(), format!("{:#06x}", self.irq_counter)));
927        info.irq_state
928            .push(("enabled".into(), format!("{}", self.irq_enabled)));
929        info.irq_state
930            .push(("counting".into(), format!("{}", self.irq_counter_enabled)));
931        info.irq_state
932            .push(("pending".into(), format!("{}", self.irq_pending)));
933        info.extra
934            .push(("cmd".into(), format!("{:#04x}", self.cmd)));
935        info.extra.push((
936            "prg_ram".into(),
937            format!("en={} sel={}", self.prg_ram_enabled, self.prg_ram_select),
938        ));
939        // v2.2.3 — surface the Sunsoft 5B audio register file. The 5B is the
940        // only part of this board with no other debug window, and its state is
941        // exactly what you need to answer "why is this cart silent?" — the
942        // mixer/enable byte ($07) and the three volume bytes ($08-$0A, bit 4 =
943        // envelope mode) decide whether anything sounds at all.
944        #[cfg(feature = "mapper-audio")]
945        {
946            let a = &self.audio;
947            info.extra
948                .push(("5b_mixer($07)".into(), format!("{:#04x}", a.reg(0x07))));
949            info.extra.push((
950                "5b_vol(A,B,C)".into(),
951                format!(
952                    "{:#04x} {:#04x} {:#04x}",
953                    a.reg(0x08),
954                    a.reg(0x09),
955                    a.reg(0x0A)
956                ),
957            ));
958            info.extra.push((
959                "5b_env".into(),
960                format!(
961                    "period={:#06x} shape={:#04x} out={}",
962                    a.envelope_period(),
963                    a.reg(0x0D),
964                    a.envelope_output()
965                ),
966            ));
967            info.extra.push(("5b_mix".into(), format!("{}", a.mix())));
968        }
969        info
970    }
971
972    fn save_state(&self) -> Vec<u8> {
973        // v2: appends the Sunsoft 5B audio state at the end.  Per ADR-0003:
974        // strictly additive, so v1 readers tolerate the tail (older builds
975        // skip-on-read since the tag is consumed at the section length).
976        // Tail size = Sunsoft5BAudio::TAIL_LEN (51 bytes).
977        // v3: appends the cartridge CHR-RAM after the audio tail; see
978        // `FME7_SECTION_VERSION`.
979        let mut out = Vec::with_capacity(
980            40 + self.prg_ram.len()
981                + self.vram.len()
982                + Sunsoft5BAudio::TAIL_LEN
983                + self.chr_ram_tail_len(),
984        );
985        out.push(FME7_SECTION_VERSION);
986        out.push(self.cmd);
987        out.extend_from_slice(&self.chr);
988        out.extend_from_slice(&self.prg_banks);
989        out.push(u8::from(self.prg_ram_enabled));
990        out.push(u8::from(self.prg_ram_select));
991        out.push(self.mirroring as u8);
992        out.extend_from_slice(&self.irq_counter.to_le_bytes());
993        out.push(u8::from(self.irq_enabled));
994        out.push(u8::from(self.irq_counter_enabled));
995        out.push(u8::from(self.irq_pending));
996        out.extend_from_slice(&self.prg_ram);
997        out.extend_from_slice(&self.vram);
998        // v2 audio tail.
999        self.audio.write_tail(&mut out);
1000        // v3 CHR-RAM tail.
1001        if self.chr_is_ram {
1002            out.extend_from_slice(&self.chr_rom);
1003        }
1004        out
1005    }
1006
1007    fn load_state(&mut self, data: &[u8]) -> Result<(), MapperError> {
1008        let scalar_len = 1 + 1 + 8 + 4 + 1 + 1 + 1 + 2 + 1 + 1 + 1;
1009        let core_expected = scalar_len + self.prg_ram.len() + self.vram.len();
1010        if data.len() < core_expected {
1011            return Err(MapperError::WrongLength {
1012                expected: core_expected,
1013                got: data.len(),
1014            });
1015        }
1016        let version = data[0];
1017        // Only the current version is read (v2.9.8, ADR 0042). v1 (no audio
1018        // tail) and v2 (no CHR-RAM tail, short audio tail tolerated) used to
1019        // load with those parts left as they were.
1020        if version != FME7_SECTION_VERSION {
1021            return Err(MapperError::UnsupportedVersion(version));
1022        }
1023        // Strict: core + the full audio tail + the CHR-RAM tail, exactly.
1024        // Validated before the first field is written.
1025        let expected = core_expected + Sunsoft5BAudio::TAIL_LEN + self.chr_ram_tail_len();
1026        if data.len() != expected {
1027            return Err(MapperError::WrongLength {
1028                expected,
1029                got: data.len(),
1030            });
1031        }
1032        self.cmd = data[1];
1033        self.chr.copy_from_slice(&data[2..10]);
1034        self.prg_banks.copy_from_slice(&data[10..14]);
1035        self.prg_ram_enabled = data[14] != 0;
1036        self.prg_ram_select = data[15] != 0;
1037        self.mirroring = match data[16] {
1038            0 => Mirroring::Horizontal,
1039            1 => Mirroring::Vertical,
1040            2 => Mirroring::SingleScreenA,
1041            3 => Mirroring::SingleScreenB,
1042            4 => Mirroring::FourScreen,
1043            5 => Mirroring::MapperControlled,
1044            other => return Err(MapperError::Invalid(format!("mirroring {other}"))),
1045        };
1046        self.irq_counter = u16::from_le_bytes(
1047            data[17..19]
1048                .try_into()
1049                .map_err(|_| MapperError::Invalid("irq_counter".into()))?,
1050        );
1051        self.irq_enabled = data[19] != 0;
1052        self.irq_counter_enabled = data[20] != 0;
1053        self.irq_pending = data[21] != 0;
1054        let mut cur = 22usize;
1055        self.prg_ram
1056            .copy_from_slice(&data[cur..cur + self.prg_ram.len()]);
1057        cur += self.prg_ram.len();
1058        self.vram.copy_from_slice(&data[cur..cur + self.vram.len()]);
1059        cur += self.vram.len();
1060
1061        // v2 tail: audio state.
1062        self.audio
1063            .read_tail(&data[cur..cur + Sunsoft5BAudio::TAIL_LEN])?;
1064        // v3 CHR-RAM tail, exactly sized by the check above.
1065        if self.chr_is_ram {
1066            self.chr_rom
1067                .copy_from_slice(&data[cur + Sunsoft5BAudio::TAIL_LEN..]);
1068        }
1069        Ok(())
1070    }
1071}
1072
1073impl Fme7 {
1074    /// Bytes the v3 tail adds: the 8 KiB CHR-RAM when the cartridge has no
1075    /// CHR-ROM, else nothing. Derived from the loaded ROM, so a save and its
1076    /// load (same ROM, checked by the `.rns` hash tag) agree.
1077    fn chr_ram_tail_len(&self) -> usize {
1078        if self.chr_is_ram {
1079            self.chr_rom.len()
1080        } else {
1081            0
1082        }
1083    }
1084}
1085
1086#[cfg(test)]
1087mod tests {
1088    use super::*;
1089
1090    fn synth(banks_8k: usize) -> Box<[u8]> {
1091        let mut v = vec![0u8; banks_8k * PRG_BANK_8K];
1092        for b in 0..banks_8k {
1093            v[b * PRG_BANK_8K] = b as u8;
1094        }
1095        v.into_boxed_slice()
1096    }
1097
1098    fn synth_chr(banks_1k: usize) -> Box<[u8]> {
1099        let mut v = vec![0u8; banks_1k * CHR_BANK_1K];
1100        for b in 0..banks_1k {
1101            v[b * CHR_BANK_1K] = b as u8;
1102        }
1103        v.into_boxed_slice()
1104    }
1105
1106    #[test]
1107    fn fme7_basic_banking() {
1108        let mut m = Fme7::new(synth(16), synth_chr(8), Mirroring::Vertical).unwrap();
1109        // cmd=9 -> writes prg_banks[1] (the $8000-$9FFF window).
1110        m.cpu_write(0x8000, 9);
1111        m.cpu_write(0xA000, 5);
1112        // Read at $8000 should now be bank 5 (offset 0 == bank index byte).
1113        assert_eq!(m.cpu_read(0x8000), 5);
1114        // cmd=10 -> prg_banks[2] ($A000-$BFFF).
1115        m.cpu_write(0x8000, 10);
1116        m.cpu_write(0xA000, 7);
1117        assert_eq!(m.cpu_read(0xA000), 7);
1118    }
1119
1120    fn fme7_audio_write(m: &mut Fme7, reg: u8, value: u8) {
1121        m.cpu_write(0xC000, reg);
1122        m.cpu_write(0xE000, value);
1123    }
1124
1125    #[test]
1126    fn sunsoft5b_register_address_latch_round_trip() {
1127        // The address latch is the gateway for every audio write; it must
1128        // round-trip distinctly from the data path.  After latching $0B
1129        // (envelope period low), a subsequent data write should target
1130        // $0B specifically.
1131        let mut m = Fme7::new(synth(8), synth_chr(8), Mirroring::Vertical).unwrap();
1132        m.cpu_write(0xC000, 0x0B);
1133        assert_eq!(m.audio.addr_latch, 0x0B);
1134        // Bits 7-4 of the address byte are ignored (masked to 4 bits).
1135        m.cpu_write(0xC100, 0xF7);
1136        assert_eq!(m.audio.addr_latch, 0x07);
1137        // A data write at $E000-$FFFF goes to the latched register.
1138        m.cpu_write(0xE800, 0xAB);
1139        assert_eq!(m.audio.regs[0x07], 0xAB);
1140    }
1141
1142    #[test]
1143    fn sunsoft5b_channel_period_decodes_into_internal_state() {
1144        // Channel A period: TP = ($01 & 0x0F) << 8 | $00.  Confirm the
1145        // 12-bit period composes correctly from the two writes, and that
1146        // bits 7-4 of $01 are masked off.
1147        let mut m = Fme7::new(synth(8), synth_chr(8), Mirroring::Vertical).unwrap();
1148        fme7_audio_write(&mut m, 0x00, 0x34);
1149        fme7_audio_write(&mut m, 0x01, 0xF7); // upper nibble (7) used; F is ignored.
1150        assert_eq!(m.audio.tone_a.period, 0x0734);
1151
1152        // Channel B / C similarly.
1153        fme7_audio_write(&mut m, 0x02, 0x12);
1154        fme7_audio_write(&mut m, 0x03, 0x03);
1155        assert_eq!(m.audio.tone_b.period, 0x0312);
1156        fme7_audio_write(&mut m, 0x04, 0xFF);
1157        fme7_audio_write(&mut m, 0x05, 0x0F);
1158        assert_eq!(m.audio.tone_c.period, 0x0FFF);
1159    }
1160
1161    #[test]
1162    fn sunsoft5b_tone_toggles_every_16_times_period_cycles() {
1163        // Per NESdev wiki: the square wave toggles every 16 CPU clocks per
1164        // period count.  With TP = 5, we expect a toggle every 80 cycles.
1165        // Drive the chip through clock() directly to isolate the tone path
1166        // from the rest of the mapper.
1167        let mut t = Sunsoft5BTone {
1168            period: 5,
1169            ..Sunsoft5BTone::default()
1170        };
1171        // First clock fires immediately (counter starts at 0) and reloads.
1172        // Count toggles across 800 cycles.
1173        let mut toggles = 0u32;
1174        let mut last = t.level;
1175        for _ in 0..800 {
1176            t.clock();
1177            if t.level != last {
1178                toggles += 1;
1179                last = t.level;
1180            }
1181        }
1182        // 800 cycles / 80 per toggle = 10 toggles.  Allow ±1 for the
1183        // counter-starts-at-zero start-up edge.
1184        assert!(
1185            (9..=11).contains(&toggles),
1186            "tone toggle count {toggles} not in 9..=11"
1187        );
1188    }
1189
1190    #[test]
1191    fn sunsoft5b_volume_scale_zero_silent_max_peak() {
1192        // Volume 0 must produce silence; volume 15 must produce the peak
1193        // entry of the log-DAC table.  These bracket the per-channel
1194        // contribution range.
1195        assert_eq!(SUNSOFT5B_LOG_VOL[0], 0);
1196        assert!(SUNSOFT5B_LOG_VOL[15] > SUNSOFT5B_LOG_VOL[14]);
1197        // amplitude() applies the envelope-mode select bit and returns the DAC
1198        // value (not the 4-bit index).
1199        let mut a = Sunsoft5BAudio::default();
1200        a.regs[0x08] = 0x0F; // fixed volume = 15.
1201        assert_eq!(a.amplitude(0), SUNSOFT5B_LOG_VOL[15]);
1202        a.regs[0x08] = 0x00; // fixed volume = 0.
1203        assert_eq!(a.amplitude(0), 0);
1204    }
1205
1206    #[test]
1207    fn sunsoft5b_envelope_mode_routes_envelope_into_channel() {
1208        // Setting bit 4 of $08/$09/$0A switches that channel from fixed volume
1209        // to envelope mode. In envelope mode (v2.2.7 "Timbre II") the FULL 5-bit
1210        // envelope level indexes the exact 1.5 dB/step SUNSOFT5B_LOG_VOL32 DAC —
1211        // no longer truncated to 4-bit via `>> 1`.
1212        let mut a = Sunsoft5BAudio::default();
1213        a.regs[0x08] = 0x10; // envelope mode, fixed-volume bits ignored.
1214        a.envelope.level = 31; // full scale.
1215        assert_eq!(a.amplitude(0), SUNSOFT5B_LOG_VOL32[31]);
1216        a.envelope.level = 6; // an even (previously-truncated) level.
1217        assert_eq!(a.amplitude(0), SUNSOFT5B_LOG_VOL32[6]);
1218        a.envelope.level = 1;
1219        assert_eq!(a.amplitude(0), 0); // env 0 and 1 both -> silence.
1220        a.envelope.level = 0;
1221        assert_eq!(a.amplitude(0), 0);
1222        // Switching back to fixed mode honors $08 bits 3-0 again (4-bit DAC).
1223        a.regs[0x08] = 0x07;
1224        assert_eq!(a.amplitude(0), SUNSOFT5B_LOG_VOL[7]);
1225    }
1226
1227    #[test]
1228    fn log_vol32_odd_entries_match_4bit() {
1229        // The 4-bit fixed-volume table is exactly the odd levels of the 5-bit
1230        // envelope DAC, so a fixed-volume channel and an envelope channel
1231        // resting on the same level agree to the bit.
1232        for v in 0..16 {
1233            assert_eq!(SUNSOFT5B_LOG_VOL32[2 * v + 1], SUNSOFT5B_LOG_VOL[v]);
1234        }
1235        // Envelope levels 0 and 1 are both silence (per the NESdev wiki).
1236        assert_eq!(SUNSOFT5B_LOG_VOL32[0], 0);
1237        assert_eq!(SUNSOFT5B_LOG_VOL32[1], 0);
1238        // A strictly increasing log ramp above the silent floor.
1239        for e in 3..32 {
1240            assert!(SUNSOFT5B_LOG_VOL32[e] > SUNSOFT5B_LOG_VOL32[e - 1]);
1241        }
1242    }
1243
1244    #[cfg(feature = "mapper-audio")]
1245    #[test]
1246    fn sunsoft5b_mix_output_sign_silent_vs_active() {
1247        // With every channel muted (mixer = 0xFF disables both tone and
1248        // noise on A/B/C; volumes don't matter), the linear sum is 0 and
1249        // the mix output sits at -DC_BIAS (centered).  With one channel
1250        // unmuted at max volume and the square wave high, the sum exceeds
1251        // the bias and the mix is positive.
1252        let mut m = Fme7::new(synth(8), synth_chr(8), Mirroring::Vertical).unwrap();
1253        fme7_audio_write(&mut m, 0x07, 0x3F); // bits 0..=5 all set => all disabled.
1254        // Volumes irrelevant when channels are muted.
1255        let silent = m.mix_audio();
1256        assert_eq!(silent, -SUNSOFT5B_DC_BIAS);
1257
1258        // Enable tone A only at max volume, then force the square level high
1259        // by ticking once with period = 0 (the chip wraps period=0 to 1).
1260        fme7_audio_write(&mut m, 0x07, 0b0011_1110); // tone A enabled (bit 0 = 0).
1261        fme7_audio_write(&mut m, 0x08, 0x0F); // channel A volume = 15.
1262        // Manually toggle the tone level so we hit the "high" half-cycle.
1263        m.audio.tone_a.level = 1;
1264        let active = m.mix_audio();
1265        assert!(
1266            active > 0,
1267            "active mix output should be positive, got {active}"
1268        );
1269    }
1270
1271    #[test]
1272    fn sunsoft5b_save_state_v2_round_trips_audio() {
1273        // Round-trip an FME-7 with a non-trivial audio register file.  The
1274        // load_state path reconstructs the live period/shape state from the
1275        // serialized register file, so verifying via `audio.tone_a.period`
1276        // exercises both the regs blob and the reconstruction path.
1277        let mut m = Fme7::new(synth(8), synth_chr(8), Mirroring::Vertical).unwrap();
1278        fme7_audio_write(&mut m, 0x00, 0x55);
1279        fme7_audio_write(&mut m, 0x01, 0x06);
1280        fme7_audio_write(&mut m, 0x08, 0x0F);
1281        fme7_audio_write(&mut m, 0x07, 0x36); // a few tone/noise enables.
1282        fme7_audio_write(&mut m, 0x0D, 0x0E); // envelope shape -> restart.
1283        let blob = m.save_state();
1284        assert_eq!(
1285            blob[0], FME7_SECTION_VERSION,
1286            "save_state writes the current version"
1287        );
1288
1289        let mut m2 = Fme7::new(synth(8), synth_chr(8), Mirroring::Vertical).unwrap();
1290        m2.load_state(&blob).expect("round-trip");
1291        assert_eq!(m2.audio.tone_a.period, 0x0655);
1292        assert_eq!(m2.audio.regs[0x07], 0x36);
1293        assert_eq!(m2.audio.regs[0x08], 0x0F);
1294        assert_eq!(m2.audio.envelope.shape, 0x0E);
1295    }
1296
1297    /// v2.9.8 (ADR 0042): only the current (v3) layout loads. A v1 blob (no
1298    /// audio tail) and a v2 blob (no CHR-RAM tail, written through v2.9.1)
1299    /// used to load with those parts left as they were.
1300    #[test]
1301    fn fme7_pre_v3_blobs_are_refused() {
1302        let m = Fme7::new(synth(8), Box::new([]), Mirroring::Vertical).unwrap();
1303        let mut v2 = m.save_state();
1304        v2.truncate(v2.len() - m.chr_rom.len());
1305        v2[0] = 2;
1306        let mut v1 = v2.clone();
1307        v1.truncate(v1.len() - Sunsoft5BAudio::TAIL_LEN);
1308        v1[0] = 1;
1309        let mut m2 = Fme7::new(synth(8), Box::new([]), Mirroring::Vertical).unwrap();
1310        for (v, old) in [(1u8, &v1), (2, &v2)] {
1311            assert!(matches!(
1312                m2.load_state(old),
1313                Err(MapperError::UnsupportedVersion(got)) if got == v
1314            ));
1315        }
1316    }
1317
1318    /// v2.9.2 cartridge-RAM sweep: the section carries the 8 KiB CHR-RAM of
1319    /// a board with no CHR-ROM. The whole-machine pin is
1320    /// `rustynes_core::nes::tests::every_board_snapshot_carries_cartridge_ram`.
1321    #[test]
1322    fn fme7_save_state_carries_chr_ram() {
1323        let mut m = Fme7::new(synth(8), Box::new([]), Mirroring::Vertical).unwrap();
1324        m.chr_rom[0x0000] = 0x11;
1325        m.chr_rom[0x1FFF] = 0x22;
1326        let blob = m.save_state();
1327        let mut m2 = Fme7::new(synth(8), Box::new([]), Mirroring::Vertical).unwrap();
1328        m2.load_state(&blob).expect("round-trip");
1329        assert_eq!(m2.chr_rom[0x0000], 0x11);
1330        assert_eq!(m2.chr_rom[0x1FFF], 0x22);
1331    }
1332
1333    /// A v3 blob one byte short (inside the CHR-RAM tail) is rejected before
1334    /// any state changes.
1335    #[test]
1336    fn fme7_truncated_chr_ram_tail_is_rejected() {
1337        let mut m = Fme7::new(synth(8), Box::new([]), Mirroring::Vertical).unwrap();
1338        m.cpu_write(0x8000, 9);
1339        m.cpu_write(0xA000, 5);
1340        let blob = m.save_state();
1341        let mut m2 = Fme7::new(synth(8), Box::new([]), Mirroring::Vertical).unwrap();
1342        let err = m2
1343            .load_state(&blob[..blob.len() - 1])
1344            .expect_err("a truncated v3 blob must be rejected");
1345        assert!(matches!(err, MapperError::WrongLength { .. }), "{err:?}");
1346        assert_eq!(m2.prg_banks[1], 0, "untouched");
1347    }
1348
1349    #[test]
1350    fn sunsoft5b_mapper_audio_off_path_latches_state_but_stays_silent() {
1351        // When the `mapper-audio` feature is OFF, the register decoder still
1352        // latches every write (so save-state round-trip stays correct) but
1353        // the oscillators never advance and `mix_audio` returns 0.
1354        //
1355        // We can't toggle the cargo feature from inside a test, but we CAN
1356        // assert the two halves of this contract directly:
1357        //   1. The register latch path is unconditional (this test runs
1358        //      regardless of the feature flag).
1359        //   2. The oscillator clock path is gated — verified by the absence
1360        //      of `audio.clock()` calls in `notify_cpu_cycle` when the
1361        //      feature is off (compile-time `#[cfg(...)]`).
1362        // To exercise (1), write to every register and confirm `regs` and
1363        // the derived period fields are populated.  To exercise (2)'s
1364        // observable effect, freeze the counters by NOT calling notify and
1365        // confirm the level state stays at zero.
1366        let mut m = Fme7::new(synth(8), synth_chr(8), Mirroring::Vertical).unwrap();
1367        for r in 0u8..=0x0F {
1368            fme7_audio_write(&mut m, r, r.wrapping_mul(0x11));
1369        }
1370        assert_eq!(m.audio.regs[0x00], 0x00);
1371        assert_eq!(m.audio.regs[0x0F], 0xFF);
1372        // Without any clock() calls, the tone level remains at default 0.
1373        assert_eq!(m.audio.tone_a.level, 0);
1374        assert_eq!(m.audio.tone_b.level, 0);
1375        assert_eq!(m.audio.tone_c.level, 0);
1376    }
1377
1378    #[test]
1379    #[cfg(feature = "mapper-audio")]
1380    fn sunsoft5b_volume_dac_follows_logarithmic_step_law() {
1381        // The DAC SHAPE criterion. (The absolute LEVEL is a separate concern
1382        // with its own oracle — `level_db_5b` against the `db_5b` ROM, wired in
1383        // v2.2.3 A1; it used to be an i16-headroom deferral.) The 5B volume DAC
1384        // is logarithmic, ~+3 dB
1385        // (×1.1885² ≈ ×1.4125) per 4-bit step, matching Mesen2's
1386        // `Sunsoft5bAudio` `_volumeLut` (LUT[12]=63, LUT[15]=177) and tetanes.
1387        assert_eq!(SUNSOFT5B_LOG_VOL[0], 0, "silence at volume 0");
1388        // Shape parity with Mesen2's table (floor(10^(0.15*i))) at the two
1389        // survey-relevant points.
1390        assert_eq!(SUNSOFT5B_LOG_VOL[12], 668);
1391        assert_eq!(SUNSOFT5B_LOG_VOL[15], 1882);
1392        // Each non-zero step multiplies by ~1.4125 (the +1.5 dB × 2 law).
1393        for v in 2..16usize {
1394            let ratio = f64::from(SUNSOFT5B_LOG_VOL[v]) / f64::from(SUNSOFT5B_LOG_VOL[v - 1]);
1395            assert!(
1396                (ratio - 1.4125).abs() < 0.06,
1397                "5B DAC step {v}: ratio {ratio:.4} not ~1.4125 (logarithmic law violated)"
1398            );
1399        }
1400        // vol-15 is ~2.82× vol-12 (three +3 dB steps = ×1.4125^3 ≈ 2.818),
1401        // the ~9 dB the `db_5b` ROM's vol-12 choice sits below full volume.
1402        let v15_v12 = f64::from(SUNSOFT5B_LOG_VOL[15]) / f64::from(SUNSOFT5B_LOG_VOL[12]);
1403        assert!(
1404            (v15_v12 - 2.818).abs() < 0.05,
1405            "vol-15/vol-12 ratio {v15_v12:.4} not ~2.818"
1406        );
1407    }
1408}