BBOX: Avoid z clipping issues by culling in clip space.

This commit is contained in:
Henrik Rydgård
2026-06-03 10:05:21 +02:00
parent b07fce1566
commit ab108f7f0c
2 changed files with 36 additions and 20 deletions
+24 -13
View File
@@ -194,8 +194,15 @@ bool DrawEngineCommon::TestBoundingBox(const void *vdata, const void *inds, int
// Try to skip NormalizeVertices if it's pure positions. No need to bother with a vertex decoder
// and a large vertex format.
// BBOX games:
// - Outrun 2006
// - Tekken 6 (FLOAT only)
// - Smash Court Tennis 3 (All formats)
// - Need for Speed Carbon
if ((vertType & 0xFFFFFF) == GE_VTYPE_POS_FLOAT && !inds) {
// Most games that use bbox use floating point bboxes (Outrun, Tekken 6 etc).
// Most games that use bbox use floating point bboxes (Outrun, Tekken 6, Smash Court Tennis 3, Need for Speed Carbon etc).
// memcpy(verts, vdata, sizeof(float) * 3 * vertexCount);
verts = (float *)vdata;
} else if ((vertType & 0xFFFFFF) == GE_VTYPE_POS_8BIT && !inds) {
@@ -213,6 +220,8 @@ bool DrawEngineCommon::TestBoundingBox(const void *vdata, const void *inds, int
u8 *temp_buffer = decoded_ + 65536 * 24;
if ((inds || (vertType & (GE_VTYPE_WEIGHT_MASK | GE_VTYPE_MORPHCOUNT_MASK)))) {
// Need for Speed Carbon ends up on this path! With a single bone weight.
u16 indexLowerBound = 0;
u16 indexUpperBound = (u16)vertexCount - 1;
@@ -259,9 +268,9 @@ bool DrawEngineCommon::TestBoundingBox(const void *vdata, const void *inds, int
}
// Unclear why the top/left is off by a pixel.
const int left = gstate.getOffsetX() + std::max(gstate.getRegionX1(), gstate.getScissorX1()) - 1;
const int top = gstate.getOffsetY() + std::max(gstate.getRegionY1(), gstate.getScissorY1()) - 1;
const int right = gstate.getOffsetX() + std::min(gstate.getRegionX2(), gstate.getScissorX2()) + 1;
const int left = gstate.getOffsetX() + std::max(gstate.getRegionX1(), gstate.getScissorX1()) - 1;
const int top = gstate.getOffsetY() + std::max(gstate.getRegionY1(), gstate.getScissorY1()) - 1;
const int right = gstate.getOffsetX() + std::min(gstate.getRegionX2(), gstate.getScissorX2()) + 1;
const int bottom = gstate.getOffsetY() + std::min(gstate.getRegionY2(), gstate.getScissorY2()) + 1;
// This is strange, it seems if the draw box is at all outside the 4096x4096 coordinate space, all checks pass.
@@ -271,6 +280,7 @@ bool DrawEngineCommon::TestBoundingBox(const void *vdata, const void *inds, int
}
// TODO: How accurate should we be?
// TODO: Use CrossSIMD.
int insideCount[6] = {0};
for (int i = 0; i < vertexCount; i++) {
// Complete the transform to see if the vertex should be ignored. Not sure if we need to go to these lengths...
@@ -286,26 +296,27 @@ bool DrawEngineCommon::TestBoundingBox(const void *vdata, const void *inds, int
insideCount[5]++;
}
const float invW = 1.0f / projpos[3];
const float w = projpos[3];
// const float invW = 1.0f / w;
const float screenpos[3] = {
projpos[0] * gstate.getViewportXScale() * invW + gstate.getViewportXCenter(),
projpos[1] * gstate.getViewportYScale() * invW + gstate.getViewportYCenter(),
projpos[2] * gstate.getViewportZScale() * invW + gstate.getViewportZCenter(),
(projpos[0] * gstate.getViewportXScale()) + gstate.getViewportXCenter() * w,
(projpos[1] * gstate.getViewportYScale()) + gstate.getViewportYCenter() * w,
(projpos[2] * gstate.getViewportZScale()) + gstate.getViewportZCenter() * w,
};
const float drawX = screenpos[0];
const float drawY = screenpos[1];
if (drawX >= left) {
if (drawX >= left * w) {
insideCount[0]++;
}
if (drawX <= right) {
if (drawX <= right * w) {
insideCount[1]++;
}
if (drawY >= top) {
if (drawY >= top * w) {
insideCount[2]++;
}
if (drawY <= bottom) {
if (drawY <= bottom * w) {
insideCount[3]++;
}
}
@@ -329,7 +340,7 @@ bool DrawEngineCommon::TestBoundingBox(const void *vdata, const void *inds, int
return true;
}
// This optionally culls collections of points against the four side planes, and always computes the min and max of Z and W.
// This optionally culls collections of points against the six planes, and always computes the min and max of Z and W.
//
// The result of that is then used to determine if we need to drop down to software transform+clip or we can hand
// off to hardware, with whatever capabilities are available.
+12 -7
View File
@@ -7,6 +7,7 @@
#include "Common/GraphicsContext.h"
#include "Common/LogReporting.h"
#include "Common/Math/SIMDHeaders.h"
#include "Common/Math/CrossSIMD.h"
#include "Common/Serialize/Serializer.h"
#include "Common/Serialize/SerializeFuncs.h"
#include "Common/Serialize/SerializeList.h"
@@ -1232,6 +1233,7 @@ void GPUCommon::Execute_BoundingBox(u32 op, u32 diff) {
int checkSize = count - 0x100;
currentList->bboxResult = drawEngineCommon_->TestBoundingBox(control_points, inds, checkSize, dec, vertType);
} else {
// This is the normal case that pretty much always happens, the others are esoteric.
currentList->bboxResult = drawEngineCommon_->TestBoundingBox(control_points, inds, count, dec, vertType);
}
AdvanceVerts(gstate.vertType, count, bytesRead);
@@ -2197,18 +2199,21 @@ bool GPUCommon::SetRestrictPrims(std::string_view rule) {
}
void GPUCommon::UpdateMatrixProducts() {
// We clean the dirty flags at the end.
// Compute any dirty product matrices.
if (gstate_c.IsDirty(DIRTY_VIEW_PROJ_MATRIX)) {
float view[4 * 4];
ConvertMatrix4x3To4x4(view, gstate.viewMatrix);
Matrix4ByMatrix4(gstate_c.viewproj, view, gstate.projMatrix);
gstate_c.Clean(DIRTY_VIEW_PROJ_MATRIX);
Mat4x3F32 view(gstate.viewMatrix);
Mat4F32 proj(gstate.projMatrix);
Mul4x3By4x4(view, proj).Store(gstate_c.viewproj);
}
if (gstate_c.IsDirty(DIRTY_WORLD_VIEW_PROJ_MATRIX)) {
float world[4 * 4];
ConvertMatrix4x3To4x4(world, gstate.worldMatrix);
Matrix4ByMatrix4(gstate_c.worldviewproj, world, gstate_c.viewproj);
Mat4x3F32 world(gstate.worldMatrix);
Mat4F32 viewproj(gstate_c.viewproj);
Mul4x3By4x4(world, viewproj).Store(gstate_c.worldviewproj);
}
if (gstate_c.IsDirty(DIRTY_CULL_MATRIX)) {
// Modify the transform matrix to take the viewport into account before culling. This is not necessary
// for most games, but there are games that rely on outside-viewport draws (such as Dante's Inferno)'s post