Add general quad methods to GrRTC

Refactor compositor GM to use GrRTC directly instead of adding draw ops.
Adds a test row for using drawTextureSet now that it takes dst clips.

Bug: skia:
Change-Id: I6863ef1286cab0f0e5cf989e4aaef8ff2ca0abb8
Reviewed-on: https://skia-review.googlesource.com/c/193023
Commit-Queue: Michael Ludwig <michaelludwig@google.com>
Reviewed-by: Brian Salomon <bsalomon@google.com>
diff --git a/gm/compositor_quads.cpp b/gm/compositor_quads.cpp
index 90672e5..02fe08d 100644
--- a/gm/compositor_quads.cpp
+++ b/gm/compositor_quads.cpp
@@ -9,11 +9,10 @@
 
 #if SK_SUPPORT_GPU
 
+#include "GrClip.h"
+#include "GrRect.h"
 #include "GrRenderTargetContextPriv.h"
 
-#include "ops/GrFillRectOp.h"
-#include "ops/GrTextureOp.h"
-
 #include "Resources.h"
 #include "SkFont.h"
 #include "SkGr.h"
@@ -188,7 +187,7 @@
 
     virtual void drawBanner(SkCanvas* canvas) = 0;
 
-    void drawTiles(SkCanvas* canvas, GrContext* context, GrRenderTargetContext* rtc) {
+    virtual void drawTiles(SkCanvas* canvas, GrContext* context, GrRenderTargetContext* rtc) {
         // TODO (michaelludwig) - once the quad APIs are in SkCanvas, drop these
         // cached fields, which drawTile() needs
         fContext = context;
@@ -236,12 +235,6 @@
         return flags;
     }
 
-    // NOTE: This will go away when the quad APIs exist on the RTC and we don't have to add
-    // draw ops manually.
-    GrAAType getDefaultAAType(GrAA aa = GrAA::kYes) const {
-        return GrChooseAAType(aa, fRTC->fsaaType(), GrAllowMixedSamples::kNo, *fRTC->caps());
-    }
-
     // Recursively splits the quadrilateral against the segments stored in 'lines', which must be
     // 2 * lineCount long. Increments 'quadCount' for each split quadrilateral, and invokes the
     // drawTile at leaves.
@@ -566,16 +559,13 @@
         paint.setColor4f(c);
 
         GrQuadAAFlags aaFlags = fEnableAAOverride ? fAAOverride : this->maskToFlags(edgeAA);
-        std::unique_ptr<GrDrawOp> op;
         if (clip) {
-            op = GrFillRectOp::MakePerEdgeQuad(fContext, std::move(paint), this->getDefaultAAType(),
-                                               aaFlags, canvas->getTotalMatrix(), clip, nullptr);
+            fRTC->fillQuadWithEdgeAA(GrNoClip(), std::move(paint), GrAA::kYes, aaFlags,
+                                     canvas->getTotalMatrix(), clip, nullptr);
         } else {
-            op = GrFillRectOp::MakePerEdge(fContext, std::move(paint), this->getDefaultAAType(),
-                                           aaFlags, canvas->getTotalMatrix(), rect);
+            fRTC->fillRectWithEdgeAA(GrNoClip(), std::move(paint), GrAA::kYes, aaFlags,
+                                     canvas->getTotalMatrix(), rect);
         }
-
-        fRTC->priv().testingOnly_addDrawOp(std::move(op));
     }
 
     void drawBanner(SkCanvas* canvas) override {
@@ -623,18 +613,13 @@
         GrPaint paint;
         paint.setColor4f(fColor);
 
-        std::unique_ptr<GrDrawOp> op;
         if (clip) {
-            op = GrFillRectOp::MakePerEdgeQuad(fContext, std::move(paint), this->getDefaultAAType(),
-                                               this->maskToFlags(edgeAA), canvas->getTotalMatrix(),
-                                               clip, nullptr);
+            fRTC->fillQuadWithEdgeAA(GrNoClip(), std::move(paint), GrAA::kYes,
+                    this->maskToFlags(edgeAA), canvas->getTotalMatrix(), clip, nullptr);
         } else {
-            op = GrFillRectOp::MakePerEdge(fContext, std::move(paint), this->getDefaultAAType(),
-                                           this->maskToFlags(edgeAA), canvas->getTotalMatrix(),
-                                           rect);
+            fRTC->fillRectWithEdgeAA(GrNoClip(), std::move(paint), GrAA::kYes,
+                    this->maskToFlags(edgeAA), canvas->getTotalMatrix(), rect);
         }
-
-        fRTC->priv().testingOnly_addDrawOp(std::move(op));
     }
 
     void drawBanner(SkCanvas* canvas) override {
@@ -666,38 +651,21 @@
         SkPaintToGrPaint(fContext, fRTC->colorSpaceInfo(), fGradient, canvas->getTotalMatrix(),
                          &paint);
 
-        std::unique_ptr<GrDrawOp> op;
-        if (fLocal) {
-            SkRect localRect = SkRect::MakeWH(kTileWidth, kTileHeight);
-            if (clip) {
-                SkMatrix toLocal = SkMatrix::MakeRectToRect(rect, localRect,
-                                                            SkMatrix::kFill_ScaleToFit);
-                SkPoint localQuad[4];
-                toLocal.mapPoints(localQuad, clip, 4);
-
-                op = GrFillRectOp::MakePerEdgeQuad(
-                        fContext, std::move(paint), this->getDefaultAAType(),
-                        this->maskToFlags(edgeAA), canvas->getTotalMatrix(),
-                        clip, localQuad);
-            } else {
-                op = GrFillRectOp::MakePerEdgeWithLocalRect(
-                        fContext, std::move(paint), this->getDefaultAAType(),
-                        this->maskToFlags(edgeAA), canvas->getTotalMatrix(), rect, localRect);
-            }
-        } else {
-            if (clip) {
-                op = GrFillRectOp::MakePerEdgeQuad(
-                        fContext, std::move(paint), this->getDefaultAAType(),
-                        this->maskToFlags(edgeAA), canvas->getTotalMatrix(),
-                        clip, nullptr);
-            } else {
-                op = GrFillRectOp::MakePerEdge(
-                        fContext, std::move(paint), this->getDefaultAAType(),
-                        this->maskToFlags(edgeAA), canvas->getTotalMatrix(), rect);
-            }
+        SkRect localRect = SkRect::MakeWH(kTileWidth, kTileHeight);
+        SkPoint localQuad[4];
+        if (fLocal && clip) {
+            GrMapRectPoints(rect, localRect, clip, localQuad, 4);
         }
 
-        fRTC->priv().testingOnly_addDrawOp(std::move(op));
+        if (clip) {
+            fRTC->fillQuadWithEdgeAA(GrNoClip(), std::move(paint), GrAA::kYes,
+                    this->maskToFlags(edgeAA), canvas->getTotalMatrix(), clip,
+                    fLocal ? localQuad : nullptr);
+        } else {
+            fRTC->fillRectWithEdgeAA(GrNoClip(), std::move(paint), GrAA::kYes,
+                    this->maskToFlags(edgeAA), canvas->getTotalMatrix(), rect,
+                    fLocal ? &localRect : nullptr);
+        }
     }
 
     void drawBanner(SkCanvas* canvas) override {
@@ -727,6 +695,17 @@
     typedef TileRenderer INHERITED;
 };
 
+static SkRect get_image_local_rect(const sk_sp<SkImage> image, const SkRect& rect) {
+    // This acts like the whole image is rendered over the entire tile grid, so derive local
+    // coordinates from 'rect', based on the grid to image transform.
+    SkMatrix gridToImage = SkMatrix::MakeRectToRect(SkRect::MakeWH(kColCount * kTileWidth,
+                                                                   kRowCount * kTileHeight),
+                                                    SkRect::MakeWH(image->width(),
+                                                                   image->height()),
+                                                    SkMatrix::kFill_ScaleToFit);
+    return gridToImage.mapRect(rect);
+}
+
 class TextureRenderer : public TileRenderer {
 public:
 
@@ -736,37 +715,24 @@
 
     void drawTile(SkCanvas* canvas, const SkRect& rect, const SkPoint clip[4], const bool edgeAA[4],
                   int tileID, int quadID) override {
-        // This acts like the whole image is rendered over the entire tile grid, so derive local
-        // coordinates from 'rect', based on the grid to image transform.
-        SkMatrix gridToImage = SkMatrix::MakeRectToRect(SkRect::MakeWH(kColCount * kTileWidth,
-                                                                       kRowCount * kTileHeight),
-                                                        SkRect::MakeWH(fImage->width(),
-                                                                       fImage->height()),
-                                                        SkMatrix::kFill_ScaleToFit);
-        SkRect localRect = gridToImage.mapRect(rect);
-
         SkPMColor4f color = {1.f, 1.f, 1.f, 1.f};
+        SkRect localRect = get_image_local_rect(fImage, rect);
+
         fImage = fImage->makeTextureImage(fContext, nullptr);
         sk_sp<GrTextureProxy> proxy = as_IB(fImage)->asTextureProxyRef();
         SkASSERT(proxy);
-
-        std::unique_ptr<GrDrawOp> op;
         if (clip) {
-            SkMatrix toLocal = SkMatrix::MakeRectToRect(rect, localRect,
-                                                        SkMatrix::kFill_ScaleToFit);
             SkPoint localQuad[4];
-            toLocal.mapPoints(localQuad, clip, 4);
-
-            op = GrTextureOp::MakeQuad(fContext, std::move(proxy), GrSamplerState::Filter::kBilerp,
-                    color, localQuad, clip, this->getDefaultAAType(), this->maskToFlags(edgeAA),
-                    nullptr, canvas->getTotalMatrix(), nullptr);
+            GrMapRectPoints(rect, localRect, clip, localQuad, 4);
+            fRTC->drawTextureQuad(GrNoClip(), std::move(proxy), GrSamplerState::Filter::kBilerp,
+                    SkBlendMode::kSrcOver, color, localQuad, clip, GrAA::kYes,
+                    this->maskToFlags(edgeAA), nullptr, canvas->getTotalMatrix(), nullptr);
         } else {
-            op = GrTextureOp::Make(fContext, std::move(proxy), GrSamplerState::Filter::kBilerp,
-                    color, localRect, rect, this->getDefaultAAType(), this->maskToFlags(edgeAA),
-                    SkCanvas::kFast_SrcRectConstraint, canvas->getTotalMatrix(), nullptr);
+            fRTC->drawTexture(GrNoClip(), std::move(proxy), GrSamplerState::Filter::kBilerp,
+                    SkBlendMode::kSrcOver, color, localRect, rect, GrAA::kYes,
+                    this->maskToFlags(edgeAA), SkCanvas::kFast_SrcRectConstraint,
+                    canvas->getTotalMatrix(), nullptr);
         }
-
-        fRTC->priv().testingOnly_addDrawOp(std::move(op));
     }
 
     void drawBanner(SkCanvas* canvas) override {
@@ -782,6 +748,101 @@
     typedef TileRenderer INHERITED;
 };
 
+// Looks like TextureRenderer, but bundles tiles into drawTextureSet calls
+class TextureSetRenderer : public TileRenderer {
+public:
+
+    static sk_sp<TileRenderer> Make(sk_sp<SkImage> image) {
+        return sk_sp<TileRenderer>(new TextureSetRenderer(image));
+    }
+
+    void drawTiles(SkCanvas* canvas, GrContext* ctx, GrRenderTargetContext* rtc) override {
+        this->INHERITED::drawTiles(canvas, ctx, rtc);
+        // Push the last tile set
+        this->drawAndReset(canvas);
+    }
+
+    void drawTile(SkCanvas* canvas, const SkRect& rect, const SkPoint clip[4], const bool edgeAA[4],
+                  int tileID, int quadID) override {
+        // Submit the last batch if we've moved on to a new tile
+        if (tileID != fCurrentTileID) {
+            this->drawAndReset(canvas);
+        }
+        SkASSERT((fCurrentTileID < 0 && fDstClips.count() == 0 && fDstClipIndices.count() == 0 &&
+                  fSetEntries.count() == 0) ||
+                 (fCurrentTileID == tileID && fSetEntries.count() > 0));
+
+        // Now don't actually draw the tile, accumulate it in the growing entry set
+        fCurrentTileID = tileID;
+
+        int clipIndex = -1;
+        if (clip) {
+            // Record the four points into fDstClips and get the pointer to the first in the array
+            clipIndex = fDstClips.count();
+            fDstClips.push_back_n(4, clip);
+        }
+
+        SkRect localRect = get_image_local_rect(fImage, rect);
+
+        fImage = fImage->makeTextureImage(fContext, nullptr);
+        sk_sp<GrTextureProxy> proxy = as_IB(fImage)->asTextureProxyRef();
+        // drawTextureSet automatically derives appropriate local quad from localRect if clipPtr
+        // is not null.
+        fSetEntries.push_back({proxy, localRect, rect, nullptr, 1.f, this->maskToFlags(edgeAA)});
+        fDstClipIndices.push_back(clipIndex);
+    }
+
+    void drawBanner(SkCanvas* canvas) override {
+        draw_text(canvas, "Texture Set");
+    }
+
+private:
+    sk_sp<SkImage> fImage;
+
+    SkTArray<SkPoint> fDstClips;
+    // Since fDstClips will reallocate as needed, can't get the final pointer for the entries'
+    // fDstClip values until submitting the entire set
+    SkTArray<int> fDstClipIndices;
+    SkTArray<GrRenderTargetContext::TextureSetEntry> fSetEntries;
+    int fCurrentTileID;
+
+    TextureSetRenderer(sk_sp<SkImage> image)
+            : fImage(image)
+            , fCurrentTileID(-1) {}
+
+    void drawAndReset(SkCanvas* canvas) {
+        // Early out if there's nothing to draw
+        if (fSetEntries.count() == 0) {
+            SkASSERT(fCurrentTileID < 0 && fDstClips.count() == 0 && fDstClipIndices.count() == 0);
+            return;
+        }
+
+        // Fill in fDstClip in the entries now that fDstClips' storage won't change until after the
+        // draw is finished.
+        // NOTE: The eventual API in SkGpuDevice will make easier to collect
+        // SkCanvas::ImageSetEntries and dst clips without this extra work, but also internally maps
+        // very cleanly on to the TextureSetEntry fDstClip approach.
+        SkASSERT(fDstClipIndices.count() == fSetEntries.count());
+        for (int i = 0; i < fSetEntries.count(); ++i) {
+            if (fDstClipIndices[i] >= 0) {
+                fSetEntries[i].fDstClip = &fDstClips[fDstClipIndices[i]];
+            }
+        }
+
+        // Send to GPU
+        fRTC->drawTextureSet(GrNoClip(), fSetEntries.begin(), fSetEntries.count(),
+                             GrSamplerState::Filter::kBilerp, SkBlendMode::kSrcOver, GrAA::kYes,
+                             canvas->getTotalMatrix(), nullptr);
+        // Reset for next tile
+        fCurrentTileID = -1;
+        fDstClips.reset();
+        fDstClipIndices.reset();
+        fSetEntries.reset();
+    }
+
+    typedef TileRenderer INHERITED;
+};
+
 DEF_GM(return new CompositorGM("debug",
         DebugTileRenderer::Make(), DebugTileRenderer::MakeAA(),
         DebugTileRenderer::MakeNonAA());)
@@ -789,6 +850,7 @@
 DEF_GM(return new CompositorGM("shader",
         GradientRenderer::MakeSeamless(), GradientRenderer::MakeLocal()));
 DEF_GM(return new CompositorGM("image",
-        TextureRenderer::Make(GetResourceAsImage("images/mandrill_512.png"))));
+        TextureRenderer::Make(GetResourceAsImage("images/mandrill_512.png")),
+        TextureSetRenderer::Make(GetResourceAsImage("images/mandrill_512.png"))));
 
 #endif // SK_SUPPORT_GPU
