From 2e91a2f6bddcf0b80d257725876f53c5a0e8b7f9 Mon Sep 17 00:00:00 2001 From: drako <98249188+drakolordx7@users.noreply.github.com> Date: Wed, 30 Sep 2026 18:19:13 -0500 Subject: [PATCH 1/2] Emulate PS2 float semantics for EE FPU results and DIV.S/RSQRT.S The EE FPU and the VUs have no Inf/NaN. A value with exponent 255 acts like a huge normal number, an overflowing result saturates to +-max (0x7F7FFFFF) and x/0 gives +-max with the sign of (fs xor ft). The generated code followed IEEE instead, so Inf/NaN leaked into later maths (a normalisation of a zero-length vector, for example). - ps2_runtime_macros.h: add ps2_fclamp/ps2_fdiv/ps2_frsqrt/ps2_vclamp. FPU_ADD_S/SUB_S/MUL_S clamp their inputs and result, FPU_DIV_S returns +-max on a zero divisor, FPU_SQRT_S takes sqrt(|x|), and PS2_VADD/VSUB/ VMUL clamp every lane. FPU_RSQRT_S is new. - fpu_translator.cpp: DIV.S no longer emits copysignf(INFINITY, ...) and wraps the DZ flag update and the division in one block. RSQRT.S was emitted as 1/sqrt(fs); the EE computes fs / sqrt(ft). Found while recompiling Killzone (SCUS-97402): RSQRT.S appears in 382 functions there (vector normalisation), and with the wrong operands the normals and lighting came out wrong. Verified with the new unit tests (edge values for each macro, and the translated DIV.S/RSQRT.S code with distinct fd/fs/ft), and by running Killzone with the change. Generated code must be regenerated and rebuilt to pick it up. CVT.W.S and the VU0 macro VDIV/VSQRT/VRSQRT Q-register operations are left to #215 and #198, which cover them. Made by drakolord and assisted with Claude Code. --- ps2xRecomp/src/lib/fpu_translator.cpp | 11 +- ps2xRuntime/include/ps2_runtime_macros.h | 67 +++++++-- ps2xTest/CMakeLists.txt | 1 + ps2xTest/src/main.cpp | 2 + ps2xTest/src/ps2_float_semantics_tests.cpp | 157 +++++++++++++++++++++ 5 files changed, 225 insertions(+), 13 deletions(-) create mode 100644 ps2xTest/src/ps2_float_semantics_tests.cpp diff --git a/ps2xRecomp/src/lib/fpu_translator.cpp b/ps2xRecomp/src/lib/fpu_translator.cpp index 944cee796..435db3aa9 100644 --- a/ps2xRecomp/src/lib/fpu_translator.cpp +++ b/ps2xRecomp/src/lib/fpu_translator.cpp @@ -53,10 +53,10 @@ namespace ps2recomp case COP1_S_MUL: return fmt::format("ctx->f[{}] = FPU_MUL_S(ctx->f[{}], ctx->f[{}]);", fd, fs, ft); case COP1_S_DIV: - return fmt::format("if (ctx->f[{}] == 0.0f) {{ ctx->fcr31 |= 0x100000; /* DZ flag */ " - "ctx->f[{}] = copysignf(INFINITY, ctx->f[{}] * 0.0f); }} " - "else ctx->f[{}] = ctx->f[{}] / ctx->f[{}];", - ft, fd, fs, fd, fs, ft); + // The EE has no Inf/NaN: x/0 gives +-max (sign of fs xor ft), see ps2_fdiv. + return fmt::format("{{ if (ctx->f[{}] == 0.0f) ctx->fcr31 |= 0x100000; /* DZ flag */ " + "ctx->f[{}] = FPU_DIV_S(ctx->f[{}], ctx->f[{}]); }}", + ft, fd, fs, ft); case COP1_S_SQRT: return fmt::format("ctx->f[{}] = FPU_SQRT_S(ctx->f[{}]);", fd, fs); case COP1_S_ABS: @@ -76,7 +76,8 @@ namespace ps2recomp case COP1_S_CVT_W: return fmt::format("{{ int32_t tmp = FPU_CVT_W_S(ctx->f[{}]); std::memcpy(&ctx->f[{}], &tmp, sizeof(tmp)); }}", fs, fd); case COP1_S_RSQRT: - return fmt::format("ctx->f[{}] = 1.0f / sqrtf(ctx->f[{}]);", fd, fs); + // RSQRT.S fd, fs, ft computes fs / sqrt(ft), not 1 / sqrt(fs). + return fmt::format("ctx->f[{}] = FPU_RSQRT_S(ctx->f[{}], ctx->f[{}]);", fd, fs, ft); case COP1_S_ADDA: return fmt::format("FPU_SET_ACC(ctx, FPU_ADD_S(ctx->f[{}], ctx->f[{}]));", fs, ft); case COP1_S_SUBA: diff --git a/ps2xRuntime/include/ps2_runtime_macros.h b/ps2xRuntime/include/ps2_runtime_macros.h index 0f19c9f24..cbcf9690e 100644 --- a/ps2xRuntime/include/ps2_runtime_macros.h +++ b/ps2xRuntime/include/ps2_runtime_macros.h @@ -140,10 +140,60 @@ static inline uint32_t ps2_plzcw32(uint32_t x) #define PS2_PXOR(a, b) _mm_xor_si128((__m128i)(a), (__m128i)(b)) #define PS2_PNOR(a, b) _mm_xor_si128(_mm_or_si128((__m128i)(a), (__m128i)(b)), _mm_set1_epi32(0xFFFFFFFF)) +// PS2 float semantics (EE FPU and VU). The PS2 has no Inf/NaN: a value with exponent 255 behaves like a huge +// normal number, an overflowing result saturates to +-max (0x7F7FFFFF) and x/0 gives +-max with the sign of +// (numerator xor denominator). Hosts that follow IEEE produce Inf/NaN here, which then poisons later maths +// (e.g. a normalisation of a zero-length vector). +inline float ps2_fclamp(float x) +{ + uint32_t b; + std::memcpy(&b, &x, sizeof(b)); + if ((b & 0x7F800000u) == 0x7F800000u) + b = (b & 0x80000000u) | 0x7F7FFFFFu; + std::memcpy(&x, &b, sizeof(b)); + return x; +} + +// Sign is sign(a) xor sign(b); used for a division by zero. +inline float ps2_fmax_signed(float a, float b) +{ + uint32_t ua, ub; + std::memcpy(&ua, &a, sizeof(ua)); + std::memcpy(&ub, &b, sizeof(ub)); + const uint32_t r = ((ua ^ ub) & 0x80000000u) | 0x7F7FFFFFu; + float f; + std::memcpy(&f, &r, sizeof(f)); + return f; +} + +inline float ps2_fdiv(float a, float b) +{ + a = ps2_fclamp(a); + b = ps2_fclamp(b); + return (b == 0.0f) ? ps2_fmax_signed(a, b) : ps2_fclamp(a / b); +} + +// fs / sqrt(|ft|); the radicand's sign is ignored. +inline float ps2_frsqrt(float a, float b) +{ + a = ps2_fclamp(a); + b = ps2_fclamp(b); + return (b == 0.0f) ? ps2_fmax_signed(a, b) : ps2_fclamp(a / std::sqrt(std::fabs(b))); +} + +inline __m128 ps2_vclamp(__m128 v) +{ + const __m128i bits = _mm_castps_si128(v); + const __m128i expMask = _mm_set1_epi32(0x7F800000); + const __m128i special = _mm_cmpeq_epi32(_mm_and_si128(bits, expMask), expMask); + const __m128i saturated = _mm_or_si128(_mm_and_si128(bits, _mm_set1_epi32((int)0x80000000)), _mm_set1_epi32(0x7F7FFFFF)); + return _mm_castsi128_ps(_mm_or_si128(_mm_and_si128(special, saturated), _mm_andnot_si128(special, bits))); +} + // PS2 VU (Vector Unit) operations -#define PS2_VADD(a, b) _mm_add_ps((__m128)(a), (__m128)(b)) -#define PS2_VSUB(a, b) _mm_sub_ps((__m128)(a), (__m128)(b)) -#define PS2_VMUL(a, b) _mm_mul_ps((__m128)(a), (__m128)(b)) +#define PS2_VADD(a, b) ps2_vclamp(_mm_add_ps(ps2_vclamp((__m128)(a)), ps2_vclamp((__m128)(b)))) +#define PS2_VSUB(a, b) ps2_vclamp(_mm_sub_ps(ps2_vclamp((__m128)(a)), ps2_vclamp((__m128)(b)))) +#define PS2_VMUL(a, b) ps2_vclamp(_mm_mul_ps(ps2_vclamp((__m128)(a)), ps2_vclamp((__m128)(b)))) #define PS2_VDIV(a, b) _mm_div_ps((__m128)(a), (__m128)(b)) #define PS2_VMULQ(a, q) _mm_mul_ps((__m128)(a), _mm_set1_ps(q)) #define PS2_VBLEND(a, b, mask) PS2_BLENDV_PS((__m128)(a), (__m128)(b), (__m128)(mask)) @@ -605,11 +655,12 @@ inline __m128i ps2_u64_to_epi64_pair(uint64_t value) // FPU (COP1) operations #define FPU_SET_ACC(ctx, res) (ctx->f_acc = res) -#define FPU_ADD_S(a, b) ((float)(a) + (float)(b)) -#define FPU_SUB_S(a, b) ((float)(a) - (float)(b)) -#define FPU_MUL_S(a, b) ((float)(a) * (float)(b)) -#define FPU_DIV_S(a, b) ((float)(a) / (float)(b)) -#define FPU_SQRT_S(a) sqrtf((float)(a)) +#define FPU_ADD_S(a, b) ps2_fclamp(ps2_fclamp((float)(a)) + ps2_fclamp((float)(b))) +#define FPU_SUB_S(a, b) ps2_fclamp(ps2_fclamp((float)(a)) - ps2_fclamp((float)(b))) +#define FPU_MUL_S(a, b) ps2_fclamp(ps2_fclamp((float)(a)) * ps2_fclamp((float)(b))) +#define FPU_DIV_S(a, b) ps2_fdiv((float)(a), (float)(b)) +#define FPU_RSQRT_S(a, b) ps2_frsqrt((float)(a), (float)(b)) +#define FPU_SQRT_S(a) std::sqrt(std::fabs(ps2_fclamp((float)(a)))) #define FPU_ABS_S(a) fabsf((float)(a)) #define FPU_MOV_S(a) ((float)(a)) #define FPU_NEG_S(a) (-(float)(a)) diff --git a/ps2xTest/CMakeLists.txt b/ps2xTest/CMakeLists.txt index 5db04b923..3eaa8b3d2 100644 --- a/ps2xTest/CMakeLists.txt +++ b/ps2xTest/CMakeLists.txt @@ -37,6 +37,7 @@ add_library(ps2_test_lib STATIC src/ps2_sif_dma_tests.cpp src/ps2_recompiler_tests.cpp src/ps2_runtime_expansion_tests.cpp + src/ps2_float_semantics_tests.cpp ) target_include_directories(ps2_test_lib PRIVATE diff --git a/ps2xTest/src/main.cpp b/ps2xTest/src/main.cpp index 40a68b16b..7b7889148 100644 --- a/ps2xTest/src/main.cpp +++ b/ps2xTest/src/main.cpp @@ -18,6 +18,7 @@ void register_ps2_sif_rpc_tests(); void register_ps2_sif_dma_tests(); void register_ps2_recompiler_tests(); void register_ps2_runtime_expansion_tests(); +void register_ps2_float_semantics_tests(); void reset_ps2_test_function_table(); int main() @@ -40,6 +41,7 @@ int main() register_ps2_sif_dma_tests(); register_ps2_recompiler_tests(); register_ps2_runtime_expansion_tests(); + register_ps2_float_semantics_tests(); int res = MiniTest::Run(); std::cout.flush(); std::cerr.flush(); diff --git a/ps2xTest/src/ps2_float_semantics_tests.cpp b/ps2xTest/src/ps2_float_semantics_tests.cpp new file mode 100644 index 000000000..adfec5852 --- /dev/null +++ b/ps2xTest/src/ps2_float_semantics_tests.cpp @@ -0,0 +1,157 @@ +#include "MiniTest.h" +#include "ps2recomp/code_generator.h" +#include "ps2recomp/instructions.h" +#include "ps2recomp/types.h" +#include "ps2_runtime_macros.h" + +#include +#include +#include +#include +#include + +using namespace ps2recomp; + +namespace +{ + constexpr uint32_t kPlusMax = 0x7F7FFFFFu; + constexpr uint32_t kMinusMax = 0xFF7FFFFFu; + + uint32_t bitsOf(float f) + { + uint32_t b; + std::memcpy(&b, &f, sizeof(b)); + return b; + } + + float floatOf(uint32_t b) + { + float f; + std::memcpy(&f, &b, sizeof(f)); + return f; + } + + Instruction makeCop1S(uint32_t function, uint8_t fd, uint8_t fs, uint8_t ft) + { + Instruction inst{}; + inst.opcode = OPCODE_COP1; + inst.rs = COP1_S; + inst.rt = ft; + inst.rd = fs; + inst.sa = fd; + inst.function = function; + return inst; + } + + uint32_t laneBits(__m128 v, int lane) + { + float lanes[4]; + _mm_storeu_ps(lanes, v); + return bitsOf(lanes[lane]); + } +} + +void register_ps2_float_semantics_tests() +{ + MiniTest::Case("PS2FloatSemantics", [](TestCase &tc) + { + tc.Run("ps2_fclamp turns Inf and NaN into +-max and leaves finite values alone", [](TestCase &t) + { + t.Equals(bitsOf(ps2_fclamp(std::numeric_limits::infinity())), kPlusMax, "+Inf should become +max"); + t.Equals(bitsOf(ps2_fclamp(-std::numeric_limits::infinity())), kMinusMax, "-Inf should become -max"); + t.Equals(bitsOf(ps2_fclamp(floatOf(0x7FC00000u))), kPlusMax, "positive NaN should become +max"); + t.Equals(bitsOf(ps2_fclamp(floatOf(0xFFC00000u))), kMinusMax, "negative NaN should become -max"); + t.Equals(bitsOf(ps2_fclamp(1.5f)), bitsOf(1.5f), "finite values should be unchanged"); + t.Equals(bitsOf(ps2_fclamp(floatOf(kPlusMax))), kPlusMax, "max should be unchanged"); + t.Equals(bitsOf(ps2_fclamp(-0.0f)), bitsOf(-0.0f), "negative zero should be unchanged"); + }); + + tc.Run("FPU ADD/SUB/MUL saturate instead of overflowing to Inf", [](TestCase &t) + { + const float maxf = floatOf(kPlusMax); + const float minf = floatOf(kMinusMax); + t.Equals(bitsOf(FPU_ADD_S(maxf, maxf)), kPlusMax, "max + max should saturate to +max"); + t.Equals(bitsOf(FPU_ADD_S(minf, minf)), kMinusMax, "-max + -max should saturate to -max"); + t.Equals(bitsOf(FPU_SUB_S(maxf, minf)), kPlusMax, "max - (-max) should saturate to +max"); + t.Equals(bitsOf(FPU_MUL_S(maxf, 2.0f)), kPlusMax, "max * 2 should saturate to +max"); + t.Equals(bitsOf(FPU_MUL_S(maxf, -2.0f)), kMinusMax, "max * -2 should saturate to -max"); + t.Equals(bitsOf(FPU_ADD_S(1.0f, 2.0f)), bitsOf(3.0f), "ordinary addition should be unchanged"); + t.Equals(bitsOf(FPU_MUL_S(1.5f, 4.0f)), bitsOf(6.0f), "ordinary multiplication should be unchanged"); + }); + + tc.Run("FPU ADD/SUB/MUL never produce NaN from Inf or NaN operands", [](TestCase &t) + { + const float inf = std::numeric_limits::infinity(); + const float nanf = floatOf(0x7FC00000u); + t.IsFalse(std::isnan(FPU_SUB_S(inf, inf)), "Inf - Inf should not be NaN"); + t.IsFalse(std::isnan(FPU_MUL_S(inf, 0.0f)), "Inf * 0 should not be NaN"); + t.Equals(bitsOf(FPU_MUL_S(inf, 0.0f)), bitsOf(0.0f), "Inf * 0 should be 0"); + t.IsFalse(std::isnan(FPU_ADD_S(nanf, 1.0f)), "NaN input should not produce NaN"); + t.IsFalse(std::isinf(FPU_ADD_S(nanf, 1.0f)), "NaN input should not leave an Inf"); + }); + + tc.Run("FPU DIV.S by zero gives +-max with the sign of fs xor ft", [](TestCase &t) + { + t.Equals(bitsOf(FPU_DIV_S(1.0f, 0.0f)), kPlusMax, "1/+0 should be +max"); + t.Equals(bitsOf(FPU_DIV_S(-1.0f, 0.0f)), kMinusMax, "-1/+0 should be -max"); + t.Equals(bitsOf(FPU_DIV_S(1.0f, -0.0f)), kMinusMax, "1/-0 should be -max"); + t.Equals(bitsOf(FPU_DIV_S(-1.0f, -0.0f)), kPlusMax, "-1/-0 should be +max"); + t.Equals(bitsOf(FPU_DIV_S(0.0f, 0.0f)), kPlusMax, "0/0 should be +max, not NaN"); + t.Equals(bitsOf(FPU_DIV_S(6.0f, 3.0f)), bitsOf(2.0f), "ordinary division should be unchanged"); + t.Equals(bitsOf(FPU_DIV_S(floatOf(kPlusMax), 0.5f)), kPlusMax, "overflowing quotient should saturate"); + }); + + tc.Run("FPU RSQRT.S computes fs / sqrt(ft)", [](TestCase &t) + { + t.Equals(bitsOf(FPU_RSQRT_S(4.0f, 16.0f)), bitsOf(1.0f), "4 / sqrt(16) should be 1"); + t.Equals(bitsOf(FPU_RSQRT_S(1.0f, 4.0f)), bitsOf(0.5f), "1 / sqrt(4) should be 0.5"); + t.Equals(bitsOf(FPU_RSQRT_S(1.0f, -4.0f)), bitsOf(0.5f), "the sign of the radicand should be ignored"); + t.Equals(bitsOf(FPU_RSQRT_S(2.0f, 0.0f)), kPlusMax, "x / sqrt(0) should be +max"); + t.Equals(bitsOf(FPU_RSQRT_S(-2.0f, 0.0f)), kMinusMax, "-x / sqrt(0) should be -max"); + }); + + tc.Run("FPU SQRT.S takes the square root of the magnitude", [](TestCase &t) + { + t.Equals(bitsOf(FPU_SQRT_S(16.0f)), bitsOf(4.0f), "sqrt(16) should be 4"); + t.Equals(bitsOf(FPU_SQRT_S(-16.0f)), bitsOf(4.0f), "sqrt(-16) should be 4, not NaN"); + }); + + tc.Run("VU vector ADD/SUB/MUL clamp every lane", [](TestCase &t) + { + // _mm_set_ps lists lanes 3..0 + const __m128 a = _mm_set_ps(1.0f, std::numeric_limits::infinity(), floatOf(kMinusMax), floatOf(kPlusMax)); + const __m128 b = _mm_set_ps(2.0f, 1.0f, floatOf(kMinusMax), floatOf(kPlusMax)); + + const __m128 sum = PS2_VADD(a, b); + t.Equals(laneBits(sum, 0), kPlusMax, "lane 0: max + max should saturate to +max"); + t.Equals(laneBits(sum, 1), kMinusMax, "lane 1: -max + -max should saturate to -max"); + t.Equals(laneBits(sum, 2), kPlusMax, "lane 2: +Inf + 1 should be +max"); + t.Equals(laneBits(sum, 3), bitsOf(3.0f), "lane 3: 1 + 2 should be 3"); + + const __m128 diff = PS2_VSUB(a, b); + t.Equals(laneBits(diff, 0), bitsOf(0.0f), "lane 0: max - max should be 0"); + t.Equals(laneBits(diff, 3), bitsOf(-1.0f), "lane 3: 1 - 2 should be -1"); + + const __m128 prod = PS2_VMUL(a, b); + t.Equals(laneBits(prod, 0), kPlusMax, "lane 0: max * max should saturate to +max"); + t.Equals(laneBits(prod, 1), kPlusMax, "lane 1: -max * -max should saturate to +max"); + t.Equals(laneBits(prod, 2), kPlusMax, "lane 2: +Inf * 1 should be +max"); + t.Equals(laneBits(prod, 3), bitsOf(2.0f), "lane 3: 1 * 2 should be 2"); + }); + + tc.Run("DIV.S and RSQRT.S translate to the PS2 semantics helpers", [](TestCase &t) + { + CodeGenerator gen({}, {}); + + // fd = f2, fs = f3, ft = f4 (all distinct so a wrong operand shows up) + const std::string div = gen.translateInstruction(makeCop1S(COP1_S_DIV, 2, 3, 4)); + t.IsTrue(div.find("ctx->f[2] = FPU_DIV_S(ctx->f[3], ctx->f[4])") != std::string::npos, + "DIV.S should write fs / ft to fd but got: " + div); + t.IsTrue(div.find("INFINITY") == std::string::npos, "DIV.S must not produce IEEE infinity but got: " + div); + + const std::string rsqrt = gen.translateInstruction(makeCop1S(COP1_S_RSQRT, 2, 3, 4)); + 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); + }); + }); +} From 27b2749d6f8af4d167888b19ced4429b9352091d Mon Sep 17 00:00:00 2001 From: drako <98249188+drakolordx7@users.noreply.github.com> Date: Tue, 13 Oct 2026 14:19:48 -0500 Subject: [PATCH 2/2] Use the PS2 float range instead of clamping to the IEEE one Review feedback on the first revision: it treated exponent 255 as an overflow and clamped operands and results to 0x7F7FFFFF, which is the largest IEEE float, not the largest PS2 float. On the PS2 exponent 255 is an ordinary exponent: 0x7F800000 is 2^128 and the maximum is 0x7FFFFFFF. - Operands with exponent 255 are no longer clamped. Whatever touches them is computed in double and converted back, so 2^127 * 2 is 2^128 and only a result of 2^129 or more saturates, to +-0x7FFFFFFF. - A zero divisor gives +-0x7FFFFFFF. An exponent-0 divisor counts as zero whatever its mantissa holds. - RSQRT.S with a zero ft takes the sign of fs alone; the first revision used fs xor ft, which is the DIV.S rule. SQRT.S of -0 is +0. - Results below 2^-126 become +-0. - ps2_fclamp, ps2_fmax_signed and ps2_vclamp are gone. The inline part of each helper is the host operation plus one range check on the result; the double path is out of line. PS2_VADD/VSUB/VMUL test the result once per vector instead of clamping both operands and the result. Not emulated: the hardware's rounding of the last mantissa bit, and an ADD.S/SUB.S/MUL.S or vector operand with exponent 0 and a non-zero mantissa (it keeps its tiny host value instead of reading as zero). Tests: the 174 hardware results for add, sub, mul, div, sqrt and rsqrt from ps2autotests (tests/cpu/ee_fpu) are now a table in the test suite. 163 match bit for bit, 10 differ in the last mantissa bit and are marked as such, 1 is the exponent-0 operand case above. ps2x_tests: 444/444. Made by drakolord and assisted with Claude Code. --- ps2xRuntime/include/ps2_runtime_macros.h | 257 +++++++++++++++++---- ps2xTest/src/ps2_float_hw_vectors.inc | 177 ++++++++++++++ ps2xTest/src/ps2_float_semantics_tests.cpp | 135 ++++++++--- 3 files changed, 498 insertions(+), 71 deletions(-) create mode 100644 ps2xTest/src/ps2_float_hw_vectors.inc diff --git a/ps2xRuntime/include/ps2_runtime_macros.h b/ps2xRuntime/include/ps2_runtime_macros.h index cbcf9690e..0485d5006 100644 --- a/ps2xRuntime/include/ps2_runtime_macros.h +++ b/ps2xRuntime/include/ps2_runtime_macros.h @@ -140,60 +140,239 @@ static inline uint32_t ps2_plzcw32(uint32_t x) #define PS2_PXOR(a, b) _mm_xor_si128((__m128i)(a), (__m128i)(b)) #define PS2_PNOR(a, b) _mm_xor_si128(_mm_or_si128((__m128i)(a), (__m128i)(b)), _mm_set1_epi32(0xFFFFFFFF)) -// PS2 float semantics (EE FPU and VU). The PS2 has no Inf/NaN: a value with exponent 255 behaves like a huge -// normal number, an overflowing result saturates to +-max (0x7F7FFFFF) and x/0 gives +-max with the sign of -// (numerator xor denominator). Hosts that follow IEEE produce Inf/NaN here, which then poisons later maths -// (e.g. a normalisation of a zero-length vector). -inline float ps2_fclamp(float x) +// PS2 float semantics (EE FPU and VU0 macro mode). The PS2 format has no Inf, NaN or denormals: +// - exponent 255 is an ordinary exponent, so 0x7F800000 is 2^128 and the largest value is 0x7FFFFFFF; +// - exponent 0 is zero whatever the mantissa holds; +// - a result of 2^129 or more saturates to +-0x7FFFFFFF, one below 2^-126 becomes +-0; +// - x/0 gives +-0x7FFFFFFF. +// A host float cannot hold the exponent-255 values, so anything that touches them is computed in double. +// That path is rare and kept out of line; the inline part is the host operation and a range check. +// One case is left to the host: ADD.S/SUB.S/MUL.S and the vector operations do not look for an operand with +// exponent 0 and a non-zero mantissa, so it takes part with its tiny host value instead of as zero. +// Checked against the hardware results in ps2autotests (tests/cpu/ee_fpu). +#if defined(_MSC_VER) +#define PS2_FLOAT_NOINLINE __declspec(noinline) +#else +#define PS2_FLOAT_NOINLINE __attribute__((noinline)) +#endif + +constexpr uint32_t PS2_FLOAT_MAX_BITS = 0x7FFFFFFFu; +constexpr uint32_t PS2_FLOAT_EXP_MASK = 0x7F800000u; +constexpr uint32_t PS2_FLOAT_SIGN_MASK = 0x80000000u; + +// True when a host float operation cannot be trusted with x: exponent 255, or exponent 0 with a mantissa. +inline bool ps2_fspecial(float x) +{ + const uint32_t t = std::bit_cast(x) & 0x7FFFFFFFu; + return t != 0u && (t - 0x00800000u) >= 0x7F000000u; +} + +inline double ps2_to_double(float x) +{ + uint32_t b = std::bit_cast(x); + const uint32_t exponent = b & PS2_FLOAT_EXP_MASK; + if (exponent == 0u) + return static_cast(std::bit_cast(b & PS2_FLOAT_SIGN_MASK)); + if (exponent == PS2_FLOAT_EXP_MASK) + return static_cast(std::bit_cast(b - 0x00800000u)) * 2.0; + return static_cast(x); +} + +inline float ps2_from_double(double d) +{ + const double magnitude = std::fabs(d); + const uint32_t sign = std::signbit(d) ? PS2_FLOAT_SIGN_MASK : 0u; + if (magnitude >= 0x1p129) + return std::bit_cast(sign | PS2_FLOAT_MAX_BITS); + if (magnitude < 0x1p-126) + return std::bit_cast(sign); + if (magnitude >= 0x1p128) + { + // Convert one exponent lower, then put the exponent back as 255. + const uint32_t b = std::bit_cast(static_cast(d * 0.5)); + if ((b & PS2_FLOAT_EXP_MASK) == PS2_FLOAT_EXP_MASK) + return std::bit_cast(sign | PS2_FLOAT_MAX_BITS); + return std::bit_cast(b + 0x00800000u); + } + // A value that rounds up to 2^128 comes out as the host's Inf pattern, which is 2^128 on the PS2. + return static_cast(d); +} + +PS2_FLOAT_NOINLINE inline float ps2_fadd_slow(float a, float b) +{ + // Zero operands and results end up here too; the host result is right for them. + const float r = a + b; + if (!(ps2_fspecial(a) | ps2_fspecial(b) | ps2_fspecial(r))) + return r; + return ps2_from_double(ps2_to_double(a) + ps2_to_double(b)); +} + +PS2_FLOAT_NOINLINE inline float ps2_fsub_slow(float a, float b) +{ + // Zero operands and results end up here too; the host result is right for them. + const float r = a - b; + if (!(ps2_fspecial(a) | ps2_fspecial(b) | ps2_fspecial(r))) + return r; + return ps2_from_double(ps2_to_double(a) - ps2_to_double(b)); +} + +PS2_FLOAT_NOINLINE inline float ps2_fmul_slow(float a, float b) +{ + // Zero operands and results end up here too; the host result is right for them. + const float r = a * b; + if (!(ps2_fspecial(a) | ps2_fspecial(b) | ps2_fspecial(r))) + return r; + return ps2_from_double(ps2_to_double(a) * ps2_to_double(b)); +} + +// A zero divisor gives +-max with the sign of fs xor ft. +PS2_FLOAT_NOINLINE inline float ps2_fdiv_slow(float a, float b) +{ + const uint32_t ua = std::bit_cast(a); + const uint32_t ub = std::bit_cast(b); + if ((ub & PS2_FLOAT_EXP_MASK) == 0u) + return std::bit_cast(((ua ^ ub) & PS2_FLOAT_SIGN_MASK) | PS2_FLOAT_MAX_BITS); + const float r = a / b; + if (!(ps2_fspecial(a) | ps2_fspecial(b) | ps2_fspecial(r))) + return r; + return ps2_from_double(ps2_to_double(a) / ps2_to_double(b)); +} + +// sqrt(|ft|); the result is never negative, sqrt(-0) is +0. +PS2_FLOAT_NOINLINE inline float ps2_fsqrt_slow(float a) +{ + if ((std::bit_cast(a) & PS2_FLOAT_EXP_MASK) == 0u) + return 0.0f; + return ps2_from_double(std::sqrt(std::fabs(ps2_to_double(a)))); +} + +// fs / sqrt(|ft|). The sign of ft is ignored everywhere, so a zero ft gives +-max with the sign of fs alone. +PS2_FLOAT_NOINLINE inline float ps2_frsqrt_slow(float a, float b) +{ + if ((std::bit_cast(b) & PS2_FLOAT_EXP_MASK) == 0u) + return std::bit_cast((std::bit_cast(a) & PS2_FLOAT_SIGN_MASK) | PS2_FLOAT_MAX_BITS); + // The root is rounded to a float before the division, as on the hardware. + const float root = ps2_from_double(std::sqrt(std::fabs(ps2_to_double(b)))); + return ps2_from_double(ps2_to_double(a) / static_cast(root)); +} + +// True when a, b and the host result r are all normal numbers, so r is also the PS2 result. +inline bool ps2_fall_normal(float a, float b, float r) +{ + // Doubling drops the sign; adding one exponent step wraps exponent 255 round to 0 next to exponent 0. + const uint32_t sa = std::bit_cast(a) * 2u + 0x01000000u; + const uint32_t sb = std::bit_cast(b) * 2u + 0x01000000u; + const uint32_t sr = std::bit_cast(r) * 2u + 0x01000000u; + uint32_t lowest = sa < sb ? sa : sb; + lowest = lowest < sr ? lowest : sr; + return lowest >= 0x02000000u; +} + +// The same test on the result alone. An exponent-255 operand always leaves exponent 255 in the host result +// of an addition, subtraction or multiplication (Inf or NaN), so this finds those operands as well. +inline bool ps2_fnormal(float r) +{ + return (std::bit_cast(r) * 2u + 0x01000000u) >= 0x02000000u; +} + +inline float ps2_fadd(float a, float b) { - uint32_t b; - std::memcpy(&b, &x, sizeof(b)); - if ((b & 0x7F800000u) == 0x7F800000u) - b = (b & 0x80000000u) | 0x7F7FFFFFu; - std::memcpy(&x, &b, sizeof(b)); - return x; + const float r = a + b; + return ps2_fnormal(r) ? r : ps2_fadd_slow(a, b); } -// Sign is sign(a) xor sign(b); used for a division by zero. -inline float ps2_fmax_signed(float a, float b) +inline float ps2_fsub(float a, float b) { - uint32_t ua, ub; - std::memcpy(&ua, &a, sizeof(ua)); - std::memcpy(&ub, &b, sizeof(ub)); - const uint32_t r = ((ua ^ ub) & 0x80000000u) | 0x7F7FFFFFu; - float f; - std::memcpy(&f, &r, sizeof(f)); - return f; + const float r = a - b; + return ps2_fnormal(r) ? r : ps2_fsub_slow(a, b); +} + +inline float ps2_fmul(float a, float b) +{ + const float r = a * b; + return ps2_fnormal(r) ? r : ps2_fmul_slow(a, b); } inline float ps2_fdiv(float a, float b) { - a = ps2_fclamp(a); - b = ps2_fclamp(b); - return (b == 0.0f) ? ps2_fmax_signed(a, b) : ps2_fclamp(a / b); + const float r = a / b; + return ps2_fall_normal(a, b, r) ? r : ps2_fdiv_slow(a, b); +} + +inline float ps2_fsqrt(float a) +{ + const float r = std::sqrt(a); + return ps2_fall_normal(a, a, r) ? r : ps2_fsqrt_slow(a); } -// fs / sqrt(|ft|); the radicand's sign is ignored. inline float ps2_frsqrt(float a, float b) { - a = ps2_fclamp(a); - b = ps2_fclamp(b); - return (b == 0.0f) ? ps2_fmax_signed(a, b) : ps2_fclamp(a / std::sqrt(std::fabs(b))); + const float r = a / std::sqrt(b); + return ps2_fall_normal(a, b, r) ? r : ps2_frsqrt_slow(a, b); +} + +enum class Ps2VecOp +{ + Add, + Sub, + Mul +}; + +// Lane by lane through the scalar helpers; only reached when a result lane has exponent 255. +PS2_FLOAT_NOINLINE inline __m128 ps2_vslow(__m128 a, __m128 b, Ps2VecOp op) +{ + alignas(16) float la[4], lb[4], lr[4]; + _mm_store_ps(la, a); + _mm_store_ps(lb, b); + for (int i = 0; i < 4; ++i) + { + switch (op) + { + case Ps2VecOp::Add: + lr[i] = ps2_fadd_slow(la[i], lb[i]); + break; + case Ps2VecOp::Sub: + lr[i] = ps2_fsub_slow(la[i], lb[i]); + break; + default: + lr[i] = ps2_fmul_slow(la[i], lb[i]); + break; + } + } + return _mm_load_ps(lr); } -inline __m128 ps2_vclamp(__m128 v) +// r is the host result of a op b. As in ps2_fnormal, testing r alone finds every lane that needs the double +// path. Results with exponent 0 are flushed to +-0. +inline __m128 ps2_vfinish(__m128 a, __m128 b, __m128 r, Ps2VecOp op) { - const __m128i bits = _mm_castps_si128(v); const __m128i expMask = _mm_set1_epi32(0x7F800000); - const __m128i special = _mm_cmpeq_epi32(_mm_and_si128(bits, expMask), expMask); - const __m128i saturated = _mm_or_si128(_mm_and_si128(bits, _mm_set1_epi32((int)0x80000000)), _mm_set1_epi32(0x7F7FFFFF)); - return _mm_castsi128_ps(_mm_or_si128(_mm_and_si128(special, saturated), _mm_andnot_si128(special, bits))); + const __m128i rexp = _mm_and_si128(_mm_castps_si128(r), expMask); + if (_mm_movemask_epi8(_mm_cmpeq_epi32(rexp, expMask)) != 0) + return ps2_vslow(a, b, op); + const __m128i flush = _mm_and_si128(_mm_cmpeq_epi32(rexp, _mm_setzero_si128()), _mm_set1_epi32(0x7FFFFFFF)); + return _mm_castsi128_ps(_mm_andnot_si128(flush, _mm_castps_si128(r))); +} + +inline __m128 ps2_vadd(__m128 a, __m128 b) +{ + return ps2_vfinish(a, b, _mm_add_ps(a, b), Ps2VecOp::Add); +} + +inline __m128 ps2_vsub(__m128 a, __m128 b) +{ + return ps2_vfinish(a, b, _mm_sub_ps(a, b), Ps2VecOp::Sub); +} + +inline __m128 ps2_vmul(__m128 a, __m128 b) +{ + return ps2_vfinish(a, b, _mm_mul_ps(a, b), Ps2VecOp::Mul); } // PS2 VU (Vector Unit) operations -#define PS2_VADD(a, b) ps2_vclamp(_mm_add_ps(ps2_vclamp((__m128)(a)), ps2_vclamp((__m128)(b)))) -#define PS2_VSUB(a, b) ps2_vclamp(_mm_sub_ps(ps2_vclamp((__m128)(a)), ps2_vclamp((__m128)(b)))) -#define PS2_VMUL(a, b) ps2_vclamp(_mm_mul_ps(ps2_vclamp((__m128)(a)), ps2_vclamp((__m128)(b)))) +#define PS2_VADD(a, b) ps2_vadd((__m128)(a), (__m128)(b)) +#define PS2_VSUB(a, b) ps2_vsub((__m128)(a), (__m128)(b)) +#define PS2_VMUL(a, b) ps2_vmul((__m128)(a), (__m128)(b)) #define PS2_VDIV(a, b) _mm_div_ps((__m128)(a), (__m128)(b)) #define PS2_VMULQ(a, q) _mm_mul_ps((__m128)(a), _mm_set1_ps(q)) #define PS2_VBLEND(a, b, mask) PS2_BLENDV_PS((__m128)(a), (__m128)(b), (__m128)(mask)) @@ -655,12 +834,12 @@ inline __m128i ps2_u64_to_epi64_pair(uint64_t value) // FPU (COP1) operations #define FPU_SET_ACC(ctx, res) (ctx->f_acc = res) -#define FPU_ADD_S(a, b) ps2_fclamp(ps2_fclamp((float)(a)) + ps2_fclamp((float)(b))) -#define FPU_SUB_S(a, b) ps2_fclamp(ps2_fclamp((float)(a)) - ps2_fclamp((float)(b))) -#define FPU_MUL_S(a, b) ps2_fclamp(ps2_fclamp((float)(a)) * ps2_fclamp((float)(b))) +#define FPU_ADD_S(a, b) ps2_fadd((float)(a), (float)(b)) +#define FPU_SUB_S(a, b) ps2_fsub((float)(a), (float)(b)) +#define FPU_MUL_S(a, b) ps2_fmul((float)(a), (float)(b)) #define FPU_DIV_S(a, b) ps2_fdiv((float)(a), (float)(b)) #define FPU_RSQRT_S(a, b) ps2_frsqrt((float)(a), (float)(b)) -#define FPU_SQRT_S(a) std::sqrt(std::fabs(ps2_fclamp((float)(a)))) +#define FPU_SQRT_S(a) ps2_fsqrt((float)(a)) #define FPU_ABS_S(a) fabsf((float)(a)) #define FPU_MOV_S(a) ((float)(a)) #define FPU_NEG_S(a) (-(float)(a)) diff --git a/ps2xTest/src/ps2_float_hw_vectors.inc b/ps2xTest/src/ps2_float_hw_vectors.inc new file mode 100644 index 000000000..1b1c5cf1c --- /dev/null +++ b/ps2xTest/src/ps2_float_hw_vectors.inc @@ -0,0 +1,177 @@ +// Results printed by a real PS2, from ps2autotests (tests/cpu/ee_fpu/*.expected, +// https://github.com/unknownbrackets/ps2autotests). Columns: operation, fs, ft, result, +// and the known difference of the row, if any (see HardwareVector in the test). +{"add", 0x00000000u, 0x00000000u, 0x00000000u, 0}, +{"add", 0x00000000u, 0x80000000u, 0x00000000u, 0}, +{"add", 0x80000000u, 0x00000000u, 0x00000000u, 0}, +{"add", 0x80000000u, 0x80000000u, 0x80000000u, 0}, +{"add", 0x00000000u, 0x3F800000u, 0x3F800000u, 0}, +{"add", 0x3F800000u, 0x3F800000u, 0x40000000u, 0}, +{"add", 0x3F800000u, 0x00000000u, 0x3F800000u, 0}, +{"add", 0x40000000u, 0x40000000u, 0x40800000u, 0}, +{"add", 0x40400000u, 0x3F800000u, 0x40800000u, 0}, +{"add", 0x40400000u, 0x40400000u, 0x40C00000u, 0}, +{"add", 0x7FFFFFFFu, 0x7FFFFFFFu, 0x7FFFFFFFu, 0}, +{"add", 0x7FFFFFFFu, 0xFFFFFFFFu, 0x00000000u, 0}, +{"add", 0xFFFFFFFFu, 0x7FFFFFFFu, 0x00000000u, 0}, +{"add", 0xFFFFFFFFu, 0xFFFFFFFFu, 0xFFFFFFFFu, 0}, +{"add", 0x7FFFFFFFu, 0x00000000u, 0x7FFFFFFFu, 0}, +{"add", 0x00000000u, 0x7FFFFFFFu, 0x7FFFFFFFu, 0}, +{"add", 0x00000000u, 0x7F800000u, 0x7F800000u, 0}, +{"add", 0x7F800000u, 0x7F800000u, 0x7FFFFFFFu, 0}, +{"add", 0xFF800000u, 0x7F800000u, 0x00000000u, 0}, +{"add", 0x3F800000u, 0xBF800000u, 0x00000000u, 0}, +{"add", 0x3FFFFFFFu, 0x3F800000u, 0x403FFFFFu, 1}, +{"add", 0x7F800001u, 0x3F800000u, 0x7F800001u, 0}, +{"add", 0x00000001u, 0x3F800000u, 0x3F800000u, 0}, +{"add", 0x3F800000u, 0x3FFFFFFFu, 0x403FFFFFu, 1}, +{"add", 0x3F800000u, 0x7F800001u, 0x7F800001u, 0}, +{"add", 0x3F800000u, 0x00000001u, 0x3F800000u, 0}, +{"add", 0x7FFFFFFFu, 0x3F800000u, 0x7FFFFFFFu, 0}, +{"add", 0xFFFFFFFFu, 0x3F800000u, 0xFFFFFFFFu, 0}, +{"add", 0x3F800000u, 0x7FFFFFFFu, 0x7FFFFFFFu, 0}, +{"add", 0x3F800000u, 0xFFFFFFFFu, 0xFFFFFFFFu, 0}, +{"add", 0x00001337u, 0xDEADBEEFu, 0xDEADBEEFu, 0}, +{"sub", 0x00000000u, 0x00000000u, 0x00000000u, 0}, +{"sub", 0x00000000u, 0x80000000u, 0x00000000u, 0}, +{"sub", 0x80000000u, 0x00000000u, 0x80000000u, 0}, +{"sub", 0x80000000u, 0x80000000u, 0x00000000u, 0}, +{"sub", 0x00000000u, 0x3F800000u, 0xBF800000u, 0}, +{"sub", 0x3F800000u, 0x3F800000u, 0x00000000u, 0}, +{"sub", 0x3F800000u, 0x00000000u, 0x3F800000u, 0}, +{"sub", 0x40000000u, 0x40000000u, 0x00000000u, 0}, +{"sub", 0x40400000u, 0x3F800000u, 0x40000000u, 0}, +{"sub", 0x40400000u, 0x40400000u, 0x00000000u, 0}, +{"sub", 0x7FFFFFFFu, 0x7FFFFFFFu, 0x00000000u, 0}, +{"sub", 0x7FFFFFFFu, 0xFFFFFFFFu, 0x7FFFFFFFu, 0}, +{"sub", 0xFFFFFFFFu, 0x7FFFFFFFu, 0xFFFFFFFFu, 0}, +{"sub", 0xFFFFFFFFu, 0xFFFFFFFFu, 0x00000000u, 0}, +{"sub", 0x7FFFFFFFu, 0x00000000u, 0x7FFFFFFFu, 0}, +{"sub", 0x00000000u, 0x7FFFFFFFu, 0xFFFFFFFFu, 0}, +{"sub", 0x00000000u, 0x7F800000u, 0xFF800000u, 0}, +{"sub", 0x7F800000u, 0x7F800000u, 0x00000000u, 0}, +{"sub", 0xFF800000u, 0x7F800000u, 0xFFFFFFFFu, 0}, +{"sub", 0x3F800000u, 0xBF800000u, 0x40000000u, 0}, +{"sub", 0x3FFFFFFFu, 0x3F800000u, 0x3F7FFFFEu, 0}, +{"sub", 0x7F800001u, 0x3F800000u, 0x7F800001u, 0}, +{"sub", 0x00000001u, 0x3F800000u, 0xBF800000u, 0}, +{"sub", 0x3F800000u, 0x3FFFFFFFu, 0xBF7FFFFEu, 0}, +{"sub", 0x3F800000u, 0x7F800001u, 0xFF800001u, 0}, +{"sub", 0x3F800000u, 0x00000001u, 0x3F800000u, 0}, +{"sub", 0x7FFFFFFFu, 0x3F800000u, 0x7FFFFFFFu, 0}, +{"sub", 0xFFFFFFFFu, 0x3F800000u, 0xFFFFFFFFu, 0}, +{"sub", 0x3F800000u, 0x7FFFFFFFu, 0xFFFFFFFFu, 0}, +{"sub", 0x3F800000u, 0xFFFFFFFFu, 0x7FFFFFFFu, 0}, +{"sub", 0x00001337u, 0xDEADBEEFu, 0x5EADBEEFu, 0}, +{"mul", 0x00000000u, 0x00000000u, 0x00000000u, 0}, +{"mul", 0x00000000u, 0x80000000u, 0x80000000u, 0}, +{"mul", 0x80000000u, 0x00000000u, 0x80000000u, 0}, +{"mul", 0x80000000u, 0x80000000u, 0x00000000u, 0}, +{"mul", 0x00000000u, 0x3F800000u, 0x00000000u, 0}, +{"mul", 0x3F800000u, 0x3F800000u, 0x3F800000u, 0}, +{"mul", 0x3F800000u, 0x00000000u, 0x00000000u, 0}, +{"mul", 0x40000000u, 0x40000000u, 0x40800000u, 0}, +{"mul", 0x40400000u, 0x3F800000u, 0x40400000u, 0}, +{"mul", 0x40400000u, 0x40400000u, 0x41100000u, 0}, +{"mul", 0x7FFFFFFFu, 0x7FFFFFFFu, 0x7FFFFFFFu, 0}, +{"mul", 0x7FFFFFFFu, 0xFFFFFFFFu, 0xFFFFFFFFu, 0}, +{"mul", 0xFFFFFFFFu, 0x7FFFFFFFu, 0xFFFFFFFFu, 0}, +{"mul", 0xFFFFFFFFu, 0xFFFFFFFFu, 0x7FFFFFFFu, 0}, +{"mul", 0x7FFFFFFFu, 0x00000000u, 0x00000000u, 0}, +{"mul", 0x00000000u, 0x7FFFFFFFu, 0x00000000u, 0}, +{"mul", 0x00000000u, 0x7F800000u, 0x00000000u, 0}, +{"mul", 0x7F800000u, 0x7F800000u, 0x7FFFFFFFu, 0}, +{"mul", 0xFF800000u, 0x7F800000u, 0xFFFFFFFFu, 0}, +{"mul", 0x3F800000u, 0xBF800000u, 0xBF800000u, 0}, +{"mul", 0x3FFFFFFFu, 0x3F800000u, 0x3FFFFFFFu, 0}, +{"mul", 0x7F800001u, 0x3F800000u, 0x7F800001u, 0}, +{"mul", 0x00000001u, 0x3F800000u, 0x00000000u, 0}, +{"mul", 0x3F800000u, 0x3FFFFFFFu, 0x3FFFFFFEu, 1}, +{"mul", 0x3F800000u, 0x7F800001u, 0x7F800001u, 0}, +{"mul", 0x3F800000u, 0x00000001u, 0x00000000u, 0}, +{"mul", 0x7FFFFFFFu, 0x3F800000u, 0x7FFFFFFFu, 0}, +{"mul", 0xFFFFFFFFu, 0x3F800000u, 0xFFFFFFFFu, 0}, +{"mul", 0x3F800000u, 0x7FFFFFFFu, 0x7FFFFFFEu, 1}, +{"mul", 0x3F800000u, 0xFFFFFFFFu, 0xFFFFFFFEu, 1}, +{"mul", 0x00001337u, 0xDEADBEEFu, 0x80000000u, 2}, +{"div", 0x00000000u, 0x00000000u, 0x7FFFFFFFu, 0}, +{"div", 0x00000000u, 0x80000000u, 0xFFFFFFFFu, 0}, +{"div", 0x80000000u, 0x00000000u, 0xFFFFFFFFu, 0}, +{"div", 0x80000000u, 0x80000000u, 0x7FFFFFFFu, 0}, +{"div", 0x00000000u, 0x3F800000u, 0x00000000u, 0}, +{"div", 0x3F800000u, 0x3F800000u, 0x3F800000u, 0}, +{"div", 0x3F800000u, 0x00000000u, 0x7FFFFFFFu, 0}, +{"div", 0x40000000u, 0x40000000u, 0x3F800000u, 0}, +{"div", 0x40400000u, 0x3F800000u, 0x40400000u, 0}, +{"div", 0x40400000u, 0x40400000u, 0x3F800000u, 0}, +{"div", 0x7FFFFFFFu, 0x7FFFFFFFu, 0x3F800000u, 0}, +{"div", 0x7FFFFFFFu, 0xFFFFFFFFu, 0xBF800000u, 0}, +{"div", 0xFFFFFFFFu, 0x7FFFFFFFu, 0xBF800000u, 0}, +{"div", 0xFFFFFFFFu, 0xFFFFFFFFu, 0x3F800000u, 0}, +{"div", 0x7FFFFFFFu, 0x00000000u, 0x7FFFFFFFu, 0}, +{"div", 0x00000000u, 0x7FFFFFFFu, 0x00000000u, 0}, +{"div", 0x00000000u, 0x7F800000u, 0x00000000u, 0}, +{"div", 0x7F800000u, 0x7F800000u, 0x3F800000u, 0}, +{"div", 0xFF800000u, 0x7F800000u, 0xBF800000u, 0}, +{"div", 0x3F800000u, 0xBF800000u, 0xBF800000u, 0}, +{"div", 0x3FFFFFFFu, 0x3F800000u, 0x3FFFFFFFu, 0}, +{"div", 0x7F800001u, 0x3F800000u, 0x7F800001u, 0}, +{"div", 0x00000001u, 0x3F800000u, 0x00000000u, 0}, +{"div", 0x3F800000u, 0x3FFFFFFFu, 0x3F000001u, 0}, +{"div", 0x3F800000u, 0x7F800001u, 0x00000000u, 0}, +{"div", 0x3F800000u, 0x00000001u, 0x7FFFFFFFu, 0}, +{"div", 0x7FFFFFFFu, 0x3F800000u, 0x7FFFFFFFu, 0}, +{"div", 0xFFFFFFFFu, 0x3F800000u, 0xFFFFFFFFu, 0}, +{"div", 0x3F800000u, 0x7FFFFFFFu, 0x00000000u, 0}, +{"div", 0x3F800000u, 0xFFFFFFFFu, 0x80000000u, 0}, +{"div", 0x00001337u, 0xDEADBEEFu, 0x80000000u, 0}, +{"sqrt", 0x00000000u, 0x00000000u, 0x00000000u, 0}, +{"sqrt", 0x80000000u, 0x00000000u, 0x00000000u, 0}, +{"sqrt", 0x3F800000u, 0x00000000u, 0x3F800000u, 0}, +{"sqrt", 0x40000000u, 0x00000000u, 0x3FB504F3u, 0}, +{"sqrt", 0x40400000u, 0x00000000u, 0x3FDDB3D7u, 0}, +{"sqrt", 0x7FFFFFFFu, 0x00000000u, 0x5FB504F3u, 0}, +{"sqrt", 0xFFFFFFFFu, 0x00000000u, 0x5FB504F3u, 0}, +{"sqrt", 0x7F800000u, 0x00000000u, 0x5F800000u, 0}, +{"sqrt", 0xFF800000u, 0x00000000u, 0x5F800000u, 0}, +{"sqrt", 0x41C80000u, 0x00000000u, 0x40A00000u, 0}, +{"sqrt", 0xBF800000u, 0x00000000u, 0x3F800000u, 0}, +{"sqrt", 0x3FFFFFFFu, 0x00000000u, 0x3FB504F3u, 0}, +{"sqrt", 0x7F800001u, 0x00000000u, 0x5F800000u, 0}, +{"sqrt", 0x00000001u, 0x00000000u, 0x00000000u, 0}, +{"sqrt", 0x00001337u, 0x00000000u, 0x00000000u, 0}, +{"sqrt", 0xDEADBEEFu, 0x00000000u, 0x4F152108u, 0}, +{"rsqrt", 0x00000000u, 0x00000000u, 0x7FFFFFFFu, 0}, +{"rsqrt", 0x00000000u, 0x80000000u, 0x7FFFFFFFu, 0}, +{"rsqrt", 0x80000000u, 0x00000000u, 0xFFFFFFFFu, 0}, +{"rsqrt", 0x80000000u, 0x80000000u, 0xFFFFFFFFu, 0}, +{"rsqrt", 0x00000000u, 0x3F800000u, 0x00000000u, 0}, +{"rsqrt", 0x3F800000u, 0x3F800000u, 0x3F800000u, 0}, +{"rsqrt", 0x3F800000u, 0x00000000u, 0x7FFFFFFFu, 0}, +{"rsqrt", 0x40000000u, 0x40000000u, 0x3FB504F3u, 0}, +{"rsqrt", 0x40400000u, 0x3F800000u, 0x40400000u, 0}, +{"rsqrt", 0x40400000u, 0x40400000u, 0x3FDDB3D7u, 1}, +{"rsqrt", 0x7FFFFFFFu, 0x7FFFFFFFu, 0x5FB504F2u, 1}, +{"rsqrt", 0x7FFFFFFFu, 0xFFFFFFFFu, 0x5FB504F2u, 1}, +{"rsqrt", 0xFFFFFFFFu, 0x7FFFFFFFu, 0xDFB504F2u, 1}, +{"rsqrt", 0xFFFFFFFFu, 0xFFFFFFFFu, 0xDFB504F2u, 1}, +{"rsqrt", 0x7FFFFFFFu, 0x00000000u, 0x7FFFFFFFu, 0}, +{"rsqrt", 0x00000000u, 0x7FFFFFFFu, 0x00000000u, 0}, +{"rsqrt", 0x00000000u, 0x7F800000u, 0x00000000u, 0}, +{"rsqrt", 0x7F800000u, 0x7F800000u, 0x5F800000u, 0}, +{"rsqrt", 0xFF800000u, 0x7F800000u, 0xDF800000u, 0}, +{"rsqrt", 0x3F800000u, 0x41C80000u, 0x3E4CCCCDu, 0}, +{"rsqrt", 0x40A00000u, 0x41C80000u, 0x3F800000u, 0}, +{"rsqrt", 0x41C80000u, 0x41C80000u, 0x40A00000u, 0}, +{"rsqrt", 0x3F800000u, 0xBF800000u, 0x3F800000u, 0}, +{"rsqrt", 0x3FFFFFFFu, 0x3F800000u, 0x3FFFFFFFu, 0}, +{"rsqrt", 0x7F800001u, 0x3F800000u, 0x7F800001u, 0}, +{"rsqrt", 0x00000001u, 0x3F800000u, 0x00000000u, 0}, +{"rsqrt", 0x3F800000u, 0x3FFFFFFFu, 0x3F3504F3u, 0}, +{"rsqrt", 0x3F800000u, 0x7F800001u, 0x1F800000u, 0}, +{"rsqrt", 0x3F800000u, 0x00000001u, 0x7FFFFFFFu, 0}, +{"rsqrt", 0x7FFFFFFFu, 0x3F800000u, 0x7FFFFFFFu, 0}, +{"rsqrt", 0xFFFFFFFFu, 0x3F800000u, 0xFFFFFFFFu, 0}, +{"rsqrt", 0x3F800000u, 0x7FFFFFFFu, 0x1F3504F3u, 0}, +{"rsqrt", 0x3F800000u, 0xFFFFFFFFu, 0x1F3504F3u, 0}, +{"rsqrt", 0x00001337u, 0xDEADBEEFu, 0x00000000u, 0}, diff --git a/ps2xTest/src/ps2_float_semantics_tests.cpp b/ps2xTest/src/ps2_float_semantics_tests.cpp index adfec5852..2866d2bff 100644 --- a/ps2xTest/src/ps2_float_semantics_tests.cpp +++ b/ps2xTest/src/ps2_float_semantics_tests.cpp @@ -6,6 +6,7 @@ #include #include +#include #include #include #include @@ -14,8 +15,10 @@ using namespace ps2recomp; namespace { - constexpr uint32_t kPlusMax = 0x7F7FFFFFu; - constexpr uint32_t kMinusMax = 0xFF7FFFFFu; + constexpr uint32_t kPlusMax = 0x7FFFFFFFu; + constexpr uint32_t kMinusMax = 0xFFFFFFFFu; + // 2^128: the host's +Inf pattern, an ordinary number on the PS2. + constexpr uint32_t kTwoPow128 = 0x7F800000u; uint32_t bitsOf(float f) { @@ -49,24 +52,87 @@ namespace _mm_storeu_ps(lanes, v); return bitsOf(lanes[lane]); } + + struct HardwareVector + { + const char *op; + uint32_t fs; + uint32_t ft; + uint32_t result; + // 0: must match exactly. + // 1: the hardware does not round to nearest (ADD.S and MUL.S drop the low bits, the divide unit has its + // own rule); rounding is not emulated, so the row differs in the last mantissa bit. + // 2: an ADD.S/SUB.S/MUL.S operand has exponent 0 and a non-zero mantissa. The hardware reads it as + // zero; the fast path does not look for it. Not checked. + int known; + }; + + const HardwareVector kHardwareVectors[] = { +#include "ps2_float_hw_vectors.inc" + }; + + uint32_t runHardwareOp(const HardwareVector &v) + { + const float fs = floatOf(v.fs); + const float ft = floatOf(v.ft); + const std::string op = v.op; + if (op == "add") + return bitsOf(FPU_ADD_S(fs, ft)); + if (op == "sub") + return bitsOf(FPU_SUB_S(fs, ft)); + if (op == "mul") + return bitsOf(FPU_MUL_S(fs, ft)); + if (op == "div") + return bitsOf(FPU_DIV_S(fs, ft)); + if (op == "sqrt") + return bitsOf(FPU_SQRT_S(fs)); + return bitsOf(FPU_RSQRT_S(fs, ft)); + } + + std::string describe(const HardwareVector &v, uint32_t got) + { + char text[160]; + std::snprintf(text, sizeof(text), "%s %08x, %08x: PS2 gives %08x, got %08x", v.op, v.fs, v.ft, v.result, got); + return text; + } } void register_ps2_float_semantics_tests() { MiniTest::Case("PS2FloatSemantics", [](TestCase &tc) { - tc.Run("ps2_fclamp turns Inf and NaN into +-max and leaves finite values alone", [](TestCase &t) + tc.Run("FPU results match the PS2 hardware results from ps2autotests", [](TestCase &t) { - t.Equals(bitsOf(ps2_fclamp(std::numeric_limits::infinity())), kPlusMax, "+Inf should become +max"); - t.Equals(bitsOf(ps2_fclamp(-std::numeric_limits::infinity())), kMinusMax, "-Inf should become -max"); - t.Equals(bitsOf(ps2_fclamp(floatOf(0x7FC00000u))), kPlusMax, "positive NaN should become +max"); - t.Equals(bitsOf(ps2_fclamp(floatOf(0xFFC00000u))), kMinusMax, "negative NaN should become -max"); - t.Equals(bitsOf(ps2_fclamp(1.5f)), bitsOf(1.5f), "finite values should be unchanged"); - t.Equals(bitsOf(ps2_fclamp(floatOf(kPlusMax))), kPlusMax, "max should be unchanged"); - t.Equals(bitsOf(ps2_fclamp(-0.0f)), bitsOf(-0.0f), "negative zero should be unchanged"); + for (const HardwareVector &v : kHardwareVectors) + { + if (v.known == 2) + continue; + const uint32_t got = runHardwareOp(v); + if (v.known == 1) + { + const uint32_t distance = got > v.result ? got - v.result : v.result - got; + t.IsTrue(distance == 1u, describe(v, got)); + } + else + { + t.Equals(got, v.result, describe(v, got)); + } + } }); - tc.Run("FPU ADD/SUB/MUL saturate instead of overflowing to Inf", [](TestCase &t) + tc.Run("exponent 255 is an ordinary exponent, not Inf or NaN", [](TestCase &t) + { + const float twoPow128 = floatOf(kTwoPow128); + t.Equals(bitsOf(FPU_ADD_S(twoPow128, 0.0f)), kTwoPow128, "2^128 + 0 should stay 2^128"); + t.Equals(bitsOf(FPU_MUL_S(twoPow128, 0.5f)), 0x7F000000u, "2^128 * 0.5 should be 2^127"); + t.Equals(bitsOf(FPU_MUL_S(floatOf(0x7F000000u), 2.0f)), kTwoPow128, "2^127 * 2 should be 2^128, not saturated"); + t.Equals(bitsOf(FPU_ADD_S(floatOf(0x7F000000u), floatOf(0x7F000000u))), kTwoPow128, "2^127 + 2^127 should be 2^128"); + t.Equals(bitsOf(FPU_SUB_S(floatOf(0x7FC00000u), twoPow128)), 0x7F000000u, "1.5*2^128 - 2^128 should be 2^127"); + t.Equals(bitsOf(FPU_DIV_S(floatOf(0x7FC00000u), 3.0f)), 0x7F000000u, "1.5*2^128 / 3 should be 2^127"); + t.Equals(bitsOf(FPU_SQRT_S(twoPow128)), 0x5F800000u, "sqrt(2^128) should be 2^64"); + }); + + tc.Run("FPU results saturate at 0x7FFFFFFF and flush below 2^-126", [](TestCase &t) { const float maxf = floatOf(kPlusMax); const float minf = floatOf(kMinusMax); @@ -75,68 +141,73 @@ void register_ps2_float_semantics_tests() t.Equals(bitsOf(FPU_SUB_S(maxf, minf)), kPlusMax, "max - (-max) should saturate to +max"); t.Equals(bitsOf(FPU_MUL_S(maxf, 2.0f)), kPlusMax, "max * 2 should saturate to +max"); t.Equals(bitsOf(FPU_MUL_S(maxf, -2.0f)), kMinusMax, "max * -2 should saturate to -max"); + t.Equals(bitsOf(FPU_DIV_S(maxf, 0.5f)), kPlusMax, "max / 0.5 should saturate to +max"); + t.Equals(bitsOf(FPU_MUL_S(floatOf(0x00800000u), 0.5f)), 0x00000000u, "2^-126 * 0.5 should flush to +0"); + t.Equals(bitsOf(FPU_MUL_S(floatOf(0x80800000u), 0.5f)), 0x80000000u, "-2^-126 * 0.5 should flush to -0"); + t.Equals(bitsOf(FPU_MUL_S(floatOf(0x00000001u), 1.0f)), 0x00000000u, "a result with exponent 0 should flush to +0"); t.Equals(bitsOf(FPU_ADD_S(1.0f, 2.0f)), bitsOf(3.0f), "ordinary addition should be unchanged"); t.Equals(bitsOf(FPU_MUL_S(1.5f, 4.0f)), bitsOf(6.0f), "ordinary multiplication should be unchanged"); }); - tc.Run("FPU ADD/SUB/MUL never produce NaN from Inf or NaN operands", [](TestCase &t) - { - const float inf = std::numeric_limits::infinity(); - const float nanf = floatOf(0x7FC00000u); - t.IsFalse(std::isnan(FPU_SUB_S(inf, inf)), "Inf - Inf should not be NaN"); - t.IsFalse(std::isnan(FPU_MUL_S(inf, 0.0f)), "Inf * 0 should not be NaN"); - t.Equals(bitsOf(FPU_MUL_S(inf, 0.0f)), bitsOf(0.0f), "Inf * 0 should be 0"); - t.IsFalse(std::isnan(FPU_ADD_S(nanf, 1.0f)), "NaN input should not produce NaN"); - t.IsFalse(std::isinf(FPU_ADD_S(nanf, 1.0f)), "NaN input should not leave an Inf"); - }); - tc.Run("FPU DIV.S by zero gives +-max with the sign of fs xor ft", [](TestCase &t) { t.Equals(bitsOf(FPU_DIV_S(1.0f, 0.0f)), kPlusMax, "1/+0 should be +max"); t.Equals(bitsOf(FPU_DIV_S(-1.0f, 0.0f)), kMinusMax, "-1/+0 should be -max"); t.Equals(bitsOf(FPU_DIV_S(1.0f, -0.0f)), kMinusMax, "1/-0 should be -max"); t.Equals(bitsOf(FPU_DIV_S(-1.0f, -0.0f)), kPlusMax, "-1/-0 should be +max"); - t.Equals(bitsOf(FPU_DIV_S(0.0f, 0.0f)), kPlusMax, "0/0 should be +max, not NaN"); + t.Equals(bitsOf(FPU_DIV_S(0.0f, 0.0f)), kPlusMax, "0/0 should be +max"); + t.Equals(bitsOf(FPU_DIV_S(1.0f, floatOf(0x00000001u))), kPlusMax, "an exponent-0 divisor should count as zero"); t.Equals(bitsOf(FPU_DIV_S(6.0f, 3.0f)), bitsOf(2.0f), "ordinary division should be unchanged"); - t.Equals(bitsOf(FPU_DIV_S(floatOf(kPlusMax), 0.5f)), kPlusMax, "overflowing quotient should saturate"); }); - tc.Run("FPU RSQRT.S computes fs / sqrt(ft)", [](TestCase &t) + tc.Run("FPU RSQRT.S computes fs / sqrt(|ft|) and takes the sign of fs alone on a zero ft", [](TestCase &t) { t.Equals(bitsOf(FPU_RSQRT_S(4.0f, 16.0f)), bitsOf(1.0f), "4 / sqrt(16) should be 1"); t.Equals(bitsOf(FPU_RSQRT_S(1.0f, 4.0f)), bitsOf(0.5f), "1 / sqrt(4) should be 0.5"); t.Equals(bitsOf(FPU_RSQRT_S(1.0f, -4.0f)), bitsOf(0.5f), "the sign of the radicand should be ignored"); - t.Equals(bitsOf(FPU_RSQRT_S(2.0f, 0.0f)), kPlusMax, "x / sqrt(0) should be +max"); - t.Equals(bitsOf(FPU_RSQRT_S(-2.0f, 0.0f)), kMinusMax, "-x / sqrt(0) should be -max"); + t.Equals(bitsOf(FPU_RSQRT_S(2.0f, 0.0f)), kPlusMax, "x / sqrt(+0) should be +max"); + t.Equals(bitsOf(FPU_RSQRT_S(2.0f, -0.0f)), kPlusMax, "x / sqrt(-0) should be +max: the sign of ft is ignored"); + t.Equals(bitsOf(FPU_RSQRT_S(-2.0f, 0.0f)), kMinusMax, "-x / sqrt(+0) should be -max"); + t.Equals(bitsOf(FPU_RSQRT_S(-2.0f, -0.0f)), kMinusMax, "-x / sqrt(-0) should be -max"); }); tc.Run("FPU SQRT.S takes the square root of the magnitude", [](TestCase &t) { t.Equals(bitsOf(FPU_SQRT_S(16.0f)), bitsOf(4.0f), "sqrt(16) should be 4"); - t.Equals(bitsOf(FPU_SQRT_S(-16.0f)), bitsOf(4.0f), "sqrt(-16) should be 4, not NaN"); + t.Equals(bitsOf(FPU_SQRT_S(-16.0f)), bitsOf(4.0f), "sqrt(-16) should be 4"); + t.Equals(bitsOf(FPU_SQRT_S(-0.0f)), bitsOf(0.0f), "sqrt(-0) should be +0"); }); - tc.Run("VU vector ADD/SUB/MUL clamp every lane", [](TestCase &t) + tc.Run("VU vector ADD/SUB/MUL follow the same rules in every lane", [](TestCase &t) { // _mm_set_ps lists lanes 3..0 - const __m128 a = _mm_set_ps(1.0f, std::numeric_limits::infinity(), floatOf(kMinusMax), floatOf(kPlusMax)); + const __m128 a = _mm_set_ps(1.0f, floatOf(kTwoPow128), floatOf(kMinusMax), floatOf(kPlusMax)); const __m128 b = _mm_set_ps(2.0f, 1.0f, floatOf(kMinusMax), floatOf(kPlusMax)); const __m128 sum = PS2_VADD(a, b); t.Equals(laneBits(sum, 0), kPlusMax, "lane 0: max + max should saturate to +max"); t.Equals(laneBits(sum, 1), kMinusMax, "lane 1: -max + -max should saturate to -max"); - t.Equals(laneBits(sum, 2), kPlusMax, "lane 2: +Inf + 1 should be +max"); + t.Equals(laneBits(sum, 2), kTwoPow128, "lane 2: 2^128 + 1 should stay 2^128"); t.Equals(laneBits(sum, 3), bitsOf(3.0f), "lane 3: 1 + 2 should be 3"); const __m128 diff = PS2_VSUB(a, b); t.Equals(laneBits(diff, 0), bitsOf(0.0f), "lane 0: max - max should be 0"); + t.Equals(laneBits(diff, 2), kTwoPow128, "lane 2: 2^128 - 1 should stay 2^128"); t.Equals(laneBits(diff, 3), bitsOf(-1.0f), "lane 3: 1 - 2 should be -1"); const __m128 prod = PS2_VMUL(a, b); t.Equals(laneBits(prod, 0), kPlusMax, "lane 0: max * max should saturate to +max"); t.Equals(laneBits(prod, 1), kPlusMax, "lane 1: -max * -max should saturate to +max"); - t.Equals(laneBits(prod, 2), kPlusMax, "lane 2: +Inf * 1 should be +max"); + t.Equals(laneBits(prod, 2), kTwoPow128, "lane 2: 2^128 * 1 should stay 2^128"); t.Equals(laneBits(prod, 3), bitsOf(2.0f), "lane 3: 1 * 2 should be 2"); + + const __m128 tiny = _mm_set_ps(1.0f, floatOf(0x80800000u), floatOf(0x00800000u), 3.0f); + const __m128 half = _mm_set1_ps(0.5f); + const __m128 flushed = PS2_VMUL(tiny, half); + t.Equals(laneBits(flushed, 0), bitsOf(1.5f), "lane 0: 3 * 0.5 should be 1.5"); + t.Equals(laneBits(flushed, 1), 0x00000000u, "lane 1: 2^-126 * 0.5 should flush to +0"); + t.Equals(laneBits(flushed, 2), 0x80000000u, "lane 2: -2^-126 * 0.5 should flush to -0"); + t.Equals(laneBits(flushed, 3), bitsOf(0.5f), "lane 3: 1 * 0.5 should be 0.5"); }); tc.Run("DIV.S and RSQRT.S translate to the PS2 semantics helpers", [](TestCase &t)