mirror of
https://github.com/Kelsidavis/WoWee.git
synced 2026-03-22 23:30:14 +00:00
Bound MPQ archive lookup cache; remove always-on composite dumps; track texture cache entries
This commit is contained in:
parent
46c672d1c2
commit
5fda1a3157
10 changed files with 169 additions and 56 deletions
|
|
@ -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_;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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};
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue