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}