Bound MPQ archive lookup cache; remove always-on composite dumps; track texture cache entries

This commit is contained in:
Kelsi 2026-02-12 16:29:36 -08:00
parent 46c672d1c2
commit 5fda1a3157
10 changed files with 169 additions and 56 deletions

View file

@ -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<std::string, HANDLE> fileArchiveCache_;
size_t fileArchiveCacheMaxEntries_ = 500000;
bool fileArchiveCacheMisses_ = false;
mutable std::mutex missingFileMutex_;
mutable std::unordered_set<std::string> missingFileWarnings_;

View file

@ -236,7 +236,15 @@ private:
bool shadowEnabled = false;
// Texture cache
std::unordered_map<std::string, GLuint> textureCache;
struct TextureCacheEntry {
GLuint id = 0;
size_t approxBytes = 0;
uint64_t lastUse = 0;
};
std::unordered_map<std::string, TextureCacheEntry> 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<uint32_t, M2ModelGPU> models;

View file

@ -356,7 +356,15 @@ private:
uint32_t lastDrawCallCount = 0;
GLuint loadTexture(const std::string& path);
std::unordered_map<std::string, GLuint> textureCache;
struct TextureCacheEntry {
GLuint id = 0;
size_t approxBytes = 0;
uint64_t lastUse = 0;
};
std::unordered_map<std::string, TextureCacheEntry> 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

View file

@ -186,7 +186,15 @@ private:
std::vector<TerrainChunkGPU> chunks;
// Texture cache (path -> GL texture ID)
std::unordered_map<std::string, GLuint> textureCache;
struct TextureCacheEntry {
GLuint id = 0;
size_t approxBytes = 0;
uint64_t lastUse = 0;
};
std::unordered_map<std::string, TextureCacheEntry> 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};

View file

@ -552,7 +552,15 @@ private:
std::string mapName_;
// Texture cache (path -> texture ID)
std::unordered_map<std::string, GLuint> textureCache;
struct TextureCacheEntry {
GLuint id = 0;
size_t approxBytes = 0;
uint64_t lastUse = 0;
};
std::unordered_map<std::string, TextureCacheEntry> 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;

View file

@ -3,6 +3,7 @@
#include <algorithm>
#include <chrono>
#include <cstdlib>
#include <limits>
#include <filesystem>
#include <fstream>
#include <sstream>
@ -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<unsigned long long>(std::numeric_limits<size_t>::max())) return defValue;
return static_cast<size_t>(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<std::chrono::milliseconds>(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<std::mutex> 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) {

View file

@ -32,10 +32,30 @@
#include <functional>
#include <unordered_map>
#include <unordered_set>
#include <cstdlib>
#include <limits>
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<size_t>::max() / (1024ull * 1024ull))) return defMb;
return static_cast<size_t>(mb);
}
size_t approxTextureBytesWithMips(int w, int h) {
if (w <= 0 || h <= 0) return 0;
size_t base = static_cast<size_t>(w) * static_cast<size_t>(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<std::string>& 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<size_t>(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<std::string>& 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);

View file

@ -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<size_t>(blp.width) * static_cast<size_t>(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;

View file

@ -8,6 +8,7 @@
#include <glm/gtc/matrix_transform.hpp>
#include <glm/gtc/type_ptr.hpp>
#include <algorithm>
#include <cstdlib>
#include <limits>
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<size_t>(blp.width) * static_cast<size_t>(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<std::stri
applyAnisotropicFiltering();
glBindTexture(GL_TEXTURE_2D, 0);
textureCache[key] = textureID;
TextureCacheEntry e;
e.id = textureID;
size_t base = static_cast<size_t>(blp.width) * static_cast<size_t>(blp.height) * 4ull;
e.approxBytes = base + (base / 3);
e.lastUse = ++textureCacheCounter_;
textureCacheBytes_ += e.approxBytes;
textureCache[key] = e;
}
}

View file

@ -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<size_t>(blp.width) * static_cast<size_t>(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;