ccpr: Add a GrOctoBounds class

Encapsulates all the various code dealing with 45-degree bounding
boxes into a central GrOctoBounds class.

Bug: skia:
Change-Id: Ibc8ac4371af8f310e711579c3c77e0b3a53aff0f
Reviewed-on: https://skia-review.googlesource.com/c/skia/+/212563
Commit-Queue: Chris Dalton <csmartdalton@google.com>
Reviewed-by: Robert Phillips <robertphillips@google.com>
diff --git a/src/gpu/ccpr/GrCCDrawPathsOp.cpp b/src/gpu/ccpr/GrCCDrawPathsOp.cpp
index 698ae4e..be5290e 100644
--- a/src/gpu/ccpr/GrCCDrawPathsOp.cpp
+++ b/src/gpu/ccpr/GrCCDrawPathsOp.cpp
@@ -14,6 +14,7 @@
 #include "src/gpu/ccpr/GrCCPathCache.h"
 #include "src/gpu/ccpr/GrCCPerFlushResources.h"
 #include "src/gpu/ccpr/GrCoverageCountingPathRenderer.h"
+#include "src/gpu/ccpr/GrOctoBounds.h"
 
 static bool has_coord_transforms(const GrPaint& paint) {
     GrFragmentProcessor::Iter iter(paint);
@@ -361,22 +362,22 @@
     // bounding boxes: One in device space, as well as a second one rotated an additional 45
     // degrees. The path vertex shader uses these two bounding boxes to generate an octagon that
     // circumscribes the path.
-    SkRect devBounds, devBounds45;
+    GrOctoBounds octoBounds;
     SkIRect devIBounds;
     SkIVector devToAtlasOffset;
     if (auto atlas = resources->renderShapeInAtlas(
-                fMaskDevIBounds, fMatrix, fShape, fStrokeDevWidth, &devBounds, &devBounds45,
-                &devIBounds, &devToAtlasOffset)) {
+                fMaskDevIBounds, fMatrix, fShape, fStrokeDevWidth, &octoBounds, &devIBounds,
+                &devToAtlasOffset)) {
         op->recordInstance(atlas->textureProxy(), resources->nextPathInstanceIdx());
-        resources->appendDrawPathInstance().set(devBounds, devBounds45, devToAtlasOffset,
-                                                SkPMColor4f_toFP16(fColor), doEvenOddFill);
+        resources->appendDrawPathInstance().set(
+                octoBounds, devToAtlasOffset, SkPMColor4f_toFP16(fColor), doEvenOddFill);
 
         if (fDoCachePathMask) {
             SkASSERT(fCacheEntry);
             SkASSERT(!fCacheEntry->cachedAtlas());
             SkASSERT(fShapeConservativeIBounds == fMaskDevIBounds);
-            fCacheEntry->setCoverageCountAtlas(onFlushRP, atlas, devToAtlasOffset, devBounds,
-                                               devBounds45, devIBounds, fCachedMaskShift);
+            fCacheEntry->setCoverageCountAtlas(
+                    onFlushRP, atlas, devToAtlasOffset, octoBounds, devIBounds, fCachedMaskShift);
         }
     }
 }
diff --git a/src/gpu/ccpr/GrCCPathCache.cpp b/src/gpu/ccpr/GrCCPathCache.cpp
index 4ca0845..3bc26c9 100644
--- a/src/gpu/ccpr/GrCCPathCache.cpp
+++ b/src/gpu/ccpr/GrCCPathCache.cpp
@@ -352,8 +352,7 @@
 
 void GrCCPathCacheEntry::setCoverageCountAtlas(
         GrOnFlushResourceProvider* onFlushRP, GrCCAtlas* atlas, const SkIVector& atlasOffset,
-        const SkRect& devBounds, const SkRect& devBounds45, const SkIRect& devIBounds,
-        const SkIVector& maskShift) {
+        const GrOctoBounds& octoBounds, const SkIRect& devIBounds, const SkIVector& maskShift) {
     SkASSERT(fOnFlushRefCnt > 0);
     SkASSERT(!fCachedAtlas);  // Otherwise we would need to call releaseCachedAtlas().
 
@@ -369,9 +368,7 @@
 
     fAtlasOffset = atlasOffset + maskShift;
 
-    float dx = (float)maskShift.fX, dy = (float)maskShift.fY;
-    fDevBounds = devBounds.makeOffset(-dx, -dy);
-    fDevBounds45 = GrCCPathProcessor::MakeOffset45(devBounds45, -dx, -dy);
+    fOctoBounds.setOffset(octoBounds, -maskShift.fX, -maskShift.fY);
     fDevIBounds = devIBounds.makeOffset(-maskShift.fX, -maskShift.fY);
 }
 
