Skip to main content

rustynes_apu/
frame_counter.rs

1// SPDX-License-Identifier: GPL-3.0-or-later
2//
3// Provenance: the lazy `$4015` frame-IRQ clear (`irq_flag_clear_cycle`, the read that schedules a clear one or two cycles later) is derived from Mesen2 (GPL-3.0-or-later), `ApuFrameCounter::GetIrqFlag` / `_irqFlagClearClock`, and the PAL step table is Mesen2's `stepCyclesPal` (also published on the NESdev wiki). See docs/originality-and-provenance.md (Section 1) and NOTICE. Classified v2.9.9 (core re-audit NC-17, maintainer's decision 2026-10-04): the in-source citations below record the derivation and are kept as written.
4
5//! APU frame counter (sequencer).
6//!
7//! Per `docs/apu-2a03.md` §Frame counter and NESdev wiki "APU Frame Counter".
8//!
9//! Two modes:
10//! - **4-step (mode 0)**: clocks at CPU cycles 7457, 14913, 22371, 29828, 29829, 29830
11//!   (NTSC).  Quarter-frame events at every step.  Half-frame events at 14913 and
12//!   29829.  Frame IRQ asserted at cycles 29828 and 29829 and 29830 if not inhibited.
13//! - **5-step (mode 1)**: clocks at CPU cycles 7457, 14913, 22371, 37281, 37282
14//!   (NTSC).  Quarter-frame at 7457, 14913, 22371, 37281.  Half-frame at 14913 and
15//!   37281.  No IRQ.
16//!
17//! ## PAL step positions (v2.1.5)
18//!
19//! The 2A03's sequencer divides the CPU clock; the PAL 2A07 uses a different
20//! divisor, so the *same* six sequencer steps land at different CPU-cycle
21//! counts.  Selected by [`FrameCounter::pal`] (true only for
22//! [`Region::Pal`](crate::Region::Pal); Dendy keeps the NTSC period):
23//! - **4-step (mode 0)**: 8313, 16627, 24939, 33252, 33253, 33254.
24//!   Quarter at 8313 / 16627 / 24939 / 33253.  Half at 16627 / 33253.
25//!   Frame IRQ at 33252 / 33253 / 33254 if not inhibited.
26//! - **5-step (mode 1)**: 8313, 16627, 24939, 41565, 41566.
27//!   Quarter at 8313 / 16627 / 24939 / 41565.  Half at 16627 / 41565.  No IRQ.
28//!
29//! These are the canonical Mesen2 `stepCyclesPal` values (verified against
30//! blargg's PAL-calibrated `pal_apu_tests` corpus — see
31//! `crates/rustynes-test-harness/tests/pal_apu_tests.rs`).  The IRQ-flag
32//! visibility / `irq_line_active` split at the terminal three cycles is
33//! identical in structure to the NTSC path; only the cycle counts move.
34//!
35//! Writing `$4017`:
36//! - Resets the cycle counter, with a 3- or 4-cycle delay (depending on whether the
37//!   write happened on an even or odd CPU cycle: 3 if write occurred on apu-clock-aligned
38//!   cycle, 4 otherwise).
39//! - If mode 1 (bit 7 set), immediately fires a quarter+half-frame clock.
40//! - If IRQ-inhibit (bit 6 set), clears any pending frame IRQ — on the write
41//!   cycle itself, not after the timer-reset delay.
42
43/// Output of one APU `tick` describing what events the frame counter fired.
44#[derive(Debug, Clone, Copy, Default)]
45pub struct FrameEvents {
46    /// Clock the channel quarter-frame sub-units (envelopes + linear counter).
47    pub quarter: bool,
48    /// Clock the channel half-frame sub-units (length counters + sweeps).
49    pub half: bool,
50    /// Frame IRQ was asserted this cycle (mode 0, not inhibited).
51    pub irq: bool,
52}
53
54/// Frame counter mode.
55#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
56pub enum Mode {
57    /// 4-step sequence with frame IRQ.
58    #[default]
59    FourStep,
60    /// 5-step sequence with no IRQ.
61    FiveStep,
62}
63
64/// Frame counter state.
65#[derive(Debug, Clone, Copy)]
66pub struct FrameCounter {
67    /// Current mode.
68    pub mode: Mode,
69    /// IRQ inhibit flag.
70    pub irq_inhibit: bool,
71    /// `$4015` bit 6 visibility flag (cleared by reading `$4015` or by
72    /// writing `$4017` with bit 6 set). **Independent of the CPU IRQ
73    /// line** (see [`irq_line_active`](Self::irq_line_active)) since
74    /// Session-26 Sprint 2 iter 5 (2026-05-23) — the AccuracyCoin
75    /// `APU Tests :: Frame Counter IRQ` Tests I/J/K specifically test
76    /// that with inhibit SET, `$4015` bit 6 is still visible for 2
77    /// CPU cycles (29828, 29829) before clearing at cycle 29830.
78    /// Mesen2 separates these two concepts: `_irqFlag` (this field)
79    /// vs `IRQSource::FrameCounter` registration on the CPU's
80    /// `_irqSource` list. RustyNES previously conflated them into a
81    /// single field, which broke the J/K axis. See
82    /// `docs/audit/session-26-sprint2-iter5-frame-counter-irq-split-2026-05-23.md`.
83    pub irq_flag: bool,
84    /// CPU IRQ line driver — true iff the frame counter is currently
85    /// asserting an IRQ on the CPU's `_irqSource` list (Mesen2's
86    /// `IRQSource::FrameCounter` registration). Set at FC steps 3, 4,
87    /// 5 (cycles 29828, 29829, 29830) ONLY when not inhibited;
88    /// cleared by `$4015` read or `$4017` inhibit-set. The CPU's
89    /// IRQ-poll path reads via [`Apu::irq_line`](crate::Apu::irq_line),
90    /// which ORs this with `dmc.irq_flag`. **Distinct from
91    /// [`irq_flag`](Self::irq_flag)**: when inhibited, `irq_flag` may
92    /// be transiently set at cycles 29828-29829 to make `$4015` bit 6
93    /// visible per Tests I/J/K, but `irq_line_active` stays false so
94    /// no spurious IRQ fires on the CPU.
95    pub irq_line_active: bool,
96    /// Current cycle counter (CPU clocks since last reset).
97    pub(crate) cycle: u32,
98    /// Pending reset (loaded by `$4017` write); `$4017_reset_in` cycles remaining.
99    pub(crate) reset_in: u8,
100    /// Pending mode that becomes active when `reset_in` reaches 0.
101    pub(crate) pending_mode: Mode,
102    /// Pending IRQ-inhibit when reset is consumed.
103    pub(crate) pending_inhibit: bool,
104    /// `apu_phase`: false on cycle that aligns with APU clock, true otherwise.
105    /// Used to time the `$4017` reset delay (3 vs 4 cycles).
106    pub apu_aligned: bool,
107    /// Future CPU cycle at which a pending `$4015`-read IRQ-flag clear
108    /// will mature. `0` = no pending clear scheduled. This mirrors
109    /// Mesen2's `ApuFrameCounter::_irqFlagClearClock` lazy-clear
110    /// algorithm (`Core/NES/APU/ApuFrameCounter.h` lines 214-227): a
111    /// `$4015` read while the flag is set SCHEDULES a future clear
112    /// (returning the OLD flag value), and a SUBSEQUENT
113    /// read/tick that observes `cpu_cycle >= irq_flag_clear_cycle`
114    /// performs the clear. The schedule delta is 1 CPU cycle for
115    /// reads on a RustyNES "get" cycle (`apu_phase=true`, odd cycle)
116    /// and 2 CPU cycles for reads on a "put" cycle
117    /// (`apu_phase=false`, even cycle); this is INVERTED relative to
118    /// Mesen2's `(clock & 0x01) ? 2 : 1` because RustyNES's
119    /// `apu_phase` polarity at the `$4015` read site is opposite to
120    /// Mesen2's master-clock parity (verified empirically against the
121    /// `frame-counter-irq.nes` oracle pair at
122    /// `crates/rustynes-test-harness/golden/irq_trace/frame-counter-irq.csv`
123    /// and `.../mesen2/frame-counter-irq.csv`). Replaces the prior
124    /// `pending_irq_clear: bool` consumed-on-next-tick mechanism that
125    /// failed `AccuracyCoin :: APU Tests :: Frame Counter IRQ` Test 7
126    /// (Session-25, 2026-05-23). See
127    /// `docs/audit/session-25-sprint2-iter3-frame-counter-irq-2026-05-23.md`.
128    pub(crate) irq_flag_clear_cycle: u64,
129    /// v2.0.0 beta.3 (A4 cycle-accurate reset): the last value written to
130    /// `$4017`, retained across warm reset. Per blargg's `apu_reset` spec
131    /// ("At reset ... the last value written to `$4017` is written AGAIN,
132    /// rather than `$00`") the 2A03's internal reset sequence re-issues the
133    /// `$4017` write with this value before execution resumes from the
134    /// reset vector. Power-on value `$00` (the power path "writes `$00`").
135    pub(crate) last_4017: u8,
136    /// v2.1.5 (PAL frame-counter step positions): true iff the console
137    /// region is [`Region::Pal`](crate::Region::Pal), selecting the PAL
138    /// sequencer clock positions (8313 / 16627 / 24939 / 33252-33254 in
139    /// 4-step; 8313 / 16627 / 24939 / 41565-41566 in 5-step) instead of
140    /// the NTSC positions (7457 / 14913 / 22371 / 29828-29830; 37281-37282).
141    /// **Dendy stays NTSC** — it is a PAL-clocked famiclone whose APU frame
142    /// counter uses the NTSC sequencer period, so only true `Region::Pal`
143    /// flips this. Derived from the owning [`Apu`](crate::Apu)'s `region`,
144    /// **not** persisted: the snapshot format is unchanged, and
145    /// [`Apu::restore`](crate::Apu::restore) re-derives it from the restored
146    /// region after reading the counter back. Power-on /
147    /// [`FrameCounter::new`] default is `false` (NTSC), which keeps every
148    /// NTSC/Dendy tick byte-identical to the pre-v2.1.5 model.
149    pub(crate) pal: bool,
150}
151
152impl Default for FrameCounter {
153    fn default() -> Self {
154        Self::new()
155    }
156}
157
158impl FrameCounter {
159    /// New frame counter (mode 0, IRQ enabled, cycle 0).
160    #[must_use]
161    pub const fn new() -> Self {
162        Self {
163            mode: Mode::FourStep,
164            irq_inhibit: false,
165            irq_flag: false,
166            irq_line_active: false,
167            cycle: 0,
168            reset_in: 0,
169            pending_mode: Mode::FourStep,
170            pending_inhibit: false,
171            apu_aligned: true,
172            irq_flag_clear_cycle: 0,
173            last_4017: 0x00,
174            pal: false,
175        }
176    }
177
178    /// v2.0.0 beta.3 (A4 cycle-accurate reset): warm-reset with the
179    /// hardware `$4017` re-write. Per blargg's `apu_reset` spec, the 2A03
180    /// reset sequence behaves as if the LAST value written to `$4017` were
181    /// written again: the retained `last_4017` is re-issued through
182    /// the normal write path (pending mode + the 3/4-cycle aligned delay,
183    /// and — for a mode-1 value — the immediate quarter+half clock), then
184    /// the sequencer restarts. The CPU's subsequent 8-cycle reset delay
185    /// (real clocked cycles on the master clock since Workstream A2) ages
186    /// the re-armed counter so execution resumes ~9-12 cycles after the
187    /// effective write — the window blargg's `4017_timing` brackets.
188    ///
189    /// Two prior frame-granular re-arm attempts (see
190    /// `tests/apu_reset.rs`'s history preamble) failed precisely because the
191    /// reset was a function call with no clocked delay; this variant exists
192    /// on the one-clock sequence path (promoted to the only path in beta.4).
193    pub fn reset_rewrite_4017(&mut self) -> u8 {
194        // Mode bit (7) is retained; the IRQ-inhibit bit (6) is CLEARED —
195        // per nesdev ("At reset, $4017 mode is unchanged, but IRQ inhibit
196        // flag is sometimes cleared") and Mesen2's frame-counter reset.
197        // Retaining bit 6 wedges blargg `4017_timing`'s second pass: with
198        // inhibit re-applied the frame IRQ flag never sets and the ROM's
199        // 14-probe measurement never terminates in-window.
200        let value = self.last_4017 & 0x80;
201        self.irq_flag = false;
202        self.irq_line_active = false;
203        self.cycle = 0;
204        // Cancel any in-flight pre-reset `$4017` write still inside its
205        // 3/4-cycle maturation window: letting it mature during the reset
206        // sequence would race the scheduled reset re-write (adopted from
207        // PR #219 review — both bots flagged the same gap).
208        self.reset_in = 0;
209        self.irq_flag_clear_cycle = 0;
210        value
211    }
212
213    /// `$4017` write.  `apu_aligned` is true if the *current* CPU cycle
214    /// is also an APU cycle (i.e., even CPU-cycle alignment).  The reset
215    /// happens 3 or 4 CPU cycles later depending on alignment.
216    pub fn write(&mut self, value: u8, apu_aligned: bool) {
217        self.last_4017 = value;
218        self.pending_mode = if (value & 0x80) != 0 {
219            Mode::FiveStep
220        } else {
221            Mode::FourStep
222        };
223        self.pending_inhibit = (value & 0x40) != 0;
224        // The inhibit bit clears the frame interrupt flag on the write
225        // itself; only the timer reset waits the 3-4 cycles below (wiki,
226        // "APU Frame Counter": bit 6 "If set, the frame interrupt flag is
227        // cleared", stated apart from "After 3 or 4 CPU clock cycles, the
228        // timer is reset"). Both the `$4015` bit and the CPU IRQ line drop,
229        // and a pending `$4015`-read clear has nothing left to do. The
230        // reset branch in `tick` still clears again, which is harmless.
231        // Pinned by `write_4017_inhibit_drops_the_irq_on_the_write_cycle`;
232        // Nintendo World Championships 1990 runs `CLI` on the next
233        // instruction and crashed into WRAM without it.
234        // The inhibit bit is a latch the write sets or clears at once, in
235        // both directions (v2.9.9, NC-16). v2.9.8 applied the SET at the
236        // write and left the CLEAR to the timer reset 3-4 cycles later, so a
237        // `$4017 = $00` written 1-3 cycles before 29828 with the inhibit set
238        // kept the old sequence's IRQ masked. The wiki gives bit 6 one
239        // effect and the 3-4 cycle delay to "the timer" only, which reads
240        // the same for both directions; no test ROM reaches the window.
241        // Pinned by `write_4017_inhibit_clear_unmasks_on_the_write_cycle`.
242        self.irq_inhibit = self.pending_inhibit;
243        if self.pending_inhibit {
244            // The inhibit itself takes effect with the write too. Clearing
245            // the flag here while leaving `irq_inhibit` to the timer reset
246            // let the OLD sequence raise the IRQ again at 29828-29830 when
247            // the write landed 1-3 cycles before it -- a frame interrupt
248            // delivered after the program inhibited it. No test ROM in the
249            // suite reaches that window (the APU and AccuracyCoin suites
250            // pass either way); the reading is the wiki's, which ties the
251            // flag clear to the inhibit bit, not to the reset. Pinned by
252            // `write_4017_inhibit_holds_through_the_reset_delay`.
253            self.irq_flag = false;
254            self.irq_line_active = false;
255            self.irq_flag_clear_cycle = 0;
256        }
257        // Schedule reset.  Per nesdev: 3 cycles if write on APU-aligned cycle, 4 otherwise.
258        // The reset effect itself fires on cycle 0 of the new sequence.
259        self.reset_in = if apu_aligned { 3 } else { 4 };
260    }
261
262    /// Reading `$4015` returns the current frame IRQ flag value and
263    /// SCHEDULES a future clear that matures one or two CPU cycles
264    /// later, mirroring Mesen2's `ApuFrameCounter::GetIrqFlag` lazy
265    /// algorithm (`Core/NES/APU/ApuFrameCounter.h` lines 214-227).
266    ///
267    /// Semantics:
268    /// - If `irq_flag` is true and no clear is scheduled, schedule
269    ///   `irq_flag_clear_cycle = cpu_cycle + delta` where
270    ///   `delta = 1` on a RustyNES "get" cycle (apu_phase=true) and
271    ///   `delta = 2` on a "put" cycle (apu_phase=false). Return the
272    ///   OLD flag value (true).
273    /// - If a schedule is already pending and `cpu_cycle >=
274    ///   irq_flag_clear_cycle`, perform the clear NOW (the silicon
275    ///   observed enough APU clocks since the read) and return the
276    ///   freshly-cleared flag (false).
277    /// - If no flag is set, return false (no schedule needed).
278    ///
279    /// The delta polarity is INVERTED vs Mesen2's `(clock & 0x01) ? 2 : 1`
280    /// because RustyNES's apu_phase polarity at the `$4015` read site
281    /// is opposite to Mesen2's master-clock parity. Verified against
282    /// the `frame-counter-irq.nes` oracle pair (Session-25,
283    /// 2026-05-23).
284    ///
285    /// `cpu_cycle` is the bus's CPU-cycle counter at the moment of
286    /// the read (passed in from `apu.rs::read_status`).
287    /// `apu_aligned` is `self.apu_phase` of the APU at the same
288    /// moment (also passed in from `apu.rs::read_status`).
289    pub fn read_status(&mut self, cpu_cycle: u64, apu_aligned: bool) -> bool {
290        // First: a previously-scheduled clear may have matured by now.
291        // This makes a second read at `cpu_cycle >= scheduled` observe
292        // the cleared flag, matching Mesen2's lazy-clear behaviour.
293        if self.irq_flag_clear_cycle != 0 && cpu_cycle >= self.irq_flag_clear_cycle {
294            self.irq_flag = false;
295            self.irq_flag_clear_cycle = 0;
296        }
297        let f = self.irq_flag;
298        // Schedule a fresh clear if the flag is still set and no
299        // schedule is currently pending. Re-reads while the schedule
300        // is pending do NOT reschedule (the silicon's clear-cycle is
301        // determined by the FIRST observation, not the latest).
302        if self.irq_flag && self.irq_flag_clear_cycle == 0 {
303            let delta: u64 = if apu_aligned { 1 } else { 2 };
304            self.irq_flag_clear_cycle = cpu_cycle.wrapping_add(delta);
305        }
306        // Session-26 iter 5: `$4015` read also deasserts the CPU IRQ
307        // line immediately (Mesen2 `ClearIrqSource(FrameCounter)` in
308        // `NesApu::ReadRam` — the IRQ source is removed from the CPU's
309        // `_irqSource` list synchronously, distinct from the lazy
310        // `_irqFlag` clear). This is what makes the AccuracyCoin
311        // Frame Counter IRQ Test M ("the IRQ does not actually fire
312        // during inhibit even though $4015 bit 6 is visible") observe
313        // a stable non-IRQ state: `irq_line_active` was never set in
314        // the inhibit path, and `$4015` reads continue to clear any
315        // stray assertion synchronously.
316        self.irq_line_active = false;
317        f
318    }
319
320    /// One CPU clock — return any frame-counter events fired by this cycle.
321    ///
322    /// `cpu_cycle` is the bus's CPU-cycle counter for the cycle
323    /// being ticked (the post-increment value, since
324    /// `apu.tick_with_external` advances `apu.cpu_cycle` BEFORE
325    /// invoking this).  `apu_aligned`: true iff this CPU cycle is
326    /// also an APU "get" cycle.  The lazy `$4015` IRQ clear matures
327    /// here if `cpu_cycle >= irq_flag_clear_cycle`; the per-frame
328    /// step events fire as before.
329    pub fn tick(&mut self, cpu_cycle: u64, apu_aligned: bool) -> FrameEvents {
330        let _ = apu_aligned;
331        // Mature any deferred `$4015` clear from a prior read.  Mesen2
332        // also matures the clear from `GetIrqFlag`; we additionally
333        // mature here so that ROMs which never re-read `$4015` still
334        // observe the canonical IRQ-line de-assertion timing.
335        if self.irq_flag_clear_cycle != 0 && cpu_cycle >= self.irq_flag_clear_cycle {
336            self.irq_flag = false;
337            self.irq_flag_clear_cycle = 0;
338        }
339        // Handle pending `$4017` write reset.
340        if self.reset_in > 0 {
341            self.reset_in -= 1;
342            if self.reset_in == 0 {
343                let new_mode = self.pending_mode;
344                // Read BEFORE the mode and position are overwritten: which
345                // clocks did the sequencer fire on the previous tick? See
346                // `prev_tick_step` for why that matters to a mode-1 write.
347                let (prev_quarter, prev_half) = self.prev_tick_step();
348                self.mode = new_mode;
349                self.irq_inhibit = self.pending_inhibit;
350                if self.irq_inhibit {
351                    self.irq_flag = false;
352                    // Session-26 iter 5: also deassert the CPU IRQ
353                    // line (separate field). Mesen2
354                    // `ApuFrameCounter::WriteRam` line 208:
355                    // `ClearIrqSource(FrameCounter)` accompanies the
356                    // `_irqFlag = false`.
357                    self.irq_line_active = false;
358                    // `$4017` inhibit clears the flag immediately and
359                    // invalidates any pending lazy `$4015`-read clear
360                    // schedule; per Mesen2 `ApuFrameCounter::WriteRam`
361                    // lines 207-211 (resets `_irqFlagClearClock`).
362                    self.irq_flag_clear_cycle = 0;
363                }
364                self.cycle = 0;
365                // Mode 1 (`Mode::FiveStep`): immediately fire quarter+half-frame events —
366                // unless the sequencer fired the same clock on the previous
367                // tick, in which case the two share one APU cycle and are one
368                // pulse, not two (v2.9.5, `prev_tick_step`).
369                if new_mode == Mode::FiveStep {
370                    return FrameEvents {
371                        quarter: !prev_quarter,
372                        half: !prev_half,
373                        irq: false,
374                    };
375                }
376                return FrameEvents::default();
377            }
378        }
379
380        self.clock_sequencer()
381    }
382
383    /// The quarter/half-frame clocks the sequencer fired on the PREVIOUS
384    /// tick, derived from its current position rather than stored.
385    ///
386    /// A mode-1 `$4017` write clocks the quarter- and half-frame units when
387    /// its reset matures. If that happens one CPU cycle after the sequencer's
388    /// own step fired the same clock, hardware produces ONE clock, not two:
389    /// the triggers are emitted on APU-cycle boundaries (nesdev wiki, *APU
390    /// Frame Counter* and its Talk page), and the step and the write land in
391    /// the same APU cycle. The rule is stated by blargg's
392    /// `tests/roms/extra/apu/apu_test_{1,2,5,6}.nes`, which fail with a
393    /// second decrement and pass with one; `apu_test_{3,4,7,8}` (one cycle
394    /// later) require the second, and `apu_test_{9,10}` show the step itself
395    /// still happens at those deltas. See
396    /// `crates/rustynes-test-harness/tests/apu_frame_clock_coincidence.rs`.
397    ///
398    /// Deriving it keeps the save-state format unchanged. Every step leaves
399    /// [`Self::cycle`] AT its step position until the next tick increments it
400    /// (the wrap steps, which reset it to 0, fire no clock), and a maturing
401    /// reset returns before [`Self::clock_sequencer`] runs. So at that
402    /// moment, "`cycle` equals a clocking step of the current mode" is exactly
403    /// "the previous tick fired that step".
404    fn prev_tick_step(&self) -> (bool, bool) {
405        let (q1, h1, q2, last) = match (self.mode, self.pal) {
406            (Mode::FourStep, false) => (7457, 14913, 22371, 29829),
407            (Mode::FourStep, true) => (8313, 16627, 24939, 33253),
408            (Mode::FiveStep, false) => (7457, 14913, 22371, 37281),
409            (Mode::FiveStep, true) => (8313, 16627, 24939, 41565),
410        };
411        let c = self.cycle;
412        let half = c == h1 || c == last;
413        (half || c == q1 || c == q2, half)
414    }
415
416    /// Advance the sequencer one CPU cycle and return the events it fires.
417    ///
418    /// Split out of [`tick`](Self::tick) so each mode's step table stays a
419    /// self-contained, readable unit (and to keep `tick` within the clippy
420    /// line budget). Dispatches to the mode-specific handler
421    /// ([`four_step`](Self::four_step) / [`five_step`](Self::five_step)),
422    /// each of which selects PAL vs NTSC step positions from [`Self::pal`].
423    fn clock_sequencer(&mut self) -> FrameEvents {
424        let mut ev = FrameEvents::default();
425        self.cycle += 1;
426        match self.mode {
427            Mode::FourStep => self.four_step(&mut ev),
428            Mode::FiveStep => self.five_step(&mut ev),
429        }
430        ev
431    }
432
433    /// 4-step (mode 0) sequencer step.  Fires quarter/half/IRQ events and wraps
434    /// the counter at the terminal step.
435    ///
436    /// - **NTSC/Dendy** (`pal == false`, default): steps at 7457 / 14913 /
437    ///   22371 / 29828 / 29829 / 29830.  Quarter at 7457 / 14913 / 22371 /
438    ///   29829; half at 14913 / 29829; frame IRQ at 29828 / 29829 / 29830.
439    ///   This arm is byte-identical to the pre-v2.1.5 model.
440    /// - **PAL** (`pal == true`, v2.1.5): steps at 8313 / 16627 / 24939 /
441    ///   33252 / 33253 / 33254 (Mesen2 `stepCyclesPal`).  Quarter at 8313 /
442    ///   16627 / 24939 / 33253; half at 16627 / 33253; frame IRQ at 33252 /
443    ///   33253 / 33254.  The IRQ-flag-visibility / `irq_line_active` split is
444    ///   structurally identical to the NTSC arm — only the cycle counts move.
445    fn four_step(&mut self, ev: &mut FrameEvents) {
446        if self.pal {
447            match self.cycle {
448                8313 => ev.quarter = true,
449                16627 => {
450                    ev.quarter = true;
451                    ev.half = true;
452                }
453                24939 => ev.quarter = true,
454                33252 => {
455                    // PAL step 4 (mirrors NTSC 29828): set the `$4015` bit-6
456                    // visibility flag unconditionally; assert the CPU IRQ line
457                    // (`irq_line_active`) only when NOT inhibited.
458                    self.irq_flag = true;
459                    self.irq_flag_clear_cycle = 0;
460                    if !self.irq_inhibit {
461                        self.irq_line_active = true;
462                        ev.irq = true;
463                    }
464                }
465                33253 => {
466                    // PAL step 5 (mirrors NTSC 29829): IRQ + quarter + half.
467                    self.irq_flag = true;
468                    self.irq_flag_clear_cycle = 0;
469                    if !self.irq_inhibit {
470                        self.irq_line_active = true;
471                        ev.irq = true;
472                    }
473                    ev.quarter = true;
474                    ev.half = true;
475                }
476                33254 => {
477                    // PAL step 6 / wrap (mirrors NTSC 29830): the inhibit
478                    // branch clears the flag (ending the visibility window),
479                    // the non-inhibit branch re-asserts the IRQ.
480                    if self.irq_inhibit {
481                        self.irq_flag = false;
482                        self.irq_flag_clear_cycle = 0;
483                        self.irq_line_active = false;
484                    } else {
485                        self.irq_flag = true;
486                        self.irq_flag_clear_cycle = 0;
487                        self.irq_line_active = true;
488                        ev.irq = true;
489                    }
490                    self.cycle = 0; // wrap
491                }
492                _ => {}
493            }
494        } else {
495            match self.cycle {
496                7457 => ev.quarter = true,
497                14913 => {
498                    ev.quarter = true;
499                    ev.half = true;
500                }
501                22371 => ev.quarter = true,
502                29828 => {
503                    // Session-26 iter 5: set `irq_flag` (the $4015 bit
504                    // 6 visibility) UNCONDITIONALLY, but assert the
505                    // CPU IRQ line (`irq_line_active`) only when NOT
506                    // inhibited. Per Mesen2 `ApuFrameCounter.h` lines
507                    // 104-107: `_irqFlag = true; _irqFlagClearClock =
508                    // 0;` runs always; `SetIrqSource(FrameCounter)`
509                    // runs only when `!_inhibitIRQ`. This is the
510                    // Tests I/J/K/L surface — $4015 bit 6 must be
511                    // visible at cycles 29828-29829 even under
512                    // inhibit; Test M then verifies no actual IRQ is
513                    // delivered (the CPU's IRQ-line state stays
514                    // false). The pre-iter-5 conflated implementation
515                    // gated everything on `!self.irq_inhibit`,
516                    // failing Tests J/K.
517                    self.irq_flag = true;
518                    self.irq_flag_clear_cycle = 0;
519                    if !self.irq_inhibit {
520                        self.irq_line_active = true;
521                        ev.irq = true;
522                    }
523                }
524                29829 => {
525                    self.irq_flag = true;
526                    self.irq_flag_clear_cycle = 0;
527                    if !self.irq_inhibit {
528                        self.irq_line_active = true;
529                        ev.irq = true;
530                    }
531                    ev.quarter = true;
532                    ev.half = true;
533                }
534                29830 => {
535                    // Per Mesen2 `ApuFrameCounter.h` lines 110-115: at
536                    // step 5 (cycle 29830), the inhibit branch
537                    // CLEARS `_irqFlag` (and the schedule) — the
538                    // "2 CPU cycle visible window" ends here. The
539                    // non-inhibit branch re-sets the flag and asserts
540                    // the IRQ. Test L verifies that with inhibit set,
541                    // `$4015` bit 6 is CLEAR at this cycle.
542                    if self.irq_inhibit {
543                        self.irq_flag = false;
544                        self.irq_flag_clear_cycle = 0;
545                        // `irq_line_active` was never set in this
546                        // run; explicitly false here for clarity.
547                        self.irq_line_active = false;
548                    } else {
549                        self.irq_flag = true;
550                        self.irq_flag_clear_cycle = 0;
551                        self.irq_line_active = true;
552                        ev.irq = true;
553                    }
554                    self.cycle = 0; // wrap
555                }
556                _ => {}
557            }
558        }
559    }
560
561    /// 5-step (mode 1) sequencer step.  No frame IRQ.
562    ///
563    /// - **NTSC/Dendy** (default): 7457 / 14913 / 22371 / 37281 / 37282.
564    ///   Quarter at 7457 / 14913 / 22371 / 37281; half at 14913 / 37281.
565    /// - **PAL** (v2.1.5): 8313 / 16627 / 24939 / 41565 / 41566.  Quarter at
566    ///   8313 / 16627 / 24939 / 41565; half at 16627 / 41565.
567    fn five_step(&mut self, ev: &mut FrameEvents) {
568        if self.pal {
569            match self.cycle {
570                8313 => ev.quarter = true,
571                16627 => {
572                    ev.quarter = true;
573                    ev.half = true;
574                }
575                24939 => ev.quarter = true,
576                // No event at 33253 in PAL 5-step.
577                41565 => {
578                    ev.quarter = true;
579                    ev.half = true;
580                }
581                41566 => {
582                    self.cycle = 0;
583                }
584                _ => {}
585            }
586        } else {
587            match self.cycle {
588                7457 => ev.quarter = true,
589                14913 => {
590                    ev.quarter = true;
591                    ev.half = true;
592                }
593                22371 => ev.quarter = true,
594                // No event at 29829 in 5-step.
595                37281 => {
596                    ev.quarter = true;
597                    ev.half = true;
598                }
599                37282 => {
600                    self.cycle = 0;
601                }
602                _ => {}
603            }
604        }
605    }
606}
607
608#[cfg(test)]
609mod tests {
610    use super::*;
611
612    /// Helper: drive `tick` with a monotonic cpu_cycle counter that
613    /// mirrors `Apu::tick_with_external`'s `self.cpu_cycle` advancement.
614    fn drive_tick(fc: &mut FrameCounter, cpu_cycle: &mut u64, apu_aligned: bool) -> FrameEvents {
615        *cpu_cycle = cpu_cycle.wrapping_add(1);
616        fc.tick(*cpu_cycle, apu_aligned)
617    }
618
619    #[test]
620    fn four_step_quarter_frame_at_7457() {
621        let mut fc = FrameCounter::new();
622        let mut cyc = 0u64;
623        for _ in 0..7456 {
624            assert!(!drive_tick(&mut fc, &mut cyc, true).quarter);
625        }
626        let ev = drive_tick(&mut fc, &mut cyc, true);
627        assert!(ev.quarter);
628        assert!(!ev.half);
629    }
630
631    #[test]
632    fn four_step_irq_at_29828() {
633        let mut fc = FrameCounter::new();
634        let mut cyc = 0u64;
635        for _ in 0..29827 {
636            drive_tick(&mut fc, &mut cyc, true);
637        }
638        let ev = drive_tick(&mut fc, &mut cyc, true);
639        assert!(ev.irq);
640        assert!(fc.irq_flag);
641    }
642
643    #[test]
644    fn read_status_returns_old_flag_and_schedules_clear() {
645        // Get-cycle (apu_aligned=true) first read at cpu_cycle=100:
646        // schedule clear at 101, return TRUE (old flag value).
647        let mut fc = FrameCounter::new();
648        fc.irq_flag = true;
649        assert!(fc.read_status(100, true));
650        assert_eq!(fc.irq_flag_clear_cycle, 101);
651        // The flag is STILL set right after the first read; Mesen2 lazy
652        // semantics (the silicon clears it at the next get cycle).
653        assert!(fc.irq_flag);
654        // Second read at cpu_cycle=101 sees the matured clear.
655        assert!(!fc.read_status(101, false));
656        assert!(!fc.irq_flag);
657        assert_eq!(fc.irq_flag_clear_cycle, 0);
658    }
659
660    #[test]
661    fn read_status_put_cycle_defers_two_cycles() {
662        // Put-cycle (apu_aligned=false) first read at cpu_cycle=200:
663        // schedule clear at 202. Second read at 201 still sees the
664        // flag set (Test 7 in `AccuracyCoin :: APU Tests :: Frame
665        // Counter IRQ`).
666        let mut fc = FrameCounter::new();
667        fc.irq_flag = true;
668        assert!(fc.read_status(200, false));
669        assert_eq!(fc.irq_flag_clear_cycle, 202);
670        // Second read at the immediately-following CPU cycle (201)
671        // observes the flag still set.
672        assert!(fc.read_status(201, true));
673        assert!(fc.irq_flag);
674        // Third read at cpu_cycle=202 matures the clear.
675        assert!(!fc.read_status(202, false));
676        assert!(!fc.irq_flag);
677    }
678
679    #[test]
680    fn write_4017_inhibit_clears_flag() {
681        let mut fc = FrameCounter::new();
682        fc.irq_flag = true;
683        fc.write(0xC0, true); // mode=1, inhibit=1
684        // After 3-cycle delay, reset.
685        let mut cyc = 0u64;
686        for _ in 0..3 {
687            drive_tick(&mut fc, &mut cyc, true);
688        }
689        assert!(!fc.irq_flag);
690        // The pending clear schedule is also wiped by the inhibit path.
691        assert_eq!(fc.irq_flag_clear_cycle, 0);
692    }
693
694    /// The inhibit clear belongs to the WRITE, not to the 3-4 cycle timer
695    /// reset. The wiki's frame-counter page gives the two separately: bit 6
696    /// "If set, the frame interrupt flag is cleared", and only "the timer is
697    /// reset" after 3 or 4 CPU cycles.
698    ///
699    /// *Nintendo World Championships 1990* (mapper 105) depends on it. Its
700    /// reset code waits two vblanks (~57,190 cycles, past the first frame
701    /// IRQ at 29,828 with the power-on `$4017 = $00`), then runs
702    /// `STA $4017` (`$40`), `CLI`, `LDA #$FF`. `LDA #imm` polls /IRQ three
703    /// cycles after the write, inside the old 3-4 cycle window, and the IRQ
704    /// vector is `$6010` in uninitialised WRAM: with the clear deferred to
705    /// the timer reset the cart executed `BRK`s from WRAM forever and
706    /// showed a blank screen on every frame.
707    #[test]
708    fn write_4017_inhibit_drops_the_irq_on_the_write_cycle() {
709        let mut fc = FrameCounter::new();
710        let mut cyc = 0u64;
711        // Past the three-cycle set window (29828-29830), with the flag held,
712        // as in the game. A write inside that window is a separate question
713        // (whether the inhibit itself also waits) that no source settles.
714        for _ in 0..29900 {
715            drive_tick(&mut fc, &mut cyc, true);
716        }
717        assert!(
718            fc.irq_flag && fc.irq_line_active,
719            "frame IRQ raised and held"
720        );
721        for aligned in [true, false] {
722            let mut f = fc;
723            f.write(0x40, aligned);
724            assert!(
725                !f.irq_line_active,
726                "IRQ line low on the write (aligned={aligned})"
727            );
728            assert!(
729                !f.irq_flag,
730                "$4015 bit 6 clear on the write (aligned={aligned})"
731            );
732            // Nothing re-raises it while the timer reset is still pending.
733            let mut c = cyc;
734            for _ in 0..4 {
735                drive_tick(&mut f, &mut c, aligned);
736                assert!(!f.irq_line_active && !f.irq_flag);
737            }
738        }
739    }
740
741    /// v2.9.8 (`CodeRabbit` on the review slice #580) — the inhibit written
742    /// 1-3 cycles before the four-step IRQ steps holds through the 3-4 cycle
743    /// timer-reset delay. With the inhibit deferred to the reset, the old
744    /// sequence reached 29828 first and raised the IRQ the write had just
745    /// inhibited.
746    #[test]
747    fn write_4017_inhibit_holds_through_the_reset_delay() {
748        for lead in 1..=3u32 {
749            for aligned in [true, false] {
750                let mut fc = FrameCounter::new();
751                let mut cyc = 0u64;
752                for _ in 0..(29828 - lead) {
753                    drive_tick(&mut fc, &mut cyc, aligned);
754                }
755                fc.write(0x40, aligned);
756                for _ in 0..8 {
757                    drive_tick(&mut fc, &mut cyc, aligned);
758                    assert!(
759                        !fc.irq_line_active,
760                        "IRQ raised after an inhibiting write {lead} cycle(s) \
761                         before 29828 (aligned={aligned})"
762                    );
763                }
764            }
765        }
766    }
767
768    /// NC-16 (v2.9.9 re-audit): the mirror of the test above. With the
769    /// inhibit set, a `$4017 = $00` written just before 29828 unmasks the old
770    /// sequence's IRQ at once rather than at the timer reset.
771    #[test]
772    fn write_4017_inhibit_clear_unmasks_on_the_write_cycle() {
773        for lead in 1..=3u32 {
774            for aligned in [true, false] {
775                let mut fc = FrameCounter::new();
776                let mut cyc = 0u64;
777                // Inhibited from power-on, as a matured `$4017 = $40` leaves
778                // it, without a write that would restart the sequence.
779                fc.irq_inhibit = true;
780                fc.pending_inhibit = true;
781                for _ in 0..(29828 - lead) {
782                    drive_tick(&mut fc, &mut cyc, aligned);
783                }
784                assert!(!fc.irq_line_active, "inhibited before the write");
785                fc.write(0x00, aligned);
786                let mut raised = false;
787                for _ in 0..8 {
788                    drive_tick(&mut fc, &mut cyc, aligned);
789                    raised |= fc.irq_line_active;
790                }
791                // The old sequence reaches 29828 after `lead` cycles; the
792                // timer reset restarts it after 3 (aligned) or 4. When the
793                // reset lands first the old sequence never gets there.
794                let reset_delay = if aligned { 3 } else { 4 };
795                assert_eq!(raised, lead < reset_delay, "lead {lead} aligned {aligned}");
796            }
797        }
798    }
799
800    #[test]
801    fn write_4017_mode1_fires_immediate_clock() {
802        let mut fc = FrameCounter::new();
803        fc.write(0x80, true); // mode=1
804        let mut cyc = 0u64;
805        for _ in 0..2 {
806            drive_tick(&mut fc, &mut cyc, true);
807        }
808        let ev = drive_tick(&mut fc, &mut cyc, true);
809        assert!(ev.quarter);
810        assert!(ev.half);
811    }
812
813    #[test]
814    fn step_29828_invalidates_pending_clear_schedule() {
815        // Tests E-H in the AccuracyCoin Frame Counter IRQ suite:
816        // reading $4015 on/near the cycle the IRQ flag is RE-SET by
817        // the frame counter does NOT clear the flag, because the step
818        // re-asserts the flag AND resets the schedule.
819        let mut fc = FrameCounter::new();
820        let mut cyc = 0u64;
821        for _ in 0..29827 {
822            drive_tick(&mut fc, &mut cyc, true);
823        }
824        let ev = drive_tick(&mut fc, &mut cyc, true);
825        assert!(ev.irq);
826        assert!(fc.irq_flag);
827        // Stage a fake pending clear from a hypothetical prior read,
828        // then re-run the 29828 step path: the step should wipe the
829        // schedule.
830        fc.irq_flag_clear_cycle = 999_999;
831        // Driving the step itself again (by walking the counter back
832        // to 29828) is harder to model in isolation; the equivalence
833        // is covered structurally by the step-setting branches above
834        // (`self.irq_flag_clear_cycle = 0` alongside
835        // `self.irq_flag = true`). This test simply asserts the
836        // post-step invariant.
837        fc.irq_flag = true;
838        fc.irq_flag_clear_cycle = 0; // mimicking the step body
839        assert_eq!(fc.irq_flag_clear_cycle, 0);
840        assert!(fc.irq_flag);
841    }
842
843    // ---- PAL sequencer step positions (v2.1.5) ----
844
845    /// Build a PAL-configured frame counter (as `Apu::new(Region::Pal, …)`
846    /// does): identical to `new()` except the PAL step-position selector.
847    fn pal_fc() -> FrameCounter {
848        let mut fc = FrameCounter::new();
849        fc.pal = true;
850        fc
851    }
852
853    #[test]
854    fn pal_four_step_quarter_frame_at_8313() {
855        // PAL step 0 fires a quarter-frame at 8313 (not the NTSC 7457), and
856        // nothing before it. Guards the region-gated step position.
857        let mut fc = pal_fc();
858        let mut cyc = 0u64;
859        for _ in 0..8312 {
860            assert!(!drive_tick(&mut fc, &mut cyc, true).quarter);
861        }
862        let ev = drive_tick(&mut fc, &mut cyc, true);
863        assert!(ev.quarter);
864        assert!(!ev.half);
865    }
866
867    #[test]
868    fn pal_four_step_half_frame_at_16627() {
869        let mut fc = pal_fc();
870        let mut cyc = 0u64;
871        for _ in 0..16626 {
872            let ev = drive_tick(&mut fc, &mut cyc, true);
873            assert!(!ev.half);
874        }
875        let ev = drive_tick(&mut fc, &mut cyc, true);
876        assert!(ev.quarter);
877        assert!(ev.half);
878    }
879
880    #[test]
881    fn pal_four_step_irq_at_33252() {
882        // PAL IRQ asserts at step 3 = cycle 33252 (mirrors NTSC 29828).
883        let mut fc = pal_fc();
884        let mut cyc = 0u64;
885        for _ in 0..33251 {
886            drive_tick(&mut fc, &mut cyc, true);
887        }
888        let ev = drive_tick(&mut fc, &mut cyc, true);
889        assert!(ev.irq);
890        assert!(fc.irq_flag);
891        assert!(fc.irq_line_active);
892    }
893
894    #[test]
895    fn pal_four_step_no_irq_at_ntsc_position() {
896        // A PAL counter must NOT fire the IRQ at the NTSC cycle 29828.
897        let mut fc = pal_fc();
898        let mut cyc = 0u64;
899        for _ in 0..29828 {
900            let ev = drive_tick(&mut fc, &mut cyc, true);
901            assert!(!ev.irq, "PAL counter fired IRQ at an NTSC step position");
902        }
903        assert!(!fc.irq_flag);
904    }
905
906    #[test]
907    fn pal_four_step_wrap_at_33254() {
908        // After the terminal step 5 (33254) the sequencer wraps: the next
909        // quarter lands at 33254 + 8313 = 41567 relative to start.
910        let mut fc = pal_fc();
911        let mut cyc = 0u64;
912        for _ in 0..33254 {
913            drive_tick(&mut fc, &mut cyc, true);
914        }
915        assert_eq!(fc.cycle, 0, "counter must wrap to 0 after cycle 33254");
916        for _ in 0..8312 {
917            assert!(!drive_tick(&mut fc, &mut cyc, true).quarter);
918        }
919        assert!(drive_tick(&mut fc, &mut cyc, true).quarter);
920    }
921
922    #[test]
923    fn pal_five_step_positions() {
924        // Mode-1 PAL: quarter at 8313/16627/24939/41565, half at 16627/41565,
925        // no IRQ, wrap at 41566.
926        let mut fc = pal_fc();
927        fc.write(0x80, true); // mode 1
928        let mut cyc = 0u64;
929        // Consume the 3-cycle reset delay + the immediate mode-1 clock.
930        for _ in 0..3 {
931            drive_tick(&mut fc, &mut cyc, true);
932        }
933        assert_eq!(fc.mode, Mode::FiveStep);
934        // 41565 is the 4th step (half + quarter); no IRQ anywhere.
935        let mut saw_irq = false;
936        for _ in 0..41566 {
937            let ev = drive_tick(&mut fc, &mut cyc, true);
938            saw_irq |= ev.irq;
939        }
940        assert!(!saw_irq, "PAL 5-step must never assert a frame IRQ");
941        assert_eq!(fc.cycle, 0, "PAL 5-step must wrap to 0 after 41566");
942    }
943
944    #[test]
945    fn ntsc_default_pal_flag_is_false() {
946        // The default (power-on / NTSC / Dendy) counter keeps the NTSC step
947        // positions — the byte-identity guarantee for NTSC/Dendy.
948        let fc = FrameCounter::new();
949        assert!(!fc.pal);
950    }
951}