Files
SpeedBreakerProject--speedb…/tests/fade_layers_test.cpp
T
2026-10-22 14:02:14 +00:00

394 lines
17 KiB
C++

// Tests for the fade to black before a load (gpu/edge_bars.h's PlanLayer,
// and gpu/renderer.cpp using it), a report of a "black rectangle right
// before the last intro race with Razor" (v0.1.7 on a 3024x1898 MacBook, the
// career prologue's EndingNis03 fading out before the storyfmv_pin11 movie):
// one flat front-end quad, FB000000 at its darkest, x +-1.121 and y +-0.972
// of the 16:9 band, short of the screen's top and bottom (and of a wide
// screen's sides), so the frozen scene showed above and below it for the
// fade's ~1.5 s; the dim over the loading screen (B8000000, x +-1.104, y
// -0.978..0.985) is the same shape. Both are now stretched to the screen's
// edges, at every screen shape. From the repo root:
// clang++ -std=c++20 -O2 -UNDEBUG -Iruntime tests/fade_layers_test.cpp -o build/fade_layers_test
// build/fade_layers_test runtime/gpu/renderer.cpp
// Planted faults: the match cases run again under deliberately wrong
// thresholds (each must fail a case), the pixel oracle on the layer as drawn
// before (it must fail), and the renderer checks on copies of renderer.cpp
// with the wiring cut (each must fail).
#include <gpu/edge_bars.h>
#include <algorithm>
#include <cmath>
#include <cstdint>
#include <cstdio>
#include <cstring>
#include <fstream>
#include <sstream>
#include <string>
using namespace gpu::bars;
static int g_failures = 0;
static bool g_quiet = false;
#define CHECK(cond, ...) do { if (!(cond)) { g_failures++; if (!g_quiet) { printf(" FAIL %s:%d: %s: ", __FILE__, __LINE__, #cond); printf(__VA_ARGS__); printf("\n"); } } } while (0)
// The screen shapes (view scale x, y), as menu_borders_test's.
struct Shape
{
const char* name;
float sx, sy;
};
static const Shape kShapes[] = {
{ "1280x720 (16:9)", 1.0f, 1.0f },
{ "1280x800 (Deck)", 1.0f, 0.9f },
{ "3024x1898 (MacBook Pro 14, the report's)", 1.0f, 0.896207f },
{ "3024x1964 (its whole screen)", 1.0f, 0.866667f },
{ "1200x800 (3:2)", 1.0f, 0.84375f },
{ "1024x768 (4:3)", 1.0f, 0.75f },
{ "2868x1320 (iPhone 18 Pro Max)", 0.818182f, 1.0f },
{ "2560x1080 (21:9)", 0.75f, 1.0f },
{ "3440x1440", 0.744186f, 1.0f },
{ "2880x810 (32:9)", 0.5f, 1.0f },
};
// As logged (NFSMW_LOG_COVERS, runs r1/x3, 2026-10-20): the fade before the
// pin11 movie and the loading screen's dim.
constexpr float kFadeX = 1.121f, kFadeY = 0.972f;
constexpr uint32_t kFade = 0xFB000000u;
struct Quad
{
Vertex v[4];
};
static Quad Rectangle(float bx0, float bx1, float by0, float by1, float sx, float sy, uint32_t argb)
{
const float x0 = bx0 * sx, x1 = bx1 * sx, y0 = by0 * sy, y1 = by1 * sy;
return { { { x0, y0, argb }, { x1, y0, argb }, { x1, y1, argb }, { x0, y1, argb } } };
}
// ---- The shape ----
static void Match(const char* what, const Quad& q, const Shape& s, bool want, const LayerRule& rule)
{
const bool got = MatchLayer(q.v, s.sx, s.sy, rule);
CHECK(got == want, "%s at %s: %s, want %s", what, s.name, got ? "layer" : "none", want ? "layer" : "none");
}
static int LayerCases(const LayerRule& rule)
{
const int before = g_failures;
for (const Shape& s : kShapes)
{
auto at = [&](float x0, float x1, float y0, float y1, uint32_t argb) { return Rectangle(x0, x1, y0, y1, s.sx, s.sy, argb); };
// Taken: short of the screen's top and bottom at every shape (and of
// its sides past 1.121 of the band: 21:9, 32:9, the iPhone).
Match("the fade before the pin11 movie", at(-kFadeX, kFadeX, -kFadeY, kFadeY, kFade), s, true, rule);
Match("the fade as it starts (alpha 08)", at(-kFadeX, kFadeX, -kFadeY, kFadeY, 0x08000000u), s, true, rule);
Match("the loading screen's dim", at(-1.104f, 1.104f, -0.978f, 0.985f, 0xB8000000u), s, true, rule);
Match("the fade, y flipped", at(-kFadeX, kFadeX, kFadeY, -kFadeY, kFade), s, true, rule);
Match("a grey layer of the fade's shape", at(-kFadeX, kFadeX, -kFadeY, kFadeY, 0x80404040u), s, true, rule);
Match("the Blacklist bio's panel fading in (the black frame rule's once opaque)", at(-0.976f, 0.976f, -0.985f, 0.985f, 0x11000000u), s,
true, rule);
// Left alone.
Match("the intro's black frame (x 0.970, y 0.939: the black frame rule's)", at(-0.970f, 0.970f, -0.939f, 0.939f, 0xFF000000u), s, false,
rule);
Match("the intro's other black frame (y 0.920)", at(-0.970f, 0.970f, -0.920f, 0.920f, 0xFE000000u), s, false, rule);
Match("a fade past the screen's edges (+-1.29 / +-1.40)", at(-1.29f / s.sx, 1.29f / s.sx, -1.40f / s.sy, 1.40f / s.sy, 0xFF000000u), s,
false, rule);
Match("vid cam dark overlay", at(-0.712f, 0.668f, -0.893f, 0.908f, 0x3C000000u), s, false, rule);
Match("cam dialog box", at(-0.578f, 0.586f, -0.167f, 0.569f, 0xFD000000u), s, false, rule);
Match("pause dim centre", at(-0.53f, 0.57f, -1.91f, 1.91f, 0xC8000000u), s, false, rule);
Match("a layer 0.93 of the band", at(-0.93f, 0.93f, -0.93f, 0.93f, kFade), s, false, rule);
Match("the fade off-centre by 0.03 in y", at(-kFadeX, kFadeX, -kFadeY + 0.03f, kFadeY + 0.03f, kFade), s, false, rule);
Quad grad = at(-kFadeX, kFadeX, -kFadeY, kFadeY, kFade);
grad.v[1].argb = grad.v[2].argb = 0x00000000u; // the pause dim's side gradients: alpha varies
Match("a gradient of the fade's shape", grad, s, false, rule);
Quad bent = at(-kFadeX, kFadeX, -kFadeY, kFadeY, kFade);
bent.v[2].x -= 0.05f;
Match("a bent quad", bent, s, false, rule);
}
// A layer short of the screen's edge by less than the threshold: the
// whole band's height at 16:9 (+-1.0): nothing to stretch.
Match("a layer over the whole 16:9 frame at 16:9", Rectangle(-kFadeX, kFadeX, -1.0f, 1.0f, 1.0f, 1.0f, kFade), kShapes[0], false, rule);
return g_failures - before;
}
static void Planted(const char* what, const LayerRule& wrong)
{
const int saved = g_failures;
g_quiet = true;
const int failed = LayerCases(wrong);
g_quiet = false;
g_failures = saved;
printf("planted %s: %d case(s) failed\n", what, failed);
CHECK(failed > 0, "planted %s passed every layer case", what);
}
// ---- The plan on the draw as the renderer sees it ----
struct Matrix
{
float c[8];
};
static Matrix FrontEndMatrix(float sx, float sy)
{
return { { 0.69f * sx, 0.0f, 0.0f, 0.0f, 0.0f, 0.92f * sy, 0.0f, 0.0f } };
}
// The front end's posScale (renderer.cpp: 2 sx / w, 2 sy / h, 2 ox / w - 1,
// 2 oy / h - 1): viewport s(640,-360) o(640,360) in the 1280x2048 target at
// `scale` (1x or 2x: the host viewport is the target's size times it).
static void FrontEndPs(float ps[4], float ox = 640.0f, float oy = 360.0f)
{
ps[0] = 2.0f * 640.0f / 1280.0f;
ps[1] = 2.0f * -360.0f / 2048.0f;
ps[2] = 2.0f * ox / 1280.0f - 1.0f;
ps[3] = 2.0f * oy / 2048.0f - 1.0f;
}
// The fade's draw as logged: blend 07060706, t0 32x32 (the white texture).
static Draw FadeDraw(const Matrix& m, float sx, float sy, const float ps[4], float hostW, float hostH)
{
return Draw{ .prim = 13,
.count = 4,
.c = m.c,
.frontEndY = std::fabs(m.c[5]),
.scaleX = sx,
.scaleY = sy,
.frontEndShader = true,
.leftAlone = true,
.occlusion = false,
.vte = 0x43F,
.blend0 = 0x07060706,
.t0Sampled = true,
.t0Width = 32,
.t0Height = 32,
.t0Format = 20,
.posScale = { ps[0], ps[1], ps[2], ps[3] },
.hostW = hostW,
.hostH = hostH,
.scissor = { 0, 0, int32_t(hostW), int32_t(hostH) } };
}
static Plan Run(const Draw& d, const Quad& q, const Matrix& m, uint32_t vertexLimit = 4)
{
uint32_t w[4][4];
for (int k = 0; k < 4; k++)
{
const float p[3] = { q.v[k].x / m.c[0], q.v[k].y / m.c[5], 0.0f };
std::memcpy(w[k], p, sizeof(p));
w[k][3] = q.v[k].argb;
}
return PlanLayer(d, [&](uint32_t k, uint32_t j, uint32_t& word) {
word = w[k & 3][j & 3];
return k < vertexLimit;
});
}
// Every host pixel of the screen (the target's top `screenH` rows) has its
// centre inside the quad as `ps` draws its box: returns those outside.
static int Outside(const Box& b, const float ps[4], float hostW, float hostH, float screenH)
{
const float nx0 = b.x0 * ps[0] + ps[2], nx1 = b.x1 * ps[0] + ps[2], ny0 = b.y0 * ps[1] + ps[3], ny1 = b.y1 * ps[1] + ps[3];
const float lx = std::min(nx0, nx1), hx = std::max(nx0, nx1), ly = std::min(ny0, ny1), hy = std::max(ny0, ny1);
int outside = 0;
for (int32_t y = 0; y < int32_t(screenH); y++)
for (int32_t x = 0; x < int32_t(hostW); x++)
{
const float ndcX = (float(x) + 0.5f) / hostW * 2.0f - 1.0f, ndcY = (float(y) + 0.5f) / hostH * 2.0f - 1.0f;
outside += !(ndcX > lx && ndcX < hx && ndcY > ly && ndcY < hy);
}
return outside;
}
static void PlanTests()
{
int stretched = 0;
for (const Shape& s : kShapes)
for (const int scale : { 1, 2 })
for (const bool half : { false, true }) // D3D's half-pixel centres
{
const Matrix m = FrontEndMatrix(s.sx, s.sy);
float ps[4];
FrontEndPs(ps, half ? 640.5f : 640.0f, half ? 360.5f : 360.0f);
const float hostW = 1280.0f * scale, hostH = 2048.0f * scale, screenH = 720.0f * scale;
const Draw d = FadeDraw(m, s.sx, s.sy, ps, hostW, hostH);
const Quad fade = Rectangle(-kFadeX, kFadeX, -kFadeY, kFadeY, s.sx, s.sy, kFade);
const Plan p = Run(d, fade, m);
CHECK(p.fill == Fill::Layer, "the fade at %s, %dx%s: not taken", s.name, scale, half ? " (half-pixel)" : "");
if (p.fill != Fill::Layer)
continue;
CHECK(p.rectCount == 0 && p.argb == kFade, "the fade's plan at %s: %u rectangles, colour %08X", s.name, p.rectCount, p.argb);
// The oracle: as drawn before, the screen's top and bottom
// rows (and a wide screen's sides) are outside it; stretched,
// none.
const int before = Outside(p.box, ps, hostW, hostH, screenH), after = Outside(p.box, p.posScale, hostW, hostH, screenH);
CHECK(before > 0, "the fade as the game draws it covers %s's whole screen (the oracle sees nothing to fix)", s.name);
CHECK(after == 0, "the fade stretched at %s, %dx: %d host pixel(s) still outside it", s.name, scale, after);
stretched++;
}
printf("the fade stretched to the screen's edges: %d shape/scale/centre cases\n", stretched);
// The register checks refuse what isn't a flat front-end layer.
const Shape& mac = kShapes[2];
const Matrix m = FrontEndMatrix(mac.sx, mac.sy);
float ps[4];
FrontEndPs(ps);
const Quad fade = Rectangle(-kFadeX, kFadeX, -kFadeY, kFadeY, mac.sx, mac.sy, kFade);
struct Fault
{
const char* what;
void (*apply)(Draw&);
};
const Fault faults[] = {
{ "over a picture (t0 1024x512: the results screen)", [](Draw& d) { d.t0Width = 1024; d.t0Height = 512; } },
{ "widened by another rule", [](Draw& d) { d.leftAlone = false; } },
{ "inside an occlusion query", [](Draw& d) { d.occlusion = true; } },
{ "not the front-end shader", [](Draw& d) { d.frontEndShader = false; } },
{ "two quads", [](Draw& d) { d.count = 8; } },
{ "a triangle list", [](Draw& d) { d.prim = 4; } },
{ "additive blending", [](Draw& d) { d.blend0 = 0x01060106; } },
{ "no viewport transform", [](Draw& d) { d.vte = 0x400; } },
};
for (const Fault& f : faults)
{
Draw d = FadeDraw(m, mac.sx, mac.sy, ps, 1280.0f, 2048.0f);
f.apply(d);
CHECK(Run(d, fade, m).fill == Fill::None, "the fade %s taken", f.what);
}
Draw d = FadeDraw(m, mac.sx, mac.sy, ps, 1280.0f, 2048.0f);
CHECK(Run(d, fade, m, 3).fill == Fill::None, "a short vertex buffer taken");
// The black frame rule keeps its own (PlanFill first in the renderer):
// the intro's black frame is PlanFill's, and PlanLayer leaves it.
const Quad frame = Rectangle(-0.970f, 0.970f, -0.939f, 0.939f, mac.sx, mac.sy, 0xFF000000u);
CHECK(Run(d, frame, m).fill == Fill::None, "the intro's black frame taken for a layer");
}
// ---- gpu/renderer.cpp uses it ----
static std::string Squash(const std::string& source)
{
std::string code, r;
std::istringstream in(source);
for (std::string line; std::getline(in, line);)
{
size_t c = line.find("//");
code += (c == std::string::npos ? line : line.substr(0, c)) + "\n";
}
for (char c : code)
if (c != ' ' && c != '\n' && c != '\t')
r += c;
return r;
}
static size_t Count(const std::string& s, const std::string& what)
{
size_t n = 0;
for (size_t at = s.find(what); at != std::string::npos; at = s.find(what, at + 1))
n++;
return n;
}
static int RendererChecks(const std::string& source, bool quiet)
{
const int before = g_failures;
const bool was = g_quiet;
g_quiet = quiet;
const std::string all = Squash(source);
// planEdgeFills: the layer last, only when nothing earlier took the draw.
const size_t vignette = all.find("plan=gpu::bars::PlanVignette(draw,read,texel);");
const size_t layer = all.find("if(plan.fill==gpu::bars::Fill::None)plan=gpu::bars::PlanLayer(draw,read);");
const size_t ret = layer == std::string::npos ? layer : all.find("returnplan;", layer);
CHECK(vignette != std::string::npos && layer != std::string::npos && vignette < layer && ret != std::string::npos && ret - layer < 200,
"planEdgeFills asks PlanLayer after PlanVignette, for a draw nothing else took");
CHECK(Count(all, "barPlan=planEdgeFills(c,") == 2, "planEdgeFills asked on both paths: %zu", Count(all, "barPlan=planEdgeFills(c,"));
// Applied to the draw itself on both paths (wide or tall screens, 16:9),
// before its constants are pushed.
const std::string apply = "if(barPlan.fill==gpu::bars::Fill::Layer)memcpy(pc.posScale,barPlan.posScale,sizeof(pc.posScale));";
CHECK(Count(all, apply) == 2, "the layer's posScale applied on both paths: %zu", Count(all, apply));
const size_t lastApply = all.rfind(apply);
const size_t push = lastApply == std::string::npos ? lastApply : all.find("pushConstants(pc);", lastApply);
const size_t draw = lastApply == std::string::npos ? lastApply : all.find("vkCmdDraw", lastApply);
CHECK(push != std::string::npos && draw != std::string::npos && push < draw,
"the posScale is set before the draw's constants are pushed and it is drawn");
CHECK(all.find("staticconstboolbarFill=[]{constchar*v=std::getenv(\"NFSMW_BAR_FILL\");return!(v&&v[0]=='0');}();") != std::string::npos,
"NFSMW_BAR_FILL=0 still turns the edge fills off");
g_quiet = was;
return g_failures - before;
}
static void RendererTests(const char* path)
{
std::ifstream f(path);
CHECK(f.good(), "cannot read %s", path);
if (!f.good())
return;
std::stringstream ss;
ss << f.rdbuf();
const std::string source = ss.str();
CHECK(RendererChecks(source, false) == 0, "renderer.cpp's layer wiring");
struct Cut
{
const char* what;
const char* from;
const char* to;
};
const Cut cuts[] = {
{ "PlanLayer never asked", "plan = gpu::bars::PlanLayer(draw, read);", ";" },
{ "PlanLayer's guarded call removed", " if (plan.fill == gpu::bars::Fill::None)\n plan = gpu::bars::PlanLayer(draw, read);\n", "" },
{ "the 16:9 path doesn't apply it",
" if (barPlan.fill == gpu::bars::Fill::Layer)\n memcpy(pc.posScale, barPlan.posScale, sizeof(pc.posScale));\n", "" },
{ "the wide/tall path doesn't apply it",
" if (barPlan.fill == gpu::bars::Fill::Layer)\n memcpy(pc.posScale, barPlan.posScale, sizeof(pc.posScale));\n", "" },
};
for (const Cut& c : cuts)
{
std::string copy = source;
const size_t at = copy.find(c.from);
CHECK(at != std::string::npos, "planted '%s': its text is not in renderer.cpp", c.what);
if (at == std::string::npos)
continue;
copy.replace(at, std::strlen(c.from), c.to);
const int saved = g_failures;
const int failed = RendererChecks(copy, true);
g_failures = saved;
printf("planted renderer '%s': %d check(s) failed\n", c.what, failed);
CHECK(failed > 0, "planted renderer '%s' passed every check", c.what);
}
}
int main(int argc, char** argv)
{
CHECK(LayerCases({}) == 0, "the layer cases");
{
LayerRule r;
r.extentMin = 0.90f; // the intro's black frames
Planted("extentMin 0.90", r);
r = {};
r.extentMin = 0.98f; // the fade itself (y 0.972)
Planted("extentMin 0.98", r);
r = {};
r.centreMax = 0.05f; // the off-centre layer
Planted("centreMax 0.05", r);
r = {};
r.shortMin = 0.05f; // the fade's 0.028 short at 16:9
Planted("shortMin 0.05", r);
r = {};
r.shortMin = -0.5f; // layers already past the edges
Planted("shortMin -0.5", r);
r = {};
r.cornerTolerance = 0.1f; // the bent quad
Planted("cornerTolerance 0.1", r);
}
PlanTests();
if (argc > 1)
RendererTests(argv[1]);
else
printf("(no renderer.cpp given: its checks skipped)\n");
if (g_failures)
{
printf("fade_layers_test: %d failure(s)\n", g_failures);
return 1;
}
printf("fade_layers_test: all passed\n");
return 0;
}