*
* The latter two ref types are private and intended only for Gr core code.
*
- * When an item is purgeable DERIVED:notifyIsPurgeable() will be called (static poly morphism using
- * CRTP). GrIORef and GrGpuResource are separate classes for organizational reasons and to be
+ * When all the ref/io counts reach zero DERIVED::notifyAllCntsAreZero() will be called (static poly
+ * morphism using CRTP). Similarly when the ref (but not necessarily pending read/write) count
+ * reaches 0 DERIVED::notifyRefCountIsZero() will be called. In the case when an unref() causes both
+ * the ref cnt to reach zero and the other counts are zero, notifyRefCountIsZero() will be called
+ * before notifyIsPurgeable(). Moreover, if notifyRefCountIsZero() returns false then
+ * notifyAllRefCntsAreZero() won't be called at all. notifyRefCountIsZero() must return false if the
+ * object may be deleted after notifyRefCntIsZero() returns.
+ *
+ * GrIORef and GrGpuResource are separate classes for organizational reasons and to be
* able to give access via friendship to only the functions related to pending IO operations.
*/
template <typename DERIVED> class GrIORef : public SkNoncopyable {
void unref() const {
this->validate();
- --fRefCnt;
- this->didUnref();
+
+ if (!(--fRefCnt)) {
+ if (!static_cast<const DERIVED*>(this)->notifyRefCountIsZero()) {
+ return;
+ }
+ }
+
+ this->didRemoveRefOrPendingIO(kRef_CntType);
}
void validate() const {
protected:
GrIORef() : fRefCnt(1), fPendingReads(0), fPendingWrites(0) { }
+ enum CntType {
+ kRef_CntType,
+ kPendingRead_CntType,
+ kPendingWrite_CntType,
+ };
+
bool isPurgeable() const { return !this->internalHasRef() && !this->internalHasPendingIO(); }
bool internalHasPendingRead() const { return SkToBool(fPendingReads); }
void completedRead() const {
this->validate();
--fPendingReads;
- this->didUnref();
+ this->didRemoveRefOrPendingIO(kPendingRead_CntType);
}
void addPendingWrite() const {
void completedWrite() const {
this->validate();
--fPendingWrites;
- this->didUnref();
+ this->didRemoveRefOrPendingIO(kPendingWrite_CntType);
}
private:
- void didUnref() const {
+ void didRemoveRefOrPendingIO(CntType cntTypeRemoved) const {
if (0 == fPendingReads && 0 == fPendingWrites && 0 == fRefCnt) {
- static_cast<const DERIVED*>(this)->notifyIsPurgeable();
+ static_cast<const DERIVED*>(this)->notifyAllCntsAreZero(cntTypeRemoved);
}
}
// See comments in CacheAccess and ResourcePriv.
void setUniqueKey(const GrUniqueKey&);
void removeUniqueKey();
- void notifyIsPurgeable() const;
+ void notifyAllCntsAreZero(CntType) const;
+ bool notifyRefCountIsZero() const;
void removeScratchKey();
void makeBudgeted();
void makeUnbudgeted();
SkAutoTUnref<const SkData> fData;
typedef GrIORef<GrGpuResource> INHERITED;
- friend class GrIORef<GrGpuResource>; // to access notifyIsPurgeable.
+ friend class GrIORef<GrGpuResource>; // to access notifyAllCntsAreZero and notifyRefCntIsZero.
};
#endif
} else {
fDrawBuffer->flush();
}
+ fResourceCache->notifyFlushOccurred();
fFlushToReduceCacheSize = false;
}
get_resource_cache(fGpu)->resourceAccess().changeUniqueKey(this, key);
}
-void GrGpuResource::notifyIsPurgeable() const {
+void GrGpuResource::notifyAllCntsAreZero(CntType lastCntTypeToReachZero) const {
if (this->wasDestroyed()) {
// We've already been removed from the cache. Goodbye cruel world!
SkDELETE(this);
- } else {
- GrGpuResource* mutableThis = const_cast<GrGpuResource*>(this);
- get_resource_cache(fGpu)->resourceAccess().notifyPurgeable(mutableThis);
+ return;
}
+
+ // We should have already handled this fully in notifyRefCntIsZero().
+ SkASSERT(kRef_CntType != lastCntTypeToReachZero);
+
+ GrGpuResource* mutableThis = const_cast<GrGpuResource*>(this);
+ static const uint32_t kFlag =
+ GrResourceCache::ResourceAccess::kAllCntsReachedZero_RefNotificationFlag;
+ get_resource_cache(fGpu)->resourceAccess().notifyCntReachedZero(mutableThis, kFlag);
+}
+
+bool GrGpuResource::notifyRefCountIsZero() const {
+ if (this->wasDestroyed()) {
+ // handle this in notifyAllCntsAreZero().
+ return true;
+ }
+
+ GrGpuResource* mutableThis = const_cast<GrGpuResource*>(this);
+ uint32_t flags =
+ GrResourceCache::ResourceAccess::kRefCntReachedZero_RefNotificationFlag;
+ if (!this->internalHasPendingIO()) {
+ flags |= GrResourceCache::ResourceAccess::kAllCntsReachedZero_RefNotificationFlag;
+ }
+ get_resource_cache(fGpu)->resourceAccess().notifyCntReachedZero(mutableThis, flags);
+
+ // There is no need to call our notifyAllCntsAreZero function at this point since we already
+ // told the cache about the state of cnts.
+ return false;
}
void GrGpuResource::setScratchKey(const GrScratchKey& scratchKey) {
return static_cast<Domain>(domain);
}
+
uint32_t GrResourceKeyHash(const uint32_t* data, size_t size) {
return SkChecksum::Compute(data, size);
}
//////////////////////////////////////////////////////////////////////////////
-static const int kDefaultMaxCount = 2 * (1 << 12);
-static const size_t kDefaultMaxSize = 96 * (1 << 20);
GrResourceCache::GrResourceCache()
: fTimestamp(0)
, fMaxCount(kDefaultMaxCount)
, fMaxBytes(kDefaultMaxSize)
+ , fMaxUnusedFlushes(kDefaultMaxUnusedFlushes)
#if GR_CACHE_STATS
, fHighWaterCount(0)
, fHighWaterBytes(0)
, fBudgetedCount(0)
, fBudgetedBytes(0)
, fOverBudgetCB(NULL)
- , fOverBudgetData(NULL) {
+ , fOverBudgetData(NULL)
+ , fFlushTimestamps(NULL)
+ , fLastFlushTimestampIndex(0){
SkDEBUGCODE(fCount = 0;)
+ SkDEBUGCODE(fNewlyPurgeableResourceForValidation = NULL;)
+ this->resetFlushTimestamps();
}
GrResourceCache::~GrResourceCache() {
this->releaseAll();
+ SkDELETE(fFlushTimestamps);
}
-void GrResourceCache::setLimits(int count, size_t bytes) {
+void GrResourceCache::setLimits(int count, size_t bytes, int maxUnusedFlushes) {
fMaxCount = count;
fMaxBytes = bytes;
+ fMaxUnusedFlushes = maxUnusedFlushes;
+ this->resetFlushTimestamps();
this->purgeAsNeeded();
}
+void GrResourceCache::resetFlushTimestamps() {
+ SkDELETE(fFlushTimestamps);
+
+ // We assume this number is a power of two when wrapping indices into the timestamp array.
+ fMaxUnusedFlushes = SkNextPow2(fMaxUnusedFlushes);
+
+ // Since our implementation is to store the timestamps of the last fMaxUnusedFlushes flush calls
+ // we just turn the feature off if that array would be large.
+ static const int kMaxSupportedTimestampHistory = 128;
+
+ if (fMaxUnusedFlushes > kMaxSupportedTimestampHistory) {
+ fFlushTimestamps = NULL;
+ return;
+ }
+
+ fFlushTimestamps = SkNEW_ARRAY(uint32_t, fMaxUnusedFlushes);
+ fLastFlushTimestampIndex = 0;
+ // Set all the historical flush timestamps to initially be at the beginning of time (timestamp
+ // 0).
+ sk_bzero(fFlushTimestamps, fMaxUnusedFlushes * sizeof(uint32_t));
+}
+
void GrResourceCache::insertResource(GrGpuResource* resource) {
SkASSERT(resource);
SkASSERT(!this->isInCache(resource));
}
void GrResourceCache::removeUniqueKey(GrGpuResource* resource) {
- // Someone has a ref to this resource in order to invalidate it. When the ref count reaches
- // zero we will get a notifyPurgable() and figure out what to do with it.
+ // Someone has a ref to this resource in order to have removed the key. When the ref count
+ // reaches zero we will get a ref cnt notification and figure out what to do with it.
if (resource->getUniqueKey().isValid()) {
SkASSERT(resource == fUniqueHash.find(resource->getUniqueKey()));
fUniqueHash.remove(resource->getUniqueKey());
this->validate();
}
-void GrResourceCache::notifyPurgeable(GrGpuResource* resource) {
+void GrResourceCache::notifyCntReachedZero(GrGpuResource* resource, uint32_t flags) {
SkASSERT(resource);
+ SkASSERT(!resource->wasDestroyed());
+ SkASSERT(flags);
SkASSERT(this->isInCache(resource));
- SkASSERT(resource->isPurgeable());
+ // This resource should always be in the nonpurgeable array when this function is called. It
+ // will be moved to the queue if it is newly purgeable.
+ SkASSERT(fNonpurgeableResources[*resource->cacheAccess().accessCacheIndex()] == resource);
+
+ if (SkToBool(ResourceAccess::kRefCntReachedZero_RefNotificationFlag & flags)) {
+#ifdef SK_DEBUG
+ // 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()) {
+ fNewlyPurgeableResourceForValidation = resource;
+ }
+#endif
+ resource->cacheAccess().setTimestamp(this->getNextTimestamp());
+ SkDEBUGCODE(fNewlyPurgeableResourceForValidation = NULL);
+ }
+ if (!SkToBool(ResourceAccess::kAllCntsReachedZero_RefNotificationFlag & flags)) {
+ SkASSERT(!resource->isPurgeable());
+ return;
+ }
+
+ SkASSERT(resource->isPurgeable());
this->removeFromNonpurgeableArray(resource);
fPurgeableQueue.insert(resource);
this->validate();
}
-void GrResourceCache::internalPurgeAsNeeded() {
- SkASSERT(this->overBudget());
+void GrResourceCache::purgeAsNeeded() {
+ SkTArray<GrUniqueKeyInvalidatedMessage> invalidKeyMsgs;
+ fInvalidUniqueKeyInbox.poll(&invalidKeyMsgs);
+ if (invalidKeyMsgs.count()) {
+ this->processInvalidUniqueKeys(invalidKeyMsgs);
+ }
- bool stillOverbudget = true;
- while (fPurgeableQueue.count()) {
+ if (fFlushTimestamps) {
+ // Assuming kNumFlushesToDeleteUnusedResource is a power of 2.
+ SkASSERT(SkIsPow2(fMaxUnusedFlushes));
+ int oldestFlushIndex = (fLastFlushTimestampIndex + 1) & (fMaxUnusedFlushes - 1);
+
+ uint32_t oldestAllowedTimestamp = fFlushTimestamps[oldestFlushIndex];
+ while (fPurgeableQueue.count()) {
+ uint32_t oldestResourceTimestamp = fPurgeableQueue.peek()->cacheAccess().timestamp();
+ if (oldestAllowedTimestamp < oldestResourceTimestamp) {
+ break;
+ }
+ GrGpuResource* resource = fPurgeableQueue.peek();
+ SkASSERT(resource->isPurgeable());
+ resource->cacheAccess().release();
+ }
+ }
+
+ bool stillOverbudget = this->overBudget();
+ while (stillOverbudget && fPurgeableQueue.count()) {
GrGpuResource* resource = fPurgeableQueue.peek();
SkASSERT(resource->isPurgeable());
resource->cacheAccess().release();
- if (!this->overBudget()) {
- stillOverbudget = false;
- break;
- }
+ stillOverbudget = this->overBudget();
}
this->validate();
if (stillOverbudget) {
// Despite the purge we're still over budget. Call our over budget callback. If this frees
- // any resources then we'll get notifyPurgeable() calls and take appropriate action.
+ // any resources then we'll get notified and take appropriate action.
(*fOverBudgetCB)(fOverBudgetData);
this->validate();
}
GrGpuResource* resource = this->findAndRefUniqueResource(msgs[i].key());
if (resource) {
resource->resourcePriv().removeUniqueKey();
- resource->unref(); // will call notifyPurgeable, if it is indeed now purgeable.
+ resource->unref(); // If this resource is now purgeable, the cache will be notified.
}
}
}
// count should be the next timestamp we return.
SkASSERT(fTimestamp == SkToU32(count));
+
+ // The historical timestamps of flushes are now invalid.
+ this->resetFlushTimestamps();
}
}
return fTimestamp++;
}
+void GrResourceCache::notifyFlushOccurred() {
+ if (fFlushTimestamps) {
+ SkASSERT(SkIsPow2(fMaxUnusedFlushes));
+ fLastFlushTimestampIndex = (fLastFlushTimestampIndex + 1) & (fMaxUnusedFlushes - 1);
+ // get the timestamp before accessing fFlushTimestamps because getNextTimestamp will
+ // reallocate fFlushTimestamps on timestamp overflow.
+ uint32_t timestamp = this->getNextTimestamp();
+ fFlushTimestamps[fLastFlushTimestampIndex] = timestamp;
+ this->purgeAsNeeded();
+ }
+}
+
#ifdef SK_DEBUG
void GrResourceCache::validate() const {
// Reduce the frequency of validations for large resource counts.
Stats stats(this);
for (int i = 0; i < fNonpurgeableResources.count(); ++i) {
- SkASSERT(!fNonpurgeableResources[i]->isPurgeable());
+ SkASSERT(!fNonpurgeableResources[i]->isPurgeable() ||
+ fNewlyPurgeableResourceForValidation == fNonpurgeableResources[i]);
SkASSERT(*fNonpurgeableResources[i]->cacheAccess().accessCacheIndex() == i);
SkASSERT(!fNonpurgeableResources[i]->wasDestroyed());
stats.update(fNonpurgeableResources[i]);
SkASSERT(stats.fContent == fUniqueHash.count());
SkASSERT(stats.fScratch + stats.fCouldBeScratch == fScratchMap.count());
- // This assertion is not currently valid because we can be in recursive notifyIsPurgeable()
+ // This assertion is not currently valid because we can be in recursive notifyCntReachedZero()
// calls. This will be fixed when subresource registration is explicit.
// bool overBudget = budgetedBytes > fMaxBytes || budgetedCount > fMaxCount;
// SkASSERT(!overBudget || locked == count || fPurging);
* A unique key always takes precedence over a scratch key when a resource has both types of keys.
* If a resource has neither key type then it will be deleted as soon as the last reference to it
* is dropped.
+ *
+ * When proactive purging is enabled, on every flush, the timestamp of that flush is stored in a
+ * n-sized ring buffer. When purging occurs each purgeable resource's timestamp is compared to the
+ * timestamp of the n-th prior flush. If the resource's last use timestamp is older than the old
+ * flush then the resource is proactively purged even when the cache is under budget. By default
+ * this feature is disabled, though it can be enabled by calling GrResourceCache::setLimits.
*/
class GrResourceCache {
public:
GrResourceCache();
~GrResourceCache();
+ // Default maximum number of budgeted resources in the cache.
+ static const int kDefaultMaxCount = 2 * (1 << 12);
+ // Default maximum number of bytes of gpu memory of budgeted resources in the cache.
+ static const size_t kDefaultMaxSize = 96 * (1 << 20);
+ // Default number of flushes a budgeted resources can go unused in the cache before it is
+ // purged. Large values disable the feature (as the ring buffer of flush timestamps would be
+ // large). This is currently the default until we decide to enable this feature
+ // of the cache by default.
+ static const int kDefaultMaxUnusedFlushes = 1024;
+
/** Used to access functionality needed by GrGpuResource for lifetime management. */
class ResourceAccess;
ResourceAccess resourceAccess();
/**
- * Sets the cache limits in terms of number of resources and max gpu memory byte size.
+ * Sets the cache limits in terms of number of resources, max gpu memory byte size, and number
+ * of GrContext flushes that a resource can be unused before it is evicted. The latter value is
+ * a suggestion and there is no promise that a resource will be purged immediately after it
+ * hasn't been used in maxUnusedFlushes flushes.
*/
- void setLimits(int count, size_t bytes);
+ void setLimits(int count, size_t bytes, int maxUnusedFlushes = kDefaultMaxUnusedFlushes);
/**
* Returns the number of resources.
/** Purges resources to become under budget and processes resources with invalidated unique
keys. */
- void purgeAsNeeded() {
- SkTArray<GrUniqueKeyInvalidatedMessage> invalidKeyMsgs;
- fInvalidUniqueKeyInbox.poll(&invalidKeyMsgs);
- if (invalidKeyMsgs.count()) {
- this->processInvalidUniqueKeys(invalidKeyMsgs);
- }
- if (fBudgetedCount <= fMaxCount && fBudgetedBytes <= fMaxBytes) {
- return;
- }
- this->internalPurgeAsNeeded();
- }
+ void purgeAsNeeded();
/** Purges all resources that don't have external owners. */
void purgeAllUnlocked();
fOverBudgetCB = overBudgetCB;
fOverBudgetData = data;
}
+
+ void notifyFlushOccurred();
#if GR_GPU_STATS
void dumpStats(SkString*) const;
////
void insertResource(GrGpuResource*);
void removeResource(GrGpuResource*);
- void notifyPurgeable(GrGpuResource*);
+ void notifyCntReachedZero(GrGpuResource*, uint32_t flags);
void didChangeGpuMemorySize(const GrGpuResource*, size_t oldSize);
void changeUniqueKey(GrGpuResource*, const GrUniqueKey&);
void removeUniqueKey(GrGpuResource*);
void refAndMakeResourceMRU(GrGpuResource*);
/// @}
- void internalPurgeAsNeeded();
+ void resetFlushTimestamps();
void processInvalidUniqueKeys(const SkTArray<GrUniqueKeyInvalidatedMessage>&);
void addToNonpurgeableArray(GrGpuResource*);
void removeFromNonpurgeableArray(GrGpuResource*);
// our budget, used in purgeAsNeeded()
int fMaxCount;
size_t fMaxBytes;
+ int fMaxUnusedFlushes;
#if GR_CACHE_STATS
int fHighWaterCount;
PFOverBudgetCB fOverBudgetCB;
void* fOverBudgetData;
+ // We keep track of the "timestamps" of the last n flushes. If a resource hasn't been used in
+ // that time then we well preemptively purge it to reduce memory usage.
+ uint32_t* fFlushTimestamps;
+ int fLastFlushTimestampIndex;
+
InvalidUniqueKeyInbox fInvalidUniqueKeyInbox;
+
+ // This resource is allowed to be in the nonpurgeable array for the sake of validate() because
+ // we're in the midst of converting it to purgeable status.
+ SkDEBUGCODE(GrGpuResource* fNewlyPurgeableResourceForValidation;)
};
class GrResourceCache::ResourceAccess {
void removeResource(GrGpuResource* resource) { fCache->removeResource(resource); }
/**
- * Called by GrGpuResources when they detects that they are newly purgeable.
+ * Notifications that should be sent to the cache when the ref/io cnt status of resources
+ * changes.
+ */
+ enum RefNotificationFlags {
+ /** All types of refs on the resource have reached zero. */
+ kAllCntsReachedZero_RefNotificationFlag = 0x1,
+ /** The normal (not pending IO type) ref cnt has reached zero. */
+ kRefCntReachedZero_RefNotificationFlag = 0x2,
+ };
+ /**
+ * Called by GrGpuResources when they detect that their ref/io cnts have reached zero. When the
+ * normal ref cnt reaches zero the flags that are set should be:
+ * a) kRefCntReachedZero if a pending IO cnt is still non-zero.
+ * b) (kRefCntReachedZero | kAllCntsReachedZero) when all pending IO cnts are also zero.
+ * kAllCntsReachedZero is set by itself if a pending IO cnt is decremented to zero and all the
+ * the other cnts are already zero.
*/
- void notifyPurgeable(GrGpuResource* resource) { fCache->notifyPurgeable(resource); }
+ void notifyCntReachedZero(GrGpuResource* resource, uint32_t flags) {
+ fCache->notifyCntReachedZero(resource, flags);
+ }
/**
* Called by GrGpuResources when their sizes change.
* found in the LICENSE file.
*/
+// Include here to ensure SK_SUPPORT_GPU is set correctly before it is examined.
+#include "SkTypes.h"
+
#if SK_SUPPORT_GPU
#include "GrContext.h"
}
}
+static void test_flush(skiatest::Reporter* reporter) {
+ Mock mock(1000000, 1000000);
+ GrContext* context = mock.context();
+ GrResourceCache* cache = mock.cache();
+
+ // The current cache impl will round the max flush count to the next power of 2. So we choose a
+ // power of two here to keep things simpler.
+ static const int kFlushCount = 16;
+ cache->setLimits(1000000, 1000000, kFlushCount);
+
+ {
+ // Insert a resource and send a flush notification kFlushCount times.
+ for (int i = 0; i < kFlushCount; ++i) {
+ TestResource* r = SkNEW_ARGS(TestResource, (context->getGpu()));
+ GrUniqueKey k;
+ make_unique_key<1>(&k, i);
+ r->resourcePriv().setUniqueKey(k);
+ r->unref();
+ cache->notifyFlushOccurred();
+ }
+
+ // Send flush notifications to the cache. Each flush should purge the oldest resource.
+ for (int i = 0; i < kFlushCount - 1; ++i) {
+ // The first resource was purged after the last flush in the initial loop, hence the -1.
+ REPORTER_ASSERT(reporter, kFlushCount - i - 1 == cache->getResourceCount());
+ for (int j = 0; j < i; ++j) {
+ GrUniqueKey k;
+ make_unique_key<1>(&k, j);
+ GrGpuResource* r = cache->findAndRefUniqueResource(k);
+ REPORTER_ASSERT(reporter, !SkToBool(r));
+ SkSafeUnref(r);
+ }
+ cache->notifyFlushOccurred();
+ }
+
+ REPORTER_ASSERT(reporter, 0 == cache->getResourceCount());
+ cache->purgeAllUnlocked();
+ }
+
+ // Do a similar test but where we leave refs on some resources to prevent them from being
+ // purged.
+ {
+ GrGpuResource* refedResources[kFlushCount >> 1];
+ for (int i = 0; i < kFlushCount; ++i) {
+ TestResource* r = SkNEW_ARGS(TestResource, (context->getGpu()));
+ GrUniqueKey k;
+ make_unique_key<1>(&k, i);
+ r->resourcePriv().setUniqueKey(k);
+ // Leave a ref on every other resource, beginning with the first.
+ if (SkToBool(i & 0x1)) {
+ refedResources[i/2] = r;
+ } else {
+ r->unref();
+ }
+ cache->notifyFlushOccurred();
+ }
+
+ for (int i = 0; i < kFlushCount; ++i) {
+ // Should get a resource purged every other flush.
+ REPORTER_ASSERT(reporter, kFlushCount - i/2 - 1 == cache->getResourceCount());
+ cache->notifyFlushOccurred();
+ }
+
+ // Unref all the resources that we kept refs on in the first loop.
+ for (int i = 0; i < kFlushCount >> 1; ++i) {
+ refedResources[i]->unref();
+ }
+
+ // When we unref'ed them their timestamps got updated. So nothing should be purged until we
+ // get kFlushCount additional flushes. Then everything should be purged.
+ for (int i = 0; i < kFlushCount; ++i) {
+ REPORTER_ASSERT(reporter, kFlushCount >> 1 == cache->getResourceCount());
+ cache->notifyFlushOccurred();
+ }
+ REPORTER_ASSERT(reporter, 0 == cache->getResourceCount());
+
+ cache->purgeAllUnlocked();
+ }
+
+ REPORTER_ASSERT(reporter, 0 == cache->getResourceCount());
+}
+
static void test_large_resource_count(skiatest::Reporter* reporter) {
// Set the cache size to double the resource count because we're going to create 2x that number
// resources, using two different key domains. Add a little slop to the bytes because we resize
test_cache_chained_purge(reporter);
test_resource_size_changed(reporter);
test_timestamp_wrap(reporter);
+ test_flush(reporter);
test_large_resource_count(reporter);
}