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>
diff --git a/src/gpu/ops/GrAAConvexPathRenderer.cpp b/src/gpu/ops/GrAAConvexPathRenderer.cpp
index e9a612f..783cac7 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 = 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);
+            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);
         }
     }
 
@@ -928,38 +928,37 @@
             SkSTArray<kPreallocDrawCnt, Draw, true> draws;
             create_vertices(segments, fanPt, args.fColor, &draws, verts, idxs);
 
-            GrMesh* meshes = target->allocMeshes(draws.count());
+            GrMesh mesh(GrPrimitiveType::kTriangles);
+
             for (int j = 0; j < draws.count(); ++j) {
                 const Draw& draw = draws[j];
-                meshes[j].setPrimitiveType(GrPrimitiveType::kTriangles);
-                meshes[j].setIndexed(indexBuffer, draw.fIndexCnt, firstIndex, 0,
-                                     draw.fVertexCnt - 1, GrPrimitiveRestart::kNo);
-                meshes[j].setVertexData(vertexBuffer, firstVertex);
+                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);
                 firstIndex += draw.fIndexCnt;
                 firstVertex += draw.fVertexCnt;
             }
-            target->draw(quadProcessor, pipe.fPipeline, pipe.fFixedDynamicState, meshes,
-                         draws.count());
         }
     }
 
-    CombineResult onCombineIfPossible(GrOp* t, const GrCaps& caps) override {
+    bool onCombineIfPossible(GrOp* t, const GrCaps& caps) override {
         AAConvexPathOp* that = t->cast<AAConvexPathOp>();
         if (!fHelper.isCompatible(that->fHelper, caps, this->bounds(), that->bounds())) {
-            return CombineResult::kCannotCombine;
+            return false;
         }
         if (fHelper.usesLocalCoords() &&
             !fPaths[0].fViewMatrix.cheapEqualTo(that->fPaths[0].fViewMatrix)) {
-            return CombineResult::kCannotCombine;
+            return false;
         }
 
         if (fLinesOnly != that->fLinesOnly) {
-            return CombineResult::kCannotCombine;
+            return false;
         }
 
         fPaths.push_back_n(that->fPaths.count(), that->fPaths.begin());
         this->joinBounds(*that);
-        return CombineResult::kMerged;
+        return true;
     }
 
     struct PathData {
diff --git a/src/gpu/ops/GrAAFillRectOp.cpp b/src/gpu/ops/GrAAFillRectOp.cpp
index 9d66299..592c0b2 100644
--- a/src/gpu/ops/GrAAFillRectOp.cpp
+++ b/src/gpu/ops/GrAAFillRectOp.cpp
@@ -265,9 +265,10 @@
         SkASSERT(vertexStride == gp->debugOnly_vertexStride());
 
         sk_sp<const GrBuffer> indexBuffer = get_index_buffer(target->resourceProvider());
-        PatternHelper helper(target, GrPrimitiveType::kTriangles, vertexStride, indexBuffer.get(),
-                             kVertsPerAAFillRect, kIndicesPerAAFillRect, fRectCnt);
-        void* vertices = helper.vertices();
+        PatternHelper helper(GrPrimitiveType::kTriangles);
+        void* vertices =
+                helper.init(target, vertexStride, indexBuffer.get(), kVertsPerAAFillRect,
+                            kIndicesPerAAFillRect, fRectCnt);
         if (!vertices || !indexBuffer) {
             SkDebugf("Could not allocate vertices\n");
             return;
@@ -291,19 +292,19 @@
             info = this->next(info);
         }
         auto pipe = fHelper.makePipeline(target);
-        helper.recordDraw(target, std::move(gp), pipe.fPipeline, pipe.fFixedDynamicState);
+        helper.recordDraw(target, gp.get(), pipe.fPipeline, pipe.fFixedDynamicState);
     }
 
-    CombineResult onCombineIfPossible(GrOp* t, const GrCaps& caps) override {
+    bool onCombineIfPossible(GrOp* t, const GrCaps& caps) override {
         AAFillRectOp* that = t->cast<AAFillRectOp>();
         if (!fHelper.isCompatible(that->fHelper, caps, this->bounds(), that->bounds())) {
-            return CombineResult::kCannotCombine;
+            return false;
         }
 
         fRectData.push_back_n(that->fRectData.count(), that->fRectData.begin());
         fRectCnt += that->fRectCnt;
         this->joinBounds(*that);
-        return CombineResult::kMerged;
+        return true;
     }
 
     struct RectInfo {
diff --git a/src/gpu/ops/GrAAHairLinePathRenderer.cpp b/src/gpu/ops/GrAAHairLinePathRenderer.cpp
index 7cff875..05f1fbd 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;
 
-    CombineResult onCombineIfPossible(GrOp* t, const GrCaps& caps) override {
+    bool onCombineIfPossible(GrOp* t, const GrCaps& caps) override {
         AAHairlineOp* that = t->cast<AAHairlineOp>();
 
         if (!fHelper.isCompatible(that->fHelper, caps, this->bounds(), that->bounds())) {
-            return CombineResult::kCannotCombine;
+            return false;
         }
 
         if (this->viewMatrix().hasPerspective() != that->viewMatrix().hasPerspective()) {
-            return CombineResult::kCannotCombine;
+            return false;
         }
 
         // We go to identity if we don't have perspective
         if (this->viewMatrix().hasPerspective() &&
             !this->viewMatrix().cheapEqualTo(that->viewMatrix())) {
-            return CombineResult::kCannotCombine;
+            return false;
         }
 
         // 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 CombineResult::kCannotCombine;
+            return false;
         }
 
         if (this->color() != that->color()) {
-            return CombineResult::kCannotCombine;
+            return false;
         }
 
         if (fHelper.usesLocalCoords() && !this->viewMatrix().cheapEqualTo(that->viewMatrix())) {
-            return CombineResult::kCannotCombine;
+            return false;
         }
 
         fPaths.push_back_n(that->fPaths.count(), that->fPaths.begin());
         this->joinBounds(*that);
-        return CombineResult::kMerged;
+        return true;
     }
 
     GrColor color() const { return fColor; }
@@ -975,11 +975,11 @@
             add_line(&lines[2*i], toSrc, this->coverage(), &verts);
         }
 
-        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);
+        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);
     }
 
     if (quadCount || conicCount) {
@@ -1030,20 +1030,20 @@
         }
 
         if (quadCount > 0) {
-            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);
+            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);
             firstVertex += quadCount * kQuadNumVertices;
         }
 
         if (conicCount > 0) {
-            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);
+            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);
         }
     }
 }
diff --git a/src/gpu/ops/GrAALinearizingConvexPathRenderer.cpp b/src/gpu/ops/GrAALinearizingConvexPathRenderer.cpp
index c40aa71..75662d3 100644
--- a/src/gpu/ops/GrAALinearizingConvexPathRenderer.cpp
+++ b/src/gpu/ops/GrAALinearizingConvexPathRenderer.cpp
@@ -210,13 +210,14 @@
     }
 
 private:
-    void draw(Target* target, sk_sp<const GrGeometryProcessor> gp, const GrPipeline* pipeline,
+    void draw(Target* target, 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);
@@ -234,11 +235,10 @@
             return;
         }
         memcpy(idxs, indices, indexCount * sizeof(uint16_t));
-        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);
+        mesh.setIndexed(indexBuffer, indexCount, firstIndex, 0, vertexCount - 1,
+                        GrPrimitiveRestart::kNo);
+        mesh.setVertexData(vertexBuffer, firstVertex);
+        target->draw(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, pipe.fPipeline, pipe.fFixedDynamicState, vertexCount,
+                this->draw(target, gp.get(), 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, std::move(gp), pipe.fPipeline, pipe.fFixedDynamicState, vertexCount,
+            this->draw(target, gp.get(), pipe.fPipeline, pipe.fFixedDynamicState, vertexCount,
                        vertexStride, vertices, indexCount, indices);
         }
         sk_free(vertices);
         sk_free(indices);
     }
 
-    CombineResult onCombineIfPossible(GrOp* t, const GrCaps& caps) override {
+    bool onCombineIfPossible(GrOp* t, const GrCaps& caps) override {
         AAFlatteningConvexPathOp* that = t->cast<AAFlatteningConvexPathOp>();
         if (!fHelper.isCompatible(that->fHelper, caps, this->bounds(), that->bounds())) {
-            return CombineResult::kCannotCombine;
+            return false;
         }
 
         fPaths.push_back_n(that->fPaths.count(), that->fPaths.begin());
         this->joinBounds(*that);
-        return CombineResult::kMerged;
+        return true;
     }
 
     const SkMatrix& viewMatrix() const { return fPaths[0].fViewMatrix; }
diff --git a/src/gpu/ops/GrAAStrokeRectOp.cpp b/src/gpu/ops/GrAAStrokeRectOp.cpp
index 0362946..92d1c01 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; }
 
-    CombineResult onCombineIfPossible(GrOp* t, const GrCaps&) override;
+    bool 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(target, GrPrimitiveType::kTriangles, vertexStride, indexBuffer.get(),
-                         verticesPerInstance, indicesPerInstance, instanceCount);
-    void* vertices = helper.vertices();
+    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);
     if (!vertices || !indexBuffer) {
         SkDebugf("Could not allocate vertices\n");
         return;
@@ -308,7 +308,7 @@
                                            fHelper.compatibleWithAlphaAsCoverage());
     }
     auto pipe = fHelper.makePipeline(target);
-    helper.recordDraw(target, std::move(gp), pipe.fPipeline, pipe.fFixedDynamicState);
+    helper.recordDraw(target, gp.get(), pipe.fPipeline, pipe.fFixedDynamicState);
 }
 
 sk_sp<const GrBuffer> AAStrokeRectOp::GetIndexBuffer(GrResourceProvider* resourceProvider,
@@ -405,27 +405,27 @@
     }
 }
 
-GrOp::CombineResult AAStrokeRectOp::onCombineIfPossible(GrOp* t, const GrCaps& caps) {
+bool AAStrokeRectOp::onCombineIfPossible(GrOp* t, const GrCaps& caps) {
     AAStrokeRectOp* that = t->cast<AAStrokeRectOp>();
 
     if (!fHelper.isCompatible(that->fHelper, caps, this->bounds(), that->bounds())) {
-        return CombineResult::kCannotCombine;
+        return false;
     }
 
     // TODO combine across miterstroke changes
     if (this->miterStroke() != that->miterStroke()) {
-        return CombineResult::kCannotCombine;
+        return false;
     }
 
     // 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 CombineResult::kCannotCombine;
+        return false;
     }
 
     fRects.push_back_n(that->fRects.count(), that->fRects.begin());
     this->joinBounds(*that);
-    return CombineResult::kMerged;
+    return true;
 }
 
 static void setup_scale(int* scale, SkScalar inset) {
diff --git a/src/gpu/ops/GrAtlasTextOp.cpp b/src/gpu/ops/GrAtlasTextOp.cpp
index ba39b97..cfd0c45 100644
--- a/src/gpu/ops/GrAtlasTextOp.cpp
+++ b/src/gpu/ops/GrAtlasTextOp.cpp
@@ -295,16 +295,9 @@
     }
     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(), kMaxTextures);
