Implement depth clamp (for safe cases) in the software transform pipeline, and make sure triangle culling is always done.

This commit is contained in:
Henrik Rydgård
2026-05-26 17:21:02 +02:00
parent a6b146b74b
commit d60d190236
6 changed files with 143 additions and 78 deletions
+127 -63
View File
@@ -109,7 +109,7 @@ static bool IsReallyAClear(const TransformedVertex *transformed, int numVerts, f
}
// At the end, this calls ProjectClipAndExpand which will expand rectangles as necessary, or apply culling.
void SoftwareTransform::Transform(const float projMtx[16], Lin::Vec3 vpScale, Lin::Vec3 vpOffset, int prim, u32 vertType, const DecVtxFormat &decVtxFormat, int &numDecodedVerts, int vertsSize, int vertexCount, u16 *&inds, int indsSize, SoftwareTransformResult *result) {
SoftwareTransformAction SoftwareTransform::Transform(const float projMtx[16], Lin::Vec3 vpScale, Lin::Vec3 vpOffset, 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;
@@ -209,9 +209,8 @@ void SoftwareTransform::Transform(const float projMtx[16], Lin::Vec3 vpScale, Li
if (!(params_.everUsedEqualDepth && gstate.isClearModeDepthMask() && result->depth > 0.0f && result->depth < 1.0f)) {
result->color = transformed[1].color0_32;
result->depth = depth;
result->action = SW_CLEAR;
gpuStats.perFrame.numClears++;
return;
return SW_CLEAR;
}
}
}
@@ -363,23 +362,15 @@ void SoftwareTransform::Transform(const float projMtx[16], Lin::Vec3 vpScale, Li
fogCoef = (v[2] + fog_end) * fog_slope;
// Then transform by the projection.
float xyzw[4];
Vec3ByMatrix44(xyzw, v, projMtx);
// Here we also need to do the perspective device and apply the viewport.
// TODO: Move this to ProjectClipAndExpand.
float recip = 1.0f / xyzw[3];
Lin::Vec3 xyz = vpOffset + vpScale.scaledBy(Lin::Vec3(xyzw[0] * recip, xyzw[1] * recip, xyzw[2] * recip));
transformed[index].x = xyz.x;
transformed[index].y = xyz.y;
transformed[index].z = xyz.z;
transformed[index].pos[3] = xyzw[3];
Vec3ByMatrix44(transformed[index].pos, v, projMtx);
transformed[index].fog = fogCoef;
memcpy(&transformed[index].uv, uv, 3 * sizeof(float));
transformed[index].color0_32 = c0.ToRGBA();
transformed[index].color1_32 = c1.ToRGBA();
// Projection happens later in ProjectClipAndExpand.
// Vertex depth rounding is done in the shader, to simulate the 16-bit depth buffer.
}
}
@@ -401,12 +392,28 @@ void SoftwareTransform::Transform(const float projMtx[16], Lin::Vec3 vpScale, Li
}
}
_dbg_assert_(result->action == SW_NOT_READY);
ProjectClipAndExpand(prim, vertexCount, vertType, inds, indsSize, numDecodedVerts, vertsSize, result);
return ProjectClipAndExpand(prim, vertexCount, vertType, inds, indsSize, numDecodedVerts, vertsSize, result);
}
void SoftwareTransform::ProjectClipAndExpand(int prim, int vertexCount, u32 vertType, u16 *&inds, int indsSize, int &numDecodedVerts, int vertsSize, SoftwareTransformResult *result) {
// Modifies the vertices in-place. Applies viewport and projection.
// TODO: SIMD.
void SoftwareTransform::ProjectVertices(TransformedVertex *transformed, int vertexCount) {
Lin::Vec3 vpOffset(gstate.getViewportXCenter(), gstate.getViewportYCenter(), gstate.getViewportZCenter());
Lin::Vec3 vpScale(gstate.getViewportXScale(), gstate.getViewportYScale(), gstate.getViewportZScale());
for (int i = 0; i < vertexCount; i++) {
// Here we also need to do the perspective device and apply the viewport.
// TODO: Move this to ProjectClipAndExpand.
const float w = transformed[i].pos_w;
const float recip = 1.0f / w;
Lin::Vec3 xyz = vpOffset + vpScale.scaledBy(Lin::Vec3(transformed[i].x * recip, transformed[i].y * recip, transformed[i].z * recip));
transformed[i].x = xyz.x;
transformed[i].y = xyz.y;
transformed[i].z = xyz.z;
}
}
SoftwareTransformAction SoftwareTransform::ProjectClipAndExpand(int prim, int vertexCount, u32 vertType, u16 *&inds, int indsSize, int &numDecodedVerts, int vertsSize, SoftwareTransformResult *result) {
TransformedVertex *transformed = params_.transformed;
TransformedVertex *transformedExpanded = params_.transformedExpanded;
bool throughmode = (vertType & GE_VTYPE_THROUGH_MASK) != 0;
@@ -419,10 +426,15 @@ void SoftwareTransform::ProjectClipAndExpand(int prim, int vertexCount, u32 vert
bool useBufferedRendering = fbman->UseBufferedRendering();
if (prim == GE_PRIM_RECTANGLES) {
// TODO: We're now able to cull here.
if (!throughmode) {
ProjectVertices(transformed, numDecodedVerts);
}
if (!ExpandRectangles(vertexCount, numDecodedVerts, vertsSize, inds, indsSize, transformed, transformedExpanded, numTrans, throughmode, &result->pixelMapped)) {
result->drawNumTrans = 0;
result->pixelMapped = false;
return;
return SW_CULLED;
}
result->drawBuffer = transformedExpanded;
@@ -440,26 +452,71 @@ void SoftwareTransform::ProjectClipAndExpand(int prim, int vertexCount, u32 vert
}
}
} else if (prim == GE_PRIM_POINTS) {
if (!throughmode) {
ProjectVertices(transformed, numDecodedVerts);
}
result->pixelMapped = false;
if (!ExpandPoints(vertexCount, numDecodedVerts, vertsSize, inds, indsSize, transformed, transformedExpanded, numTrans, throughmode)) {
result->drawNumTrans = 0;
return;
return SW_CULLED;
}
result->drawBuffer = transformedExpanded;
} else if (prim == GE_PRIM_LINES) {
if (!throughmode) {
ProjectVertices(transformed, numDecodedVerts);
}
result->pixelMapped = false;
if (!ExpandLines(vertexCount, numDecodedVerts, vertsSize, inds, indsSize, transformed, transformedExpanded, numTrans, throughmode)) {
result->drawNumTrans = 0;
return;
return SW_CULLED;
}
result->drawBuffer = transformedExpanded;
} else {
// We can simply draw the unexpanded buffer.
} else if (prim == GE_PRIM_TRIANGLES) {
// Triangles. We can simply draw the unexpanded buffer, although we do also take the opportunity to perform culling.
numTrans = vertexCount;
result->pixelMapped = false;
// If we don't support custom cull in the shader, process it here.
if (!gstate_c.Use(GPU_USE_CULL_DISTANCE) && vertexCount > 0 && !throughmode) {
if (throughmode) {
// Nothing to do, pass the vertices right through as-is. Well, we can go look for pixel mapping, but we don't do any culling or clipping.
if (g_Config.bSmart2DTexFiltering && !gstate_c.textureIsVideo) {
// We check some common cases for pixel mapping.
// TODO: It's not really optimal that some previous step has removed the triangle strip.
if (vertexCount <= 6 && prim == GE_PRIM_TRIANGLES) {
// It's enough to check UV deltas vs pos deltas between vertex pairs:
// 0-1 1-3 3-2 2-0. Maybe can even skip the last one. Probably some simple math can get us that sequence.
// Unfortunately we need to reverse the previous UV scaling operation. Fortunately these are powers of two
// so the operations are exact.
bool pixelMapped = true;
const u16 *indsIn = (const u16 *)inds;
const float uscale = gstate_c.curTextureWidth;
const float vscale = gstate_c.curTextureHeight;
for (int t = 0; t < vertexCount; t += 3) {
struct { int a; int b; } pairs[] = {{0, 1}, {1, 2}, {2, 0}};
for (int i = 0; i < ARRAY_SIZE(pairs); i++) {
int a = indsIn[t + pairs[i].a];
int b = indsIn[t + pairs[i].b];
float du = fabsf((transformed[a].u - transformed[b].u) * uscale);
float dv = fabsf((transformed[a].v - transformed[b].v) * vscale);
float dx = fabsf(transformed[a].x - transformed[b].x);
float dy = fabsf(transformed[a].y - transformed[b].y);
if (du != dx || dv != dy) {
pixelMapped = false;
}
}
if (!pixelMapped) {
break;
}
}
result->pixelMapped = pixelMapped;
}
}
} else {
// 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;
@@ -468,28 +525,34 @@ void SoftwareTransform::ProjectClipAndExpand(int prim, int vertexCount, u32 vert
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 = truncateToFloat24(transformed[indsIn[i]].z);
float z = transformed[indsIn[i]].z;
float w = transformed[indsIn[i]].pos_w;
if (z > w)
if (z > w) {
outsideZ[i] = 1;
else if (z < -w)
} else if (z < -w) {
outsideZ[i] = -1;
else
} else {
outsideZ[i] = 0;
}
}
// Now, for each primitive type, throw away the indices if:
// - Depth clamp on, and ALL verts are outside *in the same direction*.
// - Depth clamp off, and ANY vert is outside.
if (prim == GE_PRIM_TRIANGLES && gstate.isDepthClipEnabled()) {
u16 *origInds = inds;
// TODO: We should either merge the two loops, or avoid the second loop if no culling is needed.
// 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.
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;
}
memcpy(indsOut, indsIn + i, 3 * sizeof(uint16_t));
indsOut += 3;
numTrans += 3;
@@ -511,46 +574,47 @@ void SoftwareTransform::ProjectClipAndExpand(int prim, int vertexCount, u32 vert
inds = newInds;
}
} else if (throughmode && g_Config.bSmart2DTexFiltering && !gstate_c.textureIsVideo) {
// We check some common cases for pixel mapping.
// TODO: It's not really optimal that some previous step has removed the triangle strip.
if (vertexCount <= 6 && prim == GE_PRIM_TRIANGLES) {
// It's enough to check UV deltas vs pos deltas between vertex pairs:
// 0-1 1-3 3-2 2-0. Maybe can even skip the last one. Probably some simple math can get us that sequence.
// Unfortunately we need to reverse the previous UV scaling operation. Fortunately these are powers of two
// so the operations are exact.
bool pixelMapped = true;
const u16 *indsIn = (const u16 *)inds;
const float uscale = gstate_c.curTextureWidth;
const float vscale = gstate_c.curTextureHeight;
for (int t = 0; t < vertexCount; t += 3) {
struct { int a; int b; } pairs[] = { {0, 1}, {1, 2}, {2, 0} };
for (int i = 0; i < ARRAY_SIZE(pairs); i++) {
int a = indsIn[t + pairs[i].a];
int b = indsIn[t + pairs[i].b];
float du = fabsf((transformed[a].u - transformed[b].u) * uscale);
float dv = fabsf((transformed[a].v - transformed[b].v) * vscale);
float dx = fabsf(transformed[a].x - transformed[b].x);
float dy = fabsf(transformed[a].y - transformed[b].y);
if (du != dx || dv != dy) {
pixelMapped = false;
}
}
if (!pixelMapped) {
break;
// Now that we're done culling and generating clipped vertices if needed (not yet implemented), we go ahead and project.
ProjectVertices(transformed, numDecodedVerts);
// Alright! Now, we can clamp the far plane, if the hardware lacks support for doing it for us.
// Now, this can only be done exactly if all vertices in a triangle are beyond the far plane. If not
// we need to cut it in two parts to clamp accurately. However, in most cases that matter, this is fine.
if (!gstate_c.Use(GPU_USE_DEPTH_CLAMP) && gstate.isDepthClipEnabled() && gstate.getDepthRangeMax() == 0xFFFF) {
for (int i = 0; i < vertexCount - 2; i += 3) {
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 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;
// Clamp the Z when we detect it's safe to do so.
// Fixes the sky problem in Shadow of Destiny (#9545)
if (v0Beyond && v1Beyond && v2Beyond) {
transformed[indsIn[i]].z = 65535.0f;
transformed[indsIn[i + 1]].z = 65535.0f;
transformed[indsIn[i + 2]].z = 65535.0f;
} else if (v0InFront && v1InFront && v2InFront) {
transformed[indsIn[i]].z = 0.0f;
transformed[indsIn[i + 1]].z = 0.0f;
transformed[indsIn[i + 2]].z = 0.0f;
}
}
result->pixelMapped = pixelMapped;
}
}
} else {
_dbg_assert_(false);
}
if (gstate.isModeClear()) {
gpuStats.perFrame.numClears++;
}
result->action = SW_DRAW_INDEXED;
result->drawNumTrans = numTrans;
return SW_DRAW_INDEXED;
}
bool SoftwareTransform::ExpandRectangles(int vertexCount, int &numDecodedVerts, int vertsSize, u16 *&inds, int indsSize, const TransformedVertex *transformed, TransformedVertex *transformedExpanded, int &numTrans, bool throughmode, bool *pixelMappedExactly) const {
+5 -4
View File
@@ -28,13 +28,12 @@ class FramebufferManagerCommon;
class TextureCacheCommon;
enum SoftwareTransformAction {
SW_NOT_READY,
SW_DRAW_INDEXED,
SW_CLEAR,
SW_CULLED, // don't draw
};
struct SoftwareTransformResult {
SoftwareTransformAction action;
u32 color;
float depth;
@@ -71,11 +70,13 @@ public:
SoftwareTransform(SoftwareTransformParams &params) : params_(params) {}
// indsSize is in indices, not bytes.
void Transform(const float projMtx[16], Lin::Vec3 vpScale, Lin::Vec3 vpOffset, int prim, u32 vertexType, const DecVtxFormat &decVtxFormat, int &numDecodedVerts, int vertsSize, int vertexCount, u16 *&inds, int indsSize, SoftwareTransformResult *result);
SoftwareTransformAction Transform(const float projMtx[16], Lin::Vec3 vpScale, Lin::Vec3 vpOffset, int prim, u32 vertexType, const DecVtxFormat &decVtxFormat, int &numDecodedVerts, int vertsSize, int vertexCount, u16 *&inds, int indsSize, SoftwareTransformResult *result);
protected:
// NOTE: The viewport must be up to date!
void ProjectClipAndExpand(int prim, int vertexCount, u32 vertType, u16 *&inds, int indsSize, int &numDecodedVerts, int vertsSize, SoftwareTransformResult *result);
SoftwareTransformAction ProjectClipAndExpand(int prim, int vertexCount, u32 vertType, u16 *&inds, int indsSize, int &numDecodedVerts, int vertsSize, SoftwareTransformResult *result);
static void ProjectVertices(TransformedVertex *transformed, int vertexCount);
bool ExpandRectangles(int vertexCount, int &numDecodedVerts, int vertsSize, u16 *&inds, int indsSize, const TransformedVertex *transformed, TransformedVertex *transformedExpanded, int &numTrans, bool throughmode, bool *pixelMappedExactly) const;
static bool ExpandLines(int vertexCount, int &numDecodedVerts, int vertsSize, u16 *&inds, int indsSize, const TransformedVertex *transformed, TransformedVertex *transformedExpanded, int &numTrans, bool throughmode) ;
static bool ExpandPoints(int vertexCount, int &numDecodedVerts, int vertsSize, u16 *&inds, int indsSize, const TransformedVertex *transformed, TransformedVertex *transformedExpanded, int &numTrans, bool throughmode) ;
+3 -3
View File
@@ -423,7 +423,7 @@ void DrawEngineD3D11::Flush() {
params.allowSeparateAlphaClear = false; // D3D11 doesn't support separate alpha clears
SoftwareTransform swTransform(params);
swTransform.Transform(gstate.projMatrix, gstate.getViewportScale(), gstate.getViewportOffset(), prim, swDec->VertexType(), swDec->GetDecVtxFmt(), numDecodedVerts_, VERTEX_BUFFER_MAX, vertexCount, inds, RemainingIndices(inds), &result);
const SoftwareTransformAction action = swTransform.Transform(gstate.projMatrix, gstate.getViewportScale(), gstate.getViewportOffset(), prim, swDec->VertexType(), swDec->GetDecVtxFmt(), numDecodedVerts_, VERTEX_BUFFER_MAX, vertexCount, inds, RemainingIndices(inds), &result);
if (result.setSafeSize)
framebufferManager_->SetSafeSize(result.safeWidth, result.safeHeight);
@@ -439,7 +439,7 @@ void DrawEngineD3D11::Flush() {
ApplyDrawState(prim);
ApplyDrawStateLate(result.setStencil, result.stencilValue);
if (result.action == SW_DRAW_INDEXED) {
if (action == SW_DRAW_INDEXED) {
D3D11VertexShader *vshader;
D3D11FragmentShader *fshader;
shaderManager_->GetShaders(prim, swDec->VertexType(), &vshader, &fshader, pipelineState_, false, false, decOptions_.expandAllWeightsToFloat, true);
@@ -474,7 +474,7 @@ void DrawEngineD3D11::Flush() {
pushInds_->EndPush(context_);
context_->IASetIndexBuffer(pushInds_->Buf(), DXGI_FORMAT_R16_UINT, iOffset);
context_->DrawIndexed(result.drawNumTrans, 0, 0);
} else if (result.action == SW_CLEAR) {
} else if (action == SW_CLEAR) {
u32 clearColor = result.color;
float clearDepth = result.depth;
+3 -3
View File
@@ -371,7 +371,7 @@ void DrawEngineGLES::Flush() {
params.allowSeparateAlphaClear = true;
SoftwareTransform swTransform(params);
swTransform.Transform(gstate.projMatrix, gstate.getViewportScale(), gstate.getViewportOffset(), prim, swDec->VertexType(), swDec->GetDecVtxFmt(), numDecodedVerts_, VERTEX_BUFFER_MAX, vertexCount, inds, RemainingIndices(inds), &result);
const SoftwareTransformAction action = swTransform.Transform(gstate.projMatrix, gstate.getViewportScale(), gstate.getViewportOffset(), prim, swDec->VertexType(), swDec->GetDecVtxFmt(), numDecodedVerts_, VERTEX_BUFFER_MAX, vertexCount, inds, RemainingIndices(inds), &result);
if (result.setSafeSize)
framebufferManager_->SetSafeSize(result.safeWidth, result.safeHeight);
@@ -396,13 +396,13 @@ void DrawEngineGLES::Flush() {
goto bail;
}
if (result.action == SW_DRAW_INDEXED) {
if (action == SW_DRAW_INDEXED) {
vertexBufferOffset = (uint32_t)frameData.pushVertex->Push(result.drawBuffer, numDecodedVerts_ * sizeof(TransformedVertex), 4, &vertexBuffer);
indexBufferOffset = (uint32_t)frameData.pushIndex->Push(inds, sizeof(uint16_t) * result.drawNumTrans, 2, &indexBuffer);
render_->DrawIndexed(
softwareInputLayout_, vertexBuffer, vertexBufferOffset, indexBuffer, indexBufferOffset,
glprim[prim], result.drawNumTrans, GL_UNSIGNED_SHORT);
} else if (result.action == SW_CLEAR) {
} else if (action == SW_CLEAR) {
u32 clearColor = result.color;
float clearDepth = result.depth;
+2 -2
View File
@@ -585,11 +585,11 @@ u32 GPUCommonHW::CheckGPUFeatures() const {
features |= GPU_USE_BLEND_MINMAX;
}
if (draw_->GetDeviceCaps().maxClipDistances >= 3) {
if (draw_->GetDeviceCaps().maxClipDistances >= 3 && g_Config.bHardwareTransform) {
features |= GPU_USE_CLIP_DISTANCE;
}
if (draw_->GetDeviceCaps().maxCullDistances >= 1) {
if (draw_->GetDeviceCaps().maxCullDistances >= 1 && g_Config.bHardwareTransform) {
features |= GPU_USE_CULL_DISTANCE;
}
+3 -3
View File
@@ -433,14 +433,14 @@ void DrawEngineVulkan::Flush() {
params.everUsedEqualDepth = everUsedEqualDepth_;
SoftwareTransform swTransform(params);
swTransform.Transform(gstate.projMatrix, gstate.getViewportScale(), gstate.getViewportOffset(), prim, swDec->VertexType(), swDec->GetDecVtxFmt(), numDecodedVerts_, VERTEX_BUFFER_MAX, vertexCount, inds, RemainingIndices(inds), &result);
const SoftwareTransformAction action = swTransform.Transform(gstate.projMatrix, gstate.getViewportScale(), gstate.getViewportOffset(), prim, swDec->VertexType(), swDec->GetDecVtxFmt(), numDecodedVerts_, VERTEX_BUFFER_MAX, vertexCount, inds, RemainingIndices(inds), &result);
if (result.setSafeSize)
framebufferManager_->SetSafeSize(result.safeWidth, result.safeHeight);
// Only here, where we know whether to clear or to draw primitives, should we actually set the current framebuffer! Because that gives use the opportunity
// to use a "pre-clear" render pass, for high efficiency on tilers.
if (result.action == SW_DRAW_INDEXED) {
if (action == SW_DRAW_INDEXED) {
if (textureNeedsApply) {
gstate_c.pixelMapped = result.pixelMapped;
gstate_c.dstSquared = false;
@@ -528,7 +528,7 @@ void DrawEngineVulkan::Flush() {
u32 vbOffset = (uint32_t)pushVertex_->Push(result.drawBuffer, numDecodedVerts_ * sizeof(TransformedVertex), 4, &vbuf);
u32 ibOffset = (uint32_t)pushIndex_->Push(inds, sizeof(short) * result.drawNumTrans, 4, &ibuf);
renderManager->DrawIndexed(descSetIndex, ARRAY_SIZE(dynamicUBOOffsets), dynamicUBOOffsets, vbuf, vbOffset, ibuf, ibOffset, result.drawNumTrans, 1);
} else if (result.action == SW_CLEAR) {
} else if (action == SW_CLEAR) {
// Note: we won't get here if the clear is alpha but not color, or color but not alpha.
bool clearColor = gstate.isClearModeColorMask();
bool clearAlpha = gstate.isClearModeAlphaMask(); // and stencil