Implement stroking with GPU tessellation

Bootstraps tessellated stroking using GrStrokeGeometry mostly as
written. Strokes get decomposed into tessellation patches that
represent either a "cubic" (single stroked bezier curve with butt
caps) or a "join". The patches get drawn directly to the canvas
without any intermediate stencil steps. For the first revision, only
opaque, constant-color strokes are supported.

Bug: skia:10419
Change-Id: I601289189b93ebdf2f1efecd08628a6e0d9acb01
Reviewed-on: https://skia-review.googlesource.com/c/skia/+/299142
Reviewed-by: Michael Ludwig <michaelludwig@google.com>
Commit-Queue: Chris Dalton <csmartdalton@google.com>
diff --git a/src/gpu/GrProcessor.h b/src/gpu/GrProcessor.h
index 80ec7e3..ebddb0c 100644
--- a/src/gpu/GrProcessor.h
+++ b/src/gpu/GrProcessor.h
@@ -164,6 +164,7 @@
         kTessellate_GrMiddleOutCubicShader_ClassID,
         kTessellate_GrStencilTriangleShader_ClassID,
         kTessellate_GrTessellateCubicShader_ClassID,
+        kTessellate_GrTessellateStrokeShader_ClassID,
         kTessellate_GrTessellateWedgeShader_ClassID,
         kTestFP_ClassID,
         kTestRectOp_ClassID,
diff --git a/src/gpu/tessellate/GrStrokeGeometry.cpp b/src/gpu/tessellate/GrStrokeGeometry.cpp
index b097c73..9ff4c5c 100644
--- a/src/gpu/tessellate/GrStrokeGeometry.cpp
+++ b/src/gpu/tessellate/GrStrokeGeometry.cpp
@@ -155,13 +155,7 @@
 
     // Decide how many flat line segments to chop the curve into.
     int numSegments = wangs_formula_quadratic(p0, p1, p2);
-    numSegments = std::min(numSegments, 1 << kMaxNumLinearSegmentsLog2);
-    if (numSegments <= 1) {
-        this->rotateTo(leftJoinVerb, normals[0]);
-        this->lineTo(Verb::kInternalRoundJoin, P[2]);
-        this->rotateTo(Verb::kInternalRoundJoin, normals[1]);
-        return;
-    }
+    numSegments = std::clamp(numSegments, 1, 1 << kMaxNumLinearSegmentsLog2);
 
     // At + B gives a vector tangent to the quadratic.
     Sk2f A = p0 - p1*2 + p2;
@@ -193,8 +187,7 @@
     // FIXME: This is quite costly and the vast majority of curves only have moderate curvature. We
     // would benefit significantly from a quick reject that detects curves that don't need special
     // treatment for strong curvature.
-    bool isCurvatureTooStrong = calc_curvature_costheta(leftTan, rightTan) < fMaxCurvatureCosTheta;
-    if (isCurvatureTooStrong) {
+    if (numSegments > 1 && calc_curvature_costheta(leftTan, rightTan) < fMaxCurvatureCosTheta) {
         SkPoint ptsBuffer[5];
         const SkPoint* currQuadratic = P;
 
@@ -206,7 +199,7 @@
             }
             currQuadratic = ptsBuffer + 2;
         } else {
-            this->rotateTo(leftJoinVerb, normals[0]);
+            this->rotateTo(leftJoinVerb, normals[0], currQuadratic[1]);
         }
 
         if (rightT < 1) {
@@ -215,10 +208,18 @@
             this->quadraticTo(Verb::kInternalRoundJoin, ptsBuffer + 2, /*maxCurvatureT=*/0);
         } else {
             this->lineTo(Verb::kInternalRoundJoin, currQuadratic[2]);
-            this->rotateTo(Verb::kInternalRoundJoin, normals[1]);
+            this->rotateTo(Verb::kInternalRoundJoin, normals[1],
+                           currQuadratic[2]*2 - currQuadratic[1]);
         }
         return;
     }
+    if (numSegments > fMaxTessellationSegments) {
+        SkPoint ptsBuffer[5];
+        SkChopQuadAt(P, ptsBuffer, 0.5f);
+        this->quadraticTo(leftJoinVerb, ptsBuffer, 0);
+        this->quadraticTo(Verb::kInternalRoundJoin, ptsBuffer + 3, 0);
+        return;
+    }
 
     this->recordLeftJoinIfNotEmpty(leftJoinVerb, normals[0]);
     fNormals.push_back_n(2, normals);
@@ -283,13 +284,7 @@
 
     // Decide how many flat line segments to chop the curve into.
     int numSegments = wangs_formula_cubic(p0, p1, p2, p3);
-    numSegments = std::min(numSegments, 1 << kMaxNumLinearSegmentsLog2);
-    if (numSegments <= 1) {
-        this->rotateTo(leftJoinVerb, normals[0]);
-        this->lineTo(leftJoinVerb, P[3]);
-        this->rotateTo(Verb::kInternalRoundJoin, normals[1]);
-        return;
-    }
+    numSegments = std::clamp(numSegments, 1, 1 << kMaxNumLinearSegmentsLog2);
 
     // 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.)
@@ -323,8 +318,7 @@
     // FIXME: This is quite costly and the vast majority of curves only have moderate curvature. We
     // would benefit significantly from a quick reject that detects curves that don't need special
     // treatment for strong curvature.
