From f855327054933c43e8bb1310a6c2f934c36038f7 Mon Sep 17 00:00:00 2001 From: Kelsi Date: Fri, 13 Mar 2026 01:45:31 -0700 Subject: [PATCH] fix: eliminate 490ms transport-doodad stall and GPU device-loss crash MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Three root causes identified from wowee.log crash at frame 134368: 1. processPendingTransportDoodads() was doing N separate synchronous GPU uploads (vkQueueSubmit + vkWaitForFences per texture per doodad). With 30+ doodads × multiple textures, this caused the 489ms stall in the 'gameobject/transport queues' update stage. Fixed by wrapping the entire batch in beginUploadBatch()/endUploadBatch() so all texture layout transitions are submitted in a single async command buffer. 2. Game objects whose M2 model has no geometry/particles (empty or unsupported format) were retried every frame because loadModel() returns false without adding to gameObjectDisplayIdModelCache_. Added gameObjectDisplayIdFailedCache_ to permanently skip these display IDs after the first failure, stopping the per-frame spam. 3. renderM2Ribbons() only checked ribbonPipeline_ != null, not ribbonAdditivePipeline_. If additive pipeline creation failed, any ribbon with additive blending would call vkCmdBindPipeline with VK_NULL_HANDLE, causing VK_ERROR_DEVICE_LOST on the GPU side. Extended the early-return guard to cover both ribbon pipelines. --- include/core/application.hpp | 1 + src/core/application.cpp | 18 ++++++++++++++++++ src/rendering/m2_renderer.cpp | 2 +- 3 files changed, 20 insertions(+), 1 deletion(-) diff --git a/include/core/application.hpp b/include/core/application.hpp index 7da1469b..85339c04 100644 --- a/include/core/application.hpp +++ b/include/core/application.hpp @@ -271,6 +271,7 @@ private: }; std::unordered_map gameObjectDisplayIdToPath_; std::unordered_map gameObjectDisplayIdModelCache_; // displayId → M2 modelId + std::unordered_set gameObjectDisplayIdFailedCache_; // displayIds that permanently fail to load std::unordered_map gameObjectDisplayIdWmoCache_; // displayId → WMO modelId std::unordered_map gameObjectInstances_; // guid → instance info struct PendingTransportMove { diff --git a/src/core/application.cpp b/src/core/application.cpp index 0feba036..a782363c 100644 --- a/src/core/application.cpp +++ b/src/core/application.cpp @@ -7234,6 +7234,11 @@ void Application::spawnOnlineGameObject(uint64_t guid, uint32_t entry, uint32_t auto* m2Renderer = renderer->getM2Renderer(); if (!m2Renderer) return; + // Skip displayIds that permanently failed to load (e.g. empty/unsupported M2s). + // Without this guard the same empty model is re-parsed every frame, causing + // sustained log spam and wasted CPU. + if (gameObjectDisplayIdFailedCache_.count(displayId)) return; + uint32_t modelId = 0; auto itCache = gameObjectDisplayIdModelCache_.find(displayId); if (itCache != gameObjectDisplayIdModelCache_.end()) { @@ -7252,12 +7257,14 @@ void Application::spawnOnlineGameObject(uint64_t guid, uint32_t entry, uint32_t auto m2Data = assetManager->readFile(modelPath); if (m2Data.empty()) { LOG_WARNING("Failed to read gameobject M2: ", modelPath); + gameObjectDisplayIdFailedCache_.insert(displayId); return; } pipeline::M2Model model = pipeline::M2Loader::load(m2Data); if (model.vertices.empty()) { LOG_WARNING("Failed to parse gameobject M2: ", modelPath); + gameObjectDisplayIdFailedCache_.insert(displayId); return; } @@ -7269,6 +7276,7 @@ void Application::spawnOnlineGameObject(uint64_t guid, uint32_t entry, uint32_t if (!m2Renderer->loadModel(model, modelId)) { LOG_WARNING("Failed to load gameobject model: ", modelPath); + gameObjectDisplayIdFailedCache_.insert(displayId); return; } @@ -8189,6 +8197,13 @@ void Application::processPendingTransportDoodads() { auto startTime = std::chrono::steady_clock::now(); static constexpr float kDoodadBudgetMs = 4.0f; + // Batch all GPU uploads into a single async command buffer submission so that + // N doodads with multiple textures each don't each block on vkQueueSubmit + + // vkWaitForFences. Without batching, 30+ doodads × several textures = hundreds + // of sync GPU submits → the 490ms stall that preceded the VK_ERROR_DEVICE_LOST. + auto* vkCtx = renderer->getVkContext(); + if (vkCtx) vkCtx->beginUploadBatch(); + size_t budgetLeft = MAX_TRANSPORT_DOODADS_PER_FRAME; for (auto it = pendingTransportDoodadBatches_.begin(); it != pendingTransportDoodadBatches_.end() && budgetLeft > 0;) { @@ -8256,6 +8271,9 @@ void Application::processPendingTransportDoodads() { ++it; } } + + // Finalize the upload batch — submit all GPU copies in one shot (async, no wait). + if (vkCtx) vkCtx->endUploadBatch(); } void Application::processPendingMount() { diff --git a/src/rendering/m2_renderer.cpp b/src/rendering/m2_renderer.cpp index 6ef65e5f..c5af6c76 100644 --- a/src/rendering/m2_renderer.cpp +++ b/src/rendering/m2_renderer.cpp @@ -3565,7 +3565,7 @@ void M2Renderer::updateRibbons(M2Instance& inst, const M2ModelGPU& gpu, float dt // Ribbon rendering // --------------------------------------------------------------------------- void M2Renderer::renderM2Ribbons(VkCommandBuffer cmd, VkDescriptorSet perFrameSet) { - if (!ribbonPipeline_ || !ribbonVB_ || !ribbonVBMapped_) return; + if (!ribbonPipeline_ || !ribbonAdditivePipeline_ || !ribbonVB_ || !ribbonVBMapped_) return; // Build camera right vector for billboard orientation // For ribbons we orient the quad strip along the spine with screen-space up.