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); + }); +}