1
0
Fork 0

Change: Simplify sprite cache memory management

* Remove custom allocator
* Use std::unique_ptr for sprite data
* Perform LRU cache eviction in a single pass
pull/13565/merge
Peter Nelson 2024-01-28 14:46:58 +00:00 committed by Peter Nelson
parent 1d67094863
commit 7744f49a9e
4 changed files with 99 additions and 247 deletions

View File

@ -8,11 +8,8 @@
/** @file spritecache.cpp Caching of sprites. */ /** @file spritecache.cpp Caching of sprites. */
#include "stdafx.h" #include "stdafx.h"
#include "random_access_file_type.h"
#include "spriteloader/grf.hpp" #include "spriteloader/grf.hpp"
#include "spriteloader/makeindexed.h" #include "spriteloader/makeindexed.h"
#include "gfx_func.h"
#include "error.h"
#include "error_func.h" #include "error_func.h"
#include "strings_func.h" #include "strings_func.h"
#include "zoom_func.h" #include "zoom_func.h"
@ -24,7 +21,6 @@
#include "spritecache_internal.h" #include "spritecache_internal.h"
#include "table/sprites.h" #include "table/sprites.h"
#include "table/strings.h"
#include "table/palette_convert.h" #include "table/palette_convert.h"
#include "safeguards.h" #include "safeguards.h"
@ -34,6 +30,8 @@ uint _sprite_cache_size = 4;
static std::vector<SpriteCache> _spritecache; static std::vector<SpriteCache> _spritecache;
static size_t _spritecache_bytes_used = 0;
static uint32_t _sprite_lru_counter;
static std::vector<std::unique_ptr<SpriteFile>> _sprite_files; static std::vector<std::unique_ptr<SpriteFile>> _sprite_files;
static inline SpriteCache *GetSpriteCache(uint index) static inline SpriteCache *GetSpriteCache(uint index)
@ -97,18 +95,6 @@ SpriteFile &OpenCachedSpriteFile(const std::string &filename, Subdirectory subdi
return *file; return *file;
} }
struct MemBlock {
size_t size;
uint8_t data[];
};
static uint _sprite_lru_counter;
static MemBlock *_spritecache_ptr;
static uint _allocated_sprite_cache_size = 0;
static int _compact_cache_counter;
static void CompactSpriteCache();
/** /**
* Skip the given amount of sprite graphics data. * Skip the given amount of sprite graphics data.
* @param type the type of sprite (compressed etc) * @param type the type of sprite (compressed etc)
@ -641,7 +627,6 @@ bool LoadNextSprite(SpriteID load_index, SpriteFile &file, uint file_sprite_id)
uint8_t grf_type = file.ReadByte(); uint8_t grf_type = file.ReadByte();
SpriteType type; SpriteType type;
void *data = nullptr;
uint8_t control_flags = 0; uint8_t control_flags = 0;
if (grf_type == 0xFF) { if (grf_type == 0xFF) {
/* Some NewGRF files have "empty" pseudo-sprites which are 1 /* Some NewGRF files have "empty" pseudo-sprites which are 1
@ -692,7 +677,6 @@ bool LoadNextSprite(SpriteID load_index, SpriteFile &file, uint file_sprite_id)
sc->file = &file; sc->file = &file;
sc->file_pos = file_pos; sc->file_pos = file_pos;
sc->length = num; sc->length = num;
sc->ptr = data;
sc->lru = 0; sc->lru = 0;
sc->id = file_sprite_id; sc->id = file_sprite_id;
sc->type = type; sc->type = type;
@ -710,7 +694,7 @@ void DupSprite(SpriteID old_spr, SpriteID new_spr)
scnew->file = scold->file; scnew->file = scold->file;
scnew->file_pos = scold->file_pos; scnew->file_pos = scold->file_pos;
scnew->ptr = nullptr; scnew->ClearSpriteData();
scnew->id = scold->id; scnew->id = scold->id;
scnew->type = scold->type; scnew->type = scold->type;
scnew->warned = false; scnew->warned = false;
@ -718,188 +702,103 @@ void DupSprite(SpriteID old_spr, SpriteID new_spr)
} }
/** /**
* S_FREE_MASK is used to mask-out lower bits of MemBlock::size * Delete entries from the sprite cache to remove the requested number of bytes.
* If they are non-zero, the block is free. * Sprite data is removed in order of LRU values.
* S_FREE_MASK has to ensure MemBlock is correctly aligned - * The total number of bytes removed may be larger than the number requested.
* it means 8B (S_FREE_MASK == 7) on 64bit systems! * @param to_remove Requested number of bytes to remove.
*/ */
static const size_t S_FREE_MASK = sizeof(size_t) - 1; static void DeleteEntriesFromSpriteCache(size_t to_remove)
/* to make sure nobody adds things to MemBlock without checking S_FREE_MASK first */
static_assert(sizeof(MemBlock) == sizeof(size_t));
/* make sure it's a power of two */
static_assert((sizeof(size_t) & (sizeof(size_t) - 1)) == 0);
static inline MemBlock *NextBlock(MemBlock *block)
{ {
return (MemBlock*)((uint8_t*)block + (block->size & ~S_FREE_MASK)); const size_t initial_in_use = _spritecache_bytes_used;
}
static size_t GetSpriteCacheUsage() struct SpriteInfo {
{ uint32_t lru;
size_t tot_size = 0; SpriteID id;
MemBlock *s; size_t size;
for (s = _spritecache_ptr; s->size != 0; s = NextBlock(s)) { bool operator<(const SpriteInfo &other) const
if (!(s->size & S_FREE_MASK)) tot_size += s->size; {
return this->lru < other.lru;
}
};
std::vector<SpriteInfo> candidates; // max heap, ordered by LRU
size_t candidate_bytes = 0; // total bytes that would be released when clearing all sprites in candidates
auto push = [&](SpriteInfo info) {
candidates.push_back(info);
std::push_heap(candidates.begin(), candidates.end());
candidate_bytes += info.size;
};
auto pop = [&]() {
candidate_bytes -= candidates.front().size;
std::pop_heap(candidates.begin(), candidates.end());
candidates.pop_back();
};
SpriteID i = 0;
for (; i != static_cast<SpriteID>(_spritecache.size()) && candidate_bytes < to_remove; i++) {
const SpriteCache *sc = GetSpriteCache(i);
if (sc->ptr != nullptr) {
push({ sc->lru, i, sc->length });
if (candidate_bytes >= to_remove) break;
}
}
/* candidates now contains enough bytes to meet to_remove.
* only sprites with LRU values <= the maximum (i.e. the top of the heap) need to be considered */
for (; i != static_cast<SpriteID>(_spritecache.size()); i++) {
const SpriteCache *sc = GetSpriteCache(i);
if (sc->ptr != nullptr && sc->lru <= candidates.front().lru) {
push({ sc->lru, i, sc->length });
while (!candidates.empty() && candidate_bytes - candidates.front().size >= to_remove) {
pop();
}
}
} }
return tot_size; for (const auto &it : candidates) {
} GetSpriteCache(it.id)->ClearSpriteData();
}
Debug(sprite, 3, "DeleteEntriesFromSpriteCache, deleted: {}, freed: {}, in use: {} --> {}, requested: {}",
candidates.size(), candidate_bytes, initial_in_use, _spritecache_bytes_used, to_remove);
}
void IncreaseSpriteLRU() void IncreaseSpriteLRU()
{ {
/* Increase all LRU values */ int bpp = BlitterFactory::GetCurrentBlitter()->GetScreenDepth();
if (_sprite_lru_counter > 16384) { uint target_size = (bpp > 0 ? _sprite_cache_size * bpp / 8 : 1) * 1024 * 1024;
Debug(sprite, 5, "Fixing lru {}, inuse={}", _sprite_lru_counter, GetSpriteCacheUsage()); if (_spritecache_bytes_used > target_size) {
DeleteEntriesFromSpriteCache(_spritecache_bytes_used - target_size + 512 * 1024);
}
if (_sprite_lru_counter >= 0xC0000000) {
Debug(sprite, 3, "Fixing lru {}, inuse={}", _sprite_lru_counter, _spritecache_bytes_used);
for (SpriteCache &sc : _spritecache) { for (SpriteCache &sc : _spritecache) {
if (sc.ptr != nullptr) { if (sc.ptr != nullptr) {
if (sc.lru >= 0) { if (sc.lru > 0x80000000) {
sc.lru = -1; sc.lru -= 0x80000000;
} else if (sc.lru != -32768) { } else {
sc.lru--; sc.lru = 0;
} }
} }
} }
_sprite_lru_counter = 0; _sprite_lru_counter -= 0x80000000;
}
/* Compact sprite cache every now and then. */
if (++_compact_cache_counter >= 740) {
CompactSpriteCache();
_compact_cache_counter = 0;
} }
} }
/** void SpriteCache::ClearSpriteData()
* Called when holes in the sprite cache should be removed.
* That is accomplished by moving the cached data.
*/
static void CompactSpriteCache()
{ {
MemBlock *s; _spritecache_bytes_used -= this->length;
this->ptr.reset();
Debug(sprite, 3, "Compacting sprite cache, inuse={}", GetSpriteCacheUsage());
for (s = _spritecache_ptr; s->size != 0;) {
if (s->size & S_FREE_MASK) {
MemBlock *next = NextBlock(s);
MemBlock temp;
SpriteID i;
/* Since free blocks are automatically coalesced, this should hold true. */
assert(!(next->size & S_FREE_MASK));
/* If the next block is the sentinel block, we can safely return */
if (next->size == 0) break;
/* Locate the sprite belonging to the next pointer. */
for (i = 0; GetSpriteCache(i)->ptr != next->data; i++) {
assert(i != _spritecache.size());
}
GetSpriteCache(i)->ptr = s->data; // Adjust sprite array entry
/* Swap this and the next block */
temp = *s;
std::byte *p = reinterpret_cast<std::byte *>(next);
std::move(p, &p[next->size], reinterpret_cast<std::byte *>(s));
s = NextBlock(s);
*s = temp;
/* Coalesce free blocks */
while (NextBlock(s)->size & S_FREE_MASK) {
s->size += NextBlock(s)->size & ~S_FREE_MASK;
}
} else {
s = NextBlock(s);
}
}
}
/**
* Delete a single entry from the sprite cache.
* @param item Entry to delete.
*/
static void DeleteEntryFromSpriteCache(SpriteCache *item)
{
/* Mark the block as free (the block must be in use) */
MemBlock *s = static_cast<MemBlock *>(item->ptr) - 1;
assert(!(s->size & S_FREE_MASK));
s->size |= S_FREE_MASK;
item->ptr = nullptr;
/* And coalesce adjacent free blocks */
for (s = _spritecache_ptr; s->size != 0; s = NextBlock(s)) {
if (s->size & S_FREE_MASK) {
while (NextBlock(s)->size & S_FREE_MASK) {
s->size += NextBlock(s)->size & ~S_FREE_MASK;
}
}
}
}
static void DeleteEntryFromSpriteCache()
{
Debug(sprite, 3, "DeleteEntryFromSpriteCache, inuse={}", GetSpriteCacheUsage());
SpriteCache *best = nullptr;
int cur_lru = 0xffff;
for (SpriteCache &sc : _spritecache) {
if (sc.ptr != nullptr && sc.lru < cur_lru) {
cur_lru = sc.lru;
best = &sc;
}
}
/* Display an error message and die, in case we found no sprite at all.
* This shouldn't really happen, unless all sprites are locked. */
if (best == nullptr) FatalError("Out of sprite memory");
DeleteEntryFromSpriteCache(best);
}
void *CacheSpriteAllocator::AllocatePtr(size_t mem_req)
{
mem_req += sizeof(MemBlock);
/* Align this to correct boundary. This also makes sure at least one
* bit is not used, so we can use it for other things. */
mem_req = Align(mem_req, S_FREE_MASK + 1);
for (;;) {
MemBlock *s;
for (s = _spritecache_ptr; s->size != 0; s = NextBlock(s)) {
if (s->size & S_FREE_MASK) {
size_t cur_size = s->size & ~S_FREE_MASK;
/* Is the block exactly the size we need or
* big enough for an additional free block? */
if (cur_size == mem_req ||
cur_size >= mem_req + sizeof(MemBlock)) {
/* Set size and in use */
s->size = mem_req;
/* Do we need to inject a free block too? */
if (cur_size != mem_req) {
NextBlock(s)->size = (cur_size - mem_req) | S_FREE_MASK;
}
return s->data;
}
}
}
/* Reached sentinel, but no block found yet. Delete some old entry. */
DeleteEntryFromSpriteCache();
}
} }
void *UniquePtrSpriteAllocator::AllocatePtr(size_t size) void *UniquePtrSpriteAllocator::AllocatePtr(size_t size)
{ {
this->data = std::make_unique<std::byte[]>(size); this->data = std::make_unique<std::byte[]>(size);
this->size = size;
return this->data.get(); return this->data.get();
} }
@ -975,83 +874,38 @@ void *GetRawSprite(SpriteID sprite, SpriteType type, SpriteAllocator *allocator,
if (allocator == nullptr && encoder == nullptr) { if (allocator == nullptr && encoder == nullptr) {
/* Load sprite into/from spritecache */ /* Load sprite into/from spritecache */
CacheSpriteAllocator cache_allocator;
/* Update LRU */ /* Update LRU */
sc->lru = ++_sprite_lru_counter; sc->lru = ++_sprite_lru_counter;
/* Load the sprite, if it is not loaded, yet */ /* Load the sprite, if it is not loaded, yet */
if (sc->ptr == nullptr) { if (sc->ptr == nullptr) {
UniquePtrSpriteAllocator cache_allocator;
if (sc->type == SpriteType::Recolour) { if (sc->type == SpriteType::Recolour) {
sc->ptr = ReadRecolourSprite(*sc->file, sc->file_pos, sc->length, cache_allocator); ReadRecolourSprite(*sc->file, sc->file_pos, sc->length, cache_allocator);
} else { } else {
sc->ptr = ReadSprite(sc, sprite, type, cache_allocator, nullptr); ReadSprite(sc, sprite, type, cache_allocator, nullptr);
} }
sc->ptr = std::move(cache_allocator.data);
sc->length = static_cast<uint32_t>(cache_allocator.size);
_spritecache_bytes_used += sc->length;
} }
return sc->ptr; return static_cast<void *>(sc->ptr.get());
} else { } else {
/* Do not use the spritecache, but a different allocator. */ /* Do not use the spritecache, but a different allocator. */
return ReadSprite(sc, sprite, type, *allocator, encoder); return ReadSprite(sc, sprite, type, *allocator, encoder);
} }
} }
static void GfxInitSpriteCache()
{
/* initialize sprite cache heap */
int bpp = BlitterFactory::GetCurrentBlitter()->GetScreenDepth();
uint target_size = (bpp > 0 ? _sprite_cache_size * bpp / 8 : 1) * 1024 * 1024;
/* Remember 'target_size' from the previous allocation attempt, so we do not try to reach the target_size multiple times in case of failure. */
static uint last_alloc_attempt = 0;
if (_spritecache_ptr == nullptr || (_allocated_sprite_cache_size != target_size && target_size != last_alloc_attempt)) {
delete[] reinterpret_cast<uint8_t *>(_spritecache_ptr);
last_alloc_attempt = target_size;
_allocated_sprite_cache_size = target_size;
do {
/* Try to allocate 50% more to make sure we do not allocate almost all available. */
_spritecache_ptr = reinterpret_cast<MemBlock *>(new(std::nothrow) uint8_t[_allocated_sprite_cache_size + _allocated_sprite_cache_size / 2]);
if (_spritecache_ptr != nullptr) {
/* Allocation succeeded, but we wanted less. */
delete[] reinterpret_cast<uint8_t *>(_spritecache_ptr);
_spritecache_ptr = reinterpret_cast<MemBlock *>(new uint8_t[_allocated_sprite_cache_size]);
} else if (_allocated_sprite_cache_size < 2 * 1024 * 1024) {
UserError("Cannot allocate spritecache");
} else {
/* Try again to allocate half. */
_allocated_sprite_cache_size >>= 1;
}
} while (_spritecache_ptr == nullptr);
if (_allocated_sprite_cache_size != target_size) {
Debug(misc, 0, "Not enough memory to allocate {} MiB of spritecache. Spritecache was reduced to {} MiB.", target_size / 1024 / 1024, _allocated_sprite_cache_size / 1024 / 1024);
ErrorMessageData msg(GetEncodedString(STR_CONFIG_ERROR_OUT_OF_MEMORY), GetEncodedString(STR_CONFIG_ERROR_SPRITECACHE_TOO_BIG, target_size, _allocated_sprite_cache_size));
ScheduleErrorMessage(msg);
}
}
/* A big free block */
_spritecache_ptr->size = (_allocated_sprite_cache_size - sizeof(MemBlock)) | S_FREE_MASK;
/* Sentinel block (identified by size == 0) */
NextBlock(_spritecache_ptr)->size = 0;
}
void GfxInitSpriteMem() void GfxInitSpriteMem()
{ {
GfxInitSpriteCache();
/* Reset the spritecache 'pool' */ /* Reset the spritecache 'pool' */
_spritecache.clear(); _spritecache.clear();
_spritecache.shrink_to_fit(); _spritecache.shrink_to_fit();
_compact_cache_counter = 0;
_sprite_files.clear(); _sprite_files.clear();
_spritecache_bytes_used = 0;
} }
/** /**
@ -1062,7 +916,7 @@ void GfxClearSpriteCache()
{ {
/* Clear sprite ptr for all cached items */ /* Clear sprite ptr for all cached items */
for (SpriteCache &sc : _spritecache) { for (SpriteCache &sc : _spritecache) {
if (sc.ptr != nullptr) DeleteEntryFromSpriteCache(&sc); if (sc.ptr != nullptr) sc.ClearSpriteData();
} }
VideoDriver::GetInstance()->ClearSystemSprites(); VideoDriver::GetInstance()->ClearSystemSprites();
@ -1076,7 +930,7 @@ void GfxClearFontSpriteCache()
{ {
/* Clear sprite ptr for all cached font items */ /* Clear sprite ptr for all cached font items */
for (SpriteCache &sc : _spritecache) { for (SpriteCache &sc : _spritecache) {
if (sc.type == SpriteType::Font && sc.ptr != nullptr) DeleteEntryFromSpriteCache(&sc); if (sc.type == SpriteType::Font && sc.ptr != nullptr) sc.ClearSpriteData();
} }
} }

