Reland "Replace GrQuadList with variable-length quad buffer"

This reverts commit 19628ec144a6e480e56bbfff38ae07a3db1588d1.

Reason for revert: fixed struct size on 32-bit iOS

Original change's description:
> Revert "Replace GrQuadList with variable-length quad buffer"
> 
> This reverts commit f2816044292f0d405c3060572d65fb35cef7cc3a.
> 
> Reason for revert: Breaking G3 and iOS Build
> 
> Original change's description:
> > Replace GrQuadList with variable-length quad buffer
> > 
> > Change-Id: I5cc391e8d143032893511695961f5251f40e8291
> > Reviewed-on: https://skia-review.googlesource.com/c/skia/+/223803
> > Reviewed-by: Brian Salomon <bsalomon@google.com>
> > Commit-Queue: Michael Ludwig <michaelludwig@google.com>
> 
> TBR=bsalomon@google.com,michaelludwig@google.com
> 
> Change-Id: I55947c068c6472c301952e33cbc36d04505f9800
> No-Presubmit: true
> No-Tree-Checks: true
> No-Try: true
> Reviewed-on: https://skia-review.googlesource.com/c/skia/+/223993
> Reviewed-by: Brian Salomon <bsalomon@google.com>
> Commit-Queue: Brian Salomon <bsalomon@google.com>

TBR=bsalomon@google.com,michaelludwig@google.com

Change-Id: I7522270d467faf0f4e777831e9186bad010409ab
No-Presubmit: true
No-Tree-Checks: true
No-Try: true
Reviewed-on: https://skia-review.googlesource.com/c/skia/+/224184
Reviewed-by: Michael Ludwig <michaelludwig@google.com>
Commit-Queue: Michael Ludwig <michaelludwig@google.com>
diff --git a/src/gpu/geometry/GrQuad.h b/src/gpu/geometry/GrQuad.h
index ea3cdec..361c9a3 100644
--- a/src/gpu/geometry/GrQuad.h
+++ b/src/gpu/geometry/GrQuad.h
@@ -132,9 +132,11 @@
     // 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.
-
+    const float* xs() const { return fX; }
     float* xs() { return fX; }
+    const float* ys() const { return fY; }
     float* ys() { return fY; }
+    const float* ws() const { return fW; }
     float* ws() { return fW; }
 
     void setQuadType(Type newType) { fType = newType; }