-    for (unsigned i = 0; i < numActiveProxies; ++i) {
-        pipe.fFixedDynamicState->fPrimitiveProcessorTextures[i] = proxies[i].get();
-    }
+                                     target->detachAppliedClip());
 
     FlushInfo flushInfo;
     flushInfo.fPipeline = pipe.fPipeline;
@@ -407,9 +400,6 @@
     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(
@@ -425,60 +415,62 @@
                                                                       samplerState);
         }
     }
+
+    GrMesh mesh(GrPrimitiveType::kTriangles);
     int maxGlyphsPerDraw =
             static_cast<int>(flushInfo->fIndexBuffer->gpuMemorySize() / sizeof(uint16_t) / 6);
-    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);
+    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);
     flushInfo->fVertexOffset += kVerticesPerGlyph * flushInfo->fGlyphsToFlush;
     flushInfo->fGlyphsToFlush = 0;
 }
 
-GrOp::CombineResult GrAtlasTextOp::onCombineIfPossible(GrOp* t, const GrCaps& caps) {
+bool GrAtlasTextOp::onCombineIfPossible(GrOp* t, const GrCaps& caps) {
     GrAtlasTextOp* that = t->cast<GrAtlasTextOp>();
     if (fProcessors != that->fProcessors) {
-        return CombineResult::kCannotCombine;
+        return false;
     }
 
     if (!fCanCombineOnTouchOrOverlap && GrRectsTouchOrOverlap(this->bounds(), that->bounds())) {
-        return CombineResult::kCannotCombine;
+        return false;
     }
 
     if (fMaskType != that->fMaskType) {
-        return CombineResult::kCannotCombine;
+        return false;
     }
 
     const SkMatrix& thisFirstMatrix = fGeoData[0].fViewMatrix;
     const SkMatrix& thatFirstMatrix = that->fGeoData[0].fViewMatrix;
 
     if (this->usesLocalCoords() && !thisFirstMatrix.cheapEqualTo(thatFirstMatrix)) {
-        return CombineResult::kCannotCombine;
+        return false;
     }
 
     if (fNeedsGlyphTransform != that->fNeedsGlyphTransform) {
-        return CombineResult::kCannotCombine;
+        return false;
     }
 
     if (fNeedsGlyphTransform &&
         (thisFirstMatrix.hasPerspective() != thatFirstMatrix.hasPerspective())) {
-        return CombineResult::kCannotCombine;
+        return false;
     }
 
     if (this->usesDistanceFields()) {
         if (fDFGPFlags != that->fDFGPFlags) {
-            return CombineResult::kCannotCombine;
+            return false;
         }
 
         if (fLuminanceColor != that->fLuminanceColor) {
-            return CombineResult::kCannotCombine;
+            return false;
         }
     } else {
         if (kColorBitmapMask_MaskType == fMaskType && this->color() != that->color()) {
-            return CombineResult::kCannotCombine;
+            return false;
         }
+
     }
 
     // Keep the batch vertex buffer size below 32K so we don't have to create a special one
@@ -486,7 +478,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 CombineResult::kCannotCombine;
+        return false;
     }
 
     fNumGlyphs += that->numGlyphs();
@@ -516,7 +508,7 @@
     fGeoCount = newGeoCount;
 
     this->joinBounds(*that);
-    return CombineResult::kMerged;
+    return true;
 }
 
 // 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 e6e08dc..14f7e95 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;
-        GrPipeline::FixedDynamicState* fFixedDynamicState;
+        const GrPipeline::FixedDynamicState* fFixedDynamicState;
         int fGlyphsToFlush;
         int fVertexOffset;
     };
@@ -149,7 +149,7 @@
     bool usesLocalCoords() const { return fUsesLocalCoords; }
     int numGlyphs() const { return fNumGlyphs; }
 
-    CombineResult onCombineIfPossible(GrOp* t, const GrCaps& caps) override;
+    bool 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 e62667d..6e76191 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);
     }
 
-    CombineResult onCombineIfPossible(GrOp* t, const GrCaps& caps) override {
+    bool 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 CombineResult::kCannotCombine;
+            return false;
         }
         if (cb->contains(this)) {
             fClip = cb->fClip;
             this->replaceBounds(*t);
             fColor = cb->fColor;
-            return CombineResult::kMerged;
+            return true;
         } else if (cb->fColor == fColor && this->contains(cb)) {
-            return CombineResult::kMerged;
+            return true;
         }
-        return CombineResult::kCannotCombine;
+        return false;
     }
 
     bool contains(const GrClearOp* that) const {
diff --git a/src/gpu/ops/GrClearStencilClipOp.h b/src/gpu/ops/GrClearStencilClipOp.h
index 5861fca..3e7ad50 100644
--- a/src/gpu/ops/GrClearStencilClipOp.h
+++ b/src/gpu/ops/GrClearStencilClipOp.h
@@ -52,6 +52,8 @@
         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 b93cbb7..bc0e33f 100644
--- a/src/gpu/ops/GrCopySurfaceOp.h
+++ b/src/gpu/ops/GrCopySurfaceOp.h
@@ -53,6 +53,8 @@
         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 075df70..43acc5c 100644
--- a/src/gpu/ops/GrDashOp.cpp
+++ b/src/gpu/ops/GrDashOp.cpp
@@ -625,6 +625,7 @@
             return;
         }
 
+        QuadHelper helper;
         size_t vertexStride;
         if (fullDash) {
             vertexStride =
@@ -633,8 +634,7 @@
             vertexStride = sizeof(SkPoint);
         }
         SkASSERT(vertexStride == gp->debugOnly_vertexStride());
-        QuadHelper helper(target, vertexStride, totalRectCount);
-        void* vertices = helper.vertices();
+        void* vertices = helper.init(target, vertexStride, totalRectCount);
         if (!vertices) {
             return;
         }
@@ -696,43 +696,43 @@
         }
         auto pipe = target->makePipeline(pipelineFlags, std::move(fProcessorSet),
                                          target->detachAppliedClip());
-        helper.recordDraw(target, std::move(gp), pipe.fPipeline, pipe.fFixedDynamicState);
+        helper.recordDraw(target, gp.get(), pipe.fPipeline, pipe.fFixedDynamicState);
     }
 
-    CombineResult onCombineIfPossible(GrOp* t, const GrCaps& caps) override {
+    bool onCombineIfPossible(GrOp* t, const GrCaps& caps) override {
         DashOp* that = t->cast<DashOp>();
         if (fProcessorSet != that->fProcessorSet) {
-            return CombineResult::kCannotCombine;
+            return false;
         }
         if (fDisallowCombineOnTouchOrOverlap &&
             GrRectsTouchOrOverlap(this->bounds(), that->bounds())) {
-            return CombineResult::kCannotCombine;
+            return false;
         }
 
         if (this->aaMode() != that->aaMode()) {
-            return CombineResult::kCannotCombine;
+            return false;
         }
 
         if (this->fullDash() != that->fullDash()) {
-            return CombineResult::kCannotCombine;
+            return false;
         }
 
         if (this->cap() != that->cap()) {
-            return CombineResult::kCannotCombine;
+            return false;
         }
 
         // TODO vertex color
         if (this->color() != that->color()) {
-            return CombineResult::kCannotCombine;
+            return false;
         }
 
         if (fUsesLocalCoords && !this->viewMatrix().cheapEqualTo(that->viewMatrix())) {
-            return CombineResult::kCannotCombine;
+            return false;
         }
 
         fLines.push_back_n(that->fLines.count(), that->fLines.begin());
         this->joinBounds(*that);
-        return CombineResult::kMerged;
+        return true;
     }
 
     GrColor color() const { return fColor; }
diff --git a/src/gpu/ops/GrDebugMarkerOp.h b/src/gpu/ops/GrDebugMarkerOp.h
index 96c5477..061f676 100644
--- a/src/gpu/ops/GrDebugMarkerOp.h
+++ b/src/gpu/ops/GrDebugMarkerOp.h
@@ -39,6 +39,8 @@
         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 4e038df..07e40f0 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,
-                   sk_sp<const GrGeometryProcessor> geometryProcessor, const GrPipeline* pipeline,
+                   GrGeometryProcessor* geometryProcessor, const GrPipeline* pipeline,
                    const GrPipeline::FixedDynamicState* fixedDynamicState)
-            : fPrimitiveType(primitiveType)
+            : fMesh(primitiveType)
             , fTarget(target)
             , fVertexStride(sizeof(SkPoint))
-            , fGeometryProcessor(std::move(geometryProcessor))
+            , fGeometryProcessor(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 == fPrimitiveType ||
-               GrPrimitiveType::kTriangles == fPrimitiveType;
+        return GrPrimitiveType::kLines == fMesh.primitiveType() ||
+               GrPrimitiveType::kTriangles == fMesh.primitiveType();
     }
     bool isHairline() const {
-        return GrPrimitiveType::kLines == fPrimitiveType ||
-               GrPrimitiveType::kLineStrip == fPrimitiveType;
+        return GrPrimitiveType::kLines == fMesh.primitiveType() ||
+               GrPrimitiveType::kLineStrip == fMesh.primitiveType();
     }
     int indexScale() const {
-        switch (fPrimitiveType) {
+        switch (fMesh.primitiveType()) {
             case GrPrimitiveType::kLines:
                 return 2;
             case GrPrimitiveType::kTriangles:
@@ -271,15 +271,14 @@
         SkASSERT(indexCount <= fIndicesInChunk);
 
         if (this->isIndexed() ? SkToBool(indexCount) : SkToBool(vertexCount)) {
-            GrMesh* mesh = fTarget->allocMesh(fPrimitiveType);
             if (!this->isIndexed()) {
-                mesh->setNonIndexedNonInstanced(vertexCount);
+                fMesh.setNonIndexedNonInstanced(vertexCount);
             } else {
-                mesh->setIndexed(fIndexBuffer, indexCount, fFirstIndex, 0, vertexCount - 1,
+                fMesh.setIndexed(fIndexBuffer, indexCount, fFirstIndex, 0, vertexCount - 1,
                                  GrPrimitiveRestart::kNo);
             }
-            mesh->setVertexData(fVertexBuffer, fFirstVertex);
-            fTarget->draw(fGeometryProcessor, fPipeline, fFixedDynamicState, mesh);
+            fMesh.setVertexData(fVertexBuffer, fFirstVertex);
+            fTarget->draw(fGeometryProcessor, fPipeline, fFixedDynamicState, fMesh);
         }
 
         fTarget->putBackIndices((size_t)(fIndicesInChunk - indexCount));
@@ -312,10 +311,10 @@
         }
     }
 
-    GrPrimitiveType fPrimitiveType;
+    GrMesh fMesh;
     GrMeshDrawOp::Target* fTarget;
     size_t fVertexStride;
-    sk_sp<const GrGeometryProcessor> fGeometryProcessor;
+    GrGeometryProcessor* fGeometryProcessor;
     const GrPipeline* fPipeline;
     const GrPipeline::FixedDynamicState* fFixedDynamicState;
 
@@ -429,7 +428,7 @@
             primitiveType = GrPrimitiveType::kTriangles;
         }
         auto pipe = fHelper.makePipeline(target);
-        PathGeoBuilder pathGeoBuilder(primitiveType, target, std::move(gp), pipe.fPipeline,
+        PathGeoBuilder pathGeoBuilder(primitiveType, target, gp.get(), pipe.fPipeline,
                                       pipe.fFixedDynamicState);
 
         // fill buffers
@@ -439,31 +438,31 @@
         }
     }
 
