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    /// HDMA: this channel has finished its table for the frame.
51    pub hdma_completed: bool,
52    /// HDMA: perform a transfer on this line (vs. just decrement the counter).
53    pub hdma_do_transfer: bool,
54}
55
56impl Default for Channel {
57    fn default() -> Self {
58        Self {
59            dmap: 0xFF,
60            target: 0xFF,
61            source_addr: 0xFFFF,
62            source_bank: 0xFF,
63            count_or_indirect: 0xFFFF,
64            indirect_bank: 0xFF,
65            hdma_addr: 0xFFFF,
66            line_counter: 0xFF,
67            hdma_completed: false,
68            hdma_do_transfer: false,
69        }
70    }
71}
72
73impl Channel {
74    const fn direction_b_to_a(self) -> bool {
75        self.dmap & 0x80 != 0
76    }
77    const fn indirect(self) -> bool {
78        self.dmap & 0x40 != 0
79    }
80    const fn reverse(self) -> bool {
81        self.dmap & 0x10 != 0
82    }
83    const fn fixed(self) -> bool {
84        self.dmap & 0x08 != 0
85    }
86    const fn mode(self) -> u8 {
87        self.dmap & 0x07
88    }
89
90    fn save_state(self, s: &mut SaveWriter) {
91        s.write_u8(self.dmap);
92        s.write_u8(self.target);
93        s.write_u16(self.source_addr);
94        s.write_u8(self.source_bank);
95        s.write_u16(self.count_or_indirect);
96        s.write_u8(self.indirect_bank);
97        s.write_u16(self.hdma_addr);
98        s.write_u8(self.line_counter);
99        s.write_bool(self.hdma_completed);
100        s.write_bool(self.hdma_do_transfer);
101    }
102
103    fn load_state(&mut self, s: &mut SaveReader) -> Result<(), SaveStateError> {
104        self.dmap = s.read_u8()?;
105        self.target = s.read_u8()?;
106        self.source_addr = s.read_u16()?;
107        self.source_bank = s.read_u8()?;
108        self.count_or_indirect = s.read_u16()?;
109        self.indirect_bank = s.read_u8()?;
110        self.hdma_addr = s.read_u16()?;
111        self.line_counter = s.read_u8()?;
112        self.hdma_completed = s.read_bool()?;
113        self.hdma_do_transfer = s.read_bool()?;
114        Ok(())
115    }
116
117    /// The B-bus address for transfer-unit byte `index` (ares `Channel::transfer` switch).
118    const fn b_address(self, index: u8) -> u8 {
119        let bump = match self.mode() {
120            1 | 5 => index & 1,
121            3 | 7 => (index >> 1) & 1,
122            4 => index,
123            _ => 0, // modes 0, 2, 6
124        };
125        self.target.wrapping_add(bump)
126    }
127}
128
129/// The 8-channel DMA controller plus the `MDMAEN`/`HDMAEN` enables.
130#[derive(Debug, Clone, Default)]
131pub struct Dma {
132    /// The 8 channels.
133    pub channels: [Channel; 8],
134    /// `$420B` MDMAEN — GP-DMA enable mask (write triggers the run).
135    pub gp_enable: u8,
136    /// `$420C` HDMAEN — HDMA enable mask.
137    pub hdma_enable: u8,
138}
139
140impl Dma {
141    /// Construct a power-on DMA controller (all channels open, no transfers pending).
142    #[must_use]
143    pub fn new() -> Self {
144        Self::default()
145    }
146
147    /// Write a DMA channel register `$43nA` (or the `$420B/$420C` enables, handled by the bus).
148    /// `reg` is the low byte (`$00-$0A`); `ch` is the channel index `0-7`.
149    pub fn write_reg(&mut self, ch: usize, reg: u8, val: u8) {
150        let c = &mut self.channels[ch & 7];
151        match reg {
152            0x0 => c.dmap = val,
153            0x1 => c.target = val,
154            0x2 => c.source_addr = (c.source_addr & 0xFF00) | u16::from(val),
155            0x3 => c.source_addr = (c.source_addr & 0x00FF) | (u16::from(val) << 8),
156            0x4 => c.source_bank = val,
157            0x5 => c.count_or_indirect = (c.count_or_indirect & 0xFF00) | u16::from(val),
158            0x6 => c.count_or_indirect = (c.count_or_indirect & 0x00FF) | (u16::from(val) << 8),
159            0x7 => c.indirect_bank = val,
160            0x8 => c.hdma_addr = (c.hdma_addr & 0xFF00) | u16::from(val),
161            0x9 => c.hdma_addr = (c.hdma_addr & 0x00FF) | (u16::from(val) << 8),
162            0xA => c.line_counter = val,
163            _ => {}
164        }
165    }
166
167    /// Write all 8 channels + the `MDMAEN`/`HDMAEN` enables into a `"DMA0"` section.
168    pub fn save_state(&self, w: &mut SaveWriter) {
169        w.section(*b"DMA0", |s| {
170            for &c in &self.channels {
171                c.save_state(s);
172            }
173            s.write_u8(self.gp_enable);
174            s.write_u8(self.hdma_enable);
175        });
176    }
177
178    /// The inverse of [`Self::save_state`].
179    ///
180    /// # Errors
181    /// [`SaveStateError`] on truncated/corrupt input or a section with unconsumed trailing
182    /// bytes.
183    pub fn load_state(&mut self, r: &mut SaveReader) -> Result<(), SaveStateError> {
184        let mut s = r.expect_section(*b"DMA0")?;
185        for c in &mut self.channels {
186            c.load_state(&mut s)?;
187        }
188        self.gp_enable = s.read_u8()?;
189        self.hdma_enable = s.read_u8()?;
190        if s.remaining() != 0 {
191            return Err(SaveStateError::Invalid(alloc::format!(
192                "DMA0 section has {} trailing byte(s)",
193                s.remaining()
194            )));
195        }
196        Ok(())
197    }
198
199    /// Read a DMA channel register `$43nA`.
200    #[must_use]
201    pub const fn read_reg(&self, ch: usize, reg: u8) -> u8 {
202        let c = &self.channels[ch & 7];
203        match reg {
204            0x0 => c.dmap,
205            0x1 => c.target,
206            0x2 => c.source_addr as u8,
207            0x3 => (c.source_addr >> 8) as u8,
208            0x4 => c.source_bank,
209            0x5 => c.count_or_indirect as u8,
210            0x6 => (c.count_or_indirect >> 8) as u8,
211            0x7 => c.indirect_bank,
212            0x8 => c.hdma_addr as u8,
213            0x9 => (c.hdma_addr >> 8) as u8,
214            0xA => c.line_counter,
215            _ => 0,
216        }
217    }
218
219    /// Run all GP-DMA channels selected by `mask` (`$420B` write) to completion. The CPU is
220    /// considered halted for the whole run; the returned value is the **master-clock cost**
221    /// (the scheduler advances the clock by it). Ported from ares `Channel::dmaRun`.
222    #[must_use]
223    pub fn run_gp(&mut self, mask: u8, bus: &mut impl DmaBus) -> u32 {
224        let mut cost: u32 = 0;
225        if mask == 0 {
226            return 0;
227        }
228        // Whole-transfer alignment overhead (ares charges 8 once before the run). Each `bus.step`
229        // advances the master clock *now* so the PPU scanline is current at every B-bus write —
230        // see `DmaBus::step`. The returned `cost` is the same total, retained for callers/tests;
231        // the concrete Bus advances via `step` and must NOT re-charge `cost` afterwards.
232        //
233        // While this transfer runs, the bus has lent us its controller, so its own per-tick HDMA
234        // path is dormant; we interleave HDMA at every scanline boundary the transfer crosses
235        // (hardware: HDMA preempts general DMA at each scanline start). `last_line` seeds from the
236        // bus's own HDMA bookkeeping so no line runs twice or is skipped.
237        let mut last_line = bus.hdma_last_line();
238        bus.step(8);
239        cost += 8;
240        cost += self.service_hdma_during_gp(&mut last_line, bus);
241        for ch in 0..8 {
242            if mask & (1 << ch) == 0 {
243                continue;
244            }
245            bus.step(8); // per-channel setup
246            cost += 8;
247            cost += self.service_hdma_during_gp(&mut last_line, bus);
248            let channel = self.channels[ch];
249            let mut src = channel.source_addr;
250            // `count == 0` means 0x10000 bytes (ares decrements then tests).
251            let mut remaining = channel.count_or_indirect;
252            let mut index: u8 = 0;
253            loop {
254                let a = (u32::from(channel.source_bank) << 16) | u32::from(src);
255                let b = channel.b_address(index);
256                // ares `Channel::transfer`: the access side steps 4 clocks, reads, steps 4 more,
257                // then the write side lands (no extra step) — 8 clocks/byte with the destination
258                // write occurring after the scanline has advanced.
259                bus.step(4);
260                if channel.direction_b_to_a() {
261                    let data = bus.read_b(b);
262                    bus.step(4);
263                    bus.write_a(a, data);
264                } else {
265                    let data = bus.read_a(a);
266                    bus.step(4);
267                    bus.write_b(b, data);
268                }
269                cost += 8; // 8 master clocks per byte
270                // HDMA preempts at scanline starts — interleave it if this byte crossed a line.
271                cost += self.service_hdma_during_gp(&mut last_line, bus);
272                if !channel.fixed() {
273                    src = if channel.reverse() {
274                        src.wrapping_sub(1)
275                    } else {
276                        src.wrapping_add(1)
277                    };
278                }
279                index = index.wrapping_add(1);
280                remaining = remaining.wrapping_sub(1);
281                if remaining == 0 {
282                    break;
283                }
284            }
285            // Reflect the consumed source address back (hardware leaves it advanced).
286            self.channels[ch].source_addr = src;
287            self.channels[ch].count_or_indirect = 0;
288        }
289        // Clear the enable mask — GP-DMA is one-shot.
290        self.gp_enable = 0;
291        cost
292    }
293
294    /// Move one byte for one transfer unit between A-bus and B-bus (ares `Channel::transfer`,
295    /// minus the WRAM↔WRAM invalid case which the bus enforces via open-bus on `$2180`).
296    fn transfer_unit(channel: Channel, a_addr: u32, b_addr: u8, bus: &mut impl DmaBus) {
297        if channel.direction_b_to_a() {
298            let data = bus.read_b(b_addr);
299            bus.write_a(a_addr, data);
300        } else {
301            let data = bus.read_a(a_addr);
302            bus.write_b(b_addr, data);
303        }
304    }
305
306    // ---- HDMA -------------------------------------------------------------------------------
307
308    /// Service one scanline's HDMA lifecycle: at V=0 reset the bookkeeping and load the tables,
309    /// on each visible line (`1..=vh`) run one transfer, otherwise nothing. Returns the
310    /// master-clock cost. Shared by the per-master-tick path (`Bus::advance_master`) and the
311    /// in-GP-DMA interleave (`Dma::run_gp` via `Self::service_hdma_during_gp`) so HDMA stays
312    /// scanline-accurate even while a GP-DMA is advancing the clock across line boundaries.
313    #[must_use]
314    pub fn service_hdma_line(&mut self, line: u16, vh: u16, bus: &mut impl DmaBus) -> u32 {
315        // ares `timing.cpp`: `hdmaReset()` runs at frame start regardless of HDMAEN; only the
316        // subsequent `hdmaSetup()` is gated on any channel being enabled. Resetting
317        // unconditionally clears `hdma_completed` so a channel finished last frame can go active
318        // again if HDMAEN enables it mid-frame (`hdma_setup` itself no-ops when HDMAEN==0).
319        if line == 0 {
320            self.hdma_reset();
321            return self.hdma_setup(bus);
322        }
323        if self.hdma_enable == 0 {
324            return 0;
325        }
326        if line <= vh { self.hdma_run(bus) } else { 0 }
327    }
328
329    /// Interleave HDMA into a running GP-DMA: while the bus took our controller (so its own
330    /// per-tick HDMA path is dormant), fire HDMA at each scanline boundary the GP-DMA crosses,
331    /// mirroring how HDMA preempts general DMA at the start of every scanline on hardware.
332    /// `last_line` carries the last-serviced scanline across byte iterations; the bus's own
333    /// bookkeeping is synced so `Bus::advance_master` resumes cleanly afterward. Returns the
334    /// master-clock cost of any transfer performed (already stepped onto `bus`).
335    fn service_hdma_during_gp(&mut self, last_line: &mut u16, bus: &mut impl DmaBus) -> u32 {
336        if self.hdma_enable == 0 {
337            return 0;
338        }
339        let line = bus.scanline();
340        if line == *last_line {
341            return 0;
342        }
343        *last_line = line;
344        bus.set_hdma_last_line(line);
345        let vh = bus.visible_height();
346        let cost = self.service_hdma_line(line, vh, bus);
347        if cost > 0 {
348            bus.step(cost);
349        }
350        cost
351    }
352
353    /// Reset every channel's HDMA bookkeeping at the start of a frame (V=0). ares `hdmaReset`.
354    pub fn hdma_reset(&mut self) {
355        for c in &mut self.channels {
356            c.hdma_completed = false;
357            c.hdma_do_transfer = false;
358        }
359    }
360
361    /// Per-frame HDMA setup: load each enabled channel's table pointer + first line entry.
362    /// Returns the master-clock cost. ares `hdmaSetup` + `Channel::hdmaSetup/hdmaReload`.
363    #[must_use]
364    pub fn hdma_setup(&mut self, bus: &mut impl DmaBus) -> u32 {
365        if self.hdma_enable == 0 {
366            return 0;
367        }
368        let mut cost: u32 = 8;
369        for ch in 0..8 {
370            // ares `Channel::hdmaSetup`: `hdmaDoTransfer = true` for EVERY channel, then the
371            // early-out for disabled ones. A channel disabled at frame start keeps its stale
372            // address/line_counter; if HDMAEN enables it mid-frame it resumes transferring from
373            // there (the "HDMAEN latch" quirk). Skipping the flag here would wrongly leave a
374            // mid-frame-enabled channel dormant for the rest of the frame.
375            self.channels[ch].hdma_do_transfer = true;
376            if self.hdma_enable & (1 << ch) == 0 {
377                continue;
378            }
379            self.channels[ch].hdma_addr = self.channels[ch].source_addr;
380            self.channels[ch].line_counter = 0;
381            cost += self.hdma_reload(ch, bus);
382        }
383        cost
384    }
385
386    /// Reload a channel's line counter / indirect pointer when the counter reaches 0 (ares
387    /// `Channel::hdmaReload`). Returns the master-clock cost of the table reads.
388    fn hdma_reload(&mut self, ch: usize, bus: &mut impl DmaBus) -> u32 {
389        let mut cost = 0;
390        let bank = self.channels[ch].source_bank;
391        let mut addr = self.channels[ch].hdma_addr;
392
393        // The line counter's low 7 bits reaching 0 means "reload" (bit7 is the repeat flag).
394        if self.channels[ch].line_counter.trailing_zeros() >= 7 {
395            let data = bus.read_a((u32::from(bank) << 16) | u32::from(addr));
396            cost += 8;
397            self.channels[ch].line_counter = data;
398            addr = addr.wrapping_add(1);
399
400            let completed = self.channels[ch].line_counter == 0;
401            self.channels[ch].hdma_completed = completed;
402            self.channels[ch].hdma_do_transfer = !completed;
403
404            if self.channels[ch].indirect() {
405                let lo = bus.read_a((u32::from(bank) << 16) | u32::from(addr));
406                cost += 8;
407                addr = addr.wrapping_add(1);
408                // A finished table whose final entry is the indirect low byte stops here (ares
409                // skips the high-byte fetch); otherwise read the high byte and combine.
410                let indirect = if completed && self.hdma_finished(ch) {
411                    u16::from(lo)
412                } else {
413                    let hi = bus.read_a((u32::from(bank) << 16) | u32::from(addr));
414                    cost += 8;
415                    addr = addr.wrapping_add(1);
416                    (u16::from(hi) << 8) | u16::from(lo)
417                };
418                self.channels[ch].count_or_indirect = indirect;
419            }
420        }
421        self.channels[ch].hdma_addr = addr;
422        cost
423    }
424
425    /// Whether every channel after `ch` has finished (ares `Channel::hdmaFinished`).
426    fn hdma_finished(&self, ch: usize) -> bool {
427        ((ch + 1)..8).all(|i| self.hdma_enable & (1 << i) == 0 || self.channels[i].hdma_completed)
428    }
429
430    const fn hdma_active(&self, ch: usize) -> bool {
431        self.hdma_enable & (1 << ch) != 0 && !self.channels[ch].hdma_completed
432    }
433
434    /// Run one visible-scanline's HDMA for all active channels (ares `hdmaRun` →
435    /// `hdmaTransfer` + `hdmaAdvance`). Returns the master-clock cost (the per-line budget).
436    #[must_use]
437    pub fn hdma_run(&mut self, bus: &mut impl DmaBus) -> u32 {
438        if self.hdma_enable == 0 {
439            return 0;
440        }
441        let mut cost: u32 = 8; // per-line overhead
442        // Transfer pass.
443        for ch in 0..8 {
444            if !self.hdma_active(ch) || !self.channels[ch].hdma_do_transfer {
445                continue;
446            }
447            let channel = self.channels[ch];
448            let len = MODE_LENGTHS[channel.mode() as usize];
449            let indirect = channel.indirect();
450            for index in 0..len {
451                // Indirect channels stream from `indirectBank:count_or_indirect`; direct channels
452                // stream from `sourceBank:hdma_addr`. Each byte advances the running pointer.
453                let ptr = if indirect {
454                    self.channels[ch].count_or_indirect
455                } else {
456                    self.channels[ch].hdma_addr
457                };
458                let a_addr = (u32::from(if indirect {
459                    channel.indirect_bank
460                } else {
461                    channel.source_bank
462                }) << 16)
463                    | u32::from(ptr);
464                let b = channel.b_address(index);
465                Self::transfer_unit(channel, a_addr, b, bus);
466                cost += 8;
467                let next = ptr.wrapping_add(1);
468                if indirect {
469                    self.channels[ch].count_or_indirect = next;
470                } else {
471                    self.channels[ch].hdma_addr = next;
472                }
473            }
474        }
475        // Advance pass: decrement counters + reload at zero.
476        for ch in 0..8 {
477            if !self.hdma_active(ch) {
478                continue;
479            }
480            self.channels[ch].line_counter = self.channels[ch].line_counter.wrapping_sub(1);
481            self.channels[ch].hdma_do_transfer = self.channels[ch].line_counter & 0x80 != 0;
482            cost += self.hdma_reload(ch, bus);
483        }
484        cost
485    }
486}
487
488#[cfg(test)]
489mod tests {
490    use super::*;
491    use alloc::vec;
492    use alloc::vec::Vec;
493
494    /// A tiny A-bus (64 KiB flat) + B-bus ($21xx) recorder for testing transfers.
495    struct TestBus {
496        a: Vec<u8>,
497        b: [u8; 256],
498    }
499    impl DmaBus for TestBus {
500        fn read_a(&mut self, addr: u32) -> u8 {
501            *self.a.get((addr & 0xFFFF) as usize).unwrap_or(&0)
502        }
503        fn write_a(&mut self, addr: u32, val: u8) {
504            let i = (addr & 0xFFFF) as usize;
505            if i < self.a.len() {
506                self.a[i] = val;
507            }
508        }
509        fn read_b(&mut self, addr: u8) -> u8 {
510            self.b[addr as usize]
511        }
512        fn write_b(&mut self, addr: u8, val: u8) {
513            self.b[addr as usize] = val;
514        }
515    }
516
517    #[test]
518    fn gp_dma_mode0_copies_block_to_b_bus() {
519        let mut bus = TestBus {
520            a: vec![0; 0x10000],
521            b: [0; 256],
522        };
523        for i in 0..4u32 {
524            bus.a[(0x1000 + i) as usize] = (0xA0 + i) as u8;
525        }
526        let mut dma = Dma::new();
527        // channel 0: mode 0 (single reg), A→B, source $00:1000, target $18 (VMDATA), 4 bytes.
528        dma.write_reg(0, 0x0, 0x00); // DMAP: A→B, mode 0
529        dma.write_reg(0, 0x1, 0x18); // BBAD
530        dma.write_reg(0, 0x2, 0x00); // A1TL
531        dma.write_reg(0, 0x3, 0x10); // A1TH -> $1000
532        dma.write_reg(0, 0x4, 0x00); // A1B
533        dma.write_reg(0, 0x5, 0x04); // DASL = 4
534        dma.write_reg(0, 0x6, 0x00); // DASH
535        let cost = dma.run_gp(0x01, &mut bus);
536        // Mode 0 hammers a single B address, so the last byte wins.
537        assert_eq!(bus.b[0x18], 0xA3);
538        assert!(cost >= 8 + 8 + 4 * 8); // alignment + channel + 4 bytes
539    }
540
541    #[test]
542    fn gp_dma_mode1_alternates_two_b_regs() {
543        let mut bus = TestBus {
544            a: vec![0; 0x10000],
545            b: [0; 256],
546        };
547        for i in 0..4u32 {
548            bus.a[(0x2000 + i) as usize] = (0x10 + i) as u8;
549        }
550        let mut dma = Dma::new();
551        dma.write_reg(0, 0x0, 0x01); // mode 1 (2 regs)
552        dma.write_reg(0, 0x1, 0x18); // BBAD base
553        dma.write_reg(0, 0x2, 0x00);
554        dma.write_reg(0, 0x3, 0x20); // $2000
555        dma.write_reg(0, 0x4, 0x00);
556        dma.write_reg(0, 0x5, 0x04);
557        dma.write_reg(0, 0x6, 0x00);
558        let _ = dma.run_gp(0x01, &mut bus);
559        // even bytes -> $18, odd bytes -> $19; last even=0x12, last odd=0x13.
560        assert_eq!(bus.b[0x18], 0x12);
561        assert_eq!(bus.b[0x19], 0x13);
562    }
563
564    #[test]
565    fn gp_dma_enable_is_one_shot() {
566        let mut bus = TestBus {
567            a: vec![0; 0x10000],
568            b: [0; 256],
569        };
570        let mut dma = Dma::new();
571        dma.write_reg(0, 0x5, 0x01);
572        dma.gp_enable = 0x01;
573        let _ = dma.run_gp(0x01, &mut bus);
574        assert_eq!(dma.gp_enable, 0);
575    }
576
577    #[test]
578    fn all_channels_round_trip_through_save_state() {
579        let mut dma = Dma::new();
580        dma.write_reg(3, 0x0, 0x81);
581        dma.write_reg(3, 0x1, 0x18);
582        dma.gp_enable = 0x08;
583        dma.hdma_enable = 0x01;
584
585        let mut w = SaveWriter::new();
586        dma.save_state(&mut w);
587        let bytes = w.into_bytes();
588
589        let mut fresh = Dma::new();
590        let mut r = SaveReader::new(&bytes);
591        fresh.load_state(&mut r).unwrap();
592
593        assert_eq!(fresh.channels[3].dmap, 0x81);
594        assert_eq!(fresh.channels[3].target, 0x18);
595        assert_eq!(fresh.gp_enable, 0x08);
596        assert_eq!(fresh.hdma_enable, 0x01);
597        assert_eq!(r.remaining(), 0);
598    }
599}