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}