blob: a818a9fe5d51ccc543e11123250f0d28ed3ece10 [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/Test.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 "src/core/SkRandom.h"
#include "src/core/SkVx.h"
#include "src/gpu/graphite/sparse_strips/MSAA_LUT.h"
#include "tests/graphite/sparse_strips/Oracle.h"
#include <algorithm>
#include <vector>
namespace skgpu::graphite {
namespace {
struct TestCase {
const char* name;
SkPath path;
int minRowY = 0;
int maxRowY = 1;
};
void validate_oracle_path(skiatest::Reporter* reporter, const TestCase& tc) {
static const skvx::float8 kSubX =
(skvx::cast<float>(skvx::byte8::Load(kMsaaPattern<uint8_t>.data())) + 0.5f) / 8.0f;
SkRect bounds = tc.path.getBounds();
int minX = static_cast<int>(std::floor(bounds.fLeft));
int maxX = static_cast<int>(std::ceil(bounds.fRight));
int rowWidth = std::max(1, maxX - minX + 1);
ScanlineOracle8x oracle(tc.path);
for (int py = tc.minRowY; py <= tc.maxRowY; ++py) {
oracle.buildRow(py);
const auto& hits = oracle.hits();
std::vector<skvx::int8> rowWindings(rowWidth, skvx::int8(0));
skvx::int8 totalWinding(0);
for (const auto& hit : hits) {
skvx::int8 px = skvx::cast<int>(skvx::ceil(hit.x - kSubX));
skvx::int8 idx = px - minX;
auto hasHit = hit.dir != 0;
totalWinding += if_then_else(hasHit & (idx <= 0), hit.dir, skvx::int8(0));
auto inRow = hasHit & (idx > 0) & (idx < rowWidth);
for (int k = 0; k < 8; ++k) {
if (inRow[k]) {
rowWindings[idx[k]][k] += hit.dir[k];
}
}
}
rowWindings[0] += totalWinding;
// Inclusive prefix sum across the row array
for (int i = 1; i < rowWidth; ++i) {
rowWindings[i] += rowWindings[i - 1];
}
// Validate against compressed oracle RowIntervals [x_k, x_{k+1})
const auto& intervals = oracle.rowIntervals();
for (size_t k = 0; k + 1 < intervals.size(); ++k) {
int xStart = intervals[k].x;
int xEnd = intervals[k + 1].x;
const auto& expectedWinding = intervals[k].winding;
for (int px = xStart; px < xEnd; ++px) {
int idx = px - minX;
if (idx >= 0 && idx < rowWidth) {
bool match = all(rowWindings[idx] == expectedWinding);
if (!match) {
SkString diag;
diag.appendf(
"\n=== MISMATCH DEBUG at Row %d, Pixel %d (minX=%d, idx=%d) ===\n",
py,
px,
minX,
idx);
diag.append("Lanes [0..7]:\n");
for (int lane = 0; lane < 8; ++lane) {
float vPx = static_cast<float>(px) + kSubX[lane];
diag.appendf(
" Lane %d: subX=%.4f, vPx=%.4f | prefixSum=%d, "
"oracleWinding=%d\n",
lane,
kSubX[lane],
vPx,
rowWindings[idx][lane],
expectedWinding[lane]);
}
diag.append("Hits near this pixel:\n");
for (size_t h = 0; h < hits.size(); ++h) {
for (int lane = 0; lane < 8; ++lane) {
if (hits[h].dir[lane] != 0 &&
std::abs(hits[h].x[lane] - px) < 3.0f) {
float hitX = hits[h].x[lane];
int calcPx = static_cast<int>(std::ceil(hitX - kSubX[lane]));
float vPx = static_cast<float>(px) + kSubX[lane];
diag.appendf(
" Hit[%zu] Lane %d: hitX=%.6f, dir=%d, calcPx=%d, "
"(vPx >= hitX)=%d\n",
h,
lane,
hitX,
hits[h].dir[lane],
calcPx,
(vPx >= hitX) ? 1 : 0);
}
}
}
ERRORF(reporter, "%s", diag.c_str());
}
REPORTER_ASSERT(reporter,
match,
"[%s] Row %d Pixel %d: prefix-sum winding != oracle winding",
tc.name,
py,
px);
}
}
}
}
}
} // namespace
DEF_TEST(SparseStrips_Oracle_Simple, reporter) {
const TestCase kTestCases[] = {
{"Line_Down",
SkPathBuilder()
.moveTo(1.0f, -0.5f)
.lineTo(5.0f, 1.5f)
.lineTo(1.0f, 1.5f)
.close()
.detach(),
0,
1},
{"Line_Up",
SkPathBuilder()
.moveTo(5.0f, 1.5f)
.lineTo(1.0f, -0.5f)
.lineTo(5.0f, -0.5f)
.close()
.detach(),
0,
1},
{"Quad",
SkPathBuilder()
.moveTo(1.0f, -0.5f)
.quadTo(6.0f, 0.5f, 2.0f, 1.5f)
.lineTo(1.0f, 1.5f)
.close()
.detach(),
0,
1},
{"Conic",
SkPathBuilder()
.moveTo(2.0f, -0.4f)
.conicTo(5.0f, 0.4f, 1.0f, 1.4f, 0.7071f)
.lineTo(2.0f, 1.4f)
.close()
.detach(),
0,
1},
{"Cubic_Cusp",
SkPathBuilder()
.moveTo(1.0f, -0.5f)
.cubicTo(5.0f, 1.5f, 5.0f, -0.5f, 1.0f, 1.5f)
.lineTo(1.0f, -0.5f)
.close()
.detach(),
0,
1},
{"Cubic_Swallowtail",
SkPathBuilder()
.moveTo(1.0f, -0.5f)
.cubicTo(7.0f, 2.0f, 1.0f, 2.0f, 7.0f, -0.5f)
.lineTo(1.0f, -0.5f)
.close()
.detach(),
0,
1},
{"Cubic_Serpentine",
SkPathBuilder()
.moveTo(1.0f, -0.5f)
.cubicTo(6.0f, 0.2f, -1.0f, 0.8f, 5.0f, 1.5f)
.lineTo(1.0f, 1.5f)
.close()
.detach(),
0,
1},
{"Mixed_Closed_Shape",
SkPathBuilder()
.moveTo(2.0f, 0.0f)
.lineTo(8.0f, 1.0f)
.quadTo(12.0f, 4.0f, 8.0f, 7.0f)
.conicTo(4.0f, 9.0f, 1.0f, 6.0f, 0.7071f)
.cubicTo(0.0f, 4.0f, 0.0f, 2.0f, 2.0f, 0.0f)
.close()
.detach(),
0,
8},
{"Negative_Coordinates",
SkPathBuilder()
.moveTo(-5.0f, -0.5f)
.lineTo(-1.0f, 1.5f)
.lineTo(-5.0f, 1.5f)
.close()
.detach(),
0,
1},
{"Donut_Hole",
SkPathBuilder()
.addRect(SkRect::MakeXYWH(1.0f, 0.0f, 6.0f, 6.0f), SkPathDirection::kCW)
.addRect(SkRect::MakeXYWH(3.0f, 2.0f, 2.0f, 2.0f), SkPathDirection::kCCW)
.detach(),
0,
6},
};
for (const auto& tc : kTestCases) {
validate_oracle_path(reporter, tc);
}
}
DEF_TEST(SparseStrips_Oracle_ManyPathsRow, reporter) {
SkRandom rand(12345);
SkPathBuilder builder;
constexpr int kNumPaths = 600;
float currentX = -50.0f;
for (int i = 0; i < kNumPaths; ++i) {
float xOffset = currentX;
float width = rand.nextRangeF(2.0f, 6.0f);
float y0 = rand.nextRangeF(-0.8f, -0.2f);
float y1 = rand.nextRangeF(1.2f, 1.8f);
int shapeType = rand.nextULessThan(6);
switch (shapeType) {
case 0: { // Line triangle
builder.moveTo(xOffset, y0).lineTo(xOffset + width, y1).lineTo(xOffset, y1).close();
break;
}
case 1: { // Quadratic
float ctrlX = xOffset + rand.nextRangeF(0.0f, width * 1.5f);
float ctrlY = rand.nextRangeF(-0.5f, 1.5f);
builder.moveTo(xOffset, y0)
.quadTo(ctrlX, ctrlY, xOffset + width, y1)
.lineTo(xOffset, y1)
.close();
break;
}
case 2: { // Conic
float ctrlX = xOffset + rand.nextRangeF(0.0f, width * 1.5f);
float ctrlY = rand.nextRangeF(-0.5f, 1.5f);
float weight = rand.nextRangeF(0.3f, 2.0f);
builder.moveTo(xOffset, y0)
.conicTo(ctrlX, ctrlY, xOffset + width, y1, weight)
.lineTo(xOffset, y1)
.close();
break;
}
case 3: { // Cubic
float ctrlX1 = xOffset + rand.nextRangeF(-1.0f, width);
float ctrlY1 = rand.nextRangeF(-0.5f, 1.5f);
float ctrlX2 = xOffset + rand.nextRangeF(0.0f, width + 1.0f);
float ctrlY2 = rand.nextRangeF(-0.5f, 1.5f);
builder.moveTo(xOffset, y0)
.cubicTo(ctrlX1, ctrlY1, ctrlX2, ctrlY2, xOffset + width, y1)
.lineTo(xOffset, y1)
.close();
break;
}
case 4: { // Rectangle / Polygon
builder.moveTo(xOffset, y0)
.lineTo(xOffset + width, y0)
.lineTo(xOffset + width, y1)
.lineTo(xOffset, y1)
.close();
break;
}
case 5: { // Donut / nested contour
float margin = width * 0.25f;
builder.addRect(SkRect::MakeLTRB(xOffset, y0, xOffset + width, y1),
SkPathDirection::kCW);
builder.addRect(
SkRect::MakeLTRB(
xOffset + margin, y0 + 0.3f, xOffset + width - margin, y1 - 0.3f),
SkPathDirection::kCCW);
break;
}
}
// Advance X with varying spacing
currentX += width + rand.nextRangeF(0.5f, 3.0f);
}
TestCase tc = {"Many_Paths_Row_600", builder.detach(), 0, 0};
validate_oracle_path(reporter, tc);
}
} // namespace skgpu::graphite