Write GPU buffers directly from GrStrokeGeometry

Removes the intermediate stroke representation that GrStrokeGeometry
used to generate. Uses GrOpFlushState::makeVertexStateAtLeast instead
and writes patches directly to a vertex buffer as we iterate the path.
If the vertex buffer runs out of room we simply allocate a new one and
draw the stroke in chunks.

Bug: skia:10419
Bug: skia:10460
Change-Id: Ic743158366e43d4d3f5a4ff97b039d48c9c9c65b
Reviewed-on: https://skia-review.googlesource.com/c/skia/+/305380
Commit-Queue: Chris Dalton <csmartdalton@google.com>
Reviewed-by: Michael Ludwig <michaelludwig@google.com>
diff --git a/samplecode/SampleCCPRGeometry.cpp b/samplecode/SampleCCPRGeometry.cpp
index 0aa8100..758c076 100644
--- a/samplecode/SampleCCPRGeometry.cpp
+++ b/samplecode/SampleCCPRGeometry.cpp
@@ -26,7 +26,6 @@
 #include "src/gpu/GrResourceProvider.h"
 #include "src/gpu/ccpr/GrCCCoverageProcessor.h"
 #include "src/gpu/ccpr/GrCCFillGeometry.h"
-#include "src/gpu/ccpr/GrCCStroker.h"
 #include "src/gpu/ccpr/GrGSCoverageProcessor.h"
 #include "src/gpu/ccpr/GrVSCoverageProcessor.h"
 #include "src/gpu/geometry/GrPathUtils.h"
@@ -66,7 +65,7 @@
 
     void updateGpuData();
 
-    PrimitiveType fPrimitiveType = PrimitiveType::kTriangles;
+    PrimitiveType fPrimitiveType = PrimitiveType::kCubics;
     SkCubicType fCubicType;
     SkMatrix fCubicKLM;
 
@@ -75,7 +74,9 @@
 
     float fConicWeight = .5;
     float fStrokeWidth = 40;
-    bool fDoStroke = false;
+    SkPaint::Join fStrokeJoin = SkPaint::kMiter_Join;
+    SkPaint::Cap fStrokeCap = SkPaint::kButt_Cap;
+    bool fDoStroke = true;
 
     SkTArray<TriPointInstance> fTriPointInstances;
     SkTArray<QuadPointInstance> fQuadPointInstances;
