| /* | |
| * 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 |