diff --git a/include/pipeline/mpq_manager.hpp b/include/pipeline/mpq_manager.hpp index e5d57970..7de542ff 100644 --- a/include/pipeline/mpq_manager.hpp +++ b/include/pipeline/mpq_manager.hpp @@ -112,8 +112,13 @@ private: // Cache for mapping "virtual filename" -> archive handle (or INVALID_HANDLE_VALUE for not found). // This avoids scanning every archive for repeated lookups, which can otherwise appear as a hang // on screens that trigger many asset probes (character select, character preview, etc.). + // + // Important: caching misses can blow up memory if the game probes many unique non-existent filenames. + // Miss caching is disabled by default and must be explicitly enabled. mutable std::mutex fileArchiveCacheMutex_; mutable std::unordered_map fileArchiveCache_; + size_t fileArchiveCacheMaxEntries_ = 500000; + bool fileArchiveCacheMisses_ = false; mutable std::mutex missingFileMutex_; mutable std::unordered_set missingFileWarnings_; diff --git a/include/rendering/character_renderer.hpp b/include/rendering/character_renderer.hpp index 6726b08e..1986c885 100644 --- a/include/rendering/character_renderer.hpp +++ b/include/rendering/character_renderer.hpp @@ -236,7 +236,15 @@ private: bool shadowEnabled = false; // Texture cache - std::unordered_map textureCache; + struct TextureCacheEntry { + GLuint id = 0; + size_t approxBytes = 0; + uint64_t lastUse = 0; + }; + std::unordered_map textureCache; + size_t textureCacheBytes_ = 0; + uint64_t textureCacheCounter_ = 0; + size_t textureCacheBudgetBytes_ = 1024ull * 1024 * 1024; // Default, overridden at init GLuint whiteTexture = 0; std::unordered_map models; diff --git a/include/rendering/m2_renderer.hpp b/include/rendering/m2_renderer.hpp index bece252f..fb887d29 100644 --- a/include/rendering/m2_renderer.hpp +++ b/include/rendering/m2_renderer.hpp @@ -356,7 +356,15 @@ private: uint32_t lastDrawCallCount = 0; GLuint loadTexture(const std::string& path); - std::unordered_map textureCache; + struct TextureCacheEntry { + GLuint id = 0; + size_t approxBytes = 0; + uint64_t lastUse = 0; + }; + std::unordered_map textureCache; + size_t textureCacheBytes_ = 0; + uint64_t textureCacheCounter_ = 0; + size_t textureCacheBudgetBytes_ = 2048ull * 1024 * 1024; // Default, overridden at init GLuint whiteTexture = 0; GLuint glowTexture = 0; // Soft radial gradient for glow sprites diff --git a/include/rendering/terrain_renderer.hpp b/include/rendering/terrain_renderer.hpp index 4a76ba11..49335bd5 100644 --- a/include/rendering/terrain_renderer.hpp +++ b/include/rendering/terrain_renderer.hpp @@ -186,7 +186,15 @@ private: std::vector chunks; // Texture cache (path -> GL texture ID) - std::unordered_map textureCache; + struct TextureCacheEntry { + GLuint id = 0; + size_t approxBytes = 0; + uint64_t lastUse = 0; + }; + std::unordered_map textureCache; + size_t textureCacheBytes_ = 0; + uint64_t textureCacheCounter_ = 0; + size_t textureCacheBudgetBytes_ = 4096ull * 1024 * 1024; // Default, overridden at init // Lighting parameters float lightDir[3] = {-0.5f, -1.0f, -0.5f}; diff --git a/include/rendering/wmo_renderer.hpp b/include/rendering/wmo_renderer.hpp index 71ecb8b9..3d472922 100644 --- a/include/rendering/wmo_renderer.hpp +++ b/include/rendering/wmo_renderer.hpp @@ -552,7 +552,15 @@ private: std::string mapName_; // Texture cache (path -> texture ID) - std::unordered_map textureCache; + struct TextureCacheEntry { + GLuint id = 0; + size_t approxBytes = 0; + uint64_t lastUse = 0; + }; + std::unordered_map textureCache; + size_t textureCacheBytes_ = 0; + uint64_t textureCacheCounter_ = 0; + size_t textureCacheBudgetBytes_ = 2048ull * 1024 * 1024; // Default, overridden at init // Default white texture GLuint whiteTexture = 0; diff --git a/src/pipeline/mpq_manager.cpp b/src/pipeline/mpq_manager.cpp index 79db7621..cef57405 100644 --- a/src/pipeline/mpq_manager.cpp +++ b/src/pipeline/mpq_manager.cpp @@ -3,6 +3,7 @@ #include #include #include +#include #include #include #include @@ -49,6 +50,16 @@ bool envFlagEnabled(const char* name) { std::string s = toLowerCopy(v); return s == "1" || s == "true" || s == "yes" || s == "on"; } + +size_t envSizeTOrDefault(const char* name, size_t defValue) { + const char* v = std::getenv(name); + if (!v || !*v) return defValue; + char* end = nullptr; + unsigned long long value = std::strtoull(v, &end, 10); + if (end == v || value == 0) return defValue; + if (value > static_cast(std::numeric_limits::max())) return defValue; + return static_cast(value); +} } MPQManager::MPQManager() = default; @@ -66,6 +77,12 @@ bool MPQManager::initialize(const std::string& dataPath_) { dataPath = dataPath_; LOG_INFO("Initializing MPQ manager with data path: ", dataPath); + // Guard against cache blowups from huge numbers of unique probes. + fileArchiveCacheMaxEntries_ = envSizeTOrDefault("WOWEE_MPQ_ARCHIVE_CACHE_MAX", fileArchiveCacheMaxEntries_); + fileArchiveCacheMisses_ = envFlagEnabled("WOWEE_MPQ_CACHE_MISSES"); + LOG_INFO("MPQ archive lookup cache: maxEntries=", fileArchiveCacheMaxEntries_, + " cacheMisses=", (fileArchiveCacheMisses_ ? "yes" : "no")); + // Check if data directory exists if (!std::filesystem::exists(dataPath)) { LOG_ERROR("Data directory does not exist: ", dataPath); @@ -363,8 +380,23 @@ HANDLE MPQManager::findFileArchive(const std::string& filename) const { const auto end = std::chrono::steady_clock::now(); const auto ms = std::chrono::duration_cast(end - start).count(); + // Avoid caching misses unless explicitly enabled; miss caching can explode memory when + // code probes many unique non-existent paths (common with HD patch sets). + if (found == INVALID_HANDLE_VALUE && !fileArchiveCacheMisses_) { + if (ms >= 100) { + LOG_WARNING("Slow MPQ lookup: '", filename, "' scanned ", archives.size(), " archives in ", ms, " ms"); + } + return found; + } + { std::lock_guard lock(fileArchiveCacheMutex_); + if (fileArchiveCache_.size() >= fileArchiveCacheMaxEntries_) { + // Simple safety valve: clear the cache rather than allowing an unbounded growth. + LOG_WARNING("MPQ archive lookup cache cleared (size=", fileArchiveCache_.size(), + " reached maxEntries=", fileArchiveCacheMaxEntries_, ")"); + fileArchiveCache_.clear(); + } // Another thread may have raced to populate; if so, prefer the existing value. auto [it, inserted] = fileArchiveCache_.emplace(std::move(cacheKey), found); if (!inserted) { diff --git a/src/rendering/character_renderer.cpp b/src/rendering/character_renderer.cpp index f6f51f08..119bf1d0 100644 --- a/src/rendering/character_renderer.cpp +++ b/src/rendering/character_renderer.cpp @@ -32,10 +32,30 @@ #include #include #include +#include +#include namespace wowee { namespace rendering { +namespace { +size_t envSizeMBOrDefault(const char* name, size_t defMb) { + const char* v = std::getenv(name); + if (!v || !*v) return defMb; + char* end = nullptr; + unsigned long long mb = std::strtoull(v, &end, 10); + if (end == v || mb == 0) return defMb; + if (mb > (std::numeric_limits::max() / (1024ull * 1024ull))) return defMb; + return static_cast(mb); +} + +size_t approxTextureBytesWithMips(int w, int h) { + if (w <= 0 || h <= 0) return 0; + size_t base = static_cast(w) * static_cast(h) * 4ull; + return base + (base / 3); // ~4/3 for mip chain +} +} // namespace + CharacterRenderer::CharacterRenderer() { } @@ -261,6 +281,9 @@ bool CharacterRenderer::initialize() { glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST); glBindTexture(GL_TEXTURE_2D, 0); + // Diagnostics-only: cache lifetime is currently tied to renderer lifetime. + textureCacheBudgetBytes_ = envSizeMBOrDefault("WOWEE_CHARACTER_TEX_CACHE_MB", 2048) * 1024ull * 1024ull; + core::Logger::getInstance().info("Character renderer initialized"); return true; } @@ -283,11 +306,14 @@ void CharacterRenderer::shutdown() { // Clean up texture cache for (auto& pair : textureCache) { - if (pair.second && pair.second != whiteTexture) { - glDeleteTextures(1, &pair.second); + GLuint texId = pair.second.id; + if (texId && texId != whiteTexture) { + glDeleteTextures(1, &texId); } } textureCache.clear(); + textureCacheBytes_ = 0; + textureCacheCounter_ = 0; if (whiteTexture) { glDeleteTextures(1, &whiteTexture); @@ -322,7 +348,10 @@ GLuint CharacterRenderer::loadTexture(const std::string& path) { // Check cache auto it = textureCache.find(key); - if (it != textureCache.end()) return it->second; + if (it != textureCache.end()) { + it->second.lastUse = ++textureCacheCounter_; + return it->second.id; + } if (!assetManager || !assetManager->isInitialized()) { return whiteTexture; @@ -349,7 +378,18 @@ GLuint CharacterRenderer::loadTexture(const std::string& path) { applyAnisotropicFiltering(); glBindTexture(GL_TEXTURE_2D, 0); - textureCache[key] = texId; + TextureCacheEntry e; + e.id = texId; + e.approxBytes = approxTextureBytesWithMips(blpImage.width, blpImage.height); + e.lastUse = ++textureCacheCounter_; + textureCacheBytes_ += e.approxBytes; + textureCache[key] = e; + if (textureCacheBytes_ > textureCacheBudgetBytes_) { + core::Logger::getInstance().warning( + "Character texture cache over budget: ", + textureCacheBytes_ / (1024 * 1024), " MB > ", + textureCacheBudgetBytes_ / (1024 * 1024), " MB (textures=", textureCache.size(), ")"); + } core::Logger::getInstance().info("Loaded character texture: ", path, " (", blpImage.width, "x", blpImage.height, ")"); return texId; } @@ -456,29 +496,6 @@ GLuint CharacterRenderer::compositeTextures(const std::vector& laye core::Logger::getInstance().info("Composite: overlay ", layerPaths[layer], " (", overlay.width, "x", overlay.height, ")"); - // Debug: save overlay to disk - { - std::string fname = (std::filesystem::temp_directory_path() / ("overlay_debug_" + std::to_string(layer) + ".rgba")).string(); - FILE* f = fopen(fname.c_str(), "wb"); - if (f) { - fwrite(&overlay.width, 4, 1, f); - fwrite(&overlay.height, 4, 1, f); - fwrite(overlay.data.data(), 1, overlay.data.size(), f); - fclose(f); - } - // Check alpha values - int opaquePixels = 0, transPixels = 0, semiPixels = 0; - size_t pxCount = static_cast(overlay.width) * overlay.height; - for (size_t p = 0; p < pxCount; p++) { - uint8_t a = overlay.data[p * 4 + 3]; - if (a == 255) opaquePixels++; - else if (a == 0) transPixels++; - else semiPixels++; - } - core::Logger::getInstance().info(" Overlay alpha stats: opaque=", opaquePixels, - " transparent=", transPixels, " semi=", semiPixels); - } - if (overlay.width == width && overlay.height == height) { // Same size: full alpha-blend blitOverlay(composite, width, height, overlay, 0, 0); @@ -533,19 +550,7 @@ GLuint CharacterRenderer::compositeTextures(const std::vector& laye } } - // Debug: save composite as raw RGBA file - { - std::string dbgPath = (std::filesystem::temp_directory_path() / "composite_debug.rgba").string(); - FILE* f = fopen(dbgPath.c_str(), "wb"); - if (f) { - // Write width, height as 4 bytes each, then pixel data - fwrite(&width, 4, 1, f); - fwrite(&height, 4, 1, f); - fwrite(composite.data(), 1, composite.size(), f); - fclose(f); - core::Logger::getInstance().info("DEBUG: saved composite to ", dbgPath); - } - } + // Debug dump removed: it was always-on and could stall badly under load. // Upload composite to GPU GLuint texId; @@ -733,7 +738,7 @@ void CharacterRenderer::setModelTexture(uint32_t modelId, uint32_t textureSlot, if (oldTex && oldTex != whiteTexture) { bool cached = false; for (const auto& [k, v] : textureCache) { - if (v == oldTex) { cached = true; break; } + if (v.id == oldTex) { cached = true; break; } } if (!cached) { glDeleteTextures(1, &oldTex); diff --git a/src/rendering/m2_renderer.cpp b/src/rendering/m2_renderer.cpp index 83bba1f2..4dfcb2d3 100644 --- a/src/rendering/m2_renderer.cpp +++ b/src/rendering/m2_renderer.cpp @@ -650,12 +650,15 @@ void M2Renderer::shutdown() { instanceIndexById.clear(); // Delete cached textures - for (auto& [path, texId] : textureCache) { + for (auto& [path, entry] : textureCache) { + GLuint texId = entry.id; if (texId != 0 && texId != whiteTexture) { glDeleteTextures(1, &texId); } } textureCache.clear(); + textureCacheBytes_ = 0; + textureCacheCounter_ = 0; if (whiteTexture != 0) { glDeleteTextures(1, &whiteTexture); whiteTexture = 0; @@ -2685,7 +2688,8 @@ GLuint M2Renderer::loadTexture(const std::string& path) { // Check cache auto it = textureCache.find(key); if (it != textureCache.end()) { - return it->second; + it->second.lastUse = ++textureCacheCounter_; + return it->second.id; } // Load BLP texture @@ -2714,7 +2718,13 @@ GLuint M2Renderer::loadTexture(const std::string& path) { glBindTexture(GL_TEXTURE_2D, 0); - textureCache[key] = textureID; + TextureCacheEntry e; + e.id = textureID; + size_t base = static_cast(blp.width) * static_cast(blp.height) * 4ull; + e.approxBytes = base + (base / 3); + e.lastUse = ++textureCacheCounter_; + textureCacheBytes_ += e.approxBytes; + textureCache[key] = e; LOG_DEBUG("M2: Loaded texture: ", path, " (", blp.width, "x", blp.height, ")"); return textureID; diff --git a/src/rendering/terrain_renderer.cpp b/src/rendering/terrain_renderer.cpp index 9718d4ed..bed8e138 100644 --- a/src/rendering/terrain_renderer.cpp +++ b/src/rendering/terrain_renderer.cpp @@ -8,6 +8,7 @@ #include #include #include +#include #include namespace wowee { @@ -77,10 +78,15 @@ void TerrainRenderer::shutdown() { } // Delete cached textures - for (auto& pair : textureCache) { - glDeleteTextures(1, &pair.second); + for (auto& [path, entry] : textureCache) { + GLuint texId = entry.id; + if (texId != 0 && texId != whiteTexture) { + glDeleteTextures(1, &texId); + } } textureCache.clear(); + textureCacheBytes_ = 0; + textureCacheCounter_ = 0; shader.reset(); } @@ -234,7 +240,8 @@ GLuint TerrainRenderer::loadTexture(const std::string& path) { // Check cache first auto it = textureCache.find(key); if (it != textureCache.end()) { - return it->second; + it->second.lastUse = ++textureCacheCounter_; + return it->second.id; } // Load BLP texture @@ -269,7 +276,13 @@ GLuint TerrainRenderer::loadTexture(const std::string& path) { glBindTexture(GL_TEXTURE_2D, 0); // Cache texture - textureCache[key] = textureID; + TextureCacheEntry e; + e.id = textureID; + size_t base = static_cast(blp.width) * static_cast(blp.height) * 4ull; + e.approxBytes = base + (base / 3); + e.lastUse = ++textureCacheCounter_; + textureCacheBytes_ += e.approxBytes; + textureCache[key] = e; LOG_DEBUG("Loaded texture: ", path, " (", blp.width, "x", blp.height, ")"); @@ -307,7 +320,13 @@ void TerrainRenderer::uploadPreloadedTextures(const std::unordered_map(blp.width) * static_cast(blp.height) * 4ull; + e.approxBytes = base + (base / 3); + e.lastUse = ++textureCacheCounter_; + textureCacheBytes_ += e.approxBytes; + textureCache[key] = e; } } diff --git a/src/rendering/wmo_renderer.cpp b/src/rendering/wmo_renderer.cpp index 6ebb471e..8694db33 100644 --- a/src/rendering/wmo_renderer.cpp +++ b/src/rendering/wmo_renderer.cpp @@ -226,12 +226,15 @@ void WMORenderer::shutdown() { } // Free cached textures - for (auto& [path, texId] : textureCache) { + for (auto& [path, entry] : textureCache) { + GLuint texId = entry.id; if (texId != 0 && texId != whiteTexture) { glDeleteTextures(1, &texId); } } textureCache.clear(); + textureCacheBytes_ = 0; + textureCacheCounter_ = 0; // Free white texture if (whiteTexture != 0) { @@ -1626,7 +1629,8 @@ GLuint WMORenderer::loadTexture(const std::string& path) { // Check cache first auto it = textureCache.find(key); if (it != textureCache.end()) { - return it->second; + it->second.lastUse = ++textureCacheCounter_; + return it->second.id; } // Load BLP texture @@ -1662,7 +1666,13 @@ GLuint WMORenderer::loadTexture(const std::string& path) { glBindTexture(GL_TEXTURE_2D, 0); // Cache it - textureCache[key] = textureID; + TextureCacheEntry e; + e.id = textureID; + size_t base = static_cast(blp.width) * static_cast(blp.height) * 4ull; + e.approxBytes = base + (base / 3); + e.lastUse = ++textureCacheCounter_; + textureCacheBytes_ += e.approxBytes; + textureCache[key] = e; core::Logger::getInstance().debug("WMO: Loaded texture: ", path, " (", blp.width, "x", blp.height, ")"); return textureID;