Reland "Move GrGeometryProcessor's textures out of classes and into"

This reverts commit fdf05f4ff4e96e28a31e210cb471d149d736f528.

Reason for revert: Android fixed after removing multitexture support from TextureOp.

Original change's description:
> Revert "Move GrGeometryProcessor's textures out of classes and into"
> 
> This reverts commit af87483873f0b370b90ebe956301a13cc8662cbe.
> 
> Revert "GrGeometryProcessor derives from GrNonAtomicRef not GrProgramElement."
> 
> This reverts commit 607be37e3d4ddbe2163c200d6e13bcee981f6bf7.
> 
> Revert "Store GrMeshDrawOps' meshes in GrOpFlushState's arena."
> 
> This reverts commit b948572c7862214fe2e1fa6cdfcab4fc7b1666ac.
> 
> Revert "Remove multitexturing support from GrTextureOp."
> 
> This reverts commit 986f64c601f3ed99f84f0c392d1a42e298f6d618.
> 
> Revert "Make result of GrOp::combineIfPossible be an enum."
> 
> This reverts commit 641ac7daa81cbfca06b310803fb1a607d0fc2b32.
> 
> Bug: b/112244393
> Change-Id: I579491a3f2f2f2093f1e2a6141fa1e4cc7b760a4
> Reviewed-on: https://skia-review.googlesource.com/145646
> Reviewed-by: Brian Salomon <bsalomon@google.com>
> Commit-Queue: Brian Salomon <bsalomon@google.com>

TBR=bsalomon@google.com

Change-Id: I1d41c2ecf7862e31fb025a7a00bb07bae9d83a47
No-Presubmit: true
No-Tree-Checks: true
No-Try: true
Bug: b/112244393
Reviewed-on: https://skia-review.googlesource.com/145780
Reviewed-by: Brian Salomon <bsalomon@google.com>
Commit-Queue: Brian Salomon <bsalomon@google.com>
diff --git a/src/gpu/ops/GrAAConvexPathRenderer.cpp b/src/gpu/ops/GrAAConvexPathRenderer.cpp
index 783cac7..e9a612f 100644
--- a/src/gpu/ops/GrAAConvexPathRenderer.cpp
+++ b/src/gpu/ops/GrAAConvexPathRenderer.cpp
@@ -843,11 +843,11 @@
             extract_lines_only_verts(tess, verts, vertexStride, args.fColor, idxs,
                                      fHelper.compatibleWithAlphaAsCoverage());
 
-            GrMesh mesh(GrPrimitiveType::kTriangles);
-            mesh.setIndexed(indexBuffer, tess.numIndices(), firstIndex, 0, tess.numPts() - 1,
-                            GrPrimitiveRestart::kNo);
-            mesh.setVertexData(vertexBuffer, firstVertex);
-            target->draw(gp.get(), pipe.fPipeline, pipe.fFixedDynamicState, mesh);
+            GrMesh* mesh = target->allocMesh(GrPrimitiveType::kTriangles);
+            mesh->setIndexed(indexBuffer, tess.numIndices(), firstIndex, 0, tess.numPts() - 1,
+                             GrPrimitiveRestart::kNo);
+            mesh->setVertexData(vertexBuffer, firstVertex);
+            target->draw(gp, pipe.fPipeline, pipe.fFixedDynamicState, mesh);
         }
     }
 
@@ -928,37 +928,38 @@
             SkSTArray<kPreallocDrawCnt, Draw, true> draws;
             create_vertices(segments, fanPt, args.fColor, &draws, verts, idxs);
 
-            GrMesh mesh(GrPrimitiveType::kTriangles);
-
+            GrMesh* meshes = target->allocMeshes(draws.count());
             for (int j = 0; j < draws.count(); ++j) {
                 const Draw& draw = draws[j];
-                mesh.setIndexed(indexBuffer, draw.fIndexCnt, firstIndex, 0, draw.fVertexCnt - 1,
-                                GrPrimitiveRestart::kNo);
-                mesh.setVertexData(vertexBuffer, firstVertex);
-                target->draw(quadProcessor.get(), pipe.fPipeline, pipe.fFixedDynamicState, mesh);
+                meshes[j].setPrimitiveType(GrPrimitiveType::kTriangles);
+                meshes[j].setIndexed(indexBuffer, draw.fIndexCnt, firstIndex, 0,
+                                     draw.fVertexCnt - 1, GrPrimitiveRestart::kNo);
+                meshes[j].setVertexData(vertexBuffer, firstVertex);
                 firstIndex += draw.fIndexCnt;
                 firstVertex += draw.fVertexCnt;
             }
+            target->draw(quadProcessor, pipe.fPipeline, pipe.fFixedDynamicState, meshes,
+                         draws.count());
         }
     }
 
-    bool onCombineIfPossible(GrOp* t, const GrCaps& caps) override {
+    CombineResult onCombineIfPossible(GrOp* t, const GrCaps& caps) override {
         AAConvexPathOp* that = t->cast<AAConvexPathOp>();
         if (!fHelper.isCompatible(that->fHelper, caps, this->bounds(), that->bounds())) {
-            return false;
+            return CombineResult::kCannotCombine;
         }
         if (fHelper.usesLocalCoords() &&
             !fPaths[0].fViewMatrix.cheapEqualTo(that->fPaths[0].fViewMatrix)) {
-            return false;
+            return CombineResult::kCannotCombine;
         }
 
         if (fLinesOnly != that->fLinesOnly) {
-            return false;
+            return CombineResult::kCannotCombine;
         }
 
         fPaths.push_back_n(that->fPaths.count(), that->fPaths.begin());
         this->joinBounds(*that);
-        return true;
+        return CombineResult::kMerged;
     }
 
     struct PathData {
diff --git a/src/gpu/ops/GrAAFillRectOp.cpp b/src/gpu/ops/GrAAFillRectOp.cpp
index 592c0b2..9d66299 100644
--- a/src/gpu/ops/GrAAFillRectOp.cpp
+++ b/src/gpu/ops/GrAAFillRectOp.cpp
@@ -265,10 +265,9 @@
         SkASSERT(vertexStride == gp->debugOnly_vertexStride());
 
         sk_sp<const GrBuffer> indexBuffer = get_index_buffer(target->resourceProvider());
-        PatternHelper helper(GrPrimitiveType::kTriangles);
-        void* vertices =
-                helper.init(target, vertexStride, indexBuffer.get(), kVertsPerAAFillRect,
-                            kIndicesPerAAFillRect, fRectCnt);
+        PatternHelper helper(target, GrPrimitiveType::kTriangles, vertexStride, indexBuffer.get(),
+                             kVertsPerAAFillRect, kIndicesPerAAFillRect, fRectCnt);
+        void* vertices = helper.vertices();
         if (!vertices || !indexBuffer) {
             SkDebugf("Could not allocate vertices\n");
             return;
@@ -292,19 +291,19 @@
             info = this->next(info);
         }
         auto pipe = fHelper.makePipeline(target);
-        helper.recordDraw(target, gp.get(), pipe.fPipeline, pipe.fFixedDynamicState);
+        helper.recordDraw(target, std::move(gp), pipe.fPipeline, pipe.fFixedDynamicState);
     }
 
-    bool onCombineIfPossible(GrOp* t, const GrCaps& caps) override {
+    CombineResult onCombineIfPossible(GrOp* t, const GrCaps& caps) override {
         AAFillRectOp* that = t->cast<AAFillRectOp>();
         if (!fHelper.isCompatible(that->fHelper, caps, this->bounds(), that->bounds())) {
-            return false;
+            return CombineResult::kCannotCombine;
         }
 
         fRectData.push_back_n(that->fRectData.count(), that->fRectData.begin());
         fRectCnt += that->fRectCnt;
         this->joinBounds(*that);
-        return true;
+        return CombineResult::kMerged;
     }
 
     struct RectInfo {
diff --git a/src/gpu/ops/GrAAHairLinePathRenderer.cpp b/src/gpu/ops/GrAAHairLinePathRenderer.cpp
index 05f1fbd..7cff875 100644
--- a/src/gpu/ops/GrAAHairLinePathRenderer.cpp
+++ b/src/gpu/ops/GrAAHairLinePathRenderer.cpp
@@ -835,41 +835,41 @@
     typedef SkTArray<int, true> IntArray;
     typedef SkTArray<float, true> FloatArray;
 
-    bool onCombineIfPossible(GrOp* t, const GrCaps& caps) override {
+    CombineResult onCombineIfPossible(GrOp* t, const GrCaps& caps) override {
         AAHairlineOp* that = t->cast<AAHairlineOp>();
 
         if (!fHelper.isCompatible(that->fHelper, caps, this->bounds(), that->bounds())) {
-            return false;
+            return CombineResult::kCannotCombine;
         }
 
         if (this->viewMatrix().hasPerspective() != that->viewMatrix().hasPerspective()) {
-            return false;
+            return CombineResult::kCannotCombine;
         }
 
         // We go to identity if we don't have perspective
         if (this->viewMatrix().hasPerspective() &&
             !this->viewMatrix().cheapEqualTo(that->viewMatrix())) {
-            return false;
+            return CombineResult::kCannotCombine;
         }
 
         // TODO we can actually combine hairlines if they are the same color in a kind of bulk
         // method but we haven't implemented this yet
         // TODO investigate going to vertex color and coverage?
         if (this->coverage() != that->coverage()) {
-            return false;
+            return CombineResult::kCannotCombine;
         }
 
         if (this->color() != that->color()) {
-            return false;
+            return CombineResult::kCannotCombine;
         }
 
         if (fHelper.usesLocalCoords() && !this->viewMatrix().cheapEqualTo(that->viewMatrix())) {
-            return false;
+            return CombineResult::kCannotCombine;
         }
 
         fPaths.push_back_n(that->fPaths.count(), that->fPaths.begin());
         this->joinBounds(*that);
-        return true;
+        return CombineResult::kMerged;
     }
 
     GrColor color() const { return fColor; }
@@ -975,11 +975,11 @@
             add_line(&lines[2*i], toSrc, this->coverage(), &verts);
         }
 
-        GrMesh mesh(GrPrimitiveType::kTriangles);
-        mesh.setIndexedPatterned(linesIndexBuffer.get(), kIdxsPerLineSeg, kLineSegNumVertices,
-                                 lineCount, kLineSegsNumInIdxBuffer);
-        mesh.setVertexData(vertexBuffer, firstVertex);
-        target->draw(lineGP.get(), pipe.fPipeline, pipe.fFixedDynamicState, mesh);
+        GrMesh* mesh = target->allocMesh(GrPrimitiveType::kTriangles);
+        mesh->setIndexedPatterned(linesIndexBuffer.get(), kIdxsPerLineSeg, kLineSegNumVertices,
+                                  lineCount, kLineSegsNumInIdxBuffer);
+        mesh->setVertexData(vertexBuffer, firstVertex);
+        target->draw(std::move(lineGP), pipe.fPipeline, pipe.fFixedDynamicState, mesh);
     }
 
     if (quadCount || conicCount) {
@@ -1030,20 +1030,20 @@
         }
 
         if (quadCount > 0) {
-            GrMesh mesh(GrPrimitiveType::kTriangles);
-            mesh.setIndexedPatterned(quadsIndexBuffer.get(), kIdxsPerQuad, kQuadNumVertices,
-                                     quadCount, kQuadsNumInIdxBuffer);
-            mesh.setVertexData(vertexBuffer, firstVertex);
-            target->draw(quadGP.get(), pipe.fPipeline, pipe.fFixedDynamicState, mesh);
+            GrMesh* mesh = target->allocMesh(GrPrimitiveType::kTriangles);
+            mesh->setIndexedPatterned(quadsIndexBuffer.get(), kIdxsPerQuad, kQuadNumVertices,
+                                      quadCount, kQuadsNumInIdxBuffer);
+            mesh->setVertexData(vertexBuffer, firstVertex);
+            target->draw(std::move(quadGP), pipe.fPipeline, pipe.fFixedDynamicState, mesh);
             firstVertex += quadCount * kQuadNumVertices;
         }
 
         if (conicCount > 0) {
-            GrMesh mesh(GrPrimitiveType::kTriangles);
-            mesh.setIndexedPatterned(quadsIndexBuffer.get(), kIdxsPerQuad, kQuadNumVertices,
-                                     conicCount, kQuadsNumInIdxBuffer);
-            mesh.setVertexData(vertexBuffer, firstVertex);
-            target->draw(conicGP.get(), pipe.fPipeline, pipe.fFixedDynamicState, mesh);
+            GrMesh* mesh = target->allocMesh(GrPrimitiveType::kTriangles);
+            mesh->setIndexedPatterned(quadsIndexBuffer.get(), kIdxsPerQuad, kQuadNumVertices,
+                                      conicCount, kQuadsNumInIdxBuffer);
+            mesh->setVertexData(vertexBuffer, firstVertex);
+            target->draw(std::move(conicGP), pipe.fPipeline, pipe.fFixedDynamicState, mesh);
         }
     }
 }
diff --git a/src/gpu/ops/GrAALinearizingConvexPathRenderer.cpp b/src/gpu/ops/GrAALinearizingConvexPathRenderer.cpp
index 75662d3..c40aa71 100644
--- a/src/gpu/ops/GrAALinearizingConvexPathRenderer.cpp
+++ b/src/gpu/ops/GrAALinearizingConvexPathRenderer.cpp
@@ -210,14 +210,13 @@
     }
 
 private:
