Move Software and Small PathRenderers to skgpu::v1 namespace

Bug: skia:11837
Change-Id: Ia34f1840aaa7360ea9a3ca40fef5cb96b68c6ca3
Reviewed-on: https://skia-review.googlesource.com/c/skia/+/439781
Commit-Queue: Robert Phillips <robertphillips@google.com>
Reviewed-by: Michael Ludwig <michaelludwig@google.com>
diff --git a/src/gpu/ops/SmallPathRenderer.cpp b/src/gpu/ops/SmallPathRenderer.cpp
new file mode 100644
index 0000000..a8b8f3b
--- /dev/null
+++ b/src/gpu/ops/SmallPathRenderer.cpp
@@ -0,0 +1,733 @@
+/*
+ * Copyright 2014 Google Inc.
+ * Copyright 2017 ARM Ltd.
+ *
+ * Use of this source code is governed by a BSD-style license that can be
+ * found in the LICENSE file.
+ */
+
+#include "src/gpu/ops/SmallPathRenderer.h"
+
+#include "include/core/SkPaint.h"
+#include "src/core/SkAutoPixmapStorage.h"
+#include "src/core/SkDistanceFieldGen.h"
+#include "src/core/SkDraw.h"
+#include "src/core/SkMatrixPriv.h"
+#include "src/core/SkMatrixProvider.h"
+#include "src/core/SkPointPriv.h"
+#include "src/core/SkRasterClip.h"
+#include "src/gpu/GrBuffer.h"
+#include "src/gpu/GrCaps.h"
+#include "src/gpu/GrDistanceFieldGenFromVector.h"
+#include "src/gpu/GrDrawOpTest.h"
+#include "src/gpu/GrResourceProvider.h"
+#include "src/gpu/GrVertexWriter.h"
+#include "src/gpu/effects/GrBitmapTextGeoProc.h"
+#include "src/gpu/effects/GrDistanceFieldGeoProc.h"
+#include "src/gpu/geometry/GrQuad.h"
+#include "src/gpu/geometry/GrStyledShape.h"
+#include "src/gpu/ops/GrMeshDrawOp.h"
+#include "src/gpu/ops/GrSimpleMeshDrawOpHelperWithStencil.h"
+#include "src/gpu/ops/GrSmallPathAtlasMgr.h"
+#include "src/gpu/ops/GrSmallPathShapeData.h"
+#include "src/gpu/v1/SurfaceDrawContext_v1.h"
+
+namespace {
+
+// mip levels
+static constexpr SkScalar kIdealMinMIP = 12;
+static constexpr SkScalar kMaxMIP = 162;
+
+static constexpr SkScalar kMaxDim = 73;
+static constexpr SkScalar kMinSize = SK_ScalarHalf;
+static constexpr SkScalar kMaxSize = 2*kMaxMIP;
+
+////////////////////////////////////////////////////////////////////////////////
+
+// padding around path bounds to allow for antialiased pixels
+static const int kAntiAliasPad = 1;
+
+class SmallPathOp final : public GrMeshDrawOp {
+private:
+    using Helper = GrSimpleMeshDrawOpHelperWithStencil;
+
+public:
+    DEFINE_OP_CLASS_ID
+
+    static GrOp::Owner Make(GrRecordingContext* context,
+                            GrPaint&& paint,
+                            const GrStyledShape& shape,
+                            const SkMatrix& viewMatrix,
+                            bool gammaCorrect,
+                            const GrUserStencilSettings* stencilSettings) {
+        return Helper::FactoryHelper<SmallPathOp>(context, std::move(paint), shape, viewMatrix,
+                                                  gammaCorrect, stencilSettings);
+    }
+
+    SmallPathOp(GrProcessorSet* processorSet, const SkPMColor4f& color, const GrStyledShape& shape,
+                const SkMatrix& viewMatrix, bool gammaCorrect,
+                const GrUserStencilSettings* stencilSettings)
+            : INHERITED(ClassID())
+            , fHelper(processorSet, GrAAType::kCoverage, stencilSettings) {
+        SkASSERT(shape.hasUnstyledKey());
+        // Compute bounds
+        this->setTransformedBounds(shape.bounds(), viewMatrix, HasAABloat::kYes, IsHairline::kNo);
+
+#if defined(SK_BUILD_FOR_ANDROID) && !defined(SK_BUILD_FOR_ANDROID_FRAMEWORK)
+        fUsesDistanceField = true;
+#else
+        // only use distance fields on desktop and Android framework to save space in the atlas
+        fUsesDistanceField = this->bounds().width() > kMaxMIP || this->bounds().height() > kMaxMIP;
+#endif
+        // always use distance fields if in perspective
+        fUsesDistanceField = fUsesDistanceField || viewMatrix.hasPerspective();
+
+        fShapes.emplace_back(Entry{color, shape, viewMatrix});
+
+        fGammaCorrect = gammaCorrect;
+    }
+
+    const char* name() const override { return "SmallPathOp"; }
+
+    void visitProxies(const GrVisitProxyFunc& func) const override {
+        fHelper.visitProxies(func);
+    }
+
+    FixedFunctionFlags fixedFunctionFlags() const override { return fHelper.fixedFunctionFlags(); }
+
+    GrProcessorSet::Analysis finalize(const GrCaps& caps, const GrAppliedClip* clip,
+                                      GrClampType clampType) override {
+        return fHelper.finalizeProcessors(caps, clip, clampType,
+                                          GrProcessorAnalysisCoverage::kSingleChannel,
+                                          &fShapes.front().fColor, &fWideColor);
+    }
+
+private:
+    struct FlushInfo {
+        sk_sp<const GrBuffer> fVertexBuffer;
+        sk_sp<const GrBuffer> fIndexBuffer;
+        GrGeometryProcessor*  fGeometryProcessor;
+        const GrSurfaceProxy** fPrimProcProxies;
+        int fVertexOffset;
+        int fInstancesToFlush;
+    };
+
+    GrProgramInfo* programInfo() override {
+        // TODO [PI]: implement
+        return nullptr;
+    }
+
+    void onCreateProgramInfo(const GrCaps*,
+                             SkArenaAlloc*,
+                             const GrSurfaceProxyView& writeView,
+                             bool usesMSAASurface,
+                             GrAppliedClip&&,
+                             const GrDstProxyView&,
+                             GrXferBarrierFlags renderPassXferBarriers,
+                             GrLoadOp colorLoadOp) override {
+        // We cannot surface the SmallPathOp's programInfo at record time. As currently
+        // implemented, the GP is modified at flush time based on the number of pages in the
+        // atlas.
+    }
+
+    void onPrePrepareDraws(GrRecordingContext*,
+                           const GrSurfaceProxyView& writeView,
+                           GrAppliedClip*,
+                           const GrDstProxyView&,
+                           GrXferBarrierFlags renderPassXferBarriers,
+                           GrLoadOp colorLoadOp) override {
+        // TODO [PI]: implement
+    }
+
+    void onPrepareDraws(GrMeshDrawTarget* target) override {
+        int instanceCount = fShapes.count();
+
+        GrSmallPathAtlasMgr* atlasMgr = target->smallPathAtlasManager();
+        if (!atlasMgr) {
+            return;
+        }
+
+        static constexpr int kMaxTextures = GrDistanceFieldPathGeoProc::kMaxTextures;
+        static_assert(GrBitmapTextGeoProc::kMaxTextures == kMaxTextures);
+
+        FlushInfo flushInfo;
+        flushInfo.fPrimProcProxies = target->allocPrimProcProxyPtrs(kMaxTextures);
+
+        int numActiveProxies;
+        const GrSurfaceProxyView* views = atlasMgr->getViews(&numActiveProxies);
+        for (int i = 0; i < numActiveProxies; ++i) {
+            // This op does not know its atlas proxies when it is added to a GrOpsTasks, so the
+            // proxies don't get added during the visitProxies call. Thus we add them here.
+            flushInfo.fPrimProcProxies[i] = views[i].proxy();
+            target->sampledProxyArray()->push_back(views[i].proxy());
+        }
+
+        // Setup GrGeometryProcessor
+        const SkMatrix& ctm = fShapes[0].fViewMatrix;
+        if (fUsesDistanceField) {
+            uint32_t flags = 0;
+            // Still need to key off of ctm to pick the right shader for the transformed quad
+            flags |= ctm.isScaleTranslate() ? kScaleOnly_DistanceFieldEffectFlag : 0;
+            flags |= ctm.isSimilarity() ? kSimilarity_DistanceFieldEffectFlag : 0;
+            flags |= fGammaCorrect ? kGammaCorrect_DistanceFieldEffectFlag : 0;
+
+            const SkMatrix* matrix;
+            SkMatrix invert;
+            if (ctm.hasPerspective()) {
+                matrix = &ctm;
+            } else if (fHelper.usesLocalCoords()) {
+                if (!ctm.invert(&invert)) {
+                    return;
+                }
+                matrix = &invert;
+            } else {
+                matrix = &SkMatrix::I();
+            }
+            flushInfo.fGeometryProcessor = GrDistanceFieldPathGeoProc::Make(
+                    target->allocator(), *target->caps().shaderCaps(), *matrix, fWideColor,
+                    views, numActiveProxies, GrSamplerState::Filter::kLinear,
+                    flags);
+        } else {
+            SkMatrix invert;
+            if (fHelper.usesLocalCoords()) {
+                if (!ctm.invert(&invert)) {
+                    return;
+                }
+            }
+
+            flushInfo.fGeometryProcessor = GrBitmapTextGeoProc::Make(
+                    target->allocator(), *target->caps().shaderCaps(), this->color(), fWideColor,
+                    views, numActiveProxies, GrSamplerState::Filter::kNearest,
+                    kA8_GrMaskFormat, invert, false);
+        }
+
+        // allocate vertices
+        const size_t kVertexStride = flushInfo.fGeometryProcessor->vertexStride();
+
+        // We need to make sure we don't overflow a 32 bit int when we request space in the
+        // makeVertexSpace call below.
+        if (instanceCount > SK_MaxS32 / GrResourceProvider::NumVertsPerNonAAQuad()) {
+            return;
+        }
+        GrVertexWriter vertices{ target->makeVertexSpace(
+            kVertexStride, GrResourceProvider::NumVertsPerNonAAQuad() * instanceCount,
+            &flushInfo.fVertexBuffer, &flushInfo.fVertexOffset)};
+
+        flushInfo.fIndexBuffer = target->resourceProvider()->refNonAAQuadIndexBuffer();
+        if (!vertices.fPtr || !flushInfo.fIndexBuffer) {
+            SkDebugf("Could not allocate vertices\n");
+            return;
+        }
+
+        flushInfo.fInstancesToFlush = 0;
+        for (int i = 0; i < instanceCount; i++) {
+            const Entry& args = fShapes[i];
+
+            GrSmallPathShapeData* shapeData;
+            if (fUsesDistanceField) {
+                // get mip level
+                SkScalar maxScale;
+                const SkRect& bounds = args.fShape.bounds();
+                if (args.fViewMatrix.hasPerspective()) {
+                    // approximate the scale since we can't get it from the matrix
+                    SkRect xformedBounds;
+                    args.fViewMatrix.mapRect(&xformedBounds, bounds);
+                    maxScale = SkScalarAbs(std::max(xformedBounds.width() / bounds.width(),
+                                                  xformedBounds.height() / bounds.height()));
+                } else {
+                    maxScale = SkScalarAbs(args.fViewMatrix.getMaxScale());
+                }
+                SkScalar maxDim = std::max(bounds.width(), bounds.height());
+                // We try to create the DF at a 2^n scaled path resolution (1/2, 1, 2, 4, etc.)
+                // In the majority of cases this will yield a crisper rendering.
+                SkScalar mipScale = 1.0f;
+                // Our mipscale is the maxScale clamped to the next highest power of 2
+                if (maxScale <= SK_ScalarHalf) {
+                    SkScalar log = SkScalarFloorToScalar(SkScalarLog2(SkScalarInvert(maxScale)));
+                    mipScale = SkScalarPow(2, -log);
+                } else if (maxScale > SK_Scalar1) {
+                    SkScalar log = SkScalarCeilToScalar(SkScalarLog2(maxScale));
+                    mipScale = SkScalarPow(2, log);
+                }
+                // Log2 isn't very precise at values close to a power of 2,
+                // so add a little tolerance here. A little bit of scaling up is fine.
+                SkASSERT(maxScale <= mipScale + SK_ScalarNearlyZero);
+
+                SkScalar mipSize = mipScale*SkScalarAbs(maxDim);
+                // For sizes less than kIdealMinMIP we want to use as large a distance field as we can
+                // so we can preserve as much detail as possible. However, we can't scale down more
+                // than a 1/4 of the size without artifacts. So the idea is that we pick the mipsize
+                // just bigger than the ideal, and then scale down until we are no more than 4x the
+                // original mipsize.
+                if (mipSize < kIdealMinMIP) {
+                    SkScalar newMipSize = mipSize;
+                    do {
+                        newMipSize *= 2;
+                    } while (newMipSize < kIdealMinMIP);
+                    while (newMipSize > 4 * mipSize) {
+                        newMipSize *= 0.25f;
+                    }
+                    mipSize = newMipSize;
+                }
+
+                SkScalar desiredDimension = std::min(mipSize, kMaxMIP);
+                int ceilDesiredDimension = SkScalarCeilToInt(desiredDimension);
+
+                // check to see if df path is cached
+                shapeData = atlasMgr->findOrCreate(args.fShape, ceilDesiredDimension);
+                if (!shapeData->fAtlasLocator.plotLocator().isValid()) {
+                    SkScalar scale = desiredDimension / maxDim;
+
+                    if (!this->addDFPathToAtlas(target,
+                                                &flushInfo,
+                                                atlasMgr,
+                                                shapeData,
+                                                args.fShape,
+                                                ceilDesiredDimension,
+                                                scale)) {
+                        atlasMgr->deleteCacheEntry(shapeData);
+                        continue;
+                    }
+                }
+            } else {
+                // check to see if bitmap path is cached
+                shapeData = atlasMgr->findOrCreate(args.fShape, args.fViewMatrix);
+                if (!shapeData->fAtlasLocator.plotLocator().isValid()) {
+                    if (!this->addBMPathToAtlas(target,
+                                                &flushInfo,
+                                                atlasMgr,
+                                                shapeData,
+                                                args.fShape,
+                                                args.fViewMatrix)) {
+                        atlasMgr->deleteCacheEntry(shapeData);
+                        continue;
+                    }
+                }
+            }
+
+            auto uploadTarget = target->deferredUploadTarget();
+            atlasMgr->setUseToken(shapeData, uploadTarget->tokenTracker()->nextDrawToken());
+
+            this->writePathVertices(vertices, GrVertexColor(args.fColor, fWideColor),
+                                    args.fViewMatrix, shapeData);
+            flushInfo.fInstancesToFlush++;
+        }
+
+        this->flush(target, &flushInfo);
+    }
+
+    bool addToAtlasWithRetry(GrMeshDrawTarget* target,
+                             FlushInfo* flushInfo,
+                             GrSmallPathAtlasMgr* atlasMgr,
+                             int width, int height, const void* image,
+                             const SkRect& bounds, int srcInset,
+                             GrSmallPathShapeData* shapeData) const {
+        auto resourceProvider = target->resourceProvider();
+        auto uploadTarget = target->deferredUploadTarget();
+
+        auto code = atlasMgr->addToAtlas(resourceProvider, uploadTarget, width, height,
+                                         image, &shapeData->fAtlasLocator);
+        if (GrDrawOpAtlas::ErrorCode::kError == code) {
+            return false;
+        }
+
+        if (GrDrawOpAtlas::ErrorCode::kTryAgain == code) {
+            this->flush(target, flushInfo);
+
+            code = atlasMgr->addToAtlas(resourceProvider, uploadTarget, width, height,
+                                        image, &shapeData->fAtlasLocator);
+        }
+
+        shapeData->fAtlasLocator.insetSrc(srcInset);
+        shapeData->fBounds = bounds;
+
+        return GrDrawOpAtlas::ErrorCode::kSucceeded == code;
+    }
+
+    bool addDFPathToAtlas(GrMeshDrawTarget* target, FlushInfo* flushInfo,
+                          GrSmallPathAtlasMgr* atlasMgr, GrSmallPathShapeData* shapeData,
+                          const GrStyledShape& shape, uint32_t dimension, SkScalar scale) const {
+
+        const SkRect& bounds = shape.bounds();
+
+        // generate bounding rect for bitmap draw
+        SkRect scaledBounds = bounds;
+        // scale to mip level size
+        scaledBounds.fLeft *= scale;
+        scaledBounds.fTop *= scale;
+        scaledBounds.fRight *= scale;
+        scaledBounds.fBottom *= scale;
+        // subtract out integer portion of origin
+        // (SDF created will be placed with fractional offset burnt in)
+        SkScalar dx = SkScalarFloorToScalar(scaledBounds.fLeft);
+        SkScalar dy = SkScalarFloorToScalar(scaledBounds.fTop);
+        scaledBounds.offset(-dx, -dy);
+        // get integer boundary
+        SkIRect devPathBounds;
+        scaledBounds.roundOut(&devPathBounds);
+        // place devBounds at origin with padding to allow room for antialiasing
+        int width = devPathBounds.width() + 2 * kAntiAliasPad;
+        int height = devPathBounds.height() + 2 * kAntiAliasPad;
+        devPathBounds = SkIRect::MakeWH(width, height);
+        SkScalar translateX = kAntiAliasPad - dx;
+        SkScalar translateY = kAntiAliasPad - dy;
+
+        // draw path to bitmap
+        SkMatrix drawMatrix;
+        drawMatrix.setScale(scale, scale);
+        drawMatrix.postTranslate(translateX, translateY);
+
+        SkASSERT(devPathBounds.fLeft == 0);
+        SkASSERT(devPathBounds.fTop == 0);
+        SkASSERT(devPathBounds.width() > 0);
+        SkASSERT(devPathBounds.height() > 0);
+
+        // setup signed distance field storage
+        SkIRect dfBounds = devPathBounds.makeOutset(SK_DistanceFieldPad, SK_DistanceFieldPad);
+        width = dfBounds.width();
+        height = dfBounds.height();
+        // TODO We should really generate this directly into the plot somehow
+        SkAutoSMalloc<1024> dfStorage(width * height * sizeof(unsigned char));
+
+        SkPath path;
+        shape.asPath(&path);
+        // Generate signed distance field directly from SkPath
+        bool succeed = GrGenerateDistanceFieldFromPath((unsigned char*)dfStorage.get(),
+                                                       path, drawMatrix, width, height,
+                                                       width * sizeof(unsigned char));
+        if (!succeed) {
+            // setup bitmap backing
+            SkAutoPixmapStorage dst;
+            if (!dst.tryAlloc(SkImageInfo::MakeA8(devPathBounds.width(), devPathBounds.height()))) {
+                return false;
+            }
+            sk_bzero(dst.writable_addr(), dst.computeByteSize());
+
+            // rasterize path
+            SkPaint paint;
+            paint.setStyle(SkPaint::kFill_Style);
+            paint.setAntiAlias(true);
+
+            SkDraw draw;
+
+            SkRasterClip rasterClip;
+            rasterClip.setRect(devPathBounds);
+            draw.fRC = &rasterClip;
+            SkSimpleMatrixProvider matrixProvider(drawMatrix);
+            draw.fMatrixProvider = &matrixProvider;
+            draw.fDst = dst;
+
+            draw.drawPathCoverage(path, paint);
+
+            // Generate signed distance field
+            SkGenerateDistanceFieldFromA8Image((unsigned char*)dfStorage.get(),
+                                               (const unsigned char*)dst.addr(),
+                                               dst.width(), dst.height(), dst.rowBytes());
+        }
+
+        SkRect drawBounds = SkRect::Make(devPathBounds).makeOffset(-translateX, -translateY);
+        drawBounds.fLeft /= scale;
+        drawBounds.fTop /= scale;
+        drawBounds.fRight /= scale;
+        drawBounds.fBottom /= scale;
+
+        return this->addToAtlasWithRetry(target, flushInfo, atlasMgr,
+                                         width, height, dfStorage.get(),
+                                         drawBounds, SK_DistanceFieldPad, shapeData);
+    }
+
+    bool addBMPathToAtlas(GrMeshDrawTarget* target, FlushInfo* flushInfo,
+                          GrSmallPathAtlasMgr* atlasMgr, GrSmallPathShapeData* shapeData,
+                          const GrStyledShape& shape, const SkMatrix& ctm) const {
+        const SkRect& bounds = shape.bounds();
+        if (bounds.isEmpty()) {
+            return false;
+        }
+        SkMatrix drawMatrix(ctm);
+        SkScalar tx = ctm.getTranslateX();
+        SkScalar ty = ctm.getTranslateY();
+        tx -= SkScalarFloorToScalar(tx);
+        ty -= SkScalarFloorToScalar(ty);
+        drawMatrix.set(SkMatrix::kMTransX, tx);
+        drawMatrix.set(SkMatrix::kMTransY, ty);
+        SkRect shapeDevBounds;
+        drawMatrix.mapRect(&shapeDevBounds, bounds);
+        SkScalar dx = SkScalarFloorToScalar(shapeDevBounds.fLeft);
+        SkScalar dy = SkScalarFloorToScalar(shapeDevBounds.fTop);
+
+        // get integer boundary
+        SkIRect devPathBounds;
+        shapeDevBounds.roundOut(&devPathBounds);
+        // place devBounds at origin with padding to allow room for antialiasing
+        int width = devPathBounds.width() + 2 * kAntiAliasPad;
+        int height = devPathBounds.height() + 2 * kAntiAliasPad;
+        devPathBounds = SkIRect::MakeWH(width, height);
+        SkScalar translateX = kAntiAliasPad - dx;
+        SkScalar translateY = kAntiAliasPad - dy;
+
+        SkASSERT(devPathBounds.fLeft == 0);
+        SkASSERT(devPathBounds.fTop == 0);
+        SkASSERT(devPathBounds.width() > 0);
+        SkASSERT(devPathBounds.height() > 0);
+
+        SkPath path;
+        shape.asPath(&path);
+        // setup bitmap backing
+        SkAutoPixmapStorage dst;
+        if (!dst.tryAlloc(SkImageInfo::MakeA8(devPathBounds.width(), devPathBounds.height()))) {
+            return false;
+        }
+        sk_bzero(dst.writable_addr(), dst.computeByteSize());
+
+        // rasterize path
+        SkPaint paint;
+        paint.setStyle(SkPaint::kFill_Style);
+        paint.setAntiAlias(true);
+
+        SkDraw draw;
+
+        SkRasterClip rasterClip;
+        rasterClip.setRect(devPathBounds);
+        draw.fRC = &rasterClip;
+        drawMatrix.postTranslate(translateX, translateY);
+        SkSimpleMatrixProvider matrixProvider(drawMatrix);
+        draw.fMatrixProvider = &matrixProvider;
+        draw.fDst = dst;
+
+        draw.drawPathCoverage(path, paint);
+
+        SkRect drawBounds = SkRect::Make(devPathBounds).makeOffset(-translateX, -translateY);
+
+        return this->addToAtlasWithRetry(target, flushInfo, atlasMgr,
+                                         dst.width(), dst.height(), dst.addr(),
+                                         drawBounds, 0, shapeData);
+    }
+
+    void writePathVertices(GrVertexWriter& vertices,
+                           const GrVertexColor& color,
+                           const SkMatrix& ctm,
+                           const GrSmallPathShapeData* shapeData) const {
+        SkRect translatedBounds(shapeData->fBounds);
+        if (!fUsesDistanceField) {
+            translatedBounds.offset(SkScalarFloorToScalar(ctm.get(SkMatrix::kMTransX)),
+                                    SkScalarFloorToScalar(ctm.get(SkMatrix::kMTransY)));
+        }
+
+        // set up texture coordinates
+        auto texCoords = GrVertexWriter::TriStripFromUVs(shapeData->fAtlasLocator.getUVs());
+
+        if (fUsesDistanceField && !ctm.hasPerspective()) {
+            vertices.writeQuad(GrQuad::MakeFromRect(translatedBounds, ctm),
+                               color,
+                               texCoords);
+        } else {
+            vertices.writeQuad(GrVertexWriter::TriStripFromRect(translatedBounds),
+                               color,
+                               texCoords);
+        }
+    }
+
+    void flush(GrMeshDrawTarget* target, FlushInfo* flushInfo) const {
+        GrSmallPathAtlasMgr* atlasMgr = target->smallPathAtlasManager();
+        if (!atlasMgr) {
+            return;
+        }
+
+        int numActiveProxies;
+        const GrSurfaceProxyView* views = atlasMgr->getViews(&numActiveProxies);
+
+        GrGeometryProcessor* gp = flushInfo->fGeometryProcessor;
+        if (gp->numTextureSamplers() != numActiveProxies) {
+            for (int i = gp->numTextureSamplers(); i < numActiveProxies; ++i) {
+                flushInfo->fPrimProcProxies[i] = views[i].proxy();
+                // This op does not know its atlas proxies when it is added to a GrOpsTasks, so the
+                // proxies don't get added during the visitProxies call. Thus we add them here.
+                target->sampledProxyArray()->push_back(views[i].proxy());
+            }
+            // During preparation the number of atlas pages has increased.
+            // Update the proxies used in the GP to match.
+            if (fUsesDistanceField) {
+                reinterpret_cast<GrDistanceFieldPathGeoProc*>(gp)->addNewViews(
+                        views, numActiveProxies, GrSamplerState::Filter::kLinear);
+            } else {
+                reinterpret_cast<GrBitmapTextGeoProc*>(gp)->addNewViews(
+                        views, numActiveProxies, GrSamplerState::Filter::kNearest);
+            }
+        }
+
+        if (flushInfo->fInstancesToFlush) {
+            GrSimpleMesh* mesh = target->allocMesh();
+            mesh->setIndexedPatterned(flushInfo->fIndexBuffer,
+                                      GrResourceProvider::NumIndicesPerNonAAQuad(),
+                                      flushInfo->fInstancesToFlush,
+                                      GrResourceProvider::MaxNumNonAAQuads(),
+                                      flushInfo->fVertexBuffer,
+                                      GrResourceProvider::NumVertsPerNonAAQuad(),
+                                      flushInfo->fVertexOffset);
+            target->recordDraw(flushInfo->fGeometryProcessor, mesh, 1, flushInfo->fPrimProcProxies,
+                               GrPrimitiveType::kTriangles);
+            flushInfo->fVertexOffset += GrResourceProvider::NumVertsPerNonAAQuad() *
+                                        flushInfo->fInstancesToFlush;
+            flushInfo->fInstancesToFlush = 0;
+        }
+    }
+
+    void onExecute(GrOpFlushState* flushState, const SkRect& chainBounds) override {
+        auto pipeline = fHelper.createPipeline(flushState);
+
+        flushState->executeDrawsAndUploadsForMeshDrawOp(this, chainBounds, pipeline,
+                                                        fHelper.stencilSettings());
+    }
+
+    const SkPMColor4f& color() const { return fShapes[0].fColor; }
+    bool usesDistanceField() const { return fUsesDistanceField; }
+
+    CombineResult onCombineIfPossible(GrOp* t, SkArenaAlloc*, const GrCaps& caps) override {
+        SmallPathOp* that = t->cast<SmallPathOp>();
+        if (!fHelper.isCompatible(that->fHelper, caps, this->bounds(), that->bounds())) {
+            return CombineResult::kCannotCombine;
+        }
+
+        if (this->usesDistanceField() != that->usesDistanceField()) {
+            return CombineResult::kCannotCombine;
+        }
+
+        const SkMatrix& thisCtm = this->fShapes[0].fViewMatrix;
+        const SkMatrix& thatCtm = that->fShapes[0].fViewMatrix;
+
+        if (thisCtm.hasPerspective() != thatCtm.hasPerspective()) {
+            return CombineResult::kCannotCombine;
+        }
+
+        // We can position on the cpu unless we're in perspective,
+        // but also need to make sure local matrices are identical
+        if ((thisCtm.hasPerspective() || fHelper.usesLocalCoords()) &&
+            !SkMatrixPriv::CheapEqual(thisCtm, thatCtm)) {
+            return CombineResult::kCannotCombine;
+        }
+
+        // Depending on the ctm we may have a different shader for SDF paths
+        if (this->usesDistanceField()) {
+            if (thisCtm.isScaleTranslate() != thatCtm.isScaleTranslate() ||
+                thisCtm.isSimilarity() != thatCtm.isSimilarity()) {
+                return CombineResult::kCannotCombine;
+            }
+        }
+
+        fShapes.push_back_n(that->fShapes.count(), that->fShapes.begin());
+        fWideColor |= that->fWideColor;
+        return CombineResult::kMerged;
+    }
+
+#if GR_TEST_UTILS
+    SkString onDumpInfo() const override {
+        SkString string;
+        for (const auto& geo : fShapes) {
+            string.appendf("Color: 0x%08x\n", geo.fColor.toBytes_RGBA());
+        }
+        string += fHelper.dumpInfo();
+        return string;
+    }
+#endif
+
+    bool fUsesDistanceField;
+
+    struct Entry {
+        SkPMColor4f   fColor;
+        GrStyledShape fShape;
+        SkMatrix      fViewMatrix;
+    };
+
+    SkSTArray<1, Entry> fShapes;
+    Helper fHelper;
+    bool fGammaCorrect;
+    bool fWideColor;
+
+    using INHERITED = GrMeshDrawOp;
+};
+
+} // anonymous namespace
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+#if GR_TEST_UTILS
+
+GR_DRAW_OP_TEST_DEFINE(SmallPathOp) {
+    SkMatrix viewMatrix = GrTest::TestMatrix(random);
+    bool gammaCorrect = random->nextBool();
+
+    // This path renderer only allows fill styles.
+    GrStyledShape shape(GrTest::TestPath(random), GrStyle::SimpleFill());
+    return SmallPathOp::Make(context, std::move(paint), shape, viewMatrix,
+                             gammaCorrect, GrGetRandomStencil(random, context));
+}
+
+#endif // GR_TEST_UTILS
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+namespace skgpu::v1 {
+
+GrPathRenderer::CanDrawPath SmallPathRenderer::onCanDrawPath(const CanDrawPathArgs& args) const {
+    if (!args.fCaps->shaderCaps()->shaderDerivativeSupport()) {
+        return CanDrawPath::kNo;
+    }
+    // If the shape has no key then we won't get any reuse.
+    if (!args.fShape->hasUnstyledKey()) {
+        return CanDrawPath::kNo;
+    }
+    // This only supports filled paths, however, the caller may apply the style to make a filled
+    // path and try again.
+    if (!args.fShape->style().isSimpleFill()) {
+        return CanDrawPath::kNo;
+    }
+    // This does non-inverse coverage-based antialiased fills.
+    if (GrAAType::kCoverage != args.fAAType) {
+        return CanDrawPath::kNo;
+    }
+    // TODO: Support inverse fill
+    if (args.fShape->inverseFilled()) {
+        return CanDrawPath::kNo;
+    }
+
+    SkScalar scaleFactors[2] = { 1, 1 };
+    // TODO: handle perspective distortion
+    if (!args.fViewMatrix->hasPerspective() && !args.fViewMatrix->getMinMaxScales(scaleFactors)) {
+        return CanDrawPath::kNo;
+    }
+    // For affine transformations, too much shear can produce artifacts.
+    if (!scaleFactors[0] || scaleFactors[1]/scaleFactors[0] > 4) {
+        return CanDrawPath::kNo;
+    }
+    // Only support paths with bounds within kMaxDim by kMaxDim,
+    // scaled to have bounds within kMaxSize by kMaxSize.
+    // The goal is to accelerate rendering of lots of small paths that may be scaling.
+    SkRect bounds = args.fShape->styledBounds();
+    SkScalar minDim = std::min(bounds.width(), bounds.height());
+    SkScalar maxDim = std::max(bounds.width(), bounds.height());
+    SkScalar minSize = minDim * SkScalarAbs(scaleFactors[0]);
+    SkScalar maxSize = maxDim * SkScalarAbs(scaleFactors[1]);
+    if (maxDim > kMaxDim || kMinSize > minSize || maxSize > kMaxSize) {
+        return CanDrawPath::kNo;
+    }
+
+    return CanDrawPath::kYes;
+}
+
+bool SmallPathRenderer::onDrawPath(const DrawPathArgs& args) {
+    GR_AUDIT_TRAIL_AUTO_FRAME(args.fContext->priv().auditTrail(),
+                              "SmallPathRenderer::onDrawPath");
+
+    // we've already bailed on inverse filled paths, so this is safe
+    SkASSERT(!args.fShape->isEmpty());
+    SkASSERT(args.fShape->hasUnstyledKey());
+
+    GrOp::Owner op = SmallPathOp::Make(
+            args.fContext, std::move(args.fPaint), *args.fShape, *args.fViewMatrix,
+            args.fGammaCorrect, args.fUserStencilSettings);
+    args.fSurfaceDrawContext->addDrawOp(args.fClip, std::move(op));
+
+    return true;
+}
+
+} // namespace skgpu::v1