Specialize vertex writing function for common quad VertexSpecs

Change-Id: I204f04596809e1e33f20b67b454cdd0e1f269630
Reviewed-on: https://skia-review.googlesource.com/c/skia/+/255303
Commit-Queue: Michael Ludwig <michaelludwig@google.com>
Reviewed-by: Robert Phillips <robertphillips@google.com>
diff --git a/src/gpu/ops/GrFillRectOp.cpp b/src/gpu/ops/GrFillRectOp.cpp
index 2ee183a..d9d1a5b 100644
--- a/src/gpu/ops/GrFillRectOp.cpp
+++ b/src/gpu/ops/GrFillRectOp.cpp
@@ -218,14 +218,15 @@
 
         // vertices pointer advances through vdata based on Tessellate's return value
         void* vertices = vdata;
+        GrQuadPerEdgeAA::Tessellator tessellator(vertexSpec);
         auto iter = fQuads.iterator();
         while(iter.next()) {
             // All entries should have local coords, or no entries should have local coords,
             // matching !helper.isTrivial() (which is more conservative than helper.usesLocalCoords)
             SkASSERT(iter.isLocalValid() != fHelper.isTrivial());
             auto info = iter.metadata();
-            vertices = GrQuadPerEdgeAA::Tessellate(vertices, vertexSpec, iter.deviceQuad(),
-                    info.fColor, iter.localQuad(), kEmptyDomain, info.fAAFlags);
+            vertices = tessellator.append(vertices, iter.deviceQuad(), iter.localQuad(),
+                                          info.fColor, kEmptyDomain, info.fAAFlags);
         }
 
         sk_sp<const GrBuffer> indexBuffer;
