Lift the tessellation atlas into its own path renderer

Creates a new path renderer, GrAtlasPathRenderer, that handles all the
atlasing. Managing the atlas in its own path renderer gives us more
control over when atlasing happens in the chain, will allow us to more
easily use the atlas in kCoverage mode, and makes the clipping code
cleaner.

Bug: skia:12258
Change-Id: Ie0b669974936c23895c8ab794e2d97206ed140f8
Reviewed-on: https://skia-review.googlesource.com/c/skia/+/431896
Commit-Queue: Chris Dalton <csmartdalton@google.com>
Reviewed-by: Michael Ludwig <michaelludwig@google.com>
diff --git a/gm/widebuttcaps.cpp b/gm/widebuttcaps.cpp
index 2feee3c..f781cbe 100644
--- a/gm/widebuttcaps.cpp
+++ b/gm/widebuttcaps.cpp
@@ -11,6 +11,7 @@
 #include "include/core/SkPoint.h"
 #include "include/gpu/GrContextOptions.h"
 #include "include/gpu/GrDirectContext.h"
+#include "include/utils/SkRandom.h"
 #include "src/gpu/GrCaps.h"
 #include "src/gpu/GrDirectContextPriv.h"
 #include "src/gpu/GrDrawingManager.h"
diff --git a/gn/gpu.gni b/gn/gpu.gni
index 092e345..ef82d75 100644
--- a/gn/gpu.gni
+++ b/gn/gpu.gni
@@ -295,6 +295,8 @@
   "$_src/gpu/effects/GrMatrixConvolutionEffect.h",
   "$_src/gpu/effects/GrMatrixEffect.cpp",
   "$_src/gpu/effects/GrMatrixEffect.h",
+  "$_src/gpu/effects/GrModulateAtlasCoverageEffect.cpp",
+  "$_src/gpu/effects/GrModulateAtlasCoverageEffect.h",
   "$_src/gpu/effects/GrOvalEffect.cpp",
   "$_src/gpu/effects/GrOvalEffect.h",
   "$_src/gpu/effects/GrPorterDuffXferProcessor.cpp",
@@ -324,6 +326,8 @@
   "$_src/gpu/geometry/GrWangsFormula.h",
   "$_src/gpu/ops/GrAAConvexTessellator.cpp",
   "$_src/gpu/ops/GrAAConvexTessellator.h",
+  "$_src/gpu/ops/GrAtlasPathRenderer.cpp",
+  "$_src/gpu/ops/GrAtlasPathRenderer.h",
   "$_src/gpu/ops/GrAtlasTextOp.cpp",
   "$_src/gpu/ops/GrAtlasTextOp.h",
   "$_src/gpu/ops/GrClearOp.cpp",
@@ -375,13 +379,9 @@
   "$_src/gpu/gradients/GrGradientShader.h",
 
   # tessellate
-  "$_src/gpu/tessellate/GrAtlasInstancedHelper.cpp",
-  "$_src/gpu/tessellate/GrAtlasInstancedHelper.h",
   "$_src/gpu/tessellate/GrAtlasRenderTask.cpp",
   "$_src/gpu/tessellate/GrAtlasRenderTask.h",
   "$_src/gpu/tessellate/GrCullTest.h",
-  "$_src/gpu/tessellate/GrDrawAtlasPathOp.cpp",
-  "$_src/gpu/tessellate/GrDrawAtlasPathOp.h",
   "$_src/gpu/tessellate/GrMiddleOutPolygonTriangulator.h",
   "$_src/gpu/tessellate/GrPathCurveTessellator.cpp",
   "$_src/gpu/tessellate/GrPathCurveTessellator.h",
@@ -406,8 +406,6 @@
   "$_src/gpu/tessellate/GrVectorXform.h",
 
   # tessellate/shaders
-  "$_src/gpu/tessellate/shaders/GrModulateAtlasCoverageFP.cpp",
-  "$_src/gpu/tessellate/shaders/GrModulateAtlasCoverageFP.h",
   "$_src/gpu/tessellate/shaders/GrPathTessellationShader.cpp",
   "$_src/gpu/tessellate/shaders/GrPathTessellationShader.h",
   "$_src/gpu/tessellate/shaders/GrPathTessellationShader_Hardware.cpp",
@@ -580,10 +578,14 @@
   "$_src/gpu/ops/GrAAHairLinePathRenderer.h",
   "$_src/gpu/ops/GrAALinearizingConvexPathRenderer.cpp",
   "$_src/gpu/ops/GrAALinearizingConvexPathRenderer.h",
+  "$_src/gpu/ops/GrAtlasInstancedHelper.cpp",
+  "$_src/gpu/ops/GrAtlasInstancedHelper.h",
   "$_src/gpu/ops/GrDashLinePathRenderer.cpp",
   "$_src/gpu/ops/GrDashLinePathRenderer.h",
   "$_src/gpu/ops/GrDefaultPathRenderer.cpp",
   "$_src/gpu/ops/GrDefaultPathRenderer.h",
+  "$_src/gpu/ops/GrDrawAtlasPathOp.cpp",
+  "$_src/gpu/ops/GrDrawAtlasPathOp.h",
   "$_src/gpu/ops/GrSmallPathRenderer.cpp",
   "$_src/gpu/ops/GrSmallPathRenderer.h",
   "$_src/gpu/ops/GrTriangulatingPathRenderer.cpp",
