| /* | |
| * Copyright 2026 Google LLC | |
| * | |
| * Use of this source code is governed by a BSD-style license that can be | |
| * found in the LICENSE file. | |
| */ | |
| #ifndef skgpu_graphite_sparse_strips_CoverageTestUtils_DEFINED | |
| #define skgpu_graphite_sparse_strips_CoverageTestUtils_DEFINED | |
| #include "include/core/SkPoint.h" | |
| #include "include/core/SkString.h" | |
| #include "include/private/SkTDArray.h" | |
| #include "src/core/SkVx.h" | |
| #include "src/gpu/graphite/sparse_strips/MSAA_LUT.h" | |
| #include "src/gpu/graphite/sparse_strips/Polyline.h" | |
| #include "src/gpu/graphite/sparse_strips/SparseStripsTypes.h" | |
| #include "src/gpu/graphite/sparse_strips/Tiler.h" | |
| #include "tests/Test.h" | |
| #include "tests/graphite/sparse_strips/Oracle.h" | |
| #include <algorithm> | |
| #include <cmath> | |
| #include <cstdint> | |
| #include <vector> | |
| namespace skgpu::graphite { | |
| class CoverageTestUtils { | |
| public: | |
| CoverageTestUtils() = delete; | |
| // Simplified point-in-polygon verification using Even-Odd rule. | |
| static bool PointInPolygon(SkPoint pt, const Polyline& polyline) { | |
| bool inside = false; | |
| for (auto it = polyline.begin(); it != polyline.end(); ++it) { | |
| auto [line, idx] = *it; | |
| if ((line.p0.fY > pt.fY) != (line.p1.fY > pt.fY)) { | |
| float t = (pt.fY - line.p0.fY) / (line.p1.fY - line.p0.fY); | |
| float xInt = line.p0.fX + t * (line.p1.fX - line.p0.fX); | |
| if (pt.fX < xInt) { | |
| inside = !inside; | |
| } | |
| } | |
| } | |
| return inside; | |
| } | |
| // Prints ASCII map diagnostic comparing expected polygon coverage against actual rasterized | |
| // mask. | |
| template <uint16_t kTileWidth, uint16_t kTileHeight> | |
| static void PrintCoverageDiagnostics(skiatest::Reporter* reporter, | |
| const Polyline& polyline, | |
| const Tiles<kTileWidth, kTileHeight>& tiler, | |
| int failX, | |
| int failY, | |
| const SkTDArray<uint8_t>& actualMasks, | |
| size_t tileStartIdx) { | |
| SkString out("\n--- FAILURE DIAGNOSTICS ---\n"); | |
| AppendGeometryLines(&out, &polyline); | |
| AppendTileIntersections(&out, &tiler); | |
| AppendTileAsciiMap<kTileWidth, kTileHeight>( | |
| &out, | |
| failX, | |
| failY, | |
| &polyline, | |
| [&](int px, int py, int sy, SkPoint cp) { return PointInPolygon(cp, polyline); }, | |
| [&](int px, int py, int sy, uint16_t localX, uint16_t localY) { | |
| size_t bufIdx = tileStartIdx + (localY * kTileWidth + localX); | |
| return (bufIdx < static_cast<size_t>(actualMasks.size())) && | |
| ((actualMasks[bufIdx] & (1 << sy)) != 0); | |
| }); | |
| INFOF(reporter, "%s", out.c_str()); | |
| } | |
| // Prints ASCII map diagnostic comparing Oracle expected coverage against actual winding. | |
| template <uint16_t kTileWidth, uint16_t kTileHeight> | |
| static void PrintWindingDiagnostics(skiatest::Reporter* reporter, | |
| const char* testName, | |
| uint16_t tileStartX, | |
| uint16_t tileStartY, | |
| uint16_t px, | |
| uint16_t py, | |
| skvx::int8 actualWinding, | |
| skvx::int8 oracleWinding, | |
| const Polyline* polyline, | |
| const Tiles<kTileWidth, kTileHeight>* tiler, | |
| const SkTDArray<skvx::int8>& exactWindings, | |
| uint32_t tileStartAlphaIdx, | |
| ScanlineOracle8x* oracle) { | |
| SkString out; | |
| out.appendf("\n=== MISMATCH DIAGNOSTIC [%s] ===\n", testName); | |
| out.appendf("Location: Tile(%d,%d), Pixel(%d,%d) [Global px=%u, py=%u]\n", | |
| static_cast<int>(tileStartX / kTileWidth), | |
| static_cast<int>(tileStartY / kTileHeight), | |
| static_cast<int>(px - tileStartX), | |
| static_cast<int>(py - tileStartY), | |
| px, | |
| py); | |
| out.appendf("Actual Winding: [%d, %d, %d, %d, %d, %d, %d, %d]\n", | |
| actualWinding[0], actualWinding[1], actualWinding[2], actualWinding[3], | |
| actualWinding[4], actualWinding[5], actualWinding[6], actualWinding[7]); | |
| out.appendf("Oracle Winding: [%d, %d, %d, %d, %d, %d, %d, %d]\n", | |
| oracleWinding[0], oracleWinding[1], oracleWinding[2], oracleWinding[3], | |
| oracleWinding[4], oracleWinding[5], oracleWinding[6], oracleWinding[7]); | |
| AppendGeometryLines(&out, polyline, px, py); | |
| AppendTileIntersections(&out, tiler, tileStartX / kTileWidth, tileStartY / kTileHeight); | |
| std::vector<ScanlineOracle8x::RowWindingInterval> oracleIntervals; | |
| int cachedRow = -1; | |
| AppendTileAsciiMap<kTileWidth, kTileHeight>( | |
| &out, | |
| tileStartX, | |
| tileStartY, | |
| polyline, | |
| [&](int currX, int currY, int sy, SkPoint cp) { | |
| if (oracle) { | |
| if (cachedRow != currY) { | |
| oracleIntervals = oracle->buildRowIntervals(currY); | |
| cachedRow = currY; | |
| } | |
| skvx::int8 expW = | |
| ScanlineOracle8x::GetOracleWinding(currX, oracleIntervals); | |
| return (expW[sy] != 0); | |
| } | |
| return false; | |
| }, | |
| [&](int currX, int currY, int sy, uint16_t localX, uint16_t localY) { | |
| uint32_t bufIdx = tileStartAlphaIdx + (localY * kTileWidth + localX); | |
| return (bufIdx < static_cast<uint32_t>(exactWindings.size())) && | |
| (exactWindings[bufIdx][sy] != 0); | |
| }, | |
| "Oracle Expected", | |
| "Actual"); | |
| INFOF(reporter, "%s", out.c_str()); | |
| } | |
| private: | |
| static void AppendGeometryLines(SkString* out, | |
| const Polyline* polyline, | |
| int nearPx = -1, | |
| int nearPy = -1) { | |
| if (!polyline) { | |
| return; | |
| } | |
| if (nearPx < 0) { | |
| out->append("Geometry Lines: {\n"); | |
| for (auto it = polyline->begin(); it != polyline->end(); ++it) { | |
| auto [l, idx] = *it; | |
| out->appendf(" {{%f, %f}, {%f, %f}},\n", l.p0.fX, l.p0.fY, l.p1.fX, l.p1.fY); | |
| } | |
| out->append("}\n\n"); | |
| } else { | |
| out->append("Polyline Lines near pixel:\n"); | |
| for (auto it = polyline->begin(); it != polyline->end(); ++it) { | |
| auto [l, idx] = *it; | |
| float minX = std::min(l.p0.fX, l.p1.fX); | |
| float maxX = std::max(l.p0.fX, l.p1.fX); | |
| float minY = std::min(l.p0.fY, l.p1.fY); | |
| float maxY = std::max(l.p0.fY, l.p1.fY); | |
| if (maxX >= nearPx - 1 && minX <= nearPx + 2 && maxY >= nearPy - 1 && | |
| minY <= nearPy + 2) { | |
| out->appendf(" Line %d: (%.2f, %.2f) -> (%.2f, %.2f)\n", | |
| idx, l.p0.fX, l.p0.fY, l.p1.fX, l.p1.fY); | |
| } | |
| } | |
| } | |
| } | |
| template <uint16_t kTileWidth, uint16_t kTileHeight> | |
| static void AppendTileIntersections(SkString* out, | |
| const Tiles<kTileWidth, kTileHeight>* tiler, | |
| int tileX = -1, | |
| int tileY = -1) { | |
| if (!tiler) { | |
| return; | |
| } | |
| out->append("Tile Intersections:\n"); | |
| for (const auto& tile : tiler->getTiles()) { | |
| if (tileX < 0 || (tile.x == tileX && tile.y == tileY)) { | |
| uint32_t mask = tile.intersectionMask(); | |
| uint32_t lineIdx = tile.lineIdx(); | |
| out->appendf(" Tile(%u,%u) Line %u Mask: %s\n", | |
| tile.x, | |
| tile.y, | |
| lineIdx, | |
| IntersectionBits::MaskToString(mask).c_str()); | |
| } | |
| } | |
| } | |
| // Checks if a point is within threshold distance to any line in polyline. | |
| static bool PointNearLine(SkPoint pt, const Polyline& polyline, float threshold) { | |
| for (auto it = polyline.begin(); it != polyline.end(); ++it) { | |
| auto [line, idx] = *it; | |
| float l2 = (line.p0.fX - line.p1.fX) * (line.p0.fX - line.p1.fX) + | |
| (line.p0.fY - line.p1.fY) * (line.p0.fY - line.p1.fY); | |
| float dist = 0.0f; | |
| if (l2 == 0.0f) { | |
| float dx = pt.fX - line.p0.fX; | |
| float dy = pt.fY - line.p0.fY; | |
| dist = std::sqrt(dx * dx + dy * dy); | |
| } else { | |
| float t = std::max(0.0f, | |
| std::min(1.0f, | |
| ((pt.fX - line.p0.fX) * (line.p1.fX - line.p0.fX) + | |
| (pt.fY - line.p0.fY) * (line.p1.fY - line.p0.fY)) / | |
| l2)); | |
| float projX = line.p0.fX + t * (line.p1.fX - line.p0.fX); | |
| float projY = line.p0.fY + t * (line.p1.fY - line.p0.fY); | |
| dist = std::sqrt((pt.fX - projX) * (pt.fX - projX) + | |
| (pt.fY - projY) * (pt.fY - projY)); | |
| } | |
| if (dist <= threshold) { | |
| return true; | |
| } | |
| } | |
| return false; | |
| } | |
| // Formats a side-by-side ASCII map comparing expected and actual tile coverage. | |
| template <uint16_t kTileWidth, uint16_t kTileHeight, typename ExpectedFn, typename ActualFn> | |
| static void AppendTileAsciiMap(SkString* out, | |
| uint16_t tileStartX, | |
| uint16_t tileStartY, | |
| const Polyline* polyline, | |
| ExpectedFn&& isExpectedActive, | |
| ActualFn&& isActualActive, | |
| const char* leftLabel = "Expected", | |
| const char* rightLabel = "Actual") { | |
| out->appendf("\nASCII Map Tile(%d,%d) [Left: %s | Right: %s]\n", | |
| static_cast<int>(tileStartX / kTileWidth), | |
| static_cast<int>(tileStartY / kTileHeight), | |
| leftLabel, | |
| rightLabel); | |
| SkString border("+"); | |
| for (int x = 0; x < kTileWidth; ++x) { | |
| border.append("----------------+"); | |
| } | |
| for (uint16_t localY = 0; localY < kTileHeight; ++localY) { | |
| uint16_t currY = tileStartY + localY; | |
| out->appendf("%s %s\n", border.c_str(), border.c_str()); | |
| for (int sy = 0; sy < 8; ++sy) { | |
| for (int side = 0; side < 2; ++side) { // 0: Left/Expected, 1: Right/Actual | |
| out->append("|"); | |
| for (uint16_t localX = 0; localX < kTileWidth; ++localX) { | |
| uint16_t currX = tileStartX + localX; | |
| for (int sx = 0; sx < 8; ++sx) { | |
| SkPoint cp = {currX + (sx + 0.5f) / 8.0f, currY + (sy + 0.5f) / 8.0f}; | |
| bool onLine = | |
| polyline ? PointNearLine(cp, *polyline, 0.6f / 8.0f) : false; | |
| bool active = | |
| (side == 0) ? isExpectedActive(currX, currY, sy, cp) | |
| : isActualActive(currX, currY, sy, localX, localY); | |
| if (kMsaaPattern<SparseStripConfig::SubSampleType>[sy] == sx) { | |
| out->append(active ? (onLine ? "*#" : " #") | |
| : (onLine ? "*o" : " o")); | |
| } else { | |
| out->append(onLine ? "**" : " "); | |
| } | |
| } | |
| out->append("|"); | |
| } | |
| if (side == 0) { | |
| out->append(" "); | |
| } | |
| } | |
| out->append("\n"); | |
| } | |
| } | |
| out->appendf("%s %s\n", border.c_str(), border.c_str()); | |
| } | |
| }; | |
| } // namespace skgpu::graphite | |
| #endif // skgpu_graphite_sparse_strips_CoverageTestUtils_DEFINED |