-    bool isCurvatureTooStrong = calc_curvature_costheta(leftTan, rightTan) < fMaxCurvatureCosTheta;
-    if (isCurvatureTooStrong) {
+    if (numSegments > 1 && calc_curvature_costheta(leftTan, rightTan) < fMaxCurvatureCosTheta) {
         SkPoint ptsBuffer[7];
         p0.store(ptsBuffer);
         p1.store(ptsBuffer + 1);
@@ -346,7 +340,8 @@
             }
             currCubic = ptsBuffer + 3;
         } else {
-            this->rotateTo(leftJoinVerb, normals[0]);
+            SkPoint c1 = (ptsBuffer[1] == ptsBuffer[0]) ? ptsBuffer[2] : ptsBuffer[1];
+            this->rotateTo(leftJoinVerb, normals[0], c1);
         }
 
         if (rightT < 1) {
@@ -357,7 +352,8 @@
                           kLeftMaxCurvatureNone, kRightMaxCurvatureNone);
         } else {
             this->lineTo(Verb::kInternalRoundJoin, currCubic[3]);
-            this->rotateTo(Verb::kInternalRoundJoin, normals[1]);
+            SkPoint c2 = (currCubic[2] == currCubic[3]) ? currCubic[1] : currCubic[2];
+            this->rotateTo(Verb::kInternalRoundJoin, normals[1], currCubic[3]*2 - c2);
         }
         return;
     }
@@ -374,6 +370,14 @@
                       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,
+                      kRightMaxCurvatureNone);
+        return;
+    }
 
     this->recordLeftJoinIfNotEmpty(leftJoinVerb, normals[0]);
     fNormals.push_back_n(2, normals);
@@ -386,17 +390,19 @@
 
 void GrStrokeGeometry::recordStroke(Verb verb, int numSegmentsLog2) {
     SkASSERT(Verb::kLinearStroke != verb || 0 == numSegmentsLog2);
-    SkASSERT(numSegmentsLog2 <= kMaxNumLinearSegmentsLog2);
     fVerbs.push_back(verb);
     if (Verb::kLinearStroke != verb) {
-        SkASSERT(numSegmentsLog2 > 0);
         fParams.push_back().fNumLinearSegmentsLog2 = numSegmentsLog2;
     }
     ++fCurrStrokeTallies->fStrokes[numSegmentsLog2];
 }
 
-void GrStrokeGeometry::rotateTo(Verb leftJoinVerb, SkVector normal) {
+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);
 }
 
@@ -406,105 +412,7 @@
         SkASSERT(fNormals.count() == fCurrContourFirstNormalIdx);
         return;
     }
-
-    if (Verb::kBevelJoin == joinVerb) {
-        this->recordBevelJoin(Verb::kBevelJoin);
-        return;
-    }
-
-    Sk2f n0 = Sk2f::Load(&fNormals.back());
-    Sk2f n1 = Sk2f::Load(&nextNormal);
-    Sk2f base = n1 - n0;
-    if ((base.abs() * fCurrStrokeRadius < kMaxErrorFromLinearization).allTrue()) {
-        // Treat any join as a bevel when the outside corners of the two adjoining strokes are
-        // close enough to each other. This is important because "miterCapHeightOverWidth" becomes
-        // unstable when n0 and n1 are nearly equal.
-        this->recordBevelJoin(joinVerb);
-        return;
-    }
-
-    // We implement miters and round joins by placing a triangle-shaped cap on top of a bevel join.
-    // (For round joins this triangle cap comprises the conic control points.) Find how tall to make
-    // this triangle cap, relative to its width.
-    //
-    // NOTE: This value would be infinite at 180 degrees, but we clamp miterCapHeightOverWidth at
-    // near-infinity. 180-degree round joins still look perfectly acceptable like this (though
-    // technically not pure arcs).
-    Sk2f cross = base * SkNx_shuffle<1,0>(n0);
-    Sk2f dot = base * n0;
-    float miterCapHeight = SkScalarAbs(dot[0] + dot[1]);
-    float miterCapWidth = SkScalarAbs(cross[0] - cross[1]) * 2;
-
-    if (Verb::kMiterJoin == joinVerb) {
-        if (miterCapHeight > fMiterMaxCapHeightOverWidth * miterCapWidth) {
-            // This join is tighter than the miter limit. Treat it as a bevel.
-            this->recordBevelJoin(Verb::kMiterJoin);
-            return;
-        }
-        this->recordMiterJoin(miterCapHeight / miterCapWidth);
-        return;
-    }
-
-    SkASSERT(Verb::kRoundJoin == joinVerb || Verb::kInternalRoundJoin == joinVerb);
-
-    // Conic arcs become unstable when they approach 180 degrees. When the conic control point
-    // begins shooting off to infinity (i.e., height/width > 32), split the conic into two.
-    static constexpr float kAlmost180Degrees = 32;
-    if (miterCapHeight > kAlmost180Degrees * miterCapWidth) {
-        Sk2f bisect = normalize(n0 - n1);
-        this->rotateTo(joinVerb, SkVector::Make(-bisect[1], bisect[0]));
-        this->recordLeftJoinIfNotEmpty(joinVerb, nextNormal);
-        return;
-    }
-
-    float miterCapHeightOverWidth = miterCapHeight / miterCapWidth;
-
-    // Find the heights of this round join's conic control point as well as the arc itself.
-    Sk2f X, Y;
-    transpose(base * base, n0 * n1, &X, &Y);
-    Sk2f r = Sk2f::Max(X + Y + Sk2f(0, 1), 0.f).sqrt();
-    Sk2f heights = SkNx_fma(r, Sk2f(miterCapHeightOverWidth, -SK_ScalarRoot2Over2), Sk2f(0, 1));
-    float controlPointHeight = SkScalarAbs(heights[0]);
-    float curveHeight = heights[1];
-    if (curveHeight * fCurrStrokeRadius < kMaxErrorFromLinearization) {
-        // Treat round joins as bevels when their curvature is nearly flat.
-        this->recordBevelJoin(joinVerb);
-        return;
-    }
-
-    float w = curveHeight / (controlPointHeight - curveHeight);
-    this->recordRoundJoin(joinVerb, miterCapHeightOverWidth, w);
-}
-
-void GrStrokeGeometry::recordBevelJoin(Verb originalJoinVerb) {
-    if (!IsInternalJoinVerb(originalJoinVerb)) {
-        fVerbs.push_back(Verb::kBevelJoin);
-        ++fCurrStrokeTallies->fTriangles;
-    } else {
-        fVerbs.push_back(Verb::kInternalBevelJoin);
-        fCurrStrokeTallies->fTriangles += 2;
-    }
-}
-
-void GrStrokeGeometry::recordMiterJoin(float miterCapHeightOverWidth) {
-    fVerbs.push_back(Verb::kMiterJoin);
-    fParams.push_back().fMiterCapHeightOverWidth = miterCapHeightOverWidth;
-    fCurrStrokeTallies->fTriangles += 2;
-}
-
-void GrStrokeGeometry::recordRoundJoin(Verb joinVerb, float miterCapHeightOverWidth,
-                                         float conicWeight) {
     fVerbs.push_back(joinVerb);
-    fParams.push_back().fConicWeight = conicWeight;
-    fParams.push_back().fMiterCapHeightOverWidth = miterCapHeightOverWidth;
-    if (Verb::kRoundJoin == joinVerb) {
-        ++fCurrStrokeTallies->fTriangles;
-        ++fCurrStrokeTallies->fConics;
-    } else {
-        SkASSERT(Verb::kInternalRoundJoin == joinVerb);
-        fCurrStrokeTallies->fTriangles += 2;
-        fCurrStrokeTallies->fConics += 2;
-    }
 }
 
 void GrStrokeGeometry::closeContour() {
@@ -514,14 +422,7 @@
         // Draw a line back to the beginning.
         this->lineTo(fCurrStrokeJoinVerb, fPoints[fCurrContourFirstPtIdx]);
     }
