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:
parent
41a98e0a50
commit
c2a1746b42
@ -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}}); )
|
||||
|
@ -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",
|
||||
|
@ -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",
|
||||
|
@ -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;
|
||||
|
819
src/gpu/tessellate/GrStrokeIndirectOp.cpp
Normal file
819
src/gpu/tessellate/GrStrokeIndirectOp.cpp
Normal 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);
|
||||
}
|
78
src/gpu/tessellate/GrStrokeIndirectOp.h
Normal file
78
src/gpu/tessellate/GrStrokeIndirectOp.h
Normal 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
|
308
src/gpu/tessellate/GrStrokeIterator.h
Normal file
308
src/gpu/tessellate/GrStrokeIterator.h
Normal 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
|
@ -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(),
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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;
|
||||
}
|
||||
|
477
tests/StrokeIndirectTest.cpp
Normal file
477
tests/StrokeIndirectTest.cpp
Normal 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;
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user