diff --git a/src/xenia/base/platform_arm64.cc b/src/xenia/base/platform_arm64.cc index 825b334ca..d6cb9eeb1 100644 --- a/src/xenia/base/platform_arm64.cc +++ b/src/xenia/base/platform_arm64.cc @@ -7,6 +7,10 @@ ****************************************************************************** */ +#include +#include +#include + #include "xenia/base/cvar.h" #include "xenia/base/platform.h" #define XBYAK_NO_OP_NAMES @@ -17,6 +21,7 @@ DEFINE_int64(a64_extension_mask, -1LL, "features.\n" " 0 = armv8.0\n" " 1 = Large System Extensions(LSE) atomic operations\n" + " 2 = FPCR.FZ flushes denormal inputs (skip software flush)\n" " -1 = Detect and utilize all possible processor features\n", "a64"); namespace xe { @@ -43,6 +48,43 @@ void InitFeatureFlags() { Xbyak_aarch64::util::XBYAK_AARCH64_HWCAP_ATOMIC); #undef TEST_EMIT_FEATURE } + + // Detect whether FPCR.FZ flushes denormal float32 inputs to zero. + // The ARM spec says input flushing is implementation-defined. + // Modern cores (Cortex-A76+, Apple M1+) flush inputs; older ones may not. + if ((cvars::a64_extension_mask & kA64FZFlushesInputs) == + kA64FZFlushesInputs) { + // Build a denormal float32: smallest positive denormal = 0x00000001. + uint32_t denorm_bits = 1; + float denorm; + std::memcpy(&denorm, &denorm_bits, 4); + + // Save FPCR, enable FZ, add two denormals, check result. + uint64_t saved_fpcr; +#if XE_COMPILER_MSVC + saved_fpcr = _ReadStatusReg(ARM64_FPCR); + _WriteStatusReg(ARM64_FPCR, saved_fpcr | (1ULL << 24)); +#else + asm volatile("mrs %0, fpcr" : "=r"(saved_fpcr)); + uint64_t fz_fpcr = saved_fpcr | (1ULL << 24); + asm volatile("msr fpcr, %0" ::"r"(fz_fpcr)); +#endif + + volatile float a = denorm; + volatile float b = denorm; + volatile float result = a + b; + +#if XE_COMPILER_MSVC + _WriteStatusReg(ARM64_FPCR, saved_fpcr); +#else + asm volatile("msr fpcr, %0" ::"r"(saved_fpcr)); +#endif + + if (result == 0.0f) { + feature_flags_ |= kA64FZFlushesInputs; + } + } + g_feature_flags = feature_flags_; g_did_initialize_feature_flags = true; } diff --git a/src/xenia/base/platform_arm64.h b/src/xenia/base/platform_arm64.h index b9e521e52..19f21a136 100644 --- a/src/xenia/base/platform_arm64.h +++ b/src/xenia/base/platform_arm64.h @@ -15,7 +15,7 @@ namespace xe { namespace arm64 { enum A64FeatureFlags : uint64_t { kA64EmitLSE = 1 << 0, - + kA64FZFlushesInputs = 1 << 1, }; XE_NOALIAS diff --git a/src/xenia/cpu/backend/a64/a64_seq_util.h b/src/xenia/cpu/backend/a64/a64_seq_util.h index 9b34cd6e2..0e166fcc9 100644 --- a/src/xenia/cpu/backend/a64/a64_seq_util.h +++ b/src/xenia/cpu/backend/a64/a64_seq_util.h @@ -363,8 +363,11 @@ inline void PrepareVmxFpSources(A64Emitter& e, const T1& op1, const T2& op2, // Copy to scratch v0/v1 so we don't modify live allocated registers. if (s1 != 0) e.mov(VReg(0).b16, VReg(s1).b16); if (s2 != 1) e.mov(VReg(1).b16, VReg(s2).b16); - FlushDenormals_V128(e, 0); - FlushDenormals_V128(e, 1); + // Flush denormal inputs in software only if FPCR.FZ doesn't handle it. + if (!e.IsFeatureEnabled(xe::arm64::kA64FZFlushesInputs)) { + FlushDenormals_V128(e, 0); + FlushDenormals_V128(e, 1); + } out_s1 = 0; out_s2 = 1; } @@ -547,8 +550,12 @@ inline void EmitVmxFpBinOp_V128(A64Emitter& e, int dest_idx, const T1& src1, // 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. FPCR.FZ guarantees output flushing per the + // ARM spec, so skip when FZ is known to also handle inputs (implying + // the core fully supports FZ denormal handling). + if (!e.IsFeatureEnabled(xe::arm64::kA64FZFlushesInputs)) { + FlushDenormals_V128(e, 2, 0, 1); + } // Move to dest. e.mov(VReg(dest_idx).b16, VReg(2).b16); diff --git a/src/xenia/cpu/backend/a64/a64_seq_vector.cc b/src/xenia/cpu/backend/a64/a64_seq_vector.cc index 12329c22c..f224e9696 100644 --- a/src/xenia/cpu/backend/a64/a64_seq_vector.cc +++ b/src/xenia/cpu/backend/a64/a64_seq_vector.cc @@ -415,7 +415,9 @@ struct VECTOR_MAX e.fmax(VReg(2).s4, VReg(s1).s4, VReg(s2).s4); } FixupVmxMaxMinNan(e); - FlushDenormals_V128(e, 2, 0, 1); + if (!e.IsFeatureEnabled(xe::arm64::kA64FZFlushesInputs)) { + FlushDenormals_V128(e, 2, 0, 1); + } e.mov(VReg(i.dest.reg().getIdx()).b16, VReg(2).b16); }); } diff --git a/src/xenia/cpu/backend/a64/a64_sequences.cc b/src/xenia/cpu/backend/a64/a64_sequences.cc index 4ca256b05..7cea9cd5c 100644 --- a/src/xenia/cpu/backend/a64/a64_sequences.cc +++ b/src/xenia/cpu/backend/a64/a64_sequences.cc @@ -3162,7 +3162,9 @@ struct MAX_V128 : Sequence> { e.fmax(VReg(2).s4, VReg(s1).s4, VReg(s2).s4); // PPC vmaxfp: if either input is NaN, result = src1 (vA). FixupVmxMaxMinNan(e); - FlushDenormals_V128(e, 2, 0, 1); + if (!e.IsFeatureEnabled(xe::arm64::kA64FZFlushesInputs)) { + FlushDenormals_V128(e, 2, 0, 1); + } e.mov(VReg(i.dest.reg().getIdx()).b16, VReg(2).b16); }); } @@ -3299,7 +3301,9 @@ struct MIN_V128 : Sequence> { e.fmin(VReg(2).s4, VReg(s1).s4, VReg(s2).s4); // PPC vminfp: if either input is NaN, result = src1 (vA). FixupVmxMaxMinNan(e); - FlushDenormals_V128(e, 2, 0, 1); + if (!e.IsFeatureEnabled(xe::arm64::kA64FZFlushesInputs)) { + FlushDenormals_V128(e, 2, 0, 1); + } e.mov(VReg(i.dest.reg().getIdx()).b16, VReg(2).b16); }); } @@ -3993,7 +3997,9 @@ struct MUL_ADD_V128 // Flush s3 → v3, save to stack slot 2. int s3 = SrcVReg(e, i.src3, 3); if (s3 != 3) e.mov(VReg(3).b16, VReg(s3).b16); - FlushDenormals_V128(e, 3, 0, 1); + if (!e.IsFeatureEnabled(xe::arm64::kA64FZFlushesInputs)) { + FlushDenormals_V128(e, 3, 0, 1); + } e.str(QReg(3), Xbyak_aarch64::ptr( e.sp, static_cast(StackLayout::GUEST_SCRATCH) + 32)); @@ -4017,7 +4023,9 @@ struct MUL_ADD_V128 FixupVmxNan_V128_Fma(e); // Flush output denormals. - FlushDenormals_V128(e, 2, 0, 1); + if (!e.IsFeatureEnabled(xe::arm64::kA64FZFlushesInputs)) { + FlushDenormals_V128(e, 2, 0, 1); + } e.mov(VReg(d).b16, VReg(2).b16); }); } @@ -4077,7 +4085,9 @@ struct MUL_SUB_V128 // Flush s3 → v3, save un-negated for NaN fixup. int s3 = SrcVReg(e, i.src3, 3); if (s3 != 3) e.mov(VReg(3).b16, VReg(s3).b16); - FlushDenormals_V128(e, 3, 0, 1); + if (!e.IsFeatureEnabled(xe::arm64::kA64FZFlushesInputs)) { + FlushDenormals_V128(e, 3, 0, 1); + } e.str(QReg(3), Xbyak_aarch64::ptr( e.sp, static_cast(StackLayout::GUEST_SCRATCH) + 32)); @@ -4102,7 +4112,9 @@ struct MUL_SUB_V128 FixupVmxNan_V128_Fma(e); // Flush output denormals. - FlushDenormals_V128(e, 2, 0, 1); + if (!e.IsFeatureEnabled(xe::arm64::kA64FZFlushesInputs)) { + FlushDenormals_V128(e, 2, 0, 1); + } e.mov(VReg(d).b16, VReg(2).b16); }); } @@ -4643,13 +4655,17 @@ struct RECIP_V128 : Sequence> { e.mov(VReg(1).b16, VReg(i.src1.reg().getIdx()).b16); } // Flush input denormals. - FlushDenormals_V128(e, 1); // scratch v2, v3 + if (!e.IsFeatureEnabled(xe::arm64::kA64FZFlushesInputs)) { + FlushDenormals_V128(e, 1); // scratch v2, v3 + } auto d = VReg(i.dest.reg().getIdx()).s4; // Load 1.0f vector. e.fmov(VReg(0).s4, 1.0f); e.fdiv(d, VReg(0).s4, VReg(1).s4); // Flush output denormals. - FlushDenormals_V128(e, i.dest.reg().getIdx(), 0, 1); + if (!e.IsFeatureEnabled(xe::arm64::kA64FZFlushesInputs)) { + FlushDenormals_V128(e, i.dest.reg().getIdx(), 0, 1); + } }); } };