Files
2026-10-17 04:39:51 +00:00

1552 lines
93 KiB
C++

// Tests for gpu::bars (runtime/gpu/edge_bars.h): which front-end quads are
// the police vid-cam's side bars or the intro's black frame, what they fill
// (a bar past the 16:9 band, a black frame everything outside it at every
// shape: GitHub #19), the stretched copy's posScale, the band clip's rows,
// and the whole plan the renderer asks for (PlanFill) on the draws as
// dumped. From the repo root:
// clang++ -std=c++20 -O2 -UNDEBUG -Iruntime tests/edge_bars_test.cpp -o build/edge_bars_test
// build/edge_bars_test
// The match cases run again under deliberately wrong thresholds (Planted):
// each must fail at least one case, so a threshold nobody tests can't drift.
// The fill, cover, colour, band-row and plan code was checked the same way,
// by planting wrong code in a copy of the header (each mutation fails a
// check).
#include <algorithm>
#include <cmath>
#include <cstdint>
#include <cstdio>
#include <cstring>
#include <cstdlib>
#include <iterator>
#include <utility>
// On Windows gpu/renderer.cpp includes the header after <windows.h>, whose
// macros (WIN32_LEAN_AND_MEAN and NOMINMAX as platform/win32/posix.h sets
// them) turn some words into nothing or a number: "auto near = ..." was a
// Windows-only compile break. The header compiles here under the same ones.
#define near
#define far
#define pascal
#define cdecl
#define IN
#define OUT
#define OPTIONAL
#define CONST const
#define TRUE 1
#define FALSE 0
#define DELETE (0x00010000L)
#define ERROR 0
#define OPAQUE 2
#define TRANSPARENT 1
#define ABSOLUTE 1
#define RELATIVE 2
#define DIFFERENCE 11
#include <gpu/edge_bars.h>
#undef near
#undef far
#undef pascal
#undef cdecl
#undef IN
#undef OUT
#undef OPTIONAL
#undef CONST
#undef TRUE
#undef FALSE
#undef DELETE
#undef ERROR
#undef OPAQUE
#undef TRANSPARENT
#undef ABSOLUTE
#undef RELATIVE
#undef DIFFERENCE
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 picture_fit gives them.
struct Shape
{
const char* name;
float sx, sy;
};
static const Shape kShapes[] = {
{ "1280x800 (Deck)", 1.0f, 0.9f },
{ "1200x800 (3:2)", 1.0f, 0.84375f },
{ "1024x768 (4:3)", 1.0f, 0.75f },
{ "2732x2048 (iPad Pro)", 1.0f, 0.750366f },
{ "1280x720 (16:9)", 1.0f, 1.0f },
{ "2560x1080 (21:9)", 0.75f, 1.0f },
{ "3440x1440", 0.744186f, 1.0f },
{ "2880x810 (32:9)", 0.5f, 1.0f },
};
// A rectangle from band units at a shape, vertices in order around it
// (the game's: (x0,y0) (x1,y0) (x1,y1) (x0,y1)).
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 } } };
}
static const char* Name(Side s)
{
return s == Side::Right ? "right" : s == Side::Left ? "left" : "none";
}
static void Match(const char* what, const Quad& q, float sx, float sy, Side want, const Rule& rule)
{
Box box{};
const Side got = MatchSideBar(q.v, sx, sy, rule, &box);
CHECK(got == want, "%s at x %.4f y %.4f: %s, want %s", what, sx, sy, Name(got), Name(want));
}
// Every match case. Returns the failures it added (for Planted).
static int MatchCases(const Rule& rule)
{
const int before = g_failures;
constexpr uint32_t kBar = 0xFD000000;
for (const Shape& s : kShapes)
{
// The two side bars, as measured at every shape (band units).
Match("right bar", Rectangle(0.651f, 1.406f, -1.024f, 1.039f, s.sx, s.sy, kBar), s.sx, s.sy, Side::Right, rule);
Match("left bar", Rectangle(-1.410f, -0.655f, -1.027f, 1.035f, s.sx, s.sy, kBar), s.sx, s.sy, Side::Left, rule);
// The bezel quadrants: textured grey, one band edge.
Match("bezel B", Rectangle(-0.001f, 0.654f, -1.027f, 0.003f, s.sx, s.sy, 0xFB7D7D7D), s.sx, s.sy, Side::None, rule);
Match("bezel T", Rectangle(-0.657f, -0.001f, 0.003f, 1.033f, s.sx, s.sy, 0xFB7D7D7D), s.sx, s.sy, Side::None, rule);
// ... and the same shape in the bar's black: still one edge only.
Match("black bezel shape", Rectangle(-0.001f, 0.654f, 0.003f, 1.033f, s.sx, s.sy, kBar), s.sx, s.sy, Side::None, rule);
// The rest of the vid cam: noise, dark overlay, rolling bar.
Match("noise overlay", Rectangle(-0.692f, 0.709f, -1.058f, 1.050f, s.sx, s.sy, 0x3C727272), s.sx, s.sy, Side::None, rule);
Match("noise overlay in black", Rectangle(-0.692f, 0.709f, -1.058f, 1.050f, s.sx, s.sy, kBar), s.sx, s.sy, Side::None, rule);
Match("dark overlay", Rectangle(-0.712f, 0.668f, -0.893f, 0.908f, s.sx, s.sy, 0x3C000000), s.sx, s.sy, Side::None, rule);
Match("rolling bar", Rectangle(-0.68f, 0.70f, -1.04f, -0.90f, s.sx, s.sy, 0x147E7E7E), s.sx, s.sy, Side::None, rule);
// Fades and dims (the existing full-width rule's), the pause dim.
Match("fade", Rectangle(-1.29f, 1.29f, -1.30f, 1.30f, s.sx, s.sy, 0xFF000000), s.sx, s.sy, Side::None, rule);
Match("pause dim centre", Rectangle(-0.53f, 0.57f, -1.91f, 1.91f, s.sx, s.sy, 0xC8000000), s.sx, s.sy, Side::None, rule);
Quad grad = Rectangle(0.569f, 1.117f, -1.905f, 1.913f, s.sx, s.sy, 0xC8000000);
grad.v[1].argb = grad.v[2].argb = 0x00000000; // fades to nothing at the outer edge
Match("pause dim side gradient", grad, s.sx, s.sy, Side::None, rule);
Match("pause gradient shape in black", Rectangle(0.569f, 1.117f, -1.905f, 1.913f, s.sx, s.sy, kBar), s.sx, s.sy, Side::None, rule);
// Graffiti wipes, brushes and splats; the minimap; small pieces.
Match("wipe 1486D000", Rectangle(-0.34f, 1.09f, -3.30f, -0.75f, s.sx, s.sy, 0xFF000000), s.sx, s.sy, Side::None, rule);
Match("wipe 1486D000 right", Rectangle(0.70f, 1.60f, -2.55f, 2.50f, s.sx, s.sy, 0xFF000000), s.sx, s.sy, Side::None, rule);
Match("brush 15299000", Rectangle(-0.90f, 0.90f, -1.07f, -0.58f, s.sx, s.sy, 0xFF000000), s.sx, s.sy, Side::None, rule);
Match("splat 15B4D000", Rectangle(-0.69f, -0.22f, -1.234f, -0.39f, s.sx, s.sy, 0xFE000000), s.sx, s.sy, Side::None, rule);
Match("minimap piece", Rectangle(-0.73f, -0.46f, -1.02f, -0.53f, s.sx, s.sy, 0xFE7F7F7F), s.sx, s.sy, Side::None, rule);
Match("small T piece", Rectangle(0.62f, 0.69f, 0.95f, 1.07f, s.sx, s.sy, kBar), s.sx, s.sy, Side::None, rule);
Match("loading band", Rectangle(-1.29f, 1.29f, -0.30f, 0.30f, s.sx, s.sy, 0xFF000000), s.sx, s.sy, Side::None, rule);
// The rest of the front-end quads catalogued while investigating.
Match("corner bracket", Rectangle(0.568f, 0.637f, 0.723f, 0.846f, s.sx, s.sy, 0x777E7E7E), s.sx, s.sy, Side::None, rule);
Match("corner bracket slid out", Rectangle(1.04f, 1.11f, 0.723f, 0.846f, s.sx, s.sy, 0x777E7E7E), s.sx, s.sy, Side::None, rule);
Match("REC dot", Rectangle(-0.590f, -0.521f, 0.639f, 0.762f, s.sx, s.sy, 0xFB7E0505), s.sx, s.sy, Side::None, rule);
Match("garage wipe 148CD000", Rectangle(-1.67f, 1.29f, -0.40f, 2.50f, s.sx, s.sy, 0xFF000000), s.sx, s.sy, Side::None, rule);
Match("grey wipe 14CB0000", Rectangle(-1.20f, 1.97f, -1.196f, 0.18f, s.sx, s.sy, 0xFF7F7F7F), s.sx, s.sy, Side::None, rule);
Match("grey splat 1470B000", Rectangle(0.36f, 0.70f, 0.89f, 1.64f, s.sx, s.sy, 0xFE7F7F7F), s.sx, s.sy, Side::None, rule);
Match("loading glow 19A5A000", Rectangle(-0.29f, 0.05f, -1.03f, -0.44f, s.sx, s.sy, 0xFD2E2664), s.sx, s.sy, Side::None, rule);
Match("loading glow 1AD58000", Rectangle(-1.29f, 1.29f, -1.05f, 1.05f, s.sx, s.sy, 0x3C000000), s.sx, s.sy, Side::None, rule);
Match("minimap piece L", Rectangle(-1.016f, -0.73f, -0.54f, -0.04f, s.sx, s.sy, 0xFE7F7F7F), s.sx, s.sy, Side::None, rule);
Match("menu glyph L", Rectangle(-1.05f, -0.95f, -0.70f, -0.50f, s.sx, s.sy, 0xFE7F7F7F), s.sx, s.sy, Side::None, rule);
Match("parked additive 19B04000", Rectangle(0.80f, 1.10f, -1.40f, -1.10f, s.sx, s.sy, 0xFF000000), s.sx, s.sy, Side::None, rule);
Match("title key art", Rectangle(-1.105f, 1.105f, -1.105f, 1.105f, s.sx, s.sy, 0xFF000000), s.sx, s.sy, Side::None, rule);
// The FD000000 dialog boxes over the vid cam (cam_329; same colour
// as the bars, inside the band).
Match("cam dialog box 1", Rectangle(-0.578f, 0.586f, -0.075f, 0.462f, s.sx, s.sy, kBar), s.sx, s.sy, Side::None, rule);
Match("cam dialog box 2", Rectangle(-0.578f, 0.586f, -0.167f, 0.569f, s.sx, s.sy, kBar), s.sx, s.sy, Side::None, rule);
Match("cam dialog box 3", Rectangle(0.172f, 0.494f, -0.348f, -0.245f, s.sx, s.sy, kBar), s.sx, s.sy, Side::None, rule);
// A black panel at one side inside the band (a menu's side column):
// stops short of the band's top and bottom, so not a mask bar.
Match("black side panel in the band", Rectangle(0.651f, 1.406f, -0.95f, 0.95f, s.sx, s.sy, kBar), s.sx, s.sy, Side::None, rule);
}
// Each threshold from both sides (the right bar at 4:3, one value
// moved; the left bar mirrored), so a moved threshold fails a case.
const float sx = 1.0f, sy = 0.75f;
auto bar = [&](float in, float out, float bottom, float top, uint32_t argb = kBar) {
return Rectangle(in, out, -bottom, top, sx, sy, argb);
};
auto mirrored = [&](float in, float out, float bottom, float top) { return Rectangle(-out, -in, -bottom, top, sx, sy, kBar); };
Match("top 1.005", bar(0.651f, 1.406f, 1.024f, 1.005f), sx, sy, Side::None, rule);
Match("top 1.015", bar(0.651f, 1.406f, 1.024f, 1.015f), sx, sy, Side::Right, rule);
Match("top 1.095", bar(0.651f, 1.406f, 1.024f, 1.095f), sx, sy, Side::Right, rule);
Match("top 1.105", bar(0.651f, 1.406f, 1.024f, 1.105f), sx, sy, Side::None, rule);
Match("bottom 1.005", bar(0.651f, 1.406f, 1.005f, 1.039f), sx, sy, Side::None, rule);
Match("bottom 1.105", bar(0.651f, 1.406f, 1.105f, 1.039f), sx, sy, Side::None, rule);
Match("inner 0.495", bar(0.495f, 1.406f, 1.024f, 1.039f), sx, sy, Side::None, rule);
Match("inner 0.505", bar(0.505f, 1.406f, 1.024f, 1.039f), sx, sy, Side::Right, rule);
Match("inner 0.795", bar(0.795f, 1.406f, 1.024f, 1.039f), sx, sy, Side::Right, rule);
Match("inner 0.805", bar(0.805f, 1.406f, 1.024f, 1.039f), sx, sy, Side::None, rule);
Match("outer 1.195", bar(0.651f, 1.195f, 1.024f, 1.039f), sx, sy, Side::None, rule);
Match("outer 1.205", bar(0.651f, 1.205f, 1.024f, 1.039f), sx, sy, Side::Right, rule);
Match("outer 1.995", bar(0.651f, 1.995f, 1.024f, 1.039f), sx, sy, Side::Right, rule);
Match("outer 2.005", bar(0.651f, 2.005f, 1.024f, 1.039f), sx, sy, Side::None, rule);
Match("left inner 0.495", mirrored(0.495f, 1.410f, 1.027f, 1.035f), sx, sy, Side::None, rule);
Match("left inner 0.805", mirrored(0.805f, 1.410f, 1.027f, 1.035f), sx, sy, Side::None, rule);
Match("left outer 1.195", mirrored(0.655f, 1.195f, 1.027f, 1.035f), sx, sy, Side::None, rule);
Match("left outer 2.005", mirrored(0.655f, 2.005f, 1.027f, 1.035f), sx, sy, Side::None, rule);
Match("left top 1.105", mirrored(0.655f, 1.410f, 1.027f, 1.105f), sx, sy, Side::None, rule);
Match("alpha EF", bar(0.651f, 1.406f, 1.024f, 1.039f, 0xEF000000), sx, sy, Side::None, rule);
Match("alpha F0", bar(0.651f, 1.406f, 1.024f, 1.039f, 0xF0000000), sx, sy, Side::Right, rule);
Match("alpha FF", bar(0.651f, 1.406f, 1.024f, 1.039f, 0xFF000000), sx, sy, Side::Right, rule);
Match("blue 01", bar(0.651f, 1.406f, 1.024f, 1.039f, 0xFD000001), sx, sy, Side::None, rule);
Match("red 01", bar(0.651f, 1.406f, 1.024f, 1.039f, 0xFD010000), sx, sy, Side::None, rule);
Quad mixed = bar(0.651f, 1.406f, 1.024f, 1.039f);
mixed.v[2].argb = 0xFE000000;
Match("one vertex's colour differs", mixed, sx, sy, Side::None, rule);
Quad off = bar(0.651f, 1.406f, 1.024f, 1.039f);
off.v[2].x += 0.01f;
Match("a vertex 0.01 off its corner", off, sx, sy, Side::None, rule);
Quad nearly = bar(0.651f, 1.406f, 1.024f, 1.039f);
nearly.v[2].x += 0.0005f;
Match("a vertex 0.0005 off its corner", nearly, sx, sy, Side::Right, rule);
Quad bowtie = bar(0.651f, 1.406f, 1.024f, 1.039f);
std::swap(bowtie.v[1], bowtie.v[2]);
Match("bow-tie order", bowtie, sx, sy, Side::None, rule);
Quad repeated = bar(0.651f, 1.406f, 1.024f, 1.039f);
repeated.v[3] = repeated.v[2];
Match("a repeated vertex", repeated, sx, sy, Side::None, rule);
return g_failures - before;
}
// The same cases under a wrong rule must fail at least one.
static void Planted(const char* what, const Rule& wrong)
{
const int saved = g_failures;
g_quiet = true;
const int failed = MatchCases(wrong);
g_quiet = false;
g_failures = saved;
printf("planted %-26s %d case(s) fail\n", what, failed);
CHECK(failed > 0, "planted %s: every case still passes", what);
}
// 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.
static void FrontEndPs(float ps[4], float ox = 640.0f, float oy = 360.0f, float sy = -360.0f)
{
ps[0] = 2.0f * 640.0f / 1280.0f;
ps[1] = 2.0f * sy / 2048.0f;
ps[2] = 2.0f * ox / 1280.0f - 1.0f;
ps[3] = 2.0f * oy / 2048.0f - 1.0f;
}
static bool Same(const Rect& a, const Rect& b)
{
return a.x0 == b.x0 && a.y0 == b.y0 && a.x1 == b.x1 && a.y1 == b.y1;
}
static void FillBar(const char* what, Side side, float bx0, float bx1, float sx, float sy, const float ps[4], float hostW, float hostH,
const Rect& scissor, uint32_t wantN, const Rect* want)
{
const Box box{ bx0 * sx, bx1 * sx, -1.024f * sy, 1.039f * sy };
Rect got[2]{};
const uint32_t n = FillRects(side, box, ps, hostW, hostH, scissor, sx, sy, got);
printf("%s: %u rect(s)", what, n);
for (uint32_t i = 0; i < n; i++)
printf(" [%d,%d)x[%d,%d)", got[i].x0, got[i].x1, got[i].y0, got[i].y1);
printf("\n");
CHECK(n == wantN, "%s: %u rects, want %u", what, n, wantN);
for (uint32_t i = 0; i < n && i < wantN; i++)
CHECK(Same(got[i], want[i]), "%s: rect %u [%d,%d)x[%d,%d), want [%d,%d)x[%d,%d)", what, i, got[i].x0, got[i].x1, got[i].y0,
got[i].y1, want[i].x0, want[i].x1, want[i].y0, want[i].y1);
}
// An independent oracle (pixel centres, no Edge()): a pixel of the scissor
// is filled exactly when its centre is outside the band on the bar's side:
// under Vert+ |y| > sy and on the bar's half (x > 0 right, x < 0 left);
// under Hor+, when the bar ends short of the screen edge, x > sx (right) or
// x < -sx (left). A pixel whose centre lies within 1/256 pixel of one of
// those edges is a tie (Edge() takes it, by the rasteriser's rule) and is
// skipped: the exact rectangles and the complement check cover ties.
static void Oracle(const char* what, Side side, float sx, float sy, const float ps[4], float hostW, float hostH, const Rect& scissor)
{
const bool right = side == Side::Right;
const Box box = right ? Box{ 0.651f * sx, 1.406f * sx, -1.024f * sy, 1.039f * sy } : Box{ -1.410f * sx, -0.655f * sx, -1.027f * sy, 1.035f * sy };
Rect r[2]{};
const uint32_t n = FillRects(side, box, ps, hostW, hostH, scissor, sx, sy, r);
const bool horPlus = sy >= 1.0f && sx < 1.0f && (right ? box.x1 < 1.0f : box.x0 > -1.0f);
// Host pixels per clip unit, for the tie distance.
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, ties = 0;
for (int32_t y = scissor.y0; y < scissor.y1; y++)
{
const float cy = ((float(y) + 0.5f) / hostH * 2.0f - 1.0f - ps[3]) / ps[1];
for (int32_t x = scissor.x0; x < scissor.x1; x++)
{
const float cx = ((float(x) + 0.5f) / hostW * 2.0f - 1.0f - ps[2]) / ps[0];
bool want = false, onEdge = false;
if (sy < 1.0f)
{
want = std::fabs(cy) > sy && (right ? cx > 0.0f : cx < 0.0f);
onEdge = tie(std::fabs(cy) - sy, pxY) || tie(cx, pxX);
}
else if (horPlus)
{
want = right ? cx > sx : cx < -sx;
onEdge = tie(std::fabs(cx) - sx, pxX);
}
bool got = false;
for (uint32_t i = 0; i < n; i++)
got = got || (x >= r[i].x0 && x < r[i].x1 && y >= r[i].y0 && y < r[i].y1);
if (onEdge)
ties++;
else
wrong += got != want;
}
}
printf("oracle %s: %d pixel(s) wrong (%d on an edge, skipped)\n", what, wrong, ties);
CHECK(wrong == 0, "%s: %d pixels differ from the pixel-centre oracle", what, wrong);
}
// The band clip's rows (gpu::bars::BandRows, which renderer.cpp clips the
// rest of the front end with) are the rows whose centres lie inside the
// band: checked against pixel centres, ties (within 1/256 pixel) skipped.
static void BandOracle(const char* what, float sy, const float ps[4], float hostH, int32_t rows)
{
const Rows band = BandRows(-sy, sy, ps[1], ps[3], hostH);
const float pxY = std::fabs(ps[1]) * hostH * 0.5f;
int wrong = 0, ties = 0;
for (int32_t y = 0; y < rows; y++)
{
const float cy = ((float(y) + 0.5f) / hostH * 2.0f - 1.0f - ps[3]) / ps[1];
if (std::fabs(std::fabs(cy) - sy) * pxY < 1.0f / 256.0f)
{
ties++;
continue;
}
wrong += (std::fabs(cy) < sy) != (y >= band.top && y < band.bottom);
}
printf("band rows %s: [%d,%d), %d row(s) wrong (%d on an edge, skipped)\n", what, band.top, band.bottom, wrong, ties);
CHECK(wrong == 0, "%s: %d band rows differ from the pixel-centre oracle", what, wrong);
}
// The intro's black frame (the cut at ~95 s; ~57 s: kFrameY57), band units.
constexpr float kFrameX = 0.970f, kFrameY = 0.939f, kFrameY57 = 0.920f;
constexpr uint32_t kFrameColour = 0xFF000000;
// Under Vert+ the fills and the band clip's rows split the scissor's rows
// with no gap and no overlap (a bar's half); and the black frame's fill and
// the frame's own pixels split the scissor's pixels the same way.
static void Complement(const char* what, float sy, const float ps[4], float hostW, float hostH, const Rect& scissor)
{
const Rows band = BandRows(-sy, sy, ps[1], ps[3], hostH);
const int32_t top = band.top, bottom = band.bottom;
const Box box{ 0.651f, 1.406f, -1.024f * sy, 1.039f * sy };
Rect r[2]{};
const uint32_t n = FillRects(Side::Right, box, ps, hostW, hostH, scissor, 1.0f, sy, r);
int wrong = 0;
for (int32_t y = scissor.y0; y < scissor.y1; y++)
{
const bool band = y >= top && y < bottom;
bool fill = false;
for (uint32_t i = 0; i < n; i++)
fill = fill || (y >= r[i].y0 && y < r[i].y1);
wrong += band == fill;
}
// The black frame's fill: each pixel of the scissor is in exactly one of
// the frame's own pixels (its box through the same Edge rule as the
// rasteriser's pixel centres) and the fill.
const Box frame{ -kFrameX, kFrameX, -kFrameY * sy, kFrameY * sy };
const Rect own = ClipToHost(frame.x0, frame.x1, frame.y0, frame.y1, ps, hostW, hostH);
Rect k[4]{};
const uint32_t kn = BlackoutRects(frame, ps, hostW, hostH, scissor, k);
int wrongPx = 0;
for (int32_t y = scissor.y0; y < scissor.y1; y++)
for (int32_t x = scissor.x0; x < scissor.x1; x++)
{
int in = (x >= own.x0 && x < own.x1 && y >= own.y0 && y < own.y1) ? 1 : 0;
for (uint32_t i = 0; i < kn; i++)
in += (x >= k[i].x0 && x < k[i].x1 && y >= k[i].y0 && y < k[i].y1) ? 1 : 0;
wrongPx += in != 1;
}
printf("complement %s: band rows [%d,%d), %d row(s) wrong; black frame %d pixel(s) wrong\n", what, top, bottom, wrong, wrongPx);
CHECK(wrong == 0, "%s: %d rows both or neither in the band clip and the fill", what, wrong);
CHECK(wrongPx == 0, "%s: %d pixels both or neither in the black frame and its fill", what, wrongPx);
}
static void Cover(const char* what, float bx0, float bx1, float sx, float sy, const float ps[4])
{
const Box b{ bx0 * sx, bx1 * sx, -1.024f * sy, 1.039f * sy };
float q[4];
CoverFrame(b, ps, q);
// The box's corners land where this draw's viewport puts clip +-1.02.
const float e = 1.02f;
const float gx0 = b.x0 * q[0] + q[2], gx1 = b.x1 * q[0] + q[2], gy0 = b.y0 * q[1] + q[3], gy1 = b.y1 * q[1] + q[3];
const float wx0 = -e * ps[0] + ps[2], wx1 = e * ps[0] + ps[2], wy0 = -e * ps[1] + ps[3], wy1 = e * ps[1] + ps[3];
printf("cover %s: posScale %.5f %.5f %.5f %.5f\n", what, q[0], q[1], q[2], q[3]);
CHECK(std::fabs(gx0 - wx0) < 1e-5f && std::fabs(gx1 - wx1) < 1e-5f, "%s: x %.6f..%.6f, want %.6f..%.6f", what, gx0, gx1, wx0, wx1);
CHECK(std::fabs(gy0 - wy0) < 1e-5f && std::fabs(gy1 - wy1) < 1e-5f, "%s: y %.6f..%.6f, want %.6f..%.6f", what, gy0, gy1, wy0, wy1);
CHECK(q[0] * ps[0] > 0.0f && q[1] * ps[1] > 0.0f, "%s: flipped (%.5f vs %.5f, %.5f vs %.5f)", what, q[0], ps[0], q[1], ps[1]);
}
// ---- The intro's black frame (the cut between two shots, ~95 s) ----
// Its box in band units, as logged (NFSMW_LOG_FRONTEND "inset candidate",
// the Deck and 32:9): x +-0.970, y +-0.939, FF000000 (kFrameX, kFrameY); and
// the one at the intro's start (~57 s): y +-0.920, FE000000 (kFrameY57). The
// Blacklist bio's panel (GitHub #19): x +-0.976, y +-0.985, FB000000 then
// FF000000, fading in from 11000000.
constexpr float kBioX = 0.976f, kBioY = 0.985f;
static void MatchFrame(const char* what, const Quad& q, float sx, float sy, bool want, const BlackoutRule& rule)
{
Box box{};
const bool got = MatchBlackout(q.v, sx, sy, rule, &box);
CHECK(got == want, "%s at x %.4f y %.4f: %s, want %s", what, sx, sy, got ? "black frame" : "none", want ? "black frame" : "none");
}
// Every black-frame case. Returns the failures it added (for Planted).
static int BlackoutCases(const BlackoutRule& rule)
{
const int before = g_failures;
constexpr uint32_t kBar = 0xFD000000;
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); };
MatchFrame("black frame", at(-kFrameX, kFrameX, -kFrameY, kFrameY, kFrameColour), s.sx, s.sy, true, rule);
MatchFrame("black frame FD", at(-kFrameX, kFrameX, -kFrameY, kFrameY, kBar), s.sx, s.sy, true, rule);
MatchFrame("black frame at ~57 s", at(-kFrameX, kFrameX, -kFrameY57, kFrameY57, 0xFE000000), s.sx, s.sy, true, rule);
// The overlay drawn with it there (B8 alpha, past the band in x).
MatchFrame("~57 s overlay", at(-1.104f, 1.104f, -0.978f, 0.985f, 0xB8000000), s.sx, s.sy, false, rule);
MatchFrame("~57 s overlay shape, opaque", at(-1.104f, 1.104f, -0.978f, 0.985f, 0xFF000000), s.sx, s.sy, false, rule);
// The title's key art (1024x512, grey vertex colour), short of the
// band in y only: an inset candidate in the log, never a black frame.
MatchFrame("title key art", at(-1.104f, 1.104f, -0.981f, 0.981f, 0xFF7F7F7F), s.sx, s.sy, false, rule);
// Everything else black or nearly: the vid-cam bars, the cover
// rule's full-band layer, the fades, the key art, the overlays.
MatchFrame("right bar", at(0.651f, 1.406f, -1.024f, 1.039f, kBar), s.sx, s.sy, false, rule);
MatchFrame("left bar", at(-1.410f, -0.655f, -1.027f, 1.035f, kBar), s.sx, s.sy, false, rule);
MatchFrame("full band", at(-1.0f, 1.0f, -1.0f, 1.0f, 0xFF000000), s.sx, s.sy, false, rule);
MatchFrame("fade", at(-1.29f, 1.29f, -1.30f, 1.30f, 0xFF000000), s.sx, s.sy, false, rule);
MatchFrame("title key art", at(-1.105f, 1.105f, -1.105f, 1.105f, 0xFF000000), s.sx, s.sy, false, rule);
MatchFrame("loading band", at(-1.29f, 1.29f, -0.30f, 0.30f, 0xFF000000), s.sx, s.sy, false, rule);
MatchFrame("noise overlay in black", at(-0.692f, 0.709f, -1.058f, 1.050f, kBar), s.sx, s.sy, false, rule);
MatchFrame("dark overlay", at(-0.712f, 0.668f, -0.893f, 0.908f, 0x3C000000), s.sx, s.sy, false, rule);
MatchFrame("cam dialog box 2", at(-0.578f, 0.586f, -0.167f, 0.569f, kBar), s.sx, s.sy, false, rule);
MatchFrame("pause dim centre", at(-0.53f, 0.57f, -1.91f, 1.91f, 0xC8000000), s.sx, s.sy, false, rule);
MatchFrame("black frame in grey", at(-kFrameX, kFrameX, -kFrameY, kFrameY, 0xFF7F7F7F), s.sx, s.sy, false, rule);
MatchFrame("black frame at 3C", at(-kFrameX, kFrameX, -kFrameY, kFrameY, 0x3C000000), s.sx, s.sy, false, rule);
MatchFrame("frame width, full height", at(-kFrameX, kFrameX, -1.0f, 1.0f, 0xFF000000), s.sx, s.sy, false, rule);
MatchFrame("full width, frame height", at(-1.0f, 1.0f, -kFrameY, kFrameY, 0xFF000000), s.sx, s.sy, false, rule);
}
// Each threshold from both sides, on the Deck and at 32:9.
const Shape shapes[] = { { "Deck", 1.0f, 0.9f }, { "32:9", 0.5f, 1.0f } };
for (const Shape& s : shapes)
{
auto frame = [&](float dx, float dy, float hx, float hy, uint32_t argb = kFrameColour) {
return Rectangle(dx - hx, dx + hx, dy - hy, dy + hy, s.sx, s.sy, argb);
};
MatchFrame("centre x +0.009", frame(0.009f, 0.0f, kFrameX, kFrameY), s.sx, s.sy, true, rule);
MatchFrame("centre x +0.011", frame(0.011f, 0.0f, kFrameX, kFrameY), s.sx, s.sy, false, rule);
MatchFrame("centre x -0.011", frame(-0.011f, 0.0f, kFrameX, kFrameY), s.sx, s.sy, false, rule);
MatchFrame("centre y -0.009", frame(0.0f, -0.009f, kFrameX, kFrameY), s.sx, s.sy, true, rule);
MatchFrame("centre y -0.011", frame(0.0f, -0.011f, kFrameX, kFrameY), s.sx, s.sy, false, rule);
MatchFrame("centre y +0.011", frame(0.0f, 0.011f, kFrameX, kFrameY), s.sx, s.sy, false, rule);
MatchFrame("half width 0.895", frame(0.0f, 0.0f, 0.895f, kFrameY), s.sx, s.sy, false, rule);
MatchFrame("half width 0.905", frame(0.0f, 0.0f, 0.905f, kFrameY), s.sx, s.sy, true, rule);
MatchFrame("half width 0.985", frame(0.0f, 0.0f, 0.985f, kFrameY), s.sx, s.sy, true, rule);
MatchFrame("half width 0.995", frame(0.0f, 0.0f, 0.995f, kFrameY), s.sx, s.sy, false, rule);
MatchFrame("half height 0.895", frame(0.0f, 0.0f, kFrameX, 0.895f), s.sx, s.sy, false, rule);
MatchFrame("half height 0.905", frame(0.0f, 0.0f, kFrameX, 0.905f), s.sx, s.sy, true, rule);
MatchFrame("half height 0.985", frame(0.0f, 0.0f, kFrameX, 0.985f), s.sx, s.sy, true, rule);
MatchFrame("half height 0.995", frame(0.0f, 0.0f, kFrameX, 0.995f), s.sx, s.sy, false, rule);
MatchFrame("alpha EF", frame(0.0f, 0.0f, kFrameX, kFrameY, 0xEF000000), s.sx, s.sy, false, rule);
MatchFrame("alpha F0", frame(0.0f, 0.0f, kFrameX, kFrameY, 0xF0000000), s.sx, s.sy, true, rule);
MatchFrame("blue 01", frame(0.0f, 0.0f, kFrameX, kFrameY, 0xFF000001), s.sx, s.sy, false, rule);
Quad mixed = frame(0.0f, 0.0f, kFrameX, kFrameY);
mixed.v[3].argb = 0xFE000000;
MatchFrame("one vertex's colour differs", mixed, s.sx, s.sy, false, rule);
Quad off = frame(0.0f, 0.0f, kFrameX, kFrameY);
off.v[2].y += 0.01f;
MatchFrame("a vertex 0.01 off its corner", off, s.sx, s.sy, false, rule);
Quad nearly = frame(0.0f, 0.0f, kFrameX, kFrameY);
nearly.v[2].y += 0.0005f;
MatchFrame("a vertex 0.0005 off its corner", nearly, s.sx, s.sy, true, rule);
Quad bowtie = frame(0.0f, 0.0f, kFrameX, kFrameY);
std::swap(bowtie.v[1], bowtie.v[2]);
MatchFrame("bow-tie order", bowtie, s.sx, s.sy, false, rule);
}
return g_failures - before;
}
static void PlantedBlackout(const char* what, const BlackoutRule& wrong)
{
const int saved = g_failures;
g_quiet = true;
const int failed = BlackoutCases(wrong);
g_quiet = false;
g_failures = saved;
printf("planted %-26s %d case(s) fail\n", what, failed);
CHECK(failed > 0, "planted %s: every case still passes", what);
}
static void FillFrame(const char* what, float sx, float sy, const float ps[4], float hostW, float hostH, const Rect& scissor,
uint32_t wantN, const Rect* want, float fx = kFrameX, float fy = kFrameY)
{
Rect got[4]{};
const Box b{ -fx * sx, fx * sx, -fy * sy, fy * sy };
const uint32_t n = BlackoutRects(b, ps, hostW, hostH, scissor, got);
printf("%s: %u rect(s)", what, n);
for (uint32_t i = 0; i < n; i++)
printf(" [%d,%d)x[%d,%d)", got[i].x0, got[i].x1, got[i].y0, got[i].y1);
printf("\n");
CHECK(n == wantN, "%s: %u rects, want %u", what, n, wantN);
for (uint32_t i = 0; i < n && i < wantN; i++)
CHECK(Same(got[i], want[i]), "%s: rect %u [%d,%d)x[%d,%d), want [%d,%d)x[%d,%d)", what, i, got[i].x0, got[i].x1, got[i].y0,
got[i].y1, want[i].x0, want[i].x1, want[i].y0, want[i].y1);
}
// v0.1.1-v0.1.5's black frame fill, kept here as a planted fault: only what
// lies past the band (the rows above and below it under Vert+, the columns
// beside it under Hor+, nothing at 16:9), so the band's thin border round
// the frame showed what was behind (GitHub #19's Blacklist bio).
static uint32_t PastBandOnly(const Box&, 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;
}
// The black frame's fill against pixel centres: a pixel of the scissor is
// filled, exactly once, when its centre lies outside the frame's box (past
// the band and in the band's border round it, at every shape, 16:9 too),
// never inside it; ties within 1/256 pixel skipped, as above. `rects` makes
// the rectangles (BlackoutRects, or a planted fault). Returns the wrong
// pixels.
using FrameRectsFn = uint32_t (*)(const Box&, const float*, float, float, const Rect&, float, float, Rect*);
static uint32_t RingRects(const Box& b, const float* ps, float w, float h, const Rect& sc, float, float, Rect* out)
{
return BlackoutRects(b, ps, w, h, sc, out);
}
static int FrameOracle(const char* what, float sx, float sy, const float ps[4], float hostW, float hostH, const Rect& scissor,
FrameRectsFn rects = RingRects, bool print = true, float fx = kFrameX, float fy = kFrameY)
{
const Box b{ -fx * sx, fx * sx, -fy * sy, fy * sy };
Rect r[4]{};
const uint32_t n = rects(b, ps, hostW, hostH, scissor, sx, sy, r);
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, ties = 0;
for (int32_t y = scissor.y0; y < scissor.y1; y++)
{
const float cy = ((float(y) + 0.5f) / hostH * 2.0f - 1.0f - ps[3]) / ps[1];
for (int32_t x = scissor.x0; x < scissor.x1; 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))
{
ties++;
continue;
}
const bool want = !(cx > b.x0 && cx < b.x1 && cy > b.y0 && cy < b.y1);
int got = 0;
for (uint32_t i = 0; i < n; i++)
got += x >= r[i].x0 && x < r[i].x1 && y >= r[i].y0 && y < r[i].y1;
wrong += got != (want ? 1 : 0);
}
}
if (print)
{
printf("oracle black frame %s: %d pixel(s) wrong (%d on an edge, skipped)\n", what, wrong, ties);
CHECK(wrong == 0, "black frame %s: %d pixels differ from the pixel-centre oracle", what, wrong);
}
return wrong;
}
// ---- PlanFill on the draws as the renderer sees them ----
// The front end's matrix at a shape: c0 0.69 (0.92 safe area x 0.75, a 4:3
// canvas centred in 16:9) x scaleX, c5 0.92 x scaleY (positive, as logged:
// "c5 0.6900 (front end y 0.6900)" at 4:3), 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 } };
}
// A quad's ePolys (words 0..3: x, y, z, colour), the positions taken back
// through the matrix: what the game wrote for that box.
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 draw the S2 dump shows for a vid-cam bar (and the cut's black frame):
// 1280x2048 vp s(640,-360) o(640,360) sc(0,0)-(1280,720) vte43F, blend
// 07060706, t0 1AD6E000 32x32, prim 13 n4, untouched by the other rules.
static Draw S2Draw(const Matrix& m, float sx, float sy, const float ps[4], float hostW = 1280.0f, float hostH = 2048.0f,
Rect scissor = Rect{ 0, 0, 1280, 720 })
{
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,
.posScale = { ps[0], ps[1], ps[2], ps[3] },
.hostW = hostW,
.hostH = hostH,
.scissor = scissor };
}
static const char* Name(Fill f)
{
return f == Fill::RightBar ? "right bar" : f == Fill::LeftBar ? "left bar" : f == Fill::Blackout ? "black frame"
: f == Fill::Backdrop ? "backdrop" : "none";
}
// Runs PlanFill over the ePolys; the reader fails for vertices from
// `limit` on (past the vertex buffer) but still hands the word over, so a
// plan that ignored the failure would act on it. `reads` counts calls.
static Plan RunPlan(const Draw& d, const EPolys& e, uint32_t limit = 4, int* reads = nullptr)
{
int n = 0;
const Plan p = PlanFill(d, [&](uint32_t k, uint32_t j, uint32_t& word) {
n++;
word = e.w[k & 3][j & 3];
return k < limit;
});
if (reads)
*reads = n;
return p;
}
static void PlanCase(const char* what, const Draw& d, const EPolys& e, Fill want, uint32_t wantN = 0, const Rect* wantRects = nullptr,
uint32_t limit = 4)
{
const Plan p = RunPlan(d, e, limit);
printf("plan %s: %s, %u rect(s)", what, Name(p.fill), p.rectCount);
for (uint32_t i = 0; i < p.rectCount; i++)
printf(" [%d,%d)x[%d,%d)", p.rects[i].x0, p.rects[i].x1, p.rects[i].y0, p.rects[i].y1);
printf("\n");
CHECK(p.fill == want, "%s: %s, want %s", what, Name(p.fill), Name(want));
if (p.fill != want || want == Fill::None)
return;
CHECK(p.rectCount == wantN, "%s: %u rects, want %u", what, p.rectCount, wantN);
for (uint32_t i = 0; i < p.rectCount && i < wantN; i++)
CHECK(Same(p.rects[i], wantRects[i]), "%s: rect %u [%d,%d)x[%d,%d), want [%d,%d)x[%d,%d)", what, i, p.rects[i].x0, p.rects[i].x1,
p.rects[i].y0, p.rects[i].y1, wantRects[i].x0, wantRects[i].x1, wantRects[i].y0, wantRects[i].y1);
// The copy's posScale stretches the quad's own box (as the renderer
// reads it) over the frame through the draw's posScale.
float q[4];
CoverFrame(p.box, d.posScale, q);
CHECK(std::memcmp(q, p.posScale, sizeof(q)) == 0, "%s: posScale %.5f %.5f %.5f %.5f, want %.5f %.5f %.5f %.5f", what, p.posScale[0],
p.posScale[1], p.posScale[2], p.posScale[3], q[0], q[1], q[2], q[3]);
CHECK(p.argb == e.w[0][3], "%s: colour %08X, want %08X", what, p.argb, e.w[0][3]);
}
// ---- The full-band backdrop (the results screen after a race) ----
// Its box in band units: x +-1.104, y +-0.981 (the title's key art has the
// same, logged as an inset candidate); opaque, textured (1024x512).
constexpr float kBackdropX = 1.104f, kBackdropY = 0.981f;
constexpr uint32_t kBackdropColour = 0xFF7F7F7F;
static void MatchBack(const char* what, const Quad& q, float sx, float sy, bool want, const BackdropRule& rule)
{
Box box{};
const bool got = MatchBackdrop(q.v, sx, sy, rule, &box);
CHECK(got == want, "%s at x %.4f y %.4f: %s, want %s", what, sx, sy, got ? "backdrop" : "none", want ? "backdrop" : "none");
}
// Every backdrop match case. Returns the failures it added (for planting).
static int BackdropCases(const BackdropRule& 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); };
MatchBack("results backdrop", at(-kBackdropX, kBackdropX, -kBackdropY, kBackdropY, kBackdropColour), s.sx, s.sy, true, rule);
MatchBack("results backdrop, alpha F0", at(-kBackdropX, kBackdropX, -kBackdropY, kBackdropY, 0xF07F7F7F), s.sx, s.sy, true, rule);
MatchBack("a picture over the whole band", at(-1.0f, 1.0f, -1.0f, 1.0f, 0xFF808080), s.sx, s.sy, true, rule);
MatchBack("a picture 0.96 of the band's height", at(-1.0f, 1.0f, -0.96f, 0.96f, 0xFF808080), s.sx, s.sy, true, rule);
// What must show the world (or isn't such a picture).
MatchBack("~57 s overlay (B8)", at(-1.104f, 1.104f, -0.978f, 0.985f, 0xB8000000), s.sx, s.sy, false, rule);
MatchBack("a translucent picture (alpha EF)", at(-kBackdropX, kBackdropX, -kBackdropY, kBackdropY, 0xEF7F7F7F), s.sx, s.sy, false, rule);
MatchBack("fade", at(-1.29f, 1.29f, -1.30f, 1.30f, 0xFF000000), s.sx, s.sy, false, rule);
MatchBack("loading band", at(-1.29f, 1.29f, -0.30f, 0.30f, 0xFF000000), s.sx, s.sy, false, rule);
MatchBack("black frame", at(-kFrameX, kFrameX, -kFrameY, kFrameY, kFrameColour), s.sx, s.sy, false, rule);
MatchBack("black frame at ~57 s", at(-kFrameX, kFrameX, -kFrameY57, kFrameY57, 0xFE000000), s.sx, s.sy, false, rule);
MatchBack("noise overlay", at(-0.692f, 0.709f, -1.058f, 1.050f, 0x3C727272), s.sx, s.sy, false, rule);
MatchBack("noise overlay shape, opaque", at(-0.692f, 0.709f, -1.058f, 1.050f, 0xFF727272), s.sx, s.sy, false, rule);
MatchBack("pause dim centre", at(-0.53f, 0.57f, -1.91f, 1.91f, 0xC8000000), s.sx, s.sy, false, rule);
MatchBack("pause dim shape, opaque", at(-0.53f, 0.57f, -1.91f, 1.91f, 0xFF000000), s.sx, s.sy, false, rule);
MatchBack("cam dialog box", at(-0.578f, 0.586f, -0.167f, 0.569f, 0xFD000000), s.sx, s.sy, false, rule);
MatchBack("right bar", at(0.651f, 1.406f, -1.024f, 1.039f, 0xFD000000), s.sx, s.sy, false, rule);
MatchBack("minimap piece", at(-0.73f, -0.46f, -1.02f, -0.53f, 0xFE7F7F7F), s.sx, s.sy, false, rule);
MatchBack("half-height picture", at(-1.104f, 1.104f, -0.5f, 0.5f, kBackdropColour), s.sx, s.sy, false, rule);
MatchBack("a picture off-centre by 0.05", at(-kBackdropX + 0.05f, kBackdropX + 0.05f, -kBackdropY, kBackdropY, kBackdropColour), s.sx,
s.sy, false, rule);
MatchBack("a picture 0.94 of the band's height", at(-1.104f, 1.104f, -0.94f, 0.94f, kBackdropColour), s.sx, s.sy, false, rule);
MatchBack("a picture 0.98 of the band's width", at(-0.98f, 0.98f, -1.0f, 1.0f, kBackdropColour), s.sx, s.sy, false, rule);
MatchBack("a picture to 1.25", at(-1.25f, 1.25f, -1.0f, 1.0f, kBackdropColour), s.sx, s.sy, false, rule);
// Not one rectangle: a vertex off its corner.
Quad bent = at(-kBackdropX, kBackdropX, -kBackdropY, kBackdropY, kBackdropColour);
bent.v[2].x -= 0.05f;
MatchBack("a bent quad", bent, s.sx, s.sy, false, rule);
}
return g_failures - before;
}
static void PlantedBackdrop(const char* what, const BackdropRule& wrong)
{
const int saved = g_failures;
g_quiet = true;
const int failed = BackdropCases(wrong);
g_quiet = false;
g_failures = saved;
printf("planted %s: %d case(s) failed\n", what, failed);
CHECK(failed > 0, "planted %s passed every backdrop case", what);
}
// The backdrop's fill against pixel centres: every pixel of the scissor
// outside the box is filled, exactly once, if it lies past the band or in
// a row above or below the box (the band's overscan rows, at every shape,
// 16:9 too); nothing in the box is, nor in the band beside it within its
// rows (ties within 1/256 pixel skipped). `rects` makes the rectangles
// (BackdropRects, or a planted fault). Returns the wrong pixels; the
// overscan pixels it checked (in the band, above or below the box) go to
// `overscan`.
using BackdropRectsFn = uint32_t (*)(const Box&, const float*, float, float, const Rect&, float, float, Rect*);
static int BackdropOracle(const char* what, float sx, float sy, const Box& b, const float ps[4], float hostW, float hostH, const Rect& scissor,
BackdropRectsFn rects, bool print = true, int* overscan = nullptr)
{
Rect r[8]{};
const uint32_t n = rects(b, ps, hostW, hostH, scissor, sx, sy, r);
const float pxX = std::fabs(ps[0]) * hostW * 0.5f, pxY = std::fabs(ps[1]) * hostH * 0.5f;
int wrong = 0, ties = 0, rows = 0;
for (int32_t y = scissor.y0; y < scissor.y1; y++)
for (int32_t x = scissor.x0; x < scissor.x1; x++)
{
const float cx = ((float(x) + 0.5f) / hostW * 2.0f - 1.0f - ps[2]) / ps[0];
const float cy = ((float(y) + 0.5f) / hostH * 2.0f - 1.0f - ps[3]) / ps[1];
auto tie = [](float c, float e, float px) { return std::fabs(c - e) * px < 1.0f / 256.0f; };
if (tie(std::fabs(cx), sx, pxX) || tie(std::fabs(cy), sy, pxY) || tie(cx, b.x0, pxX) || tie(cx, b.x1, pxX) || tie(cy, b.y0, pxY) ||
tie(cy, b.y1, pxY))
{
ties++;
continue;
}
// The band: the columns inside +-sx (Hor+) or the rows inside
// +-sy (Vert+), all of the other axis (at 2x with half-pixel
// centres the first row's centre is past clip 1, still the band).
const bool inBand = (sx >= 1.0f || std::fabs(cx) < sx) && (sy >= 1.0f || std::fabs(cy) < sy);
const bool inBox = cx > b.x0 && cx < b.x1 && cy > b.y0 && cy < b.y1;
const bool pastRows = cy > b.y1 || cy < b.y0; // above or below the box
const bool want = !inBox && (!inBand || pastRows);
rows += inBand && pastRows;
int filled = 0;
for (uint32_t i = 0; i < n; i++)
filled += x >= r[i].x0 && x < r[i].x1 && y >= r[i].y0 && y < r[i].y1;
if (filled != (want ? 1 : 0))
{
if (print && wrong < 3 && std::getenv("EDGE_BARS_DEBUG"))
printf(" wrong px %d,%d: clip %.5f %.5f, band %d box %d rows %d filled %d\n", x, y, cx, cy, inBand, inBox, pastRows, filled);
wrong++;
}
}
if (overscan)
*overscan = rows;
if (print)
{
printf("backdrop fill %s: %u rect(s)", what, n);
for (uint32_t i = 0; i < n; i++)
printf(" [%d,%d)x[%d,%d)", r[i].x0, r[i].x1, r[i].y0, r[i].y1);
printf(", %d overscan px, %d pixel(s) wrong (%d on an edge, skipped)\n", rows, wrong, ties);
CHECK(wrong == 0, "%s: %d pixels filled wrongly (in the box or beside it in the band, twice, or left)", what, wrong);
}
return wrong;
}
// The backdrop's draw: S2Draw's registers over its 1024x512 picture.
static Draw BackdropDraw(const Matrix& m, float sx, float sy, const float ps[4], float hostW = 1280.0f, float hostH = 2048.0f,
Rect scissor = Rect{ 0, 0, 1280, 720 })
{
Draw d = S2Draw(m, sx, sy, ps, hostW, hostH, scissor);
d.t0Width = 1024;
d.t0Height = 512;
return d;
}
static void BackdropPlanCase(const char* what, const Draw& d, const EPolys& e, Fill want, uint32_t wantN = 0, const Rect* wantRects = nullptr)
{
const Plan p = PlanBackdrop(d, [&](uint32_t k, uint32_t j, uint32_t& word) {
word = e.w[k & 3][j & 3];
return k < 4;
});
printf("backdrop plan %s: %s, %u rect(s)", what, Name(p.fill), p.rectCount);
for (uint32_t i = 0; i < p.rectCount; i++)
printf(" [%d,%d)x[%d,%d)", p.rects[i].x0, p.rects[i].x1, p.rects[i].y0, p.rects[i].y1);
printf("\n");
CHECK(p.fill == want, "%s: %s, want %s", what, Name(p.fill), Name(want));
if (p.fill != want || want == Fill::None)
return;
CHECK(p.rectCount == wantN, "%s: %u rects, want %u", what, p.rectCount, wantN);
for (uint32_t i = 0; i < p.rectCount && i < wantN; i++)
CHECK(Same(p.rects[i], wantRects[i]), "%s: rect %u [%d,%d)x[%d,%d), want [%d,%d)x[%d,%d)", what, i, p.rects[i].x0, p.rects[i].x1,
p.rects[i].y0, p.rects[i].y1, wantRects[i].x0, wantRects[i].x1, wantRects[i].y0, wantRects[i].y1);
}
static void BackdropTests()
{
printf("\nbackdrop:\n");
BackdropCases(BackdropRule{});
printf("backdrop match cases at %zu screen shapes checked\n", std::size(kShapes));
printf("\nplanted, backdrop (each must fail a case):\n");
auto plant = [](const char* what, auto set) { BackdropRule r; set(r); PlantedBackdrop(what, r); };
plant("centreMax 0.06", [](BackdropRule& r) { r.centreMax = 0.06f; });
plant("extentMinX 0.97", [](BackdropRule& r) { r.extentMinX = 0.97f; });
plant("extentMinX 1.11", [](BackdropRule& r) { r.extentMinX = 1.11f; });
plant("extentMinY 0.93", [](BackdropRule& r) { r.extentMinY = 0.93f; });
plant("extentMinY 0.99", [](BackdropRule& r) { r.extentMinY = 0.99f; });
plant("extentMax 1.10", [](BackdropRule& r) { r.extentMax = 1.10f; });
plant("extentMax 1.30", [](BackdropRule& r) { r.extentMax = 1.30f; });
plant("minAlpha B0", [](BackdropRule& r) { r.minAlpha = 0xB0; });
plant("minAlpha F1", [](BackdropRule& r) { r.minAlpha = 0xF1; });
plant("cornerTolerance 0.1", [](BackdropRule& r) { r.cornerTolerance = 0.1f; });
printf("\nbackdrop fill:\n");
float ps[4], half[4], flipped[4], tiled[4];
FrontEndPs(ps);
FrontEndPs(half, 640.5f, 360.5f);
FrontEndPs(flipped, 640.0f, 360.0f, 360.0f);
FrontEndPs(tiled, 640.0f, 360.0f - 256.0f);
const Rect frame{ 0, 0, 1280, 720 }, frame2{ 0, 0, 2560, 1440 };
struct Case
{
const char* name;
float sx, sy;
};
const Case cases[] = { { "2560x1080", 0.75f, 1.0f }, { "2868x1320 (iPhone 18 Pro Max)", 0.818182f, 1.0f }, { "3440x1440", 0.744186f, 1.0f },
{ "32:9", 0.5f, 1.0f }, { "1.95:1 (the picture past the screen edge)", 0.912f, 1.0f }, { "1280x800", 1.0f, 0.9f },
{ "1024x768", 1.0f, 0.75f }, { "1200x800", 1.0f, 0.84375f }, { "1280x720 (16:9)", 1.0f, 1.0f },
{ "2732x2048 (iPad Pro)", 1.0f, 0.750366f }, { "3024x1898 (MacBook Pro 14)", 1.0f, 0.896207f } };
int wrong = 0;
for (const Case& c : cases)
{
const Box results{ -kBackdropX * c.sx, kBackdropX * c.sx, -kBackdropY * c.sy, kBackdropY * c.sy };
const Box full{ -c.sx, c.sx, -c.sy, c.sy };
char what[160];
snprintf(what, sizeof(what), "%s, results, 1x", c.name);
wrong += BackdropOracle(what, c.sx, c.sy, results, ps, 1280, 2048, frame, BackdropRects);
snprintf(what, sizeof(what), "%s, results, 2x, half-pixel", c.name);
wrong += BackdropOracle(what, c.sx, c.sy, results, half, 2560, 4096, frame2, BackdropRects);
snprintf(what, sizeof(what), "%s, results, y flipped", c.name);
wrong += BackdropOracle(what, c.sx, c.sy, results, flipped, 1280, 2048, frame, BackdropRects);
snprintf(what, sizeof(what), "%s, results, tile at y -256", c.name);
wrong += BackdropOracle(what, c.sx, c.sy, results, tiled, 1280, 2048, Rect{ 0, 0, 1280, 464 }, BackdropRects);
snprintf(what, sizeof(what), "%s, a picture over the band", c.name);
wrong += BackdropOracle(what, c.sx, c.sy, full, ps, 1280, 2048, frame, BackdropRects);
snprintf(what, sizeof(what), "%s, results, game scissor", c.name);
wrong += BackdropOracle(what, c.sx, c.sy, results, ps, 1280, 2048, Rect{ 100, 10, 1180, 700 }, BackdropRects);
}
printf("backdrop fill: %d wrong pixel(s) over %zu shapes\n", wrong, std::size(cases));
{
// The overscan rows at the 8 screen shapes, 1x and 2x (half-pixel
// centres): the results picture leaves rows of the band above and
// below it at every shape, 16:9 too, and each of them is filled
// once (the oracle above, which also counts them).
printf("\nbackdrop overscan rows:\n");
int rowsWrong = 0;
for (const Shape& sh : kShapes)
{
const Box results{ -kBackdropX * sh.sx, kBackdropX * sh.sx, -kBackdropY * sh.sy, kBackdropY * sh.sy };
char what[160];
int px1 = 0, px2 = 0;
snprintf(what, sizeof(what), "%s, overscan rows, 1x", sh.name);
rowsWrong += BackdropOracle(what, sh.sx, sh.sy, results, ps, 1280, 2048, frame, BackdropRects, true, &px1);
snprintf(what, sizeof(what), "%s, overscan rows, 2x, half-pixel", sh.name);
rowsWrong += BackdropOracle(what, sh.sx, sh.sy, results, half, 2560, 4096, frame2, BackdropRects, true, &px2);
// Rows: 0.019 of the band's half height each, ~7 * sy at 1x
// (5 at 4:3), across the band's width.
const int rowsMin = int(0.019f * sh.sy * 360.0f) - 1, wide = int(1280.0f * std::min(sh.sx, 1.0f)) - 2;
CHECK(px1 >= 2 * rowsMin * wide && px2 >= 4 * 2 * rowsMin * wide, "%s: overscan px %d (1x) %d (2x), want >= %d and %d", sh.name, px1,
px2, 2 * rowsMin * wide, 8 * rowsMin * wide);
}
printf("backdrop overscan rows: %d wrong pixel(s) over %zu shapes x 1x, 2x\n", rowsWrong, std::size(kShapes));
}
{
// The rectangles themselves, 2868x1320 at 1x: columns past the
// picture's sides (x +-1.104 band = 0.9033 clip: from column 1218,
// to 62), the corners past the band (0.8182 clip: column 1164)
// above and below it (y +-0.981: rows 0..6 and 713..719), and the
// overscan rows between them, inside the band (columns 116..1163).
Rect r[8]{};
const Box results{ -kBackdropX * 0.818182f, kBackdropX * 0.818182f, -kBackdropY, kBackdropY };
const uint32_t n = BackdropRects(results, ps, 1280, 2048, frame, 0.818182f, 1.0f, r);
const Rect want[8] = { { 1218, 0, 1280, 720 }, { 1164, 0, 1218, 7 }, { 1164, 713, 1218, 720 }, { 0, 0, 62, 720 }, { 62, 0, 116, 7 },
{ 62, 713, 116, 720 }, { 116, 0, 1164, 7 }, { 116, 713, 1164, 720 } };
CHECK(n == 8, "iPhone: %u rects, want 8", n);
for (uint32_t i = 0; i < n && i < 8; i++)
CHECK(Same(r[i], want[i]), "iPhone: rect %u [%d,%d)x[%d,%d), want [%d,%d)x[%d,%d)", i, r[i].x0, r[i].x1, r[i].y0, r[i].y1, want[i].x0,
want[i].x1, want[i].y0, want[i].y1);
// Vert+: the rows above and below the band, whole (the picture stops
// inside it in y and past the screen in x), then the band's overscan
// rows (y +-0.981 x 0.75 = 0.736: rows 90..94 and 625..629).
const Box r43{ -kBackdropX, kBackdropX, -kBackdropY * 0.75f, kBackdropY * 0.75f };
const uint32_t n43 = BackdropRects(r43, ps, 1280, 2048, frame, 1.0f, 0.75f, r);
CHECK(n43 == 4 && Same(r[0], Rect{ 0, 0, 1280, 90 }) && Same(r[1], Rect{ 0, 630, 1280, 720 }) && Same(r[2], Rect{ 0, 90, 1280, 95 }) &&
Same(r[3], Rect{ 0, 625, 1280, 630 }),
"4:3: %u rects [%d,%d)x[%d,%d) [%d,%d)x[%d,%d) ...", n43, r[0].x0, r[0].x1, r[0].y0, r[0].y1, r[2].x0, r[2].x1, r[2].y0, r[2].y1);
// 16:9: the overscan rows alone, rows 0..6 and 713..719, the whole
// width; none for a picture over the whole band.
const Box r169{ -kBackdropX, kBackdropX, -kBackdropY, kBackdropY };
const uint32_t n169 = BackdropRects(r169, ps, 1280, 2048, frame, 1.0f, 1.0f, r);
CHECK(n169 == 2 && Same(r[0], Rect{ 0, 0, 1280, 7 }) && Same(r[1], Rect{ 0, 713, 1280, 720 }), "16:9: %u rects [%d,%d)x[%d,%d) ...", n169,
r[0].x0, r[0].x1, r[0].y0, r[0].y1);
const Box whole{ -1.0f, 1.0f, -1.0f, 1.0f };
CHECK(BackdropRects(whole, ps, 1280, 2048, frame, 1.0f, 1.0f, r) == 0, "16:9: a picture over the whole band fills nothing at 1x");
// ... and at 2x with half-pixel centres only the top row, whose
// centre lies past clip 1 (the picture is one host row lower: its
// bottom edge is past the frame's).
const uint32_t n2 = BackdropRects(whole, half, 2560, 4096, frame2, 1.0f, 1.0f, r);
CHECK(n2 == 1 && Same(r[0], Rect{ 0, 0, 2560, 1 }), "16:9 2x: %u rects [%d,%d)x[%d,%d) ...", n2, r[0].x0, r[0].x1, r[0].y0, r[0].y1);
}
printf("\nplanted, backdrop fill (each must fail):\n");
{
auto run = [&](const char* what, BackdropRectsFn f) {
int bad = 0;
for (const Case& c : cases)
{
const Box results{ -kBackdropX * c.sx, kBackdropX * c.sx, -kBackdropY * c.sy, kBackdropY * c.sy };
bad += BackdropOracle(what, c.sx, c.sy, results, ps, 1280, 2048, frame, f, false);
bad += BackdropOracle(what, c.sx, c.sy, results, half, 2560, 4096, frame2, f, false);
}
printf("planted %s: %d wrong pixel(s)\n", what, bad);
CHECK(bad > 0, "planted %s: no wrong pixel", what);
};
run("the whole strips (v0.1.5's black frame fill)", [](const Box& b, const float* ps, float w, float h, const Rect& sc, float sx, float sy, Rect* out) {
return PastBandOnly(b, ps, w, h, sc, sx, sy, out);
});
run("past the picture only (no corners)", [](const Box& b, const float* ps, float w, float h, const Rect& sc, float sx, float sy, Rect* out) {
Rect r[8]{};
const uint32_t n = BackdropRects(b, ps, w, h, sc, sx, sy, r);
uint32_t m = 0;
for (uint32_t i = 0; i < n; i++)
if (r[i].y1 - r[i].y0 > 100 || sy < 1.0f)
out[m++] = r[i];
return m;
});
run("from the band's edge, under the picture", [](const Box& b, const float* ps, float w, float h, const Rect& sc, float sx, float sy, Rect* out) {
Box wider = b;
wider.x0 = std::max(b.x0, -sx);
wider.x1 = std::min(b.x1, sx);
return BackdropRects(wider, ps, w, h, sc, sx, sy, out);
});
// The overscan rows, planted: each must fail. runOverscan checks
// every shape at 1x and 2x; with `every`, each shape must fail (16:9
// included), else at least one (a fault only some shapes have).
auto runOverscan = [&](const char* what, BackdropRectsFn f, bool every) {
int bad = 0, shapesFailing = 0;
for (const Case& c : cases)
{
const Box results{ -kBackdropX * c.sx, kBackdropX * c.sx, -kBackdropY * c.sy, kBackdropY * c.sy };
const int here = BackdropOracle(what, c.sx, c.sy, results, ps, 1280, 2048, frame, f, false) +
BackdropOracle(what, c.sx, c.sy, results, half, 2560, 4096, frame2, f, false);
bad += here;
shapesFailing += here > 0;
}
printf("planted %s: %d wrong pixel(s), %d of %zu shapes\n", what, bad, shapesFailing, std::size(cases));
CHECK(every ? shapesFailing == int(std::size(cases)) : bad > 0, "planted %s: %d of %zu shapes failed", what, shapesFailing,
std::size(cases));
};
// 811cdf2's fill: past the band only, the overscan rows left.
runOverscan("no overscan rows (past the band only, as 811cdf2)", [](const Box& b, const float* ps, float w, float h, const Rect& sc, float sx,
float sy, Rect* out) { return PastBandRects(b, ps, w, h, sc, sx, sy, out); }, true);
// Rows only where something lies past the band: 16:9 left.
runOverscan("overscan rows only on wide and tall screens", [](const Box& b, const float* ps, float w, float h, const Rect& sc, float sx,
float sy, Rect* out) {
const uint32_t n = PastBandRects(b, ps, w, h, sc, sx, sy, out);
return n + (sx < 1.0f || sy < 1.0f ? OverscanRows(b, ps, w, h, sc, sx, sy, out + n) : 0u);
}, false);
// Rows to clip 1 where the band's edge is the screen's (no
// kFillEdge): at 2x the outermost row and column are left.
runOverscan("overscan rows to clip 1, not kFillEdge", [](const Box& b, const float* ps, float w, float h, const Rect& sc, float sx, float sy,
Rect* out) {
uint32_t n = PastBandRects(b, ps, w, h, sc, sx, sy, out);
const float bx = std::min(sx, 1.0f), by = std::min(sy, 1.0f);
for (const Rect& r : { ClipToHost(-bx, bx, b.y1, by, ps, w, h), ClipToHost(-bx, bx, -by, b.y0, ps, w, h) })
if (const Rect c = Intersect(r, sc); !Empty(c))
out[n++] = c;
return n;
}, false);
// Rows across the whole screen under Hor+: twice where the fill
// past the band has them already.
runOverscan("overscan rows across the whole screen", [](const Box& b, const float* ps, float w, float h, const Rect& sc, float sx, float sy,
Rect* out) {
const uint32_t n = PastBandRects(b, ps, w, h, sc, sx, sy, out);
return n + OverscanRows(b, ps, w, h, sc, 1.0f, sy, out + n);
}, false);
// Rows from just inside the picture: its own top and bottom rows
// blacked.
runOverscan("overscan rows from inside the picture", [](const Box& b, const float* ps, float w, float h, const Rect& sc, float sx, float sy,
Rect* out) {
Box in = b;
in.y0 += 0.01f;
in.y1 -= 0.01f;
const uint32_t n = PastBandRects(b, ps, w, h, sc, sx, sy, out);
return n + OverscanRows(in, ps, w, h, sc, sx, sy, out + n);
}, true);
// Rows short of the picture: a row of the race left above it.
runOverscan("overscan rows short of the picture", [](const Box& b, const float* ps, float w, float h, const Rect& sc, float sx, float sy,
Rect* out) {
Box out_ = b;
out_.y0 -= 0.01f;
out_.y1 += 0.01f;
const uint32_t n = PastBandRects(b, ps, w, h, sc, sx, sy, out);
return n + OverscanRows(out_, ps, w, h, sc, sx, sy, out + n);
}, true);
}
printf("\nbackdrop plan (the draw as the renderer hands it over):\n");
{
const Matrix iphone = FrontEndMatrix(0.818182f, 1.0f), deck = FrontEndMatrix(1.0f, 0.9f), m169 = FrontEndMatrix(1.0f, 1.0f);
const EPolys results = Polys(Rectangle(-kBackdropX, kBackdropX, -kBackdropY, kBackdropY, 0.818182f, 1.0f, kBackdropColour), iphone);
const Draw d = BackdropDraw(iphone, 0.818182f, 1.0f, ps);
const Rect want[8] = { { 1218, 0, 1280, 720 }, { 1164, 0, 1218, 7 }, { 1164, 713, 1218, 720 }, { 0, 0, 62, 720 }, { 62, 0, 116, 7 },
{ 62, 713, 116, 720 }, { 116, 0, 1164, 7 }, { 116, 713, 1164, 720 } };
BackdropPlanCase("iPhone results", d, results, Fill::Backdrop, 8, want);
// The Deck (1280x800): the rows past the band (0..35, 684..719),
// then its overscan rows (y 0.981 x 0.9 = 0.883: 36..41, 678..683).
const Rect deckWant[4] = { { 0, 0, 1280, 36 }, { 0, 684, 1280, 720 }, { 0, 36, 1280, 42 }, { 0, 678, 1280, 684 } };
BackdropPlanCase("Deck results", BackdropDraw(deck, 1.0f, 0.9f, ps),
Polys(Rectangle(-kBackdropX, kBackdropX, -kBackdropY, kBackdropY, 1.0f, 0.9f, kBackdropColour), deck), Fill::Backdrop, 4, deckWant);
// 16:9 too: its overscan rows alone (rows 0..6 and 713..719), also
// from a ppc/ without the Vert+ hooks (no front-end y scale).
const Draw d169 = BackdropDraw(m169, 1.0f, 1.0f, ps);
const EPolys results169 = Polys(Rectangle(-kBackdropX, kBackdropX, -kBackdropY, kBackdropY, 1.0f, 1.0f, kBackdropColour), m169);
const Rect want169[2] = { { 0, 0, 1280, 7 }, { 0, 713, 1280, 720 } };
BackdropPlanCase("16:9 results", d169, results169, Fill::Backdrop, 2, want169);
Draw old169 = d169;
old169.frontEndY = 0.0f;
BackdropPlanCase("16:9 results, no Vert+ hooks", old169, results169, Fill::Backdrop, 2, want169);
// ... and at 2x with half-pixel centres: the picture one host row
// lower (its top at 2 x 6.84 + 1 = 14.7, its bottom at 1427.3):
// rows 0..14 and 1427..1439.
float half169[4];
FrontEndPs(half169, 640.5f, 360.5f);
const Rect want169x2[2] = { { 0, 0, 2560, 15 }, { 0, 1427, 2560, 1440 } };
BackdropPlanCase("16:9 results, 2x", BackdropDraw(m169, 1.0f, 1.0f, half169, 2560.0f, 4096.0f, Rect{ 0, 0, 2560, 1440 }), results169,
Fill::Backdrop, 2, want169x2);
// Every register check refuses at 16:9 as elsewhere (a fade, a dim
// or the HUD never gets this far: MatchBackdrop's cases above run
// at 16:9 too).
auto refuse169 = [&](const char* what, auto change) {
Draw x = d169;
Matrix m = m169;
change(x, m);
x.c = m.c;
char name[96];
snprintf(name, sizeof(name), "16:9, %s", what);
BackdropPlanCase(name, x, results169, Fill::None);
};
refuse169("taken by another rule", [](Draw& x, Matrix&) { x.leftAlone = false; });
refuse169("not the front-end shader", [](Draw& x, Matrix&) { x.frontEndShader = false; });
refuse169("inside an occlusion query", [](Draw& x, Matrix&) { x.occlusion = true; });
refuse169("two quads", [](Draw& x, Matrix&) { x.count = 8; });
refuse169("additive blend", [](Draw& x, Matrix&) { x.blend0 = 0x01060106; });
refuse169("t0 not sampled", [](Draw& x, Matrix&) { x.t0Sampled = false; });
refuse169("t0 32x32 (the white texture)", [](Draw& x, Matrix&) { x.t0Width = x.t0Height = 32; });
refuse169("x translation c3", [](Draw&, Matrix& m) { m.c[3] = 0.01f; });
refuse169("c5 not the front end's y scale", [](Draw& x, Matrix&) { x.frontEndY = 0.83f; });
refuse169("VTE x offset off", [](Draw& x, Matrix&) { x.vte = 0x43D; });
EPolys faded169 = results169;
for (auto& w : faded169.w)
w[3] = 0xB87F7F7F;
BackdropPlanCase("16:9, alpha B8 (a fade-in)", d169, faded169, Fill::None);
// A flat full-band layer over the 32x32 white texture is not a
// picture (the black frame's and the fades' kind).
Draw flat = d;
flat.t0Width = flat.t0Height = 32;
BackdropPlanCase("over the white texture", flat, results, Fill::None);
// A translucent picture: vertex 0's alpha refuses before the rest.
EPolys faded = results;
for (auto& w : faded.w)
w[3] = 0xB87F7F7F;
BackdropPlanCase("alpha B8", d, faded, Fill::None);
auto refuse = [&](const char* what, auto change) {
Draw x = d;
Matrix m = iphone;
change(x, m);
x.c = m.c;
BackdropPlanCase(what, x, results, Fill::None);
};
refuse("taken by another rule", [](Draw& x, Matrix&) { x.leftAlone = false; });
refuse("not the front-end shader", [](Draw& x, Matrix&) { x.frontEndShader = false; });
refuse("inside an occlusion query", [](Draw& x, Matrix&) { x.occlusion = true; });
refuse("two quads", [](Draw& x, Matrix&) { x.count = 8; });
refuse("additive blend", [](Draw& x, Matrix&) { x.blend0 = 0x01060106; });
refuse("t0 not sampled", [](Draw& x, Matrix&) { x.t0Sampled = false; });
refuse("t0 64 high", [](Draw& x, Matrix&) { x.t0Height = 64; });
refuse("y translation c7", [](Draw&, Matrix& m) { m.c[7] = 0.01f; });
refuse("c5 not the front end's y scale", [](Draw& x, Matrix&) { x.frontEndY = 0.83f; });
refuse("VTE y offset off", [](Draw& x, Matrix&) { x.vte = 0x437; });
Draw opaque = d;
opaque.blend0 = 0x00010001;
BackdropPlanCase("blending off", opaque, results, Fill::Backdrop, 8, want);
// PlanFill (bars and black frames) never takes it: the renderer asks
// PlanBackdrop only when PlanFill found nothing.
CHECK(RunPlan(d, results).fill == Fill::None, "PlanFill took the backdrop");
}
}
int main()
{
printf("match:\n");
MatchCases(Rule{});
printf("\nguards:\n");
{
// The colour test the renderer asks of vertex 0 before it reads the
// other three (BarColour): black, alpha at least minAlpha.
CHECK(BarColour(0xFD000000), "FD000000 is a bar's colour");
CHECK(BarColour(0xF0000000), "F0000000 is a bar's colour (minAlpha)");
CHECK(!BarColour(0xEF000000), "EF000000 is below minAlpha");
CHECK(!BarColour(0xFD000001) && !BarColour(0xFD000100) && !BarColour(0xFD010000), "a colour other than black");
CHECK(!BarColour(0x00000000), "transparent black");
// No view scale (or a negative one): nothing matches, and the box
// is left as it was, as on any miss.
const Quad right = Rectangle(0.651f, 1.406f, -1.024f, 1.039f, 1.0f, 0.9f, 0xFD000000);
const Box sentinel{ 9.0f, 9.0f, 9.0f, 9.0f };
const float scales[][2] = { { 0.0f, 0.9f }, { 1.0f, 0.0f }, { -1.0f, 0.9f }, { 1.0f, -0.9f }, { -1.0f, -0.9f } };
for (const auto& sc : scales)
{
Box b = sentinel;
const Side got = MatchSideBar(right.v, sc[0], sc[1], Rule{}, &b);
CHECK(got == Side::None && b.x0 == 9.0f && b.y1 == 9.0f, "view scale %.1f %.1f: %s, box %s", sc[0], sc[1], Name(got),
b.x0 == 9.0f ? "kept" : "written");
}
Box b = sentinel;
const Quad bezel = Rectangle(-0.001f, 0.654f, -1.027f, 0.003f, 1.0f, 0.9f, 0xFB7D7D7D);
CHECK(MatchSideBar(bezel.v, 1.0f, 0.9f, Rule{}, &b) == Side::None && b.x0 == 9.0f, "a miss leaves the box as it was");
// ... also one that fails only the last test, the sides (a black
// column past both band edges, but neither inner nor outer edge fits).
const Quad column = Rectangle(0.20f, 0.60f, -1.024f, 1.039f, 1.0f, 0.9f, 0xFD000000);
CHECK(MatchSideBar(column.v, 1.0f, 0.9f, Rule{}, &b) == Side::None && b.x0 == 9.0f, "a miss on the sides leaves the box as it was");
CHECK(MatchSideBar(right.v, 1.0f, 0.9f, Rule{}, &b) == Side::Right && b.x0 == right.v[0].x && b.x1 == right.v[1].x &&
b.y0 == right.v[0].y && b.y1 == right.v[2].y,
"a match writes the bar's box");
CHECK(MatchSideBar(right.v, 1.0f, 0.9f) == Side::Right, "the box is optional");
printf("colour, view scale and box guards checked\n");
}
printf("\nplanted (each must fail a case):\n");
auto plant = [](const char* what, auto set) { Rule r; set(r); Planted(what, r); };
plant("pastMin 1.00", [](Rule& r) { r.pastMin = 1.00f; });
plant("pastMin 1.02", [](Rule& r) { r.pastMin = 1.02f; });
plant("pastMax 1.09", [](Rule& r) { r.pastMax = 1.09f; });
plant("pastMax 1.11", [](Rule& r) { r.pastMax = 1.11f; });
plant("innerMin 0.49", [](Rule& r) { r.innerMin = 0.49f; });
plant("innerMin 0.51", [](Rule& r) { r.innerMin = 0.51f; });
plant("innerMax 0.79", [](Rule& r) { r.innerMax = 0.79f; });
plant("innerMax 0.81", [](Rule& r) { r.innerMax = 0.81f; });
plant("outerMin 1.19", [](Rule& r) { r.outerMin = 1.19f; });
plant("outerMin 1.21", [](Rule& r) { r.outerMin = 1.21f; });
plant("outerMax 1.99", [](Rule& r) { r.outerMax = 1.99f; });
plant("outerMax 2.01", [](Rule& r) { r.outerMax = 2.01f; });
plant("minAlpha EF", [](Rule& r) { r.minAlpha = 0xEF; });
plant("minAlpha F1", [](Rule& r) { r.minAlpha = 0xF1; });
plant("cornerTolerance 0.02", [](Rule& r) { r.cornerTolerance = 0.02f; });
plant("cornerTolerance 0.0001", [](Rule& r) { r.cornerTolerance = 0.0001f; });
printf("\nfill:\n");
float ps[4], half[4], tiled[4], flipped[4], flippedHalf[4];
FrontEndPs(ps);
FrontEndPs(half, 640.5f, 360.5f); // D3D's half-pixel centre
FrontEndPs(tiled, 640.0f, 360.0f - 256.0f); // NFSMW_TILING=1, window y offset -256
FrontEndPs(flipped, 640.0f, 360.0f, 360.0f); // the viewport's y scale +360 (ps1 > 0)
FrontEndPs(flippedHalf, 640.5f, 360.5f, 360.0f);
const Rect frame{ 0, 0, 1280, 720 }, frame2{ 0, 0, 2560, 1440 }, frame3{ 0, 0, 3840, 2160 };
{
const Rect want[2] = { { 640, 0, 1280, 36 }, { 640, 684, 1280, 720 } };
FillBar("Deck right, 1x", Side::Right, 0.651f, 1.406f, 1.0f, 0.9f, ps, 1280, 2048, frame, 2, want);
FillBar("Deck right, 1x, half-pixel", Side::Right, 0.651f, 1.406f, 1.0f, 0.9f, half, 1280, 2048, frame, 2, want);
const Rect left[2] = { { 0, 0, 640, 36 }, { 0, 684, 640, 720 } };
FillBar("Deck left, 1x", Side::Left, -1.410f, -0.655f, 1.0f, 0.9f, ps, 1280, 2048, frame, 2, left);
const Rect want2[2] = { { 1280, 0, 2560, 72 }, { 1280, 1368, 2560, 1440 } };
FillBar("Deck right, 2x", Side::Right, 0.651f, 1.406f, 1.0f, 0.9f, ps, 2560, 4096, frame2, 2, want2);
const Rect r43[2] = { { 640, 0, 1280, 90 }, { 640, 630, 1280, 720 } };
FillBar("4:3 right", Side::Right, 0.651f, 1.406f, 1.0f, 0.75f, ps, 1280, 2048, frame, 2, r43);
FillBar("iPad Pro right", Side::Right, 0.651f, 1.406f, 1.0f, 0.750366f, ps, 1280, 2048, frame, 2, r43);
const Rect r32[2] = { { 640, 0, 1280, 56 }, { 640, 664, 1280, 720 } };
FillBar("3:2 right", Side::Right, 0.651f, 1.406f, 1.0f, 0.84375f, ps, 1280, 2048, frame, 2, r32);
// A tile (rows 256.. of the frame): only the bottom strip is in it.
const Rect tile{ 0, 0, 1280, 464 }, t[1] = { { 640, 428, 1280, 464 } };
FillBar("Deck right, tile at y -256", Side::Right, 0.651f, 1.406f, 1.0f, 0.9f, tiled, 1280, 2048, tile, 1, t);
// The game's own scissor narrower than the frame: kept.
const Rect game{ 100, 10, 1180, 700 }, g[2] = { { 640, 10, 1180, 36 }, { 640, 684, 1180, 700 } };
FillBar("Deck right, game scissor", Side::Right, 0.651f, 1.406f, 1.0f, 0.9f, ps, 1280, 2048, game, 2, g);
// Hor+: only past a 2.50:1 screen, the columns beyond the band.
const Rect w329[1] = { { 1920, 0, 2560, 1440 } }, w329l[1] = { { 0, 0, 640, 1440 } };
FillBar("32:9 right, 2x", Side::Right, 0.651f, 1.406f, 0.5f, 1.0f, ps, 2560, 4096, frame2, 1, w329);
FillBar("32:9 left, 2x", Side::Left, -1.410f, -0.655f, 0.5f, 1.0f, ps, 2560, 4096, frame2, 1, w329l);
FillBar("21:9 right", Side::Right, 0.651f, 1.406f, 0.75f, 1.0f, ps, 2560, 4096, frame2, 0, nullptr);
FillBar("21:9 left", Side::Left, -1.410f, -0.655f, 0.75f, 1.0f, ps, 2560, 4096, frame2, 0, nullptr);
FillBar("3440x1440 right", Side::Right, 0.651f, 1.406f, 0.744186f, 1.0f, ps, 2560, 4096, frame2, 0, nullptr);
const Rect w250[1] = { { 1095, 0, 1280, 720 } };
FillBar("2.51:1 right (bar ends at 0.9997)", Side::Right, 0.651f, 1.406f, 0.711f, 1.0f, ps, 1280, 2048, frame, 1, w250);
FillBar("16:9", Side::Right, 0.651f, 1.406f, 1.0f, 1.0f, ps, 1280, 2048, frame, 0, nullptr);
// Just short of 2.50:1 the bar ends past clip 1 (1.0100): the screen
// edge test is clip 1, not the fill's reach (kFillEdge).
FillBar("2.49:1 right (bar ends at 1.0100)", Side::Right, 0.651f, 1.406f, 0.71835f, 1.0f, ps, 1280, 2048, frame, 0, nullptr);
FillBar("2.49:1 left (bar ends at -1.0130)", Side::Left, -1.410f, -0.655f, 0.71845f, 1.0f, ps, 1280, 2048, frame, 0, nullptr);
// Half-pixel centres at 2x and 3x (viewport o(640.5,360.5)): the
// first row is filled (with fills ending at clip +-1 it was not).
const Rect h2[2] = { { 1281, 0, 2560, 73 }, { 1281, 1369, 2560, 1440 } };
FillBar("Deck right, 2x, half-pixel", Side::Right, 0.651f, 1.406f, 1.0f, 0.9f, half, 2560, 4096, frame2, 2, h2);
const Rect h2l[2] = { { 0, 0, 1281, 73 }, { 0, 1369, 1281, 1440 } };
FillBar("Deck left, 2x, half-pixel", Side::Left, -1.410f, -0.655f, 1.0f, 0.9f, half, 2560, 4096, frame2, 2, h2l);
const Rect h3[2] = { { 1921, 0, 3840, 109 }, { 1921, 2053, 3840, 2160 } };
FillBar("Deck right, 3x, half-pixel", Side::Right, 0.651f, 1.406f, 1.0f, 0.9f, half, 3840, 6144, frame3, 2, h3);
// 32:9 at 2x, half-pixel: the last column and the first row.
const Rect w329h[1] = { { 1921, 0, 2560, 1440 } }, w329hl[1] = { { 0, 0, 641, 1440 } };
FillBar("32:9 right, 2x, half-pixel", Side::Right, 0.651f, 1.406f, 0.5f, 1.0f, half, 2560, 4096, frame2, 1, w329h);
FillBar("32:9 left, 2x, half-pixel", Side::Left, -1.410f, -0.655f, 0.5f, 1.0f, half, 2560, 4096, frame2, 1, w329hl);
// The viewport's y scale flipped: the clip +y strip is the bottom rows.
const Rect fl[2] = { { 640, 684, 1280, 720 }, { 640, 0, 1280, 36 } };
FillBar("Deck right, 1x, y flipped", Side::Right, 0.651f, 1.406f, 1.0f, 0.9f, flipped, 1280, 2048, frame, 2, fl);
FillBar("no side", Side::None, 0.651f, 1.406f, 1.0f, 0.9f, ps, 1280, 2048, frame, 0, nullptr);
}
Oracle("Deck right 1x", Side::Right, 1.0f, 0.9f, ps, 1280, 2048, frame);
Oracle("Deck left 1x", Side::Left, 1.0f, 0.9f, ps, 1280, 2048, frame);
Oracle("4:3 right 2x", Side::Right, 1.0f, 0.75f, ps, 2560, 4096, frame2);
Oracle("3:2 left 1x", Side::Left, 1.0f, 0.84375f, ps, 1280, 2048, frame);
Oracle("Deck right, tile", Side::Right, 1.0f, 0.9f, tiled, 1280, 2048, Rect{ 0, 0, 1280, 464 });
// D3D's half-pixel centres at 2x and 3x: clip +-1 lands a host pixel in,
// so the first row and column are filled only because the fill reaches
// past the frame (kFillEdge) and the scissor trims it.
Oracle("Deck right 2x, half-pixel", Side::Right, 1.0f, 0.9f, half, 2560, 4096, frame2);
Oracle("Deck left 2x, half-pixel", Side::Left, 1.0f, 0.9f, half, 2560, 4096, frame2);
Oracle("Deck right 3x, half-pixel", Side::Right, 1.0f, 0.9f, half, 3840, 6144, frame3);
Oracle("Deck left 3x, half-pixel", Side::Left, 1.0f, 0.9f, half, 3840, 6144, frame3);
Oracle("32:9 right 2x, half-pixel", Side::Right, 0.5f, 1.0f, half, 2560, 4096, frame2);
Oracle("32:9 left 2x, half-pixel", Side::Left, 0.5f, 1.0f, half, 2560, 4096, frame2);
Oracle("32:9 right 1x", Side::Right, 0.5f, 1.0f, ps, 1280, 2048, frame);
// The viewport's y scale flipped (vp s y +360: ps1 > 0): the strips swap
// ends, the rows stay the same.
Oracle("Deck right 1x, y flipped", Side::Right, 1.0f, 0.9f, flipped, 1280, 2048, frame);
Oracle("Deck left 2x, y flipped, half-pixel", Side::Left, 1.0f, 0.9f, flippedHalf, 2560, 4096, frame2);
Complement("Deck 1x", 0.9f, ps, 1280, 2048, frame);
Complement("Deck 1x, half-pixel", 0.9f, half, 1280, 2048, frame);
Complement("3:2 1x, half-pixel", 0.84375f, half, 1280, 2048, frame);
Complement("iPad Pro 1x", 0.750366f, ps, 1280, 2048, frame);
Complement("4:3 3x", 0.75f, ps, 3840, 6144, Rect{ 0, 0, 3840, 2160 });
Complement("Deck, tile", 0.9f, tiled, 1280, 2048, Rect{ 0, 0, 1280, 464 });
Complement("Deck 2x, half-pixel", 0.9f, half, 2560, 4096, frame2);
Complement("Deck 3x, half-pixel", 0.9f, half, 3840, 6144, frame3);
Complement("Deck 1x, y flipped", 0.9f, flipped, 1280, 2048, frame);
Complement("4:3 2x, y flipped, half-pixel", 0.75f, flippedHalf, 2560, 4096, frame2);
printf("\ncover:\n");
Cover("Deck right", 0.651f, 1.406f, 1.0f, 0.9f, ps);
Cover("Deck left", -1.410f, -0.655f, 1.0f, 0.9f, ps);
Cover("Deck right, tile", 0.651f, 1.406f, 1.0f, 0.9f, tiled);
Cover("4:3 left, half-pixel", -1.410f, -0.655f, 1.0f, 0.75f, half);
Cover("32:9 right", 0.651f, 1.406f, 0.5f, 1.0f, ps);
Cover("32:9 left", -1.410f, -0.655f, 0.5f, 1.0f, ps);
Cover("Deck right, y flipped", 0.651f, 1.406f, 1.0f, 0.9f, flipped);
Cover("Deck left, 2x, half-pixel", -1.410f, -0.655f, 1.0f, 0.9f, half);
{
// The copy covers the whole frame (its host rectangle contains it).
const Box b{ 0.651f, 1.406f, -1.024f * 0.9f, 1.039f * 0.9f };
float q[4];
CoverFrame(b, ps, q);
const Rect r = ClipToHost(b.x0, b.x1, b.y0, b.y1, q, 1280, 2048);
CHECK(r.x0 <= 0 && r.y0 <= 0 && r.x1 >= 1280 && r.y1 >= 720, "Deck cover: [%d,%d)x[%d,%d) misses the frame", r.x0, r.x1, r.y0, r.y1);
}
printf("\nband rows:\n");
BandOracle("Deck 1x", 0.9f, ps, 2048, 720);
BandOracle("Deck 1x, half-pixel", 0.9f, half, 2048, 720);
BandOracle("3:2 1x", 0.84375f, ps, 2048, 720);
BandOracle("4:3 2x", 0.75f, ps, 4096, 1440);
BandOracle("iPad Pro 3x, half-pixel", 0.750366f, half, 6144, 2160);
BandOracle("Deck 1x, y flipped", 0.9f, flipped, 2048, 720);
BandOracle("Deck, tile", 0.9f, tiled, 2048, 464);
{
const Rows deck = BandRows(-0.9f, 0.9f, ps[1], ps[3], 2048), fl = BandRows(-0.9f, 0.9f, flipped[1], flipped[3], 2048);
CHECK(deck.top == 36 && deck.bottom == 684, "Deck band rows [%d,%d), want [36,684)", deck.top, deck.bottom);
CHECK(fl.top == 36 && fl.bottom == 684, "Deck band rows, y flipped [%d,%d), want [36,684)", fl.top, fl.bottom);
const Rows r43 = BandRows(-0.75f, 0.75f, ps[1], ps[3], 2048);
CHECK(r43.top == 90 && r43.bottom == 630, "4:3 band rows [%d,%d), want [90,630)", r43.top, r43.bottom);
}
printf("\nblack frame:\n");
BlackoutCases(BlackoutRule{});
{
// Guards: no view scale, and the box written on a match only.
const Quad f = Rectangle(-kFrameX, kFrameX, -kFrameY, kFrameY, 1.0f, 0.9f, kFrameColour);
Box b{ 9.0f, 9.0f, 9.0f, 9.0f };
CHECK(!MatchBlackout(f.v, 0.0f, 0.9f, BlackoutRule{}, &b) && !MatchBlackout(f.v, 1.0f, -0.9f, BlackoutRule{}, &b) && b.x0 == 9.0f,
"a black frame with no view scale");
const Quad bar = Rectangle(0.651f, 1.406f, -1.024f, 1.039f, 1.0f, 0.9f, 0xFD000000);
CHECK(!MatchBlackout(bar.v, 1.0f, 0.9f, BlackoutRule{}, &b) && b.x0 == 9.0f, "a miss leaves the box as it was");
CHECK(MatchBlackout(f.v, 1.0f, 0.9f, BlackoutRule{}, &b) && b.x0 == f.v[0].x && b.x1 == f.v[1].x && b.y0 == f.v[0].y && b.y1 == f.v[2].y,
"a match writes the frame's box");
// A bar is never a black frame, nor a black frame a bar.
CHECK(MatchSideBar(f.v, 1.0f, 0.9f) == Side::None, "a black frame is not a side bar");
}
printf("\nplanted, black frame (each must fail a case):\n");
auto plantFrame = [](const char* what, auto set) { BlackoutRule r; set(r); PlantedBlackout(what, r); };
plantFrame("centreMax 0", [](BlackoutRule& r) { r.centreMax = 0.0f; });
plantFrame("centreMax 0.02", [](BlackoutRule& r) { r.centreMax = 0.02f; });
plantFrame("extentMin 0.89", [](BlackoutRule& r) { r.extentMin = 0.89f; });
plantFrame("extentMin 0.91", [](BlackoutRule& r) { r.extentMin = 0.91f; });
plantFrame("extentMax 0.98", [](BlackoutRule& r) { r.extentMax = 0.98f; });
plantFrame("extentMax 1.00", [](BlackoutRule& r) { r.extentMax = 1.00f; });
plantFrame("minAlpha EF", [](BlackoutRule& r) { r.minAlpha = 0xEF; });
plantFrame("minAlpha F1", [](BlackoutRule& r) { r.minAlpha = 0xF1; });
plantFrame("cornerTolerance 0.02", [](BlackoutRule& r) { r.cornerTolerance = 0.02f; });
plantFrame("cornerTolerance 0.0001", [](BlackoutRule& r) { r.cornerTolerance = 0.0001f; });
printf("\nblack frame fill:\n");
{
// Everything outside the frame: the rows above and below it, then
// the columns beside it within its rows (v0.1.5: only past the band,
// the Deck's rows 0..35 and 684..719, nothing at 16:9).
const Rect deck[4] = { { 0, 0, 1280, 56 }, { 0, 664, 1280, 720 }, { 0, 56, 19, 664 }, { 1261, 56, 1280, 664 } };
FillFrame("Deck, 1x", 1.0f, 0.9f, ps, 1280, 2048, frame, 4, deck);
const Rect deckh[4] = { { 0, 0, 1280, 56 }, { 0, 665, 1280, 720 }, { 0, 56, 20, 665 }, { 1261, 56, 1280, 665 } };
FillFrame("Deck, 1x, half-pixel", 1.0f, 0.9f, half, 1280, 2048, frame, 4, deckh);
const Rect deck2[4] = { { 0, 0, 2560, 112 }, { 0, 1328, 2560, 1440 }, { 0, 112, 38, 1328 }, { 2522, 112, 2560, 1328 } };
FillFrame("Deck, 2x", 1.0f, 0.9f, ps, 2560, 4096, frame2, 4, deck2);
const Rect deck2h[4] = { { 0, 0, 2560, 113 }, { 0, 1329, 2560, 1440 }, { 0, 113, 39, 1329 }, { 2523, 113, 2560, 1329 } };
FillFrame("Deck, 2x, half-pixel", 1.0f, 0.9f, half, 2560, 4096, frame2, 4, deck2h);
const Rect r43[4] = { { 0, 0, 1280, 106 }, { 0, 614, 1280, 720 }, { 0, 106, 19, 614 }, { 1261, 106, 1280, 614 } };
FillFrame("4:3", 1.0f, 0.75f, ps, 1280, 2048, frame, 4, r43);
FillFrame("iPad Pro", 1.0f, 0.750366f, ps, 1280, 2048, frame, 4, r43);
const Rect r32[4] = { { 0, 0, 1280, 75 }, { 0, 645, 1280, 720 }, { 0, 75, 19, 645 }, { 1261, 75, 1280, 645 } };
FillFrame("3:2", 1.0f, 0.84375f, ps, 1280, 2048, frame, 4, r32);
const Rect t[3] = { { 0, 408, 1280, 464 }, { 0, 0, 19, 408 }, { 1261, 0, 1280, 408 } };
FillFrame("Deck, tile at y -256", 1.0f, 0.9f, tiled, 1280, 2048, Rect{ 0, 0, 1280, 464 }, 3, t);
const Rect g[2] = { { 100, 10, 1180, 56 }, { 100, 664, 1180, 700 } };
FillFrame("Deck, game scissor", 1.0f, 0.9f, ps, 1280, 2048, Rect{ 100, 10, 1180, 700 }, 2, g);
const Rect fl[4] = { { 0, 664, 1280, 720 }, { 0, 0, 1280, 56 }, { 0, 56, 19, 664 }, { 1261, 56, 1280, 664 } };
FillFrame("Deck, y flipped", 1.0f, 0.9f, flipped, 1280, 2048, frame, 4, fl);
const Rect w329[4] = { { 0, 0, 2560, 44 }, { 0, 1396, 2560, 1440 }, { 0, 44, 659, 1396 }, { 1901, 44, 2560, 1396 } };
FillFrame("32:9, 2x", 0.5f, 1.0f, ps, 2560, 4096, frame2, 4, w329);
const Rect w329h[4] = { { 0, 0, 2560, 45 }, { 0, 1397, 2560, 1440 }, { 0, 45, 660, 1397 }, { 1902, 45, 2560, 1397 } };
FillFrame("32:9, 2x, half-pixel", 0.5f, 1.0f, half, 2560, 4096, frame2, 4, w329h);
const Rect uw[4] = { { 0, 0, 1280, 22 }, { 0, 698, 1280, 720 }, { 0, 22, 174, 698 }, { 1106, 22, 1280, 698 } };
FillFrame("21:9", 0.75f, 1.0f, ps, 1280, 2048, frame, 4, uw);
const Rect u3440[4] = { { 0, 0, 1280, 22 }, { 0, 698, 1280, 720 }, { 0, 22, 178, 698 }, { 1102, 22, 1280, 698 } };
FillFrame("3440x1440", 0.744186f, 1.0f, ps, 1280, 2048, frame, 4, u3440);
const Rect r169[4] = { { 0, 0, 1280, 22 }, { 0, 698, 1280, 720 }, { 0, 22, 19, 698 }, { 1261, 22, 1280, 698 } };
FillFrame("16:9", 1.0f, 1.0f, ps, 1280, 2048, frame, 4, r169);
const Rect r169h[4] = { { 0, 0, 2560, 45 }, { 0, 1397, 2560, 1440 }, { 0, 45, 39, 1397 }, { 2523, 45, 2560, 1397 } };
FillFrame("16:9, 2x, half-pixel", 1.0f, 1.0f, half, 2560, 4096, frame2, 4, r169h);
}
FrameOracle("Deck 1x", 1.0f, 0.9f, ps, 1280, 2048, frame);
FrameOracle("Deck 3x, half-pixel", 1.0f, 0.9f, half, 3840, 6144, frame3);
FrameOracle("4:3 2x, y flipped, half-pixel", 1.0f, 0.75f, flippedHalf, 2560, 4096, frame2);
FrameOracle("Deck, tile", 1.0f, 0.9f, tiled, 1280, 2048, Rect{ 0, 0, 1280, 464 });
FrameOracle("Deck, game scissor", 1.0f, 0.9f, ps, 1280, 2048, Rect{ 100, 10, 1180, 700 });
FrameOracle("32:9 2x, half-pixel", 0.5f, 1.0f, half, 2560, 4096, frame2);
FrameOracle("21:9 1x", 0.75f, 1.0f, ps, 1280, 2048, frame);
FrameOracle("3440x1440 2x", 0.744186f, 1.0f, ps, 2560, 4096, frame2);
FrameOracle("16:9", 1.0f, 1.0f, ps, 1280, 2048, frame);
FrameOracle("16:9 2x, half-pixel", 1.0f, 1.0f, half, 2560, 4096, frame2);
FrameOracle("~57 s frame, Deck", 1.0f, 0.9f, ps, 1280, 2048, frame, RingRects, true, kFrameX, kFrameY57);
FrameOracle("Blacklist bio panel, 16:9", 1.0f, 1.0f, ps, 1280, 2048, frame, RingRects, true, kBioX, kBioY);
FrameOracle("Blacklist bio panel, Deck 2x, half-pixel", 1.0f, 0.9f, half, 2560, 4096, frame2, RingRects, true, kBioX, kBioY);
// Planted: the oracle catches v0.1.5's fill (past the band only) at
// every shape, 16:9 included, and a ring that leaves the frame's own
// rows' sides out.
{
struct Shape2
{
const char* name;
float sx, sy;
};
const Shape2 shapes[] = { { "Deck", 1.0f, 0.9f }, { "4:3", 1.0f, 0.75f }, { "16:9", 1.0f, 1.0f }, { "21:9", 0.75f, 1.0f }, { "32:9", 0.5f, 1.0f } };
int failing = 0;
for (const Shape2& sh : shapes)
failing += FrameOracle(sh.name, sh.sx, sh.sy, ps, 1280, 2048, frame, PastBandOnly, false) > 0;
printf("planted black frame fill 'past the band only (v0.1.5)': %d of %zu shapes fail\n", failing, std::size(shapes));
CHECK(failing == int(std::size(shapes)), "planted 'past the band only': %d of %zu shapes fail", failing, std::size(shapes));
const int rowsOnly = FrameOracle("rows only", 1.0f, 0.9f, ps, 1280, 2048, frame,
[](const Box& b, const float* q, float w, float h, const Rect& sc, float, float, Rect* out) {
Rect r[4]{};
const uint32_t n = BlackoutRects(b, q, w, h, sc, r);
uint32_t m = 0;
for (uint32_t i = 0; i < n && i < 2; i++)
out[m++] = r[i];
return m;
},
false);
printf("planted black frame fill 'rows above and below only': %d wrong pixel(s)\n", rowsOnly);
CHECK(rowsOnly > 0, "planted 'rows above and below only' passed");
}
Cover("black frame, Deck", -kFrameX, kFrameX, 1.0f, 0.9f, ps);
Cover("black frame, 32:9, half-pixel", -kFrameX, kFrameX, 0.5f, 1.0f, half);
printf("\nplan (the draws as dumped):\n");
{
const Matrix deck = FrontEndMatrix(1.0f, 0.9f), w329 = FrontEndMatrix(0.5f, 1.0f);
const EPolys rightDeck = Polys(Rectangle(0.651f, 1.406f, -1.024f, 1.039f, 1.0f, 0.9f, 0xFD000000), deck);
const EPolys leftDeck = Polys(Rectangle(-1.410f, -0.655f, -1.027f, 1.035f, 1.0f, 0.9f, 0xFD000000), deck);
const EPolys frameDeck = Polys(Rectangle(-kFrameX, kFrameX, -kFrameY, kFrameY, 1.0f, 0.9f, kFrameColour), deck);
const Draw d = S2Draw(deck, 1.0f, 0.9f, ps);
// The S2 run's "edge bars" line: right bar, first [640,1280)x[0,36).
const Rect right[2] = { { 640, 0, 1280, 36 }, { 640, 684, 1280, 720 } }, left[2] = { { 0, 0, 640, 36 }, { 0, 684, 640, 720 } };
PlanCase("Deck right bar", d, rightDeck, Fill::RightBar, 2, right);
PlanCase("Deck left bar", d, leftDeck, Fill::LeftBar, 2, left);
const Rect black[4] = { { 0, 0, 1280, 56 }, { 0, 664, 1280, 720 }, { 0, 56, 19, 664 }, { 1261, 56, 1280, 664 } };
PlanCase("Deck black frame", d, frameDeck, Fill::Blackout, 4, black);
// The Blacklist bio's panel (GitHub #19), as logged on the Deck:
// "black frame x -0.976..0.976 y -0.887..0.887 colour FB000000".
const Rect bio[4] = { { 0, 0, 1280, 41 }, { 0, 679, 1280, 720 }, { 0, 41, 15, 679 }, { 1265, 41, 1280, 679 } };
PlanCase("Deck Blacklist bio panel", d, Polys(Rectangle(-kBioX, kBioX, -kBioY, kBioY, 1.0f, 0.9f, 0xFB000000), deck), Fill::Blackout, 4, bio);
// ... fading in (11000000): left as it is, as before.
PlanCase("Deck Blacklist bio panel fading in", d, Polys(Rectangle(-kBioX, kBioX, -kBioY, kBioY, 1.0f, 0.9f, 0x11000000), deck), Fill::None);
{
const Plan p = RunPlan(d, rightDeck);
CHECK(std::fabs(p.box.x0 - 0.651f) < 1e-5f && std::fabs(p.box.x1 - 1.406f) < 1e-5f && std::fabs(p.box.y0 + 1.024f * 0.9f) < 1e-5f &&
std::fabs(p.box.y1 - 1.039f * 0.9f) < 1e-5f,
"Deck right bar: box x %.5f..%.5f y %.5f..%.5f", p.box.x0, p.box.x1, p.box.y0, p.box.y1);
}
// At 2x (the viewport and scissor in host pixels at twice the size).
const Rect right2[2] = { { 1280, 0, 2560, 72 }, { 1280, 1368, 2560, 1440 } };
PlanCase("Deck right bar, 2x", S2Draw(deck, 1.0f, 0.9f, ps, 2560, 4096, frame2), rightDeck, Fill::RightBar, 2, right2);
// 32:9 (2880x810, 2x): the bars past 2.50:1 and the black frame.
const EPolys right329 = Polys(Rectangle(0.651f, 1.406f, -1.024f, 1.039f, 0.5f, 1.0f, 0xFD000000), w329);
const EPolys frame329 = Polys(Rectangle(-kFrameX, kFrameX, -kFrameY, kFrameY, 0.5f, 1.0f, kFrameColour), w329);
const Draw d329 = S2Draw(w329, 0.5f, 1.0f, ps, 2560, 4096, frame2);
const Rect r329[1] = { { 1920, 0, 2560, 1440 } };
const Rect f329[4] = { { 0, 0, 2560, 44 }, { 0, 1396, 2560, 1440 }, { 0, 44, 659, 1396 }, { 1901, 44, 2560, 1396 } };
PlanCase("32:9 right bar", d329, right329, Fill::RightBar, 1, r329);
PlanCase("32:9 black frame", d329, frame329, Fill::Blackout, 4, f329);
// 21:9: the bars reach the screen edge (nothing to fill), the black
// frame does not.
const Matrix uw = FrontEndMatrix(0.75f, 1.0f);
const Draw duw = S2Draw(uw, 0.75f, 1.0f, ps);
PlanCase("21:9 right bar", duw, Polys(Rectangle(0.651f, 1.406f, -1.024f, 1.039f, 0.75f, 1.0f, 0xFD000000), uw), Fill::RightBar, 0, nullptr);
const Rect fuw[4] = { { 0, 0, 1280, 22 }, { 0, 698, 1280, 720 }, { 0, 22, 174, 698 }, { 1106, 22, 1280, 698 } };
PlanCase("21:9 black frame", duw, Polys(Rectangle(-kFrameX, kFrameX, -kFrameY, kFrameY, 0.75f, 1.0f, kFrameColour), uw), Fill::Blackout, 4,
fuw);
// 16:9 (the renderer asks PlanFill there too): the thin border round
// the frame; a vid-cam bar there fills nothing.
const Matrix m169 = FrontEndMatrix(1.0f, 1.0f);
const Rect f169[4] = { { 0, 0, 1280, 22 }, { 0, 698, 1280, 720 }, { 0, 22, 19, 698 }, { 1261, 22, 1280, 698 } };
PlanCase("16:9 black frame", S2Draw(m169, 1.0f, 1.0f, ps),
Polys(Rectangle(-kFrameX, kFrameX, -kFrameY, kFrameY, 1.0f, 1.0f, kFrameColour), m169), Fill::Blackout, 4, f169);
PlanCase("16:9 right bar", S2Draw(m169, 1.0f, 1.0f, ps), Polys(Rectangle(0.651f, 1.406f, -1.024f, 1.039f, 1.0f, 1.0f, 0xFD000000), m169),
Fill::RightBar, 0, nullptr);
// A ppc/ from before the Vert+ hooks reports no front-end y scale
// (0): Hor+ still fills (c5 the unnarrowed 0.92), a narrowed view
// can't happen there and is refused.
Draw old = d329;
old.frontEndY = 0.0f;
PlanCase("32:9 right bar, old ppc/", old, right329, Fill::RightBar, 1, r329);
Draw oldDeck = d;
oldDeck.frontEndY = 0.0f;
PlanCase("Deck right bar, old ppc/ (no y scale)", oldDeck, rightDeck, Fill::None);
// Accepted variants: blending off; c5 negative (its sign was never
// checked); a 1x1 t0.
Draw v = d;
v.blend0 = 0x00010001;
PlanCase("blend off", v, rightDeck, Fill::RightBar, 2, right);
Matrix neg = deck;
neg.c[5] = -neg.c[5];
PlanCase("c5 negative", S2Draw(neg, 1.0f, 0.9f, ps), Polys(Rectangle(0.651f, 1.406f, -1.039f, 1.024f, 1.0f, 0.9f, 0xFD000000), neg),
Fill::RightBar, 2, right);
v = d;
v.t0Width = v.t0Height = 1;
PlanCase("t0 1x1", v, rightDeck, Fill::RightBar, 2, right);
// Each register check refuses on its own (the right bar otherwise).
auto refuse = [&](const char* what, auto change) {
Draw x = d;
Matrix m = deck;
change(x, m);
x.c = m.c;
PlanCase(what, x, rightDeck, Fill::None);
};
refuse("a triangle list", [](Draw& x, Matrix&) { x.prim = 4; });
refuse("two quads", [](Draw& x, Matrix&) { x.count = 8; });
refuse("not the front-end shader", [](Draw& x, Matrix&) { x.frontEndShader = false; });
refuse("taken by another rule", [](Draw& x, Matrix&) { x.leftAlone = false; });
refuse("inside an occlusion query", [](Draw& x, Matrix&) { x.occlusion = true; });
refuse("x translation c3", [](Draw&, Matrix& m) { m.c[3] = 0.01f; });
refuse("c4", [](Draw&, Matrix& m) { m.c[4] = 0.01f; });
refuse("c6", [](Draw&, Matrix& m) { m.c[6] = 0.01f; });
refuse("y translation c7", [](Draw&, Matrix& m) { m.c[7] = 0.01f; });
refuse("c5 not the front end's y scale", [](Draw& x, Matrix&) { x.frontEndY = 0.83f; });
refuse("VTE x scale off", [](Draw& x, Matrix&) { x.vte = 0x43E; });
refuse("VTE x offset off", [](Draw& x, Matrix&) { x.vte = 0x43D; });
refuse("VTE y scale off", [](Draw& x, Matrix&) { x.vte = 0x43B; });
refuse("VTE y offset off", [](Draw& x, Matrix&) { x.vte = 0x437; });
refuse("additive blend", [](Draw& x, Matrix&) { x.blend0 = 0x01060106; });
refuse("another blend", [](Draw& x, Matrix&) { x.blend0 = 0x07060705; });
refuse("t0 not sampled", [](Draw& x, Matrix&) { x.t0Sampled = false; });
refuse("t0 64 wide", [](Draw& x, Matrix&) { x.t0Width = 64; });
refuse("t0 64 high", [](Draw& x, Matrix&) { x.t0Height = 64; });
// A vertex past the vertex buffer: nothing (the reader's words are
// still the bar's, so only the failure refuses).
PlanCase("vertex 0 past the buffer", d, rightDeck, Fill::None, 0, nullptr, 0);
PlanCase("vertex 3 past the buffer", d, rightDeck, Fill::None, 0, nullptr, 3);
// Reads: one for a quad that isn't black (vertex 0's colour), none
// when a register check refuses, 17 for a black one.
int reads = -1;
EPolys grey = rightDeck;
for (auto& w : grey.w)
w[3] = 0xFD7E7E7E;
RunPlan(d, grey, 4, &reads);
CHECK(reads == 1, "a grey quad: %d reads, want 1", reads);
Draw tri = d;
tri.prim = 4;
RunPlan(tri, rightDeck, 4, &reads);
CHECK(reads == 0, "a refused draw: %d reads, want 0", reads);
RunPlan(d, rightDeck, 4, &reads);
CHECK(reads == 17, "a bar: %d reads, want 17", reads);
}
BackdropTests();
printf("\n%s (%d failure%s)\n", g_failures ? "FAILED" : "all passed", g_failures, g_failures == 1 ? "" : "s");
return g_failures ? 1 : 0;
}