diff --git a/ps2xRecomp/src/lib/fpu_translator.cpp b/ps2xRecomp/src/lib/fpu_translator.cpp index 435db3aa9..dcb05b1de 100644 --- a/ps2xRecomp/src/lib/fpu_translator.cpp +++ b/ps2xRecomp/src/lib/fpu_translator.cpp @@ -93,9 +93,10 @@ namespace ps2recomp case COP1_S_MSUBA: return fmt::format("FPU_SET_ACC(ctx, FPU_SUB_S(ctx->f_acc, FPU_MUL_S(ctx->f[{}], ctx->f[{}])));", fs, ft); case COP1_S_MAX: - return fmt::format("ctx->f[{}] = std::max(ctx->f[{}], ctx->f[{}]);", fd, fs, ft); + // PS2 numbers are ordered sign-magnitude (no NaN): see ps2_fmax. + return fmt::format("ctx->f[{}] = FPU_MAX_S(ctx->f[{}], ctx->f[{}]);", fd, fs, ft); case COP1_S_MIN: - return fmt::format("ctx->f[{}] = std::min(ctx->f[{}], ctx->f[{}]);", fd, fs, ft); + return fmt::format("ctx->f[{}] = FPU_MIN_S(ctx->f[{}], ctx->f[{}]);", fd, fs, ft); case COP1_S_C_F: return fmt::format("ctx->fcr31 &= ~0x800000;"); case COP1_S_C_UN: diff --git a/ps2xRecomp/src/lib/vu_translation_helpers.cpp b/ps2xRecomp/src/lib/vu_translation_helpers.cpp index 509fd07b0..e21eac7ee 100644 --- a/ps2xRecomp/src/lib/vu_translation_helpers.cpp +++ b/ps2xRecomp/src/lib/vu_translation_helpers.cpp @@ -246,7 +246,7 @@ namespace ps2recomp std::string shuffle_pattern = fmt::format("_MM_SHUFFLE({},{},{},{})", field, field, field, field); - return fmt::format("{{ __m128 res = _mm_min_ps(ctx->vu0_vf[{}], _mm_shuffle_ps(ctx->vu0_vf[{}], ctx->vu0_vf[{}], {})); " + return fmt::format("{{ __m128 res = ps2_vu_min_ps(ctx->vu0_vf[{}], _mm_shuffle_ps(ctx->vu0_vf[{}], ctx->vu0_vf[{}], {})); " "__m128i mask = _mm_set_epi32({}, {}, {}, {}); " "ctx->vu0_vf[{}] = _mm_blendv_ps(ctx->vu0_vf[{}], res, _mm_castsi128_ps(mask)); }}", vfs, vft, vft, shuffle_pattern, @@ -265,7 +265,7 @@ namespace ps2recomp std::string shuffle_pattern = fmt::format("_MM_SHUFFLE({},{},{},{})", field, field, field, field); - return fmt::format("{{ __m128 res = _mm_max_ps(ctx->vu0_vf[{}], _mm_shuffle_ps(ctx->vu0_vf[{}], ctx->vu0_vf[{}], {})); " + return fmt::format("{{ __m128 res = ps2_vu_max_ps(ctx->vu0_vf[{}], _mm_shuffle_ps(ctx->vu0_vf[{}], ctx->vu0_vf[{}], {})); " "__m128i mask = _mm_set_epi32({}, {}, {}, {}); " "ctx->vu0_vf[{}] = _mm_blendv_ps(ctx->vu0_vf[{}], res, _mm_castsi128_ps(mask)); }}", vfs, vft, vft, shuffle_pattern, @@ -326,7 +326,7 @@ namespace ps2recomp uint8_t vfs = inst.rd; uint8_t vft = inst.rt; uint8_t dest_mask = inst.vectorInfo.vectorField; - return fmt::format("{{ __m128 res = _mm_max_ps(ctx->vu0_vf[{}], ctx->vu0_vf[{}]); " + return fmt::format("{{ __m128 res = ps2_vu_max_ps(ctx->vu0_vf[{}], ctx->vu0_vf[{}]); " "__m128i mask = _mm_set_epi32({}, {}, {}, {}); " "ctx->vu0_vf[{}] = _mm_blendv_ps(ctx->vu0_vf[{}], res, _mm_castsi128_ps(mask)); }}", vfs, vft, @@ -340,7 +340,7 @@ namespace ps2recomp uint8_t vfd = inst.sa; uint8_t vfs = inst.rd; uint8_t dest_mask = inst.vectorInfo.vectorField; - return fmt::format("{{ __m128 res = _mm_max_ps(ctx->vu0_vf[{}], _mm_set1_ps(ctx->vu0_i)); " + return fmt::format("{{ __m128 res = ps2_vu_max_ps(ctx->vu0_vf[{}], _mm_set1_ps(ctx->vu0_i)); " "__m128i mask = _mm_set_epi32({}, {}, {}, {}); " "ctx->vu0_vf[{}] = _mm_blendv_ps(ctx->vu0_vf[{}], res, _mm_castsi128_ps(mask)); }}", vfs, @@ -354,7 +354,7 @@ namespace ps2recomp uint8_t vfd = inst.sa; uint8_t vfs = inst.rd; uint8_t dest_mask = inst.vectorInfo.vectorField; - return fmt::format("{{ __m128 res = _mm_min_ps(ctx->vu0_vf[{}], _mm_set1_ps(ctx->vu0_i)); " + return fmt::format("{{ __m128 res = ps2_vu_min_ps(ctx->vu0_vf[{}], _mm_set1_ps(ctx->vu0_i)); " "__m128i mask = _mm_set_epi32({}, {}, {}, {}); " "ctx->vu0_vf[{}] = _mm_blendv_ps(ctx->vu0_vf[{}], res, _mm_castsi128_ps(mask)); }}", vfs, @@ -459,7 +459,7 @@ namespace ps2recomp uint8_t vfs = inst.rd; uint8_t vft = inst.rt; uint8_t dest_mask = inst.vectorInfo.vectorField; - return fmt::format("{{ __m128 res = _mm_min_ps(ctx->vu0_vf[{}], ctx->vu0_vf[{}]); " + return fmt::format("{{ __m128 res = ps2_vu_min_ps(ctx->vu0_vf[{}], ctx->vu0_vf[{}]); " "__m128i mask = _mm_set_epi32({}, {}, {}, {}); " "ctx->vu0_vf[{}] = _mm_blendv_ps(ctx->vu0_vf[{}], res, _mm_castsi128_ps(mask)); }}", vfs, vft, diff --git a/ps2xRuntime/include/ps2_runtime_macros.h b/ps2xRuntime/include/ps2_runtime_macros.h index 0485d5006..22f204cea 100644 --- a/ps2xRuntime/include/ps2_runtime_macros.h +++ b/ps2xRuntime/include/ps2_runtime_macros.h @@ -311,6 +311,56 @@ inline float ps2_frsqrt(float a, float b) return ps2_fall_normal(a, b, r) ? r : ps2_frsqrt_slow(a, b); } +// FPU comparisons, MIN.S and MAX.S order PS2 floats as sign-magnitude numbers. The EE has no NaN +// or Inf: an exponent-255 pattern (what ps2_fdiv returns for x / 0, 0x7FFFFFFF) is the largest +// magnitude, and any exponent-0 pattern is zero (the FPU flushes denormals). Host float compares +// would see a NaN and answer false, so a clamp such as min(x, limit) would let x through. +inline int32_t ps2_fkey(float a) +{ + const uint32_t bits = std::bit_cast(a); + if ((bits & PS2_FLOAT_EXP_MASK) == 0u) + return 0; + const int32_t magnitude = static_cast(bits & ~PS2_FLOAT_SIGN_MASK); + return (bits & PS2_FLOAT_SIGN_MASK) ? -magnitude : magnitude; +} + +// The selected operand, with an exponent-0 pattern flushed to a signed zero (PCSX2 MAX_S/MIN_S +// return fpuDouble of it). On a tie the first operand wins, as std::max/std::min do. +inline float ps2_fflush(float a) +{ + const uint32_t bits = std::bit_cast(a); + return (bits & PS2_FLOAT_EXP_MASK) == 0u ? std::bit_cast(bits & PS2_FLOAT_SIGN_MASK) : a; +} + +inline float ps2_fmax(float a, float b) +{ + return ps2_fflush(ps2_fkey(a) < ps2_fkey(b) ? b : a); +} + +inline float ps2_fmin(float a, float b) +{ + return ps2_fflush(ps2_fkey(b) < ps2_fkey(a) ? b : a); +} + +// VU MAX/MINI compare the raw lanes as integers, the larger magnitude winning among two negative +// values (PCSX2 VUops.cpp fp_max/fp_min). Exponent-255 lanes are numbers here too; nothing is +// flushed. +inline __m128 ps2_vu_max_ps(__m128 a, __m128 b) +{ + const __m128i ai = _mm_castps_si128(a); + const __m128i bi = _mm_castps_si128(b); + const __m128i bothNegative = _mm_srai_epi32(_mm_and_si128(ai, bi), 31); + return _mm_castsi128_ps(_mm_blendv_epi8(_mm_max_epi32(ai, bi), _mm_min_epi32(ai, bi), bothNegative)); +} + +inline __m128 ps2_vu_min_ps(__m128 a, __m128 b) +{ + const __m128i ai = _mm_castps_si128(a); + const __m128i bi = _mm_castps_si128(b); + const __m128i bothNegative = _mm_srai_epi32(_mm_and_si128(ai, bi), 31); + return _mm_castsi128_ps(_mm_blendv_epi8(_mm_min_epi32(ai, bi), _mm_max_epi32(ai, bi), bothNegative)); +} + enum class Ps2VecOp { Add, @@ -856,21 +906,26 @@ inline __m128i ps2_u64_to_epi64_pair(uint64_t value) #define FPU_CVT_W_S(a) ((int32_t)nearbyintf((float)(a))) #define FPU_CVT_L_S(a) ((int64_t)(float)(a)) #define FPU_C_F_S(a, b) (0) -#define FPU_C_UN_S(a, b) (isnan((float)(a)) || isnan((float)(b))) -#define FPU_C_EQ_S(a, b) ((float)(a) == (float)(b)) -#define FPU_C_UEQ_S(a, b) ((float)(a) == (float)(b) || isnan((float)(a)) || isnan((float)(b))) -#define FPU_C_OLT_S(a, b) ((float)(a) < (float)(b)) -#define FPU_C_ULT_S(a, b) ((float)(a) < (float)(b) || isnan((float)(a)) || isnan((float)(b))) -#define FPU_C_OLE_S(a, b) ((float)(a) <= (float)(b)) -#define FPU_C_ULE_S(a, b) ((float)(a) <= (float)(b) || isnan((float)(a)) || isnan((float)(b))) +// The R5900 FPU implements only C.F, C.EQ, C.LT and C.LE, on the function codes MIPS gives C.F, +// C.EQ, C.OLT and C.OLE, so the decoder names a title's c.lt.s / c.le.s FPU_C_OLT_S / FPU_C_OLE_S. +// Its numbers are always ordered (ps2_fkey), so the "unordered" forms reduce to those four. +#define FPU_C_UN_S(a, b) (0) +#define FPU_C_EQ_S(a, b) (ps2_fkey((float)(a)) == ps2_fkey((float)(b))) +#define FPU_C_UEQ_S(a, b) FPU_C_EQ_S(a, b) +#define FPU_C_OLT_S(a, b) FPU_C_LT_S(a, b) +#define FPU_C_ULT_S(a, b) FPU_C_LT_S(a, b) +#define FPU_C_OLE_S(a, b) FPU_C_LE_S(a, b) +#define FPU_C_ULE_S(a, b) FPU_C_LE_S(a, b) #define FPU_C_SF_S(a, b) (0) -#define FPU_C_NGLE_S(a, b) (isnan((float)(a)) || isnan((float)(b))) -#define FPU_C_SEQ_S(a, b) ((float)(a) == (float)(b)) -#define FPU_C_NGL_S(a, b) ((float)(a) == (float)(b) || isnan((float)(a)) || isnan((float)(b))) -#define FPU_C_LT_S(a, b) ((float)(a) < (float)(b)) -#define FPU_C_NGE_S(a, b) ((float)(a) < (float)(b) || isnan((float)(a)) || isnan((float)(b))) -#define FPU_C_LE_S(a, b) ((float)(a) <= (float)(b)) -#define FPU_C_NGT_S(a, b) ((float)(a) <= (float)(b) || isnan((float)(a)) || isnan((float)(b))) +#define FPU_C_NGLE_S(a, b) (0) +#define FPU_C_SEQ_S(a, b) FPU_C_EQ_S(a, b) +#define FPU_C_NGL_S(a, b) FPU_C_EQ_S(a, b) +#define FPU_C_LT_S(a, b) (ps2_fkey((float)(a)) < ps2_fkey((float)(b))) +#define FPU_C_NGE_S(a, b) FPU_C_LT_S(a, b) +#define FPU_C_LE_S(a, b) (ps2_fkey((float)(a)) <= ps2_fkey((float)(b))) +#define FPU_C_NGT_S(a, b) FPU_C_LE_S(a, b) +#define FPU_MAX_S(a, b) ps2_fmax((float)(a), (float)(b)) +#define FPU_MIN_S(a, b) ps2_fmin((float)(a), (float)(b)) // QFSRV: Quadword Funnel Shift Right Variable // Concatenates rs || rt (256 bits) and right-shifts by SA bits, taking lower 128 bits. diff --git a/ps2xTest/CMakeLists.txt b/ps2xTest/CMakeLists.txt index 3eaa8b3d2..c64442338 100644 --- a/ps2xTest/CMakeLists.txt +++ b/ps2xTest/CMakeLists.txt @@ -38,6 +38,7 @@ add_library(ps2_test_lib STATIC src/ps2_recompiler_tests.cpp src/ps2_runtime_expansion_tests.cpp src/ps2_float_semantics_tests.cpp + src/ps2_fpu_order_tests.cpp ) target_include_directories(ps2_test_lib PRIVATE @@ -104,3 +105,9 @@ if(CMAKE_BUILD_TYPE STREQUAL "Release" OR CMAKE_BUILD_TYPE STREQUAL "RelWithDebI EnableFastReleaseMode(ps2_test_lib) EnableFastReleaseMode(ps2x_tests) endif() + +# The PS2 float-ordering cases compile like the port's generated code (/fp:precise), not with the +# /fp:fast that EnableFastReleaseMode gives this library: host float compares are what they guard against. +if(MSVC) + set_source_files_properties(src/ps2_fpu_order_tests.cpp PROPERTIES COMPILE_OPTIONS "/fp:precise") +endif() diff --git a/ps2xTest/src/main.cpp b/ps2xTest/src/main.cpp index 7b7889148..1f127b4ad 100644 --- a/ps2xTest/src/main.cpp +++ b/ps2xTest/src/main.cpp @@ -19,6 +19,7 @@ void register_ps2_sif_dma_tests(); void register_ps2_recompiler_tests(); void register_ps2_runtime_expansion_tests(); void register_ps2_float_semantics_tests(); +void register_ps2_fpu_order_tests(); void reset_ps2_test_function_table(); int main() @@ -42,6 +43,7 @@ int main() register_ps2_recompiler_tests(); register_ps2_runtime_expansion_tests(); register_ps2_float_semantics_tests(); + register_ps2_fpu_order_tests(); int res = MiniTest::Run(); std::cout.flush(); std::cerr.flush(); diff --git a/ps2xTest/src/ps2_fpu_order_tests.cpp b/ps2xTest/src/ps2_fpu_order_tests.cpp new file mode 100644 index 000000000..c194db851 --- /dev/null +++ b/ps2xTest/src/ps2_fpu_order_tests.cpp @@ -0,0 +1,157 @@ +#include "MiniTest.h" +#include "ps2recomp/code_generator.h" +#include "ps2recomp/instructions.h" +#include "ps2recomp/r5900_decoder.h" +#include "ps2recomp/types.h" +#include "ps2_runtime_macros.h" + +#include +#include +#include +#include + +// PS2 floats have no NaN or Inf. Exponent 255 is the largest magnitude: 0x7FFFFFFF is what ps2_fdiv +// gives for x / 0 and what overflow saturates to. Exponent 0 is zero whatever the mantissa. The +// FPU's C.EQ/C.LT/C.LE, MIN.S and MAX.S and the VU's MAX/MINI order values that way (PCSX2 FPU.cpp +// fpuDouble, VUops.cpp fp_max/fp_min). Values are built from bit patterns at run time so the +// compiler cannot fold them. + +using namespace ps2recomp; + +namespace +{ + constexpr uint32_t kPlusMax = 0x7FFFFFFFu; + constexpr uint32_t kMinusMax = 0xFFFFFFFFu; + constexpr uint32_t kPlusE255 = 0x7F800001u; // a smaller exponent-255 number + constexpr uint32_t kDenormal = 0x00000001u; + constexpr uint32_t kMinusDenormal = 0x80000001u; + + float f(uint32_t bits) + { + volatile uint32_t stored = bits; + return std::bit_cast(static_cast(stored)); + } + + uint32_t b(float value) + { + return std::bit_cast(value); + } + + std::string hex(uint32_t value) + { + char text[12]; + std::snprintf(text, sizeof(text), "0x%08X", value); + return text; + } + + std::string translate(uint32_t address, uint32_t raw) + { + R5900Decoder decoder; + CodeGenerator gen({}, {}); + return gen.translateInstruction(decoder.decodeInstruction(address, raw)); + } + + bool contains(const std::string &text, const std::string &part) + { + return text.find(part) != std::string::npos; + } + + __m128 lanes(uint32_t x, uint32_t y, uint32_t z, uint32_t w) + { + return _mm_castsi128_ps(_mm_set_epi32(static_cast(w), static_cast(z), static_cast(y), static_cast(x))); + } + + void expectLanes(TestCase &t, __m128 value, uint32_t x, uint32_t y, uint32_t z, uint32_t w, const std::string &what) + { + alignas(16) uint32_t got[4]; + _mm_store_si128(reinterpret_cast<__m128i *>(got), _mm_castps_si128(value)); + t.IsTrue(got[0] == x && got[1] == y && got[2] == z && got[3] == w, + what + ": expected " + hex(x) + " " + hex(y) + " " + hex(z) + " " + hex(w) + ", got " + hex(got[0]) + + " " + hex(got[1]) + " " + hex(got[2]) + " " + hex(got[3])); + } +} + +void register_ps2_fpu_order_tests() +{ + MiniTest::Case("PS2FpuOrder", [](TestCase &tc) + { + // The R5900's C.LT.S and C.LE.S have the function codes MIPS gives C.OLT (0x34) and C.OLE + // (0x36), so the title's 1673 c.lt.s and 234 c.le.s arrive as FPU_C_OLT_S and FPU_C_OLE_S. + tc.Run("C.LT and C.LE treat exponent 255 as the largest numbers", [](TestCase &t) + { + t.IsTrue(FPU_C_OLT_S(f(0x00000000u), f(kPlusMax)), "0 < +max"); + t.IsTrue(!FPU_C_OLT_S(f(kPlusMax), f(b(1.0f))), "+max is not < 1"); + t.IsTrue(FPU_C_OLT_S(f(b(3.0e38f)), f(kPlusE255)), "3e38 < an exponent-255 number"); + t.IsTrue(FPU_C_OLT_S(f(kPlusE255), f(kPlusMax)), "exponent-255 numbers are ordered by mantissa"); + t.IsTrue(FPU_C_OLT_S(f(kMinusMax), f(b(-1.0f))), "-max < -1"); + t.IsTrue(FPU_C_OLE_S(f(kPlusMax), f(kPlusMax)), "+max <= +max"); + t.IsTrue(!FPU_C_OLE_S(f(kPlusMax), f(b(2.0f))), "+max is not <= 2"); + }); + + tc.Run("C.EQ, C.LT and C.LE treat exponent 0 as zero", [](TestCase &t) + { + t.IsTrue(FPU_C_EQ_S(f(kDenormal), f(0x00000000u)), "a denormal equals 0"); + t.IsTrue(FPU_C_EQ_S(f(kMinusDenormal), f(0x80000000u)), "-denormal equals -0"); + t.IsTrue(FPU_C_EQ_S(f(0x80000000u), f(0x00000000u)), "-0 equals +0"); + t.IsTrue(!FPU_C_OLT_S(f(0x00000000u), f(kDenormal)), "0 is not < a denormal"); + t.IsTrue(FPU_C_OLE_S(f(kDenormal), f(0x80000000u)), "a denormal <= -0"); + t.IsTrue(FPU_C_EQ_S(f(kPlusMax), f(kPlusMax)), "+max equals itself"); + }); + + tc.Run("MIN.S and MAX.S clamp a saturated value", [](TestCase &t) + { + t.Equals(b(FPU_MIN_S(f(kPlusMax), f(b(2.0f)))), b(2.0f), "min(+max, 2) = 2"); + t.Equals(b(FPU_MAX_S(f(b(1.0f)), f(kPlusMax))), kPlusMax, "max(1, +max) = +max"); + t.Equals(b(FPU_MAX_S(f(kMinusMax), f(b(-1.0f)))), b(-1.0f), "max(-max, -1) = -1"); + t.Equals(b(FPU_MIN_S(f(b(-1.0f)), f(kMinusMax))), kMinusMax, "min(-1, -max) = -max"); + t.Equals(b(FPU_MAX_S(f(kDenormal), f(b(-1.0f)))), 0x00000000u, "max(denormal, -1) = +0 (flushed)"); + t.Equals(b(FPU_MIN_S(f(0x00000000u), f(0x80000000u))), 0x00000000u, "a tie keeps the first operand"); + }); + + tc.Run("Stuntman's launch clamp survives the car record's zero divisor", [](TestCase &t) + { + // sub_1A93C0, 0x001A94FC-0x001A9518 clamps x / c to [0, 1], where c is +0x2C of the car + // record (0x0026C6D8), 0.0 for the first mission's car. x / 0 is +-max on the EE and the + // clamp must turn it into 1 or 0. Host min/max passed the +-max through as a NaN, so the + // clamp produced garbage that the driven-wheel code then integrated (speedometer 999). + const float divisor = f(0x00000000u); // record +0x2C + const float one = f(b(1.0f)); // $f5 + const float floor = f(0x00000000u); // $f6 + const auto clamp = [&](float x) + { + return FPU_MAX_S(FPU_MIN_S(FPU_DIV_S(x, divisor), one), floor); + }; + t.Equals(b(clamp(f(b(1.0f)))), b(1.0f), "a positive x / 0 clamps to 1"); + t.Equals(b(clamp(f(b(-19.0f)))), b(0.0f), "a negative x / 0 clamps to 0"); + // The title's words at 0x001A94FC, 0x001A9504 and 0x001A9508. + const std::string div = translate(0x001A94FCu, 0x46090003u); + t.IsTrue(contains(div, "ctx->f[0] = FPU_DIV_S(ctx->f[0], ctx->f[9])"), "div.s should use FPU_DIV_S but got: " + div); + const std::string minCode = translate(0x001A9504u, 0x46050029u); + t.IsTrue(contains(minCode, "ctx->f[0] = FPU_MIN_S(ctx->f[0], ctx->f[5])"), "min.s should use FPU_MIN_S but got: " + minCode); + const std::string maxCode = translate(0x001A9508u, 0x46060028u); + t.IsTrue(contains(maxCode, "ctx->f[0] = FPU_MAX_S(ctx->f[0], ctx->f[6])"), "max.s should use FPU_MAX_S but got: " + maxCode); + }); + + tc.Run("VU MAX and MINI order lanes as PS2 numbers", [](TestCase &t) + { + const __m128 a = lanes(b(-1.0f), b(-2.0f), kPlusMax, b(1.0f)); + const __m128 c = lanes(b(-2.0f), b(-1.0f), b(1.0f), kMinusMax); + expectLanes(t, ps2_vu_max_ps(a, c), b(-1.0f), b(-1.0f), kPlusMax, b(1.0f), "MAX"); + // Operands swapped for MINI, so that neither SSE NaN rule (the second operand wins) can pass it. + expectLanes(t, ps2_vu_min_ps(c, a), b(-2.0f), b(-2.0f), b(1.0f), kMinusMax, "MINI"); + }); + + tc.Run("VU MAX and MINI emitters use the PS2 ordering", [](TestCase &t) + { + // vmax.xyzw $vf3, $vf4, $vf5 and vmini.xyzw $vf3, $vf4, $vf5 + const uint32_t vmax = (OPCODE_COP2 << 26) | (1u << 25) | (0xFu << 21) | (5u << 16) | (4u << 11) | (3u << 6) | 0x2Bu; + const uint32_t vmini = (OPCODE_COP2 << 26) | (1u << 25) | (0xFu << 21) | (5u << 16) | (4u << 11) | (3u << 6) | 0x2Fu; + const std::string maxCode = translate(0x00200000u, vmax); + const std::string minCode = translate(0x00200000u, vmini); + t.IsTrue(contains(maxCode, "ps2_vu_max_ps(ctx->vu0_vf[4], ctx->vu0_vf[5])") && !contains(maxCode, "_mm_max_ps"), + "vmax should use ps2_vu_max_ps but got: " + maxCode); + t.IsTrue(contains(minCode, "ps2_vu_min_ps(ctx->vu0_vf[4], ctx->vu0_vf[5])") && !contains(minCode, "_mm_min_ps"), + "vmini should use ps2_vu_min_ps but got: " + minCode); + }); + }); +}