skia2/tests/StrokeIndirectTest.cpp
Chris Dalton 82094cdb9d Chain indirect stroke ops that have mismatched colors
The indirect tessellator can't combine overlapping, mismatched colors
because the log2 binning draws things out of order. But we can still
chain them together and generate a single long list of indirect draws.

Bug: chromium:1172543
Bug: skia:10419
Change-Id: Id7fc7488411a2a189e24cd7dd692e5c78497f498
Reviewed-on: https://skia-review.googlesource.com/c/skia/+/370197
Reviewed-by: Brian Salomon <bsalomon@google.com>
2021-02-19 09:25:25 +00:00

488 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/GrStrokeIndirectTessellator.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, 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);
GrStrokeIndirectTessellator tessellator(GrStrokeTessellateShader::ShaderFlags::kNone,
matrix, {path, stroke, SK_PMColor4fWHITE},
path.countVerbs(), target->allocator());
tessellator.verifyResolveLevels(r, target, matrix, path, stroke);
tessellator.prepare(target, matrix);
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) {
GrStrokeTessellateShader::Tolerances tolerances(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.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 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.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, 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());
GrStrokeTessellateShader::Tolerances tolerances(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.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;
}
}