blob: e04712dd84919d30f76737897339b8696a443b48 [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 "src/gpu/graphite/sparse_strips/Strip.h"
#include "src/gpu/graphite/sparse_strips/Tiler.h"
#include "tests/Test.h"
#include "tests/graphite/sparse_strips/TileTestCases.h"
#include <algorithm>
#include <array>
#include <cmath>
#include <optional>
#include <vector>
namespace skgpu::graphite {
namespace {
// Liang-Barsky style as ground truth. If there exists a clipped segment of the line, returns the
// the segment's endpoints (in tile space).
std::optional<std::array<SkPoint, 2>> reference_clip(
const Line& line, float minX, float minY, float maxX, float maxY) {
float t0 = 0.0f;
float t1 = 1.0f;
float dx = line.p1.fX - line.p0.fX;
float dy = line.p1.fY - line.p0.fY;
float p[4] = {-dx, dx, -dy, dy};
float q[4] = {line.p0.fX - minX, maxX - line.p0.fX, line.p0.fY - minY, maxY - line.p0.fY};
for (int i = 0; i < 4; i++) {
if (p[i] == 0) {
if (q[i] < 0) return std::nullopt; // Parallel and outside
} else {
float t = q[i] / p[i];
if (p[i] < 0) {
if (t > t1) return std::nullopt;
if (t > t0) t0 = t;
} else {
if (t < t0) return std::nullopt;
if (t < t1) t1 = t;
}
}
}
if (t0 > t1) return std::nullopt;
SkPoint pEntry = {line.p0.fX + t0 * dx, line.p0.fY + t0 * dy};
SkPoint pExit = {line.p0.fX + t1 * dx, line.p0.fY + t1 * dy};
pEntry.fX -= minX;
pEntry.fY -= minY;
pExit.fX -= minX;
pExit.fY -= minY;
float width = maxX - minX;
float height = maxY - minY;
auto clamp_pt = [&](SkPoint& pt) {
pt.fX = std::clamp(pt.fX, 0.0f, width);
pt.fY = std::clamp(pt.fY, 0.0f, height);
};
clamp_pt(pEntry);
clamp_pt(pExit);
if (pExit.fY < pEntry.fY) return {{{pExit, pEntry}}};
return {{{pEntry, pExit}}};
}
} // namespace
template <uint16_t kWidth, uint16_t kHeight>
class ClipTestRunner {
static constexpr float kScale = static_cast<float>(kWidth) / 4.0f;
static constexpr float kViewportDimf = 100.0f;
// static and not anonymous namespace function for ease of friending ClipTestRunner
static void VerifyClip(skiatest::Reporter* reporter,
const Line& line,
const Tile& tile,
const char* testName) {
float dx = line.p1.fX - line.p0.fX;
float dy = line.p1.fY - line.p0.fY;
bool isVertical = std::abs(dx) <= std::numeric_limits<float>::epsilon();
bool isHorizontal = std::abs(dy) <= std::numeric_limits<float>::epsilon();
float idx = isVertical ? 0.0f : 1.0f / dx;
float idy = isHorizontal ? 0.0f : 1.0f / dy;
float tileMinX = tile.x * static_cast<float>(kWidth);
float tileMinY = tile.y * static_cast<float>(kHeight);
float tileMaxX = tileMinX + static_cast<float>(kWidth);
float tileMaxY = tileMinY + static_cast<float>(kHeight);
std::array<float, 4> bounds = {tileMinX, tileMinY, tileMaxX, tileMaxY};
std::array<float, 4> derivatives = {dx, dy, idx, idy};
uint32_t mask = tile.intersectionMask();
bool canonicalXDir = (dx >= 0);
bool canonicalYDir = (dy >= 0);
std::array<SkPoint, 2> result =
Strip::ClipToTile(line, bounds, derivatives, mask, canonicalXDir, canonicalYDir);
// Get the expected clip using Liang-Barsky
auto expectedClipLb = reference_clip(line, tileMinX, tileMinY, tileMaxX, tileMaxY);
// If a line lies entirely outside the tile but touches it at a single point (e.g., grazing
// a corner), Liang-Barsky (correctly) reports only that single intersection. However,
// ClipToTile safely handles these cases by clamping the coordinates to create a zero-width
// degenerate line. We calculate a fallback expected value here to match and verify that
// clamping behavior.
SkPoint rawP0 = {std::clamp(line.p0.fX - tileMinX, 0.0f, static_cast<float>(kWidth)),
std::clamp(line.p0.fY - tileMinY, 0.0f, static_cast<float>(kHeight))};
SkPoint rawP1 = {std::clamp(line.p1.fX - tileMinX, 0.0f, static_cast<float>(kWidth)),
std::clamp(line.p1.fY - tileMinY, 0.0f, static_cast<float>(kHeight))};
std::array<SkPoint, 2> fallbackExp;
if (rawP1.fY < rawP0.fY) {
fallbackExp = {rawP1, rawP0};
} else {
fallbackExp = {rawP0, rawP1};
}
const float kTol = 0.005f;
auto matchPt = [&](const SkPoint& a, const SkPoint& b) {
return std::abs(a.fX - b.fX) < kTol && std::abs(a.fY - b.fY) < kTol;
};
// Check the result against both Liang-Barsky value (covers almost all the cases) and
// the fallback value
bool match = true;
for (int i = 0; i < 2; ++i) {
bool matchesLb = false;
if (expectedClipLb.has_value()) {
matchesLb = matchPt(result[i], (*expectedClipLb)[i]);
}
bool matchesFallback = matchPt(result[i], fallbackExp[i]);
if (!matchesLb && !matchesFallback) {
match = false;
break;
}
}
if (!match) {
auto exp = expectedClipLb.has_value() ? expectedClipLb.value() : fallbackExp;
ERRORF(reporter,
"[%s] Clip mismatch at Tile(%u, %u).\n"
"Line: (%.2f, %.2f) -> (%.2f, %.2f)\n"
"Exp: [(%.2f, %.2f), (%.2f, %.2f)]\n"
"Got: [(%.2f, %.2f), (%.2f, %.2f)]\n",
testName, tile.x, tile.y,
line.p0.fX, line.p0.fY, line.p1.fX, line.p1.fY,
exp[0].fX, exp[0].fY, exp[1].fX, exp[1].fY,
result[0].fX, result[0].fY, result[1].fX, result[1].fY);
}
}
static void RunClipTest(skiatest::Reporter* reporter,
const SkTDArray<Line>& lines,
const SkTDArray<Tile>& expectedTiles,
const char* testName) {
for (const auto& tile : expectedTiles) {
uint32_t lineIdx = tile.lineIdx();
if (lineIdx < static_cast<uint32_t>(lines.size())) {
VerifyClip(reporter, lines[lineIdx], tile, testName);
} else {
ERRORF(reporter,
"[%s] Test setup error: Tile references invalid line index %u",
testName,
lineIdx);
}
}
}
public:
static void RunAll(skiatest::Reporter* reporter) {
auto testCases = TileTestCases::Get(kScale, kViewportDimf);
for (const auto& testCase : testCases) {
RunClipTest(reporter, testCase.fLines, testCase.fExpected, testCase.fName);
}
}
};
DEF_TEST(SparseStrips_Clip_4x4, reporter) { ClipTestRunner<4, 4>::RunAll(reporter); }
DEF_TEST(SparseStrips_Clip_8x8, reporter) { ClipTestRunner<8, 8>::RunAll(reporter); }
} // namespace skgpu::graphite