Add idle texture callback mechanism.

A proc can be registered with a GrTexture. The proc will be called when
it is safe to delete the texture is "idle." Idle means it referred to
outside of GrResourceCache and that the I/O operations on the GPU are
completed (this latter part applieas to Vulkan only).

The intended use case for this is to call promise image texture release
procs once we start caching GrTextures for deinstantiated promise
images.

Bug= skia:8613

Change-Id: Idce9a4292fef7b15370a053060d8878a9d6828fa
Reviewed-on: https://skia-review.googlesource.com/c/178937
Reviewed-by: Greg Daniel <egdaniel@google.com>
Reviewed-by: Robert Phillips <robertphillips@google.com>
Commit-Queue: Brian Salomon <bsalomon@google.com>
diff --git a/src/gpu/GrContext.cpp b/src/gpu/GrContext.cpp
index ff386a3..ee7fa89 100644
--- a/src/gpu/GrContext.cpp
+++ b/src/gpu/GrContext.cpp
@@ -266,6 +266,9 @@
 void GrContext::releaseResourcesAndAbandonContext() {
     ASSERT_SINGLE_OWNER
 
+    if (this->abandoned()) {
+        return;
+    }
     fProxyProvider->abandon();
     fResourceProvider->abandon();
 
diff --git a/src/gpu/GrGpuResourceCacheAccess.h b/src/gpu/GrGpuResourceCacheAccess.h
index 8e9a701..ce3b597 100644
--- a/src/gpu/GrGpuResourceCacheAccess.h
+++ b/src/gpu/GrGpuResourceCacheAccess.h
@@ -35,6 +35,12 @@
     bool shouldPurgeImmediately() const { return fResource->fShouldPurgeImmediately; }
 
     /**
+     * Called by GrResourceCache when a resource becomes purgeable regardless of whether the cache
+     * has decided to keep the resource ot purge it immediately.
+     */
+    void becamePurgeable() { fResource->becamePurgeable(); }
+
+    /**
      * Called by the cache to delete the resource under normal circumstances.
      */
     void release() {
diff --git a/src/gpu/GrGpuResourcePriv.h b/src/gpu/GrGpuResourcePriv.h
index affc753..9537ffb 100644
--- a/src/gpu/GrGpuResourcePriv.h
+++ b/src/gpu/GrGpuResourcePriv.h
@@ -69,6 +69,8 @@
      */
     void removeScratchKey() const { fResource->removeScratchKey();  }
 
+    bool isPurgeable() const { return fResource->isPurgeable(); }
+
 protected:
     ResourcePriv(GrGpuResource* resource) : fResource(resource) {   }
     ResourcePriv(const ResourcePriv& that) : fResource(that.fResource) {}
diff --git a/src/gpu/GrResourceCache.cpp b/src/gpu/GrResourceCache.cpp
index ffadc30..dceb87e 100644
--- a/src/gpu/GrResourceCache.cpp
+++ b/src/gpu/GrResourceCache.cpp
@@ -6,10 +6,11 @@
  */
 
 #include "GrResourceCache.h"
+#include <atomic>
 #include "GrCaps.h"
-#include "GrSingleOwner.h"
 #include "GrGpuResourceCacheAccess.h"
 #include "GrProxyProvider.h"
+#include "GrSingleOwner.h"
 #include "GrTexture.h"
 #include "GrTextureProxyCacheAccess.h"
 #include "GrTracing.h"
@@ -17,9 +18,9 @@
 #include "SkMessageBus.h"
 #include "SkOpts.h"
 #include "SkRandom.h"
+#include "SkScopeExit.h"
 #include "SkTSort.h"
 #include "SkTo.h"
-#include <atomic>
 
 DECLARE_SKMESSAGEBUS_MESSAGE(GrUniqueKeyInvalidatedMessage);
 
@@ -109,7 +110,7 @@
     SkASSERT(resource);
     SkASSERT(!this->isInCache(resource));
     SkASSERT(!resource->wasDestroyed());
-    SkASSERT(!resource->isPurgeable());
+    SkASSERT(!resource->resourcePriv().isPurgeable());
 
     // We must set the timestamp before adding to the array in case the timestamp wraps and we wind
     // up iterating over all the resources that already have timestamps.
@@ -149,7 +150,7 @@
     SkASSERT(this->isInCache(resource));
 
     size_t size = resource->gpuMemorySize();
-    if (resource->isPurgeable()) {
+    if (resource->resourcePriv().isPurgeable()) {
         fPurgeableQueue.remove(resource);
         fPurgeableBytes -= size;
     } else {
@@ -186,6 +187,9 @@
     while (fNonpurgeableResources.count()) {
         GrGpuResource* back = *(fNonpurgeableResources.end() - 1);
         SkASSERT(!back->wasDestroyed());
+        // If these resources we're relying on a purgeable notification to release something, notify
+        // them now. They aren't in the purgeable queue but they're getting purged anyway.
+        back->cacheAccess().becamePurgeable();
         back->cacheAccess().abandon();
     }
 
@@ -223,9 +227,12 @@
     // they also have a raw pointer back to this class (which is presumably going away)!
     fProxyProvider->removeAllUniqueKeys();
 
-    while(fNonpurgeableResources.count()) {
+    while (fNonpurgeableResources.count()) {
         GrGpuResource* back = *(fNonpurgeableResources.end() - 1);
         SkASSERT(!back->wasDestroyed());
+        // If these resources we're relying on a purgeable notification to release something, notify
+        // them now. They aren't in the purgeable queue but they're getting purged anyway.
+        back->cacheAccess().becamePurgeable();
         back->cacheAccess().release();
     }
 
@@ -329,7 +336,8 @@
     if (newKey.isValid()) {
         if (GrGpuResource* old = fUniqueHash.find(newKey)) {
             // If the old resource using the key is purgeable and is unreachable, then remove it.
-            if (!old->resourcePriv().getScratchKey().isValid() && old->isPurgeable()) {
+            if (!old->resourcePriv().getScratchKey().isValid() &&
+                old->resourcePriv().isPurgeable()) {
                 old->cacheAccess().release();
             } else {
                 this->removeUniqueKey(old);
@@ -364,7 +372,7 @@
     SkASSERT(resource);
     SkASSERT(this->isInCache(resource));
 
-    if (resource->isPurgeable()) {
+    if (resource->resourcePriv().isPurgeable()) {
         // It's about to become unpurgeable.
         fPurgeableBytes -= resource->gpuMemorySize();
         fPurgeableQueue.remove(resource);
@@ -391,7 +399,7 @@
         // When the timestamp overflows validate() is called. validate() checks that resources in
         // the nonpurgeable array are indeed not purgeable. However, the movement from the array to
         // the purgeable queue happens just below in this function. So we mark it as an exception.
-        if (resource->isPurgeable()) {
+        if (resource->resourcePriv().isPurgeable()) {
             fNewlyPurgeableResourceForValidation = resource;
         }
 #endif
@@ -400,11 +408,11 @@
     }
 
     if (!SkToBool(ResourceAccess::kAllCntsReachedZero_RefNotificationFlag & flags)) {
-        SkASSERT(!resource->isPurgeable());
+        SkASSERT(!resource->resourcePriv().isPurgeable());
         return;
     }
 
-    SkASSERT(resource->isPurgeable());
+    SkASSERT(resource->resourcePriv().isPurgeable());
     this->removeFromNonpurgeableArray(resource);
     fPurgeableQueue.insert(resource);
     resource->cacheAccess().setTimeWhenResourceBecomePurgeable();
@@ -412,32 +420,34 @@
 
     bool hasUniqueKey = resource->getUniqueKey().isValid();
 
-    if (SkBudgeted::kNo == resource->resourcePriv().isBudgeted()) {
-        // We keep unbudgeted resources with a unique key in the purgable queue of the cache so they
-        // can be reused again by the image connected to the unique key.
-        if (hasUniqueKey && !resource->cacheAccess().shouldPurgeImmediately()) {
-            return;
-        }
-        // Check whether this resource could still be used as a scratch resource.
-        if (!resource->resourcePriv().refsWrappedObjects() &&
-            resource->resourcePriv().getScratchKey().isValid()) {
-            // We won't purge an existing resource to make room for this one.
-            if (fBudgetedCount < fMaxCount &&
-                fBudgetedBytes + resource->gpuMemorySize() <= fMaxBytes) {
-                resource->resourcePriv().makeBudgeted();
+    {
+        SkScopeExit notifyPurgeable([resource] { resource->cacheAccess().becamePurgeable(); });
+
+        if (SkBudgeted::kNo == resource->resourcePriv().isBudgeted()) {
+            // We keep unbudgeted resources with a unique key in the purgable queue of the cache so
+            // they can be reused again by the image connected to the unique key.
+            if (hasUniqueKey && !resource->cacheAccess().shouldPurgeImmediately()) {
+                return;
+            }
+            // Check whether this resource could still be used as a scratch resource.
+            if (!resource->resourcePriv().refsWrappedObjects() &&
+                resource->resourcePriv().getScratchKey().isValid()) {
+                // We won't purge an existing resource to make room for this one.
+                if (fBudgetedCount < fMaxCount &&
+                    fBudgetedBytes + resource->gpuMemorySize() <= fMaxBytes) {
+                    resource->resourcePriv().makeBudgeted();
+                    return;
+                }
+            }
+        } else {
+            // Purge the resource immediately if we're over budget
+            // Also purge if the resource has neither a valid scratch key nor a unique key.
+            bool hasKey = resource->resourcePriv().getScratchKey().isValid() || hasUniqueKey;
+            if (!this->overBudget() && hasKey) {
                 return;
             }
         }
-    } else {
-        // Purge the resource immediately if we're over budget
-        // Also purge if the resource has neither a valid scratch key nor a unique key.
-        bool hasKey = resource->resourcePriv().getScratchKey().isValid() ||
-                      hasUniqueKey;
-        if (!this->overBudget() && hasKey) {
-            return;
-        }
     }
-
     SkDEBUGCODE(int beforeCount = this->getResourceCount();)
     resource->cacheAccess().release();
     // We should at least free this resource, perhaps dependent resources as well.
@@ -482,7 +492,7 @@
     bool stillOverbudget = this->overBudget();
     while (stillOverbudget && fPurgeableQueue.count()) {
         GrGpuResource* resource = fPurgeableQueue.peek();
-        SkASSERT(resource->isPurgeable());
+        SkASSERT(resource->resourcePriv().isPurgeable());
         resource->cacheAccess().release();
         stillOverbudget = this->overBudget();
     }
@@ -496,7 +506,7 @@
         // complexity. Moreover, this is rarely called.
         while (fPurgeableQueue.count()) {
             GrGpuResource* resource = fPurgeableQueue.peek();
-            SkASSERT(resource->isPurgeable());
+            SkASSERT(resource->resourcePriv().isPurgeable());
             resource->cacheAccess().release();
         }
     } else {
@@ -507,7 +517,7 @@
         SkTDArray<GrGpuResource*> scratchResources;
         for (int i = 0; i < fPurgeableQueue.count(); i++) {
             GrGpuResource* resource = fPurgeableQueue.at(i);
-            SkASSERT(resource->isPurgeable());
+            SkASSERT(resource->resourcePriv().isPurgeable());
             if (!resource->getUniqueKey().isValid()) {
                 *scratchResources.append() = resource;
             }
@@ -536,7 +546,7 @@
             break;
         }
         GrGpuResource* resource = fPurgeableQueue.peek();
-        SkASSERT(resource->isPurgeable());
+        SkASSERT(resource->resourcePriv().isPurgeable());
         resource->cacheAccess().release();
     }
 }
@@ -555,7 +565,7 @@
         size_t scratchByteCount = 0;
         for (int i = 0; i < fPurgeableQueue.count() && stillOverbudget; i++) {
             GrGpuResource* resource = fPurgeableQueue.at(i);
-            SkASSERT(resource->isPurgeable());
+            SkASSERT(resource->resourcePriv().isPurgeable());
             if (!resource->getUniqueKey().isValid()) {
                 *scratchResources.append() = resource;
                 scratchByteCount += resource->gpuMemorySize();
@@ -738,7 +748,7 @@
         void update(GrGpuResource* resource) {
             fBytes += resource->gpuMemorySize();
 
-            if (!resource->isPurgeable()) {
+            if (!resource->resourcePriv().isPurgeable()) {
                 ++fLocked;
             }
 
@@ -794,14 +804,14 @@
     size_t purgeableBytes = 0;
 
     for (int i = 0; i < fNonpurgeableResources.count(); ++i) {
-        SkASSERT(!fNonpurgeableResources[i]->isPurgeable() ||
+        SkASSERT(!fNonpurgeableResources[i]->resourcePriv().isPurgeable() ||
                  fNewlyPurgeableResourceForValidation == fNonpurgeableResources[i]);
         SkASSERT(*fNonpurgeableResources[i]->cacheAccess().accessCacheIndex() == i);
         SkASSERT(!fNonpurgeableResources[i]->wasDestroyed());
         stats.update(fNonpurgeableResources[i]);
     }
     for (int i = 0; i < fPurgeableQueue.count(); ++i) {
-        SkASSERT(fPurgeableQueue.at(i)->isPurgeable());
+        SkASSERT(fPurgeableQueue.at(i)->resourcePriv().isPurgeable());
         SkASSERT(*fPurgeableQueue.at(i)->cacheAccess().accessCacheIndex() == i);
         SkASSERT(!fPurgeableQueue.at(i)->wasDestroyed());
         stats.update(fPurgeableQueue.at(i));
diff --git a/src/gpu/gl/GrGLTexture.h b/src/gpu/gl/GrGLTexture.h
index 883da78..db83eea 100644
--- a/src/gpu/gl/GrGLTexture.h
+++ b/src/gpu/gl/GrGLTexture.h
@@ -82,6 +82,11 @@
         fReleaseHelper = std::move(releaseHelper);
     }
 
+    void setIdleProc(IdleProc proc, void* context) override {
+        fIdleProc = proc;
+        fIdleProcContext = context;
+    }
+
     // These functions are used to track the texture parameters associated with the texture.
     GrGpu::ResetTimestamp getCachedParamsTimestamp() const { return fParamsTimestamp; }
     const SamplerParams& getCachedSamplerParams() const { return fSamplerParams; }
@@ -127,10 +132,16 @@
 
 private:
     void invokeReleaseProc() {
-        if (fReleaseHelper) {
-            // Depending on the ref count of fReleaseHelper this may or may not actually trigger the
-            // ReleaseProc to be called.
-            fReleaseHelper.reset();
+        // Depending on the ref count of fReleaseHelper this may or may not actually trigger the
+        // ReleaseProc to be called.
+        fReleaseHelper.reset();
+    }
+
+    void becamePurgeable() override {
+        if (fIdleProc) {
+            fIdleProc(fIdleProcContext);
+            fIdleProc = nullptr;
+            fIdleProcContext = nullptr;
         }
     }
 
@@ -138,6 +149,8 @@
     NonSamplerParams fNonSamplerParams;
     GrGpu::ResetTimestamp fParamsTimestamp;
     sk_sp<GrReleaseProcHelper> fReleaseHelper;
+    IdleProc* fIdleProc = nullptr;
+    void* fIdleProcContext = nullptr;
     GrGLuint fID;
     GrGLenum fFormat;
     GrBackendObjectOwnership fTextureIDOwnership;
diff --git a/src/gpu/mock/GrMockTexture.h b/src/gpu/mock/GrMockTexture.h
index ee8780d..ac5eab0 100644
--- a/src/gpu/mock/GrMockTexture.h
+++ b/src/gpu/mock/GrMockTexture.h
@@ -48,6 +48,11 @@
         fReleaseHelper = std::move(releaseHelper);
     }
 
+    void setIdleProc(IdleProc proc, void* context) override {
+        fIdleProc = proc;
+        fIdleProcContext = context;
+    }
+
 protected:
     // constructor for subclasses
     GrMockTexture(GrMockGpu* gpu, const GrSurfaceDesc& desc, GrMipMapsStatus mipMapsStatus,
@@ -70,16 +75,27 @@
         return false;
     }
 
-private:
-    void invokeReleaseProc() {
-        if (fReleaseHelper) {
-            // Depending on the ref count of fReleaseHelper this may or may not actually trigger the
-            // ReleaseProc to be called.
-            fReleaseHelper.reset();
+    // protected so that GrMockTextureRenderTarget can call this to avoid "inheritance via
+    // dominance" warning.
+    void becamePurgeable() override {
+        if (fIdleProc) {
+            fIdleProc(fIdleProcContext);
+            fIdleProc = nullptr;
+            fIdleProcContext = nullptr;
         }
     }
-    GrMockTextureInfo          fInfo;
+
+private:
+    void invokeReleaseProc() {
+        // Depending on the ref count of fReleaseHelper this may or may not actually trigger the
+        // ReleaseProc to be called.
+        fReleaseHelper.reset();
+    }
+
+    GrMockTextureInfo fInfo;
     sk_sp<GrReleaseProcHelper> fReleaseHelper;
+    IdleProc* fIdleProc = nullptr;
+    void* fIdleProcContext = nullptr;
 
     typedef GrTexture INHERITED;
 };
@@ -179,6 +195,9 @@
         GrMockTexture::onRelease();
     }
 
+    // We implement this to avoid the inheritance via dominance warning.
+    void becamePurgeable() override { GrMockTexture::becamePurgeable(); }
+
     size_t onGpuMemorySize() const override {
         int numColorSamples = this->numColorSamples();
         if (numColorSamples > 1) {
diff --git a/src/gpu/mtl/GrMtlTexture.h b/src/gpu/mtl/GrMtlTexture.h
index 7564f94..8037c68 100644
--- a/src/gpu/mtl/GrMtlTexture.h
+++ b/src/gpu/mtl/GrMtlTexture.h
@@ -43,15 +43,22 @@
         fReleaseHelper = std::move(releaseHelper);
     }
 
+    void setIdleProc(IdleProc proc, void* context) override {
+        fIdleProc = proc;
+        fIdleProcContext = context;
+    }
+
 protected:
     GrMtlTexture(GrMtlGpu*, const GrSurfaceDesc&, id<MTLTexture>, GrMipMapsStatus);
 
     GrMtlGpu* getMtlGpu() const;
 
     void onAbandon() override {
+        this->invokeReleaseProc();
         fTexture = nil;
     }
     void onRelease() override {
+        this->invokeReleaseProc();
         fTexture = nil;
     }
 
@@ -61,6 +68,21 @@
 
 private:
     enum Wrapped { kWrapped };
+
+    void invokeReleaseProc() {
+        // Depending on the ref count of fReleaseHelper this may or may not actually trigger the
+        // ReleaseProc to be called.
+        fReleaseHelper.reset();
+    }
+
+    void becamePurgeable() override {
+        if (fIdleProc) {
+            fIdleProc(fIdleProcContext);
+            fIdleProc = nullptr;
+            fIdleProcContext = nullptr;
+        }
+    }
+
     GrMtlTexture(GrMtlGpu*, SkBudgeted, const GrSurfaceDesc&, id<MTLTexture>,
                  GrMipMapsStatus);
 
@@ -68,8 +90,9 @@
                  GrIOType, bool purgeImmediately);
 
     id<MTLTexture> fTexture;
-
-    sk_sp<GrReleaseProcHelper>        fReleaseHelper;
+    sk_sp<GrReleaseProcHelper> fReleaseHelper;
+    IdleProc* fIdleProc = nullptr;
+    void* fIdleProcContext = nullptr;
 
     typedef GrTexture INHERITED;
 };
diff --git a/src/gpu/vk/GrVkCommandBuffer.cpp b/src/gpu/vk/GrVkCommandBuffer.cpp
index 6773269..1d75fff 100644
--- a/src/gpu/vk/GrVkCommandBuffer.cpp
+++ b/src/gpu/vk/GrVkCommandBuffer.cpp
@@ -44,14 +44,17 @@
 void GrVkCommandBuffer::freeGPUData(GrVkGpu* gpu) const {
     SkASSERT(!fIsActive);
     for (int i = 0; i < fTrackedResources.count(); ++i) {
+        fTrackedResources[i]->notifyRemovedFromCommandBuffer();
         fTrackedResources[i]->unref(gpu);
     }
 
     for (int i = 0; i < fTrackedRecycledResources.count(); ++i) {
+        fTrackedRecycledResources[i]->notifyRemovedFromCommandBuffer();
         fTrackedRecycledResources[i]->recycle(const_cast<GrVkGpu*>(gpu));
     }
 
     for (int i = 0; i < fTrackedRecordingResources.count(); ++i) {
+        fTrackedRecordingResources[i]->notifyRemovedFromCommandBuffer();
         fTrackedRecordingResources[i]->unref(gpu);
     }
 
@@ -64,15 +67,18 @@
 void GrVkCommandBuffer::abandonGPUData() const {
     SkDEBUGCODE(fResourcesReleased = true;)
     for (int i = 0; i < fTrackedResources.count(); ++i) {
+        fTrackedResources[i]->notifyRemovedFromCommandBuffer();
         fTrackedResources[i]->unrefAndAbandon();
     }
 
     for (int i = 0; i < fTrackedRecycledResources.count(); ++i) {
+        fTrackedRecycledResources[i]->notifyRemovedFromCommandBuffer();
         // We don't recycle resources when abandoning them.
         fTrackedRecycledResources[i]->unrefAndAbandon();
     }
 
     for (int i = 0; i < fTrackedRecordingResources.count(); ++i) {
+        fTrackedRecordingResources[i]->notifyRemovedFromCommandBuffer();
         fTrackedRecordingResources[i]->unrefAndAbandon();
     }
 
@@ -83,13 +89,16 @@
     SkDEBUGCODE(fResourcesReleased = true;)
     SkASSERT(!fIsActive);
     for (int i = 0; i < fTrackedResources.count(); ++i) {
+        fTrackedResources[i]->notifyRemovedFromCommandBuffer();
         fTrackedResources[i]->unref(gpu);
     }
     for (int i = 0; i < fTrackedRecycledResources.count(); ++i) {
+        fTrackedRecycledResources[i]->notifyRemovedFromCommandBuffer();
         fTrackedRecycledResources[i]->recycle(const_cast<GrVkGpu*>(gpu));
     }
 
     for (int i = 0; i < fTrackedRecordingResources.count(); ++i) {
+        fTrackedRecordingResources[i]->notifyRemovedFromCommandBuffer();
         fTrackedRecordingResources[i]->unref(gpu);
     }
 
diff --git a/src/gpu/vk/GrVkCommandBuffer.h b/src/gpu/vk/GrVkCommandBuffer.h
index 039f4d0..21ffb79 100644
--- a/src/gpu/vk/GrVkCommandBuffer.h
+++ b/src/gpu/vk/GrVkCommandBuffer.h
@@ -109,6 +109,7 @@
     // execution
     void addResource(const GrVkResource* resource) {
         resource->ref();
+        resource->notifyAddedToCommandBuffer();
         fTrackedResources.append(1, &resource);
     }
 
@@ -116,6 +117,7 @@
     // execution. When it is released, it will signal that the resource can be recycled for reuse.
     void addRecycledResource(const GrVkRecycledResource* resource) {
         resource->ref();
+        resource->notifyAddedToCommandBuffer();
         fTrackedRecycledResources.append(1, &resource);
     }
 
@@ -123,6 +125,7 @@
     // recording.
     void addRecordingResource(const GrVkResource* resource) {
         resource->ref();
+        resource->notifyAddedToCommandBuffer();
         fTrackedRecordingResources.append(1, &resource);
     }
 
diff --git a/src/gpu/vk/GrVkImage.cpp b/src/gpu/vk/GrVkImage.cpp
index 1da1275..0729531 100644
--- a/src/gpu/vk/GrVkImage.cpp
+++ b/src/gpu/vk/GrVkImage.cpp
@@ -5,9 +5,11 @@
  * found in the LICENSE file.
  */
 
-#include "GrVkGpu.h"
 #include "GrVkImage.h"
+#include "GrGpuResourcePriv.h"
+#include "GrVkGpu.h"
 #include "GrVkMemory.h"
+#include "GrVkTexture.h"
 #include "GrVkUtil.h"
 
 #define VK_CALL(GPU, X) GR_VK_CALL(GPU->vkInterface(), X)
@@ -221,6 +223,7 @@
         this->setImageLayout(gpu, this->currentLayout(), 0, 0, false, true);
     }
     if (fResource) {
+        fResource->removeOwningTexture();
         fResource->unref(gpu);
         fResource = nullptr;
     }
@@ -228,6 +231,7 @@
 
 void GrVkImage::abandonImage() {
     if (fResource) {
+        fResource->removeOwningTexture();
         fResource->unrefAndAbandon();
         fResource = nullptr;
     }
@@ -245,6 +249,37 @@
     GrVkMemory::FreeImageMemory(gpu, isLinear, fAlloc);
 }
 
+void GrVkImage::Resource::setIdleProc(GrVkTexture* owner, GrTexture::IdleProc proc,
+                                      void* context) const {
+    fOwningTexture = owner;
+    fIdleProc = proc;
+    fIdleProcContext = context;
+}
+
+void GrVkImage::Resource::removeOwningTexture() const { fOwningTexture = nullptr; }
+
+void GrVkImage::Resource::notifyAddedToCommandBuffer() const { ++fNumCommandBufferOwners; }
+
+void GrVkImage::Resource::notifyRemovedFromCommandBuffer() const {
+    SkASSERT(fNumCommandBufferOwners);
+    if (--fNumCommandBufferOwners || !fIdleProc) {
+        return;
+    }
+    if (fOwningTexture && !fOwningTexture->resourcePriv().isPurgeable()) {
+        return;
+    }
+    fIdleProc(fIdleProcContext);
+    if (fOwningTexture) {
+        fOwningTexture->setIdleProc(nullptr, nullptr);
+        // Changing the texture's proc should change ours.
+        SkASSERT(!fIdleProc);
+        SkASSERT(!fIdleProc);
+    } else {
+        fIdleProc = nullptr;
+        fIdleProcContext = nullptr;
+    }
+}
+
 void GrVkImage::BorrowedResource::freeGPUData(GrVkGpu* gpu) const {
     this->invokeReleaseProc();
 }
diff --git a/src/gpu/vk/GrVkImage.h b/src/gpu/vk/GrVkImage.h
index e593c1f..1df25eb 100644
--- a/src/gpu/vk/GrVkImage.h
+++ b/src/gpu/vk/GrVkImage.h
@@ -10,16 +10,16 @@
 
 #include "GrVkVulkan.h"
 
-#include "GrVkResource.h"
-
 #include "GrBackendSurface.h"
+#include "GrTexture.h"
 #include "GrTypesPriv.h"
 #include "GrVkImageLayout.h"
+#include "GrVkResource.h"
 #include "SkTypes.h"
-
 #include "vk/GrVkTypes.h"
 
 class GrVkGpu;
+class GrVkTexture;
 
 class GrVkImage : SkNoncopyable {
 private:
@@ -151,6 +151,24 @@
         void setRelease(sk_sp<GrReleaseProcHelper> releaseHelper) {
             fReleaseHelper = std::move(releaseHelper);
         }
+
+        /**
+         * These are used to coordinate calling the idle proc between the GrVkTexture and the
+         * Resource. If the GrVkTexture becomes purgeable and if there are no command buffers
+         * referring to the Resource then it calls the proc. Otherwise, the Resource calls it
+         * when the last command buffer reference goes away and the GrVkTexture is purgeable.
+         */
+        void setIdleProc(GrVkTexture* owner, GrTexture::IdleProc, void* context) const;
+        void removeOwningTexture() const;
+
+        /**
+         * We track how many outstanding references this Resource has in command buffers and
+         * when the count reaches zero we call the idle proc.
+         */
+        void notifyAddedToCommandBuffer() const override;
+        void notifyRemovedFromCommandBuffer() const override;
+        bool isOwnedByCommandBuffer() const { return fNumCommandBufferOwners > 0; }
+
     protected:
         mutable sk_sp<GrReleaseProcHelper> fReleaseHelper;
 
@@ -163,6 +181,10 @@
         VkImage        fImage;
         GrVkAlloc      fAlloc;
         VkImageTiling  fImageTiling;
+        mutable int fNumCommandBufferOwners = 0;
+        mutable GrTexture::IdleProc* fIdleProc = nullptr;
+        mutable void* fIdleProcContext = nullptr;
+        mutable GrVkTexture* fOwningTexture = nullptr;
 
         typedef GrVkResource INHERITED;
     };
diff --git a/src/gpu/vk/GrVkResource.h b/src/gpu/vk/GrVkResource.h
index 9cb915a..c1cd310 100644
--- a/src/gpu/vk/GrVkResource.h
+++ b/src/gpu/vk/GrVkResource.h
@@ -144,6 +144,13 @@
         }
     }
 
+    // Called every time this resource is added to a command buffer.
+    virtual void notifyAddedToCommandBuffer() const {}
+    // Called every time this resource is removed from a command buffer (typically because
+    // the command buffer finished execution on the GPU but also when the command buffer
+    // is abandoned.)
+    virtual void notifyRemovedFromCommandBuffer() const {}
+
 #ifdef SK_DEBUG
     void validate() const {
         SkASSERT(this->getRefCnt() > 0);
diff --git a/src/gpu/vk/GrVkTexture.cpp b/src/gpu/vk/GrVkTexture.cpp
index a295c04..fe9723d 100644
--- a/src/gpu/vk/GrVkTexture.cpp
+++ b/src/gpu/vk/GrVkTexture.cpp
@@ -126,6 +126,11 @@
 }
 
 void GrVkTexture::onRelease() {
+    // When there is an idle proc, the Resource will call the proc in releaseImage() so
+    // we clear it here.
+    fIdleProc = nullptr;
+    fIdleProcContext = nullptr;
+
     // we create this and don't hand it off, so we should always destroy it
     if (fTextureView) {
         fTextureView->unref(this->getVkGpu());
@@ -138,6 +143,11 @@
 }
 
 void GrVkTexture::onAbandon() {
+    // When there is an idle proc, the Resource will call the proc in abandonImage() so
+    // we clear it here.
+    fIdleProc = nullptr;
+    fIdleProcContext = nullptr;
+    // we create this and don't hand it off, so we should always destroy it
     if (fTextureView) {
         fTextureView->unrefAndAbandon();
         fTextureView = nullptr;
@@ -160,3 +170,27 @@
     return fTextureView;
 }
 
+void GrVkTexture::setIdleProc(IdleProc proc, void* context) {
+    fIdleProc = proc;
+    fIdleProcContext = context;
+    if (auto* resource = this->resource()) {
+        resource->setIdleProc(proc ? this : nullptr, proc, context);
+    }
+}
+
+void GrVkTexture::becamePurgeable() {
+    if (!fIdleProc) {
+        return;
+    }
+    // This is called when the GrTexture is purgeable. However, we need to check whether the
+    // Resource is still owned by any command buffers. If it is then it will call the proc.
+    auto* resource = this->resource();
+    SkASSERT(resource);
+    if (resource->isOwnedByCommandBuffer()) {
+        return;
+    }
+    fIdleProc(fIdleProcContext);
+    fIdleProc = nullptr;
+    fIdleProcContext = nullptr;
+    resource->setIdleProc(nullptr, nullptr, nullptr);
+}
diff --git a/src/gpu/vk/GrVkTexture.h b/src/gpu/vk/GrVkTexture.h
index b85dee0..696db6b 100644
--- a/src/gpu/vk/GrVkTexture.h
+++ b/src/gpu/vk/GrVkTexture.h
@@ -46,6 +46,8 @@
         this->setResourceRelease(std::move(releaseHelper));
     }
 
+    void setIdleProc(IdleProc, void* context) override;
+
 protected:
     GrVkTexture(GrVkGpu*, const GrSurfaceDesc&, const GrVkImageInfo&, sk_sp<GrVkImageLayout>,
                 const GrVkImageView*, GrMipMapsStatus, GrBackendObjectOwnership);
@@ -68,7 +70,11 @@
                 sk_sp<GrVkImageLayout> layout, const GrVkImageView* imageView, GrMipMapsStatus,
                 GrBackendObjectOwnership, GrIOType ioType, bool purgeImmediately);
 
-    const GrVkImageView*     fTextureView;
+    void becamePurgeable() override;
+
+    const GrVkImageView* fTextureView;
+    GrTexture::IdleProc* fIdleProc = nullptr;
+    void* fIdleProcContext = nullptr;
 
     typedef GrTexture INHERITED;
 };