From 661f32d72bdbee26a8301373e8015c91eafb6260 Mon Sep 17 00:00:00 2001 From: Shane Michael Mathews Date: Fri, 31 Jul 2026 19:22:22 -0400 Subject: [PATCH 1/2] fix(vu1): classify EFU lower-op VF reads in the pair hazard table vuLowerVfReadWriteMasks stopped at MFP and reported no read or write for the entire EFU opcode block (ESADD through EEXP), even for ELENG, ERLENG and ERCPR, which this interpreter already implements as reading VF[fs]. Add the thirteen EFU cases so the pair reorder predicate can see the hazard; WAITP and the two unallocated EFU slots stay excluded since they take no vector operand. The read is reported regardless of whether an opcode has an execution body today, so implementing one later cannot silently reopen the gap the table exists to close. --- ps2xRuntime/src/lib/vu/ps2_vu1_detail.h | 22 +++++ ps2xTest/src/ps2_vu1_tests.cpp | 111 ++++++++++++++++++++++++ 2 files changed, 133 insertions(+) diff --git a/ps2xRuntime/src/lib/vu/ps2_vu1_detail.h b/ps2xRuntime/src/lib/vu/ps2_vu1_detail.h index 40acd762e..19ad13662 100644 --- a/ps2xRuntime/src/lib/vu/ps2_vu1_detail.h +++ b/ps2xRuntime/src/lib/vu/ps2_vu1_detail.h @@ -120,6 +120,28 @@ static inline void vuLowerVfReadWriteMasks(uint32_t lower, uint32_t &readMask, u case 0x64: // MFP vuSetRegBit(writeMask, it); return; + // EFU block. Every EFU operation takes VF[is] as its only vector + // operand and writes the P register; see the VU User's Manual EFU + // instruction pages ("ESADD P, VF[fs]" through "EEXP P, VF[fs]fsf"). + // The read is reported whether or not the execution switch gives the + // opcode a body today, so implementing one later cannot silently + // reopen the hazard. WAITP (0x7B) has no vector operand and is + // deliberately absent, as are the unallocated slots 0x77 and 0x7F. + case 0x70: // ESADD + case 0x71: // ERSADD + case 0x72: // ELENG + case 0x73: // ERLENG + case 0x74: // EATANxy + case 0x75: // EATANxz + case 0x76: // ESUM + case 0x78: // ESQRT + case 0x79: // ERSQRT + case 0x7A: // ERCPR + case 0x7C: // ESIN + case 0x7D: // EATAN + case 0x7E: // EEXP + vuSetRegBit(readMask, is); + return; default: return; } diff --git a/ps2xTest/src/ps2_vu1_tests.cpp b/ps2xTest/src/ps2_vu1_tests.cpp index a66da893c..307896906 100644 --- a/ps2xTest/src/ps2_vu1_tests.cpp +++ b/ps2xTest/src/ps2_vu1_tests.cpp @@ -4,6 +4,7 @@ #include "runtime/ps2_gs_psmct32.h" #include "runtime/ps2_memory.h" #include "runtime/ps2_vu1.h" +#include "vu/ps2_vu1_detail.h" #include #include @@ -345,6 +346,116 @@ void register_ps2_vu1_tests() t.Equals(vu1.state().vf[1][3], 440.0f, "upper ADD should write w after lower read"); }); + tc.Run("EFU lower op reads the pre-pair VF value the upper half overwrites", [](TestCase &t) + { + Vu1Fixture fx; + t.IsTrue(fx.initialize(), "VU1 fixture should initialize"); + + writeVuInstructionPair(fx.code, + 0u, + makeVuLowerSpecial(0x72u, 3u, 0u, 0u, 0xEu), // ELENG P, VF[3] + makeVuUpper(0x28u, 0xFu, 2u, 1u, 3u)); // ADD.xyzw vf3, vf1, vf2 + + VU1Interpreter vu1; + vu1.state().vf[3][0] = 3.0f; + vu1.state().vf[3][1] = 4.0f; + vu1.state().vf[3][2] = 12.0f; + vu1.state().vf[3][3] = 1.0f; + vu1.state().vf[1][0] = 6.0f; + vu1.state().vf[1][1] = 8.0f; + vu1.state().vf[1][2] = 24.0f; + vu1.state().vf[1][3] = 2.0f; + vu1.state().vf[2][0] = 6.0f; + vu1.state().vf[2][1] = 8.0f; + vu1.state().vf[2][2] = 24.0f; + vu1.state().vf[2][3] = 2.0f; + + vu1.execute(fx.code, PS2_VU1_CODE_SIZE, fx.data, PS2_VU1_DATA_SIZE, fx.gs, &fx.mem, 0u, 0u, 0u, 1u); + + t.Equals(vu1.state().p, 13.0f, "ELENG should read the pre-pair length of vf3"); + t.Equals(vu1.state().vf[3][0], 12.0f, "upper ADD write should still land on x"); + t.Equals(vu1.state().vf[3][2], 48.0f, "upper ADD write should still land on z"); + }); + + tc.Run("EFU lower ops report their fs read and WAITP reports none", [](TestCase &t) + { + const uint8_t efuOps[] = {0x70u, 0x71u, 0x72u, 0x73u, 0x74u, 0x75u, 0x76u, + 0x78u, 0x79u, 0x7Au, 0x7Cu, 0x7Du, 0x7Eu}; + for (uint8_t op : efuOps) + { + uint32_t readMask = 0u; + uint32_t writeMask = 0u; + vuLowerVfReadWriteMasks(makeVuLowerSpecial(op, 3u, 5u, 0u, 0xEu), readMask, writeMask); + t.Equals(readMask, static_cast(1u << 3), "EFU op should report reading VF[fs]"); + t.Equals(writeMask, 0u, "EFU op should report no VF write"); + } + + { + uint32_t readMask = 0u; + uint32_t writeMask = 0u; + vuLowerVfReadWriteMasks(makeVuLowerSpecial(0x7Bu, 3u, 5u, 0u, 0xEu), readMask, writeMask); + t.Equals(readMask, 0u, "WAITP should report no VF read"); + t.Equals(writeMask, 0u, "WAITP should report no VF write"); + } + + { + uint32_t readMask = 0u; + uint32_t writeMask = 0u; + vuLowerVfReadWriteMasks(makeVuLowerSpecial(0x77u, 3u, 5u, 0u, 0xEu), readMask, writeMask); + t.Equals(readMask, 0u, "unallocated slot 0x77 should report no VF read"); + t.Equals(writeMask, 0u, "unallocated slot 0x77 should report no VF write"); + } + + { + uint32_t readMask = 0u; + uint32_t writeMask = 0u; + vuLowerVfReadWriteMasks(makeVuLowerSpecial(0x7Fu, 3u, 5u, 0u, 0xEu), readMask, writeMask); + t.Equals(readMask, 0u, "unallocated slot 0x7F should report no VF read"); + t.Equals(writeMask, 0u, "unallocated slot 0x7F should report no VF write"); + } + + { + uint32_t readMask = 0u; + uint32_t writeMask = 0u; + vuLowerVfReadWriteMasks(makeVuLowerSpecial(0x30u, 3u, 5u, 0u, 0xFu), readMask, writeMask); + t.Equals(readMask, static_cast(1u << 3), "MOVE positive control should report is read"); + t.Equals(writeMask, static_cast(1u << 5), "MOVE positive control should report it write"); + } + }); + + tc.Run("EFU pair with no VF write leaves the reordered branch behaviourally unchanged", [](TestCase &t) + { + Vu1Fixture fx; + t.IsTrue(fx.initialize(), "VU1 fixture should initialize"); + + writeVuInstructionPair(fx.code, + 0u, + makeVuLowerSpecial(0x73u, 3u, 0u, 0u, 0xEu), // ERLENG P, VF[3] + makeVuUpper(0x28u, 0xFu, 2u, 1u, 3u)); // ADD.xyzw vf3, vf1, vf2 + + VU1Interpreter vu1; + vu1.state().vf[3][0] = 3.0f; + vu1.state().vf[3][1] = 4.0f; + vu1.state().vf[3][2] = 12.0f; + vu1.state().vf[3][3] = 1.0f; + vu1.state().vf[1][0] = 6.0f; + vu1.state().vf[1][1] = 8.0f; + vu1.state().vf[1][2] = 24.0f; + vu1.state().vf[1][3] = 2.0f; + vu1.state().vf[2][0] = 6.0f; + vu1.state().vf[2][1] = 8.0f; + vu1.state().vf[2][2] = 24.0f; + vu1.state().vf[2][3] = 2.0f; + + vu1.execute(fx.code, PS2_VU1_CODE_SIZE, fx.data, PS2_VU1_DATA_SIZE, fx.gs, &fx.mem, 0u, 0u, 0u, 1u); + + t.Equals(vu1.state().p, 1.0f / 13.0f, "ERLENG should read the pre-pair reciprocal length of vf3"); + t.Equals(vu1.state().vf[3][0], 12.0f, "upper ADD write should land on x"); + t.Equals(vu1.state().vf[3][1], 16.0f, "upper ADD write should land on y"); + t.Equals(vu1.state().vf[3][2], 48.0f, "upper ADD write should land on z"); + t.Equals(vu1.state().vf[3][3], 4.0f, "upper ADD write should land on w"); + }); + tc.Run("DIV and SQRT update the Q register from selected vector components", [](TestCase &t) { Vu1Fixture fx; From d0b7d16409b92d76fa17686dbc971d748613d244 Mon Sep 17 00:00:00 2001 From: Shane Michael Mathews Date: Fri, 31 Jul 2026 19:23:35 -0400 Subject: [PATCH 2/2] fix(vu1): give both halves of a reordered pair the pre-pair VF and Q Both halves of a VU1 instruction pair issue concurrently, so both must observe the register state that predates the pair, and a VF write collision resolves in favour of the upper half for the whole register, not just the fields it names (VU User's Manual, section 3.4.3, "Priority for Writing to a Register"). The lowerBeforeUpper branch of run() previously ran the lower half first only to satisfy the lower-reads-upper direction of the hazard; the upper half still saw whatever the lower half had just written, and any lower write that collided with the upper half's destination survived on the fields the upper half did not name. Snapshot the VF registers the upper half's fs, ft and destination touch before the lower half runs, restore them for the upper half, and re-apply the lower half's write afterward everywhere except the upper half's own destination register. Snapshot and restore Q the same way: DIV, SQRT and RSQRT write Q from the lower half and the upper half's q-form operations read it, so the reorder fed the upper half a Q the lower half had just produced. No upper instruction writes Q, so Q is restored unconditionally and needs no priority arbitration. Q is the only register outside the VF file where this arises - no lower operation writes ACC or I, and P and R are read only by lower operations, of which a pair holds exactly one. Section 3.4.1 ("Hazards") exempts ACC, I, Q, P and R from hazard generation, but that exemption is about interlocks, not about which value a concurrently issued instruction observes; the same paragraph points at WAITQ/WAITP for synchronising Q and P. Backed further by section 3.4.2 ("Upper Instruction and Lower Instruction", concurrent issue) and section 3.4.4 ("FMAC Pipeline", no intra-pair dependency stalls). Also classify the four R-register lower operations the hazard table still left at its default arm: RNEXT and RGET write VF[ft], RINIT and RXOR read VF[fs]. None has an execution body in this tree, so nothing observable changes today; leaving them unclassified would have let an implementation of RGET or RNEXT bypass both the reorder predicate and the write-priority machinery. RGET's own instruction page is where the manual states that whole-register discard rule for a lower operation. That completes the lower-special selector space: every selector the manual allocates that names a VF operand is now reported, and the ones that name only integer registers or no operand are pinned absent. One direction of this change removes a write that previously landed: a reordered pair whose lower half wrote the upper half's destination now has that write discarded in whole-register units, including fields the upper half did not touch itself. Inter-pair Q and P visibility latency is unchanged and remains out of scope. This tree still makes a Q or P write visible immediately rather than at the end of the FDIV or EFU pipeline. --- ps2xRuntime/src/lib/vu/ps2_vu1_core.cpp | 44 +++- ps2xRuntime/src/lib/vu/ps2_vu1_detail.h | 43 ++++ ps2xTest/src/ps2_vu1_tests.cpp | 307 ++++++++++++++++++++++++ 3 files changed, 391 insertions(+), 3 deletions(-) diff --git a/ps2xRuntime/src/lib/vu/ps2_vu1_core.cpp b/ps2xRuntime/src/lib/vu/ps2_vu1_core.cpp index 6f24894c5..fcb24b50b 100644 --- a/ps2xRuntime/src/lib/vu/ps2_vu1_core.cpp +++ b/ps2xRuntime/src/lib/vu/ps2_vu1_core.cpp @@ -152,11 +152,49 @@ void VU1Interpreter::run(uint8_t *vuCode, uint32_t codeSize, } else if (decoded.lowerBeforeUpper) { - // VU upper/lower execute as a pair. If the upper op writes a VF register - // that the lower op reads or also writes, Dobie runs the lower side first - // so it observes the old VF value and the upper write has priority. + // Both halves of a pair are issued concurrently, so both observe the + // pre-pair VF register file and the pre-pair Q, and a VF write + // collision resolves in favour of the upper half for the whole + // register - not just the fields it names. Running the lower half + // first gives it the pre-pair values; the snapshots below give the + // upper half the same guarantee for its own VF operands and for Q, + // and enforce the VF write priority afterwards. No upper + // instruction writes Q, so Q is restored unconditionally and needs + // no priority arbitration. Inter-pair Q and P visibility latency is + // a separate concern and is not modelled here. + const uint32_t lowerWrites = vuLowerVfWriteMask(decoded.lower); + const uint8_t upperWriteReg = vuUpperVfWriteReg(decoded.upper); + + uint8_t guarded[3]; + uint8_t guardedCount = 0u; + vuAddGuardedReg(guarded, guardedCount, FS(decoded.upper), lowerWrites); + vuAddGuardedReg(guarded, guardedCount, FT(decoded.upper), lowerWrites); + vuAddGuardedReg(guarded, guardedCount, upperWriteReg, lowerWrites); + + float prePair[3][4]; + float postLower[3][4]; + for (uint8_t i = 0u; i < guardedCount; ++i) + std::memcpy(prePair[i], m_state.vf[guarded[i]], sizeof(prePair[i])); + const float prePairQ = m_state.q; + execLower(decoded.lower, vuData, dataSize, gs, memory, decoded.upper); + + for (uint8_t i = 0u; i < guardedCount; ++i) + { + std::memcpy(postLower[i], m_state.vf[guarded[i]], sizeof(postLower[i])); + std::memcpy(m_state.vf[guarded[i]], prePair[i], sizeof(prePair[i])); + } + const float postLowerQ = m_state.q; + m_state.q = prePairQ; + execUpper(decoded.upper); + m_state.q = postLowerQ; + + for (uint8_t i = 0u; i < guardedCount; ++i) + { + if (guarded[i] != upperWriteReg) + std::memcpy(m_state.vf[guarded[i]], postLower[i], sizeof(postLower[i])); + } } else { diff --git a/ps2xRuntime/src/lib/vu/ps2_vu1_detail.h b/ps2xRuntime/src/lib/vu/ps2_vu1_detail.h index 19ad13662..c474c7b1a 100644 --- a/ps2xRuntime/src/lib/vu/ps2_vu1_detail.h +++ b/ps2xRuntime/src/lib/vu/ps2_vu1_detail.h @@ -120,6 +120,22 @@ static inline void vuLowerVfReadWriteMasks(uint32_t lower, uint32_t &readMask, u case 0x64: // MFP vuSetRegBit(writeMask, it); return; + // R-register block. RNEXT and RGET store the random number into + // VF[ft]; RINIT and RXOR take VF[fs]fsf as their only vector + // operand. RGET's instruction page states the rule this table + // exists to serve: "When an Upper instruction in the same cycle + // writes data to the VF[ft] register, the result of this + // instruction is discarded with priority given to the Upper + // instruction, regardless of whether the data is written to the + // same field or not." + case 0x40: // RNEXT + case 0x41: // RGET + vuSetRegBit(writeMask, it); + return; + case 0x42: // RINIT + case 0x43: // RXOR + vuSetRegBit(readMask, is); + return; // EFU block. Every EFU operation takes VF[is] as its only vector // operand and writes the P register; see the VU User's Manual EFU // instruction pages ("ESADD P, VF[fs]" through "EEXP P, VF[fs]fsf"). @@ -162,6 +178,33 @@ static inline void vuLowerVfReadWriteMasks(uint32_t lower, uint32_t &readMask, u } } +static inline uint32_t vuLowerVfWriteMask(uint32_t lower) +{ + uint32_t reads = 0u; + uint32_t writes = 0u; + vuLowerVfReadWriteMasks(lower, reads, writes); + return writes; +} + +// Collects the VF registers a reordered pair has to protect: the upper half's +// source registers, whose pre-pair values it must still observe, and the +// upper half's destination register, whose lower-half write hardware discards. +// A register is collected at most once, and that is required rather than tidy: +// the caller captures and rolls back each entry in turn, so a second entry for +// an already-rolled-back register would capture the pre-pair value and replay +// it over the lower half's write. +static inline void vuAddGuardedReg(uint8_t *regs, uint8_t &count, uint8_t reg, uint32_t lowerWrites) +{ + if (reg == 0u || ((lowerWrites >> reg) & 1u) == 0u) + return; + for (uint8_t i = 0u; i < count; ++i) + { + if (regs[i] == reg) + return; + } + regs[count++] = reg; +} + static inline bool vuLowerShouldRunBeforeUpper(uint32_t upper, uint32_t lower) { const uint8_t upperWrite = vuUpperVfWriteReg(upper); diff --git a/ps2xTest/src/ps2_vu1_tests.cpp b/ps2xTest/src/ps2_vu1_tests.cpp index 307896906..0c013db2d 100644 --- a/ps2xTest/src/ps2_vu1_tests.cpp +++ b/ps2xTest/src/ps2_vu1_tests.cpp @@ -82,6 +82,15 @@ namespace static_cast(op & 0x3Fu); } + uint32_t makeVuUpperSpecial(uint8_t specialOp, uint8_t dest, uint8_t ft, uint8_t fs) + { + return (static_cast(dest & 0xFu) << 21) | + (static_cast(ft & 0x1Fu) << 16) | + (static_cast(fs & 0x1Fu) << 11) | + (static_cast((specialOp & 0x7Cu) >> 2) << 6) | + static_cast(0x3Cu | (specialOp & 0x3u)); + } + uint32_t makeVuLq(uint8_t dest, uint8_t targetVf, uint8_t baseVi, int16_t imm) { return (static_cast(dest & 0xFu) << 21) | @@ -423,6 +432,88 @@ void register_ps2_vu1_tests() } }); + tc.Run("R-register lower ops report their VF operand", [](TestCase &t) + { + { + uint32_t readMask = 0u; + uint32_t writeMask = 0u; + vuLowerVfReadWriteMasks(makeVuLowerSpecial(0x40u, 3u, 5u, 0u, 0xFu), readMask, writeMask); + t.Equals(readMask, 0u, "RNEXT should report no VF read"); + t.Equals(writeMask, static_cast(1u << 5), "RNEXT should report writing VF[ft]"); + } + + { + uint32_t readMask = 0u; + uint32_t writeMask = 0u; + vuLowerVfReadWriteMasks(makeVuLowerSpecial(0x41u, 3u, 5u, 0u, 0xFu), readMask, writeMask); + t.Equals(readMask, 0u, "RGET should report no VF read"); + t.Equals(writeMask, static_cast(1u << 5), "RGET should report writing VF[ft]"); + } + + { + uint32_t readMask = 0u; + uint32_t writeMask = 0u; + vuLowerVfReadWriteMasks(makeVuLowerSpecial(0x42u, 3u, 5u, 0u, 0u), readMask, writeMask); + t.Equals(readMask, static_cast(1u << 3), "RINIT should report reading VF[fs]"); + t.Equals(writeMask, 0u, "RINIT should report no VF write"); + } + + { + uint32_t readMask = 0u; + uint32_t writeMask = 0u; + vuLowerVfReadWriteMasks(makeVuLowerSpecial(0x43u, 3u, 5u, 0u, 0u), readMask, writeMask); + t.Equals(readMask, static_cast(1u << 3), "RXOR should report reading VF[fs]"); + t.Equals(writeMask, 0u, "RXOR should report no VF write"); + } + }); + + tc.Run("lower-special ops that name no VF operand report neither read nor write", [](TestCase &t) + { + // The lower-special selectors the manual allocates to operations + // whose only operands are integer registers, or which take no + // operand at all. Their absence from the hazard table is + // deliberate, so it is pinned rather than left to inspection. + const uint8_t viOnlyOps[] = {0x3Bu, 0x3Eu, 0x3Fu, 0x68u, 0x69u, 0x6Cu}; + for (uint8_t op : viOnlyOps) + { + uint32_t readMask = 0u; + uint32_t writeMask = 0u; + vuLowerVfReadWriteMasks(makeVuLowerSpecial(op, 3u, 5u, 0u, 0xFu), readMask, writeMask); + t.Equals(readMask, 0u, "integer-only lower op should report no VF read"); + t.Equals(writeMask, 0u, "integer-only lower op should report no VF write"); + } + }); + + tc.Run("upper half reads the pre-pair Q value the lower half overwrites", [](TestCase &t) + { + Vu1Fixture fx; + t.IsTrue(fx.initialize(), "VU1 fixture should initialize"); + + writeVuInstructionPair(fx.code, + 0u, + makeVuDiv(3u, 4u, 0u, 0u), // DIV Q, vf3x, vf4x + makeVuUpper(0x20u, 0xFu, 0u, 3u, 3u)); // ADDq.xyzw vf3, vf3, Q + + VU1Interpreter vu1; + vu1.state().q = 100.0f; + vu1.state().vf[3][0] = 8.0f; + vu1.state().vf[3][1] = 8.0f; + vu1.state().vf[3][2] = 8.0f; + vu1.state().vf[3][3] = 8.0f; + vu1.state().vf[4][0] = 2.0f; + vu1.state().vf[4][1] = 2.0f; + vu1.state().vf[4][2] = 2.0f; + vu1.state().vf[4][3] = 2.0f; + + vu1.execute(fx.code, PS2_VU1_CODE_SIZE, fx.data, PS2_VU1_DATA_SIZE, fx.gs, &fx.mem, 0u, 0u, 0u, 1u); + + t.Equals(vu1.state().vf[3][0], 108.0f, "upper ADDq should use the pre-pair Q for x"); + t.Equals(vu1.state().vf[3][1], 108.0f, "upper ADDq should use the pre-pair Q for y"); + t.Equals(vu1.state().vf[3][2], 108.0f, "upper ADDq should use the pre-pair Q for z"); + t.Equals(vu1.state().vf[3][3], 108.0f, "upper ADDq should use the pre-pair Q for w"); + t.Equals(vu1.state().q, 4.0f, "lower DIV should still leave its own result in Q"); + }); + tc.Run("EFU pair with no VF write leaves the reordered branch behaviourally unchanged", [](TestCase &t) { Vu1Fixture fx; @@ -456,6 +547,222 @@ void register_ps2_vu1_tests() t.Equals(vu1.state().vf[3][3], 4.0f, "upper ADD write should land on w"); }); + tc.Run("upper half reads the pre-pair value of an fs operand the lower half overwrites", [](TestCase &t) + { + Vu1Fixture fx; + t.IsTrue(fx.initialize(), "VU1 fixture should initialize"); + + writeVuInstructionPair(fx.code, + 0u, + makeVuLowerSpecial(0x30u, 5u, 1u, 0u, 0xFu), // MOVE.xyzw vf1, vf5 + makeVuUpper(0x28u, 0xFu, 2u, 1u, 5u)); // ADD.xyzw vf5, vf1, vf2 + + VU1Interpreter vu1; + vu1.state().vf[1][0] = 1.0f; + vu1.state().vf[1][1] = 2.0f; + vu1.state().vf[1][2] = 3.0f; + vu1.state().vf[1][3] = 4.0f; + vu1.state().vf[2][0] = 10.0f; + vu1.state().vf[2][1] = 20.0f; + vu1.state().vf[2][2] = 30.0f; + vu1.state().vf[2][3] = 40.0f; + vu1.state().vf[5][0] = 100.0f; + vu1.state().vf[5][1] = 200.0f; + vu1.state().vf[5][2] = 300.0f; + vu1.state().vf[5][3] = 400.0f; + + vu1.execute(fx.code, PS2_VU1_CODE_SIZE, fx.data, PS2_VU1_DATA_SIZE, fx.gs, &fx.mem, 0u, 0u, 0u, 1u); + + t.Equals(vu1.state().vf[5][0], 11.0f, "upper ADD should use the pre-pair vf1 for x"); + t.Equals(vu1.state().vf[5][1], 22.0f, "upper ADD should use the pre-pair vf1 for y"); + t.Equals(vu1.state().vf[5][2], 33.0f, "upper ADD should use the pre-pair vf1 for z"); + t.Equals(vu1.state().vf[5][3], 44.0f, "upper ADD should use the pre-pair vf1 for w"); + t.Equals(vu1.state().vf[1][0], 100.0f, "lower MOVE write to vf1 should still land on x"); + t.Equals(vu1.state().vf[1][1], 200.0f, "lower MOVE write to vf1 should still land on y"); + t.Equals(vu1.state().vf[1][2], 300.0f, "lower MOVE write to vf1 should still land on z"); + t.Equals(vu1.state().vf[1][3], 400.0f, "lower MOVE write to vf1 should still land on w"); + }); + + tc.Run("upper half reads the pre-pair value of an ft operand the lower half overwrites", [](TestCase &t) + { + Vu1Fixture fx; + t.IsTrue(fx.initialize(), "VU1 fixture should initialize"); + + writeVuInstructionPair(fx.code, + 0u, + makeVuLowerSpecial(0x30u, 5u, 2u, 0u, 0xFu), // MOVE.xyzw vf2, vf5 + makeVuUpper(0x2Au, 0xFu, 2u, 1u, 5u)); // MUL.xyzw vf5, vf1, vf2 + + VU1Interpreter vu1; + vu1.state().vf[1][0] = 1.0f; + vu1.state().vf[1][1] = 2.0f; + vu1.state().vf[1][2] = 3.0f; + vu1.state().vf[1][3] = 4.0f; + vu1.state().vf[2][0] = 2.0f; + vu1.state().vf[2][1] = 4.0f; + vu1.state().vf[2][2] = 5.0f; + vu1.state().vf[2][3] = 10.0f; + vu1.state().vf[5][0] = 100.0f; + vu1.state().vf[5][1] = 200.0f; + vu1.state().vf[5][2] = 300.0f; + vu1.state().vf[5][3] = 400.0f; + + vu1.execute(fx.code, PS2_VU1_CODE_SIZE, fx.data, PS2_VU1_DATA_SIZE, fx.gs, &fx.mem, 0u, 0u, 0u, 1u); + + t.Equals(vu1.state().vf[5][0], 2.0f, "upper MUL should use the pre-pair vf2 for x"); + t.Equals(vu1.state().vf[5][1], 8.0f, "upper MUL should use the pre-pair vf2 for y"); + t.Equals(vu1.state().vf[5][2], 15.0f, "upper MUL should use the pre-pair vf2 for z"); + t.Equals(vu1.state().vf[5][3], 40.0f, "upper MUL should use the pre-pair vf2 for w"); + t.Equals(vu1.state().vf[2][0], 100.0f, "lower MOVE write to vf2 should still land on x"); + t.Equals(vu1.state().vf[2][1], 200.0f, "lower MOVE write to vf2 should still land on y"); + t.Equals(vu1.state().vf[2][2], 300.0f, "lower MOVE write to vf2 should still land on z"); + t.Equals(vu1.state().vf[2][3], 400.0f, "lower MOVE write to vf2 should still land on w"); + }); + + tc.Run("upper half with fs and ft aliased to one register keeps the lower half's write to it", [](TestCase &t) + { + Vu1Fixture fx; + t.IsTrue(fx.initialize(), "VU1 fixture should initialize"); + + writeVuInstructionPair(fx.code, + 0u, + makeVuLowerSpecial(0x30u, 5u, 2u, 0u, 0xFu), // MOVE.xyzw vf2, vf5 + makeVuUpper(0x28u, 0xFu, 2u, 2u, 5u)); // ADD.xyzw vf5, vf2, vf2 + + VU1Interpreter vu1; + vu1.state().vf[2][0] = 1.0f; + vu1.state().vf[2][1] = 2.0f; + vu1.state().vf[2][2] = 3.0f; + vu1.state().vf[2][3] = 4.0f; + vu1.state().vf[5][0] = 100.0f; + vu1.state().vf[5][1] = 200.0f; + vu1.state().vf[5][2] = 300.0f; + vu1.state().vf[5][3] = 400.0f; + + vu1.execute(fx.code, PS2_VU1_CODE_SIZE, fx.data, PS2_VU1_DATA_SIZE, fx.gs, &fx.mem, 0u, 0u, 0u, 1u); + + t.Equals(vu1.state().vf[5][0], 2.0f, "upper ADD should use the pre-pair vf2 for both operands on x"); + t.Equals(vu1.state().vf[5][1], 4.0f, "upper ADD should use the pre-pair vf2 for both operands on y"); + t.Equals(vu1.state().vf[5][2], 6.0f, "upper ADD should use the pre-pair vf2 for both operands on z"); + t.Equals(vu1.state().vf[5][3], 8.0f, "upper ADD should use the pre-pair vf2 for both operands on w"); + t.Equals(vu1.state().vf[2][0], 100.0f, "aliased guard should keep the lower MOVE write to vf2 on x"); + t.Equals(vu1.state().vf[2][1], 200.0f, "aliased guard should keep the lower MOVE write to vf2 on y"); + t.Equals(vu1.state().vf[2][2], 300.0f, "aliased guard should keep the lower MOVE write to vf2 on z"); + t.Equals(vu1.state().vf[2][3], 400.0f, "aliased guard should keep the lower MOVE write to vf2 on w"); + }); + + tc.Run("upper half write discards the lower half write to the same register in whole-register units", [](TestCase &t) + { + Vu1Fixture fx; + t.IsTrue(fx.initialize(), "VU1 fixture should initialize"); + + writeVuInstructionPair(fx.code, + 0u, + makeVuLowerSpecial(0x30u, 9u, 1u, 0u, 0x1u), // MOVE.w vf1, vf9 + makeVuUpper(0x28u, 0xCu, 3u, 1u, 1u)); // ADD.xy vf1, vf1, vf3 + + VU1Interpreter vu1; + vu1.state().vf[1][0] = 1.0f; + vu1.state().vf[1][1] = 2.0f; + vu1.state().vf[1][2] = 3.0f; + vu1.state().vf[1][3] = 4.0f; + vu1.state().vf[3][0] = 10.0f; + vu1.state().vf[3][1] = 20.0f; + vu1.state().vf[3][2] = 30.0f; + vu1.state().vf[3][3] = 40.0f; + vu1.state().vf[9][0] = 5.0f; + vu1.state().vf[9][1] = 6.0f; + vu1.state().vf[9][2] = 7.0f; + vu1.state().vf[9][3] = 777.0f; + + vu1.execute(fx.code, PS2_VU1_CODE_SIZE, fx.data, PS2_VU1_DATA_SIZE, fx.gs, &fx.mem, 0u, 0u, 0u, 1u); + + t.Equals(vu1.state().vf[1][0], 11.0f, "upper ADD.xy should write x against the pre-pair vf1"); + t.Equals(vu1.state().vf[1][1], 22.0f, "upper ADD.xy should write y against the pre-pair vf1"); + t.Equals(vu1.state().vf[1][2], 3.0f, "lower MOVE.w write should be discarded even though z is untouched"); + t.Equals(vu1.state().vf[1][3], 4.0f, "lower MOVE.w write should be discarded in whole-register units"); + t.Equals(vu1.state().vf[9][0], 5.0f, "lower MOVE source vf9 should be untouched on x"); + t.Equals(vu1.state().vf[9][1], 6.0f, "lower MOVE source vf9 should be untouched on y"); + t.Equals(vu1.state().vf[9][2], 7.0f, "lower MOVE source vf9 should be untouched on z"); + t.Equals(vu1.state().vf[9][3], 777.0f, "lower MOVE source vf9 should be untouched on w"); + }); + + tc.Run("upper-special ft destination discards the lower half write to that register", [](TestCase &t) + { + Vu1Fixture fx; + t.IsTrue(fx.initialize(), "VU1 fixture should initialize"); + + writeVuInstructionPair(fx.code, + 0u, + makeVuLowerSpecial(0x30u, 7u, 4u, 0u, 0xFu), // MOVE.xyzw vf4, vf7 + makeVuUpperSpecial(0x1Du, 0xCu, 4u, 6u)); // ABS.xy vf4, vf6 + + VU1Interpreter vu1; + vu1.state().vf[4][0] = 9.0f; + vu1.state().vf[4][1] = 9.0f; + vu1.state().vf[4][2] = 9.0f; + vu1.state().vf[4][3] = 9.0f; + vu1.state().vf[6][0] = -1.0f; + vu1.state().vf[6][1] = -2.0f; + vu1.state().vf[6][2] = -3.0f; + vu1.state().vf[6][3] = -4.0f; + vu1.state().vf[7][0] = 5.0f; + vu1.state().vf[7][1] = 6.0f; + vu1.state().vf[7][2] = 7.0f; + vu1.state().vf[7][3] = 8.0f; + + vu1.execute(fx.code, PS2_VU1_CODE_SIZE, fx.data, PS2_VU1_DATA_SIZE, fx.gs, &fx.mem, 0u, 0u, 0u, 1u); + + t.Equals(vu1.state().vf[4][0], 1.0f, "ABS.xy should write x against the pre-pair vf6"); + t.Equals(vu1.state().vf[4][1], 2.0f, "ABS.xy should write y against the pre-pair vf6"); + t.Equals(vu1.state().vf[4][2], 9.0f, "lower MOVE write to vf4 z should be discarded"); + t.Equals(vu1.state().vf[4][3], 9.0f, "lower MOVE write to vf4 w should be discarded"); + t.Equals(vu1.state().vf[7][0], 5.0f, "lower MOVE source vf7 should be untouched on x"); + t.Equals(vu1.state().vf[7][1], 6.0f, "lower MOVE source vf7 should be untouched on y"); + t.Equals(vu1.state().vf[7][2], 7.0f, "lower MOVE source vf7 should be untouched on z"); + t.Equals(vu1.state().vf[7][3], 8.0f, "lower MOVE source vf7 should be untouched on w"); + }); + + tc.Run("upper half destination is guarded even when it is not one of its own operands", [](TestCase &t) + { + Vu1Fixture fx; + t.IsTrue(fx.initialize(), "VU1 fixture should initialize"); + + writeVuInstructionPair(fx.code, + 0u, + makeVuLowerSpecial(0x30u, 9u, 1u, 0u, 0xFu), // MOVE.xyzw vf1, vf9 + makeVuUpper(0x28u, 0xCu, 3u, 2u, 1u)); // ADD.xy vf1, vf2, vf3 + + VU1Interpreter vu1; + vu1.state().vf[1][0] = 1.0f; + vu1.state().vf[1][1] = 2.0f; + vu1.state().vf[1][2] = 3.0f; + vu1.state().vf[1][3] = 4.0f; + vu1.state().vf[2][0] = 10.0f; + vu1.state().vf[2][1] = 20.0f; + vu1.state().vf[2][2] = 30.0f; + vu1.state().vf[2][3] = 40.0f; + vu1.state().vf[3][0] = 100.0f; + vu1.state().vf[3][1] = 200.0f; + vu1.state().vf[3][2] = 300.0f; + vu1.state().vf[3][3] = 400.0f; + vu1.state().vf[9][0] = 5.0f; + vu1.state().vf[9][1] = 6.0f; + vu1.state().vf[9][2] = 7.0f; + vu1.state().vf[9][3] = 8.0f; + + vu1.execute(fx.code, PS2_VU1_CODE_SIZE, fx.data, PS2_VU1_DATA_SIZE, fx.gs, &fx.mem, 0u, 0u, 0u, 1u); + + t.Equals(vu1.state().vf[1][0], 110.0f, "upper ADD.xy should write x"); + t.Equals(vu1.state().vf[1][1], 220.0f, "upper ADD.xy should write y"); + t.Equals(vu1.state().vf[1][2], 3.0f, "z should keep its pre-pair value, not the lower half's write"); + t.Equals(vu1.state().vf[1][3], 4.0f, "w should keep its pre-pair value, not the lower half's write"); + t.Equals(vu1.state().vf[9][0], 5.0f, "lower MOVE source vf9 should be untouched on x"); + t.Equals(vu1.state().vf[9][1], 6.0f, "lower MOVE source vf9 should be untouched on y"); + t.Equals(vu1.state().vf[9][2], 7.0f, "lower MOVE source vf9 should be untouched on z"); + t.Equals(vu1.state().vf[9][3], 8.0f, "lower MOVE source vf9 should be untouched on w"); + }); + tc.Run("DIV and SQRT update the Q register from selected vector components", [](TestCase &t) { Vu1Fixture fx;