diff --git a/src/gpu/geometry/GrQuadBuffer.h b/src/gpu/geometry/GrQuadBuffer.h
new file mode 100644
index 0000000..a2e2f4c
--- /dev/null
+++ b/src/gpu/geometry/GrQuadBuffer.h
@@ -0,0 +1,375 @@
+/*
+ * 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 "include/private/SkTDArray.h"
+#include "src/gpu/geometry/GrQuad.h"
+
+template<typename T>
+class GrQuadBuffer {
+public:
+    GrQuadBuffer()
+            : fCount(0)
+            , fDeviceType(GrQuad::Type::kAxisAligned)
+            , fLocalType(GrQuad::Type::kAxisAligned) {
+        // Pre-allocate space for 1 2D device-space quad, metadata, and header
+        fData.reserve(this->entrySize(fDeviceType, nullptr));
+    }
+
+    // Reserves space for the given number of entries; if 'needsLocals' is true, space will be
+    // reserved for each entry to also have a 2D local quad. The reserved space assumes 2D device
+    // quad for simplicity. Since this buffer has a variable bitrate encoding for quads, this may
+    // over or under reserve, but pre-allocating still helps when possible.
+    GrQuadBuffer(int count, bool needsLocals = false)
+            : fCount(0)
+            , fDeviceType(GrQuad::Type::kAxisAligned)
+            , fLocalType(GrQuad::Type::kAxisAligned) {
+        int entrySize = this->entrySize(fDeviceType, needsLocals ? &fLocalType : nullptr);
+        fData.reserve(count * entrySize);
+    }
+
+    // The number of device-space quads (and metadata, and optional local quads) that are in the
+    // the buffer.
+    int count() const { return fCount; }
+
+    // The most general type for the device-space quads in this buffer
+    GrQuad::Type deviceQuadType() const { return fDeviceType; }
+
+    // The most general type for the local quads; if no local quads are ever added, this will
+    // return kAxisAligned.
+    GrQuad::Type localQuadType() const { return fLocalType; }
+
+    // Append the given 'deviceQuad' to this buffer, with its associated 'metadata'. If 'localQuad'
+    // is not null, the local coordinates will also be attached to the entry. When an entry
+    // has local coordinates, during iteration, the Iter::hasLocals() will return true and its
+    // Iter::localQuad() will be equivalent to the provided local coordinates. If 'localQuad' is
+    // null then Iter::hasLocals() will report false for the added entry.
+    void append(const GrQuad& deviceQuad, T&& metadata, const GrQuad* localQuad = nullptr);
+
+    // Copies all entries from 'that' to this buffer
+    void concat(const GrQuadBuffer<T>& that);
+
+    // Provides a read-only iterator over a quad buffer, giving access to the device quad, metadata
+    // and optional local quad.
+    class Iter {
+    public:
+        Iter(const GrQuadBuffer<T>* buffer)
+                : fDeviceQuad(SkRect::MakeEmpty())
+                , fLocalQuad(SkRect::MakeEmpty())
+                , fBuffer(buffer)
+                , fCurrentEntry(nullptr)
+                , fNextEntry(buffer->fData.begin()) {
+            SkDEBUGCODE(fExpectedCount = buffer->count();)
+        }
+
+        bool next();
+
+        const T& metadata() const { this->validate(); return *(fBuffer->metadata(fCurrentEntry)); }
+
+        const GrQuad& deviceQuad() const { this->validate(); return fDeviceQuad; }
+
+        // If isLocalValid() returns false, this returns an empty quad (all 0s) so that localQuad()
+        // can be called without triggering any sanitizers, for convenience when some other state
+        // ensures that the quad will eventually not be used.
+        const GrQuad& localQuad() const {
+            this->validate();
+            return fLocalQuad;
+        }
+
+        bool isLocalValid() const {
+            this->validate();
+            return fBuffer->header(fCurrentEntry)->fHasLocals;
+        }
+
+    private:
+        // Quads are stored locally so that calling code doesn't need to re-declare their own quads
+        GrQuad fDeviceQuad;
+        GrQuad fLocalQuad;
+
+        const GrQuadBuffer<T>* fBuffer;
+        // The pointer to the current entry to read metadata/header details from
+        const char* fCurrentEntry;
+        // The pointer to replace fCurrentEntry when next() is called, cached since it is calculated
+        // automatically while unpacking the quad data.
+        const char* fNextEntry;
+
+        SkDEBUGCODE(int fExpectedCount;)
+
+        void validate() const {
+            SkDEBUGCODE(fBuffer->validate(fCurrentEntry, fExpectedCount);)
+        }
+    };
+
+    Iter iterator() const { return Iter(this); }
+
+    // Provides a *mutable* iterator over just the metadata stored in the quad buffer. This skips
+    // unpacking the device and local quads into GrQuads and is intended for use during op
+    // finalization, which may require rewriting state such as color.
+    class MetadataIter {
+    public:
+        MetadataIter(GrQuadBuffer<T>* list)
+                : fBuffer(list)
+                , fCurrentEntry(nullptr) {
+            SkDEBUGCODE(fExpectedCount = list->count();)
+        }
+
+        bool next();
+
+        T& operator*() { this->validate(); return *(fBuffer->metadata(fCurrentEntry)); }
+
+        T* operator->() { this->validate(); return fBuffer->metadata(fCurrentEntry); }
+
+    private:
+        GrQuadBuffer<T>* fBuffer;
+        char* fCurrentEntry;
+
+        SkDEBUGCODE(int fExpectedCount;)
+
+        void validate() const {
+            SkDEBUGCODE(fBuffer->validate(fCurrentEntry, fExpectedCount);)
+        }
+    };
+
+    MetadataIter metadata() { return MetadataIter(this); }
+
+private:
+    struct alignas(int32_t) Header {
+        unsigned fDeviceType : 2;
+        unsigned fLocalType  : 2; // Ignore if fHasLocals is false
+        unsigned fHasLocals  : 1;
+        // Known value to detect if iteration doesn't properly advance through the buffer
+        SkDEBUGCODE(unsigned fSentinel : 27;)
+    };
+    static_assert(sizeof(Header) == sizeof(int32_t), "Header should be 4 bytes");
+
+    static constexpr unsigned kSentinel = 0xbaffe;
+    static constexpr int kMetaSize = sizeof(Header) + sizeof(T);
+    static constexpr int k2DQuadFloats = 8;
+    static constexpr int k3DQuadFloats = 12;
+
+    // Each logical entry in the buffer is a variable length tuple storing device coordinates,
+    // optional local coordinates, and metadata. An entry always has a header that defines the
+    // quad types of device and local coordinates, and always has metadata of type T. The device
+    // and local quads' data follows as a variable length array of floats:
+    //  [ header    ] = 4 bytes
+    //  [ metadata  ] = sizeof(T), assert alignof(T) == 4 so that pointer casts are valid
+    //  [ device xs ] = 4 floats = 16 bytes
+    //  [ device ys ] = 4 floats
+    //  [ device ws ] = 4 floats or 0 floats depending on fDeviceType in header
+    //  [ local xs  ] = 4 floats or 0 floats depending on fHasLocals in header
+    //  [ local ys  ] = 4 floats or 0 floats depending on fHasLocals in header
+    //  [ local ws  ] = 4 floats or 0 floats depending on fHasLocals and fLocalType in header
+    // FIXME (michaelludwig) - Since this is intended only for ops, can we use the arena to
+    //      allocate storage for the quad buffer? Since this is forward-iteration only, could also
+    //      explore a linked-list structure for concatenating quads when batching ops
+    SkTDArray<char> fData;
+
+    int fCount; // Number of (device, local, metadata) entries
+    GrQuad::Type fDeviceType; // Most general type of all entries
+    GrQuad::Type fLocalType;
+
+    inline int entrySize(GrQuad::Type deviceType, const GrQuad::Type* localType) const {
+        int size = kMetaSize;
+        size += (deviceType == GrQuad::Type::kPerspective ? k3DQuadFloats
+                                                          : k2DQuadFloats) * sizeof(float);
+        if (localType) {
+            size += (*localType == GrQuad::Type::kPerspective ? k3DQuadFloats
+                                                              : k2DQuadFloats) * sizeof(float);
+        }
+        return size;
+    }
+    inline int entrySize(const Header* header) const {
+        if (header->fHasLocals) {
+            GrQuad::Type localType = static_cast<GrQuad::Type>(header->fLocalType);
+            return this->entrySize(static_cast<GrQuad::Type>(header->fDeviceType), &localType);
+        } else {
+            return this->entrySize(static_cast<GrQuad::Type>(header->fDeviceType), nullptr);
+        }
+    }
+
+    // Helpers to access typed sections of the buffer, given the start of an entry
+    inline Header* header(char* entry) {
+        return static_cast<Header*>(static_cast<void*>(entry));
+    }
+    inline const Header* header(const char* entry) const {
+        return static_cast<const Header*>(static_cast<const void*>(entry));
+    }
+
+    inline T* metadata(char* entry) {
+        return static_cast<T*>(static_cast<void*>(entry + sizeof(Header)));
+    }
+    inline const T* metadata(const char* entry) const {
+        return static_cast<const T*>(static_cast<const void*>(entry + sizeof(Header)));
+    }
+
+    inline float* coords(char* entry) {
+        return static_cast<float*>(static_cast<void*>(entry + kMetaSize));
+    }
+    inline const float* coords(const char* entry) const {
+        return static_cast<const float*>(static_cast<const void*>(entry + kMetaSize));
+    }
+
+    // Helpers to convert from coordinates to GrQuad and vice versa, returning pointer to the
+    // next packed quad coordinates.
+    float* packQuad(const GrQuad& quad, float* coords);
+    const float* unpackQuad(GrQuad::Type type, const float* coords, GrQuad* quad) const;
+
+#ifdef SK_DEBUG
+    void validate(const char* entry, int expectedCount) const;
+#endif
+};
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+// Buffer implementation
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+template<typename T>
+float* GrQuadBuffer<T>::packQuad(const GrQuad& quad, float* coords) {
+    // Copies all 12 (or 8) floats at once, so requires the 3 arrays to be contiguous
+    // FIXME(michaelludwig) - If this turns out not to be the case, just do 4 copies
+    SkASSERT(quad.xs() + 4 == quad.ys() && quad.xs() + 8 == quad.ws());
+    if (quad.hasPerspective()) {
+        memcpy(coords, quad.xs(), k3DQuadFloats * sizeof(float));
+        return coords + k3DQuadFloats;
+    } else {
+        memcpy(coords, quad.xs(), k2DQuadFloats * sizeof(float));
+        return coords + k2DQuadFloats;
+    }
+}
+
+template<typename T>
+const float* GrQuadBuffer<T>::unpackQuad(GrQuad::Type type, const float* coords, GrQuad* quad) const {
+    SkASSERT(quad->xs() + 4 == quad->ys() && quad->xs() + 8 == quad->ws());
+    if (type == GrQuad::Type::kPerspective) {
+        // Fill in X, Y, and W in one go
+        memcpy(quad->xs(), coords, k3DQuadFloats * sizeof(float));
+        coords = coords + k3DQuadFloats;
+    } else {
+        // Fill in X and Y of the quad, and set W to 1s if needed
+        memcpy(quad->xs(), coords, k2DQuadFloats * sizeof(float));
+        coords = coords + k2DQuadFloats;
+
+        if (quad->quadType() == GrQuad::Type::kPerspective) {
+            // The output quad was previously perspective, so its ws are not 1s
+            static constexpr float kNoPerspectiveWs[4] = {1.f, 1.f, 1.f, 1.f};
+            memcpy(quad->ws(), kNoPerspectiveWs, 4 * sizeof(float));
+        }
+        // Else the quad should already have 1s in w
+        SkASSERT(quad->w(0) == 1.f && quad->w(1) == 1.f &&
+                 quad->w(2) == 1.f && quad->w(3) == 1.f);
+    }
+
+    quad->setQuadType(type);
+    return coords;
+}
+
+template<typename T>
+void GrQuadBuffer<T>::append(const GrQuad& deviceQuad, T&& metadata, const GrQuad* localQuad) {
+    GrQuad::Type localType = localQuad ? localQuad->quadType() : GrQuad::Type::kAxisAligned;
+    int entrySize = this->entrySize(deviceQuad.quadType(), localQuad ? &localType : nullptr);
+
+    // Fill in the entry, as described in fData's declaration
+    char* entry = fData.append(entrySize);
+    // First the header
+    Header* h = this->header(entry);
+    h->fDeviceType = static_cast<unsigned>(deviceQuad.quadType());
+    h->fHasLocals = static_cast<unsigned>(localQuad != nullptr);
+    h->fLocalType = static_cast<unsigned>(localQuad ? localQuad->quadType()
+                                                    : GrQuad::Type::kAxisAligned);
+    SkDEBUGCODE(h->fSentinel = static_cast<unsigned>(kSentinel);)
+
+    // Second, the fixed-size metadata
+    static_assert(alignof(T) == 4, "Metadata must be 4 byte aligned");
+    *(this->metadata(entry)) = std::move(metadata);
+
+    // Then the variable blocks of x, y, and w float coordinates
+    float* coords = this->coords(entry);
+    coords = this->packQuad(deviceQuad, coords);
+    if (localQuad) {
+        coords = this->packQuad(*localQuad, coords);
+    }
+    SkASSERT((char*)coords - entry == entrySize);
+
+    // Entry complete, update buffer-level state
+    fCount++;
+    if (deviceQuad.quadType() > fDeviceType) {
+        fDeviceType = deviceQuad.quadType();
+    }
+    if (localQuad && localQuad->quadType() > fLocalType) {
+        fLocalType = localQuad->quadType();
+    }
+}
+
+template<typename T>
+void GrQuadBuffer<T>::concat(const GrQuadBuffer<T>& that) {
+    fData.append(that.fData.count(), that.fData.begin());
+    fCount += that.fCount;
+    if (that.fDeviceType > fDeviceType) {
+        fDeviceType = that.fDeviceType;
+    }
+    if (that.fLocalType > fLocalType) {
+        fLocalType = that.fLocalType;
+    }
+}
+
+#ifdef SK_DEBUG
+template<typename T>
+void GrQuadBuffer<T>::validate(const char* entry, int expectedCount) const {
+    // Triggers if accessing before next() is called on an iterator
+    SkASSERT(entry);
+    // Triggers if accessing after next() returns false
+    SkASSERT(entry < fData.end());
+    // Triggers if elements have been added to the buffer while iterating entries
+    SkASSERT(expectedCount == fCount);
+    // Make sure the start of the entry looks like a header
+    SkASSERT(this->header(entry)->fSentinel == kSentinel);
+}
+#endif
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+// Iterator implementations
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+template<typename T>
+bool GrQuadBuffer<T>::Iter::next() {
+    SkASSERT(fNextEntry);
+    if (fNextEntry >= fBuffer->fData.end()) {
+        return false;
+    }
+    // There is at least one more entry, so store the current start for metadata access
+    fCurrentEntry = fNextEntry;
+
+    // And then unpack the device and optional local coordinates into fDeviceQuad and fLocalQuad
+    const Header* h = fBuffer->header(fCurrentEntry);
+    const float* coords = fBuffer->coords(fCurrentEntry);
+    coords = fBuffer->unpackQuad(static_cast<GrQuad::Type>(h->fDeviceType), coords, &fDeviceQuad);
+    if (h->fHasLocals) {
+        coords = fBuffer->unpackQuad(static_cast<GrQuad::Type>(h->fLocalType), coords, &fLocalQuad);
+    } else {
+        static const GrQuad kEmptyLocal(SkRect::MakeEmpty());
+        fLocalQuad = kEmptyLocal;
+    }
+    // At this point, coords points to the start of the next entry
+    fNextEntry = static_cast<const char*>(static_cast<const void*>(coords));
+    SkASSERT((fNextEntry - fCurrentEntry) == fBuffer->entrySize(h));
+    return true;
+}
+
+template<typename T>
+bool GrQuadBuffer<T>::MetadataIter::next() {
+    if (fCurrentEntry) {
+        // Advance pointer by entry size
+        if (fCurrentEntry < fBuffer->fData.end()) {
+            const Header* h = fBuffer->header(fCurrentEntry);
+            fCurrentEntry += fBuffer->entrySize(h);
+        }
+    } else {
+        // First call to next
+        fCurrentEntry = fBuffer->fData.begin();
+    }
+    // Nothing else is needed to do but report whether or not the updated pointer is valid
+    return fCurrentEntry < fBuffer->fData.end();
+}
diff --git a/src/gpu/geometry/GrQuadList.h b/src/gpu/geometry/GrQuadList.h
deleted file mode 100644
index b1cbf6a..0000000
--- a/src/gpu/geometry/GrQuadList.h
+++ /dev/null
@@ -1,172 +0,0 @@
-/*
- * Copyright 2019 Google LLC
- *
- * Use of this source code is governed by a BSD-style license that can be
- * found in the LICENSE file.
- */
-
-#ifndef GrQuadList_DEFINED
-#define GrQuadList_DEFINED
-
-#include "include/private/SkTArray.h"
-#include "src/gpu/geometry/GrQuad.h"
-
-// Underlying data used by GrQuadListBase. It is defined outside of GrQuadListBase due to compiler
-// issues related to specializing member types.
-template<typename T>
-struct QuadData {
-    float fX[4];
-    float fY[4];
-    T fMetadata;
-};
-
-template<>
-struct QuadData<void> {
-    float fX[4];
-    float fY[4];
-};
-
-// A dynamic list of (possibly) perspective quads that tracks the most general quad type of all
-// added quads. It avoids storing the 3rd component if the quad type never becomes perspective.
-// Use GrQuadList subclass when only storing quads. Use GrTQuadList subclass when storing quads
-// and per-quad templated metadata (such as color or domain).
-template<typename T>
-class GrQuadListBase {
-public:
-
-    int count() const { return fXYs.count(); }
-
-    GrQuad::Type quadType() const { return fType; }
-
-    void reserve(int count, bool needsPerspective) {
-        fXYs.reserve(count);
-        if (needsPerspective || fType == GrQuad::Type::kPerspective) {
-            fWs.reserve(4 * count);
-        }
-    }
-
-    GrQuad operator[] (int i) const {
-        SkASSERT(i < this->count());
-        SkASSERT(i >= 0);
-
-        const QuadData<T>& item = fXYs[i];
-        if (fType == GrQuad::Type::kPerspective) {
-            // Read the explicit ws
-            return GrQuad(item.fX, item.fY, fWs.begin() + 4 * i, fType);
-        } else {
-            // Ws are implicitly 1s.
-            static constexpr float kNoPerspectiveWs[4] = {1.f, 1.f, 1.f, 1.f};
-            return GrQuad(item.fX, item.fY, kNoPerspectiveWs, fType);
-        }
-    }
-
-    // Subclasses expose push_back(const GrQuad|GrPerspQuad&, GrQuadType, [const T&]), where
-    // the metadata argument is only present in GrTQuadList's push_back definition.
-
-protected:
-    GrQuadListBase() : fType(GrQuad::Type::kAxisAligned) {}
-
-    void concatImpl(const GrQuadListBase<T>& that) {
-        this->upgradeType(that.fType);
-        fXYs.push_back_n(that.fXYs.count(), that.fXYs.begin());
-        if (fType == GrQuad::Type::kPerspective) {
-            if (that.fType == GrQuad::Type::kPerspective) {
-                // Copy the other's ws into the end of this list's data
-                fWs.push_back_n(that.fWs.count(), that.fWs.begin());
-            } else {
-                // This list stores ws but the appended list had implicit 1s, so add explicit 1s to
-                // fill out the total list
-                fWs.push_back_n(4 * that.count(), 1.f);
-            }
-        }
-    }
-
-    // Returns the added item data so that its metadata can be initialized if T is not void
-    QuadData<T>& pushBackImpl(const GrQuad& quad) {
-        this->upgradeType(quad.quadType());
-        QuadData<T>& item = fXYs.push_back();
-        memcpy(item.fX, quad.fX, 4 * sizeof(float));
-        memcpy(item.fY, quad.fY, 4 * sizeof(float));
-        if (fType == GrQuad::Type::kPerspective) {
-            fWs.push_back_n(4, quad.fW);
-        }
-        return item;
-    }
-
-    const QuadData<T>& item(int i) const {
-        return fXYs[i];
-    }
-
-    QuadData<T>& item(int i) {
-        return fXYs[i];
-    }
-
-private:
-    void upgradeType(GrQuad::Type type) {
-        // Possibly upgrade the overall type tracked by the list
-        if (type > fType) {
-            fType = type;
-            if (type == GrQuad::Type::kPerspective) {
-                // All existing quads were 2D, so the ws array just needs to be filled with 1s
-                fWs.push_back_n(4 * this->count(), 1.f);
-            }
-        }
-    }
-
-    // Interleaves xs, ys, and per-quad metadata so that all data for a single quad is together
-    // (barring ws, which can be dropped entirely if the quad type allows it).
-    SkSTArray<1, QuadData<T>, true> fXYs;
-    // The w channel is kept separate so that it can remain empty when only dealing with 2D quads.
-    SkTArray<float, true> fWs;
-
-    GrQuad::Type fType;
-};
-
-// This list only stores the quad data itself.
-class GrQuadList : public GrQuadListBase<void> {
-public:
-    GrQuadList() : INHERITED() {}
-
-    void concat(const GrQuadList& that) {
-        this->concatImpl(that);
-    }
-
-    void push_back(const GrQuad& quad) {
-        this->pushBackImpl(quad);
-    }
-
-private:
-    typedef GrQuadListBase<void> INHERITED;
-};
-
-// This variant of the list allows simple metadata to be stored per quad as well, such as color
-// or texture domain.
-template<typename T>
-class GrTQuadList : public GrQuadListBase<T> {
-public:
-    GrTQuadList() : INHERITED() {}
-
-    void concat(const GrTQuadList<T>& that) {
-        this->concatImpl(that);
-    }
-
-    // Adding to the list requires metadata
-    void push_back(const GrQuad& quad, T&& metadata) {
-        QuadData<T>& item = this->pushBackImpl(quad);
-        item.fMetadata = std::move(metadata);
-    }
-
-    // And provide access to the metadata per quad
-    const T& metadata(int i) const {
-        return this->item(i).fMetadata;
-    }
-
-    T& metadata(int i) {
-        return this->item(i).fMetadata;
-    }
-
-private:
-    typedef GrQuadListBase<T> INHERITED;
-};
-
-#endif
diff --git a/src/gpu/ops/GrFillRectOp.cpp b/src/gpu/ops/GrFillRectOp.cpp
index f5f7c7c..fe3cc65 100644
--- a/src/gpu/ops/GrFillRectOp.cpp
+++ b/src/gpu/ops/GrFillRectOp.cpp
@@ -14,7 +14,7 @@
 #include "src/gpu/GrPaint.h"
 #include "src/gpu/SkGr.h"
 #include "src/gpu/geometry/GrQuad.h"
