39ce12e0d4
Instead we calculate these values on the fly as needed. This is necessary for hairlines because the tessellator will operate on them in post-transform space, which requires different tolerances than the setup code. Bug: skia:10419 Change-Id: Ia8ffa8858b45949521c085ccbe5712b3842f785f Reviewed-on: https://skia-review.googlesource.com/c/skia/+/343499 Reviewed-by: Michael Ludwig <michaelludwig@google.com> Commit-Queue: Chris Dalton <csmartdalton@google.com>
482 lines
21 KiB
C++
482 lines
21 KiB
C++
/*
|
|
* 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/GrStrokeIndirectOp.h"
|
|
#include "src/gpu/tessellate/GrStrokeTessellateShader.h"
|
|
#include "src/gpu/tessellate/GrTessellationPathRenderer.h"
|
|
#include "src/gpu/tessellate/GrWangsFormula.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, 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 op = GrOp::Make<GrStrokeIndirectOp>(ctx, GrAAType::kMSAA,
|
|
SkMatrix::Scale(scale, scale), path, stroke,
|
|
GrPaint());
|
|
auto strokeIndirectOp = op->cast<GrStrokeIndirectOp>();
|
|
strokeIndirectOp->verifyPrePrepareResolveLevels(r, target);
|
|
strokeIndirectOp->verifyPrepareBuffers(r, target);
|
|
}
|
|
}
|
|
}
|
|
|
|
DEF_TEST(tessellate_GrStrokeIndirectOp, 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) {
|
|
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 GrStrokeIndirectOp::verifyPrePrepareResolveLevels(skiatest::Reporter* r,
|
|
GrMeshDrawOp::Target* target) {
|
|
GrStrokeTessellateShader::Tolerances tolerances(fViewMatrix.getMaxScale(), fStroke.getWidth());
|
|
float tolerance = test_tolerance(fStroke.getJoin());
|
|
// Fill in fResolveLevels with our resolve levels for each curve.
|
|
this->prePrepareResolveLevels(target->allocator());
|
|
int8_t* nextResolveLevel = fResolveLevels;
|
|
// Now check out answers.
|
|
for (const SkPath& path : fPathList) {
|
|
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 (fStroke.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 (fStroke.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 || fStroke.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 && fStroke.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 (fStroke.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 GrStrokeIndirectOp::verifyPrepareBuffers(skiatest::Reporter* r, GrMeshDrawOp::Target* target) {
|
|
using IndirectInstance = GrStrokeTessellateShader::IndirectInstance;
|
|
GrStrokeTessellateShader::Tolerances tolerances(fViewMatrix.getMaxScale(), fStroke.getWidth());
|
|
float tolerance = test_tolerance(fStroke.getJoin());
|
|
// Make sure the resolve level we assign to each instance agrees with the actual data.
|
|
this->prepareBuffers(target);
|
|
// 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 = (fStroke.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 (fStroke.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;
|
|
}
|
|
}
|