Adds a mechanism for GrCacheable objects to notify the resource cache
when their size has changed. GrResourceCacheEntry now holds a
reference to the cache, and a cached value of the resource's most
recent size.

Also utilizes this new functionality for mipmaps, and adds a test for
changing resource sizes.

R=bsalomon@google.com, robertphillips@google.com

Author: cdalton@nvidia.com

Review URL: https://codereview.chromium.org/257093002

git-svn-id: http://skia.googlecode.com/svn/trunk@14576 2bbb7eff-a529-9590-31e7-b0007b416f81
diff --git a/include/gpu/GrCacheable.h b/include/gpu/GrCacheable.h
index 75dec16..39c62b1 100644
--- a/include/gpu/GrCacheable.h
+++ b/include/gpu/GrCacheable.h
@@ -46,6 +46,14 @@
 
     bool isInCache() const { return NULL != fCacheEntry; }
 
+    /**
+     * This entry point should be called whenever gpuMemorySize() begins
+     * reporting a different size. If the object is in the cache, it will call
+     * gpuMemorySize() immediately and pass the new size on to the resource
+     * cache.
+     */
+    void didChangeGpuMemorySize() const;
+
 private:
     GrResourceCacheEntry* fCacheEntry;  // NULL if not in cache
 
diff --git a/include/gpu/GrTexture.h b/include/gpu/GrTexture.h
index 1b9f7b7..ac31f51 100644
--- a/include/gpu/GrTexture.h
+++ b/include/gpu/GrTexture.h
@@ -44,22 +44,16 @@
         return 0 != (fDesc.fFlags & flags);
     }
 
-    void dirtyMipMaps(bool mipMapsDirty) {
-        fMipMapsDirty = mipMapsDirty;
-    }
+    void dirtyMipMaps(bool mipMapsDirty);
 
     bool mipMapsAreDirty() const {
-        return fMipMapsDirty;
+        return kValid_MipMapsStatus != fMipMapsStatus;
     }
 
     /**
      *  Approximate number of bytes used by the texture
      */