-    if (fNormals.count() > fCurrContourFirstNormalIdx) {
-        // Join the first and last lines.
-        this->rotateTo(fCurrStrokeJoinVerb,fNormals[fCurrContourFirstNormalIdx]);
-    } else {
-        // This contour is empty. Add a bogus normal since the iterator always expects one.
-        SkASSERT(fNormals.count() == fCurrContourFirstNormalIdx);
-        fNormals.push_back({0, 0});
-    }
+    fVerbs.push_back(fCurrStrokeJoinVerb);
     fVerbs.push_back(Verb::kEndContour);
     SkDEBUGCODE(fInsideContour = false);
 }
diff --git a/src/gpu/tessellate/GrStrokeGeometry.h b/src/gpu/tessellate/GrStrokeGeometry.h
index 98a85ff..2c4a757 100644
--- a/src/gpu/tessellate/GrStrokeGeometry.h
+++ b/src/gpu/tessellate/GrStrokeGeometry.h
@@ -22,8 +22,9 @@
 public:
     static constexpr int kMaxNumLinearSegmentsLog2 = 15;
 
-    GrStrokeGeometry(int numSkPoints = 0, int numSkVerbs = 0)
-            : fVerbs(numSkVerbs * 5/2)  // Reserve for a 2.5x expansion in verbs. (Joins get their
+    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.
@@ -39,6 +40,9 @@
         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,
@@ -102,16 +106,13 @@
                  float rightMaxCurvatureT);
 
     // Pushes a new normal to fNormals and records a join, without changing the current position.
-    void rotateTo(Verb leftJoinVerb, SkVector normal);
+    void rotateTo(Verb leftJoinVerb, SkVector normal, SkPoint controlPoint);
 
     // Records a stroke in fElememts.
     void recordStroke(Verb, int numSegmentsLog2);
 
     // Records a join in fElememts with the previous stroke, if the cuurent contour is not empty.
     void recordLeftJoinIfNotEmpty(Verb joinType, SkVector nextNormal);
-    void recordBevelJoin(Verb originalJoinVerb);
-    void recordMiterJoin(float miterCapHeightOverWidth);
-    void recordRoundJoin(Verb roundJoinVerb, float miterCapHeightOverWidth, float conicWeight);
 
     void recordCapsIfAny();
 
@@ -138,6 +139,7 @@
 
     SkDEBUGCODE(bool fInsideContour = false);
 
+    const int fMaxTessellationSegments;
     SkSTArray<128, Verb, true> fVerbs;
     SkSTArray<128, Parameter, true> fParams;
     SkSTArray<128, SkPoint, true> fPoints;
@@ -164,6 +166,7 @@
         case Verb::kLinearStroke:
         case Verb::kQuadraticStroke:
         case Verb::kCubicStroke:
+        case Verb::kRotate:
         case Verb::kBevelJoin:
         case Verb::kMiterJoin:
         case Verb::kRoundJoin:
