Implement stroke tessellation with indirect draws

Adds a vertex shader that maps a variable-length triangle strip to a
stroke and its preceding join. Adds a new op that generates stroke
instances from a path, bins them by log2 triangle strip length (using
SIMD for the calculations), and renders them with indirect draws.

Bug: skia:10419
Change-Id: I6d52df02cffe97d14827c6d66136957f1859f53b
Reviewed-on: https://skia-review.googlesource.com/c/skia/+/339716
Commit-Queue: Chris Dalton <csmartdalton@google.com>
Reviewed-by: Brian Salomon <bsalomon@google.com>
This commit is contained in:
Chris Dalton 2020-12-09 16:46:22 -07:00 committed by Skia Commit-Bot
parent 41a98e0a50
commit c2a1746b42
12 changed files with 2129 additions and 39 deletions

View File

@ -14,9 +14,11 @@
#include "src/gpu/tessellate/GrMiddleOutPolygonTriangulator.h"
#include "src/gpu/tessellate/GrPathTessellateOp.h"
#include "src/gpu/tessellate/GrResolveLevelCounter.h"
#include "src/gpu/tessellate/GrStrokeIndirectOp.h"
#include "src/gpu/tessellate/GrStrokeTessellateOp.h"
#include "src/gpu/tessellate/GrWangsFormula.h"
#include "tools/ToolUtils.h"
#include <vector>
// This is the number of cubics in desk_chalkboard.skp. (There are no quadratics in the chalkboard.)
constexpr static int kNumCubicsInChalkboard = 47182;
@ -239,3 +241,78 @@ private:
DEF_BENCH( return new GrStrokeTessellateOp::TestingOnly_Benchmark(1, ""); )
DEF_BENCH( return new GrStrokeTessellateOp::TestingOnly_Benchmark(5, "_one_chop"); )
class GrStrokeIndirectOp::Benchmark : public ::Benchmark {
public:
Benchmark(const char* nameSuffix, SkPaint::Join join, std::vector<SkPoint> pts)
: fJoin(join), fPts(std::move(pts)) {
fName.printf("tessellate_GrStrokeIndirectOpBench%s", nameSuffix);
}
private:
const char* onGetName() override { return fName.c_str(); }
bool isSuitableFor(Backend backend) final { return backend == kNonRendering_Backend; }
void onDelayedSetup() override {
fTarget = std::make_unique<GrMockOpTarget>(make_mock_context());
if (fJoin == SkPaint::kRound_Join) {
fPath.reset().moveTo(fPts.back());
for (size_t i = 0; i < kNumCubicsInChalkboard/fPts.size(); ++i) {
for (size_t j = 0; j < fPts.size(); ++j) {
fPath.lineTo(fPts[j]);
}
}
} else {
fPath.reset().moveTo(fPts[0]);
for (int i = 0; i < kNumCubicsInChalkboard/2; ++i) {
if (fPts.size() == 4) {
fPath.cubicTo(fPts[1], fPts[2], fPts[3]);
fPath.cubicTo(fPts[2], fPts[1], fPts[0]);
} else {
SkASSERT(fPts.size() == 3);
fPath.quadTo(fPts[1], fPts[2]);
fPath.quadTo(fPts[2], fPts[1]);
}
}
}
fStrokeRec.setStrokeStyle(8);
fStrokeRec.setStrokeParams(SkPaint::kButt_Cap, fJoin, 4);
}
void onDraw(int loops, SkCanvas*) final {
if (!fTarget->mockContext()) {
SkDebugf("ERROR: could not create mock context.");
return;
}
for (int i = 0; i < loops; ++i) {
GrStrokeIndirectOp op(GrAAType::kMSAA, SkMatrix::I(), fPath, fStrokeRec, GrPaint());
op.prePrepareResolveLevels(fTarget->allocator());
op.prepareBuffers(fTarget.get());
}
}
SkString fName;
SkPaint::Join fJoin;
std::vector<SkPoint> fPts;
std::unique_ptr<GrMockOpTarget> fTarget;
SkPath fPath;
SkStrokeRec fStrokeRec = SkStrokeRec(SkStrokeRec::kFill_InitStyle);
};
DEF_BENCH( return new GrStrokeIndirectOp::Benchmark(
"_inflect1", SkPaint::kBevel_Join, {{0,0}, {100,0}, {0,100}, {100,100}}); )
DEF_BENCH( return new GrStrokeIndirectOp::Benchmark(
"_inflect2", SkPaint::kBevel_Join, {{37,162}, {412,160}, {249,65}, {112,360}}); )
DEF_BENCH( return new GrStrokeIndirectOp::Benchmark(
"_loop", SkPaint::kBevel_Join, {{0,0}, {100,0}, {0,100}, {0,0}}); )
DEF_BENCH( return new GrStrokeIndirectOp::Benchmark(
"_nochop", SkPaint::kBevel_Join, {{0,0}, {50,0}, {100,50}, {100,100}}); )
DEF_BENCH( return new GrStrokeIndirectOp::Benchmark(
"_quad", SkPaint::kBevel_Join, {{0,0}, {50,100}, {100,0}}); )
DEF_BENCH( return new GrStrokeIndirectOp::Benchmark(
"_roundjoin", SkPaint::kRound_Join, {{0,0}, {50,100}, {100,0}}); )

View File

