// SPDX-License-Identifier: GPL-2.0-or-later // // Copyright 2018 Dolphin Emulator Project // // Portions of this file are derived from the Dolphin Emulator // (https://github.com/dolphin-emu/dolphin): // // * Source/Core/Common/FloatUtils.cpp - the Gekko/Broadway fres and frsqrte // estimate tables (kPpcFresEstimateInline / kPpcFrsqrteEstimateInline, // upstream fres_expected / frsqrte_expected) together with the // PpcApproximateReciprocalInline and PpcApproximateReciprocalSquareRootInline // interpolation routines, which are ports of upstream ApproximateReciprocal // and ApproximateReciprocalSquareRoot. // // The remainder of this header is original to this project. It is // GPL-2.0-or-later as a consequence of the above; see THIRD-PARTY-NOTICES.md. #pragma once // Pure floating-point and paired-single PowerPC semantics: the scalar single // and double families, every ps_* arithmetic form, the NI flush rules and the // Gekko fres/frsqrte estimates. Nothing in this header touches guest memory - // the psq_l/psq_st tier that does lives in ppc_isa_quantized.h. #include "ppc_isa_config.h" #include "ppc_isa_context.h" #include #include #include #include #include inline uint64_t PpcBitCastToU64Inline(double value) { uint64_t integral = 0; std::memcpy(&integral, &value, sizeof(integral)); return integral; } inline uint32_t PPC_FprLowWordInline(double value) { return static_cast(PpcBitCastToU64Inline(value)); } inline double PpcBitCastToDoubleInline(uint64_t value) { double result = 0.0; std::memcpy(&result, &value, sizeof(result)); return result; } inline uint32_t PpcBitCastToU32Inline(float value) { uint32_t integral = 0; std::memcpy(&integral, &value, sizeof(integral)); return integral; } inline float PpcBitCastToFloatInline(uint32_t value) { float result = 0.0f; std::memcpy(&result, &value, sizeof(result)); return result; } // NOTE: PpcGetPs0Inline / PpcGetPs1Inline / PpcPackPairedInline are defined // below the paired-single helper block (PpcPsToM128Inline and // friends) because their register-domain implementations are written in terms // of those helpers. Nothing between here and there uses them. inline double PpcGetPairedFprInline(const PPC_FPR& fpr) { return fpr.d; } inline void PpcSetPairedFprInline(PPC_FPR& fpr, double packed) { fpr.d = packed; } #if defined(__x86_64__) using PpcPairVec = __m128; #elif defined(__aarch64__) // Only 2 lanes are ever meaningful (a PPC paired-single register), so a 2-lane float32x2_t // (one 64-bit D register) is a more natural fit than mirroring x86's 128-bit register usage. using PpcPairVec = float32x2_t; #endif // Must stay inside the vector register domain. Bitcasting through a 64-bit GPR added a movq // domain crossing on every paired-single op (630 in the THP IDCT region alone); a double // local already lives in a vector register, so these casts compile to nothing. inline PpcPairVec PpcPsToM128Inline(double value) { #if defined(__x86_64__) return _mm_castpd_ps(_mm_set_sd(value)); #elif defined(__aarch64__) return vreinterpret_f32_f64(vdup_n_f64(value)); #endif } inline double PpcM128ToPsInline(PpcPairVec value) { #if defined(__x86_64__) return _mm_cvtsd_f64(_mm_castps_pd(value)); #elif defined(__aarch64__) return vget_lane_f64(vreinterpret_f64_f32(value), 0); #endif } inline PpcPairVec PpcBroadcastPs0Inline(double value) { #if defined(__x86_64__) const __m128 lanes = PpcPsToM128Inline(value); return _mm_shuffle_ps(lanes, lanes, _MM_SHUFFLE(1, 1, 1, 1)); #elif defined(__aarch64__) // ps0 lives in lane 1 (see the lane-accessor comment below). return vdup_lane_f32(PpcPsToM128Inline(value), 1); #endif } inline PpcPairVec PpcBroadcastPs1Inline(double value) { #if defined(__x86_64__) const __m128 lanes = PpcPsToM128Inline(value); return _mm_shuffle_ps(lanes, lanes, _MM_SHUFFLE(0, 0, 0, 0)); #elif defined(__aarch64__) // ps1 is already lane 0. return vdup_lane_f32(PpcPsToM128Inline(value), 0); #endif } inline PpcPairVec PpcNegateNonNanLanesInline(PpcPairVec value) { #if defined(__x86_64__) const __m128 signMask = _mm_castsi128_ps(_mm_set1_epi32(static_cast(0x80000000u))); const __m128 negated = _mm_xor_ps(value, signMask); const __m128 ordered = _mm_cmpord_ps(value, value); return _mm_or_ps(_mm_and_ps(ordered, negated), _mm_andnot_ps(ordered, value)); #elif defined(__aarch64__) const uint32x2_t signMask = vdup_n_u32(0x80000000u); const float32x2_t negated = vreinterpret_f32_u32(veor_u32(vreinterpret_u32_f32(value), signMask)); // NEON has no direct "ordered compare"; a value compares equal to itself iff it's not NaN. const uint32x2_t ordered = vceq_f32(value, value); return vbsl_f32(ordered, negated, value); #endif } // Paired-single lane accessors. The packed double's LOW 32 bits hold ps1 and HIGH 32 bits // hold ps0, so in the SSE float-lane view lane 0 == ps1 and lane 1 == ps0. Must stay pure // register-domain shuffles (no arithmetic/conversion, so NaN/denormal bits pass through); // the former union-based forms store-then-reloaded through memory, a guaranteed // store-to-load-forwarding stall on every scalar-lane op. inline float PpcGetPs0Inline(double value) { // ps0 lives in lane 1; PpcBroadcastPs0Inline already splats it. #if defined(__x86_64__) return _mm_cvtss_f32(PpcBroadcastPs0Inline(value)); #elif defined(__aarch64__) return vget_lane_f32(PpcBroadcastPs0Inline(value), 0); #endif } inline float PpcGetPs1Inline(double value) { // ps1 is already lane 0 of the packed representation. #if defined(__x86_64__) return _mm_cvtss_f32(PpcPsToM128Inline(value)); #elif defined(__aarch64__) return vget_lane_f32(PpcPsToM128Inline(value), 0); #endif } inline double PpcPackPairedInline(float ps0, float ps1) { #if defined(__x86_64__) // _mm_unpacklo_ps(x, y) -> { x[0], y[0], x[1], y[1] }, so lane 0 becomes // ps1 and lane 1 becomes ps0, matching the union layout bit for bit. return PpcM128ToPsInline(_mm_unpacklo_ps(_mm_set_ss(ps1), _mm_set_ss(ps0))); #elif defined(__aarch64__) // Lane 0 = ps1, lane 1 = ps0, matching the union layout bit for bit. const float32x2_t lane0 = vdup_n_f32(ps1); return PpcM128ToPsInline(vset_lane_f32(ps0, lane0, 1)); #endif } // FPSCR[NI] is modeled by MXCSR FTZ/DAZ, so arithmetic output flushing compiles to nothing. // Two cases still need a software check against the mirrored bit (not STMXCSR, too hot): // double->single conversion (CVTSD2SS isn't covered by FTZ) and raw lane pass-through moves. inline bool MkwHostNiActiveInline() noexcept { return g_mkwHostNiActive; } inline float PpcForceSingleValueInline(double value) { // FPSCR[NI] flushes an exact pre-round single-subnormal even when rounding would promote it // to the smallest normal. g_mkwNiFlushThreshold (2^-126 active, 0.0 inactive) turns the // flush into a branchless mask: a set compare lane keeps just the sign bit, a clear lane // passes the value to the double->float conversion. DAZ/FZ (set exactly when NI is) makes // the compare itself treat a subnormal as zero, matching the mask's answer. #if defined(__x86_64__) const __m128d v = _mm_set_sd(value); const __m128d signMask = _mm_set_sd(-0.0); const __m128d magnitude = _mm_andnot_pd(signMask, v); const __m128d flush = _mm_cmplt_sd(magnitude, _mm_set_sd(g_mkwNiFlushThreshold)); const __m128d kept = _mm_andnot_pd(_mm_andnot_pd(signMask, flush), v); return static_cast(_mm_cvtsd_f64(kept)); #elif defined(__aarch64__) const float64x1_t v = vdup_n_f64(value); const uint64x1_t signMask = vdup_n_u64(0x8000000000000000ULL); const float64x1_t magnitude = vreinterpret_f64_u64(vbic_u64(vreinterpret_u64_f64(v), signMask)); const uint64x1_t flush = vclt_f64(magnitude, vdup_n_f64(g_mkwNiFlushThreshold)); const uint64x1_t signOnly = vand_u64(vreinterpret_u64_f64(v), signMask); const uint64x1_t kept = vbsl_u64(flush, signOnly, vreinterpret_u64_f64(v)); return static_cast(vget_lane_f64(vreinterpret_f64_u64(kept), 0)); #endif } inline float PpcFlushSingleForNiInline(float value) { if (!MkwHostNiActiveInline()) return value; const uint32_t bits = PpcBitCastToU32Inline(value); if ((bits & 0x7FFFFFFFu) < 0x00800000u) [[unlikely]] return PpcBitCastToFloatInline(bits & 0x80000000u); return value; } inline double PpcFlushPairedForNiInline(double value) { // Callers pass results of SSE arithmetic; MXCSR.FTZ already flushed them. return value; } inline double PpcForce25BitInline(double value) { constexpr uint64_t kDoubleExpMask = 0x7FF0000000000000ULL; constexpr uint64_t kDoubleFracMask = 0x000FFFFFFFFFFFFFULL; constexpr int kDoubleFracWidth = 52; uint64_t integral = PpcBitCastToU64Inline(value); const uint64_t exponent = integral & kDoubleExpMask; const uint64_t fraction = integral & kDoubleFracMask; if (exponent == 0 && fraction != 0) { int64_t keepMask = 0xFFFFFFFFF8000000LL; uint64_t round = 0x8000000ULL; uint32_t leadingZeros = 0; uint64_t normalizedFraction = fraction; while ((normalizedFraction & (1ULL << 63)) == 0) { normalizedFraction <<= 1; ++leadingZeros; } const uint32_t shift = leadingZeros - (63 - kDoubleFracWidth); keepMask >>= shift; round >>= shift; integral = (integral & static_cast(keepMask)) + (integral & round); } else { integral = (integral & 0xFFFFFFFFF8000000ULL) + (integral & 0x8000000ULL); } return PpcBitCastToDoubleInline(integral); } struct PpcEstimateEntryInline { int32_t base; int32_t decrement; }; inline constexpr std::array kPpcFresEstimateInline = {{ {0x7ff800, 0x3e1}, {0x783800, 0x3a7}, {0x70ea00, 0x371}, {0x6a0800, 0x340}, {0x638800, 0x313}, {0x5d6200, 0x2ea}, {0x579000, 0x2c4}, {0x520800, 0x2a0}, {0x4cc800, 0x27f}, {0x47ca00, 0x261}, {0x430800, 0x245}, {0x3e8000, 0x22a}, {0x3a2c00, 0x212}, {0x360800, 0x1fb}, {0x321400, 0x1e5}, {0x2e4a00, 0x1d1}, {0x2aa800, 0x1be}, {0x272c00, 0x1ac}, {0x23d600, 0x19b}, {0x209e00, 0x18b}, {0x1d8800, 0x17c}, {0x1a9000, 0x16e}, {0x17ae00, 0x15b}, {0x14f800, 0x15b}, {0x124400, 0x143}, {0x0fbe00, 0x143}, {0x0d3800, 0x12d}, {0x0ade00, 0x12d}, {0x088400, 0x11a}, {0x065000, 0x11a}, {0x041c00, 0x108}, {0x020c00, 0x106}, }}; inline double PpcApproximateReciprocalInline(double value) { constexpr uint64_t kSign = 0x8000000000000000ULL; constexpr uint64_t kExponent = 0x7FF0000000000000ULL; constexpr uint64_t kFraction = 0x000FFFFFFFFFFFFFULL; constexpr uint64_t kQuietBit = 0x0008000000000000ULL; const uint64_t input = PpcBitCastToU64Inline(value); const uint64_t mantissa = input & kFraction; const uint64_t sign = input & kSign; uint64_t exponent = input & kExponent; if (mantissa == 0 && exponent == 0) return PpcBitCastToDoubleInline(sign | kExponent); if (exponent == kExponent) { if (mantissa == 0) return PpcBitCastToDoubleInline(sign); return PpcBitCastToDoubleInline(input | kQuietBit); } if (exponent < (uint64_t{895} << 52)) return std::copysign(static_cast(std::numeric_limits::max()), value); if (exponent >= (uint64_t{1149} << 52)) return std::copysign(0.0, value); exponent = (uint64_t{0x7FD} << 52) - exponent; const int index = static_cast(mantissa >> 37); const auto& entry = kPpcFresEstimateInline[static_cast(index / 1024)]; const int64_t estimate = static_cast(entry.base) - (static_cast(entry.decrement) * (index % 1024) + 1) / 2; return PpcBitCastToDoubleInline( sign | exponent | (static_cast(estimate) << 29)); } // Broadway/Gekko frsqrte lookup table. These constants and the interpolation // below match the algorithm used by the checked-in Dolphin reference rather // than substituting an exact host reciprocal square root. This lives in the // header because frsqrte is emitted at 281 translated sites and an out-of-line // call is a full register barrier at each of them. inline constexpr std::array kPpcFrsqrteEstimateInline = {{ {0x1a7e800, -0x568}, {0x17cb800, -0x4f3}, {0x1552800, -0x48d}, {0x130c000, -0x435}, {0x10f2000, -0x3e7}, {0x0eff000, -0x3a2}, {0x0d2e000, -0x365}, {0x0b7c000, -0x32e}, {0x09e5000, -0x2fc}, {0x0867000, -0x2d0}, {0x06ff000, -0x2a8}, {0x05ab800, -0x283}, {0x046a000, -0x261}, {0x0339800, -0x243}, {0x0218800, -0x226}, {0x0105800, -0x20b}, {0x3ffa000, -0x7a4}, {0x3c29000, -0x700}, {0x38aa000, -0x670}, {0x3572000, -0x5f2}, {0x3279000, -0x584}, {0x2fb7000, -0x524}, {0x2d26000, -0x4cc}, {0x2ac0000, -0x47e}, {0x2881000, -0x43a}, {0x2665000, -0x3fa}, {0x2468000, -0x3c2}, {0x2287000, -0x38e}, {0x20c1000, -0x35e}, {0x1f12000, -0x332}, {0x1d79000, -0x30a}, {0x1bf4000, -0x2e6}, }}; inline double PpcApproximateReciprocalSquareRootInline(double value) { constexpr uint64_t kSign = 0x8000000000000000ULL; constexpr uint64_t kExponent = 0x7FF0000000000000ULL; constexpr uint64_t kFraction = 0x000FFFFFFFFFFFFFULL; constexpr uint64_t kQuietBit = 0x0008000000000000ULL; constexpr uint64_t kCanonicalQuietNaN = kExponent | kQuietBit; const uint64_t input = PpcBitCastToU64Inline(value); uint64_t mantissa = input & kFraction; const uint64_t sign = input & kSign; int64_t exponent = static_cast(input & kExponent); if (mantissa == 0 && exponent == 0) { return PpcBitCastToDoubleInline(sign | kExponent); } if (static_cast(exponent) == kExponent) { if (mantissa == 0) { return sign ? PpcBitCastToDoubleInline(kCanonicalQuietNaN) : 0.0; } return PpcBitCastToDoubleInline(input | kQuietBit); } if (sign != 0) { return PpcBitCastToDoubleInline(kCanonicalQuietNaN); } if (exponent == 0) { // Normalize a subnormal while allowing the signed exponent to extend // below the IEEE-754 encoded range, exactly as the hardware estimate // interpolation expects. do { exponent -= int64_t{1} << 52; mantissa <<= 1; } while ((mantissa & (uint64_t{1} << 52)) == 0); mantissa &= kFraction; exponent += int64_t{1} << 52; } const int64_t exponentLsb = exponent & (int64_t{1} << 52); exponent = (((int64_t{0x3FF} << 52) - ((exponent - (int64_t{0x3FE} << 52)) / 2)) & static_cast(kExponent)); const int index = static_cast( (static_cast(exponentLsb) | mantissa) >> 37); const auto& entry = kPpcFrsqrteEstimateInline[static_cast(index / 2048)]; const int64_t estimate = static_cast(entry.base) + static_cast(entry.decrement) * (index % 2048); const uint64_t result = sign | static_cast(exponent) | (static_cast(estimate) << 26); return PpcBitCastToDoubleInline(result); } // fctiwz: convert to a 32-bit signed integer with round-toward-zero and place // the result in the LOW 32 bits of the FPR. The upper 32 bits are // architecturally undefined; zero is what the hardware leaves in practice and // what stfd/lwz+4 sequences in guest code expect to read back. inline int32_t PpcClampIntegerWordInline(double value) { if (std::isnan(value)) { return static_cast(0x80000000u); } if (value >= 2147483647.0) { return 2147483647; } if (value <= -2147483648.0) { return static_cast(0x80000000u); } return static_cast(value); } inline double PpcPackIntegerWordInline(int32_t value) { return PpcBitCastToDoubleInline( static_cast(static_cast(value))); } template inline float PpcAccuratePsMaddLaneInline(float a, float c, float b) { const float signedB = Subtract ? -b : b; // Paired values are stored as exact float32 lanes in this runtime. An // explicit float FMA therefore matches Gekko's single rounding point; // multiplying and adding separately can differ by an ULP on ordinary // vector and matrix workloads. return PpcFlushSingleForNiInline(std::fma(a, c, signedB)); } template inline float PpcAccuratePsMaddLaneNoNiInline(float a, float c, float b) { const float signedB = Subtract ? -b : b; return std::fma(a, c, signedB); } template inline double PpcAccurateSingleMaddIntermediateInline(double a, double c, double b) { // Gekko single-precision fused operations keep the full precision of A // and B, round C to a 25-bit significand, and round only the final result // to float32. A double FMA is almost sufficient, but an exact result just // beyond a float32 halfway point can be rounded to a double tie first. // Recover the direction of that discarded error for those tie cases. const double roundedC = PpcForce25BitInline(c); const double signedB = Subtract ? -b : b; double result = std::fma(a, roundedC, signedB); const uint64_t resultBits = PpcBitCastToU64Inline(result); constexpr uint64_t kDiscardedMask = 0x000000001FFFFFFFULL; constexpr uint64_t kEvenTie = 0x0000000010000000ULL; if ((resultBits & kDiscardedMask) == kEvenTie) { const double aPrime = signedB - result; const double bPrime = result + aPrime; const double deltaA = std::fma(a, roundedC, aPrime); const double deltaB = signedB - bPrime; const double error = deltaA + deltaB; if (error != 0.0) { result = PpcBitCastToDoubleInline( (error > 0.0) == (result > 0.0) ? resultBits + 1 : resultBits - 1); } } return result; } inline double PpcFmulsInline(double a, double c) { return static_cast( PpcForceSingleValueInline(a * PpcForce25BitInline(c))); } inline double PpcFmulsNoNiInline(double a, double c) { return static_cast(static_cast(a * PpcForce25BitInline(c))); } inline double PpcFmaddInline(double a, double c, double b) { return std::fma(a, c, b); } inline double PpcFmsubInline(double a, double c, double b) { return std::fma(a, c, -b); } inline double PpcFnmaddInline(double a, double c, double b) { const double result = std::fma(a, c, b); return std::isnan(result) ? result : -result; } inline double PpcFnmsubInline(double a, double c, double b) { const double result = std::fma(a, c, -b); return std::isnan(result) ? result : -result; } inline double PpcFmaddsInline(double a, double c, double b) { return static_cast( PpcForceSingleValueInline(PpcAccurateSingleMaddIntermediateInline(a, c, b))); } inline double PpcFmsubsInline(double a, double c, double b) { return static_cast( PpcForceSingleValueInline(PpcAccurateSingleMaddIntermediateInline(a, c, b))); } inline double PpcFnmaddsInline(double a, double c, double b) { const float result = PpcForceSingleValueInline( PpcAccurateSingleMaddIntermediateInline(a, c, b)); return static_cast(std::isnan(result) ? result : -result); } inline double PpcFnmsubsInline(double a, double c, double b) { const float result = PpcForceSingleValueInline( PpcAccurateSingleMaddIntermediateInline(a, c, b)); return static_cast(std::isnan(result) ? result : -result); } #if defined(__aarch64__) // x86 resolves a single NaN source operand (SNaN or QNaN alike) to that operand quieted with // sign and payload kept, and an invalid op with no NaN input to 0xFFC00000; NEON prefers SNaNs, // checks the accumulator first, and yields the positive default NaN 0x7FC00000. NaN result // lanes (rare: one branch on the pair) are therefore rewritten to the x86 answer, keeping the // two hosts bit-identical for every case where x86 itself is deterministic. With two or more // NaN operands even x86 is not: the winning operand depends on which FMA form / commuted // operand order clang picked per call site, so this resolver's fixed first-operand priority is // one of the answers a real x86 build can give, not a guaranteed match. inline uint64_t PpcPairNanLaneBitsInline(PpcPairVec value) { return vget_lane_u64(vreinterpret_u64_u32(vmvn_u32(vceq_f32(value, value))), 0); } inline PpcPairVec PpcQuietPairInline(PpcPairVec value) { return vreinterpret_f32_u32(vorr_u32(vreinterpret_u32_f32(value), vdup_n_u32(0x00400000u))); } inline PpcPairVec PpcResolveNanLanesInline(PpcPairVec result, PpcPairVec op1, PpcPairVec op2) { const uint32x2_t op1Nan = vmvn_u32(vceq_f32(op1, op1)); const uint32x2_t op2Nan = vmvn_u32(vceq_f32(op2, op2)); PpcPairVec replacement = vreinterpret_f32_u32(vdup_n_u32(0xFFC00000u)); replacement = vbsl_f32(op2Nan, PpcQuietPairInline(op2), replacement); replacement = vbsl_f32(op1Nan, PpcQuietPairInline(op1), replacement); const uint32x2_t resultNan = vmvn_u32(vceq_f32(result, result)); return vbsl_f32(resultNan, replacement, result); } inline PpcPairVec PpcResolveNanLanes3Inline( PpcPairVec result, PpcPairVec op1, PpcPairVec op2, PpcPairVec op3) { const uint32x2_t op3Nan = vmvn_u32(vceq_f32(op3, op3)); PpcPairVec replacement = vreinterpret_f32_u32(vdup_n_u32(0xFFC00000u)); replacement = vbsl_f32(op3Nan, PpcQuietPairInline(op3), replacement); const uint32x2_t op2Nan = vmvn_u32(vceq_f32(op2, op2)); replacement = vbsl_f32(op2Nan, PpcQuietPairInline(op2), replacement); const uint32x2_t op1Nan = vmvn_u32(vceq_f32(op1, op1)); replacement = vbsl_f32(op1Nan, PpcQuietPairInline(op1), replacement); const uint32x2_t resultNan = vmvn_u32(vceq_f32(result, result)); return vbsl_f32(resultNan, replacement, result); } #endif // defined(__aarch64__) inline PpcPairVec PpcMulPairInline(PpcPairVec lhs, PpcPairVec rhs) { #if defined(__x86_64__) return _mm_mul_ps(lhs, rhs); #elif defined(__aarch64__) const PpcPairVec result = vmul_f32(lhs, rhs); if (PpcPairNanLaneBitsInline(result) != 0) [[unlikely]] return PpcResolveNanLanesInline(result, lhs, rhs); return result; #endif } inline double PPC_PsMulInline(double lhs, double rhs) { return PpcFlushPairedForNiInline( PpcM128ToPsInline(PpcMulPairInline(PpcPsToM128Inline(lhs), PpcPsToM128Inline(rhs)))); } inline double PPC_PsMulNoNiInline(double lhs, double rhs) { return PpcM128ToPsInline(PpcMulPairInline(PpcPsToM128Inline(lhs), PpcPsToM128Inline(rhs))); } // The paired madd family lowers to one hardware FMA. Semantics match the scalar lanes exactly: a // single fused rounding per lane (std::fma(float) == vfmaddps per lane), and // the negate-unless-NaN behavior of the nmadd/nmsub forms is expressed with // PpcNegateNonNanLanesInline. NI flushing is handled by MXCSR (see // MkwApplyHostNiMode), so the NI and NoNi entry points are identical here. // Fused multiply-add/subtract on a pair. NEON's vfma_f32(acc, a, b) = acc + a*b has an // accumulator-first operand order, unlike x86's _mm_fmadd_ps(a, b, c) = a*b + c - msub is // therefore an fma against a negated accumulator. That vneg would flip a sole-NaN subtractor's // sign, which x86's vfmsub does not do, so the NaN resolver receives the original subtractor. inline PpcPairVec PpcFmaddPairInline(PpcPairVec multiplicand, PpcPairVec multiplier, PpcPairVec addend) { #if defined(__x86_64__) return _mm_fmadd_ps(multiplicand, multiplier, addend); #elif defined(__aarch64__) const PpcPairVec result = vfma_f32(addend, multiplicand, multiplier); if (PpcPairNanLaneBitsInline(result) != 0) [[unlikely]] return PpcResolveNanLanes3Inline(result, multiplicand, multiplier, addend); return result; #endif } inline PpcPairVec PpcFmsubPairInline(PpcPairVec multiplicand, PpcPairVec multiplier, PpcPairVec subtractor) { #if defined(__x86_64__) return _mm_fmsub_ps(multiplicand, multiplier, subtractor); #elif defined(__aarch64__) const PpcPairVec result = vfma_f32(vneg_f32(subtractor), multiplicand, multiplier); if (PpcPairNanLaneBitsInline(result) != 0) [[unlikely]] return PpcResolveNanLanes3Inline(result, multiplicand, multiplier, subtractor); return result; #endif } inline double PPC_PsMsubInline(double multiplicand, double multiplier, double subtractor) { return PpcM128ToPsInline(PpcFmsubPairInline( PpcPsToM128Inline(multiplicand), PpcPsToM128Inline(multiplier), PpcPsToM128Inline(subtractor))); } inline double PPC_PsMsubNoNiInline(double multiplicand, double multiplier, double subtractor) { return PPC_PsMsubInline(multiplicand, multiplier, subtractor); } inline double PPC_PsMaddInline(double multiplicand, double multiplier, double addend) { return PpcM128ToPsInline(PpcFmaddPairInline( PpcPsToM128Inline(multiplicand), PpcPsToM128Inline(multiplier), PpcPsToM128Inline(addend))); } inline double PPC_PsMaddNoNiInline(double multiplicand, double multiplier, double addend) { return PPC_PsMaddInline(multiplicand, multiplier, addend); } inline double PPC_PsMadds0Inline(double multiplicand, double multiplier, double addend) { return PpcM128ToPsInline(PpcFmaddPairInline( PpcPsToM128Inline(multiplicand), PpcBroadcastPs0Inline(multiplier), PpcPsToM128Inline(addend))); } inline double PPC_PsMadds1Inline(double multiplicand, double multiplier, double addend) { return PpcM128ToPsInline(PpcFmaddPairInline( PpcPsToM128Inline(multiplicand), PpcBroadcastPs1Inline(multiplier), PpcPsToM128Inline(addend))); } inline double PPC_PsNmsubInline(double multiplicand, double multiplier, double subtractor) { return PpcM128ToPsInline(PpcNegateNonNanLanesInline(PpcFmsubPairInline( PpcPsToM128Inline(multiplicand), PpcPsToM128Inline(multiplier), PpcPsToM128Inline(subtractor)))); } inline double PPC_PsNmsubNoNiInline(double multiplicand, double multiplier, double subtractor) { return PPC_PsNmsubInline(multiplicand, multiplier, subtractor); } inline double PPC_PsNmaddInline(double multiplicand, double multiplier, double addend) { return PpcM128ToPsInline(PpcNegateNonNanLanesInline(PpcFmaddPairInline( PpcPsToM128Inline(multiplicand), PpcPsToM128Inline(multiplier), PpcPsToM128Inline(addend)))); } inline double PPC_PsMuls0Inline(double aValue, double cValue) { return PpcFlushPairedForNiInline(PpcM128ToPsInline( PpcMulPairInline(PpcPsToM128Inline(aValue), PpcBroadcastPs0Inline(cValue)))); } inline double PPC_PsMuls1Inline(double aValue, double cValue) { return PpcFlushPairedForNiInline(PpcM128ToPsInline( PpcMulPairInline(PpcPsToM128Inline(aValue), PpcBroadcastPs1Inline(cValue)))); } inline PPC_FPR PpcMakePairedResultInline(float ps0, float ps1); inline double PPC_PsFromScalarInline(double value) { // Representation conversion, not an architectural operation: Gekko has no // "scalar to paired" instruction, so there is no NI rounding point here. // If the scalar is a single-denormal it stays one; MXCSR.DAZ flushes it as // an input at the next real arithmetic op, exactly like the hardware. const float single = static_cast(value); return PpcPackPairedInline(single, single); } inline double PPC_PsFromScalarNoNiInline(double value) { const float single = static_cast(value); return PpcPackPairedInline(single, single); } inline double PPC_PsToScalarInline(double value) { return static_cast(PpcGetPs0Inline(value)); } // ps_merge* are pure lane selections (result.ps0 from frA, result.ps1 from frB). On x86 two // shuffles build the result bit-exact without round-tripping through the pack helper; NEON has // no equally cheap 2-lane general shuffle, so its branch expresses the exact same selection // (verified against the x86 comments below, lane for lane) directly in terms of the portable // Get/Pack accessors instead. inline double PPC_PsMerge00Inline(double aValue, double bValue) { // result.ps0 = a.ps0, result.ps1 = b.ps0 (lane0 = b.ps0/b lane1, lane1 = a.ps0/a lane1) #if defined(__x86_64__) const __m128 gathered = _mm_shuffle_ps( PpcPsToM128Inline(bValue), PpcPsToM128Inline(aValue), _MM_SHUFFLE(1, 1, 1, 1)); return PpcM128ToPsInline(_mm_shuffle_ps(gathered, gathered, _MM_SHUFFLE(0, 0, 2, 0))); #elif defined(__aarch64__) return PpcPackPairedInline(PpcGetPs0Inline(aValue), PpcGetPs0Inline(bValue)); #endif } inline double PPC_PsMerge01Inline(double aValue, double bValue) { // result.ps0 = a.ps0, result.ps1 = b.ps1 (lane0 = b.ps1/b lane0, lane1 = a.ps0/a lane1) #if defined(__x86_64__) const __m128 gathered = _mm_shuffle_ps( PpcPsToM128Inline(bValue), PpcPsToM128Inline(aValue), _MM_SHUFFLE(1, 1, 0, 0)); return PpcM128ToPsInline(_mm_shuffle_ps(gathered, gathered, _MM_SHUFFLE(0, 0, 2, 0))); #elif defined(__aarch64__) return PpcPackPairedInline(PpcGetPs0Inline(aValue), PpcGetPs1Inline(bValue)); #endif } inline double PPC_PsMerge10Inline(double aValue, double bValue) { // result.ps0 = a.ps1, result.ps1 = b.ps0 (lane0 = b.ps0/b lane1, lane1 = a.ps1/a lane0) #if defined(__x86_64__) const __m128 gathered = _mm_shuffle_ps( PpcPsToM128Inline(bValue), PpcPsToM128Inline(aValue), _MM_SHUFFLE(0, 0, 1, 1)); return PpcM128ToPsInline(_mm_shuffle_ps(gathered, gathered, _MM_SHUFFLE(0, 0, 2, 0))); #elif defined(__aarch64__) return PpcPackPairedInline(PpcGetPs1Inline(aValue), PpcGetPs0Inline(bValue)); #endif } inline double PPC_PsMerge11Inline(double aValue, double bValue) { // result.ps0 = a.ps1, result.ps1 = b.ps1 (lane0 = b.ps1/b lane0, lane1 = a.ps1/a lane0): // plain unpcklps on x86. #if defined(__x86_64__) return PpcM128ToPsInline( _mm_unpacklo_ps(PpcPsToM128Inline(bValue), PpcPsToM128Inline(aValue))); #elif defined(__aarch64__) return PpcPackPairedInline(PpcGetPs1Inline(aValue), PpcGetPs1Inline(bValue)); #endif } inline PpcPairVec PpcAddPairInline(PpcPairVec lhs, PpcPairVec rhs) { #if defined(__x86_64__) return _mm_add_ps(lhs, rhs); #elif defined(__aarch64__) const PpcPairVec result = vadd_f32(lhs, rhs); if (PpcPairNanLaneBitsInline(result) != 0) [[unlikely]] return PpcResolveNanLanesInline(result, lhs, rhs); return result; #endif } inline PpcPairVec PpcSubPairInline(PpcPairVec lhs, PpcPairVec rhs) { #if defined(__x86_64__) return _mm_sub_ps(lhs, rhs); #elif defined(__aarch64__) const PpcPairVec result = vsub_f32(lhs, rhs); if (PpcPairNanLaneBitsInline(result) != 0) [[unlikely]] return PpcResolveNanLanesInline(result, lhs, rhs); return result; #endif } inline PpcPairVec PpcDivPairInline(PpcPairVec lhs, PpcPairVec rhs) { #if defined(__x86_64__) return _mm_div_ps(lhs, rhs); #elif defined(__aarch64__) const PpcPairVec result = vdiv_f32(lhs, rhs); if (PpcPairNanLaneBitsInline(result) != 0) [[unlikely]] return PpcResolveNanLanesInline(result, lhs, rhs); return result; #endif } inline double PPC_PsAddInline(double aValue, double bValue) { return PpcFlushPairedForNiInline( PpcM128ToPsInline(PpcAddPairInline(PpcPsToM128Inline(aValue), PpcPsToM128Inline(bValue)))); } inline double PPC_PsAddNoNiInline(double aValue, double bValue) { return PpcM128ToPsInline( PpcAddPairInline(PpcPsToM128Inline(aValue), PpcPsToM128Inline(bValue))); } inline double PPC_PsSelInline(double lhsValue, double controlValue, double rhsValue) { const float lhs0 = PpcGetPs0Inline(lhsValue); const float lhs1 = PpcGetPs1Inline(lhsValue); const float control0 = PpcGetPs0Inline(controlValue); const float control1 = PpcGetPs1Inline(controlValue); const float rhs0 = PpcGetPs0Inline(rhsValue); const float rhs1 = PpcGetPs1Inline(rhsValue); return PpcPackPairedInline( control0 >= -0.0f ? lhs0 : rhs0, control1 >= -0.0f ? lhs1 : rhs1); } inline double PPC_PsSubInline(double aValue, double bValue) { return PpcFlushPairedForNiInline( PpcM128ToPsInline(PpcSubPairInline(PpcPsToM128Inline(aValue), PpcPsToM128Inline(bValue)))); } inline double PPC_PsSubNoNiInline(double aValue, double bValue) { return PpcM128ToPsInline( PpcSubPairInline(PpcPsToM128Inline(aValue), PpcPsToM128Inline(bValue))); } inline double PPC_PsDivInline(double aValue, double bValue) { return PpcFlushPairedForNiInline( PpcM128ToPsInline(PpcDivPairInline(PpcPsToM128Inline(aValue), PpcPsToM128Inline(bValue)))); } inline double PPC_PsNegInline(double value) { return PpcPackPairedInline(-PpcGetPs0Inline(value), -PpcGetPs1Inline(value)); } inline double PPC_PsAbsInline(double value) { return PpcPackPairedInline(std::abs(PpcGetPs0Inline(value)), std::abs(PpcGetPs1Inline(value))); } inline double PPC_PsSum0Inline(double aValue, double bValue, double cValue) { return PpcPackPairedInline( PpcForceSingleValueInline(static_cast(PpcGetPs0Inline(aValue)) + PpcGetPs1Inline(bValue)), PpcFlushSingleForNiInline(PpcGetPs1Inline(cValue))); } inline double PPC_PsSum1Inline(double aValue, double bValue, double cValue) { return PpcPackPairedInline( PpcFlushSingleForNiInline(PpcGetPs0Inline(cValue)), PpcForceSingleValueInline(static_cast(PpcGetPs0Inline(aValue)) + PpcGetPs1Inline(bValue))); } inline uint32_t PpcConvertToSingleFTZInline(uint64_t value) { const uint32_t exp = static_cast((value >> 52) & 0x7FFu); if (exp > 896u || (value & 0x7FFFFFFFFFFFFFFFULL) == 0) { return static_cast(((value >> 32) & 0xC0000000ULL) | ((value >> 29) & 0x3FFFFFFFULL)); } return static_cast((value >> 32) & 0x80000000ULL); } inline uint64_t PpcConvertToDoubleBitsInline(uint32_t value) { uint64_t x = value; uint64_t exp = (x >> 23) & 0xFFu; uint64_t frac = x & 0x007FFFFFu; if (exp > 0 && exp < 255) { const uint64_t y = !(exp >> 7); const uint64_t z = (y << 61) | (y << 60) | (y << 59); return ((x & 0xC0000000ULL) << 32) | z | ((x & 0x3FFFFFFFULL) << 29); } if (exp == 0 && frac != 0) { exp = 1023 - 126; do { frac <<= 1; --exp; } while ((frac & 0x00800000u) == 0); return ((x & 0x80000000ULL) << 32) | (exp << 52) | ((frac & 0x007FFFFFULL) << 29); } const uint64_t y = exp >> 7; const uint64_t z = (y << 61) | (y << 60) | (y << 59); return ((x & 0xC0000000ULL) << 32) | z | ((x & 0x3FFFFFFFULL) << 29); } inline double PpcPackPairedBitsInline(uint32_t ps0, uint32_t ps1) { return PpcBitCastToDoubleInline((static_cast(ps0) << 32) | ps1); } inline PPC_FPR PpcMakePairedResultInline(float ps0, float ps1) { PPC_FPR result{}; result.paired.ps0 = ps0; result.paired.ps1 = ps1; return result; } extern "C" void PPC_Mtfsf(uint32_t fieldMask, double source); extern "C" void PPC_Mtfsfi(uint32_t field, uint32_t value); extern "C" void PPC_Mtfsb0(uint32_t bit); extern "C" void PPC_Mtfsb1(uint32_t bit); extern "C" double PPC_Mffs(); extern "C" double PPC_PsAdd(double lhs, double rhs); extern "C" double PPC_PsSub(double lhs, double rhs); extern "C" double PPC_PsDiv(double lhs, double rhs); extern "C" double PPC_PsNeg(double value); extern "C" double PPC_PsMul(double lhs, double rhs); extern "C" double PPC_PsMsub(double lhs, double mul, double sub); extern "C" double PPC_PsMadd(double lhs, double mul, double add); extern "C" double PPC_PsNmsub(double lhs, double mul, double sub); extern "C" double PPC_PsMadds0(double lhs, double mul, double add); extern "C" double PPC_PsMadds1(double lhs, double mul, double add); extern "C" double PPC_PsNmadd(double lhs, double mul, double add); extern "C" double PPC_PsSel(double lhs, double control, double rhs); // ps_res is hot in the THP dequant path (148 static call sites in the IDCT // region); the out-of-line definition cost a call + full spill barrier per // use, so it is defined inline here. The estimate logic is byte-identical to // the old ppc_helpers.cpp body (both used PpcApproximateReciprocalInline). extern "C" inline double PPC_PsRes(double value) { const float ps0 = static_cast( PpcApproximateReciprocalInline(static_cast(PpcGetPs0Inline(value)))); const float ps1 = static_cast( PpcApproximateReciprocalInline(static_cast(PpcGetPs1Inline(value)))); return PpcPackPairedInline(ps0, ps1); } extern "C" double PPC_PsRsqrte(double value); // Pack a scalar double into a paired-single FPR value (ps0=ps1=float(value)). extern "C" double PPC_PsFromScalar(double value); // Extract the ps0 lane as a scalar double (used to feed single-precision ops). extern "C" double PPC_PsToScalar(double value); extern "C" double PPC_PsMerge00(double a, double b); extern "C" double PPC_PsMerge01(double a, double b); extern "C" double PPC_PsMerge10(double a, double b); extern "C" double PPC_PsMerge11(double a, double b); extern "C" double PPC_Fsqrt(double value); // Hot scalar float helpers, defined inline so the compiler can see through them instead of // taking a cross-TU caller-saved register barrier at ~2,500 call sites in the hottest float // code in the game (bodies moved verbatim from ppc_helpers.cpp/fpu_helpers.cpp). PPC_PsRes // above uses the same pattern. extern "C" inline double PPC_Fres(double value) { // The generated paired/scalar boundary presents fres as a packed input and // expects its architecturally replicated single result in the same format. const float result = static_cast( PpcApproximateReciprocalInline(static_cast(PpcGetPs0Inline(value)))); return PpcPackPairedInline(result, result); } extern "C" inline double PPC_Frsqrte(double value) { // frsqrte (opcode 63) takes the full scalar-double input (generated code normalizes // scalar/paired ownership beforehand) and returns exact Gekko estimate bits only; FPSCR/ // Rc/FPRF updates need instruction-level context this value-only helper doesn't have. return PpcApproximateReciprocalSquareRootInline(value); } extern "C" inline double PPC_Fsel(double control, double negative, double positive) { // Dolphin models fsel/ps_sel as "fra >= -0.0 ? frC : frB". // That comparison deliberately sends unordered/NaN controls to frB. return (control >= -0.0) ? positive : negative; } // PowerPC fctiwz instruction: Float Convert to Integer Word with Round toward // Zero. The result goes in the LOWER 32 bits of the FPR (bits 32-63); the upper // 32 bits are undefined. When the value is stored via stfd and reloaded via lwz // at offset +4, the integer is correctly retrieved. extern "C" inline double PPC_Fctiwz(double value) { return PpcPackIntegerWordInline(PpcClampIntegerWordInline(value)); } extern "C" double PPC_Fmadd(double multiplicand, double multiplier, double addend); extern "C" double PPC_Fmsub(double multiplicand, double multiplier, double subtractor); extern "C" double PPC_Fnmadd(double multiplicand, double multiplier, double addend); extern "C" double PPC_Fnmsub(double multiplicand, double multiplier, double subtractor); // Single-precision scalar helpers (fadds/fsubs/fmuls/fdivs and fused variants). extern "C" double PPC_Fadds(double a, double b); extern "C" double PPC_Fsubs(double a, double b); extern "C" double PPC_Fmuls(double a, double b); extern "C" double PPC_Fdivs(double a, double b); // The fused single-precision family is a thin wrapper over the Ppc*Inline // bodies above; keeping the wrapper out of line meant the wrapper itself was // the register barrier. See the block at PPC_Fres for why there is no LTO to // fall back on. extern "C" inline double PPC_Fmadds(double multiplicand, double multiplier, double addend) { return PpcFmaddsInline(multiplicand, multiplier, addend); } extern "C" inline double PPC_Fmsubs(double multiplicand, double multiplier, double subtractor) { return PpcFmsubsInline(multiplicand, multiplier, subtractor); } extern "C" inline double PPC_Fnmadds(double multiplicand, double multiplier, double addend) { return PpcFnmaddsInline(multiplicand, multiplier, addend); } extern "C" inline double PPC_Fnmsubs(double multiplicand, double multiplier, double subtractor) { return PpcFnmsubsInline(multiplicand, multiplier, subtractor); } extern "C" double PPC_Fctiw(double value); // Convert float to int using FPSCR rounding mode extern "C" void PPC_Stfiwx(uint32_t addr, double fprValue); // Store Float as Integer Word (indexed) extern "C" double PPC_PsSum0(double a, double b, double c); extern "C" double PPC_PsSum1(double a, double b, double c); extern "C" double PPC_PsMuls0(double a, double c); extern "C" double PPC_PsMuls1(double a, double c); extern "C" double PPC_PsAbs(double value); // Floating-point comparison helper (fcmpu/fcmpo) extern "C" void PPC_Fcmp(uint32_t crField, double a, double b); extern "C" void PPC_PsCmpo0(uint32_t crField, double a, double b); extern "C" void PPC_PsCmpu0(uint32_t crField, double a, double b); extern "C" void PPC_PsCmpo1(uint32_t crField, double a, double b); extern "C" void PPC_PsCmpu1(uint32_t crField, double a, double b); extern "C" double PPC_PsNabs(double value); extern "C" double PPC_PsMr(double value);