psx_gpu/frame.rs
1//! Borrow-checked frame building over an ordering table.
2//!
3//! The GPU's linked-list DMA reads every packet of a frame after the CPU has
4//! handed it over, so a packet must outlive the walk and must not change
5//! during it. This module states that with lifetimes instead of `unsafe`:
6//!
7//! - [`OrderingTable::frame`] clears the table and borrows it as an
8//! [`OtFrame<'f, N>`] for `'f`.
9//! - [`OtFrame::add`] takes each packet as `&'f mut P`, so the packet stays
10//! borrowed, and unmovable, for as long as the frame can be walked.
11//! - [`OtFrame::submit`] and [`OtFrame::submit_with`] wait for the walk
12//! before they return, which is what lets `'f` end.
13//! - [`PrimitiveArena`] splits caller storage into `&'f mut P` packets.
14//! - For a walk that outlives the call that kicked it (CPU frame N+1 built
15//! while the GPU walks frame N), [`FrameStorage::draw_async`] needs the
16//! table and packet storage `&'static mut` and returns an [`InFlight`]
17//! that owns them, and the [`GpuDma`] token, until [`InFlight::wait`].
18//! Forgetting it leaks the storage rather than freeing it under the walk.
19//! [`FramePair`] runs the usual two-buffer ping-pong on top.
20//!
21//! Every type here is a thin wrapper over the same stores and kicks as
22//! [`OtFrame::add_raw`] and [`crate::chain::submit_raw`].
23//!
24//! # Usage
25//!
26//! ```no_run
27//! use psx_gpu::frame::{FramePair, FrameStorage, PrimitiveArena};
28//! use psx_gpu::{ot::OrderingTable, prim::TriFlat};
29//!
30//! fn blocking(ot: &mut OrderingTable<8>, dma: &mut psx_io::periph::GpuDma) {
31//! let mut storage: [TriFlat; 4] =
32//! core::array::from_fn(|_| TriFlat::new([(0, 0); 3], 0, 0, 0));
33//! let mut frame = ot.frame();
34//! let mut arena = PrimitiveArena::new(&mut storage);
35//! if let Some(tri) = arena.push(TriFlat::new([(0, 0), (8, 0), (0, 8)], 255, 0, 0)) {
36//! frame.add(1, tri);
37//! }
38//! frame.submit(dma);
39//! // The walk is over: the table and the storage are free again.
40//! ot.clear();
41//! storage[0] = TriFlat::new([(0, 0); 3], 0, 0, 0);
42//! }
43//!
44//! fn overlapped(
45//! storage: &'static mut FrameStorage<8, [TriFlat; 4]>,
46//! dma: psx_io::periph::GpuDma,
47//! ) {
48//! let (in_flight, added) = storage.draw_async(dma, |frame, packets| {
49//! let mut arena = PrimitiveArena::new(packets);
50//! let tri = arena.push(TriFlat::new([(0, 0), (8, 0), (0, 8)], 0, 255, 0));
51//! tri.map(|tri| frame.add(1, tri)).is_some()
52//! });
53//! // ... CPU work while the GPU walks ...
54//! let (storage, dma) = in_flight.wait();
55//! let mut pair = FramePair::new(storage, other_storage(), dma);
56//! pair.present(|_frame, _packets| {});
57//! # let _ = added;
58//! }
59//! # fn other_storage() -> &'static mut FrameStorage<8, [TriFlat; 4]> { unimplemented!() }
60//! ```
61//!
62//! # What the borrow checker rejects
63//!
64//! A packet that dies before the frame is submitted:
65//!
66//! ```compile_fail,E0597
67//! # use psx_gpu::{ot::OrderingTable, prim::TriFlat};
68//! # fn f(dma: &mut psx_io::periph::GpuDma) {
69//! let mut ot = OrderingTable::<8>::new();
70//! let mut frame = ot.frame();
71//! {
72//! let mut tri = TriFlat::new([(0, 0), (8, 0), (0, 8)], 255, 0, 0);
73//! frame.add(1, &mut tri);
74//! } // `tri` dropped here while the frame still links it
75//! frame.submit(dma);
76//! # }
77//! ```
78//!
79//! Clearing (or otherwise touching) the table while its walk is in flight:
80//!
81//! ```compile_fail,E0499
82//! # use psx_gpu::ot::OrderingTable;
83//! # fn f(dma: &mut psx_io::periph::GpuDma) {
84//! let mut ot = OrderingTable::<8>::new();
85//! let frame = ot.frame();
86//! frame.submit_with(dma, || ot.clear());
87//! # }
88//! ```
89//!
90//! Reusing the storage of an asynchronously kicked frame before waiting:
91//!
92//! ```compile_fail,E0499
93//! # use psx_gpu::{frame::FrameStorage, prim::TriFlat};
94//! # fn f(storage: &'static mut FrameStorage<8, [TriFlat; 4]>, dma: psx_io::periph::GpuDma) {
95//! let (in_flight, ()) = storage.draw_async(dma, |_frame, _packets| {});
96//! storage.draw_async(unsafe { psx_io::periph::GpuDma::steal() }, |_frame, _packets| {});
97//! in_flight.wait();
98//! # }
99//! ```
100//!
101//! Restarting an arena over storage whose packets are still linked:
102//!
103//! ```compile_fail,E0499
104//! # use psx_gpu::{frame::PrimitiveArena, ot::OrderingTable, prim::TriFlat};
105//! # fn f(dma: &mut psx_io::periph::GpuDma) {
106//! let mut ot = OrderingTable::<8>::new();
107//! let mut storage: [TriFlat; 4] = core::array::from_fn(|_| TriFlat::new([(0, 0); 3], 0, 0, 0));
108//! let mut frame = ot.frame();
109//! let mut arena = PrimitiveArena::new(&mut storage);
110//! frame.add(1, arena.push(TriFlat::new([(0, 0), (8, 0), (0, 8)], 255, 0, 0)).unwrap());
111//! let mut again = PrimitiveArena::new(&mut storage); // still borrowed by the frame
112//! again.push(TriFlat::new([(0, 0); 3], 0, 0, 0));
113//! frame.submit(dma);
114//! # }
115//! ```
116
117use crate::ot::OrderingTable;
118use crate::prim::GpuPacket;
119use psx_io::periph::GpuDma;
120
121/// One frame's view of an [`OrderingTable`]: insert packets, then submit.
122///
123/// Made by [`OrderingTable::frame`]. Every packet added must live for `'f`,
124/// the frame's borrow of the table, and is exclusively borrowed for that
125/// long; submitting consumes the frame and waits for the walk.
126#[derive(Debug)]
127#[must_use = "a frame does nothing until it is submitted"]
128pub struct OtFrame<'f, const N: usize> {
129 ot: &'f mut OrderingTable<N>,
130}
131
132impl<const N: usize> OrderingTable<N> {
133 /// Clear the table and start building a frame in it.
134 ///
135 /// The table stays borrowed until the frame is submitted and its walk
136 /// has finished, so it can neither be cleared nor moved under the DMA.
137 #[doc(alias = "ClearOTagR")]
138 #[inline]
139 pub fn frame(&mut self) -> OtFrame<'_, N> {
140 self.clear();
141 OtFrame { ot: self }
142 }
143
144 /// Continue a frame in this table without clearing it, for a frame
145 /// built in several phases (a scene pass, then an overlay pass).
146 ///
147 /// # Safety
148 ///
149 /// Every packet the table links already, through an earlier
150 /// [`OtFrame`] or a raw add, must
151 /// stay live and unmodified, except for tags later adds write, until the
152 /// returned frame's walk has finished (or for good, if it is never
153 /// submitted). Packets an earlier frame added with [`OtFrame::add`] were
154 /// borrowed only for that frame, so the caller now owns their lifetime.
155 /// The table must not be walking.
156 #[inline]
157 pub unsafe fn resume_frame(&mut self) -> OtFrame<'_, N> {
158 OtFrame { ot: self }
159 }
160}
161
162impl<'f, const N: usize> OtFrame<'f, N> {
163 /// Prepend `packet` to depth slot `z` (clamped to `N - 1`). Slot `N - 1`
164 /// is walked first, slot 0 last, so lower slots draw on top.
165 #[inline(always)]
166 pub fn add<P: GpuPacket>(&mut self, z: usize, packet: &'f mut P) {
167 const {
168 assert!(P::WORDS as usize <= crate::chain::MAX_NODE_WORDS);
169 assert!(core::mem::size_of::<P>() >= 4 * (1 + P::WORDS as usize));
170 assert!(core::mem::align_of::<P>() >= 4);
171 };
172 // SAFETY: `packet` is a `GpuPacket` (tag word first, `WORDS` payload
173 // words after it, at most one node), borrowed exclusively for 'f.
174 // The frame can only be walked by `submit`/`submit_with`, which wait
175 // before 'f can end, or by `FrameStorage` with 'f = 'static.
176 unsafe { self.ot.link(z, (packet as *mut P).cast::<u32>(), P::WORDS) };
177 }
178
179 /// Prepend a raw packet to depth slot `z` (clamped to `N - 1`).
180 ///
181 /// # Safety
182 ///
183 /// `packet` must point at a 4-byte-aligned tag word followed by `words`
184 /// payload words, all in RAM that stays live and unmodified, except for
185 /// the tag this call writes, until this frame's walk has finished (or
186 /// for good, if the frame is never submitted).
187 ///
188 /// # Panics
189 ///
190 /// If `words` exceeds [`crate::chain::MAX_NODE_WORDS`].
191 #[inline(always)]
192 pub unsafe fn add_raw(&mut self, z: usize, packet: *mut u32, words: u8) {
193 // SAFETY: forwarded contract.
194 unsafe { self.ot.link(z, packet, words) };
195 }
196
197 /// [`add_raw`](Self::add_raw) without the depth clamp or the node-length
198 /// check.
199 ///
200 /// # Safety
201 ///
202 /// The contract of [`add_raw`](Self::add_raw), and also: `z` is less
203 /// than `N`, and `words` is at most [`crate::chain::MAX_NODE_WORDS`].
204 #[inline(always)]
205 pub unsafe fn add_raw_unchecked(&mut self, z: usize, packet: *mut u32, words: u8) {
206 // SAFETY: forwarded contract.
207 unsafe { self.ot.link_unchecked(z, packet, words) };
208 }
209
210 /// [`add_raw_unchecked`](Self::add_raw_unchecked) with the word count
211 /// already in tag form, in bits 24..31 of `tag_high`.
212 ///
213 /// # Safety
214 ///
215 /// The contract of [`add_raw_unchecked`](Self::add_raw_unchecked), with
216 /// `tag_high >> 24` as the word count, and also: the low 24 bits of
217 /// `tag_high` are zero.
218 #[inline(always)]
219 pub unsafe fn add_raw_tag_high_unchecked(&mut self, z: usize, packet: *mut u32, tag_high: u32) {
220 // SAFETY: forwarded contract.
221 unsafe { self.ot.link_tag_high_unchecked(z, packet, tag_high) };
222 }
223
224 /// Add packed two-word commands, first to last, the way repeated classic
225 /// `addPrim` calls do: packets that share a slot come out in reverse.
226 ///
227 /// Each command is two machine words: the packet's address (`ptr as
228 /// usize`, which exposes it), then a word with the slot in bits 0..=15
229 /// and the packet's payload word count in bits 24..=31. On the console
230 /// this is one hand-scheduled loop; host builds run a scalar twin.
231 ///
232 /// # Safety
233 ///
234 /// `commands` points at `command_count * 2` readable words in that
235 /// layout; every slot is less than `N` and every word count at most
236 /// [`crate::chain::MAX_NODE_WORDS`]; and every packet meets the contract of
237 /// [`add_raw`](Self::add_raw).
238 #[inline(always)]
239 pub unsafe fn add_packed_commands_unchecked(
240 &mut self,
241 commands: *const usize,
242 command_count: usize,
243 ) {
244 // SAFETY: forwarded contract.
245 unsafe {
246 self.ot
247 .link_packed_commands_unchecked(commands, command_count)
248 };
249 }
250
251 /// [`add_packed_commands_unchecked`](Self::add_packed_commands_unchecked)
252 /// last to first, so packets that share a slot keep their array order.
253 ///
254 /// # Safety
255 ///
256 /// As [`add_packed_commands_unchecked`](Self::add_packed_commands_unchecked).
257 #[inline(always)]
258 pub unsafe fn add_packed_commands_reverse_unchecked(
259 &mut self,
260 commands: *const usize,
261 command_count: usize,
262 ) {
263 // SAFETY: forwarded contract.
264 unsafe {
265 self.ot
266 .link_packed_commands_reverse_unchecked(commands, command_count)
267 };
268 }
269
270 /// Add a contiguous stream of packets whose tags carry their own slot.
271 ///
272 /// Before this call each tag holds the packet's payload word count in
273 /// bits 24..=31 and its slot in bits 0..=15. Packets are prepended from
274 /// `first` to `end`, as repeated `addPrim` calls would; slot `0xFFFF`
275 /// skips a packet, so separately ordered HUD packets can share the
276 /// arena. Renderers stage depth keys in the tags as they build packets
277 /// and leave the link pass to this one loop.
278 ///
279 /// # Safety
280 ///
281 /// `first..end` is a writable, contiguous sequence of complete packets
282 /// in that layout; every word count describes the next packet exactly
283 /// and is at most [`crate::chain::MAX_NODE_WORDS`]; every slot other than
284 /// `0xFFFF` is less than `N`; and the whole range meets the contract of
285 /// [`add_raw`](Self::add_raw).
286 #[inline(always)]
287 pub unsafe fn add_tagged_packet_stream_unchecked(&mut self, first: *mut u32, end: *mut u32) {
288 // SAFETY: forwarded contract.
289 unsafe { self.ot.link_tagged_packet_stream_unchecked(first, end) };
290 }
291
292 /// [`add_tagged_packet_stream_unchecked`](Self::add_tagged_packet_stream_unchecked)
293 /// with every staged slot other than `0xFFFF` shifted right by
294 /// `SLOT_SHIFT`, so a smaller table can back a fine depth range
295 /// (`SLOT_SHIFT = 3` maps depths 0..2047 onto 256 slots).
296 ///
297 /// # Safety
298 ///
299 /// As [`add_tagged_packet_stream_unchecked`](Self::add_tagged_packet_stream_unchecked),
300 /// with every shifted slot less than `N`, and `SLOT_SHIFT` less than 16.
301 #[inline(always)]
302 pub unsafe fn add_tagged_packet_stream_shifted_unchecked<const SLOT_SHIFT: u32>(
303 &mut self,
304 first: *mut u32,
305 end: *mut u32,
306 ) {
307 // SAFETY: forwarded contract.
308 unsafe {
309 self.ot
310 .link_tagged_packet_stream_shifted_unchecked::<SLOT_SHIFT>(first, end)
311 };
312 }
313
314 /// End the walk with GP0(1Fh) so [`crate::is_draw_done`] rises once the
315 /// frame is drawn; see [`OrderingTable::end_with_draw_done`].
316 ///
317 /// # Panics
318 ///
319 /// If anything was already added at slot 0, or the frame already ends
320 /// in a link.
321 #[inline]
322 pub fn end_with_draw_done(&mut self) {
323 self.ot.end_with_draw_done();
324 }
325
326 /// The node the frame's walk starts at, for a chain that continues into
327 /// this frame (a recorded preamble handed to `psx_rt::present`). Only an
328 /// address: kicking a walk from it is up to an unsafe call that takes on
329 /// the frame's lifetime itself.
330 #[inline]
331 pub fn submit_head(&self) -> *const u32 {
332 self.ot.submit_head()
333 }
334
335 /// Continue this frame's walk into `head`, a chain that ends the list
336 /// itself; see [`OrderingTable::end_with_chain`].
337 ///
338 /// # Safety
339 ///
340 /// Every node reachable from `head` must stay live and unmodified for
341 /// `'f`, and for any walk of this frame that outlives it, under
342 /// [`crate::chain::submit_async_raw`]'s node rules.
343 ///
344 /// # Panics
345 ///
346 /// If anything was already added at slot 0, or the frame already ends
347 /// in a link.
348 #[inline]
349 pub unsafe fn end_with_chain(&mut self, head: *const u32) {
350 // SAFETY: forwarded contract.
351 unsafe { self.ot.end_with_chain(head) };
352 }
353
354 /// Continue this frame's walk into `next` once its own slot 0 is done,
355 /// so both tables go out in one kick.
356 ///
357 /// `next` is consumed: its table stays borrowed for `'f` and is walked as
358 /// part of this frame. Its own packets keep their order; anything added
359 /// to this frame's slot 0 afterwards still draws before `next`.
360 ///
361 /// Host builds link for real, but [`OrderingTable::packets`] only
362 /// follows links within one table's address window.
363 ///
364 /// # Panics
365 ///
366 /// If anything was already added at slot 0, or the frame already ends
367 /// in a link or GP0(1Fh): call it before slot 0 is used, and close only
368 /// the last frame of a chain with [`end_with_draw_done`](Self::end_with_draw_done).
369 #[inline]
370 pub fn link_after<const M: usize>(&mut self, next: OtFrame<'f, M>) {
371 // SAFETY: `next`'s table is borrowed for 'f, as long as this frame,
372 // and was consumed, so nothing else can clear it or submit it.
373 unsafe { self.ot.end_with_node(next.ot.submit_head()) };
374 }
375
376 /// Kick the walk and wait for it.
377 #[doc(alias = "DrawOTag")]
378 #[inline]
379 pub fn submit(self, dma: &mut GpuDma) {
380 // SAFETY: every node is the table (borrowed for 'f), a packet
381 // borrowed for 'f through `add`, a raw packet whose `add_raw`
382 // contract covers the walk, a linked `OtFrame<'f>`, or the static
383 // GP0(1Fh) node. The walk ends before this returns.
384 unsafe { crate::chain::submit_raw(dma, self.ot.submit_head()) };
385 }
386
387 /// Kick the walk, run `overlap` while the GPU walks, then wait.
388 ///
389 /// `overlap` cannot reach the table or the packets: they are still
390 /// borrowed by this frame.
391 #[inline]
392 pub fn submit_with<R>(self, _dma: &mut GpuDma, overlap: impl FnOnce() -> R) -> R {
393 // SAFETY: as `submit`; `_wait` waits for the walk before this
394 // returns, on every path out of `overlap`.
395 unsafe { crate::chain::start_walk(self.ot.submit_head()) };
396 let _wait = WaitOnDrop;
397 overlap()
398 }
399}
400
401/// Waits for the GPU DMA walk when dropped.
402struct WaitOnDrop;
403
404impl Drop for WaitOnDrop {
405 #[inline(always)]
406 fn drop(&mut self) {
407 crate::chain::wait_walk();
408 }
409}
410
411/// An ordering table together with the packet storage its frames link.
412///
413/// The unit [`draw_async`](Self::draw_async) and [`FramePair`] keep
414/// `'static` while the GPU walks it. `S` is whatever holds the packets:
415/// an array, or a struct of arrays per packet type.
416#[derive(Debug)]
417pub struct FrameStorage<const N: usize, S> {
418 ot: OrderingTable<N>,
419 packets: S,
420}
421
422impl<const N: usize, S> FrameStorage<N, S> {
423 /// An empty table and `packets` as the packet storage.
424 pub const fn new(packets: S) -> Self {
425 Self {
426 ot: OrderingTable::new(),
427 packets,
428 }
429 }
430
431 /// The packet storage, outside any frame.
432 pub fn packets_mut(&mut self) -> &mut S {
433 &mut self.packets
434 }
435
436 /// Clear the table and run `build` with the frame and the storage.
437 fn build<R>(&mut self, build: impl for<'f> FnOnce(&mut OtFrame<'f, N>, &'f mut S) -> R) -> R {
438 let Self { ot, packets } = self;
439 let mut frame = ot.frame();
440 build(&mut frame, packets)
441 }
442
443 /// Kick the table built last without waiting.
444 ///
445 /// # Safety
446 ///
447 /// `self` must stay untouched (no `build`, no access to the packets)
448 /// until the walk has been waited out.
449 #[inline]
450 unsafe fn kick(&mut self) {
451 // SAFETY: the table and packets were linked by `build`, which only
452 // admits packets in `self` or `'static` ones; the caller keeps
453 // `self` untouched until the wait.
454 unsafe { crate::chain::start_walk(self.ot.submit_head()) };
455 }
456
457 /// Build a frame with `build`, then kick it and return without waiting.
458 ///
459 /// The storage and the DMA token move into the returned [`InFlight`]
460 /// until [`InFlight::wait`] hands them back. `build` gets the frame and
461 /// the packet storage for a lifetime it cannot leak.
462 pub fn draw_async<R>(
463 &'static mut self,
464 dma: GpuDma,
465 build: impl for<'f> FnOnce(&mut OtFrame<'f, N>, &'f mut S) -> R,
466 ) -> (InFlight<N, S>, R) {
467 let result = self.build(build);
468 // SAFETY: the storage moves into `InFlight`, which hands it back
469 // only from `wait`, after the walk.
470 unsafe { self.kick() };
471 (InFlight { storage: self, dma }, result)
472 }
473}
474
475/// A frame the GPU is walking, owning its `'static` storage and the
476/// [`GpuDma`] token until [`wait`](Self::wait).
477///
478/// Dropping or forgetting it leaks both: the storage then stays out of
479/// reach for the rest of the run instead of being reused under the walk.
480#[derive(Debug)]
481#[must_use = "dropping an in-flight frame leaks its storage; call wait()"]
482pub struct InFlight<const N: usize, S: 'static> {
483 storage: &'static mut FrameStorage<N, S>,
484 dma: GpuDma,
485}
486
487impl<const N: usize, S> InFlight<N, S> {
488 /// True once the walk has finished (the GPU may still be drawing the
489 /// last packets; the packets themselves are free).
490 #[inline]
491 pub fn is_done(&self) -> bool {
492 !psx_io::dma::is_busy(psx_io::dma::Channel::Gpu)
493 }
494
495 /// Wait for the walk, then hand back the storage and the token.
496 #[inline]
497 pub fn wait(self) -> (&'static mut FrameStorage<N, S>, GpuDma) {
498 crate::chain::wait_walk();
499 (self.storage, self.dma)
500 }
501}
502
503/// Two [`FrameStorage`]s in the usual ping-pong: build frame N+1 into one
504/// while the GPU walks frame N from the other.
505///
506/// ```ignore
507/// loop {
508/// pair.build(|frame, packets| { /* add packets */ });
509/// pair.wait(); // frame N's walk is done
510/// psx_gpu::arm_draw_done(); // anything that must precede the kick
511/// pair.kick(); // frame N+1 goes out; storages swap
512/// }
513/// ```
514#[derive(Debug)]
515pub struct FramePair<const N: usize, S: 'static> {
516 storage: [&'static mut FrameStorage<N, S>; 2],
517 /// Index of the storage `build` writes; the other one may be in flight.
518 next: usize,
519 dma: GpuDma,
520}
521
522impl<const N: usize, S> FramePair<N, S> {
523 /// Ping-pong between `a` and `b`, owning the GPU DMA token.
524 pub fn new(
525 a: &'static mut FrameStorage<N, S>,
526 b: &'static mut FrameStorage<N, S>,
527 dma: GpuDma,
528 ) -> Self {
529 Self {
530 storage: [a, b],
531 next: 0,
532 dma,
533 }
534 }
535
536 /// Build the next frame into the storage the GPU is not walking.
537 ///
538 /// Building again before [`kick`](Self::kick) starts that frame over.
539 pub fn build<R>(
540 &mut self,
541 build: impl for<'f> FnOnce(&mut OtFrame<'f, N>, &'f mut S) -> R,
542 ) -> R {
543 self.storage[self.next].build(build)
544 }
545
546 /// Wait until the frame kicked last has been walked, and lend the token
547 /// for work that must not overlap it (VRAM uploads, immediate GP0).
548 #[inline]
549 pub fn wait(&mut self) -> &mut GpuDma {
550 crate::chain::wait_walk();
551 &mut self.dma
552 }
553
554 /// Wait for the previous walk, kick the frame built last, and switch
555 /// [`build`](Self::build) to the other storage.
556 #[inline]
557 pub fn kick(&mut self) {
558 // SAFETY: `raw_async` waits for the previous walk before it starts
559 // this one. The kicked storage is not touched again until the next
560 // `kick` has waited this walk out: `build` only writes the other
561 // storage from now on.
562 unsafe { self.storage[self.next].kick() };
563 self.next ^= 1;
564 }
565
566 /// Wait for the last walk, then hand back both storages and the token.
567 pub fn release(self) -> ([&'static mut FrameStorage<N, S>; 2], GpuDma) {
568 crate::chain::wait_walk();
569 (self.storage, self.dma)
570 }
571
572 /// [`build`](Self::build), then [`kick`](Self::kick).
573 pub fn present<R>(
574 &mut self,
575 build: impl for<'f> FnOnce(&mut OtFrame<'f, N>, &'f mut S) -> R,
576 ) -> R {
577 let result = self.build(build);
578 self.kick();
579 result
580 }
581}
582
583/// Hands out packets from caller storage as `&'f mut P`, one slot each.
584///
585/// The storage is borrowed for `'f`, so it cannot be reused (a new arena
586/// over it, say) while any packet it handed out is still linked into a
587/// frame.
588#[derive(Debug)]
589pub struct PrimitiveArena<'f, P> {
590 // A base pointer and a count rather than a shrinking `&mut [P]`: a push is
591 // then one compare and one increment, where re-slicing loaded and stored
592 // the slice's pointer and length on every packet and measured +1% work
593 // per frame in NitroXide's wall pass.
594 storage: *mut P,
595 capacity: usize,
596 used: usize,
597 _storage: core::marker::PhantomData<&'f mut [P]>,
598}
599
600impl<'f, P> PrimitiveArena<'f, P> {
601 /// Hand out the slots of `storage`, front to back.
602 #[inline]
603 pub fn new(storage: &'f mut [P]) -> Self {
604 Self {
605 storage: storage.as_mut_ptr(),
606 capacity: storage.len(),
607 used: 0,
608 _storage: core::marker::PhantomData,
609 }
610 }
611
612 /// Store `packet` in the next free slot and return it for the rest of
613 /// `'f`; `None` once the storage is used up.
614 #[inline(always)]
615 pub fn push(&mut self, packet: P) -> Option<&'f mut P> {
616 if self.used >= self.capacity {
617 return None;
618 }
619 // SAFETY: `used < capacity`, so the slot lies in the storage `new` took
620 // as `&'f mut [P]`; `used` only grows, so no slot is handed out twice
621 // and the returned borrow aliases nothing else for 'f.
622 let slot = unsafe { &mut *self.storage.add(self.used) };
623 self.used += 1;
624 *slot = packet;
625 Some(slot)
626 }
627
628 /// Slots handed out so far.
629 #[inline]
630 pub fn len(&self) -> usize {
631 self.used
632 }
633
634 /// True if nothing was handed out yet.
635 #[inline]
636 pub fn is_empty(&self) -> bool {
637 self.used == 0
638 }
639
640 /// Slots still free.
641 #[inline]
642 pub fn remaining(&self) -> usize {
643 self.capacity - self.used
644 }
645}
646
647#[cfg(all(test, not(target_arch = "mips")))]
648mod tests {
649 use super::*;
650 use crate::prim::{RectFlat, TriFlat};
651
652 const END: u32 = psx_hw::dma::linked_list::END;
653
654 fn addr<T>(value: &T) -> u32 {
655 (value as *const T as usize as u32) & END
656 }
657
658 #[test]
659 fn frame_links_packets_like_insert() {
660 let mut storage: [TriFlat; 2] =
661 core::array::from_fn(|_| TriFlat::new([(0, 0); 3], 0, 0, 0));
662 let mut ot = OrderingTable::<4>::new();
663 let mut frame = ot.frame();
664 let mut arena = PrimitiveArena::new(&mut storage);
665 let a = arena
666 .push(TriFlat::new([(1, 2), (3, 4), (5, 6)], 1, 2, 3))
667 .unwrap();
668 let a_addr = addr(a);
669 frame.add(2, a);
670 let b = arena
671 .push(TriFlat::new([(1, 2), (3, 4), (5, 6)], 4, 5, 6))
672 .unwrap();
673 let b_addr = addr(b);
674 frame.add(2, b);
675 assert!(arena.push(TriFlat::new([(0, 0); 3], 0, 0, 0)).is_none());
676 assert_eq!((arena.len(), arena.remaining()), (2, 0));
677 drop(frame);
678
679 // Slot 2 now heads b, which links a, which links slot 1's entry.
680 let walked: [(usize, u8); 2] = {
681 // SAFETY: the table links only `storage`, alive for the test.
682 let mut it = unsafe { ot.packets() };
683 let first = it.next().unwrap();
684 let second = it.next().unwrap();
685 assert!(it.next().is_none());
686 [
687 (first.0 as usize & END as usize, first.1),
688 (second.0 as usize & END as usize, second.1),
689 ]
690 };
691 assert_eq!(
692 walked,
693 [
694 (b_addr as usize, TriFlat::WORDS),
695 (a_addr as usize, TriFlat::WORDS)
696 ]
697 );
698 assert_eq!(storage[1].tag >> 24, TriFlat::WORDS as u32);
699 assert_eq!(storage[1].tag & END, a_addr);
700 }
701
702 #[test]
703 fn frame_clamps_depth_into_the_table() {
704 let mut rect = RectFlat::new(0, 0, 4, 4, 9, 9, 9);
705 let mut ot = OrderingTable::<4>::new();
706 let mut frame = ot.frame();
707 frame.add(99, &mut rect);
708 drop(frame);
709 // SAFETY: the table links only `rect`, alive for the test.
710 let first = unsafe { ot.packets() }.next().unwrap();
711 assert_eq!(first.0 as usize & END as usize, addr(&rect) as usize);
712 }
713
714 #[test]
715 fn link_after_continues_into_the_next_table() {
716 let mut first = OrderingTable::<4>::new();
717 let mut second = OrderingTable::<2>::new();
718 let mut frame = first.frame();
719 let next = second.frame();
720 let next_head = next.ot.submit_head() as usize as u32 & END;
721 frame.link_after(next);
722 drop(frame);
723 // Slot 0, walked last, now links the second table's head.
724 // SAFETY: `submit_head` is entry 3 of a 4-entry table; entry 0 is
725 // three words below it, inside the same array.
726 let slot0 = unsafe { first.submit_head().sub(3).read() };
727 assert_eq!(slot0, next_head);
728 }
729
730 #[test]
731 #[should_panic(expected = "empty slot 0")]
732 fn link_after_refuses_a_used_slot_zero() {
733 let mut rect = RectFlat::new(0, 0, 4, 4, 9, 9, 9);
734 let mut first = OrderingTable::<4>::new();
735 let mut second = OrderingTable::<2>::new();
736 let mut frame = first.frame();
737 frame.add(0, &mut rect);
738 frame.link_after(second.frame());
739 }
740
741 #[test]
742 fn frame_storage_builds_into_its_own_packets() {
743 let mut storage = FrameStorage::<4, [RectFlat; 2]>::new(core::array::from_fn(|_| {
744 RectFlat::new(0, 0, 0, 0, 0, 0, 0)
745 }));
746 let count = storage.build(|frame, packets| {
747 let mut arena = PrimitiveArena::new(packets);
748 for z in 0..3 {
749 match arena.push(RectFlat::new(z, z, 2, 2, 1, 1, 1)) {
750 Some(rect) => frame.add(z as usize, rect),
751 None => break,
752 }
753 }
754 arena.len()
755 });
756 assert_eq!(count, 2);
757 // SAFETY: the table links only the storage's own packets.
758 let walked = unsafe { storage.ot.packets() }.count();
759 assert_eq!(walked, 2);
760 assert_eq!(storage.packets_mut()[1].tag >> 24, RectFlat::WORDS as u32);
761 }
762
763 #[test]
764 fn resume_frame_keeps_the_links_already_made() {
765 let mut first = RectFlat::new(0, 0, 4, 4, 1, 1, 1);
766 let mut second = RectFlat::new(0, 0, 4, 4, 2, 2, 2);
767 let mut ot = OrderingTable::<4>::new();
768 let mut frame = ot.frame();
769 frame.add(1, &mut first);
770 drop(frame);
771 // SAFETY: `first` and `second` outlive every use of the table, and
772 // nothing walks it.
773 let mut frame = unsafe { ot.resume_frame() };
774 frame.add(2, &mut second);
775 drop(frame);
776 let walked: [usize; 2] = {
777 // SAFETY: the table links only `first` and `second`, alive here.
778 let mut it = unsafe { ot.packets() };
779 let order = [it.next().unwrap().0 as usize, it.next().unwrap().0 as usize];
780 assert!(it.next().is_none());
781 order
782 };
783 assert_eq!(
784 walked.map(|a| a as u32 & END),
785 [addr(&second), addr(&first)]
786 );
787 }
788
789 #[test]
790 fn unchecked_adds_link_like_add_raw() {
791 let mut checked = OrderingTable::<4>::new();
792 let mut unchecked = OrderingTable::<4>::new();
793 let mut a = [0u32; 3];
794 let mut b = [0u32; 3];
795 let mut c = [0u32; 3];
796 let mut d = [0u32; 3];
797 {
798 let mut frame = checked.frame();
799 // SAFETY: the arrays outlive every use of the tables in this
800 // test and hold a tag plus two words.
801 unsafe {
802 frame.add_raw(2, a.as_mut_ptr(), 2);
803 frame.add_raw(2, b.as_mut_ptr(), 2);
804 }
805 }
806 {
807 let mut frame = unchecked.frame();
808 // SAFETY: as above; slot 2 is below 4 and two words fit a node.
809 unsafe {
810 frame.add_raw_unchecked(2, c.as_mut_ptr(), 2);
811 frame.add_raw_tag_high_unchecked(2, d.as_mut_ptr(), 2 << 24);
812 }
813 }
814 assert_eq!(a[0] >> 24, c[0] >> 24);
815 assert_eq!(b[0] >> 24, d[0] >> 24);
816 assert_eq!(b[0] & END, a.as_ptr() as usize as u32 & END);
817 assert_eq!(d[0] & END, c.as_ptr() as usize as u32 & END);
818 // SAFETY: the tables link only the arrays above, alive here.
819 let heads = unsafe {
820 (
821 checked.packets().next().unwrap().0,
822 unchecked.packets().next().unwrap().0,
823 )
824 };
825 assert_eq!(heads, (b.as_ptr(), d.as_ptr()));
826 }
827
828 #[test]
829 fn packed_and_tagged_adds_forward_to_the_table() {
830 let mut a = [0u32; 2];
831 let mut b = [0u32; 2];
832 let commands: [usize; 4] = [
833 a.as_mut_ptr() as usize,
834 3 | (1 << 24),
835 b.as_mut_ptr() as usize,
836 3 | (1 << 24),
837 ];
838 let mut stream = [(1u32 << 24) | 1, 0xAAAA_AAAA];
839 let mut ot = OrderingTable::<4>::new();
840 let mut frame = ot.frame();
841 // SAFETY: the commands, packets and stream outlive every use of the
842 // table; slots 3 and 1 fit it and every node is one word long.
843 unsafe {
844 frame.add_packed_commands_reverse_unchecked(commands.as_ptr(), 2);
845 let first = stream.as_mut_ptr();
846 frame.add_tagged_packet_stream_unchecked(first, first.add(stream.len()));
847 }
848 drop(frame);
849 // SAFETY: the table links only `a`, `b` and `stream`, alive here.
850 let walked: [*const u32; 3] = unsafe {
851 let mut it = ot.packets();
852 let order = [
853 it.next().unwrap().0,
854 it.next().unwrap().0,
855 it.next().unwrap().0,
856 ];
857 assert!(it.next().is_none());
858 order
859 };
860 assert_eq!(walked, [a.as_ptr(), b.as_ptr(), stream.as_ptr()]);
861 }
862
863 #[test]
864 fn arena_hands_out_every_slot_once_then_refuses() {
865 let mut storage: [RectFlat; 3] =
866 core::array::from_fn(|_| RectFlat::new(0, 0, 0, 0, 0, 0, 0));
867 let base = storage.as_ptr() as usize;
868 let mut arena = PrimitiveArena::new(&mut storage);
869 assert_eq!((arena.len(), arena.remaining()), (0, 3));
870 let a = arena.push(RectFlat::new(1, 0, 1, 1, 1, 0, 0)).unwrap();
871 let b = arena.push(RectFlat::new(2, 0, 1, 1, 2, 0, 0)).unwrap();
872 let c = arena.push(RectFlat::new(3, 0, 1, 1, 3, 0, 0)).unwrap();
873 assert!(arena.push(RectFlat::new(4, 0, 1, 1, 4, 0, 0)).is_none());
874 assert_eq!((arena.len(), arena.remaining()), (3, 0));
875 // Each slot is its own, in storage order: write through every borrow,
876 // then read every one back.
877 let size = core::mem::size_of::<RectFlat>();
878 for (index, slot) in [&*a, &*b, &*c].iter().enumerate() {
879 assert_eq!(*slot as *const RectFlat as usize, base + index * size);
880 }
881 a.tag = 0x11;
882 b.tag = 0x22;
883 c.tag = 0x33;
884 assert_eq!([a.tag, b.tag, c.tag], [0x11, 0x22, 0x33]);
885 assert_eq!(
886 [storage[0].tag, storage[1].tag, storage[2].tag],
887 [0x11, 0x22, 0x33]
888 );
889 }
890
891 #[test]
892 fn arena_over_empty_storage_refuses_the_first_push() {
893 let mut storage: [RectFlat; 0] = [];
894 let mut arena = PrimitiveArena::new(&mut storage);
895 assert!(arena.push(RectFlat::new(0, 0, 0, 0, 0, 0, 0)).is_none());
896 assert!(arena.is_empty());
897 assert_eq!(arena.remaining(), 0);
898 }
899}