diff --git a/src/gpu/ccpr/GrCCPathCache.h b/src/gpu/ccpr/GrCCPathCache.h
index c674d62..17e11b4 100644
--- a/src/gpu/ccpr/GrCCPathCache.h
+++ b/src/gpu/ccpr/GrCCPathCache.h
@@ -229,9 +229,9 @@
     // Called once our path has been rendered into the mainline CCPR (fp16, coverage count) atlas.
     // The caller will stash this atlas texture away after drawing, and during the next flush,
     // recover it and attempt to copy any paths that got reused into permanent 8-bit atlases.
-    void setCoverageCountAtlas(GrOnFlushResourceProvider*, GrCCAtlas*, const SkIVector& atlasOffset,
-                               const SkRect& devBounds, const SkRect& devBounds45,
-                               const SkIRect& devIBounds, const SkIVector& maskShift);
+    void setCoverageCountAtlas(
+            GrOnFlushResourceProvider*, GrCCAtlas*, const SkIVector& atlasOffset,
+            const GrOctoBounds& octoBounds, const SkIRect& devIBounds, const SkIVector& maskShift);
 
     // Called once our path mask has been copied into a permanent, 8-bit atlas. This method points
     // the entry at the new atlas and updates the GrCCCCachedAtlas data.
@@ -260,8 +260,7 @@
     SkIVector fAtlasOffset;
 
     MaskTransform fMaskTransform;
-    SkRect fDevBounds;
-    SkRect fDevBounds45;
+    GrOctoBounds fOctoBounds;
     SkIRect fDevIBounds;
 
     int fOnFlushRefCnt = 0;
@@ -363,8 +362,8 @@
                                              const SkIVector& shift, uint64_t color,
                                              DoEvenOddFill doEvenOddFill) {
     float dx = (float)shift.fX, dy = (float)shift.fY;
-    this->set(entry.fDevBounds.makeOffset(dx, dy), MakeOffset45(entry.fDevBounds45, dx, dy),
-              entry.fAtlasOffset - shift, color, doEvenOddFill);
+    this->set(
+            entry.fOctoBounds.makeOffset(dx, dy), entry.fAtlasOffset - shift, color, doEvenOddFill);
 }
 
 #endif
diff --git a/src/gpu/ccpr/GrCCPathProcessor.h b/src/gpu/ccpr/GrCCPathProcessor.h
index 642cbdc..416cdb1 100644
--- a/src/gpu/ccpr/GrCCPathProcessor.h
+++ b/src/gpu/ccpr/GrCCPathProcessor.h
@@ -13,6 +13,7 @@
 #include "src/gpu/GrCaps.h"
 #include "src/gpu/GrGeometryProcessor.h"
 #include "src/gpu/GrPipeline.h"
+#include "src/gpu/ccpr/GrOctoBounds.h"
 
 class GrCCPathCacheEntry;
 class GrCCPerFlushResources;
