blob: 70e893f942a947ac62dbe053e8561eac367c3e4e [file]
/*
* Copyright 2026 Google LLC
*
* Use of this source code is governed by a BSD-style license that can be
* found in the LICENSE file.
*/
#include "include/core/SkMatrix.h"
#include "include/core/SkPath.h"
#include "include/core/SkPathBuilder.h"
#include "include/core/SkPoint.h"
#include "include/core/SkRect.h"
#include "include/core/SkString.h"
#include "include/private/SkTDArray.h"
#include "src/gpu/graphite/sparse_strips/Flatten.h"
#include "src/gpu/graphite/sparse_strips/MSAA_LUT.h"
#include "src/gpu/graphite/sparse_strips/MakeStrips.h"
#include "src/gpu/graphite/sparse_strips/Polyline.h"
#include "src/gpu/graphite/sparse_strips/Strip.h"
#include "src/gpu/graphite/sparse_strips/Tiler.h"
#include "tests/Test.h"
#include <array>
#include <cmath>
#include <cstdint>
namespace skgpu::graphite {
namespace {
// Simplified point-in-polygon verification using Even-Odd rule.
bool is_inside(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;
}
bool is_near_line(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;
}
template <uint16_t kTileWidth, uint16_t kTileHeight>
void print_diagnostics(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");
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");
out.append("Tile Intersections:\n");
for (const auto& tile : tiler.getTiles()) {
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());
}
out.appendf("\nASCII Map Tile(%d,%d) [Left: Expected | Right: Actual]\n",
failX / kTileWidth,
failY / kTileHeight);
SkString border("+");
for (int x = 0; x < kTileWidth; ++x) border.append("----------------+");
for (int y = 0; y < kTileHeight; ++y) {
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: Expected, 1: Actual
out.append("|");
for (int x = 0; x < kTileWidth; ++x) {
for (int sx = 0; sx < 8; ++sx) {
SkPoint cp = {failX + x + (sx + 0.5f) / 8.0f,
failY + y + (sy + 0.5f) / 8.0f};
bool onLine = is_near_line(cp, polyline, 0.6f / 8.0f);
bool active = false;
if (side == 0) {
active = is_inside(cp, polyline);
} else {
int32_t bufIdx = tileStartIdx + (y * kTileWidth + x);
uint8_t mask =
(bufIdx < actualMasks.size()) ? actualMasks[bufIdx] : 0;
active = (mask & (1 << sy));
}
if (MSAA_LUT<uint8_t>::kPattern[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());
INFOF(reporter, "%s", out.c_str());
}
} // namespace
template <uint16_t kTileWidth, uint16_t kTileHeight> class CoverageTestRunner {
public:
static constexpr float kTileWidthF = static_cast<float>(kTileWidth);
static constexpr float kTileHeightF = static_cast<float>(kTileHeight);
static constexpr uint32_t kViewportWidth = 400;
static constexpr uint32_t kViewportHeight = 400;
static constexpr float kViewportWidthF = static_cast<float>(kViewportWidth);
static constexpr float kViewportHeightF = static_cast<float>(kViewportHeight);
using StripFunc = void (*)(const Tiles<kTileWidth, kTileHeight>&,
SkTDArray<Strip>* stripBuf,
SkTDArray<uint8_t>* alphaBuf,
bool isInverse,
const Polyline& polyline,
const SkTDArray<uint8_t>& msaaLut,
MsaaExactMaskObserver observer);
static void RunScalarWinding(const Tiles<kTileWidth, kTileHeight>& tileContainer,
SkTDArray<Strip>* stripBuf,
SkTDArray<uint8_t>* alphaBuf,
bool isInverse,
const Polyline& polyline,
const SkTDArray<uint8_t>& maskLut,
MsaaExactMaskObserver observer) {
SkPathFillType fillType =
isInverse ? SkPathFillType::kInverseWinding : SkPathFillType::kWinding;
MakeStrips::MsaaScalar<kTileWidth, kTileHeight>(
tileContainer, stripBuf, alphaBuf, fillType, polyline, maskLut, observer);
}
static void RunSimdWinding(const Tiles<kTileWidth, kTileHeight>& tileContainer,
SkTDArray<Strip>* stripBuf,
SkTDArray<uint8_t>* alphaBuf,
bool isInverse,
const Polyline& polyline,
const SkTDArray<uint8_t>& maskLut,
MsaaExactMaskObserver observer) {
SkPathFillType fillType =
isInverse ? SkPathFillType::kInverseWinding : SkPathFillType::kWinding;
MakeStrips::MsaaSimd<kTileWidth, kTileHeight>(
tileContainer, stripBuf, alphaBuf, fillType, polyline, maskLut, observer);
}
CoverageTestRunner(StripFunc func, const char* implName) : fFunc(func), fImplName(implName) {}
void runAll(skiatest::Reporter* reporter) {
const SkTDArray<uint8_t> lut = GenerateMSAALUT<uint8_t>();
constexpr int kErrorLimit = 3;
std::array<uint32_t, kErrorLimit> minorErrorCount = {0, 0, 0};
int totalTestsRun = 0;
struct TestCase {
SkPath path;
const char* name;
};
std::vector<TestCase> baseGeometries;
auto addRect = [&](float w, float h, const char* name) {
baseGeometries.push_back(
{SkPathBuilder().addRect(SkRect::MakeWH(w, h)).detach(), name});
};
addRect(kTileWidthF * 0.5f, kTileHeightF * 0.5f, "Rect(Small)");
addRect(kTileWidthF, kTileHeightF, "Rect(ExactTile)");
addRect(kTileWidthF * 2.5f, kTileHeightF * 1.5f, "Rect(MultiTile)");
addRect(kTileWidthF * 4.0f, 0.2f, "Rect(HorizSliver)");
addRect(0.2f, kTileHeightF * 4.0f, "Rect(VertSliver)");
baseGeometries.push_back(
{SkPathBuilder()
.addCircle(kTileWidthF * 1.5f, kTileHeightF * 1.5f, kTileWidthF * 1.2f)
.detach(),
"Circle"});
baseGeometries.push_back({
SkPathBuilder().addOval(SkRect::MakeWH(kTileWidth * 4.0f, 0.5f)).detach(),
"ThinOval"
});
SkPathBuilder inset;
inset.addRect(SkRect::MakeWH(kTileWidthF * 3.0f, kTileHeightF * 3.0f),
SkPathDirection::kCW);
inset.addRect(SkRect::MakeXYWH(kTileWidthF, kTileHeightF, kTileWidthF, kTileHeightF),
SkPathDirection::kCCW);
baseGeometries.push_back({inset.detach(), "InsetRect"});
// Tile-relative alignments (dx, dy)
const SkPoint alignments[] = {
{0.0f, 0.0f}, // Top & Left aligned
{0.0f, kTileHeightF * 0.5f}, // Left aligned, offset top
{kTileWidthF * 0.5f, 0.0f}, // Top aligned, offset left
{kTileWidthF * 0.33f, kTileHeightF * 0.33f}, // Strictly inside
{kTileWidthF - 0.01f, kTileHeightF - 0.01f} // Right on a tile boundary edge
};
for (const TestCase& geom : baseGeometries) {
// Progressively rotate the geometry
for (int angleDeg = 0; angleDeg < 360; ++angleDeg) {
float angle = static_cast<float>(angleDeg);
for (const SkPoint& alignment : alignments) {
SkMatrix rotMatrix;
rotMatrix.setRotate(angle);
SkRect rotatedBounds = geom.path.makeTransform(rotMatrix).getBounds();
SkMatrix transMatrix;
transMatrix.setTranslate(alignment.fX - rotatedBounds.fLeft,
alignment.fY - rotatedBounds.fTop);
SkMatrix ctm = SkMatrix::Concat(transMatrix, rotMatrix);
SkPath deviceSpacePath = geom.path.makeTransform(ctm);
SkString testName;
testName.printf("%s - %s Rot(%.1f) Align(%.2f,%.2f)",
fImplName,
geom.name,
angle,
alignment.fX,
alignment.fY);
if (!this->runSingleTest(reporter,
deviceSpacePath,
testName.c_str(),
lut,
&minorErrorCount)) {
return;
}
totalTestsRun++;
}
}
}
INFOF(reporter,
"[%s (%dx%d)] Coverage LUT Test Complete. Ran %d variants. "
"Minor Error Summary: 1-sample: %u, 2-sample: %u, 3-sample: %u\n",
fImplName,
kTileWidth,
kTileHeight,
totalTestsRun,
minorErrorCount[0],
minorErrorCount[1],
minorErrorCount[2]);
}
private:
StripFunc fFunc;
const char* fImplName;
bool runSingleTest(skiatest::Reporter* reporter,
const SkPath& path,
const char* name,
const SkTDArray<uint8_t>& lut,
std::array<uint32_t, 3>* minorErrorCount) {
Flatten flattener;
Polyline polyline;
flattener.processPaths<FlattenMode::kSimd>(
path, SkMatrix(), kViewportWidthF, kViewportHeightF, &polyline);
Tiles<kTileWidth, kTileHeight> tiler;
tiler.makeTilesMSAA(polyline, kViewportWidth, kViewportHeight);
tiler.sortTiles();
SkTDArray<Strip> stripBuf;
SkTDArray<uint8_t> alphaBuf;
SkTDArray<uint8_t> exactMasks;
auto observer = [&](uint8_t exactMask) { exactMasks.push_back(exactMask); };
fFunc(tiler, &stripBuf, &alphaBuf, /*isInverse=*/false, polyline, lut, observer);
if (stripBuf.empty()) {
bool bufferSizeMatch = alphaBuf.empty();
REPORTER_ASSERT(
reporter, bufferSizeMatch, "[%s] No strips but alpha buffer has data.", name);
return bufferSizeMatch;
}
int32_t alphaIdx = 0;
for (int32_t i = 0; i < stripBuf.size() - 1; ++i) {
const Strip& curr = stripBuf[i];
const Strip& next = stripBuf[i + 1];
uint32_t startIdx = curr.alphaIndex();
uint32_t endIdx = next.alphaIndex();
uint16_t spannedTiles = (endIdx - startIdx) / (kTileWidth * kTileHeight);
uint16_t currX = curr.fX;
uint16_t currY = curr.fY;
for (int32_t s = 0; s < spannedTiles; ++s) {
int32_t tileStartIdx = alphaIdx;
for (int32_t y = 0; y < kTileHeight; ++y) {
for (int32_t x = 0; x < kTileWidth; ++x) {
uint8_t expectedMask = 0;
int expectedSamples = 0;
for (int k = 0; k < 8; ++k) {
if (is_inside(
{currX + x + (MSAA_LUT<uint8_t>::kPattern[k] + 0.5f) / 8.0f,
currY + y + (k + 0.5f) / 8.0f},
polyline)) {
expectedSamples++;
expectedMask |= (1 << k);
}
}
uint8_t actualMask =
(alphaIdx < exactMasks.size()) ? exactMasks[alphaIdx] : 0;
uint8_t actualAlpha = (alphaIdx < alphaBuf.size()) ? alphaBuf[alphaIdx] : 0;
int sampleDiff = 0;
int actualSamples = 0;
for (int k = 0; k < 8; ++k) {
if (actualMask & (1 << k)) actualSamples++;
if ((expectedMask & (1 << k)) != (actualMask & (1 << k))) sampleDiff++;
}
uint8_t expectedAlphaFromMask =
static_cast<uint8_t>((actualSamples * 255 + 4) / 8);
if (actualAlpha != expectedAlphaFromMask) {
REPORTER_ASSERT(
reporter,
false,
"[%s] Alpha Reduction Mismatch at tile(%d,%d) pixel(%d,%d). "
"Observer tracked %d active bits (expected alpha %d), "
"but AlphaBuf output was %d.",
name,
currX / kTileWidth,
currY / kTileHeight,
x,
y,
actualSamples,
expectedAlphaFromMask,
actualAlpha);
return false;
}
if (sampleDiff > 3) {
print_diagnostics(reporter,
polyline,
tiler,
currX,
currY,
exactMasks,
tileStartIdx);
REPORTER_ASSERT(reporter,
false,
"[%s] Fail at tile(%d,%d). Exp %d, Got %d (alpha %d)",
name,
currX / kTileWidth,
currY / kTileHeight,
expectedSamples,
actualSamples,
actualAlpha);
return false;
} else if (sampleDiff > 0) {
(*minorErrorCount)[sampleDiff - 1]++;
}
alphaIdx++;
}
}
currX += kTileWidth;
}
}
bool bufferSizeMatch = (alphaIdx == alphaBuf.size());
REPORTER_ASSERT(reporter,
bufferSizeMatch,
"[%s] Checked %d alpha bytes but buffer size is %d",
name,
alphaIdx,
alphaBuf.size());
return bufferSizeMatch;
}
};
DEF_TEST(SparseStrips_CoverageScalar_4x4, reporter) {
skgpu::graphite::CoverageTestRunner<4, 4> scalarRunner(
&skgpu::graphite::CoverageTestRunner<4, 4>::RunScalarWinding, "Scalar");
scalarRunner.runAll(reporter);
}
DEF_TEST(SparseStrips_CoverageSIMD_4x4, reporter) {
skgpu::graphite::CoverageTestRunner<4, 4> simdRunner(
&skgpu::graphite::CoverageTestRunner<4, 4>::RunSimdWinding, "SIMD");
simdRunner.runAll(reporter);
}
DEF_TEST(SparseStrips_CoverageSIMD_8x8, reporter) {
skgpu::graphite::CoverageTestRunner<8, 8> simdRunner(
&skgpu::graphite::CoverageTestRunner<8, 8>::RunSimdWinding, "SIMD");
simdRunner.runAll(reporter);
}
} // namespace skgpu::graphite