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

psx_gpu/
ordered.rs

1//! Painter-order command streaming with bounded GPU DMA nodes.
2//!
3//! Each complete GP0 packet stays together in one node of at most
4//! [`NODE_PAYLOAD_WORDS`] payload words after its DMA tag. Submitted storage
5//! remains immutable until DMA completes. The backing slice is static, so
6//! moving or forgetting the stream cannot invalidate an in-flight DMA
7//! address. No allocation is used.
8
9use core::marker::PhantomData;
10use core::ptr::NonNull;
11use psx_io::periph::GpuDma;
12
13/// Most payload words the stream packs into one node, after its tag.
14///
15/// One word under [`crate::chain::MAX_NODE_WORDS`], the node length the
16/// hardware-tests v1.24 drawing case points to. The spare word is a margin,
17/// not a second measured limit.
18pub const NODE_PAYLOAD_WORDS: usize = 15;
19const _: () = assert!(NODE_PAYLOAD_WORDS <= crate::chain::MAX_NODE_WORDS);
20const END: u32 = psx_hw::dma::linked_list::END;
21
22/// DMA operations required by an ordered stream.
23///
24/// The default implementation uses the SDK's GPU channel ownership and bounded
25/// waits. Alternate implementations can test the transport without MMIO.
26///
27/// # Safety
28///
29/// [`OrderedCommandStream`] is a safe API that rewrites node storage as soon
30/// as this transport says the hardware is done with it, so an implementation
31/// must tell the truth:
32///
33/// * after [`Self::wait`] returns, nothing reads storage passed to an earlier
34///   [`Self::submit`] (a timed-out transfer must be stopped, not abandoned);
35/// * [`Self::is_busy`] returns `true` while a submitted list may still be read,
36///   because a `false` lets the stream start the next list at once;
37/// * [`Self::submit`] reads only the linked nodes it was given.
38pub unsafe trait CommandStreamDma {
39    /// Whether the previous GPU DMA transfer is still reading its storage.
40    fn is_busy(&mut self) -> bool;
41    /// Start a valid linked list after the channel becomes idle.
42    ///
43    /// # Safety
44    /// `head` and every linked node must remain valid and immutable until
45    /// `wait` returns. Tags must describe aligned RAM addresses and lengths.
46    unsafe fn submit(&mut self, head: *const u32);
47    /// Wait until the DMA engine no longer reads the submitted storage.
48    fn wait(&mut self);
49    /// Wait until previously submitted GPU drawing has finished.
50    #[doc(alias = "DrawSync")]
51    fn wait_idle(&mut self);
52}
53
54// SAFETY: the token is the channel-2 transport. `is_busy` reads the
55// channel's CHCR start bit, which stays set until the walk ends. `wait`
56// returns only after that bit clears or after it aborts the channel (and
57// resets the GPU) on timeout, so no walk outlives it.
58unsafe impl CommandStreamDma for GpuDma {
59    #[inline]
60    fn is_busy(&mut self) -> bool {
61        psx_io::dma::is_busy(psx_io::dma::Channel::Gpu)
62    }
63    #[inline]
64    unsafe fn submit(&mut self, head: *const u32) {
65        // SAFETY: forwarded from this method's own contract.
66        unsafe { crate::chain::start_walk(head) };
67    }
68    #[inline]
69    fn wait(&mut self) {
70        crate::chain::wait_walk();
71    }
72    #[inline]
73    fn wait_idle(&mut self) {
74        crate::wait_idle_impl();
75    }
76}
77
78/// Forward, incremental GPU command list over caller-owned static storage.
79///
80/// Append complete packets with [`Self::push_packet`], then [`Self::submit`]
81/// to overlap CPU work with GPU drawing. Call [`Self::flush`] before
82/// immediate GP0 drawing, VRAM uploads, or framebuffer presentation. Capacity
83/// exhaustion performs that same synchronization before reusing storage.
84/// Dropping the stream waits for in-flight DMA and discards unsent commands.
85///
86/// To present through psx-rt's queued flip, arm the draw-done flag
87/// ([`crate::Gpu::arm_draw_done`] on the transport [`Self::flush`] returns)
88/// before the frame's first packet (nodes can start walking as soon as they
89/// close) and end the frame with `push_packet([gp0::REQUEST_IRQ])` and
90/// [`Self::submit`]; see [`crate::is_draw_done`].
91pub struct OrderedCommandStream<D: CommandStreamDma = GpuDma> {
92    // One base pointer for the whole `'static` buffer rather than the
93    // `&mut [u32]` itself: every access goes through it, so writing the open
94    // node never reborrows the nodes a walk is still reading.
95    words: NonNull<u32>,
96    capacity: usize,
97    _words: PhantomData<&'static mut [u32]>,
98    len: usize,
99    head: usize,
100    sent: usize,
101    submitted: bool,
102    dma: D,
103}
104
105// SAFETY: the stream owns its `'static` buffer exclusively, as the
106// `&'static mut [u32]` it was built from did, so moving it to another
107// context is as safe as moving that reference and the transport.
108unsafe impl<D: CommandStreamDma + Send> Send for OrderedCommandStream<D> {}
109// SAFETY: `&self` methods read no buffer word, so shared references are as
110// safe as for the `&'static mut [u32]` it replaces.
111unsafe impl<D: CommandStreamDma + Sync> Sync for OrderedCommandStream<D> {}
112
113impl OrderedCommandStream {}
114
115impl<D: CommandStreamDma> OrderedCommandStream<D> {
116    /// A stream over `words`, at least 17 word-aligned RAM words, driven by
117    /// `dma`: the `GpuDma` token on the console, a test double on the host.
118    pub fn with_dma(words: &'static mut [u32], dma: D) -> Self {
119        assert!(
120            words.len() >= NODE_PAYLOAD_WORDS + 2,
121            "ordered stream needs a packet, tag, and spare tag"
122        );
123        words[0] = END;
124        Self {
125            capacity: words.len(),
126            words: NonNull::from(words).cast(),
127            _words: PhantomData,
128            len: 1,
129            head: 0,
130            sent: 0,
131            submitted: false,
132            dma,
133        }
134    }
135
136    /// Word `index` of the buffer.
137    ///
138    /// # Panics
139    ///
140    /// If `index` is past the buffer, as slice indexing would.
141    #[inline(always)]
142    fn word_mut(&mut self, index: usize) -> &mut u32 {
143        assert!(index < self.capacity);
144        // SAFETY: `index` is inside the buffer this stream owns for `'static`;
145        // the borrow covers this one word, which no walk is reading (callers
146        // only touch the open node and words past it).
147        unsafe { &mut *self.words.as_ptr().add(index) }
148    }
149
150    #[inline]
151    fn open_node(&mut self) {
152        self.head = self.len;
153        *self.word_mut(self.head) = END;
154        self.len += 1;
155    }
156
157    #[inline]
158    fn close_node(&mut self, next: Option<usize>) {
159        let payload = (self.len - self.head - 1) as u32;
160        let link = next.map_or(END, |i| {
161            self.words.as_ptr().wrapping_add(i).expose_provenance() as u32 & END
162        });
163        // Finish the tag before checking the channel; the shared submit helper
164        // supplies the compiler release barrier before DMA starts.
165        // SAFETY: `head` indexes a tag word written by `open_node`, so it is
166        // below `len` and inside `words`.
167        unsafe {
168            core::ptr::write_volatile(self.words.as_ptr().add(self.head), payload << 24 | link);
169        }
170        if !self.dma.is_busy() {
171            self.kick_pending();
172        }
173    }
174
175    fn kick_pending(&mut self) {
176        if self.sent > self.head {
177            return;
178        }
179        let tag = self.word_mut(self.head);
180        *tag = *tag & 0xff00_0000 | END;
181        // Only closed nodes are visible. Future appends start beyond len,
182        // never in the region now owned by DMA.
183        // SAFETY: `sent..len` holds closed nodes whose tags link only inside
184        // `words`, which is `'static`. The stream writes none of them again
185        // until `dma.wait()` returns (flush and drop both wait first).
186        unsafe {
187            self.dma.submit(self.words.as_ptr().add(self.sent));
188        }
189        self.sent = self.len;
190        self.submitted = true;
191    }
192
193    #[inline(always)]
194    fn reserve(&mut self, count: usize) {
195        assert!(
196            count > 0 && count <= NODE_PAYLOAD_WORDS,
197            "ordered GP0 packet must contain 1..=15 words"
198        );
199        // Reserve the next tag too, even if this packet fits the current node.
200        // Otherwise an exactly full arena makes submit's open_node overflow.
201        if self.len + count + 1 > self.capacity {
202            self.reuse_full_buffer();
203        }
204        if self.len - self.head - 1 + count > NODE_PAYLOAD_WORDS {
205            if self.len + count + 2 > self.capacity {
206                self.reuse_full_buffer();
207            } else {
208                self.close_node(Some(self.len));
209                self.open_node();
210            }
211        }
212    }
213
214    // Capacity exhaustion is rare for normal frame-sized storage. Keep its
215    // complete DMA/GPU drain out of every inlined packet emitter, where it
216    // otherwise increases live values and stack spills on the R3000.
217    #[cold]
218    #[inline(never)]
219    fn reuse_full_buffer(&mut self) {
220        self.flush();
221    }
222
223    /// Append one complete GP0 packet in painter order.
224    ///
225    /// `N` must be 1..=15. Packets are never split across DMA nodes.
226    #[inline(always)]
227    pub fn push_packet<const N: usize>(&mut self, words: [u32; N]) {
228        self.reserve(N);
229        let mut len = self.len;
230        for word in words {
231            // reserve(N) checked the whole packet plus a spare tag, including
232            // any capacity-driven reset. This index is therefore in bounds;
233            // repeating a slice check per GP0 word bloats hot draw loops.
234            // SAFETY: as above, `len < self.capacity` for every word of the
235            // packet.
236            unsafe {
237                self.words.as_ptr().add(len).write(word);
238            }
239            len += 1;
240        }
241        self.len = len;
242    }
243
244    /// Append a small A0 upload between surrounding draw commands.
245    ///
246    /// Coordinates and dimensions use VRAM halfwords. The rectangle must
247    /// match `pixels` and fit one node (at most 24 pixels). Odd pixel counts
248    /// have a zero-padded final halfword. Large asset uploads should use the
249    /// VRAM upload helpers after [`Self::flush`].
250    pub fn push_upload(&mut self, x: u16, y: u16, width: u16, height: u16, pixels: &[u16]) {
251        assert!(width > 0 && height > 0);
252        assert_eq!(pixels.len(), usize::from(width) * usize::from(height));
253        let count = 3 + pixels.len().div_ceil(2);
254        self.reserve(count);
255        *self.word_mut(self.len) = psx_hw::gpu::gp0::COPY_CPU_TO_VRAM;
256        *self.word_mut(self.len + 1) = (u32::from(y) << 16) | u32::from(x);
257        *self.word_mut(self.len + 2) = (u32::from(height) << 16) | u32::from(width);
258        self.len += 3;
259        for pair in pixels.chunks(2) {
260            *self.word_mut(self.len) =
261                u32::from(pair[0]) | (u32::from(*pair.get(1).unwrap_or(&0)) << 16);
262            self.len += 1;
263        }
264    }
265
266    /// Submit all pending commands, retaining storage until completion.
267    pub fn submit(&mut self) {
268        if self.len <= self.head + 1 {
269            return;
270        }
271        self.close_node(None);
272        if self.sent <= self.head {
273            self.dma.wait();
274            self.kick_pending();
275        }
276        self.open_node();
277    }
278
279    /// Submit, wait for DMA and GPU, and reset the buffer for reuse.
280    ///
281    /// Returns the transport, idle, for work that must not overlap the
282    /// stream: `Gpu::from_dma_mut(stream.flush())` draws immediately or
283    /// arms the draw-done flag before the next frame's first packet.
284    #[doc(alias = "DrawSync")]
285    pub fn flush(&mut self) -> &mut D {
286        self.submit();
287        if self.submitted {
288            self.dma.wait();
289            self.submitted = false;
290        }
291        self.len = 1;
292        self.head = 0;
293        self.sent = 0;
294        *self.word_mut(0) = END;
295        self.dma.wait_idle();
296        &mut self.dma
297    }
298
299    /// Flush, then hand back the buffer and the transport.
300    pub fn release(self) -> (&'static mut [u32], D) {
301        let mut this = core::mem::ManuallyDrop::new(self);
302        this.flush();
303        // SAFETY: `this` is never dropped, so the transport is read out of
304        // it exactly once; the buffer is the `&'static mut [u32]` the stream
305        // was built from, rebuilt from its base and length now that nothing
306        // walks it.
307        unsafe {
308            (
309                core::slice::from_raw_parts_mut(this.words.as_ptr(), this.capacity),
310                core::ptr::read(&this.dma),
311            )
312        }
313    }
314}
315
316/// Shows the buffer's fill state, not its words.
317impl<D: CommandStreamDma> core::fmt::Debug for OrderedCommandStream<D> {
318    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
319        f.debug_struct("OrderedCommandStream")
320            .field("capacity", &self.capacity)
321            .field("len", &self.len)
322            .field("sent", &self.sent)
323            .field("submitted", &self.submitted)
324            .finish_non_exhaustive()
325    }
326}
327
328impl<D: CommandStreamDma> Drop for OrderedCommandStream<D> {
329    fn drop(&mut self) {
330        if self.submitted {
331            self.dma.wait();
332        }
333    }
334}