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}