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

psx_gpu/
ot.rs

1//! Ordering Table: depth-sorted linked-list of GPU primitives.
2//!
3//! The PS1 has no Z-buffer. Games sort primitives back-to-front
4//! (painter's algorithm) by inserting them into an OT slot indexed
5//! by depth. Each OT slot is the head of a linked list; primitives
6//! prepend themselves so the most-recently-inserted draws first
7//! within a slot.
8//!
9//! Once a frame's primitives are inserted, the whole OT is shipped
10//! to GPU GP0 via DMA channel 2 in linked-list mode. The DMA walker
11//! follows the `next` pointers embedded in each packet's first word
12//! until it hits `0x00FFFFFF` (end of chain).
13//!
14//! Each 32-bit OT entry (and primitive header) is:
15//!
16//! ```text
17//!   bits 0..=23: address of next packet (24-bit, masked into RAM)
18//!   bits 24..=31: word count of this packet's data, at most
19//!                 `crate::chain::MAX_NODE_WORDS` (16) on silicon
20//! ```
21//!
22//! An "empty OT" has every entry pointing at its predecessor,
23//! ending in `0x00FFFFFF`. Submitting such an OT sends nothing to
24//! GP0. As primitives are added, their packets chain in.
25
26use core::marker::PhantomData;
27use core::ptr;
28use psx_io::periph::OrderingTableClearDma;
29
30const OT_ADDR_MASK: u32 = psx_hw::dma::linked_list::ADDRESS_MASK;
31const OT_END: u32 = psx_hw::dma::linked_list::END;
32const OT_MAX_EXTRA_HOPS: usize = 131_072;
33/// A staged-tag bit that tagged-stream insertion ignores: the stream inserts
34/// keep only the word count and the slot from a staged tag.
35#[deprecated(note = "has no effect: the scoped texture-window coalescing that read it was removed")]
36pub const TAG_SCOPED_TEXTURE_WINDOW: u32 = 1 << 16;
37/// Whether DMA can read `packet`: on the console, main RAM (any mirror or
38/// segment) and not the scratchpad, whose low 24 bits would name a RAM
39/// address instead. Host builds accept any address.
40fn is_dma_reachable(packet: *mut u32) -> bool {
41    if cfg!(target_arch = "mips") {
42        let physical = psx_hw::memory::to_physical(packet.addr() as u32);
43        physical < psx_hw::memory::ram::MIRROR_END
44    } else {
45        true
46    }
47}
48
49/// Fixed-size OT. `N` depth slots. Typical values: 256, 1024, 4096.
50#[repr(C, align(4))]
51pub struct OrderingTable<const N: usize> {
52    entries: [u32; N],
53}
54
55impl<const N: usize> OrderingTable<N> {
56    /// Create a table with every slot being a chain terminator.
57    /// Call [`clear`](Self::clear) before submitting -- that wires
58    /// up the inter-slot chain so DMA walks across all `N` slots.
59    pub const fn new() -> Self {
60        const { assert!(N > 0, "an ordering table needs at least one slot") };
61        Self {
62            entries: [OT_END; N],
63        }
64    }
65
66    /// Reset every slot for a fresh frame. Entry `[0]` is the
67    /// terminator (farthest from camera); each higher slot points
68    /// to the slot below. Submission starts at `[N-1]` so the
69    /// DMA walker visits `[N-1] → [N-2] → … → [0] → end`.
70    #[doc(alias = "ClearOTagR")]
71    pub fn clear(&mut self) {
72        // CPU clear by default: the OTC DMA is one of the channels the
73        // CL2 silicon probes showed can wedge busy-forever on real
74        // hardware, and a wedged boot-time clear freezes the engine at
75        // its first frame with no diagnostic. N stores per frame is a
76        // measurable but small cost; callers that trust their DMA can
77        // opt back in via [`Self::clear_with_dma`].
78        self.clear_software();
79    }
80
81    /// [`clear`](Self::clear) through the ordering-table-clear DMA channel
82    /// (channel 6) instead of the CPU.
83    ///
84    /// The wait is bounded: if the channel wedges, which the CL2 silicon
85    /// probes saw it do, or the table is longer than a 16-bit word count,
86    /// the CPU clear runs instead, so the table is always valid. Host
87    /// builds always clear with the CPU.
88    #[doc(alias = "ClearOTagR")]
89    pub fn clear_with_dma(&mut self, dma: &mut OrderingTableClearDma) {
90        #[cfg(not(target_arch = "mips"))]
91        let _ = &dma;
92        #[cfg(target_arch = "mips")]
93        if dma.clear_table(&mut self.entries) {
94            return;
95        }
96        self.clear_software();
97    }
98
99    #[cfg(not(target_arch = "mips"))]
100    fn clear_software(&mut self) {
101        // Slot 0 is the sentinel; chain walks stop here. Each other slot links
102        // the one below it, by the low 24 bits of its address.
103        let base = self.entries.as_mut_ptr().expose_provenance() as u32;
104        self.entries[0] = OT_END;
105        for i in 1..N {
106            self.entries[i] = base.wrapping_add(4 * (i as u32 - 1)) & OT_ADDR_MASK;
107        }
108    }
109
110    #[cfg(target_arch = "mips")]
111    fn clear_software(&mut self) {
112        // The PS1 RAM window occupies only the low 2 MiB of the DMA address
113        // domain, so advancing a low-24-bit OT address across this table
114        // cannot wrap. Schedule eight dependent pointer values per branch;
115        // this preserves the exact OTC chain while avoiding the scalar
116        // pointer-mask and branch cost for every one of the 2,048 Quake slots.
117        let entries = self.entries.as_mut_ptr();
118        // SAFETY: `entries` points at the `N` (at least one) words `self` owns.
119        unsafe { ptr::write(entries, OT_END) };
120        let bulk_words = (N.saturating_sub(1) / 8) * 8;
121        // SAFETY: one past entry 0 stays inside `entries`, or one past its end.
122        let mut cursor = unsafe { entries.add(1) };
123        // SAFETY: `bulk_words` is at most `N - 1`, so this stays inside `entries` or one past
124        // it.
125        let bulk_end = unsafe { cursor.add(bulk_words) };
126        let mut previous = entries.expose_provenance() as u32 & OT_ADDR_MASK;
127        if bulk_words != 0 {
128            // SAFETY: the loop stores `bulk_words` words from `entries + 1`, all inside the
129            // table, and touches no other memory or the stack.
130            unsafe {
131                core::arch::asm!(
132                    ".set noreorder",
133                    "2:",
134                    "sw $9, 0($8)",
135                    "addiu $9, $9, 4",
136                    "sw $9, 4($8)",
137                    "addiu $9, $9, 4",
138                    "sw $9, 8($8)",
139                    "addiu $9, $9, 4",
140                    "sw $9, 12($8)",
141                    "addiu $9, $9, 4",
142                    "sw $9, 16($8)",
143                    "addiu $9, $9, 4",
144                    "sw $9, 20($8)",
145                    "addiu $9, $9, 4",
146                    "sw $9, 24($8)",
147                    "addiu $9, $9, 4",
148                    "sw $9, 28($8)",
149                    "addiu $9, $9, 4",
150                    "addiu $8, $8, 32",
151                    "bne $8, $10, 2b",
152                    "nop",
153                    ".set reorder",
154                    inout("$8") cursor,
155                    inout("$9") previous,
156                    in("$10") bulk_end,
157                    options(nostack),
158                );
159            }
160        }
161        // SAFETY: one past the last entry of `entries`, the loop bound.
162        while cursor < unsafe { entries.add(N) } {
163            // SAFETY: `cursor` is below `entries + N`, inside the table.
164            unsafe { ptr::write(cursor, previous) };
165            // SAFETY: stays at most one past the end of `entries`.
166            cursor = unsafe { cursor.add(1) };
167            previous = previous.wrapping_add(4);
168        }
169    }
170
171    /// Prepend a primitive packet into the depth-`z` slot. `packet_ptr`
172    /// must point at the packet's tag word (first `u32`); `words` is
173    /// the count of data words that follow the tag, at most
174    /// [`crate::chain::MAX_NODE_WORDS`].
175    ///
176    /// # Safety
177    /// Caller guarantees that `[packet_ptr .. packet_ptr + 1 + words]`
178    /// is live, writable, 4-byte-aligned RAM for the duration of the
179    /// OT submission. Primitives returned by the builders in
180    /// [`crate::prim`] satisfy this.
181    ///
182    /// # Panics
183    /// If `words` exceeds [`crate::chain::MAX_NODE_WORDS`]: silicon loses words
184    /// from a longer node.
185    pub(crate) unsafe fn link(&mut self, z: usize, packet_ptr: *mut u32, words: u8) {
186        assert!(
187            words as usize <= crate::chain::MAX_NODE_WORDS,
188            "GPU DMA node longer than MAX_NODE_WORDS"
189        );
190        let z = z.min(N - 1);
191        // SAFETY: `z` was clamped below `N`; the packet contract is forwarded.
192        unsafe { self.link_unchecked(z, packet_ptr, words) };
193    }
194
195    /// Prepend a primitive packet into an already-clamped depth slot.
196    ///
197    /// # Safety
198    /// Same packet lifetime/alignment requirements as [`link`](Self::link).
199    /// In addition, `z` must be less than `N`.
200    #[inline(always)]
201    pub(crate) unsafe fn link_unchecked(&mut self, z: usize, packet_ptr: *mut u32, words: u8) {
202        debug_assert!(words as usize <= crate::chain::MAX_NODE_WORDS);
203        // SAFETY: forwarded contract; a word count below 256 fills the top byte only.
204        unsafe { self.link_tag_high_unchecked(z, packet_ptr, (words as u32) << 24) };
205    }
206
207    /// Prepend a primitive whose packet-word count is already stored in the
208    /// high byte of `tag_high` into an already-clamped depth slot.
209    ///
210    /// # Safety
211    /// Same requirements as [`link_unchecked`](Self::link_unchecked).
212    /// The low 24 bits of `tag_high` must be zero.
213    #[inline(always)]
214    pub(crate) unsafe fn link_tag_high_unchecked(
215        &mut self,
216        z: usize,
217        packet_ptr: *mut u32,
218        tag_high: u32,
219    ) {
220        debug_assert!(z < N);
221        debug_assert_eq!(tag_high & OT_ADDR_MASK, 0);
222        debug_assert!((tag_high >> 24) as usize <= crate::chain::MAX_NODE_WORDS);
223        debug_assert!(is_dma_reachable(packet_ptr), "GPU DMA reads main RAM only");
224        // SAFETY: the caller guarantees `z < N`.
225        let entry = unsafe { self.entries.get_unchecked_mut(z) };
226        let old_head = *entry & OT_ADDR_MASK;
227        // SAFETY: the caller guarantees a live, writable, aligned tag word.
228        unsafe { ptr::write_volatile(packet_ptr, tag_high | old_head) };
229        *entry = packet_ptr.expose_provenance() as u32 & OT_ADDR_MASK;
230    }
231
232    /// Insert an array of compact raw packet commands in caller order.
233    ///
234    /// Each command is two machine words: a packet pointer followed by a
235    /// packed word containing the OT slot in bits 0..15 and the GPU packet
236    /// word count in bits 24..31. Commands are consumed first-to-last, so the
237    /// OT's prepend semantics deliberately reverse commands which share a
238    /// slot. This matches repeated classic `addPrim` calls exactly.
239    ///
240    /// Use [`Self::link_packed_commands_reverse_unchecked`] when same-slot
241    /// submission order must instead be preserved.
242    ///
243    /// # Safety
244    /// `commands` must point to `command_count * 2` readable machine words in
245    /// the documented layout. Every packet pointer must meet the lifetime,
246    /// alignment, and writability requirements of [`Self::link_unchecked`],
247    /// every encoded slot must be less than `N`, and every word count at most
248    /// [`crate::chain::MAX_NODE_WORDS`].
249    #[inline]
250    pub(crate) unsafe fn link_packed_commands_unchecked(
251        &mut self,
252        commands: *const usize,
253        command_count: usize,
254    ) {
255        if command_count == 0 {
256            return;
257        }
258        debug_assert!(N > 0);
259
260        #[cfg(target_arch = "mips")]
261        {
262            // SAFETY: the caller guarantees `command_count * 2` readable words at `commands`.
263            let command_end = unsafe { commands.add(command_count.saturating_mul(2)) };
264            let entries = self.entries.as_mut_ptr();
265            // SAFETY: the loop reads the caller's command words and writes each packet's tag
266            // and its slot entry, all covered by this function's contract; it uses only the
267            // clobbered registers.
268            unsafe {
269                core::arch::asm!(
270                    ".set noreorder",
271                    "lui $15, 0x00ff",
272                    "ori $15, $15, 0xffff",
273                    "2:",
274                    "lw $11, 0($8)",
275                    "lw $12, 4($8)",
276                    "addiu $8, $8, 8",
277                    "sll $14, $11, 8",
278                    "andi $13, $12, 0xffff",
279                    "srl $14, $14, 8",
280                    "srl $12, $12, 24",
281                    "sll $13, $13, 2",
282                    "sll $12, $12, 24",
283                    "addu $13, $10, $13",
284                    "lw $9, 0($13)",
285                    "nop",
286                    "and $9, $9, $15",
287                    "or $9, $9, $12",
288                    "sw $9, 0($11)",
289                    "sw $14, 0($13)",
290                    "bne $8, $16, 2b",
291                    "nop",
292                    ".set reorder",
293                    inout("$8") commands => _,
294                    in("$10") entries,
295                    in("$16") command_end,
296                    lateout("$9") _,
297                    lateout("$11") _,
298                    lateout("$12") _,
299                    lateout("$13") _,
300                    lateout("$14") _,
301                    lateout("$15") _,
302                    options(nostack),
303                );
304            }
305        }
306
307        #[cfg(not(target_arch = "mips"))]
308        {
309            for index in 0..command_count {
310                // SAFETY: `index < command_count`, inside the caller's command array.
311                let command = unsafe { commands.add(index * 2) };
312                // SAFETY: the first word of each command is its packet pointer.
313                let packet_ptr =
314                    ptr::with_exposed_provenance_mut::<u32>(unsafe { ptr::read(command) });
315                // SAFETY: the second word of each command is its slot and word count.
316                let slot_words = unsafe { ptr::read(command.add(1)) } as u32;
317                let slot = (slot_words & u16::MAX as u32) as usize;
318                debug_assert!(slot < N);
319                // SAFETY: the caller guarantees the packet and the slot meet
320                // `link_tag_high_unchecked`'s contract.
321                unsafe { self.link_tag_high_unchecked(slot, packet_ptr, slot_words & 0xFF00_0000) };
322            }
323        }
324    }
325
326    /// Insert a reverse-ordered array of compact raw packet commands.
327    ///
328    /// Each command is exactly two machine words: a packet pointer followed by a
329    /// packed word containing the OT slot in bits 0..15 and the GPU packet
330    /// word count in bits 24..31. Commands are consumed last-to-first, which
331    /// preserves their original submission order despite OT insertion being
332    /// prepend-only.
333    ///
334    /// On PS1 this is one tightly scheduled MIPS loop, matching the direct OT
335    /// linking used by late commercial engines while retaining the caller's
336    /// exact same-slot packet order. Host builds use the scalar equivalent so
337    /// command-stream tests exercise identical semantics.
338    ///
339    /// # Safety
340    /// `commands` must point to `command_count * 2` readable machine words in the
341    /// documented layout. Every encoded packet pointer must meet the lifetime,
342    /// alignment, and writability requirements of [`Self::link_unchecked`],
343    /// every encoded slot must be less than `N`, and every word count at most
344    /// [`crate::chain::MAX_NODE_WORDS`].
345    #[inline]
346    pub(crate) unsafe fn link_packed_commands_reverse_unchecked(
347        &mut self,
348        commands: *const usize,
349        command_count: usize,
350    ) {
351        if command_count == 0 {
352            return;
353        }
354        debug_assert!(N > 0);
355
356        #[cfg(target_arch = "mips")]
357        {
358            // SAFETY: the caller guarantees `command_count * 2` readable words at `commands`.
359            let command_end = unsafe { commands.add(command_count.saturating_mul(2)) };
360            let entries = self.entries.as_mut_ptr();
361            // SAFETY: the loop reads the caller's command words and writes each packet's tag
362            // and its slot entry, all covered by this function's contract; it uses only the
363            // clobbered registers.
364            unsafe {
365                core::arch::asm!(
366                    ".set noreorder",
367                    "lui $15, 0x00ff",
368                    "ori $15, $15, 0xffff",
369                    "2:",
370                    "addiu $8, $8, -8",
371                    "lw $11, 0($8)",
372                    "lw $12, 4($8)",
373                    // Fill the packet-pointer load delay while making its
374                    // low-24-bit OT representation. Fill the metadata load
375                    // delay before reading its slot.
376                    "sll $14, $11, 8",
377                    "andi $13, $12, 0xffff",
378                    "srl $14, $14, 8",
379                    "srl $12, $12, 24",
380                    "sll $13, $13, 2",
381                    "sll $12, $12, 24",
382                    "addu $13, $10, $13",
383                    "lw $9, 0($13)",
384                    "nop",
385                    "and $9, $9, $15",
386                    "or $9, $9, $12",
387                    "sw $9, 0($11)",
388                    "sw $14, 0($13)",
389                    "bne $8, $16, 2b",
390                    "nop",
391                    ".set reorder",
392                    inout("$8") command_end => _,
393                    in("$10") entries,
394                    in("$16") commands,
395                    lateout("$9") _,
396                    lateout("$11") _,
397                    lateout("$12") _,
398                    lateout("$13") _,
399                    lateout("$14") _,
400                    lateout("$15") _,
401                    options(nostack),
402                );
403            }
404        }
405
406        #[cfg(not(target_arch = "mips"))]
407        {
408            let mut index = command_count;
409            while index != 0 {
410                index -= 1;
411                // SAFETY: `index < command_count`, inside the caller's command array.
412                let command = unsafe { commands.add(index * 2) };
413                // SAFETY: the first word of each command is its packet pointer.
414                let packet_ptr =
415                    ptr::with_exposed_provenance_mut::<u32>(unsafe { ptr::read(command) });
416                // SAFETY: the second word of each command is its slot and word count.
417                let slot_words = unsafe { ptr::read(command.add(1)) } as u32;
418                let slot = (slot_words & u16::MAX as u32) as usize;
419                debug_assert!(slot < N);
420                // SAFETY: the caller guarantees the packet and the slot meet
421                // `link_tag_high_unchecked`'s contract.
422                unsafe { self.link_tag_high_unchecked(slot, packet_ptr, slot_words & 0xFF00_0000) };
423            }
424        }
425    }
426
427    /// Insert a contiguous stream of classic tagged GPU packets.
428    ///
429    /// Before this call, each packet tag stores its GPU data-word count in
430    /// bits 24..31 and its target OT slot in bits 0..15. Packets are walked
431    /// from `first` to `end` and prepended in that order, exactly matching a
432    /// sequence of classic `addPrim` calls. A slot value of `0xffff` skips the
433    /// packet, which lets callers keep separately ordered HUD packets in the
434    /// same arena.
435    ///
436    /// This format lets C and retained-mode renderers stage depth keys in the
437    /// packet tags without a cross-language call or a separate command array
438    /// per primitive. The final link pass remains owned by PSoXide.
439    ///
440    /// # Safety
441    /// `first..end` must be a writable, contiguous sequence of complete GPU
442    /// packets. Every packet's word count must describe the next packet
443    /// exactly and be at most [`crate::chain::MAX_NODE_WORDS`], and every
444    /// non-sentinel slot must be less than `N`.
445    #[inline]
446    pub(crate) unsafe fn link_tagged_packet_stream_unchecked(
447        &mut self,
448        first: *mut u32,
449        end: *mut u32,
450    ) {
451        if first >= end {
452            return;
453        }
454        debug_assert!(N > 0);
455
456        #[cfg(target_arch = "mips")]
457        {
458            let entries = self.entries.as_mut_ptr();
459            // Sixteen instructions per packet, two RAM loads. Every OT slot
460            // already holds a 24-bit address with a zero top byte (`clear`,
461            // `link_unchecked*` and this loop all store masked packet
462            // addresses), so the old head needs no mask before the packet's
463            // word count is OR-ed in.
464            // SAFETY: the loop walks `first..end` by each packet's own word count, which the
465            // caller guarantees is exact, and writes only packet tags and in-range slot
466            // entries.
467            unsafe {
468                core::arch::asm!(
469                    ".set noreorder",
470                    // Persistent constants: low-24-bit DMA address mask, the
471                    // screen-packet sentinel staged by retained callers, and
472                    // the word-count byte mask.
473                    "lui $15, 0x00ff",
474                    "ori $15, $15, 0xffff",
475                    "ori $17, $0, 0xffff",
476                    "lui $18, 0xff00",
477                    "2:",
478                    "lw $9, 0($8)",
479                    // The packet's 24-bit address fills the tag load delay.
480                    "and $11, $8, $15",
481                    // tag>>22 is the packet byte count excluding its tag;
482                    // add four bytes while filling the sentinel branch slot.
483                    "srl $10, $9, 22",
484                    "andi $13, $9, 0xffff",
485                    "addu $10, $8, $10",
486                    "beq $13, $17, 3f",
487                    "addiu $10, $10, 4",
488                    // Prepend the packet to its already-bounded OT slot; the
489                    // word-count mask fills the OT-head load delay.
490                    "sll $13, $13, 2",
491                    "addu $13, $12, $13",
492                    "lw $14, 0($13)",
493                    "and $9, $9, $18",
494                    "or $14, $14, $9",
495                    "sw $14, 0($8)",
496                    "sw $11, 0($13)",
497                    "3:",
498                    "bne $10, $16, 2b",
499                    "move $8, $10",
500                    ".set reorder",
501                    inout("$8") first => _,
502                    in("$12") entries,
503                    in("$16") end,
504                    lateout("$9") _,
505                    lateout("$10") _,
506                    lateout("$11") _,
507                    lateout("$13") _,
508                    lateout("$14") _,
509                    lateout("$15") _,
510                    lateout("$17") _,
511                    lateout("$18") _,
512                    options(nostack),
513                );
514            }
515        }
516
517        #[cfg(not(target_arch = "mips"))]
518        {
519            let mut packet = first;
520            while packet < end {
521                // SAFETY: `packet` is below `end`, at a packet tag the caller vouches for.
522                let staged_tag = unsafe { ptr::read(packet) };
523                let words = (staged_tag >> 24) as usize;
524                let slot = (staged_tag & u16::MAX as u32) as usize;
525                // SAFETY: the tag's word count is exact, so this is the next packet or `end`.
526                let next = unsafe { packet.add(words + 1) };
527                if slot != u16::MAX as usize {
528                    debug_assert!(slot < N);
529                    // SAFETY: the caller guarantees a writable packet and an in-range slot.
530                    unsafe { self.link_tag_high_unchecked(slot, packet, staged_tag & 0xFF00_0000) };
531                }
532                packet = next;
533            }
534            debug_assert_eq!(packet, end);
535        }
536    }
537
538    /// Insert a tagged packet stream while quantising every non-sentinel OT
539    /// slot by a compile-time right shift.
540    ///
541    /// This preserves the packet sequence and every raw depth calculation,
542    /// but lets a caller back the final DMA chain with a smaller ordering
543    /// table. A staged slot of `0xffff` remains the screen-packet sentinel and
544    /// is skipped before the shift. For example, `SLOT_SHIFT = 3` maps the
545    /// classic 0..2047 depth range onto 256 slots.
546    ///
547    /// # Safety
548    ///
549    /// The packet lifetime and layout requirements match
550    /// [`Self::link_tagged_packet_stream_unchecked`]. Every shifted slot must
551    /// be less than `N`, and `SLOT_SHIFT` must be less than 16.
552    #[inline]
553    pub(crate) unsafe fn link_tagged_packet_stream_shifted_unchecked<const SLOT_SHIFT: u32>(
554        &mut self,
555        first: *mut u32,
556        end: *mut u32,
557    ) {
558        if first >= end {
559            return;
560        }
561        debug_assert!(N > 0);
562        debug_assert!(SLOT_SHIFT < 16);
563
564        #[cfg(target_arch = "mips")]
565        {
566            let entries = self.entries.as_mut_ptr();
567            // The `link_tagged_packet_stream_unchecked` loop with the slot
568            // shift after the sentinel test.
569            // SAFETY: the loop walks the caller's exact packet sequence and writes only tags
570            // and slot entries, which `SLOT_SHIFT` keeps below `N` per the contract.
571            unsafe {
572                core::arch::asm!(
573                    ".set noreorder",
574                    "lui $15, 0x00ff",
575                    "ori $15, $15, 0xffff",
576                    "ori $17, $0, 0xffff",
577                    "lui $18, 0xff00",
578                    "2:",
579                    "lw $9, 0($8)",
580                    "and $11, $8, $15",
581                    "srl $10, $9, 22",
582                    "andi $13, $9, 0xffff",
583                    "addu $10, $8, $10",
584                    "beq $13, $17, 3f",
585                    "addiu $10, $10, 4",
586                    "srl $13, $13, {slot_shift}",
587                    "sll $13, $13, 2",
588                    "addu $13, $12, $13",
589                    "lw $14, 0($13)",
590                    "and $9, $9, $18",
591                    "or $14, $14, $9",
592                    "sw $14, 0($8)",
593                    "sw $11, 0($13)",
594                    "3:",
595                    "bne $10, $16, 2b",
596                    "move $8, $10",
597                    ".set reorder",
598                    slot_shift = const SLOT_SHIFT,
599                    inout("$8") first => _,
600                    in("$12") entries,
601                    in("$16") end,
602                    lateout("$9") _,
603                    lateout("$10") _,
604                    lateout("$11") _,
605                    lateout("$13") _,
606                    lateout("$14") _,
607                    lateout("$15") _,
608                    lateout("$17") _,
609                    lateout("$18") _,
610                    options(nostack),
611                );
612            }
613        }
614
615        #[cfg(not(target_arch = "mips"))]
616        {
617            let mut packet = first;
618            while packet < end {
619                // SAFETY: `packet` is below `end`, at a packet tag the caller vouches for.
620                let staged_tag = unsafe { ptr::read(packet) };
621                let words = (staged_tag >> 24) as usize;
622                let slot = (staged_tag & u16::MAX as u32) as usize;
623                // SAFETY: the tag's word count is exact, so this is the next packet or `end`.
624                let next = unsafe { packet.add(words + 1) };
625                if slot != u16::MAX as usize {
626                    let shifted_slot = slot >> SLOT_SHIFT;
627                    debug_assert!(shifted_slot < N);
628                    // SAFETY: the caller guarantees a writable packet and an in-range shifted
629                    // slot.
630                    unsafe {
631                        self.link_tag_high_unchecked(shifted_slot, packet, staged_tag & 0xFF00_0000)
632                    };
633                }
634                packet = next;
635            }
636            debug_assert_eq!(packet, end);
637        }
638    }
639
640    /// Prepend a raw packet to slot `z` (clamped to `N - 1`).
641    ///
642    /// # Safety
643    ///
644    /// As [`OtFrame::add_raw`](crate::frame::OtFrame::add_raw).
645    #[deprecated(note = "use `OtFrame::add_raw`, through `frame()` or `resume_frame()`")]
646    #[inline(always)]
647    pub unsafe fn insert(&mut self, z: usize, packet_ptr: *mut u32, words: u8) {
648        // SAFETY: forwarded contract.
649        unsafe { self.link(z, packet_ptr, words) }
650    }
651
652    /// Prepend a raw packet to slot `z` without the clamp or length check.
653    ///
654    /// # Safety
655    ///
656    /// As [`OtFrame::add_raw_unchecked`](crate::frame::OtFrame::add_raw_unchecked).
657    #[deprecated(note = "use `OtFrame::add_raw_unchecked`, through `frame()` or `resume_frame()`")]
658    #[inline(always)]
659    pub unsafe fn insert_unchecked(&mut self, z: usize, packet_ptr: *mut u32, words: u8) {
660        // SAFETY: forwarded contract.
661        unsafe { self.link_unchecked(z, packet_ptr, words) }
662    }
663
664    /// Add packed two-word commands, last to first.
665    ///
666    /// # Safety
667    ///
668    /// As [`OtFrame::add_packed_commands_reverse_unchecked`](crate::frame::OtFrame::add_packed_commands_reverse_unchecked).
669    #[deprecated(
670        note = "use `OtFrame::add_packed_commands_reverse_unchecked`, through `frame()` or `resume_frame()`"
671    )]
672    #[inline(always)]
673    pub unsafe fn insert_packed_commands_reverse_unchecked(
674        &mut self,
675        commands: *const usize,
676        command_count: usize,
677    ) {
678        // SAFETY: forwarded contract.
679        unsafe { self.link_packed_commands_reverse_unchecked(commands, command_count) }
680    }
681
682    /// Add a contiguous stream of packets whose tags carry their slot.
683    ///
684    /// # Safety
685    ///
686    /// As [`OtFrame::add_tagged_packet_stream_unchecked`](crate::frame::OtFrame::add_tagged_packet_stream_unchecked).
687    #[deprecated(
688        note = "use `OtFrame::add_tagged_packet_stream_unchecked`, through `frame()` or `resume_frame()`"
689    )]
690    #[inline(always)]
691    pub unsafe fn insert_tagged_packet_stream_unchecked(&mut self, first: *mut u32, end: *mut u32) {
692        // SAFETY: forwarded contract.
693        unsafe { self.link_tagged_packet_stream_unchecked(first, end) }
694    }
695
696    /// End this table's DMA walk with GP0(1Fh), so the GPU raises
697    /// [`crate::is_draw_done`] once everything in the table is drawn.
698    ///
699    /// Links slot 0, the last one walked, to [`crate::chain::DRAW_DONE_NODE`];
700    /// packets inserted at slot 0 afterwards still draw before it. Call it
701    /// after every [`clear`](Self::clear) and before anything is inserted at
702    /// slot 0. Pair the submission with [`crate::arm_draw_done`].
703    ///
704    /// Host builds leave the table as it is: the shared node lives outside
705    /// the table's address window, which [`packets`](Self::packets)
706    /// relies on.
707    ///
708    /// # Panics
709    /// If slot 0 is not empty.
710    pub fn end_with_draw_done(&mut self) {
711        #[cfg(target_arch = "mips")]
712        // SAFETY: the shared node is immutable static RAM for the whole run.
713        unsafe {
714            self.end_with_node(crate::chain::DRAW_DONE_NODE.as_ptr())
715        };
716    }
717
718    /// Continue this table's walk into `head`, the first node of a chain
719    /// that ends the list itself: a command recording
720    /// (`psx_io::gpu::begin_recording_raw`) closed on [`crate::chain::DRAW_DONE_NODE`],
721    /// say, for a frame published to `psx_rt::present`.
722    ///
723    /// # Safety
724    ///
725    /// `head` must point at a 4-byte-aligned node tag in RAM, and every node
726    /// reachable from it must meet [`crate::chain::submit_async_raw`]'s
727    /// contract (at most [`crate::chain::MAX_NODE_WORDS`] payload words each, live
728    /// and unmodified) until every walk of this table has finished.
729    ///
730    /// # Panics
731    ///
732    /// If slot 0 is not empty: call it right after [`clear`](Self::clear),
733    /// before anything is inserted at slot 0.
734    #[inline]
735    pub unsafe fn end_with_chain(&mut self, head: *const u32) {
736        // SAFETY: forwarded contract.
737        unsafe { self.end_with_node(head) }
738    }
739
740    /// Link slot 0 to `node`, a node whose own link ends the list.
741    ///
742    /// # Safety
743    /// `node` must stay live and unmodified while the table is submitted.
744    pub(crate) unsafe fn end_with_node(&mut self, node: *const u32) {
745        assert!(
746            self.entries[0] == OT_END,
747            "end_with_draw_done needs an empty slot 0: call it right after clear"
748        );
749        self.entries[0] = node.expose_provenance() as u32 & OT_ADDR_MASK;
750    }
751
752    /// Pointer to the slot where DMA starts (`[N-1]`). Passed to
753    /// [`crate::chain::submit_raw`] as the linked-list entry point.
754    #[inline]
755    pub fn submit_head(&self) -> *const u32 {
756        // From the whole array, so the pointer may reach every entry.
757        self.entries.as_ptr().wrapping_add(N - 1)
758    }
759
760    /// Submit the whole table to GPU via DMA channel 2 linked-list
761    /// mode and wait for completion.
762    ///
763    /// # Safety
764    ///
765    /// As [`crate::chain::submit_raw`] for the chain this table heads:
766    /// every packet linked into it must be live and unmodified, and the
767    /// table must not have moved since it was cleared.
768    #[deprecated(
769        note = "nothing proves the linked packets are alive; use `OrderingTable::frame` and `OtFrame::submit`"
770    )]
771    pub unsafe fn submit(&self) {
772        // SAFETY: forwarded contract.
773        unsafe { crate::chain::start_walk(self.submit_head()) };
774        crate::chain::wait_walk();
775    }
776
777    /// Kick the table's DMA walk without waiting for it to finish; pair
778    /// it with [`crate::chain::wait`].
779    ///
780    /// # Safety
781    ///
782    /// As [`crate::chain::submit_async_raw`]: the table and every
783    /// packet it chains must stay live, unmoved and unmodified until that
784    /// wait returns.
785    #[deprecated(
786        note = "returns mid-walk with the table still mutable; use `OtFrame::submit_with`, `FrameStorage::draw_async` or `FramePair`"
787    )]
788    pub unsafe fn submit_async(&self) {
789        // SAFETY: forwarded contract.
790        unsafe { crate::chain::start_walk(self.submit_head()) };
791    }
792
793    /// Walk the linked chain in DMA submission order, producing one
794    /// `(packet_ptr, words)` pair per primitive packet.
795    ///
796    /// Used by the editor's host-side preview to convert an OT into a
797    /// `psx-gpu-render` command log without DMAing through real
798    /// hardware. The hardware DMA walker follows the same links in
799    /// the same order, so the iterator yields what the GPU would consume.
800    ///
801    /// A link holds only the low 24 bits of an address, as on the console.
802    /// A link into this table is read through the table; any other link is
803    /// read as an exposed address with the table's upper address bits,
804    /// which every packet in console RAM shares.
805    ///
806    /// # Safety
807    /// Every chained packet must be live for the lifetime of the
808    /// returned iterator, exactly the invariant a submission requires, and
809    /// its address must have been exposed, as every insert does.
810    pub unsafe fn packets(&self) -> Packets<'_> {
811        let table = self.entries.as_ptr();
812        Packets {
813            table,
814            table_low: table.addr() as u32 & OT_ADDR_MASK,
815            table_bytes: N * 4,
816            next: self.entries[N - 1] & OT_ADDR_MASK,
817            base_high: table.addr() & !(OT_ADDR_MASK as usize),
818            last_packet: OT_END,
819            remaining_hops: N.saturating_add(OT_MAX_EXTRA_HOPS),
820            _table: PhantomData,
821        }
822    }
823}
824
825/// Walks an [`OrderingTable`]'s chain in DMA submission order.
826///
827/// Each `next()` returns the pointer to a packet and the number of
828/// data words that follow its tag (so the full packet occupies
829/// `1 + words` u32s starting at the returned pointer). The terminal
830/// `0x00FFFFFF` marker stops iteration cleanly.
831#[derive(Debug)]
832pub struct Packets<'a> {
833    /// The table's first entry, with the table's provenance.
834    table: *const u32,
835    /// Low 24 bits of `table`'s address, as links name it.
836    table_low: u32,
837    /// Size of the table in bytes.
838    table_bytes: usize,
839    next: u32,
840    base_high: usize,
841    last_packet: u32,
842    remaining_hops: usize,
843    _table: PhantomData<&'a [u32]>,
844}
845
846impl Packets<'_> {
847    /// The word `link` names: a table entry through the table, anything
848    /// else through its exposed address.
849    fn resolve(&self, link: u32) -> *const u32 {
850        let offset = link.wrapping_sub(self.table_low) as usize;
851        if offset < self.table_bytes {
852            self.table.wrapping_byte_add(offset)
853        } else {
854            ptr::with_exposed_provenance(self.base_high | link as usize)
855        }
856    }
857}
858
859impl Iterator for Packets<'_> {
860    type Item = (*const u32, u8);
861
862    fn next(&mut self) -> Option<Self::Item> {
863        // Walk the chain, skipping empty stepping-stones -- OT slots
864        // that hold `words=0` because they were never targeted by an
865        // `insert`. The DMA hardware silently no-ops through those
866        // and only forwards entries with actual packet data, so this
867        // iterator presents the same view to the cmd-log adapter.
868        loop {
869            if self.next == OT_END {
870                return None;
871            }
872            if self.remaining_hops == 0 || self.next == self.last_packet {
873                self.next = OT_END;
874                return None;
875            }
876            self.remaining_hops -= 1;
877            let link = self.next;
878            let ptr = self.resolve(link);
879            // SAFETY: ptr was reached by walking the chain that
880            // `packets`'s caller swore was live; tag word is
881            // always present in any chained slot.
882            let tag = unsafe { ptr::read_volatile(ptr) };
883            let words = ((tag >> 24) & 0xFF) as u8;
884            self.next = tag & OT_ADDR_MASK;
885            if words > 0 {
886                self.last_packet = link;
887                return Some((ptr, words));
888            }
889        }
890    }
891}
892
893/// Shows the slot count, not the `N` link words.
894impl<const N: usize> core::fmt::Debug for OrderingTable<N> {
895    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
896        f.debug_struct("OrderingTable")
897            .field("slots", &N)
898            .finish_non_exhaustive()
899    }
900}
901
902impl<const N: usize> Default for OrderingTable<N> {
903    fn default() -> Self {
904        Self::new()
905    }
906}
907
908#[cfg(all(test, not(target_arch = "mips")))]
909mod tests {
910    use super::*;
911
912    #[repr(C)]
913    struct PackedCommand {
914        /// The packet's exposed address.
915        packet: usize,
916        slot_words: usize,
917    }
918
919    #[test]
920    fn packed_reverse_insert_preserves_submission_order_within_slots() {
921        let mut ot: OrderingTable<8> = OrderingTable::new();
922        ot.clear();
923        let mut a = [0u32; 2];
924        let mut b = [0u32; 2];
925        let mut c = [0u32; 2];
926        let commands = [
927            PackedCommand {
928                packet: a.as_mut_ptr().expose_provenance(),
929                slot_words: 4 | (1 << 24),
930            },
931            PackedCommand {
932                packet: b.as_mut_ptr().expose_provenance(),
933                slot_words: 4 | (1 << 24),
934            },
935            PackedCommand {
936                packet: c.as_mut_ptr().expose_provenance(),
937                slot_words: 2 | (1 << 24),
938            },
939        ];
940
941        // SAFETY: the packets are locals that outlive every use of the table in this test, and
942        // their slots and word counts fit the table.
943        unsafe {
944            ot.link_packed_commands_reverse_unchecked(
945                commands.as_ptr().cast::<usize>(),
946                commands.len(),
947            );
948        }
949
950        // SAFETY: every packet linked into the table is a local still alive here.
951        let mut iter = unsafe { ot.packets() };
952        assert_eq!(iter.next().unwrap().0, a.as_ptr());
953        assert_eq!(iter.next().unwrap().0, b.as_ptr());
954        assert_eq!(iter.next().unwrap().0, c.as_ptr());
955        assert!(iter.next().is_none());
956    }
957
958    #[test]
959    fn packed_forward_insert_matches_repeated_prepend_semantics() {
960        let mut ot: OrderingTable<8> = OrderingTable::new();
961        ot.clear();
962        let mut a = [0u32; 2];
963        let mut b = [0u32; 2];
964        let commands = [
965            PackedCommand {
966                packet: a.as_mut_ptr().expose_provenance(),
967                slot_words: 4 | (1 << 24),
968            },
969            PackedCommand {
970                packet: b.as_mut_ptr().expose_provenance(),
971                slot_words: 4 | (1 << 24),
972            },
973        ];
974
975        // SAFETY: the packets are locals that outlive every use of the table in this test, and
976        // their slots and word counts fit the table.
977        unsafe {
978            ot.link_packed_commands_unchecked(commands.as_ptr().cast::<usize>(), commands.len());
979        }
980
981        // SAFETY: every packet linked into the table is a local still alive here.
982        let mut iter = unsafe { ot.packets() };
983        assert_eq!(iter.next().unwrap().0, b.as_ptr());
984        assert_eq!(iter.next().unwrap().0, a.as_ptr());
985        assert!(iter.next().is_none());
986    }
987
988    #[test]
989    fn tagged_packet_stream_matches_prepend_and_skips_sentinel_packets() {
990        let mut ot: OrderingTable<8> = OrderingTable::new();
991        ot.clear();
992        let mut packets = [
993            (1 << 24) | 4,
994            0xAAAA_AAAA,
995            (2 << 24) | u16::MAX as u32,
996            0xBBBB_BBBB,
997            0xCCCC_CCCC,
998            (1 << 24) | 4,
999            0xDDDD_DDDD,
1000        ];
1001
1002        let first = packets.as_mut_ptr();
1003        // SAFETY: the packets are locals that outlive every use of the table in this test, and
1004        // their slots and word counts fit the table.
1005        unsafe {
1006            ot.link_tagged_packet_stream_unchecked(first, first.add(packets.len()));
1007        }
1008
1009        // SAFETY: every packet linked into the table is a local still alive here.
1010        let mut iter = unsafe { ot.packets() };
1011        // SAFETY: index 5 is inside `packets`.
1012        assert_eq!(iter.next().unwrap().0, unsafe { packets.as_ptr().add(5) });
1013        assert_eq!(iter.next().unwrap().0, packets.as_ptr());
1014        assert!(iter.next().is_none());
1015        assert_eq!(packets[2] & 0x00ff_ffff, u16::MAX as u32);
1016    }
1017
1018    #[test]
1019    fn shifted_tagged_stream_quantises_slots_after_the_sentinel_test() {
1020        let mut ot: OrderingTable<4> = OrderingTable::new();
1021        ot.clear();
1022        let mut packets = [
1023            (1 << 24) | 31,
1024            0xAAAA_AAAA,
1025            (1 << 24) | u16::MAX as u32,
1026            0xBBBB_BBBB,
1027            (1 << 24) | 24,
1028            0xCCCC_CCCC,
1029        ];
1030
1031        // SAFETY: `packets` is one contiguous run of complete packets whose slots fit the
1032        // table.
1033        unsafe {
1034            {
1035                let first = packets.as_mut_ptr();
1036                ot.link_tagged_packet_stream_shifted_unchecked::<3>(
1037                    first,
1038                    first.add(packets.len()),
1039                );
1040            }
1041        }
1042
1043        // SAFETY: every packet linked into the table is a local still alive here.
1044        let mut iter = unsafe { ot.packets() };
1045        // SAFETY: index 4 is inside `packets`.
1046        assert_eq!(iter.next().unwrap().0, unsafe { packets.as_ptr().add(4) });
1047        assert_eq!(iter.next().unwrap().0, packets.as_ptr());
1048        assert!(iter.next().is_none());
1049        assert_eq!(packets[2] & OT_ADDR_MASK, u16::MAX as u32);
1050    }
1051
1052    /// Build a primitive packet by hand (one tag word + N data words),
1053    /// insert it, and walk the chain. The iterator must report the
1054    /// same `(ptr, words)` pair we inserted.
1055    #[test]
1056    fn packets_walks_a_single_inserted_primitive() {
1057        let mut ot: OrderingTable<8> = OrderingTable::new();
1058        ot.clear();
1059        // Packet layout: [tag, w0, w1, w2] -- 3 data words after the tag.
1060        let mut packet: [u32; 4] = [0; 4];
1061        packet[1] = 0xAAAA_BBBB;
1062        packet[2] = 0xCCCC_DDDD;
1063        packet[3] = 0xEEEE_FFFF;
1064        // SAFETY: the packets are locals that outlive every use of the table in this test, and
1065        // their slots and word counts fit the table.
1066        unsafe {
1067            ot.link(2, packet.as_mut_ptr(), 3);
1068        }
1069
1070        // SAFETY: every packet linked into the table is a local still alive here.
1071        let mut iter = unsafe { ot.packets() };
1072        let entry = iter.next().expect("one entry");
1073        assert_eq!(entry.0 as usize, packet.as_ptr() as usize);
1074        assert_eq!(entry.1, 3);
1075        assert!(iter.next().is_none());
1076    }
1077
1078    /// Two primitives in different slots -- chain walks both; later
1079    /// inserts (lower slot) come first because `clear()` chains
1080    /// high-to-low and the DMA head is `[N-1]`.
1081    #[test]
1082    fn packets_walks_multiple_slots_in_dma_order() {
1083        let mut ot: OrderingTable<8> = OrderingTable::new();
1084        ot.clear();
1085        let mut a: [u32; 2] = [0, 0xA];
1086        let mut b: [u32; 2] = [0, 0xB];
1087        // SAFETY: the packets are locals that outlive every use of the table in this test, and
1088        // their slots and word counts fit the table.
1089        unsafe {
1090            // a is in a deeper (further from camera) slot than b, so b
1091            // should appear first when walking from the head.
1092            ot.link(2, a.as_mut_ptr(), 1);
1093            ot.link(5, b.as_mut_ptr(), 1);
1094        }
1095
1096        // SAFETY: every packet linked into the table is a local still alive here.
1097        let mut iter = unsafe { ot.packets() };
1098        // DMA walker starts at [N-1] = [7] and chains down to [0].
1099        // b lives in slot 5, a in slot 2 -- both should appear, b first.
1100        let first = iter.next().expect("first entry").0 as usize;
1101        let second = iter.next().expect("second entry").0 as usize;
1102        assert!(iter.next().is_none());
1103        assert_eq!(first, b.as_ptr() as usize);
1104        assert_eq!(second, a.as_ptr() as usize);
1105    }
1106
1107    #[test]
1108    fn insert_takes_a_full_fifo_of_words() {
1109        let mut ot: OrderingTable<4> = OrderingTable::new();
1110        ot.clear();
1111        let mut packet = [0u32; 1 + crate::chain::MAX_NODE_WORDS];
1112        // SAFETY: the packets are locals that outlive every use of the table in this test, and
1113        // their slots and word counts fit the table.
1114        unsafe { ot.link(1, packet.as_mut_ptr(), crate::chain::MAX_NODE_WORDS as u8) };
1115        assert_eq!(packet[0] >> 24, crate::chain::MAX_NODE_WORDS as u32);
1116    }
1117
1118    #[test]
1119    #[should_panic(expected = "longer than MAX_NODE_WORDS")]
1120    fn insert_refuses_a_node_longer_than_the_fifo() {
1121        let mut ot: OrderingTable<4> = OrderingTable::new();
1122        ot.clear();
1123        let mut packet = [0u32; 2 + crate::chain::MAX_NODE_WORDS];
1124        // SAFETY: the call panics on the word count before it touches the packet.
1125        unsafe {
1126            ot.link(
1127                1,
1128                packet.as_mut_ptr(),
1129                crate::chain::MAX_NODE_WORDS as u8 + 1,
1130            )
1131        };
1132    }
1133
1134    /// The draw-done node is the last thing walked, after every slot and
1135    /// after packets inserted at slot 0 once it is linked.
1136    #[test]
1137    fn end_with_node_puts_the_node_after_every_packet() {
1138        let mut ot: OrderingTable<4> = OrderingTable::new();
1139        ot.clear();
1140        let node = [(1u32 << 24) | OT_END, 0x1F00_0000];
1141        let mut far: [u32; 2] = [0, 0xF];
1142        let mut near: [u32; 2] = [0, 0xE];
1143        // SAFETY: the packets are locals that outlive every use of the table in this test, and
1144        // their slots and word counts fit the table.
1145        unsafe {
1146            ot.end_with_node(node.as_ptr());
1147            ot.link(0, far.as_mut_ptr(), 1);
1148            ot.link(3, near.as_mut_ptr(), 1);
1149        }
1150        let walked: [usize; 3] = {
1151            // SAFETY: every packet linked into the table is a local still alive here.
1152            let mut iter = unsafe { ot.packets() };
1153            let order = [
1154                iter.next().expect("near").0 as usize,
1155                iter.next().expect("far").0 as usize,
1156                iter.next().expect("node").0 as usize,
1157            ];
1158            assert!(iter.next().is_none());
1159            order
1160        };
1161        assert_eq!(
1162            walked,
1163            [
1164                near.as_ptr() as usize,
1165                far.as_ptr() as usize,
1166                node.as_ptr() as usize
1167            ]
1168        );
1169    }
1170
1171    #[test]
1172    #[should_panic(expected = "empty slot 0")]
1173    fn end_with_node_refuses_a_used_slot_0() {
1174        let mut ot: OrderingTable<4> = OrderingTable::new();
1175        ot.clear();
1176        let node = [(1u32 << 24) | OT_END, 0x1F00_0000];
1177        let mut packet: [u32; 2] = [0, 0xF];
1178        // SAFETY: the packet and the node are locals that outlive the table's use.
1179        unsafe {
1180            ot.link(0, packet.as_mut_ptr(), 1);
1181            ot.end_with_node(node.as_ptr());
1182        }
1183    }
1184
1185    #[test]
1186    fn the_shared_draw_done_node_is_one_gp0_1f_word_then_the_end() {
1187        // SAFETY: the node is two words of immutable static data.
1188        let node = unsafe { core::slice::from_raw_parts(crate::chain::DRAW_DONE_NODE.as_ptr(), 2) };
1189        assert_eq!(node[0] >> 24, 1);
1190        assert_eq!(node[0] & OT_ADDR_MASK, OT_END);
1191        assert_eq!(node[1], psx_hw::gpu::gp0::REQUEST_IRQ);
1192        assert_eq!(node[1] >> 24, 0x1F);
1193    }
1194
1195    /// Multiple primitives in the same slot chain via the most-
1196    /// recently-inserted-first rule.
1197    #[test]
1198    fn packets_chains_primitives_within_one_slot() {
1199        let mut ot: OrderingTable<4> = OrderingTable::new();
1200        ot.clear();
1201        let mut first: [u32; 2] = [0, 0x1111];
1202        let mut second: [u32; 2] = [0, 0x2222];
1203        // SAFETY: the packets are locals that outlive every use of the table in this test, and
1204        // their slots and word counts fit the table.
1205        unsafe {
1206            ot.link(1, first.as_mut_ptr(), 1);
1207            ot.link(1, second.as_mut_ptr(), 1);
1208        }
1209
1210        // SAFETY: every packet linked into the table is a local still alive here.
1211        let mut iter = unsafe { ot.packets() };
1212        // `second` was inserted last and prepends to the chain head;
1213        // it walks first.
1214        let head = iter.next().expect("first").0 as usize;
1215        let tail = iter.next().expect("second").0 as usize;
1216        assert!(iter.next().is_none());
1217        assert_eq!(head, second.as_ptr() as usize);
1218        assert_eq!(tail, first.as_ptr() as usize);
1219    }
1220
1221    /// Re-inserting the same packet is an invalid OT chain because
1222    /// the packet only has one tag word to store its next pointer.
1223    /// The host iterator should fail closed instead of spinning
1224    /// forever while previewing a malformed frame.
1225    #[test]
1226    fn packets_stops_on_duplicate_packet_in_same_slot() {
1227        let mut ot: OrderingTable<4> = OrderingTable::new();
1228        ot.clear();
1229        let mut packet: [u32; 2] = [0, 0xAA00_0000];
1230        // SAFETY: the packets are locals that outlive every use of the table in this test, and
1231        // their slots and word counts fit the table.
1232        unsafe {
1233            ot.link(1, packet.as_mut_ptr(), 1);
1234            ot.link(1, packet.as_mut_ptr(), 1);
1235        }
1236
1237        // SAFETY: every packet linked into the table is a local still alive here.
1238        let mut iter = unsafe { ot.packets() };
1239        let entry = iter.next().expect("first duplicate packet");
1240        assert_eq!(entry.0 as usize, packet.as_ptr() as usize);
1241        assert_eq!(entry.1, 1);
1242        assert!(iter.next().is_none());
1243    }
1244
1245    /// A duplicate packet can also form a two-hop cycle through an
1246    /// empty OT slot when it is inserted into different slots. This
1247    /// catches the editor-preview failure mode where the cmd-log walk
1248    /// could peg the host thread.
1249    #[test]
1250    fn packets_stops_on_duplicate_packet_through_empty_slot() {
1251        let mut ot: OrderingTable<4> = OrderingTable::new();
1252        ot.clear();
1253        let mut packet: [u32; 2] = [0, 0xBB00_0000];
1254        // SAFETY: the packets are locals that outlive every use of the table in this test, and
1255        // their slots and word counts fit the table.
1256        unsafe {
1257            ot.link(1, packet.as_mut_ptr(), 1);
1258            ot.link(2, packet.as_mut_ptr(), 1);
1259        }
1260
1261        // SAFETY: every packet linked into the table is a local still alive here.
1262        let mut iter = unsafe { ot.packets() };
1263        let entry = iter.next().expect("first duplicate packet");
1264        assert_eq!(entry.0 as usize, packet.as_ptr() as usize);
1265        assert_eq!(entry.1, 1);
1266        assert!(iter.next().is_none());
1267    }
1268}