diff --git a/src/gpu/GrRect.h b/src/gpu/GrRect.h
index 8c44ed7..aa00534 100644
--- a/src/gpu/GrRect.h
+++ b/src/gpu/GrRect.h
@@ -72,4 +72,14 @@
     SkASSERT(!b.isFinite() || (b.fLeft <= b.fRight && b.fTop <= b.fBottom));
     return a.fRight >= b.fLeft && a.fBottom >= b.fTop && b.fRight >= a.fLeft && b.fBottom >= a.fTop;
 }
+
+/**
+ * Apply the transform from 'inRect' to 'outRect' to each point in 'inPts', storing the mapped point
+ * into the parallel index of 'outPts'.
+ */
+static inline void GrMapRectPoints(const SkRect& inRect, const SkRect& outRect,
+                                   const SkPoint inPts[], SkPoint outPts[], int ptCount) {
+    SkMatrix rectTransform = SkMatrix::MakeRectToRect(inRect, outRect, SkMatrix::kFill_ScaleToFit);
+    rectTransform.mapPoints(outPts, inPts, ptCount);
+}
 #endif
diff --git a/src/gpu/GrRenderTargetContext.cpp b/src/gpu/GrRenderTargetContext.cpp
index c870ef1..910fcdb 100644
--- a/src/gpu/GrRenderTargetContext.cpp
+++ b/src/gpu/GrRenderTargetContext.cpp
@@ -928,6 +928,22 @@
     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, GrAllowMixedSamples::kNo);