-    virtual size_t gpuMemorySize() const SK_OVERRIDE {
-        return (size_t) fDesc.fWidth *
-                        fDesc.fHeight *
-                        GrBytesPerPixel(fDesc.fConfig);
-    }
+    virtual size_t gpuMemorySize() const SK_OVERRIDE;
 
     // GrSurface overrides
     virtual bool readPixels(int left, int top, int width, int height,
@@ -144,7 +138,7 @@
     GrTexture(GrGpu* gpu, bool isWrapped, const GrTextureDesc& desc)
     : INHERITED(gpu, isWrapped, desc)
     , fRenderTarget(NULL)
-    , fMipMapsDirty(true) {
+    , fMipMapsStatus(kNotAllocated_MipMapsStatus) {
 
         // only make sense if alloc size is pow2
         fShiftFixedX = 31 - SkCLZ(fDesc.fWidth);
@@ -159,12 +153,18 @@
     void validateDesc() const;
 
 private:
+    enum MipMapsStatus {
+        kNotAllocated_MipMapsStatus,
+        kAllocated_MipMapsStatus,
+        kValid_MipMapsStatus
+    };
+
     // these two shift a fixed-point value into normalized coordinates
     // for this texture if the texture is power of two sized.
     int                 fShiftFixedX;
     int                 fShiftFixedY;
 
-    bool                fMipMapsDirty;
+    MipMapsStatus       fMipMapsStatus;
 
     virtual void internal_dispose() const SK_OVERRIDE;
 
diff --git a/src/gpu/GrResourceCache.cpp b/src/gpu/GrResourceCache.cpp
index 26f7592..529c3a5 100644
--- a/src/gpu/GrResourceCache.cpp
+++ b/src/gpu/GrResourceCache.cpp
@@ -13,6 +13,16 @@
 
 DECLARE_SKMESSAGEBUS_MESSAGE(GrResourceInvalidatedMessage);
 
+///////////////////////////////////////////////////////////////////////////////
+
+void GrCacheable::didChangeGpuMemorySize() const {
+    if (this->isInCache()) {
+        fCacheEntry->didChangeResourceSize();
+    }
+}
+
+///////////////////////////////////////////////////////////////////////////////
+
 GrResourceKey::ResourceType GrResourceKey::GenerateResourceType() {
     static int32_t gNextType = 0;
 
@@ -26,8 +36,14 @@
 
 ///////////////////////////////////////////////////////////////////////////////
 
-GrResourceCacheEntry::GrResourceCacheEntry(const GrResourceKey& key, GrCacheable* resource)
-        : fKey(key), fResource(resource) {
+GrResourceCacheEntry::GrResourceCacheEntry(GrResourceCache* resourceCache,
+                                           const GrResourceKey& key,
+                                           GrCacheable* resource)
+        : fResourceCache(resourceCache),
+          fKey(key),
+          fResource(resource),
+          fCachedSize(resource->gpuMemorySize()),
+          fIsExclusive(false) {
     // we assume ownership of the resource, and will unref it when we die
     SkASSERT(resource);
     resource->ref();
@@ -40,12 +56,24 @@
 
 #ifdef SK_DEBUG
 void GrResourceCacheEntry::validate() const {
+    SkASSERT(fResourceCache);
     SkASSERT(fResource);
     SkASSERT(fResource->getCacheEntry() == this);
+    SkASSERT(fResource->gpuMemorySize() == fCachedSize);
     fResource->validate();
 }
 #endif
 
+void GrResourceCacheEntry::didChangeResourceSize() {
+    size_t oldSize = fCachedSize;
+    fCachedSize = fResource->gpuMemorySize();
+    if (fCachedSize > oldSize) {
+        fResourceCache->didIncreaseResourceSize(this, fCachedSize - oldSize);
+    } else if (fCachedSize < oldSize) {
+        fResourceCache->didDecreaseResourceSize(this, oldSize - fCachedSize);
+    }
+}
+
 ///////////////////////////////////////////////////////////////////////////////
 
 GrResourceCache::GrResourceCache(int maxCount, size_t maxBytes) :
@@ -115,7 +143,7 @@
     // update our stats
     if (kIgnore_BudgetBehavior == behavior) {
         fClientDetachedCount += 1;
-        fClientDetachedBytes += entry->resource()->gpuMemorySize();
+        fClientDetachedBytes += entry->fCachedSize;
 
 #if GR_CACHE_STATS
         if (fHighWaterClientDetachedCount < fClientDetachedCount) {
@@ -130,7 +158,7 @@
         SkASSERT(kAccountFor_BudgetBehavior == behavior);
 
         fEntryCount -= 1;
-        fEntryBytes -= entry->resource()->gpuMemorySize();
+        fEntryBytes -= entry->fCachedSize;
     }
 }
 
@@ -141,12 +169,12 @@
     // update our stats
     if (kIgnore_BudgetBehavior == behavior) {
         fClientDetachedCount -= 1;
-        fClientDetachedBytes -= entry->resource()->gpuMemorySize();
+        fClientDetachedBytes -= entry->fCachedSize;
     } else {
         SkASSERT(kAccountFor_BudgetBehavior == behavior);
 
         fEntryCount += 1;
-        fEntryBytes += entry->resource()->gpuMemorySize();
+        fEntryBytes += entry->fCachedSize;
 
 #if GR_CACHE_STATS
         if (fHighWaterEntryCount < fEntryCount) {
@@ -208,7 +236,7 @@
     SkASSERT(!fPurging);
     GrAutoResourceCacheValidate atcv(this);
 
-    GrResourceCacheEntry* entry = SkNEW_ARGS(GrResourceCacheEntry, (key, resource));
+    GrResourceCacheEntry* entry = SkNEW_ARGS(GrResourceCacheEntry, (this, key, resource));
     resource->setCacheEntry(entry);
 
     this->attachToHead(entry);
@@ -223,6 +251,9 @@
 void GrResourceCache::makeExclusive(GrResourceCacheEntry* entry) {
     GrAutoResourceCacheValidate atcv(this);
 
+    SkASSERT(!entry->fIsExclusive);
+    entry->fIsExclusive = true;
+
     // When scratch textures are detached (to hide them from future finds) they
     // still count against the resource budget
     this->internalDetach(entry, kIgnore_BudgetBehavior);
@@ -239,11 +270,12 @@
     // the client called GrContext::contextDestroyed() to notify Gr,
     // and then later an SkGpuDevice's destructor releases its backing
     // texture (which was invalidated at contextDestroyed time).
+    // TODO: Safely delete the GrResourceCacheEntry as well.
     fClientDetachedCount -= 1;
     fEntryCount -= 1;
-    size_t size = entry->resource()->gpuMemorySize();
-    fClientDetachedBytes -= size;
-    fEntryBytes -= size;
+    fClientDetachedBytes -= entry->fCachedSize;
+    fEntryBytes -= entry->fCachedSize;
+    entry->fCachedSize = 0;
 }
 
 void GrResourceCache::makeNonExclusive(GrResourceCacheEntry* entry) {
@@ -259,11 +291,32 @@
         // alter the budget information.
         attachToHead(entry, kIgnore_BudgetBehavior);
         fCache.insert(entry->key(), entry);
+
+        SkASSERT(entry->fIsExclusive);
+        entry->fIsExclusive = false;
     } else {
         this->removeInvalidResource(entry);
     }
 }
 
+void GrResourceCache::didIncreaseResourceSize(const GrResourceCacheEntry* entry, size_t amountInc) {
+    fEntryBytes += amountInc;
+    if (entry->fIsExclusive) {
+        fClientDetachedBytes += amountInc;
+    }
+    this->purgeAsNeeded();
+}
+
+void GrResourceCache::didDecreaseResourceSize(const GrResourceCacheEntry* entry, size_t amountDec) {
+    fEntryBytes -= amountDec;
+    if (entry->fIsExclusive) {
+        fClientDetachedBytes -= amountDec;
+    }
+#ifdef SK_DEBUG
+    this->validate();
+#endif
+}
+
 /**
  * Destroying a resource may potentially trigger the unlock of additional
  * resources which in turn will trigger a nested purge. We block the nested
diff --git a/src/gpu/GrResourceCache.h b/src/gpu/GrResourceCache.h
index b2f91cd..1a81fe6 100644
--- a/src/gpu/GrResourceCache.h
+++ b/src/gpu/GrResourceCache.h
@@ -19,6 +19,7 @@
 #include "SkTInternalLList.h"
 
 class GrCacheable;
+class GrResourceCache;
 class GrResourceCacheEntry;
 
 class GrResourceKey {
@@ -128,12 +129,24 @@
     void validate() const {}
 #endif
 
+    /**
+     *  Update the cached size for this entry and inform the resource cache that
+     *  it has changed. Usually invoked from GrCacheable::didChangeGpuMemorySize,
+     *  not directly from here.
+     */
+    void didChangeResourceSize();
+
 private:
-    GrResourceCacheEntry(const GrResourceKey& key, GrCacheable* resource);
+    GrResourceCacheEntry(GrResourceCache* resourceCache,
+                         const GrResourceKey& key,
+                         GrCacheable* resource);
     ~GrResourceCacheEntry();
 
+    GrResourceCache* fResourceCache;
     GrResourceKey    fKey;
     GrCacheable*     fResource;
+    size_t           fCachedSize;
+    bool             fIsExclusive;
 
     // Linked list for the LRU ordering.
     SK_DECLARE_INTERNAL_LLIST_INTERFACE(GrResourceCacheEntry);
@@ -272,6 +285,12 @@
     void makeNonExclusive(GrResourceCacheEntry* entry);
 
     /**
+     * Notify the cache that the size of a resource has changed.
+     */
+    void didIncreaseResourceSize(const GrResourceCacheEntry*, size_t amountInc);
+    void didDecreaseResourceSize(const GrResourceCacheEntry*, size_t amountDec);
+
+    /**
      * Remove a resource from the cache and delete it!
      */
     void deleteResource(GrResourceCacheEntry* entry);
diff --git a/src/gpu/GrTexture.cpp b/src/gpu/GrTexture.cpp
index f851515..3186d89 100644
--- a/src/gpu/GrTexture.cpp
+++ b/src/gpu/GrTexture.cpp
@@ -44,6 +44,33 @@
     this->INHERITED::internal_dispose();
 }
 
+void GrTexture::dirtyMipMaps(bool mipMapsDirty) {
+    if (mipMapsDirty) {
+        if (kValid_MipMapsStatus == fMipMapsStatus) {
+            fMipMapsStatus = kAllocated_MipMapsStatus;
+        }
+    } else {
+        const bool sizeChanged = kNotAllocated_MipMapsStatus == fMipMapsStatus;
+        fMipMapsStatus = kValid_MipMapsStatus;
+        if (sizeChanged) {
+            // This must not be called until after changing fMipMapsStatus.
+            this->didChangeGpuMemorySize();
+        }
+    }
+}
+
+size_t GrTexture::gpuMemorySize() const {
+    size_t textureSize =  (size_t) fDesc.fWidth *
+                                   fDesc.fHeight *
+                                   GrBytesPerPixel(fDesc.fConfig);
+    if (kNotAllocated_MipMapsStatus != fMipMapsStatus) {
+        // We don't have to worry about the mipmaps being a different size than
+        // we'd expect because we never change fDesc.fWidth/fHeight.
+        textureSize *= 2;
+    }
+    return textureSize;
+}
+
 bool GrTexture::readPixels(int left, int top, int width, int height,
                            GrPixelConfig config, void* buffer,
                            size_t rowBytes, uint32_t pixelOpsFlags) {
diff --git a/tests/ResourceCacheTest.cpp b/tests/ResourceCacheTest.cpp
index 7262b43..f6c8b1d 100644
--- a/tests/ResourceCacheTest.cpp
+++ b/tests/ResourceCacheTest.cpp
@@ -57,11 +57,14 @@
 }
 
 class TestResource : public GrCacheable {
+    static const size_t kDefaultSize = 100;
+
 public:
     SK_DECLARE_INST_COUNT(TestResource);
-    TestResource()
+    TestResource(size_t size = kDefaultSize)
         : fCache(NULL)
-        , fToDelete(NULL) {
+        , fToDelete(NULL)
+        , fSize(size) {
         ++fAlive;
     }
 
@@ -74,7 +77,12 @@
         }
     }
 
-    size_t gpuMemorySize() const SK_OVERRIDE { return 100; }
+    void setSize(size_t size) {
+        fSize = size;
+        this->didChangeGpuMemorySize();
+    }
+
+    size_t gpuMemorySize() const SK_OVERRIDE { return fSize; }
 
     bool isValidOnGpu() const SK_OVERRIDE { return true; }
 
@@ -88,6 +96,7 @@
 private:
     GrResourceCache* fCache;
     TestResource* fToDelete;
+    size_t fSize;
     static int fAlive;
 
     typedef GrCacheable INHERITED;
@@ -174,6 +183,99 @@
     }
 }
 
+static void test_resource_size_changed(skiatest::Reporter* reporter,
+                                       GrContext* context) {
+    GrCacheID::Domain domain = GrCacheID::GenerateDomain();
+    GrResourceKey::ResourceType t = GrResourceKey::GenerateResourceType();
+
+    GrCacheID::Key key1Data;
+    key1Data.fData64[0] = 0;
+    key1Data.fData64[1] = 0;
+    GrResourceKey key1(GrCacheID(domain, key1Data), t, 0);
+
+    GrCacheID::Key key2Data;
+    key2Data.fData64[0] = 1;
+    key2Data.fData64[1] = 0;
+    GrResourceKey key2(GrCacheID(domain, key2Data), t, 0);
+
+    // Test changing resources sizes (both increase & decrease).
+    {
+        GrResourceCache cache(2, 300);
+
+        TestResource* a = new TestResource(0);
+        a->setSize(100); // Test didChangeGpuMemorySize() when not in the cache.
+        cache.addResource(key1, a);
+        a->unref();
+
+        TestResource* b = new TestResource(0);
+        b->setSize(100);
+        cache.addResource(key2, b);
+        b->unref();
+
+        REPORTER_ASSERT(reporter, 200 == cache.getCachedResourceBytes());
+        REPORTER_ASSERT(reporter, 2 == cache.getCachedResourceCount());
+
+        static_cast<TestResource*>(cache.find(key2))->setSize(200);
+        static_cast<TestResource*>(cache.find(key1))->setSize(50);
+
+        REPORTER_ASSERT(reporter, 250 == cache.getCachedResourceBytes());
+        REPORTER_ASSERT(reporter, 2 == cache.getCachedResourceCount());
+    }
+
+    // Test increasing a resources size beyond the cache budget.
+    {
+        GrResourceCache cache(2, 300);
+
+        TestResource* a = new TestResource(100);
+        cache.addResource(key1, a);
+        a->unref();
+
+        TestResource* b = new TestResource(100);
+        cache.addResource(key2, b);
+        b->unref();
+
+        REPORTER_ASSERT(reporter, 200 == cache.getCachedResourceBytes());
+        REPORTER_ASSERT(reporter, 2 == cache.getCachedResourceCount());
+
+        static_cast<TestResource*>(cache.find(key2))->setSize(201);
+        REPORTER_ASSERT(reporter, NULL == cache.find(key1));
+
+        REPORTER_ASSERT(reporter, 201 == cache.getCachedResourceBytes());
+        REPORTER_ASSERT(reporter, 1 == cache.getCachedResourceCount());
+    }
+
+    // Test changing the size of an exclusively-held resource.
+    {
+        GrResourceCache cache(2, 300);
+
+        TestResource* a = new TestResource(100);
+        cache.addResource(key1, a);
+        cache.makeExclusive(a->getCacheEntry());
+
+        TestResource* b = new TestResource(100);
+        cache.addResource(key2, b);
+        b->unref();
+
+        REPORTER_ASSERT(reporter, 200 == cache.getCachedResourceBytes());
+        REPORTER_ASSERT(reporter, 2 == cache.getCachedResourceCount());
+        REPORTER_ASSERT(reporter, NULL == cache.find(key1));
+
+        a->setSize(200);
+
+        REPORTER_ASSERT(reporter, 300 == cache.getCachedResourceBytes());
+        REPORTER_ASSERT(reporter, 2 == cache.getCachedResourceCount());
+        // Internal resource cache validation will test the detached size (debug mode only).
+
+        cache.makeNonExclusive(a->getCacheEntry());
+        a->unref();
+
+        REPORTER_ASSERT(reporter, 300 == cache.getCachedResourceBytes());
+        REPORTER_ASSERT(reporter, 2 == cache.getCachedResourceCount());
+        REPORTER_ASSERT(reporter, NULL != cache.find(key1));
+        // Internal resource cache validation will test the detached size (debug mode only).
+    }
+}
+
 ////////////////////////////////////////////////////////////////////////////////
 DEF_GPUTEST(ResourceCache, reporter, factory) {
     for (int type = 0; type < GrContextFactory::kLastGLContextType; ++type) {
@@ -199,6 +301,7 @@
         test_cache(reporter, context, &canvas);
         test_purge_invalidated(reporter, context);
         test_cache_delete_on_destruction(reporter, context);
+        test_resource_size_changed(reporter, context);
     }
 }