| /* |
| * Copyright 2020 Google Inc. |
| * |
| * 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/private/SkFloatingPoint.h" |
| #include "src/core/SkGeometry.h" |
| #include "src/gpu/geometry/GrPathUtils.h" |
| #include "src/gpu/geometry/GrWangsFormula.h" |
| #include "src/gpu/mock/GrMockOpTarget.h" |
| #include "src/gpu/tessellate/GrStrokeIndirectTessellator.h" |
| #include "src/gpu/tessellate/GrStrokeShader.h" |
| #include "src/gpu/tessellate/GrTessellationPathRenderer.h" |
| |
| static sk_sp<GrDirectContext> make_mock_context() { |
| GrMockOptions mockOptions; |
| mockOptions.fDrawInstancedSupport = true; |
| mockOptions.fMaxTessellationSegments = 64; |
| mockOptions.fMapBufferFlags = GrCaps::kCanMap_MapFlag; |
| mockOptions.fConfigOptions[(int)GrColorType::kAlpha_8].fRenderability = |
| GrMockOptions::ConfigOptions::Renderability::kMSAA; |
| mockOptions.fConfigOptions[(int)GrColorType::kAlpha_8].fTexturable = true; |
| mockOptions.fIntegerSupport = true; |
| |
| GrContextOptions ctxOptions; |
| ctxOptions.fGpuPathRenderers = GpuPathRenderers::kTessellation; |
| |
| return GrDirectContext::MakeMock(&mockOptions, ctxOptions); |
| } |
| |
| static void test_stroke(skiatest::Reporter* r, GrDirectContext* ctx, GrMockOpTarget* target, |
| const SkPath& path, SkRandom& rand) { |
| SkStrokeRec stroke(SkStrokeRec::kFill_InitStyle); |
| stroke.setStrokeStyle(.1f); |
| for (auto join : {SkPaint::kMiter_Join, SkPaint::kRound_Join}) { |
| stroke.setStrokeParams(SkPaint::kButt_Cap, join, 4); |
| for (int i = 0; i < 16; ++i) { |
| float scale = ldexpf(rand.nextF() + 1, i); |
| auto matrix = SkMatrix::Scale(scale, scale); |
| GrStrokeTessellator::PathStrokeList pathStrokeList(path, stroke, SK_PMColor4fWHITE); |
| GrStrokeIndirectTessellator tessellator(GrStrokeShader::ShaderFlags::kNone, matrix, |
| &pathStrokeList, {scale, scale}, {0,0,0,0}, |
| path.countVerbs(), target->allocator()); |
| tessellator.verifyResolveLevels(r, target, matrix, path, stroke); |
| tessellator.prepare(target, path.countVerbs()); |
| tessellator.verifyBuffers(r, target, matrix, stroke); |
| } |
| } |
| } |
| |
| DEF_TEST(tessellate_GrStrokeIndirectTessellator, r) { |
| auto ctx = make_mock_context(); |
| auto target = std::make_unique<GrMockOpTarget>(ctx); |
| SkRandom rand; |
| |
| // Empty strokes. |
| SkPath path = SkPath(); |
| test_stroke(r, ctx.get(), target.get(), path, rand); |
| path.moveTo(1,1); |
| test_stroke(r, ctx.get(), target.get(), path, rand); |
| path.moveTo(1,1); |
| test_stroke(r, ctx.get(), target.get(), path, rand); |
| path.close(); |
| test_stroke(r, ctx.get(), target.get(), path, rand); |
| path.moveTo(1,1); |
| test_stroke(r, ctx.get(), target.get(), path, rand); |
| |
| // Single line. |
| path = SkPath().lineTo(1,1); |
| test_stroke(r, ctx.get(), target.get(), path, rand); |
| path.close(); |
| test_stroke(r, ctx.get(), target.get(), path, rand); |
| |
| // Single quad. |
| path = SkPath().quadTo(1,0,1,1); |
| test_stroke(r, ctx.get(), target.get(), path, rand); |
| path.close(); |
| test_stroke(r, ctx.get(), target.get(), path, rand); |
| |
| // Single cubic. |
| path = SkPath().cubicTo(1,0,0,1,1,1); |
| test_stroke(r, ctx.get(), target.get(), path, rand); |
| path.close(); |
| test_stroke(r, ctx.get(), target.get(), path, rand); |
| |
| // All types of lines. |
| path.reset(); |
| for (int i = 0; i < (1 << 4); ++i) { |
| path.moveTo((i>>0)&1, (i>>1)&1); |
| path.lineTo((i>>2)&1, (i>>3)&1); |
| path.close(); |
| } |
| test_stroke(r, ctx.get(), target.get(), path, rand); |
| |
| // All types of quads. |
| path.reset(); |
| for (int i = 0; i < (1 << 6); ++i) { |
| path.moveTo((i>>0)&1, (i>>1)&1); |
| path.quadTo((i>>2)&1, (i>>3)&1, (i>>4)&1, (i>>5)&1); |
| path.close(); |
| } |
| test_stroke(r, ctx.get(), target.get(), path, rand); |
| |
| // All types of cubics. |
| path.reset(); |
| for (int i = 0; i < (1 << 8); ++i) { |
| path.moveTo((i>>0)&1, (i>>1)&1); |
| path.cubicTo((i>>2)&1, (i>>3)&1, (i>>4)&1, (i>>5)&1, (i>>6)&1, (i>>7)&1); |
| path.close(); |
| } |
| test_stroke(r, ctx.get(), target.get(), path, rand); |
| |
| { |
| // This cubic has a convex-180 chop at T=1-"epsilon" |
| static const uint32_t hexPts[] = {0x3ee0ac74, 0x3f1e061a, 0x3e0fc408, 0x3f457230, |
| 0x3f42ac7c, 0x3f70d76c, 0x3f4e6520, 0x3f6acafa}; |
| SkPoint pts[4]; |
| memcpy(pts, hexPts, sizeof(pts)); |
| test_stroke(r, ctx.get(), target.get(), |
| SkPath().moveTo(pts[0]).cubicTo(pts[1], pts[2], pts[3]).close(), rand); |
| } |
| |
| // Random paths. |
| for (int j = 0; j < 50; ++j) { |
| path.reset(); |
| // Empty contours behave differently if closed. |
| path.moveTo(0,0); |
| path.moveTo(0,0); |
| path.close(); |
| path.moveTo(0,0); |
| SkPoint startPoint = {rand.nextF(), rand.nextF()}; |
| path.moveTo(startPoint); |
| // Degenerate curves get skipped. |
| path.lineTo(startPoint); |
| path.quadTo(startPoint, startPoint); |
| path.cubicTo(startPoint, startPoint, startPoint); |
| for (int i = 0; i < 100; ++i) { |
| switch (rand.nextRangeU(0, 4)) { |
| case 0: |
| path.lineTo(rand.nextF(), rand.nextF()); |
| break; |
| case 1: |
| path.quadTo(rand.nextF(), rand.nextF(), rand.nextF(), rand.nextF()); |
| break; |
| case 2: |
| case 3: |
| case 4: |
| path.cubicTo(rand.nextF(), rand.nextF(), rand.nextF(), rand.nextF(), |
| rand.nextF(), rand.nextF()); |
| break; |
| default: |
| SkUNREACHABLE; |
| } |
| if (i % 19 == 0) { |
| switch (i/19 % 4) { |
| case 0: |
| break; |
| case 1: |
| path.lineTo(startPoint); |
| break; |
| case 2: |
| path.quadTo(SkPoint::Make(1.1f, 1.1f), startPoint); |
| break; |
| case 3: |
| path.cubicTo(SkPoint::Make(1.1f, 1.1f), SkPoint::Make(1.1f, 1.1f), |
| startPoint); |
| break; |
| } |
| path.close(); |
| if (rand.nextU() & 1) { // Implicit or explicit move? |
| startPoint = {rand.nextF(), rand.nextF()}; |
| path.moveTo(startPoint); |
| } |
| } |
| } |
| test_stroke(r, ctx.get(), target.get(), path, rand); |
| } |
| } |
| |
| // Returns the control point for the first/final join of a contour. |
| // If the contour is not closed, returns the start point. |
| static SkPoint get_contour_closing_control_point(SkPathPriv::RangeIter iter, |
| const SkPathPriv::RangeIter& end) { |
| auto [verb, p, w] = *iter; |
| SkASSERT(verb == SkPathVerb::kMove); |
| // Peek ahead to find the last control point. |
| SkPoint startPoint=p[0], lastControlPoint=p[0]; |
| for (++iter; iter != end; ++iter) { |
| auto [verb, p, w] = *iter; |
| switch (verb) { |
| case SkPathVerb::kMove: |
| return startPoint; |
| case SkPathVerb::kCubic: |
| if (p[2] != p[3]) { |
| lastControlPoint = p[2]; |
| break; |
| } |
| [[fallthrough]]; |
| case SkPathVerb::kQuad: |
| if (p[1] != p[2]) { |
| lastControlPoint = p[1]; |
| break; |
| } |
| [[fallthrough]]; |
| case SkPathVerb::kLine: |
| if (p[0] != p[1]) { |
| lastControlPoint = p[0]; |
| } |
| break; |
| case SkPathVerb::kConic: |
| SkUNREACHABLE; |
| case SkPathVerb::kClose: |
| return (p[0] == startPoint) ? lastControlPoint : p[0]; |
| } |
| } |
| return startPoint; |
| } |
| |
| static bool check_resolve_level(skiatest::Reporter* r, float numCombinedSegments, |
| int8_t actualLevel, float tolerance, bool printError = true) { |
| int8_t expectedLevel = sk_float_nextlog2(numCombinedSegments); |
| if ((actualLevel > expectedLevel && |
| actualLevel > sk_float_nextlog2(numCombinedSegments + tolerance)) || |
| (actualLevel < expectedLevel && |
| actualLevel < sk_float_nextlog2(numCombinedSegments - tolerance))) { |
| if (printError) { |
| ERRORF(r, "expected %f segments => resolveLevel=%i (got %i)\n", |
| numCombinedSegments, expectedLevel, actualLevel); |
| } |
| return false; |
| } |
| return true; |
| } |
| |
| static bool check_first_resolve_levels(skiatest::Reporter* r, |
| const SkTArray<float>& firstNumSegments, |
| int8_t** nextResolveLevel, float tolerance) { |
| for (float numSegments : firstNumSegments) { |
| if (numSegments < 0) { |
| int8_t val = *(*nextResolveLevel)++; |
| REPORTER_ASSERT(r, val == (int)numSegments); |
| continue; |
| } |
| // The first stroke's resolve levels aren't written out until the end of |
| // the contour. |
| if (!check_resolve_level(r, numSegments, *(*nextResolveLevel)++, tolerance)) { |
| return false; |
| } |
| } |
| return true; |
| } |
| |
| static float test_tolerance(SkPaint::Join joinType) { |
| // Ensure our fast approximation falls within 1.15 tessellation segments of the "correct" |
| // answer. This is more than good enough when our matrix scale can go up to 2^17. |
| float tolerance = 1.15f; |
| if (joinType == SkPaint::kRound_Join) { |
| // We approximate two different angles when there are round joins. Double the tolerance. |
| tolerance *= 2; |
| } |
| return tolerance; |
| } |
| |
| void GrStrokeIndirectTessellator::verifyResolveLevels(skiatest::Reporter* r, |
| GrMockOpTarget* target, |
| const SkMatrix& viewMatrix, |
| const SkPath& path, |
| const SkStrokeRec& stroke) { |
| auto tolerances = GrStrokeTolerances::MakeNonHairline(viewMatrix.getMaxScale(), |
| stroke.getWidth()); |
| int8_t resolveLevelForCircles = SkTPin<float>( |
| sk_float_nextlog2(tolerances.fNumRadialSegmentsPerRadian * SK_ScalarPI), |
| 1, kMaxResolveLevel); |
| float tolerance = test_tolerance(stroke.getJoin()); |
| int8_t* nextResolveLevel = fResolveLevels; |
| auto iterate = SkPathPriv::Iterate(path); |
| SkSTArray<3, float> firstNumSegments; |
| bool isFirstStroke = true; |
| SkPoint startPoint = {0,0}; |
| SkPoint lastControlPoint; |
| for (auto iter = iterate.begin(); iter != iterate.end(); ++iter) { |
| auto [verb, pts, w] = *iter; |
| switch (verb) { |
| int n; |
| SkPoint chops[10]; |
| case SkPathVerb::kMove: |
| startPoint = pts[0]; |
| lastControlPoint = get_contour_closing_control_point(iter, iterate.end()); |
| if (!check_first_resolve_levels(r, firstNumSegments, &nextResolveLevel, |
| tolerance)) { |
| return; |
| } |
| firstNumSegments.reset(); |
| isFirstStroke = true; |
| break; |
| case SkPathVerb::kLine: |
| if (pts[0] == pts[1]) { |
| break; |
| } |
| if (stroke.getJoin() == SkPaint::kRound_Join) { |
| float rotation = SkMeasureAngleBetweenVectors(pts[0] - lastControlPoint, |
| pts[1] - pts[0]); |
| float numSegments = rotation * tolerances.fNumRadialSegmentsPerRadian; |
| if (isFirstStroke) { |
| firstNumSegments.push_back(numSegments); |
| } else if (!check_resolve_level(r, numSegments, *nextResolveLevel++, |
| tolerance)) { |
| return; |
| } |
| } |
| lastControlPoint = pts[0]; |
| isFirstStroke = false; |
| break; |
| case SkPathVerb::kQuad: { |
| if (pts[0] == pts[1] && pts[1] == pts[2]) { |
| break; |
| } |
| SkVector a = pts[1] - pts[0]; |
| SkVector b = pts[2] - pts[1]; |
| bool hasCusp = (a.cross(b) == 0 && a.dot(b) < 0); |
| if (hasCusp) { |
| // The quad has a cusp. Make sure we wrote out a -resolveLevelForCircles. |
| if (isFirstStroke) { |
| firstNumSegments.push_back(-resolveLevelForCircles); |
| } else { |
| REPORTER_ASSERT(r, *nextResolveLevel++ == -resolveLevelForCircles); |
| } |
| } |
| float numParametricSegments = (hasCusp) ? 0 : GrWangsFormula::quadratic( |
| tolerances.fParametricPrecision, pts); |
| float rotation = (hasCusp) ? 0 : SkMeasureQuadRotation(pts); |
| if (stroke.getJoin() == SkPaint::kRound_Join) { |
| SkVector controlPoint = (pts[0] == pts[1]) ? pts[2] : pts[1]; |
| rotation += SkMeasureAngleBetweenVectors(pts[0] - lastControlPoint, |
| controlPoint - pts[0]); |
| } |
| float numRadialSegments = rotation * tolerances.fNumRadialSegmentsPerRadian; |
| float numSegments = numParametricSegments + numRadialSegments; |
| if (!hasCusp || stroke.getJoin() == SkPaint::kRound_Join) { |
| if (isFirstStroke) { |
| firstNumSegments.push_back(numSegments); |
| } else if (!check_resolve_level(r, numSegments, *nextResolveLevel++, |
| tolerance)) { |
| return; |
| } |
| } |
| lastControlPoint = (pts[2] == pts[1]) ? pts[0] : pts[1]; |
| isFirstStroke = false; |
| break; |
| } |
| case SkPathVerb::kCubic: { |
| if (pts[0] == pts[1] && pts[1] == pts[2] && pts[2] == pts[3]) { |
| break; |
| } |
| float T[2]; |
| bool areCusps = false; |
| n = GrPathUtils::findCubicConvex180Chops(pts, T, &areCusps); |
| SkChopCubicAt(pts, chops, T, n); |
| if (n > 0) { |
| int cuspResolveLevel = (areCusps) ? resolveLevelForCircles : 0; |
| int signal = -((n << 4) | cuspResolveLevel); |
| if (isFirstStroke) { |
| firstNumSegments.push_back((float)signal); |
| } else { |
| REPORTER_ASSERT(r, *nextResolveLevel++ == signal); |
| } |
| } |
| for (int i = 0; i <= n; ++i) { |
| // Find the number of segments with our unoptimized approach and make sure |
| // it matches the answer we got already. |
| SkPoint* p = chops + i*3; |
| float numParametricSegments = |
| GrWangsFormula::cubic(tolerances.fParametricPrecision, p); |
| SkVector tan0 = |
| ((p[0] == p[1]) ? (p[1] == p[2]) ? p[3] : p[2] : p[1]) - p[0]; |
| SkVector tan1 = |
| p[3] - ((p[3] == p[2]) ? (p[2] == p[1]) ? p[0] : p[1] : p[2]); |
| float rotation = SkMeasureAngleBetweenVectors(tan0, tan1); |
| if (i == 0 && stroke.getJoin() == SkPaint::kRound_Join) { |
| rotation += SkMeasureAngleBetweenVectors(p[0] - lastControlPoint, tan0); |
| } |
| float numRadialSegments = rotation * tolerances.fNumRadialSegmentsPerRadian; |
| float numSegments = numParametricSegments + numRadialSegments; |
| if (isFirstStroke) { |
| firstNumSegments.push_back(numSegments); |
| } else if (!check_resolve_level(r, numSegments, *nextResolveLevel++, |
| tolerance)) { |
| return; |
| } |
| } |
| lastControlPoint = |
| (pts[3] == pts[2]) ? (pts[2] == pts[1]) ? pts[0] : pts[1] : pts[2]; |
| isFirstStroke = false; |
| break; |
| } |
| case SkPathVerb::kConic: |
| SkUNREACHABLE; |
| case SkPathVerb::kClose: |
| if (pts[0] != startPoint) { |
| SkASSERT(!isFirstStroke); |
| if (stroke.getJoin() == SkPaint::kRound_Join) { |
| // Line from pts[0] to startPoint, with a preceding join. |
| float rotation = SkMeasureAngleBetweenVectors(pts[0] - lastControlPoint, |
| startPoint - pts[0]); |
| if (!check_resolve_level( |
| r, rotation * tolerances.fNumRadialSegmentsPerRadian, |
| *nextResolveLevel++, tolerance)) { |
| return; |
| } |
| } |
| } |
| if (!check_first_resolve_levels(r, firstNumSegments, &nextResolveLevel, |
| tolerance)) { |
| return; |
| } |
| firstNumSegments.reset(); |
| isFirstStroke = true; |
| break; |
| } |
| } |
| if (!check_first_resolve_levels(r, firstNumSegments, &nextResolveLevel, tolerance)) { |
| return; |
| } |
| firstNumSegments.reset(); |
| SkASSERT(nextResolveLevel == fResolveLevels + fResolveLevelArrayCount); |
| } |
| |
| void GrStrokeIndirectTessellator::verifyBuffers(skiatest::Reporter* r, GrMockOpTarget* target, |
| const SkMatrix& viewMatrix, |
| const SkStrokeRec& stroke) { |
| // Make sure the resolve level we assigned to each instance agrees with the actual data. |
| struct IndirectInstance { |
| SkPoint fPts[4]; |
| SkPoint fLastControlPoint; |
| float fNumTotalEdges; |
| }; |
| auto instance = static_cast<const IndirectInstance*>(target->peekStaticVertexData()); |
| auto* indirect = static_cast<const GrDrawIndirectCommand*>(target->peekStaticIndirectData()); |
| auto tolerances = GrStrokeTolerances::MakeNonHairline(viewMatrix.getMaxScale(), |
| stroke.getWidth()); |
| float tolerance = test_tolerance(stroke.getJoin()); |
| for (int i = 0; i < fChainedDrawIndirectCount; ++i) { |
| int numExtraEdgesInJoin = (stroke.getJoin() == SkPaint::kMiter_Join) ? 4 : 3; |
| int numStrokeEdges = indirect->fVertexCount/2 - numExtraEdgesInJoin; |
| int numSegments = numStrokeEdges - 1; |
| bool isPow2 = !(numSegments & (numSegments - 1)); |
| REPORTER_ASSERT(r, isPow2); |
| int resolveLevel = sk_float_nextlog2(numSegments); |
| REPORTER_ASSERT(r, 1 << resolveLevel == numSegments); |
| for (unsigned j = 0; j < indirect->fInstanceCount; ++j) { |
| SkASSERT(fabsf(instance->fNumTotalEdges) == indirect->fVertexCount/2); |
| const SkPoint* p = instance->fPts; |
| float numParametricSegments = GrWangsFormula::cubic(tolerances.fParametricPrecision, p); |
| float alternateNumParametricSegments = numParametricSegments; |
| if (p[0] == p[1] && p[2] == p[3]) { |
| // We articulate lines as "p0,p0,p1,p1". This one might actually expect 0 parametric |
| // segments. |
| alternateNumParametricSegments = 0; |
| } |
| SkVector tan0 = ((p[0] == p[1]) ? (p[1] == p[2]) ? p[3] : p[2] : p[1]) - p[0]; |
| SkVector tan1 = p[3] - ((p[3] == p[2]) ? (p[2] == p[1]) ? p[0] : p[1] : p[2]); |
| float rotation = SkMeasureAngleBetweenVectors(tan0, tan1); |
| // Negative fNumTotalEdges means the curve is a chop, and chops always get treated as a |
| // bevel join. |
| if (stroke.getJoin() == SkPaint::kRound_Join && instance->fNumTotalEdges > 0) { |
| SkVector lastTangent = p[0] - instance->fLastControlPoint; |
| rotation += SkMeasureAngleBetweenVectors(lastTangent, tan0); |
| } |
| // Degenerate strokes are a special case that actually mean the GPU should draw a cusp |
| // (i.e. circle). |
| if (p[0] == p[1] && p[1] == p[2] && p[2] == p[3]) { |
| rotation = SK_ScalarPI; |
| } |
| float numRadialSegments = rotation * tolerances.fNumRadialSegmentsPerRadian; |
| float numSegments = numParametricSegments + numRadialSegments; |
| float alternateNumSegments = alternateNumParametricSegments + numRadialSegments; |
| if (!check_resolve_level(r, numSegments, resolveLevel, tolerance, false) && |
| !check_resolve_level(r, alternateNumSegments, resolveLevel, tolerance, true)) { |
| return; |
| } |
| ++instance; |
| } |
| ++indirect; |
| } |
| } |