psx_io/gpu.rs
1//! GPU MMIO: `GP0`, `GP1`, `GPUREAD`, `GPUSTAT`.
2//!
3//! Thin wrappers over [`crate::read_u32`] / [`crate::write_u32`] that use
4//! the register addresses from `psx-hw`. Each helper commits exactly
5//! one MMIO access; higher-level SDK code composes them into commands.
6
7use psx_hw::gpu::{GpuStat, GP0, GP1, GPUREAD, GPUSTAT};
8
9#[cfg(any(feature = "present-queue", test))]
10mod handoff;
11#[cfg(any(feature = "present-queue", test))]
12pub use handoff::{
13 arm_direct_access_guard, is_recording, run_direct_access_guard, set_direct_access_guard,
14 start_recording_raw, ActiveRecording, CommandRecording, RecordingOverflow,
15 RECORDING_NODE_WORDS,
16};
17#[cfg(any(feature = "present-queue", test))]
18pub use handoff::{pause_recording, RecordingPause};
19
20/// Returned by [`pause_recording`]. Without the `present-queue` feature
21/// nothing can record, so it holds nothing and does nothing.
22#[cfg(not(any(feature = "present-queue", test)))]
23#[must_use = "the recording resumes as soon as this is dropped"]
24#[derive(Debug)]
25pub struct RecordingPause(());
26
27/// Pause any open command recording until the returned guard is dropped,
28/// so the command writes in between reach the port; psx-vram's uploads
29/// take one. Without the `present-queue` feature it compiles to nothing.
30#[cfg(not(any(feature = "present-queue", test)))]
31#[inline(always)]
32pub fn pause_recording() -> RecordingPause {
33 RecordingPause(())
34}
35
36/// Run the direct-access guard if a queued frame armed it. Without the
37/// `present-queue` feature nothing can arm it, so it compiles to nothing.
38#[cfg(not(any(feature = "present-queue", test)))]
39#[inline(always)]
40pub fn run_direct_access_guard() {}
41
42/// Push a drawing or VRAM command word, or one of its parameters, to the
43/// GPU's command port (`GP0`). Pairs with the word builders in
44/// `psx_hw::gpu::gp0`: `write_command(gp0::draw_mode(...))`.
45///
46/// With the `present-queue` feature, the word goes into the open command
47/// recording instead, if there is one, and an armed direct-access guard runs
48/// before the port write.
49#[doc(alias = "GP0")]
50#[inline(always)]
51pub fn write_command(word: u32) {
52 #[cfg(any(feature = "present-queue", test))]
53 if handoff::is_slow() {
54 handoff::write_command_slow(word);
55 return;
56 }
57 // SAFETY: GP0 (`psx_hw::gpu::GP0`) is the GPU's aligned 32-bit command/data port on every PS1. Any
58 // word is a legal write: the GPU parses it as a command or parameter, with no effect on
59 // CPU-visible memory.
60 unsafe { crate::write_u32(GP0, word) }
61}
62
63/// [`write_command`] straight to the port: never recorded, never behind the
64/// direct-access guard. For code that has already made the port safe, such
65/// as the present queue's stall recovery, which the guard itself runs.
66/// Without the `present-queue` feature it is the same store as [`write_command`].
67#[inline(always)]
68pub fn write_command_unguarded(word: u32) {
69 // SAFETY: GP0 (`psx_hw::gpu::GP0`) is the GPU's aligned 32-bit command/data port on every PS1. Any
70 // word is a legal write: the GPU parses it as a command or parameter, with no effect on
71 // CPU-visible memory.
72 unsafe { crate::write_u32(GP0, word) }
73}
74
75/// Write a display-control command (reset, display mode, display area,
76/// DMA direction) to the GPU's control port (`GP1`).
77///
78/// With the `present-queue` feature an armed direct-access guard runs first,
79/// unless a command recording is open (display control is never recorded).
80#[doc(alias = "GP1")]
81#[inline(always)]
82pub fn write_display_control(word: u32) {
83 #[cfg(any(feature = "present-queue", test))]
84 if handoff::is_slow() {
85 handoff::write_display_control_slow(word);
86 return;
87 }
88 // SAFETY: GP1 (`psx_hw::gpu::GP1`) is the GPU's aligned 32-bit control port on every PS1; any word is
89 // a legal write and only changes GPU state.
90 unsafe { crate::write_u32(GP1, word) }
91}
92
93/// [`write_display_control`] straight to the port, past the direct-access
94/// guard; see [`write_command_unguarded`].
95#[inline(always)]
96pub fn write_display_control_unguarded(word: u32) {
97 // SAFETY: GP1 (`psx_hw::gpu::GP1`) is the GPU's aligned 32-bit control port on every PS1; any word is
98 // a legal write and only changes GPU state.
99 unsafe { crate::write_u32(GP1, word) }
100}
101
102/// Read the GPU status register.
103#[doc(alias = "GPUSTAT")]
104#[inline(always)]
105pub fn status() -> GpuStat {
106 // SAFETY: GPUSTAT (`psx_hw::gpu::GP1` on read) is the GPU's aligned 32-bit status register; reading it
107 // has no side effects.
108 GpuStat::from_bits_retain(unsafe { crate::read_u32(GPUSTAT) })
109}
110
111/// Read the GPU's response latch: VRAM-to-CPU transfer data or a GP1(10h)
112/// info reply.
113#[doc(alias = "GPUREAD")]
114#[inline(always)]
115pub fn read_data() -> u32 {
116 // SAFETY: GPUREAD (`psx_hw::gpu::GP0` on read) is the GPU's aligned 32-bit response latch. A read may
117 // advance a VRAM-to-CPU transfer, which is its purpose, and touches no CPU memory.
118 unsafe { crate::read_u32(GPUREAD) }
119}
120
121/// Spin until the GPU is ready to accept a new command word.
122///
123/// Polls `GPUSTAT.READY_CMD`. On real hardware this bit is briefly
124/// cleared while the GPU is busy ingesting a multi-word packet; our
125/// emulator forces it on, so the loop is essentially a single read.
126/// Do not use this between GP0(0xA0) image payload words: after the
127/// transfer setup packet, the GPU is waiting for data, not a new
128/// normal command.
129#[inline]
130pub fn wait_command_ready() {
131 wait_ready(GpuStat::READY_CMD);
132}
133
134/// Spin budget for one GPUSTAT ready-wait. Long enough for the slowest
135/// legitimate primitive, short enough that a stuck GPU hands control
136/// back well inside a frame.
137pub const READY_SPINS: u32 = 500_000;
138
139/// Bounded ready-wait with recovery.
140///
141/// A GPU left mid-command never re-asserts its ready bits, so a bare
142/// `while !ready {}` hangs forever. That is not hypothetical: aborting a
143/// wedged linked-list DMA (see `psx_io::dma::abort`) stops the walker
144/// partway through a packet, and the GPU then sits waiting for command
145/// words that will never arrive. On timeout, GP1(01h) resets the command
146/// buffer, which is the documented way to discard that partial command
147/// and make the GPU accept work again.
148fn wait_ready(flag: GpuStat) {
149 #[cfg(any(feature = "present-queue", test))]
150 if handoff::is_recording() {
151 return;
152 }
153 let mut spins = 0u32;
154 while !status().contains(flag) {
155 if spins >= READY_SPINS {
156 count_command_reset();
157 write_display_control(psx_hw::gpu::gp1::RESET_CMD_BUFFER);
158 return;
159 }
160 spins += 1;
161 }
162}
163
164/// GP1(01h) resets [`wait_command_ready`] and [`wait_dma_ready`] have issued
165/// after a ready bit stayed low for [`READY_SPINS`] polls.
166static mut COMMAND_RESETS: u32 = 0;
167
168#[cold]
169fn count_command_reset() {
170 // SAFETY: volatile aligned accesses to this private static; only this
171 // function writes it and the program is single threaded (no handler
172 // touches it).
173 unsafe {
174 let count = core::ptr::addr_of_mut!(COMMAND_RESETS);
175 count.write_volatile(count.read_volatile().wrapping_add(1));
176 }
177}
178
179/// How many times a ready-wait has timed out and reset the GPU's command
180/// buffer since boot: a count of GPU hangs the SDK recovered from.
181pub fn command_reset_count() -> u32 {
182 // SAFETY: a volatile aligned read of a private static.
183 unsafe { core::ptr::addr_of!(COMMAND_RESETS).read_volatile() }
184}
185
186/// Spin until the GPU can start a DMA block transfer.
187#[inline]
188pub fn wait_dma_ready() {
189 wait_ready(GpuStat::READY_DMA_RECV);
190}
191
192/// Poll command readiness without resetting the GPU on timeout.
193/// Reads once, then retries at most `spin_limit` times.
194#[inline]
195pub fn try_wait_command_ready(spin_limit: u32) -> bool {
196 try_wait_ready(GpuStat::READY_CMD, spin_limit)
197}
198/// Poll DMA receive readiness without resetting the GPU on timeout.
199#[inline]
200pub fn try_wait_dma_ready(spin_limit: u32) -> bool {
201 try_wait_ready(GpuStat::READY_DMA_RECV, spin_limit)
202}
203fn try_wait_ready(flag: GpuStat, spin_limit: u32) -> bool {
204 #[cfg(any(feature = "present-queue", test))]
205 if handoff::is_recording() {
206 return true;
207 }
208 poll_ready(spin_limit, || status().contains(flag))
209}
210
211/// Renamed to [`write_command`].
212#[deprecated(note = "renamed to `write_command`")]
213#[inline(always)]
214pub fn write_gp0(word: u32) {
215 write_command(word)
216}
217
218/// Renamed to [`write_display_control`].
219#[deprecated(note = "renamed to `write_display_control`")]
220#[inline(always)]
221pub fn write_gp1(word: u32) {
222 write_display_control(word)
223}
224
225/// Renamed to [`status`].
226#[deprecated(note = "renamed to `status`")]
227#[inline(always)]
228pub fn gpustat() -> GpuStat {
229 status()
230}
231
232/// Renamed to [`wait_command_ready`].
233#[deprecated(note = "renamed to `wait_command_ready`")]
234#[inline(always)]
235pub fn wait_cmd_ready() {
236 wait_command_ready()
237}
238
239fn poll_ready(spin_limit: u32, mut ready: impl FnMut() -> bool) -> bool {
240 let mut remaining = spin_limit;
241 loop {
242 if ready() {
243 return true;
244 }
245 if remaining == 0 {
246 return false;
247 }
248 remaining -= 1;
249 }
250}
251#[cfg(test)]
252mod tests {
253 use super::*;
254 #[test]
255 fn readiness_budget_includes_initial_probe() {
256 for limit in 0..5 {
257 let mut reads = 0;
258 assert!(!poll_ready(limit, || {
259 reads += 1;
260 false
261 }));
262 assert_eq!(reads, limit + 1);
263 }
264 let mut reads = 0;
265 assert!(poll_ready(2, || {
266 reads += 1;
267 reads == 3
268 }));
269 assert_eq!(reads, 3);
270 }
271}