Revise Text & Small Path Atlas so instantiation failure is handled at flush time

This paves the way to having the AtlasTextOps not need the RestrictedAtlasManager at op creation time.

Change-Id: I1028faba730d50d3d3349a4c0809465d036ed611
Reviewed-on: https://skia-review.googlesource.com/111807
Reviewed-by: Jim Van Verth <jvanverth@google.com>
Commit-Queue: Robert Phillips <robertphillips@google.com>
diff --git a/src/atlastext/SkAtlasTextTarget.cpp b/src/atlastext/SkAtlasTextTarget.cpp
index 9da5fd1..4513952 100644
--- a/src/atlastext/SkAtlasTextTarget.cpp
+++ b/src/atlastext/SkAtlasTextTarget.cpp
@@ -192,15 +192,20 @@
                 resourceProvider, fGeoData[i].fBlob, fGeoData[i].fRun, fGeoData[i].fSubRun,
                 fGeoData[i].fViewMatrix, fGeoData[i].fX, fGeoData[i].fY, fGeoData[i].fColor,
                 &context, glyphCache, fullAtlasManager, &autoGlyphCache);
-        GrAtlasTextBlob::VertexRegenerator::Result result;
-        do {
-            result = regenerator.regenerate();
+        bool done = false;
+        while (!done) {
+            GrAtlasTextBlob::VertexRegenerator::Result result;
+            if (!regenerator.regenerate(&result)) {
+                break;
+            }
+            done = result.fFinished;
+
             context.recordDraw(result.fFirstVertex, result.fGlyphsRegenerated,
                                fGeoData[i].fViewMatrix, target->handle());
             if (!result.fFinished) {
                 // Make space in the atlas so we can continue generating vertices.
                 context.flush();
             }
-        } while (!result.fFinished);
+        }
     }
 }
diff --git a/src/gpu/GrDrawOpAtlas.cpp b/src/gpu/GrDrawOpAtlas.cpp
index bbb87f7..629d91b 100644
--- a/src/gpu/GrDrawOpAtlas.cpp
+++ b/src/gpu/GrDrawOpAtlas.cpp
@@ -186,7 +186,7 @@
         , fTextureHeight(height)
         , fAtlasGeneration(kInvalidAtlasGeneration + 1)
         , fPrevFlushToken(GrDeferredUploadToken::AlreadyFlushedToken())
-        , fAllowMultitexturing(allowMultitexturing)
+        , fMaxPages(AllowMultitexturing::kYes == allowMultitexturing ? kMaxMultitexturePages : 1)
         , fNumActivePages(0) {
     fPlotWidth = fTextureWidth / numPlotsX;
     fPlotHeight = fTextureHeight / numPlotsY;
@@ -230,6 +230,25 @@
     return true;
 }
 
+bool GrDrawOpAtlas::uploadToPage(unsigned int pageIdx, AtlasID* id, GrDeferredUploadTarget* target,
+                                 int width, int height, const void* image, SkIPoint16* loc) {
+    SkASSERT(fProxies[pageIdx] && fProxies[pageIdx]->priv().isInstantiated());
+
+    // look through all allocated plots for one we can share, in Most Recently Refed order
+    PlotList::Iter plotIter;
+    plotIter.init(fPages[pageIdx].fPlotList, PlotList::Iter::kHead_IterStart);
+
+    for (Plot* plot = plotIter.get(); plot; plot = plotIter.next()) {
+        SkASSERT(GrBytesPerPixel(fProxies[pageIdx]->config()) == plot->bpp());
+
+        if (plot->addSubImage(width, height, image, loc)) {
+            return this->updatePlot(target, id, plot);
+        }
+    }
+
+    return false;
+}
+
 // Number of atlas-related flushes beyond which we consider a plot to no longer be in use.
 //
 // This value is somewhat arbitrary -- the idea is to keep it low enough that
@@ -238,28 +257,20 @@
 // are rare; i.e., we are not continually refreshing the frame.
 static constexpr auto kRecentlyUsedCount = 256;
 