-    CombineResult onCombineIfPossible(GrOp* t, const GrCaps& caps) override {
+    bool onCombineIfPossible(GrOp* t, const GrCaps& caps) override {
         DefaultPathOp* that = t->cast<DefaultPathOp>();
         if (!fHelper.isCompatible(that->fHelper, caps, this->bounds(), that->bounds())) {
-            return CombineResult::kCannotCombine;
+            return false;
         }
 
         if (this->color() != that->color()) {
-            return CombineResult::kCannotCombine;
+            return false;
         }
 
         if (this->coverage() != that->coverage()) {
-            return CombineResult::kCannotCombine;
+            return false;
         }
 
         if (!this->viewMatrix().cheapEqualTo(that->viewMatrix())) {
-            return CombineResult::kCannotCombine;
+            return false;
         }
 
         if (this->isHairline() != that->isHairline()) {
-            return CombineResult::kCannotCombine;
+            return false;
         }
 
         fPaths.push_back_n(that->fPaths.count(), that->fPaths.begin());
         this->joinBounds(*that);
-        return CombineResult::kMerged;
+        return true;
     }
 
     GrColor color() const { return fColor; }
diff --git a/src/gpu/ops/GrDrawAtlasOp.cpp b/src/gpu/ops/GrDrawAtlasOp.cpp
index 107b0be..d280ad9 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();
-    QuadHelper helper(target, vertexStride, numQuads);
-    void* verts = helper.vertices();
+    void* verts = helper.init(target, vertexStride, numQuads);
     if (!verts) {
         SkDebugf("Could not allocate vertices\n");
         return;
@@ -147,34 +147,34 @@
         vertPtr += allocSize;
     }
     auto pipe = fHelper.makePipeline(target);
-    helper.recordDraw(target, std::move(gp), pipe.fPipeline, pipe.fFixedDynamicState);
+    helper.recordDraw(target, gp.get(), pipe.fPipeline, pipe.fFixedDynamicState);
 }
 
-GrOp::CombineResult GrDrawAtlasOp::onCombineIfPossible(GrOp* t, const GrCaps& caps) {
+bool GrDrawAtlasOp::onCombineIfPossible(GrOp* t, const GrCaps& caps) {
     GrDrawAtlasOp* that = t->cast<GrDrawAtlasOp>();
 
     if (!fHelper.isCompatible(that->fHelper, caps, this->bounds(), that->bounds())) {
-        return CombineResult::kCannotCombine;
+        return false;
     }
 
     // We currently use a uniform viewmatrix for this op.
     if (!this->viewMatrix().cheapEqualTo(that->viewMatrix())) {
-        return CombineResult::kCannotCombine;
+        return false;
     }
 
     if (this->hasColors() != that->hasColors()) {
-        return CombineResult::kCannotCombine;
+        return false;
     }
 
     if (!this->hasColors() && this->color() != that->color()) {
-        return CombineResult::kCannotCombine;
+        return false;
     }
 
     fGeoData.push_back_n(that->fGeoData.count(), that->fGeoData.begin());
     fQuadCount += that->quadCount();
 
     this->joinBounds(*that);
-    return CombineResult::kMerged;
+    return true;
 }
 
 GrDrawOp::FixedFunctionFlags GrDrawAtlasOp::fixedFunctionFlags() const {
diff --git a/src/gpu/ops/GrDrawAtlasOp.h b/src/gpu/ops/GrDrawAtlasOp.h
index d15bd3c..4e894ce 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; }
 
-    CombineResult onCombineIfPossible(GrOp* t, const GrCaps&) override;
+    bool 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 3016fd7..63986d7 100644
--- a/src/gpu/ops/GrDrawPathOp.h
+++ b/src/gpu/ops/GrDrawPathOp.h
@@ -93,6 +93,8 @@
         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 dd91452..d227920 100644
--- a/src/gpu/ops/GrDrawVerticesOp.cpp
+++ b/src/gpu/ops/GrDrawVerticesOp.cpp
@@ -261,7 +261,7 @@
                       indices);
 
     // Draw the vertices.
-    this->drawVertices(target, std::move(gp), vertexBuffer, firstVertex, indexBuffer, firstIndex);
+    this->drawVertices(target, gp.get(), 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, std::move(gp), vertexBuffer.get(), 0, indexBuffer.get(), 0);
+        this->drawVertices(target, gp.get(), vertexBuffer.get(), 0, indexBuffer.get(), 0);
         return;
     }
 
@@ -353,7 +353,7 @@
     rp->assignUniqueKeyToResource(indexKey, indexBuffer.get());
 
     // Draw the vertices.
-    this->drawVertices(target, std::move(gp), vertexBuffer.get(), 0, indexBuffer.get(), 0);
+    this->drawVertices(target, gp.get(), vertexBuffer.get(), 0, indexBuffer.get(), 0);
 }
 
 void GrDrawVerticesOp::fillBuffers(bool hasColorAttribute,
@@ -465,58 +465,59 @@
 }
 
 void GrDrawVerticesOp::drawVertices(Target* target,
-                                    sk_sp<const GrGeometryProcessor> gp,
+                                    GrGeometryProcessor* gp,
                                     const GrBuffer* vertexBuffer,
                                     int firstVertex,
                                     const GrBuffer* indexBuffer,
                                     int firstIndex) {
-    GrMesh* mesh = target->allocMesh(this->primitiveType());
+    GrMesh mesh(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(std::move(gp), pipe.fPipeline, pipe.fFixedDynamicState, mesh);
+    target->draw(gp, pipe.fPipeline, pipe.fFixedDynamicState, mesh);
 }
 
-GrOp::CombineResult GrDrawVerticesOp::onCombineIfPossible(GrOp* t, const GrCaps& caps) {
+bool GrDrawVerticesOp::onCombineIfPossible(GrOp* t, const GrCaps& caps) {
     GrDrawVerticesOp* that = t->cast<GrDrawVerticesOp>();
 
     if (!fHelper.isCompatible(that->fHelper, caps, this->bounds(), that->bounds())) {
-        return CombineResult::kCannotCombine;
+        return false;
     }
 
     // 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 CombineResult::kCannotCombine;
+        return false;
     }
 
     // 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 CombineResult::kCannotCombine;
+        return false;
     }
 
     if (!this->combinablePrimitive() || this->primitiveType() != that->primitiveType()) {
-        return CombineResult::kCannotCombine;
+        return false;
     }
 
     if (fMeshes[0].fVertices->hasIndices() != that->fMeshes[0].fVertices->hasIndices()) {
-        return CombineResult::kCannotCombine;
+        return false;
     }
 
     if (fColorArrayType != that->fColorArrayType) {
-        return CombineResult::kCannotCombine;
+        return false;
     }
 
     if (fVertexCount + that->fVertexCount > SkTo<int>(UINT16_MAX)) {
-        return CombineResult::kCannotCombine;
+        return false;
     }
 
     // NOTE: For SkColor vertex colors, the source color space is always sRGB, and the destination
@@ -541,7 +542,7 @@
     fIndexCount += that->fIndexCount;
 
     this->joinBounds(*that);
-    return CombineResult::kMerged;
+    return true;
 }
 
 ///////////////////////////////////////////////////////////////////////////////////////////////////
diff --git a/src/gpu/ops/GrDrawVerticesOp.h b/src/gpu/ops/GrDrawVerticesOp.h
index 07227e0..7695dd9 100644
--- a/src/gpu/ops/GrDrawVerticesOp.h
+++ b/src/gpu/ops/GrDrawVerticesOp.h
@@ -80,7 +80,7 @@
                      uint16_t* indices) const;
 
     void drawVertices(Target*,
-                      sk_sp<const GrGeometryProcessor>,
+                      GrGeometryProcessor*,
                       const GrBuffer* vertexBuffer,
                       int firstVertex,
                       const GrBuffer* indexBuffer,
@@ -98,7 +98,7 @@
                GrPrimitiveType::kPoints == fPrimitiveType;
     }
 
-    CombineResult onCombineIfPossible(GrOp* t, const GrCaps&) override;
+    bool 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 4832ee7..00dbef5 100644
--- a/src/gpu/ops/GrLatticeOp.cpp
+++ b/src/gpu/ops/GrLatticeOp.cpp
@@ -32,10 +32,10 @@
         GrColor fColor;
     };
 
-    static sk_sp<GrGeometryProcessor> Make(const GrTextureProxy* proxy,
+    static sk_sp<GrGeometryProcessor> Make(sk_sp<GrTextureProxy> proxy,
                                            sk_sp<GrColorSpaceXform> csxf,
                                            GrSamplerState::Filter filter) {
-        return sk_sp<GrGeometryProcessor>(new LatticeGP(proxy, std::move(csxf), filter));
+        return sk_sp<GrGeometryProcessor>(new LatticeGP(std::move(proxy), std::move(csxf), filter));
     }
 
     const char* name() const override { return "LatticeGP"; }
@@ -92,10 +92,10 @@
     }
 
 private:
