mirror of
https://github.com/SpeedBreakerProject/speedbreaker.git
synced 2026-10-17 23:01:20 +02:00
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
318 lines
14 KiB
C++
318 lines
14 KiB
C++
// Tests for gpu/depth24_glsl.h: the resolve shader's 24-bit depth pack
|
|
// (formats 22 and 23, k_24_8 and k_24_8_FLOAT) and its C++ mirror. Until
|
|
// d24-fix the pack was `unorm(c.r, 16777215.0) << 8`, which wrapped depth
|
|
// 1.0 (the cleared far plane) to 0: every texel of the sun's shadow map
|
|
// without a caster read "nearest", and roads lost their sunlight. From the
|
|
// repo root:
|
|
// clang++ -std=c++20 -O2 -Iruntime tests/depth24_pack_test.cpp -o build/depth24_pack_test
|
|
// build/depth24_pack_test runtime/gpu/renderer.cpp
|
|
// PackDepth24 evaluates the shader's own tokens (SB_DEPTH24_PACK_EXPR), so
|
|
// what is checked here is the expression the GPU compiles. With
|
|
// renderer.cpp's path (ctest passes it) it also checks that the resolve
|
|
// shader, rebuilt from its literal and comments left out, defines the
|
|
// helper and packs formats 22 and 23 with it.
|
|
#include <gpu/depth24_glsl.h>
|
|
#include <gpu/shared_memory_glsl.h>
|
|
|
|
#include <cfloat>
|
|
#include <cmath>
|
|
#include <cstdio>
|
|
#include <cstring>
|
|
#include <fstream>
|
|
#include <limits>
|
|
#include <utility>
|
|
#include <sstream>
|
|
#include <string>
|
|
|
|
using gpu::Depth24Rounding;
|
|
using gpu::PackDepth24;
|
|
using gpu::UnpackDepth24;
|
|
|
|
static int g_failures = 0;
|
|
#define CHECK(cond, ...) do { if (!(cond)) { g_failures++; printf(" FAIL %s:%d: %s: ", __FILE__, __LINE__, #cond); printf(__VA_ARGS__); printf("\n"); } } while (0)
|
|
|
|
static const Depth24Rounding kRoundings[] = { Depth24Rounding::Separate, Depth24Rounding::Fused };
|
|
static const char* Name(Depth24Rounding r) { return r == Depth24Rounding::Fused ? "fma" : "separate"; }
|
|
|
|
// The released pack (renderer.cpp up to v0.1.0): the same arithmetic, no
|
|
// clamp to 24 bits, shifted into the word.
|
|
static uint32_t ReleasedWord(float v, Depth24Rounding rounding)
|
|
{
|
|
float c = std::fmin(std::fmax(v, 0.0f), 1.0f);
|
|
float sum;
|
|
if (rounding == Depth24Rounding::Fused)
|
|
sum = std::fmaf(c, 16777215.0f, 0.5f);
|
|
else
|
|
{
|
|
volatile float product = c * 16777215.0f;
|
|
sum = product + 0.5f;
|
|
}
|
|
return uint32_t(sum) << 8;
|
|
}
|
|
|
|
static float FromBits(uint32_t bits)
|
|
{
|
|
float v;
|
|
memcpy(&v, &bits, 4);
|
|
return v;
|
|
}
|
|
|
|
static void TestFarPlane()
|
|
{
|
|
printf("far plane (1.0)\n");
|
|
for (Depth24Rounding r : kRoundings)
|
|
{
|
|
CHECK(PackDepth24(1.0f, r) == 0xFFFFFFu, "%s: 1.0 packs to %06X", Name(r), PackDepth24(1.0f, r));
|
|
CHECK(uint32_t(PackDepth24(1.0f, r) << 8) == 0xFFFFFF00u, "%s: word %08X", Name(r), uint32_t(PackDepth24(1.0f, r) << 8));
|
|
CHECK(UnpackDepth24(PackDepth24(1.0f, r)) == 1.0f, "%s: reads back as %.9g", Name(r), UnpackDepth24(PackDepth24(1.0f, r)));
|
|
// The bug this guards: the arithmetic without the clamp wraps 1.0 to
|
|
// the word 0 (nearest) either way. If this ever stops holding, the
|
|
// mirror no longer models the shader's arithmetic.
|
|
CHECK(ReleasedWord(1.0f, r) == 0u, "%s: the unclamped pack gave %08X, not the wrap to 0", Name(r), ReleasedWord(1.0f, r));
|
|
}
|
|
CHECK(PackDepth24(1.0f) == 0xFFFFFFu, "default rounding: %06X", PackDepth24(1.0f));
|
|
}
|
|
|
|
// Every binary32 in [0.5, 1] (the top of the range, where 24 bits can
|
|
// overflow): at most 0xFFFFFF, never decreasing, within 1 LSB of v * (2^24 - 1).
|
|
static void TestTopHalf()
|
|
{
|
|
printf("every binary32 in [0.5, 1]\n");
|
|
const uint32_t first = 0x3F000000u, last = 0x3F800000u; // 0.5, 1.0
|
|
for (Depth24Rounding r : kRoundings)
|
|
{
|
|
uint64_t over = 0, decreasing = 0, off = 0, released = 0;
|
|
uint32_t previous = 0, firstBad = 0;
|
|
double worst = 0;
|
|
for (uint32_t bits = first; bits <= last; bits++)
|
|
{
|
|
float v = FromBits(bits);
|
|
uint32_t d = PackDepth24(v, r);
|
|
double exact = double(v) * 16777215.0; // exact: 24 x 24 significant bits
|
|
double error = std::fabs(double(d) - exact);
|
|
worst = std::max(worst, error);
|
|
bool bad = false;
|
|
if (d > 0xFFFFFFu)
|
|
over++, bad = true;
|
|
if (bits > first && d < previous)
|
|
decreasing++, bad = true;
|
|
if (error > 1.0)
|
|
off++, bad = true;
|
|
if (bad && !firstBad)
|
|
firstBad = bits;
|
|
released += (ReleasedWord(v, r) >> 8) != d;
|
|
previous = d;
|
|
}
|
|
printf(" %s: %u values, worst error %.8f LSB; %llu differ from the released pack\n", Name(r), last - first + 1, worst,
|
|
(unsigned long long)released);
|
|
CHECK(over == 0 && decreasing == 0 && off == 0, "%s: %llu above 0xFFFFFF, %llu decreasing, %llu more than 1 LSB off (first at %.9g)",
|
|
Name(r), (unsigned long long)over, (unsigned long long)decreasing, (unsigned long long)off, FromBits(firstBad));
|
|
// Only 1.0 changed: the clamp touches nothing else.
|
|
CHECK(released == 1, "%s: %llu values pack differently from the released arithmetic, expected 1 (1.0)", Name(r),
|
|
(unsigned long long)released);
|
|
}
|
|
}
|
|
|
|
static void TestZeroAndBelow()
|
|
{
|
|
printf("0, negatives, and outside [0, 1]\n");
|
|
const float inf = std::numeric_limits<float>::infinity();
|
|
const float low[] = { 0.0f, -0.0f, -FLT_TRUE_MIN, -FLT_MIN, -1e-30f, -0.25f, -0.5f, -1.0f, -2.0f, -FLT_MAX, -inf,
|
|
FLT_TRUE_MIN, FLT_MIN, 1e-30f, 1e-8f };
|
|
const float high[] = { std::nextafter(1.0f, 2.0f), 1.5f, 2.0f, 1e30f, FLT_MAX, inf };
|
|
for (Depth24Rounding r : kRoundings)
|
|
{
|
|
for (float v : low)
|
|
CHECK(PackDepth24(v, r) == 0u, "%s: %.9g packs to %06X", Name(r), v, PackDepth24(v, r));
|
|
for (float v : high)
|
|
CHECK(PackDepth24(v, r) == 0xFFFFFFu, "%s: %.9g packs to %06X", Name(r), v, PackDepth24(v, r));
|
|
CHECK(PackDepth24(std::numeric_limits<float>::quiet_NaN(), r) == 0u, "%s: NaN packs to %06X", Name(r),
|
|
PackDepth24(std::numeric_limits<float>::quiet_NaN(), r));
|
|
}
|
|
}
|
|
|
|
// Every 24-bit value, read as the untile reads it (float(d) / 16777215) and
|
|
// resolved again: within 1 LSB, and the ends exact.
|
|
static void TestRoundTrip()
|
|
{
|
|
printf("every 24-bit value round-trips\n");
|
|
for (Depth24Rounding r : kRoundings)
|
|
{
|
|
uint64_t offByOne = 0, worse = 0;
|
|
uint32_t firstWorse = 0;
|
|
for (uint32_t k = 0; k <= 0xFFFFFFu; k++)
|
|
{
|
|
uint32_t back = PackDepth24(UnpackDepth24(k), r);
|
|
uint32_t diff = back > k ? back - k : k - back;
|
|
offByOne += diff == 1;
|
|
if (diff > 1 && !worse++)
|
|
firstWorse = k;
|
|
}
|
|
printf(" %s: %llu of 16777216 off by one, %llu by more\n", Name(r), (unsigned long long)offByOne, (unsigned long long)worse);
|
|
CHECK(worse == 0, "%s: %llu values more than 1 LSB off (first %06X -> %06X)", Name(r), (unsigned long long)worse, firstWorse,
|
|
PackDepth24(UnpackDepth24(firstWorse), r));
|
|
CHECK(PackDepth24(UnpackDepth24(0), r) == 0u, "%s: 0 came back as %06X", Name(r), PackDepth24(UnpackDepth24(0), r));
|
|
CHECK(PackDepth24(UnpackDepth24(0xFFFFFFu), r) == 0xFFFFFFu, "%s: 0xFFFFFF came back as %06X", Name(r),
|
|
PackDepth24(UnpackDepth24(0xFFFFFFu), r));
|
|
}
|
|
}
|
|
|
|
// GLSL text without its comments (GLSL has no string literals): a line
|
|
// commented out, or an expression quoted in a comment, doesn't count.
|
|
static std::string StripGlslComments(const std::string& text)
|
|
{
|
|
std::string out;
|
|
for (size_t i = 0; i < text.size();)
|
|
{
|
|
if (text.compare(i, 2, "//") == 0)
|
|
i = std::min(text.find('\n', i), text.size());
|
|
else if (text.compare(i, 2, "/*") == 0)
|
|
{
|
|
size_t end = text.find("*/", i + 2);
|
|
i = end == std::string::npos ? text.size() : end + 2;
|
|
out += ' ';
|
|
}
|
|
else
|
|
out += text[i++];
|
|
}
|
|
return out;
|
|
}
|
|
|
|
static uint32_t Count(const std::string& text, const std::string& what)
|
|
{
|
|
uint32_t n = 0;
|
|
for (size_t at = text.find(what); at != std::string::npos; at = text.find(what, at + 1))
|
|
n++;
|
|
return n;
|
|
}
|
|
|
|
// Each line of `text` that is exactly `line` once surrounding blanks are
|
|
// trimmed.
|
|
static uint32_t CountLines(const std::string& text, const std::string& line)
|
|
{
|
|
uint32_t n = 0;
|
|
std::istringstream in(text);
|
|
for (std::string l; std::getline(in, l);)
|
|
{
|
|
size_t a = l.find_first_not_of(" \t\r"), b = l.find_last_not_of(" \t\r");
|
|
n += a != std::string::npos && l.compare(a, b - a + 1, line) == 0;
|
|
}
|
|
return n;
|
|
}
|
|
|
|
// The GLSL text: packDepth24 is defined once, outside comments, with the
|
|
// body PackDepth24 evaluates (SB_DEPTH24_PACK_EXPR's tokens, so the two
|
|
// can't differ; GLSL_DEPTH24_PACK_DEFINITION is that line).
|
|
static void TestGlslText()
|
|
{
|
|
printf("GLSL text\n");
|
|
const std::string definition = GLSL_DEPTH24_PACK_DEFINITION;
|
|
printf(" %s\n", definition.c_str());
|
|
CHECK(definition == "uint packDepth24(float v) { return " SB_DEPTH24_STRINGIFY(SB_DEPTH24_PACK_EXPR(v)) "; }",
|
|
"the definition isn't SB_DEPTH24_PACK_EXPR's body: %s", definition.c_str());
|
|
// The clamp, as text (the checks above test what it does).
|
|
CHECK(definition.find("min(") != std::string::npos && definition.find("0xFFFFFFu") != std::string::npos,
|
|
"no clamp to 0xFFFFFF in %s", definition.c_str());
|
|
const std::string code = StripGlslComments(GLSL_DEPTH24_PACK);
|
|
CHECK(CountLines(code, definition) == 1, "GLSL_DEPTH24_PACK doesn't define packDepth24 on a line of its own, outside comments:\n%s",
|
|
GLSL_DEPTH24_PACK);
|
|
CHECK(Count(code, "packDepth24") == 1, "%u packDepth24 outside comments in GLSL_DEPTH24_PACK, expected 1:\n%s",
|
|
Count(code, "packDepth24"), GLSL_DEPTH24_PACK);
|
|
}
|
|
|
|
// The GLSL that `name`'s literal in renderer.cpp's source makes: its raw
|
|
// string pieces, and the macros between them replaced by their text. A
|
|
// macro this test doesn't include (another branch's splice) is left out,
|
|
// and named.
|
|
static bool ShaderText(const std::string& src, const char* name, std::string& glsl)
|
|
{
|
|
std::string start = std::string("const char* ") + name + " =";
|
|
size_t at = src.find(start);
|
|
CHECK(at != std::string::npos, "no %s", name);
|
|
if (at == std::string::npos)
|
|
return false;
|
|
at += start.size();
|
|
const std::pair<const char*, const char*> splices[] = {
|
|
{ "GLSL_DEPTH24_PACK", GLSL_DEPTH24_PACK },
|
|
{ "GLSL_SHARED_MEMORY_WRITE", GLSL_SHARED_MEMORY_WRITE },
|
|
{ "GLSL_SHARED_MEMORY_READ", GLSL_SHARED_MEMORY_READ },
|
|
};
|
|
while (true)
|
|
{
|
|
at = src.find_first_not_of(" \t\r\n", at);
|
|
if (at == std::string::npos)
|
|
break;
|
|
if (src[at] == ';')
|
|
return true;
|
|
if (src.compare(at, 3, "R\"(") == 0)
|
|
{
|
|
size_t end = src.find(")\"", at + 3);
|
|
if (end == std::string::npos)
|
|
break;
|
|
glsl += src.substr(at + 3, end - at - 3);
|
|
at = end + 2;
|
|
continue;
|
|
}
|
|
size_t end = src.find_first_of(" \t\r\n;", at);
|
|
std::string word = src.substr(at, end - at);
|
|
bool known = false, macro = !word.empty() && word.find_first_not_of("ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789_") == std::string::npos;
|
|
for (auto& [splice, text] : splices)
|
|
if (word == splice)
|
|
glsl += text, known = true;
|
|
if (!known && macro)
|
|
printf(" (%s splices %s, which isn't checked here)\n", name, word.c_str());
|
|
CHECK(known || macro, "%s: '%s' is neither a raw string nor a macro", name, word.c_str());
|
|
if (!known && !macro)
|
|
return false;
|
|
at = end;
|
|
}
|
|
CHECK(false, "%s doesn't end", name);
|
|
return false;
|
|
}
|
|
|
|
// renderer.cpp: the resolve shader takes the helper and packs 22 and 23
|
|
// with it (every variant compiles from this one text). The text is the
|
|
// shader's own, comments left out.
|
|
static void TestRendererSource(const char* path)
|
|
{
|
|
printf("resolve shader in %s\n", path);
|
|
std::ifstream file(path);
|
|
CHECK(file.good(), "can't read %s", path);
|
|
if (!file.good())
|
|
return;
|
|
std::stringstream ss;
|
|
ss << file.rdbuf();
|
|
std::string glsl;
|
|
if (!ShaderText(ss.str(), "kResolveGlsl", glsl))
|
|
return;
|
|
const std::string code = StripGlslComments(glsl);
|
|
CHECK(CountLines(code, GLSL_DEPTH24_PACK_DEFINITION) == 1, "kResolveGlsl doesn't define packDepth24 as GLSL_DEPTH24_PACK does");
|
|
CHECK(Count(code, "packDepth24(float") == 1, "%u packDepth24 definitions in kResolveGlsl, expected 1", Count(code, "packDepth24(float"));
|
|
// pack32's line for 22 and 23, and nothing else packing 24 bits.
|
|
CHECK(Count(code, "case 22u: case 23u:") == 1, "%u case 22u/23u lines in kResolveGlsl, expected 1 (pack32)",
|
|
Count(code, "case 22u: case 23u:"));
|
|
CHECK(CountLines(code, "case 22u: case 23u: return packDepth24(c.r) << 8;") == 1, "formats 22/23 don't pack with packDepth24");
|
|
size_t pack32 = code.find("uint pack32(vec4 c)"), line = code.find("case 22u: case 23u:");
|
|
CHECK(pack32 != std::string::npos && line > pack32 && code.find("uint pack16(", pack32) > line,
|
|
"the case 22u/23u line isn't in pack32");
|
|
// (The untile's float(v >> 8) / 16777215.0 reads depth back; packs
|
|
// either shift the result or pass the scale to unorm.)
|
|
CHECK(code.find("16777215.0) <<") == std::string::npos && code.find("16777215.0)<<") == std::string::npos &&
|
|
code.find(", 16777215.0)") == std::string::npos, "an unclamped 24-bit pack is left in kResolveGlsl");
|
|
}
|
|
|
|
int main(int argc, char** argv)
|
|
{
|
|
TestFarPlane();
|
|
TestTopHalf();
|
|
TestZeroAndBelow();
|
|
TestRoundTrip();
|
|
TestGlslText();
|
|
if (argc > 1)
|
|
TestRendererSource(argv[1]);
|
|
else
|
|
printf("(no renderer.cpp path given: its resolve shader not checked)\n");
|
|
printf(g_failures ? "%d FAILED\n" : "all passed\n", g_failures);
|
|
return g_failures ? 1 : 0;
|
|
}
|