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}