From 1db6b4516b488414ee8de7488e678ea020a8d2f1 Mon Sep 17 00:00:00 2001 From: Shane Michael Mathews Date: Mon, 27 Jul 2026 06:57:00 -0400 Subject: [PATCH] fix(vu1): maintain MAC/STATUS/CLIP flag registers, correct the flag-reading lower-op table, and saturate FTOI The VU1 interpreter declared mac/status/clip fields and the lower-op instructions that read them, but the upper FMAC pipeline never wrote mac, and STATUS was written by exactly one instruction using an unsourced immediate extraction. Every arithmetic FMAC vf-writer and ACC-writer now computes MAC over its DEST lanes and folds it into a STATUS live/sticky pair; MAX/MINI (which the PS2 FMAC pipeline runs without any flag update, matching PCSX2's applyMinMax) and the nine ITOF/FTOI/ABS unary ops and the plain dest-mask lower ops (LQ, MOVE, MR32, LQI, LQD, MFIR, MFP) are explicitly excluded from the flag path. Separately, the flag-reading lower-op table at 0x18-0x1C had two of its four case bodies swapped (0x18/0x1A) and was missing FMOR (0x1B) and FCGET (0x1C, which ran a duplicate FMOR body instead). FSSET's immediate extraction is replaced with the formula sourced from PCSX2's reference VU interpreter, since none of the formulas previously in circulation for this codebase matched it. CLIP's shift-register accumulation is now masked to 24 bits, since without it FCOR permanently loses the ability to compare true once five or more CLIP instructions have run. FTOI0/ 4/12/15 now saturate on overflow instead of relying on undefined behavior, which on x86 silently produced INT_MIN for large positive values too. All of this is sourced against PCSX2 pcsx2/VUops.cpp and pcsx2/VUflags.cpp (see EVIDENCE.md); pipeline latency (FDIV/Q, EFU/P, and any multi-cycle flag commit delay) is out of scope here and left for a follow-up with its own citation. --- ps2xRuntime/include/runtime/ps2_vu1.h | 3 +- ps2xRuntime/src/lib/vu/ps2_vu1_core.cpp | 69 +++- ps2xRuntime/src/lib/vu/ps2_vu1_lower.cpp | 39 +- ps2xRuntime/src/lib/vu/ps2_vu1_upper.cpp | 125 ++++-- ps2xTest/src/ps2_vu1_tests.cpp | 501 +++++++++++++++++++++++ 5 files changed, 680 insertions(+), 57 deletions(-) diff --git a/ps2xRuntime/include/runtime/ps2_vu1.h b/ps2xRuntime/include/runtime/ps2_vu1.h index cdbdec9c7..0b22f51ec 100644 --- a/ps2xRuntime/include/runtime/ps2_vu1.h +++ b/ps2xRuntime/include/runtime/ps2_vu1.h @@ -80,8 +80,9 @@ class VU1Interpreter void execUpper(uint32_t instr); void execLower(uint32_t instr, uint8_t *vuData, uint32_t dataSize, GS &gs, PS2Memory *memory, uint32_t upperInstr); - void applyDest(float *dst, const float *result, uint8_t dest); + void writeDestMasked(float *dst, const float *result, uint8_t dest); void applyDestAcc(const float *result, uint8_t dest); + void computeFmacFlags(const float *result, uint8_t dest); float broadcast(const float *vf, uint8_t bc); }; diff --git a/ps2xRuntime/src/lib/vu/ps2_vu1_core.cpp b/ps2xRuntime/src/lib/vu/ps2_vu1_core.cpp index 6f24894c5..3724b79c3 100644 --- a/ps2xRuntime/src/lib/vu/ps2_vu1_core.cpp +++ b/ps2xRuntime/src/lib/vu/ps2_vu1_core.cpp @@ -21,7 +21,7 @@ float VU1Interpreter::broadcast(const float *vf, uint8_t bc) return vf[bc & 3]; } -void VU1Interpreter::applyDest(float *dst, const float *result, uint8_t dest) +void VU1Interpreter::writeDestMasked(float *dst, const float *result, uint8_t dest) { if (dest & 0x8) dst[0] = result[0]; // x @@ -35,7 +35,72 @@ void VU1Interpreter::applyDest(float *dst, const float *result, uint8_t dest) void VU1Interpreter::applyDestAcc(const float *result, uint8_t dest) { - applyDest(m_state.acc, result, dest); + // ACC-writers are FMAC ops: MAC reflects the FMAC result whether it targets vf or acc. + computeFmacFlags(result, dest); + writeDestMasked(m_state.acc, result, dest); +} + +// MAC bit layout sourced from PCSX2 pcsx2/VUflags.cpp (VU_MAC_UPDATE / VU_STAT_UPDATE): +// nibbles from low to high are Z[3:0], S[7:4], U[11:8], O[15:12]; within each nibble the +// bit order is x,y,z,w from high to low (x uses shift 3, y shift 2, z shift 1, w shift 0). +// Z is set for +/-0.0f or a denormal (an underflow PCSX2 flushes to zero, raising both U +// and Z together); S mirrors the IEEE754 sign bit unconditionally; U is a nonzero denormal; +// O is an exponent field of all ones (inf or NaN). +// Lanes outside DEST are not evaluated and read 0 in every group, matching PCSX2's per-lane +// VU_MACx/y/z/w_CLEAR calls for lanes the FMAC op did not write. +void VU1Interpreter::computeFmacFlags(const float *result, uint8_t dest) +{ + static const uint8_t kLaneDestBit[4] = {0x8u, 0x4u, 0x2u, 0x1u}; // x, y, z, w + uint32_t mac = 0u; + + for (int c = 0; c < 4; ++c) + { + if ((dest & kLaneDestBit[c]) == 0u) + continue; + + uint32_t bits; + std::memcpy(&bits, &result[c], sizeof(bits)); + const uint32_t exp = (bits >> 23) & 0xFFu; + const int shift = 3 - c; + + if (bits & 0x80000000u) + mac |= (0x0010u << shift); // S + + if (result[c] == 0.0f) + { + mac |= (0x0001u << shift); // Z + } + else if (exp == 0u) + { + mac |= (0x0101u << shift); // U + Z: a denormal is an underflow PCSX2 flushes to zero + } + else if (exp == 0xFFu) + { + mac |= (0x1000u << shift); // O + } + } + + m_state.mac = mac; + + // STATUS folds MAC the way PCSX2's VU_STAT_UPDATE does: a live bit is set if ANY dest + // lane raised that condition this instruction. The live half is bits[3:0] (Z,S,U,O in + // that order) and is replaced whole every FMAC; the sticky half is bits[9:6] and only + // ever accumulates (OR), matching how FSOR/FSAND read "has this ever happened". Bits + // 4,5,10,11 (I/D, live and sticky) are not modelled here and stay zero. + uint32_t live = 0u; + if (mac & 0x000Fu) + live |= 0x1u; // Z + if (mac & 0x00F0u) + live |= 0x2u; // S + if (mac & 0x0F00u) + live |= 0x4u; // U + if (mac & 0xF000u) + live |= 0x8u; // O + + const uint32_t stickyPrev = (m_state.status >> 6) & 0xFu; + const uint32_t sticky = stickyPrev | live; + + m_state.status = (live & 0x3Fu) | ((sticky & 0x3Fu) << 6); } VU1Interpreter::DecodedInstructionPair VU1Interpreter::decodeInstructionPair(const uint8_t *vuCode, uint32_t pc) const diff --git a/ps2xRuntime/src/lib/vu/ps2_vu1_lower.cpp b/ps2xRuntime/src/lib/vu/ps2_vu1_lower.cpp index f89ba7bc4..37d3c76ec 100644 --- a/ps2xRuntime/src/lib/vu/ps2_vu1_lower.cpp +++ b/ps2xRuntime/src/lib/vu/ps2_vu1_lower.cpp @@ -35,7 +35,7 @@ void VU1Interpreter::execLower(uint32_t instr, uint8_t *vuData, uint32_t dataSiz { float tmp[4]; std::memcpy(tmp, vuData + addr, 16); - applyDest(m_state.vf[it], tmp, dest); + writeDestMasked(m_state.vf[it], tmp, dest); } return; } @@ -164,7 +164,11 @@ void VU1Interpreter::execLower(uint32_t instr, uint8_t *vuData, uint32_t dataSiz } case 0x15: // FSSET { - m_state.status = (instr >> 6) & 0xFC0; + // Immediate extraction sourced from PCSX2 pcsx2/VUops.cpp _vuFSSET: + // imm12 = ((instr>>21)&1)<<11 | (instr&0x7FF). FSSET only ever writes the sticky + // half (bits 11:6); the live half (bits 5:0) is left as whatever the last FMAC set. + uint32_t imm12 = (((instr >> 21) & 0x1u) << 11) | (instr & 0x7FFu); + m_state.status = (imm12 & 0xFC0u) | (m_state.status & 0x3Fu); return; } case 0x16: // FSAND @@ -181,23 +185,25 @@ void VU1Interpreter::execLower(uint32_t instr, uint8_t *vuData, uint32_t dataSiz m_state.vi[1] = ((m_state.status | imm12) == 0xFFF) ? 1 : 0; return; } - case 0x18: // FMAND + case 0x18: // FMEQ { + // Lower-op table sourced from PCSX2 pcsx2/VUops.cpp _LOWER_OPCODE[128]: 0x18=FMEQ, + // 0x19 unassigned, 0x1A=FMAND, 0x1B=FMOR, 0x1C=FCGET. uint8_t it = VIT(instr); uint8_t is = VIS(instr); if (it != 0) - m_state.vi[it] = (int32_t)(m_state.mac & (uint32_t)(uint16_t)m_state.vi[is]); + m_state.vi[it] = ((m_state.mac & 0xFFFF) == (uint32_t)(uint16_t)m_state.vi[is]) ? 1 : 0; return; } - case 0x1A: // FMEQ + case 0x1A: // FMAND { uint8_t it = VIT(instr); uint8_t is = VIS(instr); if (it != 0) - m_state.vi[it] = ((m_state.mac & 0xFFFF) == (uint32_t)(uint16_t)m_state.vi[is]) ? 1 : 0; + m_state.vi[it] = (int32_t)(m_state.mac & (uint32_t)(uint16_t)m_state.vi[is]); return; } - case 0x1C: // FMOR + case 0x1B: // FMOR { uint8_t it = VIT(instr); uint8_t is = VIS(instr); @@ -205,6 +211,13 @@ void VU1Interpreter::execLower(uint32_t instr, uint8_t *vuData, uint32_t dataSiz m_state.vi[it] = (int32_t)(m_state.mac | (uint32_t)(uint16_t)m_state.vi[is]); return; } + case 0x1C: // FCGET + { + uint8_t it = VIT(instr); + if (it != 0) + m_state.vi[it] = (int32_t)(m_state.clip & 0xFFFu); + return; + } case 0x20: // B (unconditional branch) { int16_t imm = IMM11(instr); @@ -465,13 +478,13 @@ void VU1Interpreter::execLower(uint32_t instr, uint8_t *vuData, uint32_t dataSiz { float tmp[4]; std::memcpy(tmp, m_state.vf[vfS], 16); - applyDest(m_state.vf[vfT], tmp, dest); + writeDestMasked(m_state.vf[vfT], tmp, dest); return; } case 0x31: // MR32 (rotate right by 32 bits = shift xyzw -> yzwx) { float tmp[4] = {m_state.vf[vfS][1], m_state.vf[vfS][2], m_state.vf[vfS][3], m_state.vf[vfS][0]}; - applyDest(m_state.vf[vfT], tmp, dest); + writeDestMasked(m_state.vf[vfT], tmp, dest); return; } case 0x34: // LQI (Load Quadword, post-increment) @@ -482,7 +495,7 @@ void VU1Interpreter::execLower(uint32_t instr, uint8_t *vuData, uint32_t dataSiz { float tmp[4]; std::memcpy(tmp, vuData + addr, 16); - applyDest(m_state.vf[vfT], tmp, dest); + writeDestMasked(m_state.vf[vfT], tmp, dest); } if (viS != 0) m_state.vi[viS] = (int16_t)(m_state.vi[viS] + 1); @@ -520,7 +533,7 @@ void VU1Interpreter::execLower(uint32_t instr, uint8_t *vuData, uint32_t dataSiz { float tmp[4]; std::memcpy(tmp, vuData + addr, 16); - applyDest(m_state.vf[vfT], tmp, dest); + writeDestMasked(m_state.vf[vfT], tmp, dest); } return; } @@ -604,7 +617,7 @@ void VU1Interpreter::execLower(uint32_t instr, uint8_t *vuData, uint32_t dataSiz result[1] = result[0]; result[2] = result[0]; result[3] = result[0]; - applyDest(m_state.vf[vfT], result, dest); + writeDestMasked(m_state.vf[vfT], result, dest); return; } case 0x3E: // ILWR - integer load word from address in VI[is] @@ -658,7 +671,7 @@ void VU1Interpreter::execLower(uint32_t instr, uint8_t *vuData, uint32_t dataSiz case 0x64: // MFP (Move From P register) { float result[4] = {m_state.p, m_state.p, m_state.p, m_state.p}; - applyDest(m_state.vf[vfT], result, dest); + writeDestMasked(m_state.vf[vfT], result, dest); return; } case 0x68: // XTOP - move current VIF1 TOP into VI register diff --git a/ps2xRuntime/src/lib/vu/ps2_vu1_upper.cpp b/ps2xRuntime/src/lib/vu/ps2_vu1_upper.cpp index 27dde2d81..151c696c7 100644 --- a/ps2xRuntime/src/lib/vu/ps2_vu1_upper.cpp +++ b/ps2xRuntime/src/lib/vu/ps2_vu1_upper.cpp @@ -3,6 +3,24 @@ #include #include +#include + +namespace +{ + // Saturating float->int32 conversion sourced from PCSX2 pcsx2/VUops.cpp floatToInt. + // `scaled` is the FTOI operand already multiplied by 2^Offset (1/16/4096/32768 for + // FTOI0/4/12/15). A scaled magnitude of 2^31 or more, including inf and NaN, saturates to + // INT_MAX or INT_MIN by the sign bit; NaN follows the same sign-based clamp as overflow, + // there is no separate NaN case in the source. + int32_t ftoiSaturate(float scaled) + { + uint32_t bits; + std::memcpy(&bits, &scaled, sizeof(bits)); + if ((bits & 0x7F800000u) >= 0x4F000000u) + return (bits & 0x80000000u) ? static_cast(0x80000000u) : 0x7FFFFFFF; + return static_cast(scaled); + } +} // ============================================================================ // Upper instructions (FMAC pipeline) @@ -31,7 +49,8 @@ void VU1Interpreter::execUpper(uint32_t instr) float bc = broadcast(vt, op & 3); for (int c = 0; c < 4; c++) result[c] = vs[c] + bc; - applyDest(vd, result, dest); + computeFmacFlags(result, dest); + writeDestMasked(vd, result, dest); return; } case 0x04: @@ -42,7 +61,8 @@ void VU1Interpreter::execUpper(uint32_t instr) float bc = broadcast(vt, op & 3); for (int c = 0; c < 4; c++) result[c] = vs[c] - bc; - applyDest(vd, result, dest); + computeFmacFlags(result, dest); + writeDestMasked(vd, result, dest); return; } case 0x08: @@ -53,7 +73,8 @@ void VU1Interpreter::execUpper(uint32_t instr) float bc = broadcast(vt, op & 3); for (int c = 0; c < 4; c++) result[c] = m_state.acc[c] + vs[c] * bc; - applyDest(vd, result, dest); + computeFmacFlags(result, dest); + writeDestMasked(vd, result, dest); return; } case 0x0C: @@ -64,7 +85,8 @@ void VU1Interpreter::execUpper(uint32_t instr) float bc = broadcast(vt, op & 3); for (int c = 0; c < 4; c++) result[c] = m_state.acc[c] - vs[c] * bc; - applyDest(vd, result, dest); + computeFmacFlags(result, dest); + writeDestMasked(vd, result, dest); return; } case 0x10: @@ -75,7 +97,7 @@ void VU1Interpreter::execUpper(uint32_t instr) float bc = broadcast(vt, op & 3); for (int c = 0; c < 4; c++) result[c] = (vs[c] > bc) ? vs[c] : bc; - applyDest(vd, result, dest); + writeDestMasked(vd, result, dest); return; } case 0x14: @@ -86,7 +108,7 @@ void VU1Interpreter::execUpper(uint32_t instr) float bc = broadcast(vt, op & 3); for (int c = 0; c < 4; c++) result[c] = (vs[c] < bc) ? vs[c] : bc; - applyDest(vd, result, dest); + writeDestMasked(vd, result, dest); return; } case 0x18: @@ -97,110 +119,127 @@ void VU1Interpreter::execUpper(uint32_t instr) float bc = broadcast(vt, op & 3); for (int c = 0; c < 4; c++) result[c] = vs[c] * bc; - applyDest(vd, result, dest); + computeFmacFlags(result, dest); + writeDestMasked(vd, result, dest); return; } case 0x1C: // MULq for (int c = 0; c < 4; c++) result[c] = vs[c] * m_state.q; - applyDest(vd, result, dest); + computeFmacFlags(result, dest); + writeDestMasked(vd, result, dest); return; case 0x1D: // MAXi for (int c = 0; c < 4; c++) result[c] = (vs[c] > m_state.i) ? vs[c] : m_state.i; - applyDest(vd, result, dest); + writeDestMasked(vd, result, dest); return; case 0x1E: // MULi for (int c = 0; c < 4; c++) result[c] = vs[c] * m_state.i; - applyDest(vd, result, dest); + computeFmacFlags(result, dest); + writeDestMasked(vd, result, dest); return; case 0x1F: // MINIi for (int c = 0; c < 4; c++) result[c] = (vs[c] < m_state.i) ? vs[c] : m_state.i; - applyDest(vd, result, dest); + writeDestMasked(vd, result, dest); return; case 0x20: // ADDq for (int c = 0; c < 4; c++) result[c] = vs[c] + m_state.q; - applyDest(vd, result, dest); + computeFmacFlags(result, dest); + writeDestMasked(vd, result, dest); return; case 0x21: // MADDq for (int c = 0; c < 4; c++) result[c] = m_state.acc[c] + vs[c] * m_state.q; - applyDest(vd, result, dest); + computeFmacFlags(result, dest); + writeDestMasked(vd, result, dest); return; case 0x22: // ADDi for (int c = 0; c < 4; c++) result[c] = vs[c] + m_state.i; - applyDest(vd, result, dest); + computeFmacFlags(result, dest); + writeDestMasked(vd, result, dest); return; case 0x23: // MADDi for (int c = 0; c < 4; c++) result[c] = m_state.acc[c] + vs[c] * m_state.i; - applyDest(vd, result, dest); + computeFmacFlags(result, dest); + writeDestMasked(vd, result, dest); return; case 0x24: // SUBq for (int c = 0; c < 4; c++) result[c] = vs[c] - m_state.q; - applyDest(vd, result, dest); + computeFmacFlags(result, dest); + writeDestMasked(vd, result, dest); return; case 0x25: // MSUBq for (int c = 0; c < 4; c++) result[c] = m_state.acc[c] - vs[c] * m_state.q; - applyDest(vd, result, dest); + computeFmacFlags(result, dest); + writeDestMasked(vd, result, dest); return; case 0x26: // SUBi for (int c = 0; c < 4; c++) result[c] = vs[c] - m_state.i; - applyDest(vd, result, dest); + computeFmacFlags(result, dest); + writeDestMasked(vd, result, dest); return; case 0x27: // MSUBi for (int c = 0; c < 4; c++) result[c] = m_state.acc[c] - vs[c] * m_state.i; - applyDest(vd, result, dest); + computeFmacFlags(result, dest); + writeDestMasked(vd, result, dest); return; case 0x28: // ADD for (int c = 0; c < 4; c++) result[c] = vs[c] + vt[c]; - applyDest(vd, result, dest); + computeFmacFlags(result, dest); + writeDestMasked(vd, result, dest); return; case 0x29: // MADD for (int c = 0; c < 4; c++) result[c] = m_state.acc[c] + vs[c] * vt[c]; - applyDest(vd, result, dest); + computeFmacFlags(result, dest); + writeDestMasked(vd, result, dest); return; case 0x2A: // MUL for (int c = 0; c < 4; c++) result[c] = vs[c] * vt[c]; - applyDest(vd, result, dest); + computeFmacFlags(result, dest); + writeDestMasked(vd, result, dest); return; case 0x2B: // MAX for (int c = 0; c < 4; c++) result[c] = (vs[c] > vt[c]) ? vs[c] : vt[c]; - applyDest(vd, result, dest); + writeDestMasked(vd, result, dest); return; case 0x2C: // SUB for (int c = 0; c < 4; c++) result[c] = vs[c] - vt[c]; - applyDest(vd, result, dest); + computeFmacFlags(result, dest); + writeDestMasked(vd, result, dest); return; case 0x2D: // MSUB for (int c = 0; c < 4; c++) result[c] = m_state.acc[c] - vs[c] * vt[c]; - applyDest(vd, result, dest); + computeFmacFlags(result, dest); + writeDestMasked(vd, result, dest); return; case 0x2E: // OPMSUB result[0] = m_state.acc[0] - vs[1] * vt[2]; result[1] = m_state.acc[1] - vs[2] * vt[0]; result[2] = m_state.acc[2] - vs[0] * vt[1]; result[3] = 0.0f; - applyDest(vd, result, dest); + computeFmacFlags(result, dest); + writeDestMasked(vd, result, dest); return; case 0x2F: // MINI for (int c = 0; c < 4; c++) result[c] = (vs[c] < vt[c]) ? vs[c] : vt[c]; - applyDest(vd, result, dest); + writeDestMasked(vd, result, dest); return; // Upper special group (low op 0x3C..0x3F). @@ -268,7 +307,7 @@ void VU1Interpreter::execUpper(uint32_t instr) std::memcpy(&iv, &vs[c], 4); result[c] = static_cast(iv); } - applyDest(vtDest, result, dest); + writeDestMasked(vtDest, result, dest); return; case 0x11: // ITOF4 for (int c = 0; c < 4; c++) @@ -277,7 +316,7 @@ void VU1Interpreter::execUpper(uint32_t instr) std::memcpy(&iv, &vs[c], 4); result[c] = static_cast(iv) / 16.0f; } - applyDest(vtDest, result, dest); + writeDestMasked(vtDest, result, dest); return; case 0x12: // ITOF12 for (int c = 0; c < 4; c++) @@ -286,7 +325,7 @@ void VU1Interpreter::execUpper(uint32_t instr) std::memcpy(&iv, &vs[c], 4); result[c] = static_cast(iv) / 4096.0f; } - applyDest(vtDest, result, dest); + writeDestMasked(vtDest, result, dest); return; case 0x13: // ITOF15 for (int c = 0; c < 4; c++) @@ -295,39 +334,39 @@ void VU1Interpreter::execUpper(uint32_t instr) std::memcpy(&iv, &vs[c], 4); result[c] = static_cast(iv) / 32768.0f; } - applyDest(vtDest, result, dest); + writeDestMasked(vtDest, result, dest); return; case 0x14: // FTOI0 for (int c = 0; c < 4; c++) { - int32_t iv = static_cast(vs[c]); + int32_t iv = ftoiSaturate(vs[c]); std::memcpy(&result[c], &iv, 4); } - applyDest(vtDest, result, dest); + writeDestMasked(vtDest, result, dest); return; case 0x15: // FTOI4 for (int c = 0; c < 4; c++) { - int32_t iv = static_cast(vs[c] * 16.0f); + int32_t iv = ftoiSaturate(vs[c] * 16.0f); std::memcpy(&result[c], &iv, 4); } - applyDest(vtDest, result, dest); + writeDestMasked(vtDest, result, dest); return; case 0x16: // FTOI12 for (int c = 0; c < 4; c++) { - int32_t iv = static_cast(vs[c] * 4096.0f); + int32_t iv = ftoiSaturate(vs[c] * 4096.0f); std::memcpy(&result[c], &iv, 4); } - applyDest(vtDest, result, dest); + writeDestMasked(vtDest, result, dest); return; case 0x17: // FTOI15 for (int c = 0; c < 4; c++) { - int32_t iv = static_cast(vs[c] * 32768.0f); + int32_t iv = ftoiSaturate(vs[c] * 32768.0f); std::memcpy(&result[c], &iv, 4); } - applyDest(vtDest, result, dest); + writeDestMasked(vtDest, result, dest); return; case 0x18: case 0x19: @@ -348,7 +387,7 @@ void VU1Interpreter::execUpper(uint32_t instr) case 0x1D: // ABS for (int c = 0; c < 4; c++) result[c] = std::fabs(vs[c]); - applyDest(vtDest, result, dest); + writeDestMasked(vtDest, result, dest); return; case 0x1E: // MULAi for (int c = 0; c < 4; c++) @@ -365,7 +404,11 @@ void VU1Interpreter::execUpper(uint32_t instr) if (vs[1] < -w) flags |= 0x08; if (vs[2] > +w) flags |= 0x10; if (vs[2] < -w) flags |= 0x20; - m_state.clip = (m_state.clip << 6) | flags; + // Masked to 24 bits: sourced from PCSX2 pcsx2/VUops.cpp _vuCLIP, which ANDs the + // shift register with 0xFFFFFF after every accumulation. Without the mask, bits + // shifted past 23 persist forever and FCOR's == 0xFFFFFF check never goes true + // again once five or more CLIPs have run. + m_state.clip = ((m_state.clip << 6) | flags) & 0xFFFFFFu; return; } case 0x20: // ADDAq diff --git a/ps2xTest/src/ps2_vu1_tests.cpp b/ps2xTest/src/ps2_vu1_tests.cpp index a66da893c..f64fcd0d1 100644 --- a/ps2xTest/src/ps2_vu1_tests.cpp +++ b/ps2xTest/src/ps2_vu1_tests.cpp @@ -13,6 +13,14 @@ namespace { constexpr uint32_t kVuUpperNop = 0u; + // kVuUpperNop (raw 0) decodes as ADDbc.x vf0, vf0, vf0 with dest=0: a real FMAC + // instruction that writes no VF lanes but, once MAC/STATUS are modelled, legitimately + // clears MAC/STATUS every time it runs (dest=0 means every lane's flags are cleared, + // not skipped). Tests that check MAC/STATUS/CLIP need filler that is truly inert on the + // upper pipe: the special-group NOP at specialOp 0x2F, which returns before touching any + // VF/MAC/STATUS state. kVuUpperFmacNop is that encoding. + constexpr uint32_t kVuUpperFmacNop = (0x0Bu << 6) | 0x3Fu; // specialOp 0x2F, dest=ft=fs=0 + struct Vu1Fixture { PS2Memory mem; @@ -156,6 +164,32 @@ namespace { std::memcpy(values, data + qwordIndex * 16u, sizeof(float) * 4u); } + + // Encodes the lower-word primary-opcode space 0x10..0x1C: the flag-condition ops + // (FCEQ/FCSET/FCAND/FCOR/FSEQ/FSSET/FSAND/FSOR, which use a bare 24/12-bit immediate) + // and the flag-register ops (FMEQ/FMAND/FMOR/FCGET, which use it/is like an integer op). + // Callers pass 0 for whichever field group their instruction does not have. + uint32_t makeVuLowerOpHi(uint8_t opHi, uint8_t it, uint8_t is, uint32_t imm) + { + return (static_cast(opHi & 0x7Fu) << 25) | + (static_cast(it & 0xFu) << 16) | + (static_cast(is & 0xFu) << 11) | + (imm & 0xFFFFFFu); + } + + // Encodes the upper-word special group (raw op 0x3C..0x3F). The real selector is + // (instr & 0x3) | ((instr >> 4) & 0x7C); the FD field doubles as the top bits of that + // selector, which is why CLIP/ITOF/FTOI/ABS cannot be built with makeVuUpper. + uint32_t makeVuUpperSpecial(uint8_t specialOp, uint8_t dest, uint8_t ft, uint8_t fs) + { + const uint8_t op6 = static_cast(0x3Cu | (specialOp & 0x3u)); + const uint8_t fdField = static_cast((specialOp >> 2) & 0x1Fu); + return (static_cast(dest & 0xFu) << 21) | + (static_cast(ft & 0x1Fu) << 16) | + (static_cast(fs & 0x1Fu) << 11) | + (static_cast(fdField) << 6) | + static_cast(op6); + } } void register_ps2_vu1_tests() @@ -555,5 +589,472 @@ void register_ps2_vu1_tests() } t.IsTrue(imageOk, "MSCAL-triggered XGKICK should route PATH1 packet into GS VRAM"); }); + + tc.Run("FMAC computes MAC Z and S bits for zero and negative results", [](TestCase &t) + { + // Z case: SUB.xyzw vf3, vf1, vf1 -> all-lane zero result. + { + Vu1Fixture fx; + t.IsTrue(fx.initialize(), "VU1 fixture should initialize"); + + uint32_t pc = 0u; + writeVuInstructionPair(fx.code, pc, 0u, makeVuUpper(0x2Cu, 0xFu, 1u, 1u, 3u)); // SUB.xyzw vf3, vf1, vf1 + pc += 8u; + for (int i = 0; i < 4; ++i) + { + writeVuInstructionPair(fx.code, pc, 0u, kVuUpperFmacNop); + pc += 8u; + } + writeVuInstructionPair(fx.code, pc, makeVuLowerOpHi(0x1Au, 1u, 2u, 0u), kVuUpperFmacNop); // FMAND vi1, vi2 + pc += 8u; + + VU1Interpreter vu1; + vu1.state().vf[1][0] = 5.0f; + vu1.state().vf[1][1] = 5.0f; + vu1.state().vf[1][2] = 5.0f; + vu1.state().vf[1][3] = 5.0f; + vu1.state().vi[2] = 0xFFFF; + + vu1.execute(fx.code, PS2_VU1_CODE_SIZE, fx.data, PS2_VU1_DATA_SIZE, fx.gs, &fx.mem, 0u, 0u, 0u, pc / 8u); + + t.Equals(vu1.state().vi[1], 0xF, "FMAND after a zero FMAC result should read back the four Z bits"); + } + + // S case: SUB.xyzw vf3, vf1, vf2 with vf1 < vf2 in every lane -> all-lane negative result. + { + Vu1Fixture fx; + t.IsTrue(fx.initialize(), "VU1 fixture should initialize"); + + uint32_t pc = 0u; + writeVuInstructionPair(fx.code, pc, 0u, makeVuUpper(0x2Cu, 0xFu, 2u, 1u, 3u)); // SUB.xyzw vf3, vf1, vf2 + pc += 8u; + for (int i = 0; i < 4; ++i) + { + writeVuInstructionPair(fx.code, pc, 0u, kVuUpperFmacNop); + pc += 8u; + } + writeVuInstructionPair(fx.code, pc, makeVuLowerOpHi(0x1Au, 1u, 2u, 0u), kVuUpperFmacNop); // FMAND vi1, vi2 + pc += 8u; + + VU1Interpreter vu1; + vu1.state().vf[1][0] = 1.0f; + vu1.state().vf[1][1] = 1.0f; + vu1.state().vf[1][2] = 1.0f; + vu1.state().vf[1][3] = 1.0f; + vu1.state().vf[2][0] = 5.0f; + vu1.state().vf[2][1] = 5.0f; + vu1.state().vf[2][2] = 5.0f; + vu1.state().vf[2][3] = 5.0f; + vu1.state().vi[2] = 0xFFFF; + + vu1.execute(fx.code, PS2_VU1_CODE_SIZE, fx.data, PS2_VU1_DATA_SIZE, fx.gs, &fx.mem, 0u, 0u, 0u, pc / 8u); + + t.Equals(vu1.state().vi[1], 0xF0, "FMAND after a negative FMAC result should read back the four S bits"); + } + }); + + tc.Run("FMAC sets both MAC U and Z bits on a denormal (underflow) result", [](TestCase &t) + { + Vu1Fixture fx; + t.IsTrue(fx.initialize(), "VU1 fixture should initialize"); + + uint32_t pc = 0u; + // MUL.xyzw vf3, vf1, vf2 with vf1=vf2=1e-20 -> ~1e-40 in every lane: a positive + // denormal (exponent field 0, nonzero mantissa). PCSX2 flushes this underflow to + // zero, raising both U and Z per lane. + writeVuInstructionPair(fx.code, pc, 0u, makeVuUpper(0x2Au, 0xFu, 2u, 1u, 3u)); // MUL.xyzw vf3, vf1, vf2 + pc += 8u; + for (int i = 0; i < 4; ++i) + { + writeVuInstructionPair(fx.code, pc, 0u, kVuUpperFmacNop); + pc += 8u; + } + writeVuInstructionPair(fx.code, pc, makeVuLowerOpHi(0x1Au, 1u, 2u, 0u), kVuUpperFmacNop); // FMAND vi1, vi2 + pc += 8u; + + VU1Interpreter vu1; + const float tiny = 1e-20f; + vu1.state().vf[1][0] = tiny; + vu1.state().vf[1][1] = tiny; + vu1.state().vf[1][2] = tiny; + vu1.state().vf[1][3] = tiny; + vu1.state().vf[2][0] = tiny; + vu1.state().vf[2][1] = tiny; + vu1.state().vf[2][2] = tiny; + vu1.state().vf[2][3] = tiny; + vu1.state().vi[2] = 0xFFFF; + + vu1.execute(fx.code, PS2_VU1_CODE_SIZE, fx.data, PS2_VU1_DATA_SIZE, fx.gs, &fx.mem, 0u, 0u, 0u, pc / 8u); + + // Positive denormal in all four lanes -> U nibble 0x0F00 and Z nibble 0x000F, + // no S (positive), no O -> mac == 0x0F0F. A U-only denormal branch would read 0x0F00. + t.Equals(vu1.state().vi[1], 0x0F0F, "FMAND after a denormal FMAC result should read back both the U and Z bits"); + }); + + tc.Run("FMAC honors the DEST mask when computing MAC bits", [](TestCase &t) + { + Vu1Fixture fx; + t.IsTrue(fx.initialize(), "VU1 fixture should initialize"); + + uint32_t pc = 0u; + writeVuInstructionPair(fx.code, pc, 0u, makeVuUpper(0x2Cu, 0x8u, 2u, 1u, 3u)); // SUB.x vf3, vf1, vf2 (dest=x only) + pc += 8u; + for (int i = 0; i < 4; ++i) + { + writeVuInstructionPair(fx.code, pc, 0u, kVuUpperFmacNop); + pc += 8u; + } + writeVuInstructionPair(fx.code, pc, makeVuLowerOpHi(0x1Au, 1u, 2u, 0u), kVuUpperFmacNop); // FMAND vi1, vi2 + pc += 8u; + + VU1Interpreter vu1; + // x lane produces zero (Z bit, 0x8). y/z/w produce a NEGATIVE result (S bits 0x40/0x20/0x10) + // if wrongly included, which is a different bit pattern than the x-only Z bit, so a dest-mask + // bug that computes all four lanes is distinguishable from the correct x-only result. + vu1.state().vf[1][0] = 5.0f; + vu1.state().vf[1][1] = 1.0f; + vu1.state().vf[1][2] = 1.0f; + vu1.state().vf[1][3] = 1.0f; + vu1.state().vf[2][0] = 5.0f; + vu1.state().vf[2][1] = 5.0f; + vu1.state().vf[2][2] = 5.0f; + vu1.state().vf[2][3] = 5.0f; + vu1.state().vi[2] = 0xFFFF; + + vu1.execute(fx.code, PS2_VU1_CODE_SIZE, fx.data, PS2_VU1_DATA_SIZE, fx.gs, &fx.mem, 0u, 0u, 0u, pc / 8u); + + t.Equals(vu1.state().vi[1], 0x8, "MAC should only reflect the x lane when dest masks out y/z/w"); + }); + + tc.Run("MAX and MINI run in the FMAC pipeline but must not update MAC or STATUS", [](TestCase &t) + { + // MAX site (0x2B). + { + Vu1Fixture fx; + t.IsTrue(fx.initialize(), "VU1 fixture should initialize"); + + uint32_t pc = 0u; + // MUL.xyzw vf3, vf1, vf2 with vf1=-1, vf2=0 -> -0.0 in every lane: Z and S + // both set, mac == 0xFF. This is the pattern FMAND must still read back after + // the MAX below, if MAX correctly leaves MAC untouched. + writeVuInstructionPair(fx.code, pc, 0u, makeVuUpper(0x2Au, 0xFu, 2u, 1u, 3u)); + pc += 8u; + // MAX.xyzw vf12, vf10, vf11 with vf10=5.0, vf11=6.0 -> 6.0 in every lane: a + // positive nonzero result that raises no MAC bits at all. If MAX wrongly + // computed flags it would clobber mac to 0x00, a different pattern than 0xFF, + // so the two outcomes are distinguishable by the read below. + writeVuInstructionPair(fx.code, pc, 0u, makeVuUpper(0x2Bu, 0xFu, 11u, 10u, 12u)); + pc += 8u; + for (int i = 0; i < 4; ++i) + { + writeVuInstructionPair(fx.code, pc, 0u, kVuUpperFmacNop); + pc += 8u; + } + writeVuInstructionPair(fx.code, pc, makeVuLowerOpHi(0x1Au, 1u, 2u, 0u), kVuUpperFmacNop); // FMAND vi1, vi2 + pc += 8u; + + VU1Interpreter vu1; + vu1.state().vf[1][0] = -1.0f; + vu1.state().vf[1][1] = -1.0f; + vu1.state().vf[1][2] = -1.0f; + vu1.state().vf[1][3] = -1.0f; + vu1.state().vf[2][0] = 0.0f; + vu1.state().vf[2][1] = 0.0f; + vu1.state().vf[2][2] = 0.0f; + vu1.state().vf[2][3] = 0.0f; + vu1.state().vf[10][0] = 5.0f; + vu1.state().vf[10][1] = 5.0f; + vu1.state().vf[10][2] = 5.0f; + vu1.state().vf[10][3] = 5.0f; + vu1.state().vf[11][0] = 6.0f; + vu1.state().vf[11][1] = 6.0f; + vu1.state().vf[11][2] = 6.0f; + vu1.state().vf[11][3] = 6.0f; + vu1.state().vi[2] = 0xFFFF; + + vu1.execute(fx.code, PS2_VU1_CODE_SIZE, fx.data, PS2_VU1_DATA_SIZE, fx.gs, &fx.mem, 0u, 0u, 0u, pc / 8u); + + t.Equals(vu1.state().vi[1], 0xFF, "MAX must not clobber MAC; FMAND should still read the producing FMAC's flags"); + } + + // MINI site (0x2F). + { + Vu1Fixture fx; + t.IsTrue(fx.initialize(), "VU1 fixture should initialize"); + + uint32_t pc = 0u; + // Same -0.0 Z+S producer as the MAX block above, mac == 0xFF. + writeVuInstructionPair(fx.code, pc, 0u, makeVuUpper(0x2Au, 0xFu, 2u, 1u, 3u)); + pc += 8u; + // MINI.xyzw vf12, vf10, vf11 with vf10=5.0, vf11=6.0 -> 5.0 in every lane: a + // positive nonzero result that raises no MAC bits, distinguishable from 0xFF + // the same way as the MAX block. + writeVuInstructionPair(fx.code, pc, 0u, makeVuUpper(0x2Fu, 0xFu, 11u, 10u, 12u)); + pc += 8u; + for (int i = 0; i < 4; ++i) + { + writeVuInstructionPair(fx.code, pc, 0u, kVuUpperFmacNop); + pc += 8u; + } + writeVuInstructionPair(fx.code, pc, makeVuLowerOpHi(0x1Au, 1u, 2u, 0u), kVuUpperFmacNop); // FMAND vi1, vi2 + pc += 8u; + + VU1Interpreter vu1; + vu1.state().vf[1][0] = -1.0f; + vu1.state().vf[1][1] = -1.0f; + vu1.state().vf[1][2] = -1.0f; + vu1.state().vf[1][3] = -1.0f; + vu1.state().vf[2][0] = 0.0f; + vu1.state().vf[2][1] = 0.0f; + vu1.state().vf[2][2] = 0.0f; + vu1.state().vf[2][3] = 0.0f; + vu1.state().vf[10][0] = 5.0f; + vu1.state().vf[10][1] = 5.0f; + vu1.state().vf[10][2] = 5.0f; + vu1.state().vf[10][3] = 5.0f; + vu1.state().vf[11][0] = 6.0f; + vu1.state().vf[11][1] = 6.0f; + vu1.state().vf[11][2] = 6.0f; + vu1.state().vf[11][3] = 6.0f; + vu1.state().vi[2] = 0xFFFF; + + vu1.execute(fx.code, PS2_VU1_CODE_SIZE, fx.data, PS2_VU1_DATA_SIZE, fx.gs, &fx.mem, 0u, 0u, 0u, pc / 8u); + + t.Equals(vu1.state().vi[1], 0xFF, "MINI must not clobber MAC; FMAND should still read the producing FMAC's flags"); + } + }); + + tc.Run("STATUS folds MAC into a live half that resets and a sticky half that accumulates", [](TestCase &t) + { + Vu1Fixture fx; + t.IsTrue(fx.initialize(), "VU1 fixture should initialize"); + + uint32_t pc = 0u; + // MUL.xyzw vf3, vf1, vf2 with vf1=-1, vf2=0 -> -0.0 in every lane (Z and S both set). + writeVuInstructionPair(fx.code, pc, 0u, makeVuUpper(0x2Au, 0xFu, 2u, 1u, 3u)); + pc += 8u; + // ADD.xyzw vf4, vf5, vf6 with 5+5=10 -> positive nonzero, no flags. + writeVuInstructionPair(fx.code, pc, 0u, makeVuUpper(0x28u, 0xFu, 6u, 5u, 4u)); + pc += 8u; + for (int i = 0; i < 4; ++i) + { + writeVuInstructionPair(fx.code, pc, 0u, kVuUpperFmacNop); + pc += 8u; + } + writeVuInstructionPair(fx.code, pc, makeVuLowerOpHi(0x16u, 1u, 0u, 0xFFFu), kVuUpperFmacNop); // FSAND vi1, 0xFFF + pc += 8u; + + VU1Interpreter vu1; + vu1.state().vf[1][0] = -1.0f; + vu1.state().vf[1][1] = -1.0f; + vu1.state().vf[1][2] = -1.0f; + vu1.state().vf[1][3] = -1.0f; + vu1.state().vf[2][0] = 0.0f; + vu1.state().vf[2][1] = 0.0f; + vu1.state().vf[2][2] = 0.0f; + vu1.state().vf[2][3] = 0.0f; + vu1.state().vf[5][0] = 5.0f; + vu1.state().vf[5][1] = 5.0f; + vu1.state().vf[5][2] = 5.0f; + vu1.state().vf[5][3] = 5.0f; + vu1.state().vf[6][0] = 5.0f; + vu1.state().vf[6][1] = 5.0f; + vu1.state().vf[6][2] = 5.0f; + vu1.state().vf[6][3] = 5.0f; + + vu1.execute(fx.code, PS2_VU1_CODE_SIZE, fx.data, PS2_VU1_DATA_SIZE, fx.gs, &fx.mem, 0u, 0u, 0u, pc / 8u); + + const int32_t status = vu1.state().vi[1]; + t.IsTrue((status & 0x40) != 0, "sticky Z should survive a later non-zero FMAC"); + t.IsTrue((status & 0x80) != 0, "sticky S should survive a later non-zero FMAC"); + t.IsTrue((status & 0x01) == 0, "live Z should not persist from the first FMAC once a later FMAC clears it"); + }); + + tc.Run("FSSET preserves the live half and writes the sourced immediate into the sticky half", [](TestCase &t) + { + Vu1Fixture fx; + t.IsTrue(fx.initialize(), "VU1 fixture should initialize"); + + // Sourced from PCSX2 _vuFSSET: imm12 = ((instr>>21)&1)<<11 | (instr&0x7FF). + // Target imm12 = 0xFFF, so instr bit21=1 and instr bits10:0=0x7FF -> imm24 = 0x2007FF. + writeVuInstructionPair(fx.code, 0u, makeVuLowerOpHi(0x15u, 0u, 0u, 0x2007FFu), kVuUpperFmacNop); // FSSET + + VU1Interpreter vu1; + vu1.state().status = 0x15u; // arbitrary live half, must survive FSSET untouched + + 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().status & 0x3Fu, 0x15u, "FSSET should preserve the live half"); + t.Equals(vu1.state().status & 0xFC0u, 0xFC0u, "FSSET should place the sourced immediate in the sticky half"); + }); + + tc.Run("the lower flag-read table decodes FMEQ, FMAND, FMOR, and FCGET at their correct opcodes", [](TestCase &t) + { + // FMEQ @ 0x18 + { + Vu1Fixture fx; + t.IsTrue(fx.initialize(), "VU1 fixture should initialize"); + writeVuInstructionPair(fx.code, 0u, makeVuLowerOpHi(0x18u, 1u, 2u, 0u), kVuUpperFmacNop); + VU1Interpreter vu1; + vu1.state().mac = 0x1234u; + vu1.state().vi[2] = 0x1234; + 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().vi[1], 1, "0x18 should decode FMEQ and compare equal MAC/VI values"); + } + // FMAND @ 0x1A + { + Vu1Fixture fx; + t.IsTrue(fx.initialize(), "VU1 fixture should initialize"); + writeVuInstructionPair(fx.code, 0u, makeVuLowerOpHi(0x1Au, 1u, 2u, 0u), kVuUpperFmacNop); + VU1Interpreter vu1; + vu1.state().mac = 0x0F0Fu; + vu1.state().vi[2] = 0x00FF; + 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().vi[1], 0x0F, "0x1A should decode FMAND and AND the MAC and VI values"); + } + // FMOR @ 0x1B + { + Vu1Fixture fx; + t.IsTrue(fx.initialize(), "VU1 fixture should initialize"); + writeVuInstructionPair(fx.code, 0u, makeVuLowerOpHi(0x1Bu, 1u, 2u, 0u), kVuUpperFmacNop); + VU1Interpreter vu1; + vu1.state().mac = 0x0F00u; + vu1.state().vi[2] = 0x000F; + 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().vi[1], 0x0F0F, "0x1B should decode FMOR and OR the MAC and VI values"); + } + // FCGET @ 0x1C + { + Vu1Fixture fx; + t.IsTrue(fx.initialize(), "VU1 fixture should initialize"); + writeVuInstructionPair(fx.code, 0u, makeVuLowerOpHi(0x1Cu, 1u, 0u, 0u), kVuUpperFmacNop); + VU1Interpreter vu1; + vu1.state().clip = 0xABCDEFu; + 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().vi[1], 0xDEF, "0x1C should decode FCGET and read the low 12 bits of CLIP"); + } + }); + + tc.Run("ITOF, FTOI, and ABS never feed the flag-computation path", [](TestCase &t) + { + Vu1Fixture fx; + t.IsTrue(fx.initialize(), "VU1 fixture should initialize"); + + uint32_t pc = 0u; + // MUL.xyzw vf3, vf1, vf2 with vf1=-1, vf2=0 -> -0.0 in every lane (mac becomes 0xFF for dest=xyzw). + writeVuInstructionPair(fx.code, pc, 0u, makeVuUpper(0x2Au, 0xFu, 2u, 1u, 3u)); + pc += 8u; + writeVuInstructionPair(fx.code, pc, 0u, makeVuUpperSpecial(0x10u, 0xFu, 5u, 4u)); // ITOF0.xyzw vf5, vf4 + pc += 8u; + writeVuInstructionPair(fx.code, pc, 0u, makeVuUpperSpecial(0x14u, 0xFu, 6u, 7u)); // FTOI0.xyzw vf6, vf7 + pc += 8u; + writeVuInstructionPair(fx.code, pc, 0u, makeVuUpperSpecial(0x1Du, 0xFu, 8u, 9u)); // ABS.xyzw vf8, vf9 + pc += 8u; + for (int i = 0; i < 4; ++i) + { + writeVuInstructionPair(fx.code, pc, 0u, kVuUpperFmacNop); + pc += 8u; + } + writeVuInstructionPair(fx.code, pc, makeVuLowerOpHi(0x1Au, 1u, 2u, 0u), kVuUpperFmacNop); // FMAND vi1, vi2 + pc += 8u; + + VU1Interpreter vu1; + vu1.state().vf[1][0] = -1.0f; + vu1.state().vf[1][1] = -1.0f; + vu1.state().vf[1][2] = -1.0f; + vu1.state().vf[1][3] = -1.0f; + vu1.state().vf[2][0] = 0.0f; + vu1.state().vf[2][1] = 0.0f; + vu1.state().vf[2][2] = 0.0f; + vu1.state().vf[2][3] = 0.0f; + + uint32_t intBits = 1000u; + float asFloat; + std::memcpy(&asFloat, &intBits, 4); + vu1.state().vf[4][0] = asFloat; + vu1.state().vf[4][1] = asFloat; + vu1.state().vf[4][2] = asFloat; + vu1.state().vf[4][3] = asFloat; + + vu1.state().vf[7][0] = 2.5f; + vu1.state().vf[7][1] = 2.5f; + vu1.state().vf[7][2] = 2.5f; + vu1.state().vf[7][3] = 2.5f; + + vu1.state().vf[9][0] = -3.0f; + vu1.state().vf[9][1] = -3.0f; + vu1.state().vf[9][2] = -3.0f; + vu1.state().vf[9][3] = -3.0f; + + vu1.state().vi[2] = 0xFFFF; + + vu1.execute(fx.code, PS2_VU1_CODE_SIZE, fx.data, PS2_VU1_DATA_SIZE, fx.gs, &fx.mem, 0u, 0u, 0u, pc / 8u); + + t.Equals(vu1.state().vi[1], 0xFF, "MAC must reflect only the producing FMAC, not the intervening ITOF/FTOI/ABS"); + }); + + tc.Run("CLIP accumulation is masked to 24 bits so FCOR can go true again", [](TestCase &t) + { + Vu1Fixture fx; + t.IsTrue(fx.initialize(), "VU1 fixture should initialize"); + + uint32_t pc = 0u; + for (int i = 0; i < 5; ++i) + { + writeVuInstructionPair(fx.code, pc, 0u, makeVuUpperSpecial(0x1Fu, 0u, 2u, 1u)); // CLIP vf1.xyz, vf2.w + pc += 8u; + } + writeVuInstructionPair(fx.code, pc, makeVuLowerOpHi(0x13u, 0u, 0u, 0x555555u), kVuUpperFmacNop); // FCOR vi1, 0x555555 + pc += 8u; + + VU1Interpreter vu1; + vu1.state().vf[1][0] = -2.0f; + vu1.state().vf[1][1] = -2.0f; + vu1.state().vf[1][2] = -2.0f; + vu1.state().vf[2][3] = 1.0f; + + vu1.execute(fx.code, PS2_VU1_CODE_SIZE, fx.data, PS2_VU1_DATA_SIZE, fx.gs, &fx.mem, 0u, 0u, 0u, pc / 8u); + + t.Equals(vu1.state().vi[1], 1, "FCOR should go true again once CLIP is masked to 24 bits after five accumulations"); + }); + + tc.Run("FTOI4 saturates out-of-range floats instead of wrapping the plain int32 cast", [](TestCase &t) + { + Vu1Fixture fx; + t.IsTrue(fx.initialize(), "VU1 fixture should initialize"); + + uint32_t pc = 0u; + for (uint8_t i = 0; i < 6u; ++i) + { + writeVuInstructionPair(fx.code, pc, 0u, + makeVuUpperSpecial(0x15u, 0xFu, static_cast(20u + i), static_cast(10u + i))); + pc += 8u; + } + + VU1Interpreter vu1; + vu1.state().vf[10][0] = 1e30f; + vu1.state().vf[11][0] = -1e30f; + vu1.state().vf[12][0] = 0.5f; + vu1.state().vf[13][0] = -0.5f; + vu1.state().vf[14][0] = 0.0f; + const uint32_t nanBits = 0x7FC00000u; // positive quiet NaN + std::memcpy(&vu1.state().vf[15][0], &nanBits, 4); + + vu1.execute(fx.code, PS2_VU1_CODE_SIZE, fx.data, PS2_VU1_DATA_SIZE, fx.gs, &fx.mem, 0u, 0u, 0u, 6u); + + auto bitsOf = [&](int reg) -> uint32_t + { + uint32_t v; + std::memcpy(&v, &vu1.state().vf[reg][0], 4); + return v; + }; + + t.Equals(bitsOf(20), 0x7FFFFFFFu, "FTOI4 should clamp large positive overflow to INT_MAX"); + t.Equals(bitsOf(21), 0x80000000u, "FTOI4 should clamp large negative overflow to INT_MIN"); + t.Equals(static_cast(bitsOf(22)), 8, "FTOI4 should convert +0.5 scaled by 16 to 8"); + t.Equals(static_cast(bitsOf(23)), -8, "FTOI4 should convert -0.5 scaled by 16 to -8"); + t.Equals(static_cast(bitsOf(24)), 0, "FTOI4 should convert 0.0 to 0"); + t.Equals(bitsOf(25), 0x7FFFFFFFu, "FTOI4 should clamp a positive-signed NaN to INT_MAX per the sourced convention"); + }); }); }