[graphite] Add caching functionality to ClipAtlasManager

Bug: b/388809647
Bug: b/388808216
Change-Id: I9908a0059d4f39cfc2707049afb640d92a95fca9
Reviewed-on: https://skia-review.googlesource.com/c/skia/+/937417
Reviewed-by: Michael Ludwig <michaelludwig@google.com>
Commit-Queue: Jim Van Verth <jvanverth@google.com>
diff --git a/src/gpu/graphite/ClipAtlasManager.cpp b/src/gpu/graphite/ClipAtlasManager.cpp
index 8a65dca..128d107 100644
--- a/src/gpu/graphite/ClipAtlasManager.cpp
+++ b/src/gpu/graphite/ClipAtlasManager.cpp
@@ -9,6 +9,7 @@
 
 #include "include/gpu/graphite/Recorder.h"
 #include "src/gpu/graphite/AtlasProvider.h"
+#include "src/gpu/graphite/RasterPathUtils.h"
 #include "src/gpu/graphite/RecorderPriv.h"
 #include "src/gpu/graphite/TextureProxy.h"
 
@@ -38,36 +39,187 @@
     }
 }
 
-std::tuple<const TextureProxy*, Rect> ClipAtlasManager::findClip(const UniqueKey&) {
-    // TODO
-    return {nullptr, {}};
+namespace {
+// TODO: is this necessary for clips?
+constexpr int kEntryPadding = 1;
+}  // namespace
+
+const TextureProxy* ClipAtlasManager::findOrCreateEntry(uint32_t stackRecordID,
+                                                        const ClipStack::ElementList* elementList,
+                                                        const Rect& bounds,
+                                                        skvx::half2* outPos) {
+    skgpu::UniqueKey maskKey = GenerateClipMaskKey(stackRecordID, elementList);
+
+    MaskHashArray* cachedArray = fMaskCache.find(maskKey);
+    if (cachedArray) {
+        for (int i = 0; i < cachedArray->size(); ++i) {
+            MaskHashEntry& entry = (*cachedArray)[i];
+            // We can reuse a clip mask if has the same key and our bounds is contained in it
+            if (entry.fBounds.contains(bounds)) {
+                SkIPoint topLeft = entry.fLocator.topLeft();
+                // We need to adjust the returned outPos to reflect the subset we're using
+                skvx::float2 subsetRelativePos = bounds.topLeft() - entry.fBounds.topLeft();
+                *outPos = skvx::half2(topLeft.x() + kEntryPadding + subsetRelativePos.x(),
+                                      topLeft.y() + kEntryPadding + subsetRelativePos.y());
+                fDrawAtlas->setLastUseToken(entry.fLocator,
+                                            fRecorder->priv().tokenTracker()->nextFlushToken());
+                return fDrawAtlas->getProxies()[entry.fLocator.pageIndex()].get();
+            }
+        }
+    }
+
+    AtlasLocator locator;
+    const TextureProxy* proxy = this->addToAtlas(elementList, bounds, outPos, &locator);
+    if (!proxy) {
+        return nullptr;
+    }
+
+    // Add locator and bounds to MaskCache.
+    if (cachedArray) {
+        cachedArray->push_back({bounds, locator});
+    } else {
+        MaskHashArray initialArray;
+        initialArray.push_back({bounds, locator});
+        fMaskCache.set(maskKey, initialArray);
+    }
+    // Add key to Plot's MaskKeyList.
+    uint32_t index = fDrawAtlas->getListIndex(locator.plotLocator());
+    MaskKeyEntry* keyEntry = new MaskKeyEntry();
+    keyEntry->fKey = maskKey;
+    keyEntry->fBounds = bounds;
+    fKeyLists[index].addToTail(keyEntry);
+
+    return proxy;
 }
 
-std::tuple<const TextureProxy*, Rect> ClipAtlasManager::addClip(const UniqueKey&, Rect bounds,
-                                                                const ClipStack::ElementList*) {
-    // TODO
-    return {nullptr, {}};
+// Copied and modified from Ganesh ClipStack
+void draw_to_sw_mask(RasterMaskHelper* helper,
+                     const ClipStack::Element& e,
+                     bool clearMask,
+                     const SkIRect& resultBounds) {
+    // If the first element to draw is an intersect, we clear to 0 and will draw it directly with
+    // coverage 1 (subsequent intersect elements will be inverse-filled and draw 0 outside).
+    // If the first element to draw is a difference, we clear to 1, and in all cases we draw the
+    // difference element directly with coverage 0.
+    if (clearMask) {
+        helper->clear(e.fOp == SkClipOp::kIntersect ? 0x00 : 0xFF, resultBounds);
+    }
+
+    uint8_t alpha;
+    bool invert;
+    if (e.fOp == SkClipOp::kIntersect) {
+        // Intersect modifies pixels outside of its geometry. If this isn't the first op, we
+        // draw the inverse-filled shape with 0 coverage to erase everything outside the element
+        // But if we are the first element, we can draw directly with coverage 1 since we
+        // cleared to 0.
+        if (clearMask) {
+            alpha = 0xFF;
+            invert = false;
+        } else {
+            alpha = 0x00;
+            invert = true;
+        }
+    } else {
+        // For difference ops, can always just subtract the shape directly by drawing 0 coverage
+        SkASSERT(e.fOp == SkClipOp::kDifference);
+        alpha = 0x00;
+        invert = false;
+    }
+
+    // Draw the shape; based on how we've initialized the buffer and chosen alpha+invert,
+    // every element is drawn with the kReplace_Op
+    if (invert) {
+        // Must invert the path
+        SkASSERT(!e.fShape.inverted());
+        // TODO: this is an extra copy effectively, just so we can toggle inversion; would be
+        // better perhaps to just call a drawPath() since we know it'll use path rendering w/
+        // the inverse fill type.
+        Shape inverted(e.fShape);
+        inverted.setInverted(true);
+        helper->drawClip(inverted, e.fLocalToDevice, alpha, resultBounds);
+    } else {
+        helper->drawClip(e.fShape, e.fLocalToDevice, alpha, resultBounds);
+    }
+}
+
+const TextureProxy* ClipAtlasManager::addToAtlas(const ClipStack::ElementList* elementsForMask,
+                                                 const Rect& bounds,
+                                                 skvx::half2* outPos,
+                                                 AtlasLocator* locator) {
+    // Render mask.
+    skvx::float2 maskSize = bounds.size();
+    if (!all(maskSize)) {
+        return nullptr;
+    }
+
+    SkIRect iShapeBounds = SkIRect::MakeXYWH(0, 0, maskSize.x(), maskSize.y());
+    // Outset to take padding into account
+    SkIRect iAtlasBounds = iShapeBounds.makeOutset(kEntryPadding, kEntryPadding);
+
+    // Request space in DrawAtlas.
+    DrawAtlas::ErrorCode errorCode = fDrawAtlas->addRect(fRecorder,
+                                                         iAtlasBounds.width(),
+                                                         iAtlasBounds.height(),
+                                                         locator);
+    if (errorCode != DrawAtlas::ErrorCode::kSucceeded) {
+        return nullptr;
+    }
+    SkIPoint topLeft = locator->topLeft();
+    *outPos = skvx::half2(topLeft.x() + kEntryPadding, topLeft.y() + kEntryPadding);
+
+    // Rasterize path to backing pixmap.
+    // This pixmap will be the size of the Plot that contains the given rect, not the entire atlas,
+    // and hence the position we render at will be relative to that Plot.
+    // The value of outPos is relative to the entire texture, to be used for texture coords.
+    SkAutoPixmapStorage dst;
+    SkIPoint renderPos = fDrawAtlas->prepForRender(*locator, &dst);
+
+    // This will remove the base integer translation from each element's transform
+    Rect iBounds = bounds.makeRoundOut();
+    RasterMaskHelper helper(&dst);
+    if (!helper.init(fDrawAtlas->plotSize(), iBounds.topLeft())) {
+        return nullptr;
+    }
+
+    // Offset to plot location and draw
+    iShapeBounds.offset(renderPos.x() + kEntryPadding, renderPos.y() + kEntryPadding);
+
+    SkASSERT(elementsForMask->size() > 0);
+    for (int i = 0; i < elementsForMask->size(); ++i) {
+        draw_to_sw_mask(&helper, *(*elementsForMask)[i], i == 0, iShapeBounds);
+    }
+
+    fDrawAtlas->setLastUseToken(*locator,
+                                fRecorder->priv().tokenTracker()->nextFlushToken());
+
+    return fDrawAtlas->getProxies()[locator->pageIndex()].get();
 }
 
 bool ClipAtlasManager::recordUploads(DrawContext* dc) {
     return (fDrawAtlas && !fDrawAtlas->recordUploads(dc, fRecorder));
 }
 
-namespace {
-uint32_t mask_key_list_index(const PlotLocator& locator, const DrawAtlas* drawAtlas) {
-    return locator.pageIndex() * drawAtlas->numPlots() + locator.plotIndex();
-}
-}  // namespace
-
 void ClipAtlasManager::evict(PlotLocator plotLocator) {
     // Remove all entries for this Plot from the MaskCache
-    uint32_t index = mask_key_list_index(plotLocator, fDrawAtlas.get());
+    uint32_t index = fDrawAtlas->getListIndex(plotLocator);
     MaskKeyList::Iter iter;
     iter.init(fKeyLists[index], MaskKeyList::Iter::kHead_IterStart);
     MaskKeyEntry* currEntry;
     while ((currEntry = iter.get())) {
         iter.next();
-        fMaskCache.remove(currEntry->fKey);
+        MaskHashArray* cachedArray = fMaskCache.find(currEntry->fKey);
+        // Remove this entry from the hashed array
+        for (int i = cachedArray->size()-1; i >=0 ; --i) {
+            MaskHashEntry& entry = (*cachedArray)[i];
+            if (entry.fBounds == currEntry->fBounds) {
+                (*cachedArray).removeShuffle(i);
+                break;
+            }
+        }
+        // If we removed the last one, remove the hash entry
+        if (cachedArray->size() == 0) {
+            fMaskCache.remove(currEntry->fKey);
+        }
         fKeyLists[index].remove(currEntry);
         delete currEntry;
     }
diff --git a/src/gpu/graphite/ClipAtlasManager.h b/src/gpu/graphite/ClipAtlasManager.h
index 11e0ca7..a791303 100644
--- a/src/gpu/graphite/ClipAtlasManager.h
+++ b/src/gpu/graphite/ClipAtlasManager.h
@@ -28,9 +28,14 @@
     ClipAtlasManager(Recorder* recorder);
     ~ClipAtlasManager() override = default;
 
-    std::tuple<const TextureProxy*, Rect> findClip(const UniqueKey&);
-    std::tuple<const TextureProxy*, Rect> addClip(const UniqueKey&, Rect bounds,
-                                                  const ClipStack::ElementList*);
+    const TextureProxy* findOrCreateEntry(uint32_t stackRecordID,
+                                          const ClipStack::ElementList*,
+                                          const Rect& bounds,
+                                          skvx::half2* outPos);
+    const TextureProxy* addToAtlas(const ClipStack::ElementList*,
+                                   const Rect& bounds,
+                                   skvx::half2* outPos,
+                                   AtlasLocator* locator);
 
     bool recordUploads(DrawContext* dc);
     void evict(PlotLocator) override;
@@ -42,11 +47,17 @@
     Recorder* fRecorder;
     std::unique_ptr<DrawAtlas> fDrawAtlas;
 
-    // Tracks whether a clip mask is already in the DrawAtlas, and its location in the atlas
+    // Tracks whether a combined clip mask is already in the DrawAtlas and its location in the atlas
+    struct MaskHashEntry {
+        Rect fBounds;
+        AtlasLocator fLocator;
+        SK_DECLARE_INTERNAL_LLIST_INTERFACE(MaskHashEntry);
+    };
+    using MaskHashArray = SkTDArray<MaskHashEntry>;
     struct UniqueKeyHash {
         uint32_t operator()(const skgpu::UniqueKey& key) const { return key.hash(); }
     };
-    using MaskCache = skia_private::THashMap<skgpu::UniqueKey, AtlasLocator, UniqueKeyHash>;
+    using MaskCache = skia_private::THashMap<skgpu::UniqueKey, MaskHashArray, UniqueKeyHash>;
     MaskCache fMaskCache;
 
     // List of stored keys per Plot, used to invalidate cache entries.
@@ -55,6 +66,7 @@
     // then iterate through the list and remove entries matching those keys from the MaskCache.
     struct MaskKeyEntry {
         skgpu::UniqueKey fKey;
+        Rect fBounds;
         SK_DECLARE_INTERNAL_LLIST_INTERFACE(MaskKeyEntry);
     };
     using MaskKeyList = SkTInternalLList<MaskKeyEntry>;
diff --git a/src/gpu/graphite/DrawAtlas.h b/src/gpu/graphite/DrawAtlas.h
index 3fc169d..d81e192 100644
--- a/src/gpu/graphite/DrawAtlas.h
+++ b/src/gpu/graphite/DrawAtlas.h
@@ -124,6 +124,9 @@
     uint32_t numActivePages() const { return fNumActivePages; }
     unsigned int numPlots() const { return fNumPlots; }
     SkISize plotSize() const { return {fPlotWidth, fPlotHeight}; }
+    uint32_t getListIndex(const PlotLocator& locator) {
+        return locator.pageIndex() * fNumPlots + locator.plotIndex();
+    }
 
     bool hasID(const PlotLocator& plotLocator) {
         if (!plotLocator.isValid()) {
diff --git a/src/gpu/graphite/PathAtlas.cpp b/src/gpu/graphite/PathAtlas.cpp
index 7f28a5f..27adba9 100644
--- a/src/gpu/graphite/PathAtlas.cpp
+++ b/src/gpu/graphite/PathAtlas.cpp
@@ -103,12 +103,6 @@
     }
 }
 
-namespace {
-uint32_t shape_key_list_index(const PlotLocator& locator, const DrawAtlas* drawAtlas) {
-    return locator.pageIndex() * drawAtlas->numPlots() + locator.plotIndex();
-}
-}  // namespace
-
 const TextureProxy* PathAtlas::DrawAtlasMgr::findOrCreateEntry(Recorder* recorder,
                                                                const Shape& shape,
                                                                const Transform& localToDevice,
@@ -139,7 +133,7 @@
     // Add locator to ShapeCache.
     fShapeCache.set(maskKey, locator);
     // Add key to Plot's ShapeKeyList.
-    uint32_t index = shape_key_list_index(locator.plotLocator(), fDrawAtlas.get());
+    uint32_t index = fDrawAtlas->getListIndex(locator.plotLocator());
     ShapeKeyEntry* keyEntry = new ShapeKeyEntry();
     keyEntry->fKey = maskKey;
     fKeyLists[index].addToTail(keyEntry);
@@ -197,7 +191,7 @@
 
 void PathAtlas::DrawAtlasMgr::evict(PlotLocator plotLocator) {
     // Remove all entries for this Plot from the ShapeCache
-    uint32_t index = shape_key_list_index(plotLocator, fDrawAtlas.get());
+    uint32_t index = fDrawAtlas->getListIndex(plotLocator);
     ShapeKeyList::Iter iter;
     iter.init(fKeyLists[index], ShapeKeyList::Iter::kHead_IterStart);
     ShapeKeyEntry* currEntry;
diff --git a/src/gpu/graphite/RasterPathUtils.cpp b/src/gpu/graphite/RasterPathUtils.cpp
index 1ce5d29..5d1548a 100644
--- a/src/gpu/graphite/RasterPathUtils.cpp
+++ b/src/gpu/graphite/RasterPathUtils.cpp
@@ -60,6 +60,7 @@
                                    resultBounds.y() - fTransformedMaskOffset.y());
 
     fDraw.fCTM = &translatedMatrix;
+    // TODO: use drawRect, drawRRect, drawArc
     SkPath path = shape.asPath();
     if (path.isInverseFillType()) {
         // The shader will handle the inverse fill in this case
@@ -68,6 +69,69 @@
     fDraw.drawPathCoverage(path, paint);
 }
 
+void RasterMaskHelper::drawClip(const Shape& shape,
+                                const Transform& transform,
+                                uint8_t alpha,
+                                const SkIRect& resultBounds) {
+    fRasterClip.setRect(resultBounds);
+
+    SkPaint paint;
+    paint.setBlendMode(SkBlendMode::kSrc);  // "Replace" mode
+    paint.setAntiAlias(true);
+    // SkPaint's color is unpremul so this will produce alpha in every channel.
+    paint.setColor(SkColorSetARGB(alpha, 0xFF, 0xFF, 0xFF));
+
+    SkMatrix translatedMatrix = SkMatrix(transform);
+    // The atlas transform of the shape is the linear-components (scale, rotation, skew) of
+    // `localToDevice` translated by the top-left offset of the resultBounds.
+    // We will need to translate draws so the bound's UL corner is at the origin
+    translatedMatrix.postTranslate(resultBounds.x(), resultBounds.y());
+
+    fDraw.fCTM = &translatedMatrix;
+    // TODO: use drawRect, drawRRect, drawArc
+    SkPath path = shape.asPath();
+    // Because we could be combining multiple paths into one entry we don't touch
+    // the inverse fill in this case.
+    if (0xFF == alpha) {
+        SkASSERT(0xFF == paint.getAlpha());
+        fDraw.drawPathCoverage(path, paint);
+    } else {
+        fDraw.drawPath(path, paint, nullptr, true);
+    }
+}
+
+uint32_t add_transform_key(skgpu::UniqueKey::Builder* builder,
+                           int startIndex,
+                           const Transform& transform) {
+    // We require the upper left 2x2 of the matrix to match exactly for a cache hit.
+    SkMatrix mat = transform.matrix().asM33();
+    SkScalar sx = mat.get(SkMatrix::kMScaleX);
+    SkScalar sy = mat.get(SkMatrix::kMScaleY);
+    SkScalar kx = mat.get(SkMatrix::kMSkewX);
+    SkScalar ky = mat.get(SkMatrix::kMSkewY);
+#ifdef SK_BUILD_FOR_ANDROID_FRAMEWORK
+    // Fractional translate does not affect caching on Android. This is done for better cache
+    // hit ratio and speed and is matching HWUI behavior, which didn't consider the matrix
+    // at all when caching paths.
+    SkFixed fracX = 0;
+    SkFixed fracY = 0;
+#else
+    SkScalar tx = mat.get(SkMatrix::kMTransX);
+    SkScalar ty = mat.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;
+#endif
+    (*builder)[startIndex + 0] = SkFloat2Bits(sx);
+    (*builder)[startIndex + 1] = SkFloat2Bits(sy);
+    (*builder)[startIndex + 2] = SkFloat2Bits(kx);
+    (*builder)[startIndex + 3] = SkFloat2Bits(ky);
+    // FracX and fracY are &ed with 0x0000ff00, so need to shift one down to fill 16 bits.
+    uint32_t fracBits = fracX | (fracY >> 8);
+
+    return fracBits;
+}
+
 skgpu::UniqueKey GeneratePathMaskKey(const Shape& shape,
                                      const Transform& transform,
                                      const SkStrokeRec& strokeRec,
@@ -86,31 +150,8 @@
         builder[0] = maskOrigin.x() | (maskOrigin.y() << 16);
         builder[1] = maskSize.x() | (maskSize.y() << 16);
 
-        // We require the upper left 2x2 of the matrix to match exactly for a cache hit.
-        SkMatrix mat = transform.matrix().asM33();
-        SkScalar sx = mat.get(SkMatrix::kMScaleX);
-        SkScalar sy = mat.get(SkMatrix::kMScaleY);
-        SkScalar kx = mat.get(SkMatrix::kMSkewX);
-        SkScalar ky = mat.get(SkMatrix::kMSkewY);
-#ifdef SK_BUILD_FOR_ANDROID_FRAMEWORK
-        // Fractional translate does not affect caching on Android. This is done for better cache
-        // hit ratio and speed and is matching HWUI behavior, which didn't consider the matrix
-        // at all when caching paths.
-        SkFixed fracX = 0;
-        SkFixed fracY = 0;
-#else
-        SkScalar tx = mat.get(SkMatrix::kMTransX);
-        SkScalar ty = mat.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;
-#endif
-        builder[2] = SkFloat2Bits(sx);
-        builder[3] = SkFloat2Bits(sy);
-        builder[4] = SkFloat2Bits(kx);
-        builder[5] = SkFloat2Bits(ky);
-        // FracX and fracY are &ed with 0x0000ff00, so need to shift one down to fill 16 bits.
-        uint32_t fracBits = fracX | (fracY >> 8);
+        // Add transform key and get packed fractional translation bits
+        uint32_t fracBits = add_transform_key(&builder, 2, transform);
         // Distinguish between path styles. For anything but fill, we also need to include
         // the cap. (SW grows hairlines by 0.5 pixel with round and square caps). For stroke
         // or fill-and-stroke we need to include the join, width, and miter.
@@ -132,4 +173,45 @@
     return maskKey;
 }
 
+skgpu::UniqueKey GenerateClipMaskKey(uint32_t stackRecordID,
+                                     const ClipStack::ElementList* elementsForMask) {
+    skgpu::UniqueKey maskKey;
+    {
+        static constexpr int kMaxShapeCountForKey = 2;
+
+        static const skgpu::UniqueKey::Domain kDomain = skgpu::UniqueKey::GenerateDomain();
+        // if the element list is too large we just use the stackRecordID
+        if (elementsForMask->size() > kMaxShapeCountForKey) {
+            skgpu::UniqueKey::Builder builder(&maskKey, kDomain, 1, "Clip Path Mask");
+            builder[0] = stackRecordID;
+        } else {
+            int xformKeySize = 5;
+            int keySize = 0;
+            for (int i = 0; i < elementsForMask->size(); ++i) {
+                keySize += xformKeySize + (*elementsForMask)[i]->fShape.keySize();
+            }
+            skgpu::UniqueKey::Builder builder(&maskKey, kDomain, keySize,
+                                              "Clip Path Mask");
+            int elementKeyIndex = 0;
+            for (int i = 0; i < elementsForMask->size(); ++i) {
+                // Add transform key and get packed fractional translation bits
+                uint32_t fracBits = add_transform_key(&builder,
+                                                      elementKeyIndex,
+                                                      (*elementsForMask)[i]->fLocalToDevice);
+                uint32_t opBits = static_cast<uint32_t>((*elementsForMask)[i]->fOp);
+                builder[elementKeyIndex + 4] = fracBits | (opBits << 16);
+
+                const Shape& shape = (*elementsForMask)[i]->fShape;
+                shape.writeKey(&builder[elementKeyIndex + xformKeySize], /*includeInverted=*/true);
+
+                elementKeyIndex += xformKeySize + shape.keySize();
+            }
+        }
+
+    }
+
+    return maskKey;
+
+}
+
 }  // namespace skgpu::graphite
diff --git a/src/gpu/graphite/RasterPathUtils.h b/src/gpu/graphite/RasterPathUtils.h
index baa4cea..e431fc6 100644
--- a/src/gpu/graphite/RasterPathUtils.h
+++ b/src/gpu/graphite/RasterPathUtils.h
@@ -14,6 +14,7 @@
 #include "src/core/SkDrawBase.h"
 #include "src/core/SkRasterClip.h"
 #include "src/gpu/ResourceKey.h"
+#include "src/gpu/graphite/ClipStack_graphite.h"
 
 namespace skgpu::graphite {
 
@@ -40,12 +41,25 @@
 
     bool init(SkISize pixmapSize, skvx::float2 transformedMaskOffset);
 
-    // Draw a single shape into the bitmap (as a path) at location resultBounds
+    void clear(uint8_t alpha, const SkIRect& resultBounds) {
+        SkPaint paint;
+        paint.setColor(SkColorSetARGB(alpha, 0xFF, 0xFF, 0xFF));
+        fDraw.drawRect(SkRect::Make(resultBounds), paint);
+    }
+
+    // Draw a single shape into the bitmap (as a path) at location resultBounds.
     void drawShape(const Shape& shape,
                    const Transform& localToDevice,
                    const SkStrokeRec& strokeRec,
                    const SkIRect& resultBounds);
 
+    // Draw a single shape into the bitmap (as a path) at location resultBounds.
+    // Variant used for clipping.
+    void drawClip(const Shape& shape,
+                  const Transform& transform,
+                  uint8_t alpha,
+                  const SkIRect& resultBounds);
+
 private:
     SkAutoPixmapStorage* fPixels;
     SkDrawBase           fDraw;
@@ -58,6 +72,10 @@
                                      const SkStrokeRec& strokeRec,
                                      skvx::half2 maskOrigin,
                                      skvx::half2 maskSize);
+
+skgpu::UniqueKey GenerateClipMaskKey(uint32_t stackRecordID,
+                                     const ClipStack::ElementList* elementsForMask);
+
 }  // namespace skgpu::graphite
 
 #endif  // skgpu_graphite_RasterPathUtils_DEFINED