From 0223fbcce31bb4e24fbcd67e08ed56ccad8df001 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Henrik=20Rydg=C3=A5rd?= Date: Sat, 3 Oct 2026 07:01:09 -0600 Subject: [PATCH] SoftGPU: Walk triangle rows between their exact ends Each row's covered pixels now come from the three edges directly, as a range from 64-bit integer bounds at the pixel centers, and only the spans in it are visited, with lane masks from the range. The edge functions per span (32-bit, with a coarse row narrowing) are gone. The mip level's q comes from the row's covered range instead of edge tests per pixel. A tall triangle's long edge, which the GE checks at the other end of a span for its first or last pixel, is still checked per pixel. Co-Authored-By: Claude Opus 5.5 (1M context) --- GPU/Software/Rasterizer.cpp | 232 ++++++++++++++---------------------- 1 file changed, 87 insertions(+), 145 deletions(-) diff --git a/GPU/Software/Rasterizer.cpp b/GPU/Software/Rasterizer.cpp index eeebc119bf..c30db21930 100644 --- a/GPU/Software/Rasterizer.cpp +++ b/GPU/Software/Rasterizer.cpp @@ -852,10 +852,8 @@ static inline Vec4 SOFTRAST_CALL CheckDepthTestPassed4(const Vec4 &mas template struct TriangleEdge { Vec4 Start(const ScreenCoords &v0, const ScreenCoords &v1, const ScreenCoords &origin); - inline Vec4 StepX(const Vec4 &w); inline Vec4 StepY(const Vec4 &w); - inline void NarrowMinMaxX(const Vec4 &w, int64_t minX, int64_t &rowMinX, int64_t &rowMaxX); inline Vec4 StepXTimes(const Vec4 &w, int c); Vec4 stepX; @@ -896,17 +894,6 @@ Vec4 TriangleEdge::Start(const ScreenCoords &v0, const ScreenCoord return Vec4::AssignToAll(xf) * initX + Vec4::AssignToAll(yf) * initY + Vec4::AssignToAll(c); } -template -inline Vec4 TriangleEdge::StepX(const Vec4 &w) { -#if defined(_M_SSE) && !PPSSPP_ARCH(X86) - return _mm_add_epi32(w.ivec, stepX.ivec); -#elif PPSSPP_ARCH(ARM64_NEON) - return vaddq_s32(w.ivec, stepX.ivec); -#else - return w + stepX; -#endif -} - template inline Vec4 TriangleEdge::StepY(const Vec4 &w) { #if defined(_M_SSE) && !PPSSPP_ARCH(X86) @@ -918,47 +905,6 @@ inline Vec4 TriangleEdge::StepY(const Vec4 &w) { #endif } -#if defined(_M_SSE) && !PPSSPP_ARCH(X86) -#if defined(__GNUC__) || defined(__clang__) || defined(__INTEL_COMPILER) -[[gnu::target("sse4.1")]] -#endif -static inline int SOFTRAST_CALL MaxWeightSSE4(__m128i w) { - __m128i max2 = _mm_max_epi32(w, _mm_shuffle_epi32(w, _MM_SHUFFLE(3, 2, 3, 2))); - __m128i max1 = _mm_max_epi32(max2, _mm_shuffle_epi32(max2, _MM_SHUFFLE(1, 1, 1, 1))); - return _mm_cvtsi128_si32(max1); -} -#endif - -template -void TriangleEdge::NarrowMinMaxX(const Vec4 &w, int64_t minX, int64_t &rowMinX, int64_t &rowMaxX) { - int wmax; -#if defined(_M_SSE) && !PPSSPP_ARCH(X86) - if constexpr (useSSE4) { - wmax = MaxWeightSSE4(w.ivec); - } else { - wmax = std::max(std::max(w.x, w.y), std::max(w.z, w.w)); - } -#elif PPSSPP_ARCH(ARM64_NEON) - int32x2_t wmax_temp = vpmax_s32(vget_low_s32(w.ivec), vget_high_s32(w.ivec)); - wmax = vget_lane_s32(vpmax_s32(wmax_temp, wmax_temp), 0); -#else - wmax = std::max(std::max(w.x, w.y), std::max(w.z, w.w)); -#endif - if (wmax < 0) { - if (stepX.x > 0) { - int steps = -wmax / stepX.x; - rowMinX = std::max(rowMinX, minX + steps * SCREEN_SCALE_FACTOR * 4); - } else if (stepX.x <= 0) { - rowMinX = rowMaxX + 1; - } - } - - if (wmax >= 0 && stepX.x < 0) { - int steps = (-wmax / stepX.x) + 1; - rowMaxX = std::min(rowMaxX, minX + steps * SCREEN_SCALE_FACTOR * 4); - } -} - #if defined(_M_SSE) && !PPSSPP_ARCH(X86) #if defined(__GNUC__) || defined(__clang__) || defined(__INTEL_COMPILER) [[gnu::target("sse4.1")]] @@ -979,24 +925,6 @@ inline Vec4 TriangleEdge::StepXTimes(const Vec4 &w, int c) { return w + stepX * c; } -static inline Vec4 MakeMask(const Vec4 &w0, const Vec4 &w1, const Vec4 &w2, const Vec4 &bias0, const Vec4 &bias1, const Vec4 &bias2, const Vec4 &scissor) { -#if defined(_M_SSE) && !PPSSPP_ARCH(X86) - __m128i biased0 = _mm_add_epi32(w0.ivec, bias0.ivec); - __m128i biased1 = _mm_add_epi32(w1.ivec, bias1.ivec); - __m128i biased2 = _mm_add_epi32(w2.ivec, bias2.ivec); - - return _mm_or_si128(_mm_or_si128(biased0, _mm_or_si128(biased1, biased2)), scissor.ivec); -#elif PPSSPP_ARCH(ARM64_NEON) - int32x4_t biased0 = vaddq_s32(w0.ivec, bias0.ivec); - int32x4_t biased1 = vaddq_s32(w1.ivec, bias1.ivec); - int32x4_t biased2 = vaddq_s32(w2.ivec, bias2.ivec); - - return vorrq_s32(vorrq_s32(biased0, vorrq_s32(biased1, biased2)), scissor.ivec); -#else - return (w0 + bias0) | (w1 + bias1) | (w2 + bias2) | scissor; -#endif -} - #if defined(_M_SSE) && !PPSSPP_ARCH(X86) #if defined(__GNUC__) || defined(__clang__) || defined(__INTEL_COMPILER) [[gnu::target("sse4.1")]] @@ -1212,21 +1140,17 @@ static float UVPlaneGradient(const UVPlanes &planes, const RasterizerState &stat // second pixel in the direction it walks the row, or when that one is outside the triangle, at the // span's first pixel inside. Left to right that's x = 4k + 1; right to left (when the long edge is the // right side, as for the plane anchor) 4k + 2 (gpu/probe exp93, exp103-106). quadX is the first of four -// pixels in a row in drawing coordinates, centerX/Y its center in screen subpixels, and covered(x, y) the -// triangle's coverage there. -template -static inline Vec4 LodQFromPlanes(const UVPlanes &planes, int64_t centerX, int64_t centerY, int quadX, const Covered &covered) { +// pixels in a row in drawing coordinates, centerX/Y its center in screen subpixels, and the triangle covers +// the row's pixels coverLo to coverHi. +static inline Vec4 LodQFromPlanes(const UVPlanes &planes, int64_t centerX, int64_t centerY, int quadX, int64_t coverLo, int64_t coverHi) { const bool rtl = planes.q.rightAnchored; auto spanQ = [&](int spanX, int64_t y) { - int pick = spanX + (rtl ? 2 : 1); - if (!covered(centerX + (pick - quadX) * SCREEN_SCALE_FACTOR, y)) { - for (int j = 0; j < 4; ++j) { - const int c = rtl ? spanX + 3 - j : spanX + j; - if (covered(centerX + (c - quadX) * SCREEN_SCALE_FACTOR, y)) { - pick = c; - break; - } - } + int64_t pick = spanX + (rtl ? 2 : 1); + if (pick < coverLo || pick > coverHi) { + // The first pixel inside, in the direction the row is walked. + const int64_t first = rtl ? std::min(spanX + 3, coverHi) : std::max(spanX, coverLo); + if (first >= std::max(spanX, coverLo) && first <= std::min(spanX + 3, coverHi)) + pick = first; } const int64_t x = centerX + (pick - quadX) * SCREEN_SCALE_FACTOR; return TruncateToFloat24((float)((double)planes.q.At(x, y) * planes.scaleQ)); @@ -1292,14 +1216,8 @@ void DrawTriangleSlice( int64_t minX = x1 & ~(SCREEN_SCALE_FACTOR * 4 - 1), maxX = x2, minY = y1, maxY = y2; ScreenCoords pprime(minX, minY, 0); - // Coverage of any pixel center, for picking the mip level's q (LodQFromPlanes), when it matters. + // Whether the mip level's q matters (LodQFromPlanes). const bool lodUsesQ = state.TexLevelMode() != GE_TEXLEVEL_MODE_CONST && (state.maxTexLevel > 0 || state.minFilt != state.magFilt); - auto edgeAt = [](const ScreenCoords &a, const ScreenCoords &b, int64_t x, int64_t y) { - return (int64_t)(a.y - b.y) * x + (int64_t)(b.x - a.x) * y + ((int64_t)b.y * a.x - (int64_t)b.x * a.y); - }; - auto coveredAt = [&](int64_t x, int64_t y) { - return edgeAt(v1.screenpos, v2.screenpos, x, y) + bias0[0] >= 0 && edgeAt(v2.screenpos, v0.screenpos, x, y) + bias1[0] >= 0 && edgeAt(v0.screenpos, v1.screenpos, x, y) + bias2[0] >= 0; - }; // A very tall triangle's long edge (top to bottom vertex): when 3 dy > 2^17 (in subpixels), the first pixel // of each 4-pixel span (for a left edge, the last for a right edge) is inside it when any of the span is // (gpu/probe exp131-135). @@ -1324,13 +1242,6 @@ void DrawTriangleSlice( snapLeft = (int64_t)(vs[third]->screenpos.x - vs[top]->screenpos.x) * dy > ex; } } - auto edgeK = [&](int k, int64_t x, int64_t y) { - switch (k) { - case 0: return edgeAt(v1.screenpos, v2.screenpos, x, y) + bias0[0]; - case 1: return edgeAt(v2.screenpos, v0.screenpos, x, y) + bias1[0]; - default: return edgeAt(v0.screenpos, v1.screenpos, x, y) + bias2[0]; - } - }; Vec4 w0_base = e0.Start(v1.screenpos, v2.screenpos, pprime); Vec4 w1_base = e1.Start(v2.screenpos, v0.screenpos, pprime); @@ -1380,60 +1291,84 @@ void DrawTriangleSlice( const Vec4 minz = Vec4::AssignToAll(pixelID.cached.minz); const Vec4 maxz = Vec4::AssignToAll(pixelID.cached.maxz); + // The edges as A x + B y + C + bias >= 0 at pixel centers, in 64 bits. + struct RowEdge { + int64_t a, b, c; + }; + const ScreenCoords *edgeEnds[3][2] = { { &v1.screenpos, &v2.screenpos }, { &v2.screenpos, &v0.screenpos }, { &v0.screenpos, &v1.screenpos } }; + const int biases[3] = { bias0[0], bias1[0], bias2[0] }; + RowEdge edges[3]; + for (int k = 0; k < 3; ++k) { + const ScreenCoords &a = *edgeEnds[k][0], &b = *edgeEnds[k][1]; + edges[k] = { (int64_t)(a.y - b.y), (int64_t)(b.x - a.x), (int64_t)b.y * a.x - (int64_t)b.x * a.y + biases[k] }; + } + auto floorDiv = [](int64_t n, int64_t m) { + return n >= 0 ? n / m : -((-n + m - 1) / m); + }; + // An edge's pixels in a row (x centers 16 px + 8): px from lo up, or up to hi, or all or none. + auto edgeBounds = [&](const RowEdge &edge, int64_t yc, int64_t &lo, int64_t &hi) { + const int64_t rest = edge.b * yc + edge.c; + if (edge.a > 0) { + // x >= -rest / a, so px >= (x - 8) / 16. + const int64_t x = -floorDiv(rest, edge.a); + lo = std::max(lo, -floorDiv(SCREEN_SCALE_FACTOR / 2 - x, SCREEN_SCALE_FACTOR)); + } else if (edge.a < 0) { + const int64_t x = floorDiv(rest, -edge.a); + hi = std::min(hi, floorDiv(x - SCREEN_SCALE_FACTOR / 2, SCREEN_SCALE_FACTOR)); + } else if (rest < 0) { + lo = INT64_MAX / 2; + } + }; + // The scissor's pixels. + const int64_t scissorLo = -floorDiv(-(int64_t)x1, SCREEN_SCALE_FACTOR), scissorHi = floorDiv(x2, SCREEN_SCALE_FACTOR); + for (int64_t curY = minY; curY <= maxY; curY += SCREEN_SCALE_FACTOR, w0_base = e0.StepY(w0_base), w1_base = e1.StepY(w1_base), w2_base = e2.StepY(w2_base)) { - Vec4 w0 = w0_base; - Vec4 w1 = w1_base; - Vec4 w2 = w2_base; + const int64_t yc = curY + SCREEN_SCALE_FACTOR / 2; + // The triangle's pixels in this row (lo to hi), and those drawn, within the scissor. A snapping edge + // is checked per pixel, the rest by bounds. + int64_t coverLo = INT64_MIN / 2, coverHi = INT64_MAX / 2; + for (int k = 0; k < 3; ++k) + edgeBounds(edges[k], yc, coverLo, coverHi); + int64_t lo = scissorLo, hi = scissorHi; + for (int k = 0; k < 3; ++k) { + if (k != snapEdge) + edgeBounds(edges[k], yc, lo, hi); + } + int64_t snapLo = INT64_MIN / 2, snapHi = INT64_MAX / 2; + if (snapEdge >= 0) { + edgeBounds(edges[snapEdge], yc, snapLo, snapHi); + // The span's first pixel (left edge) or last (right edge) is checked at the span's other end. + int64_t walkLo = lo, walkHi = hi; + walkLo = std::max(walkLo, snapLo - 3); + walkHi = std::min(walkHi, snapHi + 3); + lo = std::max(lo, walkLo); + hi = std::min(hi, walkHi); + } else { + lo = std::max(lo, coverLo); + hi = std::min(hi, coverHi); + } + if (lo > hi) + continue; DrawingCoords p = TransformUnit::ScreenToDrawing(minX, curY); - - int64_t rowMinX = minX, rowMaxX = maxX; - // A snapping edge can light pixels up to three past it. - if (snapEdge != 0) - e0.NarrowMinMaxX(w0, minX, rowMinX, rowMaxX); - if (snapEdge != 1) - e1.NarrowMinMaxX(w1, minX, rowMinX, rowMaxX); - if (snapEdge != 2) - e2.NarrowMinMaxX(w2, minX, rowMinX, rowMaxX); - - int skipX = (rowMinX - minX) / (SCREEN_SCALE_FACTOR * 4); - w0 = e0.StepXTimes(w0, skipX); - w1 = e1.StepXTimes(w1, skipX); - w2 = e2.StepXTimes(w2, skipX); + const int64_t firstSpan = lo & ~3; + const int skipX = (int)((firstSpan * SCREEN_SCALE_FACTOR - minX) / (SCREEN_SCALE_FACTOR * 4)); p.x = (p.x + 4 * skipX) & 0x3FF; - // Negative for pixels left of x1 or right of x2. - Vec4 scissorLeft = Vec4::AssignToAll((int)(rowMinX - x1)) + Vec4(0, SCREEN_SCALE_FACTOR, SCREEN_SCALE_FACTOR * 2, SCREEN_SCALE_FACTOR * 3); - Vec4 scissorRight = Vec4::AssignToAll((int)(x2 - rowMinX)) - Vec4(0, SCREEN_SCALE_FACTOR, SCREEN_SCALE_FACTOR * 2, SCREEN_SCALE_FACTOR * 3); - const Vec4 scissorStep = Vec4::AssignToAll(SCREEN_SCALE_FACTOR * 4); - - for (int64_t curX = rowMinX; curX <= rowMaxX; curX += SCREEN_SCALE_FACTOR * 4, - w0 = e0.StepX(w0), - w1 = e1.StepX(w1), - w2 = e2.StepX(w2), - scissorLeft = scissorLeft + scissorStep, - scissorRight = scissorRight - scissorStep, - p.x = (p.x + 4) & 0x3FF) { - const Vec4 scissor_mask = scissorLeft | scissorRight; - - // If p is on or inside all edges, render pixel - Vec4 mask = MakeMask(w0, w1, w2, bias0, bias1, bias2, scissor_mask); - if (snapEdge >= 0) { - for (int i = 0; i < 4; ++i) { - const int64_t x = curX + SCREEN_SCALE_FACTOR / 2 + i * SCREEN_SCALE_FACTOR; - const int64_t y = curY + SCREEN_SCALE_FACTOR / 2; - const int px = p.x + i; - // Only the span's first pixel (left edge) or last (right edge) takes the edge at the other end. - const int spanX = snapLeft ? ((px & 3) == 0 ? (px | 3) : px) : ((px & 3) == 3 ? (px & ~3) : px); - const int64_t xs = x + (int64_t)(spanX - px) * SCREEN_SCALE_FACTOR; - bool inside = true; - for (int k = 0; k < 3; ++k) - inside = inside && edgeK(k, k == snapEdge ? xs : x, y) >= 0; - mask[i] = (inside ? 0 : -1) | scissor_mask[i]; + for (int64_t spanX = firstSpan; spanX <= hi; spanX += 4, p.x = (p.x + 4) & 0x3FF) { + const int64_t curX = spanX * SCREEN_SCALE_FACTOR; + Vec4 mask; + for (int i = 0; i < 4; ++i) { + const int64_t px = spanX + i; + bool inside = px >= lo && px <= hi; + if (inside && snapEdge >= 0) { + const int64_t at = snapLeft ? ((px & 3) == 0 ? (px | 3) : px) : ((px & 3) == 3 ? (px & ~3) : px); + inside = at >= snapLo && at <= snapHi; } + mask[i] = inside ? 0 : -1; } if (AnyMask(mask)) { Vec4 z; @@ -1500,6 +1435,13 @@ void DrawTriangleSlice( Vec4 s, t; Vec4 q = Vec4::AssignToAll(1.0f); // Without planes, from the edge weights (of these pixels, or the row below). + Vec4 w0, w1, w2; + if (!uvPlanes.valid) { + const int steps = (int)((curX - minX) / (SCREEN_SCALE_FACTOR * 4)); + w0 = e0.StepXTimes(w0_base, steps); + w1 = e1.StepXTimes(w1_base, steps); + w2 = e2.StepXTimes(w2_base, steps); + } auto interpolatedST = [&](const Vec4 &a, const Vec4 &b, const Vec4 &c, Vec4 &os, Vec4 &ot) { if (state.throughMode) { os = Interpolate(v0.texturecoords.s(), v1.texturecoords.s(), v2.texturecoords.s(), a, b, c, wsum_recip); @@ -1514,7 +1456,7 @@ void DrawTriangleSlice( if (uvPlanes.valid) { GetTextureCoordinatesGE(uvPlanes, centerX, centerY, s, t, q); if (lodUsesQ && !state.throughMode) - q = LodQFromPlanes(uvPlanes, centerX, centerY, p.x, coveredAt); + q = LodQFromPlanes(uvPlanes, centerX, centerY, p.x, coverLo, coverHi); } else { interpolatedST(w0, w1, w2, s, t); }