@ -465,6 +465,9 @@ skia_gpu_sources = [
"$_src/gpu/tessellate/GrResolveLevelCounter.h",
"$_src/gpu/tessellate/GrStencilPathShader.cpp",
"$_src/gpu/tessellate/GrStencilPathShader.h",
"$_src/gpu/tessellate/GrStrokeIndirectOp.cpp",
"$_src/gpu/tessellate/GrStrokeIndirectOp.h",
"$_src/gpu/tessellate/GrStrokeIterator.h",
"$_src/gpu/tessellate/GrStrokeOp.cpp",
"$_src/gpu/tessellate/GrStrokeOp.h",
"$_src/gpu/tessellate/GrStrokeTessellateOp.cpp",

View File

@ -288,6 +288,7 @@ tests_sources = [
"$_tests/StreamBufferTest.cpp",
"$_tests/StreamTest.cpp",
"$_tests/StringTest.cpp",
"$_tests/StrokeIndirectTest.cpp",
"$_tests/StrokeTest.cpp",
"$_tests/StrokerTest.cpp",
"$_tests/SubsetPath.cpp",

View File

@ -99,7 +99,7 @@ public:
private:
sk_sp<GrDirectContext> fMockContext;
char fStaticVertexData[4 * 1024 * 1024];
char fStaticVertexData[6 * 1024 * 1024];
GrDrawIndirectCommand fStaticDrawIndirectData[32];
GrDrawIndexedIndirectCommand fStaticDrawIndexedIndirectData[32];
SkSTArenaAllocWithReset<1024 * 1024> fAllocator;

View File

@ -0,0 +1,819 @@
/*
* 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 "src/gpu/tessellate/GrStrokeIndirectOp.h"
#include "src/core/SkGeometry.h"
#include "src/core/SkPathPriv.h"
#include "src/gpu/GrRecordingContextPriv.h"
#include "src/gpu/GrVx.h"
#include "src/gpu/geometry/GrPathUtils.h"
#include "src/gpu/tessellate/GrStrokeIterator.h"
#include "src/gpu/tessellate/GrStrokeTessellateShader.h"
#include "src/gpu/tessellate/GrWangsFormula.h"
GrStrokeIndirectOp::GrStrokeIndirectOp(GrAAType aaType, const SkMatrix& viewMatrix,
const SkPath& path, const SkStrokeRec& stroke,
GrPaint&& paint)
: GrStrokeOp(ClassID(), aaType, viewMatrix, stroke, path, std::move(paint))
, fResolveLevelForCircles(sk_float_nextlog2(fNumRadialSegmentsPerRadian * SK_ScalarPI)) {
}
void GrStrokeIndirectOp::onPrePrepare(GrRecordingContext* context,
const GrSurfaceProxyView& writeView, GrAppliedClip* clip,
const GrXferProcessor::DstProxyView& dstProxyView,
GrXferBarrierFlags renderPassXferBarriers,
GrLoadOp colorLoadOp) {
auto* arena = context->priv().recordTimeAllocator();
this->prePrepareResolveLevels(context->priv().recordTimeAllocator());
SkASSERT(fResolveLevels);
if (!fTotalInstanceCount) {
return;
}
auto* strokeTessellateShader = arena->make<GrStrokeTessellateShader>(
GrStrokeTessellateShader::Mode::kIndirect, fStroke, fParametricIntolerance,
fNumRadialSegmentsPerRadian, fViewMatrix, fColor);
this->prePrepareColorProgram(context->priv().recordTimeAllocator(), strokeTessellateShader,
writeView, std::move(*clip), dstProxyView, renderPassXferBarriers,
colorLoadOp, *context->priv().caps());
context->priv().recordProgramInfo(fColorProgram);
}
// Helpers for GrStrokeIndirectOp::prePrepareResolveLevels.
namespace {
#ifndef SKNX_NO_SIMD
using grvx::vec;
using grvx::ivec;
using grvx::uvec;
// Muxes between "N" (Nx2/2) 2d vectors in SIMD based on the provided conditions. This is equivalent
// to returning the following at each point:
//
// (conds.lo[i] & conds.hi[i])) ? {t[i].lo, t[i].hi} : {e[i].lo, e[i].hi}.
//
template<int Nx2>
static SK_ALWAYS_INLINE vec<Nx2> if_both_then_else(ivec<Nx2> conds, vec<Nx2> t, vec<Nx2> e) {
auto both = conds.lo & conds.hi;
return skvx::if_then_else(skvx::join(both, both), t, e);
}
// Returns the lengths squared of "N" (Nx2/2) 2d vectors in SIMD. The x values are in "v.lo" and
// the y values are in "v.hi".
template<int Nx2> static SK_ALWAYS_INLINE vec<Nx2/2> length_pow2(vec<Nx2> v) {
auto vv = v*v;
return vv.lo + vv.hi;
}
// Interpolates between "a" and "b" by a factor of T. T must be <1 and >= 0.
//
// NOTE: This does not return b when T==1. It's implemented as-is because it otherwise seems to get
// better precision than "a*(1 - T) + b*T" for things like chopping cubics on exact cusp points.
// The responsibility falls on the caller to check that T != 1 before calling.
template<int N> SK_ALWAYS_INLINE vec<N> unchecked_mix(vec<N> a, vec<N> b, vec<N> T) {
return grvx::fast_madd(b - a, T, a);
}
#endif
// Computes and writes out the resolveLevels for individual strokes. Maintains a counter of the
// number of instances at each resolveLevel. If SIMD is available, then these calculations are done
// in batches.
class ResolveLevelCounter {
public:
ResolveLevelCounter(float parametricIntolerance, float numRadialSegmentsPerRadian,
bool isRoundJoin, int* resolveLevelCounts)
#ifdef SKNX_NO_SIMD
: fParametricIntolerance(parametricIntolerance)
#else
: fWangsTermQuadratic(GrWangsFormula::length_term<2>(parametricIntolerance))
, fWangsTermCubic(GrWangsFormula::length_term<3>(parametricIntolerance))
#endif
, fNumRadialSegmentsPerRadian(numRadialSegmentsPerRadian)
, fIsRoundJoin(isRoundJoin)
, fResolveLevelCounts(resolveLevelCounts) {
}
#ifdef SKNX_NO_SIMD
bool SK_WARN_UNUSED_RESULT countLine(const SkPoint pts[2], SkPoint lastControlPoint,
int8_t* resolveLevelPtr) {
if (!fIsRoundJoin) {
// There is no resolve level to track. It's always zero.
++fResolveLevelCounts[0];
return false;
}
float rotation = SkMeasureAngleBetweenVectors(pts[0] - lastControlPoint, pts[1] - pts[0]);
this->writeResolveLevel(0, rotation, resolveLevelPtr);
return true;
}
void countQuad(const SkPoint pts[3], SkPoint lastControlPoint, int8_t* resolveLevelPtr) {
float numParametricSegments = GrWangsFormula::quadratic(fParametricIntolerance, pts);
float rotation = SkMeasureQuadRotation(pts);
if (fIsRoundJoin) {
SkVector nextTan = ((pts[0] == pts[1]) ? pts[2] : pts[1]) - pts[0];
rotation += SkMeasureAngleBetweenVectors(pts[0] - lastControlPoint, nextTan);
}
this->writeResolveLevel(numParametricSegments, rotation, resolveLevelPtr);
}
void countCubic(const SkPoint pts[4], SkPoint lastControlPoint, int8_t* resolveLevelPtr) {
float numParametricSegments = GrWangsFormula::cubic(fParametricIntolerance, pts);
SkVector tan0 = ((pts[0] == pts[1]) ? pts[2] : pts[1]) - pts[0];
SkVector tan1 = pts[3] - ((pts[3] == pts[2]) ? pts[1] : pts[2]);
float rotation = SkMeasureAngleBetweenVectors(tan0, tan1);
if (fIsRoundJoin && pts[0] != lastControlPoint) {
SkVector nextTan = (tan0.isZero()) ? tan1 : tan0;
rotation += SkMeasureAngleBetweenVectors(pts[0] - lastControlPoint, nextTan);
}
this->writeResolveLevel(numParametricSegments, rotation, resolveLevelPtr);
}
void countChoppedCubic(const SkPoint pts[4], const float chopT, SkPoint lastControlPoint,
int8_t* resolveLevelPtr) {
SkPoint chops[7];
SkChopCubicAt(pts, chops, chopT);
this->countCubic(chops, lastControlPoint, resolveLevelPtr);
this->countCubic(chops + 3, chops[3], resolveLevelPtr + 1);
}
void flush() {}
private:
void writeResolveLevel(float numParametricSegments, float rotation,
int8_t* resolveLevelPtr) const {
float numCombinedSegments = fNumRadialSegmentsPerRadian * rotation + numParametricSegments;
int8_t resolveLevel = sk_float_nextlog2(numCombinedSegments);
resolveLevel = std::min(resolveLevel, GrStrokeIndirectOp::kMaxResolveLevel);
++fResolveLevelCounts[(*resolveLevelPtr = resolveLevel)];
}
const float fParametricIntolerance;
#else // !defined(SKNX_NO_SIMD)
~ResolveLevelCounter() {
// Always call flush() when finished.
SkASSERT(fLineQueue.fCount == 0);
SkASSERT(fQuadQueue.fCount == 0);
SkASSERT(fCubicQueue.fCount == 0);
SkASSERT(fChoppedCubicQueue.fCount == 0);
}
bool SK_WARN_UNUSED_RESULT countLine(const SkPoint pts[2], SkPoint lastControlPoint,
int8_t* resolveLevelPtr) {
if (!fIsRoundJoin) {
// There is no resolve level to track. It's always zero.
++fResolveLevelCounts[0];
return false;
}
if (fLineQueue.push(pts, fIsRoundJoin, lastControlPoint, resolveLevelPtr) == 3) {
this->flushLines<4>();
}
return true;
}
void countQuad(const SkPoint pts[3], SkPoint lastControlPoint, int8_t* resolveLevelPtr) {
if (fQuadQueue.push(pts, fIsRoundJoin, lastControlPoint, resolveLevelPtr) == 3) {
this->flushQuads<4>();
}
}
void countCubic(const SkPoint pts[4], SkPoint lastControlPoint, int8_t* resolveLevelPtr) {
if (fCubicQueue.push(pts, fIsRoundJoin, lastControlPoint, resolveLevelPtr) == 3) {
this->flushCubics<4>();
}
}
void countChoppedCubic(const SkPoint pts[4], const float chopT, SkPoint lastControlPoint,
int8_t* resolveLevelPtr) {
int i = fChoppedCubicQueue.push(pts, fIsRoundJoin, lastControlPoint, resolveLevelPtr);
fCubicChopTs[i] = chopT;
if (i == 3) {
this->flushChoppedCubics<4>();
}
}
void flush() {
// Flush each queue, crunching either 2 curves in SIMD or 4. We do 2 when the queue is low
// because it allows us to expand two points into a single float4: [x0,x1,y0,y1].
if (fLineQueue.fCount) {
SkASSERT(fIsRoundJoin);
if (fLineQueue.fCount <= 2) {
this->flushLines<2>();
} else {
this->flushLines<4>();
}
}
if (fQuadQueue.fCount) {
if (fQuadQueue.fCount <= 2) {
this->flushQuads<2>();
} else {
this->flushQuads<4>();
}
}
if (fCubicQueue.fCount) {
if (fCubicQueue.fCount <= 2) {
this->flushCubics<2>();
} else {
this->flushCubics<4>();
}
}
if (fChoppedCubicQueue.fCount) {
if (fChoppedCubicQueue.fCount <= 2) {
this->flushChoppedCubics<2>();
} else {
this->flushChoppedCubics<4>();
}
}
}
private:
// This struct stores deferred resolveLevel calculations for performing in SIMD batches.
template<int NumPts> struct SIMDQueue {
// Enqueues a stroke.
SK_ALWAYS_INLINE int push(const SkPoint pts[NumPts], bool pushRoundJoin,
SkPoint lastControlPoint, int8_t* resolveLevelPtr) {
SkASSERT(0 <= fCount && fCount < 4);
if constexpr (NumPts == 4) {
// Store the points transposed in 4-point queues. The caller can use a strided load.
vec<4> x, y;
grvx::strided_load2(&pts[0].fX, x, y);
x.store(fXs[fCount].data());
y.store(fYs[fCount].data());
} else {
for (int i = 0; i < NumPts; ++i) {
fXs[i][fCount] = pts[i].fX;
fYs[i][fCount] = pts[i].fY;
}
}
if (pushRoundJoin) {
fLastControlPointsX[fCount] = lastControlPoint.fX;
fLastControlPointsY[fCount] = lastControlPoint.fY;
}
fResolveLevelPtrs[fCount] = resolveLevelPtr;
return fCount++;
}
// Loads pts[idx] in SIMD for all 4 strokes, with the x values in the "vec.lo" and the y
// values in "vec.hi".
template<int N> SK_ALWAYS_INLINE vec<N*2> loadPoint(int idx) const {
SkASSERT(0 <= idx && idx < NumPts);
static_assert(NumPts != 4, "4-point queues store transposed. Use grvx::strided_load4.");
return skvx::join(vec<N>::Load(fXs[idx].data()), vec<N>::Load(fYs[idx].data()));
}
// Loads all 4 lastControlPoints in SIMD, with the x values in "vec.lo" and the y values in
// "vec.hi".
template<int N> SK_ALWAYS_INLINE vec<N*2> loadLastControlPoint() const {
return skvx::join(vec<N>::Load(fLastControlPointsX), vec<N>::Load(fLastControlPointsY));
}
std::array<float,4> fXs[NumPts];
std::array<float,4> fYs[NumPts];
float fLastControlPointsX[4];
float fLastControlPointsY[4];
int8_t* fResolveLevelPtrs[4];
int fCount = 0;
};
template<int N> void flushLines() {
SkASSERT(fLineQueue.fCount > 0);
// Find the angle of rotation in the preceding round join.
auto a = fLineQueue.loadLastControlPoint<N>();
auto b = fLineQueue.loadPoint<N>(0);
auto c = fLineQueue.loadPoint<N>(1);
auto rotation = grvx::approx_angle_between_vectors(b - a, c - b);
this->writeResolveLevels<N>(0, rotation, fLineQueue.fCount, fLineQueue.fResolveLevelPtrs);
fLineQueue.fCount = 0;
}
template<int N> void flushQuads() {
SkASSERT(fQuadQueue.fCount > 0);
auto p0 = fQuadQueue.loadPoint<N>(0);
auto p1 = fQuadQueue.loadPoint<N>(1);
auto p2 = fQuadQueue.loadPoint<N>(2);
// Execute Wang's formula to determine how many parametric segments the curve needs to be
// divided into. (See GrWangsFormula::quadratic().)
auto l = length_pow2(grvx::fast_madd<N*2>(-2, p1, p2) + p0);
auto numParametricSegments = skvx::sqrt(fWangsTermQuadratic * skvx::sqrt(l));
// Find the curve's rotation. Since quads cannot inflect or rotate more than 180 degrees,
// this is equal to the angle between the beginning and ending tangents.
// NOTE: If p0==p1 or p1==p2, this will give rotation=0.
auto tan0 = p1 - p0;
auto tan1 = p2 - p1;
auto rotation = grvx::approx_angle_between_vectors(tan0, tan1);
if (fIsRoundJoin) {
// Add rotation for the preceding round join.
auto lastControlPoint = fQuadQueue.loadLastControlPoint<N>();
auto nextTan = if_both_then_else((tan0 == 0), tan1, tan0);
rotation += grvx::approx_angle_between_vectors(p0 - lastControlPoint, nextTan);
}
this->writeResolveLevels<N>(numParametricSegments, rotation, fQuadQueue.fCount,
fQuadQueue.fResolveLevelPtrs);
fQuadQueue.fCount = 0;
}
template<int N> void flushCubics() {
SkASSERT(fCubicQueue.fCount > 0);
vec<N*2> p0, p1, p2, p3;
grvx::strided_load4(fCubicQueue.fXs[0].data(), p0.lo, p1.lo, p2.lo, p3.lo);
grvx::strided_load4(fCubicQueue.fYs[0].data(), p0.hi, p1.hi, p2.hi, p3.hi);
this->flushCubics<N>(fCubicQueue, p0, p1, p2, p3, fIsRoundJoin, 0);
fCubicQueue.fCount = 0;
}
template<int N> void flushChoppedCubics() {
SkASSERT(fChoppedCubicQueue.fCount > 0);
vec<N*2> p0, p1, p2, p3;
grvx::strided_load4(fChoppedCubicQueue.fXs[0].data(), p0.lo, p1.lo, p2.lo, p3.lo);
grvx::strided_load4(fChoppedCubicQueue.fYs[0].data(), p0.hi, p1.hi, p2.hi, p3.hi);
// Chop the cubic at its chopT and find the resolve level for each half.
auto T = skvx::join(vec<N>::Load(fCubicChopTs), vec<N>::Load(fCubicChopTs));
auto ab = unchecked_mix(p0, p1, T);
auto bc = unchecked_mix(p1, p2, T);
auto cd = unchecked_mix(p2, p3, T);
auto abc = unchecked_mix(ab, bc, T);
auto bcd = unchecked_mix(bc, cd, T);
auto abcd = unchecked_mix(abc, bcd, T);
this->flushCubics<N>(fChoppedCubicQueue, p0, ab, abc, abcd, fIsRoundJoin, 0);
this->flushCubics<N>(fChoppedCubicQueue, abcd, bcd, cd, p3, false/*countRoundJoin*/, 1);
fChoppedCubicQueue.fCount = 0;
}
template<int N> SK_ALWAYS_INLINE void flushCubics(const SIMDQueue<4>& queue, vec<N*2> p0,
vec<N*2> p1, vec<N*2> p2, vec<N*2> p3,
bool countRoundJoin, int resultOffset) const {
// Execute Wang's formula to determine how many parametric segments the curve needs to be
// divided into. (See GrWangsFormula::cubic().)
auto l0 = length_pow2(grvx::fast_madd<N*2>(-2, p1, p2) + p0);
auto l1 = length_pow2(grvx::fast_madd<N*2>(-2, p2, p3) + p1);
auto numParametricSegments = skvx::sqrt(fWangsTermCubic * skvx::sqrt(skvx::max(l0, l1)));
// Find the starting tangent (or zero if p0==p1==p2).
auto tan0 = p1 - p0;
tan0 = if_both_then_else((tan0 == 0), p2 - p0, tan0);
// Find the ending tangent (or zero if p1==p2==p3).
auto tan1 = p3 - p2;
tan1 = if_both_then_else((tan1 == 0), p3 - p1, tan1);
// Find the curve's rotation. Since it cannot inflect or rotate more than 180 degrees at
// this point, this is equal to the angle between the beginning and ending tangents.
auto rotation = grvx::approx_angle_between_vectors(tan0, tan1);
if (countRoundJoin) {
// Add rotation for the preceding round join.
auto lastControlPoint = queue.loadLastControlPoint<N>();
auto nextTan = if_both_then_else((tan0 == 0), tan1, tan0);
rotation += grvx::approx_angle_between_vectors(p0 - lastControlPoint, nextTan);
}
this->writeResolveLevels<N>(numParametricSegments, rotation, queue.fCount,
queue.fResolveLevelPtrs, resultOffset);
}
template<int N> SK_ALWAYS_INLINE void writeResolveLevels(
vec<N> numParametricSegments, vec<N> rotation, int count,
int8_t* const* resolveLevelPtrs, int offset = 0) const {
auto numCombinedSegments = grvx::fast_madd<N>(
fNumRadialSegmentsPerRadian, rotation, numParametricSegments);
// Find ceil(log2(numCombinedSegments)) by twiddling the exponents. See sk_float_nextlog2().
auto bits = skvx::bit_pun<uvec<N>>(numCombinedSegments);
bits += (1u << 23) - 1u; // Increment the exponent for non-powers-of-2.
// This will make negative values, denorms, and negative exponents all < 0.
auto exp = (skvx::bit_pun<ivec<N>>(bits) >> 23) - 127;
auto level = skvx::pin<N,int>(exp, 0, GrStrokeIndirectOp::kMaxResolveLevel);
switch (count) {
default: SkUNREACHABLE;
case 4: ++fResolveLevelCounts[resolveLevelPtrs[3][offset] = level[3]]; [[fallthrough]];
case 3: ++fResolveLevelCounts[resolveLevelPtrs[2][offset] = level[2]]; [[fallthrough]];
case 2: ++fResolveLevelCounts[resolveLevelPtrs[1][offset] = level[1]]; [[fallthrough]];
case 1: ++fResolveLevelCounts[resolveLevelPtrs[0][offset] = level[0]]; break;
}
}
SIMDQueue<2> fLineQueue;
SIMDQueue<3> fQuadQueue;
SIMDQueue<4> fCubicQueue;
SIMDQueue<4> fChoppedCubicQueue;
float fCubicChopTs[4];
const float fWangsTermQuadratic;
const float fWangsTermCubic;
#endif
const float fNumRadialSegmentsPerRadian;
const bool fIsRoundJoin;
int* const fResolveLevelCounts;
};
} // namespace
void GrStrokeIndirectOp::prePrepareResolveLevels(SkArenaAlloc* alloc) {
SkASSERT(!fTotalInstanceCount);
SkASSERT(!fResolveLevels);
SkASSERT(!fResolveLevelArrayCount);
// The maximum potential number of values we will need in fResolveLevels is:
//
// * 3 segments per verb (from two chops)
// * Plus 1 extra resolveLevel per verb that says how many chops it needs
// * Plus 2 final resolveLevels for square caps at the very end not initiated by a "kMoveTo".
//
int resolveLevelAllocCount = fTotalCombinedVerbCnt * (3 + 1) + 2;
fResolveLevels = alloc->makeArrayDefault<int8_t>(resolveLevelAllocCount);
int8_t* nextResolveLevel = fResolveLevels;
// The maximum potential number of chopT values we will need is 2 per verb.
int chopTAllocCount = fTotalCombinedVerbCnt * 2;
fChopTs = alloc->makeArrayDefault<float>(chopTAllocCount);
float* nextChopTs = fChopTs;
SkPoint lastControlPoint = {0,0};
bool isRoundJoin = (fStroke.getJoin() == SkPaint::kRound_Join);
ResolveLevelCounter counter(fParametricIntolerance, fNumRadialSegmentsPerRadian,
isRoundJoin, fResolveLevelCounts);
for (const SkPath& path : fPathList) {
// Iterate through each verb in the stroke, counting its resolveLevel(s).
GrStrokeIterator iter(path, fStroke);
while (iter.next()) {
using Verb = GrStrokeIterator::Verb;
Verb verb = iter.verb();
if (!GrStrokeIterator::IsVerbGeometric(verb)) {
// We don't need to handle non-geomtric verbs.
continue;
}
const SkPoint* pts = iter.pts();
if (isRoundJoin) {
// Round joins need a "lastControlPoint" so we can measure the angle of the previous
// join. This doesn't have to be the exact control point we will send the GPU after
// any chopping; we just need a direction.
const SkPoint* prevPts = iter.prevPts();
switch (iter.prevVerb()) {
case Verb::kCubic:
if (prevPts[2] != prevPts[3]) {
lastControlPoint = prevPts[2];
break;
}
[[fallthrough]];
case Verb::kQuad:
if (prevPts[1] != prevPts[2]) {
lastControlPoint = prevPts[1];
break;
}
[[fallthrough]];
case Verb::kLine:
lastControlPoint = prevPts[0];
break;
case Verb::kMoveWithinContour:
case Verb::kCircle:
// There is no previous stroke to join to. Set lastControlPoint equal to the
// current point, which makes the direction 0 and the number of radial
// segments in the join 0.
lastControlPoint = pts[0];
break;
case Verb::kContourFinished:
SkUNREACHABLE;
}
}
switch (verb) {
case Verb::kLine:
if (counter.countLine(pts, lastControlPoint, nextResolveLevel)) {
++nextResolveLevel;
}
++fTotalInstanceCount;
break;
case Verb::kQuad: {
// Check for a cusp. A quadratic can only have a cusp if it is a degenerate flat
// line with a 180 degree turnarund. To detect this, the beginning and ending
// tangents must be parallel (a.cross(b) == 0) and pointing in opposite
// directions (a.dot(b) < 0).
SkVector a = pts[1] - pts[0];
SkVector b = pts[2] - pts[1];
if (a.cross(b) == 0 && a.dot(b) < 0) {
// The curve has a cusp. Draw two lines and a circle instead of a quad.
*nextResolveLevel++ = -1; // -1 signals a cusp.
if (counter.countLine(pts, lastControlPoint, nextResolveLevel)) {
++nextResolveLevel;
}
++fResolveLevelCounts[fResolveLevelForCircles]; // Circle instance.
++fResolveLevelCounts[0]; // Second line instance.
fTotalInstanceCount += 3;
} else {
counter.countQuad(pts, lastControlPoint, nextResolveLevel++);
++fTotalInstanceCount;
}
break;
}
case Verb::kCubic: {
bool areCusps = false;
int numChops = GrPathUtils::findCubicConvex180Chops(pts, nextChopTs, &areCusps);
if (areCusps && numChops > 0) {
fResolveLevelCounts[fResolveLevelForCircles] += numChops;
fTotalInstanceCount += numChops;
}
if (numChops == 0) {
counter.countCubic(pts, lastControlPoint, nextResolveLevel);
} else if (numChops == 1) {
// A negative resolveLevel indicates how many chops the curve needs, and
// whether they are cusps.
*nextResolveLevel++ = -((1 << 1) | (int)areCusps);
counter.countChoppedCubic(pts, nextChopTs[0], lastControlPoint,
nextResolveLevel);
} else {
SkASSERT(numChops == 2);
// A negative resolveLevel indicates how many chops the curve needs, and
// whether they are cusps.
*nextResolveLevel++ = -((2 << 1) | (int)areCusps);
SkPoint pts_[10];
SkChopCubicAt(pts, pts_, nextChopTs, 2);
counter.countCubic(pts_, lastControlPoint, nextResolveLevel);
counter.countCubic(pts_ + 3, pts_[3], nextResolveLevel + 1);
counter.countCubic(pts_ + 6, pts_[6], nextResolveLevel + 2);
}
nextResolveLevel += numChops + 1;
nextChopTs += numChops;
fTotalInstanceCount += numChops + 1;
break;
}
case Verb::kCircle:
// The iterator implements round caps as circles.
++fResolveLevelCounts[fResolveLevelForCircles];
++fTotalInstanceCount;
break;
case Verb::kMoveWithinContour:
case Verb::kContourFinished:
// We should have continued early for non-geometric verbs.
SkUNREACHABLE;
break;
}
}
}
counter.flush();
#ifdef SK_DEBUG
SkASSERT(nextResolveLevel <= fResolveLevels + resolveLevelAllocCount);
fResolveLevelArrayCount = nextResolveLevel - fResolveLevels;
SkASSERT(nextChopTs <= fChopTs + chopTAllocCount);
fChopTsArrayCount = nextChopTs - fChopTs;
fChopTsArrayCount = nextChopTs - fChopTs;
#endif
}
void GrStrokeIndirectOp::onPrepare(GrOpFlushState* flushState) {
if (!fResolveLevels) {
auto* arena = flushState->allocator();
this->prePrepareResolveLevels(arena);
if (!fTotalInstanceCount) {
return;
}
auto* strokeTessellateShader = arena->make<GrStrokeTessellateShader>(
GrStrokeTessellateShader::Mode::kIndirect, fStroke, fParametricIntolerance,
fNumRadialSegmentsPerRadian, fViewMatrix, fColor);
this->prePrepareColorProgram(arena, strokeTessellateShader, flushState->writeView(),
flushState->detachAppliedClip(), flushState->dstProxyView(),
flushState->renderPassBarriers(), flushState->colorLoadOp(),
flushState->caps());
}
SkASSERT(fResolveLevels);
this->prepareBuffers(flushState);
}
constexpr static int num_edges_in_resolve_level(int resolveLevel) {
// A resolveLevel means the instance is composed of 2^resolveLevel line segments.
int numSegments = 1 << resolveLevel;
// There are edges at the beginning and end both, so there is always one more edge than there
// are segments.
int numStrokeEdges = numSegments + 1;
return numStrokeEdges;
}
void GrStrokeIndirectOp::prepareBuffers(GrMeshDrawOp::Target* target) {
using IndirectInstance = GrStrokeTessellateShader::IndirectInstance;
SkASSERT(fResolveLevels);
SkASSERT(!fDrawIndirectBuffer);
SkASSERT(!fInstanceBuffer);
if (!fTotalInstanceCount) {
return;
}
// Allocate enough indirect commands for every resolve level. We will putBack the unused ones
// at the end.
GrDrawIndirectCommand* drawIndirectData = target->makeDrawIndirectSpace(
kMaxResolveLevel + 1, &fDrawIndirectBuffer, &fDrawIndirectOffset);
if (!drawIndirectData) {
SkASSERT(!fDrawIndirectBuffer);
return;
}
// We already know the instance count. Allocate an instance for each.
int baseInstance;
IndirectInstance* instanceData = static_cast<IndirectInstance*>(target->makeVertexSpace(
sizeof(IndirectInstance), fTotalInstanceCount, &fInstanceBuffer, &baseInstance));
if (!instanceData) {
SkASSERT(!fInstanceBuffer);
fDrawIndirectBuffer.reset();
return;
}
// Fill out our drawIndirect commands and determine the layout of the instance buffer.
fDrawIndirectCount = 0;
int numExtraEdgesInJoin = IndirectInstance::NumExtraEdgesInJoin(fStroke.getJoin());
int currentInstanceIdx = 0;
float numEdgesPerResolveLevel[kMaxResolveLevel];
IndirectInstance* nextInstanceLocations[kMaxResolveLevel + 1];
SkDEBUGCODE(IndirectInstance* endInstanceLocations[kMaxResolveLevel];)
for (int i = 0; i <= kMaxResolveLevel; ++i) {
if (fResolveLevelCounts[i]) {
int numEdges = numExtraEdgesInJoin + num_edges_in_resolve_level(i);
auto& cmd = drawIndirectData[fDrawIndirectCount++];
cmd.fVertexCount = numEdges * 2;
cmd.fInstanceCount = fResolveLevelCounts[i];
cmd.fBaseVertex = 0;
cmd.fBaseInstance = baseInstance + currentInstanceIdx;
numEdgesPerResolveLevel[i] = numEdges;
nextInstanceLocations[i] = instanceData + currentInstanceIdx;
#ifdef SK_DEBUG
} else {
nextInstanceLocations[i] = nullptr;
}
if (i > 0) {
endInstanceLocations[i - 1] = instanceData + currentInstanceIdx;
SkASSERT(endInstanceLocations[i - 1] <= instanceData + fTotalInstanceCount);
#endif
}
currentInstanceIdx += fResolveLevelCounts[i];
}
SkASSERT(currentInstanceIdx == fTotalInstanceCount);
SkASSERT(fDrawIndirectCount);
target->putBackIndirectDraws(kMaxResolveLevel + 1 - fDrawIndirectCount);
SkPoint scratchBuffer[4 + 10];
SkPoint* scratch = scratchBuffer;
bool isRoundJoin = (fStroke.getJoin() == SkPaint::kRound_Join);
int8_t* nextResolveLevel = fResolveLevels;
float* nextChopTs = fChopTs;
SkPoint lastControlPoint = {0,0};
const SkPoint* firstCubic = nullptr;
int8_t firstResolveLevel = -1;
int8_t resolveLevel;
// Now write out each instance to its resolveLevel's designated location in the instance buffer.
for (const SkPath& path : fPathList) {
GrStrokeIterator iter(path, fStroke);
bool hasLastControlPoint = false;
while (iter.next()) {
using Verb = GrStrokeIterator::Verb;
int numChops = 0;
const SkPoint* pts=iter.pts(), *pts_=pts;
Verb verb = iter.verb();
switch (verb) {
case Verb::kCircle:
nextInstanceLocations[fResolveLevelForCircles]++->setCircle(
pts[0], numEdgesPerResolveLevel[fResolveLevelForCircles]);
[[fallthrough]];
case Verb::kMoveWithinContour:
// The next verb won't be joined to anything.
lastControlPoint = pts[0];
hasLastControlPoint = true;
continue;
case Verb::kContourFinished:
SkASSERT(hasLastControlPoint);
if (firstCubic) {
// Emit the initial cubic that we deferred at the beginning.
nextInstanceLocations[firstResolveLevel]++->set(firstCubic,
lastControlPoint, numEdgesPerResolveLevel[firstResolveLevel]);
firstCubic = nullptr;
}
hasLastControlPoint = false;
// Restore "scratch" to the original scratchBuffer.
scratch = scratchBuffer;
continue;
case Verb::kLine:
scratch[0] = scratch[1] = pts[0];
scratch[2] = scratch[3] = pts[1];
pts_ = scratch;
resolveLevel = (isRoundJoin) ? *nextResolveLevel++ : 0;
break;
case Verb::kQuad:
resolveLevel = *nextResolveLevel++;
if (resolveLevel < 0) {
// The curve has a cusp. Draw two lines and a circle instead of a quad.
SkASSERT(resolveLevel == -1);
float cuspT = SkFindQuadMidTangent(pts);
SkPoint cusp = SkEvalQuadAt(pts, cuspT);
resolveLevel = (isRoundJoin) ? *nextResolveLevel++ : 0;
numChops = 1;
scratch[0] = scratch[1] = pts[0];
scratch[2] = scratch[3] = scratch[4] = cusp;
scratch[5] = scratch[6] = pts[2];
nextInstanceLocations[fResolveLevelForCircles]++->setCircle(
cusp, numEdgesPerResolveLevel[fResolveLevelForCircles]);
} else {
GrPathUtils::convertQuadToCubic(pts, scratch);
}
pts_ = scratch;
break;
case Verb::kCubic:
resolveLevel = *nextResolveLevel++;
if (resolveLevel < 0) {
// A negative resolveLevel indicates how many chops the curve needs, and
// whether they are cusps.
numChops = -resolveLevel >> 1;
SkChopCubicAt(pts, scratch, nextChopTs, numChops);
nextChopTs += numChops;
pts_ = scratch;
if (-resolveLevel & 1) { // Are the chop points cusps?
for (int i = 1; i <= numChops; ++i) {
nextInstanceLocations[fResolveLevelForCircles]++->setCircle(
pts_[i*3],numEdgesPerResolveLevel[fResolveLevelForCircles]);
}
}
resolveLevel = *nextResolveLevel++;
}
break;
}
for (int i = 0;;) {
if (!hasLastControlPoint) {
SkASSERT(!firstCubic);
// Defer the initial cubic until we know its previous control point.
firstCubic = pts_;
firstResolveLevel = resolveLevel;
// Increment the scratch pts in case that's where our first cubic is stored.
scratch += 4;
} else {
int numEdges = numEdgesPerResolveLevel[resolveLevel];
nextInstanceLocations[resolveLevel]++->set(pts_, lastControlPoint,
// Negative numEdges will tell the GPU that this stroke instance follows
// a chop, and round joins from chopping always get exactly one segment.
(i == 0) ? numEdges : -numEdges);
}
// Determine the last control point.
if (pts_[2] != pts_[3]) {
lastControlPoint = pts_[2];
} else if (pts_[1] != pts_[3]) {
lastControlPoint = pts_[1];
} else if (pts_[0] != pts_[3]) {
lastControlPoint = pts_[0];
} else {
// This is very unusual, but all chops became degenerate. Don't update the
// lastControlPoint.
}
hasLastControlPoint = true;
if (i++ == numChops) {
break;
}
pts_ += 3;
// If a non-cubic got chopped, it means it was chopped into lines and a circle.
resolveLevel = (verb == Verb::kCubic) ? *nextResolveLevel++ : 0;
SkASSERT(verb == Verb::kQuad || verb == Verb::kCubic);
}
}
}
#ifdef SK_DEBUG
SkASSERT(nextResolveLevel == fResolveLevels + fResolveLevelArrayCount);
SkASSERT(nextChopTs == fChopTs + fChopTsArrayCount);
auto lastInstanceLocation = nextInstanceLocations[kMaxResolveLevel];
for (int i = kMaxResolveLevel - 1; i >= 0; --i) {
if (nextInstanceLocations[i]) {
SkASSERT(nextInstanceLocations[i] == endInstanceLocations[i]);
}
if (!lastInstanceLocation) {
lastInstanceLocation = nextInstanceLocations[i];
}
}
SkDEBUGCODE(lastInstanceLocation = instanceData + fTotalInstanceCount;)
#endif
}
void GrStrokeIndirectOp::onExecute(GrOpFlushState* flushState, const SkRect& chainBounds) {
if (!fInstanceBuffer) {
return;
}
SkASSERT(fDrawIndirectCount);
SkASSERT(fTotalInstanceCount > 0);
SkASSERT(fColorProgram);
SkASSERT(chainBounds == this->bounds());
flushState->bindPipelineAndScissorClip(*fColorProgram, this->bounds());
flushState->bindTextures(fColorProgram->primProc(), nullptr, fColorProgram->pipeline());
flushState->bindBuffers(nullptr, fInstanceBuffer, nullptr);
flushState->drawIndirect(fDrawIndirectBuffer.get(), fDrawIndirectOffset, fDrawIndirectCount);
}