-    LatticeGP(const GrTextureProxy* proxy, sk_sp<GrColorSpaceXform> csxf,
+    LatticeGP(sk_sp<GrTextureProxy> proxy, sk_sp<GrColorSpaceXform> csxf,
               GrSamplerState::Filter filter)
             : INHERITED(kLatticeGP_ClassID), fColorSpaceXform(std::move(csxf)) {
-        fSampler.reset(proxy->textureType(), proxy->config(), filter);
+        fSampler.reset(std::move(proxy), filter);
         this->setTextureSamplerCnt(1);
         this->setVertexAttributeCnt(4);
     }
@@ -202,7 +202,7 @@
 
 private:
     void onPrepareDraws(Target* target) override {
-        auto gp = LatticeGP::Make(fProxy.get(), fColorSpaceXform, fFilter);
+        auto gp = LatticeGP::Make(fProxy, 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(target, GrPrimitiveType::kTriangles, kVertexStide, indexBuffer.get(),
-                             kVertsPerRect, kIndicesPerRect, numRects);
-        void* vertices = helper.vertices();
+        PatternHelper helper(GrPrimitiveType::kTriangles);
+        void* vertices = helper.init(target, kVertexStide, indexBuffer.get(), kVertsPerRect,
+                                     kIndicesPerRect, numRects);
         if (!vertices || !indexBuffer) {
             SkDebugf("Could not allocate vertices\n");
             return;
@@ -281,29 +281,28 @@
                                                   kVertsPerRect * patch.fIter->numRectsToDraw());
             }
         }
-        auto pipe = fHelper.makePipeline(target, 1);
-        pipe.fFixedDynamicState->fPrimitiveProcessorTextures[0] = fProxy.get();
-        helper.recordDraw(target, std::move(gp), pipe.fPipeline, pipe.fFixedDynamicState);
+        auto pipe = fHelper.makePipeline(target);
+        helper.recordDraw(target, gp.get(), pipe.fPipeline, pipe.fFixedDynamicState);
     }
 
-    CombineResult onCombineIfPossible(GrOp* t, const GrCaps& caps) override {
+    bool onCombineIfPossible(GrOp* t, const GrCaps& caps) override {
         NonAALatticeOp* that = t->cast<NonAALatticeOp>();
         if (fProxy != that->fProxy) {
-            return CombineResult::kCannotCombine;
+            return false;
         }
         if (fFilter != that->fFilter) {
-            return CombineResult::kCannotCombine;
+            return false;
         }
         if (GrColorSpaceXform::Equals(fColorSpaceXform.get(), that->fColorSpaceXform.get())) {
-            return CombineResult::kCannotCombine;
+            return false;
         }
         if (!fHelper.isCompatible(that->fHelper, caps, this->bounds(), that->bounds())) {
-            return CombineResult::kCannotCombine;
+            return false;
         }
 
         fPatches.move_back_n(that->fPatches.count(), that->fPatches.begin());
         this->joinBounds(*that);
-        return CombineResult::kMerged;
+        return true;
     }
 
     struct Patch {
diff --git a/src/gpu/ops/GrMeshDrawOp.cpp b/src/gpu/ops/GrMeshDrawOp.cpp
index 620ea47..0bac5ef 100644
--- a/src/gpu/ops/GrMeshDrawOp.cpp
+++ b/src/gpu/ops/GrMeshDrawOp.cpp
@@ -14,68 +14,56 @@
 
 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,
-        int numPrimProcTextures) {
+        uint32_t pipelineFlags, GrProcessorSet&& processorSet, GrAppliedClip&& clip) {
     GrPipeline::InitArgs pipelineArgs;
     pipelineArgs.fFlags = pipelineFlags;
     pipelineArgs.fProxy = this->proxy();
@@ -83,12 +71,8 @@
     pipelineArgs.fCaps = &this->caps();
     pipelineArgs.fResourceProvider = this->resourceProvider();
     GrPipeline::FixedDynamicState* fixedDynamicState = nullptr;
-    if (clip.scissorState().enabled() || numPrimProcTextures) {
+    if (clip.scissorState().enabled()) {
         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 a6e7da5..e90de47 100644
--- a/src/gpu/ops/GrMeshDrawOp.h
+++ b/src/gpu/ops/GrMeshDrawOp.h
@@ -12,6 +12,7 @@
 #include "GrDrawOp.h"
 #include "GrGeometryProcessor.h"
 #include "GrMesh.h"
+#include "GrPendingProgramElement.h"
 
 class GrAtlasManager;
 class GrCaps;
@@ -33,23 +34,18 @@
         space for the vertices and flushes the draws to the GrMeshDrawOp::Target. */
     class PatternHelper {
     public:
-        PatternHelper(Target*, GrPrimitiveType, size_t vertexStride, const GrBuffer*,
-                      int verticesPerRepetition, int indicesPerRepetition, int repeatCount);
+        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);
 
-        /** 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);
+        /** Call after init() to issue draws to the GrMeshDrawOp::Target.*/
+        void recordDraw(Target*, const GrGeometryProcessor*, const GrPipeline*,
+                        const GrPipeline::FixedDynamicState*);
 
     private:
-        void* fVertices = nullptr;
-        GrMesh* fMesh = nullptr;
+        GrMesh fMesh;
     };
 
     static const int kVerticesPerQuad = 4;
@@ -58,11 +54,13 @@
     /** A specialization of InstanceHelper for quad rendering. */
     class QuadHelper : private PatternHelper {
     public:
-        QuadHelper() = delete;
-        QuadHelper(Target* target, size_t vertexStride, int quadsToDraw);
+        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);
 
         using PatternHelper::recordDraw;
-        using PatternHelper::vertices;
 
     private:
         typedef PatternHelper INHERITED;
@@ -80,19 +78,8 @@
     virtual ~Target() {}
 
     /** Adds a draw of a mesh. */
-    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);
-    }
+    virtual void draw(const GrGeometryProcessor*, const GrPipeline*,
+                      const GrPipeline::FixedDynamicState*, const GrMesh&) = 0;
 
     /**
      * Makes space for vertex data. The returned pointer is the location where vertex data
@@ -145,25 +132,10 @@
         return this->pipelineArena()->make<GrPipeline>(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);
+    template <typename... Args>
+    GrPipeline::FixedDynamicState* allocFixedDynamicState(Args&... args) {
+        return this->pipelineArena()->make<GrPipeline::FixedDynamicState>(
+                std::forward<Args>(args)...);
     }
 
     // Once we have C++17 structured bindings make this just be a tuple because then we can do:
@@ -172,7 +144,7 @@
     //      std::tie(flushInfo.fPipeline, flushInfo.fFixedState) = target->makePipeline(...);
     struct PipelineAndFixedDynamicState {
         const GrPipeline* fPipeline;
-        GrPipeline::FixedDynamicState* fFixedDynamicState;
+        const GrPipeline::FixedDynamicState* fFixedDynamicState;
     };
 
     /**
@@ -180,8 +152,7 @@
      * GrAppliedClip and uses a fixed dynamic state.
      */
     PipelineAndFixedDynamicState makePipeline(uint32_t pipelineFlags, GrProcessorSet&&,
-                                              GrAppliedClip&&,
-                                              int numPrimitiveProcessorTextures = 0);
+                                              GrAppliedClip&&);
 
     virtual GrRenderTargetProxy* proxy() const = 0;
 
diff --git a/src/gpu/ops/GrNonAAFillRectOp.cpp b/src/gpu/ops/GrNonAAFillRectOp.cpp
index fd8a9b3..c554232 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(target, GrPrimitiveType::kTriangles, kVertexStride, indexBuffer.get(),
-                             kVertsPerRect, kIndicesPerRect, rectCount);
-        void* vertices = helper.vertices();
+        PatternHelper helper(GrPrimitiveType::kTriangles);
+        void* vertices = helper.init(target, kVertexStride, indexBuffer.get(), kVertsPerRect,
+                                     kIndicesPerRect, rectCount);
         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, std::move(gp), pipe.fPipeline, pipe.fFixedDynamicState);
+        helper.recordDraw(target, gp.get(), pipe.fPipeline, pipe.fFixedDynamicState);
     }
 
-    CombineResult onCombineIfPossible(GrOp* t, const GrCaps& caps) override {
+    bool onCombineIfPossible(GrOp* t, const GrCaps& caps) override {
         NonAAFillRectOp* that = t->cast<NonAAFillRectOp>();
         if (!fHelper.isCompatible(that->fHelper, caps, this->bounds(), that->bounds())) {
-            return CombineResult::kCannotCombine;
+            return false;
         }
         fRects.push_back_n(that->fRects.count(), that->fRects.begin());
         this->joinBounds(*that);
-        return CombineResult::kMerged;
+        return true;
     }
 
     struct RectInfo {
@@ -325,9 +325,9 @@
         int rectCount = fRects.count();
 
         sk_sp<const GrBuffer> indexBuffer = target->resourceProvider()->refQuadIndexBuffer();
-        PatternHelper helper(target, GrPrimitiveType::kTriangles, vertexStride, indexBuffer.get(),
-                             kVertsPerRect, kIndicesPerRect, rectCount);
-        void* vertices = helper.vertices();
+        PatternHelper helper(GrPrimitiveType::kTriangles);
+        void* vertices = helper.init(target, vertexStride, indexBuffer.get(), kVertsPerRect,
+                                     kIndicesPerRect, rectCount);
         if (!vertices || !indexBuffer) {
             SkDebugf("Could not allocate vertices\n");
             return;
@@ -345,29 +345,29 @@
             }
         }
         auto pipe = fHelper.makePipeline(target);
-        helper.recordDraw(target, std::move(gp), pipe.fPipeline, pipe.fFixedDynamicState);
+        helper.recordDraw(target, gp.get(), pipe.fPipeline, pipe.fFixedDynamicState);
     }
 
-    CombineResult onCombineIfPossible(GrOp* t, const GrCaps& caps) override {
+    bool onCombineIfPossible(GrOp* t, const GrCaps& caps) override {
         NonAAFillRectPerspectiveOp* that = t->cast<NonAAFillRectPerspectiveOp>();
         if (!fHelper.isCompatible(that->fHelper, caps, this->bounds(), that->bounds())) {
-            return CombineResult::kCannotCombine;
+            return false;
         }
 
         // We could combine across perspective vm changes if we really wanted to.
         if (!fViewMatrix.cheapEqualTo(that->fViewMatrix)) {
-            return CombineResult::kCannotCombine;
+            return false;
         }
         if (fHasLocalRect != that->fHasLocalRect) {
-            return CombineResult::kCannotCombine;
+            return false;
         }
         if (fHasLocalMatrix && !fLocalMatrix.cheapEqualTo(that->fLocalMatrix)) {
-            return CombineResult::kCannotCombine;
+            return false;
         }
 
         fRects.push_back_n(that->fRects.count(), that->fRects.begin());
         this->joinBounds(*that);
-        return CombineResult::kMerged;
+        return true;
     }
 
     struct RectInfo {
diff --git a/src/gpu/ops/GrNonAAStrokeRectOp.cpp b/src/gpu/ops/GrNonAAStrokeRectOp.cpp
index 500b2eb..21636b2 100644
--- a/src/gpu/ops/GrNonAAStrokeRectOp.cpp
+++ b/src/gpu/ops/GrNonAAStrokeRectOp.cpp
@@ -188,14 +188,18 @@
             vertex[4].set(fRect.fLeft, fRect.fTop);
         }
 
-        GrMesh* mesh = target->allocMesh(primType);
-        mesh->setNonIndexedNonInstanced(vertexCount);
-        mesh->setVertexData(vertexBuffer, firstVertex);
+        GrMesh mesh(primType);
+        mesh.setNonIndexedNonInstanced(vertexCount);
+        mesh.setVertexData(vertexBuffer, firstVertex);
         auto pipe = fHelper.makePipeline(target);
-        target->draw(std::move(gp), pipe.fPipeline, pipe.fFixedDynamicState, mesh);
+        target->draw(gp.get(), pipe.fPipeline, pipe.fFixedDynamicState, mesh);
     }
 
