diff --git a/include/rendering/m2_renderer.hpp b/include/rendering/m2_renderer.hpp index de4fbf84..5e402fe6 100644 --- a/include/rendering/m2_renderer.hpp +++ b/include/rendering/m2_renderer.hpp @@ -357,6 +357,9 @@ public: void removeInstances(const std::vector& instanceIds); void setSkipCollision(uint32_t instanceId, bool skip); void clear(); + /** Drop all instances but keep models in GPU memory. Cheap path for the + * editor's rebuild loop where the same model is re-instanced repeatedly. */ + void clearInstances(); void cleanupUnusedModels(); bool checkCollision(const glm::vec3& from, const glm::vec3& to, diff --git a/src/rendering/m2_renderer_instance.cpp b/src/rendering/m2_renderer_instance.cpp index 269fab62..caa16f3c 100644 --- a/src/rendering/m2_renderer_instance.cpp +++ b/src/rendering/m2_renderer_instance.cpp @@ -377,6 +377,22 @@ void M2Renderer::clear() { textureBudgetRejectWarnings_ = 0; } +void M2Renderer::clearInstances() { + if (vkCtx_) vkDeviceWaitIdle(vkCtx_->getDevice()); + for (auto& inst : instances) destroyInstanceBones(inst); + instances.clear(); + spatialGrid.clear(); + instanceIndexById.clear(); + instanceDedupMap_.clear(); + smokeInstanceIndices_.clear(); + portalInstanceIndices_.clear(); + animatedInstanceIndices_.clear(); + particleOnlyInstanceIndices_.clear(); + particleInstanceIndices_.clear(); + smokeParticles.clear(); + smokeEmitAccum = 0.0f; +} + void M2Renderer::setCollisionFocus(const glm::vec3& worldPos, float radius) { collisionFocusEnabled = (radius > 0.0f); collisionFocusPos = worldPos; diff --git a/tools/editor/editor_viewport.cpp b/tools/editor/editor_viewport.cpp index f56e04ba..fea9610b 100644 --- a/tools/editor/editor_viewport.cpp +++ b/tools/editor/editor_viewport.cpp @@ -80,6 +80,13 @@ bool EditorViewport::loadTerrain(const pipeline::TerrainMesh& mesh, void EditorViewport::clearTerrain() { if (terrainRenderer_) terrainRenderer_->clear(); + // Loading a different zone invalidates the cached models; flush them so + // their slots can be reused without leaking GPU memory across zones. + persistentM2ModelIds_.clear(); + persistentWMOModelIds_.clear(); + nextPersistentModelId_ = 1; + if (m2Renderer_) m2Renderer_->clear(); + if (wmoRenderer_) wmoRenderer_->clearAll(); } void EditorViewport::updateWater(const pipeline::ADTTerrain& terrain, int tileX, int tileY) { @@ -106,12 +113,16 @@ void EditorViewport::clearObjects() { ghostModelId_ = 0; ghostModelPath_.clear(); + // Drop instances but keep models cached on the GPU. The editor's rebuild + // path destroys-and-recreates instances every time the placement set + // changes; preserving model GPU buffers makes that path much cheaper for + // large NPC populations using shared models. if (m2Renderer_) { vkCtx_->waitAllUploads(); - m2Renderer_->clear(); + m2Renderer_->clearInstances(); } if (wmoRenderer_) { - wmoRenderer_->clearAll(); + wmoRenderer_->clearInstances(); } } @@ -120,9 +131,11 @@ void EditorViewport::rebuildObjects(const std::vector& objects, clearObjects(); if (objects.empty() && npcs.empty()) return; - // Don't call beginUploadBatch here — loadModel starts its own batch - uint32_t nextModelId = 1; - std::unordered_map m2ModelIds, wmoModelIds; + // Don't call beginUploadBatch here — loadModel starts its own batch. + // Use the persistent model-id maps so models stay cached across rebuilds. + auto& m2ModelIds = persistentM2ModelIds_; + auto& wmoModelIds = persistentWMOModelIds_; + uint32_t& nextModelId = nextPersistentModelId_; for (const auto& obj : objects) { if (obj.type == PlaceableType::M2 && m2Renderer_) { diff --git a/tools/editor/editor_viewport.hpp b/tools/editor/editor_viewport.hpp index d173f5e6..307dab18 100644 --- a/tools/editor/editor_viewport.hpp +++ b/tools/editor/editor_viewport.hpp @@ -13,6 +13,8 @@ #include #include #include +#include +#include namespace wowee { namespace pipeline { class AssetManager; } @@ -114,6 +116,12 @@ private: float fogNear_ = 5000.0f, fogFar_ = 10000.0f; float timeOfDay_ = 12.0f; + // Persistent path -> renderer model ID maps. Keeping these across rebuilds + // lets the renderer skip re-uploading models that are still in its cache. + std::unordered_map persistentM2ModelIds_; + std::unordered_map persistentWMOModelIds_; + uint32_t nextPersistentModelId_ = 1; + // Ghost preview state std::string ghostModelPath_; uint32_t ghostModelId_ = 0;