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

psx_io/gpu/
handoff.rs

1//! Command recording and the direct-access guard: what lets a frame's GPU
2//! work be handed to a queue (`psx_rt::present`) instead of the ports.
3//!
4//! Built only with the `present-queue` feature. Without it the port writes
5//! in [`super`] compile to a single store, as before.
6//!
7//! - **Recording.** While the [`ActiveRecording`] that
8//!   [`start_recording_raw`] returns is alive, [`super::write_command`]
9//!   appends to a caller buffer laid out as DMA linked-list nodes instead of
10//!   writing the port, and the ready-waits return at once. Immediate drawing
11//!   (every psx-gpu `draw_*`, every HUD helper) can then be replayed later by
12//!   one DMA walk. [`ActiveRecording::end`] consumes the guard, so a
13//!   recording cannot be ended twice, and a closed recording never reopens:
14//!   neither a late [`RecordingPause`] nor the deprecated [`end_recording`]
15//!   touches the buffer of a recording that is no longer open.
16//! - **Direct-access guard.** Once armed ([`arm_direct_access_guard`]), the
17//!   first direct command or display-control write, or GPU DMA address
18//!   store, runs the registered guard function first, which waits until no
19//!   queued walk can collide with the access.
20//!
21//! Both states share one flag, so a plain port write costs one extra load
22//! and branch while the feature is on.
23
24use core::ptr::{addr_of, addr_of_mut, read_volatile, write_volatile};
25
26/// Payload words per recorded linked-list node. Silicon (hardware-tests
27/// v1.24) loses words from DMA nodes longer than 16 while drawing.
28pub const RECORDING_NODE_WORDS: u32 = 16;
29
30/// The one command recording. The buffer pointers are meaningful only while
31/// `open` is set; closing clears `open` and `active` before anything else,
32/// so no path reaches a buffer whose recording has ended.
33struct Recording {
34    /// Between a start and the close of that recording.
35    open: bool,
36    /// Open and not paused: what [`is_recording`] reports.
37    active: bool,
38    /// Counts starts, so a guard or pause knows whether the open recording
39    /// is still the one it was made for.
40    generation: u32,
41    overflowed: bool,
42    head: *mut u32,
43    node: *mut u32,
44    node_words: u32,
45    next: *mut u32,
46    end: *mut u32,
47}
48
49static mut RECORDING: Recording = Recording {
50    open: false,
51    active: false,
52    generation: 0,
53    overflowed: false,
54    head: core::ptr::null_mut(),
55    node: core::ptr::null_mut(),
56    node_words: 0,
57    next: core::ptr::null_mut(),
58    end: core::ptr::null_mut(),
59};
60
61/// Set while recording or while the guard is armed: the rare state the port
62/// writes test with one load before their plain store.
63static mut SLOW: bool = false;
64static mut GUARD_ARMED: bool = false;
65static mut GUARD: Option<fn()> = None;
66
67#[inline(always)]
68pub(super) fn is_slow() -> bool {
69    // SAFETY: a volatile read of a `bool` static; the program is single
70    // threaded and the VBlank handler never touches it.
71    unsafe { read_volatile(addr_of!(SLOW)) }
72}
73
74fn refresh_slow() {
75    // SAFETY: volatile reads and a write of `bool` statics owned by this
76    // module; the program is single threaded and no interrupt handler
77    // touches them.
78    unsafe {
79        let slow =
80            read_volatile(addr_of!(RECORDING.active)) || read_volatile(addr_of!(GUARD_ARMED));
81        write_volatile(addr_of_mut!(SLOW), slow);
82    }
83}
84
85fn set_recording_active(active: bool) {
86    // SAFETY: a volatile write of a `bool` static owned by this module.
87    unsafe { write_volatile(addr_of_mut!(RECORDING.active), active) };
88    refresh_slow();
89}
90
91/// The slow half of [`super::write_command`]: record the word, or run the
92/// guard before the port write. Out of line and cold, so each write site
93/// carries only the flag load and branch.
94#[cold]
95#[inline(never)]
96pub(super) fn write_command_slow(word: u32) {
97    if is_recording() {
98        record_word(word);
99        return;
100    }
101    run_direct_access_guard();
102    super::write_command_unguarded(word);
103}
104
105/// The slow half of [`super::write_display_control`]: run the guard unless a
106/// recording is open (display control is never recorded), then write the
107/// port.
108#[cold]
109#[inline(never)]
110pub(super) fn write_display_control_slow(word: u32) {
111    if !is_recording() {
112        run_direct_access_guard();
113    }
114    super::write_display_control_unguarded(word);
115}
116
117/// A command stream recorded by [`start_recording_raw`]: DMA linked-list
118/// nodes that end the list until [`link_to`](Self::link_to) chains them
119/// onward.
120#[derive(Debug)]
121pub struct CommandRecording {
122    head: *mut u32,
123    tail: *mut u32,
124    words: usize,
125}
126
127impl CommandRecording {
128    /// First node, the address a chain links to or a DMA walk starts at.
129    #[inline]
130    pub fn head(&self) -> *const u32 {
131        self.head
132    }
133
134    /// Words used in the buffer, node headers included.
135    #[inline]
136    pub fn len_words(&self) -> usize {
137        self.words
138    }
139
140    /// Point the last node at `next` (a node tag) instead of ending the list
141    /// there.
142    ///
143    /// # Safety
144    ///
145    /// The recording's buffer must still be live, and no walk of it may have
146    /// started.
147    #[inline]
148    pub unsafe fn link_to(&self, next: *const u32) {
149        // SAFETY: `tail` is the last node's tag inside the caller's buffer,
150        // which the caller keeps live and unwalked (this fn's `# Safety`).
151        unsafe {
152            *self.tail = (*self.tail & 0xFF00_0000) | (next as u32 & 0x00FF_FFFF);
153        }
154    }
155}
156
157/// The recording ran out of buffer, so it was discarded: a command cut short
158/// would make the GPU take the words after it as its parameters.
159#[derive(Clone, Copy, Debug, PartialEq, Eq)]
160pub struct RecordingOverflow;
161
162/// True while a recording is open and not paused by [`pause_recording`].
163#[inline(always)]
164pub fn is_recording() -> bool {
165    // SAFETY: a volatile read of a `bool` static owned by this module.
166    unsafe { read_volatile(addr_of!(RECORDING.active)) }
167}
168
169/// Open the recording on `buffer` and return its generation.
170///
171/// # Safety
172///
173/// As [`start_recording_raw`].
174unsafe fn open(buffer: *mut u32, words: usize) -> u32 {
175    // SAFETY: exclusive access to this module's static (single threaded, no
176    // handler touches it). `buffer.add(words)` stays inside or one past the
177    // caller's buffer (the caller's `# Safety`).
178    let generation = unsafe {
179        let recording = &mut *addr_of_mut!(RECORDING);
180        debug_assert!(!recording.open, "a recording is already open");
181        recording.generation = recording.generation.wrapping_add(1);
182        recording.open = true;
183        recording.overflowed = false;
184        recording.head = core::ptr::null_mut();
185        recording.node = core::ptr::null_mut();
186        recording.node_words = 0;
187        recording.next = buffer;
188        recording.end = buffer.add(words);
189        recording.generation
190    };
191    set_recording_active(true);
192    generation
193}
194
195/// True when the recording of `generation` is still the open one.
196fn is_open(generation: u32) -> bool {
197    // SAFETY: volatile reads of this module's static.
198    unsafe {
199        read_volatile(addr_of!(RECORDING.open))
200            && read_volatile(addr_of!(RECORDING.generation)) == generation
201    }
202}
203
204/// Close the open recording, if it is the one of `generation` (any open one
205/// for `None`), and terminate its last node. A recording that is already
206/// closed is left alone: `Ok(None)`, and no memory is touched.
207fn close(generation: Option<u32>) -> Result<Option<CommandRecording>, RecordingOverflow> {
208    // SAFETY: a volatile read of this module's static.
209    let open = unsafe { read_volatile(addr_of!(RECORDING.open)) };
210    if !open || generation.is_some_and(|generation| !is_open(generation)) {
211        return Ok(None);
212    }
213    set_recording_active(false);
214    // SAFETY: exclusive access to this module's static. The recording is
215    // open (checked above), so `node` and `head` point into the buffer its
216    // start's contract keeps live until it is closed, which is now; `next`
217    // is in the same buffer at or after `head`. The pointers are cleared
218    // before returning, so nothing reaches the buffer through them again.
219    unsafe {
220        let recording = &mut *addr_of_mut!(RECORDING);
221        recording.open = false;
222        let result = if recording.overflowed {
223            Err(RecordingOverflow)
224        } else if recording.node.is_null() {
225            Ok(None)
226        } else {
227            *recording.node = (recording.node_words << 24) | 0x00FF_FFFF;
228            Ok(Some(CommandRecording {
229                head: recording.head,
230                tail: recording.node,
231                words: recording.next.offset_from(recording.head) as usize,
232            }))
233        };
234        recording.head = core::ptr::null_mut();
235        recording.node = core::ptr::null_mut();
236        recording.node_words = 0;
237        recording.next = core::ptr::null_mut();
238        recording.end = core::ptr::null_mut();
239        result
240    }
241}
242
243/// The open recording [`start_recording_raw`] returned. [`end`](Self::end)
244/// closes it and hands back the recorded nodes; dropping it closes it too,
245/// discarding the result.
246#[must_use = "dropping the recording ends it at once"]
247#[derive(Debug)]
248pub struct ActiveRecording {
249    generation: u32,
250}
251
252impl ActiveRecording {
253    /// Stop recording. `Ok(None)` when nothing was written.
254    pub fn end(self) -> Result<Option<CommandRecording>, RecordingOverflow> {
255        let generation = self.generation;
256        core::mem::forget(self);
257        close(Some(generation))
258    }
259}
260
261impl Drop for ActiveRecording {
262    fn drop(&mut self) {
263        let _ = close(Some(self.generation));
264    }
265}
266
267/// Record every [`super::write_command`] into `buffer` instead of the port,
268/// until the returned [`ActiveRecording`] is ended or dropped. The
269/// ready-waits return at once while recording.
270///
271/// Display-control writes and DMA are not recorded: do not change display
272/// state inside a recording, and upload images only through
273/// [`pause_recording`] (psx-vram's uploads do).
274///
275/// # Panics
276///
277/// In debug builds, if a recording is already open. Release builds close
278/// that one first, as dropping its guard would.
279///
280/// # Safety
281///
282/// `buffer` must be valid for `words` word writes and 4-byte aligned. It
283/// must stay live and unmodified (apart from [`CommandRecording::link_to`])
284/// until the recording is closed, which a leaked [`ActiveRecording`] never
285/// does, and after that until every walk of the recording has finished.
286#[doc(alias = "begin_recording_raw")]
287pub unsafe fn start_recording_raw(buffer: *mut u32, words: usize) -> ActiveRecording {
288    let _ = close(None);
289    // SAFETY: forwarded from this fn's `# Safety`.
290    let generation = unsafe { open(buffer, words) };
291    ActiveRecording { generation }
292}
293
294/// An open recording paused by [`pause_recording`]; it resumes where it left
295/// off when this is dropped, provided that recording is still open.
296#[must_use = "the recording resumes as soon as this is dropped"]
297#[derive(Debug)]
298pub struct RecordingPause {
299    /// Generation of the recording this paused, if it paused one.
300    resume: Option<u32>,
301}
302
303/// Pause any open recording until the returned guard is dropped, so command
304/// writes in between go to the port (behind the direct-access guard)
305/// instead of into the buffer. For VRAM uploads, which a recording cannot
306/// carry: a texture first drawn by a recorded HUD is uploaded at once,
307/// before the recording is walked, which is all a draw of it needs.
308#[inline]
309pub fn pause_recording() -> RecordingPause {
310    if !is_recording() {
311        return RecordingPause { resume: None };
312    }
313    set_recording_active(false);
314    // SAFETY: a volatile read of this module's static.
315    let generation = unsafe { read_volatile(addr_of!(RECORDING.generation)) };
316    RecordingPause {
317        resume: Some(generation),
318    }
319}
320
321impl Drop for RecordingPause {
322    #[inline]
323    fn drop(&mut self) {
324        // A recording closed (or replaced) while paused stays closed: its
325        // buffer may already be gone.
326        if let Some(generation) = self.resume {
327            if is_open(generation) {
328                set_recording_active(true);
329            }
330        }
331    }
332}
333
334#[inline(never)]
335fn record_word(word: u32) {
336    // SAFETY: exclusive access to this module's static. Every store lands
337    // at `next`, which the checks below keep below `end`, inside the buffer
338    // `begin_recording_raw`'s caller provided.
339    unsafe {
340        let recording = &mut *addr_of_mut!(RECORDING);
341        if recording.overflowed {
342            return;
343        }
344        if recording.node.is_null() || recording.node_words == RECORDING_NODE_WORDS {
345            // A new node needs its tag plus at least this word.
346            if (recording.end as usize).saturating_sub(recording.next as usize) < 8 {
347                recording.overflowed = true;
348                return;
349            }
350            let tag = recording.next;
351            if recording.node.is_null() {
352                recording.head = tag;
353            } else {
354                *recording.node = (recording.node_words << 24) | (tag as u32 & 0x00FF_FFFF);
355            }
356            recording.node = tag;
357            recording.node_words = 0;
358            recording.next = tag.add(1);
359        } else if recording.next >= recording.end {
360            recording.overflowed = true;
361            return;
362        }
363        *recording.next = word;
364        recording.next = recording.next.add(1);
365        recording.node_words += 1;
366    }
367}
368
369/// Register the function that makes direct GPU access safe while a queued
370/// frame may be walking (`psx_rt::present` registers its `wait_idle`). It
371/// runs once, on the first direct command or display-control write or GPU
372/// DMA address store after [`arm_direct_access_guard`], and is then
373/// disarmed.
374pub fn set_direct_access_guard(guard: fn()) {
375    // SAFETY: a volatile write of this module's static.
376    unsafe { write_volatile(addr_of_mut!(GUARD), Some(guard)) };
377}
378
379/// Arm the guard: the GPU may be busy with work the CPU handed off, so the
380/// next direct access must wait for it first.
381pub fn arm_direct_access_guard() {
382    // SAFETY: a volatile write of a `bool` static owned by this module.
383    unsafe { write_volatile(addr_of_mut!(GUARD_ARMED), true) };
384    refresh_slow();
385}
386
387/// Run and disarm the guard if it is armed. Direct-access paths outside the
388/// port writes (GPU DMA setup) call it first.
389#[inline]
390pub fn run_direct_access_guard() {
391    // SAFETY: volatile accesses to this module's statics.
392    let armed = unsafe { read_volatile(addr_of!(GUARD_ARMED)) };
393    if armed {
394        run_armed_guard();
395    }
396}
397
398/// Disarm and run the guard: the rare half of [`run_direct_access_guard`],
399/// kept out of line so each GPU DMA start carries only the flag test.
400#[cold]
401#[inline(never)]
402fn run_armed_guard() {
403    // SAFETY: volatile accesses to this module's statics.
404    unsafe { write_volatile(addr_of_mut!(GUARD_ARMED), false) };
405    refresh_slow();
406    // SAFETY: as above.
407    if let Some(guard) = unsafe { read_volatile(addr_of!(GUARD)) } {
408        guard();
409    }
410}
411
412#[cfg(test)]
413mod tests {
414    extern crate std;
415
416    use super::super::{wait_command_ready, write_command};
417    use super::*;
418    use std::sync::{Mutex, MutexGuard};
419
420    /// The recording state is one global, so tests that touch it take turns.
421    fn serial() -> MutexGuard<'static, ()> {
422        static LOCK: Mutex<()> = Mutex::new(());
423        LOCK.lock().unwrap_or_else(|poisoned| poisoned.into_inner())
424    }
425
426    /// Words land in nodes of at most 16 with tags linking each node to the
427    /// next and the last ending the list; waits do not block; an overflow
428    /// discards the whole recording; a paused stretch bypasses it.
429    #[test]
430    fn recording_lays_out_sixteen_word_nodes() {
431        let _serial = serial();
432        let mut buffer = [0u32; 64];
433        // Every access goes through this one pointer: a reference to
434        // `buffer` taken while the recording's pointers are in use would
435        // retire them (Tree Borrows).
436        let words = buffer.as_mut_ptr();
437        // SAFETY: in bounds of `buffer`, which outlives this closure's uses.
438        let word = |index: usize| unsafe { words.add(index).read() };
439        // SAFETY: `buffer` outlives the recording and is never walked.
440        let active = unsafe { start_recording_raw(words, 64) };
441        assert!(is_recording());
442        for word in 0..40u32 {
443            wait_command_ready();
444            write_command(0x1000 + word);
445        }
446        let recording = active.end().expect("fits").expect("words recorded");
447        assert!(!is_recording());
448        let base = words as u32;
449        assert_eq!(recording.head() as u32, base);
450        assert_eq!(word(0) >> 24, 16);
451        assert_eq!(word(0) & 0x00FF_FFFF, (base + 17 * 4) & 0x00FF_FFFF);
452        assert_eq!([word(1), word(2)], [0x1000, 0x1001]);
453        assert_eq!(word(17) >> 24, 16);
454        assert_eq!(word(34), (8 << 24) | 0x00FF_FFFF);
455        assert_eq!(word(42), 0x1000 + 39);
456        assert_eq!(recording.len_words(), 43);
457        // SAFETY: `buffer` is live and never walked.
458        unsafe { recording.link_to(0x8001_2340 as *const u32) };
459        assert_eq!(word(34), (8 << 24) | 0x0001_2340);
460
461        let mut small = [0u32; 8];
462        // SAFETY: as above.
463        let active = unsafe { start_recording_raw(small.as_mut_ptr(), small.len()) };
464        for word in 0..10u32 {
465            write_command(word);
466        }
467        assert_eq!(active.end().unwrap_err(), RecordingOverflow);
468
469        let mut empty = [0u32; 4];
470        // SAFETY: as above.
471        let active = unsafe { start_recording_raw(empty.as_mut_ptr(), empty.len()) };
472        assert!(active.end().expect("no overflow").is_none());
473
474        let mut resumed = [0u32; 8];
475        // SAFETY: as above.
476        let active = unsafe { start_recording_raw(resumed.as_mut_ptr(), resumed.len()) };
477        write_command(0xAA);
478        {
479            let _pause = pause_recording();
480            assert!(!is_recording());
481        }
482        assert!(is_recording());
483        write_command(0xBB);
484        let recording = active.end().expect("fits").expect("words recorded");
485        assert_eq!(recording.len_words(), 3);
486        assert_eq!(&resumed[1..3], &[0xAA, 0xBB]);
487        assert!(!is_slow());
488    }
489
490    /// Starts a recording on a stack buffer, pauses it, ends it while
491    /// paused, and hands back the pause after the buffer is gone.
492    #[inline(never)]
493    fn pause_outliving_its_recording() -> RecordingPause {
494        let mut buffer = [0u32; 8];
495        // SAFETY: the recording is ended below, before `buffer` goes, and it
496        // is never walked.
497        let active = unsafe { start_recording_raw(buffer.as_mut_ptr(), buffer.len()) };
498        write_command(0xCAFE);
499        let pause = pause_recording();
500        assert!(active.end().expect("fits").is_some());
501        pause
502    }
503
504    /// io-01: dropping a pause taken before its recording ended reopened
505    /// the closed recording, after which a safe `write_command` recorded
506    /// into the dead buffer. The closed recording now stays closed.
507    #[test]
508    fn a_pause_outliving_its_recording_does_not_reopen_it() {
509        let _serial = serial();
510        drop(pause_outliving_its_recording());
511        assert!(!is_recording());
512        assert!(!is_slow());
513    }
514
515    /// Dropping the guard without `end` closes the recording, and a pause
516    /// that belongs to an older recording never resumes a newer one.
517    #[test]
518    fn dropping_the_guard_closes_and_old_pauses_stay_inert() {
519        let _serial = serial();
520        let mut first = [0u32; 8];
521        // SAFETY: both recordings end inside this test, before their buffers
522        // go, and neither is walked.
523        let active = unsafe { start_recording_raw(first.as_mut_ptr(), first.len()) };
524        let pause = pause_recording();
525        drop(active);
526        assert!(!is_recording());
527
528        let mut second = [0u32; 8];
529        // SAFETY: as above.
530        let active = unsafe { start_recording_raw(second.as_mut_ptr(), second.len()) };
531        let inner = pause_recording();
532        drop(pause);
533        assert!(!is_recording(), "an older pause must not resume");
534        drop(inner);
535        assert!(is_recording());
536        write_command(0x55);
537        let recording = active.end().expect("fits").expect("words recorded");
538        assert_eq!(recording.len_words(), 2);
539        assert_eq!(second[1], 0x55);
540        assert!(!is_slow());
541    }
542}