blob: f23a55ad15bf7cb0ab86180bec67ac221da21f1f [file] [log] [blame]
/*
* Copyright 2018 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/GrStrokePatchBuilder.h"
#include "include/core/SkStrokeRec.h"
#include "include/private/SkFloatingPoint.h"
#include "include/private/SkNx.h"
#include "src/core/SkGeometry.h"
#include "src/core/SkMathPriv.h"
#include "src/core/SkPathPriv.h"
#include "src/gpu/tessellate/GrStrokeTessellateShader.h"
#include "src/gpu/tessellate/GrVectorXform.h"
#include "src/gpu/tessellate/GrWangsFormula.h"
using Patch = GrStrokeTessellateShader::Patch;
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;
}
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 std::max(numCombinedSegments + 1 - numRadialSegments, 0.f);
}
static float pow4(float x) {
float xx = x*x;
return xx*xx;
}
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);
float maxParametricSegments180 = num_parametric_segments(fMaxTessellationSegments,
numRadialSegments180);
fMaxParametricSegments180_pow4 = pow4(maxParametricSegments180);
float numRadialSegments360 = std::max(std::ceil(
2*SK_ScalarPI * fNumRadialSegmentsPerRadian), 1.f);
float maxParametricSegments360 = num_parametric_segments(fMaxTessellationSegments,
numRadialSegments360);
fMaxParametricSegments360_pow4 = pow4(maxParametricSegments360);
// 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_pow4_withJoin = pow4(std::max(
maxParametricSegments180 - maxNumSegmentsInJoin - 1, 0.f));
fMaxParametricSegments360_pow4_withJoin = pow4(std::max(
maxParametricSegments360 - maxNumSegmentsInJoin - 1, 0.f));
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_pow4 = GrWangsFormula::quadratic_pow4(fLinearizationIntolerance, p);
if (numParametricSegments_pow4 <= fMaxParametricSegments180_pow4_withJoin &&
prevJoinType != JoinType::kCusp) {
this->cubicToRaw(prevJoinType, asCubic);
return;
}
if (numParametricSegments_pow4 <= fMaxParametricSegments180_pow4 || maxDepth == 0) {
if (numParametricSegments_pow4 > fMaxParametricSegments180_pow4_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);
float numParametricSegments = GrWangsFormula::root4(numParametricSegments_pow4);
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_pow4 = GrWangsFormula::cubic_pow4(fLinearizationIntolerance, p);
if (numParametricSegments_pow4 <= fMaxParametricSegments360_pow4_withJoin &&
prevJoinType != JoinType::kCusp) {
this->cubicToRaw(prevJoinType, p);
return;
}
float maxParametricSegments_pow4 = (convex180Status == Convex180Status::kYes) ?
fMaxParametricSegments180_pow4 : fMaxParametricSegments360_pow4;
if (numParametricSegments_pow4 <= maxParametricSegments_pow4 || maxDepth == 0) {
float maxParametricSegments_pow4_withJoin = (convex180Status == Convex180Status::kYes) ?
fMaxParametricSegments180_pow4_withJoin : fMaxParametricSegments360_pow4_withJoin;
if (numParametricSegments_pow4 > maxParametricSegments_pow4_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);
float numParametricSegments = GrWangsFormula::root4(numParametricSegments_pow4);
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() {
if (fPatchChunkArray->back().fPatchCount >= fCurrChunkPatchCapacity) {
// The current chunk is full. Time to allocate a new one. (And no need to put back vertices;
// the buffer is full.)
this->allocPatchChunkAtLeast(fCurrChunkMinPatchAllocCount * 2);
}
if (!fCurrChunkPatchData) {
SkDebugf("WARNING: Failed to allocate vertex buffer for tessellated stroke.");
return nullptr;
}
SkASSERT(fPatchChunkArray->back().fPatchCount <= fCurrChunkPatchCapacity);
Patch* patch = fCurrChunkPatchData + fPatchChunkArray->back().fPatchCount;
++fPatchChunkArray->back().fPatchCount;
return patch;
}
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;
}