-    // TODO: override onCombineIfPossible
+    bool onCombineIfPossible(GrOp* t, const GrCaps&) override {
+        // NonAA stroke rects cannot combine right now
+        // TODO make these combinable.
+        return false;
+    }
 
     Helper fHelper;
     GrColor fColor;
diff --git a/src/gpu/ops/GrOp.h b/src/gpu/ops/GrOp.h
index 57e028e..5c776fc 100644
--- a/src/gpu/ops/GrOp.h
+++ b/src/gpu/ops/GrOp.h
@@ -75,21 +75,9 @@
         // This default implementation assumes the op has no proxies
     }
 
-    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) {
+    bool combineIfPossible(GrOp* that, const GrCaps& caps) {
         if (this->classID() != that->classID()) {
-            return CombineResult::kCannotCombine;
+            return false;
         }
 
         return this->onCombineIfPossible(that, caps);
@@ -223,9 +211,7 @@
     static uint32_t GenOpClassID() { return GenID(&gCurrOpClassID); }
 
 private:
-    virtual CombineResult onCombineIfPossible(GrOp*, const GrCaps&) {
-        return CombineResult::kCannotCombine;
-    }
+    virtual bool onCombineIfPossible(GrOp*, const GrCaps& caps) = 0;
 
     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 c0158ba..54bb37c 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 = target->allocMesh(GrPrimitiveType::kTriangles);
-        mesh->setIndexed(indexBuffer, fIndexCount, firstIndex, 0, fVertCount - 1,
-                         GrPrimitiveRestart::kNo);
-        mesh->setVertexData(vertexBuffer, firstVertex);
+        GrMesh mesh(GrPrimitiveType::kTriangles);
+        mesh.setIndexed(indexBuffer, fIndexCount, firstIndex, 0, fVertCount - 1,
+                        GrPrimitiveRestart::kNo);
+        mesh.setVertexData(vertexBuffer, firstVertex);
         auto pipe = fHelper.makePipeline(target);
-        target->draw(std::move(gp), pipe.fPipeline, pipe.fFixedDynamicState, mesh);
+        target->draw(gp.get(), pipe.fPipeline, pipe.fFixedDynamicState, mesh);
     }
 
-    CombineResult onCombineIfPossible(GrOp* t, const GrCaps& caps) override {
+    bool 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 CombineResult::kCannotCombine;
+            return false;
         }
 
         if (!fHelper.isCompatible(that->fHelper, caps, this->bounds(), that->bounds())) {
-            return CombineResult::kCannotCombine;
+            return false;
         }
 
         if (fHelper.usesLocalCoords() &&
             !fViewMatrixIfUsingLocalCoords.cheapEqualTo(that->fViewMatrixIfUsingLocalCoords)) {
-            return CombineResult::kCannotCombine;
+            return false;
         }
 
         // 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 CombineResult::kMerged;
+        return true;
     }
 
     struct Circle {
@@ -1786,36 +1786,36 @@
             vertices += circle_type_to_vert_count(true) * kVertexStride;
         }
 
-        GrMesh* mesh = target->allocMesh(GrPrimitiveType::kTriangles);
-        mesh->setIndexed(indexBuffer, fIndexCount, firstIndex, 0, fVertCount - 1,
-                         GrPrimitiveRestart::kNo);
-        mesh->setVertexData(vertexBuffer, firstVertex);
+        GrMesh mesh(GrPrimitiveType::kTriangles);
+        mesh.setIndexed(indexBuffer, fIndexCount, firstIndex, 0, fVertCount - 1,
+                        GrPrimitiveRestart::kNo);
+        mesh.setVertexData(vertexBuffer, firstVertex);
         auto pipe = fHelper.makePipeline(target);
-        target->draw(std::move(gp), pipe.fPipeline, pipe.fFixedDynamicState, mesh);
+        target->draw(gp.get(), pipe.fPipeline, pipe.fFixedDynamicState, mesh);
     }
 
-    CombineResult onCombineIfPossible(GrOp* t, const GrCaps& caps) override {
+    bool 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 CombineResult::kCannotCombine;
+            return false;
         }
 
         if (!fHelper.isCompatible(that->fHelper, caps, this->bounds(), that->bounds())) {
-            return CombineResult::kCannotCombine;
+            return false;
         }
 
         if (fHelper.usesLocalCoords() &&
             !fViewMatrixIfUsingLocalCoords.cheapEqualTo(that->fViewMatrixIfUsingLocalCoords)) {
-            return CombineResult::kCannotCombine;
+            return false;
         }
 
         fCircles.push_back_n(that->fCircles.count(), that->fCircles.begin());
         this->joinBounds(*that);
         fVertCount += that->fVertCount;
         fIndexCount += that->fIndexCount;
-        return CombineResult::kMerged;
+        return true;
     }
 
     struct Circle {
@@ -1984,9 +1984,10 @@
         // Setup geometry processor
         sk_sp<GrGeometryProcessor> gp(new EllipseGeometryProcessor(fStroked, localMatrix));
 
+        QuadHelper helper;
         SkASSERT(sizeof(EllipseVertex) == gp->debugOnly_vertexStride());
-        QuadHelper helper(target, sizeof(EllipseVertex), fEllipses.count());
-        EllipseVertex* verts = reinterpret_cast<EllipseVertex*>(helper.vertices());
+        EllipseVertex* verts = reinterpret_cast<EllipseVertex*>(
+                helper.init(target, sizeof(EllipseVertex), fEllipses.count()));
         if (!verts) {
             return;
         }
@@ -2039,28 +2040,28 @@
             verts += kVerticesPerQuad;
         }
         auto pipe = fHelper.makePipeline(target);
-        helper.recordDraw(target, std::move(gp), pipe.fPipeline, pipe.fFixedDynamicState);
+        helper.recordDraw(target, gp.get(), pipe.fPipeline, pipe.fFixedDynamicState);
     }
 
-    CombineResult onCombineIfPossible(GrOp* t, const GrCaps& caps) override {
+    bool onCombineIfPossible(GrOp* t, const GrCaps& caps) override {
         EllipseOp* that = t->cast<EllipseOp>();
 
         if (!fHelper.isCompatible(that->fHelper, caps, this->bounds(), that->bounds())) {
-            return CombineResult::kCannotCombine;
+            return false;
         }
 
         if (fStroked != that->fStroked) {
-            return CombineResult::kCannotCombine;
+            return false;
         }
 
         if (fHelper.usesLocalCoords() &&
             !fViewMatrixIfUsingLocalCoords.cheapEqualTo(that->fViewMatrixIfUsingLocalCoords)) {
-            return CombineResult::kCannotCombine;
+            return false;
         }
 
         fEllipses.push_back_n(that->fEllipses.count(), that->fEllipses.begin());
         this->joinBounds(*that);
-        return CombineResult::kMerged;
+        return true;
     }
 
     struct Ellipse {
@@ -2218,8 +2219,9 @@
                 new DIEllipseGeometryProcessor(this->viewMatrix(), this->style()));
 
         SkASSERT(sizeof(DIEllipseVertex) == gp->debugOnly_vertexStride());
-        QuadHelper helper(target, sizeof(DIEllipseVertex), fEllipses.count());
-        DIEllipseVertex* verts = reinterpret_cast<DIEllipseVertex*>(helper.vertices());
+        QuadHelper helper;
+        DIEllipseVertex* verts = reinterpret_cast<DIEllipseVertex*>(
+                helper.init(target, sizeof(DIEllipseVertex), fEllipses.count()));
         if (!verts) {
             return;
         }
@@ -2272,27 +2274,27 @@
             verts += kVerticesPerQuad;
         }
         auto pipe = fHelper.makePipeline(target);
-        helper.recordDraw(target, std::move(gp), pipe.fPipeline, pipe.fFixedDynamicState);
+        helper.recordDraw(target, gp.get(), pipe.fPipeline, pipe.fFixedDynamicState);
     }
 
-    CombineResult onCombineIfPossible(GrOp* t, const GrCaps& caps) override {
+    bool onCombineIfPossible(GrOp* t, const GrCaps& caps) override {
         DIEllipseOp* that = t->cast<DIEllipseOp>();
         if (!fHelper.isCompatible(that->fHelper, caps, this->bounds(), that->bounds())) {
-            return CombineResult::kCannotCombine;
+            return false;
         }
 
         if (this->style() != that->style()) {
-            return CombineResult::kCannotCombine;
+            return false;
         }
 
         // TODO rewrite to allow positioning on CPU
         if (!this->viewMatrix().cheapEqualTo(that->viewMatrix())) {
-            return CombineResult::kCannotCombine;
+            return false;
         }
 
         fEllipses.push_back_n(that->fEllipses.count(), that->fEllipses.begin());
         this->joinBounds(*that);
-        return CombineResult::kMerged;
+        return true;
     }
 
     const SkMatrix& viewMatrix() const { return fEllipses[0].fViewMatrix; }
@@ -2723,29 +2725,29 @@
             currStartVertex += rrect_type_to_vert_count(rrect.fType);
         }
 
-        GrMesh* mesh = target->allocMesh(GrPrimitiveType::kTriangles);
-        mesh->setIndexed(indexBuffer, fIndexCount, firstIndex, 0, fVertCount - 1,
-                         GrPrimitiveRestart::kNo);
-        mesh->setVertexData(vertexBuffer, firstVertex);
+        GrMesh mesh(GrPrimitiveType::kTriangles);
+        mesh.setIndexed(indexBuffer, fIndexCount, firstIndex, 0, fVertCount - 1,
+                        GrPrimitiveRestart::kNo);
+        mesh.setVertexData(vertexBuffer, firstVertex);
         auto pipe = fHelper.makePipeline(target);
-        target->draw(std::move(gp), pipe.fPipeline, pipe.fFixedDynamicState, mesh);
+        target->draw(gp.get(), pipe.fPipeline, pipe.fFixedDynamicState, mesh);
     }
 
-    CombineResult onCombineIfPossible(GrOp* t, const GrCaps& caps) override {
+    bool 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 CombineResult::kCannotCombine;
+            return false;
         }
 
         if (!fHelper.isCompatible(that->fHelper, caps, this->bounds(), that->bounds())) {
-            return CombineResult::kCannotCombine;
+            return false;
         }
 
         if (fHelper.usesLocalCoords() &&
             !fViewMatrixIfUsingLocalCoords.cheapEqualTo(that->fViewMatrixIfUsingLocalCoords)) {
-            return CombineResult::kCannotCombine;
+            return false;
         }
 
         fRRects.push_back_n(that->fRRects.count(), that->fRRects.begin());