diff --git a/src/gpu/tessellate/GrTessellateStrokeOp.cpp b/src/gpu/tessellate/GrTessellateStrokeOp.cpp
new file mode 100644
index 0000000..7c1c29c
--- /dev/null
+++ b/src/gpu/tessellate/GrTessellateStrokeOp.cpp
@@ -0,0 +1,307 @@
+/*
+ * Copyright 2019 Google LLC.
+ *
+ * Use of this source code is governed by a BSD-style license that can be
+ * found in the LICENSE file.
+ */
+
+#include "src/gpu/tessellate/GrTessellateStrokeOp.h"
+
+#include "src/core/SkPathPriv.h"
+#include "src/gpu/tessellate/GrStrokeGeometry.h"
+#include "src/gpu/tessellate/GrTessellateStrokeShader.h"
+
+static SkPath transform_path(const SkMatrix& viewMatrix, const SkPath& path) {
+    SkPath devPath;
+    // 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.
+    SkASSERT(viewMatrix.isSimilarity());
+    path.transform(viewMatrix, &devPath);
+    return devPath;
+}
+
+static SkStrokeRec transform_stroke(const SkMatrix& viewMatrix, const SkStrokeRec& stroke) {
+    SkStrokeRec devStroke = stroke;
+    // kStrokeAndFill_Style is not yet supported.
+    SkASSERT(stroke.getStyle() == SkStrokeRec::kStroke_Style ||
+             stroke.getStyle() == SkStrokeRec::kHairline_Style);
+    float strokeWidth = (stroke.getStyle() == SkStrokeRec::kHairline_Style) ?
+            1 : viewMatrix.getMaxScale() * stroke.getWidth();
+    devStroke.setStrokeStyle(strokeWidth, /*strokeAndFill=*/false);
+    return devStroke;
+}
+
+static SkPMColor4f get_paint_constant_blended_color(const GrPaint& paint) {
+    SkPMColor4f constantColor;
+    // Patches can overlap, so until a stencil technique is implemented, the provided paints must be
+    // constant blended colors.
+    SkAssertResult(paint.isConstantBlendedColor(&constantColor));
+    return constantColor;
+}
+
+GrTessellateStrokeOp::GrTessellateStrokeOp(const SkMatrix& viewMatrix, const SkPath& path,
+                                           const SkStrokeRec& stroke, GrPaint&& paint,
+                                           GrAAType aaType)
+        : GrDrawOp(ClassID())
+        , fDevPath(transform_path(viewMatrix, path))
+        , fDevStroke(transform_stroke(viewMatrix, stroke))
+        , fAAType(aaType)
+        , fColor(get_paint_constant_blended_color(paint))
+        , fProcessors(std::move(paint)) {
+    SkASSERT(fAAType != GrAAType::kCoverage);  // No mixed samples support yet.
+    SkRect devBounds = fDevPath.getBounds();
+    float inflationRadius = fDevStroke.getInflationRadius();
+    devBounds.outset(inflationRadius, inflationRadius);
+    this->setBounds(devBounds, HasAABloat(GrAAType::kCoverage == fAAType), IsHairline::kNo);
+}
+
+GrProcessorSet::Analysis GrTessellateStrokeOp::finalize(const GrCaps& caps,
+                                                        const GrAppliedClip* clip,
+                                                        bool hasMixedSampledCoverage,
+                                                        GrClampType clampType) {
+    return fProcessors.finalize(fColor, GrProcessorAnalysisCoverage::kNone, clip,
+                                &GrUserStencilSettings::kUnused, hasMixedSampledCoverage, caps,
+                                clampType, &fColor);
+}
+
+GrDrawOp::FixedFunctionFlags GrTessellateStrokeOp::fixedFunctionFlags() const {
+    auto flags = FixedFunctionFlags::kNone;
+    if (GrAAType::kNone != fAAType) {
+        flags |= FixedFunctionFlags::kUsesHWAA;
+    }
+    return flags;
+}
+
+void GrTessellateStrokeOp::onPrePrepare(GrRecordingContext*, const GrSurfaceProxyView* writeView,
+                                        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) {
+    float strokeRadius = fDevStroke.getWidth() * .5f;
+
+    // Rebuild the stroke using GrStrokeGeometry.
+    GrStrokeGeometry strokeGeometry(flushState->caps().shaderCaps()->maxTessellationSegments(),
+                                    fDevPath.countPoints(), fDevPath.countVerbs());
+    GrStrokeGeometry::InstanceTallies tallies = GrStrokeGeometry::InstanceTallies();
+    strokeGeometry.beginPath(fDevStroke, strokeRadius * 2, &tallies);
+    SkPathVerb previousVerb = SkPathVerb::kClose;
+    for (auto [verb, pts, w] : SkPathPriv::Iterate(fDevPath)) {
+        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();
+    }
+
+    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;
+    for (auto verb : strokeGeometry.verbs()) {
+        SkPoint patch[4];
+        float overrideNumSegments = 0;
+        switch (verb) {
+            case Verb::kBeginPath:
+                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, strokeRadius);
+                // 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, strokeRadius);
+                    break;
+                case Verb::kMiterJoin:
+                    vertexData[4].set(2, strokeRadius);
+                    break;
+                case Verb::kRoundJoin:
+                    vertexData[4].set(3, strokeRadius);
+                    break;
+                case Verb::kInternalRoundJoin:
+                case Verb::kInternalBevelJoin:
+                default:
+                    vertexData[4].set(4, strokeRadius);
+                    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, strokeRadius);
+            vertexData += 5;
+            fVertexCount += 5;
+        }
+    }
+
+    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;
+        SkASSERT(flushState->proxy()->numSamples() > 1);  // No mixed samples yet.
+        SkASSERT(fAAType != GrAAType::kCoverage);  // No mixed samples yet.
+    }
+    initArgs.fCaps = &flushState->caps();
+    initArgs.fDstProxyView = flushState->drawOpArgs().dstProxyView();
+    initArgs.fWriteSwizzle = flushState->drawOpArgs().writeSwizzle();
+    GrPipeline pipeline(initArgs, std::move(fProcessors), flushState->detachAppliedClip());
+
+    GrTessellateStrokeShader strokeShader(SkMatrix::I(), fDevStroke.getMiter(), fColor);
+    GrPathShader::ProgramInfo programInfo(flushState->writeView(), &pipeline, &strokeShader);
+
+    flushState->bindPipelineAndScissorClip(programInfo, this->bounds() /*chainBounds??*/);
+    flushState->bindTextures(strokeShader, nullptr, pipeline);
+
+    flushState->bindBuffers(nullptr, nullptr, fVertexBuffer.get());
+    flushState->draw(fVertexCount, fBaseVertex);
+}
diff --git a/src/gpu/tessellate/GrTessellateStrokeOp.h b/src/gpu/tessellate/GrTessellateStrokeOp.h
new file mode 100644
index 0000000..6dadc83
--- /dev/null
+++ b/src/gpu/tessellate/GrTessellateStrokeOp.h
@@ -0,0 +1,50 @@
+/*
+ * Copyright 2020 Google LLC.
+ *
+ * Use of this source code is governed by a BSD-style license that can be
+ * found in the LICENSE file.
+ */
+
+#ifndef GrTessellateStrokeOp_DEFINED
+#define GrTessellateStrokeOp_DEFINED
+
+#include "include/core/SkStrokeRec.h"
+#include "src/gpu/ops/GrDrawOp.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
+// MSAA if antialiasing is desired.
+class GrTessellateStrokeOp : public GrDrawOp {
+    DEFINE_OP_CLASS_ID
+
+    // 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.
+    GrTessellateStrokeOp(const SkMatrix&, const SkPath&, const SkStrokeRec&, GrPaint&&, GrAAType);
+
+    const char* name() const override { return "GrTessellateStrokeOp"; }
+    void visitProxies(const VisitProxyFunc& fn) const override { fProcessors.visitProxies(fn); }
+    GrProcessorSet::Analysis finalize(const GrCaps&, const GrAppliedClip*,
+                                      bool hasMixedSampledCoverage, GrClampType) override;
+    FixedFunctionFlags fixedFunctionFlags() const override;
+    void onPrePrepare(GrRecordingContext*, const GrSurfaceProxyView*, GrAppliedClip*,
+                      const GrXferProcessor::DstProxyView&) override;
+    void onPrepare(GrOpFlushState* state) override;
+    void onExecute(GrOpFlushState*, const SkRect& chainBounds) override;
+
+    const SkPath fDevPath;
+    const SkStrokeRec fDevStroke;
+    const GrAAType fAAType;
+    SkPMColor4f fColor;
+    GrProcessorSet fProcessors;
+
+    sk_sp<const GrBuffer> fVertexBuffer;
+    int fVertexCount = 0;
+    int fBaseVertex;
+
+    friend class GrOpMemoryPool;  // For ctor.
+};
+
+#endif
diff --git a/src/gpu/tessellate/GrTessellateStrokeShader.cpp b/src/gpu/tessellate/GrTessellateStrokeShader.cpp
new file mode 100644
index 0000000..d0f4229
--- /dev/null
+++ b/src/gpu/tessellate/GrTessellateStrokeShader.cpp
@@ -0,0 +1,259 @@
+/*
+ * Copyright 2020 Google LLC.
+ *
+ * Use of this source code is governed by a BSD-style license that can be
+ * found in the LICENSE file.
+ */
+
+#include "src/gpu/tessellate/GrTessellateStrokeShader.h"
+
+#include "src/gpu/glsl/GrGLSLFragmentShaderBuilder.h"
+#include "src/gpu/glsl/GrGLSLGeometryProcessor.h"
+#include "src/gpu/glsl/GrGLSLVarying.h"
+#include "src/gpu/glsl/GrGLSLVertexGeoBuilder.h"
+#include "src/gpu/tessellate/GrWangsFormula.h"
+
+class GrTessellateStrokeShader::Impl : public GrGLSLGeometryProcessor {
+public:
+    const char* getMiterLimitUniformName(const GrGLSLUniformHandler& uniformHandler) const {
+        return uniformHandler.getUniformCStr(fMiterLimitUniform);
+    }
+
+private:
+    void onEmitCode(EmitArgs& args, GrGPArgs* gpArgs) override {
+        const auto& shader = args.fGP.cast<GrTessellateStrokeShader>();
+        args.fVaryingHandler->emitAttributes(shader);
+
+        fMiterLimitUniform = args.fUniformHandler->addUniform(
+                nullptr, kTessControl_GrShaderFlag, kFloat_GrSLType, "miterLimit", nullptr);
+
+        const char* colorUniformName;
+        fColorUniform = args.fUniformHandler->addUniform(
+                nullptr, kFragment_GrShaderFlag, kHalf4_GrSLType, "color", &colorUniformName);
+
+        // The vertex shader is pure pass-through. Stroke widths and normals are defined in local
+        // path space, so we don't apply the view matrix until after tessellation.
+        args.fVertBuilder->declareGlobal(GrShaderVar("P", kFloat2_GrSLType,
+                                                     GrShaderVar::TypeModifier::Out));
+        args.fVertBuilder->codeAppendf("P = inputPoint;");
+
+        // The fragment shader just outputs a uniform color.
+        args.fFragBuilder->codeAppendf("%s = %s;", args.fOutputColor, colorUniformName);
+        args.fFragBuilder->codeAppendf("%s = half4(1);", args.fOutputCoverage);
+    }
+
+    void setData(const GrGLSLProgramDataManager& pdman, const GrPrimitiveProcessor& primProc,
+                 const CoordTransformRange& transformRange) override {
+        const auto& shader = primProc.cast<GrTessellateStrokeShader>();
+
+        if (fCachedMiterLimitValue != shader.fMiterLimit) {
+            pdman.set1f(fMiterLimitUniform, shader.fMiterLimit);
+            fCachedMiterLimitValue = shader.fMiterLimit;
+        }
+
+        if (fCachedColorValue != shader.fColor) {
+            pdman.set4fv(fColorUniform, 1, shader.fColor.vec());
+            fCachedColorValue = shader.fColor;
+        }
+
+        this->setTransformDataHelper(pdman, transformRange);
+    }
+
+    GrGLSLUniformHandler::UniformHandle fMiterLimitUniform;
+    GrGLSLUniformHandler::UniformHandle fColorUniform;
+
+    float fCachedMiterLimitValue = -1;
+    SkMatrix fCachedViewMatrixValue = SkMatrix::I();
+    SkPMColor4f fCachedColorValue = {-1, -1, -1, -1};
+};
+
+SkString GrTessellateStrokeShader::getTessControlShaderGLSL(
+        const GrGLSLPrimitiveProcessor* glslPrimProc, const char* versionAndExtensionDecls,
+        const GrGLSLUniformHandler& uniformHandler, const GrShaderCaps& shaderCaps) const {
+    auto impl = static_cast<const GrTessellateStrokeShader::Impl*>(glslPrimProc);
+
+    SkString code(versionAndExtensionDecls);
+    code.append("layout(vertices = 1) out;\n");
+
+    // TODO: CCPR stroking was written with a linearization tolerance of 1/8 pixel. Readdress this
+    // ASAP to see if we can use GrTessellationPathRenderer::kLinearizationIntolerance (1/4 pixel)
+    // instead.
+    constexpr static float kIntolerance = 8;  // 1/8 pixel.
+    code.appendf("const float kTolerance = %f;\n", 1/kIntolerance);
+    code.appendf("const float kCubicK = %f;\n", GrWangsFormula::cubic_k(kIntolerance));
+
+    const char* miterLimitName = impl->getMiterLimitUniformName(uniformHandler);
+    code.appendf("uniform float %s;\n", miterLimitName);
+    code.appendf("#define uMiterLimit %s\n", miterLimitName);
+
+    code.append(R"(
+            in vec2 P[];
+
+            out vec4 X[];
+            out vec4 Y[];
+            out vec2 fanAngles[];
+            out vec2 strokeRadii[];
+            out vec2 outsetClamp[];
+
+            void main() {
+                // The 5th point contains the patch type and stroke radius.
+                float strokeRadius = P[4].y;
+
+                X[gl_InvocationID /*== 0*/] = vec4(P[0].x, P[1].x, P[2].x, P[3].x);
+                Y[gl_InvocationID /*== 0*/] = vec4(P[0].y, P[1].y, P[2].y, P[3].y);
+                fanAngles[gl_InvocationID /*== 0*/] = vec2(0);
+                strokeRadii[gl_InvocationID /*== 0*/] = vec2(strokeRadius);
+                outsetClamp[gl_InvocationID /*== 0*/] = vec2(-1, 1);
+
+                // Calculate how many linear segments to chop this curve into.
+                // (See GrWangsFormula::cubic().)
+                float numSegments = sqrt(kCubicK * length(max(abs(P[2] - P[1]*2.0 + P[0]),
+                                                              abs(P[3] - P[2]*2.0 + P[1]))));
+
+                // A patch can override the number of segments it gets chopped into by passing a
+                // negative value as P[4].x. (Square caps do this to only draw one segment.)
+                if (P[4].x < 0) {
+                    numSegments = -P[4].x;
+                }
+
+                // A positive value in P[4].x means this patch actually represents a join instead
+                // of a stroked cubic. Joins are implemented as radial fans from the junction point.
+                if (P[4].x > 0) {
+                    // Start by finding the angle between the tangents coming in and out of the
+                    // join.
+                    vec2 c0 = P[1] - P[0];
+                    vec2 c1 = P[3] - P[2];
+                    float theta = atan(determinant(mat2(c0, c1)), dot(c0, c1));
+
+                    // Determine the beginning and end angles of our join.
+                    fanAngles[gl_InvocationID /*== 0*/] = atan(c0.y, c0.x) + vec2(0, theta);
+
+                    float joinType = P[4].x;
+                    if (joinType >= 3) {
+                        // Round join. Decide how many fan segments we need in order to be smooth.
+                        numSegments = abs(theta) / (2 * acos(1 - kTolerance/strokeRadius));
+                    } else if (joinType == 2) {
+                        // Miter join. Draw a fan with 2 segments and lengthen the interior radius
+                        // so it matches the miter point.
+                        // (Or draw a 1-segment fan if we exceed the miter limit.)
+                        float miterRatio = 1.0 / cos(.5 * theta);
+                        strokeRadii[gl_InvocationID /*== 0*/] = strokeRadius * vec2(1, miterRatio);
+                        numSegments = (miterRatio <= uMiterLimit) ? 2.0 : 1.0;
+                    } else {
+                        // Bevel join. Make a fan with only one segment.
+                        numSegments = 1;
+                    }
+
+                    if (strokeRadius * abs(theta) < kTolerance) {
+                        // The join angle is too tight to guarantee there won't be gaps on the
+                        // inside of the junction. Just in case our join was supposed to only go on
+                        // the outside, switch to an internal bevel that ties all 4 incoming
+                        // vertices together. The join angle is so tight that bevels, miters, and
+                        // rounds will all look the same anyway.
+                        numSegments = 1;
+                        // Paranoia. The next shader uses "fanAngles.x != fanAngles.y" as the test
+                        // to decide whether it is emitting a cubic or a fan. But if theta is close
+                        // enough to zero, that might fail. Assign arbitrary, nonequal values. This
+                        // is fine because we will only draw one segment with vertices at T=0 and
+                        // T=1, and the shader won't use fanAngles on the two outer vertices.
+                        fanAngles[gl_InvocationID /*== 0*/] = vec2(1, 0);
+                    } else if (joinType != 4) {
+                        // This is a standard join. Restrict it to the outside of the junction.
+                        outsetClamp[gl_InvocationID /*== 0*/] = mix(
+                                vec2(-1, 1), vec2(0), lessThan(vec2(-theta, theta), vec2(0)));
+                    }
+                }
+
+                // Tessellate a "strip" of numSegments quads.
+                numSegments = max(1, numSegments);
+                gl_TessLevelInner[0] = numSegments;
+                gl_TessLevelInner[1] = 2.0;
+                gl_TessLevelOuter[0] = 2.0;
+                gl_TessLevelOuter[1] = numSegments;
+                gl_TessLevelOuter[2] = 2.0;
+                gl_TessLevelOuter[3] = numSegments;
+            }
+    )");
+
+    return code;
+}
+
+SkString GrTessellateStrokeShader::getTessEvaluationShaderGLSL(
+        const GrGLSLPrimitiveProcessor*, const char* versionAndExtensionDecls,
+        const GrGLSLUniformHandler&, const GrShaderCaps&) const {
+    SkString code(versionAndExtensionDecls);
+    code.append(R"(
+            layout(quads, equal_spacing, ccw) in;
+
+            in vec4 X[];
+            in vec4 Y[];
+            in vec2 fanAngles[];
+            in vec2 strokeRadii[];
+            in vec2 outsetClamp[];
+
+            uniform vec4 sk_RTAdjust;
+
+            void main() {
+                float strokeRadius = strokeRadii[0].x;
+
+                mat4x2 P = transpose(mat2x4(X[0], Y[0]));
+                float T = gl_TessCoord.x;
+
+                // Evaluate the cubic at T. Use De Casteljau's for its accuracy and stability.
+                vec2 ab = mix(P[0], P[1], T);
+                vec2 bc = mix(P[1], P[2], T);
+                vec2 cd = mix(P[2], P[3], T);
+                vec2 abc = mix(ab, bc, T);
+                vec2 bcd = mix(bc, cd, T);
+                vec2 position = mix(abc, bcd, T);
+
+                // Find the normalized tangent vector at T.
+                vec2 tangent = bcd - abc;
+                if (tangent == vec2(0)) {
+                    // We get tangent=0 if (P0 == P1 and T == 0), of if (P2 == P3 and T == 1).
+                    tangent = (T == 0) ? P[2] - P[0] : P[3] - P[1];
+                }
+                tangent = normalize(tangent);
+
+                // If the fanAngles are not equal, it means this patch actually represents a join
+                // instead of a stroked cubic. Joins are implemented as radial fans from the
+                // junction point.
+                //
+                // The caller carefully sets up the control points on junctions so the above math
+                // lines up exactly with the incoming stroke vertices at T=0 and T=1, but for
+                // interior T values we fall back on the fan's arc equation instead.
+                if (fanAngles[0].x != fanAngles[0].y && T != 0 && T != 1) {
+                    position = P[0];
+                    float theta = mix(fanAngles[0].x, fanAngles[0].y, T);
+                    tangent = vec2(cos(theta), sin(theta));
+                    // Miters use a larger radius for the internal vertex.
+                    strokeRadius = strokeRadii[0].y;
+                }
+
+                // Determine how far to outset our vertex orthogonally from the curve.
+                float outset = gl_TessCoord.y * 2 - 1;
+                outset = clamp(outset, outsetClamp[0].x, outsetClamp[0].y);
+                outset *= strokeRadius;
+
+                vec2 vertexpos = position + vec2(-tangent.y, tangent.x) * outset;
+    )");
+
+    // Transform after tessellation. Stroke widths and normals are defined in (pre-transform) local
+    // path space.
+    if (!this->viewMatrix().isIdentity()) {
+        SK_ABORT("Non-identity matrices not supported.");
+        // TODO: implement.
+    }
+
+    code.append(R"(
+                gl_Position = vec4(vertexpos * sk_RTAdjust.xz + sk_RTAdjust.yw, 0.0, 1.0);
+            }
+    )");
+
+    return code;
+}
+
+GrGLSLPrimitiveProcessor* GrTessellateStrokeShader::createGLSLInstance(
+        const GrShaderCaps&) const {
+    return new Impl;
+}
diff --git a/src/gpu/tessellate/GrTessellateStrokeShader.h b/src/gpu/tessellate/GrTessellateStrokeShader.h
new file mode 100644
index 0000000..715e9f2
--- /dev/null
+++ b/src/gpu/tessellate/GrTessellateStrokeShader.h
@@ -0,0 +1,72 @@
+/*
+ * Copyright 2020 Google LLC.
+ *
+ * Use of this source code is governed by a BSD-style license that can be
+ * found in the LICENSE file.
+ */
+
+#ifndef GrTessellateStrokeShader_DEFINED
+#define GrTessellateStrokeShader_DEFINED
+
+#include "src/gpu/tessellate/GrPathShader.h"
+
+#include "src/gpu/tessellate/GrTessellationPathRenderer.h"
+
+class GrGLSLUniformHandler;
+
+// Tessellates a batch of stroke patches directly to the canvas. A patch is either a "cubic"
+// (single stroked bezier curve with butt caps) or a "join". A patch is defined by 5 points as
+// follows:
+//
+//   P0..P3      : Represent the cubic control points.
+//   (P4.x == 0) : The patch is a cubic and the shader decides how many linear segments to produce.
+//   (P4.x < 0)  : The patch is still a cubic, but will be linearized into exactly |P4.x| segments.
+//   (P4.x == 1) : The patch is an outer bevel join.
+//   (P4.x == 2) : The patch is an outer miter join.
+//   (P4.x == 3) : The patch is an outer round join.
+//   (P4.x == 4) : The patch is an inner and outer round join.
+//   P4.y        : Represents the stroke radius.
+//
+// If a patch is a join, P0 must equal P3, P1 must equal the control point coming into the junction,
+// and P2 must equal the control point going out. It's imperative that a junction's control points
+// match the control points of their neighbor cubics exactly, or the rasterization might not be
+// water tight. (Also note that if P1==P0 or P2==P3, the junction needs to be given its neighbor's
+// opposite cubic control point.)
+//
+// To use this shader, construct a GrProgramInfo with a primitiveType of "kPatches" and a
+// tessellationPatchVertexCount of 5.
+class GrTessellateStrokeShader : public GrPathShader {
+public:
+    GrTessellateStrokeShader(const SkMatrix& viewMatrix, float miterLimit, SkPMColor4f color)
+            : GrPathShader(kTessellate_GrTessellateStrokeShader_ClassID, viewMatrix,
+                           GrPrimitiveType::kPatches, 5)
+            , fMiterLimit(miterLimit)
+            , fColor(color) {
+        constexpr static Attribute kInputPointAttrib{"inputPoint", kFloat2_GrVertexAttribType,
+                                                     kFloat2_GrSLType};
+        this->setVertexAttributes(&kInputPointAttrib, 1);
+    }
+
+private:
+    const char* name() const override { return "GrTessellateStrokeShader"; }
+    void getGLSLProcessorKey(const GrShaderCaps&, GrProcessorKeyBuilder* b) const override {
+        b->add32(this->viewMatrix().isIdentity());
+    }
+    GrGLSLPrimitiveProcessor* createGLSLInstance(const GrShaderCaps&) const final;
+
+    SkString getTessControlShaderGLSL(const GrGLSLPrimitiveProcessor*,
+                                      const char* versionAndExtensionDecls,
+                                      const GrGLSLUniformHandler&,
+                                      const GrShaderCaps&) const override;
+    SkString getTessEvaluationShaderGLSL(const GrGLSLPrimitiveProcessor*,
+                                         const char* versionAndExtensionDecls,
+                                         const GrGLSLUniformHandler&,
+                                         const GrShaderCaps&) const override;
+
+    const float fMiterLimit;
+    const SkPMColor4f fColor;
+
+    class Impl;
+};
+
+#endif
diff --git a/src/gpu/tessellate/GrTessellationPathRenderer.cpp b/src/gpu/tessellate/GrTessellationPathRenderer.cpp
index 482a1ef..519a83b 100644
--- a/src/gpu/tessellate/GrTessellationPathRenderer.cpp
+++ b/src/gpu/tessellate/GrTessellationPathRenderer.cpp
@@ -19,6 +19,7 @@
 #include "src/gpu/ops/GrFillRectOp.h"
 #include "src/gpu/tessellate/GrDrawAtlasPathOp.h"
 #include "src/gpu/tessellate/GrTessellatePathOp.h"