+
+    AutoCheckFlush acf(this->drawingManager());
+    // MakePerEdgeQuad automatically does the right thing if localQuad is null or not
+    this->addDrawOp(clip, GrFillRectOp::MakePerEdgeQuad(fContext, std::move(paint), aaType, edgeAA,
+                                                        viewMatrix, quad, localQuad));
+}
+
 // 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,
@@ -938,7 +954,12 @@
 
     std::unique_ptr<GrFragmentProcessor> fp;
     if (domain) {
-        fp = GrTextureDomainEffect::Make(std::move(proxy), SkMatrix::I(), *domain,
+        SkRect correctedDomain = *domain;
+        if (filter == GrSamplerState::Filter::kBilerp) {
+            // Inset by 1/2 pixel, which GrTextureOp and GrTextureAdjuster handle automatically
+            correctedDomain.inset(0.5f, 0.5f);
+        }
+        fp = GrTextureDomainEffect::Make(std::move(proxy), SkMatrix::I(), correctedDomain,
                                          GrTextureDomain::kClamp_Mode, filter);
     } else {
         fp = GrSimpleTextureEffect::Make(std::move(proxy), SkMatrix::I(), filter);
@@ -982,16 +1003,9 @@
             filter = GrSamplerState::Filter::kNearest;
         }
 
-        SkRect domain = srcRect;
-        if (constraint == SkCanvas::kStrict_SrcRectConstraint &&
-            filter == GrSamplerState::Filter::kBilerp) {
-            // Inset by 1/2 pixel, which GrTextureOp and GrTextureAdjuster handle automatically
-            domain.inset(0.5f, 0.5f);
-        }
-
         GrPaint paint;
         draw_texture_to_grpaint(std::move(proxy),
-                constraint == SkCanvas::kStrict_SrcRectConstraint ? &domain : nullptr,
+                constraint == SkCanvas::kStrict_SrcRectConstraint ? &srcRect : nullptr,
                 filter, mode, color, std::move(textureColorSpaceXform), &paint);
         op = GrFillRectOp::MakePerEdgeWithLocalRect(fContext, std::move(paint), aaType, aaFlags,
                                                     viewMatrix, clippedDstRect, clippedSrcRect);
@@ -1005,6 +1019,44 @@
     this->addDrawOp(clip, std::move(op));
 }
 
