[graphite] Device, DrawContext, and DrawList use Shape

Adds a line primitive back to geom::Shape since I just couldn't tolerate
drawPoints mallocing a path for every line. When Shape provides a path
like iterator, it will look equivalent to an SkPath that is a line, so
there won't be any reason to analyze an incoming path for a line, but
if we know we want a line, there's no reason to wrap it in a path.

DrawList and DrawContext now take Shapes for their path rendering funcs,
but are documented to use path rendering. It won't be like GrSDC that
tries to choose the algorithm under the hood from you.

Device has been heavily updated to funnel all the draw calls into a
new drawShape(), so the primitive functions do not malloc skpaths any
longer, even if they still end up using path renderering.

Additionally, instead of making new paints that have updated styles or
removed components, drawShape() takes an SkStrokeRec and ignores what's
on the paint, and has additional flags that tell it to ignore aspects
of the paint. This is used when it recurses, or for cases like drawPaint
where path effects and mask filters should be ignored (and eventually
other things like drawImage will ignore path effects and force a fill
style).

Bug: skia:12466
Change-Id: I4700c895ce3fefe2e437f3b4d329fd381593e037
Reviewed-on: https://skia-review.googlesource.com/c/skia/+/457398
Commit-Queue: Michael Ludwig <michaelludwig@google.com>
Reviewed-by: Robert Phillips <robertphillips@google.com>
diff --git a/experimental/graphite/src/Device.cpp b/experimental/graphite/src/Device.cpp
index 1ab7bf2..5bdb8a7 100644
--- a/experimental/graphite/src/Device.cpp
+++ b/experimental/graphite/src/Device.cpp
@@ -11,9 +11,9 @@
 #include "experimental/graphite/include/SkStuff.h"
 #include "experimental/graphite/src/DrawContext.h"
 #include "experimental/graphite/src/DrawList.h"
+#include "experimental/graphite/src/geom/Shape.h"
 
 #include "include/core/SkPath.h"
-#include "include/core/SkPathBuilder.h"
 #include "include/core/SkPathEffect.h"
 #include "include/core/SkStrokeRec.h"
 