View File

@ -0,0 +1,78 @@
/*
* Copyright 2020 Google Inc.
*
* Use of this source code is governed by a BSD-style license that can be
* found in the LICENSE file.
*/
#ifndef GrStrokeIndirectOp_DEFINED
#define GrStrokeIndirectOp_DEFINED
#include "src/gpu/ops/GrMeshDrawOp.h"
#include "src/gpu/tessellate/GrStrokeOp.h"
struct SkPoint;
namespace skiatest { class Reporter; }
// This class bins strokes into indirect draws for consumption by GrStrokeTessellateShader.
class GrStrokeIndirectOp : public GrStrokeOp {
public:
DEFINE_OP_CLASS_ID
// Don't allow more than 2^15 stroke edges in a triangle strip. GrTessellationPathRenderer
// already crops paths that require more than 2^10 parametric segments, so this should only
// become an issue if we try to draw a stroke with an astronomically wide width.
constexpr static int8_t kMaxResolveLevel = 15;
private:
GrStrokeIndirectOp(GrAAType, const SkMatrix&, const SkPath&, const SkStrokeRec&, GrPaint&&);
const char* name() const override { return "GrStrokeIndirectOp"; }
void onPrePrepare(GrRecordingContext*, const GrSurfaceProxyView&, GrAppliedClip*,
const GrXferProcessor::DstProxyView&, GrXferBarrierFlags,
GrLoadOp colorLoadOp) override;
void prePrepareResolveLevels(SkArenaAlloc*);
void onPrepare(GrOpFlushState*) override;
void prepareBuffers(GrMeshDrawOp::Target*);
void onExecute(GrOpFlushState*, const SkRect& chainBounds) override;
int fResolveLevelCounts[kMaxResolveLevel + 1] = {0}; // # of instances at each resolve level.
int fTotalInstanceCount = 0; // Total number of stroke instances we will draw.
// This array holds a resolveLevel for each stroke in the path, stored in the iteration order of
// GrStrokeIterator. If a stroke needs to be chopped, the array will contain a negative number
// whose absolute value is the number of chops required, followed by a resolveLevel for each
// resulting stroke after the chop(s).
int8_t* fResolveLevels = nullptr;
// fResolveLevelArrayCount != fTotalInstanceCount because we don't always need to write out
// resolve levels for line instances. (If they don't have round caps then their resolve level is
// just 0.)
SkDEBUGCODE(int fResolveLevelArrayCount = 0;)
// A "circle" is a stroke-width circle drawn as a 180-degree point stroke. We draw them at cusp
// points on curves and for round caps.
const int8_t fResolveLevelForCircles;
// Stores the in-order chop locations for all chops indicated by fResolveLevels.
float* fChopTs = nullptr;
SkDEBUGCODE(int fChopTsArrayCount = 0;)
// GPU buffers for drawing.
sk_sp<const GrBuffer> fDrawIndirectBuffer;
sk_sp<const GrBuffer> fInstanceBuffer;
size_t fDrawIndirectOffset;
int fDrawIndirectCount = 0;
friend class GrOp; // For ctor.
#if GR_TEST_UTILS
public:
void verifyPrePrepareResolveLevels(skiatest::Reporter*, GrMeshDrawOp::Target*);
void verifyPrepareBuffers(skiatest::Reporter*, GrMeshDrawOp::Target*);
class Benchmark;
#endif
};
#endif