diff --git a/include/private/GrTypesPriv.h b/include/private/GrTypesPriv.h
index c429a3d..cb2ca30 100644
--- a/include/private/GrTypesPriv.h
+++ b/include/private/GrTypesPriv.h
@@ -852,14 +852,15 @@
 enum class GpuPathRenderers {
     kNone              =   0,  // Always use software masks and/or GrDefaultPathRenderer.
     kDashLine          =   1 << 0,
-    kTessellation      =   1 << 1,
-    kCoverageCounting  =   1 << 2,
-    kAAHairline        =   1 << 3,
-    kAAConvex          =   1 << 4,
-    kAALinearizing     =   1 << 5,
-    kSmall             =   1 << 6,
-    kTriangulating     =   1 << 7,
-    kDefault           = ((1 << 8) - 1)  // All path renderers.
+    kAtlas             =   1 << 1,
+    kTessellation      =   1 << 2,
+    kCoverageCounting  =   1 << 3,
+    kAAHairline        =   1 << 4,
+    kAAConvex          =   1 << 5,
+    kAALinearizing     =   1 << 6,
+    kSmall             =   1 << 7,
+    kTriangulating     =   1 << 8,
+    kDefault           = ((1 << 9) - 1)  // All path renderers.
 };
 
 /**
diff --git a/infra/bots/gen_tasks_logic/dm_flags.go b/infra/bots/gen_tasks_logic/dm_flags.go
index 379a228..ae72df2 100644
--- a/infra/bots/gen_tasks_logic/dm_flags.go
+++ b/infra/bots/gen_tasks_logic/dm_flags.go
@@ -471,7 +471,8 @@
 			// Use hardware tessellation as much as possible for testing. Use 16 segments max to
 			// verify the chopping logic.
 			args = append(args,
-				"--pr", "tess", "--hwtess", "--alwaysHwTess", "--maxTessellationSegments", "16")
+				"--pr", "atlas", "tess", "--hwtess", "--alwaysHwTess",
+				"--maxTessellationSegments", "16")
 		}
 
 		// DDL is a GPU-only feature
diff --git a/infra/bots/tasks.json b/infra/bots/tasks.json
index 88469f2..d27e15a 100755
--- a/infra/bots/tasks.json
+++ b/infra/bots/tasks.json
@@ -53153,7 +53153,7 @@
         "skia/infra/bots/run_recipe.py",
         "${ISOLATED_OUTDIR}",
         "test",
-        "{\"$kitchen\":{\"devshell\":true,\"git_auth\":true},\"buildbucket_build_id\":\"<(BUILDBUCKET_BUILD_ID)\",\"buildername\":\"Test-Win10-Clang-Golo-GPU-QuadroP400-x86_64-Debug-All-GpuTess\",\"dm_flags\":\"[\\\"dm\\\",\\\"--nameByHash\\\",\\\"--key\\\",\\\"arch\\\",\\\"x86_64\\\",\\\"compiler\\\",\\\"Clang\\\",\\\"configuration\\\",\\\"Debug\\\",\\\"cpu_or_gpu\\\",\\\"GPU\\\",\\\"cpu_or_gpu_value\\\",\\\"QuadroP400\\\",\\\"extra_config\\\",\\\"GpuTess\\\",\\\"model\\\",\\\"Golo\\\",\\\"os\\\",\\\"Win10\\\",\\\"style\\\",\\\"default\\\",\\\"--randomProcessorTest\\\",\\\"--nocpu\\\",\\\"--pr\\\",\\\"tess\\\",\\\"--hwtess\\\",\\\"--alwaysHwTess\\\",\\\"--maxTessellationSegments\\\",\\\"16\\\",\\\"--config\\\",\\\"glmsaa4\\\",\\\"--src\\\",\\\"tests\\\",\\\"gm\\\",\\\"image\\\",\\\"colorImage\\\",\\\"svg\\\",\\\"--skip\\\",\\\"_\\\",\\\"svg\\\",\\\"_\\\",\\\"svgparse_\\\",\\\"_\\\",\\\"image\\\",\\\"gen_platf\\\",\\\"pal8os2v2.bmp\\\",\\\"_\\\",\\\"image\\\",\\\"gen_platf\\\",\\\"pal8os2v2-16.bmp\\\",\\\"_\\\",\\\"image\\\",\\\"gen_platf\\\",\\\"rgba32abf.bmp\\\",\\\"_\\\",\\\"image\\\",\\\"gen_platf\\\",\\\"rgb24prof.bmp\\\",\\\"_\\\",\\\"image\\\",\\\"gen_platf\\\",\\\"rgb24lprof.bmp\\\",\\\"_\\\",\\\"image\\\",\\\"gen_platf\\\",\\\"8bpp-pixeldata-cropped.bmp\\\",\\\"_\\\",\\\"image\\\",\\\"gen_platf\\\",\\\"4bpp-pixeldata-cropped.bmp\\\",\\\"_\\\",\\\"image\\\",\\\"gen_platf\\\",\\\"32bpp-pixeldata-cropped.bmp\\\",\\\"_\\\",\\\"image\\\",\\\"gen_platf\\\",\\\"24bpp-pixeldata-cropped.bmp\\\",\\\"_\\\",\\\"image\\\",\\\"gen_platf\\\",\\\"testimgari.jpg\\\",\\\"_\\\",\\\"image\\\",\\\"gen_platf\\\",\\\"rle8-height-negative.bmp\\\",\\\"_\\\",\\\"image\\\",\\\"gen_platf\\\",\\\"rle4-height-negative.bmp\\\",\\\"_\\\",\\\"image\\\",\\\"gen_platf\\\",\\\"error\\\",\\\"_\\\",\\\"image\\\",\\\"_\\\",\\\"interlaced1.png\\\",\\\"_\\\",\\\"image\\\",\\\"_\\\",\\\"interlaced2.png\\\",\\\"_\\\",\\\"image\\\",\\\"_\\\",\\\"interlaced3.png\\\",\\\"_\\\",\\\"image\\\",\\\"_\\\",\\\".arw\\\",\\\"_\\\",\\\"image\\\",\\\"_\\\",\\\".cr2\\\",\\\"_\\\",\\\"image\\\",\\\"_\\\",\\\".dng\\\",\\\"_\\\",\\\"image\\\",\\\"_\\\",\\\".nef\\\",\\\"_\\\",\\\"image\\\",\\\"_\\\",\\\".nrw\\\",\\\"_\\\",\\\"image\\\",\\\"_\\\",\\\".orf\\\",\\\"_\\\",\\\"image\\\",\\\"_\\\",\\\".raf\\\",\\\"_\\\",\\\"image\\\",\\\"_\\\",\\\".rw2\\\",\\\"_\\\",\\\"image\\\",\\\"_\\\",\\\".pef\\\",\\\"_\\\",\\\"image\\\",\\\"_\\\",\\\".srw\\\",\\\"_\\\",\\\"image\\\",\\\"_\\\",\\\".ARW\\\",\\\"_\\\",\\\"image\\\",\\\"_\\\",\\\".CR2\\\",\\\"_\\\",\\\"image\\\",\\\"_\\\",\\\".DNG\\\",\\\"_\\\",\\\"image\\\",\\\"_\\\",\\\".NEF\\\",\\\"_\\\",\\\"image\\\",\\\"_\\\",\\\".NRW\\\",\\\"_\\\",\\\"image\\\",\\\"_\\\",\\\".ORF\\\",\\\"_\\\",\\\"image\\\",\\\"_\\\",\\\".RAF\\\",\\\"_\\\",\\\"image\\\",\\\"_\\\",\\\".RW2\\\",\\\"_\\\",\\\"image\\\",\\\"_\\\",\\\".PEF\\\",\\\"_\\\",\\\"image\\\",\\\"_\\\",\\\".SRW\\\",\\\"--nonativeFonts\\\",\\\"--verbose\\\"]\",\"dm_properties\":\"{\\\"buildbucket_build_id\\\":\\\"<(BUILDBUCKET_BUILD_ID)\\\",\\\"builder\\\":\\\"Test-Win10-Clang-Golo-GPU-QuadroP400-x86_64-Debug-All-GpuTess\\\",\\\"gitHash\\\":\\\"<(REVISION)\\\",\\\"issue\\\":\\\"<(ISSUE)\\\",\\\"patch_storage\\\":\\\"<(PATCH_STORAGE)\\\",\\\"patchset\\\":\\\"<(PATCHSET)\\\",\\\"swarming_bot_id\\\":\\\"${SWARMING_BOT_ID}\\\",\\\"swarming_task_id\\\":\\\"${SWARMING_TASK_ID}\\\",\\\"task_id\\\":\\\"<(TASK_ID)\\\"}\",\"do_upload\":\"true\",\"gold_hashes_url\":\"https://storage.googleapis.com/skia-infra-gm/hash_files/gold-prod-hashes.txt\",\"images\":\"true\",\"patch_issue\":\"<(ISSUE_INT)\",\"patch_ref\":\"<(PATCH_REF)\",\"patch_repo\":\"<(PATCH_REPO)\",\"patch_set\":\"<(PATCHSET_INT)\",\"patch_storage\":\"<(PATCH_STORAGE)\",\"repository\":\"<(REPO)\",\"resources\":\"true\",\"revision\":\"<(REVISION)\",\"skps\":\"true\",\"svgs\":\"true\",\"swarm_out_dir\":\"test\",\"task_id\":\"<(TASK_ID)\"}",
+        "{\"$kitchen\":{\"devshell\":true,\"git_auth\":true},\"buildbucket_build_id\":\"<(BUILDBUCKET_BUILD_ID)\",\"buildername\":\"Test-Win10-Clang-Golo-GPU-QuadroP400-x86_64-Debug-All-GpuTess\",\"dm_flags\":\"[\\\"dm\\\",\\\"--nameByHash\\\",\\\"--key\\\",\\\"arch\\\",\\\"x86_64\\\",\\\"compiler\\\",\\\"Clang\\\",\\\"configuration\\\",\\\"Debug\\\",\\\"cpu_or_gpu\\\",\\\"GPU\\\",\\\"cpu_or_gpu_value\\\",\\\"QuadroP400\\\",\\\"extra_config\\\",\\\"GpuTess\\\",\\\"model\\\",\\\"Golo\\\",\\\"os\\\",\\\"Win10\\\",\\\"style\\\",\\\"default\\\",\\\"--randomProcessorTest\\\",\\\"--nocpu\\\",\\\"--pr\\\",\\\"atlas\\\",\\\"tess\\\",\\\"--hwtess\\\",\\\"--alwaysHwTess\\\",\\\"--maxTessellationSegments\\\",\\\"16\\\",\\\"--config\\\",\\\"glmsaa4\\\",\\\"--src\\\",\\\"tests\\\",\\\"gm\\\",\\\"image\\\",\\\"colorImage\\\",\\\"svg\\\",\\\"--skip\\\",\\\"_\\\",\\\"svg\\\",\\\"_\\\",\\\"svgparse_\\\",\\\"_\\\",\\\"image\\\",\\\"gen_platf\\\",\\\"pal8os2v2.bmp\\\",\\\"_\\\",\\\"image\\\",\\\"gen_platf\\\",\\\"pal8os2v2-16.bmp\\\",\\\"_\\\",\\\"image\\\",\\\"gen_platf\\\",\\\"rgba32abf.bmp\\\",\\\"_\\\",\\\"image\\\",\\\"gen_platf\\\",\\\"rgb24prof.bmp\\\",\\\"_\\\",\\\"image\\\",\\\"gen_platf\\\",\\\"rgb24lprof.bmp\\\",\\\"_\\\",\\\"image\\\",\\\"gen_platf\\\",\\\"8bpp-pixeldata-cropped.bmp\\\",\\\"_\\\",\\\"image\\\",\\\"gen_platf\\\",\\\"4bpp-pixeldata-cropped.bmp\\\",\\\"_\\\",\\\"image\\\",\\\"gen_platf\\\",\\\"32bpp-pixeldata-cropped.bmp\\\",\\\"_\\\",\\\"image\\\",\\\"gen_platf\\\",\\\"24bpp-pixeldata-cropped.bmp\\\",\\\"_\\\",\\\"image\\\",\\\"gen_platf\\\",\\\"testimgari.jpg\\\",\\\"_\\\",\\\"image\\\",\\\"gen_platf\\\",\\\"rle8-height-negative.bmp\\\",\\\"_\\\",\\\"image\\\",\\\"gen_platf\\\",\\\"rle4-height-negative.bmp\\\",\\\"_\\\",\\\"image\\\",\\\"gen_platf\\\",\\\"error\\\",\\\"_\\\",\\\"image\\\",\\\"_\\\",\\\"interlaced1.png\\\",\\\"_\\\",\\\"image\\\",\\\"_\\\",\\\"interlaced2.png\\\",\\\"_\\\",\\\"image\\\",\\\"_\\\",\\\"interlaced3.png\\\",\\\"_\\\",\\\"image\\\",\\\"_\\\",\\\".arw\\\",\\\"_\\\",\\\"image\\\",\\\"_\\\",\\\".cr2\\\",\\\"_\\\",\\\"image\\\",\\\"_\\\",\\\".dng\\\",\\\"_\\\",\\\"image\\\",\\\"_\\\",\\\".nef\\\",\\\"_\\\",\\\"image\\\",\\\"_\\\",\\\".nrw\\\",\\\"_\\\",\\\"image\\\",\\\"_\\\",\\\".orf\\\",\\\"_\\\",\\\"image\\\",\\\"_\\\",\\\".raf\\\",\\\"_\\\",\\\"image\\\",\\\"_\\\",\\\".rw2\\\",\\\"_\\\",\\\"image\\\",\\\"_\\\",\\\".pef\\\",\\\"_\\\",\\\"image\\\",\\\"_\\\",\\\".srw\\\",\\\"_\\\",\\\"image\\\",\\\"_\\\",\\\".ARW\\\",\\\"_\\\",\\\"image\\\",\\\"_\\\",\\\".CR2\\\",\\\"_\\\",\\\"image\\\",\\\"_\\\",\\\".DNG\\\",\\\"_\\\",\\\"image\\\",\\\"_\\\",\\\".NEF\\\",\\\"_\\\",\\\"image\\\",\\\"_\\\",\\\".NRW\\\",\\\"_\\\",\\\"image\\\",\\\"_\\\",\\\".ORF\\\",\\\"_\\\",\\\"image\\\",\\\"_\\\",\\\".RAF\\\",\\\"_\\\",\\\"image\\\",\\\"_\\\",\\\".RW2\\\",\\\"_\\\",\\\"image\\\",\\\"_\\\",\\\".PEF\\\",\\\"_\\\",\\\"image\\\",\\\"_\\\",\\\".SRW\\\",\\\"--nonativeFonts\\\",\\\"--verbose\\\"]\",\"dm_properties\":\"{\\\"buildbucket_build_id\\\":\\\"<(BUILDBUCKET_BUILD_ID)\\\",\\\"builder\\\":\\\"Test-Win10-Clang-Golo-GPU-QuadroP400-x86_64-Debug-All-GpuTess\\\",\\\"gitHash\\\":\\\"<(REVISION)\\\",\\\"issue\\\":\\\"<(ISSUE)\\\",\\\"patch_storage\\\":\\\"<(PATCH_STORAGE)\\\",\\\"patchset\\\":\\\"<(PATCHSET)\\\",\\\"swarming_bot_id\\\":\\\"${SWARMING_BOT_ID}\\\",\\\"swarming_task_id\\\":\\\"${SWARMING_TASK_ID}\\\",\\\"task_id\\\":\\\"<(TASK_ID)\\\"}\",\"do_upload\":\"true\",\"gold_hashes_url\":\"https://storage.googleapis.com/skia-infra-gm/hash_files/gold-prod-hashes.txt\",\"images\":\"true\",\"patch_issue\":\"<(ISSUE_INT)\",\"patch_ref\":\"<(PATCH_REF)\",\"patch_repo\":\"<(PATCH_REPO)\",\"patch_set\":\"<(PATCHSET_INT)\",\"patch_storage\":\"<(PATCH_STORAGE)\",\"repository\":\"<(REPO)\",\"resources\":\"true\",\"revision\":\"<(REVISION)\",\"skps\":\"true\",\"svgs\":\"true\",\"swarm_out_dir\":\"test\",\"task_id\":\"<(TASK_ID)\"}",
         "skia"
       ],
       "dependencies": [
diff --git a/src/gpu/GrClipStack.cpp b/src/gpu/GrClipStack.cpp
index 3a4affa..bddd44d 100644
--- a/src/gpu/GrClipStack.cpp
+++ b/src/gpu/GrClipStack.cpp
@@ -25,7 +25,7 @@
 #include "src/gpu/effects/GrRRectEffect.h"
 #include "src/gpu/effects/GrTextureEffect.h"
 #include "src/gpu/geometry/GrQuadUtils.h"
-#include "src/gpu/tessellate/GrTessellationPathRenderer.h"
+#include "src/gpu/ops/GrAtlasPathRenderer.h"
 
 namespace {
 
@@ -236,7 +236,7 @@
 // into a shared atlas.
 static GrFPResult clip_atlas_fp(GrRecordingContext* rContext,
                                 const GrOp* opBeingClipped,
-                                GrTessellationPathRenderer* tessellator,
+                                GrAtlasPathRenderer* atlasPathRenderer,
                                 const SkIRect& scissorBounds,
                                 const GrClipStack::Element& e,
                                 std::unique_ptr<GrFragmentProcessor> inputFP) {
@@ -250,8 +250,8 @@
         // Toggling fill type does not affect the path's "generationID" key.
         path.toggleInverseFillType();
     }
-    return tessellator->makeAtlasClipFP(rContext, opBeingClipped, std::move(inputFP), scissorBounds,
-                                        e.fLocalToDevice, path);
+    return atlasPathRenderer->makeAtlasClipEffect(rContext, opBeingClipped, std::move(inputFP),
+                                                  scissorBounds, e.fLocalToDevice, path);
 }
 
 static void draw_to_sw_mask(GrSWMaskHelper* helper, const GrClipStack::Element& e, bool clearMask) {
@@ -1340,7 +1340,7 @@
     SkSTArray<kNumStackMasks, const Element*> elementsForMask;
 
     bool maskRequiresAA = false;
-    auto* tessellator = context->priv().drawingManager()->getTessellationPathRenderer();
+    auto* atlasPathRenderer = context->priv().drawingManager()->getAtlasPathRenderer();
 
     int i = fElements.count();
     for (const RawElement& e : fElements.ritems()) {
@@ -1412,8 +1412,9 @@
                     std::tie(fullyApplied, clipFP) = analytic_clip_fp(e.asElement(),
                                                                       *caps->shaderCaps(),
                                                                       std::move(clipFP));
-                    if (!fullyApplied && tessellator) {
-                        std::tie(fullyApplied, clipFP) = clip_atlas_fp(context, op, tessellator,
+                    if (!fullyApplied && atlasPathRenderer) {
+                        std::tie(fullyApplied, clipFP) = clip_atlas_fp(context, op,
+                                                                       atlasPathRenderer,
                                                                        scissorBounds, e.asElement(),
                                                                        std::move(clipFP));
                     }
diff --git a/src/gpu/GrDrawingManager.cpp b/src/gpu/GrDrawingManager.cpp
index bc981e3..fcc3b36 100644
--- a/src/gpu/GrDrawingManager.cpp
+++ b/src/gpu/GrDrawingManager.cpp
@@ -960,6 +960,14 @@
     return fSoftwarePathRenderer.get();
 }
 
+GrAtlasPathRenderer* GrDrawingManager::getAtlasPathRenderer() {
+    if (!fPathRendererChain) {
+        fPathRendererChain = std::make_unique<GrPathRendererChain>(fContext,
+                                                                   fOptionsForPathRendererChain);
+    }
+    return fPathRendererChain->getAtlasPathRenderer();
+}
+
 GrTessellationPathRenderer* GrDrawingManager::getTessellationPathRenderer() {
     if (!fPathRendererChain) {
         fPathRendererChain = std::make_unique<GrPathRendererChain>(fContext,
diff --git a/src/gpu/GrDrawingManager.h b/src/gpu/GrDrawingManager.h
index 8b5d273..957fee8 100644
--- a/src/gpu/GrDrawingManager.h
+++ b/src/gpu/GrDrawingManager.h
@@ -119,6 +119,10 @@
 
     GrPathRenderer* getSoftwarePathRenderer();
 
+    // Returns a direct pointer to the atlas path renderer, or null if it is not supported and
+    // turned on.
+    GrAtlasPathRenderer* getAtlasPathRenderer();
+
     // Returns a direct pointer to the tessellation path renderer, or null if it is not supported
     // and turned on.
     GrTessellationPathRenderer* getTessellationPathRenderer();
diff --git a/src/gpu/GrPathRendererChain.cpp b/src/gpu/GrPathRendererChain.cpp
index 8f13bf6..ea4a190 100644
--- a/src/gpu/GrPathRendererChain.cpp
+++ b/src/gpu/GrPathRendererChain.cpp
@@ -19,6 +19,7 @@
 #include "src/gpu/ops/GrAAConvexPathRenderer.h"
 #include "src/gpu/ops/GrAAHairLinePathRenderer.h"
 #include "src/gpu/ops/GrAALinearizingConvexPathRenderer.h"
+#include "src/gpu/ops/GrAtlasPathRenderer.h"
 #include "src/gpu/ops/GrDashLinePathRenderer.h"
 #include "src/gpu/ops/GrDefaultPathRenderer.h"
 #include "src/gpu/ops/GrSmallPathRenderer.h"
@@ -45,11 +46,17 @@
     if (options.fGpuPathRenderers & GpuPathRenderers::kTriangulating) {
         fChain.push_back(sk_make_sp<GrTriangulatingPathRenderer>());
     }
+    if (options.fGpuPathRenderers & GpuPathRenderers::kAtlas) {
+        if (auto atlasPathRenderer = GrAtlasPathRenderer::Make(context)) {
+            fAtlasPathRenderer = atlasPathRenderer.get();
+            context->priv().addOnFlushCallbackObject(atlasPathRenderer.get());
+            fChain.push_back(std::move(atlasPathRenderer));
+        }
+    }
     if (options.fGpuPathRenderers & GpuPathRenderers::kTessellation) {
         if (GrTessellationPathRenderer::IsSupported(caps)) {
-            auto tess = sk_make_sp<GrTessellationPathRenderer>(context);
+            auto tess = sk_make_sp<GrTessellationPathRenderer>();
             fTessellationPathRenderer = tess.get();
-            context->priv().addOnFlushCallbackObject(tess.get());
             fChain.push_back(std::move(tess));
         }
     }
diff --git a/src/gpu/GrPathRendererChain.h b/src/gpu/GrPathRendererChain.h
index b38efea..53bf0bc 100644
--- a/src/gpu/GrPathRendererChain.h
+++ b/src/gpu/GrPathRendererChain.h
@@ -15,6 +15,7 @@
 #include "include/private/SkNoncopyable.h"
 #include "include/private/SkTArray.h"
 
+class GrAtlasPathRenderer;
 class GrTessellationPathRenderer;
 
 /**
@@ -47,6 +48,12 @@
                                     DrawType drawType,
                                     GrPathRenderer::StencilSupport* stencilSupport);
 
+    /** Returns a direct pointer to the atlas path renderer, or null if it is not in the
+        chain. */
+    GrAtlasPathRenderer* getAtlasPathRenderer() {
+        return fAtlasPathRenderer;
+    }
+
     /** Returns a direct pointer to the tessellation path renderer, or null if it is not in the
         chain. */
     GrTessellationPathRenderer* getTessellationPathRenderer() {
@@ -58,6 +65,7 @@
         kPreAllocCount = 8,
     };
     SkSTArray<kPreAllocCount, sk_sp<GrPathRenderer>>    fChain;
