[A64/Vector] Scope VMX FPCR in vector helpers

Save and restore FPCR around each VMX FP operation so vector code
doesn't leak FPCR state into scalar paths. Drop the DN bit so NaN
propagation fixup can inspect original payloads.
This commit is contained in:
Reality
2026-03-21 16:39:44 +09:00
committed by Herman S.
parent 5b0b15676c
commit 3906ff11ef
5 changed files with 352 additions and 66 deletions

View File

@@ -0,0 +1,162 @@
/**
******************************************************************************
* Xenia : Xbox 360 Emulator Research Project *
******************************************************************************
* Copyright 2026 Ben Vanik. All rights reserved. *
* Released under the BSD license - see LICENSE in the root for more details. *
******************************************************************************
*/
#include "xenia/cpu/testing/util.h"
using namespace xe;
using namespace xe::cpu;
using namespace xe::cpu::hir;
using namespace xe::cpu::testing;
using xe::cpu::ppc::PPCContext;
// =============================================================================
// PPC NaN propagation rules for VMX vector float operations:
//
// 1. If src1 is NaN, result = src1 with bit 22 set (quieted).
// 2. Else if src2 is NaN, result = src2 with bit 22 set (quieted).
// 3. Else if the operation itself produces NaN (e.g., inf - inf),
// result = PPC default QNaN = 0xFFC00000.
//
// These tests verify that behavior for VECTOR_ADD with FLOAT32_TYPE.
// =============================================================================
// src1 has QNaN in lane 0, src2 is normal → result lane 0 = src1's NaN quieted.
TEST_CASE("VECTOR_ADD_F32_NAN_SRC1_PROPAGATES", "[instr]") {
TestFunction test([](HIRBuilder& b) {
StoreVR(b, 3, b.VectorAdd(LoadVR(b, 4), LoadVR(b, 5), FLOAT32_TYPE));
b.Return();
});
test.Run(
[](PPCContext* ctx) {
// QNaN with payload 0x1234 in lane 0; normal values elsewhere.
ctx->v[4] = vec128i(0x7FC01234, 0x3F800000, 0x40000000, 0x40400000);
ctx->v[5] = vec128i(0x3F800000, 0x3F800000, 0x3F800000, 0x3F800000);
},
[](PPCContext* ctx) {
auto result = ctx->v[3];
// Lane 0: src1 NaN wins, bit 22 set, payload preserved.
REQUIRE((result.u32[0] & 0xFFC00000) == 0x7FC00000);
REQUIRE((result.u32[0] & 0x003FFFFF) == 0x1234);
// Lane 1: 1.0 + 1.0 = 2.0
REQUIRE(result.u32[1] == 0x40000000);
});
}
// src1 is normal, src2 has QNaN → result = src2's NaN quieted.
TEST_CASE("VECTOR_ADD_F32_NAN_SRC2_PROPAGATES", "[instr]") {
TestFunction test([](HIRBuilder& b) {
StoreVR(b, 3, b.VectorAdd(LoadVR(b, 4), LoadVR(b, 5), FLOAT32_TYPE));
b.Return();
});
test.Run(
[](PPCContext* ctx) {
ctx->v[4] = vec128i(0x3F800000, 0x3F800000, 0x3F800000, 0x3F800000);
// QNaN with payload 0x5678 in lane 0.
ctx->v[5] = vec128i(0x7FC05678, 0x3F800000, 0x3F800000, 0x3F800000);
},
[](PPCContext* ctx) {
auto result = ctx->v[3];
// Lane 0: src2 NaN wins (src1 was not NaN), payload preserved.
REQUIRE((result.u32[0] & 0xFFC00000) == 0x7FC00000);
REQUIRE((result.u32[0] & 0x003FFFFF) == 0x5678);
});
}
// Both src1 and src2 have NaN in the same lane → src1 wins.
TEST_CASE("VECTOR_ADD_F32_NAN_BOTH_SRC1_WINS", "[instr]") {
TestFunction test([](HIRBuilder& b) {
StoreVR(b, 3, b.VectorAdd(LoadVR(b, 4), LoadVR(b, 5), FLOAT32_TYPE));
b.Return();
});
test.Run(
[](PPCContext* ctx) {
// src1: QNaN with payload 0xAAAA
ctx->v[4] = vec128i(0x7FC0AAAA, 0x3F800000, 0x3F800000, 0x3F800000);
// src2: QNaN with payload 0xBBBB
ctx->v[5] = vec128i(0x7FC0BBBB, 0x3F800000, 0x3F800000, 0x3F800000);
},
[](PPCContext* ctx) {
auto result = ctx->v[3];
// src1's NaN wins when both are NaN.
REQUIRE((result.u32[0] & 0xFFC00000) == 0x7FC00000);
REQUIRE((result.u32[0] & 0x003FFFFF) == 0x0AAAA);
});
}
// SNaN in src1 → result is src1 with bit 22 SET (quieted to QNaN).
TEST_CASE("VECTOR_ADD_F32_SNAN_QUIETED", "[instr]") {
TestFunction test([](HIRBuilder& b) {
StoreVR(b, 3, b.VectorAdd(LoadVR(b, 4), LoadVR(b, 5), FLOAT32_TYPE));
b.Return();
});
test.Run(
[](PPCContext* ctx) {
// SNaN: exponent all 1s, bit 22 = 0, mantissa != 0.
// 0x7F812345 = SNaN with payload.
ctx->v[4] = vec128i(0x7F812345, 0x3F800000, 0x3F800000, 0x3F800000);
ctx->v[5] = vec128i(0x3F800000, 0x3F800000, 0x3F800000, 0x3F800000);
},
[](PPCContext* ctx) {
auto result = ctx->v[3];
// SNaN quieted: bit 22 forced on, rest of payload preserved.
REQUIRE(result.u32[0] == (0x7F812345 | 0x00400000));
});
}
// Neither input is NaN, but operation produces NaN (inf + (-inf)).
// Result should be PPC default QNaN = 0xFFC00000.
TEST_CASE("VECTOR_ADD_F32_INF_MINUS_INF_DEFAULT_NAN", "[instr]") {
TestFunction test([](HIRBuilder& b) {
StoreVR(b, 3, b.VectorAdd(LoadVR(b, 4), LoadVR(b, 5), FLOAT32_TYPE));
b.Return();
});
test.Run(
[](PPCContext* ctx) {
// +inf + (-inf) → NaN
ctx->v[4] = vec128i(0x7F800000, 0x3F800000, 0x3F800000, 0x3F800000);
ctx->v[5] = vec128i(0xFF800000, 0x3F800000, 0x3F800000, 0x3F800000);
},
[](PPCContext* ctx) {
auto result = ctx->v[3];
// PPC default QNaN (negative).
REQUIRE(result.u32[0] == 0xFFC00000);
// Other lanes: normal 1.0 + 1.0 = 2.0
REQUIRE(result.u32[1] == 0x40000000);
});
}
// NaN in different lanes — verify per-lane independence.
TEST_CASE("VECTOR_ADD_F32_NAN_PER_LANE", "[instr]") {
TestFunction test([](HIRBuilder& b) {
StoreVR(b, 3, b.VectorAdd(LoadVR(b, 4), LoadVR(b, 5), FLOAT32_TYPE));
b.Return();
});
test.Run(
[](PPCContext* ctx) {
// Lane 0: normal + normal
// Lane 1: NaN in src1
// Lane 2: NaN in src2
// Lane 3: inf + (-inf)
ctx->v[4] = vec128i(0x3F800000, 0x7FC00001, 0x3F800000, 0x7F800000);
ctx->v[5] = vec128i(0x3F800000, 0x3F800000, 0x7FC00002, 0xFF800000);
},
[](PPCContext* ctx) {
auto result = ctx->v[3];
// Lane 0: 1.0 + 1.0 = 2.0
REQUIRE(result.u32[0] == 0x40000000);
// Lane 1: src1 NaN propagated, quieted.
REQUIRE((result.u32[1] & 0xFFC00000) == 0x7FC00000);
REQUIRE((result.u32[1] & 0x003FFFFF) == 0x00001);
// Lane 2: src2 NaN propagated, quieted.
REQUIRE((result.u32[2] & 0xFFC00000) == 0x7FC00000);
REQUIRE((result.u32[2] & 0x003FFFFF) == 0x00002);
// Lane 3: generated NaN → PPC default.
REQUIRE(result.u32[3] == 0xFFC00000);
});
}

