Correct culling in case of some weird viewport setups

This commit is contained in:
Henrik Rydgård committed 2026-05-23 14:49:17 +02:00
1 parent fa856fbf68
commit 2eca0123f1
5 files changed
+109 -24

No files matched your search

+9
View File
@@ -66,6 +66,15 @@ static inline float32x4_t vmlaq_laneq_f32(float32x4_t a, float32x4_t b, float32x
}
}
static inline float32x4_t vdupq_laneq_f32(float32x4_t vec, int lane) {
switch (lane & 3) {
case 0: return vdupq_lane_f32(vget_low_f32(vec), 0);
case 1: return vdupq_lane_f32(vget_low_f32(vec), 1);
case 2: return vdupq_lane_f32(vget_high_f32(vec), 0);
default: return vdupq_lane_f32(vget_high_f32(vec), 1);
}
}
#define vfmaq_laneq_f32 vmlaq_laneq_f32
static inline uint32x4_t vcgezq_f32(float32x4_t v) {
+92 -14
View File
@@ -328,7 +328,7 @@ bool DrawEngineCommon::TestBoundingBox(const void *vdata, const void *inds, int
// corners. That way we can cull more draws quite cheaply.
// We could take the min/max during the regular vertex decode, and just skip the draw call if it's trivially culled.
// This would help games like Midnight Club (that one does a lot of out-of-bounds drawing) immensely.
bool DrawEngineCommon::TestBoundingBoxFast(const void *vdata, int vertexCount, const VertexDecoder *dec, u32 vertType) {
bool DrawEngineCommon::TestBoundingBoxFast(const float *worldViewProj, const void *vdata, int vertexCount, const VertexDecoder *dec, u32 vertType) {
SimpleVertex *corners = (SimpleVertex *)(decoded_ + 65536 * 12);
float *verts = (float *)(decoded_ + 65536 * 18);
@@ -346,13 +346,47 @@ bool DrawEngineCommon::TestBoundingBoxFast(const void *vdata, int vertexCount, c
// TODO: Possibly do the plane tests directly against the source formats instead of converting.
switch (vertType & GE_VTYPE_POS_MASK) {
case GE_VTYPE_POS_8BIT:
{
#if PPSSPP_ARCH(SSE2)
__m128 scaleFactor = _mm_set1_ps(1.0f / 128.0f);
for (int i = 0; i < vertexCount; i++) {
const s8 *data = (const s8 *)vdata + i * stride + offset;
// Load 4 bytes (only first 3 will be used, 4th doesn't matter)
int32_t temp;
memcpy(&temp, data, sizeof(temp));
__m128i bits8 = _mm_cvtsi32_si128(temp);
// Unpack 8->16 and 16->32, placing the original bytes in the high byte of each 32-bit lane
bits8 = _mm_unpacklo_epi8(bits8, bits8);
__m128i bits32 = _mm_unpacklo_epi16(bits8, bits8);
// Sign extend with a single shift right by 24
bits32 = _mm_srai_epi32(bits32, 24);
__m128 pos = _mm_mul_ps(_mm_cvtepi32_ps(bits32), scaleFactor);
_mm_storeu_ps(verts + i * 3, pos);
}
#elif PPSSPP_ARCH(ARM_NEON)
for (int i = 0; i < vertexCount; i++) {
const s8 *data = (const s8 *)vdata + i * stride + offset;
// Load 4 bytes (only first 3 will be used, 4th doesn't matter)
int32_t temp;
memcpy(&temp, data, sizeof(temp));
int32x2_t data32x2 = vdup_n_s32(temp);
int8x8_t data8 = vreinterpret_s8_s32(data32x2);
// Sign extend 8-bit to 16-bit, then to 32-bit
int16x8_t data16 = vmovl_s8(data8);
int32x4_t data32 = vmovl_s16(vget_low_s16(data16));
float32x4_t pos = vcvtq_n_f32_s32(data32, 7); // >> 7 = division by 128.0f
vst1q_f32(verts + i * 3, pos);
}
#else
for (int i = 0; i < vertexCount; i++) {
const s8 *data = (const s8 *)vdata + i * stride + offset;
for (int j = 0; j < 3; j++) {
verts[i * 3 + j] = data[j] * (1.0f / 128.0f);
}
}
#endif
break;
}
case GE_VTYPE_POS_16BIT:
{
#if PPSSPP_ARCH(SSE2)
@@ -389,6 +423,50 @@ bool DrawEngineCommon::TestBoundingBoxFast(const void *vdata, int vertexCount, c
break;
}
// 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
// processing effects, and we don't want to cull those.
// Potentially we should cache this transform matrix too, but hopefully this is not a bottleneck.
// I guess we could also do this directly when computing worldviewproj...
const float vpXCenter = gstate.getViewportXCenter();
const float vpYCenter = gstate.getViewportYCenter();
const float vpXScale = gstate.getViewportXScale();
const float vpYScale = gstate.getViewportYScale();
const int scissorX2 = gstate.getScissorX2();
const int scissorY2 = gstate.getScissorY2();
// Check for weird scaling that can make graphics extend beyond the viewport.
// NOTE: These checks are not bullet proof.
float mtx[16];
if (vpXCenter != 2048.0f || vpYCenter != 2048.0f || vpXScale < ((scissorX2 + 1) >> 1) && vpYScale < ((scissorY2 + 1) >> 1)) {
// Note that the PSP does not clip against the viewport.
const Vec2f baseOffset = Vec2f(gstate.getOffsetX(), gstate.getOffsetY());
// Region1 (rate) is used as an X1/Y1 here, matching PSP behavior.
Vec2f minOffset = baseOffset + Vec2f(std::max(gstate.getRegionX1(), gstate.getScissorX1()), std::max(gstate.getRegionY1(), gstate.getScissorY1()));
Vec2f maxOffset = baseOffset + Vec2f(std::min(gstate.getRegionX2(), gstate.getScissorX2()), std::min(gstate.getRegionY2(), gstate.getScissorY2()));
// Now let's apply the viewport to our scissor/region + offset range.
Vec2f inverseViewportScale = Vec2f(1.0f / gstate.getViewportXScale(), 1.0f / gstate.getViewportYScale());
Vec2f minViewport = (minOffset - Vec2f(gstate.getViewportXCenter(), gstate.getViewportYCenter())) * inverseViewportScale;
Vec2f maxViewport = (maxOffset - Vec2f(gstate.getViewportXCenter(), gstate.getViewportYCenter())) * inverseViewportScale;
Vec2f viewportInvSize = Vec2f(1.0f / (maxViewport.x - minViewport.x), 1.0f / (maxViewport.y - minViewport.y));
Lin::Matrix4x4 applyViewport{};
// Scale to the viewport's size.
applyViewport.xx = 2.0f * viewportInvSize.x;
applyViewport.yy = 2.0f * viewportInvSize.y;
applyViewport.zz = 1.0f;
applyViewport.ww = 1.0f;
// And offset to the viewport's centers.
applyViewport.wx = -(maxViewport.x + minViewport.x) * viewportInvSize.x;
applyViewport.wy = -(maxViewport.y + minViewport.y) * viewportInvSize.y;
// TODO: Optimize.
Matrix4ByMatrix4(mtx, worldViewProj, applyViewport.m);
}
// We only check the 4 sides. Near/far won't likely make a huge difference.
// We test one vertex against 4 planes to get some SIMD. Vertices need to be transformed to world space
// for testing, don't want to re-do that, so we have to use that "pivot" of the data.
@@ -407,10 +485,10 @@ bool DrawEngineCommon::TestBoundingBoxFast(const void *vdata, int vertexCount, c
alignas(16) static const float sse_planes[8] = { 1, -1, 1, -1 };
const __m128 worldX = _mm_loadu_ps(gstate_c.worldviewproj);
const __m128 worldY = _mm_loadu_ps(gstate_c.worldviewproj + 4);
const __m128 worldZ = _mm_loadu_ps(gstate_c.worldviewproj + 8);
const __m128 worldW = _mm_loadu_ps(gstate_c.worldviewproj + 12);
const __m128 worldX = _mm_loadu_ps(worldViewProj);
const __m128 worldY = _mm_loadu_ps(worldViewProj + 4);
const __m128 worldZ = _mm_loadu_ps(worldViewProj + 8);
const __m128 worldW = _mm_loadu_ps(worldViewProj + 12);
const __m128 planesXY = _mm_load_ps(sse_planes);
__m128 inside = _mm_set1_ps(0.0f);
for (int i = 0; i < vertexCount; i++) {
@@ -438,10 +516,10 @@ bool DrawEngineCommon::TestBoundingBoxFast(const void *vdata, int vertexCount, c
return _mm_movemask_ps(inside) == 0xF;
#elif PPSSPP_ARCH(ARM_NEON)
alignas(16) static const float planesXY[4] = { 1, -1, 1, -1 };
const float32x4_t worldX = vld1q_f32(gstate_c.worldviewproj);
const float32x4_t worldY = vld1q_f32(gstate_c.worldviewproj + 4);
const float32x4_t worldZ = vld1q_f32(gstate_c.worldviewproj + 8);
const float32x4_t worldW = vld1q_f32(gstate_c.worldviewproj + 12);
const float32x4_t worldX = vld1q_f32(worldViewProj);
const float32x4_t worldY = vld1q_f32(worldViewProj + 4);
const float32x4_t worldZ = vld1q_f32(worldViewProj + 8);
const float32x4_t worldW = vld1q_f32(worldViewProj + 12);
const float32x4_t planesMul = vld1q_f32(planesXY);
uint32x4_t inside = vdupq_n_u32(0);
for (int i = 0; i < vertexCount; i++) {
@@ -458,7 +536,7 @@ bool DrawEngineCommon::TestBoundingBoxFast(const void *vdata, int vertexCount, c
// Build [x, x, y, y] using vdup_lane
float32x2_t xy = vget_low_f32(clippos);
float32x4_t posXY = vcombine_f32(vdup_lane_f32(xy, 0), vdup_lane_f32(xy, 1)); // [x, x, y, y]
float32x4_t posW = vdupq_lane_f32(vget_high_f32(clippos), 1); // [w, w, w, w]
float32x4_t posW = vdupq_laneq_f32(clippos, 3); // [w, w, w, w]
float32x4_t planeDist = vmlaq_f32(posW, posXY, planesMul);
inside = vorrq_u32(inside, vcgeq_f32(planeDist, vdupq_n_f32(0.0f)));
}
@@ -467,10 +545,10 @@ bool DrawEngineCommon::TestBoundingBoxFast(const void *vdata, int vertexCount, c
#elif 0 && PPSSPP_ARCH(LOONGARCH64_LSX)
// NOTE: Untested
alignas(16) static const float planesXY[4] = { 1, -1, 1, -1 };
const __m128 worldX = (__m128)__lsx_vld(gstate_c.worldviewproj, 0);
const __m128 worldY = (__m128)__lsx_vld(gstate_c.worldviewproj + 4, 0);
const __m128 worldZ = (__m128)__lsx_vld(gstate_c.worldviewproj + 8, 0);
const __m128 worldW = (__m128)__lsx_vld(gstate_c.worldviewproj + 12, 0);
const __m128 worldX = (__m128)__lsx_vld(worldViewProj, 0);
const __m128 worldY = (__m128)__lsx_vld(worldViewProj + 4, 0);
const __m128 worldZ = (__m128)__lsx_vld(worldViewProj + 8, 0);
const __m128 worldW = (__m128)__lsx_vld(worldViewProj + 12, 0);
const __m128 planesMul = (__m128)__lsx_vld(planesXY, 0);
__m128 inside = (__m128)__lsx_vreplfr2vr_s(0.0f);
for (int i = 0; i < vertexCount; i++) {
+1 -1
View File
@@ -104,7 +104,7 @@ public:
// This is a less accurate version of TestBoundingBox, but faster. Can have more false positives.
// Doesn't support indexing.
bool TestBoundingBoxFast(const void *control_points, int vertexCount, const VertexDecoder *dec, u32 vertType);
bool TestBoundingBoxFast(const float *worldViewProj, const void *control_points, int vertexCount, const VertexDecoder *dec, u32 vertType);
bool TestBoundingBoxThrough(const void *vdata, int vertexCount, const VertexDecoder *dec, u32 vertType, int *bytesRead);
void FlushPartialDecode() {
+2 -2
View File
@@ -1047,7 +1047,7 @@ void GPUCommonHW::Execute_Prim(u32 op, u32 diff) {
bool passCulling = PASSES_CULLING;
if (!passCulling) {
// Do software culling.
if (drawEngineCommon_->TestBoundingBoxFast(verts, count, decoder, vertexType)) {
if (drawEngineCommon_->TestBoundingBoxFast(gstate_c.worldviewproj, verts, count, decoder, vertexType)) {
passCulling = true;
} else {
gpuStats.numCulledDraws++;
@@ -1137,7 +1137,7 @@ void GPUCommonHW::Execute_Prim(u32 op, u32 diff) {
if (!passCulling) {
// Do software culling.
_dbg_assert_((vertexType & GE_VTYPE_IDX_MASK) == GE_VTYPE_IDX_NONE);
if (drawEngineCommon_->TestBoundingBoxFast(verts, count, decoder, vertexType)) {
if (drawEngineCommon_->TestBoundingBoxFast(gstate_c.worldviewproj, verts, count, decoder, vertexType)) {
passCulling = true;
} else {
gpuStats.numCulledDraws++;
+5 -7
View File
@@ -184,9 +184,8 @@
<SubSystem>Console</SubSystem>
<GenerateDebugInformation>true</GenerateDebugInformation>
<AdditionalDependencies>d2d1.lib;dwrite.lib;d3d11.lib;winhttp.lib;mfuuid.lib;shlwapi.lib;Winmm.lib;Ws2_32.lib;avcodec.lib;avformat.lib;avutil.lib;swresample.lib;swscale.lib;comctl32.lib;dxguid.lib;opengl32.lib;glu32.lib;%(AdditionalDependencies)</AdditionalDependencies>
<BaseAddress>0x00400000</BaseAddress>
<RandomizedBaseAddress>false</RandomizedBaseAddress>
<FixedBaseAddress>true</FixedBaseAddress>
<BaseAddress>
</BaseAddress>
<AdditionalOptions>/ignore:4049 /ignore:4217 %(AdditionalOptions)</AdditionalOptions>
<AdditionalLibraryDirectories>../ffmpeg/Windows/x86_64/lib</AdditionalLibraryDirectories>
</Link>
@@ -287,9 +286,8 @@
<EnableCOMDATFolding>true</EnableCOMDATFolding>
<OptimizeReferences>true</OptimizeReferences>
<AdditionalDependencies>d2d1.lib;dwrite.lib;d3d11.lib;winhttp.lib;mfuuid.lib;shlwapi.lib;Winmm.lib;Ws2_32.lib;avcodec.lib;avformat.lib;avutil.lib;swresample.lib;swscale.lib;comctl32.lib;dxguid.lib;opengl32.lib;glu32.lib;%(AdditionalDependencies)</AdditionalDependencies>
<BaseAddress>0x00400000</BaseAddress>
<RandomizedBaseAddress>false</RandomizedBaseAddress>
<FixedBaseAddress>true</FixedBaseAddress>
<BaseAddress>
</BaseAddress>
<AdditionalOptions>/ignore:4049 /ignore:4217 %(AdditionalOptions)</AdditionalOptions>
<AdditionalLibraryDirectories>../ffmpeg/Windows/x86_64/lib;%(AdditionalLibraryDirectories)</AdditionalLibraryDirectories>
</Link>
@@ -409,4 +407,4 @@
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.targets" />
<ImportGroup Label="ExtensionTargets">
</ImportGroup>
</Project>
</Project>