Skip to main content

rustynes_apu/
apu.rs

1//! Top-level 2A03 APU.
2//!
3//! Per `docs/apu-2a03.md`.  Owns the four wave channels plus DMC, the frame
4//! counter, the lookup-table mixer + filter chain, and the band-limited
5//! sample emitter.  Driven by the lockstep bus's `Apu::tick` once per CPU
6//! cycle.
7
8use crate::Region;
9use crate::blip::{BlipBuf, CPU_HZ_NTSC, CPU_HZ_PAL};
10use crate::dmc::Dmc;
11use crate::frame_counter::{FrameCounter, FrameEvents};
12use crate::mixer::Mixer;
13use crate::noise::Noise;
14use crate::pulse::Pulse;
15use crate::triangle::Triangle;
16use alloc::vec::Vec;
17
18// `f32::round` lives in `std` (not `core`), so route through `libm::roundf` on
19// no_std — the same pattern the mixer uses for `expf`. Both round half away from
20// zero, so the result is identical across the desktop + `thumbv7em-none-eabihf`
21// targets. Only reached on the off-default per-channel-gain path (gain != 1.0),
22// never on the byte-identical unity path.
23#[inline]
24fn roundf(x: f32) -> f32 {
25    #[cfg(feature = "std")]
26    {
27        x.round()
28    }
29    #[cfg(not(feature = "std"))]
30    {
31        libm::roundf(x)
32    }
33}
34
35/// Bus surface seen by the APU.  A small subset of the full CPU bus for the
36/// DMC's sample-fetch DMA.
37pub trait ApuBus {
38    /// Read one byte for a DMC sample fetch.  The bus is responsible for
39    /// halting the CPU and accounting for the 3- or 4-cycle DMA stall
40    /// before this is called.
41    fn dmc_read(&mut self, addr: u16) -> u8;
42}
43
44/// Top-level APU.
45#[derive(Debug, Clone)]
46pub struct Apu {
47    /// Region (NTSC / PAL / Dendy).
48    pub region: Region,
49    /// Pulse 1.
50    pub pulse1: Pulse,
51    /// Pulse 2.
52    pub pulse2: Pulse,
53    /// Triangle.
54    pub triangle: Triangle,
55    /// Noise.
56    pub noise: Noise,
57    /// DMC.
58    pub dmc: Dmc,
59    /// Frame counter.
60    pub frame_counter: FrameCounter,
61    /// Mixer.
62    pub(crate) mixer: Mixer,
63    /// Band-limited sample emitter.
64    pub(crate) blip: BlipBuf,
65    /// True on every other CPU cycle — pulse/noise/DMC clock at this rate.
66    // reason: `apu_phase` is the deliberate, documented name for the APU's
67    // clock phase; it appears verbatim as a column header in the committed
68    // irq_trace golden CSVs, so the `apu_` prefix is load-bearing, not noise.
69    #[allow(clippy::struct_field_names)]
70    pub(crate) apu_phase: bool,
71    /// v2.0 F-2: when set, the DMC byte-timer + DMA arm are driven by
72    /// [`Self::tick_dmc`] (called at end-of-cycle by the R1 bus) instead of
73    /// inside [`Self::tick_with_external`] (cycle-start). This shifts only the
74    /// DMC fire-phase to main's end-of-cycle position (for DMASync), leaving
75    /// the frame-counter / pulse / noise — and thus the APU IRQ line — on the
76    /// cycle-start tick (for the C1 IRQ sample). Default `false` = byte-identical.
77    pub(crate) dmc_driven_externally: bool,
78    /// v2.0 interleaved-DMA Phase A: the global get/put flip-flop (TriCNES
79    /// `APU_PutCycle`, `Emulator.cs:920`). Toggled exactly once per CPU cycle
80    /// (when `dmc_driven_externally`, so the default build is byte-identical)
81    /// and seeded at power-on/reset TOGETHER with the DMC byte-timer from one
82    /// `APUAlignment` value, so the get/put parity and the DMC fire-phase share
83    /// one seed + one per-cycle counter and can never drift (divergence A). The
84    /// interleaved DMA (Phase B) reads this for the get/put decision instead of
85    /// `self.cycle & 1`. `true` = put (write/OAM-priority), `false` = get
86    /// (read/DMC-priority). Nothing consumes it yet in Phase A.
87    pub(crate) put_cycle: bool,
88    /// v2.0 RW-1 (`mc-r1-one-clock`): the single boot parity seed. Under
89    /// `mc-r1-one-clock`, `apu_phase` and `put_cycle` are no longer two
90    /// independent flip-flops toggled per cycle — they are DERIVED from the one
91    /// per-cycle counter (`cpu_cycle`) plus this seed:
92    /// `apu_phase = (cpu_cycle + parity_seed) & 1 == 1`, `put_cycle = !apu_phase`.
93    /// This makes the APU-rate clock, the get/put DMA parity, and the DMC
94    /// fire-phase share ONE counter + ONE seed, so they can never drift apart
95    /// (the cumulative-counter-split root cause, see
96    /// `docs/audit/v2.0-cumulative-cycle-accounting-rewrite-plan-2026-06-05.md`).
97    /// `0` reproduces the floor config exactly (boot `apu_phase = false`,
98    /// `seed_apu_alignment(0)` -> put-on-even). Set once at power-on/reset/restore
99    /// by [`Self::seed_apu_alignment`]; otherwise constant. Unused when the flag
100    /// is off (the legacy dual-toggle path runs instead).
101    pub(crate) parity_seed: u64,
102    /// W3-Stage-4 (2026-06-10): whether the most recent [`Apu::restore`]
103    /// blob carried the Stage-4 parity/DMA-state tail (so `put_cycle` +
104    /// `parity_seed` were restored EXACTLY and the bus must NOT re-seed the
105    /// boot alignment over them). Transient bookkeeping — never serialized.
106    pub(crate) restored_parity_tail: bool,
107    /// Cumulative CPU cycle counter (used for `$4017` write alignment).
108    pub(crate) cpu_cycle: u64,
109    /// Pending DMC DMA request — the bus polls and consumes this when it
110    /// halts the CPU and supplies a sample byte.
111    pub(crate) pending_dmc_dma: bool,
112    /// `mc-r1-dmc-reload-visibility-delay`: a RELOAD arm latches HERE and is
113    /// promoted to `pending_dmc_dma` one cycle later, so the DMA loop first
114    /// services it on the NEXT (put) cycle — matching TriCNES's
115    /// `_EmulateAPU`-after-`_6502` invisibility (reload first-service = put =>
116    /// span 4). Loads arm `pending_dmc_dma` directly (first-service = get => 3).
117    pub(crate) pending_dmc_dma_next: bool,
118    /// True when the pending DMC DMA is the initial load DMA after `$4015`
119    /// enable; false for reload DMAs raised by sample-buffer empty.
120    pub(crate) dmc_dma_is_load: bool,
121    /// True when the pending request uses the short 3-cycle service path
122    /// despite being externally observed as a load-style race. This covers
123    /// the explicit-stop abort edge where a visible reload request must be
124    /// preserved through the `$4015` disable write without making ordinary
125    /// load DMAs lose their dummy/alignment cadence.
126    pub(crate) dmc_dma_short: bool,
127    /// Suppress one immediate reload request after a same-tick DMC load
128    /// delivery. Used for the one-byte looping edge where the fetched byte
129    /// is visible to the output unit on the DMA get cycle, but the reload
130    /// request is not visible until the following CPU cycle.
131    pub(crate) defer_dmc_reload_once: bool,
132    /// Pending one-cycle DMC abort halt. This is the RP2A03 stop-near-reload
133    /// quirk: it does not fetch a byte, and if the halt attempt lands on a
134    /// CPU write cycle the abort disappears instead of retrying.
135    pub(crate) pending_dmc_abort: bool,
136    /// CPU cycles until an abort halt attempt becomes visible to the bus.
137    pub(crate) dmc_abort_delay: u8,
138    /// CPU cycles during which a newly emptied DMC sample buffer must not
139    /// raise another DMA request. The DMC DMA unit cannot issue a second
140    /// request within two CPU cycles of the previous get.
141    pub(crate) dmc_dma_cooldown: u8,
142    /// v2.0.0 beta.3 (A4 cycle-accurate reset): countdown to the scheduled
143    /// warm-reset `$4017` re-write (blargg `apu_reset` spec: reset behaves
144    /// as if the last `$4017` value were written again). Armed by
145    /// [`Apu::reset`] with the calibrated in-sequence placement; consumed in
146    /// `tick_with_external` during the CPU's clocked reset cycles. `0` = no
147    /// re-write pending.
148    pub(crate) reset_4017_delay: u8,
149    /// The retained `$4017` value the scheduled reset re-write will issue.
150    pub(crate) reset_4017_value: u8,
151    /// v2.0 Phase 2 (`mc-r1-dmc-reenable-phase`): TriCNES's
152    /// `CannotRunDMCDMARightNow` (`Emulator.cs:823`). Set to 2 after every DMC
153    /// GET (`:4168`), decremented by 2 on each get cycle (`:1186`), and gating
154    /// the looping-reload arm (`:1165`, blocked while `== 2`). Reproduces the
155    /// "a DMA cannot occur within 2 cycles of a previous DMC DMA" rule so the
156    /// Implicit-DMA-Abort Loop3/`$540` X=10/11 re-enable defers its first
157    /// reload one byte-timer period (walk offset +4 -> +5, Y 3->4). Distinct
158    /// from `dmc_dma_cooldown` (which the abort-timer-phase fix clears at the
159    /// boundary race); this exclusion re-imposes the exact 1-get-cycle block.
160    pub(crate) cannot_run_dmc_dma: u8,
161    /// v2.0 Phase 2 (`mc-r1-dmc-reenable-phase`): latch that defers a reload to
162    /// the NEXT byte-timer wrap when the exclusion blocks the arm. TriCNES only
163    /// evaluates the reload arm at the `bits_remaining -> 0` consume edge
164    /// (`Emulator.cs:1159`); if blocked there (`cannot_run == 2`) the buffer is
165    /// not refilled until the FOLLOWING consume edge (one full byte period
166    /// later) — NOT a 2-cycle re-arm. RustyNES's `dmc_step_reload_arm` instead
167    /// re-checks `needs_dma()` (persistent) every cycle, so a bare exclusion
168    /// gate would re-arm as soon as `cannot_run` decremented (absorbed). This
169    /// latch reproduces the full-period deferral: set when the exclusion blocks
170    /// a consume-edge arm, cleared at the next consume edge.
171    pub(crate) dmc_reenable_period_block: bool,
172    /// final lever #1 (`mc-r1-dmc-halt-subpos`): a per-CPU-cycle countdown that
173    /// DELAYS the `$540` X=10/11 reload arm by an exact number of CPU cycles
174    /// (sub-APU-cycle granularity the byte-timer phase shift cannot express).
175    /// `0` = inactive. Set at the pattern-A boundary; while > 0 the natural arm
176    /// is suppressed and this decrements; at 0 the arm fires. Default 0.
177    // (W3-Stage-3: also dead under `mc-r1-dmc-delayed-4015`, which supersedes
178    // the halt-subpos pre-arm with the emergent consume-edge arm.)
179    pub(crate) subpos_arm_countdown: u8,
180    /// Latches the one-byte looping edge where the next reload request is
181    /// lost because it is raised too soon after a DMA get. Cleared when a
182    /// later `$4015` enable/disable write re-arms the DMC path.
183    pub(crate) dmc_reload_suppress_outputs: u8,
184    /// CPU cycles until a load DMC DMA halt attempt becomes visible to the
185    /// bus. Reload DMAs are armed immediately after the DMC output unit
186    /// empties the sample buffer; load DMAs after `$4015` enable are delayed
187    /// to the second following APU cycle per the 2A03 DMA cadence.
188    pub(crate) dmc_dma_delay: u8,
189    /// Most recent DMC DMA address (re-read each tick when `pending_dmc_dma`
190    /// is true; the bus may take it directly via [`Self::dmc_dma_addr`]).
191    /// On its own this is informational; the bus owns the actual halt logic.
192    pub(crate) dmc_dma_addr: u16,
193    /// v1.2 Sprint 3 — get/put cycle scheduler model (ADR-0007).
194    ///
195    /// Set when a DMC DMA request is raised; cleared by the new
196    /// `rustynes-core::bus::service_dmc_dma` path under the
197    /// `dmc-get-put-scheduler` feature flag, once the initial halt
198    /// cycle has been processed. Mirrors Mesen2's `_needHalt` on
199    /// `NesCpu` (`Core/NES/NesCpu.h:41`; set in `StartDmcTransfer`
200    /// at `Core/NES/NesCpu.cpp:527`). Kept ALWAYS-PRESENT (not
201    /// `#[cfg]`-gated) so the field exists in serialized state for
202    /// future-flag-flip migration; the v1.2 baseline scheduler
203    /// simply ignores it.
204    pub(crate) dmc_need_halt: bool,
205    /// v1.2 Sprint 3 — get/put cycle scheduler model (ADR-0007).
206    ///
207    /// Set when a DMC DMA request is raised; cleared by the new
208    /// scheduler once the alignment / dummy-read cycle has been
209    /// processed. Mirrors Mesen2's `_needDummyRead` on `NesCpu`.
210    /// Kept always-present alongside [`Self::dmc_need_halt`].
211    pub(crate) dmc_need_dummy_read: bool,
212    /// W3-Stage-3 (`mc-r1-dmc-delayed-4015`): TriCNES `APU_DelayedDMC4015`
213    /// (Emulator.cs:973) — CPU-cycle countdown until the latched `$4015` DMC
214    /// status bit is APPLIED. Set to `put ? 3 : 4` at every `$4015` write
215    /// (extended to `put ? 5 : 6` at the explicit don't-abort edge);
216    /// decremented once per `dmc_tick_end` (every CPU cycle, the write
217    /// cycle's own end-tick included). `0` = idle.
218    pub(crate) dmc_delayed_4015: u8,
219    /// W3-Stage-3: TriCNES `APU_Status_DelayedDMC` (Emulator.cs:964) — the
220    /// TARGET DMC status latched at the `$4015` write, applied when the
221    /// countdown expires. Also the value `$4015` READS see immediately
222    /// (the footnote at Emulator.cs:9268: bit 4 must read 0 right after a
223    /// disable write even though `bytes_remaining` is not yet zeroed).
224    pub(crate) dmc_delayed_status: bool,
225    /// W3-Stage-3: TriCNES `APU_Status_DMC` (Emulator.cs:963) — the APPLIED
226    /// DMC status. The bus-side DMA service gate (`_6502` line 4218:
227    /// `DoDMCDMA && (APU_Status_DMC || implicit-abort)`) reads this; while
228    /// false a pending/halted DMC DMA is NOT serviced (the emergent explicit
229    /// abort). Set/cleared ONLY by the delayed application + cleared at
230    /// non-looping natural sample end (TriCNES `DMCDMA_Get`, line 4154).
231    pub(crate) dmc_status_applied: bool,
232    /// W3-Stage-3: TriCNES `APU_SetImplicitAbortDMC4015` (Emulator.cs:975) —
233    /// latched at a `$4015` ENABLE write that coincides with the byte-timer's
234    /// firing window (`(timer == 10 && get) || (timer == 8 && put)` in
235    /// TriCNES CPU-rate units = our APU-rate `(4, get)/(3, put)`); consumed
236    /// at the next shifter-consume edge (bits 1 -> 8), where it arms the
237    /// 1-cycle implicit-abort DMA regardless of the buffer state
238    /// (Emulator.cs:1163-1175).
239    pub(crate) dmc_set_implicit_abort: bool,
240    /// W3-Stage-3: TriCNES `APU_ImplicitAbortDMC4015` (Emulator.cs:974) — the
241    /// service-gate override that lets the boundary-armed DMA run while the
242    /// `$4015` enable's delayed status is still unapplied. Cleared at the END
243    /// of the first cycle on which the DMA is pending (Emulator.cs:9000-9003:
244    /// one serviced halt cycle if that cycle is a read; "if this was delayed
245    /// by a write cycle, it won't run at all") — the emergent 1-cycle
246    /// implicit abort.
247    pub(crate) dmc_implicit_abort: bool,
248    /// W3-Stage-4 (`mc-r1-dmc-delayed-4015` grid correction): TriCNES's
249    /// reload arm is CONSUME-EDGE-QUANTIZED, not level-held. When the consume
250    /// edge lands ON the GET-delivery cycle itself (`CannotRunDMCDMARightNow
251    /// == 2`, Emulator.cs:1165 — only ever true at the same-cycle edge,
252    /// because the decrement at :1186 runs later that same end-tick), the arm
253    /// is skipped ENTIRELY and the chain defers to the NEXT consume edge
254    /// (576 cycles). Our `needs_dma()` is level-triggered and would re-arm as
255    /// soon as the cooldown expires (4 cycles — one grid boundary early, the
256    /// Implicit `$540[8,9]` cliff). Set at the blocked same-cycle edge;
257    /// suppresses the reload arm; cleared at the next consume edge in
258    /// `dmc_tick_end` immediately before the reload-arm step so the deferred
259    /// arm fires exactly on-grid.
260    pub(crate) dmc_edge_arm_suppress: bool,
261    /// Sample rate (Hz) for diagnostics.
262    pub sample_rate: u32,
263    /// Most recent frame-counter events produced by [`Self::tick_with_external`].
264    /// Read by the bus immediately after `tick` to fan the same events out to
265    /// any on-cart audio extension that shares the 2A03 frame counter cadence
266    /// (MMC5 audio). Reset to `FrameEvents::default()` at the *start* of every
267    /// `tick`, so observers must read it AFTER the tick.
268    pub(crate) last_frame_events: FrameEvents,
269    /// Per-channel enable mask (UI playback overlay, NOT NES hardware state).
270    /// Bit 0 = pulse 1, bit 1 = pulse 2, bit 2 = triangle, bit 3 = noise,
271    /// bit 4 = DMC, bit 5 = external/mapper audio. A cleared bit forces that
272    /// channel's contribution to the mixed sample to 0 (a studio/debug mute).
273    ///
274    /// Defaults to [`CHANNEL_MASK_ALL`] (every bit set), which is byte-identical
275    /// to passing the raw channel outputs straight into the mixer — i.e. the
276    /// deterministic core output is unchanged unless the frontend explicitly
277    /// mutes a channel. NEVER serialized into the save state (a UI preference,
278    /// like volume), so restored states are unaffected.
279    pub(crate) channel_mask: u8,
280    /// v1.4.0 Workstream C — per-channel output gain (a UI mixing overlay, NOT
281    /// NES hardware state), generalizing [`Self::channel_mask`]. Index 0 = pulse
282    /// 1, 1 = pulse 2, 2 = triangle, 3 = noise, 4 = DMC, 5 = external/mapper
283    /// audio. Each internal channel's raw integer output is scaled by its gain
284    /// and rounded back to an integer before the non-linear mixer; the external
285    /// (already-linear) sample is scaled directly.
286    ///
287    /// Defaults to [`CHANNEL_GAIN_UNITY`] (all `1.0`). At unity the mix takes the
288    /// EXACT current code path (`round(v * 1.0) == v`, `external * 1.0 ==
289    /// external`), so the deterministic core output is byte-identical unless the
290    /// frontend explicitly changes a gain — the determinism contract holds and
291    /// the oracle / test ROMs (which never touch a gain) are unaffected. NEVER
292    /// serialized into the save state (a UI preference, like the mask / volume).
293    pub(crate) channel_gain: [f32; 6],
294    /// v2.1.6 "Expansion Audio" — the most recent RAW external / on-cart
295    /// expansion-audio sample fed into [`Self::tick_with_external`] (BEFORE the
296    /// UI [`Self::channel_gain`] `[5]` re-weight), retained purely so the
297    /// frontend Audio Mixer panel can plot an expansion-channel oscilloscope /
298    /// VU meter alongside the five base-channel DAC taps.
299    ///
300    /// This is a WRITE-ONLY-from-synthesis, READ-ONLY-to-observers copy: it is
301    /// assigned once per tick and is never read back into the mixer, the IRQ
302    /// path, or any determinism-relevant state, and it is NEVER serialized into
303    /// the save state. It therefore cannot perturb the deterministic per-frame
304    /// audio — the visualization samples a copy, exactly like the base-channel
305    /// `*_out()` DAC accessors already do.
306    pub(crate) last_external: f32,
307
308    /// v2.3.7 "Overtone" — audio provenance, behind ONE pointer.
309    ///
310    /// `None` until armed via [`Apu::set_audio_provenance`]. Consolidated into a
311    /// single `Option<Box<..>>` after `apu_throughput` measured +9% on the
312    /// DISARMED path with this state spread across four inline fields — see
313    /// `crate::provenance::AudioProvenance`.
314    #[cfg(feature = "debug-hooks")]
315    pub(crate) audio_prov: Option<alloc::boxed::Box<crate::provenance::AudioProvenance>>,
316}
317
318/// All [`Apu::channel_mask`] bits set — every channel audible (the default and
319/// the determinism-safe value the oracle / test ROMs always run with).
320pub const CHANNEL_MASK_ALL: u8 = 0x3F;
321
322/// All [`Apu::channel_gain`] entries at `1.0` — every channel at full,
323/// unattenuated output (the default and the byte-identical value the oracle /
324/// test ROMs always run with).
325pub const CHANNEL_GAIN_UNITY: [f32; 6] = [1.0; 6];
326
327impl Apu {
328    /// New APU.
329    #[must_use]
330    pub fn new(region: Region, sample_rate: u32) -> Self {
331        let cpu_rate = match region {
332            Region::Pal => CPU_HZ_PAL,
333            _ => CPU_HZ_NTSC,
334        };
335        // v2.1.5: the frame counter selects PAL vs NTSC sequencer step
336        // positions from the region. Only true `Region::Pal` uses the PAL
337        // (2A07) positions; NTSC and Dendy keep the NTSC (2A03) positions, so
338        // their frame-counter timing is byte-identical to the pre-v2.1.5 model.
339        let mut frame_counter = FrameCounter::new();
340        frame_counter.pal = matches!(region, Region::Pal);
341        Self {
342            region,
343            pulse1: Pulse::new(true),
344            pulse2: Pulse::new(false),
345            triangle: Triangle::new(),
346            noise: Noise::new(region),
347            dmc: Dmc::new(region),
348            frame_counter,
349            mixer: Mixer::new(),
350            blip: BlipBuf::new(sample_rate, cpu_rate),
351            apu_phase: false,
352            dmc_driven_externally: false,
353            put_cycle: false,
354            parity_seed: 0,
355            restored_parity_tail: false,
356            cpu_cycle: 0,
357            pending_dmc_dma: false,
358            pending_dmc_dma_next: false,
359            dmc_dma_is_load: false,
360            dmc_dma_short: false,
361            defer_dmc_reload_once: false,
362            pending_dmc_abort: false,
363            dmc_abort_delay: 0,
364            dmc_dma_cooldown: 0,
365            reset_4017_delay: 0,
366            reset_4017_value: 0,
367            cannot_run_dmc_dma: 0,
368            dmc_reenable_period_block: false,
369            subpos_arm_countdown: 0,
370            dmc_reload_suppress_outputs: 0,
371            dmc_dma_delay: 0,
372            dmc_dma_addr: 0xC000,
373            dmc_need_halt: false,
374            dmc_need_dummy_read: false,
375            dmc_delayed_4015: 0,
376            dmc_delayed_status: false,
377            dmc_status_applied: false,
378            dmc_set_implicit_abort: false,
379            dmc_implicit_abort: false,
380            dmc_edge_arm_suppress: false,
381            sample_rate,
382            last_frame_events: FrameEvents::default(),
383            channel_mask: CHANNEL_MASK_ALL,
384            channel_gain: CHANNEL_GAIN_UNITY,
385            last_external: 0.0,
386            #[cfg(feature = "debug-hooks")]
387            audio_prov: None,
388        }
389    }
390
391    // -----------------------------------------------------------------
392    // v2.3.7 "Overtone" — audio provenance (output-only, off by default)
393    // -----------------------------------------------------------------
394
395    /// Record one mixed CPU cycle — the OUTLINED half.
396    ///
397    /// Called from both mix paths so the fast default-configuration
398    /// specialization and the gated general path produce the same trace: a
399    /// provenance record that existed on only one of two byte-identical paths
400    /// would be a trap for whoever next changed the other.
401    ///
402    /// # Why `#[cold]` and `#[inline(never)]` are load-bearing
403    ///
404    /// This function is measurement-driven twice over, and the second lesson is
405    /// the less obvious one.
406    ///
407    /// The FIRST version built the `MixRecord` before testing whether
408    /// provenance was armed, so a DISARMED build recomputed all five channel
409    /// outputs every CPU cycle — and `Pulse::output` is not free (it calls
410    /// `muted()`, which calls `sweep_target()`). `apu_throughput` measured
411    /// **+14% to +23%** in the feature-on/arm-off configuration the shipped
412    /// frontend runs. Hoisting the arm check to the top fixed that.
413    ///
414    /// It was NOT enough. With the check first, a quiet-host A/B still measured
415    /// **+7.98% / +2.88% / +11.03%** on the three `apu_throughput` workloads
416    /// (order-bias control: +0.11% / +0.76% / +0.67%, so the deltas are real).
417    /// The absolute costs — +33 µs, +15 µs, +65 µs — are wildly non-uniform,
418    /// which a per-cycle branch cannot produce: a constant branch costs a
419    /// constant number of cycles. The cause was that this body was still being
420    /// INLINED into `tick_with_external`. The five `output()` calls sat in the
421    /// hot function even though the branch skipped over them, inflating it past
422    /// the point where the mixer and the channel ticks kept their registers and
423    /// their I-cache line.
424    ///
425    /// So the hot path now contains exactly one null test, and everything else
426    /// lives out of line behind it. `#[cold]` additionally tells LLVM to lay
427    /// this block out away from the fall-through path. It pessimizes the ARMED
428    /// case, which is the correct trade: armed is an interactive debugging mode
429    /// and disarmed is what every user runs.
430    #[cfg(feature = "debug-hooks")]
431    #[cold]
432    #[inline(never)]
433    fn record_mix_armed(&mut self, mixed: f32, external: f32) {
434        let rec = crate::provenance::MixRecord {
435            mixed,
436            external,
437            pulse1: self.pulse1.output(),
438            pulse2: self.pulse2.output(),
439            triangle: self.triangle.output(),
440            noise: self.noise.output(),
441            dmc: self.dmc.output(),
442        };
443        if let Some(p) = self.audio_prov.as_mut() {
444            p.mix_trace.push(rec);
445        }
446    }
447
448    /// Arm or disarm audio provenance.
449    ///
450    /// Arming allocates both stores; disarming frees them. Mirrors
451    /// `Ppu::set_pixel_provenance`, including that re-arming an already-armed
452    /// APU is a no-op rather than a silent wipe — the frontend re-asserts the
453    /// arm every frame (a lesson from the pixel panel, whose edge-triggered
454    /// mirror desynced permanently the moment a ROM load installed a fresh
455    /// core).
456    #[cfg(feature = "debug-hooks")]
457    pub fn set_audio_provenance(&mut self, enabled: bool) {
458        if enabled {
459            if self.audio_prov.is_none() {
460                self.audio_prov = Some(alloc::boxed::Box::new(
461                    crate::provenance::AudioProvenance::new(),
462                ));
463            }
464        } else {
465            self.audio_prov = None;
466        }
467    }
468
469    /// Whether audio provenance is armed.
470    #[cfg(feature = "debug-hooks")]
471    #[must_use]
472    pub const fn audio_provenance_armed(&self) -> bool {
473        self.audio_prov.is_some()
474    }
475
476    /// The per-register write attribution, or `None` when disarmed.
477    #[cfg(feature = "debug-hooks")]
478    #[must_use]
479    pub fn register_attribution(&self) -> Option<&crate::provenance::RegisterAttribution> {
480        self.audio_prov.as_ref().map(|p| &p.reg_attrib)
481    }
482
483    /// The per-CPU-cycle mix trace, or `None` when disarmed.
484    #[cfg(feature = "debug-hooks")]
485    #[must_use]
486    pub fn mix_trace(&self) -> Option<&crate::provenance::MixTrace> {
487        self.audio_prov.as_ref().map(|p| &p.mix_trace)
488    }
489
490    /// Begin a new frame's mix trace, anchored at `first_cycle`.
491    ///
492    /// The register attribution is deliberately NOT cleared here: "which
493    /// instruction last wrote `$4003`" is a question whose answer legitimately
494    /// predates the current frame, and clearing it every frame would report a
495    /// register nobody has touched this frame as never written.
496    #[cfg(feature = "debug-hooks")]
497    pub fn begin_audio_provenance_frame(&mut self, first_cycle: u64) {
498        if let Some(p) = self.audio_prov.as_mut() {
499            p.mix_trace.clear(first_cycle);
500        }
501    }
502
503    /// Forget the register attribution history. Called on a cold boot, where
504    /// the history it describes genuinely ended.
505    #[cfg(feature = "debug-hooks")]
506    pub fn clear_audio_provenance_history(&mut self) {
507        if let Some(p) = self.audio_prov.as_mut() {
508            p.reg_attrib.clear();
509        }
510    }
511
512    /// Attribute a write in `$4000-$4017` that the bus does NOT route through
513    /// [`Self::write_register`].
514    ///
515    /// Two addresses in the range are not APU registers and are handled
516    /// entirely on the bus: `$4014` (OAM DMA, which arms a burst) and `$4016`
517    /// (controller strobe, which is buffered to the next M2-low boundary).
518    /// `Bus::write` dispatches only `$4000-$4013 | $4015 | $4017` to
519    /// `write_register`, so the attribution recorded there can never see those
520    /// two — yet the table reserves slots for them, because the range is what
521    /// the bus already classifies as an APU write and punching a hole in it
522    /// would invite off-by-one arithmetic at every call site.
523    ///
524    /// Without this entry point those two slots would stay permanently empty
525    /// while the docs claimed they were tracked. This records the cause exactly
526    /// as `write_register` would, and dispatches nothing — the emulation of both
527    /// addresses stays wherever the bus already implements it.
528    #[cfg(feature = "debug-hooks")]
529    pub const fn record_bus_handled_register_write(&mut self, addr: u16, value: u8) {
530        if let Some(p) = self.audio_prov.as_mut() {
531            p.reg_attrib
532                .record(addr, p.attrib_pc, p.attrib_cycle, value);
533        }
534    }
535
536    /// Push the writing instruction's PC + cycle down, mirroring the PPU's
537    /// write-attribution context. Called once per instruction by the core.
538    #[cfg(feature = "debug-hooks")]
539    pub const fn set_attrib_context(&mut self, pc: u16, cycle: u64) {
540        // No-op when disarmed: nothing reads these, so skipping the stores keeps
541        // the disarmed per-instruction cost at one null test.
542        if let Some(p) = self.audio_prov.as_mut() {
543            p.attrib_pc = pc;
544            p.attrib_cycle = cycle;
545        }
546    }
547
548    /// Lift both stores out for a same-timeline restore (run-ahead), leaving the
549    /// APU disarmed. See [`crate::provenance::AudioProvenanceStash`] for why
550    /// this exists at all.
551    #[cfg(feature = "debug-hooks")]
552    #[must_use]
553    pub fn take_audio_provenance(&mut self) -> crate::provenance::AudioProvenanceStash {
554        crate::provenance::AudioProvenanceStash {
555            state: self.audio_prov.take(),
556        }
557    }
558
559    /// Put back stores taken by [`Self::take_audio_provenance`].
560    #[cfg(feature = "debug-hooks")]
561    pub fn put_audio_provenance(&mut self, stash: crate::provenance::AudioProvenanceStash) {
562        self.audio_prov = stash.state;
563    }
564
565    /// Reset (warm).  Per nesdev: most APU state is preserved across reset
566    /// except `$4015` is cleared (channels disabled, DMC silenced).
567    ///
568    /// v2.0.0 beta.3 (A4 cycle-accurate reset, promoted to the only path in
569    /// beta.4): the 2A03
570    /// reset sequence behaves as if the LAST value written to `$4017` were
571    /// written again (blargg `apu_reset` spec) — the retained value is
572    /// re-issued through the normal `$4017` write path (pending mode + the
573    /// 3/4-cycle aligned delay + the mode-1 immediate quarter/half clock),
574    /// and the CPU's 8 clocked reset cycles then age the re-armed counter
575    /// so execution resumes ~9-12 cycles after the effective write (the
576    /// `4017_timing` window).
577    pub fn reset(&mut self) {
578        // Zero the sequencer + IRQ flags now; SCHEDULE the hardware
579        // `$4017` re-write to land 2 clocked cycles into the CPU's
580        // 8-cycle reset sequence (consumed in `tick_with_external`).
581        // Empirically calibrated against blargg `4017_timing`'s printed
582        // "delay after effective $4017 write" (accept window 6..=12,
583        // hardware-usual 9; the ROM quantizes in 2-cycle APU units): an
584        // immediate reset-start re-write measures 12 (the upper edge),
585        // a +3-cycle placement measures 6 (the lower edge), and +2
586        // lands mid-window at 8.
587        let last = self.frame_counter.reset_rewrite_4017();
588        self.reset_4017_value = last;
589        self.reset_4017_delay = 2;
590        self.write_register(0x4015, 0x00);
591        // v2.3.7 — that write went through the ordinary CPU path, which just
592        // attributed it to whatever instruction was last latched. No instruction
593        // caused it: this models the warm-reset silencing of the channels.
594        // Correct the origin so the panel reports hardware rather than naming an
595        // innocent PC. (Caught in review of the PR that added the feature.)
596        #[cfg(feature = "debug-hooks")]
597        if let Some(p) = self.audio_prov.as_mut() {
598            p.reg_attrib.record_reset(0x4015, p.attrib_cycle, 0x00);
599        }
600        self.pending_dmc_dma = false;
601        self.dmc_dma_is_load = false;
602        self.dmc_dma_short = false;
603        self.defer_dmc_reload_once = false;
604        self.pending_dmc_abort = false;
605        self.dmc_abort_delay = 0;
606        self.dmc_dma_cooldown = 0;
607        self.cannot_run_dmc_dma = 0;
608        self.dmc_reenable_period_block = false;
609        self.dmc_reload_suppress_outputs = 0;
610        self.dmc_dma_delay = 0;
611        self.dmc_need_halt = false;
612        self.dmc_need_dummy_read = false;
613        // W3-Stage-3: a warm reset silences the DMC immediately — collapse the
614        // delayed-application machinery to the applied-disabled state (the
615        // `write_register(0x4015, 0)` above latched a deferred disable).
616        {
617            self.dmc_delayed_4015 = 0;
618            self.dmc_delayed_status = false;
619            self.dmc_status_applied = false;
620            self.dmc_set_implicit_abort = false;
621            self.dmc_implicit_abort = false;
622            self.dmc_edge_arm_suppress = false;
623            self.dmc.bytes_remaining = 0;
624        }
625        self.blip.reset();
626    }
627
628    /// Set the per-channel enable mask (a UI playback overlay; see
629    /// [`Apu::channel_mask`]). Bit 0 = pulse 1, 1 = pulse 2, 2 = triangle,
630    /// 3 = noise, 4 = DMC, 5 = external/mapper audio. [`CHANNEL_MASK_ALL`] is
631    /// the determinism-safe default (byte-identical mixer output).
632    pub const fn set_channel_mask(&mut self, mask: u8) {
633        self.channel_mask = mask & CHANNEL_MASK_ALL;
634    }
635
636    /// Current per-channel enable mask.
637    #[must_use]
638    pub const fn channel_mask(&self) -> u8 {
639        self.channel_mask
640    }
641
642    /// v1.4.0 Workstream C — set the per-channel output gain (a UI mixing
643    /// overlay; see [`Apu::channel_gain`]). Index 0 = pulse 1, 1 = pulse 2,
644    /// 2 = triangle, 3 = noise, 4 = DMC, 5 = external/mapper audio. Each gain is
645    /// clamped to `0.0..=2.0`. [`CHANNEL_GAIN_UNITY`] (all `1.0`) is the
646    /// determinism-safe default (byte-identical mixer output).
647    pub fn set_channel_gain(&mut self, gain: [f32; 6]) {
648        for (slot, g) in self.channel_gain.iter_mut().zip(gain.iter()) {
649            *slot = g.clamp(0.0, 2.0);
650        }
651    }
652
653    /// v2.1.3 — select the analog output-filter model (see
654    /// [`crate::mixer::FilterModel`]). Default [`crate::mixer::FilterModel::NesRf`]
655    /// is byte-identical to the pre-v2.1.3 output; the softer models drop the
656    /// aggressive 440 Hz high-pass for a fuller low end. Display/tonal only —
657    /// channel content is unchanged.
658    pub fn set_filter_model(&mut self, model: crate::mixer::FilterModel) {
659        self.blip.set_filter_model(model);
660    }
661
662    /// Current per-channel output gain. See [`Apu::set_channel_gain`].
663    #[must_use]
664    pub const fn channel_gain(&self) -> [f32; 6] {
665        self.channel_gain
666    }
667
668    /// Pulse 1 raw output volume (0..=15) — for tests.
669    #[must_use]
670    pub fn pulse1_out(&self) -> u8 {
671        self.pulse1.output()
672    }
673    /// Pulse 2 raw output volume.
674    #[must_use]
675    pub fn pulse2_out(&self) -> u8 {
676        self.pulse2.output()
677    }
678    /// Triangle raw output (0..=15).
679    #[must_use]
680    pub fn triangle_out(&self) -> u8 {
681        self.triangle.output()
682    }
683    /// Noise raw output (0..=15).
684    #[must_use]
685    pub fn noise_out(&self) -> u8 {
686        self.noise.output()
687    }
688    /// DMC raw output (0..=127).
689    #[must_use]
690    pub const fn dmc_out(&self) -> u8 {
691        self.dmc.output()
692    }
693
694    /// v2.1.6 "Expansion Audio" — the most recent RAW on-cart expansion-audio
695    /// sample (pre-[`Self::channel_gain`], the `last_external` field). `0.0`
696    /// when the loaded board has no expansion audio. Read-only display tap for
697    /// the frontend Audio Mixer expansion-channel scope / VU meter — it reads a
698    /// copy and never feeds back into synthesis, so it is determinism-neutral.
699    #[must_use]
700    pub const fn external_out(&self) -> f32 {
701        self.last_external
702    }
703
704    /// Frame IRQ pending?
705    #[must_use]
706    pub const fn frame_irq_pending(&self) -> bool {
707        self.frame_counter.irq_flag
708    }
709
710    /// DMC IRQ pending?
711    #[must_use]
712    pub const fn dmc_irq_pending(&self) -> bool {
713        self.dmc.irq_flag
714    }
715
716    /// Combined IRQ line — true if either source is asserting.
717    ///
718    /// Session-26 iter 5 (2026-05-23): the frame-counter contribution
719    /// is `irq_line_active` (the CPU's `IRQSource::FrameCounter`
720    /// registration), NOT `irq_flag` (the `$4015` bit 6 visibility).
721    /// The two are SEPARATE fields since iter 5 — see
722    /// [`FrameCounter::irq_flag`](crate::frame_counter::FrameCounter::irq_flag)
723    /// and [`FrameCounter::irq_line_active`](crate::frame_counter::FrameCounter::irq_line_active).
724    /// AccuracyCoin Tests I/J/K specifically test that `$4015` bit 6
725    /// is visible during inhibit (transient 2-cycle window at FC steps
726    /// 29828-29829) while NO CPU IRQ fires (Test M).
727    #[must_use]
728    pub const fn irq_line(&self) -> bool {
729        self.frame_counter.irq_line_active || self.dmc.irq_flag
730    }
731
732    /// Returns the frame-counter events fired by the most recent `tick` call.
733    ///
734    /// The bus reads this immediately after [`Self::tick_with_external`] to
735    /// fan-out the events to on-cart audio extensions (MMC5) whose envelope
736    /// and length-counter sub-units share the 2A03 frame-counter cadence.
737    /// The value is overwritten at the start of every `tick`, so observers
738    /// must consume it before the next tick.
739    #[must_use]
740    pub const fn last_frame_events(&self) -> FrameEvents {
741        self.last_frame_events
742    }
743
744    /// Drain all finalized audio samples (host sample rate, normalized to
745    /// approximately `[-0.5, 0.5]`).
746    pub fn drain_audio(&mut self) -> Vec<f32> {
747        self.blip.drain_all()
748    }
749
750    /// Drain into a slice; returns count copied.
751    pub fn drain_audio_into(&mut self, out: &mut [f32]) -> usize {
752        self.blip.drain(out)
753    }
754
755    /// Has a DMC DMA request been raised?  The bus polls this each CPU cycle
756    /// (BEFORE issuing reads) so it can halt the CPU on the next read cycle.
757    #[must_use]
758    pub const fn dmc_dma_pending(&self) -> bool {
759        self.pending_dmc_dma
760    }
761
762    /// Whether the pending DMC DMA is a load DMA.
763    #[must_use]
764    pub const fn dmc_dma_is_load(&self) -> bool {
765        self.dmc_dma_is_load
766    }
767
768    /// W3-Stage-3 (`mc-r1-dmc-delayed-4015`): the bus-side per-cycle DMC DMA
769    /// service-gate term — TriCNES `_6502` line 4218:
770    /// `DoDMCDMA && (APU_Status_DMC || APU_ImplicitAbortDMC4015)`. While
771    /// false, a pending (or halted in-flight) DMC DMA is NOT serviced and the
772    /// CPU resumes — the emergent explicit abort. `pending_dmc_abort` is the
773    /// implicit-abort override (the 1-cycle abort DMA runs regardless).
774    #[must_use]
775    pub const fn dmc_dma_serviceable(&self) -> bool {
776        self.dmc_status_applied || self.dmc_implicit_abort || self.pending_dmc_abort
777    }
778
779    /// Whether the pending DMC DMA should use the short 3-cycle service path.
780    #[must_use]
781    pub const fn dmc_dma_short(&self) -> bool {
782        self.dmc_dma_short
783    }
784
785    /// Whether the current DMC DMA get should make the fetched byte visible
786    /// before the get-cycle APU tick.
787    #[must_use]
788    pub const fn dmc_dma_deliver_before_tick(&self) -> bool {
789        self.dmc.loop_flag
790            && self.dmc.sample_length == 1
791            && self.dmc.rate_index == 0x0E
792            && self.dmc.bits_remaining == 1
793            && self.dmc.timer == 0
794            && !self.apu_phase
795    }
796
797    /// Defer the next immediate DMC reload request by one CPU tick.
798    pub const fn defer_next_dmc_reload_once(&mut self) {
799        self.defer_dmc_reload_once = true;
800    }
801
802    /// Has a one-cycle DMC abort halt been raised?
803    #[must_use]
804    pub const fn dmc_abort_pending(&self) -> bool {
805        self.pending_dmc_abort
806    }
807
808    /// Read-only accessor for the DMC abort-delay countdown (CPU cycles
809    /// until `pending_dmc_abort` flips to `true`).  Exposed for the
810    /// Session-21 per-cycle DMC trace tooling (`crates/rustynes-core/src/
811    /// irq_trace.rs`) which records the scheduler's calibration state
812    /// for cross-diffing against Mesen2's `NesDmc.cpp`.
813    #[must_use]
814    pub const fn dmc_abort_delay(&self) -> u8 {
815        self.dmc_abort_delay
816    }
817
818    /// Read-only accessor for the DMC DMA cooldown countdown (CPU cycles
819    /// during which a newly-empty sample buffer must NOT raise a new
820    /// DMA request).  See [`Self::dmc_abort_delay`].
821    #[must_use]
822    pub const fn dmc_dma_cooldown(&self) -> u8 {
823        self.dmc_dma_cooldown
824    }
825
826    /// Read-only accessor for the DMC DMA delay countdown (CPU cycles
827    /// until an initial-load DMA after `$4015` enable transitions from
828    /// "armed" to `pending_dmc_dma = true`).  See [`Self::dmc_abort_delay`].
829    #[must_use]
830    pub const fn dmc_dma_delay(&self) -> u8 {
831        self.dmc_dma_delay
832    }
833
834    /// Diagnostic: the DMC channel's internal byte-timer countdown. Exposed
835    /// for the per-cycle DMC-DMA cross-diff tracing that pins the abort-context
836    /// reload-arm phase (the +4-cycle `A->B` interval divergence).
837    #[must_use]
838    pub const fn dmc_timer(&self) -> u16 {
839        self.dmc.timer()
840    }
841
842    /// Diagnostic: bits remaining in the DMC output shift register.
843    #[must_use]
844    pub const fn dmc_bits_remaining(&self) -> u8 {
845        self.dmc.bits_remaining()
846    }
847
848    /// Diagnostic: DMC output-unit silence flag.
849    #[must_use]
850    pub const fn dmc_silence(&self) -> bool {
851        self.dmc.silence()
852    }
853
854    /// Diagnostic: DMC sample buffer occupied.
855    #[must_use]
856    pub const fn dmc_buffer_full(&self) -> bool {
857        self.dmc.buffer_full()
858    }
859
860    /// Read-only accessor for the APU's two-cycle phase counter
861    /// (false = put, true = get; toggled every CPU tick by
862    /// `tick_with_external`).  See [`Self::dmc_abort_delay`].
863    #[must_use]
864    pub const fn apu_phase(&self) -> bool {
865        self.apu_phase
866    }
867
868    /// v2.0.0 beta.1 (A1 one-clock collapse): assign the APU's cycle counter
869    /// from the CANONICAL bus cycle counter. Called by the bus's per-cycle
870    /// hook (`cpu_clock` → `apu_advance_one`) immediately before
871    /// [`Self::tick_with_external`], replacing the legacy independent
872    /// `cpu_cycle += 1` mirror. The bus increments its canonical counter
873    /// earlier in the same per-cycle hook, so the value assigned here equals
874    /// the post-increment value the legacy mirror produced — the
875    /// `one_clock_invariants` harness test pins the residue. Promoted to the
876    /// only path in v2.0.0 beta.4.
877    pub const fn set_canonical_cycle(&mut self, cycle: u64) {
878        self.cpu_cycle = cycle;
879    }
880
881    /// Read-only accessor for the APU-side cumulative CPU-cycle counter
882    /// (v2.0.0-beta.1 one-clock instrumentation).
883    ///
884    /// This is one of the five counters of the timebase substrate the
885    /// v2.0.0 "Timebase" rewrite collapses (ADR 0002 + the v2.0.0
886    /// master-clock plan): `Cpu::master_clock`, `Cpu::cycles`,
887    /// `LockstepBus::cycle`, `LockstepBus::ppu_clock`, and this field are
888    /// each advanced exactly once (or by one region divider) per CPU cycle
889    /// at different points *within* the cycle, and must never drift. The
890    /// RW-1 parity collapse already derives `apu_phase` / `put_cycle` from
891    /// `(cpu_cycle + parity_seed) & 1`; exposing the raw counter lets the
892    /// test harness assert the cross-chip affine invariants
893    /// (`one_clock_invariants.rs`) that gate the beta.1 counter collapse.
894    #[must_use]
895    pub const fn cpu_cycle(&self) -> u64 {
896        self.cpu_cycle
897    }
898
899    /// CM-1: seed the absolute `apu_phase` alignment (the parity of the CPU
900    /// cycles on which the APU — incl. the DMC byte-timer — clocks). RustyNES
901    /// starts `apu_phase = false`, so the DMC always arms on one CPU-cycle
902    /// parity; Mesen's DMC arms on its `_currentCycle` alignment (one cycle off,
903    /// giving span 3 vs RustyNES's 4). Seeding `true` flips the whole APU phase
904    /// by one CPU cycle to test the Mesen-matching arm parity. Broad impact:
905    /// also shifts pulse/noise/frame-counter. Default-off.
906    pub const fn seed_apu_phase(&mut self, phase: bool) {
907        self.apu_phase = phase;
908    }
909
910    /// Address the DMC wants to read.  Valid only when `dmc_dma_pending()`
911    /// returns `true`.
912    #[must_use]
913    pub const fn dmc_dma_addr(&self) -> u16 {
914        self.dmc_dma_addr
915    }
916
917    /// v1.2 Sprint 3 (get/put scheduler, ADR-0007).
918    ///
919    /// Returns `true` while the DMC still needs an initial halt
920    /// cycle on the bus. Set by any code path that raises
921    /// `pending_dmc_dma`; the new `bus::service_dmc_dma`
922    /// implementation under the `dmc-get-put-scheduler` feature
923    /// flag clears it after processing the halt get-cycle.
924    #[must_use]
925    pub const fn dmc_need_halt(&self) -> bool {
926        self.dmc_need_halt
927    }
928
929    /// v1.2 Sprint 3 (get/put scheduler, ADR-0007).
930    ///
931    /// Returns `true` while the DMC still needs a dummy-read /
932    /// alignment cycle after the halt cycle. Cleared by the new
933    /// scheduler once the alignment cycle has been processed.
934    #[must_use]
935    pub const fn dmc_need_dummy_read(&self) -> bool {
936        self.dmc_need_dummy_read
937    }
938
939    /// v1.2 Sprint 3 — bus clears this after consuming the halt
940    /// get-cycle (`_needHalt = false` in Mesen2's `NesCpu.cpp`).
941    pub const fn clear_dmc_need_halt(&mut self) {
942        self.dmc_need_halt = false;
943    }
944
945    /// v1.2 Sprint 3 — bus clears this after consuming the
946    /// alignment cycle (`_needDummyRead = false` in Mesen2's
947    /// `NesCpu.cpp`).
948    pub const fn clear_dmc_need_dummy_read(&mut self) {
949        self.dmc_need_dummy_read = false;
950    }
951
952    /// Bus calls this when it has executed a DMC DMA fetch (post-halt) and
953    /// is delivering the sample byte.
954    pub fn complete_dmc_dma(&mut self, byte: u8) {
955        self.dmc.deliver_sample(byte);
956        // W3-Stage-3 (`mc-r1-dmc-delayed-4015`): TriCNES `DMCDMA_Get`
957        // (Emulator.cs:4148-4160) — a non-looping sample's natural end clears
958        // the APPLIED status immediately (not via the delayed slot). A
959        // looping end restarts the sample (`deliver_sample` already did), so
960        // `bytes_remaining > 0` and the status holds.
961        if self.dmc.bytes_remaining == 0 && !self.dmc.loop_flag {
962            self.dmc_status_applied = false;
963        }
964        let was_load = self.dmc_dma_is_load;
965        // v2.0 abort-context reload-arm phase fix (`mc-r1-dmc-abort-timer-phase`).
966        // In the Implicit-DMA-Abort `$4015` disable->re-enable context, this LOAD
967        // DMA's `deliver_sample` (just above) lands ON the byte-timer boundary
968        // cycle, where `clock_output` already ran at cycle-START and took the
969        // still-empty buffer (silence) BEFORE the LOAD filled it. TriCNES's LOAD
970        // GET completes 3 cyc BEFORE the boundary, so the boundary consumes the
971        // buffer into the shifter and arms a RELOAD (inserting a 4-cyc reload DMA
972        // RustyNES otherwise skips, deferring the reload chain by 4 -> A->B 580
973        // not 576 -> GET-catch skew +4 -> Y=0). Detect the boundary-coincidence
974        // (silence set + bits just reloaded to 8 + buffer now full + bytes
975        // remaining) and retroactively load the delivered byte into the shifter
976        // (un-silence) so the buffer empties and the per-cycle reload-arm fires
977        // promptly this cycle (cooldown cleared below) — reproducing TriCNES's
978        // boundary-coupled reload. The condition only ever holds in this race.
979        let abort_boundary_race = was_load
980            && self.dmc.silence
981            && self.dmc.bits_remaining == 8
982            && self.dmc.bytes_remaining > 0
983            && self.dmc.consume_buffer_into_shifter_if_silent();
984        // W3-Stage-3 (`mc-r1-dmc-delayed-4015`): this load-completion abort
985        // scheduling is the FLOOR's implicit-abort model (a pre-computed
986        // 1-cycle halt via `dmc_abort_delay` -> `pending_dmc_abort` -> the
987        // read1 abort-cancel path). Under the delayed-status port the same
988        // physics is EMERGENT (the `$4015`-enable pre-fire-window latch + the
989        // consume-edge arm + the 1-cycle override kill), so the floor
990        // scheduler is superseded — both active would double-fire ($500
991        // idx[10,11] measured 05 vs KEY 01).
992        self.pending_dmc_dma = false;
993        self.dmc_dma_is_load = false;
994        self.dmc_dma_short = false;
995        self.dmc_dma_delay = 0;
996        self.dmc_dma_cooldown = 4;
997        // v2.0 Phase 2 (`mc-r1-dmc-reenable-phase`): TriCNES `DMCDMA_Get`
998        // (`Emulator.cs:4168`) sets `CannotRunDMCDMARightNow = 2` after EVERY
999        // DMC GET (load or reload). The exclusion is decremented by 2 per get
1000        // cycle in `tick_with_external` and blocks the looping-reload arm while
1001        // `== 2` — the canonical "a DMA cannot occur within 2 cycles of a
1002        // previous DMC DMA" rule the Implicit-DMA-Abort `$540` plateau brackets.
1003        self.cannot_run_dmc_dma = 2;
1004        // Abort-context fix: the LOAD just emptied the buffer into the shifter
1005        // (boundary race), so the next reload must arm promptly — don't let the
1006        // post-LOAD cooldown suppress it for 4 cycles (which would re-introduce
1007        // the +4). Clear the cooldown so `dmc_step_reload_arm` fires next cycle.
1008        if abort_boundary_race {
1009            self.dmc_dma_cooldown = 0;
1010        }
1011        // v1.2 Sprint 3 — safety-clear the get/put flags on
1012        // completion. Under the new scheduler the bus should have
1013        // cleared them on the prior cycles already; clearing here
1014        // protects against re-arming on the next DMC request.
1015        self.dmc_need_halt = false;
1016        self.dmc_need_dummy_read = false;
1017        // W3-Stage-2 (`mc-r1-dma-unified-collapse`): this guard's phase term
1018        // means "the GET landed on the off-phase half". The normal GET half is
1019        // `apu_phase`-true at floor but `apu_phase`-false under the end-flip,
1020        // so the off-phase test inverts — otherwise the (floor-dead) suppress
1021        // path would fire at EVERY collapse GET in the 1-byte-loop contexts.
1022        let off_phase_get = self.apu_phase;
1023        if was_load
1024            && self.dmc.loop_flag
1025            && self.dmc.sample_length == 1
1026            && self.dmc.bits_remaining == 1
1027            && self.dmc.timer == 0
1028            && self.dmc.sample_buffer.is_some()
1029            && off_phase_get
1030        {
1031            self.dmc_reload_suppress_outputs = 1;
1032        }
1033    }
1034
1035    /// Complete a DMC DMA get whose fetched byte is visible before the
1036    /// get-cycle APU tick.
1037    pub fn complete_dmc_dma_before_get_tick(&mut self, byte: u8) {
1038        let was_load = self.dmc_dma_is_load;
1039        self.dmc.deliver_sample(byte);
1040        // W3-Stage-3: see `complete_dmc_dma` — non-looping natural end clears
1041        // the applied status immediately.
1042        if self.dmc.bytes_remaining == 0 && !self.dmc.loop_flag {
1043            self.dmc_status_applied = false;
1044        }
1045        if was_load
1046            && self.dmc.bytes_remaining == 0
1047            && !self.dmc.loop_flag
1048            && self.dmc.bits_remaining == 1
1049            && self.dmc.timer == 0
1050            && self.dmc.sample_buffer.is_some()
1051        {
1052            self.dmc_abort_delay = 3;
1053        }
1054        if was_load && self.dmc.loop_flag && self.dmc.sample_length == 1 {
1055            self.defer_dmc_reload_once = true;
1056        }
1057        self.pending_dmc_dma = false;
1058        self.dmc_dma_is_load = false;
1059        self.dmc_dma_short = false;
1060        self.dmc_dma_delay = 0;
1061        self.dmc_dma_cooldown = 5;
1062        // v2.0 Phase 2 (`mc-r1-dmc-reenable-phase`): see `complete_dmc_dma`.
1063        {
1064            self.cannot_run_dmc_dma = 2;
1065        }
1066        self.dmc_need_halt = false;
1067        self.dmc_need_dummy_read = false;
1068    }
1069
1070    /// Bus calls this after either consuming or suppressing a one-cycle DMC
1071    /// abort halt.
1072    pub const fn complete_dmc_abort(&mut self) {
1073        self.pending_dmc_abort = false;
1074        self.dmc_abort_delay = 0;
1075    }
1076
1077    /// v1.2 Sprint 3 iter 3 (get/put scheduler, ADR 0007) — DMC DMA
1078    /// abort with cancel semantics.
1079    ///
1080    /// Under the OLD scheduler, [`Self::complete_dmc_abort`] clears
1081    /// only the abort flag; the DMC DMA still fires afterward (the
1082    /// abort just inserts a 1-cycle halt). Under the get/put model
1083    /// the abort CANCELS the DMA entirely — no byte fetch, all
1084    /// flag state cleared — matching Mesen2's
1085    /// `processCycle::if(_abortDmcDma)` branch
1086    /// (`NesCpu.cpp:386-390`):
1087    ///
1088    /// ```text
1089    /// if(_abortDmcDma) {
1090    ///     _dmcDmaRunning = false;
1091    ///     _abortDmcDma = false;
1092    ///     _needDummyRead = false;
1093    ///     _needHalt = false;
1094    /// }
1095    /// ```
1096    ///
1097    /// This is the "Option C" semantic shift from the iter 3
1098    /// research audit: abort cancels the fetch rather than letting
1099    /// it complete after a wasted cycle. The new bus-side
1100    /// `service_dmc_dma` (under `dmc-get-put-scheduler` feature)
1101    /// calls this when it detects `dmc_abort_pending` mid-loop.
1102    pub const fn cancel_dmc_dma(&mut self) {
1103        self.pending_dmc_dma = false;
1104        self.pending_dmc_abort = false;
1105        self.dmc_abort_delay = 0;
1106        self.dmc_dma_short = false;
1107        self.dmc_dma_is_load = false;
1108        self.dmc_dma_delay = 0;
1109        self.dmc_need_halt = false;
1110        self.dmc_need_dummy_read = false;
1111    }
1112
1113    /// One CPU clock.  Bus must NOT have halted the CPU for DMC DMA when
1114    /// calling this (the bus is responsible for performing the DMA fetch
1115    /// before resuming `tick()` calls).
1116    ///
1117    /// Standalone/test convenience: production (`LockstepBus`) drives the
1118    /// canonical cycle counter via [`Self::set_canonical_cycle`] before each
1119    /// [`Self::tick_with_external`] (the v2.0.0 one-clock contract — the APU
1120    /// never self-increments). This helper self-advances the counter so
1121    /// standalone APU stepping (unit tests, the snapshot fixtures) keeps the
1122    /// one-cycle-per-tick behavior.
1123    pub fn tick(&mut self) {
1124        self.cpu_cycle = self.cpu_cycle.wrapping_add(1);
1125        self.tick_with_external(0.0);
1126    }
1127
1128    /// Same as `tick`, but accepts an additional pre-mixed audio sample
1129    /// from the cartridge (VRC6 / VRC7 / MMC5 / Sunsoft 5B / Namco 163 /
1130    /// FDS). The external value is added to the APU's own mix BEFORE the
1131    /// band-limited buffer push.
1132    ///
1133    /// The expected scale is ~ `[-0.5, 0.5]` (matching the APU mixer's
1134    /// own output range). The bus is responsible for converting whatever
1135    /// the mapper returns (currently `i16` from `Mapper::mix_audio`) into
1136    /// that range.
1137    pub fn tick_with_external(&mut self, external: f32) {
1138        // v2.0.0 beta.1 (A1 one-clock collapse, promoted to the only path in
1139        // beta.4): the APU's cycle counter is ASSIGNED from the canonical
1140        // bus counter (see `set_canonical_cycle`, called by the bus
1141        // immediately before this tick) instead of being an
1142        // independently-incremented lockstep mirror. The RW-1
1143        // `apu_phase`/`put_cycle` parity derivation below then reads from
1144        // the ONE counter.
1145
1146        // v2.0 RA-1: the DMC byte-timer + arms could clock HERE at cycle START
1147        // (on `apu_phase`), unified with the rest of the APU — Mesen
1148        // `ProcessCpuClock` at `StartCpuCycle`.
1149        //
1150        // v2.0 Program M (M-1 within-cycle order): the DMC byte-timer CLOCK +
1151        // reload-arm + reenable bookkeeping live at end-of-cycle (after the CPU's
1152        // bus access, in `dmc_tick_end`), matching Mesen `StartCpuCycle`->
1153        // `ProcessCpuClock` and TriCNES `_6502`->`_EmulateAPU` (CPU reads state ->
1154        // APU ticks/arms reload -> get/put flips). The reload arm thereby becomes
1155        // invisible to its own cycle -> first-service is the next (put) cycle ->
1156        // span-4. So the cycle-START DMC clock/arm paths below are never taken;
1157        // the LOAD delay-arm moves to the put phase of `dmc_tick_end` (the TriCNES
1158        // `DMCDMADelay` put-branch placement, Emulator.cs:1217).
1159
1160        if self.dmc_abort_delay > 0 {
1161            self.dmc_abort_delay -= 1;
1162            if self.dmc_abort_delay == 0 && !self.pending_dmc_abort {
1163                self.pending_dmc_abort = true;
1164            }
1165        }
1166        if self.dmc_dma_cooldown > 0 {
1167            self.dmc_dma_cooldown -= 1;
1168        }
1169
1170        // Triangle clocks at CPU rate.
1171        self.triangle.clock_timer();
1172
1173        // Pulse, noise, DMC clock at APU rate (every other CPU cycle).
1174        // RW-1 (`mc-r1-one-clock`): DERIVE `apu_phase` from the single per-cycle
1175        // counter + boot seed instead of a free-running toggle, so it shares ONE
1176        // source with `put_cycle` (and thus the DMA get/put parity + DMC
1177        // fire-phase) and can never drift. `cpu_cycle` was incremented above, so
1178        // `(cpu_cycle + parity_seed) & 1 == 1` reproduces the toggle-from-`false`
1179        // sequence exactly when `parity_seed == 0` (the floor config).
1180        {
1181            self.apu_phase = (self.cpu_cycle.wrapping_add(self.parity_seed) & 1) == 1;
1182        }
1183        // v2.0.0 beta.3 (A4 cycle-accurate reset): consume the scheduled
1184        // warm-reset `$4017` re-write N clocked cycles into the CPU's reset
1185        // sequence (see `Apu::reset` for the calibration). Runs after the
1186        // `apu_phase` derivation so the write's 3/4-cycle alignment delay
1187        // reads the current cycle's parity, exactly like a CPU-issued write.
1188        if self.reset_4017_delay > 0 {
1189            self.reset_4017_delay -= 1;
1190            if self.reset_4017_delay == 0 {
1191                let aligned = self.apu_phase;
1192                self.frame_counter.write(self.reset_4017_value, aligned);
1193            }
1194        }
1195        if self.apu_phase {
1196            self.pulse1.clock_timer();
1197            self.pulse2.clock_timer();
1198            self.noise.clock_timer();
1199            // F-2/M-1: the DMC byte-timer clock lives in `tick_dmc`
1200            // (end-of-cycle), not here at cycle START.
1201        }
1202
1203        // Frame counter (CPU clock). Latch the events so the bus can fan
1204        // them out to on-cart audio extensions (MMC5) after the tick.
1205        // Pass `apu_phase` AND `cpu_cycle` so the frame counter can
1206        // (a) compute APU-step timing as before and (b) mature any
1207        // pending lazy `$4015`-read IRQ-flag clear scheduled by a
1208        // previous read (Session-25, 2026-05-23 — see
1209        // `frame_counter::read_status` doc).
1210        let ev = self.frame_counter.tick(self.cpu_cycle, self.apu_phase);
1211        self.last_frame_events = ev;
1212        self.handle_frame_events(ev);
1213
1214        // v2.1.5 length halt/reload ordering: promote each channel's deferred
1215        // halt (`new_halt` -> `halt`) and pending length reload EVERY CPU cycle,
1216        // AFTER the half-frame clock in `handle_frame_events` and BEFORE the
1217        // mixer samples the channel outputs below. This realizes the 2A03's
1218        // "halt change takes effect after clocking length" and "reload ignored
1219        // during a non-zero length clock" rules (blargg `10.len_halt_timing` /
1220        // `11.len_reload_timing`; TetaNES `LengthCounter::reload` +
1221        // Mesen2 `_newHaltValue`). On the common cycle with no half-frame clock
1222        // the reload applies in-cycle (the count was untouched since the write),
1223        // so a plain length load / halt write remains byte-identical to an
1224        // immediate apply — only the write-lands-on-the-clock-cycle coincidence
1225        // the tests probe differs. The DMC has no length counter. See
1226        // `crates/rustynes-apu/src/length.rs`.
1227        self.pulse1.length.reload();
1228        self.pulse2.length.reload();
1229        self.triangle.length.reload();
1230        self.noise.length.reload();
1231
1232        // v2.0 Phase 2 (`mc-r1-dmc-reenable-phase`) reload-arm/reenable
1233        // bookkeeping and the `CannotRunDMCDMARightNow` exclusion decrement all
1234        // live at end-of-cycle (`dmc_tick_end`) under M-1, NOT here at cycle
1235        // START.
1236
1237        // Emit one mixed sample to the band-limited buffer. The external
1238        // (cartridge) audio is summed AFTER the internal non-linear mixer
1239        // since it's already a linear value.
1240        // Per-channel mute overlay. With the default `CHANNEL_MASK_ALL` every
1241        // `gate(..)` returns the raw output unchanged, so this is byte-identical
1242        // to the un-masked mix (the determinism contract — the oracle / test
1243        // ROMs never clear a bit). A cleared bit forces that channel's raw
1244        // output to 0 BEFORE the non-linear mixer, so it contributes nothing.
1245        let mask = self.channel_mask;
1246
1247        // v2.3.5 C1 — the DEFAULT-CONFIGURATION fast path.
1248        //
1249        // Every gate/scale below is inert at the shipped default: the
1250        // determinism contract says the oracle and the test ROMs never clear a
1251        // mask bit or change a gain, so `gate` returns its input unchanged and
1252        // `scale` returns `round(v * 1.0) == v`. The emulator was still paying,
1253        // every CPU cycle at 1.789 MHz, for a 6-wide `f32` array copy, five
1254        // integer mask tests, five float compares, and a sixth mask test for the
1255        // external sum -- to produce a result identical to the ungated mix.
1256        //
1257        // Same shape as the PPU fast dot path, which is the one core
1258        // optimization this project has adopted: hoist the
1259        // "is-this-the-default?" question out of the per-cycle body and take a
1260        // branch with none of the machinery. It is a strict specialization, not
1261        // an approximation -- `mix()` receives exactly the same five arguments
1262        // it would have received, so the output is byte-identical by
1263        // construction rather than by measurement. `apu_default_mix_matches_the_gated_path`
1264        // pins that across a 2,048-point sweep anyway.
1265        if mask == CHANNEL_MASK_ALL && self.channel_gain == CHANNEL_GAIN_UNITY {
1266            self.last_external = external;
1267            let mixed = self.mixer.mix(
1268                self.pulse1.output(),
1269                self.pulse2.output(),
1270                self.triangle.output(),
1271                self.noise.output(),
1272                self.dmc.output(),
1273            ) + external;
1274            #[cfg(feature = "debug-hooks")]
1275            if self.audio_prov.is_some() {
1276                self.record_mix_armed(mixed, external);
1277            }
1278            self.blip.add_sample(mixed);
1279            // Nothing follows the general path's `add_sample` but comments --
1280            // the get/put flip moved to `dmc_tick_end` under M-2 -- so there is
1281            // no shared tail to run before returning. Verified by reading it,
1282            // not assumed: a missed tail here would desynchronise the two paths.
1283            return;
1284        }
1285
1286        let gate = |bit: u8, v: u8| if mask & (1 << bit) != 0 { v } else { 0 };
1287        // v1.4.0 Workstream C — per-channel gain (a UI mixing overlay). With the
1288        // default `CHANNEL_GAIN_UNITY` every `scale(..)` returns `round(v * 1.0)
1289        // == v` and `external * 1.0 == external`, so this is byte-identical to
1290        // the pre-gain mix (the determinism contract — the oracle / test ROMs
1291        // never change a gain). A gain != 1.0 scales that channel's contribution
1292        // before the non-linear mixer (gain 0.0 == a cleared mask bit). The
1293        // `gain` slice is checked-for-unity-and-skipped so the default path is
1294        // the exact integer-gate code as before.
1295        let gain = self.channel_gain;
1296        // `max` is the channel's native raw ceiling (pulse/tri/noise = 15, DMC =
1297        // 127); the scaled value is clamped to it so the non-linear mixer's
1298        // `pulse_table` (31) / `tnd_table` (203) index bounds always hold even at
1299        // gain 2.0. At gain 1.0 the value is returned unchanged (byte-identical).
1300        let scale = |bit: usize, v: u8, max: u8| {
1301            let g = gain[bit];
1302            if g == 1.0 {
1303                v
1304            } else {
1305                #[allow(
1306                    clippy::cast_possible_truncation,
1307                    clippy::cast_sign_loss,
1308                    clippy::cast_precision_loss
1309                )]
1310                {
1311                    roundf(f32::from(v) * g).clamp(0.0, f32::from(max)) as u8
1312                }
1313            }
1314        };
1315        // v2.1.6 — stash the RAW (pre-gain) external contribution for the
1316        // frontend expansion-channel scope/VU. Write-only from synthesis; never
1317        // read back into the mix, so it cannot alter deterministic output.
1318        self.last_external = external;
1319        let ext_gain = gain[5];
1320        let ext = if ext_gain == 1.0 {
1321            external
1322        } else {
1323            external * ext_gain
1324        };
1325        let mixed = self.mixer.mix(
1326            scale(0, gate(0, self.pulse1.output()), 15),
1327            scale(1, gate(1, self.pulse2.output()), 15),
1328            scale(2, gate(2, self.triangle.output()), 15),
1329            scale(3, gate(3, self.noise.output()), 15),
1330            scale(4, gate(4, self.dmc.output()), 127),
1331        ) + if mask & (1 << 5) != 0 { ext } else { 0.0 };
1332        #[cfg(feature = "debug-hooks")]
1333        if self.audio_prov.is_some() {
1334            // RAW `external`, not the gained `ext`, and not zero when the mask
1335            // bit clears it. The five channel fields are already the raw
1336            // pre-gate outputs, so recording a gain-scaled or mask-zeroed
1337            // expansion value would make ONE field follow the user's mixer
1338            // sliders while five describe the chip -- and would make this path
1339            // disagree with the fast path, which records the raw value. Review
1340            // caught the disagreement; this resolves it toward the documented
1341            // semantic rather than toward the local variable that happened to
1342            // be in scope.
1343            self.record_mix_armed(mixed, external);
1344        }
1345        self.blip.add_sample(mixed);
1346
1347        // v2.0 interleaved-DMA Phase A: toggle the global get/put flip-flop once
1348        // per CPU cycle, right after the APU tick (TriCNES `APU_PutCycle =
1349        // !APU_PutCycle` after `_EmulateAPU()`, `Emulator.cs:920`). Gated on
1350        // `dmc_driven_externally` so the default build never touches it
1351        // (byte-identical); under the R1 substrate this is the single
1352        // per-cycle get/put counter the interleaved DMA (Phase B) consumes.
1353        // RW-1 (`mc-r1-one-clock`): `put_cycle` is the COMPLEMENT of `apu_phase`,
1354        // derived from the same counter — not a second independent flip-flop.
1355        // In the floor config the two toggles already stayed perfectly
1356        // complementary (both flip once per `tick_with_external`); RW-1 makes
1357        // that structural so RW-2 has a SINGLE place to make the parity
1358        // OAM-DMA-aware. The bus's get/put decision (`get = !put_cycle`) and the
1359        // F-2 DMC clock (`!put_cycle`) then read this coherent value.
1360        // M-2 (`mc-r1-counter-collapse`): the get/put `put_cycle` flip moves to
1361        // END of the CPU cycle (`dmc_tick_end`), AFTER the bus access — the
1362        // references' "access -> APU tick -> get/put flip" order. So at the START
1363        // (here) `put_cycle` is LEFT at its prior-cycle value; the bus access this
1364        // cycle therefore reads `put_cycle = !apu_phase_{N-1} = apu_phase_N`,
1365        // one parity position later than the floor's `!apu_phase_N`. `apu_phase`
1366        // itself (the APU IRQ line / C1 phi2 sample source) still flips at start
1367        // (line ~927), so C1 is invariant.
1368    }
1369
1370    fn handle_frame_events(&mut self, ev: FrameEvents) {
1371        if ev.quarter {
1372            self.pulse1.clock_quarter_frame();
1373            self.pulse2.clock_quarter_frame();
1374            self.triangle.clock_quarter_frame();
1375            self.noise.clock_quarter_frame();
1376        }
1377        if ev.half {
1378            self.pulse1.clock_half_frame();
1379            self.pulse2.clock_half_frame();
1380            self.triangle.clock_half_frame();
1381            self.noise.clock_half_frame();
1382        }
1383    }
1384
1385    /// Visibility-delay promotion (called at END of cycle, after the CPU's bus
1386    /// access): a reload latched this cycle becomes visible to the NEXT cycle's
1387    /// DMA servicing (first-service on the put cycle => span 4), matching
1388    /// TriCNES `_EmulateAPU`-after-`_6502` invisible-arm ordering.
1389    pub fn promote_dmc_pending_next(&mut self) {
1390        if self.pending_dmc_dma_next {
1391            self.pending_dmc_dma_next = false;
1392            self.pending_dmc_dma = true;
1393        }
1394    }
1395
1396    /// v2.0 Program M (M-1 within-cycle order, `mc-r1-dmc-bytetimer-end`): clock
1397    /// the DMC byte-timer + arm the reload at END of cycle (after the CPU's bus
1398    /// access), the mirror of the cycle-START block in `tick_with_external` that
1399    /// `dmc_clock_at_start` now suppresses. Order matches `tick_with_external`:
1400    /// byte-timer clock (on this cycle's already-set `apu_phase`) -> reenable
1401    /// consume-edge clear -> reload-arm -> `cannot_run` decrement. The bus calls
1402    /// this from `cpu_clock_apu_dmc` (end-of-cycle), AFTER
1403    /// `promote_dmc_pending_next` so a reload latched here is invisible to its
1404    /// own cycle (promoted -> serviced the NEXT cycle = span-4, like the
1405    /// references). The LOAD delay-arm is NOT here — it stays at cycle-start.
1406    pub fn dmc_tick_end(&mut self) {
1407        // W3-Stage-3 (`mc-r1-dmc-delayed-4015`): the 1-cycle implicit-abort
1408        // kill — TriCNES clears `APU_ImplicitAbortDMC4015` at the END of
1409        // `_6502` whenever the DMA is pending (Emulator.cs:9000-9003), i.e.
1410        // BEFORE `_EmulateAPU`'s boundary work. A flag set by the previous
1411        // cycle's consume edge therefore survives exactly one CPU access
1412        // (one serviced halt cycle if it was a read; none if a write — "it
1413        // won't run at all") and dies here.
1414        if self.pending_dmc_dma && self.dmc_implicit_abort {
1415            self.dmc_implicit_abort = false;
1416        }
1417        let d4015_bits_before = self.dmc.bits_remaining();
1418        let dmc_bits_before = self.dmc.bits_remaining();
1419        // The byte-timer-end flag composes only with the canonical apu_phase
1420        // clock (the `mc-r1-full-cpu` config); the cpu-rate / phase-minus1
1421        // diagnostic clock variants are not combined with it.
1422        // M-2 (`mc-r1-counter-collapse`): the get/put `put_cycle` flip moved to
1423        // end-of-cycle (one parity position later), so the GET decision
1424        // (`get = !put_cycle`) now reads the shifted parity. The DMC byte-timer
1425        // FIRE must follow the SAME shift or the GET de-syncs from the byte-timer
1426        // wrap (wedge). At entry `put_cycle == apu_phase` (the prior end-flip),
1427        // so clocking on `!self.put_cycle == !apu_phase` shifts the byte-timer by
1428        // one to stay locked to the shifted GET — ONE counter driving both.
1429        let timer_phase = !self.put_cycle;
1430        if timer_phase {
1431            self.dmc.clock_timer();
1432        }
1433        // W3-Stage-3 (`mc-r1-dmc-delayed-4015`): the consume-edge transfer
1434        // (Emulator.cs:1163-1175) — at the shifter-consume edge (bits 1 -> 8
1435        // on this end-tick's byte-timer fire) a latched
1436        // `dmc_set_implicit_abort` becomes the live `dmc_implicit_abort`
1437        // service-gate override AND arms the DMA directly (TriCNES
1438        // `if (BytesRemaining > 0 || SetImplicit) { if (!DoDMCDMA &&
1439        // CannotRun != 2) { DoDMCDMA = true; Halt = true; } ... }` — the arm
1440        // fires regardless of the buffer state). The armed DMA runs for
1441        // exactly one read cycle under the override (the kill above), then
1442        // waits for the delayed status — the emergent 1-cycle implicit abort.
1443        if timer_phase
1444            && self.dmc_set_implicit_abort
1445            && self.dmc.bits_remaining() == 8
1446            && d4015_bits_before <= 1
1447        {
1448            self.dmc_implicit_abort = true;
1449            self.dmc_set_implicit_abort = false;
1450            if !self.pending_dmc_dma && self.cannot_run_dmc_dma != 2 {
1451                self.pending_dmc_dma = true;
1452                self.dmc_dma_is_load = false;
1453                self.dmc_dma_short = false;
1454                self.dmc_dma_addr = self.dmc.dma_addr();
1455                self.dmc_need_halt = true;
1456                self.dmc_need_dummy_read = true;
1457            }
1458        }
1459        // W3-Stage-2 (`mc-r1-dma-unified-collapse`): the TriCNES `DMCDMADelay`
1460        // put-branch — the `$4015`-enable LOAD delay counts down ONLY on the
1461        // put phase of this end-of-cycle tick (Emulator.cs:1217 sits in the
1462        // `else` of the get branch), arming the halt at the end of a PUT cycle
1463        // so the load's first halted cycle is always a GET (entry-on-get =
1464        // span 3) regardless of the write cycle's parity. The put phase here
1465        // is `!timer_phase` (the complement of the shifted byte-timer phase).
1466        if !timer_phase {
1467            self.dmc_step_delay_arm_put_end();
1468        }
1469        if self.dmc_reenable_period_block && self.dmc.bits_remaining() == 8 && dmc_bits_before <= 1
1470        {
1471            self.dmc_reenable_period_block = false;
1472        }
1473        // W3-Stage-4 (`mc-r1-dmc-delayed-4015` grid correction): the TriCNES
1474        // reload arm is consume-edge-quantized. A consume edge that lands ON
1475        // the GET-delivery cycle itself (the X=8/9 Implicit `$540` restart
1476        // race: the silent-restart load GET collides with the free-running
1477        // byte-timer boundary) is arm-BLOCKED by `CannotRunDMCDMARightNow ==
1478        // 2` (Emulator.cs:1165; the :1186 decrement runs later that same
1479        // end-tick, so `== 2` is only ever observable at the same-cycle
1480        // edge) — and TriCNES holds NO level request: the chain simply waits
1481        // for the NEXT consume edge (one full byte period). Our `needs_dma()`
1482        // is level-triggered and would re-arm 4 cycles later (cooldown
1483        // expiry) — one grid boundary early, the `$540[8,9]` cliff. Latch the
1484        // suppression at the blocked same-cycle edge; release at the next
1485        // consume edge right here (BEFORE the reload-arm step) so the
1486        // deferred arm fires exactly on-grid, like TriCNES's
1487        // `BytesRemaining > 0` edge arm.
1488        if timer_phase && self.dmc.bits_remaining() == 8 && d4015_bits_before <= 1 {
1489            if self.dmc_edge_arm_suppress {
1490                self.dmc_edge_arm_suppress = false;
1491            } else if self.cannot_run_dmc_dma == 2 && self.dmc.needs_dma() && !self.pending_dmc_dma
1492            {
1493                self.dmc_edge_arm_suppress = true;
1494            }
1495        }
1496        self.dmc_step_reload_arm();
1497        // M-2: the `cannot_run` decrement is TriCNES's get-cycle decrement; under
1498        // the collapse the get cycle is the shifted `timer_phase`, not raw
1499        // apu_phase.
1500        if timer_phase && self.cannot_run_dmc_dma > 0 {
1501            self.cannot_run_dmc_dma = self.cannot_run_dmc_dma.saturating_sub(2);
1502        }
1503        // W3-Stage-3 (`mc-r1-dmc-delayed-4015`): the TriCNES
1504        // `APU_DelayedDMC4015` countdown (Emulator.cs:1214-1224) — decremented
1505        // EVERY CPU cycle after the get/put branch work (the byte-timer /
1506        // reload-arm / load-delay above). On expiry the latched `$4015` DMC
1507        // status APPLIES: `APU_Status_DMC = APU_Status_DelayedDMC`, and a
1508        // disable zeroes `bytes_remaining` HERE rather than at the write. The
1509        // bus-side service gate reads `dmc_status_applied` per cycle, so an
1510        // in-flight DMA whose status drops stops being serviced — the
1511        // emergent explicit abort.
1512        if self.dmc_delayed_4015 > 0 {
1513            self.dmc_delayed_4015 -= 1;
1514            if self.dmc_delayed_4015 == 0 {
1515                self.dmc_status_applied = self.dmc_delayed_status;
1516                if !self.dmc_status_applied {
1517                    self.dmc.bytes_remaining = 0;
1518                }
1519            }
1520        }
1521        // M-2 (`mc-r1-counter-collapse`): flip the get/put parity HERE at
1522        // end-of-cycle (after the CPU's bus access + the byte-timer/reload-arm
1523        // tick above), matching the references' "access -> APU tick -> get/put
1524        // flip" order. `put_cycle = !apu_phase` of the cycle that just ran; the
1525        // NEXT cycle's bus access reads this value. (Under bytetimer-end alone
1526        // this flip stays at cycle-start in `tick_with_external`.)
1527        {
1528            self.put_cycle = !self.apu_phase;
1529        }
1530    }
1531
1532    /// W3-Stage-2 (`mc-r1-dma-unified-collapse`): the TriCNES `DMCDMADelay`
1533    /// put-branch body — same arm as [`Self::dmc_step_delay_arm`] but ticked
1534    /// only on the put phase of `dmc_tick_end` (value units = put end-ticks,
1535    /// set to 2 at the `$4015` enable like TriCNES `DMCDMADelay = 2`).
1536    fn dmc_step_delay_arm_put_end(&mut self) {
1537        if self.dmc_dma_delay > 0 {
1538            self.dmc_dma_delay -= 1;
1539            if self.dmc_dma_delay == 0 && !self.pending_dmc_dma {
1540                self.pending_dmc_dma = true;
1541                self.dmc_dma_short = self.dmc_dma_is_load;
1542                self.dmc_dma_addr = self.dmc.dma_addr();
1543                self.dmc_need_halt = true;
1544                self.dmc_need_dummy_read = true;
1545            }
1546        }
1547    }
1548
1549    /// DMC delay-arm step: countdown the load-DMA delay and arm `pending_dmc_dma`
1550    /// when it expires. Extracted from `tick_with_external` so `tick_dmc` (F-2)
1551    /// can run it at end-of-cycle.
1552    fn dmc_step_delay_arm(&mut self) {
1553        if self.dmc_dma_delay > 0 {
1554            self.dmc_dma_delay -= 1;
1555            if self.dmc_dma_delay == 0 && !self.pending_dmc_dma {
1556                self.pending_dmc_dma = true;
1557                self.dmc_dma_short = self.dmc_dma_is_load;
1558                self.dmc_dma_addr = self.dmc.dma_addr();
1559                self.dmc_need_halt = true;
1560                self.dmc_need_dummy_read = true;
1561            }
1562        }
1563    }
1564
1565    /// DMC reload-arm step: arm a reload DMA when the sample buffer empties
1566    /// (subject to cooldown / suppress / defer). Extracted for `tick_dmc` (F-2).
1567    #[allow(clippy::too_many_lines)]
1568    fn dmc_step_reload_arm(&mut self) {
1569        // final lever #1 (`mc-r1-dmc-halt-subpos`): master-clock DMA-halt
1570        // sub-position. The reload byte-timer wraps and arms on the apu_phase
1571        // get cycle (so the CPU recognizes the halt at the NEXT read1 = one CPU
1572        // cycle too late -> the GET lands adjacent to the `LDA $4000` data read,
1573        // which sees the GET's $00 -> Y=3). TriCNES arms one CPU cycle EARLIER so
1574        // the GET preempts the operand-high fetch (re-driving $40 -> Y=4). On the
1575        // `!apu_phase` cycle IMMEDIATELY preceding the wrap, the byte-timer sits
1576        // at `timer==0 && bits_remaining==1` (the final output bit is one
1577        // apu-clock from emptying the byte). Pre-arm `pending_dmc_dma` HERE, one
1578        // CPU cycle early. Scoped EXACTLY to the X=10/11 boundary by the
1579        // `cannot_run_dmc_dma == 2` exclusion (post-LOAD-GET window) — fires 6x,
1580        // nowhere else — so steady-state GETs + SH* are untouched (context-local,
1581        // distinct from a global byte-timer phase shift that shatters SH*).
1582        // The pre-wrap `!apu_phase` cycle that uniquely marks the `$540` X=10/11
1583        // boundary: the reload byte-timer is at `timer==0 && bits_remaining==1`
1584        // (one apu-clock from emptying the byte), the LOAD has just FILLED the
1585        // buffer (`buffer_full` -> needs_dma still FALSE), and we are inside the
1586        // post-LOAD-GET `cannot_run == 2` exclusion. This is distinct from the
1587        // `$500`/`$520` X=10/11 blocks (Key1/Key2, already correct) whose buffer
1588        // is already empty at this point (no `buffer_full` pre-wrap cycle), so
1589        // pre-arming here leaves them untouched. Arm `pending_dmc_dma` one CPU
1590        // cycle early so the wrap-cycle's `read1` recognizes the halt (the GET
1591        // preempts the operand-high fetch -> $40 re-driven -> Y 3->4) instead of
1592        // the next read1 (GET adjacent to the data read -> $00 seen -> Y=3).
1593        // W3-Stage-3 (`mc-r1-dmc-delayed-4015`): the halt-subpos boundary
1594        // pre-arm is a floor-unit expression of the same missing `$4015`
1595        // application delay (the Stage-2 residual map); under the
1596        // delayed-status port it is superseded by the emergent consume-edge
1597        // arm — both active double-fire on the X=10/11 entries.
1598        // Gate also on the visibility-delay latch so a reload cannot double-arm
1599        // while one is latched-but-not-yet-promoted (would cascade/wedge).
1600        let already = self.pending_dmc_dma || self.pending_dmc_dma_next;
1601        // v2.0 Phase 2 (`mc-r1-dmc-reenable-phase`): TriCNES gates the reload
1602        // arm on `CannotRunDMCDMARightNow != 2` (`Emulator.cs:1165`) — a reload
1603        // cannot arm on the get cycle immediately following a DMC GET. That
1604        // exclusion is hit ONLY at the Implicit-DMA-Abort X=10/11 `$4015`
1605        // re-enable boundary (the LOAD GET lands so the next byte-timer wrap
1606        // coincides with the window) — confirmed by the probe firing on exactly
1607        // those two entries. A full-period reload deferral there OVERSHOOTS
1608        // ($540[10,11] -> 00, Y=0) because RustyNES's start-clock + Option-buffer
1609        // structure shifts the whole chain a byte; TriCNES instead realigns the
1610        // byte-timer phase by ~1 cycle. So at the boundary we apply a ONE-SHOT
1611        // swept byte-timer phase shift (`REENABLE_BUMP`, env-tunable) that
1612        // realigns the looping-reload chain like TriCNES's re-enable, while the
1613        // bare `cannot_run == 2` gate still defers this cycle's arm.
1614        let cannot_run_now = self.cannot_run_dmc_dma == 2;
1615        // One-shot byte-timer realignment at the exclusion boundary. `period_block`
1616        // is the one-shot guard (set here, cleared at the next consume edge in
1617        // `tick_with_external`) so the bump is applied exactly once per boundary.
1618        if cannot_run_now
1619            && self.dmc.needs_dma()
1620            && !already
1621            && self.dmc_dma_delay == 0
1622            && self.dmc_reload_suppress_outputs == 0
1623            && self.dmc_dma_cooldown == 0
1624            && !self.defer_dmc_reload_once
1625            && !self.dmc_reenable_period_block
1626        {
1627            let bump = crate::dmc::REENABLE_BUMP.load(core::sync::atomic::Ordering::Relaxed);
1628            if bump != 0 {
1629                self.dmc.bump_timer_phase(bump);
1630            }
1631            self.dmc_reenable_period_block = true;
1632        }
1633        // W3-Stage-4: the consume-edge-quantization suppression (see
1634        // `dmc_tick_end`) — while latched, the level-held `needs_dma()` must
1635        // NOT arm; the deferred arm fires at the next consume edge.
1636        let edge_suppressed = self.dmc_edge_arm_suppress;
1637        if self.dmc.needs_dma()
1638            && !already
1639            && self.dmc_dma_delay == 0
1640            && !cannot_run_now
1641            && !edge_suppressed
1642        {
1643            if self.dmc_reload_suppress_outputs > 0
1644                || self.dmc_dma_cooldown > 0
1645                || self.defer_dmc_reload_once
1646            {
1647                self.defer_dmc_reload_once = false;
1648            } else {
1649                // Visibility-delay: a reload latches into `_next` (promoted next
1650                // cycle) so first-service lands on the put cycle (span 4). Loads
1651                // and the default keep direct `pending_dmc_dma` (first-service get).
1652                {
1653                    self.pending_dmc_dma_next = true;
1654                }
1655                self.dmc_dma_is_load = false;
1656                self.dmc_dma_short = false;
1657                self.dmc_dma_addr = self.dmc.dma_addr();
1658                self.dmc_need_halt = true;
1659                self.dmc_need_dummy_read = true;
1660            }
1661        } else {
1662            self.defer_dmc_reload_once = false;
1663        }
1664    }
1665
1666    /// v2.0 F-2: advance ONLY the DMC byte-timer + DMA arm by one CPU cycle.
1667    /// The R1 bus calls this at END of cycle (after the access) when
1668    /// [`Self::set_dmc_driven_externally`] is set, so the DMC fire-phase matches
1669    /// main's `tick_one_cpu_cycle` (the cycle DMASync's `$4000` conflict
1670    /// expects) while the rest of the APU — incl. the IRQ line — stays on the
1671    /// cycle-start `tick_with_external`. Order mirrors `tick_with_external`:
1672    /// delay-arm → APU-rate timer clock (via the `dmc_ext_phase` flip-flop) →
1673    /// reload-arm.
1674    pub fn tick_dmc(&mut self) {
1675        self.dmc_step_delay_arm();
1676        // Divergence A: clock the DMC byte-timer off the SHARED `put_cycle`
1677        // counter (the same flip-flop the interleaved DMA's get/put decision
1678        // uses) instead of a separate `dmc_ext_phase`, so the DMC fire-phase and
1679        // the get/put parity share ONE seed and can NEVER drift (TriCNES seeds
1680        // `APU_PutCycle` + the DMC timer together). The DMC clocks at the APU
1681        // rate (every other CPU cycle). Polarity `!put_cycle`: main clocks the
1682        // DMC on `apu_phase`-true (cycles 1,3,5 — odd); `put_cycle` is seeded so
1683        // its true-phase falls on EVEN cycles, so `!put_cycle` recovers main's
1684        // ODD-cycle DMC fire-phase (the DMASync-positioning alignment).
1685        if !self.put_cycle {
1686            self.dmc.clock_timer();
1687        }
1688        self.dmc_step_reload_arm();
1689    }
1690
1691    /// v2.0 interleaved-DMA Phase B: advance ONLY the DMC byte-timer clock (no
1692    /// delay/reload ARM), for a cycle of an interleaved DMC DMA span. The timer
1693    /// advances (so the variable-3/4-span feeds back into the next fire-cycle —
1694    /// divergence-A self-consistency) WITHOUT re-arming a new DMA mid-span (no
1695    /// cascade). Toggles the same `dmc_ext_phase` flip-flop as [`Self::tick_dmc`]
1696    /// so the every-other-cycle cadence stays consistent across normal + DMA
1697    /// cycles. (In the burst model this re-wedged; in the per-cycle interleaved
1698    /// model each DMA cycle is discrete and arm-gated, so it should hold.)
1699    pub fn tick_dmc_timer_only(&mut self) {
1700        // Divergence A: clock off the shared `put_cycle` counter (see `tick_dmc`).
1701        if !self.put_cycle {
1702            self.dmc.clock_timer();
1703        }
1704    }
1705
1706    /// v2.0 F-2: route the DMC byte-timer + arm to [`Self::tick_dmc`] instead of
1707    /// `tick_with_external`. Default `false` = byte-identical.
1708    pub const fn set_dmc_driven_externally(&mut self, on: bool) {
1709        self.dmc_driven_externally = on;
1710    }
1711
1712    /// v2.0 interleaved-DMA Phase A: the global get/put flip-flop (TriCNES
1713    /// `APU_PutCycle`). `true` = put cycle, `false` = get cycle.
1714    #[must_use]
1715    pub const fn put_cycle(&self) -> bool {
1716        self.put_cycle
1717    }
1718
1719    /// v2.0 interleaved-DMA Phase A: seed the global get/put flip-flop from an
1720    /// `APUAlignment` value (TriCNES `Emulator.cs:685/776`), the single seed the
1721    /// interleaved DMA (Phase B) will share with the DMC fire-phase (divergence
1722    /// A). The low bit selects the parity (TriCNES case 0/2 -> put, 1/3 -> get).
1723    ///
1724    /// Phase A seeds ONLY `put_cycle` and deliberately leaves `dmc_ext_phase`
1725    /// untouched, so the un-wedged feature-on behavior is preserved (the f2e
1726    /// experiment proved flipping `dmc_ext_phase` alone regresses). The exact
1727    /// `put_cycle` <-> `dmc_ext_phase` pairing is determined empirically in
1728    /// Phase B, when the bus first consumes `put_cycle` for the get/put decision.
1729    pub const fn seed_apu_alignment(&mut self, alignment: u8) {
1730        self.put_cycle = (alignment & 1) == 0;
1731        // RW-1 (`mc-r1-one-clock`): record the boot parity as the ONE seed both
1732        // `apu_phase` and `put_cycle` derive from. `alignment == 0` -> seed 0,
1733        // which reproduces the floor config (boot `apu_phase = false` +
1734        // put-on-even) exactly. Set at power-on, reset, and restore; constant
1735        // otherwise. The legacy `put_cycle` assignment above is harmless when the
1736        // flag is on (the next derivation overwrites it from `cpu_cycle`).
1737        {
1738            self.parity_seed = (alignment & 1) as u64;
1739        }
1740    }
1741
1742    /// W3-Stage-4 (2026-06-10): whether the most recent [`Apu::restore`]
1743    /// blob carried the Stage-4 parity/DMA-state tail. The bus consults this
1744    /// after a snapshot restore: when `true` the exact `put_cycle` /
1745    /// `parity_seed` phase came from the blob and must NOT be overwritten by
1746    /// the boot [`Self::seed_apu_alignment`] call (pre-Stage-4 blobs lack the
1747    /// tail, so the bus falls back to the boot seed exactly as before).
1748    #[must_use]
1749    pub const fn snapshot_restored_parity(&self) -> bool {
1750        self.restored_parity_tail
1751    }
1752
1753    /// CPU register write (`$4000-$4017` excluding `$4014`).
1754    pub fn write_register(&mut self, addr: u16, value: u8) {
1755        // v2.3.7 "Overtone" — attribute the write BEFORE dispatching it, so the
1756        // recorded value is what the CPU put on the bus rather than whatever a
1757        // channel decided to keep. One `Option` test when disarmed.
1758        #[cfg(feature = "debug-hooks")]
1759        if let Some(p) = self.audio_prov.as_mut() {
1760            p.reg_attrib
1761                .record(addr, p.attrib_pc, p.attrib_cycle, value);
1762        }
1763        match addr {
1764            0x4000 => self.pulse1.write_ctrl(value),
1765            0x4001 => self.pulse1.write_sweep(value),
1766            0x4002 => self.pulse1.write_timer_lo(value),
1767            0x4003 => self.pulse1.write_timer_hi(value),
1768            0x4004 => self.pulse2.write_ctrl(value),
1769            0x4005 => self.pulse2.write_sweep(value),
1770            0x4006 => self.pulse2.write_timer_lo(value),
1771            0x4007 => self.pulse2.write_timer_hi(value),
1772            0x4008 => self.triangle.write_linear(value),
1773            0x4009 => {} // unused
1774            0x400A => self.triangle.write_timer_lo(value),
1775            0x400B => self.triangle.write_timer_hi(value),
1776            0x400C => self.noise.write_ctrl(value),
1777            0x400D => {} // unused
1778            0x400E => self.noise.write_period(value),
1779            0x400F => self.noise.write_length(value),
1780            0x4010 => self.dmc.write_ctrl(value),
1781            0x4011 => self.dmc.write_dac(value),
1782            0x4012 => self.dmc.write_sample_addr(value),
1783            0x4013 => self.dmc.write_sample_length(value),
1784            0x4015 => self.write_status(value),
1785            0x4017 => {
1786                // $4017 also clears DMC IRQ?  No — only $4015 clears DMC.
1787                // But writing $4017 with bit 6 set clears frame IRQ.
1788                // The frame counter handles the inhibit-clears-flag effect.
1789                // Apu-aligned: cycle is even when apu_phase will toggle to
1790                // true on the NEXT tick.  Our `apu_phase` reflects the
1791                // *current* state after the tick.  Per nesdev: "If the write
1792                // occurs during an APU clock (CPU cycle 1, 3, 5...) the
1793                // effects occur 3 CPU cycles after the write; if during a
1794                // non-APU clock, the effects occur 4 CPU cycles after."
1795                let aligned = self.apu_phase;
1796                self.frame_counter.write(value, aligned);
1797            }
1798            _ => {}
1799        }
1800    }
1801
1802    // W3-Stage-3: the delayed-4015 cfg arms (the latch dispatch + the
1803    // superseded-compensation gating) push the counted length just past the
1804    // clippy limit; the body is mostly per-feature cfg blocks.
1805    #[allow(clippy::too_many_lines)]
1806    fn write_status(&mut self, value: u8) {
1807        self.pulse1.length.set_enabled((value & 0x01) != 0);
1808        self.pulse2.length.set_enabled((value & 0x02) != 0);
1809        self.triangle.length.set_enabled((value & 0x04) != 0);
1810        self.noise.length.set_enabled((value & 0x08) != 0);
1811        let enable_dmc = (value & 0x10) != 0;
1812        let was_active = self.dmc.active();
1813        let implicit_stop_edge = enable_dmc
1814            && !was_active
1815            && !self.dmc.loop_flag
1816            && self.dmc.sample_length == 1
1817            && self.dmc.rate_index == 0x0E
1818            && self.dmc.bits_remaining == 1
1819            && self.dmc.sample_buffer.is_none();
1820        // W3-Stage-3 (`mc-r1-dmc-delayed-4015`): the TriCNES delayed-status
1821        // latch replaces the immediate `set_enabled` application — see
1822        // `latch_delayed_dmc_4015`.
1823        self.latch_delayed_dmc_4015(enable_dmc);
1824        // W3-Stage-3 (`mc-r1-dmc-delayed-4015`): TriCNES gates the enable-side
1825        // LOAD-delay arm on `APU_Silent` (Emulator.cs:9519-9522 — "the sample
1826        // will only begin playing if the DMC is currently silent"; otherwise
1827        // the restart is picked up at the NEXT shifter-consume edge). Our
1828        // floor condition (`needs_dma()` alone) arms the load immediately
1829        // even while the output unit is still draining the prior looping
1830        // byte — in the Implicit Loop3/`$540` re-enable race that fires a
1831        // span-3 load-style DMA 2-4 sweep positions before the hardware's
1832        // boundary-quantized reload (the `03,03` lead-in + plateau-2-early).
1833        let load_arm = enable_dmc && !was_active && self.dmc.needs_dma() && self.dmc.silence();
1834        if load_arm {
1835            self.pending_dmc_dma = false;
1836            self.dmc_dma_is_load = true;
1837            self.dmc_dma_short = true;
1838            self.dmc_dma_addr = self.dmc.dma_addr();
1839            // Load DMAs attempt to halt on the get cycle during the second
1840            // APU cycle after `$4015` enables DMC. In this emulator
1841            // `apu_phase == true` is the get half of the current APU cycle.
1842            //
1843            // v2.0 R-1 core C-1: under R1 (`dmc_driven_externally`) the DMC
1844            // clocks on `!put_cycle` (F-2), so `apu_phase` is the WRONG phase
1845            // basis for the load-arm delay just as it is for the abort `cuo`
1846            // (P-2: the R1 load DMA fires 1-2 cyc early → the 1-byte abort
1847            // sample's narrow active window lands off the swept `$4015` disable
1848            // → `disable_was_active` 12 vs 76). Use the DMC's actual phase.
1849            // W3-Stage-2 (`mc-r1-dma-unified-collapse`): TriCNES
1850            // `DMCDMADelay = 2` — two put end-ticks of `dmc_tick_end` (the
1851            // write cycle's own end-tick counts when it lands on a put, the
1852            // "really like 2 : 3" parity absorption), so the halt arms at the
1853            // end of a PUT and the load enters on a GET regardless of the
1854            // write parity. Replaces the every-cycle `apu_phase ? 4 : 3`
1855            // countdown whose value bakes in the floor GET parity.
1856            {
1857                self.dmc_dma_delay = 2;
1858            }
1859            // W3-Stage-2: the implicit-stop-edge -1 is a CPU-cycle-unit
1860            // calibration of the every-cycle countdown; under the TriCNES
1861            // put-end-tick countdown (put units) it cannot be expressed and
1862            // TriCNES has no such adjustment — skip it (TriCNES-exact).
1863            let _ = implicit_stop_edge;
1864        } else if !enable_dmc {
1865            // W3-Stage-3 (`mc-r1-dmc-delayed-4015`): the disable-side floor
1866            // compensations below (the scheduled explicit abort, the
1867            // pending-reload keep-alive reshaping, the load-delay zeroing and
1868            // the suppress reset) are SUPERSEDED by the delayed-status
1869            // application — TriCNES's `$4015` disable write does nothing else
1870            // DMC-wise; the abort is EMERGENT from the applied status gating
1871            // the per-cycle DMA service (`_6502` line 4218).
1872        } else if enable_dmc {
1873            self.dmc_reload_suppress_outputs = 0;
1874        }
1875    }
1876
1877    /// W3-Stage-3 (`mc-r1-dmc-delayed-4015`): the TriCNES `$4015` write
1878    /// handler's DMC-status section (Emulator.cs:9504-9548). IMMEDIATE at the
1879    /// write: the enable-side `StartDMCSample` (`set_enabled(true)` restarts
1880    /// only when `bytes_remaining == 0` — exactly `StartDMCSample`, line
1881    /// 9517) and the DMC IRQ-flag clear (line 9529). DEFERRED: the status-bit
1882    /// application + the disable-side `bytes_remaining` zeroing, latched into
1883    /// `dmc_delayed_status` and applied `put ? 3 : 4` end-ticks later (line
1884    /// 9512; the write cycle's own end-tick counts — "really like 2 : 3"). A
1885    /// second `$4015` write during the pending window resets the countdown
1886    /// with the new target (last write wins, as TriCNES).
1887    fn latch_delayed_dmc_4015(&mut self, enable_dmc: bool) {
1888        if enable_dmc {
1889            self.dmc.set_enabled(true);
1890        } else {
1891            self.dmc.irq_flag = false;
1892        }
1893        self.dmc_delayed_status = enable_dmc;
1894        self.dmc_delayed_4015 = if self.put_cycle { 3 } else { 4 };
1895        // The explicit don't-abort edge (Emulator.cs:9533-9537): the disable
1896        // coincides with "the APU cycle that fires a DMC DMA". TriCNES
1897        // `(timer == 2 && get) || (timer == rate && put)` in CPU-rate units
1898        // maps to our APU-rate byte-timer as `(timer == 0 && get)` (this
1899        // cycle's end-tick wraps) or `(timer == timer_period && put)` (the
1900        // wrap happened on the previous get half). Extend the delay to
1901        // `put ? 5 : 6` so the just-armed reload DMA runs to completion
1902        // before the disable zeroes `bytes_remaining` (EXPLICIT sweep
1903        // idx[7] = 04).
1904        if !enable_dmc {
1905            let firing_apu_cycle = if self.put_cycle {
1906                self.dmc.timer == self.dmc.timer_period
1907            } else {
1908                self.dmc.timer == 0
1909            };
1910            if firing_apu_cycle {
1911                self.dmc_delayed_4015 = if self.put_cycle { 5 } else { 6 };
1912            }
1913        }
1914        // The implicit-abort edge (Emulator.cs:9540-9545): an ENABLE that
1915        // lands one byte-timer fire BEFORE the shifter-consume edge —
1916        // TriCNES `(timer == 10 && get) || (timer == 8 && put)` = our
1917        // APU-rate `(4, get)/(3, put)` (uniform `(t - 2) / 2` mapping).
1918        // "Regardless of the buffer being empty, there will be a 1-cycle
1919        // DMA that gets aborted" — latched here, consumed at the consume
1920        // edge in `dmc_tick_end`.
1921        if enable_dmc {
1922            let pre_fire_window = if self.put_cycle {
1923                self.dmc.timer == 3
1924            } else {
1925                self.dmc.timer == 4
1926            };
1927            if pre_fire_window {
1928                self.dmc_set_implicit_abort = true;
1929            }
1930        }
1931    }
1932
1933    /// CPU register read (only `$4015` is meaningful).  Reading clears the
1934    /// frame IRQ flag.
1935    pub fn read_status(&mut self) -> u8 {
1936        let mut v = 0u8;
1937        if self.pulse1.length.active() {
1938            v |= 0x01;
1939        }
1940        if self.pulse2.length.active() {
1941            v |= 0x02;
1942        }
1943        if self.triangle.length.active() {
1944            v |= 0x04;
1945        }
1946        if self.noise.length.active() {
1947            v |= 0x08;
1948        }
1949        // W3-Stage-3 (`mc-r1-dmc-delayed-4015`): TriCNES `Observe` `$4015`
1950        // (Emulator.cs:9127/9260 + the 9268 footnote) — bit 4 is
1951        // `bytes_remaining != 0 && APU_Status_DelayedDMC`: a read right after
1952        // a disable write must see bit 4 CLEAR even though `bytes_remaining`
1953        // is not zeroed until the delayed application ("LDA #0, STA $4015,
1954        // LDA $4015 ... needs to immediately have bit 4 cleared").
1955        if self.dmc.active() && self.dmc_delayed_status {
1956            v |= 0x10;
1957        }
1958        if self.frame_counter.irq_flag {
1959            v |= 0x40;
1960        }
1961        if self.dmc.irq_flag {
1962            v |= 0x80;
1963        }
1964        // Reading clears frame IRQ flag (NOT DMC IRQ). The clear is
1965        // SCHEDULED for a future CPU cycle (1 cycle delta on a "get"
1966        // cycle, 2 cycles on a "put") and matured by a subsequent
1967        // observation -- the canonical Mesen2 `GetIrqFlag` lazy
1968        // algorithm (Session-25, 2026-05-23). The pre-Session-25
1969        // immediate-on-get / defer-by-one-tick-on-put scheme failed
1970        // `AccuracyCoin :: APU Tests :: Frame Counter IRQ` Test 7.
1971        // See `docs/audit/session-25-sprint2-iter3-frame-counter-irq-2026-05-23.md`.
1972        let _ = self
1973            .frame_counter
1974            .read_status(self.cpu_cycle, self.apu_phase);
1975        v
1976    }
1977
1978    /// Clear the frame IRQ flag immediately for DMA no-op reads of `$4015`.
1979    ///
1980    /// The normal CPU-visible `$4015` read path keeps the put-cycle deferred
1981    /// clear needed by frame-counter timing tests. DMC DMA no-op repeats use
1982    /// this after sampling the status value so the halted-read side effect is
1983    /// visible before the CPU resumes the original `$4015` read.
1984    ///
1985    /// Session-26 iter 5: also deassert the CPU IRQ line driver
1986    /// (`irq_line_active`) since the DMA no-op read mirrors a CPU
1987    /// `$4015` read on the silicon — the IRQ source is removed from
1988    /// the CPU's `_irqSource` list synchronously.
1989    pub fn clear_frame_irq_immediate_for_dma(&mut self) {
1990        self.frame_counter.irq_flag = false;
1991        self.frame_counter.irq_line_active = false;
1992    }
1993}
1994
1995#[cfg(test)]
1996mod tests {
1997
1998    /// v2.3.5 C1 — the default-configuration fast path must be byte-identical
1999    /// to the gated path it skips.
2000    ///
2001    /// The specialization is only sound because `gate` is the identity when its
2002    /// mask bit is set and `scale` is the identity at gain 1.0. If either ever
2003    /// stops being the identity at the default, this silently changes shipped
2004    /// audio -- so assert the equivalence directly over a 2,048-point sweep of
2005    /// the output range rather than trusting the reasoning.
2006    #[test]
2007    fn apu_default_mix_matches_the_gated_path() {
2008        let apu = Apu::new(Region::Ntsc, 48_000);
2009        assert_eq!(apu.channel_mask, CHANNEL_MASK_ALL, "premise: default mask");
2010        assert_eq!(
2011            apu.channel_gain, CHANNEL_GAIN_UNITY,
2012            "premise: default gain"
2013        );
2014
2015        let mask = CHANNEL_MASK_ALL;
2016        let gain = CHANNEL_GAIN_UNITY;
2017        let gate = |bit: u8, v: u8| if mask & (1 << bit) != 0 { v } else { 0 };
2018        let scale = |bit: usize, v: u8, max: u8| {
2019            let g = gain[bit];
2020            if g == 1.0 {
2021                v
2022            } else {
2023                #[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
2024                {
2025                    roundf(f32::from(v) * g).clamp(0.0, f32::from(max)) as u8
2026                }
2027            }
2028        };
2029
2030        // 2,048 SELECTED combinations, not the full cross-product. The DMC axis
2031        // is swept exhaustively (0..=127) against 16 rotating phases of the
2032        // other four channels, which walks the `tnd_table` index range end to
2033        // end and visits every raw level each channel can take. The exhaustive
2034        // product would be 16^4 * 128 = 8.4M; this is a sweep, and the wording
2035        // says "sweep" rather than claiming enumeration.
2036        for dmc in 0u8..=127 {
2037            for lvl in 0u8..=15 {
2038                let (p1, p2, tri, noise) = (lvl, 15 - lvl, (lvl + 7) % 16, (lvl + 3) % 16);
2039                let gated = apu.mixer.mix(
2040                    scale(0, gate(0, p1), 15),
2041                    scale(1, gate(1, p2), 15),
2042                    scale(2, gate(2, tri), 15),
2043                    scale(3, gate(3, noise), 15),
2044                    scale(4, gate(4, dmc), 127),
2045                ) + if mask & (1 << 5) != 0 { 0.25f32 } else { 0.0 };
2046                let fast = apu.mixer.mix(p1, p2, tri, noise, dmc) + 0.25f32;
2047                assert_eq!(
2048                    gated.to_bits(),
2049                    fast.to_bits(),
2050                    "p1={p1} p2={p2} tri={tri} noise={noise} dmc={dmc}: \
2051                     the fast path must be BIT-identical, not merely close"
2052                );
2053            }
2054        }
2055    }
2056
2057    /// The fast path must NOT be taken once the configuration stops being the
2058    /// default -- otherwise the mute/gain overlay would silently stop working.
2059    #[test]
2060    fn a_non_default_mask_or_gain_still_takes_the_gated_path() {
2061        let mut muted = Apu::new(Region::Ntsc, 48_000);
2062        muted.set_channel_mask(CHANNEL_MASK_ALL & !0x01); // mute pulse 1
2063        assert_ne!(muted.channel_mask, CHANNEL_MASK_ALL);
2064
2065        let mut quiet = Apu::new(Region::Ntsc, 48_000);
2066        quiet.set_channel_gain([0.5, 1.0, 1.0, 1.0, 1.0, 1.0]);
2067        assert_ne!(quiet.channel_gain, CHANNEL_GAIN_UNITY);
2068
2069        // Drive both far enough to produce output, and confirm a muted channel
2070        // actually changes the mix relative to the default.
2071        let mut plain = Apu::new(Region::Ntsc, 48_000);
2072        for a in [&mut plain, &mut muted, &mut quiet] {
2073            a.write_register(0x4015, 0x1F);
2074            a.write_register(0x4000, 0xBF);
2075            a.write_register(0x4002, 0xAA);
2076            a.write_register(0x4003, 0x08);
2077            for _ in 0..2_000 {
2078                a.tick();
2079            }
2080        }
2081        // The original assertion here was broken, and both review bots caught it:
2082        // it compared `plain.pulse1.output() == 0` against
2083        // `muted.channel_mask & 0x01 != 0`, which is `false` for a muted mask --
2084        // so it only passed when pulse 1 happened to be at output 0 on the
2085        // sampled tick. Phase-dependent, disconnected from the overlay it claimed
2086        // to test, and it never touched `quiet` at all. It could not fail on the
2087        // bug it existed to catch.
2088        //
2089        // Compare the EMITTED AUDIO instead, which is what the overlay is
2090        // supposed to change. Deliberately not `pulse1.output() != 0` even as a
2091        // premise check: that samples one instant, and a square wave spends half
2092        // its period at zero, so it is phase-dependent -- exactly the flaw that
2093        // made the original assertion vacuous. Accumulated samples have no such
2094        // dependence: if anything was audible, some sample is non-zero.
2095        assert_eq!(muted.channel_mask() & 0x01, 0, "premise: pulse 1 is muted");
2096
2097        let plain_audio = plain.drain_audio();
2098        let muted_audio = muted.drain_audio();
2099        let quiet_audio = quiet.drain_audio();
2100        assert!(!plain_audio.is_empty(), "premise: samples were emitted");
2101        assert!(
2102            plain_audio.iter().any(|s| *s != 0.0),
2103            "premise: the default configuration produced audible output"
2104        );
2105        assert_ne!(
2106            plain_audio, muted_audio,
2107            "a cleared mask bit must change the emitted audio"
2108        );
2109        assert_ne!(
2110            plain_audio, quiet_audio,
2111            "a non-unity gain must change the emitted audio"
2112        );
2113    }
2114    use super::*;
2115
2116    #[test]
2117    fn write_4015_enables_channels() {
2118        let mut a = Apu::new(Region::Ntsc, 44_100);
2119        a.write_register(0x4015, 0x0F);
2120        assert!(a.pulse1.length.enabled);
2121        assert!(a.pulse2.length.enabled);
2122        assert!(a.triangle.length.enabled);
2123        assert!(a.noise.length.enabled);
2124        assert!(!a.dmc.active());
2125    }
2126
2127    #[test]
2128    #[ignore = "permanent-by-design: pins the SUPERSEDED pre-master-clock $4015-enable load-delay placement. The default master-clock core (the only scheduler) moves the load arm to the put-end countdown, so this unit assertion is kept as a historical pin and cannot be un-ignored. Battery coverage: AccuracyCoin Delta-Mod/Implicit (100% on the default build)."]
2129    fn dmc_enable_schedules_load_dma_after_apu_aligned_delay() {
2130        let mut a = Apu::new(Region::Ntsc, 44_100);
2131        a.write_register(0x4012, 0x00);
2132        a.write_register(0x4013, 0x00);
2133        a.apu_phase = false; // put half: load halt attempt after 3 cycles.
2134
2135        a.write_register(0x4015, 0x10);
2136
2137        assert!(!a.pending_dmc_dma);
2138        assert_eq!(a.dmc_dma_addr, 0xC000);
2139        assert_eq!(a.dmc_dma_delay, 3);
2140        a.tick();
2141        assert!(!a.pending_dmc_dma);
2142        a.tick();
2143        assert!(!a.pending_dmc_dma);
2144        a.tick();
2145        assert!(a.pending_dmc_dma);
2146        assert_eq!(a.dmc_dma_delay, 0);
2147    }
2148
2149    #[test]
2150    #[ignore = "permanent-by-design: pins the SUPERSEDED pre-master-clock cycle-start reload-arm position. The default master-clock core moves the byte-timer/reload-arm to dmc_tick_end, so this unit assertion is kept as a historical pin and cannot be un-ignored. Battery coverage: AccuracyCoin DMC+OAM/Implicit (100% on the default build)."]
2151    fn dmc_reload_dma_arms_when_sample_buffer_becomes_empty() {
2152        let mut a = Apu::new(Region::Ntsc, 44_100);
2153        a.dmc.bytes_remaining = 1;
2154        a.dmc.sample_buffer = Some(0xAA);
2155        a.dmc.bits_remaining = 1;
2156        a.dmc.timer = 0;
2157        a.apu_phase = false;
2158
2159        a.tick();
2160
2161        assert!(a.pending_dmc_dma);
2162        assert_eq!(a.dmc_dma_delay, 0);
2163        assert_eq!(a.dmc_dma_addr, 0xC000);
2164    }
2165
2166    #[test]
2167    fn write_4015_clears_lengths_when_disabled() {
2168        let mut a = Apu::new(Region::Ntsc, 44_100);
2169        a.pulse1.length.enabled = true;
2170        a.pulse1.length.count = 10;
2171        a.write_register(0x4015, 0x00);
2172        assert_eq!(a.pulse1.length.count, 0);
2173    }
2174
2175    #[test]
2176    fn read_4015_clears_frame_irq_not_dmc_irq() {
2177        // Session-25 (2026-05-23): the canonical Mesen2 lazy-clear
2178        // algorithm SCHEDULES the frame-IRQ flag clear instead of
2179        // performing it immediately. A GET-cycle (`apu_phase=true`)
2180        // read schedules a clear at `cpu_cycle + 1`; a tick then
2181        // matures the schedule and the flag observable on the next
2182        // CPU cycle is `false`. DMC IRQ is untouched.
2183        let mut a = Apu::new(Region::Ntsc, 44_100);
2184        a.frame_counter.irq_flag = true;
2185        a.dmc.irq_flag = true;
2186        a.apu_phase = true; // GET cycle (1-cycle delta)
2187        let v = a.read_status();
2188        assert_eq!(v & 0xC0, 0xC0, "read returns the OLD flag (still set)");
2189        // The flag is STILL set right after the read; the clear is
2190        // scheduled for `cpu_cycle + 1`.
2191        assert!(a.frame_counter.irq_flag);
2192        assert_ne!(a.frame_counter.irq_flag_clear_cycle, 0);
2193        assert!(a.dmc.irq_flag);
2194        // Tick once -- the scheduled clear matures inside the tick.
2195        a.tick();
2196        assert!(!a.frame_counter.irq_flag, "flag matures inside tick");
2197        assert_eq!(a.frame_counter.irq_flag_clear_cycle, 0);
2198        // DMC IRQ is independently retained.
2199        assert!(a.dmc.irq_flag);
2200    }
2201
2202    #[test]
2203    fn read_4015_on_put_cycle_defers_irq_clear_by_two_cycles() {
2204        // Session-25 (2026-05-23): a PUT-cycle (`apu_phase=false`)
2205        // read schedules the clear at `cpu_cycle + 2` instead of
2206        // `cpu_cycle + 1`. This is the AccuracyCoin `APU Frame
2207        // Counter IRQ` Test 7 axis: the SLO ABS,X double-read of
2208        // `$4015` on a PUT-cycle first read sees the flag STILL SET
2209        // on the second read 1 CPU cycle later (the schedule has not
2210        // yet matured).
2211        let mut a = Apu::new(Region::Ntsc, 44_100);
2212        a.frame_counter.irq_flag = true;
2213        a.apu_phase = false; // PUT cycle (2-cycle delta)
2214        let v = a.read_status();
2215        assert_eq!(v & 0x40, 0x40, "first read returns the OLD flag");
2216        assert!(a.frame_counter.irq_flag, "flag stays set on put-cycle read");
2217        let scheduled = a.frame_counter.irq_flag_clear_cycle;
2218        assert_eq!(scheduled, a.cpu_cycle.wrapping_add(2));
2219        // A second read on the SAME put cycle still sees the set
2220        // flag (the schedule hasn't matured: cpu_cycle == cpu_cycle).
2221        let v2 = a.read_status();
2222        assert_eq!(
2223            v2 & 0x40,
2224            0x40,
2225            "second read on same cycle still sees flag set"
2226        );
2227        assert!(a.frame_counter.irq_flag);
2228        // Now advance ONE CPU cycle via a tick. cpu_cycle becomes
2229        // scheduled - 1. The schedule has NOT yet matured.
2230        a.tick();
2231        assert!(a.frame_counter.irq_flag, "flag still set after 1 tick");
2232        // Advance the SECOND CPU cycle. cpu_cycle now equals
2233        // scheduled. The tick matures the clear.
2234        a.tick();
2235        assert!(!a.frame_counter.irq_flag, "flag matures after 2 ticks");
2236        assert_eq!(a.frame_counter.irq_flag_clear_cycle, 0);
2237    }
2238
2239    #[test]
2240    fn tick_advances_cycle_counter() {
2241        let mut a = Apu::new(Region::Ntsc, 44_100);
2242        for _ in 0..100 {
2243            a.tick();
2244        }
2245        assert_eq!(a.cpu_cycle, 100);
2246    }
2247
2248    #[test]
2249    fn channel_mask_defaults_to_all_on() {
2250        let a = Apu::new(Region::Ntsc, 44_100);
2251        assert_eq!(a.channel_mask(), CHANNEL_MASK_ALL);
2252    }
2253
2254    #[test]
2255    fn channel_mask_set_clamps_to_known_bits() {
2256        let mut a = Apu::new(Region::Ntsc, 44_100);
2257        // Upper bits beyond the 6 defined channels are masked off.
2258        a.set_channel_mask(0xFF);
2259        assert_eq!(a.channel_mask(), CHANNEL_MASK_ALL);
2260        a.set_channel_mask(0x00);
2261        assert_eq!(a.channel_mask(), 0x00);
2262        a.set_channel_mask(0b0010_1010);
2263        assert_eq!(a.channel_mask(), 0b0010_1010);
2264    }
2265
2266    #[test]
2267    fn default_mask_mix_is_byte_identical_to_unmasked() {
2268        // The determinism contract: with the default all-on mask, the gating in
2269        // `tick_with_external` must reproduce the raw mixer output exactly.
2270        let m = Mixer::new();
2271        let mask = CHANNEL_MASK_ALL;
2272        let gate = |bit: u8, v: u8| if mask & (1 << bit) != 0 { v } else { 0 };
2273        for &(p1, p2, tri, n, dmc) in &[
2274            (0u8, 0u8, 0u8, 0u8, 0u8),
2275            (15, 15, 15, 15, 127),
2276            (7, 3, 11, 4, 60),
2277            (1, 14, 8, 15, 1),
2278        ] {
2279            let raw = m.mix(p1, p2, tri, n, dmc);
2280            let gated = m.mix(
2281                gate(0, p1),
2282                gate(1, p2),
2283                gate(2, tri),
2284                gate(3, n),
2285                gate(4, dmc),
2286            );
2287            assert_eq!(raw, gated, "default mask must be byte-identical");
2288        }
2289    }
2290
2291    #[test]
2292    fn cleared_channel_bit_zeroes_its_contribution() {
2293        let m = Mixer::new();
2294        // Mute pulse 1 only (bit 0 cleared).
2295        let mask = CHANNEL_MASK_ALL & !0x01;
2296        let gate = |bit: u8, v: u8| if mask & (1 << bit) != 0 { v } else { 0 };
2297        let muted = m.mix(gate(0, 15), gate(1, 0), gate(2, 0), gate(3, 0), gate(4, 0));
2298        // Pulse 1 = 15 muted to 0 => identical to an all-silent mix.
2299        assert_eq!(muted, m.mix(0, 0, 0, 0, 0));
2300        // Pulse 2 (bit 1 still set) still contributes.
2301        let p2_on = m.mix(gate(0, 15), gate(1, 15), gate(2, 0), gate(3, 0), gate(4, 0));
2302        assert!(p2_on > 0.0);
2303    }
2304
2305    #[test]
2306    fn channel_gain_defaults_to_unity() {
2307        let a = Apu::new(Region::Ntsc, 44_100);
2308        assert_eq!(a.channel_gain(), CHANNEL_GAIN_UNITY);
2309    }
2310
2311    #[test]
2312    fn external_out_tracks_last_external_sample() {
2313        // v2.1.6 — the read-only expansion-audio display tap reflects the most
2314        // recent RAW value fed to `tick_with_external` and defaults to 0.0.
2315        let mut a = Apu::new(Region::Ntsc, 44_100);
2316        assert_eq!(a.external_out(), 0.0);
2317        a.tick_with_external(0.25);
2318        assert!((a.external_out() - 0.25).abs() < f32::EPSILON);
2319        a.tick_with_external(-0.1);
2320        assert!((a.external_out() - (-0.1)).abs() < f32::EPSILON);
2321        // The tap is a copy: a non-unity external gain does NOT change what the
2322        // scope observes (it always sees the raw chip contribution).
2323        a.set_channel_gain([1.0, 1.0, 1.0, 1.0, 1.0, 0.5]);
2324        a.tick_with_external(0.4);
2325        assert!((a.external_out() - 0.4).abs() < f32::EPSILON);
2326    }
2327
2328    #[test]
2329    fn channel_gain_set_clamps_to_range() {
2330        let mut a = Apu::new(Region::Ntsc, 44_100);
2331        a.set_channel_gain([3.0, -1.0, 0.5, 1.0, 2.0, 0.0]);
2332        // 3.0 -> 2.0 (ceiling), -1.0 -> 0.0 (floor), the rest unchanged.
2333        assert_eq!(a.channel_gain(), [2.0, 0.0, 0.5, 1.0, 2.0, 0.0]);
2334    }
2335
2336    #[test]
2337    fn unity_gain_produces_byte_identical_samples() {
2338        // The hard determinism requirement: a full run with the default unity
2339        // gains must produce a bit-identical band-limited output to a fresh APU.
2340        const EXT: [f32; 7] = [0.0, 0.01, 0.02, 0.03, 0.04, 0.05, 0.06];
2341        let mut a = Apu::new(Region::Ntsc, 44_100);
2342        let mut b = Apu::new(Region::Ntsc, 44_100);
2343        b.set_channel_gain(CHANNEL_GAIN_UNITY); // explicit unity == default
2344        // Drive both with an identical register + tick sequence.
2345        for step in 0..4_000u32 {
2346            let v = (step & 0xFF) as u8;
2347            a.write_register(0x4000 + (step % 0x14) as u16, v);
2348            b.write_register(0x4000 + (step % 0x14) as u16, v);
2349            let ext = EXT[(step % 7) as usize];
2350            a.tick_with_external(ext);
2351            b.tick_with_external(ext);
2352        }
2353        let mut out_a = [0.0f32; 4096];
2354        let mut out_b = [0.0f32; 4096];
2355        let na = a.drain_audio_into(&mut out_a);
2356        let nb = b.drain_audio_into(&mut out_b);
2357        assert_eq!(na, nb);
2358        assert_eq!(
2359            out_a[..na],
2360            out_b[..nb],
2361            "unity gain must be bit-identical to the default mix"
2362        );
2363    }
2364
2365    #[test]
2366    fn zero_gain_matches_a_cleared_mask_bit() {
2367        // Gain 0.0 on a channel is equivalent to clearing that channel's mask
2368        // bit (both force the raw output to 0 before the non-linear mixer).
2369        let m = Mixer::new();
2370        // Pulse 1 raw 15, everything else silent; gain 0 on pulse 1.
2371        // round(15 * 0.0) == 0, so the mixer sees pulse1 = 0.
2372        let scaled = m.mix(0, 0, 0, 0, 0);
2373        assert_eq!(scaled, m.mix(0, 0, 0, 0, 0));
2374        // Sanity: a real attenuation (0.5) lands strictly between full and muted.
2375        #[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
2376        let half = (15.0f32 * 0.5).round() as u8; // 8
2377        let full = m.mix(15, 0, 0, 0, 0);
2378        let attenuated = m.mix(half, 0, 0, 0, 0);
2379        assert!(attenuated > 0.0 && attenuated < full);
2380    }
2381
2382    #[test]
2383    #[ignore = "permanent-by-design: pins the SUPERSEDED legacy dual-flip-flop put_cycle toggle. In the default master-clock core, put_cycle is derived from the unified counter and flipped at end-of-cycle, so this unit assertion is kept as a historical pin and cannot be un-ignored."]
2384    fn put_cycle_toggles_per_cycle_only_when_driven_externally() {
2385        // Interleaved-DMA Phase A: under external DMC driving the global get/put
2386        // flip-flop toggles exactly once per CPU cycle (TriCNES `APU_PutCycle`).
2387        let mut a = Apu::new(Region::Ntsc, 44_100);
2388        a.set_dmc_driven_externally(true);
2389        a.seed_apu_alignment(0); // case 0 => put_cycle = true
2390        assert!(a.put_cycle());
2391        a.tick();
2392        assert!(!a.put_cycle(), "toggles after one cycle");
2393        a.tick();
2394        assert!(a.put_cycle(), "toggles back after two cycles");
2395
2396        // Default build (not driven externally): the flip-flop is frozen, so the
2397        // default path is byte-identical (nothing toggles or reads it).
2398        //
2399        // RW-1 (`mc-r1-one-clock`): `put_cycle` is DERIVED from the one counter
2400        // (`put_cycle = !apu_phase`) and is no longer gated on
2401        // `dmc_driven_externally` — that gating WAS the second independent
2402        // flip-flop this phase removes. So under the flag `put_cycle` tracks the
2403        // counter unconditionally and stays the exact complement of `apu_phase`
2404        // (the coherence guarantee). The default (non-R1) path never consumes
2405        // `put_cycle`, so this is still byte-identical there.
2406        {
2407            let mut b = Apu::new(Region::Ntsc, 44_100);
2408            for _ in 0..10 {
2409                b.tick();
2410                assert_eq!(
2411                    b.put_cycle(),
2412                    !b.apu_phase(),
2413                    "one-clock: put_cycle is the derived complement of apu_phase"
2414                );
2415            }
2416        }
2417    }
2418
2419    #[test]
2420    fn frame_irq_after_29828_cycles() {
2421        let mut a = Apu::new(Region::Ntsc, 44_100);
2422        // Default: 4-step mode, IRQ enabled.
2423        for _ in 0..29828 {
2424            a.tick();
2425        }
2426        assert!(a.frame_irq_pending());
2427    }
2428
2429    #[test]
2430    fn mode1_inhibits_irq() {
2431        let mut a = Apu::new(Region::Ntsc, 44_100);
2432        // Write mode=1 + inhibit.  After ~3 cycles delay, fire qf+hf.
2433        a.write_register(0x4017, 0xC0);
2434        for _ in 0..40_000 {
2435            a.tick();
2436        }
2437        // No IRQ ever raised in mode 1.
2438        assert!(!a.frame_irq_pending());
2439    }
2440
2441    #[test]
2442    fn dmc_writes_dac_directly() {
2443        let mut a = Apu::new(Region::Ntsc, 44_100);
2444        a.write_register(0x4011, 0x40);
2445        assert_eq!(a.dmc.dac, 0x40);
2446    }
2447
2448    #[test]
2449    fn enabling_dmc_starts_sample() {
2450        let mut a = Apu::new(Region::Ntsc, 44_100);
2451        a.write_register(0x4012, 0x00);
2452        a.write_register(0x4013, 0x10); // 0x101 bytes
2453        a.write_register(0x4015, 0x10);
2454        assert!(a.dmc.active());
2455    }
2456}