mirror of
https://github.com/SpeedBreakerProject/speedbreaker.git
synced 2026-10-17 22:01:34 +02:00
Co-authored-by: aleszczynskig <36401256+aleszczynskig@users.noreply.github.com> Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
754 lines
35 KiB
C++
754 lines
35 KiB
C++
// Tests for GitHub #19, "rectangular borders in specific places in menus"
|
|
// (gpu/edge_bars.h, and gpu/renderer.cpp using it), at every screen shape:
|
|
// - the Showcase (the car lot's, customize shop's and Blacklist rival cars'
|
|
// "Rotate / Zoom" viewer) darkens the 3D garage with one front-end quad,
|
|
// centred, x +-0.970 and y +-0.958 of the 16:9 band, its 256x256 texture
|
|
// clear in the middle and dark toward its edges (MatchVignette,
|
|
// TexelOffset32, PlanVignette): it is drawn stretched to the screen's
|
|
// edges;
|
|
// - the Blacklist rival bio (SafeHouseRivalBio.fng) sets its text and
|
|
// picture on a black panel, x +-0.976 and y +-0.985 (a black frame,
|
|
// PlanFill): its copies now fill everything outside it, the band's thin
|
|
// border too, at 16:9 as well (v0.1.5: only past the band).
|
|
// Both stopped short of every screen edge: a thin border of what was behind
|
|
// at 16:9, as on a real 360's own output (a period TV's overscan hid it);
|
|
// under Vert+ the Showcase's garage above and below the band showed
|
|
// undimmed too, a bright rectangular frame.
|
|
// From the repo root:
|
|
// clang++ -std=c++20 -O2 -UNDEBUG -Iruntime tests/menu_borders_test.cpp -o build/menu_borders_test
|
|
// build/menu_borders_test runtime/gpu/renderer.cpp
|
|
// Planted faults: the match cases run again under deliberately wrong
|
|
// thresholds (each must fail a case), the texel and register checks are
|
|
// broken one at a time (each must refuse the plan), the pixel oracles run on
|
|
// wrong fills (each must fail), and the renderer checks run 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 <iterator>
|
|
#include <utility>
|
|
#include <sstream>
|
|
#include <string>
|
|
#include <vector>
|
|
|
|
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 video/picture_fit gives them, and
|
|
// the host size each is drawn at (Auto internal resolution: 2x on the big
|
|
// ones; posScale covers it).
|
|
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)", 1.0f, 0.896207f },
|
|
{ "3600x2338 less the notch (the report's MacBook, 3600x2260)", 1.0f, 0.896f },
|
|
{ "1200x800 (3:2)", 1.0f, 0.84375f },
|
|
{ "1024x768 (4:3)", 1.0f, 0.75f },
|
|
{ "2732x2048 (iPad Pro)", 1.0f, 0.750366f },
|
|
{ "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 },
|
|
};
|
|
|
|
// The vignette as logged (NFSMW_LOG_FRONTEND, "inset candidate": band x
|
|
// -0.970..0.970 y -0.958..0.958, t0 14749000 256x256 f6, blend 07060706,
|
|
// colour 047F7F7F fading in to FE7F7F7F) at every shape.
|
|
constexpr float kVigX = 0.970f, kVigY = 0.958f;
|
|
constexpr uint32_t kVigColour = 0xFE7F7F7F;
|
|
|
|
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, float sx, float sy, bool want, const VignetteRule& rule)
|
|
{
|
|
Box box{};
|
|
const bool got = MatchVignette(q.v, sx, sy, rule, &box);
|
|
CHECK(got == want, "%s at x %.4f y %.4f: %s, want %s", what, sx, sy, got ? "vignette" : "none", want ? "vignette" : "none");
|
|
if (got && want)
|
|
{
|
|
const float x0 = std::min(q.v[0].x, q.v[2].x), x1 = std::max(q.v[0].x, q.v[2].x);
|
|
const float y0 = std::min(q.v[0].y, q.v[2].y), y1 = std::max(q.v[0].y, q.v[2].y);
|
|
CHECK(box.x0 == x0 && box.x1 == x1 && box.y0 == y0 && box.y1 == y1, "%s: box %.4f..%.4f %.4f..%.4f", what, box.x0, box.x1, box.y0,
|
|
box.y1);
|
|
}
|
|
}
|
|
|
|
// Every shape case. Returns the failures it added (for the planted rules).
|
|
static int VignetteCases(const VignetteRule& 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); };
|
|
Match("Showcase vignette", at(-kVigX, kVigX, -kVigY, kVigY, kVigColour), s.sx, s.sy, true, rule);
|
|
Match("Showcase vignette fading in (alpha 04)", at(-kVigX, kVigX, -kVigY, kVigY, 0x047F7F7F), s.sx, s.sy, true, rule);
|
|
Match("an overlay 0.92 of the band", at(-0.92f, 0.92f, -0.92f, 0.92f, kVigColour), s.sx, s.sy, true, rule);
|
|
Match("an overlay 0.985 of the band", at(-0.985f, 0.985f, -0.985f, 0.985f, kVigColour), s.sx, s.sy, true, rule);
|
|
// Its vertices in another order around it (y flipped).
|
|
Quad flip = at(-kVigX, kVigX, kVigY, -kVigY, kVigColour);
|
|
Match("Showcase vignette, y flipped", flip, s.sx, s.sy, true, rule);
|
|
// What must keep its own shape (or isn't this overlay).
|
|
Match("title key art / results picture", at(-1.104f, 1.104f, -0.981f, 0.981f, 0xFF7F7F7F), s.sx, s.sy, false, rule);
|
|
Match("a picture over the whole band", at(-1.0f, 1.0f, -1.0f, 1.0f, 0xFF808080), s.sx, s.sy, false, rule);
|
|
Match("an overlay 0.995 of the band", at(-0.995f, 0.995f, -0.995f, 0.995f, kVigColour), s.sx, s.sy, false, rule);
|
|
Match("an overlay 0.88 of the band", at(-0.88f, 0.88f, -0.88f, 0.88f, kVigColour), s.sx, s.sy, false, rule);
|
|
Match("vignette width, half the height", at(-kVigX, kVigX, -0.5f, 0.5f, kVigColour), s.sx, s.sy, false, rule);
|
|
Match("vignette height, the whole band's width", at(-1.0f, 1.0f, -kVigY, kVigY, kVigColour), s.sx, s.sy, false, rule);
|
|
Match("fade", at(-1.29f, 1.29f, -1.30f, 1.30f, 0xFF000000), s.sx, s.sy, false, rule);
|
|
Match("vid cam noise overlay", at(-0.692f, 0.709f, -1.058f, 1.050f, 0x3C727272), s.sx, s.sy, false, rule);
|
|
Match("vid cam dark overlay", at(-0.712f, 0.668f, -0.893f, 0.908f, 0x3C000000), s.sx, s.sy, false, rule);
|
|
Match("pause dim centre", at(-0.53f, 0.57f, -1.91f, 1.91f, 0xC8000000), s.sx, s.sy, false, rule);
|
|
Match("cam dialog box", at(-0.578f, 0.586f, -0.167f, 0.569f, 0xFD000000), s.sx, s.sy, false, rule);
|
|
Match("vignette off-centre by 0.03 in x", at(-kVigX + 0.03f, kVigX + 0.03f, -kVigY, kVigY, kVigColour), s.sx, s.sy, false, rule);
|
|
Match("vignette off-centre by 0.03 in y", at(-kVigX, kVigX, -kVigY + 0.03f, kVigY + 0.03f, kVigColour), s.sx, s.sy, false, rule);
|
|
Quad grad = at(-kVigX, kVigX, -kVigY, kVigY, kVigColour);
|
|
grad.v[1].argb = grad.v[2].argb = 0x007F7F7F; // a gradient, not one colour
|
|
Match("a gradient of the vignette's shape", grad, s.sx, s.sy, false, rule);
|
|
Quad bent = at(-kVigX, kVigX, -kVigY, kVigY, kVigColour);
|
|
bent.v[2].x -= 0.05f;
|
|
Match("a bent quad", bent, s.sx, s.sy, false, rule);
|
|
}
|
|
return g_failures - before;
|
|
}
|
|
|
|
static void Planted(const char* what, const VignetteRule& wrong)
|
|
{
|
|
const int saved = g_failures;
|
|
g_quiet = true;
|
|
const int failed = VignetteCases(wrong);
|
|
g_quiet = false;
|
|
g_failures = saved;
|
|
printf("planted %s: %d case(s) failed\n", what, failed);
|
|
CHECK(failed > 0, "planted %s passed every vignette case", what);
|
|
}
|
|
|
|
// ---- Texel addressing ----
|
|
|
|
static void TexelTests()
|
|
{
|
|
// Reference offsets: gpu/renderer.cpp's GLSL tiled2D, as a separate
|
|
// Python port computed them (that port untiled the vignette's texture
|
|
// dumped from guest memory into the clear-middle picture).
|
|
struct Ref
|
|
{
|
|
uint32_t x, y, pitch, offset;
|
|
};
|
|
const Ref refs[] = { { 0, 0, 256, 0x0u }, { 1, 0, 256, 0x4u }, { 0, 1, 256, 0x10u }, { 7, 0, 256, 0x2Cu }, { 8, 0, 256, 0x40u },
|
|
{ 0, 8, 256, 0x480u }, { 0, 16, 256, 0x800u }, { 31, 31, 256, 0xF7Cu }, { 32, 0, 256, 0x1000u }, { 0, 32, 256, 0x8000u },
|
|
{ 128, 128, 256, 0x24000u }, { 255, 255, 256, 0x3FF7Cu }, { 5, 3, 256, 0x134u }, { 100, 200, 256, 0x334A0u },
|
|
{ 13, 77, 128, 0x86F4u } };
|
|
for (const Ref& r : refs)
|
|
CHECK(TexelOffset32(r.x, r.y, r.pitch, true) == r.offset, "tiled (%u,%u) pitch %u: %X, want %X", r.x, r.y, r.pitch,
|
|
TexelOffset32(r.x, r.y, r.pitch, true), r.offset);
|
|
CHECK(TexelOffset32(5, 3, 256, false) == (3u * 256u + 5u) * 4u, "linear (5,3) pitch 256");
|
|
// A 256x256 tiled texture: every texel at its own word, all of them
|
|
// within the 256 KB.
|
|
std::vector<uint8_t> seen(256 * 256, 0);
|
|
int bad = 0;
|
|
for (uint32_t y = 0; y < 256; y++)
|
|
for (uint32_t x = 0; x < 256; x++)
|
|
{
|
|
const uint32_t o = TexelOffset32(x, y, 256, true);
|
|
if (o % 4 || o / 4 >= seen.size() || seen[o / 4]++)
|
|
bad++;
|
|
}
|
|
CHECK(bad == 0, "tiled 256x256: %d texel(s) outside, unaligned or on another's word", bad);
|
|
printf("texel addressing: %zu reference offsets, 65536 texels each on its own word\n", std::size(refs));
|
|
}
|
|
|
|
// ---- The plan on the draw as the renderer sees it ----
|
|
|
|
// The front end's matrix at a shape (edge_bars_test's): c0 0.69 x scaleX,
|
|
// c5 0.92 x scaleY, the rest 0.
|
|
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 } };
|
|
}
|
|
|
|
struct EPolys
|
|
{
|
|
uint32_t w[4][4];
|
|
};
|
|
static EPolys Polys(const Quad& q, const Matrix& m)
|
|
{
|
|
EPolys e{};
|
|
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(e.w[k], p, sizeof(p));
|
|
e.w[k][3] = q.v[k].argb;
|
|
}
|
|
return e;
|
|
}
|
|
|
|
// 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, as
|
|
// edge_bars_test's; `ox`, `oy` (D3D's half-pixel centres: 640.5, 360.5) and
|
|
// `vy` (flipped: +360) move it. The screen is the target's top 720 rows.
|
|
static void FrontEndPs(float ps[4], float ox = 640.0f, float oy = 360.0f, float vy = -360.0f)
|
|
{
|
|
ps[0] = 2.0f * 640.0f / 1280.0f;
|
|
ps[1] = 2.0f * vy / 2048.0f;
|
|
ps[2] = 2.0f * ox / 1280.0f - 1.0f;
|
|
ps[3] = 2.0f * oy / 2048.0f - 1.0f;
|
|
}
|
|
|
|
// The vignette's draw as dumped: blend 07060706, t0 256x256 format 6.
|
|
static Draw ShowcaseDraw(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 = 256,
|
|
.t0Height = 256,
|
|
.t0Format = 6,
|
|
.posScale = { ps[0], ps[1], ps[2], ps[3] },
|
|
.hostW = hostW,
|
|
.hostH = hostH,
|
|
.scissor = { 0, 0, int32_t(hostW), int32_t(hostH) } };
|
|
}
|
|
|
|
// A 256x256 tiled k_8_8_8_8 texture in guest byte order: the vignette's
|
|
// (black, alpha 0 inside an ellipse, rising to the edges; its dumped texture
|
|
// is all 00000000 in the middle) or a picture's (opaque everywhere).
|
|
struct Texture
|
|
{
|
|
uint32_t w = 256, h = 256;
|
|
std::vector<uint32_t> words = std::vector<uint32_t>(256 * 256, 0);
|
|
void Set(uint32_t x, uint32_t y, uint32_t v) { words[TexelOffset32(x, y, w, true) / 4] = v; }
|
|
uint32_t Get(uint32_t x, uint32_t y) const { return words[TexelOffset32(x, y, w, true) / 4]; }
|
|
};
|
|
static Texture VignetteTexture()
|
|
{
|
|
Texture t;
|
|
for (uint32_t y = 0; y < t.h; y++)
|
|
for (uint32_t x = 0; x < t.w; x++)
|
|
{
|
|
const float u = (float(x) + 0.5f) / t.w * 2.0f - 1.0f, v = (float(y) + 0.5f) / t.h * 2.0f - 1.0f;
|
|
const float r = std::sqrt(u * u * 1.1f + v * v);
|
|
const uint32_t a = r < 0.8f ? 0u : uint32_t(std::min(255.0f, (r - 0.8f) * 600.0f));
|
|
t.Set(x, y, a); // A in byte 0 as stored (big-endian ARGB word), RGB 0
|
|
}
|
|
return t;
|
|
}
|
|
static Texture PictureTexture()
|
|
{
|
|
Texture t;
|
|
for (uint32_t y = 0; y < t.h; y++)
|
|
for (uint32_t x = 0; x < t.w; x++)
|
|
t.Set(x, y, 0xFF000000u | (x << 16) | (y << 8) | 0x40u);
|
|
return t;
|
|
}
|
|
|
|
static Plan RunPlan(const Draw& d, const EPolys& e, const Texture& t, uint32_t vertexLimit = 4, bool texelsReadable = true)
|
|
{
|
|
return PlanVignette(
|
|
d,
|
|
[&](uint32_t k, uint32_t j, uint32_t& word) {
|
|
word = e.w[k & 3][j & 3];
|
|
return k < vertexLimit;
|
|
},
|
|
[&](uint32_t x, uint32_t y, uint32_t& word) {
|
|
if (!texelsReadable || x >= t.w || y >= t.h)
|
|
return false;
|
|
word = t.Get(x, y);
|
|
return true;
|
|
});
|
|
}
|
|
|
|
static const char* Name(Fill f)
|
|
{
|
|
return f == Fill::Vignette ? "vignette" : f == Fill::None ? "none" : "another fill";
|
|
}
|
|
|
|
// The stretched quad against pixel centres: every host pixel of the screen
|
|
// (the target's top `screenH` rows) has its centre inside the quad as the
|
|
// new posScale draws it, and the box lands at +-kFillEdge of the viewport
|
|
// (clip space through the new posScale = the old one's at +-kFillEdge).
|
|
static void CoverOracle(const char* what, const Plan& p, const float ps[4], float hostW, float hostH, float screenH)
|
|
{
|
|
const float* q = p.posScale;
|
|
// The box's corners through the new posScale, and clip +-kFillEdge
|
|
// through the draw's own: the same NDC.
|
|
const float nx0 = p.box.x0 * q[0] + q[2], nx1 = p.box.x1 * q[0] + q[2];
|
|
const float ny0 = p.box.y0 * q[1] + q[3], ny1 = p.box.y1 * q[1] + q[3];
|
|
const float ex0 = -kFillEdge * ps[0] + ps[2], ex1 = kFillEdge * ps[0] + ps[2];
|
|
const float ey0 = -kFillEdge * ps[1] + ps[3], ey1 = kFillEdge * ps[1] + ps[3];
|
|
CHECK(std::fabs(nx0 - ex0) < 1e-4f && std::fabs(nx1 - ex1) < 1e-4f && std::fabs(ny0 - ey0) < 1e-4f && std::fabs(ny1 - ey1) < 1e-4f,
|
|
"%s: the box lands at NDC x %.5f..%.5f y %.5f..%.5f, want x %.5f..%.5f y %.5f..%.5f", what, nx0, nx1, ny0, ny1, ex0, ex1, ey0, ey1);
|
|
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);
|
|
}
|
|
CHECK(outside == 0, "%s: %d host pixel(s) left outside the stretched vignette", what, outside);
|
|
}
|
|
|
|
static void PlanTests()
|
|
{
|
|
const Texture vignette = VignetteTexture(), picture = PictureTexture();
|
|
struct Host
|
|
{
|
|
const char* name;
|
|
float scale, ox, oy;
|
|
};
|
|
// 1x, and 2x with D3D's half-pixel centres (as edge_bars_test's: the
|
|
// viewport offset 640.5, 360.5).
|
|
const Host hosts[] = { { "1x", 1.0f, 640.0f, 360.0f }, { "2x half-pixel", 2.0f, 640.5f, 360.5f } };
|
|
int planned = 0;
|
|
for (const Shape& s : kShapes)
|
|
for (const Host& h : hosts)
|
|
for (int flipped = 0; flipped < 2; flipped++)
|
|
{
|
|
float ps[4];
|
|
// Flipped: the viewport's y scale positive, its offset the
|
|
// band's far edge (the screen rows stay 0..720).
|
|
FrontEndPs(ps, h.ox, flipped ? 360.0f - (h.oy - 360.0f) : h.oy, flipped ? 360.0f : -360.0f);
|
|
const float hostW = 1280.0f * h.scale, hostH = 2048.0f * h.scale, screenH = 720.0f * h.scale;
|
|
const Matrix m = FrontEndMatrix(s.sx, s.sy);
|
|
const Quad q = Rectangle(-kVigX, kVigX, -kVigY, kVigY, s.sx, s.sy, kVigColour);
|
|
const EPolys e = Polys(q, m);
|
|
const Draw d = ShowcaseDraw(m, s.sx, s.sy, ps, hostW, hostH);
|
|
const Plan p = RunPlan(d, e, vignette);
|
|
char what[160];
|
|
snprintf(what, sizeof(what), "%s, %s%s", s.name, h.name, flipped ? ", y flipped" : "");
|
|
CHECK(p.fill == Fill::Vignette, "%s: %s, want vignette", what, Name(p.fill));
|
|
if (p.fill != Fill::Vignette)
|
|
continue;
|
|
CHECK(p.rectCount == 0, "%s: %u rects, want none (the draw itself is stretched)", what, p.rectCount);
|
|
CHECK(p.argb == kVigColour, "%s: colour %08X", what, p.argb);
|
|
float want[4];
|
|
CoverFrame(p.box, d.posScale, want);
|
|
CHECK(std::memcmp(want, p.posScale, sizeof(want)) == 0, "%s: posScale %.5f %.5f %.5f %.5f, want CoverFrame's", what, p.posScale[0],
|
|
p.posScale[1], p.posScale[2], p.posScale[3]);
|
|
CoverOracle(what, p, ps, hostW, hostH, screenH);
|
|
planned++;
|
|
}
|
|
printf("plan: the Showcase's draw at %zu shapes x 1x/2x x y-flipped: %d stretched, every host pixel covered\n", std::size(kShapes), planned);
|
|
// Planted: the oracle catches a vignette left as the game draws it, and
|
|
// one stretched only to clip +-1 at 2x (D3D's half-pixel centres: the
|
|
// outermost column's centre lies past it).
|
|
{
|
|
const Shape deck = kShapes[1];
|
|
float ps[4];
|
|
FrontEndPs(ps, 640.5f, 360.5f);
|
|
const Matrix m = FrontEndMatrix(deck.sx, deck.sy);
|
|
const Draw d = ShowcaseDraw(m, deck.sx, deck.sy, ps, 2560.0f, 4096.0f);
|
|
const Plan good = RunPlan(d, Polys(Rectangle(-kVigX, kVigX, -kVigY, kVigY, deck.sx, deck.sy, kVigColour), m), vignette);
|
|
Plan unstretched = good, toOne = good;
|
|
std::memcpy(unstretched.posScale, d.posScale, sizeof(unstretched.posScale));
|
|
CoverFrame(good.box, d.posScale, toOne.posScale, 1.0f);
|
|
for (const auto& [what, plan] : { std::pair{ "left as the game draws it", unstretched }, std::pair{ "stretched to clip +-1 only", toOne } })
|
|
{
|
|
const int saved = g_failures;
|
|
g_quiet = true;
|
|
CoverOracle(what, plan, ps, 2560.0f, 4096.0f, 1440.0f);
|
|
g_quiet = false;
|
|
const int failed = g_failures - saved;
|
|
g_failures = saved;
|
|
printf("planted oracle '%s' (Deck, 2x): %d check(s) failed\n", what, failed);
|
|
CHECK(failed > 0, "planted oracle '%s' passed", what);
|
|
}
|
|
}
|
|
|
|
// A ppc/ without the Vert+ hooks (front-end y scale 0) at 16:9.
|
|
{
|
|
float ps[4];
|
|
FrontEndPs(ps);
|
|
const Matrix m = FrontEndMatrix(1.0f, 1.0f);
|
|
Draw d = ShowcaseDraw(m, 1.0f, 1.0f, ps, 1280.0f, 2048.0f);
|
|
d.frontEndY = 0.0f;
|
|
const Plan p = RunPlan(d, Polys(Rectangle(-kVigX, kVigX, -kVigY, kVigY, 1.0f, 1.0f, kVigColour), m), vignette);
|
|
CHECK(p.fill == Fill::Vignette, "16:9 with an old ppc/: %s", Name(p.fill));
|
|
}
|
|
|
|
// What must be left as it is: each breaks one condition of the
|
|
// vignette's draw (planted register and texel faults).
|
|
float ps[4];
|
|
FrontEndPs(ps);
|
|
const Shape deck = kShapes[1];
|
|
const Matrix m = FrontEndMatrix(deck.sx, deck.sy);
|
|
const EPolys e = Polys(Rectangle(-kVigX, kVigX, -kVigY, kVigY, deck.sx, deck.sy, kVigColour), m);
|
|
const Draw base = ShowcaseDraw(m, deck.sx, deck.sy, ps, 1280.0f, 2048.0f);
|
|
struct Case
|
|
{
|
|
const char* what;
|
|
void (*change)(Draw&);
|
|
};
|
|
const Case refuse[] = {
|
|
{ "opaque, no blending (00010001)", [](Draw& d) { d.blend0 = 0x00010001; } },
|
|
{ "additive blending", [](Draw& d) { d.blend0 = 0x07010701; } },
|
|
{ "the 32x32 white texture", [](Draw& d) { d.t0Width = d.t0Height = 32; } },
|
|
{ "a 64x256 texture", [](Draw& d) { d.t0Width = 64; } },
|
|
{ "a DXT1 texture (format 18)", [](Draw& d) { d.t0Format = 18; } },
|
|
{ "no texture sampled", [](Draw& d) { d.t0Sampled = false; } },
|
|
{ "inside an occlusion query", [](Draw& d) { d.occlusion = true; } },
|
|
{ "widened, covered or clipped by the other rules", [](Draw& d) { d.leftAlone = false; } },
|
|
{ "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; } },
|
|
{ "no viewport transform", [](Draw& d) { d.vte = 0x300; } },
|
|
{ "c5 not the front end's y scale", [](Draw& d) { d.frontEndY = 0.5f; } },
|
|
};
|
|
for (const Case& c : refuse)
|
|
{
|
|
Draw d = base;
|
|
c.change(d);
|
|
const Plan p = RunPlan(d, e, vignette);
|
|
CHECK(p.fill == Fill::None, "%s: %s, want none", c.what, Name(p.fill));
|
|
}
|
|
// A translation in the matrix (c3 / c7).
|
|
{
|
|
Matrix t = m;
|
|
t.c[3] = 0.1f;
|
|
const Plan p = RunPlan(ShowcaseDraw(t, deck.sx, deck.sy, ps, 1280.0f, 2048.0f), e, vignette);
|
|
CHECK(p.fill == Fill::None, "x translation: %s, want none", Name(p.fill));
|
|
}
|
|
// The texture: an opaque picture of the same shape, a vignette with any
|
|
// one of its middle samples dark, an unreadable texture, a vertex past
|
|
// the buffer.
|
|
CHECK(RunPlan(base, e, picture).fill == Fill::None, "an opaque picture of the vignette's shape was stretched");
|
|
for (uint32_t i = 0; i < kClearSamples; i++)
|
|
{
|
|
Texture t = VignetteTexture();
|
|
uint32_t x, y;
|
|
ClearSample(i, t.w, t.h, x, y);
|
|
CHECK(t.Get(x, y) == 0, "the test vignette is clear at sample %u", i);
|
|
t.Set(x, y, 0x01000000u);
|
|
CHECK(RunPlan(base, e, t).fill == Fill::None, "middle sample %u (%u,%u) not clear: still stretched", i, x, y);
|
|
}
|
|
CHECK(RunPlan(base, e, vignette, 4, false).fill == Fill::None, "unreadable texels: stretched anyway");
|
|
CHECK(RunPlan(base, e, vignette, 2).fill == Fill::None, "a vertex past the buffer: stretched anyway");
|
|
// And it is the samples that decide: a vignette whose middle is clear
|
|
// only between the samples is refused, one clear only at them is taken.
|
|
{
|
|
Texture t = PictureTexture();
|
|
for (uint32_t i = 0; i < kClearSamples; i++)
|
|
{
|
|
uint32_t x, y;
|
|
ClearSample(i, t.w, t.h, x, y);
|
|
t.Set(x, y, 0);
|
|
}
|
|
CHECK(RunPlan(base, e, t).fill == Fill::Vignette, "the five samples clear: not taken");
|
|
}
|
|
printf("plan: %zu register faults, an opaque picture, %u dark samples, unreadable texels and a short vertex buffer refused\n",
|
|
std::size(refuse) + 1, kClearSamples);
|
|
}
|
|
|
|
// ---- The Blacklist bio's panel (a black frame, PlanFill) ----
|
|
|
|
// As logged on the Deck (NFSMW_LOG_FRONTEND, the bio pushed from the main
|
|
// menu by a test-only hook): "black frame x -0.976..0.976 y -0.887..0.887
|
|
// colour FB000000 blend 07060706 t0 1AD6E000 32x32", fading in from
|
|
// 11000000 over ~15 frames; the picture inset on it (x +-0.970, y +-0.920,
|
|
// blend 01060106) and the text are drawn after it.
|
|
constexpr float kBioX = 0.976f, kBioY = 0.985f;
|
|
|
|
static Draw PanelDraw(const Matrix& m, float sx, float sy, const float ps[4], float hostW, float hostH)
|
|
{
|
|
Draw d = ShowcaseDraw(m, sx, sy, ps, hostW, hostH);
|
|
d.t0Width = d.t0Height = 32; // the game's white texture
|
|
d.t0Format = 20;
|
|
return d;
|
|
}
|
|
|
|
static Plan RunPanel(const Draw& d, const EPolys& e)
|
|
{
|
|
return PlanFill(d, [&](uint32_t k, uint32_t j, uint32_t& word) {
|
|
word = e.w[k & 3][j & 3];
|
|
return k < 4;
|
|
});
|
|
}
|
|
|
|
// The panel's fill against pixel centres, over the screen (the target's top
|
|
// `screenH` rows): each pixel is in exactly one of the panel (its centre
|
|
// inside the box) and the fill rectangles; ties within 1/256 pixel skipped.
|
|
// Returns the wrong pixels.
|
|
static int PanelOracle(const Box& b, const Rect* r, uint32_t n, const float ps[4], float hostW, float hostH, float screenH)
|
|
{
|
|
const float pxX = std::fabs(ps[0]) * hostW * 0.5f, pxY = std::fabs(ps[1]) * hostH * 0.5f;
|
|
auto tie = [](float d, float px) { return std::fabs(d) * px < 1.0f / 256.0f; };
|
|
int wrong = 0;
|
|
for (int32_t y = 0; y < int32_t(screenH); y++)
|
|
{
|
|
const float cy = ((float(y) + 0.5f) / hostH * 2.0f - 1.0f - ps[3]) / ps[1];
|
|
for (int32_t x = 0; x < int32_t(hostW); x++)
|
|
{
|
|
const float cx = ((float(x) + 0.5f) / hostW * 2.0f - 1.0f - ps[2]) / ps[0];
|
|
if (tie(cx - b.x0, pxX) || tie(cx - b.x1, pxX) || tie(cy - b.y0, pxY) || tie(cy - b.y1, pxY))
|
|
continue;
|
|
int in = cx > b.x0 && cx < b.x1 && cy > b.y0 && cy < b.y1;
|
|
for (uint32_t i = 0; i < n; i++)
|
|
in += x >= r[i].x0 && x < r[i].x1 && y >= r[i].y0 && y < r[i].y1;
|
|
wrong += in != 1;
|
|
}
|
|
}
|
|
return wrong;
|
|
}
|
|
|
|
// v0.1.1-v0.1.5's fill (a planted fault): only past the band.
|
|
static uint32_t PastBandOnly(const float ps[4], float hostW, float hostH, const Rect& scissor, float scaleX, float scaleY, Rect out[4])
|
|
{
|
|
uint32_t n = 0;
|
|
auto add = [&](const Rect& r) {
|
|
const Rect c = Intersect(r, scissor);
|
|
if (!Empty(c))
|
|
out[n++] = c;
|
|
};
|
|
const float e = kFillEdge;
|
|
if (scaleY < 1.0f)
|
|
{
|
|
add(ClipToHost(-e, e, scaleY, e, ps, hostW, hostH));
|
|
add(ClipToHost(-e, e, -e, -scaleY, ps, hostW, hostH));
|
|
}
|
|
else if (scaleX < 1.0f)
|
|
{
|
|
add(ClipToHost(scaleX, e, -e, e, ps, hostW, hostH));
|
|
add(ClipToHost(-e, -scaleX, -e, e, ps, hostW, hostH));
|
|
}
|
|
return n;
|
|
}
|
|
|
|
static void BioTests()
|
|
{
|
|
struct Host
|
|
{
|
|
const char* name;
|
|
float scale, ox, oy;
|
|
};
|
|
const Host hosts[] = { { "1x", 1.0f, 640.0f, 360.0f }, { "2x half-pixel", 2.0f, 640.5f, 360.5f } };
|
|
int planned = 0, oldFails = 0, cases = 0;
|
|
for (const Shape& s : kShapes)
|
|
for (const Host& h : hosts)
|
|
for (int flipped = 0; flipped < 2; flipped++)
|
|
{
|
|
float ps[4];
|
|
FrontEndPs(ps, h.ox, flipped ? 360.0f - (h.oy - 360.0f) : h.oy, flipped ? 360.0f : -360.0f);
|
|
const float hostW = 1280.0f * h.scale, hostH = 2048.0f * h.scale, screenH = 720.0f * h.scale;
|
|
const Matrix m = FrontEndMatrix(s.sx, s.sy);
|
|
const EPolys e = Polys(Rectangle(-kBioX, kBioX, -kBioY, kBioY, s.sx, s.sy, 0xFF000000), m);
|
|
const Draw d = PanelDraw(m, s.sx, s.sy, ps, hostW, hostH);
|
|
const Plan p = RunPanel(d, e);
|
|
char what[160];
|
|
snprintf(what, sizeof(what), "bio panel, %s, %s%s", s.name, h.name, flipped ? ", y flipped" : "");
|
|
CHECK(p.fill == Fill::Blackout, "%s: %s, want a black frame", what, Name(p.fill));
|
|
if (p.fill != Fill::Blackout)
|
|
continue;
|
|
float want[4];
|
|
CoverFrame(p.box, d.posScale, want);
|
|
CHECK(std::memcmp(want, p.posScale, sizeof(want)) == 0, "%s: the copies' posScale is not CoverFrame's", what);
|
|
const int wrong = PanelOracle(p.box, p.rects, p.rectCount, ps, hostW, hostH, screenH);
|
|
CHECK(wrong == 0, "%s: %d pixel(s) in both or neither of the panel and its fill", what, wrong);
|
|
Rect old[4]{};
|
|
const uint32_t n = PastBandOnly(ps, hostW, hostH, d.scissor, s.sx, s.sy, old);
|
|
oldFails += PanelOracle(p.box, old, n, ps, hostW, hostH, screenH) > 0;
|
|
cases++;
|
|
planned++;
|
|
}
|
|
printf("bio panel: %d plans at %zu shapes x 1x/2x x y-flipped, the panel and its fill split every pixel once\n", planned, std::size(kShapes));
|
|
printf("planted bio panel fill 'past the band only (v0.1.5)': %d of %d cases fail\n", oldFails, cases);
|
|
CHECK(oldFails == cases && cases > 0, "planted 'past the band only' passed %d of %d cases", cases - oldFails, cases);
|
|
// Fading in (11000000): below the black frame's alpha, left alone (as
|
|
// before); over a picture (a large t0), never a black frame, and not a
|
|
// vignette (opaque in the middle).
|
|
float ps[4];
|
|
FrontEndPs(ps);
|
|
const Matrix m = FrontEndMatrix(1.0f, 0.9f);
|
|
const Draw d = PanelDraw(m, 1.0f, 0.9f, ps, 1280.0f, 2048.0f);
|
|
CHECK(RunPanel(d, Polys(Rectangle(-kBioX, kBioX, -kBioY, kBioY, 1.0f, 0.9f, 0x11000000), m)).fill == Fill::None, "the panel fading in was filled");
|
|
const EPolys picture = Polys(Rectangle(-kBioX, kBioX, -kBioY, kBioY, 1.0f, 0.9f, 0xFF000000), m);
|
|
Draw big = d;
|
|
big.t0Width = big.t0Height = 256;
|
|
big.t0Format = 6;
|
|
CHECK(RunPanel(big, picture).fill == Fill::None, "a black quad over a large picture taken for a black frame");
|
|
CHECK(RunPlan(big, picture, PictureTexture()).fill == Fill::None, "a picture of the panel's shape taken for a vignette");
|
|
}
|
|
|
|
// ---- 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;
|
|
}
|
|
|
|
// The renderer's wiring; returns the failures (quiet: for the planted copies).
|
|
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: bars and black frames, then the backdrop, then the
|
|
// vignette, each only when nothing earlier took the draw.
|
|
const size_t fill = all.find("gpu::bars::Planplan=gpu::bars::PlanFill(draw,read);");
|
|
const size_t backdrop = all.find("if(plan.fill==gpu::bars::Fill::None)plan=gpu::bars::PlanBackdrop(draw,read);");
|
|
const size_t vignette = all.find("plan=gpu::bars::PlanVignette(draw,read,texel);");
|
|
CHECK(fill != std::string::npos && backdrop != std::string::npos && vignette != std::string::npos && fill < backdrop &&
|
|
backdrop < vignette,
|
|
"planEdgeFills asks PlanFill, then PlanBackdrop, then PlanVignette");
|
|
// Both paths (wide or tall screens, and 16:9) ask it: the black frame's
|
|
// fill and the vignette at every shape.
|
|
CHECK(Count(all, "barPlan=planEdgeFills(c,") == 2, "planEdgeFills asked on both paths: %zu", Count(all, "barPlan=planEdgeFills(c,"));
|
|
CHECK(vignette != std::string::npos && all.rfind("if(plan.fill==gpu::bars::Fill::None){", vignette) != std::string::npos &&
|
|
vignette - all.rfind("if(plan.fill==gpu::bars::Fill::None){", vignette) < 600,
|
|
"PlanVignette only for a draw nothing else took");
|
|
// Its texture as the GPU reads it.
|
|
CHECK(all.find(".t0Format=uint32_t(t0.format),") != std::string::npos, "the draw's t0 format is handed over");
|
|
CHECK(all.find("word=GpuSwap(LoadPhysical(t0Base+gpu::bars::TexelOffset32(x,y,t0Pitch,t0.tiled!=0)),uint32_t(t0.endianness));") !=
|
|
std::string::npos,
|
|
"texels read through TexelOffset32 at t0's base, pitch, tiling and endian");
|
|
// The plan is applied to the draw itself on both paths (wide or tall
|
|
// screens, and 16:9), before its constants are pushed.
|
|
const std::string apply = "if(barPlan.fill==gpu::bars::Fill::Vignette)memcpy(pc.posScale,barPlan.posScale,sizeof(pc.posScale));";
|
|
CHECK(Count(all, apply) == 2, "the vignette'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");
|
|
// Both behind the A/B switch, as every edge fill.
|
|
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 vignette wiring");
|
|
// Planted: copies with the wiring cut, each must fail.
|
|
struct Cut
|
|
{
|
|
const char* what;
|
|
const char* from;
|
|
const char* to;
|
|
bool all;
|
|
};
|
|
const Cut cuts[] = {
|
|
{ "PlanVignette never asked", "plan = gpu::bars::PlanVignette(draw, read, texel);", "(void)texel;", false },
|
|
{ "PlanFill only on wide or tall screens (v0.1.5)", "gpu::bars::Plan plan = gpu::bars::PlanFill(draw, read);",
|
|
"gpu::bars::Plan plan = aspectX < 1.0f || aspectY < 1.0f ? gpu::bars::PlanFill(draw, read) : gpu::bars::Plan{};", false },
|
|
{ "the 16:9 path never plans", "barPlan = planEdgeFills(c, game::FrontEndScaleY(), s, sv, feShader, true);", ";", false },
|
|
{ "the 16:9 path doesn't apply it", " if (barPlan.fill == gpu::bars::Fill::Vignette)\n memcpy(pc.posScale, barPlan.posScale, sizeof(pc.posScale));\n }\n }\n }", " }\n }\n }", false },
|
|
{ "no posScale applied at all", "memcpy(pc.posScale, barPlan.posScale, sizeof(pc.posScale));", ";", true },
|
|
{ "texels read linear", "t0.tiled != 0", "false", false },
|
|
{ "texels read without the endian", "uint32_t(t0.endianness)", "0u", false },
|
|
};
|
|
for (const Cut& c : cuts)
|
|
{
|
|
std::string copy = source;
|
|
size_t at = copy.find(c.from), n = 0;
|
|
while (at != std::string::npos)
|
|
{
|
|
copy.replace(at, std::strlen(c.from), c.to);
|
|
n++;
|
|
if (!c.all)
|
|
break;
|
|
at = copy.find(c.from, at + std::strlen(c.to));
|
|
}
|
|
CHECK(n > 0, "planted '%s': its text is not in renderer.cpp", c.what);
|
|
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)
|
|
{
|
|
printf("menu borders (GitHub #19): the Showcase's vignette\n");
|
|
VignetteCases(VignetteRule{});
|
|
printf("shape cases at %zu screen shapes checked\n", std::size(kShapes));
|
|
printf("\nplanted, shape (each must fail a case):\n");
|
|
auto plant = [](const char* what, auto set) { VignetteRule r; set(r); Planted(what, r); };
|
|
plant("centreMax 0.05", [](VignetteRule& r) { r.centreMax = 0.05f; });
|
|
plant("extentMin 0.96", [](VignetteRule& r) { r.extentMin = 0.96f; });
|
|
plant("extentMin 0.85", [](VignetteRule& r) { r.extentMin = 0.85f; });
|
|
plant("extentMax 0.96", [](VignetteRule& r) { r.extentMax = 0.96f; });
|
|
plant("extentMax 1.0", [](VignetteRule& r) { r.extentMax = 1.0f; });
|
|
plant("cornerTolerance 0.1", [](VignetteRule& r) { r.cornerTolerance = 0.1f; });
|
|
printf("\n");
|
|
TexelTests();
|
|
printf("\n");
|
|
PlanTests();
|
|
printf("\n");
|
|
BioTests();
|
|
printf("\n");
|
|
if (argc > 1)
|
|
RendererTests(argv[1]);
|
|
else
|
|
printf("(no renderer.cpp given: its checks skipped)\n");
|
|
if (g_failures)
|
|
{
|
|
printf("menu_borders_test: %d failure(s)\n", g_failures);
|
|
return 1;
|
|
}
|
|
printf("menu_borders_test: all passed\n");
|
|
return 0;
|
|
}
|