| /* | |
| * 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/SkMatrix.h" | |
| #include "include/core/SkPaint.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/core/SkStrokeRec.h" | |
| #include "src/core/SkVx.h" | |
| #include "src/gpu/graphite/sparse_strips/Flatten.h" | |
| #include "src/gpu/graphite/sparse_strips/Polyline.h" | |
| #include "tests/graphite/sparse_strips/CullingRepros.h" | |
| #include <cmath> | |
| namespace skgpu::graphite { | |
| namespace { | |
| struct TrickyCurve { | |
| SkPoint fPoints[4]; | |
| int fNumPts; | |
| float fScale = 1.0f; | |
| }; | |
| void eval_quad_SIMD(const SkPoint pts[3], skvx::float4 t, skvx::float4& outX, skvx::float4& outY) { | |
| skvx::float4 mt = 1.0f - t; | |
| skvx::float4 a = mt * mt; | |
| skvx::float4 b = 2.0f * mt * t; | |
| skvx::float4 c = t * t; | |
| outX = pts[0].fX * a + pts[1].fX * b + pts[2].fX * c; | |
| outY = pts[0].fY * a + pts[1].fY * b + pts[2].fY * c; | |
| } | |
| void eval_conic_SIMD( | |
| const SkPoint pts[3], float w, skvx::float4 t, skvx::float4& outX, skvx::float4& outY) { | |
| skvx::float4 mt = 1.0f - t; | |
| skvx::float4 a = mt * mt; | |
| skvx::float4 b = 2.0f * w * t * mt; | |
| skvx::float4 c = t * t; | |
| skvx::float4 denom = a + b + c; | |
| skvx::float4 invDenom = 1.0f / denom; | |
| outX = (pts[0].fX * a + pts[1].fX * b + pts[2].fX * c) * invDenom; | |
| outY = (pts[0].fY * a + pts[1].fY * b + pts[2].fY * c) * invDenom; | |
| } | |
| void eval_cubic_SIMD(const SkPoint pts[4], skvx::float4 t, skvx::float4& outX, skvx::float4& outY) { | |
| skvx::float4 mt = 1.0f - t; | |
| skvx::float4 mt2 = mt * mt; | |
| skvx::float4 t2 = t * t; | |
| skvx::float4 a = mt2 * mt; | |
| skvx::float4 b = 3.0f * mt2 * t; | |
| skvx::float4 c = 3.0f * mt * t2; | |
| skvx::float4 d = t2 * t; | |
| outX = pts[0].fX * a + pts[1].fX * b + pts[2].fX * c + pts[3].fX * d; | |
| outY = pts[0].fY * a + pts[1].fY * b + pts[2].fY * c + pts[3].fY * d; | |
| } | |
| skvx::float4 dist_sq_point_SIMD( | |
| SkPoint p, skvx::float4 ax, skvx::float4 ay, skvx::float4 bx, skvx::float4 by) { | |
| skvx::float4 abx = bx - ax; | |
| skvx::float4 aby = by - ay; | |
| skvx::float4 apx = p.fX - ax; | |
| skvx::float4 apy = p.fY - ay; | |
| skvx::float4 lenSq = abx * abx + aby * aby; | |
| skvx::float4 dot = apx * abx + apy * aby; | |
| skvx::float4 t = skvx::if_then_else(lenSq == 0.0f, skvx::float4(0.0f), dot / lenSq); | |
| t = skvx::pin(t, skvx::float4(0.0f), skvx::float4(1.0f)); | |
| skvx::float4 projx = ax + abx * t; | |
| skvx::float4 projy = ay + aby * t; | |
| skvx::float4 dx = p.fX - projx; | |
| skvx::float4 dy = p.fY - projy; | |
| return dx * dx + dy * dy; | |
| } | |
| float estimate_max_length_SIMD(const SkPoint pts[], int count) { | |
| if (count < 2) return 0.0f; | |
| skvx::float4 px(0.0f), py(0.0f), nx(0.0f), ny(0.0f); | |
| for (int i = 0; i < count - 1; ++i) { | |
| px[i] = pts[i].fX; | |
| py[i] = pts[i].fY; | |
| nx[i] = pts[i + 1].fX; | |
| ny[i] = pts[i + 1].fY; | |
| } | |
| skvx::float4 dx = nx - px; | |
| skvx::float4 dy = ny - py; | |
| skvx::float4 lens = skvx::sqrt(dx * dx + dy * dy); | |
| skvx::int4 mask = skvx::int4(0, 1, 2, 3) < skvx::int4(count - 1); | |
| lens = skvx::if_then_else(mask, lens, skvx::float4(0.0f)); | |
| return lens[0] + lens[1] + lens[2] + lens[3]; | |
| } | |
| template <typename SegmentCallback> | |
| void for_each_path_segment_SIMD(const SkPath& path, float maxStepSize, SegmentCallback&& callback) { | |
| SkPath::Iter iter(path, false); | |
| SkPoint pts[4]; | |
| SkPath::Verb verb; | |
| SkPoint lastPt = {0, 0}; | |
| SkPoint contourStart = {0, 0}; | |
| bool hasContour = false; | |
| // Helper to dispatch a single segment (Line, Close, Move), returns false to early out if the | |
| // tolerance has been met | |
| auto emitSingleSegment = [&](SkPoint p0, SkPoint p1, const char* name) -> bool { | |
| skvx::float4 ax(p0.fX), ay(p0.fY); | |
| skvx::float4 bx(p1.fX), by(p1.fY); | |
| return callback(ax, ay, bx, by, 1, name); | |
| }; | |
| while ((verb = iter.next(pts)) != SkPath::kDone_Verb) { | |
| switch (verb) { | |
| case SkPath::kMove_Verb: | |
| if (hasContour && lastPt != contourStart) { | |
| if (!emitSingleSegment(lastPt, contourStart, "ImplicitClose")) return; | |
| } | |
| contourStart = pts[0]; | |
| lastPt = pts[0]; | |
| hasContour = true; | |
| // Emit a 0-length segment so coverage checks evaluate the starting point | |
| if (!emitSingleSegment(pts[0], pts[0], "Move")) return; | |
| break; | |
| case SkPath::kLine_Verb: | |
| if (!emitSingleSegment(pts[0], pts[1], "Line")) return; | |
| lastPt = pts[1]; | |
| break; | |
| case SkPath::kQuad_Verb: | |
| case SkPath::kConic_Verb: | |
| case SkPath::kCubic_Verb: { | |
| int countPts = (verb == SkPath::kQuad_Verb || verb == SkPath::kConic_Verb) ? 3 : 4; | |
| int samples = std::max( | |
| 1, (int)std::ceil(estimate_max_length_SIMD(pts, countPts) / maxStepSize)); | |
| SkPoint p0 = pts[0]; | |
| float w = (verb == SkPath::kConic_Verb) ? iter.conicWeight() : 1.0f; | |
| for (int i = 0; i < samples; i += 4) { | |
| int count = std::min(4, samples - i); | |
| skvx::float4 t = skvx::float4(i + 1, i + 2, i + 3, i + 4) / (float)samples; | |
| skvx::float4 px, py; | |
| switch (verb) { | |
| case SkPath::kQuad_Verb: | |
| eval_quad_SIMD(pts, t, px, py); | |
| break; | |
| case SkPath::kConic_Verb: | |
| eval_conic_SIMD(pts, w, t, px, py); | |
| break; | |
| case SkPath::kCubic_Verb: | |
| eval_cubic_SIMD(pts, t, px, py); | |
| break; | |
| default: | |
| SkUNREACHABLE; | |
| } | |
| skvx::float4 ax, ay; | |
| ax[0] = p0.fX; ax[1] = px[0]; ax[2] = px[1]; ax[3] = px[2]; | |
| ay[0] = p0.fY; ay[1] = py[0]; ay[2] = py[1]; ay[3] = py[2]; | |
| const char* name = (verb == SkPath::kQuad_Verb) ? "Quad" | |
| : (verb == SkPath::kConic_Verb) ? "Conic" | |
| : "Cubic"; | |
| if (!callback(ax, ay, px, py, count, name)) return; | |
| p0 = SkPoint::Make(px[count - 1], py[count - 1]); | |
| } | |
| lastPt = pts[countPts - 1]; | |
| break; | |
| } | |
| case SkPath::kClose_Verb: | |
| if (lastPt != contourStart) { | |
| if (!emitSingleSegment(lastPt, contourStart, "Close")) return; | |
| } | |
| lastPt = contourStart; | |
| hasContour = false; | |
| break; | |
| default: | |
| break; | |
| } | |
| } | |
| if (hasContour && lastPt != contourStart) { | |
| emitSingleSegment(lastPt, contourStart, "ImplicitClose"); | |
| } | |
| } | |
| } // anonymous namespace | |
| template <FlattenMode kMode> class FlattenTestRunner { | |
| static constexpr float kQuadErrTolerance = Flatten::kQuadErrTolerance; | |
| static constexpr float kAllowedTolSq = Flatten::kQuadTolerance2 + 0.0001f; | |
| static constexpr float kMinDistSq = 1e10f; | |
| static constexpr float kMaxStepSize = | |
| .1f > kQuadErrTolerance * 0.5f ? .1f : kQuadErrTolerance * 0.5f; | |
| static constexpr int kLineSamples = 100; | |
| static constexpr float kLineDt = 1.0f / static_cast<float>(kLineSamples); | |
| static constexpr float kViewDim = 10000.0f; | |
| static float DistSqPointToPath(SkPoint pt, const SkPath& path) { | |
| float minDistSq = kMinDistSq; | |
| auto processSegment = [&minDistSq, pt](skvx::float4 ax, skvx::float4 ay, | |
| skvx::float4 bx, skvx::float4 by, | |
| int validCount, const char* verbName) { | |
| skvx::float4 dists = dist_sq_point_SIMD(pt, ax, ay, bx, by); | |
| skvx::int4 mask = skvx::int4(0, 1, 2, 3) < skvx::int4(validCount); | |
| dists = skvx::if_then_else(mask, dists, skvx::float4(kMinDistSq)); | |
| minDistSq = std::min(minDistSq, skvx::min(dists)); | |
| // Return false to potentially early out | |
| return minDistSq > kAllowedTolSq; | |
| }; | |
| for_each_path_segment_SIMD(path, kMaxStepSize, processSegment); | |
| return minDistSq; | |
| } | |
| static void CheckFlattenedPath(skiatest::Reporter* reporter, | |
| const SkPath& originalPath, | |
| const char* testName) { | |
| SkRect bounds = originalPath.computeTightBounds(); | |
| SkMatrix shift = SkMatrix::Translate(1000.0f - bounds.fLeft, 1000.0f - bounds.fTop); | |
| SkPath referencePath = originalPath.makeTransform(shift); | |
| Flatten flattener; | |
| Polyline polyline; | |
| flattener.processPaths<kMode>(originalPath, shift, kViewDim, kViewDim, &polyline); | |
| if (originalPath.isEmpty()) { | |
| REPORTER_ASSERT(reporter, polyline.empty(), "[%s] Expected empty output", testName); | |
| return; | |
| } | |
| const SkTDArray<SkPoint>& rawPts = polyline.points(); | |
| // ========================================================================================= | |
| // Watertightness Check | |
| // Iterate through the raw flattened points (which are separated by NaNs to denote distinct | |
| // contours) and assert that the final point of every contour matches its starting point. | |
| // ========================================================================================= | |
| { | |
| SkPoint contourStart = {0, 0}; | |
| SkPoint lastPt = {0, 0}; | |
| bool inContour = false; | |
| for (int i = 0; i < rawPts.size(); ++i) { | |
| if (std::isnan(rawPts[i].fX)) { | |
| if (inContour) { | |
| REPORTER_ASSERT(reporter, | |
| lastPt == contourStart, | |
| "[%s] Contour did not close tightly. End: (%f, %f), Start: " | |
| "(%f, %f)", | |
| testName, | |
| lastPt.fX, | |
| lastPt.fY, | |
| contourStart.fX, | |
| contourStart.fY); | |
| } | |
| inContour = false; | |
| } else { | |
| if (!inContour) { | |
| contourStart = rawPts[i]; | |
| inContour = true; | |
| } | |
| lastPt = rawPts[i]; | |
| } | |
| } | |
| // Catch the final contour if the array doesn't end with a NaN separator. (Should never | |
| // happen) | |
| if (inContour) { | |
| REPORTER_ASSERT( | |
| reporter, | |
| lastPt == contourStart, | |
| "[%s] Contour did not close tightly. End: (%f, %f), Start: (%f, %f)", | |
| testName, | |
| lastPt.fX, | |
| lastPt.fY, | |
| contourStart.fX, | |
| contourStart.fY); | |
| } | |
| } | |
| // ========================================================================================= | |
| // Polyline-to-Path (Hausdorff Distance Approximation -> True Curve) | |
| // | |
| // Ensure that the flattener didn't generate line segments that stray too far from the true | |
| // path. To avoid numerical solvers, we sample *along the generated line segments* and | |
| // projecting them back onto the true path to find the shortest distance. | |
| // ========================================================================================= | |
| for (auto [line, index] : polyline) { | |
| for (int i = 0; i < kLineSamples; i += 4) { | |
| skvx::float4 t03 = skvx::float4(i, i + 1, i + 2, i + 3) * kLineDt; | |
| skvx::float4 px03 = line.p0.fX * (1.0f - t03) + line.p1.fX * t03; | |
| skvx::float4 py03 = line.p0.fY * (1.0f - t03) + line.p1.fY * t03; | |
| int count = std::min(4, kLineSamples - i); | |
| for (int j = 0; j < count; ++j) { | |
| SkPoint samplePt = SkPoint::Make(px03[j], py03[j]); | |
| float minDistSq = DistSqPointToPath(samplePt, referencePath); | |
| REPORTER_ASSERT( | |
| reporter, | |
| minDistSq <= kAllowedTolSq, | |
| "[%s] Interpolated line point (%f, %f) deviated from true curve by " | |
| "%f (tol: %f)", | |
| testName, | |
| samplePt.fX, | |
| samplePt.fY, | |
| std::sqrt(minDistSq), | |
| kAllowedTolSq); | |
| } | |
| } | |
| // Check terminal t=1.0 separately | |
| SkPoint pt4 = line.p1; | |
| float minDistSq = DistSqPointToPath(pt4, referencePath); | |
| REPORTER_ASSERT(reporter, | |
| minDistSq <= kAllowedTolSq, | |
| "[%s] Interpolated line point (%f, %f) deviated from true curve by %f " | |
| "(tol: %f)", | |
| testName, | |
| pt4.fX, | |
| pt4.fY, | |
| std::sqrt(minDistSq), | |
| kAllowedTolSq); | |
| } | |
| // ============================================================================== | |
| // Path-to-Polyline (Hausdorff Distance True Curve -> Approximation) | |
| // | |
| // Ensure that the flattenner produces geometry for the length of the entire original curve, | |
| // instead of collapsing the curve into a single valid point or drawing an incomplete | |
| // segment that just happens to lie on the mathematical path. | |
| // ============================================================================== | |
| auto distSqPointToPolyline = [&polyline, &rawPts](SkPoint pt) { | |
| float minDistSq = kMinDistSq; | |
| skvx::float4 ax, ay, bx, by; | |
| int lane = 0; | |
| for (auto [line, index] : polyline) { | |
| ax[lane] = line.p0.fX; | |
| ay[lane] = line.p0.fY; | |
| bx[lane] = line.p1.fX; | |
| by[lane] = line.p1.fY; | |
| if (++lane == 4) { | |
| skvx::float4 dists = dist_sq_point_SIMD(pt, ax, ay, bx, by); | |
| minDistSq = std::min(minDistSq, skvx::min(dists)); | |
| if (minDistSq <= kAllowedTolSq) return minDistSq; | |
| lane = 0; | |
| } | |
| } | |
| if (lane > 0) { | |
| skvx::int4 valid = skvx::int4(0, 1, 2, 3) < skvx::int4(lane); | |
| skvx::float4 dists = dist_sq_point_SIMD(pt, ax, ay, bx, by); | |
| dists = skvx::if_then_else(valid, dists, skvx::float4(kMinDistSq)); | |
| minDistSq = std::min(minDistSq, skvx::min(dists)); | |
| if (minDistSq <= kAllowedTolSq) return minDistSq; | |
| } | |
| for (SkPoint rawPt : rawPts) { | |
| if (!std::isnan(rawPt.fX)) { | |
| SkPoint d = pt - rawPt; | |
| minDistSq = std::min(minDistSq, SkPoint::DotProduct(d, d)); | |
| if (minDistSq <= kAllowedTolSq) return minDistSq; | |
| } | |
| } | |
| return minDistSq; | |
| }; | |
| auto checkPathCoverage = [&reporter, testName, &distSqPointToPolyline]( | |
| skvx::float4 ax, skvx::float4 ay, | |
| skvx::float4 bx, skvx::float4 by, | |
| int validCount, const char* verbName) { | |
| for (int i = 0; i < validCount; ++i) { | |
| SkPoint pt = SkPoint::Make(bx[i], by[i]); | |
| float minDistSq = distSqPointToPolyline(pt); | |
| REPORTER_ASSERT(reporter, | |
| minDistSq <= kAllowedTolSq, | |
| "[%s] Path %s point (%f, %f) lacks polyline " | |
| "coverage. Deviated by %f (tol: %f)", | |
| testName, | |
| verbName, | |
| pt.fX, | |
| pt.fY, | |
| std::sqrt(minDistSq), | |
| kAllowedTolSq); | |
| } | |
| return true; // continue checking the rest of the path | |
| }; | |
| for_each_path_segment_SIMD(referencePath, kMaxStepSize, checkPathCoverage); | |
| } | |
| static void TestCulling(skiatest::Reporter* reporter) { | |
| const float kViewW = 100.0f; | |
| const float kViewH = 100.0f; | |
| enum class Expect { | |
| kCulled, // Removed: Start pt + Cull NaN (2 points) | |
| kSimplified, // Left side simplification: Start pt + End pt + Close pt + NaN (4 points) | |
| kSubdivided // Intersects viewport: Subdivided curve (> 4 points) | |
| }; | |
| auto checkPath = [&](const SkPath& path, Expect expect, const char* testName) { | |
| Flatten flattener; | |
| Polyline polyline; | |
| flattener.processPaths<kMode>(path, SkMatrix(), kViewW, kViewH, &polyline); | |
| int count = polyline.points().size(); | |
| if (expect == Expect::kCulled) { | |
| REPORTER_ASSERT( | |
| reporter, | |
| count == 2, | |
| "[%s] Expected curve to be entirely culled (2 points), got %d points", | |
| testName, | |
| count); | |
| } else if (expect == Expect::kSimplified) { | |
| REPORTER_ASSERT( | |
| reporter, | |
| count == 4, | |
| "[%s] Expected curve to be simplified to 1 line (4 points), got %d points", | |
| testName, | |
| count); | |
| } else { | |
| REPORTER_ASSERT(reporter, | |
| count > 4, | |
| "[%s] Expected curve to cross viewport and be subdivided (>4 " | |
| "points), got %d points", | |
| testName, | |
| count); | |
| } | |
| }; | |
| checkPath(SkPathBuilder().moveTo(10, -20).quadTo(50, -80, 90, -20).detach(), | |
| Expect::kCulled, | |
| "Cull_Top"); | |
| checkPath(SkPathBuilder().moveTo(10, 120).cubicTo(30, 180, 70, 180, 90, 120).detach(), | |
| Expect::kCulled, | |
| "Cull_Bottom"); | |
| checkPath(SkPathBuilder().moveTo(120, 10).quadTo(180, 50, 120, 90).detach(), | |
| Expect::kCulled, | |
| "Cull_Right"); | |
| checkPath(SkPathBuilder().moveTo(-50, 10).cubicTo(-20, 30, -20, 70, -50, 90).detach(), | |
| Expect::kSimplified, | |
| "Simplify_Left"); | |
| checkPath(SkPathBuilder().moveTo(-20, -20).quadTo(120, -20, 120, 120).detach(), | |
| Expect::kSubdivided, | |
| "Crosses_Viewport"); | |
| } | |
| // A contour whose verbs are all culled must still emit its closing edge whenever that edge is | |
| // not itself culled, since it may be the only geometry contributing winding inside the clip. | |
| // This only matters for open contours: an explicit close() reaches Flatten as a (never culled) | |
| // line. Note: This only verifies the polyline; FastValidator validates the resulting coverage. | |
| static void TestCulledContourClosingEdge(skiatest::Reporter* reporter) { | |
| // Flattens a single contour whose verbs all lie outside of `clip` and verifies that its | |
| // closing edge is the only line that survives culling. | |
| auto checkOnlyClosingEdge = [&](const SkPath& path, | |
| const SkMatrix& ctm, | |
| const SkIRect& clip, | |
| const char* testName) { | |
| Flatten flattener; | |
| Polyline polyline; | |
| flattener.processPaths<kMode>(path, ctm, clip, &polyline); | |
| const SkPoint startPt = ctm.mapPoint(path.getPoint(0)); | |
| const SkPoint lastPt = ctm.mapPoint(path.getPoint(path.countPoints() - 1)); | |
| // Allow for differences in how the ctm is applied to large device space coordinates. | |
| constexpr float kTolerance = 0.01f; | |
| int numLines = 0; | |
| bool hasClosingEdge = false; | |
| for (auto [line, index] : polyline) { | |
| ++numLines; | |
| hasClosingEdge |= SkPoint::Distance(line.p0, lastPt) <= kTolerance && | |
| SkPoint::Distance(line.p1, startPt) <= kTolerance; | |
| } | |
| REPORTER_ASSERT(reporter, | |
| numLines == 1 && hasClosingEdge, | |
| "[%s] Expected only the closing edge (%f, %f) -> (%f, %f) to survive " | |
| "culling, got %d lines (closing edge %s)", | |
| testName, | |
| lastPt.fX, | |
| lastPt.fY, | |
| startPt.fX, | |
| startPt.fY, | |
| numLines, | |
| hasClosingEdge ? "found" : "missing"); | |
| }; | |
| const SkIRect clip = SkIRect::MakeWH(100, 100); | |
| // Closing edge runs down the left of the clip, covering all of it. | |
| checkOnlyClosingEdge(CullingRepros::AllVerbsCulledContour(clip, /*close=*/false), | |
| SkMatrix::I(), | |
| clip, | |
| "AllVerbsCulled_ClosingEdgeLeft_Open"); | |
| checkOnlyClosingEdge(CullingRepros::AllVerbsCulledContour(clip, /*close=*/true), | |
| SkMatrix::I(), | |
| clip, | |
| "AllVerbsCulled_ClosingEdgeLeft_Closed"); | |
| // Closing edge crosses the clip at x = 50, covering only its right half. | |
| checkOnlyClosingEdge(SkPathBuilder() | |
| .moveTo(50, -20) | |
| .cubicTo(70, -60, 100, -60, 120, -20) // Above | |
| .cubicTo(160, 20, 160, 80, 120, 120) // Right | |
| .cubicTo(100, 160, 70, 160, 50, 120) // Below | |
| .detach(), | |
| SkMatrix::I(), | |
| clip, | |
| "AllVerbsCulled_ClosingEdgeCrossesClip"); | |
| // Exact inputs captured from the viewer, with a non-identity ctm. | |
| const CullingRepros::Repro repro = CullingRepros::DeskTiger8Capture3Draw2(); | |
| checkOnlyClosingEdge(repro.fPath, repro.fCtm, repro.fClip, "DeskTiger8Capture3Draw2"); | |
| } | |
| static void TestTrickyStrokes(skiatest::Reporter* reporter) { | |
| const float kStrokeWidth = 30.0f; | |
| static const TrickyCurve kTrickyCurves[] = { | |
| {{{122, 737}, {348, 553}, {403, 761}, {400, 760}}, 4}, | |
| {{{244, 520}, {244, 518}, {1141, 634}, {394, 688}}, 4}, | |
| {{{550, 194}, {138, 130}, {1035, 246}, {288, 300}}, 4}, | |
| {{{226, 733}, {556, 779}, {-43, 471}, {348, 683}}, 4}, | |
| {{{268, 204}, {492, 304}, {352, 23}, {433, 412}}, 4}, | |
| {{{172, 480}, {396, 580}, {256, 299}, {338, 677}}, 4}, | |
| {{{731, 340}, {318, 252}, {1026, -64}, {367, 265}}, 4}, | |
| {{{475, 708}, {62, 620}, {770, 304}, {220, 659}}, 4}, | |
| {{{0, 0}, {128, 128}, {128, 0}, {0, 128}}, 4}, | |
| {{{0, .01f}, {128, 127.999f}, {128, .01f}, {0, 127.99f}}, 4}, | |
| {{{0, -.01f}, {128, 128.001f}, {128, -.01f}, {0, 128.001f}}, 4}, | |
| {{{0, 0}, {0, -10}, {0, -10}, {0, 10}}, 4, 1.098283f}, | |
| {{{10, 0}, {0, 0}, {20, 0}, {10, 0}}, 4}, | |
| {{{39, -39}, {40, -40}, {40, -40}, {0, 0}}, 4}, | |
| {{{39, -39}, {40, -40}, {37, -39}, {0, 0}}, 4}, | |
| {{{40, 40}, {0, 0}, {200, 200}, {0, 0}}, 4}, | |
| {{{0, 0}, {1e-2f, 0}, {-1e-2f, 0}, {0, 0}}, 4}, | |
| {{{400.75f, 100.05f}, {400.75f, 100.05f}, {100.05f, 300.95f}, {100.05f, 300.95f}}, 4}, | |
| {{{0.5f, 0}, {0, 0}, {20, 0}, {10, 0}}, 4}, | |
| {{{10, 0}, {0, 0}, {10, 0}, {10, 0}}, 4}, | |
| {{{1, 1}, {2, 1}, {1, 1}, {1, SK_ScalarNaN}}, 3}, | |
| {{{1, 1}, {100, 1}, {25, 1}, {.3f, SK_ScalarNaN}}, 3}, | |
| {{{1, 1}, {100, 1}, {25, 1}, {1.5f, SK_ScalarNaN}}, 3}, | |
| }; | |
| auto strokeAndCheck = [&](const SkPath& originalPath, const char* nameBase, int index) { | |
| SkStrokeRec miterStyle(SkStrokeRec::kHairline_InitStyle); | |
| miterStyle.setStrokeStyle(kStrokeWidth); | |
| miterStyle.setStrokeParams(SkPaint::kButt_Cap, SkPaint::kMiter_Join, 4.0f); | |
| SkPathBuilder miterBuilder; | |
| miterStyle.applyToPath(&miterBuilder, originalPath); | |
| CheckFlattenedPath(reporter, | |
| miterBuilder.detach(), | |
| SkStringPrintf("%s_%d_Miter", nameBase, index).c_str()); | |
| SkStrokeRec roundStyle(SkStrokeRec::kHairline_InitStyle); | |
| roundStyle.setStrokeStyle(kStrokeWidth); | |
| roundStyle.setStrokeParams(SkPaint::kRound_Cap, SkPaint::kRound_Join, 4.0f); | |
| SkPathBuilder roundBuilder; | |
| roundStyle.applyToPath(&roundBuilder, originalPath); | |
| CheckFlattenedPath(reporter, | |
| roundBuilder.detach(), | |
| SkStringPrintf("%s_%d_Round", nameBase, index).c_str()); | |
| }; | |
| for (size_t i = 0; i < std::size(kTrickyCurves); ++i) { | |
| auto [originalPts, numPts, scale] = kTrickyCurves[i]; | |
| SkPoint p[4]; | |
| memcpy(p, originalPts, sizeof(SkPoint) * numPts); | |
| for (int j = 0; j < numPts; ++j) { | |
| p[j] *= scale; | |
| } | |
| SkPathBuilder builder; | |
| builder.moveTo(p[0]); | |
| float w = originalPts[3].fX; | |
| if (numPts == 4) { | |
| builder.cubicTo(p[1], p[2], p[3]); | |
| } else if (w == 1.0f) { | |
| builder.quadTo(p[1], p[2]); | |
| } else { | |
| builder.conicTo(p[1], p[2], w); | |
| } | |
| strokeAndCheck(builder.detach(), "TrickyStroke", static_cast<int>(i)); | |
| } | |
| SkPathBuilder largeRad; | |
| for (int y = 0; y < 2; ++y) { | |
| float shift = 210.f * y; | |
| float dy = 5.f * y; | |
| largeRad.moveTo(159.429f, 149.808f + shift) | |
| .cubicTo({232.5f, 149.808f + dy + shift}, | |
| {232.5f, 149.808f + dy + shift}, | |
| {305.572f, 149.808f + shift}); | |
| } | |
| SkStrokeRec thickStyle(SkStrokeRec::kHairline_InitStyle); | |
| thickStyle.setStrokeStyle(200.0f); | |
| thickStyle.setStrokeParams(SkPaint::kButt_Cap, SkPaint::kMiter_Join, 4.0f); | |
| SkPathBuilder thickBuilder; | |
| SkPath largeRadPath = largeRad.detach(); | |
| thickStyle.applyToPath(&thickBuilder, largeRadPath); | |
| CheckFlattenedPath(reporter, thickBuilder.detach(), "TrickyStroke_LargeRadius"); | |
| } | |
| public: | |
| static void RunAll(skiatest::Reporter* reporter) { | |
| CheckFlattenedPath(reporter, SkPath(), "EmptyPath"); | |
| CheckFlattenedPath(reporter, | |
| SkPathBuilder().moveTo(10, 10).lineTo(50, 50).lineTo(90, 10).detach(), | |
| "SimpleLines"); | |
| CheckFlattenedPath(reporter, | |
| SkPathBuilder().moveTo(10, 10).quadTo(50, 100, 90, 10).detach(), | |
| "SimpleQuad"); | |
| CheckFlattenedPath( | |
| reporter, | |
| SkPathBuilder().moveTo(10, 10).cubicTo(30, 100, 70, 100, 90, 10).detach(), | |
| "SimpleCubic"); | |
| CheckFlattenedPath(reporter, | |
| SkPathBuilder().moveTo(0, 0).quadTo(50, 100, 100, 0).close().detach(), | |
| "ClosedQuad"); | |
| CheckFlattenedPath( | |
| reporter, | |
| SkPathBuilder().moveTo(10, 10).cubicTo(100, 10, 10, 100, 100, 100).detach(), | |
| "SCurveCubic"); | |
| CheckFlattenedPath(reporter, | |
| SkPathBuilder() | |
| .moveTo(10, 10) | |
| .quadTo(10, 10, 10, 10) | |
| .cubicTo(10, 10, 10, 10, 10, 10) | |
| .detach(), | |
| "DegenerateCurves"); | |
| CheckFlattenedPath(reporter, | |
| SkPathBuilder().moveTo(10, 10).cubicTo(40, 10, 70, 10, 100, 10).detach(), | |
| "CollinearCubic"); | |
| CheckFlattenedPath( | |
| reporter, | |
| SkPathBuilder().moveTo(50, 50).cubicTo(150, -50, 150, 150, 50, 50).detach(), | |
| "LoopCubic"); | |
| CheckFlattenedPath(reporter, | |
| SkPathBuilder() | |
| .moveTo(10, 10) | |
| .lineTo(20, 20) | |
| .moveTo(30, 30) | |
| .quadTo(50, 80, 70, 30) | |
| .close() | |
| .moveTo(100, 100) | |
| .cubicTo(120, 150, 180, 150, 200, 100) | |
| .detach(), | |
| "MultiContour"); | |
| CheckFlattenedPath(reporter, | |
| SkPathBuilder().moveTo(10, 10).conicTo(50, 100, 90, 10, 0.5f).detach(), | |
| "SimpleConicWeightHalf"); | |
| CheckFlattenedPath(reporter, | |
| SkPathBuilder().moveTo(10, 10).conicTo(50, 100, 90, 10, 2.0f).detach(), | |
| "SimpleConicWeightTwo"); | |
| CheckFlattenedPath(reporter, | |
| SkPathBuilder().moveTo(10, 10).conicTo(50, 100, 90, 10, 1.0f).detach(), | |
| "SimpleConicWeightOne"); | |
| CheckFlattenedPath(reporter, | |
| SkPathBuilder() | |
| .moveTo(100, 0) | |
| .conicTo(100, 100, 0, 100, std::sqrt(2.0f) / 2.0f) | |
| .detach(), | |
| "QuarterCircleConic"); | |
| CheckFlattenedPath(reporter, | |
| SkPathBuilder().moveTo(10, 10).conicTo(50, 100, 90, 10, 0.0f).detach(), | |
| "WorstCaseWeightZero"); | |
| CheckFlattenedPath(reporter, | |
| SkPathBuilder().moveTo(10, 10).conicTo(50, 100, 90, 10, 1e-5f).detach(), | |
| "WorstCaseWeightMicroscopic"); | |
| CheckFlattenedPath(reporter, | |
| SkPathBuilder().moveTo(10, 10).conicTo(50, 100, 90, 10, 1e5f).detach(), | |
| "WorstCaseWeightMassive"); | |
| CheckFlattenedPath(reporter, | |
| SkPathBuilder().moveTo(50, 50).conicTo(50, 50, 50, 50, 1.0f).detach(), | |
| "WorstCaseDegenerate"); | |
| CheckFlattenedPath(reporter, | |
| SkPathBuilder().moveTo(0, 0).cubicTo(50, 0, 100, 0, 100, 50).detach(), | |
| "CubicPartialLinear"); | |
| CheckFlattenedPath(reporter, | |
| SkPathBuilder() | |
| .moveTo(10, 10) | |
| .lineTo(20, 10) | |
| .quadTo(20, -20, 30, -10) | |
| .close() | |
| .detach(), | |
| "CulledEndQuadClosed"); | |
| CheckFlattenedPath(reporter, | |
| SkPathBuilder().moveTo(0, 0).cubicTo(50, 0, 100, 0, 100, 100).detach(), | |
| "CubicInflectionFlatStart"); | |
| CheckFlattenedPath(reporter, | |
| SkPathBuilder().moveTo(10, 10).conicTo(50, 10, 90, 10, 0.001f).detach(), | |
| "CollinearFlatConic"); | |
| TestCulling(reporter); | |
| TestCulledContourClosingEdge(reporter); | |
| TestTrickyStrokes(reporter); | |
| } | |
| }; | |
| DEF_TEST(SparseStrips_Flatten_Scalar, reporter) { | |
| skgpu::graphite::FlattenTestRunner<FlattenMode::kScalar>::RunAll(reporter); | |
| } | |
| DEF_TEST(SparseStrips_Flatten_SIMD, reporter) { | |
| skgpu::graphite::FlattenTestRunner<FlattenMode::kSimd>::RunAll(reporter); | |
| } | |
| } // namespace skgpu::graphite |