Consolidate quad optimizations into single internal function.

Routes all non-textured quad draws through single internal function

Change-Id: Ief66864a0ad2d598982c5bf500c8a84ecbf84387
Reviewed-on: https://skia-review.googlesource.com/c/skia/+/215455
Commit-Queue: Michael Ludwig <michaelludwig@google.com>
Reviewed-by: Robert Phillips <robertphillips@google.com>
diff --git a/src/gpu/GrRenderTargetContext.cpp b/src/gpu/GrRenderTargetContext.cpp
index f10bda9..c579c1c 100644
--- a/src/gpu/GrRenderTargetContext.cpp
+++ b/src/gpu/GrRenderTargetContext.cpp
@@ -44,6 +44,7 @@
 #include "src/gpu/effects/GrTextureDomain.h"
 #include "src/gpu/effects/generated/GrColorMatrixFragmentProcessor.h"
 #include "src/gpu/geometry/GrQuad.h"
+#include "src/gpu/geometry/GrQuadUtils.h"
 #include "src/gpu/geometry/GrShape.h"
 #include "src/gpu/ops/GrAtlasTextOp.h"
 #include "src/gpu/ops/GrClearOp.h"
@@ -450,67 +451,27 @@
 void GrRenderTargetContext::drawPaint(const GrClip& clip,
                                       GrPaint&& paint,
                                       const SkMatrix& viewMatrix) {
-    ASSERT_SINGLE_OWNER
-    RETURN_IF_ABANDONED
-    SkDEBUGCODE(this->validate();)
-    GR_CREATE_TRACE_MARKER_CONTEXT("GrRenderTargetContext", "drawPaint", fContext);
-
-    // set rect to be big enough to fill the space, but not super-huge, so we
-    // don't overflow fixed-point implementations
-
+    // Start with the render target, since that is the maximum content we could possibly fill.
+    // drawFilledQuad() will automatically restrict it to clip bounds for us if possible.
     SkRect r = fRenderTargetProxy->getBoundsRect();
-
-    // Check if we can optimize a clipped drawPaint(). We only do the transformation when there are
-    // no fragment processors because they may depend on having correct local coords and this path
-    // draws in device space without a local matrix. It currently handles converting clipRRect()
-    // to drawRRect() and solid colors to screen-filling drawRects() (which are then converted into
-    // clears if possible in drawRect).
     if (!paint.numTotalFragmentProcessors()) {
-        SkRRect rrect;
-        GrAA aa = GrAA::kNo;
-        if (clip.isRRect(r, &rrect, &aa)) {
-            if (rrect.isRect()) {
-                // Use drawFilledRect() with no clip and the reduced rectangle
-                this->drawFilledRect(GrNoClip(), std::move(paint), aa, SkMatrix::I(), rrect.rect());
-            } else {
-                // Use drawRRect() with no clip
-                this->drawRRect(GrNoClip(), std::move(paint), aa, SkMatrix::I(), rrect,
-                                GrStyle::SimpleFill());
-            }
-        } else {
-            // Use drawFilledRect() with no view matrix to draw a fullscreen quad, but preserve
-            // the clip. Since the paint has no FPs we can drop the view matrix without worrying
-            // about local coordinates. If the clip is simple, drawFilledRect() will turn this into
-            // a clear or a scissored clear.
-            this->drawFilledRect(clip, std::move(paint), aa, SkMatrix::I(), r);
+        // The paint is trivial so we won't need to use local coordinates, so skip calculating the
+        // inverse view matrix.
+        this->fillRectToRect(clip, std::move(paint), GrAA::kNo, SkMatrix::I(), r, r);
+    } else {
+        // Use the inverse view matrix to arrive at appropriate local coordinates for the paint.
+        SkMatrix localMatrix;
+        if (!viewMatrix.invert(&localMatrix)) {
+            return;
         }
-        return;
+        this->fillRectWithLocalMatrix(clip, std::move(paint), GrAA::kNo, SkMatrix::I(), r,
+                                      localMatrix);
     }
-
-    // Since the paint is not trivial, there's no way at this point drawRect() could have converted
-    // this drawPaint() into an optimized clear. drawRect() would then use GrFillRectOp without
-    // a local matrix, so we can simplify things and use the local matrix variant to draw a screen
-    // filling rect with the inverse view matrix for local coords, which works for all matrix
-    // conditions.
-    SkMatrix localMatrix;
-    if (!viewMatrix.invert(&localMatrix)) {
-        return;
-    }
-
-    AutoCheckFlush acf(this->drawingManager());
-    std::unique_ptr<GrDrawOp> op = GrFillRectOp::Make(
-            fContext, std::move(paint), GrAAType::kNone, GrQuadAAFlags::kNone,
-            GrQuad(r), GrQuad::MakeFromRect(r, localMatrix));
-    this->addDrawOp(clip, std::move(op));
-}
-
-static inline bool rect_contains_inclusive(const SkRect& rect, const SkPoint& point) {
-    return point.fX >= rect.fLeft && point.fX <= rect.fRight &&
-           point.fY >= rect.fTop && point.fY <= rect.fBottom;
 }
 
 // Attempts to crop a rect and optional local rect to the clip boundaries.
 // Returns false if the draw can be skipped entirely.
+// FIXME to be removed once drawTexture et al are updated to use attemptQuadOptimization instead
 static bool crop_filled_rect(int width, int height, const GrClip& clip,
                              const SkMatrix& viewMatrix, SkRect* rect,
                              SkRect* localRect = nullptr) {
@@ -560,173 +521,186 @@
     return rect->intersect(clipBounds);
 }
 
-GrQuadAAFlags set_edge_flag(GrQuadAAFlags currentFlags, GrQuadAAFlags edge, GrAA edgeState) {
-    if (edgeState == GrAA::kNo) {
-        // Turn off 'edge' in currentFlags
-        return currentFlags & (~edge);
-    } else {
-        // Turn on 'edge' in currentFlags
-        return currentFlags | edge;
-    }
-}
+enum class GrRenderTargetContext::QuadOptimization {
+    // The rect to draw doesn't intersect clip or render target, so no draw op should be added
+    kDiscarded,
+    // The rect to draw was converted to some other op and appended to the oplist, so no additional
+    // op is necessary. Currently this can convert it to a clear op or a rrect op. Only valid if
+    // a constColor is provided.
+    kSubmitted,
+    // The clip was folded into the device quad, with updated edge flags and local coords, and
+    // caller is responsible for adding an appropriate op.
+    kClipApplied,
+    // No change to clip, but quad updated to better fit clip/render target, and caller is
+    // responsible for adding an appropriate op.
+    kCropped
+};
 
-bool GrRenderTargetContext::drawFilledRectAsClear(const GrClip& clip, GrPaint&& paint, GrAA aa,
-                                                  const SkMatrix& viewMatrix, const SkRect& rect) {
-    // Rules for a filled rect to become a clear [+scissor]:
-    // 1. The paint is a constant blend color with no other FPs
-    // 2. The view matrix maps rectangles to rectangles, or the transformed quad fully covers
-    //    the render target (or clear region in #3).
-    // 3. The clip is an intersection of rectangles, so the clear region will be the
-    //    intersection of the clip and the provided rect.
-    // 4. The clear region aligns with pixel bounds
-    // 5. There are no user stencil settings (and since the clip was IOR, the clip won't need
-    //    to use the stencil either).
-    // If all conditions pass, the filled rect can either be a fullscreen clear (if it's big
-    // enough), or the rectangle geometry will be used as the scissor clip on the clear.
-    // If everything passes but rule #4, this submits a simplified fill rect op instead so that the
-    // rounding differences between clip and draws don't fight each other.
-    // NOTE: we route draws into clear() regardless of performColorClearsAsDraws() since the
-    // clear call is allowed to reset the oplist even when it also happens to use a GrFillRectOp.
+GrRenderTargetContext::QuadOptimization GrRenderTargetContext::attemptQuadOptimization(
+        const GrClip& clip, const SkPMColor4f* constColor, bool allowAAChange,
+        GrAA* aa, GrQuadAAFlags* edgeFlags, GrQuad* deviceQuad, GrQuad* localQuad) {
+    // Optimization requirements:
+    // 1. kDiscard applies when clip bounds and quad bounds do not intersect
+    // 2. kClear applies when constColor and final geom is pixel aligned rect;
+    //       pixel aligned rect requires rect clip and (rect quad or quad covers clip)
+    // 3. kRRect applies when constColor and rrect clip and quad covers clip
+    // 4. kExplicitClip applies when rect clip and (rect quad or quad covers clip)
+    // 5. kCropped applies when rect quad (currently)
+    // 6. kNone always applies
+    GrQuadAAFlags newFlags = *edgeFlags;
 
-    SkPMColor4f clearColor;
-    if (paint.numCoverageFragmentProcessors() > 0 || !paint.isConstantBlendedColor(&clearColor)) {
-        return false;
+    // Must use worst case bounds so that stencil buffer updates on approximately sized
+    // render targets don't get corrupted.
+    const SkRect rtRect = SkRect::MakeWH(fRenderTargetProxy->worstCaseWidth(),
+                                         fRenderTargetProxy->worstCaseHeight());
+    SkRect drawBounds = deviceQuad->bounds();
+    if (constColor) {
+        // Don't bother updating local coordinates when the paint will ignore them anyways
+        localQuad = nullptr;
     }
 
-    const SkRect rtRect = fRenderTargetProxy->getBoundsRect();
-    // Will be the intersection of render target, clip, and quad
-    SkRect combinedRect = rtRect;
+    // If the quad is entirely off screen, it doesn't matter what the clip does
+    if (!rtRect.intersects(drawBounds)) {
+        return QuadOptimization::kDiscarded;
+    }
 
-    SkRRect clipRRect;
-    GrAA clipAA;
+    // Check if clip can be represented as a rounded rect (initialize as if clip fully contained
+    // the render target).
+    SkRRect clipRRect = SkRRect::MakeRect(rtRect);
+    GrAA clipAA = allowAAChange ? GrAA::kNo : *aa;
+    bool axisAlignedClip = true;
     if (!clip.quickContains(rtRect)) {
-        // If the clip is an rrect with no rounding, then it can replace the full RT bounds as the
-        // limiting region, although we will have to worry about AA. If the clip is anything
-        // more complicated, just punt to the regular fill rect op.
-        if (!clip.isRRect(rtRect, &clipRRect, &clipAA) || !clipRRect.isRect()) {
-            return false;
+        if (!clip.isRRect(rtRect, &clipRRect, &clipAA)) {
+            axisAlignedClip = false;
         }
-
-        combinedRect = clipRRect.rect();
-    } else {
-        // The clip is outside the render target, so the clip can be ignored
-        clipAA = GrAA::kNo;
     }
 
-    GrQuadAAFlags edgeFlags; // To account for clip and draw mixing AA modes
-    if (viewMatrix.rectStaysRect()) {
-        // Skip the extra overhead of inverting the view matrix to see if rtRect is contained in the
-        // drawn rectangle, and instead just intersect rtRect with the transformed rect. It will be
-        // the new clear region.
-        SkRect drawRect = viewMatrix.mapRect(rect);
-        if (!combinedRect.intersect(drawRect)) {
-            // No intersection means nothing should be drawn, so return true but don't add an op
-            return true;
+    // If the clip rrect is valid (i.e. axis-aligned), we can potentially combine it with the
+    // draw geometry so that no clip is needed when drawing.
+    if (axisAlignedClip && (allowAAChange || clipAA == *aa)) {
+        // Tighten clip bounds (if clipRRect.isRect() is true, clipBounds now holds the intersection
+        // of the render target and the clip rect)
+        SkRect clipBounds = rtRect;
+        if (!clipBounds.intersect(clipRRect.rect()) || !clipBounds.intersects(drawBounds)) {
+            return QuadOptimization::kDiscarded;
         }
 
-        // In this case, edge flags start based on draw's AA and then switch per-edge to the clip's
-        // AA setting if that edge was inset.
-        edgeFlags = aa == GrAA::kNo ? GrQuadAAFlags::kNone : GrQuadAAFlags::kAll;
-        if (combinedRect.fLeft > drawRect.fLeft) {
-            edgeFlags = set_edge_flag(edgeFlags, GrQuadAAFlags::kLeft, clipAA);
+        if (clipRRect.isRect()) {
+            // No rounded corners, so the kClear and kExplicitClip optimizations are possible
+            if (GrQuadUtils::CropToRect(clipBounds, clipAA, &newFlags, deviceQuad, localQuad)) {
+                if (constColor && deviceQuad->quadType() == GrQuad::Type::kAxisAligned) {
+                    // Clear optimization is possible
+                    drawBounds = deviceQuad->bounds();
+                    if (drawBounds.contains(rtRect)) {
+                        // Fullscreen clear
+                        this->clear(nullptr, *constColor, CanClearFullscreen::kYes);
+                        return QuadOptimization::kSubmitted;
+                    } else if (GrClip::IsPixelAligned(drawBounds) &&
+                               drawBounds.width() > 256 && drawBounds.height() > 256) {
+                        // Scissor + clear (round shouldn't do anything since we are pixel aligned)
+                        SkIRect scissorRect;
+                        drawBounds.round(&scissorRect);
+                        this->clear(&scissorRect, *constColor, CanClearFullscreen::kNo);
+                        return QuadOptimization::kSubmitted;
+                    }
+                }
+
+                // Update overall AA setting.
+                *edgeFlags = newFlags;
+                if (*aa == GrAA::kNo && clipAA == GrAA::kYes &&
+                    newFlags != GrQuadAAFlags::kNone) {
+                    // The clip was anti-aliased and now the draw needs to be upgraded to AA to
+                    // properly reflect the smooth edge of the clip.
+                    *aa = GrAA::kYes;
+                }
+                // We intentionally do not downgrade AA here because we don't know if we need to
+                // preserve MSAA (see GrQuadAAFlags docs). But later in the pipeline, the ops can
+                // use GrResolveAATypeForQuad() to turn off coverage AA when all flags are off.
+
+                // deviceQuad is exactly the intersection of original quad and clip, so it can be
+                // drawn with no clip (submitted by caller)
+                return QuadOptimization::kClipApplied;
+            } else {
+                // The quads have been updated to better fit the clip bounds, but can't get rid of
+                // the clip entirely
+                return QuadOptimization::kCropped;
+            }
+        } else if (constColor) {
+            // Rounded corners and constant filled color (limit ourselves to solid colors because
+            // there is no way to use custom local coordinates with drawRRect).
+            if (GrQuadUtils::CropToRect(clipBounds, clipAA, &newFlags, deviceQuad, localQuad) &&
+                deviceQuad->quadType() == GrQuad::Type::kAxisAligned &&
+                deviceQuad->bounds().contains(clipBounds)) {
+                // Since the cropped quad became a rectangle which covered the bounds of the rrect,
+                // we can draw the rrect directly and ignore the edge flags
+                GrPaint paint;
+                clear_to_grpaint(*constColor, &paint);
+                this->drawRRect(GrFixedClip::Disabled(), std::move(paint), clipAA, SkMatrix::I(),
+                                clipRRect, GrStyle::SimpleFill());
+                return QuadOptimization::kSubmitted;
+            } else {
+                // The quad has been updated to better fit clip bounds, but can't remove the clip
+                return QuadOptimization::kCropped;
+            }
         }
-        if (combinedRect.fTop > drawRect.fTop) {
-            edgeFlags = set_edge_flag(edgeFlags, GrQuadAAFlags::kTop, clipAA);
-        }
-        if (combinedRect.fRight < drawRect.fRight) {
-            edgeFlags = set_edge_flag(edgeFlags, GrQuadAAFlags::kRight, clipAA);
-        }
-        if (combinedRect.fBottom < drawRect.fBottom) {
-            edgeFlags = set_edge_flag(edgeFlags, GrQuadAAFlags::kBottom, clipAA);
-        }
-    } else {
-        // If the transformed rectangle does not contain the combined rt and clip, the draw is too
-        // complex to be implemented as a clear
-        SkMatrix invM;
-        if (!viewMatrix.invert(&invM)) {
-            return false;
-        }
-        // The clip region in the rect's local space, so the test becomes the local rect containing
-        // the quad's points. If clip is non-AA, test rounded out region to avoid the scenario where
-        // the draw contains the unrounded non-aa clip, but does not contain the rounded version. Be
-        // conservative since we don't know how the GPU would round.
-        SkRect conservative;
-        if (clipAA == GrAA::kNo) {
-            conservative = SkRect::Make(combinedRect.roundOut());
-        } else {
-            conservative = combinedRect;
-        }
-        GrQuad quad = GrQuad::MakeFromRect(conservative, invM);
-        if (!rect_contains_inclusive(rect, quad.point(0)) ||
-            !rect_contains_inclusive(rect, quad.point(1)) ||
-            !rect_contains_inclusive(rect, quad.point(2)) ||
-            !rect_contains_inclusive(rect, quad.point(3))) {
-            // No containment, so combinedRect can't be filled by a solid color
-            return false;
-        }
-        // combinedRect can be filled by a solid color but doesn't need to be modified since it's
-        // inside the quad to be drawn, which also means the edge AA flags respect the clip AA
-        edgeFlags = clipAA == GrAA::kNo ? GrQuadAAFlags::kNone : GrQuadAAFlags::kAll;
     }
 
-    // Almost every condition is met; now it requires that the combined rect align with pixel
-    // boundaries in order for it to become a scissor-clear. Ignore the AA status in this case
-    // since non-AA with partial-pixel coordinates can be rounded differently on the GPU,
-    // leading to unexpected differences between a scissor test and a rasterized quad.
-    // Also skip very small rectangles since the scissor+clear doesn't by us much then.
-    if (combinedRect.contains(rtRect)) {
-        // Full screen clear
-        this->clear(nullptr, clearColor, CanClearFullscreen::kYes);
-        return true;
-    } else if (GrClip::IsPixelAligned(combinedRect) &&
-               combinedRect.width() > 256 && combinedRect.height() > 256) {
-        // Scissor + clear (round shouldn't do anything since we are pixel aligned)
-        SkIRect scissorRect;
-        combinedRect.round(&scissorRect);
-        this->clear(&scissorRect, clearColor, CanClearFullscreen::kNo);
-        return true;
+    // Crop the quad to the conservative bounds of the clip.
+    SkIRect clipDevBounds;
+    clip.getConservativeBounds(rtRect.width(), rtRect.height(), &clipDevBounds);
+    SkRect clipBounds = SkRect::Make(clipDevBounds);
+
+    // One final check for discarding, since we may have gone here directly due to a complex clip
+    if (!clipBounds.intersects(drawBounds)) {
+        return QuadOptimization::kDiscarded;
     }
 
-    // If we got here, we can't use a scissor + clear, but combinedRect represents the correct
-    // geometry combination of quad + clip so we can perform a simplified fill rect op. We do this
-    // mostly to avoid mismatches in rounding logic on the CPU vs. the GPU, which frequently appears
-    // when drawing and clipping something to the same non-AA rect that never-the-less has
-    // non-integer coordinates.
-    aa = edgeFlags == GrQuadAAFlags::kNone ? GrAA::kNo : GrAA::kYes;
-    GrAAType aaType = this->chooseAAType(aa);
-    this->addDrawOp(GrFixedClip::Disabled(),
-                    GrFillRectOp::Make(fContext, std::move(paint), aaType, edgeFlags,
-                                       GrQuad(combinedRect), GrQuad(combinedRect)));
-    return true;
+    // Even if this were to return true, the crop rect does not exactly match the clip, so can not
+    // report explicit-clip. Since these edges aren't visible, don't update the final edge flags.
+    GrQuadUtils::CropToRect(clipBounds, clipAA, &newFlags, deviceQuad, localQuad);
+
+    return QuadOptimization::kCropped;
 }
 
-void GrRenderTargetContext::drawFilledRect(const GrClip& clip,
+void GrRenderTargetContext::drawFilledQuad(const GrClip& clip,
                                            GrPaint&& paint,
                                            GrAA aa,
-                                           const SkMatrix& viewMatrix,
-                                           const SkRect& rect,
+                                           GrQuadAAFlags edgeFlags,
+                                           const GrQuad& deviceQuad,
+                                           const GrQuad& localQuad,
                                            const GrUserStencilSettings* ss) {
+    ASSERT_SINGLE_OWNER
+    RETURN_IF_ABANDONED
+    SkDEBUGCODE(this->validate();)
+    GR_CREATE_TRACE_MARKER_CONTEXT("GrRenderTargetContext", "drawFilledQuad", fContext);
 
-    if (!ss) {
-        if (this->drawFilledRectAsClear(clip, std::move(paint), aa, viewMatrix, rect)) {
-            return;
-        }
-        // Fall through to fill rect op
-        assert_alive(paint);
+    AutoCheckFlush acf(this->drawingManager());
+
+    SkPMColor4f* constColor = nullptr;
+    SkPMColor4f paintColor;
+    if (!ss && !paint.numCoverageFragmentProcessors() &&
+        paint.isConstantBlendedColor(&paintColor)) {
+        // Only consider clears/rrects when it's easy to guarantee 100% fill with single color
+        constColor = &paintColor;
     }
 
-    SkRect croppedRect = rect;
-    if (!crop_filled_rect(this->width(), this->height(), clip, viewMatrix, &croppedRect)) {
-        // The rectangle would not be drawn, so no need to add a draw op to the list
-        return;
+    GrQuad croppedDeviceQuad = deviceQuad;
+    GrQuad croppedLocalQuad = localQuad;
+    // Only allow AA settings to change if there are no stencil settings. If they were allowed to
+    // change at this stage, it would bypass higher-level decisions about stencil MSAA.
+    QuadOptimization opt = this->attemptQuadOptimization(clip, constColor, /* allowAAChange */ !ss,
+                                                         &aa, &edgeFlags, &croppedDeviceQuad,
+                                                         &croppedLocalQuad);
+    if (opt >= QuadOptimization::kClipApplied) {
+        // These optimizations require caller to add an op themselves
+        const GrClip& finalClip = opt == QuadOptimization::kClipApplied ? GrFixedClip::Disabled()
+                                                                        : clip;
+        GrAAType aaType = ss ? (aa == GrAA::kYes ? GrAAType::kMSAA : GrAAType::kNone)
+                             : this->chooseAAType(aa);
+        this->addDrawOp(finalClip, GrFillRectOp::Make(fContext, std::move(paint), aaType, edgeFlags,
+                                                      croppedDeviceQuad, croppedLocalQuad, ss));
     }
-
-    GrAAType aaType = this->chooseAAType(aa);
-    GrQuadAAFlags edgeFlags = aa == GrAA::kYes ? GrQuadAAFlags::kAll : GrQuadAAFlags::kNone;
-    this->addDrawOp(clip,
-                    GrFillRectOp::Make(fContext, std::move(paint), aaType, edgeFlags,
-                                       GrQuad::MakeFromRect(croppedRect, viewMatrix),
-                                       GrQuad(croppedRect), ss));
+    // All other optimization levels were completely handled inside attempt(), so no extra op needed
 }
 
 void GrRenderTargetContext::drawRect(const GrClip& clip,
@@ -750,7 +724,8 @@
 
     const SkStrokeRec& stroke = style->strokeRec();
     if (stroke.getStyle() == SkStrokeRec::kFill_Style) {
-        this->drawFilledRect(clip, std::move(paint), aa, viewMatrix, rect);
+        // Fills the rect, using rect as its own local coordinates
+        this->fillRectToRect(clip, std::move(paint), aa, viewMatrix, rect, rect);
         return;
     } else if (stroke.getStyle() == SkStrokeRec::kStroke_Style ||
                stroke.getStyle() == SkStrokeRec::kHairline_Style) {
@@ -892,98 +867,6 @@
     fRenderTargetContext->getRTOpList()->addOp(std::move(op), *fRenderTargetContext->caps());
 }
 
-void GrRenderTargetContextPriv::stencilRect(const GrClip& clip,
-                                            const GrUserStencilSettings* ss,
-                                            GrPaint&& paint,
-                                            GrAA doStencilMSAA,
-                                            const SkMatrix& viewMatrix,
-                                            const SkRect& rect,
-                                            const SkMatrix* localMatrix) {
-    ASSERT_SINGLE_OWNER_PRIV
-    RETURN_IF_ABANDONED_PRIV
-    SkDEBUGCODE(fRenderTargetContext->validate();)
-    GR_CREATE_TRACE_MARKER_CONTEXT("GrRenderTargetContextPriv", "stencilRect",
-                                   fRenderTargetContext->fContext);
-
-    AutoCheckFlush acf(fRenderTargetContext->drawingManager());
-
-    auto aaType = (GrAA::kYes == doStencilMSAA) ? GrAAType::kMSAA : GrAAType::kNone;
-
-    GrQuad localQuad = localMatrix ? GrQuad::MakeFromRect(rect, *localMatrix)
-                                   : GrQuad(rect);
-    std::unique_ptr<GrDrawOp> op = GrFillRectOp::Make(
-            fRenderTargetContext->fContext, std::move(paint), aaType, GrQuadAAFlags::kNone,
-            GrQuad::MakeFromRect(rect, viewMatrix), localQuad, ss);
-    fRenderTargetContext->addDrawOp(clip, std::move(op));
-}
-
-void GrRenderTargetContext::fillRectWithEdgeAA(const GrClip& clip, GrPaint&& paint, GrAA aa,
-                                               GrQuadAAFlags edgeAA, const SkMatrix& viewMatrix,
-                                               const SkRect& rect, const SkRect* localRect) {
-    ASSERT_SINGLE_OWNER
-    RETURN_IF_ABANDONED
-    SkDEBUGCODE(this->validate();)
-    GR_CREATE_TRACE_MARKER_CONTEXT("GrRenderTargetContext", "fillRectWithEdgeAA", fContext);
-
-    GrAAType aaType = this->chooseAAType(aa);
-    std::unique_ptr<GrDrawOp> op;
-
-    if (localRect) {
-        // If local coordinates are provided, skip the optimization check to go through
-        // drawFilledRect, and also calculate clipped local coordinates
-        SkRect croppedRect = rect;
-        SkRect croppedLocalRect = *localRect;
-        if (!crop_filled_rect(this->width(), this->height(), clip, viewMatrix, &croppedRect,
-                              &croppedLocalRect)) {
-            return;
-        }
-        op = GrFillRectOp::Make(fContext, std::move(paint), aaType, edgeAA,
-                                GrQuad::MakeFromRect(croppedRect, viewMatrix),
-                                GrQuad(croppedLocalRect));
-    } else {
-        // If aaType turns into MSAA, make sure to keep quads with no AA edges as MSAA. Sending
-        // those to drawFilledRect() would have it turn off MSAA in that case, which breaks seaming
-        // with any partial AA edges that kept MSAA.
-        if (aaType != GrAAType::kMSAA &&
-            (edgeAA == GrQuadAAFlags::kNone || edgeAA == GrQuadAAFlags::kAll)) {
-            // This is equivalent to a regular filled rect draw, so route through there to take
-            // advantage of draw->clear optimizations
-            this->drawFilledRect(clip, std::move(paint), GrAA(edgeAA == GrQuadAAFlags::kAll),
-                                 viewMatrix, rect);
-            return;
-        }
-
-        SkRect croppedRect = rect;
-        if (!crop_filled_rect(this->width(), this->height(), clip, viewMatrix, &croppedRect)) {
-            return;
-        }
-        op = GrFillRectOp::Make(fContext, std::move(paint), aaType, edgeAA,
-                                GrQuad::MakeFromRect(croppedRect, viewMatrix),
-                                GrQuad(croppedRect));
-    }
-
-    AutoCheckFlush acf(this->drawingManager());
-    this->addDrawOp(clip, std::move(op));
-}
-
-void GrRenderTargetContext::fillQuadWithEdgeAA(const GrClip& clip, GrPaint&& paint, GrAA aa,
-                                               GrQuadAAFlags edgeAA, const SkMatrix& viewMatrix,
-                                               const SkPoint quad[4], const SkPoint localQuad[4]) {
-    ASSERT_SINGLE_OWNER
-    RETURN_IF_ABANDONED
-    SkDEBUGCODE(this->validate();)
-    GR_CREATE_TRACE_MARKER_CONTEXT("GrRenderTargetContext", "fillQuadWithEdgeAA", fContext);
-
-    GrAAType aaType = this->chooseAAType(aa);
-
-    AutoCheckFlush acf(this->drawingManager());
-    const SkPoint* localPoints = localQuad ? localQuad : quad;
-    this->addDrawOp(clip,
-                    GrFillRectOp::Make(fContext, std::move(paint), aaType, edgeAA,
-                                       GrQuad::MakeFromSkQuad(quad, viewMatrix),
-                                       GrQuad::MakeFromSkQuad(localPoints, SkMatrix::I())));
-}
-
 // Creates a paint for GrFillRectOp that matches behavior of GrTextureOp
 static void draw_texture_to_grpaint(sk_sp<GrTextureProxy> proxy, const SkRect* domain,
                                     GrSamplerState::Filter filter, SkBlendMode mode,
@@ -1147,32 +1030,6 @@
     }
 }
 
-void GrRenderTargetContext::fillRectWithLocalMatrix(const GrClip& clip,
-                                                    GrPaint&& paint,
-                                                    GrAA aa,
-                                                    const SkMatrix& viewMatrix,
-                                                    const SkRect& rectToDraw,
-                                                    const SkMatrix& localMatrix) {
-    ASSERT_SINGLE_OWNER
-    RETURN_IF_ABANDONED
-    SkDEBUGCODE(this->validate();)
-    GR_CREATE_TRACE_MARKER_CONTEXT("GrRenderTargetContext", "fillRectWithLocalMatrix", fContext);
-
-    SkRect croppedRect = rectToDraw;
-    if (!crop_filled_rect(this->width(), this->height(), clip, viewMatrix, &croppedRect)) {
-        return;
-    }
-
-    AutoCheckFlush acf(this->drawingManager());
-
-    GrAAType aaType = this->chooseAAType(aa);
-    GrQuadAAFlags edgeFlags = aa == GrAA::kYes ? GrQuadAAFlags::kAll : GrQuadAAFlags::kNone;
-    this->addDrawOp(clip,
-                    GrFillRectOp::Make(fContext, std::move(paint), aaType, edgeFlags,
-                                       GrQuad::MakeFromRect(croppedRect, viewMatrix),
-                                       GrQuad::MakeFromRect(croppedRect, localMatrix)));
-}
-
 void GrRenderTargetContext::drawVertices(const GrClip& clip,
                                          GrPaint&& paint,
                                          const SkMatrix& viewMatrix,
@@ -1918,8 +1775,8 @@
             GrPaint paint;
             paint.addColorFragmentProcessor(std::move(fp));
             paint.setPorterDuffXPFactory(SkBlendMode::kSrc);
-            currRTC->drawFilledRect(GrNoClip(), std::move(paint), GrAA::kNo, SkMatrix::I(),
-                                    dstRect);
+            currRTC->fillRectToRect(GrNoClip(), std::move(paint), GrAA::kNo, SkMatrix::I(),
+                                    dstRect, dstRect);
         } else {
             auto filter = rescaleQuality == kNone_SkFilterQuality ? GrSamplerState::Filter::kNearest
                                                                   : GrSamplerState::Filter::kBilerp;
@@ -2274,6 +2131,9 @@
 
     auto texMatrix = SkMatrix::MakeTrans(x, y);
 
+    SkRect dstRectY = SkRect::MakeWH(dstW, dstH);
+    SkRect dstRectUV = SkRect::MakeWH(dstW / 2, dstH / 2);
+
     // This matrix generates (r,g,b,a) = (0, 0, 0, y)
     float yM[20];
     std::fill_n(yM, 15, 0.f);
@@ -2283,8 +2143,8 @@
     auto yFP = GrColorMatrixFragmentProcessor::Make(yM, false, true, false);
     yPaint.addColorFragmentProcessor(std::move(yFP));
     yPaint.setPorterDuffXPFactory(SkBlendMode::kSrc);
-    yRTC->drawFilledRect(GrNoClip(), std::move(yPaint), GrAA::kNo, SkMatrix::I(),
-                         SkRect::MakeWH(dstW, dstH));
+    yRTC->fillRectToRect(GrNoClip(), std::move(yPaint), GrAA::kNo, SkMatrix::I(),
+                         dstRectY, dstRectY);
     auto yTransfer = yRTC->transferPixels(GrColorType::kAlpha_8,
                                           SkIRect::MakeWH(yRTC->width(), yRTC->height()));
     if (!yTransfer.fTransferBuffer) {
@@ -2302,8 +2162,8 @@
     auto uFP = GrColorMatrixFragmentProcessor::Make(uM, false, true, false);
     uPaint.addColorFragmentProcessor(std::move(uFP));
     uPaint.setPorterDuffXPFactory(SkBlendMode::kSrc);
-    uRTC->drawFilledRect(GrNoClip(), std::move(uPaint), GrAA::kNo, SkMatrix::I(),
-                         SkRect::MakeWH(dstW / 2, dstH / 2));
+    uRTC->fillRectToRect(GrNoClip(), std::move(uPaint), GrAA::kNo, SkMatrix::I(),
+                         dstRectUV, dstRectUV);
     auto uTransfer = uRTC->transferPixels(GrColorType::kAlpha_8,
                                           SkIRect::MakeWH(uRTC->width(), uRTC->height()));
     if (!uTransfer.fTransferBuffer) {
@@ -2320,8 +2180,8 @@
     auto vFP = GrColorMatrixFragmentProcessor::Make(vM, false, true, false);
     vPaint.addColorFragmentProcessor(std::move(vFP));
     vPaint.setPorterDuffXPFactory(SkBlendMode::kSrc);
-    vRTC->drawFilledRect(GrNoClip(), std::move(vPaint), GrAA::kNo, SkMatrix::I(),
-                         SkRect::MakeWH(dstW / 2, dstH / 2));
+    vRTC->fillRectToRect(GrNoClip(), std::move(vPaint), GrAA::kNo, SkMatrix::I(),
+                         dstRectUV, dstRectUV);
     auto vTransfer = vRTC->transferPixels(GrColorType::kAlpha_8,
                                           SkIRect::MakeWH(vRTC->width(), vRTC->height()));
     if (!vTransfer.fTransferBuffer) {
diff --git a/src/gpu/GrRenderTargetContext.h b/src/gpu/GrRenderTargetContext.h
index fd72e52..cc46660 100644
--- a/src/gpu/GrRenderTargetContext.h
+++ b/src/gpu/GrRenderTargetContext.h
@@ -18,6 +18,7 @@
 #include "src/gpu/GrRenderTargetProxy.h"
 #include "src/gpu/GrSurfaceContext.h"
 #include "src/gpu/GrXferProcessor.h"
+#include "src/gpu/geometry/GrQuad.h"
 #include "src/gpu/text/GrTextTarget.h"
 
 class GrBackendSemaphore;
@@ -116,9 +117,9 @@
                         const SkMatrix& viewMatrix,
                         const SkRect& rectToDraw,
                         const SkRect& localRect) {
-        this->fillRectWithEdgeAA(clip, std::move(paint), aa,
-                                 aa == GrAA::kYes ? GrQuadAAFlags::kAll : GrQuadAAFlags::kNone,
-                                 viewMatrix, rectToDraw, &localRect);
+        this->drawFilledQuad(clip, std::move(paint), aa,
+                             aa == GrAA::kYes ? GrQuadAAFlags::kAll : GrQuadAAFlags::kNone,
+                             GrQuad::MakeFromRect(rectToDraw, viewMatrix), GrQuad(localRect));
     }
 
     /**
@@ -126,10 +127,15 @@
      */
     void fillRectWithLocalMatrix(const GrClip& clip,
                                  GrPaint&& paint,
-                                 GrAA,
+                                 GrAA aa,
                                  const SkMatrix& viewMatrix,
                                  const SkRect& rect,
-                                 const SkMatrix& localMatrix);
+                                 const SkMatrix& localMatrix) {
+        this->drawFilledQuad(clip, std::move(paint), aa,
+                             aa == GrAA::kYes ? GrQuadAAFlags::kAll : GrQuadAAFlags::kNone,
+                             GrQuad::MakeFromRect(rect, viewMatrix),
+                             GrQuad::MakeFromRect(rect, localMatrix));
+    }
 
     /**
      * Creates an op that draws a fill rect with per-edge control over anti-aliasing.
@@ -139,7 +145,11 @@
      */
     void fillRectWithEdgeAA(const GrClip& clip, GrPaint&& paint, GrAA aa, GrQuadAAFlags edgeAA,
                             const SkMatrix& viewMatrix, const SkRect& rect,
-                            const SkRect* optionalLocalRect = nullptr);
+                            const SkRect* optionalLocalRect = nullptr) {
+        const SkRect& localRect = optionalLocalRect ? *optionalLocalRect : rect;
+        this->drawFilledQuad(clip, std::move(paint), aa, edgeAA,
+                             GrQuad::MakeFromRect(rect, viewMatrix), GrQuad(localRect));
+    }
 
     /**
      * Similar to fillRectWithEdgeAA but draws an arbitrary 2D convex quadrilateral transformed
@@ -155,7 +165,12 @@
      */
     void fillQuadWithEdgeAA(const GrClip& clip, GrPaint&& paint, GrAA aa, GrQuadAAFlags edgeAA,
                             const SkMatrix& viewMatrix, const SkPoint quad[4],
-                            const SkPoint optionalLocalQuad[4]);
+                            const SkPoint optionalLocalQuad[4]) {
+        const SkPoint* localQuad = optionalLocalQuad ? optionalLocalQuad : quad;
+        this->drawFilledQuad(clip, std::move(paint), aa, edgeAA,
+                             GrQuad::MakeFromSkQuad(quad, viewMatrix),
+                             GrQuad::MakeFromSkQuad(localQuad, SkMatrix::I()));
+    }
 
     /** Used with drawQuadSet */
     struct QuadSetEntry {
@@ -487,6 +502,7 @@
 
 private:
     class TextTarget;
+    enum class QuadOptimization;
 
     GrAAType chooseAAType(GrAA);
 
@@ -525,19 +541,34 @@
                           const SkRRect& origOuter,
                           const SkRRect& origInner);
 
-    void drawFilledRect(const GrClip& clip,
-                        GrPaint&& paint,
-                        GrAA,
-                        const SkMatrix& viewMatrix,
-                        const SkRect& rect,
-                        const GrUserStencilSettings* ss = nullptr);
+    // If the drawn quad's paint is a const blended color, provide it as a non-null pointer to
+    // 'constColor', which enables the draw-as-clear optimization. Otherwise it is assumed the paint
+    // requires some form of shading that invalidates using a clear op.
+    //
+    // The non-const pointers should be the original draw request on input, and will be updated as
+    // appropriate depending on the returned optimization level.
+    //
+    // If 'allowAAChange' is true, 'aa' and 'edgeFlags' may be updated to incorporate the AA of the
+    // clip. When it is false, only optimizations that will not change the AA state will be applied.
+    QuadOptimization attemptQuadOptimization(const GrClip& clip,
+                                             const SkPMColor4f* constColor,
+                                             bool allowAAChange,
+                                             GrAA* aa,
+                                             GrQuadAAFlags* edgeFlags,
+                                             GrQuad* deviceQuad,
+                                             GrQuad* localQuad);
 
-    // Only consumes the GrPaint if successful.
-    bool drawFilledRectAsClear(const GrClip& clip,
-                               GrPaint&& paint,
-                               GrAA aa,
-                               const SkMatrix& viewMatrix,
-                               const SkRect& rect);
+    // If stencil settings, 'ss', are non-null, AA controls MSAA or no AA. If they are null, then AA
+    // can choose between coverage, MSAA as per chooseAAType(). This will always attempt to apply
+    // quad optimizations, so all quad/rect public APIs should rely on this function for consistent
+    // clipping behavior.
+    void drawFilledQuad(const GrClip& clip,
+                        GrPaint&& paint,
+                        GrAA aa,
+                        GrQuadAAFlags edgeFlags,
+                        const GrQuad& deviceQuad,
+                        const GrQuad& localQuad,
+                        const GrUserStencilSettings* ss = nullptr);
 
     void drawShapeUsingPathRenderer(const GrClip&, GrPaint&&, GrAA, const SkMatrix&,
                                     const GrShape&);
diff --git a/src/gpu/GrRenderTargetContextPriv.h b/src/gpu/GrRenderTargetContextPriv.h
index fa1d9fd..ef60655 100644
--- a/src/gpu/GrRenderTargetContextPriv.h
+++ b/src/gpu/GrRenderTargetContextPriv.h
@@ -66,8 +66,16 @@
     // care to only provide hard clips or we could get stuck in a loop. The general clip is needed
     // so that path renderers can use this function.
     void stencilRect(
-            const GrClip&, const GrUserStencilSettings* ss, GrPaint&& paint, GrAA doStencilMSAA,
-            const SkMatrix& viewMatrix, const SkRect& rect, const SkMatrix* localMatrix = nullptr);
+            const GrClip& clip, const GrUserStencilSettings* ss, GrPaint&& paint,
+            GrAA doStencilMSAA, const SkMatrix& viewMatrix, const SkRect& rect,
+            const SkMatrix* localMatrix = nullptr) {
+        // Since this provides stencil settings to drawFilledQuad, it performs a different AA type
+        // resolution compared to regular rect draws, which is the main reason it remains separate.
+        GrQuad localQuad = localMatrix ? GrQuad::MakeFromRect(rect, *localMatrix) : GrQuad(rect);
+        fRenderTargetContext->drawFilledQuad(
+                clip, std::move(paint), doStencilMSAA, GrQuadAAFlags::kNone,
+                GrQuad::MakeFromRect(rect, viewMatrix), localQuad, ss);
+    }
 
     void stencilPath(
             const GrHardClip&, GrAA doStencilMSAA, const SkMatrix& viewMatrix, const GrPath*);
@@ -84,12 +92,6 @@
                             const SkMatrix& viewMatrix,
                             const SkPath&);
 
-    void drawFilledRect(
-            const GrClip& clip, GrPaint&& paint, GrAA aa, const SkMatrix& m, const SkRect& rect,
-            const GrUserStencilSettings* ss = nullptr) {
-        fRenderTargetContext->drawFilledRect(clip, std::move(paint), aa, m, rect, ss);
-    }
-
     SkBudgeted isBudgeted() const;
 
     int maxWindowRectangles() const;
diff --git a/src/gpu/geometry/GrQuad.h b/src/gpu/geometry/GrQuad.h
index 9f4f974..9a080f3 100644
--- a/src/gpu/geometry/GrQuad.h
+++ b/src/gpu/geometry/GrQuad.h
@@ -117,6 +117,15 @@
     // True if anti-aliasing affects this quad. Only valid when quadType == kAxisAligned
     bool aaHasEffectOnRect() const;
 
+    // The non-const pointers are provided to support modifying a GrQuad in-place, but care must be
+    // taken to keep its quad type aligned with the geometric nature of the new coordinates. This is
+    // no different than using the constructors that accept a quad type.
+
+    float* xs() { return fX; }
+    float* ys() { return fY; }
+    float* ws() { return fW; }
+
+    void setQuadType(Type newType) { fType = newType; }
 private:
     template<typename T>
     friend class GrQuadListBase; // for access to fX, fY, fW
diff --git a/src/gpu/geometry/GrQuadUtils.cpp b/src/gpu/geometry/GrQuadUtils.cpp
index ba109cf..fce38d2 100644
--- a/src/gpu/geometry/GrQuadUtils.cpp
+++ b/src/gpu/geometry/GrQuadUtils.cpp
@@ -7,9 +7,158 @@
 
 #include "src/gpu/geometry/GrQuadUtils.h"
 
+#include "include/core/SkRect.h"
 #include "include/private/GrTypesPriv.h"
+#include "include/private/SkVx.h"
 #include "src/gpu/geometry/GrQuad.h"
 
+using V4f = skvx::Vec<4, float>;
+using M4f = skvx::Vec<4, int32_t>;
+
+// Since the local quad may not be type kRect, this uses the opposites for each vertex when
+// interpolating, and calculates new ws in addition to new xs, ys.
+static void interpolate_local(float alpha, int v0, int v1, int v2, int v3,
+                              float lx[4], float ly[4], float lw[4]) {
+    SkASSERT(v0 >= 0 && v0 < 4);
+    SkASSERT(v1 >= 0 && v1 < 4);
+    SkASSERT(v2 >= 0 && v2 < 4);
+    SkASSERT(v3 >= 0 && v3 < 4);
+
+    float beta = 1.f - alpha;
+    lx[v0] = alpha * lx[v0] + beta * lx[v2];
+    ly[v0] = alpha * ly[v0] + beta * ly[v2];
+    lw[v0] = alpha * lw[v0] + beta * lw[v2];
+
+    lx[v1] = alpha * lx[v1] + beta * lx[v3];
+    ly[v1] = alpha * ly[v1] + beta * ly[v3];
+    lw[v1] = alpha * lw[v1] + beta * lw[v3];
+}
+
+// Crops v0 to v1 based on the clipDevRect. v2 is opposite of v0, v3 is opposite of v1.
+// It is written to not modify coordinates if there's no intersection along the edge.
+// Ideally this would have been detected earlier and the entire draw is skipped.
+static bool crop_rect_edge(const SkRect& clipDevRect, int v0, int v1, int v2, int v3,
+                           float x[4], float y[4], float lx[4], float ly[4], float lw[4]) {
+    SkASSERT(v0 >= 0 && v0 < 4);
+    SkASSERT(v1 >= 0 && v1 < 4);
+    SkASSERT(v2 >= 0 && v2 < 4);
+    SkASSERT(v3 >= 0 && v3 < 4);
+
+    if (SkScalarNearlyEqual(x[v0], x[v1])) {
+        // A vertical edge
+        if (x[v0] < clipDevRect.fLeft && x[v2] >= clipDevRect.fLeft) {
+            // Overlapping with left edge of clipDevRect
+            if (lx) {
+                float alpha = (x[v2] - clipDevRect.fLeft) / (x[v2] - x[v0]);
+                interpolate_local(alpha, v0, v1, v2, v3, lx, ly, lw);
+            }
+            x[v0] = clipDevRect.fLeft;
+            x[v1] = clipDevRect.fLeft;
+            return true;
+        } else if (x[v0] > clipDevRect.fRight && x[v2] <= clipDevRect.fRight) {
+            // Overlapping with right edge of clipDevRect
+            if (lx) {
+                float alpha = (clipDevRect.fRight - x[v2]) / (x[v0] - x[v2]);
+                interpolate_local(alpha, v0, v1, v2, v3, lx, ly, lw);
+            }
+            x[v0] = clipDevRect.fRight;
+            x[v1] = clipDevRect.fRight;
+            return true;
+        }
+    } else {
+        // A horizontal edge
+        SkASSERT(SkScalarNearlyEqual(y[v0], y[v1]));
+        if (y[v0] < clipDevRect.fTop && y[v2] >= clipDevRect.fTop) {
+            // Overlapping with top edge of clipDevRect
+            if (lx) {
+                float alpha = (y[v2] - clipDevRect.fTop) / (y[v2] - y[v0]);
+                interpolate_local(alpha, v0, v1, v2, v3, lx, ly, lw);
+            }
+            y[v0] = clipDevRect.fTop;
+            y[v1] = clipDevRect.fTop;
+            return true;
+        } else if (y[v0] > clipDevRect.fBottom && y[v2] <= clipDevRect.fBottom) {
+            // Overlapping with bottom edge of clipDevRect
+            if (lx) {
+                float alpha = (clipDevRect.fBottom - y[v2]) / (y[v0] - y[v2]);
+                interpolate_local(alpha, v0, v1, v2, v3, lx, ly, lw);
+            }
+            y[v0] = clipDevRect.fBottom;
+            y[v1] = clipDevRect.fBottom;
+            return true;
+        }
+    }
+
+    // No overlap so don't crop it
+    return false;
+}
+
+// Updates x and y to intersect with clipDevRect, and applies clipAA policy to edgeFlags for each
+// intersected edge. lx, ly, and lw are updated appropriately and may be null to skip calculations.
+static void crop_rect(const SkRect& clipDevRect, GrAA clipAA, GrQuadAAFlags* edgeFlags,
+                      float x[4], float y[4], float lx[4], float ly[4], float lw[4]) {
+    // Filled in as if clipAA were true, will be inverted at the end if needed.
+    GrQuadAAFlags clipEdgeFlags = GrQuadAAFlags::kNone;
+
+    // However, the quad's left edge may not align with the SkRect notion of left due to 90 degree
+    // rotations or mirrors. So, this processes the logical edges of the quad and clamps it to the 4
+    // sides of clipDevRect.
+
+    // Quad's left is v0 to v1 (op. v2 and v3)
+    if (crop_rect_edge(clipDevRect, 0, 1, 2, 3, x, y, lx, ly, lw)) {
+        clipEdgeFlags |= GrQuadAAFlags::kLeft;
+    }
+    // Quad's top edge is v0 to v2 (op. v1 and v3)
+    if (crop_rect_edge(clipDevRect, 0, 2, 1, 3, x, y, lx, ly, lw)) {
+        clipEdgeFlags |= GrQuadAAFlags::kTop;
+    }
+    // Quad's right edge is v2 to v3 (op. v0 and v1)
+    if (crop_rect_edge(clipDevRect, 2, 3, 0, 1, x, y, lx, ly, lw)) {
+        clipEdgeFlags |= GrQuadAAFlags::kRight;
+    }
+    // Quad's bottom edge is v1 to v3 (op. v0 and v2)
+    if (crop_rect_edge(clipDevRect, 1, 3, 0, 2, x, y, lx, ly, lw)) {
+        clipEdgeFlags |= GrQuadAAFlags::kBottom;
+    }
+
+    if (clipAA == GrAA::kYes) {
+        // Turn on all edges that were clipped
+        *edgeFlags |= clipEdgeFlags;
+    } else {
+        // Turn off all edges that were clipped
+        *edgeFlags &= ~clipEdgeFlags;
+    }
+}
+
+// Calculates barycentric coordinates for each point in (testX, testY) in the triangle formed by
+// (x0,y0) - (x1,y1) - (x2, y2) and stores them in u, v, w.
+static void barycentric_coords(float x0, float y0, float x1, float y1, float x2, float y2,
+                               const V4f& testX, const V4f& testY,
+                               V4f* u, V4f* v, V4f* w) {
+    // Modeled after SkPathOpsQuad::pointInTriangle() but uses float instead of double, is
+    // vectorized and outputs normalized barycentric coordinates instead of inside/outside test
+    float v0x = x2 - x0;
+    float v0y = y2 - y0;
+    float v1x = x1 - x0;
+    float v1y = y1 - y0;
+    V4f v2x = testX - x0;
+    V4f v2y = testY - y0;
+
+    float dot00 = v0x * v0x + v0y * v0y;
+    float dot01 = v0x * v1x + v0y * v1y;
+    V4f   dot02 = v0x * v2x + v0y * v2y;
+    float dot11 = v1x * v1x + v1y * v1y;
+    V4f   dot12 = v1x * v2x + v1y * v2y;
+    float invDenom = sk_ieee_float_divide(1.f, dot00 * dot11 - dot01 * dot01);
+    *u = (dot11 * dot02 - dot01 * dot12) * invDenom;
+    *v = (dot00 * dot12 - dot01 * dot02) * invDenom;
+    *w = 1.f - *u - *v;
+}
+
+static M4f inside_triangle(const V4f& u, const V4f& v, const V4f& w) {
+    return ((u >= 0.f) & (u <= 1.f)) & ((v >= 0.f) & (v <= 1.f)) & ((w >= 0.f) & (w <= 1.f));
+}
+
 namespace GrQuadUtils {
 
 void ResolveAAType(GrAAType requestedAAType, GrQuadAAFlags requestedEdgeFlags, const GrQuad& quad,
@@ -45,6 +194,79 @@
             SK_ABORT("Should not use mixed sample AA with edge AA flags");
             break;
     }
-};
+}
+
+bool CropToRect(const SkRect& cropRect, GrAA cropAA, GrQuadAAFlags* edgeFlags, GrQuad* quad,
+                GrQuad* local) {
+    if (quad->quadType() == GrQuad::Type::kAxisAligned) {
+        // crop_rect keeps the rectangles as rectangles, so there's no need to modify types
+        if (local) {
+            crop_rect(cropRect, cropAA, edgeFlags, quad->xs(), quad->ys(),
+                      local->xs(), local->ys(), local->ws());
+        } else {
+            crop_rect(cropRect, cropAA, edgeFlags, quad->xs(), quad->ys(),
+                      nullptr, nullptr, nullptr);
+        }
+        return true;
+    }
+
+    if (local) {
+        // FIXME (michaelludwig) Calculate cropped local coordinates when not kAxisAligned
+        return false;
+    }
+
+    V4f devX = quad->x4f();
+    V4f devY = quad->y4f();
+    V4f devIW = quad->iw4f();
+    // Project the 3D coordinates to 2D
+    if (quad->quadType() == GrQuad::Type::kPerspective) {
+        devX *= devIW;
+        devY *= devIW;
+    }
+
+    V4f clipX = {cropRect.fLeft, cropRect.fLeft, cropRect.fRight, cropRect.fRight};
+    V4f clipY = {cropRect.fTop, cropRect.fBottom, cropRect.fTop, cropRect.fBottom};
+
+    // Calculate barycentric coordinates for the 4 rect corners in the 2 triangles that the quad
+    // is tessellated into when drawn.
+    V4f u1, v1, w1;
+    barycentric_coords(devX[0], devY[0], devX[1], devY[1], devX[2], devY[2], clipX, clipY,
+                       &u1, &v1, &w1);
+    V4f u2, v2, w2;
+    barycentric_coords(devX[1], devY[1], devX[3], devY[3], devX[2], devY[2], clipX, clipY,
+                       &u2, &v2, &w2);
+
+    // clipDevRect is completely inside this quad if each corner is in at least one of two triangles
+    M4f inTri1 = inside_triangle(u1, v1, w1);
+    M4f inTri2 = inside_triangle(u2, v2, w2);
+    if (all(inTri1 | inTri2)) {
+        // We can crop to exactly the clipDevRect.
+        // FIXME (michaelludwig) - there are other ways to have determined quad covering the clip
+        // rect, but the barycentric coords will be useful to derive local coordinates in the future
+
+        // Since we are cropped to exactly clipDevRect, we have discarded any perspective and the
+        // type becomes kRect. If updated locals were requested, they will incorporate perspective.
+        // FIXME (michaelludwig) - once we have local coordinates handled, it may be desirable to
+        // keep the draw as perspective so that the hardware does perspective interpolation instead
+        // of pushing it into a local coord w and having the shader do an extra divide.
+        clipX.store(quad->xs());
+        clipY.store(quad->ys());
+        quad->ws()[0] = 1.f;
+        quad->ws()[1] = 1.f;
+        quad->ws()[2] = 1.f;
+        quad->ws()[3] = 1.f;
+        quad->setQuadType(GrQuad::Type::kAxisAligned);
+
+        // Update the edge flags to match the clip setting since all 4 edges have been clipped
+        *edgeFlags = cropAA == GrAA::kYes ? GrQuadAAFlags::kAll : GrQuadAAFlags::kNone;
+
+        return true;
+    }
+
+    // FIXME (michaelludwig) - use the GrQuadPerEdgeAA tessellation inset/outset math to move
+    // edges to the closest clip corner they are outside of
+
+    return false;
+}
 
 }; // namespace GrQuadUtils
diff --git a/src/gpu/geometry/GrQuadUtils.h b/src/gpu/geometry/GrQuadUtils.h
index 29feeeb..5f6d283 100644
--- a/src/gpu/geometry/GrQuadUtils.h
+++ b/src/gpu/geometry/GrQuadUtils.h
@@ -9,8 +9,10 @@
 #define GrQuadUtils_DEFINED
 
 enum class GrQuadAAFlags;
+enum class GrAA : bool;
 enum class GrAAType : unsigned;
 class GrQuad;
+struct SkRect;
 
 namespace GrQuadUtils {
 
@@ -19,6 +21,23 @@
     void ResolveAAType(GrAAType requestedAAType, GrQuadAAFlags requestedEdgeFlags,
                        const GrQuad& quad, GrAAType* outAAtype, GrQuadAAFlags* outEdgeFlags);
 
+    /**
+     * Crops quad to the provided device-space axis-aligned rectangle. If the intersection of this
+     * quad (projected) and cropRect results in a quadrilateral, this returns true. If not, this
+     * quad may be updated to be a smaller quad of the same type such that its intersection with
+     * cropRect is visually the same.
+     *
+     * The provided edge flags are updated to reflect edges clipped by cropRect (toggling on or off
+     * based on cropAA policy). If provided, the local coordinates will be updated to reflect the
+     * updated device coordinates of this quad.
+     *
+     * 'local' may be null, in which case the new local coordinates will not be calculated. This is
+     * useful when it's known a paint does not require local coordinates. However, neither
+     * 'edgeFlags' nore 'quad' can be null.
+     */
+    bool CropToRect(const SkRect& cropRect, GrAA cropAA, GrQuadAAFlags* edgeFlags, GrQuad* quad,
+                    GrQuad* local=nullptr);
+
 }; // namespace GrQuadUtils
 
 #endif