@@ -23,6 +23,12 @@
 
 namespace skgpu {
 
+namespace {
+
+static const SkStrokeRec kFillStyle(SkStrokeRec::kFill_InitStyle);
+
+} // anonymous namespace
+
 sk_sp<Device> Device::Make(sk_sp<Context> context, const SkImageInfo& ii) {
     sk_sp<DrawContext> dc = DrawContext::Make(ii);
     if (!dc) {
@@ -61,6 +67,7 @@
 
 void Device::drawPaint(const SkPaint& paint) {
     SkRect deviceBounds = SkRect::Make(this->devClipBounds());
+    // TODO: Should be able to get the inverse from the matrix cache
     SkM44 devToLocal;
     if (!this->localToDevice44().invert(&devToLocal)) {
         // TBD: This matches legacy behavior for drawPaint() that requires local coords, although
@@ -70,28 +77,28 @@
         return;
     }
     SkRect localCoveringBounds = SkMatrixPriv::MapRect(devToLocal, deviceBounds);
-    this->drawRect(localCoveringBounds, paint);
+    this->drawShape(Shape(localCoveringBounds), paint, kFillStyle,
+                    DrawFlags::kIgnorePathEffect | DrawFlags::kIgnoreMaskFilter);
 }
 
 void Device::drawRect(const SkRect& r, const SkPaint& paint) {
-    // TODO: If the SDC primitive is a rrect (and no simpler), this can be wasted effort since
-    // SkCanvas checks SkRRects for being a rect and reduces it, only for Device to rebuild it
-    // It would be less effort if we can skip the validation of SkRRect ctors here.
-    // TBD: For now rects are paths too, but they may become an SDC primitive
-    this->drawPath(SkPath::Rect(r), paint, /*pathIsMutable=*/true);
+    this->drawShape(Shape(r), paint, SkStrokeRec(paint));
 }
 
 void Device::drawOval(const SkRect& oval, const SkPaint& paint) {
     // TODO: This has wasted effort from the SkCanvas level since it instead converts rrects that
     // happen to be ovals into this, only for us to go right back to rrect.
-
-    // Ovals are always a simplification of round rects
-    this->drawRRect(SkRRect::MakeOval(oval), paint);
+    this->drawShape(Shape(SkRRect::MakeOval(oval)), paint, SkStrokeRec(paint));
 }
 
 void Device::drawRRect(const SkRRect& rr, const SkPaint& paint) {
-    // TBD: If the SDC has a rrect primitive, this won't need to be converted to a path
-    this->drawPath(SkPath::RRect(rr), paint, /*pathIsMutable=*/true);
+    this->drawShape(Shape(rr), paint, SkStrokeRec(paint));
+}
+
+void Device::drawPath(const SkPath& path, const SkPaint& paint, bool pathIsMutable) {
+    // TODO: If we do try to inspect the path, it should happen here and possibly after computing
+    // the path effect. Alternatively, all that should be handled in SkCanvas.
+    this->drawShape(Shape(path), paint, SkStrokeRec(paint));
 }
 
 void Device::drawPoints(SkCanvas::PointMode mode, size_t count,
@@ -99,112 +106,109 @@
     // TODO: I'm [ml] not sure either CPU or GPU backend really has a fast path for this that
     // isn't captured by drawOval and drawLine, so could easily be moved into SkCanvas.
     if (mode == SkCanvas::kPoints_PointMode) {
-        SkPaint filled = paint;
-        filled.setStyle(SkPaint::kFill_Style);
         float radius = 0.5f * paint.getStrokeWidth();
         for (size_t i = 0; i < count; ++i) {
-            SkRect cap = SkRect::MakeLTRB(points[i].fX - radius, points[i].fY - radius,
-                                          points[i].fX + radius, points[i].fY + radius);
+            SkRect pointRect = SkRect::MakeLTRB(points[i].fX - radius, points[i].fY - radius,
+                                                points[i].fX + radius, points[i].fY + radius);
+            // drawOval/drawRect with a forced fill style
             if (paint.getStrokeCap() == SkPaint::kRound_Cap) {
-                this->drawOval(cap, filled);
+                this->drawShape(Shape(SkRRect::MakeOval(pointRect)), paint, kFillStyle);
             } else {
-                this->drawRect(cap, filled);
+                this->drawShape(Shape(pointRect), paint, kFillStyle);
             }
         }
     } else {
+        // Force the style to be a stroke, using the radius and cap from the paint
+        SkStrokeRec stroke(paint, SkPaint::kStroke_Style);
         size_t inc = (mode == SkCanvas::kLines_PointMode) ? 2 : 1;
-        SkPathBuilder builder;
         for (size_t i = 0; i < count; i += inc) {
-            builder.moveTo(points[i]);
-            builder.lineTo(points[i + 1]);
-            this->drawPath(builder.detach(), paint, /*pathIsMutable=*/true);
+            this->drawShape(Shape(points[i], points[i + 1]), paint, stroke);
         }
     }
 }
 
-void Device::drawPath(const SkPath& path, const SkPaint& paint, bool pathIsMutable) {
+void Device::drawShape(const Shape& shape,
+                       const SkPaint& paint,
+                       const SkStrokeRec& style,
+                       Mask<DrawFlags> flags) {
     // Heavy weight paint options like path effects, mask filters, and stroke-and-fill style are
-    // applied on the CPU by generating a new path and recursing on drawPath().
-    if (paint.getPathEffect()) {
+    // applied on the CPU by generating a new shape and recursing on drawShape() with updated flags
+    if (!(flags & DrawFlags::kIgnorePathEffect) && paint.getPathEffect()) {
         // Apply the path effect before anything else
         // TODO: If asADash() returns true and the base path matches the dashing fast path, then
-        // that should be detected now as well.
-        // TODO: This logic is also a candidate for moving to SkCanvas if SkDevice exposes a faster
-        // dash path.
-
-        // Strip off path effect
-        SkPaint noPE = paint;
-        noPE.setPathEffect(nullptr);
-
-        float scaleFactor = SkPaintPriv::ComputeResScaleForStroking(this->localToDevice());
-        SkStrokeRec stroke(paint, scaleFactor);
+        // that should be detected now as well. Maybe add dashPath to Device so canvas can handle it
+        SkStrokeRec newStyle = style;
+        // FIXME: use matrix cache to get res scale for free
+        newStyle.setResScale(SkPaintPriv::ComputeResScaleForStroking(this->localToDevice()));
         SkPath dst;
-        if (paint.getPathEffect()->filterPath(&dst, path, &stroke,
+        if (paint.getPathEffect()->filterPath(&dst, shape.asPath(), &newStyle,
                                               nullptr, this->localToDevice())) {
-            // Adjust paint style to reflect modifications to stroke rec
-            stroke.applyToPaint(&noPE);
-            this->drawPath(dst, noPE, /*pathIsMutable=*/true);
+            // Recurse using the path and new style, while disabling downstream path effect handling
+            this->drawShape(Shape(dst), paint, newStyle, flags | DrawFlags::kIgnorePathEffect);
             return;
         } else {
             // TBD: This warning should go through the general purpose graphite logging system
             SkDebugf("[graphite] WARNING - Path effect failed to apply, drawing original path.\n");
-            this->drawPath(path, noPE, pathIsMutable);
+            this->drawShape(shape, paint, style, flags | DrawFlags::kIgnorePathEffect);
             return;
         }
     }
 
-    // TODO: Handle mask filters, ignored for now but would be applied at this point. My[ml]
-    // thinking is that if there's a mask filter we call a helper function with the path and the
-    // paint, which returns a coverage mask. Then we do a rectangular draw sampling the mask and
-    // handling the rest of the paint's shading. I don't think that's really any different from
-    // the way it is right now. (not 100% sure, but this may also be a reasonable approach for CPU
-    // so could make SkCanvas handle all path effects, image filters, and mask filters and Devices
-    // only need to handle shaders, color filters, and blenders).
-    if (paint.getMaskFilter()) {
+    if (!(flags & DrawFlags::kIgnoreMaskFilter) && paint.getMaskFilter()) {
+        // TODO: Handle mask filters, ignored for the sprint.
+        // TODO: Could this be handled by SkCanvas by drawing a mask, blurring, and then sampling
+        // with a rect draw? What about fast paths for rrect blur masks...
+        this->drawShape(shape, paint, style, flags | DrawFlags::kIgnoreMaskFilter);
         return;
     }
 
-    // Resolve stroke-and-fill -> fill, and hairline -> stroke since the SDC only supports stroke
-    // or fill for path rendering.
-    if (paint.getStyle() == SkPaint::kStrokeAndFill_Style) {
-        // TODO: Could const-cast path when pathIsMutable is true, might not be worth complexity...
-        SkPath simplified;
-        SkPaint styledPaint = paint;
-        if (paint.getFillPath(path, &simplified, nullptr, this->localToDevice())) {
-            styledPaint.setStyle(SkPaint::kFill_Style);
-        } else {
-            styledPaint.setStyle(SkPaint::kStroke_Style);
-            styledPaint.setStrokeWidth(0.f);
-        }
-        this->drawPath(simplified, styledPaint, /*pathIsMutable=*/true);
+    // If we got here, then path effects and mask filters should have been handled and the style
+    // should be fill or stroke/hairline. Stroke-and-fill is not handled by DrawContext, but is
+    // emulated here by drawing twice--one stroke and one fill--using the same depth value.
+    SkASSERT(!SkToBool(paint.getPathEffect()) || (flags & DrawFlags::kIgnorePathEffect));
+    SkASSERT(!SkToBool(paint.getMaskFilter()) || (flags & DrawFlags::kIgnoreMaskFilter));
+
+    // TODO: This will actually be a query to the matrix cache
+    const SkM44& localToDevice = this->localToDevice44();
+    // TODO: Need to track actual z value for painters order in addition to the compressed index,
+    // that might be done here, or as part of the applyClipToDraw() function.
+    auto [colorDepthOrder, scissor] = this->applyClipToDraw(localToDevice, shape, style);
+    if (scissor.isEmpty()) {
+        // Clipped out, so don't record anything
         return;
     }
 
-    // TODO: Implement clipping and z determination
-    SkIRect scissor = this->devClipBounds();
-
     auto blendMode = paint.asBlendMode();
     PaintParams shading{paint.getColor4f(),
                         blendMode.has_value() ? *blendMode : SkBlendMode::kSrcOver,
                         paint.refShader()};
-    if (paint.getStyle() == SkPaint::kStroke_Style) {
-        StrokeParams stroke{paint.getStrokeWidth(), paint.getStrokeMiter(),
-                            paint.getStrokeJoin(), paint.getStrokeCap()};
-        if (paint.getStrokeWidth() <= 0.f) {
-            // Handle hairlines by transforming the control points into device space and drawing
-            // that path with a stroke width of 1 and the identity matrix
-            // FIXME: This doesn't work if the shading requires local coords...
-            SkPath devicePath = path.makeTransform(this->localToDevice());
-            stroke.fWidth = 1.f;
-            fDC->strokePath(SkM44(), devicePath, stroke, scissor, 0, 0, &shading);
-        } else {
-            fDC->strokePath(this->localToDevice44(), path, stroke, scissor, 0, 0, &shading);
-        }
-    } else if (path.isConvex()) {
-        fDC->fillConvexPath(this->localToDevice44(), path, scissor, 0, 0, &shading);
-    } else {
-        fDC->stencilAndFillPath(this->localToDevice44(), path, scissor, 0, 0, 0, &shading);
+
+    SkStrokeRec::Style styleType = style.getStyle();
+    if (styleType == SkStrokeRec::kStroke_Style ||
+        styleType == SkStrokeRec::kHairline_Style ||
+        styleType == SkStrokeRec::kStrokeAndFill_Style) {
+        // TODO: If DC supports stroked primitives, Device could choose one of those based on shape
+        StrokeParams stroke(style.getWidth(), style.getMiter(), style.getJoin(), style.getCap());
+        fDC->strokePath(localToDevice, shape, stroke, scissor, colorDepthOrder, 0, &shading);
     }
+    if (styleType == SkStrokeRec::kFill_Style ||
+        styleType == SkStrokeRec::kStrokeAndFill_Style) {
+        // TODO: If DC supports filled primitives, Device could choose one of those based on shape
+        if (shape.convex()) {
+            fDC->fillConvexPath(localToDevice, shape, scissor, colorDepthOrder, 0, &shading);
+        } else {
+            // FIXME must determine stencil order
+            fDC->stencilAndFillPath(localToDevice, shape, scissor, colorDepthOrder, 0, 0, &shading);
+        }
+    }
+}
+
+std::pair<CompressedPaintersOrder, SkIRect>
+Device::applyClipToDraw(const SkM44& localToDevice,
+                        const Shape& shape,
+                        const SkStrokeRec& style) {
+    // TODO: actually implement this
+    return {0, this->devClipBounds()};
 }
 
 sk_sp<SkSpecialImage> Device::makeSpecial(const SkBitmap&) {
diff --git a/experimental/graphite/src/Device.h b/experimental/graphite/src/Device.h
index 4e70135..baae84c 100644
--- a/experimental/graphite/src/Device.h
+++ b/experimental/graphite/src/Device.h
@@ -8,12 +8,20 @@
 #ifndef skgpu_Device_DEFINED
 #define skgpu_Device_DEFINED
 
+#include "experimental/graphite/include/GraphiteTypes.h"
+
 #include "src/core/SkDevice.h"
 
+class SkStrokeRec;
+
 namespace skgpu {
 
 class Context;
 class DrawContext;
+class Shape;
+
+struct PaintParams;
+struct StrokeParams;
 
 class Device final : public SkBaseDevice  {
 public:
@@ -101,12 +109,45 @@
     sk_sp<SkSpecialImage> snapSpecial(const SkIRect& subset, bool forceCopy = false) override;
 
 private:
+    // DrawFlags alters the effects used by drawShape.
+    enum class DrawFlags : unsigned {
+        kNone             = 0b00,
+
+        // Any SkMaskFilter on the SkPaint passed into drawShape() is ignored.
+        // - drawPaint, drawVertices, drawAtlas
+        // - drawShape after it's applied the mask filter.
+        kIgnoreMaskFilter = 0b01,
+
+        // Any SkPathEffect on the SkPaint passed into drawShape() is ignored.
+        // - drawPaint, drawImageLattice, drawImageRect, drawEdgeAAImageSet, drawVertices, drawAtlas
+        // - drawShape after it's applied the path effect.
+        kIgnorePathEffect = 0b10,
+    };
+    SKGPU_DECL_MASK_OPS_FRIENDS(DrawFlags);
+
     Device(sk_sp<Context>, sk_sp<DrawContext>);
 
+    // Handles applying path effects, mask filters, stroke-and-fill styles, and hairlines.
+    // Ignores geometric style on the paint in favor of explicitly provided SkStrokeRec and flags.
+    void drawShape(const Shape&,
+                   const SkPaint&,
+                   const SkStrokeRec&,
+                   Mask<DrawFlags> = DrawFlags::kNone);
+
+    // Determines most optimal painters order for a draw of the given shape and style.
+    //
+    // This also records the draw's bounds to any clip elements that affect it so that they are
+    // recorded when popped off the stack. Returns the scissor and minimum compressed painter's
+    // order for the draw to be rendered/clipped correctly.
+    std::pair<CompressedPaintersOrder, SkIRect>
+    applyClipToDraw(const SkM44&, const Shape&, const SkStrokeRec&);
+
     sk_sp<Context> fContext;
     sk_sp<DrawContext> fDC;
 };
 
+SKGPU_MAKE_MASK_OPS(Device::DrawFlags)
+
 } // namespace skgpu
 
 #endif // skgpu_Device_DEFINED
diff --git a/experimental/graphite/src/DrawContext.cpp b/experimental/graphite/src/DrawContext.cpp
index 4622b60..cd13717 100644
--- a/experimental/graphite/src/DrawContext.cpp
+++ b/experimental/graphite/src/DrawContext.cpp
@@ -11,6 +11,7 @@
 #include "experimental/graphite/src/DrawPass.h"
 #include "experimental/graphite/src/RenderPassTask.h"
 #include "experimental/graphite/src/geom/BoundsManager.h"
+#include "experimental/graphite/src/geom/Shape.h"
 
 namespace skgpu {
 
@@ -34,33 +35,33 @@
 }
 
 void DrawContext::stencilAndFillPath(const SkM44& localToDevice,
-                                     const SkPath& path,
+                                     const Shape& shape,
                                      const SkIRect& scissor,
                                      CompressedPaintersOrder colorDepthOrder,
                                      CompressedPaintersOrder stencilOrder,
                                      uint16_t depth,
                                      const PaintParams* paint)  {
-    fPendingDraws->stencilAndFillPath(localToDevice, path, scissor, colorDepthOrder, stencilOrder,
+    fPendingDraws->stencilAndFillPath(localToDevice, shape, scissor, colorDepthOrder, stencilOrder,
                                       depth, paint);
 }
 
 void DrawContext::fillConvexPath(const SkM44& localToDevice,
-                                 const SkPath& path,
+                                 const Shape& shape,
                                  const SkIRect& scissor,
                                  CompressedPaintersOrder colorDepthOrder,
                                  uint16_t depth,
                                  const PaintParams* paint) {
-    fPendingDraws->fillConvexPath(localToDevice, path, scissor, colorDepthOrder, depth, paint);
+    fPendingDraws->fillConvexPath(localToDevice, shape, scissor, colorDepthOrder, depth, paint);
 }
 
 void DrawContext::strokePath(const SkM44& localToDevice,
-                             const SkPath& path,
+                             const Shape& shape,
                              const StrokeParams& stroke,
                              const SkIRect& scissor,
                              CompressedPaintersOrder colorDepthOrder,
                              uint16_t depth,
                              const PaintParams* paint) {
-    fPendingDraws->strokePath(localToDevice, path, stroke, scissor, colorDepthOrder, depth, paint);
+    fPendingDraws->strokePath(localToDevice, shape, stroke, scissor, colorDepthOrder, depth, paint);
 }
 
 void DrawContext::snapDrawPass(const BoundsManager* occlusionCuller) {
diff --git a/experimental/graphite/src/DrawContext.h b/experimental/graphite/src/DrawContext.h
index 0258b11..18e73af 100644
--- a/experimental/graphite/src/DrawContext.h
+++ b/experimental/graphite/src/DrawContext.h
@@ -15,12 +15,13 @@
 
 #include <vector>
 
-class SkPath;
 class SkM44;
 
 namespace skgpu {
 
 class BoundsManager;
+class Shape;
+
 class DrawList;
 class DrawPass;
 class Task;
@@ -43,7 +44,7 @@
     // TODO: need color/depth clearing functions (so DCL will probably need those too)
 
     void stencilAndFillPath(const SkM44& localToDevice,
-                            const SkPath& path,
+                            const Shape& shape,
                             const SkIRect& scissor,
                             CompressedPaintersOrder colorDepthOrder,
                             CompressedPaintersOrder stencilOrder,
@@ -51,14 +52,14 @@
                             const PaintParams* paint);
 
     void fillConvexPath(const SkM44& localToDevice,
-                        const SkPath& path,
+                        const Shape& shape,
                         const SkIRect& scissor,
                         CompressedPaintersOrder colorDepthOrder,
                         uint16_t depth,
                         const PaintParams* paint);
 
     void strokePath(const SkM44& localToDevice,
-                    const SkPath& path,
+                    const Shape& shape,
                     const StrokeParams& stroke,
                     const SkIRect& scissor,
                     CompressedPaintersOrder colorDepthOrder,
diff --git a/experimental/graphite/src/DrawList.h b/experimental/graphite/src/DrawList.h
index 0510b7a..6faea31 100644
--- a/experimental/graphite/src/DrawList.h
+++ b/experimental/graphite/src/DrawList.h
@@ -22,6 +22,8 @@
 
 namespace skgpu {
 
+class Shape;
+
 // Forward declarations that capture the intermediate state lying between public Skia types and
 // the direct GPU representation.
 struct PaintParams;
@@ -65,8 +67,13 @@
     // sense for it to compute these dependent values and provide them here. Storing the scale
     // factor per draw command is low overhead, but unsure about storing 2 matrices per command.
 
+    // NOTE: All path rendering functions, e.g. [fill|stroke|...]Path() that take a geom::Shape
+    // draw using the same underlying techniques regardless of the shape's type. If a Shape has
+    // a type matching a simpler primitive technique or coverage AA, the caller must explicitly
+    // invoke it to use that rendering algorithms.
+
     void stencilAndFillPath(const SkM44& localToDevice,
-                            const SkPath& path,
+                            const Shape& shape,
                             const SkIRect& scissor, // TBD: expand this to one xformed rrect clip?
                             CompressedPaintersOrder colorDepthOrder,
                             CompressedPaintersOrder stencilOrder,
@@ -74,14 +81,14 @@
                             const PaintParams* paint) {}
 
     void fillConvexPath(const SkM44& localToDevice,
-                        const SkPath& path,
+                        const Shape& shape,
                         const SkIRect& scissor,
                         CompressedPaintersOrder colorDepthOrder,
                         uint16_t depth,
                         const PaintParams* paint) {}
 
     void strokePath(const SkM44& localToDevice,
-                    const SkPath& path,
+                    const Shape& shape,
                     const StrokeParams& stroke,
                     const SkIRect& scissor,
                     CompressedPaintersOrder colorDepthOrder,
@@ -127,12 +134,30 @@
     // active clipShader().
 };
 
-// NOTE: Only represents the stroke style; stroke-and-fill and hairline must be handled higher up.
+// NOTE: Only represents the stroke or hairline styles; stroke-and-fill must be handled higher up.
 struct StrokeParams {
-    float         fWidth; // > 0 and relative to shape's transform
-    float         fMiterLimit;
-    SkPaint::Join fJoin;
-    SkPaint::Cap  fCap;
+    float        fHalfWidth;  // >0: relative to transform; ==0: hairline, 1px in device space
+    float        fJoinLimit; // >0: miter join; ==0: bevel join; <0: round join
+    SkPaint::Cap fCap;
+
+    StrokeParams() : fHalfWidth(0.f), fJoinLimit(0.f), fCap(SkPaint::kButt_Cap) {}
+    StrokeParams(float width, float miterLimit, SkPaint::Join join, SkPaint::Cap cap)
+            : fHalfWidth(std::max(0.f, 0.5f * width))
+            , fJoinLimit(join == SkPaint::kMiter_Join ? std::max(0.f, miterLimit) :
+                         (join == SkPaint::kBevel_Join ? 0.f : -1.f))
+            , fCap(cap) {}
+    StrokeParams(const StrokeParams&) = default;
+
+    bool isMiterJoin() const { return fJoinLimit > 0.f; }
+    bool isBevelJoin() const { return fJoinLimit == 0.f; }
+    bool isRoundJoin() const { return fJoinLimit < 0.f; }
+
+    float width() const { return 2.f * fHalfWidth; }
+    float miterLimit() const { return std::max(0.f, fJoinLimit); }
+    SkPaint::Join join() const {
+        return fJoinLimit > 0.f ? SkPaint::kMiter_Join :
+               (fJoinLimit == 0.f ? SkPaint::kBevel_Join : SkPaint::kRound_Join);
+    }
 };
 
 // TBD: Separate DashParams extracted from an SkDashPathEffect? Or folded into StrokeParams?
diff --git a/experimental/graphite/src/geom/Shape.cpp b/experimental/graphite/src/geom/Shape.cpp
index ed8d445..6a31b4c 100644
--- a/experimental/graphite/src/geom/Shape.cpp
+++ b/experimental/graphite/src/geom/Shape.cpp
@@ -15,18 +15,11 @@
 
 Shape& Shape::operator=(const Shape& shape) {
     switch (shape.type()) {
-        case Type::kEmpty:
-            this->reset();
-            break;
-        case Type::kRect:
-            this->setRect(shape.rect());
-            break;
-        case Type::kRRect:
-            this->setRRect(shape.rrect());
-            break;
-        case Type::kPath:
-            this->setPath(shape.path());
-            break;
+        case Type::kEmpty: this->reset();                         break;
+        case Type::kLine:  this->setLine(shape.p0(), shape.p1()); break;
+        case Type::kRect:  this->setRect(shape.rect());           break;
+        case Type::kRRect: this->setRRect(shape.rrect());         break;
+        case Type::kPath:  this->setPath(shape.path());           break;
     }
 
     fInverted = shape.fInverted;
@@ -35,30 +28,33 @@
 
 bool Shape::conservativeContains(const SkRect& rect) const {
     switch (fType) {
-        case Type::kEmpty:   return false;
-        case Type::kRect:    return fRect.contains(rect);
-        case Type::kRRect:   return fRRect.contains(rect);
-        case Type::kPath:    return fPath.conservativelyContainsRect(rect);
+        case Type::kEmpty: return false;
+        case Type::kLine:  return false;
+        case Type::kRect:  return fRect.contains(rect);
+        case Type::kRRect: return fRRect.contains(rect);
+        case Type::kPath:  return fPath.conservativelyContainsRect(rect);
     }
     SkUNREACHABLE;
 }
 
 bool Shape::conservativeContains(const SkV2& point) const {
     switch (fType) {
-        case Type::kEmpty:   return false;
-        case Type::kRect:    return fRect.contains(point.x, point.y);
-        case Type::kRRect:   return SkRRectPriv::ContainsPoint(fRRect, {point.x, point.y});
-        case Type::kPath:    return fPath.contains(point.x, point.y);
+        case Type::kEmpty: return false;
+        case Type::kLine:  return false;
+        case Type::kRect:  return fRect.contains(point.x, point.y);
+        case Type::kRRect: return SkRRectPriv::ContainsPoint(fRRect, {point.x, point.y});
+        case Type::kPath:  return fPath.contains(point.x, point.y);
     }
     SkUNREACHABLE;
 }
 
 bool Shape::closed() const {
     switch (fType) {
-        case Type::kEmpty:   return true;
-        case Type::kRect:    return true;
-        case Type::kRRect:   return true;
-        case Type::kPath:    return SkPathPriv::IsClosedSingleContour(fPath);
+        case Type::kEmpty: return true;
+        case Type::kLine:  return false;
+        case Type::kRect:  return true;
+        case Type::kRRect: return true;
+        case Type::kPath:  return SkPathPriv::IsClosedSingleContour(fPath);
     }
     SkUNREACHABLE;
 }
@@ -78,10 +74,12 @@
     // inverted bounds for a truly empty shape.
     static constexpr SkRect kInverted = SkRect::MakeLTRB(1, 1, -1, -1);
     switch (fType) {
-        case Type::kEmpty:   return kInverted;
-        case Type::kRect:    return fRect.makeSorted();
-        case Type::kRRect:   return fRRect.getBounds();
-        case Type::kPath:    return fPath.getBounds();
+        case Type::kEmpty: return kInverted;
+        case Type::kLine:  return SkRect::MakeLTRB(fLine[0].x, fLine[0].y, fLine[1].x, fLine[1].y)
+                                         .makeSorted();
+        case Type::kRect:  return fRect.makeSorted();
+        case Type::kRRect: return fRRect.getBounds();
+        case Type::kPath:  return fPath.getBounds();
     }
     SkUNREACHABLE;
 }
@@ -93,10 +91,12 @@
 
     SkPathBuilder builder(this->fillType());
     switch (fType) {
-        case Type::kEmpty:   /* do nothing */                             break;
-        case Type::kRect:    builder.addRect(fRect);                      break;
-        case Type::kRRect:   builder.addRRect(fRRect);                    break;
-        case Type::kPath:    SkUNREACHABLE;
+        case Type::kEmpty: /* do nothing */                        break;
+        case Type::kLine:  builder.moveTo(fLine[0].x, fLine[0].y)
+                                  .lineTo(fLine[1].x, fLine[1].y); break;
+        case Type::kRect:  builder.addRect(fRect);                 break;
+        case Type::kRRect: builder.addRRect(fRRect);               break;
+        case Type::kPath:  SkUNREACHABLE;
     }
     return builder.detach();
 }
diff --git a/experimental/graphite/src/geom/Shape.h b/experimental/graphite/src/geom/Shape.h
index abedd9e..a520d2a 100644
--- a/experimental/graphite/src/geom/Shape.h
+++ b/experimental/graphite/src/geom/Shape.h
@@ -12,7 +12,6 @@
 #include "include/core/SkPath.h"
 #include "include/core/SkRRect.h"
 #include "include/core/SkRect.h"
-#include "include/core/SkSpan.h"
 
 #include <array>
 
@@ -26,7 +25,7 @@
 class Shape {
 public:
     enum class Type : uint8_t {
-        kEmpty, kRect, kRRect, kPath
+        kEmpty, kLine, kRect, kRRect, kPath
     };
     inline static constexpr int kTypeCount = static_cast<int>(Type::kPath) + 1;
 
@@ -34,9 +33,11 @@
     Shape(const Shape& shape)            { *this = shape; }
     Shape(Shape&&) = delete;
 
-    explicit Shape(const SkRect& rect)   { this->setRect(rect); }
-    explicit Shape(const SkRRect& rrect) { this->setRRect(rrect); }
-    explicit Shape(const SkPath& path)   { this->setPath(path); }
+    Shape(const SkPoint& p0, const SkPoint& p1) { this->setLine(p0, p1); }
+    Shape(const SkV2& p0, const SkV2& p1)       { this->setLine(p0, p1); }
+    explicit Shape(const SkRect& rect)          { this->setRect(rect);   }
+    explicit Shape(const SkRRect& rrect)        { this->setRRect(rrect); }
+    explicit Shape(const SkPath& path)          { this->setPath(path);   }
 
     ~Shape() { this->reset(); }
 
@@ -50,10 +51,11 @@
     // corners is kRRect and not kRect).
     Type type() const { return fType; }
 
-    bool isEmpty()    const { return fType == Type::kEmpty; }
-    bool isRect()     const { return fType == Type::kRect; }
-    bool isRRect()    const { return fType == Type::kRRect; }
-    bool isPath()     const { return fType == Type::kPath; }
+    bool isEmpty() const { return fType == Type::kEmpty; }
+    bool isLine()  const { return fType == Type::kLine;  }
+    bool isRect()  const { return fType == Type::kRect;  }
+    bool isRRect() const { return fType == Type::kRRect; }
+    bool isPath()  const { return fType == Type::kPath;  }
 
     bool inverted() const {
         SkASSERT(fType != Type::kPath || fInverted == fPath.isInverseFillType());
@@ -96,15 +98,26 @@
 
     // Access the actual geometric description of the shape. May only access the appropriate type
     // based on what was last set.
-    const SkRect&      rect()        const { SkASSERT(this->isRect()); return fRect; }
-    const SkRRect&     rrect()       const { SkASSERT(this->isRRect()); return fRRect; }
-    const SkPath&      path()        const { SkASSERT(this->isPath()); return fPath; }
+    const SkV2&    p0()    const { SkASSERT(this->isLine());  return fLine[0]; }
+    const SkV2&    p1()    const { SkASSERT(this->isLine());  return fLine[1]; }
+    const SkRect&  rect()  const { SkASSERT(this->isRect());  return fRect;    }
+    const SkRRect& rrect() const { SkASSERT(this->isRRect()); return fRRect;   }
+    const SkPath&  path()  const { SkASSERT(this->isPath());  return fPath;    }
 
     // Update the geometry stored in the Shape and update its associated type to match. This
     // performs no simplification, so calling setRRect() with a round rect that has isRect() return
     // true will still be considered an rrect by Shape.
     //
     // These reset inversion to the default for the geometric type.
+    void setLine(const SkPoint& p0, const SkPoint& p1) {
+        this->setLine(SkV2{p0.fX, p0.fY}, SkV2{p1.fX, p1.fY});
+    }
+    void setLine(const SkV2& p0, const SkV2& p1) {
+        this->setType(Type::kLine);
+        fLine[0] = p0;
+        fLine[1] = p1;
+        fInverted = false;
+    }
     void setRect(const SkRect& rect) {
         this->setType(Type::kRect);
         fRect = rect;
@@ -141,13 +154,14 @@
     }
 
     union {
+        SkV2    fLine[2];
         SkRect  fRect;
         SkRRect fRRect;
         SkPath  fPath;
     };
 
-    Type    fType        = Type::kEmpty;
-    bool    fInverted    = false;
+    Type    fType     = Type::kEmpty;
+    bool    fInverted = false;
 };
 
 } // namespace skgpu