-    void draw(Target* target, const GrGeometryProcessor* gp, const GrPipeline* pipeline,
+    void draw(Target* target, sk_sp<const GrGeometryProcessor> gp, const GrPipeline* pipeline,
               const GrPipeline::FixedDynamicState* fixedDynamicState, int vertexCount,
               size_t vertexStride, void* vertices, int indexCount, uint16_t* indices) const {
         if (vertexCount == 0 || indexCount == 0) {
             return;
         }
         const GrBuffer* vertexBuffer;
-        GrMesh mesh(GrPrimitiveType::kTriangles);
         int firstVertex;
         void* verts = target->makeVertexSpace(vertexStride, vertexCount, &vertexBuffer,
                                               &firstVertex);
@@ -235,10 +234,11 @@
             return;
         }
         memcpy(idxs, indices, indexCount * sizeof(uint16_t));
-        mesh.setIndexed(indexBuffer, indexCount, firstIndex, 0, vertexCount - 1,
-                        GrPrimitiveRestart::kNo);
-        mesh.setVertexData(vertexBuffer, firstVertex);
-        target->draw(gp, pipeline, fixedDynamicState, mesh);
+        GrMesh* mesh = target->allocMesh(GrPrimitiveType::kTriangles);
+        mesh->setIndexed(indexBuffer, indexCount, firstIndex, 0, vertexCount - 1,
+                         GrPrimitiveRestart::kNo);
+        mesh->setVertexData(vertexBuffer, firstVertex);
+        target->draw(std::move(gp), pipeline, fixedDynamicState, mesh);
     }
 
     void onPrepareDraws(Target* target) override {
@@ -279,7 +279,7 @@
             if (vertexCount + currentVertices > static_cast<int>(UINT16_MAX)) {
                 // if we added the current instance, we would overflow the indices we can store in a
                 // uint16_t. Draw what we've got so far and reset.
-                this->draw(target, gp.get(), pipe.fPipeline, pipe.fFixedDynamicState, vertexCount,
+                this->draw(target, gp, pipe.fPipeline, pipe.fFixedDynamicState, vertexCount,
                            vertexStride, vertices, indexCount, indices);
                 vertexCount = 0;
                 indexCount = 0;
@@ -311,22 +311,22 @@
             indexCount += currentIndices;
         }
         if (vertexCount <= SK_MaxS32 && indexCount <= SK_MaxS32) {
-            this->draw(target, gp.get(), pipe.fPipeline, pipe.fFixedDynamicState, vertexCount,
+            this->draw(target, std::move(gp), pipe.fPipeline, pipe.fFixedDynamicState, vertexCount,
                        vertexStride, vertices, indexCount, indices);
         }
         sk_free(vertices);
         sk_free(indices);
     }
 
-    bool onCombineIfPossible(GrOp* t, const GrCaps& caps) override {
+    CombineResult onCombineIfPossible(GrOp* t, const GrCaps& caps) override {
         AAFlatteningConvexPathOp* that = t->cast<AAFlatteningConvexPathOp>();
         if (!fHelper.isCompatible(that->fHelper, caps, this->bounds(), that->bounds())) {
-            return false;
+            return CombineResult::kCannotCombine;
         }
 
         fPaths.push_back_n(that->fPaths.count(), that->fPaths.begin());
         this->joinBounds(*that);
-        return true;
+        return CombineResult::kMerged;
     }
 
     const SkMatrix& viewMatrix() const { return fPaths[0].fViewMatrix; }
diff --git a/src/gpu/ops/GrAAStrokeRectOp.cpp b/src/gpu/ops/GrAAStrokeRectOp.cpp
index 92d1c01..0362946 100644
--- a/src/gpu/ops/GrAAStrokeRectOp.cpp
+++ b/src/gpu/ops/GrAAStrokeRectOp.cpp
@@ -227,7 +227,7 @@
     const SkMatrix& viewMatrix() const { return fViewMatrix; }
     bool miterStroke() const { return fMiterStroke; }
 
-    bool onCombineIfPossible(GrOp* t, const GrCaps&) override;
+    CombineResult onCombineIfPossible(GrOp* t, const GrCaps&) override;
 
     void generateAAStrokeRectGeometry(void* vertices,
                                       size_t offset,
@@ -282,11 +282,11 @@
     int indicesPerInstance = this->miterStroke() ? kMiterIndexCnt : kBevelIndexCnt;
     int instanceCount = fRects.count();
 
-    sk_sp<const GrBuffer> indexBuffer = GetIndexBuffer(target->resourceProvider(), this->miterStroke());
-    PatternHelper helper(GrPrimitiveType::kTriangles);
-    void* vertices =
-            helper.init(target, vertexStride, indexBuffer.get(),
-                        verticesPerInstance, indicesPerInstance, instanceCount);
+    sk_sp<const GrBuffer> indexBuffer =
+            GetIndexBuffer(target->resourceProvider(), this->miterStroke());
+    PatternHelper helper(target, GrPrimitiveType::kTriangles, vertexStride, indexBuffer.get(),
+                         verticesPerInstance, indicesPerInstance, instanceCount);
+    void* vertices = helper.vertices();
     if (!vertices || !indexBuffer) {
         SkDebugf("Could not allocate vertices\n");
         return;
@@ -308,7 +308,7 @@
                                            fHelper.compatibleWithAlphaAsCoverage());
     }
     auto pipe = fHelper.makePipeline(target);
-    helper.recordDraw(target, gp.get(), pipe.fPipeline, pipe.fFixedDynamicState);
+    helper.recordDraw(target, std::move(gp), pipe.fPipeline, pipe.fFixedDynamicState);
 }
 
 sk_sp<const GrBuffer> AAStrokeRectOp::GetIndexBuffer(GrResourceProvider* resourceProvider,
@@ -405,27 +405,27 @@
     }
 }
 
-bool AAStrokeRectOp::onCombineIfPossible(GrOp* t, const GrCaps& caps) {
+GrOp::CombineResult AAStrokeRectOp::onCombineIfPossible(GrOp* t, const GrCaps& caps) {
     AAStrokeRectOp* that = t->cast<AAStrokeRectOp>();
 
     if (!fHelper.isCompatible(that->fHelper, caps, this->bounds(), that->bounds())) {
-        return false;
+        return CombineResult::kCannotCombine;
     }
 
     // TODO combine across miterstroke changes
     if (this->miterStroke() != that->miterStroke()) {
-        return false;
+        return CombineResult::kCannotCombine;
     }
 
     // We apply the viewmatrix to the rect points on the cpu.  However, if the pipeline uses
     // local coords then we won't be able to combine. TODO: Upload local coords as an attribute.
     if (fHelper.usesLocalCoords() && !this->viewMatrix().cheapEqualTo(that->viewMatrix())) {
-        return false;
+        return CombineResult::kCannotCombine;
     }
 
     fRects.push_back_n(that->fRects.count(), that->fRects.begin());
     this->joinBounds(*that);
-    return true;
+    return CombineResult::kMerged;
 }
 
 static void setup_scale(int* scale, SkScalar inset) {
diff --git a/src/gpu/ops/GrAtlasTextOp.cpp b/src/gpu/ops/GrAtlasTextOp.cpp
index cfd0c45..ba39b97 100644
--- a/src/gpu/ops/GrAtlasTextOp.cpp
+++ b/src/gpu/ops/GrAtlasTextOp.cpp
@@ -295,9 +295,16 @@
     }
     SkASSERT(proxies[0]);
 
+    static constexpr int kMaxTextures = GrBitmapTextGeoProc::kMaxTextures;
+    GR_STATIC_ASSERT(GrDistanceFieldA8TextGeoProc::kMaxTextures == kMaxTextures);
+    GR_STATIC_ASSERT(GrDistanceFieldLCDTextGeoProc::kMaxTextures == kMaxTextures);
+
     static const uint32_t kPipelineFlags = 0;
     auto pipe = target->makePipeline(kPipelineFlags, std::move(fProcessors),
-                                     target->detachAppliedClip());
+                                     target->detachAppliedClip(), kMaxTextures);
+    for (unsigned i = 0; i < numActiveProxies; ++i) {
+        pipe.fFixedDynamicState->fPrimitiveProcessorTextures[i] = proxies[i].get();
+    }
 
     FlushInfo flushInfo;
     flushInfo.fPipeline = pipe.fPipeline;
@@ -400,6 +407,9 @@
     if (gp->numTextureSamplers() != (int) numActiveProxies) {
         // During preparation the number of atlas pages has increased.
         // Update the proxies used in the GP to match.
+        for (unsigned i = gp->numTextureSamplers(); i < numActiveProxies; ++i) {
+            flushInfo->fFixedDynamicState->fPrimitiveProcessorTextures[i] = proxies[i].get();
+        }
         if (this->usesDistanceFields()) {
             if (this->isLCD()) {
                 reinterpret_cast<GrDistanceFieldLCDTextGeoProc*>(gp)->addNewProxies(
@@ -415,62 +425,60 @@
                                                                       samplerState);
         }
     }
-
-    GrMesh mesh(GrPrimitiveType::kTriangles);
     int maxGlyphsPerDraw =
             static_cast<int>(flushInfo->fIndexBuffer->gpuMemorySize() / sizeof(uint16_t) / 6);
-    mesh.setIndexedPatterned(flushInfo->fIndexBuffer.get(), kIndicesPerGlyph, kVerticesPerGlyph,
-                             flushInfo->fGlyphsToFlush, maxGlyphsPerDraw);
-    mesh.setVertexData(flushInfo->fVertexBuffer.get(), flushInfo->fVertexOffset);
-    target->draw(flushInfo->fGeometryProcessor.get(), flushInfo->fPipeline,
-                 flushInfo->fFixedDynamicState, mesh);
+    GrMesh* mesh = target->allocMesh(GrPrimitiveType::kTriangles);
+    mesh->setIndexedPatterned(flushInfo->fIndexBuffer.get(), kIndicesPerGlyph, kVerticesPerGlyph,
+                              flushInfo->fGlyphsToFlush, maxGlyphsPerDraw);
+    mesh->setVertexData(flushInfo->fVertexBuffer.get(), flushInfo->fVertexOffset);
+    target->draw(flushInfo->fGeometryProcessor, flushInfo->fPipeline, flushInfo->fFixedDynamicState,
+                 mesh);
     flushInfo->fVertexOffset += kVerticesPerGlyph * flushInfo->fGlyphsToFlush;
     flushInfo->fGlyphsToFlush = 0;
 }
 
-bool GrAtlasTextOp::onCombineIfPossible(GrOp* t, const GrCaps& caps) {
+GrOp::CombineResult GrAtlasTextOp::onCombineIfPossible(GrOp* t, const GrCaps& caps) {
     GrAtlasTextOp* that = t->cast<GrAtlasTextOp>();
     if (fProcessors != that->fProcessors) {
-        return false;
+        return CombineResult::kCannotCombine;
     }
 
     if (!fCanCombineOnTouchOrOverlap && GrRectsTouchOrOverlap(this->bounds(), that->bounds())) {
-        return false;
+        return CombineResult::kCannotCombine;
     }
 
     if (fMaskType != that->fMaskType) {
-        return false;
+        return CombineResult::kCannotCombine;
     }
 
     const SkMatrix& thisFirstMatrix = fGeoData[0].fViewMatrix;
     const SkMatrix& thatFirstMatrix = that->fGeoData[0].fViewMatrix;
 
     if (this->usesLocalCoords() && !thisFirstMatrix.cheapEqualTo(thatFirstMatrix)) {
-        return false;
+        return CombineResult::kCannotCombine;
     }
 
     if (fNeedsGlyphTransform != that->fNeedsGlyphTransform) {
-        return false;
+        return CombineResult::kCannotCombine;
     }
 
     if (fNeedsGlyphTransform &&
         (thisFirstMatrix.hasPerspective() != thatFirstMatrix.hasPerspective())) {
-        return false;
+        return CombineResult::kCannotCombine;
     }
 
     if (this->usesDistanceFields()) {
         if (fDFGPFlags != that->fDFGPFlags) {
-            return false;
+            return CombineResult::kCannotCombine;
         }
 
         if (fLuminanceColor != that->fLuminanceColor) {
-            return false;
+            return CombineResult::kCannotCombine;
         }
     } else {
         if (kColorBitmapMask_MaskType == fMaskType && this->color() != that->color()) {
-            return false;
+            return CombineResult::kCannotCombine;
         }
-
     }
 
     // Keep the batch vertex buffer size below 32K so we don't have to create a special one
@@ -478,7 +486,7 @@
     static const int kVertexSize = sizeof(SkPoint) + sizeof(SkColor) + 2 * sizeof(uint16_t);
     static const int kMaxGlyphs = 32768 / (kVerticesPerGlyph * kVertexSize);
     if (this->fNumGlyphs + that->fNumGlyphs > kMaxGlyphs) {
-        return false;
+        return CombineResult::kCannotCombine;
     }
 
     fNumGlyphs += that->numGlyphs();
@@ -508,7 +516,7 @@
     fGeoCount = newGeoCount;
 
     this->joinBounds(*that);
-    return true;
+    return CombineResult::kMerged;
 }
 
 // TODO trying to figure out why lcd is so whack
diff --git a/src/gpu/ops/GrAtlasTextOp.h b/src/gpu/ops/GrAtlasTextOp.h
index 14f7e95..e6e08dc 100644
--- a/src/gpu/ops/GrAtlasTextOp.h
+++ b/src/gpu/ops/GrAtlasTextOp.h
@@ -107,7 +107,7 @@
         sk_sp<const GrBuffer> fIndexBuffer;
         sk_sp<GrGeometryProcessor> fGeometryProcessor;
         const GrPipeline* fPipeline;
-        const GrPipeline::FixedDynamicState* fFixedDynamicState;
+        GrPipeline::FixedDynamicState* fFixedDynamicState;
         int fGlyphsToFlush;
         int fVertexOffset;
     };
@@ -149,7 +149,7 @@
     bool usesLocalCoords() const { return fUsesLocalCoords; }
     int numGlyphs() const { return fNumGlyphs; }
 
-    bool onCombineIfPossible(GrOp* t, const GrCaps& caps) override;
+    CombineResult onCombineIfPossible(GrOp* t, const GrCaps& caps) override;
 
     sk_sp<GrGeometryProcessor> setupDfProcessor(const sk_sp<GrTextureProxy>* proxies,
                                                 unsigned int numActiveProxies) const;
diff --git a/src/gpu/ops/GrClearOp.h b/src/gpu/ops/GrClearOp.h
index 6e76191..e62667d 100644
--- a/src/gpu/ops/GrClearOp.h
+++ b/src/gpu/ops/GrClearOp.h
@@ -62,23 +62,23 @@
         this->setBounds(SkRect::Make(rect), HasAABloat::kNo, IsZeroArea::kNo);
     }
 
-    bool onCombineIfPossible(GrOp* t, const GrCaps& caps) override {
+    CombineResult onCombineIfPossible(GrOp* t, const GrCaps& caps) override {
         // This could be much more complicated. Currently we look at cases where the new clear
         // contains the old clear, or when the new clear is a subset of the old clear and is the
         // same color.
         GrClearOp* cb = t->cast<GrClearOp>();
         if (fClip.windowRectsState() != cb->fClip.windowRectsState()) {
-            return false;
+            return CombineResult::kCannotCombine;
         }
         if (cb->contains(this)) {
             fClip = cb->fClip;
             this->replaceBounds(*t);
             fColor = cb->fColor;
-            return true;
+            return CombineResult::kMerged;
         } else if (cb->fColor == fColor && this->contains(cb)) {
-            return true;
+            return CombineResult::kMerged;
         }
-        return false;
+        return CombineResult::kCannotCombine;
     }
 
     bool contains(const GrClearOp* that) const {
diff --git a/src/gpu/ops/GrClearStencilClipOp.h b/src/gpu/ops/GrClearStencilClipOp.h
index 3e7ad50..5861fca 100644
--- a/src/gpu/ops/GrClearStencilClipOp.h
+++ b/src/gpu/ops/GrClearStencilClipOp.h
@@ -52,8 +52,6 @@
         this->setBounds(bounds, HasAABloat::kNo, IsZeroArea::kNo);
     }
 
-    bool onCombineIfPossible(GrOp* t, const GrCaps& caps) override { return false; }
-
     void onPrepare(GrOpFlushState*) override {}
 
     void onExecute(GrOpFlushState* state) override;
diff --git a/src/gpu/ops/GrCopySurfaceOp.h b/src/gpu/ops/GrCopySurfaceOp.h
index bc0e33f..b93cbb7 100644
--- a/src/gpu/ops/GrCopySurfaceOp.h
+++ b/src/gpu/ops/GrCopySurfaceOp.h
@@ -53,8 +53,6 @@
         this->setBounds(bounds, HasAABloat::kNo, IsZeroArea::kNo);
     }
 
-    bool onCombineIfPossible(GrOp* that, const GrCaps& caps) override { return false; }
-
     void onPrepare(GrOpFlushState*) override {}
 
     void onExecute(GrOpFlushState* state) override;
diff --git a/src/gpu/ops/GrDashOp.cpp b/src/gpu/ops/GrDashOp.cpp
index 43acc5c..075df70 100644
--- a/src/gpu/ops/GrDashOp.cpp
+++ b/src/gpu/ops/GrDashOp.cpp
@@ -625,7 +625,6 @@
             return;
         }
 
-        QuadHelper helper;
         size_t vertexStride;
         if (fullDash) {
             vertexStride =
@@ -634,7 +633,8 @@
             vertexStride = sizeof(SkPoint);
         }
         SkASSERT(vertexStride == gp->debugOnly_vertexStride());
-        void* vertices = helper.init(target, vertexStride, totalRectCount);
+        QuadHelper helper(target, vertexStride, totalRectCount);
+        void* vertices = helper.vertices();
         if (!vertices) {
             return;
         }
@@ -696,43 +696,43 @@
         }
         auto pipe = target->makePipeline(pipelineFlags, std::move(fProcessorSet),
                                          target->detachAppliedClip());
-        helper.recordDraw(target, gp.get(), pipe.fPipeline, pipe.fFixedDynamicState);
+        helper.recordDraw(target, std::move(gp), pipe.fPipeline, pipe.fFixedDynamicState);
     }
 
-    bool onCombineIfPossible(GrOp* t, const GrCaps& caps) override {
+    CombineResult onCombineIfPossible(GrOp* t, const GrCaps& caps) override {
         DashOp* that = t->cast<DashOp>();
         if (fProcessorSet != that->fProcessorSet) {
-            return false;
+            return CombineResult::kCannotCombine;
         }
         if (fDisallowCombineOnTouchOrOverlap &&
             GrRectsTouchOrOverlap(this->bounds(), that->bounds())) {
-            return false;
+            return CombineResult::kCannotCombine;
         }
 
         if (this->aaMode() != that->aaMode()) {
-            return false;
+            return CombineResult::kCannotCombine;
         }
 
         if (this->fullDash() != that->fullDash()) {
-            return false;
+            return CombineResult::kCannotCombine;
         }
 
         if (this->cap() != that->cap()) {
-            return false;
+            return CombineResult::kCannotCombine;
         }
 
         // TODO vertex color
         if (this->color() != that->color()) {
-            return false;
+            return CombineResult::kCannotCombine;
         }
 
         if (fUsesLocalCoords && !this->viewMatrix().cheapEqualTo(that->viewMatrix())) {
-            return false;
+            return CombineResult::kCannotCombine;
         }
 
         fLines.push_back_n(that->fLines.count(), that->fLines.begin());
         this->joinBounds(*that);
-        return true;
+        return CombineResult::kMerged;
     }
 
     GrColor color() const { return fColor; }
diff --git a/src/gpu/ops/GrDebugMarkerOp.h b/src/gpu/ops/GrDebugMarkerOp.h
index 061f676..96c5477 100644
--- a/src/gpu/ops/GrDebugMarkerOp.h
+++ b/src/gpu/ops/GrDebugMarkerOp.h
@@ -39,8 +39,6 @@
         this->makeFullScreen(proxy);
     }
 
-    bool onCombineIfPossible(GrOp* that, const GrCaps& caps) override { return false; }
-
     void onPrepare(GrOpFlushState*) override {}
 
     void onExecute(GrOpFlushState* state) override;
