Stabilize streaming memory and parser handling; revert socket recv optimizations

This commit is contained in:
Kelsi 2026-02-22 07:26:54 -08:00
parent c914295d20
commit ae88b226b5
15 changed files with 591 additions and 161 deletions

View file

@ -250,7 +250,7 @@ bool CharacterRenderer::initialize(VkContext* ctx, VkDescriptorSetLayout perFram
}
// Diagnostics-only: cache lifetime is currently tied to renderer lifetime.
textureCacheBudgetBytes_ = envSizeMBOrDefault("WOWEE_CHARACTER_TEX_CACHE_MB", 2048) * 1024ull * 1024ull;
textureCacheBudgetBytes_ = envSizeMBOrDefault("WOWEE_CHARACTER_TEX_CACHE_MB", 512) * 1024ull * 1024ull;
core::Logger::getInstance().info("Character renderer initialized (Vulkan)");
return true;

View file

@ -40,6 +40,15 @@ bool envFlagEnabled(const char* key, bool defaultValue) {
return !(v == "0" || v == "false" || v == "off" || v == "no");
}
size_t envSizeMBOrDefault(const char* name, size_t defMb) {
const char* raw = std::getenv(name);
if (!raw || !*raw) return defMb;
char* end = nullptr;
unsigned long long mb = std::strtoull(raw, &end, 10);
if (end == raw || mb == 0) return defMb;
return static_cast<size_t>(mb);
}
static constexpr uint32_t kParticleFlagRandomized = 0x40;
static constexpr uint32_t kParticleFlagTiled = 0x80;
@ -601,6 +610,11 @@ bool M2Renderer::initialize(VkContext* ctx, VkDescriptorSetLayout perFrameLayout
glowTexture_->upload(*vkCtx_, px.data(), SZ, SZ, VK_FORMAT_R8G8B8A8_UNORM);
glowTexture_->createSampler(device, VK_FILTER_LINEAR, VK_FILTER_LINEAR, VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_EDGE);
}
textureCacheBudgetBytes_ =
envSizeMBOrDefault("WOWEE_M2_TEX_CACHE_MB", 512) * 1024ull * 1024ull;
modelCacheLimit_ = envSizeMBOrDefault("WOWEE_M2_MODEL_LIMIT", 6000);
LOG_INFO("M2 texture cache budget: ", textureCacheBudgetBytes_ / (1024 * 1024), " MB");
LOG_INFO("M2 model cache limit: ", modelCacheLimit_);
LOG_INFO("M2 renderer initialized (Vulkan)");
initialized_ = true;
@ -635,6 +649,9 @@ void M2Renderer::shutdown() {
textureCacheCounter_ = 0;
textureHasAlphaByPtr_.clear();
textureColorKeyBlackByPtr_.clear();
failedTextureCache_.clear();
loggedTextureLoadFails_.clear();
textureBudgetRejectWarnings_ = 0;
whiteTexture_.reset();
glowTexture_.reset();
@ -827,6 +844,14 @@ bool M2Renderer::loadModel(const pipeline::M2Model& model, uint32_t modelId) {
// Already loaded
return true;
}
if (models.size() >= modelCacheLimit_) {
if (modelLimitRejectWarnings_ < 8 || (modelLimitRejectWarnings_ % 120) == 0) {
LOG_WARNING("M2 model cache full (", models.size(), "/", modelCacheLimit_,
"), skipping model load: id=", modelId, " name=", model.name);
}
++modelLimitRejectWarnings_;
return false;
}
bool hasGeometry = !model.vertices.empty() && !model.indices.empty();
bool hasParticles = !model.particleEmitters.empty();
@ -1134,10 +1159,15 @@ bool M2Renderer::loadModel(const pipeline::M2Model& model, uint32_t modelId) {
VkTexture* texPtr = loadTexture(texPath, tex.flags);
bool failed = (texPtr == whiteTexture_.get());
if (failed) {
static std::unordered_set<std::string> loggedModelTextureFails;
std::string failKey = model.name + "|" + texKey;
if (loggedModelTextureFails.insert(failKey).second) {
static uint32_t loggedModelTextureFails = 0;
static bool loggedModelTextureFailSuppressed = false;
if (loggedModelTextureFails < 250) {
LOG_WARNING("M2 model ", model.name, " texture[", ti, "] failed to load: ", texPath);
++loggedModelTextureFails;
} else if (!loggedModelTextureFailSuppressed) {
LOG_WARNING("M2 model texture-failure warnings suppressed after ",
loggedModelTextureFails, " entries");
loggedModelTextureFailSuppressed = true;
}
}
if (isInvisibleTrap) {
@ -3155,6 +3185,9 @@ VkTexture* M2Renderer::loadTexture(const std::string& path, uint32_t texFlags) {
it->second.lastUse = ++textureCacheCounter_;
return it->second.texture.get();
}
if (failedTextureCache_.count(key)) {
return whiteTexture_.get();
}
auto containsToken = [](const std::string& haystack, const char* token) {
return haystack.find(token) != std::string::npos;
@ -3175,13 +3208,28 @@ VkTexture* M2Renderer::loadTexture(const std::string& path, uint32_t texFlags) {
// Load BLP texture
pipeline::BLPImage blp = assetManager->loadTexture(key);
if (!blp.isValid()) {
static std::unordered_set<std::string> loggedTextureLoadFails;
if (loggedTextureLoadFails.insert(key).second) {
static constexpr size_t kMaxFailedTextureCache = 200000;
if (failedTextureCache_.size() < kMaxFailedTextureCache) {
failedTextureCache_.insert(key);
}
if (loggedTextureLoadFails_.insert(key).second) {
LOG_WARNING("M2: Failed to load texture: ", path);
}
return whiteTexture_.get();
}
size_t base = static_cast<size_t>(blp.width) * static_cast<size_t>(blp.height) * 4ull;
size_t approxBytes = base + (base / 3);
if (textureCacheBytes_ + approxBytes > textureCacheBudgetBytes_) {
if (textureBudgetRejectWarnings_ < 8 || (textureBudgetRejectWarnings_ % 120) == 0) {
LOG_WARNING("M2 texture cache full (", textureCacheBytes_ / (1024 * 1024),
" MB / ", textureCacheBudgetBytes_ / (1024 * 1024),
" MB), rejecting texture: ", path);
}
++textureBudgetRejectWarnings_;
return whiteTexture_.get();
}
// Track whether the texture actually uses alpha (any pixel with alpha < 255).
bool hasAlpha = false;
for (size_t i = 3; i < blp.data.size(); i += 4) {
@ -3204,8 +3252,7 @@ VkTexture* M2Renderer::loadTexture(const std::string& path, uint32_t texFlags) {
TextureCacheEntry e;
e.texture = std::move(tex);
size_t base = static_cast<size_t>(blp.width) * static_cast<size_t>(blp.height) * 4ull;
e.approxBytes = base + (base / 3);
e.approxBytes = approxBytes;
e.hasAlpha = hasAlpha;
e.colorKeyBlack = colorKeyBlackHint;
e.lastUse = ++textureCacheCounter_;

View file

@ -2756,6 +2756,19 @@ void Renderer::update(float deltaTime) {
performanceHUD->update(deltaTime);
}
// Periodic cache hygiene: drop model GPU data no longer referenced by active instances.
static float modelCleanupTimer = 0.0f;
modelCleanupTimer += deltaTime;
if (modelCleanupTimer >= 5.0f) {
if (wmoRenderer) {
wmoRenderer->cleanupUnusedModels();
}
if (m2Renderer) {
m2Renderer->cleanupUnusedModels();
}
modelCleanupTimer = 0.0f;
}
auto updateEnd = std::chrono::steady_clock::now();
lastUpdateMs = std::chrono::duration<double, std::milli>(updateEnd - updateStart).count();

View file

@ -20,6 +20,17 @@
namespace wowee {
namespace rendering {
namespace {
size_t envSizeMBOrDefault(const char* name, size_t defMb) {
const char* raw = std::getenv(name);
if (!raw || !*raw) return defMb;
char* end = nullptr;
unsigned long long mb = std::strtoull(raw, &end, 10);
if (end == raw || mb == 0) return defMb;
return static_cast<size_t>(mb);
}
} // namespace
// Matches set 1 binding 7 in terrain.frag.glsl
struct TerrainParamsUBO {
int32_t layerCount;
@ -185,6 +196,9 @@ bool TerrainRenderer::initialize(VkContext* ctx, VkDescriptorSetLayout perFrameL
opaqueAlphaTexture->upload(*vkCtx, &opaqueAlpha, 1, 1, VK_FORMAT_R8_UNORM, false);
opaqueAlphaTexture->createSampler(device, VK_FILTER_LINEAR, VK_FILTER_LINEAR,
VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_EDGE);
textureCacheBudgetBytes_ =
envSizeMBOrDefault("WOWEE_TERRAIN_TEX_CACHE_MB", 512) * 1024ull * 1024ull;
LOG_INFO("Terrain texture cache budget: ", textureCacheBudgetBytes_ / (1024 * 1024), " MB");
LOG_INFO("Terrain renderer initialized (Vulkan)");
return true;
@ -287,6 +301,9 @@ void TerrainRenderer::shutdown() {
textureCache.clear();
textureCacheBytes_ = 0;
textureCacheCounter_ = 0;
failedTextureCache_.clear();
loggedTextureLoadFails_.clear();
textureBudgetRejectWarnings_ = 0;
if (whiteTexture) { whiteTexture->destroy(device, allocator); whiteTexture.reset(); }
if (opaqueAlphaTexture) { opaqueAlphaTexture->destroy(device, allocator); opaqueAlphaTexture.reset(); }
@ -425,10 +442,31 @@ VkTexture* TerrainRenderer::loadTexture(const std::string& path) {
it->second.lastUse = ++textureCacheCounter_;
return it->second.texture.get();
}
if (failedTextureCache_.count(key)) {
return whiteTexture.get();
}
pipeline::BLPImage blp = assetManager->loadTexture(key);
if (!blp.isValid()) {
LOG_WARNING("Failed to load texture: ", path);
static constexpr size_t kMaxFailedTextureCache = 200000;
if (failedTextureCache_.size() < kMaxFailedTextureCache) {
failedTextureCache_.insert(key);
}
if (loggedTextureLoadFails_.insert(key).second) {
LOG_WARNING("Failed to load texture: ", path);
}
return whiteTexture.get();
}
size_t base = static_cast<size_t>(blp.width) * static_cast<size_t>(blp.height) * 4ull;
size_t approxBytes = base + (base / 3);
if (textureCacheBytes_ + approxBytes > textureCacheBudgetBytes_) {
if (textureBudgetRejectWarnings_ < 8 || (textureBudgetRejectWarnings_ % 120) == 0) {
LOG_WARNING("Terrain texture cache full (", textureCacheBytes_ / (1024 * 1024),
" MB / ", textureCacheBudgetBytes_ / (1024 * 1024),
" MB), rejecting texture: ", path);
}
++textureBudgetRejectWarnings_;
return whiteTexture.get();
}
@ -444,8 +482,7 @@ VkTexture* TerrainRenderer::loadTexture(const std::string& path) {
VkTexture* raw = tex.get();
TextureCacheEntry e;
e.texture = std::move(tex);
size_t base = static_cast<size_t>(blp.width) * static_cast<size_t>(blp.height) * 4ull;
e.approxBytes = base + (base / 3);
e.approxBytes = approxBytes;
e.lastUse = ++textureCacheCounter_;
textureCacheBytes_ += e.approxBytes;
textureCache[key] = std::move(e);

View file

@ -17,6 +17,7 @@
#include <algorithm>
#include <chrono>
#include <cmath>
#include <cstdlib>
#include <filesystem>
#include <fstream>
#include <future>
@ -27,6 +28,17 @@
namespace wowee {
namespace rendering {
namespace {
size_t envSizeMBOrDefault(const char* name, size_t defMb) {
const char* raw = std::getenv(name);
if (!raw || !*raw) return defMb;
char* end = nullptr;
unsigned long long mb = std::strtoull(raw, &end, 10);
if (end == raw || mb == 0) return defMb;
return static_cast<size_t>(mb);
}
} // namespace
static void transformAABB(const glm::mat4& modelMatrix,
const glm::vec3& localMin,
const glm::vec3& localMax,
@ -214,6 +226,12 @@ bool WMORenderer::initialize(VkContext* ctx, VkDescriptorSetLayout perFrameLayou
whiteTexture_->upload(*vkCtx_, whitePixel, 1, 1, VK_FORMAT_R8G8B8A8_UNORM, false);
whiteTexture_->createSampler(device, VK_FILTER_LINEAR, VK_FILTER_LINEAR,
VK_SAMPLER_ADDRESS_MODE_REPEAT);
textureCacheBudgetBytes_ =
envSizeMBOrDefault("WOWEE_WMO_TEX_CACHE_MB", 512) * 1024ull * 1024ull;
modelCacheLimit_ = envSizeMBOrDefault("WOWEE_WMO_MODEL_LIMIT", 4000);
core::Logger::getInstance().info("WMO texture cache budget: ",
textureCacheBudgetBytes_ / (1024 * 1024), " MB");
core::Logger::getInstance().info("WMO model cache limit: ", modelCacheLimit_);
core::Logger::getInstance().info("WMO renderer initialized (Vulkan)");
initialized_ = true;
@ -251,6 +269,9 @@ void WMORenderer::shutdown() {
textureCache.clear();
textureCacheBytes_ = 0;
textureCacheCounter_ = 0;
failedTextureCache_.clear();
loggedTextureLoadFails_.clear();
textureBudgetRejectWarnings_ = 0;
// Free white texture
if (whiteTexture_) { whiteTexture_->destroy(device, allocator); whiteTexture_.reset(); }
@ -301,6 +322,14 @@ bool WMORenderer::loadModel(const pipeline::WMOModel& model, uint32_t id) {
}
}
static std::unordered_set<uint32_t> retryReloadedModels;
static bool retryReloadedModelsCapped = false;
if (retryReloadedModels.size() > 8192) {
retryReloadedModels.clear();
if (!retryReloadedModelsCapped) {
core::Logger::getInstance().warning("WMO fallback-retry set exceeded 8192 entries; reset");
retryReloadedModelsCapped = true;
}
}
if (!hasResolvedTexture && retryReloadedModels.insert(id).second) {
core::Logger::getInstance().warning(
"WMO model ", id,
@ -313,6 +342,15 @@ bool WMORenderer::loadModel(const pipeline::WMOModel& model, uint32_t id) {
return true;
}
}
if (loadedModels.size() >= modelCacheLimit_) {
if (modelLimitRejectWarnings_ < 8 || (modelLimitRejectWarnings_ % 120) == 0) {
core::Logger::getInstance().warning("WMO model cache full (",
loadedModels.size(), "/", modelCacheLimit_,
"), skipping model load: id=", id);
}
++modelLimitRejectWarnings_;
return false;
}
core::Logger::getInstance().debug("Loading WMO model ", id, " with ", model.groups.size(), " groups, ",
model.textures.size(), " textures...");
@ -1863,10 +1901,21 @@ VkTexture* WMORenderer::loadTexture(const std::string& path) {
}
}
std::vector<std::string> attemptedCandidates;
attemptedCandidates.reserve(uniqueCandidates.size());
for (const auto& c : uniqueCandidates) {
if (!failedTextureCache_.count(c)) {
attemptedCandidates.push_back(c);
}
}
if (attemptedCandidates.empty()) {
return whiteTexture_.get();
}
// Try loading all candidates until one succeeds
pipeline::BLPImage blp;
std::string resolvedKey;
for (const auto& c : uniqueCandidates) {
for (const auto& c : attemptedCandidates) {
blp = assetManager->loadTexture(c);
if (blp.isValid()) {
resolvedKey = c;
@ -1874,7 +1923,15 @@ VkTexture* WMORenderer::loadTexture(const std::string& path) {
}
}
if (!blp.isValid()) {
core::Logger::getInstance().warning("WMO: Failed to load texture: ", path);
static constexpr size_t kMaxFailedTextureCache = 200000;
for (const auto& c : attemptedCandidates) {
if (failedTextureCache_.size() < kMaxFailedTextureCache) {
failedTextureCache_.insert(c);
}
}
if (loggedTextureLoadFails_.insert(key).second) {
core::Logger::getInstance().warning("WMO: Failed to load texture: ", path);
}
// Do not cache failures as white. MPQ reads can fail transiently
// during streaming/contention, and caching white here permanently
// poisons the texture for this session.
@ -1883,6 +1940,19 @@ VkTexture* WMORenderer::loadTexture(const std::string& path) {
core::Logger::getInstance().debug("WMO texture: ", path, " size=", blp.width, "x", blp.height);
size_t base = static_cast<size_t>(blp.width) * static_cast<size_t>(blp.height) * 4ull;
size_t approxBytes = base + (base / 3);
if (textureCacheBytes_ + approxBytes > textureCacheBudgetBytes_) {
if (textureBudgetRejectWarnings_ < 8 || (textureBudgetRejectWarnings_ % 120) == 0) {
core::Logger::getInstance().warning(
"WMO texture cache full (", textureCacheBytes_ / (1024 * 1024),
" MB / ", textureCacheBudgetBytes_ / (1024 * 1024),
" MB), rejecting texture: ", path);
}
++textureBudgetRejectWarnings_;
return whiteTexture_.get();
}
// Create Vulkan texture
auto texture = std::make_unique<VkTexture>();
if (!texture->upload(*vkCtx_, blp.data.data(), blp.width, blp.height,
@ -1896,8 +1966,7 @@ VkTexture* WMORenderer::loadTexture(const std::string& path) {
// Cache it
TextureCacheEntry e;
VkTexture* rawPtr = texture.get();
size_t base = static_cast<size_t>(blp.width) * static_cast<size_t>(blp.height) * 4ull;
e.approxBytes = base + (base / 3);
e.approxBytes = approxBytes;
e.lastUse = ++textureCacheCounter_;
e.texture = std::move(texture);
textureCacheBytes_ += e.approxBytes;