fix: stabilize turtle world entry session handling

This commit is contained in:
Kelsi 2026-03-15 01:21:23 -07:00
parent 4dba20b757
commit b0fafe5efa
20 changed files with 2283 additions and 1380 deletions

View file

@ -343,6 +343,8 @@ void CharacterRenderer::shutdown() {
// Clean up composite cache
compositeCache_.clear();
failedTextureCache_.clear();
failedTextureRetryAt_.clear();
textureLookupSerial_ = 0;
whiteTexture_.reset();
transparentTexture_.reset();
@ -430,6 +432,8 @@ void CharacterRenderer::clear() {
textureCacheBytes_ = 0;
textureCacheCounter_ = 0;
loggedTextureLoadFails_.clear();
failedTextureRetryAt_.clear();
textureLookupSerial_ = 0;
// Clear composite and failed caches
compositeCache_.clear();
@ -604,6 +608,7 @@ CharacterRenderer::NormalMapResult CharacterRenderer::generateNormalHeightMapCPU
}
VkTexture* CharacterRenderer::loadTexture(const std::string& path) {
constexpr uint64_t kFailedTextureRetryLookups = 512;
// Skip empty or whitespace-only paths (type-0 textures have no filename)
if (path.empty()) return whiteTexture_.get();
bool allWhitespace = true;
@ -619,6 +624,7 @@ VkTexture* CharacterRenderer::loadTexture(const std::string& path) {
return key;
};
std::string key = normalizeKey(path);
const uint64_t lookupSerial = ++textureLookupSerial_;
auto containsToken = [](const std::string& haystack, const char* token) {
return haystack.find(token) != std::string::npos;
};
@ -634,6 +640,10 @@ VkTexture* CharacterRenderer::loadTexture(const std::string& path) {
it->second.lastUse = ++textureCacheCounter_;
return it->second.texture.get();
}
auto failIt = failedTextureRetryAt_.find(key);
if (failIt != failedTextureRetryAt_.end() && lookupSerial < failIt->second) {
return whiteTexture_.get();
}
if (!assetManager || !assetManager->isInitialized()) {
return whiteTexture_.get();
@ -652,8 +662,9 @@ VkTexture* CharacterRenderer::loadTexture(const std::string& path) {
blpImage = assetManager->loadTexture(key);
}
if (!blpImage.isValid()) {
// Return white fallback but don't cache the failure — allow retry
// on next character load in case the asset becomes available.
// Cache misses briefly to avoid repeated expensive MPQ/disk probes.
failedTextureCache_.insert(key);
failedTextureRetryAt_[key] = lookupSerial + kFailedTextureRetryLookups;
if (loggedTextureLoadFails_.insert(key).second) {
core::Logger::getInstance().warning("Failed to load texture: ", path);
}
@ -666,6 +677,7 @@ VkTexture* CharacterRenderer::loadTexture(const std::string& path) {
if (failedTextureCache_.size() < kMaxFailedTextureCache) {
// Budget is saturated; avoid repeatedly decoding/uploading this texture.
failedTextureCache_.insert(key);
failedTextureRetryAt_[key] = lookupSerial + kFailedTextureRetryLookups;
}
if (textureBudgetRejectWarnings_ < 3) {
core::Logger::getInstance().warning(
@ -724,6 +736,8 @@ VkTexture* CharacterRenderer::loadTexture(const std::string& path) {
textureHasAlphaByPtr_[texPtr] = hasAlpha;
textureColorKeyBlackByPtr_[texPtr] = colorKeyBlackHint;
textureCache[key] = std::move(e);
failedTextureCache_.erase(key);
failedTextureRetryAt_.erase(key);
core::Logger::getInstance().debug("Loaded character texture: ", path, " (", blpImage.width, "x", blpImage.height, ")");
return texPtr;

View file

@ -714,7 +714,9 @@ void M2Renderer::shutdown() {
textureHasAlphaByPtr_.clear();
textureColorKeyBlackByPtr_.clear();
failedTextureCache_.clear();
failedTextureRetryAt_.clear();
loggedTextureLoadFails_.clear();
textureLookupSerial_ = 0;
textureBudgetRejectWarnings_ = 0;
whiteTexture_.reset();
glowTexture_.reset();
@ -4251,6 +4253,7 @@ void M2Renderer::cleanupUnusedModels() {
}
VkTexture* M2Renderer::loadTexture(const std::string& path, uint32_t texFlags) {
constexpr uint64_t kFailedTextureRetryLookups = 512;
auto normalizeKey = [](std::string key) {
std::replace(key.begin(), key.end(), '/', '\\');
std::transform(key.begin(), key.end(), key.begin(),
@ -4258,6 +4261,7 @@ VkTexture* M2Renderer::loadTexture(const std::string& path, uint32_t texFlags) {
return key;
};
std::string key = normalizeKey(path);
const uint64_t lookupSerial = ++textureLookupSerial_;
// Check cache
auto it = textureCache.find(key);
@ -4265,7 +4269,10 @@ VkTexture* M2Renderer::loadTexture(const std::string& path, uint32_t texFlags) {
it->second.lastUse = ++textureCacheCounter_;
return it->second.texture.get();
}
// No negative cache check — allow retries for transiently missing textures
auto failIt = failedTextureRetryAt_.find(key);
if (failIt != failedTextureRetryAt_.end() && lookupSerial < failIt->second) {
return whiteTexture_.get();
}
auto containsToken = [](const std::string& haystack, const char* token) {
return haystack.find(token) != std::string::npos;
@ -4296,8 +4303,9 @@ VkTexture* M2Renderer::loadTexture(const std::string& path, uint32_t texFlags) {
blp = assetManager->loadTexture(key);
}
if (!blp.isValid()) {
// Return white fallback but don't cache the failure — MPQ reads can
// fail transiently during streaming; allow retry on next model load.
// Cache misses briefly to avoid repeated expensive MPQ/disk probes.
failedTextureCache_.insert(key);
failedTextureRetryAt_[key] = lookupSerial + kFailedTextureRetryLookups;
if (loggedTextureLoadFails_.insert(key).second) {
LOG_WARNING("M2: Failed to load texture: ", path);
}
@ -4312,6 +4320,7 @@ VkTexture* M2Renderer::loadTexture(const std::string& path, uint32_t texFlags) {
// Cache budget-rejected keys too; without this we repeatedly decode/load
// the same textures every frame once budget is saturated.
failedTextureCache_.insert(key);
failedTextureRetryAt_[key] = lookupSerial + kFailedTextureRetryLookups;
}
if (textureBudgetRejectWarnings_ < 3) {
LOG_WARNING("M2 texture cache full (", textureCacheBytes_ / (1024 * 1024),
@ -4350,6 +4359,8 @@ VkTexture* M2Renderer::loadTexture(const std::string& path, uint32_t texFlags) {
e.lastUse = ++textureCacheCounter_;
textureCacheBytes_ += e.approxBytes;
textureCache[key] = std::move(e);
failedTextureCache_.erase(key);
failedTextureRetryAt_.erase(key);
textureHasAlphaByPtr_[texPtr] = hasAlpha;
textureColorKeyBlackByPtr_[texPtr] = colorKeyBlackHint;
LOG_DEBUG("M2: Loaded texture: ", path, " (", blp.width, "x", blp.height, ")");

View file

@ -54,9 +54,11 @@ int computeTerrainWorkerCount() {
unsigned hc = std::thread::hardware_concurrency();
if (hc > 0) {
// Use most cores for loading — leave 1-2 for render/update threads.
const unsigned reserved = (hc >= 8u) ? 2u : 1u;
const unsigned targetWorkers = std::max(4u, hc - reserved);
// Keep terrain workers conservative by default. Over-subscribing loader
// threads can starve main-thread networking/render updates on large-core CPUs.
const unsigned reserved = (hc >= 16u) ? 4u : ((hc >= 8u) ? 2u : 1u);
const unsigned maxDefaultWorkers = 8u;
const unsigned targetWorkers = std::max(4u, std::min(maxDefaultWorkers, hc - reserved));
return static_cast<int>(targetWorkers);
}
return 4; // Fallback
@ -896,6 +898,9 @@ bool TerrainManager::advanceFinalization(FinalizingTile& ft) {
if (p.uniqueId != 0 && placedDoodadIds.count(p.uniqueId)) {
continue;
}
if (!m2Renderer->hasModel(p.modelId)) {
continue;
}
uint32_t instId = m2Renderer->createInstance(p.modelId, p.position, p.rotation, p.scale);
if (instId) {
ft.m2InstanceIds.push_back(instId);
@ -961,6 +966,9 @@ bool TerrainManager::advanceFinalization(FinalizingTile& ft) {
if (wmoReady.uniqueId != 0 && placedWmoIds.count(wmoReady.uniqueId)) {
continue;
}
if (!wmoRenderer->isModelLoaded(wmoReady.modelId)) {
continue;
}
uint32_t wmoInstId = wmoRenderer->createInstance(wmoReady.modelId, wmoReady.position, wmoReady.rotation);
if (wmoInstId) {
ft.wmoInstanceIds.push_back(wmoInstId);

View file

@ -307,7 +307,9 @@ void WMORenderer::shutdown() {
textureCacheBytes_ = 0;
textureCacheCounter_ = 0;
failedTextureCache_.clear();
failedTextureRetryAt_.clear();
loggedTextureLoadFails_.clear();
textureLookupSerial_ = 0;
textureBudgetRejectWarnings_ = 0;
// Free white texture and flat normal texture
@ -1087,7 +1089,9 @@ void WMORenderer::clearAll() {
textureCacheBytes_ = 0;
textureCacheCounter_ = 0;
failedTextureCache_.clear();
failedTextureRetryAt_.clear();
loggedTextureLoadFails_.clear();
textureLookupSerial_ = 0;
textureBudgetRejectWarnings_ = 0;
precomputedFloorGrid.clear();
@ -2237,6 +2241,7 @@ std::unique_ptr<VkTexture> WMORenderer::generateNormalHeightMap(
}
VkTexture* WMORenderer::loadTexture(const std::string& path) {
constexpr uint64_t kFailedTextureRetryLookups = 512;
if (!assetManager || !vkCtx_) {
return whiteTexture_.get();
}
@ -2312,7 +2317,19 @@ VkTexture* WMORenderer::loadTexture(const std::string& path) {
}
}
const auto& attemptedCandidates = uniqueCandidates;
const uint64_t lookupSerial = ++textureLookupSerial_;
std::vector<std::string> attemptedCandidates;
attemptedCandidates.reserve(uniqueCandidates.size());
for (const auto& c : uniqueCandidates) {
auto fit = failedTextureRetryAt_.find(c);
if (fit != failedTextureRetryAt_.end() && lookupSerial < fit->second) {
continue;
}
attemptedCandidates.push_back(c);
}
if (attemptedCandidates.empty()) {
return whiteTexture_.get();
}
// Try loading all candidates until one succeeds
// Check pre-decoded BLP cache first (populated by background worker threads)
@ -2339,6 +2356,10 @@ VkTexture* WMORenderer::loadTexture(const std::string& path) {
}
}
if (!blp.isValid()) {
for (const auto& c : attemptedCandidates) {
failedTextureCache_.insert(c);
failedTextureRetryAt_[c] = lookupSerial + kFailedTextureRetryLookups;
}
if (loggedTextureLoadFails_.insert(key).second) {
core::Logger::getInstance().warning("WMO: Failed to load texture: ", path);
}
@ -2353,6 +2374,10 @@ VkTexture* WMORenderer::loadTexture(const std::string& path) {
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_) {
for (const auto& c : attemptedCandidates) {
failedTextureCache_.insert(c);
failedTextureRetryAt_[c] = lookupSerial + kFailedTextureRetryLookups;
}
if (textureBudgetRejectWarnings_ < 3) {
core::Logger::getInstance().warning(
"WMO texture cache full (", textureCacheBytes_ / (1024 * 1024),
@ -2394,8 +2419,12 @@ VkTexture* WMORenderer::loadTexture(const std::string& path) {
textureCacheBytes_ += e.approxBytes;
if (!resolvedKey.empty()) {
textureCache[resolvedKey] = std::move(e);
failedTextureCache_.erase(resolvedKey);
failedTextureRetryAt_.erase(resolvedKey);
} else {
textureCache[key] = std::move(e);
failedTextureCache_.erase(key);
failedTextureRetryAt_.erase(key);
}
core::Logger::getInstance().debug("WMO: Loaded texture: ", path, " (", blp.width, "x", blp.height, ")");