// 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 #include #include #include #include #include #include #include // On Windows gpu/renderer.cpp includes the header after , 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 #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; }