diff --git a/src/gpu/ops/GrDefaultPathRenderer.cpp b/src/gpu/ops/GrDefaultPathRenderer.cpp
index 07e40f0..4e038df 100644
--- a/src/gpu/ops/GrDefaultPathRenderer.cpp
+++ b/src/gpu/ops/GrDefaultPathRenderer.cpp
@@ -65,12 +65,12 @@
 class PathGeoBuilder {
 public:
     PathGeoBuilder(GrPrimitiveType primitiveType, GrMeshDrawOp::Target* target,
-                   GrGeometryProcessor* geometryProcessor, const GrPipeline* pipeline,
+                   sk_sp<const GrGeometryProcessor> geometryProcessor, const GrPipeline* pipeline,
                    const GrPipeline::FixedDynamicState* fixedDynamicState)
-            : fMesh(primitiveType)
+            : fPrimitiveType(primitiveType)
             , fTarget(target)
             , fVertexStride(sizeof(SkPoint))
-            , fGeometryProcessor(geometryProcessor)
+            , fGeometryProcessor(std::move(geometryProcessor))
             , fPipeline(pipeline)
             , fFixedDynamicState(fixedDynamicState)
             , fIndexBuffer(nullptr)
@@ -200,15 +200,15 @@
      *  TODO: Cache some of these for better performance, rather than re-computing?
      */
     bool isIndexed() const {
-        return GrPrimitiveType::kLines == fMesh.primitiveType() ||
-               GrPrimitiveType::kTriangles == fMesh.primitiveType();
+        return GrPrimitiveType::kLines == fPrimitiveType ||
+               GrPrimitiveType::kTriangles == fPrimitiveType;
     }
     bool isHairline() const {
-        return GrPrimitiveType::kLines == fMesh.primitiveType() ||
-               GrPrimitiveType::kLineStrip == fMesh.primitiveType();
+        return GrPrimitiveType::kLines == fPrimitiveType ||
+               GrPrimitiveType::kLineStrip == fPrimitiveType;
     }
     int indexScale() const {
-        switch (fMesh.primitiveType()) {
+        switch (fPrimitiveType) {
             case GrPrimitiveType::kLines:
                 return 2;
             case GrPrimitiveType::kTriangles:
@@ -271,14 +271,15 @@
         SkASSERT(indexCount <= fIndicesInChunk);
 
         if (this->isIndexed() ? SkToBool(indexCount) : SkToBool(vertexCount)) {
+            GrMesh* mesh = fTarget->allocMesh(fPrimitiveType);
             if (!this->isIndexed()) {
-                fMesh.setNonIndexedNonInstanced(vertexCount);
+                mesh->setNonIndexedNonInstanced(vertexCount);
             } else {
-                fMesh.setIndexed(fIndexBuffer, indexCount, fFirstIndex, 0, vertexCount - 1,
+                mesh->setIndexed(fIndexBuffer, indexCount, fFirstIndex, 0, vertexCount - 1,
                                  GrPrimitiveRestart::kNo);
             }
-            fMesh.setVertexData(fVertexBuffer, fFirstVertex);
-            fTarget->draw(fGeometryProcessor, fPipeline, fFixedDynamicState, fMesh);
+            mesh->setVertexData(fVertexBuffer, fFirstVertex);
+            fTarget->draw(fGeometryProcessor, fPipeline, fFixedDynamicState, mesh);
         }
 
         fTarget->putBackIndices((size_t)(fIndicesInChunk - indexCount));
@@ -311,10 +312,10 @@
         }
     }
 
-    GrMesh fMesh;
+    GrPrimitiveType fPrimitiveType;
     GrMeshDrawOp::Target* fTarget;
     size_t fVertexStride;
-    GrGeometryProcessor* fGeometryProcessor;
+    sk_sp<const GrGeometryProcessor> fGeometryProcessor;
     const GrPipeline* fPipeline;
     const GrPipeline::FixedDynamicState* fFixedDynamicState;
 
@@ -428,7 +429,7 @@
             primitiveType = GrPrimitiveType::kTriangles;
         }
         auto pipe = fHelper.makePipeline(target);
-        PathGeoBuilder pathGeoBuilder(primitiveType, target, gp.get(), pipe.fPipeline,
+        PathGeoBuilder pathGeoBuilder(primitiveType, target, std::move(gp), pipe.fPipeline,
                                       pipe.fFixedDynamicState);
 
         // fill buffers
@@ -438,31 +439,31 @@
         }
     }
 
-    bool onCombineIfPossible(GrOp* t, const GrCaps& caps) override {
+    CombineResult onCombineIfPossible(GrOp* t, const GrCaps& caps) override {
         DefaultPathOp* that = t->cast<DefaultPathOp>();
         if (!fHelper.isCompatible(that->fHelper, caps, this->bounds(), that->bounds())) {
-            return false;
+            return CombineResult::kCannotCombine;
         }
 
         if (this->color() != that->color()) {
-            return false;
+            return CombineResult::kCannotCombine;
         }
 
         if (this->coverage() != that->coverage()) {
-            return false;
+            return CombineResult::kCannotCombine;
         }
 
         if (!this->viewMatrix().cheapEqualTo(that->viewMatrix())) {
-            return false;
+            return CombineResult::kCannotCombine;
         }
 
         if (this->isHairline() != that->isHairline()) {
-            return false;
+            return CombineResult::kCannotCombine;
         }
 
         fPaths.push_back_n(that->fPaths.count(), that->fPaths.begin());
         this->joinBounds(*that);
-        return true;
+        return CombineResult::kMerged;
     }
 
     GrColor color() const { return fColor; }
diff --git a/src/gpu/ops/GrDrawAtlasOp.cpp b/src/gpu/ops/GrDrawAtlasOp.cpp
index d280ad9..107b0be 100644
--- a/src/gpu/ops/GrDrawAtlasOp.cpp
+++ b/src/gpu/ops/GrDrawAtlasOp.cpp
@@ -130,9 +130,9 @@
             sizeof(SkPoint) + sizeof(SkPoint) + (this->hasColors() ? sizeof(GrColor) : 0);
     SkASSERT(vertexStride == gp->debugOnly_vertexStride());
 
-    QuadHelper helper;
     int numQuads = this->quadCount();
-    void* verts = helper.init(target, vertexStride, numQuads);
+    QuadHelper helper(target, vertexStride, numQuads);
+    void* verts = helper.vertices();
     if (!verts) {
         SkDebugf("Could not allocate vertices\n");
         return;
@@ -147,34 +147,34 @@
         vertPtr += allocSize;
     }
     auto pipe = fHelper.makePipeline(target);
-    helper.recordDraw(target, gp.get(), pipe.fPipeline, pipe.fFixedDynamicState);
+    helper.recordDraw(target, std::move(gp), pipe.fPipeline, pipe.fFixedDynamicState);
 }
 
-bool GrDrawAtlasOp::onCombineIfPossible(GrOp* t, const GrCaps& caps) {
+GrOp::CombineResult GrDrawAtlasOp::onCombineIfPossible(GrOp* t, const GrCaps& caps) {
     GrDrawAtlasOp* that = t->cast<GrDrawAtlasOp>();
 
     if (!fHelper.isCompatible(that->fHelper, caps, this->bounds(), that->bounds())) {
-        return false;
+        return CombineResult::kCannotCombine;
     }
 
     // We currently use a uniform viewmatrix for this op.
     if (!this->viewMatrix().cheapEqualTo(that->viewMatrix())) {
-        return false;
+        return CombineResult::kCannotCombine;
     }
 
     if (this->hasColors() != that->hasColors()) {
-        return false;
+        return CombineResult::kCannotCombine;
     }
 
     if (!this->hasColors() && this->color() != that->color()) {
-        return false;
+        return CombineResult::kCannotCombine;
     }
 
     fGeoData.push_back_n(that->fGeoData.count(), that->fGeoData.begin());
     fQuadCount += that->quadCount();
 
     this->joinBounds(*that);
-    return true;
+    return CombineResult::kMerged;
 }
 
 GrDrawOp::FixedFunctionFlags GrDrawAtlasOp::fixedFunctionFlags() const {
diff --git a/src/gpu/ops/GrDrawAtlasOp.h b/src/gpu/ops/GrDrawAtlasOp.h
index 4e894ce..d15bd3c 100644
--- a/src/gpu/ops/GrDrawAtlasOp.h
+++ b/src/gpu/ops/GrDrawAtlasOp.h
@@ -56,7 +56,7 @@
     bool hasColors() const { return fHasColors; }
     int quadCount() const { return fQuadCount; }
 
-    bool onCombineIfPossible(GrOp* t, const GrCaps&) override;
+    CombineResult onCombineIfPossible(GrOp* t, const GrCaps&) override;
 
     struct Geometry {
         GrColor fColor;
diff --git a/src/gpu/ops/GrDrawPathOp.h b/src/gpu/ops/GrDrawPathOp.h
index 63986d7..3016fd7 100644
--- a/src/gpu/ops/GrDrawPathOp.h
+++ b/src/gpu/ops/GrDrawPathOp.h
@@ -93,8 +93,6 @@
         this->setTransformedBounds(path->getBounds(), viewMatrix, HasAABloat::kNo, IsZeroArea::kNo);
     }
 
-    bool onCombineIfPossible(GrOp* t, const GrCaps& caps) override { return false; }
-
     void onExecute(GrOpFlushState* state) override;
 
     GrPendingIOResource<const GrPath, kRead_GrIOType> fPath;
diff --git a/src/gpu/ops/GrDrawVerticesOp.cpp b/src/gpu/ops/GrDrawVerticesOp.cpp
index d227920..dd91452 100644
--- a/src/gpu/ops/GrDrawVerticesOp.cpp
+++ b/src/gpu/ops/GrDrawVerticesOp.cpp
@@ -261,7 +261,7 @@
                       indices);
 
     // Draw the vertices.
-    this->drawVertices(target, gp.get(), vertexBuffer, firstVertex, indexBuffer, firstIndex);
+    this->drawVertices(target, std::move(gp), vertexBuffer, firstVertex, indexBuffer, firstIndex);
 }
 
 void GrDrawVerticesOp::drawNonVolatile(Target* target) {
@@ -298,7 +298,7 @@
 
     // Draw using the cached buffers if possible.
     if (vertexBuffer && (!this->isIndexed() || indexBuffer)) {
-        this->drawVertices(target, gp.get(), vertexBuffer.get(), 0, indexBuffer.get(), 0);
+        this->drawVertices(target, std::move(gp), vertexBuffer.get(), 0, indexBuffer.get(), 0);
         return;
     }
 
@@ -353,7 +353,7 @@
     rp->assignUniqueKeyToResource(indexKey, indexBuffer.get());
 
     // Draw the vertices.
-    this->drawVertices(target, gp.get(), vertexBuffer.get(), 0, indexBuffer.get(), 0);
+    this->drawVertices(target, std::move(gp), vertexBuffer.get(), 0, indexBuffer.get(), 0);
 }
 
 void GrDrawVerticesOp::fillBuffers(bool hasColorAttribute,
@@ -465,59 +465,58 @@
 }
 
 void GrDrawVerticesOp::drawVertices(Target* target,
-                                    GrGeometryProcessor* gp,
+                                    sk_sp<const GrGeometryProcessor> gp,
                                     const GrBuffer* vertexBuffer,
                                     int firstVertex,
                                     const GrBuffer* indexBuffer,
                                     int firstIndex) {
-    GrMesh mesh(this->primitiveType());
+    GrMesh* mesh = target->allocMesh(this->primitiveType());
     if (this->isIndexed()) {
-        mesh.setIndexed(indexBuffer, fIndexCount,
-                        firstIndex, 0, fVertexCount - 1,
-                        GrPrimitiveRestart::kNo);
+        mesh->setIndexed(indexBuffer, fIndexCount, firstIndex, 0, fVertexCount - 1,
+                         GrPrimitiveRestart::kNo);
     } else {
-        mesh.setNonIndexedNonInstanced(fVertexCount);
+        mesh->setNonIndexedNonInstanced(fVertexCount);
     }
-    mesh.setVertexData(vertexBuffer, firstVertex);
+    mesh->setVertexData(vertexBuffer, firstVertex);
     auto pipe = fHelper.makePipeline(target);
-    target->draw(gp, pipe.fPipeline, pipe.fFixedDynamicState, mesh);
+    target->draw(std::move(gp), pipe.fPipeline, pipe.fFixedDynamicState, mesh);
 }
 
-bool GrDrawVerticesOp::onCombineIfPossible(GrOp* t, const GrCaps& caps) {
+GrOp::CombineResult GrDrawVerticesOp::onCombineIfPossible(GrOp* t, const GrCaps& caps) {
     GrDrawVerticesOp* that = t->cast<GrDrawVerticesOp>();
 
     if (!fHelper.isCompatible(that->fHelper, caps, this->bounds(), that->bounds())) {
-        return false;
+        return CombineResult::kCannotCombine;
     }
 
     // Meshes with bones cannot be combined because different meshes use different bones, so to
     // combine them, the matrices would have to be combined, and the bone indices on each vertex
     // would change, thus making the vertices uncacheable.
     if (this->hasBones() || that->hasBones()) {
-        return false;
+        return CombineResult::kCannotCombine;
     }
 
     // Non-volatile meshes cannot batch, because if a non-volatile mesh batches with another mesh,
     // then on the next frame, if that non-volatile mesh is drawn, it will draw the other mesh
     // that was saved in its vertex buffer, which is not necessarily there anymore.
     if (!this->fMeshes[0].fVertices->isVolatile() || !that->fMeshes[0].fVertices->isVolatile()) {
-        return false;
+        return CombineResult::kCannotCombine;
     }
 
     if (!this->combinablePrimitive() || this->primitiveType() != that->primitiveType()) {
-        return false;
+        return CombineResult::kCannotCombine;
     }
 
     if (fMeshes[0].fVertices->hasIndices() != that->fMeshes[0].fVertices->hasIndices()) {
-        return false;
+        return CombineResult::kCannotCombine;
     }
 
     if (fColorArrayType != that->fColorArrayType) {
-        return false;
+        return CombineResult::kCannotCombine;
     }
 
     if (fVertexCount + that->fVertexCount > SkTo<int>(UINT16_MAX)) {
-        return false;
+        return CombineResult::kCannotCombine;
     }
 
     // NOTE: For SkColor vertex colors, the source color space is always sRGB, and the destination
@@ -542,7 +541,7 @@
     fIndexCount += that->fIndexCount;
 
     this->joinBounds(*that);
-    return true;
+    return CombineResult::kMerged;
 }
 
 ///////////////////////////////////////////////////////////////////////////////////////////////////
diff --git a/src/gpu/ops/GrDrawVerticesOp.h b/src/gpu/ops/GrDrawVerticesOp.h
index 7695dd9..07227e0 100644
--- a/src/gpu/ops/GrDrawVerticesOp.h
+++ b/src/gpu/ops/GrDrawVerticesOp.h
@@ -80,7 +80,7 @@
                      uint16_t* indices) const;
 
     void drawVertices(Target*,
-                      GrGeometryProcessor*,
+                      sk_sp<const GrGeometryProcessor>,
                       const GrBuffer* vertexBuffer,
                       int firstVertex,
                       const GrBuffer* indexBuffer,
@@ -98,7 +98,7 @@
                GrPrimitiveType::kPoints == fPrimitiveType;
     }
 
-    bool onCombineIfPossible(GrOp* t, const GrCaps&) override;
+    CombineResult onCombineIfPossible(GrOp* t, const GrCaps&) override;
 
     struct Mesh {
         GrColor fColor;  // Used if this->hasPerVertexColors() is false.
diff --git a/src/gpu/ops/GrLatticeOp.cpp b/src/gpu/ops/GrLatticeOp.cpp
index 00dbef5..4832ee7 100644
--- a/src/gpu/ops/GrLatticeOp.cpp
+++ b/src/gpu/ops/GrLatticeOp.cpp
@@ -32,10 +32,10 @@
         GrColor fColor;
     };
 
-    static sk_sp<GrGeometryProcessor> Make(sk_sp<GrTextureProxy> proxy,
+    static sk_sp<GrGeometryProcessor> Make(const GrTextureProxy* proxy,
                                            sk_sp<GrColorSpaceXform> csxf,
                                            GrSamplerState::Filter filter) {
-        return sk_sp<GrGeometryProcessor>(new LatticeGP(std::move(proxy), std::move(csxf), filter));
+        return sk_sp<GrGeometryProcessor>(new LatticeGP(proxy, std::move(csxf), filter));
     }
 
     const char* name() const override { return "LatticeGP"; }
@@ -92,10 +92,10 @@
     }
 
 private:
-    LatticeGP(sk_sp<GrTextureProxy> proxy, sk_sp<GrColorSpaceXform> csxf,
+    LatticeGP(const GrTextureProxy* proxy, sk_sp<GrColorSpaceXform> csxf,
               GrSamplerState::Filter filter)
             : INHERITED(kLatticeGP_ClassID), fColorSpaceXform(std::move(csxf)) {
-        fSampler.reset(std::move(proxy), filter);
+        fSampler.reset(proxy->textureType(), proxy->config(), filter);
         this->setTextureSamplerCnt(1);
         this->setVertexAttributeCnt(4);
     }
@@ -202,7 +202,7 @@
 
 private:
     void onPrepareDraws(Target* target) override {
-        auto gp = LatticeGP::Make(fProxy, fColorSpaceXform, fFilter);
+        auto gp = LatticeGP::Make(fProxy.get(), fColorSpaceXform, fFilter);
         if (!gp) {
             SkDebugf("Couldn't create GrGeometryProcessor\n");
             return;
@@ -223,9 +223,9 @@
         }
 
         sk_sp<const GrBuffer> indexBuffer = target->resourceProvider()->refQuadIndexBuffer();
-        PatternHelper helper(GrPrimitiveType::kTriangles);
-        void* vertices = helper.init(target, kVertexStide, indexBuffer.get(), kVertsPerRect,
-                                     kIndicesPerRect, numRects);
+        PatternHelper helper(target, GrPrimitiveType::kTriangles, kVertexStide, indexBuffer.get(),
+                             kVertsPerRect, kIndicesPerRect, numRects);
+        void* vertices = helper.vertices();
         if (!vertices || !indexBuffer) {
             SkDebugf("Could not allocate vertices\n");
             return;
@@ -281,28 +281,29 @@
                                                   kVertsPerRect * patch.fIter->numRectsToDraw());
             }
         }
-        auto pipe = fHelper.makePipeline(target);
-        helper.recordDraw(target, gp.get(), pipe.fPipeline, pipe.fFixedDynamicState);
+        auto pipe = fHelper.makePipeline(target, 1);
+        pipe.fFixedDynamicState->fPrimitiveProcessorTextures[0] = fProxy.get();
+        helper.recordDraw(target, std::move(gp), pipe.fPipeline, pipe.fFixedDynamicState);
     }
 