@@ -2753,7 +2755,7 @@
         fVertCount += that->fVertCount;
         fIndexCount += that->fIndexCount;
         fAllFill = fAllFill && that->fAllFill;
-        return CombineResult::kMerged;
+        return true;
     }
 
     struct RRect {
@@ -2925,10 +2927,10 @@
         sk_sp<const GrBuffer> indexBuffer = get_rrect_index_buffer(
                 fStroked ? kStroke_RRectType : kFill_RRectType, target->resourceProvider());
 
-        PatternHelper helper(target, GrPrimitiveType::kTriangles, sizeof(EllipseVertex),
-                             indexBuffer.get(), kVertsPerStandardRRect, indicesPerInstance,
-                             fRRects.count());
-        EllipseVertex* verts = reinterpret_cast<EllipseVertex*>(helper.vertices());
+        PatternHelper helper(GrPrimitiveType::kTriangles);
+        EllipseVertex* verts = reinterpret_cast<EllipseVertex*>(
+                helper.init(target, sizeof(EllipseVertex), indexBuffer.get(),
+                            kVertsPerStandardRRect, indicesPerInstance, fRRects.count()));
         if (!verts || !indexBuffer) {
             SkDebugf("Could not allocate vertices\n");
             return;
@@ -2995,28 +2997,28 @@
             }
         }
         auto pipe = fHelper.makePipeline(target);
-        helper.recordDraw(target, std::move(gp), pipe.fPipeline, pipe.fFixedDynamicState);
+        helper.recordDraw(target, gp.get(), pipe.fPipeline, pipe.fFixedDynamicState);
     }
 
-    CombineResult onCombineIfPossible(GrOp* t, const GrCaps& caps) override {
+    bool onCombineIfPossible(GrOp* t, const GrCaps& caps) override {
         EllipticalRRectOp* that = t->cast<EllipticalRRectOp>();
 
         if (!fHelper.isCompatible(that->fHelper, caps, this->bounds(), that->bounds())) {
-            return CombineResult::kCannotCombine;
+            return false;
         }
 
         if (fStroked != that->fStroked) {
-            return CombineResult::kCannotCombine;
+            return false;
         }
 
         if (fHelper.usesLocalCoords() &&
             !fViewMatrixIfUsingLocalCoords.cheapEqualTo(that->fViewMatrixIfUsingLocalCoords)) {
-            return CombineResult::kCannotCombine;
+            return false;
         }
 
         fRRects.push_back_n(that->fRRects.count(), that->fRRects.begin());
         this->joinBounds(*that);
-        return CombineResult::kMerged;
+        return true;
     }
 
     struct RRect {
diff --git a/src/gpu/ops/GrRegionOp.cpp b/src/gpu/ops/GrRegionOp.cpp
index b876295..d0c24e6 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(target, GrPrimitiveType::kTriangles, kVertexStride, indexBuffer.get(),
-                             kVertsPerInstance, kIndicesPerInstance, numRects);
-        void* vertices = helper.vertices();
+        PatternHelper helper(GrPrimitiveType::kTriangles);
+        void* vertices = helper.init(target, kVertexStride, indexBuffer.get(), kVertsPerInstance,
+                                     kIndicesPerInstance, numRects);
         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, std::move(gp), pipe.fPipeline, pipe.fFixedDynamicState);
+        helper.recordDraw(target, gp.get(), pipe.fPipeline, pipe.fFixedDynamicState);
     }
 
-    CombineResult onCombineIfPossible(GrOp* t, const GrCaps& caps) override {
+    bool onCombineIfPossible(GrOp* t, const GrCaps& caps) override {
         RegionOp* that = t->cast<RegionOp>();
         if (!fHelper.isCompatible(that->fHelper, caps, this->bounds(), that->bounds())) {
-            return CombineResult::kCannotCombine;
+            return false;
         }
 
         if (fViewMatrix != that->fViewMatrix) {
-            return CombineResult::kCannotCombine;
+            return false;
         }
 
         fRegions.push_back_n(that->fRegions.count(), that->fRegions.begin());
         this->joinBounds(*that);
-        return CombineResult::kMerged;
+        return true;
     }
 
     struct RegionInfo {
diff --git a/src/gpu/ops/GrSemaphoreOp.h b/src/gpu/ops/GrSemaphoreOp.h
index 53c76c3..234c76c 100644
--- a/src/gpu/ops/GrSemaphoreOp.h
+++ b/src/gpu/ops/GrSemaphoreOp.h
@@ -34,6 +34,7 @@
     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 69489a2..09a2442 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 = 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);
+        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);
     }
 
-    CombineResult onCombineIfPossible(GrOp* t, const GrCaps& caps) override {
+    bool 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 CombineResult::kMerged;
+        return true;
     }
 
     SkSTArray<1, Geometry, true> fGeoData;
diff --git a/src/gpu/ops/GrSimpleMeshDrawOpHelper.cpp b/src/gpu/ops/GrSimpleMeshDrawOpHelper.cpp
index f1692e6..7ed74c3 100644
--- a/src/gpu/ops/GrSimpleMeshDrawOpHelper.cpp
+++ b/src/gpu/ops/GrSimpleMeshDrawOpHelper.cpp
@@ -135,8 +135,7 @@
 }
 
 auto GrSimpleMeshDrawOpHelper::internalMakePipeline(GrMeshDrawOp::Target* target,
-                                                    const GrPipeline::InitArgs& args,
-                                                    int numPrimitiveProcessorProxies)
+                                                    const GrPipeline::InitArgs& args)
         -> PipelineAndFixedDynamicState {
     // A caller really should only call this once as the processor set and applied clip get
     // moved into the GrPipeline.
@@ -144,12 +143,8 @@
     SkDEBUGCODE(fMadePipeline = true);
     auto clip = target->detachAppliedClip();
     GrPipeline::FixedDynamicState* fixedDynamicState = nullptr;
-    if (clip.scissorState().enabled() || numPrimitiveProcessorProxies) {
+    if (clip.scissorState().enabled()) {
         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)),
@@ -181,12 +176,11 @@
            fStencilSettings == that.fStencilSettings;
 }
 
