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"); + }); }); }