rustynes_apu/dmc.rs
1//! DMC (delta-modulation channel).
2//!
3//! Per `docs/apu-2a03.md` §DMC channel and NESdev wiki "APU DMC" page.
4//!
5//! Architecture:
6//! - **Memory reader**: fetches sample bytes via DMA.
7//! - **Sample buffer**: 1-byte; loaded from memory reader, drained into the
8//! bit-shift register.
9//! - **Output unit**: 8-bit shift register + bits-remaining counter; clocks
10//! the 7-bit DAC value by ±2 per bit.
11//! - **Timer**: counts at the APU clock; period from rate table.
12
13use crate::Region;
14
15/// v2.0 Phase 2 (`mc-r1-dmc-reenable-phase`): swept byte-timer realignment.
16///
17/// A signed byte-timer phase shift (in APU-rate timer units) applied ONCE at
18/// the `$4015` re-enable exclusion boundary (when `cannot_run == 2` blocks a
19/// would-be looping-reload arm). `0` = no shift (the bare exclusion gate). Env
20/// `RUSTYNES_REENABLE_BUMP`.
21pub static REENABLE_BUMP: core::sync::atomic::AtomicI32 = core::sync::atomic::AtomicI32::new(0);
22
23/// v2.0 final lever #1 (`mc-r1-dmc-halt-subpos`): swept per-CPU-cycle arm DELAY.
24///
25/// Number of CPU cycles to DELAY the `$540` X=10/11 reload arm (pattern-A
26/// boundary) past the natural cannot_run-deferred re-arm. Sub-APU-cycle
27/// granularity the byte-timer phase shift cannot express. `0` = arm at the
28/// boundary (no delay). Env `RUSTYNES_SUBPOS_DELAY` (default 1).
29pub static SUBPOS_DELAY: core::sync::atomic::AtomicI32 = core::sync::atomic::AtomicI32::new(1);
30
31/// 16-entry NTSC rate table (CPU cycles per output bit). NESdev wiki.
32pub const NTSC_DMC_RATES: [u16; 16] = [
33 428, 380, 340, 320, 286, 254, 226, 214, 190, 160, 142, 128, 106, 84, 72, 54,
34];
35
36/// 16-entry PAL rate table.
37pub const PAL_DMC_RATES: [u16; 16] = [
38 398, 354, 316, 298, 276, 236, 210, 198, 176, 148, 132, 118, 98, 78, 66, 50,
39];
40
41/// DMC channel state.
42#[derive(Debug, Clone, Copy)]
43pub struct Dmc {
44 // ----- Configuration (set by `$4010-$4013`) -----
45 /// IRQ enable bit (`$4010` bit 7).
46 pub irq_enable: bool,
47 /// Loop bit (`$4010` bit 6).
48 pub loop_flag: bool,
49 /// Rate index (`$4010` bits 0-3).
50 pub(crate) rate_index: u8,
51 /// Sample address (`$4012` × 64 + 0xC000).
52 pub(crate) sample_addr: u16,
53 /// Sample length (`$4013` × 16 + 1).
54 pub(crate) sample_length: u16,
55
56 // ----- Memory reader -----
57 /// Current memory address.
58 pub current_addr: u16,
59 /// Bytes remaining to fetch.
60 pub bytes_remaining: u16,
61
62 // ----- Sample buffer -----
63 /// Sample byte awaiting transfer to shift register.
64 pub(crate) sample_buffer: Option<u8>,
65
66 // ----- Output unit -----
67 /// 8-bit shift register.
68 pub(crate) shift_register: u8,
69 /// Bits remaining in shift register (0..=8).
70 pub(crate) bits_remaining: u8,
71 /// 7-bit DAC value (0..=127).
72 pub dac: u8,
73 /// Silenced flag (no sample data when output cycle began).
74 pub(crate) silence: bool,
75
76 // ----- Timer -----
77 /// Timer reload (from rate table).
78 pub(crate) timer_period: u16,
79 /// Current timer.
80 pub(crate) timer: u16,
81
82 // ----- IRQ -----
83 /// Latched IRQ flag (cleared by writing `$4015`).
84 pub irq_flag: bool,
85
86 /// Region.
87 region: Region,
88}
89
90impl Dmc {
91 /// Construct a new DMC channel.
92 #[must_use]
93 pub const fn new(region: Region) -> Self {
94 let cpu_period = match region {
95 Region::Pal => PAL_DMC_RATES[0],
96 _ => NTSC_DMC_RATES[0],
97 };
98 let timer_period = cpu_period / 2 - 1;
99 Self {
100 irq_enable: false,
101 loop_flag: false,
102 rate_index: 0,
103 sample_addr: 0xC000,
104 sample_length: 1,
105 current_addr: 0xC000,
106 bytes_remaining: 0,
107 sample_buffer: None,
108 shift_register: 0,
109 bits_remaining: 0,
110 dac: 0,
111 silence: true,
112 timer_period,
113 timer: 0,
114 irq_flag: false,
115 region,
116 }
117 }
118
119 /// `$4010` write: IRQ enable + loop + rate index.
120 ///
121 /// Note: the public NTSC/PAL rate tables are the period in CPU cycles
122 /// per output bit. The DMC timer ticks at the APU clock (= half CPU
123 /// rate), so the internal reload value is `cpu_period / 2 - 1`.
124 pub fn write_ctrl(&mut self, value: u8) {
125 self.irq_enable = (value & 0x80) != 0;
126 self.loop_flag = (value & 0x40) != 0;
127 self.rate_index = value & 0x0F;
128 let cpu_period = match self.region {
129 Region::Pal => PAL_DMC_RATES[self.rate_index as usize],
130 _ => NTSC_DMC_RATES[self.rate_index as usize],
131 };
132 // The DMC timer ticks at the APU clock (= half CPU rate), so the
133 // internal reload value is `cpu_period / 2 - 1`.
134 self.timer_period = (cpu_period / 2).saturating_sub(1);
135 if !self.irq_enable {
136 self.irq_flag = false;
137 }
138 }
139
140 /// `$4011` write: direct 7-bit DAC.
141 pub fn write_dac(&mut self, value: u8) {
142 self.dac = value & 0x7F;
143 }
144
145 /// `$4012` write: sample address.
146 pub fn write_sample_addr(&mut self, value: u8) {
147 self.sample_addr = 0xC000 | (u16::from(value) << 6);
148 }
149
150 /// `$4013` write: sample length.
151 pub fn write_sample_length(&mut self, value: u8) {
152 self.sample_length = (u16::from(value) << 4) | 1;
153 }
154
155 /// Status (`$4015` read bit 4): bytes-remaining > 0.
156 #[must_use]
157 pub const fn active(&self) -> bool {
158 self.bytes_remaining > 0
159 }
160
161 /// `$4015` write effect on DMC: bit 4 set restarts sample if not running;
162 /// bit 4 clear silences (sets bytes-remaining=0). Always clears `irq_flag`.
163 pub fn set_enabled(&mut self, enabled: bool) {
164 if enabled {
165 if self.bytes_remaining == 0 {
166 self.current_addr = self.sample_addr;
167 self.bytes_remaining = self.sample_length;
168 }
169 } else {
170 self.bytes_remaining = 0;
171 }
172 // `$4015` write clears DMC IRQ flag (per nesdev: any write to $4015).
173 self.irq_flag = false;
174 }
175
176 /// Returns `true` if the DMC needs a DMA fetch right now (sample buffer
177 /// empty and bytes remaining). Caller is responsible for halting the
178 /// CPU and supplying the byte via [`Self::deliver_sample`].
179 #[must_use]
180 pub const fn needs_dma(&self) -> bool {
181 self.sample_buffer.is_none() && self.bytes_remaining > 0
182 }
183
184 /// The address the DMA controller must read.
185 #[must_use]
186 pub const fn dma_addr(&self) -> u16 {
187 self.current_addr
188 }
189
190 /// Consume a fetched byte from the DMA controller.
191 pub fn deliver_sample(&mut self, byte: u8) {
192 self.sample_buffer = Some(byte);
193 // Advance memory reader: addr wraps from $FFFF -> $8000.
194 self.current_addr = match self.current_addr {
195 0xFFFF => 0x8000,
196 other => other.wrapping_add(1),
197 };
198 // A `$4015` bit-4 clear that races a DMA in flight can land between
199 // `needs_dma() == true` and `deliver_sample`. In that case
200 // `bytes_remaining` is already 0; accept the fetched byte into the
201 // buffer (the playback unit consumes it) but don't underflow the
202 // counter or re-trigger a DMA chain.
203 if self.bytes_remaining == 0 {
204 return;
205 }
206 self.bytes_remaining -= 1;
207 if self.bytes_remaining == 0 {
208 if self.loop_flag {
209 self.current_addr = self.sample_addr;
210 self.bytes_remaining = self.sample_length;
211 } else if self.irq_enable {
212 self.irq_flag = true;
213 }
214 }
215 }
216
217 /// One APU clock — drives the timer, output unit, and (on buffer empty)
218 /// loads the shift register from the sample buffer.
219 pub fn clock_timer(&mut self) {
220 if self.timer == 0 {
221 self.timer = self.timer_period;
222 self.clock_output();
223 } else {
224 self.timer -= 1;
225 }
226 }
227
228 fn clock_output(&mut self) {
229 if !self.silence {
230 // Bit 0 of shift register modifies DAC by ±2.
231 if (self.shift_register & 1) != 0 {
232 if self.dac <= 125 {
233 self.dac += 2;
234 }
235 } else if self.dac >= 2 {
236 self.dac -= 2;
237 }
238 }
239 self.shift_register >>= 1;
240 if self.bits_remaining > 0 {
241 self.bits_remaining -= 1;
242 }
243 if self.bits_remaining == 0 {
244 // Reload from sample buffer.
245 self.bits_remaining = 8;
246 if let Some(b) = self.sample_buffer.take() {
247 self.silence = false;
248 self.shift_register = b;
249 } else {
250 self.silence = true;
251 }
252 }
253 }
254
255 /// Per-cycle output (0..=127).
256 #[must_use]
257 pub const fn output(&self) -> u8 {
258 self.dac
259 }
260
261 /// v2.0 Phase 2 (`mc-r1-dmc-reenable-phase`): apply a one-time signed phase
262 /// shift to the byte-timer at the `$4015` re-enable boundary, realigning the
263 /// looping-reload chain by `delta` CPU cycles (the TriCNES re-enable model
264 /// resets `APU_ChannelTimer_DMC` to a phase RustyNES otherwise lands 1 cycle
265 /// off for the Implicit-DMA-Abort X=10/11 entries). `delta` is in the
266 /// timer's own (APU-rate) units; wraps within `[0, timer_period]`.
267 pub(crate) fn bump_timer_phase(&mut self, delta: i32) {
268 let p = i32::from(self.timer_period) + 1;
269 #[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
270 let t = (i32::from(self.timer) + delta).rem_euclid(p) as u16;
271 self.timer = t;
272 }
273
274 /// Diagnostic: current byte-timer countdown value. Used by the per-cycle
275 /// DMC-DMA cross-diff tracing (`crates/rustynes-test-harness/src/bin/
276 /// trace_dma_4015.rs`) to expose the internal byte-timer phase that the
277 /// abort-context reload arm depends on.
278 #[must_use]
279 pub const fn timer(&self) -> u16 {
280 self.timer
281 }
282
283 /// Diagnostic: bits remaining in the output shift register (0..=8).
284 #[must_use]
285 pub const fn bits_remaining(&self) -> u8 {
286 self.bits_remaining
287 }
288
289 /// Diagnostic: output-unit silence flag.
290 #[must_use]
291 pub const fn silence(&self) -> bool {
292 self.silence
293 }
294
295 /// Diagnostic: sample buffer occupied (a byte awaits transfer to the
296 /// shift register).
297 #[must_use]
298 pub const fn buffer_full(&self) -> bool {
299 self.sample_buffer.is_some()
300 }
301
302 /// v2.0 abort-context reload-arm phase fix (`mc-r1-dmc-abort-timer-phase`).
303 /// When the output unit is silent (the byte-timer boundary's `clock_output`
304 /// took an empty buffer this cycle) but a LOAD DMA has just filled the
305 /// buffer, consume that byte into the shift register and clear silence —
306 /// matching TriCNES, whose LOAD completes before the boundary so the boundary
307 /// consumes the load byte. Emptying the buffer here lets the per-cycle
308 /// reload-arm fire promptly (the reload that RustyNES otherwise deferred 4
309 /// cycles). Returns `true` if a byte was consumed.
310 pub(crate) fn consume_buffer_into_shifter_if_silent(&mut self) -> bool {
311 // Gate on the byte-timer having JUST reset this cycle (`timer ==
312 // timer_period`): that is the exact boundary-coincidence — `clock_output`
313 // wrapped the timer + reloaded bits this same cycle and took the empty
314 // buffer (silence) before the LOAD `deliver_sample` filled it. A LOAD
315 // that delivers mid-byte (timer below period — e.g. the Loop1/Loop2
316 // implicit-abort 1-byte loads) must NOT be re-consumed here; that would
317 // corrupt the implicit-abort 1-cycle-DMA measurement.
318 if self.silence
319 && self.timer == self.timer_period
320 && let Some(b) = self.sample_buffer.take()
321 {
322 self.shift_register = b;
323 self.silence = false;
324 return true;
325 }
326 false
327 }
328}
329
330#[cfg(test)]
331mod tests {
332 use super::*;
333
334 #[test]
335 fn write_dac_masks_high_bit() {
336 let mut d = Dmc::new(Region::Ntsc);
337 d.write_dac(0xFF);
338 assert_eq!(d.dac, 0x7F);
339 }
340
341 #[test]
342 fn enabling_starts_sample() {
343 let mut d = Dmc::new(Region::Ntsc);
344 d.write_sample_addr(0x10); // $C000 + 0x10*64 = $C400
345 d.write_sample_length(0x10); // 0x10*16 + 1 = 0x101
346 d.set_enabled(true);
347 assert_eq!(d.current_addr, 0xC400);
348 assert_eq!(d.bytes_remaining, 0x101);
349 }
350
351 #[test]
352 fn disabling_silences() {
353 let mut d = Dmc::new(Region::Ntsc);
354 d.write_sample_length(0x10);
355 d.set_enabled(true);
356 d.set_enabled(false);
357 assert_eq!(d.bytes_remaining, 0);
358 }
359
360 #[test]
361 fn deliver_sample_wraps_address() {
362 let mut d = Dmc::new(Region::Ntsc);
363 d.bytes_remaining = 2;
364 d.current_addr = 0xFFFF;
365 d.deliver_sample(0xAA);
366 assert_eq!(d.current_addr, 0x8000);
367 }
368
369 #[test]
370 fn end_of_sample_raises_irq_when_enabled() {
371 let mut d = Dmc::new(Region::Ntsc);
372 d.irq_enable = true;
373 d.bytes_remaining = 1;
374 d.deliver_sample(0xAA);
375 assert!(d.irq_flag);
376 }
377
378 #[test]
379 fn writing_4015_clears_irq_flag() {
380 let mut d = Dmc::new(Region::Ntsc);
381 d.irq_flag = true;
382 d.set_enabled(false);
383 assert!(!d.irq_flag);
384 }
385
386 #[test]
387 fn deliver_sample_after_disable_does_not_underflow() {
388 // Race: DMA scheduled (needs_dma == true), then `$4015` bit 4 cleared
389 // before the bus could service it. The in-flight byte arrives with
390 // bytes_remaining already 0 — must not underflow the u16.
391 let mut d = Dmc::new(Region::Ntsc);
392 d.bytes_remaining = 0;
393 d.deliver_sample(0xAA);
394 assert_eq!(d.bytes_remaining, 0);
395 assert_eq!(d.sample_buffer, Some(0xAA));
396 }
397}