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}