-auto GrSimpleMeshDrawOpHelperWithStencil::makePipeline(GrMeshDrawOp::Target* target,
-                                                       int numPrimitiveProcessorTextures)
+auto GrSimpleMeshDrawOpHelperWithStencil::makePipeline(GrMeshDrawOp::Target* target)
         -> PipelineAndFixedDynamicState {
     auto args = INHERITED::pipelineInitArgs(target);
     args.fUserStencil = fStencilSettings;
-    return this->internalMakePipeline(target, args, numPrimitiveProcessorTextures);
+    return this->internalMakePipeline(target, args);
 }
 
 SkString GrSimpleMeshDrawOpHelperWithStencil::dumpInfo() const {
diff --git a/src/gpu/ops/GrSimpleMeshDrawOpHelper.h b/src/gpu/ops/GrSimpleMeshDrawOpHelper.h
index cd72786..2bffa09 100644
--- a/src/gpu/ops/GrSimpleMeshDrawOpHelper.h
+++ b/src/gpu/ops/GrSimpleMeshDrawOpHelper.h
@@ -88,10 +88,8 @@
 
     using PipelineAndFixedDynamicState = GrOpFlushState::PipelineAndFixedDynamicState;
     /** Makes a pipeline that consumes the processor set and the op's applied clip. */
-    PipelineAndFixedDynamicState makePipeline(GrMeshDrawOp::Target* target,
-                                              int numPrimitiveProcessorTextures = 0) {
-        return this->internalMakePipeline(target, this->pipelineInitArgs(target),
-                                          numPrimitiveProcessorTextures);
+    PipelineAndFixedDynamicState makePipeline(GrMeshDrawOp::Target* target) {
+        return this->internalMakePipeline(target, this->pipelineInitArgs(target));
     }
 
     struct MakeArgs {
@@ -118,8 +116,7 @@
     GrPipeline::InitArgs pipelineInitArgs(GrMeshDrawOp::Target* target) const;
 
     PipelineAndFixedDynamicState internalMakePipeline(GrMeshDrawOp::Target*,
-                                                      const GrPipeline::InitArgs&,
-                                                      int numPrimitiveProcessorTextures);
+                                                      const GrPipeline::InitArgs&);
 
 private:
     GrProcessorSet* fProcessors;
@@ -165,8 +162,7 @@
     bool isCompatible(const GrSimpleMeshDrawOpHelperWithStencil& that, const GrCaps&,
                       const SkRect& thisBounds, const SkRect& thatBounds) const;
 
-    PipelineAndFixedDynamicState makePipeline(GrMeshDrawOp::Target*,
-                                              int numPrimitiveProcessorTextures = 0);
+    PipelineAndFixedDynamicState makePipeline(GrMeshDrawOp::Target*);
 
     SkString dumpInfo() const;
 
diff --git a/src/gpu/ops/GrSmallPathRenderer.cpp b/src/gpu/ops/GrSmallPathRenderer.cpp
index c480bc0..286fab7 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;
-        GrPipeline::FixedDynamicState* fFixedDynamicState;
+        const GrPipeline::FixedDynamicState* fFixedDynamicState;
         int fVertexOffset;
         int fInstancesToFlush;
     };
@@ -312,15 +312,7 @@
     void onPrepareDraws(Target* target) override {
         int instanceCount = fShapes.count();
 
-        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();
-        }
+        auto pipe = fHelper.makePipeline(target);
 
         FlushInfo flushInfo;
         flushInfo.fPipeline = pipe.fPipeline;
@@ -815,12 +807,7 @@
 
     void flush(GrMeshDrawOp::Target* target, FlushInfo* flushInfo) const {
         GrGeometryProcessor* gp = flushInfo->fGeometryProcessor.get();
-        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();
-            }
+        if (gp->numTextureSamplers() != (int)fAtlas->numActivePages()) {
             // During preparation the number of atlas pages has increased.
             // Update the proxies used in the GP to match.
             if (fUsesDistanceField) {
@@ -833,14 +820,14 @@
         }
 
         if (flushInfo->fInstancesToFlush) {
-            GrMesh* mesh = target->allocMesh(GrPrimitiveType::kTriangles);
+            GrMesh mesh(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, flushInfo->fPipeline,
+            mesh.setIndexedPatterned(flushInfo->fIndexBuffer.get(), kIndicesPerQuad,
+                                     kVerticesPerQuad, flushInfo->fInstancesToFlush,
+                                     maxInstancesPerDraw);
+            mesh.setVertexData(flushInfo->fVertexBuffer.get(), flushInfo->fVertexOffset);
+            target->draw(flushInfo->fGeometryProcessor.get(), flushInfo->fPipeline,
                          flushInfo->fFixedDynamicState, mesh);
             flushInfo->fVertexOffset += kVerticesPerQuad * flushInfo->fInstancesToFlush;
             flushInfo->fInstancesToFlush = 0;
@@ -850,41 +837,41 @@
     GrColor color() const { return fShapes[0].fColor; }
     bool usesDistanceField() const { return fUsesDistanceField; }
 
-    CombineResult onCombineIfPossible(GrOp* t, const GrCaps& caps) override {
+    bool onCombineIfPossible(GrOp* t, const GrCaps& caps) override {
         SmallPathOp* that = t->cast<SmallPathOp>();
         if (!fHelper.isCompatible(that->fHelper, caps, this->bounds(), that->bounds())) {
-            return CombineResult::kCannotCombine;
+            return false;
         }
 
         if (this->usesDistanceField() != that->usesDistanceField()) {
-            return CombineResult::kCannotCombine;
+            return false;
         }
 
         const SkMatrix& thisCtm = this->fShapes[0].fViewMatrix;
         const SkMatrix& thatCtm = that->fShapes[0].fViewMatrix;
 
         if (thisCtm.hasPerspective() != thatCtm.hasPerspective()) {
-            return CombineResult::kCannotCombine;
+            return false;
         }
 
         // 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 CombineResult::kCannotCombine;
+            return false;
         }
 
         // 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 CombineResult::kCannotCombine;
+                return false;
             }
         }
 
         fShapes.push_back_n(that->fShapes.count(), that->fShapes.begin());
         this->joinBounds(*that);
-        return CombineResult::kMerged;
+        return true;
     }
 
     bool fUsesDistanceField;
diff --git a/src/gpu/ops/GrStencilPathOp.h b/src/gpu/ops/GrStencilPathOp.h
index edfcaa8..563bdc3 100644
--- a/src/gpu/ops/GrStencilPathOp.h
+++ b/src/gpu/ops/GrStencilPathOp.h
@@ -56,6 +56,8 @@
         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 f722868..2fea9cb 100644
--- a/src/gpu/ops/GrTessellatingPathRenderer.cpp
+++ b/src/gpu/ops/GrTessellatingPathRenderer.cpp
@@ -237,7 +237,7 @@
         return path;
     }
 
-    void draw(Target* target, sk_sp<const GrGeometryProcessor> gp, size_t vertexStride) {
+    void draw(Target* target, 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, std::move(gp), cachedVertexBuffer.get(), 0, actualCount);
+            this->drawVertices(target, gp, cachedVertexBuffer.get(), 0, actualCount);
             return;
         }
 
@@ -280,7 +280,7 @@
         if (count == 0) {
             return;
         }
-        this->drawVertices(target, std::move(gp), allocator.vertexBuffer(), 0, count);
+        this->drawVertices(target, 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, sk_sp<const GrGeometryProcessor> gp, size_t vertexStride) {
+    void drawAA(Target* target, const GrGeometryProcessor* gp, size_t vertexStride) {
         SkASSERT(fAntiAlias);
         SkPath path = getPath();
         if (path.isEmpty()) {
@@ -306,8 +306,7 @@
         if (count == 0) {
             return;
         }
-        this->drawVertices(target, std::move(gp), allocator.vertexBuffer(), allocator.firstVertex(),
-                           count);
+        this->drawVertices(target, gp, allocator.vertexBuffer(), allocator.firstVertex(), count);
     }
 
     void onPrepareDraws(Target* target) override {
@@ -350,22 +349,23 @@
         }
         SkASSERT(vertexStride == gp->debugOnly_vertexStride());
         if (fAntiAlias) {
-            this->drawAA(target, std::move(gp), vertexStride);
+            this->drawAA(target, gp.get(), vertexStride);
         } else {
-            this->draw(target, std::move(gp), vertexStride);
+            this->draw(target, gp.get(), vertexStride);
         }
     }
 
-    void drawVertices(Target* target, sk_sp<const GrGeometryProcessor> gp, const GrBuffer* vb,
+    void drawVertices(Target* target, const GrGeometryProcessor* gp, const GrBuffer* vb,
                       int firstVertex, int count) {
-        GrMesh* mesh = target->allocMesh(TESSELLATOR_WIREFRAME ? GrPrimitiveType::kLines
-                                                               : GrPrimitiveType::kTriangles);
-        mesh->setNonIndexedNonInstanced(count);
-        mesh->setVertexData(vb, firstVertex);
+        GrMesh mesh(TESSELLATOR_WIREFRAME ? GrPrimitiveType::kLines : GrPrimitiveType::kTriangles);
+        mesh.setNonIndexedNonInstanced(count);
+        mesh.setVertexData(vb, firstVertex);
         auto pipe = fHelper.makePipeline(target);
-        target->draw(std::move(gp), pipe.fPipeline, pipe.fFixedDynamicState, mesh);
+        target->draw(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 2ba3639..72900ed 100644
--- a/src/gpu/ops/GrTextureOp.cpp
+++ b/src/gpu/ops/GrTextureOp.cpp
@@ -37,6 +37,8 @@
 
 namespace {
 
+enum class MultiTexture : bool { kNo = false, kYes = true };
+
 enum class Domain : bool { kNo = false, kYes = true };
 
 /**
@@ -53,40 +55,79 @@
         SkPoint fTextureCoords;
     };
 
-    template <typename Pos, Domain D> struct OptionalDomainVertex;
+    template <typename Pos, MultiTexture MT> struct OptionalMultiTextureVertex;
     template <typename Pos>
-    struct OptionalDomainVertex<Pos, Domain::kNo> : VertexCommon<Pos> {
-        static constexpr Domain kDomain = Domain::kNo;
+    struct OptionalMultiTextureVertex<Pos, MultiTexture::kNo> : VertexCommon<Pos> {
+        static constexpr MultiTexture kMultiTexture = MultiTexture::kNo;
     };
     template <typename Pos>
-    struct OptionalDomainVertex<Pos, Domain::kYes> : VertexCommon<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> {
+        static constexpr Domain kDomain = Domain::kNo;
+    };
+    template <typename Pos, MultiTexture MT>
+    struct OptionalDomainVertex<Pos, MT, Domain::kYes> : OptionalMultiTextureVertex<Pos, MT> {
         static constexpr Domain kDomain = Domain::kYes;
         SkRect fTextureDomain;
     };
 
-    template <typename Pos, Domain D, GrAA> struct OptionalAAVertex;
-    template <typename Pos, Domain D>
-    struct OptionalAAVertex<Pos, D, GrAA::kNo> : OptionalDomainVertex<Pos, D> {
+    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> {
         static constexpr GrAA kAA = GrAA::kNo;
     };
-    template <typename Pos, Domain D>
-    struct OptionalAAVertex<Pos, D, GrAA::kYes> : OptionalDomainVertex<Pos, D> {
+    template <typename Pos, MultiTexture MT, Domain D>
+    struct OptionalAAVertex<Pos, MT, D, GrAA::kYes> : OptionalDomainVertex<Pos, MT, D> {
         static constexpr GrAA kAA = GrAA::kYes;
         SkPoint3 fEdges[4];
     };
 
-    template <typename Pos, Domain D, GrAA AA>
-    using Vertex = OptionalAAVertex<Pos, D, AA>;
+    template <typename Pos, MultiTexture MT, Domain D, GrAA AA>
+    using Vertex = OptionalAAVertex<Pos, MT, D, AA>;
 
-    static sk_sp<GrGeometryProcessor> Make(GrTextureType textureType, GrPixelConfig textureConfig,
-                                           const GrSamplerState::Filter filter,
+    // 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,
                                            sk_sp<GrColorSpaceXform> textureColorSpaceXform,
                                            sk_sp<GrColorSpaceXform> paintColorSpaceXform,
-                                           bool coverageAA, bool perspective, Domain domain,
+                                           bool coverageAA,
+                                           bool perspective, Domain domain,
+                                           const GrSamplerState::Filter filters[],
                                            const GrShaderCaps& caps) {
-        return sk_sp<TextureGeometryProcessor>(new TextureGeometryProcessor(
-                textureType, textureConfig, filter, std::move(textureColorSpaceXform),
-                std::move(paintColorSpaceXform), coverageAA, perspective, domain, 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();
+        }
     }
 
     const char* name() const override { return "TextureGeometryProcessor"; }
@@ -156,10 +197,28 @@
                     args.fFragBuilder->codeAppend(
                             "texCoord = clamp(texCoord, domain.xy, domain.zw);");
                 }
-                args.fFragBuilder->codeAppendf("%s = ", args.fOutputColor);
-                args.fFragBuilder->appendTextureLookupAndModulate(
-                        args.fOutputColor, args.fTexSamplers[0], "texCoord", kFloat2_GrSLType,
-                        &fTextureColorSpaceXformHelper);
+                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->codeAppend(";");
                 if (textureGP.usesCoverageEdgeAA()) {
                     bool mulByFragCoordW = false;
@@ -212,16 +271,36 @@
     bool usesCoverageEdgeAA() const { return SkToBool(fAAEdges[0].isInitialized()); }
 
 private:
-    TextureGeometryProcessor(GrTextureType textureType, GrPixelConfig textureConfig,
-                             GrSamplerState::Filter filter,
+    // 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,
                              sk_sp<GrColorSpaceXform> textureColorSpaceXform,
-                             sk_sp<GrColorSpaceXform> paintColorSpaceXform, bool coverageAA,
-                             bool perspective, Domain domain, const GrShaderCaps& caps)
+                             sk_sp<GrColorSpaceXform> paintColorSpaceXform,
+                             bool coverageAA, bool perspective, Domain domain,
+                             const GrSamplerState::Filter filters[], const GrShaderCaps& caps)
             : INHERITED(kTextureGeometryProcessor_ClassID)
             , fTextureColorSpaceXform(std::move(textureColorSpaceXform))
-            , fPaintColorSpaceXform(std::move(paintColorSpaceXform))
-            , fSampler(textureType, textureConfig, filter) {
-        this->setTextureSamplerCnt(1);
+            , 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]);
+        }
 
         if (perspective) {
             fPositions = {"position", kFloat3_GrVertexAttribType};
@@ -232,6 +311,17 @@
         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;
@@ -244,23 +334,25 @@
             vertexAttributeCnt += 4;
         }
         this->setVertexAttributeCnt(vertexAttributeCnt);
+        this->setTextureSamplerCnt(samplerCnt);
     }
 
     const Attribute& onVertexAttribute(int i) const override {
-        return IthInitializedAttribute(i, fPositions, fColors, fTextureCoords, fDomain, fAAEdges[0],
-                                       fAAEdges[1], fAAEdges[2], fAAEdges[3]);
+        return IthInitializedAttribute(i, fPositions, fColors, fTextureCoords, fTextureIdx, fDomain,
+                                       fAAEdges[0], fAAEdges[1], fAAEdges[2], fAAEdges[3]);
     }
 
-    const TextureSampler& onTextureSampler(int) const override { return fSampler; }
+    const TextureSampler& onTextureSampler(int i) const override { return fSamplers[i]; }
 
     Attribute fPositions;
     Attribute fColors;
     Attribute fTextureCoords;
+    Attribute fTextureIdx;
     Attribute fDomain;
     Attribute fAAEdges[4];
     sk_sp<GrColorSpaceXform> fTextureColorSpaceXform;
     sk_sp<GrColorSpaceXform> fPaintColorSpaceXform;
-    TextureSampler fSampler;
+    TextureSampler fSamplers[1];
 
     typedef GrGeometryProcessor INHERITED;
 };
@@ -435,6 +527,20 @@
     }
 };
 
+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> {
@@ -479,7 +585,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, Domain domain) {
+                            SkScalar iw, SkScalar ih, int textureIdx, Domain domain) {
     SkRect texRect = {
             iw * srcRect.fLeft,
             ih * srcRect.fTop,
@@ -495,6 +601,7 @@
     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);
 }
 
@@ -525,27 +632,42 @@
 
     ~TextureOp() override {
         if (fFinalized) {
-            fProxy->completedRead();
+            auto proxies = this->proxies();
+            for (int i = 0; i < fProxyCnt; ++i) {
+                proxies[i]->completedRead();
+            }
+            if (fProxyCnt > 1) {
+                delete[] reinterpret_cast<const char*>(proxies);
+            }
         } else {
-            fProxy->unref();
+            SkASSERT(1 == fProxyCnt);
+            fProxy0->unref();
         }
     }
 
     const char* name() const override { return "TextureOp"; }
 
-    void visitProxies(const VisitProxyFunc& func) const override { func(fProxy); }
+    void visitProxies(const VisitProxyFunc& func) const override {
+        auto proxies = this->proxies();
+        for (int i = 0; i < fProxyCnt; ++i) {
+            func(proxies[i]);
+        }
+    }
 
     SkString dumpInfo() const override {
         SkString str;
         str.appendf("# draws: %d\n", fDraws.count());
-        str.appendf("Proxy ID: %d, Filter: %d\n", fProxy->uniqueID().asUInt(),
-                    static_cast<int>(fFilter));
+        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]));
+        }
         for (int i = 0; i < fDraws.count(); ++i) {
             const Draw& draw = fDraws[i];
             str.appendf(
-                    "%d: Color: 0x%08x, TexRect [L: %.2f, T: %.2f, R: %.2f, B: %.2f] "
+                    "%d: Color: 0x%08x, ProxyIdx: %d, TexRect [L: %.2f, T: %.2f, R: %.2f, B: %.2f] "
                     "Quad [(%.2f, %.2f), (%.2f, %.2f), (%.2f, %.2f), (%.2f, %.2f)]\n",
-                    i, draw.color(), draw.srcRect().fLeft, draw.srcRect().fTop,
+                    i, draw.color(), draw.textureIdx(), 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,
@@ -557,9 +679,10 @@
 
     RequiresDstTexture finalize(const GrCaps& caps, const GrAppliedClip* clip) override {
         SkASSERT(!fFinalized);
+        SkASSERT(1 == fProxyCnt);
         fFinalized = true;
-        fProxy->addPendingRead();
-        fProxy->unref();
+        fProxy0->addPendingRead();
+        fProxy0->unref();
         return RequiresDstTexture::kNo;
     }
 
@@ -573,6 +696,17 @@
 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,
@@ -581,8 +715,9 @@
             : INHERITED(ClassID())
             , fTextureColorSpaceXform(std::move(textureColorSpaceXform))
             , fPaintColorSpaceXform(std::move(paintColorSpaceXform))
-            , fProxy(proxy.release())
-            , fFilter(filter)
+            , fProxy0(proxy.release())
+            , fFilter0(filter)
+            , fProxyCnt(1)
             , fAAType(static_cast<unsigned>(aaType))
             , fFinalized(0) {
         SkASSERT(aaType != GrAAType::kMixedSamples);
@@ -605,39 +740,46 @@
             }
         }
 #endif
-        const auto& draw = fDraws.emplace_back(srcRect, quad, constraint, color);
+        const auto& draw = fDraws.emplace_back(srcRect, 0, quad, constraint, color);
         this->setBounds(bounds, HasAABloat::kNo, IsZeroArea::kNo);
         fDomain = static_cast<bool>(draw.domain());
+        fMaxApproxDstPixelArea = RectSizeAsSizeT(bounds);
     }
 
-    template <typename Pos, Domain D, GrAA AA>
-    void tess(void* v, const GrGeometryProcessor* gp) {
-        using Vertex = TextureGeometryProcessor::Vertex<Pos, D, AA>;
+    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>;
         SkASSERT(gp->debugOnly_vertexStride() == sizeof(Vertex));
         auto vertices = static_cast<Vertex*>(v);
-        auto origin = fProxy->origin();
-        const auto* texture = fProxy->peekTexture();
-        float iw = 1.f / texture->width();
-        float ih = 1.f / texture->height();
-
+        auto proxies = this->proxies();
+        auto filters = this->filters();
         for (const auto& draw : fDraws) {
-            tessellate_quad<Vertex>(draw.quad(), draw.srcRect(), draw.color(), origin, fFilter,
-                                    vertices, iw, ih, draw.domain());
+            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());
             vertices += 4;
         }
     }
 
     void onPrepareDraws(Target* target) override {
-        if (!fProxy->instantiate(target->resourceProvider())) {
-            return;
+        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]);
         }
 
         Domain domain = fDomain ? Domain::kYes : Domain::kNo;
         bool coverageAA = GrAAType::kCoverage == this->aaType();
         sk_sp<GrGeometryProcessor> gp = TextureGeometryProcessor::Make(
-                fProxy->textureType(), fProxy->config(), fFilter,
-                std::move(fTextureColorSpaceXform), std::move(fPaintColorSpaceXform), coverageAA,
-                fPerspective, domain, *target->caps().shaderCaps());
+                proxiesSPs, fProxyCnt, std::move(fTextureColorSpaceXform),
+                std::move(fPaintColorSpaceXform), coverageAA, fPerspective,
+                domain, filters, *target->caps().shaderCaps());
         GrPipeline::InitArgs args;
         args.fProxy = target->proxy();
         args.fCaps = &target->caps();
@@ -648,34 +790,43 @@
         }
 
         auto clip = target->detachAppliedClip();
-        auto* fixedDynamicState = target->allocFixedDynamicState(clip.scissorState().rect(), 1);
-        fixedDynamicState->fPrimitiveProcessorTextures[0] = fProxy;
+        const auto* fixedDynamicState = target->allocFixedDynamicState(clip.scissorState().rect());
         const auto* pipeline =
                 target->allocPipeline(args, GrProcessorSet::MakeEmptySet(), std::move(clip));
-        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>) \
+        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>) \
     }
         static constexpr struct {
             TessFn fTessFn;
             size_t fVertexSize;
         } kTessFnsAndVertexSizes[] = {
-                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),
+                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),
         };
 #undef TESS_FN_AND_VERTEX_SIZE
         int tessFnIdx = 0;
-        tessFnIdx |= coverageAA   ? 0x1 : 0x0;
-        tessFnIdx |= fDomain      ? 0x2 : 0x0;
-        tessFnIdx |= fPerspective ? 0x4 : 0x0;
+        tessFnIdx |= coverageAA      ? 0x1 : 0x0;
+        tessFnIdx |= fDomain         ? 0x2 : 0x0;
+        tessFnIdx |= (fProxyCnt > 1) ? 0x4 : 0x0;
+        tessFnIdx |= fPerspective    ? 0x8 : 0x0;
 
         SkASSERT(kTessFnsAndVertexSizes[tessFnIdx].fVertexSize == gp->debugOnly_vertexStride());
 
@@ -688,75 +839,212 @@
             return;
         }
 
-        (this->*(kTessFnsAndVertexSizes[tessFnIdx].fTessFn))(vdata, gp.get());
+        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());
 
         GrPrimitiveType primitiveType =
                 fDraws.count() > 1 ? GrPrimitiveType::kTriangles : GrPrimitiveType::kTriangleStrip;
-        GrMesh* mesh = target->allocMesh(primitiveType);
+        GrMesh mesh(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(std::move(gp), pipeline, fixedDynamicState, mesh);
+        mesh.setVertexData(vbuffer, vstart);
+        target->draw(gp.get(), pipeline, fixedDynamicState, mesh);
     }
 
-    CombineResult onCombineIfPossible(GrOp* t, const GrCaps&) override {
+    bool onCombineIfPossible(GrOp* t, const GrCaps& caps) override {
         const auto* that = t->cast<TextureOp>();
+        const auto& shaderCaps = *caps.shaderCaps();
         if (!GrColorSpaceXform::Equals(fTextureColorSpaceXform.get(),
                                        that->fTextureColorSpaceXform.get())) {
-            return CombineResult::kCannotCombine;
+            return false;
         }
         if (!GrColorSpaceXform::Equals(fPaintColorSpaceXform.get(),
                                        that->fPaintColorSpaceXform.get())) {
-            return CombineResult::kCannotCombine;
+            return false;
         }
         if (this->aaType() != that->aaType()) {
-            return CombineResult::kCannotCombine;
+            return false;
         }
-        if (fProxy->uniqueID() != that->fProxy->uniqueID() || fFilter != that->fFilter) {
-            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());
         }
-        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 CombineResult::kMerged;
+        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;
     }
 
     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, const GrPerspQuad& quad, SkCanvas::SrcRectConstraint constraint,
-             GrColor color)
+        Draw(const SkRect& srcRect, int textureIdx, const GrPerspQuad& quad,
+             SkCanvas::SrcRectConstraint constraint, GrColor color)
                 : fSrcRect(srcRect)
+                , fHasDomain(constraint == SkCanvas::kStrict_SrcRectConstraint)
+                , fTextureIdx(SkToUInt(textureIdx))
                 , fQuad(quad)
-                , fColor(color)
-                , fHasDomain(constraint == SkCanvas::kStrict_SrcRectConstraint) {}
+                , fColor(color) {}
         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;
-    GrTextureProxy* fProxy;
-    GrSamplerState::Filter fFilter;
+    // 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;
     unsigned fAAType : 2;
     unsigned fPerspective : 1;
     unsigned fDomain : 1;
@@ -766,6 +1054,9 @@
     typedef GrMeshDrawOp INHERITED;
 };
 
+constexpr int TextureGeometryProcessor::kMaxTextures;
+constexpr int TextureOp::kMaxTextures;
+
 }  // anonymous namespace
 
 namespace GrTextureOp {