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}