gb/ppu/hdma.rs
1//! The Game Boy Color's copier into video memory.
2//!
3//! The quickest way to put data in video memory. There are two, and what
4//! separates them is not speed but what the program may do meanwhile. See
5//! <https://gbdev.io/pandocs/CGB_Registers.html#ff51ff52--hdma1-hdma2-cgb-mode-only-vram-dma-source-high-low-write-only>.
6//!
7//! [`copy`] stops the CPU until the whole block has moved, so nothing else runs
8//! and there is nothing to get wrong. One of the largest size roughly fills a
9//! VBlank, which is most of a tileset.
10//!
11//! [`stream`] moves one block per HBlank and lets the program run in between,
12//! reaching a comparable amount over a frame. What it costs is listed
13//! on [`Stream`], and the list is long: it holds the video memory bank and the
14//! source bank still for its whole life, and a sleeping CPU stops it. It suits a
15//! stretch where nothing else touches video memory, a loading screen rather than
16//! a scrolling level.
17//!
18//! Neither reaches an original Game Boy, which has no such hardware: the write
19//! that would start a transfer goes nowhere, and the call returns having moved
20//! nothing.
21
22use crate::mmio::cgb::{HDMA1, HDMA2, HDMA3, HDMA4, HDMA5, HdmaCtrl};
23
24use super::Access;
25
26/// Bytes one transfer step moves, and the granularity of everything here.
27pub const BLOCK_LEN: usize = 16;
28
29/// Bytes the largest transfer carries.
30pub const MAX_LEN: usize = 128 * BLOCK_LEN;
31
32/// Storage the copier can read, aligned as it needs.
33///
34/// The hardware ignores the low four bits of both addresses, so an unaligned
35/// source would be read from the wrong place rather than refused. [`copy`] and
36/// [`stream`] check for it instead, and this is how the check is passed: a
37/// `[Tile; N]` is byte-aligned and lands wherever the linker puts it.
38///
39/// ```ignore
40/// static TILES: hdma::Source<[Tile; 64]> = hdma::Source([..]);
41/// ```
42#[repr(align(16))]
43pub struct Source<T>(pub T);
44
45/// Copy `src` to `dst` in video memory, stopping the CPU until it is done.
46///
47/// The copier does not wait for the PPU. The [`Access`] settles where the
48/// transfer starts, not where it ends, so a block long enough to outlast the
49/// window is still being written while the lines it reaches are drawn, and those
50/// come out garbled: size one to the window, or switch the display off.
51///
52/// Nothing runs while it works, which makes a source in a switchable bank safe
53/// here in a way it is not in [`stream`].
54///
55/// # Panics
56///
57/// If `src` is empty, longer than [`MAX_LEN`], not a whole number of
58/// [`BLOCK_LEN`]s, or misaligned; if `dst` is outside `0x8000..0xA000`, is
59/// misaligned, or has no room; or if a [`stream`] is still running, which this
60/// would stop rather than join.
61pub fn copy(access: Access<'_>, dst: u16, src: &[u8]) {
62 assert!(HDMA5.read().hblank(), "a stream is still running");
63 let blocks = prepare(dst, src);
64 if matches!(access, Access::Polled) {
65 super::wait_blank();
66 }
67 HDMA5.write(HdmaCtrl::new().with_blocks(blocks).with_hblank(false));
68}
69
70/// Start moving `src` to `dst` sixteen bytes at a time, one step per HBlank.
71///
72/// `None` if a transfer is already running. Read [`Stream`] before using this:
73/// what the program may do while it runs is narrow.
74///
75/// `src` is `'static` because the copier reads it over many frames and a
76/// switchable bank would move underneath it. That rules out banked ROM and
77/// cartridge RAM, neither of which lends for longer than a scope.
78///
79/// # Panics
80///
81/// As [`copy`].
82pub fn stream(dst: u16, src: &'static [u8]) -> Option<Stream> {
83 if !HDMA5.read().hblank() {
84 return None;
85 }
86 let blocks = prepare(dst, src);
87 // Starting one during HBlank is the documented way to get a broken transfer.
88 // Mode 3 is excluded too: it can be a dot from HBlank, which the write below
89 // would then land in. From mode 2 the nearest HBlank is 252 dots off.
90 while matches!(
91 crate::mmio::STAT.read().mode(),
92 crate::mmio::PpuMode::HBlank | crate::mmio::PpuMode::Drawing
93 ) {}
94 HDMA5.write(HdmaCtrl::new().with_blocks(blocks).with_hblank(true));
95 Some(Stream(()))
96}
97
98/// A stream under way, one block per HBlank.
99///
100/// Until it is done or [`stopped`](Self::stop), the program must not:
101///
102/// - change the video memory bank, which rules out
103/// [`with_vram_bank`](super::with_vram_bank) and
104/// [`edit_attrs`](super::map::edit_attrs);
105/// - unmap the bank the source sits in;
106/// - execute `halt`, which stops the copier until the CPU wakes. That includes
107/// [`Vblank::wait`](super::Vblank::wait), so a frame paced with it will barely
108/// advance the transfer.
109///
110/// Dropping this leaves the transfer running. Nothing is torn by that; the
111/// obligations above simply go unwatched.
112pub struct Stream(());
113
114impl Stream {
115 /// Whether the copier has stopped, whether by finishing or by [`stop`](Self::stop).
116 #[inline]
117 pub fn is_done(&self) -> bool {
118 HDMA5.read().hblank()
119 }
120
121 /// Bytes still to move, zero once [`is_done`](Self::is_done).
122 #[inline]
123 pub fn remaining(&self) -> u16 {
124 let c = HDMA5.read();
125 if c.hblank() {
126 0
127 } else {
128 (c.blocks() as u16 + 1) * BLOCK_LEN as u16
129 }
130 }
131
132 /// Stop early, leaving what has arrived in place. Does nothing once the
133 /// transfer has finished on its own.
134 #[inline]
135 pub fn stop(self) {
136 // Clearing bit 7 terminates a running transfer, but starts a
137 // general-purpose one where none is running: the addresses this one left
138 // behind would be copied from and to all over again. A transfer that
139 // ends in the few cycles between the check and the write still gets one,
140 // which the hardware gives no way to close.
141 if !self.is_done() {
142 HDMA5.write(HdmaCtrl::new().with_hblank(false));
143 }
144 }
145}
146
147/// Load the addresses and return the length in blocks, minus one.
148fn prepare(dst: u16, src: &[u8]) -> u8 {
149 let s = src.as_ptr() as usize;
150 assert!(!src.is_empty() && src.len() <= MAX_LEN && src.len() % BLOCK_LEN == 0);
151 assert!(s % BLOCK_LEN == 0);
152 // Both ends, not just the start: a block running off the end of read-only
153 // memory reaches into VRAM, which the copier reads as rubbish.
154 let end = s + src.len() - 1;
155 assert!((s < 0x8000 && end < 0x8000) || (0xA000..0xE000).contains(&s) && end < 0xE000);
156 let d = dst as usize;
157 assert!((0x8000..0xA000).contains(&d) && d % BLOCK_LEN == 0 && d + src.len() <= 0xA000);
158
159 HDMA1.write((s >> 8) as u8);
160 HDMA2.write(s as u8);
161 HDMA3.write((dst >> 8) as u8);
162 HDMA4.write(dst as u8);
163 (src.len() / BLOCK_LEN - 1) as u8
164}