Respect the max indexBuffer limits in the bulk texture draw API

This is required before we can lower the max AA quad count (again).

Bug: b/143572065 skia:9601
Change-Id: Id34123476ad49a57dc9ce7fe13f941c06f721b74
Reviewed-on: https://skia-review.googlesource.com/c/skia/+/252603
Commit-Queue: Robert Phillips <robertphillips@google.com>
Reviewed-by: Michael Ludwig <michaelludwig@google.com>
diff --git a/gn/tests.gni b/gn/tests.gni
index b893768..f488058 100644
--- a/gn/tests.gni
+++ b/gn/tests.gni
@@ -24,6 +24,7 @@
   "$_tests/BlendTest.cpp",
   "$_tests/BlitMaskClip.cpp",
   "$_tests/BlurTest.cpp",
+  "$_tests/BulkRectTest.cpp",
   "$_tests/CTest.cpp",
   "$_tests/CachedDataTest.cpp",
   "$_tests/CachedDecodingPixelRefTest.cpp",
diff --git a/src/gpu/GrOpsTask.h b/src/gpu/GrOpsTask.h
index 1a4c517..fd69509 100644
--- a/src/gpu/GrOpsTask.h
+++ b/src/gpu/GrOpsTask.h
@@ -106,6 +106,11 @@
     SkDEBUGCODE(int numClips() const override { return fNumClips; })
     SkDEBUGCODE(void visitProxies_debugOnly(const VisitSurfaceProxyFunc&) const override;)
 
+#if GR_TEST_UTILS
+    int numOpChains() const { return fOpChains.count(); }
+    const GrOp* getChain(int index) const { return fOpChains[index].head(); }
+#endif
+
 private:
     bool isNoOp() const {
         // TODO: GrLoadOp::kDiscard (i.e., storing a discard) should also be grounds for skipping
diff --git a/src/gpu/GrRenderTargetContext.cpp b/src/gpu/GrRenderTargetContext.cpp
index 1e0cc19..41b3e60 100644
--- a/src/gpu/GrRenderTargetContext.cpp
+++ b/src/gpu/GrRenderTargetContext.cpp
@@ -903,15 +903,15 @@
                                    srcQuad, domain);
         }
     } else {
-        // Can use a single op, avoiding GrPaint creation, and can batch across proxies
+        // Create the minimum number of GrTextureOps needed to draw this set. Individual
+        // GrTextureOps can rebind the texture between draws thus avoiding GrPaint (re)creation.
         AutoCheckFlush acf(this->drawingManager());
         GrAAType aaType = this->chooseAAType(aa);
         auto clampType = GrColorTypeClampType(this->colorInfo().colorType());
         auto saturate = clampType == GrClampType::kManual ? GrTextureOp::Saturate::kYes
                                                           : GrTextureOp::Saturate::kNo;
-        auto op = GrTextureOp::MakeSet(fContext, set, cnt, filter, saturate, aaType, constraint,
-                                       viewMatrix, std::move(texXform));
-        this->addDrawOp(clip, std::move(op));
+        GrTextureOp::CreateTextureSetOps(this, clip, fContext, set, cnt, filter, saturate, aaType,
+                                         constraint, viewMatrix, std::move(texXform));
     }
 }
 
diff --git a/src/gpu/GrRenderTargetContext.h b/src/gpu/GrRenderTargetContext.h
index eacff51..3543dbe 100644
--- a/src/gpu/GrRenderTargetContext.h
+++ b/src/gpu/GrRenderTargetContext.h
@@ -549,6 +549,7 @@
     friend class GrTessellatingPathRenderer;         // for access to add[Mesh]DrawOp
     friend class GrCCPerFlushResources;              // for access to addDrawOp
     friend class GrCoverageCountingPathRenderer;     // for access to addDrawOp
+    friend class GrTextureOp;                        // for access to addDrawOp
     // for a unit test
     friend void test_draw_op(GrContext*,
                              GrRenderTargetContext*,
diff --git a/src/gpu/ops/GrDrawOp.h b/src/gpu/ops/GrDrawOp.h
index a9e9752..f366a3b 100644
--- a/src/gpu/ops/GrDrawOp.h
+++ b/src/gpu/ops/GrDrawOp.h
@@ -54,6 +54,11 @@
     }
 #endif
 
