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

psx_rt/
present.rs

1//! VBlank-kicked presentation: a one-frame-deep queue between the CPU and the
2//! GPU. Built with the `present-queue` feature.
3//!
4//! A game publishes each finished frame's DMA chain together with the
5//! GP1(05h) word that shows the frame before it, and returns at once.
6//! psx-rt's VBlank handler kicks the chain on the first edge where the
7//! previous chain's closing GP0(1Fh) has run (GPUSTAT bit 24) and DMA channel
8//! 2 is idle: it flips the display to the finished frame, acknowledges the
9//! flag with GP1(02h), and starts the new walk. A flip therefore lands only on
10//! a blank edge and only on a fully drawn frame, and the CPU builds the next
11//! frame while the GPU draws this one. The CPU waits only when it is a whole
12//! frame ahead, and before reusing memory a walk may still read.
13//!
14//! Ported from quake-psx's `platform.rs` (3ca915c, 16e27a9: E1M1 chain bench
15//! +25.5%, monster route +46.1% over its blocking present) so every game
16//! shares one queue.
17//!
18//! # Protocol
19//!
20//! 1. Call [`start`] once, after [`crate::interrupts::install_vblank_counter`],
21//!    with nothing queued and channel 2 idle. It raises the draw-done flag so
22//!    the first published frame finds it set.
23//! 2. Every published chain must end on GP0(1Fh) (`psx_gpu::DRAW_DONE_NODE`,
24//!    `OrderingTable::end_with_draw_done`), and must carry its own draw
25//!    area, offset and clear as leading packets: the handler writes only
26//!    GP1. `psx_io::gpu::start_recording_raw` records those from the usual
27//!    immediate calls.
28//! 3. Before [`publish_raw`], call [`wait_slot_empty`]. Before rebuilding
29//!    memory that the frame before the last published one used, call
30//!    [`wait_arena_free`].
31//! 4. Direct command or display-control writes and GPU DMA starts are safe
32//!    at any time: [`publish_raw`] arms psx-io's direct-access guard, so the
33//!    first such access after it runs [`wait_idle`]. Code that only records
34//!    keeps the overlap.
35//!
36//! Keep interrupt source 1 (GPU) masked in `I_MASK`: GP0(1Fh) raises it and
37//! the handler, which owns VBlank only, would acknowledge it as a stray. The handler returns past a GTE
38//! command an edge interrupted (8055e87f6), so publishing from GTE-heavy code
39//! is safe.
40//!
41//! # With `psx_gpu::frame`
42//!
43//! A frame kicked from the CPU (`psx_gpu::frame::OtFrame::submit`,
44//! `FrameStorage::draw_async`, or any other GPU DMA start) first runs the
45//! guard, so it starts only once every published frame has been kicked and
46//! has finished drawing: the channel is never kicked twice, and no queued
47//! walk reads memory the CPU path is about to reuse. The reverse also holds:
48//! the handler never kicks while a CPU-kicked walk runs, since it needs
49//! channel 2 idle. A queued flip waits on GPUSTAT bit 24, which only chains
50//! ending in GP0(1Fh) raise, so a CPU-kicked chain should end the same way
51//! when a published frame follows it.
52
53use core::ptr::{addr_of, addr_of_mut, read_volatile, write_volatile};
54
55use crate::interrupts::{
56    __psx_rt_present_head, __psx_rt_present_kick_count, __psx_rt_present_skip_count,
57};
58use crate::wait;
59
60/// Edges a published frame may wait before the CPU treats the chain ahead of
61/// it as stalled (its walk wedged, or it lost its GP0(1Fh)).
62pub const STALL_VBLANKS: u32 = 8;
63
64static mut RECOVERY_COUNT: u32 = 0;
65
66/// Counters for telemetry and A/B reports.
67#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
68pub struct PresentStats {
69    /// Chains the handler has kicked.
70    pub kick_count: u32,
71    /// Edges on which a published chain had to wait for the one before it.
72    pub skip_count: u32,
73    /// Stalled chains the CPU stopped by hand (see [`STALL_VBLANKS`]).
74    pub recovery_count: u32,
75}
76
77/// Register the direct-access guard and raise the draw-done flag once, so
78/// the first published frame's edge finds it set. Nothing may be queued and
79/// channel 2 must be idle.
80pub fn start() {
81    psx_io::gpu::set_direct_access_guard(wait_idle);
82    #[cfg(target_arch = "mips")]
83    {
84        psx_io::gpu::wait_command_ready();
85        psx_io::gpu::write_command(psx_hw::gpu::gp0::REQUEST_IRQ);
86    }
87}
88
89/// True while a published chain is waiting for the handler to kick it.
90#[inline]
91pub fn is_slot_full() -> bool {
92    // SAFETY: a volatile aligned u32 read through a raw pointer; the VBlank
93    // handler is the only other accessor and an aligned word load cannot
94    // tear.
95    unsafe { read_volatile(addr_of!(__psx_rt_present_head)) != 0 }
96}
97
98#[inline]
99fn is_channel_busy() -> bool {
100    #[cfg(target_arch = "mips")]
101    return psx_io::dma::is_busy(psx_io::dma::Channel::Gpu);
102    #[cfg(not(target_arch = "mips"))]
103    false
104}
105
106#[inline]
107fn is_draw_done() -> bool {
108    psx_io::gpu::status().contains(psx_hw::gpu::GpuStat::IRQ1)
109}
110
111/// Hand one chain, and the GP1(05h) word that shows the frame before it, to
112/// the VBlank handler. Pass `display` 0 when nothing is on screen yet.
113///
114/// # Safety
115///
116/// The slot must be empty ([`wait_slot_empty`]). `head` must be the first
117/// node tag of a linked-list chain ending on GP0(1Fh), every node at most
118/// 16 payload words, and the chain and every packet it links must stay live
119/// and unmodified until its walk has finished ([`wait_arena_free`] after
120/// the next publish, or [`wait_idle`]).
121#[inline]
122pub unsafe fn publish_raw(head: *const u32, display: u32) {
123    debug_assert!(!is_slot_full());
124    #[cfg(target_arch = "mips")]
125    {
126        // Keep the chain's packet stores ahead of the slot write the handler
127        // reads.
128        compiler_barrier();
129        // SAFETY: volatile aligned u32 stores through raw pointers. The slot
130        // is empty (this fn's `# Safety`), so the handler ignores both words
131        // until the head store, which comes last.
132        unsafe {
133            write_volatile(
134                addr_of_mut!(crate::interrupts::__psx_rt_present_display),
135                display,
136            );
137            write_volatile(addr_of_mut!(__psx_rt_present_head), head as u32);
138        }
139    }
140    #[cfg(not(target_arch = "mips"))]
141    let _ = (head, display);
142    psx_io::gpu::arm_direct_access_guard();
143}
144
145/// Block until the handler has kicked the published frame. Spins only when
146/// the CPU is a whole frame ahead of presentation. Not inlined, so a profile
147/// can tell the spin from work.
148///
149/// Bounded: with no psx-rt handler running (the counter never installed, or
150/// interrupts masked) nothing kicks the frame and the VBlank stall test never
151/// fires, so the wait gives up after a few million reads and asserts in a
152/// debug build.
153#[inline(never)]
154pub fn wait_slot_empty() {
155    let start = crate::interrupts::vblank_count();
156    let done = wait::wait_while(
157        is_slot_full,
158        || is_stalled(start),
159        release_stalled_slot,
160        wait::SPIN_LIMIT,
161    );
162    debug_assert!(
163        done,
164        "present: the slot never emptied; is the VBlank handler installed?"
165    );
166}
167
168/// Wait until memory used by the frame before the most recently published
169/// one is no longer walked. Once the published frame has been kicked (slot
170/// empty), the handler saw the older frame's GP0(1Fh), so its walk had
171/// ended; while it is still queued, the only walk that can be running is the
172/// older frame's. Bounded like [`wait_slot_empty`].
173#[inline(never)]
174pub fn wait_arena_free() {
175    let start = crate::interrupts::vblank_count();
176    let done = wait::wait_while(
177        || is_slot_full() && is_channel_busy(),
178        || is_stalled(start),
179        release_stalled_slot,
180        wait::SPIN_LIMIT,
181    );
182    debug_assert!(
183        done,
184        "present: the arena never freed; is the VBlank handler installed?"
185    );
186    // Keep the rebuild's stores after the completion read.
187    compiler_barrier();
188}
189
190/// Wait until the GPU is completely idle: nothing queued, no walk running and
191/// the last chain's drawing done. The direct-access guard runs this before
192/// the first direct GPU access after a publish. Bounded like
193/// [`wait_slot_empty`]; a walk it gives up on is stopped by hand.
194#[doc(alias = "DrawSync")]
195#[inline(never)]
196pub fn wait_idle() {
197    wait_slot_empty();
198    let start = crate::interrupts::vblank_count();
199    let mut spins = 0u32;
200    while is_channel_busy() || !is_draw_done() {
201        spins += 1;
202        if is_stalled(start) || spins >= wait::SPIN_LIMIT {
203            stop_walk_and_raise();
204            break;
205        }
206    }
207    compiler_barrier();
208}
209
210/// True once [`STALL_VBLANKS`] edges have passed since `start`.
211fn is_stalled(start: u32) -> bool {
212    crate::interrupts::vblank_count().wrapping_sub(start) >= STALL_VBLANKS
213}
214
215/// The published frame has waited [`STALL_VBLANKS`] edges for the chain
216/// ahead of it. With interrupts masked, so the handler cannot kick in
217/// between, stop a walk still running the way
218/// `psx_gpu::submit_linked_list_wait` recovers, then raise the flag through
219/// the port so the next edge kicks the queued frame.
220#[cold]
221#[inline(never)]
222fn release_stalled_slot() {
223    crate::critical_section::with(|_| {
224        if is_slot_full() && !is_draw_done() {
225            stop_walk_and_raise();
226        }
227    });
228}
229
230/// Port writes bypass the direct-access guard: this runs inside it
231/// ([`wait_idle`]), and from the paired-arena fence, whose scratchpad stack
232/// stack-guard must bound without the guard's function pointer.
233#[cold]
234fn stop_walk_and_raise() {
235    #[cfg(target_arch = "mips")]
236    {
237        use psx_io::dma::{self, Channel};
238        if dma::is_busy(Channel::Gpu) && !dma::wait_done(Channel::Gpu, dma::DEFAULT_SPINS) {
239            dma::abort(Channel::Gpu);
240            psx_io::gpu::write_display_control_unguarded(psx_hw::gpu::gp1::RESET_CMD_BUFFER);
241        }
242        // `wait_command_ready`, with its timeout reset written past the guard.
243        if !psx_io::gpu::try_wait_command_ready(psx_io::gpu::READY_SPINS) {
244            psx_io::gpu::write_display_control_unguarded(psx_hw::gpu::gp1::RESET_CMD_BUFFER);
245        }
246        psx_io::gpu::write_command_unguarded(psx_hw::gpu::gp0::REQUEST_IRQ);
247    }
248    // SAFETY: volatile aligned u32 accesses to this module's private static;
249    // the program is single threaded and no handler touches it.
250    unsafe {
251        write_volatile(
252            addr_of_mut!(RECOVERY_COUNT),
253            read_volatile(addr_of!(RECOVERY_COUNT)).wrapping_add(1),
254        );
255    }
256}
257
258/// Kick, skip and recovery counts since boot.
259pub fn stats() -> PresentStats {
260    // SAFETY: volatile aligned u32 reads through raw pointers; the handler
261    // writes the first two with single word stores, which cannot tear.
262    unsafe {
263        PresentStats {
264            kick_count: read_volatile(addr_of!(__psx_rt_present_kick_count)),
265            skip_count: read_volatile(addr_of!(__psx_rt_present_skip_count)),
266            recovery_count: read_volatile(addr_of!(RECOVERY_COUNT)),
267        }
268    }
269}
270
271/// Compiler-only barrier (see `psx_io::dma`'s): the pinned MIPS-I backend
272/// lowers a compiler fence to `SYNC`, which the R3000 lacks, so the target
273/// uses an empty `asm!` with its default memory clobber.
274#[inline(always)]
275fn compiler_barrier() {
276    #[cfg(target_arch = "mips")]
277    // SAFETY: an empty asm block; it only constrains compiler ordering.
278    unsafe {
279        core::arch::asm!("", options(nostack, preserves_flags));
280    }
281    #[cfg(not(target_arch = "mips"))]
282    core::sync::atomic::compiler_fence(core::sync::atomic::Ordering::SeqCst);
283}