Skip to main content

rustynes_apu/
length.rs

1//! Length-counter sub-unit (shared by pulse, triangle, noise).
2//!
3//! Per `docs/apu-2a03.md` §State and the NESdev wiki "APU Length Counter"
4//! page. A 5-bit register selects from a fixed 32-entry lookup table; when
5//! non-zero and clocked at half-frame, decrements toward zero. A `halt` bit
6//! freezes the counter (also doubles as the envelope-loop bit on pulse and
7//! noise channels).
8//!
9//! ## Halt/reload write ordering vs the half-frame clock (v2.1.5)
10//!
11//! The 2A03 applies a length-counter **halt** change and a length **reload**
12//! (`$4003`/`$4007`/`$400B`/`$400F` load) with a one-step deferral relative to
13//! the frame sequencer's half-frame length clock — the behaviour blargg's
14//! `pal_apu_tests` `10.len_halt_timing` and `11.len_reload_timing` (and their
15//! NTSC `blargg_apu_2005` twins) pin:
16//!
17//! - **Halt after clock, not before.** A `$4000`-bit-5 write that lands on the
18//!   *same* CPU cycle as a half-frame length clock does **not** suppress that
19//!   cycle's clock; the halt takes effect for the *next* clock. Modelled by
20//!   latching the written value in [`new_halt`](LengthCounter::new_halt) and
21//!   promoting it to the effective [`halt`](LengthCounter::halt) in
22//!   [`reload`](LengthCounter::reload), which the owning APU calls once per CPU
23//!   cycle *after* the half-frame [`clock`](LengthCounter::clock) but *before*
24//!   the mixer samples the channel output.
25//! - **Reload ignored during a non-zero clock.** A length load that lands on the
26//!   half-frame clock cycle is honoured only if the counter was **not** clocked
27//!   this cycle (i.e. it was already zero, so the decrement was a no-op).
28//!   Modelled by snapshotting the pre-clock count in
29//!   [`previous_count`](LengthCounter::previous_count) at load time and, in
30//!   [`reload`](LengthCounter::reload), applying the pending
31//!   [`reload_val`](LengthCounter::reload_val) only when the (post-clock) count
32//!   still equals that snapshot.
33//!
34//! This mirrors the `TetaNES` `LengthCounter` (`new_halt` / `reload` /
35//! `previous_counter`) and Mesen2's `ApuLengthCounter` (`_newHaltValue` +
36//! reload-request) mechanisms verbatim. On a write that does **not** coincide
37//! with a half-frame clock the deferral is invisible: `reload` runs in the same
38//! cycle as the write (after the no-op clock, before the sample), so the count
39//! settles to the loaded value / the halt settles to the written value *within
40//! the write cycle* — byte-identical to an immediate apply. Only the
41//! write-lands-exactly-on-the-clock-cycle coincidence differs, which is the
42//! precise edge the test ROMs probe.
43
44/// 32-entry length lookup table (from the NESdev wiki).
45pub const LENGTH_TABLE: [u8; 32] = [
46    10, 254, 20, 2, 40, 4, 80, 6, 160, 8, 60, 10, 14, 12, 26, 14, 12, 16, 24, 18, 48, 20, 96, 22,
47    192, 24, 72, 26, 16, 28, 32, 30,
48];
49
50/// Length counter shared by pulse, triangle, noise channels.
51#[derive(Debug, Clone, Copy, Default)]
52pub struct LengthCounter {
53    /// Current count (0..=254). 0 = silenced.
54    pub count: u8,
55    /// Effective halt flag consulted by [`clock`](Self::clock) (also serves as
56    /// envelope-loop on pulse/noise; control on triangle). Updated from
57    /// [`new_halt`](Self::new_halt) in [`reload`](Self::reload).
58    pub halt: bool,
59    /// Latched halt value from the most recent `$4000`/`$4004`/`$4008`/`$400C`
60    /// write, promoted to [`halt`](Self::halt) by [`reload`](Self::reload)
61    /// *after* the half-frame clock (the "halt change occurs after clocking
62    /// length" rule). See the module docs.
63    pub new_halt: bool,
64    /// Channel-enable flag from `$4015` write.
65    pub enabled: bool,
66    /// Pending reload value from the most recent length load. `0` = no pending
67    /// reload (a real load never selects table entry 0 for index 0 → value 10,
68    /// so `0` is an unambiguous "empty" sentinel here; the table has no 0
69    /// entry). Consumed by [`reload`](Self::reload).
70    pub reload_val: u8,
71    /// Snapshot of [`count`](Self::count) captured at load time. If the
72    /// half-frame clock decremented the counter this cycle, the post-clock
73    /// count differs from this snapshot and the pending reload is dropped (the
74    /// "reload ignored during clocking when ctr > 0" rule). See the module docs.
75    pub previous_count: u8,
76}
77
78impl LengthCounter {
79    /// Load a new value from a `$4003`/`$4007`/`$400B`/`$400F` write.
80    /// Lookup index = top 5 bits of the value.
81    ///
82    /// The reload is **deferred**: it latches [`reload_val`](Self::reload_val)
83    /// and snapshots the current [`count`](Self::count) into
84    /// [`previous_count`](Self::previous_count); [`reload`](Self::reload)
85    /// applies it (or drops it, if a same-cycle half-frame clock moved the
86    /// count). A disabled channel ignores the load entirely.
87    pub fn load(&mut self, raw: u8) {
88        if self.enabled {
89            self.reload_val = LENGTH_TABLE[(raw >> 3) as usize];
90            self.previous_count = self.count;
91        }
92    }
93
94    /// Latch a halt-flag change from a `$4000`/`$4004`/`$4008`/`$400C` write.
95    /// The value takes effect at the next [`reload`](Self::reload) (after the
96    /// half-frame clock), not immediately — see the module docs.
97    pub const fn set_halt(&mut self, halt: bool) {
98        self.new_halt = halt;
99    }
100
101    /// Channel-enable update from `$4015` write. Clearing the bit forces
102    /// the count to 0 (silences the channel).
103    pub const fn set_enabled(&mut self, enabled: bool) {
104        self.enabled = enabled;
105        if !enabled {
106            self.count = 0;
107        }
108    }
109
110    /// Half-frame clock.
111    pub const fn clock(&mut self) {
112        if !self.halt && self.count > 0 {
113            self.count -= 1;
114        }
115    }
116
117    /// Apply the deferred halt and reload. Called by the owning APU once per CPU
118    /// cycle, **after** the half-frame [`clock`](Self::clock) and **before** the
119    /// mixer samples the channel:
120    ///
121    /// - A pending reload is honoured only if the post-clock count still equals
122    ///   the [`previous_count`](Self::previous_count) snapshot taken at load —
123    ///   i.e. a same-cycle half-frame clock did not decrement it (it was already
124    ///   zero). Otherwise the reload is dropped.
125    /// - The effective [`halt`](Self::halt) is refreshed from
126    ///   [`new_halt`](Self::new_halt) unconditionally, so a halt change becomes
127    ///   effective for the *next* clock.
128    ///
129    /// On a cycle with no half-frame clock (the overwhelmingly common case),
130    /// the count is untouched between the write and this call, so `count ==
131    /// previous_count` holds and the reload applies in-cycle — byte-identical to
132    /// an immediate load.
133    pub const fn reload(&mut self) {
134        if self.reload_val > 0 {
135            if self.count == self.previous_count {
136                self.count = self.reload_val;
137            }
138            self.reload_val = 0;
139        }
140        self.halt = self.new_halt;
141    }
142
143    /// `$4015` read — bit set if count > 0.
144    #[must_use]
145    pub const fn active(&self) -> bool {
146        self.count > 0
147    }
148}