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 @@ +