fix: eliminate 490ms transport-doodad stall and GPU device-loss crash

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.
This commit is contained in:
Kelsi 2026-03-13 01:45:31 -07:00
parent 367b48af6b
commit f855327054
3 changed files with 20 additions and 1 deletions

View file

@ -271,6 +271,7 @@ private:
};
std::unordered_map<uint32_t, std::string> gameObjectDisplayIdToPath_;
std::unordered_map<uint32_t, uint32_t> gameObjectDisplayIdModelCache_; // displayId → M2 modelId
std::unordered_set<uint32_t> gameObjectDisplayIdFailedCache_; // displayIds that permanently fail to load
std::unordered_map<uint32_t, uint32_t> gameObjectDisplayIdWmoCache_; // displayId → WMO modelId
std::unordered_map<uint64_t, GameObjectInstanceInfo> gameObjectInstances_; // guid → instance info
struct PendingTransportMove {

View file

@ -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() {

View file

@ -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.