-    bool onCombineIfPossible(GrOp* t, const GrCaps& caps) override {
+    CombineResult onCombineIfPossible(GrOp* t, const GrCaps& caps) override {
         NonAALatticeOp* that = t->cast<NonAALatticeOp>();
         if (fProxy != that->fProxy) {
-            return false;
+            return CombineResult::kCannotCombine;
         }
         if (fFilter != that->fFilter) {
-            return false;
+            return CombineResult::kCannotCombine;
         }
         if (GrColorSpaceXform::Equals(fColorSpaceXform.get(), that->fColorSpaceXform.get())) {
-            return false;
+            return CombineResult::kCannotCombine;
         }
         if (!fHelper.isCompatible(that->fHelper, caps, this->bounds(), that->bounds())) {
-            return false;
+            return CombineResult::kCannotCombine;
         }
 
         fPatches.move_back_n(that->fPatches.count(), that->fPatches.begin());
         this->joinBounds(*that);
-        return true;
+        return CombineResult::kMerged;
     }
 
     struct Patch {
diff --git a/src/gpu/ops/GrMeshDrawOp.cpp b/src/gpu/ops/GrMeshDrawOp.cpp
index 0bac5ef..620ea47 100644
--- a/src/gpu/ops/GrMeshDrawOp.cpp
+++ b/src/gpu/ops/GrMeshDrawOp.cpp
@@ -14,56 +14,68 @@
 
 void GrMeshDrawOp::onPrepare(GrOpFlushState* state) { this->onPrepareDraws(state); }
 
-void* GrMeshDrawOp::PatternHelper::init(Target* target, size_t vertexStride,
-                                        const GrBuffer* indexBuffer, int verticesPerRepetition,
-                                        int indicesPerRepetition, int repeatCount) {
-    SkASSERT(target);
-    if (!indexBuffer) {
-        return nullptr;
-    }
-    const GrBuffer* vertexBuffer;
-    int firstVertex;
-    int vertexCount = verticesPerRepetition * repeatCount;
-    void* vertices =
-            target->makeVertexSpace(vertexStride, vertexCount, &vertexBuffer, &firstVertex);
-    if (!vertices) {
-        SkDebugf("Vertices could not be allocated for instanced rendering.");
-        return nullptr;
-    }
-    SkASSERT(vertexBuffer);
-    size_t ibSize = indexBuffer->gpuMemorySize();
-    int maxRepetitions = static_cast<int>(ibSize / (sizeof(uint16_t) * indicesPerRepetition));
-
-    fMesh.setIndexedPatterned(indexBuffer, indicesPerRepetition, verticesPerRepetition,
-                              repeatCount, maxRepetitions);
-    fMesh.setVertexData(vertexBuffer, firstVertex);
-    return vertices;
-}
-
-void GrMeshDrawOp::PatternHelper::recordDraw(
-        Target* target, const GrGeometryProcessor* gp, const GrPipeline* pipeline,
-        const GrPipeline::FixedDynamicState* fixedDynamicState) {
-    target->draw(gp, pipeline, fixedDynamicState, fMesh);
-}
-
-void* GrMeshDrawOp::QuadHelper::init(Target* target, size_t vertexStride, int quadsToDraw) {
-    sk_sp<const GrBuffer> quadIndexBuffer = target->resourceProvider()->refQuadIndexBuffer();
-    if (!quadIndexBuffer) {
-        SkDebugf("Could not get quad index buffer.");
-        return nullptr;
-    }
-    return this->INHERITED::init(target, vertexStride, quadIndexBuffer.get(), kVerticesPerQuad,
-                                 kIndicesPerQuad, quadsToDraw);
-}
-
 void GrMeshDrawOp::onExecute(GrOpFlushState* state) {
     state->executeDrawsAndUploadsForMeshDrawOp(this->uniqueID(), this->bounds());
 }
 
 //////////////////////////////////////////////////////////////////////////////
 
+GrMeshDrawOp::PatternHelper::PatternHelper(Target* target, GrPrimitiveType primitiveType,
+                                           size_t vertexStride, const GrBuffer* indexBuffer,
+                                           int verticesPerRepetition, int indicesPerRepetition,
+                                           int repeatCount) {
+    this->init(target, primitiveType, vertexStride, indexBuffer, verticesPerRepetition,
+               indicesPerRepetition, repeatCount);
+}
+
+void GrMeshDrawOp::PatternHelper::init(Target* target, GrPrimitiveType primitiveType,
+                                       size_t vertexStride, const GrBuffer* indexBuffer,
+                                       int verticesPerRepetition, int indicesPerRepetition,
+                                       int repeatCount) {
+    SkASSERT(target);
+    if (!indexBuffer) {
+        return;
+    }
+    const GrBuffer* vertexBuffer;
+    int firstVertex;
+    int vertexCount = verticesPerRepetition * repeatCount;
+    fVertices = target->makeVertexSpace(vertexStride, vertexCount, &vertexBuffer, &firstVertex);
+    if (!fVertices) {
+        SkDebugf("Vertices could not be allocated for patterned rendering.");
+        return;
+    }
+    SkASSERT(vertexBuffer);
+    size_t ibSize = indexBuffer->gpuMemorySize();
+    int maxRepetitions = static_cast<int>(ibSize / (sizeof(uint16_t) * indicesPerRepetition));
+    fMesh = target->allocMesh(primitiveType);
+    fMesh->setIndexedPatterned(indexBuffer, indicesPerRepetition, verticesPerRepetition,
+                               repeatCount, maxRepetitions);
+    fMesh->setVertexData(vertexBuffer, firstVertex);
+}
+
+void GrMeshDrawOp::PatternHelper::recordDraw(
+        Target* target, sk_sp<const GrGeometryProcessor> gp, const GrPipeline* pipeline,
+        const GrPipeline::FixedDynamicState* fixedDynamicState) const {
+    target->draw(std::move(gp), pipeline, fixedDynamicState, fMesh);
+}
+
+//////////////////////////////////////////////////////////////////////////////
+
+GrMeshDrawOp::QuadHelper::QuadHelper(Target* target, size_t vertexStride, int quadsToDraw) {
+    sk_sp<const GrBuffer> quadIndexBuffer = target->resourceProvider()->refQuadIndexBuffer();
+    if (!quadIndexBuffer) {
+        SkDebugf("Could not get quad index buffer.");
+        return;
+    }
+    this->init(target, GrPrimitiveType::kTriangles, vertexStride, quadIndexBuffer.get(),
+               kVerticesPerQuad, kIndicesPerQuad, quadsToDraw);
+}
+
+//////////////////////////////////////////////////////////////////////////////
+
 GrMeshDrawOp::Target::PipelineAndFixedDynamicState GrMeshDrawOp::Target::makePipeline(
-        uint32_t pipelineFlags, GrProcessorSet&& processorSet, GrAppliedClip&& clip) {
+        uint32_t pipelineFlags, GrProcessorSet&& processorSet, GrAppliedClip&& clip,
+        int numPrimProcTextures) {
     GrPipeline::InitArgs pipelineArgs;
     pipelineArgs.fFlags = pipelineFlags;
     pipelineArgs.fProxy = this->proxy();
@@ -71,8 +83,12 @@
     pipelineArgs.fCaps = &this->caps();
     pipelineArgs.fResourceProvider = this->resourceProvider();
     GrPipeline::FixedDynamicState* fixedDynamicState = nullptr;
-    if (clip.scissorState().enabled()) {
+    if (clip.scissorState().enabled() || numPrimProcTextures) {
         fixedDynamicState = this->allocFixedDynamicState(clip.scissorState().rect());
+        if (numPrimProcTextures) {
+            fixedDynamicState->fPrimitiveProcessorTextures =
+                    this->allocPrimitiveProcessorTextureArray(numPrimProcTextures);
+        }
     }
     return {this->allocPipeline(pipelineArgs, std::move(processorSet), std::move(clip)),
             fixedDynamicState};
diff --git a/src/gpu/ops/GrMeshDrawOp.h b/src/gpu/ops/GrMeshDrawOp.h
index e90de47..a6e7da5 100644
--- a/src/gpu/ops/GrMeshDrawOp.h
+++ b/src/gpu/ops/GrMeshDrawOp.h
@@ -12,7 +12,6 @@
 #include "GrDrawOp.h"
 #include "GrGeometryProcessor.h"
 #include "GrMesh.h"
-#include "GrPendingProgramElement.h"
 
 class GrAtlasManager;
 class GrCaps;
@@ -34,18 +33,23 @@
         space for the vertices and flushes the draws to the GrMeshDrawOp::Target. */
     class PatternHelper {
     public:
-        PatternHelper(GrPrimitiveType primitiveType) : fMesh(primitiveType) {}
-        /** Returns the allocated storage for the vertices. The caller should populate the vertices
-            before calling recordDraws(). */
-        void* init(Target*, size_t vertexStride, const GrBuffer*, int verticesPerRepetition,
-                   int indicesPerRepetition, int repeatCount);
+        PatternHelper(Target*, GrPrimitiveType, size_t vertexStride, const GrBuffer*,
+                      int verticesPerRepetition, int indicesPerRepetition, int repeatCount);
 
-        /** Call after init() to issue draws to the GrMeshDrawOp::Target.*/
-        void recordDraw(Target*, const GrGeometryProcessor*, const GrPipeline*,
-                        const GrPipeline::FixedDynamicState*);
+        /** Called to issue draws to the GrMeshDrawOp::Target.*/
+        void recordDraw(Target*, sk_sp<const GrGeometryProcessor>, const GrPipeline*,
+                        const GrPipeline::FixedDynamicState*) const;
+
+        void* vertices() const { return fVertices; }
+
+    protected:
+        PatternHelper() = default;
+        void init(Target*, GrPrimitiveType, size_t vertexStride, const GrBuffer*,
+                  int verticesPerRepetition, int indicesPerRepetition, int repeatCount);
 
     private:
-        GrMesh fMesh;
+        void* fVertices = nullptr;
+        GrMesh* fMesh = nullptr;
     };
 
     static const int kVerticesPerQuad = 4;
@@ -54,13 +58,11 @@
     /** A specialization of InstanceHelper for quad rendering. */
     class QuadHelper : private PatternHelper {
     public:
-        QuadHelper() : INHERITED(GrPrimitiveType::kTriangles) {}
-        /** Finds the cached quad index buffer and reserves vertex space. Returns nullptr on failure
-            and on success a pointer to the vertex data that the caller should populate before
-            calling recordDraws(). */
-        void* init(Target*, size_t vertexStride, int quadsToDraw);
+        QuadHelper() = delete;
+        QuadHelper(Target* target, size_t vertexStride, int quadsToDraw);
 
         using PatternHelper::recordDraw;
+        using PatternHelper::vertices;
 
     private:
         typedef PatternHelper INHERITED;
@@ -78,8 +80,19 @@
     virtual ~Target() {}
 
     /** Adds a draw of a mesh. */
-    virtual void draw(const GrGeometryProcessor*, const GrPipeline*,
-                      const GrPipeline::FixedDynamicState*, const GrMesh&) = 0;
+    virtual void draw(sk_sp<const GrGeometryProcessor>,
+                      const GrPipeline*,
+                      const GrPipeline::FixedDynamicState*,
+                      const GrMesh[],
+                      int meshCount) = 0;
+
+    /** Helper for drawing a single GrMesh. */
+    void draw(sk_sp<const GrGeometryProcessor> gp,
+              const GrPipeline* pipeline,
+              const GrPipeline::FixedDynamicState* fixedDynamicState,
+              const GrMesh* mesh) {
+        this->draw(std::move(gp), pipeline, fixedDynamicState, mesh, 1);
+    }
 
     /**
      * Makes space for vertex data. The returned pointer is the location where vertex data
@@ -132,10 +145,25 @@
         return this->pipelineArena()->make<GrPipeline>(std::forward<Args>(args)...);
     }
 
-    template <typename... Args>
-    GrPipeline::FixedDynamicState* allocFixedDynamicState(Args&... args) {
-        return this->pipelineArena()->make<GrPipeline::FixedDynamicState>(
-                std::forward<Args>(args)...);
+    GrMesh* allocMesh(GrPrimitiveType primitiveType) {
+        return this->pipelineArena()->make<GrMesh>(primitiveType);
+    }
+
+    GrMesh* allocMeshes(int n) { return this->pipelineArena()->makeArray<GrMesh>(n); }
+
+    GrPipeline::FixedDynamicState* allocFixedDynamicState(const SkIRect& rect,
+                                                          int numPrimitiveProcessorTextures = 0) {
+        auto result = this->pipelineArena()->make<GrPipeline::FixedDynamicState>(rect);
+        if (numPrimitiveProcessorTextures) {
+            result->fPrimitiveProcessorTextures =
+                    this->allocPrimitiveProcessorTextureArray(numPrimitiveProcessorTextures);
+        }
+        return result;
+    }
+
+    GrTextureProxy** allocPrimitiveProcessorTextureArray(int n) {
+        SkASSERT(n > 0);
+        return this->pipelineArena()->makeArrayDefault<GrTextureProxy*>(n);
     }
 
     // Once we have C++17 structured bindings make this just be a tuple because then we can do:
@@ -144,7 +172,7 @@
     //      std::tie(flushInfo.fPipeline, flushInfo.fFixedState) = target->makePipeline(...);
     struct PipelineAndFixedDynamicState {
         const GrPipeline* fPipeline;
-        const GrPipeline::FixedDynamicState* fFixedDynamicState;
+        GrPipeline::FixedDynamicState* fFixedDynamicState;
     };
 
     /**
@@ -152,7 +180,8 @@
      * GrAppliedClip and uses a fixed dynamic state.
      */
     PipelineAndFixedDynamicState makePipeline(uint32_t pipelineFlags, GrProcessorSet&&,
-                                              GrAppliedClip&&);
+                                              GrAppliedClip&&,
+                                              int numPrimitiveProcessorTextures = 0);
 
     virtual GrRenderTargetProxy* proxy() const = 0;
 
diff --git a/src/gpu/ops/GrNonAAFillRectOp.cpp b/src/gpu/ops/GrNonAAFillRectOp.cpp
index c554232..fd8a9b3 100644
--- a/src/gpu/ops/GrNonAAFillRectOp.cpp
+++ b/src/gpu/ops/GrNonAAFillRectOp.cpp
@@ -192,9 +192,9 @@
         int rectCount = fRects.count();
 
         sk_sp<const GrBuffer> indexBuffer = target->resourceProvider()->refQuadIndexBuffer();
-        PatternHelper helper(GrPrimitiveType::kTriangles);
-        void* vertices = helper.init(target, kVertexStride, indexBuffer.get(), kVertsPerRect,
-                                     kIndicesPerRect, rectCount);
+        PatternHelper helper(target, GrPrimitiveType::kTriangles, kVertexStride, indexBuffer.get(),
+                             kVertsPerRect, kIndicesPerRect, rectCount);
+        void* vertices = helper.vertices();
         if (!vertices || !indexBuffer) {
             SkDebugf("Could not allocate vertices\n");
             return;
@@ -207,17 +207,17 @@
                       fRects[i].fRect, &fRects[i].fLocalQuad);
         }
         auto pipe = fHelper.makePipeline(target);
-        helper.recordDraw(target, gp.get(), pipe.fPipeline, pipe.fFixedDynamicState);
+        helper.recordDraw(target, std::move(gp), pipe.fPipeline, pipe.fFixedDynamicState);
     }
 
-    bool onCombineIfPossible(GrOp* t, const GrCaps& caps) override {
+    CombineResult onCombineIfPossible(GrOp* t, const GrCaps& caps) override {
         NonAAFillRectOp* that = t->cast<NonAAFillRectOp>();
         if (!fHelper.isCompatible(that->fHelper, caps, this->bounds(), that->bounds())) {
-            return false;
+            return CombineResult::kCannotCombine;
         }
         fRects.push_back_n(that->fRects.count(), that->fRects.begin());
         this->joinBounds(*that);
-        return true;
+        return CombineResult::kMerged;
     }
 
     struct RectInfo {
@@ -325,9 +325,9 @@
         int rectCount = fRects.count();
 
         sk_sp<const GrBuffer> indexBuffer = target->resourceProvider()->refQuadIndexBuffer();
-        PatternHelper helper(GrPrimitiveType::kTriangles);
-        void* vertices = helper.init(target, vertexStride, indexBuffer.get(), kVertsPerRect,
-                                     kIndicesPerRect, rectCount);
+        PatternHelper helper(target, GrPrimitiveType::kTriangles, vertexStride, indexBuffer.get(),
+                             kVertsPerRect, kIndicesPerRect, rectCount);
+        void* vertices = helper.vertices();
         if (!vertices || !indexBuffer) {
             SkDebugf("Could not allocate vertices\n");
             return;
@@ -345,29 +345,29 @@
             }
         }
         auto pipe = fHelper.makePipeline(target);
-        helper.recordDraw(target, gp.get(), pipe.fPipeline, pipe.fFixedDynamicState);
+        helper.recordDraw(target, std::move(gp), pipe.fPipeline, pipe.fFixedDynamicState);
     }
 
-    bool onCombineIfPossible(GrOp* t, const GrCaps& caps) override {
+    CombineResult onCombineIfPossible(GrOp* t, const GrCaps& caps) override {
         NonAAFillRectPerspectiveOp* that = t->cast<NonAAFillRectPerspectiveOp>();
         if (!fHelper.isCompatible(that->fHelper, caps, this->bounds(), that->bounds())) {
-            return false;
+            return CombineResult::kCannotCombine;
         }
 
         // We could combine across perspective vm changes if we really wanted to.
         if (!fViewMatrix.cheapEqualTo(that->fViewMatrix)) {
-            return false;
+            return CombineResult::kCannotCombine;
         }
         if (fHasLocalRect != that->fHasLocalRect) {
-            return false;
+            return CombineResult::kCannotCombine;
         }
         if (fHasLocalMatrix && !fLocalMatrix.cheapEqualTo(that->fLocalMatrix)) {
-            return false;
+            return CombineResult::kCannotCombine;
         }
 
         fRects.push_back_n(that->fRects.count(), that->fRects.begin());
         this->joinBounds(*that);
-        return true;
+        return CombineResult::kMerged;
     }
 
     struct RectInfo {
diff --git a/src/gpu/ops/GrNonAAStrokeRectOp.cpp b/src/gpu/ops/GrNonAAStrokeRectOp.cpp
index 21636b2..500b2eb 100644
--- a/src/gpu/ops/GrNonAAStrokeRectOp.cpp
+++ b/src/gpu/ops/GrNonAAStrokeRectOp.cpp
@@ -188,18 +188,14 @@
             vertex[4].set(fRect.fLeft, fRect.fTop);
         }
 
-        GrMesh mesh(primType);
-        mesh.setNonIndexedNonInstanced(vertexCount);
-        mesh.setVertexData(vertexBuffer, firstVertex);
+        GrMesh* mesh = target->allocMesh(primType);
+        mesh->setNonIndexedNonInstanced(vertexCount);
+        mesh->setVertexData(vertexBuffer, firstVertex);
         auto pipe = fHelper.makePipeline(target);
-        target->draw(gp.get(), pipe.fPipeline, pipe.fFixedDynamicState, mesh);
+        target->draw(std::move(gp), pipe.fPipeline, pipe.fFixedDynamicState, mesh);
     }
 
-    bool onCombineIfPossible(GrOp* t, const GrCaps&) override {
-        // NonAA stroke rects cannot combine right now
-        // TODO make these combinable.
-        return false;
-    }
+    // TODO: override onCombineIfPossible
 
     Helper fHelper;
     GrColor fColor;
diff --git a/src/gpu/ops/GrOp.h b/src/gpu/ops/GrOp.h
index 5c776fc..57e028e 100644
--- a/src/gpu/ops/GrOp.h
+++ b/src/gpu/ops/GrOp.h
@@ -75,9 +75,21 @@
         // This default implementation assumes the op has no proxies
     }
 
-    bool combineIfPossible(GrOp* that, const GrCaps& caps) {
+    enum class CombineResult {
+        /**
+         * The op that combineIfPossible was called on now represents its own work plus that of
+         * the passed op. The passed op should be destroyed without being flushed.
+         */
+        kMerged,
+        /**
+         * The ops cannot be combined.
+         */
+        kCannotCombine
+    };
+
+    CombineResult combineIfPossible(GrOp* that, const GrCaps& caps) {
         if (this->classID() != that->classID()) {
-            return false;
+            return CombineResult::kCannotCombine;
         }
 
         return this->onCombineIfPossible(that, caps);
@@ -211,7 +223,9 @@
     static uint32_t GenOpClassID() { return GenID(&gCurrOpClassID); }
 
 private:
-    virtual bool onCombineIfPossible(GrOp*, const GrCaps& caps) = 0;
+    virtual CombineResult onCombineIfPossible(GrOp*, const GrCaps&) {
+        return CombineResult::kCannotCombine;
+    }
 
     virtual void onPrepare(GrOpFlushState*) = 0;
     virtual void onExecute(GrOpFlushState*) = 0;
diff --git a/src/gpu/ops/GrOvalOpFactory.cpp b/src/gpu/ops/GrOvalOpFactory.cpp
index 54bb37c..c0158ba 100644
--- a/src/gpu/ops/GrOvalOpFactory.cpp
+++ b/src/gpu/ops/GrOvalOpFactory.cpp
@@ -1470,29 +1470,29 @@
             vertices += circle_type_to_vert_count(circle.fStroked) * vertexStride;
         }
 
-        GrMesh mesh(GrPrimitiveType::kTriangles);
-        mesh.setIndexed(indexBuffer, fIndexCount, firstIndex, 0, fVertCount - 1,
-                        GrPrimitiveRestart::kNo);
-        mesh.setVertexData(vertexBuffer, firstVertex);
+        GrMesh* mesh = target->allocMesh(GrPrimitiveType::kTriangles);
+        mesh->setIndexed(indexBuffer, fIndexCount, firstIndex, 0, fVertCount - 1,
+                         GrPrimitiveRestart::kNo);
+        mesh->setVertexData(vertexBuffer, firstVertex);
         auto pipe = fHelper.makePipeline(target);
-        target->draw(gp.get(), pipe.fPipeline, pipe.fFixedDynamicState, mesh);
+        target->draw(std::move(gp), pipe.fPipeline, pipe.fFixedDynamicState, mesh);
     }
 
-    bool onCombineIfPossible(GrOp* t, const GrCaps& caps) override {
+    CombineResult onCombineIfPossible(GrOp* t, const GrCaps& caps) override {
         CircleOp* that = t->cast<CircleOp>();
 
         // can only represent 65535 unique vertices with 16-bit indices
         if (fVertCount + that->fVertCount > 65536) {
-            return false;
+            return CombineResult::kCannotCombine;
         }
 
         if (!fHelper.isCompatible(that->fHelper, caps, this->bounds(), that->bounds())) {
-            return false;
+            return CombineResult::kCannotCombine;
         }
 
         if (fHelper.usesLocalCoords() &&
             !fViewMatrixIfUsingLocalCoords.cheapEqualTo(that->fViewMatrixIfUsingLocalCoords)) {
-            return false;
+            return CombineResult::kCannotCombine;
         }
 
         // Because we've set up the ops that don't use the planes with noop values
@@ -1507,7 +1507,7 @@
         fVertCount += that->fVertCount;
         fIndexCount += that->fIndexCount;
         fAllFill = fAllFill && that->fAllFill;
-        return true;
+        return CombineResult::kMerged;
     }
 
     struct Circle {
@@ -1786,36 +1786,36 @@
             vertices += circle_type_to_vert_count(true) * kVertexStride;
         }
 
-        GrMesh mesh(GrPrimitiveType::kTriangles);
-        mesh.setIndexed(indexBuffer, fIndexCount, firstIndex, 0, fVertCount - 1,
-                        GrPrimitiveRestart::kNo);
-        mesh.setVertexData(vertexBuffer, firstVertex);
+        GrMesh* mesh = target->allocMesh(GrPrimitiveType::kTriangles);
+        mesh->setIndexed(indexBuffer, fIndexCount, firstIndex, 0, fVertCount - 1,
+                         GrPrimitiveRestart::kNo);
+        mesh->setVertexData(vertexBuffer, firstVertex);
         auto pipe = fHelper.makePipeline(target);
-        target->draw(gp.get(), pipe.fPipeline, pipe.fFixedDynamicState, mesh);
+        target->draw(std::move(gp), pipe.fPipeline, pipe.fFixedDynamicState, mesh);
     }
 
-    bool onCombineIfPossible(GrOp* t, const GrCaps& caps) override {
+    CombineResult onCombineIfPossible(GrOp* t, const GrCaps& caps) override {
         ButtCapDashedCircleOp* that = t->cast<ButtCapDashedCircleOp>();
 
         // can only represent 65535 unique vertices with 16-bit indices
         if (fVertCount + that->fVertCount > 65536) {
-            return false;
+            return CombineResult::kCannotCombine;
         }
 
         if (!fHelper.isCompatible(that->fHelper, caps, this->bounds(), that->bounds())) {
-            return false;
+            return CombineResult::kCannotCombine;
         }
 
         if (fHelper.usesLocalCoords() &&
             !fViewMatrixIfUsingLocalCoords.cheapEqualTo(that->fViewMatrixIfUsingLocalCoords)) {
-            return false;
+            return CombineResult::kCannotCombine;
         }
 
         fCircles.push_back_n(that->fCircles.count(), that->fCircles.begin());
         this->joinBounds(*that);
         fVertCount += that->fVertCount;
         fIndexCount += that->fIndexCount;
-        return true;
+        return CombineResult::kMerged;
     }
 
     struct Circle {
@@ -1984,10 +1984,9 @@
         // Setup geometry processor
         sk_sp<GrGeometryProcessor> gp(new EllipseGeometryProcessor(fStroked, localMatrix));
 
-        QuadHelper helper;
         SkASSERT(sizeof(EllipseVertex) == gp->debugOnly_vertexStride());
-        EllipseVertex* verts = reinterpret_cast<EllipseVertex*>(
-                helper.init(target, sizeof(EllipseVertex), fEllipses.count()));
+        QuadHelper helper(target, sizeof(EllipseVertex), fEllipses.count());
+        EllipseVertex* verts = reinterpret_cast<EllipseVertex*>(helper.vertices());
         if (!verts) {
             return;
         }
@@ -2040,28 +2039,28 @@
             verts += kVerticesPerQuad;
         }
         auto pipe = fHelper.makePipeline(target);
-        helper.recordDraw(target, gp.get(), pipe.fPipeline, pipe.fFixedDynamicState);
+        helper.recordDraw(target, std::move(gp), pipe.fPipeline, pipe.fFixedDynamicState);
     }
 
-    bool onCombineIfPossible(GrOp* t, const GrCaps& caps) override {
+    CombineResult onCombineIfPossible(GrOp* t, const GrCaps& caps) override {
         EllipseOp* that = t->cast<EllipseOp>();
 
         if (!fHelper.isCompatible(that->fHelper, caps, this->bounds(), that->bounds())) {
-            return false;
+            return CombineResult::kCannotCombine;
         }
 
         if (fStroked != that->fStroked) {
-            return false;
+            return CombineResult::kCannotCombine;
         }
 
         if (fHelper.usesLocalCoords() &&
             !fViewMatrixIfUsingLocalCoords.cheapEqualTo(that->fViewMatrixIfUsingLocalCoords)) {
-            return false;
+            return CombineResult::kCannotCombine;
         }
 
         fEllipses.push_back_n(that->fEllipses.count(), that->fEllipses.begin());
         this->joinBounds(*that);
-        return true;
+        return CombineResult::kMerged;
     }
 
     struct Ellipse {
@@ -2219,9 +2218,8 @@
                 new DIEllipseGeometryProcessor(this->viewMatrix(), this->style()));
 
         SkASSERT(sizeof(DIEllipseVertex) == gp->debugOnly_vertexStride());
-        QuadHelper helper;
-        DIEllipseVertex* verts = reinterpret_cast<DIEllipseVertex*>(
-                helper.init(target, sizeof(DIEllipseVertex), fEllipses.count()));
+        QuadHelper helper(target, sizeof(DIEllipseVertex), fEllipses.count());
+        DIEllipseVertex* verts = reinterpret_cast<DIEllipseVertex*>(helper.vertices());
         if (!verts) {
             return;
         }
@@ -2274,27 +2272,27 @@
             verts += kVerticesPerQuad;
         }
         auto pipe = fHelper.makePipeline(target);
-        helper.recordDraw(target, gp.get(), pipe.fPipeline, pipe.fFixedDynamicState);
+        helper.recordDraw(target, std::move(gp), pipe.fPipeline, pipe.fFixedDynamicState);
     }
 
