diff --git a/include/rendering/m2_renderer.hpp b/include/rendering/m2_renderer.hpp index 45900e8b..517e42ce 100644 --- a/include/rendering/m2_renderer.hpp +++ b/include/rendering/m2_renderer.hpp @@ -478,7 +478,7 @@ private: uint32_t instanceCount; uint32_t _pad[3] = {}; }; - static constexpr uint32_t MAX_CULL_INSTANCES = 16384; + static constexpr uint32_t MAX_CULL_INSTANCES = 65536; VkPipeline cullPipeline_ = VK_NULL_HANDLE; VkPipelineLayout cullPipelineLayout_ = VK_NULL_HANDLE; VkDescriptorSetLayout cullSetLayout_ = VK_NULL_HANDLE; diff --git a/src/core/entity_spawner.cpp b/src/core/entity_spawner.cpp index 838e1f58..b745bb85 100644 --- a/src/core/entity_spawner.cpp +++ b/src/core/entity_spawner.cpp @@ -757,6 +757,12 @@ void EntitySpawner::buildGameObjectDisplayLookups() { gameObjectDisplayIdToPath_[displayId] = modelName; } LOG_INFO("Loaded ", gameObjectDisplayIdToPath_.size(), " gameobject display mappings"); + } else { + LOG_WARNING("GameObjectDisplayInfo.dbc failed to load — no GO display mappings available"); + } + + if (gameObjectDisplayIdToPath_.empty()) { + LOG_WARNING("GO display mapping table is EMPTY — game objects will not render"); } gameObjectLookupsBuilt_ = true; @@ -3102,6 +3108,10 @@ void EntitySpawner::spawnOnlineGameObject(uint64_t guid, uint32_t entry, uint32_ } if (!gameObjectLookupsBuilt_) return; + LOG_DEBUG("GO spawn attempt: guid=0x", std::hex, guid, std::dec, + " displayId=", displayId, " entry=", entry, + " pos=(", x, ", ", y, ", ", z, ")"); + auto goIt = gameObjectInstances_.find(guid); if (goIt != gameObjectInstances_.end()) { // Already have a render instance — update its position (e.g. transport re-creation) @@ -3363,8 +3373,17 @@ void EntitySpawner::spawnOnlineGameObject(uint64_t guid, uint32_t entry, uint32_ auto skinData = assetManager_->readFile(skinPath); if (!skinData.empty() && model.version >= 264) { pipeline::M2Loader::loadSkin(skinData, model); + } else if (skinData.empty() && model.version >= 264) { + LOG_WARNING("GO skin file MISSING for WotLK M2 (no indices/batches): ", skinPath); } + LOG_DEBUG("GO model: ", modelPath, " v=", model.version, + " verts=", model.vertices.size(), + " idx=", model.indices.size(), + " batches=", model.batches.size(), + " bones=", model.bones.size(), + " skin=", (skinData.empty() ? "MISSING" : "ok")); + if (!m2Renderer->loadModel(model, modelId)) { LOG_WARNING("Failed to load gameobject model: ", modelPath); gameObjectDisplayIdFailedCache_.insert(displayId); @@ -4222,6 +4241,13 @@ void EntitySpawner::processGameObjectSpawnQueue() { if (pendingGameObjectSpawns_.empty()) return; + static int goQueueLogCounter = 0; + if (++goQueueLogCounter % 60 == 1) { + LOG_DEBUG("GO queue: ", pendingGameObjectSpawns_.size(), " pending, ", + gameObjectInstances_.size(), " spawned, ", + gameObjectDisplayIdFailedCache_.size(), " failed"); + } + // Process spawns: cached WMOs and M2s go sync (cheap), uncached WMOs go async auto startTime = std::chrono::steady_clock::now(); static constexpr float kBudgetMs = 2.0f; diff --git a/src/rendering/m2_renderer.cpp b/src/rendering/m2_renderer.cpp index 0a9629d7..c0532787 100644 --- a/src/rendering/m2_renderer.cpp +++ b/src/rendering/m2_renderer.cpp @@ -1371,6 +1371,11 @@ bool M2Renderer::loadModel(const pipeline::M2Model& model, uint32_t modelId) { gpuModel.boundMin = tightMin; gpuModel.boundMax = tightMax; gpuModel.boundRadius = model.boundRadius; + // Fallback: compute bound radius from vertex extents when M2 header reports 0 + if (gpuModel.boundRadius < 0.01f && !model.vertices.empty()) { + glm::vec3 extent = tightMax - tightMin; + gpuModel.boundRadius = glm::length(extent) * 0.5f; + } gpuModel.indexCount = static_cast(model.indices.size()); gpuModel.vertexCount = static_cast(model.vertices.size()); @@ -1915,12 +1920,14 @@ uint32_t M2Renderer::createInstance(uint32_t modelId, const glm::vec3& position, // Initialize animation: play first sequence (usually Stand/Idle) const auto& mdl = mdlRef; - if (mdl.hasAnimation && !mdl.disableAnimation && !mdl.sequences.empty()) { - instance.currentSequenceIndex = 0; - instance.idleSequenceIndex = 0; - instance.animDuration = static_cast(mdl.sequences[0].duration); - instance.animTime = static_cast(randRange(std::max(1u, mdl.sequences[0].duration))); - instance.variationTimer = randFloat(3000.0f, 11000.0f); + if (mdl.hasAnimation && !mdl.disableAnimation) { + if (!mdl.sequences.empty()) { + instance.currentSequenceIndex = 0; + instance.idleSequenceIndex = 0; + instance.animDuration = static_cast(mdl.sequences[0].duration); + instance.animTime = static_cast(randRange(std::max(1u, mdl.sequences[0].duration))); + instance.variationTimer = randFloat(3000.0f, 11000.0f); + } // Seed bone matrices from an existing instance of the same model so the // new instance renders immediately instead of being invisible until the @@ -2022,12 +2029,14 @@ uint32_t M2Renderer::createInstanceWithMatrix(uint32_t modelId, const glm::mat4& instance.cachedModel = &mdl2; // Initialize animation - if (mdl2.hasAnimation && !mdl2.disableAnimation && !mdl2.sequences.empty()) { - instance.currentSequenceIndex = 0; - instance.idleSequenceIndex = 0; - instance.animDuration = static_cast(mdl2.sequences[0].duration); - instance.animTime = static_cast(randRange(std::max(1u, mdl2.sequences[0].duration))); - instance.variationTimer = randFloat(3000.0f, 11000.0f); + if (mdl2.hasAnimation && !mdl2.disableAnimation) { + if (!mdl2.sequences.empty()) { + instance.currentSequenceIndex = 0; + instance.idleSequenceIndex = 0; + instance.animDuration = static_cast(mdl2.sequences[0].duration); + instance.animTime = static_cast(randRange(std::max(1u, mdl2.sequences[0].duration))); + instance.variationTimer = randFloat(3000.0f, 11000.0f); + } // Seed bone matrices from an existing sibling so the instance renders immediately for (const auto& existing : instances) { @@ -2612,6 +2621,28 @@ void M2Renderer::render(VkCommandBuffer cmd, VkDescriptorSet perFrameSet, const LOG_INFO("M2 render: ", instances.size(), " instances, ", models.size(), " models"); } + // Periodic diagnostic: report render pipeline stats every 10 seconds + static int diagCounter = 0; + if (++diagCounter == 600) { // ~10s at 60fps + diagCounter = 0; + uint32_t totalValid = 0, totalAnimated = 0, totalBonesReady = 0, totalMegaBoneOk = 0; + for (const auto& inst : instances) { + if (inst.cachedIsValid) totalValid++; + if (inst.cachedHasAnimation && !inst.cachedDisableAnimation) { + totalAnimated++; + if (!inst.boneMatrices.empty()) totalBonesReady++; + if (inst.megaBoneOffset != 0) totalMegaBoneOk++; + } + } + LOG_INFO("M2 diag: total=", instances.size(), + " valid=", totalValid, + " animated=", totalAnimated, + " bonesReady=", totalBonesReady, + " megaBoneOk=", totalMegaBoneOk, + " visible=", sortedVisible_.size(), + " draws=", lastDrawCallCount); + } + // Reuse persistent buffers (clear instead of reallocating) glowSprites_.clear();