From 2cca146b7e196df1e8d1ce753000a3dd7b95e88e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Henrik=20Rydg=C3=A5rd?= Date: Sat, 3 Oct 2026 10:48:49 -0600 Subject: [PATCH 1/5] SoftGPU: Filter the four-pixel bilinear samples a channel per vector SampleLinearQuadT filtered each pixel's four texels as 16-bit channels in a 64-bit integer, then transposed the results into a vector per channel for the texture function. Now the lerps run on a channel of all four pixels at a time, producing that layout directly. God of War about 6% faster. Co-Authored-By: Claude Opus 5.5 (1M context) --- GPU/Software/Sampler.cpp | 80 +++++++++++++++++++++++++--------------- 1 file changed, 51 insertions(+), 29 deletions(-) diff --git a/GPU/Software/Sampler.cpp b/GPU/Software/Sampler.cpp index 216c7219a4..3df075998b 100644 --- a/GPU/Software/Sampler.cpp +++ b/GPU/Software/Sampler.cpp @@ -962,23 +962,16 @@ static inline Vec4S32 ColumnOffsets4(Vec4S32 u) { return swizzled ? b.Shr<4>().Shl<7>() + (b & Vec4S32::Splat(15)) : b; } -// SampleLinearLevelT for four pixels at the same level: the coordinates and offsets in vector lanes, the -// texel reads one by one. +// The four texels of SampleLinearLevelT for four pixels at the same level (texels[pixel][tl, tr, bl, br]), +// the coordinates and offsets in vector lanes, the texel reads one by one. template -static inline void SampleLinearLevel4T(Vec4F32 s, Vec4F32 t, const u8 *tptr, int bufw, int level, const SamplerID &samplerID, uint64_t out[4]) { - if (!tptr) { - out[0] = out[1] = out[2] = out[3] = 0; - return; - } - Vec4S32 u0, u1, v0, v1, fracU, fracV; +static inline void ReadTexels4T(Vec4F32 s, Vec4F32 t, const u8 *tptr, int bufw, int level, const SamplerID &samplerID, uint32_t texels[4][4], Vec4S32 &fracU, Vec4S32 &fracV) { + Vec4S32 u0, u1, v0, v1; TexelPairs4(s, samplerID.cached.sizes[level].w, samplerID.clampS, u0, u1, fracU); TexelPairs4(t, samplerID.cached.sizes[level].h, samplerID.clampT, v0, v1, fracV); - alignas(16) int us0[4], us1[4], fu[4], fv[4]; + alignas(16) int us0[4], us1[4]; u0.Store(us0); u1.Store(us1); - fracU.Store(fu); - fracV.Store(fv); - uint32_t texels[4][4]; if constexpr (fmt == GE_TFMT_DXT1 || fmt == GE_TFMT_DXT3 || fmt == GE_TFMT_DXT5) { alignas(16) int vs0[4], vs1[4]; v0.Store(vs0); @@ -1008,14 +1001,49 @@ static inline void SampleLinearLevel4T(Vec4F32 s, Vec4F32 t, const u8 *tptr, int texels[i][3] = ReadTexelT(tptr + (uint32_t)br[i], us1[i], clutOffset, samplerID); } } +} + +// LerpSpread of one channel in vector lanes. +static inline Vec4S32 LerpLanes(Vec4S32 a, Vec4S32 b, Vec4S32 f) { + return (a * (Vec4S32::Splat(16) - f) + b * f).Shr<4>(); +} + +// SampleLinearLevelT for four pixels at the same level, a channel per vector (as TextureFunction4 takes them). +template +static inline void SampleLinearLevel4LanesT(Vec4F32 s, Vec4F32 t, const u8 *tptr, int bufw, int level, const SamplerID &samplerID, Vec4S32 out[4]) { + if (!tptr) { + out[0] = out[1] = out[2] = out[3] = Vec4S32::Zero(); + return; + } + alignas(16) uint32_t texels[4][4]; + Vec4S32 fracU, fracV; + ReadTexels4T(s, t, tptr, bufw, level, samplerID, texels, fracU, fracV); + // Texels by corner, each vector a corner's texel for the four pixels. + alignas(16) int corners[4][4]; for (int i = 0; i < 4; ++i) { + for (int k = 0; k < 4; ++k) + corners[k][i] = (int)texels[i][k]; + } + const Vec4S32 tl = Vec4S32::LoadAligned(corners[0]), tr = Vec4S32::LoadAligned(corners[1]); + const Vec4S32 bl = Vec4S32::LoadAligned(corners[2]), br = Vec4S32::LoadAligned(corners[3]); + const Vec4S32 byteMask = Vec4S32::Splat(0xFF); + auto channel = [&](Vec4S32 v, int c) { + switch (c) { + case 0: return v & byteMask; + case 1: return v.Shr<8>() & byteMask; + case 2: return v.Shr<16>() & byteMask; + default: return v.Shr<24>() & byteMask; + } + }; + for (int c = 0; c < 4; ++c) { // Like the GE: horizontal lerps truncated to 8 bits, then the vertical one (gpu/probe exp52). - const uint64_t top = LerpSpread(SpreadRGBA(texels[i][0]), SpreadRGBA(texels[i][1]), fu[i]); - const uint64_t bot = LerpSpread(SpreadRGBA(texels[i][2]), SpreadRGBA(texels[i][3]), fu[i]); - out[i] = LerpSpread(top, bot, fv[i]); + const Vec4S32 top = LerpLanes(channel(tl, c), channel(tr, c), fracU); + const Vec4S32 bot = LerpLanes(channel(bl, c), channel(br, c), fracU); + out[c] = LerpLanes(top, bot, fracV); } } + // GetTextureFunctionOutput for four pixels, one per lane: prim and tex are R, G, B and A (all 0 to 255). static inline void TextureFunction4(const Vec4S32 prim[4], const Vec4S32 tex[4], const SamplerID &samplerID, Vec4S32 out[4]) { const bool rgba = samplerID.useTextureAlpha; @@ -1087,24 +1115,18 @@ static void SOFTRAST_CALL SampleLinearQuadT(const float *s, const float *t, cons if (sameLevel) { const int l = level[first]; const Vec4F32 sv = Vec4F32::Load(s), tv = Vec4F32::Load(t); - SampleLinearLevel4T(sv, tv, texptr[l], texbufw[l], l, samplerID, c); + Vec4S32 prim[4], tex[4], out[4]; + SampleLinearLevel4LanesT(sv, tv, texptr[l], texbufw[l], l, samplerID, tex); if (levelFrac[first]) { - uint64_t c1[4]; - SampleLinearLevel4T(sv, tv, texptr[l + 1], texbufw[l + 1], l + 1, samplerID, c1); - for (int i = 0; i < 4; ++i) - c[i] = LerpSpread(c[i], c1[i], levelFrac[first]); + Vec4S32 tex1[4]; + SampleLinearLevel4LanesT(sv, tv, texptr[l + 1], texbufw[l + 1], l + 1, samplerID, tex1); + const Vec4S32 f = Vec4S32::Splat(levelFrac[first]); + for (int ch = 0; ch < 4; ++ch) + tex[ch] = LerpLanes(tex[ch], tex1[ch], f); } // The texture function, in lanes too. - alignas(16) int texLanes[4][4]; - for (int i = 0; i < 4; ++i) { - for (int ch = 0; ch < 4; ++ch) - texLanes[ch][i] = (int)((c[i] >> (16 * ch)) & 0xFFFF); - } - Vec4S32 prim[4], tex[4], out[4]; - for (int ch = 0; ch < 4; ++ch) { + for (int ch = 0; ch < 4; ++ch) prim[ch] = Vec4S32::Load(colors + ch * colorStride); - tex[ch] = Vec4S32::Load(texLanes[ch]); - } TextureFunction4(prim, tex, samplerID, out); alignas(16) static const int laneBits[4] = { 1, 2, 4, 8 }; const Vec4S32 keep = (Vec4S32::Splat(active) & Vec4S32::LoadAligned(laneBits)).CompareEq(Vec4S32::Zero()); From 078773216860c755027da8c08133483f68a7f7ab Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Henrik=20Rydg=C3=A5rd?= Date: Sun, 4 Oct 2026 06:35:55 -0600 Subject: [PATCH 2/5] SoftGPU: Skip the level of detail when nothing depends on it Without mip levels and with one filter for both, a span's texturing is the quad sampler at level 0: staged spans call it directly instead of going through ApplyTexturing, which also skips the level calculation. God of War retires 7% fewer instructions. Co-Authored-By: Claude Opus 5.5 (1M context) --- GPU/Software/Rasterizer.cpp | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/GPU/Software/Rasterizer.cpp b/GPU/Software/Rasterizer.cpp index 7463de14ed..0e8bba8370 100644 --- a/GPU/Software/Rasterizer.cpp +++ b/GPU/Software/Rasterizer.cpp @@ -720,7 +720,13 @@ static inline void ApplyTexturing(const RasterizerState &state, int *colors, int int levelFrac; bool bilinear; bool perPixel = state.TexLevelMode() == GE_TEXLEVEL_MODE_SLOPE || (state.TexLevelMode() == GE_TEXLEVEL_MODE_AUTO && autoGrad >= 0.0f); - if (!perPixel) { + if (state.maxTexLevel == 0 && state.minFilt == state.magFilt) { + // Nothing depends on the level of detail: level 0, and either filter is the same. + level = 0; + levelFrac = 0; + bilinear = state.magFilt; + perPixel = false; + } else if (!perPixel) { CalculateSamplingParams(ds, dt, 0.0f, state, level, levelFrac, bilinear); } else if (sameQ) { CalculateSamplingParams(ds, dt, q[0], state, level, levelFrac, bilinear, autoGrad); @@ -1272,6 +1278,9 @@ static void DrawStagedSpans(const StagedSpans &ctx, int count, int chunkX, int64 const RasterizerState &state = *ctx.state; const UVPlanes &uvPlanes = *ctx.uvPlanes; const int64_t qdx = uvPlanes.q.kx * SCREEN_SCALE_FACTOR, sdx = uvPlanes.s.kx * SCREEN_SCALE_FACTOR, tdx = uvPlanes.t.kx * SCREEN_SCALE_FACTOR; + // No mip levels and one filter, bilinear: what ApplyTexturing comes to is the quad sampler at level 0. + const bool directQuad = textured && state.linearQuad && state.maxTexLevel == 0 && state.minFilt == state.magFilt && state.magFilt; + alignas(16) static const int zeros[4] = {}; for (int k = 0; k < count; k += 4, qv += 4 * qdx, sv += 4 * sdx, tv += 4 * tdx) { const Vec4 mask(maskBuf[k], maskBuf[k + 1], maskBuf[k + 2], maskBuf[k + 3]); if (!AnyMask(mask)) @@ -1301,7 +1310,12 @@ static void DrawStagedSpans(const StagedSpans &ctx, int count, int chunkX, int64 s *= 1.0f / (float)(1 << state.samplerID.width0Shift); t *= 1.0f / (float)(1 << state.samplerID.height0Shift); } - ApplyTexturing(state, prim_color, STAGED_CHUNK, mask, s, t, q, 0.0f, 0.0f, ctx.sameQ, ctx.autoGrad); + if (directQuad) { + const int active = (mask[0] >= 0 ? 1 : 0) | (mask[1] >= 0 ? 2 : 0) | (mask[2] >= 0 ? 4 : 0) | (mask[3] >= 0 ? 8 : 0); + state.linearQuad(s.AsArray(), t.AsArray(), zeros, zeros, active, state.texptr, state.texbufw, prim_color, STAGED_CHUNK, state.samplerID); + } else { + ApplyTexturing(state, prim_color, STAGED_CHUNK, mask, s, t, q, 0.0f, 0.0f, ctx.sameQ, ctx.autoGrad); + } } if (ctx.addSecondary) { for (int c = 0; c < 3; ++c) { From cf3848d0734065ea5f8ff7912bffeb4ad7d28617 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Henrik=20Rydg=C3=A5rd?= Date: Sun, 4 Oct 2026 06:40:26 -0600 Subject: [PATCH 3/5] SoftGPU: Lerp two channels per lane in the four-pixel bilinear sampler The texels come out corner by corner, so no transpose, and R with B, G with A, share 32-bit lanes as 16-bit halves: no lerp of 8-bit values needs more than 16 bits, and masking after each shift truncates both halves as before. 2.5% fewer instructions in God of War. Co-Authored-By: Claude Opus 5.5 (1M context) --- GPU/Software/Sampler.cpp | 59 ++++++++++++++++++++-------------------- 1 file changed, 30 insertions(+), 29 deletions(-) diff --git a/GPU/Software/Sampler.cpp b/GPU/Software/Sampler.cpp index 3df075998b..9e0b095e63 100644 --- a/GPU/Software/Sampler.cpp +++ b/GPU/Software/Sampler.cpp @@ -962,7 +962,7 @@ static inline Vec4S32 ColumnOffsets4(Vec4S32 u) { return swizzled ? b.Shr<4>().Shl<7>() + (b & Vec4S32::Splat(15)) : b; } -// The four texels of SampleLinearLevelT for four pixels at the same level (texels[pixel][tl, tr, bl, br]), +// The four texels of SampleLinearLevelT for four pixels at the same level (texels[tl, tr, bl, br][pixel]), // the coordinates and offsets in vector lanes, the texel reads one by one. template static inline void ReadTexels4T(Vec4F32 s, Vec4F32 t, const u8 *tptr, int bufw, int level, const SamplerID &samplerID, uint32_t texels[4][4], Vec4S32 &fracU, Vec4S32 &fracV) { @@ -977,10 +977,10 @@ static inline void ReadTexels4T(Vec4F32 s, Vec4F32 t, const u8 *tptr, int bufw, v0.Store(vs0); v1.Store(vs1); for (int i = 0; i < 4; ++i) { - texels[i][0] = ReadDXTTexelT(tptr, bufw, us0[i], vs0[i]); - texels[i][1] = ReadDXTTexelT(tptr, bufw, us1[i], vs0[i]); - texels[i][2] = ReadDXTTexelT(tptr, bufw, us0[i], vs1[i]); - texels[i][3] = ReadDXTTexelT(tptr, bufw, us1[i], vs1[i]); + texels[0][i] = ReadDXTTexelT(tptr, bufw, us0[i], vs0[i]); + texels[1][i] = ReadDXTTexelT(tptr, bufw, us1[i], vs0[i]); + texels[2][i] = ReadDXTTexelT(tptr, bufw, us0[i], vs1[i]); + texels[3][i] = ReadDXTTexelT(tptr, bufw, us1[i], vs1[i]); } } else { constexpr uint32_t bits = TexelBitsT(); @@ -995,10 +995,10 @@ static inline void ReadTexels4T(Vec4F32 s, Vec4F32 t, const u8 *tptr, int bufw, (row1 + col0).Store(bl); (row1 + col1).Store(br); for (int i = 0; i < 4; ++i) { - texels[i][0] = ReadTexelT(tptr + (uint32_t)tl[i], us0[i], clutOffset, samplerID); - texels[i][1] = ReadTexelT(tptr + (uint32_t)tr[i], us1[i], clutOffset, samplerID); - texels[i][2] = ReadTexelT(tptr + (uint32_t)bl[i], us0[i], clutOffset, samplerID); - texels[i][3] = ReadTexelT(tptr + (uint32_t)br[i], us1[i], clutOffset, samplerID); + texels[0][i] = ReadTexelT(tptr + (uint32_t)tl[i], us0[i], clutOffset, samplerID); + texels[1][i] = ReadTexelT(tptr + (uint32_t)tr[i], us1[i], clutOffset, samplerID); + texels[2][i] = ReadTexelT(tptr + (uint32_t)bl[i], us0[i], clutOffset, samplerID); + texels[3][i] = ReadTexelT(tptr + (uint32_t)br[i], us1[i], clutOffset, samplerID); } } } @@ -1018,29 +1018,30 @@ static inline void SampleLinearLevel4LanesT(Vec4F32 s, Vec4F32 t, const u8 *tptr alignas(16) uint32_t texels[4][4]; Vec4S32 fracU, fracV; ReadTexels4T(s, t, tptr, bufw, level, samplerID, texels, fracU, fracV); - // Texels by corner, each vector a corner's texel for the four pixels. - alignas(16) int corners[4][4]; - for (int i = 0; i < 4; ++i) { - for (int k = 0; k < 4; ++k) - corners[k][i] = (int)texels[i][k]; - } - const Vec4S32 tl = Vec4S32::LoadAligned(corners[0]), tr = Vec4S32::LoadAligned(corners[1]); - const Vec4S32 bl = Vec4S32::LoadAligned(corners[2]), br = Vec4S32::LoadAligned(corners[3]); - const Vec4S32 byteMask = Vec4S32::Splat(0xFF); - auto channel = [&](Vec4S32 v, int c) { - switch (c) { - case 0: return v & byteMask; - case 1: return v.Shr<8>() & byteMask; - case 2: return v.Shr<16>() & byteMask; - default: return v.Shr<24>() & byteMask; - } + // Two channels per lane, R and B (G and A) as 16-bit halves: no lerp of 8-bit values exceeds 16 bits, + // so the halves don't carry into each other, and masking after each shift truncates both to 8 bits. + const Vec4S32 tl = Vec4S32::LoadAligned((const int *)texels[0]), tr = Vec4S32::LoadAligned((const int *)texels[1]); + const Vec4S32 bl = Vec4S32::LoadAligned((const int *)texels[2]), br = Vec4S32::LoadAligned((const int *)texels[3]); + const Vec4S32 halves = Vec4S32::Splat(0x00FF00FF); + const Vec4S32 invU = Vec4S32::Splat(16) - fracU, invV = Vec4S32::Splat(16) - fracV; + auto lerp = [&](Vec4S32 a, Vec4S32 b, Vec4S32 inv, Vec4S32 f) { + return (a.Mul(inv) + b.Mul(f)).Shr<4>() & halves; }; - for (int c = 0; c < 4; ++c) { + Vec4S32 pairs[2]; + for (int h = 0; h < 2; ++h) { + auto part = [&](Vec4S32 v) { + return (h == 0 ? v : v.Shr<8>()) & halves; + }; // Like the GE: horizontal lerps truncated to 8 bits, then the vertical one (gpu/probe exp52). - const Vec4S32 top = LerpLanes(channel(tl, c), channel(tr, c), fracU); - const Vec4S32 bot = LerpLanes(channel(bl, c), channel(br, c), fracU); - out[c] = LerpLanes(top, bot, fracV); + const Vec4S32 top = lerp(part(tl), part(tr), invU, fracU); + const Vec4S32 bot = lerp(part(bl), part(br), invU, fracU); + pairs[h] = lerp(top, bot, invV, fracV); } + const Vec4S32 byteMask = Vec4S32::Splat(0xFF); + out[0] = pairs[0] & byteMask; + out[1] = pairs[1] & byteMask; + out[2] = pairs[0].Shr<16>(); + out[3] = pairs[1].Shr<16>(); } From 9ce774d9f97e614802964a541655f33ec9264514 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Henrik=20Rydg=C3=A5rd?= Date: Sun, 4 Oct 2026 07:05:20 -0600 Subject: [PATCH 4/5] SoftGPU: Let the quad sampler skip the level check when all is level 0 Staged spans without mip levels pass no level arrays, and the sampler goes straight to its one level path. 4% fewer instructions in God of War. Co-Authored-By: Claude Opus 5.5 (1M context) --- GPU/Software/Rasterizer.cpp | 3 +-- GPU/Software/Sampler.cpp | 15 +++++++++------ GPU/Software/Sampler.h | 2 +- 3 files changed, 11 insertions(+), 9 deletions(-) diff --git a/GPU/Software/Rasterizer.cpp b/GPU/Software/Rasterizer.cpp index 0e8bba8370..213de1fc9a 100644 --- a/GPU/Software/Rasterizer.cpp +++ b/GPU/Software/Rasterizer.cpp @@ -1280,7 +1280,6 @@ static void DrawStagedSpans(const StagedSpans &ctx, int count, int chunkX, int64 const int64_t qdx = uvPlanes.q.kx * SCREEN_SCALE_FACTOR, sdx = uvPlanes.s.kx * SCREEN_SCALE_FACTOR, tdx = uvPlanes.t.kx * SCREEN_SCALE_FACTOR; // No mip levels and one filter, bilinear: what ApplyTexturing comes to is the quad sampler at level 0. const bool directQuad = textured && state.linearQuad && state.maxTexLevel == 0 && state.minFilt == state.magFilt && state.magFilt; - alignas(16) static const int zeros[4] = {}; for (int k = 0; k < count; k += 4, qv += 4 * qdx, sv += 4 * sdx, tv += 4 * tdx) { const Vec4 mask(maskBuf[k], maskBuf[k + 1], maskBuf[k + 2], maskBuf[k + 3]); if (!AnyMask(mask)) @@ -1312,7 +1311,7 @@ static void DrawStagedSpans(const StagedSpans &ctx, int count, int chunkX, int64 } if (directQuad) { const int active = (mask[0] >= 0 ? 1 : 0) | (mask[1] >= 0 ? 2 : 0) | (mask[2] >= 0 ? 4 : 0) | (mask[3] >= 0 ? 8 : 0); - state.linearQuad(s.AsArray(), t.AsArray(), zeros, zeros, active, state.texptr, state.texbufw, prim_color, STAGED_CHUNK, state.samplerID); + state.linearQuad(s.AsArray(), t.AsArray(), nullptr, nullptr, active, state.texptr, state.texbufw, prim_color, STAGED_CHUNK, state.samplerID); } else { ApplyTexturing(state, prim_color, STAGED_CHUNK, mask, s, t, q, 0.0f, 0.0f, ctx.sameQ, ctx.autoGrad); } diff --git a/GPU/Software/Sampler.cpp b/GPU/Software/Sampler.cpp index 9e0b095e63..068a7879e1 100644 --- a/GPU/Software/Sampler.cpp +++ b/GPU/Software/Sampler.cpp @@ -1106,19 +1106,22 @@ static void SOFTRAST_CALL SampleLinearQuadT(const float *s, const float *t, cons if (!active) return; uint64_t c[4]; - // Usually all at one level (a span of a triangle always is): then in vector lanes. + // Usually all at one level (a span of a triangle always is): then in vector lanes. No level arrays: all at + // level 0, without a fraction. const int first = active & 1 ? 0 : active & 2 ? 1 : active & 4 ? 2 : 3; bool sameLevel = true; - for (int i = first + 1; i < 4; ++i) { - if ((active & (1 << i)) && (level[i] != level[first] || levelFrac[i] != levelFrac[first])) - sameLevel = false; + if (level) { + for (int i = first + 1; i < 4; ++i) { + if ((active & (1 << i)) && (level[i] != level[first] || levelFrac[i] != levelFrac[first])) + sameLevel = false; + } } if (sameLevel) { - const int l = level[first]; + const int l = level ? level[first] : 0; const Vec4F32 sv = Vec4F32::Load(s), tv = Vec4F32::Load(t); Vec4S32 prim[4], tex[4], out[4]; SampleLinearLevel4LanesT(sv, tv, texptr[l], texbufw[l], l, samplerID, tex); - if (levelFrac[first]) { + if (level && levelFrac[first]) { Vec4S32 tex1[4]; SampleLinearLevel4LanesT(sv, tv, texptr[l + 1], texbufw[l + 1], l + 1, samplerID, tex1); const Vec4S32 f = Vec4S32::Splat(levelFrac[first]); diff --git a/GPU/Software/Sampler.h b/GPU/Software/Sampler.h index 6e77dfe034..b4124559a7 100644 --- a/GPU/Software/Sampler.h +++ b/GPU/Software/Sampler.h @@ -48,7 +48,7 @@ LinearFunc GetLinearFunc(SamplerID id, BinManager *binner); // Bilinear samples for the pixels of a quad with bit i of active set, each at its own level, through the // texture function: colors goes in as the primitive's colors and comes out textured. A channel at a time, -// as for SpanFunc. +// as for SpanFunc. level and levelFrac nullptr: all at level 0. typedef void (SOFTRAST_CALL *LinearQuadFunc)(const float *s, const float *t, const int *level, const int *levelFrac, int active, const u8 *const *texptr, const uint16_t *texbufw, int *colors, int colorStride, const SamplerID &samplerID); // Only for the samplers without a JIT (linear is what GetLinearFunc returned), nullptr otherwise. LinearQuadFunc GetLinearQuadFunc(const SamplerID &id, LinearFunc linear); From 8a7a67ae25c77e89bbaa9483d7ccda52e2fce548 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Henrik=20Rydg=C3=A5rd?= Date: Mon, 5 Oct 2026 14:40:24 -0600 Subject: [PATCH 5/5] SoftGPU: Use larger tiles now that pixels are cheaper With the span drawing and sampling faster, the setup a triangle repeats for each tile it touches weighs more, so the rule is now eight tiles per thread instead of sixteen (64x32 on eight threads). Against sixteen, in wall time on five games: God of War -2.6%, Wipeout -1.7%, LocoRoco -5.2%, Tekken 6 -5.6%, Burnout -3.3%, with 2.5-8% less CPU. Four per thread used the least CPU but was slower than eight in wall time in most of them. Co-Authored-By: Claude Opus 5.5 (1M context) --- GPU/Software/BinManager.cpp | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/GPU/Software/BinManager.cpp b/GPU/Software/BinManager.cpp index c4d637b0cf..b6c76ece3f 100644 --- a/GPU/Software/BinManager.cpp +++ b/GPU/Software/BinManager.cpp @@ -1106,20 +1106,19 @@ bool BinManager::NeedsOrder(const BinItem &item) { return false; } -// Each extra tile a triangle touches sets it up again, so tiles are as large as they can be with plenty to -// spare for every thread: at least sixteen per thread over a 480x272 screen. With fewer, a scene whose -// drawing is concentrated on part of the screen leaves threads idle (LocoRoco took a third longer with four -// per thread on eight threads). +// Each extra tile a triangle touches sets it up again, so tiles are as large as they can be with enough to +// spare for every thread: at least eight per thread over a 480x272 screen (64x32 on eight threads). With +// fewer, a scene whose drawing is concentrated on part of the screen leaves threads idle: four per thread +// used the least CPU but took longer in wall time than eight in most games, Wipeout longer than sixteen. // The multiplier trades the repeated setup against the load balance, so the best one depends on what drawing -// a pixel costs. When that gets cheaper, larger tiles can win: with faster span drawing, four per thread was -// as fast in wall time and used less CPU. Measure again (Tools/headless_bench.py, wall and CPU time on -// several games) after changes to the per-pixel cost. +// a pixel costs (sixteen was best before the span drawing got faster). Measure again +// (Tools/headless_bench.py, wall and CPU time on several games) after changes to the per-pixel cost. void BinManager::PickTileSize(int threads) { static const int sizes[][2] = { { 128, 32 }, { 64, 32 }, { 64, 16 } }; int w = MIN_TILE_W, h = MIN_TILE_H; for (const auto &size : sizes) { const int tiles = ((480 + size[0] - 1) / size[0]) * ((272 + size[1] - 1) / size[1]); - if (tiles >= 16 * threads) { + if (tiles >= 8 * threads) { w = size[0]; h = size[1]; break;