Create an abstract GrStrokeTessellator class

Now there is only one op to tessellate a stroke, and it creates its
own GrStrokeIndirectTessellator or GrStrokeHardwareTessellator
internally. This will allow us to dynamically switch into hardware
tessellation when we need to batch strokes that have different
parameters or colors.

Bug: chromium:1172543
Bug: skia:10419
Change-Id: I3cddb855fdbb9ab018785584497c843e3e31b75e
Reviewed-on: https://skia-review.googlesource.com/c/skia/+/366056
Commit-Queue: Chris Dalton <csmartdalton@google.com>
Reviewed-by: Greg Daniel <egdaniel@google.com>
diff --git a/bench/TessellateBench.cpp b/bench/TessellateBench.cpp
index 34a6ef7..1a44236 100644
--- a/bench/TessellateBench.cpp
+++ b/bench/TessellateBench.cpp
@@ -13,8 +13,8 @@
 #include "src/gpu/mock/GrMockOpTarget.h"
 #include "src/gpu/tessellate/GrMiddleOutPolygonTriangulator.h"
 #include "src/gpu/tessellate/GrPathTessellator.h"
-#include "src/gpu/tessellate/GrStrokeIndirectOp.h"
-#include "src/gpu/tessellate/GrStrokeTessellateOp.h"
+#include "src/gpu/tessellate/GrStrokeHardwareTessellator.h"
+#include "src/gpu/tessellate/GrStrokeIndirectTessellator.h"
 #include "src/gpu/tessellate/GrWangsFormula.h"
 #include "tools/ToolUtils.h"
 #include <vector>
@@ -146,10 +146,10 @@
     GrMiddleOutPolygonTriangulator::WritePathInnerFan(vertexData, 3, fPath);
 }
 
-class GrStrokeTessellateOp::TestingOnly_Benchmark : public Benchmark {
+class GrStrokeHardwareTessellator::TestingOnly_Benchmark : public Benchmark {
 public:
     TestingOnly_Benchmark(float matrixScale, const char* suffix) : fMatrixScale(matrixScale) {
-        fName.printf("tessellate_GrStrokeTessellateOp_prepare%s", suffix);
+        fName.printf("tessellate_GrStrokeHardwareTessellator_prepare%s", suffix);
     }
 
 private:
@@ -173,10 +173,10 @@
             return;
         }
         for (int i = 0; i < loops; ++i) {
-            GrStrokeTessellateOp op(GrAAType::kMSAA, SkMatrix::Scale(fMatrixScale, fMatrixScale),
-                                    fStrokeRec, fPath, GrPaint());
-            op.fTarget = fTarget.get();
-            op.prepareBuffers();
+            SkMatrix matrix = SkMatrix::Scale(fMatrixScale, fMatrixScale);
+            GrStrokeHardwareTessellator tessellator(*fTarget->caps().shaderCaps(), matrix,
+                                                    fStrokeRec);
+            tessellator.prepare(fTarget.get(), matrix, fPath, fStrokeRec, fPath.countVerbs());
         }
     }
 
@@ -187,13 +187,13 @@
     SkStrokeRec fStrokeRec = SkStrokeRec(SkStrokeRec::kFill_InitStyle);
 };
 
-DEF_BENCH( return new GrStrokeTessellateOp::TestingOnly_Benchmark(1, ""); )
-DEF_BENCH( return new GrStrokeTessellateOp::TestingOnly_Benchmark(5, "_one_chop"); )
+DEF_BENCH( return new GrStrokeHardwareTessellator::TestingOnly_Benchmark(1, ""); )
+DEF_BENCH( return new GrStrokeHardwareTessellator::TestingOnly_Benchmark(5, "_one_chop"); )
 
-class GrStrokeIndirectOp::Benchmark : public ::Benchmark {
+class GrStrokeIndirectTessellator::Benchmark : public ::Benchmark {
 protected:
     Benchmark(const char* nameSuffix, SkPaint::Join join) : fJoin(join) {
-        fName.printf("tessellate_GrStrokeIndirectOpBench%s", nameSuffix);
+        fName.printf("tessellate_GrStrokeIndirectTessellator%s", nameSuffix);
     }
 
     const SkPaint::Join fJoin;
@@ -214,9 +214,10 @@
         }
         for (int i = 0; i < loops; ++i) {
             for (const SkPath& path : fPaths) {
-                GrStrokeIndirectOp op(GrAAType::kMSAA, SkMatrix::I(), path, fStrokeRec, GrPaint());
-                op.prePrepareResolveLevels(fTarget->allocator());
-                op.prepareBuffers(fTarget.get());
+                GrStrokeIndirectTessellator tessellator(SkMatrix::I(), path, fStrokeRec,
+                                                        path.countVerbs(), fTarget->allocator());
+                tessellator.prepare(fTarget.get(), SkMatrix::I(), path, fStrokeRec,
+                                    path.countVerbs());
             }
             fTarget->resetAllocator();
         }
@@ -229,7 +230,7 @@
     SkStrokeRec fStrokeRec{SkStrokeRec::kHairline_InitStyle};
 };
 