@@ -176,14 +177,18 @@
 void CCPRGeometryView::onDrawContent(SkCanvas* canvas) {
     canvas->clear(SK_ColorBLACK);
 
-    if (!fDoStroke) {
-        SkPaint outlinePaint;
-        outlinePaint.setColor(0x80ffffff);
-        outlinePaint.setStyle(SkPaint::kStroke_Style);
+    SkPaint outlinePaint;
+    outlinePaint.setColor(0xff808080);
+    outlinePaint.setStyle(SkPaint::kStroke_Style);
+    if (fDoStroke) {
+        outlinePaint.setStrokeWidth(fStrokeWidth);
+    } else {
         outlinePaint.setStrokeWidth(0);
-        outlinePaint.setAntiAlias(true);
-        canvas->drawPath(fPath, outlinePaint);
     }
+    outlinePaint.setStrokeJoin(fStrokeJoin);
+    outlinePaint.setStrokeCap(fStrokeCap);
+    outlinePaint.setAntiAlias(true);
+    canvas->drawPath(fPath, outlinePaint);
 
 #if 0
     SkPaint gridPaint;
@@ -200,7 +205,18 @@
 #endif
 
     SkString caption;
-    if (GrRenderTargetContext* rtc = canvas->internal_private_accessTopLayerRenderTargetContext()) {
+    caption.appendf("PrimitiveType_%s",
+                    GrCCCoverageProcessor::PrimitiveTypeName(fPrimitiveType));
+    if (PrimitiveType::kCubics == fPrimitiveType) {
+        caption.appendf(" (%s)", SkCubicTypeName(fCubicType));
+    } else if (PrimitiveType::kConics == fPrimitiveType) {
+        caption.appendf(" (w=%f)", fConicWeight);
+    }
+
+    if (fDoStroke) {
+        caption.appendf(" (stroke_width=%f)", fStrokeWidth);
+    } else if (GrRenderTargetContext* rtc =
+            canvas->internal_private_accessTopLayerRenderTargetContext()) {
         // Render coverage count.
         auto ctx = canvas->recordingContext();
         SkASSERT(ctx);
@@ -222,18 +238,6 @@
         paint.setPorterDuffXPFactory(SkBlendMode::kSrcOver);
         rtc->drawRect(nullptr, std::move(paint), GrAA::kNo, SkMatrix::I(),
                       SkRect::MakeIWH(this->width(), this->height()));
-
-        // Add label.
-        caption.appendf("PrimitiveType_%s",
-                        GrCCCoverageProcessor::PrimitiveTypeName(fPrimitiveType));
-        if (PrimitiveType::kCubics == fPrimitiveType) {
-            caption.appendf(" (%s)", SkCubicTypeName(fCubicType));
-        } else if (PrimitiveType::kConics == fPrimitiveType) {
-            caption.appendf(" (w=%f)", fConicWeight);
-        }
-        if (fDoStroke) {
-            caption.appendf(" (stroke_width=%f)", fStrokeWidth);
-        }
     } else {
         caption = "Use GPU backend to visualize geometry.";
     }
@@ -345,8 +349,8 @@
 void CCPRGeometryView::DrawCoverageCountOp::onExecute(GrOpFlushState* state,
                                                       const SkRect& chainBounds) {
     GrResourceProvider* rp = state->resourceProvider();
-    auto direct = state->gpu()->getContext();
 #ifdef SK_GL
+    auto direct = state->gpu()->getContext();
     GrGLGpu* glGpu = GrBackendApi::kOpenGL == direct->backend()
                              ? static_cast<GrGLGpu*>(state->gpu())
                              : nullptr;
@@ -370,52 +374,30 @@
 
     GrOpsRenderPass* renderPass = state->opsRenderPass();
 
-    if (!fView->fDoStroke) {
-        for (int i = 0; i < proc->numSubpasses(); ++i) {
-            proc->reset(fView->fPrimitiveType, i, rp);
-            proc->bindPipeline(state, pipeline, this->bounds());
+    for (int i = 0; i < proc->numSubpasses(); ++i) {
+        proc->reset(fView->fPrimitiveType, i, rp);
+        proc->bindPipeline(state, pipeline, this->bounds());
 
-            if (PrimitiveType::kCubics == fView->fPrimitiveType ||
-                PrimitiveType::kConics == fView->fPrimitiveType) {
-                sk_sp<GrGpuBuffer> instBuff(rp->createBuffer(
-                        fView->fQuadPointInstances.count() * sizeof(QuadPointInstance),
-                        GrGpuBufferType::kVertex, kDynamic_GrAccessPattern,
-                        fView->fQuadPointInstances.begin()));
-                if (!fView->fQuadPointInstances.empty() && instBuff) {
-                    proc->bindBuffers(renderPass, std::move(instBuff));
-                    proc->drawInstances(renderPass, fView->fQuadPointInstances.count(), 0);
-                }
-            } else {
-                sk_sp<GrGpuBuffer> instBuff(rp->createBuffer(
-                        fView->fTriPointInstances.count() * sizeof(TriPointInstance),
-                        GrGpuBufferType::kVertex, kDynamic_GrAccessPattern,
-                        fView->fTriPointInstances.begin()));
-                if (!fView->fTriPointInstances.empty() && instBuff) {
-                    proc->bindBuffers(renderPass, std::move(instBuff));
-                    proc->drawInstances(renderPass, fView->fTriPointInstances.count(), 0);
-                }
+        if (PrimitiveType::kCubics == fView->fPrimitiveType ||
+            PrimitiveType::kConics == fView->fPrimitiveType) {
+            sk_sp<GrGpuBuffer> instBuff(rp->createBuffer(
+                    fView->fQuadPointInstances.count() * sizeof(QuadPointInstance),
+                    GrGpuBufferType::kVertex, kDynamic_GrAccessPattern,
+                    fView->fQuadPointInstances.begin()));
+            if (!fView->fQuadPointInstances.empty() && instBuff) {
+                proc->bindBuffers(renderPass, std::move(instBuff));
+                proc->drawInstances(renderPass, fView->fQuadPointInstances.count(), 0);
+            }
+        } else {
+            sk_sp<GrGpuBuffer> instBuff(rp->createBuffer(
+                    fView->fTriPointInstances.count() * sizeof(TriPointInstance),
+                    GrGpuBufferType::kVertex, kDynamic_GrAccessPattern,
+                    fView->fTriPointInstances.begin()));
+            if (!fView->fTriPointInstances.empty() && instBuff) {
+                proc->bindBuffers(renderPass, std::move(instBuff));
+                proc->drawInstances(renderPass, fView->fTriPointInstances.count(), 0);
             }
         }
-    } else if (PrimitiveType::kConics != fView->fPrimitiveType) {  // No conic stroke support yet.
-        GrCCStroker stroker(0,0,0);
-
-        SkPaint p;
-        p.setStyle(SkPaint::kStroke_Style);
-        p.setStrokeWidth(fView->fStrokeWidth);
-        p.setStrokeJoin(SkPaint::kMiter_Join);
-        p.setStrokeMiter(4);
-        // p.setStrokeCap(SkPaint::kRound_Cap);
-        stroker.parseDeviceSpaceStroke(fView->fPath, SkPathPriv::PointData(fView->fPath),
-                                       SkStrokeRec(p), p.getStrokeWidth(), GrScissorTest::kDisabled,
-                                       SkIRect::MakeWH(fView->width(), fView->height()), {0, 0});
-        GrCCStroker::BatchID batchID = stroker.closeCurrentBatch();
-
-        GrOnFlushResourceProvider onFlushRP(direct->priv().drawingManager());
-        stroker.prepareToDraw(&onFlushRP);
-
-        SkIRect ibounds;
-        this->bounds().roundOut(&ibounds);
-        stroker.drawStrokes(state, proc.get(), batchID, ibounds);
     }
 
 #ifdef SK_GL
@@ -511,6 +493,17 @@
         if (unichar == 'S') {
             fDoStroke = !fDoStroke;
             this->updateAndInval();
+            return true;
+        }
+        if (unichar == 'J') {
+            fStrokeJoin = (SkPaint::Join)((fStrokeJoin + 1) % 3);
+            this->updateAndInval();
+            return true;
+        }
+        if (unichar == 'C') {
+            fStrokeCap = (SkPaint::Cap)((fStrokeCap + 1) % 3);
+            this->updateAndInval();
+            return true;
         }
         return false;
 }
diff --git a/src/gpu/tessellate/GrStrokeGeometry.cpp b/src/gpu/tessellate/GrStrokeGeometry.cpp
index 13674ed..196ae9d 100644
--- a/src/gpu/tessellate/GrStrokeGeometry.cpp
+++ b/src/gpu/tessellate/GrStrokeGeometry.cpp
@@ -11,22 +11,24 @@
 #include "include/private/SkNx.h"
 #include "src/core/SkGeometry.h"
 #include "src/core/SkMathPriv.h"
+#include "src/core/SkPathPriv.h"
 
 // This is the maximum distance in pixels that we can stray from the edge of a stroke when
 // converting it to flat line segments.
 static constexpr float kMaxErrorFromLinearization = 1/8.f;
 
+constexpr static float kInternalRoundJoinType = GrTessellateStrokeShader::kInternalRoundJoinType;
+
+static Sk2f lerp(const Sk2f& a, const Sk2f& b, float T) {
+    SkASSERT(1 != T);  // The below does not guarantee lerp(a, b, 1) === b.
+    return (b - a) * T + a;
+}
+
 static inline float length(const Sk2f& n) {
     Sk2f nn = n*n;
     return SkScalarSqrt(nn[0] + nn[1]);
 }
 
-static inline Sk2f normalize(const Sk2f& v) {
-    Sk2f vv = v*v;
-    vv += SkNx_shuffle<1,0>(vv);
-    return v * vv.rsqrt();
-}
-
 static inline void transpose(const Sk2f& a, const Sk2f& b, Sk2f* X, Sk2f* Y) {
     float transpose[4];
     a.store(transpose);
@@ -34,13 +36,6 @@
     Sk2f::Load2(transpose, X, Y);
 }
 
-static inline void normalize2(const Sk2f& v0, const Sk2f& v1, SkPoint out[2]) {
-    Sk2f X, Y;
-    transpose(v0, v1, &X, &Y);
-    Sk2f invlength = (X*X + Y*Y).rsqrt();
-    Sk2f::Store2(out, Y * invlength, -X * invlength);
-}
-
 static inline float calc_curvature_costheta(const Sk2f& leftTan, const Sk2f& rightTan) {
     Sk2f X, Y;
     transpose(leftTan, rightTan, &X, &Y);
@@ -49,80 +44,194 @@
     return (dotprod[0] + dotprod[1]) * invlength[0] * invlength[1];
 }
 
-static GrStrokeGeometry::Verb join_verb_from_join(SkPaint::Join join) {
-    using Verb = GrStrokeGeometry::Verb;
-    switch (join) {
-        case SkPaint::kBevel_Join:
-            return Verb::kBevelJoin;
-        case SkPaint::kMiter_Join:
-            return Verb::kMiterJoin;
-        case SkPaint::kRound_Join:
-            return Verb::kRoundJoin;
-    }
-    SK_ABORT("Invalid SkPaint::Join.");
+void GrStrokeGeometry::allocVertexChunk(int minVertexAllocCount) {
+    VertexChunk* chunk = &fVertexChunkArray->push_back();
+    fCurrChunkVertexData = (SkPoint*)fTarget->makeVertexSpaceAtLeast(
+            sizeof(SkPoint), minVertexAllocCount, minVertexAllocCount, &chunk->fVertexBuffer,
+            &chunk->fBaseVertex, &fCurrChunkVertexCapacity);
+    fCurrChunkMinVertexAllocCount = minVertexAllocCount;
 }
 
-void GrStrokeGeometry::beginPath(const SkStrokeRec& stroke, float strokeDevWidth,
-                                   InstanceTallies* tallies) {
-    SkASSERT(!fInsideContour);
+SkPoint* GrStrokeGeometry::reservePatch() {
+    constexpr static int kNumVerticesPerPatch = GrTessellateStrokeShader::kNumVerticesPerPatch;
+    if (fVertexChunkArray->back().fVertexCount + kNumVerticesPerPatch > fCurrChunkVertexCapacity) {
+        // No need to put back vertices; the buffer is full.
+        this->allocVertexChunk(fCurrChunkMinVertexAllocCount * 2);
+    }
+    if (!fCurrChunkVertexData) {
+        SkDebugf("WARNING: Failed to allocate vertex buffer for tessellated stroke.");
+        return nullptr;
+    }
+    SkASSERT(fVertexChunkArray->back().fVertexCount + kNumVerticesPerPatch <=
+             fCurrChunkVertexCapacity);
+    SkPoint* patch = fCurrChunkVertexData + fVertexChunkArray->back().fVertexCount;
+    fVertexChunkArray->back().fVertexCount += kNumVerticesPerPatch;
+    return patch;
+}
+
+void GrStrokeGeometry::writeCubicSegment(float leftJoinType, const SkPoint pts[4],
+                                         float overrideNumSegments) {
+    SkPoint c1 = (pts[1] == pts[0]) ? pts[2] : pts[1];
+    SkPoint c2 = (pts[2] == pts[3]) ? pts[1] : pts[2];
+
+    if (fHasPreviousSegment) {
+        this->writeJoin(leftJoinType, pts[0], fLastControlPoint, c1);
+    } else {
+        fCurrContourFirstControlPoint = c1;
+        fHasPreviousSegment = true;
+    }
+
+    if (SkPoint* patch = this->reservePatch()) {
+        memcpy(patch, pts, sizeof(SkPoint) * 4);
+        patch[4].set(-overrideNumSegments, fCurrStrokeRadius);
+    }
+
+    fLastControlPoint = c2;
+    fCurrentPoint = pts[3];
+}
+
+void GrStrokeGeometry::writeJoin(float joinType, const SkPoint& anchorPoint,
+                                 const SkPoint& prevControlPoint, const SkPoint& nextControlPoint) {
+    if (SkPoint* joinPatch = this->reservePatch()) {
+        joinPatch[0] = anchorPoint;
+        joinPatch[1] = prevControlPoint;
+        joinPatch[2] = nextControlPoint;
+        joinPatch[3] = anchorPoint;
+        joinPatch[4].set(joinType, fCurrStrokeRadius);
+    }
+}
+
+void GrStrokeGeometry::writeSquareCap(const SkPoint& endPoint, const SkPoint& controlPoint) {
+    SkVector v = (endPoint - controlPoint);
+    v.normalize();
+    SkPoint capPoint = endPoint + v*fCurrStrokeRadius;
+    // Construct a line that incorporates controlPoint so we get a water tight edge with the rest of
+    // the stroke. The cubic will technically step outside the cap, but we will force it to only
+    // have one segment, giving edges only at the endpoints.
+    if (SkPoint* capPatch = this->reservePatch()) {
+        capPatch[0] = endPoint;
+        capPatch[1] = controlPoint;
+        // Straddle the midpoint of the cap because the tessellated geometry emits a center point at
+        // T=.5, and we need to ensure that point stays inside the cap.
+        capPatch[2] = endPoint + capPoint - controlPoint;
+        capPatch[3] = capPoint;
+        capPatch[4].set(-1, fCurrStrokeRadius);
+    }
+}
+
+void GrStrokeGeometry::writeCaps() {
+    if (!fHasPreviousSegment) {
+        // We don't have any control points to orient the caps. In this case, square and round caps
+        // are specified to be drawn as an axis-aligned square or circle respectively. Assign
+        // default control points that achieve this.
+        fCurrContourFirstControlPoint = fCurrContourStartPoint - SkPoint{1,0};
+        fLastControlPoint = fCurrContourStartPoint + SkPoint{1,0};
+        fCurrentPoint = fCurrContourStartPoint;
+    }
+
+    switch (fCurrStrokeCapType) {
+        case SkPaint::kButt_Cap:
+            break;
+        case SkPaint::kRound_Cap:
+            // A round cap is the same thing as a 180-degree round join.
+            this->writeJoin(3, fCurrContourStartPoint, fCurrContourFirstControlPoint,
+                            fCurrContourFirstControlPoint);
+            this->writeJoin(3, fCurrentPoint, fLastControlPoint, fLastControlPoint);
+            break;
+        case SkPaint::kSquare_Cap:
+            this->writeSquareCap(fCurrContourStartPoint, fCurrContourFirstControlPoint);
+            this->writeSquareCap(fCurrentPoint, fLastControlPoint);
+            break;
+    }
+}
+
+void GrStrokeGeometry::addPath(const SkPath& path, const SkStrokeRec& stroke) {
+    this->beginPath(stroke, stroke.getWidth());
+    SkPathVerb previousVerb = SkPathVerb::kClose;
+    for (auto [verb, pts, w] : SkPathPriv::Iterate(path)) {
+        switch (verb) {
+            case SkPathVerb::kMove:
+                // "A subpath ... consisting of a single moveto shall not be stroked."
+                // https://www.w3.org/TR/SVG11/painting.html#StrokeProperties
+                if (previousVerb != SkPathVerb::kMove && previousVerb != SkPathVerb::kClose) {
+                    this->writeCaps();
+                }
+                this->moveTo(pts[0]);
+                break;
+            case SkPathVerb::kClose:
+                this->close();
+                break;
+            case SkPathVerb::kLine:
+                SkASSERT(previousVerb != SkPathVerb::kClose);
+                this->lineTo(pts[0], pts[1]);
+                break;
+            case SkPathVerb::kQuad:
+                SkASSERT(previousVerb != SkPathVerb::kClose);
+                this->quadraticTo(pts);
+                break;
+            case SkPathVerb::kCubic:
+                SkASSERT(previousVerb != SkPathVerb::kClose);
+                this->cubicTo(pts);
+                break;
+            case SkPathVerb::kConic:
+                SkASSERT(previousVerb != SkPathVerb::kClose);
+                SkUNREACHABLE;
+        }
+        previousVerb = verb;
+    }
+    if (previousVerb != SkPathVerb::kMove && previousVerb != SkPathVerb::kClose) {
+        this->writeCaps();
+    }
+}
+
+static float join_type_from_join(SkPaint::Join join) {
+    switch (join) {
+        case SkPaint::kBevel_Join:
+            return GrTessellateStrokeShader::kBevelJoinType;
+        case SkPaint::kMiter_Join:
+            return GrTessellateStrokeShader::kMiterJoinType;
+        case SkPaint::kRound_Join:
+            return GrTessellateStrokeShader::kRoundJoinType;
+    }
+    SkUNREACHABLE;
+}
+
+void GrStrokeGeometry::beginPath(const SkStrokeRec& stroke, float strokeDevWidth) {
     // Client should have already converted the stroke to device space (i.e. width=1 for hairline).
     SkASSERT(strokeDevWidth > 0);
 
     fCurrStrokeRadius = strokeDevWidth/2;
-    fCurrStrokeJoinVerb = join_verb_from_join(stroke.getJoin());
+    fCurrStrokeJoinType = join_type_from_join(stroke.getJoin());
     fCurrStrokeCapType = stroke.getCap();
-    fCurrStrokeTallies = tallies;
-
-    if (Verb::kMiterJoin == fCurrStrokeJoinVerb) {
-        // We implement miters by placing a triangle-shaped cap on top of a bevel join. Convert the
-        // "miter limit" to how tall that triangle cap can be.
-        float m = stroke.getMiter();
-        fMiterMaxCapHeightOverWidth = .5f * SkScalarSqrt(m*m - 1);
-    }
 
     // Find the angle of curvature where the arc height above a simple line from point A to point B
     // is equal to kMaxErrorFromLinearization.
     float r = std::max(1 - kMaxErrorFromLinearization / fCurrStrokeRadius, 0.f);
     fMaxCurvatureCosTheta = 2*r*r - 1;
 
-    fCurrContourFirstPtIdx = -1;
-    fCurrContourFirstNormalIdx = -1;
-
-    fVerbs.push_back(Verb::kBeginPath);
+    fHasPreviousSegment = false;
 }
 
-void GrStrokeGeometry::moveTo(SkPoint pt) {
-    SkASSERT(!fInsideContour);
-    fCurrContourFirstPtIdx = fPoints.count();
-    fCurrContourFirstNormalIdx = fNormals.count();
-    fPoints.push_back(pt);
-    SkDEBUGCODE(fInsideContour = true);
+void GrStrokeGeometry::moveTo(const SkPoint& pt) {
+    fHasPreviousSegment = false;
+    fCurrContourStartPoint = pt;
 }
 
-void GrStrokeGeometry::lineTo(SkPoint pt) {
-    SkASSERT(fInsideContour);
-    this->lineTo(fCurrStrokeJoinVerb, pt);
+void GrStrokeGeometry::lineTo(const SkPoint& p0, const SkPoint& p1) {
+    this->lineTo(fCurrStrokeJoinType, p0, p1);
 }
 
-void GrStrokeGeometry::lineTo(Verb leftJoinVerb, SkPoint pt) {
-    Sk2f tan = Sk2f::Load(&pt) - Sk2f::Load(&fPoints.back());
-    if ((tan == 0).allTrue()) {
+void GrStrokeGeometry::lineTo(float leftJoinType, const SkPoint& pt0, const SkPoint& pt1) {
+    Sk2f p0 = Sk2f::Load(&pt0);
+    Sk2f p1 = Sk2f::Load(&pt1);
+    if ((p0 == p1).allTrue()) {
         return;
     }
-
-    tan = normalize(tan);
-    SkVector n = SkVector::Make(tan[1], -tan[0]);
-
-    this->recordLeftJoinIfNotEmpty(leftJoinVerb, n);
-    fNormals.push_back(n);
-
-    this->recordStroke(Verb::kLinearStroke, 0);
-    fPoints.push_back(pt);
+    this->writeCubicSegment(leftJoinType, p0, lerp(p0, p1, 1/3.f), lerp(p0, p1, 2/3.f), p1, 1);
 }
 
 void GrStrokeGeometry::quadraticTo(const SkPoint P[3]) {
-    SkASSERT(fInsideContour);
-    this->quadraticTo(fCurrStrokeJoinVerb, P, SkFindQuadMaxCurvature(P));
+    this->quadraticTo(fCurrStrokeJoinType, P, SkFindQuadMaxCurvature(P));
 }
 
 // Wang's formula for quadratics (1985) gives us the number of evenly spaced (in the parametric
@@ -134,7 +243,7 @@
     return SkScalarCeilToInt(f);
 }
 
-void GrStrokeGeometry::quadraticTo(Verb leftJoinVerb, const SkPoint P[3], float maxCurvatureT) {
+void GrStrokeGeometry::quadraticTo(float leftJoinType, const SkPoint P[3], float maxCurvatureT) {
     Sk2f p0 = Sk2f::Load(P);
     Sk2f p1 = Sk2f::Load(P+1);
     Sk2f p2 = Sk2f::Load(P+2);
@@ -146,16 +255,13 @@
     // an issue.
     if ((tan0.abs() < SK_ScalarNearlyZero).allTrue() ||  // p0 ~= p1
         (tan1.abs() < SK_ScalarNearlyZero).allTrue()) {  // p1 ~= p2
-        this->lineTo(leftJoinVerb, P[2]);
+        this->lineTo(leftJoinType, P[0], P[2]);
         return;
     }
 
-    SkPoint normals[2];
-    normalize2(tan0, tan1, normals);
-
     // Decide how many flat line segments to chop the curve into.
     int numSegments = wangs_formula_quadratic(p0, p1, p2);
-    numSegments = SkTPin(numSegments, 1, 1 << kMaxNumLinearSegmentsLog2);
+    numSegments = std::max(numSegments, 1);
 
     // At + B gives a vector tangent to the quadratic.
     Sk2f A = p0 - p1*2 + p2;
@@ -193,22 +299,22 @@
 
         if (leftT > 0) {
             SkChopQuadAt(currQuadratic, ptsBuffer, leftT);
-            this->quadraticTo(leftJoinVerb, ptsBuffer, /*maxCurvatureT=*/1);
+            this->quadraticTo(leftJoinType, ptsBuffer, /*maxCurvatureT=*/1);
             if (rightT < 1) {
                 rightT = (rightT - leftT) / (1 - leftT);
             }
             currQuadratic = ptsBuffer + 2;
         } else {
-            this->rotateTo(leftJoinVerb, normals[0], currQuadratic[1]);
+            this->rotateTo(leftJoinType, currQuadratic[0], currQuadratic[1]);
         }
 
         if (rightT < 1) {
             SkChopQuadAt(currQuadratic, ptsBuffer, rightT);
-            this->lineTo(Verb::kInternalRoundJoin, ptsBuffer[2]);
-            this->quadraticTo(Verb::kInternalRoundJoin, ptsBuffer + 2, /*maxCurvatureT=*/0);
+            this->lineTo(kInternalRoundJoinType, ptsBuffer[0], ptsBuffer[2]);
+            this->quadraticTo(kInternalRoundJoinType, ptsBuffer + 2, /*maxCurvatureT=*/0);
         } else {
-            this->lineTo(Verb::kInternalRoundJoin, currQuadratic[2]);
-            this->rotateTo(Verb::kInternalRoundJoin, normals[1],
+            this->lineTo(kInternalRoundJoinType, currQuadratic[0], currQuadratic[2]);
+            this->rotateTo(kInternalRoundJoinType, currQuadratic[2],
                            currQuadratic[2]*2 - currQuadratic[1]);
         }
         return;
@@ -216,24 +322,18 @@
     if (numSegments > fMaxTessellationSegments) {
         SkPoint ptsBuffer[5];
         SkChopQuadAt(P, ptsBuffer, 0.5f);
-        this->quadraticTo(leftJoinVerb, ptsBuffer, 0);
-        this->quadraticTo(Verb::kInternalRoundJoin, ptsBuffer + 3, 0);
+        this->quadraticTo(leftJoinType, ptsBuffer, 0);
+        this->quadraticTo(kInternalRoundJoinType, ptsBuffer + 3, 0);
         return;
     }
 
-    this->recordLeftJoinIfNotEmpty(leftJoinVerb, normals[0]);
-    fNormals.push_back_n(2, normals);
-
-    this->recordStroke(Verb::kQuadraticStroke, SkNextLog2(numSegments));
-    p1.store(&fPoints.push_back());
-    p2.store(&fPoints.push_back());
+    this->writeCubicSegment(leftJoinType, p0, lerp(p0, p1, 2/3.f), lerp(p1, p2, 1/3.f), p2);
 }
 
 void GrStrokeGeometry::cubicTo(const SkPoint P[4]) {
-    SkASSERT(fInsideContour);
     float roots[3];
     int numRoots = SkFindCubicMaxCurvature(P, roots);
-    this->cubicTo(fCurrStrokeJoinVerb, P,
+    this->cubicTo(fCurrStrokeJoinType, P,
                   numRoots > 0 ? roots[numRoots/2] : 0,
                   numRoots > 1 ? roots[0] : kLeftMaxCurvatureNone,
                   numRoots > 2 ? roots[2] : kRightMaxCurvatureNone);
@@ -250,8 +350,8 @@
     return SkScalarCeilToInt(f);
 }
 
-void GrStrokeGeometry::cubicTo(Verb leftJoinVerb, const SkPoint P[4], float maxCurvatureT,
-                                 float leftMaxCurvatureT, float rightMaxCurvatureT) {
+void GrStrokeGeometry::cubicTo(float leftJoinType, const SkPoint P[4], float maxCurvatureT,
+                               float leftMaxCurvatureT, float rightMaxCurvatureT) {
     Sk2f p0 = Sk2f::Load(P);
     Sk2f p1 = Sk2f::Load(P+1);
     Sk2f p2 = Sk2f::Load(P+2);
@@ -265,7 +365,7 @@
         p1 = p0;
         tan0 = p2 - p0;
         if ((tan0.abs() < SK_ScalarNearlyZero).allTrue()) {  // p0 ~= p1 ~= p2
-            this->lineTo(leftJoinVerb, P[3]);
+            this->lineTo(leftJoinType, P[0], P[3]);
             return;
         }
     }
@@ -274,17 +374,14 @@
         tan1 = p3 - p1;
         if ((tan1.abs() < SK_ScalarNearlyZero).allTrue() ||  // p1 ~= p2 ~= p3
             (p0 == p1).allTrue()) {  // p0 ~= p1 AND p2 ~= p3
-            this->lineTo(leftJoinVerb, P[3]);
+            this->lineTo(leftJoinType, P[0], P[3]);
             return;
         }
     }
 
-    SkPoint normals[2];
-    normalize2(tan0, tan1, normals);
-
     // Decide how many flat line segments to chop the curve into.
     int numSegments = wangs_formula_cubic(p0, p1, p2, p3);
-    numSegments = SkTPin(numSegments, 1, 1 << kMaxNumLinearSegmentsLog2);
+    numSegments = std::max(numSegments, 1);
 
     // At^2 + Bt + C gives a vector tangent to the cubic. (More specifically, it's the derivative
     // minus an irrelevant scale by 3, since all we care about is the direction.)
@@ -328,7 +425,7 @@
 
         if (leftT > 0) {
             SkChopCubicAt(currCubic, ptsBuffer, leftT);
-            this->cubicTo(leftJoinVerb, ptsBuffer, /*maxCurvatureT=*/1,
+            this->cubicTo(leftJoinType, ptsBuffer, /*maxCurvatureT=*/1,
                           (kLeftMaxCurvatureNone != leftMaxCurvatureT)
                                   ? leftMaxCurvatureT/leftT : kLeftMaxCurvatureNone,
                           kRightMaxCurvatureNone);
@@ -341,144 +438,73 @@
             currCubic = ptsBuffer + 3;
         } else {
             SkPoint c1 = (ptsBuffer[1] == ptsBuffer[0]) ? ptsBuffer[2] : ptsBuffer[1];
-            this->rotateTo(leftJoinVerb, normals[0], c1);
+            this->rotateTo(leftJoinType, ptsBuffer[0], c1);
         }
 
         if (rightT < 1) {
             SkChopCubicAt(currCubic, ptsBuffer, rightT);
-            this->lineTo(Verb::kInternalRoundJoin, ptsBuffer[3]);
+            this->lineTo(kInternalRoundJoinType, ptsBuffer[0], ptsBuffer[3]);
             currCubic = ptsBuffer + 3;
-            this->cubicTo(Verb::kInternalRoundJoin, currCubic, /*maxCurvatureT=*/0,
+            this->cubicTo(kInternalRoundJoinType, currCubic, /*maxCurvatureT=*/0,
                           kLeftMaxCurvatureNone, kRightMaxCurvatureNone);
         } else {
-            this->lineTo(Verb::kInternalRoundJoin, currCubic[3]);
+            this->lineTo(kInternalRoundJoinType, currCubic[0], currCubic[3]);
             SkPoint c2 = (currCubic[2] == currCubic[3]) ? currCubic[1] : currCubic[2];
-            this->rotateTo(Verb::kInternalRoundJoin, normals[1], currCubic[3]*2 - c2);
+            this->rotateTo(kInternalRoundJoinType, currCubic[3], currCubic[3]*2 - c2);
         }
         return;
     }
 
     // Recurse and check the other two points of max curvature, if any.
     if (kRightMaxCurvatureNone != rightMaxCurvatureT) {
-        this->cubicTo(leftJoinVerb, P, rightMaxCurvatureT, leftMaxCurvatureT,
+        this->cubicTo(leftJoinType, P, rightMaxCurvatureT, leftMaxCurvatureT,
                       kRightMaxCurvatureNone);
         return;
     }
     if (kLeftMaxCurvatureNone != leftMaxCurvatureT) {
         SkASSERT(kRightMaxCurvatureNone == rightMaxCurvatureT);
-        this->cubicTo(leftJoinVerb, P, leftMaxCurvatureT, kLeftMaxCurvatureNone,
+        this->cubicTo(leftJoinType, P, leftMaxCurvatureT, kLeftMaxCurvatureNone,
                       kRightMaxCurvatureNone);
         return;
     }
     if (numSegments > fMaxTessellationSegments) {
         SkPoint ptsBuffer[7];
         SkChopCubicAt(P, ptsBuffer, 0.5f);
-        this->cubicTo(leftJoinVerb, ptsBuffer, 0, kLeftMaxCurvatureNone, kRightMaxCurvatureNone);
-        this->cubicTo(Verb::kInternalRoundJoin, ptsBuffer + 3, 0, kLeftMaxCurvatureNone,
+        this->cubicTo(leftJoinType, ptsBuffer, 0, kLeftMaxCurvatureNone, kRightMaxCurvatureNone);
+        this->cubicTo(kInternalRoundJoinType, ptsBuffer + 3, 0, kLeftMaxCurvatureNone,
                       kRightMaxCurvatureNone);
         return;
     }
 
-    this->recordLeftJoinIfNotEmpty(leftJoinVerb, normals[0]);
-    fNormals.push_back_n(2, normals);
-
-    this->recordStroke(Verb::kCubicStroke, SkNextLog2(numSegments));
-    p1.store(&fPoints.push_back());
-    p2.store(&fPoints.push_back());
-    p3.store(&fPoints.push_back());
+    this->writeCubicSegment(leftJoinType, p0, p1, p2, p3);
 }
 
-void GrStrokeGeometry::recordStroke(Verb verb, int numSegmentsLog2) {
-    SkASSERT(Verb::kLinearStroke != verb || 0 == numSegmentsLog2);
-    fVerbs.push_back(verb);
-    if (Verb::kLinearStroke != verb) {
-        fParams.push_back().fNumLinearSegmentsLog2 = numSegmentsLog2;
-    }
-    ++fCurrStrokeTallies->fStrokes[numSegmentsLog2];
+void GrStrokeGeometry::rotateTo(float leftJoinType, const SkPoint& anchorPoint,
+                                const SkPoint& controlPoint) {
+    // Effectively rotate the current normal by drawing a zero length, 1-segment cubic.
+    // writeCubicSegment automatically adds the necessary join and the zero length cubic serves as
+    // a glue that guarantees a water tight rasterized edge between the new join and the segment
+    // that comes after the rotate.
+    SkPoint pts[4] = {anchorPoint, controlPoint, anchorPoint*2 - controlPoint, anchorPoint};
+    this->writeCubicSegment(leftJoinType, pts, 1);
 }
 
-void GrStrokeGeometry::rotateTo(Verb leftJoinVerb, SkVector normal, SkPoint controlPoint) {
-    SkASSERT(fPoints.count() > fCurrContourFirstPtIdx);
-    this->recordLeftJoinIfNotEmpty(leftJoinVerb, normal);
-    fVerbs.push_back(Verb::kRotate);
-    fPoints.push_back(controlPoint);
-    fPoints.push_back(fPoints[fPoints.count() - 2]);
-    fNormals.push_back(normal);
-}
-
-void GrStrokeGeometry::recordLeftJoinIfNotEmpty(Verb joinVerb, SkVector nextNormal) {
-    if (fNormals.count() <= fCurrContourFirstNormalIdx) {
-        // The contour is empty. Nothing to join with.
-        SkASSERT(fNormals.count() == fCurrContourFirstNormalIdx);
-        return;
-    }
-    fVerbs.push_back(joinVerb);
-}
-
-void GrStrokeGeometry::closeContour() {
-    SkASSERT(fInsideContour);
-    SkASSERT(fPoints.count() > fCurrContourFirstPtIdx);
-    if (fPoints.back() != fPoints[fCurrContourFirstPtIdx]) {
-        // Draw a line back to the beginning.
-        this->lineTo(fCurrStrokeJoinVerb, fPoints[fCurrContourFirstPtIdx]);
-    }
-    fVerbs.push_back(fCurrStrokeJoinVerb);
-    fVerbs.push_back(Verb::kEndContour);
-    SkDEBUGCODE(fInsideContour = false);
-}
-
-void GrStrokeGeometry::capContourAndExit() {
-    SkASSERT(fInsideContour);
-    if (fCurrContourFirstNormalIdx >= fNormals.count()) {
-        // This contour is empty. Add a normal in the direction that caps orient on empty geometry.
-        SkASSERT(fNormals.count() == fCurrContourFirstNormalIdx);
-        fNormals.push_back({1, 0});
-    }
-
-    this->recordCapsIfAny();
-    fVerbs.push_back(Verb::kEndContour);
-
-    SkDEBUGCODE(fInsideContour = false);
-}
-
-void GrStrokeGeometry::recordCapsIfAny() {
-    SkASSERT(fInsideContour);
-    SkASSERT(fCurrContourFirstNormalIdx < fNormals.count());
-
-    if (SkPaint::kButt_Cap == fCurrStrokeCapType) {
+void GrStrokeGeometry::close() {
+    if (!fHasPreviousSegment) {
+        // Draw caps instead of closing if the subpath is zero length:
+        //
+        //   "Any zero length subpath ...  shall be stroked if the 'stroke-linecap' property has a
+        //   value of round or square producing respectively a circle or a square."
+        //
+        //   (https://www.w3.org/TR/SVG11/painting.html#StrokeProperties)
+        //
+        this->writeCaps();
         return;
     }
 
-    Verb capVerb;
-    if (SkPaint::kSquare_Cap == fCurrStrokeCapType) {
-        if (fCurrStrokeRadius * SK_ScalarRoot2Over2 < kMaxErrorFromLinearization) {
-            return;
-        }
-        capVerb = Verb::kSquareCap;
-        fCurrStrokeTallies->fStrokes[0] += 2;
-    } else {
-        SkASSERT(SkPaint::kRound_Cap == fCurrStrokeCapType);
-        if (fCurrStrokeRadius < kMaxErrorFromLinearization) {
-            return;
-        }
-        capVerb = Verb::kRoundCap;
-        fCurrStrokeTallies->fTriangles += 2;
-        fCurrStrokeTallies->fConics += 4;
-    }
-
-    fVerbs.push_back(capVerb);
-    fVerbs.push_back(Verb::kEndContour);
-
-    fVerbs.push_back(capVerb);
-
-    // Reserve the space first, since push_back() takes the point by reference and might
-    // invalidate the reference if the array grows.
-    fPoints.reserve(fPoints.count() + 1);
-    fPoints.push_back(fPoints[fCurrContourFirstPtIdx]);
-
-    // Reserve the space first, since push_back() takes the normal by reference and might
-    // invalidate the reference if the array grows. (Although in this case we should be fine
-    // since there is a negate operator.)
-    fNormals.reserve(fNormals.count() + 1);
-    fNormals.push_back(-fNormals[fCurrContourFirstNormalIdx]);
+    // Draw a line back to the beginning. (This will be discarded if
+    // fCurrentPoint == fCurrContourStartPoint.)
+    this->lineTo(fCurrStrokeJoinType, fCurrentPoint, fCurrContourStartPoint);
+    this->writeJoin(fCurrStrokeJoinType, fCurrContourStartPoint, fLastControlPoint,
+                    fCurrContourFirstControlPoint);
 }
diff --git a/src/gpu/tessellate/GrStrokeGeometry.h b/src/gpu/tessellate/GrStrokeGeometry.h
index 2c4a757..2bc6340 100644
--- a/src/gpu/tessellate/GrStrokeGeometry.h
+++ b/src/gpu/tessellate/GrStrokeGeometry.h
@@ -11,120 +11,108 @@
 #include "include/core/SkPaint.h"
 #include "include/core/SkPoint.h"
 #include "include/private/SkTArray.h"
+#include "src/gpu/ops/GrMeshDrawOp.h"
+#include "src/gpu/tessellate/GrTessellateStrokeShader.h"
 
 class SkStrokeRec;
 
-/**
- * This class converts post-transform stroked paths into a set of independent strokes, joins, and
- * caps that map directly to GPU instances.
- */
+// This is an RAII class that expands strokes into tessellation patches for consumption by
+// GrTessellateStrokeShader. The provided GrMeshDrawOp::Target must not be used externally for the
+// entire lifetime of this class. e.g.:
+//
+//   void onPrepare(GrOpFlushState* target)  {
+//        GrStrokeGeometry g(target, &fMyVertexChunks, count);  // Locks target.
+//        for (...) {
+//            g.addPath(path, stroke);
+//        }
+//   }
+//   ... target can now be used normally again.
+//   ... fMyVertexChunks now contains chunks that can be drawn during onExecute.
 class GrStrokeGeometry {
 public:
-    static constexpr int kMaxNumLinearSegmentsLog2 = 15;
-
-    GrStrokeGeometry(int maxTessellationSegments, int numSkPoints = 0, int numSkVerbs = 0)
-            : fMaxTessellationSegments(maxTessellationSegments)
-            , fVerbs(numSkVerbs * 5/2)  // Reserve for a 2.5x expansion in verbs. (Joins get their
-                                        // own separate verb in our representation.)
-            , fParams(numSkVerbs * 3)  // Somewhere around 1-2 params per verb.
-            , fPoints(numSkPoints * 5/4)  // Reserve for a 1.25x expansion in points and normals.
-            , fNormals(numSkPoints * 5/4) {}
-
-    // A string of verbs and their corresponding, params, points, and normals are a compact
-    // representation of what will eventually be independent instances in GPU buffers.
-    enum class Verb : uint8_t {
-        kBeginPath,  // Instructs the iterator to advance its stroke width, atlas offset, etc.
-
-        // Independent strokes of a single line or curve, with (antialiased) butt caps on the ends.
-        kLinearStroke,
-        kQuadraticStroke,
-        kCubicStroke,
-
-        // Updates the last tangent without moving the current position on the stroke.
-        kRotate,
-
-        // Joins are a triangles that connect the outer corners of two adjoining strokes. Miters
-        // have an additional triangle cap on top of the bevel, and round joins have an arc on top.
-        kBevelJoin,
-        kMiterJoin,
-        kRoundJoin,
-
-        // We use internal joins when we have to internally break up a stroke because its curvature
-        // is too strong for a triangle strip. They are coverage-counted, self-intersecting
-        // quadrilaterals that tie the four corners of two adjoining strokes together a like a
-        // shoelace. (Coverage is negative on the inside half.) We place an arc on both ends of an
-        // internal round join.
-        kInternalBevelJoin,
-        kInternalRoundJoin,
-
-        kSquareCap,
-        kRoundCap,
-
-        kEndContour  // Instructs the iterator to advance its internal point and normal ptrs.
-    };
-    static bool IsInternalJoinVerb(Verb verb);
-
-    // Some verbs require additional parameters(s).
-    union Parameter {
-        // For cubic and quadratic strokes: How many flat line segments to chop the curve into?
-        int fNumLinearSegmentsLog2;
-        // For miter and round joins: How tall should the triangle cap be on top of the join?
-        // (This triangle is the conic control points for a round join.)
-        float fMiterCapHeightOverWidth;
-        float fConicWeight;  // Round joins only.
+    // We generate vertex 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 VertexChunk {
+        sk_sp<const GrBuffer> fVertexBuffer;
+        int fVertexCount = 0;
+        int fBaseVertex;
     };
 
-    const SkTArray<Verb, true>& verbs() const { SkASSERT(!fInsideContour); return fVerbs; }
-    const SkTArray<Parameter, true>& params() const { SkASSERT(!fInsideContour); return fParams; }
-    const SkTArray<SkPoint, true>& points() const { SkASSERT(!fInsideContour); return fPoints; }
-    const SkTArray<SkVector, true>& normals() const { SkASSERT(!fInsideContour); return fNormals; }
+    // Stores raw pointers to the provided target and vertexChunkArray, which this class will use
+    // and push to as addPath is called. The caller is responsible to bind and draw each chunk that
+    // gets pushed to the array. (See GrTessellateStrokeShader.)
+    GrStrokeGeometry(GrMeshDrawOp::Target* target, SkTArray<VertexChunk>* vertexChunkArray,
+                     int totalCombinedVerbCnt)
+            : fMaxTessellationSegments(target->caps().shaderCaps()->maxTessellationSegments())
+            , fTarget(target)
+            , fVertexChunkArray(vertexChunkArray) {
+        this->allocVertexChunk(
+                (totalCombinedVerbCnt * 3) * GrTessellateStrokeShader::kNumVerticesPerPatch);
+    }
 
-    // These track the numbers of instances required to draw all the recorded strokes.
-    struct InstanceTallies {
-        int fStrokes[kMaxNumLinearSegmentsLog2 + 1];
-        int fTriangles;
-        int fConics;
+    // "Releases" the target to be used externally again by putting back any unused pre-allocated
+    // vertices.
+    ~GrStrokeGeometry() {
+        fTarget->putBackVertices(fCurrChunkVertexCapacity - fVertexChunkArray->back().fVertexCount,
+                                 sizeof(SkPoint));
+    }
 
-        InstanceTallies operator+(const InstanceTallies&) const;
-    };
-
-    void beginPath(const SkStrokeRec&, float strokeDevWidth, InstanceTallies*);
-    void moveTo(SkPoint);
-    void lineTo(SkPoint);
-    void quadraticTo(const SkPoint[3]);
-    void cubicTo(const SkPoint[4]);
-    void closeContour();  // Connect back to the first point in the contour and exit.
-    void capContourAndExit();  // Add endcaps (if any) and exit the contour.
+    void addPath(const SkPath&, const SkStrokeRec&);
 
 private:
-    void lineTo(Verb leftJoinVerb, SkPoint);
-    void quadraticTo(Verb leftJoinVerb, const SkPoint[3], float maxCurvatureT);
+    void allocVertexChunk(int minVertexAllocCount);
+    SkPoint* reservePatch();
+
+    // Join types are written as floats in P4.x. See GrTessellateStrokeShader for definitions.
+    void writeCubicSegment(float leftJoinType, const SkPoint pts[4], float overrideNumSegments = 0);
+    void writeCubicSegment(float leftJoinType, const Sk2f& p0, const Sk2f& p1, const Sk2f& p2,
+                           const Sk2f& p3, float overrideNumSegments = 0) {
+        SkPoint pts[4];
+        p0.store(&pts[0]);
+        p1.store(&pts[1]);
+        p2.store(&pts[2]);
+        p3.store(&pts[3]);
+        this->writeCubicSegment(leftJoinType, pts, overrideNumSegments);
+    }
+    void writeJoin(float joinType, const SkPoint& anchorPoint, const SkPoint& prevControlPoint,
+                   const SkPoint& nextControlPoint);
+    void writeSquareCap(const SkPoint& endPoint, const SkPoint& controlPoint);
+    void writeCaps();
+
+    void beginPath(const SkStrokeRec&, float strokeDevWidth);
+    void moveTo(const SkPoint&);
+    void lineTo(const SkPoint& p0, const SkPoint& p1);
+    void quadraticTo(const SkPoint[3]);
+    void cubicTo(const SkPoint[4]);
+    void close();
+
+    void lineTo(float leftJoinType, const SkPoint& p0, const SkPoint& p1);
+    void quadraticTo(float leftJoinType, const SkPoint[3], float maxCurvatureT);
 
     static constexpr float kLeftMaxCurvatureNone = 1;
     static constexpr float kRightMaxCurvatureNone = 0;
-    void cubicTo(Verb leftJoinVerb, const SkPoint[4], float maxCurvatureT, float leftMaxCurvatureT,
+    void cubicTo(float leftJoinType, const SkPoint[4], float maxCurvatureT, float leftMaxCurvatureT,
                  float rightMaxCurvatureT);
 
-    // Pushes a new normal to fNormals and records a join, without changing the current position.
-    void rotateTo(Verb leftJoinVerb, SkVector normal, SkPoint controlPoint);
+    // TEMPORARY: Rotates the current control point without changing the current position.
+    // This is used when we convert a curve to a lineTo, and that behavior will soon go away.
+    void rotateTo(float leftJoinType, const SkPoint& anchorPoint, const SkPoint& controlPoint);
 
-    // Records a stroke in fElememts.
-    void recordStroke(Verb, int numSegmentsLog2);
+    const int fMaxTessellationSegments;
 
-    // Records a join in fElememts with the previous stroke, if the cuurent contour is not empty.
-    void recordLeftJoinIfNotEmpty(Verb joinType, SkVector nextNormal);
+    // These are raw pointers whose lifetimes are controlled outside this class.
+    GrMeshDrawOp::Target* const fTarget;
+    SkTArray<VertexChunk>* const fVertexChunkArray;
 
-    void recordCapsIfAny();
+    // Variables related to the vertex chunk that we are currently filling.
+    int fCurrChunkVertexCapacity;
+    int fCurrChunkMinVertexAllocCount;
+    SkPoint* fCurrChunkVertexData;
 
+    // Variables related to the path that we are currently iterating.
     float fCurrStrokeRadius;
-    Verb fCurrStrokeJoinVerb;
+    float fCurrStrokeJoinType;  // See GrTessellateStrokeShader for join type definitions .
     SkPaint::Cap fCurrStrokeCapType;
-    InstanceTallies* fCurrStrokeTallies = nullptr;
-
-    // We implement miters by placing a triangle-shaped cap on top of a bevel join. This field tells
-    // us what the miter limit is, restated in terms of how tall that triangle cap can be.
-    float fMiterMaxCapHeightOverWidth;
-
     // Any curvature on the original curve gets magnified on the outer edge of the stroke,
     // proportional to how thick the stroke radius is. This field tells us the maximum curvature we
     // can tolerate using the current stroke radius, before linearization artifacts begin to appear
@@ -134,47 +122,12 @@
     // section with strong curvature into lineTo's with round joins in between.)
     float fMaxCurvatureCosTheta;
 
-    int fCurrContourFirstPtIdx;
-    int fCurrContourFirstNormalIdx;
-
-    SkDEBUGCODE(bool fInsideContour = false);
-
-    const int fMaxTessellationSegments;
-    SkSTArray<128, Verb, true> fVerbs;
-    SkSTArray<128, Parameter, true> fParams;
-    SkSTArray<128, SkPoint, true> fPoints;
-    SkSTArray<128, SkVector, true> fNormals;
+    // Variables related to the specific contour that we are currently iterating.
+    bool fHasPreviousSegment = false;
+    SkPoint fCurrContourStartPoint;
+    SkPoint fCurrContourFirstControlPoint;
+    SkPoint fLastControlPoint;
+    SkPoint fCurrentPoint;
 };
 
-inline GrStrokeGeometry::InstanceTallies GrStrokeGeometry::InstanceTallies::operator+(
-        const InstanceTallies& t) const {
-    InstanceTallies ret;
-    for (int i = 0; i <= kMaxNumLinearSegmentsLog2; ++i) {
-        ret.fStrokes[i] = fStrokes[i] + t.fStrokes[i];
-    }
-    ret.fTriangles = fTriangles + t.fTriangles;
-    ret.fConics = fConics + t.fConics;
-    return ret;
-}
-
-inline bool GrStrokeGeometry::IsInternalJoinVerb(Verb verb) {
-    switch (verb) {
-        case Verb::kInternalBevelJoin:
-        case Verb::kInternalRoundJoin:
-            return true;
-        case Verb::kBeginPath:
-        case Verb::kLinearStroke:
-        case Verb::kQuadraticStroke:
-        case Verb::kCubicStroke:
-        case Verb::kRotate:
-        case Verb::kBevelJoin:
-        case Verb::kMiterJoin:
-        case Verb::kRoundJoin:
-        case Verb::kSquareCap:
-        case Verb::kRoundCap:
-        case Verb::kEndContour:
-            return false;
-    }
-    SK_ABORT("Invalid GrStrokeGeometry::Verb.");
-}
 #endif
diff --git a/src/gpu/tessellate/GrTessellateStrokeOp.cpp b/src/gpu/tessellate/GrTessellateStrokeOp.cpp
index f8b274f..854d4ab 100644
--- a/src/gpu/tessellate/GrTessellateStrokeOp.cpp
+++ b/src/gpu/tessellate/GrTessellateStrokeOp.cpp
@@ -1,5 +1,5 @@
 /*
- * Copyright 2019 Google LLC.
+ * Copyright 2020 Google LLC.
  *
  * Use of this source code is governed by a BSD-style license that can be
  * found in the LICENSE file.
@@ -44,8 +44,7 @@
                                            GrAAType aaType)
         : GrDrawOp(ClassID())
         , fPathStrokes(transform_path(viewMatrix, path), transform_stroke(viewMatrix, stroke))
-        , fNumVerbs(path.countVerbs())
-        , fNumPoints(path.countPoints())
+        , fTotalCombinedVerbCnt(path.countVerbs())
         , fColor(get_paint_constant_blended_color(paint))
         , fAAType(aaType)
         , fProcessors(std::move(paint)) {
@@ -100,8 +99,7 @@
         SkASSERT(fMiterLimitOrZero == 0 || fMiterLimitOrZero == op->fMiterLimitOrZero);
         fMiterLimitOrZero = op->fMiterLimitOrZero;
     }
-    fNumVerbs += op->fNumVerbs;
-    fNumPoints += op->fNumPoints;
+    fTotalCombinedVerbCnt += op->fTotalCombinedVerbCnt;
 
     return CombineResult::kMerged;
 }
@@ -110,226 +108,14 @@
                                         GrAppliedClip*, const GrXferProcessor::DstProxyView&) {
 }
 
-static SkPoint lerp(const SkPoint& a, const SkPoint& b, float T) {
-    SkASSERT(1 != T);  // The below does not guarantee lerp(a, b, 1) === b.
-    return (b - a) * T + a;
-}
-
-static void write_line(SkPoint* patch, const SkPoint& p0, const SkPoint& p1) {
-    patch[0] = p0;
-    patch[1] = lerp(p0, p1, 1/3.f);
-    patch[2] = lerp(p0, p1, 2/3.f);
-    patch[3] = p1;
-}
-
-static void write_quadratic(SkPoint* patch, const SkPoint pts[]) {
-    patch[0] = pts[0];
-    patch[1] = lerp(pts[0], pts[1], 2/3.f);
-    patch[2] = lerp(pts[1], pts[2], 1/3.f);
-    patch[3] = pts[2];
-}
-
-static void write_loop(SkPoint* patch, const SkPoint& intersectionPoint,
-                       const SkPoint lastControlPt, const SkPoint& nextControlPt) {
-    patch[0] = intersectionPoint;
-    patch[1] = lastControlPt;
-    patch[2] = nextControlPt;
-    patch[3] = intersectionPoint;
-}
-
-static void write_square_cap(SkPoint* patch, const SkPoint& endPoint,
-                             const SkPoint controlPoint, float strokeRadius) {
-    SkVector v = (endPoint - controlPoint);
-    v.normalize();
-    SkPoint capPoint = endPoint + v*strokeRadius;
-    // Construct a line that incorporates controlPoint so we get a water tight edge with the rest of
-    // the stroke. The cubic will technically step outside the cap, but we will force it to only
-    // have one segment, giving edges only at the endpoints.
-    patch[0] = endPoint;
-    patch[1] = controlPoint;
-    // Straddle the midpoint of the cap because the tessellated geometry emits a center point at
-    // T=.5, and we need to ensure that point stays inside the cap.
-    patch[2] = endPoint + capPoint - controlPoint;
-    patch[3] = capPoint;
-}
-
 void GrTessellateStrokeOp::onPrepare(GrOpFlushState* flushState) {
-    // Rebuild the stroke using GrStrokeGeometry.
-    GrStrokeGeometry strokeGeometry(flushState->caps().shaderCaps()->maxTessellationSegments(),
-                                    fNumPoints, fNumVerbs);
+    GrStrokeGeometry strokeGeometry(flushState, &fVertexChunks, fTotalCombinedVerbCnt);
     for (auto& [path, stroke] : fPathStrokes) {
-        float strokeRadius = stroke.getWidth() * .5f;
-        GrStrokeGeometry::InstanceTallies tallies = GrStrokeGeometry::InstanceTallies();
-        strokeGeometry.beginPath(stroke, strokeRadius * 2, &tallies);
-        SkPathVerb previousVerb = SkPathVerb::kClose;
-        for (auto [verb, pts, w] : SkPathPriv::Iterate(path)) {
-            switch (verb) {
-                case SkPathVerb::kMove:
-                    if (previousVerb != SkPathVerb::kClose) {
-                        strokeGeometry.capContourAndExit();
-                    }
-                    strokeGeometry.moveTo(pts[0]);
-                    break;
-                case SkPathVerb::kClose:
-                    strokeGeometry.closeContour();
-                    break;
-                case SkPathVerb::kLine:
-                    strokeGeometry.lineTo(pts[1]);
-                    break;
-                case SkPathVerb::kQuad:
-                    strokeGeometry.quadraticTo(pts);
-                    break;
-                case SkPathVerb::kCubic:
-                    strokeGeometry.cubicTo(pts);
-                    break;
-                case SkPathVerb::kConic:
-                    SkUNREACHABLE;
-            }
-            previousVerb = verb;
-        }
-        if (previousVerb != SkPathVerb::kClose) {
-            strokeGeometry.capContourAndExit();
-        }
+        strokeGeometry.addPath(path, stroke);
     }
-
-    auto vertexData = static_cast<SkPoint*>(flushState->makeVertexSpace(
-            sizeof(SkPoint), strokeGeometry.verbs().count() * 2 * 5, &fVertexBuffer, &fBaseVertex));
-    if (!vertexData) {
-        return;
-    }
-
-    using Verb = GrStrokeGeometry::Verb;
-
-    // Dump GrStrokeGeometry into tessellation patches.
-    //
-    // This loop is only a temporary adapter for GrStrokeGeometry so we can bootstrap the
-    // tessellation shaders. Once the shaders are landed and tested, we will overhaul
-    // GrStrokeGeometry and remove this loop.
-    int i = 0;
-    const SkTArray<SkPoint, true>& pathPts = strokeGeometry.points();
-    auto pendingJoin = Verb::kEndContour;
-    SkPoint firstJoinControlPoint = {0, 0};
-    SkPoint lastJoinControlPoint = {0, 0};
-    bool hasFirstControlPoint = false;
-    float currStrokeRadius = 0;
-    auto pathStrokesIter = fPathStrokes.begin();
-    for (auto verb : strokeGeometry.verbs()) {
-        SkPoint patch[4];
-        float overrideNumSegments = 0;
-        switch (verb) {
-            case Verb::kBeginPath:
-                SkASSERT(pathStrokesIter != fPathStrokes.end());
-                pendingJoin = Verb::kEndContour;
-                firstJoinControlPoint = {0, 0};
-                lastJoinControlPoint = {0, 0};
-                hasFirstControlPoint = false;
-                currStrokeRadius = (*pathStrokesIter).fStroke.getWidth() * .5f;
-                ++pathStrokesIter;
-                continue;
-            case Verb::kRoundJoin:
-            case Verb::kInternalRoundJoin:
-            case Verb::kMiterJoin:
-            case Verb::kBevelJoin:
-            case Verb::kInternalBevelJoin:
-                pendingJoin = verb;
-                continue;
-            case Verb::kLinearStroke:
-                write_line(patch, pathPts[i], pathPts[i+1]);
-                ++i;
-                break;
-            case Verb::kQuadraticStroke:
-                write_quadratic(patch, &pathPts[i]);
-                i += 2;
-                break;
-            case Verb::kCubicStroke:
-                memcpy(patch, &pathPts[i], sizeof(SkPoint) * 4);
-                i += 3;
-                break;
-            case Verb::kRotate:
-                write_loop(patch, pathPts[i], pathPts[i+1], pathPts[i]*2 - pathPts[i+1]);
-                i += 2;
-                break;
-            case Verb::kSquareCap: {
-                SkASSERT(pendingJoin == Verb::kEndContour);
-                write_square_cap(patch, pathPts[i], lastJoinControlPoint, currStrokeRadius);
-                // This cubic steps outside the cap, but if we force it to only have one segment, we
-                // will just get the rectangular cap.
-                overrideNumSegments = 1;
-                break;
-            }
-            case Verb::kRoundCap:
-                // A round cap is the same thing as a 180-degree round join.
-                SkASSERT(pendingJoin == Verb::kEndContour);
-                pendingJoin = Verb::kRoundJoin;
-                write_loop(patch, pathPts[i], lastJoinControlPoint, lastJoinControlPoint);
-                break;
-            case Verb::kEndContour:
-                // Final join
-                write_loop(patch, pathPts[i], firstJoinControlPoint, lastJoinControlPoint);
-                ++i;
-                break;
-        }
-
-        SkPoint c1 = (patch[1] == patch[0]) ? patch[2] : patch[1];
-        SkPoint c2 = (patch[2] == patch[3]) ? patch[1] : patch[2];
-
-        if (pendingJoin != Verb::kEndContour) {
-            vertexData[0] = patch[0];
-            vertexData[1] = lastJoinControlPoint;
-            vertexData[2] = c1;
-            vertexData[3] = patch[0];
-            switch (pendingJoin) {
-                case Verb::kBevelJoin:
-                    vertexData[4].set(1, currStrokeRadius);
-                    break;
-                case Verb::kMiterJoin:
-                    vertexData[4].set(2, currStrokeRadius);
-                    break;
-                case Verb::kRoundJoin:
-                    vertexData[4].set(3, currStrokeRadius);
-                    break;
-                case Verb::kInternalRoundJoin:
-                case Verb::kInternalBevelJoin:
-                default:
-                    vertexData[4].set(4, currStrokeRadius);
-                    break;
-            }
-            vertexData += 5;
-            fVertexCount += 5;
-            pendingJoin = Verb::kEndContour;
-        }
-
-        if (verb != Verb::kRoundCap) {
-            if (!hasFirstControlPoint) {
-                firstJoinControlPoint = c1;
-                hasFirstControlPoint = true;
-            }
-            lastJoinControlPoint = c2;
-        }
-
-        if (verb == Verb::kEndContour) {
-            // Temporary hack for this adapter in case the next contour is a round cap.
-            lastJoinControlPoint = firstJoinControlPoint;
-            hasFirstControlPoint = false;
-        } else if (verb != Verb::kRotate && verb != Verb::kRoundCap) {
-            memcpy(vertexData, patch, sizeof(SkPoint) * 4);
-            vertexData[4].set(-overrideNumSegments, currStrokeRadius);
-            vertexData += 5;
-            fVertexCount += 5;
-        }
-    }
-    SkASSERT(pathStrokesIter == fPathStrokes.end());
-
-    SkASSERT(fVertexCount <= strokeGeometry.verbs().count() * 2 * 5);
-    flushState->putBackVertices(strokeGeometry.verbs().count() * 2 * 5 - fVertexCount,
-                                sizeof(SkPoint));
 }
 
 void GrTessellateStrokeOp::onExecute(GrOpFlushState* flushState, const SkRect& chainBounds) {
-    if (!fVertexBuffer) {
-        return;
-    }
-
     GrPipeline::InitArgs initArgs;
     if (GrAAType::kNone != fAAType) {
         initArgs.fInputFlags |= GrPipeline::InputFlags::kHWAntialias;
@@ -345,9 +131,14 @@
     GrTessellateStrokeShader strokeShader(fViewMatrix, fColor, fMiterLimitOrZero);
     GrPathShader::ProgramInfo programInfo(flushState->writeView(), &pipeline, &strokeShader);
 
-    flushState->bindPipelineAndScissorClip(programInfo, this->bounds() /*chainBounds??*/);
+    SkASSERT(chainBounds == this->bounds());
+    flushState->bindPipelineAndScissorClip(programInfo, this->bounds());
     flushState->bindTextures(strokeShader, nullptr, pipeline);
 
-    flushState->bindBuffers(nullptr, nullptr, std::move(fVertexBuffer));
-    flushState->draw(fVertexCount, fBaseVertex);
+    for (const auto& chunk : fVertexChunks) {
+        if (chunk.fVertexBuffer) {
+            flushState->bindBuffers(nullptr, nullptr, std::move(chunk.fVertexBuffer));
+            flushState->draw(chunk.fVertexCount, chunk.fBaseVertex);
+        }
+    }
 }
diff --git a/src/gpu/tessellate/GrTessellateStrokeOp.h b/src/gpu/tessellate/GrTessellateStrokeOp.h
index 5326164..23b0f2e 100644
--- a/src/gpu/tessellate/GrTessellateStrokeOp.h
+++ b/src/gpu/tessellate/GrTessellateStrokeOp.h
@@ -11,6 +11,7 @@
 #include "include/core/SkStrokeRec.h"
 #include "src/gpu/GrSTArenaList.h"
 #include "src/gpu/ops/GrDrawOp.h"
+#include "src/gpu/tessellate/GrStrokeGeometry.h"
 
 // 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
@@ -45,8 +46,7 @@
     };
 
     GrSTArenaList<PathStroke> fPathStrokes;
-    int fNumVerbs;
-    int fNumPoints;
+    int fTotalCombinedVerbCnt;
 
     SkPMColor4f fColor;
     const SkMatrix fViewMatrix = SkMatrix::I();
@@ -54,9 +54,8 @@
     float fMiterLimitOrZero = 0;  // Zero if there is not a stroke with a miter join type.
     GrProcessorSet fProcessors;
 
-    sk_sp<const GrBuffer> fVertexBuffer;
-    int fVertexCount = 0;
-    int fBaseVertex;
+    // S=1 because we will almost always fit everything into one single chunk.
+    SkSTArray<1, GrStrokeGeometry::VertexChunk> fVertexChunks;
 
     friend class GrOpMemoryPool;  // For ctor.
 };
diff --git a/src/gpu/tessellate/GrTessellateStrokeShader.h b/src/gpu/tessellate/GrTessellateStrokeShader.h
index 262a40d..5e5fbc4 100644
--- a/src/gpu/tessellate/GrTessellateStrokeShader.h
+++ b/src/gpu/tessellate/GrTessellateStrokeShader.h
@@ -38,9 +38,16 @@
 // tessellationPatchVertexCount of 5.
 class GrTessellateStrokeShader : public GrPathShader {
 public:
+    constexpr static float kBevelJoinType = 1;
+    constexpr static float kMiterJoinType = 2;
+    constexpr static float kRoundJoinType = 3;
+    constexpr static float kInternalRoundJoinType = 4;
+
+    constexpr static int kNumVerticesPerPatch = 5;
+
     GrTessellateStrokeShader(const SkMatrix& viewMatrix, SkPMColor4f color, float miterLimitOrZero)
             : GrPathShader(kTessellate_GrTessellateStrokeShader_ClassID, viewMatrix,
-                           GrPrimitiveType::kPatches, 5)
+                           GrPrimitiveType::kPatches, kNumVerticesPerPatch)
             , fColor(color)
             , fMiterLimitOrZero(miterLimitOrZero) {
         constexpr static Attribute kInputPointAttrib{"inputPoint", kFloat2_GrVertexAttribType,