+void GrRenderTargetContext::drawTextureQuad(const GrClip& clip, sk_sp<GrTextureProxy> proxy,
+                                            GrSamplerState::Filter filter, SkBlendMode mode,
+                                            const SkPMColor4f& color, const SkPoint srcQuad[4],
+                                            const SkPoint dstQuad[4], GrAA aa,
+                                            GrQuadAAFlags aaFlags, const SkRect* domain,
+                                            const SkMatrix& viewMatrix,
+                                            sk_sp<GrColorSpaceXform> texXform) {
+    ASSERT_SINGLE_OWNER
+    RETURN_IF_ABANDONED
+    SkDEBUGCODE(this->validate();)
+    GR_CREATE_TRACE_MARKER_CONTEXT("GrRenderTargetContext", "drawTextureQuad", fContext);
+    if (domain && domain->contains(proxy->getWorstCaseBoundsRect())) {
+        domain = nullptr;
+    }
+
+    GrAAType aaType = this->chooseAAType(aa, GrAllowMixedSamples::kNo);
+
+    // Unlike drawTexture(), don't bother cropping or optimizing the filter type since we're
+    // sampling an arbitrary quad of the texture.
+    AutoCheckFlush acf(this->drawingManager());
+    std::unique_ptr<GrDrawOp> op;
+    if (mode != SkBlendMode::kSrcOver) {
+        // Emulation mode, but don't bother converting to kNearest filter since it's an arbitrary
+        // quad that is being drawn, which makes the tests too expensive here
+        GrPaint paint;
+        draw_texture_to_grpaint(
+                std::move(proxy), domain, filter, mode, color, std::move(texXform), &paint);
+        op = GrFillRectOp::MakePerEdgeQuad(fContext, std::move(paint), aaType, aaFlags, viewMatrix,
+                                           dstQuad, srcQuad);
+    } else {
+        // Use lighter weight GrTextureOp
+        op = GrTextureOp::MakeQuad(fContext, std::move(proxy), filter, color, srcQuad, dstQuad,
+                                   aaType, aaFlags, domain, viewMatrix, std::move(texXform));
+    }
+
+    this->addDrawOp(clip, std::move(op));
+}
+
 void GrRenderTargetContext::drawTextureSet(const GrClip& clip, const TextureSetEntry set[], int cnt,
                                            GrSamplerState::Filter filter, SkBlendMode mode,
                                            GrAA aa, const SkMatrix& viewMatrix,
@@ -1019,9 +1071,23 @@
         // Draw one at a time with GrFillRectOp and a GrPaint that emulates what GrTextureOp does
         for (int i = 0; i < cnt; ++i) {
             float alpha = set[i].fAlpha;
-            this->drawTexture(clip, set[i].fProxy, filter, mode, {alpha, alpha, alpha, alpha},
-                              set[i].fSrcRect, set[i].fDstRect, aa, set[i].fAAFlags,
-                              SkCanvas::kFast_SrcRectConstraint, viewMatrix, texXform);
+            if (set[i].fDstClip == nullptr) {
+                // Stick with original rectangles, which allows the ops to know more about what's
+                // being drawn.
+                this->drawTexture(clip, set[i].fProxy, filter, mode, {alpha, alpha, alpha, alpha},
+                                  set[i].fSrcRect, set[i].fDstRect, aa, set[i].fAAFlags,
+                                  SkCanvas::kFast_SrcRectConstraint, viewMatrix, texXform);
+            } else {
+                // Generate interpolated texture coordinates to match the dst clip
+                SkPoint srcQuad[4];
+                GrMapRectPoints(set[i].fDstRect, set[i].fSrcRect, set[i].fDstClip, srcQuad, 4);
+                // Don't send srcRect as the domain, since the normal case doesn't use a constraint
+                // with the entire srcRect, so sampling into dstRect outside of dstClip will just
+                // keep seams look more correct.
+                this->drawTextureQuad(clip, set[i].fProxy, filter, mode,
+                                      {alpha, alpha, alpha, alpha}, srcQuad, set[i].fDstClip,
+                                      aa, set[i].fAAFlags, nullptr, viewMatrix, texXform);
+            }
         }
     } else {
         // Can use a single op, avoiding GrPaint creation, and can batch across proxies
diff --git a/src/gpu/GrRenderTargetContext.h b/src/gpu/GrRenderTargetContext.h
index 443a03e..d21af5a 100644
--- a/src/gpu/GrRenderTargetContext.h
+++ b/src/gpu/GrRenderTargetContext.h
@@ -132,11 +132,30 @@
 
     /**
      * Creates an op that draws a fill rect with per-edge control over anti-aliasing.
+     *
+     * This is a specialized version of fillQuadWithEdgeAA, but is kept separate since knowing
+     * the geometry is a rectangle affords more optimizations.
      */
     void fillRectWithEdgeAA(const GrClip& clip, GrPaint&& paint, GrAA aa, GrQuadAAFlags edgeAA,
                             const SkMatrix& viewMatrix, const SkRect& rect,
                             const SkRect* optionalLocalRect = nullptr);
 
+    /**
+     * Similar to fillRectWithEdgeAA but draws an arbitrary 2D convex quadrilateral transformed
+     * by 'viewMatrix', with per-edge control over anti-aliasing. The quad should follow the
+     * ordering used by SkRect::toQuad(), which determines how the edge AA is applied:
+     *  - "top" = points [0] and [1]
+     *  - "right" = points[1] and [2]
+     *  - "bottom" = points[2] and [3]
+     *  - "left" = points[3] and [0]
+     *
+     * The last argument, 'optionalLocalQuad', can be null if no separate local coordinates are
+     * necessary.
+     */
+    void fillQuadWithEdgeAA(const GrClip& clip, GrPaint&& paint, GrAA aa, GrQuadAAFlags edgeAA,
+                            const SkMatrix& viewMatrix, const SkPoint quad[4],
+                            const SkPoint optionalLocalQuad[4]);
+
     /** Used with drawQuadSet */
     struct QuadSetEntry {
         SkRect fRect;
@@ -160,17 +179,32 @@
                      const SkRect& dstRect, GrAA, GrQuadAAFlags, SkCanvas::SrcRectConstraint,
                      const SkMatrix& viewMatrix, sk_sp<GrColorSpaceXform> texXform);
 
+    /**
+     * Variant of drawTexture that instead draws the texture applied to 'dstQuad' transformed by
+     * 'viewMatrix', using the 'srcQuad' texture coordinates clamped to the optional 'domain'. If
+     * 'domain' is null, it's equivalent to using the fast src rect constraint. If 'domain' is
+     * provided, the strict src rect constraint is applied using 'domain'.
+     */
+    void drawTextureQuad(const GrClip& clip, sk_sp<GrTextureProxy>, GrSamplerState::Filter,
+                         SkBlendMode mode, const SkPMColor4f&, const SkPoint srcQuad[4],
+                         const SkPoint dstQuad[4], GrAA, GrQuadAAFlags, const SkRect* domain,
+                         const SkMatrix& viewMatrix, sk_sp<GrColorSpaceXform> texXform);
+
     /** Used with drawTextureSet */
     struct TextureSetEntry {
         sk_sp<GrTextureProxy> fProxy;
         SkRect fSrcRect;
         SkRect fDstRect;
+        SkPoint* fDstClip; // Must be null, or point to an array of 4 points
         float fAlpha;
         GrQuadAAFlags fAAFlags;
     };
     /**
      * Draws a set of textures with a shared filter, color, view matrix, color xform, and
      * texture color xform. The textures must all have the same GrTextureType and GrConfig.
+     *
+     * If any entries provide a non-null fDstClip array, it will be read from immediately based on
+     * fDstClipCount, so the pointer can become invalid after this returns.
      */
     void drawTextureSet(const GrClip&, const TextureSetEntry[], int cnt, GrSamplerState::Filter,
                         SkBlendMode mode, GrAA aa, const SkMatrix& viewMatrix,
diff --git a/src/gpu/SkGpuDevice.cpp b/src/gpu/SkGpuDevice.cpp
index ba4a48f..cb5c3dc 100644
--- a/src/gpu/SkGpuDevice.cpp
+++ b/src/gpu/SkGpuDevice.cpp
@@ -1468,6 +1468,7 @@
         }
         textures[i].fSrcRect = set[i].fSrcRect;
         textures[i].fDstRect = set[i].fDstRect;
+        textures[i].fDstClip = nullptr; // TODO(michaelludwig) Not exposed in SkGpuDevice API yet
         textures[i].fAlpha = set[i].fAlpha;
         textures[i].fAAFlags = SkToGrQuadAAFlags(set[i].fAAFlags);
         if (n > 0 &&
diff --git a/src/gpu/ops/GrTextureOp.cpp b/src/gpu/ops/GrTextureOp.cpp
index cb97b9b..5cf35ec 100644
--- a/src/gpu/ops/GrTextureOp.cpp
+++ b/src/gpu/ops/GrTextureOp.cpp
@@ -307,9 +307,10 @@
         GrAAType overallAAType = GrAAType::kNone; // aa type maximally compatible with all dst rects
         bool mustFilter = false;
         fCanSkipAllocatorGather = static_cast<unsigned>(true);
-        // All dst rects are transformed by the same view matrix, so their quad types are identical
-        GrQuadType quadType = GrQuadTypeForTransformedRect(viewMatrix);
-        fQuads.reserve(cnt, quadType);
+        // All dst rects are transformed by the same view matrix, so their quad types are identical,
+        // unless an entry provides a dstClip that forces quad type to be at least standard.
+        GrQuadType baseQuadType = GrQuadTypeForTransformedRect(viewMatrix);
+        fQuads.reserve(cnt, baseQuadType);
 
         for (unsigned p = 0; p < fProxyCnt; ++p) {
             fProxies[p].fProxy = SkRef(set[p].fProxy.get());
@@ -319,7 +320,16 @@
             if (!fProxies[p].fProxy->canSkipResourceAllocator()) {
                 fCanSkipAllocatorGather = static_cast<unsigned>(false);
             }
-            auto quad = GrPerspQuad::MakeFromRect(set[p].fDstRect, viewMatrix);
+
+            // Use dstRect unless dstClip is provided, which is assumed to be a quad
+            auto quad = set[p].fDstClip == nullptr ?
+                    GrPerspQuad::MakeFromRect(set[p].fDstRect, viewMatrix) :
+                    GrPerspQuad::MakeFromSkQuad(set[p].fDstClip, viewMatrix);
+            GrQuadType quadType = baseQuadType;
+            if (set[p].fDstClip && baseQuadType != GrQuadType::kPerspective) {
+                quadType = GrQuadType::kStandard;
+            }
+
             bounds.joinPossiblyEmptyRect(quad.bounds(quadType));
             GrQuadAAFlags aaFlags;
             // Don't update the overall aaType, might be inappropriate for some of the quads
@@ -337,9 +347,18 @@
             }
             float alpha = SkTPin(set[p].fAlpha, 0.f, 1.f);
             SkPMColor4f color{alpha, alpha, alpha, alpha};
-            // TODO(michaelludwig) - Once TextureSetEntry is updated to include a dstClip, fSrcQuads
-            // will need to be used similarly to the single-image ctor.
-            fQuads.push_back(quad, quadType, {color, set[p].fSrcRect, -1, Domain::kNo, aaFlags});
+            int srcQuadIndex = -1;
+            if (set[p].fDstClip) {
+                // Derive new source coordinates that match dstClip's relative locations in dstRect,
+                // but with respect to srcRect
+                SkPoint srcQuad[4];
+                GrMapRectPoints(set[p].fDstRect, set[p].fSrcRect, set[p].fDstClip, srcQuad, 4);
+                fSrcQuads.push_back(GrPerspQuad::MakeFromSkQuad(srcQuad, SkMatrix::I()),
+                                    GrQuadType::kStandard);
+                srcQuadIndex = fSrcQuads.count() - 1;
+            }
+            fQuads.push_back(quad, quadType,
+                             {color, set[p].fSrcRect, srcQuadIndex, Domain::kNo, aaFlags});
         }
         fAAType = static_cast<unsigned>(overallAAType);
         if (!mustFilter) {