@@ -31,13 +32,6 @@
  */
 class GrCCPathProcessor : public GrGeometryProcessor {
 public:
-    // Helper to offset the 45-degree bounding box returned by GrCCPathParser::parsePath().
-    static SkRect MakeOffset45(const SkRect& devBounds45, float dx, float dy) {
-        // devBounds45 is in "| 1  -1 | * devCoords" space.
-        //                    | 1   1 |
-        return devBounds45.makeOffset(dx - dy, dx + dy);
-    }
-
     enum class DoEvenOddFill : bool {
         kNo = false,
         kYes = true
@@ -45,13 +39,13 @@
 
     struct Instance {
         SkRect fDevBounds;  // "right < left" indicates even-odd fill type.
-        SkRect fDevBounds45;  // Bounding box in "| 1  -1 | * devCoords" space.
+        SkRect fDevBounds45;  // Bounding box in "| 1  -1 | * devCoords" space. See GrOctoBounds.
                               //                  | 1   1 |
         SkIVector fDevToAtlasOffset;  // Translation from device space to location in atlas.
         uint64_t fColor;  // Color always stored as 4 x fp16
 
-        void set(const SkRect& devBounds, const SkRect& devBounds45,
-                 const SkIVector& devToAtlasOffset, uint64_t, DoEvenOddFill = DoEvenOddFill::kNo);
+        void set(const GrOctoBounds&, const SkIVector& devToAtlasOffset, uint64_t,
+                 DoEvenOddFill = DoEvenOddFill::kNo);
         void set(const GrCCPathCacheEntry&, const SkIVector& shift, uint64_t,
                  DoEvenOddFill = DoEvenOddFill::kNo);
     };
@@ -96,19 +90,21 @@
 };
 
 inline void GrCCPathProcessor::Instance::set(
-        const SkRect& devBounds, const SkRect& devBounds45, const SkIVector& devToAtlasOffset,
-        uint64_t color, DoEvenOddFill doEvenOddFill) {
+        const GrOctoBounds& octoBounds, const SkIVector& devToAtlasOffset, uint64_t color,
+        DoEvenOddFill doEvenOddFill) {
     if (DoEvenOddFill::kNo == doEvenOddFill) {
         // We cover "nonzero" paths with clockwise triangles, which is the default result from
-        // normal bounding boxes.
-        fDevBounds = devBounds;
-        fDevBounds45 = devBounds45;
+        // normal octo bounds.
+        fDevBounds = octoBounds.bounds();
+        fDevBounds45 = octoBounds.bounds45();
     } else {
-        // We cover "even/odd" paths with counterclockwise triangles. Reorder the bounding box
-        // vertices so the output is flipped horizontally.
-        fDevBounds.setLTRB(devBounds.fRight, devBounds.fTop, devBounds.fLeft, devBounds.fBottom);
+        // We cover "even/odd" paths with counterclockwise triangles. Here we reorder the bounding
+        // box vertices so the output is flipped horizontally.
+        fDevBounds.setLTRB(
+                octoBounds.right(), octoBounds.top(), octoBounds.left(), octoBounds.bottom());
         fDevBounds45.setLTRB(
-                devBounds45.fBottom, devBounds45.fRight, devBounds45.fTop, devBounds45.fLeft);
+                octoBounds.bottom45(), octoBounds.right45(), octoBounds.top45(),
+                octoBounds.left45());
     }
     fDevToAtlasOffset = devToAtlasOffset;
     fColor = color;
diff --git a/src/gpu/ccpr/GrCCPerFlushResources.cpp b/src/gpu/ccpr/GrCCPerFlushResources.cpp
index 567a778..ae001ce 100644
--- a/src/gpu/ccpr/GrCCPerFlushResources.cpp
+++ b/src/gpu/ccpr/GrCCPerFlushResources.cpp
@@ -282,9 +282,9 @@
     emplace_at_memcpy(&fCopyPathRanges, fCurrCopyAtlasRangesIdx, std::move(srcProxy), 1);
 }
 
