Apply triangle near-clipping in software transform.

Fixes, but only in SW transform, #10914

Re-enable range culling, oops.
This commit is contained in:
Henrik Rydgård
2026-05-24 14:25:15 +02:00
parent 280ddbf86c
commit 8b84fd555d
10 changed files with 237 additions and 71 deletions
+1 -1
View File
@@ -284,7 +284,7 @@ public:
bool bSoftwareRendering;
bool bSoftwareRenderingJit;
bool bHardwareTransform; // only used in the GLES backend
bool bHardwareTransform;
bool bSoftwareSkinning;
bool bVendorBugChecksEnabled;
bool bUseGeometryShader;
+1 -1
View File
@@ -61,7 +61,7 @@ std::string VertexShaderDesc(const VShaderID &id) {
if (id.Bit(VS_BIT_HAS_TEXCOORD_TESS)) desc << "TessT ";
if (id.Bit(VS_BIT_HAS_NORMAL_TESS)) desc << "TessN ";
if (id.Bit(VS_BIT_NORM_REVERSE_TESS)) desc << "TessRevN ";
if (id.Bit(VS_BIT_VERTEX_RANGE_CULLING)) desc << "Cull ";
if (id.Bit(VS_BIT_VERTEX_RANGE_CULLING)) desc << "RangeCull ";
if (id.Bit(VS_BIT_SIMPLE_STEREO)) desc << "SimpleStereo ";
+194 -54
View File
@@ -33,6 +33,7 @@
#include "GPU/Common/TransformCommon.h"
#include "GPU/Common/VertexDecoderCommon.h"
#include "GPU/Common/DrawEngineCommon.h"
#include "GPU/Software/Clipper.h"
// This is the software transform pipeline, which is necessary for supporting RECT
// primitives correctly without geometry shaders, and may be easier to use for
@@ -112,8 +113,6 @@ static bool IsReallyAClear(const TransformedVertex *transformed, int numVerts, f
// At the end, this calls ProjectClipAndExpand which will expand rectangles as necessary, or apply culling.
SoftwareTransformAction SoftwareTransform::Transform(SoftwareTransformParams &params, int prim, u32 vertType, const DecVtxFormat &decVtxFormat, int &numDecodedVerts, int vertsSize, int vertexCount, u16 *&inds, int indsSize, SoftwareTransformResult *result) {
u8 *decoded = params.decoded;
TransformedVertex *transformed = params.transformed;
bool throughmode = (vertType & GE_VTYPE_THROUGH_MASK) != 0;
bool lmode = gstate.isUsingSecondaryColor() && gstate.isLightingEnabled();
@@ -143,7 +142,8 @@ SoftwareTransformAction SoftwareTransform::Transform(SoftwareTransformParams &pa
fog_slope = std::signbit(fog_slope) ? -largeFogValue : largeFogValue;
}
VertexReader reader(decoded, decVtxFormat, vertType);
TransformedVertex *transformed = params.transformed;
VertexReader reader(params.decoded, decVtxFormat, vertType);
if (throughmode) {
const u32 materialAmbientRGBA = gstate.getMaterialAmbientRGBA();
const bool hasColor = reader.hasColor0();
@@ -151,7 +151,6 @@ SoftwareTransformAction SoftwareTransform::Transform(SoftwareTransformParams &pa
for (int index = 0; index < numDecodedVerts; index++) {
// Do not touch the coordinates or the colors. No lighting.
reader.Goto(index);
// TODO: Write to a flexible buffer, we don't always need all four components.
TransformedVertex &vert = transformed[index];
reader.ReadPosThrough(vert.pos);
vert.pos_w = 1.0f;
@@ -378,6 +377,7 @@ SoftwareTransformAction SoftwareTransform::Transform(SoftwareTransformParams &pa
}
// TODO: This doesn't seem to be a very good check, but let's leave it for now.
// Detect full screen "clears" that might not be so obvious, to set the safe size if possible.
if (!result->setSafeSize && prim == GE_PRIM_RECTANGLES && numDecodedVerts == 2 && throughmode) {
bool clearingColor = gstate.isModeClear() && (gstate.isClearModeColorMask() || gstate.isClearModeAlphaMask());
@@ -428,6 +428,142 @@ void SoftwareTransform::ProjectVertices(TransformedVertex *transformed, int vert
#endif
}
// Helper to check if a vertex is inside the near plane (z >= -w)
// We add a tiny epsilon to prevent floating-point precision issues at the exact boundary
inline bool IsInsideNearPlane(const TransformedVertex& v) {
return v.z >= -v.pos_w;
}
inline bool IsInsideFarPlane(const TransformedVertex& v) {
return v.z <= v.pos_w;
}
// Generated by Gemini, and adapted to fit.
void ClipTrianglesAgainstNearPlane(
TransformedVertex *transformed, int &transformedCount, int maxTransformed,
u16 *indicesIn, int numIndicesIn,
u16 *indicesOut, int &numIndicesOut, int maxIndicesOut
) {
int clipCount = 0;
// Process one triangle (3 indices) at a time
for (size_t i = 0; i < numIndicesIn; i += 3) {
u16 idx0 = indicesIn[i];
u16 idx1 = indicesIn[i + 1];
u16 idx2 = indicesIn[i + 2];
TransformedVertex& v0 = transformed[idx0];
TransformedVertex& v1 = transformed[idx1];
TransformedVertex& v2 = transformed[idx2];
bool in0 = IsInsideNearPlane(v0);
bool in1 = IsInsideNearPlane(v1);
bool in2 = IsInsideNearPlane(v2);
bool inFar0 = IsInsideFarPlane(v0);
bool inFar1 = IsInsideFarPlane(v1);
bool inFar2 = IsInsideFarPlane(v2);
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) {
indicesOut[numIndicesOut++] = idx0;
indicesOut[numIndicesOut++] = idx1;
indicesOut[numIndicesOut++] = idx2;
}
// Case 2: Entirely clipped / behind near plane
else if (insideCount == 0) {
// Cull, no clipping needed.
continue;
}
// Case 3: Entirely beyond far plane
else if (insideFarCount == 0) {
// All are beyond the far plane. Cull.
continue;
}
// Case 3: Partially clipped
else {
clipCount++;
// We need to organize vertices cleanly to calculate intersections.
// We will create a local polygon array of the inside/outside states.
u16 triIdx[3] = {idx0, idx1, idx2};
bool triIn[3] = {in0, in1, in2};
// Output generated vertex indices for this clipped polygon
u16 polyIndices[4];
int polyLength = 0;
for (int j = 0; j < 3; ++j) {
int next = (j + 1) % 3;
u16 currIdx = triIdx[j];
u16 nextIdx = triIdx[next];
// If current vertex is inside, it stays a part of the output polygon
if (triIn[j]) {
polyIndices[polyLength++] = currIdx;
}
// 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];
// Find interpolation factor 't' where: z_interpolated = -w_interpolated
// Lerp formulation:
// z = a.z + t*(b.z - a.z)
// w = a.w + t*(b.w - a.w)
// Set z = -w => a.z + t*(b.z - a.z) = -(a.pos_w + t*(b.pos_w - a.pos_w))
// Solve for t:
float denominator = (b.z - a.z) + (b.pos_w - a.pos_w);
float t = 0.0f;
if (fabsf(denominator) > 0.000001f) {
t = (-a.z - a.pos_w) / denominator;
}
// Clamp safely due to float precision
if (t < 0.0f) t = 0.0f;
if (t > 1.0f) t = 1.0f;
// Generate new vertex at the intersection point
TransformedVertex newVertex;
TransformedVertex::Lerp(&newVertex, const_cast<TransformedVertex&>(a), const_cast<TransformedVertex&>(b), t);
// Force exact intersection to eliminate precision creeping down the pipeline
newVertex.z = -newVertex.pos_w;
newVertex.color0_32 = 0xFFFF00FF;
// a.color0_32 = 0xFFFF00FF;
// b.color0_32 = 0xFFFF00FF;
// Append to global vertex buffer
transformed[transformedCount++] = newVertex;
u16 newIdx = static_cast<u16>(transformedCount - 1);
polyIndices[polyLength++] = newIdx;
}
}
// Triangulate the resulting polygon array (will be either 3 or 4 vertices)
if (polyLength == 3) {
indicesOut[numIndicesOut++] = polyIndices[0];
indicesOut[numIndicesOut++] = polyIndices[1];
indicesOut[numIndicesOut++] = polyIndices[2];
} else if (polyLength == 4) {
// Triangle 1
indicesOut[numIndicesOut++] = polyIndices[0];
indicesOut[numIndicesOut++] = polyIndices[1];
indicesOut[numIndicesOut++] = polyIndices[2];
// Triangle 2
indicesOut[numIndicesOut++] = polyIndices[0];
indicesOut[numIndicesOut++] = polyIndices[2];
indicesOut[numIndicesOut++] = polyIndices[3];
}
}
}
gpuStats.perFrame.numSoftClippedTriangles++;
}
SoftwareTransformAction SoftwareTransform::ProjectClipAndExpand(SoftwareTransformParams &params, int prim, int vertexCount, u32 vertType, u16 *&inds, int indsSize, int &numDecodedVerts, int vertsSize, SoftwareTransformResult *result) {
TransformedVertex *transformed = params.transformed;
TransformedVertex *transformedExpanded = params.transformedExpanded;
@@ -440,6 +576,7 @@ SoftwareTransformAction SoftwareTransform::ProjectClipAndExpand(SoftwareTransfor
FramebufferManagerCommon *fbman = params.fbman;
bool useBufferedRendering = fbman->UseBufferedRendering();
// NOTE: ExpandRectanges/lines/etc should do clipping while they're at it.
if (prim == GE_PRIM_RECTANGLES) {
// TODO: We should cull rectangles outzide -W<Z<W here if *both* points are outside in the same direction,
// like we do with triangles.
@@ -538,29 +675,7 @@ SoftwareTransformAction SoftwareTransform::ProjectClipAndExpand(SoftwareTransfor
// Culling and clipping needs to be done here, it doesn't happen in the shader in the case of software transform.
// However, fast culling should already have taken care of the Z<-W and Z>W culling, but we check for it on a per-triangle
// basis here anyway.
const u16 *indsIn = (const u16 *)inds;
u16 *newInds = inds + vertexCount;
u16 *indsOut = newInds;
std::vector<int> outsideZ;
outsideZ.resize(vertexCount);
// First, check inside/outside directions for each index.
// We are still in clip space here, so we can cull aggressively in Z.
// TODO: This is so cheap now that we can probably avoid the buffer and just do the work below.
for (int i = 0; i < vertexCount; ++i) {
float z = transformed[indsIn[i]].z;
float w = transformed[indsIn[i]].pos_w;
if (z > w) {
outsideZ[i] = 1;
} else if (z < -w) {
outsideZ[i] = -1;
} else {
outsideZ[i] = 0;
}
}
u16 *origInds = inds;
// TODO: We should either merge the two loops, or avoid the second loop if no culling is needed.
@@ -568,20 +683,34 @@ SoftwareTransformAction SoftwareTransform::ProjectClipAndExpand(SoftwareTransfor
// Now, for each triangle, throw away the indices if:
// - Depth clip/clamp on, and ALL verts are outside *in the same direction*.
// - Depth clip/clamp off, and ANY vert is outside.
u16 *newInds = inds + vertexCount;
u16 *indsOut = newInds;
if (gstate.isDepthClipEnabled()) {
numTrans = 0;
for (int i = 0; i < vertexCount - 2; i += 3) {
if (outsideZ[i + 0] != 0 && outsideZ[i + 0] == outsideZ[i + 1] && outsideZ[i + 0] == outsideZ[i + 2]) {
// All outside, and all the same direction. Nuke this triangle.
continue;
const u16 *indsIn = (const u16 *)inds;
int newIndexCount = 0;
ClipTrianglesAgainstNearPlane(transformed, numDecodedVerts, 65536, inds, vertexCount, indsOut, newIndexCount, 65336);
numTrans = newIndexCount;
} else {
std::vector<int> outsideZ;
outsideZ.resize(vertexCount);
// First, check inside/outside directions for each index.
// We are still in clip space here, so we can cull aggressively in Z.
// TODO: This is so cheap now that we can probably avoid the buffer and just do the work below.
for (int i = 0; i < vertexCount; ++i) {
float z = transformed[indsIn[i]].z;
float w = transformed[indsIn[i]].pos_w;
if (z > w) {
outsideZ[i] = 1;
} else if (z < -w) {
outsideZ[i] = -1;
} else {
outsideZ[i] = 0;
}
memcpy(indsOut, indsIn + i, 3 * sizeof(uint16_t));
indsOut += 3;
numTrans += 3;
}
inds = newInds;
} else if (prim == GE_PRIM_TRIANGLES) {
numTrans = 0;
for (int i = 0; i < vertexCount - 2; i += 3) {
if (outsideZ[i + 0] != 0 || outsideZ[i + 1] != 0 || outsideZ[i + 2] != 0) {
@@ -593,10 +722,10 @@ SoftwareTransformAction SoftwareTransform::ProjectClipAndExpand(SoftwareTransfor
indsOut += 3;
numTrans += 3;
}
inds = newInds;
}
inds = newInds;
// Now that we're done culling and generating clipped vertices if needed (not yet implemented), we go ahead and project.
ProjectVertices(transformed, numDecodedVerts);
@@ -605,33 +734,44 @@ SoftwareTransformAction SoftwareTransform::ProjectClipAndExpand(SoftwareTransfor
// If not we technically need to cut it in two parts to clamp accurately.
// However, in most cases that matter (such as missing skies, etc), this is fine.
const int maxZInt = gstate.getDepthRangeMax();
float maxZ = maxZInt / 65535.0f;
// float maxZ = maxZInt / 65535.0f;
const int minZInt = gstate.getDepthRangeMin();
float minZ = minZInt / 65535.0f;
// float minZ = minZInt / 65535.0f;
// We only need to clamp if minZ and maxZ aren't at the extreme in each direction, as otherwise
// minZ and maxZ will cut things off.
if (!gstate_c.Use(GPU_USE_DEPTH_CLAMP) && gstate.isDepthClipEnabled() && (maxZInt == 0 || maxZInt == 65535)) {
for (int i = 0; i < vertexCount - 2; i += 3) {
// If gstate_c.Use(GPU_USE_DEPTH_CLAMP), we can theoretically skip this. However, to keep behavior the same regardless of the flag, we won't.
if (gstate.isDepthClipEnabled() && (maxZInt == 0 || maxZInt == 65535)) {
for (int i = 0; i < numTrans - 2; i += 3) {
TransformedVertex &v0 = transformed[indsIn[i]];
TransformedVertex &v1 = transformed[indsIn[i + 1]];
TransformedVertex &v2 = transformed[indsIn[i + 2]];
if (v0.x < 0.0f || v0.x > 4096.0f ||
v1.x < 0.0f || v1.y > 4096.0f ||
v2.x < 0.0f || v2.y > 4096.0f) {
// If it's outside the viewport, we might as well skip the clamping, as it won't be visible anyway.
// continue;
}
if (minZInt == 0) {
bool v0InFront = transformed[indsIn[i]].z < 0.0f;
bool v1InFront = transformed[indsIn[i + 1]].z < 0.0f;
bool v2InFront = transformed[indsIn[i + 2]].z < 0.0f;
bool v0InFront = v0.z < 0.0f;
bool v1InFront = v1.z < 0.0f;
bool v2InFront = v2.z < 0.0f;
if (v0InFront && v1InFront && v2InFront) {
transformed[indsIn[i]].z = 0.0f;
transformed[indsIn[i + 1]].z = 0.0f;
transformed[indsIn[i + 2]].z = 0.0f;
v0.z = 0.0f;
v1.z = 0.0f;
v2.z = 0.0f;
}
}
if (maxZInt == 65535) {
bool v0Beyond = transformed[indsIn[i]].z >= 65535.0f;
bool v1Beyond = transformed[indsIn[i + 1]].z >= 65535.0f;
bool v2Beyond = transformed[indsIn[i + 2]].z >= 65535.0f;
bool v0Beyond = v0.z >= 65535.0f;
bool v1Beyond = v1.z >= 65535.0f;
bool v2Beyond = v2.z >= 65535.0f;
if (v0Beyond && v1Beyond && v2Beyond) {
transformed[indsIn[i]].z = 65535.0f;
transformed[indsIn[i + 1]].z = 65535.0f;
transformed[indsIn[i + 2]].z = 65535.0f;
v0.z = 65535.0f;
v1.z = 65535.0f;
v2.z = 65535.0f;
}
}
}
+1
View File
@@ -68,6 +68,7 @@ void IndexBufferProvokingLastToFirst(int prim, u16 *inds, int indsSize);
class SoftwareTransform {
public:
// indsSize is in indices, not bytes.
// NOTE: In case of clipping, this might write extra vertices after params.transformed.
static SoftwareTransformAction Transform(SoftwareTransformParams &params, int prim, u32 vertexType, const DecVtxFormat &decVtxFormat, int &numDecodedVerts, int vertsSize, int vertexCount, u16 *&inds, int indsSize, SoftwareTransformResult *result);
protected:
+10
View File
@@ -80,6 +80,15 @@ inline float roundToFloat24(float f) {
return retval;
}
inline float truncateToFloat24(float f) {
unsigned int i;
memcpy(&i, &f, 4);
i &= 0xFFFFFF00;
float retval;
memcpy(&retval, &i, 4);
return retval;
}
// TODO: Possibly use macros to disable expensive parts of stats tracking in release builds.
struct GPUStatsPerFrame {
// Per frame statistics
@@ -89,6 +98,7 @@ struct GPUStatsPerFrame {
int numDrawSyncs;
int numListSyncs;
int numFlushes;
int numSoftClippedTriangles;
int numBBOXJumps;
int numVertsSubmitted;
int numVertsDecoded;
+6 -1
View File
@@ -624,6 +624,10 @@ u32 GPUCommonHW::CheckGPUFeatures() const {
features |= GPU_USE_FRAGMENT_UBERSHADER;
}
if (!draw_->GetBugs().Has(Draw::Bugs::BROKEN_NAN_IN_CONDITIONAL)) {
features |= GPU_USE_VS_RANGE_CULLING;
}
return features;
}
@@ -1801,7 +1805,7 @@ void GPUCommonHW::FormatGPUStatsCommon(StringWriter &w) {
w.F(
"DL processing time: %0.2f ms, %d drawsync, %d listsync\n"
"Draw: %d (%d dec, %d culled), flushes %d, clears %d, bbox jumps %d\n"
"Vertices: %d dec: %d drawn: %d\n"
"Vertices: %d dec: %d drawn: %d clipped tris: %d\n"
"FBOs active: %d (evaluations: %d, created %d)\n"
"Textures: %d, dec: %d, invalidated: %d, hashed: %d kB, clut %d\n"
"readbacks %d (%d non-block), upload %d (cached %d), depal %d\n"
@@ -1823,6 +1827,7 @@ void GPUCommonHW::FormatGPUStatsCommon(StringWriter &w) {
gpuStats.perFrame.numVertsSubmitted,
gpuStats.perFrame.numVertsDecoded,
gpuStats.perFrame.numUncachedVertsDrawn,
gpuStats.perFrame.numSoftClippedTriangles,
(int)framebufferManager_->NumVFBs(),
gpuStats.perFrame.numFramebufferEvaluations,
gpuStats.perFrame.numFBOsCreated,
+16
View File
@@ -21,6 +21,7 @@
#include <cstring>
#include "Common/Common.h"
#include "Common/Data/Color/RGBAUtil.h"
#include "Core/MemMap.h"
@@ -217,4 +218,19 @@ struct TransformedVertex {
this->y = other.y + yoff;
memcpy(&this->z, &other.z, sizeof(*this) - sizeof(float) * 2);
}
static void Lerp(TransformedVertex *dest, TransformedVertex &a, TransformedVertex &b, float t) {
dest->x = a.x + (b.x - a.x) * t;
dest->y = a.y + (b.y - a.y) * t;
dest->z = a.z + (b.z - a.z) * t;
dest->pos_w = a.pos_w + (b.pos_w - a.pos_w) * t;
dest->u = a.u + (b.u - a.u) * t;
dest->v = a.v + (b.v - a.v) * t;
dest->uv_w = a.uv_w + (b.uv_w - a.uv_w) * t;
dest->fog = a.fog + (b.fog - a.fog) * t;
// note: colorBlend is backwards.
dest->color0_32 = colorBlend(b.color0_32, a.color0_32, t);
dest->color1_32 = colorBlend(b.color1_32, a.color1_32, t);
}
};
+1 -1
View File
@@ -31,4 +31,4 @@ void ProcessLine(const ClipVertexData &v0, const ClipVertexData &v1, BinManager
void ProcessTriangle(const ClipVertexData &v0, const ClipVertexData &v1, const ClipVertexData &v2, const ClipVertexData &provoking, BinManager &binner);
void ProcessRect(const ClipVertexData &v0, const ClipVertexData &v1, BinManager &binner);
}
} // namespace
-2
View File
@@ -153,8 +153,6 @@ WorldCoords TransformUnit::ModelToWorldNormal(const ModelCoords &coords) {
template <bool depthClamp, bool alwaysCheckRange>
static ScreenCoords ClipToScreenInternal(Vec3f scaled, const ClipCoords &coords, bool *outside_range_flag) {
ScreenCoords ret;
// Account for rounding for X and Y.
// TODO: Validate actual rounding range.
constexpr float SCREEN_BOUND = 4095.0f + (15.5f / 16.0f);
+7 -11
View File
@@ -43,10 +43,9 @@ enum class CullType {
OFF = 2,
};
struct ScreenCoords
{
ScreenCoords() {}
ScreenCoords(int x, int y, u16 z) : x(x), y(y), z(z) {}
struct ScreenCoords {
ScreenCoords() = default;
ScreenCoords(int _x, int _y, u16 _z) : x(_x), y(_y), z(_z) {}
int x;
int y;
@@ -54,24 +53,21 @@ struct ScreenCoords
Vec2<int> xy() const { return Vec2<int>(x, y); }
ScreenCoords operator * (const float t) const
{
ScreenCoords operator * (const float t) const {
return ScreenCoords((int)(x * t), (int)(y * t), (u16)(z * t));
}
ScreenCoords operator / (const int t) const
{
ScreenCoords operator / (const int t) const {
return ScreenCoords(x / t, y / t, z / t);
}
ScreenCoords operator + (const ScreenCoords& oth) const
{
ScreenCoords operator + (const ScreenCoords& oth) const {
return ScreenCoords(x + oth.x, y + oth.y, z + oth.z);
}
};
struct DrawingCoords {
DrawingCoords() {}
DrawingCoords() = default;
DrawingCoords(s16 x, s16 y) : x(x), y(y) {}
s16 x;