pub struct Apu {
pub region: Region,
pub pulse1: Pulse,
pub pulse2: Pulse,
pub triangle: Triangle,
pub noise: Noise,
pub dmc: Dmc,
pub frame_counter: FrameCounter,
pub sample_rate: u32,
/* private fields */
}Expand description
Top-level APU.
Fields§
§region: RegionRegion (NTSC / PAL / Dendy).
pulse1: PulsePulse 1.
pulse2: PulsePulse 2.
triangle: TriangleTriangle.
noise: NoiseNoise.
dmc: DmcDMC.
frame_counter: FrameCounterFrame counter.
sample_rate: u32Sample rate (Hz) for diagnostics.
Implementations§
Source§impl Apu
impl Apu
Sourcepub fn set_audio_provenance(&mut self, enabled: bool)
pub fn set_audio_provenance(&mut self, enabled: bool)
Arm or disarm audio provenance.
Arming allocates both stores; disarming frees them. Mirrors
Ppu::set_pixel_provenance, including that re-arming an already-armed
APU is a no-op rather than a silent wipe — the frontend re-asserts the
arm every frame (a lesson from the pixel panel, whose edge-triggered
mirror desynced permanently the moment a ROM load installed a fresh
core).
Sourcepub const fn audio_provenance_armed(&self) -> bool
pub const fn audio_provenance_armed(&self) -> bool
Whether audio provenance is armed.
Sourcepub fn register_attribution(&self) -> Option<&RegisterAttribution>
pub fn register_attribution(&self) -> Option<&RegisterAttribution>
The per-register write attribution, or None when disarmed.
Sourcepub fn mix_trace(&self) -> Option<&MixTrace>
pub fn mix_trace(&self) -> Option<&MixTrace>
The per-CPU-cycle mix trace, or None when disarmed.
Sourcepub fn begin_audio_provenance_frame(&mut self, first_cycle: u64)
pub fn begin_audio_provenance_frame(&mut self, first_cycle: u64)
Begin a new frame’s mix trace, anchored at first_cycle.
The register attribution is deliberately NOT cleared here: “which
instruction last wrote $4003” is a question whose answer legitimately
predates the current frame, and clearing it every frame would report a
register nobody has touched this frame as never written.
Sourcepub fn clear_audio_provenance_history(&mut self)
pub fn clear_audio_provenance_history(&mut self)
Forget the register attribution history. Called on a cold boot, where the history it describes genuinely ended.
Sourcepub const fn record_bus_handled_register_write(&mut self, addr: u16, value: u8)
pub const fn record_bus_handled_register_write(&mut self, addr: u16, value: u8)
Attribute a write in $4000-$4017 that the bus does NOT route through
Self::write_register.
Two addresses in the range are not APU registers and are handled
entirely on the bus: $4014 (OAM DMA, which arms a burst) and $4016
(controller strobe, which is buffered to the next M2-low boundary).
Bus::write dispatches only $4000-$4013 | $4015 | $4017 to
write_register, so the attribution recorded there can never see those
two — yet the table reserves slots for them, because the range is what
the bus already classifies as an APU write and punching a hole in it
would invite off-by-one arithmetic at every call site.
Without this entry point those two slots would stay permanently empty
while the docs claimed they were tracked. This records the cause exactly
as write_register would, and dispatches nothing — the emulation of both
addresses stays wherever the bus already implements it.
Sourcepub const fn set_attrib_context(&mut self, pc: u16, cycle: u64)
pub const fn set_attrib_context(&mut self, pc: u16, cycle: u64)
Push the writing instruction’s PC + cycle down, mirroring the PPU’s write-attribution context. Called once per instruction by the core.
Sourcepub fn take_audio_provenance(&mut self) -> AudioProvenanceStash
pub fn take_audio_provenance(&mut self) -> AudioProvenanceStash
Lift both stores out for a same-timeline restore (run-ahead), leaving the
APU disarmed. See crate::provenance::AudioProvenanceStash for why
this exists at all.
Sourcepub fn put_audio_provenance(&mut self, stash: AudioProvenanceStash)
pub fn put_audio_provenance(&mut self, stash: AudioProvenanceStash)
Put back stores taken by Self::take_audio_provenance.
Sourcepub fn reset(&mut self)
pub fn reset(&mut self)
Reset (warm). Per nesdev: most APU state is preserved across reset
except $4015 is cleared (channels disabled, DMC silenced).
v2.0.0 beta.3 (A4 cycle-accurate reset, promoted to the only path in
beta.4): the 2A03
reset sequence behaves as if the LAST value written to $4017 were
written again (blargg apu_reset spec) — the retained value is
re-issued through the normal $4017 write path (pending mode + the
3/4-cycle aligned delay + the mode-1 immediate quarter/half clock),
and the CPU’s 8 clocked reset cycles then age the re-armed counter
so execution resumes ~9-12 cycles after the effective write (the
4017_timing window).
Sourcepub const fn set_channel_mask(&mut self, mask: u8)
pub const fn set_channel_mask(&mut self, mask: u8)
Set the per-channel enable mask (a UI playback overlay; see
Apu::channel_mask). Bit 0 = pulse 1, 1 = pulse 2, 2 = triangle,
3 = noise, 4 = DMC, 5 = external/mapper audio. CHANNEL_MASK_ALL is
the determinism-safe default (byte-identical mixer output).
Sourcepub const fn channel_mask(&self) -> u8
pub const fn channel_mask(&self) -> u8
Current per-channel enable mask.
Sourcepub fn set_channel_gain(&mut self, gain: [f32; 6])
pub fn set_channel_gain(&mut self, gain: [f32; 6])
v1.4.0 Workstream C — set the per-channel output gain (a UI mixing
overlay; see Apu::channel_gain). Index 0 = pulse 1, 1 = pulse 2,
2 = triangle, 3 = noise, 4 = DMC, 5 = external/mapper audio. Each gain is
clamped to 0.0..=2.0. CHANNEL_GAIN_UNITY (all 1.0) is the
determinism-safe default (byte-identical mixer output).
Sourcepub fn set_filter_model(&mut self, model: FilterModel)
pub fn set_filter_model(&mut self, model: FilterModel)
v2.1.3 — select the analog output-filter model (see
crate::mixer::FilterModel). Default crate::mixer::FilterModel::NesRf
is byte-identical to the pre-v2.1.3 output; the softer models drop the
aggressive 440 Hz high-pass for a fuller low end. Display/tonal only —
channel content is unchanged.
Sourcepub const fn channel_gain(&self) -> [f32; 6]
pub const fn channel_gain(&self) -> [f32; 6]
Current per-channel output gain. See Apu::set_channel_gain.
Sourcepub fn pulse1_out(&self) -> u8
pub fn pulse1_out(&self) -> u8
Pulse 1 raw output volume (0..=15) — for tests.
Sourcepub fn pulse2_out(&self) -> u8
pub fn pulse2_out(&self) -> u8
Pulse 2 raw output volume.
Sourcepub fn triangle_out(&self) -> u8
pub fn triangle_out(&self) -> u8
Triangle raw output (0..=15).
Sourcepub const fn external_out(&self) -> f32
pub const fn external_out(&self) -> f32
v2.1.6 “Expansion Audio” — the most recent RAW on-cart expansion-audio
sample (pre-Self::channel_gain, the last_external field). 0.0
when the loaded board has no expansion audio. Read-only display tap for
the frontend Audio Mixer expansion-channel scope / VU meter — it reads a
copy and never feeds back into synthesis, so it is determinism-neutral.
Sourcepub const fn frame_irq_pending(&self) -> bool
pub const fn frame_irq_pending(&self) -> bool
Frame IRQ pending?
Sourcepub const fn dmc_irq_pending(&self) -> bool
pub const fn dmc_irq_pending(&self) -> bool
DMC IRQ pending?
Sourcepub const fn irq_line(&self) -> bool
pub const fn irq_line(&self) -> bool
Combined IRQ line — true if either source is asserting.
Session-26 iter 5 (2026-05-23): the frame-counter contribution
is irq_line_active (the CPU’s IRQSource::FrameCounter
registration), NOT irq_flag (the $4015 bit 6 visibility).
The two are SEPARATE fields since iter 5 — see
FrameCounter::irq_flag
and FrameCounter::irq_line_active.
AccuracyCoin Tests I/J/K specifically test that $4015 bit 6
is visible during inhibit (transient 2-cycle window at FC steps
29828-29829) while NO CPU IRQ fires (Test M).
Sourcepub const fn last_frame_events(&self) -> FrameEvents
pub const fn last_frame_events(&self) -> FrameEvents
Returns the frame-counter events fired by the most recent tick call.
The bus reads this immediately after Self::tick_with_external to
fan-out the events to on-cart audio extensions (MMC5) whose envelope
and length-counter sub-units share the 2A03 frame-counter cadence.
The value is overwritten at the start of every tick, so observers
must consume it before the next tick.
Sourcepub fn drain_audio(&mut self) -> Vec<f32>
pub fn drain_audio(&mut self) -> Vec<f32>
Drain all finalized audio samples (host sample rate, normalized to
approximately [-0.5, 0.5]).
Sourcepub fn drain_audio_into(&mut self, out: &mut [f32]) -> usize
pub fn drain_audio_into(&mut self, out: &mut [f32]) -> usize
Drain into a slice; returns count copied.
Sourcepub const fn dmc_dma_pending(&self) -> bool
pub const fn dmc_dma_pending(&self) -> bool
Has a DMC DMA request been raised? The bus polls this each CPU cycle (BEFORE issuing reads) so it can halt the CPU on the next read cycle.
Sourcepub const fn dmc_dma_is_load(&self) -> bool
pub const fn dmc_dma_is_load(&self) -> bool
Whether the pending DMC DMA is a load DMA.
Sourcepub const fn dmc_dma_serviceable(&self) -> bool
pub const fn dmc_dma_serviceable(&self) -> bool
W3-Stage-3 (mc-r1-dmc-delayed-4015): the bus-side per-cycle DMC DMA
service-gate term — TriCNES _6502 line 4218:
DoDMCDMA && (APU_Status_DMC || APU_ImplicitAbortDMC4015). While
false, a pending (or halted in-flight) DMC DMA is NOT serviced and the
CPU resumes — the emergent explicit abort. pending_dmc_abort is the
implicit-abort override (the 1-cycle abort DMA runs regardless).
Sourcepub const fn dmc_dma_short(&self) -> bool
pub const fn dmc_dma_short(&self) -> bool
Whether the pending DMC DMA should use the short 3-cycle service path.
Sourcepub const fn dmc_dma_deliver_before_tick(&self) -> bool
pub const fn dmc_dma_deliver_before_tick(&self) -> bool
Whether the current DMC DMA get should make the fetched byte visible before the get-cycle APU tick.
Sourcepub const fn defer_next_dmc_reload_once(&mut self)
pub const fn defer_next_dmc_reload_once(&mut self)
Defer the next immediate DMC reload request by one CPU tick.
Sourcepub const fn dmc_abort_pending(&self) -> bool
pub const fn dmc_abort_pending(&self) -> bool
Has a one-cycle DMC abort halt been raised?
Sourcepub const fn dmc_abort_delay(&self) -> u8
pub const fn dmc_abort_delay(&self) -> u8
Read-only accessor for the DMC abort-delay countdown (CPU cycles
until pending_dmc_abort flips to true). Exposed for the
Session-21 per-cycle DMC trace tooling (crates/rustynes-core/src/ irq_trace.rs) which records the scheduler’s calibration state
for cross-diffing against Mesen2’s NesDmc.cpp.
Sourcepub const fn dmc_dma_cooldown(&self) -> u8
pub const fn dmc_dma_cooldown(&self) -> u8
Read-only accessor for the DMC DMA cooldown countdown (CPU cycles
during which a newly-empty sample buffer must NOT raise a new
DMA request). See Self::dmc_abort_delay.
Sourcepub const fn dmc_dma_delay(&self) -> u8
pub const fn dmc_dma_delay(&self) -> u8
Read-only accessor for the DMC DMA delay countdown (CPU cycles
until an initial-load DMA after $4015 enable transitions from
“armed” to pending_dmc_dma = true). See Self::dmc_abort_delay.
Sourcepub const fn dmc_timer(&self) -> u16
pub const fn dmc_timer(&self) -> u16
Diagnostic: the DMC channel’s internal byte-timer countdown. Exposed
for the per-cycle DMC-DMA cross-diff tracing that pins the abort-context
reload-arm phase (the +4-cycle A->B interval divergence).
Sourcepub const fn dmc_bits_remaining(&self) -> u8
pub const fn dmc_bits_remaining(&self) -> u8
Diagnostic: bits remaining in the DMC output shift register.
Sourcepub const fn dmc_silence(&self) -> bool
pub const fn dmc_silence(&self) -> bool
Diagnostic: DMC output-unit silence flag.
Sourcepub const fn dmc_buffer_full(&self) -> bool
pub const fn dmc_buffer_full(&self) -> bool
Diagnostic: DMC sample buffer occupied.
Sourcepub const fn apu_phase(&self) -> bool
pub const fn apu_phase(&self) -> bool
Read-only accessor for the APU’s two-cycle phase counter
(false = put, true = get; toggled every CPU tick by
tick_with_external). See Self::dmc_abort_delay.
Sourcepub const fn set_canonical_cycle(&mut self, cycle: u64)
pub const fn set_canonical_cycle(&mut self, cycle: u64)
v2.0.0 beta.1 (A1 one-clock collapse): assign the APU’s cycle counter
from the CANONICAL bus cycle counter. Called by the bus’s per-cycle
hook (cpu_clock → apu_advance_one) immediately before
Self::tick_with_external, replacing the legacy independent
cpu_cycle += 1 mirror. The bus increments its canonical counter
earlier in the same per-cycle hook, so the value assigned here equals
the post-increment value the legacy mirror produced — the
one_clock_invariants harness test pins the residue. Promoted to the
only path in v2.0.0 beta.4.
Sourcepub const fn cpu_cycle(&self) -> u64
pub const fn cpu_cycle(&self) -> u64
Read-only accessor for the APU-side cumulative CPU-cycle counter (v2.0.0-beta.1 one-clock instrumentation).
This is one of the five counters of the timebase substrate the
v2.0.0 “Timebase” rewrite collapses (ADR 0002 + the v2.0.0
master-clock plan): Cpu::master_clock, Cpu::cycles,
LockstepBus::cycle, LockstepBus::ppu_clock, and this field are
each advanced exactly once (or by one region divider) per CPU cycle
at different points within the cycle, and must never drift. The
RW-1 parity collapse already derives apu_phase / put_cycle from
(cpu_cycle + parity_seed) & 1; exposing the raw counter lets the
test harness assert the cross-chip affine invariants
(one_clock_invariants.rs) that gate the beta.1 counter collapse.
Sourcepub const fn seed_apu_phase(&mut self, phase: bool)
pub const fn seed_apu_phase(&mut self, phase: bool)
CM-1: seed the absolute apu_phase alignment (the parity of the CPU
cycles on which the APU — incl. the DMC byte-timer — clocks). RustyNES
starts apu_phase = false, so the DMC always arms on one CPU-cycle
parity; Mesen’s DMC arms on its _currentCycle alignment (one cycle off,
giving span 3 vs RustyNES’s 4). Seeding true flips the whole APU phase
by one CPU cycle to test the Mesen-matching arm parity. Broad impact:
also shifts pulse/noise/frame-counter. Default-off.
Sourcepub const fn dmc_dma_addr(&self) -> u16
pub const fn dmc_dma_addr(&self) -> u16
Address the DMC wants to read. Valid only when dmc_dma_pending()
returns true.
Sourcepub const fn dmc_need_halt(&self) -> bool
pub const fn dmc_need_halt(&self) -> bool
v1.2 Sprint 3 (get/put scheduler, ADR-0007).
Returns true while the DMC still needs an initial halt
cycle on the bus. Set by any code path that raises
pending_dmc_dma; the new bus::service_dmc_dma
implementation under the dmc-get-put-scheduler feature
flag clears it after processing the halt get-cycle.
Sourcepub const fn dmc_need_dummy_read(&self) -> bool
pub const fn dmc_need_dummy_read(&self) -> bool
v1.2 Sprint 3 (get/put scheduler, ADR-0007).
Returns true while the DMC still needs a dummy-read /
alignment cycle after the halt cycle. Cleared by the new
scheduler once the alignment cycle has been processed.
Sourcepub const fn clear_dmc_need_halt(&mut self)
pub const fn clear_dmc_need_halt(&mut self)
v1.2 Sprint 3 — bus clears this after consuming the halt
get-cycle (_needHalt = false in Mesen2’s NesCpu.cpp).
Sourcepub const fn clear_dmc_need_dummy_read(&mut self)
pub const fn clear_dmc_need_dummy_read(&mut self)
v1.2 Sprint 3 — bus clears this after consuming the
alignment cycle (_needDummyRead = false in Mesen2’s
NesCpu.cpp).
Sourcepub fn complete_dmc_dma(&mut self, byte: u8)
pub fn complete_dmc_dma(&mut self, byte: u8)
Bus calls this when it has executed a DMC DMA fetch (post-halt) and is delivering the sample byte.
Sourcepub fn complete_dmc_dma_before_get_tick(&mut self, byte: u8)
pub fn complete_dmc_dma_before_get_tick(&mut self, byte: u8)
Complete a DMC DMA get whose fetched byte is visible before the get-cycle APU tick.
Sourcepub const fn complete_dmc_abort(&mut self)
pub const fn complete_dmc_abort(&mut self)
Bus calls this after either consuming or suppressing a one-cycle DMC abort halt.
Sourcepub const fn cancel_dmc_dma(&mut self)
pub const fn cancel_dmc_dma(&mut self)
v1.2 Sprint 3 iter 3 (get/put scheduler, ADR 0007) — DMC DMA abort with cancel semantics.
Under the OLD scheduler, Self::complete_dmc_abort clears
only the abort flag; the DMC DMA still fires afterward (the
abort just inserts a 1-cycle halt). Under the get/put model
the abort CANCELS the DMA entirely — no byte fetch, all
flag state cleared — matching Mesen2’s
processCycle::if(_abortDmcDma) branch
(NesCpu.cpp:386-390):
if(_abortDmcDma) {
_dmcDmaRunning = false;
_abortDmcDma = false;
_needDummyRead = false;
_needHalt = false;
}This is the “Option C” semantic shift from the iter 3
research audit: abort cancels the fetch rather than letting
it complete after a wasted cycle. The new bus-side
service_dmc_dma (under dmc-get-put-scheduler feature)
calls this when it detects dmc_abort_pending mid-loop.
Sourcepub fn tick(&mut self)
pub fn tick(&mut self)
One CPU clock. Bus must NOT have halted the CPU for DMC DMA when
calling this (the bus is responsible for performing the DMA fetch
before resuming tick() calls).
Standalone/test convenience: production (LockstepBus) drives the
canonical cycle counter via Self::set_canonical_cycle before each
Self::tick_with_external (the v2.0.0 one-clock contract — the APU
never self-increments). This helper self-advances the counter so
standalone APU stepping (unit tests, the snapshot fixtures) keeps the
one-cycle-per-tick behavior.
Sourcepub fn tick_with_external(&mut self, external: f32)
pub fn tick_with_external(&mut self, external: f32)
Same as tick, but accepts an additional pre-mixed audio sample
from the cartridge (VRC6 / VRC7 / MMC5 / Sunsoft 5B / Namco 163 /
FDS). The external value is added to the APU’s own mix BEFORE the
band-limited buffer push.
The expected scale is ~ [-0.5, 0.5] (matching the APU mixer’s
own output range). The bus is responsible for converting whatever
the mapper returns (currently i16 from Mapper::mix_audio) into
that range.
Sourcepub fn promote_dmc_pending_next(&mut self)
pub fn promote_dmc_pending_next(&mut self)
Visibility-delay promotion (called at END of cycle, after the CPU’s bus
access): a reload latched this cycle becomes visible to the NEXT cycle’s
DMA servicing (first-service on the put cycle => span 4), matching
TriCNES _EmulateAPU-after-_6502 invisible-arm ordering.
Sourcepub fn dmc_tick_end(&mut self)
pub fn dmc_tick_end(&mut self)
v2.0 Program M (M-1 within-cycle order, mc-r1-dmc-bytetimer-end): clock
the DMC byte-timer + arm the reload at END of cycle (after the CPU’s bus
access), the mirror of the cycle-START block in tick_with_external that
dmc_clock_at_start now suppresses. Order matches tick_with_external:
byte-timer clock (on this cycle’s already-set apu_phase) -> reenable
consume-edge clear -> reload-arm -> cannot_run decrement. The bus calls
this from cpu_clock_apu_dmc (end-of-cycle), AFTER
promote_dmc_pending_next so a reload latched here is invisible to its
own cycle (promoted -> serviced the NEXT cycle = span-4, like the
references). The LOAD delay-arm is NOT here — it stays at cycle-start.
Sourcepub fn tick_dmc(&mut self)
pub fn tick_dmc(&mut self)
v2.0 F-2: advance ONLY the DMC byte-timer + DMA arm by one CPU cycle.
The R1 bus calls this at END of cycle (after the access) when
Self::set_dmc_driven_externally is set, so the DMC fire-phase matches
main’s tick_one_cpu_cycle (the cycle DMASync’s $4000 conflict
expects) while the rest of the APU — incl. the IRQ line — stays on the
cycle-start tick_with_external. Order mirrors tick_with_external:
delay-arm → APU-rate timer clock (via the dmc_ext_phase flip-flop) →
reload-arm.
Sourcepub fn tick_dmc_timer_only(&mut self)
pub fn tick_dmc_timer_only(&mut self)
v2.0 interleaved-DMA Phase B: advance ONLY the DMC byte-timer clock (no
delay/reload ARM), for a cycle of an interleaved DMC DMA span. The timer
advances (so the variable-3/4-span feeds back into the next fire-cycle —
divergence-A self-consistency) WITHOUT re-arming a new DMA mid-span (no
cascade). Toggles the same dmc_ext_phase flip-flop as Self::tick_dmc
so the every-other-cycle cadence stays consistent across normal + DMA
cycles. (In the burst model this re-wedged; in the per-cycle interleaved
model each DMA cycle is discrete and arm-gated, so it should hold.)
Sourcepub const fn set_dmc_driven_externally(&mut self, on: bool)
pub const fn set_dmc_driven_externally(&mut self, on: bool)
v2.0 F-2: route the DMC byte-timer + arm to Self::tick_dmc instead of
tick_with_external. Default false = byte-identical.
Sourcepub const fn put_cycle(&self) -> bool
pub const fn put_cycle(&self) -> bool
v2.0 interleaved-DMA Phase A: the global get/put flip-flop (TriCNES
APU_PutCycle). true = put cycle, false = get cycle.
Sourcepub const fn seed_apu_alignment(&mut self, alignment: u8)
pub const fn seed_apu_alignment(&mut self, alignment: u8)
v2.0 interleaved-DMA Phase A: seed the global get/put flip-flop from an
APUAlignment value (TriCNES Emulator.cs:685/776), the single seed the
interleaved DMA (Phase B) will share with the DMC fire-phase (divergence
A). The low bit selects the parity (TriCNES case 0/2 -> put, 1/3 -> get).
Phase A seeds ONLY put_cycle and deliberately leaves dmc_ext_phase
untouched, so the un-wedged feature-on behavior is preserved (the f2e
experiment proved flipping dmc_ext_phase alone regresses). The exact
put_cycle <-> dmc_ext_phase pairing is determined empirically in
Phase B, when the bus first consumes put_cycle for the get/put decision.
Sourcepub const fn snapshot_restored_parity(&self) -> bool
pub const fn snapshot_restored_parity(&self) -> bool
W3-Stage-4 (2026-06-10): whether the most recent Apu::restore
blob carried the Stage-4 parity/DMA-state tail. The bus consults this
after a snapshot restore: when true the exact put_cycle /
parity_seed phase came from the blob and must NOT be overwritten by
the boot Self::seed_apu_alignment call (pre-Stage-4 blobs lack the
tail, so the bus falls back to the boot seed exactly as before).
Sourcepub fn write_register(&mut self, addr: u16, value: u8)
pub fn write_register(&mut self, addr: u16, value: u8)
CPU register write ($4000-$4017 excluding $4014).
Sourcepub fn read_status(&mut self) -> u8
pub fn read_status(&mut self) -> u8
CPU register read (only $4015 is meaningful). Reading clears the
frame IRQ flag.
Sourcepub fn clear_frame_irq_immediate_for_dma(&mut self)
pub fn clear_frame_irq_immediate_for_dma(&mut self)
Clear the frame IRQ flag immediately for DMA no-op reads of $4015.
The normal CPU-visible $4015 read path keeps the put-cycle deferred
clear needed by frame-counter timing tests. DMC DMA no-op repeats use
this after sampling the status value so the halted-read side effect is
visible before the CPU resumes the original $4015 read.
Session-26 iter 5: also deassert the CPU IRQ line driver
(irq_line_active) since the DMA no-op read mirrors a CPU
$4015 read on the silicon — the IRQ source is removed from
the CPU’s _irqSource list synchronously.