Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions ps2xRecomp/src/lib/fpu_translator.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
12 changes: 6 additions & 6 deletions ps2xRecomp/src/lib/vu_translation_helpers.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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,
Expand Down Expand Up @@ -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,
Expand All @@ -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,
Expand All @@ -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,
Expand Down Expand Up @@ -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,
Expand Down
83 changes: 69 additions & 14 deletions ps2xRuntime/include/ps2_runtime_macros.h
Original file line number Diff line number Diff line change
Expand Up @@ -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<uint32_t>(a);
if ((bits & PS2_FLOAT_EXP_MASK) == 0u)
return 0;
const int32_t magnitude = static_cast<int32_t>(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<uint32_t>(a);
return (bits & PS2_FLOAT_EXP_MASK) == 0u ? std::bit_cast<float>(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,
Expand Down Expand Up @@ -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.
Expand Down
7 changes: 7 additions & 0 deletions ps2xTest/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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()
2 changes: 2 additions & 0 deletions ps2xTest/src/main.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand All @@ -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();
Expand Down
157 changes: 157 additions & 0 deletions ps2xTest/src/ps2_fpu_order_tests.cpp
Original file line number Diff line number Diff line change
@@ -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 <bit>
#include <cstdint>
#include <cstdio>
#include <string>

// 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<float>(static_cast<uint32_t>(stored));
}

uint32_t b(float value)
{
return std::bit_cast<uint32_t>(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<int>(w), static_cast<int>(z), static_cast<int>(y), static_cast<int>(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);
});
});
}