/* ScummVM - Graphic Adventure Engine * * ScummVM is the legal property of its developers, whose names * are too numerous to list here. Please refer to the COPYRIGHT * file distributed with this source distribution. * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . * */ //============================================================================= // // ResourceCache is an abstract storage that tracks use history with MRU list. // Cache is limited to a certain size, in bytes. // When a total size of items reaches the limit, and more items are put into, // the Cache uses MRU list to find the least used items and disposes them // one by one until the necessary space is freed. // ResourceCache's implementations must provide a method for calculating an // item's size. // // Supports copyable and movable items, have 2 variants of Put function for // each of them. This lets it store both std::shared_ptr and std::unique_ptr. // // TODO: support data Priority, which tells which items may be disposed // when adding new item and surpassing the cache limit. // // TODO: as an option, consider to have Locked items separate from the normal // cache limit, and probably have their own limit setting as a safety measure. // (after reaching this limit ResourceCache would simply ignore any further // Lock commands until some items are unlocked.) // Rethink this when it's time to design a better resource handling in AGS. // // TODO: as an option, consider supporting a specialized container type that // has an associative container's interface, but is optimized for having most // keys allocated in large continious sequences by default. // This may be suitable for e.g. sprites, and may (or may not?) save some mem. // // Only for the reference: one of the ideas is for container to have a table // of arrays of fixed size internally. When getting an item the hash would be // first divided on array size to find the array the item resides in, then the // item is taken from item from slot index = (hash - arrsize * arrindex). // Find out if there is already a hash table kind that follows similar // principle. // //============================================================================= #ifndef AGS_SHARED_UTIL_RESOURCE_CACHE_H #define AGS_SHARED_UTIL_RESOURCE_CACHE_H #include "common/std/list.h" #include "common/std/map.h" #include "ags/shared/util/string.h" namespace AGS3 { namespace AGS { namespace Shared { template > class ResourceCache { public: // Flags determine management rules for the particular item enum ItemFlags { // Locked items are temporarily saved from disposal when freeing cache space; // they still count towards the cache size though. kCacheItem_Locked = 0x0001, // External items are managed strictly by the external user; // do not count towards the cache size, do not prevent caching normal items. // They cannot be locked or released (considered permanently locked), // only removed by request. kCacheItem_External = 0x0002, }; ResourceCache(TSize max_size = 0u) : _maxSize(max_size), _sectionLocked(_mru.end()) {} // Get the MRU cache size limit inline size_t GetMaxCacheSize() const { return _maxSize; } // Get the current total MRU cache size inline size_t GetCacheSize() const { return _cacheSize; } // Get the summed size of locked items (included in total cache size) inline size_t GetLockedSize() const { return _lockedSize; } // Get the summed size of external items (excluded from total cache size) inline size_t GetExternalSize() const { return _externalSize; } // Set the MRU cache size limit void SetMaxCacheSize(TSize size) { _maxSize = size; FreeMem(0u); // makes sure it does not exceed max size } // Tells if particular key is in the cache bool Exists(const TKey &key) const { return _storage.find(key) != _storage.end(); } // Gets the item with the given key if it exists; // reorders the item as recently used. const TValue &Get(const TKey &key) { auto it = _storage.find(key); if (it == _storage.end()) return _dummy; // no such key // Unless locked, move the item ref to the beginning of the MRU list const auto &item = it->second; if ((item.Flags & kCacheItem_Locked) == 0) _mru.splice(_mru.begin(), _mru, item.MruIt); return item.Value; } // Add particular item into the cache, disposes existing item if such key is already taken. // If a new item will exceed the cache size limit, cache will remove oldest items // in order to free mem. void Put(const TKey &key, const TValue &value, uint32_t flags = 0u) { if (_maxSize == 0) return; // cache is disabled auto it = _storage.find(key); if (it != _storage.end()) { // Remove previous cached item RemoveImpl(it); } PutImpl(key, TValue(value), flags); // make a temp local copy for safe std::move } void Put(const TKey &key, TValue &&value, uint32_t flags = 0u) { if (_maxSize == 0) return; // cache is disabled auto it = _storage.find(key); if (it != _storage.end()) { // Remove previous cached item RemoveImpl(it); } PutImpl(key, std::move(value), flags); } // Locks the item with the given key, // temporarily excluding it from MRU disposal rules void Lock(const TKey &key) { auto it = _storage.find(key); if (it == _storage.end()) return; // no such key auto &item = it->second; if ((item.Flags & kCacheItem_Locked) != 0) return; // already locked // Lock item and move to the locked section item.Flags |= kCacheItem_Locked; _mru.splice(_sectionLocked, _mru, item.MruIt); // CHECKME: TEST!! _sectionLocked = item.MruIt; _lockedSize += item.Size; } // Releases (unlocks) the item with the given key, // adds it back to MRU disposal rules void Release(const TKey &key) { auto it = _storage.find(key); if (it == _storage.end()) return; // no such key auto &item = it->second; if ((item.Flags & kCacheItem_External) != 0) return; // never release external data, must be removed by user if ((item.Flags & kCacheItem_Locked) == 0) return; // not locked // Unlock, and move the item to the beginning of the MRU list item.Flags &= ~kCacheItem_Locked; if (_sectionLocked == item.MruIt) _sectionLocked = std::next(item.MruIt); _mru.splice(_mru.begin(), _mru, item.MruIt); // CHECKME: TEST!! _lockedSize -= item.Size; } // Deletes the cached item void Dispose(const TKey &key) { auto it = _storage.find(key); if (it == _storage.end()) return; // no such key RemoveImpl(it); } // Removes the item from the cache and returns to the caller. TValue Remove(const TKey &key) { auto it = _storage.find(key); if (it == _storage.end()) return TValue(); // no such key TValue value = std::move(it->second.Value); RemoveImpl(it); return value; } // Disposes all items that are not locked or external void DisposeFreeItems() { for (auto mru_it = _sectionLocked; mru_it != _mru.end(); ++mru_it) { auto it = _storage.find(*mru_it); assert(it != _storage.end()); auto &item = it->second; _cacheSize -= item.Size; _storage.erase(it); _mru.erase(mru_it); } } // Clear the cache, dispose all items void Clear() { _storage.clear(); _mru.clear(); _sectionLocked = _mru.end(); _cacheSize = 0u; _lockedSize = 0u; _externalSize = 0u; } protected: struct TItem; // MRU list type typedef std::list TMruList; // MRU list reference type typedef typename TMruList::iterator TMruIt; // Storage type typedef std::unordered_map TStorage; struct TItem { TMruIt MruIt; // MRU list reference TValue Value; TSize Size = 0u; uint32_t Flags = 0u; // flags determine management rules for this item TItem() = default; TItem(const TItem &item) = default; TItem(TItem &&item) = default; TItem(const TMruIt &mru_it, const TValue &value, const TSize size, uint32_t flags) : MruIt(mru_it), Value(value), Size(size), Flags(flags) {} TItem(const TMruIt &mru_it, TValue &&value, const TSize size, uint32_t flags) : MruIt(mru_it), Value(std::move(value)), Size(size), Flags(flags) {} TItem &operator=(const TItem &item) = default; TItem &operator=(TItem &&item) = default; }; // Calculates item size; expects to return 0 if an item is invalid // and should not be added to the cache. virtual TSize CalcSize(const TValue &item) = 0; private: // Add particular item into the cache. // If a new item will exceed the cache size limit, cache will remove oldest items // in order to free mem. void PutImpl(const TKey &key, TValue &&value, uint32_t flags) { // Request item's size, and test if it's a valid item TSize size = CalcSize(value); if (size == 0u) return; // invalid item if ((flags & kCacheItem_External) == 0) { // clear up space before adding if (_cacheSize + size > _maxSize) FreeMem(size); _cacheSize += size; } else { // always mark external data as locked, easier to handle flags |= kCacheItem_Locked; _externalSize += size; } // Prepare a MRU slot, then add an item TMruIt mru_it = _mru.end(); // only normal items are added to MRU at all if ((flags & kCacheItem_External) == 0) { if ((flags & kCacheItem_Locked) == 0) { // normal item, add to the list mru_it = _mru.insert(_mru.begin(), key); } else { // locked item, add to the dedicated list section mru_it = _mru.insert(_sectionLocked, key); _sectionLocked = mru_it; _lockedSize += size; } } TItem item = TItem(mru_it, std::move(value), size, flags); _storage[key] = std::move(item); } // Removes the item from the container void RemoveImpl(typename TStorage::iterator it) { auto &item = it->second; // normal items are removed from MRU, and discounted from cache size if ((item.Flags & kCacheItem_External) == 0) { TMruIt mru_it = item.MruIt; if (_sectionLocked == mru_it) _sectionLocked = std::next(mru_it); _cacheSize -= item.Size; if ((item.Flags & kCacheItem_Locked) != 0) _lockedSize -= item.Size; _mru.erase(mru_it); } else { _externalSize -= item.Size; } _storage.erase(it); } // Remove the oldest (least recently used) item in cache void DisposeOldest() { assert(_mru.begin() != _sectionLocked); if (_mru.begin() == _sectionLocked) return; // Remove from the storage and mru list auto mru_it = std::prev(_sectionLocked); auto it = _storage.find(*mru_it); assert(it != _storage.end()); auto &item = it->second; assert((item.Flags & (kCacheItem_Locked | kCacheItem_External)) == 0); _cacheSize -= item.Size; _storage.erase(it); _mru.erase(mru_it); } // Keep disposing oldest elements until cache has at least the given free space void FreeMem(size_t space) { // TODO: consider sprite cache's behavior where it would just clear // whole cache in case disposing one by one were taking too much iterations while ((_mru.begin() != _sectionLocked) && (_cacheSize + space > _maxSize)) { DisposeOldest(); } } // Size of tracked data stored in this cache; // note that this is an abstract value, which may or not refer to an // actual size in bytes, and depends on the implementation. TSize _cacheSize = 0u; // Size of data locked (forbidden from disposal), // this size is *included* in _cacheSize; provided for stats. TSize _lockedSize = 0u; // Size of the external data, that is - data that does not count towards // cache limit, and which is not our reponsibility; provided for stats. TSize _externalSize = 0u; // Maximal size of tracked data. // When the inserted item increases the cache size past this limit, // the cache will try to free the space by removing oldest items. // "External" data does not count towards this limit. TSize _maxSize = 0u; // MRU list: the way to track which items were used recently. // When clearing up space for new items, cache first deletes the items // that were last time used long ago. TMruList _mru; // A locked section border iterator, points to the *last* locked item // starting from the end of the list, or equals _mru.end() if there's none. TMruIt _sectionLocked; // Key-to-mru lookup map TStorage _storage; // Dummy value, return in case of a missing key TValue _dummy; }; } // namespace Shared } // namespace AGS } // namespace AGS3 #endif // AGS_SHARED_UTIL_RESOURCE_CACHE_H