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

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}