/* * 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/mock/GrMockOpTarget.h" #include "src/gpu/tessellate/GrStrokeIndirectTessellator.h" #include "src/gpu/tessellate/GrStrokeTessellateShader.h" #include "src/gpu/tessellate/GrTessellationPathRenderer.h" #include "src/gpu/tessellate/GrWangsFormula.h" static sk_sp 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, GrMeshDrawOp::Target* 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); GrStrokeIndirectTessellator tessellator(matrix, path, stroke, path.countVerbs(), target->allocator()); tessellator.verifyResolveLevels(r, target, matrix, path, stroke); tessellator.prepare(target, matrix, path, stroke, path.countVerbs()); tessellator.verifyBuffers(r, target, matrix, stroke); } } } DEF_TEST(tessellate_GrStrokeIndirectTessellator, r) { auto ctx = make_mock_context(); auto target = std::make_unique(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& firstNumSegments, int8_t** nextResolveLevel, float tolerance) { for (float numSegments : firstNumSegments) { if (numSegments < 0) { REPORTER_ASSERT(r, *(*nextResolveLevel)++ == (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, GrMeshDrawOp::Target* target, const SkMatrix& viewMatrix, const SkPath& path, const SkStrokeRec& stroke) { GrStrokeTessellateShader::Tolerances tolerances(viewMatrix.getMaxScale(), stroke.getWidth()); 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 -1 to signal that. if (isFirstStroke) { firstNumSegments.push_back(-1); } else { REPORTER_ASSERT(r, *nextResolveLevel++ == -1); } } float numParametricSegments = (hasCusp) ? 0 : GrWangsFormula::quadratic( tolerances.fParametricIntolerance, 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 signal = -((n << 1) | (int)areCusps); 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.fParametricIntolerance, 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, GrMeshDrawOp::Target* target, const SkMatrix& viewMatrix, const SkStrokeRec& stroke) { using IndirectInstance = GrStrokeTessellateShader::IndirectInstance; GrStrokeTessellateShader::Tolerances tolerances(viewMatrix.getMaxScale(), stroke.getWidth()); float tolerance = test_tolerance(stroke.getJoin()); // Make sure the resolve level we assign to each instance agrees with the actual data. // GrMockOpTarget returns the same pointers every time. int _; auto instance = (const IndirectInstance*)target->makeVertexSpace(0, 0, nullptr, &_); size_t __; auto indirect = target->makeDrawIndirectSpace(0, nullptr, &__); for (int i = 0; i < fDrawIndirectCount; ++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.data(); float numParametricSegments = GrWangsFormula::cubic( tolerances.fParametricIntolerance, 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; } }