From f78d885e13372d3c068ef9d1353ab67c3869297e Mon Sep 17 00:00:00 2001 From: Kelsi Date: Wed, 18 Mar 2026 07:00:50 -0700 Subject: [PATCH] fix: add 60-second grace period to M2 model cleanup Models that lose all instances are no longer immediately evicted from GPU memory. Instead they get a 60-second grace period, preventing the thrash cycle where GO models (barrels, chests, herbs) were evicted every 5 seconds and re-loaded when the same object type respawned. --- include/rendering/m2_renderer.hpp | 4 ++++ src/rendering/m2_renderer.cpp | 23 +++++++++++++++++++++-- 2 files changed, 25 insertions(+), 2 deletions(-) diff --git a/include/rendering/m2_renderer.hpp b/include/rendering/m2_renderer.hpp index 22578309..1f19b46e 100644 --- a/include/rendering/m2_renderer.hpp +++ b/include/rendering/m2_renderer.hpp @@ -13,6 +13,7 @@ #include #include #include +#include #include namespace wowee { @@ -434,6 +435,9 @@ private: void* glowVBMapped_ = nullptr; std::unordered_map models; + // Grace period for model cleanup: track when a model first became instanceless. + // Models are only evicted after 60 seconds with no instances. + std::unordered_map modelUnusedSince_; std::vector instances; // O(1) dedup: key = (modelId, quantized x, quantized y, quantized z) → instanceId diff --git a/src/rendering/m2_renderer.cpp b/src/rendering/m2_renderer.cpp index 390ee2c5..40ffd4b1 100644 --- a/src/rendering/m2_renderer.cpp +++ b/src/rendering/m2_renderer.cpp @@ -1753,6 +1753,7 @@ uint32_t M2Renderer::createInstance(uint32_t modelId, const glm::vec3& position, return 0; } const auto& mdlRef = modelIt->second; + modelUnusedSince_.erase(modelId); // Deduplicate: skip if same model already at nearly the same position. // Uses hash map for O(1) lookup instead of O(N) scan. @@ -1864,6 +1865,7 @@ uint32_t M2Renderer::createInstanceWithMatrix(uint32_t modelId, const glm::mat4& LOG_WARNING("Cannot create instance: model ", modelId, " not loaded"); return 0; } + modelUnusedSince_.erase(modelId); // Deduplicate: O(1) hash lookup { @@ -4276,11 +4278,28 @@ void M2Renderer::cleanupUnusedModels() { usedModelIds.insert(instance.modelId); } - // Find and remove models with no instances + const auto now = std::chrono::steady_clock::now(); + constexpr auto kGracePeriod = std::chrono::seconds(60); + + // Find models with no instances that have exceeded the grace period. + // Models that just lost their last instance get tracked but not evicted + // immediately — this prevents thrashing when GO models are briefly + // instance-free between despawn and respawn cycles. std::vector toRemove; for (const auto& [id, model] : models) { - if (usedModelIds.find(id) == usedModelIds.end()) { + if (usedModelIds.find(id) != usedModelIds.end()) { + // Model still in use — clear any pending unused timestamp + modelUnusedSince_.erase(id); + continue; + } + auto unusedIt = modelUnusedSince_.find(id); + if (unusedIt == modelUnusedSince_.end()) { + // First cycle with no instances — start the grace timer + modelUnusedSince_[id] = now; + } else if (now - unusedIt->second >= kGracePeriod) { + // Grace period expired — mark for removal toRemove.push_back(id); + modelUnusedSince_.erase(unusedIt); } }