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

psx_gte_core/
state.rs

1//! GTE -- Geometry Transformation Engine (COP2).
2//!
3//! 32 data registers + 32 control registers, all in fixed-point.
4//! Vertices and matrix elements are signed 1.3.12 (`i16` × 1/4096);
5//! translations and color-bias terms are signed 31.0; intermediate
6//! accumulation runs in 64-bit signed with a 44-bit overflow check
7//! before truncation to 32-bit `MAC1..3`.
8//!
9//! Each function call clears `FLAG`, runs its math (which may set the
10//! per-result saturation/overflow bits), then folds the error bits
11//! into the master `FLAG[31]`. Software polls `FLAG` to detect
12//! geometry that would otherwise wrap or clip incorrectly.
13//!
14//! The division used by `RTPS`/`RTPT` is the documented unsigned
15//! Newton-Raphson iteration off a 257-entry seed table -- keeping the
16//! exact PSX algorithm matters because games consume the resulting
17//! `SX2`/`SY2` values directly and any drift moves vertices on screen.
18//!
19//! Reference: nocash PSX-SPX, section "GTE Coprocessor". Cross-checked
20//! against PCSX-Redux's `gte.cc` interpreter.
21//!
22//! ## Provenance
23//!
24//! Implemented from public hardware documentation (nocash PSX-SPX, "GTE
25//! Coprocessor") and parity-verified against PCSX-Redux's `gte.cc`
26//! (<https://github.com/grumpycoders/pcsx-redux>), GPL-2.0-or-later. The
27//! division seed table and saturation rules are hardware behaviour.
28//! PSoXide is released under GPL-2.0-or-later for compatibility with that
29//! reference; see `LICENSE` and `docs/license-audit.md`.
30
31#![allow(clippy::needless_range_loop)]
32
33use core::cmp;
34
35/// One of the 11 documented GTE function opcodes that needs its raw
36/// command-word bits decoded for sub-options (`sf`, `lm`, `mx`, `vx`,
37/// `cv`). Stored only so call sites read as data, never as parsing.
38#[derive(Copy, Clone)]
39struct Cmd {
40    /// `sf` -- fraction shift. `false` = no shift, `true` = shift the
41    /// 44-bit MAC result right arithmetically by 12 before truncating.
42    sf: bool,
43    /// `lm` -- IR1..3 saturation lower bound. `false` = -0x8000,
44    /// `true` = 0. Used by NCDS / CC / etc. to clamp negative
45    /// intermediate components into the unsigned color range.
46    lm: bool,
47    /// MVMVA matrix selector (bits 18..17). `0` = rotation,
48    /// `1` = light, `2` = light-color, `3` = invalid (uses a buggy
49    /// hardware path; we emulate it).
50    mx: u8,
51    /// MVMVA multiplied-vector selector (bits 16..15). `0` = V0,
52    /// `1` = V1, `2` = V2, `3` = `[IR1, IR2, IR3]`.
53    vx: u8,
54    /// MVMVA translation/bias-vector selector (bits 14..13).
55    /// `0` = TR, `1` = BK, `2` = FC (buggy), `3` = none.
56    cv: u8,
57}
58
59impl Cmd {
60    fn decode(instr: u32) -> Self {
61        Self {
62            sf: (instr >> 19) & 1 != 0,
63            lm: (instr >> 10) & 1 != 0,
64            mx: ((instr >> 17) & 0b11) as u8,
65            vx: ((instr >> 15) & 0b11) as u8,
66            cv: ((instr >> 13) & 0b11) as u8,
67        }
68    }
69
70    fn shift(&self) -> u32 {
71        if self.sf {
72            12
73        } else {
74            0
75        }
76    }
77}
78
79/// FLAG (control register 31) bit positions. Names mirror PSX-SPX.
80mod flag {
81    pub const IR0_SAT: u32 = 1 << 12;
82    pub const SY2_SAT: u32 = 1 << 13;
83    pub const SX2_SAT: u32 = 1 << 14;
84    pub const MAC0_NEG: u32 = 1 << 15;
85    pub const MAC0_POS: u32 = 1 << 16;
86    pub const DIV_OVERFLOW: u32 = 1 << 17;
87    pub const SZ3_OTZ_SAT: u32 = 1 << 18;
88    pub const COLOR_B_SAT: u32 = 1 << 19;
89    pub const COLOR_G_SAT: u32 = 1 << 20;
90    pub const COLOR_R_SAT: u32 = 1 << 21;
91    pub const IR3_SAT: u32 = 1 << 22;
92    pub const IR2_SAT: u32 = 1 << 23;
93    pub const IR1_SAT: u32 = 1 << 24;
94    pub const MAC3_NEG: u32 = 1 << 25;
95    pub const MAC2_NEG: u32 = 1 << 26;
96    pub const MAC1_NEG: u32 = 1 << 27;
97    pub const MAC3_POS: u32 = 1 << 28;
98    pub const MAC2_POS: u32 = 1 << 29;
99    pub const MAC1_POS: u32 = 1 << 30;
100    /// Bits that participate in the FLAG[31] master OR.
101    pub const ERROR_MASK: u32 = 0x7F87_E000;
102}
103
104/// Full GTE state. Field grouping matches the PSX register map:
105/// vectors / FIFOs / accumulators in `data`-prefixed fields, matrices
106/// and translations in `ctrl`-prefixed fields. The MFC2/MTC2 paths
107/// pack and unpack these into 32-bit views.
108#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
109pub struct Gte {
110    // Data registers ----------------------------------------------------
111    /// V0, V1, V2 -- input vertex vectors (X, Y, Z), signed 1.3.12.
112    v: [[i16; 3]; 3],
113    /// RGBC -- packed [R, G, B, CODE]. CODE is the texpage / blending
114    /// hint; preserved through color-FIFO pushes.
115    rgbc: [u8; 4],
116    /// OTZ -- average Z written by AVSZ3/AVSZ4, unsigned 16-bit.
117    otz: u16,
118    /// IR0 -- scalar accumulator written by INTPL/DPCS/RTPS. Saturates
119    /// to 0..0x1000.
120    ir0: i16,
121    /// IR1, IR2, IR3 -- vector accumulators. Saturate to ±0x7FFF
122    /// (lm=0) or 0..0x7FFF (lm=1).
123    ir: [i16; 3],
124    /// SXY FIFO -- three slots of (SX, SY) ∈ -1024..1023, plus a
125    /// virtual "P" slot at index 3 that aliases SXY2 on read and
126    /// rotates the FIFO on write.
127    sxy: [[i16; 2]; 3],
128    /// SZ FIFO -- four slots of unsigned 16-bit Z. New Z values land
129    /// at SZ3 and shift the prior contents toward SZ0.
130    sz: [u16; 4],
131    /// RGB FIFO -- three slots of [R, G, B, CODE]. RTPS-family ops
132    /// push the latest RGBC through here.
133    rgb_fifo: [[u8; 4]; 3],
134    /// Reserved word at data-reg 23. Round-trips writes (some games
135    /// cache values here knowing the PS1 leaves it untouched).
136    res1: u32,
137    /// MAC0 -- scalar 32-bit accumulator (e.g. perspective math).
138    mac0: i32,
139    /// MAC1, MAC2, MAC3 -- 32-bit truncations of the 44-bit vector
140    /// accumulators.
141    mac: [i32; 3],
142    /// LZCS / LZCR -- leading-zero counter input / result. Writing
143    /// LZCS recomputes LZCR; reads of LZCR return the cached value.
144    /// At reset both are zero -- LZCR is *not* eagerly seeded to 32
145    /// (the canonical lzcnt of LZCS=0). Real software always writes
146    /// LZCS before reading LZCR, but the parity oracle (PCSX-Redux)
147    /// snapshots raw register storage which is `memset(0)` on boot,
148    /// so we must match that to keep step-0 traces identical.
149    lzcs: u32,
150    lzcr: u32,
151
152    /// HWB-010 hazard injection: when set, the NEXT V0 transform computes
153    /// its first row with this value as V0.x, while later rows see the fresh
154    /// register. Models the MTC2 VXY0 write committing between sequential
155    /// GTE compute windows, measured on silicon in the HWB-010 live capture.
156    /// The captured operation was MVMVA; RTPS support is retained as a
157    /// diagnostic reproduction mode for the identical unpadded input schedule.
158    /// Transient: cleared by
159    /// [`Gte::execute_with_stale_v0x`] after the command.
160    ///
161    /// Excluded from save states: this only ever lives for the
162    /// duration of a single GTE command, so it can never be observed
163    /// set at a save-state boundary in practice, and carrying a stale
164    /// hazard across a save/load would inject a phantom one-shot bug
165    /// into the next MVMVA.
166    #[cfg_attr(feature = "serde", serde(skip))]
167    stale_v0x: Option<i16>,
168    /// OP counterpart to `stale_v0x`: only its first MAC row sees the IR3
169    /// value that preceded an immediately adjacent MTC2 IR3. Later rows see
170    /// the committed value. Transient and one-shot for the same reason.
171    #[cfg_attr(feature = "serde", serde(skip))]
172    stale_op_ir3: Option<i16>,
173
174    // Control registers -------------------------------------------------
175    /// Rotation matrix RT, signed 1.3.12.
176    rotation: [[i16; 3]; 3],
177    /// Translation vector TR, signed 31.0.
178    translation: [i32; 3],
179    /// Light-direction matrix LLM (rows = light directions), signed 1.3.12.
180    light: [[i16; 3]; 3],
181    /// Background color BK = (RBK, GBK, BBK), signed 19.12. Stored as
182    /// 32-bit but only 32 bits round-trip; the documented field width
183    /// is "any" -- the constraint comes from the math (44-bit MAC).
184    bg_color: [i32; 3],
185    /// Light-color matrix LCM, signed 1.3.12.
186    light_color: [[i16; 3]; 3],
187    /// Far color FC = (RFC, GFC, BFC), signed 19.12.
188    far_color: [i32; 3],
189    /// Screen-offset X, signed 15.16.
190    ofx: i32,
191    /// Screen-offset Y, signed 15.16.
192    ofy: i32,
193    /// H -- projection plane distance. Unsigned 16-bit; consumed by the
194    /// RTPS divisor.
195    h: u16,
196    /// DQA -- depth-cue interpolation coefficient A, signed 7.8.
197    dqa: i16,
198    /// DQB -- depth-cue interpolation bias B, signed 7.24.
199    dqb: i32,
200    /// ZSF3, ZSF4 -- averaging weights for AVSZ3/AVSZ4, signed 0.12.
201    zsf3: i16,
202    zsf4: i16,
203    /// FLAG -- error/saturation bits. Bit 31 is the OR of [`flag::ERROR_MASK`].
204    flag: u32,
205    /// Diagnostic command counter. This deliberately does not drive
206    /// timing yet; it lets frontends report GTE pressure separately
207    /// from the current CPU/bus cycle model.
208    profile_ops: u64,
209    /// Sum of documented GTE command latencies for recognised ops.
210    /// Real hardware can hide some of this behind independent CPU
211    /// work, so consumers should treat it as internal GTE load rather
212    /// than extra bus cycles.
213    profile_estimated_cycles: u64,
214    /// Per-opcode diagnostic counts, indexed by the low 6 command bits.
215    /// Pure profiling data -- excluded from save states (see
216    /// [`default_opcode_counts`]).
217    #[cfg_attr(feature = "serde", serde(skip, default = "default_opcode_counts"))]
218    profile_opcode_counts: [u64; 64],
219}
220
221/// `#[serde(default = ...)]` target for [`Gte::profile_opcode_counts`].
222/// A plain `#[serde(skip)]` needs `[u64; 64]: Default`, which std only
223/// provides for arrays up to length 32 -- an explicit zeroed literal
224/// sidesteps that gap.
225#[cfg(feature = "serde")]
226fn default_opcode_counts() -> [u64; 64] {
227    [0; 64]
228}
229
230/// Monotonic diagnostic counters for GTE command pressure.
231#[derive(Clone, Copy, Debug, PartialEq, Eq)]
232pub struct GteProfileSnapshot {
233    /// Recognised GTE function commands executed since reset.
234    pub ops: u64,
235    /// Sum of estimated internal GTE cycles since reset.
236    pub estimated_cycles: u64,
237    /// Per-opcode command counts, indexed by the low 6 command bits.
238    pub opcode_counts: [u64; 64],
239}
240
241/// Documented command latencies from the public PSX GTE tables. The
242/// profiler reports these as internal GTE load only; the emulator's
243/// bus cycle counter remains the source of truth for elapsed guest time.
244fn gte_command_cycles(opcode: u8) -> Option<u8> {
245    match opcode {
246        0x01 => Some(15), // RTPS
247        0x06 => Some(8),  // NCLIP
248        0x0C => Some(6),  // OP
249        0x10 => Some(8),  // DPCS
250        0x11 => Some(8),  // INTPL
251        0x12 => Some(8),  // MVMVA
252        0x13 => Some(19), // NCDS
253        0x14 => Some(13), // CDP
254        0x16 => Some(44), // NCDT
255        0x1B => Some(17), // NCCS
256        0x1C => Some(11), // CC
257        0x1E => Some(14), // NCS
258        0x20 => Some(30), // NCT
259        0x28 => Some(5),  // SQR
260        0x29 => Some(8),  // DCPL
261        0x2A => Some(17), // DPCT
262        0x2D => Some(5),  // AVSZ3
263        0x2E => Some(6),  // AVSZ4
264        0x30 => Some(23), // RTPT
265        0x3D => Some(5),  // GPF
266        0x3E => Some(5),  // GPL
267        0x3F => Some(39), // NCCT
268        _ => None,
269    }
270}
271
272impl Gte {
273    /// Real-hardware cycle latency of the GTE command encoded in `instr`
274    /// (the bottom six bits select the function), or 0 for encodings that
275    /// decode as nops. Lets a CPU model charge the latency it stalls on
276    /// when a GTE result register is read before the op completes.
277    pub fn command_cycles(instr: u32) -> u32 {
278        gte_command_cycles((instr & 0x3F) as u8).map_or(0, u32::from)
279    }
280
281    /// Construct a freshly-reset GTE -- all registers cleared. Real
282    /// hardware powers on with garbage, but the BIOS zeroes the lot
283    /// before first use, so we save a redundant write by starting
284    /// clean.
285    pub fn new() -> Self {
286        Self {
287            v: [[0; 3]; 3],
288            rgbc: [0; 4],
289            otz: 0,
290            ir0: 0,
291            ir: [0; 3],
292            sxy: [[0; 2]; 3],
293            sz: [0; 4],
294            rgb_fifo: [[0; 4]; 3],
295            res1: 0,
296            mac0: 0,
297            mac: [0; 3],
298            lzcs: 0,
299            lzcr: 0,
300            stale_v0x: None,
301            stale_op_ir3: None,
302            rotation: [[0; 3]; 3],
303            translation: [0; 3],
304            light: [[0; 3]; 3],
305            bg_color: [0; 3],
306            light_color: [[0; 3]; 3],
307            far_color: [0; 3],
308            ofx: 0,
309            ofy: 0,
310            h: 0,
311            dqa: 0,
312            dqb: 0,
313            zsf3: 0,
314            zsf4: 0,
315            flag: 0,
316            profile_ops: 0,
317            profile_estimated_cycles: 0,
318            profile_opcode_counts: [0; 64],
319        }
320    }
321
322    /// Current monotonic GTE diagnostic counters.
323    pub fn profile_snapshot(&self) -> GteProfileSnapshot {
324        GteProfileSnapshot {
325            ops: self.profile_ops,
326            estimated_cycles: self.profile_estimated_cycles,
327            opcode_counts: self.profile_opcode_counts,
328        }
329    }
330
331    /// MFC2 view of data register `idx`. Mirrors what the CPU sees on
332    /// `mfc2 rt, $rd`.
333    pub fn read_data(&self, idx: u8) -> u32 {
334        match idx & 31 {
335            0 => pack_xy_i16(self.v[0][0], self.v[0][1]),
336            1 => sign_extend_16(self.v[0][2]),
337            2 => pack_xy_i16(self.v[1][0], self.v[1][1]),
338            3 => sign_extend_16(self.v[1][2]),
339            4 => pack_xy_i16(self.v[2][0], self.v[2][1]),
340            5 => sign_extend_16(self.v[2][2]),
341            6 => u32::from_le_bytes(self.rgbc),
342            7 => self.otz as u32,
343            8 => sign_extend_16(self.ir0),
344            9 => sign_extend_16(self.ir[0]),
345            10 => sign_extend_16(self.ir[1]),
346            11 => sign_extend_16(self.ir[2]),
347            12 => pack_xy_i16(self.sxy[0][0], self.sxy[0][1]),
348            13 => pack_xy_i16(self.sxy[1][0], self.sxy[1][1]),
349            // SXY2 and SXYP both read SXY2 -- only their write semantics
350            // differ.
351            14 | 15 => pack_xy_i16(self.sxy[2][0], self.sxy[2][1]),
352            16 => self.sz[0] as u32,
353            17 => self.sz[1] as u32,
354            18 => self.sz[2] as u32,
355            19 => self.sz[3] as u32,
356            20 => u32::from_le_bytes(self.rgb_fifo[0]),
357            21 => u32::from_le_bytes(self.rgb_fifo[1]),
358            22 => u32::from_le_bytes(self.rgb_fifo[2]),
359            23 => self.res1,
360            24 => self.mac0 as u32,
361            25 => self.mac[0] as u32,
362            26 => self.mac[1] as u32,
363            27 => self.mac[2] as u32,
364            // IRGB/ORGB: pack saturated IR1..3 into 5:5:5 (BGR order
365            // in the PS1 framebuffer convention).
366            28 | 29 => pack_irgb(&self.ir),
367            30 => self.lzcs,
368            31 => self.lzcr,
369            _ => unreachable!(),
370        }
371    }
372
373    /// MTC2 view of data register `idx`.
374    pub fn write_data(&mut self, idx: u8, value: u32) {
375        match idx & 31 {
376            0 => {
377                let (x, y) = unpack_xy_i16(value);
378                self.v[0][0] = x;
379                self.v[0][1] = y;
380            }
381            1 => self.v[0][2] = value as i16,
382            2 => {
383                let (x, y) = unpack_xy_i16(value);
384                self.v[1][0] = x;
385                self.v[1][1] = y;
386            }
387            3 => self.v[1][2] = value as i16,
388            4 => {
389                let (x, y) = unpack_xy_i16(value);
390                self.v[2][0] = x;
391                self.v[2][1] = y;
392            }
393            5 => self.v[2][2] = value as i16,
394            6 => self.rgbc = value.to_le_bytes(),
395            7 => self.otz = value as u16,
396            8 => self.ir0 = value as i16,
397            9 => self.ir[0] = value as i16,
398            10 => self.ir[1] = value as i16,
399            11 => self.ir[2] = value as i16,
400            12 => {
401                let (x, y) = unpack_xy_i16(value);
402                self.sxy[0] = [x, y];
403            }
404            13 => {
405                let (x, y) = unpack_xy_i16(value);
406                self.sxy[1] = [x, y];
407            }
408            14 => {
409                let (x, y) = unpack_xy_i16(value);
410                self.sxy[2] = [x, y];
411            }
412            // SXYP -- push: SXY1 → SXY0, SXY2 → SXY1, value → SXY2.
413            15 => {
414                let (x, y) = unpack_xy_i16(value);
415                self.sxy[0] = self.sxy[1];
416                self.sxy[1] = self.sxy[2];
417                self.sxy[2] = [x, y];
418            }
419            16 => self.sz[0] = value as u16,
420            17 => self.sz[1] = value as u16,
421            18 => self.sz[2] = value as u16,
422            19 => self.sz[3] = value as u16,
423            20 => self.rgb_fifo[0] = value.to_le_bytes(),
424            21 => self.rgb_fifo[1] = value.to_le_bytes(),
425            22 => self.rgb_fifo[2] = value.to_le_bytes(),
426            23 => self.res1 = value,
427            24 => self.mac0 = value as i32,
428            25 => self.mac[0] = value as i32,
429            26 => self.mac[1] = value as i32,
430            27 => self.mac[2] = value as i32,
431            // IRGB write -- unpack 5:5:5 and replicate into IR1..3 with
432            // each component shifted up by 7 (so 5-bit 31 → 0xF80).
433            28 => {
434                let r = (value & 0x1F) as i16;
435                let g = ((value >> 5) & 0x1F) as i16;
436                let b = ((value >> 10) & 0x1F) as i16;
437                self.ir[0] = r << 7;
438                self.ir[1] = g << 7;
439                self.ir[2] = b << 7;
440            }
441            // ORGB is read-only; write is silently dropped.
442            29 => {}
443            30 => {
444                self.lzcs = value;
445                self.lzcr = leading_count_signed(value);
446            }
447            // LZCR is read-only.
448            31 => {}
449            _ => unreachable!(),
450        }
451    }
452
453    /// CFC2 view of control register `idx`.
454    pub fn read_control(&self, idx: u8) -> u32 {
455        match idx & 31 {
456            0 => pack_xy_i16(self.rotation[0][0], self.rotation[0][1]),
457            1 => pack_xy_i16(self.rotation[0][2], self.rotation[1][0]),
458            2 => pack_xy_i16(self.rotation[1][1], self.rotation[1][2]),
459            3 => pack_xy_i16(self.rotation[2][0], self.rotation[2][1]),
460            // RT33 sits alone in the low halfword, sign-extended.
461            4 => sign_extend_16(self.rotation[2][2]),
462            5 => self.translation[0] as u32,
463            6 => self.translation[1] as u32,
464            7 => self.translation[2] as u32,
465            8 => pack_xy_i16(self.light[0][0], self.light[0][1]),
466            9 => pack_xy_i16(self.light[0][2], self.light[1][0]),
467            10 => pack_xy_i16(self.light[1][1], self.light[1][2]),
468            11 => pack_xy_i16(self.light[2][0], self.light[2][1]),
469            12 => sign_extend_16(self.light[2][2]),
470            13 => self.bg_color[0] as u32,
471            14 => self.bg_color[1] as u32,
472            15 => self.bg_color[2] as u32,
473            16 => pack_xy_i16(self.light_color[0][0], self.light_color[0][1]),
474            17 => pack_xy_i16(self.light_color[0][2], self.light_color[1][0]),
475            18 => pack_xy_i16(self.light_color[1][1], self.light_color[1][2]),
476            19 => pack_xy_i16(self.light_color[2][0], self.light_color[2][1]),
477            20 => sign_extend_16(self.light_color[2][2]),
478            21 => self.far_color[0] as u32,
479            22 => self.far_color[1] as u32,
480            23 => self.far_color[2] as u32,
481            24 => self.ofx as u32,
482            25 => self.ofy as u32,
483            // H is a hardware quirk: written as unsigned 16-bit but read
484            // back **sign-extended** (so writing 0x8000 reads back as
485            // 0xFFFF8000). Caught by parity tests against Redux.
486            26 => sign_extend_16(self.h as i16),
487            27 => sign_extend_16(self.dqa),
488            28 => self.dqb as u32,
489            29 => sign_extend_16(self.zsf3),
490            30 => sign_extend_16(self.zsf4),
491            31 => self.flag,
492            _ => unreachable!(),
493        }
494    }
495
496    /// CTC2 view of control register `idx`.
497    pub fn write_control(&mut self, idx: u8, value: u32) {
498        let (lo, hi) = unpack_xy_i16(value);
499        match idx & 31 {
500            0 => {
501                self.rotation[0][0] = lo;
502                self.rotation[0][1] = hi;
503            }
504            1 => {
505                self.rotation[0][2] = lo;
506                self.rotation[1][0] = hi;
507            }
508            2 => {
509                self.rotation[1][1] = lo;
510                self.rotation[1][2] = hi;
511            }
512            3 => {
513                self.rotation[2][0] = lo;
514                self.rotation[2][1] = hi;
515            }
516            4 => self.rotation[2][2] = value as i16,
517            5 => self.translation[0] = value as i32,
518            6 => self.translation[1] = value as i32,
519            7 => self.translation[2] = value as i32,
520            8 => {
521                self.light[0][0] = lo;
522                self.light[0][1] = hi;
523            }
524            9 => {
525                self.light[0][2] = lo;
526                self.light[1][0] = hi;
527            }
528            10 => {
529                self.light[1][1] = lo;
530                self.light[1][2] = hi;
531            }
532            11 => {
533                self.light[2][0] = lo;
534                self.light[2][1] = hi;
535            }
536            12 => self.light[2][2] = value as i16,
537            13 => self.bg_color[0] = value as i32,
538            14 => self.bg_color[1] = value as i32,
539            15 => self.bg_color[2] = value as i32,
540            16 => {
541                self.light_color[0][0] = lo;
542                self.light_color[0][1] = hi;
543            }
544            17 => {
545                self.light_color[0][2] = lo;
546                self.light_color[1][0] = hi;
547            }
548            18 => {
549                self.light_color[1][1] = lo;
550                self.light_color[1][2] = hi;
551            }
552            19 => {
553                self.light_color[2][0] = lo;
554                self.light_color[2][1] = hi;
555            }
556            20 => self.light_color[2][2] = value as i16,
557            21 => self.far_color[0] = value as i32,
558            22 => self.far_color[1] = value as i32,
559            23 => self.far_color[2] = value as i32,
560            24 => self.ofx = value as i32,
561            25 => self.ofy = value as i32,
562            26 => self.h = value as u16,
563            27 => self.dqa = value as i16,
564            28 => self.dqb = value as i32,
565            29 => self.zsf3 = value as i16,
566            30 => self.zsf4 = value as i16,
567            // FLAG: writes leave the master bit derived from the rest.
568            31 => {
569                self.flag = value & 0x7FFF_F000;
570                self.update_flag_master();
571            }
572            _ => unreachable!(),
573        }
574    }
575
576    /// Execute one COP2 function with the HWB-010 MTC2-commit hazard
577    /// injected: the first transform row reading V0 sees `stale_x` as V0.x
578    /// (the value held before the in-flight MTC2 VXY0 write), while later
579    /// rows see the fresh register.
580    /// Silicon-measured: six exact arithmetic confirmations across two live
581    /// hardware captures, HWB-009 and HWB-010.
582    pub fn execute_with_stale_v0x(&mut self, instr: u32, stale_x: i16) {
583        self.stale_v0x = Some(stale_x);
584        self.execute(instr);
585        self.stale_v0x = None;
586    }
587
588    /// Execute OP with an MTC2 IR3 value still committing during its first
589    /// sequential MAC row. MAC1 sees `stale_ir3`; MAC2/MAC3 see current IR3.
590    pub fn execute_op_with_stale_ir3(&mut self, instr: u32, stale_ir3: i16) {
591        self.stale_op_ir3 = Some(stale_ir3);
592        self.execute(instr);
593        self.stale_op_ir3 = None;
594    }
595
596    /// Execute one COP2 function. `instr` is the full 32-bit
597    /// instruction word so we can pull `sf`/`lm`/`mx`/`vx`/`cv`.
598    /// Unrecognised commands return without updating state -- real
599    /// hardware decodes them as nops, which matches what we observe
600    /// in PCSX-Redux's interpreter.
601    pub fn execute(&mut self, instr: u32) {
602        let cmd = Cmd::decode(instr);
603        let opcode = (instr & 0x3F) as u8;
604        if let Some(cycles) = gte_command_cycles(opcode) {
605            self.profile_ops = self.profile_ops.saturating_add(1);
606            self.profile_estimated_cycles =
607                self.profile_estimated_cycles.saturating_add(cycles as u64);
608            self.profile_opcode_counts[opcode as usize] =
609                self.profile_opcode_counts[opcode as usize].saturating_add(1);
610        }
611        self.flag = 0;
612        match opcode {
613            0x01 => self.op_rtps(cmd, 0, true),
614            0x06 => self.op_nclip(),
615            0x0C => self.op_op(cmd),
616            0x10 => self.op_dpcs(cmd, false),
617            0x11 => self.op_intpl(cmd),
618            0x12 => self.op_mvmva(cmd),
619            0x13 => self.op_ncds(cmd, 0),
620            0x14 => self.op_cdp(cmd),
621            0x16 => self.op_ncdt(cmd),
622            0x1B => self.op_nccs(cmd, 0),
623            0x1C => self.op_cc(cmd),
624            0x1E => self.op_ncs(cmd, 0),
625            0x20 => self.op_nct(cmd),
626            0x28 => self.op_sqr(cmd),
627            0x29 => self.op_dcpl(cmd),
628            0x2A => self.op_dpct(cmd),
629            0x2D => self.op_avsz3(),
630            0x2E => self.op_avsz4(),
631            0x30 => self.op_rtpt(cmd),
632            0x3D => self.op_gpf(cmd),
633            0x3E => self.op_gpl(cmd),
634            0x3F => self.op_ncct(cmd),
635            _ => {
636                // Real GTE silently ignores undefined commands. We
637                // mirror that -- the BIOS shouldn't issue any but
638                // games occasionally encode trailing instruction words
639                // with stray COP2 patterns.
640            }
641        }
642        self.update_flag_master();
643    }
644
645    // ------------------------------------------------------------------
646    // Operations
647    // ------------------------------------------------------------------
648
649    /// `RTPS` -- perspective transformation of `V[idx]`. When `last` is
650    /// true, also updates IR0/MAC0 from DQA/DQB (so `RTPT` can call
651    /// this for the first two vertices with `last=false`).
652    fn op_rtps(&mut self, cmd: Cmd, idx: usize, last: bool) {
653        let mut v = self.v[idx];
654        // Diagnostic extension of the console-measured MVMVA input hazard:
655        // the SDK's single-vertex projection helper uses the same adjacent
656        // MTC2 VXY0/MTC2 VZ0/RTPS schedule. Let the emulator reproduce that
657        // schedule with the previous X while preserving the fresh Y/Z values.
658        if idx == 0 {
659            if let Some(stale_x) = self.stale_v0x {
660                v[0] = stale_x;
661            }
662        }
663        let sf = cmd.shift();
664
665        // [MAC1,MAC2,MAC3] = (TR << 12 + RT * V) >> sf
666        let tr = self.translation;
667        let rt = self.rotation;
668        let acc1 = self.mac_add_row(1, tr[0], &rt[0], &v, sf);
669        let acc2 = self.mac_add_row(2, tr[1], &rt[1], &v, sf);
670        let acc3 = self.mac_add_row(3, tr[2], &rt[2], &v, sf);
671        let mac1 = (acc1 >> sf) as i32;
672        let mac2 = (acc2 >> sf) as i32;
673        let mac3 = (acc3 >> sf) as i32;
674
675        self.ir[0] = self.saturate_ir(1, mac1, cmd.lm);
676        self.ir[1] = self.saturate_ir(2, mac2, cmd.lm);
677        // IR3 quirk: the saturation FLAG is checked against the 44-bit
678        // accumulator >> 12 (always, regardless of sf, always lm=false),
679        // while the stored IR3 saturates the 32-bit MAC3 with cmd.lm.
680        // Console-verified by the fuzz corpus: with sf=0 the wrapped
681        // MAC3 and the 44-bit acc >> 12 land on opposite clamp sides.
682        let ir3_flag_value = (acc3 >> 12) as i32;
683        let _ = self.saturate_ir_flag_only(3, ir3_flag_value, false);
684        self.ir[2] = self.saturate_value_for_ir(mac3, cmd.lm);
685
686        // Push SZ3 = 44-bit MAC3 accumulator >> 12 (NOT the wrapped
687        // 32-bit register), saturated to 0..0xFFFF.
688        let sz_value = (acc3 >> 12) as i32;
689        self.push_sz(sz_value);
690
691        // Perspective division: divisor = clamp_17((H<<16) / SZ3).
692        let divisor = self.unr_divide();
693
694        // SX2 = (divisor * IR1 + OFX) / 0x10000  → clamped to ±0x400.
695        let mac0_x = (divisor as i64) * (self.ir[0] as i64) + (self.ofx as i64);
696        let mac0_y = (divisor as i64) * (self.ir[1] as i64) + (self.ofy as i64);
697        // MAC0 stores the *post*-screen-X computation only briefly;
698        // we re-overwrite it with the depth-cue result if last=true.
699        // The screen coordinate saturates on the 32-BIT WRAP of the shifted
700        // accumulator (s32(Sx >> 16)): a huge positive product can wrap
701        // negative and saturate to -0x400, not +0x3FF. Console-verified by
702        // the fuzz corpus (0xFC00 pairs where the unwrapped math says 0x3FF).
703        let _ = self.check_mac0(mac0_x);
704        let sx = self.saturate_screen(((mac0_x >> 16) as i32) as i64, true);
705        let _ = self.check_mac0(mac0_y);
706        let sy = self.saturate_screen(((mac0_y >> 16) as i32) as i64, false);
707        self.push_sxy(sx, sy);
708
709        if last {
710            // MAC0 = divisor * DQA + DQB; IR0 = MAC0 / 0x1000 saturated 0..0x1000.
711            let mac0 = (divisor as i64) * (self.dqa as i64) + (self.dqb as i64);
712            self.mac0 = self.check_mac0(mac0);
713            self.ir0 = self.saturate_ir0((mac0 >> 12) as i32);
714        } else {
715            // For RTPT non-final iterations we still write MAC0 from
716            // the screen-Y math so software reading MAC0 between
717            // calls sees the post-perspective accumulator. Redux
718            // mirrors this.
719            self.mac0 = self.check_mac0(mac0_y);
720        }
721    }
722
723    /// `RTPT` -- perspective transform of all three vectors.
724    fn op_rtpt(&mut self, cmd: Cmd) {
725        self.op_rtps(cmd, 0, false);
726        self.op_rtps(cmd, 1, false);
727        self.op_rtps(cmd, 2, true);
728    }
729
730    /// `NCLIP` -- normal clipping. Computes the Z component of the
731    /// cross-product `(SXY1 - SXY0) × (SXY2 - SXY0)` to determine
732    /// front/back facing.
733    ///
734    /// `MAC0 = SX0*(SY1-SY2) + SX1*(SY2-SY0) + SX2*(SY0-SY1)`
735    fn op_nclip(&mut self) {
736        let sx0 = self.sxy[0][0] as i64;
737        let sy0 = self.sxy[0][1] as i64;
738        let sx1 = self.sxy[1][0] as i64;
739        let sy1 = self.sxy[1][1] as i64;
740        let sx2 = self.sxy[2][0] as i64;
741        let sy2 = self.sxy[2][1] as i64;
742        let result = sx0 * (sy1 - sy2) + sx1 * (sy2 - sy0) + sx2 * (sy0 - sy1);
743        self.mac0 = self.check_mac0(result);
744    }
745
746    /// `OP` -- outer product of IR vector with the diagonal of the
747    /// rotation matrix. Produces a vector cross-product variant used
748    /// for normal generation.
749    fn op_op(&mut self, cmd: Cmd) {
750        let sf = cmd.shift();
751        let d1 = self.rotation[0][0] as i64;
752        let d2 = self.rotation[1][1] as i64;
753        let d3 = self.rotation[2][2] as i64;
754        let ir1 = self.ir[0] as i64;
755        let ir2 = self.ir[1] as i64;
756        let ir3 = self.ir[2] as i64;
757        let ir3_mac1 = i64::from(self.stale_op_ir3.unwrap_or(self.ir[2]));
758        let mac1 = self.check_mac(1, (ir3_mac1 * d2) - (ir2 * d3)) >> sf;
759        let mac2 = self.check_mac(2, (ir1 * d3) - (ir3 * d1)) >> sf;
760        let mac3 = self.check_mac(3, (ir2 * d1) - (ir1 * d2)) >> sf;
761        self.mac[0] = mac1 as i32;
762        self.mac[1] = mac2 as i32;
763        self.mac[2] = mac3 as i32;
764        self.ir[0] = self.saturate_ir(1, self.mac[0], cmd.lm);
765        self.ir[1] = self.saturate_ir(2, self.mac[1], cmd.lm);
766        self.ir[2] = self.saturate_ir(3, self.mac[2], cmd.lm);
767    }
768
769    /// `MVMVA` -- multiply matrix by vector and add translation. Both
770    /// the matrix, the vector, and the translation are selected by
771    /// command bits.
772    fn op_mvmva(&mut self, cmd: Cmd) {
773        let sf = cmd.shift();
774        let mx = self.select_matrix(cmd.mx);
775        let v = self.select_vector(cmd.vx);
776        let tr = self.select_translation(cmd.cv);
777        // HWB-010 hazard injection: the GTE computes its MAC rows
778        // SEQUENTIALLY (MAC1 first); a V0.x value still committing from
779        // an MTC2 lands between the MAC1 and MAC2 phases, so only the
780        // MAC1 row sees the stale x. Applies to V0 reads only.
781        let v_mac1 = match self.stale_v0x {
782            Some(stale_x) if cmd.vx == 0 => [stale_x, v[1], v[2]],
783            _ => v,
784        };
785
786        // The cv=2 (FC) path is famously buggy (PSX-SPX, "GTE Opcode
787        // Bugs"): the far-color translation is NOT added, and the bug
788        // also DROPS the first matrix column, so the result reduces to
789        // the last two products: MAC_i = (Mx_i2*Vy + Mx_i3*Vz) SAR sf.
790        // Confirmed against real hardware (cortex_ignition_v1 GTE disc
791        // 2026-06-09: MAC = 0xfffffcc5,0xffffe36c,0xffffee75).
792        //
793        // The FLAG/IR saturation, however, IS evaluated against the full
794        // first term (FC<<12 + Mx_i1*Vx): step 1 sets only the IR flags
795        // from that discarded value, step 2 computes the real result.
796        if cmd.cv == 2 {
797            for i in 0..3 {
798                let bias = (tr[i] as i64) << 12;
799                let vx_in = if i == 0 { v_mac1[0] } else { v[0] };
800                let prod = (mx[i][0] as i64) * (vx_in as i64);
801                let stage1 = self.check_mac((i + 1) as u8, bias + prod) >> sf;
802                let _ = self.saturate_ir_flag_only((i + 1) as u8, stage1 as i32, false);
803            }
804            for i in 0..3 {
805                // Bug: first column (Mx_i1 * Vx) is dropped along with TR.
806                // The stage-2 result still runs the normal IR saturation
807                // (flag set with cmd.lm) on top of the stage-1 flags.
808                let prod = (mx[i][1] as i64) * (v[1] as i64) + (mx[i][2] as i64) * (v[2] as i64);
809                let mac = self.check_mac((i + 1) as u8, prod) >> sf;
810                self.mac[i] = mac as i32;
811                self.ir[i] = self.saturate_ir((i + 1) as u8, mac as i32, cmd.lm);
812            }
813            return;
814        }
815
816        for i in 0..3 {
817            let v_row = if i == 0 { &v_mac1 } else { &v };
818            let acc = self.mac_add_row((i + 1) as u8, tr[i], &mx[i], v_row, sf);
819            self.ir[i] = self.saturate_ir((i + 1) as u8, (acc >> sf) as i32, cmd.lm);
820        }
821    }
822
823    /// `SQR` -- square the IR vector. `MAC = IR * IR`, IR = saturate(MAC).
824    fn op_sqr(&mut self, cmd: Cmd) {
825        let sf = cmd.shift();
826        for i in 0..3 {
827            let v = self.ir[i] as i64;
828            let mac = self.check_mac((i + 1) as u8, v * v) >> sf;
829            self.mac[i] = mac as i32;
830            self.ir[i] = self.saturate_ir((i + 1) as u8, mac as i32, cmd.lm);
831        }
832    }
833
834    /// `AVSZ3` -- average of three Z values in the FIFO. `OTZ = ZSF3 *
835    /// (SZ1 + SZ2 + SZ3) >> 12`, saturated to 0..0xFFFF.
836    fn op_avsz3(&mut self) {
837        let sum = (self.sz[1] as i64) + (self.sz[2] as i64) + (self.sz[3] as i64);
838        let mac0 = (self.zsf3 as i64) * sum;
839        self.mac0 = self.check_mac0(mac0);
840        self.otz = self.saturate_otz(mac0 >> 12);
841    }
842
843    /// `AVSZ4` -- average of all four Z values. Same shape as AVSZ3.
844    fn op_avsz4(&mut self) {
845        let sum =
846            (self.sz[0] as i64) + (self.sz[1] as i64) + (self.sz[2] as i64) + (self.sz[3] as i64);
847        let mac0 = (self.zsf4 as i64) * sum;
848        self.mac0 = self.check_mac0(mac0);
849        self.otz = self.saturate_otz(mac0 >> 12);
850    }
851
852    /// `DPCS` -- depth-cue colour single. Interpolates the current RGBC
853    /// toward the far colour using IR0 as the blend factor. `is_dpct`
854    /// distinguishes the variant that pushes RGB FIFO state instead of
855    /// reading from RGBC.
856    fn op_dpcs(&mut self, cmd: Cmd, is_dpct: bool) {
857        let (r, g, b) = if is_dpct {
858            (
859                self.rgb_fifo[0][0],
860                self.rgb_fifo[0][1],
861                self.rgb_fifo[0][2],
862            )
863        } else {
864            (self.rgbc[0], self.rgbc[1], self.rgbc[2])
865        };
866        let sf = cmd.shift();
867        // [MAC1,2,3] = [R,G,B,...] << 16
868        //   + IR0 * limB((FC << 12) - ([R,G,B] << 16))
869        // Per PSX-SPX, the difference is computed in 12-bit fractional
870        // intermediate via IR clamp (lm=false). We follow that.
871        let bases = [r as i64, g as i64, b as i64];
872        let fc = self.far_color;
873        for i in 0..3 {
874            let base = bases[i] << 16;
875            let diff = ((fc[i] as i64) << 12) - base;
876            let temp = self.check_mac((i + 1) as u8, diff) >> sf;
877            let ir = self.saturate_value_for_ir(temp as i32, false);
878            let _ = self.saturate_ir_flag_only((i + 1) as u8, temp as i32, false);
879            let combined =
880                self.check_mac((i + 1) as u8, base + (self.ir0 as i64) * (ir as i64)) >> sf;
881            self.mac[i] = combined as i32;
882            self.ir[i] = self.saturate_ir((i + 1) as u8, combined as i32, cmd.lm);
883        }
884        self.push_color_from_mac(cmd);
885    }
886
887    /// `DPCT` -- depth cue triple: DPCS run three times against the
888    /// RGB FIFO.
889    fn op_dpct(&mut self, cmd: Cmd) {
890        for _ in 0..3 {
891            self.op_dpcs(cmd, true);
892        }
893    }
894
895    /// `INTPL` -- interpolate IR vector toward FC by IR0.
896    fn op_intpl(&mut self, cmd: Cmd) {
897        let sf = cmd.shift();
898        let fc = self.far_color;
899        let initial_ir = self.ir;
900        for i in 0..3 {
901            let base = (initial_ir[i] as i64) << 12;
902            let diff = ((fc[i] as i64) << 12) - base;
903            let temp = self.check_mac((i + 1) as u8, diff) >> sf;
904            let _ = self.saturate_ir_flag_only((i + 1) as u8, temp as i32, false);
905            let ir = self.saturate_value_for_ir(temp as i32, false);
906            let combined =
907                self.check_mac((i + 1) as u8, base + (self.ir0 as i64) * (ir as i64)) >> sf;
908            self.mac[i] = combined as i32;
909            self.ir[i] = self.saturate_ir((i + 1) as u8, combined as i32, cmd.lm);
910        }
911        self.push_color_from_mac(cmd);
912    }
913
914    /// `NCS` -- normal colour single. Lights `V[idx]` against the LLM,
915    /// then colours via LCM. Unlike NCCS/NCDS/CC, this opcode does
916    /// not modulate the result by RGBC.
917    ///
918    /// Stage 1 formula per PSX-SPX: `MAC = (LLM × V) >> (sf×12)` --
919    /// *no* BK bias. (The previous implementation added BK in both
920    /// stages and overcounted the bias.) Stage 2 is where BK bias
921    /// belongs: `MAC = (BK<<12 + LCM × IR) >> (sf×12)`.
922    fn op_ncs(&mut self, cmd: Cmd, idx: usize) {
923        let v = self.v[idx];
924        // [IR1,IR2,IR3] = (LLM * V) >> (sf*12)
925        self.light_matrix_pass(&v, cmd);
926        // [IR1,IR2,IR3] = (bg_color << 12  +  LCM * IR) >> (sf*12)
927        self.color_matrix_pass(cmd);
928        self.push_color_from_mac(cmd);
929    }
930
931    /// `NCT` -- NCS for V0, V1, V2.
932    fn op_nct(&mut self, cmd: Cmd) {
933        for i in 0..3 {
934            self.op_ncs(cmd, i);
935        }
936    }
937
938    /// `NCDS` -- normal colour depth-cue single. Like NCS but with the
939    /// final colour interpolated toward FC by IR0.
940    ///
941    /// Stage-1 BK bias removed -- see [`Gte::op_ncs`] for the rationale.
942    fn op_ncds(&mut self, cmd: Cmd, idx: usize) {
943        let sf = cmd.shift();
944        let v = self.v[idx];
945        self.light_matrix_pass(&v, cmd);
946        self.color_matrix_pass(cmd);
947        // Interpolate toward FC.
948        let cs = [
949            (self.rgbc[0] as i64) << 4,
950            (self.rgbc[1] as i64) << 4,
951            (self.rgbc[2] as i64) << 4,
952        ];
953        for i in 0..3 {
954            let base = cs[i] * (self.ir[i] as i64);
955            let diff = ((self.far_color[i] as i64) << 12) - base;
956            let temp = self.check_mac((i + 1) as u8, diff) >> sf;
957            let _ = self.saturate_ir_flag_only((i + 1) as u8, temp as i32, false);
958            let ir = self.saturate_value_for_ir(temp as i32, false);
959            let combined =
960                self.check_mac((i + 1) as u8, base + (self.ir0 as i64) * (ir as i64)) >> sf;
961            self.mac[i] = combined as i32;
962            self.ir[i] = self.saturate_ir((i + 1) as u8, combined as i32, cmd.lm);
963        }
964        self.push_color_from_mac(cmd);
965    }
966
967    /// `NCDT` -- NCDS for V0, V1, V2.
968    fn op_ncdt(&mut self, cmd: Cmd) {
969        for i in 0..3 {
970            self.op_ncds(cmd, i);
971        }
972    }
973
974    /// `NCCS` -- normal colour single (no depth cue, but colour multiplied
975    /// against the input RGBC).
976    ///
977    /// Stage-1 BK bias removed -- see [`Gte::op_ncs`] for the rationale.
978    fn op_nccs(&mut self, cmd: Cmd, idx: usize) {
979        let sf = cmd.shift();
980        let v = self.v[idx];
981        self.light_matrix_pass(&v, cmd);
982        self.color_matrix_pass(cmd);
983        let cs = [
984            (self.rgbc[0] as i64) << 4,
985            (self.rgbc[1] as i64) << 4,
986            (self.rgbc[2] as i64) << 4,
987        ];
988        for i in 0..3 {
989            let mac = self.check_mac((i + 1) as u8, cs[i] * (self.ir[i] as i64)) >> sf;
990            self.mac[i] = mac as i32;
991            self.ir[i] = self.saturate_ir((i + 1) as u8, mac as i32, cmd.lm);
992        }
993        self.push_color_from_mac(cmd);
994    }
995
996    fn op_ncct(&mut self, cmd: Cmd) {
997        for i in 0..3 {
998            self.op_nccs(cmd, i);
999        }
1000    }
1001
1002    /// `CC` -- colour-colour: blend RGBC against IR using the LCM and
1003    /// background-colour bias.
1004    fn op_cc(&mut self, cmd: Cmd) {
1005        let sf = cmd.shift();
1006        self.color_matrix_pass(cmd);
1007        let cs = [
1008            (self.rgbc[0] as i64) << 4,
1009            (self.rgbc[1] as i64) << 4,
1010            (self.rgbc[2] as i64) << 4,
1011        ];
1012        for i in 0..3 {
1013            let mac = self.check_mac((i + 1) as u8, cs[i] * (self.ir[i] as i64)) >> sf;
1014            self.mac[i] = mac as i32;
1015            self.ir[i] = self.saturate_ir((i + 1) as u8, mac as i32, cmd.lm);
1016        }
1017        self.push_color_from_mac(cmd);
1018    }
1019
1020    /// `CDP` -- colour depth-queue: same as CC but with FC interpolation.
1021    fn op_cdp(&mut self, cmd: Cmd) {
1022        let sf = cmd.shift();
1023        self.color_matrix_pass(cmd);
1024        let cs = [
1025            (self.rgbc[0] as i64) << 4,
1026            (self.rgbc[1] as i64) << 4,
1027            (self.rgbc[2] as i64) << 4,
1028        ];
1029        for i in 0..3 {
1030            let base = cs[i] * (self.ir[i] as i64);
1031            let diff = ((self.far_color[i] as i64) << 12) - base;
1032            let temp = self.check_mac((i + 1) as u8, diff) >> sf;
1033            let _ = self.saturate_ir_flag_only((i + 1) as u8, temp as i32, false);
1034            let ir = self.saturate_value_for_ir(temp as i32, false);
1035            let combined =
1036                self.check_mac((i + 1) as u8, base + (self.ir0 as i64) * (ir as i64)) >> sf;
1037            self.mac[i] = combined as i32;
1038            self.ir[i] = self.saturate_ir((i + 1) as u8, combined as i32, cmd.lm);
1039        }
1040        self.push_color_from_mac(cmd);
1041    }
1042
1043    /// `DCPL` -- depth-cue color light. Interpolate `RGBC * IR` toward
1044    /// FC by IR0, with no LCM stage.
1045    fn op_dcpl(&mut self, cmd: Cmd) {
1046        let sf = cmd.shift();
1047        let cs = [
1048            (self.rgbc[0] as i64) << 4,
1049            (self.rgbc[1] as i64) << 4,
1050            (self.rgbc[2] as i64) << 4,
1051        ];
1052        let ir = self.ir;
1053        for i in 0..3 {
1054            let base = cs[i] * (ir[i] as i64);
1055            let diff = ((self.far_color[i] as i64) << 12) - base;
1056            let temp = self.check_mac((i + 1) as u8, diff) >> sf;
1057            let _ = self.saturate_ir_flag_only((i + 1) as u8, temp as i32, false);
1058            let irc = self.saturate_value_for_ir(temp as i32, false);
1059            let combined =
1060                self.check_mac((i + 1) as u8, base + (self.ir0 as i64) * (irc as i64)) >> sf;
1061            self.mac[i] = combined as i32;
1062            self.ir[i] = self.saturate_ir((i + 1) as u8, combined as i32, cmd.lm);
1063        }
1064        self.push_color_from_mac(cmd);
1065    }
1066
1067    /// `GPF` -- general-purpose interpolation. `MAC = IR * IR0`, then
1068    /// IR/RGB push.
1069    fn op_gpf(&mut self, cmd: Cmd) {
1070        let sf = cmd.shift();
1071        for i in 0..3 {
1072            let mac = self.check_mac((i + 1) as u8, (self.ir0 as i64) * (self.ir[i] as i64)) >> sf;
1073            self.mac[i] = mac as i32;
1074            self.ir[i] = self.saturate_ir((i + 1) as u8, mac as i32, cmd.lm);
1075        }
1076        self.push_color_from_mac(cmd);
1077    }
1078
1079    /// `GPL` -- general-purpose interpolation with base. `MAC = MAC + IR * IR0`.
1080    fn op_gpl(&mut self, cmd: Cmd) {
1081        let sf = cmd.shift();
1082        for i in 0..3 {
1083            // MAC base is shifted left by sf before addition (so the
1084            // pre-existing MAC value is treated as a fixed-point with
1085            // the same scaling as the new product).
1086            let base = (self.mac[i] as i64) << sf;
1087            let mac = self.check_mac(
1088                (i + 1) as u8,
1089                base + (self.ir0 as i64) * (self.ir[i] as i64),
1090            ) >> sf;
1091            self.mac[i] = mac as i32;
1092            self.ir[i] = self.saturate_ir((i + 1) as u8, mac as i32, cmd.lm);
1093        }
1094        self.push_color_from_mac(cmd);
1095    }
1096
1097    // ------------------------------------------------------------------
1098    // Helpers
1099    // ------------------------------------------------------------------
1100
1101    /// Compute one row of `(translation << 12) + matrix * vector`, with
1102    /// the 44-bit overflow check applied, then arithmetic-shift the
1103    /// 64-bit result right by `sf` and store the truncated 32-bit form
1104    /// into `MAC[idx]`.
1105    fn mac_add_row(&mut self, idx: u8, tr: i32, row: &[i16; 3], v: &[i16; 3], sf: u32) -> i64 {
1106        // Hardware accumulates STAGED: the 44-bit overflow check fires and
1107        // the accumulator sign-extends from bit 43 after EVERY addition
1108        // step, so an intermediate overflow both flags and wraps even when
1109        // the final sum is back in range. The console-verified fuzz corpus
1110        // (gte_fuzz_replay) is sensitive to both effects.
1111        //
1112        // Returns the 44-bit accumulator UNSHIFTED: RTPS derives SZ3 and
1113        // the IR3 saturation flag from `acc >> 12` of the full-width value
1114        // even when sf=0 leaves MAC3 itself unshifted (and 32-bit wrapped).
1115        let mut acc = self.check_mac(idx, ((tr as i64) << 12) + (row[0] as i64) * (v[0] as i64));
1116        acc = self.check_mac(idx, acc + (row[1] as i64) * (v[1] as i64));
1117        acc = self.check_mac(idx, acc + (row[2] as i64) * (v[2] as i64));
1118        self.mac[(idx - 1) as usize] = (acc >> sf) as i32;
1119        acc
1120    }
1121
1122    /// Lighting stage 1: `[MAC,IR] = (LLM * V) >> sf` with staged 44-bit
1123    /// accumulation (intermediate overflows flag and wrap per step).
1124    fn light_matrix_pass(&mut self, v: &[i16; 3], cmd: Cmd) {
1125        let sf = cmd.shift();
1126        for i in 0..3 {
1127            let row = self.light[i];
1128            let acc = self.mac_add_row((i + 1) as u8, 0, &row, v, sf);
1129            self.ir[i] = self.saturate_ir((i + 1) as u8, (acc >> sf) as i32, cmd.lm);
1130        }
1131    }
1132
1133    /// Lighting stage 2: `[MAC,IR] = (BK<<12 + LCM * IR) >> sf`, staged.
1134    fn color_matrix_pass(&mut self, cmd: Cmd) {
1135        let sf = cmd.shift();
1136        let n = self.ir;
1137        for i in 0..3 {
1138            let row = self.light_color[i];
1139            let bk = self.bg_color[i];
1140            let acc = self.mac_add_row((i + 1) as u8, bk, &row, &n, sf);
1141            self.ir[i] = self.saturate_ir((i + 1) as u8, (acc >> sf) as i32, cmd.lm);
1142        }
1143    }
1144
1145    /// Apply the 44-bit signed overflow check, returning the value
1146    /// SIGN-EXTENDED from bit 43 (the hardware accumulator width).
1147    /// Sets the appropriate `FLAG` bit on overflow.
1148    fn check_mac(&mut self, idx: u8, value: i64) -> i64 {
1149        let pos_limit = (1i64 << 43) - 1;
1150        let neg_limit = -(1i64 << 43);
1151        if value > pos_limit {
1152            self.flag |= match idx {
1153                1 => flag::MAC1_POS,
1154                2 => flag::MAC2_POS,
1155                3 => flag::MAC3_POS,
1156                _ => 0,
1157            };
1158        }
1159        if value < neg_limit {
1160            self.flag |= match idx {
1161                1 => flag::MAC1_NEG,
1162                2 => flag::MAC2_NEG,
1163                3 => flag::MAC3_NEG,
1164                _ => 0,
1165            };
1166        }
1167        (value << 20) >> 20
1168    }
1169
1170    /// MAC0 32-bit overflow check + truncation.
1171    fn check_mac0(&mut self, value: i64) -> i32 {
1172        if value > i32::MAX as i64 {
1173            self.flag |= flag::MAC0_POS;
1174        }
1175        if value < i32::MIN as i64 {
1176            self.flag |= flag::MAC0_NEG;
1177        }
1178        value as i32
1179    }
1180
1181    /// Saturate a value to the IR1/IR2/IR3 range and store it. Sets
1182    /// the corresponding `FLAG` bit on saturation.
1183    fn saturate_ir(&mut self, idx: u8, value: i32, lm: bool) -> i16 {
1184        let lo = if lm { 0 } else { -0x8000 };
1185        let hi = 0x7FFFi32;
1186        let sat = if value > hi {
1187            self.set_ir_flag(idx);
1188            hi
1189        } else if value < lo {
1190            self.set_ir_flag(idx);
1191            lo
1192        } else {
1193            value
1194        };
1195        sat as i16
1196    }
1197
1198    /// Like [`saturate_ir`] but only sets the flag -- does not store.
1199    /// Used when the flag for one IR slot needs to reflect a different
1200    /// value than the one stored (RTPS IR3 quirk, MVMVA-FC quirk).
1201    fn saturate_ir_flag_only(&mut self, idx: u8, value: i32, lm: bool) -> i16 {
1202        let lo = if lm { 0 } else { -0x8000 };
1203        let hi = 0x7FFFi32;
1204        if value > hi || value < lo {
1205            self.set_ir_flag(idx);
1206        }
1207        value.clamp(lo, hi) as i16
1208    }
1209
1210    /// Saturate-to-IR without touching the FLAG. Used when the flag was
1211    /// already set by [`saturate_ir_flag_only`].
1212    fn saturate_value_for_ir(&self, value: i32, lm: bool) -> i16 {
1213        let lo = if lm { 0 } else { -0x8000 };
1214        value.clamp(lo, 0x7FFF) as i16
1215    }
1216
1217    fn set_ir_flag(&mut self, idx: u8) {
1218        self.flag |= match idx {
1219            1 => flag::IR1_SAT,
1220            2 => flag::IR2_SAT,
1221            3 => flag::IR3_SAT,
1222            _ => 0,
1223        };
1224    }
1225
1226    fn saturate_ir0(&mut self, value: i32) -> i16 {
1227        if value < 0 {
1228            self.flag |= flag::IR0_SAT;
1229            0
1230        } else if value > 0x1000 {
1231            self.flag |= flag::IR0_SAT;
1232            0x1000
1233        } else {
1234            value as i16
1235        }
1236    }
1237
1238    /// Saturate `value` to -1024..1023 and set the appropriate screen
1239    /// flag (`is_x` chooses SX2 vs SY2).
1240    fn saturate_screen(&mut self, value: i64, is_x: bool) -> i16 {
1241        let lo = -0x400i64;
1242        let hi = 0x3FFi64;
1243        let bit = if is_x { flag::SX2_SAT } else { flag::SY2_SAT };
1244        if value > hi {
1245            self.flag |= bit;
1246            hi as i16
1247        } else if value < lo {
1248            self.flag |= bit;
1249            lo as i16
1250        } else {
1251            value as i16
1252        }
1253    }
1254
1255    fn saturate_otz(&mut self, value: i64) -> u16 {
1256        if value < 0 {
1257            self.flag |= flag::SZ3_OTZ_SAT;
1258            0
1259        } else if value > 0xFFFF {
1260            self.flag |= flag::SZ3_OTZ_SAT;
1261            0xFFFF
1262        } else {
1263            value as u16
1264        }
1265    }
1266
1267    /// Push a Z value onto SZ FIFO with 0..0xFFFF saturation.
1268    fn push_sz(&mut self, value: i32) {
1269        let z = if value < 0 {
1270            self.flag |= flag::SZ3_OTZ_SAT;
1271            0
1272        } else if value > 0xFFFF {
1273            self.flag |= flag::SZ3_OTZ_SAT;
1274            0xFFFF
1275        } else {
1276            value as u16
1277        };
1278        self.sz[0] = self.sz[1];
1279        self.sz[1] = self.sz[2];
1280        self.sz[2] = self.sz[3];
1281        self.sz[3] = z;
1282    }
1283
1284    /// Push an XY pair onto the SXY FIFO.
1285    fn push_sxy(&mut self, x: i16, y: i16) {
1286        self.sxy[0] = self.sxy[1];
1287        self.sxy[1] = self.sxy[2];
1288        self.sxy[2] = [x, y];
1289    }
1290
1291    /// Pull MAC1/2/3 into a saturated RGB and push onto the colour FIFO.
1292    /// `code` is taken from the current RGBC.
1293    fn push_color_from_mac(&mut self, _cmd: Cmd) {
1294        let r = self.saturate_color(self.mac[0] >> 4, flag::COLOR_R_SAT);
1295        let g = self.saturate_color(self.mac[1] >> 4, flag::COLOR_G_SAT);
1296        let b = self.saturate_color(self.mac[2] >> 4, flag::COLOR_B_SAT);
1297        let code = self.rgbc[3];
1298        self.rgb_fifo[0] = self.rgb_fifo[1];
1299        self.rgb_fifo[1] = self.rgb_fifo[2];
1300        self.rgb_fifo[2] = [r, g, b, code];
1301    }
1302
1303    fn saturate_color(&mut self, value: i32, flag_bit: u32) -> u8 {
1304        if value < 0 {
1305            self.flag |= flag_bit;
1306            0
1307        } else if value > 0xFF {
1308            self.flag |= flag_bit;
1309            0xFF
1310        } else {
1311            value as u8
1312        }
1313    }
1314
1315    /// Newton-Raphson divide: returns `clamp((H<<16) / SZ3, 0..0x1FFFF)`.
1316    /// Sets [`flag::DIV_OVERFLOW`] if `SZ3 == 0` or `H >= 2*SZ3`.
1317    fn unr_divide(&mut self) -> u32 {
1318        let h = self.h as u32;
1319        let sz3 = self.sz[3] as u32;
1320        if h >= sz3 * 2 {
1321            self.flag |= flag::DIV_OVERFLOW;
1322            return 0x1FFFF;
1323        }
1324        let z = (sz3 as u16).leading_zeros();
1325        let n = h << z;
1326        let d = sz3 << z;
1327        // Table index is `(d - 0x7FC0) >> 7`, range 0..=256 (UNR_TABLE
1328        // has 257 entries). A `& 0xFF` mask aliases index 256 →
1329        // index 0, which corrupts the reciprocal whenever the
1330        // normalised divisor lands in {0xFFFE, 0xFFFF}: table[256]=0x00
1331        // gives u=0x101 (smallest seed) but table[0]=0xFF gives
1332        // u=0x200 (largest), producing a divisor ~2000× too small and
1333        // collapsing projected vertices toward the screen offset.
1334        // Matches Redux's `gte_divide` and PSX-SPX exactly.
1335        let table_index = (d.wrapping_sub(0x7FC0) >> 7) as usize;
1336        let u = (UNR_TABLE[table_index] as u32) + 0x101;
1337        let d = (0x2000080u32.wrapping_sub(d.wrapping_mul(u))) >> 8;
1338        let d = (0x80u32.wrapping_add(d.wrapping_mul(u))) >> 8;
1339        let result = (((n as u64) * (d as u64)) + 0x8000) >> 16;
1340        cmp::min(0x1FFFF, result as u32)
1341    }
1342
1343    /// Recompute the master `FLAG[31]` bit from the error-mask OR.
1344    fn update_flag_master(&mut self) {
1345        if self.flag & flag::ERROR_MASK != 0 {
1346            self.flag |= 1 << 31;
1347        }
1348    }
1349
1350    fn select_matrix(&self, mx: u8) -> [[i16; 3]; 3] {
1351        match mx {
1352            0 => self.rotation,
1353            1 => self.light,
1354            2 => self.light_color,
1355            // mx=3 is the documented "garbage matrix" (PSX-SPX: GTE
1356            // Opcodes Summary). Row 0 col 2 is **IR0** (data reg 8),
1357            // not IR1 -- catching this was why `mvmva_mx3_garbage_matrix`
1358            // got added. Rows 1 and 2 replicate RT[0][2] and RT[1][1]
1359            // across all three columns.
1360            _ => {
1361                let r = (self.rgbc[0] as i16) << 4;
1362                [
1363                    [-r, r, self.ir0],
1364                    [
1365                        self.rotation[0][2],
1366                        self.rotation[0][2],
1367                        self.rotation[0][2],
1368                    ],
1369                    [
1370                        self.rotation[1][1],
1371                        self.rotation[1][1],
1372                        self.rotation[1][1],
1373                    ],
1374                ]
1375            }
1376        }
1377    }
1378
1379    fn select_vector(&self, vx: u8) -> [i16; 3] {
1380        match vx {
1381            0 => self.v[0],
1382            1 => self.v[1],
1383            2 => self.v[2],
1384            _ => self.ir,
1385        }
1386    }
1387
1388    fn select_translation(&self, cv: u8) -> [i32; 3] {
1389        match cv {
1390            0 => self.translation,
1391            1 => self.bg_color,
1392            2 => self.far_color,
1393            _ => [0, 0, 0],
1394        }
1395    }
1396}
1397
1398impl Default for Gte {
1399    fn default() -> Self {
1400        Self::new()
1401    }
1402}
1403
1404// ---------------------------------------------------------------------
1405// Bit-packing helpers
1406// ---------------------------------------------------------------------
1407
1408fn pack_xy_i16(x: i16, y: i16) -> u32 {
1409    ((y as u16 as u32) << 16) | (x as u16 as u32)
1410}
1411
1412/// Unpack the low/high halves of a packed XY word into two `i16`s.
1413fn unpack_xy_i16(value: u32) -> (i16, i16) {
1414    (value as i16, (value >> 16) as i16)
1415}
1416
1417fn sign_extend_16(value: i16) -> u32 {
1418    value as i32 as u32
1419}
1420
1421fn pack_irgb(ir: &[i16; 3]) -> u32 {
1422    let r = ir[0].clamp(0, 0xF80) >> 7;
1423    let g = ir[1].clamp(0, 0xF80) >> 7;
1424    let b = ir[2].clamp(0, 0xF80) >> 7;
1425    ((b as u32) << 10) | ((g as u32) << 5) | (r as u32)
1426}
1427
1428/// LZCR algorithm: count the run of leading bits matching the sign bit.
1429/// For positive values that's leading zeros; for negative values it's
1430/// leading ones. The returned count is in 1..=32.
1431fn leading_count_signed(value: u32) -> u32 {
1432    let test = if (value as i32) < 0 { !value } else { value };
1433    if test == 0 {
1434        32
1435    } else {
1436        test.leading_zeros()
1437    }
1438}
1439
1440// ---------------------------------------------------------------------
1441// UNR division seed table (PSX-SPX section "GTE Division Inaccuracy")
1442// ---------------------------------------------------------------------
1443
1444/// 257-entry seed for the Newton-Raphson divider. Indexed by
1445/// `(d - 0x7FC0) >> 7` after `d` has been left-aligned. The final
1446/// entry is required so the index can saturate without bounds checks.
1447#[rustfmt::skip]
1448static UNR_TABLE: [u8; 257] = [
1449    0xFF,0xFD,0xFB,0xF9,0xF7,0xF5,0xF3,0xF1, 0xEF,0xEE,0xEC,0xEA,0xE8,0xE6,0xE4,0xE3,
1450    0xE1,0xDF,0xDD,0xDC,0xDA,0xD8,0xD6,0xD5, 0xD3,0xD1,0xD0,0xCE,0xCD,0xCB,0xC9,0xC8,
1451    0xC6,0xC5,0xC3,0xC1,0xC0,0xBE,0xBD,0xBB, 0xBA,0xB8,0xB7,0xB5,0xB4,0xB2,0xB1,0xB0,
1452    0xAE,0xAD,0xAB,0xAA,0xA9,0xA7,0xA6,0xA4, 0xA3,0xA2,0xA0,0x9F,0x9E,0x9C,0x9B,0x9A,
1453    0x99,0x97,0x96,0x95,0x94,0x92,0x91,0x90, 0x8F,0x8D,0x8C,0x8B,0x8A,0x89,0x87,0x86,
1454    0x85,0x84,0x83,0x82,0x81,0x7F,0x7E,0x7D, 0x7C,0x7B,0x7A,0x79,0x78,0x77,0x75,0x74,
1455    0x73,0x72,0x71,0x70,0x6F,0x6E,0x6D,0x6C, 0x6B,0x6A,0x69,0x68,0x67,0x66,0x65,0x64,
1456    0x63,0x62,0x61,0x60,0x5F,0x5E,0x5D,0x5D, 0x5C,0x5B,0x5A,0x59,0x58,0x57,0x56,0x55,
1457    0x54,0x53,0x53,0x52,0x51,0x50,0x4F,0x4E, 0x4D,0x4D,0x4C,0x4B,0x4A,0x49,0x48,0x48,
1458    0x47,0x46,0x45,0x44,0x43,0x43,0x42,0x41, 0x40,0x3F,0x3F,0x3E,0x3D,0x3C,0x3C,0x3B,
1459    0x3A,0x39,0x39,0x38,0x37,0x36,0x36,0x35, 0x34,0x33,0x33,0x32,0x31,0x31,0x30,0x2F,
1460    0x2E,0x2E,0x2D,0x2C,0x2C,0x2B,0x2A,0x2A, 0x29,0x28,0x28,0x27,0x26,0x26,0x25,0x24,
1461    0x24,0x23,0x22,0x22,0x21,0x20,0x20,0x1F, 0x1E,0x1E,0x1D,0x1D,0x1C,0x1B,0x1B,0x1A,
1462    0x19,0x19,0x18,0x18,0x17,0x16,0x16,0x15, 0x15,0x14,0x14,0x13,0x12,0x12,0x11,0x11,
1463    0x10,0x0F,0x0F,0x0E,0x0E,0x0D,0x0D,0x0C, 0x0C,0x0B,0x0A,0x0A,0x09,0x09,0x08,0x08,
1464    0x07,0x07,0x06,0x06,0x05,0x05,0x04,0x04, 0x03,0x03,0x02,0x02,0x01,0x01,0x00,0x00,
1465    0x00,
1466];
1467
1468#[cfg(test)]
1469mod tests {
1470    use super::*;
1471
1472    /// HWB-010 silicon values: joint 9's compose column 1 from the SC 2147
1473    /// live capture. With the stale V0.x injected (previous column's x = 0x0218),
1474    /// the MAC1 phase
1475    /// reproduces the photographed hardware value EXACTLY while
1476    /// MAC2/MAC3 stay fresh; without injection, all three match the
1477    /// clean compose.
1478    #[test]
1479    fn mvmva_stale_v0x_reproduces_hwb010_compose_column() {
1480        let seed = |g: &mut Gte| {
1481            // VI rotation: (0F6D 0000 FBC1 / 0125 F098 0427 / FBEA FBB1 F125)
1482            g.write_control(0, 0x0000_0F6D);
1483            g.write_control(1, 0x0125_FBC1);
1484            g.write_control(2, 0x0427_F098);
1485            g.write_control(3, 0xFBB1_FBEA);
1486            g.write_control(4, 0xFFFF_F125);
1487            g.write_control(5, 0);
1488            g.write_control(6, 0);
1489            g.write_control(7, 0);
1490            // V0 = column 1 of the joint-9 model matrix: (-27, 536, 8).
1491            g.write_data(0, 0x0218_FFE5);
1492            g.write_data(1, 0x0000_0008);
1493        };
1494        const MVMVA_RT_V0_TR_SF1: u32 = 0x4A08_0012;
1495
1496        // Clean execute: the console-exact compose values.
1497        let mut g = Gte::new();
1498        seed(&mut g);
1499        g.execute(MVMVA_RT_V0_TR_SF1);
1500        assert_eq!(g.read_data(25), 0xFFFF_FFE3); // -29
1501        assert_eq!(g.read_data(26), 0xFFFF_FDFC); // -516
1502        assert_eq!(g.read_data(27), 0xFFFF_FF6F); // -145
1503
1504        // Hazard execute: stale x = previous column's x (0x0218 = 536).
1505        // MAC1 flips to the photographed corrupt value; MAC2/3 unchanged.
1506        let mut g = Gte::new();
1507        seed(&mut g);
1508        g.execute_with_stale_v0x(MVMVA_RT_V0_TR_SF1, 0x0218);
1509        assert_eq!(g.read_data(25), 0x0000_0202); // 514 -- hardware photo
1510        assert_eq!(g.read_data(26), 0xFFFF_FDFC);
1511        assert_eq!(g.read_data(27), 0xFFFF_FF6F);
1512
1513        // The injection is one-shot: a following clean execute is clean.
1514        g.execute(MVMVA_RT_V0_TR_SF1);
1515        assert_eq!(g.read_data(25), 0xFFFF_FFE3);
1516    }
1517
1518    #[test]
1519    fn rtps_stale_v0x_reproduces_unpadded_projection_hazard() {
1520        let seed = |g: &mut Gte| {
1521            // Identity rotation, H=160, V0=(10, 0, 160). With H/Z=1 the
1522            // projected X is the input X, making stale/fresh selection exact.
1523            g.write_control(0, 0x0000_1000);
1524            g.write_control(2, 0x0000_1000);
1525            g.write_control(4, 0x0000_1000);
1526            g.write_control(26, 160);
1527            g.write_data(0, 10);
1528            g.write_data(1, 160);
1529        };
1530        const RTPS_SF1: u32 = 0x4A08_0001;
1531
1532        let mut clean = Gte::new();
1533        seed(&mut clean);
1534        clean.execute(RTPS_SF1);
1535        assert_eq!(clean.read_data(14) as u16 as i16, 10);
1536
1537        let mut hazarded = Gte::new();
1538        seed(&mut hazarded);
1539        hazarded.execute_with_stale_v0x(RTPS_SF1, -10);
1540        assert_eq!(hazarded.read_data(14) as u16 as i16, -10);
1541    }
1542
1543    #[test]
1544    fn reset_state_is_zero() {
1545        let g = Gte::new();
1546        for i in 0..32 {
1547            // Including LZCR (reg 31): Redux memsets register storage
1548            // to zero on boot and only updates LZCR when LZCS is
1549            // written. We mirror that so step-0 parity traces match.
1550            assert_eq!(g.read_data(i), 0, "data reg {i}");
1551        }
1552        for i in 0..31 {
1553            assert_eq!(g.read_control(i), 0, "ctrl reg {i} should reset to 0");
1554        }
1555    }
1556
1557    #[test]
1558    fn data_reg_v0_xy_round_trip() {
1559        let mut g = Gte::new();
1560        g.write_data(0, 0xDEAD_BEEF);
1561        // Stored as two i16; reads back the same packed word.
1562        assert_eq!(g.read_data(0), 0xDEAD_BEEF);
1563    }
1564
1565    #[test]
1566    fn data_reg_vz0_sign_extends_on_read() {
1567        let mut g = Gte::new();
1568        g.write_data(1, 0x0000_8000);
1569        assert_eq!(g.read_data(1), 0xFFFF_8000);
1570    }
1571
1572    #[test]
1573    fn sxyp_write_pushes_fifo() {
1574        let mut g = Gte::new();
1575        g.write_data(12, pack_xy_i16(1, 2)); // SXY0
1576        g.write_data(13, pack_xy_i16(3, 4)); // SXY1
1577        g.write_data(14, pack_xy_i16(5, 6)); // SXY2
1578        g.write_data(15, pack_xy_i16(7, 8)); // SXYP -- pushes
1579        assert_eq!(g.read_data(12), pack_xy_i16(3, 4));
1580        assert_eq!(g.read_data(13), pack_xy_i16(5, 6));
1581        assert_eq!(g.read_data(14), pack_xy_i16(7, 8));
1582    }
1583
1584    #[test]
1585    fn lzcs_write_recomputes_lzcr() {
1586        let mut g = Gte::new();
1587        g.write_data(30, 0x0000_FFFF);
1588        assert_eq!(g.read_data(31), 16);
1589        g.write_data(30, 0xFFFF_0000);
1590        // Negative -- count of leading 1-bits.
1591        assert_eq!(g.read_data(31), 16);
1592        g.write_data(30, 0);
1593        assert_eq!(g.read_data(31), 32);
1594        g.write_data(30, !0);
1595        assert_eq!(g.read_data(31), 32);
1596    }
1597
1598    #[test]
1599    fn ctrl_h_reads_back_sign_extended() {
1600        let mut g = Gte::new();
1601        g.write_control(26, 0x0000_8000);
1602        assert_eq!(g.read_control(26), 0xFFFF_8000);
1603    }
1604
1605    #[test]
1606    fn flag_reset_at_start_of_each_op() {
1607        let mut g = Gte::new();
1608        // Force a stale flag.
1609        g.write_control(31, flag::MAC1_POS);
1610        assert_eq!(g.read_control(31) & flag::MAC1_POS, flag::MAC1_POS);
1611        // NCLIP clears + recomputes; with all zeros it produces 0.
1612        g.execute(0x06); // NCLIP, sf=0
1613        assert_eq!(g.read_control(31), 0);
1614    }
1615
1616    #[test]
1617    fn rtps_identity_transform_against_origin() {
1618        // With RT = identity and TR = 0, RTPS of V0=(0,0,0) should
1619        // produce IR = 0, MAC = 0, push (0,0,0) onto SZ FIFO. SZ3 = 0
1620        // → divide overflow flag set, SX2/SY2 saturate.
1621        let mut g = Gte::new();
1622        // Identity rotation, scaled by 0x1000 (1.0 in 1.3.12).
1623        g.write_control(0, pack_xy_i16(0x1000, 0));
1624        g.write_control(1, pack_xy_i16(0, 0x1000));
1625        g.write_control(2, pack_xy_i16(0, 0));
1626        g.write_control(3, pack_xy_i16(0, 0));
1627        g.write_control(4, 0x1000);
1628        // V0 = (0, 0, 0)
1629        g.execute(0x0180_0001); // RTPS sf=1
1630                                // Flag should record divide overflow (sz3=0, h=0 still triggers).
1631        assert!(g.read_control(31) & flag::DIV_OVERFLOW != 0);
1632    }
1633
1634    #[test]
1635    fn nclip_computes_z_cross_product() {
1636        // SXY0 = (0,0), SXY1 = (10,0), SXY2 = (0,10):
1637        // result = 0*(0-10) + 10*(10-0) + 0*(0-0) = 100
1638        let mut g = Gte::new();
1639        g.write_data(12, pack_xy_i16(0, 0));
1640        g.write_data(13, pack_xy_i16(10, 0));
1641        g.write_data(14, pack_xy_i16(0, 10));
1642        g.execute(0x06); // NCLIP
1643        assert_eq!(g.read_data(24) as i32, 100);
1644    }
1645
1646    #[test]
1647    fn avsz3_averages_with_zsf3() {
1648        let mut g = Gte::new();
1649        g.write_data(17, 0x0100); // SZ1
1650        g.write_data(18, 0x0200); // SZ2
1651        g.write_data(19, 0x0300); // SZ3
1652                                  // ZSF3 = 0x555 -- close enough to 0x1000/3 to land OTZ near the
1653                                  // simple arithmetic mean.
1654        g.write_control(29, 0x0555);
1655        g.execute(0x2D); // AVSZ3
1656                         // MAC0 = 0x555 * (0x100 + 0x200 + 0x300) = 0x555 * 0x600 = 0x1FFE00.
1657                         // OTZ = 0x1FFE00 >> 12 = 0x1FF.
1658        assert_eq!(g.read_data(24) as i32, 0x1FFE00);
1659        assert_eq!(g.read_data(7), 0x1FF);
1660    }
1661
1662    #[test]
1663    fn sqr_squares_ir() {
1664        let mut g = Gte::new();
1665        g.write_data(9, 0x10);
1666        g.write_data(10, 0x20);
1667        g.write_data(11, 0x30);
1668        g.execute(0x28); // SQR sf=0
1669        assert_eq!(g.read_data(25), 0x100);
1670        assert_eq!(g.read_data(26), 0x400);
1671        assert_eq!(g.read_data(27), 0x900);
1672    }
1673
1674    #[test]
1675    fn op_decode_extracts_command_word_fields() {
1676        // sf=1, lm=1, mx=2, vx=1, cv=0, opcode=0x12 → instr =
1677        //   (1<<19) | (1<<10) | (2<<17) | (1<<15) | (0<<13) | 0x12
1678        let instr = (1u32 << 19) | (1 << 10) | (2 << 17) | (1 << 15) | 0x12;
1679        let cmd = Cmd::decode(instr);
1680        assert!(cmd.sf);
1681        assert!(cmd.lm);
1682        assert_eq!(cmd.mx, 2);
1683        assert_eq!(cmd.vx, 1);
1684        assert_eq!(cmd.cv, 0);
1685    }
1686
1687    #[test]
1688    fn unknown_function_is_silently_ignored() {
1689        let mut g = Gte::new();
1690        g.execute(0x00); // not a valid GTE op
1691        assert_eq!(g.read_control(31), 0);
1692    }
1693
1694    #[test]
1695    fn profile_counts_recognised_commands_only() {
1696        let mut g = Gte::new();
1697
1698        g.execute(0x01); // RTPS
1699        g.execute(0x30); // RTPT
1700        g.execute(0x00); // undefined nop
1701
1702        let profile = g.profile_snapshot();
1703        assert_eq!(profile.ops, 2);
1704        assert_eq!(profile.estimated_cycles, 15 + 23);
1705        assert_eq!(profile.opcode_counts[0x01], 1);
1706        assert_eq!(profile.opcode_counts[0x30], 1);
1707        assert_eq!(profile.opcode_counts[0x00], 0);
1708    }
1709
1710    // ------------------------------------------------------------------
1711    // Broader known-value fixtures -- verify arithmetic for the ops games
1712    // use most, plus the register-view edge cases. Every expected number
1713    // below comes from hand-computed PSX-SPX math on small inputs; if one
1714    // of these fires it means a refactor has drifted the implementation.
1715    // ------------------------------------------------------------------
1716
1717    /// Install the identity rotation matrix scaled 1.0 in 1.3.12 (i.e.
1718    /// every diagonal = 0x1000, off-diagonal = 0). Keeps RTPS/MVMVA
1719    /// fixtures readable: `MAC = V` in the shifted space.
1720    fn install_identity_rotation(g: &mut Gte) {
1721        g.write_control(0, pack_xy_i16(0x1000, 0));
1722        g.write_control(1, pack_xy_i16(0, 0));
1723        g.write_control(2, pack_xy_i16(0x1000, 0));
1724        g.write_control(3, pack_xy_i16(0, 0));
1725        g.write_control(4, 0x1000);
1726    }
1727
1728    /// GTE command-word builder: COP2 functions leave the high bits
1729    /// alone -- only sf/lm/mx/vx/cv/opcode matter. Keeps test instructions
1730    /// readable next to their intent.
1731    fn cmd_word(sf: bool, lm: bool, mx: u8, vx: u8, cv: u8, opcode: u8) -> u32 {
1732        ((sf as u32) << 19)
1733            | ((mx as u32 & 3) << 17)
1734            | ((vx as u32 & 3) << 15)
1735            | ((cv as u32 & 3) << 13)
1736            | ((lm as u32) << 10)
1737            | (opcode as u32)
1738    }
1739
1740    #[test]
1741    fn rtps_with_h_zero_projects_to_screen_offset() {
1742        // With H=0 the divisor collapses to 0, so the projected X/Y are
1743        // just OFX>>16 and OFY>>16 -- a crisp fixture for checking every
1744        // RTPS side-effect (MAC1/2/3, IR1/2/3, SZ push, SXY push, MAC0,
1745        // IR0) without the Newton-Raphson divider in the way.
1746        let mut g = Gte::new();
1747        install_identity_rotation(&mut g);
1748        g.write_data(0, pack_xy_i16(1, 2)); // V0.x, V0.y
1749        g.write_data(1, 3); // V0.z
1750        g.write_control(24, 0x0100_0000); // OFX = 256.0 in 15.16
1751        g.write_control(25, 0x0200_0000); // OFY = 512.0 in 15.16
1752        g.write_control(26, 0); // H = 0
1753        g.write_control(27, 100); // DQA
1754        g.write_control(28, 5000); // DQB
1755        g.execute(cmd_word(true, false, 0, 0, 0, 0x01));
1756        // MAC = V (identity × V >> 12 = V for these small inputs)
1757        assert_eq!(g.read_data(25) as i32, 1, "MAC1");
1758        assert_eq!(g.read_data(26) as i32, 2, "MAC2");
1759        assert_eq!(g.read_data(27) as i32, 3, "MAC3");
1760        assert_eq!(g.read_data(9), 1, "IR1");
1761        assert_eq!(g.read_data(10), 2, "IR2");
1762        assert_eq!(g.read_data(11), 3, "IR3");
1763        // SZ3 pushed from mac[2]=3 (sf=1 path).
1764        assert_eq!(g.read_data(19), 3, "SZ3");
1765        // H=0 ⇒ divisor=0 ⇒ SXY2 = (OFX>>16, OFY>>16).
1766        assert_eq!(g.read_data(14), pack_xy_i16(0x100, 0x200), "SXY2");
1767        // MAC0 = divisor * DQA + DQB = 0 + 5000.
1768        assert_eq!(g.read_data(24) as i32, 5000, "MAC0");
1769        // IR0 = MAC0 >> 12 saturated to 0..0x1000.
1770        assert_eq!(g.read_data(8), 1, "IR0");
1771        // H<2*SZ3 so no DIV_OVERFLOW.
1772        assert_eq!(g.read_control(31) & flag::DIV_OVERFLOW, 0);
1773    }
1774
1775    #[test]
1776    fn rtpt_transforms_all_three_vertices_and_pushes_sz_fifo() {
1777        let mut g = Gte::new();
1778        install_identity_rotation(&mut g);
1779        g.write_data(0, pack_xy_i16(1, 2));
1780        g.write_data(1, 3); // V0
1781        g.write_data(2, pack_xy_i16(4, 5));
1782        g.write_data(3, 6); // V1
1783        g.write_data(4, pack_xy_i16(7, 8));
1784        g.write_data(5, 9); // V2
1785        g.write_control(24, 0);
1786        g.write_control(25, 0);
1787        g.write_control(26, 0); // H=0 keeps projection math simple.
1788        g.execute(cmd_word(true, false, 0, 0, 0, 0x30));
1789        // Final IR is from the *last* vertex transformed (V2 = (7,8,9)).
1790        assert_eq!(g.read_data(9), 7);
1791        assert_eq!(g.read_data(10), 8);
1792        assert_eq!(g.read_data(11), 9);
1793        // SZ FIFO after three pushes of 3, 6, 9.
1794        assert_eq!(g.read_data(17), 3, "SZ1");
1795        assert_eq!(g.read_data(18), 6, "SZ2");
1796        assert_eq!(g.read_data(19), 9, "SZ3");
1797    }
1798
1799    #[test]
1800    fn mvmva_rt_v0_tr_computes_rotation_plus_translation() {
1801        let mut g = Gte::new();
1802        install_identity_rotation(&mut g);
1803        g.write_data(0, pack_xy_i16(1, 2));
1804        g.write_data(1, 3);
1805        g.write_control(5, 10);
1806        g.write_control(6, 20);
1807        g.write_control(7, 30);
1808        g.execute(cmd_word(true, false, 0, 0, 0, 0x12));
1809        // MAC_i = ((TR_i<<12) + RT_row_i · V) >> 12 = TR_i + V_i.
1810        assert_eq!(g.read_data(25) as i32, 11, "MAC1 = TR0 + V0.x");
1811        assert_eq!(g.read_data(26) as i32, 22, "MAC2 = TR1 + V0.y");
1812        assert_eq!(g.read_data(27) as i32, 33, "MAC3 = TR2 + V0.z");
1813        assert_eq!(g.read_data(9), 11);
1814        assert_eq!(g.read_data(10), 22);
1815        assert_eq!(g.read_data(11), 33);
1816    }
1817
1818    #[test]
1819    fn mvmva_cv3_drops_translation() {
1820        let mut g = Gte::new();
1821        install_identity_rotation(&mut g);
1822        g.write_data(0, pack_xy_i16(1, 2));
1823        g.write_data(1, 3);
1824        g.write_control(5, 100); // TR should be ignored with cv=3.
1825        g.write_control(6, 200);
1826        g.write_control(7, 300);
1827        g.execute(cmd_word(true, false, 0, 0, 3, 0x12));
1828        assert_eq!(g.read_data(25) as i32, 1, "MAC1 = V.x (no TR)");
1829        assert_eq!(g.read_data(26) as i32, 2, "MAC2 = V.y");
1830        assert_eq!(g.read_data(27) as i32, 3, "MAC3 = V.z");
1831    }
1832
1833    #[test]
1834    fn avsz4_averages_using_zsf4() {
1835        let mut g = Gte::new();
1836        g.write_data(16, 0x010);
1837        g.write_data(17, 0x020);
1838        g.write_data(18, 0x030);
1839        g.write_data(19, 0x040);
1840        g.write_control(30, 0x0400); // ZSF4 = 0x400 (1/4 in 0.12)
1841        g.execute(0x2E);
1842        // MAC0 = ZSF4 * (SZ0+SZ1+SZ2+SZ3) = 0x400 * 0xA0 = 0x28000.
1843        assert_eq!(g.read_data(24) as i32, 0x28000, "MAC0");
1844        assert_eq!(g.read_data(7), 0x28, "OTZ = MAC0>>12");
1845    }
1846
1847    #[test]
1848    fn irgb_write_unpacks_into_ir_vector() {
1849        // IRGB packed (B<<10) | (G<<5) | R. Each 5-bit component scales
1850        // to IR_i = component << 7, matching PSX-SPX.
1851        let mut g = Gte::new();
1852        g.write_data(28, (0x0A << 10) | (0x10 << 5) | 0x1F);
1853        assert_eq!(g.read_data(9) as i16, 0x1F << 7);
1854        assert_eq!(g.read_data(10) as i16, 0x10 << 7);
1855        assert_eq!(g.read_data(11) as i16, 0x0A << 7);
1856    }
1857
1858    #[test]
1859    fn orgb_packs_ir_into_5_5_5() {
1860        let mut g = Gte::new();
1861        g.write_data(9, 0); // R=0
1862        g.write_data(10, 0xF80); // G=0x1F (max)
1863        g.write_data(11, 0x780); // B=0x0F
1864        assert_eq!(g.read_data(29), (0x0F << 10) | (0x1F << 5));
1865    }
1866
1867    #[test]
1868    fn orgb_write_is_read_only() {
1869        let mut g = Gte::new();
1870        g.write_data(9, 0x500);
1871        g.write_data(29, 0xFFFF); // Writes to ORGB are silently dropped.
1872        assert_eq!(g.read_data(9) as i16, 0x500);
1873    }
1874
1875    #[test]
1876    fn mac_overflow_sets_positive_flag() {
1877        // TR<<12 alone sits at 8.795_893e12; adding RT[0][0]·V0.x (another
1878        // ~1e9 with maxed-out i16s) tips past 2^43-1 = 8.796_093e12.
1879        let mut g = Gte::new();
1880        install_identity_rotation(&mut g);
1881        g.write_control(5, 0x7FFF_FFFF); // TR.x = i32::MAX
1882        g.write_control(0, pack_xy_i16(0x7FFF, 0)); // RT[0][0] = max i16
1883        g.write_data(0, pack_xy_i16(0x7FFF, 0)); // V0.x = max i16
1884        g.execute(cmd_word(false, false, 0, 0, 0, 0x01));
1885        assert!(g.read_control(31) & flag::MAC1_POS != 0, "MAC1_POS");
1886        assert!(g.read_control(31) & 0x8000_0000 != 0, "master bit");
1887    }
1888
1889    #[test]
1890    fn gpf_multiplies_ir_by_ir0_and_pushes_color() {
1891        let mut g = Gte::new();
1892        g.write_data(8, 0x1000); // IR0 = 1.0
1893        g.write_data(9, 0x800);
1894        g.write_data(10, 0x1000); // will saturate color G
1895        g.write_data(11, 0x200);
1896        g.execute(cmd_word(true, false, 0, 0, 0, 0x3D));
1897        assert_eq!(g.read_data(25) as i32, 0x800, "MAC1");
1898        assert_eq!(g.read_data(26) as i32, 0x1000, "MAC2");
1899        assert_eq!(g.read_data(27) as i32, 0x200, "MAC3");
1900        // Newest color: (r,g,b,code) = (0x80, 0xFF, 0x20, 0) packed LE.
1901        // 0x1000>>4 = 0x100 → clamps to 0xFF, setting COLOR_G_SAT.
1902        assert_eq!(g.read_data(22), 0x0020_FF80);
1903        assert!(g.read_control(31) & flag::COLOR_G_SAT != 0);
1904    }
1905
1906    #[test]
1907    fn ncs_does_not_modulate_by_rgbc() {
1908        // Redux/PSX NCS emits the lit colour after LCM+BK directly.
1909        // NCCS/NCDS/CC are the opcodes that multiply by RGBC. This
1910        // distinction matters for Crash's Naughty Dog logo, whose
1911        // material RGBC can be tiny while the lit colour should remain
1912        // visible.
1913        let mut g = Gte::new();
1914        g.write_data(0, pack_xy_i16(0x1000, 0));
1915        g.write_data(1, 0);
1916        g.write_data(6, 0x0001_0101); // tiny RGBC; NCS must ignore it.
1917
1918        // LLM row 0 = [1,0,0], LCM row 0 = [0.25,0,0].
1919        g.write_control(8, pack_xy_i16(0x1000, 0));
1920        g.write_control(16, pack_xy_i16(0x0400, 0));
1921
1922        g.execute(cmd_word(true, false, 0, 0, 0, 0x1E)); // NCS sf=1
1923
1924        assert_eq!(g.read_data(25) as i32, 0x0400, "MAC1 after LCM");
1925        assert_eq!(g.read_data(22), 0x0000_0040, "RGB2 should ignore tiny RGBC");
1926    }
1927
1928    #[test]
1929    fn dpcs_far_color_uses_twelve_bit_fractional_scale() {
1930        let mut g = Gte::new();
1931        g.write_data(6, 0x0000_0000); // RGBC = black
1932        g.write_data(8, 0x1000); // IR0 = 1.0
1933        g.write_control(21, 0x0100); // RFC
1934        g.write_control(22, 0x0200); // GFC
1935        g.write_control(23, 0x0300); // BFC
1936
1937        g.execute(cmd_word(true, false, 0, 0, 0, 0x10)); // DPCS sf=1
1938
1939        assert_eq!(g.read_data(25) as i32, 0x0100, "MAC1");
1940        assert_eq!(g.read_data(26) as i32, 0x0200, "MAC2");
1941        assert_eq!(g.read_data(27) as i32, 0x0300, "MAC3");
1942        assert_eq!(g.read_data(22), 0x0030_2010, "RGB2");
1943    }
1944
1945    #[test]
1946    fn intpl_far_color_uses_twelve_bit_fractional_scale() {
1947        let mut g = Gte::new();
1948        g.write_data(8, 0x1000); // IR0 = 1.0
1949        g.write_data(9, 0);
1950        g.write_data(10, 0);
1951        g.write_data(11, 0);
1952        g.write_control(21, 0x0100); // RFC
1953        g.write_control(22, 0x0200); // GFC
1954        g.write_control(23, 0x0300); // BFC
1955
1956        g.execute(cmd_word(true, false, 0, 0, 0, 0x11)); // INTPL sf=1
1957
1958        assert_eq!(g.read_data(25) as i32, 0x0100, "MAC1");
1959        assert_eq!(g.read_data(26) as i32, 0x0200, "MAC2");
1960        assert_eq!(g.read_data(27) as i32, 0x0300, "MAC3");
1961        assert_eq!(g.read_data(22), 0x0030_2010, "RGB2");
1962    }
1963
1964    #[test]
1965    fn gpl_accumulates_base_mac_into_product() {
1966        // GPL reshifts the existing MAC left by sf before adding the new
1967        // product so the base acts as the same fixed-point scale.
1968        let mut g = Gte::new();
1969        g.write_data(25, 0x100);
1970        g.write_data(26, 0x200);
1971        g.write_data(27, 0x300);
1972        g.write_data(8, 0x1000); // IR0
1973        g.write_data(9, 0x400);
1974        g.write_data(10, 0x500);
1975        g.write_data(11, 0x600);
1976        g.execute(cmd_word(true, false, 0, 0, 0, 0x3E));
1977        assert_eq!(g.read_data(25) as i32, 0x500, "MAC1 = 0x100 + 0x400");
1978        assert_eq!(g.read_data(26) as i32, 0x700, "MAC2 = 0x200 + 0x500");
1979        assert_eq!(g.read_data(27) as i32, 0x900, "MAC3 = 0x300 + 0x600");
1980    }
1981
1982    #[test]
1983    fn flag_master_bit_set_on_any_error() {
1984        let mut g = Gte::new();
1985        g.write_control(31, flag::IR1_SAT);
1986        assert_eq!(g.read_control(31) & flag::IR1_SAT, flag::IR1_SAT);
1987        assert!(g.read_control(31) & 0x8000_0000 != 0);
1988    }
1989
1990    #[test]
1991    fn flag_master_bit_clear_on_non_error_bits() {
1992        // MAC0_POS lives in the error mask; a bit outside it (the RTPS
1993        // divider's SZ3_OTZ_SAT is in-mask, but there are no gaps we can
1994        // address via write_control since the mask covers bits 12..30
1995        // except bit 11 which write_control already strips). Instead we
1996        // verify: explicitly clearing produces no master bit.
1997        let mut g = Gte::new();
1998        g.write_control(31, 0);
1999        assert_eq!(g.read_control(31) & 0x8000_0000, 0);
2000    }
2001
2002    #[test]
2003    fn nclip_collinear_points_produce_zero() {
2004        // Three points on y=x line ⇒ signed area = 0 regardless of spacing.
2005        let mut g = Gte::new();
2006        g.write_data(12, pack_xy_i16(0, 0));
2007        g.write_data(13, pack_xy_i16(5, 5));
2008        g.write_data(14, pack_xy_i16(10, 10));
2009        g.execute(0x06);
2010        assert_eq!(g.read_data(24) as i32, 0);
2011    }
2012
2013    #[test]
2014    fn op_outer_product_of_ir_with_unit_diagonal() {
2015        // With diag=(1,1,1) in 1.3.12 the outer product reduces to the
2016        // standard cross-product of IR with itself-as-axis.
2017        let mut g = Gte::new();
2018        g.write_control(0, pack_xy_i16(0x1000, 0));
2019        g.write_control(2, pack_xy_i16(0x1000, 0));
2020        g.write_control(4, 0x1000);
2021        g.write_data(9, 3);
2022        g.write_data(10, 4);
2023        g.write_data(11, 5);
2024        g.execute(cmd_word(true, false, 0, 0, 0, 0x0C));
2025        // MAC1 = IR3*D2 - IR2*D3 = 5-4 = 1
2026        // MAC2 = IR1*D3 - IR3*D1 = 3-5 = -2
2027        // MAC3 = IR2*D1 - IR1*D2 = 4-3 = 1
2028        assert_eq!(g.read_data(25) as i32, 1, "MAC1");
2029        assert_eq!(g.read_data(26) as i32, -2, "MAC2");
2030        assert_eq!(g.read_data(27) as i32, 1, "MAC3");
2031        assert_eq!(g.read_data(9) as i16, 1);
2032        assert_eq!(g.read_data(10) as i16, -2);
2033        assert_eq!(g.read_data(11) as i16, 1);
2034    }
2035
2036    #[test]
2037    fn ir0_saturates_to_upper_bound() {
2038        // RTPS with H=SZ3 gives divisor exactly 1.0 (0x10000). Then
2039        // MAC0 = 0x10000 * DQA (for DQB=0) vastly exceeds 0x1000 once
2040        // shifted by 12 → IR0 clamps, flag asserts.
2041        let mut g = Gte::new();
2042        install_identity_rotation(&mut g);
2043        g.write_data(0, pack_xy_i16(0, 0));
2044        g.write_data(1, 1); // V0.z=1 so SZ3=1 post-RTPS.
2045        g.write_control(26, 1); // H=1
2046        g.write_control(27, 0x7FFF); // DQA max
2047        g.write_control(28, 0); // DQB=0 keeps MAC0 in i32 range.
2048        g.execute(cmd_word(true, false, 0, 0, 0, 0x01));
2049        assert_eq!(g.read_data(8) as i16, 0x1000, "IR0 saturated");
2050        assert!(g.read_control(31) & flag::IR0_SAT != 0);
2051    }
2052
2053    #[test]
2054    fn color_fifo_push_order_across_three_ops() {
2055        // Three GPFs with distinct IR vectors -- after three pushes,
2056        // slot 0 holds the oldest, slot 2 the newest.
2057        let mut g = Gte::new();
2058        g.write_data(8, 0x1000); // IR0 = 1.0
2059        let instr = cmd_word(true, false, 0, 0, 0, 0x3D);
2060        g.write_data(9, 0x100);
2061        g.write_data(10, 0x200);
2062        g.write_data(11, 0x300);
2063        g.execute(instr);
2064        g.write_data(9, 0x400);
2065        g.write_data(10, 0x500);
2066        g.write_data(11, 0x600);
2067        g.execute(instr);
2068        g.write_data(9, 0x700);
2069        g.write_data(10, 0x100);
2070        g.write_data(11, 0x200);
2071        g.execute(instr);
2072        // Each push: r=MAC1>>4, g=MAC2>>4, b=MAC3>>4, code=RGBC[3]=0.
2073        // u32 view packs little-endian: r | (g<<8) | (b<<16) | (code<<24).
2074        assert_eq!(g.read_data(20), 0x0030_2010, "oldest push");
2075        assert_eq!(g.read_data(21), 0x0060_5040, "middle push");
2076        assert_eq!(g.read_data(22), 0x0020_1070, "newest push");
2077    }
2078
2079    #[test]
2080    fn sqr_sf1_scales_product_down_by_4096() {
2081        let mut g = Gte::new();
2082        g.write_data(9, 0x1000);
2083        g.write_data(10, 0x0800);
2084        g.write_data(11, 0x2000);
2085        g.execute(cmd_word(true, false, 0, 0, 0, 0x28));
2086        // MAC_i = (IR_i * IR_i) >> 12
2087        assert_eq!(g.read_data(25) as i32, 0x1000);
2088        assert_eq!(g.read_data(26) as i32, 0x400);
2089        assert_eq!(g.read_data(27) as i32, 0x4000);
2090    }
2091
2092    #[test]
2093    fn rtps_h_equals_2sz3_sets_divide_overflow() {
2094        // Boundary: H exactly 2*SZ3 triggers DIV_OVERFLOW and forces
2095        // divisor = 0x1FFFF (clamped max).
2096        let mut g = Gte::new();
2097        install_identity_rotation(&mut g);
2098        g.write_data(0, pack_xy_i16(0, 0));
2099        g.write_data(1, 1); // SZ3 → 1
2100        g.write_control(26, 2); // H = 2 → H >= 2*SZ3.
2101        g.execute(cmd_word(true, false, 0, 0, 0, 0x01));
2102        assert!(g.read_control(31) & flag::DIV_OVERFLOW != 0);
2103    }
2104
2105    #[test]
2106    fn rtps_unr_table_index_256_is_not_aliased() {
2107        // Regression: when the normalised SZ3 lands in {0xFFFE, 0xFFFF}
2108        // the UNR-table index is 256 -- the upper bound of the
2109        // 257-entry table. A defensive `& 0xFF` mask on the index
2110        // aliased it to 0, swapping the smallest reciprocal multiplier
2111        // (table[256]=0x00 → u=0x101) for the largest
2112        // (table[0]=0xFF → u=0x200). The resulting divisor was about
2113        // 2000× too small, which collapses the projected vertex
2114        // toward (OFX>>16, OFY>>16) -- visually "triangles exploding
2115        // to the screen centre". Pin the boundary so the mask can't
2116        // sneak back in.
2117        let mut g = Gte::new();
2118        install_identity_rotation(&mut g);
2119        // V0 = (0x100, 0, 0x7FFF). With identity rotation and sf=1,
2120        // MAC3 = V.z = 0x7FFF, so SZ3 = 0x7FFF; leading_zeros
2121        // normalises that to d = 0xFFFE which exercises table[256].
2122        g.write_data(0, pack_xy_i16(0x100, 0));
2123        g.write_data(1, 0x7FFF);
2124        g.write_control(24, 0); // OFX
2125        g.write_control(25, 0); // OFY
2126        g.write_control(26, 0x4000); // H
2127        g.execute(cmd_word(true, false, 0, 0, 0, 0x01));
2128        assert_eq!(g.read_data(19), 0x7FFF, "SZ3 fixture");
2129        // Correct divisor is 0x8001 (≈(H<<16)/SZ3=0x8000.4, rounded
2130        // up by Newton-Raphson). With the buggy mask divisor=0x4 and
2131        // SX2 collapses to 0. Expected SX2 = (0x8001*0x100)>>16 = 0x80.
2132        let sxy2 = g.read_data(14);
2133        let sx2 = (sxy2 & 0xFFFF) as i16;
2134        assert_eq!(sx2, 0x80, "SX2 should be 0x80, was {sx2:#x}");
2135    }
2136
2137    #[test]
2138    fn unknown_opcode_leaves_all_registers_clear() {
2139        // Regression: make sure an invalid opcode doesn't accidentally
2140        // flip flag bits from prior execution of a zeroed sub-path.
2141        let mut g = Gte::new();
2142        g.execute(0x3C); // unassigned slot between 0x3B and 0x3D
2143        for i in 0..31 {
2144            assert_eq!(g.read_control(i), 0, "ctrl {i}");
2145        }
2146        assert_eq!(g.read_control(31), 0, "flag");
2147    }
2148
2149    #[test]
2150    fn mvmva_cv2_fc_bug_drops_translation_and_first_column() {
2151        // Documented FC-translation bug (PSX-SPX, confirmed on real
2152        // hardware 2026-06-09): the far-color translation is NOT added,
2153        // and the bug ALSO drops the first matrix column, so the result
2154        // reduces to MAC_i = (Mx_i2*Vy + Mx_i3*Vz) >> sf. The IR
2155        // saturation flags, however, still reflect the discarded first
2156        // term (FC<<12 + Mx_i1*Vx).
2157        let mut g = Gte::new();
2158        install_identity_rotation(&mut g);
2159        g.write_control(21, 0x1000_0000); // FC.r
2160        g.write_data(0, pack_xy_i16(1, 0)); // V = (1, 0, 0)
2161        g.write_data(1, 0);
2162        g.execute(cmd_word(true, false, 0, 0, 2, 0x12));
2163        // V=(1,0,0): the only contribution is column 0 (R11*Vx), which
2164        // the bug drops, so MAC1 = 0 -- the Vx term is lost entirely.
2165        assert_eq!(g.read_data(25) as i32, 0, "MAC1: column 0 (Vx) dropped");
2166        assert_eq!(g.read_data(9), 0, "IR1: column 0 (Vx) dropped");
2167        // Stage 1 IR saturation still fires: (FC<<12 + 0x1000*1) >> 12 huge.
2168        assert!(
2169            g.read_control(31) & flag::IR1_SAT != 0,
2170            "IR1_SAT from discarded first term"
2171        );
2172    }
2173
2174    #[test]
2175    fn mvmva_mx3_uses_garbage_matrix() {
2176        // mx=3 builds a junk matrix from RGBC, IR0, RT[0][2] and RT[1][1]
2177        // per PSX-SPX. Row 0 col 2 is specifically *IR0* (data reg 8),
2178        // not IR1 -- pinning that here so a refactor can't re-introduce
2179        // the IR1-vs-IR0 confusion.
2180        let mut g = Gte::new();
2181        g.write_data(6, 0x0000_0010); // RGBC.r = 0x10
2182        g.write_data(8, 0x100); // IR0 = 0x100
2183        g.write_control(1, pack_xy_i16(0x200, 0)); // RT[0][2] = 0x200
2184        g.write_control(2, pack_xy_i16(0x300, 0)); // RT[1][1] = 0x300
2185        g.write_data(0, pack_xy_i16(1, 1));
2186        g.write_data(1, 1); // V0 = (1,1,1)
2187        g.execute(cmd_word(false, false, 3, 0, 3, 0x12));
2188        // Garbage matrix:
2189        //   [[-0x100, 0x100, IR0=0x100], [0x200; 3], [0x300; 3]]
2190        // With V = (1,1,1), sf=0:
2191        //   MAC1 = -0x100 + 0x100 + 0x100 = 0x100
2192        //   MAC2 = 3 * 0x200 = 0x600
2193        //   MAC3 = 3 * 0x300 = 0x900
2194        assert_eq!(g.read_data(25) as i32, 0x100, "MAC1");
2195        assert_eq!(g.read_data(26) as i32, 0x600, "MAC2");
2196        assert_eq!(g.read_data(27) as i32, 0x900, "MAC3");
2197    }
2198
2199    #[test]
2200    fn mvmva_vx3_uses_ir_as_vector() {
2201        // vx=3 substitutes [IR1, IR2, IR3] for the multiplied vector --
2202        // used by the lighting chain when colors feed back into geometry.
2203        let mut g = Gte::new();
2204        install_identity_rotation(&mut g);
2205        g.write_data(9, 5);
2206        g.write_data(10, 6);
2207        g.write_data(11, 7);
2208        // cv=3 (no TR), sf=1
2209        g.execute(cmd_word(true, false, 0, 3, 3, 0x12));
2210        assert_eq!(g.read_data(25) as i32, 5);
2211        assert_eq!(g.read_data(26) as i32, 6);
2212        assert_eq!(g.read_data(27) as i32, 7);
2213    }
2214}