+    GrAtlasPathRenderer*                                fAtlasPathRenderer = nullptr;
     GrTessellationPathRenderer*                         fTessellationPathRenderer = nullptr;
 };
 
diff --git a/src/gpu/GrProcessor.h b/src/gpu/GrProcessor.h
index a291e91..a77ba53 100644
--- a/src/gpu/GrProcessor.h
+++ b/src/gpu/GrProcessor.h
@@ -97,7 +97,7 @@
         kFwidthSquircleTestProcessor_ClassID,
         kSwizzleFragmentProcessor_ClassID,
         kTessellate_BoundingBoxShader_ClassID,
-        kTessellate_GrModulateAtlasCoverageFP_ClassID,
+        kTessellate_GrModulateAtlasCoverageEffect_ClassID,
         kTessellate_GrStrokeTessellationShader_ClassID,
         kTessellate_HardwareCurveShader_ClassID,
         kTessellate_HardwareWedgeShader_ClassID,
diff --git a/src/gpu/tessellate/shaders/GrModulateAtlasCoverageFP.cpp b/src/gpu/effects/GrModulateAtlasCoverageEffect.cpp
similarity index 81%
rename from src/gpu/tessellate/shaders/GrModulateAtlasCoverageFP.cpp
rename to src/gpu/effects/GrModulateAtlasCoverageEffect.cpp
index 8c1b153..c23e1e6 100644
--- a/src/gpu/tessellate/shaders/GrModulateAtlasCoverageFP.cpp
+++ b/src/gpu/effects/GrModulateAtlasCoverageEffect.cpp
@@ -5,17 +5,18 @@
  * found in the LICENSE file.
  */
 
-#include "src/gpu/tessellate/shaders/GrModulateAtlasCoverageFP.h"
+#include "src/gpu/effects/GrModulateAtlasCoverageEffect.h"
 
 #include "src/gpu/GrDynamicAtlas.h"
 #include "src/gpu/effects/GrTextureEffect.h"
 
-GrModulateAtlasCoverageFP::GrModulateAtlasCoverageFP(Flags flags,
-                                                     std::unique_ptr<GrFragmentProcessor> inputFP,
-                                                     GrSurfaceProxyView atlasView,
-                                                     const SkMatrix& devToAtlasMatrix,
-                                                     const SkIRect& devIBounds)
-        : GrFragmentProcessor(kTessellate_GrModulateAtlasCoverageFP_ClassID,
+GrModulateAtlasCoverageEffect::GrModulateAtlasCoverageEffect(
+        Flags flags,
+        std::unique_ptr<GrFragmentProcessor> inputFP,
+        GrSurfaceProxyView atlasView,
+        const SkMatrix& devToAtlasMatrix,
+        const SkIRect& devIBounds)
+        : GrFragmentProcessor(kTessellate_GrModulateAtlasCoverageEffect_ClassID,
                               kCompatibleWithCoverageAsAlpha_OptimizationFlag)
         , fFlags(flags)
         , fBounds((fFlags & Flags::kCheckBounds) ? devIBounds : SkIRect{0,0,0,0}) {
@@ -25,18 +26,19 @@
                         SkSL::SampleUsage::Explicit());
 }
 