+#if GR_TEST_UTILS
+    // This is really only intended for GrTextureOp to override
+    virtual int numQuads() const { return -1; }
+#endif
+
 private:
     typedef GrOp INHERITED;
 };
diff --git a/src/gpu/ops/GrQuadPerEdgeAA.cpp b/src/gpu/ops/GrQuadPerEdgeAA.cpp
index 86fe00f..8048f20 100644
--- a/src/gpu/ops/GrQuadPerEdgeAA.cpp
+++ b/src/gpu/ops/GrQuadPerEdgeAA.cpp
@@ -166,6 +166,18 @@
     }
 }
 
+#ifdef SK_DEBUG
+int QuadLimit(IndexBufferOption option) {
+    switch (option) {
+        case IndexBufferOption::kPictureFramed: return GrResourceProvider::MaxNumAAQuads();
+        case IndexBufferOption::kIndexedRects:  return GrResourceProvider::MaxNumNonAAQuads();
+        case IndexBufferOption::kTriStrips:     return SK_MaxS32; // not limited by an indexBuffer
+    }
+
+    SkUNREACHABLE;
+}
+#endif
+
 
 void ConfigureMesh(GrMesh* mesh, const VertexSpec& spec,
                    int runningQuadCount, int quadsInDraw, int maxVerts,
diff --git a/src/gpu/ops/GrQuadPerEdgeAA.h b/src/gpu/ops/GrQuadPerEdgeAA.h
index ae245ea..5f6f21e 100644
--- a/src/gpu/ops/GrQuadPerEdgeAA.h
+++ b/src/gpu/ops/GrQuadPerEdgeAA.h
@@ -151,6 +151,11 @@
     // It will, correctly, return nullptr if the indexBufferOption is kTriStrips.
     sk_sp<const GrBuffer> GetIndexBuffer(GrMeshDrawOp::Target*, IndexBufferOption);
 
+#ifdef SK_DEBUG
+    // What is the maximum number of quads allowed for the specified indexBuffer option?
+    int QuadLimit(IndexBufferOption);
+#endif
+
     // This method will configure the vertex and index data of the provided 'mesh' to comply
     // with the indexing method specified in the vertexSpec. It is up to the calling code
     // to allocate and fill in the vertex data and acquire the correct indexBuffer if it is needed.
diff --git a/src/gpu/ops/GrTextureOp.cpp b/src/gpu/ops/GrTextureOp.cpp
index 7c19c4e..50d20c1 100644
--- a/src/gpu/ops/GrTextureOp.cpp
+++ b/src/gpu/ops/GrTextureOp.cpp
@@ -159,6 +159,7 @@
                                          color, saturate, aaType, aaFlags, deviceQuad, localQuad,
                                          domain);
     }
