From 7c5eda25f4fb98eebbece14a302d5c224abe5e05 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Henrik=20Rydg=C3=A5rd?= Date: Thu, 8 Oct 2026 06:10:44 +0200 Subject: [PATCH] Software transform clipping: split clipped quads like the GE, cull beyond far first With one vertex behind the near plane, the GE splits the remaining quad from the vertex before the clipped one (gpu/probe exp43, exp44, as in SoftGPU's Clipper.cpp). The other diagonal could put a far off-screen near-plane vertex in both halves, so range culling dropped the whole triangle: the floor in #16131's box test vanished. The index generator reverses triangles to flip culling, so the batch's order is passed along. Triangles entirely beyond the far plane are now culled even when in front of the near plane, with the cull-distance epsilon, so z/w = 1.0000172 (exactly 1.0 to the GE) still counts as inside. Co-Authored-By: Claude Opus 5.5 --- GPU/Common/DrawEngineCommon.h | 3 ++ GPU/Common/SoftwareTransformCommon.cpp | 44 +++++++++++++++++--------- GPU/Common/SoftwareTransformCommon.h | 2 ++ GPU/D3D11/DrawEngineD3D11.cpp | 1 + GPU/GLES/DrawEngineGLES.cpp | 1 + GPU/Vulkan/DrawEngineVulkan.cpp | 1 + 6 files changed, 37 insertions(+), 15 deletions(-) diff --git a/GPU/Common/DrawEngineCommon.h b/GPU/Common/DrawEngineCommon.h index b41f65cf82..ec2f3fc387 100644 --- a/GPU/Common/DrawEngineCommon.h +++ b/GPU/Common/DrawEngineCommon.h @@ -322,6 +322,9 @@ protected: enum { MAX_DEFERRED_DRAW_INDS = 512 }; // Monster Hunter spams indexed calls that we end up merging. DeferredVerts drawVerts_[MAX_DEFERRED_DRAW_VERTS]; uint32_t drawVertexOffsets_[MAX_DEFERRED_DRAW_VERTS]; + // A cull mode change flushes, so all draws in a batch share this. + bool BatchTrianglesReversed() const { return numDrawInds_ > 0 && !drawInds_[0].clockwise; } + DeferredInds drawInds_[MAX_DEFERRED_DRAW_INDS]; const VertexDecoder *dec_ = nullptr; diff --git a/GPU/Common/SoftwareTransformCommon.cpp b/GPU/Common/SoftwareTransformCommon.cpp index 1b0a21811c..edc93d09bc 100644 --- a/GPU/Common/SoftwareTransformCommon.cpp +++ b/GPU/Common/SoftwareTransformCommon.cpp @@ -406,8 +406,9 @@ inline bool IsInsideNearPlane(const TransformedVertex& v) { return v.z >= -v.pos_w; } +// With the same epsilon as the cull distances in the vertex shader (z/w up to 0x3F8000XX counts as 1.0). inline bool IsInsideFarPlane(const TransformedVertex& v) { - return v.z <= v.pos_w; + return v.z <= v.pos_w + 0.0000304f / v.pos_w; } // TODO: Use CrossSIMD, should help. @@ -430,7 +431,7 @@ inline void LerpTransformedVertex(TransformedVertex *dest, TransformedVertex &a, static void ClipTrianglesAgainstNearPlane( TransformedVertex *transformed, int &transformedCount, int maxTransformed, u16 *indicesIn, int numIndicesIn, - u16 *indicesOut, int &numIndicesOut, int maxIndicesOut, TransformStats *stats + u16 *indicesOut, int &numIndicesOut, int maxIndicesOut, bool reversed, TransformStats *stats ) { // Process one triangle (3 indices) at a time for (size_t i = 0; i < numIndicesIn; i += 3) { @@ -453,25 +454,25 @@ static void ClipTrianglesAgainstNearPlane( int insideCount = (in0 ? 1 : 0) + (in1 ? 1 : 0) + (in2 ? 1 : 0); int insideFarCount = (inFar0 ? 1 : 0) + (inFar1 ? 1 : 0) + (inFar2 ? 1 : 0); - // Case 1: Entirely visible - if (insideCount == 3) { + // Case 1: Entirely beyond far plane + if (insideFarCount == 0) { + // All are beyond the far plane. Cull. + stats->culledTrianglesFar++; + continue; + } + // Case 2: Entirely visible + else if (insideCount == 3) { indicesOut[numIndicesOut++] = idx0; indicesOut[numIndicesOut++] = idx1; indicesOut[numIndicesOut++] = idx2; } - // Case 2: Entirely clipped / behind near plane + // Case 3: Entirely clipped / behind near plane else if (insideCount == 0) { // Cull, no clipping needed. stats->culledTrianglesNear++; continue; } - // Case 3: Entirely beyond far plane - else if (insideFarCount == 0) { - // All are beyond the far plane. Cull. - stats->culledTrianglesFar++; - continue; - } - // Case 3: Partially clipped + // Case 4: Partially clipped else { stats->clippedTriangles++; @@ -498,8 +499,9 @@ static void ClipTrianglesAgainstNearPlane( // If we cross the clipping plane line (inside->outside or outside->inside) if (triIn[j] != triIn[next]) { - /* const */ TransformedVertex& a = transformed[currIdx]; - /* const */ TransformedVertex& b = transformed[nextIdx]; + // Interpolate from the inside vertex, like the GE. + /* const */ TransformedVertex& a = transformed[triIn[j] ? currIdx : nextIdx]; + /* const */ TransformedVertex& b = transformed[triIn[j] ? nextIdx : currIdx]; // Find interpolation factor 't' where: z_interpolated = -w_interpolated // Lerp formulation: @@ -541,6 +543,18 @@ static void ClipTrianglesAgainstNearPlane( indicesOut[numIndicesOut++] = polyIndices[1]; indicesOut[numIndicesOut++] = polyIndices[2]; } else if (polyLength == 4) { + // One vertex (o) was outside. The GE splits the quad from the vertex before it (p), giving + // (p, a, b) and (p, b, n), which matters when a new vertex fails the range check + // (gpu/probe exp43, exp44; see Clipper.cpp). Rotate p to the front. + const int o = !in0 ? 0 : (!in1 ? 1 : 2); + const u16 p = triIdx[reversed ? (o + 1) % 3 : (o + 2) % 3]; + while (polyIndices[0] != p) { + const u16 first = polyIndices[0]; + polyIndices[0] = polyIndices[1]; + polyIndices[1] = polyIndices[2]; + polyIndices[2] = polyIndices[3]; + polyIndices[3] = first; + } // Triangle 1 indicesOut[numIndicesOut++] = polyIndices[0]; indicesOut[numIndicesOut++] = polyIndices[1]; @@ -831,7 +845,7 @@ static SoftwareTransformAction ProjectClipAndExpand(SoftwareTransformParams &par if (gstate.isDepthClipEnabled()) { const u16 *indsIn = (const u16 *)inds; int newIndexCount = 0; - ClipTrianglesAgainstNearPlane(transformed, numDecodedVerts, 65536, inds, vertexCount, indsOut, newIndexCount, 65336, &result->stats); + ClipTrianglesAgainstNearPlane(transformed, numDecodedVerts, 65536, inds, vertexCount, indsOut, newIndexCount, 65336, params.trianglesReversed, &result->stats); drawIndexCount = newIndexCount; } else { std::vector outsideZ; diff --git a/GPU/Common/SoftwareTransformCommon.h b/GPU/Common/SoftwareTransformCommon.h index 5962076711..dd8ee6b18e 100644 --- a/GPU/Common/SoftwareTransformCommon.h +++ b/GPU/Common/SoftwareTransformCommon.h @@ -63,6 +63,8 @@ struct SoftwareTransformParams { bool allowSeparateAlphaClear; bool everUsedEqualDepth; float pointScale = 1.0f; // Useful to increase these for debug views of bounding box corners. + // The index generator reversed every triangle (to flip the winding for culling), so they're not in the GE's order. + bool trianglesReversed = false; }; // Converts an index buffer to make the provoking vertex the last. diff --git a/GPU/D3D11/DrawEngineD3D11.cpp b/GPU/D3D11/DrawEngineD3D11.cpp index bcfc96e546..238106bb62 100644 --- a/GPU/D3D11/DrawEngineD3D11.cpp +++ b/GPU/D3D11/DrawEngineD3D11.cpp @@ -452,6 +452,7 @@ void DrawEngineD3D11::Flush() { params.allowClear = true; params.allowSeparateAlphaClear = false; // D3D11 doesn't support separate alpha clears params.clipInfoFlags = clipInfoFlags_; + params.trianglesReversed = BatchTrianglesReversed(); const SoftwareTransformAction action = RunSoftwareTransform(params, prim, dec_->VertexType(), dec_->GetDecVtxFmt(), numDecodedVerts_, VERTEX_BUFFER_MAX, vertexCount, inds, RemainingIndices(inds), &result); diff --git a/GPU/GLES/DrawEngineGLES.cpp b/GPU/GLES/DrawEngineGLES.cpp index 0ee92fc4d1..3b05d01408 100644 --- a/GPU/GLES/DrawEngineGLES.cpp +++ b/GPU/GLES/DrawEngineGLES.cpp @@ -401,6 +401,7 @@ void DrawEngineGLES::Flush() { params.allowClear = true; // Clear in OpenGL respects scissor rects, so we'll use it. params.allowSeparateAlphaClear = true; params.clipInfoFlags = clipInfoFlags_; + params.trianglesReversed = BatchTrianglesReversed(); const SoftwareTransformAction action = RunSoftwareTransform(params, prim, dec_->VertexType(), dec_->GetDecVtxFmt(), numDecodedVerts_, VERTEX_BUFFER_MAX, vertexCount, inds, RemainingIndices(inds), &result); if (textureNeedsApply) { diff --git a/GPU/Vulkan/DrawEngineVulkan.cpp b/GPU/Vulkan/DrawEngineVulkan.cpp index da97c670be..aadc9edee9 100644 --- a/GPU/Vulkan/DrawEngineVulkan.cpp +++ b/GPU/Vulkan/DrawEngineVulkan.cpp @@ -430,6 +430,7 @@ void DrawEngineVulkan::Flush() { params.allowSeparateAlphaClear = false; params.everUsedEqualDepth = everUsedEqualDepth_; params.clipInfoFlags = clipInfoFlags_; + params.trianglesReversed = BatchTrianglesReversed(); // The texture needs to have been applied when running RunSoftwareTransform, however we must not have applied the sampler yet since it takes // result.pixelMapped as an input.