blob: ec5e60a18f44710a5e4d2433d67b4ea9bc379d49 [file] [log] [blame]
/*
* 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/GrStrokeIndirectTessellator.h"
#include "src/core/SkGeometry.h"
#include "src/core/SkPathPriv.h"
#include "src/gpu/GrRecordingContextPriv.h"
#include "src/gpu/GrVertexWriter.h"
#include "src/gpu/GrVx.h"
#include "src/gpu/geometry/GrPathUtils.h"
#include "src/gpu/geometry/GrWangsFormula.h"
#include "src/gpu/tessellate/GrStrokeIterator.h"
#include "src/gpu/tessellate/GrStrokeShader.h"
namespace {
// Only use SIMD if SkVx will use a built-in compiler extensions for vectors.
#if !defined(SKNX_NO_SIMD) && (defined(__clang__) || defined(__GNUC__))
#define USE_SIMD 1
#else
#define USE_SIMD 0
#endif
#if USE_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:
constexpr static int8_t kMaxResolveLevel = GrStrokeIndirectTessellator::kMaxResolveLevel;
ResolveLevelCounter(int* resolveLevelCounts, std::array<float, 2> matrixMinMaxScales)
: fResolveLevelCounts(resolveLevelCounts), fMatrixMinMaxScales(matrixMinMaxScales) {
}
void updateTolerances(float strokeWidth, bool isRoundJoin) {
this->flush();
fTolerances = GrStrokeTolerances::Make(fMatrixMinMaxScales.data(), strokeWidth);
fResolveLevelForCircles = SkTPin<float>(
sk_float_nextlog2(fTolerances.fNumRadialSegmentsPerRadian * SK_ScalarPI),
1, kMaxResolveLevel);
fIsRoundJoin = isRoundJoin;
#if USE_SIMD
fWangsTermQuadratic = GrWangsFormula::length_term<2>(fTolerances.fParametricPrecision);
fWangsTermCubic = GrWangsFormula::length_term<3>(fTolerances.fParametricPrecision);
#endif
}
bool isRoundJoin() const { return fIsRoundJoin; }
// Accounts for 180-degree point strokes, which render as circles with diameters equal to the
// stroke width. We draw circles at cusp points on curves and for round caps.
//
// Returns the resolveLevel to use when drawing these circles.
int8_t countCircles(int numCircles) {
fResolveLevelCounts[fResolveLevelForCircles] += numCircles;
return fResolveLevelForCircles;
}
#if !USE_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(fTolerances.fParametricPrecision, 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(fTolerances.fParametricPrecision, 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 =
fTolerances.fNumRadialSegmentsPerRadian * rotation + numParametricSegments;
int8_t resolveLevel = sk_float_nextlog2(numCombinedSegments);
resolveLevel = std::min(resolveLevel, kMaxResolveLevel);
++fResolveLevelCounts[(*resolveLevelPtr = resolveLevel)];
}
#else // USE_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] = fCubicChopTs[4 + 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);
for (int i = 0; i < NumPts; ++i) {
fPts[i][fCount] = pts[i].fX;
fPts[i][4 + fCount] = pts[i].fY;
}
if (pushRoundJoin) {
fLastControlPoints[fCount] = lastControlPoint.fX;
fLastControlPoints[4 + fCount] = lastControlPoint.fY;
}
fResolveLevelPtrs[fCount] = resolveLevelPtr;
return fCount++;
}
// Loads pts[idx] in SIMD for fCount strokes, with the x values in the "vec.lo" and the y
// values in "vec.hi".
template<int N> vec<N*2> loadPoint(int idx) const {
SkASSERT(0 <= idx && idx < NumPts);
return this->loadPointFromArray<N>(fPts[idx]);
}
// Loads fCount lastControlPoints in SIMD, with the x values in "vec.lo" and the y values in
// "vec.hi".
template<int N> vec<N*2> loadLastControlPoint() const {
return this->loadPointFromArray<N>(fLastControlPoints);
}
// Loads fCount points from the given array in SIMD, with the x values in "vec.lo" and the y
// values in "vec.hi". The array must be ordered as: [x0,x1,x2,x3,y0,y1,y2,y3].
template<int N> vec<N*2> loadPointFromArray(const float array[8]) const {
if constexpr (N == 4) {
if (fCount == 4) {
return vec<8>::Load(array);
} else {
SkASSERT(fCount == 3);
return {array[0], array[1], array[2], 0, array[4], array[5], array[6], 0};
}
} else {
if (fCount == 2) {
return {array[0], array[1], array[4], array[5]};
} else {
SkASSERT(fCount == 1);
return {array[0], 0, array[4], 0};
}
}
}
struct alignas(sizeof(float) * 8) {
float fPts[NumPts][8];
float fLastControlPoints[8];
};
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);
auto p0 = fCubicQueue.loadPoint<N>(0);
auto p1 = fCubicQueue.loadPoint<N>(1);
auto p2 = fCubicQueue.loadPoint<N>(2);
auto p3 = fCubicQueue.loadPoint<N>(3);
this->flushCubics<N>(fCubicQueue, p0, p1, p2, p3, fIsRoundJoin, 0);
fCubicQueue.fCount = 0;
}
template<int N> void flushChoppedCubics() {
SkASSERT(fChoppedCubicQueue.fCount > 0);
auto p0 = fChoppedCubicQueue.loadPoint<N>(0);
auto p1 = fChoppedCubicQueue.loadPoint<N>(1);
auto p2 = fChoppedCubicQueue.loadPoint<N>(2);
auto p3 = fChoppedCubicQueue.loadPoint<N>(3);
auto T = fChoppedCubicQueue.loadPointFromArray<N>(fCubicChopTs);
// Chop the cubic at its chopT and find the resolve level for each half.
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>(
fTolerances.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, 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;
struct alignas(sizeof(float) * 8) {
float fCubicChopTs[8];
};
float fWangsTermQuadratic;
float fWangsTermCubic;
#endif
int* const fResolveLevelCounts;
std::array<float, 2> fMatrixMinMaxScales;
GrStrokeTolerances fTolerances;
int fResolveLevelForCircles;
bool fIsRoundJoin;
};
} // namespace
GrStrokeIndirectTessellator::GrStrokeIndirectTessellator(ShaderFlags shaderFlags,
const SkMatrix& viewMatrix,
PathStrokeList* pathStrokeList,
std::array<float, 2> matrixMinMaxScales,
const SkRect& strokeCullBounds,
int totalCombinedVerbCnt,
SkArenaAlloc* alloc)
: GrStrokeTessellator(GrStrokeShader::Mode::kLog2Indirect, shaderFlags, viewMatrix,
pathStrokeList, matrixMinMaxScales, strokeCullBounds) {
// 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 or round caps at the very end not initiated by a
// "kMoveTo".
int resolveLevelAllocCount = totalCombinedVerbCnt * (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 = totalCombinedVerbCnt * 2;
fChopTs = alloc->makeArrayDefault<float>(chopTAllocCount);
float* nextChopTs = fChopTs;
ResolveLevelCounter counter(fResolveLevelCounts, fMatrixMinMaxScales);
float lastStrokeWidth = -1;
SkPoint lastControlPoint = {0,0};
for (PathStrokeList* pathStroke = fPathStrokeList; pathStroke; pathStroke = pathStroke->fNext) {
const SkStrokeRec& stroke = pathStroke->fStroke;
SkASSERT(stroke.getWidth() >= 0); // Otherwise we can't initialize lastStrokeWidth=-1.
if (stroke.getWidth() != lastStrokeWidth ||
(stroke.getJoin() == SkPaint::kRound_Join) != counter.isRoundJoin()) {
counter.updateTolerances(stroke.getWidth(), (stroke.getJoin() == SkPaint::kRound_Join));
lastStrokeWidth = stroke.getWidth();
}
fMaxNumExtraEdgesInJoin = std::max(fMaxNumExtraEdgesInJoin,
GrStrokeShader::NumFixedEdgesInJoin(stroke.getJoin()));
// Iterate through each verb in the stroke, counting its resolveLevel(s).
GrStrokeIterator iter(pathStroke->fPath, &stroke, &viewMatrix);
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 (counter.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:
case Verb::kConic:
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;
}
break;
case Verb::kConic:
// We use the same quadratic formula for conics, ignoring w. This is pretty
// close to what the actual number of subdivisions would have been.
[[fallthrough]];
case Verb::kQuad: {
if (GrPathUtils::conicHasCusp(pts)) {
// The curve has a cusp. Draw two lines and a circle instead of a quad.
int8_t cuspResolveLevel = counter.countCircles(1);
*nextResolveLevel++ = -cuspResolveLevel; // Negative signals a cusp.
if (counter.countLine(pts, lastControlPoint, nextResolveLevel)) {
++nextResolveLevel;
}
++fResolveLevelCounts[0]; // Second line instance.
} else {
counter.countQuad(pts, lastControlPoint, nextResolveLevel++);
}
break;
}
case Verb::kCubic: {
int8_t cuspResolveLevel = 0;
bool areCusps;
int numChops = GrPathUtils::findCubicConvex180Chops(pts, nextChopTs, &areCusps);
if (areCusps && numChops > 0) {
cuspResolveLevel = counter.countCircles(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.
static_assert(kMaxResolveLevel <= 0xf);
SkASSERT(cuspResolveLevel <= 0xf);
*nextResolveLevel++ = -((1 << 4) | cuspResolveLevel);
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.
static_assert(kMaxResolveLevel <= 0xf);
SkASSERT(cuspResolveLevel <= 0xf);
*nextResolveLevel++ = -((2 << 4) | cuspResolveLevel);
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;
break;
}
case Verb::kCircle:
// The iterator implements round caps as circles.
*nextResolveLevel++ = counter.countCircles(1);
break;
case Verb::kMoveWithinContour:
case Verb::kContourFinished:
// We should have continued early for non-geometric verbs.
SkUNREACHABLE;
break;
}
}
}
counter.flush();
for (int resolveLevelInstanceCount : fResolveLevelCounts) {
fTotalInstanceCount += resolveLevelInstanceCount;
if (resolveLevelInstanceCount) {
++fChainedDrawIndirectCount;
}
}
fChainedInstanceCount = fTotalInstanceCount;
#ifdef SK_DEBUG
SkASSERT(nextResolveLevel <= fResolveLevels + resolveLevelAllocCount);
fResolveLevelArrayCount = nextResolveLevel - fResolveLevels;
SkASSERT(nextChopTs <= fChopTs + chopTAllocCount);
fChopTsArrayCount = nextChopTs - fChopTs;
fChopTsArrayCount = nextChopTs - fChopTs;
#endif
}
void GrStrokeIndirectTessellator::addToChain(GrStrokeIndirectTessellator* tessellator) {
SkASSERT(tessellator->fShader.flags() == fShader.flags());
fChainedInstanceCount += tessellator->fChainedInstanceCount;
tessellator->fChainedInstanceCount = 0;
fChainedDrawIndirectCount += tessellator->fChainedDrawIndirectCount;
tessellator->fChainedDrawIndirectCount = 0;
fMaxNumExtraEdgesInJoin = std::max(tessellator->fMaxNumExtraEdgesInJoin,
fMaxNumExtraEdgesInJoin);
tessellator->fMaxNumExtraEdgesInJoin = 0;
*fChainTail = tessellator;
fChainTail = tessellator->fChainTail;
tessellator->fChainTail = nullptr;
}
namespace {
constexpr static int num_edges_in_resolve_level(int resolveLevel) {
// A "resolveLevel" means the stroke is composed of 2^resolveLevel line segments.
int numSegments = 1 << resolveLevel;
// There are edges both at the beginning and end of a stroke, so there is always one more edge
// than there are segments.
int numStrokeEdges = numSegments + 1;
return numStrokeEdges;
}
// Partitions the instance buffer into bins for each resolveLevel. Writes out indirect draw commands
// per bin. Provides methods to write strokes to their respective bins.
class BinningInstanceWriter {
public:
using ShaderFlags = GrStrokeShader::ShaderFlags;
using DynamicStroke = GrStrokeShader::DynamicStroke;
constexpr static int kNumBins = GrStrokeIndirectTessellator::kMaxResolveLevel + 1;
BinningInstanceWriter(GrDrawIndirectWriter* indirectWriter, GrVertexWriter* instanceWriter,
ShaderFlags shaderFlags, size_t instanceStride, int baseInstance,
int numExtraEdgesInJoin, const int resolveLevelCounts[kNumBins])
: fShaderFlags(shaderFlags) {
SkASSERT(numExtraEdgesInJoin == 3 || numExtraEdgesInJoin == 4);
// Partition the instance buffer into bins and write out indirect draw commands per bin.
int runningInstanceCount = 0;
for (int i = 0; i < kNumBins; ++i) {
if (resolveLevelCounts[i]) {
int numEdges = numExtraEdgesInJoin + num_edges_in_resolve_level(i);
indirectWriter->write(resolveLevelCounts[i], baseInstance + runningInstanceCount,
numEdges * 2, 0);
fInstanceWriters[i] = instanceWriter->makeOffset(instanceStride *
runningInstanceCount);
fNumEdgesPerResolveLevel[i] = numEdges;
#ifdef SK_DEBUG
} else {
fInstanceWriters[i] = {nullptr};
}
if (i > 0) {
fEndWriters[i - 1] = instanceWriter->makeOffset(instanceStride *
runningInstanceCount);
#endif
}
runningInstanceCount += resolveLevelCounts[i];
}
SkDEBUGCODE(fEndWriters[kNumBins - 1] =
instanceWriter->makeOffset(instanceStride * runningInstanceCount));
*instanceWriter = instanceWriter->makeOffset(instanceStride * runningInstanceCount);
}
void updateDynamicStroke(const SkStrokeRec& stroke) {
SkASSERT(fShaderFlags & ShaderFlags::kDynamicStroke);
fDynamicStroke.set(stroke);
}
void updateDynamicColor(const SkPMColor4f& color) {
SkASSERT(fShaderFlags & ShaderFlags::kDynamicColor);
bool wideColor = fShaderFlags & ShaderFlags::kWideColor;
SkASSERT(wideColor || color.fitsInBytes());
fDynamicColor.set(color, wideColor);
}
void writeStroke(int8_t resolveLevel, const SkPoint pts[4], SkPoint prevControlPoint,
bool isInternalChop = false) {
SkASSERT(0 <= resolveLevel && resolveLevel < kNumBins);
float numEdges = fNumEdgesPerResolveLevel[resolveLevel];
fInstanceWriters[resolveLevel].writeArray(pts, 4);
fInstanceWriters[resolveLevel].write(prevControlPoint,
// Negative numEdges will tell the GPU that this stroke
// instance follows a chop, and round joins from
// chopping always get exactly one segment.
(isInternalChop) ? -numEdges : +numEdges);
this->writeDynamicAttribs(resolveLevel);
}
// Writes out a 180-degree point stroke, which renders as a circle with a diameter equal to the
// stroke width. These should be drawn at at cusp points on curves and for round caps.
void writeCircle(int8_t resolveLevel, SkPoint center) {
SkASSERT(0 <= resolveLevel && resolveLevel < kNumBins);
// An empty stroke is a special case that denotes a circle, or 180-degree point stroke.
fInstanceWriters[resolveLevel].fill(center, 5);
// Mark numTotalEdges negative so the shader assigns the least possible number of edges to
// its (empty) preceding join.
fInstanceWriters[resolveLevel].write(-fNumEdgesPerResolveLevel[resolveLevel]);
this->writeDynamicAttribs(resolveLevel);
}
#ifdef SK_DEBUG
~BinningInstanceWriter() {
for (int i = 0; i < kNumBins; ++i) {
if (fInstanceWriters[i]) {
SkASSERT(fInstanceWriters[i] == fEndWriters[i]);
}
}
}
#endif
private:
void writeDynamicAttribs(int8_t resolveLevel) {
if (fShaderFlags & ShaderFlags::kDynamicStroke) {
fInstanceWriters[resolveLevel].write(fDynamicStroke);
}
if (fShaderFlags & ShaderFlags::kDynamicColor) {
fInstanceWriters[resolveLevel].write(fDynamicColor);
}
}
const ShaderFlags fShaderFlags;
GrVertexWriter fInstanceWriters[kNumBins];
float fNumEdgesPerResolveLevel[kNumBins];
SkDEBUGCODE(GrVertexWriter fEndWriters[kNumBins];)
// Stateful values for the dynamic state (if any) that will get written out with each instance.
DynamicStroke fDynamicStroke;
GrVertexColor fDynamicColor;
};
} // namespace
void GrStrokeIndirectTessellator::prepare(GrMeshDrawOp::Target* target,
int /*totalCombinedVerbCnt*/) {
SkASSERT(fResolveLevels);
SkASSERT(!fDrawIndirectBuffer);
SkASSERT(!fInstanceBuffer);
if (!fChainedDrawIndirectCount) {
return;
}
SkASSERT(fChainedDrawIndirectCount > 0);
SkASSERT(fChainedInstanceCount > 0);
// Allocate indirect draw commands.
GrDrawIndirectWriter indirectWriter = target->makeDrawIndirectSpace(fChainedDrawIndirectCount,
&fDrawIndirectBuffer,
&fDrawIndirectOffset);
if (!indirectWriter) {
SkASSERT(!fDrawIndirectBuffer);
return;
}
SkDEBUGCODE(auto endIndirectWriter = indirectWriter.makeOffset(fChainedDrawIndirectCount));
// We already know the instance count. Allocate an instance for each.
int baseInstance;
GrVertexWriter instanceWriter = {target->makeVertexSpace(fShader.instanceStride(),
fChainedInstanceCount,
&fInstanceBuffer, &baseInstance)};
if (!instanceWriter) {
SkASSERT(!fInstanceBuffer);
fDrawIndirectBuffer.reset();
return;
}
SkDEBUGCODE(auto endInstanceWriter = instanceWriter.makeOffset(fShader.instanceStride() *
fChainedInstanceCount);)
// Fill in the indirect-draw and instance buffers.
for (auto* tess = this; tess; tess = tess->fNextInChain) {
tess->writeBuffers(&indirectWriter, &instanceWriter, fShader.viewMatrix(),
fShader.instanceStride(), baseInstance, fMaxNumExtraEdgesInJoin);
baseInstance += tess->fTotalInstanceCount;
}
SkASSERT(indirectWriter == endIndirectWriter);
SkASSERT(instanceWriter == endInstanceWriter);
}
void GrStrokeIndirectTessellator::writeBuffers(GrDrawIndirectWriter* indirectWriter,
GrVertexWriter* instanceWriter,
const SkMatrix& viewMatrix,
size_t instanceStride, int baseInstance,
int numExtraEdgesInJoin) {
BinningInstanceWriter binningWriter(indirectWriter, instanceWriter, fShader.flags(),
instanceStride, baseInstance, numExtraEdgesInJoin,
fResolveLevelCounts);
SkPoint scratchBuffer[4 + 10];
SkPoint* scratch = scratchBuffer;
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 (PathStrokeList* pathStroke = fPathStrokeList; pathStroke; pathStroke = pathStroke->fNext) {
const SkStrokeRec& stroke = pathStroke->fStroke;
SkASSERT(stroke.getJoin() != SkPaint::kMiter_Join || numExtraEdgesInJoin == 4);
bool isRoundJoin = (stroke.getJoin() == SkPaint::kRound_Join);
if (fShader.hasDynamicStroke()) {
binningWriter.updateDynamicStroke(stroke);
}
if (fShader.hasDynamicColor()) {
binningWriter.updateDynamicColor(pathStroke->fColor);
}
GrStrokeIterator iter(pathStroke->fPath, &stroke, &viewMatrix);
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:
binningWriter.writeCircle(*nextResolveLevel++, pts[0]);
[[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.
binningWriter.writeStroke(firstResolveLevel, firstCubic, lastControlPoint);
firstCubic = nullptr;
}
hasLastControlPoint = false;
// Restore "scratch" to the original scratchBuffer.
scratch = scratchBuffer;
continue;
case Verb::kLine:
resolveLevel = (isRoundJoin) ? *nextResolveLevel++ : 0;
scratch[0] = scratch[1] = pts[0];
scratch[2] = scratch[3] = pts[1];
pts_ = scratch;
break;
case Verb::kQuad:
resolveLevel = *nextResolveLevel++;
if (resolveLevel < 0) {
// The curve has a cusp. Draw two lines and a circle instead of a quad.
int8_t cuspResolveLevel = -resolveLevel;
float cuspT = SkFindQuadMidTangent(pts);
SkPoint cusp = SkEvalQuadAt(pts, cuspT);
numChops = 1;
scratch[0] = scratch[1] = pts[0];
scratch[2] = scratch[3] = scratch[4] = cusp;
scratch[5] = scratch[6] = pts[2];
binningWriter.writeCircle(cuspResolveLevel, cusp);
resolveLevel = (isRoundJoin) ? *nextResolveLevel++ : 0;
} else {
GrPathUtils::convertQuadToCubic(pts, scratch);
}
pts_ = scratch;
break;
case Verb::kConic:
resolveLevel = *nextResolveLevel++;
if (resolveLevel < 0) {
// The curve has a cusp. Draw two lines and a cusp instead of a conic.
int8_t cuspResolveLevel = -resolveLevel;
SkPoint cusp;
SkConic conic(pts, iter.w());
float cuspT = conic.findMidTangent();
conic.evalAt(cuspT, &cusp);
numChops = 1;
scratch[0] = scratch[1] = pts[0];
scratch[2] = scratch[3] = scratch[4] = cusp;
scratch[5] = scratch[6] = pts[2];
binningWriter.writeCircle(cuspResolveLevel, cusp);
resolveLevel = (isRoundJoin) ? *nextResolveLevel++ : 0;
} else {
GrPathShader::WriteConicPatch(pts, iter.w(), 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 >> 4;
SkChopCubicAt(pts, scratch, nextChopTs, numChops);
nextChopTs += numChops;
pts_ = scratch;
// Are the chop points cusps?
if (int8_t cuspResolveLevel = (-resolveLevel & 0xf)) {
for (int i = 1; i <= numChops; ++i) {
binningWriter.writeCircle(cuspResolveLevel, pts_[i*3]);
}
}
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 {
binningWriter.writeStroke(resolveLevel, pts_, lastControlPoint, (i != 0));
}
// Determine the last control point.
if (pts_[2] != pts_[3] && verb != Verb::kConic) { // Conics use pts_[3] for w.
lastControlPoint = pts_[2];
} else if (pts_[1] != pts_[2]) {
lastControlPoint = pts_[1];
} else if (pts_[0] != pts_[1]) {
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::kConic || verb == Verb::kCubic);
}
}
}
SkASSERT(nextResolveLevel == fResolveLevels + fResolveLevelArrayCount);
SkASSERT(nextChopTs == fChopTs + fChopTsArrayCount);
}
void GrStrokeIndirectTessellator::draw(GrOpFlushState* flushState) const {
if (!fDrawIndirectBuffer) {
return;
}
SkASSERT(fChainedDrawIndirectCount > 0);
SkASSERT(fChainedInstanceCount > 0);
flushState->bindBuffers(nullptr, fInstanceBuffer, nullptr);
flushState->drawIndirect(fDrawIndirectBuffer.get(), fDrawIndirectOffset,
fChainedDrawIndirectCount);
}