+
     static std::unique_ptr<GrDrawOp> Make(GrRecordingContext* context,
                                           const GrRenderTargetContext::TextureSetEntry set[],
                                           int cnt,
@@ -595,9 +596,11 @@
         auto textureType = fViewCountPairs[0].fProxyView.asTextureProxy()->textureType();
         GrAAType aaType = this->aaType();
 
+        int quadCount = 0;
         for (const auto& op : ChainRange<TextureOp>(this)) {
             for (unsigned p = 0; p < op.fProxyCnt; ++p) {
                 auto* proxy = op.fViewCountPairs[p].fProxyView.asTextureProxy();
+                quadCount += op.fViewCountPairs[p].fQuadCnt;
                 SkASSERT(proxy);
                 SkASSERT(proxy->textureType() == textureType);
                 SkASSERT(op.fViewCountPairs[p].fProxyView.swizzle() ==
@@ -612,9 +615,15 @@
                 SkASSERT(aaType == GrAAType::kMSAA && op.aaType() == GrAAType::kMSAA);
             }
         }
+
+        SkASSERT(quadCount == this->numChainedQuads());
     }
 #endif
 
+#if GR_TEST_UTILS
+    int numQuads() const final { return this->totNumQuads(); }
+#endif
+
     void characterize(PrePreparedDesc* desc) const {
         GrQuad::Type quadType = GrQuad::Type::kAxisAligned;
         ColorType colorType = ColorType::kNone;
@@ -659,6 +668,8 @@
         desc->fVertexSpec = VertexSpec(quadType, colorType, srcQuadType, /* hasLocal */ true,
                                        domain, overallAAType, /* alpha as coverage */ true,
                                        indexBufferOption);
+
+        SkASSERT(desc->fNumTotalQuads <= GrQuadPerEdgeAA::QuadLimit(indexBufferOption));
     }
 
     int totNumQuads() const {
@@ -879,21 +890,25 @@
 
 }  // anonymous namespace
 
-namespace GrTextureOp {
+#if GR_TEST_UTILS
+uint32_t GrTextureOp::ClassID() {
+    return TextureOp::ClassID();
+}
+#endif
 
-std::unique_ptr<GrDrawOp> Make(GrRecordingContext* context,
-                               GrSurfaceProxyView proxyView,
-                               GrColorType srcColorType,
-                               sk_sp<GrColorSpaceXform> textureXform,
-                               GrSamplerState::Filter filter,
-                               const SkPMColor4f& color,
-                               Saturate saturate,
-                               SkBlendMode blendMode,
-                               GrAAType aaType,
-                               GrQuadAAFlags aaFlags,
-                               const GrQuad& deviceQuad,
-                               const GrQuad& localQuad,
-                               const SkRect* domain) {
+std::unique_ptr<GrDrawOp> GrTextureOp::Make(GrRecordingContext* context,
+                                            GrSurfaceProxyView proxyView,
+                                            GrColorType srcColorType,
+                                            sk_sp<GrColorSpaceXform> textureXform,
+                                            GrSamplerState::Filter filter,
+                                            const SkPMColor4f& color,
+                                            Saturate saturate,
+                                            SkBlendMode blendMode,
+                                            GrAAType aaType,
+                                            GrQuadAAFlags aaFlags,
+                                            const GrQuad& deviceQuad,
+                                            const GrQuad& localQuad,
+                                            const SkRect* domain) {
     GrTextureProxy* proxy = proxyView.asTextureProxy();
     // Apply optimizations that are valid whether or not using GrTextureOp or GrFillRectOp
     if (domain && domain->contains(proxy->backingStoreBoundsRect())) {
@@ -938,20 +953,143 @@
     }
 }
 
-std::unique_ptr<GrDrawOp> MakeSet(GrRecordingContext* context,
-                                  const GrRenderTargetContext::TextureSetEntry set[],
-                                  int cnt,
-                                  GrSamplerState::Filter filter,
-                                  Saturate saturate,
-                                  GrAAType aaType,
-                                  SkCanvas::SrcRectConstraint constraint,
-                                  const SkMatrix& viewMatrix,
-                                  sk_sp<GrColorSpaceXform> textureColorSpaceXform) {
-    return TextureOp::Make(context, set, cnt, filter, saturate, aaType, constraint, viewMatrix,
-                           std::move(textureColorSpaceXform));
-}
+// A helper class that assists in breaking up bulk API quad draws into manageable chunks.
+class GrTextureOp::BatchSizeLimiter {
+public:
+    BatchSizeLimiter(GrRenderTargetContext* rtc,
+                     const GrClip& clip,
+                     GrRecordingContext* context,
+                     int numEntries,
+                     GrSamplerState::Filter filter,
+                     GrTextureOp::Saturate saturate,
+                     SkCanvas::SrcRectConstraint constraint,
+                     const SkMatrix& viewMatrix,
+                     sk_sp<GrColorSpaceXform> textureColorSpaceXform)
+            : fRTC(rtc)
+            , fClip(clip)
+            , fContext(context)
+            , fFilter(filter)
+            , fSaturate(saturate)
+            , fConstraint(constraint)
+            , fViewMatrix(viewMatrix)
+            , fTextureColorSpaceXform(textureColorSpaceXform)
+            , fNumLeft(numEntries) {
+    }
 
-}  // namespace GrTextureOp
+    void createOp(const GrRenderTargetContext::TextureSetEntry set[],
+                  int clumpSize,
+                  GrAAType aaType) {
+        std::unique_ptr<GrDrawOp> op = TextureOp::Make(fContext, &set[fNumClumped], clumpSize,
+                                                       fFilter, fSaturate, aaType,
+                                                       fConstraint, fViewMatrix,
+                                                       fTextureColorSpaceXform);
+        fRTC->addDrawOp(fClip, std::move(op));
+
+        fNumLeft -= clumpSize;
+        fNumClumped += clumpSize;
+    }
+
+    int numLeft() const { return fNumLeft;  }
+    int baseIndex() const { return fNumClumped; }
+
+private:
+    GrRenderTargetContext*      fRTC;
+    const GrClip&               fClip;
+    GrRecordingContext*         fContext;
+    GrSamplerState::Filter      fFilter;
+    GrTextureOp::Saturate       fSaturate;
+    SkCanvas::SrcRectConstraint fConstraint;
+    const SkMatrix&             fViewMatrix;
+    sk_sp<GrColorSpaceXform>    fTextureColorSpaceXform;
+
+    int                         fNumLeft;
+    int                         fNumClumped = 0; // also the offset for the start of the next clump
+};
+
+// Greedily clump quad draws together until the index buffer limit is exceeded.
+void GrTextureOp::CreateTextureSetOps(GrRenderTargetContext* rtc,
+                                      const GrClip& clip,
+                                      GrRecordingContext* context,
+                                      const GrRenderTargetContext::TextureSetEntry set[],
+                                      int cnt,
+                                      GrSamplerState::Filter filter,
+                                      Saturate saturate,
+                                      GrAAType aaType,
+                                      SkCanvas::SrcRectConstraint constraint,
+                                      const SkMatrix& viewMatrix,
+                                      sk_sp<GrColorSpaceXform> textureColorSpaceXform) {
+
+    // First check if we can always just make a single op and avoid the extra iteration
+    // needed to clump things together.
+    if (cnt <= SkTMin(GrResourceProvider::MaxNumNonAAQuads(),
+                      GrResourceProvider::MaxNumAAQuads())) {
+        auto op = TextureOp::Make(context, set, cnt, filter, saturate, aaType,
+                                  constraint, viewMatrix, std::move(textureColorSpaceXform));
+        rtc->addDrawOp(clip, std::move(op));
+        return;
+    }
+
+    BatchSizeLimiter state(rtc, clip, context, cnt, filter, saturate, constraint, viewMatrix,
+                           std::move(textureColorSpaceXform));
+
+    // kNone and kMSAA never get altered
+    if (aaType == GrAAType::kNone || aaType == GrAAType::kMSAA) {
+        // Clump these into series of MaxNumNonAAQuads-sized GrTextureOps
+        while (state.numLeft() > 0) {
+            int clumpSize = SkTMin(state.numLeft(), GrResourceProvider::MaxNumNonAAQuads());
+
+            state.createOp(set, clumpSize, aaType);
+        }
+    } else {
+        // kCoverage can be downgraded to kNone. Note that the following is conservative. kCoverage
+        // can also get downgraded to kNone if all the quads are on integer coordinates and
+        // axis-aligned.
+        SkASSERT(aaType == GrAAType::kCoverage);
+
+        while (state.numLeft() > 0) {
+            GrAAType runningAA = GrAAType::kNone;
+            bool clumped = false;
+
+            for (int i = 0; i < state.numLeft(); ++i) {
+                int absIndex = state.baseIndex() + i;
+
+                if (set[absIndex].fAAFlags != GrQuadAAFlags::kNone) {
+
+                    if (i >= GrResourceProvider::MaxNumAAQuads()) {
+                        // Here we either need to boost the AA type to kCoverage, but doing so with
+                        // all the accumulated quads would overflow, or we have a set of AA quads
+                        // that has just gotten too large. In either case, calve off the existing
+                        // quads as their own TextureOp.
+                        state.createOp(
+                            set,
+                            runningAA == GrAAType::kNone ? i : GrResourceProvider::MaxNumAAQuads(),
+                            runningAA); // maybe downgrading AA here
+                        clumped = true;
+                        break;
+                    }
+
+                    runningAA = GrAAType::kCoverage;
+                } else if (runningAA == GrAAType::kNone) {
+
+                    if (i >= GrResourceProvider::MaxNumNonAAQuads()) {
+                        // Here we've found a consistent batch of non-AA quads that has gotten too
+                        // large. Calve it off as its own GrTextureOp.
+                        state.createOp(set, GrResourceProvider::MaxNumNonAAQuads(),
+                                       GrAAType::kNone); // definitely downgrading AA here
+                        clumped = true;
+                        break;
+                    }
+                }
+            }
+
+            if (!clumped) {
+                // We ran through the above loop w/o hitting a limit. Spit out this last clump of
+                // quads and call it a day.
+                state.createOp(set, state.numLeft(), runningAA); // maybe downgrading AA here
+            }
+        }
+    }
+}
 
 #if GR_TEST_UTILS
 #include "include/private/GrRecordingContext.h"
diff --git a/src/gpu/ops/GrTextureOp.h b/src/gpu/ops/GrTextureOp.h
index a4eb15b..600bcc8 100644
--- a/src/gpu/ops/GrTextureOp.h
+++ b/src/gpu/ops/GrTextureOp.h
@@ -20,49 +20,59 @@
 struct SkRect;
 class SkMatrix;
 
-namespace GrTextureOp {
+class GrTextureOp {
+public:
 
-/**
- * Controls whether saturate() is called after the texture is color-converted to ensure all
- * color values are in 0..1 range.
- */
-enum class Saturate : bool { kNo = false, kYes = true };
+    /**
+     * Controls whether saturate() is called after the texture is color-converted to ensure all
+     * color values are in 0..1 range.
+     */
+    enum class Saturate : bool { kNo = false, kYes = true };
 
-/**
- * Creates an op that draws a sub-quadrilateral of a texture. The passed color is modulated by the
- * texture's color. 'deviceQuad' specifies the device-space coordinates to draw, using 'localQuad'
- * to map into the proxy's texture space. If non-null, 'domain' represents the boundary for the
- * strict src rect constraint. If GrAAType is kCoverage then AA is applied to the edges
- * indicated by GrQuadAAFlags. Otherwise, GrQuadAAFlags is ignored.
- *
- * This is functionally very similar to GrFillRectOp::Make, except that the GrPaint has been
- * deconstructed into the texture, filter, modulating color, and blend mode. When blend mode is
- * src over, this will return a GrFillRectOp with a paint that samples the proxy.
- */
-std::unique_ptr<GrDrawOp> Make(GrRecordingContext*,
-                               GrSurfaceProxyView,
-                               GrColorType srcColorType,
-                               sk_sp<GrColorSpaceXform>,
-                               GrSamplerState::Filter,
-                               const SkPMColor4f&,
-                               Saturate,
-                               SkBlendMode,
-                               GrAAType,
-                               GrQuadAAFlags,
-                               const GrQuad& deviceQuad,
-                               const GrQuad& localQuad,
-                               const SkRect* domain = nullptr);
+    /**
+     * Creates an op that draws a sub-quadrilateral of a texture. The passed color is modulated by
+     * the texture's color. 'deviceQuad' specifies the device-space coordinates to draw, using
+     * 'localQuad' to map into the proxy's texture space. If non-null, 'domain' represents the
+     * boundary for the strict src rect constraint. If GrAAType is kCoverage then AA is applied to
+     * the edges indicated by GrQuadAAFlags. Otherwise, GrQuadAAFlags is ignored.
+     *
+     * This is functionally very similar to GrFillRectOp::Make, except that the GrPaint has been
+     * deconstructed into the texture, filter, modulating color, and blend mode. When blend mode is
+     * src over, this will return a GrFillRectOp with a paint that samples the proxy.
+     */
+    static std::unique_ptr<GrDrawOp> Make(GrRecordingContext*,
+                                          GrSurfaceProxyView,
+                                          GrColorType srcColorType,
+                                          sk_sp<GrColorSpaceXform>,
+                                          GrSamplerState::Filter,
+                                          const SkPMColor4f&,
+                                          Saturate,
+                                          SkBlendMode,
+                                          GrAAType,
+                                          GrQuadAAFlags,
+                                          const GrQuad& deviceQuad,
+                                          const GrQuad& localQuad,
+                                          const SkRect* domain = nullptr);
 
-// Unlike the single-proxy factory, this only supports src-over blending.
-std::unique_ptr<GrDrawOp> MakeSet(GrRecordingContext*,
-                                  const GrRenderTargetContext::TextureSetEntry[],
-                                  int cnt,
-                                  GrSamplerState::Filter,
-                                  Saturate,
-                                  GrAAType,
-                                  SkCanvas::SrcRectConstraint,
-                                  const SkMatrix& viewMatrix,
-                                  sk_sp<GrColorSpaceXform> textureXform);
+    // Unlike the single-proxy factory, this only supports src-over blending.
+    static void CreateTextureSetOps(GrRenderTargetContext*,
+                                    const GrClip& clip,
+                                    GrRecordingContext*,
+                                    const GrRenderTargetContext::TextureSetEntry[],
+                                    int cnt,
+                                    GrSamplerState::Filter,
+                                    Saturate,
+                                    GrAAType,
+                                    SkCanvas::SrcRectConstraint,
+                                    const SkMatrix& viewMatrix,
+                                    sk_sp<GrColorSpaceXform> textureXform);
 
-}
+#if GR_TEST_UTILS
+    static uint32_t ClassID();
+#endif
+
+private:
+    class BatchSizeLimiter;
+};
+
 #endif  // GrTextureOp_DEFINED
diff --git a/tests/BulkRectTest.cpp b/tests/BulkRectTest.cpp
new file mode 100644
index 0000000..b89ccc9
--- /dev/null
+++ b/tests/BulkRectTest.cpp
@@ -0,0 +1,147 @@
+/*
+ * Copyright 2019 Google LLC
+ *
+ * Use of this source code is governed by a BSD-style license that can be
+ * found in the LICENSE file.
+ */
+
+#include "src/gpu/GrClip.h"
+#include "src/gpu/GrContextPriv.h"
+#include "src/gpu/GrMemoryPool.h"
+#include "src/gpu/GrRenderTargetContext.h"
+#include "src/gpu/ops/GrTextureOp.h"
+#include "tests/Test.h"
+
+static std::unique_ptr<GrRenderTargetContext> new_RTC(GrContext* context) {
+    return context->priv().makeDeferredRenderTargetContext(SkBackingFit::kExact, 128, 128,
+                                                           GrColorType::kRGBA_8888, nullptr);
+}
+
+sk_sp<GrSurfaceProxy> create_proxy(GrContext* context) {
+    GrSurfaceDesc desc;
+    desc.fConfig = kRGBA_8888_GrPixelConfig;
+    desc.fWidth  = 128;
+    desc.fHeight = 128;
+
+    const GrBackendFormat format = context->priv().caps()->getDefaultBackendFormat(
+                                                                           GrColorType::kRGBA_8888,
+                                                                           GrRenderable::kYes);
+
+    return context->priv().proxyProvider()->createProxy(
+        format, desc, GrRenderable::kYes, 1, kTopLeft_GrSurfaceOrigin, GrMipMapped::kNo,
+        SkBackingFit::kExact, SkBudgeted::kNo, GrProtected::kNo, GrInternalSurfaceFlags::kNone);
+}
+
+typedef GrQuadAAFlags (*GimmeSomeAAFunc)(int i);
+
+static void bulk_rect_create_test(skiatest::Reporter* reporter, GrContext* context,
+                                  GimmeSomeAAFunc gimmeSomeAA, GrAAType overallAA,
+                                  int requestedTotNumQuads, int expectedNumOps) {
+
+    std::unique_ptr<GrRenderTargetContext> rtc = new_RTC(context);
+
+    sk_sp<GrSurfaceProxy> proxy = create_proxy(context);
+
+    GrSurfaceProxyView proxyView(std::move(proxy), kTopLeft_GrSurfaceOrigin, GrSwizzle::RGBA());
+
+    auto set = new GrRenderTargetContext::TextureSetEntry[requestedTotNumQuads];
+
+    for (int i = 0; i < requestedTotNumQuads; ++i) {
+        set[i].fProxyView = proxyView;
+        set[i].fSrcColorType = GrColorType::kRGBA_8888;
+        set[i].fSrcRect = SkRect::MakeWH(100.0f, 100.0f);
+        set[i].fDstRect = SkRect::MakeWH(100.5f, 100.5f); // prevent the int non-AA optimization
+        set[i].fDstClipQuad = nullptr;
+        set[i].fPreViewMatrix = nullptr;
+        set[i].fAlpha = 1.0f;
+        set[i].fAAFlags = gimmeSomeAA(i);
+    }
+
+    GrTextureOp::CreateTextureSetOps(rtc.get(), GrNoClip(), context, set, requestedTotNumQuads,
+                                     GrSamplerState::Filter::kNearest,
+                                     GrTextureOp::Saturate::kYes,
+                                     overallAA,
+                                     SkCanvas::kStrict_SrcRectConstraint,
+                                     SkMatrix::I(), nullptr);
+
+    GrOpsTask* opsTask = rtc->testingOnly_PeekLastOpsTask();
+    int actualNumOps = opsTask->numOpChains();
+
+    int actualTotNumQuads = 0;
+
+    for (int i = 0; i < actualNumOps; ++i) {
+        const GrOp* tmp = opsTask->getChain(i);
+        REPORTER_ASSERT(reporter, tmp->classID() == GrTextureOp::ClassID());
+        REPORTER_ASSERT(reporter, tmp->isChainTail());
+        actualTotNumQuads += ((GrDrawOp*) tmp)->numQuads();
+    }
+
+    REPORTER_ASSERT(reporter, expectedNumOps == actualNumOps);
+    REPORTER_ASSERT(reporter, requestedTotNumQuads == actualTotNumQuads);
+
+    context->flush();
+
+    delete[] set;
+}
+
+DEF_GPUTEST_FOR_RENDERING_CONTEXTS(BulkRect, reporter, ctxInfo) {
+    GrContext* context = ctxInfo.grContext();
+
+    if (!context->priv().caps()->dynamicStateArrayGeometryProcessorTextureSupport()) {
+        return;
+    }
+
+    // This is the simple case where there is no AA at all. We expect 2 non-AA clumps of quads.
+    {
+        auto noAA = [](int i) -> GrQuadAAFlags {
+            return GrQuadAAFlags::kNone;
+        };
+
+        static const int kNumExpectedOps = 2;
+
+        bulk_rect_create_test(reporter, context, noAA, GrAAType::kNone,
+                              2*GrResourceProvider::MaxNumNonAAQuads(), kNumExpectedOps);
+    }
+
+    // This is the same as the above case except the overall AA is kCoverage. However, since
+    // the per-quad AA is still none, all the quads should be downgraded to non-AA.
+    {
+        auto noAA = [](int i) -> GrQuadAAFlags {
+            return GrQuadAAFlags::kNone;
+        };
+
+        static const int kNumExpectedOps = 2;
+
+        bulk_rect_create_test(reporter, context, noAA, GrAAType::kCoverage,
+                              2*GrResourceProvider::MaxNumNonAAQuads(), kNumExpectedOps);
+    }
+
+    // This case has an overall AA of kCoverage but the per-quad AA alternates.
+    // We should end up with several aa-sized clumps
+    {
+        auto alternateAA = [](int i) -> GrQuadAAFlags {
+            return (i % 2) ? GrQuadAAFlags::kAll : GrQuadAAFlags::kNone;
+        };
+
+        int numExpectedOps = 2*GrResourceProvider::MaxNumNonAAQuads() /
+                                                 GrResourceProvider::MaxNumAAQuads();
+
+        bulk_rect_create_test(reporter, context, alternateAA, GrAAType::kCoverage,
+                              2*GrResourceProvider::MaxNumNonAAQuads(), numExpectedOps);
+    }
+
+    // In this case we have a run of MaxNumAAQuads non-AA quads and then AA quads. This
+    // exercises the case where we have a clump of quads that can't be upgraded to AA bc of
+    // its size. We expect one clump of non-AA quads followed by one clump of AA quads.
+    {
+        auto runOfNonAA = [](int i) -> GrQuadAAFlags {
+            return (i < GrResourceProvider::MaxNumAAQuads()) ? GrQuadAAFlags::kNone
+                                                             : GrQuadAAFlags::kAll;
+        };
+
+        static const int kNumExpectedOps = 2;
+
+        bulk_rect_create_test(reporter, context, runOfNonAA, GrAAType::kCoverage,
+                              2*GrResourceProvider::MaxNumAAQuads(), kNumExpectedOps);
+    }
+}