Skip to main content

rustyn64_rsp/
vu.rs

1//! The **vector unit** — the RSP's 8-lane SIMD coprocessor (Sprint 2).
2//!
3//! 32 registers of 128 bits, each eight lanes of 16 bits, exposed through COP2.
4//! This module currently implements the **register file and the SU/VU moves**;
5//! the computational instructions, the 48-bit accumulator and the `VRCP`/`VRSQ`
6//! tables are the rest of the sprint.
7//!
8//! # Lanes are a view over bytes, not the storage
9//!
10//! Every move here addresses the register by **byte offset**, not by lane, and
11//! the two disagree in ways that matter: `MTC2` with an odd offset straddles two
12//! lanes, and an offset of 15 wraps. Modeling the register as eight `u16`s and
13//! converting at the edges is what keeps that expressible — the alternative,
14//! treating a lane as the unit, silently rounds every odd offset.
15
16use crate::Rsp;
17use serde::{Deserialize, Serialize};
18
19/// The VU's three control registers (N64brew *RSP CPU Core* §Control registers).
20///
21/// `VCO` and `VCC` are 16 bits, `VCE` is 8. They are flag registers rather than
22/// data: each instruction defines what it reads and writes, so there is no
23/// useful general description of their contents.
24#[derive(Clone, Copy, Debug, Default, Eq, PartialEq, Serialize, Deserialize)]
25pub struct Control {
26    /// Carry / overflow, 16 bits — two flags per lane.
27    pub vco: u16,
28    /// Compare results, 16 bits.
29    pub vcc: u16,
30    /// Clip-equality, 8 bits.
31    pub vce: u8,
32}
33
34/// The reciprocal unit's staging latches.
35///
36/// `VRCP`/`VRSQ` take a 16-bit operand, but the two-instruction `VRCPH`+`VRCPL`
37/// sequence feeds them a **32-bit** one: the `H` instruction latches the high
38/// half into `input` and sets `pending`, and the following `L` sees it and
39/// combines. `pending` is what distinguishes "a high half was just staged" from
40/// "there is a stale value in the latch" — without it, an `L` instruction issued
41/// on its own would silently consume whatever the last `H` left behind.
42#[derive(Clone, Copy, Debug, Default, Eq, PartialEq, Serialize, Deserialize)]
43pub struct Divide {
44    /// The high half staged by a `VRCPH`/`VRSQH`.
45    pub input: u16,
46    /// The high half of the last result, which `VRCPH`/`VRSQH` reads back.
47    pub output: u16,
48    /// Whether `input` was staged by the immediately preceding instruction.
49    pub pending: bool,
50}
51
52/// Which control register a `CFC2`/`CTC2` names.
53///
54/// Only `0`, `1` and `2` are defined. The RSP has no exception mechanism, so a
55/// wider index cannot fault; it is masked, which is what the encoding's two
56/// usable bits already imply.
57const fn control_index(vs: u32) -> u32 {
58    vs & 3
59}
60
61impl Rsp {
62    /// Read a byte of a vector register, big-endian within the 128 bits.
63    ///
64    /// Byte 0 is the most significant half of lane 0, matching the wiki's
65    /// convention that byte indices count *"from the higher part of the register
66    /// (in big-endian order)"*.
67    #[must_use]
68    pub const fn vu_byte(&self, reg: usize, byte: usize) -> u8 {
69        let lane = self.vu_regs[reg & 31][(byte & 15) >> 1];
70        if byte & 1 == 0 {
71            (lane >> 8) as u8
72        } else {
73            lane as u8
74        }
75    }
76
77    /// Write a byte of a vector register.
78    pub const fn set_vu_byte(&mut self, reg: usize, byte: usize, val: u8) {
79        let lane = &mut self.vu_regs[reg & 31][(byte & 15) >> 1];
80        if byte & 1 == 0 {
81            *lane = (*lane & 0x00FF) | ((val as u16) << 8);
82        } else {
83            *lane = (*lane & 0xFF00) | val as u16;
84        }
85    }
86
87    /// `MFC2` — copy two bytes of a vector register into a GPR, sign-extended.
88    ///
89    /// The pair is taken at a **byte** offset, so an odd offset straddles two
90    /// lanes. At offset 15 the second byte **wraps to byte 0** of the same
91    /// register rather than reading past the end — a rule that is invisible
92    /// until something actually addresses the last byte.
93    pub fn mfc2(&mut self, rt: usize, vs: usize, elem: usize) -> u32 {
94        let hi = self.vu_byte(vs, elem);
95        let lo = self.vu_byte(vs, (elem + 1) & 15);
96        let v = (u16::from(hi) << 8) | u16::from(lo);
97        let sext = i32::from(v.cast_signed()).cast_unsigned();
98        self.set_su(rt, sext);
99        sext
100    }
101
102    /// `MTC2` — copy the low 16 bits of a GPR into a vector register at a byte
103    /// offset.
104    ///
105    /// At offset 15 **only one byte is written**, taken from `rt[15..8]`: there
106    /// is no byte 16 to receive the other half, and unlike `MFC2` it does not
107    /// wrap around to byte 0. The asymmetry between the two is deliberate on
108    /// hardware and is exactly what a lane-oriented implementation loses.
109    pub const fn mtc2(&mut self, value: u32, vs: usize, elem: usize) {
110        let hi = (value >> 8) as u8;
111        let lo = value as u8;
112        self.set_vu_byte(vs, elem, hi);
113        if elem != 15 {
114            self.set_vu_byte(vs, elem + 1, lo);
115        }
116    }
117
118    /// `CFC2` — copy a VU control register into a GPR, sign-extended from 16
119    /// bits. The element field is ignored.
120    pub fn cfc2(&mut self, rt: usize, vs: u32) -> u32 {
121        let v = match control_index(vs) {
122            0 => self.vu_ctrl.vco,
123            1 => self.vu_ctrl.vcc,
124            // `VCE` is only 8 bits wide, and the read is still described as a
125            // 16-bit value sign-extended to 32 -- so the byte is zero-extended
126            // into the halfword first, and the halfword's sign bit is therefore
127            // always clear.
128            _ => u16::from(self.vu_ctrl.vce),
129        };
130        let sext = i32::from(v.cast_signed()).cast_unsigned();
131        self.set_su(rt, sext);
132        sext
133    }
134
135    /// `CTC2` — copy the low 16 bits of a GPR into a VU control register.
136    pub const fn ctc2(&mut self, value: u32, vs: u32) {
137        let v = value as u16;
138        match control_index(vs) {
139            0 => self.vu_ctrl.vco = v,
140            1 => self.vu_ctrl.vcc = v,
141            _ => self.vu_ctrl.vce = v as u8,
142        }
143    }
144}
145
146#[cfg(test)]
147mod tests {
148    use super::*;
149
150    /// Bytes are big-endian across the 128 bits: byte 0 is the top half of
151    /// lane 0, byte 1 the bottom half, and so on.
152    #[test]
153    fn bytes_address_the_register_big_endian() {
154        let mut rsp = Rsp::new();
155        rsp.vu_regs[4][0] = 0xAABB;
156        rsp.vu_regs[4][7] = 0x1122;
157        assert_eq!(rsp.vu_byte(4, 0), 0xAA);
158        assert_eq!(rsp.vu_byte(4, 1), 0xBB);
159        assert_eq!(rsp.vu_byte(4, 14), 0x11);
160        assert_eq!(rsp.vu_byte(4, 15), 0x22);
161
162        rsp.set_vu_byte(4, 1, 0xCC);
163        assert_eq!(rsp.vu_regs[4][0], 0xAACC, "only the low byte moved");
164    }
165
166    /// **`MTC2` takes a byte offset, so an odd one straddles two lanes.**
167    ///
168    /// A lane-oriented implementation rounds this to a lane and writes the
169    /// wrong 16 bits — and the two agree for every *even* offset, so a test
170    /// that only uses aligned offsets cannot tell them apart.
171    #[test]
172    fn mtc2_at_an_odd_offset_straddles_two_lanes() {
173        let mut rsp = Rsp::new();
174        rsp.mtc2(0x1234, 2, 1);
175        assert_eq!(
176            rsp.vu_regs[2][0], 0x0012,
177            "high byte into lane 0's low half"
178        );
179        assert_eq!(
180            rsp.vu_regs[2][1], 0x3400,
181            "low byte into lane 1's high half"
182        );
183    }
184
185    /// At byte 15 `MTC2` writes **one** byte and does not wrap.
186    #[test]
187    fn mtc2_at_the_last_byte_writes_only_one() {
188        let mut rsp = Rsp::new();
189        rsp.mtc2(0x1234, 3, 15);
190        assert_eq!(rsp.vu_regs[3][7], 0x0012, "rt[15..8] into the last byte");
191        assert_eq!(rsp.vu_regs[3][0], 0, "and nothing wrapped to the start");
192    }
193
194    /// **`MFC2` at byte 15 wraps its second byte to byte 0** — the asymmetry
195    /// with `MTC2`, which does not.
196    #[test]
197    fn mfc2_at_the_last_byte_wraps_to_the_first() {
198        let mut rsp = Rsp::new();
199        rsp.vu_regs[5][7] = 0x00AB; // byte 15 = 0xAB
200        rsp.vu_regs[5][0] = 0xCD00; // byte 0  = 0xCD
201        let v = rsp.mfc2(1, 5, 15);
202        assert_eq!(v, 0xFFFF_ABCD, "0xABCD, sign-extended");
203        assert_eq!(rsp.su_regs[1], 0xFFFF_ABCD);
204    }
205
206    /// `MFC2` sign-extends, so a value with bit 15 set fills the upper half.
207    #[test]
208    fn mfc2_sign_extends_to_thirty_two_bits() {
209        let mut rsp = Rsp::new();
210        rsp.vu_regs[6][2] = 0x8001;
211        assert_eq!(rsp.mfc2(2, 6, 4), 0xFFFF_8001);
212        rsp.vu_regs[6][2] = 0x7FFF;
213        assert_eq!(rsp.mfc2(2, 6, 4), 0x0000_7FFF);
214    }
215
216    /// The three control registers round-trip, and `VCE` keeps only 8 bits.
217    #[test]
218    fn the_control_registers_round_trip() {
219        let mut rsp = Rsp::new();
220        rsp.ctc2(0xFFFF_1234, 0);
221        rsp.ctc2(0xFFFF_5678, 1);
222        rsp.ctc2(0xFFFF_00AB, 2);
223        assert_eq!(rsp.vu_ctrl.vco, 0x1234);
224        assert_eq!(rsp.vu_ctrl.vcc, 0x5678);
225        assert_eq!(rsp.vu_ctrl.vce, 0xAB, "VCE is 8 bits wide");
226
227        assert_eq!(rsp.cfc2(1, 0), 0x0000_1234);
228        assert_eq!(rsp.cfc2(1, 1), 0x0000_5678);
229        // 0xAB zero-extends into the halfword, so the sign bit is clear and the
230        // 32-bit result has no upper ones.
231        assert_eq!(rsp.cfc2(1, 2), 0x0000_00AB);
232    }
233
234    /// `CFC2` sign-extends from 16 bits, which `VCO` and `VCC` can reach.
235    #[test]
236    fn cfc2_sign_extends_the_sixteen_bit_registers() {
237        let mut rsp = Rsp::new();
238        rsp.ctc2(0x8000, 0);
239        assert_eq!(rsp.cfc2(1, 0), 0xFFFF_8000);
240    }
241}
242
243/// The 48-bit-per-lane accumulator, and the computational instructions.
244///
245/// # The accumulator is one 48-bit register per lane, not three 16-bit ones
246///
247/// `VSAR` slices it into `ACC_HI` (bits 47..32), `ACC_MD` (31..16) and `ACC_LO`
248/// (15..0), which invites modeling it as three separate halfwords. It is not:
249/// the multiply instructions write and accumulate across the full 48 bits, and
250/// the extraction that produces `vd` reads a 32-bit window *spanning* two of
251/// those slices. Splitting the storage makes carries between them disappear.
252impl Rsp {
253    /// Broadcast-modified read of a `vt` lane (N64brew *RSP CPU Core*
254    /// §Broadcast modifier).
255    ///
256    /// `element` 0 and 1 both mean "no broadcast" — the table lists them
257    /// separately and gives them identical lane sets, so this is not a
258    /// simplification.
259    #[must_use]
260    pub const fn vt_lane(&self, vt: usize, element: u32, lane: usize) -> u16 {
261        let src = match element {
262            0 | 1 => lane,
263            // Quarter broadcasts: pairs share the even (2) or odd (3) lane.
264            2 => lane & !1,
265            3 => (lane & !1) | 1,
266            // Half broadcasts: each group of four takes one lane.
267            4..=7 => (lane & !3) | (element as usize - 4),
268            // Single-lane broadcast across all eight.
269            _ => element as usize - 8,
270        };
271        self.vu_regs[vt & 31][src & 7]
272    }
273
274    /// Signed clamp to `[-32768, 32767]`.
275    const fn clamp_signed(v: i64) -> u16 {
276        if v < -32768 {
277            0x8000
278        } else if v > 32767 {
279            0x7FFF
280        } else {
281            (v as i16).cast_unsigned()
282        }
283    }
284
285    /// Unsigned clamp: negatives become 0, and the saturating threshold is
286    /// **15-bit** while the saturated value is 16-bit.
287    ///
288    /// That asymmetry is the documented rule, not a typo — anything above
289    /// `0x7FFF` saturates to `0xFFFF` rather than passing through. A naive
290    /// `> 65535` test lets values in the range `0x8000..=0xFFFF` through
291    /// unchanged and fails the VU tests.
292    const fn clamp_unsigned(v: i64) -> u16 {
293        if v < 0 {
294            0
295        } else if v > 32767 {
296            0xFFFF
297        } else {
298            // In range, so the truncation is exact and the sign is already
299            // known non-negative.
300            v.cast_unsigned() as u16
301        }
302    }
303
304    /// The extraction `VMADL` and `VMADN` use, which is **not** either clamp.
305    ///
306    /// When `acc >> 16` fits in a signed 16-bit value the result is the
307    /// accumulator's *low* slice, untouched. When it does not, the result
308    /// saturates to `0x0000` or `0xFFFF` by the sign. So the low bits are
309    /// returned or discarded wholesale depending on a test applied to a
310    /// different part of the accumulator -- which is why neither
311    /// `clamp_signed` nor `clamp_unsigned` expresses it, and why reusing one of
312    /// them looks right for small values and fails at the boundary.
313    ///
314    /// Derived from n64-systemtest's vectors: `VMADL` lane 4 leaves
315    /// `acc = 0x0000_7FFF_C000` and yields `0xC000` (the low slice), while lane
316    /// 7 leaves `0x0000_8000_C000` -- one step further -- and yields `0xFFFF`.
317    const fn extract_low(acc: i64) -> u16 {
318        let mid = acc >> 16;
319        if mid > 32767 {
320            0xFFFF
321        } else if mid < -32768 {
322            0
323        } else {
324            (acc.cast_unsigned() & 0xFFFF) as u16
325        }
326    }
327
328    /// Sign-extend the 48-bit accumulator lane to a signed 64-bit value.
329    const fn acc_signed(&self, lane: usize) -> i64 {
330        let v = self.vu_acc[lane] & 0xFFFF_FFFF_FFFF;
331        // Bit 47 is the sign; shifting up and back propagates it.
332        (v << 16).cast_signed() >> 16
333    }
334
335    /// Store a signed value back into a 48-bit accumulator lane.
336    const fn set_acc(&mut self, lane: usize, v: i64) {
337        self.vu_acc[lane] = v.cast_unsigned() & 0xFFFF_FFFF_FFFF;
338    }
339
340    /// The computational COP2 instructions.
341    ///
342    /// Returns `false` for an opcode this does not implement yet, so the caller
343    /// can leave the instruction inert rather than writing a wrong result.
344    pub fn vu_compute(&mut self, op: u32, element: u32, vs: usize, vt: usize, vd: usize) -> bool {
345        // The whole-instruction forms (VRND, clip, VZERO, VMACQ) act on the
346        // accumulator or the flag words as a unit and do not fit the per-lane
347        // match below; dispatch them first.
348        if self.vu_whole_instruction(op, element, vs, vt, vd) {
349            return true;
350        }
351        let mut clear_vco = false;
352        let mut set_vco = false;
353        let mut new_vco = 0u16;
354        // The compare group rebuilds VCC's low half from its per-lane results
355        // and clears the high half; `wrote_vcc` distinguishes that from an
356        // instruction that leaves VCC alone entirely (VMRG does).
357        let mut wrote_vcc = false;
358        let mut compare_flags = 0u16;
359        // Snapshot both sources before writing any lane (read-before-write):
360        // the hardware reads the whole broadcast `vt`/`vs` first, so a
361        // destructive `vd == vt`/`vd == vs` broadcast must not read a lane an
362        // earlier iteration overwrote (`VNE V6,V6,V7[Q0]`, the `(e=H1)` cases).
363        let sv: [u16; 8] = core::array::from_fn(|l| self.vu_regs[vs & 31][l]);
364        let tv: [u16; 8] = core::array::from_fn(|l| self.vt_lane(vt, element, l));
365        for lane in 0..8 {
366            let s = sv[lane];
367            let t = tv[lane];
368            let ss = i64::from(s.cast_signed());
369            let ts = i64::from(t.cast_signed());
370            let su = i64::from(s);
371            let tu = i64::from(t);
372
373            let out = match op {
374                0x00..=0x0F => self.multiply_lane(op, lane, ss, ts, su, tu),
375                // VADD/VSUB: signed, **with the carry from VCO's low half**,
376                // and they CLEAR the whole of VCO afterwards. Forgetting the
377                // clear leaves the next VADD adding a stale carry, which shows
378                // up as an off-by-one in one lane of a later frame.
379                0x10 | 0x11 => {
380                    let carry = i64::from((self.vu_ctrl.vco >> lane) & 1);
381                    let r = if op == 0x10 {
382                        ss + ts + carry
383                    } else {
384                        ss - ts - carry
385                    };
386                    self.set_acc_low(lane, r.cast_unsigned() as u16);
387                    clear_vco = true;
388                    Self::clamp_signed(r)
389                }
390                0x13 => self.vabs_lane(lane, ss, ts, t),
391                // VADDC/VSUBC: unsigned, and they *produce* the carry rather
392                // than consuming it. VSUBC also sets VCO's high half from
393                // "result was non-zero", which is what makes the pair usable
394                // for a multi-precision compare.
395                0x14 | 0x15 => {
396                    let r = if op == 0x14 { su + tu } else { su - tu };
397                    let low = r.cast_unsigned() as u16;
398                    self.set_acc_low(lane, low);
399                    let borrow = (r >> 16) & 1 != 0;
400                    if borrow {
401                        new_vco |= 1 << lane;
402                    }
403                    if op == 0x15 && low != 0 {
404                        new_vco |= 1 << (lane + 8);
405                    }
406                    set_vco = true;
407                    low
408                }
409                0x20..=0x23 | 0x27 => {
410                    let (picked, flag) = self.compare_lane(op, lane, s, t, ss, ts);
411                    if let Some(set) = flag {
412                        if set {
413                            compare_flags |= 1 << lane;
414                        }
415                        wrote_vcc = true;
416                    }
417                    clear_vco = true;
418                    self.set_acc_low(lane, picked);
419                    picked
420                }
421                // VSAR: read a 16-bit slice of the accumulator. The slice is
422                // chosen by the *element* field, not by an operand.
423                0x1D => {
424                    let acc = self.vu_acc[lane];
425                    match element {
426                        8 => (acc >> 32) as u16,
427                        9 => (acc >> 16) as u16,
428                        10 => acc as u16,
429                        // Any other element reads zero; the RSP has no
430                        // exception to raise for an undefined selector.
431                        _ => 0,
432                    }
433                }
434                // The bitwise group. Each also writes its result into ACC_LO.
435                0x28 => s & t,
436                0x29 => !(s & t),
437                0x2A => s | t,
438                0x2B => !(s | t),
439                0x2C => s ^ t,
440                0x2D => !(s ^ t),
441                _ => return false,
442            };
443
444            // The logical operations leave ACC_LO holding their result.
445            if (0x28..=0x2D).contains(&op) {
446                self.vu_acc[lane] = (self.vu_acc[lane] & 0xFFFF_FFFF_0000) | u64::from(out);
447            }
448            self.vu_regs[vd & 31][lane] = out;
449        }
450        // VCC is rebuilt once for the whole instruction: the low half from the
451        // predicates, the high half cleared.
452        if wrote_vcc {
453            self.vu_ctrl.vcc = compare_flags;
454        }
455        // VCO is written once for the whole instruction, not per lane.
456        if clear_vco {
457            self.vu_ctrl.vco = 0;
458        } else if set_vco {
459            self.vu_ctrl.vco = new_vco;
460        }
461        true
462    }
463
464    /// The multiply family for one lane: `VMUL*`, `VMUD*` and their
465    /// accumulating `VMAC*`/`VMAD*` partners.
466    ///
467    /// Split out to keep [`Rsp::vu_compute`]'s decode readable; it is one arm of
468    /// that match, not a separable unit.
469    fn multiply_lane(&mut self, op: u32, lane: usize, ss: i64, ts: i64, su: i64, tu: i64) -> u16 {
470        match op {
471            // Multiply, S1.15 * S1.15, doubled and rounded. The +0x8000 is
472            // the rounding constant, and it lands in the accumulator, not
473            // just in the result -- the oracle reads ACC_LO back as 0x8000
474            // for a zero product, which is how the constant is visible.
475            0x00 | 0x01 => {
476                let acc = ss * ts * 2 + 0x8000;
477                self.set_acc(lane, acc);
478                let extracted = self.acc_signed(lane) >> 16;
479                if op == 0x00 {
480                    Self::clamp_signed(extracted)
481                } else {
482                    Self::clamp_unsigned(extracted)
483                }
484            }
485            // VMUDL: U0.16 * U0.16, keeping the high half. The product is
486            // unsigned, so nothing sign-extends into the upper accumulator.
487            0x04 => {
488                let acc = (su * tu) >> 16;
489                self.set_acc(lane, acc);
490                acc.cast_unsigned() as u16
491            }
492            // VMUDM: S0.16 * U0.16.
493            0x05 => {
494                let acc = ss * tu;
495                self.set_acc(lane, acc);
496                (acc >> 16).cast_unsigned() as u16
497            }
498            // VMUDN: U0.16 * S0.16 -- the mirror of VMUDM, and it extracts
499            // the LOW half where VMUDM takes the high one.
500            0x06 => {
501                let acc = su * ts;
502                self.set_acc(lane, acc);
503                acc.cast_unsigned() as u16
504            }
505            // VMUDH: S0.16 * S0.16, shifted into the upper accumulator.
506            0x07 => {
507                let acc = (ss * ts) << 16;
508                self.set_acc(lane, acc);
509                Self::clamp_signed(self.acc_signed(lane) >> 16)
510            }
511            // VMULQ: a 32-bit product placed in the accumulator's MIDDLE 32
512            // bits with the low 16 zeroed, then extracted with a >>1, a signed
513            // clamp, and the low 4 bits masked off. The odd `+ 31` rounds only
514            // negative products.
515            0x03 => {
516                let mut product = ss * ts;
517                if product < 0 {
518                    product += 31;
519                }
520                self.set_acc(lane, product << 16);
521                Self::clamp_signed(product >> 1) & !15
522            }
523            // The accumulating forms. Each adds the same product its
524            // VMUL/VMUD counterpart *sets* -- with one difference that is
525            // easy to miss: VMACF adds `2 * vs * vt` with **no rounding
526            // constant**, where VMULF adds `+ 0x8000`. The oracle shows it
527            // directly: lane 3 moves the accumulator from 0xC000 to
528            // 0x1_0000, a delta of exactly 0x4000 = 2*8192.
529            0x08 | 0x09 => {
530                let acc = self.acc_signed(lane) + ss * ts * 2;
531                self.set_acc(lane, acc);
532                let extracted = self.acc_signed(lane) >> 16;
533                if op == 0x08 {
534                    Self::clamp_signed(extracted)
535                } else {
536                    Self::clamp_unsigned(extracted)
537                }
538            }
539            // VMADL: accumulate VMUDL's product, extract the low slice.
540            0x0C => {
541                let acc = self.acc_signed(lane) + ((su * tu) >> 16);
542                self.set_acc(lane, acc);
543                Self::extract_low(self.acc_signed(lane))
544            }
545            // VMADM: accumulate VMUDM's product. Unlike VMUDM this one
546            // CLAMPS the extracted middle rather than truncating it.
547            0x0D => {
548                let acc = self.acc_signed(lane) + ss * tu;
549                self.set_acc(lane, acc);
550                Self::clamp_signed(self.acc_signed(lane) >> 16)
551            }
552            // VMADN: accumulate VMUDN's product, extract the low slice.
553            0x0E => {
554                let acc = self.acc_signed(lane) + su * ts;
555                self.set_acc(lane, acc);
556                Self::extract_low(self.acc_signed(lane))
557            }
558            // VMADH: accumulate VMUDH's product.
559            0x0F => {
560                let acc = self.acc_signed(lane) + ((ss * ts) << 16);
561                self.set_acc(lane, acc);
562                Self::clamp_signed(self.acc_signed(lane) >> 16)
563            }
564            _ => 0,
565        }
566    }
567
568    /// One lane of the compare/select group.
569    ///
570    /// Returns the selected operand and, for the four true compares, whether the
571    /// predicate held (`VMRG` returns `None` — it consumes `VCC` and produces no
572    /// new flag). Split out to keep [`Rsp::vu_compute`]'s decode under length.
573    fn compare_lane(
574        &self,
575        op: u32,
576        lane: usize,
577        s: u16,
578        t: u16,
579        ss: i64,
580        ts: i64,
581    ) -> (u16, Option<bool>) {
582        // VCO's low half is the carry/borrow; its high half is the "result was
583        // non-zero" flag a preceding VSUBC left.
584        let carry = (self.vu_ctrl.vco >> lane) & 1 != 0;
585        let non_zero = (self.vu_ctrl.vco >> (lane + 8)) & 1 != 0;
586        if op == 0x27 {
587            // VMRG selects on VCC without changing it.
588            let picked = if (self.vu_ctrl.vcc >> lane) & 1 != 0 {
589                s
590            } else {
591                t
592            };
593            return (picked, None);
594        }
595        let cond = match op {
596            0x20 => ss < ts || (ss == ts && carry && non_zero),
597            0x21 => !non_zero && s == t,
598            0x22 => s != t || non_zero,
599            _ => ss > ts || (ss == ts && (!carry || !non_zero)),
600        };
601        (if cond { s } else { t }, Some(cond))
602    }
603
604    /// `VRNDN`/`VRNDP` — accumulator rounding (N64brew *RSP CPU Core*; ares).
605    ///
606    /// These do **not** read a `vs` register. The low bit of the `vs` *field*
607    /// number selects whether the sign-extended `vt` element is shifted left 16
608    /// before use, and the accumulator — not an operand — is what the product
609    /// is conditionally added to. `positive` is `VRNDP`, which adds when the
610    /// 48-bit accumulator is `>= 0`; `VRNDN` adds when it is `< 0`. The result
611    /// is the signed-clamped middle of the accumulator, and the whole 48-bit
612    /// accumulator is written back.
613    fn vrnd(&mut self, positive: bool, element: u32, vs_field: usize, vt: usize, vd: usize) {
614        let shift = vs_field & 1 != 0;
615        // Snapshot `vt` first: `VRNDP/N V6,V6[...]` overwrites its own source,
616        // and a broadcast read of an already-written lane would be corrupt.
617        let tv: [u16; 8] = core::array::from_fn(|l| self.vt_lane(vt, element, l));
618        for (lane, &tl) in tv.iter().enumerate() {
619            let mut product = i64::from(tl.cast_signed());
620            if shift {
621                product <<= 16;
622            }
623            let mut acc = self.acc_signed(lane);
624            if (positive && acc >= 0) || (!positive && acc < 0) {
625                acc = Self::sclip48(acc + product);
626            }
627            self.set_acc(lane, acc);
628            self.vu_regs[vd & 31][lane] = Self::clamp_signed(acc >> 16);
629        }
630    }
631
632    /// The clip compares `VCL` (0x24), `VCH` (0x25), `VCR` (0x26).
633    ///
634    /// The most flag-entangled VU instructions: they read and write `VCO`,
635    /// `VCC` and `VCE` together, per lane. Transcribed from **n64-systemtest's
636    /// reference**, which diverges from ares in `VCH`'s else branch (the suite's
637    /// `VCOH` is just `diff != 0`; ares adds `&& vs != ~vt`).
638    ///
639    /// **Inputs are snapshotted before any write.** With `vd == vt` and a
640    /// broadcast element, a lane can read a `vt` lane a *later*-indexed lane has
641    /// not yet reached — but an *earlier* one may already have overwritten if
642    /// the reads and writes interleave. Hardware reads the whole (broadcast)
643    /// `vt` and `vs` first; so does this. The failing oracle case that forced
644    /// this was `V6,V6,V7[Q0]`.
645    /// The computational opcodes that act on the whole vector at once rather
646    /// than through [`Rsp::vu_compute`]'s per-lane match. Returns whether `op`
647    /// was one of them.
648    ///
649    /// - `VRNDN`/`VRNDP` (`0x02`/`0x0A`) read the **accumulator**, use the low
650    ///   bit of the `vs` *field* as a shift selector rather than reading a `vs`
651    ///   register, and conditionally add on the accumulator's sign.
652    /// - The clip compares `VCL`/`VCH`/`VCR` (`0x24..=0x26`) read and write the
653    ///   whole `VCO`/`VCC`/`VCE` set; this is where ares and the suite diverge.
654    /// - `VMACQ` (`0x0B`) re-rounds the accumulator's magnitude toward a
655    ///   multiple of `0x20_0000` with no operands.
656    /// - The reserved VZERO family (see [`Rsp::vzero`]).
657    fn vu_whole_instruction(
658        &mut self,
659        op: u32,
660        element: u32,
661        vs: usize,
662        vt: usize,
663        vd: usize,
664    ) -> bool {
665        match op {
666            0x02 | 0x0A => self.vrnd(op == 0x02, element, vs, vt, vd),
667            0x0B => self.vmacq(vd),
668            0x24..=0x26 => self.clip(op, element, vs, vt, vd),
669            0x12 | 0x16..=0x1C | 0x1E | 0x1F | 0x2E | 0x2F | 0x38..=0x3E => {
670                self.vzero(element, vs, vt, vd);
671            }
672            _ => return false,
673        }
674        true
675    }
676
677    /// The reserved "VZERO" opcode family (VSUT, VADDB/VSUBB/VACCB/VSUCB/VSAD/
678    /// VSAC/VSUM, V30/V31, V46/V47, VEXT{T,Q,N}/V59/VINS{T,Q,N}).
679    ///
680    /// n64-systemtest pins every one of these undocumented opcodes to a single
681    /// `run_vzero` reference: `ACC_LO = vs + vt` per lane, `vd` zeroed, and no
682    /// flag word touched. Snapshotting the sources is not strictly needed here
683    /// (the write is a constant zero), but it keeps the shape identical to the
684    /// other broadcast consumers.
685    fn vzero(&mut self, element: u32, vs: usize, vt: usize, vd: usize) {
686        let sv: [u16; 8] = core::array::from_fn(|l| self.vu_regs[vs & 31][l]);
687        let tv: [u16; 8] = core::array::from_fn(|l| self.vt_lane(vt, element, l));
688        for lane in 0..8 {
689            self.set_acc_low(lane, sv[lane].wrapping_add(tv[lane]));
690            self.vu_regs[vd & 31][lane] = 0;
691        }
692    }
693
694    fn clip(&mut self, op: u32, element: u32, vs: usize, vt: usize, vd: usize) {
695        let sv: [u16; 8] = core::array::from_fn(|l| self.vu_regs[vs & 31][l]);
696        let tv: [u16; 8] = core::array::from_fn(|l| self.vt_lane(vt, element, l));
697        for lane in 0..8 {
698            let s = sv[lane];
699            let t = tv[lane];
700            let ss = s.cast_signed();
701            let ts = t.cast_signed();
702            let out = match op {
703                0x25 => self.vch_lane(lane, s, t, ss, ts),
704                0x26 => self.vcr_lane(lane, s, t, ss, ts),
705                _ => self.vcl_lane(lane, s, t),
706            };
707            self.set_acc_low(lane, out);
708            self.vu_regs[vd & 31][lane] = out;
709        }
710        if op != 0x25 {
711            self.vu_ctrl.vco = 0;
712            self.vu_ctrl.vce = 0;
713        }
714    }
715
716    const fn vco_l(&self, n: usize) -> bool {
717        (self.vu_ctrl.vco >> n) & 1 != 0
718    }
719    const fn vco_h(&self, n: usize) -> bool {
720        (self.vu_ctrl.vco >> (n + 8)) & 1 != 0
721    }
722    const fn vcc_l(&self, n: usize) -> bool {
723        (self.vu_ctrl.vcc >> n) & 1 != 0
724    }
725    const fn vcc_h(&self, n: usize) -> bool {
726        (self.vu_ctrl.vcc >> (n + 8)) & 1 != 0
727    }
728    const fn vce_bit(&self, n: usize) -> bool {
729        (self.vu_ctrl.vce >> n) & 1 != 0
730    }
731    const fn set_flag(reg: &mut u16, bit: usize, v: bool) -> bool {
732        if v {
733            *reg |= 1 << bit;
734        } else {
735            *reg &= !(1 << bit);
736        }
737        v
738    }
739    const fn set_vce(reg: &mut u8, bit: usize, v: bool) {
740        if v {
741            *reg |= 1 << bit;
742        } else {
743            *reg &= !(1 << bit);
744        }
745    }
746
747    fn vch_lane(&mut self, n: usize, s: u16, t: u16, ss: i16, ts: i16) -> u16 {
748        if (ss ^ ts) < 0 {
749            let r = ss.wrapping_add(ts);
750            Self::set_flag(&mut self.vu_ctrl.vcc, n, r <= 0);
751            Self::set_flag(&mut self.vu_ctrl.vcc, n + 8, ts < 0);
752            Self::set_flag(&mut self.vu_ctrl.vco, n, true);
753            Self::set_flag(&mut self.vu_ctrl.vco, n + 8, r != 0 && s != (t ^ 0xFFFF));
754            Self::set_vce(&mut self.vu_ctrl.vce, n, r == -1);
755            if r <= 0 { t.wrapping_neg() } else { s }
756        } else {
757            let r = ss.wrapping_sub(ts);
758            Self::set_flag(&mut self.vu_ctrl.vcc, n, ts < 0);
759            Self::set_flag(&mut self.vu_ctrl.vcc, n + 8, r >= 0);
760            Self::set_flag(&mut self.vu_ctrl.vco, n, false);
761            // Suite: else-branch VCOH is `diff != 0` alone (ares adds a term).
762            Self::set_flag(&mut self.vu_ctrl.vco, n + 8, r != 0);
763            Self::set_vce(&mut self.vu_ctrl.vce, n, false);
764            if r >= 0 { t } else { s }
765        }
766    }
767
768    fn vcl_lane(&mut self, n: usize, s: u16, t: u16) -> u16 {
769        if self.vco_l(n) {
770            if self.vco_h(n) {
771                if self.vcc_l(n) { t.wrapping_neg() } else { s }
772            } else {
773                let sum = s.wrapping_add(t);
774                let carry = u32::from(s) + u32::from(t) != u32::from(sum);
775                let v = if self.vce_bit(n) {
776                    sum == 0 || !carry
777                } else {
778                    sum == 0 && !carry
779                };
780                Self::set_flag(&mut self.vu_ctrl.vcc, n, v);
781                if v { t.wrapping_neg() } else { s }
782            }
783        } else if self.vco_h(n) {
784            if self.vcc_h(n) { t } else { s }
785        } else {
786            let v = i32::from(s) - i32::from(t) >= 0;
787            Self::set_flag(&mut self.vu_ctrl.vcc, n + 8, v);
788            if v { t } else { s }
789        }
790    }
791
792    fn vcr_lane(&mut self, n: usize, s: u16, t: u16, ss: i16, ts: i16) -> u16 {
793        if (ss ^ ts) < 0 {
794            Self::set_flag(&mut self.vu_ctrl.vcc, n + 8, ts < 0);
795            let v = i32::from(ss) + i32::from(ts) < 0;
796            Self::set_flag(&mut self.vu_ctrl.vcc, n, v);
797            if v { !t } else { s }
798        } else {
799            Self::set_flag(&mut self.vu_ctrl.vcc, n, ts < 0);
800            let v = i32::from(ss) - i32::from(ts) >= 0;
801            Self::set_flag(&mut self.vu_ctrl.vcc, n + 8, v);
802            if v { t } else { s }
803        }
804    }
805
806    /// `VMACQ` — nudge the accumulator toward a multiple of `0x20_0000`, in
807    /// place, no operands.
808    ///
809    /// Transcribed from n64-systemtest's `simulate`: when bit 21 is clear the
810    /// 48-bit accumulator is moved by `±0x20_0000` toward zero-mod-that (only
811    /// away from the `[0x20_0000, 0x3F_FFFF]` band, which bit 21 being set
812    /// excludes), and the result is `acc >> 17`, saturated past 32 bits and
813    /// masked to clear its low 4 bits. `ACC_LO` survives because the delta sits
814    /// at bit 21, above it.
815    fn vmacq(&mut self, vd: usize) {
816        for lane in 0..8 {
817            let acc = self.acc_signed(lane);
818            let out = if acc & 0x20_0000 == 0 {
819                match (acc >> 22).cmp(&0) {
820                    core::cmp::Ordering::Less => acc + 0x20_0000,
821                    core::cmp::Ordering::Greater => acc - 0x20_0000,
822                    core::cmp::Ordering::Equal => acc,
823                }
824            } else {
825                acc
826            };
827            self.set_acc(lane, out);
828            let clamped: u16 = if out < 0 {
829                if (!out) >> 32 != 0 {
830                    0x8000
831                } else {
832                    (out >> 17).cast_unsigned() as u16
833                }
834            } else if out >> 32 != 0 {
835                0x7FFF
836            } else {
837                (out >> 17).cast_unsigned() as u16
838            };
839            self.vu_regs[vd & 31][lane] = clamped & 0xFFF0;
840        }
841    }
842
843    /// Sign-clip to 48 bits, as the accumulator is.
844    const fn sclip48(v: i64) -> i64 {
845        (v << 16) >> 16
846    }
847
848    /// `VABS` for one lane: the **sign of `vs` applied to `vt`**, which is not
849    /// the absolute value of either operand despite the mnemonic.
850    ///
851    /// The most negative input is the interesting case: its negation is not
852    /// representable, so `vd` saturates to `0x7FFF` while the accumulator keeps
853    /// `0x8000`. The two deliberately disagree, and a test reading only `vd`
854    /// cannot see it.
855    fn vabs_lane(&mut self, lane: usize, ss: i64, ts: i64, t: u16) -> u16 {
856        match ss.cmp(&0) {
857            core::cmp::Ordering::Less => {
858                if ts == -32768 {
859                    self.set_acc_low(lane, 0x8000);
860                    0x7FFF
861                } else {
862                    let neg = (-ts).cast_unsigned() as u16;
863                    self.set_acc_low(lane, neg);
864                    neg
865                }
866            }
867            core::cmp::Ordering::Greater => {
868                self.set_acc_low(lane, t);
869                t
870            }
871            core::cmp::Ordering::Equal => {
872                self.set_acc_low(lane, 0);
873                0
874            }
875        }
876    }
877
878    /// Write one lane's accumulator low slice, leaving the upper 32 bits alone.
879    const fn set_acc_low(&mut self, lane: usize, value: u16) {
880        self.vu_acc[lane] = (self.vu_acc[lane] & 0xFFFF_FFFF_0000) | value as u64;
881    }
882}
883
884#[cfg(test)]
885mod compute_tests {
886    use super::*;
887
888    /// The oracle's own input pair, named as **it** names them.
889    ///
890    /// n64-systemtest loads these into `$v0` and `$v1` and then assembles e.g.
891    /// `write_vmulf(V2, V0, V1)` — whose signature is **`(vd, vt, vs)`**, not
892    /// the `(vd, vs, vt)` it reads like. So `$v1` is the instruction's `vs` and
893    /// `$v0` is its `vt`.
894    ///
895    /// Getting that backwards is invisible for every *symmetric* multiply —
896    /// `VMULF`, `VMACF`, `VMUDH`, `VMUDL` all commute — and shows up only on
897    /// `VMUDM`/`VMADM` and `VMUDN`/`VMADN`, where one operand is read signed
898    /// and the other unsigned. Naming the constants after the registers rather
899    /// than after the operand roles is what keeps the distinction visible here.
900    const V0: [u16; 8] = [
901        0x0000, 0x0000, 0x0000, 0xE000, 0x8001, 0x8000, 0x7FFF, 0x8000,
902    ];
903    const V1: [u16; 8] = [
904        0x0000, 0x0001, 0xFFFF, 0xFFFF, 0x8000, 0x7FFF, 0x7FFF, 0x8000,
905    ];
906    /// `vs` is `$v1` and `vt` is `$v0`; see [`V0`].
907    const VS_REG: usize = 1;
908    const VT_REG: usize = 0;
909
910    fn seeded() -> Rsp {
911        let mut rsp = Rsp::new();
912        rsp.vu_regs[0] = V0;
913        rsp.vu_regs[1] = V1;
914        rsp
915    }
916
917    fn acc_slice(rsp: &Rsp, shift: u32) -> [u16; 8] {
918        core::array::from_fn(|i| (rsp.vu_acc[i] >> shift) as u16)
919    }
920
921    /// **`VMULF` against n64-systemtest's own expected vectors.**
922    ///
923    /// Result *and* all three accumulator slices, because the result alone
924    /// cannot distinguish the rounding constant landing in the accumulator from
925    /// it being applied only to the extracted value — `ACC_LO` reading back
926    /// `0x8000` for a zero product is the only place that shows.
927    #[test]
928    fn vmulf_matches_the_oracle_vectors() {
929        let mut rsp = seeded();
930        assert!(rsp.vu_compute(0x00, 0, VS_REG, VT_REG, 2));
931        assert_eq!(
932            rsp.vu_regs[2],
933            [0, 0, 0, 0, 0x7fff, 0x8001, 0x7ffe, 0x7fff],
934            "VMULF result"
935        );
936        assert_eq!(acc_slice(&rsp, 32), [0, 0, 0, 0, 0, 0xffff, 0, 0], "ACC_HI");
937        assert_eq!(
938            acc_slice(&rsp, 16),
939            [0, 0, 0, 0, 0x7fff, 0x8001, 0x7ffe, 0x8000],
940            "ACC_MD"
941        );
942        assert_eq!(
943            acc_slice(&rsp, 0),
944            [
945                0x8000, 0x8000, 0x8000, 0xc000, 0x8000, 0x8000, 0x8002, 0x8000
946            ],
947            "ACC_LO"
948        );
949    }
950
951    /// The last lane is the one that pins the **clamp**: the 48-bit accumulator
952    /// is positive there, so `acc >> 16` is `0x8000` = 32768, one past the
953    /// signed maximum, and the result saturates to `0x7FFF`.
954    #[test]
955    fn vmulf_saturates_where_the_accumulator_overflows_the_result() {
956        let mut rsp = seeded();
957        rsp.vu_compute(0x00, 0, VS_REG, VT_REG, 2);
958        assert_eq!(rsp.vu_acc[7] >> 16, 0x8000, "the accumulator holds 32768");
959        assert_eq!(rsp.vu_regs[2][7], 0x7FFF, "and the result saturates");
960    }
961
962    /// **`VMULU` against the oracle's vectors** — the same accumulator as
963    /// `VMULF`, differing *only* in the clamp.
964    ///
965    /// That shared path is exactly why this needs its own instruction-level
966    /// case: `clamp_unsigned` being right as a helper says nothing about the
967    /// `op == 0x01` arm selecting it, and a mis-selection would hide behind the
968    /// `VMULF` coverage. Lane 5 is the discriminator — `VMULF` gives `0x8001`
969    /// there and `VMULU` gives `0`, because the accumulator is negative and
970    /// unsigned clamping floors it. Lane 7 is the other half: positive and over
971    /// the 15-bit threshold, so it saturates to `0xFFFF` where `VMULF` gives
972    /// `0x7FFF`.
973    ///
974    /// Note this test's `vs` differs from `VMULF`'s in lane 2 (`0x0010`), which
975    /// is the oracle's own input — kept rather than normalized, so the expected
976    /// vectors can be compared against the suite verbatim.
977    #[test]
978    fn vmulu_matches_the_oracle_vectors() {
979        let mut rsp = Rsp::new();
980        rsp.vu_regs[0] = [
981            0x0000, 0x0000, 0x0010, 0xE000, 0x8001, 0x8000, 0x7FFF, 0x8000,
982        ];
983        rsp.vu_regs[1] = V1;
984        assert!(rsp.vu_compute(0x01, 0, VS_REG, VT_REG, 2));
985        assert_eq!(
986            rsp.vu_regs[2],
987            [0, 0, 0, 0, 0x7fff, 0, 0x7ffe, 0xffff],
988            "VMULU result"
989        );
990        assert_eq!(acc_slice(&rsp, 32), [0, 0, 0, 0, 0, 0xffff, 0, 0], "ACC_HI");
991        assert_eq!(
992            acc_slice(&rsp, 16),
993            [0, 0, 0, 0, 0x7fff, 0x8001, 0x7ffe, 0x8000],
994            "ACC_MD"
995        );
996        assert_eq!(
997            acc_slice(&rsp, 0),
998            [
999                0x8000, 0x8000, 0x7fe0, 0xc000, 0x8000, 0x8000, 0x8002, 0x8000
1000            ],
1001            "ACC_LO -- identical to VMULF's, since only the clamp differs"
1002        );
1003    }
1004
1005    /// **`VMUDL` against the oracle's vectors** — an unsigned product keeping
1006    /// the high half, so nothing sign-extends into the upper accumulator.
1007    #[test]
1008    fn vmudl_matches_the_oracle_vectors() {
1009        let mut rsp = seeded();
1010        assert!(rsp.vu_compute(0x04, 0, VS_REG, VT_REG, 2));
1011        assert_eq!(
1012            rsp.vu_regs[2],
1013            [0, 0, 0, 0xdfff, 0x4000, 0x3fff, 0x3fff, 0x4000],
1014            "VMUDL result"
1015        );
1016        assert_eq!(acc_slice(&rsp, 32), [0; 8], "ACC_HI stays clear");
1017        assert_eq!(acc_slice(&rsp, 16), [0; 8], "ACC_MD too");
1018    }
1019
1020    /// **The six accumulating forms, against n64-systemtest's vectors.**
1021    ///
1022    /// The suite primes the accumulator with a `VMULF` and *then* runs the
1023    /// accumulating instruction, so these reproduce that exactly — the whole
1024    /// point of the family is what it adds to an existing accumulator, and a
1025    /// test starting from zero would pass for an implementation that ignored
1026    /// the previous contents entirely.
1027    #[test]
1028    fn the_accumulating_forms_match_the_oracle_vectors() {
1029        /// `VMADN`'s test uses a **different** `$v0` from the other five —
1030        /// checked against each file rather than assumed, after taking the
1031        /// shared vector on faith produced a mismatch that looked like a bug in
1032        /// the instruction.
1033        const VMADN_V0: [u16; 8] = [
1034            0x0000, 0x8000, 0xFFFF, 0x8000, 0x8001, 0x8000, 0x7FFF, 0x8000,
1035        ];
1036
1037        /// One accumulating case, transcribed from the matching `op_*.rs`.
1038        struct Case {
1039            op: u32,
1040            name: &'static str,
1041            v0: [u16; 8],
1042            result: [u16; 8],
1043            hi: [u16; 8],
1044            md: [u16; 8],
1045            lo: [u16; 8],
1046        }
1047
1048        let cases = [
1049            Case {
1050                op: 0x08,
1051                name: "VMACF",
1052                v0: V0,
1053                result: [0, 0, 0, 0x1, 0x7fff, 0x8000, 0x7fff, 0x7fff],
1054                hi: [0, 0, 0, 0, 0, 0xffff, 0, 1],
1055                md: [0, 0, 0, 1, 0xfffe, 2, 0xfffc, 0],
1056                lo: [0x8000, 0x8000, 0x8000, 0, 0x8000, 0x8000, 0x8004, 0x8000],
1057            },
1058            Case {
1059                op: 0x09,
1060                name: "VMACU",
1061                v0: V0,
1062                result: [0, 0, 0, 1, 0xffff, 0, 0xffff, 0xffff],
1063                hi: [0, 0, 0, 0, 0, 0xffff, 0, 1],
1064                md: [0, 0, 0, 1, 0xfffe, 2, 0xfffc, 0],
1065                lo: [0x8000, 0x8000, 0x8000, 0, 0x8000, 0x8000, 0x8004, 0x8000],
1066            },
1067            Case {
1068                op: 0x0C,
1069                name: "VMADL",
1070                v0: V0,
1071                result: [
1072                    0x8000, 0x8000, 0x8000, 0x9fff, 0xc000, 0xbfff, 0xc001, 0xffff,
1073                ],
1074                hi: [0, 0, 0, 0, 0, 0xffff, 0, 0],
1075                md: [0, 0, 0, 1, 0x7fff, 0x8001, 0x7ffe, 0x8000],
1076                lo: [
1077                    0x8000, 0x8000, 0x8000, 0x9fff, 0xc000, 0xbfff, 0xc001, 0xc000,
1078                ],
1079            },
1080            Case {
1081                op: 0x0D,
1082                name: "VMADM",
1083                v0: V0,
1084                result: [0, 0, 0, 0xffff, 0x3fff, 0xc001, 0x7fff, 0x4000],
1085                hi: [0, 0, 0, 0xffff, 0, 0xffff, 0, 0],
1086                md: [0, 0, 0, 0xffff, 0x3fff, 0xc001, 0xbffd, 0x4000],
1087                lo: [0x8000, 0x8000, 0x8000, 0xe000, 0, 0, 0x8003, 0x8000],
1088            },
1089            Case {
1090                op: 0x0E,
1091                name: "VMADN",
1092                v0: VMADN_V0,
1093                result: [0x8000, 0, 0x8003, 0, 0, 0, 0xffff, 0x8000],
1094                hi: [0, 0xffff, 0xffff, 0xffff, 0, 0xffff, 0, 0],
1095                md: [0, 0xffff, 0xffff, 0x8002, 0x4000, 0x4002, 0xbffd, 0x4000],
1096                lo: [0x8000, 0, 0x8003, 0, 0, 0, 0x8003, 0x8000],
1097            },
1098            Case {
1099                op: 0x0F,
1100                name: "VMADH",
1101                v0: V0,
1102                result: [0, 0, 0, 0x2000, 0x7fff, 0x8000, 0x7fff, 0x7fff],
1103                hi: [0, 0, 0, 0, 0x3fff, 0xc000, 0x3fff, 0x4000],
1104                md: [0, 0, 0, 0x2000, 0xffff, 1, 0x7fff, 0x8000],
1105                lo: [
1106                    0x8000, 0x8000, 0x8000, 0xc000, 0x8000, 0x8000, 0x8002, 0x8000,
1107                ],
1108            },
1109        ];
1110
1111        for Case {
1112            op,
1113            name,
1114            v0,
1115            result,
1116            hi,
1117            md,
1118            lo,
1119        } in cases
1120        {
1121            let mut rsp = seeded();
1122            rsp.vu_regs[0] = v0;
1123            // Prime the accumulator exactly as the suite does.
1124            assert!(
1125                rsp.vu_compute(0x00, 0, VS_REG, VT_REG, 2),
1126                "{name}: priming VMULF"
1127            );
1128            assert!(
1129                rsp.vu_compute(op, 0, VS_REG, VT_REG, 2),
1130                "{name} is implemented"
1131            );
1132            assert_eq!(rsp.vu_regs[2], result, "{name} result");
1133            assert_eq!(acc_slice(&rsp, 32), hi, "{name} ACC_HI");
1134            assert_eq!(acc_slice(&rsp, 16), md, "{name} ACC_MD");
1135            assert_eq!(acc_slice(&rsp, 0), lo, "{name} ACC_LO");
1136        }
1137    }
1138
1139    /// **`VMACF` adds no rounding constant, where `VMULF` adds `0x8000`.**
1140    ///
1141    /// The single most confusable difference in the family, and the accumulator
1142    /// is the only place it shows: lane 3 moves from `0xC000` to `0x1_0000`, a
1143    /// delta of exactly `0x4000` = 2 x 8192. An implementation that reused
1144    /// `VMULF`'s expression would land on `0x1_8000`.
1145    #[test]
1146    fn vmacf_adds_no_rounding_constant() {
1147        let mut rsp = seeded();
1148        rsp.vu_compute(0x00, 0, VS_REG, VT_REG, 2);
1149        assert_eq!(rsp.vu_acc[3], 0xC000, "after the priming VMULF");
1150        rsp.vu_compute(0x08, 0, VS_REG, VT_REG, 2);
1151        assert_eq!(
1152            rsp.vu_acc[3], 0x1_0000,
1153            "delta is 2*vs*vt exactly, with no 0x8000 added"
1154        );
1155    }
1156
1157    /// The broadcast modifier selects which `vt` lane each lane reads.
1158    #[test]
1159    fn the_broadcast_modifier_selects_lanes() {
1160        let rsp = seeded();
1161        // 0 and 1 are both "no broadcast".
1162        for e in [0, 1] {
1163            assert_eq!(
1164                core::array::from_fn::<u16, 8, _>(|i| rsp.vt_lane(1, e, i)),
1165                V1
1166            );
1167        }
1168        // e(0q): pairs take the even lane.
1169        assert_eq!(
1170            core::array::from_fn::<u16, 8, _>(|i| rsp.vt_lane(1, 2, i)),
1171            [V1[0], V1[0], V1[2], V1[2], V1[4], V1[4], V1[6], V1[6]]
1172        );
1173        // e(2h): each group of four takes lane 2 or 6.
1174        assert_eq!(
1175            core::array::from_fn::<u16, 8, _>(|i| rsp.vt_lane(1, 6, i)),
1176            [V1[2], V1[2], V1[2], V1[2], V1[6], V1[6], V1[6], V1[6]]
1177        );
1178        // e(5): lane 5 everywhere.
1179        assert_eq!(
1180            core::array::from_fn::<u16, 8, _>(|i| rsp.vt_lane(1, 13, i)),
1181            [V1[5]; 8]
1182        );
1183    }
1184
1185    /// `VSAR` reads back the slice the element field names.
1186    #[test]
1187    fn vsar_reads_the_accumulator_slices() {
1188        let mut rsp = seeded();
1189        rsp.vu_compute(0x00, 0, VS_REG, VT_REG, 2);
1190        let hi = acc_slice(&rsp, 32);
1191        let md = acc_slice(&rsp, 16);
1192        let lo = acc_slice(&rsp, 0);
1193
1194        rsp.vu_compute(0x1D, 8, 0, 0, 3);
1195        assert_eq!(rsp.vu_regs[3], hi, "element 8 = ACC_HI");
1196        rsp.vu_compute(0x1D, 9, 0, 0, 4);
1197        assert_eq!(rsp.vu_regs[4], md, "element 9 = ACC_MD");
1198        rsp.vu_compute(0x1D, 10, 0, 0, 5);
1199        assert_eq!(rsp.vu_regs[5], lo, "element 10 = ACC_LO");
1200    }
1201
1202    /// **Unsigned clamping saturates at a 15-bit threshold to a 16-bit value.**
1203    ///
1204    /// A naive `> 65535` test lets `0x8000..=0xFFFF` through unchanged; the rule
1205    /// is that anything above `0x7FFF` becomes `0xFFFF`.
1206    #[test]
1207    fn unsigned_clamping_uses_a_fifteen_bit_threshold() {
1208        assert_eq!(Rsp::clamp_unsigned(-1), 0);
1209        assert_eq!(Rsp::clamp_unsigned(0x7FFF), 0x7FFF);
1210        assert_eq!(Rsp::clamp_unsigned(0x8000), 0xFFFF, "not 0x8000");
1211        assert_eq!(Rsp::clamp_unsigned(0xFFFF), 0xFFFF);
1212    }
1213
1214    /// The bitwise group computes and leaves its result in `ACC_LO`.
1215    #[test]
1216    fn the_bitwise_group_writes_the_accumulator_low_slice() {
1217        let mut rsp = seeded();
1218        assert!(rsp.vu_compute(0x28, 0, VS_REG, VT_REG, 2)); // VAND
1219        assert_eq!(rsp.vu_regs[2][3], V1[3] & V0[3]);
1220        assert_eq!(acc_slice(&rsp, 0)[3], V1[3] & V0[3], "ACC_LO follows");
1221
1222        assert!(rsp.vu_compute(0x29, 0, VS_REG, VT_REG, 3)); // VNAND
1223        assert_eq!(rsp.vu_regs[3][3], !(V1[3] & V0[3]));
1224    }
1225
1226    /// An unimplemented opcode reports so, rather than writing a wrong result.
1227    ///
1228    /// `0x3F` is chosen because it is genuinely unassigned — an opcode from the
1229    /// not-yet-implemented list would silently turn this test into a no-op the
1230    /// day it lands, which is what happened when it named `VMACF`.
1231    #[test]
1232    fn an_unimplemented_opcode_is_reported_not_guessed() {
1233        let mut rsp = seeded();
1234        assert!(
1235            !rsp.vu_compute(0x3F, 0, VS_REG, VT_REG, 2),
1236            "an opcode with no implementation reports rather than guessing"
1237        );
1238        assert_eq!(rsp.vu_regs[2], [0; 8], "and it wrote nothing");
1239    }
1240}
1241
1242/// The vector load/store family (Sprint 3, brought forward).
1243///
1244/// Encoding: `LWC2`/`SWC2` | `base` (25..21) | `vt` (20..16) | `opcode`
1245/// (15..11) | `element` (10..7) | `offset` (6..0, **signed 7-bit**).
1246///
1247/// The offset is scaled by the access size, and `element` is a **byte** index
1248/// into the vector register naming the first byte the operation touches — so a
1249/// non-zero element means *fewer* bytes move, not a shifted window.
1250impl Rsp {
1251    /// Sign-extend the 7-bit offset field.
1252    const fn sext7(offset: u32) -> i32 {
1253        (offset & 0x7F).cast_signed() << 25 >> 25
1254    }
1255
1256    /// Execute a vector load or store. Returns `false` for an opcode not
1257    /// implemented yet, leaving the instruction inert.
1258    pub fn vector_mem(
1259        &mut self,
1260        store: bool,
1261        op: u32,
1262        base: usize,
1263        vt: usize,
1264        element: usize,
1265        offset: u32,
1266    ) -> bool {
1267        let rs = self.r(base);
1268        match op {
1269            // Scalar group: 1, 2, 4 or 8 bytes, the size doubling with the
1270            // opcode. The offset scales by that same size.
1271            0x00..=0x03 => {
1272                let size = 1usize << op;
1273                let addr = rs.wrapping_add_signed(Self::sext7(offset) * size.cast_signed() as i32);
1274                // **Loads and stores treat the register end differently** --
1275                // n64-systemtest says so outright (op_vector_stores.rs:17):
1276                // "the element specifier specifies the starting element. If
1277                // there isn't enough room after e, there is wrap-around inside
1278                // of the register (this is *different from loads*)".
1279                //
1280                // LOAD: no wrap; a short tail past byte 15 simply reduces the
1281                // count ("only LSV/LLV/LDV can overflow"). STORE: the register
1282                // index wraps (`& 15`) and the full width always moves -- the
1283                // element rotates the source, it does not shorten the transfer.
1284                if store {
1285                    for i in 0..size {
1286                        let byte = (element + i) & 15;
1287                        let at = addr.wrapping_add(i as u32);
1288                        let v = self.vu_byte(vt, byte);
1289                        self.dmem_write_pub(at, v);
1290                    }
1291                } else {
1292                    let size = core::cmp::min(size, 16 - element);
1293                    for i in 0..size {
1294                        let byte = element + i;
1295                        let at = addr.wrapping_add(i as u32);
1296                        let v = self.dmem_read_pub(at);
1297                        self.set_vu_byte(vt, byte, v);
1298                    }
1299                }
1300                true
1301            }
1302            // `LQV`/`SQV`: up to 16 bytes, **left-aligned** — the transfer runs
1303            // from the address up to (and including) the last byte before the
1304            // next 16-byte boundary, so a misaligned address moves fewer bytes
1305            // rather than crossing the boundary.
1306            0x04 => {
1307                let addr = rs.wrapping_add_signed(Self::sext7(offset) * 16);
1308                let end = addr | 15;
1309                // The transfer runs to the 16-byte boundary. On a STORE that
1310                // count is `16 - (addr & 15)` regardless of the element (the
1311                // element only rotates the wrapped source); on a LOAD the
1312                // element shortens it, since loads do not wrap (see the scalar
1313                // group above and op_vector_stores.rs:17).
1314                let size = if store {
1315                    end - addr
1316                } else {
1317                    core::cmp::min(end - addr, 15 - element as u32)
1318                };
1319                for i in 0..=size {
1320                    let byte = (element + i as usize) & 15;
1321                    let at = addr.wrapping_add(i);
1322                    if store {
1323                        let v = self.vu_byte(vt, byte);
1324                        self.dmem_write_pub(at, v);
1325                    } else {
1326                        let v = self.dmem_read_pub(at);
1327                        self.set_vu_byte(vt, byte, v);
1328                    }
1329                }
1330                true
1331            }
1332            // `LRV`/`SRV`: the **right-aligned** partner of `LQV`, and the
1333            // reason a misaligned 128-bit access needs two instructions.
1334            //
1335            // The transfer runs from the *previous* 16-byte boundary up to (and
1336            // excluding) the address, and it lands at the *far end* of the
1337            // register: with 8 bytes to move they go to `VPR[8..15]`, not
1338            // `VPR[0..7]`. The element field then shortens it from the front on
1339            // the DMEM side while moving the destination up — the wiki's own
1340            // worked example has `e(2)` read bytes `0x10..0x13` into
1341            // `VPR[12..15]`, which pins both halves of that at once.
1342            0x05 => {
1343                let end = rs.wrapping_add_signed(Self::sext7(offset) * 16);
1344                let addr = end & !15;
1345                let n = (end & 15) as usize;
1346                if store {
1347                    // SRV: the `n` bytes from the boundary up to `end`, pulled
1348                    // from the register starting at `16 - n` and wrapping (`&
1349                    // 15`). The element rotates the source; unlike the load it
1350                    // does not shorten the transfer (op_vector_stores.rs:191).
1351                    for i in 0..n {
1352                        let byte = (element + 16 - n + i) & 15;
1353                        let at = addr.wrapping_add(i as u32);
1354                        let v = self.vu_byte(vt, byte);
1355                        self.dmem_write_pub(at, v);
1356                    }
1357                } else if element < n {
1358                    // LRV: loads do not wrap, so the element shortens the count
1359                    // and shifts the destination toward the register's far end.
1360                    let count = n - element;
1361                    let dest_base = 16 - n + element;
1362                    for i in 0..count {
1363                        let byte = dest_base + i;
1364                        let at = addr.wrapping_add(i as u32);
1365                        let v = self.dmem_read_pub(at);
1366                        self.set_vu_byte(vt, byte, v);
1367                    }
1368                }
1369                true
1370            }
1371            0x06..=0x08 => self.vector_mem_packed(store, op, rs, vt, element, offset),
1372            // LTV/STV: the **transpose**. These touch a whole *group* of eight
1373            // registers (`vt & ~7`), moving one lane into or out of each with
1374            // rotating byte offsets and wraparound inside a 16-byte window. They
1375            // do not fit the single-register loop above, so they have their own
1376            // handler. LFV/SFV (0x09) and SWV (0x0A) are handled below.
1377            0x0B => {
1378                let addr = Self::base_16(rs, offset);
1379                if store {
1380                    self.stv(addr, vt, element);
1381                } else {
1382                    self.ltv(addr, vt, element);
1383                }
1384                true
1385            }
1386            // LFV load. Derived from n64-systemtest's OWN reference, which
1387            // conflicts with ares for e != 0 (ares uses `mis - e` for lane 0
1388            // where the suite uses `mis + e`). The oracle wins.
1389            0x09 if !store => {
1390                self.lfv(Self::base_16(rs, offset), vt, element);
1391                true
1392            }
1393            // SFV store. Also from the suite reference: it writes FOUR bytes,
1394            // whose source lanes are chosen by an `e`-dependent table, and for
1395            // any `e` not in that table it writes **zero** -- not a wrap, an
1396            // actual zero, which a table-less implementation gets wrong.
1397            0x09 => {
1398                self.sfv(Self::base_16(rs, offset), vt, element);
1399                true
1400            }
1401            // SWV store. All 16 bytes leave the register, but the DMEM target
1402            // rotates within a 16-byte window anchored to the *8-byte*-aligned
1403            // base -- `misalignment` is `& 7`, not `& 15`. The source index
1404            // wraps with the element (op_vector_stores.rs:353).
1405            0x0A if store => {
1406                let ea = rs.wrapping_add_signed(Self::sext7(offset) * 16);
1407                let base = ea & !7;
1408                let mis = ea & 7;
1409                for i in 0..16u32 {
1410                    let at = base.wrapping_add((mis + i) & 15);
1411                    let byte = (element + i as usize) & 15;
1412                    let v = self.vu_byte(vt, byte);
1413                    self.dmem_write_pub(at, v);
1414                }
1415                true
1416            }
1417            _ => false,
1418        }
1419    }
1420
1421    /// The packed (`LPV`/`LUV`/`SPV`/`SUV`) and strided (`LHV`/`SHV`) memory
1422    /// ops, split out of [`Rsp::vector_mem`] to keep its decode under length.
1423    fn vector_mem_packed(
1424        &mut self,
1425        store: bool,
1426        op: u32,
1427        rs: u32,
1428        vt: usize,
1429        element: usize,
1430        offset: u32,
1431    ) -> bool {
1432        match op {
1433            // LPV/LUV (loads) and SPV/SUV (stores): the **packed** family.
1434            // Eight bytes move, one per lane, each byte occupying a lane's high
1435            // portion. LPV/SPV place it at bit 15 (`<< 8` / `.byte()`), LUV/SUV
1436            // one bit lower at bit 14 (`<< 7`). The offset scales by 8.
1437            0x06 | 0x07 => {
1438                let base = rs.wrapping_add_signed(Self::sext7(offset) * 8);
1439                // `index` folds the element field into the DMEM byte the first
1440                // lane reads; it can go negative, so it wraps mod 16.
1441                let index = (base & 7).wrapping_sub(element as u32);
1442                let low = op == 0x07; // LUV/SUV shift one bit lower
1443                if store {
1444                    // The store side does NOT align the address (ares SPV/SUV),
1445                    // unlike the load, which is why `base` is passed unmasked.
1446                    self.packed_store(vt, base, element, low);
1447                } else {
1448                    let addr = base & !7;
1449                    for lane in 0..8u32 {
1450                        let byte = index.wrapping_add(lane) & 15;
1451                        let v = u16::from(self.dmem_read_pub(addr.wrapping_add(byte)));
1452                        self.vu_regs[vt & 31][lane as usize] = v << if low { 7 } else { 8 };
1453                    }
1454                }
1455                true
1456            }
1457            // LHV/SHV: the **strided** pair, accessing every *other* DMEM byte.
1458            // Like the packed family but the DMEM index steps by 2 per lane, and
1459            // the value lands one bit lower (bit 14). The offset scales by 16.
1460            // LFV/SFV (0x09) and the transposing LTV/STV (0x0B) are not here yet
1461            // -- LFV/SFV have element-dependent lane subsets the suite itself
1462            // calls "complicated", and are left to derive fresh rather than
1463            // guessed.
1464            0x08 => {
1465                let base = rs.wrapping_add_signed(Self::sext7(offset) * 16);
1466                let addr = base & !7;
1467                if store {
1468                    // The store reads the register at stride 2 and combines each
1469                    // adjacent byte pair into one DMEM byte; index is the raw low
1470                    // 3 bits, NOT folded with the element field (which the load
1471                    // does). See ares SHV vs LHV.
1472                    let index = base & 7;
1473                    for lane in 0..8u32 {
1474                        let byte = element + (lane as usize) * 2;
1475                        let hi = u16::from(self.vu_byte(vt, byte & 15));
1476                        let lo = u16::from(self.vu_byte(vt, (byte + 1) & 15));
1477                        let value = ((hi << 1) | (lo >> 7)) as u8;
1478                        let at = index.wrapping_add(lane * 2) & 15;
1479                        self.dmem_write_pub(addr.wrapping_add(at), value);
1480                    }
1481                } else {
1482                    let index = (base & 7).wrapping_sub(element as u32);
1483                    for lane in 0..8u32 {
1484                        let at = index.wrapping_add(lane * 2) & 15;
1485                        let v = u16::from(self.dmem_read_pub(addr.wrapping_add(at)));
1486                        self.vu_regs[vt & 31][lane as usize] = v << 7;
1487                    }
1488                }
1489                true
1490            }
1491            _ => false,
1492        }
1493    }
1494
1495    /// Base address for a 16-scaled offset, used by the whole-vector ops.
1496    fn base_16(rs: u32, offset: u32) -> u32 {
1497        rs.wrapping_add_signed(Self::sext7(offset) * 16)
1498    }
1499
1500    /// `LFV` — the "complicated" fractional load, transcribed from
1501    /// n64-systemtest's reference (not ares, which differs for `e != 0`).
1502    ///
1503    /// Eight lanes are computed from a fixed offset pattern around the aligned
1504    /// address, each byte placed at bit 14. Then only the register **bytes**
1505    /// `[e, e + min(8, 16 - e))` are written from that temporary — the rest of
1506    /// the register is left as it was, which is the partial-write behavior the
1507    /// name hides.
1508    fn lfv(&mut self, address: u32, vt: usize, element: usize) {
1509        let aligned = address & !7;
1510        let mis = (address & 7).cast_signed();
1511        let e = i32::try_from(element).unwrap_or(0);
1512        let at = |k: i32| -> u16 {
1513            let idx = aligned.wrapping_add(((mis + k).rem_euclid(16)) as u32);
1514            u16::from(self.dmem_read_pub(idx)) << 7
1515        };
1516        // The offset pattern, per n64-systemtest's LFV reference.
1517        let temp: [u16; 8] = [
1518            at(e),
1519            at(4 - e),
1520            at(8 - e),
1521            at(12 - e),
1522            at(8 - e),
1523            at(12 - e),
1524            at(-e),
1525            at(4 - e),
1526        ];
1527        // temp as 16 big-endian bytes.
1528        let mut bytes = [0u8; 16];
1529        for (lane, v) in temp.iter().enumerate() {
1530            bytes[lane * 2] = (v >> 8) as u8;
1531            bytes[lane * 2 + 1] = *v as u8;
1532        }
1533        let length = core::cmp::min(8, 16 - element);
1534        for (i, &b) in bytes.iter().enumerate().skip(element).take(length) {
1535            self.set_vu_byte(vt, i, b);
1536        }
1537    }
1538
1539    /// `SFV` — the fractional store, transcribed from n64-systemtest's
1540    /// reference.
1541    ///
1542    /// Four bytes are written at DMEM stride 4 within the aligned window. Which
1543    /// four source lanes supply them depends on `e` through a fixed table; for
1544    /// any `e` **not** in the table the bytes are **zero** — a real zero, not a
1545    /// wrap, which is the "even 0 for some E" behavior the suite warns about.
1546    fn sfv(&mut self, address: u32, vt: usize, element: usize) {
1547        // The source-lane table (N64brew / n64-systemtest). `None` -> write 0.
1548        let lanes: Option<[usize; 4]> = match element {
1549            0 | 15 => Some([0, 1, 2, 3]),
1550            1 => Some([6, 7, 4, 5]),
1551            4 => Some([1, 2, 3, 0]),
1552            5 => Some([7, 4, 5, 6]),
1553            8 => Some([4, 5, 6, 7]),
1554            11 => Some([3, 0, 1, 2]),
1555            12 => Some([5, 6, 7, 4]),
1556            _ => None,
1557        };
1558        let a = address & 7;
1559        let b = address & !7;
1560        for i in 0..4u32 {
1561            let value = lanes.map_or(0, |l| (self.vu_regs[vt & 31][l[i as usize]] >> 7) as u8);
1562            let at = b + ((a + (i << 2)) & 15);
1563            self.dmem_write_pub(at, value);
1564        }
1565    }
1566
1567    /// `LTV` — load a transposed diagonal into a register group.
1568    ///
1569    /// Transcribed from ares. The window is the 16 bytes at `rs & ~7`; the read
1570    /// pointer starts at an element-and-address-dependent offset and wraps at
1571    /// the window's end; each iteration fills one lane of the next register in
1572    /// the group, the register index rotating mod 8. Modeled and cross-checked
1573    /// in a scratch script before implementation, per the note in
1574    /// `docs/rsp.md`.
1575    fn ltv(&mut self, address: u32, vt: usize, element: usize) {
1576        let begin = address & !7;
1577        let mut ptr = begin + ((element as u32 + (address & 8)) & 15);
1578        let vtbase = vt & !7;
1579        let mut vtoff = element >> 1;
1580        for i in 0..8usize {
1581            for half in 0..2 {
1582                let v = self.dmem_read_pub(ptr);
1583                self.set_vu_byte(vtbase + vtoff, i * 2 + half, v);
1584                ptr += 1;
1585                if ptr == begin + 16 {
1586                    ptr = begin;
1587                }
1588            }
1589            vtoff = (vtoff + 1) & 7;
1590        }
1591    }
1592
1593    /// `STV` — store a register group as a transposed diagonal. The mirror of
1594    /// [`Rsp::ltv`], with its own distinct offset arithmetic (also from ares).
1595    fn stv(&mut self, address: u32, vt: usize, element: usize) {
1596        let start = vt & !7;
1597        let mut elem = 16 - (element & !1);
1598        let mut base = (address & 7).wrapping_sub((element & !1) as u32);
1599        let addr = address & !7;
1600        for offset in start..start + 8 {
1601            let b0 = self.vu_byte(offset, elem & 15);
1602            self.dmem_write_pub(addr.wrapping_add(base & 15), b0);
1603            base = base.wrapping_add(1);
1604            elem += 1;
1605            let b1 = self.vu_byte(offset, elem & 15);
1606            self.dmem_write_pub(addr.wrapping_add(base & 15), b1);
1607            base = base.wrapping_add(1);
1608            elem += 1;
1609        }
1610    }
1611
1612    /// The `SPV`/`SUV` store half of the packed family.
1613    ///
1614    /// Each of eight consecutive DMEM bytes comes from either a lane's **high**
1615    /// byte or its value shifted down 7, and *which* is chosen alternates on a
1616    /// `(offset & 15) < 8` test — with `SPV` and `SUV` taking opposite branches.
1617    /// That split is why the store is not a mirror of the load.
1618    fn packed_store(&mut self, vt: usize, addr: u32, element: usize, suv: bool) {
1619        for i in 0..8u32 {
1620            let offset = element as u32 + i;
1621            let lane = (offset & 7) as usize;
1622            let first_half = offset & 15 < 8;
1623            // SPV: high-byte in the first half, shifted-down in the second.
1624            // SUV swaps the two.
1625            let high_byte = first_half != suv;
1626            let byte = if high_byte {
1627                self.vu_byte(vt, lane << 1)
1628            } else {
1629                (self.vu_regs[vt & 31][lane] >> 7) as u8
1630            };
1631            self.dmem_write_pub(addr.wrapping_add(i), byte);
1632        }
1633    }
1634
1635    /// DMEM byte read, for the vector memory paths.
1636    pub(crate) const fn dmem_read_pub(&self, addr: u32) -> u8 {
1637        self.dmem[(addr & 0xFFF) as usize]
1638    }
1639
1640    /// DMEM byte write, for the vector memory paths.
1641    pub(crate) const fn dmem_write_pub(&mut self, addr: u32, val: u8) {
1642        self.dmem[(addr & 0xFFF) as usize] = val;
1643    }
1644}
1645
1646#[cfg(test)]
1647mod mem_tests {
1648    use super::*;
1649
1650    fn with_dmem(pattern: &[u8]) -> Rsp {
1651        let mut rsp = Rsp::new();
1652        for (i, b) in pattern.iter().enumerate() {
1653            rsp.dmem[i] = *b;
1654        }
1655        rsp
1656    }
1657
1658    /// **An aligned `LQV` fills the whole register.**
1659    #[test]
1660    fn an_aligned_lqv_loads_sixteen_bytes() {
1661        let bytes: [u8; 16] = core::array::from_fn(|i| i as u8 + 0x10);
1662        let mut rsp = with_dmem(&bytes);
1663        assert!(rsp.vector_mem(false, 0x04, 0, 1, 0, 0));
1664        for (i, want) in bytes.iter().enumerate() {
1665            assert_eq!(rsp.vu_byte(1, i), *want, "byte {i}");
1666        }
1667    }
1668
1669    /// **A misaligned `LQV` stops at the 16-byte boundary rather than crossing
1670    /// it.** This is the whole reason `LRV` exists, and an implementation that
1671    /// simply reads 16 bytes from the address passes an aligned test and fails
1672    /// here.
1673    #[test]
1674    fn a_misaligned_lqv_stops_at_the_boundary() {
1675        let bytes: [u8; 32] = core::array::from_fn(|i| i as u8 + 0x10);
1676        let mut rsp = with_dmem(&bytes);
1677        // Address 0x08: eight bytes to the boundary at 0x10.
1678        rsp.set_su(2, 8);
1679        assert!(rsp.vector_mem(false, 0x04, 2, 1, 0, 0));
1680        for i in 0..8 {
1681            assert_eq!(rsp.vu_byte(1, i), bytes[8 + i], "loaded byte {i}");
1682        }
1683        for i in 8..16 {
1684            assert_eq!(rsp.vu_byte(1, i), 0, "byte {i} must be untouched");
1685        }
1686    }
1687
1688    /// A non-zero `element` moves **fewer** bytes: the window is
1689    /// `VPR[element..15]`, not a shifted 16.
1690    #[test]
1691    fn a_non_zero_element_shortens_the_transfer() {
1692        let bytes: [u8; 16] = core::array::from_fn(|i| i as u8 + 0x10);
1693        let mut rsp = with_dmem(&bytes);
1694        assert!(rsp.vector_mem(false, 0x04, 0, 1, 12, 0));
1695        for i in 0..12 {
1696            assert_eq!(rsp.vu_byte(1, i), 0, "below the element, untouched");
1697        }
1698        for i in 12..16 {
1699            assert_eq!(rsp.vu_byte(1, i), bytes[i - 12], "from the start of DMEM");
1700        }
1701    }
1702
1703    /// `SQV` is the mirror: the register's bytes land in DMEM.
1704    #[test]
1705    fn sqv_round_trips_with_lqv() {
1706        let bytes: [u8; 16] = core::array::from_fn(|i| i as u8 + 0xA0);
1707        let mut rsp = with_dmem(&bytes);
1708        rsp.vector_mem(false, 0x04, 0, 1, 0, 0);
1709        rsp.set_su(2, 0x100);
1710        assert!(rsp.vector_mem(true, 0x04, 2, 1, 0, 0));
1711        for (i, want) in bytes.iter().enumerate() {
1712            assert_eq!(rsp.dmem[0x100 + i], *want, "stored byte {i}");
1713        }
1714    }
1715
1716    /// The scalar group's offset scales by the access size, which is what makes
1717    /// `LDV`'s reach eight times `LBV`'s for the same encoded offset.
1718    #[test]
1719    fn the_scalar_offset_scales_with_the_access_size() {
1720        let bytes: [u8; 64] = core::array::from_fn(|i| i as u8);
1721        let mut rsp = with_dmem(&bytes);
1722        // LBV, offset 2 -> address 2.
1723        assert!(rsp.vector_mem(false, 0x00, 0, 1, 0, 2));
1724        assert_eq!(rsp.vu_byte(1, 0), 2);
1725        // LDV, offset 2 -> address 16.
1726        assert!(rsp.vector_mem(false, 0x03, 0, 2, 0, 2));
1727        assert_eq!(rsp.vu_byte(2, 0), 16);
1728    }
1729
1730    /// **`LRV` lands at the far end of the register, not the near one.**
1731    ///
1732    /// The wiki's worked example, reproduced exactly: with `a0` 16-byte aligned,
1733    /// `lrv $v0, 0x18(a0)` reads bytes `0x10..0x17` into `VPR[8..15]`. An
1734    /// implementation that writes from byte 0 — the natural mirror of `LQV` —
1735    /// puts the right-hand half of the vector in the left-hand slots, and the
1736    /// pair no longer reconstructs a misaligned 128-bit load.
1737    #[test]
1738    fn lrv_loads_into_the_far_end_of_the_register() {
1739        let bytes: [u8; 48] = core::array::from_fn(|i| i as u8 + 0x10);
1740        let mut rsp = with_dmem(&bytes);
1741        rsp.set_su(2, 0x18);
1742        assert!(rsp.vector_mem(false, 0x05, 2, 1, 0, 0));
1743        for i in 0..8 {
1744            assert_eq!(rsp.vu_byte(1, i), 0, "the left half stays untouched");
1745        }
1746        for i in 8..16 {
1747            assert_eq!(
1748                rsp.vu_byte(1, i),
1749                bytes[0x10 + (i - 8)],
1750                "byte {i} comes from the previous 16-byte boundary"
1751            );
1752        }
1753    }
1754
1755    /// The element field shortens `LRV` from the front on the DMEM side while
1756    /// moving the destination **up** — the wiki's `e(2)` example reads
1757    /// `0x10..0x13` into `VPR[12..15]`.
1758    #[test]
1759    fn lrv_with_an_element_shortens_from_the_front() {
1760        let bytes: [u8; 48] = core::array::from_fn(|i| i as u8 + 0x10);
1761        let mut rsp = with_dmem(&bytes);
1762        rsp.set_su(2, 0x18);
1763        assert!(rsp.vector_mem(false, 0x05, 2, 1, 4, 0));
1764        for i in 0..12 {
1765            assert_eq!(rsp.vu_byte(1, i), 0, "byte {i} untouched");
1766        }
1767        for i in 12..16 {
1768            assert_eq!(
1769                rsp.vu_byte(1, i),
1770                bytes[0x10 + (i - 12)],
1771                "byte {i} still starts from the boundary, not from +4"
1772            );
1773        }
1774    }
1775
1776    /// **`LQV` and `LRV` together reconstruct a misaligned 128-bit load**, which
1777    /// is the entire point of the pair. Neither alone can.
1778    #[test]
1779    fn lqv_and_lrv_together_load_a_misaligned_vector() {
1780        let bytes: [u8; 48] = core::array::from_fn(|i| i as u8 + 0x10);
1781        let mut rsp = with_dmem(&bytes);
1782        rsp.set_su(2, 0x08);
1783        rsp.vector_mem(false, 0x04, 2, 1, 0, 0); // lqv 0x08
1784        rsp.set_su(3, 0x18);
1785        rsp.vector_mem(false, 0x05, 3, 1, 0, 0); // lrv 0x18
1786        for i in 0..16 {
1787            assert_eq!(
1788                rsp.vu_byte(1, i),
1789                bytes[8 + i],
1790                "the pair must yield the 16 bytes at 0x08"
1791            );
1792        }
1793    }
1794
1795    /// **A scalar load near the end of the register loads fewer bytes rather
1796    /// than wrapping.**
1797    ///
1798    /// The regression test for the bug this branch fixed: the register side
1799    /// masked its byte index with 15, so an `LDV` at element 12 wrapped its
1800    /// last four bytes back to `VPR[0..3]` instead of stopping. Nothing here
1801    /// caught it before — the existing scalar test uses `element = 0`, where
1802    /// wrapping and truncating agree.
1803    #[test]
1804    fn a_scalar_load_near_the_end_of_the_register_truncates() {
1805        let bytes: [u8; 16] = core::array::from_fn(|i| i as u8 + 0x10);
1806        let mut rsp = with_dmem(&bytes);
1807        // LDV wants 8 bytes; at element 12 only 4 fit.
1808        assert!(rsp.vector_mem(false, 0x03, 0, 1, 12, 0));
1809        for i in 12..16 {
1810            assert_eq!(rsp.vu_byte(1, i), bytes[i - 12], "byte {i} loaded");
1811        }
1812        for i in 0..12 {
1813            assert_eq!(
1814                rsp.vu_byte(1, i),
1815                0,
1816                "byte {i} must be untouched -- the tail must NOT wrap to the start"
1817            );
1818        }
1819    }
1820
1821    /// An unimplemented opcode reports so rather than moving wrong bytes.
1822    #[test]
1823    fn an_unimplemented_vector_memory_op_is_reported() {
1824        let mut rsp = Rsp::new();
1825        assert!(
1826            !rsp.vector_mem(false, 0x0A, 0, 1, 0, 0),
1827            "an unassigned opcode"
1828        );
1829    }
1830}
1831
1832/// The `VRCP` / `VRSQ` reciprocal ROMs.
1833///
1834/// 512 entries each, **generated by exact integer arithmetic** rather than
1835/// stored as literals — a deliberate departure from `docs/rsp.md`'s "data, not
1836/// a formula" rule, argued and bounded in accuracy ledger **C-31**. The short
1837/// version: the rule guards against *approximation*, and these constructions
1838/// have no rounding freedom, so they reproduce the ROM rather than estimating
1839/// it. The construction is ares's (ISC).
1840pub mod rom {
1841    /// The reciprocal ROM, built at **compile time**.
1842    ///
1843    /// A `static` rather than a function, which is what makes it a table in the
1844    /// binary: the generator runs during const evaluation and nothing computes
1845    /// a reciprocal at run time. An earlier revision computed each entry on
1846    /// demand, which made every `VRSQ` pay for a search — see the note on
1847    /// [`INVERSE_SQUARE_ROOT`].
1848    pub static RECIPROCAL: [u16; 512] = build_reciprocal();
1849
1850    /// The inverse-square-root ROM, likewise built at compile time.
1851    pub static INVERSE_SQUARE_ROOT: [u16; 512] = build_inverse_square_root();
1852
1853    /// `(1 << 34) / (index + 512)`, rounded.
1854    ///
1855    /// Entry 0 is `0xFFFF` and does **not** follow the formula — hardware pins
1856    /// it, and a generator that applies the formula uniformly gets the
1857    /// most-used entry wrong.
1858    const fn build_reciprocal() -> [u16; 512] {
1859        let mut table = [0u16; 512];
1860        table[0] = u16::MAX;
1861        let mut index = 1usize;
1862        while index < 512 {
1863            let a = (index as u64) + 512;
1864            let b = (1u64 << 34) / a;
1865            table[index] = ((b + 1) >> 8) as u16;
1866            index += 1;
1867        }
1868        table
1869    }
1870
1871    /// The **smallest** `b ≥ 2¹⁷` with `a·(b+1)² ≥ 2⁴⁴`, where `a` is halved on
1872    /// odd indices.
1873    ///
1874    /// Note the predicate, which is *not* what ares's comment above the same
1875    /// loop says. That comment reads "find the largest b where b < 1.0 /
1876    /// sqrt(a)", but the loop is `while cond { b += 1 }` — it walks *through*
1877    /// the last satisfying value and stops one past it. The value the table
1878    /// actually holds is therefore one greater than the comment describes.
1879    ///
1880    /// This was found by pinning the bisection against twelve values captured
1881    /// from the original upward scan: implementing the comment's predicate gave
1882    /// `26964` where the scan gives `26965`. Without that test the off-by-one
1883    /// would have shipped, since every property the other tests check
1884    /// (monotonicity, the odd/even interleave, the 16-bit range) holds just as
1885    /// well one step to the left.
1886    ///
1887    /// **Binary search, not a linear scan.** The naive upward walk runs
1888    /// ~131,000 iterations of two 64-bit multiplications for the smallest `a`,
1889    /// per entry — tolerable once at build time, disastrous when an earlier
1890    /// revision called it per instruction, and emulated in software on
1891    /// `thumbv7em`, which this crate must build for.
1892    const fn build_inverse_square_root() -> [u16; 512] {
1893        let mut table = [0u16; 512];
1894        let mut index = 0usize;
1895        while index < 512 {
1896            let a = ((index as u64) + 512) >> ((index % 2 == 1) as u32);
1897            // `b + 1` stays under 2^19 for the smallest `a`, so `a*(b+1)^2`
1898            // fits in `u64`.
1899            let mut lo = 1u64 << 17;
1900            let mut hi = 1u64 << 19;
1901            while lo < hi {
1902                let mid = u64::midpoint(lo, hi);
1903                if a * (mid + 1) * (mid + 1) >= (1u64 << 44) {
1904                    hi = mid;
1905                } else {
1906                    lo = mid + 1;
1907                }
1908            }
1909            table[index] = (lo >> 1) as u16;
1910            index += 1;
1911        }
1912        table
1913    }
1914
1915    /// The reciprocal ROM entry for `index`.
1916    #[must_use]
1917    pub fn reciprocal(index: usize) -> u16 {
1918        RECIPROCAL[index & 511]
1919    }
1920
1921    /// The inverse-square-root ROM entry for `index`.
1922    #[must_use]
1923    pub fn inverse_square_root(index: usize) -> u16 {
1924        INVERSE_SQUARE_ROOT[index & 511]
1925    }
1926}
1927
1928#[cfg(test)]
1929mod rom_tests {
1930    use super::rom;
1931
1932    /// **Entry 0 is a special case in both tables' construction.**
1933    ///
1934    /// The reciprocal formula does not produce `0xFFFF` at index 0; hardware
1935    /// pins it, and a generator that applies the formula uniformly gets the
1936    /// most-used entry wrong.
1937    #[test]
1938    fn the_reciprocal_rom_pins_its_first_entry() {
1939        assert_eq!(rom::reciprocal(0), 0xFFFF);
1940        // And the formula does take over immediately afterwards.
1941        assert_eq!(rom::reciprocal(1), ((((1u64 << 34) / 513) + 1) >> 8) as u16);
1942    }
1943
1944    /// The reciprocal ROM is **monotonically decreasing**: it approximates
1945    /// `1/x` over an increasing divisor, so any entry that rises above its
1946    /// predecessor is a construction error rather than a rounding artifact.
1947    ///
1948    /// This is a property test rather than a transcription check — it cannot
1949    /// prove the values are the hardware's, but it fails loudly for the class
1950    /// of error a generator actually makes (a wrong shift, a wrong width),
1951    /// which a handful of spot-checked entries would not.
1952    #[test]
1953    fn the_reciprocal_rom_decreases_monotonically() {
1954        for i in 2..512 {
1955            assert!(
1956                rom::reciprocal(i) <= rom::reciprocal(i - 1),
1957                "entry {i} rose above its predecessor"
1958            );
1959        }
1960    }
1961
1962    /// **The binary search reproduces the linear scan exactly.**
1963    ///
1964    /// The build switched from an upward scan to a bisection for speed, and the
1965    /// two agreeing is the whole basis for calling that a refactor rather than
1966    /// a change. These twelve values were captured from the scan *before* the
1967    /// switch; if the bisection has an off-by-one at the boundary it lands
1968    /// here rather than in a wrong vertex months later.
1969    #[test]
1970    fn the_inverse_square_root_rom_matches_the_linear_scan() {
1971        assert_eq!(
1972            core::array::from_fn::<u16, 12, _>(rom::inverse_square_root),
1973            [
1974                27145, 65535, 26965, 65280, 26785, 65026, 26607, 64774, 26430, 64523, 26253, 64274
1975            ]
1976        );
1977    }
1978
1979    /// Every entry fits the 16 bits the ROM is, at both ends of the range.
1980    #[test]
1981    fn both_roms_stay_within_sixteen_bits() {
1982        // Exercised for its panic-freedom and range; `u16` makes the bound
1983        // structural, so what this really pins is that the generators do not
1984        // overflow their intermediate `u64` arithmetic on the way.
1985        for i in 0..512 {
1986            let _ = rom::reciprocal(i);
1987            let _ = rom::inverse_square_root(i);
1988        }
1989    }
1990
1991    /// **The inverse-square-root ROM is two interleaved decreasing sequences**,
1992    /// not one.
1993    ///
1994    /// Odd indices *halve* `a`, so they cover the neighboring binade and come
1995    /// out **larger** than their even predecessors — the table alternates
1996    /// between roughly 92,000 and 131,000 rather than descending. Asserting a
1997    /// single monotonic run fails, and my first version of this test asserted
1998    /// the pairing backwards; the structure was checked against the generator's
1999    /// output rather than the assertion being flipped until it passed.
2000    ///
2001    /// That interleaving is what lets `VRSQ` cover a 2:1 input range with one
2002    /// table.
2003    #[test]
2004    fn the_inverse_square_root_rom_is_two_interleaved_sequences() {
2005        for i in (2..510).step_by(2) {
2006            assert!(
2007                rom::inverse_square_root(i) <= rom::inverse_square_root(i - 2),
2008                "the even subsequence rose at {i}"
2009            );
2010            assert!(
2011                rom::inverse_square_root(i + 1) <= rom::inverse_square_root(i - 1),
2012                "the odd subsequence rose at {i}"
2013            );
2014            assert!(
2015                rom::inverse_square_root(i + 1) > rom::inverse_square_root(i),
2016                "the odd entry at {i} must exceed its even neighbor"
2017            );
2018        }
2019    }
2020}
2021
2022/// The single-lane reciprocal group: `VRCP`, `VRSQ`, their `L`/`H` partners,
2023/// `VMOV` and `VNOP`.
2024///
2025/// These do not operate lane-wise. They read **one** lane of `vt` (selected by
2026/// the element field), write **one** lane of `vd` (selected by the `de` field,
2027/// which occupies the `vs` position), and set the whole accumulator low slice to
2028/// the broadcast `vt`.
2029impl Rsp {
2030    /// The shared core of `VRCP`/`VRCPL` and `VRSQ`/`VRSQL`.
2031    ///
2032    /// `long` selects whether a staged high half is consumed; `sqrt` selects the
2033    /// inverse-square-root table and its halved shift.
2034    fn reciprocal_core(&mut self, element: u32, vt: usize, de: usize, long: bool, sqrt: bool) {
2035        let lane = self.vt_lane(vt, element, (element & 7) as usize);
2036        // A 32-bit operand only when an `H` instruction staged one immediately
2037        // before; otherwise the 16-bit lane, sign-extended.
2038        let input: i32 = if long && self.div.pending {
2039            ((i32::from(self.div.input)) << 16) | i32::from(lane)
2040        } else {
2041            i32::from(lane.cast_signed())
2042        };
2043
2044        let mask = input >> 31;
2045        let mut data = input ^ mask;
2046        if input > -32768 {
2047            data -= mask;
2048        }
2049
2050        let result: i32 = if data == 0 {
2051            // Division by zero saturates rather than faulting -- the RSP has no
2052            // exception mechanism to report it with.
2053            0x7FFF_FFFF
2054        } else if input == -32768 {
2055            // The one input whose negation is not representable.
2056            0xFFFF_0000u32.cast_signed()
2057        } else {
2058            let shift = data.cast_unsigned().leading_zeros();
2059            let index = ((u64::from(data.cast_unsigned()) << shift) & 0x7FC0_0000) >> 22;
2060            let entry = if sqrt {
2061                // The odd/even interleave: the low bit of the shift picks which
2062                // of the two sequences the entry comes from.
2063                rom::inverse_square_root(((index as usize) & 0x1FE) | (shift as usize & 1))
2064            } else {
2065                rom::reciprocal(index as usize)
2066            };
2067            let r = (0x10000 | i32::from(entry)) << 14;
2068            // The square root halves the renormalizing shift, because it is
2069            // undoing a squaring.
2070            let back = if sqrt { (31 - shift) >> 1 } else { 31 - shift };
2071            (r >> back) ^ mask
2072        };
2073
2074        self.div.pending = false;
2075        self.div.output = (result.cast_unsigned() >> 16) as u16;
2076        self.acc_low_from_broadcast(element, vt);
2077        self.set_vd_lane(de, result.cast_unsigned() as u16);
2078    }
2079
2080    /// Set every accumulator low slice to the broadcast `vt`, which each of
2081    /// these instructions does regardless of what it computes.
2082    fn acc_low_from_broadcast(&mut self, element: u32, vt: usize) {
2083        for lane in 0..8 {
2084            let v = self.vt_lane(vt, element, lane);
2085            self.vu_acc[lane] = (self.vu_acc[lane] & 0xFFFF_FFFF_0000) | u64::from(v);
2086        }
2087    }
2088
2089    /// Write the destination lane. Stored separately so `vd` can be resolved
2090    /// once by the caller.
2091    fn set_vd_lane(&mut self, de: usize, value: u16) {
2092        self.pending_vd_lane = Some((de, value));
2093    }
2094
2095    /// The single-lane group's dispatch. Returns `false` for an opcode outside
2096    /// it, so the caller can fall through.
2097    pub fn vu_single_lane(
2098        &mut self,
2099        op: u32,
2100        element: u32,
2101        vt: usize,
2102        de: usize,
2103        vd: usize,
2104    ) -> bool {
2105        let de = de & 7;
2106        match op {
2107            0x30 | 0x31 | 0x34 | 0x35 => {
2108                let long = op == 0x31 || op == 0x35;
2109                let sqrt = op >= 0x34;
2110                self.reciprocal_core(element, vt, de, long, sqrt);
2111            }
2112            // The `H` partners stage the high half and hand back the high half
2113            // of the previous result. They compute nothing themselves.
2114            0x32 | 0x36 => {
2115                self.acc_low_from_broadcast(element, vt);
2116                self.div.input = self.vt_lane(vt, element, (element & 7) as usize);
2117                self.div.pending = true;
2118                self.pending_vd_lane = Some((de, self.div.output));
2119            }
2120            // VMOV copies one lane of the broadcast source.
2121            0x33 => {
2122                self.acc_low_from_broadcast(element, vt);
2123                let v = self.vt_lane(vt, element, de);
2124                self.pending_vd_lane = Some((de, v));
2125            }
2126            // VNOP and VNULL retire without effect.
2127            0x37 | 0x3F => return true,
2128            _ => return false,
2129        }
2130        if let Some((lane, value)) = self.pending_vd_lane.take() {
2131            self.vu_regs[vd & 31][lane] = value;
2132        }
2133        true
2134    }
2135}
2136
2137#[cfg(test)]
2138mod reciprocal_tests {
2139    use super::*;
2140
2141    /// **`VRCPH` stages a high half and hands back the previous result's.**
2142    ///
2143    /// Both halves of that matter: the write-back is the *old* `DIVOUT`, not
2144    /// anything derived from this instruction's operand, so an implementation
2145    /// that returns the staged value instead looks plausible and produces
2146    /// garbage on the second use.
2147    #[test]
2148    fn vrcph_stages_the_input_and_returns_the_previous_output() {
2149        let mut rsp = Rsp::new();
2150        rsp.vu_regs[1] = [0x1234; 8];
2151        rsp.div.output = 0xBEEF;
2152
2153        assert!(rsp.vu_single_lane(0x32, 0, 1, 0, 2));
2154        assert_eq!(rsp.vu_regs[2][0], 0xBEEF, "the PREVIOUS output comes back");
2155        assert_eq!(rsp.div.input, 0x1234, "and this operand is staged");
2156        assert!(rsp.div.pending);
2157    }
2158
2159    /// **`VRCPL` consumes a staged half only when one was just staged.**
2160    ///
2161    /// `pending` is what separates "a high half was staged by the preceding
2162    /// instruction" from "there is a stale value in the latch". Without it an
2163    /// `L` issued on its own silently consumes whatever the last `H` left, and
2164    /// the result depends on unrelated code that ran earlier.
2165    #[test]
2166    fn vrcpl_consumes_a_staged_half_only_once() {
2167        let mut rsp = Rsp::new();
2168        rsp.vu_regs[1] = [0x0002; 8];
2169
2170        // Stage, then consume: `pending` must be cleared by the consumer.
2171        rsp.vu_single_lane(0x32, 0, 1, 0, 2);
2172        assert!(rsp.div.pending);
2173        rsp.vu_single_lane(0x31, 0, 1, 0, 3);
2174        assert!(!rsp.div.pending, "the L instruction clears the staging");
2175
2176        // A second L with nothing staged must take the 16-bit path. Compare it
2177        // against a plain VRCP of the same operand, which is the same path.
2178        let mut plain = Rsp::new();
2179        plain.vu_regs[1] = [0x0002; 8];
2180        plain.vu_single_lane(0x30, 0, 1, 0, 4);
2181
2182        rsp.vu_single_lane(0x31, 0, 1, 0, 5);
2183        assert_eq!(
2184            rsp.vu_regs[5][0], plain.vu_regs[4][0],
2185            "an unstaged L is a plain 16-bit reciprocal"
2186        );
2187    }
2188
2189    /// Division by zero **saturates** rather than faulting — the RSP has no
2190    /// exception mechanism, so there is nothing for it to raise.
2191    #[test]
2192    fn reciprocal_of_zero_saturates() {
2193        let mut rsp = Rsp::new();
2194        rsp.vu_regs[1] = [0; 8];
2195        rsp.vu_single_lane(0x30, 0, 1, 0, 2);
2196        assert_eq!(rsp.div.output, 0x7FFF, "the high half of 0x7FFF_FFFF");
2197        assert_eq!(rsp.vu_regs[2][0], 0xFFFF, "and the low half");
2198    }
2199
2200    /// Every one of these writes the accumulator's low slice from the broadcast
2201    /// source, whatever else it does — including `VRCPH`, which computes
2202    /// nothing.
2203    #[test]
2204    fn the_single_lane_group_always_writes_the_accumulator_low_slice() {
2205        for op in [0x30u32, 0x31, 0x32, 0x33, 0x34, 0x35, 0x36] {
2206            let mut rsp = Rsp::new();
2207            rsp.vu_regs[1] = [0xABCD; 8];
2208            rsp.vu_single_lane(op, 0, 1, 0, 2);
2209            assert_eq!(
2210                (rsp.vu_acc[5] & 0xFFFF) as u16,
2211                0xABCD,
2212                "opcode {op:#04x} did not write ACC_LO"
2213            );
2214        }
2215    }
2216
2217    /// `VNOP` retires without touching anything — including the accumulator,
2218    /// which separates it from the rest of the group.
2219    #[test]
2220    fn vnop_does_nothing_at_all() {
2221        let mut rsp = Rsp::new();
2222        rsp.vu_regs[1] = [0xABCD; 8];
2223        assert!(rsp.vu_single_lane(0x37, 0, 1, 0, 2));
2224        assert_eq!(rsp.vu_regs[2], [0; 8]);
2225        assert_eq!(rsp.vu_acc, [0; 8], "not even the accumulator");
2226    }
2227}
2228
2229#[cfg(test)]
2230mod arithmetic_tests {
2231    use super::*;
2232
2233    fn pair(a: [u16; 8], b: [u16; 8]) -> Rsp {
2234        let mut rsp = Rsp::new();
2235        rsp.vu_regs[1] = a; // vs
2236        rsp.vu_regs[0] = b; // vt
2237        rsp
2238    }
2239
2240    /// **`VADD` consumes `VCO`'s carry and then clears the whole register.**
2241    ///
2242    /// Both halves matter. Not consuming it makes multi-precision addition
2243    /// silently lose the carry; not clearing it makes the *next* `VADD` add a
2244    /// stale one, which surfaces as an off-by-one in a single lane much later.
2245    #[test]
2246    fn vadd_consumes_the_carry_and_clears_vco() {
2247        let mut rsp = pair([1; 8], [1; 8]);
2248        rsp.vu_ctrl.vco = 0b0000_0101; // carry into lanes 0 and 2
2249        assert!(rsp.vu_compute(0x10, 0, 1, 0, 2));
2250
2251        assert_eq!(rsp.vu_regs[2][0], 3, "1 + 1 + carry");
2252        assert_eq!(rsp.vu_regs[2][1], 2, "1 + 1, no carry");
2253        assert_eq!(rsp.vu_regs[2][2], 3, "carry again");
2254        assert_eq!(rsp.vu_ctrl.vco, 0, "VCO is cleared wholesale");
2255    }
2256
2257    /// `VADD` **clamps** its result while the accumulator keeps the unclamped
2258    /// low bits — the two disagree on overflow, and a test reading only `vd`
2259    /// cannot tell.
2260    #[test]
2261    fn vadd_clamps_the_result_but_not_the_accumulator() {
2262        let mut rsp = pair([0x7FFF; 8], [0x7FFF; 8]);
2263        rsp.vu_compute(0x10, 0, 1, 0, 2);
2264        assert_eq!(rsp.vu_regs[2][0], 0x7FFF, "saturated");
2265        assert_eq!(
2266            (rsp.vu_acc[0] & 0xFFFF) as u16,
2267            0xFFFE,
2268            "the accumulator keeps 0x7FFF + 0x7FFF unclamped"
2269        );
2270    }
2271
2272    /// **`VADDC` produces a carry where `VADD` consumes one**, and writes no
2273    /// clamped result — it is the low half of a multi-precision add.
2274    #[test]
2275    fn vaddc_produces_the_carry() {
2276        let mut rsp = pair([0xFFFF; 8], [0x0001; 8]);
2277        assert!(rsp.vu_compute(0x14, 0, 1, 0, 2));
2278        assert_eq!(rsp.vu_regs[2][0], 0, "the wrapped low half, not clamped");
2279        assert_eq!(rsp.vu_ctrl.vco, 0xFF, "a carry out of every lane");
2280    }
2281
2282    /// `VSUBC` sets `VCO`'s **high** half from "the result was non-zero", which
2283    /// is what makes the pair usable as a comparison.
2284    #[test]
2285    fn vsubc_records_borrow_and_non_zero_separately() {
2286        let mut rsp = pair([5, 5, 0, 0, 0, 0, 0, 0], [3, 7, 0, 0, 0, 0, 0, 0]);
2287        assert!(rsp.vu_compute(0x15, 0, 1, 0, 2));
2288        assert_eq!(rsp.vu_regs[2][0], 2, "5 - 3");
2289        assert_eq!(rsp.vu_regs[2][1], 0xFFFE, "5 - 7 wraps");
2290        assert_eq!(rsp.vu_ctrl.vco & 1, 0, "no borrow out of lane 0");
2291        assert_ne!(rsp.vu_ctrl.vco & 2, 0, "lane 1 borrowed");
2292        assert_ne!(rsp.vu_ctrl.vco & (1 << 8), 0, "lane 0 was non-zero");
2293        assert_ne!(rsp.vu_ctrl.vco & (1 << 9), 0, "lane 1 was non-zero");
2294        assert_eq!(rsp.vu_ctrl.vco & (1 << 10), 0, "lane 2 was zero");
2295    }
2296
2297    /// **`VABS` applies the sign of `vs` to `vt`** — it is not the absolute
2298    /// value of either operand, which is what the name suggests.
2299    #[test]
2300    fn vabs_applies_the_sign_of_vs_to_vt() {
2301        let mut rsp = pair(
2302            [0xFFFF, 0x0001, 0x0000, 0, 0, 0, 0, 0], // vs: negative, positive, zero
2303            [0x0005, 0x0005, 0x0005, 0, 0, 0, 0, 0], // vt: 5 throughout
2304        );
2305        assert!(rsp.vu_compute(0x13, 0, 1, 0, 2));
2306        assert_eq!(rsp.vu_regs[2][0], 0xFFFB, "negative vs negates vt");
2307        assert_eq!(rsp.vu_regs[2][1], 5, "positive vs passes it through");
2308        assert_eq!(rsp.vu_regs[2][2], 0, "zero vs yields zero, not 5");
2309    }
2310
2311    /// The one input whose negation is not representable: `vd` saturates while
2312    /// the **accumulator keeps the unsaturated value**, so the two disagree.
2313    #[test]
2314    fn vabs_of_the_most_negative_value_disagrees_with_its_accumulator() {
2315        let mut rsp = pair([0xFFFF; 8], [0x8000; 8]);
2316        rsp.vu_compute(0x13, 0, 1, 0, 2);
2317        assert_eq!(rsp.vu_regs[2][0], 0x7FFF, "the result saturates");
2318        assert_eq!(
2319            (rsp.vu_acc[0] & 0xFFFF) as u16,
2320            0x8000,
2321            "the accumulator does not"
2322        );
2323    }
2324}
2325
2326#[cfg(test)]
2327mod compare_tests {
2328    use super::*;
2329
2330    fn pair(a: [u16; 8], b: [u16; 8]) -> Rsp {
2331        let mut rsp = Rsp::new();
2332        rsp.vu_regs[1] = a; // vs
2333        rsp.vu_regs[0] = b; // vt
2334        rsp
2335    }
2336
2337    /// **A compare writes a *selection*, not a boolean.** `vd` receives `vs` or
2338    /// `vt` per lane; the predicate goes to `VCC`. An implementation that wrote
2339    /// 0/1 into `vd` would satisfy any test that only inspected `VCC`.
2340    #[test]
2341    fn a_compare_selects_an_operand_and_records_the_predicate() {
2342        let mut rsp = pair([1, 9, 5, 0, 0, 0, 0, 0], [5, 5, 5, 0, 0, 0, 0, 0]);
2343        assert!(rsp.vu_compute(0x20, 0, 1, 0, 2)); // VLT
2344
2345        assert_eq!(rsp.vu_regs[2][0], 1, "1 < 5, so vs is selected");
2346        assert_eq!(rsp.vu_regs[2][1], 5, "9 !< 5, so vt is");
2347        assert_eq!(rsp.vu_ctrl.vcc & 0b011, 0b001, "only lane 0 compared true");
2348        assert_eq!(rsp.vu_ctrl.vcc >> 8, 0, "VCC's high half is cleared");
2349    }
2350
2351    /// **`VLT`'s equality case consults `VCO`**, which is what chains it onto a
2352    /// preceding `VSUBC`. Dropping the `VCO` terms leaves a compare that looks
2353    /// right on unequal operands and wrong on equal ones — the exact case a
2354    /// casual test omits.
2355    #[test]
2356    fn vlt_on_equal_operands_depends_on_vco() {
2357        // Equal operands, with both VCO halves set for lane 0 only.
2358        let mut rsp = pair([5; 8], [5; 8]);
2359        rsp.vu_ctrl.vco = (1 << 0) | (1 << 8);
2360        rsp.vu_compute(0x20, 0, 1, 0, 2);
2361        assert_ne!(rsp.vu_ctrl.vcc & 1, 0, "equal + carry + ne compares true");
2362        assert_eq!(rsp.vu_ctrl.vcc & 2, 0, "lane 1 has neither, so false");
2363
2364        // Same operands, no VCO: now false everywhere.
2365        let mut rsp = pair([5; 8], [5; 8]);
2366        rsp.vu_compute(0x20, 0, 1, 0, 2);
2367        assert_eq!(rsp.vu_ctrl.vcc, 0, "without VCO, equal is not less-than");
2368    }
2369
2370    /// `VGE`'s equality case uses the **opposite** VCO condition to `VLT`'s, so
2371    /// the two are not complements of each other on equal operands.
2372    #[test]
2373    fn vge_and_vlt_are_not_complements_on_equal_operands() {
2374        let mut lt = pair([5; 8], [5; 8]);
2375        lt.vu_ctrl.vco = (1 << 0) | (1 << 8);
2376        lt.vu_compute(0x20, 0, 1, 0, 2);
2377
2378        let mut ge = pair([5; 8], [5; 8]);
2379        ge.vu_ctrl.vco = (1 << 0) | (1 << 8);
2380        ge.vu_compute(0x23, 0, 1, 0, 2);
2381
2382        assert_ne!(lt.vu_ctrl.vcc & 1, 0, "VLT true for lane 0");
2383        assert_eq!(
2384            ge.vu_ctrl.vcc & 1,
2385            0,
2386            "and VGE ALSO false -- not a negation"
2387        );
2388    }
2389
2390    /// `VEQ` and `VNE` consult `VCO`'s **high** half (the non-zero flag) rather
2391    /// than its carry.
2392    #[test]
2393    fn veq_and_vne_consult_the_non_zero_flag() {
2394        let mut rsp = pair([5; 8], [5; 8]);
2395        rsp.vu_ctrl.vco = 1 << 8; // lane 0's "non-zero" flag
2396        rsp.vu_compute(0x21, 0, 1, 0, 2); // VEQ
2397        assert_eq!(rsp.vu_ctrl.vcc & 1, 0, "equal, but the ne flag suppresses");
2398        assert_ne!(rsp.vu_ctrl.vcc & 2, 0, "lane 1 has no flag, so equal holds");
2399
2400        let mut rsp = pair([5; 8], [5; 8]);
2401        rsp.vu_ctrl.vco = 1 << 8;
2402        rsp.vu_compute(0x22, 0, 1, 0, 2); // VNE
2403        assert_ne!(rsp.vu_ctrl.vcc & 1, 0, "the flag forces not-equal");
2404        assert_eq!(rsp.vu_ctrl.vcc & 2, 0, "lane 1 is genuinely equal");
2405    }
2406
2407    /// **`VMRG` consumes `VCC` without changing it** — it is the consumer of a
2408    /// compare, not another producer. It still clears `VCO`.
2409    #[test]
2410    fn vmrg_selects_on_vcc_and_leaves_it_alone() {
2411        let mut rsp = pair([0xAAAA; 8], [0xBBBB; 8]);
2412        rsp.vu_ctrl.vcc = 0b1010_0101;
2413        rsp.vu_ctrl.vco = 0xFFFF;
2414        assert!(rsp.vu_compute(0x27, 0, 1, 0, 2));
2415
2416        assert_eq!(rsp.vu_regs[2][0], 0xAAAA, "VCC bit set selects vs");
2417        assert_eq!(rsp.vu_regs[2][1], 0xBBBB, "clear selects vt");
2418        assert_eq!(rsp.vu_ctrl.vcc, 0b1010_0101, "VCC survives untouched");
2419        assert_eq!(rsp.vu_ctrl.vco, 0, "but VCO is cleared");
2420    }
2421
2422    /// Every compare clears `VCO` wholesale, so a second compare does not
2423    /// inherit the first's flags.
2424    #[test]
2425    fn a_compare_clears_vco() {
2426        let mut rsp = pair([5; 8], [5; 8]);
2427        rsp.vu_ctrl.vco = 0xFFFF;
2428        rsp.vu_compute(0x21, 0, 1, 0, 2);
2429        assert_eq!(rsp.vu_ctrl.vco, 0);
2430    }
2431}
2432
2433/// `VCH` and its `VCL`/`VCR` siblings write `VCC`/`VCO`/`VCE` together per lane.
2434/// The expected flags below are computed by hand from **n64-systemtest's**
2435/// reference algorithm (`op_vector_arithmetic.rs`), NOT from this code, so they
2436/// stay valid under a mutation of the implementation.
2437#[cfg(test)]
2438mod clip_tests {
2439    use super::*;
2440
2441    fn pair(vs: [u16; 8], vt: [u16; 8]) -> Rsp {
2442        let mut rsp = Rsp::new();
2443        rsp.vu_regs[1] = vs; // vs
2444        rsp.vu_regs[0] = vt; // vt
2445        rsp
2446    }
2447
2448    /// **`VCH` exercises both branches, the `s == ~t` term, and `VCE`.** With
2449    /// the suite's operand mapping (source1 -> vt, source2 -> vs) the reference
2450    /// is `i1 = vt`, `i2 = vs`:
2451    ///
2452    /// - Lane 0 (`vs=5`, `vt=3`, same sign -> *else*): `diff = vs - vt = 2`, so
2453    ///   `VCC.hi = (diff>=0) = 1`, `VCO.hi = (diff!=0) = 1`, everything else 0,
2454    ///   output `= vt = 3`.
2455    /// - Lane 1 (`vs=0xFFFB=-5`, `vt=4`, opposite sign -> *if*): `sum = -1`, so
2456    ///   `VCC.lo = (sum<=0) = 1`, `VCO.lo = 1`, `VCE = (sum==-1) = 1`, and
2457    ///   critically `VCO.hi = (sum!=0 && vs!=~vt) = (1 && 0) = 0` because
2458    ///   `0xFFFB == ~4`; output `= -vt = 0xFFFC`.
2459    ///
2460    /// Dropping the `s != !t` term flips lane 1's `VCO.hi` on (VCO -> 0x0302);
2461    /// collapsing the else-branch `VCO.hi` clears lane 0's (VCO -> 0x0002).
2462    #[test]
2463    fn vch_writes_the_three_flag_words_per_lane() {
2464        let mut rsp = pair([5, 0xFFFB, 0, 0, 0, 0, 0, 0], [3, 0x0004, 0, 0, 0, 0, 0, 0]);
2465        assert!(rsp.vu_compute(0x25, 0, 1, 0, 2));
2466
2467        assert_eq!(
2468            rsp.vu_regs[2],
2469            [3, 0xFFFC, 0, 0, 0, 0, 0, 0],
2470            "selected output"
2471        );
2472        assert_eq!(rsp.vu_ctrl.vcc, 0xFD02, "VCC: hi=lanes!=1, lo=lane1");
2473        assert_eq!(rsp.vu_ctrl.vco, 0x0102, "VCO: hi=lane0, lo=lane1");
2474        assert_eq!(rsp.vu_ctrl.vce, 0x02, "VCE: lane1 only (sum == -1)");
2475    }
2476
2477    /// **`VCL` and `VCR` clear `VCO` and `VCE` after they run; `VCH` does not.**
2478    /// The whole point of the pair is to *consume* the flags a preceding compare
2479    /// left, so a stale `VCO` would corrupt the next multi-precision step.
2480    #[test]
2481    fn vcl_and_vcr_clear_vco_and_vce_but_vch_does_not() {
2482        for op in [0x24u32, 0x26] {
2483            let mut rsp = pair([1; 8], [2; 8]);
2484            rsp.vu_ctrl.vco = 0xFFFF;
2485            rsp.vu_ctrl.vce = 0xFF;
2486            assert!(rsp.vu_compute(op, 0, 1, 0, 2));
2487            assert_eq!(rsp.vu_ctrl.vco, 0, "op {op:#x} clears VCO");
2488            assert_eq!(rsp.vu_ctrl.vce, 0, "op {op:#x} clears VCE");
2489        }
2490
2491        // VCH keeps them: it is the producer, not the consumer.
2492        let mut rsp = pair([1; 8], [2; 8]);
2493        rsp.vu_compute(0x25, 0, 1, 0, 2);
2494        assert_ne!(rsp.vu_ctrl.vco, 0, "VCH leaves VCO set");
2495    }
2496}
2497
2498/// The reserved "VZERO" opcode family. n64-systemtest pins all of them to one
2499/// `run_vzero` reference: `ACC_LO = vs + vt`, `vd = 0`, flags untouched.
2500#[cfg(test)]
2501mod vzero_tests {
2502    use super::*;
2503
2504    /// **Every reserved op writes the sum to `ACC_LO` and zeroes `vd`.** The
2505    /// `vd` sentinel (`0xDEAD`) is what makes a *decoded* no-op visible: an
2506    /// instruction that fell through the decode would leave it in place. The
2507    /// expected `ACC_LO` is `vs + vt` computed from the seeds, not the code, and
2508    /// the flag words are checked to prove the family touches none of them.
2509    #[test]
2510    fn reserved_ops_sum_into_acc_low_and_zero_vd() {
2511        let vs = [10u16, 20, 0xFFFF, 0x8000, 1, 2, 3, 0x7FFF];
2512        let vt = [5u16, 0x7FFF, 2, 0x8000, 0, 0, 0, 1];
2513        // One representative from each disjoint funct band.
2514        for op in [0x12u32, 0x16, 0x1C, 0x1E, 0x2E, 0x38, 0x3E] {
2515            let mut rsp = Rsp::new();
2516            rsp.vu_regs[1] = vs;
2517            rsp.vu_regs[0] = vt;
2518            rsp.vu_regs[2] = [0xDEAD; 8];
2519            rsp.vu_ctrl.vco = 0xABCD;
2520            rsp.vu_ctrl.vcc = 0x1234;
2521            rsp.vu_ctrl.vce = 0x56;
2522            assert!(rsp.vu_compute(op, 0, 1, 0, 2), "op {op:#x} is decoded");
2523            for lane in 0..8 {
2524                assert_eq!(rsp.vu_regs[2][lane], 0, "op {op:#x} zeroes vd lane {lane}");
2525                assert_eq!(
2526                    (rsp.vu_acc[lane] & 0xFFFF) as u16,
2527                    vs[lane].wrapping_add(vt[lane]),
2528                    "op {op:#x} ACC_LO lane {lane} = vs + vt"
2529                );
2530            }
2531            assert_eq!(rsp.vu_ctrl.vco, 0xABCD, "op {op:#x} leaves VCO");
2532            assert_eq!(rsp.vu_ctrl.vcc, 0x1234, "op {op:#x} leaves VCC");
2533            assert_eq!(rsp.vu_ctrl.vce, 0x56, "op {op:#x} leaves VCE");
2534        }
2535    }
2536}
2537
2538#[cfg(test)]
2539mod vrnd_tests {
2540    use super::*;
2541
2542    /// The suite primes the accumulator with `VMUDH` then `VMADL`, using these
2543    /// two vectors, before running `VRND`.
2544    const V0: [u16; 8] = [
2545        0x0000, 0x0001, 0x0001, 0x7FFF, 0xFFFF, 0x7FFF, 0x3FFF, 0x8000,
2546    ];
2547    const V1: [u16; 8] = [
2548        0x0000, 0x0001, 0xFFFF, 0xFFFF, 0xFFFF, 0x7FFF, 0x7FFF, 0x7FFF,
2549    ];
2550    /// The `vt` input to VRND (`0x20`); its odd lanes are ignored by the suite.
2551    const VT: [u16; 8] = [
2552        0x0000, 0x0001, 0x0002, 0x7FFF, 0xFFFF, 0x8000, 0x8001, 0x8002,
2553    ];
2554
2555    /// Prime the accumulator the way the oracle does: `V0=$v0`, `V1=$v1`,
2556    /// `vmudh $v2,$v0,$v1` then `vmadl $v2,$v0,$v1`.
2557    fn primed() -> Rsp {
2558        let mut rsp = Rsp::new();
2559        rsp.vu_regs[0] = V0;
2560        rsp.vu_regs[1] = V1;
2561        rsp.vu_compute(0x07, 0, 0, 1, 2); // VMUDH: vs=$v0, vt=$v1
2562        rsp.vu_compute(0x0C, 0, 0, 1, 2); // VMADL
2563        rsp
2564    }
2565
2566    fn acc_slice(rsp: &Rsp, shift: u32) -> [u16; 8] {
2567        core::array::from_fn(|i| (rsp.vu_acc[i] >> shift) as u16)
2568    }
2569
2570    /// **`VRNDN` against n64-systemtest's vectors**, with an **even** `vs` field
2571    /// (no shift). Result and all three accumulator slices, because VRND writes
2572    /// the whole 48-bit accumulator and the result alone hides the low bits.
2573    #[test]
2574    fn vrndn_matches_the_oracle_with_an_even_vs() {
2575        let mut rsp = primed();
2576        // vt is $v0 in the oracle's i==0 case; vs field is register 0 (even).
2577        rsp.vu_regs[0] = VT;
2578        assert!(rsp.vu_compute(0x0A, 0, 0, 0, 2));
2579
2580        assert_eq!(
2581            rsp.vu_regs[2],
2582            [0, 1, 0xFFFF, 0x8001, 1, 0x7FFF, 0x7FFF, 0x8000],
2583            "VRNDN result"
2584        );
2585        assert_eq!(
2586            acc_slice(&rsp, 32),
2587            [0, 0, 0xFFFF, 0xFFFF, 0, 0x3FFF, 0x1FFF, 0xC000],
2588            "ACC_HI"
2589        );
2590        assert_eq!(
2591            acc_slice(&rsp, 16),
2592            [0, 1, 0xFFFF, 0x8001, 1, 1, 0x4001, 0x7FFF],
2593            "ACC_MD"
2594        );
2595        assert_eq!(
2596            acc_slice(&rsp, 0),
2597            [0, 0, 2, 0xFFFD, 0xFFFE, 0x3FFF, 0x1FFF, 0xC001],
2598            "ACC_LO"
2599        );
2600    }
2601
2602    /// **The low bit of the `vs` field shifts the product left 16.** An even
2603    /// and an odd `vs` over the same operand must differ, which is the only
2604    /// thing that distinguishes the field-as-immediate reading from treating
2605    /// `vs` as a register.
2606    #[test]
2607    fn the_vs_field_low_bit_selects_the_shift() {
2608        let mut even = primed();
2609        even.vu_regs[0] = VT;
2610        even.vu_compute(0x0A, 0, 0, 0, 2); // vs field 0
2611
2612        let mut odd = primed();
2613        odd.vu_regs[0] = VT;
2614        odd.vu_compute(0x0A, 0, 1, 0, 2); // vs field 1 -> shift
2615
2616        assert_ne!(
2617            even.vu_regs[2], odd.vu_regs[2],
2618            "an odd vs field shifts the product and must change the result"
2619        );
2620    }
2621
2622    /// **`VRNDP` against n64-systemtest's vectors.** It shares everything with
2623    /// `VRNDN` except the sign condition — `VRNDP` adds on a non-negative
2624    /// accumulator — so its `ACC_HI` slice is identical to `VRNDN`'s while `ACC_MD`,
2625    /// `ACC_LO` and the result diverge wherever a lane's accumulator sign differs.
2626    /// Asserting the full published vector pins that divergence exactly, without
2627    /// having to reason about which lanes are negative.
2628    #[test]
2629    fn vrndp_matches_the_oracle_with_an_even_vs() {
2630        let mut rsp = primed();
2631        rsp.vu_regs[0] = VT;
2632        assert!(rsp.vu_compute(0x02, 0, 0, 0, 2));
2633
2634        assert_eq!(
2635            rsp.vu_regs[2],
2636            [0, 1, 0xFFFF, 0x8001, 1, 0x7FFF, 0x7FFF, 0x8000],
2637            "VRNDP result"
2638        );
2639        assert_eq!(
2640            acc_slice(&rsp, 32),
2641            [0, 0, 0xFFFF, 0xFFFF, 0, 0x3FFF, 0x1FFF, 0xC000],
2642            "ACC_HI -- identical to VRNDN's"
2643        );
2644        assert_eq!(
2645            acc_slice(&rsp, 16),
2646            [0, 1, 0xFFFF, 0x8001, 1, 0, 0x4000, 0x8000],
2647            "ACC_MD"
2648        );
2649        assert_eq!(
2650            acc_slice(&rsp, 0),
2651            [0, 1, 0, 0x7FFE, 0xFFFD, 0xBFFF, 0xA000, 0x3FFF],
2652            "ACC_LO"
2653        );
2654    }
2655}
2656
2657#[cfg(test)]
2658mod vmulq_tests {
2659    use super::*;
2660
2661    const VS: [u16; 8] = [
2662        0x0000, 0x0001, 0x7FFF, 0x7FFF, 0x8000, 0x8000, 0xFFFE, 0xFFFF,
2663    ];
2664    const VT: [u16; 8] = [
2665        0x0000, 0x0001, 0x7FFF, 0xFFFF, 0x7FFF, 0x7FFF, 0x0001, 0x0001,
2666    ];
2667
2668    fn seeded() -> Rsp {
2669        let mut rsp = Rsp::new();
2670        rsp.vu_regs[0] = VS; // used as vs
2671        rsp.vu_regs[1] = VT;
2672        rsp
2673    }
2674
2675    fn acc_slice(rsp: &Rsp, shift: u32) -> [u16; 8] {
2676        core::array::from_fn(|i| (rsp.vu_acc[i] >> shift) as u16)
2677    }
2678
2679    /// **`VMULQ` against n64-systemtest's vectors.** The result's low 4 bits are
2680    /// masked off and `ACC_LO` is always zero — two properties a naive multiply
2681    /// gets wrong and the accumulator slices expose.
2682    #[test]
2683    fn vmulq_matches_the_oracle_vectors() {
2684        let mut rsp = seeded();
2685        assert!(rsp.vu_compute(0x03, 0, 0, 1, 2));
2686        assert_eq!(
2687            rsp.vu_regs[2],
2688            [0, 0, 0x7FF0, 0xC010, 0x8000, 0x8000, 0, 0],
2689            "VMULQ result -- note the low nibble is always clear"
2690        );
2691        assert_eq!(
2692            acc_slice(&rsp, 32),
2693            [0, 0, 0x3FFF, 0xFFFF, 0xC000, 0xC000, 0, 0],
2694            "ACC_HI"
2695        );
2696        assert_eq!(
2697            acc_slice(&rsp, 16),
2698            [0, 1, 1, 0x8020, 0x801F, 0x801F, 0x1D, 0x1E],
2699            "ACC_MD"
2700        );
2701        assert_eq!(acc_slice(&rsp, 0), [0; 8], "ACC_LO is zeroed");
2702    }
2703
2704    /// **The result always has its low 4 bits clear**, independent of the
2705    /// operands — the `& ~15` is not incidental.
2706    #[test]
2707    fn vmulq_masks_the_low_nibble() {
2708        let mut rsp = Rsp::new();
2709        rsp.vu_regs[0] = [0x00FF; 8];
2710        rsp.vu_regs[1] = [0x00FF; 8];
2711        rsp.vu_compute(0x03, 0, 0, 1, 2);
2712        for lane in 0..8 {
2713            assert_eq!(rsp.vu_regs[2][lane] & 15, 0, "lane {lane} low nibble");
2714        }
2715    }
2716}
2717
2718#[cfg(test)]
2719mod packed_tests {
2720    use super::*;
2721
2722    fn with_dmem(pattern: &[u8]) -> Rsp {
2723        let mut rsp = Rsp::new();
2724        for (i, b) in pattern.iter().enumerate() {
2725            rsp.dmem[i] = *b;
2726        }
2727        rsp
2728    }
2729
2730    /// **`LPV` loads one byte per lane into the lane's high byte.**
2731    ///
2732    /// Each of the eight lanes takes a consecutive DMEM byte and places it at
2733    /// bit 15, so the low byte of every lane is zero — a property a 16-bit load
2734    /// would not have.
2735    #[test]
2736    fn lpv_loads_one_byte_per_lane_high() {
2737        let bytes: [u8; 8] = core::array::from_fn(|i| i as u8 + 0x10);
2738        let mut rsp = with_dmem(&bytes);
2739        assert!(rsp.vector_mem(false, 0x06, 0, 1, 0, 0));
2740        for (lane, &b) in bytes.iter().enumerate() {
2741            assert_eq!(
2742                rsp.vu_regs[1][lane],
2743                u16::from(b) << 8,
2744                "lane {lane} in the high byte"
2745            );
2746        }
2747    }
2748
2749    /// **`LUV` is `LPV` shifted one bit lower** — bit 14 rather than 15.
2750    #[test]
2751    fn luv_shifts_one_bit_below_lpv() {
2752        let bytes: [u8; 8] = core::array::from_fn(|i| i as u8 + 0x10);
2753        let mut rsp = with_dmem(&bytes);
2754        assert!(rsp.vector_mem(false, 0x07, 0, 1, 0, 0));
2755        for (lane, &b) in bytes.iter().enumerate() {
2756            assert_eq!(rsp.vu_regs[1][lane], u16::from(b) << 7);
2757        }
2758    }
2759
2760    /// The element field rotates which DMEM byte each lane reads, wrapping
2761    /// within the 16-byte window — the packed loads' equivalent of an offset.
2762    #[test]
2763    fn lpv_element_rotates_the_source_bytes() {
2764        let bytes: [u8; 16] = core::array::from_fn(|i| i as u8 + 0x10);
2765        let mut base = with_dmem(&bytes);
2766        base.vector_mem(false, 0x06, 0, 1, 0, 0);
2767        let mut rot = with_dmem(&bytes);
2768        rot.vector_mem(false, 0x06, 0, 1, 1, 0); // element 1
2769        assert_ne!(
2770            base.vu_regs[1], rot.vu_regs[1],
2771            "a non-zero element must rotate the mapping"
2772        );
2773    }
2774
2775    /// **The `SPV`/`SUV` branch split only appears when the element field
2776    /// pushes an offset past 8**, and `SPV`/`SUV` take opposite branches.
2777    ///
2778    /// With `e = 4` the eight offsets are `4..=11`: the first four (`< 8`) take
2779    /// one branch and the last four the other. This test computes the reference
2780    /// exactly as the hardware model does, per byte, rather than asserting a
2781    /// summary — my earlier attempts at a summary were wrong three times because
2782    /// the split is genuinely position-dependent.
2783    #[test]
2784    fn spv_and_suv_take_opposite_branches() {
2785        let regs: [u16; 8] =
2786            core::array::from_fn(|lane| ((0x10 + lane as u16) << 8) | (0x80 + lane as u16));
2787        // The reference: for offset in e..e+8, byte comes from the high byte
2788        // when (offset & 15 < 8) for SPV, or the opposite for SUV.
2789        let expect = |suv: bool, e: usize| -> [u8; 8] {
2790            core::array::from_fn(|i| {
2791                let offset = e + i;
2792                let lane = offset & 7;
2793                let high = (offset & 15 < 8) != suv;
2794                if high {
2795                    (regs[lane] >> 8) as u8
2796                } else {
2797                    (regs[lane] >> 7) as u8
2798                }
2799            })
2800        };
2801
2802        for (op, suv) in [(0x06u32, false), (0x07u32, true)] {
2803            let mut rsp = Rsp::new();
2804            rsp.vu_regs[1] = regs;
2805            rsp.set_su(2, 0x100);
2806            assert!(rsp.vector_mem(true, op, 2, 1, 4, 0)); // e = 4
2807            let got: [u8; 8] = core::array::from_fn(|i| rsp.dmem[0x100 + i]);
2808            assert_eq!(
2809                got,
2810                expect(suv, 4),
2811                "{} with e=4",
2812                if suv { "SUV" } else { "SPV" }
2813            );
2814        }
2815    }
2816}
2817
2818#[cfg(test)]
2819mod strided_tests {
2820    use super::*;
2821
2822    /// **`LHV` reads every *other* DMEM byte, one per lane**, placing it at
2823    /// bit 14. With `rs = 0`, `e = 0` the eight lanes take bytes 0, 2, 4 … 14.
2824    /// Expected values were computed independently before being pinned here.
2825    #[test]
2826    fn lhv_reads_every_other_byte() {
2827        let bytes: [u8; 16] = core::array::from_fn(|i| i as u8 + 0x10);
2828        let mut rsp = Rsp::new();
2829        for (i, b) in bytes.iter().enumerate() {
2830            rsp.dmem[i] = *b;
2831        }
2832        assert!(rsp.vector_mem(false, 0x08, 0, 1, 0, 0));
2833        assert_eq!(
2834            rsp.vu_regs[1],
2835            [
2836                0x0800, 0x0900, 0x0A00, 0x0B00, 0x0C00, 0x0D00, 0x0E00, 0x0F00
2837            ],
2838            "each lane holds byte (2*lane) at bit 14"
2839        );
2840    }
2841
2842    /// **`SHV` combines each adjacent byte pair of the register into one strided
2843    /// DMEM byte** (`hi << 1 | lo >> 7`). The eight results land at DMEM 0, 2,
2844    /// 4 … 14. Independently computed.
2845    #[test]
2846    fn shv_combines_adjacent_bytes() {
2847        let mut rsp = Rsp::new();
2848        rsp.vu_regs[1] = core::array::from_fn(|l| ((0x10 + l as u16) << 8) | (0x80 + l as u16));
2849        rsp.set_su(2, 0);
2850        assert!(rsp.vector_mem(true, 0x08, 2, 1, 0, 0));
2851
2852        let expected: [(usize, u8); 8] = [
2853            (0x0, 0x21),
2854            (0x2, 0x23),
2855            (0x4, 0x25),
2856            (0x6, 0x27),
2857            (0x8, 0x29),
2858            (0xA, 0x2B),
2859            (0xC, 0x2D),
2860            (0xE, 0x2F),
2861        ];
2862        for (at, want) in expected {
2863            assert_eq!(rsp.dmem[at], want, "DMEM byte {at:#x}");
2864        }
2865        // The odd bytes between them are untouched.
2866        assert_eq!(rsp.dmem[0x1], 0, "byte 1 is not written");
2867    }
2868}
2869
2870#[cfg(test)]
2871mod transpose_tests {
2872    use super::*;
2873
2874    /// **`LTV` loads a transposed *diagonal*** — each of the eight registers in
2875    /// the group gets one lane filled, at a rotating position. With `rs = 0`,
2876    /// `e = 0` and DMEM `0x10..0x1F`, register `v_r` receives its `r`-th lane.
2877    /// The expected diagonal was computed in a scratch script first.
2878    #[test]
2879    fn ltv_loads_a_transposed_diagonal() {
2880        let mut rsp = Rsp::new();
2881        for i in 0..16 {
2882            rsp.dmem[i] = 0x10 + i as u8;
2883        }
2884        assert!(rsp.vector_mem(false, 0x0B, 0, 0, 0, 0)); // vt=0 -> group 0..7
2885        let diag: [u16; 8] = [
2886            0x1011, 0x1213, 0x1415, 0x1617, 0x1819, 0x1A1B, 0x1C1D, 0x1E1F,
2887        ];
2888        for (r, &want) in diag.iter().enumerate() {
2889            assert_eq!(rsp.vu_regs[r][r], want, "register {r}, diagonal lane");
2890            // Every other lane in that register stays zero.
2891            for l in 0..8 {
2892                if l != r {
2893                    assert_eq!(rsp.vu_regs[r][l], 0, "reg {r} lane {l} untouched");
2894                }
2895            }
2896        }
2897    }
2898
2899    /// **`STV` stores the transposed diagonal back out.** Register `v_r`, byte
2900    /// `b` is seeded to `0x10*r + b` so every byte in the group is distinct;
2901    /// the expected DMEM pattern was computed independently.
2902    #[test]
2903    fn stv_stores_a_transposed_diagonal() {
2904        let mut rsp = Rsp::new();
2905        for r in 0..8usize {
2906            for b in 0..16usize {
2907                let byte = (0x10 * r + b) as u8;
2908                rsp.set_vu_byte(r, b, byte);
2909            }
2910        }
2911        rsp.set_su(2, 0);
2912        assert!(rsp.vector_mem(true, 0x0B, 2, 0, 0, 0)); // vt=0 -> group 0..7
2913        let expected: [u8; 16] = [
2914            0x00, 0x01, 0x12, 0x13, 0x24, 0x25, 0x36, 0x37, 0x48, 0x49, 0x5A, 0x5B, 0x6C, 0x6D,
2915            0x7E, 0x7F,
2916        ];
2917        for (i, want) in expected.iter().enumerate() {
2918            assert_eq!(rsp.dmem[i], *want, "DMEM byte {i}");
2919        }
2920    }
2921}
2922
2923/// Store-side register wrap. Loads shorten on a non-zero element; **stores wrap
2924/// the register index and move the full width** (`op_vector_stores.rs:17`). The
2925/// expected DMEM below is computed by hand from that rule, not from the code, so
2926/// it fails if the store silently adopts the load's shortening behavior.
2927#[cfg(test)]
2928mod store_wrap_tests {
2929    use super::*;
2930
2931    fn seeded() -> Rsp {
2932        let mut rsp = Rsp::new();
2933        // Register 1, byte b = 0xA0 + b: every byte distinct so a wrap shows.
2934        for b in 0..16usize {
2935            rsp.set_vu_byte(1, b, 0xA0 + b as u8);
2936        }
2937        rsp
2938    }
2939
2940    /// **`SQV [e=1]` on an aligned address writes 16 bytes, the last wrapped.**
2941    /// Bytes 1..15 land at DMEM 0..14; byte 0 wraps to DMEM 15. The load rule
2942    /// (`16 - e = 15` bytes) would leave DMEM 15 untouched.
2943    #[test]
2944    fn sqv_wraps_the_register_tail() {
2945        let mut rsp = seeded();
2946        rsp.set_su(0, 0);
2947        assert!(rsp.vector_mem(true, 0x04, 0, 1, 1, 0));
2948        let expected: [u8; 16] = [
2949            0xA1, 0xA2, 0xA3, 0xA4, 0xA5, 0xA6, 0xA7, 0xA8, 0xA9, 0xAA, 0xAB, 0xAC, 0xAD, 0xAE,
2950            0xAF, 0xA0,
2951        ];
2952        for (i, want) in expected.iter().enumerate() {
2953            assert_eq!(rsp.dmem[i], *want, "DMEM byte {i}");
2954        }
2955    }
2956
2957    /// **`SWV` rotates all 16 bytes within the window anchored to the 8-byte
2958    /// base.** With `ea = 3`, base is `0` and `misalignment` is `3`, so byte `i`
2959    /// lands at DMEM `(3 + i) & 15`; the last three wrap to DMEM 0..2.
2960    #[test]
2961    fn swv_rotates_within_the_eight_byte_window() {
2962        let mut rsp = seeded();
2963        rsp.set_su(2, 3);
2964        assert!(rsp.vector_mem(true, 0x0A, 2, 1, 0, 0));
2965        let expected: [u8; 16] = [
2966            0xAD, 0xAE, 0xAF, 0xA0, 0xA1, 0xA2, 0xA3, 0xA4, 0xA5, 0xA6, 0xA7, 0xA8, 0xA9, 0xAA,
2967            0xAB, 0xAC,
2968        ];
2969        for (i, want) in expected.iter().enumerate() {
2970            assert_eq!(rsp.dmem[i], *want, "DMEM byte {i}");
2971        }
2972    }
2973}
2974
2975#[cfg(test)]
2976mod lfv_tests {
2977    use super::*;
2978
2979    fn with_dmem() -> Rsp {
2980        let mut rsp = Rsp::new();
2981        for i in 0..16 {
2982            rsp.dmem[i] = 0x10 + i as u8;
2983        }
2984        rsp
2985    }
2986
2987    /// **`LFV` at `e = 0` writes only the first four lanes** — its length is
2988    /// `min(8, 16 - e)` *bytes*, so a zero element fills bytes 0..8 = lanes 0..3
2989    /// and leaves the upper half untouched. Computed from n64-systemtest's own
2990    /// reference.
2991    #[test]
2992    fn lfv_at_element_zero_fills_the_low_half() {
2993        let mut rsp = with_dmem();
2994        assert!(rsp.vector_mem(false, 0x09, 0, 1, 0, 0));
2995        assert_eq!(rsp.vu_regs[1], [0x0800, 0x0A00, 0x0C00, 0x0E00, 0, 0, 0, 0]);
2996    }
2997
2998    /// **`LFV` at `e = 4` follows a different offset pattern** — this is the
2999    /// case where ares and the suite reference diverge, and the value here is
3000    /// the suite's. The partial write now touches bytes 4..12 = lanes 2..5,
3001    /// leaving lanes 0, 1, 6, 7 as they were.
3002    #[test]
3003    fn lfv_at_element_four_matches_the_suite_not_ares() {
3004        let mut rsp = with_dmem();
3005        assert!(rsp.vector_mem(false, 0x09, 0, 1, 4, 0));
3006        assert_eq!(rsp.vu_regs[1], [0, 0, 0x0A00, 0x0C00, 0x0A00, 0x0C00, 0, 0]);
3007    }
3008}
3009
3010#[cfg(test)]
3011mod sfv_tests {
3012    use super::*;
3013
3014    fn seeded() -> Rsp {
3015        let mut rsp = Rsp::new();
3016        // Lane l = (l+1) << 8, so lane>>7 = (l+1)<<1 -- distinct per lane.
3017        rsp.vu_regs[1] = core::array::from_fn(|l| (l as u16 + 1) << 8);
3018        rsp.set_su(2, 0);
3019        rsp
3020    }
3021
3022    /// **`SFV` writes four bytes, source lanes chosen by an `e`-table.** `e=0`
3023    /// takes lanes 0..3; `e=4` rotates to 1,2,3,0. Values computed from the
3024    /// suite reference.
3025    #[test]
3026    fn sfv_selects_source_lanes_by_element() {
3027        let mut rsp = seeded();
3028        assert!(rsp.vector_mem(true, 0x09, 2, 1, 0, 0)); // e=0
3029        assert_eq!(
3030            [rsp.dmem[0], rsp.dmem[4], rsp.dmem[8], rsp.dmem[12]],
3031            [2, 4, 6, 8]
3032        );
3033
3034        let mut rsp = seeded();
3035        assert!(rsp.vector_mem(true, 0x09, 2, 1, 4, 0)); // e=4 -> lanes 1,2,3,0
3036        assert_eq!(
3037            [rsp.dmem[0], rsp.dmem[4], rsp.dmem[8], rsp.dmem[12]],
3038            [4, 6, 8, 2]
3039        );
3040    }
3041
3042    /// **An `e` outside the table writes zero, not a wrapped lane.** This is the
3043    /// "even 0 for some E" case; a table-less implementation would store some
3044    /// plausible lane instead.
3045    #[test]
3046    fn sfv_writes_zero_for_an_undefined_element() {
3047        let mut rsp = seeded();
3048        // Seed the DMEM non-zero so a zero write is visible.
3049        for i in 0..16 {
3050            rsp.dmem[i] = 0xEE;
3051        }
3052        assert!(rsp.vector_mem(true, 0x09, 2, 1, 2, 0)); // e=2 not in table
3053        assert_eq!(
3054            [rsp.dmem[0], rsp.dmem[4], rsp.dmem[8], rsp.dmem[12]],
3055            [0, 0, 0, 0],
3056            "undefined e writes real zeros"
3057        );
3058        assert_eq!(rsp.dmem[1], 0xEE, "untouched bytes stay");
3059    }
3060}
3061
3062#[cfg(test)]
3063mod vmacq_tests {
3064    use super::*;
3065
3066    /// Seed one lane's accumulator from its three 16-bit slices.
3067    fn set_acc_slices(rsp: &mut Rsp, lane: usize, top: u16, mid: u16, low: u16) {
3068        rsp.vu_acc[lane] = (u64::from(top) << 32) | (u64::from(mid) << 16) | u64::from(low);
3069    }
3070
3071    /// **`VMACQ` against n64-systemtest's `simulate`.** Each case is
3072    /// `(top, mid, low) -> (vd, ACC_HI, ACC_MD, ACC_LO)`, computed by running
3073    /// the suite's reference in a scratch script.
3074    #[test]
3075    fn vmacq_matches_the_suite_simulate() {
3076        // (top, mid, low, vd, ACC_HI, ACC_MD, ACC_LO)
3077        let cases: [[u16; 7]; 5] = [
3078            [0, 0, 0, 0, 0, 0, 0],
3079            [0, 0x40, 0, 0x10, 0, 0x20, 0],
3080            [0, 0x10, 0, 0, 0, 0x10, 0],
3081            [0xFFFF, 0xFF00, 0, 0xFF90, 0xFFFF, 0xFF20, 0],
3082            [0, 0x80, 0x1234, 0x30, 0, 0x60, 0x1234],
3083        ];
3084        for [top, mid, low, vd, ah, am, al] in cases {
3085            let mut rsp = Rsp::new();
3086            set_acc_slices(&mut rsp, 0, top, mid, low);
3087            assert!(rsp.vu_compute(0x0B, 0, 0, 0, 2));
3088            assert_eq!(rsp.vu_regs[2][0], vd, "vd for ({top:#x},{mid:#x},{low:#x})");
3089            assert_eq!((rsp.vu_acc[0] >> 32) as u16, ah, "ACC_HI");
3090            assert_eq!((rsp.vu_acc[0] >> 16) as u16, am, "ACC_MD");
3091            assert_eq!(rsp.vu_acc[0] as u16, al, "ACC_LO preserved");
3092        }
3093    }
3094
3095    /// **`ACC_LO` is preserved** — the nudge is at bit 21, above it. A whole-
3096    /// accumulator rewrite that recomputed the low slice would corrupt it.
3097    #[test]
3098    fn vmacq_preserves_the_low_slice() {
3099        let mut rsp = Rsp::new();
3100        set_acc_slices(&mut rsp, 0, 0, 0x40, 0xBEEF);
3101        rsp.vu_compute(0x0B, 0, 0, 0, 2);
3102        assert_eq!(rsp.vu_acc[0] as u16, 0xBEEF, "the low slice is untouched");
3103    }
3104
3105    /// **The result's low nibble is always clear** (`& 0xFFF0`).
3106    #[test]
3107    fn vmacq_masks_the_low_nibble() {
3108        let mut rsp = Rsp::new();
3109        set_acc_slices(&mut rsp, 0, 0, 0x7F, 0xFFFF);
3110        rsp.vu_compute(0x0B, 0, 0, 0, 2);
3111        assert_eq!(rsp.vu_regs[2][0] & 0xF, 0);
3112    }
3113}