+#include "src/gpu/tessellate/GrTessellateStrokeOp.h"
 #include "src/gpu/tessellate/GrWangsFormula.h"
 
 constexpr static SkISize kAtlasInitialSize{512, 512};
@@ -100,21 +101,38 @@
 
 GrPathRenderer::CanDrawPath GrTessellationPathRenderer::onCanDrawPath(
         const CanDrawPathArgs& args) const {
-    if (!args.fShape->style().isSimpleFill() || args.fShape->inverseFilled() ||
+    const GrStyledShape& shape = *args.fShape;
+    if (shape.inverseFilled() || shape.style().hasPathEffect() ||
         args.fViewMatrix->hasPerspective()) {
         return CanDrawPath::kNo;
     }
+
     if (GrAAType::kCoverage == args.fAAType) {
         SkASSERT(1 == args.fProxy->numSamples());
         if (!args.fProxy->canUseMixedSamples(*args.fCaps)) {
             return CanDrawPath::kNo;
         }
     }
+
     SkPath path;
-    args.fShape->asPath(&path);
+    shape.asPath(&path);
     if (SkPathPriv::ConicWeightCnt(path)) {
         return CanDrawPath::kNo;
     }
+
+    if (!shape.style().isSimpleFill()) {
+        SkPMColor4f constantColor;
+        // These are only temporary restrictions while we bootstrap tessellated stroking. Every one
+        // of them will eventually go away.
+        if (shape.style().strokeRec().getStyle() == SkStrokeRec::kStrokeAndFill_Style ||
+            !args.fCaps->shaderCaps()->tessellationSupport() ||
+            GrAAType::kCoverage == args.fAAType || !args.fViewMatrix->isSimilarity() ||
+            !args.fPaint->isConstantBlendedColor(&constantColor) ||
+            args.fPaint->numCoverageFragmentProcessors()) {
+            return CanDrawPath::kNo;
+        }
+    }
+
     return CanDrawPath::kYes;
 }
 
@@ -137,7 +155,8 @@
     SkIRect devIBounds;
     SkIPoint16 locationInAtlas;
     bool transposedInAtlas;
-    if (this->tryAddPathToAtlas(*args.fContext->priv().caps(), *args.fViewMatrix, path, devBounds,
+    if (args.fShape->style().isSimpleFill() &&
+        this->tryAddPathToAtlas(*args.fContext->priv().caps(), *args.fViewMatrix, path, devBounds,
                                 args.fAAType, &devIBounds, &locationInAtlas, &transposedInAtlas)) {
 #ifdef SK_DEBUG
         // If using hardware tessellation in the atlas, make sure the max number of segments is
@@ -158,8 +177,6 @@
         return true;
     }
 
-    auto drawPathFlags = OpFlags::kNone;
-
     // Find the worst-case log2 number of line segments that a curve in this path might need to be
     // divided into.
     int worstCaseResolveLevel = GrWangsFormula::worst_case_cubic_log2(kLinearizationIntolerance,
@@ -167,16 +184,28 @@
                                                                       devBounds.height());
     if (worstCaseResolveLevel > kMaxResolveLevel) {
         // The path is too large for our internal indirect draw shaders. Crop it to the viewport.
-        SkPath viewport;
-        viewport.addRect(SkRect::MakeIWH(renderTargetContext->width(),
-                                         renderTargetContext->height()).makeOutset(1, 1));
+        auto viewport = SkRect::MakeIWH(renderTargetContext->width(),
+                                        renderTargetContext->height());
+        float inflationRadius = 1;
+        const SkStrokeRec& stroke = args.fShape->style().strokeRec();
+        if (stroke.getStyle() == SkStrokeRec::kHairline_Style) {
+            inflationRadius += SkStrokeRec::GetInflationRadius(stroke.getJoin(), stroke.getMiter(),
+                                                               stroke.getCap(), 1);
+        } else if (stroke.getStyle() != SkStrokeRec::kFill_Style) {
+            inflationRadius += stroke.getInflationRadius() * args.fViewMatrix->getMaxScale();
+        }
+        viewport.outset(inflationRadius, inflationRadius);
+
+        SkPath viewportPath;
+        viewportPath.addRect(viewport);
         // Perform the crop in device space so it's a simple rect-path intersection.
         path.transform(*args.fViewMatrix);
-        if (!Op(viewport, path, kIntersect_SkPathOp, &path)) {
+        if (!Op(viewportPath, path, kIntersect_SkPathOp, &path)) {
             // The crop can fail if the PathOps encounter NaN or infinities. Return true
             // because drawing nothing is acceptable behavior for FP overflow.
             return true;
         }
+
         // Transform the path back to its own local space.
         SkMatrix inverse;
         if (!args.fViewMatrix->invert(&inverse)) {
@@ -193,6 +222,16 @@
         SkASSERT(worstCaseResolveLevel <= kMaxResolveLevel);
     }
 
+    if (!args.fShape->style().isSimpleFill()) {
+        const SkStrokeRec& stroke = args.fShape->style().strokeRec();
+        SkASSERT(stroke.getStyle() != SkStrokeRec::kStrokeAndFill_Style);
+        auto op = pool->allocate<GrTessellateStrokeOp>(*args.fViewMatrix, path, stroke,
+                                                       std::move(args.fPaint), args.fAAType);
+        renderTargetContext->addDrawOp(args.fClip, std::move(op));
+        return true;
+    }
+
+    auto drawPathFlags = OpFlags::kNone;
     if ((1 << worstCaseResolveLevel) > shaderCaps.maxTessellationSegments()) {
         // The path is too large for hardware tessellation; a curve in this bounding box could
         // potentially require more segments than are supported by the hardware. Fall back on