Skip to main content

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}