-bool GrDrawOpAtlas::addToAtlas(GrResourceProvider* resourceProvider,
-                               AtlasID* id, GrDeferredUploadTarget* target,
-                               int width, int height, const void* image, SkIPoint16* loc) {
+GrDrawOpAtlas::ErrorCode GrDrawOpAtlas::addToAtlas(GrResourceProvider* resourceProvider,
+                                                   AtlasID* id, GrDeferredUploadTarget* target,
+                                                   int width, int height,
+                                                   const void* image, SkIPoint16* loc) {
     if (width > fPlotWidth || height > fPlotHeight) {
-        return false;
+        return ErrorCode::kError;
     }
 
     // Look through each page to see if we can upload without having to flush
     // We prioritize this upload to the first pages, not the most recently used, to make it easier
     // to remove unused pages in reverse page order.
     for (unsigned int pageIdx = 0; pageIdx < fNumActivePages; ++pageIdx) {
-        SkASSERT(fProxies[pageIdx]);
-        // look through all allocated plots for one we can share, in Most Recently Refed order
-        PlotList::Iter plotIter;
-        plotIter.init(fPages[pageIdx].fPlotList, PlotList::Iter::kHead_IterStart);
-        Plot* plot;
-        while ((plot = plotIter.get())) {
-            SkASSERT(GrBytesPerPixel(fProxies[pageIdx]->config()) == plot->bpp());
-            if (plot->addSubImage(width, height, image, loc)) {
-                return this->updatePlot(target, id, plot);
-            }
-            plotIter.next();
+        if (this->uploadToPage(pageIdx, id, target, width, height, image, loc)) {
+            return ErrorCode::kSucceeded;
         }
     }
 
@@ -268,37 +279,39 @@
     // We wait until we've grown to the full number of pages to begin evicting already flushed
     // plots so that we can maximize the opportunity for reuse.
     // As before we prioritize this upload to the first pages, not the most recently used.
-    for (unsigned int pageIdx = 0; pageIdx < fNumActivePages; ++pageIdx) {
-        Plot* plot = fPages[pageIdx].fPlotList.tail();
-        SkASSERT(plot);
-        if ((fNumActivePages == this->maxPages() &&
-             plot->lastUseToken() < target->tokenTracker()->nextTokenToFlush()) ||
-            plot->flushesSinceLastUsed() >= kRecentlyUsedCount) {
-            this->processEvictionAndResetRects(plot);
-            SkASSERT(GrBytesPerPixel(fProxies[pageIdx]->config()) == plot->bpp());
-            SkDEBUGCODE(bool verify = )plot->addSubImage(width, height, image, loc);
-            SkASSERT(verify);
-            if (!this->updatePlot(target, id, plot)) {
-                return false;
+    if (fNumActivePages == this->maxPages()) {
+        for (unsigned int pageIdx = 0; pageIdx < fNumActivePages; ++pageIdx) {
+            Plot* plot = fPages[pageIdx].fPlotList.tail();
+            SkASSERT(plot);
+            if (plot->lastUseToken() < target->tokenTracker()->nextTokenToFlush() ||
+                plot->flushesSinceLastUsed() >= kRecentlyUsedCount) {
+                this->processEvictionAndResetRects(plot);
+                SkASSERT(GrBytesPerPixel(fProxies[pageIdx]->config()) == plot->bpp());
+                SkDEBUGCODE(bool verify = )plot->addSubImage(width, height, image, loc);
+                SkASSERT(verify);
+                if (!this->updatePlot(target, id, plot)) {
+                    return ErrorCode::kError;
+                }
+                return ErrorCode::kSucceeded;
             }
-            return true;
+        }
+    } else {
+        // If we haven't activated all the available pages, try to create a new one and add to it
+        if (!this->activateNewPage(resourceProvider)) {
+            return ErrorCode::kError;
+        }
+
+        if (this->uploadToPage(fNumActivePages-1, id, target, width, height, image, loc)) {
+            return ErrorCode::kSucceeded;
+        } else {
+            // If we fail to upload to a newly activated page then something has gone terribly
+            // wrong - return an error
+            return ErrorCode::kError;
         }
     }
 
-    // If the simple cases fail, try to create a new page and add to it
-    if (this->activateNewPage(resourceProvider)) {
-        unsigned int pageIdx = fNumActivePages-1;
-        SkASSERT(fProxies[pageIdx] && fProxies[pageIdx]->priv().isInstantiated());
-
-        Plot* plot = fPages[pageIdx].fPlotList.head();
-        SkASSERT(GrBytesPerPixel(fProxies[pageIdx]->config()) == plot->bpp());
-        if (plot->addSubImage(width, height, image, loc)) {
-            return this->updatePlot(target, id, plot);
-        }
-
-        // we shouldn't get here -- if so, something has gone terribly wrong
-        SkASSERT(false);
-        return false;
+    if (!fNumActivePages) {
+        return ErrorCode::kError;
     }
 
     // Try to find a plot that we can perform an inline upload to.
@@ -316,9 +329,9 @@
     // we have to fail. This gives the op a chance to enqueue the draw, and call back into this
     // function. When that draw is enqueued, the draw token advances, and the subsequent call will
     // continue past this branch and prepare an inline upload that will occur after the enqueued
-    //draw which references the plot's pre-upload content.
+    // draw which references the plot's pre-upload content.
     if (!plot) {
-        return false;
+        return ErrorCode::kTryAgain;
     }
 
     this->processEviction(plot->id());
@@ -348,7 +361,7 @@
 
     *id = newPlot->id();
 
-    return true;
+    return ErrorCode::kSucceeded;
 }
 
 void GrDrawOpAtlas::compact(GrDeferredUploadToken startTokenForNextFlush) {
@@ -537,9 +550,7 @@
 
 
 bool GrDrawOpAtlas::activateNewPage(GrResourceProvider* resourceProvider) {
-    if (fNumActivePages >= this->maxPages()) {
-        return false;
-    }
+    SkASSERT(fNumActivePages < this->maxPages());
 
     if (!fProxies[fNumActivePages]->instantiate(resourceProvider)) {
         return false;
diff --git a/src/gpu/GrDrawOpAtlas.h b/src/gpu/GrDrawOpAtlas.h
index b849d9e..6106c0c 100644
--- a/src/gpu/GrDrawOpAtlas.h
+++ b/src/gpu/GrDrawOpAtlas.h
@@ -98,19 +98,30 @@
                                                GrDrawOpAtlas::EvictionFunc func, void* data);
 
     /**
-     * Adds a width x height subimage to the atlas. Upon success it returns an ID and the subimage's
-     * coordinates in the backing texture. False is returned if the subimage cannot fit in the
-     * atlas without overwriting texels that will be read in the current draw. This indicates that
-     * the op should end its current draw and begin another before adding more data. Upon success,
-     * an upload of the provided image data will have been added to the GrDrawOp::Target, in "asap"
-     * mode if possible, otherwise in "inline" mode. Successive uploads in either mode may be
-     * consolidated.
+     * Adds a width x height subimage to the atlas. Upon success it returns 'kSucceeded' and returns
+     * the ID and the subimage's coordinates in the backing texture. 'kTryAgain' is returned if
+     * the subimage cannot fit in the atlas without overwriting texels that will be read in the
+     * current draw. This indicates that the op should end its current draw and begin another
+     * before adding more data. Upon success, an upload of the provided image data will have
+     * been added to the GrDrawOp::Target, in "asap" mode if possible, otherwise in "inline" mode.
+     * Successive uploads in either mode may be consolidated.
+     * 'kError' will be returned when some unrecoverable error was encountered while trying to
+     * add the subimage. In this case the op being created should be discarded.
+     *
      * NOTE: When the GrDrawOp prepares a draw that reads from the atlas, it must immediately call
      * 'setUseToken' with the currentToken from the GrDrawOp::Target, otherwise the next call to
      * addToAtlas might cause the previous data to be overwritten before it has been read.
      */
-    bool addToAtlas(GrResourceProvider*, AtlasID*, GrDeferredUploadTarget*, int width, int height,
-                    const void* image, SkIPoint16* loc);
+
+    enum class ErrorCode {
+        kError,
+        kSucceeded,
+        kTryAgain
+    };
+
+    ErrorCode addToAtlas(GrResourceProvider*, AtlasID*, GrDeferredUploadTarget*,
+                         int width, int height,
+                         const void* image, SkIPoint16* loc);
 
     const sk_sp<GrTextureProxy>* getProxies() const { return fProxies; }
 
@@ -229,10 +240,11 @@
     void instantiate(GrOnFlushResourceProvider*);
 
     uint32_t maxPages() const {
-        return AllowMultitexturing::kYes == fAllowMultitexturing ? kMaxMultitexturePages : 1;
+        return fMaxPages;
     }
 
     int numAllocated_TestingOnly() const;
+    void setMaxPages_TestingOnly(uint32_t maxPages);
 
 private:
     GrDrawOpAtlas(GrProxyProvider*, GrPixelConfig, int width, int height, int numPlotsX,
@@ -359,6 +371,9 @@
         // the front and remove from the back there is no need for MRU.
     }
 
+    bool uploadToPage(unsigned int pageIdx, AtlasID* id, GrDeferredUploadTarget* target,
+                      int width, int height, const void* image, SkIPoint16* loc);
+
     bool createPages(GrProxyProvider*);
     bool activateNewPage(GrResourceProvider*);
     void deactivateLastPage();
@@ -396,7 +411,7 @@
     // proxies kept separate to make it easier to pass them up to client
     sk_sp<GrTextureProxy> fProxies[kMaxMultitexturePages];
     Page fPages[kMaxMultitexturePages];
-    AllowMultitexturing fAllowMultitexturing;
+    uint32_t fMaxPages;
 
     uint32_t fNumActivePages;
 };
diff --git a/src/gpu/ops/GrAtlasTextOp.cpp b/src/gpu/ops/GrAtlasTextOp.cpp
index 9e144ed..07e1141 100644
--- a/src/gpu/ops/GrAtlasTextOp.cpp
+++ b/src/gpu/ops/GrAtlasTextOp.cpp
@@ -292,9 +292,14 @@
                 resourceProvider, blob, args.fRun, args.fSubRun, args.fViewMatrix, args.fX, args.fY,
                 args.fColor, target->deferredUploadTarget(), glyphCache, fullAtlasManager,
                 &autoGlyphCache);
-        GrAtlasTextBlob::VertexRegenerator::Result result;
-        do {
-            result = regenerator.regenerate();
+        bool done = false;
+        while (!done) {
+            GrAtlasTextBlob::VertexRegenerator::Result result;
+            if (!regenerator.regenerate(&result)) {
+                break;
+            }
+            done = result.fFinished;
+
             // Copy regenerated vertices from the blob to our vertex buffer.
             size_t vertexBytes = result.fGlyphsRegenerated * kVerticesPerGlyph * vertexStride;
             if (args.fClipRect.isEmpty()) {
@@ -325,12 +330,16 @@
                 this->flush(target, &flushInfo);
             }
             currVertex += vertexBytes;
-        } while (!result.fFinished);
+        }
     }
     this->flush(target, &flushInfo);
 }
 
 void GrAtlasTextOp::flush(GrMeshDrawOp::Target* target, FlushInfo* flushInfo) const {
+    if (!flushInfo->fGlyphsToFlush) {
+        return;
+    }
+
     auto fullAtlasManager = target->fullAtlasManager();
     SkASSERT(fRestrictedAtlasManager == fullAtlasManager);
 
diff --git a/src/gpu/ops/GrSmallPathRenderer.cpp b/src/gpu/ops/GrSmallPathRenderer.cpp
index 1607069..73f0da5 100644
--- a/src/gpu/ops/GrSmallPathRenderer.cpp
+++ b/src/gpu/ops/GrSmallPathRenderer.cpp
@@ -43,6 +43,91 @@
 static const SkScalar kMinSize = SK_ScalarHalf;
 static const SkScalar kMaxSize = 2*kMaxMIP;
 
+class ShapeDataKey {
+public:
+    ShapeDataKey() {}
+    ShapeDataKey(const ShapeDataKey& that) { *this = that; }
+    ShapeDataKey(const GrShape& shape, uint32_t dim) { this->set(shape, dim); }
+    ShapeDataKey(const GrShape& shape, const SkMatrix& ctm) { this->set(shape, ctm); }
+
+    ShapeDataKey& operator=(const ShapeDataKey& that) {
+        fKey.reset(that.fKey.count());
+        memcpy(fKey.get(), that.fKey.get(), fKey.count() * sizeof(uint32_t));
+        return *this;
+    }
+
+    // for SDF paths
+    void set(const GrShape& shape, uint32_t dim) {
+        // Shapes' keys are for their pre-style geometry, but by now we shouldn't have any
+        // relevant styling information.
+        SkASSERT(shape.style().isSimpleFill());
+        SkASSERT(shape.hasUnstyledKey());
+        int shapeKeySize = shape.unstyledKeySize();
+        fKey.reset(1 + shapeKeySize);
+        fKey[0] = dim;
+        shape.writeUnstyledKey(&fKey[1]);
+    }
+
+    // for bitmap paths
+    void set(const GrShape& shape, const SkMatrix& ctm) {
+        // Shapes' keys are for their pre-style geometry, but by now we shouldn't have any
+        // relevant styling information.
+        SkASSERT(shape.style().isSimpleFill());
+        SkASSERT(shape.hasUnstyledKey());
+        // We require the upper left 2x2 of the matrix to match exactly for a cache hit.
+        SkScalar sx = ctm.get(SkMatrix::kMScaleX);
+        SkScalar sy = ctm.get(SkMatrix::kMScaleY);
+        SkScalar kx = ctm.get(SkMatrix::kMSkewX);
+        SkScalar ky = ctm.get(SkMatrix::kMSkewY);
+        SkScalar tx = ctm.get(SkMatrix::kMTransX);
+        SkScalar ty = ctm.get(SkMatrix::kMTransY);
+        // Allow 8 bits each in x and y of subpixel positioning.
+        SkFixed fracX = SkScalarToFixed(SkScalarFraction(tx)) & 0x0000FF00;
+        SkFixed fracY = SkScalarToFixed(SkScalarFraction(ty)) & 0x0000FF00;
+        int shapeKeySize = shape.unstyledKeySize();
+        fKey.reset(5 + shapeKeySize);
+        fKey[0] = SkFloat2Bits(sx);
+        fKey[1] = SkFloat2Bits(sy);
+        fKey[2] = SkFloat2Bits(kx);
+        fKey[3] = SkFloat2Bits(ky);
+        fKey[4] = fracX | (fracY >> 8);
+        shape.writeUnstyledKey(&fKey[5]);
+    }
+
+    bool operator==(const ShapeDataKey& that) const {
+        return fKey.count() == that.fKey.count() &&
+                0 == memcmp(fKey.get(), that.fKey.get(), sizeof(uint32_t) * fKey.count());
+    }
+
+    int count32() const { return fKey.count(); }
+    const uint32_t* data() const { return fKey.get(); }
+
+private:
+    // The key is composed of the GrShape's key, and either the dimensions of the DF
+    // generated for the path (32x32 max, 64x64 max, 128x128 max) if an SDF image or
+    // the matrix for the path with only fractional translation.
+    SkAutoSTArray<24, uint32_t> fKey;
+};
+
+class ShapeData {
+public:
+    ShapeDataKey           fKey;
+    GrDrawOpAtlas::AtlasID fID;
+    SkRect                 fBounds;
+    GrIRect16              fTextureCoords;
+    SK_DECLARE_INTERNAL_LLIST_INTERFACE(ShapeData);
+
+    static inline const ShapeDataKey& GetKey(const ShapeData& data) {
+        return data.fKey;
+    }
+
+    static inline uint32_t Hash(ShapeDataKey key) {
+        return SkOpts::hash(key.data(), sizeof(uint32_t) * key.count32());
+    }
+};
+
+
+
 // Callback to clear out internal path cache when eviction occurs
 void GrSmallPathRenderer::HandleEviction(GrDrawOpAtlas::AtlasID id, void* pr) {
     GrSmallPathRenderer* dfpr = (GrSmallPathRenderer*)pr;
@@ -134,8 +219,7 @@
 public:
     DEFINE_OP_CLASS_ID
 
-    using ShapeData = GrSmallPathRenderer::ShapeData;
-    using ShapeCache = SkTDynamicHash<ShapeData, ShapeData::Key>;
+    using ShapeCache = SkTDynamicHash<ShapeData, ShapeDataKey>;
     using ShapeDataList = GrSmallPathRenderer::ShapeDataList;
 
     static std::unique_ptr<GrDrawOp> Make(GrPaint&& paint, const GrShape& shape,
@@ -330,7 +414,7 @@
                 SkScalar desiredDimension = SkTMin(mipSize, kMaxMIP);
 
                 // check to see if df path is cached
-                ShapeData::Key key(args.fShape, SkScalarCeilToInt(desiredDimension));
+                ShapeDataKey key(args.fShape, SkScalarCeilToInt(desiredDimension));
                 shapeData = fShapeCache->find(key);
                 if (nullptr == shapeData || !fAtlas->hasID(shapeData->fID)) {
                     // Remove the stale cache entry
@@ -355,7 +439,7 @@
                 }
             } else {
                 // check to see if bitmap path is cached
-                ShapeData::Key key(args.fShape, args.fViewMatrix);
+                ShapeDataKey key(args.fShape, args.fViewMatrix);
                 shapeData = fShapeCache->find(key);
                 if (nullptr == shapeData || !fAtlas->hasID(shapeData->fID)) {
                     // Remove the stale cache entry
@@ -394,10 +478,32 @@
         this->flush(target, &flushInfo);
     }
 
+    bool addToAtlas(GrMeshDrawOp::Target* target, FlushInfo* flushInfo, GrDrawOpAtlas* atlas,
+                    int width, int height, const void* image,
+                    GrDrawOpAtlas::AtlasID* id, SkIPoint16* atlasLocation) const {
+        auto resourceProvider = target->resourceProvider();
+        auto uploadTarget = target->deferredUploadTarget();
+
+        GrDrawOpAtlas::ErrorCode code = atlas->addToAtlas(resourceProvider, id,
+                                                          uploadTarget, width, height,
+                                                          image, atlasLocation);
+        if (GrDrawOpAtlas::ErrorCode::kError == code) {
+            return false;
+        }
+
+        if (GrDrawOpAtlas::ErrorCode::kTryAgain == code) {
+            this->flush(target, flushInfo);
+
+            code = atlas->addToAtlas(resourceProvider, id, uploadTarget, width, height,
+                                     image, atlasLocation);
+        }
+
+        return GrDrawOpAtlas::ErrorCode::kSucceeded == code;
+    }
+
     bool addDFPathToAtlas(GrMeshDrawOp::Target* target, FlushInfo* flushInfo,
                           GrDrawOpAtlas* atlas, ShapeData* shapeData, const GrShape& shape,
                           uint32_t dimension, SkScalar scale) const {
-        auto resourceProvider = target->resourceProvider();
 
         const SkRect& bounds = shape.bounds();
 
@@ -486,14 +592,10 @@
         // add to atlas
         SkIPoint16 atlasLocation;
         GrDrawOpAtlas::AtlasID id;
-        auto uploadTarget = target->deferredUploadTarget();
-        if (!atlas->addToAtlas(resourceProvider, &id, uploadTarget, width, height,
-                               dfStorage.get(), &atlasLocation)) {
-            this->flush(target, flushInfo);
-            if (!atlas->addToAtlas(resourceProvider, &id, uploadTarget, width, height,
-                                   dfStorage.get(), &atlasLocation)) {
-                return false;
-            }
+
+        if (!this->addToAtlas(target, flushInfo, atlas,
+                              width, height, dfStorage.get(), &id, &atlasLocation)) {
+            return false;
         }
 
         // add to cache
@@ -530,8 +632,6 @@
     bool addBMPathToAtlas(GrMeshDrawOp::Target* target, FlushInfo* flushInfo,
                           GrDrawOpAtlas* atlas, ShapeData* shapeData, const GrShape& shape,
                           const SkMatrix& ctm) const {
-        auto resourceProvider = target->resourceProvider();
-
         const SkRect& bounds = shape.bounds();
         if (bounds.isEmpty()) {
             return false;
@@ -591,14 +691,10 @@
         // add to atlas
         SkIPoint16 atlasLocation;
         GrDrawOpAtlas::AtlasID id;
-        auto uploadTarget = target->deferredUploadTarget();
-        if (!atlas->addToAtlas(resourceProvider, &id, uploadTarget, dst.width(), dst.height(),
-                               dst.addr(), &atlasLocation)) {
-            this->flush(target, flushInfo);
-            if (!atlas->addToAtlas(resourceProvider, &id, uploadTarget, dst.width(), dst.height(),
-                                   dst.addr(), &atlasLocation)) {
-                return false;
-            }
+
+        if (!this->addToAtlas(target, flushInfo, atlas,
+                              dst.width(), dst.height(), dst.addr(), &id, &atlasLocation)) {
+            return false;
         }
 
         // add to cache
@@ -853,6 +949,21 @@
     ShapeDataList fShapeList;
 };
 
+std::unique_ptr<GrDrawOp> GrSmallPathRenderer::createOp_TestingOnly(
+                                                        GrPaint&& paint,
+                                                        const GrShape& shape,
+                                                        const SkMatrix& viewMatrix,
+                                                        GrDrawOpAtlas* atlas,
+                                                        ShapeCache* shapeCache,
+                                                        ShapeDataList* shapeList,
+                                                        bool gammaCorrect,
+                                                        const GrUserStencilSettings* stencil) {
+
+    return GrSmallPathRenderer::SmallPathOp::Make(std::move(paint), shape, viewMatrix, atlas,
+                                                  shapeCache, shapeList, gammaCorrect, stencil);
+
+}
+
 GR_DRAW_OP_TEST_DEFINE(SmallPathOp) {
     using PathTestStruct = GrSmallPathRenderer::PathTestStruct;
     static PathTestStruct gTestStruct;
@@ -874,10 +985,13 @@
 
     // This path renderer only allows fill styles.
     GrShape shape(GrTest::TestPath(random), GrStyle::SimpleFill());
-
-    return GrSmallPathRenderer::SmallPathOp::Make(
-            std::move(paint), shape, viewMatrix, gTestStruct.fAtlas.get(), &gTestStruct.fShapeCache,
-            &gTestStruct.fShapeList, gammaCorrect, GrGetRandomStencil(random, context));
+    return GrSmallPathRenderer::createOp_TestingOnly(
+                                         std::move(paint), shape, viewMatrix,
+                                         gTestStruct.fAtlas.get(),
+                                         &gTestStruct.fShapeCache,
+                                         &gTestStruct.fShapeList,
+                                         gammaCorrect,
+                                         GrGetRandomStencil(random, context));
 }
 
 #endif
diff --git a/src/gpu/ops/GrSmallPathRenderer.h b/src/gpu/ops/GrSmallPathRenderer.h
index ef83c77..ab58bdf 100644
--- a/src/gpu/ops/GrSmallPathRenderer.h
+++ b/src/gpu/ops/GrSmallPathRenderer.h
@@ -19,14 +19,14 @@
 
 class GrContext;
 
+class ShapeData;
+class ShapeDataKey;
+
 class GrSmallPathRenderer : public GrPathRenderer, public GrOnFlushCallbackObject {
 public:
     GrSmallPathRenderer();
     ~GrSmallPathRenderer() override;
 
-    class SmallPathOp;
-    struct PathTestStruct;
-
     // GrOnFlushCallbackObject overrides
     //
     // Note: because this class is associated with a path renderer we want it to be removed from
@@ -41,13 +41,26 @@
     }
 
     void postFlush(GrDeferredUploadToken startTokenForNextFlush,
-                   const uint32_t* opListIDs, int numOpListIDs) override {
+                   const uint32_t* /*opListIDs*/, int /*numOpListIDs*/) override {
         if (fAtlas) {
             fAtlas->compact(startTokenForNextFlush);
         }
     }
 
+    using ShapeCache = SkTDynamicHash<ShapeData, ShapeDataKey>;
+    typedef SkTInternalLList<ShapeData> ShapeDataList;
+
+    static std::unique_ptr<GrDrawOp> createOp_TestingOnly(GrPaint&&, const GrShape&,
+                                                          const SkMatrix& viewMatrix,
+                                                          GrDrawOpAtlas* atlas,
+                                                          ShapeCache*, ShapeDataList*,
+                                                          bool gammaCorrect,
+                                                          const GrUserStencilSettings*);
+    struct PathTestStruct;
+
 private:
+    class SmallPathOp;
+
     StencilSupport onGetStencilSupport(const GrShape&) const override {
         return GrPathRenderer::kNoSupport_StencilSupport;
     }
@@ -56,92 +69,8 @@
 
     bool onDrawPath(const DrawPathArgs&) override;
 
-    struct ShapeData {
-        class Key {
-        public:
-            Key() {}
-            Key(const Key& that) { *this = that; }
-            Key(const GrShape& shape, uint32_t dim) { this->set(shape, dim); }
-            Key(const GrShape& shape, const SkMatrix& ctm) { this->set(shape, ctm); }
-
-            Key& operator=(const Key& that) {
-                fKey.reset(that.fKey.count());
-                memcpy(fKey.get(), that.fKey.get(), fKey.count() * sizeof(uint32_t));
-                return *this;
-            }
-
-            // for SDF paths
-            void set(const GrShape& shape, uint32_t dim) {
-                // Shapes' keys are for their pre-style geometry, but by now we shouldn't have any
-                // relevant styling information.
-                SkASSERT(shape.style().isSimpleFill());
-                SkASSERT(shape.hasUnstyledKey());
-                int shapeKeySize = shape.unstyledKeySize();
-                fKey.reset(1 + shapeKeySize);
-                fKey[0] = dim;
-                shape.writeUnstyledKey(&fKey[1]);
-            }
-
-            // for bitmap paths
-            void set(const GrShape& shape, const SkMatrix& ctm) {
-                // Shapes' keys are for their pre-style geometry, but by now we shouldn't have any
-                // relevant styling information.
-                SkASSERT(shape.style().isSimpleFill());
-                SkASSERT(shape.hasUnstyledKey());
-                // We require the upper left 2x2 of the matrix to match exactly for a cache hit.
-                SkScalar sx = ctm.get(SkMatrix::kMScaleX);
-                SkScalar sy = ctm.get(SkMatrix::kMScaleY);
-                SkScalar kx = ctm.get(SkMatrix::kMSkewX);
-                SkScalar ky = ctm.get(SkMatrix::kMSkewY);
-                SkScalar tx = ctm.get(SkMatrix::kMTransX);
-                SkScalar ty = ctm.get(SkMatrix::kMTransY);
-                // Allow 8 bits each in x and y of subpixel positioning.
-                SkFixed fracX = SkScalarToFixed(SkScalarFraction(tx)) & 0x0000FF00;
-                SkFixed fracY = SkScalarToFixed(SkScalarFraction(ty)) & 0x0000FF00;
-                int shapeKeySize = shape.unstyledKeySize();
-                fKey.reset(5 + shapeKeySize);
-                fKey[0] = SkFloat2Bits(sx);
-                fKey[1] = SkFloat2Bits(sy);
-                fKey[2] = SkFloat2Bits(kx);
-                fKey[3] = SkFloat2Bits(ky);
-                fKey[4] = fracX | (fracY >> 8);
-                shape.writeUnstyledKey(&fKey[5]);
-            }
-
-            bool operator==(const Key& that) const {
-                return fKey.count() == that.fKey.count() &&
-                        0 == memcmp(fKey.get(), that.fKey.get(), sizeof(uint32_t) * fKey.count());
-            }
-
-            int count32() const { return fKey.count(); }
-            const uint32_t* data() const { return fKey.get(); }
-
-        private:
-            // The key is composed of the GrShape's key, and either the dimensions of the DF
-            // generated for the path (32x32 max, 64x64 max, 128x128 max) if an SDF image or
-            // the matrix for the path with only fractional translation.
-            SkAutoSTArray<24, uint32_t> fKey;
-        };
-        Key                    fKey;
-        GrDrawOpAtlas::AtlasID fID;
-        SkRect                 fBounds;
-        GrIRect16              fTextureCoords;
-        SK_DECLARE_INTERNAL_LLIST_INTERFACE(ShapeData);
-
-        static inline const Key& GetKey(const ShapeData& data) {
-            return data.fKey;
-        }
-
-        static inline uint32_t Hash(Key key) {
-            return SkOpts::hash(key.data(), sizeof(uint32_t) * key.count32());
-        }
-    };
-
     static void HandleEviction(GrDrawOpAtlas::AtlasID, void*);
 
-    typedef SkTDynamicHash<ShapeData, ShapeData::Key> ShapeCache;
-    typedef SkTInternalLList<ShapeData> ShapeDataList;
-
     std::unique_ptr<GrDrawOpAtlas> fAtlas;
     ShapeCache fShapeCache;
     ShapeDataList fShapeList;
diff --git a/src/gpu/text/GrAtlasManager.cpp b/src/gpu/text/GrAtlasManager.cpp
index c6a6056..b688c1c 100644
--- a/src/gpu/text/GrAtlasManager.cpp
+++ b/src/gpu/text/GrAtlasManager.cpp
@@ -94,14 +94,15 @@
 }
 
 // add to texture atlas that matches this format
-bool GrAtlasManager::addToAtlas(GrResourceProvider* resourceProvider,
+GrDrawOpAtlas::ErrorCode GrAtlasManager::addToAtlas(
+                                GrResourceProvider* resourceProvider,
                                 GrGlyphCache* glyphCache,
                                 GrTextStrike* strike, GrDrawOpAtlas::AtlasID* id,
                                 GrDeferredUploadTarget* target, GrMaskFormat format,
                                 int width, int height, const void* image, SkIPoint16* loc) {
     glyphCache->setStrikeToPreserve(strike);
     return this->getAtlas(format)->addToAtlas(resourceProvider, id, target, width, height,
-                                                image, loc);
+                                              image, loc);
 }
 
 void GrAtlasManager::addGlyphToBulkAndSetUseToken(GrDrawOpAtlas::BulkUseTokenUpdater* updater,
diff --git a/src/gpu/text/GrAtlasManager.h b/src/gpu/text/GrAtlasManager.h
index eb9c113..9241193 100644
--- a/src/gpu/text/GrAtlasManager.h
+++ b/src/gpu/text/GrAtlasManager.h
@@ -100,7 +100,8 @@
     }
 
     // add to texture atlas that matches this format
-    bool addToAtlas(GrResourceProvider*, GrGlyphCache*, GrTextStrike*,
+    GrDrawOpAtlas::ErrorCode addToAtlas(
+                    GrResourceProvider*, GrGlyphCache*, GrTextStrike*,
                     GrDrawOpAtlas::AtlasID*, GrDeferredUploadTarget*, GrMaskFormat,
                     int width, int height, const void* image, SkIPoint16* loc);
 
@@ -142,6 +143,7 @@
 #endif
 
     void setAtlasSizes_ForTesting(const GrDrawOpAtlasConfig configs[3]);
+    void setMaxPages_TestingOnly(uint32_t maxPages);
 
 private:
     bool initAtlas(GrMaskFormat) override;
diff --git a/src/gpu/text/GrAtlasTextBlob.h b/src/gpu/text/GrAtlasTextBlob.h
index b4a11a4..a01041f 100644
--- a/src/gpu/text/GrAtlasTextBlob.h
+++ b/src/gpu/text/GrAtlasTextBlob.h
@@ -595,11 +595,11 @@
         const char* fFirstVertex;
     };
 
-    Result regenerate();
+    bool regenerate(Result*);
 
 private:
     template <bool regenPos, bool regenCol, bool regenTexCoords, bool regenGlyphs>
-    Result doRegen();
+    bool doRegen(Result*);
 
     GrResourceProvider* fResourceProvider;
     const SkMatrix& fViewMatrix;
diff --git a/src/gpu/text/GrAtlasTextBlobVertexRegenerator.cpp b/src/gpu/text/GrAtlasTextBlobVertexRegenerator.cpp
index 296df22..19b0959 100644
--- a/src/gpu/text/GrAtlasTextBlobVertexRegenerator.cpp
+++ b/src/gpu/text/GrAtlasTextBlobVertexRegenerator.cpp
@@ -230,7 +230,7 @@
 }
 
 template <bool regenPos, bool regenCol, bool regenTexCoords, bool regenGlyphs>
-Regenerator::Result Regenerator::doRegen() {
+bool Regenerator::doRegen(Regenerator::Result* result) {
     static_assert(!regenGlyphs || regenTexCoords, "must regenTexCoords along regenGlyphs");
     sk_sp<GrTextStrike> strike;
     if (regenTexCoords) {
@@ -255,11 +255,10 @@
     }
 
     bool hasW = fSubRun->hasWCoord();
-    Result result;
     auto vertexStride = GetVertexStride(fSubRun->maskFormat(), hasW);
     char* currVertex = fBlob->fVertices + fSubRun->vertexStartIndex() +
                        fCurrGlyph * kVerticesPerGlyph * vertexStride;
-    result.fFirstVertex = currVertex;
+    result->fFirstVertex = currVertex;
 
     for (int glyphIdx = fCurrGlyph; glyphIdx < (int)fSubRun->glyphCount(); glyphIdx++) {
         GrGlyph* glyph = nullptr;
@@ -277,14 +276,21 @@
             glyph = fBlob->fGlyphs[glyphOffset];
             SkASSERT(glyph && glyph->fMaskFormat == fSubRun->maskFormat());
 
-            if (!fFullAtlasManager->hasGlyph(glyph) &&
-                !strike->addGlyphToAtlas(fResourceProvider, fUploadTarget, fGlyphCache,
-                                         fFullAtlasManager, glyph,
-                                         fLazyCache->get(), fSubRun->maskFormat(),
-                                         fSubRun->hasScaledGlyphs())) {
-                fBrokenRun = glyphIdx > 0;
-                result.fFinished = false;
-                return result;
+            if (!fFullAtlasManager->hasGlyph(glyph)) {
+                GrDrawOpAtlas::ErrorCode code;
+                code = strike->addGlyphToAtlas(fResourceProvider, fUploadTarget, fGlyphCache,
+                                              fFullAtlasManager, glyph,
+                                              fLazyCache->get(), fSubRun->maskFormat(),
+                                              fSubRun->hasScaledGlyphs());
+                if (GrDrawOpAtlas::ErrorCode::kError == code) {
+                    // Something horrible has happened - drop the op
+                    return false;
+                }
+                else if (GrDrawOpAtlas::ErrorCode::kTryAgain == code) {
+                    fBrokenRun = glyphIdx > 0;
+                    result->fFinished = false;
+                    return true;
+                }
             }
             auto tokenTracker = fUploadTarget->tokenTracker();
             fFullAtlasManager->addGlyphToBulkAndSetUseToken(fSubRun->bulkUseToken(), glyph,
@@ -295,7 +301,7 @@
                                                            fSubRun->drawAsDistanceFields(), fTransX,
                                                            fTransY, fColor);
         currVertex += vertexStride * GrAtlasTextOp::kVerticesPerGlyph;
-        ++result.fGlyphsRegenerated;
+        ++result->fGlyphsRegenerated;
         ++fCurrGlyph;
     }
 
@@ -309,10 +315,10 @@
                                     ? GrDrawOpAtlas::kInvalidAtlasGeneration
                                     : fFullAtlasManager->atlasGeneration(fSubRun->maskFormat()));
     }
-    return result;
+    return true;
 }
 
-Regenerator::Result Regenerator::regenerate() {
+bool Regenerator::regenerate(Regenerator::Result* result) {
     uint64_t currentAtlasGen = fFullAtlasManager->atlasGeneration(fSubRun->maskFormat());
     // If regenerate() is called multiple times then the atlas gen may have changed. So we check
     // this each time.
@@ -322,36 +328,36 @@
 
     switch (static_cast<RegenMask>(fRegenFlags)) {
         case kRegenPos:
-            return this->doRegen<true, false, false, false>();
+            return this->doRegen<true, false, false, false>(result);
         case kRegenCol:
-            return this->doRegen<false, true, false, false>();
+            return this->doRegen<false, true, false, false>(result);
         case kRegenTex:
-            return this->doRegen<false, false, true, false>();
+            return this->doRegen<false, false, true, false>(result);
         case kRegenGlyph:
-            return this->doRegen<false, false, true, true>();
+            return this->doRegen<false, false, true, true>(result);
 
         // combinations
         case kRegenPosCol:
-            return this->doRegen<true, true, false, false>();
+            return this->doRegen<true, true, false, false>(result);
         case kRegenPosTex:
-            return this->doRegen<true, false, true, false>();
+            return this->doRegen<true, false, true, false>(result);
         case kRegenPosTexGlyph:
-            return this->doRegen<true, false, true, true>();
+            return this->doRegen<true, false, true, true>(result);
         case kRegenPosColTex:
-            return this->doRegen<true, true, true, false>();
+            return this->doRegen<true, true, true, false>(result);
         case kRegenPosColTexGlyph:
-            return this->doRegen<true, true, true, true>();
+            return this->doRegen<true, true, true, true>(result);
         case kRegenColTex:
-            return this->doRegen<false, true, true, false>();
+            return this->doRegen<false, true, true, false>(result);
         case kRegenColTexGlyph:
-            return this->doRegen<false, true, true, true>();
+            return this->doRegen<false, true, true, true>(result);
         case kNoRegen: {
-            Result result;
             bool hasW = fSubRun->hasWCoord();
             auto vertexStride = GetVertexStride(fSubRun->maskFormat(), hasW);
-            result.fGlyphsRegenerated = fSubRun->glyphCount() - fCurrGlyph;
-            result.fFirstVertex = fBlob->fVertices + fSubRun->vertexStartIndex() +
-                                  fCurrGlyph * kVerticesPerGlyph * vertexStride;
+            result->fFinished = true;
+            result->fGlyphsRegenerated = fSubRun->glyphCount() - fCurrGlyph;
+            result->fFirstVertex = fBlob->fVertices + fSubRun->vertexStartIndex() +
+                                    fCurrGlyph * kVerticesPerGlyph * vertexStride;
             fCurrGlyph = fSubRun->glyphCount();
 
             // set use tokens for all of the glyphs in our subrun.  This is only valid if we
@@ -359,9 +365,9 @@
             fFullAtlasManager->setUseTokenBulk(*fSubRun->bulkUseToken(),
                                                fUploadTarget->tokenTracker()->nextDrawToken(),
                                                fSubRun->maskFormat());
-            return result;
+            return true;
         }
     }
     SK_ABORT("Should not get here");
-    return Result();
+    return false;
 }
diff --git a/src/gpu/text/GrAtlasTextContext.cpp b/src/gpu/text/GrAtlasTextContext.cpp
index ad708d7..ef71fe0 100644
--- a/src/gpu/text/GrAtlasTextContext.cpp
+++ b/src/gpu/text/GrAtlasTextContext.cpp
@@ -930,6 +930,37 @@
 
 #include "GrRenderTargetContext.h"
 
+std::unique_ptr<GrDrawOp> GrAtlasTextContext::createOp_TestingOnly(
+                                                       GrContext* context,
+                                                       GrAtlasTextContext* textContext,
+                                                       GrRenderTargetContext* rtc,
+                                                       const SkPaint& skPaint,
+                                                       const SkMatrix& viewMatrix,
+                                                       const char* text, int x, int y) {
+    auto glyphCache = context->contextPriv().getGlyphCache();
+    auto restrictedAtlasManager = context->contextPriv().getRestrictedAtlasManager();
+
+    static SkSurfaceProps surfaceProps(SkSurfaceProps::kLegacyFontHost_InitType);
+
+    size_t textLen = (int)strlen(text);
+
+    GrTextUtils::Paint utilsPaint(&skPaint, &rtc->colorSpaceInfo());
+
+    // right now we don't handle textblobs, nor do we handle drawPosText. Since we only intend to
+    // test the text op with this unit test, that is okay.
+    sk_sp<GrAtlasTextBlob> blob(textContext->makeDrawTextBlob(
+                                            context->contextPriv().getTextBlobCache(), glyphCache,
+                                            *context->caps()->shaderCaps(), utilsPaint,
+                                            GrAtlasTextContext::kTextBlobOpScalerContextFlags,
+                                            viewMatrix, surfaceProps, text,
+                                            static_cast<size_t>(textLen),
+                                            SkIntToScalar(x), SkIntToScalar(y)));
+
+    return blob->test_makeOp(textLen, 0, 0, viewMatrix, x, y, utilsPaint, surfaceProps,
+                             textContext->dfAdjustTable(), restrictedAtlasManager,
+                             rtc->textTarget());
+}
+
 GR_DRAW_OP_TEST_DEFINE(GrAtlasTextOp) {
     static uint32_t gContextID = SK_InvalidGenID;
     static std::unique_ptr<GrAtlasTextContext> gTextContext;
@@ -953,10 +984,8 @@
     skPaint.setLCDRenderText(random->nextBool());
     skPaint.setAntiAlias(skPaint.isLCDRenderText() ? true : random->nextBool());
     skPaint.setSubpixelText(random->nextBool());
-    GrTextUtils::Paint utilsPaint(&skPaint, &rtc->colorSpaceInfo());
 
     const char* text = "The quick brown fox jumps over the lazy dog.";
-    int textLen = (int)strlen(text);
 
     // create some random x/y offsets, including negative offsets
     static const int kMaxTrans = 1024;
@@ -964,23 +993,9 @@
     int yPos = (random->nextU() % 2) * 2 - 1;
     int xInt = (random->nextU() % kMaxTrans) * xPos;
     int yInt = (random->nextU() % kMaxTrans) * yPos;
-    SkScalar x = SkIntToScalar(xInt);
-    SkScalar y = SkIntToScalar(yInt);
 
-    auto glyphCache = context->contextPriv().getGlyphCache();
-    auto restrictedAtlasManager = context->contextPriv().getRestrictedAtlasManager();
-
-    // right now we don't handle textblobs, nor do we handle drawPosText. Since we only intend to
-    // test the text op with this unit test, that is okay.
-    sk_sp<GrAtlasTextBlob> blob(gTextContext->makeDrawTextBlob(
-            context->contextPriv().getTextBlobCache(), glyphCache,
-            *context->caps()->shaderCaps(), utilsPaint,
-            GrAtlasTextContext::kTextBlobOpScalerContextFlags, viewMatrix, gSurfaceProps, text,
-            static_cast<size_t>(textLen), x, y));
-
-    return blob->test_makeOp(textLen, 0, 0, viewMatrix, x, y, utilsPaint, gSurfaceProps,
-                             gTextContext->dfAdjustTable(), restrictedAtlasManager,
-                             rtc->textTarget());
+    return gTextContext->createOp_TestingOnly(context, gTextContext.get(), rtc.get(),
+                                              skPaint, viewMatrix, text, xInt, yInt);
 }
 
 #endif
diff --git a/src/gpu/text/GrAtlasTextContext.h b/src/gpu/text/GrAtlasTextContext.h
index 2f95d9a..1342ee7 100644
--- a/src/gpu/text/GrAtlasTextContext.h
+++ b/src/gpu/text/GrAtlasTextContext.h
@@ -55,6 +55,11 @@
                       const SkMatrix& viewMatrix, const SkSurfaceProps&, const SkTextBlob*,
                       SkScalar x, SkScalar y, SkDrawFilter*, const SkIRect& clipBounds);
 
+    std::unique_ptr<GrDrawOp> createOp_TestingOnly(GrContext*, GrAtlasTextContext*,
+                                                   GrRenderTargetContext*, const SkPaint&,
+                                                   const SkMatrix& viewMatrix, const char* text,
+                                                   int x, int y);
+
 private:
     GrAtlasTextContext(const Options& options);
 
diff --git a/src/gpu/text/GrGlyphCache.cpp b/src/gpu/text/GrGlyphCache.cpp
index 2372af8..49e24b8 100644
--- a/src/gpu/text/GrGlyphCache.cpp
+++ b/src/gpu/text/GrGlyphCache.cpp
@@ -292,7 +292,8 @@
     }
 }
 
-bool GrTextStrike::addGlyphToAtlas(GrResourceProvider* resourceProvider,
+GrDrawOpAtlas::ErrorCode GrTextStrike::addGlyphToAtlas(
+                                   GrResourceProvider* resourceProvider,
                                    GrDeferredUploadTarget* target,
                                    GrGlyphCache* glyphCache,
                                    GrAtlasManager* fullAtlasManager,
@@ -325,7 +326,7 @@
     if (isSDFGlyph) {
         if (!get_packed_glyph_df_image(cache, skGlyph, width, height,
                                        storage.get())) {
-            return false;
+            return GrDrawOpAtlas::ErrorCode::kError;
         }
     } else {
         void* dataPtr = storage.get();
@@ -336,15 +337,16 @@
         if (!get_packed_glyph_image(cache, skGlyph, glyph->width(), glyph->height(),
                                     rowBytes, expectedMaskFormat,
                                     dataPtr)) {
-            return false;
+            return GrDrawOpAtlas::ErrorCode::kError;
         }
     }
 
-    bool success = fullAtlasManager->addToAtlas(resourceProvider, glyphCache, this,
+    GrDrawOpAtlas::ErrorCode result = fullAtlasManager->addToAtlas(
+                                                resourceProvider, glyphCache, this,
                                                 &glyph->fID, target, expectedMaskFormat,
                                                 width, height,
                                                 storage.get(), &glyph->fAtlasLocation);
-    if (success) {
+    if (GrDrawOpAtlas::ErrorCode::kSucceeded == result) {
         if (addPad) {
             glyph->fAtlasLocation.fX += 1;
             glyph->fAtlasLocation.fY += 1;
@@ -352,5 +354,5 @@
         SkASSERT(GrDrawOpAtlas::kInvalidAtlasID != glyph->fID);
         fAtlasedGlyphs++;
     }
-    return success;
+    return result;
 }
diff --git a/src/gpu/text/GrGlyphCache.h b/src/gpu/text/GrGlyphCache.h
index 7e6056e..bbc199f 100644
--- a/src/gpu/text/GrGlyphCache.h
+++ b/src/gpu/text/GrGlyphCache.h
@@ -63,9 +63,10 @@
     // happen.
     // TODO we can handle some of these cases if we really want to, but the long term solution is to
     // get the actual glyph image itself when we get the glyph metrics.
-    bool addGlyphToAtlas(GrResourceProvider*, GrDeferredUploadTarget*, GrGlyphCache*,
-                         GrAtlasManager*, GrGlyph*,
-                         SkGlyphCache*, GrMaskFormat expectedMaskFormat, bool isScaledGlyph);
+    GrDrawOpAtlas::ErrorCode addGlyphToAtlas(GrResourceProvider*, GrDeferredUploadTarget*,
+                                             GrGlyphCache*, GrAtlasManager*, GrGlyph*,
+                                             SkGlyphCache*, GrMaskFormat expectedMaskFormat,
+                                             bool isScaledGlyph);
 
     // testing
     int countGlyphs() const { return fCache.count(); }
diff --git a/tests/DrawOpAtlasTest.cpp b/tests/DrawOpAtlasTest.cpp
index 7e35a07..a579063 100644
--- a/tests/DrawOpAtlasTest.cpp
+++ b/tests/DrawOpAtlasTest.cpp
@@ -28,6 +28,20 @@
     return count;
 }
 
+void GrAtlasManager::setMaxPages_TestingOnly(uint32_t numPages) {
+    for (int i = 0; i < kMaskFormatCount; i++) {
+        if (fAtlases[i]) {
+            fAtlases[i]->setMaxPages_TestingOnly(numPages);
+        }
+    }
+}
+
+void GrDrawOpAtlas::setMaxPages_TestingOnly(uint32_t maxPages) {
+    SkASSERT(!fNumActivePages);
+
+    fMaxPages = maxPages;
+}
+
 void EvictionFunc(GrDrawOpAtlas::AtlasID atlasID, void*) {
     SkASSERT(0); // The unit test shouldn't exercise this code path
 }
@@ -43,9 +57,8 @@
 public:
     TestingUploadTarget() { }
 
-    const GrTokenTracker* tokenTracker() final {
-        return &fTokenTracker;
-    }
+    const GrTokenTracker* tokenTracker() final { return &fTokenTracker; }
+    GrTokenTracker* writeableTokenTracker() { return &fTokenTracker; }
 
     GrDeferredUploadToken addInlineUpload(GrDeferredTextureUploadFn&&) final {
         SkASSERT(0); // this test shouldn't invoke this code path
@@ -77,13 +90,16 @@
     data.eraseARGB(alpha, 0, 0, 0);
 
     SkIPoint16 loc;
-    bool result = atlas->addToAtlas(resourceProvider, atlasID, target, kPlotSize, kPlotSize,
-                                    data.getAddr(0, 0), &loc);
-    return result;
+    GrDrawOpAtlas::ErrorCode code;
+    code = atlas->addToAtlas(resourceProvider, atlasID, target, kPlotSize, kPlotSize,
+                              data.getAddr(0, 0), &loc);
+    return GrDrawOpAtlas::ErrorCode::kSucceeded == code;
 }
 
 
-DEF_GPUTEST_FOR_RENDERING_CONTEXTS(DrawOpAtlas, reporter, ctxInfo) {
+// This is a basic DrawOpAtlas test. It simply verifies that multitexture atlases correctly
+// add and remove pages. Note that this is simulating flush-time behavior.
+DEF_GPUTEST_FOR_RENDERING_CONTEXTS(BasicDrawOpAtlas, reporter, ctxInfo) {
     auto context = ctxInfo.grContext();
     auto proxyProvider = context->contextPriv().proxyProvider();
     auto resourceProvider = context->contextPriv().resourceProvider();
@@ -129,4 +145,68 @@
     check(reporter, atlas.get(), 1, 4, 1);
 }
 
+#include "GrTest.h"
+
+#include "GrDrawingManager.h"
+#include "GrOpFlushState.h"
+#include "GrProxyProvider.h"
+
+#include "effects/GrConstColorProcessor.h"
+#include "ops/GrAtlasTextOp.h"
+#include "text/GrAtlasTextContext.h"
+
+// This test verifies that the GrAtlasTextOp::onPrepare method correctly handles a failure
+// when allocating an atlas page.
+DEF_GPUTEST_FOR_RENDERING_CONTEXTS(GrAtlasTextOpPreparation, reporter, ctxInfo) {
+
+    auto context = ctxInfo.grContext();
+
+    auto gpu = context->contextPriv().getGpu();
+    auto resourceProvider = context->contextPriv().resourceProvider();
+    auto drawingManager = context->contextPriv().drawingManager();
+    auto textContext = drawingManager->getAtlasTextContext();
+
+    auto rtc =  context->contextPriv().makeDeferredRenderTargetContext(SkBackingFit::kApprox,
+                                                                       32, 32,
+                                                                       kRGBA_8888_GrPixelConfig,
+                                                                       nullptr);
+
+    SkPaint paint;
+    paint.setColor(SK_ColorRED);
+    paint.setLCDRenderText(false);
+    paint.setAntiAlias(false);
+    paint.setSubpixelText(false);
+    GrTextUtils::Paint utilsPaint(&paint, &rtc->colorSpaceInfo());
+
+    const char* text = "a";
+
+    std::unique_ptr<GrDrawOp> op = textContext->createOp_TestingOnly(context, textContext,
+                                                                     rtc.get(), paint,
+                                                                     SkMatrix::I(), text,
+                                                                     16, 16);
+    op->finalize(*context->caps(), nullptr, GrPixelConfigIsClamped::kNo);
+
+    TestingUploadTarget uploadTarget;
+
+    GrOpFlushState flushState(gpu, resourceProvider, uploadTarget.writeableTokenTracker());
+    GrOpFlushState::OpArgs opArgs = {
+        op.get(),
+        rtc->asRenderTargetProxy(),
+        nullptr,
+        GrXferProcessor::DstProxy(nullptr, SkIPoint::Make(0, 0))
+    };
+
+    // Cripple the atlas manager so it can't allocate any pages. This will force a failure
+    // in the preparation of the text op
+    auto atlasManager = context->contextPriv().getFullAtlasManager();
+    unsigned int numProxies;
+    atlasManager->getProxies(kA8_GrMaskFormat, &numProxies);
+    atlasManager->setMaxPages_TestingOnly(0);
+
+    flushState.setOpArgs(&opArgs);
+    op->prepare(&flushState);
+    flushState.setOpArgs(nullptr);
+}
+
+
 #endif
diff --git a/tools/gpu/GrTest.cpp b/tools/gpu/GrTest.cpp
index 3e9f73a..aa2b07e 100644
--- a/tools/gpu/GrTest.cpp
+++ b/tools/gpu/GrTest.cpp
@@ -31,10 +31,9 @@
 
 namespace GrTest {
 
-void SetupAlwaysEvictAtlas(GrContext* context) {
+void SetupAlwaysEvictAtlas(GrContext* context, int dim) {
     // These sizes were selected because they allow each atlas to hold a single plot and will thus
     // stress the atlas
-    int dim = GrDrawOpAtlas::kGlyphMaxDim;
     GrDrawOpAtlasConfig configs[3];
     configs[kA8_GrMaskFormat].fWidth = dim;
     configs[kA8_GrMaskFormat].fHeight = dim;
diff --git a/tools/gpu/GrTest.h b/tools/gpu/GrTest.h
index 5d988c7..6666ab1 100644
--- a/tools/gpu/GrTest.h
+++ b/tools/gpu/GrTest.h
@@ -10,13 +10,14 @@
 
 #include "GrBackendSurface.h"
 #include "GrContext.h"
+#include "GrDrawOpAtlas.h"
 
 namespace GrTest {
     /**
      * Forces the GrContext to use a small atlas which only has room for one plot and will thus
      * constantly be evicting entries
      */
-    void SetupAlwaysEvictAtlas(GrContext*);
+    void SetupAlwaysEvictAtlas(GrContext*, int dim = GrDrawOpAtlas::kGlyphMaxDim);
 
     // TODO: remove this. It is only used in the SurfaceSemaphores Test.
     GrBackendTexture CreateBackendTexture(GrBackend, int width, int height,