-    bool onCombineIfPossible(GrOp* t, const GrCaps& caps) override {
+    CombineResult onCombineIfPossible(GrOp* t, const GrCaps& caps) override {
         DIEllipseOp* that = t->cast<DIEllipseOp>();
         if (!fHelper.isCompatible(that->fHelper, caps, this->bounds(), that->bounds())) {
-            return false;
+            return CombineResult::kCannotCombine;
         }
 
         if (this->style() != that->style()) {
-            return false;
+            return CombineResult::kCannotCombine;
         }
 
         // TODO rewrite to allow positioning on CPU
         if (!this->viewMatrix().cheapEqualTo(that->viewMatrix())) {
-            return false;
+            return CombineResult::kCannotCombine;
         }
 
         fEllipses.push_back_n(that->fEllipses.count(), that->fEllipses.begin());
         this->joinBounds(*that);
-        return true;
+        return CombineResult::kMerged;
     }
 
     const SkMatrix& viewMatrix() const { return fEllipses[0].fViewMatrix; }
@@ -2725,29 +2723,29 @@
             currStartVertex += rrect_type_to_vert_count(rrect.fType);
         }
 
-        GrMesh mesh(GrPrimitiveType::kTriangles);
-        mesh.setIndexed(indexBuffer, fIndexCount, firstIndex, 0, fVertCount - 1,
-                        GrPrimitiveRestart::kNo);
-        mesh.setVertexData(vertexBuffer, firstVertex);
+        GrMesh* mesh = target->allocMesh(GrPrimitiveType::kTriangles);
+        mesh->setIndexed(indexBuffer, fIndexCount, firstIndex, 0, fVertCount - 1,
+                         GrPrimitiveRestart::kNo);
+        mesh->setVertexData(vertexBuffer, firstVertex);
         auto pipe = fHelper.makePipeline(target);
-        target->draw(gp.get(), pipe.fPipeline, pipe.fFixedDynamicState, mesh);
+        target->draw(std::move(gp), pipe.fPipeline, pipe.fFixedDynamicState, mesh);
     }
 
-    bool onCombineIfPossible(GrOp* t, const GrCaps& caps) override {
+    CombineResult onCombineIfPossible(GrOp* t, const GrCaps& caps) override {
         CircularRRectOp* that = t->cast<CircularRRectOp>();
 
         // can only represent 65535 unique vertices with 16-bit indices
         if (fVertCount + that->fVertCount > 65536) {
-            return false;
+            return CombineResult::kCannotCombine;
         }
 
         if (!fHelper.isCompatible(that->fHelper, caps, this->bounds(), that->bounds())) {
-            return false;
+            return CombineResult::kCannotCombine;
         }
 
         if (fHelper.usesLocalCoords() &&
             !fViewMatrixIfUsingLocalCoords.cheapEqualTo(that->fViewMatrixIfUsingLocalCoords)) {
-            return false;
+            return CombineResult::kCannotCombine;
         }
 
         fRRects.push_back_n(that->fRRects.count(), that->fRRects.begin());
@@ -2755,7 +2753,7 @@
         fVertCount += that->fVertCount;
         fIndexCount += that->fIndexCount;
         fAllFill = fAllFill && that->fAllFill;
-        return true;
+        return CombineResult::kMerged;
     }
 
     struct RRect {
@@ -2927,10 +2925,10 @@
         sk_sp<const GrBuffer> indexBuffer = get_rrect_index_buffer(
                 fStroked ? kStroke_RRectType : kFill_RRectType, target->resourceProvider());
 
-        PatternHelper helper(GrPrimitiveType::kTriangles);
-        EllipseVertex* verts = reinterpret_cast<EllipseVertex*>(
-                helper.init(target, sizeof(EllipseVertex), indexBuffer.get(),
-                            kVertsPerStandardRRect, indicesPerInstance, fRRects.count()));
+        PatternHelper helper(target, GrPrimitiveType::kTriangles, sizeof(EllipseVertex),
+                             indexBuffer.get(), kVertsPerStandardRRect, indicesPerInstance,
+                             fRRects.count());
+        EllipseVertex* verts = reinterpret_cast<EllipseVertex*>(helper.vertices());
         if (!verts || !indexBuffer) {
             SkDebugf("Could not allocate vertices\n");
             return;
@@ -2997,28 +2995,28 @@
             }
         }
         auto pipe = fHelper.makePipeline(target);
-        helper.recordDraw(target, gp.get(), pipe.fPipeline, pipe.fFixedDynamicState);
+        helper.recordDraw(target, std::move(gp), pipe.fPipeline, pipe.fFixedDynamicState);
     }
 
-    bool onCombineIfPossible(GrOp* t, const GrCaps& caps) override {
+    CombineResult onCombineIfPossible(GrOp* t, const GrCaps& caps) override {
         EllipticalRRectOp* that = t->cast<EllipticalRRectOp>();
 
         if (!fHelper.isCompatible(that->fHelper, caps, this->bounds(), that->bounds())) {
-            return false;
+            return CombineResult::kCannotCombine;
         }
 
         if (fStroked != that->fStroked) {
-            return false;
+            return CombineResult::kCannotCombine;
         }
 
         if (fHelper.usesLocalCoords() &&
             !fViewMatrixIfUsingLocalCoords.cheapEqualTo(that->fViewMatrixIfUsingLocalCoords)) {
-            return false;
+            return CombineResult::kCannotCombine;
         }
 
         fRRects.push_back_n(that->fRRects.count(), that->fRRects.begin());
         this->joinBounds(*that);
-        return true;
+        return CombineResult::kMerged;
     }
 
     struct RRect {
diff --git a/src/gpu/ops/GrRegionOp.cpp b/src/gpu/ops/GrRegionOp.cpp
index d0c24e6..b876295 100644
--- a/src/gpu/ops/GrRegionOp.cpp
+++ b/src/gpu/ops/GrRegionOp.cpp
@@ -129,9 +129,9 @@
             return;
         }
         sk_sp<const GrBuffer> indexBuffer = target->resourceProvider()->refQuadIndexBuffer();
-        PatternHelper helper(GrPrimitiveType::kTriangles);
-        void* vertices = helper.init(target, kVertexStride, indexBuffer.get(), kVertsPerInstance,
-                                     kIndicesPerInstance, numRects);
+        PatternHelper helper(target, GrPrimitiveType::kTriangles, kVertexStride, indexBuffer.get(),
+                             kVertsPerInstance, kIndicesPerInstance, numRects);
+        void* vertices = helper.vertices();
         if (!vertices || !indexBuffer) {
             SkDebugf("Could not allocate vertices\n");
             return;
@@ -144,22 +144,22 @@
             verts += numRectsInRegion * kVertsPerInstance * kVertexStride;
         }
         auto pipe = fHelper.makePipeline(target);
-        helper.recordDraw(target, gp.get(), pipe.fPipeline, pipe.fFixedDynamicState);
+        helper.recordDraw(target, std::move(gp), pipe.fPipeline, pipe.fFixedDynamicState);
     }
 
