Skip to main content

rustysnes_core/
dma.rs

1//! GP-DMA + HDMA — the 8-channel DMA controller (`$420B`/`$420C`, `$43n0-$43nA`).
2//!
3//! Clean-room port of the ares (ISC, vendor-ok) `sfc/cpu/dma.cpp` transfer model; never a
4//! verbatim copy. The DMA controller moves bytes between the A-bus (the 24-bit CPU address
5//! space) and the B-bus (the PPU/APU register window `$2100-$21FF`). Two flavors:
6//!
7//! - **GP-DMA** (`MDMAEN $420B`): writing a non-zero mask runs every selected channel to
8//!   completion **with the CPU fully halted**, `8` master clocks per byte (+ per-channel and
9//!   alignment overhead). Cannot cross a bank (`sourceAddress` wraps in-bank).
10//! - **HDMA** (`HDMAEN $420C`): per visible scanline, fires at H≈`$116`; each active channel
11//!   transfers its line entry (direct or indirect), `8` clocks/byte plus overhead, and HDMA
12//!   **preempts** an in-flight GP-DMA.
13//!
14//! The controller never touches the concrete `Bus` directly: it drives the [`DmaBus`] trait so
15//! it stays decoupled (and unit-testable in isolation). The master-clock cost is reported back
16//! to the caller (the scheduler advances the clock by it).
17
18// Byte-splitting 16-bit DMA registers into `u8` halves is the core of the controller; the
19// deliberate narrowing casts are allowed module-wide (mirrors the CPU/bus modules).
20#![allow(clippy::cast_possible_truncation, clippy::cast_lossless)]
21
22use rustysnes_savestate::{SaveReader, SaveStateError, SaveWriter};
23
24use crate::dma_bus::DmaBus;
25
26/// Per-mode B-bus register count (how many distinct B-bus addresses a transfer unit touches).
27/// ares `lengths[8] = {1, 2, 2, 4, 4, 4, 2, 4}`.
28const MODE_LENGTHS: [u8; 8] = [1, 2, 2, 4, 4, 4, 2, 4];
29
30/// One of the 8 DMA channels (`$43n0-$43nA`).
31#[derive(Debug, Clone, Copy)]
32pub struct Channel {
33    /// `$43n0` DMAP — transfer params: bit7 direction (0 = A→B, 1 = B→A), bit6 indirect (HDMA),
34    /// bit4 reverse (GP-DMA addr decrement), bit3 fixed (GP-DMA addr no-change), bits2-0 mode.
35    pub dmap: u8,
36    /// `$43n1` BBAD — B-bus target low byte (the `$21xx` register).
37    pub target: u8,
38    /// `$43n2-3` A1T — A-bus source address (GP) / table address (HDMA).
39    pub source_addr: u16,
40    /// `$43n4` A1B — A-bus source bank.
41    pub source_bank: u8,
42    /// `$43n5-6` DAS — GP byte count / HDMA indirect address.
43    pub count_or_indirect: u16,
44    /// `$43n7` DASB — HDMA indirect bank.
45    pub indirect_bank: u8,
46    /// `$43n8-9` A2A — HDMA table running address.
47    pub hdma_addr: u16,
48    /// `$43nA` NTRL — HDMA line counter (bit7 = repeat, bits0-6 = lines).
49    pub line_counter: u8,
50    /// The undocumented readable scratch latch at `$43xB`, mirrored at `$43xF`.
51    ///
52    /// Nothing in the DMA controller reads it — it is a byte of storage the hardware exposes and
53    /// then forgets about. It matters because it is CPU-visible: AccuracySNES `D1.10` writes it
54    /// and reads it back, snes9x passes that test, and RustySNES returned 0 from both addresses
55    /// until the test was written.
56    ///
57    /// **Deliberately not in the save state**, and **reset to 0 on load**. ares and bsnes do
58    /// serialise theirs, but adding a byte to the `DMA0` section changes its length, and this
59    /// format's compatibility rules make that a version-bump decision rather than a free one
60    /// (`docs/adr/0006`). The latch has no effect on emulation, so the only cost is that a
61    /// `$43xB` read after a load returns 0 instead of the saved byte. Resetting matters more than
62    /// the omission does: inheriting the pre-load value would make that read depend on what ran
63    /// before the load, which the determinism contract rules out. Recorded in
64    /// `docs/accuracysnes-plan.md` so it stays a decision rather than an oversight.
65    pub scratch: u8,
66    /// HDMA: this channel has finished its table for the frame.
67    pub hdma_completed: bool,
68    /// HDMA: perform a transfer on this line (vs. just decrement the counter).
69    pub hdma_do_transfer: bool,
70}
71
72impl Default for Channel {
73    fn default() -> Self {
74        Self {
75            dmap: 0xFF,
76            target: 0xFF,
77            source_addr: 0xFFFF,
78            source_bank: 0xFF,
79            count_or_indirect: 0xFFFF,
80            indirect_bank: 0xFF,
81            hdma_addr: 0xFFFF,
82            line_counter: 0xFF,
83            // $43xB powers on as $FF like every other channel register, not as zero. fullsnes'
84            // register table and the SNESdev DMA-registers page both give $FF, and ares and bsnes
85            // default their equivalent field (`unknown`) to $FF too. Found by AccuracySNES `D1.11`,
86            // which snes9x and Mesen2 both passed while this failed.
87            scratch: 0xFF,
88            hdma_completed: false,
89            hdma_do_transfer: false,
90        }
91    }
92}
93
94impl Channel {
95    const fn direction_b_to_a(self) -> bool {
96        self.dmap & 0x80 != 0
97    }
98    const fn indirect(self) -> bool {
99        self.dmap & 0x40 != 0
100    }
101    const fn reverse(self) -> bool {
102        self.dmap & 0x10 != 0
103    }
104    const fn fixed(self) -> bool {
105        self.dmap & 0x08 != 0
106    }
107    const fn mode(self) -> u8 {
108        self.dmap & 0x07
109    }
110
111    fn save_state(self, s: &mut SaveWriter) {
112        s.write_u8(self.dmap);
113        s.write_u8(self.target);
114        s.write_u16(self.source_addr);
115        s.write_u8(self.source_bank);
116        s.write_u16(self.count_or_indirect);
117        s.write_u8(self.indirect_bank);
118        s.write_u16(self.hdma_addr);
119        s.write_u8(self.line_counter);
120        s.write_bool(self.hdma_completed);
121        s.write_bool(self.hdma_do_transfer);
122    }
123
124    fn load_state(&mut self, s: &mut SaveReader) -> Result<(), SaveStateError> {
125        self.dmap = s.read_u8()?;
126        self.target = s.read_u8()?;
127        self.source_addr = s.read_u16()?;
128        self.source_bank = s.read_u8()?;
129        self.count_or_indirect = s.read_u16()?;
130        self.indirect_bank = s.read_u8()?;
131        self.hdma_addr = s.read_u16()?;
132        self.line_counter = s.read_u8()?;
133        self.hdma_completed = s.read_bool()?;
134        self.hdma_do_transfer = s.read_bool()?;
135        // Not in the blob, so reset rather than inherit. Leaving it would make a `$43xB` read
136        // after a load depend on what ran BEFORE the load, which is precisely the kind of
137        // pre-load leakage the determinism contract (`docs/adr/0004`) exists to rule out. If the
138        // latch is ever added to the format, delete this line with it.
139        self.scratch = 0;
140        Ok(())
141    }
142
143    /// The B-bus address for transfer-unit byte `index` (ares `Channel::transfer` switch).
144    const fn b_address(self, index: u8) -> u8 {
145        let bump = match self.mode() {
146            1 | 5 => index & 1,
147            3 | 7 => (index >> 1) & 1,
148            4 => index,
149            _ => 0, // modes 0, 2, 6
150        };
151        self.target.wrapping_add(bump)
152    }
153}
154
155/// The 8-channel DMA controller plus the `MDMAEN`/`HDMAEN` enables.
156#[derive(Debug, Clone, Default)]
157pub struct Dma {
158    /// The 8 channels.
159    pub channels: [Channel; 8],
160    /// `$420B` MDMAEN — GP-DMA enable mask (write triggers the run).
161    pub gp_enable: u8,
162    /// `$420C` HDMAEN — HDMA enable mask.
163    pub hdma_enable: u8,
164}
165
166/// Whether an A-bus-to-B-bus transfer performs no write.
167///
168/// One case, and it is an erratum rather than a rule: WRAM to `$2180` is a WRAM-to-WRAM transfer
169/// through the data port, and the hardware does not perform the write (fullsnes: "does not cause
170/// a write to occur"). The read still happens and the time is still spent. Implementing `$2180`
171/// as an ordinary port copies the bytes and looks right until a game relies on the no-op.
172///
173/// Shared by both transfer paths — GP-DMA has its own inline loop because it interleaves HDMA and
174/// accounts clocks per byte, so this cannot live in `transfer_unit` alone.
175const fn suppress_write_b(b_addr: u8, a_addr: u32) -> bool {
176    b_addr == 0x80 && is_wram_address(a_addr)
177}
178
179/// Whether a 24-bit A-bus address names WRAM — either bank `$7E`/`$7F` directly, or the low-WRAM
180/// mirror that banks `$00`-`$3F` and `$80`-`$BF` expose at `$0000`-`$1FFF`.
181///
182/// Used only by the `$2180` no-write rule, which is about the memory behind the address rather
183/// than about how it was spelled: a transfer sourced from the mirror is just as much WRAM-to-WRAM
184/// as one sourced from `$7E`.
185const fn is_wram_address(addr24: u32) -> bool {
186    let bank = (addr24 >> 16) & 0xFF;
187    let addr = addr24 & 0xFFFF;
188    matches!(bank, 0x7E | 0x7F) || (matches!(bank, 0x00..=0x3F | 0x80..=0xBF) && addr <= 0x1FFF)
189}
190
191impl Dma {
192    /// Construct a power-on DMA controller (all channels open, no transfers pending).
193    #[must_use]
194    pub fn new() -> Self {
195        Self::default()
196    }
197
198    /// Write a DMA channel register `$43nA` (or the `$420B/$420C` enables, handled by the bus).
199    /// `reg` is the low byte (`$00-$0A`); `ch` is the channel index `0-7`.
200    pub fn write_reg(&mut self, ch: usize, reg: u8, val: u8) {
201        let c = &mut self.channels[ch & 7];
202        match reg {
203            0x0 => c.dmap = val,
204            0x1 => c.target = val,
205            0x2 => c.source_addr = (c.source_addr & 0xFF00) | u16::from(val),
206            0x3 => c.source_addr = (c.source_addr & 0x00FF) | (u16::from(val) << 8),
207            0x4 => c.source_bank = val,
208            0x5 => c.count_or_indirect = (c.count_or_indirect & 0xFF00) | u16::from(val),
209            0x6 => c.count_or_indirect = (c.count_or_indirect & 0x00FF) | (u16::from(val) << 8),
210            0x7 => c.indirect_bank = val,
211            0x8 => c.hdma_addr = (c.hdma_addr & 0xFF00) | u16::from(val),
212            0x9 => c.hdma_addr = (c.hdma_addr & 0x00FF) | (u16::from(val) << 8),
213            0xA => c.line_counter = val,
214            // $43xB and $43xF are one latch seen at two addresses, per channel.
215            0xB | 0xF => c.scratch = val,
216            _ => {}
217        }
218    }
219
220    /// Write all 8 channels + the `MDMAEN`/`HDMAEN` enables into a `"DMA0"` section.
221    pub fn save_state(&self, w: &mut SaveWriter) {
222        w.section(*b"DMA0", |s| {
223            for &c in &self.channels {
224                c.save_state(s);
225            }
226            s.write_u8(self.gp_enable);
227            s.write_u8(self.hdma_enable);
228        });
229    }
230
231    /// The inverse of [`Self::save_state`].
232    ///
233    /// # Errors
234    /// [`SaveStateError`] on truncated/corrupt input or a section with unconsumed trailing
235    /// bytes.
236    pub fn load_state(&mut self, r: &mut SaveReader) -> Result<(), SaveStateError> {
237        let mut s = r.expect_section(*b"DMA0")?;
238        for c in &mut self.channels {
239            c.load_state(&mut s)?;
240        }
241        self.gp_enable = s.read_u8()?;
242        self.hdma_enable = s.read_u8()?;
243        if s.remaining() != 0 {
244            return Err(SaveStateError::Invalid(alloc::format!(
245                "DMA0 section has {} trailing byte(s)",
246                s.remaining()
247            )));
248        }
249        Ok(())
250    }
251
252    /// Read a DMA channel register `$43nA`.
253    #[must_use]
254    pub const fn read_reg(&self, ch: usize, reg: u8) -> u8 {
255        let c = &self.channels[ch & 7];
256        match reg {
257            0x0 => c.dmap,
258            0x1 => c.target,
259            0x2 => c.source_addr as u8,
260            0x3 => (c.source_addr >> 8) as u8,
261            0x4 => c.source_bank,
262            0x5 => c.count_or_indirect as u8,
263            0x6 => (c.count_or_indirect >> 8) as u8,
264            0x7 => c.indirect_bank,
265            0x8 => c.hdma_addr as u8,
266            0x9 => (c.hdma_addr >> 8) as u8,
267            0xA => c.line_counter,
268            0xB | 0xF => c.scratch,
269            _ => 0,
270        }
271    }
272
273    /// Run all GP-DMA channels selected by `mask` (`$420B` write) to completion. The CPU is
274    /// considered halted for the whole run; the returned value is the **master-clock cost**
275    /// (the scheduler advances the clock by it). Ported from ares `Channel::dmaRun`.
276    #[must_use]
277    pub fn run_gp(&mut self, mask: u8, bus: &mut impl DmaBus) -> u32 {
278        let mut cost: u32 = 0;
279        if mask == 0 {
280            return 0;
281        }
282        // Whole-transfer alignment overhead (ares charges 8 once before the run). Each `bus.step`
283        // advances the master clock *now* so the PPU scanline is current at every B-bus write —
284        // see `DmaBus::step`. The returned `cost` is the same total, retained for callers/tests;
285        // the concrete Bus advances via `step` and must NOT re-charge `cost` afterwards.
286        //
287        // While this transfer runs, the bus has lent us its controller, so its own per-tick HDMA
288        // path is dormant; we interleave HDMA at every scanline boundary the transfer crosses
289        // (hardware: HDMA preempts general DMA at each scanline start). `last_line` seeds from the
290        // bus's own HDMA bookkeeping so no line runs twice or is skipped.
291        let mut last_line = bus.hdma_last_line();
292        bus.step(8);
293        cost += 8;
294        cost += self.service_hdma_during_gp(&mut last_line, bus);
295        for ch in 0..8 {
296            if mask & (1 << ch) == 0 {
297                continue;
298            }
299            bus.step(8); // per-channel setup
300            cost += 8;
301            cost += self.service_hdma_during_gp(&mut last_line, bus);
302            let channel = self.channels[ch];
303            let mut src = channel.source_addr;
304            // `count == 0` means 0x10000 bytes (ares decrements then tests).
305            let mut remaining = channel.count_or_indirect;
306            let mut index: u8 = 0;
307            loop {
308                let a = (u32::from(channel.source_bank) << 16) | u32::from(src);
309                let b = channel.b_address(index);
310                // ares `Channel::transfer`: the access side steps 4 clocks, reads, steps 4 more,
311                // then the write side lands (no extra step) — 8 clocks/byte with the destination
312                // write occurring after the scanline has advanced.
313                bus.step(4);
314                if channel.direction_b_to_a() {
315                    let data = bus.read_b(b);
316                    bus.step(4);
317                    bus.write_a(a, data);
318                } else {
319                    let data = bus.read_a(a);
320                    bus.step(4);
321                    // The `$2180` no-write rule — see `transfer_unit`, which HDMA uses. The two
322                    // paths are separate on purpose (GP-DMA interleaves HDMA and accounts clocks
323                    // per byte), so a rule that belongs to the transfer itself has to be stated
324                    // in both. Fixing only `transfer_unit` left `D1.09` failing, which is how
325                    // this second site was found.
326                    if !suppress_write_b(b, a) {
327                        bus.write_b(b, data);
328                    }
329                }
330                cost += 8; // 8 master clocks per byte
331                // HDMA preempts at scanline starts — interleave it if this byte crossed a line.
332                cost += self.service_hdma_during_gp(&mut last_line, bus);
333                if !channel.fixed() {
334                    src = if channel.reverse() {
335                        src.wrapping_sub(1)
336                    } else {
337                        src.wrapping_add(1)
338                    };
339                }
340                index = index.wrapping_add(1);
341                remaining = remaining.wrapping_sub(1);
342                if remaining == 0 {
343                    break;
344                }
345            }
346            // Reflect the consumed source address back (hardware leaves it advanced).
347            self.channels[ch].source_addr = src;
348            self.channels[ch].count_or_indirect = 0;
349        }
350        // Clear the enable mask — GP-DMA is one-shot.
351        self.gp_enable = 0;
352        cost
353    }
354
355    /// Move one byte for one transfer unit between A-bus and B-bus (ares `Channel::transfer`,
356    /// minus the WRAM↔WRAM invalid case which the bus enforces via open-bus on `$2180`).
357    fn transfer_unit(channel: Channel, a_addr: u32, b_addr: u8, bus: &mut impl DmaBus) {
358        if channel.direction_b_to_a() {
359            let data = bus.read_b(b_addr);
360            bus.write_a(a_addr, data);
361        } else {
362            // WRAM -> $2180 is a WRAM-to-WRAM transfer through the data port, and the hardware
363            // performs NO WRITE at all — the read still happens and the time is still spent, but
364            // nothing lands (fullsnes: "does not cause a write to occur"). Implementing $2180 as
365            // an ordinary port copies the bytes and looks right until a game relies on the
366            // transfer being a no-op. AccuracySNES `D1.09` asserts it; snes9x passes, and
367            // RustySNES did not until this check existed.
368            let data = bus.read_a(a_addr);
369            if !suppress_write_b(b_addr, a_addr) {
370                bus.write_b(b_addr, data);
371            }
372        }
373    }
374
375    // ---- HDMA -------------------------------------------------------------------------------
376
377    /// Service one scanline's HDMA lifecycle: at V=0 reset the bookkeeping and load the tables,
378    /// on each visible line (`1..=vh`) run one transfer, otherwise nothing. Returns the
379    /// master-clock cost. Shared by the per-master-tick path (`Bus::advance_master`) and the
380    /// in-GP-DMA interleave (`Dma::run_gp` via `Self::service_hdma_during_gp`) so HDMA stays
381    /// scanline-accurate even while a GP-DMA is advancing the clock across line boundaries.
382    #[must_use]
383    pub fn service_hdma_line(&mut self, line: u16, vh: u16, bus: &mut impl DmaBus) -> u32 {
384        // ares `timing.cpp`: `hdmaReset()` runs at frame start regardless of HDMAEN; only the
385        // subsequent `hdmaSetup()` is gated on any channel being enabled. Resetting
386        // unconditionally clears `hdma_completed` so a channel finished last frame can go active
387        // again if HDMAEN enables it mid-frame (`hdma_setup` itself no-ops when HDMAEN==0).
388        if line == 0 {
389            self.hdma_reset();
390            return self.hdma_setup(bus);
391        }
392        if self.hdma_enable == 0 {
393            return 0;
394        }
395        if line <= vh { self.hdma_run(bus) } else { 0 }
396    }
397
398    /// Interleave HDMA into a running GP-DMA: while the bus took our controller (so its own
399    /// per-tick HDMA path is dormant), fire HDMA at each scanline boundary the GP-DMA crosses,
400    /// mirroring how HDMA preempts general DMA at the start of every scanline on hardware.
401    /// `last_line` carries the last-serviced scanline across byte iterations; the bus's own
402    /// bookkeeping is synced so `Bus::advance_master` resumes cleanly afterward. Returns the
403    /// master-clock cost of any transfer performed (already stepped onto `bus`).
404    fn service_hdma_during_gp(&mut self, last_line: &mut u16, bus: &mut impl DmaBus) -> u32 {
405        if self.hdma_enable == 0 {
406            return 0;
407        }
408        let line = bus.scanline();
409        if line == *last_line {
410            return 0;
411        }
412        *last_line = line;
413        bus.set_hdma_last_line(line);
414        let vh = bus.visible_height();
415        let cost = self.service_hdma_line(line, vh, bus);
416        if cost > 0 {
417            bus.step(cost);
418        }
419        cost
420    }
421
422    /// Reset every channel's HDMA bookkeeping at the start of a frame (V=0). ares `hdmaReset`.
423    pub fn hdma_reset(&mut self) {
424        for c in &mut self.channels {
425            c.hdma_completed = false;
426            c.hdma_do_transfer = false;
427        }
428    }
429
430    /// Per-frame HDMA setup: load each enabled channel's table pointer + first line entry.
431    /// Returns the master-clock cost. ares `hdmaSetup` + `Channel::hdmaSetup/hdmaReload`.
432    #[must_use]
433    pub fn hdma_setup(&mut self, bus: &mut impl DmaBus) -> u32 {
434        if self.hdma_enable == 0 {
435            return 0;
436        }
437        let mut cost: u32 = 8;
438        for ch in 0..8 {
439            // ares `Channel::hdmaSetup`: `hdmaDoTransfer = true` for EVERY channel, then the
440            // early-out for disabled ones. A channel disabled at frame start keeps its stale
441            // address/line_counter; if HDMAEN enables it mid-frame it resumes transferring from
442            // there (the "HDMAEN latch" quirk). Skipping the flag here would wrongly leave a
443            // mid-frame-enabled channel dormant for the rest of the frame.
444            self.channels[ch].hdma_do_transfer = true;
445            if self.hdma_enable & (1 << ch) == 0 {
446                continue;
447            }
448            self.channels[ch].hdma_addr = self.channels[ch].source_addr;
449            self.channels[ch].line_counter = 0;
450            cost += self.hdma_reload(ch, bus);
451        }
452        cost
453    }
454
455    /// Reload a channel's line counter / indirect pointer when the counter reaches 0 (ares
456    /// `Channel::hdmaReload`). Returns the master-clock cost of the table reads.
457    fn hdma_reload(&mut self, ch: usize, bus: &mut impl DmaBus) -> u32 {
458        let mut cost = 0;
459        let bank = self.channels[ch].source_bank;
460        let mut addr = self.channels[ch].hdma_addr;
461
462        // The line counter's low 7 bits reaching 0 means "reload" (bit7 is the repeat flag).
463        if self.channels[ch].line_counter.trailing_zeros() >= 7 {
464            let data = bus.read_a((u32::from(bank) << 16) | u32::from(addr));
465            cost += 8;
466            self.channels[ch].line_counter = data;
467            addr = addr.wrapping_add(1);
468
469            let completed = self.channels[ch].line_counter == 0;
470            self.channels[ch].hdma_completed = completed;
471            self.channels[ch].hdma_do_transfer = !completed;
472
473            if self.channels[ch].indirect() {
474                let lo = bus.read_a((u32::from(bank) << 16) | u32::from(addr));
475                cost += 8;
476                addr = addr.wrapping_add(1);
477                // A finished table whose final entry is the indirect low byte stops here (ares
478                // skips the high-byte fetch); otherwise read the high byte and combine.
479                let indirect = if completed && self.hdma_finished(ch) {
480                    u16::from(lo)
481                } else {
482                    let hi = bus.read_a((u32::from(bank) << 16) | u32::from(addr));
483                    cost += 8;
484                    addr = addr.wrapping_add(1);
485                    (u16::from(hi) << 8) | u16::from(lo)
486                };
487                self.channels[ch].count_or_indirect = indirect;
488            }
489        }
490        self.channels[ch].hdma_addr = addr;
491        cost
492    }
493
494    /// Whether every channel after `ch` has finished (ares `Channel::hdmaFinished`).
495    fn hdma_finished(&self, ch: usize) -> bool {
496        ((ch + 1)..8).all(|i| self.hdma_enable & (1 << i) == 0 || self.channels[i].hdma_completed)
497    }
498
499    const fn hdma_active(&self, ch: usize) -> bool {
500        self.hdma_enable & (1 << ch) != 0 && !self.channels[ch].hdma_completed
501    }
502
503    /// Run one visible-scanline's HDMA for all active channels (ares `hdmaRun` →
504    /// `hdmaTransfer` + `hdmaAdvance`). Returns the master-clock cost (the per-line budget).
505    #[must_use]
506    pub fn hdma_run(&mut self, bus: &mut impl DmaBus) -> u32 {
507        if self.hdma_enable == 0 {
508            return 0;
509        }
510        let mut cost: u32 = 8; // per-line overhead
511        // Transfer pass.
512        for ch in 0..8 {
513            if !self.hdma_active(ch) || !self.channels[ch].hdma_do_transfer {
514                continue;
515            }
516            let channel = self.channels[ch];
517            let len = MODE_LENGTHS[channel.mode() as usize];
518            let indirect = channel.indirect();
519            for index in 0..len {
520                // Indirect channels stream from `indirectBank:count_or_indirect`; direct channels
521                // stream from `sourceBank:hdma_addr`. Each byte advances the running pointer.
522                let ptr = if indirect {
523                    self.channels[ch].count_or_indirect
524                } else {
525                    self.channels[ch].hdma_addr
526                };
527                let a_addr = (u32::from(if indirect {
528                    channel.indirect_bank
529                } else {
530                    channel.source_bank
531                }) << 16)
532                    | u32::from(ptr);
533                let b = channel.b_address(index);
534                Self::transfer_unit(channel, a_addr, b, bus);
535                cost += 8;
536                let next = ptr.wrapping_add(1);
537                if indirect {
538                    self.channels[ch].count_or_indirect = next;
539                } else {
540                    self.channels[ch].hdma_addr = next;
541                }
542            }
543        }
544        // Advance pass: decrement counters + reload at zero.
545        for ch in 0..8 {
546            if !self.hdma_active(ch) {
547                continue;
548            }
549            self.channels[ch].line_counter = self.channels[ch].line_counter.wrapping_sub(1);
550            self.channels[ch].hdma_do_transfer = self.channels[ch].line_counter & 0x80 != 0;
551            cost += self.hdma_reload(ch, bus);
552        }
553        cost
554    }
555}
556
557#[cfg(test)]
558mod tests {
559    use super::*;
560    use alloc::vec;
561    use alloc::vec::Vec;
562
563    /// A tiny A-bus (64 KiB flat) + B-bus ($21xx) recorder for testing transfers.
564    struct TestBus {
565        a: Vec<u8>,
566        b: [u8; 256],
567    }
568    impl DmaBus for TestBus {
569        fn read_a(&mut self, addr: u32) -> u8 {
570            *self.a.get((addr & 0xFFFF) as usize).unwrap_or(&0)
571        }
572        fn write_a(&mut self, addr: u32, val: u8) {
573            let i = (addr & 0xFFFF) as usize;
574            if i < self.a.len() {
575                self.a[i] = val;
576            }
577        }
578        fn read_b(&mut self, addr: u8) -> u8 {
579            self.b[addr as usize]
580        }
581        fn write_b(&mut self, addr: u8, val: u8) {
582            self.b[addr as usize] = val;
583        }
584    }
585
586    #[test]
587    fn gp_dma_mode0_copies_block_to_b_bus() {
588        let mut bus = TestBus {
589            a: vec![0; 0x10000],
590            b: [0; 256],
591        };
592        for i in 0..4u32 {
593            bus.a[(0x1000 + i) as usize] = (0xA0 + i) as u8;
594        }
595        let mut dma = Dma::new();
596        // channel 0: mode 0 (single reg), A→B, source $00:1000, target $18 (VMDATA), 4 bytes.
597        dma.write_reg(0, 0x0, 0x00); // DMAP: A→B, mode 0
598        dma.write_reg(0, 0x1, 0x18); // BBAD
599        dma.write_reg(0, 0x2, 0x00); // A1TL
600        dma.write_reg(0, 0x3, 0x10); // A1TH -> $1000
601        dma.write_reg(0, 0x4, 0x00); // A1B
602        dma.write_reg(0, 0x5, 0x04); // DASL = 4
603        dma.write_reg(0, 0x6, 0x00); // DASH
604        let cost = dma.run_gp(0x01, &mut bus);
605        // Mode 0 hammers a single B address, so the last byte wins.
606        assert_eq!(bus.b[0x18], 0xA3);
607        assert!(cost >= 8 + 8 + 4 * 8); // alignment + channel + 4 bytes
608    }
609
610    #[test]
611    fn gp_dma_mode1_alternates_two_b_regs() {
612        let mut bus = TestBus {
613            a: vec![0; 0x10000],
614            b: [0; 256],
615        };
616        for i in 0..4u32 {
617            bus.a[(0x2000 + i) as usize] = (0x10 + i) as u8;
618        }
619        let mut dma = Dma::new();
620        dma.write_reg(0, 0x0, 0x01); // mode 1 (2 regs)
621        dma.write_reg(0, 0x1, 0x18); // BBAD base
622        dma.write_reg(0, 0x2, 0x00);
623        dma.write_reg(0, 0x3, 0x20); // $2000
624        dma.write_reg(0, 0x4, 0x00);
625        dma.write_reg(0, 0x5, 0x04);
626        dma.write_reg(0, 0x6, 0x00);
627        let _ = dma.run_gp(0x01, &mut bus);
628        // even bytes -> $18, odd bytes -> $19; last even=0x12, last odd=0x13.
629        assert_eq!(bus.b[0x18], 0x12);
630        assert_eq!(bus.b[0x19], 0x13);
631    }
632
633    #[test]
634    fn gp_dma_enable_is_one_shot() {
635        let mut bus = TestBus {
636            a: vec![0; 0x10000],
637            b: [0; 256],
638        };
639        let mut dma = Dma::new();
640        dma.write_reg(0, 0x5, 0x01);
641        dma.gp_enable = 0x01;
642        let _ = dma.run_gp(0x01, &mut bus);
643        assert_eq!(dma.gp_enable, 0);
644    }
645
646    #[test]
647    fn all_channels_round_trip_through_save_state() {
648        let mut dma = Dma::new();
649        dma.write_reg(3, 0x0, 0x81);
650        dma.write_reg(3, 0x1, 0x18);
651        dma.gp_enable = 0x08;
652        dma.hdma_enable = 0x01;
653
654        let mut w = SaveWriter::new();
655        dma.save_state(&mut w);
656        let bytes = w.into_bytes();
657
658        let mut fresh = Dma::new();
659        let mut r = SaveReader::new(&bytes);
660        fresh.load_state(&mut r).unwrap();
661
662        assert_eq!(fresh.channels[3].dmap, 0x81);
663        assert_eq!(fresh.channels[3].target, 0x18);
664        assert_eq!(fresh.gp_enable, 0x08);
665        assert_eq!(fresh.hdma_enable, 0x01);
666        assert_eq!(r.remaining(), 0);
667    }
668}