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

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}