mirror of
https://github.com/SpeedBreakerProject/speedbreaker.git
synced 2026-10-17 18:03:04 +02:00
Co-authored-by: Lydacious <81742991+lydacious@users.noreply.github.com> Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
1297 lines
76 KiB
C++
1297 lines
76 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
|
|
// past the 16:9 band, 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);
|
|
}
|
|
|
|
// 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 a black frame's whole width.
|
|
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, every column: each pixel of the scissor is in
|
|
// exactly one of the band's rows and the fill.
|
|
Rect k[2]{};
|
|
const uint32_t kn = BlackoutRects(ps, hostW, hostH, scissor, 1.0f, sy, 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 = (y >= top && y < bottom) ? 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 band clip and the black frame's 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; and the one at the
|
|
// intro's start (~57 s): y +-0.920, FE000000.
|
|
constexpr float kFrameX = 0.970f, kFrameY = 0.939f, kFrameY57 = 0.920f;
|
|
constexpr uint32_t kFrameColour = 0xFF000000;
|
|
|
|
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)
|
|
{
|
|
Rect got[2]{};
|
|
const uint32_t n = BlackoutRects(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);
|
|
}
|
|
|
|
// The black frame's fill against pixel centres: a pixel of the scissor is
|
|
// filled exactly when its centre is outside the band (|y| > sy under Vert+,
|
|
// |x| > sx under Hor+); ties within 1/256 pixel skipped, as above.
|
|
static void FrameOracle(const char* what, float sx, float sy, const float ps[4], float hostW, float hostH, const Rect& scissor)
|
|
{
|
|
Rect r[2]{};
|
|
const uint32_t n = BlackoutRects(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];
|
|
const bool want = sy < 1.0f ? std::fabs(cy) > sy : sx < 1.0f ? std::fabs(cx) > sx : false;
|
|
const bool onEdge = sy < 1.0f ? tie(std::fabs(cy) - sy, pxY) : sx < 1.0f && 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 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);
|
|
}
|
|
|
|
// ---- 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
|
|
// past the band and outside the box is filled, exactly once; nothing in the
|
|
// band or the box is (ties within 1/256 pixel skipped). `rects` makes the
|
|
// rectangles (BackdropRects, or a planted fault). Returns the wrong pixels.
|
|
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)
|
|
{
|
|
Rect r[6]{};
|
|
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;
|
|
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;
|
|
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 != ((inBand || inBox) ? 0 : 1))
|
|
{
|
|
if (print && wrong < 3 && std::getenv("EDGE_BARS_DEBUG"))
|
|
printf(" wrong px %d,%d: clip %.5f %.5f, band %d box %d filled %d\n", x, y, cx, cy, inBand, inBox, filled);
|
|
wrong++;
|
|
}
|
|
}
|
|
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 pixel(s) wrong (%d on an edge, skipped)\n", wrong, ties);
|
|
CHECK(wrong == 0, "%s: %d pixels filled wrongly (in the band or the box, 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 } };
|
|
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 rectangles themselves, 2868x1320 at 1x: columns past the
|
|
// picture's sides (x +-1.104 band = 0.9033 clip: from column 1218,
|
|
// to 62), and the corners past the band (0.8182 clip: column 1164)
|
|
// above and below it (y +-0.981: rows 0..6 and 713..719).
|
|
Rect r[6]{};
|
|
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[6] = { { 1218, 0, 1280, 720 }, { 1164, 0, 1218, 7 }, { 1164, 713, 1218, 720 }, { 0, 0, 62, 720 }, { 62, 0, 116, 7 },
|
|
{ 62, 713, 116, 720 } };
|
|
CHECK(n == 6, "iPhone: %u rects, want 6", n);
|
|
for (uint32_t i = 0; i < n && i < 6; 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).
|
|
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 == 2 && Same(r[0], Rect{ 0, 0, 1280, 90 }) && Same(r[1], Rect{ 0, 630, 1280, 720 }), "4:3: %u rects [%d,%d)x[%d,%d) ...", n43,
|
|
r[0].x0, r[0].x1, r[0].y0, r[0].y1);
|
|
// 16:9: nothing.
|
|
const Box r169{ -kBackdropX, kBackdropX, -kBackdropY, kBackdropY };
|
|
CHECK(BackdropRects(r169, ps, 1280, 2048, frame, 1.0f, 1.0f, r) == 0, "16:9: a backdrop fills nothing");
|
|
}
|
|
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 (the black frame's fill)", [](const Box&, const float* ps, float w, float h, const Rect& sc, float sx, float sy, Rect* out) {
|
|
return BlackoutRects(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[6]{};
|
|
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);
|
|
});
|
|
}
|
|
|
|
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[6] = { { 1218, 0, 1280, 720 }, { 1164, 0, 1218, 7 }, { 1164, 713, 1218, 720 }, { 0, 0, 62, 720 }, { 62, 0, 116, 7 },
|
|
{ 62, 713, 116, 720 } };
|
|
BackdropPlanCase("iPhone results", d, results, Fill::Backdrop, 6, want);
|
|
const Rect deckWant[2] = { { 0, 0, 1280, 36 }, { 0, 684, 1280, 720 } };
|
|
BackdropPlanCase("Deck results", BackdropDraw(deck, 1.0f, 0.9f, ps),
|
|
Polys(Rectangle(-kBackdropX, kBackdropX, -kBackdropY, kBackdropY, 1.0f, 0.9f, kBackdropColour), deck), Fill::Backdrop, 2, deckWant);
|
|
// Never at 16:9 (nothing past the band to fill).
|
|
BackdropPlanCase("16:9 results", BackdropDraw(m169, 1.0f, 1.0f, ps),
|
|
Polys(Rectangle(-kBackdropX, kBackdropX, -kBackdropY, kBackdropY, 1.0f, 1.0f, kBackdropColour), m169), 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, 6, 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");
|
|
{
|
|
const Rect deck[2] = { { 0, 0, 1280, 36 }, { 0, 684, 1280, 720 } };
|
|
FillFrame("Deck, 1x", 1.0f, 0.9f, ps, 1280, 2048, frame, 2, deck);
|
|
FillFrame("Deck, 1x, half-pixel", 1.0f, 0.9f, half, 1280, 2048, frame, 2, deck);
|
|
const Rect deck2[2] = { { 0, 0, 2560, 72 }, { 0, 1368, 2560, 1440 } };
|
|
FillFrame("Deck, 2x", 1.0f, 0.9f, ps, 2560, 4096, frame2, 2, deck2);
|
|
const Rect deck2h[2] = { { 0, 0, 2560, 73 }, { 0, 1369, 2560, 1440 } };
|
|
FillFrame("Deck, 2x, half-pixel", 1.0f, 0.9f, half, 2560, 4096, frame2, 2, deck2h);
|
|
const Rect r43[2] = { { 0, 0, 1280, 90 }, { 0, 630, 1280, 720 } };
|
|
FillFrame("4:3", 1.0f, 0.75f, ps, 1280, 2048, frame, 2, r43);
|
|
FillFrame("iPad Pro", 1.0f, 0.750366f, ps, 1280, 2048, frame, 2, r43);
|
|
const Rect r32[2] = { { 0, 0, 1280, 56 }, { 0, 664, 1280, 720 } };
|
|
FillFrame("3:2", 1.0f, 0.84375f, ps, 1280, 2048, frame, 2, r32);
|
|
const Rect t[1] = { { 0, 428, 1280, 464 } };
|
|
FillFrame("Deck, tile at y -256", 1.0f, 0.9f, tiled, 1280, 2048, Rect{ 0, 0, 1280, 464 }, 1, t);
|
|
const Rect g[2] = { { 100, 10, 1180, 36 }, { 100, 684, 1180, 700 } };
|
|
FillFrame("Deck, game scissor", 1.0f, 0.9f, ps, 1280, 2048, Rect{ 100, 10, 1180, 700 }, 2, g);
|
|
const Rect fl[2] = { { 0, 684, 1280, 720 }, { 0, 0, 1280, 36 } };
|
|
FillFrame("Deck, y flipped", 1.0f, 0.9f, flipped, 1280, 2048, frame, 2, fl);
|
|
const Rect w329[2] = { { 1920, 0, 2560, 1440 }, { 0, 0, 640, 1440 } };
|
|
FillFrame("32:9, 2x", 0.5f, 1.0f, ps, 2560, 4096, frame2, 2, w329);
|
|
const Rect w329h[2] = { { 1921, 0, 2560, 1440 }, { 0, 0, 641, 1440 } };
|
|
FillFrame("32:9, 2x, half-pixel", 0.5f, 1.0f, half, 2560, 4096, frame2, 2, w329h);
|
|
const Rect uw[2] = { { 1120, 0, 1280, 720 }, { 0, 0, 160, 720 } };
|
|
FillFrame("21:9", 0.75f, 1.0f, ps, 1280, 2048, frame, 2, uw);
|
|
const Rect u3440[2] = { { 1116, 0, 1280, 720 }, { 0, 0, 164, 720 } };
|
|
FillFrame("3440x1440", 0.744186f, 1.0f, ps, 1280, 2048, frame, 2, u3440);
|
|
FillFrame("16:9", 1.0f, 1.0f, ps, 1280, 2048, frame, 0, nullptr);
|
|
}
|
|
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("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);
|
|
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[2] = { { 0, 0, 1280, 36 }, { 0, 684, 1280, 720 } };
|
|
PlanCase("Deck black frame", d, frameDeck, Fill::Blackout, 2, black);
|
|
{
|
|
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 } }, f329[2] = { { 1920, 0, 2560, 1440 }, { 0, 0, 640, 1440 } };
|
|
PlanCase("32:9 right bar", d329, right329, Fill::RightBar, 1, r329);
|
|
PlanCase("32:9 black frame", d329, frame329, Fill::Blackout, 2, 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[2] = { { 1120, 0, 1280, 720 }, { 0, 0, 160, 720 } };
|
|
PlanCase("21:9 black frame", duw, Polys(Rectangle(-kFrameX, kFrameX, -kFrameY, kFrameY, 0.75f, 1.0f, kFrameColour), uw), Fill::Blackout, 2,
|
|
fuw);
|
|
// 16:9 never gets here (the renderer's block needs a view scale
|
|
// below 1), and a plan there fills nothing.
|
|
const Matrix m169 = FrontEndMatrix(1.0f, 1.0f);
|
|
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, 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;
|
|
}
|