Skip to main content
← PSoXide SDK guides and examples · SDK f2fec1c301f1 · All Cargo features enabled; check feature gates before use.

psx_io/
dma.rs

1//! DMA controller.
2//!
3//! The PS1 has 7 DMA channels (MDEC in, MDEC out, GPU, CD-ROM, SPU,
4//! expansion, ordering-table clear). Each channel has an address, a size and
5//! a control register at fixed 16-byte strides, plus a global enable and a
6//! global interrupt register. The addresses and bit layouts live in
7//! [`psx_hw::dma`].
8
9use crate::periph::{OrderingTableClearDma, SpuDma};
10use psx_hw::dma as reg;
11
12/// Channel index 0..=6 in the order the DMA controller presents them.
13#[derive(Clone, Copy, Debug, PartialEq, Eq)]
14#[repr(u8)]
15pub enum Channel {
16    /// RAM→MDEC: run-length coefficient data *into* the decoder. This is
17    /// the direction the name refers to, not the direction of transfer to
18    /// RAM -- channel 0 feeds the MDEC, channel 1 drains it.
19    MdecIn = 0,
20    /// MDEC→RAM: decoded macroblocks *out of* the decoder, 256 pixels
21    /// (16x16) per macroblock at 15bpp.
22    MdecOut = 1,
23    /// RAM↔GPU.
24    Gpu = 2,
25    /// CD-ROM drive → RAM.
26    #[doc(alias = "CDROM")]
27    Cd = 3,
28    /// SPU ↔ RAM.
29    Spu = 4,
30    /// Expansion port.
31    #[doc(alias = "PIO")]
32    Expansion = 5,
33    /// Ordering-table clear: writes a reverse-linked empty ordering table.
34    #[doc(alias = "OTC")]
35    OrderingTableClear = 6,
36}
37
38#[allow(non_upper_case_globals)]
39impl Channel {
40    /// Renamed to [`Channel::Cd`].
41    #[deprecated(note = "renamed to `Channel::Cd`")]
42    pub const Cdrom: Channel = Channel::Cd;
43}
44
45impl Channel {
46    /// Address of this channel's register block.
47    #[inline(always)]
48    pub const fn register_base(self) -> u32 {
49        reg::CHANNEL_BASE + reg::CHANNEL_STRIDE * (self as u32)
50    }
51
52    /// Bit position of this channel's enable flag in the global enable
53    /// register.
54    #[doc(alias = "DPCR")]
55    #[inline(always)]
56    pub const fn enable_bit(self) -> u32 {
57        3 + 4 * (self as u32)
58    }
59}
60
61/// Size word for manual or linked-list sync: a 16-bit word count
62/// (linked-list mode ignores it, but silicon wants it written).
63#[doc(alias = "BCR")]
64#[inline(always)]
65pub const fn size_words(words: u16) -> u32 {
66    words as u32
67}
68
69/// Size word for block sync: `block_count` blocks of `block_size` words.
70///
71/// A zero field means 0x1_0000 on silicon, so a zero count is a 65,536-block
72/// transfer, not an empty one.
73#[doc(alias = "BCR")]
74#[inline(always)]
75pub const fn size_blocks(block_size: u16, block_count: u16) -> u32 {
76    (block_size as u32) | ((block_count as u32) << 16)
77}
78
79/// The three per-channel register values that describe one transfer.
80///
81/// [`start`] stores them in the order silicon expects: address, size, then
82/// control (which starts the transfer when it carries `CHCR_START`).
83#[derive(Clone, Copy, Debug, PartialEq, Eq)]
84pub struct Transfer {
85    /// RAM address the channel reads from or writes to.
86    #[doc(alias = "MADR")]
87    pub address: u32,
88    /// Word or block count; see [`size_words`] and [`size_blocks`].
89    #[doc(alias = "BCR")]
90    pub size: u32,
91    /// Control word ([`psx_hw::dma`]'s `CHCR_*` bits): direction, step, sync
92    /// mode, start.
93    #[doc(alias = "CHCR")]
94    pub control: u32,
95}
96
97/// Program channel `ch` with `transfer` and start it.
98///
99/// The same three stores every SDK driver makes: address, size, a
100/// compiler-only barrier that publishes ordinary RAM stores made before the
101/// call (it emits no instruction), then control. The caller still enables
102/// the channel ([`enable_channel`]) and waits for completion ([`wait_done`]).
103///
104/// # Safety
105///
106/// From the control store until the channel goes idle ([`wait_done`]
107/// returns `true`, or [`abort`] stops it), the DMA controller reads or writes
108/// RAM with no regard for Rust's borrow rules. The caller must guarantee that
109/// for that whole window:
110///
111/// - every word the transfer can **write** (a device-to-RAM channel, such
112///   as MDEC-out, CD-ROM, GPU readback or the ordering-table clear) lies in
113///   memory the caller owns exclusively, with no live Rust reference to it;
114/// - every word the transfer can **read** (a RAM-to-device channel, or each
115///   node of a linked list and the nodes its tags link to) is live,
116///   initialised and not written by anyone else;
117/// - the extent is the one `size` and the sync mode describe, remembering
118///   that a zero block count means 65,536 blocks;
119/// - the channel is idle when this is called (silicon ignores a control
120///   write to a busy channel, so the old transfer would keep running).
121#[inline(always)]
122pub unsafe fn start(ch: Channel, transfer: Transfer) {
123    // SAFETY: the caller upholds this function's contract for the transfer
124    // these three stores arm and start.
125    unsafe {
126        raw::set_address(ch, transfer.address);
127        raw::set_size(ch, transfer.size);
128    }
129    compiler_barrier();
130    // SAFETY: as above.
131    unsafe { raw::set_control(ch, transfer.control) };
132}
133
134/// Compiler-only barrier: ordinary RAM accesses are not moved across it in
135/// either direction, so stores before it land before an MMIO store after it
136/// and accesses after it follow an MMIO read before it.
137///
138/// The pinned MIPS-I backend lowers even a single-thread compiler fence to
139/// `SYNC`, which the R3000 lacks, so the target uses an empty `asm!` with its
140/// default memory clobber. Do not add `nomem` or `readonly`: both would drop
141/// the guarantee.
142#[inline(always)]
143pub fn compiler_barrier() {
144    #[cfg(target_arch = "mips")]
145    // SAFETY: an empty asm block; it only constrains compiler ordering.
146    unsafe {
147        core::arch::asm!("", options(nostack, preserves_flags));
148    }
149    #[cfg(not(target_arch = "mips"))]
150    core::sync::atomic::compiler_fence(core::sync::atomic::Ordering::SeqCst);
151}
152
153/// Single-register writes for silicon probes and drivers that need a store
154/// order other than [`start`]'s.
155pub mod raw {
156    use super::{reg, Channel};
157
158    /// Write the channel's RAM address register.
159    ///
160    /// # Safety
161    ///
162    /// The value arms the next transfer on `ch`: the caller takes on
163    /// [`super::start`]'s contract for whatever transfer later starts with it.
164    ///
165    /// With the `present-queue` feature, a store to the GPU channel first
166    /// runs an armed direct-access guard (`gpu::arm_direct_access_guard`),
167    /// so it never re-arms the channel under a queued walk. Every GPU DMA
168    /// start, [`super::start`] included, goes through here.
169    #[doc(alias = "MADR")]
170    #[inline(always)]
171    pub unsafe fn set_address(ch: Channel, addr: u32) {
172        #[cfg(feature = "present-queue")]
173        if matches!(ch, Channel::Gpu) {
174            crate::gpu::run_direct_access_guard();
175        }
176        // SAFETY: a store to this channel's address register; see the contract above.
177        unsafe { crate::write_u32(ch.register_base() + reg::MADR, addr) }
178    }
179
180    /// Write the channel's size register.
181    ///
182    /// # Safety
183    ///
184    /// As [`set_address`]: the count sets the extent of the next transfer.
185    #[doc(alias = "BCR")]
186    #[inline(always)]
187    pub unsafe fn set_size(ch: Channel, value: u32) {
188        // SAFETY: a store to this channel's size register; see the contract above.
189        unsafe { crate::write_u32(ch.register_base() + reg::BCR, value) }
190    }
191
192    /// Write the channel's control register. With `CHCR_START` set this
193    /// starts a transfer from whatever the address and size registers hold.
194    ///
195    /// # Safety
196    ///
197    /// A value with `CHCR_START` set must satisfy [`super::start`]'s contract
198    /// for the transfer it starts. A value without it (such as 0, an abort)
199    /// starts nothing.
200    #[doc(alias = "CHCR")]
201    #[inline(always)]
202    pub unsafe fn set_control(ch: Channel, value: u32) {
203        // SAFETY: a store to this channel's control register; see the contract above.
204        unsafe { crate::write_u32(ch.register_base() + reg::CHCR, value) }
205    }
206}
207
208/// Write the channel's RAM address register.
209///
210/// # Safety
211///
212/// The value arms or starts a transfer on `ch`; see [`start`]'s contract.
213#[deprecated(
214    note = "a safe address store lets safe code aim DMA anywhere in RAM; use the unsafe `dma::start`, or `dma::raw::set_address` for probes"
215)]
216#[inline(always)]
217pub unsafe fn set_madr(ch: Channel, addr: u32) {
218    // SAFETY: forwarded contract.
219    unsafe { raw::set_address(ch, addr) }
220}
221
222/// Read the channel's RAM address register.
223#[doc(alias = "MADR")]
224#[inline(always)]
225pub fn address(ch: Channel) -> u32 {
226    // SAFETY: a side-effect-free read of this channel's address register.
227    unsafe { crate::read_u32(ch.register_base() + reg::MADR) }
228}
229
230/// Write the size register in manual / linked-list mode: just a 16-bit word
231/// count.
232///
233/// # Safety
234///
235/// The value arms or starts a transfer on `ch`; see [`start`]'s contract.
236#[deprecated(
237    note = "use the unsafe `dma::start` with `dma::size_words`, or `dma::raw::set_size` for probes"
238)]
239#[inline(always)]
240pub unsafe fn set_bcr_manual(ch: Channel, words: u16) {
241    // SAFETY: forwarded contract.
242    unsafe { raw::set_size(ch, size_words(words)) }
243}
244
245/// Write the control register. Starts the transfer if `CHCR_START` is set.
246///
247/// # Safety
248///
249/// The value arms or starts a transfer on `ch`; see [`start`]'s contract.
250#[deprecated(
251    note = "a safe control store starts DMA from safe code; use the unsafe `dma::start`, `dma::abort` to stop a channel, or `dma::raw::set_control` for probes"
252)]
253#[inline(always)]
254pub unsafe fn set_chcr(ch: Channel, value: u32) {
255    // SAFETY: forwarded contract.
256    unsafe { raw::set_control(ch, value) }
257}
258
259/// Read the channel's control register.
260#[doc(alias = "CHCR")]
261#[inline(always)]
262pub fn control(ch: Channel) -> u32 {
263    // SAFETY: a side-effect-free read of this channel's control register.
264    unsafe { crate::read_u32(ch.register_base() + reg::CHCR) }
265}
266
267/// True while the channel is busy with an in-flight transfer.
268#[inline(always)]
269pub fn is_busy(ch: Channel) -> bool {
270    control(ch) & reg::CHCR_START != 0
271}
272
273/// Enable a channel without disturbing the others.
274///
275/// The read-modify-write of the global enable register runs with CPU
276/// interrupts masked: with the `present-queue` feature the VBlank handler
277/// sets the GPU channel's bit in the same register, and a handler that ran
278/// between this read and write would have its bit overwritten.
279pub fn enable_channel(ch: Channel) {
280    crate::irq::without_interrupts(|| {
281        // SAFETY: a side-effect-free read of the global enable register.
282        let enabled = unsafe { crate::read_u32(reg::DPCR) };
283        // SAFETY: sets one channel's enable bit and keeps the rest; enabling a
284        // channel starts no transfer.
285        unsafe { crate::write_u32(reg::DPCR, enabled | (1 << ch.enable_bit())) }
286    });
287}
288
289impl OrderingTableClearDma {
290    /// Ordering-table clear: writes `buf` as a reverse-linked chain the
291    /// GPU-DMA walker consumes. The hardware starts from the last word and
292    /// steps backward, writing a terminator at the first transfer and then
293    /// predecessor pointers.
294    ///
295    /// Convenience wrapper: programs the channel and blocks until done.
296    /// Returns false if the channel wedged, or if `buf` is longer than the
297    /// 16-bit word count can express.
298    #[doc(alias = "ClearOTagR")]
299    pub fn clear_table(&mut self, buf: &mut [u32]) -> bool {
300        clear_table(buf)
301    }
302}
303
304/// [`OrderingTableClearDma::clear_table`] on a token the caller does not hold.
305#[deprecated(note = "use `OrderingTableClearDma::clear_table` with the token")]
306#[inline(always)]
307pub fn clear_ordering_table(buf: &mut [u32]) -> bool {
308    // SAFETY: a token is a logic guard, not a memory-safety one (see
309    // `crate::periph`), and the old function never took one.
310    unsafe { OrderingTableClearDma::steal() }.clear_table(buf)
311}
312
313fn clear_table(buf: &mut [u32]) -> bool {
314    let Ok(words) = u16::try_from(buf.len()) else {
315        // Truncating to 16 bits would clear only the tail of the table
316        // and leave the head pointing into words the DMA never touched.
317        return false;
318    };
319    let Some(last) = buf.last_mut() else {
320        return true;
321    };
322    let last_addr = last as *mut u32 as u32;
323    // Abort whatever the channel was doing before arming it. On silicon a
324    // transfer that never completes leaves START latched, and a write to
325    // the control register while the channel is still busy is ignored: one
326    // stuck kick then poisons every later transfer on that channel, which is
327    // how a single wedge turned into "the DMA moves nothing, forever" across
328    // the CD reader, the ordering-table clear, and the boot uploads.
329    abort(Channel::OrderingTableClear);
330    enable_channel(Channel::OrderingTableClear);
331    // Ordering-table clear: direction backward (step -4), manual sync,
332    // trigger bit.
333    // SAFETY: the channel was just aborted, so it is idle. The transfer
334    // writes `words` words stepping back from `last_addr`, which is exactly
335    // `buf`, borrowed exclusively until this function returns. It cannot
336    // outlive that borrow, and this does not rest on what an abort does:
337    // DMA6 has only CHCR bits 24, 28 and 30 writable, so it always runs in
338    // burst mode (SyncMode 0) without chopping, and psx-spx ("DMA Channels",
339    // "CPU Operation during DMA") documents that any RAM or I/O read stalls
340    // the CPU until such a transfer is finished, allowing only I/O reads
341    // within 3 cycles of the CHCR write. The wait below reads CHCR until it
342    // reads idle or its budget ends, so by the time it returns the transfer
343    // has finished or, wedged with START latched, never ran.
344    unsafe {
345        start(
346            Channel::OrderingTableClear,
347            Transfer {
348                address: last_addr,
349                size: size_words(words),
350                control: reg::CHCR_STEP_BACKWARD
351                    | reg::CHCR_SYNC_MANUAL
352                    | reg::CHCR_START
353                    | reg::CHCR_TRIGGER,
354            },
355        )
356    };
357    // A wedge is aborted, which leaves the controller usable for the next
358    // caller rather than handing back a channel that will swallow its kick.
359    wait_or_abort(Channel::OrderingTableClear, DEFAULT_SPINS)
360}
361
362impl SpuDma {
363    /// Copy `words` to the SPU over DMA channel 4, in blocks of `block_words`
364    /// words, and wait for the channel to finish.
365    ///
366    /// Enables the channel without disturbing the others, starts a block-sync
367    /// transfer, and ends it with [`wait_or_abort`], so the SPU has taken
368    /// every word or the channel has been aborted by the time this returns.
369    /// The caller has already put the SPU in its DMA-write transfer mode.
370    ///
371    /// Returns `false` without starting anything when `words` is empty (a zero
372    /// block count means 65,536 blocks on silicon), when `block_words` is 0 or
373    /// does not divide the length, or when the block count does not fit the
374    /// 16-bit field; and `false` when the channel wedged and was aborted.
375    pub fn write_blocks(&mut self, words: &[u32], block_words: u16) -> bool {
376        let block = usize::from(block_words);
377        if words.is_empty() || block == 0 || !words.len().is_multiple_of(block) {
378            return false;
379        }
380        let Ok(block_count) = u16::try_from(words.len() / block) else {
381            return false;
382        };
383        enable_channel(Channel::Spu);
384        // SAFETY: the transfer reads `block_words * block_count` words from
385        // `words`, which is exactly the slice (a whole number of non-empty
386        // blocks, checked above), borrowed until this function returns. The
387        // wait below, or its abort on a wedge, ends the transfer before then.
388        unsafe {
389            start(
390                Channel::Spu,
391                Transfer {
392                    address: words.as_ptr() as u32,
393                    size: size_blocks(block_words, block_count),
394                    control: reg::CHCR_TO_DEVICE | reg::CHCR_SYNC_BLOCK | reg::CHCR_START,
395                },
396            )
397        };
398        wait_or_abort(Channel::Spu, DEFAULT_SPINS)
399    }
400}
401
402/// Spin budget for one DMA completion wait. Comfortably longer than the
403/// largest legitimate transfer (a full-screen VRAM upload) and short
404/// enough that a wedged channel returns control inside a frame.
405pub const DEFAULT_SPINS: u32 = 500_000;
406
407/// Clear `ch`'s control register, dropping its START bit, so the channel can
408/// be re-armed: on silicon a CHCR write to a channel whose START is still
409/// latched is ignored, which is how one wedged kick used to poison every
410/// later transfer on the channel.
411///
412/// psx-spx documents only that START clears when a transfer completes, not
413/// that clearing it stops one in progress. Code whose memory safety needs a
414/// transfer to have stopped must not rely on this call for it; see
415/// [`OrderingTableClearDma::clear_table`] for a bound that does not.
416pub fn abort(ch: Channel) {
417    // SAFETY: a control word of 0 has no START bit, so it starts nothing.
418    unsafe { raw::set_control(ch, 0) };
419}
420
421/// Bounded completion wait. `false` means the channel was still busy
422/// when the budget ran out, i.e. the transfer is wedged and the caller
423/// must not assume its data landed.
424pub fn wait_done(ch: Channel, spins: u32) -> bool {
425    let mut waited = 0u32;
426    while is_busy(ch) {
427        if waited >= spins {
428            return false;
429        }
430        waited += 1;
431    }
432    true
433}
434
435/// End a transfer started with [`start`]: [`wait_done`]; if the budget runs
436/// out, [`abort`] the channel and wait again, within the same budget, for it
437/// to read idle; then a compiler barrier so the caller's next RAM access to
438/// the buffer is not moved before the completion read (the barrier emits no
439/// instruction). `false` means the data did not all land.
440///
441/// The second wait is there because clearing START is not documented to stop
442/// a running transfer (see [`abort`]). What it leaves assumed: a channel that
443/// still reads busy after both budgets is wedged and moving no data, as the
444/// CD channel's console wedges were (START latched, MADR frozen). Burst-mode
445/// channels without chopping need no assumption; see
446/// [`OrderingTableClearDma::clear_table`].
447pub fn wait_or_abort(ch: Channel, spins: u32) -> bool {
448    let done = wait_done(ch, spins);
449    if !done {
450        abort(ch);
451        let _ = wait_done(ch, spins);
452    }
453    compiler_barrier();
454    done
455}
456
457#[cfg(test)]
458mod tests {
459    extern crate std;
460    use super::*;
461
462    #[test]
463    fn block_size_packs_size_low_and_count_high() {
464        assert_eq!(size_blocks(16, 3), 0x0003_0010);
465        assert_eq!(size_blocks(0xFFFF, 0xFFFF), 0xFFFF_FFFF);
466        assert_eq!(size_words(0x1234), 0x1234);
467    }
468
469    #[test]
470    fn channel_blocks_sit_at_sixteen_byte_strides() {
471        assert_eq!(Channel::MdecIn.register_base(), reg::CHANNEL_BASE);
472        assert_eq!(
473            Channel::Gpu.register_base(),
474            reg::CHANNEL_BASE + 2 * reg::CHANNEL_STRIDE
475        );
476        assert_eq!(
477            Channel::OrderingTableClear.register_base(),
478            reg::CHANNEL_BASE + 6 * reg::CHANNEL_STRIDE
479        );
480        assert_eq!(Channel::OrderingTableClear.enable_bit(), 27);
481    }
482
483    #[test]
484    fn an_spu_write_refuses_a_shape_it_cannot_describe_before_touching_the_channel() {
485        // SAFETY: a test-local token on the host; every call below returns
486        // before any register is touched.
487        let mut dma = unsafe { SpuDma::steal() };
488        let words = std::vec![0u32; 12];
489        // Nothing to send: a zero block count would be 65,536 blocks.
490        assert!(!dma.write_blocks(&[], 4));
491        // No block size, or one that leaves a partial block.
492        assert!(!dma.write_blocks(&words, 0));
493        assert!(!dma.write_blocks(&words, 5));
494        // More blocks than the 16-bit count holds.
495        let long = std::vec![0u32; 65_536];
496        assert!(!dma.write_blocks(&long, 1));
497    }
498
499    #[test]
500    fn the_clear_refuses_a_table_it_cannot_describe_before_touching_the_channel() {
501        // SAFETY: a test-local token on the host; both calls return before any
502        // register is touched.
503        let mut dma = unsafe { OrderingTableClearDma::steal() };
504        // An empty table has nothing to clear.
505        assert!(dma.clear_table(&mut []));
506        // 65,536 words does not fit the 16-bit count: refused whole, not
507        // truncated to its tail.
508        let mut long = std::vec![0u32; 65_536];
509        assert!(!dma.clear_table(&mut long));
510    }
511}