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

psx_gte/
scene.rs

1//! Scene / camera / projection helpers.
2//!
3//! The register macros in [`regs`][crate::regs] are the only thing
4//! that actually touches the GTE; everything here is a convenience
5//! layer that bundles the ~8 writes a typical 3D frame needs into
6//! named functions. All functions are safe -- the macros they wrap
7//! already contain the `unsafe { asm! }` internally, and there's
8//! nothing we can do with a bad matrix value that would be undefined
9//! behaviour (worst case: the projected vertex is garbage).
10//!
11//! Typical frame:
12//!
13//! ```ignore
14//! scene::set_screen_offset(160 << 16, 120 << 16);
15//! scene::set_projection_plane(200);
16//! let rot = Mat3I16::rotate_y(angle);
17//! scene::load_rotation(&rot);
18//! scene::load_translation(Vec3I32::new(0, 0, 0x4000));
19//! for v in vertices {
20//!     let p = scene::project_vertex(v);
21//!     draw_point(p.sx, p.sy);
22//! }
23//! ```
24
25use crate::math::{Mat3I16, Vec3I16, Vec3I32};
26use crate::ops;
27use crate::regs::pack_xy;
28use crate::{read_control, read_data, write_control, write_data};
29#[cfg(target_arch = "mips")]
30use core::arch::asm;
31
32/// Result of a single perspective-projected vertex -- screen-space
33/// (x, y) in pixels plus the MAC3 depth used for ordering-table
34/// inserts. `Projected` is `Copy` + trivially packed so the caller
35/// can collect per-vertex results into an array and rasterise later.
36#[derive(Copy, Clone, Debug, PartialEq, Eq, Default)]
37#[repr(C)]
38pub struct Projected {
39    /// Screen-space X, clamped to GTE's ±0x400 range.
40    pub sx: i16,
41    /// Screen-space Y.
42    pub sy: i16,
43    /// Depth post-divide, 0..0xFFFF after saturation.
44    pub sz: u16,
45}
46
47/// A direction scaled to Q12 unit length by
48/// [`normalize_classic_q12_scheduled`], with the squared lengths it came
49/// from.
50#[derive(Copy, Clone, Debug, PartialEq, Eq, Default)]
51#[repr(C)]
52pub struct ClassicNormalizedVector {
53    /// The input direction at length 4096 (Q12 1.0); zero when the
54    /// squared length is zero or wrapped negative.
55    pub vector: Vec3I16,
56    /// `x * x + y * y` of the whole-unit components, wrapping.
57    pub xy_squared: i32,
58    /// `x * x + y * y + z * z` of the whole-unit components, wrapping.
59    pub squared: i32,
60}
61
62/// floor(sqrt(n)) by bisection, for building tables at compile time.
63const fn isqrt_u32(n: u32) -> u32 {
64    let (mut lo, mut hi) = (0u32, 1u32 << 16);
65    while hi - lo > 1 {
66        let mid = (lo + hi) / 2;
67        if mid * mid <= n {
68            lo = mid;
69        } else {
70            hi = mid;
71        }
72    }
73    lo
74}
75
76/// Q12 reciprocal square roots of the sums 64..=255, scaled so the sum 64
77/// maps to 1.0: entry `m - 64` is floor(4096 * sqrt(64 / m)). That equals
78/// floor(32768 / sqrt(m)) = floor(sqrt(2^30 / m)), and since flooring the
79/// argument first doesn't change an integer square root's floor, it is
80/// computed exactly in integers.
81const RSQRT_Q12_FROM_64: [i16; 192] = {
82    let mut table = [0i16; 192];
83    let mut i = 0;
84    while i < 192 {
85        table[i] = isqrt_u32((1u32 << 30) / (64 + i as u32)) as i16;
86        i += 1;
87    }
88    table
89};
90
91/// Scale a Q12 direction to length 4096 with a 192-entry reciprocal square
92/// root table, the classic fixed-point way.
93///
94/// Each component is floored to whole units (`>> 12`) and kept as 16 bits.
95/// With `s` the sum of their squares (wrapping `i32`):
96///
97/// 1. `L` = leading zero bits of `s`, rounded down to even.
98/// 2. `s` shifted by `L - 24` lands in 64..=255; the table gives
99///    `r` = floor(4096 * sqrt(64 / that)).
100/// 3. Each component becomes `(c * r) >> ((31 - L) / 2)`.
101///
102/// Because `L` is even, the table entry equals 4096 * 2^(15 - L/2) / sqrt(s),
103/// and the shift in step 3 removes that power of two exactly, leaving
104/// 4096 * c / sqrt(s) up to the table's truncation. A zero or wrapped (negative)
105/// `s` gives a zero vector. Pure integer work: no GTE state is used.
106pub fn normalize_classic_q12_scheduled(input: Vec3I32) -> ClassicNormalizedVector {
107    let c = [
108        (input.x >> 12) as i16,
109        (input.y >> 12) as i16,
110        (input.z >> 12) as i16,
111    ];
112    let square = |v: i16| i32::from(v) * i32::from(v);
113    let xy_squared = square(c[0]).wrapping_add(square(c[1]));
114    let squared = xy_squared.wrapping_add(square(c[2]));
115    if squared <= 0 {
116        return ClassicNormalizedVector {
117            vector: Vec3I16::new(0, 0, 0),
118            xy_squared,
119            squared,
120        };
121    }
122    let even_zeros = (squared as u32).leading_zeros() & !1;
123    let in_range = if even_zeros >= 24 {
124        squared << (even_zeros - 24)
125    } else {
126        squared >> (24 - even_zeros)
127    };
128    let r = i32::from(RSQRT_Q12_FROM_64[(in_range - 64) as usize]);
129    let shift = (31 - even_zeros) / 2;
130    let scale = |v: i16| ((i32::from(v) * r) >> shift) as i16;
131    ClassicNormalizedVector {
132        vector: Vec3I16::new(scale(c[0]), scale(c[1]), scale(c[2])),
133        xy_squared,
134        squared,
135    }
136}
137
138/// Four-byte-aligned plane record used by the GTE AABB clip batch.
139///
140/// This layout deliberately matches retained engines that store
141/// `(normal, kind, signbits, distance)`. `kind` remains caller-owned, while
142/// `signbits` caches the negative-normal mask (`x | y << 1 | z << 2`) used to
143/// select AABB support points without rereading the normal components.
144#[derive(Copy, Clone, Debug, PartialEq, Eq, Default)]
145#[repr(C)]
146pub struct AabbClipPlane {
147    /// Signed Q12 plane normal.
148    pub normal: [i16; 3],
149    /// Caller-owned plane kind or axial classification.
150    pub kind: u8,
151    /// Negative-normal mask (`x | y << 1 | z << 2`).
152    pub signbits: u8,
153    /// Q12 plane distance.
154    pub distance: i32,
155}
156
157/// Load the rotation matrix into the GTE's RT control registers (0..=4).
158#[doc(alias = "SetRotMatrix")]
159pub fn load_rotation(m: &Mat3I16) {
160    write_control!(0, pack_xy(m.m[0][0], m.m[0][1]));
161    write_control!(1, pack_xy(m.m[0][2], m.m[1][0]));
162    write_control!(2, pack_xy(m.m[1][1], m.m[1][2]));
163    write_control!(3, pack_xy(m.m[2][0], m.m[2][1]));
164    write_control!(4, m.m[2][2] as i32 as u32);
165}
166
167/// Load the light-direction matrix (LLM, control 8..=12).
168#[doc(alias = "SetLightMatrix")]
169pub fn load_light_matrix(m: &Mat3I16) {
170    write_control!(8, pack_xy(m.m[0][0], m.m[0][1]));
171    write_control!(9, pack_xy(m.m[0][2], m.m[1][0]));
172    write_control!(10, pack_xy(m.m[1][1], m.m[1][2]));
173    write_control!(11, pack_xy(m.m[2][0], m.m[2][1]));
174    write_control!(12, m.m[2][2] as i32 as u32);
175}
176
177/// Load four clip-plane normals for [`classify_aabb_clip4`] and
178/// [`is_aabb_outside_clip4`].
179///
180/// Planes zero through two occupy the rotation-matrix rows. Plane three uses
181/// the first light-matrix row. This intentionally replaces both matrices;
182/// callers must restore their camera and lighting state before projection or
183/// lit geometry submission.
184pub fn load_aabb_clip4(planes: &[AabbClipPlane; 4]) {
185    load_rotation(&Mat3I16 {
186        m: [planes[0].normal, planes[1].normal, planes[2].normal],
187    });
188    load_light_matrix(&Mat3I16 {
189        m: [planes[3].normal, [0; 3], [0; 3]],
190    });
191}
192
193/// Load the light-color matrix (LCM, control 16..=20).
194#[doc(alias = "SetColorMatrix")]
195pub fn load_light_color_matrix(m: &Mat3I16) {
196    write_control!(16, pack_xy(m.m[0][0], m.m[0][1]));
197    write_control!(17, pack_xy(m.m[0][2], m.m[1][0]));
198    write_control!(18, pack_xy(m.m[1][1], m.m[1][2]));
199    write_control!(19, pack_xy(m.m[2][0], m.m[2][1]));
200    write_control!(20, m.m[2][2] as i32 as u32);
201}
202
203/// Load the translation vector (TR, control 5..=7).
204#[doc(alias = "SetTransMatrix")]
205pub fn load_translation(t: Vec3I32) {
206    write_control!(5, t.x as u32);
207    write_control!(6, t.y as u32);
208    write_control!(7, t.z as u32);
209}
210
211/// Load the background-color bias (BK, control 13..=15).
212#[doc(alias = "SetBackColor")]
213pub fn load_background_color(c: Vec3I32) {
214    write_control!(13, c.x as u32);
215    write_control!(14, c.y as u32);
216    write_control!(15, c.z as u32);
217}
218
219/// Load the far-color bias (FC, control 21..=23) used by depth-cue
220/// interpolation.
221#[doc(alias = "SetFarColor")]
222pub fn load_far_color(c: Vec3I32) {
223    write_control!(21, c.x as u32);
224    write_control!(22, c.y as u32);
225    write_control!(23, c.z as u32);
226}
227
228/// Set OFX and OFY (control 24, 25) -- the screen-space offsets applied
229/// post-divide. Values are 15.16 fixed point; `160 << 16` = 160.0 px.
230#[doc(alias = "SetGeomOffset")]
231pub fn set_screen_offset(ofx_15_16: i32, ofy_15_16: i32) {
232    write_control!(24, ofx_15_16 as u32);
233    write_control!(25, ofy_15_16 as u32);
234}
235
236/// Set the projection-plane distance H (control 26). Larger H = longer
237/// focal length = narrower FOV.
238#[doc(alias = "SetGeomScreen")]
239pub fn set_projection_plane(h: u16) {
240    write_control!(26, h as i32 as u32);
241}
242
243/// Set the depth-cue coefficients DQA / DQB (control 27, 28).
244/// Depth-cue outputs IR0 = DQA/H + DQB, scaled to 0..0x1000.
245pub fn set_depth_cue(dqa: i16, dqb: i32) {
246    write_control!(27, dqa as i32 as u32);
247    write_control!(28, dqb as u32);
248}
249
250/// Set the AVSZ3/AVSZ4 averaging weights (control 29, 30). Typical
251/// values: `ZSF3 = 0x555` (= 1/3 in 0.12), `ZSF4 = 0x400` (= 1/4).
252#[doc(alias = "AVSZ")]
253#[doc(alias = "ZSF3")]
254#[doc(alias = "ZSF4")]
255pub fn set_average_z_weights(zsf3: i16, zsf4: i16) {
256    write_control!(29, zsf3 as i32 as u32);
257    write_control!(30, zsf4 as i32 as u32);
258}
259
260/// Load `v` into the V0 input slot (data registers 0 and 1) and run
261/// RTPS to project it. Returns the screen-space pair + depth so the
262/// caller can immediately use the result.
263///
264/// Assumes the rotation matrix, translation, screen offset, and
265/// projection plane have already been set.
266#[doc(alias = "RotTransPers")]
267pub fn project_vertex(v: Vec3I16) -> Projected {
268    write_data!(0, v.xy_packed());
269    write_data!(1, v.z_packed());
270    // SAFETY: V0 has just been loaded; RT / TR / H / OFX / OFY are
271    // assumed to be set by the caller's scene setup.
272    unsafe { ops::project_single() };
273    let sxy = read_data!(14);
274    let sz = read_data!(19) as u16;
275    Projected {
276        sx: sxy as i16,
277        sy: (sxy >> 16) as i16,
278        sz,
279    }
280}
281
282/// Project three vertices as a batch via RTPT -- one GTE call, three
283/// results out of the SXY FIFO + SZ FIFO. Slightly faster than three
284/// successive [`project_vertex`] calls because RTPT shares setup.
285///
286/// The returned array is `[v0_result, v1_result, v2_result]`.
287#[doc(alias = "RotTransPers3")]
288pub fn project_triangle(v0: Vec3I16, v1: Vec3I16, v2: Vec3I16) -> [Projected; 3] {
289    // Load all three vertices first (data regs 0..=5), then fire RTPT.
290    write_data!(0, v0.xy_packed());
291    write_data!(1, v0.z_packed());
292    write_data!(2, v1.xy_packed());
293    write_data!(3, v1.z_packed());
294    write_data!(4, v2.xy_packed());
295    write_data!(5, v2.z_packed());
296    // SAFETY: all three vertices are loaded; scene-setup registers
297    // are the caller's responsibility.
298    unsafe { ops::project_triple() };
299    // After RTPT, SXY FIFO holds (v0, v1, v2) in slots 0/1/2, and
300    // SZ FIFO holds them in SZ1/SZ2/SZ3.
301    let sxy0 = read_data!(12);
302    let sxy1 = read_data!(13);
303    let sxy2 = read_data!(14);
304    let sz1 = read_data!(17) as u16;
305    let sz2 = read_data!(18) as u16;
306    let sz3 = read_data!(19) as u16;
307    [
308        Projected {
309            sx: sxy0 as i16,
310            sy: (sxy0 >> 16) as i16,
311            sz: sz1,
312        },
313        Projected {
314            sx: sxy1 as i16,
315            sy: (sxy1 >> 16) as i16,
316            sz: sz2,
317        },
318        Projected {
319            sx: sxy2 as i16,
320            sy: (sxy2 >> 16) as i16,
321            sz: sz3,
322        },
323    ]
324}
325
326/// Transform one vertex by the currently loaded RT/TR matrix without
327/// perspective projection. Returns MAC1/2/3 in view-space units.
328///
329/// Assumes the rotation matrix and translation have already been set.
330#[doc(alias = "RotTrans")]
331pub fn transform_vertex(v: Vec3I16) -> Vec3I32 {
332    write_data!(0, v.xy_packed());
333    write_data!(1, v.z_packed());
334    // SAFETY: V0 has just been loaded; RT/TR are set by scene setup.
335    unsafe { ops::rotate_translate_v0() };
336    Vec3I32::new(
337        read_data!(25) as i32,
338        read_data!(26) as i32,
339        read_data!(27) as i32,
340    )
341}
342
343/// Transform one vertex with a lower-overhead MIPS register schedule.
344///
345/// Keep the default helper compact; use this variant in measured hot
346/// paths that already keep the relevant GTE camera matrix loaded.
347#[inline(always)]
348pub fn transform_vertex_scheduled(v: Vec3I16) -> Vec3I32 {
349    #[cfg(target_arch = "mips")]
350    {
351        transform_vertex_mips(v)
352    }
353    #[cfg(not(target_arch = "mips"))]
354    {
355        transform_vertex(v)
356    }
357}
358
359/// Compose two Q12 rotation/scale matrices with the GTE's scheduled MVMVA
360/// path.
361///
362/// The left matrix is loaded into the rotation registers and each column of
363/// `right` is transformed in turn. Translation is forced to zero, so the
364/// result is exactly the rotation/scale product. This clobbers the live GTE
365/// rotation and translation state; callers normally load the returned scene
366/// matrix immediately afterward.
367#[inline]
368pub fn compose_rotation_scheduled(left: &Mat3I16, right: &Mat3I16) -> Mat3I16 {
369    load_rotation(left);
370    load_translation(Vec3I32::ZERO);
371
372    let c0 = transform_vertex_scheduled(Vec3I16::new(right.m[0][0], right.m[1][0], right.m[2][0]));
373    let c1 = transform_vertex_scheduled(Vec3I16::new(right.m[0][1], right.m[1][1], right.m[2][1]));
374    let c2 = transform_vertex_scheduled(Vec3I16::new(right.m[0][2], right.m[1][2], right.m[2][2]));
375    let clamp = |value: i32| value.clamp(i16::MIN as i32, i16::MAX as i32) as i16;
376
377    Mat3I16 {
378        m: [
379            [clamp(c0.x), clamp(c1.x), clamp(c2.x)],
380            [clamp(c0.y), clamp(c1.y), clamp(c2.y)],
381            [clamp(c0.z), clamp(c1.z), clamp(c2.z)],
382        ],
383    }
384}
385
386/// Project one vertex with a lower-overhead MIPS register schedule.
387///
388/// This is intended for very hot batched paths that have been benchmarked
389/// with the larger inlined code shape. The portable path delegates to
390/// [`project_vertex`] so host preview/emulator tests remain identical.
391#[inline(always)]
392pub fn project_vertex_scheduled(v: Vec3I16) -> Projected {
393    #[cfg(target_arch = "mips")]
394    {
395        project_vertex_mips(v)
396    }
397    #[cfg(not(target_arch = "mips"))]
398    {
399        project_vertex(v)
400    }
401}
402
403/// Project three vertices with a lower-overhead MIPS register schedule.
404///
405/// The normal [`project_triangle`] helper stays compact for general users;
406/// this variant is used only by renderer loops where profiling shows that
407/// shaving COP2 register wrapper overhead pays for the extra code size.
408#[inline(always)]
409pub fn project_triangle_scheduled(v0: Vec3I16, v1: Vec3I16, v2: Vec3I16) -> [Projected; 3] {
410    #[cfg(target_arch = "mips")]
411    {
412        project_triangle_mips(v0, v1, v2)
413    }
414    #[cfg(not(target_arch = "mips"))]
415    {
416        project_triangle(v0, v1, v2)
417    }
418}
419
420/// In-flight RTPT kicked by [`start_project_triple`]; [`read`](Self::read)
421/// collects the three projected results.
422///
423/// Between kick and read the caller must not issue any other GTE op
424/// or touch GTE registers; arbitrary scalar CPU work is fine and is
425/// the whole point -- it runs while the GTE projects. On the host
426/// build the projection happens eagerly at kick and `read` just
427/// returns it.
428#[must_use = "call read() to collect the projected triple"]
429#[doc(alias = "RTPT")]
430pub struct ProjectTripleInFlight(#[cfg(not(target_arch = "mips"))] [Projected; 3]);
431
432/// Load V0..V2 and issue RTPT without reading results, so the caller
433/// can overlap the GTE op with scalar work for the previous triple.
434///
435/// The MTC2 order matches [`project_triangle_scheduled`]: V0 is
436/// written 6 instructions before RTPT issues and RTPT consumes V2
437/// last, so the HWB-010/011 commit-slip hazard profile is unchanged.
438#[inline(always)]
439#[doc(alias = "RTPT")]
440pub fn start_project_triple(v0: Vec3I16, v1: Vec3I16, v2: Vec3I16) -> ProjectTripleInFlight {
441    #[cfg(target_arch = "mips")]
442    {
443        let v0_xy = v0.xy_packed();
444        let v0_z = v0.z_packed();
445        let v1_xy = v1.xy_packed();
446        let v1_z = v1.z_packed();
447        let v2_xy = v2.xy_packed();
448        let v2_z = v2.z_packed();
449        // SAFETY: the six words are MTC2 $8..$13 into data regs 0..5 (V0..V2),
450        // two NOPs, then RTPT. Each MTC2 only reads its declared `in` register,
451        // RTPT only touches GTE registers, and no CPU register is written, so
452        // `nomem`/`nostack` with no outputs is accurate.
453        unsafe {
454            asm!(
455                // MTC2 $8..$13 into V0/V1/V2 input registers.
456                ".word {w0}",
457                ".word {w1}",
458                ".word {w2}",
459                ".word {w3}",
460                ".word {w4}",
461                ".word {w5}",
462                // Conservative HWB-010/011 input-commit gap for the final V2
463                // write. The original console failure proved the rule for
464                // MVMVA/RTPS, not RTPT; hardware-tests v1.20 records 0xC0-C3
465                // explicitly arbitrate whether RTPT needs these two slots.
466                ".word 0",
467                ".word 0",
468                // RTPT.
469                ".word {w6}",
470                w0 = const psx_hw::gte::mtc2(8, 0),
471                w1 = const psx_hw::gte::mtc2(9, 1),
472                w2 = const psx_hw::gte::mtc2(10, 2),
473                w3 = const psx_hw::gte::mtc2(11, 3),
474                w4 = const psx_hw::gte::mtc2(12, 4),
475                w5 = const psx_hw::gte::mtc2(13, 5),
476                w6 = const psx_hw::gte::command::RTPT,
477                in("$8") v0_xy,
478                in("$9") v0_z,
479                in("$10") v1_xy,
480                in("$11") v1_z,
481                in("$12") v2_xy,
482                in("$13") v2_z,
483                options(nostack, nomem, preserves_flags),
484            );
485        }
486        ProjectTripleInFlight()
487    }
488    #[cfg(not(target_arch = "mips"))]
489    {
490        ProjectTripleInFlight(project_triangle(v0, v1, v2))
491    }
492}
493
494impl ProjectTripleInFlight {
495    /// Collect the projected triple.
496    ///
497    /// MFC2 does not provide a general GTE-busy interlock: the console result
498    /// in hardware-tests record 0xB0 disproved that older assumption for
499    /// NCLIP/MAC0. This RTPT sequence is safe only if its first result is ready
500    /// by the read issue point; v1.20 records 0xC4-C7 measure that exact window.
501    /// Until silicon answers them, callers must not treat a short overlap as a
502    /// guaranteed blocking wait.
503    #[inline(always)]
504    pub fn read(self) -> [Projected; 3] {
505        #[cfg(target_arch = "mips")]
506        {
507            let sxy0: u32;
508            let sxy1: u32;
509            let sxy2: u32;
510            let sz1: u32;
511            let sz2: u32;
512            let sz3: u32;
513            // SAFETY: six MFC2s copy SXY0..SXY2 (data 12..14) and SZ1..SZ3 (17..19)
514            // into $8..$13, all declared as outputs; each read's load-delay slot is
515            // covered by the next MFC2 and the final NOP. No memory or stack is
516            // touched. Reading before RTPT finishes yields stale values, not UB (see
517            // the doc comment above).
518            unsafe {
519                asm!(
520                    // Read SXY0/SXY1/SXY2/SZ1/SZ2/SZ3; each MFC2's
521                    // load-delay slot is filled by the next, so only
522                    // the final read needs the explicit NOP.
523                    ".word {w0}",
524                    ".word {w1}",
525                    ".word {w2}",
526                    ".word {w3}",
527                    ".word {w4}",
528                    ".word {w5}",
529                    ".word 0",
530                    w0 = const psx_hw::gte::mfc2(8, 12),
531                    w1 = const psx_hw::gte::mfc2(9, 13),
532                    w2 = const psx_hw::gte::mfc2(10, 14),
533                    w3 = const psx_hw::gte::mfc2(11, 17),
534                    w4 = const psx_hw::gte::mfc2(12, 18),
535                    w5 = const psx_hw::gte::mfc2(13, 19),
536                    out("$8") sxy0,
537                    out("$9") sxy1,
538                    out("$10") sxy2,
539                    out("$11") sz1,
540                    out("$12") sz2,
541                    out("$13") sz3,
542                    options(nostack, nomem, preserves_flags),
543                );
544            }
545            [
546                Projected {
547                    sx: sxy0 as i16,
548                    sy: (sxy0 >> 16) as i16,
549                    sz: sz1 as u16,
550                },
551                Projected {
552                    sx: sxy1 as i16,
553                    sy: (sxy1 >> 16) as i16,
554                    sz: sz2 as u16,
555                },
556                Projected {
557                    sx: sxy2 as i16,
558                    sy: (sxy2 >> 16) as i16,
559                    sz: sz3 as u16,
560                },
561            ]
562        }
563        #[cfg(not(target_arch = "mips"))]
564        {
565            self.0
566        }
567    }
568}
569
570#[cfg(target_arch = "mips")]
571#[inline(always)]
572fn project_vertex_mips(v: Vec3I16) -> Projected {
573    let mut sxy = v.xy_packed();
574    let mut sz = v.z_packed();
575    // SAFETY: MTC2 $8/$9 into V0, two NOPs, RTPS, then MFC2 SXY2 (data 14)
576    // into $8 and SZ3 (data 19) into $9 plus a load-delay NOP. Only $8 and $9
577    // are written and both are `inlateout`; no memory or stack is touched.
578    unsafe {
579        asm!(
580            // MTC2 $8,VXY0 and $9,VZ0.
581            ".word {w0}",
582            ".word {w1}",
583            // HWB-010/011 input-commit gap. The single-vertex path used to
584            // issue RTPS immediately here, leaving VXY0 at the same two-tick
585            // distance as the console-confirmed MVMVA vertex-explosion case.
586            // Two NOPs move it to the known-safe four-instruction distance.
587            ".word 0",
588            ".word 0",
589            // RTPS.
590            ".word {w2}",
591            // Read SXY2 and SZ3. Two MFC2s can share one final
592            // load-delay NOP instead of one NOP per wrapper call.
593            ".word {w3}",
594            ".word {w4}",
595            ".word 0",
596            w0 = const psx_hw::gte::mtc2(8, 0),
597            w1 = const psx_hw::gte::mtc2(9, 1),
598            w2 = const psx_hw::gte::command::RTPS,
599            w3 = const psx_hw::gte::mfc2(8, 14),
600            w4 = const psx_hw::gte::mfc2(9, 19),
601            inlateout("$8") sxy,
602            inlateout("$9") sz,
603            options(nostack, nomem, preserves_flags),
604        );
605    }
606    Projected {
607        sx: sxy as i16,
608        sy: (sxy >> 16) as i16,
609        sz: sz as u16,
610    }
611}
612
613#[cfg(target_arch = "mips")]
614#[inline(always)]
615fn project_triangle_mips(v0: Vec3I16, v1: Vec3I16, v2: Vec3I16) -> [Projected; 3] {
616    let v0_xy = v0.xy_packed();
617    let v0_z = v0.z_packed();
618    let v1_xy = v1.xy_packed();
619    let v1_z = v1.z_packed();
620    let v2_xy = v2.xy_packed();
621    let v2_z = v2.z_packed();
622    let sxy0: u32;
623    let sxy1: u32;
624    let sxy2: u32;
625    let sz1: u32;
626    let sz2: u32;
627    let sz3: u32;
628    // SAFETY: MTC2 $8..$13 into V0..V2, two NOPs, RTPT, then MFC2s of SXY0..2
629    // and SZ1..3 back into $8..$13 with a final load-delay NOP. Exactly those
630    // six registers are written, each declared `inlateout`; no memory or
631    // stack is touched.
632    unsafe {
633        asm!(
634            // MTC2 $8..$13 into V0/V1/V2 input registers.
635            ".word {w0}",
636            ".word {w1}",
637            ".word {w2}",
638            ".word {w3}",
639            ".word {w4}",
640            ".word {w5}",
641            // Conservative HWB-010/011 input-commit gap. The original
642            // console failure proved MVMVA/RTPS; hardware-tests v1.20 records
643            // 0xC0-C3 measure RTPT directly before we remove these slots.
644            // Keep this in sync with start_project_triple.
645            ".word 0",
646            ".word 0",
647            // RTPT.
648            ".word {w6}",
649            // Read SXY0/SXY1/SXY2/SZ1/SZ2/SZ3. Each MFC2's
650            // load-delay slot is filled by the next MFC2, so only the
651            // final read needs an explicit NOP before Rust observes
652            // the output registers.
653            ".word {w7}",
654            ".word {w8}",
655            ".word {w9}",
656            ".word {w10}",
657            ".word {w11}",
658            ".word {w12}",
659            ".word 0",
660            w0 = const psx_hw::gte::mtc2(8, 0),
661            w1 = const psx_hw::gte::mtc2(9, 1),
662            w2 = const psx_hw::gte::mtc2(10, 2),
663            w3 = const psx_hw::gte::mtc2(11, 3),
664            w4 = const psx_hw::gte::mtc2(12, 4),
665            w5 = const psx_hw::gte::mtc2(13, 5),
666            w6 = const psx_hw::gte::command::RTPT,
667            w7 = const psx_hw::gte::mfc2(8, 12),
668            w8 = const psx_hw::gte::mfc2(9, 13),
669            w9 = const psx_hw::gte::mfc2(10, 14),
670            w10 = const psx_hw::gte::mfc2(11, 17),
671            w11 = const psx_hw::gte::mfc2(12, 18),
672            w12 = const psx_hw::gte::mfc2(13, 19),
673            inlateout("$8") v0_xy => sxy0,
674            inlateout("$9") v0_z => sxy1,
675            inlateout("$10") v1_xy => sxy2,
676            inlateout("$11") v1_z => sz1,
677            inlateout("$12") v2_xy => sz2,
678            inlateout("$13") v2_z => sz3,
679            options(nostack, nomem, preserves_flags),
680        );
681    }
682    [
683        Projected {
684            sx: sxy0 as i16,
685            sy: (sxy0 >> 16) as i16,
686            sz: sz1 as u16,
687        },
688        Projected {
689            sx: sxy1 as i16,
690            sy: (sxy1 >> 16) as i16,
691            sz: sz2 as u16,
692        },
693        Projected {
694            sx: sxy2 as i16,
695            sy: (sxy2 >> 16) as i16,
696            sz: sz3 as u16,
697        },
698    ]
699}
700
701#[cfg(target_arch = "mips")]
702#[inline(always)]
703fn transform_vertex_mips(v: Vec3I16) -> Vec3I32 {
704    let xy = v.xy_packed();
705    let z = v.z_packed();
706    let mac1: u32;
707    let mac2: u32;
708    let mac3: u32;
709    // SAFETY: MTC2 $8/$9 into V0, two NOPs, MVMVA (RT, V0, TR), then MFC2
710    // MAC1..MAC3 (data 25..27) into $8..$10 with a final load-delay NOP. Only
711    // $8..$10 are written and all are declared outputs; no memory or stack is
712    // touched.
713    unsafe {
714        asm!(
715            // MTC2 $8,VXY0 and $9,VZ0.
716            ".word {w0}",
717            ".word {w1}",
718            // HWB-010/011 hazard gap (console-confirmed fix): two buffer
719            // NOPs push the VXY0 write's commit distance from 2 to 4
720            // instructions. Without them, real silicon can commit the
721            // write BETWEEN the MVMVA's sequential MAC1 and MAC2 compute
722            // phases, so MAC1 reads the previous V0.x -- the cortex
723            // vertex-explosion mechanism seen in the HWB-010 live capture.
724            ".word 0",
725            ".word 0",
726            // MVMVA RT,V0,TR,sf=1.
727            ".word {w2}",
728            // Read MAC1/MAC2/MAC3. Consecutive MFC2 instructions fill
729            // each other's load-delay slot; only the final read needs
730            // an explicit NOP before Rust observes the outputs.
731            ".word {w3}",
732            ".word {w4}",
733            ".word {w5}",
734            ".word 0",
735            w0 = const psx_hw::gte::mtc2(8, 0),
736            w1 = const psx_hw::gte::mtc2(9, 1),
737            w2 = const psx_hw::gte::command::ROTATE_TRANSLATE_V0,
738            w3 = const psx_hw::gte::mfc2(8, 25),
739            w4 = const psx_hw::gte::mfc2(9, 26),
740            w5 = const psx_hw::gte::mfc2(10, 27),
741            inlateout("$8") xy => mac1,
742            inlateout("$9") z => mac2,
743            lateout("$10") mac3,
744            options(nostack, nomem, preserves_flags),
745        );
746    }
747    Vec3I32::new(mac1 as i32, mac2 as i32, mac3 as i32)
748}
749
750/// Result of [`transform_vertex_probed`]: the live-schedule transform
751/// output plus post-op hazard evidence.
752#[derive(Clone, Copy)]
753pub struct TransformProbe {
754    /// The transform result exactly as the hot path reads it (the
755    /// IMMEDIATE MAC1/2/3 reads, same schedule as
756    /// `transform_vertex_mips`). Consumers keep using this so the
757    /// probed build behaves identically to the live engine.
758    pub out: Vec3I32,
759    /// MAC1 re-read after a 4-NOP settle gap. Differs from `out.x`
760    /// only if the immediate MAC1 read was served stale.
761    pub x_settled: i32,
762    /// Translation control regs (TRX/TRY/TRZ, cr5..cr7) read back
763    /// AFTER the op (nothing writes them during, so this is the value
764    /// in effect while the MVMVA executed). The compose path loads
765    /// zero here; nonzero = the zero write did not land for this op.
766    pub tr: [i32; 3],
767}
768
769/// Hazard-hunt instrument (keep): one MVMVA on the DELIBERATELY
770/// UNPADDED pre-HWB-011 schedule (V0 writes 1-2 instructions before
771/// the op -- the schedule that trips the silicon MTC2-commit hazard),
772/// with evidence reads appended strictly AFTER the op: a settled MAC1
773/// re-read and a TRX/TRY/TRZ readback. This is the live evidence
774/// channel that decoded the vertex explosion; it stays in the SDK so the
775/// next hardware mystery starts from a proven instrument. Not for production paths -- use
776/// [`transform_vertex_scheduled`], whose schedule carries the
777/// console-confirmed hazard gap.
778#[inline(always)]
779pub fn transform_vertex_probed(v: Vec3I16) -> TransformProbe {
780    #[cfg(target_arch = "mips")]
781    {
782        let xy = v.xy_packed();
783        let z = v.z_packed();
784        let mac1: u32;
785        let mac2: u32;
786        let mac3: u32;
787        let mac1_settled: u32;
788        let trx: u32;
789        let try_: u32;
790        let trz: u32;
791        // SAFETY: same MTC2/MVMVA/MFC2 shape as `transform_vertex_mips` minus the
792        // input gap, then a MAC1 re-read into $11 and CFC2 TRX/TRY/TRZ (control
793        // 5..7) into $12..$14. Every written register ($8..$14) is a declared
794        // output and no memory or stack is touched. The deliberately unpadded
795        // schedule can return stale MAC values; that is a data hazard, not UB.
796        unsafe {
797            asm!(
798                // Live schedule, byte-identical to transform_vertex_mips:
799                // MTC2 $8,VXY0 / MTC2 $9,VZ0 / MVMVA / MAC1,2,3 reads.
800                ".word {w0}",
801                ".word {w1}",
802                ".word {w2}",
803                ".word {w3}",
804                ".word {w4}",
805                ".word {w5}",
806                ".word 0",
807                // Probe tail, strictly AFTER the live reads: 4-NOP
808                // settle gap, MAC1 re-read ($11), then TRX/TRY/TRZ
809                // (cr5..cr7) read-back; chained CFC2s share delay
810                // slots, final NOP covers the last one.
811                ".word 0",
812                ".word 0",
813                ".word 0",
814                ".word 0",
815                ".word {w6}",
816                ".word {w7}",
817                ".word {w8}",
818                ".word {w9}",
819                ".word 0",
820                w0 = const psx_hw::gte::mtc2(8, 0),
821                w1 = const psx_hw::gte::mtc2(9, 1),
822                w2 = const psx_hw::gte::command::ROTATE_TRANSLATE_V0,
823                w3 = const psx_hw::gte::mfc2(8, 25),
824                w4 = const psx_hw::gte::mfc2(9, 26),
825                w5 = const psx_hw::gte::mfc2(10, 27),
826                w6 = const psx_hw::gte::mfc2(11, 25),
827                w7 = const psx_hw::gte::cfc2(12, 5),
828                w8 = const psx_hw::gte::cfc2(13, 6),
829                w9 = const psx_hw::gte::cfc2(14, 7),
830                inlateout("$8") xy => mac1,
831                inlateout("$9") z => mac2,
832                lateout("$10") mac3,
833                lateout("$11") mac1_settled,
834                lateout("$12") trx,
835                lateout("$13") try_,
836                lateout("$14") trz,
837                options(nostack, nomem, preserves_flags),
838            );
839        }
840        TransformProbe {
841            out: Vec3I32::new(mac1 as i32, mac2 as i32, mac3 as i32),
842            x_settled: mac1_settled as i32,
843            tr: [trx as i32, try_ as i32, trz as i32],
844        }
845    }
846    #[cfg(not(target_arch = "mips"))]
847    {
848        let out = transform_vertex(v);
849        TransformProbe {
850            out,
851            x_settled: out.x,
852            tr: [
853                read_control!(5) as i32,
854                read_control!(6) as i32,
855                read_control!(7) as i32,
856            ],
857        }
858    }
859}
860
861/// Read the last three projected Z values and compute their average
862/// via AVSZ3 (weighted by ZSF3). Returns OTZ -- the depth key most
863/// renderers use for ordering-table inserts.
864pub fn average_z_triangle() -> u16 {
865    // SAFETY: no input registers to prepare -- AVSZ3 reads SZ1..SZ3
866    // which were populated by the most recent RTPT / project_triangle.
867    unsafe { ops::average_z3() };
868    read_data!(7) as u16
869}
870
871/// Reload three cached projected depths into the GTE SZ FIFO and compute OTZ.
872///
873/// This is the indexed-mesh counterpart to [`average_z_triangle`]. It keeps
874/// cached vertex projection while using the PS1's AVSZ3 unit instead of a
875/// software 64-bit multiply for every face. The configured ZSF3 weight is
876/// read from GTE control register 29.
877#[inline(always)]
878pub fn average_cached_z3(depths: [u16; 3]) -> u16 {
879    #[cfg(target_arch = "mips")]
880    {
881        let mut otz = depths[0] as u32;
882        // SAFETY: MTC2 $8..$10 into SZ1..SZ3 (data 17..19), two NOPs, AVSZ3,
883        // then MFC2 OTZ (data 7) into $8 plus a load-delay NOP. Only $8 is written
884        // (`inlateout`); $9/$10 are read-only inputs; no memory or stack is
885        // touched.
886        unsafe {
887            asm!(
888                // Load SZ1..SZ3, then leave the hardware-safe two-slot MTC2
889                // commit gap before AVSZ3 consumes the final write.
890                ".word {w0}",
891                ".word {w1}",
892                ".word {w2}",
893                ".word 0",
894                ".word 0",
895                ".word {w3}",
896                // MFC2 has one CPU load-delay slot.
897                ".word {w4}",
898                ".word 0",
899                w0 = const psx_hw::gte::mtc2(8, 17),
900                w1 = const psx_hw::gte::mtc2(9, 18),
901                w2 = const psx_hw::gte::mtc2(10, 19),
902                w3 = const psx_hw::gte::command::AVSZ3,
903                w4 = const psx_hw::gte::mfc2(8, 7),
904                inlateout("$8") otz,
905                in("$9") depths[1] as u32,
906                in("$10") depths[2] as u32,
907                options(nostack, nomem, preserves_flags),
908            );
909        }
910        otz as u16
911    }
912    #[cfg(not(target_arch = "mips"))]
913    {
914        write_data!(17, depths[0] as u32);
915        write_data!(18, depths[1] as u32);
916        write_data!(19, depths[2] as u32);
917        // SAFETY: SZ1, SZ2 and SZ3 were loaded immediately above.
918        unsafe { ops::average_z3() };
919        read_data!(7) as u16
920    }
921}
922
923/// Scale a three-depth sum into the classic 2,048-slot OTZ domain.
924///
925/// This is exactly `(sum * 0x155) >> 12`. The MIPS sequence factors 0x155 as
926/// `5 * 17 * 4 + 1`, avoiding two shifts and additions emitted for the flat
927/// constant multiply on MIPS I.
928#[inline(always)]
929#[doc(alias = "OTZ")]
930pub fn classic_ordering_depth3_from_sum(sum: u32) -> u16 {
931    #[cfg(target_arch = "mips")]
932    {
933        let mut otz = sum;
934        // SAFETY: pure ALU shift-add on $8..$10. $8 is `inlateout` and the
935        // $9/$10 scratch registers are declared `lateout(_)`; no memory or stack
936        // is touched and `addu`/`sll`/`srl` never trap.
937        unsafe {
938            asm!(
939                "sll $9, $8, 2",
940                "addu $9, $9, $8",
941                "sll $10, $9, 4",
942                "addu $9, $10, $9",
943                "sll $9, $9, 2",
944                "addu $9, $9, $8",
945                "srl $8, $9, 12",
946                inlateout("$8") otz,
947                lateout("$9") _,
948                lateout("$10") _,
949                options(nostack, nomem, preserves_flags),
950            );
951        }
952        otz as u16
953    }
954    #[cfg(not(target_arch = "mips"))]
955    {
956        ((sum * 0x155) >> 12) as u16
957    }
958}
959
960/// Reload four cached projected depths into the GTE SZ FIFO and compute OTZ.
961///
962/// This is the AVSZ4 counterpart to [`average_cached_z3`]. The configured
963/// ZSF4 weight is read from GTE control register 30.
964#[inline(always)]
965pub fn average_cached_z4(depths: [u16; 4]) -> u16 {
966    #[cfg(target_arch = "mips")]
967    {
968        let mut otz = depths[0] as u32;
969        // SAFETY: MTC2 $8..$11 into SZ0..SZ3 (data 16..19), two NOPs, AVSZ4,
970        // then MFC2 OTZ (data 7) into $8 plus a load-delay NOP. Only $8 is written
971        // (`inlateout`); $9..$11 are read-only inputs; no memory or stack is
972        // touched.
973        unsafe {
974            asm!(
975                // Load SZ0..SZ3, then leave the hardware-safe two-slot MTC2
976                // commit gap before AVSZ4 consumes the final write.
977                ".word {w0}",
978                ".word {w1}",
979                ".word {w2}",
980                ".word {w3}",
981                ".word 0",
982                ".word 0",
983                ".word {w4}",
984                // MFC2 has one CPU load-delay slot.
985                ".word {w5}",
986                ".word 0",
987                w0 = const psx_hw::gte::mtc2(8, 16),
988                w1 = const psx_hw::gte::mtc2(9, 17),
989                w2 = const psx_hw::gte::mtc2(10, 18),
990                w3 = const psx_hw::gte::mtc2(11, 19),
991                w4 = const psx_hw::gte::command::AVSZ4,
992                w5 = const psx_hw::gte::mfc2(8, 7),
993                inlateout("$8") otz,
994                in("$9") depths[1] as u32,
995                in("$10") depths[2] as u32,
996                in("$11") depths[3] as u32,
997                options(nostack, nomem, preserves_flags),
998            );
999        }
1000        otz as u16
1001    }
1002    #[cfg(not(target_arch = "mips"))]
1003    {
1004        write_data!(16, depths[0] as u32);
1005        write_data!(17, depths[1] as u32);
1006        write_data!(18, depths[2] as u32);
1007        write_data!(19, depths[3] as u32);
1008        // SAFETY: SZ0 through SZ3 were loaded immediately above.
1009        unsafe { ops::average_z4() };
1010        read_data!(7) as u16
1011    }
1012}
1013
1014/// Compute AVSZ3's saturated OTZ result from three cached projected depths.
1015///
1016/// This pure-software form is useful for host processing and for callers that
1017/// cannot disturb the live GTE FIFO. MIPS render loops should normally prefer
1018/// [`average_cached_z3`]. The arithmetic is exactly the GTE operation: sum the
1019/// unsigned 16-bit depths, multiply by signed `ZSF3`, shift right by 12, then
1020/// saturate to OTZ's unsigned 16-bit range.
1021#[inline]
1022#[doc(alias = "OTZ")]
1023#[doc(alias = "AverageZ3")]
1024pub fn average_z3_ordering_depth(depths: [u16; 3], zsf3: i16) -> u16 {
1025    // ZSF3 is i16 and each depth is u16, so the product reaches ~2^41: the
1026    // hardware AVSZ3 accumulates in the GTE's own wide register and this is
1027    // the software mirror of it. An i32 accumulator would wrap and hand the
1028    // ordering table a near OTZ for far geometry.
1029    // psx-numeric-allow-next-line: AVSZ3 wide accumulator, see above
1030    let sum = depths[0] as i64 + depths[1] as i64 + depths[2] as i64;
1031    // psx-numeric-allow-next-line: AVSZ3 wide accumulator
1032    ((sum * zsf3 as i64) >> 12).clamp(0, u16::MAX as i64) as u16
1033}
1034
1035/// Compute AVSZ4's saturated OTZ result from four cached projected depths.
1036///
1037/// This is the four-vertex counterpart to [`average_z3_ordering_depth`] and matches the
1038/// GTE's `AVSZ4` operation with the supplied `ZSF4` value. MIPS render loops
1039/// should normally prefer [`average_cached_z4`].
1040#[inline]
1041#[doc(alias = "OTZ")]
1042#[doc(alias = "AverageZ4")]
1043pub fn average_z4_ordering_depth(depths: [u16; 4], zsf4: i16) -> u16 {
1044    // psx-numeric-allow-next-line: AVSZ4 wide accumulator, same reasoning as average_z3_ordering_depth
1045    let sum = depths[0] as i64 + depths[1] as i64 + depths[2] as i64 + depths[3] as i64;
1046    // psx-numeric-allow-next-line: AVSZ4 wide accumulator
1047    ((sum * zsf4 as i64) >> 12).clamp(0, u16::MAX as i64) as u16
1048}
1049
1050#[inline(always)]
1051fn aabb_outer_support(mins: [i16; 3], maxs: [i16; 3], signbits: u8) -> Vec3I16 {
1052    Vec3I16::new(
1053        if signbits & 1 != 0 { mins[0] } else { maxs[0] },
1054        if signbits & 2 != 0 { mins[1] } else { maxs[1] },
1055        if signbits & 4 != 0 { mins[2] } else { maxs[2] },
1056    )
1057}
1058
1059/// `MVMVA` of V0 by the rotation matrix, no translation, `sf` clear.
1060#[cfg(target_arch = "mips")]
1061const RT_V0_NO_TRANSLATION: u32 = psx_hw::gte::mvmva(
1062    psx_hw::gte::Matrix::Rotation,
1063    psx_hw::gte::Vector::V0,
1064    psx_hw::gte::Translation::None,
1065);
1066
1067/// `MVMVA` of V0 by the light matrix, no translation, `sf` clear.
1068#[cfg(target_arch = "mips")]
1069const LLM_V0_NO_TRANSLATION: u32 = psx_hw::gte::mvmva(
1070    psx_hw::gte::Matrix::Light,
1071    psx_hw::gte::Vector::V0,
1072    psx_hw::gte::Translation::None,
1073);
1074
1075/// `MFC2 $8, MAC1 + n`.
1076#[cfg(target_arch = "mips")]
1077const fn read_mac(n: u32) -> u32 {
1078    psx_hw::gte::mfc2(8, 25 + n)
1079}
1080
1081#[cfg(target_arch = "mips")]
1082#[inline(always)]
1083fn aabb_dot_mvmva<const OP: u32, const READ_MAC: u32>(v: Vec3I16) -> i32 {
1084    let mut dot = v.xy_packed();
1085    // SAFETY: MTC2 $8/$9 into V0, two NOPs, then `OP` and `READ_MAC`. The
1086    // only instantiations (`aabb_clip_dot`) pass MVMVA encodings (`0x4a00_6012`
1087    // RT, `0x4a02_6012` LLM; cv=none, sf=0) and MFC2 of MAC1..MAC3 into $8,
1088    // so only $8 is written (`inlateout`) and no memory or stack is touched.
1089    // This private fn trusts its const arguments; any new caller must keep
1090    // `READ_MAC` writing $8 only.
1091    unsafe {
1092        asm!(
1093            ".word {w0}",
1094            ".word {w1}",
1095            // Same console-confirmed V0 commit distance as
1096            // transform_vertex_mips.
1097            ".word 0",
1098            ".word 0",
1099            ".word {op}",
1100            ".word {read_mac}",
1101            ".word 0",
1102            w0 = const psx_hw::gte::mtc2(8, 0),
1103            w1 = const psx_hw::gte::mtc2(9, 1),
1104            op = const OP,
1105            read_mac = const READ_MAC,
1106            inlateout("$8") dot,
1107            in("$9") v.z_packed(),
1108            options(nostack, nomem, preserves_flags),
1109        );
1110    }
1111    dot as i32
1112}
1113
1114#[inline(always)]
1115fn aabb_clip_dot(v: Vec3I16, _plane: &AabbClipPlane, index: usize) -> i32 {
1116    #[cfg(target_arch = "mips")]
1117    {
1118        match index {
1119            0 => aabb_dot_mvmva::<{ RT_V0_NO_TRANSLATION }, { read_mac(0) }>(v),
1120            1 => aabb_dot_mvmva::<{ RT_V0_NO_TRANSLATION }, { read_mac(1) }>(v),
1121            2 => aabb_dot_mvmva::<{ RT_V0_NO_TRANSLATION }, { read_mac(2) }>(v),
1122            3 => aabb_dot_mvmva::<{ LLM_V0_NO_TRANSLATION }, { read_mac(0) }>(v),
1123            _ => 0,
1124        }
1125    }
1126    #[cfg(not(target_arch = "mips"))]
1127    {
1128        let _ = index;
1129        _plane.normal[0] as i32 * v.x as i32
1130            + _plane.normal[1] as i32 * v.y as i32
1131            + _plane.normal[2] as i32 * v.z as i32
1132    }
1133}
1134
1135/// Classify a signed-integer AABB against four planes loaded by
1136/// [`load_aabb_clip4`].
1137///
1138/// `clip_flags` selects active planes in bits zero through three. Returns
1139/// `-1` when the box is fully outside any active plane; otherwise returns the
1140/// remaining intersecting-plane flags after fully-inside planes are cleared.
1141/// The GTE path computes the same unshifted signed Q12 dot products as six
1142/// scalar MIPS multiplications per active plane.
1143#[inline]
1144pub fn classify_aabb_clip4(
1145    mins: [i16; 3],
1146    maxs: [i16; 3],
1147    planes: &[AabbClipPlane; 4],
1148    mut clip_flags: u8,
1149) -> i32 {
1150    let mut index = 0usize;
1151    while index < 4 {
1152        let flag = 1u8 << index;
1153        if clip_flags & flag != 0 {
1154            let plane = &planes[index];
1155            let outer = aabb_outer_support(mins, maxs, plane.signbits);
1156            if aabb_clip_dot(outer, plane, index) < plane.distance {
1157                return -1;
1158            }
1159            let inner = aabb_outer_support(maxs, mins, plane.signbits);
1160            if aabb_clip_dot(inner, plane, index) >= plane.distance {
1161                clip_flags &= !flag;
1162            }
1163        }
1164        index += 1;
1165    }
1166    clip_flags as i32
1167}
1168
1169/// Return whether a signed-integer AABB is outside any selected plane loaded
1170/// by [`load_aabb_clip4`].
1171///
1172/// Always inlined: renderers call this once per candidate face inside their
1173/// selection loop, and as an out-of-line call it spent a third of its cycles
1174/// on the call itself (two arrays by value plus the plane pointer).
1175#[inline(always)]
1176pub fn is_aabb_outside_clip4(
1177    mins: [i16; 3],
1178    maxs: [i16; 3],
1179    planes: &[AabbClipPlane; 4],
1180    clip_flags: u8,
1181) -> bool {
1182    let mut index = 0usize;
1183    while index < 4 {
1184        let flag = 1u8 << index;
1185        if clip_flags & flag != 0 {
1186            let plane = &planes[index];
1187            let outer = aabb_outer_support(mins, maxs, plane.signbits);
1188            if aabb_clip_dot(outer, plane, index) < plane.distance {
1189                return true;
1190            }
1191        }
1192        index += 1;
1193    }
1194    false
1195}
1196
1197/// Signed screen-space area of a triangle -- the same value GTE `NCLIP`
1198/// writes to MAC0: `SX0*(SY1-SY2) + SX1*(SY2-SY0) + SX2*(SY0-SY1)`.
1199/// Positive = front-facing, `<= 0` = back-facing/degenerate.
1200///
1201/// Computed in software rather than via `NCLIP` on purpose: reading MAC0
1202/// immediately after `NCLIP` returns a STALE value on real PS1 hardware
1203/// (the GTE result-read hazard -- MAC0 settles a few cycles later than the
1204/// CPU reads it), which mis-culls and drops wall faces on silicon while
1205/// looking fine on every emulator. The i32 cross product is exact for
1206/// screen coordinates (|coord| <= 0x400 after clamping) and has no read
1207/// latency. Confirmed against real hardware (cortex GTE disc 2026-06-09:
1208/// NCLIP MAC0 back-to-back read is stale, +8 nops reads correct).
1209#[inline]
1210#[doc(alias = "NCLIP")]
1211#[doc(alias = "MAC0")]
1212pub fn screen_area(vertices: [(i16, i16); 3]) -> i32 {
1213    let (sx0, sy0) = (vertices[0].0 as i32, vertices[0].1 as i32);
1214    let (sx1, sy1) = (vertices[1].0 as i32, vertices[1].1 as i32);
1215    let (sx2, sy2) = (vertices[2].0 as i32, vertices[2].1 as i32);
1216    // Algebraically identical to the three-product NCLIP expansion above,
1217    // but expressed as one 2D cross product. On MIPS-I this removes one MULT
1218    // from every cached-room and model-face backface test.
1219    (sx1 - sx0) * (sy2 - sy0) - (sy1 - sy0) * (sx2 - sx0)
1220}
1221
1222/// Run a hardware-safe `NCLIP` for an already-projected triangle.
1223///
1224/// The two input NOPs and eight-instruction result distance are both required
1225/// by the console measurements documented on [`screen_area`]. This is
1226/// useful in tight indexed-model loops where the GTE would otherwise be idle.
1227#[inline(always)]
1228#[doc(alias = "NCLIP")]
1229#[doc(alias = "MAC0")]
1230pub fn screen_area_scheduled(vertices: [(i16, i16); 3]) -> i32 {
1231    #[cfg(target_arch = "mips")]
1232    {
1233        let sxy0 = pack_xy(vertices[0].0, vertices[0].1);
1234        let sxy1 = pack_xy(vertices[1].0, vertices[1].1);
1235        let mut area = pack_xy(vertices[2].0, vertices[2].1);
1236        // SAFETY: MTC2 $8..$10 into SXY0..SXY2 (data 12..14), two NOPs, NCLIP,
1237        // eight NOPs, then MFC2 MAC0 (data 24) into $10 plus a load-delay NOP.
1238        // Only $10 is written (`inlateout`); no memory or stack is touched.
1239        unsafe {
1240            asm!(
1241                // MTC2 $8/$9/$10,SXY0/SXY1/SXY2.
1242                ".word {w0}",
1243                ".word {w1}",
1244                ".word {w2}",
1245                // HWB-010/011 input-commit gap before NCLIP consumes SXY2.
1246                ".word 0",
1247                ".word 0",
1248                // NCLIP plus its hardware-confirmed MAC0 result gap.
1249                ".word {w3}",
1250                ".word 0",
1251                ".word 0",
1252                ".word 0",
1253                ".word 0",
1254                ".word 0",
1255                ".word 0",
1256                ".word 0",
1257                ".word 0",
1258                // MFC2 $10,MAC0 plus its CPU load-delay slot.
1259                ".word {w4}",
1260                ".word 0",
1261                w0 = const psx_hw::gte::mtc2(8, 12),
1262                w1 = const psx_hw::gte::mtc2(9, 13),
1263                w2 = const psx_hw::gte::mtc2(10, 14),
1264                w3 = const psx_hw::gte::command::NCLIP,
1265                w4 = const psx_hw::gte::mfc2(10, 24),
1266                in("$8") sxy0,
1267                in("$9") sxy1,
1268                inlateout("$10") area,
1269                options(nostack, nomem, preserves_flags),
1270            );
1271        }
1272        area as i32
1273    }
1274    #[cfg(not(target_arch = "mips"))]
1275    {
1276        screen_area(vertices)
1277    }
1278}
1279
1280/// Run hardware-safe `NCLIP` while unpacking one aligned model-face record.
1281///
1282/// Runtime model faces already carry three `vertex | uv << 16` corner words,
1283/// with a two-bit palette selector in bits 14..15 of the first vertex word.
1284/// The five register-only unpack instructions occupy five of NCLIP's eight
1285/// mandatory MAC0 result slots. No GTE input or result hazard is shortened.
1286#[inline(always)]
1287pub fn screen_area_and_unpack_model_face_scheduled(
1288    vertices: [(i16, i16); 3],
1289    corner_words: [u32; 3],
1290) -> (i32, [u16; 3], u8) {
1291    #[cfg(target_arch = "mips")]
1292    {
1293        let sxy0 = pack_xy(vertices[0].0, vertices[0].1);
1294        let sxy1 = pack_xy(vertices[1].0, vertices[1].1);
1295        let mut area = pack_xy(vertices[2].0, vertices[2].1);
1296        let mut uv0 = corner_words[0];
1297        let mut uv1 = corner_words[1];
1298        let mut uv2 = corner_words[2];
1299        let palette_bank: u32;
1300        // SAFETY: MTC2 $8..$10 into SXY0..SXY2, NCLIP, register-only `srl`/`andi`
1301        // on $11..$14, then MFC2 MAC0 into $10. The written registers ($10..$14)
1302        // are all declared `inlateout`/`lateout`; no memory or stack is touched.
1303        unsafe {
1304            asm!(
1305                // MTC2 SXY0..SXY2 and the hardware-confirmed input gap.
1306                ".word {w0}",
1307                ".word {w1}",
1308                ".word {w2}",
1309                ".word 0",
1310                ".word 0",
1311                ".word {w3}",
1312                // Register-only face unpacking inside NCLIP's complete
1313                // eight-instruction MAC0 result gap.
1314                "srl $14, $11, 14",
1315                "andi $14, $14, 3",
1316                "srl $11, $11, 16",
1317                "srl $12, $12, 16",
1318                "srl $13, $13, 16",
1319                ".word 0",
1320                ".word 0",
1321                ".word 0",
1322                // MFC2 MAC0 plus the CPU load-delay slot.
1323                ".word {w4}",
1324                ".word 0",
1325                w0 = const psx_hw::gte::mtc2(8, 12),
1326                w1 = const psx_hw::gte::mtc2(9, 13),
1327                w2 = const psx_hw::gte::mtc2(10, 14),
1328                w3 = const psx_hw::gte::command::NCLIP,
1329                w4 = const psx_hw::gte::mfc2(10, 24),
1330                in("$8") sxy0,
1331                in("$9") sxy1,
1332                inlateout("$10") area,
1333                inlateout("$11") uv0,
1334                inlateout("$12") uv1,
1335                inlateout("$13") uv2,
1336                lateout("$14") palette_bank,
1337                options(nostack, nomem, preserves_flags),
1338            );
1339        }
1340        (
1341            area as i32,
1342            [uv0 as u16, uv1 as u16, uv2 as u16],
1343            palette_bank as u8,
1344        )
1345    }
1346    #[cfg(not(target_arch = "mips"))]
1347    {
1348        (
1349            screen_area(vertices),
1350            [
1351                (corner_words[0] >> 16) as u16,
1352                (corner_words[1] >> 16) as u16,
1353                (corner_words[2] >> 16) as u16,
1354            ],
1355            ((corner_words[0] >> 14) & 3) as u8,
1356        )
1357    }
1358}
1359
1360/// Run hardware-safe `NCLIP` and `AVSZ3` for an already-projected indexed
1361/// triangle.
1362///
1363/// The cached screen coordinates and depths are loaded together. The three
1364/// SZ writes occupy part of the silicon-required NCLIP result gap, so callers
1365/// that need both winding and an OT key avoid paying two independent GTE
1366/// schedules. The returned area is identical to [`screen_area`], and the
1367/// depth is identical to [`average_cached_z3`] with the installed ZSF3.
1368#[inline(always)]
1369pub fn screen_area_and_average_cached_z3_scheduled(
1370    vertices: [(i16, i16); 3],
1371    depths: [u16; 3],
1372) -> (i32, u16) {
1373    #[cfg(target_arch = "mips")]
1374    {
1375        let sxy0 = pack_xy(vertices[0].0, vertices[0].1);
1376        let sxy1 = pack_xy(vertices[1].0, vertices[1].1);
1377        let mut area = pack_xy(vertices[2].0, vertices[2].1);
1378        let mut otz = depths[0] as u32;
1379        // SAFETY: MTC2 $8..$10 into SXY0..SXY2, NCLIP, MTC2 $11..$13 into
1380        // SZ1..SZ3, then MFC2 MAC0 into $10, AVSZ3, and MFC2 OTZ into $11 with a
1381        // final load-delay NOP. Only $10 and $11 are written (`inlateout`);
1382        // $12/$13 are read-only; no memory or stack is touched.
1383        unsafe {
1384            asm!(
1385                // Load SXY0..SXY2 and leave the measured input-commit gap.
1386                ".word {w0}",
1387                ".word {w1}",
1388                ".word {w2}",
1389                ".word 0",
1390                ".word 0",
1391                ".word {w3}",
1392                // Loading SZ1..SZ3 is independent work inside NCLIP's
1393                // hardware-confirmed eight-instruction MAC0 result gap.
1394                ".word {w4}",
1395                ".word {w5}",
1396                ".word {w6}",
1397                ".word 0",
1398                ".word 0",
1399                ".word 0",
1400                ".word 0",
1401                ".word 0",
1402                // Capture NCLIP's MAC0 before AVSZ3 overwrites it. AVSZ3 is
1403                // independent of the CPU load-delay result.
1404                ".word {w7}",
1405                ".word {w8}",
1406                ".word {w9}",
1407                ".word 0",
1408                w0 = const psx_hw::gte::mtc2(8, 12),
1409                w1 = const psx_hw::gte::mtc2(9, 13),
1410                w2 = const psx_hw::gte::mtc2(10, 14),
1411                w3 = const psx_hw::gte::command::NCLIP,
1412                w4 = const psx_hw::gte::mtc2(11, 17),
1413                w5 = const psx_hw::gte::mtc2(12, 18),
1414                w6 = const psx_hw::gte::mtc2(13, 19),
1415                w7 = const psx_hw::gte::mfc2(10, 24),
1416                w8 = const psx_hw::gte::command::AVSZ3,
1417                w9 = const psx_hw::gte::mfc2(11, 7),
1418                in("$8") sxy0,
1419                in("$9") sxy1,
1420                inlateout("$10") area,
1421                inlateout("$11") otz,
1422                in("$12") depths[1] as u32,
1423                in("$13") depths[2] as u32,
1424                options(nostack, nomem, preserves_flags),
1425            );
1426        }
1427        (area as i32, otz as u16)
1428    }
1429    #[cfg(not(target_arch = "mips"))]
1430    {
1431        (screen_area(vertices), average_cached_z3(depths))
1432    }
1433}
1434
1435/// Run hardware-safe `NCLIP` and compute the classic 0x155-scaled OTZ from
1436/// three cached depths without disturbing the GTE depth FIFO.
1437///
1438/// `ZSF3 = 0x155` is the historical 2,048-slot ordering-table scale used by
1439/// the classic affine path. The CPU shift-add sequence is placed entirely in
1440/// NCLIP's required MAC0 result gap, so it replaces the later AVSZ3 command
1441/// without extending the hazard schedule.
1442#[inline(always)]
1443#[doc(alias = "OTZ")]
1444pub fn screen_area_and_classic_ordering_depth3_scheduled(
1445    vertices: [(i16, i16); 3],
1446    depths: [u16; 3],
1447) -> (i32, u16) {
1448    #[cfg(target_arch = "mips")]
1449    {
1450        let sxy0 = pack_xy(vertices[0].0, vertices[0].1);
1451        let sxy1 = pack_xy(vertices[1].0, vertices[1].1);
1452        let mut area = pack_xy(vertices[2].0, vertices[2].1);
1453        let mut otz = depths[0] as u32;
1454        let depth1 = depths[1] as u32;
1455        let depth2 = depths[2] as u32;
1456        // SAFETY: MTC2 $8..$10 into SXY0..SXY2, NCLIP, ALU shift-add on
1457        // $11..$13, MFC2 MAC0 into $10 and a final `srl` into $11. Every written
1458        // register ($10..$13) is declared `inlateout` (scratch outputs discarded);
1459        // no memory or stack is touched and the ALU ops never trap.
1460        unsafe {
1461            asm!(
1462                // Load SXY0..SXY2 and leave the measured input-commit gap.
1463                ".word {w0}",
1464                ".word {w1}",
1465                ".word {w2}",
1466                ".word 0",
1467                ".word 0",
1468                // NCLIP.
1469                ".word {w3}",
1470                // Fill NCLIP's eight-instruction MAC0 result gap with the
1471                // exact sum * 0x155 sequence: 5x, 85x, then 341x.
1472                "addu $11, $11, $12",
1473                "addu $11, $11, $13",
1474                "sll $12, $11, 2",
1475                "addu $12, $12, $11",
1476                "sll $13, $12, 4",
1477                "addu $12, $13, $12",
1478                "sll $12, $12, 2",
1479                "addu $12, $12, $11",
1480                // Read MAC0 and use its CPU load-delay slot for the OTZ
1481                // scale. Neither instruction depends on the other's result.
1482                ".word {w4}",
1483                "srl $11, $12, 12",
1484                w0 = const psx_hw::gte::mtc2(8, 12),
1485                w1 = const psx_hw::gte::mtc2(9, 13),
1486                w2 = const psx_hw::gte::mtc2(10, 14),
1487                w3 = const psx_hw::gte::command::NCLIP,
1488                w4 = const psx_hw::gte::mfc2(10, 24),
1489                in("$8") sxy0,
1490                in("$9") sxy1,
1491                inlateout("$10") area,
1492                inlateout("$11") otz,
1493                inlateout("$12") depth1 => _,
1494                inlateout("$13") depth2 => _,
1495                options(nostack, nomem, preserves_flags),
1496            );
1497        }
1498        (area as i32, otz as u16)
1499    }
1500    #[cfg(not(target_arch = "mips"))]
1501    {
1502        let sum = depths[0] as u32 + depths[1] as u32 + depths[2] as u32;
1503        (screen_area(vertices), ((sum * 0x155) >> 12) as u16)
1504    }
1505}
1506
1507/// Back-face test for three already-projected screen-space vertices.
1508///
1509/// Useful when a renderer cached/projected vertices first and later wants
1510/// the signed screen-space area test for arbitrary indexed faces. Uses the
1511/// software [`screen_area`] (see its note on the NCLIP MAC0 hazard).
1512pub fn is_screen_triangle_back_facing(vertices: [(i16, i16); 3]) -> bool {
1513    screen_area(vertices) <= 0
1514}
1515
1516/// Read the GTE FLAG register. Non-zero indicates at least one error
1517/// bit fired during the last op (overflow, saturation, divide
1518/// overflow). Useful for debug prints on a frame that looks wrong.
1519#[doc(alias = "FLAG")]
1520pub fn error_flags() -> u32 {
1521    read_control!(31)
1522}
1523
1524/// Renamed to [`start_project_triple`].
1525#[deprecated(note = "renamed to `start_project_triple`")]
1526#[inline(always)]
1527pub fn rtpt_kick(v0: Vec3I16, v1: Vec3I16, v2: Vec3I16) -> ProjectTripleInFlight {
1528    start_project_triple(v0, v1, v2)
1529}
1530
1531/// Renamed to [`classic_ordering_depth3_from_sum`].
1532#[deprecated(note = "renamed to `classic_ordering_depth3_from_sum`")]
1533#[inline(always)]
1534pub fn classic_otz3_from_sum(sum: u32) -> u16 {
1535    classic_ordering_depth3_from_sum(sum)
1536}
1537
1538/// Renamed to [`is_aabb_outside_clip4`].
1539#[deprecated(note = "renamed to `is_aabb_outside_clip4`")]
1540#[inline(always)]
1541pub fn aabb_outside_clip4(
1542    mins: [i16; 3],
1543    maxs: [i16; 3],
1544    planes: &[AabbClipPlane; 4],
1545    clip_flags: u8,
1546) -> bool {
1547    is_aabb_outside_clip4(mins, maxs, planes, clip_flags)
1548}
1549
1550/// Renamed to [`screen_area_scheduled`].
1551#[deprecated(note = "renamed to `screen_area_scheduled`")]
1552#[inline(always)]
1553pub fn screen_area_mac0_scheduled(vertices: [(i16, i16); 3]) -> i32 {
1554    screen_area_scheduled(vertices)
1555}
1556
1557#[cfg(all(test, not(target_arch = "mips")))]
1558mod host_smoke {
1559    //! Smoke tests for the host-side software-GTE shim.
1560    //!
1561    //! On hardware these helpers compile to inline COP2 instructions,
1562    //! so testing them via Rust integration would require running on
1563    //! a PS1. On host they route through the per-thread Gte from
1564    //! `psx-gte-core`, which we *can* poke at directly to confirm the
1565    //! routing produces matching output.
1566    use super::*;
1567    use crate::host;
1568
1569    fn install_identity() {
1570        load_rotation(&Mat3I16::IDENTITY);
1571        load_translation(Vec3I32::ZERO);
1572        set_screen_offset(160 << 16, 120 << 16);
1573        set_projection_plane(200);
1574    }
1575
1576    #[test]
1577    fn rtps_through_host_shim_projects_an_in_front_vertex() {
1578        host::reset();
1579        install_identity();
1580        // V0 = (0, 0, 1024) -- straight ahead, depth 1024. With H=200
1581        // the GTE divides 200/sz3 (≈0x4000/sz3 internally), giving an
1582        // X/Y near the screen offset for a vertex at the origin.
1583        let projected = project_vertex(Vec3I16::new(0, 0, 1024));
1584        assert_eq!(projected.sx, 160);
1585        assert_eq!(projected.sy, 120);
1586        assert!(
1587            projected.sz > 0,
1588            "near-plane vertex must yield non-zero depth"
1589        );
1590    }
1591
1592    #[test]
1593    fn rtpt_through_host_shim_matches_three_separate_rtps_calls() {
1594        host::reset();
1595        install_identity();
1596        let a = Vec3I16::new(-256, 0, 1024);
1597        let b = Vec3I16::new(256, 0, 1024);
1598        let c = Vec3I16::new(0, 256, 1024);
1599
1600        let batch = project_triangle(a, b, c);
1601
1602        host::reset();
1603        install_identity();
1604        let p_a = project_vertex(a);
1605        let p_b = project_vertex(b);
1606        let p_c = project_vertex(c);
1607
1608        assert_eq!(batch[0], p_a);
1609        assert_eq!(batch[1], p_b);
1610        assert_eq!(batch[2], p_c);
1611    }
1612
1613    #[test]
1614    fn mvmva_transform_through_host_shim_applies_rt_and_tr() {
1615        host::reset();
1616        load_rotation(&Mat3I16::IDENTITY);
1617        load_translation(Vec3I32::new(10, -20, 30));
1618
1619        let transformed = transform_vertex(Vec3I16::new(100, 200, 300));
1620
1621        assert_eq!(transformed, Vec3I32::new(110, 180, 330));
1622    }
1623
1624    #[test]
1625    fn scheduled_rotation_compose_matches_cpu_matrix_product() {
1626        host::reset();
1627        let left = Mat3I16::rotate_z(37).mul(&Mat3I16::rotate_y(91));
1628        let right = Mat3I16::rotate_y(13)
1629            .mul(&Mat3I16::rotate_x(55))
1630            .scale_columns_q12([3072, 4096, 5120]);
1631
1632        assert_eq!(compose_rotation_scheduled(&left, &right), left.mul(&right));
1633    }
1634
1635    #[test]
1636    fn classic_normalize_matches_reference_table_results() {
1637        for (input, vector, xy_squared, squared) in [
1638            ([1, 0, 0], [4096, 0, 0], 1, 1),
1639            ([3, 4, 0], [2457, 3276, 0], 25, 25),
1640            ([10, 20, 30], [1097, 2195, 3293], 500, 1400),
1641            ([-10, 20, 30], [-1098, 2195, 3293], 500, 1400),
1642            ([100, 50, -20], [3610, 1805, -723], 12_500, 12_900),
1643            ([600, 0, 200], [3898, 0, 1299], 360_000, 400_000),
1644        ] {
1645            host::reset();
1646            let result = normalize_classic_q12_scheduled(Vec3I32::new(
1647                input[0] << 12,
1648                input[1] << 12,
1649                input[2] << 12,
1650            ));
1651            assert_eq!(
1652                result,
1653                ClassicNormalizedVector {
1654                    vector: Vec3I16::new(vector[0], vector[1], vector[2]),
1655                    xy_squared,
1656                    squared,
1657                }
1658            );
1659        }
1660
1661        host::reset();
1662        assert_eq!(
1663            normalize_classic_q12_scheduled(Vec3I32::ZERO),
1664            ClassicNormalizedVector::default()
1665        );
1666    }
1667
1668    #[test]
1669    fn scheduled_nclip_matches_software_area_on_host() {
1670        let vertices = [(-320, 112), (47, -91), (511, 230)];
1671        assert_eq!(screen_area_scheduled(vertices), screen_area(vertices));
1672    }
1673
1674    #[test]
1675    fn scheduled_nclip_face_unpack_matches_packed_record() {
1676        let vertices = [(-320, 112), (47, -91), (511, 230)];
1677        let corners = [0xabcd_c123, 0x0123_4567, 0xfedc_89ab];
1678        assert_eq!(
1679            screen_area_and_unpack_model_face_scheduled(vertices, corners),
1680            (
1681                screen_area(vertices),
1682                [0xabcd, 0x0123, 0xfedc],
1683                ((0xc123 >> 14) & 3) as u8,
1684            ),
1685        );
1686    }
1687
1688    #[test]
1689    fn cached_average_z_matches_avsz_saturation_rules() {
1690        assert_eq!(average_z3_ordering_depth([100, 200, 300], 1_365), 199);
1691        assert_eq!(average_z4_ordering_depth([100, 200, 300, 400], 1_024), 250);
1692        assert_eq!(average_z3_ordering_depth([u16::MAX; 3], i16::MAX), u16::MAX);
1693        assert_eq!(average_z4_ordering_depth([u16::MAX; 4], -1), 0);
1694
1695        set_average_z_weights(1_365, 1_024);
1696        assert_eq!(average_cached_z3([100, 200, 300]), 199);
1697        assert_eq!(average_cached_z4([100, 200, 300, 400]), 250);
1698        assert_eq!(classic_ordering_depth3_from_sum(100 + 200 + 300), 49);
1699
1700        let vertices = [(-320, 112), (47, -91), (511, 230)];
1701        assert_eq!(
1702            screen_area_and_average_cached_z3_scheduled(vertices, [100, 200, 300]),
1703            (screen_area(vertices), 199),
1704        );
1705        assert_eq!(
1706            screen_area_and_classic_ordering_depth3_scheduled(vertices, [100, 200, 300]),
1707            (screen_area(vertices), 49),
1708        );
1709    }
1710
1711    #[test]
1712    fn four_plane_aabb_clip_matches_scalar_support_points() {
1713        let plane = |normal: [i16; 3], distance| AabbClipPlane {
1714            normal,
1715            kind: 0,
1716            signbits: (normal[0] < 0) as u8
1717                | (((normal[1] < 0) as u8) << 1)
1718                | (((normal[2] < 0) as u8) << 2),
1719            distance,
1720        };
1721        let planes = [
1722            plane([4096, 1024, -512], -3000),
1723            plane([-2048, 4096, 256], 7000),
1724            plane([300, -900, 4096], -12000),
1725            plane([-700, -500, -4096], 16000),
1726        ];
1727        load_aabb_clip4(&planes);
1728
1729        let scalar = |mins: [i16; 3], maxs: [i16; 3], mut flags: u8| {
1730            for (index, plane) in planes.iter().enumerate() {
1731                let flag = 1u8 << index;
1732                if flags & flag == 0 {
1733                    continue;
1734                }
1735                let outer = [
1736                    if plane.normal[0] < 0 {
1737                        mins[0]
1738                    } else {
1739                        maxs[0]
1740                    },
1741                    if plane.normal[1] < 0 {
1742                        mins[1]
1743                    } else {
1744                        maxs[1]
1745                    },
1746                    if plane.normal[2] < 0 {
1747                        mins[2]
1748                    } else {
1749                        maxs[2]
1750                    },
1751                ];
1752                let inner = [
1753                    if plane.normal[0] < 0 {
1754                        maxs[0]
1755                    } else {
1756                        mins[0]
1757                    },
1758                    if plane.normal[1] < 0 {
1759                        maxs[1]
1760                    } else {
1761                        mins[1]
1762                    },
1763                    if plane.normal[2] < 0 {
1764                        maxs[2]
1765                    } else {
1766                        mins[2]
1767                    },
1768                ];
1769                let dot = |point: [i16; 3]| {
1770                    plane.normal[0] as i32 * point[0] as i32
1771                        + plane.normal[1] as i32 * point[1] as i32
1772                        + plane.normal[2] as i32 * point[2] as i32
1773                };
1774                if dot(outer) < plane.distance {
1775                    return -1;
1776                }
1777                if dot(inner) >= plane.distance {
1778                    flags &= !flag;
1779                }
1780            }
1781            flags as i32
1782        };
1783
1784        for (mins, maxs, flags) in [
1785            ([-10, -20, -30], [40, 50, 60], 0x0f),
1786            ([100, 200, 300], [120, 240, 360], 0x0f),
1787            ([-32000, -100, 20], [-30000, 100, 80], 0x05),
1788        ] {
1789            let expected = scalar(mins, maxs, flags);
1790            assert_eq!(classify_aabb_clip4(mins, maxs, &planes, flags), expected);
1791            assert_eq!(
1792                is_aabb_outside_clip4(mins, maxs, &planes, flags),
1793                expected < 0,
1794            );
1795        }
1796        assert_eq!(core::mem::size_of::<AabbClipPlane>(), 12);
1797        assert_eq!(core::mem::align_of::<AabbClipPlane>(), 4);
1798    }
1799
1800    /// FNV-1a 64 of `normalize_classic_q12_scheduled` over 40,000 inputs
1801    /// spread across every magnitude, including ones whose squares wrap.
1802    /// Recorded from the implementation that came before the generated
1803    /// table; the rewrite has to land on the same digest.
1804    #[test]
1805    fn classic_normalize_matches_previous_results_digest() {
1806        let mut seed = 0x1234_5678u32;
1807        let mut next = move || {
1808            seed = seed.wrapping_mul(1_664_525).wrapping_add(1_013_904_223);
1809            seed
1810        };
1811        let mut hash = 0xCBF2_9CE4_8422_2325u64;
1812        let mut eat = |value: i32| {
1813            for byte in value.to_le_bytes() {
1814                hash ^= u64::from(byte);
1815                hash = hash.wrapping_mul(0x0000_0100_0000_01B3);
1816            }
1817        };
1818        for i in 0..40_000u32 {
1819            // Shift each component right by 0..=31 bits so small, medium,
1820            // large and wrapping magnitudes all show up.
1821            let mut component = || (next() as i32) >> (next() % 32);
1822            let input = Vec3I32::new(component(), component(), component());
1823            let input = if i % 7 == 0 {
1824                Vec3I32::new(input.x, 0, 0)
1825            } else {
1826                input
1827            };
1828            host::reset();
1829            let out = normalize_classic_q12_scheduled(input);
1830            eat(i32::from(out.vector.x));
1831            eat(i32::from(out.vector.y));
1832            eat(i32::from(out.vector.z));
1833            eat(out.xy_squared);
1834            eat(out.squared);
1835        }
1836        assert_eq!(hash, 0x803B_4A0D_7EA1_0CB5);
1837    }
1838
1839    /// The table the previous implementation used, captured by running it.
1840    const PREVIOUS_TABLE: [i16; 192] = [
1841        4096, 4064, 4033, 4003, 3973, 3944, 3916, 3888, 3861, 3835, 3809, 3783, 3758, 3734, 3710,
1842        3686, 3663, 3640, 3618, 3596, 3575, 3554, 3533, 3513, 3493, 3473, 3454, 3435, 3416, 3397,
1843        3379, 3361, 3344, 3327, 3310, 3293, 3276, 3260, 3244, 3228, 3213, 3197, 3182, 3167, 3153,
1844        3138, 3124, 3110, 3096, 3082, 3069, 3055, 3042, 3029, 3016, 3003, 2991, 2978, 2966, 2954,
1845        2942, 2930, 2919, 2907, 2896, 2885, 2873, 2862, 2852, 2841, 2830, 2820, 2809, 2799, 2789,
1846        2779, 2769, 2759, 2749, 2740, 2730, 2721, 2711, 2702, 2693, 2684, 2675, 2666, 2657, 2649,
1847        2640, 2631, 2623, 2615, 2606, 2598, 2590, 2582, 2574, 2566, 2558, 2550, 2543, 2535, 2528,
1848        2520, 2513, 2505, 2498, 2491, 2484, 2477, 2469, 2462, 2456, 2449, 2442, 2435, 2428, 2422,
1849        2415, 2409, 2402, 2396, 2389, 2383, 2377, 2371, 2364, 2358, 2352, 2346, 2340, 2334, 2328,
1850        2322, 2317, 2311, 2305, 2299, 2294, 2288, 2283, 2277, 2272, 2266, 2261, 2255, 2250, 2245,
1851        2239, 2234, 2229, 2224, 2219, 2214, 2209, 2204, 2199, 2194, 2189, 2184, 2179, 2174, 2170,
1852        2165, 2160, 2155, 2151, 2146, 2142, 2137, 2133, 2128, 2124, 2119, 2115, 2110, 2106, 2102,
1853        2097, 2093, 2089, 2084, 2080, 2076, 2072, 2068, 2064, 2060, 2056, 2052,
1854    ];
1855
1856    #[test]
1857    fn generated_rsqrt_table_matches_the_previous_table() {
1858        assert_eq!(RSQRT_Q12_FROM_64, PREVIOUS_TABLE);
1859    }
1860}