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}