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..0485d5006 100644 --- a/ps2xRuntime/include/ps2_runtime_macros.h +++ b/ps2xRuntime/include/ps2_runtime_macros.h @@ -140,10 +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 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) +{ + const float r = a + b; + return ps2_fnormal(r) ? r : ps2_fadd_slow(a, b); +} + +inline float ps2_fsub(float a, float b) +{ + 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) +{ + 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); +} + +inline float ps2_frsqrt(float a, float 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); +} + +// 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 expMask = _mm_set1_epi32(0x7F800000); + 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) _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_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)) @@ -605,11 +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) ((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_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) 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/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_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 new file mode 100644 index 000000000..2866d2bff --- /dev/null +++ b/ps2xTest/src/ps2_float_semantics_tests.cpp @@ -0,0 +1,228 @@ +#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 +#include + +using namespace ps2recomp; + +namespace +{ + 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) + { + 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]); + } + + 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("FPU results match the PS2 hardware results from ps2autotests", [](TestCase &t) + { + 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("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); + 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_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 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"); + 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"); + }); + + 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)), 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"); + t.Equals(bitsOf(FPU_SQRT_S(-0.0f)), bitsOf(0.0f), "sqrt(-0) should be +0"); + }); + + 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, 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), 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), 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) + { + 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); + }); + }); +}