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

psx_gpu/
prim.rs

1//! Primitive packet types for DMA-based GPU submission.
2//!
3//! Each struct is `#[repr(C)]` with the tag word first -- that's the
4//! shape the DMA linked-list walker expects. The field names match
5//! the on-wire GP0 word order so a reader can cross-reference
6//! PSX-SPX without redundant decoding.
7//!
8//! Builders (`new` constructors) zero the tag; [`crate::frame::OtFrame::add`]
9//! fills it in during insertion with `(words_after_tag << 24) | next`.
10
11use crate::material::{
12    BlendMode, TextureMaterial, TexturedGouraudPacketMaterial, TexturedPacketMaterial,
13};
14use psx_hw::gpu::packet::{self, SEMI_TRANSPARENT};
15use psx_hw::gpu::{gp0, pack_color, pack_texcoord, pack_vertex, pack_xy};
16
17/// Flat-shaded triangle. 5 words (tag + 4 data).
18#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
19#[repr(C, align(4))]
20pub struct TriFlat {
21    /// DMA / OT linkage word. Written by the OT at insert time.
22    pub tag: u32,
23    /// `0x20000000 | rgb24` header.
24    pub color_cmd: u32,
25    /// Vertex 0 packed via [`pack_vertex`].
26    pub v0: u32,
27    /// Vertex 1.
28    pub v1: u32,
29    /// Vertex 2.
30    pub v2: u32,
31}
32
33impl TriFlat {
34    /// Data-word count after the tag. Passed to `ot::add`.
35    pub const WORDS: u8 = 4;
36
37    /// Build a flat triangle ready for OT insertion.
38    pub const fn new(verts: [(i16, i16); 3], r: u8, g: u8, b: u8) -> Self {
39        Self {
40            tag: 0,
41            color_cmd: gp0::polygon_opcode(false, false, false, false, false) | pack_color(r, g, b),
42            v0: pack_vertex(verts[0].0, verts[0].1),
43            v1: pack_vertex(verts[1].0, verts[1].1),
44            v2: pack_vertex(verts[2].0, verts[2].1),
45        }
46    }
47
48    /// The same triangle drawn with the semi-transparency equation set by the
49    /// last GP0(E1h) ([`crate::Gpu::set_draw_mode`]).
50    pub const fn translucent(mut self) -> Self {
51        self.color_cmd |= SEMI_TRANSPARENT;
52        self
53    }
54}
55
56/// Gouraud-shaded triangle. 7 words (tag + 6 data).
57#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
58#[repr(C, align(4))]
59pub struct TriGouraud {
60    /// OT linkage.
61    pub tag: u32,
62    /// Vertex 0: `opcode | color0`.
63    pub color0_cmd: u32,
64    /// Vertex 0 position.
65    pub v0: u32,
66    /// Vertex 1 color.
67    pub color1: u32,
68    /// Vertex 1 position.
69    pub v1: u32,
70    /// Vertex 2 color.
71    pub color2: u32,
72    /// Vertex 2 position.
73    pub v2: u32,
74}
75
76impl TriGouraud {
77    /// Data-word count after the tag.
78    pub const WORDS: u8 = 6;
79
80    /// Build a Gouraud-shaded triangle.
81    pub const fn new(verts: [(i16, i16); 3], colors: [(u8, u8, u8); 3]) -> Self {
82        let (r0, g0, b0) = colors[0];
83        let (r1, g1, b1) = colors[1];
84        let (r2, g2, b2) = colors[2];
85        Self {
86            tag: 0,
87            color0_cmd: gp0::polygon_opcode(true, false, false, false, false)
88                | pack_color(r0, g0, b0),
89            v0: pack_vertex(verts[0].0, verts[0].1),
90            color1: pack_color(r1, g1, b1),
91            v1: pack_vertex(verts[1].0, verts[1].1),
92            color2: pack_color(r2, g2, b2),
93            v2: pack_vertex(verts[2].0, verts[2].1),
94        }
95    }
96
97    /// The same triangle drawn with the semi-transparency equation set by the
98    /// last GP0(E1h) ([`crate::Gpu::set_draw_mode`]).
99    pub const fn translucent(mut self) -> Self {
100        self.color0_cmd |= SEMI_TRANSPARENT;
101        self
102    }
103}
104
105/// Flat-shaded quad. 6 words (tag + 5 data).
106#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
107#[repr(C, align(4))]
108pub struct QuadFlat {
109    /// OT linkage.
110    pub tag: u32,
111    /// `opcode | color`.
112    pub color_cmd: u32,
113    /// Vertex 0.
114    pub v0: u32,
115    /// Vertex 1.
116    pub v1: u32,
117    /// Vertex 2.
118    pub v2: u32,
119    /// Vertex 3.
120    pub v3: u32,
121}
122
123impl QuadFlat {
124    /// Data-word count.
125    pub const WORDS: u8 = 5;
126
127    /// Build a flat quad.
128    pub const fn new(verts: [(i16, i16); 4], r: u8, g: u8, b: u8) -> Self {
129        Self {
130            tag: 0,
131            color_cmd: gp0::polygon_opcode(false, true, false, false, false) | pack_color(r, g, b),
132            v0: pack_vertex(verts[0].0, verts[0].1),
133            v1: pack_vertex(verts[1].0, verts[1].1),
134            v2: pack_vertex(verts[2].0, verts[2].1),
135            v3: pack_vertex(verts[3].0, verts[3].1),
136        }
137    }
138
139    /// The same quad drawn with the semi-transparency equation set by the
140    /// last GP0(E1h) ([`crate::Gpu::set_draw_mode`]).
141    pub const fn translucent(mut self) -> Self {
142        self.color_cmd |= SEMI_TRANSPARENT;
143        self
144    }
145
146    /// An axis-aligned rectangle: `size` pixels from `origin`, in `color`.
147    ///
148    /// Unlike [`FillRect`] it goes through the rasteriser, so it clips to
149    /// the draw area and follows the draw offset: the right shape for UI
150    /// panels and HUD backgrounds.
151    pub const fn rect(origin: (i16, i16), size: (u16, u16), color: (u8, u8, u8)) -> Self {
152        let (x0, y0) = origin;
153        let x1 = x0.wrapping_add(size.0 as i16);
154        let y1 = y0.wrapping_add(size.1 as i16);
155        Self::new(
156            [(x0, y0), (x1, y0), (x0, y1), (x1, y1)],
157            color.0,
158            color.1,
159            color.2,
160        )
161    }
162}
163
164/// Untextured variable-size rectangle. 4 words (tag + 3 data).
165/// Ignores draw-area clip on some GPU revisions; prefer `QuadFlat`
166/// when you need clipping.
167#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
168#[repr(C, align(4))]
169pub struct RectFlat {
170    /// OT linkage.
171    pub tag: u32,
172    /// `0x60000000 | color` (monochrome rect opcode).
173    pub color_cmd: u32,
174    /// Top-left `xy`.
175    pub xy: u32,
176    /// Size `wh`.
177    pub wh: u32,
178}
179
180impl RectFlat {
181    /// Data-word count.
182    pub const WORDS: u8 = 3;
183
184    /// Build a rect.
185    pub const fn new(x: i16, y: i16, w: u16, h: u16, r: u8, g: u8, b: u8) -> Self {
186        Self {
187            tag: 0,
188            color_cmd: packet::FLAT_RECT | pack_color(r, g, b),
189            xy: pack_vertex(x, y),
190            wh: pack_xy(w, h),
191        }
192    }
193}
194
195/// VRAM fill (GP0 02h). 4 words (tag + 3 data).
196///
197/// Writes VRAM directly: it ignores the draw area, the draw offset and the
198/// mask bits, and the GPU rounds the X range to 16-pixel steps (psx-spx,
199/// GP0(02h)). Use it to clear a buffer; use [`QuadFlat::rect`] for a
200/// panel that should clip and follow the draw offset.
201#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
202#[repr(C, align(4))]
203pub struct FillRect {
204    /// OT linkage.
205    pub tag: u32,
206    /// `0x02000000 | color`.
207    pub color_command: u32,
208    /// Top-left corner in VRAM.
209    pub origin: u32,
210    /// Width and height.
211    pub size: u32,
212}
213
214impl FillRect {
215    /// Data-word count.
216    pub const WORDS: u8 = 3;
217
218    /// Fill `size` pixels of VRAM at `origin` with `color`.
219    pub const fn new(origin: (u16, u16), size: (u16, u16), color: (u8, u8, u8)) -> Self {
220        Self {
221            tag: 0,
222            color_command: gp0::fill_rect(color.0, color.1, color.2),
223            origin: pack_xy(origin.0, origin.1),
224            size: pack_xy(size.0, size.1),
225        }
226    }
227}
228
229/// Gouraud-shaded quad. 9 words (tag + 8 data).
230///
231/// Same vertex order as [`QuadFlat`] (V0=TL, V1=TR, V2=BL, V3=BR
232/// by convention, though the GPU actually draws (V0,V1,V2) then
233/// (V1,V2,V3)). Each vertex carries its own RGB; the GPU
234/// gouraud-interpolates across the primitive.
235#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
236#[repr(C, align(4))]
237pub struct QuadGouraud {
238    /// OT linkage.
239    pub tag: u32,
240    /// Vertex 0 colour + polygon opcode.
241    pub color0_cmd: u32,
242    /// Vertex 0 position.
243    pub v0: u32,
244    /// Vertex 1 colour.
245    pub color1: u32,
246    /// Vertex 1 position.
247    pub v1: u32,
248    /// Vertex 2 colour.
249    pub color2: u32,
250    /// Vertex 2 position.
251    pub v2: u32,
252    /// Vertex 3 colour.
253    pub color3: u32,
254    /// Vertex 3 position.
255    pub v3: u32,
256}
257
258impl QuadGouraud {
259    /// Data-word count after the tag.
260    pub const WORDS: u8 = 8;
261
262    /// Build a Gouraud quad. `colors[i]` corresponds to `verts[i]`.
263    pub const fn new(verts: [(i16, i16); 4], colors: [(u8, u8, u8); 4]) -> Self {
264        let (r0, g0, b0) = colors[0];
265        let (r1, g1, b1) = colors[1];
266        let (r2, g2, b2) = colors[2];
267        let (r3, g3, b3) = colors[3];
268        Self {
269            tag: 0,
270            color0_cmd: gp0::polygon_opcode(true, true, false, false, false)
271                | pack_color(r0, g0, b0),
272            v0: pack_vertex(verts[0].0, verts[0].1),
273            color1: pack_color(r1, g1, b1),
274            v1: pack_vertex(verts[1].0, verts[1].1),
275            color2: pack_color(r2, g2, b2),
276            v2: pack_vertex(verts[2].0, verts[2].1),
277            color3: pack_color(r3, g3, b3),
278            v3: pack_vertex(verts[3].0, verts[3].1),
279        }
280    }
281
282    /// The same quad drawn with the semi-transparency equation set by the
283    /// last GP0(E1h) ([`crate::Gpu::set_draw_mode`]).
284    pub const fn translucent(mut self) -> Self {
285        self.color0_cmd |= SEMI_TRANSPARENT;
286        self
287    }
288}
289
290/// Semi-transparent Gouraud quad with its GP0(E1) blend state embedded in
291/// the same DMA packet.
292///
293/// Untextured translucent polygons do not carry blend bits of their own: they
294/// read them from the current draw mode. Keeping the state word and polygon in
295/// one OT packet makes the result independent of whichever textured surface
296/// happened to draw immediately before it.
297#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
298#[repr(C, align(4))]
299pub struct QuadGouraudBlended {
300    /// OT linkage.
301    pub tag: u32,
302    /// GP0(E1) draw mode selecting the native semi-transparency equation.
303    pub draw_mode: u32,
304    /// Vertex 0 colour + semi-transparent Gouraud-quad opcode.
305    pub color0_cmd: u32,
306    /// Vertex 0 position.
307    pub v0: u32,
308    /// Vertex 1 colour.
309    pub color1: u32,
310    /// Vertex 1 position.
311    pub v1: u32,
312    /// Vertex 2 colour.
313    pub color2: u32,
314    /// Vertex 2 position.
315    pub v2: u32,
316    /// Vertex 3 colour.
317    pub color3: u32,
318    /// Vertex 3 position.
319    pub v3: u32,
320}
321
322impl QuadGouraudBlended {
323    /// Data-word count after the tag.
324    pub const WORDS: u8 = 9;
325
326    /// Build a native semi-transparent Gouraud ribbon segment.
327    pub const fn new(
328        verts: [(i16, i16); 4],
329        colors: [(u8, u8, u8); 4],
330        blend_mode: BlendMode,
331    ) -> Self {
332        let (r0, g0, b0) = colors[0];
333        let (r1, g1, b1) = colors[1];
334        let (r2, g2, b2) = colors[2];
335        let (r3, g3, b3) = colors[3];
336        Self {
337            tag: 0,
338            draw_mode: TextureMaterial::blended(0, 0, (0, 0, 0), blend_mode).draw_mode_word(),
339            color0_cmd: gp0::polygon_opcode(true, true, false, true, false)
340                | pack_color(r0, g0, b0),
341            v0: pack_vertex(verts[0].0, verts[0].1),
342            color1: pack_color(r1, g1, b1),
343            v1: pack_vertex(verts[1].0, verts[1].1),
344            color2: pack_color(r2, g2, b2),
345            v2: pack_vertex(verts[2].0, verts[2].1),
346            color3: pack_color(r3, g3, b3),
347            v3: pack_vertex(verts[3].0, verts[3].1),
348        }
349    }
350}
351
352/// Monochrome single line. 4 words (tag + 3 data). GP0 0x40 -- the
353/// real diagonal-capable line rasteriser (unlike `RectFlat`, which
354/// the GPU snaps to 16-pixel X boundaries in its GP0 0x02 fill).
355#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
356#[repr(C, align(4))]
357pub struct LineMono {
358    /// OT linkage.
359    pub tag: u32,
360    /// `0x40000000 | color` header.
361    pub color_cmd: u32,
362    /// First endpoint.
363    pub v0: u32,
364    /// Second endpoint.
365    pub v1: u32,
366}
367
368impl LineMono {
369    /// Data-word count.
370    pub const WORDS: u8 = 3;
371
372    /// Build a mono line.
373    pub const fn new(x0: i16, y0: i16, x1: i16, y1: i16, r: u8, g: u8, b: u8) -> Self {
374        Self {
375            tag: 0,
376            color_cmd: packet::FLAT_LINE | pack_color(r, g, b),
377            v0: pack_vertex(x0, y0),
378            v1: pack_vertex(x1, y1),
379        }
380    }
381
382    /// The same line drawn with the semi-transparency equation set by the
383    /// last GP0(E1h) ([`crate::Gpu::set_draw_mode`]).
384    pub const fn translucent(mut self) -> Self {
385        self.color_cmd |= SEMI_TRANSPARENT;
386        self
387    }
388}
389
390/// Gouraud-shaded single line (GP0 50h). 5 words (tag + 4 data).
391///
392/// The GPU interpolates the colour from one endpoint to the other.
393#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
394#[repr(C, align(4))]
395pub struct LineGouraud {
396    /// OT linkage.
397    pub tag: u32,
398    /// `0x50000000 | start color`.
399    pub color0_command: u32,
400    /// Start point.
401    pub v0: u32,
402    /// End color.
403    pub color1: u32,
404    /// End point.
405    pub v1: u32,
406}
407
408impl LineGouraud {
409    /// Data-word count.
410    pub const WORDS: u8 = 4;
411
412    /// A line from `from` to `to`, shaded from `from_color` to `to_color`.
413    pub const fn new(
414        from: (i16, i16),
415        from_color: (u8, u8, u8),
416        to: (i16, i16),
417        to_color: (u8, u8, u8),
418    ) -> Self {
419        Self {
420            tag: 0,
421            color0_command: packet::SHADED_LINE
422                | pack_color(from_color.0, from_color.1, from_color.2),
423            v0: pack_vertex(from.0, from.1),
424            color1: pack_color(to_color.0, to_color.1, to_color.2),
425            v1: pack_vertex(to.0, to.1),
426        }
427    }
428
429    /// The same line drawn with the semi-transparency equation set by the
430    /// last GP0(E1h) ([`crate::Gpu::set_draw_mode`]).
431    pub const fn translucent(mut self) -> Self {
432        self.color0_command |= SEMI_TRANSPARENT;
433        self
434    }
435}
436
437/// Textured triangle with a single flat tint. 9 words (tag + 8 data).
438///
439/// The first data word is GP0(E2) texture-window state, followed by
440/// vertex + UV pairs; CLUT rides in vertex 0's UV high word, tpage in
441/// vertex 1's UV high word (PSX-SPX convention for GP0 0x24). Emitting
442/// E2 per triangle keeps windowed world materials from leaking state to
443/// model triangles when the ordering table interleaves both.
444#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
445#[repr(C, align(4))]
446pub struct TriTextured {
447    /// OT linkage.
448    pub tag: u32,
449    /// GP0(E2) texture-window command.
450    pub tex_window: u32,
451    /// `0x24000000 | tint` header.
452    pub color_cmd: u32,
453    /// Vertex 0 position.
454    pub v0: u32,
455    /// `(u0, v0, clut)` packed.
456    pub uv0_clut: u32,
457    /// Vertex 1 position.
458    pub v1: u32,
459    /// `(u1, v1, tpage)` packed.
460    pub uv1_tpage: u32,
461    /// Vertex 2 position.
462    pub v2: u32,
463    /// `(u2, v2, 0)` packed.
464    pub uv2: u32,
465}
466
467impl TriTextured {
468    /// Data-word count.
469    pub const WORDS: u8 = 8;
470
471    /// Build a textured triangle. `tint = (128, 128, 128)` leaves
472    /// texels unmodulated.
473    pub const fn new(
474        verts: [(i16, i16); 3],
475        uvs: [(u8, u8); 3],
476        clut: u16,
477        tpage: u16,
478        tint: (u8, u8, u8),
479    ) -> Self {
480        Self::with_material(verts, uvs, TextureMaterial::opaque(clut, tpage, tint))
481    }
482
483    /// Build a textured triangle using a [`TextureMaterial`].
484    pub const fn with_material(
485        verts: [(i16, i16); 3],
486        uvs: [(u8, u8); 3],
487        material: TextureMaterial,
488    ) -> Self {
489        let (u0, v0) = uvs[0];
490        let (u1, v1) = uvs[1];
491        let (u2, v2) = uvs[2];
492        let clut = material.clut_word();
493        let tpage = material.texture_page_word();
494        Self {
495            tag: 0,
496            tex_window: material.texture_window_word(),
497            color_cmd: material.flat_textured_polygon_header(false),
498            v0: pack_vertex(verts[0].0, verts[0].1),
499            uv0_clut: pack_texcoord(u0, v0, clut),
500            v1: pack_vertex(verts[1].0, verts[1].1),
501            uv1_tpage: pack_texcoord(u1, v1, tpage),
502            v2: pack_vertex(verts[2].0, verts[2].1),
503            uv2: pack_texcoord(u2, v2, 0),
504        }
505    }
506
507    /// The same packet as [`Self::with_material`], which it forwards to.
508    #[deprecated(note = "identical to `with_material`")]
509    #[inline(always)]
510    pub const fn with_material_packet_texcoords(
511        verts: [(i16, i16); 3],
512        uvs: [(u8, u8); 3],
513        material: TextureMaterial,
514    ) -> Self {
515        Self::with_material(verts, uvs, material)
516    }
517
518    /// Build a textured triangle from UV words that already contain
519    /// the low `(u, v)` bytes in packet layout. The material still
520    /// supplies CLUT, tpage, tint, blend, and texture-window state.
521    pub const fn with_material_packed_uv_words(
522        verts: [(i16, i16); 3],
523        uv_words: [u16; 3],
524        material: TextureMaterial,
525    ) -> Self {
526        let clut = material.clut_word();
527        let tpage = material.texture_page_word();
528        Self {
529            tag: 0,
530            tex_window: material.texture_window_word(),
531            color_cmd: material.flat_textured_polygon_header(false),
532            v0: pack_vertex(verts[0].0, verts[0].1),
533            uv0_clut: (uv_words[0] as u32) | ((clut as u32) << 16),
534            v1: pack_vertex(verts[1].0, verts[1].1),
535            uv1_tpage: (uv_words[1] as u32) | ((tpage as u32) << 16),
536            v2: pack_vertex(verts[2].0, verts[2].1),
537            uv2: uv_words[2] as u32,
538        }
539    }
540
541    /// Build a textured triangle from UV words and prepacked material words.
542    pub const fn with_packet_material_packed_uv_words(
543        verts: [(i16, i16); 3],
544        uv_words: [u16; 3],
545        material: TexturedPacketMaterial,
546    ) -> Self {
547        Self {
548            tag: 0,
549            tex_window: material.tex_window_word,
550            color_cmd: material.color_command_word,
551            v0: pack_vertex(verts[0].0, verts[0].1),
552            uv0_clut: (uv_words[0] as u32) | material.clut_high_word,
553            v1: pack_vertex(verts[1].0, verts[1].1),
554            uv1_tpage: (uv_words[1] as u32) | material.tpage_high_word,
555            v2: pack_vertex(verts[2].0, verts[2].1),
556            uv2: uv_words[2] as u32,
557        }
558    }
559}
560
561/// Classic flat-tinted textured triangle without an inline GP0(E2)
562/// texture-window command. Renderers that keep one texture-window state for
563/// the whole pass can use this compact GP0(24h) packet. 8 words (tag + 7
564/// data).
565#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
566#[repr(C, align(4))]
567pub struct ClassicTriTextured {
568    /// Staged OT tag or final DMA linkage.
569    pub tag: u32,
570    /// GP0(24h) plus the flat tint.
571    pub color_cmd: u32,
572    /// Vertex-zero screen coordinate.
573    pub v0: u32,
574    /// Vertex-zero UV and CLUT.
575    pub uv0_clut: u32,
576    /// Vertex-one screen coordinate.
577    pub v1: u32,
578    /// Vertex-one UV and tpage.
579    pub uv1_tpage: u32,
580    /// Vertex-two screen coordinate.
581    pub v2: u32,
582    /// Vertex-two UV.
583    pub uv2: u32,
584}
585
586impl ClassicTriTextured {
587    /// Data-word count after the tag.
588    pub const WORDS: u8 = 7;
589
590    /// Build a compact packet whose tag temporarily carries `ot_slot` for a
591    /// later tagged-stream OT linking pass.
592    pub const fn with_staged_slot(
593        verts: [(i16, i16); 3],
594        uv_words: [u16; 3],
595        tint: u32,
596        clut: u16,
597        tpage: u16,
598        ot_slot: u16,
599    ) -> Self {
600        Self {
601            tag: ((Self::WORDS as u32) << 24) | ot_slot as u32,
602            color_cmd: packet::FLAT_TEXTURED_TRIANGLE | (tint & 0x00ff_ffff),
603            v0: pack_vertex(verts[0].0, verts[0].1),
604            uv0_clut: uv_words[0] as u32 | ((clut as u32) << 16),
605            v1: pack_vertex(verts[1].0, verts[1].1),
606            uv1_tpage: uv_words[1] as u32 | ((tpage as u32) << 16),
607            v2: pack_vertex(verts[2].0, verts[2].1),
608            uv2: uv_words[2] as u32,
609        }
610    }
611}
612
613/// Textured **Gouraud-shaded** triangle. 11 words (tag + 10 data).
614///
615/// Per-vertex tint: the GPU multiplies each texel by the
616/// interpolated vertex colour, so GTE-lit-and-fogged per-vertex
617/// colours drive the final shade smoothly across the triangle.
618/// The first data word is GP0(E2) texture-window state, matching
619/// [`TriTextured`] so windowed/tiled world materials do not leak
620/// state across ordering-table interleaving. CLUT rides in v0's
621/// UV high word, tpage in v1's UV high word (PSX-SPX convention
622/// for GP0 0x34).
623#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
624#[repr(C, align(4))]
625pub struct TriTexturedGouraud {
626    /// OT linkage.
627    pub tag: u32,
628    /// GP0(E2) texture-window command.
629    pub tex_window: u32,
630    /// `0x34000000 | color0` header -- v0's RGB is packed into the
631    /// same word as the polygon opcode.
632    pub color0_cmd: u32,
633    /// Vertex 0 position.
634    pub v0: u32,
635    /// `(u0, v0, clut)` packed.
636    pub uv0_clut: u32,
637    /// Vertex 1 colour (RGB in low 24 bits; top byte ignored).
638    pub color1: u32,
639    /// Vertex 1 position.
640    pub v1: u32,
641    /// `(u1, v1, tpage)` packed.
642    pub uv1_tpage: u32,
643    /// Vertex 2 colour.
644    pub color2: u32,
645    /// Vertex 2 position.
646    pub v2: u32,
647    /// `(u2, v2, 0)` packed.
648    pub uv2: u32,
649}
650
651impl TriTexturedGouraud {
652    /// Data-word count.
653    pub const WORDS: u8 = 10;
654
655    /// Build a textured Gouraud triangle. Each vertex carries its
656    /// own RGB (the NCDT-lit-and-fogged colour in the typical
657    /// commercial-game path) which modulates the sampled texel.
658    pub const fn new(
659        verts: [(i16, i16); 3],
660        uvs: [(u8, u8); 3],
661        colors: [(u8, u8, u8); 3],
662        clut: u16,
663        tpage: u16,
664    ) -> Self {
665        Self::with_material(verts, uvs, colors, TextureMaterial::new(clut, tpage))
666    }
667
668    /// Build a textured Gouraud triangle using a [`TextureMaterial`].
669    pub const fn with_material(
670        verts: [(i16, i16); 3],
671        uvs: [(u8, u8); 3],
672        colors: [(u8, u8, u8); 3],
673        material: TextureMaterial,
674    ) -> Self {
675        let (r0, g0, b0) = colors[0];
676        let (r1, g1, b1) = colors[1];
677        let (r2, g2, b2) = colors[2];
678        let (u0, v0) = uvs[0];
679        let (u1, v1) = uvs[1];
680        let (u2, v2) = uvs[2];
681        let clut = material.clut_word();
682        let tpage = material.texture_page_word();
683        Self {
684            tag: 0,
685            tex_window: material.texture_window_word(),
686            color0_cmd: material.textured_polygon_command(true, false) | pack_color(r0, g0, b0),
687            v0: pack_vertex(verts[0].0, verts[0].1),
688            uv0_clut: pack_texcoord(u0, v0, clut),
689            color1: pack_color(r1, g1, b1),
690            v1: pack_vertex(verts[1].0, verts[1].1),
691            uv1_tpage: pack_texcoord(u1, v1, tpage),
692            color2: pack_color(r2, g2, b2),
693            v2: pack_vertex(verts[2].0, verts[2].1),
694            uv2: pack_texcoord(u2, v2, 0),
695        }
696    }
697
698    /// The same packet as [`Self::with_material`], which it forwards to.
699    #[deprecated(note = "identical to `with_material`")]
700    #[inline(always)]
701    pub const fn with_material_packet_texcoords(
702        verts: [(i16, i16); 3],
703        uvs: [(u8, u8); 3],
704        colors: [(u8, u8, u8); 3],
705        material: TextureMaterial,
706    ) -> Self {
707        Self::with_material(verts, uvs, colors, material)
708    }
709
710    /// Build a textured Gouraud triangle from UV words that already
711    /// contain the low `(u, v)` packet bytes.
712    pub const fn with_material_packed_uv_words(
713        verts: [(i16, i16); 3],
714        uv_words: [u16; 3],
715        colors: [(u8, u8, u8); 3],
716        material: TextureMaterial,
717    ) -> Self {
718        let (r0, g0, b0) = colors[0];
719        let (r1, g1, b1) = colors[1];
720        let (r2, g2, b2) = colors[2];
721        let clut = material.clut_word();
722        let tpage = material.texture_page_word();
723        Self {
724            tag: 0,
725            tex_window: material.texture_window_word(),
726            color0_cmd: material.textured_polygon_command(true, false) | pack_color(r0, g0, b0),
727            v0: pack_vertex(verts[0].0, verts[0].1),
728            uv0_clut: (uv_words[0] as u32) | ((clut as u32) << 16),
729            color1: pack_color(r1, g1, b1),
730            v1: pack_vertex(verts[1].0, verts[1].1),
731            uv1_tpage: (uv_words[1] as u32) | ((tpage as u32) << 16),
732            color2: pack_color(r2, g2, b2),
733            v2: pack_vertex(verts[2].0, verts[2].1),
734            uv2: uv_words[2] as u32,
735        }
736    }
737
738    /// Build a textured Gouraud triangle from UV words and material
739    /// packet words that were precomputed once for a hot material.
740    pub const fn with_packet_material_packed_uv_words(
741        verts: [(i16, i16); 3],
742        uv_words: [u16; 3],
743        colors: [(u8, u8, u8); 3],
744        material: TexturedGouraudPacketMaterial,
745    ) -> Self {
746        let (r0, g0, b0) = colors[0];
747        let (r1, g1, b1) = colors[1];
748        let (r2, g2, b2) = colors[2];
749        Self {
750            tag: 0,
751            tex_window: material.tex_window_word,
752            color0_cmd: material.color0_command_word | pack_color(r0, g0, b0),
753            v0: pack_vertex(verts[0].0, verts[0].1),
754            uv0_clut: (uv_words[0] as u32) | material.clut_high_word,
755            color1: pack_color(r1, g1, b1),
756            v1: pack_vertex(verts[1].0, verts[1].1),
757            uv1_tpage: (uv_words[1] as u32) | material.tpage_high_word,
758            color2: pack_color(r2, g2, b2),
759            v2: pack_vertex(verts[2].0, verts[2].1),
760            uv2: uv_words[2] as u32,
761        }
762    }
763
764    /// Build a self-contained texture-window packet whose tag temporarily
765    /// carries `ot_slot` for a later tagged-stream OT linking pass.
766    ///
767    /// This is the windowed counterpart to
768    /// [`ClassicTriTexturedGouraud::with_staged_slot_prepacked_colors`].
769    /// It is intended for classic affine renderers whose tiled materials can
770    /// interleave in the ordering table, so every polygon restores its own
771    /// GP0(E2) state instead of relying on a global draw-mode side effect.
772    ///
773    /// Every color word must have its high byte clear, and `clut_high_word`
774    /// and `tpage_high_word` must hold only their high halfwords; debug builds
775    /// assert both. A word that breaks this changes what the GPU draws, not
776    /// memory, so the constructor is safe.
777    pub const fn with_staged_slot_prepacked_colors(
778        verts: [(i16, i16); 3],
779        uv_words: [u16; 3],
780        colors: [u32; 3],
781        clut_high_word: u32,
782        tpage_high_word: u32,
783        texture_window_word: u32,
784        ot_slot: u16,
785    ) -> Self {
786        debug_assert!((colors[0] | colors[1] | colors[2]) >> 24 == 0);
787        debug_assert!((clut_high_word | tpage_high_word) & 0xFFFF == 0);
788        Self {
789            tag: ((Self::WORDS as u32) << 24) | ot_slot as u32,
790            tex_window: texture_window_word,
791            color0_cmd: packet::SHADED_TEXTURED_TRIANGLE | colors[0],
792            v0: pack_vertex(verts[0].0, verts[0].1),
793            uv0_clut: uv_words[0] as u32 | clut_high_word,
794            color1: colors[1],
795            v1: pack_vertex(verts[1].0, verts[1].1),
796            uv1_tpage: uv_words[1] as u32 | tpage_high_word,
797            color2: colors[2],
798            v2: pack_vertex(verts[2].0, verts[2].1),
799            uv2: uv_words[2] as u32,
800        }
801    }
802
803    /// Renamed to [`Self::with_staged_slot_prepacked_colors`], which is safe.
804    ///
805    /// # Safety
806    ///
807    /// Nothing beyond that function's preconditions; it stays `unsafe` so
808    /// existing `unsafe` blocks keep compiling without a warning.
809    #[deprecated(note = "renamed to `with_staged_slot_prepacked_colors`, which is safe")]
810    #[inline(always)]
811    pub const unsafe fn with_staged_slot_prepacked_unchecked(
812        verts: [(i16, i16); 3],
813        uv_words: [u16; 3],
814        colors: [u32; 3],
815        clut_high_word: u32,
816        tpage_high_word: u32,
817        texture_window_word: u32,
818        ot_slot: u16,
819    ) -> Self {
820        Self::with_staged_slot_prepacked_colors(
821            verts,
822            uv_words,
823            colors,
824            clut_high_word,
825            tpage_high_word,
826            texture_window_word,
827            ot_slot,
828        )
829    }
830
831    /// Zeroed packet for static prebuilt-pool initialisation. Real
832    /// content is written before the packet is ever linked into an
833    /// ordering table.
834    pub const EMPTY: Self = Self {
835        tag: 0,
836        tex_window: 0,
837        color0_cmd: 0,
838        v0: 0,
839        uv0_clut: 0,
840        color1: 0,
841        v1: 0,
842        uv1_tpage: 0,
843        color2: 0,
844        v2: 0,
845        uv2: 0,
846    };
847
848    /// Rewrite the three vertex words of a prebuilt packet, leaving
849    /// material, UV, and colour words untouched.
850    #[inline(always)]
851    pub fn set_positions(&mut self, verts: [(i16, i16); 3]) {
852        self.v0 = pack_vertex(verts[0].0, verts[0].1);
853        self.v1 = pack_vertex(verts[1].0, verts[1].1);
854        self.v2 = pack_vertex(verts[2].0, verts[2].1);
855    }
856
857    /// Copy every packet word except the OT tag from a prebuilt
858    /// skeleton. The destination can then patch positions and link
859    /// into an ordering table without inheriting a stale DMA link.
860    #[inline(always)]
861    pub fn copy_payload_from(&mut self, source: &Self) {
862        self.tex_window = source.tex_window;
863        self.color0_cmd = source.color0_cmd;
864        self.v0 = source.v0;
865        self.uv0_clut = source.uv0_clut;
866        self.color1 = source.color1;
867        self.v1 = source.v1;
868        self.uv1_tpage = source.uv1_tpage;
869        self.color2 = source.color2;
870        self.v2 = source.v2;
871        self.uv2 = source.uv2;
872    }
873
874    /// Rewrite the three colour words of a prebuilt packet, preserving
875    /// the opcode byte that shares v0's colour word.
876    #[inline(always)]
877    pub fn set_colors(&mut self, colors: [(u8, u8, u8); 3]) {
878        let (r0, g0, b0) = colors[0];
879        let (r1, g1, b1) = colors[1];
880        let (r2, g2, b2) = colors[2];
881        self.color0_cmd = (self.color0_cmd & 0xFF00_0000) | pack_color(r0, g0, b0);
882        self.color1 = pack_color(r1, g1, b1);
883        self.color2 = pack_color(r2, g2, b2);
884    }
885}
886
887/// Classic textured Gouraud triangle without an inline GP0(E2) texture-window
888/// command. This is the compact GP0(34h) packet used by renderers that keep a
889/// single texture-window state for the whole pass. 10 words (tag + 9 data).
890#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
891#[repr(C, align(4))]
892pub struct ClassicTriTexturedGouraud {
893    /// Staged OT tag or final DMA linkage.
894    pub tag: u32,
895    /// GP0(34h) plus vertex-zero RGB.
896    pub color0_cmd: u32,
897    /// Vertex-zero screen coordinate.
898    pub v0: u32,
899    /// Vertex-zero UV and CLUT.
900    pub uv0_clut: u32,
901    /// Vertex-one RGB.
902    pub color1: u32,
903    /// Vertex-one screen coordinate.
904    pub v1: u32,
905    /// Vertex-one UV and tpage.
906    pub uv1_tpage: u32,
907    /// Vertex-two RGB.
908    pub color2: u32,
909    /// Vertex-two screen coordinate.
910    pub v2: u32,
911    /// Vertex-two UV.
912    pub uv2: u32,
913}
914
915impl ClassicTriTexturedGouraud {
916    /// Data-word count after the tag.
917    pub const WORDS: u8 = 9;
918
919    /// Build a compact packet whose tag temporarily carries `ot_slot` for a
920    /// later [`crate::ot::OrderingTable::insert_tagged_packet_stream_unchecked`]
921    /// pass.
922    pub const fn with_staged_slot(
923        verts: [(i16, i16); 3],
924        uv_words: [u16; 3],
925        colors: [u32; 3],
926        clut: u16,
927        tpage: u16,
928        ot_slot: u16,
929    ) -> Self {
930        Self::with_staged_slot_prepacked_material(
931            verts,
932            uv_words,
933            colors,
934            (clut as u32) << 16,
935            (tpage as u32) << 16,
936            ot_slot,
937        )
938    }
939
940    /// Build a staged compact packet from CLUT and tpage words that have
941    /// already been shifted into their GP0 high-halfword positions.
942    pub const fn with_staged_slot_prepacked_material(
943        verts: [(i16, i16); 3],
944        uv_words: [u16; 3],
945        colors: [u32; 3],
946        clut_high_word: u32,
947        tpage_high_word: u32,
948        ot_slot: u16,
949    ) -> Self {
950        Self {
951            tag: ((Self::WORDS as u32) << 24) | ot_slot as u32,
952            color0_cmd: packet::SHADED_TEXTURED_TRIANGLE | (colors[0] & 0x00ff_ffff),
953            v0: pack_vertex(verts[0].0, verts[0].1),
954            uv0_clut: uv_words[0] as u32 | clut_high_word,
955            color1: colors[1] & 0x00ff_ffff,
956            v1: pack_vertex(verts[1].0, verts[1].1),
957            uv1_tpage: uv_words[1] as u32 | tpage_high_word,
958            color2: colors[2] & 0x00ff_ffff,
959            v2: pack_vertex(verts[2].0, verts[2].1),
960            uv2: uv_words[2] as u32,
961        }
962    }
963
964    /// Build a staged packet without masking the supplied packet RGB words.
965    ///
966    /// Every color word must have its high byte clear, and `clut_high_word`
967    /// and `tpage_high_word` must hold only their high halfwords; debug builds
968    /// assert both. A word that breaks this changes what the GPU draws, not
969    /// memory, so the constructor is safe.
970    pub const fn with_staged_slot_prepacked_colors(
971        verts: [(i16, i16); 3],
972        uv_words: [u16; 3],
973        colors: [u32; 3],
974        clut_high_word: u32,
975        tpage_high_word: u32,
976        ot_slot: u16,
977    ) -> Self {
978        debug_assert!((colors[0] | colors[1] | colors[2]) >> 24 == 0);
979        debug_assert!((clut_high_word | tpage_high_word) & 0xFFFF == 0);
980        Self {
981            tag: ((Self::WORDS as u32) << 24) | ot_slot as u32,
982            color0_cmd: packet::SHADED_TEXTURED_TRIANGLE | colors[0],
983            v0: pack_vertex(verts[0].0, verts[0].1),
984            uv0_clut: uv_words[0] as u32 | clut_high_word,
985            color1: colors[1],
986            v1: pack_vertex(verts[1].0, verts[1].1),
987            uv1_tpage: uv_words[1] as u32 | tpage_high_word,
988            color2: colors[2],
989            v2: pack_vertex(verts[2].0, verts[2].1),
990            uv2: uv_words[2] as u32,
991        }
992    }
993
994    /// Renamed to [`Self::with_staged_slot_prepacked_colors`], which is safe.
995    ///
996    /// # Safety
997    ///
998    /// Nothing beyond that function's preconditions; it stays `unsafe` so
999    /// existing `unsafe` blocks keep compiling without a warning.
1000    #[deprecated(note = "renamed to `with_staged_slot_prepacked_colors`, which is safe")]
1001    #[inline(always)]
1002    pub const unsafe fn with_staged_slot_prepacked_unchecked(
1003        verts: [(i16, i16); 3],
1004        uv_words: [u16; 3],
1005        colors: [u32; 3],
1006        clut_high_word: u32,
1007        tpage_high_word: u32,
1008        ot_slot: u16,
1009    ) -> Self {
1010        Self::with_staged_slot_prepacked_colors(
1011            verts,
1012            uv_words,
1013            colors,
1014            clut_high_word,
1015            tpage_high_word,
1016            ot_slot,
1017        )
1018    }
1019}
1020
1021/// Classic textured Gouraud quad without inline texture-window state. This
1022/// is the compact GP0(3Ch) packet paired with
1023/// [`ClassicTriTexturedGouraud`]. 13 words (tag + 12 data).
1024#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
1025#[repr(C, align(4))]
1026pub struct ClassicQuadTexturedGouraud {
1027    /// Staged OT tag or final DMA linkage.
1028    pub tag: u32,
1029    /// GP0(3Ch) plus vertex-zero RGB.
1030    pub color0_cmd: u32,
1031    /// Vertex-zero screen coordinate.
1032    pub v0: u32,
1033    /// Vertex-zero UV and CLUT.
1034    pub uv0_clut: u32,
1035    /// Vertex-one RGB.
1036    pub color1: u32,
1037    /// Vertex-one screen coordinate.
1038    pub v1: u32,
1039    /// Vertex-one UV and tpage.
1040    pub uv1_tpage: u32,
1041    /// Vertex-two RGB.
1042    pub color2: u32,
1043    /// Vertex-two screen coordinate.
1044    pub v2: u32,
1045    /// Vertex-two UV.
1046    pub uv2: u32,
1047    /// Vertex-three RGB.
1048    pub color3: u32,
1049    /// Vertex-three screen coordinate.
1050    pub v3: u32,
1051    /// Vertex-three UV.
1052    pub uv3: u32,
1053}
1054
1055impl ClassicQuadTexturedGouraud {
1056    /// Data-word count after the tag.
1057    pub const WORDS: u8 = 12;
1058
1059    /// Build a compact packet with a staged OT slot.
1060    pub const fn with_staged_slot(
1061        verts: [(i16, i16); 4],
1062        uv_words: [u16; 4],
1063        colors: [u32; 4],
1064        clut: u16,
1065        tpage: u16,
1066        ot_slot: u16,
1067    ) -> Self {
1068        Self::with_staged_slot_prepacked_material(
1069            verts,
1070            uv_words,
1071            colors,
1072            (clut as u32) << 16,
1073            (tpage as u32) << 16,
1074            ot_slot,
1075        )
1076    }
1077
1078    /// Build a staged compact packet from CLUT and tpage words that have
1079    /// already been shifted into their GP0 high-halfword positions.
1080    pub const fn with_staged_slot_prepacked_material(
1081        verts: [(i16, i16); 4],
1082        uv_words: [u16; 4],
1083        colors: [u32; 4],
1084        clut_high_word: u32,
1085        tpage_high_word: u32,
1086        ot_slot: u16,
1087    ) -> Self {
1088        Self {
1089            tag: ((Self::WORDS as u32) << 24) | ot_slot as u32,
1090            color0_cmd: packet::SHADED_TEXTURED_QUAD | (colors[0] & 0x00ff_ffff),
1091            v0: pack_vertex(verts[0].0, verts[0].1),
1092            uv0_clut: uv_words[0] as u32 | clut_high_word,
1093            color1: colors[1] & 0x00ff_ffff,
1094            v1: pack_vertex(verts[1].0, verts[1].1),
1095            uv1_tpage: uv_words[1] as u32 | tpage_high_word,
1096            color2: colors[2] & 0x00ff_ffff,
1097            v2: pack_vertex(verts[2].0, verts[2].1),
1098            uv2: uv_words[2] as u32,
1099            color3: colors[3] & 0x00ff_ffff,
1100            v3: pack_vertex(verts[3].0, verts[3].1),
1101            uv3: uv_words[3] as u32,
1102        }
1103    }
1104
1105    /// Build a staged packet without masking the supplied packet RGB words.
1106    ///
1107    /// Every color word must have its high byte clear, and `clut_high_word`
1108    /// and `tpage_high_word` must hold only their high halfwords; debug builds
1109    /// assert both. A word that breaks this changes what the GPU draws, not
1110    /// memory, so the constructor is safe.
1111    pub const fn with_staged_slot_prepacked_colors(
1112        verts: [(i16, i16); 4],
1113        uv_words: [u16; 4],
1114        colors: [u32; 4],
1115        clut_high_word: u32,
1116        tpage_high_word: u32,
1117        ot_slot: u16,
1118    ) -> Self {
1119        debug_assert!((colors[0] | colors[1] | colors[2] | colors[3]) >> 24 == 0);
1120        debug_assert!((clut_high_word | tpage_high_word) & 0xFFFF == 0);
1121        Self {
1122            tag: ((Self::WORDS as u32) << 24) | ot_slot as u32,
1123            color0_cmd: packet::SHADED_TEXTURED_QUAD | colors[0],
1124            v0: pack_vertex(verts[0].0, verts[0].1),
1125            uv0_clut: uv_words[0] as u32 | clut_high_word,
1126            color1: colors[1],
1127            v1: pack_vertex(verts[1].0, verts[1].1),
1128            uv1_tpage: uv_words[1] as u32 | tpage_high_word,
1129            color2: colors[2],
1130            v2: pack_vertex(verts[2].0, verts[2].1),
1131            uv2: uv_words[2] as u32,
1132            color3: colors[3],
1133            v3: pack_vertex(verts[3].0, verts[3].1),
1134            uv3: uv_words[3] as u32,
1135        }
1136    }
1137
1138    /// Renamed to [`Self::with_staged_slot_prepacked_colors`], which is safe.
1139    ///
1140    /// # Safety
1141    ///
1142    /// Nothing beyond that function's preconditions; it stays `unsafe` so
1143    /// existing `unsafe` blocks keep compiling without a warning.
1144    #[deprecated(note = "renamed to `with_staged_slot_prepacked_colors`, which is safe")]
1145    #[inline(always)]
1146    pub const unsafe fn with_staged_slot_prepacked_unchecked(
1147        verts: [(i16, i16); 4],
1148        uv_words: [u16; 4],
1149        colors: [u32; 4],
1150        clut_high_word: u32,
1151        tpage_high_word: u32,
1152        ot_slot: u16,
1153    ) -> Self {
1154        Self::with_staged_slot_prepacked_colors(
1155            verts,
1156            uv_words,
1157            colors,
1158            clut_high_word,
1159            tpage_high_word,
1160            ot_slot,
1161        )
1162    }
1163}
1164
1165/// Textured Gouraud quad with inline texture-window state. Mirrors
1166/// [`TriTexturedGouraud`] extended to four vertices: GP0(E2)
1167/// texture-window state immediately followed by the GP0(3Ch)
1168/// Gouraud-textured-quad command. Per-vertex RGB modulates the sampled
1169/// texel exactly like the triangle. 14 words (tag + 13 data).
1170///
1171/// The PS1 GPU rasterizes this quad as the two triangles `(v0,v1,v2)`
1172/// and `(v1,v2,v3)` -- the `1`-`2` diagonal. A caller whose engine
1173/// splits quads on the `0`-`2` diagonal as `tri(a,b,c)+tri(a,c,d)` must
1174/// reorder its perimeter `[a,b,c,d]` to `[b,a,c,d]` before building the
1175/// packet so the hardware split lands on the same edge; that yields
1176/// pixel-identical output (proved by
1177/// `textured_gouraud_quad_matches_two_triangle_split_bitexact` in the
1178/// emulator GPU tests).
1179#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
1180#[repr(C, align(4))]
1181pub struct QuadTexturedGouraud {
1182    /// OT linkage.
1183    pub tag: u32,
1184    /// GP0(E2) texture-window command.
1185    pub tex_window: u32,
1186    /// `0x3C000000 | color0` header -- v0's RGB shares the opcode word.
1187    pub color0_cmd: u32,
1188    /// Vertex 0 position.
1189    pub v0: u32,
1190    /// `(u0, v0, clut)` packed.
1191    pub uv0_clut: u32,
1192    /// Vertex 1 colour.
1193    pub color1: u32,
1194    /// Vertex 1 position.
1195    pub v1: u32,
1196    /// `(u1, v1, tpage)` packed.
1197    pub uv1_tpage: u32,
1198    /// Vertex 2 colour.
1199    pub color2: u32,
1200    /// Vertex 2 position.
1201    pub v2: u32,
1202    /// `(u2, v2, 0)` packed.
1203    pub uv2: u32,
1204    /// Vertex 3 colour.
1205    pub color3: u32,
1206    /// Vertex 3 position.
1207    pub v3: u32,
1208    /// `(u3, v3, 0)` packed.
1209    pub uv3: u32,
1210}
1211
1212impl QuadTexturedGouraud {
1213    /// Data-word count (tag excluded).
1214    pub const WORDS: u8 = 13;
1215
1216    /// Opcode bit promoting the Gouraud-textured-triangle header
1217    /// (`0x34`) to the Gouraud-textured-quad header (`0x3C`).
1218    const QUAD_OPCODE_BIT: u32 = packet::QUAD;
1219
1220    /// Build a textured Gouraud quad from per-vertex UVs and a
1221    /// [`TextureMaterial`]. Vertex order is TL, TR, BL, BR; each colour
1222    /// tints its vertex (`(128, 128, 128)` leaves texels unmodulated).
1223    pub const fn with_material(
1224        verts: [(i16, i16); 4],
1225        uvs: [(u8, u8); 4],
1226        colors: [(u8, u8, u8); 4],
1227        material: TextureMaterial,
1228    ) -> Self {
1229        let mut uv_words = [0u16; 4];
1230        let mut i = 0;
1231        while i < 4 {
1232            uv_words[i] = uvs[i].0 as u16 | (uvs[i].1 as u16) << 8;
1233            i += 1;
1234        }
1235        Self::with_packet_material_packed_uv_words(
1236            verts,
1237            uv_words,
1238            colors,
1239            material.textured_gouraud_packet_material(),
1240        )
1241    }
1242
1243    /// Build a textured Gouraud quad from UV words and a packet material
1244    /// precomputed for a hot material, mirroring
1245    /// [`TriTexturedGouraud::with_packet_material_packed_uv_words`].
1246    pub const fn with_packet_material_packed_uv_words(
1247        verts: [(i16, i16); 4],
1248        uv_words: [u16; 4],
1249        colors: [(u8, u8, u8); 4],
1250        material: TexturedGouraudPacketMaterial,
1251    ) -> Self {
1252        let (r0, g0, b0) = colors[0];
1253        let (r1, g1, b1) = colors[1];
1254        let (r2, g2, b2) = colors[2];
1255        let (r3, g3, b3) = colors[3];
1256        Self {
1257            tag: 0,
1258            tex_window: material.tex_window_word,
1259            color0_cmd: (material.color0_command_word | Self::QUAD_OPCODE_BIT)
1260                | pack_color(r0, g0, b0),
1261            v0: pack_vertex(verts[0].0, verts[0].1),
1262            uv0_clut: (uv_words[0] as u32) | material.clut_high_word,
1263            color1: pack_color(r1, g1, b1),
1264            v1: pack_vertex(verts[1].0, verts[1].1),
1265            uv1_tpage: (uv_words[1] as u32) | material.tpage_high_word,
1266            color2: pack_color(r2, g2, b2),
1267            v2: pack_vertex(verts[2].0, verts[2].1),
1268            uv2: uv_words[2] as u32,
1269            color3: pack_color(r3, g3, b3),
1270            v3: pack_vertex(verts[3].0, verts[3].1),
1271            uv3: uv_words[3] as u32,
1272        }
1273    }
1274
1275    /// Build a self-contained texture-window packet whose tag temporarily
1276    /// carries `ot_slot` for a later tagged-stream OT linking pass.
1277    ///
1278    /// Every color word must have its high byte clear, and `clut_high_word`
1279    /// and `tpage_high_word` must hold only their high halfwords; debug builds
1280    /// assert both. A word that breaks this changes what the GPU draws, not
1281    /// memory, so the constructor is safe.
1282    pub const fn with_staged_slot_prepacked_colors(
1283        verts: [(i16, i16); 4],
1284        uv_words: [u16; 4],
1285        colors: [u32; 4],
1286        clut_high_word: u32,
1287        tpage_high_word: u32,
1288        texture_window_word: u32,
1289        ot_slot: u16,
1290    ) -> Self {
1291        debug_assert!((colors[0] | colors[1] | colors[2] | colors[3]) >> 24 == 0);
1292        debug_assert!((clut_high_word | tpage_high_word) & 0xFFFF == 0);
1293        Self {
1294            tag: ((Self::WORDS as u32) << 24) | ot_slot as u32,
1295            tex_window: texture_window_word,
1296            color0_cmd: packet::SHADED_TEXTURED_QUAD | colors[0],
1297            v0: pack_vertex(verts[0].0, verts[0].1),
1298            uv0_clut: uv_words[0] as u32 | clut_high_word,
1299            color1: colors[1],
1300            v1: pack_vertex(verts[1].0, verts[1].1),
1301            uv1_tpage: uv_words[1] as u32 | tpage_high_word,
1302            color2: colors[2],
1303            v2: pack_vertex(verts[2].0, verts[2].1),
1304            uv2: uv_words[2] as u32,
1305            color3: colors[3],
1306            v3: pack_vertex(verts[3].0, verts[3].1),
1307            uv3: uv_words[3] as u32,
1308        }
1309    }
1310
1311    /// Renamed to [`Self::with_staged_slot_prepacked_colors`], which is safe.
1312    ///
1313    /// # Safety
1314    ///
1315    /// Nothing beyond that function's preconditions; it stays `unsafe` so
1316    /// existing `unsafe` blocks keep compiling without a warning.
1317    #[deprecated(note = "renamed to `with_staged_slot_prepacked_colors`, which is safe")]
1318    #[inline(always)]
1319    pub const unsafe fn with_staged_slot_prepacked_unchecked(
1320        verts: [(i16, i16); 4],
1321        uv_words: [u16; 4],
1322        colors: [u32; 4],
1323        clut_high_word: u32,
1324        tpage_high_word: u32,
1325        texture_window_word: u32,
1326        ot_slot: u16,
1327    ) -> Self {
1328        Self::with_staged_slot_prepacked_colors(
1329            verts,
1330            uv_words,
1331            colors,
1332            clut_high_word,
1333            tpage_high_word,
1334            texture_window_word,
1335            ot_slot,
1336        )
1337    }
1338
1339    /// Zeroed packet for static prebuilt-pool initialisation. Real
1340    /// content is written by the first draw of each surface before the
1341    /// packet ever links into an ordering table (per-surface validity
1342    /// bytes guarantee it).
1343    pub const EMPTY: Self = Self {
1344        tag: 0,
1345        tex_window: 0,
1346        color0_cmd: 0,
1347        v0: 0,
1348        uv0_clut: 0,
1349        color1: 0,
1350        v1: 0,
1351        uv1_tpage: 0,
1352        color2: 0,
1353        v2: 0,
1354        uv2: 0,
1355        color3: 0,
1356        v3: 0,
1357        uv3: 0,
1358    };
1359
1360    /// Rewrite the four vertex words of a prebuilt packet, leaving
1361    /// every material/UV/colour word untouched. This is the per-frame
1362    /// patch for precompiled static world geometry.
1363    #[inline(always)]
1364    pub fn set_positions(&mut self, verts: [(i16, i16); 4]) {
1365        self.v0 = pack_vertex(verts[0].0, verts[0].1);
1366        self.v1 = pack_vertex(verts[1].0, verts[1].1);
1367        self.v2 = pack_vertex(verts[2].0, verts[2].1);
1368        self.v3 = pack_vertex(verts[3].0, verts[3].1);
1369    }
1370
1371    /// Copy every packet word except the OT tag from a prebuilt
1372    /// skeleton. The destination can then patch positions and link
1373    /// into an ordering table without inheriting a stale DMA link.
1374    #[inline(always)]
1375    pub fn copy_payload_from(&mut self, source: &Self) {
1376        self.tex_window = source.tex_window;
1377        self.color0_cmd = source.color0_cmd;
1378        self.v0 = source.v0;
1379        self.uv0_clut = source.uv0_clut;
1380        self.color1 = source.color1;
1381        self.v1 = source.v1;
1382        self.uv1_tpage = source.uv1_tpage;
1383        self.color2 = source.color2;
1384        self.v2 = source.v2;
1385        self.uv2 = source.uv2;
1386        self.color3 = source.color3;
1387        self.v3 = source.v3;
1388        self.uv3 = source.uv3;
1389    }
1390
1391    /// Rewrite the four colour words of a prebuilt packet, preserving
1392    /// the opcode byte that shares v0's colour word.
1393    #[inline(always)]
1394    pub fn set_colors(&mut self, colors: [(u8, u8, u8); 4]) {
1395        let (r0, g0, b0) = colors[0];
1396        let (r1, g1, b1) = colors[1];
1397        let (r2, g2, b2) = colors[2];
1398        let (r3, g3, b3) = colors[3];
1399        self.color0_cmd = (self.color0_cmd & 0xFF00_0000) | pack_color(r0, g0, b0);
1400        self.color1 = pack_color(r1, g1, b1);
1401        self.color2 = pack_color(r2, g2, b2);
1402        self.color3 = pack_color(r3, g3, b3);
1403    }
1404}
1405
1406/// Textured quad with a single flat tint. 10 words (tag + 9 data).
1407///
1408/// Same CLUT + tpage embedding as [`TriTextured`], extended by
1409/// one vertex. Vertex order: TL, TR, BL, BR.
1410#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
1411#[repr(C, align(4))]
1412pub struct QuadTextured {
1413    /// OT linkage.
1414    pub tag: u32,
1415    /// `0x2C000000 | tint` header.
1416    pub color_cmd: u32,
1417    /// V0 position.
1418    pub v0: u32,
1419    /// `(u0, v0, clut)`.
1420    pub uv0_clut: u32,
1421    /// V1 position.
1422    pub v1: u32,
1423    /// `(u1, v1, tpage)`.
1424    pub uv1_tpage: u32,
1425    /// V2 position.
1426    pub v2: u32,
1427    /// `(u2, v2, 0)`.
1428    pub uv2: u32,
1429    /// V3 position.
1430    pub v3: u32,
1431    /// `(u3, v3, 0)`.
1432    pub uv3: u32,
1433}
1434
1435impl QuadTextured {
1436    /// Data-word count.
1437    pub const WORDS: u8 = 9;
1438
1439    /// Build a textured quad.
1440    pub const fn new(
1441        verts: [(i16, i16); 4],
1442        uvs: [(u8, u8); 4],
1443        clut: u16,
1444        tpage: u16,
1445        tint: (u8, u8, u8),
1446    ) -> Self {
1447        Self::with_material(verts, uvs, TextureMaterial::opaque(clut, tpage, tint))
1448    }
1449
1450    /// Build a textured quad using a [`TextureMaterial`].
1451    pub const fn with_material(
1452        verts: [(i16, i16); 4],
1453        uvs: [(u8, u8); 4],
1454        material: TextureMaterial,
1455    ) -> Self {
1456        let (u0, v0) = uvs[0];
1457        let (u1, v1) = uvs[1];
1458        let (u2, v2) = uvs[2];
1459        let (u3, v3) = uvs[3];
1460        let clut = material.clut_word();
1461        let tpage = material.texture_page_word();
1462        Self {
1463            tag: 0,
1464            color_cmd: material.flat_textured_polygon_header(true),
1465            v0: pack_vertex(verts[0].0, verts[0].1),
1466            uv0_clut: pack_texcoord(u0, v0, clut),
1467            v1: pack_vertex(verts[1].0, verts[1].1),
1468            uv1_tpage: pack_texcoord(u1, v1, tpage),
1469            v2: pack_vertex(verts[2].0, verts[2].1),
1470            uv2: pack_texcoord(u2, v2, 0),
1471            v3: pack_vertex(verts[3].0, verts[3].1),
1472            uv3: pack_texcoord(u3, v3, 0),
1473        }
1474    }
1475}
1476
1477/// Textured quad with inline texture-window state. 11 words (tag + 10 data).
1478///
1479/// This mirrors [`TriTextured`]'s self-contained OT packet shape for
1480/// quads: GP0(E2) texture-window state immediately followed by the
1481/// GP0(2Ch) textured-quad command. Use it when OT interleaving must
1482/// not leak window state across different textured draws.
1483#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
1484#[repr(C, align(4))]
1485pub struct QuadTexturedMaterial {
1486    /// OT linkage.
1487    pub tag: u32,
1488    /// GP0(E2) texture-window command.
1489    pub tex_window: u32,
1490    /// `0x2C000000 | tint` header.
1491    pub color_cmd: u32,
1492    /// V0 position.
1493    pub v0: u32,
1494    /// `(u0, v0, clut)`.
1495    pub uv0_clut: u32,
1496    /// V1 position.
1497    pub v1: u32,
1498    /// `(u1, v1, tpage)`.
1499    pub uv1_tpage: u32,
1500    /// V2 position.
1501    pub v2: u32,
1502    /// `(u2, v2, 0)`.
1503    pub uv2: u32,
1504    /// V3 position.
1505    pub v3: u32,
1506    /// `(u3, v3, 0)`.
1507    pub uv3: u32,
1508}
1509
1510impl QuadTexturedMaterial {
1511    /// Data-word count.
1512    pub const WORDS: u8 = 10;
1513
1514    /// Build a textured quad with self-contained material state.
1515    pub const fn with_material(
1516        verts: [(i16, i16); 4],
1517        uvs: [(u8, u8); 4],
1518        material: TextureMaterial,
1519    ) -> Self {
1520        let (u0, v0) = uvs[0];
1521        let (u1, v1) = uvs[1];
1522        let (u2, v2) = uvs[2];
1523        let (u3, v3) = uvs[3];
1524        let clut = material.clut_word();
1525        let tpage = material.texture_page_word();
1526        Self {
1527            tag: 0,
1528            tex_window: material.texture_window_word(),
1529            color_cmd: material.flat_textured_polygon_header(true),
1530            v0: pack_vertex(verts[0].0, verts[0].1),
1531            uv0_clut: pack_texcoord(u0, v0, clut),
1532            v1: pack_vertex(verts[1].0, verts[1].1),
1533            uv1_tpage: pack_texcoord(u1, v1, tpage),
1534            v2: pack_vertex(verts[2].0, verts[2].1),
1535            uv2: pack_texcoord(u2, v2, 0),
1536            v3: pack_vertex(verts[3].0, verts[3].1),
1537            uv3: pack_texcoord(u3, v3, 0),
1538        }
1539    }
1540}
1541
1542/// Textured sprite (variable size). 5 words (tag + 4 data).
1543#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
1544#[repr(C, align(4))]
1545pub struct Sprite {
1546    /// OT linkage.
1547    pub tag: u32,
1548    /// `0x64000000 | color` header (blend color applied over texture).
1549    pub color_cmd: u32,
1550    /// Top-left `xy`.
1551    pub xy: u32,
1552    /// `uv | clut` (U/V in low half, CLUT handle in high half).
1553    pub uv_clut: u32,
1554    /// Size `wh`.
1555    pub wh: u32,
1556}
1557
1558impl Sprite {
1559    /// Data-word count.
1560    pub const WORDS: u8 = 4;
1561
1562    /// Build a textured sprite. `clut` is the CLUT register handle
1563    /// (`y << 6 | x >> 4`); `uv` is the 8-bit texcoord within the
1564    /// texture page.
1565    pub const fn new(
1566        x: i16,
1567        y: i16,
1568        w: u16,
1569        h: u16,
1570        uv: (u8, u8),
1571        clut: u16,
1572        r: u8,
1573        g: u8,
1574        b: u8,
1575    ) -> Self {
1576        let material = TextureMaterial::opaque(clut, 0, (r, g, b));
1577        Self::with_material(x, y, w, h, uv, material)
1578    }
1579
1580    /// Build a textured sprite using a [`TextureMaterial`].
1581    ///
1582    /// Sprite packets do not carry a tpage word. The material's CLUT,
1583    /// tint, raw-texture bit, and semi-transparent command bit are
1584    /// encoded in the packet; the caller must set the matching draw
1585    /// mode before OT submission if the sprite samples a non-current
1586    /// tpage.
1587    pub const fn with_material(
1588        x: i16,
1589        y: i16,
1590        w: u16,
1591        h: u16,
1592        uv: (u8, u8),
1593        material: TextureMaterial,
1594    ) -> Self {
1595        Self {
1596            tag: 0,
1597            color_cmd: material.textured_rect_header(),
1598            xy: pack_vertex(x, y),
1599            uv_clut: pack_texcoord(uv.0, uv.1, material.clut_word()),
1600            wh: pack_xy(w, h),
1601        }
1602    }
1603}
1604
1605/// A GPU packet that can be linked into an ordering table as one DMA node.
1606///
1607/// [`crate::frame::OtFrame::add`] uses it to insert a packet without a
1608/// separate word count.
1609///
1610/// # Safety
1611///
1612/// The implementing type must be `#[repr(C)]` with 4-byte alignment and a
1613/// `u32` tag word as its first field, followed by at least `WORDS` more
1614/// initialised `u32`s that form the packet's GP0 payload. `WORDS` must not
1615/// exceed [`crate::chain::MAX_NODE_WORDS`]. The type must have no interior
1616/// mutability, so the payload cannot change while it is borrowed.
1617pub unsafe trait GpuPacket {
1618    /// Payload words after the tag.
1619    const WORDS: u8;
1620}
1621
1622macro_rules! impl_gpu_packet {
1623    ($($ty:ty),+ $(,)?) => {
1624        $(
1625            // SAFETY: an SDK packet: `#[repr(C, align(4))]`, tag first, then
1626            // `WORDS` payload words of plain `u32` data (checked below).
1627            unsafe impl GpuPacket for $ty {
1628                const WORDS: u8 = <$ty>::WORDS;
1629            }
1630            const _: () = {
1631                assert!(core::mem::size_of::<$ty>() >= 4 * (1 + <$ty>::WORDS as usize));
1632                assert!(core::mem::align_of::<$ty>() >= 4);
1633            };
1634        )+
1635    };
1636}
1637
1638impl_gpu_packet!(
1639    TriFlat,
1640    TriGouraud,
1641    QuadFlat,
1642    RectFlat,
1643    FillRect,
1644    QuadGouraud,
1645    QuadGouraudBlended,
1646    LineMono,
1647    LineGouraud,
1648    TriTextured,
1649    ClassicTriTextured,
1650    TriTexturedGouraud,
1651    ClassicTriTexturedGouraud,
1652    ClassicQuadTexturedGouraud,
1653    QuadTexturedGouraud,
1654    QuadTextured,
1655    QuadTexturedMaterial,
1656    Sprite,
1657);
1658
1659// Every packet here fits one linked-list node (see `crate::chain::MAX_NODE_WORDS`).
1660const _: () = {
1661    let words = [
1662        TriFlat::WORDS,
1663        TriGouraud::WORDS,
1664        QuadFlat::WORDS,
1665        RectFlat::WORDS,
1666        FillRect::WORDS,
1667        QuadGouraud::WORDS,
1668        QuadGouraudBlended::WORDS,
1669        LineMono::WORDS,
1670        LineGouraud::WORDS,
1671        TriTextured::WORDS,
1672        ClassicTriTextured::WORDS,
1673        TriTexturedGouraud::WORDS,
1674        ClassicTriTexturedGouraud::WORDS,
1675        ClassicQuadTexturedGouraud::WORDS,
1676        QuadTexturedGouraud::WORDS,
1677        QuadTextured::WORDS,
1678        QuadTexturedMaterial::WORDS,
1679        Sprite::WORDS,
1680    ];
1681    let mut i = 0;
1682    while i < words.len() {
1683        assert!(words[i] as usize <= crate::chain::MAX_NODE_WORDS);
1684        i += 1;
1685    }
1686};
1687
1688#[cfg(test)]
1689mod tests {
1690    use super::*;
1691
1692    #[test]
1693    fn blended_gouraud_quad_keeps_draw_mode_and_polygon_together() {
1694        let quad = QuadGouraudBlended::new(
1695            [(1, 2), (3, 4), (5, 6), (7, 8)],
1696            [
1697                (0x10, 0x20, 0x30),
1698                (0x40, 0x50, 0x60),
1699                (0x70, 0x80, 0x90),
1700                (0xa0, 0xb0, 0xc0),
1701            ],
1702            BlendMode::AddQuarter,
1703        );
1704        // SAFETY: QuadGouraudBlended is `repr(C, align(4))` with only u32 fields, so `quad` is
1705        // aligned and its size_of / 4 words are initialised u32s that outlive the slice.
1706        let words = unsafe {
1707            core::slice::from_raw_parts(
1708                (&quad as *const QuadGouraudBlended).cast::<u32>(),
1709                core::mem::size_of::<QuadGouraudBlended>() / core::mem::size_of::<u32>(),
1710            )
1711        };
1712        assert_eq!(words.len(), 10);
1713        assert_eq!(QuadGouraudBlended::WORDS, 9);
1714        assert_eq!(words[0], 0, "tag zero until OT insert");
1715        assert_eq!(words[1] >> 24, 0xe1, "draw-mode prefix");
1716        assert_eq!((words[1] >> 5) & 3, 3, "add-quarter ABR bits");
1717        assert_eq!(words[2] >> 24, 0x3a, "Gouraud+quad+semi-trans opcode");
1718    }
1719
1720    /// The Gouraud-textured quad packet must serialize to the exact GP0
1721    /// 0x3C word stream: a GP0(E2) texture-window prefix, then
1722    /// `[0x3C|c0, v0, uv0|clut, c1, v1, uv1|tpage, c2, v2, uv2, c3, v3, uv3]`.
1723    /// This guards the on-wire layout the DMA walker and emulator decode.
1724    #[test]
1725    fn quad_textured_gouraud_serializes_to_gp0_3c_stream() {
1726        let material =
1727            TexturedGouraudPacketMaterial::from_texture(TextureMaterial::new(0x1234, 0x0105));
1728        let uvw = [0x0201u16, 0x3C04, 0x3C42, 0x0240];
1729        let cols = [
1730            (0x11u8, 0x22u8, 0x33u8),
1731            (0x44, 0x55, 0x66),
1732            (0x77, 0x88, 0x99),
1733            (0xAA, 0xBB, 0xCC),
1734        ];
1735        let verts = [(10i16, 20i16), (110, 20), (110, 90), (10, 90)];
1736        let quad =
1737            QuadTexturedGouraud::with_packet_material_packed_uv_words(verts, uvw, cols, material);
1738
1739        assert_eq!(core::mem::size_of::<QuadTexturedGouraud>(), 14 * 4);
1740        assert_eq!(QuadTexturedGouraud::WORDS, 13);
1741
1742        // SAFETY: QuadTexturedGouraud is `repr(C, align(4))` with only u32 fields, and its size was
1743        // just asserted to be 14 words, so the slice covers exactly `quad`.
1744        let words = unsafe {
1745            core::slice::from_raw_parts((&quad as *const QuadTexturedGouraud).cast::<u32>(), 14)
1746        };
1747        assert_eq!(words[0], 0, "tag zero until OT insert");
1748        assert_eq!(words[1], material.tex_window_word, "E2 window prefix");
1749        assert_eq!(words[2] >> 24, 0x3C, "Gouraud+textured+quad opcode");
1750        assert_eq!(words[2] & 0x00FF_FFFF, pack_color(0x11, 0x22, 0x33));
1751        assert_eq!(words[3], pack_vertex(10, 20));
1752        assert_eq!(words[4], 0x0201 | material.clut_high_word);
1753        assert_eq!(words[5], pack_color(0x44, 0x55, 0x66));
1754        assert_eq!(words[6], pack_vertex(110, 20));
1755        assert_eq!(words[7], 0x3C04 | material.tpage_high_word);
1756        assert_eq!(words[8], pack_color(0x77, 0x88, 0x99));
1757        assert_eq!(words[9], pack_vertex(110, 90));
1758        assert_eq!(words[10], 0x3C42);
1759        assert_eq!(words[11], pack_color(0xAA, 0xBB, 0xCC));
1760        assert_eq!(words[12], pack_vertex(10, 90));
1761        assert_eq!(words[13], 0x0240);
1762    }
1763
1764    #[test]
1765    fn classic_packets_omit_texture_window_and_stage_ot_slot() {
1766        let tri = ClassicTriTexturedGouraud::with_staged_slot(
1767            [(1, 2), (3, 4), (5, 6)],
1768            [0x0201, 0x0403, 0x0605],
1769            [0x0033_2211, 0x0066_5544, 0x0099_8877],
1770            0x1234,
1771            0x0105,
1772            77,
1773        );
1774        assert_eq!(core::mem::size_of::<ClassicTriTexturedGouraud>(), 10 * 4);
1775        assert_eq!(tri.tag, (9 << 24) | 77);
1776        assert_eq!(tri.color0_cmd, 0x3433_2211);
1777        assert_eq!(tri.uv0_clut, 0x1234_0201);
1778        assert_eq!(tri.uv1_tpage, 0x0105_0403);
1779
1780        let quad = ClassicQuadTexturedGouraud::with_staged_slot(
1781            [(1, 2), (3, 4), (5, 6), (7, 8)],
1782            [0x0201, 0x0403, 0x0605, 0x0807],
1783            [0x0033_2211, 0x0066_5544, 0x0099_8877, 0x00cc_bbaa],
1784            0x1234,
1785            0x0105,
1786            88,
1787        );
1788        assert_eq!(core::mem::size_of::<ClassicQuadTexturedGouraud>(), 13 * 4);
1789        assert_eq!(quad.tag, (12 << 24) | 88);
1790        assert_eq!(quad.color0_cmd, 0x3c33_2211);
1791        assert_eq!(quad.uv3, 0x0807);
1792    }
1793
1794    #[test]
1795    fn classic_unchecked_packets_match_checked_packets_for_valid_words() {
1796        let tri_verts = [(1, 2), (3, 4), (5, 6)];
1797        let tri_uvs = [0x0201, 0x0403, 0x0605];
1798        let tri_colors = [0x0033_2211, 0x0066_5544, 0x0099_8877];
1799        let checked_tri = ClassicTriTexturedGouraud::with_staged_slot_prepacked_material(
1800            tri_verts,
1801            tri_uvs,
1802            tri_colors,
1803            0x1234_0000,
1804            0x0105_0000,
1805            77,
1806        );
1807        let unchecked_tri = ClassicTriTexturedGouraud::with_staged_slot_prepacked_colors(
1808            tri_verts,
1809            tri_uvs,
1810            tri_colors,
1811            0x1234_0000,
1812            0x0105_0000,
1813            77,
1814        );
1815        // SAFETY: ClassicTriTexturedGouraud is `repr(C, align(4))` with only u32 fields, so the
1816        // slice covers exactly the initialised local, which outlives it.
1817        let checked_tri_words = unsafe {
1818            core::slice::from_raw_parts(
1819                (&raw const checked_tri).cast::<u32>(),
1820                core::mem::size_of::<ClassicTriTexturedGouraud>() / core::mem::size_of::<u32>(),
1821            )
1822        };
1823        // SAFETY: ClassicTriTexturedGouraud is `repr(C, align(4))` with only u32 fields, so the
1824        // slice covers exactly the initialised local, which outlives it.
1825        let unchecked_tri_words = unsafe {
1826            core::slice::from_raw_parts(
1827                (&raw const unchecked_tri).cast::<u32>(),
1828                core::mem::size_of::<ClassicTriTexturedGouraud>() / core::mem::size_of::<u32>(),
1829            )
1830        };
1831        assert_eq!(checked_tri_words, unchecked_tri_words);
1832
1833        let quad_verts = [(1, 2), (3, 4), (5, 6), (7, 8)];
1834        let quad_uvs = [0x0201, 0x0403, 0x0605, 0x0807];
1835        let quad_colors = [0x0033_2211, 0x0066_5544, 0x0099_8877, 0x00cc_bbaa];
1836        let checked_quad = ClassicQuadTexturedGouraud::with_staged_slot_prepacked_material(
1837            quad_verts,
1838            quad_uvs,
1839            quad_colors,
1840            0x1234_0000,
1841            0x0105_0000,
1842            88,
1843        );
1844        let unchecked_quad = ClassicQuadTexturedGouraud::with_staged_slot_prepacked_colors(
1845            quad_verts,
1846            quad_uvs,
1847            quad_colors,
1848            0x1234_0000,
1849            0x0105_0000,
1850            88,
1851        );
1852        // SAFETY: ClassicQuadTexturedGouraud is `repr(C, align(4))` with only u32 fields, so the
1853        // slice covers exactly the initialised local, which outlives it.
1854        let checked_quad_words = unsafe {
1855            core::slice::from_raw_parts(
1856                (&raw const checked_quad).cast::<u32>(),
1857                core::mem::size_of::<ClassicQuadTexturedGouraud>() / core::mem::size_of::<u32>(),
1858            )
1859        };
1860        // SAFETY: ClassicQuadTexturedGouraud is `repr(C, align(4))` with only u32 fields, so the
1861        // slice covers exactly the initialised local, which outlives it.
1862        let unchecked_quad_words = unsafe {
1863            core::slice::from_raw_parts(
1864                (&raw const unchecked_quad).cast::<u32>(),
1865                core::mem::size_of::<ClassicQuadTexturedGouraud>() / core::mem::size_of::<u32>(),
1866            )
1867        };
1868        assert_eq!(checked_quad_words, unchecked_quad_words);
1869    }
1870}