Merge joins into the same tessellation patch as their following stroke

Implements the previous join as a sub-section of the tessellation
patch. This cuts the number of vertex shader invocations in half since
we are no longer treating joins as their own patch. (This therefore
cuts the amount of inflection/midtangent chopping work in half also.)

This required a lot of modifications to GrStrokePatchBuilder.cpp, so
this CL also finishes up the chopping logic in that file for when there
aren't enough tessellation segments to render a curve.

Bug: skia:10419
Change-Id: I3da081fe756c97aeeb65e27f1319a29763b4ad34
Reviewed-on: https://skia-review.googlesource.com/c/skia/+/318876
Reviewed-by: Chris Dalton <csmartdalton@google.com>
Reviewed-by: Michael Ludwig <michaelludwig@google.com>
Commit-Queue: Chris Dalton <csmartdalton@google.com>
This commit is contained in:
Chris Dalton 2020-09-23 12:23:38 -06:00 committed by Skia Commit-Bot
parent d1e6716f6a
commit 49814b9fdc
7 changed files with 815 additions and 570 deletions

View File

@ -19,21 +19,547 @@
using Patch = GrStrokeTessellateShader::Patch;
constexpr static float kDoubleSidedRoundJoinType = -Patch::kRoundJoinType;
static SkPoint lerp(const SkPoint& a, const SkPoint& b, float T) {
SkASSERT(1 != T); // The below does not guarantee lerp(a, b, 1) === b.
return (b - a) * T + a;
}
void GrStrokePatchBuilder::allocPatchChunkAtLeast(int minPatchAllocCount) {
PatchChunk* chunk = &fPatchChunkArray->push_back();
fCurrChunkPatchData = (Patch*)fTarget->makeVertexSpaceAtLeast(sizeof(Patch), minPatchAllocCount,
minPatchAllocCount,
&chunk->fPatchBuffer,
&chunk->fBasePatch,
&fCurrChunkPatchCapacity);
fCurrChunkMinPatchAllocCount = minPatchAllocCount;
static float num_combined_segments(float numParametricSegments, float numRadialSegments) {
// The first and last edges are shared by both the parametric and radial sets of edges, so the
// total number of edges is:
//
// numCombinedEdges = numParametricEdges + numRadialEdges - 2
//
// It's also important to differentiate between the number of edges and segments in a strip:
//
// numCombinedSegments = numCombinedEdges - 1
//
// So the total number of segments in the combined strip is:
//
// numCombinedSegments = numParametricEdges + numRadialEdges - 2 - 1
// = numParametricSegments + 1 + numRadialSegments + 1 - 2 - 1
// = numParametricSegments + numRadialSegments - 1
//
return numParametricSegments + numRadialSegments - 1;
}
static float num_parametric_segments(float numCombinedSegments, float numRadialSegments) {
// numCombinedSegments = numParametricSegments + numRadialSegments - 1.
// (See num_combined_segments()).
return numCombinedSegments + 1 - numRadialSegments;
}
GrStrokePatchBuilder::GrStrokePatchBuilder(GrMeshDrawOp::Target* target,
SkTArray<PatchChunk>* patchChunkArray, float matrixScale,
const SkStrokeRec& stroke, int totalCombinedVerbCnt)
: fTarget(target)
, fPatchChunkArray(patchChunkArray)
, fMaxTessellationSegments(target->caps().shaderCaps()->maxTessellationSegments())
, fLinearizationIntolerance(matrixScale *
GrTessellationPathRenderer::kLinearizationIntolerance)
, fStroke(stroke) {
// We don't support hairline strokes. For now, the client can transform the path into device
// space and then use a stroke width of 1.
SkASSERT(fStroke.getWidth() > 0);
// This is the number of radial segments we need to add to a triangle strip for each radian
// of rotation, given the current stroke radius. Any fewer radial segments and our error
// would fall outside the linearization tolerance.
fNumRadialSegmentsPerRadian = 1 / std::acos(
std::max(1 - 1 / (fLinearizationIntolerance * fStroke.getWidth() * .5f), -1.f));
// Calculate the worst-case numbers of parametric segments our hardware can support for the
// current stroke radius, in the event that there are also enough radial segments to rotate
// 180 and 360 degrees respectively. These are used for "quick accepts" that allow us to
// send almost all curves directly to the hardware without having to chop.
float numRadialSegments180 = std::max(std::ceil(
SK_ScalarPI * fNumRadialSegmentsPerRadian), 1.f);
fMaxParametricSegments180 = num_parametric_segments(fMaxTessellationSegments,
numRadialSegments180);
float numRadialSegments360 = std::max(std::ceil(
2*SK_ScalarPI * fNumRadialSegmentsPerRadian), 1.f);
fMaxParametricSegments360 = num_parametric_segments(fMaxTessellationSegments,
numRadialSegments360);
// Now calculate the worst-case numbers of parametric segments if we are to integrate a join
// into the same patch as the curve.
float maxNumSegmentsInJoin;
switch (fStroke.getJoin()) {
case SkPaint::kBevel_Join:
maxNumSegmentsInJoin = 1;
break;
case SkPaint::kMiter_Join:
maxNumSegmentsInJoin = 2;
break;
case SkPaint::kRound_Join:
// 180-degree round join.
maxNumSegmentsInJoin = numRadialSegments180;
break;
}
// Subtract an extra 1 off the end because when we integrate a join, the tessellator has to add
// a redundant edge between the join and curve.
fMaxParametricSegments180_withJoin = fMaxParametricSegments180 - maxNumSegmentsInJoin - 1;
fMaxParametricSegments360_withJoin = fMaxParametricSegments360 - maxNumSegmentsInJoin - 1;
fMaxCombinedSegments_withJoin = fMaxTessellationSegments - maxNumSegmentsInJoin - 1;
fSoloRoundJoinAlwaysFitsInPatch = (numRadialSegments180 <= fMaxTessellationSegments);
// Pre-allocate at least enough vertex space for 1 in 4 strokes to chop, and for 8 caps.
int strokePreallocCount = totalCombinedVerbCnt * 5/4;
int capPreallocCount = 8;
this->allocPatchChunkAtLeast(strokePreallocCount + capPreallocCount);
}
void GrStrokePatchBuilder::addPath(const SkPath& path) {
fHasLastControlPoint = false;
SkDEBUGCODE(fHasCurrentPoint = false;)
SkPathVerb previousVerb = SkPathVerb::kClose;
for (auto [verb, pts, w] : SkPathPriv::Iterate(path)) {
switch (verb) {
case SkPathVerb::kMove:
// "A subpath ... consisting of a single moveto shall not be stroked."
// https://www.w3.org/TR/SVG11/painting.html#StrokeProperties
if (previousVerb != SkPathVerb::kMove && previousVerb != SkPathVerb::kClose) {
this->cap();
}
this->moveTo(pts[0]);
break;
case SkPathVerb::kLine:
SkASSERT(fHasCurrentPoint);
SkASSERT(pts[0] == fCurrentPoint);
this->lineTo(pts[1]);
break;
case SkPathVerb::kQuad:
this->quadraticTo(pts);
break;
case SkPathVerb::kCubic:
this->cubicTo(pts);
break;
case SkPathVerb::kConic:
SkUNREACHABLE;
case SkPathVerb::kClose:
this->close();
break;
}
previousVerb = verb;
}
if (previousVerb != SkPathVerb::kMove && previousVerb != SkPathVerb::kClose) {
this->cap();
}
}
void GrStrokePatchBuilder::moveTo(SkPoint pt) {
fCurrentPoint = fCurrContourStartPoint = pt;
fHasLastControlPoint = false;
SkDEBUGCODE(fHasCurrentPoint = true;)
}
void GrStrokePatchBuilder::moveTo(SkPoint pt, SkPoint lastControlPoint) {
fCurrentPoint = fCurrContourStartPoint = pt;
fCurrContourFirstControlPoint = fLastControlPoint = lastControlPoint;
fHasLastControlPoint = true;
SkDEBUGCODE(fHasCurrentPoint = true;)
}
void GrStrokePatchBuilder::lineTo(SkPoint pt, JoinType prevJoinType) {
SkASSERT(fHasCurrentPoint);
// Zero-length paths need special treatment because they are spec'd to behave differently.
if (pt == fCurrentPoint) {
return;
}
if (fMaxCombinedSegments_withJoin < 1 || prevJoinType == JoinType::kCusp) {
// Either the stroke has extremely thick round joins and there aren't enough guaranteed
// segments to always combine a join with a line patch, or we need a cusp. Either way we
// handle the join in its own separate patch.
this->joinTo(prevJoinType, pt);
prevJoinType = JoinType::kNone;
}
SkPoint asCubic[4] = {fCurrentPoint, fCurrentPoint, pt, pt};
this->cubicToRaw(prevJoinType, asCubic);
}
static bool chop_pt_is_cusp(const SkPoint& prevControlPoint, const SkPoint& chopPoint,
const SkPoint& nextControlPoint) {
// Adjacent chops should almost always be colinear. The only case where they will not be is a
// cusp, which will rotate a minimum of 180 degrees.
return (nextControlPoint - chopPoint).dot(chopPoint - prevControlPoint) <= 0;
}
static bool quad_chop_is_cusp(const SkPoint chops[5]) {
SkPoint chopPt = chops[2];
SkPoint prevCtrlPt = (chops[1] != chopPt) ? chops[1] : chops[0];
SkPoint nextCtrlPt = (chops[3] != chopPt) ? chops[3] : chops[4];
return chop_pt_is_cusp(prevCtrlPt, chopPt, nextCtrlPt);
}
void GrStrokePatchBuilder::quadraticTo(const SkPoint p[3], JoinType prevJoinType, int maxDepth) {
SkASSERT(fHasCurrentPoint);
SkASSERT(p[0] == fCurrentPoint);
// Zero-length paths need special treatment because they are spec'd to behave differently. If
// the control point is colocated on an endpoint then this might end up being the case. Fall
// back on a lineTo and let it make the final check.
if (p[1] == p[0] || p[1] == p[2]) {
this->lineTo(p[2], prevJoinType);
return;
}
// Convert to a cubic.
SkPoint asCubic[4] = {p[0], lerp(p[0], p[1], 2/3.f), lerp(p[1], p[2], 1/3.f), p[2]};
// Ensure our hardware supports enough tessellation segments to render the curve. This early out
// assumes a worst-case quadratic rotation of 180 degrees and a worst-case number of segments in
// the join.
//
// An informal survey of skottie animations and gms revealed that even with a bare minimum of 64
// tessellation segments, 99.9%+ of quadratics take this early out.
float numParametricSegments = GrWangsFormula::quadratic(fLinearizationIntolerance, p);
if (numParametricSegments <= fMaxParametricSegments180_withJoin &&
prevJoinType != JoinType::kCusp) {
this->cubicToRaw(prevJoinType, asCubic);
return;
}
if (numParametricSegments <= fMaxParametricSegments180 || maxDepth == 0) {
if (numParametricSegments > fMaxParametricSegments180_withJoin ||
prevJoinType == JoinType::kCusp) {
// Either there aren't enough guaranteed segments to include the join in the quadratic's
// patch, or we need a cusp. Emit a standalone patch for the join.
this->joinTo(prevJoinType, asCubic);
prevJoinType = JoinType::kNone;
}
this->cubicToRaw(prevJoinType, asCubic);
return;
}
// We still might have enough tessellation segments to render the curve. Check again with the
// actual rotation.
float numRadialSegments = SkMeasureQuadRotation(p) * fNumRadialSegmentsPerRadian;
numRadialSegments = std::max(std::ceil(numRadialSegments), 1.f);
numParametricSegments = std::max(std::ceil(numParametricSegments), 1.f);
float numCombinedSegments = num_combined_segments(numParametricSegments, numRadialSegments);
if (numCombinedSegments > fMaxTessellationSegments) {
// The hardware doesn't support enough segments for this curve. Chop and recurse.
if (maxDepth < 0) {
// Decide on an extremely conservative upper bound for when to quit chopping. This
// is solely to protect us from infinite recursion in instances where FP error
// prevents us from chopping at the correct midtangent.
maxDepth = sk_float_nextlog2(numParametricSegments) +
sk_float_nextlog2(numRadialSegments) + 1;
maxDepth = std::max(maxDepth, 1);
}
SkPoint chops[5];
if (numParametricSegments >= numRadialSegments) {
SkChopQuadAtHalf(p, chops);
} else {
SkChopQuadAtMidTangent(p, chops);
}
this->quadraticTo(chops, prevJoinType, maxDepth - 1);
// If we chopped at a cusp then rotation is not continuous between the two curves. Insert a
// cusp to make up for lost rotation.
JoinType nextJoinType = (quad_chop_is_cusp(chops)) ?
JoinType::kCusp : JoinType::kFromStroke;
this->quadraticTo(chops + 2, nextJoinType, maxDepth - 1);
return;
}
if (numCombinedSegments > fMaxCombinedSegments_withJoin ||
prevJoinType == JoinType::kCusp) {
// Either there aren't enough guaranteed segments to include the join in the quadratic's
// patch, or we need a cusp. Emit a standalone patch for the join.
this->joinTo(prevJoinType, asCubic);
prevJoinType = JoinType::kNone;
}
this->cubicToRaw(prevJoinType, asCubic);
}
static bool cubic_chop_is_cusp(const SkPoint chops[7]) {
SkPoint chopPt = chops[3];
auto prevCtrlPt = (chops[2] != chopPt) ? chops[2] : (chops[1] != chopPt) ? chops[1] : chops[0];
auto nextCtrlPt = (chops[4] != chopPt) ? chops[4] : (chops[5] != chopPt) ? chops[5] : chops[6];
return chop_pt_is_cusp(prevCtrlPt, chopPt, nextCtrlPt);
}
void GrStrokePatchBuilder::cubicTo(const SkPoint p[4], JoinType prevJoinType,
Convex180Status convex180Status, int maxDepth) {
SkASSERT(fHasCurrentPoint);
SkASSERT(p[0] == fCurrentPoint);
// The stroke tessellation shader assigns special meaning to p0==p1==p2 and p1==p2==p3. If this
// is the case then we need to rewrite the cubic.
if (p[1] == p[2] && (p[1] == p[0] || p[1] == p[3])) {
this->lineTo(p[3], prevJoinType);
return;
}
// Ensure our hardware supports enough tessellation segments to render the curve. This early out
// assumes a worst-case cubic rotation of 360 degrees and a worst-case number of segments in the
// join.
//
// An informal survey of skottie animations revealed that with a bare minimum of 64 tessellation
// segments, 95% of cubics take this early out.
float numParametricSegments = GrWangsFormula::cubic(fLinearizationIntolerance, p);
if (numParametricSegments <= fMaxParametricSegments360_withJoin &&
prevJoinType != JoinType::kCusp) {
this->cubicToRaw(prevJoinType, p);
return;
}
float maxParametricSegments = (convex180Status == Convex180Status::kYes) ?
fMaxParametricSegments180 : fMaxParametricSegments360;
if (numParametricSegments <= maxParametricSegments || maxDepth == 0) {
float maxParametricSegments_withJoin = (convex180Status == Convex180Status::kYes) ?
fMaxParametricSegments180_withJoin : fMaxParametricSegments360_withJoin;
if (numParametricSegments > maxParametricSegments_withJoin ||
prevJoinType == JoinType::kCusp) {
// Either there aren't enough guaranteed segments to include the join in the cubic's
// patch, or we need a cusp. Emit a standalone patch for the join.
this->joinTo(prevJoinType, p);
prevJoinType = JoinType::kNone;
}
this->cubicToRaw(prevJoinType, p);
return;
}
// Ensure the curve does not inflect or rotate >180 degrees before we start subdividing and
// measuring rotation.
SkPoint chops[10];
if (convex180Status == Convex180Status::kUnknown) {
float chopT[2];
int n = SkFindCubicInflections(p, chopT);
if (n == 0) {
// No inflections. Chop at midtangent to guarantee rotation <= 180 degrees.
chopT[0] = SkFindCubicMidTangent(p);
n = 1;
}
SkChopCubicAt(p, chops, chopT, n);
this->cubicTo(chops, prevJoinType, Convex180Status::kYes, maxDepth);
for (int i = 1; i <= n; ++i) {
// If we chopped at a cusp then rotation is not continuous between the two curves.
// Insert a double cuspe up for lost rotation. (This should not be possible from a
// purely mathematical standpoint, since an inflection is not a cusp, but we still check
// for the sake of robust handling of FP32 precision issues.)
JoinType nextJoinType = (cubic_chop_is_cusp(chops + (i - 1)*3)) ?
JoinType::kCusp : JoinType::kFromStroke;
this->cubicTo(chops + i*3, nextJoinType, Convex180Status::kYes, maxDepth);
}
return;
}
// We still might have enough tessellation segments to render the curve. Check again with
// its actual rotation.
float numRadialSegments = SkMeasureNonInflectCubicRotation(p) * fNumRadialSegmentsPerRadian;
numRadialSegments = std::max(std::ceil(numRadialSegments), 1.f);
numParametricSegments = std::max(std::ceil(numParametricSegments), 1.f);
float numCombinedSegments = num_combined_segments(numParametricSegments, numRadialSegments);
if (numCombinedSegments > fMaxTessellationSegments) {
// The hardware doesn't support enough segments for this curve. Chop and recurse.
if (maxDepth < 0) {
// Decide on an extremely conservative upper bound for when to quit chopping. This
// is solely to protect us from infinite recursion in instances where FP error
// prevents us from chopping at the correct midtangent.
maxDepth = sk_float_nextlog2(numParametricSegments) +
sk_float_nextlog2(numRadialSegments) + 1;
maxDepth = std::max(maxDepth, 1);
}
if (numParametricSegments >= numRadialSegments) {
SkChopCubicAtHalf(p, chops);
} else {
SkChopCubicAtMidTangent(p, chops);
}
// If we chopped at a cusp then rotation is not continuous between the two curves. Insert a
// cusp to make up for lost rotation.
JoinType nextJoinType = (cubic_chop_is_cusp(chops)) ?
JoinType::kCusp : JoinType::kFromStroke;
this->cubicTo(chops, prevJoinType, Convex180Status::kYes, maxDepth - 1);
this->cubicTo(chops + 3, nextJoinType, Convex180Status::kYes, maxDepth - 1);
return;
}
if (numCombinedSegments > fMaxCombinedSegments_withJoin || prevJoinType == JoinType::kCusp) {
// Either there aren't enough guaranteed segments to include the join in the cubic's patch,
// or we need a cusp. Emit a standalone patch for the join.
this->joinTo(prevJoinType, p);
prevJoinType = JoinType::kNone;
}
this->cubicToRaw(prevJoinType, p);
}
void GrStrokePatchBuilder::joinTo(JoinType joinType, SkPoint nextControlPoint, int maxDepth) {
SkASSERT(fHasCurrentPoint);
if (!fHasLastControlPoint) {
// The first stroke doesn't have a previous join.
return;
}
if (!fSoloRoundJoinAlwaysFitsInPatch && maxDepth != 0 &&
(fStroke.getJoin() == SkPaint::kRound_Join || joinType == JoinType::kCusp)) {
SkVector tan0 = fCurrentPoint - fLastControlPoint;
SkVector tan1 = nextControlPoint - fCurrentPoint;
float rotation = SkMeasureAngleInsideVectors(tan0, tan1);
float numRadialSegments = rotation * fNumRadialSegmentsPerRadian;
if (numRadialSegments > fMaxTessellationSegments) {
// This is a round join that requires more segments than the tessellator supports.
// Split it and recurse.
if (maxDepth < 0) {
// Decide on an upper bound for when to quit chopping. This is solely to protect
// us from infinite recursion due to FP precision issues.
maxDepth = sk_float_nextlog2(numRadialSegments / fMaxTessellationSegments);
maxDepth = std::max(maxDepth, 1);
}
// Find the bisector so we can split the join in half.
SkPoint bisector = SkFindBisector(tan0, tan1);
// c0 will be the "next" control point for the first join half, and c1 will be the
// "previous" control point for the second join half.
SkPoint c0, c1;
// FIXME: This hack ensures "c0 - fCurrentPoint" gives the exact same ieee fp32 vector
// as "-(c1 - fCurrentPoint)". If our current strategy of join chopping sticks, we may
// want to think of a cleaner method to avoid T-junctions when we chop joins.
int maxAttempts = 10;
do {
bisector = (fCurrentPoint + bisector) - (fCurrentPoint - bisector);
c0 = fCurrentPoint + bisector;
c1 = fCurrentPoint - bisector;
} while (c0 - fCurrentPoint != -(c1 - fCurrentPoint) && --maxAttempts);
this->joinTo(joinType, c0, maxDepth - 1); // First join half.
fLastControlPoint = c1;
this->joinTo(joinType, nextControlPoint, maxDepth - 1); // Second join half.
return;
}
}
this->joinToRaw(joinType, nextControlPoint);
}
void GrStrokePatchBuilder::close() {
SkASSERT(fHasCurrentPoint);
if (!fHasLastControlPoint) {
// Draw caps instead of closing if the subpath is zero length:
//
// "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)
//
this->cap();
return;
}
// Draw a line back to the beginning. (This will be discarded if
// fCurrentPoint == fCurrContourStartPoint.)
this->lineTo(fCurrContourStartPoint);
this->joinTo(JoinType::kFromStroke, fCurrContourFirstControlPoint);
fHasLastControlPoint = false;
SkDEBUGCODE(fHasCurrentPoint = false;)
}
static SkVector normalize(const SkVector& v) {
SkVector norm = v;
norm.normalize();
return norm;
}
void GrStrokePatchBuilder::cap() {
SkASSERT(fHasCurrentPoint);
if (!fHasLastControlPoint) {
// We don't have any control points to orient the caps. In this case, square and round caps
// are specified to be drawn as an axis-aligned square or circle respectively. Assign
// default control points that achieve this.
fCurrContourFirstControlPoint = fCurrContourStartPoint - SkPoint{1,0};
fLastControlPoint = fCurrContourStartPoint + SkPoint{1,0};
fCurrentPoint = fCurrContourStartPoint;
fHasLastControlPoint = true;
}
switch (fStroke.getCap()) {
case SkPaint::kButt_Cap:
break;
case SkPaint::kRound_Cap: {
// A round cap is the same thing as a 180-degree round join.
// If our join type isn't round we can alternatively use a cusp.
JoinType roundCapJoinType = (fStroke.getJoin() == SkPaint::kRound_Join) ?
JoinType::kFromStroke : JoinType::kCusp;
this->joinTo(roundCapJoinType, fLastControlPoint);
this->moveTo(fCurrContourStartPoint, fCurrContourFirstControlPoint);
this->joinTo(roundCapJoinType, fCurrContourFirstControlPoint);
break;
}
case SkPaint::kSquare_Cap: {
// A square cap is the same as appending lineTos.
float rad = fStroke.getWidth() * .5f;
this->lineTo(fCurrentPoint + normalize(fCurrentPoint - fLastControlPoint) * rad);
this->moveTo(fCurrContourStartPoint, fCurrContourFirstControlPoint);
this->lineTo(fCurrContourStartPoint +
normalize(fCurrContourStartPoint - fCurrContourFirstControlPoint) * rad);
break;
}
}
fHasLastControlPoint = false;
SkDEBUGCODE(fHasCurrentPoint = false;)
}
void GrStrokePatchBuilder::cubicToRaw(JoinType prevJoinType, const SkPoint pts[4]) {
// Cusps can't be combined with a stroke patch. They need to have been written out already as
// their own standalone patch.
SkASSERT(prevJoinType != JoinType::kCusp);
SkPoint c1 = (pts[1] == pts[0]) ? pts[2] : pts[1];
SkPoint c2 = (pts[2] == pts[3]) ? pts[1] : pts[2];
if (!fHasLastControlPoint) {
// The first stroke doesn't have a previous join (yet). If the current contour ends up
// closing itself, we will add that join as its own patch.
// TODO: Consider deferring the first stroke until we know whether the contour will close.
// This will allow us to use the closing join as the first patch's previous join.
prevJoinType = JoinType::kNone;
fCurrContourFirstControlPoint = c1;
fHasLastControlPoint = true;
} else {
// By using JoinType::kNone, the caller promises to have written out their own join that
// seams exactly with this curve.
SkASSERT((prevJoinType != JoinType::kNone) || fLastControlPoint == c1);
}
if (Patch* patch = this->reservePatch()) {
// Disable the join section of this patch if prevJoinType is kNone by setting the previous
// control point equal to p0.
patch->fPrevControlPoint = (prevJoinType == JoinType::kNone) ? pts[0] : fLastControlPoint;
patch->fPts = {pts[0], pts[1], pts[2], pts[3]};
}
fLastControlPoint = c2;
fCurrentPoint = pts[3];
}
void GrStrokePatchBuilder::joinToRaw(JoinType joinType, SkPoint nextControlPoint) {
// We should never write out joins before the first curve.
SkASSERT(fHasLastControlPoint);
SkASSERT(fHasCurrentPoint);
if (Patch* joinPatch = this->reservePatch()) {
joinPatch->fPrevControlPoint = fLastControlPoint;
joinPatch->fPts[0] = fCurrentPoint;
if (joinType == JoinType::kFromStroke) {
// [p0, p3, p3, p3] is a reserved pattern that means this patch is a join only (no cubic
// sections in the patch).
joinPatch->fPts[1] = joinPatch->fPts[2] = nextControlPoint;
} else {
SkASSERT(joinType == JoinType::kCusp);
// [p0, p0, p0, p3] is a reserved pattern that means this patch is a cusp point.
joinPatch->fPts[1] = joinPatch->fPts[2] = fCurrentPoint;
}
joinPatch->fPts[3] = nextControlPoint;
}
fLastControlPoint = nextControlPoint;
}
Patch* GrStrokePatchBuilder::reservePatch() {
@ -52,301 +578,12 @@ Patch* GrStrokePatchBuilder::reservePatch() {
return patch;
}
void GrStrokePatchBuilder::writeCubicSegment(float prevJoinType, const SkPoint pts[4],
float cubicType) {
SkPoint c1 = (pts[1] == pts[0]) ? pts[2] : pts[1];
SkPoint c2 = (pts[2] == pts[3]) ? pts[1] : pts[2];
if (fHasPreviousSegment) {
this->writeJoin(prevJoinType, fLastControlPoint, pts[0], c1);
} else {
fCurrContourFirstControlPoint = c1;
fHasPreviousSegment = true;
}
SkASSERT(cubicType == Patch::kStandardCubicType || cubicType == Patch::kFlatLineType);
if (Patch* patch = this->reservePatch()) {
memcpy(patch->fPts.data(), pts, sizeof(patch->fPts));
patch->fPatchType = cubicType;
patch->fStrokeRadius = fCurrStrokeRadius;
}
fLastControlPoint = c2;
fCurrentPoint = pts[3];
}
void GrStrokePatchBuilder::writeJoin(float joinType, const SkPoint& prevControlPoint,
const SkPoint& anchorPoint, const SkPoint& nextControlPoint) {
SkASSERT(SkScalarAbs(joinType) == Patch::kRoundJoinType ||
SkScalarAbs(joinType) == Patch::kMiterJoinType ||
SkScalarAbs(joinType) == Patch::kBevelJoinType);
if (Patch* joinPatch = this->reservePatch()) {
joinPatch->fPts = {{prevControlPoint, anchorPoint, anchorPoint, nextControlPoint}};
joinPatch->fPatchType = joinType;
joinPatch->fStrokeRadius = fCurrStrokeRadius;
}
}
void GrStrokePatchBuilder::writeSquareCap(const SkPoint& endPoint, const SkPoint& controlPoint) {
SkVector v = (endPoint - controlPoint);
v.normalize();
SkPoint capPoint = endPoint + v*fCurrStrokeRadius;
// Add a join to guarantee we get water tight seaming. Make the join type negative so it's
// double sided.
this->writeJoin(-fCurrStrokeJoinType, controlPoint, endPoint, capPoint);
if (Patch* capPatch = this->reservePatch()) {
capPatch->fPts = {{endPoint, endPoint, capPoint, capPoint}};
capPatch->fPatchType = Patch::kFlatLineType;
capPatch->fStrokeRadius = fCurrStrokeRadius;
}
}
void GrStrokePatchBuilder::writeCaps(SkPaint::Cap capType) {
if (!fHasPreviousSegment) {
// We don't have any control points to orient the caps. In this case, square and round caps
// are specified to be drawn as an axis-aligned square or circle respectively. Assign
// default control points that achieve this.
fCurrContourFirstControlPoint = fCurrContourStartPoint - SkPoint{1,0};
fLastControlPoint = fCurrContourStartPoint + SkPoint{1,0};
fCurrentPoint = fCurrContourStartPoint;
}
switch (capType) {
case SkPaint::kButt_Cap:
break;
case SkPaint::kRound_Cap:
// A round cap is the same thing as a 180-degree round join.
this->writeJoin(Patch::kRoundJoinType, fCurrContourFirstControlPoint,
fCurrContourStartPoint, fCurrContourFirstControlPoint);
this->writeJoin(Patch::kRoundJoinType, fLastControlPoint, fCurrentPoint,
fLastControlPoint);
break;
case SkPaint::kSquare_Cap:
this->writeSquareCap(fCurrContourStartPoint, fCurrContourFirstControlPoint);
this->writeSquareCap(fCurrentPoint, fLastControlPoint);
break;
}
}
static float join_type_from_join(SkPaint::Join join) {
switch (join) {
case SkPaint::kBevel_Join:
return GrStrokeTessellateShader::Patch::kBevelJoinType;
case SkPaint::kMiter_Join:
return GrStrokeTessellateShader::Patch::kMiterJoinType;
case SkPaint::kRound_Join:
return GrStrokeTessellateShader::Patch::kRoundJoinType;
}
SkUNREACHABLE;
}
void GrStrokePatchBuilder::addPath(const SkPath& path, const SkStrokeRec& stroke) {
// We don't support hairline strokes. For now, the client can transform the path into device
// space and then use a stroke width of 1.
SkASSERT(stroke.getWidth() > 0);
fCurrStrokeRadius = stroke.getWidth()/2;
fCurrStrokeJoinType = join_type_from_join(stroke.getJoin());
// This is the number of radial segments we need to add to a triangle strip for each radian of
// rotation, given the current stroke radius. Any fewer radial segments and our error would fall
// outside the linearization tolerance.
fNumRadialSegmentsPerRad = 1 / std::acos(
std::max(1 - 1 / (fLinearizationIntolerance * fCurrStrokeRadius), -1.f));
// Calculate the worst-case numbers of parametric segments our hardware can support for the
// current stroke radius, in the event that there are also enough radial segments to rotate 180
// and 360 degrees respectively. These are used for "quick accepts" that allow us to send almost
// all curves directly to the hardware without having to chop or think any further.
fMaxParametricSegments180 = fMaxTessellationSegments + 1 - std::max(std::ceil(
SK_ScalarPI * fNumRadialSegmentsPerRad), 1.f);
fMaxParametricSegments360 = fMaxTessellationSegments + 1 - std::max(std::ceil(
2*SK_ScalarPI * fNumRadialSegmentsPerRad), 1.f);
fHasPreviousSegment = false;
SkPathVerb previousVerb = SkPathVerb::kClose;
for (auto [verb, pts, w] : SkPathPriv::Iterate(path)) {
switch (verb) {
case SkPathVerb::kMove:
// "A subpath ... consisting of a single moveto shall not be stroked."
// https://www.w3.org/TR/SVG11/painting.html#StrokeProperties
if (previousVerb != SkPathVerb::kMove && previousVerb != SkPathVerb::kClose) {
this->writeCaps(stroke.getCap());
}
this->moveTo(pts[0]);
break;
case SkPathVerb::kClose:
this->close(stroke.getCap());
break;
case SkPathVerb::kLine:
SkASSERT(previousVerb != SkPathVerb::kClose);
this->lineTo(fCurrStrokeJoinType, pts[0], pts[1]);
break;
case SkPathVerb::kQuad:
SkASSERT(previousVerb != SkPathVerb::kClose);
this->quadraticTo(fCurrStrokeJoinType, pts);
break;
case SkPathVerb::kCubic:
SkASSERT(previousVerb != SkPathVerb::kClose);
this->cubicTo(fCurrStrokeJoinType, pts);
break;
case SkPathVerb::kConic:
SkASSERT(previousVerb != SkPathVerb::kClose);
SkUNREACHABLE;
}
previousVerb = verb;
}
if (previousVerb != SkPathVerb::kMove && previousVerb != SkPathVerb::kClose) {
this->writeCaps(stroke.getCap());
}
}
void GrStrokePatchBuilder::moveTo(const SkPoint& pt) {
fHasPreviousSegment = false;
fCurrContourStartPoint = pt;
}
void GrStrokePatchBuilder::lineTo(float prevJoinType, const SkPoint& p0, const SkPoint& p1) {
// Zero-length paths need special treatment because they are spec'd to behave differently.
if (p0 == p1) {
return;
}
SkPoint cubic[4] = {p0, p0, p1, p1};
this->writeCubicSegment(prevJoinType, cubic, Patch::kFlatLineType);
}
void GrStrokePatchBuilder::quadraticTo(float prevJoinType, const SkPoint p[3], int maxDepth) {
// The stroker relies on p1 to find tangents at the endpoints. (We have to treat the endpoint
// tangents carefully in order to get water tight seams with the join segments.) If p1 is
// colocated on an endpoint then we need to draw this quadratic as a line instead.
if (p[1] == p[0] || p[1] == p[2]) {
this->lineTo(prevJoinType, p[0], p[2]);
return;
}
// Ensure our hardware supports enough tessellation segments to render the curve. The first
// branch assumes a worst-case rotation of 180 degrees and checks if even then we have enough.
// In practice it is rare to take even the first branch.
float numParametricSegments = GrWangsFormula::quadratic(fLinearizationIntolerance, p);
if (numParametricSegments > fMaxParametricSegments180 && maxDepth != 0) {
// We still might have enough tessellation segments to render the curve. Check again with
// the actual rotation.
float numRadialSegments = SkMeasureQuadRotation(p) * fNumRadialSegmentsPerRad;
numRadialSegments = std::max(std::ceil(numRadialSegments), 1.f);
numParametricSegments = std::max(std::ceil(numParametricSegments), 1.f);
if (numParametricSegments + numRadialSegments - 1 > fMaxTessellationSegments) {
// The hardware doesn't support enough segments for this curve. Chop and recurse.
if (maxDepth < 0) {
// Decide on an extremely conservative upper bound for when to quit chopping. This
// is solely to protect us from infinite recursion in instances where FP error
// prevents us from chopping at the correct midtangent.
maxDepth = sk_float_nextlog2(numParametricSegments) +
sk_float_nextlog2(numRadialSegments) + 1;
SkASSERT(maxDepth >= 1);
}
SkPoint chopped[5];
if (numParametricSegments >= numRadialSegments) {
SkChopQuadAtHalf(p, chopped);
} else {
SkChopQuadAtMidTangent(p, chopped);
}
this->quadraticTo(prevJoinType, chopped, maxDepth - 1);
// Use kDoubleSidedRoundJoinType in case we happened to chop at the exact turnaround
// point of a flat quadratic, in which case we would lose 180 degrees of rotation.
this->quadraticTo(kDoubleSidedRoundJoinType, chopped + 2, maxDepth - 1);
return;
}
}
SkPoint cubic[4] = {p[0], lerp(p[0], p[1], 2/3.f), lerp(p[1], p[2], 1/3.f), p[2]};
this->writeCubicSegment(prevJoinType, cubic, Patch::kStandardCubicType);
}
void GrStrokePatchBuilder::cubicTo(float prevJoinType, const SkPoint p[4], int maxDepth,
bool mightInflect) {
// The stroker relies on p1 and p2 to find tangents at the endpoints. (We have to treat the
// endpoint tangents carefully in order to get water tight seams with the join segments.) If p0
// and p1 are both colocated on an endpoint then we need to draw this cubic as a line instead.
if (p[1] == p[2] && (p[1] == p[0] || p[1] == p[3])) {
this->lineTo(prevJoinType, p[0], p[3]);
return;
}
// Early-out if by conservative estimate we can ensure our hardware supports enough tessellation
// segments to render the curve. In practice we almost always take this branch.
float numParametricSegments = GrWangsFormula::cubic(fLinearizationIntolerance, p);
if (numParametricSegments <= fMaxParametricSegments360 || maxDepth == 0) {
this->writeCubicSegment(prevJoinType, p, Patch::kStandardCubicType);
return;
}
// Ensure the curve does not inflect before we attempt to measure its rotation.
SkPoint chopped[10];
if (mightInflect) {
float inflectT[2];
if (int n = SkFindCubicInflections(p, inflectT)) {
SkChopCubicAt(p, chopped, inflectT, n);
for (int i = 0; i <= n; ++i) {
this->cubicTo(prevJoinType, chopped + i*3, maxDepth, false);
// Switch to kDoubleSidedRoundJoinType in case we happened to chop at an exact cusp
// or turnaround point of a flat cubic, in which case we would lose 180 degrees of
// rotation.
prevJoinType = kDoubleSidedRoundJoinType;
}
return;
}
}
// We still might have enough tessellation segments to render the curve. Check again with
// its actual rotation.
float numRadialSegments = SkMeasureNonInflectCubicRotation(p) * fNumRadialSegmentsPerRad;
numRadialSegments = std::max(std::ceil(numRadialSegments), 1.f);
numParametricSegments = std::max(std::ceil(numParametricSegments), 1.f);
if (numParametricSegments + numRadialSegments - 1 <= fMaxTessellationSegments) {
this->writeCubicSegment(prevJoinType, p, Patch::kStandardCubicType);
return;
}
// The hardware doesn't support enough segments to tessellate this curve. Chop and recurse.
if (numParametricSegments >= numRadialSegments) {
SkChopCubicAtHalf(p, chopped);
} else {
SkChopCubicAtMidTangent(p, chopped);
}
if (maxDepth < 0) {
// Decide on an extremely conservative upper bound for when to quit chopping. This
// is solely to protect us from infinite recursion in instances where FP error
// prevents us from chopping at the correct midtangent.
maxDepth = sk_float_nextlog2(numParametricSegments) +
sk_float_nextlog2(numRadialSegments) + 1;
SkASSERT(maxDepth >= 1);
}
this->cubicTo(prevJoinType, chopped, maxDepth - 1, false);
// Use kDoubleSidedRoundJoinType in case we happened to chop at an exact cusp or
// turnaround point of a flat cubic, in which case we would lose 180 degrees of
// rotation.
this->cubicTo(kDoubleSidedRoundJoinType, chopped + 3, maxDepth - 1, false);
}
void GrStrokePatchBuilder::close(SkPaint::Cap capType) {
if (!fHasPreviousSegment) {
// Draw caps instead of closing if the subpath is zero length:
//
// "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)
//
this->writeCaps(capType);
return;
}
// Draw a line back to the beginning. (This will be discarded if
// fCurrentPoint == fCurrContourStartPoint.)
this->lineTo(fCurrStrokeJoinType, fCurrentPoint, fCurrContourStartPoint);
this->writeJoin(fCurrStrokeJoinType, fLastControlPoint, fCurrContourStartPoint,
fCurrContourFirstControlPoint);
void GrStrokePatchBuilder::allocPatchChunkAtLeast(int minPatchAllocCount) {
PatchChunk* chunk = &fPatchChunkArray->push_back();
fCurrChunkPatchData = (Patch*)fTarget->makeVertexSpaceAtLeast(sizeof(Patch), minPatchAllocCount,
minPatchAllocCount,
&chunk->fPatchBuffer,
&chunk->fBasePatch,
&fCurrChunkPatchCapacity);
fCurrChunkMinPatchAllocCount = minPatchAllocCount;
}

View File

@ -10,6 +10,7 @@
#include "include/core/SkPaint.h"
#include "include/core/SkPoint.h"
#include "include/core/SkStrokeRec.h"
#include "include/private/SkTArray.h"
#include "src/gpu/ops/GrMeshDrawOp.h"
#include "src/gpu/tessellate/GrStrokeTessellateShader.h"
@ -43,19 +44,8 @@ public:
// pushed to the array. (See GrStrokeTessellateShader.)
//
// All points are multiplied by 'matrixScale' before being written to the GPU buffer.
GrStrokePatchBuilder(GrMeshDrawOp::Target* target, SkTArray<PatchChunk>* patchChunkArray,
float matrixScale, int totalCombinedVerbCnt)
: fTarget(target)
, fPatchChunkArray(patchChunkArray)
, fMaxTessellationSegments(target->caps().shaderCaps()->maxTessellationSegments())
, fLinearizationIntolerance(matrixScale *
GrTessellationPathRenderer::kLinearizationIntolerance) {
// Pre-allocate at least enough vertex space for one stroke in 3 to split, and for 8 caps.
int strokePreallocCount = totalCombinedVerbCnt * 4/3;
int capPreallocCount = 8;
int joinPreallocCount = strokePreallocCount + capPreallocCount;
this->allocPatchChunkAtLeast(strokePreallocCount + capPreallocCount + joinPreallocCount);
}
GrStrokePatchBuilder(GrMeshDrawOp::Target*, SkTArray<PatchChunk>*, float matrixScale,
const SkStrokeRec&, int totalCombinedVerbCnt);
// "Releases" the target to be used externally again by putting back any unused pre-allocated
// vertices.
@ -64,23 +54,43 @@ public:
sizeof(GrStrokeTessellateShader::Patch));
}
void addPath(const SkPath&, const SkStrokeRec&);
void addPath(const SkPath&);
private:
void allocPatchChunkAtLeast(int minPatchAllocCount);
enum class JoinType {
kFromStroke, // The shader will use the join type defined in our fStrokeRec.
kCusp, // Double sided round join.
kNone
};
// Is a cubic curve convex, and does it rotate no more than 180 degrees?
enum class Convex180Status : bool {
kUnknown,
kYes
};
void moveTo(SkPoint);
void moveTo(SkPoint, SkPoint lastControlPoint);
void lineTo(SkPoint, JoinType prevJoinType = JoinType::kFromStroke);
void quadraticTo(const SkPoint[3], JoinType prevJoinType = JoinType::kFromStroke,
int maxDepth = -1);
void cubicTo(const SkPoint[4], JoinType prevJoinType = JoinType::kFromStroke,
Convex180Status = Convex180Status::kUnknown, int maxDepth = -1);
void joinTo(JoinType joinType, const SkPoint nextCubic[]) {
const SkPoint& nextCtrlPt = (nextCubic[1] == nextCubic[0]) ? nextCubic[2] : nextCubic[1];
// The caller should have culled out cubics where p0==p1==p2 by this point.
SkASSERT(nextCtrlPt != nextCubic[0]);
this->joinTo(joinType, nextCtrlPt);
}
void joinTo(JoinType, SkPoint nextControlPoint, int maxDepth = -1);
void close();
void cap();
void cubicToRaw(JoinType prevJoinType, const SkPoint pts[4]);
void joinToRaw(JoinType, SkPoint nextControlPoint);
GrStrokeTessellateShader::Patch* reservePatch();
void writeCubicSegment(float prevJoinType, const SkPoint pts[4], float cubicType);
void writeJoin(float joinType, const SkPoint& prevControlPoint, const SkPoint& anchorPoint,
const SkPoint& nextControlPoint);
void writeSquareCap(const SkPoint& endPoint, const SkPoint& controlPoint);
void writeCaps(SkPaint::Cap);
void moveTo(const SkPoint&);
void lineTo(float prevJoinType, const SkPoint&, const SkPoint&);
void quadraticTo(float prevJoinType, const SkPoint[3], int maxDepth = -1);
void cubicTo(float prevJoinType, const SkPoint[4], int maxDepth = -1, bool mightInflect = true);
void close(SkPaint::Cap);
void allocPatchChunkAtLeast(int minPatchAllocCount);
// These are raw pointers whose lifetimes are controlled outside this class.
GrMeshDrawOp::Target* const fTarget;
@ -90,24 +100,28 @@ private:
// GrTessellationPathRenderer::kIntolerance adjusted for the matrix scale.
const float fLinearizationIntolerance;
// Variables related to the patch chunk that we are currently filling.
int fCurrChunkPatchCapacity;
int fCurrChunkMinPatchAllocCount;
GrStrokeTessellateShader::Patch* fCurrChunkPatchData;
// Variables related to the path that we are currently iterating.
float fCurrStrokeRadius;
float fCurrStrokeJoinType; // See GrStrokeTessellateShader for join type definitions.
float fNumRadialSegmentsPerRad;
// Variables related to the stroke parameters.
const SkStrokeRec fStroke;
float fNumRadialSegmentsPerRadian;
// These values contain worst-case numbers of parametric segments our hardware can support for
// the current stroke radius, in the event that there are also enough radial segments to rotate
// 180 and 360 degrees respectively. These are used for "quick accepts" that allow us to send
// almost all curves directly to the hardware without having to chop or think any further.
float fMaxParametricSegments180;
float fMaxParametricSegments360;
float fMaxParametricSegments180_withJoin;
float fMaxParametricSegments360_withJoin;
float fMaxCombinedSegments_withJoin;
bool fSoloRoundJoinAlwaysFitsInPatch;
// Variables related to the vertex chunk that we are currently filling.
int fCurrChunkPatchCapacity;
int fCurrChunkMinPatchAllocCount;
GrStrokeTessellateShader::Patch* fCurrChunkPatchData;
// Variables related to the specific contour that we are currently iterating.
bool fHasPreviousSegment = false;
bool fHasLastControlPoint = false;
SkDEBUGCODE(bool fHasCurrentPoint = false;)
SkPoint fCurrContourStartPoint;
SkPoint fCurrContourFirstControlPoint;
SkPoint fLastControlPoint;

View File

@ -20,28 +20,21 @@ static SkPMColor4f get_paint_constant_blended_color(const GrPaint& paint) {
}
GrStrokeTessellateOp::GrStrokeTessellateOp(GrAAType aaType, const SkMatrix& viewMatrix,
const SkPath& path, const SkStrokeRec& stroke,
const SkStrokeRec& stroke, const SkPath& path,
GrPaint&& paint)
: GrDrawOp(ClassID())
, fPathStrokes(path, stroke)
, fTotalCombinedVerbCnt(path.countVerbs())
, fAAType(aaType)
, fViewMatrix(viewMatrix)
, fMatrixScale(fViewMatrix.getMaxScale())
, fStroke(stroke)
, fColor(get_paint_constant_blended_color(paint))
, fProcessors(std::move(paint)) {
, fProcessors(std::move(paint))
, fPaths(path)
, fTotalCombinedVerbCnt(path.countVerbs()) {
SkASSERT(fAAType != GrAAType::kCoverage); // No mixed samples support yet.
SkASSERT(fMatrixScale >= 0);
if (stroke.getJoin() == SkPaint::kMiter_Join) {
float miter = stroke.getMiter();
if (miter <= 0) {
fPathStrokes.head().fStroke.setStrokeParams(stroke.getCap(), SkPaint::kBevel_Join, 0);
} else {
fMiterLimitOrZero = miter;
}
}
SkRect devBounds = fPathStrokes.head().fPath.getBounds();
float inflationRadius = fPathStrokes.head().fStroke.getInflationRadius();
SkRect devBounds = path.getBounds();
float inflationRadius = fStroke.getInflationRadius();
devBounds.outset(inflationRadius, inflationRadius);
viewMatrix.mapRect(&devBounds, devBounds);
this->setBounds(devBounds, HasAABloat(GrAAType::kCoverage == fAAType), IsHairline::kNo);
@ -71,17 +64,12 @@ GrOp::CombineResult GrStrokeTessellateOp::onCombineIfPossible(GrOp* grOp,
if (fColor != op->fColor ||
fViewMatrix != op->fViewMatrix ||
fAAType != op->fAAType ||
((fMiterLimitOrZero * op->fMiterLimitOrZero != 0) && // Are both non-zero?
fMiterLimitOrZero != op->fMiterLimitOrZero) ||
!fStroke.hasEqualEffect(op->fStroke) ||
fProcessors != op->fProcessors) {
return CombineResult::kCannotCombine;
}
fPathStrokes.concat(std::move(op->fPathStrokes), arenas->recordTimeAllocator());
if (op->fMiterLimitOrZero != 0) {
SkASSERT(fMiterLimitOrZero == 0 || fMiterLimitOrZero == op->fMiterLimitOrZero);
fMiterLimitOrZero = op->fMiterLimitOrZero;
}
fPaths.concat(std::move(op->fPaths), arenas->recordTimeAllocator());
fTotalCombinedVerbCnt += op->fTotalCombinedVerbCnt;
return CombineResult::kMerged;
@ -93,9 +81,10 @@ void GrStrokeTessellateOp::onPrePrepare(GrRecordingContext*, const GrSurfaceProx
GrXferBarrierFlags renderPassXferBarriers) {}
void GrStrokeTessellateOp::onPrepare(GrOpFlushState* flushState) {
GrStrokePatchBuilder builder(flushState, &fPatchChunks, fMatrixScale, fTotalCombinedVerbCnt);
for (auto& [path, stroke] : fPathStrokes) {
builder.addPath(path, stroke);
GrStrokePatchBuilder builder(flushState, &fPatchChunks, fMatrixScale, fStroke,
fTotalCombinedVerbCnt);
for (const SkPath& path : fPaths) {
builder.addPath(path);
}
}
@ -111,7 +100,7 @@ void GrStrokeTessellateOp::onExecute(GrOpFlushState* flushState, const SkRect& c
initArgs.fWriteSwizzle = flushState->drawOpArgs().writeSwizzle();
GrPipeline pipeline(initArgs, std::move(fProcessors), flushState->detachAppliedClip());
GrStrokeTessellateShader strokeShader(fMatrixScale, fMiterLimitOrZero, fViewMatrix, fColor);
GrStrokeTessellateShader strokeShader(fStroke, fMatrixScale, fViewMatrix, fColor);
GrPathShader::ProgramInfo programInfo(flushState->writeView(), &pipeline, &strokeShader,
flushState->renderPassBarriers());

View File

@ -26,7 +26,7 @@ private:
//
// Patches can overlap, so until a stencil technique is implemented, the provided paint must be
// a constant blended color.
GrStrokeTessellateOp(GrAAType, const SkMatrix&, const SkPath&, const SkStrokeRec&, GrPaint&&);
GrStrokeTessellateOp(GrAAType, const SkMatrix&, const SkStrokeRec&, const SkPath&, GrPaint&&);
const char* name() const override { return "GrStrokeTessellateOp"; }
void visitProxies(const VisitProxyFunc& fn) const override { fProcessors.visitProxies(fn); }
@ -40,22 +40,16 @@ private:
void onPrepare(GrOpFlushState* state) override;
void onExecute(GrOpFlushState*, const SkRect& chainBounds) override;
struct PathStroke {
PathStroke(const SkPath& path, const SkStrokeRec& stroke) : fPath(path), fStroke(stroke) {}
SkPath fPath;
SkStrokeRec fStroke;
};
GrSTArenaList<PathStroke> fPathStrokes;
int fTotalCombinedVerbCnt;
const GrAAType fAAType;
SkMatrix fViewMatrix;
float fMatrixScale;
float fMiterLimitOrZero = 0; // Zero if there is not a stroke with a miter join type.
const SkMatrix fViewMatrix;
const float fMatrixScale;
const SkStrokeRec fStroke;
SkPMColor4f fColor;
GrProcessorSet fProcessors;
GrSTArenaList<SkPath> fPaths;
int fTotalCombinedVerbCnt;
// S=1 because we will almost always fit everything into one single chunk.
SkSTArray<1, GrStrokePatchBuilder::PatchChunk> fPatchChunks;

View File

@ -34,8 +34,10 @@ private:
args.fVaryingHandler->emitAttributes(shader);
auto* uniHandler = args.fUniformHandler;
fTessControlArgsUniform = uniHandler->addUniform(nullptr, kTessControl_GrShaderFlag,
kFloat2_GrSLType, "tessControlArgs",
fTessControlArgsUniform = uniHandler->addUniform(nullptr,
kTessControl_GrShaderFlag |
kTessEvaluation_GrShaderFlag,
kFloat4_GrSLType, "tessControlArgs",
nullptr);
if (!shader.viewMatrix().isIdentity()) {
fTranslateUniform = uniHandler->addUniform(nullptr, kTessEvaluation_GrShaderFlag,
@ -59,8 +61,6 @@ private:
auto* v = args.fVertBuilder;
v->defineConstantf("float", "kParametricEpsilon", "1.0 / (%i * 128)",
args.fShaderCaps->maxTessellationSegments()); // 1/128 of a segment.
v->defineConstantf("float", "kStandardCubicType", "%.0f", Patch::kStandardCubicType);
// Declare outputs to the tessellation control shader.
v->declareGlobal(GrShaderVar("vsPts01", kFloat4_GrSLType, TypeModifier::Out));
v->declareGlobal(GrShaderVar("vsPts23", kFloat4_GrSLType, TypeModifier::Out));
v->declareGlobal(GrShaderVar("vsPts45", kFloat4_GrSLType, TypeModifier::Out));
@ -68,18 +68,28 @@ private:
v->declareGlobal(GrShaderVar("vsPts89", kFloat4_GrSLType, TypeModifier::Out));
v->declareGlobal(GrShaderVar("vsTans01", kFloat4_GrSLType, TypeModifier::Out));
v->declareGlobal(GrShaderVar("vsTans23", kFloat4_GrSLType, TypeModifier::Out));
v->declareGlobal(GrShaderVar("vsTurnDirsAndPatchType", kHalf4_GrSLType, TypeModifier::Out));
v->declareGlobal(GrShaderVar("vsStrokeRadius", kFloat_GrSLType, TypeModifier::Out));
v->declareGlobal(GrShaderVar("vsPrevJoinTangent", kFloat2_GrSLType, TypeModifier::Out));
v->codeAppendf(R"(
// Unpack the control points.
float4x2 P = float4x2(inputPts01, inputPts23);
half patchType = half(inputArgs.x);
float2 prevJoinTangent = P[0] - inputPrevCtrlPt;
// Find the beginning and ending tangents. It's imperative that we compute these tangents
// form the original input points or else the seams might crack.
// (This works for now because P0=P1=P2 and/or P1=P2=P3 are both illegal.)
float2 tan0 = (P[1] == P[0]) ? P[2] - P[0] : P[1] - P[0];
float2 tan1 = (P[3] == P[2]) ? P[3] - P[1] : P[3] - P[2];
if (tan1 == float2(0)) {
// [p0, p3, p3, p3] is a reserved pattern that means this patch is a join only.
P[1] = P[2] = P[3] = P[0]; // Colocate all the curve's points.
// This will disable the (colocated) curve sections by making their tangents equal.
tan1 = tan0;
}
if (tan0 == float2(0)) {
// [p0, p0, p0, p3] is a reserved pattern that means this patch is a cusp point.
P[3] = P[0]; // Colocate all the points on the cusp.
// This will disable the join section by making its tangents equal.
tan0 = prevJoinTangent;
}
// Find the cubic's power basis coefficient matrix "C":
//
@ -162,17 +172,11 @@ private:
// just found, then subdivide uniformly in parametric space if needed.
float2 chopT = roots;
if (chopT[0] <= kParametricEpsilon) {
if (chopT[1] <= kParametricEpsilon) {
chopT[1] = 2/3.0;
}
chopT[0] = chopT[1];
}
chopT = (chopT[1] <= kParametricEpsilon) ? float2(2/3.0) : chopT.tt;
} // chopT's are now both > 0.
if (chopT[1] >= 1 - kParametricEpsilon) {
if (chopT[0] >= 1 - kParametricEpsilon) {
chopT[0] = 1/3.0;
}
chopT[1] = chopT[0];
}
chopT = (chopT[0] >= 1 - kParametricEpsilon) ? float2(1/3.0) : chopT.ss;
} // Now, 0 < chopT's < 1.
if (chopT[0] == chopT[1]) {
// The chop points are colocated. Split the larger section in half.
if (chopT[0] > .5) {
@ -181,9 +185,10 @@ private:
chopT[1] = fma(chopT[1], .5, .5);
}
}
if (patchType != kStandardCubicType) {
// The patch is actually a flat line or join. Don't chop it into sections after all.
if (P[0] == P[1] && P[2] == P[3]) {
// The curve is either a flat line or a point. Don't chop it into sections after all.
chopT = float2(0);
midtangent = float2(0);
}
// Chop the curve at T0.
@ -194,63 +199,50 @@ private:
float2 bcd = mix(bc, cd, chopT[0]);
float2 abcd = mix(abc, bcd, chopT[0]);
// Chop the curve in reverse at T1.
float2 wz = mix(P[2], P[3], chopT[1]);
float2 zy = mix(P[1], P[2], chopT[1]);
float2 yx = mix(P[0], P[1], chopT[1]);
float2 wzy = mix(zy, wz, chopT[1]);
float2 zyx = mix(yx, zy, chopT[1]);
float2 wzyx = mix(zyx, wzy, chopT[1]);
// Chop the curve at T1.
float2 xy = mix(P[0], P[1], chopT[1]);
float2 yz = mix(P[1], P[2], chopT[1]);
float2 zw = mix(P[2], P[3], chopT[1]);
float2 xyz = mix(xy, yz, chopT[1]);
float2 yzw = mix(yz, zw, chopT[1]);
float2 xyzw = mix(xyz, yzw, chopT[1]);
// Find tangents at the chop points.
float2 innerTan0 = (chopT[0] != 0) ? bcd - abc : tan0;
float2 innerTan1 = (chopT[1] != 0) ? wzy - zyx : tan0;
float2 innerTan1 = (chopT[1] != 0) ? yzw - xyz : tan0;
// Figure out which direction the curve turns as T approaches infinity.
float inflectValAtInf = (I[0] != 0) ? I[0] : (I[1] != 0) ? I[1] : I[2];
half turnDirAtInf = (inflectValAtInf >= 0) ? +1 : -1;
half3 turnDirs; // Will hold the turn directions for each section we just chopped.
if (patchType != kStandardCubicType) {
// The patch is a flat line or join. We didn't actually subdivide it.
innerTan0 = innerTan1 = tan0;
// The input points for joins aren't actually a cubic, but the math still works out that
// we can use the inflection function to decide which direction the join turns.
turnDirs = half3(-turnDirAtInf);
} else if (midtangent != float2(0)) {
if (midtangent != float2(0)) {
// The curve did not inflect so we chopped at midtangent. The parametric definition of
// tangent can be undefined at points that divide rotation in half, so use midtangent
// instead at these locations.
//
// Start by finding which direction the curve turns. Since it does not inflect, it will
// always turn in the same direction, which is also equal to the sign of the inflection
// function as it approaches infinity.
float inflectSignAtInf = (I[0] != 0) ? I[0] : I[2];
float2x2 derivative2AtChops = transpose(float2x2(3*chopT, 1,1) * float2x2(C));
if (chopT[0] == roots[0] || chopT[0] == roots[1]) {
float2 secondDerivate = float2(3*chopT[0], 1) * float2x2(C);
float midtangentTurn = determinant(float2x2(midtangent, secondDerivate));
innerTan0 = (midtangentTurn * turnDirAtInf >= 0) ? +midtangent : -midtangent;
// Point midtangent in the same direction that the curve is turning at chopT[0].
float midtangentTurn = determinant(float2x2(midtangent, derivative2AtChops[0]));
innerTan0 = (midtangentTurn * inflectSignAtInf >= 0) ? +midtangent : -midtangent;
}
if (chopT[1] == roots[0] || chopT[1] == roots[1]) {
float2 secondDerivate = float2(3*chopT[1], 1) * float2x2(C);
float midtangentTurn = determinant(float2x2(midtangent, secondDerivate));
innerTan1 = (midtangentTurn * turnDirAtInf >= 0) ? +midtangent : -midtangent;
// Point midtangent in the same direction that the curve is turning at chopT[1].
float midtangentTurn = determinant(float2x2(midtangent, derivative2AtChops[1]));
innerTan1 = (midtangentTurn * inflectSignAtInf >= 0) ? +midtangent : -midtangent;
}
// Non-inflecting curves always turn in the same direction.
turnDirs = half3(turnDirAtInf);
} else {
// We just chopped at inflection points so it is most stable to actually evaluate the
// inflection function at the center of each section.
float3 turnT = mix(float3(0, chopT), float3(chopT, 1), .5);
turnDirs = half3(sign(float3x3(turnT*turnT, turnT, 1,1,1) * I));
}
// Package arguments for the tessellation control stage.
vsPts01 = float4(P[0], ab);
vsPts23 = float4(abc, abcd);
vsPts45 = float4(mix(abcd, bcd, (chopT[1] - chopT[0]) / (1 - chopT[0])),
mix(zyx, wzyx, chopT[0] / chopT[1]));
vsPts67 = float4(wzyx, wzy);
vsPts89 = float4(wz, P[3]);
mix(xyz, xyzw, (chopT[1] != 0) ? chopT[0] / chopT[1] : 0));
vsPts67 = float4(xyzw, yzw);
vsPts89 = float4(zw, P[3]);
vsTans01 = float4(tan0, innerTan0);
vsTans23 = float4(innerTan1, tan1);
vsTurnDirsAndPatchType = half4(turnDirs, patchType);
vsStrokeRadius = inputArgs.y;
vsPrevJoinTangent = (prevJoinTangent == float2(0)) ? tan0 : prevJoinTangent;
)");
// The fragment shader just outputs a uniform color.
@ -261,9 +253,23 @@ private:
void setData(const GrGLSLProgramDataManager& pdman,
const GrPrimitiveProcessor& primProc) override {
const auto& shader = primProc.cast<GrStrokeTessellateShader>();
// tessControlArgs.x is the tolerance in pixels.
pdman.set2f(fTessControlArgsUniform, 1 / (kLinearizationIntolerance * shader.fMatrixScale),
shader.fMiterLimit);
float numSegmentsInJoin;
switch (shader.fStroke.getJoin()) {
case SkPaint::kBevel_Join:
numSegmentsInJoin = 1;
break;
case SkPaint::kMiter_Join:
numSegmentsInJoin = (shader.fStroke.getMiter() > 0) ? 2 : 1;
break;
case SkPaint::kRound_Join:
numSegmentsInJoin = 0; // Use the rotation to calculate the number of segments.
break;
}
pdman.set4f(fTessControlArgsUniform,
shader.fStroke.getWidth() * .5, // uStrokeRadius.
numSegmentsInJoin, // uNumSegmentsInJoin
kLinearizationIntolerance * shader.fMatrixScale, // uIntolerance in path space.
1/(shader.fStroke.getMiter()*shader.fStroke.getMiter())); // uMiterLimitPowMinus2.
const SkMatrix& m = shader.viewMatrix();
if (!m.isIdentity()) {
pdman.set2f(fTranslateUniform, m.getTranslateX(), m.getTranslateY());
@ -285,20 +291,20 @@ SkString GrStrokeTessellateShader::getTessControlShaderGLSL(
auto impl = static_cast<const GrStrokeTessellateShader::Impl*>(glslPrimProc);
SkString code(versionAndExtensionDecls);
// Run 3 invocations: 1 for each section that the vertex shader chopped the curve into.
code.append("layout(vertices = 3) out;\n");
// Run 4 invocations: 1 for the previous join plus 1 for each section that the vertex shader
// chopped the curve into.
code.append("layout(vertices = 4) out;\n");
code.appendf("const float kPI = 3.141592653589793238;\n");
code.appendf("const float kMaxTessellationSegments = %i;\n",
shaderCaps.maxTessellationSegments());
code.appendf("const float kStandardCubicType = %f;\n", Patch::kStandardCubicType);
code.appendf("const float kMiterJoinType = %.0f;\n", Patch::kMiterJoinType);
code.appendf("const float kBevelJoinType = %.0f;\n", Patch::kBevelJoinType);
const char* tessControlArgsName = impl->getTessControlArgsUniformName(uniformHandler);
code.appendf("uniform vec2 %s;\n", tessControlArgsName);
code.appendf("#define uTolerance %s.x\n", tessControlArgsName);
code.appendf("#define uMiterLimit %s.y\n", tessControlArgsName);
code.appendf("uniform vec4 %s;\n", tessControlArgsName);
code.appendf("#define uStrokeRadius %s.x\n", tessControlArgsName);
code.appendf("#define uNumSegmentsInJoin %s.y\n", tessControlArgsName);
code.appendf("#define uIntolerance %s.z\n", tessControlArgsName);
code.appendf("#define uMiterLimitPowMinus2 %s.w\n", tessControlArgsName);
code.append(R"(
in vec4 vsPts01[];
@ -308,14 +314,13 @@ SkString GrStrokeTessellateShader::getTessControlShaderGLSL(
in vec4 vsPts89[];
in vec4 vsTans01[];
in vec4 vsTans23[];
in mediump vec4 vsTurnDirsAndPatchType[];
in float vsStrokeRadius[];
in vec2 vsPrevJoinTangent[];
out vec4 tcsPts01[];
out vec4 tcsPt2Tan0[];
out vec4 tcsTessArgs[];
patch out vec4 tcsEndPtEndTan;
patch out vec4 tcsStrokeArgs;
patch out vec3 tcsJoinArgs;
// 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.
@ -332,35 +337,37 @@ SkString GrStrokeTessellateShader::getTessControlShaderGLSL(
// Unpack the input arguments from the vertex shader.
mat4x2 P;
mat2 tangents;
mediump float turnDir;
if (gl_InvocationID == 0) {
// This is the join section of the patch.
P = mat4x2(vsPts01[0].xyxy, vsPts01[0].xyxy);
tangents = mat2(vsPrevJoinTangent[0], vsTans01[0].xy);
} else if (gl_InvocationID == 1) {
// This is the first curve section of the patch.
P = mat4x2(vsPts01[0], vsPts23[0]);
tangents = mat2(vsTans01[0]);
turnDir = vsTurnDirsAndPatchType[0].x;
} else if (gl_InvocationID == 1) {
} else if (gl_InvocationID == 2) {
// This is the second curve section of the patch.
P = mat4x2(vsPts23[0].zw, vsPts45[0], vsPts67[0].xy);
tangents = mat2(vsTans01[0].zw, vsTans23[0].xy);
turnDir = vsTurnDirsAndPatchType[0].y;
} else {
// This is the third curve section of the patch.
P = mat4x2(vsPts67[0], vsPts89[0]);
tangents = mat2(vsTans23[0]);
turnDir = vsTurnDirsAndPatchType[0].z;
}
mediump float patchType = vsTurnDirsAndPatchType[0].w;
float strokeRadius = vsStrokeRadius[0];
// Calculate the number of evenly spaced (in the parametric sense) segments to chop this
// section of the curve into. (See GrWangsFormula::cubic() for more documentation on this
// formula.) The final tessellated strip will be a composition of these parametric segments
// as well as radial segments.
float numParametricSegments = sqrt(
.75/uTolerance * length(max(abs(P[2] - P[1]*2.0 + P[0]),
abs(P[3] - P[2]*2.0 + P[1]))));
numParametricSegments = max(ceil(numParametricSegments), 1);
if (patchType != kStandardCubicType) {
// Joins and flat lines don't need parametric segments.
.75*uIntolerance * length(max(abs(P[2] - P[1]*2.0 + P[0]),
abs(P[3] - P[2]*2.0 + P[1]))));
if (P[0] == P[1] && P[2] == P[3]) {
// This is how the patch builder articulates lineTos but Wang's formula returns
// >>1 segment in this scenario. Assign 1 parametric segment.
numParametricSegments = 1;
}
numParametricSegments = max(ceil(numParametricSegments), 1);
// Determine the curve's start angle.
float angle0 = atan2(tangents[0]);
@ -371,43 +378,37 @@ SkString GrStrokeTessellateShader::getTessControlShaderGLSL(
vec2 tan1norm = normalize(tangents[1]);
float cosTheta = dot(tan1norm, tan0norm);
float rotation = acos(clamp(cosTheta, -1, +1));
// Adjust sign of rotation to match the direction the curve turns.
rotation *= turnDir;
// Adjust sign of rotation to match the direction the curve turns. Since at this point the
// curve cannot rotate >180 degrees, `cross(tan0, tan1)' is all we need to know.
if (determinant(tangents) < 0) {
rotation = -rotation;
}
// Calculate the number of evenly spaced radial segments to chop this section of the curve
// into. Radial segments divide the curve's rotation into even steps. The final tessellated
// strip will be a composition of both parametric and radial segments.
float numRadialSegments = abs(rotation) / (2 * acos(max(1 - uTolerance/strokeRadius, -1)));
float radialTolerance = 1 / (uStrokeRadius * uIntolerance);
float numRadialSegments = abs(rotation) / (2 * acos(max(1 - radialTolerance, -1)));
numRadialSegments = max(ceil(numRadialSegments), 1);
// Set up joins.
float innerStrokeRadius = 0; // Used for miter joins.
vec2 strokeOutsetClamp = vec2(-1, 1);
float joinType = abs(patchType);
if (joinType >= kBevelJoinType) {
innerStrokeRadius = strokeRadius; // A non-zero innerStrokeRadius designates a join.
if (joinType == kMiterJoinType) {
// Miter join. Draw a fan with 2 segments and lengthen the interior radius
// so it reaches the miter point.
// (Or draw a 1-segment fan if we exceed the miter limit.)
float miterRatio = 1.0 / cos(.5 * rotation);
numRadialSegments = (miterRatio <= uMiterLimit) ? 2.0 : 1.0;
innerStrokeRadius = strokeRadius * miterRatio;
} else if (joinType == kBevelJoinType) {
// Bevel join. Make a fan with only one segment.
numRadialSegments = 1;
if (gl_InvocationID == 0) {
// Set up joins.
numParametricSegments = 1; // Joins don't have parametric segments.
numRadialSegments = (uNumSegmentsInJoin == 0) ? numRadialSegments : uNumSegmentsInJoin;
float innerStrokeRadius = uStrokeRadius;
if (uNumSegmentsInJoin == 2) {
// Miter join: extend the middle radius to either the miter point or the bevel edge.
float x = fma(cosTheta, .5, .5);
innerStrokeRadius *= (x >= uMiterLimitPowMinus2) ? inversesqrt(x) : sqrt(x);
}
if (length(tan0norm - tan1norm) * strokeRadius < uTolerance) {
// The join angle is too tight to guarantee there won't be cracks on the interior
// side of the junction. In case our join was intended to go on the exterior side
// only, switch to a double sided bevel that ties all 4 incoming vertices together
// like a bowtie. The join angle is so tight that bevels, miters, and rounds will
// all look the same anyway.
numRadialSegments = 1;
} else if (patchType > 0) {
// This join is single-sided. Clamp it to the outer side of the junction.
vec2 strokeOutsetClamp = vec2(-1, 1);
if (distance(tan0norm, tan1norm) > radialTolerance) {
// Clamp the join to the exterior side of its junction. We only do this if the join
// angle is large enough to guarantee there won't be cracks on the interior side of
// the junction.
strokeOutsetClamp = (rotation > 0) ? vec2(-1,0) : vec2(0,1);
}
tcsJoinArgs = vec3(innerStrokeRadius, strokeOutsetClamp);
}
// The first and last edges are shared by both the parametric and radial sets of edges, so
@ -427,31 +428,45 @@ SkString GrStrokeTessellateShader::getTessControlShaderGLSL(
//
float numCombinedSegments = numParametricSegments + numRadialSegments - 1;
if (P[0] == P[3] && tangents[0] == tangents[1]) {
// The vertex shader intentionally disabled our section. Set numCombinedSegments to 0.
numCombinedSegments = 0;
}
// Pack the arguments for the evaluation stage.
tcsPts01[gl_InvocationID] = vec4(P[0], P[1]);
tcsPt2Tan0[gl_InvocationID] = vec4(P[2], tangents[0]);
tcsTessArgs[gl_InvocationID] = vec4(numCombinedSegments, numParametricSegments, angle0,
rotation / numRadialSegments);
if (gl_InvocationID == 2) {
if (gl_InvocationID == 3) {
tcsEndPtEndTan = vec4(P[3], tangents[1]);
tcsStrokeArgs = vec4(strokeRadius, innerStrokeRadius, strokeOutsetClamp);
} else if (patchType != kStandardCubicType) {
// We don't actually chop flat lines or joins. Disable their 0th & 1st curve sections by
// marking their number of segments as 0.
tcsTessArgs[gl_InvocationID].x = 0;
}
barrier();
// Tessellate a quad strip with enough segments for all 3 curve sections combined.
float numTotalCombinedSegments = tcsTessArgs[0].x + tcsTessArgs[1].x + tcsTessArgs[2].x;
numTotalCombinedSegments = min(numTotalCombinedSegments, kMaxTessellationSegments);
gl_TessLevelInner[0] = numTotalCombinedSegments;
gl_TessLevelInner[1] = 2.0;
gl_TessLevelOuter[0] = 2.0;
gl_TessLevelOuter[1] = numTotalCombinedSegments;
gl_TessLevelOuter[2] = 2.0;
gl_TessLevelOuter[3] = numTotalCombinedSegments;
if (gl_InvocationID == 0) {
// Tessellate a quad strip with enough segments for the join plus all 3 curve sections
// combined.
float numTotalCombinedSegments = tcsTessArgs[0].x + tcsTessArgs[1].x +
tcsTessArgs[2].x + tcsTessArgs[3].x;
if (tcsTessArgs[0].x != 0 && tcsTessArgs[0].x != numTotalCombinedSegments) {
// We are tessellating a quad strip with both a single-sided join and a double-sided
// stroke. Add one more edge to the join. This new edge will fall parallel with the
// first edge of the stroke, eliminating artifacts on the transition from single
// sided to double.
++tcsTessArgs[gl_InvocationID].x;
++numTotalCombinedSegments;
}
numTotalCombinedSegments = min(numTotalCombinedSegments, kMaxTessellationSegments);
gl_TessLevelInner[0] = numTotalCombinedSegments;
gl_TessLevelInner[1] = 2.0;
gl_TessLevelOuter[0] = 2.0;
gl_TessLevelOuter[1] = numTotalCombinedSegments;
gl_TessLevelOuter[2] = 2.0;
gl_TessLevelOuter[3] = numTotalCombinedSegments;
}
}
)");
@ -472,6 +487,10 @@ SkString GrStrokeTessellateShader::getTessEvaluationShaderGLSL(
code.appendf("const float kPI = 3.141592653589793238;\n");
const char* tessControlArgsName = impl->getTessControlArgsUniformName(uniformHandler);
code.appendf("uniform vec4 %s;\n", tessControlArgsName);
code.appendf("#define uStrokeRadius %s.x\n", tessControlArgsName);
if (!this->viewMatrix().isIdentity()) {
const char* translateName = impl->getTranslateUniformName(uniformHandler);
code.appendf("uniform vec2 %s;\n", translateName);
@ -486,7 +505,7 @@ SkString GrStrokeTessellateShader::getTessEvaluationShaderGLSL(
in vec4 tcsPt2Tan0[];
in vec4 tcsTessArgs[];
patch in vec4 tcsEndPtEndTan;
patch in vec4 tcsStrokeArgs;
patch in vec3 tcsJoinArgs;
uniform vec4 sk_RTAdjust;
@ -495,7 +514,8 @@ SkString GrStrokeTessellateShader::getTessEvaluationShaderGLSL(
// run orthogonal to the curve and make a strip of "numTotalCombinedSegments" quads.
// Determine which discrete edge belongs to this invocation. An edge can either come from a
// parametric segment or a radial one.
float numTotalCombinedSegments = tcsTessArgs[0].x + tcsTessArgs[1].x + tcsTessArgs[2].x;
float numTotalCombinedSegments = tcsTessArgs[0].x + tcsTessArgs[1].x + tcsTessArgs[2].x +
tcsTessArgs[3].x;
float totalEdgeID = round(gl_TessCoord.x * numTotalCombinedSegments);
// Furthermore, the vertex shader may have chopped the curve into 3 different sections.
@ -504,20 +524,31 @@ SkString GrStrokeTessellateShader::getTessEvaluationShaderGLSL(
mat4x2 P;
vec2 tan0;
vec3 tessellationArgs;
if (localEdgeID >= tcsTessArgs[0].x + tcsTessArgs[1].x) {
localEdgeID -= tcsTessArgs[0].x + tcsTessArgs[1].x;
P = mat4x2(tcsPts01[2], tcsPt2Tan0[2].xy, tcsEndPtEndTan.xy);
tan0 = tcsPt2Tan0[2].zw;
tessellationArgs = tcsTessArgs[2].yzw;
} else if (localEdgeID >= tcsTessArgs[0].x) {
localEdgeID -= tcsTessArgs[0].x;
P = mat4x2(tcsPts01[1], tcsPt2Tan0[1].xy, tcsPts01[2].xy);
tan0 = tcsPt2Tan0[1].zw;
tessellationArgs = tcsTessArgs[1].yzw;
} else {
float strokeRadius = uStrokeRadius;
vec2 strokeOutsetClamp = vec2(-1, 1);
if (localEdgeID < tcsTessArgs[0].x || tcsTessArgs[0].x == numTotalCombinedSegments) {
// Our edge belongs to the join preceding the curve.
P = mat4x2(tcsPts01[0], tcsPt2Tan0[0].xy, tcsPts01[1].xy);
tan0 = tcsPt2Tan0[0].zw;
tessellationArgs = tcsTessArgs[0].yzw;
strokeRadius = (localEdgeID == 1) ? tcsJoinArgs.x : strokeRadius;
strokeOutsetClamp = tcsJoinArgs.yz;
} else if ((localEdgeID -= tcsTessArgs[0].x) < tcsTessArgs[1].x) {
// Our edge belongs to the first curve section.
P = mat4x2(tcsPts01[1], tcsPt2Tan0[1].xy, tcsPts01[2].xy);
tan0 = tcsPt2Tan0[1].zw;
tessellationArgs = tcsTessArgs[1].yzw;
} else if ((localEdgeID -= tcsTessArgs[1].x) < tcsTessArgs[2].x) {
// Our edge belongs to the second curve section.
P = mat4x2(tcsPts01[2], tcsPt2Tan0[2].xy, tcsPts01[3].xy);
tan0 = tcsPt2Tan0[2].zw;
tessellationArgs = tcsTessArgs[2].yzw;
} else {
// Our edge belongs to the third curve section.
localEdgeID -= tcsTessArgs[2].x;
P = mat4x2(tcsPts01[3], tcsPt2Tan0[3].xy, tcsEndPtEndTan.xy);
tan0 = tcsPt2Tan0[3].zw;
tessellationArgs = tcsTessArgs[3].yzw;
}
float numParametricSegments = tessellationArgs.x;
float angle0 = tessellationArgs.y;
@ -540,7 +571,7 @@ SkString GrStrokeTessellateShader::getTessEvaluationShaderGLSL(
// | 1|
//
mat3x2 C_s = mat3x2(C_[0], C_[1] * numParametricSegments,
C_[2] * numParametricSegments * numParametricSegments);
C_[2] * (numParametricSegments * numParametricSegments));
// Run an O(log N) search to determine the highest parametric edge that is located on or
// before the localEdgeID. A local edge ID is determined by the sum of complete parametric
@ -636,31 +667,19 @@ SkString GrStrokeTessellateShader::getTessEvaluationShaderGLSL(
tangent = bcd - abc;
}
float strokeRadius = tcsStrokeArgs.x;
float innerStrokeRadius = tcsStrokeArgs.y;
vec2 strokeOutsetClamp = tcsStrokeArgs.zw;
if (localEdgeID == 0) {
// The first local edge of each section uses the provided P[0] and tan0. This ensures
// continuous rotation across chops made by the vertex shader as well as crack-free
// seaming between patches.
position = P[0];
tangent = tan0;
} else if (gl_TessCoord.x == 1) {
}
if (gl_TessCoord.x == 1) {
// The final edge of the quad strip always uses the provided endPt and endTan. This
// ensures crack-free seaming between patches.
position = tcsEndPtEndTan.xy;
tangent = tcsEndPtEndTan.zw;
} else if (innerStrokeRadius != 0) {
// Miter joins use a larger radius for the internal vertex in order to reach the miter
// point.
strokeRadius = innerStrokeRadius;
}
// If innerStrokeRadius != 0 then this patch is a join.
if (innerStrokeRadius != 0) {
// ... Aaaand nevermind again if we are a join. Those all rotate around P[1].
position = P[1];
}
// Determine how far to outset our vertex orthogonally from the curve.

View File

@ -10,6 +10,7 @@
#include "src/gpu/tessellate/GrPathShader.h"
#include "include/core/SkStrokeRec.h"
#include "src/gpu/tessellate/GrTessellationPathRenderer.h"
#include <array>
@ -39,31 +40,20 @@ class GrGLSLUniformHandler;
class GrStrokeTessellateShader : public GrPathShader {
public:
// The vertex array bound for this shader should contain a vector of Patch structs. A Patch is
// either a "cubic" (single stroked bezier curve with butt caps) or a "join". A set of
// coincident cubic patches with join patches in between will render a complete stroke.
// a join followed by a cubic stroke.
struct Patch {
// A value of 0 in fPatchType means this patch is a normal stroked cubic.
constexpr static float kStandardCubicType = 0;
// A value of 1 in fPatchType means this patch is a flat line.
constexpr static float kFlatLineType = 1;
// An absolute value >=2 in fPatchType means that this patch is a join. A positive value
// means the join geometry should only go on the outer side of the junction point (spec
// behavior for standard joins), and a negative value means the join geometry should be
// double-sided.
// A join calculates its starting angle using fPrevControlPoint.
SkPoint fPrevControlPoint;
// fPts define the cubic stroke as well as the ending angle of the previous join.
//
// If a patch is a join, fPts[0] must equal the control point coming into the junction,
// fPts[1] and fPts[2] must both equal the junction point, and fPts[3] must equal the
// control point going out. It's imperative for a join's control points match the control
// points of their adjoining cubics exactly or else the seams might crack.
constexpr static float kBevelJoinType = 2;
constexpr static float kMiterJoinType = 3;
constexpr static float kRoundJoinType = 4;
// If fPts[0] == fPrevControlPoint, then no join is emitted.
//
// fPts=[p0, p3, p3, p3] is a reserved pattern that means this patch is a join only, whose
// start and end tangents are (fPts[0] - fPrevControlPoint) and (fPts[3] - fPts[0]).
//
// fPts=[p0, p0, p0, p3] is a reserved pattern that means this patch is a cusp point
// anchored on p0 and rotating from (fPts[0] - fPrevControlPoint) to (fPts[3] - fPts[0]).
std::array<SkPoint, 4> fPts;
float fPatchType;
float fStrokeRadius;
};
// 'matrixScale' is used to set up an appropriate number of tessellation triangles. It should be
@ -73,18 +63,20 @@ public:
// be miter joins.
//
// 'viewMatrix' is applied to the geometry post tessellation. It cannot have perspective.
GrStrokeTessellateShader(float matrixScale, float miterLimit, const SkMatrix& viewMatrix,
SkPMColor4f color)
GrStrokeTessellateShader(const SkStrokeRec& stroke, float matrixScale,
const SkMatrix& viewMatrix, SkPMColor4f color)
: GrPathShader(kTessellate_GrStrokeTessellateShader_ClassID, viewMatrix,
GrPrimitiveType::kPatches, 1)
, fStroke(stroke)
, fMatrixScale(matrixScale)
, fMiterLimit(miterLimit)
, 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},
{"inputArgs", kFloat2_GrVertexAttribType, kFloat2_GrSLType}};
{"inputPts23", kFloat4_GrVertexAttribType, kFloat4_GrSLType}};
this->setVertexAttributes(kInputPointAttribs, SK_ARRAY_COUNT(kInputPointAttribs));
SkASSERT(this->vertexStride() == sizeof(Patch));
}
private:
@ -103,8 +95,8 @@ private:
const GrGLSLUniformHandler&,
const GrShaderCaps&) const override;
const SkStrokeRec fStroke;
const float fMatrixScale;
const float fMiterLimit;
const SkPMColor4f fColor;
class Impl;

View File

@ -252,8 +252,8 @@ bool GrTessellationPathRenderer::onDrawPath(const DrawPathArgs& args) {
path.transform(*args.fViewMatrix, &devPath);
SkStrokeRec devStroke = args.fShape->style().strokeRec();
devStroke.setStrokeStyle(1);
auto op = pool->allocate<GrStrokeTessellateOp>(args.fAAType, SkMatrix::I(), devPath,
devStroke, std::move(args.fPaint));
auto op = pool->allocate<GrStrokeTessellateOp>(args.fAAType, SkMatrix::I(), devStroke,
devPath, std::move(args.fPaint));
renderTargetContext->addDrawOp(args.fClip, std::move(op));
return true;
}
@ -261,8 +261,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 = pool->allocate<GrStrokeTessellateOp>(args.fAAType, *args.fViewMatrix, path,
stroke, std::move(args.fPaint));
auto op = pool->allocate<GrStrokeTessellateOp>(args.fAAType, *args.fViewMatrix, stroke,
path, std::move(args.fPaint));
renderTargetContext->addDrawOp(args.fClip, std::move(op));
return true;
}