-    bool onCombineIfPossible(GrOp* t, const GrCaps& caps) override {
+    CombineResult onCombineIfPossible(GrOp* t, const GrCaps& caps) override {
         RegionOp* that = t->cast<RegionOp>();
         if (!fHelper.isCompatible(that->fHelper, caps, this->bounds(), that->bounds())) {
-            return false;
+            return CombineResult::kCannotCombine;
         }
 
         if (fViewMatrix != that->fViewMatrix) {
-            return false;
+            return CombineResult::kCannotCombine;
         }
 
         fRegions.push_back_n(that->fRegions.count(), that->fRegions.begin());
         this->joinBounds(*that);
-        return true;
+        return CombineResult::kMerged;
     }
 
     struct RegionInfo {
diff --git a/src/gpu/ops/GrSemaphoreOp.h b/src/gpu/ops/GrSemaphoreOp.h
index 234c76c..53c76c3 100644
--- a/src/gpu/ops/GrSemaphoreOp.h
+++ b/src/gpu/ops/GrSemaphoreOp.h
@@ -34,7 +34,6 @@
     sk_sp<GrSemaphore> fSemaphore;
 
 private:
-    bool onCombineIfPossible(GrOp* that, const GrCaps& caps) override { return false; }
     void onPrepare(GrOpFlushState*) override {}
 
     typedef GrOp INHERITED;
diff --git a/src/gpu/ops/GrShadowRRectOp.cpp b/src/gpu/ops/GrShadowRRectOp.cpp
index 09a2442..69489a2 100644
--- a/src/gpu/ops/GrShadowRRectOp.cpp
+++ b/src/gpu/ops/GrShadowRRectOp.cpp
@@ -627,20 +627,20 @@
         auto pipe = target->makePipeline(kPipelineFlags, GrProcessorSet::MakeEmptySet(),
                                          target->detachAppliedClip());
 
-        GrMesh mesh(GrPrimitiveType::kTriangles);
-        mesh.setIndexed(indexBuffer, fIndexCount, firstIndex, 0, fVertCount - 1,
-                        GrPrimitiveRestart::kNo);
-        mesh.setVertexData(vertexBuffer, firstVertex);
-        target->draw(gp.get(), pipe.fPipeline, pipe.fFixedDynamicState, mesh);
+        GrMesh* mesh = target->allocMesh(GrPrimitiveType::kTriangles);
+        mesh->setIndexed(indexBuffer, fIndexCount, firstIndex, 0, fVertCount - 1,
+                         GrPrimitiveRestart::kNo);
+        mesh->setVertexData(vertexBuffer, firstVertex);
+        target->draw(std::move(gp), pipe.fPipeline, pipe.fFixedDynamicState, mesh);
     }
 
-    bool onCombineIfPossible(GrOp* t, const GrCaps& caps) override {
+    CombineResult onCombineIfPossible(GrOp* t, const GrCaps& caps) override {
         ShadowCircularRRectOp* that = t->cast<ShadowCircularRRectOp>();
         fGeoData.push_back_n(that->fGeoData.count(), that->fGeoData.begin());
         this->joinBounds(*that);
         fVertCount += that->fVertCount;
         fIndexCount += that->fIndexCount;
-        return true;
+        return CombineResult::kMerged;
     }
 
     SkSTArray<1, Geometry, true> fGeoData;
diff --git a/src/gpu/ops/GrSimpleMeshDrawOpHelper.cpp b/src/gpu/ops/GrSimpleMeshDrawOpHelper.cpp
index 7ed74c3..f1692e6 100644
--- a/src/gpu/ops/GrSimpleMeshDrawOpHelper.cpp
+++ b/src/gpu/ops/GrSimpleMeshDrawOpHelper.cpp
@@ -135,7 +135,8 @@
 }
 
 auto GrSimpleMeshDrawOpHelper::internalMakePipeline(GrMeshDrawOp::Target* target,
-                                                    const GrPipeline::InitArgs& args)
+                                                    const GrPipeline::InitArgs& args,
+                                                    int numPrimitiveProcessorProxies)
         -> PipelineAndFixedDynamicState {
     // A caller really should only call this once as the processor set and applied clip get
     // moved into the GrPipeline.
@@ -143,8 +144,12 @@
     SkDEBUGCODE(fMadePipeline = true);
     auto clip = target->detachAppliedClip();
     GrPipeline::FixedDynamicState* fixedDynamicState = nullptr;
-    if (clip.scissorState().enabled()) {
+    if (clip.scissorState().enabled() || numPrimitiveProcessorProxies) {
         fixedDynamicState = target->allocFixedDynamicState(clip.scissorState().rect());
+        if (numPrimitiveProcessorProxies) {
+            fixedDynamicState->fPrimitiveProcessorTextures =
+                    target->allocPrimitiveProcessorTextureArray(numPrimitiveProcessorProxies);
+        }
     }
     if (fProcessors) {
         return {target->allocPipeline(args, std::move(*fProcessors), std::move(clip)),
@@ -176,11 +181,12 @@
            fStencilSettings == that.fStencilSettings;
 }
 
-auto GrSimpleMeshDrawOpHelperWithStencil::makePipeline(GrMeshDrawOp::Target* target)
+auto GrSimpleMeshDrawOpHelperWithStencil::makePipeline(GrMeshDrawOp::Target* target,
+                                                       int numPrimitiveProcessorTextures)
         -> PipelineAndFixedDynamicState {
     auto args = INHERITED::pipelineInitArgs(target);
     args.fUserStencil = fStencilSettings;
-    return this->internalMakePipeline(target, args);
+    return this->internalMakePipeline(target, args, numPrimitiveProcessorTextures);
 }
 
 SkString GrSimpleMeshDrawOpHelperWithStencil::dumpInfo() const {
diff --git a/src/gpu/ops/GrSimpleMeshDrawOpHelper.h b/src/gpu/ops/GrSimpleMeshDrawOpHelper.h
index 2bffa09..cd72786 100644
--- a/src/gpu/ops/GrSimpleMeshDrawOpHelper.h
+++ b/src/gpu/ops/GrSimpleMeshDrawOpHelper.h
@@ -88,8 +88,10 @@
 
     using PipelineAndFixedDynamicState = GrOpFlushState::PipelineAndFixedDynamicState;
     /** Makes a pipeline that consumes the processor set and the op's applied clip. */
-    PipelineAndFixedDynamicState makePipeline(GrMeshDrawOp::Target* target) {
-        return this->internalMakePipeline(target, this->pipelineInitArgs(target));
+    PipelineAndFixedDynamicState makePipeline(GrMeshDrawOp::Target* target,
+                                              int numPrimitiveProcessorTextures = 0) {
+        return this->internalMakePipeline(target, this->pipelineInitArgs(target),
+                                          numPrimitiveProcessorTextures);
     }
 
     struct MakeArgs {
@@ -116,7 +118,8 @@
     GrPipeline::InitArgs pipelineInitArgs(GrMeshDrawOp::Target* target) const;
 
     PipelineAndFixedDynamicState internalMakePipeline(GrMeshDrawOp::Target*,
-                                                      const GrPipeline::InitArgs&);
+                                                      const GrPipeline::InitArgs&,
+                                                      int numPrimitiveProcessorTextures);
 
 private:
     GrProcessorSet* fProcessors;
@@ -162,7 +165,8 @@
     bool isCompatible(const GrSimpleMeshDrawOpHelperWithStencil& that, const GrCaps&,
                       const SkRect& thisBounds, const SkRect& thatBounds) const;
 
-    PipelineAndFixedDynamicState makePipeline(GrMeshDrawOp::Target*);
+    PipelineAndFixedDynamicState makePipeline(GrMeshDrawOp::Target*,
+                                              int numPrimitiveProcessorTextures = 0);
 
     SkString dumpInfo() const;
 
diff --git a/src/gpu/ops/GrSmallPathRenderer.cpp b/src/gpu/ops/GrSmallPathRenderer.cpp
index 286fab7..c480bc0 100644
--- a/src/gpu/ops/GrSmallPathRenderer.cpp
+++ b/src/gpu/ops/GrSmallPathRenderer.cpp
@@ -304,7 +304,7 @@
         sk_sp<const GrBuffer> fIndexBuffer;
         sk_sp<GrGeometryProcessor>   fGeometryProcessor;
         const GrPipeline* fPipeline;
-        const GrPipeline::FixedDynamicState* fFixedDynamicState;
+        GrPipeline::FixedDynamicState* fFixedDynamicState;
         int fVertexOffset;
         int fInstancesToFlush;
     };
@@ -312,7 +312,15 @@
     void onPrepareDraws(Target* target) override {
         int instanceCount = fShapes.count();
 
-        auto pipe = fHelper.makePipeline(target);
+        static constexpr int kMaxTextures = GrDistanceFieldPathGeoProc::kMaxTextures;
+        GR_STATIC_ASSERT(GrBitmapTextGeoProc::kMaxTextures == kMaxTextures);
+
+        auto pipe = fHelper.makePipeline(target, kMaxTextures);
+        int numActiveProxies = fAtlas->numActivePages();
+        const auto proxies = fAtlas->getProxies();
+        for (int i = 0; i < numActiveProxies; ++i) {
+            pipe.fFixedDynamicState->fPrimitiveProcessorTextures[i] = proxies[i].get();
+        }
 
         FlushInfo flushInfo;
         flushInfo.fPipeline = pipe.fPipeline;
@@ -807,7 +815,12 @@
 
     void flush(GrMeshDrawOp::Target* target, FlushInfo* flushInfo) const {
         GrGeometryProcessor* gp = flushInfo->fGeometryProcessor.get();
-        if (gp->numTextureSamplers() != (int)fAtlas->numActivePages()) {
+        int numAtlasTextures = SkToInt(fAtlas->numActivePages());
+        auto proxies = fAtlas->getProxies();
+        if (gp->numTextureSamplers() != numAtlasTextures) {
+            for (int i = gp->numTextureSamplers(); i < numAtlasTextures; ++i) {
+                flushInfo->fFixedDynamicState->fPrimitiveProcessorTextures[i] = proxies[i].get();
+            }
             // During preparation the number of atlas pages has increased.
             // Update the proxies used in the GP to match.
             if (fUsesDistanceField) {
@@ -820,14 +833,14 @@
         }
 
         if (flushInfo->fInstancesToFlush) {
-            GrMesh mesh(GrPrimitiveType::kTriangles);
+            GrMesh* mesh = target->allocMesh(GrPrimitiveType::kTriangles);
             int maxInstancesPerDraw =
                 static_cast<int>(flushInfo->fIndexBuffer->gpuMemorySize() / sizeof(uint16_t) / 6);
-            mesh.setIndexedPatterned(flushInfo->fIndexBuffer.get(), kIndicesPerQuad,
-                                     kVerticesPerQuad, flushInfo->fInstancesToFlush,
-                                     maxInstancesPerDraw);
-            mesh.setVertexData(flushInfo->fVertexBuffer.get(), flushInfo->fVertexOffset);
-            target->draw(flushInfo->fGeometryProcessor.get(), flushInfo->fPipeline,
+            mesh->setIndexedPatterned(flushInfo->fIndexBuffer.get(), kIndicesPerQuad,
+                                      kVerticesPerQuad, flushInfo->fInstancesToFlush,
+                                      maxInstancesPerDraw);
+            mesh->setVertexData(flushInfo->fVertexBuffer.get(), flushInfo->fVertexOffset);
+            target->draw(flushInfo->fGeometryProcessor, flushInfo->fPipeline,
                          flushInfo->fFixedDynamicState, mesh);
             flushInfo->fVertexOffset += kVerticesPerQuad * flushInfo->fInstancesToFlush;
             flushInfo->fInstancesToFlush = 0;
@@ -837,41 +850,41 @@
     GrColor color() const { return fShapes[0].fColor; }
     bool usesDistanceField() const { return fUsesDistanceField; }
 
-    bool onCombineIfPossible(GrOp* t, const GrCaps& caps) override {
+    CombineResult onCombineIfPossible(GrOp* t, const GrCaps& caps) override {
         SmallPathOp* that = t->cast<SmallPathOp>();
         if (!fHelper.isCompatible(that->fHelper, caps, this->bounds(), that->bounds())) {
-            return false;
+            return CombineResult::kCannotCombine;
         }
 
         if (this->usesDistanceField() != that->usesDistanceField()) {
-            return false;
+            return CombineResult::kCannotCombine;
         }
 
         const SkMatrix& thisCtm = this->fShapes[0].fViewMatrix;
         const SkMatrix& thatCtm = that->fShapes[0].fViewMatrix;
 
         if (thisCtm.hasPerspective() != thatCtm.hasPerspective()) {
-            return false;
+            return CombineResult::kCannotCombine;
         }
 
         // We can position on the cpu unless we're in perspective,
         // but also need to make sure local matrices are identical
         if ((thisCtm.hasPerspective() || fHelper.usesLocalCoords()) &&
             !thisCtm.cheapEqualTo(thatCtm)) {
-            return false;
+            return CombineResult::kCannotCombine;
         }
 
         // Depending on the ctm we may have a different shader for SDF paths
         if (this->usesDistanceField()) {
             if (thisCtm.isScaleTranslate() != thatCtm.isScaleTranslate() ||
                 thisCtm.isSimilarity() != thatCtm.isSimilarity()) {
-                return false;
+                return CombineResult::kCannotCombine;
             }
         }
 
         fShapes.push_back_n(that->fShapes.count(), that->fShapes.begin());
         this->joinBounds(*that);
-        return true;
+        return CombineResult::kMerged;
     }
 
     bool fUsesDistanceField;
diff --git a/src/gpu/ops/GrStencilPathOp.h b/src/gpu/ops/GrStencilPathOp.h
index 563bdc3..edfcaa8 100644
--- a/src/gpu/ops/GrStencilPathOp.h
+++ b/src/gpu/ops/GrStencilPathOp.h
@@ -56,8 +56,6 @@
         this->setBounds(path->getBounds(), HasAABloat::kNo, IsZeroArea::kNo);
     }
 
-    bool onCombineIfPossible(GrOp* t, const GrCaps& caps) override { return false; }
-
     void onPrepare(GrOpFlushState*) override {}
 
     void onExecute(GrOpFlushState* state) override;
diff --git a/src/gpu/ops/GrTessellatingPathRenderer.cpp b/src/gpu/ops/GrTessellatingPathRenderer.cpp
index 2fea9cb..f722868 100644
--- a/src/gpu/ops/GrTessellatingPathRenderer.cpp
+++ b/src/gpu/ops/GrTessellatingPathRenderer.cpp
@@ -237,7 +237,7 @@
         return path;
     }
 
-    void draw(Target* target, const GrGeometryProcessor* gp, size_t vertexStride) {
+    void draw(Target* target, sk_sp<const GrGeometryProcessor> gp, size_t vertexStride) {
         SkASSERT(!fAntiAlias);
         GrResourceProvider* rp = target->resourceProvider();
         bool inverseFill = fShape.inverseFilled();
@@ -261,7 +261,7 @@
         SkScalar tol = GrPathUtils::kDefaultTolerance;
         tol = GrPathUtils::scaleToleranceToSrc(tol, fViewMatrix, fShape.bounds());
         if (cache_match(cachedVertexBuffer.get(), tol, &actualCount)) {
-            this->drawVertices(target, gp, cachedVertexBuffer.get(), 0, actualCount);
+            this->drawVertices(target, std::move(gp), cachedVertexBuffer.get(), 0, actualCount);
             return;
         }
 
@@ -280,7 +280,7 @@
         if (count == 0) {
             return;
         }
-        this->drawVertices(target, gp, allocator.vertexBuffer(), 0, count);
+        this->drawVertices(target, std::move(gp), allocator.vertexBuffer(), 0, count);
         TessInfo info;
         info.fTolerance = isLinear ? 0 : tol;
         info.fCount = count;
@@ -289,7 +289,7 @@
         fShape.addGenIDChangeListener(sk_make_sp<PathInvalidator>(key, target->contextUniqueID()));
     }
 
-    void drawAA(Target* target, const GrGeometryProcessor* gp, size_t vertexStride) {
+    void drawAA(Target* target, sk_sp<const GrGeometryProcessor> gp, size_t vertexStride) {
         SkASSERT(fAntiAlias);
         SkPath path = getPath();
         if (path.isEmpty()) {
@@ -306,7 +306,8 @@
         if (count == 0) {
             return;
         }
-        this->drawVertices(target, gp, allocator.vertexBuffer(), allocator.firstVertex(), count);
+        this->drawVertices(target, std::move(gp), allocator.vertexBuffer(), allocator.firstVertex(),
+                           count);
     }
 
     void onPrepareDraws(Target* target) override {
@@ -349,23 +350,22 @@
         }
         SkASSERT(vertexStride == gp->debugOnly_vertexStride());
         if (fAntiAlias) {
-            this->drawAA(target, gp.get(), vertexStride);
+            this->drawAA(target, std::move(gp), vertexStride);
         } else {
-            this->draw(target, gp.get(), vertexStride);
+            this->draw(target, std::move(gp), vertexStride);
         }
     }
 
-    void drawVertices(Target* target, const GrGeometryProcessor* gp, const GrBuffer* vb,
+    void drawVertices(Target* target, sk_sp<const GrGeometryProcessor> gp, const GrBuffer* vb,
                       int firstVertex, int count) {
-        GrMesh mesh(TESSELLATOR_WIREFRAME ? GrPrimitiveType::kLines : GrPrimitiveType::kTriangles);
-        mesh.setNonIndexedNonInstanced(count);
-        mesh.setVertexData(vb, firstVertex);
+        GrMesh* mesh = target->allocMesh(TESSELLATOR_WIREFRAME ? GrPrimitiveType::kLines
+                                                               : GrPrimitiveType::kTriangles);
+        mesh->setNonIndexedNonInstanced(count);
+        mesh->setVertexData(vb, firstVertex);
         auto pipe = fHelper.makePipeline(target);
-        target->draw(gp, pipe.fPipeline, pipe.fFixedDynamicState, mesh);
+        target->draw(std::move(gp), pipe.fPipeline, pipe.fFixedDynamicState, mesh);
     }
 
-    bool onCombineIfPossible(GrOp*, const GrCaps&) override { return false; }
-
     Helper fHelper;
     GrColor                 fColor;
     GrShape                 fShape;
diff --git a/src/gpu/ops/GrTextureOp.cpp b/src/gpu/ops/GrTextureOp.cpp
index 72900ed..2ba3639 100644
--- a/src/gpu/ops/GrTextureOp.cpp
+++ b/src/gpu/ops/GrTextureOp.cpp
@@ -37,8 +37,6 @@
 
 namespace {
 
-enum class MultiTexture : bool { kNo = false, kYes = true };
-
 enum class Domain : bool { kNo = false, kYes = true };
 
 /**
@@ -55,79 +53,40 @@
         SkPoint fTextureCoords;
     };
 
-    template <typename Pos, MultiTexture MT> struct OptionalMultiTextureVertex;
+    template <typename Pos, Domain D> struct OptionalDomainVertex;
     template <typename Pos>
-    struct OptionalMultiTextureVertex<Pos, MultiTexture::kNo> : VertexCommon<Pos> {
-        static constexpr MultiTexture kMultiTexture = MultiTexture::kNo;
-    };
-    template <typename Pos>
-    struct OptionalMultiTextureVertex<Pos, MultiTexture::kYes> : VertexCommon<Pos> {
-        static constexpr MultiTexture kMultiTexture = MultiTexture::kYes;
-        int fTextureIdx;
-    };
-
-    template <typename Pos, MultiTexture MT, Domain D> struct OptionalDomainVertex;
-    template <typename Pos, MultiTexture MT>
-    struct OptionalDomainVertex<Pos, MT, Domain::kNo> : OptionalMultiTextureVertex<Pos, MT> {
+    struct OptionalDomainVertex<Pos, Domain::kNo> : VertexCommon<Pos> {
         static constexpr Domain kDomain = Domain::kNo;
     };
-    template <typename Pos, MultiTexture MT>
-    struct OptionalDomainVertex<Pos, MT, Domain::kYes> : OptionalMultiTextureVertex<Pos, MT> {
+    template <typename Pos>
+    struct OptionalDomainVertex<Pos, Domain::kYes> : VertexCommon<Pos> {
         static constexpr Domain kDomain = Domain::kYes;
         SkRect fTextureDomain;
     };
 
-    template <typename Pos, MultiTexture MT, Domain D, GrAA> struct OptionalAAVertex;
-    template <typename Pos, MultiTexture MT, Domain D>
-    struct OptionalAAVertex<Pos, MT, D, GrAA::kNo> : OptionalDomainVertex<Pos, MT, D> {
+    template <typename Pos, Domain D, GrAA> struct OptionalAAVertex;
+    template <typename Pos, Domain D>
+    struct OptionalAAVertex<Pos, D, GrAA::kNo> : OptionalDomainVertex<Pos, D> {
         static constexpr GrAA kAA = GrAA::kNo;
     };
-    template <typename Pos, MultiTexture MT, Domain D>
-    struct OptionalAAVertex<Pos, MT, D, GrAA::kYes> : OptionalDomainVertex<Pos, MT, D> {
+    template <typename Pos, Domain D>
+    struct OptionalAAVertex<Pos, D, GrAA::kYes> : OptionalDomainVertex<Pos, D> {
         static constexpr GrAA kAA = GrAA::kYes;
         SkPoint3 fEdges[4];
     };
 
-    template <typename Pos, MultiTexture MT, Domain D, GrAA AA>
-    using Vertex = OptionalAAVertex<Pos, MT, D, AA>;
+    template <typename Pos, Domain D, GrAA AA>
+    using Vertex = OptionalAAVertex<Pos, D, AA>;
 
-    // Maximum number of textures supported by this op. Must also be checked against the caps
-    // limit. These numbers were based on some limited experiments on a HP Z840 and Pixel XL 2016
-    // and could probably use more tuning.
-#ifdef SK_BUILD_FOR_ANDROID
-    static constexpr int kMaxTextures = 4;
-#else
-    static constexpr int kMaxTextures = 8;
-#endif
-
-    static int SupportsMultitexture(const GrShaderCaps& caps) {
-        return caps.integerSupport() && caps.maxFragmentSamplers() > 1;
-    }
-
-    static sk_sp<GrGeometryProcessor> Make(sk_sp<GrTextureProxy> proxies[], int proxyCnt,
+    static sk_sp<GrGeometryProcessor> Make(GrTextureType textureType, GrPixelConfig textureConfig,
+                                           const GrSamplerState::Filter filter,
                                            sk_sp<GrColorSpaceXform> textureColorSpaceXform,
                                            sk_sp<GrColorSpaceXform> paintColorSpaceXform,
-                                           bool coverageAA,
-                                           bool perspective, Domain domain,
-                                           const GrSamplerState::Filter filters[],
+                                           bool coverageAA, bool perspective, Domain domain,
                                            const GrShaderCaps& caps) {
-        // We use placement new to avoid always allocating space for kMaxTextures TextureSampler
-        // instances.
-        int samplerCnt = NumSamplersToUse(proxyCnt, caps);
-        size_t size = sizeof(TextureGeometryProcessor) + sizeof(TextureSampler) * (samplerCnt - 1);
-        void* mem = GrGeometryProcessor::operator new(size);
-        return sk_sp<TextureGeometryProcessor>(
-                new (mem) TextureGeometryProcessor(proxies, proxyCnt, samplerCnt,
-                                                   std::move(textureColorSpaceXform),
-                                                   std::move(paintColorSpaceXform),
-                                                   coverageAA, perspective, domain, filters, caps));
-    }
-
-    ~TextureGeometryProcessor() override {
-        int cnt = this->numTextureSamplers();
-        for (int i = 1; i < cnt; ++i) {
-            fSamplers[i].~TextureSampler();
-        }
+        return sk_sp<TextureGeometryProcessor>(new TextureGeometryProcessor(
+                textureType, textureConfig, filter, std::move(textureColorSpaceXform),
+                std::move(paintColorSpaceXform), coverageAA, perspective, domain, caps));
     }
 
     const char* name() const override { return "TextureGeometryProcessor"; }
@@ -197,28 +156,10 @@
                     args.fFragBuilder->codeAppend(
                             "texCoord = clamp(texCoord, domain.xy, domain.zw);");
                 }
-                if (textureGP.numTextureSamplers() > 1) {
-                    // If this changes to float, reconsider Interpolation::kMustBeFlat.
-                    SkASSERT(kInt_GrVertexAttribType == textureGP.fTextureIdx.type());
-                    SkASSERT(args.fShaderCaps->integerSupport());
-                    args.fFragBuilder->codeAppend("int texIdx;");
-                    args.fVaryingHandler->addPassThroughAttribute(textureGP.fTextureIdx, "texIdx",
-                                                                  Interpolation::kMustBeFlat);
-                    args.fFragBuilder->codeAppend("switch (texIdx) {");
-                    for (int i = 0; i < textureGP.numTextureSamplers(); ++i) {
-                        args.fFragBuilder->codeAppendf("case %d: %s = ", i, args.fOutputColor);
-                        args.fFragBuilder->appendTextureLookupAndModulate(
-                                args.fOutputColor, args.fTexSamplers[i], "texCoord",
-                                kFloat2_GrSLType, &fTextureColorSpaceXformHelper);
-                        args.fFragBuilder->codeAppend("; break;");
-                    }
-                    args.fFragBuilder->codeAppend("}");
-                } else {
-                    args.fFragBuilder->codeAppendf("%s = ", args.fOutputColor);
-                    args.fFragBuilder->appendTextureLookupAndModulate(
-                            args.fOutputColor, args.fTexSamplers[0], "texCoord",
-                            kFloat2_GrSLType, &fTextureColorSpaceXformHelper);
-                }
+                args.fFragBuilder->codeAppendf("%s = ", args.fOutputColor);
+                args.fFragBuilder->appendTextureLookupAndModulate(
+                        args.fOutputColor, args.fTexSamplers[0], "texCoord", kFloat2_GrSLType,
+                        &fTextureColorSpaceXformHelper);
                 args.fFragBuilder->codeAppend(";");
                 if (textureGP.usesCoverageEdgeAA()) {
                     bool mulByFragCoordW = false;
@@ -271,36 +212,16 @@
     bool usesCoverageEdgeAA() const { return SkToBool(fAAEdges[0].isInitialized()); }
 
 private:
-    // This exists to reduce the number of shaders generated. It does some rounding of sampler
-    // counts.
-    static int NumSamplersToUse(int numRealProxies, const GrShaderCaps& caps) {
-        SkASSERT(numRealProxies > 0 && numRealProxies <= kMaxTextures &&
-                 numRealProxies <= caps.maxFragmentSamplers());
-        if (1 == numRealProxies) {
-            return 1;
-        }
-        if (numRealProxies <= 4) {
-            return 4;
-        }
-        // Round to the next power of 2 and then clamp to kMaxTextures and the max allowed by caps.
-        return SkTMin(SkNextPow2(numRealProxies), SkTMin(kMaxTextures, caps.maxFragmentSamplers()));
-    }
-
-    TextureGeometryProcessor(sk_sp<GrTextureProxy> proxies[], int proxyCnt, int samplerCnt,
+    TextureGeometryProcessor(GrTextureType textureType, GrPixelConfig textureConfig,
+                             GrSamplerState::Filter filter,
                              sk_sp<GrColorSpaceXform> textureColorSpaceXform,
-                             sk_sp<GrColorSpaceXform> paintColorSpaceXform,
-                             bool coverageAA, bool perspective, Domain domain,
-                             const GrSamplerState::Filter filters[], const GrShaderCaps& caps)
+                             sk_sp<GrColorSpaceXform> paintColorSpaceXform, bool coverageAA,
+                             bool perspective, Domain domain, const GrShaderCaps& caps)
             : INHERITED(kTextureGeometryProcessor_ClassID)
             , fTextureColorSpaceXform(std::move(textureColorSpaceXform))
-            , fPaintColorSpaceXform(std::move(paintColorSpaceXform)) {
-        SkASSERT(proxyCnt > 0 && samplerCnt >= proxyCnt);
-        fSamplers[0].reset(std::move(proxies[0]), filters[0]);
-        for (int i = 1; i < proxyCnt; ++i) {
-            // This class has one sampler built in, the rest come from memory this processor was
-            // placement-newed into and so haven't been constructed.
-            new (&fSamplers[i]) TextureSampler(std::move(proxies[i]), filters[i]);
-        }
+            , fPaintColorSpaceXform(std::move(paintColorSpaceXform))
+            , fSampler(textureType, textureConfig, filter) {
+        this->setTextureSamplerCnt(1);
 
         if (perspective) {
             fPositions = {"position", kFloat3_GrVertexAttribType};
@@ -311,17 +232,6 @@
         fTextureCoords = {"textureCoords", kFloat2_GrVertexAttribType};
         int vertexAttributeCnt = 3;
 
-        if (samplerCnt > 1) {
-            // Here we initialize any extra samplers by repeating the last one samplerCnt - proxyCnt
-            // times.
-            GrTextureProxy* dupeProxy = fSamplers[proxyCnt - 1].proxy();
-            for (int i = proxyCnt; i < samplerCnt; ++i) {
-                new (&fSamplers[i]) TextureSampler(sk_ref_sp(dupeProxy), filters[proxyCnt - 1]);
-            }
-            SkASSERT(caps.integerSupport());
-            fTextureIdx = {"textureIdx", kInt_GrVertexAttribType};
-            ++vertexAttributeCnt;
-        }
         if (domain == Domain::kYes) {
             fDomain = {"domain", kFloat4_GrVertexAttribType};
             ++vertexAttributeCnt;
@@ -334,25 +244,23 @@
             vertexAttributeCnt += 4;
         }
         this->setVertexAttributeCnt(vertexAttributeCnt);
-        this->setTextureSamplerCnt(samplerCnt);
     }
 
     const Attribute& onVertexAttribute(int i) const override {
-        return IthInitializedAttribute(i, fPositions, fColors, fTextureCoords, fTextureIdx, fDomain,
-                                       fAAEdges[0], fAAEdges[1], fAAEdges[2], fAAEdges[3]);
+        return IthInitializedAttribute(i, fPositions, fColors, fTextureCoords, fDomain, fAAEdges[0],
+                                       fAAEdges[1], fAAEdges[2], fAAEdges[3]);
     }
 
-    const TextureSampler& onTextureSampler(int i) const override { return fSamplers[i]; }
+    const TextureSampler& onTextureSampler(int) const override { return fSampler; }
 
     Attribute fPositions;
     Attribute fColors;
     Attribute fTextureCoords;
-    Attribute fTextureIdx;
     Attribute fDomain;
     Attribute fAAEdges[4];
     sk_sp<GrColorSpaceXform> fTextureColorSpaceXform;
     sk_sp<GrColorSpaceXform> fPaintColorSpaceXform;
-    TextureSampler fSamplers[1];
+    TextureSampler fSampler;
 
     typedef GrGeometryProcessor INHERITED;
 };
@@ -527,20 +435,6 @@
     }
 };
 
-template <typename V, MultiTexture MT = V::kMultiTexture> struct TexIdAssigner;
-
-template <typename V> struct TexIdAssigner<V, MultiTexture::kYes> {
-    static void Assign(V* vertices, int textureIdx) {
-        for (int i = 0; i < 4; ++i) {
-            vertices[i].fTextureIdx = textureIdx;
-        }
-    }
-};
-
-template <typename V> struct TexIdAssigner<V, MultiTexture::kNo> {
-    static void Assign(V* vertices, int textureIdx) {}
-};
-
 template <typename V, Domain D = V::kDomain> struct DomainAssigner;
 
 template <typename V> struct DomainAssigner<V, Domain::kYes> {
@@ -585,7 +479,7 @@
 template <typename V>
 static void tessellate_quad(const GrPerspQuad& devQuad, const SkRect& srcRect, GrColor color,
                             GrSurfaceOrigin origin, GrSamplerState::Filter filter, V* vertices,
-                            SkScalar iw, SkScalar ih, int textureIdx, Domain domain) {
+                            SkScalar iw, SkScalar ih, Domain domain) {
     SkRect texRect = {
             iw * srcRect.fLeft,
             ih * srcRect.fTop,
@@ -601,7 +495,6 @@
     vertices[1].fColor = color;
     vertices[2].fColor = color;
     vertices[3].fColor = color;
-    TexIdAssigner<V>::Assign(vertices, textureIdx);
     DomainAssigner<V>::Assign(vertices, domain, filter, srcRect, origin, iw, ih);
 }
 
@@ -632,42 +525,27 @@
 
     ~TextureOp() override {
         if (fFinalized) {
-            auto proxies = this->proxies();
-            for (int i = 0; i < fProxyCnt; ++i) {
-                proxies[i]->completedRead();
-            }
-            if (fProxyCnt > 1) {
-                delete[] reinterpret_cast<const char*>(proxies);
-            }
+            fProxy->completedRead();
         } else {
-            SkASSERT(1 == fProxyCnt);
-            fProxy0->unref();
+            fProxy->unref();
         }
     }
 
     const char* name() const override { return "TextureOp"; }
 
-    void visitProxies(const VisitProxyFunc& func) const override {
-        auto proxies = this->proxies();
-        for (int i = 0; i < fProxyCnt; ++i) {
-            func(proxies[i]);
-        }
-    }
+    void visitProxies(const VisitProxyFunc& func) const override { func(fProxy); }
 
     SkString dumpInfo() const override {
         SkString str;
         str.appendf("# draws: %d\n", fDraws.count());
-        auto proxies = this->proxies();
-        for (int i = 0; i < fProxyCnt; ++i) {
-            str.appendf("Proxy ID %d: %d, Filter: %d\n", i, proxies[i]->uniqueID().asUInt(),
-                        static_cast<int>(this->filters()[i]));
-        }
+        str.appendf("Proxy ID: %d, Filter: %d\n", fProxy->uniqueID().asUInt(),
+                    static_cast<int>(fFilter));
         for (int i = 0; i < fDraws.count(); ++i) {
             const Draw& draw = fDraws[i];
             str.appendf(
-                    "%d: Color: 0x%08x, ProxyIdx: %d, TexRect [L: %.2f, T: %.2f, R: %.2f, B: %.2f] "
+                    "%d: Color: 0x%08x, TexRect [L: %.2f, T: %.2f, R: %.2f, B: %.2f] "
                     "Quad [(%.2f, %.2f), (%.2f, %.2f), (%.2f, %.2f), (%.2f, %.2f)]\n",
-                    i, draw.color(), draw.textureIdx(), draw.srcRect().fLeft, draw.srcRect().fTop,
+                    i, draw.color(), draw.srcRect().fLeft, draw.srcRect().fTop,
                     draw.srcRect().fRight, draw.srcRect().fBottom, draw.quad().point(0).fX,
                     draw.quad().point(0).fY, draw.quad().point(1).fX, draw.quad().point(1).fY,
                     draw.quad().point(2).fX, draw.quad().point(2).fY, draw.quad().point(3).fX,
@@ -679,10 +557,9 @@
 
     RequiresDstTexture finalize(const GrCaps& caps, const GrAppliedClip* clip) override {
         SkASSERT(!fFinalized);
-        SkASSERT(1 == fProxyCnt);
         fFinalized = true;
-        fProxy0->addPendingRead();
-        fProxy0->unref();
+        fProxy->addPendingRead();
+        fProxy->unref();
         return RequiresDstTexture::kNo;
     }
 
@@ -696,17 +573,6 @@
 private:
     friend class ::GrOpMemoryPool;
 
-    // This is used in a heursitic for choosing a code path. We don't care what happens with
-    // really large rects, infs, nans, etc.
-#if defined(__clang__) && (__clang_major__ * 1000 + __clang_minor__) >= 3007
-__attribute__((no_sanitize("float-cast-overflow")))
-#endif
-    size_t RectSizeAsSizeT(const SkRect& rect) {;
-        return static_cast<size_t>(SkTMax(rect.width(), 1.f) * SkTMax(rect.height(), 1.f));
-    }
-
-    static constexpr int kMaxTextures = TextureGeometryProcessor::kMaxTextures;
-
     TextureOp(sk_sp<GrTextureProxy> proxy, GrSamplerState::Filter filter, GrColor color,
               const SkRect& srcRect, const SkRect& dstRect, GrAAType aaType,
               SkCanvas::SrcRectConstraint constraint, const SkMatrix& viewMatrix,
@@ -715,9 +581,8 @@
             : INHERITED(ClassID())
             , fTextureColorSpaceXform(std::move(textureColorSpaceXform))
             , fPaintColorSpaceXform(std::move(paintColorSpaceXform))
-            , fProxy0(proxy.release())
-            , fFilter0(filter)
-            , fProxyCnt(1)
+            , fProxy(proxy.release())
+            , fFilter(filter)
             , fAAType(static_cast<unsigned>(aaType))
             , fFinalized(0) {
         SkASSERT(aaType != GrAAType::kMixedSamples);
@@ -740,46 +605,39 @@
             }
         }
 #endif
-        const auto& draw = fDraws.emplace_back(srcRect, 0, quad, constraint, color);
+        const auto& draw = fDraws.emplace_back(srcRect, quad, constraint, color);
         this->setBounds(bounds, HasAABloat::kNo, IsZeroArea::kNo);
         fDomain = static_cast<bool>(draw.domain());
-        fMaxApproxDstPixelArea = RectSizeAsSizeT(bounds);
     }
 
-    template <typename Pos, MultiTexture MT, Domain D, GrAA AA>
-    void tess(void* v, const float iw[], const float ih[], const GrGeometryProcessor* gp) {
-        using Vertex = TextureGeometryProcessor::Vertex<Pos, MT, D, AA>;
+    template <typename Pos, Domain D, GrAA AA>
+    void tess(void* v, const GrGeometryProcessor* gp) {
+        using Vertex = TextureGeometryProcessor::Vertex<Pos, D, AA>;
         SkASSERT(gp->debugOnly_vertexStride() == sizeof(Vertex));
         auto vertices = static_cast<Vertex*>(v);
-        auto proxies = this->proxies();
-        auto filters = this->filters();
+        auto origin = fProxy->origin();
+        const auto* texture = fProxy->peekTexture();
+        float iw = 1.f / texture->width();
+        float ih = 1.f / texture->height();
+
         for (const auto& draw : fDraws) {
-            auto textureIdx = draw.textureIdx();
-            auto origin = proxies[textureIdx]->origin();
-            tessellate_quad<Vertex>(draw.quad(), draw.srcRect(), draw.color(), origin,
-                                    filters[textureIdx], vertices, iw[textureIdx], ih[textureIdx],
-                                    textureIdx, draw.domain());
+            tessellate_quad<Vertex>(draw.quad(), draw.srcRect(), draw.color(), origin, fFilter,
+                                    vertices, iw, ih, draw.domain());
             vertices += 4;
         }
     }
 
     void onPrepareDraws(Target* target) override {
-        sk_sp<GrTextureProxy> proxiesSPs[kMaxTextures];
-        auto proxies = this->proxies();
-        auto filters = this->filters();
-        for (int i = 0; i < fProxyCnt; ++i) {
-            if (!proxies[i]->instantiate(target->resourceProvider())) {
-                return;
-            }
-            proxiesSPs[i] = sk_ref_sp(proxies[i]);
+        if (!fProxy->instantiate(target->resourceProvider())) {
+            return;
         }
 
         Domain domain = fDomain ? Domain::kYes : Domain::kNo;
         bool coverageAA = GrAAType::kCoverage == this->aaType();
         sk_sp<GrGeometryProcessor> gp = TextureGeometryProcessor::Make(
-                proxiesSPs, fProxyCnt, std::move(fTextureColorSpaceXform),
-                std::move(fPaintColorSpaceXform), coverageAA, fPerspective,
-                domain, filters, *target->caps().shaderCaps());
+                fProxy->textureType(), fProxy->config(), fFilter,
+                std::move(fTextureColorSpaceXform), std::move(fPaintColorSpaceXform), coverageAA,
+                fPerspective, domain, *target->caps().shaderCaps());
         GrPipeline::InitArgs args;
         args.fProxy = target->proxy();
         args.fCaps = &target->caps();
@@ -790,43 +648,34 @@
         }
 
         auto clip = target->detachAppliedClip();
-        const auto* fixedDynamicState = target->allocFixedDynamicState(clip.scissorState().rect());
+        auto* fixedDynamicState = target->allocFixedDynamicState(clip.scissorState().rect(), 1);
+        fixedDynamicState->fPrimitiveProcessorTextures[0] = fProxy;
         const auto* pipeline =
                 target->allocPipeline(args, GrProcessorSet::MakeEmptySet(), std::move(clip));
-        using TessFn =
-                decltype(&TextureOp::tess<SkPoint, MultiTexture::kNo, Domain::kNo, GrAA::kNo>);
-#define TESS_FN_AND_VERTEX_SIZE(Point, MT, Domain, AA)                          \
-    {                                                                           \
-        &TextureOp::tess<Point, MT, Domain, AA>,                                \
-                sizeof(TextureGeometryProcessor::Vertex<Point, MT, Domain, AA>) \
+        using TessFn = decltype(&TextureOp::tess<SkPoint, Domain::kNo, GrAA::kNo>);
+#define TESS_FN_AND_VERTEX_SIZE(Point, Domain, AA)                          \
+    {                                                                       \
+        &TextureOp::tess<Point, Domain, AA>,                                \
+                sizeof(TextureGeometryProcessor::Vertex<Point, Domain, AA>) \
     }
         static constexpr struct {
             TessFn fTessFn;
             size_t fVertexSize;
         } kTessFnsAndVertexSizes[] = {
-                TESS_FN_AND_VERTEX_SIZE(SkPoint, MultiTexture::kNo, Domain::kNo, GrAA::kNo),
-                TESS_FN_AND_VERTEX_SIZE(SkPoint, MultiTexture::kNo, Domain::kNo, GrAA::kYes),
-                TESS_FN_AND_VERTEX_SIZE(SkPoint, MultiTexture::kNo, Domain::kYes, GrAA::kNo),
-                TESS_FN_AND_VERTEX_SIZE(SkPoint, MultiTexture::kNo, Domain::kYes, GrAA::kYes),
-                TESS_FN_AND_VERTEX_SIZE(SkPoint, MultiTexture::kYes, Domain::kNo, GrAA::kNo),
-                TESS_FN_AND_VERTEX_SIZE(SkPoint, MultiTexture::kYes, Domain::kNo, GrAA::kYes),
-                TESS_FN_AND_VERTEX_SIZE(SkPoint, MultiTexture::kYes, Domain::kYes, GrAA::kNo),
-                TESS_FN_AND_VERTEX_SIZE(SkPoint, MultiTexture::kYes, Domain::kYes, GrAA::kYes),
-                TESS_FN_AND_VERTEX_SIZE(SkPoint3, MultiTexture::kNo, Domain::kNo, GrAA::kNo),
-                TESS_FN_AND_VERTEX_SIZE(SkPoint3, MultiTexture::kNo, Domain::kNo, GrAA::kYes),
-                TESS_FN_AND_VERTEX_SIZE(SkPoint3, MultiTexture::kNo, Domain::kYes, GrAA::kNo),
-                TESS_FN_AND_VERTEX_SIZE(SkPoint3, MultiTexture::kNo, Domain::kYes, GrAA::kYes),
-                TESS_FN_AND_VERTEX_SIZE(SkPoint3, MultiTexture::kYes, Domain::kNo, GrAA::kNo),
-                TESS_FN_AND_VERTEX_SIZE(SkPoint3, MultiTexture::kYes, Domain::kNo, GrAA::kYes),
-                TESS_FN_AND_VERTEX_SIZE(SkPoint3, MultiTexture::kYes, Domain::kYes, GrAA::kNo),
-                TESS_FN_AND_VERTEX_SIZE(SkPoint3, MultiTexture::kYes, Domain::kYes, GrAA::kYes),
+                TESS_FN_AND_VERTEX_SIZE(SkPoint,  Domain::kNo,  GrAA::kNo),
+                TESS_FN_AND_VERTEX_SIZE(SkPoint,  Domain::kNo,  GrAA::kYes),
+                TESS_FN_AND_VERTEX_SIZE(SkPoint,  Domain::kYes, GrAA::kNo),
+                TESS_FN_AND_VERTEX_SIZE(SkPoint,  Domain::kYes, GrAA::kYes),
+                TESS_FN_AND_VERTEX_SIZE(SkPoint3, Domain::kNo,  GrAA::kNo),
+                TESS_FN_AND_VERTEX_SIZE(SkPoint3, Domain::kNo,  GrAA::kYes),
+                TESS_FN_AND_VERTEX_SIZE(SkPoint3, Domain::kYes, GrAA::kNo),
+                TESS_FN_AND_VERTEX_SIZE(SkPoint3, Domain::kYes, GrAA::kYes),
         };
 #undef TESS_FN_AND_VERTEX_SIZE
         int tessFnIdx = 0;
-        tessFnIdx |= coverageAA      ? 0x1 : 0x0;
-        tessFnIdx |= fDomain         ? 0x2 : 0x0;
-        tessFnIdx |= (fProxyCnt > 1) ? 0x4 : 0x0;
-        tessFnIdx |= fPerspective    ? 0x8 : 0x0;
+        tessFnIdx |= coverageAA   ? 0x1 : 0x0;
+        tessFnIdx |= fDomain      ? 0x2 : 0x0;
+        tessFnIdx |= fPerspective ? 0x4 : 0x0;
 
         SkASSERT(kTessFnsAndVertexSizes[tessFnIdx].fVertexSize == gp->debugOnly_vertexStride());
 
@@ -839,212 +688,75 @@
             return;
         }
 
-        float iw[kMaxTextures];
-        float ih[kMaxTextures];
-        for (int t = 0; t < fProxyCnt; ++t) {
-            const auto* texture = proxies[t]->peekTexture();
-            iw[t] = 1.f / texture->width();
-            ih[t] = 1.f / texture->height();
-        }
-
-        (this->*(kTessFnsAndVertexSizes[tessFnIdx].fTessFn))(vdata, iw, ih, gp.get());
+        (this->*(kTessFnsAndVertexSizes[tessFnIdx].fTessFn))(vdata, gp.get());
 
         GrPrimitiveType primitiveType =
                 fDraws.count() > 1 ? GrPrimitiveType::kTriangles : GrPrimitiveType::kTriangleStrip;
-        GrMesh mesh(primitiveType);
+        GrMesh* mesh = target->allocMesh(primitiveType);
         if (fDraws.count() > 1) {
             sk_sp<const GrBuffer> ibuffer = target->resourceProvider()->refQuadIndexBuffer();
             if (!ibuffer) {
                 SkDebugf("Could not allocate quad indices\n");
                 return;
             }
-            mesh.setIndexedPatterned(ibuffer.get(), 6, 4, fDraws.count(),
-                                     GrResourceProvider::QuadCountOfQuadBuffer());
+            mesh->setIndexedPatterned(ibuffer.get(), 6, 4, fDraws.count(),
+                                      GrResourceProvider::QuadCountOfQuadBuffer());
         } else {
-            mesh.setNonIndexedNonInstanced(4);
+            mesh->setNonIndexedNonInstanced(4);
         }
-        mesh.setVertexData(vbuffer, vstart);
-        target->draw(gp.get(), pipeline, fixedDynamicState, mesh);
+        mesh->setVertexData(vbuffer, vstart);
+        target->draw(std::move(gp), pipeline, fixedDynamicState, mesh);
     }
 
-    bool onCombineIfPossible(GrOp* t, const GrCaps& caps) override {
+    CombineResult onCombineIfPossible(GrOp* t, const GrCaps&) override {
         const auto* that = t->cast<TextureOp>();
-        const auto& shaderCaps = *caps.shaderCaps();
         if (!GrColorSpaceXform::Equals(fTextureColorSpaceXform.get(),
                                        that->fTextureColorSpaceXform.get())) {
-            return false;
+            return CombineResult::kCannotCombine;
         }
         if (!GrColorSpaceXform::Equals(fPaintColorSpaceXform.get(),
                                        that->fPaintColorSpaceXform.get())) {
-            return false;
+            return CombineResult::kCannotCombine;
         }
         if (this->aaType() != that->aaType()) {
-            return false;
+            return CombineResult::kCannotCombine;
         }
-        // Because of an issue where GrColorSpaceXform adds the same function every time it is used
-        // in a texture lookup, we only allow multiple textures when there is no transform.
-        if (TextureGeometryProcessor::SupportsMultitexture(shaderCaps) &&
-            !fTextureColorSpaceXform &&
-            fMaxApproxDstPixelArea <= shaderCaps.disableImageMultitexturingDstRectAreaThreshold() &&
-            that->fMaxApproxDstPixelArea <=
-                    shaderCaps.disableImageMultitexturingDstRectAreaThreshold()) {
-            int map[kMaxTextures];
-            int numNewProxies = this->mergeProxies(that, map, shaderCaps);
-            if (numNewProxies < 0) {
-                return false;
-            }
-            if (1 == fProxyCnt && numNewProxies) {
-                void* mem = new char[(sizeof(GrSamplerState::Filter) + sizeof(GrTextureProxy*)) *
-                                     kMaxTextures];
-                auto proxies = reinterpret_cast<GrTextureProxy**>(mem);
-                auto filters = reinterpret_cast<GrSamplerState::Filter*>(proxies + kMaxTextures);
-                proxies[0] = fProxy0;
-                filters[0] = fFilter0;
-                fProxyArray = proxies;
-            }
-            fProxyCnt += numNewProxies;
-            auto thisProxies = fProxyArray;
-            auto thatProxies = that->proxies();
-            auto thatFilters = that->filters();
-            auto thisFilters = reinterpret_cast<GrSamplerState::Filter*>(thisProxies +
-                    kMaxTextures);
-            for (int i = 0; i < that->fProxyCnt; ++i) {
-                if (map[i] < 0) {
-                    thatProxies[i]->addPendingRead();
-
-                    thisProxies[-map[i]] = thatProxies[i];
-                    thisFilters[-map[i]] = thatFilters[i];
-                    map[i] = -map[i];
-                }
-            }
-            int firstNewDraw = fDraws.count();
-            fDraws.push_back_n(that->fDraws.count(), that->fDraws.begin());
-            for (int i = firstNewDraw; i < fDraws.count(); ++i) {
-                fDraws[i].setTextureIdx(map[fDraws[i].textureIdx()]);
-            }
-        } else {
-            // We can get here when one of the ops is already multitextured but the other cannot
-            // be because of the dst rect size.
-            if (fProxyCnt > 1 || that->fProxyCnt > 1) {
-                return false;
-            }
-            if (fProxy0->uniqueID() != that->fProxy0->uniqueID() || fFilter0 != that->fFilter0) {
-                return false;
-            }
-            fDraws.push_back_n(that->fDraws.count(), that->fDraws.begin());
+        if (fProxy->uniqueID() != that->fProxy->uniqueID() || fFilter != that->fFilter) {
+            return CombineResult::kCannotCombine;
         }
+        fDraws.push_back_n(that->fDraws.count(), that->fDraws.begin());
         this->joinBounds(*that);
-        fMaxApproxDstPixelArea = SkTMax(that->fMaxApproxDstPixelArea, fMaxApproxDstPixelArea);
         fPerspective |= that->fPerspective;
         fDomain |= that->fDomain;
-        return true;
-    }
-
-    /**
-     * Determines a mapping of indices from that's proxy array to this's proxy array. A negative map
-     * value means that's proxy should be added to this's proxy array at the absolute value of
-     * the map entry. If it is determined that the ops shouldn't combine their proxies then a
-     * negative value is returned. Otherwise, return value indicates the number of proxies that have
-     * to be added to this op or, equivalently, the number of negative entries in map.
-     */
-    int mergeProxies(const TextureOp* that, int map[kMaxTextures], const GrShaderCaps& caps) const {
-        std::fill_n(map, kMaxTextures, -kMaxTextures);
-        int sharedProxyCnt = 0;
-        auto thisProxies = this->proxies();
-        auto thisFilters = this->filters();
-        auto thatProxies = that->proxies();
-        auto thatFilters = that->filters();
-        for (int i = 0; i < fProxyCnt; ++i) {
-            for (int j = 0; j < that->fProxyCnt; ++j) {
-                if (thisProxies[i]->uniqueID() == thatProxies[j]->uniqueID()) {
-                    if (thisFilters[i] != thatFilters[j]) {
-                        // In GL we don't currently support using the same texture with different
-                        // samplers. If we added support for sampler objects and a cap bit to know
-                        // it's ok to use different filter modes then we could support this.
-                        // Otherwise, we could also only allow a single filter mode for each op
-                        // instance.
-                        return -1;
-                    }
-                    map[j] = i;
-                    ++sharedProxyCnt;
-                    break;
-                }
-            }
-        }
-        int actualMaxTextures = SkTMin(caps.maxFragmentSamplers(), kMaxTextures);
-        int newProxyCnt = that->fProxyCnt - sharedProxyCnt;
-        if (newProxyCnt + fProxyCnt > actualMaxTextures) {
-            return -1;
-        }
-        GrPixelConfig config = thisProxies[0]->config();
-        int nextSlot = fProxyCnt;
-        for (int j = 0; j < that->fProxyCnt; ++j) {
-            // We want to avoid making many shaders because of different permutations of shader
-            // based swizzle and sampler types. The approach taken here is to require the configs to
-            // be the same and to only allow already instantiated proxies that have the most
-            // common sampler type. Otherwise we don't merge.
-            if (thatProxies[j]->config() != config) {
-                return -1;
-            }
-            if (GrTexture* tex = thatProxies[j]->peekTexture()) {
-                if (tex->texturePriv().textureType() != GrTextureType::k2D) {
-                    return -1;
-                }
-            }
-            if (map[j] < 0) {
-                map[j] = -(nextSlot++);
-            }
-        }
-        return newProxyCnt;
+        return CombineResult::kMerged;
     }
 
     GrAAType aaType() const { return static_cast<GrAAType>(fAAType); }
 
-    GrTextureProxy* const* proxies() const { return fProxyCnt > 1 ? fProxyArray : &fProxy0; }
-
-    const GrSamplerState::Filter* filters() const {
-        if (fProxyCnt > 1) {
-            return reinterpret_cast<const GrSamplerState::Filter*>(fProxyArray + kMaxTextures);
-        }
-        return &fFilter0;
-    }
-
     class Draw {
     public:
-        Draw(const SkRect& srcRect, int textureIdx, const GrPerspQuad& quad,
-             SkCanvas::SrcRectConstraint constraint, GrColor color)
+        Draw(const SkRect& srcRect, const GrPerspQuad& quad, SkCanvas::SrcRectConstraint constraint,
+             GrColor color)
                 : fSrcRect(srcRect)
-                , fHasDomain(constraint == SkCanvas::kStrict_SrcRectConstraint)
-                , fTextureIdx(SkToUInt(textureIdx))
                 , fQuad(quad)
-                , fColor(color) {}
+                , fColor(color)
+                , fHasDomain(constraint == SkCanvas::kStrict_SrcRectConstraint) {}
         const GrPerspQuad& quad() const { return fQuad; }
-        int textureIdx() const { return SkToInt(fTextureIdx); }
         const SkRect& srcRect() const { return fSrcRect; }
         GrColor color() const { return fColor; }
         Domain domain() const { return Domain(fHasDomain); }
-        void setTextureIdx(int i) { fTextureIdx = SkToUInt(i); }
 
     private:
         SkRect fSrcRect;
-        unsigned fHasDomain : 1;
-        unsigned fTextureIdx : 31;
         GrPerspQuad fQuad;
         GrColor fColor;
+        bool fHasDomain;
     };
     SkSTArray<1, Draw, true> fDraws;
     sk_sp<GrColorSpaceXform> fTextureColorSpaceXform;
     sk_sp<GrColorSpaceXform> fPaintColorSpaceXform;
-    // Initially we store a single proxy ptr and a single filter. If we grow to have more than
-    // one proxy we instead store pointers to dynamically allocated arrays of size kMaxTextures
-    // followed by kMaxTextures filters.
-    union {
-        GrTextureProxy* fProxy0;
-        GrTextureProxy** fProxyArray;
-    };
-    size_t fMaxApproxDstPixelArea;
-    GrSamplerState::Filter fFilter0;
-    uint8_t fProxyCnt;
+    GrTextureProxy* fProxy;
+    GrSamplerState::Filter fFilter;
     unsigned fAAType : 2;
     unsigned fPerspective : 1;
     unsigned fDomain : 1;
@@ -1054,9 +766,6 @@
     typedef GrMeshDrawOp INHERITED;
 };
 
-constexpr int TextureGeometryProcessor::kMaxTextures;
-constexpr int TextureOp::kMaxTextures;
-
 }  // anonymous namespace
 
 namespace GrTextureOp {