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}