In GrShape detect that stroked axis-aligned lines are rrects.
authorbsalomon <bsalomon@google.com>
Mon, 18 Jul 2016 14:31:13 +0000 (07:31 -0700)
committerCommit bot <commit-bot@chromium.org>
Mon, 18 Jul 2016 14:31:13 +0000 (07:31 -0700)
GOLD_TRYBOT_URL= https://gold.skia.org/search?issue=2151313002

Review-Url: https://codereview.chromium.org/2151313002

include/core/SkRect.h
src/gpu/GrShape.cpp
src/gpu/GrStyle.cpp
src/gpu/GrStyle.h
tests/DFPathRendererTest.cpp
tests/GrShapeTest.cpp

index 39cbb33..f25cac6 100644 (file)
@@ -418,10 +418,9 @@ struct SK_API SkRect {
         return r;
     }
 
-    static SkRect SK_WARN_UNUSED_RESULT MakeLTRB(SkScalar l, SkScalar t, SkScalar r, SkScalar b) {
-        SkRect rect;
-        rect.set(l, t, r, b);
-        return rect;
+    static constexpr SkRect SK_WARN_UNUSED_RESULT MakeLTRB(SkScalar l, SkScalar t, SkScalar r,
+                                                           SkScalar b) {
+        return SkRect {l, t, r, b};
     }
 
     static SkRect SK_WARN_UNUSED_RESULT MakeXYWH(SkScalar x, SkScalar y, SkScalar w, SkScalar h) {
index 5ffd32d..698ff48 100644 (file)
@@ -30,10 +30,12 @@ GrShape& GrShape::operator=(const GrShape& that) {
 }
 
 SkRect GrShape::bounds() const {
-    static constexpr SkRect kEmpty = SkRect::MakeEmpty();
+    // Bounds where left == bottom or top == right can indicate a line or point shape. We return
+    // inverted bounds for a truly empty shape.
+    static constexpr SkRect kInverted = SkRect::MakeLTRB(1, 1, -1, -1);
     switch (fType) {
         case Type::kEmpty:
-            return kEmpty;
+            return kInverted;
         case Type::kLine: {
             SkRect bounds;
             if (fLineData.fPts[0].fX < fLineData.fPts[1].fX) {
@@ -58,7 +60,7 @@ SkRect GrShape::bounds() const {
             return this->path().getBounds();
     }
     SkFAIL("Unknown shape type");
-    return kEmpty;
+    return kInverted;
 }
 
 SkRect GrShape::styledBounds() const {
@@ -158,6 +160,9 @@ void GrShape::setInheritedKey(const GrShape &parent, GrStyle::Apply apply, SkSca
         if (parent.knownToBeClosed()) {
             styleKeyFlags |= GrStyle::kClosed_KeyFlag;
         }
+        if (parent.asLine(nullptr, nullptr)) {
+            styleKeyFlags |= GrStyle::kNoJoins_KeyFlag;
+        }
         int styleCnt = GrStyle::KeySize(parent.fStyle, apply, styleKeyFlags);
         if (styleCnt < 0) {
             // The style doesn't allow a key, set the path gen ID to 0 so that we fail when
@@ -232,17 +237,9 @@ GrShape::GrShape(const GrShape& parent, GrStyle::Apply apply, SkScalar scale) {
         SkStrokeRec strokeRec = parent.fStyle.strokeRec();
         if (!parent.fStyle.applyPathEffectToPath(&this->path(), &strokeRec, *srcForPathEffect,
                                                  scale)) {
-            // If the path effect fails then we continue as though there was no path effect.
-            // If the original was a rrect that we couldn't canonicalize because of the path
-            // effect, then do so now.
-            if (parent.fType == Type::kRRect && (parent.fRRectData.fDir != kDefaultRRectDir ||
-                                                 parent.fRRectData.fStart != kDefaultRRectStart)) {
-                SkASSERT(srcForPathEffect == tmpPath.get());
-                tmpPath.get()->reset();
-                tmpPath.get()->addRRect(parent.fRRectData.fRRect, kDefaultRRectDir,
-                                        kDefaultRRectDir);
-            }
-            this->path() = *srcForPathEffect;
+            tmpParent.init(*srcForPathEffect, GrStyle(strokeRec, nullptr));
+            *this = tmpParent.get()->applyStyle(apply, scale);
+            return;
         }
         // A path effect has access to change the res scale but we aren't expecting it to and it
         // would mess up our key computation.
@@ -262,8 +259,16 @@ GrShape::GrShape(const GrShape& parent, GrStyle::Apply apply, SkScalar scale) {
             }
             tmpParent.get()->asPath(tmpPath.get());
             SkStrokeRec::InitStyle fillOrHairline;
-            SkAssertResult(tmpParent.get()->style().applyToPath(&this->path(), &fillOrHairline,
-                                                                *tmpPath.get(), scale));
+            // The parent shape may have simplified away the strokeRec, check for that here.
+            if (tmpParent.get()->style().applies()) {
+                SkAssertResult(tmpParent.get()->style().applyToPath(&this->path(), &fillOrHairline,
+                                                                    *tmpPath.get(), scale));
+            } else if (tmpParent.get()->style().isSimpleFill()) {
+                fillOrHairline = SkStrokeRec::kFill_InitStyle;
+            } else {
+                SkASSERT(tmpParent.get()->style().isSimpleHairline());
+                fillOrHairline = SkStrokeRec::kHairline_InitStyle;
+            }
             fStyle.resetToInitStyle(fillOrHairline);
             parentForKey = tmpParent.get();
         } else {
@@ -401,19 +406,76 @@ void GrShape::attemptToSimplifyRRect() {
 }
 
 void GrShape::attemptToSimplifyLine() {
+    SkASSERT(Type::kLine == fType);
+    SkASSERT(!fInheritedKey.count());
+    if (fStyle.isDashed()) {
+        // Dashing ignores inverseness.
+        fLineData.fInverted = false;
+        return;
+    } else if (fStyle.hasPathEffect()) {
+        return;
+    }
+    if (fStyle.strokeRec().getStyle() == SkStrokeRec::kStrokeAndFill_Style) {
+        // Make stroke + fill be stroke since the fill is empty.
+        SkStrokeRec rec = fStyle.strokeRec();
+        rec.setStrokeStyle(fStyle.strokeRec().getWidth(), false);
+        fStyle = GrStyle(rec, nullptr);
+    }
     if (fStyle.isSimpleFill() && !fLineData.fInverted) {
         this->changeType(Type::kEmpty);
-    } else {
-        // Only path effects could care about the order of the points. Otherwise canonicalize
-        // the point order
-        if (!fStyle.hasPathEffect()) {
-            SkPoint* pts = fLineData.fPts;
-            if (pts[1].fY < pts[0].fY || (pts[1].fY == pts[0].fY && pts[1].fX < pts[0].fX)) {
-                SkTSwap(pts[0], pts[1]);
+        return;
+    }
+    SkPoint* pts = fLineData.fPts;
+    if (fStyle.strokeRec().getStyle() == SkStrokeRec::kStroke_Style) {
+        // If it is horizontal or vertical we will turn it into a filled rrect.
+        SkRect rect;
+        rect.fLeft = SkTMin(pts[0].fX, pts[1].fX);
+        rect.fRight = SkTMax(pts[0].fX, pts[1].fX);
+        rect.fTop = SkTMin(pts[0].fY, pts[1].fY);
+        rect.fBottom = SkTMax(pts[0].fY, pts[1].fY);
+        bool eqX = rect.fLeft == rect.fRight;
+        bool eqY = rect.fTop == rect.fBottom;
+        if (eqX || eqY) {
+            SkScalar r = fStyle.strokeRec().getWidth() / 2;
+            bool inverted = fLineData.fInverted;
+            this->changeType(Type::kRRect);
+            switch (fStyle.strokeRec().getCap()) {
+                case SkPaint::kButt_Cap:
+                    if (eqX && eqY) {
+                        this->changeType(Type::kEmpty);
+                        return;
+                    }
+                    if (eqX) {
+                        rect.outset(r, 0);
+                    } else {
+                        rect.outset(0, r);
+                    }
+                    fRRectData.fRRect = SkRRect::MakeRect(rect);
+                    break;
+                case SkPaint::kSquare_Cap:
+                    rect.outset(r, r);
+                    fRRectData.fRRect = SkRRect::MakeRect(rect);
+                    break;
+                case SkPaint::kRound_Cap:
+                    rect.outset(r, r);
+                    fRRectData.fRRect = SkRRect::MakeRectXY(rect, r, r);
+                    break;
             }
-        } else if (fStyle.isDashed()) {
-            // Dashing ignores inverseness.
-            fLineData.fInverted = false;
+            fRRectData.fInverted = inverted;
+            fRRectData.fDir = kDefaultRRectDir;
+            fRRectData.fStart = kDefaultRRectStart;
+            if (fRRectData.fRRect.isEmpty()) {
+                // This can happen when r is very small relative to the rect edges.
+                this->changeType(Type::kEmpty);
+                return;
+            }
+            fStyle = GrStyle::SimpleFill();
+            return;
         }
     }
+    // Only path effects could care about the order of the points. Otherwise canonicalize
+    // the point order.
+    if (pts[1].fY < pts[0].fY || (pts[1].fY == pts[0].fY && pts[1].fX < pts[0].fX)) {
+        SkTSwap(pts[0], pts[1]);
+    }
 }
index 43718db..7ea5193 100644 (file)
@@ -75,25 +75,30 @@ void GrStyle::WriteKey(uint32_t *key, const GrStyle &style, Apply apply, SkScala
         GR_STATIC_ASSERT(SkPaint::kCapCount <= (1 << kCapBits));
         // The cap type only matters for unclosed shapes. However, a path effect could unclose
         // the shape before it is stroked.
-        SkPaint::Cap cap;
-        if ((flags & kClosed_KeyFlag) && !style.pathEffect()) {
-            cap = SkPaint::kButt_Cap;
-        } else {
+        SkPaint::Cap cap = SkPaint::kDefault_Cap;
+        if (!(flags & kClosed_KeyFlag) || style.pathEffect()) {
             cap = style.strokeRec().getCap();
         }
+        SkScalar miter = -1.f;
+        SkPaint::Join join = SkPaint::kDefault_Join;
+
+        // Dashing will not insert joins but other path effects may.
+        if (!(flags & kNoJoins_KeyFlag) || style.hasNonDashPathEffect()) {
+            join = style.strokeRec().getJoin();
+            // Miter limit only affects miter joins
+            if (SkPaint::kMiter_Join == join) {
+                miter = style.strokeRec().getMiter();
+            }
+        }
+
         key[i++] = style.strokeRec().getStyle() |
-                   style.strokeRec().getJoin() << kJoinShift |
+                   join << kJoinShift |
                    cap << kCapShift;
 
-        SkScalar scalar;
-        // Miter limit only affects miter joins
-        scalar = SkPaint::kMiter_Join == style.strokeRec().getJoin()
-                 ? style.strokeRec().getMiter()
-                 : -1.f;
-        memcpy(&key[i++], &scalar, sizeof(scalar));
+        memcpy(&key[i++], &miter, sizeof(miter));
 
-        scalar = style.strokeRec().getWidth();
-        memcpy(&key[i++], &scalar, sizeof(scalar));
+        SkScalar width = style.strokeRec().getWidth();
+        memcpy(&key[i++], &width, sizeof(width));
     }
     SkASSERT(KeySize(style, apply) == i);
 }
index 326f800..9091166 100644 (file)
@@ -51,7 +51,9 @@ public:
      */
     enum KeyFlags {
         // The shape being styled has no open contours.
-        kClosed_KeyFlag = 0x1
+        kClosed_KeyFlag = 0x1,
+        // The shape being styled doesn't have any joins and so isn't affected by join type.
+        kNoJoins_KeyFlag = 0x2
     };
 
     /**
index 561ec45..a616399 100644 (file)
@@ -21,6 +21,9 @@ static void test_far_from_origin(GrDrawContext* drawContext, GrPathRenderer* pr,
                                  GrResourceProvider* rp) {
     SkPath path;
     path.lineTo(49.0255089839f, 0.473541f);
+    // This extra line wasn't in the original bug but was added to fake out GrShape's special
+    // handling of single line segments.
+    path.rLineTo(0.015f, 0.015f);
     static constexpr SkScalar mvals[] = {14.0348252854f, 2.13026182736f,
                                          13.6122547187f, 118.309922702f,
                                          1912337682.09f, 2105391889.87f};
index ca907a1..5d1fa90 100644 (file)
@@ -119,7 +119,7 @@ private:
         SkPath path;
         shape.asPath(&path);
         // If the bounds are empty, the path ought to be as well.
-        if (bounds.isEmpty()) {
+        if (bounds.fLeft > bounds.fRight || bounds.fTop > bounds.fBottom) {
             REPORTER_ASSERT(r, path.isEmpty());
             return;
         }
@@ -513,6 +513,13 @@ static void test_basic(skiatest::Reporter* reporter, const GEO& geo) {
     REPORTER_ASSERT(reporter, hairlineCase.appliedPathEffectShape().style().isSimpleHairline());
 }
 
+// Was the shape pre-style geometry stored as something other than a general path. This somewhat
+// relies on knowing the internals of GrShape to know that this combination of tests is sufficient.
+static bool is_non_path(const GrShape& shape) {
+    return shape.asRRect(nullptr, nullptr, nullptr, nullptr) || shape.asLine(nullptr, nullptr) ||
+           shape.isEmpty();
+}
+
 template<typename GEO>
 static void test_scale(skiatest::Reporter* reporter, const GEO& geo) {
     sk_sp<SkPathEffect> dashPE = make_dash();
@@ -520,6 +527,15 @@ static void test_scale(skiatest::Reporter* reporter, const GEO& geo) {
     static const SkScalar kS1 = 1.f;
     static const SkScalar kS2 = 2.f;
 
+    // Scale may affect the key for stroked results. However, there are two ways in which that may
+    // not occur. The base shape may instantly recognized that the geo + stroke is equivalent to
+    // a simple filled geometry. An example is a stroked line may become a filled rrect.
+    // Alternatively, after applying the style the output path may be recognized as a simpler shape
+    // causing the shape with style applied to have a purely geometric key rather than a key derived
+    // from the base geometry and the style params (and scale factor).
+    auto wasSimplified = [](const TestCase& c) {
+        return !c.baseShape().style().applies() || is_non_path(c.appliedFullStyleShape());
+    };
     SkPaint fill;
     TestCase fillCase1(geo, fill, reporter, kS1);
     TestCase fillCase2(geo, fill, reporter, kS2);
@@ -539,15 +555,27 @@ static void test_scale(skiatest::Reporter* reporter, const GEO& geo) {
     stroke.setStrokeWidth(2.f);
     TestCase strokeCase1(geo, stroke, reporter, kS1);
     TestCase strokeCase2(geo, stroke, reporter, kS2);
-    // Scale affects the stroke.
-    strokeCase1.compare(reporter, strokeCase2, TestCase::kSameUpToStroke_ComparisonExpecation);
+    // Scale affects the stroke
+    if (wasSimplified(strokeCase1)) {
+        REPORTER_ASSERT(reporter, wasSimplified(strokeCase2));
+        strokeCase1.compare(reporter, strokeCase2, TestCase::kAllSame_ComparisonExpecation);
+    } else {
+        strokeCase1.compare(reporter, strokeCase2, TestCase::kSameUpToStroke_ComparisonExpecation);
+    }
 
     SkPaint strokeDash = stroke;
     strokeDash.setPathEffect(make_dash());
     TestCase strokeDashCase1(geo, strokeDash, reporter, kS1);
     TestCase strokeDashCase2(geo, strokeDash, reporter, kS2);
     // Scale affects the dash and the stroke.
-    strokeDashCase1.compare(reporter, strokeDashCase2,  TestCase::kSameUpToPE_ComparisonExpecation);
+    if (wasSimplified(strokeDashCase1)) {
+        REPORTER_ASSERT(reporter, wasSimplified(strokeDashCase2));
+        strokeDashCase1.compare(reporter, strokeDashCase2,
+                                TestCase::kAllSame_ComparisonExpecation);
+    } else {
+        strokeDashCase1.compare(reporter, strokeDashCase2,
+                                TestCase::kSameUpToPE_ComparisonExpecation);
+    }
 
     // Stroke and fill cases
     SkPaint strokeAndFill = stroke;
@@ -559,22 +587,20 @@ static void test_scale(skiatest::Reporter* reporter, const GEO& geo) {
     // Dash is ignored for stroke and fill
     TestCase strokeAndFillDashCase1(geo, strokeAndFillDash, reporter, kS1);
     TestCase strokeAndFillDashCase2(geo, strokeAndFillDash, reporter, kS2);
-    // Scale affects the stroke. Though, this can wind up creating a rect when the input is a rect.
-    // In that case we wind up with a pure geometry key and the geometries are the same.
-    SkRRect rrect;
-    if (strokeAndFillCase1.appliedFullStyleShape().asRRect(&rrect, nullptr, nullptr, nullptr)) {
-        // We currently only expect to get here in the rect->rect case.
-        REPORTER_ASSERT(reporter, rrect.isRect());
-        REPORTER_ASSERT(reporter,
-                        strokeAndFillCase1.baseShape().asRRect(&rrect, nullptr, nullptr, nullptr) &&
-                        rrect.isRect());
+    // Scale affects the stroke. Scale affects the stroke, but check to make sure this didn't
+    // become a simpler shape (e.g. stroke-and-filled rect can become a rect), in which case the
+    // scale shouldn't matter and the geometries should agree.
+    if (wasSimplified(strokeAndFillCase1)) {
+        REPORTER_ASSERT(reporter, wasSimplified(strokeAndFillCase1));
+        REPORTER_ASSERT(reporter, wasSimplified(strokeAndFillDashCase1));
+        REPORTER_ASSERT(reporter, wasSimplified(strokeAndFillDashCase2));
         strokeAndFillCase1.compare(reporter, strokeAndFillCase2,
                                    TestCase::kAllSame_ComparisonExpecation);
+        strokeAndFillDashCase1.compare(reporter, strokeAndFillDashCase2,
+                                       TestCase::kAllSame_ComparisonExpecation);
     } else {
         strokeAndFillCase1.compare(reporter, strokeAndFillCase2,
                                    TestCase::kSameUpToStroke_ComparisonExpecation);
-        strokeAndFillDashCase1.compare(reporter, strokeAndFillDashCase2,
-                                       TestCase::kSameUpToStroke_ComparisonExpecation);
     }
     strokeAndFillDashCase1.compare(reporter, strokeAndFillCase1,
                                    TestCase::kAllSame_ComparisonExpecation);
@@ -601,7 +627,15 @@ static void test_stroke_param_impl(skiatest::Reporter* reporter, const GEO& geo,
     TestCase strokeACase(geo, strokeA, reporter);
     TestCase strokeBCase(geo, strokeB, reporter);
     if (paramAffectsStroke) {
-        strokeACase.compare(reporter, strokeBCase, TestCase::kSameUpToStroke_ComparisonExpecation);
+        // If stroking is immediately incorporated into a geometric transformation then the base
+        // shapes will differ.
+        if (strokeACase.baseShape().style().applies()) {
+            strokeACase.compare(reporter, strokeBCase,
+                                TestCase::kSameUpToStroke_ComparisonExpecation);
+        } else {
+            strokeACase.compare(reporter, strokeBCase,
+                                TestCase::kAllDifferent_ComparisonExpecation);
+        }
     } else {
         strokeACase.compare(reporter, strokeBCase, TestCase::kAllSame_ComparisonExpecation);
     }
@@ -613,8 +647,15 @@ static void test_stroke_param_impl(skiatest::Reporter* reporter, const GEO& geo,
     TestCase strokeAndFillACase(geo, strokeAndFillA, reporter);
     TestCase strokeAndFillBCase(geo, strokeAndFillB, reporter);
     if (paramAffectsStroke) {
-        strokeAndFillACase.compare(reporter, strokeAndFillBCase,
-                                   TestCase::kSameUpToStroke_ComparisonExpecation);
+        // If stroking is immediately incorporated into a geometric transformation then the base
+        // shapes will differ.
+        if (strokeAndFillACase.baseShape().style().applies()) {
+            strokeAndFillACase.compare(reporter, strokeAndFillBCase,
+                                TestCase::kSameUpToStroke_ComparisonExpecation);
+        } else {
+            strokeAndFillACase.compare(reporter, strokeAndFillBCase,
+                                TestCase::kAllDifferent_ComparisonExpecation);
+        }
     } else {
         strokeAndFillACase.compare(reporter, strokeAndFillBCase,
                                    TestCase::kAllSame_ComparisonExpecation);
@@ -636,7 +677,13 @@ static void test_stroke_param_impl(skiatest::Reporter* reporter, const GEO& geo,
     TestCase dashACase(geo, dashA, reporter);
     TestCase dashBCase(geo, dashB, reporter);
     if (paramAffectsDashAndStroke) {
-        dashACase.compare(reporter, dashBCase, TestCase::kSameUpToStroke_ComparisonExpecation);
+        // If stroking is immediately incorporated into a geometric transformation then the base
+        // shapes will differ.
+        if (dashACase.baseShape().style().applies()) {
+            dashACase.compare(reporter, dashBCase, TestCase::kSameUpToStroke_ComparisonExpecation);
+        } else {
+            dashACase.compare(reporter, dashBCase, TestCase::kAllDifferent_ComparisonExpecation);
+        }
     } else {
         dashACase.compare(reporter, dashBCase, TestCase::kAllSame_ComparisonExpecation);
     }
@@ -664,6 +711,26 @@ static void test_stroke_cap(skiatest::Reporter* reporter, const GEO& geo) {
         affectsDashAndStroke);
 };
 
+static bool shape_known_not_to_have_joins(const GrShape& shape) {
+    return shape.asLine(nullptr, nullptr) || shape.isEmpty();
+}
+
+template <typename GEO>
+static void test_stroke_join(skiatest::Reporter* reporter, const GEO& geo) {
+    GrShape shape(geo, GrStyle(SkStrokeRec::kHairline_InitStyle));
+    // GrShape recognizes certain types don't have joins and will prevent the join type from
+    // affecting the style key.
+    // Dashing doesn't add additional joins. However, GrShape currently loses track of this
+    // after applying the dash.
+    bool affectsStroke = !shape_known_not_to_have_joins(shape);
+    test_stroke_param_impl<GEO, SkPaint::Join>(
+            reporter,
+            geo,
+            [](SkPaint* p, SkPaint::Join j) { p->setStrokeJoin(j);},
+            SkPaint::kRound_Join, SkPaint::kBevel_Join,
+            affectsStroke, true);
+};
+
 template <typename GEO>
 static void test_miter_limit(skiatest::Reporter* reporter, const GEO& geo) {
     auto setMiterJoinAndLimit = [](SkPaint* p, SkScalar miter) {
@@ -676,6 +743,9 @@ static void test_miter_limit(skiatest::Reporter* reporter, const GEO& geo) {
         p->setStrokeMiter(miter);
     };
 
+    GrShape shape(geo, GrStyle(SkStrokeRec::kHairline_InitStyle));
+    bool mayHaveJoins = !shape_known_not_to_have_joins(shape);
+
     // The miter limit should affect stroked and dashed-stroked cases when the join type is
     // miter.
     test_stroke_param_impl<GEO, SkScalar>(
@@ -683,7 +753,7 @@ static void test_miter_limit(skiatest::Reporter* reporter, const GEO& geo) {
         geo,
         setMiterJoinAndLimit,
         0.5f, 0.75f,
-        true,
+        mayHaveJoins,
         true);
 
     // The miter limit should not affect stroked and dashed-stroked cases when the join type is
@@ -991,7 +1061,7 @@ void test_path_effect_makes_empty_shape(skiatest::Reporter* reporter, const GEO&
 template <typename GEO>
 void test_path_effect_fails(skiatest::Reporter* reporter, const GEO& geo) {
     /**
-     * This path effect returns an empty path.
+     * This path effect always fails to apply.
      */
     class FailurePathEffect : SkPathEffect {
     public:
@@ -1461,6 +1531,63 @@ void test_lines(skiatest::Reporter* r) {
 
 }
 
+static void test_stroked_lines(skiatest::Reporter* r) {
+    // Paints to try
+    SkPaint buttCap;
+    buttCap.setStyle(SkPaint::kStroke_Style);
+    buttCap.setStrokeWidth(4);
+    buttCap.setStrokeCap(SkPaint::kButt_Cap);
+
+    SkPaint squareCap = buttCap;
+    squareCap.setStrokeCap(SkPaint::kSquare_Cap);
+
+    SkPaint roundCap = buttCap;
+    roundCap.setStrokeCap(SkPaint::kRound_Cap);
+
+    // vertical
+    SkPath linePath;
+    linePath.moveTo(4, 4);
+    linePath.lineTo(4, 5);
+
+    SkPaint fill;
+
+    TestCase(linePath, buttCap, r).compare(r, TestCase(SkRect::MakeLTRB(2, 4, 6, 5), fill, r),
+                                           TestCase::kAllSame_ComparisonExpecation);
+
+    TestCase(linePath, squareCap, r).compare(r, TestCase(SkRect::MakeLTRB(2, 2, 6, 7), fill, r),
+                                             TestCase::kAllSame_ComparisonExpecation);
+
+    TestCase(linePath, roundCap, r).compare(r,
+        TestCase(SkRRect::MakeRectXY(SkRect::MakeLTRB(2, 2, 6, 7), 2, 2), fill, r),
+        TestCase::kAllSame_ComparisonExpecation);
+
+    // horizontal
+    linePath.reset();
+    linePath.moveTo(4, 4);
+    linePath.lineTo(5, 4);
+
+    TestCase(linePath, buttCap, r).compare(r, TestCase(SkRect::MakeLTRB(4, 2, 5, 6), fill, r),
+                                           TestCase::kAllSame_ComparisonExpecation);
+    TestCase(linePath, squareCap, r).compare(r, TestCase(SkRect::MakeLTRB(2, 2, 7, 6), fill, r),
+                                             TestCase::kAllSame_ComparisonExpecation);
+    TestCase(linePath, roundCap, r).compare(r,
+         TestCase(SkRRect::MakeRectXY(SkRect::MakeLTRB(2, 2, 7, 6), 2, 2), fill, r),
+         TestCase::kAllSame_ComparisonExpecation);
+
+    // point
+    linePath.reset();
+    linePath.moveTo(4, 4);
+    linePath.lineTo(4, 4);
+
+    TestCase(linePath, buttCap, r).compare(r, TestCase(SkRect::MakeEmpty(), fill, r),
+                                           TestCase::kAllSame_ComparisonExpecation);
+    TestCase(linePath, squareCap, r).compare(r, TestCase(SkRect::MakeLTRB(2, 2, 6, 6), fill, r),
+                                             TestCase::kAllSame_ComparisonExpecation);
+    TestCase(linePath, roundCap, r).compare(r,
+        TestCase(SkRRect::MakeRectXY(SkRect::MakeLTRB(2, 2, 6, 6), 2, 2), fill, r),
+        TestCase::kAllSame_ComparisonExpecation);
+}
+
 DEF_TEST(GrShape, reporter) {
     for (auto r : { SkRect::MakeWH(10, 20),
                     SkRect::MakeWH(-10, -20),
@@ -1475,10 +1602,7 @@ DEF_TEST(GrShape, reporter) {
                 reporter, r,
                 [](SkPaint* p, SkScalar w) { p->setStrokeWidth(w);},
                 SkIntToScalar(2), SkIntToScalar(4));
-        test_stroke_param<SkRect, SkPaint::Join>(
-                reporter, r,
-                [](SkPaint* p, SkPaint::Join j) { p->setStrokeJoin(j);},
-                SkPaint::kMiter_Join, SkPaint::kRound_Join);
+        test_stroke_join(reporter, r);
         test_stroke_cap(reporter, r);
         test_miter_limit(reporter, r);
         test_path_effect_makes_rrect(reporter, r);
@@ -1503,10 +1627,7 @@ DEF_TEST(GrShape, reporter) {
                           reporter, rr,
                           [](SkPaint* p, SkScalar w) { p->setStrokeWidth(w);},
                           SkIntToScalar(2), SkIntToScalar(4));
-        test_stroke_param<SkRRect, SkPaint::Join>(
-                          reporter, rr,
-                          [](SkPaint* p, SkPaint::Join j) { p->setStrokeJoin(j);},
-                          SkPaint::kMiter_Join, SkPaint::kRound_Join);
+        test_stroke_join(reporter, rr);
         test_stroke_cap(reporter, rr);
         test_miter_limit(reporter, rr);
         test_path_effect_makes_rrect(reporter, rr);
@@ -1557,6 +1678,15 @@ DEF_TEST(GrShape, reporter) {
     linePath.lineTo(10, 10);
     paths.emplace_back(linePath, false, false, true, SkRRect());
 
+    // Horizontal and vertical paths become rrects when stroked.
+    SkPath vLinePath;
+    vLinePath.lineTo(0, 10);
+    paths.emplace_back(vLinePath, false, false, true, SkRRect());
+
+    SkPath hLinePath;
+    hLinePath.lineTo(10, 0);
+    paths.emplace_back(hLinePath, false, false, true, SkRRect());
+
     for (auto testPath : paths) {
         for (bool inverseFill : {false, true}) {
             if (inverseFill) {
@@ -1587,10 +1717,7 @@ DEF_TEST(GrShape, reporter) {
                 reporter, path,
                 [](SkPaint* p, SkScalar w) { p->setStrokeWidth(w);},
                 SkIntToScalar(2), SkIntToScalar(4));
-            test_stroke_param<SkPath, SkPaint::Join>(
-                reporter, path,
-                [](SkPaint* p, SkPaint::Join j) { p->setStrokeJoin(j);},
-                SkPaint::kMiter_Join, SkPaint::kRound_Join);
+            test_stroke_join(reporter, path);
             test_stroke_cap(reporter, path);
             test_miter_limit(reporter, path);
             test_unknown_path_effect(reporter, path);
@@ -1621,9 +1748,11 @@ DEF_TEST(GrShape, reporter) {
         strokePaint.setStrokeWidth(3.f);
         strokePaint.setStyle(SkPaint::kStroke_Style);
         TestCase strokePathCase(path, strokePaint, reporter);
-        REPORTER_ASSERT(reporter, testPath.fIsRRectForStroke ==
-                                  strokePathCase.baseShape().asRRect(&rrect, nullptr, nullptr,
-                                                                     nullptr));
+        if (testPath.fIsRRectForStroke) {
+            REPORTER_ASSERT(reporter, strokePathCase.baseShape().asRRect(&rrect, nullptr, nullptr,
+                                                                         nullptr));
+        }
+
         if (testPath.fIsRRectForStroke) {
             REPORTER_ASSERT(reporter, rrect == testPath.fRRect);
             TestCase strokeRRectCase(rrect, strokePaint, reporter);
@@ -1638,6 +1767,8 @@ DEF_TEST(GrShape, reporter) {
     test_empty_shape(reporter);
 
     test_lines(reporter);
+
+    test_stroked_lines(reporter);
 }
 
 #endif