-GrModulateAtlasCoverageFP::GrModulateAtlasCoverageFP(const GrModulateAtlasCoverageFP& that)
-        : GrFragmentProcessor(kTessellate_GrModulateAtlasCoverageFP_ClassID,
+GrModulateAtlasCoverageEffect::GrModulateAtlasCoverageEffect(
+        const GrModulateAtlasCoverageEffect& that)
+        : GrFragmentProcessor(kTessellate_GrModulateAtlasCoverageEffect_ClassID,
                               kCompatibleWithCoverageAsAlpha_OptimizationFlag)
         , fFlags(that.fFlags)
         , fBounds(that.fBounds) {
     this->cloneAndRegisterAllChildProcessors(that);
 }
 
-std::unique_ptr<GrGLSLFragmentProcessor> GrModulateAtlasCoverageFP::onMakeProgramImpl() const {
+std::unique_ptr<GrGLSLFragmentProcessor> GrModulateAtlasCoverageEffect::onMakeProgramImpl() const {
     class Impl : public GrGLSLFragmentProcessor {
         void emitCode(EmitArgs& args) override {
-            auto fp = args.fFp.cast<GrModulateAtlasCoverageFP>();
+            auto fp = args.fFp.cast<GrModulateAtlasCoverageEffect>();
             auto f = args.fFragBuilder;
             auto uniHandler = args.fUniformHandler;
             SkString inputColor = this->invokeChild(0, args);
@@ -65,7 +67,7 @@
         }
         void onSetData(const GrGLSLProgramDataManager& pdman,
                        const GrFragmentProcessor& processor) override {
-            auto fp = processor.cast<GrModulateAtlasCoverageFP>();
+            auto fp = processor.cast<GrModulateAtlasCoverageEffect>();
             if (fp.fFlags & Flags::kCheckBounds) {
                 pdman.set4fv(fBoundsUniform, 1, SkRect::Make(fp.fBounds).asScalars());
             }
diff --git a/src/gpu/tessellate/shaders/GrModulateAtlasCoverageFP.h b/src/gpu/effects/GrModulateAtlasCoverageEffect.h
similarity index 62%
rename from src/gpu/tessellate/shaders/GrModulateAtlasCoverageFP.h
rename to src/gpu/effects/GrModulateAtlasCoverageEffect.h
index c3ab717..fee39f1 100644
--- a/src/gpu/tessellate/shaders/GrModulateAtlasCoverageFP.h
+++ b/src/gpu/effects/GrModulateAtlasCoverageEffect.h
@@ -5,13 +5,13 @@
  * found in the LICENSE file.
  */
 
-#ifndef GrGrModulateAtlasCoverageFP_DEFINED
-#define GrGrModulateAtlasCoverageFP_DEFINED
+#ifndef GrGrModulateAtlasCoverageEffect_DEFINED
+#define GrGrModulateAtlasCoverageEffect_DEFINED
 
 #include "src/gpu/GrFragmentProcessor.h"
 
 // Multiplies 'inputFP' by the coverage value in an atlas, optionally inverting or clamping to 0.
-class GrModulateAtlasCoverageFP : public GrFragmentProcessor {
+class GrModulateAtlasCoverageEffect : public GrFragmentProcessor {
 public:
     enum class Flags {
         kNone = 0,
@@ -21,11 +21,11 @@
 
     GR_DECL_BITFIELD_CLASS_OPS_FRIENDS(Flags);
 
-    GrModulateAtlasCoverageFP(Flags flags, std::unique_ptr<GrFragmentProcessor> inputFP,
-                              GrSurfaceProxyView atlasView, const SkMatrix& devToAtlasMatrix,
-                              const SkIRect& devIBounds);
+    GrModulateAtlasCoverageEffect(Flags flags, std::unique_ptr<GrFragmentProcessor> inputFP,
+                                  GrSurfaceProxyView atlasView, const SkMatrix& devToAtlasMatrix,
+                                  const SkIRect& devIBounds);
 
-    GrModulateAtlasCoverageFP(const GrModulateAtlasCoverageFP& that);
+    GrModulateAtlasCoverageEffect(const GrModulateAtlasCoverageEffect& that);
 
     const char* name() const override {
         return "GrModulateAtlasCoverageFP";
@@ -34,10 +34,10 @@
         b->add32(fFlags & Flags::kCheckBounds);
     }
     std::unique_ptr<GrFragmentProcessor> clone() const override {
-        return std::make_unique<GrModulateAtlasCoverageFP>(*this);
+        return std::make_unique<GrModulateAtlasCoverageEffect>(*this);
     }
     bool onIsEqual(const GrFragmentProcessor& that) const override {
-        auto fp = that.cast<GrModulateAtlasCoverageFP>();
+        auto fp = that.cast<GrModulateAtlasCoverageEffect>();
         return fFlags == fp.fFlags && fBounds == fp.fBounds;
     }
     std::unique_ptr<GrGLSLFragmentProcessor> onMakeProgramImpl() const override;
@@ -47,6 +47,6 @@
     const SkIRect fBounds;
 };
 
-GR_MAKE_BITFIELD_CLASS_OPS(GrModulateAtlasCoverageFP::Flags)
+GR_MAKE_BITFIELD_CLASS_OPS(GrModulateAtlasCoverageEffect::Flags)
 
 #endif
diff --git a/src/gpu/tessellate/GrAtlasInstancedHelper.cpp b/src/gpu/ops/GrAtlasInstancedHelper.cpp
similarity index 98%
rename from src/gpu/tessellate/GrAtlasInstancedHelper.cpp
rename to src/gpu/ops/GrAtlasInstancedHelper.cpp
index 19a7638..082c240 100644
--- a/src/gpu/tessellate/GrAtlasInstancedHelper.cpp
+++ b/src/gpu/ops/GrAtlasInstancedHelper.cpp
@@ -5,7 +5,7 @@
  * found in the LICENSE file.
  */
 
-#include "src/gpu/tessellate/GrAtlasInstancedHelper.h"
+#include "src/gpu/ops/GrAtlasInstancedHelper.h"
 
 #include "src/gpu/GrVertexWriter.h"
 #include "src/gpu/glsl/GrGLSLFragmentShaderBuilder.h"
diff --git a/src/gpu/tessellate/GrAtlasInstancedHelper.h b/src/gpu/ops/GrAtlasInstancedHelper.h
similarity index 100%
rename from src/gpu/tessellate/GrAtlasInstancedHelper.h
rename to src/gpu/ops/GrAtlasInstancedHelper.h
diff --git a/src/gpu/ops/GrAtlasPathRenderer.cpp b/src/gpu/ops/GrAtlasPathRenderer.cpp
new file mode 100644
index 0000000..4620bf3
--- /dev/null
+++ b/src/gpu/ops/GrAtlasPathRenderer.cpp
@@ -0,0 +1,386 @@
+/*
+ * Copyright 2019 Google LLC.
+ *
+ * Use of this source code is governed by a BSD-style license that can be
+ * found in the LICENSE file.
+ */
+
+#include "src/gpu/ops/GrAtlasPathRenderer.h"
+
+#include "include/private/SkVx.h"
+#include "src/core/SkIPoint16.h"
+#include "src/gpu/GrClip.h"
+#include "src/gpu/GrDirectContextPriv.h"
+#include "src/gpu/GrSurfaceDrawContext.h"
+#include "src/gpu/GrVx.h"
+#include "src/gpu/effects/GrModulateAtlasCoverageEffect.h"
+#include "src/gpu/geometry/GrStyledShape.h"
+#include "src/gpu/ops/GrDrawAtlasPathOp.h"
+#include "src/gpu/tessellate/GrAtlasRenderTask.h"
+#include "src/gpu/tessellate/GrTessellationPathRenderer.h"
+#include "src/gpu/tessellate/shaders/GrTessellationShader.h"
+
+using grvx::float2;
+using grvx::int2;
+
+constexpr static auto kAtlasAlpha8Type = GrColorType::kAlpha_8;
+constexpr static int kAtlasInitialSize = 512;
+
+// The atlas is only used for small-area paths, which means at least one dimension of every path is
+// guaranteed to be quite small. So if we transpose tall paths, then every path will have a small
+// height, which lends very well to efficient pow2 atlas packing.
+constexpr static auto kAtlasAlgorithm = GrDynamicAtlas::RectanizerAlgorithm::kPow2;
+
+// Ensure every path in the atlas falls in or below the 128px high rectanizer band.
+constexpr static int kAtlasMaxPathHeight = 128;
+
+bool GrAtlasPathRenderer::IsSupported(GrRecordingContext* rContext) {
+    const GrCaps& caps = *rContext->priv().caps();
+    auto atlasFormat = caps.getDefaultBackendFormat(kAtlasAlpha8Type, GrRenderable::kYes);
+    return rContext->asDirectContext() &&  // The atlas doesn't support DDL yet.
+           caps.internalMultisampleCount(atlasFormat) > 1 &&
+           // GrAtlasRenderTask currently requires tessellation. In the future it could use the
+           // default path renderer when tessellation isn't available.
+           GrTessellationPathRenderer::IsSupported(caps);
+}
+
+sk_sp<GrAtlasPathRenderer> GrAtlasPathRenderer::Make(GrRecordingContext* rContext) {
+    return IsSupported(rContext)
+            ? sk_sp<GrAtlasPathRenderer>(new GrAtlasPathRenderer(rContext->asDirectContext()))
+            : nullptr;
+}
+
+GrAtlasPathRenderer::GrAtlasPathRenderer(GrDirectContext* dContext) {
+    SkASSERT(IsSupported(dContext));
+    const GrCaps& caps = *dContext->priv().caps();
+#if GR_TEST_UTILS
+    fAtlasMaxSize = dContext->priv().options().fMaxTextureAtlasSize;
+#else
+    fAtlasMaxSize = 2048;
+#endif
+    fAtlasMaxSize = SkPrevPow2(std::min(fAtlasMaxSize, (float)caps.maxPreferredRenderTargetSize()));
+    fAtlasInitialSize = SkNextPow2(std::min(kAtlasInitialSize, (int)fAtlasMaxSize));
+}
+
+// Returns the rect [topLeftFloor, botRightCeil], which is the rect [r] rounded out to integer
+// boundaries.
+static std::tuple<float2,float2> round_out(const SkRect& r) {
+    return {skvx::floor(float2::Load(&r.fLeft)), skvx::ceil(float2::Load(&r.fRight))};
+}
+
+bool GrAtlasPathRenderer::pathFitsInAtlas(const SkRect& pathDevBounds) const {
+    auto [topLeftFloor, botRightCeil] = round_out(pathDevBounds);
+    float2 size = botRightCeil - topLeftFloor;
+    return // Ensure the path's largest dimension fits in the atlas.
+           skvx::all(size <= fAtlasMaxSize) &&
+           // Since we will transpose tall skinny paths, limiting to kAtlasMaxPathHeight^2 pixels
+           // guarantees heightInAtlas <= kAtlasMaxPathHeight, while also allowing paths that are
+           // very wide and short.
+           size[0] * size[1] <= kAtlasMaxPathHeight * kAtlasMaxPathHeight;
+}
+
+void GrAtlasPathRenderer::AtlasPathKey::set(const SkMatrix& m, const SkPath& path) {
+    using grvx::float2;
+    fPathGenID = path.getGenerationID();
+    fAffineMatrix[0] = m.getScaleX();
+    fAffineMatrix[1] = m.getSkewX();
+    fAffineMatrix[2] = m.getSkewY();
+    fAffineMatrix[3] = m.getScaleY();
+    float2 translate = {m.getTranslateX(), m.getTranslateY()};
+    float2 subpixelPosition = translate - skvx::floor(translate);
+    float2 subpixelPositionKey = skvx::trunc(subpixelPosition *
+                                             GrTessellationShader::kLinearizationPrecision);
+    skvx::cast<uint8_t>(subpixelPositionKey).store(fSubpixelPositionKey);
+    fFillRule = (uint16_t)GrFillRuleForSkPath(path);  // Fill rule doesn't affect the path's genID.
+}
+
+bool GrAtlasPathRenderer::addPathToAtlas(GrRecordingContext* rContext,
+                                         const SkMatrix& viewMatrix,
+                                         const SkPath& path,
+                                         const SkRect& pathDevBounds,
+                                         SkIRect* devIBounds,
+                                         SkIPoint16* locationInAtlas,
+                                         bool* transposedInAtlas,
+                                         const DrawRefsAtlasCallback& drawRefsAtlasCallback) {
+    SkASSERT(!viewMatrix.hasPerspective());  // See onCanDrawPath().
+
+    pathDevBounds.roundOut(devIBounds);
+#ifdef SK_DEBUG
+    // is_visible() should have guaranteed the path's bounds were representable as ints, since clip
+    // bounds within the max render target size are nowhere near INT_MAX.
+    auto [topLeftFloor, botRightCeil] = round_out(pathDevBounds);
+    SkASSERT(skvx::all(skvx::cast<float>(int2::Load(&devIBounds->fLeft)) == topLeftFloor));
+    SkASSERT(skvx::all(skvx::cast<float>(int2::Load(&devIBounds->fRight)) == botRightCeil));
+#endif
+
+    int widthInAtlas = devIBounds->width();
+    int heightInAtlas = devIBounds->height();
+    // is_visible() should have guaranteed the path's bounds were non-empty.
+    SkASSERT(widthInAtlas > 0 && heightInAtlas > 0);
+
+    if (SkNextPow2(widthInAtlas) == SkNextPow2(heightInAtlas)) {
+        // Both dimensions go to the same pow2 band in the atlas. Use the larger dimension as height
+        // for more efficient packing.
+        *transposedInAtlas = widthInAtlas > heightInAtlas;
+    } else {
+        // Both dimensions go to different pow2 bands in the atlas. Use the smaller pow2 band for
+        // most efficient packing.
+        *transposedInAtlas = heightInAtlas > widthInAtlas;
+    }
+    if (*transposedInAtlas) {
+        std::swap(heightInAtlas, widthInAtlas);
+    }
+    SkASSERT(widthInAtlas <= (int)fAtlasMaxSize);
+    SkASSERT(heightInAtlas <= kAtlasMaxPathHeight);
+
+    // Check if this path is already in the atlas. This is mainly for clip paths.
+    AtlasPathKey atlasPathKey;
+    if (!path.isVolatile()) {
+        atlasPathKey.set(viewMatrix, path);
+        if (const SkIPoint16* existingLocation = fAtlasPathCache.find(atlasPathKey)) {
+            *locationInAtlas = *existingLocation;
+            return true;
+        }
+    }
+
+    if (fAtlasRenderTasks.empty() ||
+        !fAtlasRenderTasks.back()->addPath(viewMatrix, path, devIBounds->topLeft(), widthInAtlas,
+                                           heightInAtlas, *transposedInAtlas, locationInAtlas)) {
+        // We either don't have an atlas yet or the current one is full. Try to replace it.
+        GrAtlasRenderTask* currentAtlasTask = (!fAtlasRenderTasks.empty())
+                ? fAtlasRenderTasks.back().get() : nullptr;
+        if (currentAtlasTask &&
+            drawRefsAtlasCallback &&
+            drawRefsAtlasCallback(currentAtlasTask->atlasProxy())) {
+            // The draw already refs the current atlas. Give up. Otherwise the draw would ref two
+            // different atlases and they couldn't share a texture.
+            return false;
+        }
+        // Replace the atlas with a new one.
+        auto dynamicAtlas = std::make_unique<GrDynamicAtlas>(
+                kAtlasAlpha8Type, GrDynamicAtlas::InternalMultisample::kYes,
+                SkISize{fAtlasInitialSize, fAtlasInitialSize}, fAtlasMaxSize,
+                *rContext->priv().caps(), kAtlasAlgorithm);
+        auto newAtlasTask = sk_make_sp<GrAtlasRenderTask>(rContext,
+                                                          sk_make_sp<GrArenas>(),
+                                                          std::move(dynamicAtlas));
+        rContext->priv().drawingManager()->addAtlasTask(newAtlasTask, currentAtlasTask);
+        SkAssertResult(newAtlasTask->addPath(viewMatrix, path, devIBounds->topLeft(), widthInAtlas,
+                                             heightInAtlas, *transposedInAtlas, locationInAtlas));
+        fAtlasRenderTasks.push_back(std::move(newAtlasTask));
+        fAtlasPathCache.reset();
+    }
+
+    // Remember this path's location in the atlas, in case it gets drawn again.
+    if (!path.isVolatile()) {
+        fAtlasPathCache.set(atlasPathKey, *locationInAtlas);
+    }
+    return true;
+}
+
+// Returns whether the given proxyOwner uses the atlasProxy.
+template<typename T> bool refs_atlas(const T* proxyOwner, const GrSurfaceProxy* atlasProxy) {
+    bool refsAtlas = false;
+    auto checkForAtlasRef = [atlasProxy, &refsAtlas](GrSurfaceProxy* proxy, GrMipmapped) {
+        if (proxy == atlasProxy) {
+            refsAtlas = true;
+        }
+    };
+    if (proxyOwner) {
+        proxyOwner->visitProxies(checkForAtlasRef);
+    }
+    return refsAtlas;
+}
+
+GrPathRenderer::CanDrawPath GrAtlasPathRenderer::onCanDrawPath(const CanDrawPathArgs& args) const {
+#ifdef SK_DEBUG
+    if (!fAtlasRenderTasks.empty()) {
+        // args.fPaint should NEVER reference our current atlas. If it does, it means somebody
+        // intercepted a clip FP meant for a different op and will cause rendering artifacts.
+        const GrSurfaceProxy* atlasProxy = fAtlasRenderTasks.back()->atlasProxy();
+        SkASSERT(!refs_atlas(args.fPaint->getColorFragmentProcessor(), atlasProxy));
+        SkASSERT(!refs_atlas(args.fPaint->getCoverageFragmentProcessor(), atlasProxy));
+    }
+    SkASSERT(!args.fHasUserStencilSettings);  // See onGetStencilSupport().
+#endif
+    bool canDrawPath = args.fShape->style().isSimpleFill() &&
+                       // The MSAA requirement is a temporary limitation in order to preserve
+                       // functionality for refactoring. TODO: Allow kCoverage AA types.
+                       args.fAAType == GrAAType::kMSAA &&
+                       !args.fShape->style().hasPathEffect() &&
+                       !args.fViewMatrix->hasPerspective() &&
+                       this->pathFitsInAtlas(args.fViewMatrix->mapRect(args.fShape->bounds()));
+    return canDrawPath ? CanDrawPath::kYes : CanDrawPath::kNo;
+}
+
+static bool is_visible(const SkRect& pathDevBounds, const SkIRect& clipBounds) {
+    float2 pathTopLeft = float2::Load(&pathDevBounds.fLeft);
+    float2 pathBotRight = float2::Load(&pathDevBounds.fRight);
+    // Empty paths are never visible. Phrase this as a NOT of positive logic so we also return false
+    // in the case of NaN.
+    if (!skvx::all(pathTopLeft < pathBotRight)) {
+        return false;
+    }
+    float2 clipTopLeft = skvx::cast<float>(int2::Load(&clipBounds.fLeft));
+    float2 clipBotRight = skvx::cast<float>(int2::Load(&clipBounds.fRight));
+    static_assert(sizeof(clipBounds) == sizeof(clipTopLeft) + sizeof(clipBotRight));
+    return skvx::all(pathTopLeft < clipBotRight) && skvx::all(pathBotRight > clipTopLeft);
+}
+
+bool GrAtlasPathRenderer::onDrawPath(const DrawPathArgs& args) {
+    SkPath path;
+    args.fShape->asPath(&path);
+
+    const SkRect pathDevBounds = args.fViewMatrix->mapRect(args.fShape->bounds());
+    SkASSERT(this->pathFitsInAtlas(pathDevBounds));
+
+    if (!is_visible(pathDevBounds, args.fClip->getConservativeBounds())) {
+        // The path is empty or outside the clip. No mask is needed.
+        if (path.isInverseFillType()) {
+            args.fSurfaceDrawContext->drawPaint(args.fClip, std::move(args.fPaint),
+                                                *args.fViewMatrix);
+        }
+        return true;
+    }
+
+    SkIRect devIBounds;
+    SkIPoint16 locationInAtlas;
+    bool transposedInAtlas;
+    SkAssertResult(this->addPathToAtlas(args.fContext, *args.fViewMatrix, path, pathDevBounds,
+                                        &devIBounds, &locationInAtlas, &transposedInAtlas,
+                                        nullptr/*DrawRefsAtlasCallback -- see onCanDrawPath()*/));
+
+    const SkIRect& fillBounds = args.fShape->inverseFilled()
+            ? (args.fClip
+                    ? args.fClip->getConservativeBounds()
+                    : args.fSurfaceDrawContext->asSurfaceProxy()->backingStoreBoundsIRect())
+            : devIBounds;
+    const GrCaps& caps = *args.fSurfaceDrawContext->caps();
+    auto op = GrOp::Make<GrDrawAtlasPathOp>(args.fContext,
+                                            args.fSurfaceDrawContext->arenaAlloc(),
+                                            fillBounds, *args.fViewMatrix,
+                                            std::move(args.fPaint), locationInAtlas,
+                                            devIBounds, transposedInAtlas,
+                                            fAtlasRenderTasks.back()->readView(caps),
+                                            args.fShape->inverseFilled());
+    args.fSurfaceDrawContext->addDrawOp(args.fClip, std::move(op));
+    return true;
+}
+
+GrFPResult GrAtlasPathRenderer::makeAtlasClipEffect(GrRecordingContext* rContext,
+                                                    const GrOp* opBeingClipped,
+                                                    std::unique_ptr<GrFragmentProcessor> inputFP,
+                                                    const SkIRect& drawBounds,
+                                                    const SkMatrix& viewMatrix,
+                                                    const SkPath& path) {
+    if (viewMatrix.hasPerspective()) {
+        return GrFPFailure(std::move(inputFP));
+    }
+
+    const SkRect pathDevBounds = viewMatrix.mapRect(path.getBounds());
+    if (!is_visible(pathDevBounds, drawBounds)) {
+        // The path is empty or outside the drawBounds. No mask is needed.
+        return path.isInverseFillType() ? GrFPSuccess(std::move(inputFP))
+                                        : GrFPFailure(std::move(inputFP));
+    }
+
+    if (!this->pathFitsInAtlas(pathDevBounds)) {
+        // The path is too big.
+        return GrFPFailure(std::move(inputFP));
+    }
+
+    SkIRect devIBounds;
+    SkIPoint16 locationInAtlas;
+    bool transposedInAtlas;
+    // Called if the atlas runs out of room, to determine if it's safe to create a new one. (Draws
+    // can never access more than one atlas.)
+    auto drawRefsAtlasCallback = [opBeingClipped, &inputFP](const GrSurfaceProxy* atlasProxy) {
+        return refs_atlas(opBeingClipped, atlasProxy) ||
+               refs_atlas(inputFP.get(), atlasProxy);
+    };
+    // addPathToAtlas() ignores inverseness of the fill. See GrAtlasRenderTask::getAtlasUberPath().
+    if (!this->addPathToAtlas(rContext, viewMatrix, path, pathDevBounds, &devIBounds,
+                              &locationInAtlas, &transposedInAtlas, drawRefsAtlasCallback)) {
+        // The atlas ran out of room and we were unable to start a new one.
+        return GrFPFailure(std::move(inputFP));
+    }
+
+    SkMatrix atlasMatrix;
+    auto [atlasX, atlasY] = locationInAtlas;
+    if (!transposedInAtlas) {
+        atlasMatrix = SkMatrix::Translate(atlasX - devIBounds.left(), atlasY - devIBounds.top());
+    } else {
+        atlasMatrix.setAll(0, 1, atlasX - devIBounds.top(),
+                           1, 0, atlasY - devIBounds.left(),
+                           0, 0, 1);
+    }
+    auto flags = GrModulateAtlasCoverageEffect::Flags::kNone;
+    if (path.isInverseFillType()) {
+        flags |= GrModulateAtlasCoverageEffect::Flags::kInvertCoverage;
+    }
+    if (!devIBounds.contains(drawBounds)) {
+        flags |= GrModulateAtlasCoverageEffect::Flags::kCheckBounds;
+        // At this point in time we expect callers to tighten the scissor for "kIntersect" clips, as
+        // opposed to us having to check the path bounds. Feel free to remove this assert if that
+        // ever changes.
+        SkASSERT(path.isInverseFillType());
+    }
+    GrSurfaceProxyView atlasView = fAtlasRenderTasks.back()->readView(*rContext->priv().caps());
+    return GrFPSuccess(std::make_unique<GrModulateAtlasCoverageEffect>(flags, std::move(inputFP),
+                                                                       std::move(atlasView),
+                                                                       atlasMatrix, devIBounds));
+}
+
+#ifdef SK_DEBUG
+// Ensures the atlas dependencies are set up such that each atlas will be totally out of service
+// before we render the next one in line. This means there will only ever be one atlas active at a
+// time and that they can all share the same texture.
+static void validate_atlas_dependencies(const SkTArray<sk_sp<GrAtlasRenderTask>>& atlasTasks) {
+    for (int i = atlasTasks.count() - 1; i >= 1; --i) {
+        GrAtlasRenderTask* atlasTask = atlasTasks[i].get();
+        GrAtlasRenderTask* previousAtlasTask = atlasTasks[i - 1].get();
+        // Double check that atlasTask depends on every dependent of its previous atlas. If this
+        // fires it might mean previousAtlasTask gained a new dependent after atlasTask came into
+        // service (maybe by an op that hadn't yet been added to an opsTask when we registered the
+        // new atlas with the drawingManager).
+        for (GrRenderTask* previousAtlasUser : previousAtlasTask->dependents()) {
+            SkASSERT(atlasTask->dependsOn(previousAtlasUser));
+        }
+    }
+}
+#endif
+
+void GrAtlasPathRenderer::preFlush(GrOnFlushResourceProvider* onFlushRP,
+                                          SkSpan<const uint32_t> /* taskIDs */) {
+    if (fAtlasRenderTasks.empty()) {
+        SkASSERT(fAtlasPathCache.count() == 0);
+        return;
+    }
+
+    // Verify the atlases can all share the same texture.
+    SkDEBUGCODE(validate_atlas_dependencies(fAtlasRenderTasks);)
+
+    // Instantiate the first atlas.
+    fAtlasRenderTasks[0]->instantiate(onFlushRP);
+
+    // Instantiate the remaining atlases.
+    GrTexture* firstAtlasTexture = fAtlasRenderTasks[0]->atlasProxy()->peekTexture();
+    SkASSERT(firstAtlasTexture);
+    for (int i = 1; i < fAtlasRenderTasks.count(); ++i) {
+        GrAtlasRenderTask* atlasTask = fAtlasRenderTasks[i].get();
+        if (atlasTask->atlasProxy()->backingStoreDimensions() == firstAtlasTexture->dimensions()) {
+            atlasTask->instantiate(onFlushRP, sk_ref_sp(firstAtlasTexture));
+        } else {
+            // The atlases are expected to all be full size except possibly the final one.
+            SkASSERT(i == fAtlasRenderTasks.count() - 1);
+            SkASSERT(atlasTask->atlasProxy()->backingStoreDimensions().area() <
+                     firstAtlasTexture->dimensions().area());
+            // TODO: Recycle the larger atlas texture anyway?
+            atlasTask->instantiate(onFlushRP);
+        }
+    }
+
+    // Reset all atlas data.
+    fAtlasRenderTasks.reset();
+    fAtlasPathCache.reset();
+}
diff --git a/src/gpu/ops/GrAtlasPathRenderer.h b/src/gpu/ops/GrAtlasPathRenderer.h
new file mode 100644
index 0000000..55eb824
--- /dev/null
+++ b/src/gpu/ops/GrAtlasPathRenderer.h
@@ -0,0 +1,108 @@
+/*
+ * Copyright 2019 Google LLC.
+ *
+ * Use of this source code is governed by a BSD-style license that can be
+ * found in the LICENSE file.
+ */
+
+#ifndef GrAtlasPathRenderer_DEFINED
+#define GrAtlasPathRenderer_DEFINED
+
+#include "include/private/SkTHash.h"
+#include "src/core/SkIPoint16.h"
+#include "src/gpu/GrDynamicAtlas.h"
+#include "src/gpu/GrFragmentProcessor.h"
+#include "src/gpu/GrOnFlushResourceProvider.h"
+#include "src/gpu/GrPathRenderer.h"
+
+class GrAtlasRenderTask;
+class GrOp;
+class GrRecordingContext;
+
+// Draws paths by first rendering their coverage mask into an offscreen atlas.
+class GrAtlasPathRenderer : public GrPathRenderer, public GrOnFlushCallbackObject {
+public:
+    static bool IsSupported(GrRecordingContext*);
+
+    // Returns a GrAtlasPathRenderer if it is supported, otherwise null.
+    static sk_sp<GrAtlasPathRenderer> Make(GrRecordingContext* rContext);
+
+    const char* name() const final { return "GrAtlasPathRenderer"; }
+
+    StencilSupport onGetStencilSupport(const GrStyledShape&) const override {
+        return kNoSupport_StencilSupport;
+    }
+
+    CanDrawPath onCanDrawPath(const CanDrawPathArgs&) const override;
+
+    bool onDrawPath(const DrawPathArgs&) override;
+
+    // Returns a fragment processor that modulates inputFP by the given deviceSpacePath's coverage,
+    // implemented using an internal atlas.
+    //
+    // Returns 'inputFP' wrapped in GrFPFailure() if the path was too large, or if the current atlas
+    // is full and already used by either opBeingClipped or inputFP. (Currently, "too large" means
+    // more than 128*128 total pixels, or larger than the atlas size in either dimension.)
+    //
+    // Also returns GrFPFailure() if the view matrix has perspective.
+    GrFPResult makeAtlasClipEffect(GrRecordingContext*,
+                                   const GrOp* opBeingClipped,
+                                   std::unique_ptr<GrFragmentProcessor> inputFP,
+                                   const SkIRect& drawBounds,
+                                   const SkMatrix&,
+                                   const SkPath&);
+
+private:
+    // The atlas is not compatible with DDL. We can only use it on direct contexts.
+    GrAtlasPathRenderer(GrDirectContext*);
+
+    // Returns true if the given device-space path bounds are no larger than 128*128 total pixels
+    // and no larger than the max atlas size in either dimension.
+    bool pathFitsInAtlas(const SkRect& pathDevBounds) const;
+
+    // Returns true if the draw being set up already uses the given atlasProxy.
+    using DrawRefsAtlasCallback = std::function<bool(const GrSurfaceProxy* atlasProxy)>;
+
+    // Adds the filled path to an atlas.
+    //
+    // pathFitsInAtlas() and is_visible() both must have returned true before making this call.
+    //
+    // Fails and returns false if the current atlas is full and already in use according to
+    // DrawRefsAtlasCallback.
+    bool addPathToAtlas(GrRecordingContext*,
+                        const SkMatrix&,
+                        const SkPath&,
+                        const SkRect& pathDevBounds,
+                        SkIRect* devIBounds,
+                        SkIPoint16* locationInAtlas,
+                        bool* transposedInAtlas,
+                        const DrawRefsAtlasCallback&);
+
+    // Instantiates texture(s) for all atlases we've created since the last flush. Atlases that are
+    // the same size will be instantiated with the same backing texture.
+    void preFlush(GrOnFlushResourceProvider*, SkSpan<const uint32_t> taskIDs) override;
+
+    float fAtlasMaxSize = 0;
+    int fAtlasInitialSize = 0;
+
+    // A collection of all atlases we've created and used since the last flush. We instantiate these
+    // at flush time during preFlush().
+    SkSTArray<4, sk_sp<GrAtlasRenderTask>> fAtlasRenderTasks;
+
+    // This simple cache remembers the locations of cacheable path masks in the most recent atlas.
+    // Its main motivation is for clip paths.
+    struct AtlasPathKey {
+        void set(const SkMatrix&, const SkPath&);
+        bool operator==(const AtlasPathKey& k) const {
+            static_assert(sizeof(*this) == sizeof(uint32_t) * 6);
+            return !memcmp(this, &k, sizeof(*this));
+        }
+        uint32_t fPathGenID;
+        float fAffineMatrix[4];
+        uint8_t fSubpixelPositionKey[2];
+        uint16_t fFillRule;
+    };
+    SkTHashMap<AtlasPathKey, SkIPoint16> fAtlasPathCache;
+};
+
+#endif
diff --git a/src/gpu/tessellate/GrDrawAtlasPathOp.cpp b/src/gpu/ops/GrDrawAtlasPathOp.cpp
similarity index 99%
rename from src/gpu/tessellate/GrDrawAtlasPathOp.cpp
rename to src/gpu/ops/GrDrawAtlasPathOp.cpp
index 8b7f0e0..a05999c 100644
--- a/src/gpu/tessellate/GrDrawAtlasPathOp.cpp
+++ b/src/gpu/ops/GrDrawAtlasPathOp.cpp
@@ -5,7 +5,7 @@
  * found in the LICENSE file.
  */
 
-#include "src/gpu/tessellate/GrDrawAtlasPathOp.h"
+#include "src/gpu/ops/GrDrawAtlasPathOp.h"
 
 #include "src/gpu/GrOpFlushState.h"
 #include "src/gpu/GrOpsRenderPass.h"
diff --git a/src/gpu/tessellate/GrDrawAtlasPathOp.h b/src/gpu/ops/GrDrawAtlasPathOp.h
similarity index 98%
rename from src/gpu/tessellate/GrDrawAtlasPathOp.h
rename to src/gpu/ops/GrDrawAtlasPathOp.h
index 4ec6872..1e79ec9 100644
--- a/src/gpu/tessellate/GrDrawAtlasPathOp.h
+++ b/src/gpu/ops/GrDrawAtlasPathOp.h
@@ -9,8 +9,8 @@
 #define GrDrawAtlasPathOp_DEFINED
 
 #include "src/core/SkIPoint16.h"
+#include "src/gpu/ops/GrAtlasInstancedHelper.h"
 #include "src/gpu/ops/GrDrawOp.h"
-#include "src/gpu/tessellate/GrAtlasInstancedHelper.h"
 
 // Fills a rectangle of pixels with a clip against coverage values from an atlas.
 class GrDrawAtlasPathOp : public GrDrawOp {
diff --git a/src/gpu/tessellate/GrTessellationPathRenderer.cpp b/src/gpu/tessellate/GrTessellationPathRenderer.cpp
index 9df42a9..23ea140 100644
--- a/src/gpu/tessellate/GrTessellationPathRenderer.cpp
+++ b/src/gpu/tessellate/GrTessellationPathRenderer.cpp
@@ -8,7 +8,6 @@
 #include "src/gpu/tessellate/GrTessellationPathRenderer.h"
 
 #include "include/private/SkVx.h"
-#include "src/core/SkIPoint16.h"
 #include "src/core/SkPathPriv.h"
 #include "src/gpu/GrClip.h"
 #include "src/gpu/GrMemoryPool.h"
@@ -17,24 +16,10 @@
 #include "src/gpu/GrVx.h"
 #include "src/gpu/effects/GrDisableColorXP.h"
 #include "src/gpu/geometry/GrStyledShape.h"
-#include "src/gpu/tessellate/GrAtlasRenderTask.h"
-#include "src/gpu/tessellate/GrDrawAtlasPathOp.h"
 #include "src/gpu/tessellate/GrPathInnerTriangulateOp.h"
 #include "src/gpu/tessellate/GrPathStencilCoverOp.h"
 #include "src/gpu/tessellate/GrPathTessellateOp.h"
 #include "src/gpu/tessellate/GrStrokeTessellateOp.h"
-#include "src/gpu/tessellate/shaders/GrModulateAtlasCoverageFP.h"
-
-constexpr static auto kAtlasAlpha8Type = GrColorType::kAlpha_8;
-constexpr static int kAtlasInitialSize = 512;
-
-// The atlas is only used for small-area paths, which means at least one dimension of every path is
-// guaranteed to be quite small. So if we transpose tall paths, then every path will have a small
-// height, which lends very well to efficient pow2 atlas packing.
-constexpr static auto kAtlasAlgorithm = GrDynamicAtlas::RectanizerAlgorithm::kPow2;
-
-// Ensure every path in the atlas falls in or below the 128px high rectanizer band.
-constexpr static int kAtlasMaxPathHeight = 128;
 
 bool GrTessellationPathRenderer::IsSupported(const GrCaps& caps) {
     return !caps.avoidStencilBuffers() &&
@@ -42,21 +27,6 @@
            !caps.disableTessellationPathRenderer();
 }
 
-GrTessellationPathRenderer::GrTessellationPathRenderer(GrRecordingContext* rContext) {
-    const GrCaps& caps = *rContext->priv().caps();
-    auto atlasFormat = caps.getDefaultBackendFormat(kAtlasAlpha8Type, GrRenderable::kYes);
-    if (rContext->asDirectContext() &&  // The atlas doesn't support DDL yet.
-        caps.internalMultisampleCount(atlasFormat) > 1) {
-#if GR_TEST_UTILS
-        fAtlasMaxSize = rContext->priv().options().fMaxTextureAtlasSize;
-#else
-        fAtlasMaxSize = 2048;
-#endif
-        fAtlasMaxSize = SkPrevPow2(std::min(fAtlasMaxSize, caps.maxPreferredRenderTargetSize()));
-        fAtlasInitialSize = SkNextPow2(std::min(kAtlasInitialSize, fAtlasMaxSize));
-    }
-}
-
 GrPathRenderer::StencilSupport GrTessellationPathRenderer::onGetStencilSupport(
         const GrStyledShape& shape) const {
     if (!shape.style().isSimpleFill() || shape.inverseFilled()) {
@@ -136,6 +106,7 @@
         return true;
     }
 
+    // Handle empty paths.
     const SkRect pathDevBounds = args.fViewMatrix->mapRect(args.fShape->bounds());
     if (pathDevBounds.isEmpty()) {
         if (path.isInverseFillType()) {
@@ -145,46 +116,7 @@
         return true;
     }
 
-    if (args.fUserStencilSettings->isUnused() && args.fAAType == GrAAType::kMSAA) {
-        // See if the path is small and simple enough to atlas instead of drawing directly.
-        //
-        // NOTE: The atlas uses alpha8 coverage even for msaa render targets. We could theoretically
-        // render the sample mask to an integer texture, but such a scheme would probably require
-        // GL_EXT_post_depth_coverage, which appears to have low adoption.
-        SkIRect devIBounds;
-        SkIPoint16 locationInAtlas;
-        bool transposedInAtlas;
-        auto visitProxiesUsedByDraw = [&args](GrVisitProxyFunc visitor) {
-            if (args.fPaint.hasColorFragmentProcessor()) {
-                args.fPaint.getColorFragmentProcessor()->visitProxies(visitor);
-            }
-            if (args.fPaint.hasCoverageFragmentProcessor()) {
-                args.fPaint.getCoverageFragmentProcessor()->visitProxies(visitor);
-            }
-        };
-        if (this->tryAddPathToAtlas(args.fContext, *args.fViewMatrix, path, pathDevBounds,
-                                    &devIBounds, &locationInAtlas, &transposedInAtlas,
-                                    visitProxiesUsedByDraw)) {
-            const GrCaps& caps = *args.fSurfaceDrawContext->caps();
-            const SkIRect& fillBounds = path.isInverseFillType()
-                    ? (args.fClip
-                            ? args.fClip->getConservativeBounds()
-                            : args.fSurfaceDrawContext->asSurfaceProxy()->backingStoreBoundsIRect())
-                    : devIBounds;
-            auto op = GrOp::Make<GrDrawAtlasPathOp>(args.fContext,
-                                                    args.fSurfaceDrawContext->arenaAlloc(),
-                                                    fillBounds, *args.fViewMatrix,
-                                                    std::move(args.fPaint), locationInAtlas,
-                                                    devIBounds, transposedInAtlas,
-                                                    fAtlasRenderTasks.back()->readView(caps),
-                                                    path.isInverseFillType());
-            surfaceDrawContext->addDrawOp(args.fClip, std::move(op));
-            return true;
-        }
-    }
-
-    // Handle convex paths only if we couldn't fit them in the atlas. We give the atlas priority in
-    // an effort to reduce DMSAA triggers.
+    // Handle convex paths.
     if (args.fShape->knownToBeConvex() && !path.isInverseFillType()) {
         auto op = GrOp::Make<GrPathTessellateOp>(args.fContext, *args.fViewMatrix, path,
                                                  std::move(args.fPaint), args.fAAType,
@@ -239,228 +171,3 @@
                                       *args.fViewMatrix, path, GrPaint());
     surfaceDrawContext->addDrawOp(args.fClip, std::move(op));
 }
-
-GrFPResult GrTessellationPathRenderer::makeAtlasClipFP(GrRecordingContext* rContext,
-                                                       const GrOp* opBeingClipped,
-                                                       std::unique_ptr<GrFragmentProcessor> inputFP,
-                                                       const SkIRect& drawBounds,
-                                                       const SkMatrix& viewMatrix,
-                                                       const SkPath& path) {
-    if (viewMatrix.hasPerspective()) {
-        return GrFPFailure(std::move(inputFP));
-    }
-    const SkRect pathDevBounds = viewMatrix.mapRect(path.getBounds());
-    if (pathDevBounds.isEmpty()) {
-        return path.isInverseFillType() ? GrFPSuccess(std::move(inputFP))
-                                        : GrFPFailure(std::move(inputFP));
-    }
-    SkIRect devIBounds;
-    SkIPoint16 locationInAtlas;
-    bool transposedInAtlas;
-    auto visitProxiesUsedByDraw = [&opBeingClipped, &inputFP](GrVisitProxyFunc visitor) {
-        opBeingClipped->visitProxies(visitor);
-        if (inputFP) {
-            inputFP->visitProxies(visitor);
-        }
-    };
-    // tryAddPathToAtlas() ignores inverseness of the fill. See getAtlasUberPath().
-    if (!this->tryAddPathToAtlas(rContext, viewMatrix, path, pathDevBounds, &devIBounds,
-                                 &locationInAtlas, &transposedInAtlas, visitProxiesUsedByDraw)) {
-        // The path is too big, or the atlas ran out of room.
-        return GrFPFailure(std::move(inputFP));
-    }
-    SkMatrix atlasMatrix;
-    auto [atlasX, atlasY] = locationInAtlas;
-    if (!transposedInAtlas) {
-        atlasMatrix = SkMatrix::Translate(atlasX - devIBounds.left(), atlasY - devIBounds.top());
-    } else {
-        atlasMatrix.setAll(0, 1, atlasX - devIBounds.top(),
-                           1, 0, atlasY - devIBounds.left(),
-                           0, 0, 1);
-    }
-    auto flags = GrModulateAtlasCoverageFP::Flags::kNone;
-    if (path.isInverseFillType()) {
-        flags |= GrModulateAtlasCoverageFP::Flags::kInvertCoverage;
-    }
-    if (!devIBounds.contains(drawBounds)) {
-        flags |= GrModulateAtlasCoverageFP::Flags::kCheckBounds;
-        // At this point in time we expect callers to tighten the scissor for "kIntersect" clips, as
-        // opposed to us having to check the path bounds. Feel free to remove this assert if that
-        // ever changes.
-        SkASSERT(path.isInverseFillType());
-    }
-    GrSurfaceProxyView atlasView = fAtlasRenderTasks.back()->readView(*rContext->priv().caps());
-    return GrFPSuccess(std::make_unique<GrModulateAtlasCoverageFP>(flags, std::move(inputFP),
-                                                                   std::move(atlasView),
-                                                                   atlasMatrix, devIBounds));
-}
-
-void GrTessellationPathRenderer::AtlasPathKey::set(const SkMatrix& m, const SkPath& path) {
-    using grvx::float2;
-    fPathGenID = path.getGenerationID();
-    fAffineMatrix[0] = m.getScaleX();
-    fAffineMatrix[1] = m.getSkewX();
-    fAffineMatrix[2] = m.getSkewY();
-    fAffineMatrix[3] = m.getScaleY();
-    float2 translate = {m.getTranslateX(), m.getTranslateY()};
-    float2 subpixelPosition = translate - skvx::floor(translate);
-    float2 subpixelPositionKey = skvx::trunc(subpixelPosition *
-                                             GrTessellationShader::kLinearizationPrecision);
-    skvx::cast<uint8_t>(subpixelPositionKey).store(fSubpixelPositionKey);
-    fFillRule = (uint16_t)GrFillRuleForSkPath(path);  // Fill rule doesn't affect the path's genID.
-}
-
-bool GrTessellationPathRenderer::tryAddPathToAtlas(GrRecordingContext* rContext,
-                                                   const SkMatrix& viewMatrix, const SkPath& path,
-                                                   const SkRect& pathDevBounds,
-                                                   SkIRect* devIBounds, SkIPoint16* locationInAtlas,
-                                                   bool* transposedInAtlas,
-                                                   const VisitProxiesFn& visitProxiesUsedByDraw) {
-    SkASSERT(!viewMatrix.hasPerspective());  // See onCanDrawPath().
-
-    // Write as the NOT of positive logic, so we will return false if any values are NaN.
-    if (!(pathDevBounds.width() > 0 && pathDevBounds.width() <= fAtlasMaxSize) ||
-        !(pathDevBounds.height() > 0 && pathDevBounds.height() <= fAtlasMaxSize)) {
-        return false;
-    }
-
-    // The atlas is not compatible with DDL. We should only be using it on direct contexts.
-    SkASSERT(rContext->asDirectContext());
-
-    pathDevBounds.roundOut(devIBounds);
-    int widthInAtlas = devIBounds->width();
-    int heightInAtlas = devIBounds->height();
-    if (widthInAtlas <= 0 || heightInAtlas <= 0) {
-        return false;
-    }
-
-    if (SkNextPow2(widthInAtlas) == SkNextPow2(heightInAtlas)) {
-        // Both dimensions go to the same pow2 band in the atlas. Use the larger dimension as height
-        // for more efficient packing.
-        *transposedInAtlas = widthInAtlas > heightInAtlas;
-    } else {
-        // Both dimensions go to different pow2 bands in the atlas. Use the smaller pow2 band for
-        // most efficient packing.
-        *transposedInAtlas = heightInAtlas > widthInAtlas;
-    }
-    if (*transposedInAtlas) {
-        std::swap(heightInAtlas, widthInAtlas);
-    }
-
-    // Check if the path is too large for an atlas. Since we transpose tall skinny paths, limiting
-    // to kAtlasMaxPathHeight^2 pixels guarantees heightInAtlas <= kAtlasMaxPathHeight, while also
-    // allowing paths that are very wide and short.
-    if ((uint64_t)widthInAtlas * heightInAtlas > kAtlasMaxPathHeight * kAtlasMaxPathHeight ||
-        widthInAtlas > fAtlasMaxSize) {
-        return false;
-    }
-    SkASSERT(heightInAtlas <= kAtlasMaxPathHeight);
-
-    // Check if this path is already in the atlas. This is mainly for clip paths.
-    AtlasPathKey atlasPathKey;
-    if (!path.isVolatile()) {
-        atlasPathKey.set(viewMatrix, path);
-        if (const SkIPoint16* existingLocation = fAtlasPathCache.find(atlasPathKey)) {
-            *locationInAtlas = *existingLocation;
-            return true;
-        }
-    }
-
-    if (fAtlasRenderTasks.empty() ||
-        !fAtlasRenderTasks.back()->addPath(viewMatrix, path, devIBounds->topLeft(), widthInAtlas,
-                                           heightInAtlas, *transposedInAtlas, locationInAtlas)) {
-        // We either don't have an atlas yet or the current one is full. Try to replace it.
-        GrAtlasRenderTask* currentAtlasTask = (!fAtlasRenderTasks.empty())
-                ? fAtlasRenderTasks.back().get() : nullptr;
-        if (currentAtlasTask) {
-            // Don't allow the current atlas to be replaced if the draw already uses it. Otherwise
-            // the draw would use two different atlases, which breaks our guarantee that there will
-            // only ever be one atlas active at a time.
-            const GrSurfaceProxy* currentAtlasProxy = currentAtlasTask->atlasProxy();
-            bool drawUsesCurrentAtlas = false;
-            visitProxiesUsedByDraw([currentAtlasProxy, &drawUsesCurrentAtlas](GrSurfaceProxy* proxy,
-                                                                              GrMipmapped) {
-                if (proxy == currentAtlasProxy) {
-                    drawUsesCurrentAtlas = true;
-                }
-            });
-            if (drawUsesCurrentAtlas) {
-                // The draw already uses the current atlas. Give up.
-                return false;
-            }
-        }
-        // Replace the atlas with a new one.
-        auto dynamicAtlas = std::make_unique<GrDynamicAtlas>(
-                kAtlasAlpha8Type, GrDynamicAtlas::InternalMultisample::kYes,
-                SkISize{fAtlasInitialSize, fAtlasInitialSize}, fAtlasMaxSize,
-                *rContext->priv().caps(), kAtlasAlgorithm);
-        auto newAtlasTask = sk_make_sp<GrAtlasRenderTask>(rContext,
-                                                          sk_make_sp<GrArenas>(),
-                                                          std::move(dynamicAtlas));
-        rContext->priv().drawingManager()->addAtlasTask(newAtlasTask, currentAtlasTask);
-        SkAssertResult(newAtlasTask->addPath(viewMatrix, path, devIBounds->topLeft(), widthInAtlas,
-                                             heightInAtlas, *transposedInAtlas, locationInAtlas));
-        fAtlasRenderTasks.push_back(std::move(newAtlasTask));
-        fAtlasPathCache.reset();
-    }
-
-    // Remember this path's location in the atlas, in case it gets drawn again.
-    if (!path.isVolatile()) {
-        fAtlasPathCache.set(atlasPathKey, *locationInAtlas);
-    }
-    return true;
-}
-
-#ifdef SK_DEBUG
-// Ensures the atlas dependencies are set up such that each atlas will be totally out of service
-// before we render the next one in line. This means there will only ever be one atlas active at a
-// time and that they can all share the same texture.
-void validate_atlas_dependencies(const SkTArray<sk_sp<GrAtlasRenderTask>>& atlasTasks) {
-    for (int i = atlasTasks.count() - 1; i >= 1; --i) {
-        GrAtlasRenderTask* atlasTask = atlasTasks[i].get();
-        GrAtlasRenderTask* previousAtlasTask = atlasTasks[i - 1].get();
-        // Double check that atlasTask depends on every dependent of its previous atlas. If this
-        // fires it might mean previousAtlasTask gained a new dependent after atlasTask came into
-        // service (maybe by an op that hadn't yet been added to an opsTask when we registered the
-        // new atlas with the drawingManager).
-        for (GrRenderTask* previousAtlasUser : previousAtlasTask->dependents()) {
-            SkASSERT(atlasTask->dependsOn(previousAtlasUser));
-        }
-    }
-}
-#endif
-
-void GrTessellationPathRenderer::preFlush(GrOnFlushResourceProvider* onFlushRP,
-                                          SkSpan<const uint32_t> /* taskIDs */) {
-    if (fAtlasRenderTasks.empty()) {
-        SkASSERT(fAtlasPathCache.count() == 0);
-        return;
-    }
-
-    // Verify the atlases can all share the same texture.
-    SkDEBUGCODE(validate_atlas_dependencies(fAtlasRenderTasks);)
-
-    // Instantiate the first atlas.
-    fAtlasRenderTasks[0]->instantiate(onFlushRP);
-
-    // Instantiate the remaining atlases.
-    GrTexture* firstAtlasTexture = fAtlasRenderTasks[0]->atlasProxy()->peekTexture();
-    SkASSERT(firstAtlasTexture);
-    for (int i = 1; i < fAtlasRenderTasks.count(); ++i) {
-        GrAtlasRenderTask* atlasTask = fAtlasRenderTasks[i].get();
-        if (atlasTask->atlasProxy()->backingStoreDimensions() == firstAtlasTexture->dimensions()) {
-            atlasTask->instantiate(onFlushRP, sk_ref_sp(firstAtlasTexture));
-        } else {
-            // The atlases are expected to all be full size except possibly the final one.
-            SkASSERT(i == fAtlasRenderTasks.count() - 1);
-            SkASSERT(atlasTask->atlasProxy()->backingStoreDimensions().area() <
-                     firstAtlasTexture->dimensions().area());
-            // TODO: Recycle the larger atlas texture anyway?
-            atlasTask->instantiate(onFlushRP);
-        }
-    }
-
-    // Reset all atlas data.
-    fAtlasRenderTasks.reset();
-    fAtlasPathCache.reset();
-}
diff --git a/src/gpu/tessellate/GrTessellationPathRenderer.h b/src/gpu/tessellate/GrTessellationPathRenderer.h
index e5bf415..e007632 100644
--- a/src/gpu/tessellate/GrTessellationPathRenderer.h
+++ b/src/gpu/tessellate/GrTessellationPathRenderer.h
@@ -12,22 +12,13 @@
 
 #if SK_GPU_V1
 
-#include "include/private/SkTHash.h"
-#include "src/core/SkIPoint16.h"
-#include "src/gpu/GrDynamicAtlas.h"
-#include "src/gpu/GrFragmentProcessor.h"
-#include "src/gpu/GrOnFlushResourceProvider.h"
 #include "src/gpu/GrPathRenderer.h"
 
-class GrAtlasRenderTask;
-class GrOp;
-class GrRecordingContext;
-
 // This is the tie-in point for path rendering via GrPathTessellateOp. This path renderer draws
 // paths using a hybrid Red Book "stencil, then cover" method. Curves get linearized by GPU
 // tessellation shaders. This path renderer doesn't apply analytic AA, so it requires MSAA if AA is
 // desired.
-class GrTessellationPathRenderer : public GrPathRenderer, public GrOnFlushCallbackObject {
+class GrTessellationPathRenderer : public GrPathRenderer {
 public:
     // We send these flags to the internal path filling Ops to control how a path gets rendered.
     enum class PathFlags {
@@ -38,63 +29,11 @@
 
     static bool IsSupported(const GrCaps&);
 
-    GrTessellationPathRenderer(GrRecordingContext*);
     const char* name() const final { return "GrTessellationPathRenderer"; }
-
     StencilSupport onGetStencilSupport(const GrStyledShape&) const override;
     CanDrawPath onCanDrawPath(const CanDrawPathArgs&) const override;
-
     bool onDrawPath(const DrawPathArgs&) override;
     void onStencilPath(const StencilPathArgs&) override;
-
-    // Returns an antialiased fragment processor that modulates inputFP by the given
-    // deviceSpacePath's coverage, implemented using an internal atlas. Non-aa is not supported.
-    //
-    // Returns 'inputFP' wrapped in GrFPFailure() if the path was too large, or if the current atlas
-    // is full and already used by either opBeingClipped or inputFP. (Currently, "too large" means
-    // more than 128*128 total pixels, or larger than the atlas size in either dimension.)
-    //
-    // Also returns GrFPFailure() if the view matrix has perspective.
-    GrFPResult makeAtlasClipFP(GrRecordingContext*, const GrOp* opBeingClipped,
-                               std::unique_ptr<GrFragmentProcessor> inputFP,
-                               const SkIRect& drawBounds, const SkMatrix&, const SkPath&);
-
-    void preFlush(GrOnFlushResourceProvider*, SkSpan<const uint32_t> taskIDs) override;
-
-private:
-    using VisitProxiesFn = std::function<void(const GrVisitProxyFunc&)>;
-
-    // Adds the filled path to an atlas.
-    //
-    // Fails and returns false if the path is too large, or if the current atlas is full and already
-    // in use according to 'visitProxiesUsedByDraw'. (Currently, "too large" means more than 128*128
-    // total pixels, or larger than the atlas size in either dimension.)
-    bool tryAddPathToAtlas(GrRecordingContext*, const SkMatrix&, const SkPath&,
-                           const SkRect& pathDevBounds, SkIRect* devIBounds,
-                           SkIPoint16* locationInAtlas, bool* transposedInAtlas,
-                           const VisitProxiesFn& visitProxiesUsedByDraw);
-
-    int fAtlasMaxSize = 0;
-    int fAtlasInitialSize = 0;
-
-    // A collection of all atlases we've created and used since the last flush. We instantiate these
-    // at flush time during preFlush().
-    SkSTArray<4, sk_sp<GrAtlasRenderTask>> fAtlasRenderTasks;
-
-    // This simple cache remembers the locations of cacheable path masks in the most recent atlas.
-    // Its main motivation is for clip paths.
-    struct AtlasPathKey {
-        void set(const SkMatrix&, const SkPath&);
-        bool operator==(const AtlasPathKey& k) const {
-            static_assert(sizeof(*this) == sizeof(uint32_t) * 6);
-            return !memcmp(this, &k, sizeof(*this));
-        }
-        uint32_t fPathGenID;
-        float fAffineMatrix[4];
-        uint8_t fSubpixelPositionKey[2];
-        uint16_t fFillRule;
-    };
-    SkTHashMap<AtlasPathKey, SkIPoint16> fAtlasPathCache;
 };
 
 GR_MAKE_BITFIELD_CLASS_OPS(GrTessellationPathRenderer::PathFlags)
diff --git a/tools/flags/CommonFlagsGpu.cpp b/tools/flags/CommonFlagsGpu.cpp
index d6b4b7e..56b4f8b 100644
--- a/tools/flags/CommonFlagsGpu.cpp
+++ b/tools/flags/CommonFlagsGpu.cpp
@@ -31,7 +31,7 @@
 static DEFINE_string(pr, "",
               "Set of enabled gpu path renderers. Defined as a list of: "
               "[~]none [~]dashline [~]aahairline [~]aaconvex [~]aalinearizing [~]small [~]tri "
-              "[~]tess [~]all");
+              "[~]atlas [~]tess [~]all");
 
 static DEFINE_int(internalSamples, -1,
         "Number of samples for internal draws that use MSAA, or default value if negative.");
@@ -66,6 +66,8 @@
         return GpuPathRenderers::kSmall;
     } else if (!strcmp(name, "tri")) {
         return GpuPathRenderers::kTriangulating;
+    } else if (!strcmp(name, "atlas")) {
+        return GpuPathRenderers::kAtlas;
     } else if (!strcmp(name, "tess")) {
         return GpuPathRenderers::kTessellation;
     } else if (!strcmp(name, "default")) {
diff --git a/tools/viewer/Viewer.cpp b/tools/viewer/Viewer.cpp
index 51c9e5c..8d7a7be 100644
--- a/tools/viewer/Viewer.cpp
+++ b/tools/viewer/Viewer.cpp
@@ -31,6 +31,7 @@
 #include "src/gpu/GrGpu.h"
 #include "src/gpu/GrPersistentCacheUtils.h"
 #include "src/gpu/GrShaderUtils.h"
+#include "src/gpu/ops/GrAtlasPathRenderer.h"
 #include "src/gpu/tessellate/GrTessellationPathRenderer.h"
 #include "src/image/SkImage_Base.h"
 #include "src/sksl/SkSLCompiler.h"
@@ -341,6 +342,7 @@
     SkGraphics::Init();
 
     gPathRendererNames[GpuPathRenderers::kDefault] = "Default Path Renderers";
+    gPathRendererNames[GpuPathRenderers::kAtlas] = "Atlas (tessellation)";
     gPathRendererNames[GpuPathRenderers::kTessellation] = "Tessellation";
     gPathRendererNames[GpuPathRenderers::kSmall] = "Small paths (cached sdf or alpha masks)";
     gPathRendererNames[GpuPathRenderers::kTriangulating] = "Triangulating";
@@ -1938,6 +1940,9 @@
                         const auto* caps = ctx->priv().caps();
                         prButton(GpuPathRenderers::kDefault);
                         if (fWindow->sampleCount() > 1 || FLAGS_dmsaa) {
+                            if (GrAtlasPathRenderer::IsSupported(ctx)) {
+                                prButton(GpuPathRenderers::kAtlas);
+                            }
                             if (GrTessellationPathRenderer::IsSupported(*caps)) {
                                 prButton(GpuPathRenderers::kTessellation);
                             }
@@ -2770,6 +2775,10 @@
                 const auto* caps = ctx->priv().caps();
                 writer.appendString(gPathRendererNames[GpuPathRenderers::kDefault].c_str());
                 if (fWindow->sampleCount() > 1 || FLAGS_dmsaa) {
+                    if (GrAtlasPathRenderer::IsSupported(ctx)) {
+                        writer.appendString(
+                                gPathRendererNames[GpuPathRenderers::kAtlas].c_str());
+                    }
                     if (GrTessellationPathRenderer::IsSupported(*caps)) {
                         writer.appendString(
                                 gPathRendererNames[GpuPathRenderers::kTessellation].c_str());