From 3906ff11ef93bcfe4cb1784ace26144356df3051 Mon Sep 17 00:00:00 2001 From: Reality Date: Sat, 21 Mar 2026 16:39:44 +0900 Subject: [PATCH] [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. --- src/xenia/cpu/backend/a64/a64_backend.h | 4 +- src/xenia/cpu/backend/a64/a64_seq_util.h | 67 +++++--- src/xenia/cpu/backend/a64/a64_seq_vector.cc | 82 +++++---- .../testing/vector_nan_propagation_test.cc | 162 ++++++++++++++++++ .../cpu/testing/vmx_fpcr_isolation_test.cc | 103 +++++++++++ 5 files changed, 352 insertions(+), 66 deletions(-) create mode 100644 src/xenia/cpu/testing/vector_nan_propagation_test.cc create mode 100644 src/xenia/cpu/testing/vmx_fpcr_isolation_test.cc diff --git a/src/xenia/cpu/backend/a64/a64_backend.h b/src/xenia/cpu/backend/a64/a64_backend.h index d51508b00..7f4ea0ed6 100644 --- a/src/xenia/cpu/backend/a64/a64_backend.h +++ b/src/xenia/cpu/backend/a64/a64_backend.h @@ -88,8 +88,8 @@ struct A64BackendContext { // Default FPCR for FPU mode (round to nearest, no flush to zero). constexpr unsigned int DEFAULT_FPU_FPCR = 0; -// Default FPCR for VMX mode (flush to zero, default NaN). -constexpr unsigned int DEFAULT_VMX_FPCR = (1 << 24) | (1 << 25); // FZ | DN +// Default FPCR for VMX mode (flush to zero, preserve NaN payloads). +constexpr unsigned int DEFAULT_VMX_FPCR = (1 << 24); // FZ class A64Backend : public Backend { public: diff --git a/src/xenia/cpu/backend/a64/a64_seq_util.h b/src/xenia/cpu/backend/a64/a64_seq_util.h index b8f2793e2..ecb235e2e 100644 --- a/src/xenia/cpu/backend/a64/a64_seq_util.h +++ b/src/xenia/cpu/backend/a64/a64_seq_util.h @@ -29,6 +29,21 @@ using Xbyak_aarch64::VReg; using Xbyak_aarch64::WReg; using Xbyak_aarch64::XReg; +template +inline void EmitWithVmxFpcr(A64Emitter& e, Fn&& emit_op) { + // VMX vector FP uses its own cached FPCR state in the backend context. Save + // and restore around each VMX op so vector code doesn't leak FPCR changes + // into later scalar instructions. + e.mrs(e.x13, 3, 3, 4, 4, 0); + e.sub(e.x14, e.GetContextReg(), + static_cast(sizeof(A64BackendContext))); + e.ldr(e.w15, Xbyak_aarch64::ptr(e.x14, static_cast(offsetof( + A64BackendContext, fpcr_vmx)))); + e.msr(3, 3, 4, 4, 0, e.x15); + emit_op(); + e.msr(3, 3, 4, 4, 0, e.x13); +} + // Load a compile-time vec128_t constant into a NEON register. inline void LoadV128Const(A64Emitter& e, int vreg_idx, const vec128_t& val) { e.mov(e.x0, val.low); @@ -302,36 +317,36 @@ enum class VmxFpBinOp { Add, Sub, Mul, Div }; template inline void EmitVmxFpBinOp_V128(A64Emitter& e, int dest_idx, const T1& src1, const T2& src2, VmxFpBinOp op) { - e.ChangeFpcrMode(FPCRMode::Vmx); + EmitWithVmxFpcr(e, [&] { + // Flush input denormals → v0=s1, v1=s2. + int s1, s2; + PrepareVmxFpSources(e, src1, src2, s1, s2); - // Flush input denormals → v0=s1, v1=s2. - int s1, s2; - PrepareVmxFpSources(e, src1, src2, s1, s2); + // Hardware FP op → v2. + switch (op) { + case VmxFpBinOp::Add: + e.fadd(VReg(2).s4, VReg(s1).s4, VReg(s2).s4); + break; + case VmxFpBinOp::Sub: + e.fsub(VReg(2).s4, VReg(s1).s4, VReg(s2).s4); + break; + case VmxFpBinOp::Mul: + e.fmul(VReg(2).s4, VReg(s1).s4, VReg(s2).s4); + break; + case VmxFpBinOp::Div: + e.fdiv(VReg(2).s4, VReg(s1).s4, VReg(s2).s4); + break; + } - // Hardware FP op → v2. - switch (op) { - case VmxFpBinOp::Add: - e.fadd(VReg(2).s4, VReg(s1).s4, VReg(s2).s4); - break; - case VmxFpBinOp::Sub: - e.fsub(VReg(2).s4, VReg(s1).s4, VReg(s2).s4); - break; - case VmxFpBinOp::Mul: - e.fmul(VReg(2).s4, VReg(s1).s4, VReg(s2).s4); - break; - case VmxFpBinOp::Div: - e.fdiv(VReg(2).s4, VReg(s1).s4, VReg(s2).s4); - break; - } + // PPC NaN propagation fixup (fast-path skip when no NaN). + FixupVmxNan_V128(e); - // PPC NaN propagation fixup (fast-path skip when no NaN). - FixupVmxNan_V128(e); + // Flush output denormals. + FlushDenormals_V128(e, 2, 0, 1); - // Flush output denormals. - FlushDenormals_V128(e, 2, 0, 1); - - // Move to dest. - e.mov(VReg(dest_idx).b16, VReg(2).b16); + // Move to dest. + e.mov(VReg(dest_idx).b16, VReg(2).b16); + }); } } // namespace a64 diff --git a/src/xenia/cpu/backend/a64/a64_seq_vector.cc b/src/xenia/cpu/backend/a64/a64_seq_vector.cc index 411ac6ddf..1dbcebf07 100644 --- a/src/xenia/cpu/backend/a64/a64_seq_vector.cc +++ b/src/xenia/cpu/backend/a64/a64_seq_vector.cc @@ -223,16 +223,17 @@ struct VECTOR_CONVERT_I2F : Sequence> { static void Emit(A64Emitter& e, const EmitArgType& i) { - e.ChangeFpcrMode(FPCRMode::Vmx); - int s = SrcVReg(e, i.src1, 0); - int d = i.dest.reg().getIdx(); - if (i.instr->flags & ARITHMETIC_UNSIGNED) { - // ARM64 ucvtf does a single-step unsigned int->float conversion, - // avoiding the double-rounding issue that x64 has to work around. - e.ucvtf(VReg(d).s4, VReg(s).s4); - } else { - e.scvtf(VReg(d).s4, VReg(s).s4); - } + EmitWithVmxFpcr(e, [&] { + int s = SrcVReg(e, i.src1, 0); + int d = i.dest.reg().getIdx(); + if (i.instr->flags & ARITHMETIC_UNSIGNED) { + // ARM64 ucvtf does a single-step unsigned int->float conversion, + // avoiding the double-rounding issue that x64 has to work around. + e.ucvtf(VReg(d).s4, VReg(s).s4); + } else { + e.scvtf(VReg(d).s4, VReg(s).s4); + } + }); } }; EMITTER_OPCODE_TABLE(OPCODE_VECTOR_CONVERT_I2F, VECTOR_CONVERT_I2F); @@ -244,16 +245,17 @@ struct VECTOR_CONVERT_F2I : Sequence> { static void Emit(A64Emitter& e, const EmitArgType& i) { - e.ChangeFpcrMode(FPCRMode::Vmx); - int s = SrcVReg(e, i.src1, 0); - int d = i.dest.reg().getIdx(); - if (i.instr->flags & ARITHMETIC_UNSIGNED) { - // ARM64 fcvtzu: NaN->0, negative->0, overflow->UINT_MAX. - e.fcvtzu(VReg(d).s4, VReg(s).s4); - } else { - // ARM64 fcvtzs: NaN->0, overflow saturates to INT_MIN/INT_MAX. - e.fcvtzs(VReg(d).s4, VReg(s).s4); - } + EmitWithVmxFpcr(e, [&] { + int s = SrcVReg(e, i.src1, 0); + int d = i.dest.reg().getIdx(); + if (i.instr->flags & ARITHMETIC_UNSIGNED) { + // ARM64 fcvtzu: NaN->0, negative->0, overflow->UINT_MAX. + e.fcvtzu(VReg(d).s4, VReg(s).s4); + } else { + // ARM64 fcvtzs: NaN->0, overflow saturates to INT_MIN/INT_MAX. + e.fcvtzs(VReg(d).s4, VReg(s).s4); + } + }); } }; EMITTER_OPCODE_TABLE(OPCODE_VECTOR_CONVERT_F2I, VECTOR_CONVERT_F2I); @@ -414,16 +416,18 @@ struct VECTOR_MAX template static void EmitVmxFpMaxMin(A64Emitter& e, const T& i, bool is_min) { - e.ChangeFpcrMode(FPCRMode::Vmx); - int s1, s2; - PrepareVmxFpSources(e, i.src1, i.src2, s1, s2); - if (is_min) - e.fmin(VReg(2).s4, VReg(s1).s4, VReg(s2).s4); - else - e.fmax(VReg(2).s4, VReg(s1).s4, VReg(s2).s4); - FixupVmxMaxMinNan(e); - FlushDenormals_V128(e, 2, 0, 1); - e.mov(VReg(i.dest.reg().getIdx()).b16, VReg(2).b16); + EmitWithVmxFpcr(e, [&] { + int s1, s2; + PrepareVmxFpSources(e, i.src1, i.src2, s1, s2); + if (is_min) { + e.fmin(VReg(2).s4, VReg(s1).s4, VReg(s2).s4); + } else { + e.fmax(VReg(2).s4, VReg(s1).s4, VReg(s2).s4); + } + FixupVmxMaxMinNan(e); + FlushDenormals_V128(e, 2, 0, 1); + e.mov(VReg(i.dest.reg().getIdx()).b16, VReg(2).b16); + }); } }; EMITTER_OPCODE_TABLE(OPCODE_VECTOR_MAX, VECTOR_MAX); @@ -491,8 +495,8 @@ struct VECTOR_COMPARE_EQ_V128 e.cmeq(VReg(d).s4, VReg(s1).s4, VReg(s2).s4); break; case FLOAT32_TYPE: - e.ChangeFpcrMode(FPCRMode::Vmx); - e.fcmeq(VReg(d).s4, VReg(s1).s4, VReg(s2).s4); + EmitWithVmxFpcr(e, + [&] { e.fcmeq(VReg(d).s4, VReg(s1).s4, VReg(s2).s4); }); break; default: assert_unhandled_case(i.instr->flags); @@ -523,8 +527,8 @@ struct VECTOR_COMPARE_SGT_V128 e.cmgt(VReg(d).s4, VReg(s1).s4, VReg(s2).s4); break; case FLOAT32_TYPE: - e.ChangeFpcrMode(FPCRMode::Vmx); - e.fcmgt(VReg(d).s4, VReg(s1).s4, VReg(s2).s4); + EmitWithVmxFpcr(e, + [&] { e.fcmgt(VReg(d).s4, VReg(s1).s4, VReg(s2).s4); }); break; default: assert_unhandled_case(i.instr->flags); @@ -555,8 +559,8 @@ struct VECTOR_COMPARE_SGE_V128 e.cmge(VReg(d).s4, VReg(s1).s4, VReg(s2).s4); break; case FLOAT32_TYPE: - e.ChangeFpcrMode(FPCRMode::Vmx); - e.fcmge(VReg(d).s4, VReg(s1).s4, VReg(s2).s4); + EmitWithVmxFpcr(e, + [&] { e.fcmge(VReg(d).s4, VReg(s1).s4, VReg(s2).s4); }); break; default: assert_unhandled_case(i.instr->flags); @@ -588,7 +592,8 @@ struct VECTOR_COMPARE_UGT_V128 break; case FLOAT32_TYPE: // Unsigned FP compare = ordered GT. - e.fcmgt(VReg(d).s4, VReg(s1).s4, VReg(s2).s4); + EmitWithVmxFpcr(e, + [&] { e.fcmgt(VReg(d).s4, VReg(s1).s4, VReg(s2).s4); }); break; default: assert_unhandled_case(i.instr->flags); @@ -619,7 +624,8 @@ struct VECTOR_COMPARE_UGE_V128 e.cmhs(VReg(d).s4, VReg(s1).s4, VReg(s2).s4); break; case FLOAT32_TYPE: - e.fcmge(VReg(d).s4, VReg(s1).s4, VReg(s2).s4); + EmitWithVmxFpcr(e, + [&] { e.fcmge(VReg(d).s4, VReg(s1).s4, VReg(s2).s4); }); break; default: assert_unhandled_case(i.instr->flags); diff --git a/src/xenia/cpu/testing/vector_nan_propagation_test.cc b/src/xenia/cpu/testing/vector_nan_propagation_test.cc new file mode 100644 index 000000000..3a6916430 --- /dev/null +++ b/src/xenia/cpu/testing/vector_nan_propagation_test.cc @@ -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); + }); +} diff --git a/src/xenia/cpu/testing/vmx_fpcr_isolation_test.cc b/src/xenia/cpu/testing/vmx_fpcr_isolation_test.cc new file mode 100644 index 000000000..33333635b --- /dev/null +++ b/src/xenia/cpu/testing/vmx_fpcr_isolation_test.cc @@ -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 + +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(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(ctx->f[3]); + float expected = std::nextafterf(1.0f, 2.0f); + REQUIRE(result == expected); + test.processors[0]->backend()->SetGuestRoundingMode(ctx, 0); + }); +}