From a14712c65ef8a2e408d00bcf930101aa38f980cc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Henrik=20Rydg=C3=A5rd?= Date: Sat, 3 Oct 2026 11:00:42 -0600 Subject: [PATCH] SoftGPU: Skin a vertex without per-operation fallback checks Every GEAdd4 and GEMulFloat24x4 checked its lanes for the cases it can't do and branched to a fallback, a vector-to-scalar move and a branch per operation, about 14% of skinning. The unchecked variants only collect those lanes; a vertex where any appears is skinned again with the checked ones. The products use GEMulFloat24x4 (the same as a one-term GERowSum4 here), and the position's and normal's add chains are interleaved. Skinning is about 20% faster. Co-Authored-By: Claude Opus 5.5 (1M context) --- GPU/Software/GEMath.h | 26 ++++++++-- GPU/Software/TransformUnit.cpp | 94 ++++++++++++++++++---------------- 2 files changed, 73 insertions(+), 47 deletions(-) diff --git a/GPU/Software/GEMath.h b/GPU/Software/GEMath.h index cbad3e7c9a..f7273629fd 100644 --- a/GPU/Software/GEMath.h +++ b/GPU/Software/GEMath.h @@ -251,7 +251,8 @@ Vec4F32 GEAdd4Fallback(Vec4F32 a, Vec4F32 b, Vec4S32 lanes, Vec4F32 result); // Four of GEAdd at once: each term goes to 16-bit fixed point at the larger exponent by scaling with a power // of two and truncating toward zero, which drops what GEAdd's masks clear, then the sum is exact. -inline Vec4F32 GEAdd4(Vec4F32 a, Vec4F32 b) { +// GEAdd4 without its fallback: the lanes that need it are added to bad, and their results are wrong. +inline Vec4F32 GEAdd4Unchecked(Vec4F32 a, Vec4F32 b, Vec4S32 &bad) { const Vec4S32 expMask = Vec4S32::Splat(0x7F800000); const Vec4S32 ba = Vec4S32FromBits(a); const Vec4S32 bb = Vec4S32FromBits(b); @@ -269,7 +270,13 @@ inline Vec4F32 GEAdd4(Vec4F32 a, Vec4F32 b) { const Vec4S32 none = e.CompareEq(Vec4S32::Zero()); const Vec4F32 sum = Vec4F32FromS32(Vec4S32FromF32(za * down) + Vec4S32FromF32(zb * down)) * up; const Vec4F32 result = Vec4F32FromBits(Vec4S32FromBits(sum).AndNot(none)); - const Vec4S32 outside = e.CompareLt(Vec4S32::Splat(16 << 23)).AndNot(none) | e.CompareEq(expMask); + bad = bad | e.CompareLt(Vec4S32::Splat(16 << 23)).AndNot(none) | e.CompareEq(expMask); + return result; +} + +inline Vec4F32 GEAdd4(Vec4F32 a, Vec4F32 b) { + Vec4S32 outside = Vec4S32::Zero(); + const Vec4F32 result = GEAdd4Unchecked(a, b, outside); if (AnyCompareBitsSet(outside)) return GEAdd4Fallback(a, b, outside, result); return result; @@ -282,7 +289,8 @@ Vec4F32 GEMulFloat24x4Fallback(Vec4F32 a, Vec4F32 b, Vec4S32 lanes, Vec4F32 resu // Per lane, the product of a and b truncated to float24, as ProductToFloat24((double)a * b) with both // truncated to float24 first, except that a zero operand gives +0. With 15 fraction bits each, the // significand product truncated to 17 bits is 32768 + a' + b' + (a' b' >> 15), as in GERowSum4. -inline Vec4F32 GEMulFloat24x4(Vec4F32 a, Vec4F32 b) { +// GEMulFloat24x4 without its fallback, as GEAdd4Unchecked. +inline Vec4F32 GEMulFloat24x4Unchecked(Vec4F32 a, Vec4F32 b, Vec4S32 &bad) { const Vec4S32 expMask = Vec4S32::Splat(0x7F800000); const Vec4S32 fracMask = Vec4S32::Splat(0x7FFF); const Vec4S32 aBits = Vec4S32FromBits(a) & Vec4S32::Splat((int)0xFFFFFF00); @@ -300,7 +308,13 @@ inline Vec4F32 GEMulFloat24x4(Vec4F32 a, Vec4F32 b) { const Vec4S32 zero = (aBits & Vec4S32::Splat(0x7FFFFFFF)).CompareEq(Vec4S32::Zero()) | (bBits & Vec4S32::Splat(0x7FFFFFFF)).CompareEq(Vec4S32::Zero()); const Vec4F32 result = Vec4F32FromBits((((fbits + lsb.Shl<23>()) & Vec4S32::Splat((int)0xFFFFFF00)) | sign).AndNot(zero)); const Vec4S32 special = ea.CompareEq(Vec4S32::Zero()) | eb.CompareEq(Vec4S32::Zero()) | ea.CompareEq(Vec4S32::Splat(255)) | eb.CompareEq(Vec4S32::Splat(255)); - const Vec4S32 outside = (special | resultExp.CompareLt(Vec4S32::Splat(1)) | resultExp.CompareGt(Vec4S32::Splat(254))).AndNot(zero); + bad = bad | (special | resultExp.CompareLt(Vec4S32::Splat(1)) | resultExp.CompareGt(Vec4S32::Splat(254))).AndNot(zero); + return result; +} + +inline Vec4F32 GEMulFloat24x4(Vec4F32 a, Vec4F32 b) { + Vec4S32 outside = Vec4S32::Zero(); + const Vec4F32 result = GEMulFloat24x4Unchecked(a, b, outside); if (AnyCompareBitsSet(outside)) return GEMulFloat24x4Fallback(a, b, outside, result); return result; @@ -310,6 +324,10 @@ inline Vec4F32 GEMulFloat24x4(Vec4F32 a, Vec4F32 b) { inline Vec4F32 GEAddFloat24x4(Vec4F32 a, Vec4F32 b) { return TruncateToFloat24x4(GEAdd4(a, b)); } + +inline Vec4F32 GEAddFloat24x4Unchecked(Vec4F32 a, Vec4F32 b, Vec4S32 &bad) { + return TruncateToFloat24x4(GEAdd4Unchecked(a, b, bad)); +} float GEDot(const Vec3f &a, const Vec3f &b); float GENormalize(Vec3f &v); diff --git a/GPU/Software/TransformUnit.cpp b/GPU/Software/TransformUnit.cpp index c807b77265..2636803f2b 100644 --- a/GPU/Software/TransformUnit.cpp +++ b/GPU/Software/TransformUnit.cpp @@ -616,57 +616,65 @@ static float GEMorphComponent(const VertexDecoder &dec, const u8 *in, int off, i static void ApplyGESkinning(u8 *decoded, const VertexDecoder &dec, const u8 *raw, int count) { const DecVtxFormat &fmt = dec.GetDecVtxFmt(); const int nweights = dec.nweights; - // The x, y and z components go in lanes 0-2. Each one is a chain of GEAdds in a fixed order: per bone, the - // translation, then the three products. GEAdd drops a GEProduct's 17th bit, so float24 products (one-term - // row sums) give the same sums. - auto product = [](float a, Vec4F32 b) { - return GERowSum4<1>(Vec4F32::Splat(a), &b); - }; + const bool hasPos = dec.pos != 0; + const bool hasNrm = dec.nrm && fmt.nrmfmt == DEC_FLOAT_3; for (int v = 0; v < count; ++v) { const u8 *in = raw + v * dec.VertexSize(); u8 *out = decoded + v * fmt.stride; auto component = [&](int off, int c, auto read) { return dec.morphcount > 1 ? GEMorphComponent(dec, in, off, c, read) : read(in + off, c); }; - // Each bone's rows (matrix entries 3j..3j+2, the translation j = 3), times its weight. Lane 3 reads past - // the row (and past the matrices, for the last translation). - Vec4F32 bones[8][4]; - bool used[8]; - for (int b = 0; b < nweights; ++b) { - const float w = TruncateToFloat24(component(dec.weightoff, b, [&](const u8 *p, int i) { return ReadRawWeight(p, dec.weighttype, i); })); - used[b] = w != 0.0f; - if (!used[b]) - continue; - const float *m = gstate.boneMatrix + b * 12; - for (int j = 0; j < 4; ++j) { - bones[b][j] = GEMulFloat24x4(Vec4F32::Splat(w), Vec4F32::Load(m + 3 * j).WithLane3Zero()); - } - } - auto skin = [&](const float src[3], bool translate, u8 *dst) { - Vec4F32 acc = Vec4F32::Zero(); - for (int b = 0; b < nweights; ++b) { - if (!used[b]) - continue; - if (translate) - acc = GEAddFloat24x4(acc, bones[b][3]); - for (int j = 0; j < 3; ++j) - acc = GEAddFloat24x4(acc, product(src[j], bones[b][j])); - } - alignas(16) float result[4]; - acc.Store(result); - memcpy(dst, result, 3 * sizeof(float)); - }; - if (dec.pos) { - float pos[3]; - for (int i = 0; i < 3; ++i) + float weights[8]; + for (int b = 0; b < nweights; ++b) + weights[b] = TruncateToFloat24(component(dec.weightoff, b, [&](const u8 *p, int i) { return ReadRawWeight(p, dec.weighttype, i); })); + float pos[3]{}, nrm[3]{}; + for (int i = 0; i < 3; ++i) { + if (hasPos) pos[i] = component(dec.posoff, i, [&](const u8 *p, int c) { return ReadRawComponent(p, dec.pos, c); }); - skin(pos, true, out + fmt.posoff); - } - if (dec.nrm && fmt.nrmfmt == DEC_FLOAT_3) { - float nrm[3]; - for (int i = 0; i < 3; ++i) + if (hasNrm) nrm[i] = component(dec.nrmoff, i, [&](const u8 *p, int c) { return ReadRawComponent(p, dec.nrm, c); }); - skin(nrm, false, out + fmt.nrmoff); + } + + // The x, y and z components go in lanes 0-2. Each one is a chain of GEAdds in a fixed order: per bone, the + // translation, then the three products. GEAdd drops a GEProduct's 17th bit, so float24 products give the + // same sums. The position's and the normal's chains are independent, interleaved. + Vec4F32 accPos, accNrm; + auto skin = [&](auto mul, auto add) { + accPos = Vec4F32::Zero(); + accNrm = Vec4F32::Zero(); + for (int b = 0; b < nweights; ++b) { + const float w = weights[b]; + if (w == 0.0f) + continue; + // The bone's rows (matrix entries 3j..3j+2, the translation j = 3), times its weight. + const float *m = gstate.boneMatrix + b * 12; + const Vec4F32 wv = Vec4F32::Splat(w); + Vec4F32 rows[4]; + for (int j = 0; j < 4; ++j) + rows[j] = mul(wv, Vec4F32::Load(m + 3 * j).WithLane3Zero()); + accPos = add(accPos, rows[3]); + for (int j = 0; j < 3; ++j) { + accPos = add(accPos, mul(Vec4F32::Splat(pos[j]), rows[j])); + if (hasNrm) + accNrm = add(accNrm, mul(Vec4F32::Splat(nrm[j]), rows[j])); + } + } + }; + // Without the fallbacks first: they're rarely needed, and then the vertex is done again with them. + Vec4S32 bad = Vec4S32::Zero(); + skin([&](Vec4F32 a, Vec4F32 b) { return GEMulFloat24x4Unchecked(a, b, bad); }, + [&](Vec4F32 a, Vec4F32 b) { return GEAddFloat24x4Unchecked(a, b, bad); }); + if (AnyCompareBitsSet(bad)) + skin(GEMulFloat24x4, GEAddFloat24x4); + + alignas(16) float result[4]; + if (hasPos) { + accPos.Store(result); + memcpy(out + fmt.posoff, result, 3 * sizeof(float)); + } + if (hasNrm) { + accNrm.Store(result); + memcpy(out + fmt.nrmoff, result, 3 * sizeof(float)); } } }