Extract stateful parts of GrStrokeHardwareTessellator to helper class

Bug: chromium:1172543
Change-Id: I67107cba00c525cdf519a6008b02299339dd05e6
Reviewed-on: https://skia-review.googlesource.com/c/skia/+/372289
Commit-Queue: Chris Dalton <csmartdalton@google.com>
Reviewed-by: John Stiles <johnstiles@google.com>
diff --git a/src/gpu/tessellate/GrStrokeHardwareTessellator.cpp b/src/gpu/tessellate/GrStrokeHardwareTessellator.cpp
index 1f177b1..619de5b 100644
--- a/src/gpu/tessellate/GrStrokeHardwareTessellator.cpp
+++ b/src/gpu/tessellate/GrStrokeHardwareTessellator.cpp
@@ -12,6 +12,10 @@
 #include "src/gpu/geometry/GrPathUtils.h"
 #include "src/gpu/tessellate/GrWangsFormula.h"
 
+using Tolerances = GrStrokeTessellateShader::Tolerances;
+
+namespace {
+
 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:
@@ -42,48 +46,633 @@
     return xx*xx;
 }
 
-void GrStrokeHardwareTessellator::updateTolerances(Tolerances tolerances, SkPaint::Join joinType) {
-    // 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 * tolerances.fNumRadialSegmentsPerRadian), 1.f);
-    float maxParametricSegments180 = num_parametric_segments(fMaxTessellationSegments,
-                                                             numRadialSegments180);
-    fMaxParametricSegments180_pow4 = pow4(maxParametricSegments180);
+class PatchWriter {
+public:
+    using ShaderFlags = GrStrokeTessellator::ShaderFlags;
+    using PatchChunk = GrStrokeHardwareTessellator::PatchChunk;
 
-    float numRadialSegments360 = std::max(std::ceil(
-            2*SK_ScalarPI * tolerances.fNumRadialSegmentsPerRadian), 1.f);
-    float maxParametricSegments360 = num_parametric_segments(fMaxTessellationSegments,
-                                                             numRadialSegments360);
-    fMaxParametricSegments360_pow4 = pow4(maxParametricSegments360);
+    enum class JoinType {
+        kMiter = SkPaint::kMiter_Join,
+        kRound = SkPaint::kRound_Join,
+        kBevel = SkPaint::kBevel_Join,
+        kBowtie = SkPaint::kLast_Join + 1,  // Double sided round join.
+        kNone
+    };
 
-    // 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 (joinType) {
-        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;
+    PatchWriter(ShaderFlags shaderFlags, GrMeshDrawOp::Target* target,
+                SkTArray<PatchChunk>* patchChunks, int totalCombinedVerbCnt)
+            : fShaderFlags(shaderFlags)
+            , fTarget(target)
+            , fPatchChunks(patchChunks)
+            , fPatchStride(GrStrokeTessellateShader::PatchStride(fShaderFlags))
+            // Subtract 2 because the tessellation shader chops every cubic at two locations, and
+            // each chop has the potential to introduce an extra segment.
+            , fMaxTessellationSegments(target->caps().shaderCaps()->maxTessellationSegments() - 2) {
+        // 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);
     }
-    // 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);
-    fTolerances = tolerances;
-}
+
+    ~PatchWriter() {
+        fTarget->putBackVertices(fCurrChunkPatchCapacity - fPatchChunks->back().fPatchCount,
+                                 fPatchStride);
+    }
+
+    void updateTolerances(Tolerances tolerances, SkPaint::Join joinType) {
+        // 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 * tolerances.fNumRadialSegmentsPerRadian), 1.f);
+        float maxParametricSegments180 = num_parametric_segments(fMaxTessellationSegments,
+                                                                 numRadialSegments180);
+        fMaxParametricSegments180_pow4 = pow4(maxParametricSegments180);
+
+        float numRadialSegments360 = std::max(std::ceil(
+                2*SK_ScalarPI * tolerances.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 (joinType) {
+            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);
+        fTolerances = tolerances;
+    }
+
+    void updateDynamicStroke(const SkStrokeRec& stroke) {
+        SkASSERT(fShaderFlags & ShaderFlags::kDynamicStroke);
+        fDynamicStroke.set(stroke);
+    }
+
+    void updateDynamicColor(const SkPMColor4f& color) {
+        SkASSERT(fShaderFlags & ShaderFlags::kDynamicColor);
+        bool wideColor = fShaderFlags & ShaderFlags::kWideColor;
+        SkASSERT(wideColor || color.fitsInBytes());
+        fDynamicColor.set(color, wideColor);
+    }
+
+    void moveTo(SkPoint pt) {
+        fCurrContourStartPoint = pt;
+        fHasLastControlPoint = false;
+    }
+
+    void lineTo(JoinType prevJoinType, SkPoint p0, SkPoint p1) {
+        // Zero-length paths need special treatment because they are spec'd to behave differently.
+        if (p0 == p1) {
+            return;
+        }
+
+        if (fMaxCombinedSegments_withJoin < 1) {
+            // The stroke has extremely thick round joins and there aren't enough guaranteed
+            // segments to always combine a join with a line patch. Emit the join in its own
+            // separate patch.
+            this->joinTo(prevJoinType, p0, p1);
+            prevJoinType = JoinType::kNone;
+        }
+
+        SkPoint asPatch[4] = {p0, p0, p1, p1};
+        this->emitPatch(prevJoinType, asPatch, p1);
+    }
+
+    void conicTo(JoinType prevJoinType, const SkPoint p[3], float w, int maxDepth = -1) {
+        // 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] || w == 0) {
+            this->lineTo(prevJoinType, p[0], p[2]);
+            return;
+        }
+
+        // Convert to a patch.
+        SkPoint asPatch[4];
+        if (w == 1) {
+            GrPathUtils::convertQuadToCubic(p, asPatch);
+        } else {
+            GrPathShader::WriteConicPatch(p, w, asPatch);
+        }
+
+        // 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(fTolerances.fParametricIntolerance, p);
+        if (numParametricSegments_pow4 <= fMaxParametricSegments180_pow4_withJoin) {
+            this->emitPatch(prevJoinType, asPatch, p[2]);
+            return;
+        }
+
+        if (numParametricSegments_pow4 <= fMaxParametricSegments180_pow4 || maxDepth == 0) {
+            if (numParametricSegments_pow4 > fMaxParametricSegments180_pow4_withJoin) {
+                // There aren't enough guaranteed segments to include the join. Emit a standalone
+                // patch for the join.
+                this->joinTo(prevJoinType, asPatch);
+                prevJoinType = JoinType::kNone;
+            }
+            this->emitPatch(prevJoinType, asPatch, p[2]);
+            return;
+        }
+
+        // We still might have enough tessellation segments to render the curve. Check again with
+        // the actual rotation.
+        float numRadialSegments =
+                SkMeasureQuadRotation(p) * fTolerances.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 (w == 1) {
+                SkPoint chops[5];
+                if (numParametricSegments >= numRadialSegments) {
+                    SkChopQuadAtHalf(p, chops);
+                } else {
+                    SkChopQuadAtMidTangent(p, chops);
+                }
+                this->conicTo(prevJoinType, chops, 1, maxDepth - 1);
+                this->conicTo(JoinType::kBowtie, chops + 2, 1, maxDepth - 1);
+            } else {
+                SkConic conic(p, w);
+                float chopT = (numParametricSegments >= numRadialSegments) ? .5f
+                                                                           : conic.findMidTangent();
+                SkConic chops[2];
+                if (conic.chopAt(chopT, chops)) {
+                    this->conicTo(prevJoinType, chops[0].fPts, chops[0].fW, maxDepth - 1);
+                    this->conicTo(JoinType::kBowtie, chops[1].fPts, chops[1].fW, maxDepth - 1);
+                }
+            }
+            return;
+        }
+
+        if (numCombinedSegments > fMaxCombinedSegments_withJoin) {
+            // There aren't enough guaranteed segments to include the join. Emit a standalone patch
+            // for the join.
+            this->joinTo(prevJoinType, asPatch);
+            prevJoinType = JoinType::kNone;
+        }
+        this->emitPatch(prevJoinType, asPatch, p[2]);
+    }
+
+    // Is a cubic curve convex, and does it rotate no more than 180 degrees?
+    enum class Convex180Status : bool {
+        kUnknown,
+        kYes
+    };
+
+    void cubicTo(JoinType prevJoinType, const SkPoint p[4],
+                 Convex180Status convex180Status = Convex180Status::kUnknown, int maxDepth = -1) {
+        // 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(prevJoinType, p[0], p[3]);
+            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(fTolerances.fParametricIntolerance, p);
+        if (numParametricSegments_pow4 <= fMaxParametricSegments360_pow4_withJoin) {
+            this->emitPatch(prevJoinType, p, p[3]);
+            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) {
+                // There aren't enough guaranteed segments to include the join. Emit a standalone
+                // patch for the join.
+                this->joinTo(prevJoinType, p);
+                prevJoinType = JoinType::kNone;
+            }
+            this->emitPatch(prevJoinType, p, p[3]);
+            return;
+        }
+
+        // Ensure the curve does not inflect or rotate >180 degrees before we start subdividing and
+        // measuring rotation.
+        if (convex180Status == Convex180Status::kUnknown) {
+            this->cubicConvex180SegmentsTo(prevJoinType, p);
+            return;
+        }
+
+        // We still might have enough tessellation segments to render the curve. Check again with
+        // its actual rotation.
+        float numRadialSegments =
+                SkMeasureNonInflectCubicRotation(p) * fTolerances.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.
+            SkPoint chops[7];
+            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);
+            }
+            this->cubicTo(prevJoinType, chops, Convex180Status::kYes, maxDepth - 1);
+            this->cubicTo(JoinType::kBowtie, chops + 3, Convex180Status::kYes, maxDepth - 1);
+            return;
+        }
+
+        if (numCombinedSegments > fMaxCombinedSegments_withJoin) {
+            // There aren't enough guaranteed segments to include the join. Emit a standalone patch
+            // for the join.
+            this->joinTo(prevJoinType, p);
+            prevJoinType = JoinType::kNone;
+        }
+        this->emitPatch(prevJoinType, p, p[3]);
+    }
+
+    void cubicConvex180SegmentsTo(JoinType prevJoinType, const SkPoint p[4]) {
+        SkPoint chops[10];
+        float chopT[2];
+        bool areCusps = false;
+        int numChops = GrPathUtils::findCubicConvex180Chops(p, chopT, &areCusps);
+        if (numChops == 0) {
+            // The curve is already convex and rotates no more than 180 degrees.
+            this->cubicTo(prevJoinType, p, Convex180Status::kYes);
+        } else if (numChops == 1) {
+            SkChopCubicAt(p, chops, chopT[0]);
+            if (areCusps) {
+                // When chopping on a perfect cusp, these 3 points will be equal.
+                chops[2] = chops[4] = chops[3];
+            }
+            this->cubicTo(prevJoinType, chops, Convex180Status::kYes);
+            this->cubicTo(JoinType::kBowtie, chops + 3, Convex180Status::kYes);
+        } else {
+            SkASSERT(numChops == 2);
+            SkChopCubicAt(p, chops, chopT[0], chopT[1]);
+            // Two cusps are only possible on a flat line with two 180-degree turnarounds.
+            if (areCusps) {
+                this->lineTo(prevJoinType, chops[0], chops[3]);
+                this->lineTo(JoinType::kBowtie, chops[3], chops[6]);
+                this->lineTo(JoinType::kBowtie, chops[6], chops[9]);
+                return;
+            }
+            this->cubicTo(prevJoinType, chops, Convex180Status::kYes);
+            this->cubicTo(JoinType::kBowtie, chops + 3, Convex180Status::kYes);
+            this->cubicTo(JoinType::kBowtie, chops + 6, Convex180Status::kYes);
+        }
+    }
+
+    void joinTo(JoinType joinType, const SkPoint nextCubic[]) {
+        const SkPoint& nextCtrlPt = (nextCubic[1] == nextCubic[0]) ? nextCubic[2] : nextCubic[1];
+        // The caller should have culled out curves where p0==p1==p2 by this point.
+        SkASSERT(nextCtrlPt != nextCubic[0]);
+        this->joinTo(joinType, nextCubic[0], nextCtrlPt);
+    }
+
+    void joinTo(JoinType joinType, SkPoint junctionPoint, SkPoint nextControlPoint,
+                int maxDepth = -1) {
+        if (!fHasLastControlPoint) {
+            // The first stroke doesn't have a previous join.
+            return;
+        }
+
+        if (!fSoloRoundJoinAlwaysFitsInPatch && maxDepth != 0 &&
+            (joinType == JoinType::kRound || joinType == JoinType::kBowtie)) {
+            SkVector tan0 = junctionPoint - fLastControlPoint;
+            SkVector tan1 = nextControlPoint - junctionPoint;
+            float rotation = SkMeasureAngleBetweenVectors(tan0, tan1);
+            float numRadialSegments = rotation * fTolerances.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(skia:11347): This hack ensures "c0 - junctionPoint" gives the exact same
+                // ieee fp32 vector as "-(c1 - junctionPoint)". Tessellated stroking is becoming
+                // less experimental, so t's time to think of a cleaner method to avoid T-junctions
+                // when we chop joins.
+                int maxAttempts = 10;
+                do {
+                    bisector = (junctionPoint + bisector) - (junctionPoint - bisector);
+                    c0 = junctionPoint + bisector;
+                    c1 = junctionPoint - bisector;
+                } while (c0 - junctionPoint != -(c1 - junctionPoint) && --maxAttempts);
+                this->joinTo(joinType, junctionPoint, c0, maxDepth - 1);  // First join half.
+                fLastControlPoint = c1;
+                // Second join half.
+                this->joinTo(joinType, junctionPoint, nextControlPoint, maxDepth - 1);
+                return;
+            }
+        }
+
+        this->emitJoinPatch(joinType, junctionPoint, nextControlPoint);
+    }
+
+    void close(SkPoint contourEndpoint, const SkMatrix& viewMatrix, const SkStrokeRec& stroke) {
+        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(contourEndpoint, viewMatrix, stroke);
+            return;
+        }
+
+        // Draw a line back to the beginning. (This will be discarded if
+        // contourEndpoint == fCurrContourStartPoint.)
+        auto strokeJoinType = JoinType(stroke.getJoin());
+        this->lineTo(strokeJoinType, contourEndpoint, fCurrContourStartPoint);
+        this->joinTo(strokeJoinType, fCurrContourStartPoint, fCurrContourFirstControlPoint);
+
+        fHasLastControlPoint = false;
+    }
+
+    void cap(SkPoint contourEndpoint, const SkMatrix& viewMatrix, const SkStrokeRec& stroke) {
+        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.
+            SkVector outset;
+            if (!stroke.isHairlineStyle()) {
+                outset = {1, 0};
+            } else {
+                // If the stroke is hairline, orient the square on the post-transform x-axis
+                // instead. We don't need to worry about the vector length since it will be
+                // normalized later. Since the matrix cannot have perspective, the below is
+                // equivalent to:
+                //
+                //    outset = inverse(|a b|) * |1| * arbitrary_scale
+                //                     |c d|    |0|
+                //
+                //    == 1/det * | d -b| * |1| * arbitrary_scale
+                //               |-c  a|   |0|
+                //
+                //    == 1/det * | d| * arbitrary_scale
+                //               |-c|
+                //
+                //    == | d|
+                //       |-c|
+                //
+                SkASSERT(!viewMatrix.hasPerspective());
+                float c=viewMatrix.getSkewY(), d=viewMatrix.getScaleY();
+                outset = {d, -c};
+            }
+            fCurrContourFirstControlPoint = fCurrContourStartPoint - outset;
+            fLastControlPoint = fCurrContourStartPoint + outset;
+            fHasLastControlPoint = true;
+            contourEndpoint = fCurrContourStartPoint;
+        }
+
+        switch (stroke.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 bowtie.
+                JoinType roundCapJoinType = (stroke.getJoin() == SkPaint::kRound_Join)
+                        ? JoinType::kRound : JoinType::kBowtie;
+                this->joinTo(roundCapJoinType, contourEndpoint, fLastControlPoint);
+                this->moveTo(fCurrContourStartPoint, fCurrContourFirstControlPoint);
+                this->joinTo(roundCapJoinType, fCurrContourStartPoint,
+                             fCurrContourFirstControlPoint);
+                break;
+            }
+            case SkPaint::kSquare_Cap: {
+                // A square cap is the same as appending lineTos.
+                auto strokeJoinType = JoinType(stroke.getJoin());
+                SkVector lastTangent = contourEndpoint - fLastControlPoint;
+                if (!stroke.isHairlineStyle()) {
+                    // Extend the cap by 1/2 stroke width.
+                    lastTangent *= (.5f * stroke.getWidth()) / lastTangent.length();
+                } else {
+                    // Extend the cap by what will be 1/2 pixel after transformation.
+                    lastTangent *=
+                            .5f / viewMatrix.mapVector(lastTangent.fX, lastTangent.fY).length();
+                }
+                this->lineTo(strokeJoinType, contourEndpoint, contourEndpoint + lastTangent);
+                this->moveTo(fCurrContourStartPoint, fCurrContourFirstControlPoint);
+                SkVector firstTangent = fCurrContourFirstControlPoint - fCurrContourStartPoint;
+                if (!stroke.isHairlineStyle()) {
+                    // Set the the cap back by 1/2 stroke width.
+                    firstTangent *= (-.5f * stroke.getWidth()) / firstTangent.length();
+                } else {
+                    // Set the cap back by what will be 1/2 pixel after transformation.
+                    firstTangent *=
+                            -.5f / viewMatrix.mapVector(firstTangent.fX, firstTangent.fY).length();
+                }
+                this->lineTo(strokeJoinType, fCurrContourStartPoint,
+                             fCurrContourStartPoint + firstTangent);
+                break;
+            }
+        }
+
+        fHasLastControlPoint = false;
+    }
+
+private:
+    void moveTo(SkPoint pt, SkPoint lastControlPoint) {
+        fCurrContourStartPoint = pt;
+        fCurrContourFirstControlPoint = fLastControlPoint = lastControlPoint;
+        fHasLastControlPoint = true;
+    }
+
+    void emitPatch(JoinType prevJoinType, const SkPoint p[4], SkPoint endPt) {
+        SkPoint c1 = (p[1] == p[0]) ? p[2] : p[1];
+        SkPoint c2 = (p[2] == endPt) ? p[1] : p[2];
+
+        if (prevJoinType == JoinType::kBowtie) {
+            // Bowties need to go in their own patch if they will have >1 segment. TODO: Investigate
+            // if an optimization like "x < fCosRadiansPerSegment" would be worth it.
+            float rotation = SkMeasureAngleBetweenVectors(p[0] - fLastControlPoint, c1 - p[0]);
+            if (rotation * fTolerances.fNumRadialSegmentsPerRadian > 1) {
+                this->joinTo(prevJoinType, p[0], c1);
+                prevJoinType = JoinType::kNone;
+            }
+        }
+
+        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 (this->reservePatch()) {
+            // Disable the join section of this patch if prevJoinType is kNone by setting the
+            // previous control point equal to p0.
+            fPatchWriter.write((prevJoinType == JoinType::kNone) ? p[0] : fLastControlPoint);
+            fPatchWriter.writeArray(p, 4);
+            this->emitDynamicAttribs();
+        }
+
+        fLastControlPoint = c2;
+    }
+
+    void emitJoinPatch(JoinType joinType, SkPoint junctionPoint, SkPoint nextControlPoint) {
+        // We should never write out joins before the first curve.
+        SkASSERT(fHasLastControlPoint);
+
+        if (this->reservePatch()) {
+            fPatchWriter.write(fLastControlPoint, junctionPoint);
+            if (joinType == JoinType::kBowtie) {
+                // {prevControlPoint, [p0, p0, p0, p3]} is a reserved patch pattern that means this
+                // patch is a bowtie. The bowtie is anchored on p0 and its tangent angles go from
+                // (p0 - prevControlPoint) to (p3 - p0).
+                fPatchWriter.write(junctionPoint, junctionPoint);
+            } else {
+                SkASSERT(joinType != JoinType::kNone);
+                // {prevControlPoint, [p0, p3, p3, p3]} is a reserved patch pattern that means this
+                // patch is a join only (no curve sections in the patch). The join is anchored on p0 and
+                // its tangent angles go from (p0 - prevControlPoint) to (p3 - p0).
+                fPatchWriter.write(nextControlPoint, nextControlPoint);
+            }
+            fPatchWriter.write(nextControlPoint);
+            this->emitDynamicAttribs();
+        }
+
+        fLastControlPoint = nextControlPoint;
+    }
+
+    void emitDynamicAttribs() {
+        if (fShaderFlags & ShaderFlags::kDynamicStroke) {
+            fPatchWriter.write(fDynamicStroke);
+        }
+        if (fShaderFlags & ShaderFlags::kDynamicColor) {
+            fPatchWriter.write(fDynamicColor);
+        }
+    }
+
+    bool reservePatch() {
+        if (fPatchChunks->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 (!fPatchWriter.isValid()) {
+            SkDebugf("WARNING: Failed to allocate vertex buffer for tessellated stroke.");
+            return false;
+        }
+        SkASSERT(fPatchChunks->back().fPatchCount <= fCurrChunkPatchCapacity);
+        ++fPatchChunks->back().fPatchCount;
+        return true;
+    }
+
+    void allocPatchChunkAtLeast(int minPatchAllocCount) {
+        SkASSERT(fTarget);
+        PatchChunk* chunk = &fPatchChunks->push_back();
+        fPatchWriter = {fTarget->makeVertexSpaceAtLeast(fPatchStride, minPatchAllocCount,
+                                                        minPatchAllocCount, &chunk->fPatchBuffer,
+                                                        &chunk->fBasePatch,
+                                                        &fCurrChunkPatchCapacity)};
+        fCurrChunkMinPatchAllocCount = minPatchAllocCount;
+    }
+
+    const ShaderFlags fShaderFlags;
+    GrMeshDrawOp::Target* const fTarget;
+    SkTArray<PatchChunk>* const fPatchChunks;
+
+    // Size in bytes of a tessellation patch with our shader flags.
+    const size_t fPatchStride;
+
+    // The maximum number of tessellation segments the hardware can emit for a single patch.
+    const int fMaxTessellationSegments;
+
+    // These values contain worst-case numbers of parametric segments, raised to the 4th power, that
+    // our hardware can support for the current stroke radius. They assume curve rotations of 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. We raise to the 4th power because
+    // the "pow4" variants of Wang's formula are the quickest to evaluate.
+    GrStrokeTessellateShader::Tolerances fTolerances;
+    float fMaxParametricSegments180_pow4;
+    float fMaxParametricSegments360_pow4;
+    float fMaxParametricSegments180_pow4_withJoin;
+    float fMaxParametricSegments360_pow4_withJoin;
+    float fMaxCombinedSegments_withJoin;
+    bool fSoloRoundJoinAlwaysFitsInPatch;
+
+    // Variables related to the patch chunk that we are currently writing out during prepareBuffers.
+    int fCurrChunkPatchCapacity;
+    int fCurrChunkMinPatchAllocCount;
+    GrVertexWriter fPatchWriter;
+
+    // Variables related to the specific contour that we are currently iterating during
+    // prepareBuffers().
+    bool fHasLastControlPoint = false;
+    SkPoint fCurrContourStartPoint;
+    SkPoint fCurrContourFirstControlPoint;
+    SkPoint fLastControlPoint;
+
+    // Values for the current dynamic state (if any) that will get written out with each patch.
+    GrStrokeTessellateShader::DynamicStroke fDynamicStroke;
+    GrVertexColor fDynamicColor;
+};
+
+}  // namespace
 
 static bool conic_has_cusp(const SkPoint p[3]) {
     SkVector a = p[1] - p[0];
@@ -96,42 +685,33 @@
 
 void GrStrokeHardwareTessellator::prepare(GrMeshDrawOp::Target* target,
                                           const SkMatrix& viewMatrix) {
-    SkASSERT(!fTarget);
-    fTarget = target;
+    using JoinType = PatchWriter::JoinType;
 
     std::array<float, 2> matrixScales;
     if (!viewMatrix.getMinMaxScales(matrixScales.data())) {
         matrixScales.fill(1);
     }
 
-    // Pre-allocate at least enough vertex space for 1 in 4 strokes to chop, and for 8 caps.
-    int strokePreallocCount = fTotalCombinedVerbCnt * 5/4;
-    int capPreallocCount = 8;
-    this->allocPatchChunkAtLeast(strokePreallocCount + capPreallocCount);
-
+    PatchWriter patchWriter(fShaderFlags, target, &fPatchChunks, fTotalCombinedVerbCnt);
     const SkStrokeRec* strokeForTolerances = nullptr;
 
     for (const auto& pathStroke : fPathStrokeList) {
         const SkStrokeRec& stroke = pathStroke.fStroke;
         if (!strokeForTolerances || strokeForTolerances->getWidth() != stroke.getWidth() ||
-            strokeForTolerances->getJoin() != stroke.getJoin()) {
+            strokeForTolerances->getCap() != stroke.getCap()) {
             auto tolerances = Tolerances::MakePreTransform(matrixScales.data(), stroke.getWidth());
-            this->updateTolerances(tolerances, stroke.getJoin());
+            patchWriter.updateTolerances(tolerances, stroke.getJoin());
             strokeForTolerances = &stroke;
         }
-        auto strokeJoinType = JoinType(stroke.getJoin());
-
         if (fShaderFlags & ShaderFlags::kDynamicStroke) {
-            fDynamicStroke.set(stroke);
+            patchWriter.updateDynamicStroke(stroke);
         }
         if (fShaderFlags & ShaderFlags::kDynamicColor) {
-            bool wideColor = fShaderFlags & ShaderFlags::kWideColor;
-            SkASSERT(wideColor || pathStroke.fColor.fitsInBytes());
-            fDynamicColor.set(pathStroke.fColor, wideColor);
+            patchWriter.updateDynamicColor(pathStroke.fColor);
         }
 
         const SkPath& path = pathStroke.fPath;
-        fHasLastControlPoint = false;
+        auto strokeJoinType = JoinType(stroke.getJoin());
         SkPathVerb previousVerb = SkPathVerb::kClose;
         for (auto [verb, p, w] : SkPathPriv::Iterate(path)) {
             switch (verb) {
@@ -139,541 +719,52 @@
                     // "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(p[-1], viewMatrix, stroke);
+                        patchWriter.cap(p[-1], viewMatrix, stroke);
                     }
-                    this->moveTo(p[0]);
+                    patchWriter.moveTo(p[0]);
                     break;
                 case SkPathVerb::kLine:
-                    this->lineTo(strokeJoinType, p[0], p[1]);
+                    patchWriter.lineTo(strokeJoinType, p[0], p[1]);
                     break;
                 case SkPathVerb::kQuad:
                     if (conic_has_cusp(p)) {
                         SkPoint cusp = SkEvalQuadAt(p, SkFindQuadMidTangent(p));
-                        this->lineTo(strokeJoinType, p[0], cusp);
-                        this->lineTo(JoinType::kBowtie, cusp, p[2]);
+                        patchWriter.lineTo(strokeJoinType, p[0], cusp);
+                        patchWriter.lineTo(JoinType::kBowtie, cusp, p[2]);
                     } else {
-                        this->conicTo(strokeJoinType, p, 1);
+                        patchWriter.conicTo(strokeJoinType, p, 1);
                     }
                     break;
                 case SkPathVerb::kConic:
                     if (conic_has_cusp(p)) {
                         SkConic conic(p, *w);
                         SkPoint cusp = conic.evalAt(conic.findMidTangent());
-                        this->lineTo(strokeJoinType, p[0], cusp);
-                        this->lineTo(JoinType::kBowtie, cusp, p[2]);
+                        patchWriter.lineTo(strokeJoinType, p[0], cusp);
+                        patchWriter.lineTo(JoinType::kBowtie, cusp, p[2]);
                     } else {
-                        this->conicTo(strokeJoinType, p, *w);
+                        patchWriter.conicTo(strokeJoinType, p, *w);
                     }
                     break;
                 case SkPathVerb::kCubic:
                     bool areCusps;
                     GrPathUtils::findCubicConvex180Chops(p, nullptr, &areCusps);
                     if (areCusps) {
-                        this->cubicConvex180SegmentsTo(strokeJoinType, p);
+                        patchWriter.cubicConvex180SegmentsTo(strokeJoinType, p);
                     } else {
-                        this->cubicTo(strokeJoinType, p);
+                        patchWriter.cubicTo(strokeJoinType, p);
                     }
                     break;
                 case SkPathVerb::kClose:
-                    this->close(p[0], viewMatrix, stroke);
+                    patchWriter.close(p[0], viewMatrix, stroke);
                     break;
             }
             previousVerb = verb;
         }
         if (previousVerb != SkPathVerb::kMove && previousVerb != SkPathVerb::kClose) {
             const SkPoint* p = SkPathPriv::PointData(path);
-            this->cap(p[path.countPoints() - 1], viewMatrix, stroke);
+            patchWriter.cap(p[path.countPoints() - 1], viewMatrix, stroke);
         }
     }
-
-    fTarget->putBackVertices(fCurrChunkPatchCapacity - fPatchChunks.back().fPatchCount,
-                             fPatchStride);
-
-    fTarget = nullptr;
-}
-
-void GrStrokeHardwareTessellator::moveTo(SkPoint pt) {
-    fCurrContourStartPoint = pt;
-    fHasLastControlPoint = false;
-}
-
-void GrStrokeHardwareTessellator::moveTo(SkPoint pt, SkPoint lastControlPoint) {
-    fCurrContourStartPoint = pt;
-    fCurrContourFirstControlPoint = fLastControlPoint = lastControlPoint;
-    fHasLastControlPoint = true;
-}
-
-void GrStrokeHardwareTessellator::lineTo(JoinType prevJoinType, SkPoint p0, SkPoint p1) {
-    // Zero-length paths need special treatment because they are spec'd to behave differently.
-    if (p0 == p1) {
-        return;
-    }
-
-    if (fMaxCombinedSegments_withJoin < 1) {
-        // The stroke has extremely thick round joins and there aren't enough guaranteed segments to
-        // always combine a join with a line patch. Emit the join in its own separate patch.
-        this->joinTo(prevJoinType, p0, p1);
-        prevJoinType = JoinType::kNone;
-    }
-
-    SkPoint asPatch[4] = {p0, p0, p1, p1};
-    this->emitPatch(prevJoinType, asPatch, p1);
-}
-
-void GrStrokeHardwareTessellator::conicTo(JoinType prevJoinType, const SkPoint p[3], float w,
-                                          int maxDepth) {
-    // 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] || w == 0) {
-        this->lineTo(prevJoinType, p[0], p[2]);
-        return;
-    }
-
-    // Convert to a patch.
-    SkPoint asPatch[4];
-    if (w == 1) {
-        GrPathUtils::convertQuadToCubic(p, asPatch);
-    } else {
-        GrPathShader::WriteConicPatch(p, w, asPatch);
-    }
-
-    // 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(fTolerances.fParametricIntolerance, p);
-    if (numParametricSegments_pow4 <= fMaxParametricSegments180_pow4_withJoin) {
-        this->emitPatch(prevJoinType, asPatch, p[2]);
-        return;
-    }
-
-    if (numParametricSegments_pow4 <= fMaxParametricSegments180_pow4 || maxDepth == 0) {
-        if (numParametricSegments_pow4 > fMaxParametricSegments180_pow4_withJoin) {
-            // There aren't enough guaranteed segments to include the join. Emit a standalone patch
-            // for the join.
-            this->joinTo(prevJoinType, asPatch);
-            prevJoinType = JoinType::kNone;
-        }
-        this->emitPatch(prevJoinType, asPatch, p[2]);
-        return;
-    }
-
-    // We still might have enough tessellation segments to render the curve. Check again with the
-    // actual rotation.
-    float numRadialSegments = SkMeasureQuadRotation(p) * fTolerances.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 (w == 1) {
-            SkPoint chops[5];
-            if (numParametricSegments >= numRadialSegments) {
-                SkChopQuadAtHalf(p, chops);
-            } else {
-                SkChopQuadAtMidTangent(p, chops);
-            }
-            this->conicTo(prevJoinType, chops, 1, maxDepth - 1);
-            this->conicTo(JoinType::kBowtie, chops + 2, 1, maxDepth - 1);
-        } else {
-            SkConic conic(p, w);
-            float chopT = (numParametricSegments >= numRadialSegments) ? .5f
-                                                                       : conic.findMidTangent();
-            SkConic chops[2];
-            if (conic.chopAt(chopT, chops)) {
-                this->conicTo(prevJoinType, chops[0].fPts, chops[0].fW, maxDepth - 1);
-                this->conicTo(JoinType::kBowtie, chops[1].fPts, chops[1].fW, maxDepth - 1);
-            }
-        }
-        return;
-    }
-
-    if (numCombinedSegments > fMaxCombinedSegments_withJoin) {
-        // There aren't enough guaranteed segments to include the join. Emit a standalone patch for
-        // the join.
-        this->joinTo(prevJoinType, asPatch);
-        prevJoinType = JoinType::kNone;
-    }
-    this->emitPatch(prevJoinType, asPatch, p[2]);
-}
-
-void GrStrokeHardwareTessellator::cubicTo(JoinType prevJoinType, const SkPoint p[4],
-                                          Convex180Status convex180Status, int maxDepth) {
-    // 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(prevJoinType, p[0], p[3]);
-        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(fTolerances.fParametricIntolerance, p);
-    if (numParametricSegments_pow4 <= fMaxParametricSegments360_pow4_withJoin) {
-        this->emitPatch(prevJoinType, p, p[3]);
-        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) {
-            // There aren't enough guaranteed segments to include the join. Emit a standalone patch
-            // for the join.
-            this->joinTo(prevJoinType, p);
-            prevJoinType = JoinType::kNone;
-        }
-        this->emitPatch(prevJoinType, p, p[3]);
-        return;
-    }
-
-    // Ensure the curve does not inflect or rotate >180 degrees before we start subdividing and
-    // measuring rotation.
-    if (convex180Status == Convex180Status::kUnknown) {
-        this->cubicConvex180SegmentsTo(prevJoinType, p);
-        return;
-    }
-
-    // We still might have enough tessellation segments to render the curve. Check again with
-    // its actual rotation.
-    float numRadialSegments =
-            SkMeasureNonInflectCubicRotation(p) * fTolerances.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.
-        SkPoint chops[7];
-        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);
-        }
-        this->cubicTo(prevJoinType, chops, Convex180Status::kYes, maxDepth - 1);
-        this->cubicTo(JoinType::kBowtie, chops + 3, Convex180Status::kYes, maxDepth - 1);
-        return;
-    }
-
-    if (numCombinedSegments > fMaxCombinedSegments_withJoin) {
-        // There aren't enough guaranteed segments to include the join. Emit a standalone patch for
-        // the join.
-        this->joinTo(prevJoinType, p);
-        prevJoinType = JoinType::kNone;
-    }
-    this->emitPatch(prevJoinType, p, p[3]);
-}
-
-void GrStrokeHardwareTessellator::cubicConvex180SegmentsTo(JoinType prevJoinType,
-                                                           const SkPoint p[4]) {
-    SkPoint chops[10];
-    float chopT[2];
-    bool areCusps = false;
-    int numChops = GrPathUtils::findCubicConvex180Chops(p, chopT, &areCusps);
-    if (numChops == 0) {
-        // The curve is already convex and rotates no more than 180 degrees.
-        this->cubicTo(prevJoinType, p, Convex180Status::kYes);
-    } else if (numChops == 1) {
-        SkChopCubicAt(p, chops, chopT[0]);
-        if (areCusps) {
-            // When chopping on a perfect cusp, these 3 points will be equal.
-            chops[2] = chops[4] = chops[3];
-        }
-        this->cubicTo(prevJoinType, chops, Convex180Status::kYes);
-        this->cubicTo(JoinType::kBowtie, chops + 3, Convex180Status::kYes);
-    } else {
-        SkASSERT(numChops == 2);
-        SkChopCubicAt(p, chops, chopT[0], chopT[1]);
-        // Two cusps are only possible on a flat line with two 180-degree turnarounds.
-        if (areCusps) {
-            this->lineTo(prevJoinType, chops[0], chops[3]);
-            this->lineTo(JoinType::kBowtie, chops[3], chops[6]);
-            this->lineTo(JoinType::kBowtie, chops[6], chops[9]);
-            return;
-        }
-        this->cubicTo(prevJoinType, chops, Convex180Status::kYes);
-        this->cubicTo(JoinType::kBowtie, chops + 3, Convex180Status::kYes);
-        this->cubicTo(JoinType::kBowtie, chops + 6, Convex180Status::kYes);
-    }
-}
-
-void GrStrokeHardwareTessellator::joinTo(JoinType joinType, SkPoint junctionPoint,
-                                         SkPoint nextControlPoint, int maxDepth) {
-    if (!fHasLastControlPoint) {
-        // The first stroke doesn't have a previous join.
-        return;
-    }
-
-    if (!fSoloRoundJoinAlwaysFitsInPatch && maxDepth != 0 &&
-        (joinType == JoinType::kRound || joinType == JoinType::kBowtie)) {
-        SkVector tan0 = junctionPoint - fLastControlPoint;
-        SkVector tan1 = nextControlPoint - junctionPoint;
-        float rotation = SkMeasureAngleBetweenVectors(tan0, tan1);
-        float numRadialSegments = rotation * fTolerances.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(skia:11347): This hack ensures "c0 - junctionPoint" gives the exact same ieee
-            // fp32 vector as "-(c1 - junctionPoint)". Tessellated stroking is becoming less
-            // experimental, so t's time to think of a cleaner method to avoid T-junctions when we
-            // chop joins.
-            int maxAttempts = 10;
-            do {
-                bisector = (junctionPoint + bisector) - (junctionPoint - bisector);
-                c0 = junctionPoint + bisector;
-                c1 = junctionPoint - bisector;
-            } while (c0 - junctionPoint != -(c1 - junctionPoint) && --maxAttempts);
-            this->joinTo(joinType, junctionPoint, c0, maxDepth - 1);  // First join half.
-            fLastControlPoint = c1;
-            // Second join half.
-            this->joinTo(joinType, junctionPoint, nextControlPoint, maxDepth - 1);
-            return;
-        }
-    }
-
-    this->emitJoinPatch(joinType, junctionPoint, nextControlPoint);
-}
-
-void GrStrokeHardwareTessellator::close(SkPoint contourEndpoint, const SkMatrix& viewMatrix,
-                                        const SkStrokeRec& stroke) {
-    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(contourEndpoint, viewMatrix, stroke);
-        return;
-    }
-
-    // Draw a line back to the beginning. (This will be discarded if
-    // contourEndpoint == fCurrContourStartPoint.)
-    auto strokeJoinType = JoinType(stroke.getJoin());
-    this->lineTo(strokeJoinType, contourEndpoint, fCurrContourStartPoint);
-    this->joinTo(strokeJoinType, fCurrContourStartPoint, fCurrContourFirstControlPoint);
-
-    fHasLastControlPoint = false;
-}
-
-void GrStrokeHardwareTessellator::cap(SkPoint contourEndpoint, const SkMatrix& viewMatrix,
-                                      const SkStrokeRec& stroke) {
-    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.
-        SkVector outset;
-        if (!stroke.isHairlineStyle()) {
-            outset = {1, 0};
-        } else {
-            // If the stroke is hairline, orient the square on the post-transform x-axis instead.
-            // We don't need to worry about the vector length since it will be normalized later.
-            // Since the matrix cannot have perspective, the below is equivalent to:
-            //
-            //    outset = inverse(|a b|) * |1| * arbitrary_scale
-            //                     |c d|    |0|
-            //
-            //    == 1/det * | d -b| * |1| * arbitrary_scale
-            //               |-c  a|   |0|
-            //
-            //    == 1/det * | d| * arbitrary_scale
-            //               |-c|
-            //
-            //    == | d|
-            //       |-c|
-            //
-            SkASSERT(!viewMatrix.hasPerspective());
-            float c=viewMatrix.getSkewY(), d=viewMatrix.getScaleY();
-            outset = {d, -c};
-        }
-        fCurrContourFirstControlPoint = fCurrContourStartPoint - outset;
-        fLastControlPoint = fCurrContourStartPoint + outset;
-        fHasLastControlPoint = true;
-        contourEndpoint = fCurrContourStartPoint;
-    }
-
-    switch (stroke.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 bowtie.
-            JoinType roundCapJoinType = (stroke.getJoin() == SkPaint::kRound_Join)
-                    ? JoinType::kRound : JoinType::kBowtie;
-            this->joinTo(roundCapJoinType, contourEndpoint, fLastControlPoint);
-            this->moveTo(fCurrContourStartPoint, fCurrContourFirstControlPoint);
-            this->joinTo(roundCapJoinType, fCurrContourStartPoint,
-                         fCurrContourFirstControlPoint);
-            break;
-        }
-        case SkPaint::kSquare_Cap: {
-            // A square cap is the same as appending lineTos.
-            auto strokeJoinType = JoinType(stroke.getJoin());
-            SkVector lastTangent = contourEndpoint - fLastControlPoint;
-            if (!stroke.isHairlineStyle()) {
-                // Extend the cap by 1/2 stroke width.
-                lastTangent *= (.5f * stroke.getWidth()) / lastTangent.length();
-            } else {
-                // Extend the cap by what will be 1/2 pixel after transformation.
-                lastTangent *= .5f / viewMatrix.mapVector(lastTangent.fX, lastTangent.fY).length();
-            }
-            this->lineTo(strokeJoinType, contourEndpoint, contourEndpoint + lastTangent);
-            this->moveTo(fCurrContourStartPoint, fCurrContourFirstControlPoint);
-            SkVector firstTangent = fCurrContourFirstControlPoint - fCurrContourStartPoint;
-            if (!stroke.isHairlineStyle()) {
-                // Set the the cap back by 1/2 stroke width.
-                firstTangent *= (-.5f * stroke.getWidth()) / firstTangent.length();
-            } else {
-                // Set the cap back by what will be 1/2 pixel after transformation.
-                firstTangent *=
-                        -.5f / viewMatrix.mapVector(firstTangent.fX, firstTangent.fY).length();
-            }
-            this->lineTo(strokeJoinType, fCurrContourStartPoint,
-                         fCurrContourStartPoint + firstTangent);
-            break;
-        }
-    }
-
-    fHasLastControlPoint = false;
-}
-
-void GrStrokeHardwareTessellator::emitPatch(JoinType prevJoinType, const SkPoint p[4],
-                                            SkPoint endPt) {
-    SkPoint c1 = (p[1] == p[0]) ? p[2] : p[1];
-    SkPoint c2 = (p[2] == endPt) ? p[1] : p[2];
-
-    if (prevJoinType == JoinType::kBowtie) {
-        // Bowties need to go in their own patch if they will have >1 segment.
-        // TODO: Investigate if an optimization like "x < fCosRadiansPerSegment" would be worth it.
-        float rotation = SkMeasureAngleBetweenVectors(p[0] - fLastControlPoint, c1 - p[0]);
-        if (rotation * fTolerances.fNumRadialSegmentsPerRadian > 1) {
-            this->joinTo(prevJoinType, p[0], c1);
-            prevJoinType = JoinType::kNone;
-        }
-    }
-
-    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 (this->reservePatch()) {
-        // Disable the join section of this patch if prevJoinType is kNone by setting the previous
-        // control point equal to p0.
-        fPatchWriter.write((prevJoinType == JoinType::kNone) ? p[0] : fLastControlPoint);
-        fPatchWriter.writeArray(p, 4);
-        this->emitDynamicAttribs();
-    }
-
-    fLastControlPoint = c2;
-}
-
-void GrStrokeHardwareTessellator::emitJoinPatch(JoinType joinType, SkPoint junctionPoint,
-                                                SkPoint nextControlPoint) {
-    // We should never write out joins before the first curve.
-    SkASSERT(fHasLastControlPoint);
-
-    if (this->reservePatch()) {
-        fPatchWriter.write(fLastControlPoint, junctionPoint);
-        if (joinType == JoinType::kBowtie) {
-            // {prevControlPoint, [p0, p0, p0, p3]} is a reserved patch pattern that means this
-            // patch is a bowtie. The bowtie is anchored on p0 and its tangent angles go from
-            // (p0 - prevControlPoint) to (p3 - p0).
-            fPatchWriter.write(junctionPoint, junctionPoint);
-        } else {
-            SkASSERT(joinType != JoinType::kNone);
-            // {prevControlPoint, [p0, p3, p3, p3]} is a reserved patch pattern that means this
-            // patch is a join only (no curve sections in the patch). The join is anchored on p0 and
-            // its tangent angles go from (p0 - prevControlPoint) to (p3 - p0).
-            fPatchWriter.write(nextControlPoint, nextControlPoint);
-        }
-        fPatchWriter.write(nextControlPoint);
-        this->emitDynamicAttribs();
-    }
-
-    fLastControlPoint = nextControlPoint;
-}
-
-void GrStrokeHardwareTessellator::emitDynamicAttribs() {
-    if (fShaderFlags & ShaderFlags::kDynamicStroke) {
-        fPatchWriter.write(fDynamicStroke);
-    }
-    if (fShaderFlags & ShaderFlags::kDynamicColor) {
-        fPatchWriter.write(fDynamicColor);
-    }
-}
-
-bool GrStrokeHardwareTessellator::reservePatch() {
-    if (fPatchChunks.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 (!fPatchWriter.isValid()) {
-        SkDebugf("WARNING: Failed to allocate vertex buffer for tessellated stroke.");
-        return false;
-    }
-    SkASSERT(fPatchChunks.back().fPatchCount <= fCurrChunkPatchCapacity);
-    ++fPatchChunks.back().fPatchCount;
-    return true;
-}
-
-void GrStrokeHardwareTessellator::allocPatchChunkAtLeast(int minPatchAllocCount) {
-    SkASSERT(fTarget);
-    PatchChunk* chunk = &fPatchChunks.push_back();
-    fPatchWriter = {fTarget->makeVertexSpaceAtLeast(fPatchStride, minPatchAllocCount,
-                                                    minPatchAllocCount, &chunk->fPatchBuffer,
-                                                    &chunk->fBasePatch, &fCurrChunkPatchCapacity)};
-    fCurrChunkMinPatchAllocCount = minPatchAllocCount;
 }
 
 void GrStrokeHardwareTessellator::draw(GrOpFlushState* flushState) const {
diff --git a/src/gpu/tessellate/GrStrokeHardwareTessellator.h b/src/gpu/tessellate/GrStrokeHardwareTessellator.h
index a07ed05..37a4041 100644
--- a/src/gpu/tessellate/GrStrokeHardwareTessellator.h
+++ b/src/gpu/tessellate/GrStrokeHardwareTessellator.h
@@ -18,91 +18,6 @@
 // MSAA if antialiasing is desired.
 class GrStrokeHardwareTessellator : public GrStrokeTessellator {
 public:
-    GrStrokeHardwareTessellator(ShaderFlags shaderFlags,
-                                GrSTArenaList<PathStroke>&& pathStrokeList,
-                                int totalCombinedVerbCnt, const GrShaderCaps& shaderCaps)
-            : GrStrokeTessellator(shaderFlags, std::move(pathStrokeList))
-            , fTotalCombinedVerbCnt(totalCombinedVerbCnt)
-            , fPatchStride(GrStrokeTessellateShader::PatchStride(fShaderFlags))
-            // Subtract 2 because the tessellation shader chops every cubic at two locations, and
-            // each chop has the potential to introduce an extra segment.
-            , fMaxTessellationSegments(shaderCaps.maxTessellationSegments() - 2) {
-    }
-
-    void prepare(GrMeshDrawOp::Target*, const SkMatrix&) override;
-
-    void draw(GrOpFlushState*) const override;
-
-private:
-    using Tolerances = GrStrokeTessellateShader::Tolerances;
-
-    enum class JoinType {
-        kMiter = SkPaint::kMiter_Join,
-        kRound = SkPaint::kRound_Join,
-        kBevel = SkPaint::kBevel_Join,
-        kBowtie = SkPaint::kLast_Join + 1,  // 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
-    };
-
-    // Updates our internal tolerances for determining how much subdivision to do. We need to ensure
-    // every curve we emit requires no more segments than fMaxTessellationSegments.
-    void updateTolerances(Tolerances, SkPaint::Join strokeJoin);
-
-    void moveTo(SkPoint);
-    void moveTo(SkPoint, SkPoint lastControlPoint);
-    void lineTo(JoinType prevJoinType, SkPoint p0, SkPoint p1);
-    void conicTo(JoinType prevJoinType, const SkPoint[3], float w, int maxDepth = -1);
-    void cubicTo(JoinType prevJoinType, const SkPoint[4],
-                 Convex180Status = Convex180Status::kUnknown, int maxDepth = -1);
-    // Chops the curve into 1-3 convex sections that rotate no more than 180 degrees, then calls
-    // cubicTo() for each section.
-    void cubicConvex180SegmentsTo(JoinType prevJoinType, const SkPoint[4]);
-    void joinTo(JoinType joinType, const SkPoint nextCubic[]) {
-        const SkPoint& nextCtrlPt = (nextCubic[1] == nextCubic[0]) ? nextCubic[2] : nextCubic[1];
-        // The caller should have culled out curves where p0==p1==p2 by this point.
-        SkASSERT(nextCtrlPt != nextCubic[0]);
-        this->joinTo(joinType, nextCubic[0], nextCtrlPt);
-    }
-    void joinTo(JoinType, SkPoint junctionPoint, SkPoint nextControlPoint, int maxDepth = -1);
-    void close(SkPoint contourEndpoint, const SkMatrix&, const SkStrokeRec&);
-    void cap(SkPoint contourEndpoint, const SkMatrix&, const SkStrokeRec&);
-    void emitPatch(JoinType prevJoinType, const SkPoint pts[4], SkPoint endPt);
-    void emitJoinPatch(JoinType, SkPoint junctionPoint, SkPoint nextControlPoint);
-    void emitDynamicAttribs();
-    bool reservePatch();
-    void allocPatchChunkAtLeast(int minPatchAllocCount);
-
-    // The combined number of path verbs from all paths in fPathStrokeList.
-    const int fTotalCombinedVerbCnt;
-
-    // Size in bytes of a tessellation patch with our shader flags.
-    const size_t fPatchStride;
-
-    // The maximum number of tessellation segments the hardware can emit for a single patch.
-    const int fMaxTessellationSegments;
-
-    // This will only be valid during prepare() and its callees.
-    GrMeshDrawOp::Target* fTarget = nullptr;
-
-    // These values contain worst-case numbers of parametric segments, raised to the 4th power, that
-    // our hardware can support for the current stroke radius. They assume curve rotations of 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. We raise to the 4th power because
-    // the "pow4" variants of Wang's formula are the quickest to evaluate.
-    GrStrokeTessellateShader::Tolerances fTolerances;
-    float fMaxParametricSegments180_pow4;
-    float fMaxParametricSegments360_pow4;
-    float fMaxParametricSegments180_pow4_withJoin;
-    float fMaxParametricSegments360_pow4_withJoin;
-    float fMaxCombinedSegments_withJoin;
-    bool fSoloRoundJoinAlwaysFitsInPatch;
-
     // We generate and store patch buffers in chunks. Normally there will only be one chunk, but in
     // rare cases the first can run out of space if too many cubics needed to be subdivided.
     struct PatchChunk {
@@ -110,24 +25,23 @@
         int fPatchCount = 0;
         int fBasePatch;
     };
+
+    GrStrokeHardwareTessellator(ShaderFlags shaderFlags,
+                                GrSTArenaList<PathStroke>&& pathStrokeList,
+                                int totalCombinedVerbCnt, const GrShaderCaps& shaderCaps)
+            : GrStrokeTessellator(shaderFlags, std::move(pathStrokeList))
+            , fTotalCombinedVerbCnt(totalCombinedVerbCnt) {
+    }
+
+    void prepare(GrMeshDrawOp::Target*, const SkMatrix&) override;
+    void draw(GrOpFlushState*) const override;
+
+private:
+    // The combined number of path verbs from all paths in fPathStrokeList.
+    const int fTotalCombinedVerbCnt;
+
     SkSTArray<1, PatchChunk> fPatchChunks;
 
-    // Variables related to the patch chunk that we are currently writing out during prepareBuffers.
-    int fCurrChunkPatchCapacity;
-    int fCurrChunkMinPatchAllocCount;
-    GrVertexWriter fPatchWriter;
-
-    // Variables related to the specific contour that we are currently iterating during
-    // prepareBuffers().
-    bool fHasLastControlPoint = false;
-    SkPoint fCurrContourStartPoint;
-    SkPoint fCurrContourFirstControlPoint;
-    SkPoint fLastControlPoint;
-
-    // Stateful values for the dynamic state (if any) that will get written out with each patch.
-    GrStrokeTessellateShader::DynamicStroke fDynamicStroke;
-    GrVertexColor fDynamicColor;
-
     friend class GrOp;  // For ctor.
 
 public: