diff --git a/ps2xRuntime/include/ps2_runtime_macros.h b/ps2xRuntime/include/ps2_runtime_macros.h index 0485d50060..3f75ef065a 100644 --- a/ps2xRuntime/include/ps2_runtime_macros.h +++ b/ps2xRuntime/include/ps2_runtime_macros.h @@ -311,6 +311,17 @@ inline float ps2_frsqrt(float a, float b) return ps2_fall_normal(a, b, r) ? r : ps2_frsqrt_slow(a, b); } +// CVT.W.S: the EE always truncates toward zero, whatever FCR31 says. A magnitude of 2^31 or more +// saturates by sign to 0x7FFFFFFF / 0x80000000. The EE has no Inf/NaN, so exponent-255 patterns are +// just large numbers and saturate the same way (as PCSX2's interpreter CVT_W does). +inline int32_t ps2_fcvt_w(float a) +{ + const uint32_t bits = std::bit_cast(a); + if ((bits & PS2_FLOAT_EXP_MASK) <= 0x4E800000u) // exponent <= 157: |a| < 2^31 + return static_cast(a); + return (bits & PS2_FLOAT_SIGN_MASK) ? INT32_MIN : INT32_MAX; +} + enum class Ps2VecOp { Add, @@ -853,7 +864,7 @@ inline __m128i ps2_u64_to_epi64_pair(uint64_t value) #define FPU_FLOOR_W_S(a) ((int32_t)floorf((float)(a))) #define FPU_CVT_S_W(a) ((float)(int32_t)(a)) #define FPU_CVT_S_L(a) ((float)(int64_t)(a)) -#define FPU_CVT_W_S(a) ((int32_t)nearbyintf((float)(a))) +#define FPU_CVT_W_S(a) ps2_fcvt_w((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))) diff --git a/ps2xTest/src/ps2_float_semantics_tests.cpp b/ps2xTest/src/ps2_float_semantics_tests.cpp index 2866d2bff3..9db40a33e8 100644 --- a/ps2xTest/src/ps2_float_semantics_tests.cpp +++ b/ps2xTest/src/ps2_float_semantics_tests.cpp @@ -224,5 +224,24 @@ void register_ps2_float_semantics_tests() t.IsTrue(rsqrt.find("ctx->f[2] = FPU_RSQRT_S(ctx->f[3], ctx->f[4])") != std::string::npos, "RSQRT.S should compute fs / sqrt(ft) but got: " + rsqrt); }); + + tc.Run("FPU CVT.W.S truncates toward zero and saturates by sign", [](TestCase &t) + { + const auto cvt = [](float f) { return static_cast(FPU_CVT_W_S(f)); }; + // 2.5 and 3.7 differ between round-to-nearest (2/4) and truncation (2/3). + t.Equals(cvt(3.7f), 3u, "3.7 should truncate to 3"); + t.Equals(cvt(-3.7f), static_cast(-3), "-3.7 should truncate toward zero to -3"); + t.Equals(cvt(0.999f), 0u, "0.999 should truncate to 0"); + // A volume scale like 0.9 * 0x3FFF = 14744.7 truncates to 14744, not 14745. + t.Equals(cvt(FPU_MUL_S(0.9f, 16383.0f)), 14744u, "0.9 * 0x3FFF should truncate to 14744"); + t.Equals(cvt(floatOf(0x4EFFFFFFu)), 0x7FFFFF80u, "the largest float below 2^31 should convert exactly"); + t.Equals(cvt(2147483648.0f), 0x7FFFFFFFu, "2^31 should saturate to 0x7FFFFFFF"); + t.Equals(cvt(-2147483648.0f), 0x80000000u, "-2^31 should give 0x80000000"); + t.Equals(cvt(1.0e20f), 0x7FFFFFFFu, "1e20 should saturate to 0x7FFFFFFF"); + t.Equals(cvt(-1.0e20f), 0x80000000u, "-1e20 should saturate to 0x80000000"); + t.Equals(cvt(floatOf(0x7FFFFFFFu)), 0x7FFFFFFFu, "+max (a NaN pattern on the host) should saturate to 0x7FFFFFFF"); + t.Equals(cvt(floatOf(0xFFC00000u)), 0x80000000u, "a negative exponent-255 pattern should saturate to 0x80000000"); + t.Equals(cvt(floatOf(0x00000001u)), 0u, "a denormal should convert to 0"); + }); }); }