View File

@ -0,0 +1,308 @@
/*
* Copyright 2020 Google Inc.
*
* Use of this source code is governed by a BSD-style license that can be
* found in the LICENSE file.
*/
#ifndef GrStrokeIterator_DEFINED
#define GrStrokeIterator_DEFINED
#include "include/core/SkPaint.h"
#include "include/core/SkStrokeRec.h"
#include "src/core/SkPathPriv.h"
#include <array>
// This class iterates over the stroke geometry defined by a path and stroke. It automatically
// converts closes and square caps to lines, and round caps to circles so the user doesn't have to
// worry about it. At each location it provides a verb and "prevVerb" so there is context about the
// preceding join. Usage:
//
// GrStrokeIterator iter(path, stroke);
// while (iter.next()) { // Call next() first.
// iter.verb();
// iter.pts();
// iter.prevVerb();
// iter.prevPts();
// }
//
class GrStrokeIterator {
public:
GrStrokeIterator(const SkPath& path, const SkStrokeRec& stroke)
: fCapType(stroke.getCap()), fStrokeRadius(stroke.getWidth() * .5) {
SkPathPriv::Iterate it(path);
fIter = it.begin();
fEnd = it.end();
}
enum class Verb {
// Verbs that describe stroke geometry.
kLine = (int)SkPathVerb::kLine,
kQuad = (int)SkPathVerb::kQuad,
kCubic = (int)SkPathVerb::kCubic,
kCircle, // A stroke-width circle drawn as a 180-degree point stroke.
// Helper verbs that notify callers to update their own iteration state.
kMoveWithinContour,
kContourFinished
};
constexpr static bool IsVerbGeometric(Verb verb) { return verb < Verb::kMoveWithinContour; }
// Must be called first. Loads the next pair of "prev" and "current" stroke. Returns false if
// iteration is complete.
bool next() {
if (fQueueCount) {
SkASSERT(fQueueCount >= 2);
this->popFront();
if (fQueueCount >= 2) {
return true;
}
SkASSERT(fQueueCount == 1);
if (this->atVerb(0) == Verb::kContourFinished) {
// Don't let "kContourFinished" be prevVerb at the start of the next contour.
fQueueCount = 0;
}
}
for (; fIter != fEnd; ++fIter) {
SkASSERT(fQueueCount == 0 || fQueueCount == 1);
auto [verb, pts, w] = *fIter;
switch (verb) {
case SkPathVerb::kMove:
if (!this->finishOpenContour()) {
continue;
}
break;
case SkPathVerb::kCubic:
if (pts[3] == pts[2]) {
[[fallthrough]]; // i.e., "if (p3 == p2 && p2 == p1 && p1 == p0)"
case SkPathVerb::kQuad:
if (pts[2] == pts[1]) {
[[fallthrough]]; // i.e., "if (p2 == p1 && p1 == p0)"
case SkPathVerb::kLine:
if (pts[1] == pts[0]) {
fLastDegenerateStrokePt = pts;
continue;
}}}
this->enqueue((Verb)verb, pts);
if (fQueueCount == 1) {
// Defer the first verb until the end when we know what it's joined to.
fFirstVerbInContour = (Verb)verb;
fFirstPtsInContour = pts;
continue;
}
break;
case SkPathVerb::kClose:
if (!fQueueCount) {
fLastDegenerateStrokePt = pts;
continue;
}
if (pts[0] != fFirstPtsInContour[0]) {
// Draw a line back to the contour's starting point.
fClosePts = {pts[0], fFirstPtsInContour[0]};
this->enqueue(Verb::kLine, fClosePts.data());
}
// Repeat the first verb, this time as the "current" stroke instead of the prev.
this->enqueue(fFirstVerbInContour, fFirstPtsInContour);
this->enqueue(Verb::kContourFinished, nullptr);
fLastDegenerateStrokePt = nullptr;
break;
case SkPathVerb::kConic:
SkUNREACHABLE;
}
SkASSERT(fQueueCount >= 2);
++fIter;
return true;
}
return this->finishOpenContour();
}
Verb prevVerb() const { return this->atVerb(0); }
const SkPoint* prevPts() const { return this->atPts(0); }
Verb verb() const { return this->atVerb(1); }
const SkPoint* pts() const { return this->atPts(1); }
Verb firstVerbInContour() const { SkASSERT(fQueueCount > 0); return fFirstVerbInContour; }
const SkPoint* firstPtsInContour() const {
SkASSERT(fQueueCount > 0);
return fFirstPtsInContour;
}
private:
constexpr static int kQueueBufferCount = 8;
Verb atVerb(int i) const {
SkASSERT(0 <= i && i < fQueueCount);
return fVerbs[(fQueueFrontIdx + i) & (kQueueBufferCount - 1)];
}
Verb backVerb() const {
return this->atVerb(fQueueCount - 1);
}
const SkPoint* atPts(int i) const {
SkASSERT(0 <= i && i < fQueueCount);
return fPts[(fQueueFrontIdx + i) & (kQueueBufferCount - 1)];
}
const SkPoint* backPts() const {
return this->atPts(fQueueCount - 1);
}
void enqueue(Verb verb, const SkPoint* pts) {
SkASSERT(fQueueCount < kQueueBufferCount);
int i = (fQueueFrontIdx + fQueueCount) & (kQueueBufferCount - 1);
fVerbs[i] = verb;
fPts[i] = pts;
++fQueueCount;
}
void popFront() {
SkASSERT(fQueueCount > 0);
++fQueueFrontIdx;
--fQueueCount;
}
// Finishes the current contour without closing it. Enqueues any necessary caps as well as the
// contour's first stroke that we deferred at the beginning.
// Returns false and makes no changes if the current contour was already finished.
bool finishOpenContour() {
if (fQueueCount) {
SkASSERT(this->backVerb() == Verb::kLine || this->backVerb() == Verb::kQuad ||
this->backVerb() == Verb::kCubic);
switch (fCapType) {
case SkPaint::kButt_Cap:
// There are no caps, but inject a "move" so the first stroke doesn't get joined
// with the end of the contour when it's processed.
this->enqueue(Verb::kMoveWithinContour, fFirstPtsInContour);
break;
case SkPaint::kRound_Cap: {
// The "kCircle" verb serves as our barrier to prevent the first stroke from
// getting joined with the end of the contour. We just need to make sure that
// the first point of the contour goes last.
int backIdx = SkPathPriv::PtsInIter((unsigned)this->backVerb()) - 1;
this->enqueue(Verb::kCircle, this->backPts() + backIdx);
this->enqueue(Verb::kCircle, fFirstPtsInContour);
break;
}
case SkPaint::kSquare_Cap:
this->fillSquareCapPoints(); // Fills in fEndingCapPts and fBeginningCapPts.
// Append the ending cap onto the current contour.
this->enqueue(Verb::kLine, fEndingCapPts.data());
// Move to the beginning cap and append it right before (and joined to) the
// first stroke (that we will add below).
this->enqueue(Verb::kMoveWithinContour, fBeginningCapPts.data());
this->enqueue(Verb::kLine, fBeginningCapPts.data());
break;
}
} else if (fLastDegenerateStrokePt) {
// fQueueCount=0 means this subpath is zero length. Generates caps on its location.
//
// "Any zero length subpath ... shall be stroked if the 'stroke-linecap' property has
// a value of round or square producing respectively a circle or a square."
//
// (https://www.w3.org/TR/SVG11/painting.html#StrokeProperties)
//
switch (fCapType) {
case SkPaint::kButt_Cap:
// Zero-length contour with butt caps. There are no caps and no first stroke to
// generate.
return false;
case SkPaint::kRound_Cap:
this->enqueue(Verb::kCircle, fLastDegenerateStrokePt);
// Setting the "first" stroke as the circle causes it to be added again below,
// this time as the "current" stroke.
fFirstVerbInContour = Verb::kCircle;
fFirstPtsInContour = fLastDegenerateStrokePt;
break;
case SkPaint::kSquare_Cap:
fEndingCapPts = {*fLastDegenerateStrokePt - SkPoint{fStrokeRadius, 0},
*fLastDegenerateStrokePt + SkPoint{fStrokeRadius, 0}};
// Add the square first as the "prev" join.
this->enqueue(Verb::kLine, fEndingCapPts.data());
this->enqueue(Verb::kMoveWithinContour, fEndingCapPts.data());
// Setting the "first" stroke as the square causes it to be added again below,
// this time as the "current" stroke.
fFirstVerbInContour = Verb::kLine;
fFirstPtsInContour = fEndingCapPts.data();
break;
}
} else {
// This contour had no lines, beziers, or "close" verbs. There are no caps and no first
// stroke to generate.
return false;
}
// Repeat the first verb, this time as the "current" stroke instead of the prev.
this->enqueue(fFirstVerbInContour, fFirstPtsInContour);
this->enqueue(Verb::kContourFinished, nullptr);
fLastDegenerateStrokePt = nullptr;
return true;
}
// We implement square caps as two extra "kLine" verbs. This method finds the endpoints for
// those lines.
void fillSquareCapPoints() {
// Find the endpoints of the cap at the end of the contour.
SkVector lastTangent;
const SkPoint* lastPts = this->backPts();
Verb lastVerb = this->backVerb();
switch (lastVerb) {
case Verb::kCubic:
lastTangent = lastPts[3] - lastPts[2];
if (!lastTangent.isZero()) {
break;
}
[[fallthrough]];
case Verb::kQuad:
lastTangent = lastPts[2] - lastPts[1];
if (!lastTangent.isZero()) {
break;
}
[[fallthrough]];
case Verb::kLine:
lastTangent = lastPts[1] - lastPts[0];
SkASSERT(!lastTangent.isZero());
break;
default:
SkUNREACHABLE;
}
lastTangent.normalize();
SkPoint lastPoint = lastPts[SkPathPriv::PtsInIter((unsigned)lastVerb) - 1];
fEndingCapPts = {lastPoint, lastPoint + lastTangent * fStrokeRadius};
// Find the endpoints of the cap at the beginning of the contour.
SkVector firstTangent = fFirstPtsInContour[1] - fFirstPtsInContour[0];
if (firstTangent.isZero()) {
SkASSERT(fFirstVerbInContour == Verb::kQuad || fFirstVerbInContour == Verb::kCubic);
firstTangent = fFirstPtsInContour[2] - fFirstPtsInContour[0];
if (firstTangent.isZero()) {
SkASSERT(fFirstVerbInContour == Verb::kCubic);
firstTangent = fFirstPtsInContour[3] - fFirstPtsInContour[0];
SkASSERT(!firstTangent.isZero());
}
}
firstTangent.normalize();
fBeginningCapPts = {fFirstPtsInContour[0] - firstTangent * fStrokeRadius,
fFirstPtsInContour[0]};
}
// Info and iterators from the original path.
const SkPaint::Cap fCapType;
const float fStrokeRadius;
SkPathPriv::RangeIter fIter;
SkPathPriv::RangeIter fEnd;
// Info for the current contour we are iterating.
Verb fFirstVerbInContour;
const SkPoint* fFirstPtsInContour;
const SkPoint* fLastDegenerateStrokePt = nullptr;
// The queue is implemented as a roll-over array with a floating front index.
Verb fVerbs[kQueueBufferCount];
const SkPoint* fPts[kQueueBufferCount];
int fQueueFrontIdx = 0;
int fQueueCount = 0;
// Storage space for geometry that gets defined implicitly by the path, but does not have
// actual points in memory to reference.
std::array<SkPoint, 2> fClosePts;
std::array<SkPoint, 2> fEndingCapPts;
std::array<SkPoint, 2> fBeginningCapPts;
};
#endif