-#include "src/gpu/geometry/GrQuadList.h"
+#include "src/gpu/geometry/GrQuadBuffer.h"
 #include "src/gpu/geometry/GrQuadUtils.h"
 #include "src/gpu/glsl/GrGLSLColorSpaceXformHelper.h"
 #include "src/gpu/glsl/GrGLSLGeometryProcessor.h"
@@ -79,17 +79,13 @@
                GrQuadAAFlags edgeFlags, const GrUserStencilSettings* stencil,
                const GrQuad& deviceQuad, const GrQuad& localQuad)
             : INHERITED(ClassID())
-            , fHelper(args, aaType, stencil) {
-        // The color stored with the quad is the clear color if a scissor-clear is decided upon
-        // when executing the op.
-        fDeviceQuads.push_back(deviceQuad, { paintColor, edgeFlags });
-
-        if (!fHelper.isTrivial()) {
-            // Conservatively keep track of the local coordinates; it may be that the paint doesn't
-            // need them after analysis is finished. If the paint is known to be solid up front they
-            // can be skipped entirely.
-            fLocalQuads.push_back(localQuad);
-        }
+            , fHelper(args, aaType, stencil)
+            , fQuads(1, !fHelper.isTrivial()) {
+        // Conservatively keep track of the local coordinates; it may be that the paint doesn't
+        // need them after analysis is finished. If the paint is known to be solid up front they
+        // can be skipped entirely.
+        fQuads.append(deviceQuad, { paintColor, edgeFlags },
+                      fHelper.isTrivial() ? nullptr : &localQuad);
         this->setBounds(deviceQuad.bounds(), HasAABloat(aaType == GrAAType::kCoverage),
                         IsZeroArea::kNo);
     }
@@ -103,18 +99,17 @@
 #ifdef SK_DEBUG
     SkString dumpInfo() const override {
         SkString str;
-        str.appendf("# draws: %u\n", this->quadCount());
+        str.appendf("# draws: %u\n", fQuads.count());
         str.appendf("Device quad type: %u, local quad type: %u\n",
-                    (uint32_t) fDeviceQuads.quadType(), (uint32_t) fLocalQuads.quadType());
+                    (uint32_t) fQuads.deviceQuadType(), (uint32_t) fQuads.localQuadType());
         str += fHelper.dumpInfo();
-        GrQuad device, local;
-        for (int i = 0; i < this->quadCount(); i++) {
-            device = fDeviceQuads[i];
-            const ColorAndAA& info = fDeviceQuads.metadata(i);
-            if (!fHelper.isTrivial()) {
-                local = fLocalQuads[i];
-            }
-            str += dump_quad_info(i, device, local, info.fColor, info.fAAFlags);
+        int i = 0;
+        auto iter = fQuads.iterator();
+        while(iter.next()) {
+            const ColorAndAA& info = iter.metadata();
+            str += dump_quad_info(i, iter.deviceQuad(), iter.localQuad(),
+                                  info.fColor, info.fAAFlags);
+            i++;
         }
         str += INHERITED::dumpInfo();
         return str;
@@ -125,12 +120,12 @@
             const GrCaps& caps, const GrAppliedClip* clip, bool hasMixedSampledCoverage,
             GrClampType clampType) override {
         // Initialize aggregate color analysis with the first quad's color (which always exists)
-        SkASSERT(this->quadCount() > 0);
-        GrProcessorAnalysisColor quadColors(fDeviceQuads.metadata(0).fColor);
+        auto iter = fQuads.metadata();
+        SkAssertResult(iter.next());
+        GrProcessorAnalysisColor quadColors(iter->fColor);
         // Then combine the colors of any additional quads (e.g. from MakeSet)
-        for (int i = 1; i < this->quadCount(); ++i) {
-            quadColors = GrProcessorAnalysisColor::Combine(quadColors,
-                                                           fDeviceQuads.metadata(i).fColor);
+        while(iter.next()) {
+            quadColors = GrProcessorAnalysisColor::Combine(quadColors, iter->fColor);
             if (quadColors.isUnknown()) {
                 // No point in accumulating additional starting colors, combining cannot make it
                 // less unknown.
@@ -147,19 +142,19 @@
                 caps, clip, hasMixedSampledCoverage, clampType, coverage, &quadColors);
         // If there is a constant color after analysis, that means all of the quads should be set
         // to the same color (even if they started out with different colors).
+        iter = fQuads.metadata();
         SkPMColor4f colorOverride;
         if (quadColors.isConstant(&colorOverride)) {
             fColorType = GrQuadPerEdgeAA::MinColorType(colorOverride, clampType, caps);
-            for (int i = 0; i < this->quadCount(); ++i) {
-                fDeviceQuads.metadata(i).fColor = colorOverride;
+            while(iter.next()) {
+                iter->fColor = colorOverride;
             }
         } else {
             // Otherwise compute the color type needed as the max over all quads.
             fColorType = ColorType::kNone;
-            for (int i = 0; i < this->quadCount(); ++i) {
-                SkPMColor4f* color = &fDeviceQuads.metadata(i).fColor;
+            while(iter.next()) {
                 fColorType = SkTMax(fColorType,
-                                    GrQuadPerEdgeAA::MinColorType(*color, clampType, caps));
+                                    GrQuadPerEdgeAA::MinColorType(iter->fColor, clampType, caps));
             }
         }
         // Most SkShaders' FPs multiply their calculated color by the paint color or alpha. We want
@@ -197,7 +192,7 @@
         using Domain = GrQuadPerEdgeAA::Domain;
         static constexpr SkRect kEmptyDomain = SkRect::MakeEmpty();
 
-        VertexSpec vertexSpec(fDeviceQuads.quadType(), fColorType, fLocalQuads.quadType(),
+        VertexSpec vertexSpec(fQuads.deviceQuadType(), fColorType, fQuads.localQuadType(),
                               fHelper.usesLocalCoords(), Domain::kNo, fHelper.aaType(),
                               fHelper.compatibleWithCoverageAsAlpha());
         // Make sure that if the op thought it was a solid color, the vertex spec does not use
@@ -212,7 +207,7 @@
 
         // Fill the allocated vertex data
         void* vdata = target->makeVertexSpace(
-                vertexSize, this->quadCount() * vertexSpec.verticesPerQuad(),
+                vertexSize, fQuads.count() * vertexSpec.verticesPerQuad(),
                 &vbuffer, &vertexOffsetInBuffer);
         if (!vdata) {
             SkDebugf("Could not allocate vertices\n");
@@ -221,27 +216,19 @@
 
         // vertices pointer advances through vdata based on Tessellate's return value
         void* vertices = vdata;
-        if (fHelper.isTrivial()) {
-            SkASSERT(fLocalQuads.count() == 0); // No local coords, so send an ignored dummy quad
-            static const GrQuad kIgnoredLocal(SkRect::MakeEmpty());
-
-            for (int i = 0; i < this->quadCount(); ++i) {
-                const ColorAndAA& info = fDeviceQuads.metadata(i);
-                vertices = GrQuadPerEdgeAA::Tessellate(vertices, vertexSpec, fDeviceQuads[i],
-                        info.fColor, kIgnoredLocal, kEmptyDomain, info.fAAFlags);
-            }
-        } else {
-            SkASSERT(fLocalQuads.count() == fDeviceQuads.count());
-            for (int i = 0; i < this->quadCount(); ++i) {
-                const ColorAndAA& info = fDeviceQuads.metadata(i);
-                vertices = GrQuadPerEdgeAA::Tessellate(vertices, vertexSpec, fDeviceQuads[i],
-                        info.fColor, fLocalQuads[i], kEmptyDomain, info.fAAFlags);
-            }
+        auto iter = fQuads.iterator();
+        while(iter.next()) {
+            // All entries should have local coords, or no entries should have local coords,
+            // matching !helper.isTrivial() (which is more conservative than helper.usesLocalCoords)
+            SkASSERT(iter.isLocalValid() != fHelper.isTrivial());
+            auto info = iter.metadata();
+            vertices = GrQuadPerEdgeAA::Tessellate(vertices, vertexSpec, iter.deviceQuad(),
+                    info.fColor, iter.localQuad(), kEmptyDomain, info.fAAFlags);
         }
 
         // Configure the mesh for the vertex data
         GrMesh* mesh = target->allocMeshes(1);
-        if (!GrQuadPerEdgeAA::ConfigureMeshIndices(target, mesh, vertexSpec, this->quadCount())) {
+        if (!GrQuadPerEdgeAA::ConfigureMeshIndices(target, mesh, vertexSpec, fQuads.count())) {
             SkDebugf("Could not allocate indices\n");
             return;
         }
@@ -259,7 +246,7 @@
 
         if ((fHelper.aaType() == GrAAType::kCoverage ||
              that->fHelper.aaType() == GrAAType::kCoverage) &&
-            this->quadCount() + that->quadCount() > GrQuadPerEdgeAA::kNumAAQuadsInIndexBuffer) {
+            fQuads.count() + that->fQuads.count() > GrQuadPerEdgeAA::kNumAAQuadsInIndexBuffer) {
             // This limit on batch size seems to help on Adreno devices
             return CombineResult::kCannotCombine;
         }
@@ -285,10 +272,7 @@
             fHelper.setAAType(GrAAType::kCoverage);
         }
 
-        fDeviceQuads.concat(that->fDeviceQuads);
-        if (!fHelper.isTrivial()) {
-            fLocalQuads.concat(that->fLocalQuads);
-        }
+        fQuads.concat(that->fQuads);
         return CombineResult::kMerged;
     }
 
@@ -316,17 +300,7 @@
         newBounds.joinPossiblyEmptyRect(deviceQuad.bounds());
         this->setBounds(newBounds, HasAABloat(fHelper.aaType() == GrAAType::kCoverage),
                         IsZeroArea::kNo);
-        fDeviceQuads.push_back(deviceQuad, { color, edgeAA });
-        if (!fHelper.isTrivial()) {
-            fLocalQuads.push_back(localQuad);
-        }
-    }
-
-    int quadCount() const {
-        // Sanity check that the parallel arrays for quad properties all have the same size
-        SkASSERT(fDeviceQuads.count() == fLocalQuads.count() ||
-                 (fLocalQuads.count() == 0 && fHelper.isTrivial()));
-        return fDeviceQuads.count();
+        fQuads.append(deviceQuad, { color, edgeAA }, fHelper.isTrivial() ? nullptr : &localQuad);
     }
 
     struct ColorAndAA {
@@ -335,9 +309,7 @@
     };
 
     Helper fHelper;
-    GrTQuadList<ColorAndAA> fDeviceQuads;
-    // No metadata attached to the local quads; this list is empty when local coords are not needed.
-    GrQuadList fLocalQuads;
+    GrQuadBuffer<ColorAndAA> fQuads;
 
     ColorType fColorType;
 
diff --git a/src/gpu/ops/GrTextureOp.cpp b/src/gpu/ops/GrTextureOp.cpp
index d77e88b..2aa2c06 100644
--- a/src/gpu/ops/GrTextureOp.cpp
+++ b/src/gpu/ops/GrTextureOp.cpp
@@ -30,7 +30,7 @@
 #include "src/gpu/GrTextureProxy.h"
 #include "src/gpu/SkGr.h"
 #include "src/gpu/geometry/GrQuad.h"
-#include "src/gpu/geometry/GrQuadList.h"
+#include "src/gpu/geometry/GrQuadBuffer.h"
 #include "src/gpu/geometry/GrQuadUtils.h"
 #include "src/gpu/glsl/GrGLSLVarying.h"
 #include "src/gpu/ops/GrMeshDrawOp.h"
@@ -200,13 +200,14 @@
     SkString dumpInfo() const override {
         SkString str;
         str.appendf("# draws: %d\n", fQuads.count());
-        int q = 0;
+        auto iter = fQuads.iterator();
         for (unsigned p = 0; p < fProxyCnt; ++p) {
             str.appendf("Proxy ID: %d, Filter: %d\n", fProxies[p].fProxy->uniqueID().asUInt(),
                         static_cast<int>(fFilter));
-            for (int i = 0; i < fProxies[p].fQuadCnt; ++i, ++q) {
-                GrQuad quad = fQuads[q];
-                const ColorDomainAndAA& info = fQuads.metadata(i);
+            int i = 0;
+            while(i < fProxies[p].fQuadCnt && iter.next()) {
+                const GrQuad& quad = iter.deviceQuad();
+                const ColorDomainAndAA& info = iter.metadata();
                 str.appendf(
                         "%d: Color: 0x%08x, TexRect [L: %.2f, T: %.2f, R: %.2f, B: %.2f] "
                         "Quad [(%.2f, %.2f), (%.2f, %.2f), (%.2f, %.2f), (%.2f, %.2f)]\n",
@@ -215,6 +216,7 @@
                         quad.point(0).fY, quad.point(1).fX, quad.point(1).fY,
                         quad.point(2).fX, quad.point(2).fY, quad.point(3).fX,
                         quad.point(3).fY);
+                i++;
             }
         }
         str += INHERITED::dumpInfo();
@@ -226,9 +228,9 @@
             const GrCaps& caps, const GrAppliedClip*, bool hasMixedSampledCoverage,
             GrClampType clampType) override {
         fColorType = static_cast<unsigned>(ColorType::kNone);
-        for (int q = 0; q < fQuads.count(); ++q) {
-            const ColorDomainAndAA& info = fQuads.metadata(q);
-            auto colorType = GrQuadPerEdgeAA::MinColorType(info.fColor, clampType, caps);
+        auto iter = fQuads.metadata();
+        while(iter.next()) {
+            auto colorType = GrQuadPerEdgeAA::MinColorType(iter->fColor, clampType, caps);
             fColorType = SkTMax(fColorType, static_cast<unsigned>(colorType));
         }
         return GrProcessorSet::EmptySetAnalysis();
@@ -244,6 +246,35 @@
 private:
     friend class ::GrOpMemoryPool;
 
+    struct ColorDomainAndAA {
+        // Special constructor to convert enums into the packed bits, which should not delete
+        // the implicit move constructor (but it does require us to declare an empty ctor for
+        // use with the GrTQuadList).
+        ColorDomainAndAA(const SkPMColor4f& color, const SkRect& srcRect,
+                         Domain hasDomain, GrQuadAAFlags aaFlags)
+                : fColor(color)
+                , fSrcRect(srcRect)
+                , fHasDomain(static_cast<unsigned>(hasDomain))
+                , fAAFlags(static_cast<unsigned>(aaFlags)) {
+            SkASSERT(fHasDomain == static_cast<unsigned>(hasDomain));
+            SkASSERT(fAAFlags == static_cast<unsigned>(aaFlags));
+        }
+        ColorDomainAndAA() = default;
+
+        SkPMColor4f fColor;
+        // Even if fSrcQuadIndex provides source coords, use fSrcRect for domain constraint
+        SkRect fSrcRect;
+        unsigned fHasDomain : 1;
+        unsigned fAAFlags : 4;
+
+        Domain domain() const { return Domain(fHasDomain); }
+        GrQuadAAFlags aaFlags() const { return static_cast<GrQuadAAFlags>(fAAFlags); }
+    };
+    struct Proxy {
+        GrTextureProxy* fProxy;
+        int fQuadCnt;
+    };
+
     // dstQuad and dstQuadType should be the geometry transformed by the view matrix.
     // srcRect represents original src rect and will be used as the domain when constraint is strict
     // If srcQuad is provided, it will be used for the local coords instead of srcRect, although
@@ -253,6 +284,7 @@
               SkCanvas::SrcRectConstraint constraint, const GrQuad* srcQuad, GrAAType aaType,
               GrQuadAAFlags aaFlags, sk_sp<GrColorSpaceXform> textureColorSpaceXform)
             : INHERITED(ClassID())
+            , fQuads(1, srcQuad != nullptr)
             , fTextureColorSpaceXform(std::move(textureColorSpaceXform))
             , fFilter(static_cast<unsigned>(filter)) {
         // Clean up disparities between the overall aa type and edge configuration and apply
@@ -275,11 +307,8 @@
 
         Domain domain = constraint == SkCanvas::kStrict_SrcRectConstraint ? Domain::kYes
                                                                           : Domain::kNo;
-        // Initially, if srcQuad is provided it will always be at index 0 of fSrcQuads
-        fQuads.push_back(dstQuad, {color, srcRect, srcQuad ? 0 : -1, domain, aaFlags});
-        if (srcQuad) {
-            fSrcQuads.push_back(*srcQuad);
-        }
+        fQuads.append(dstQuad, {color, srcRect, domain, aaFlags}, srcQuad);
+
         fProxyCnt = 1;
         fProxies[0] = {proxy.release(), 1};
         this->setBounds(dstQuad.bounds(), HasAABloat(aaType == GrAAType::kCoverage),
@@ -291,16 +320,13 @@
               SkCanvas::SrcRectConstraint constraint, const SkMatrix& viewMatrix,
               sk_sp<GrColorSpaceXform> textureColorSpaceXform)
             : INHERITED(ClassID())
+            , fQuads(cnt, false)
             , fTextureColorSpaceXform(std::move(textureColorSpaceXform))
             , fFilter(static_cast<unsigned>(filter)) {
         fProxyCnt = SkToUInt(cnt);
         SkRect bounds = SkRectPriv::MakeLargestInverted();
         GrAAType overallAAType = GrAAType::kNone; // aa type maximally compatible with all dst rects
         bool mustFilter = false;
-        // Most dst rects are transformed by the same view matrix, so their quad types start
-        // identical, unless an entry provides a dstClip or additional transform that changes it.
-        // The quad list will automatically adapt to that.
-        fQuads.reserve(cnt, viewMatrix.hasPerspective());
         bool allOpaque = true;
         Domain netDomain = Domain::kNo;
         for (unsigned p = 0; p < fProxyCnt; ++p) {
@@ -348,16 +374,17 @@
             float alpha = SkTPin(set[p].fAlpha, 0.f, 1.f);
             allOpaque &= (1.f == alpha);
             SkPMColor4f color{alpha, alpha, alpha, alpha};
-            int srcQuadIndex = -1;
+            GrQuad srcQuad;
+            const GrQuad* srcQuadPtr = nullptr;
             if (set[p].fDstClipQuad) {
                 // 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].fDstClipQuad, srcQuad, 4);
-                fSrcQuads.push_back(GrQuad::MakeFromSkQuad(srcQuad, SkMatrix::I()));
-                srcQuadIndex = fSrcQuads.count() - 1;
+                SkPoint srcPts[4];
+                GrMapRectPoints(set[p].fDstRect, set[p].fSrcRect, set[p].fDstClipQuad, srcPts, 4);
+                srcQuad = GrQuad::MakeFromSkQuad(srcPts, SkMatrix::I());
+                srcQuadPtr = &srcQuad;
             }
-            fQuads.push_back(quad, {color, set[p].fSrcRect, srcQuadIndex, domainForQuad, aaFlags});
+            fQuads.append(quad, {color, set[p].fSrcRect, domainForQuad, aaFlags}, srcQuadPtr);
         }
         fAAType = static_cast<unsigned>(overallAAType);
         if (!mustFilter) {
@@ -367,8 +394,8 @@
         fDomain = static_cast<unsigned>(netDomain);
     }
 
-    void tess(void* v, const VertexSpec& spec, const GrTextureProxy* proxy, int start,
-              int cnt) const {
+    void tess(void* v, const VertexSpec& spec, const GrTextureProxy* proxy,
+              GrQuadBuffer<ColorDomainAndAA>::Iter* iter, int cnt) const {
         TRACE_EVENT0("skia", TRACE_FUNC);
         auto origin = proxy->origin();
         const auto* texture = proxy->peekTexture();
@@ -382,17 +409,18 @@
             h = 1.f;
         }
 
-        for (int i = start; i < start + cnt; ++i) {
-            const GrQuad& device = fQuads[i];
-            const ColorDomainAndAA& info = fQuads.metadata(i);
+        int i = 0;
+        while(i < cnt && iter->next()) {
+            const ColorDomainAndAA& info = iter->metadata();
 
-            GrQuad srcQuad = info.fSrcQuadIndex >= 0 ?
-                    compute_src_quad(origin, fSrcQuads[info.fSrcQuadIndex], iw, ih, h) :
+            GrQuad srcQuad = iter->isLocalValid() ?
+                    compute_src_quad(origin, iter->localQuad(), iw, ih, h) :
                     compute_src_quad_from_rect(origin, info.fSrcRect, iw, ih, h);
             SkRect domain =
                     compute_domain(info.domain(), this->filter(), origin, info.fSrcRect, iw, ih, h);
-            v = GrQuadPerEdgeAA::Tessellate(v, spec, device, info.fColor, srcQuad, domain,
-                                            info.aaFlags());
+            v = GrQuadPerEdgeAA::Tessellate(v, spec, iter->deviceQuad(), info.fColor, srcQuad,
+                                            domain, info.aaFlags());
+            i++;
         }
     }
 
@@ -409,13 +437,11 @@
         const GrSwizzle& swizzle = fProxies[0].fProxy->textureSwizzle();
         GrAAType aaType = this->aaType();
         for (const auto& op : ChainRange<TextureOp>(this)) {
-            if (op.fQuads.quadType() > quadType) {
-                quadType = op.fQuads.quadType();
+            if (op.fQuads.deviceQuadType() > quadType) {
+                quadType = op.fQuads.deviceQuadType();
             }
-            if (op.fSrcQuads.quadType() > srcQuadType) {
-                // Should only become more general if there are quads to use instead of fSrcRect
-                SkASSERT(op.fSrcQuads.count() > 0);
-                srcQuadType = op.fSrcQuads.quadType();
+            if (op.fQuads.localQuadType() > srcQuadType) {
+                srcQuadType = op.fQuads.localQuadType();
             }
             if (op.fDomain) {
                 domain = Domain::kYes;
@@ -475,7 +501,7 @@
 
         int m = 0;
         for (const auto& op : ChainRange<TextureOp>(this)) {
-            int q = 0;
+            auto iter = op.fQuads.iterator();
             for (unsigned p = 0; p < op.fProxyCnt; ++p) {
                 int quadCnt = op.fProxies[p].fQuadCnt;
                 auto* proxy = op.fProxies[p].fProxy;
@@ -492,7 +518,7 @@
                 }
                 SkASSERT(numAllocatedVertices >= meshVertexCnt);
 
-                op.tess(vdata, vertexSpec, proxy, q, quadCnt);
+                op.tess(vdata, vertexSpec, proxy, &iter, quadCnt);
 
                 if (!GrQuadPerEdgeAA::ConfigureMeshIndices(target, &(meshes[m]), vertexSpec,
                                                            quadCnt)) {
@@ -508,8 +534,10 @@
                 numQuadVerticesLeft -= meshVertexCnt;
                 vertexOffsetInBuffer += meshVertexCnt;
                 vdata = reinterpret_cast<char*>(vdata) + vertexSize * meshVertexCnt;
-                q += quadCnt;
             }
+            // If quad counts per proxy were calculated correctly, the entire iterator should have
+            // been consumed.
+            SkASSERT(!iter.next());
         }
         SkASSERT(!numQuadVerticesLeft);
         SkASSERT(!numAllocatedVertices);
@@ -567,79 +595,17 @@
             fAAType = static_cast<unsigned>(GrAAType::kCoverage);
         }
 
-        // Concatenate quad lists together, updating the fSrcQuadIndex in the appended quads
-        // to account for the new starting index in fSrcQuads
-        int srcQuadOffset = fSrcQuads.count();
-        int oldQuadCount = fQuads.count();
-
-        fSrcQuads.concat(that->fSrcQuads);
+        // Concatenate quad lists together
         fQuads.concat(that->fQuads);
         fProxies[0].fQuadCnt += that->fQuads.count();
 
-        if (that->fSrcQuads.count() > 0) {
-            // Some of the concatenated quads pointed to fSrcQuads, so adjust the indices for the
-            // newly appended quads
-            for (int i = oldQuadCount; i < fQuads.count(); ++i) {
-                if (fQuads.metadata(i).fSrcQuadIndex >= 0) {
-                    fQuads.metadata(i).fSrcQuadIndex += srcQuadOffset;
-                }
-            }
-        }
-
-        // Confirm all tracked state makes sense when in debug builds
-#ifdef SK_DEBUG
-        SkASSERT(fSrcQuads.count() <= fQuads.count());
-        for (int i = 0; i < fQuads.count(); ++i) {
-            int srcIndex = fQuads.metadata(i).fSrcQuadIndex;
-            if (srcIndex >= 0) {
-                // Make sure it points to a valid index, in the right region of the list
-                SkASSERT(srcIndex < fSrcQuads.count());
-                SkASSERT((i < oldQuadCount && srcIndex < srcQuadOffset) ||
-                         (i >= oldQuadCount && srcIndex >= srcQuadOffset));
-            }
-        }
-#endif
         return CombineResult::kMerged;
     }
 
     GrAAType aaType() const { return static_cast<GrAAType>(fAAType); }
     GrSamplerState::Filter filter() const { return static_cast<GrSamplerState::Filter>(fFilter); }
 
-    struct ColorDomainAndAA {
-        // Special constructor to convert enums into the packed bits, which should not delete
-        // the implicit move constructor (but it does require us to declare an empty ctor for
-        // use with the GrTQuadList).
-        ColorDomainAndAA(const SkPMColor4f& color, const SkRect& srcRect, int srcQuadIndex,
-                         Domain hasDomain, GrQuadAAFlags aaFlags)
-                : fColor(color)
-                , fSrcRect(srcRect)
-                , fSrcQuadIndex(srcQuadIndex)
-                , fHasDomain(static_cast<unsigned>(hasDomain))
-                , fAAFlags(static_cast<unsigned>(aaFlags)) {
-            SkASSERT(fHasDomain == static_cast<unsigned>(hasDomain));
-            SkASSERT(fAAFlags == static_cast<unsigned>(aaFlags));
-        }
-        ColorDomainAndAA() = default;
-
-        SkPMColor4f fColor;
-        // Even if fSrcQuadIndex provides source coords, use fSrcRect for domain constraint
-        SkRect fSrcRect;
-        // If >= 0, use to access fSrcQuads instead of fSrcRect for the source coordinates
-        int fSrcQuadIndex;
-        unsigned fHasDomain : 1;
-        unsigned fAAFlags : 4;
-
-        Domain domain() const { return Domain(fHasDomain); }
-        GrQuadAAFlags aaFlags() const { return static_cast<GrQuadAAFlags>(fAAFlags); }
-    };
-    struct Proxy {
-        GrTextureProxy* fProxy;
-        int fQuadCnt;
-    };
-    GrTQuadList<ColorDomainAndAA> fQuads;
-    // The majority of texture ops will not track a complete src quad so this is indexed separately
-    // and may be of different size to fQuads.
-    GrQuadList fSrcQuads;
+    GrQuadBuffer<ColorDomainAndAA> fQuads;
     sk_sp<GrColorSpaceXform> fTextureColorSpaceXform;
     unsigned fFilter : 2;
     unsigned fAAType : 2;