-class StrokeIndirectBenchmark : public GrStrokeIndirectOp::Benchmark {
+class StrokeIndirectBenchmark : public GrStrokeIndirectTessellator::Benchmark {
 public:
     StrokeIndirectBenchmark(const char* nameSuffix, SkPaint::Join join, std::vector<SkPoint> pts)
             : Benchmark(nameSuffix, join), fPts(std::move(pts)) {}
@@ -280,7 +281,7 @@
 DEF_BENCH( return new StrokeIndirectBenchmark(
         "_roundjoin", SkPaint::kRound_Join, {{0,0}, {50,100}, {100,0}}); )
 
-class SingleVerbStrokeIndirectBenchmark : public GrStrokeIndirectOp::Benchmark {
+class SingleVerbStrokeIndirectBenchmark : public GrStrokeIndirectTessellator::Benchmark {
 public:
     SingleVerbStrokeIndirectBenchmark(const char* nameSuffix, SkPathVerb verb)
             : Benchmark(nameSuffix, SkPaint::kBevel_Join), fVerb(verb) {}
diff --git a/gn/gpu.gni b/gn/gpu.gni
index 1686028..86e6348 100644
--- a/gn/gpu.gni
+++ b/gn/gpu.gni
@@ -479,13 +479,13 @@
   "$_src/gpu/tessellate/GrPathTessellator.h",
   "$_src/gpu/tessellate/GrStencilPathShader.cpp",
   "$_src/gpu/tessellate/GrStencilPathShader.h",
-  "$_src/gpu/tessellate/GrStrokeIndirectOp.cpp",
-  "$_src/gpu/tessellate/GrStrokeIndirectOp.h",
+  "$_src/gpu/tessellate/GrStrokeHardwareTessellator.cpp",
+  "$_src/gpu/tessellate/GrStrokeHardwareTessellator.h",
+  "$_src/gpu/tessellate/GrStrokeIndirectTessellator.cpp",
+  "$_src/gpu/tessellate/GrStrokeIndirectTessellator.h",
   "$_src/gpu/tessellate/GrStrokeIterator.h",
   "$_src/gpu/tessellate/GrStrokeOp.cpp",
   "$_src/gpu/tessellate/GrStrokeOp.h",
-  "$_src/gpu/tessellate/GrStrokeTessellateOp.cpp",
-  "$_src/gpu/tessellate/GrStrokeTessellateOp.h",
   "$_src/gpu/tessellate/GrStrokeTessellateShader.cpp",
   "$_src/gpu/tessellate/GrStrokeTessellateShader.h",
   "$_src/gpu/tessellate/GrTessellatingStencilFillOp.cpp",
diff --git a/src/gpu/GrProcessorSet.h b/src/gpu/GrProcessorSet.h
index abd8d2d..5b5bc8e 100644
--- a/src/gpu/GrProcessorSet.h
+++ b/src/gpu/GrProcessorSet.h
@@ -43,6 +43,11 @@
         return fCoverageFragmentProcessor.get();
     }
 
+    bool usesVaryingCoords() const {
+        return (fColorFragmentProcessor && fColorFragmentProcessor->usesVaryingCoords()) ||
+               (fCoverageFragmentProcessor && fCoverageFragmentProcessor->usesVaryingCoords());
+    }
+
     const GrXferProcessor* xferProcessor() const {
         SkASSERT(this->isFinalized());
         return fXP.fProcessor;
diff --git a/src/gpu/GrSTArenaList.h b/src/gpu/GrSTArenaList.h
index eb86c70..5a4c530 100644
--- a/src/gpu/GrSTArenaList.h
+++ b/src/gpu/GrSTArenaList.h
@@ -40,12 +40,12 @@
         bool operator!=(const Iter& it) const { return fCurr != it.fCurr; }
         bool operator==(const Iter& it) const { return fCurr == it.fCurr; }
         void operator++() { fCurr = fCurr->fNext; }
-        T& operator*() { return fCurr->fElement; }
-        Node* fCurr;
+        const T& operator*() { return fCurr->fElement; }
+        const Node* fCurr;
     };
 
-    Iter begin() { return Iter{&fHead}; }
-    Iter end() { return Iter{nullptr}; }
+    Iter begin() const { return Iter{&fHead}; }
+    Iter end() const { return Iter{nullptr}; }
 
 private:
     Node fHead;
diff --git a/src/gpu/tessellate/GrPathShader.h b/src/gpu/tessellate/GrPathShader.h
index 81fba8a..53c6a3a 100644
--- a/src/gpu/tessellate/GrPathShader.h
+++ b/src/gpu/tessellate/GrPathShader.h
@@ -34,23 +34,6 @@
     int tessellationPatchVertexCount() const { return fTessellationPatchVertexCount; }
     const SkMatrix& viewMatrix() const { return fViewMatrix; }
 
-    static GrProgramInfo* MakeProgramInfo(const GrPathShader* shader, SkArenaAlloc* arena,
-                                          const GrSurfaceProxyView& writeView,
-                                          const GrPipeline* pipeline,
-                                          const GrXferProcessor::DstProxyView& dstProxyView,
-                                          GrXferBarrierFlags renderPassXferBarriers,
-                                          GrLoadOp colorLoadOp,
-                                          const GrUserStencilSettings* stencil,
-                                          const GrCaps& caps) {
-        return arena->make<GrProgramInfo>(writeView,
-                                          pipeline,
-                                          stencil,
-                                          shader,
-                                          shader->fPrimitiveType,
-                                          shader->fTessellationPatchVertexCount,
-                                          renderPassXferBarriers, colorLoadOp);
-    }
-
     struct ProgramArgs {
         SkArenaAlloc* fArena;
         const GrSurfaceProxyView& fWriteView;
diff --git a/src/gpu/tessellate/GrStrokeTessellateOp.cpp b/src/gpu/tessellate/GrStrokeHardwareTessellator.cpp
similarity index 83%
rename from src/gpu/tessellate/GrStrokeTessellateOp.cpp
rename to src/gpu/tessellate/GrStrokeHardwareTessellator.cpp
index bb9c3aa..31e3f3e 100644
--- a/src/gpu/tessellate/GrStrokeTessellateOp.cpp
+++ b/src/gpu/tessellate/GrStrokeHardwareTessellator.cpp
@@ -5,7 +5,7 @@
  * found in the LICENSE file.
  */
 
-#include "src/gpu/tessellate/GrStrokeTessellateOp.h"
+#include "src/gpu/tessellate/GrStrokeHardwareTessellator.h"
 
 #include "src/core/SkPathPriv.h"
 #include "src/gpu/GrRecordingContextPriv.h"
@@ -14,36 +14,29 @@
 
 using Patch = GrStrokeTessellateShader::Patch;
 
-void GrStrokeTessellateOp::onPrePrepare(GrRecordingContext* context,
-                                        const GrSurfaceProxyView& writeView, GrAppliedClip* clip,
-                                        const GrXferProcessor::DstProxyView& dstProxyView,
-                                        GrXferBarrierFlags renderPassXferBarriers,
-                                        GrLoadOp colorLoadOp) {
-    this->prePreparePrograms(GrStrokeTessellateShader::Mode::kTessellation,
-                             context->priv().recordTimeAllocator(), writeView,
-                             (clip) ? std::move(*clip) : GrAppliedClip::Disabled(), dstProxyView,
-                             renderPassXferBarriers, colorLoadOp, *context->priv().caps());
-    if (fStencilProgram) {
-        context->priv().recordProgramInfo(fStencilProgram);
-    }
-    if (fFillProgram) {
-        context->priv().recordProgramInfo(fFillProgram);
-    }
+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;
 }
 
-void GrStrokeTessellateOp::onPrepare(GrOpFlushState* flushState) {
-    if (!fFillProgram && !fStencilProgram) {
-        this->prePreparePrograms(GrStrokeTessellateShader::Mode::kTessellation,
-                                 flushState->allocator(), flushState->writeView(),
-                                 flushState->detachAppliedClip(), flushState->dstProxyView(),
-                                 flushState->renderPassBarriers(), flushState->colorLoadOp(),
-                                 flushState->caps());
-    }
-    SkASSERT(fFillProgram || fStencilProgram);
-
-    fTarget = flushState;
-    this->prepareBuffers();
-    fTarget = nullptr;
+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) {
@@ -51,29 +44,28 @@
     return xx*xx;
 }
 
-void GrStrokeTessellateOp::prepareBuffers() {
-    SkASSERT(fTarget);
-
-    // Subtract 2 because the tessellation shader chops every cubic at two locations, and each chop
-    // has the potential to introduce an extra segment.
-    fMaxTessellationSegments = fTarget->caps().shaderCaps()->maxTessellationSegments() - 2;
-
-    fTolerances = this->preTransformTolerances();
-
+GrStrokeHardwareTessellator::GrStrokeHardwareTessellator(const GrShaderCaps& shaderCaps,
+                                                         const SkMatrix& viewMatrix,
+                                                         const SkStrokeRec& stroke)
+        // 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)
+        , fStroke(stroke)
+        , fTolerances(GrStrokeTessellateShader::Tolerances::MakePreTransform(viewMatrix, stroke)) {
     // 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 * fTolerances.fNumRadialSegmentsPerRadian), 1.f);
-    float maxParametricSegments180 = NumParametricSegments(fMaxTessellationSegments,
-                                                           numRadialSegments180);
+    float maxParametricSegments180 = num_parametric_segments(fMaxTessellationSegments,
+                                                             numRadialSegments180);
     fMaxParametricSegments180_pow4 = pow4(maxParametricSegments180);
 
     float numRadialSegments360 = std::max(std::ceil(
             2*SK_ScalarPI * fTolerances.fNumRadialSegmentsPerRadian), 1.f);
-    float maxParametricSegments360 = NumParametricSegments(fMaxTessellationSegments,
-                                                           numRadialSegments360);
+    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
@@ -99,13 +91,20 @@
             maxParametricSegments360 - maxNumSegmentsInJoin - 1, 0.f));
     fMaxCombinedSegments_withJoin = fMaxTessellationSegments - maxNumSegmentsInJoin - 1;
     fSoloRoundJoinAlwaysFitsInPatch = (numRadialSegments180 <= fMaxTessellationSegments);
+}
+
+void GrStrokeHardwareTessellator::prepare(GrMeshDrawOp::Target* target, const SkMatrix& viewMatrix,
+                                          const GrSTArenaList<SkPath>& pathList, const SkStrokeRec&,
+                                          int totalCombinedVerbCnt) {
+    fTarget = target;
+    fViewMatrix = &viewMatrix;
 
     // Pre-allocate at least enough vertex space for 1 in 4 strokes to chop, and for 8 caps.
-    int strokePreallocCount = fTotalCombinedVerbCnt * 5/4;
+    int strokePreallocCount = totalCombinedVerbCnt * 5/4;
     int capPreallocCount = 8;
     this->allocPatchChunkAtLeast(strokePreallocCount + capPreallocCount);
 
-    for (const SkPath& path : fPathList) {
+    for (const SkPath& path : pathList) {
         fHasLastControlPoint = false;
         SkDEBUGCODE(fHasCurrentPoint = false;)
         SkPathVerb previousVerb = SkPathVerb::kClose;
@@ -143,22 +142,25 @@
             this->cap();
         }
     }
+
+    fTarget = nullptr;
+    fViewMatrix = nullptr;
 }
 
-void GrStrokeTessellateOp::moveTo(SkPoint pt) {
+void GrStrokeHardwareTessellator::moveTo(SkPoint pt) {
     fCurrentPoint = fCurrContourStartPoint = pt;
     fHasLastControlPoint = false;
     SkDEBUGCODE(fHasCurrentPoint = true;)
 }
 
-void GrStrokeTessellateOp::moveTo(SkPoint pt, SkPoint lastControlPoint) {
+void GrStrokeHardwareTessellator::moveTo(SkPoint pt, SkPoint lastControlPoint) {
     fCurrentPoint = fCurrContourStartPoint = pt;
     fCurrContourFirstControlPoint = fLastControlPoint = lastControlPoint;
     fHasLastControlPoint = true;
     SkDEBUGCODE(fHasCurrentPoint = true;)
 }
 
-void GrStrokeTessellateOp::lineTo(SkPoint pt, JoinType prevJoinType) {
+void GrStrokeHardwareTessellator::lineTo(SkPoint pt, JoinType prevJoinType) {
     SkASSERT(fHasCurrentPoint);
 
     // Zero-length paths need special treatment because they are spec'd to behave differently.
@@ -177,8 +179,8 @@
     this->emitPatch(prevJoinType, asPatch, pt);
 }
 
-void GrStrokeTessellateOp::conicTo(const SkPoint p[3], float w, JoinType prevJoinType,
-                                   int maxDepth) {
+void GrStrokeHardwareTessellator::conicTo(const SkPoint p[3], float w, JoinType prevJoinType,
+                                          int maxDepth) {
     SkASSERT(fHasCurrentPoint);
     SkASSERT(p[0] == fCurrentPoint);
 
@@ -241,7 +243,7 @@
     numRadialSegments = std::max(std::ceil(numRadialSegments), 1.f);
     float numParametricSegments = GrWangsFormula::root4(numParametricSegments_pow4);
     numParametricSegments = std::max(std::ceil(numParametricSegments), 1.f);
-    float numCombinedSegments = NumCombinedSegments(numParametricSegments, numRadialSegments);
+    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) {
@@ -283,8 +285,8 @@
     this->emitPatch(prevJoinType, asPatch, p[2]);
 }
 
-void GrStrokeTessellateOp::cubicTo(const SkPoint p[4], JoinType prevJoinType,
-                                   Convex180Status convex180Status, int maxDepth) {
+void GrStrokeHardwareTessellator::cubicTo(const SkPoint p[4], JoinType prevJoinType,
+                                          Convex180Status convex180Status, int maxDepth) {
     SkASSERT(fHasCurrentPoint);
     SkASSERT(p[0] == fCurrentPoint);
 
@@ -364,7 +366,7 @@
     numRadialSegments = std::max(std::ceil(numRadialSegments), 1.f);
     float numParametricSegments = GrWangsFormula::root4(numParametricSegments_pow4);
     numParametricSegments = std::max(std::ceil(numParametricSegments), 1.f);
-    float numCombinedSegments = NumCombinedSegments(numParametricSegments, numRadialSegments);
+    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) {
@@ -394,7 +396,8 @@
     this->emitPatch(prevJoinType, p, p[3]);
 }
 
-void GrStrokeTessellateOp::joinTo(JoinType joinType, SkPoint nextControlPoint, int maxDepth) {
+void GrStrokeHardwareTessellator::joinTo(JoinType joinType, SkPoint nextControlPoint,
+                                         int maxDepth) {
     SkASSERT(fHasCurrentPoint);
 
     if (!fHasLastControlPoint) {
@@ -441,7 +444,7 @@
     this->emitJoinPatch(joinType, nextControlPoint);
 }
 
-void GrStrokeTessellateOp::close() {
+void GrStrokeHardwareTessellator::close() {
     SkASSERT(fHasCurrentPoint);
 
     if (!fHasLastControlPoint) {
@@ -465,7 +468,8 @@
     SkDEBUGCODE(fHasCurrentPoint = false;)
 }
 
-void GrStrokeTessellateOp::cap() {
+void GrStrokeHardwareTessellator::cap() {
+    SkASSERT(fViewMatrix);
     SkASSERT(fHasCurrentPoint);
 
     if (!fHasLastControlPoint) {
@@ -492,8 +496,8 @@
             //    == | d|
             //       |-c|
             //
-            SkASSERT(!fViewMatrix.hasPerspective());
-            float c=fViewMatrix.getSkewY(), d=fViewMatrix.getScaleY();
+            SkASSERT(!fViewMatrix->hasPerspective());
+            float c=fViewMatrix->getSkewY(), d=fViewMatrix->getScaleY();
             outset = {d, -c};
         }
         fCurrContourFirstControlPoint = fCurrContourStartPoint - outset;
@@ -523,7 +527,8 @@
                 lastTangent *= (.5f * fStroke.getWidth()) / lastTangent.length();
             } else {
                 // Extend the cap by what will be 1/2 pixel after transformation.
-                lastTangent *= .5f / fViewMatrix.mapVector(lastTangent.fX, lastTangent.fY).length();
+                lastTangent *=
+                        .5f / fViewMatrix->mapVector(lastTangent.fX, lastTangent.fY).length();
             }
             this->lineTo(fCurrentPoint + lastTangent);
             this->moveTo(fCurrContourStartPoint, fCurrContourFirstControlPoint);
@@ -534,7 +539,7 @@
             } else {
                 // Set the cap back by what will be 1/2 pixel after transformation.
                 firstTangent *=
-                        -.5f / fViewMatrix.mapVector(firstTangent.fX, firstTangent.fY).length();
+                        -.5f / fViewMatrix->mapVector(firstTangent.fX, firstTangent.fY).length();
             }
             this->lineTo(fCurrContourStartPoint + firstTangent);
             break;
@@ -545,7 +550,8 @@
     SkDEBUGCODE(fHasCurrentPoint = false;)
 }
 
-void GrStrokeTessellateOp::emitPatch(JoinType prevJoinType, const SkPoint p[4], SkPoint endPt) {
+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];
 
@@ -584,7 +590,7 @@
     fCurrentPoint = endPt;
 }
 
-void GrStrokeTessellateOp::emitJoinPatch(JoinType joinType, SkPoint nextControlPoint) {
+void GrStrokeHardwareTessellator::emitJoinPatch(JoinType joinType, SkPoint nextControlPoint) {
     // We should never write out joins before the first curve.
     SkASSERT(fHasLastControlPoint);
     SkASSERT(fHasCurrentPoint);
@@ -607,7 +613,7 @@
     fLastControlPoint = nextControlPoint;
 }
 
-Patch* GrStrokeTessellateOp::reservePatch() {
+Patch* 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.)
@@ -623,7 +629,8 @@
     return patch;
 }
 
-void GrStrokeTessellateOp::allocPatchChunkAtLeast(int minPatchAllocCount) {
+void GrStrokeHardwareTessellator::allocPatchChunkAtLeast(int minPatchAllocCount) {
+    SkASSERT(fTarget);
     PatchChunk* chunk = &fPatchChunks.push_back();
     fCurrChunkPatchData = (Patch*)fTarget->makeVertexSpaceAtLeast(sizeof(Patch), minPatchAllocCount,
                                                                   minPatchAllocCount,
@@ -633,26 +640,11 @@
     fCurrChunkMinPatchAllocCount = minPatchAllocCount;
 }
 
-void GrStrokeTessellateOp::onExecute(GrOpFlushState* flushState, const SkRect& chainBounds) {
-    SkASSERT(chainBounds == this->bounds());
-    if (fStencilProgram) {
-        flushState->bindPipelineAndScissorClip(*fStencilProgram, this->bounds());
-        flushState->bindTextures(fStencilProgram->primProc(), nullptr, fStencilProgram->pipeline());
-        for (const auto& chunk : fPatchChunks) {
-            if (chunk.fPatchBuffer) {
-                flushState->bindBuffers(nullptr, nullptr, std::move(chunk.fPatchBuffer));
-                flushState->draw(chunk.fPatchCount, chunk.fBasePatch);
-            }
-        }
-    }
-    if (fFillProgram) {
-        flushState->bindPipelineAndScissorClip(*fFillProgram, this->bounds());
-        flushState->bindTextures(fFillProgram->primProc(), nullptr, fFillProgram->pipeline());
-        for (const auto& chunk : fPatchChunks) {
-            if (chunk.fPatchBuffer) {
-                flushState->bindBuffers(nullptr, nullptr, std::move(chunk.fPatchBuffer));
-                flushState->draw(chunk.fPatchCount, chunk.fBasePatch);
-            }
+void GrStrokeHardwareTessellator::draw(GrOpFlushState* flushState) const {
+    for (const auto& chunk : fPatchChunks) {
+        if (chunk.fPatchBuffer) {
+            flushState->bindBuffers(nullptr, nullptr, std::move(chunk.fPatchBuffer));
+            flushState->draw(chunk.fPatchCount, chunk.fBasePatch);
         }
     }
 }
diff --git a/src/gpu/tessellate/GrStrokeTessellateOp.h b/src/gpu/tessellate/GrStrokeHardwareTessellator.h
similarity index 78%
rename from src/gpu/tessellate/GrStrokeTessellateOp.h
rename to src/gpu/tessellate/GrStrokeHardwareTessellator.h
index 4459525..f04e206 100644
--- a/src/gpu/tessellate/GrStrokeTessellateOp.h
+++ b/src/gpu/tessellate/GrStrokeHardwareTessellator.h
@@ -5,8 +5,8 @@
  * found in the LICENSE file.
  */
 
-#ifndef GrStrokeTessellateOp_DEFINED
-#define GrStrokeTessellateOp_DEFINED
+#ifndef GrStrokeHardwareTessellator_DEFINED
+#define GrStrokeHardwareTessellator_DEFINED
 
 #include "include/core/SkStrokeRec.h"
 #include "src/gpu/tessellate/GrStrokeOp.h"
@@ -15,22 +15,16 @@
 // Renders opaque, constant-color strokes by decomposing them into standalone tessellation patches.
 // Each patch is either a "cubic" (single stroked bezier curve with butt caps) or a "join". Requires
 // MSAA if antialiasing is desired.
-class GrStrokeTessellateOp : public GrStrokeOp {
+class GrStrokeHardwareTessellator : public GrStrokeTessellator {
 public:
-    DEFINE_OP_CLASS_ID
+    GrStrokeHardwareTessellator(const GrShaderCaps&, const SkMatrix&, const SkStrokeRec& stroke);
+
+    void prepare(GrMeshDrawOp::Target*, const SkMatrix&, const GrSTArenaList<SkPath>&,
+                 const SkStrokeRec&, int totalCombinedVerbCnt) override;
+
+    void draw(GrOpFlushState*) const override;
 
 private:
-    // Patches can overlap, so until a stencil technique is implemented, the provided paint must be
-    // a constant blended color.
-    GrStrokeTessellateOp(GrAAType aaType, const SkMatrix& viewMatrix, const SkStrokeRec& stroke,
-                         const SkPath& path, GrPaint&& paint)
-            : GrStrokeOp(ClassID(), aaType, viewMatrix, stroke, path, std::move(paint)) {
-    }
-
-    void onPrePrepare(GrRecordingContext*, const GrSurfaceProxyView&, GrAppliedClip*,
-                      const GrXferProcessor::DstProxyView&, GrXferBarrierFlags,
-                      GrLoadOp colorLoadOp) override;
-
     enum class JoinType {
         kFromStroke,  // The shader will use the join type defined in our fStrokeRec.
         kBowtie,  // Double sided round join.
@@ -43,8 +37,6 @@
         kYes
     };
 
-    void onPrepare(GrOpFlushState*) override;
-    void prepareBuffers();
     void moveTo(SkPoint);
     void moveTo(SkPoint, SkPoint lastControlPoint);
     void lineTo(SkPoint, JoinType prevJoinType = JoinType::kFromStroke);
@@ -54,7 +46,7 @@
                  Convex180Status = Convex180Status::kUnknown, int maxDepth = -1);
     void joinTo(JoinType joinType, const SkPoint nextCubic[]) {
         const SkPoint& nextCtrlPt = (nextCubic[1] == nextCubic[0]) ? nextCubic[2] : nextCubic[1];
-        // The caller should have culled out cubics where p0==p1==p2 by this point.
+        // The caller should have culled out curves where p0==p1==p2 by this point.
         SkASSERT(nextCtrlPt != nextCubic[0]);
         this->joinTo(joinType, nextCtrlPt);
     }
@@ -66,28 +58,18 @@
     GrStrokeTessellateShader::Patch* reservePatch();
     void allocPatchChunkAtLeast(int minPatchAllocCount);
 
-    void onExecute(GrOpFlushState*, const SkRect& chainBounds) override;
-
-    // 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 {
-        sk_sp<const GrBuffer> fPatchBuffer;
-        int fPatchCount = 0;
-        int fBasePatch;
-    };
-    SkSTArray<1, PatchChunk> fPatchChunks;
-
-    // The target will be non-null during prepareBuffers. It is used to allocate vertex space for
-    // the patch chunks.
-    GrMeshDrawOp::Target* fTarget = nullptr;
-
     // The maximum number of tessellation segments the hardware can emit for a single patch.
-    int fMaxTessellationSegments;
+    const int fMaxTessellationSegments;
+    const SkStrokeRec fStroke;
 
     // Tolerances the tessellation shader will use for determining how much subdivision to do. We
     // need to ensure every curve we emit doesn't require more than fMaxTessellationSegments.
     GrStrokeTessellateShader::Tolerances fTolerances;
 
+    // The target and view matrix will only be non-null during prepare() and its callees.
+    GrMeshDrawOp::Target* fTarget = nullptr;
+    const SkMatrix* fViewMatrix = 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
@@ -100,6 +82,15 @@
     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 {
+        sk_sp<const GrBuffer> fPatchBuffer;
+        int fPatchCount = 0;
+        int fBasePatch;
+    };
+    SkSTArray<1, PatchChunk> fPatchChunks;
+
     // Variables related to the patch chunk that we are currently writing out during prepareBuffers.
     int fCurrChunkPatchCapacity;
     int fCurrChunkMinPatchAllocCount;
diff --git a/src/gpu/tessellate/GrStrokeIndirectOp.cpp b/src/gpu/tessellate/GrStrokeIndirectTessellator.cpp
similarity index 90%
rename from src/gpu/tessellate/GrStrokeIndirectOp.cpp
rename to src/gpu/tessellate/GrStrokeIndirectTessellator.cpp
index d7ab421..1e8080c 100644
--- a/src/gpu/tessellate/GrStrokeIndirectOp.cpp
+++ b/src/gpu/tessellate/GrStrokeIndirectTessellator.cpp
@@ -5,7 +5,7 @@
  * found in the LICENSE file.
  */
 
-#include "src/gpu/tessellate/GrStrokeIndirectOp.h"
+#include "src/gpu/tessellate/GrStrokeIndirectTessellator.h"
 
 #include "src/core/SkGeometry.h"
 #include "src/core/SkPathPriv.h"
@@ -16,29 +16,6 @@
 #include "src/gpu/tessellate/GrStrokeTessellateShader.h"
 #include "src/gpu/tessellate/GrWangsFormula.h"
 
-void GrStrokeIndirectOp::onPrePrepare(GrRecordingContext* context,
-                                      const GrSurfaceProxyView& writeView, GrAppliedClip* clip,
-                                      const GrXferProcessor::DstProxyView& dstProxyView,
-                                      GrXferBarrierFlags renderPassXferBarriers,
-                                      GrLoadOp colorLoadOp) {
-    auto* arena = context->priv().recordTimeAllocator();
-    this->prePrepareResolveLevels(arena);
-    SkASSERT(fResolveLevels);
-    if (!fTotalInstanceCount) {
-        return;
-    }
-    this->prePreparePrograms(GrStrokeTessellateShader::Mode::kIndirect, arena, writeView,
-                             (clip) ? std::move(*clip) : GrAppliedClip::Disabled(), dstProxyView,
-                             renderPassXferBarriers, colorLoadOp, *context->priv().caps());
-    if (fFillProgram) {
-        context->priv().recordProgramInfo(fFillProgram);
-    }
-    if (fStencilProgram) {
-        context->priv().recordProgramInfo(fStencilProgram);
-    }
-}
-
-// Helpers for GrStrokeIndirectOp::prePrepareResolveLevels.
 namespace {
 
 // Only use SIMD if SkVx will use a built-in compiler extensions for vectors.
@@ -152,7 +129,7 @@
         float numCombinedSegments =
                 fTolerances.fNumRadialSegmentsPerRadian * rotation + numParametricSegments;
         int8_t resolveLevel = sk_float_nextlog2(numCombinedSegments);
-        resolveLevel = std::min(resolveLevel, GrStrokeIndirectOp::kMaxResolveLevel);
+        resolveLevel = std::min(resolveLevel, GrStrokeIndirectTessellator::kMaxResolveLevel);
         ++fResolveLevelCounts[(*resolveLevelPtr = resolveLevel)];
     }
 
@@ -408,7 +385,7 @@
         bits += (1u << 23) - 1u;  // Increment the exponent for non-powers-of-2.
         // This will make negative values, denorms, and negative exponents all < 0.
         auto exp = (skvx::bit_pun<ivec<N>>(bits) >> 23) - 127;
-        auto level = skvx::pin<N,int>(exp, 0, GrStrokeIndirectOp::kMaxResolveLevel);
+        auto level = skvx::pin<N,int>(exp, 0, GrStrokeIndirectTessellator::kMaxResolveLevel);
 
         switch (count) {
             default: SkUNREACHABLE;
@@ -438,7 +415,9 @@
 
 }  // namespace
 
-void GrStrokeIndirectOp::prePrepareResolveLevels(SkArenaAlloc* alloc) {
+GrStrokeIndirectTessellator::GrStrokeIndirectTessellator(
+        const SkMatrix& viewMatrix, const GrSTArenaList<SkPath>& pathList,
+        const SkStrokeRec& stroke, int totalCombinedVerbCnt, SkArenaAlloc* alloc) {
     SkASSERT(!fTotalInstanceCount);
     SkASSERT(!fResolveLevels);
     SkASSERT(!fResolveLevelArrayCount);
@@ -450,24 +429,24 @@
     //   * Plus 1 extra resolveLevel per verb that says how many chops it needs
     //   * Plus 2 final resolveLevels for square caps at the very end not initiated by a "kMoveTo".
     //
-    int resolveLevelAllocCount = fTotalCombinedVerbCnt * (3 + 1) + 2;
+    int resolveLevelAllocCount = totalCombinedVerbCnt * (3 + 1) + 2;
     fResolveLevels = alloc->makeArrayDefault<int8_t>(resolveLevelAllocCount);
     int8_t* nextResolveLevel = fResolveLevels;
 
     // The maximum potential number of chopT values we will need is 2 per verb.
-    int chopTAllocCount = fTotalCombinedVerbCnt * 2;
+    int chopTAllocCount = totalCombinedVerbCnt * 2;
     fChopTs = alloc->makeArrayDefault<float>(chopTAllocCount);
     float* nextChopTs = fChopTs;
 
-    auto tolerances = this->preTransformTolerances();
+    auto tolerances = GrStrokeTessellateShader::Tolerances::MakePreTransform(viewMatrix, stroke);
     fResolveLevelForCircles =
             std::max(sk_float_nextlog2(tolerances.fNumRadialSegmentsPerRadian * SK_ScalarPI), 1);
-    ResolveLevelCounter counter(fStroke, tolerances, fResolveLevelCounts);
+    ResolveLevelCounter counter(stroke, tolerances, fResolveLevelCounts);
 
     SkPoint lastControlPoint = {0,0};
-    for (const SkPath& path : fPathList) {
+    for (const SkPath& path : pathList) {
         // Iterate through each verb in the stroke, counting its resolveLevel(s).
-        GrStrokeIterator iter(path, &fStroke, &fViewMatrix);
+        GrStrokeIterator iter(path, &stroke, &viewMatrix);
         while (iter.next()) {
             using Verb = GrStrokeIterator::Verb;
             Verb verb = iter.verb();
@@ -597,23 +576,6 @@
 #endif
 }
 
-void GrStrokeIndirectOp::onPrepare(GrOpFlushState* flushState) {
-    if (!fResolveLevels) {
-        auto* arena = flushState->allocator();
-        this->prePrepareResolveLevels(arena);
-        if (!fTotalInstanceCount) {
-            return;
-        }
-        this->prePreparePrograms(GrStrokeTessellateShader::Mode::kIndirect, arena,
-                                 flushState->writeView(), flushState->detachAppliedClip(),
-                                 flushState->dstProxyView(), flushState->renderPassBarriers(),
-                                 flushState->colorLoadOp(), flushState->caps());
-    }
-    SkASSERT(fResolveLevels);
-
-    this->prepareBuffers(flushState);
-}
-
 constexpr static int num_edges_in_resolve_level(int resolveLevel) {
     // A resolveLevel means the instance is composed of 2^resolveLevel line segments.
     int numSegments = 1 << resolveLevel;
@@ -623,7 +585,9 @@
     return numStrokeEdges;
 }
 
-void GrStrokeIndirectOp::prepareBuffers(GrMeshDrawOp::Target* target) {
+void GrStrokeIndirectTessellator::prepare(GrMeshDrawOp::Target* target, const SkMatrix& viewMatrix,
+                                          const GrSTArenaList<SkPath>& pathList,
+                                          const SkStrokeRec& stroke, int totalCombinedVerbCnt) {
     using IndirectInstance = GrStrokeTessellateShader::IndirectInstance;
 
     SkASSERT(fResolveLevels);
@@ -655,7 +619,7 @@
     }
 
     // Fill out our drawIndirect commands and determine the layout of the instance buffer.
-    int numExtraEdgesInJoin = IndirectInstance::NumExtraEdgesInJoin(fStroke.getJoin());
+    int numExtraEdgesInJoin = IndirectInstance::NumExtraEdgesInJoin(stroke.getJoin());
     int currentInstanceIdx = 0;
     float numEdgesPerResolveLevel[kMaxResolveLevel];
     IndirectInstance* nextInstanceLocations[kMaxResolveLevel + 1];
@@ -688,7 +652,7 @@
     SkPoint scratchBuffer[4 + 10];
     SkPoint* scratch = scratchBuffer;
 
-    bool isRoundJoin = (fStroke.getJoin() == SkPaint::kRound_Join);
+    bool isRoundJoin = (stroke.getJoin() == SkPaint::kRound_Join);
     int8_t* nextResolveLevel = fResolveLevels;
     float* nextChopTs = fChopTs;
 
@@ -698,8 +662,8 @@
     int8_t resolveLevel;
 
     // Now write out each instance to its resolveLevel's designated location in the instance buffer.
-    for (const SkPath& path : fPathList) {
-        GrStrokeIterator iter(path, &fStroke, &fViewMatrix);
+    for (const SkPath& path : pathList) {
+        GrStrokeIterator iter(path, &stroke, &viewMatrix);
         bool hasLastControlPoint = false;
         while (iter.next()) {
             using Verb = GrStrokeIterator::Verb;
@@ -847,27 +811,14 @@
 #endif
 }
 
-void GrStrokeIndirectOp::onExecute(GrOpFlushState* flushState, const SkRect& chainBounds) {
+void GrStrokeIndirectTessellator::draw(GrOpFlushState* flushState) const {
     if (!fInstanceBuffer) {
         return;
     }
 
     SkASSERT(fDrawIndirectCount);
     SkASSERT(fTotalInstanceCount > 0);
-    SkASSERT(chainBounds == this->bounds());
 
-    if (fStencilProgram) {
-        flushState->bindPipelineAndScissorClip(*fStencilProgram, this->bounds());
-        flushState->bindTextures(fStencilProgram->primProc(), nullptr, fStencilProgram->pipeline());
-        flushState->bindBuffers(nullptr, fInstanceBuffer, nullptr);
-        flushState->drawIndirect(fDrawIndirectBuffer.get(), fDrawIndirectOffset,
-                                 fDrawIndirectCount);
-    }
-    if (fFillProgram) {
-        flushState->bindPipelineAndScissorClip(*fFillProgram, this->bounds());
-        flushState->bindTextures(fFillProgram->primProc(), nullptr, fFillProgram->pipeline());
-        flushState->bindBuffers(nullptr, fInstanceBuffer, nullptr);
-        flushState->drawIndirect(fDrawIndirectBuffer.get(), fDrawIndirectOffset,
-                                 fDrawIndirectCount);
-    }
+    flushState->bindBuffers(nullptr, fInstanceBuffer, nullptr);
+    flushState->drawIndirect(fDrawIndirectBuffer.get(), fDrawIndirectOffset, fDrawIndirectCount);
 }
diff --git a/src/gpu/tessellate/GrStrokeIndirectOp.h b/src/gpu/tessellate/GrStrokeIndirectTessellator.h
similarity index 66%
rename from src/gpu/tessellate/GrStrokeIndirectOp.h
rename to src/gpu/tessellate/GrStrokeIndirectTessellator.h
index 10b5c1a..39267d9 100644
--- a/src/gpu/tessellate/GrStrokeIndirectOp.h
+++ b/src/gpu/tessellate/GrStrokeIndirectTessellator.h
@@ -5,43 +5,31 @@
  * found in the LICENSE file.
  */
 
-#ifndef GrStrokeIndirectOp_DEFINED
-#define GrStrokeIndirectOp_DEFINED
+#ifndef GrStrokeIndirectTessellator_DEFINED
+#define GrStrokeIndirectTessellator_DEFINED
 
-#include "src/gpu/ops/GrMeshDrawOp.h"
 #include "src/gpu/tessellate/GrStrokeOp.h"
 
 struct SkPoint;
 namespace skiatest { class Reporter; }
 
 // This class bins strokes into indirect draws for consumption by GrStrokeTessellateShader.
-class GrStrokeIndirectOp : public GrStrokeOp {
+class GrStrokeIndirectTessellator : public GrStrokeTessellator {
 public:
-    DEFINE_OP_CLASS_ID
-
     // Don't allow more than 2^15 stroke edges in a triangle strip. GrTessellationPathRenderer
     // already crops paths that require more than 2^10 parametric segments, so this should only
     // become an issue if we try to draw a stroke with an astronomically wide width.
     constexpr static int8_t kMaxResolveLevel = 15;
 
+    GrStrokeIndirectTessellator(const SkMatrix&, const GrSTArenaList<SkPath>&,
+                                const SkStrokeRec&, int totalCombinedVerbCnt, SkArenaAlloc*);
+
+    void prepare(GrMeshDrawOp::Target*, const SkMatrix&, const GrSTArenaList<SkPath>&,
+                 const SkStrokeRec&, int totalCombinedVerbCnt) override;
+
+    void draw(GrOpFlushState*) const override;
+
 private:
-    GrStrokeIndirectOp(GrAAType aaType, const SkMatrix& viewMatrix, const SkPath& path,
-                       const SkStrokeRec& stroke, GrPaint&& paint)
-            : GrStrokeOp(ClassID(), aaType, viewMatrix, stroke, path, std::move(paint)) {
-    }
-
-    const char* name() const override { return "GrStrokeIndirectOp"; }
-
-    void onPrePrepare(GrRecordingContext*, const GrSurfaceProxyView&, GrAppliedClip*,
-                      const GrXferProcessor::DstProxyView&, GrXferBarrierFlags,
-                      GrLoadOp colorLoadOp) override;
-    void prePrepareResolveLevels(SkArenaAlloc*);
-
-    void onPrepare(GrOpFlushState*) override;
-    void prepareBuffers(GrMeshDrawOp::Target*);
-
-    void onExecute(GrOpFlushState*, const SkRect& chainBounds) override;
-
     int fResolveLevelCounts[kMaxResolveLevel + 1] = {0};  // # of instances at each resolve level.
     int fTotalInstanceCount = 0;  // Total number of stroke instances we will draw.
 
@@ -73,8 +61,10 @@
 
 #if GR_TEST_UTILS
 public:
-    void verifyPrePrepareResolveLevels(skiatest::Reporter*, GrMeshDrawOp::Target*);
-    void verifyPrepareBuffers(skiatest::Reporter*, GrMeshDrawOp::Target*);
+    void verifyResolveLevels(skiatest::Reporter*, GrMeshDrawOp::Target*, const SkMatrix&,
+                             const SkPath&, const SkStrokeRec&);
+    void verifyBuffers(skiatest::Reporter*, GrMeshDrawOp::Target*, const SkMatrix&,
+                       const SkStrokeRec&);
     class Benchmark;
 #endif
 };
diff --git a/src/gpu/tessellate/GrStrokeOp.cpp b/src/gpu/tessellate/GrStrokeOp.cpp
index 29f1128..396f289 100644
--- a/src/gpu/tessellate/GrStrokeOp.cpp
+++ b/src/gpu/tessellate/GrStrokeOp.cpp
@@ -9,11 +9,14 @@
 
 #include "src/core/SkPathPriv.h"
 #include "src/gpu/GrRecordingContextPriv.h"
-#include "src/gpu/tessellate/GrStrokeTessellateOp.h"
+#include "src/gpu/tessellate/GrFillPathShader.h"
+#include "src/gpu/tessellate/GrStencilPathShader.h"
+#include "src/gpu/tessellate/GrStrokeHardwareTessellator.h"
+#include "src/gpu/tessellate/GrStrokeIndirectTessellator.h"
 
-GrStrokeOp::GrStrokeOp(uint32_t classID, GrAAType aaType, const SkMatrix& viewMatrix,
-                       const SkStrokeRec& stroke, const SkPath& path, GrPaint&& paint)
-        : GrDrawOp(classID)
+GrStrokeOp::GrStrokeOp(GrAAType aaType, const SkMatrix& viewMatrix, const SkPath& path,
+                       const SkStrokeRec& stroke, GrPaint&& paint)
+        : GrDrawOp(ClassID())
         , fAAType(aaType)
         , fViewMatrix(viewMatrix)
         , fStroke(stroke)
@@ -21,7 +24,7 @@
         , fProcessors(std::move(paint))
         , fPathList(path)
         , fTotalCombinedVerbCnt(path.countVerbs())
-        , fTotalConicWeightCnt(SkPathPriv::ConicWeightCnt(path)) {
+        , fHasConics(SkPathPriv::ConicWeightCnt(path) != 0) {
     SkRect devBounds = path.getBounds();
     float inflationRadius = fStroke.getInflationRadius();
     devBounds.outset(inflationRadius, inflationRadius);
@@ -76,7 +79,7 @@
 
     fPathList.concat(std::move(op->fPathList), alloc);
     fTotalCombinedVerbCnt += op->fTotalCombinedVerbCnt;
-    fTotalConicWeightCnt += op->fTotalConicWeightCnt;
+    fHasConics |= op->fHasConics;
 
     return CombineResult::kMerged;
 }
@@ -103,63 +106,95 @@
         GrUserStencilOp::kReplace,
         0xffff>());
 
-void GrStrokeOp::prePreparePrograms(GrStrokeTessellateShader::Mode shaderMode, SkArenaAlloc* arena,
-                                    const GrSurfaceProxyView& writeView, GrAppliedClip&& clip,
-                                    const GrXferProcessor::DstProxyView& dstProxyView,
-                                    GrXferBarrierFlags renderPassXferBarriers,
-                                    GrLoadOp colorLoadOp, const GrCaps& caps) {
-    using InputFlags = GrPipeline::InputFlags;
+void GrStrokeOp::prePrepareTessellator(GrPathShader::ProgramArgs&& args, GrAppliedClip&& clip) {
+    SkASSERT(!fTessellator);
     SkASSERT(!fFillProgram);
     SkASSERT(!fStencilProgram);
 
-    // This will be created iff the stencil pass can't share a pipeline with the fill pass.
-    GrPipeline* standaloneStencilPipeline = nullptr;
+    const GrCaps& caps = *args.fCaps;
+    SkArenaAlloc* arena = args.fArena;
 
-    GrPipeline::InitArgs fillArgs;
-    fillArgs.fCaps = &caps;
-    fillArgs.fDstProxyView = dstProxyView;
-    fillArgs.fWriteSwizzle = writeView.swizzle();
-    if (fAAType != GrAAType::kNone) {
-        if (writeView.asRenderTargetProxy()->numSamples() == 1) {
-            // We are mixed sampled. We need to either enable conservative raster (preferred) or
-            // disable MSAA in order to avoid double blend artifacts. (Even if we disable MSAA for
-            // the cover geometry, the stencil test is still multisampled and will still produce
-            // smooth results.)
-            SkASSERT(GrAAType::kCoverage == fAAType);
-            if (caps.conservativeRasterSupport()) {
-                fillArgs.fInputFlags |= InputFlags::kHWAntialias | InputFlags::kConservativeRaster;
-            }
-            // Since we either need conservative raster enabled or MSAA disabled during fill, we
-            // need a separate pipeline for the stencil pass.
-            SkASSERT(fNeedsStencil);  // Mixed samples always needs stencil.
-            GrPipeline::InitArgs stencilArgs;
-            stencilArgs.fCaps = &caps;
-            stencilArgs.fInputFlags = InputFlags::kHWAntialias;
-            stencilArgs.fWriteSwizzle = writeView.swizzle();
-            standaloneStencilPipeline = arena->make<GrPipeline>(
-                    stencilArgs, GrDisableColorXPFactory::MakeXferProcessor(), clip.hardClip());
-        } else {
-            // We are standard MSAA. Leave MSAA enabled for both the fill and stencil passes.
-            fillArgs.fInputFlags |= InputFlags::kHWAntialias;
-        }
+    // Only use hardware tessellation if the path has a somewhat large number of verbs. Otherwise we
+    // seem to be better off using indirect draws. Our back door for HW tessellation shaders isn't
+    // currently capable of passing varyings to the fragment shader either, so if the processors
+    // have varyings we need to use indirect draws.
+    GrStrokeTessellateShader::Mode shaderMode;
+    if (caps.shaderCaps()->tessellationSupport() &&
+        fTotalCombinedVerbCnt > 50 &&
+        !fProcessors.usesVaryingCoords()) {
+        fTessellator = arena->make<GrStrokeHardwareTessellator>(*caps.shaderCaps(), fViewMatrix,
+                                                                fStroke);
+        shaderMode = GrStrokeTessellateShader::Mode::kTessellation;
+    } else {
+        fTessellator = arena->make<GrStrokeIndirectTessellator>(fViewMatrix, fPathList, fStroke,
+                                                                fTotalCombinedVerbCnt, arena);
+        shaderMode = GrStrokeTessellateShader::Mode::kIndirect;
+    }
+
+    // If we are mixed sampled then we need a separate pipeline for the stencil pass. This is
+    // because mixed samples either needs conservative raster enabled or MSAA disabled during fill.
+    const GrPipeline* mixedSampledStencilPipeline = nullptr;
+    if (fAAType == GrAAType::kCoverage) {
+        SkASSERT(args.fWriteView.asRenderTargetProxy()->numSamples() == 1);
+        SkASSERT(fNeedsStencil);  // Mixed samples always needs stencil.
+        mixedSampledStencilPipeline = GrStencilPathShader::MakeStencilPassPipeline(
+                args, fAAType, GrTessellationPathRenderer::OpFlags::kNone, clip.hardClip());
     }
 
     auto* strokeTessellateShader = arena->make<GrStrokeTessellateShader>(
-            shaderMode, fTotalConicWeightCnt, fStroke, fViewMatrix, fColor);
-    auto fillPipeline = arena->make<GrPipeline>(fillArgs, std::move(fProcessors), std::move(clip));
+            shaderMode, fHasConics, fStroke, fViewMatrix, fColor);
+    auto* fillPipeline = GrFillPathShader::MakeFillPassPipeline(args, fAAType, std::move(clip),
+                                                                std::move(fProcessors));
     auto fillStencil = &GrUserStencilSettings::kUnused;
-    auto fillXferFlags = renderPassXferBarriers;
     if (fNeedsStencil) {
-        auto* stencilPipeline = (standaloneStencilPipeline) ? standaloneStencilPipeline
-                                                            : fillPipeline;
-        fStencilProgram = GrPathShader::MakeProgramInfo(strokeTessellateShader, arena, writeView,
-                                                        stencilPipeline, dstProxyView,
-                                                        renderPassXferBarriers, colorLoadOp,
-                                                        &kMarkStencil, caps);
+        auto* stencilPipeline = (mixedSampledStencilPipeline) ? mixedSampledStencilPipeline
+                                                              : fillPipeline;
+        fStencilProgram = GrPathShader::MakeProgram(args, strokeTessellateShader, stencilPipeline,
+                                                    &kMarkStencil);
         fillStencil = &kTestAndResetStencil;
-        fillXferFlags = GrXferBarrierFlags::kNone;
+        args.fXferBarrierFlags = GrXferBarrierFlags::kNone;
     }
-    fFillProgram = GrPathShader::MakeProgramInfo(strokeTessellateShader, arena, writeView,
-                                                 fillPipeline, dstProxyView, fillXferFlags,
-                                                 colorLoadOp, fillStencil, caps);
+
+    fFillProgram = GrPathShader::MakeProgram(args, strokeTessellateShader, fillPipeline,
+                                             fillStencil);
+}
+
+void GrStrokeOp::onPrePrepare(GrRecordingContext* context, const GrSurfaceProxyView& writeView,
+                              GrAppliedClip* clip,
+                              const GrXferProcessor::DstProxyView& dstProxyView,
+                              GrXferBarrierFlags renderPassXferBarriers, GrLoadOp colorLoadOp) {
+    this->prePrepareTessellator({context->priv().recordTimeAllocator(), writeView, &dstProxyView,
+                                renderPassXferBarriers, colorLoadOp, context->priv().caps()},
+                                (clip) ? std::move(*clip) : GrAppliedClip::Disabled());
+    if (fStencilProgram) {
+        context->priv().recordProgramInfo(fStencilProgram);
+    }
+    if (fFillProgram) {
+        context->priv().recordProgramInfo(fFillProgram);
+    }
+}
+
+void GrStrokeOp::onPrepare(GrOpFlushState* flushState) {
+    if (!fTessellator) {
+        this->prePrepareTessellator({flushState->allocator(), flushState->writeView(),
+                                    &flushState->dstProxyView(), flushState->renderPassBarriers(),
+                                    flushState->colorLoadOp(), &flushState->caps()},
+                                    flushState->detachAppliedClip());
+    }
+    SkASSERT(fTessellator);
+    fTessellator->prepare(flushState, fViewMatrix, fPathList, fStroke, fTotalCombinedVerbCnt);
+}
+
+void GrStrokeOp::onExecute(GrOpFlushState* flushState, const SkRect& chainBounds) {
+    SkASSERT(chainBounds == this->bounds());
+    if (fStencilProgram) {
+        flushState->bindPipelineAndScissorClip(*fStencilProgram, this->bounds());
+        flushState->bindTextures(fStencilProgram->primProc(), nullptr, fStencilProgram->pipeline());
+        fTessellator->draw(flushState);
+    }
+    if (fFillProgram) {
+        flushState->bindPipelineAndScissorClip(*fFillProgram, this->bounds());
+        flushState->bindTextures(fFillProgram->primProc(), nullptr, fFillProgram->pipeline());
+        fTessellator->draw(flushState);
+    }
 }
diff --git a/src/gpu/tessellate/GrStrokeOp.h b/src/gpu/tessellate/GrStrokeOp.h
index c5adc91..6a2933d 100644
--- a/src/gpu/tessellate/GrStrokeOp.h
+++ b/src/gpu/tessellate/GrStrokeOp.h
@@ -9,85 +9,57 @@
 #define GrStrokeOp_DEFINED
 
 #include "include/core/SkStrokeRec.h"
-#include "include/gpu/GrRecordingContext.h"
 #include "src/gpu/GrSTArenaList.h"
-#include "src/gpu/ops/GrDrawOp.h"
-#include "src/gpu/tessellate/GrStrokeTessellateShader.h"
-#include <array>
+#include "src/gpu/ops/GrMeshDrawOp.h"
+#include "src/gpu/tessellate/GrPathShader.h"
 
-class GrStrokeTessellateShader;
+class GrRecordingContext;
+
+// Prepares GPU data for, and then draws a stroke's tessellated geometry.
+class GrStrokeTessellator {
+public:
+    // Called before draw(). Prepares GPU buffers containing the geometry to tessellate.
+    virtual void prepare(GrMeshDrawOp::Target*, const SkMatrix&, const GrSTArenaList<SkPath>&,
+                         const SkStrokeRec&, int totalCombinedVerbCnt) = 0;
+
+    // Issues draw calls for the tessellated stroie. The caller is responsible for binding its
+    // desired pipeline ahead of time.
+    virtual void draw(GrOpFlushState*) const = 0;
+
+    virtual ~GrStrokeTessellator() {}
+};
 
 // Base class for ops that render opaque, constant-color strokes by linearizing them into sorted
 // "parametric" and "radial" edges. See GrStrokeTessellateShader.
 class GrStrokeOp : public GrDrawOp {
-protected:
+public:
     // The provided matrix must be a similarity matrix for the time being. This is so we can
     // bootstrap this Op on top of GrStrokeGeometry with minimal modifications.
     //
     // Patches can overlap, so until a stencil technique is implemented, the provided paint must be
     // a constant blended color.
-    GrStrokeOp(uint32_t classID, GrAAType, const SkMatrix&, const SkStrokeRec&, const SkPath&,
-               GrPaint&&);
+    GrStrokeOp(GrAAType, const SkMatrix&, const SkPath&, const SkStrokeRec&, GrPaint&&);
 
-    const char* name() const override { return "GrStrokeTessellateOp"; }
+protected:
+    DEFINE_OP_CLASS_ID
+
+    const char* name() const override { return "GrStrokeOp"; }
     void visitProxies(const VisitProxyFunc& fn) const override;
     FixedFunctionFlags fixedFunctionFlags() const override;
     GrProcessorSet::Analysis finalize(const GrCaps&, const GrAppliedClip*,
                                       bool hasMixedSampledCoverage, GrClampType) override;
     CombineResult onCombineIfPossible(GrOp*, SkArenaAlloc*, const GrCaps&) override;
 
-    void prePreparePrograms(GrStrokeTessellateShader::Mode, SkArenaAlloc*,
-                            const GrSurfaceProxyView&, GrAppliedClip&&,
-                            const GrXferProcessor::DstProxyView&, GrXferBarrierFlags,
-                            GrLoadOp colorLoadOp, const GrCaps&);
+    // Creates the tessellator and the stencil/fill program(s) we will use with it.
+    void prePrepareTessellator(GrPathShader::ProgramArgs&&, GrAppliedClip&&);
 
-    static float NumCombinedSegments(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;
-    }
+    void onPrePrepare(GrRecordingContext*, const GrSurfaceProxyView&, GrAppliedClip*,
+                      const GrXferProcessor::DstProxyView&, GrXferBarrierFlags,
+                      GrLoadOp colorLoadOp) override;
 
-    static float NumParametricSegments(float numCombinedSegments, float numRadialSegments) {
-        // numCombinedSegments = numParametricSegments + numRadialSegments - 1.
-        // (See num_combined_segments()).
-        return std::max(numCombinedSegments + 1 - numRadialSegments, 0.f);
-    }
+    void onPrepare(GrOpFlushState*) override;
 
-    // Returns the equivalent tolerances in (pre-viewMatrix) local path space that the tessellator
-    // will use when rendering this stroke.
-    GrStrokeTessellateShader::Tolerances preTransformTolerances() const {
-        std::array<float,2> matrixScales;
-        if (!fViewMatrix.getMinMaxScales(matrixScales.data())) {
-            matrixScales.fill(1);
-        }
-        auto [matrixMinScale, matrixMaxScale] = matrixScales;
-        float localStrokeWidth = fStroke.getWidth();
-        if (fStroke.isHairlineStyle()) {
-            // If the stroke is hairline then the tessellator will operate in post-transform space
-            // instead. But for the sake of CPU methods that need to conservatively approximate the
-            // number of segments to emit, we use localStrokeWidth ~= 1/matrixMinScale.
-            float approxScale = matrixMinScale;
-            // If the matrix has strong skew, don't let the scale shoot off to infinity. (This does
-            // not affect the tessellator; only the CPU methods that approximate the number of
-            // segments to emit.)
-            approxScale = std::max(matrixMinScale, matrixMaxScale * .25f);
-            localStrokeWidth = 1/approxScale;
-        }
-        return GrStrokeTessellateShader::Tolerances(matrixMaxScale, localStrokeWidth);
-    }
+    void onExecute(GrOpFlushState*, const SkRect& chainBounds) override;
 
     const GrAAType fAAType;
     const SkMatrix fViewMatrix;
@@ -98,9 +70,10 @@
 
     GrSTArenaList<SkPath> fPathList;
     int fTotalCombinedVerbCnt = 0;
-    int fTotalConicWeightCnt = 0;
+    bool fHasConics = false;
 
-    const GrProgramInfo* fStencilProgram = nullptr;
+    GrStrokeTessellator* fTessellator = nullptr;
+    const GrProgramInfo* fStencilProgram = nullptr;  // Only used if the stroke has transparency.
     const GrProgramInfo* fFillProgram = nullptr;
 };
 
diff --git a/src/gpu/tessellate/GrStrokeTessellateShader.h b/src/gpu/tessellate/GrStrokeTessellateShader.h
index b3d2fba..70e6579 100644
--- a/src/gpu/tessellate/GrStrokeTessellateShader.h
+++ b/src/gpu/tessellate/GrStrokeTessellateShader.h
@@ -101,6 +101,29 @@
     // These tolerances decide the number of parametric and radial segments the tessellator will
     // linearize curves into. These decisions are made in (pre-viewMatrix) local path space.
     struct Tolerances {
+        // Returns the equivalent tolerances in (pre-viewMatrix) local path space that the
+        // tessellator will use when rendering this stroke.
+        static Tolerances MakePreTransform(const SkMatrix& viewMatrix, const SkStrokeRec& stroke) {
+            std::array<float,2> matrixScales;
+            if (!viewMatrix.getMinMaxScales(matrixScales.data())) {
+                matrixScales.fill(1);
+            }
+            auto [matrixMinScale, matrixMaxScale] = matrixScales;
+            float localStrokeWidth = stroke.getWidth();
+            if (stroke.isHairlineStyle()) {
+                // If the stroke is hairline then the tessellator will operate in post-transform
+                // space instead. But for the sake of CPU methods that need to conservatively
+                // approximate the number of segments to emit, we use
+                // localStrokeWidth ~= 1/matrixMinScale.
+                float approxScale = matrixMinScale;
+                // If the matrix has strong skew, don't let the scale shoot off to infinity. (This
+                // does not affect the tessellator; only the CPU methods that approximate the number
+                // of segments to emit.)
+                approxScale = std::max(matrixMinScale, matrixMaxScale * .25f);
+                localStrokeWidth = 1/approxScale;
+            }
+            return GrStrokeTessellateShader::Tolerances(matrixMaxScale, localStrokeWidth);
+        }
         Tolerances() = default;
         Tolerances(float matrixMaxScale, float strokeWidth) {
             this->set(matrixMaxScale, strokeWidth);
diff --git a/src/gpu/tessellate/GrTessellationPathRenderer.cpp b/src/gpu/tessellate/GrTessellationPathRenderer.cpp
index 692a8fd..eddcb4e 100644
--- a/src/gpu/tessellate/GrTessellationPathRenderer.cpp
+++ b/src/gpu/tessellate/GrTessellationPathRenderer.cpp
@@ -18,8 +18,7 @@
 #include "src/gpu/ops/GrFillRectOp.h"
 #include "src/gpu/tessellate/GrDrawAtlasPathOp.h"
 #include "src/gpu/tessellate/GrPathInnerTriangulateOp.h"
-#include "src/gpu/tessellate/GrStrokeIndirectOp.h"
-#include "src/gpu/tessellate/GrStrokeTessellateOp.h"
+#include "src/gpu/tessellate/GrStrokeOp.h"
 #include "src/gpu/tessellate/GrTessellatingStencilFillOp.h"
 #include "src/gpu/tessellate/GrWangsFormula.h"
 
@@ -212,19 +211,7 @@
     if (!shape.style().isSimpleFill()) {
         const SkStrokeRec& stroke = shape.style().strokeRec();
         SkASSERT(stroke.getStyle() != SkStrokeRec::kStrokeAndFill_Style);
-        // Only use hardware tessellation if the path has a somewhat large number of verbs.
-        // Otherwise we seem to be better off using indirect draws. Our back door for HW
-        // tessellation shaders isn't currently capable of passing varyings to the fragment shader
-        // either, so if the paint uses varyings we need to use indirect draws.
-        if (shaderCaps.tessellationSupport() &&
-            path.countVerbs() > 50 &&
-            !paint.usesVaryingCoords()) {
-            return GrOp::Make<GrStrokeTessellateOp>(rContext, aaType, viewMatrix, stroke, path,
-                                                    std::move(paint));
-        } else {
-            return GrOp::Make<GrStrokeIndirectOp>(rContext, aaType, viewMatrix, path, stroke,
-                                                  std::move(paint));
-        }
+        return GrOp::Make<GrStrokeOp>(rContext, aaType, viewMatrix, path, stroke, std::move(paint));
     } else {
         if ((1 << worstCaseResolveLevel) > shaderCaps.maxTessellationSegments()) {
             // The path is too large for hardware tessellation; a curve in this bounding box could
diff --git a/tests/StrokeIndirectTest.cpp b/tests/StrokeIndirectTest.cpp
index 3c5d977..df56789 100644
--- a/tests/StrokeIndirectTest.cpp
+++ b/tests/StrokeIndirectTest.cpp
@@ -11,7 +11,7 @@
 #include "src/core/SkGeometry.h"
 #include "src/gpu/geometry/GrPathUtils.h"
 #include "src/gpu/mock/GrMockOpTarget.h"
-#include "src/gpu/tessellate/GrStrokeIndirectOp.h"
+#include "src/gpu/tessellate/GrStrokeIndirectTessellator.h"
 #include "src/gpu/tessellate/GrStrokeTessellateShader.h"
 #include "src/gpu/tessellate/GrTessellationPathRenderer.h"
 #include "src/gpu/tessellate/GrWangsFormula.h"
@@ -40,17 +40,17 @@
         stroke.setStrokeParams(SkPaint::kButt_Cap, join, 4);
         for (int i = 0; i < 16; ++i) {
             float scale = ldexpf(rand.nextF() + 1, i);
-            auto op = GrOp::Make<GrStrokeIndirectOp>(ctx, GrAAType::kMSAA,
-                                                     SkMatrix::Scale(scale, scale), path, stroke,
-                                                     GrPaint());
-            auto strokeIndirectOp = op->cast<GrStrokeIndirectOp>();
-            strokeIndirectOp->verifyPrePrepareResolveLevels(r, target);
-            strokeIndirectOp->verifyPrepareBuffers(r, target);
+            auto matrix = SkMatrix::Scale(scale, scale);
+            GrStrokeIndirectTessellator tessellator(matrix, path, stroke, path.countVerbs(),
+                                                    target->allocator());
+            tessellator.verifyResolveLevels(r, target, matrix, path, stroke);
+            tessellator.prepare(target, matrix, path, stroke, path.countVerbs());
+            tessellator.verifyBuffers(r, target, matrix, stroke);
         }
     }
 }
 
-DEF_TEST(tessellate_GrStrokeIndirectOp, r) {
+DEF_TEST(tessellate_GrStrokeIndirectTessellator, r) {
     auto ctx = make_mock_context();
     auto target = std::make_unique<GrMockOpTarget>(ctx);
     SkRandom rand;
@@ -262,180 +262,180 @@
     return tolerance;
 }
 
-void GrStrokeIndirectOp::verifyPrePrepareResolveLevels(skiatest::Reporter* r,
-                                                       GrMeshDrawOp::Target* target) {
-    GrStrokeTessellateShader::Tolerances tolerances(fViewMatrix.getMaxScale(), fStroke.getWidth());
-    float tolerance = test_tolerance(fStroke.getJoin());
-    // Fill in fResolveLevels with our resolve levels for each curve.
-    this->prePrepareResolveLevels(target->allocator());
+void GrStrokeIndirectTessellator::verifyResolveLevels(skiatest::Reporter* r,
+                                                      GrMeshDrawOp::Target* target,
+                                                      const SkMatrix& viewMatrix,
+                                                      const SkPath& path,
+                                                      const SkStrokeRec& stroke) {
+    GrStrokeTessellateShader::Tolerances tolerances(viewMatrix.getMaxScale(), stroke.getWidth());
+    float tolerance = test_tolerance(stroke.getJoin());
     int8_t* nextResolveLevel = fResolveLevels;
-    // Now check out answers.
-    for (const SkPath& path : fPathList) {
-        auto iterate = SkPathPriv::Iterate(path);
-        SkSTArray<3, float> firstNumSegments;
-        bool isFirstStroke = true;
-        SkPoint startPoint = {0,0};
-        SkPoint lastControlPoint;
-        for (auto iter = iterate.begin(); iter != iterate.end(); ++iter) {
-            auto [verb, pts, w] = *iter;
-            switch (verb) {
-                int n;
-                SkPoint chops[10];
-                case SkPathVerb::kMove:
-                    startPoint = pts[0];
-                    lastControlPoint = get_contour_closing_control_point(iter, iterate.end());
-                    if (!check_first_resolve_levels(r, firstNumSegments, &nextResolveLevel,
+    auto iterate = SkPathPriv::Iterate(path);
+    SkSTArray<3, float> firstNumSegments;
+    bool isFirstStroke = true;
+    SkPoint startPoint = {0,0};
+    SkPoint lastControlPoint;
+    for (auto iter = iterate.begin(); iter != iterate.end(); ++iter) {
+        auto [verb, pts, w] = *iter;
+        switch (verb) {
+            int n;
+            SkPoint chops[10];
+            case SkPathVerb::kMove:
+                startPoint = pts[0];
+                lastControlPoint = get_contour_closing_control_point(iter, iterate.end());
+                if (!check_first_resolve_levels(r, firstNumSegments, &nextResolveLevel,
+                                                tolerance)) {
+                    return;
+                }
+                firstNumSegments.reset();
+                isFirstStroke = true;
+                break;
+            case SkPathVerb::kLine:
+                if (pts[0] == pts[1]) {
+                    break;
+                }
+                if (stroke.getJoin() == SkPaint::kRound_Join) {
+                    float rotation = SkMeasureAngleBetweenVectors(pts[0] - lastControlPoint,
+                                                                  pts[1] - pts[0]);
+                    float numSegments = rotation * tolerances.fNumRadialSegmentsPerRadian;
+                    if (isFirstStroke) {
+                        firstNumSegments.push_back(numSegments);
+                    } else if (!check_resolve_level(r, numSegments, *nextResolveLevel++,
                                                     tolerance)) {
                         return;
                     }
-                    firstNumSegments.reset();
-                    isFirstStroke = true;
+                }
+                lastControlPoint = pts[0];
+                isFirstStroke = false;
+                break;
+            case SkPathVerb::kQuad: {
+                if (pts[0] == pts[1] && pts[1] == pts[2]) {
                     break;
-                case SkPathVerb::kLine:
-                    if (pts[0] == pts[1]) {
-                        break;
+                }
+                SkVector a = pts[1] - pts[0];
+                SkVector b = pts[2] - pts[1];
+                bool hasCusp = (a.cross(b) == 0 && a.dot(b) < 0);
+                if (hasCusp) {
+                    // The quad has a cusp. Make sure we wrote out a -1 to signal that.
+                    if (isFirstStroke) {
+                        firstNumSegments.push_back(-1);
+                    } else {
+                        REPORTER_ASSERT(r, *nextResolveLevel++ == -1);
                     }
-                    if (fStroke.getJoin() == SkPaint::kRound_Join) {
-                        float rotation = SkMeasureAngleBetweenVectors(pts[0] - lastControlPoint,
-                                                                      pts[1] - pts[0]);
-                        float numSegments = rotation * tolerances.fNumRadialSegmentsPerRadian;
-                        if (isFirstStroke) {
-                            firstNumSegments.push_back(numSegments);
-                        } else if (!check_resolve_level(r, numSegments, *nextResolveLevel++,
-                                                        tolerance)) {
-                            return;
-                        }
+                }
+                float numParametricSegments = (hasCusp) ? 0 : GrWangsFormula::quadratic(
+                        tolerances.fParametricIntolerance, pts);
+                float rotation = (hasCusp) ? 0 : SkMeasureQuadRotation(pts);
+                if (stroke.getJoin() == SkPaint::kRound_Join) {
+                    SkVector controlPoint = (pts[0] == pts[1]) ? pts[2] : pts[1];
+                    rotation += SkMeasureAngleBetweenVectors(pts[0] - lastControlPoint,
+                                                             controlPoint - pts[0]);
+                }
+                float numRadialSegments = rotation * tolerances.fNumRadialSegmentsPerRadian;
+                float numSegments = numParametricSegments + numRadialSegments;
+                if (!hasCusp || stroke.getJoin() == SkPaint::kRound_Join) {
+                    if (isFirstStroke) {
+                        firstNumSegments.push_back(numSegments);
+                    } else if (!check_resolve_level(r, numSegments, *nextResolveLevel++,
+                                                    tolerance)) {
+                        return;
                     }
-                    lastControlPoint = pts[0];
-                    isFirstStroke = false;
+                }
+                lastControlPoint = (pts[2] == pts[1]) ? pts[0] : pts[1];
+                isFirstStroke = false;
+                break;
+            }
+            case SkPathVerb::kCubic: {
+                if (pts[0] == pts[1] && pts[1] == pts[2] && pts[2] == pts[3]) {
                     break;
-                case SkPathVerb::kQuad: {
-                    if (pts[0] == pts[1] && pts[1] == pts[2]) {
-                        break;
+                }
+                float T[2];
+                bool areCusps = false;
+                n = GrPathUtils::findCubicConvex180Chops(pts, T, &areCusps);
+                SkChopCubicAt(pts, chops, T, n);
+                if (n > 0) {
+                    int signal = -((n << 1) | (int)areCusps);
+                    if (isFirstStroke) {
+                        firstNumSegments.push_back((float)signal);
+                    } else {
+                        REPORTER_ASSERT(r, *nextResolveLevel++ == signal);
                     }
-                    SkVector a = pts[1] - pts[0];
-                    SkVector b = pts[2] - pts[1];
-                    bool hasCusp = (a.cross(b) == 0 && a.dot(b) < 0);
-                    if (hasCusp) {
-                        // The quad has a cusp. Make sure we wrote out a -1 to signal that.
-                        if (isFirstStroke) {
-                            firstNumSegments.push_back(-1);
-                        } else {
-                            REPORTER_ASSERT(r, *nextResolveLevel++ == -1);
-                        }
-                    }
-                    float numParametricSegments = (hasCusp) ? 0 : GrWangsFormula::quadratic(
-                            tolerances.fParametricIntolerance, pts);
-                    float rotation = (hasCusp) ? 0 : SkMeasureQuadRotation(pts);
-                    if (fStroke.getJoin() == SkPaint::kRound_Join) {
-                        SkVector controlPoint = (pts[0] == pts[1]) ? pts[2] : pts[1];
-                        rotation += SkMeasureAngleBetweenVectors(pts[0] - lastControlPoint,
-                                                                 controlPoint - pts[0]);
+                }
+                for (int i = 0; i <= n; ++i) {
+                    // Find the number of segments with our unoptimized approach and make sure
+                    // it matches the answer we got already.
+                    SkPoint* p = chops + i*3;
+                    float numParametricSegments =
+                            GrWangsFormula::cubic(tolerances.fParametricIntolerance, p);
+                    SkVector tan0 =
+                            ((p[0] == p[1]) ? (p[1] == p[2]) ? p[3] : p[2] : p[1]) - p[0];
+                    SkVector tan1 =
+                            p[3] - ((p[3] == p[2]) ? (p[2] == p[1]) ? p[0] : p[1] : p[2]);
+                    float rotation = SkMeasureAngleBetweenVectors(tan0, tan1);
+                    if (i == 0 && stroke.getJoin() == SkPaint::kRound_Join) {
+                        rotation += SkMeasureAngleBetweenVectors(p[0] - lastControlPoint, tan0);
                     }
                     float numRadialSegments = rotation * tolerances.fNumRadialSegmentsPerRadian;
                     float numSegments = numParametricSegments + numRadialSegments;
-                    if (!hasCusp || fStroke.getJoin() == SkPaint::kRound_Join) {
-                        if (isFirstStroke) {
-                            firstNumSegments.push_back(numSegments);
-                        } else if (!check_resolve_level(r, numSegments, *nextResolveLevel++,
-                                                        tolerance)) {
-                            return;
-                        }
-                    }
-                    lastControlPoint = (pts[2] == pts[1]) ? pts[0] : pts[1];
-                    isFirstStroke = false;
-                    break;
-                }
-                case SkPathVerb::kCubic: {
-                    if (pts[0] == pts[1] && pts[1] == pts[2] && pts[2] == pts[3]) {
-                        break;
-                    }
-                    float T[2];
-                    bool areCusps = false;
-                    n = GrPathUtils::findCubicConvex180Chops(pts, T, &areCusps);
-                    SkChopCubicAt(pts, chops, T, n);
-                    if (n > 0) {
-                        int signal = -((n << 1) | (int)areCusps);
-                        if (isFirstStroke) {
-                            firstNumSegments.push_back((float)signal);
-                        } else {
-                            REPORTER_ASSERT(r, *nextResolveLevel++ == signal);
-                        }
-                    }
-                    for (int i = 0; i <= n; ++i) {
-                        // Find the number of segments with our unoptimized approach and make sure
-                        // it matches the answer we got already.
-                        SkPoint* p = chops + i*3;
-                        float numParametricSegments =
-                                GrWangsFormula::cubic(tolerances.fParametricIntolerance, p);
-                        SkVector tan0 =
-                                ((p[0] == p[1]) ? (p[1] == p[2]) ? p[3] : p[2] : p[1]) - p[0];
-                        SkVector tan1 =
-                                p[3] - ((p[3] == p[2]) ? (p[2] == p[1]) ? p[0] : p[1] : p[2]);
-                        float rotation = SkMeasureAngleBetweenVectors(tan0, tan1);
-                        if (i == 0 && fStroke.getJoin() == SkPaint::kRound_Join) {
-                            rotation += SkMeasureAngleBetweenVectors(p[0] - lastControlPoint, tan0);
-                        }
-                        float numRadialSegments = rotation * tolerances.fNumRadialSegmentsPerRadian;
-                        float numSegments = numParametricSegments + numRadialSegments;
-                        if (isFirstStroke) {
-                            firstNumSegments.push_back(numSegments);
-                        } else if (!check_resolve_level(r, numSegments, *nextResolveLevel++,
-                                                        tolerance)) {
-                            return;
-                        }
-                    }
-                    lastControlPoint =
-                            (pts[3] == pts[2]) ? (pts[2] == pts[1]) ? pts[0] : pts[1] : pts[2];
-                    isFirstStroke = false;
-                    break;
-                }
-                case SkPathVerb::kConic:
-                    SkUNREACHABLE;
-                case SkPathVerb::kClose:
-                    if (pts[0] != startPoint) {
-                        SkASSERT(!isFirstStroke);
-                        if (fStroke.getJoin() == SkPaint::kRound_Join) {
-                            // Line from pts[0] to startPoint, with a preceding join.
-                            float rotation = SkMeasureAngleBetweenVectors(pts[0] - lastControlPoint,
-                                                                          startPoint - pts[0]);
-                            if (!check_resolve_level(
-                                    r, rotation * tolerances.fNumRadialSegmentsPerRadian,
-                                    *nextResolveLevel++, tolerance)) {
-                                return;
-                            }
-                        }
-                    }
-                    if (!check_first_resolve_levels(r, firstNumSegments, &nextResolveLevel,
+                    if (isFirstStroke) {
+                        firstNumSegments.push_back(numSegments);
+                    } else if (!check_resolve_level(r, numSegments, *nextResolveLevel++,
                                                     tolerance)) {
                         return;
                     }
-                    firstNumSegments.reset();
-                    isFirstStroke = true;
-                    break;
+                }
+                lastControlPoint =
+                        (pts[3] == pts[2]) ? (pts[2] == pts[1]) ? pts[0] : pts[1] : pts[2];
+                isFirstStroke = false;
+                break;
             }
+            case SkPathVerb::kConic:
+                SkUNREACHABLE;
+            case SkPathVerb::kClose:
+                if (pts[0] != startPoint) {
+                    SkASSERT(!isFirstStroke);
+                    if (stroke.getJoin() == SkPaint::kRound_Join) {
+                        // Line from pts[0] to startPoint, with a preceding join.
+                        float rotation = SkMeasureAngleBetweenVectors(pts[0] - lastControlPoint,
+                                                                      startPoint - pts[0]);
+                        if (!check_resolve_level(
+                                r, rotation * tolerances.fNumRadialSegmentsPerRadian,
+                                *nextResolveLevel++, tolerance)) {
+                            return;
+                        }
+                    }
+                }
+                if (!check_first_resolve_levels(r, firstNumSegments, &nextResolveLevel,
+                                                tolerance)) {
+                    return;
+                }
+                firstNumSegments.reset();
+                isFirstStroke = true;
+                break;
         }
-        if (!check_first_resolve_levels(r, firstNumSegments, &nextResolveLevel, tolerance)) {
-            return;
-        }
-        firstNumSegments.reset();
     }
+    if (!check_first_resolve_levels(r, firstNumSegments, &nextResolveLevel, tolerance)) {
+        return;
+    }
+    firstNumSegments.reset();
     SkASSERT(nextResolveLevel == fResolveLevels + fResolveLevelArrayCount);
 }
 
-void GrStrokeIndirectOp::verifyPrepareBuffers(skiatest::Reporter* r, GrMeshDrawOp::Target* target) {
+void GrStrokeIndirectTessellator::verifyBuffers(skiatest::Reporter* r,
+                                                GrMeshDrawOp::Target* target,
+                                                const SkMatrix& viewMatrix,
+                                                const SkStrokeRec& stroke) {
     using IndirectInstance = GrStrokeTessellateShader::IndirectInstance;
-    GrStrokeTessellateShader::Tolerances tolerances(fViewMatrix.getMaxScale(), fStroke.getWidth());
-    float tolerance = test_tolerance(fStroke.getJoin());
+    GrStrokeTessellateShader::Tolerances tolerances(viewMatrix.getMaxScale(), stroke.getWidth());
+    float tolerance = test_tolerance(stroke.getJoin());
     // Make sure the resolve level we assign to each instance agrees with the actual data.
-    this->prepareBuffers(target);
     // GrMockOpTarget returns the same pointers every time.
     int _;
     auto instance = (const IndirectInstance*)target->makeVertexSpace(0, 0, nullptr, &_);
     size_t __;
     auto indirect = target->makeDrawIndirectSpace(0, nullptr, &__);
     for (int i = 0; i < fDrawIndirectCount; ++i) {
-        int numExtraEdgesInJoin = (fStroke.getJoin() == SkPaint::kMiter_Join) ? 4 : 3;
+        int numExtraEdgesInJoin = (stroke.getJoin() == SkPaint::kMiter_Join) ? 4 : 3;
         int numStrokeEdges = indirect->fVertexCount/2 - numExtraEdgesInJoin;
         int numSegments = numStrokeEdges - 1;
         bool isPow2 = !(numSegments & (numSegments - 1));
@@ -458,7 +458,7 @@
             float rotation = SkMeasureAngleBetweenVectors(tan0, tan1);
             // Negative fNumTotalEdges means the curve is a chop, and chops always get treated as a
             // bevel join.
-            if (fStroke.getJoin() == SkPaint::kRound_Join && instance->fNumTotalEdges > 0) {
+            if (stroke.getJoin() == SkPaint::kRound_Join && instance->fNumTotalEdges > 0) {
                 SkVector lastTangent = p[0] - instance->fLastControlPoint;
                 rotation += SkMeasureAngleBetweenVectors(lastTangent, tan0);
             }