From 87f50b9609b97b8f1ea3a8a7ebe37413908b7de9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Henrik=20Rydg=C3=A5rd?= Date: Thu, 1 Oct 2026 12:16:52 -0600 Subject: [PATCH] Add GEMath unit tests The GE arithmetic in GEMath.h against values measured on a PSP: the reciprocal for sampled mantissas (gpu/probe exp15), the triangle setup reciprocal (exp38-41), antialiased line alphas (exp112), and the cases the other probes pinned down (float24 truncation, the adder and row sums, rsqrt, the mip log2, texture coordinate precision, the lighting pow and color scaling). Co-Authored-By: Claude Opus 5.5 (1M context) --- CMakeLists.txt | 1 + android/jni/Android.mk | 1 + unittest/TestGEMath.cpp | 213 +++++++++++++++++++++++++++++ unittest/UnitTest.cpp | 2 + unittest/UnitTests.vcxproj | 1 + unittest/UnitTests.vcxproj.filters | 1 + 6 files changed, 219 insertions(+) create mode 100644 unittest/TestGEMath.cpp diff --git a/CMakeLists.txt b/CMakeLists.txt index 590e82f13a..291b570fe6 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1374,6 +1374,7 @@ if(UNITTEST) unittest/TestLzrc.cpp unittest/TestMpegCsc.cpp unittest/TestSplineTessellation.cpp + unittest/TestGEMath.cpp unittest/TestDemangle.cpp unittest/TestTextureReplacer.cpp unittest/TestRiscVEmitter.cpp diff --git a/android/jni/Android.mk b/android/jni/Android.mk index c6b6c6c911..7147d4b93f 100644 --- a/android/jni/Android.mk +++ b/android/jni/Android.mk @@ -1060,6 +1060,7 @@ ifeq ($(UNITTEST),1) $(SRC)/unittest/TestLzrc.cpp \ $(SRC)/unittest/TestMpegCsc.cpp \ $(SRC)/unittest/TestSplineTessellation.cpp \ + $(SRC)/unittest/TestGEMath.cpp \ $(SRC)/unittest/TestZipSlip.cpp \ $(SRC)/unittest/UnitTest.cpp diff --git a/unittest/TestGEMath.cpp b/unittest/TestGEMath.cpp new file mode 100644 index 0000000000..d76f98c88c --- /dev/null +++ b/unittest/TestGEMath.cpp @@ -0,0 +1,213 @@ +// 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/. + +// The GE arithmetic in GPU/Software/GEMath.h against values measured on a PSP. The probes are in +// pspautotests/tests/gpu/probe (expNN.py), with notes in the ppsspp-re repository: reciprocals from +// exp15 and exp38-41, the AA line pixels from exp112. The rest are cases those probes pinned down. + +#include +#include +#include + +#include "GPU/Software/GEMath.h" + +#include "unittest/UnitTest.h" + +static float FromBits(uint32_t u) { + float f; + memcpy(&f, &u, sizeof(f)); + return f; +} + +static uint32_t ToBits(float f) { + uint32_t u; + memcpy(&u, &f, sizeof(u)); + return u; +} + +static bool TestFloat24() { + // 15 stored mantissa bits, truncated toward zero. + EXPECT_EQ_FLOAT(TruncateToFloat24(1.0f + ldexpf(1.0f, -16)), 1.0f); + EXPECT_EQ_FLOAT(TruncateToFloat24(1.0f + ldexpf(1.0f, -15)), 1.0f + ldexpf(1.0f, -15)); + EXPECT_EQ_FLOAT(TruncateToFloat24(-1.0f - ldexpf(3.0f, -17)), -1.0f); + EXPECT_EQ_FLOAT(TruncateToFloat24(-1.0f - ldexpf(5.0f, -17)), -1.0f - ldexpf(1.0f, -15)); + // The product is truncated in double, not rounded to a float first. + EXPECT_EQ_FLOAT(ProductToFloat24(1.0 + ldexp(1.0, -15) + ldexp(1.0, -16) - ldexp(1.0, -40)), 1.0f + ldexpf(1.0f, -15)); + return true; +} + +static bool TestGERecip() { + // 1 / (1 + i / 32768) in units of 2^-16, measured on a PSP for every mantissa (exp15). + static const int measured[][2] = { + { 0, 65536 }, { 1, 65534 }, { 255, 65030 }, { 256, 65027 }, { 909, 63768 }, { 1149, 63316 }, { 2816, 60350 }, { 4727, 57274 }, + { 8168, 52459 }, { 8423, 52135 }, { 8953, 51472 }, { 10218, 49957 }, { 10809, 49280 }, { 13408, 46507 }, { 13869, 46047 }, { 14837, 45110 }, + { 15497, 44494 }, { 16762, 43357 }, { 17635, 42606 }, { 18007, 42294 }, { 21093, 39871 }, { 24689, 37376 }, { 24729, 37350 }, { 26789, 36057 }, + { 26951, 35960 }, { 27112, 35863 }, { 27453, 35660 }, { 28359, 35131 }, { 28463, 35072 }, { 31067, 33641 }, { 31123, 33612 }, { 32767, 32769 }, + }; + for (const auto &m : measured) { + const float w = FromBits(0x3F800000 | (m[0] << 8)); + const float expected = ldexpf((float)m[1], -16); + if (GERecip(w) != expected) { + printf("GERecip(1 + %d / 32768) = %.9f, PSP %.9f\n", m[0], GERecip(w), expected); + return false; + } + } + // Sign and exponent are carried over. + EXPECT_EQ_FLOAT(GERecip(-2.0f), -0.5f); + EXPECT_EQ_FLOAT(GERecip(0.25f), 4.0f); + return true; +} + +static bool TestGERsqrt() { + // The table for odd exponents stores 1 / sqrt(2 * 1.i) (exp69). + EXPECT_EQ_FLOAT(GERsqrt(1.0f), 1.0f); + EXPECT_EQ_FLOAT(GERsqrt(4.0f), 0.5f); + EXPECT_EQ_FLOAT(GERsqrt(0.25f), 2.0f); + EXPECT_EQ_FLOAT(GERsqrt(2.0f), ldexpf(46340.0f, -16)); + return true; +} + +static bool TestGEAdd() { + // No guard bits: the smaller term is truncated (toward zero) to the larger one's last bit. + EXPECT_EQ_FLOAT(GEAddFloat24(1.0f, ldexpf(3.0f, -17)), 1.0f); + EXPECT_EQ_FLOAT(GEAddFloat24(1.0f, ldexpf(5.0f, -17)), 1.0f + ldexpf(1.0f, -15)); + EXPECT_EQ_FLOAT(GEAddFloat24(1.0f, -ldexpf(5.0f, -17)), 1.0f - ldexpf(1.0f, -15)); + EXPECT_EQ_FLOAT(GEAddFloat24(0.0f, 1.0f + ldexpf(1.0f, -20)), 1.0f); + return true; +} + +static bool TestGEDot() { + // A row is summed in one go: every product truncated at the largest term's last bit, so two small + // terms that would carry together in pairwise adds are both dropped. + EXPECT_EQ_FLOAT(GEDot(Vec3f(1.0f, 1.0f, 1.0f), Vec3f(1.0f, ldexpf(1.0f, -16), ldexpf(1.0f, -16))), 1.0f); + EXPECT_EQ_FLOAT(GEDot(Vec3f(2.0f, 3.0f, 0.0f), Vec3f(0.5f, 0.25f, 9.0f)), 1.75f); + // Normalization goes through GERsqrt, and returns the length the same way. + Vec3f v(3.0f, 4.0f, 0.0f); + const float len = GENormalize(v); + EXPECT_EQ_FLOAT(len, ProductToFloat24(25.0 * GERsqrt(25.0f))); + EXPECT_EQ_FLOAT(v.x, ProductToFloat24(3.0 * GERsqrt(25.0f))); + EXPECT_EQ_FLOAT(v.y, ProductToFloat24(4.0 * GERsqrt(25.0f))); + EXPECT_TRUE(fabsf(len - 5.0f) < 0.001f); + return true; +} + +static bool TestGESetupRecip() { + // The triangle setup's reciprocal for 16-bit significand indices, measured on a PSP (exp38-41). + static const int measured[][2] = { + { 0, 65536 }, { 2, 65534 }, { 254, 65282 }, { 256, 65280 }, { 2594, 63040 }, { 4086, 61689 }, { 7588, 58735 }, { 10520, 56471 }, + { 18132, 51333 }, { 19190, 50692 }, { 21126, 49560 }, { 23770, 48092 }, { 24120, 47905 }, { 27582, 46123 }, { 27812, 46010 }, { 27998, 45918 }, + { 29846, 45029 }, { 30100, 44909 }, { 33802, 43235 }, { 34636, 42876 }, { 35440, 42534 }, { 40160, 40635 }, { 40842, 40374 }, { 47082, 38137 }, + { 47536, 37984 }, { 50630, 36973 }, { 50882, 36892 }, { 57148, 35008 }, { 60100, 34186 }, { 60570, 34058 }, { 63526, 33278 }, { 65534, 32768 }, + }; + for (const auto &m : measured) { + int e = 0; + const int64_t q = GESetupRecip(65536 + m[0], &e); + if (q != m[1] || e != 16) { + printf("GESetupRecip(index %d) = %lld (e %d), PSP %d\n", m[0], (long long)q, e, m[1]); + return false; + } + // Only the top 16 bits count. + const int64_t q2 = GESetupRecip((uint64_t)(65536 + m[0]) << 5 | 31, &e); + EXPECT_EQ_INT(q2, m[1]); + EXPECT_EQ_INT(e, 21); + } + return true; +} + +static bool TestGELog16() { + // The exponent and the top 4 mantissa bits, in 1/16 (exp58-60). + EXPECT_EQ_INT(GELog16(1.0f), 0); + EXPECT_EQ_INT(GELog16(2.0f), 16); + EXPECT_EQ_INT(GELog16(0.5f), -16); + EXPECT_EQ_INT(GELog16(1.5f), 8); + EXPECT_EQ_INT(GELog16(0.75f), -8); + EXPECT_EQ_INT(GELog16(1.0625f), 1); + EXPECT_EQ_INT(GELog16(1.0624f), 0); + return true; +} + +static bool TestTexCoordPrecision() { + // A UV plane keeps 15 significant bits of a lone value (exp83). + EXPECT_EQ_FLOAT(GETruncateTexCoord(300015.75f), 300000.0f); + EXPECT_EQ_FLOAT(GETruncateTexCoord(-300015.75f), -300000.0f); + EXPECT_EQ_FLOAT(GETruncateTexCoord(100.75f), 100.75f); + // u = s * R(q) keeps 24 significant bits, truncated (exp82). + EXPECT_EQ_FLOAT(GEUVProduct(1.0 + ldexp(1.0, -23) + ldexp(1.0, -30)), 1.0f + ldexpf(1.0f, -23)); + EXPECT_EQ_FLOAT(GEUVProduct(1.0 + ldexp(1.0, -24)), 1.0f); + return true; +} + +static bool TestGELightPow() { + // Mitchell's approximation: straight lines between powers of two (exp98, exp102). + EXPECT_EQ_FLOAT(GELightPow(0.5f, 2.0f), 0.25f); + EXPECT_EQ_FLOAT(GELightPow(0.75f, 2.0f), 0.5f); + EXPECT_EQ_FLOAT(GELightPow(0.3f, 0.0f), 1.0f); + EXPECT_EQ_FLOAT(GELightPow(-0.5f, 2.0f), -0.5f); + // The exponent times the bits is exact and floored; a rounded float multiply gives 0x3DA64400. + EXPECT_EQ_HEX(ToBits(GELightPow(FromBits(0x3DC22200), 1.0625f)), 0x3DA64420U); + return true; +} + +static bool TestGELightColor() { + // 8-bit color products and scaling, ((2a + 1) * (2b + 1)) >> 10 (exp61-63; exp62 all 65536 pairs). + // GELightColorProduct takes the colors already expanded as 2c + 1. + const Vec4 white = Vec4::AssignToAll(2 * 255 + 1); + EXPECT_EQ_INT(GELightColorProduct(white, white).x, 255); + EXPECT_EQ_INT(GELightColorProduct(Vec4::AssignToAll(2 * 128 + 1), Vec4::AssignToAll(2 * 200 + 1)).x, 100); + EXPECT_EQ_INT(GELightColorProduct(Vec4::AssignToAll(1), Vec4::AssignToAll(1)).x, 0); + // A factor becomes s = floor(256 f), expanded like a color; 1 leaves the color alone. + EXPECT_EQ_INT(GELightColorScale(Vec4::AssignToAll(200), 1.0f).x, 200); + EXPECT_EQ_INT(GELightColorScale(Vec4::AssignToAll(200), 0.5f).x, 100); + EXPECT_EQ_INT(GELightColorScale(Vec4::AssignToAll(255), 0.999f).x, 255); + EXPECT_EQ_INT(GELightColorScale(Vec4::AssignToAll(255), 0.99f).x, 253); + return true; +} + +static bool TestGELineCoverageAlpha() { + // Antialiased line pixels measured on a PSP (exp112): line ends in subpixels, the pixel, its alpha. + // The first four have the offset exactly on a step. + static const int measured[][7] = { + { 831, 146, 567, 227, 48, 10, 47 }, { 831, 146, 567, 227, 37, 13, 113 }, { 2470, 191, 2544, 154, 158, 9, 31 }, { 2470, 191, 2544, 154, 156, 10, 31 }, + { 328, 240, 198, 171, 13, 11, 73 }, { 7310, 112, 7502, 157, 461, 8, 22 }, { 5529, 766, 5559, 569, 347, 37, 37 }, { 4038, 929, 4232, 1136, 253, 59, 71 }, + { 2801, 1456, 2632, 1657, 172, 94, 31 }, { 1616, 2013, 1699, 1796, 101, 124, 127 }, { 2279, 2535, 2310, 2229, 144, 141, 40 }, { 7629, 2356, 7550, 2350, 474, 147, 20 }, + { 6299, 2652, 6609, 2836, 403, 171, 109 }, { 4407, 3243, 4531, 3239, 275, 202, 80 }, + }; + for (const auto &m : measured) { + const int a = GELineCoverageAlpha(m[0], m[1], m[2], m[3], m[4], m[5]); + if (a != m[6]) { + printf("GELineCoverageAlpha((%d, %d) - (%d, %d), pixel %d, %d) = %d, PSP %d\n", m[0], m[1], m[2], m[3], m[4], m[5], a, m[6]); + return false; + } + } + return true; +} + +bool TestGEMath() { + bool ok = true; + ok = TestFloat24() && ok; + ok = TestGERecip() && ok; + ok = TestGERsqrt() && ok; + ok = TestGEAdd() && ok; + ok = TestGEDot() && ok; + ok = TestGESetupRecip() && ok; + ok = TestGELog16() && ok; + ok = TestTexCoordPrecision() && ok; + ok = TestGELightPow() && ok; + ok = TestGELightColor() && ok; + ok = TestGELineCoverageAlpha() && ok; + return ok; +} diff --git a/unittest/UnitTest.cpp b/unittest/UnitTest.cpp index 8831ccf636..32c0108a1b 100644 --- a/unittest/UnitTest.cpp +++ b/unittest/UnitTest.cpp @@ -2943,6 +2943,7 @@ bool TestZipSlip(); bool TestLzrc(); bool TestMpegCsc(); bool TestSplineTessellation(); +bool TestGEMath(); bool TestDemangle(); // The 8.3 short names games read out of d_private. These aren't verified against hardware yet (no @@ -3165,6 +3166,7 @@ TestItem availableTests[] = { TEST_ITEM(Lzrc), TEST_ITEM(MpegCsc), TEST_ITEM(SplineTessellation), + TEST_ITEM(GEMath), TEST_ITEM(Demangle), TEST_ITEM(TextureReplacer), TEST_ITEM(UITabOrder), diff --git a/unittest/UnitTests.vcxproj b/unittest/UnitTests.vcxproj index 3602781f19..094d9a6951 100644 --- a/unittest/UnitTests.vcxproj +++ b/unittest/UnitTests.vcxproj @@ -297,6 +297,7 @@ + diff --git a/unittest/UnitTests.vcxproj.filters b/unittest/UnitTests.vcxproj.filters index 2bd5c4dd27..17dbb60231 100644 --- a/unittest/UnitTests.vcxproj.filters +++ b/unittest/UnitTests.vcxproj.filters @@ -20,6 +20,7 @@ +