From a4e99366fb7bd6eb31e8962a223da211f42c70f6 Mon Sep 17 00:00:00 2001 From: Stenzek Date: Wed, 19 Apr 2023 21:26:38 +1000 Subject: [PATCH] GS/HW: Put a cap on the hash cache count As well as VRAM usage. Stops Corvette allocating 16,000+ textures. Also reduce max age for hash cache sources, since they get kept around in the latter for another 30 frames. --- pcsx2/GS/Renderers/HW/GSTextureCache.cpp | 90 ++++++++++++++++++------ pcsx2/GS/Renderers/HW/GSTextureCache.h | 6 +- 2 files changed, 75 insertions(+), 21 deletions(-) diff --git a/pcsx2/GS/Renderers/HW/GSTextureCache.cpp b/pcsx2/GS/Renderers/HW/GSTextureCache.cpp index e9050e076b..f3c81fc084 100644 --- a/pcsx2/GS/Renderers/HW/GSTextureCache.cpp +++ b/pcsx2/GS/Renderers/HW/GSTextureCache.cpp @@ -35,6 +35,9 @@ std::unique_ptr g_texture_cache; static u8* s_unswizzle_buffer; +/// List of candidates for purging when the hash cache gets too large. +static std::vector> s_hash_cache_purge_list; + GSTextureCache::GSTextureCache() { // In theory 4MB is enough but 9MB is safer for overflow (8MB @@ -50,6 +53,7 @@ GSTextureCache::~GSTextureCache() { RemoveAll(); + s_hash_cache_purge_list = {}; _aligned_free(s_unswizzle_buffer); } @@ -2839,29 +2843,11 @@ void GSTextureCache::IncAge() Source* s = *i; ++i; - if (++s->m_age > (s->CanPreload() ? max_preload_age : max_age)) + if (++s->m_age > ((!s->m_from_hash_cache && s->CanPreload()) ? max_preload_age : max_age)) m_src.RemoveAt(s); } - const u32 max_hash_cache_age = 30; - for (auto it = m_hash_cache.begin(); it != m_hash_cache.end();) - { - HashCacheEntry& e = it->second; - if (e.refcount == 0 && ++e.age > max_hash_cache_age) - { - const u32 mem_usage = e.texture->GetMemUsage(); - if (e.is_replacement) - m_hash_cache_replacement_memory_usage -= mem_usage; - else - m_hash_cache_memory_usage -= mem_usage; - g_gs_device->Recycle(e.texture); - m_hash_cache.erase(it++); - } - else - { - ++it; - } - } + AgeHashCache(); // Clearing of Rendertargets causes flickering in many scene transitions. // Sigh, this seems to be used to invalidate surfaces. So set a huge maxage to avoid flicker, @@ -3707,6 +3693,70 @@ GSTextureCache::HashCacheEntry* GSTextureCache::LookupHashCache(const GIFRegTEX0 return &m_hash_cache.emplace(key, entry).first->second; } +void GSTextureCache::RemoveFromHashCache(HashCacheMap::iterator it) +{ + HashCacheEntry& e = it->second; + const u32 mem_usage = e.texture->GetMemUsage(); + if (e.is_replacement) + m_hash_cache_replacement_memory_usage -= mem_usage; + else + m_hash_cache_memory_usage -= mem_usage; + g_gs_device->Recycle(e.texture); + m_hash_cache.erase(it); +} + +void GSTextureCache::AgeHashCache() +{ + // Where did this number come from? + // A game called Corvette draws its background FMVs with a ton of 17x17 tiles, which ends up + // being about 600 texture uploads per frame. We'll use 800 as an upper bound for a bit of + // a buffer, hopefully nothing's going to end up with more textures than that. + constexpr u32 MAX_HASH_CACHE_SIZE = 800; + constexpr u32 MAX_HASH_CACHE_AGE = 30; + + bool might_need_cache_purge = (m_hash_cache.size() > MAX_HASH_CACHE_SIZE); + if (might_need_cache_purge) + s_hash_cache_purge_list.clear(); + + for (auto it = m_hash_cache.begin(); it != m_hash_cache.end();) + { + HashCacheEntry& e = it->second; + if (e.refcount > 0) + { + ++it; + continue; + } + + if (++e.age > MAX_HASH_CACHE_AGE) + { + RemoveFromHashCache(it++); + continue; + } + + // We might free up enough just with "normal" removals above. + if (might_need_cache_purge) + { + might_need_cache_purge = (m_hash_cache.size() > MAX_HASH_CACHE_SIZE); + if (might_need_cache_purge) + s_hash_cache_purge_list.emplace_back(it, static_cast(e.age)); + } + + ++it; + } + + // Pushing to a list, sorting, and removing ends up faster than re-iterating the map. + if (might_need_cache_purge) + { + std::sort(s_hash_cache_purge_list.begin(), s_hash_cache_purge_list.end(), + [](const auto& lhs, const auto& rhs) { return lhs.second - rhs.second; }); + + const u32 entries_to_purge = std::min(static_cast(m_hash_cache.size() - MAX_HASH_CACHE_SIZE), + static_cast(s_hash_cache_purge_list.size())); + for (u32 i = 0; i < entries_to_purge; i++) + RemoveFromHashCache(s_hash_cache_purge_list[i].first); + } +} + GSTextureCache::Target* GSTextureCache::CreateTarget(const GIFRegTEX0& TEX0, int w, int h, float scale, int type, const bool clear) { ASSERT(type == RenderTarget || type == DepthStencil); diff --git a/pcsx2/GS/Renderers/HW/GSTextureCache.h b/pcsx2/GS/Renderers/HW/GSTextureCache.h index 7d7df755f3..922da7cc48 100644 --- a/pcsx2/GS/Renderers/HW/GSTextureCache.h +++ b/pcsx2/GS/Renderers/HW/GSTextureCache.h @@ -123,6 +123,8 @@ public: bool is_replacement; }; + using HashCacheMap = std::unordered_map; + class Surface : public GSAlignedClass<32> { protected: @@ -374,7 +376,7 @@ protected: PaletteMap m_palette_map; SourceMap m_src; u64 m_source_memory_usage = 0; - std::unordered_map m_hash_cache; + HashCacheMap m_hash_cache; u64 m_hash_cache_memory_usage = 0; u64 m_hash_cache_replacement_memory_usage = 0; @@ -402,6 +404,8 @@ protected: bool PrepareDownloadTexture(u32 width, u32 height, GSTexture::Format format, std::unique_ptr* tex); HashCacheEntry* LookupHashCache(const GIFRegTEX0& TEX0, const GIFRegTEXA& TEXA, bool& paltex, const u32* clut, const GSVector2i* lod, SourceRegion region); + void RemoveFromHashCache(HashCacheMap::iterator it); + void AgeHashCache(); static void PreloadTexture(const GIFRegTEX0& TEX0, const GIFRegTEXA& TEXA, SourceRegion region, GSLocalMemory& mem, bool paltex, GSTexture* tex, u32 level); static HashType HashTexture(const GIFRegTEX0& TEX0, const GIFRegTEXA& TEXA, SourceRegion region);