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) <noreply@anthropic.com>
This commit is contained in:
Henrik RydgårdandClaude Opus 5.5 committed 2026-10-09 09:07:12 +02:00
1 parent d272703721
commit a14712c65e
2 files changed
+73 -47

No files matched your search

+22 -4
View File
@@ -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);
+51 -43
View File
@@ -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));
}
}
}