View File

@@ -0,0 +1,103 @@
/**
******************************************************************************
* Xenia : Xbox 360 Emulator Research Project *
******************************************************************************
* Copyright 2026 Ben Vanik. All rights reserved. *
* Released under the BSD license - see LICENSE in the root for more details. *
******************************************************************************
*/
#include "xenia/cpu/testing/util.h"
#include <cmath>
using namespace xe;
using namespace xe::cpu;
using namespace xe::cpu::hir;
using namespace xe::cpu::testing;
using xe::cpu::ppc::PPCContext;
// =============================================================================
// FPCR isolation: VMX vector float ops must not leak their FPCR state into
// subsequent scalar FP operations.
//
// The bug: without scoped FPCR save/restore, a VMX op sets FPCR to
// round-to-nearest + flush-to-zero, and that state persists into the next
// scalar FP op — overriding whatever rounding mode the scalar path expects.
// =============================================================================
TEST_CASE("VMX_FPCR_DOES_NOT_LEAK_INTO_SCALAR", "[backend]") {
// Strategy:
// 1. SET_ROUNDING_MODE to toward-positive-infinity (mode 2)
// 2. Do a VMX vector float add (internally sets FPCR to RN + FZ)
// 3. Do a scalar float add of 1.0 + 2^-24
// 4. If FPCR leaked, the scalar add uses round-to-nearest → result = 1.0
// If FPCR was properly restored, it uses toward-+inf → result > 1.0
TestFunction test([](HIRBuilder& b) {
// Set scalar rounding to toward +infinity.
b.SetRoundingMode(b.LoadConstantInt32(2));
// VMX vector float add — this touches FPCR internally.
StoreVR(b, 3, b.VectorAdd(LoadVR(b, 4), LoadVR(b, 5), FLOAT32_TYPE));
// Now do a scalar float add. If FPCR leaked, this will round-to-nearest.
auto a = b.Convert(LoadFPR(b, 6), FLOAT32_TYPE);
auto c = b.Convert(LoadFPR(b, 7), FLOAT32_TYPE);
auto sum = b.Add(a, c);
StoreFPR(b, 3, b.Convert(sum, FLOAT64_TYPE));
b.Return();
});
test.Run(
[](PPCContext* ctx) {
// Vector inputs — just normal values, we don't care about the result.
ctx->v[4] = vec128f(1.0f, 2.0f, 3.0f, 4.0f);
ctx->v[5] = vec128f(5.0f, 6.0f, 7.0f, 8.0f);
// Scalar inputs: 1.0 + 2^-24 — rounds differently under different
// modes.
ctx->f[6] = 1.0;
ctx->f[7] = std::ldexp(1.0, -24);
},
[&test](PPCContext* ctx) {
auto result = static_cast<float>(ctx->f[3]);
// Under toward-+infinity, 1.0f + 2^-24 rounds UP to nextafter(1.0f).
// Under round-to-nearest, it rounds to 1.0f (ties to even).
float expected = std::nextafterf(1.0f, 2.0f);
REQUIRE(result == expected);
// Reset rounding mode for subsequent tests.
test.processors[0]->backend()->SetGuestRoundingMode(ctx, 0);
});
}
TEST_CASE("VMX_FPCR_DOES_NOT_LEAK_INTO_SCALAR_MULTIPLE_OPS", "[backend]") {
// Same idea but with two consecutive VMX vector float adds before the
// scalar op, to verify FPCR is restored after each one.
TestFunction test([](HIRBuilder& b) {
b.SetRoundingMode(b.LoadConstantInt32(2)); // toward +inf
// Two VMX vector float adds back to back.
StoreVR(b, 3, b.VectorAdd(LoadVR(b, 4), LoadVR(b, 5), FLOAT32_TYPE));
StoreVR(b, 6, b.VectorAdd(LoadVR(b, 4), LoadVR(b, 3), FLOAT32_TYPE));
// Scalar add — must still use toward-+inf.
auto a = b.Convert(LoadFPR(b, 6), FLOAT32_TYPE);
auto c = b.Convert(LoadFPR(b, 7), FLOAT32_TYPE);
auto sum = b.Add(a, c);
StoreFPR(b, 3, b.Convert(sum, FLOAT64_TYPE));
b.Return();
});
test.Run(
[](PPCContext* ctx) {
ctx->v[4] = vec128f(1.0f, 2.0f, 3.0f, 4.0f);
ctx->v[5] = vec128f(5.0f, 6.0f, 7.0f, 8.0f);
ctx->f[6] = 1.0;
ctx->f[7] = std::ldexp(1.0, -24);
},
[&test](PPCContext* ctx) {
auto result = static_cast<float>(ctx->f[3]);
float expected = std::nextafterf(1.0f, 2.0f);
REQUIRE(result == expected);
test.processors[0]->backend()->SetGuestRoundingMode(ctx, 0);
});
}