diff --git a/src/gpu/ops/GrQuadPerEdgeAA.cpp b/src/gpu/ops/GrQuadPerEdgeAA.cpp
index 6833107..c0608b8 100644
--- a/src/gpu/ops/GrQuadPerEdgeAA.cpp
+++ b/src/gpu/ops/GrQuadPerEdgeAA.cpp
@@ -21,14 +21,14 @@
 
 namespace {
 
-// Writes four vertices in triangle strip order, including the additional data for local
-// coordinates, geometry + texture domains, color, and coverage as needed to satisfy the vertex spec
-static void write_quad(GrVertexWriter* vb, const GrQuadPerEdgeAA::VertexSpec& spec,
-                       GrQuadPerEdgeAA::CoverageMode mode, const skvx::Vec<4, float>& coverage,
-                       SkPMColor4f color4f, const SkRect& geomDomain, const SkRect& texDomain,
-                       const GrQuad& deviceQuad, const GrQuad& localQuad) {
+// Generic WriteQuadProc that can handle any VertexSpec. It writes the 4 vertices in triangle strip
+// order, although the data per-vertex is dependent on the VertexSpec.
+static void write_quad_generic(GrVertexWriter* vb, const GrQuadPerEdgeAA::VertexSpec& spec,
+                               const GrQuad& deviceQuad, const GrQuad& localQuad,
+                               const float coverage[4], const SkPMColor4f& color,
+                               const SkRect& geomDomain, const SkRect& texDomain) {
     static constexpr auto If = GrVertexWriter::If<float>;
-
+    GrQuadPerEdgeAA::CoverageMode mode = spec.coverageMode();
     for (int i = 0; i < 4; ++i) {
         // save position, this is a float2 or float3 or float4 depending on the combination of
         // perspective and coverage mode.
@@ -40,7 +40,7 @@
         if (spec.hasVertexColors()) {
             bool wide = spec.colorType() == GrQuadPerEdgeAA::ColorType::kHalf;
             vb->write(GrVertexColor(
-                color4f * (mode == GrQuadPerEdgeAA::CoverageMode::kWithColor ? coverage[i] : 1.f),
+                color * (mode == GrQuadPerEdgeAA::CoverageMode::kWithColor ? coverage[i] : 1.f),
                 wide));
         }
 
@@ -62,6 +62,166 @@
     }
 }
 
+// Specialized WriteQuadProcs for particular VertexSpecs that show up frequently (determined
+// experimentally through recorded GMs, SKPs, and SVGs, as well as SkiaRenderer's usage patterns):
+
+// 2D (XY), no explicit coverage, vertex color, no locals, no geometry domain, no texture domain
+// This represents simple, solid color or shader, non-AA (or AA with cov. as alpha) rects.
+static void write_2d_color(GrVertexWriter* vb, const GrQuadPerEdgeAA::VertexSpec& spec,
+                           const GrQuad& deviceQuad, const GrQuad& localQuad,
+                           const float coverage[4], const SkPMColor4f& color,
+                           const SkRect& geomDomain, const SkRect& texDomain) {
+    // Assert assumptions about VertexSpec
+    SkASSERT(spec.deviceQuadType() != GrQuad::Type::kPerspective);
+    SkASSERT(!spec.hasLocalCoords());
+    SkASSERT(spec.coverageMode() == GrQuadPerEdgeAA::CoverageMode::kNone ||
+             spec.coverageMode() == GrQuadPerEdgeAA::CoverageMode::kWithColor);
+    SkASSERT(spec.hasVertexColors());
+    SkASSERT(!spec.requiresGeometryDomain());
+    SkASSERT(!spec.hasDomain());
+
+    bool wide = spec.colorType() == GrQuadPerEdgeAA::ColorType::kHalf;
+    for (int i = 0; i < 4; ++i) {
+        // If this is not coverage-with-alpha, make sure coverage == 1 so it doesn't do anything
+        SkASSERT(spec.coverageMode() == GrQuadPerEdgeAA::CoverageMode::kWithColor ||
+                 coverage[i] == 1.f);
+        vb->write(deviceQuad.x(i), deviceQuad.y(i), GrVertexColor(color * coverage[i], wide));
+    }
+}
+
+// 2D (XY), no explicit coverage, UV locals, no color, no geometry domain, no texture domain
+// This represents opaque, non AA, textured rects
+static void write_2d_uv(GrVertexWriter* vb, const GrQuadPerEdgeAA::VertexSpec& spec,
+                        const GrQuad& deviceQuad, const GrQuad& localQuad,
+                        const float coverage[4], const SkPMColor4f& color,
+                        const SkRect& geomDomain, const SkRect& texDomain) {
+    // Assert assumptions about VertexSpec
+    SkASSERT(spec.deviceQuadType() != GrQuad::Type::kPerspective);
+    SkASSERT(spec.hasLocalCoords() && spec.localQuadType() != GrQuad::Type::kPerspective);
+    SkASSERT(spec.coverageMode() == GrQuadPerEdgeAA::CoverageMode::kNone);
+    SkASSERT(!spec.hasVertexColors());
+    SkASSERT(!spec.requiresGeometryDomain());
+    SkASSERT(!spec.hasDomain());
+
+    for (int i = 0; i < 4; ++i) {
+        vb->write(deviceQuad.x(i), deviceQuad.y(i), localQuad.x(i), localQuad.y(i));
+    }
+}
+
+// 2D (XY), no explicit coverage, UV locals, vertex color, no geometry or texture domains
+// This represents transparent, non AA (or AA with cov. as alpha), textured rects
+static void write_2d_color_uv(GrVertexWriter* vb, const GrQuadPerEdgeAA::VertexSpec& spec,
+                              const GrQuad& deviceQuad, const GrQuad& localQuad,
+                              const float coverage[4], const SkPMColor4f& color,
+                              const SkRect& geomDomain, const SkRect& texDomain) {
+    // Assert assumptions about VertexSpec
+    SkASSERT(spec.deviceQuadType() != GrQuad::Type::kPerspective);
+    SkASSERT(spec.hasLocalCoords() && spec.localQuadType() != GrQuad::Type::kPerspective);
+    SkASSERT(spec.coverageMode() == GrQuadPerEdgeAA::CoverageMode::kNone ||
+             spec.coverageMode() == GrQuadPerEdgeAA::CoverageMode::kWithColor);
+    SkASSERT(spec.hasVertexColors());
+    SkASSERT(!spec.requiresGeometryDomain());
+    SkASSERT(!spec.hasDomain());
+
+    bool wide = spec.colorType() == GrQuadPerEdgeAA::ColorType::kHalf;
+    for (int i = 0; i < 4; ++i) {
+        // If this is not coverage-with-alpha, make sure coverage == 1 so it doesn't do anything
+        SkASSERT(spec.coverageMode() == GrQuadPerEdgeAA::CoverageMode::kWithColor ||
+                 coverage[i] == 1.f);
+        vb->write(deviceQuad.x(i), deviceQuad.y(i), GrVertexColor(color * coverage[i], wide),
+                  localQuad.x(i), localQuad.y(i));
+    }
+}
+
+// 2D (XY), explicit coverage, UV locals, no color, no geometry domain, no texture domain
+// This represents opaque, AA, textured rects
+static void write_2d_cov_uv(GrVertexWriter* vb, const GrQuadPerEdgeAA::VertexSpec& spec,
+                            const GrQuad& deviceQuad, const GrQuad& localQuad,
+                            const float coverage[4], const SkPMColor4f& color,
+                            const SkRect& geomDomain, const SkRect& texDomain) {
+    // Assert assumptions about VertexSpec
+    SkASSERT(spec.deviceQuadType() != GrQuad::Type::kPerspective);
+    SkASSERT(spec.hasLocalCoords() && spec.localQuadType() != GrQuad::Type::kPerspective);
+    SkASSERT(spec.coverageMode() == GrQuadPerEdgeAA::CoverageMode::kWithPosition);
+    SkASSERT(!spec.hasVertexColors());
+    SkASSERT(!spec.requiresGeometryDomain());
+    SkASSERT(!spec.hasDomain());
+
+    for (int i = 0; i < 4; ++i) {
+        vb->write(deviceQuad.x(i), deviceQuad.y(i), coverage[i], localQuad.x(i), localQuad.y(i));
+    }
+}
+
+// NOTE: The three _strict specializations below match the non-strict uv functions above, except
+// that they also write the UV domain. These are included to benefit SkiaRenderer, which must make
+// use of both fast and strict constrained domains. When testing _strict was not that common across
+// GMS, SKPs, and SVGs but we have little visibility into actual SkiaRenderer statistics. If
+// SkiaRenderer can avoid domains more, these 3 functions should probably be removed for simplicity.
+
+// 2D (XY), no explicit coverage, UV locals, no color, tex domain but no geometry domain
+// This represents opaque, non AA, textured rects with strict uv sampling
+static void write_2d_uv_strict(GrVertexWriter* vb, const GrQuadPerEdgeAA::VertexSpec& spec,
+                               const GrQuad& deviceQuad, const GrQuad& localQuad,
+                               const float coverage[4], const SkPMColor4f& color,
+                               const SkRect& geomDomain, const SkRect& texDomain) {
+    // Assert assumptions about VertexSpec
+    SkASSERT(spec.deviceQuadType() != GrQuad::Type::kPerspective);
+    SkASSERT(spec.hasLocalCoords() && spec.localQuadType() != GrQuad::Type::kPerspective);
+    SkASSERT(spec.coverageMode() == GrQuadPerEdgeAA::CoverageMode::kNone);
+    SkASSERT(!spec.hasVertexColors());
+    SkASSERT(!spec.requiresGeometryDomain());
+    SkASSERT(spec.hasDomain());
+
+    for (int i = 0; i < 4; ++i) {
+        vb->write(deviceQuad.x(i), deviceQuad.y(i), localQuad.x(i), localQuad.y(i), texDomain);
+    }
+}
+
+// 2D (XY), no explicit coverage, UV locals, vertex color, tex domain but no geometry domain
+// This represents transparent, non AA (or AA with cov. as alpha), textured rects with strict sample
+static void write_2d_color_uv_strict(GrVertexWriter* vb, const GrQuadPerEdgeAA::VertexSpec& spec,
+                                     const GrQuad& deviceQuad, const GrQuad& localQuad,
+                                     const float coverage[4], const SkPMColor4f& color,
+                                     const SkRect& geomDomain, const SkRect& texDomain) {
+    // Assert assumptions about VertexSpec
+    SkASSERT(spec.deviceQuadType() != GrQuad::Type::kPerspective);
+    SkASSERT(spec.hasLocalCoords() && spec.localQuadType() != GrQuad::Type::kPerspective);
+    SkASSERT(spec.coverageMode() == GrQuadPerEdgeAA::CoverageMode::kNone ||
+             spec.coverageMode() == GrQuadPerEdgeAA::CoverageMode::kWithColor);
+    SkASSERT(spec.hasVertexColors());
+    SkASSERT(!spec.requiresGeometryDomain());
+    SkASSERT(spec.hasDomain());
+
+    bool wide = spec.colorType() == GrQuadPerEdgeAA::ColorType::kHalf;
+    for (int i = 0; i < 4; ++i) {
+        // If this is not coverage-with-alpha, make sure coverage == 1 so it doesn't do anything
+        SkASSERT(spec.coverageMode() == GrQuadPerEdgeAA::CoverageMode::kWithColor ||
+                 coverage[i] == 1.f);
+        vb->write(deviceQuad.x(i), deviceQuad.y(i), GrVertexColor(color * coverage[i], wide),
+                  localQuad.x(i), localQuad.y(i), texDomain);
+    }
+}
+
+// 2D (XY), explicit coverage, UV locals, no color, tex domain but no geometry domain
+// This represents opaque, AA, textured rects with strict uv sampling
+static void write_2d_cov_uv_strict(GrVertexWriter* vb, const GrQuadPerEdgeAA::VertexSpec& spec,
+                                   const GrQuad& deviceQuad, const GrQuad& localQuad,
+                                   const float coverage[4], const SkPMColor4f& color,
+                                   const SkRect& geomDomain, const SkRect& texDomain) {
+    // Assert assumptions about VertexSpec
+    SkASSERT(spec.deviceQuadType() != GrQuad::Type::kPerspective);
+    SkASSERT(spec.hasLocalCoords() && spec.localQuadType() != GrQuad::Type::kPerspective);
+    SkASSERT(spec.coverageMode() == GrQuadPerEdgeAA::CoverageMode::kWithPosition);
+    SkASSERT(!spec.hasVertexColors());
+    SkASSERT(!spec.requiresGeometryDomain());
+    SkASSERT(spec.hasDomain());
+
+    for (int i = 0; i < 4; ++i) {
+        vb->write(deviceQuad.x(i), deviceQuad.y(i), coverage[i],
+                  localQuad.x(i), localQuad.y(i), texDomain);
+    }
+}
+
 } // anonymous namespace
 
 namespace GrQuadPerEdgeAA {
@@ -86,40 +246,82 @@
     }
 }
 