View File

@ -35,6 +35,7 @@ extern uint _sprite_cache_size;
class UniquePtrSpriteAllocator : public SpriteAllocator { class UniquePtrSpriteAllocator : public SpriteAllocator {
public: public:
std::unique_ptr<std::byte[]> data; std::unique_ptr<std::byte[]> data;
size_t size;
protected: protected:
void *AllocatePtr(size_t size) override; void *AllocatePtr(size_t size) override;
}; };

View File

@ -19,21 +19,17 @@
/* These declarations are internal to spritecache but need to be exposed for unit-tests. */ /* These declarations are internal to spritecache but need to be exposed for unit-tests. */
struct SpriteCache { struct SpriteCache {
void *ptr; std::unique_ptr<std::byte[]> ptr;
size_t file_pos; size_t file_pos = 0;
SpriteFile *file; ///< The file the sprite in this entry can be found in. SpriteFile *file = nullptr; ///< The file the sprite in this entry can be found in.
uint32_t length; ///< Length of sprite data. uint32_t length; ///< Length of sprite data.
uint32_t id; uint32_t id = 0;
int16_t lru; uint32_t lru = 0;
SpriteType type; ///< In some cases a single sprite is misused by two NewGRFs. Once as real sprite and once as recolour sprite. If the recolour sprite gets into the cache it might be drawn as real sprite which causes enormous trouble. SpriteType type = SpriteType::Invalid; ///< In some cases a single sprite is misused by two NewGRFs. Once as real sprite and once as recolour sprite. If the recolour sprite gets into the cache it might be drawn as real sprite which causes enormous trouble.
bool warned; ///< True iff the user has been warned about incorrect use of this sprite bool warned = false; ///< True iff the user has been warned about incorrect use of this sprite
uint8_t control_flags; ///< Control flags, see SpriteCacheCtrlFlags uint8_t control_flags = 0; ///< Control flags, see SpriteCacheCtrlFlags
};
/** SpriteAllocator that allocates memory from the sprite cache. */ void ClearSpriteData();
class CacheSpriteAllocator : public SpriteAllocator {
protected:
void *AllocatePtr(size_t size) override;
}; };
inline bool IsMapgenSpriteID(SpriteID sprite) inline bool IsMapgenSpriteID(SpriteID sprite)

View File

@ -19,15 +19,16 @@
static bool MockLoadNextSprite(SpriteID load_index) static bool MockLoadNextSprite(SpriteID load_index)
{ {
static UniquePtrSpriteAllocator allocator; UniquePtrSpriteAllocator allocator;
static Sprite *sprite = allocator.Allocate<Sprite>(sizeof(*sprite)); allocator.Allocate<Sprite>(sizeof(Sprite));
bool is_mapgen = IsMapgenSpriteID(load_index); bool is_mapgen = IsMapgenSpriteID(load_index);
SpriteCache *sc = AllocateSpriteCache(load_index); SpriteCache *sc = AllocateSpriteCache(load_index);
sc->file = nullptr; sc->file = nullptr;
sc->file_pos = 0; sc->file_pos = 0;
sc->ptr = sprite; sc->ptr = std::move(allocator.data);
sc->length = static_cast<uint32_t>(allocator.size);
sc->lru = 0; sc->lru = 0;
sc->id = 0; sc->id = 0;
sc->type = is_mapgen ? SpriteType::MapGen : SpriteType::Normal; sc->type = is_mapgen ? SpriteType::MapGen : SpriteType::Normal;