View File

@ -21,7 +21,8 @@ void GrStrokeTessellateOp::onPrePrepare(GrRecordingContext* context,
GrLoadOp colorLoadOp) {
SkArenaAlloc* arena = context->priv().recordTimeAllocator();
auto* strokeTessellateShader = arena->make<GrStrokeTessellateShader>(
fStroke, fParametricIntolerance, fNumRadialSegmentsPerRadian, fViewMatrix, fColor);
GrStrokeTessellateShader::Mode::kTessellation, fStroke, fParametricIntolerance,
fNumRadialSegmentsPerRadian, fViewMatrix, fColor);
this->prePrepareColorProgram(arena, strokeTessellateShader, writeView, std::move(*clip),
dstProxyView, renderPassXferBarriers, colorLoadOp,
*context->priv().caps());
@ -32,7 +33,8 @@ void GrStrokeTessellateOp::onPrepare(GrOpFlushState* flushState) {
if (!fColorProgram) {
SkArenaAlloc* arena = flushState->allocator();
auto* strokeTessellateShader = arena->make<GrStrokeTessellateShader>(
fStroke, fParametricIntolerance, fNumRadialSegmentsPerRadian, fViewMatrix, fColor);
GrStrokeTessellateShader::Mode::kTessellation, fStroke, fParametricIntolerance,
fNumRadialSegmentsPerRadian, fViewMatrix, fColor);
this->prePrepareColorProgram(flushState->allocator(), strokeTessellateShader,
flushState->writeView(), flushState->detachAppliedClip(),
flushState->dstProxyView(), flushState->renderPassBarriers(),

View File

@ -13,7 +13,7 @@
#include "src/gpu/glsl/GrGLSLVertexGeoBuilder.h"
#include "src/gpu/tessellate/GrWangsFormula.h"
class GrStrokeTessellateShader::Impl : public GrGLSLGeometryProcessor {
class GrStrokeTessellateShader::TessellationImpl : public GrGLSLGeometryProcessor {
public:
const char* getTessArgs1UniformName(const GrGLSLUniformHandler& uniformHandler) const {
return uniformHandler.getUniformCStr(fTessArgs1Uniform);
@ -287,13 +287,13 @@ private:
};
// The built-in atan() is undefined when x==0. This method relieves that restriction, but also can
// return values larger than 2*kPI. This shouldn't matter for our purposes.
// return values larger than 2*PI. This shouldn't matter for our purposes.
static const char* kAtan2Fn = R"(
float atan2(float2 v) {
float bias = 0;
if (abs(v.y) > abs(v.x)) {
v = float2(v.y, -v.x);
bias = kPI/2;
bias = PI/2;
}
return atan(v.y, v.x) + bias;
})";
@ -322,7 +322,8 @@ float miter_extent(float cosTheta, float miterLimitInvPow2) {
SkString GrStrokeTessellateShader::getTessControlShaderGLSL(
const GrGLSLPrimitiveProcessor* glslPrimProc, const char* versionAndExtensionDecls,
const GrGLSLUniformHandler& uniformHandler, const GrShaderCaps& shaderCaps) const {
auto impl = static_cast<const GrStrokeTessellateShader::Impl*>(glslPrimProc);
SkASSERT(fMode == Mode::kTessellation);
auto impl = static_cast<const GrStrokeTessellateShader::TessellationImpl*>(glslPrimProc);
SkString code(versionAndExtensionDecls);
// Run 4 invocations: 1 for the previous join plus 1 for each section that the vertex shader
@ -335,9 +336,8 @@ SkString GrStrokeTessellateShader::getTessControlShaderGLSL(
code.appendf("#define float4 vec4\n");
code.appendf("#define float2x2 mat2\n");
code.appendf("#define float4x2 mat4x2\n");
code.appendf("const float kPI = 3.141592653589793238;\n");
code.appendf("const float kMaxTessellationSegments = %i;\n",
code.appendf("#define PI 3.141592653589793238\n");
code.appendf("#define MAX_TESSELLATION_SEGMENTS %i\n",
shaderCaps.maxTessellationSegments());
const char* tessArgs1Name = impl->getTessArgs1UniformName(uniformHandler);
@ -500,7 +500,7 @@ SkString GrStrokeTessellateShader::getTessControlShaderGLSL(
++numTotalCombinedSegments;
}
numTotalCombinedSegments = min(numTotalCombinedSegments, kMaxTessellationSegments);
numTotalCombinedSegments = min(numTotalCombinedSegments, MAX_TESSELLATION_SEGMENTS);
gl_TessLevelInner[0] = numTotalCombinedSegments;
gl_TessLevelInner[1] = 2.0;
gl_TessLevelOuter[0] = 2.0;
@ -559,7 +559,7 @@ void eval_stroke_edge(in float4x2 P, in float numParametricSegments, in float co
float2 tan0norm = normalize(tan0);
float negAbsRadsPerSegment = -abs(radsPerSegment);
float maxRotation0 = (1 + combinedEdgeID) * abs(radsPerSegment);
for (int exp = MAX_TESSELLATION_SEGMENTS_LOG2 - 1; exp >= 0; --exp) {
for (int exp = MAX_PARAMETRIC_SEGMENTS_LOG2 - 1; exp >= 0; --exp) {
// Test the parametric edge at lastParametricEdgeID + 2^exp.
float testParametricID = lastParametricEdgeID + (1 << exp);
if (testParametricID <= maxParametricEdgeID) {
@ -567,7 +567,7 @@ void eval_stroke_edge(in float4x2 P, in float numParametricSegments, in float co
testTan = fma(float2(testParametricID), testTan, C_);
float cosRotation = dot(normalize(testTan), tan0norm);
float maxRotation = fma(testParametricID, negAbsRadsPerSegment, maxRotation0);
maxRotation = min(maxRotation, kPI);
maxRotation = min(maxRotation, PI);
// Is rotation <= maxRotation? (i.e., is the number of complete radial segments
// behind testT, + testParametricID <= combinedEdgeID?)
if (cosRotation >= cos(maxRotation)) {
@ -643,7 +643,8 @@ void eval_stroke_edge(in float4x2 P, in float numParametricSegments, in float co
SkString GrStrokeTessellateShader::getTessEvaluationShaderGLSL(
const GrGLSLPrimitiveProcessor* glslPrimProc, const char* versionAndExtensionDecls,
const GrGLSLUniformHandler& uniformHandler, const GrShaderCaps& shaderCaps) const {
auto impl = static_cast<const GrStrokeTessellateShader::Impl*>(glslPrimProc);
SkASSERT(fMode == Mode::kTessellation);
auto impl = static_cast<const GrStrokeTessellateShader::TessellationImpl*>(glslPrimProc);
SkString code(versionAndExtensionDecls);
code.append("layout(quads, equal_spacing, ccw) in;\n");
@ -657,10 +658,9 @@ SkString GrStrokeTessellateShader::getTessEvaluationShaderGLSL(
code.appendf("#define float4x2 mat4x2\n");
// Use a #define to make extra sure we don't prevent the loop from unrolling.
code.appendf("#define MAX_TESSELLATION_SEGMENTS_LOG2 %i\n",
code.appendf("#define MAX_PARAMETRIC_SEGMENTS_LOG2 %i\n",
SkNextLog2(shaderCaps.maxTessellationSegments()));
code.appendf("const float kPI = 3.141592653589793238;\n");
code.appendf("#define PI 3.141592653589793238\n");
const char* tessArgs2Name = impl->getTessArgs2UniformName(uniformHandler);
code.appendf("uniform vec2 %s;\n", tessArgs2Name);
@ -773,7 +773,249 @@ SkString GrStrokeTessellateShader::getTessEvaluationShaderGLSL(
return code;
}
class GrStrokeTessellateShader::IndirectImpl : public GrGLSLGeometryProcessor {
void onEmitCode(EmitArgs& args, GrGPArgs* gpArgs) override {
const auto& shader = args.fGP.cast<GrStrokeTessellateShader>();
SkPaint::Join joinType = shader.fStroke.getJoin();
args.fVaryingHandler->emitAttributes(shader);
// Constants.
args.fVertBuilder->defineConstant("MAX_PARAMETRIC_SEGMENTS_LOG2",
GrTessellationPathRenderer::kMaxResolveLevel);
args.fVertBuilder->defineConstant("float", "PI", "3.141592653589793238");
// Helper functions.
args.fVertBuilder->insertFunction(kAtan2Fn);
args.fVertBuilder->insertFunction(kWangsFormulaCubicFn);
args.fVertBuilder->insertFunction(kMiterExtentFn);
args.fVertBuilder->insertFunction(kUncheckedMixFn);
args.fVertBuilder->insertFunction(kEvalStrokeEdgeFn);
args.fVertBuilder->insertFunction(R"(
float cosine_between_vectors(float2 a, float2 b) {
float ab_cosTheta = dot(a,b);
float ab_pow2 = dot(a,a) * dot(b,b);
return (ab_pow2 == 0) ? 1 : clamp(ab_cosTheta * inversesqrt(ab_pow2), -1, 1);
})");
// Tessellation control uniforms.
const char* tessArgsName;
fTessControlArgsUniform = args.fUniformHandler->addUniform(
nullptr, kVertex_GrShaderFlag, kFloat4_GrSLType, "tessControlArgs", &tessArgsName);
args.fVertBuilder->codeAppendf("float uWangsTermPow2 = %s.x;\n", tessArgsName);
args.fVertBuilder->codeAppendf("float uNumRadialSegmentsPerRadian = %s.y;\n", tessArgsName);
args.fVertBuilder->codeAppendf("float uMiterLimitInvPow2 = %s.z;\n", tessArgsName);
args.fVertBuilder->codeAppendf("float uStrokeRadius = %s.w;\n", tessArgsName);
// Tessellation code.
args.fVertBuilder->codeAppend(R"(
float4x2 P = float4x2(pts01, pts23);
float2 lastControlPoint = args.xy;
float numTotalEdges = abs(args.z);
// Find how many parametric segments this stroke requires.
float numParametricSegments = min(wangs_formula_cubic(P, uWangsTermPow2),
1 << MAX_PARAMETRIC_SEGMENTS_LOG2);
if (P[0] == P[1] && P[2] == P[3]) {
// This is how we describe lines, but Wang's formula does not return 1 in this case.
numParametricSegments = 1;
}
// Find the starting and ending tangents.
float2 tan0 = ((P[0] == P[1]) ? (P[1] == P[2]) ? P[3] : P[2] : P[1]) - P[0];
float2 tan1 = P[3] - ((P[3] == P[2]) ? (P[2] == P[1]) ? P[0] : P[1] : P[2]);
if (tan0 == float2(0)) {
// The stroke is a point. This special case tells us to draw a stroke-width circle as a
// 180 degree point stroke instead.
tan0 = float2(1,0);
tan1 = float2(-1,0);
})");
if (shader.fStroke.getJoin() == SkPaint::kRound_Join) {
args.fVertBuilder->codeAppend(R"(
// Determine how many edges to give to the round join. We emit the first and final edges
// of the join twice: once full width and once restricted to half width. This guarantees
// perfect seaming by matching the vertices from the join as well as from the strokes on
// either side.
float joinRads = acos(cosine_between_vectors(P[0] - lastControlPoint, tan0));
float numRadialSegmentsInJoin = max(ceil(joinRads * uNumRadialSegmentsPerRadian), 1);
// +2 because we emit the beginning and ending edges twice (see above comment).
float numEdgesInJoin = numRadialSegmentsInJoin + 2;
// The stroke section needs at least two edges. Don't assign more to the join than
// "numTotalEdges - 2".
numEdgesInJoin = min(numEdgesInJoin, numTotalEdges - 2);
// Lines give all their extra edges to the join.
if (numParametricSegments == 1) {
numEdgesInJoin = numTotalEdges - 2;
}
// Negative args.z means the join is a chop, and chop joins get exactly one segment.
if (args.z < 0) {
// +2 because we emit the beginning and ending edges twice (see above comment).
numEdgesInJoin = 1 + 2;
})");
} else {
args.fVertBuilder->codeAppendf(R"(
float numEdgesInJoin = %i;")", IndirectInstance::NumExtraEdgesInJoin(joinType));
}
args.fVertBuilder->codeAppend(R"(
// Find which direction the curve turns.
// NOTE: Since the curve is not allowed to inflect, we can just check F'(.5) x F''(.5).
// NOTE: F'(.5) x F''(.5) has the same sign as (P2 - P0) x (P3 - P1)
float turn = cross(P[2] - P[0], P[3] - P[1]);
float numCombinedSegments;
float outset = ((sk_VertexID & 1) == 0) ? +1 : -1;
float combinedEdgeID = float(sk_VertexID >> 1) - numEdgesInJoin;
if (combinedEdgeID < 0) {
// We belong to the preceding join. The first and final edges get duplicated, so we only
// have "numEdgesInJoin - 2" segments.
numCombinedSegments = numEdgesInJoin - 2;
numParametricSegments = 1; // Joins don't have parametric segments.
P = float4x2(P[0], P[0], P[0], P[0]); // Colocate all points on the junction point.
tan1 = tan0;
// Don't let tan0 become zero. The code as-is isn't built to handle that case. tan0=0
// means the join is disabled, and to disable it with the existing code we can leave
// tan0 equal to tan1.
if (lastControlPoint != P[0]) {
tan0 = P[0] - lastControlPoint;
}
turn = cross(tan0, tan1);
// Shift combinedEdgeID to the range [-1, numCombinedSegments]. This duplicates the
// first edge and lands one edge at the very end of the join. (The duplicated final edge
// will actually come from the section of our strip that belongs to the stroke.)
combinedEdgeID += numCombinedSegments + 1;
// We normally restrict the join on one side of the junction, but if the tangents are
// nearly equivalent this could theoretically result in bad seaming and/or cracks on the
// side we don't put it on. If the tangents are nearly equivalent then we leave the join
// double-sided.
float sinEpsilon = 1e-2; // ~= sin(180deg / 3000)
bool tangentsNearlyParallel =
(abs(turn) * inversesqrt(dot(tan0, tan0) * dot(tan1, tan1))) < sinEpsilon;
if (!tangentsNearlyParallel || dot(tan0, tan1) < 0) {
// There are two edges colocated at the beginning. Leave the first one double sided
// for seaming with the previous stroke. (The double sided edge at the end will
// actually come from the section of our strip that belongs to the stroke.)
if (combinedEdgeID >= 0) {
outset = clamp(outset, (turn < 0) ? -1 : 0, (turn >= 0) ? 1 : 0);
}
}
combinedEdgeID = max(combinedEdgeID, 0);
} else {
// We belong to the stroke.
numCombinedSegments = numTotalEdges - numEdgesInJoin - 1;
}
// Don't take more parametric segments than there are total segments.
numParametricSegments = min(numParametricSegments, numCombinedSegments);
// Any leftover edges go to radial segments.
float numRadialSegments = numCombinedSegments + 1 - numParametricSegments;
// Calculate the curve's starting angle and rotation.
float angle0 = atan2(tan0);
float cosTheta = cosine_between_vectors(tan0, tan1);
float rotation = acos(cosTheta);
if (turn < 0) {
// Adjust sign of rotation to match the direction the curve turns.
rotation = -rotation;
}
float radsPerSegment = rotation / numRadialSegments;)");
if (joinType == SkPaint::kMiter_Join) {
args.fVertBuilder->codeAppend(R"(
// Vertices #4 and #5 belong to the edge of the join that extends to the miter point.
if ((sk_VertexID | 1) == (4 | 5)) {
outset *= miter_extent(cosTheta, uMiterLimitInvPow2);
})");
}
args.fVertBuilder->codeAppend(R"(
float2 tangent, position;
eval_stroke_edge(P, numParametricSegments, combinedEdgeID, tan0, radsPerSegment, angle0,
tangent, position);
if (combinedEdgeID == 0) {
// Edges at the beginning of their section use P[0] and tan0. This ensures crack-free
// seaming between instances.
position = P[0];
tangent = tan0;
}
if (combinedEdgeID == numCombinedSegments) {
// Edges at the end of their section use P[1] and tan1. This ensures crack-free seaming
// between instances.
position = P[3];
tangent = tan1;
}
float2 ortho = normalize(float2(tangent.y, -tangent.x));
position += ortho * (uStrokeRadius * outset);)");
// Do the transform after tessellation. Stroke widths and normals are defined in
// (pre-transform) local path space.
if (!shader.viewMatrix().isIdentity()) {
const char* translateName, *affineMatrixName;
fTranslateUniform = args.fUniformHandler->addUniform(
nullptr, kVertex_GrShaderFlag, kFloat2_GrSLType, "translate", &translateName);
fAffineMatrixUniform = args.fUniformHandler->addUniform(
nullptr, kVertex_GrShaderFlag, kFloat4_GrSLType, "affineMatrix",
&affineMatrixName);
args.fVertBuilder->codeAppendf("position = float2x2(%s) * position + %s;",
affineMatrixName, translateName);
}
gpArgs->fPositionVar.set(kFloat2_GrSLType, "position");
gpArgs->fLocalCoordVar.set(kFloat2_GrSLType, "position");
// The fragment shader just outputs a uniform color.
const char* colorUniformName;
fColorUniform = args.fUniformHandler->addUniform(
nullptr, kFragment_GrShaderFlag, kHalf4_GrSLType, "color", &colorUniformName);
args.fFragBuilder->codeAppendf("%s = %s;", args.fOutputColor, colorUniformName);
args.fFragBuilder->codeAppendf("%s = half4(1);", args.fOutputCoverage);
}
void setData(const GrGLSLProgramDataManager& pdman,
const GrPrimitiveProcessor& primProc) override {
const auto& shader = primProc.cast<GrStrokeTessellateShader>();
// Set up the tessellation control uniforms.
float miterLimit = shader.fStroke.getMiter();
pdman.set4f(fTessControlArgsUniform,
GrWangsFormula::length_term_pow2<3>(shader.fParametricIntolerance), // uWangsTermPow2
shader.fNumRadialSegmentsPerRadian, // uNumRadialSegmentsPerRadian
1 / (miterLimit * miterLimit), // uMiterLimitInvPow2.
shader.fStroke.getWidth() * .5); // uStrokeRadius.
// Set up the view matrix, if any.
const SkMatrix& m = shader.viewMatrix();
if (!m.isIdentity()) {
pdman.set2f(fTranslateUniform, m.getTranslateX(), m.getTranslateY());
pdman.set4f(fAffineMatrixUniform, m.getScaleX(), m.getSkewY(), m.getSkewX(),
m.getScaleY());
}
pdman.set4fv(fColorUniform, 1, shader.fColor.vec());
}
GrGLSLUniformHandler::UniformHandle fTessControlArgsUniform;
GrGLSLUniformHandler::UniformHandle fTranslateUniform;
GrGLSLUniformHandler::UniformHandle fAffineMatrixUniform;
GrGLSLUniformHandler::UniformHandle fColorUniform;
};
void GrStrokeTessellateShader::getGLSLProcessorKey(const GrShaderCaps&,
GrProcessorKeyBuilder* b) const {
uint32_t key = this->viewMatrix().isIdentity();
if (fMode == Mode::kIndirect) {
SkASSERT(fStroke.getJoin() >> 2 == 0);
key = (key << 2) | fStroke.getJoin();
}
key = (key << 1) | (uint32_t)fMode; // Must be last.
b->add32(key);
}
GrGLSLPrimitiveProcessor* GrStrokeTessellateShader::createGLSLInstance(
const GrShaderCaps&) const {
return new Impl;
return (fMode == Mode::kTessellation) ?
(GrGLSLPrimitiveProcessor*)new TessellationImpl : new IndirectImpl;
}

View File

@ -25,8 +25,13 @@ class GrGLSLUniformHandler;
// curve, regardless of curvature.
class GrStrokeTessellateShader : public GrPathShader {
public:
// The vertex array bound for this shader should contain a vector of Patch structs. A Patch is
// a join followed by a cubic stroke.
// Are we using hardware tessellation or indirect draws?
enum class Mode : bool {
kTessellation,
kIndirect
};
// When using Mode::kTessellation, this is how patches sent to the GPU are structured.
struct Patch {
// A join calculates its starting angle using fPrevControlPoint.
SkPoint fPrevControlPoint;
@ -42,6 +47,57 @@ public:
std::array<SkPoint, 4> fPts;
};
// When using Mode::kIndirect, these are the instances that get sent to the GPU.
struct IndirectInstance {
constexpr static int NumExtraEdgesInJoin(SkPaint::Join joinType) {
// We expect a fixed number of additional edges to be appended onto each instance in
// order to implement its preceding join. Specifically, each join emits:
//
// * Two colocated edges at the beginning (a double-sided edge to seam with the
// preceding stroke and a single-sided edge to seam with the join).
//
// * An extra edge in the middle for miter joins, or else a variable number for round
// joins (counted in the resolveLevel).
//
// * A single sided edge at the end of the join that is colocated with the first
// (double sided) edge of the stroke
//
switch (joinType) {
case SkPaint::kMiter_Join:
return 4;
case SkPaint::kRound_Join:
// The inner edges for round joins are counted in the stroke's resolveLevel.
[[fallthrough]];
case SkPaint::kBevel_Join:
return 3;
}
SkUNREACHABLE;
}
// Writes the given stroke into this instance. "abs(numTotalEdges)" tells the shader the
// literal number of edges in the triangle strip being rendered (i.e., it should be
// vertexCount/2). If numTotalEdges is negative and the join type is "kRound", it also
// instructs the shader to only allocate one segment the preceding round join.
void set(const SkPoint pts[4], SkPoint lastControlPoint, int numTotalEdges) {
memcpy(fPts.data(), pts, sizeof(fPts));
fLastControlPoint = lastControlPoint;
fNumTotalEdges = numTotalEdges;
}
// A "circle" is a stroke-width circle drawn as a 180-degree point stroke. They should be
// drawn at cusp points on the curve and for round caps.
void setCircle(SkPoint pt, int numEdgesForCircles) {
SkASSERT(numEdgesForCircles >= 0);
// An empty stroke is a special case that denotes a circle, or 180-degree point stroke.
fPts.fill(pt);
fLastControlPoint = pt;
// Mark fNumTotalEdges negative so the shader assigns the least possible number of edges
// to its (empty) preceding join.
fNumTotalEdges = -numEdgesForCircles;
}
std::array<SkPoint, 4> fPts;
SkPoint fLastControlPoint;
float fNumTotalEdges;
};
// 'parametricIntolerance' controls the number of parametric segments we add for each curve.
// We add enough parametric segments so that the center of each one falls within
// 1/parametricIntolerance local path units from the true curve.
@ -51,29 +107,41 @@ public:
// smoothness.
//
// 'viewMatrix' is applied to the geometry post tessellation. It cannot have perspective.
GrStrokeTessellateShader(const SkStrokeRec& stroke, float parametricIntolerance,
GrStrokeTessellateShader(Mode mode, const SkStrokeRec& stroke, float parametricIntolerance,
float numRadialSegmentsPerRadian, const SkMatrix& viewMatrix,
SkPMColor4f color)
: GrPathShader(kTessellate_GrStrokeTessellateShader_ClassID, viewMatrix,
GrPrimitiveType::kPatches, 1)
(mode == Mode::kTessellation) ?
GrPrimitiveType::kPatches : GrPrimitiveType::kTriangleStrip,
(mode == Mode::kTessellation) ? 1 : 0)
, fMode(mode)
, fStroke(stroke)
, fParametricIntolerance(parametricIntolerance)
, fNumRadialSegmentsPerRadian(numRadialSegmentsPerRadian)
, fColor(color) {
SkASSERT(!fStroke.isHairlineStyle()); // No hairline support yet.
constexpr static Attribute kInputPointAttribs[] = {
{"inputPrevCtrlPt", kFloat2_GrVertexAttribType, kFloat2_GrSLType},
{"inputPts01", kFloat4_GrVertexAttribType, kFloat4_GrSLType},
{"inputPts23", kFloat4_GrVertexAttribType, kFloat4_GrSLType}};
this->setVertexAttributes(kInputPointAttribs, SK_ARRAY_COUNT(kInputPointAttribs));
SkASSERT(this->vertexStride() == sizeof(Patch));
if (fMode == Mode::kTessellation) {
constexpr static Attribute kTessellationAttribs[] = {
{"inputPrevCtrlPt", kFloat2_GrVertexAttribType, kFloat2_GrSLType},
{"inputPts01", kFloat4_GrVertexAttribType, kFloat4_GrSLType},
{"inputPts23", kFloat4_GrVertexAttribType, kFloat4_GrSLType}};
this->setVertexAttributes(kTessellationAttribs, SK_ARRAY_COUNT(kTessellationAttribs));
SkASSERT(this->vertexStride() == sizeof(Patch));
} else {
constexpr static Attribute kIndirectAttribs[] = {
{"pts01", kFloat4_GrVertexAttribType, kFloat4_GrSLType},
{"pts23", kFloat4_GrVertexAttribType, kFloat4_GrSLType},
// "fLastControlPoint" and "fNumTotalEdges" are both packed into these args.
// See IndirectInstance.
{"args", kFloat3_GrVertexAttribType, kFloat3_GrSLType}};
this->setInstanceAttributes(kIndirectAttribs, SK_ARRAY_COUNT(kIndirectAttribs));
SkASSERT(this->instanceStride() == sizeof(IndirectInstance));
}
}
private:
const char* name() const override { return "GrStrokeTessellateShader"; }
void getGLSLProcessorKey(const GrShaderCaps&, GrProcessorKeyBuilder* b) const override {
b->add32(this->viewMatrix().isIdentity());
}
void getGLSLProcessorKey(const GrShaderCaps&, GrProcessorKeyBuilder* b) const override;
GrGLSLPrimitiveProcessor* createGLSLInstance(const GrShaderCaps&) const final;
SkString getTessControlShaderGLSL(const GrGLSLPrimitiveProcessor*,
@ -85,12 +153,14 @@ private:
const GrGLSLUniformHandler&,
const GrShaderCaps&) const override;
const Mode fMode;
const SkStrokeRec fStroke;
const float fParametricIntolerance;
const float fNumRadialSegmentsPerRadian;
const SkPMColor4f fColor;
class Impl;
class TessellationImpl;
class IndirectImpl;
};
#endif

View File

@ -18,6 +18,7 @@
#include "src/gpu/ops/GrFillRectOp.h"
#include "src/gpu/tessellate/GrDrawAtlasPathOp.h"
#include "src/gpu/tessellate/GrPathTessellateOp.h"
#include "src/gpu/tessellate/GrStrokeIndirectOp.h"
#include "src/gpu/tessellate/GrStrokeTessellateOp.h"
#include "src/gpu/tessellate/GrWangsFormula.h"
@ -147,7 +148,6 @@ GrPathRenderer::CanDrawPath GrTessellationPathRenderer::onCanDrawPath(
// These are only temporary restrictions while we bootstrap tessellated stroking. Every one
// of them will eventually go away.
if (shape.style().strokeRec().getStyle() == SkStrokeRec::kStrokeAndFill_Style ||
!args.fCaps->shaderCaps()->tessellationSupport() ||
GrAAType::kCoverage == args.fAAType ||
!args.fPaint->isConstantBlendedColor(&constantColor) ||
args.fPaint->hasCoverageFragmentProcessor()) {
@ -158,6 +158,21 @@ GrPathRenderer::CanDrawPath GrTessellationPathRenderer::onCanDrawPath(
return CanDrawPath::kYes;
}
static GrOp::Owner make_stroke_op(GrRecordingContext* context, GrAAType aaType,
const SkMatrix& viewMatrix, const SkStrokeRec& stroke,
const SkPath& path, GrPaint&& paint,
const GrShaderCaps& shaderCaps) {
// Only use hardware tessellation if the path has a somewhat large number of verbs. Otherwise we
// seem to be better off using indirect draws.
if (shaderCaps.tessellationSupport() && path.countVerbs() > 50) {
return GrOp::Make<GrStrokeTessellateOp>(context, aaType, viewMatrix, stroke, path,
std::move(paint));
} else {
return GrOp::Make<GrStrokeIndirectOp>(context, aaType, viewMatrix, path, stroke,
std::move(paint));
}
}
bool GrTessellationPathRenderer::onDrawPath(const DrawPathArgs& args) {
GrRenderTargetContext* renderTargetContext = args.fRenderTargetContext;
const GrShaderCaps& shaderCaps = *args.fContext->priv().caps()->shaderCaps();
@ -257,9 +272,8 @@ bool GrTessellationPathRenderer::onDrawPath(const DrawPathArgs& args) {
path.transform(*args.fViewMatrix, &devPath);
SkStrokeRec devStroke = args.fShape->style().strokeRec();
devStroke.setStrokeStyle(1);
auto op = GrOp::Make<GrStrokeTessellateOp>(
args.fContext, args.fAAType, SkMatrix::I(), devStroke,
devPath, std::move(args.fPaint));
auto op = make_stroke_op(args.fContext, args.fAAType, SkMatrix::I(), devStroke, devPath,
std::move(args.fPaint), shaderCaps);
renderTargetContext->addDrawOp(args.fClip, std::move(op));
return true;
}
@ -267,9 +281,8 @@ bool GrTessellationPathRenderer::onDrawPath(const DrawPathArgs& args) {
if (!args.fShape->style().isSimpleFill()) {
const SkStrokeRec& stroke = args.fShape->style().strokeRec();
SkASSERT(stroke.getStyle() == SkStrokeRec::kStroke_Style);
auto op = GrOp::Make<GrStrokeTessellateOp>(
args.fContext, args.fAAType, *args.fViewMatrix, stroke,
path, std::move(args.fPaint));
auto op = make_stroke_op(args.fContext, args.fAAType, *args.fViewMatrix, stroke, path,
std::move(args.fPaint), shaderCaps);
renderTargetContext->addDrawOp(args.fClip, std::move(op));
return true;
}

View File

@ -0,0 +1,477 @@
/*
* 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) {
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 * 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(
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 * 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(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 * 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 * 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;
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(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 * 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;
}
}