-////////////////// Tessellate Implementation
+////////////////// Tessellator Implementation
 
-void* Tessellate(void* vertices, const VertexSpec& spec, const GrQuad& deviceQuad,
-                 const SkPMColor4f& color4f, const GrQuad& localQuad, const SkRect& domain,
-                 GrQuadAAFlags aaFlags) {
-    SkASSERT(deviceQuad.quadType() <= spec.deviceQuadType());
-    SkASSERT(!spec.hasLocalCoords() || localQuad.quadType() <= spec.localQuadType());
+Tessellator::WriteQuadProc Tessellator::GetWriteQuadProc(const VertexSpec& spec) {
+    // All specialized writing functions requires 2D geometry and no geometry domain. This is not
+    // the same as just checking device type vs. kRectilinear since non-AA general 2D quads do not
+    // require a geometry domain and could then go through a fast path.
+    if (spec.deviceQuadType() != GrQuad::Type::kPerspective && !spec.requiresGeometryDomain()) {
+        CoverageMode mode = spec.coverageMode();
+        if (spec.hasVertexColors()) {
+            if (mode != CoverageMode::kWithPosition) {
+                // Vertex colors, but no explicit coverage
+                if (!spec.hasLocalCoords()) {
+                    // Non-UV with vertex colors (possibly with coverage folded into alpha)
+                    return write_2d_color;
+                } else if (spec.localQuadType() != GrQuad::Type::kPerspective) {
+                    // UV locals with vertex colors (possibly with coverage-as-alpha)
+                    return spec.hasDomain() ? write_2d_color_uv_strict : write_2d_color_uv;
+                }
+            }
+            // Else fall through; this is a spec that requires vertex colors and explicit coverage,
+            // which means it's anti-aliased and the FPs don't support coverage as alpha, or
+            // it uses 3D local coordinates.
+        } else if (spec.hasLocalCoords() && spec.localQuadType() != GrQuad::Type::kPerspective) {
+            if (mode == CoverageMode::kWithPosition) {
+                // UV locals with explicit coverage
+                return spec.hasDomain() ? write_2d_cov_uv_strict : write_2d_cov_uv;
+            } else {
+                SkASSERT(mode == CoverageMode::kNone);
+                return spec.hasDomain() ? write_2d_uv_strict : write_2d_uv;
+            }
+        }
+        // Else fall through to generic vertex function; this is a spec that has no vertex colors
+        // and [no|uvr] local coords, which doesn't happen often enough to warrant specialization.
+    }
 
-    GrQuadPerEdgeAA::CoverageMode mode = spec.coverageMode();
+    // Arbitrary spec hits the slow path
+    return write_quad_generic;
+}
+
+Tessellator::Tessellator(const VertexSpec& spec)
+        : fVertexSpec(spec)
+        , fWriteProc(Tessellator::GetWriteQuadProc(spec)) {}
+
+void* Tessellator::append(void* vertices, const GrQuad& deviceQuad, const GrQuad& localQuad,
+                          const SkPMColor4f& color, const SkRect& uvDomain, GrQuadAAFlags aaFlags) {
+    SkASSERT(deviceQuad.quadType() <= fVertexSpec.deviceQuadType());
+    SkASSERT(!fVertexSpec.hasLocalCoords() || localQuad.quadType() <= fVertexSpec.localQuadType());
+
+    static const float kFullCoverage[4] = {1.f, 1.f, 1.f, 1.f};
+    static const float kZeroCoverage[4] = {0.f, 0.f, 0.f, 0.f};
+    static const SkRect kIgnoredDomain = SkRect::MakeEmpty();
 
     GrVertexWriter vb{vertices};
-    if (spec.usesCoverageAA()) {
-        SkASSERT(mode == CoverageMode::kWithPosition || mode == CoverageMode::kWithColor);
+    if (fVertexSpec.usesCoverageAA()) {
+        SkASSERT(fVertexSpec.coverageMode() == CoverageMode::kWithColor ||
+                 fVertexSpec.coverageMode() == CoverageMode::kWithPosition);
         // Must calculate inner and outer quadrilaterals for the vertex coverage ramps, and possibly
-        // a geometry domain
+        // a geometry domain if corners are not right angles
         SkRect geomDomain;
-        if (spec.requiresGeometryDomain()) {
+        if (fVertexSpec.requiresGeometryDomain()) {
             geomDomain = deviceQuad.bounds();
             geomDomain.outset(0.5f, 0.5f); // account for AA expansion
         }
 
-        GrQuadUtils::TessellationHelper helper;
         if (aaFlags == GrQuadAAFlags::kNone) {
             // Have to write the coverage AA vertex structure, but there's no math to be done for a
             // non-aa quad batched into a coverage AA op.
-            write_quad(&vb, spec, mode, 1.f, color4f, geomDomain, domain, deviceQuad, localQuad);
+            fWriteProc(&vb, fVertexSpec, deviceQuad, localQuad, kFullCoverage, color,
+                       geomDomain, uvDomain);
             // Since we pass the same corners in, the outer vertex structure will have 0 area and
             // the coverage interpolation from 1 to 0 will not be visible.
-            write_quad(&vb, spec, mode, 0.f, color4f, geomDomain, domain, deviceQuad, localQuad);
+            fWriteProc(&vb, fVertexSpec, deviceQuad, localQuad, kZeroCoverage, color,
+                       geomDomain, uvDomain);
         } else {
-            // TODO(michaelludwig) - Update TessellateHelper to select processing functions based on
-            // the vertexspec once per op, and then burn through all quads with the selected
-            // function ptr.
-            helper.reset(deviceQuad, spec.hasLocalCoords() ? &localQuad : nullptr);
+            // Reset the tessellation helper to match the current geometry
+            fAAHelper.reset(deviceQuad, fVertexSpec.hasLocalCoords() ? &localQuad : nullptr);
 
             // Edge inset/outset distance ordered LBTR, set to 0.5 for a half pixel if the AA flag
             // is turned on, or 0.0 if the edge is not anti-aliased.
@@ -133,22 +335,25 @@
                                   (aaFlags & GrQuadAAFlags::kRight)  ? 0.5f : 0.f };
             }
 
-            // Write inner vertices first
             GrQuad aaDeviceQuad, aaLocalQuad;
-            skvx::Vec<4, float> coverage = helper.inset(edgeDistances, &aaDeviceQuad, &aaLocalQuad);
-            write_quad(&vb, spec, mode, coverage, color4f, geomDomain, domain,
-                       aaDeviceQuad, aaLocalQuad);
+
+            // Write inner vertices first
+            float coverage[4];
+            fAAHelper.inset(edgeDistances, &aaDeviceQuad, &aaLocalQuad).store(coverage);
+            fWriteProc(&vb, fVertexSpec, aaDeviceQuad, aaLocalQuad, coverage, color,
+                       geomDomain, uvDomain);
 
             // Then outer vertices, which use 0.f for their coverage
-            helper.outset(edgeDistances, &aaDeviceQuad, &aaLocalQuad);
-            write_quad(&vb, spec, mode, 0.f, color4f, geomDomain, domain,
-                       aaDeviceQuad, aaLocalQuad);
+            fAAHelper.outset(edgeDistances, &aaDeviceQuad, &aaLocalQuad);
+            fWriteProc(&vb, fVertexSpec, aaDeviceQuad, aaLocalQuad, kZeroCoverage, color,
+                       geomDomain, uvDomain);
         }
     } else {
         // No outsetting needed, just write a single quad with full coverage
-        SkASSERT(mode == CoverageMode::kNone && !spec.requiresGeometryDomain());
-        write_quad(&vb, spec, mode, 1.f, color4f, SkRect::MakeEmpty(), domain,
-                   deviceQuad, localQuad);
+        SkASSERT(fVertexSpec.coverageMode() == CoverageMode::kNone &&
+                 !fVertexSpec.requiresGeometryDomain());
+        fWriteProc(&vb, fVertexSpec, deviceQuad, localQuad, kFullCoverage, color,
+                   kIgnoredDomain, uvDomain);
     }
 
     return vb.fPtr;
diff --git a/src/gpu/ops/GrQuadPerEdgeAA.h b/src/gpu/ops/GrQuadPerEdgeAA.h
index 1e5675b..c15c840 100644
--- a/src/gpu/ops/GrQuadPerEdgeAA.h
+++ b/src/gpu/ops/GrQuadPerEdgeAA.h
@@ -15,12 +15,14 @@
 #include "src/gpu/GrGeometryProcessor.h"
 #include "src/gpu/GrSamplerState.h"
 #include "src/gpu/geometry/GrQuad.h"
+#include "src/gpu/geometry/GrQuadUtils.h"
 #include "src/gpu/ops/GrMeshDrawOp.h"
 #include "src/gpu/ops/GrTextureOp.h"
 
 class GrCaps;
 class GrColorSpaceXform;
 class GrShaderCaps;
+struct GrVertexWriter;
 
 namespace GrQuadPerEdgeAA {
     using Saturate = GrTextureOp::Saturate;
@@ -127,6 +129,35 @@
         unsigned fRequiresGeometryDomain: 1;
     };
 
+    // A Tessellator is responsible for processing a series of device+local GrQuads into a VBO,
+    // as specified by a VertexSpec. This vertex data can then be processed by a GP created with
+    // MakeProcessor and/or MakeTexturedProcessor.
+    class Tessellator {
+    public:
+        explicit Tessellator(const VertexSpec& spec);
+
+        // Calculates (as needed) inset and outset geometry for anti-aliasing, and writes all
+        // necessary position and vertex attributes required by this Tessellator's VertexSpec.
+        // After appending, this will return the pointer to the next vertex in the VBO.
+        void* append(void* vertices, const GrQuad& deviceQuad, const GrQuad& localQuad,
+                    const SkPMColor4f& color, const SkRect& uvDomain, GrQuadAAFlags aaFlags);
+
+    private:
+        // VertexSpec defines many unique ways to write vertex attributes, which can be handled
+        // generically by branching per-quad based on the VertexSpec. However, there are several
+        // specs that appear in the wild far more frequently, so they use explicit WriteQuadProcs
+        // that have no branches.
+        typedef void (*WriteQuadProc)(GrVertexWriter* vertices, const VertexSpec& spec,
+                                      const GrQuad& deviceQuad, const GrQuad& localQuad,
+                                      const float coverage[4], const SkPMColor4f& color,
+                                      const SkRect& geomDomain, const SkRect& texDomain);
+        static WriteQuadProc GetWriteQuadProc(const VertexSpec& spec);
+
+        GrQuadUtils::TessellationHelper fAAHelper;
+        VertexSpec                      fVertexSpec;
+        WriteQuadProc                   fWriteProc;
+    };
+
     sk_sp<GrGeometryProcessor> MakeProcessor(const VertexSpec& spec);
 
     sk_sp<GrGeometryProcessor> MakeTexturedProcessor(
@@ -134,19 +165,6 @@
             const GrSamplerState& samplerState, const GrSwizzle& swizzle,
             sk_sp<GrColorSpaceXform> textureColorSpaceXform, Saturate saturate);
 
-    // Fill vertices with the vertex data needed to represent the given quad. The device position,
-    // local coords, vertex color, domain, and edge coefficients will be written and/or computed
-    // based on the configuration in the vertex spec; if that attribute is disabled in the spec,
-    // then its corresponding function argument is ignored.
-    //
-    // Tessellation is based on the quad type of the vertex spec, not the provided GrQuad's
-    // so that all quads in a batch are tessellated the same.
-    //
-    // Returns the advanced pointer in vertices.
-    void* Tessellate(void* vertices, const VertexSpec& spec, const GrQuad& deviceQuad,
-                     const SkPMColor4f& color, const GrQuad& localQuad, const SkRect& domain,
-                     GrQuadAAFlags aa);
-
     // This method will return the correct index buffer for the specified indexBufferOption.
     // It will, correctly, return nullptr if the indexBufferOption is kTriStrips.
     sk_sp<const GrBuffer> GetIndexBuffer(GrMeshDrawOp::Target*, IndexBufferOption);
diff --git a/src/gpu/ops/GrTextureOp.cpp b/src/gpu/ops/GrTextureOp.cpp
index 85faf9d..d2dbd9a 100644
--- a/src/gpu/ops/GrTextureOp.cpp
+++ b/src/gpu/ops/GrTextureOp.cpp
@@ -480,9 +480,9 @@
         fDomain = static_cast<unsigned>(netDomain);
     }
 
-    static void Tess(void* v, const VertexSpec& spec, const GrTextureProxy* proxy,
-                     GrQuadBuffer<ColorDomainAndAA>::Iter* iter, int cnt,
-                     GrSamplerState::Filter filter) {
+    static void Tess(void* v, GrQuadPerEdgeAA::Tessellator* tessellator,
+                     const GrTextureProxy* proxy, GrSamplerState::Filter filter,
+                     GrQuadBuffer<ColorDomainAndAA>::Iter* iter, int cnt) {
         TRACE_EVENT0("skia.gpu", TRACE_FUNC);
         auto origin = proxy->origin();
         SkISize dimensions = proxy->backingStoreDimensions();
@@ -507,10 +507,9 @@
             // Must correct the texture coordinates and domain now that the real texture size
             // is known
             compute_src_quad(origin, iter->localQuad(), iw, ih, h, &srcQuad);
-            compute_domain(info.domain(), filter, origin, info.fDomainRect, iw, ih, h,
-                           &domain);
-            v = GrQuadPerEdgeAA::Tessellate(v, spec, iter->deviceQuad(), info.fColor, srcQuad,
-                                            domain, info.aaFlags());
+            compute_domain(info.domain(), filter, origin, info.fDomainRect, iw, ih, h, &domain);
+            v = tessellator->append(v, iter->deviceQuad(), srcQuad, info.fColor,
+                                    domain, info.aaFlags());
             i++;
         }
     }
@@ -548,6 +547,7 @@
         char* dst = pVertexData;
         const size_t vertexSize = desc->fVertexSpec.vertexSize();
 
+        GrQuadPerEdgeAA::Tessellator tessellator(desc->fVertexSpec);
         int meshIndex = 0;
         for (const auto& op : ChainRange<TextureOp>(texOp)) {
             auto iter = op.fQuads.iterator();
@@ -561,7 +561,7 @@
                 SkASSERT(meshIndex < desc->fNumProxies);
 
                 if (dst) {
-                    Tess(dst, desc->fVertexSpec, proxy, &iter, quadCnt, op.filter());
+                    Tess(dst, &tessellator, proxy, op.filter(), &iter, quadCnt);
                     desc->setMeshProxy(meshIndex, proxy);
 
                     SkASSERT(totVerticesSeen * vertexSize == (size_t)(dst - pVertexData));