-static bool transform_path_pts(const SkMatrix& m, const SkPath& path,
-                               const SkAutoSTArray<32, SkPoint>& outDevPts, SkRect* devBounds,
-                               SkRect* devBounds45) {
+static bool transform_path_pts(
+        const SkMatrix& m, const SkPath& path, const SkAutoSTArray<32, SkPoint>& outDevPts,
+        GrOctoBounds* octoBounds) {
     const SkPoint* pts = SkPathPriv::PointData(path);
     int numPts = path.countPoints();
     SkASSERT(numPts + 1 <= outDevPts.count());
@@ -331,16 +331,19 @@
     SkPoint topLeftPts[2], bottomRightPts[2];
     topLeft.store(topLeftPts);
     bottomRight.store(bottomRightPts);
-    devBounds->setLTRB(topLeftPts[0].x(), topLeftPts[0].y(), bottomRightPts[0].x(),
-                       bottomRightPts[0].y());
-    devBounds45->setLTRB(topLeftPts[1].x(), topLeftPts[1].y(), bottomRightPts[1].x(),
-                         bottomRightPts[1].y());
+
+    const SkRect& devBounds = SkRect::MakeLTRB(
+            topLeftPts[0].x(), topLeftPts[0].y(), bottomRightPts[0].x(), bottomRightPts[0].y());
+    const SkRect& devBounds45 = SkRect::MakeLTRB(
+            topLeftPts[1].x(), topLeftPts[1].y(), bottomRightPts[1].x(), bottomRightPts[1].y());
+
+    octoBounds->set(devBounds, devBounds45);
     return true;
 }
 
 GrCCAtlas* GrCCPerFlushResources::renderShapeInAtlas(
         const SkIRect& clipIBounds, const SkMatrix& m, const GrShape& shape, float strokeDevWidth,
-        SkRect* devBounds, SkRect* devBounds45, SkIRect* devIBounds, SkIVector* devToAtlasOffset) {
+        GrOctoBounds* octoBounds, SkIRect* devIBounds, SkIVector* devToAtlasOffset) {
     SkASSERT(this->isMapped());
     SkASSERT(fNextPathInstanceIdx < fEndPathInstance);
 
@@ -350,7 +353,7 @@
         SkDEBUGCODE(--fEndPathInstance);
         return nullptr;
     }
-    if (!transform_path_pts(m, path, fLocalDevPtsBuffer, devBounds, devBounds45)) {
+    if (!transform_path_pts(m, path, fLocalDevPtsBuffer, octoBounds)) {
         // The transformed path had infinite or NaN bounds.
         SkDEBUGCODE(--fEndPathInstance);
         return nullptr;
@@ -358,14 +361,11 @@
 
     const SkStrokeRec& stroke = shape.style().strokeRec();
     if (!stroke.isFillStyle()) {
-        float r = SkStrokeRec::GetInflationRadius(stroke.getJoin(), stroke.getMiter(),
-                                                  stroke.getCap(), strokeDevWidth);
-        devBounds->outset(r, r);
-        // devBounds45 is in (| 1 -1 | * devCoords) space.
-        //                    | 1  1 |
-        devBounds45->outset(r*SK_ScalarSqrt2, r*SK_ScalarSqrt2);
+        float r = SkStrokeRec::GetInflationRadius(
+                stroke.getJoin(), stroke.getMiter(), stroke.getCap(), strokeDevWidth);
+        octoBounds->outset(r);
     }
-    devBounds->roundOut(devIBounds);
+    octoBounds->roundOut(devIBounds);
 
     GrScissorTest scissorTest;
     SkIRect clippedPathIBounds;
diff --git a/src/gpu/ccpr/GrCCPerFlushResources.h b/src/gpu/ccpr/GrCCPerFlushResources.h
index b005a3b..4fcb114 100644
--- a/src/gpu/ccpr/GrCCPerFlushResources.h
+++ b/src/gpu/ccpr/GrCCPerFlushResources.h
@@ -16,6 +16,7 @@
 
 class GrCCPathCache;
 class GrCCPathCacheEntry;
+class GrOctoBounds;
 class GrOnFlushResourceProvider;
 class GrShape;
 
@@ -79,9 +80,9 @@
     //
     // strokeDevWidth must be 0 for fills, 1 for hairlines, or the stroke width in device-space
     // pixels for non-hairline strokes (implicitly requiring a rigid-body transform).
-    GrCCAtlas* renderShapeInAtlas(const SkIRect& clipIBounds, const SkMatrix&, const GrShape&,
-                                  float strokeDevWidth, SkRect* devBounds, SkRect* devBounds45,
-                                  SkIRect* devIBounds, SkIVector* devToAtlasOffset);
+    GrCCAtlas* renderShapeInAtlas(
+            const SkIRect& clipIBounds, const SkMatrix&, const GrShape&, float strokeDevWidth,
+            GrOctoBounds*, SkIRect* devIBounds, SkIVector* devToAtlasOffset);
     const GrCCAtlas* renderDeviceSpacePathInAtlas(const SkIRect& clipIBounds, const SkPath& devPath,
                                                   const SkIRect& devPathIBounds,
                                                   SkIVector* devToAtlasOffset);
diff --git a/src/gpu/ccpr/GrOctoBounds.cpp b/src/gpu/ccpr/GrOctoBounds.cpp
new file mode 100644
index 0000000..facf1aa
--- /dev/null
+++ b/src/gpu/ccpr/GrOctoBounds.cpp
@@ -0,0 +1,57 @@
+/*
+ * Copyright 2019 Google Inc.
+ *
+ * Use of this source code is governed by a BSD-style license that can be
+ * found in the LICENSE file.
+ */
+
+#include "src/gpu/ccpr/GrOctoBounds.h"
+
+#ifdef SK_DEBUG
+void GrOctoBounds::validateBoundsAreTight() const {
+    this->validateBoundsAreTight([](bool cond, const char* file, int line, const char* code) {
+        SkASSERTF(cond, "%s(%d): assertion failure: \"assert(%s)\"", file, line, code);
+    });
+}
+
+void GrOctoBounds::validateBoundsAreTight(const std::function<void(
+        bool cond, const char* file, int line, const char* code)>& validateFn) const {
+    constexpr static float epsilon = 1e-3;
+
+    float l=fBounds.left(), l45=fBounds45.left();
+    float t=fBounds.top(), t45=fBounds45.top();
+    float r=fBounds.right(), r45=fBounds45.right();
+    float b=fBounds.bottom(), b45=fBounds45.bottom();
+
+#define VALIDATE(CODE) validateFn(CODE, __FILE__, __LINE__, #CODE)
+    // Verify diagonals are inside far corners of the dev bounds.
+    VALIDATE(l45 >= Get_x45(l,b) - epsilon);
+    VALIDATE(t45 >= Get_y45(l,t) - epsilon);
+    VALIDATE(r45 <= Get_x45(r,t) + epsilon);
+    VALIDATE(b45 <= Get_y45(r,b) + epsilon);
+    // Verify verticals and horizontals are inside far corners of the 45-degree dev bounds.
+    VALIDATE(l >= Get_x(l45,t45) - epsilon);
+    VALIDATE(t >= Get_y(r45,t45) - epsilon);
+    VALIDATE(r <= Get_x(r45,b45) + epsilon);
+    VALIDATE(b <= Get_y(l45,b45) + epsilon);
+    // Verify diagonals are outside middle corners of the dev bounds.
+    VALIDATE(l45 <= Get_x45(r,b) + epsilon);
+    VALIDATE(l45 <= Get_x45(l,t) + epsilon);
+    VALIDATE(t45 <= Get_y45(l,b) + epsilon);
+    VALIDATE(t45 <= Get_y45(r,t) + epsilon);
+    VALIDATE(r45 >= Get_x45(l,t) - epsilon);
+    VALIDATE(r45 >= Get_x45(r,b) - epsilon);
+    VALIDATE(b45 >= Get_y45(r,t) - epsilon);
+    VALIDATE(b45 >= Get_y45(l,b) - epsilon);
+    // Verify verticals and horizontals are outside middle corners of the 45-degree dev bounds.
+    VALIDATE(l <= Get_x(l45,b45) + epsilon);
+    VALIDATE(l <= Get_x(r45,t45) + epsilon);
+    VALIDATE(t <= Get_y(r45,b45) + epsilon);
+    VALIDATE(t <= Get_y(l45,t45) + epsilon);
+    VALIDATE(r >= Get_x(r45,t45) - epsilon);
+    VALIDATE(r >= Get_x(l45,b45) - epsilon);
+    VALIDATE(b >= Get_y(l45,t45) - epsilon);
+    VALIDATE(b >= Get_y(r45,b45) - epsilon);
+#undef VALIDATE
+}
+#endif
diff --git a/src/gpu/ccpr/GrOctoBounds.h b/src/gpu/ccpr/GrOctoBounds.h
new file mode 100644
index 0000000..7630618
--- /dev/null
+++ b/src/gpu/ccpr/GrOctoBounds.h
@@ -0,0 +1,98 @@
+/*
+ * Copyright 2019 Google Inc.
+ *
+ * Use of this source code is governed by a BSD-style license that can be
+ * found in the LICENSE file.
+ */
+
+#ifndef GrOctoBounds_DEFINED
+#define GrOctoBounds_DEFINED
+
+#include "include/core/SkRect.h"
+#include <functional>
+
+/**
+ * This class is composed of two bounding boxes: one in device space, and one in a 45-degree rotated
+ * space.
+ *
+ * The 45-degree bounding box resides in "| 1  -1 | * coords" space.
+ *                                        | 1   1 |
+ *
+ * The intersection of these two boxes defines the bounding octagon of a shape.
+ *
+ * Furthermore, both bounding boxes are fully tightened. This means we can blindly find the
+ * intersections between each diagonal and its vertical and horizontal neighbors, and be left with
+ * 8 points that define a convex (possibly degenerate) octagon.
+ */
+class GrOctoBounds {
+public:
+    void set(const SkRect& bounds, const SkRect& bounds45) {
+        fBounds = bounds;
+        fBounds45 = bounds45;
+        SkDEBUGCODE(this->validateBoundsAreTight());
+    }
+
+    const SkRect& bounds() const { return fBounds; }
+    float left() const { return fBounds.left(); }
+    float top() const { return fBounds.top(); }
+    float right() const { return fBounds.right(); }
+    float bottom() const { return fBounds.bottom(); }
+
+
+    // The 45-degree bounding box resides in "| 1  -1 | * coords" space.
+    //                                        | 1   1 |
+    const SkRect& bounds45() const { return fBounds45; }
+    float left45() const { return fBounds45.left(); }
+    float top45() const { return fBounds45.top(); }
+    float right45() const { return fBounds45.right(); }
+    float bottom45() const { return fBounds45.bottom(); }
+
+    void roundOut(SkIRect* out) const {
+        // The octagon is the intersection of fBounds and fBounds45 (see the comment at the start of
+        // the class). The octagon's bounding box is therefore just fBounds. And the integer
+        // bounding box can be found by simply rounding out fBounds.
+        fBounds.roundOut(out);
+    }
+
+    GrOctoBounds makeOffset(float dx, float dy) const {
+        GrOctoBounds offset;
+        offset.setOffset(*this, dx, dy);
+        return offset;
+    }
+
+    void setOffset(const GrOctoBounds& octoBounds, float dx, float dy) {
+        fBounds = octoBounds.fBounds.makeOffset(dx, dy);
+        fBounds45 = octoBounds.fBounds45.makeOffset(dx - dy, dx + dy);
+        SkDEBUGCODE(this->validateBoundsAreTight());
+    }
+
+    void outset(float radius) {
+        fBounds.outset(radius, radius);
+        fBounds45.outset(radius*SK_ScalarSqrt2, radius*SK_ScalarSqrt2);
+        SkDEBUGCODE(this->validateBoundsAreTight());
+    }
+
+    // The 45-degree bounding box resides in "| 1  -1 | * coords" space.
+    //                                        | 1   1 |
+    //
+    //  i.e., | x45 | = | x - y |
+    //        | y45 | = | x + y |
+    //
+    // These methods transform points between device space and 45-degree space.
+    constexpr static float Get_x45(float x, float y) { return x - y; }
+    constexpr static float Get_y45(float x, float y) { return x + y; }
+    constexpr static float Get_x(float x45, float y45) { return (x45 + y45) * .5f; }
+    constexpr static float Get_y(float x45, float y45) { return (y45 - x45) * .5f; }
+
+#ifdef SK_DEBUG
+    void validateBoundsAreTight() const;
+    void validateBoundsAreTight(const std::function<void(
+            bool cond, const char* file, int line, const char* code)>& validateFn) const;
+#endif
+
+private:
+    SkRect fBounds;
+    SkRect fBounds45;
+};
+
+#endif