From 83639a73e86332df782cf68eeb29b99efb6e1b33 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Henrik=20Rydg=C3=A5rd?= Date: Thu, 15 Oct 2026 10:01:00 +0200 Subject: [PATCH] GPU: Cull-test draws four vertices at a time TestBoundingBoxFast transformed and tested one vertex per iteration, with a scalar divide and a min/max chain each vertex had to wait on. It now transposes four vertices into lanes and does it all as vectors, with the same operations in the same order, so the results don't change. It was 16.6% of the emulator thread in Wipeout Pure (with depth raster off) and 5% in GTA: Vice City Stories. Per vertex, it's 1.8x faster on an M1, 2x on x86-64, 1.7x on a Galaxy S8+ and 10% faster in 32-bit mode there. The test compares it with the old version on random draws in every position and index format. Co-Authored-By: Claude Opus 5.5 (1M context) --- CMakeLists.txt | 1 + GPU/Common/DrawEngineCommon.cpp | 185 +++++++++------- GPU/Common/DrawEngineCommon.h | 3 +- android/jni/Android.mk | 1 + unittest/TestBoundingBox.cpp | 332 +++++++++++++++++++++++++++++ unittest/UnitTest.cpp | 2 + unittest/UnitTests.vcxproj | 1 + unittest/UnitTests.vcxproj.filters | 1 + 8 files changed, 444 insertions(+), 82 deletions(-) create mode 100644 unittest/TestBoundingBox.cpp diff --git a/CMakeLists.txt b/CMakeLists.txt index 5e9047006f..f3eabfc8db 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1386,6 +1386,7 @@ if(UNITTEST) unittest/TestCrossSIMD.cpp unittest/TestIRPassSimplify.cpp unittest/TestX64Emitter.cpp + unittest/TestBoundingBox.cpp unittest/TestVertexJit.cpp unittest/TestVertexJit.h unittest/TestVFS.cpp diff --git a/GPU/Common/DrawEngineCommon.cpp b/GPU/Common/DrawEngineCommon.cpp index ae8a7bf215..093c367363 100644 --- a/GPU/Common/DrawEngineCommon.cpp +++ b/GPU/Common/DrawEngineCommon.cpp @@ -357,94 +357,119 @@ bool DrawEngineCommon::TestBoundingBox(const void *vdata, const void *inds, int // off to hardware, with whatever capabilities are available. // // NOTE: This doesn't handle through-mode or indexing (morph or skinning can be handled if they're implemented in software during decode). +template +static inline const s8 *BoundingBoxVertex(const s8 *srcdata, const void *idata, int stride, int i) { + switch (idxFmt) { + case GE_VTYPE_IDX_8BIT: return srcdata + ((const u8 *)idata)[i] * stride; + case GE_VTYPE_IDX_16BIT: return srcdata + ((const u16 *)idata)[i] * stride; + // The PSP ignores the upper 16 bits. + case GE_VTYPE_IDX_32BIT: return srcdata + (u16)((const u32 *)idata)[i] * stride; + default: return srcdata + i * stride; + } +} + +template +static inline Vec4F32 BoundingBoxPos(const s8 *data) { + switch (posFmt) { + case GE_VTYPE_POS_8BIT: return Vec4F32::LoadS8Norm(data); + case GE_VTYPE_POS_16BIT: return Vec4F32::LoadS16Norm((const s16 *)data); + default: return Vec4F32::Load((const float *)data); + } +} + +// a / b, rounded correctly (ARM32 NEON can only approximate it). +static inline Vec4F32 DivideExact(Vec4F32 a, Vec4F32 b) { +#if PPSSPP_ARCH(ARM_NEON) && !PPSSPP_ARCH(ARM64_NEON) + // The VFP registers overlap the NEON ones, so these divide in place. + float32x4_t r = a.v; + r = vsetq_lane_f32(vgetq_lane_f32(a.v, 0) / vgetq_lane_f32(b.v, 0), r, 0); + r = vsetq_lane_f32(vgetq_lane_f32(a.v, 1) / vgetq_lane_f32(b.v, 1), r, 1); + r = vsetq_lane_f32(vgetq_lane_f32(a.v, 2) / vgetq_lane_f32(b.v, 2), r, 2); + r = vsetq_lane_f32(vgetq_lane_f32(a.v, 3) / vgetq_lane_f32(b.v, 3), r, 3); + return Vec4F32{ r }; +#else + a /= b; + return a; +#endif +} + +// 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. +// +// NOTE: This doesn't handle through-mode (morph or skinning can be handled if they're implemented in software during decode). template static bool TestBoundingBoxFast(const float *cullMatrix, const void *vdata, const void *idata, int vertexCount, const VertexDecoder *dec, ClipInfoFlags *clipInfoFlags) { - Mat4F32 cullMat(cullMatrix); - alignas(16) static const float planesXYData[4] = { 1, -1, 1, -1 }; - Vec4F32 planesXY = Vec4F32::LoadAligned(planesXYData); - Vec4S32 insideMaskXY = Vec4S32::Zero(); - Vec4S32 insideMaskZ = Vec4S32::Zero(); // Note: This does some duplicate computation. We could avoid it on ARM32 using Vec2S32 but not really worth it. - Vec4S32 anyOutsideMaskZ = Vec4S32::Zero(); + // Four vertices at a time, one per lane, with the matrix splatted. Past the end, the last vertex fills the lanes, + // which changes nothing. + Vec4F32 m[16]; + for (int i = 0; i < 16; i++) + m[i] = Vec4F32::Splat(cullMatrix[i]); + + // For each side of the clipping cube, whether any vertex is inside it, and whether any is outside the Z sides. + Vec4S32 insideLeft = Vec4S32::Zero(); + Vec4S32 insideRight = Vec4S32::Zero(); + Vec4S32 insideTop = Vec4S32::Zero(); + Vec4S32 insideBottom = Vec4S32::Zero(); + Vec4S32 insideNear = Vec4S32::Zero(); + Vec4S32 insideFar = Vec4S32::Zero(); + Vec4S32 anyOutsideZ = Vec4S32::Zero(); - // Used to reduce the Z precision. This effectively implements the small offsets where Z can be very slightly outside -1..1. - // In reality we should probably affect X and Y too, but meh. - alignas(16) static const u32 vertexMaskData[4] = {0xFFFFFFFF, 0xFFFFFFFF, 0xFFFFFF00, 0xFFFFFFFF}; const int stride = dec->VertexSize(); const s8 *srcdata = (const s8 *)vdata + dec->posoff; - const s8 *data = srcdata; + const int last = vertexCount - 1; - const float vpZScale = gstate.getViewportZScale(); + const Vec4F32 vpZScale = Vec4F32::Splat(gstate.getViewportZScale()); + const Vec4F32 zero = Vec4F32::Zero(); + Vec4F32 minZ = Vec4F32::Splat(FLT_MAX); + Vec4F32 maxZ = Vec4F32::Splat(-FLT_MAX); + + for (int i = 0; i < vertexCount; i += 4) { + Vec4F32 objX = BoundingBoxPos(BoundingBoxVertex(srcdata, idata, stride, i)); + Vec4F32 objY = BoundingBoxPos(BoundingBoxVertex(srcdata, idata, stride, std::min(i + 1, last))); + Vec4F32 objZ = BoundingBoxPos(BoundingBoxVertex(srcdata, idata, stride, std::min(i + 2, last))); + Vec4F32 unused = BoundingBoxPos(BoundingBoxVertex(srcdata, idata, stride, std::min(i + 3, last))); + Vec4F32::Transpose(objX, objY, objZ, unused); + + // The same operations in the same order as AsVec3ByMatrix44(), so the same rounding. + const Vec4F32 x = (m[0] * objX + m[4] * objY) + (m[8] * objZ + m[12]); + const Vec4F32 y = (m[1] * objX + m[5] * objY) + (m[9] * objZ + m[13]); + const Vec4F32 z = (m[2] * objX + m[6] * objY) + (m[10] * objZ + m[14]); + const Vec4F32 w = (m[3] * objX + m[7] * objY) + (m[11] * objZ + m[15]); + + insideLeft |= (x + w).CompareGe(zero); + insideRight |= (w - x).CompareGe(zero); + insideTop |= (y + w).CompareGe(zero); + insideBottom |= (w - y).CompareGe(zero); + // We need to add the same culling epsilons as when setting up the cull distances in the vertex shader, + // so we don't over-cull here. We could also cull looser, but I can't figure out how to do so accurately. + const Vec4F32 deltaZ = w.RecipApprox() * 0.0000304f; + const Vec4F32 nearDist = (z + w) + deltaZ; + const Vec4F32 farDist = (w - z) + deltaZ; + insideNear |= nearDist.CompareGe(zero); + insideFar |= farDist.CompareGe(zero); + anyOutsideZ |= nearDist.CompareLt(zero) | farDist.CompareLt(zero); + + // NaN compares false, so it's never picked. + const Vec4F32 projZ = DivideExact(vpZScale * z, w); + minZ = Select(projZ.CompareLt(minZ), projZ, minZ); + maxZ = Select(projZ.CompareGt(maxZ), projZ, maxZ); + } + + if (!AnyCompareBitsSet(insideLeft) || !AnyCompareBitsSet(insideRight) || !AnyCompareBitsSet(insideTop) || + !AnyCompareBitsSet(insideBottom) || !AnyCompareBitsSet(insideNear) || !AnyCompareBitsSet(insideFar)) { + // All vertices were outside one side of the clipping cube. We can skip the draw entirely. + return false; + } float minProjZ = FLT_MAX; float maxProjZ = -FLT_MAX; - - for (int i = 0; i < vertexCount; i++) { - switch (idxFmt) { - case GE_VTYPE_IDX_8BIT: - { - u8 idx = ((u8 *)idata)[i]; - data = (const s8 *)srcdata + idx * stride; - break; - } - case GE_VTYPE_IDX_16BIT: - { - u16 idx = ((u16 *)idata)[i]; - data = (const s8 *)srcdata + idx * stride; - break; - } - case GE_VTYPE_IDX_32BIT: - { - // The PSP ignores the upper 16 bits. - u16 idx = (u16)((u32 *)idata)[i]; - data = (const s8 *)srcdata + idx * stride; - break; - } - default: - // We just increment data at the end of the loop. - break; - } - - Vec4F32 objPos; - switch (posFmt) { - case GE_VTYPE_POS_8BIT: - objPos = Vec4F32::LoadS8Norm(data); - break; - case GE_VTYPE_POS_16BIT: - objPos = Vec4F32::LoadS16Norm((const s16 *)data); - break; - default: - objPos = Vec4F32::Load((const float *)data); - break; - } - Vec4F32 clipPos = objPos.AsVec3ByMatrix44(cullMat); - Vec4F32 posW = clipPos.ShuffleWWWW(); - Vec4F32 posXY = clipPos.ShuffleXXYY(); - Vec4F32 planeDistXY = posXY * planesXY + posW; - insideMaskXY |= planeDistXY.CompareGe(Vec4F32::Zero()); - Vec4F32 posZ = clipPos.ShuffleZZZZ(); // This means that we compute the Z sides twice. Oh well. - // We need to add the same culling epsilons as when setting up the cull distances in the vertex shader, - // so we don't over-cull here. We could also cull looser, but I can't figure out how to do so accurately. - // It's a bit unnecessary to take four reciprocals here, let's see if we can avoid that later. - Vec4F32 deltaZ = posW.RecipApprox() * 0.0000304f; - Vec4F32 planeDistZ = posZ * planesXY + posW + deltaZ; - anyOutsideMaskZ |= planeDistZ.CompareLt(Vec4F32::Zero()); - insideMaskZ |= planeDistZ.CompareGe(Vec4F32::Zero()); - const float projZ = vpZScale * clipPos[2] / clipPos[3]; - if (projZ < minProjZ) { - minProjZ = projZ; - } - if (projZ > maxProjZ) { // else ruins the minss/maxss optimization. - maxProjZ = projZ; - } - - if (idxFmt == GE_VTYPE_IDX_NONE) { - data += stride; - } - } - - if (!AllCompareBitsSet(insideMaskXY) || !AllCompareBitsSet(insideMaskZ)) { - // All vertices were outside one side of the clipping cube. We can skip the draw entirely. - return false; + for (int i = 0; i < 4; i++) { + if (minZ[i] < minProjZ) + minProjZ = minZ[i]; + if (maxZ[i] > maxProjZ) + maxProjZ = maxZ[i]; } const float vpZOffset = gstate.getViewportZCenter(); @@ -472,7 +497,7 @@ static bool TestBoundingBoxFast(const float *cullMatrix, const void *vdata, cons } } - if (AnyCompareBitsSet(anyOutsideMaskZ) && (!gstate_c.viewportNearPlaneMatchesOutput || PSP_CoreParameter().compat.flags().CorrectCullAfterClip || !gstate_c.Use(GPU_USE_CULL_DISTANCE) || !gstate_c.Use(GPU_USE_CLIP_DISTANCE))) { + if (AnyCompareBitsSet(anyOutsideZ) && (!gstate_c.viewportNearPlaneMatchesOutput || PSP_CoreParameter().compat.flags().CorrectCullAfterClip || !gstate_c.Use(GPU_USE_CULL_DISTANCE) || !gstate_c.Use(GPU_USE_CLIP_DISTANCE))) { // Some vertices were outside the Z clipping planes. Clip againt Z=-W in software (and do culling, too). // Without cull distances the shader can't cull, and triangles entirely beyond the far plane would be // depth clamped and drawn (the stands over the pitch in FIFA Soccer). Without clip distances it can't diff --git a/GPU/Common/DrawEngineCommon.h b/GPU/Common/DrawEngineCommon.h index e84bf5e488..e8de52fa66 100644 --- a/GPU/Common/DrawEngineCommon.h +++ b/GPU/Common/DrawEngineCommon.h @@ -102,8 +102,7 @@ public: bool TestBoundingBox(const void *control_points, const void *inds, int vertexCount, const VertexDecoder *dec, u32 vertType); // This is a less accurate version of TestBoundingBox, but faster. Can have more false positives. - // Doesn't support indexing. - bool TestBoundingBoxFast(const float *cullMatrix, const void *vdata, const void *idata, int vertexCount, const VertexDecoder *dec, u32 vertType, ClipInfoFlags *clipInfoFlags); + static bool TestBoundingBoxFast(const float *cullMatrix, const void *vdata, const void *idata, int vertexCount, const VertexDecoder *dec, u32 vertType, ClipInfoFlags *clipInfoFlags); bool TestBoundingBoxThrough(GEPrimitiveType prim, const void *vdata, const void *idata, int vertexCount, const VertexDecoder *dec, u32 vertType, int *bytesRead, ClipInfoFlags *flags); // For a draw too big or complex for TestBoundingBoxFast: the fallbacks it would need on a GPU that lacks // depth clamp or clip distances, assuming the worst, or nothing (not Valid) if none are needed. diff --git a/android/jni/Android.mk b/android/jni/Android.mk index 9a20982d63..1821bcde16 100644 --- a/android/jni/Android.mk +++ b/android/jni/Android.mk @@ -1100,6 +1100,7 @@ ifeq ($(UNITTEST),1) $(SRC)/unittest/TestIRPassSimplify.cpp \ $(SRC)/unittest/TestShaderGenerators.cpp \ $(SRC)/unittest/TestThreadManager.cpp \ + $(SRC)/unittest/TestBoundingBox.cpp \ $(SRC)/unittest/TestVertexJit.cpp \ $(SRC)/unittest/TestTextureReplacer.cpp \ $(SRC)/unittest/TestVFS.cpp \ diff --git a/unittest/TestBoundingBox.cpp b/unittest/TestBoundingBox.cpp new file mode 100644 index 0000000000..bf23c3d7d9 --- /dev/null +++ b/unittest/TestBoundingBox.cpp @@ -0,0 +1,332 @@ +// Copyright (c) 2012- PPSSPP Project. + +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, version 2.0 or later versions. + +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License 2.0 for more details. + +// A copy of the GPL 2.0 should have been included with the program. +// If not, see http://www.gnu.org/licenses/ + +// Official git repository and contact information can be found at +// https://github.com/hrydgard/ppsspp and http://www.ppsspp.org/. + +#include +#include +#include +#include +#include + +#include "Common/CommonTypes.h" +#include "Common/Math/CrossSIMD.h" +#include "Common/TimeUtil.h" +#include "Core/System.h" +#include "GPU/Common/DrawEngineCommon.h" +#include "GPU/Common/ShaderId.h" +#include "GPU/Common/VertexDecoderCommon.h" +#include "GPU/ge_constants.h" +#include "GPU/GPUState.h" +#include "unittest/UnitTest.h" + +// The one-vertex-at-a-time TestBoundingBoxFast, as the reference. +template +static bool RefTestBoundingBoxFast(const float *cullMatrix, const void *vdata, const void *idata, int vertexCount, const VertexDecoder *dec, ClipInfoFlags *clipInfoFlags) { + Mat4F32 cullMat(cullMatrix); + alignas(16) static const float planesXYData[4] = { 1, -1, 1, -1 }; + Vec4F32 planesXY = Vec4F32::LoadAligned(planesXYData); + Vec4S32 insideMaskXY = Vec4S32::Zero(); + Vec4S32 insideMaskZ = Vec4S32::Zero(); // Note: This does some duplicate computation. We could avoid it on ARM32 using Vec2S32 but not really worth it. + Vec4S32 anyOutsideMaskZ = Vec4S32::Zero(); + + // Used to reduce the Z precision. This effectively implements the small offsets where Z can be very slightly outside -1..1. + // In reality we should probably affect X and Y too, but meh. + alignas(16) static const u32 vertexMaskData[4] = {0xFFFFFFFF, 0xFFFFFFFF, 0xFFFFFF00, 0xFFFFFFFF}; + const int stride = dec->VertexSize(); + const s8 *srcdata = (const s8 *)vdata + dec->posoff; + const s8 *data = srcdata; + + const float vpZScale = gstate.getViewportZScale(); + + float minProjZ = FLT_MAX; + float maxProjZ = -FLT_MAX; + + for (int i = 0; i < vertexCount; i++) { + switch (idxFmt) { + case GE_VTYPE_IDX_8BIT: + { + u8 idx = ((u8 *)idata)[i]; + data = (const s8 *)srcdata + idx * stride; + break; + } + case GE_VTYPE_IDX_16BIT: + { + u16 idx = ((u16 *)idata)[i]; + data = (const s8 *)srcdata + idx * stride; + break; + } + case GE_VTYPE_IDX_32BIT: + { + // The PSP ignores the upper 16 bits. + u16 idx = (u16)((u32 *)idata)[i]; + data = (const s8 *)srcdata + idx * stride; + break; + } + default: + // We just increment data at the end of the loop. + break; + } + + Vec4F32 objPos; + switch (posFmt) { + case GE_VTYPE_POS_8BIT: + objPos = Vec4F32::LoadS8Norm(data); + break; + case GE_VTYPE_POS_16BIT: + objPos = Vec4F32::LoadS16Norm((const s16 *)data); + break; + default: + objPos = Vec4F32::Load((const float *)data); + break; + } + Vec4F32 clipPos = objPos.AsVec3ByMatrix44(cullMat); + Vec4F32 posW = clipPos.ShuffleWWWW(); + Vec4F32 posXY = clipPos.ShuffleXXYY(); + Vec4F32 planeDistXY = posXY * planesXY + posW; + insideMaskXY |= planeDistXY.CompareGe(Vec4F32::Zero()); + Vec4F32 posZ = clipPos.ShuffleZZZZ(); // This means that we compute the Z sides twice. Oh well. + // We need to add the same culling epsilons as when setting up the cull distances in the vertex shader, + // so we don't over-cull here. We could also cull looser, but I can't figure out how to do so accurately. + // It's a bit unnecessary to take four reciprocals here, let's see if we can avoid that later. + Vec4F32 deltaZ = posW.RecipApprox() * 0.0000304f; + Vec4F32 planeDistZ = posZ * planesXY + posW + deltaZ; + anyOutsideMaskZ |= planeDistZ.CompareLt(Vec4F32::Zero()); + insideMaskZ |= planeDistZ.CompareGe(Vec4F32::Zero()); + const float projZ = vpZScale * clipPos[2] / clipPos[3]; + if (projZ < minProjZ) { + minProjZ = projZ; + } + if (projZ > maxProjZ) { // else ruins the minss/maxss optimization. + maxProjZ = projZ; + } + + if (idxFmt == GE_VTYPE_IDX_NONE) { + data += stride; + } + } + + if (!AllCompareBitsSet(insideMaskXY) || !AllCompareBitsSet(insideMaskZ)) { + // All vertices were outside one side of the clipping cube. We can skip the draw entirely. + return false; + } + + const float vpZOffset = gstate.getViewportZCenter(); + minProjZ += vpZOffset; + maxProjZ += vpZOffset; + + ClipInfoFlags flags = ClipInfoFlags::Valid; + + // If the W=-Z plane was intersected, here we can go through the vertices again, and check for X/Y bounds for range culling. + // However! We need to find a valid way to do so by "backprojecting" the range culling into clip space, which may be a little tricky. + // + // If nothing is outside the box, the "inversion" cases (vertices hit the boundary after clipping like Flatout, Sengoku Cannon) + // cannot happen, and soft clipping is only needed if the viewport is smaller than the valid Z range. + // + // Alternatively, we just do a compat flag for the affected games until we can solve this. + + const int depthRangeMin = gstate.getDepthRangeMin(); + const int depthRangeMax = gstate.getDepthRangeMax(); + if ((depthRangeMin != 0 && minProjZ < depthRangeMin) || (depthRangeMax != 0xFFFF && maxProjZ > depthRangeMax)) { + if (gstate_c.Use(GPU_USE_CLIP_DISTANCE)) { + flags |= ClipInfoFlags::MinMaxZClip; + } else { + // Implement min/max in the fragment shader. + flags |= ClipInfoFlags::MinMaxZDiscard; + } + } + + if (AnyCompareBitsSet(anyOutsideMaskZ) && (!gstate_c.viewportNearPlaneMatchesOutput || PSP_CoreParameter().compat.flags().CorrectCullAfterClip || !gstate_c.Use(GPU_USE_CULL_DISTANCE) || !gstate_c.Use(GPU_USE_CLIP_DISTANCE))) { + // Some vertices were outside the Z clipping planes. Clip againt Z=-W in software (and do culling, too). + // Without cull distances the shader can't cull, and triangles entirely beyond the far plane would be + // depth clamped and drawn (the stands over the pitch in FIFA Soccer). Without clip distances it can't + // clip against the near plane (Test Drive's hills over the sky, on old Mali). + // TODO: With a compat flag for Flatout/Sengoku, we'll be able to avoid this in many cases. + flags |= ClipInfoFlags::SoftClipCull; + } + + if (minProjZ == maxProjZ) { + // Probably a 2D draw. Send it through software transform! + flags |= ClipInfoFlags::FlatZ | ClipInfoFlags::SoftClipCull; + } + + if (needFragmentDepthClamp()) { + if (gstate_c.Use(GPU_USE_DEPTH_CLAMP)) { + if (minProjZ < 0 || maxProjZ > 65535) { + flags |= ClipInfoFlags::DepthClamp; + } + } else if (minProjZ <= 0 || maxProjZ >= 65535) { + // Including draws right on the limit (Puyo Puyo Fever 2's flat Z = 0 ground): hardware clamp keeps + // what rounding puts just outside, the clip without it doesn't. + flags |= ClipInfoFlags::DepthClampFragment; + } + } + + *clipInfoFlags = flags; + return true; +} + +typedef bool (*BoundingBoxFunc)(const float *, const void *, const void *, int, const VertexDecoder *, ClipInfoFlags *); + +static BoundingBoxFunc RefBoundingBoxFunc(u32 vtype) { +#define REF_IDX(pos) \ + switch (vtype & GE_VTYPE_IDX_MASK) { \ + case GE_VTYPE_IDX_NONE: return &RefTestBoundingBoxFast; \ + case GE_VTYPE_IDX_8BIT: return &RefTestBoundingBoxFast; \ + case GE_VTYPE_IDX_16BIT: return &RefTestBoundingBoxFast; \ + default: return &RefTestBoundingBoxFast; \ + } + switch (vtype & GE_VTYPE_POS_MASK) { + case GE_VTYPE_POS_8BIT: REF_IDX(GE_VTYPE_POS_8BIT); + case GE_VTYPE_POS_16BIT: REF_IDX(GE_VTYPE_POS_16BIT); + default: REF_IDX(GE_VTYPE_POS_FLOAT); + } +#undef REF_IDX +} + +struct BoundingBoxRng { + u32 state = 0x12345678; + u32 Next() { + state ^= state << 13; + state ^= state >> 17; + state ^= state << 5; + return state; + } + int Int(int n) { return (int)(Next() % (u32)n); } + float Float(float lo, float hi) { return lo + (hi - lo) * (float)(Next() & 0xFFFFFF) / (float)0x1000000; } +}; + +static u32 ToFloat24(float f) { + u32 bits; + memcpy(&bits, &f, sizeof(bits)); + return bits >> 8; +} + +static const u32 posFormats[] = { GE_VTYPE_POS_8BIT, GE_VTYPE_POS_16BIT, GE_VTYPE_POS_FLOAT }; +static const u32 idxFormats[] = { GE_VTYPE_IDX_NONE, GE_VTYPE_IDX_8BIT, GE_VTYPE_IDX_16BIT, GE_VTYPE_IDX_32BIT }; + +// Fills a draw with random vertices (positions mostly in a range that straddles the clipping cube, the rest of +// each vertex random bytes) and indices. +static void MakeBoundingBoxDraw(BoundingBoxRng &rng, const VertexDecoder &dec, u32 vtype, int numVerts, int count, std::vector &verts, std::vector &inds) { + const int stride = dec.VertexSize(); + verts.resize(stride * numVerts + 16); + for (u8 &b : verts) + b = (u8)rng.Next(); + for (int v = 0; v < numVerts; v++) { + u8 *pos = verts.data() + v * stride + dec.posoff; + for (int c = 0; c < 3; c++) { + switch (vtype & GE_VTYPE_POS_MASK) { + case GE_VTYPE_POS_8BIT: pos[c] = (u8)rng.Next(); break; + case GE_VTYPE_POS_16BIT: { s16 s = (s16)rng.Next(); memcpy(pos + c * 2, &s, 2); break; } + default: + { + float f = rng.Float(-2.0f, 2.0f); + const int special = rng.Int(64); + if (special == 0) + f = 0.0f; + else if (special == 1) + f = -0.0f; + else if (special == 2) + f = rng.Float(-1e30f, 1e30f); + else if (special == 3) + f = NAN; + memcpy(pos + c * 4, &f, 4); + break; + } + } + } + } + const int idxSize = (vtype & GE_VTYPE_IDX_MASK) == GE_VTYPE_IDX_8BIT ? 1 : (vtype & GE_VTYPE_IDX_MASK) == GE_VTYPE_IDX_16BIT ? 2 : 4; + inds.resize(count * idxSize); + for (int i = 0; i < count; i++) { + // The PSP ignores the upper 16 bits of 32-bit indices. + u32 idx = (u32)rng.Int(numVerts) | (idxSize == 4 ? (rng.Next() & 0xFFFF0000) : 0); + memcpy(inds.data() + i * idxSize, &idx, idxSize); + } +} + +// The vectorized TestBoundingBoxFast against the one-vertex-at-a-time version it replaced: the same result and +// flags for random draws in every position and index format. +bool TestBoundingBoxFast() { + const GEState savedState = gstate; + BoundingBoxRng rng; + VertexDecoderOptions opts{}; + std::vector verts, inds; + int culled = 0, passed = 0, mismatches = 0; + + for (int trial = 0; trial < 30000; trial++) { + const u32 vtype = posFormats[rng.Int(3)] | idxFormats[rng.Int(4)] | (rng.Int(2) ? GE_VTYPE_TC_16BIT | GE_VTYPE_COL_8888 : 0); + VertexDecoder dec{}; + dec.SetVertexType(GetVertTypeID(vtype, 0), opts, nullptr); + const bool indexed = (vtype & GE_VTYPE_IDX_MASK) != 0; + const int count = 1 + rng.Int(trial % 8 == 0 ? 300 : 13); + const int numVerts = indexed ? 1 + rng.Int(64) : count; + MakeBoundingBoxDraw(rng, dec, vtype, numVerts, count, verts, inds); + + // A projection-like matrix: some of the draws are behind the camera, or partly outside. + float matrix[16]; + for (int i = 0; i < 16; i++) + matrix[i] = rng.Float(-1.5f, 1.5f); + matrix[15] = rng.Float(-0.5f, 3.0f); + + gstate.viewportzscale = (GE_CMD_VIEWPORTZSCALE << 24) | ToFloat24(rng.Float(-40000.0f, 40000.0f)); + gstate.viewportzcenter = (GE_CMD_VIEWPORTZCENTER << 24) | ToFloat24(rng.Float(0.0f, 65535.0f)); + gstate.minz = (GE_CMD_MINZ << 24) | (rng.Int(2) ? 0 : rng.Int(30000)); + gstate.maxz = (GE_CMD_MAXZ << 24) | (rng.Int(2) ? 0xFFFF : 30000 + rng.Int(35535)); + gstate.depthClipEnable = (GE_CMD_DEPTHCLIPENABLE << 24) | rng.Int(2); + + ClipInfoFlags refFlags{}, flags{}; + const bool refResult = RefBoundingBoxFunc(vtype)(matrix, verts.data(), inds.data(), count, &dec, &refFlags); + const bool result = DrawEngineCommon::TestBoundingBoxFast(matrix, verts.data(), inds.data(), count, &dec, vtype, &flags); + if (result != refResult || (result && flags != refFlags)) { + if (mismatches++ < 10) + printf("Mismatch: vtype %08x count %d: %d (flags %x), expected %d (flags %x)\n", vtype, count, result, (u32)flags, refResult, (u32)refFlags); + } + (refResult ? passed : culled)++; + } + printf("%d draws culled, %d passed\n", culled, passed); + EXPECT_EQ_INT(mismatches, 0); + // Both outcomes need plenty of cases for the comparison to mean anything. + EXPECT_TRUE(culled > 3000 && passed > 3000); + + // Speed, on a typical large draw: 16-bit positions, no indices. + { + const u32 vtype = GE_VTYPE_POS_16BIT | GE_VTYPE_TC_16BIT | GE_VTYPE_COL_8888; + VertexDecoder dec{}; + dec.SetVertexType(GetVertTypeID(vtype, 0), opts, nullptr); + const int count = 256; + MakeBoundingBoxDraw(rng, dec, vtype, count, count, verts, inds); + const float matrix[16] = { 1.2f, 0, 0, 0, 0, 1.5f, 0, 0, 0, 0, -1.0f, -1.0f, 0, 0, 0.9f, 1.1f }; + gstate.minz = (GE_CMD_MINZ << 24); + gstate.maxz = (GE_CMD_MAXZ << 24) | 0xFFFF; + const int reps = 20000; + for (int pass = 0; pass < 2; pass++) { + ClipInfoFlags flags{}; + int sum = 0; + const double start = time_now_d(); + for (int r = 0; r < reps; r++) { + if (pass == 0) + sum += RefTestBoundingBoxFast(matrix, verts.data(), nullptr, count, &dec, &flags); + else + sum += DrawEngineCommon::TestBoundingBoxFast(matrix, verts.data(), nullptr, count, &dec, vtype, &flags); + } + const double ns = (time_now_d() - start) * 1e9 / ((double)reps * count); + printf("%s: %.2f ns per vertex (%d)\n", pass == 0 ? "one at a time" : "four at a time", ns, sum); + } + } + + gstate = savedState; + return true; +} diff --git a/unittest/UnitTest.cpp b/unittest/UnitTest.cpp index 9aada185a7..fd559e4bdb 100644 --- a/unittest/UnitTest.cpp +++ b/unittest/UnitTest.cpp @@ -3103,6 +3103,7 @@ bool TestLzrc(); bool TestMpegCsc(); bool TestSplineTessellation(); bool TestGEMath(); +bool TestBoundingBoxFast(); bool TestDemangle(); // The 8.3 short names games read out of d_private. These aren't verified against hardware yet (no @@ -3430,6 +3431,7 @@ TestItem availableTests[] = { TEST_ITEM(LoongArch64Emitter), #endif TEST_ITEM(VertexJit), + TEST_ITEM(BoundingBoxFast), TEST_ITEM(VFPUSinCos), TEST_ITEM(VFPUDot), TEST_ITEM(MathUtil), diff --git a/unittest/UnitTests.vcxproj b/unittest/UnitTests.vcxproj index ff66ee2cc2..2c3410a7db 100644 --- a/unittest/UnitTests.vcxproj +++ b/unittest/UnitTests.vcxproj @@ -300,6 +300,7 @@ + diff --git a/unittest/UnitTests.vcxproj.filters b/unittest/UnitTests.vcxproj.filters index 16717016ec..7f1e9d1d1a 100644 --- a/unittest/UnitTests.vcxproj.filters +++ b/unittest/UnitTests.vcxproj.filters @@ -8,6 +8,7 @@ +