blob: 141ac050b3b6983e26b3af98962d8113806da848 [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 "tests/graphite/sparse_strips/FastValidator.h"
#include "include/core/SkMatrix.h"
#include "include/core/SkPath.h"
#include "include/core/SkPathBuilder.h"
#include "include/core/SkPathTypes.h"
#include "include/core/SkPoint.h"
#include "include/core/SkRect.h"
#include "include/core/SkString.h"
#include "include/gpu/graphite/Recorder.h"
#include "include/private/SkTDArray.h"
#include "src/core/SkVx.h"
#include "src/gpu/graphite/geom/EndCaps.h"
#include "src/gpu/graphite/geom/WideTiles.h"
#include "src/gpu/graphite/sparse_strips/AlphaAtlasManager.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/SparseStripsTypes.h"
#include "src/gpu/graphite/sparse_strips/Tiler.h"
#include "src/gpu/graphite/sparse_strips/WindingHistogram.h"
#include "tests/Test.h"
#include "tests/graphite/sparse_strips/CoverageTestUtils.h"
#include <algorithm>
#include <array>
#include <cmath>
#include <utility>
#include <vector>
namespace skgpu::graphite {
namespace {
struct LineHit {
skvx::float8 x;
skvx::int8 dir; // +1 downward, -1 upward, 0 inactive
};
template <uint16_t kTileHeight>
std::vector<std::vector<Line>> build_tile_row_line_buckets(const Polyline& polyline,
int clipBottom) {
constexpr float kTileHeightF = static_cast<float>(kTileHeight);
uint16_t numTileRows = (clipBottom + kTileHeight - 1) / kTileHeight;
std::vector<std::vector<Line>> buckets(numTileRows);
for (auto it = polyline.begin(); it != polyline.end(); ++it) {
auto [line, idx] = *it;
float minY = std::min(line.p0.fY, line.p1.fY);
float maxY = std::max(line.p0.fY, line.p1.fY);
if (maxY < 0.0f || minY >= static_cast<float>(clipBottom)) {
continue;
}
int startTileY = std::max(0, static_cast<int>(std::floor(minY / kTileHeightF)));
int endTileY = std::min(static_cast<int>(numTileRows) - 1,
static_cast<int>(std::floor(maxY / kTileHeightF)));
for (int ty = startTileY; ty <= endTileY; ++ty) {
buckets[ty].push_back(line);
}
}
return buckets;
}
void collect_row_hits(int py,
const std::vector<Line>& candidateLines,
std::vector<LineHit>* rowHits) {
static const skvx::float8 kSubY =
(skvx::float8{0.0f, 1.0f, 2.0f, 3.0f, 4.0f, 5.0f, 6.0f, 7.0f} + 0.5f) / 8.0f;
skvx::float8 vPy = static_cast<float>(py) + kSubY;
rowHits->clear();
for (const Line& line : candidateLines) {
float dy = line.p1.fY - line.p0.fY;
if (dy == 0.0f) {
continue;
}
float invDy = 1.0f / dy;
float dx = line.p1.fX - line.p0.fX;
int32_t dir = dy > 0.0f ? 1 : -1;
// Each line covers the sub-scanlines in [minY, maxY) regardless of its direction, so a
// sub-scanline passing exactly through a vertex where the path turns vertically sees both
// or neither of its lines, rather than just one, which would leave a nonzero winding
// extending to the right of the path.
float minY = std::min(line.p0.fY, line.p1.fY);
float maxY = std::max(line.p0.fY, line.p1.fY);
auto valid = (vPy >= minY) & (vPy < maxY);
if (any(valid)) {
skvx::float8 t = (vPy - line.p0.fY) * invDy;
LineHit hit;
hit.x = line.p0.fX + t * dx;
hit.dir = if_then_else(valid, skvx::int8(dir), skvx::int8(0));
rowHits->push_back(hit);
}
}
}
enum class TileKind : uint8_t { kEmpty, kEndCap, kWide };
// EndCaps only cover the tiles that edges pass through, so this validates the rest of the clip:
// every tile whose subsamples are all inside the path must be covered by exactly one WideTile, and
// every tile whose subsamples are all outside must be empty. A tile with subsamples on both sides
// of the path must be an EndCap. Rather than evaluating every subsample, this relies on the winding
// of each sub-scanline being constant between the crossings of the lines. Every tile outside of the
// clip must be empty.
template <uint16_t kTileWidth, uint16_t kTileHeight>
bool validate_solid_tiles(skiatest::Reporter* reporter,
const std::vector<std::vector<Line>>& lineBuckets,
const WideTiles& wides,
const EndCaps& ends,
const skvx::float8& subX,
const SkIRect& clip,
bool isEvenOdd,
bool isInverse,
const char* testName) {
// The grid spans from the origin to the clip, so that tiles left of or above the clip can be
// detected. The clip's left and top are tile aligned.
const int minTileCol = clip.left() / kTileWidth;
const int minTileRow = clip.top() / kTileHeight;
const int numTileCols = (clip.right() + kTileWidth - 1) / kTileWidth;
const int numTileRows = (clip.bottom() + kTileHeight - 1) / kTileHeight;
// Rasterize the strip output into the tile grid, verifying that each tile is emitted at most
// once and lies within the clip.
std::vector<TileKind> tileKinds(numTileCols * numTileRows, TileKind::kEmpty);
auto markTiles = [&](uint16_t x, uint16_t y, uint16_t width, TileKind kind) {
const char* type = kind == TileKind::kWide ? "WideTile" : "EndCap";
const int right = x + width;
// Note: WideTiles may end at the right edge of the clip rather than a tile boundary.
if (x % kTileWidth != 0 || y % kTileHeight != 0 || width == 0 ||
(right % kTileWidth != 0 && right != clip.right())) {
REPORTER_ASSERT(reporter,
false,
"[%s] %s (x:%d, y:%d, width:%d) is not tile aligned",
testName,
type,
x,
y,
width);
return false;
}
const int row = y / kTileHeight;
const int startCol = x / kTileWidth;
const int endCol = (right + kTileWidth - 1) / kTileWidth;
if (row < minTileRow || row >= numTileRows || startCol < minTileCol ||
endCol > numTileCols) {
REPORTER_ASSERT(reporter,
false,
"[%s] %s (x:%d, y:%d, width:%d) lies outside of the clip "
"(l:%d, t:%d, r:%d, b:%d)",
testName,
type,
x,
y,
width,
clip.left(),
clip.top(),
clip.right(),
clip.bottom());
return false;
}
for (int col = startCol; col < endCol; ++col) {
TileKind& tileKind = tileKinds[row * numTileCols + col];
if (tileKind != TileKind::kEmpty) {
REPORTER_ASSERT(reporter,
false,
"[%s] %s (x:%d, y:%d, width:%d) overlaps another tile at "
"tile(%d,%d)",
testName,
type,
x,
y,
width,
col,
row);
return false;
}
tileKind = kind;
}
return true;
};
for (const EndCaps::EndCap& cap : ends.caps()) {
if (!markTiles(cap.fX, cap.fY, cap.fWidth, TileKind::kEndCap)) {
return false;
}
}
for (const WideTiles::WideTile& wide : wides.tiles()) {
if (!markTiles(wide.fX, wide.fY, wide.fWidth, TileKind::kWide)) {
return false;
}
}
// Per-row prefix counts of the WideTiles and EndCaps, so that each span of constant winding can
// be checked in constant time.
std::vector<int> widePrefix(numTileCols + 1, 0);
std::vector<int> endCapPrefix(numTileCols + 1, 0);
// Verifies that the tiles overlapping pixels [startX, endX) of pixel row `py`, across which the
// winding of subsample `k` is constant, are consistent with that winding.
auto checkSpan = [&](int row, int py, int k, int startX, int endX, int winding) {
if (startX >= endX) {
return true;
}
const int startCol = startX / kTileWidth;
const int endCol = (endX - 1) / kTileWidth + 1;
const int numWides = widePrefix[endCol] - widePrefix[startCol];
const int numEndCaps = endCapPrefix[endCol] - endCapPrefix[startCol];
bool inside = isEvenOdd ? (winding & 1) != 0 : winding != 0;
if (isInverse) {
inside = !inside;
}
if (inside ? numWides + numEndCaps == endCol - startCol : numWides == 0) {
return true;
}
const TileKind expectedKind = inside ? TileKind::kWide : TileKind::kEmpty;
for (int col = startCol; col < endCol; ++col) {
const TileKind kind = tileKinds[row * numTileCols + col];
if (kind != TileKind::kEndCap && kind != expectedKind) {
REPORTER_ASSERT(reporter,
false,
"[%s] tile(%d,%d) is %s, but subsample %d of pixel(%d,%d) is %s "
"the path (winding %d)",
testName,
col,
row,
kind == TileKind::kWide ? "a WideTile" : "empty",
k,
std::max(startX, col * kTileWidth),
py,
inside ? "inside" : "outside",
winding);
return false;
}
}
SkDEBUGFAIL("Mismatched tile counts without a mismatched tile");
return false;
};
std::vector<LineHit> rowHits;
std::vector<std::pair<float, int>> crossings;
for (int row = minTileRow; row < numTileRows; ++row) {
const TileKind* rowKinds = &tileKinds[row * numTileCols];
for (int col = 0; col < numTileCols; ++col) {
widePrefix[col + 1] = widePrefix[col] + (rowKinds[col] == TileKind::kWide ? 1 : 0);
endCapPrefix[col + 1] =
endCapPrefix[col] + (rowKinds[col] == TileKind::kEndCap ? 1 : 0);
}
const std::vector<Line>& candidateLines = lineBuckets[row];
if (candidateLines.empty()) {
// Nothing crosses this row, so the winding is zero everywhere.
if (!checkSpan(row,
row * kTileHeight,
/*k=*/0,
clip.left(),
clip.right(),
/*winding=*/0)) {
return false;
}
continue;
}
const int endY = std::min<int>((row + 1) * kTileHeight, clip.bottom());
for (int py = row * kTileHeight; py < endY; ++py) {
collect_row_hits(py, candidateLines, &rowHits);
for (int k = 0; k < 8; ++k) {
crossings.clear();
for (const LineHit& hit : rowHits) {
if (hit.dir[k] != 0) {
crossings.push_back({hit.x[k], hit.dir[k]});
}
}
std::sort(crossings.begin(), crossings.end());
// Returns the first pixel whose subsample `k` is affected by a crossing at `x`,
// which exactly matches the `vPx >= th.x` test of the EndCap validation. Crossings
// left of the clip affect its leftmost pixel.
const float sx = subX[k];
auto firstAffectedPixel = [&](float x) {
// Clamp before converting, since crossings may lie far outside of the clip.
int px = static_cast<int>(std::clamp(std::ceil(x - sx),
static_cast<float>(clip.left()),
static_cast<float>(clip.right())));
while (px > clip.left() && static_cast<float>(px - 1) + sx >= x) {
--px;
}
while (px < clip.right() && static_cast<float>(px) + sx < x) {
++px;
}
return px;
};
int winding = 0;
int spanStart = clip.left();
for (size_t i = 0; i < crossings.size();) {
const int px = firstAffectedPixel(crossings[i].first);
if (!checkSpan(row, py, k, spanStart, px, winding)) {
return false;
}
while (i < crossings.size() && firstAffectedPixel(crossings[i].first) <= px) {
winding += crossings[i].second;
++i;
}
spanStart = std::max(spanStart, px);
}
if (!checkSpan(row, py, k, spanStart, clip.right(), winding)) {
return false;
}
}
}
}
return true;
}
} // namespace
template <uint16_t kTileWidth, uint16_t kTileHeight>
void FastValidator<kTileWidth, kTileHeight>::RunScalarWinding(
const Tiles<kTileWidth, kTileHeight>& tileContainer,
WideTiles* wides,
EndCaps* ends,
AlphaAtlasManager* atlasManager,
SkPathFillType fillType,
const Polyline& polyline,
const SparseStripConfig::LUTArray& maskLut,
bool isCulled,
const WindingHistogram& windingHistogram,
const SkIRect& clipRect,
MsaaExactMaskObserver observer) {
MakeStrips::MsaaScalar<kTileWidth, kTileHeight>(tileContainer,
wides,
ends,
atlasManager,
fillType,
polyline,
maskLut,
isCulled,
windingHistogram,
clipRect,
observer);
}
template <uint16_t kTileWidth, uint16_t kTileHeight>
void FastValidator<kTileWidth, kTileHeight>::RunSimdWinding(
const Tiles<kTileWidth, kTileHeight>& tileContainer,
WideTiles* wides,
EndCaps* ends,
AlphaAtlasManager* atlasManager,
SkPathFillType fillType,
const Polyline& polyline,
const SparseStripConfig::LUTArray& maskLut,
bool isCulled,
const WindingHistogram& windingHistogram,
const SkIRect& clipRect,
MsaaExactMaskObserver observer) {
MakeStrips::MsaaSimd<kTileWidth, kTileHeight>(tileContainer,
wides,
ends,
atlasManager,
fillType,
polyline,
maskLut,
isCulled,
windingHistogram,
clipRect,
observer);
}
template <uint16_t kTileWidth, uint16_t kTileHeight>
bool FastValidator<kTileWidth, kTileHeight>::ValidateStripOutput(
skiatest::Reporter* reporter,
const SkPath& path,
const Polyline& polyline,
const Polyline& unculledPolyline,
const Tiles<kTileWidth, kTileHeight>& tiler,
const WideTiles& wides,
const EndCaps& ends,
const AlphaAtlasManager& atlasManager,
const SkTDArray<uint8_t>& exactMasks,
const SkIRect& clip,
SkPathFillType fillType,
const char* testName,
std::array<uint32_t, 3>* minorErrorCount) {
if (ends.empty() && !exactMasks.empty()) {
REPORTER_ASSERT(reporter, false, "[%s] No endcaps but mask observer has data.", testName);
return false;
}
static const skvx::float8 kSubX =
(skvx::cast<float>(skvx::byte8::Load(kMsaaPattern<uint8_t>.data())) + 0.5f) / 8.0f;
bool isEvenOdd = SkPathFillType_IsEvenOdd(fillType);
bool isInverse = SkPathFillType_IsInverse(fillType);
auto lineBuckets = build_tile_row_line_buckets<kTileHeight>(unculledPolyline, clip.bottom());
int32_t maskIdx = 0;
int cachedPy = -1;
std::vector<LineHit> rowHits;
for (const auto& cap : ends.caps()) {
uint16_t spannedTiles = cap.fWidth / kTileWidth;
uint16_t currX = cap.fX;
uint16_t currY = cap.fY;
int tileRowY = currY / kTileHeight;
const auto& candidateLines = (tileRowY < static_cast<int>(lineBuckets.size()))
? lineBuckets[tileRowY]
: std::vector<Line>();
const uint8_t* pageData = atlasManager.getPageData(cap.fTexPage);
int32_t capAlphaOffset = cap.fAlphaIndex;
for (int32_t s = 0; s < spannedTiles; ++s) {
uint16_t tileStartX = currX + s * kTileWidth;
float tileMinX = static_cast<float>(tileStartX);
float tileMaxX = static_cast<float>(tileStartX + kTileWidth);
int32_t tileStartIdx = maskIdx;
for (int32_t y = 0; y < kTileHeight; ++y) {
int py = currY + y;
if (py != cachedPy) {
collect_row_hits(py, candidateLines, &rowHits);
cachedPy = py;
}
// Partition hits for this tile of interest:
// - hits entirely left of tile contribute to baseWinding
// - hits crossing inside tile are kept in tileHits
// - hits entirely right of tile are ignored
skvx::int8 baseWinding(0);
std::vector<LineHit> tileHits;
for (const auto& hit : rowHits) {
auto isLeft = (hit.dir != 0) & (hit.x < tileMinX);
auto isRight = (hit.dir != 0) & (hit.x >= tileMaxX);
baseWinding += if_then_else(isLeft, hit.dir, skvx::int8(0));
auto inTile = (hit.dir != 0) & (!isLeft) & (!isRight);
if (any(inTile)) {
LineHit th;
th.x = hit.x;
th.dir = if_then_else(inTile, hit.dir, skvx::int8(0));
tileHits.push_back(th);
}
}
for (int32_t x = 0; x < kTileWidth; ++x) {
float px = static_cast<float>(tileStartX + x);
skvx::float8 vPx = px + kSubX;
skvx::int8 pixelWinding = baseWinding;
for (const auto& th : tileHits) {
pixelWinding += if_then_else(vPx >= th.x, th.dir, skvx::int8(0));
}
uint8_t expectedMask = 0;
int expectedSamples = 0;
for (int k = 0; k < 8; ++k) {
int w = pixelWinding[k];
bool inside = isEvenOdd ? ((w & 1) != 0) : (w != 0);
if (isInverse) {
inside = !inside;
}
if (inside) {
expectedSamples++;
expectedMask |= (1 << k);
}
}
uint8_t actualMask = (maskIdx < exactMasks.size()) ? exactMasks[maskIdx] : 0;
int32_t alphaOffset = capAlphaOffset + (s * kTileHeight + y) * kTileWidth + x;
uint8_t actualAlpha = pageData ? pageData[alphaOffset] : 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) {
CoverageTestUtils::PrintCoverageDiagnostics(reporter,
polyline,
tiler,
tileStartX,
currY,
exactMasks,
tileStartIdx);
REPORTER_ASSERT(
reporter,
false,
"[%s] Alpha Reduction Mismatch at tile(%d,%d) pixel(%d,%d). "
"Observer tracked %d active bits (expected alpha %d), "
"but AlphaAtlas output was %d.",
testName,
tileStartX / kTileWidth,
currY / kTileHeight,
x,
y,
actualSamples,
expectedAlphaFromMask,
actualAlpha);
return false;
}
// Pixels outside of the clip are never drawn, and the geometry that only
// affects them may have been culled, so only their alpha reduction is checked.
const bool inClip = clip.contains(tileStartX + x, py);
if (inClip && sampleDiff > 3) {
CoverageTestUtils::PrintCoverageDiagnostics(reporter,
polyline,
tiler,
tileStartX,
currY,
exactMasks,
tileStartIdx);
REPORTER_ASSERT(reporter,
false,
"[%s] Fail at tile(%d,%d). Exp %d, Got %d (alpha %d)",
testName,
tileStartX / kTileWidth,
currY / kTileHeight,
expectedSamples,
actualSamples,
actualAlpha);
return false;
} else if (inClip && sampleDiff > 0) {
if (minorErrorCount) {
(*minorErrorCount)[sampleDiff - 1]++;
}
}
maskIdx++;
}
}
}
}
bool bufferSizeMatch = (maskIdx == exactMasks.size());
REPORTER_ASSERT(reporter,
bufferSizeMatch,
"[%s] Checked %d mask/alpha bytes but observer size is %d",
testName,
maskIdx,
exactMasks.size());
if (!bufferSizeMatch) {
return false;
}
return validate_solid_tiles<kTileWidth, kTileHeight>(
reporter, lineBuckets, wides, ends, kSubX, clip, isEvenOdd, isInverse, testName);
}
template <uint16_t kTileWidth, uint16_t kTileHeight>
bool FastValidator<kTileWidth, kTileHeight>::ValidatePath(
skiatest::Reporter* reporter,
Recorder* recorder,
const SkPath& path,
const SkMatrix& ctm,
uint16_t viewportWidth,
uint16_t viewportHeight,
const char* testName,
const SparseStripConfig::LUTArray& maskLut,
StripFunc stripFunc,
std::array<uint32_t, 3>* minorErrorCount) {
return ValidatePath(reporter,
recorder,
path,
ctm,
SkIRect::MakeWH(viewportWidth, viewportHeight),
testName,
maskLut,
stripFunc,
minorErrorCount);
}
template <uint16_t kTileWidth, uint16_t kTileHeight>
bool FastValidator<kTileWidth, kTileHeight>::ValidatePath(
skiatest::Reporter* reporter,
Recorder* recorder,
const SkPath& path,
const SkMatrix& ctm,
const SkIRect& clipRect,
const char* testName,
const SparseStripConfig::LUTArray& maskLut,
StripFunc stripFunc,
std::array<uint32_t, 3>* minorErrorCount) {
if (path.isEmpty() || !path.isFinite()) {
return true;
}
if (clipRect.isEmpty() || clipRect.fLeft < 0 || clipRect.fTop < 0 ||
clipRect.fLeft % kTileWidth != 0 || clipRect.fTop % kTileHeight != 0) {
REPORTER_ASSERT(reporter,
false,
"[%s] Clip (l:%d, t:%d, r:%d, b:%d) must be non-empty, with a tile aligned "
"left and top",
testName,
clipRect.fLeft,
clipRect.fTop,
clipRect.fRight,
clipRect.fBottom);
return false;
}
Flatten flattener;
// 1. Unculled ground-truth polyline (computes true topology without culling)
Polyline unculledPolyline;
flattener.processPathsSimdTest(path, ctm, clipRect, &unculledPolyline);
// 2. Production culled polyline for the pipeline under test
Polyline testPolyline;
flattener.processPaths<FlattenMode::kSimd>(path, ctm, clipRect, &testPolyline);
// Like StripGenerator's, the histogram is indexed by the tile rows of the device, so it must
// cover every row down to the bottom of the clip.
uint16_t numTileRows = (clipRect.bottom() + kTileHeight - 1) / kTileHeight;
WindingHistogram windingHistogram(numTileRows);
windingHistogram.resize(numTileRows);
windingHistogram.clear();
Tiles<kTileWidth, kTileHeight> tiler;
bool isCulled = tiler.makeTilesMSAA(testPolyline, clipRect, &windingHistogram);
tiler.sortTiles();
WideTiles wides;
EndCaps ends;
AlphaAtlasManager atlasManager(recorder);
SkTDArray<uint8_t> exactMasks;
auto observer = [&](uint8_t exactMask, skvx::int8) { exactMasks.push_back(exactMask); };
if (stripFunc) {
stripFunc(tiler,
&wides,
&ends,
&atlasManager,
path.getFillType(),
testPolyline,
maskLut,
isCulled,
windingHistogram,
clipRect,
observer);
} else {
RunSimdWinding(tiler,
&wides,
&ends,
&atlasManager,
path.getFillType(),
testPolyline,
maskLut,
isCulled,
windingHistogram,
clipRect,
observer);
}
return ValidateStripOutput(reporter,
path,
testPolyline,
unculledPolyline,
tiler,
wides,
ends,
atlasManager,
exactMasks,
clipRect,
path.getFillType(),
testName,
minorErrorCount);
}
// Explicit template instantiations
template class FastValidator<4, 4>;
template class FastValidator<8, 8>;
} // namespace skgpu::graphite