[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

@@ -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:

View File

@@ -29,6 +29,21 @@ using Xbyak_aarch64::VReg;
using Xbyak_aarch64::WReg;
using Xbyak_aarch64::XReg;
template <typename Fn>
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<uint32_t>(sizeof(A64BackendContext)));
e.ldr(e.w15, Xbyak_aarch64::ptr(e.x14, static_cast<uint32_t>(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 <typename T1, typename T2>
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

View File

@@ -223,16 +223,17 @@ struct VECTOR_CONVERT_I2F
: Sequence<VECTOR_CONVERT_I2F,
I<OPCODE_VECTOR_CONVERT_I2F, V128Op, V128Op>> {
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<VECTOR_CONVERT_F2I,
I<OPCODE_VECTOR_CONVERT_F2I, V128Op, V128Op>> {
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 <typename T>
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);