diff --git a/include/core/application.hpp b/include/core/application.hpp index 1294ee12..92e96e8e 100644 --- a/include/core/application.hpp +++ b/include/core/application.hpp @@ -9,6 +9,7 @@ #include #include #include +#include namespace wowee { @@ -189,6 +190,12 @@ private: uint32_t wyvernDisplayId_ = 0; bool lastTaxiFlight_ = false; uint32_t loadedMapId_ = 0xFFFFFFFF; // Map ID of currently loaded terrain (0xFFFFFFFF = none) + uint32_t worldLoadGeneration_ = 0; // Incremented on each world entry to detect re-entrant loads + bool loadingWorld_ = false; // True while loadOnlineWorldTerrain is running + struct PendingWorldEntry { + uint32_t mapId; float x, y, z; + }; + std::optional pendingWorldEntry_; // Deferred world entry during loading float taxiLandingClampTimer_ = 0.0f; float worldEntryMovementGraceTimer_ = 0.0f; float facingSendCooldown_ = 0.0f; // Rate-limits MSG_MOVE_SET_FACING diff --git a/include/rendering/character_renderer.hpp b/include/rendering/character_renderer.hpp index 9ee98a7d..3a2d24ec 100644 --- a/include/rendering/character_renderer.hpp +++ b/include/rendering/character_renderer.hpp @@ -48,6 +48,7 @@ public: VkRenderPass renderPassOverride = VK_NULL_HANDLE, VkSampleCountFlagBits msaaSamples = VK_SAMPLE_COUNT_1_BIT); void shutdown(); + void clear(); // Remove all models/instances/textures but keep pipelines/pools void setAssetManager(pipeline::AssetManager* am) { assetManager = am; } diff --git a/include/rendering/m2_renderer.hpp b/include/rendering/m2_renderer.hpp index c2a663b5..d3f13aa1 100644 --- a/include/rendering/m2_renderer.hpp +++ b/include/rendering/m2_renderer.hpp @@ -254,6 +254,7 @@ public: * Initialize shadow pipeline (Phase 7) */ bool initializeShadow(VkRenderPass shadowRenderPass); + bool hasShadowPipeline() const { return shadowPipeline_ != VK_NULL_HANDLE; } /** * Render depth-only pass for shadow casting diff --git a/src/core/application.cpp b/src/core/application.cpp index cf37e881..c2e85c44 100644 --- a/src/core/application.cpp +++ b/src/core/application.cpp @@ -1456,11 +1456,21 @@ void Application::setupUICallbacks() { return; } + // If a world load is already in progress (re-entrant call from + // gameHandler->update() processing SMSG_NEW_WORLD during warmup), + // defer this entry. The current load will pick it up when it finishes. + if (loadingWorld_) { + LOG_WARNING("World entry deferred: map ", mapId, " while loading (will process after current load)"); + pendingWorldEntry_ = {mapId, x, y, z}; + return; + } + worldEntryMovementGraceTimer_ = 2.0f; taxiLandingClampTimer_ = 0.0f; lastTaxiFlight_ = false; loadOnlineWorldTerrain(mapId, x, y, z); - loadedMapId_ = mapId; + // loadedMapId_ is set inside loadOnlineWorldTerrain (including + // any deferred entries it processes), so we must NOT override it here. }); auto sampleBestFloorAt = [this](float x, float y, float probeZ) -> std::optional { @@ -3160,6 +3170,11 @@ void Application::loadOnlineWorldTerrain(uint32_t mapId, float x, float y, float return; } + // Guard against re-entrant calls. The worldEntryCallback defers new + // entries while this flag is set; we process them at the end. + loadingWorld_ = true; + pendingWorldEntry_.reset(); + // --- Loading screen for online mode --- rendering::LoadingScreen loadingScreen; loadingScreen.setVkContext(window->getVkContext()); @@ -3196,43 +3211,44 @@ void Application::loadOnlineWorldTerrain(uint32_t mapId, float x, float y, float // --- Clean up previous map's state on map change --- // (Same cleanup as logout, but preserves player identity and renderer objects.) - if (loadedMapId_ != 0xFFFFFFFF) { - LOG_INFO("Map change: cleaning up old map ", loadedMapId_, " before loading map ", mapId); - - // Clear entity instances from old map - creatureInstances_.clear(); - creatureModelIds_.clear(); - creatureRenderPosCache_.clear(); - creatureWeaponsAttached_.clear(); - creatureWeaponAttachAttempts_.clear(); - deadCreatureGuids_.clear(); - nonRenderableCreatureDisplayIds_.clear(); - creaturePermanentFailureGuids_.clear(); + LOG_WARNING("loadOnlineWorldTerrain: mapId=", mapId, " loadedMapId_=", loadedMapId_); + bool hasRendererData = renderer && (renderer->getWMORenderer() || renderer->getM2Renderer()); + if (loadedMapId_ != 0xFFFFFFFF || hasRendererData) { + LOG_WARNING("Map change: cleaning up old map ", loadedMapId_, " before loading map ", mapId); + // Clear pending queues first (these don't touch GPU resources) pendingCreatureSpawns_.clear(); pendingCreatureSpawnGuids_.clear(); creatureSpawnRetryCounts_.clear(); - - playerInstances_.clear(); - onlinePlayerAppearance_.clear(); - pendingOnlinePlayerEquipment_.clear(); - deferredEquipmentQueue_.clear(); pendingPlayerSpawns_.clear(); pendingPlayerSpawnGuids_.clear(); - - gameObjectInstances_.clear(); + pendingOnlinePlayerEquipment_.clear(); + deferredEquipmentQueue_.clear(); pendingGameObjectSpawns_.clear(); pendingTransportMoves_.clear(); pendingTransportDoodadBatches_.clear(); if (renderer) { - // Clear all world geometry from old map (including textures/models) + // Clear all world geometry from old map (including textures/models). + // WMO clearAll and M2 clear both call vkDeviceWaitIdle internally, + // ensuring no GPU command buffers reference old resources. if (auto* wmo = renderer->getWMORenderer()) { wmo->clearAll(); } if (auto* m2 = renderer->getM2Renderer()) { m2->clear(); } + + // Full clear of character renderer: removes all instances, models, + // textures, and resets descriptor pools. This prevents stale GPU + // resources from accumulating across map changes (old creature + // models, bone buffers, texture descriptor sets) which can cause + // VK_ERROR_DEVICE_LOST on some drivers. + if (auto* cr = renderer->getCharacterRenderer()) { + cr->clear(); + renderer->setCharacterFollow(0); + } + if (auto* terrain = renderer->getTerrainManager()) { terrain->softReset(); terrain->setStreamingEnabled(true); // Re-enable in case previous map disabled it @@ -3243,6 +3259,22 @@ void Application::loadOnlineWorldTerrain(uint32_t mapId, float x, float y, float renderer->clearMount(); } + // Clear application-level instance tracking (after renderer cleanup) + creatureInstances_.clear(); + creatureModelIds_.clear(); + creatureRenderPosCache_.clear(); + creatureWeaponsAttached_.clear(); + creatureWeaponAttachAttempts_.clear(); + deadCreatureGuids_.clear(); + nonRenderableCreatureDisplayIds_.clear(); + creaturePermanentFailureGuids_.clear(); + + playerInstances_.clear(); + onlinePlayerAppearance_.clear(); + + gameObjectInstances_.clear(); + gameObjectDisplayIdModelCache_.clear(); + // Force player character re-spawn on new map playerCharacterSpawned = false; } @@ -3395,25 +3427,22 @@ void Application::loadOnlineWorldTerrain(uint32_t mapId, float x, float y, float LOG_WARNING("WMO-only map detected — loading root WMO: ", wdtInfo.rootWMOPath); showProgress("Loading instance geometry...", 0.25f); - // Still call loadTestTerrain with a dummy path to initialize all renderers - // (terrain, WMO, M2, character). The terrain load will fail gracefully. - auto [tileX, tileY] = core::coords::canonicalToTile(spawnCanonical.x, spawnCanonical.y); - std::string dummyAdtPath = "World\\Maps\\" + mapName + "\\" + mapName + "_" + - std::to_string(tileX) + "_" + std::to_string(tileY) + ".adt"; - LOG_WARNING("WMO-only: calling loadTestTerrain with dummy ADT: ", dummyAdtPath); - renderer->loadTestTerrain(assetManager.get(), dummyAdtPath); - LOG_WARNING("WMO-only: loadTestTerrain returned"); + // Initialize renderers if they don't exist yet (first login to a WMO-only map). + // On map change, renderers already exist from the previous map. + if (!renderer->getWMORenderer() || !renderer->getTerrainManager()) { + auto [tileX, tileY] = core::coords::canonicalToTile(spawnCanonical.x, spawnCanonical.y); + std::string dummyAdtPath = "World\\Maps\\" + mapName + "\\" + mapName + "_" + + std::to_string(tileX) + "_" + std::to_string(tileY) + ".adt"; + LOG_WARNING("WMO-only: calling loadTestTerrain to create renderers"); + renderer->loadTestTerrain(assetManager.get(), dummyAdtPath); + } - // Set map name on the newly-created WMO renderer (loadTestTerrain creates it) + // Set map name on WMO and terrain renderers if (renderer->getWMORenderer()) { renderer->getWMORenderer()->setMapName(mapName); } if (renderer->getTerrainManager()) { renderer->getTerrainManager()->setMapName(mapName); - } - - // Disable terrain streaming — no ADT tiles for WMO-only maps - if (renderer->getTerrainManager()) { renderer->getTerrainManager()->setStreamingEnabled(false); } @@ -3606,6 +3635,15 @@ void Application::loadOnlineWorldTerrain(uint32_t mapId, float x, float y, float LOG_INFO("Online world terrain loading initiated"); } + // Set map name on the newly-created WMO/terrain renderers + // (loadTestTerrain creates them, so the earlier setMapName at line ~3296 was a no-op) + if (renderer->getWMORenderer()) { + renderer->getWMORenderer()->setMapName(mapName); + } + if (renderer->getTerrainManager()) { + renderer->getTerrainManager()->setMapName(mapName); + } + // Character renderer is created inside loadTestTerrain(), so spawn the // player model now that the renderer actually exists. if (!playerCharacterSpawned) { @@ -3791,6 +3829,15 @@ void Application::loadOnlineWorldTerrain(uint32_t mapId, float x, float y, float // Drain network and process deferred spawn/composite queues while hidden. if (gameHandler) gameHandler->update(1.0f / 60.0f); + + // If a new world entry was deferred during packet processing, + // stop warming up this map — we'll load the new one after cleanup. + if (pendingWorldEntry_) { + LOG_WARNING("loadOnlineWorldTerrain(map ", mapId, + ") — deferred world entry pending, stopping warmup"); + break; + } + if (world) world->update(1.0f / 60.0f); processPlayerSpawnQueue(); processCreatureSpawnQueue(); @@ -3823,8 +3870,26 @@ void Application::loadOnlineWorldTerrain(uint32_t mapId, float x, float y, float loadingScreen.shutdown(); } + // Track which map we actually loaded (used by same-map teleport check). + loadedMapId_ = mapId; + // Set game state setState(AppState::IN_GAME); + + // Clear loading flag and process any deferred world entry. + // A deferred entry occurs when SMSG_NEW_WORLD arrived during our warmup + // (e.g., an area trigger in a dungeon immediately teleporting the player out). + loadingWorld_ = false; + if (pendingWorldEntry_) { + auto entry = *pendingWorldEntry_; + pendingWorldEntry_.reset(); + LOG_WARNING("Processing deferred world entry: map ", entry.mapId); + worldEntryMovementGraceTimer_ = 2.0f; + taxiLandingClampTimer_ = 0.0f; + lastTaxiFlight_ = false; + // Recursive call — sets loadedMapId_ to entry.mapId inside. + loadOnlineWorldTerrain(entry.mapId, entry.x, entry.y, entry.z); + } } void Application::buildCreatureDisplayLookups() { @@ -6181,7 +6246,15 @@ void Application::spawnOnlineGameObject(uint64_t guid, uint32_t entry, uint32_t auto itCache = gameObjectDisplayIdModelCache_.find(displayId); if (itCache != gameObjectDisplayIdModelCache_.end()) { modelId = itCache->second; - } else { + if (!m2Renderer->hasModel(modelId)) { + LOG_WARNING("GO M2 cache hit but model gone: displayId=", displayId, + " modelId=", modelId, " path=", modelPath, + " — reloading"); + gameObjectDisplayIdModelCache_.erase(itCache); + itCache = gameObjectDisplayIdModelCache_.end(); + } + } + if (itCache == gameObjectDisplayIdModelCache_.end()) { modelId = nextGameObjectModelId_++; auto m2Data = assetManager->readFile(modelPath); diff --git a/src/rendering/character_renderer.cpp b/src/rendering/character_renderer.cpp index bc2c77d6..ab0bbe78 100644 --- a/src/rendering/character_renderer.cpp +++ b/src/rendering/character_renderer.cpp @@ -383,6 +383,85 @@ void CharacterRenderer::shutdown() { vkCtx_ = nullptr; } +void CharacterRenderer::clear() { + if (!vkCtx_) return; + + LOG_INFO("CharacterRenderer::clear instances=", instances.size(), + " models=", models.size()); + + vkDeviceWaitIdle(vkCtx_->getDevice()); + VkDevice device = vkCtx_->getDevice(); + VmaAllocator alloc = vkCtx_->getAllocator(); + + // Destroy GPU resources for all models + for (auto& pair : models) { + destroyModelGPU(pair.second); + } + + // Destroy bone buffers for all instances + for (auto& pair : instances) { + destroyInstanceBones(pair.second); + } + + // Clear texture cache (VkTexture unique_ptrs auto-destroy) + textureCache.clear(); + textureHasAlphaByPtr_.clear(); + textureColorKeyBlackByPtr_.clear(); + textureCacheBytes_ = 0; + textureCacheCounter_ = 0; + loggedTextureLoadFails_.clear(); + + // Clear composite and failed caches + compositeCache_.clear(); + failedTextureCache_.clear(); + + // Recreate default textures (needed by loadModel/loadTexture fallbacks) + whiteTexture_.reset(); + transparentTexture_.reset(); + flatNormalTexture_.reset(); + { + uint8_t white[] = {255, 255, 255, 255}; + whiteTexture_ = std::make_unique(); + whiteTexture_->upload(*vkCtx_, white, 1, 1, VK_FORMAT_R8G8B8A8_UNORM, false); + whiteTexture_->createSampler(device, VK_FILTER_NEAREST, VK_FILTER_NEAREST, VK_SAMPLER_ADDRESS_MODE_REPEAT); + } + { + uint8_t transparent[] = {0, 0, 0, 0}; + transparentTexture_ = std::make_unique(); + transparentTexture_->upload(*vkCtx_, transparent, 1, 1, VK_FORMAT_R8G8B8A8_UNORM, false); + transparentTexture_->createSampler(device, VK_FILTER_NEAREST, VK_FILTER_NEAREST, VK_SAMPLER_ADDRESS_MODE_REPEAT); + } + { + uint8_t flatNormal[] = {128, 128, 255, 128}; + flatNormalTexture_ = std::make_unique(); + flatNormalTexture_->upload(*vkCtx_, flatNormal, 1, 1, VK_FORMAT_R8G8B8A8_UNORM, false); + flatNormalTexture_->createSampler(device, VK_FILTER_NEAREST, VK_FILTER_NEAREST, VK_SAMPLER_ADDRESS_MODE_REPEAT); + } + + models.clear(); + instances.clear(); + + // Release deferred transient material UBOs + for (int i = 0; i < 2; i++) { + for (const auto& b : transientMaterialUbos_[i]) { + if (b.first) { + vmaDestroyBuffer(alloc, b.first, b.second); + } + } + transientMaterialUbos_[i].clear(); + } + + // Reset descriptor pools (don't destroy — reuse for new allocations) + for (int i = 0; i < 2; i++) { + if (materialDescPools_[i]) { + vkResetDescriptorPool(device, materialDescPools_[i], 0); + } + } + if (boneDescPool_) { + vkResetDescriptorPool(device, boneDescPool_, 0); + } +} + void CharacterRenderer::destroyModelGPU(M2ModelGPU& gpuModel) { if (!vkCtx_) return; VmaAllocator alloc = vkCtx_->getAllocator(); diff --git a/src/rendering/m2_renderer.cpp b/src/rendering/m2_renderer.cpp index 23c1c6d9..1d32d0e4 100644 --- a/src/rendering/m2_renderer.cpp +++ b/src/rendering/m2_renderer.cpp @@ -3254,6 +3254,35 @@ void M2Renderer::clear() { for (auto& inst : instances) { destroyInstanceBones(inst); } + // Reset descriptor pools so new allocations succeed after reload. + // destroyModelGPU/destroyInstanceBones don't free individual sets, + // so the pools fill up across map changes without this reset. + VkDevice device = vkCtx_->getDevice(); + if (materialDescPool_) { + vkResetDescriptorPool(device, materialDescPool_, 0); + // Re-allocate the glow texture descriptor set (pre-allocated during init, + // invalidated by pool reset). + if (glowTexture_ && particleTexLayout_) { + VkDescriptorSetAllocateInfo ai{VK_STRUCTURE_TYPE_DESCRIPTOR_SET_ALLOCATE_INFO}; + ai.descriptorPool = materialDescPool_; + ai.descriptorSetCount = 1; + ai.pSetLayouts = &particleTexLayout_; + glowTexDescSet_ = VK_NULL_HANDLE; + if (vkAllocateDescriptorSets(device, &ai, &glowTexDescSet_) == VK_SUCCESS) { + VkDescriptorImageInfo imgInfo = glowTexture_->descriptorInfo(); + VkWriteDescriptorSet write{VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET}; + write.dstSet = glowTexDescSet_; + write.dstBinding = 0; + write.descriptorCount = 1; + write.descriptorType = VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER; + write.pImageInfo = &imgInfo; + vkUpdateDescriptorSets(device, 1, &write, 0, nullptr); + } + } + } + if (boneDescPool_) { + vkResetDescriptorPool(device, boneDescPool_, 0); + } } models.clear(); instances.clear(); diff --git a/src/rendering/renderer.cpp b/src/rendering/renderer.cpp index cf4500b7..85ce1788 100644 --- a/src/rendering/renderer.cpp +++ b/src/rendering/renderer.cpp @@ -3165,6 +3165,10 @@ void Renderer::renderOverlay(const glm::vec4& color) { void Renderer::renderWorld(game::World* world, game::GameHandler* gameHandler) { (void)world; + // GPU crash diagnostic: skip ALL world rendering to isolate crash source + static const bool skipAll = (std::getenv("WOWEE_SKIP_ALL_RENDER") != nullptr); + if (skipAll) return; + auto renderStart = std::chrono::steady_clock::now(); lastTerrainRenderMs = 0.0; lastWMORenderMs = 0.0; @@ -3208,6 +3212,11 @@ void Renderer::renderWorld(game::World* world, game::GameHandler* gameHandler) { skySystem->render(currentCmd, perFrameSet, *camera, skyParams); } + // GPU crash diagnostic: skip individual renderers to isolate which one faults + static const bool skipWMO = (std::getenv("WOWEE_SKIP_WMO") != nullptr); + static const bool skipChars = (std::getenv("WOWEE_SKIP_CHARS") != nullptr); + static const bool skipM2 = (std::getenv("WOWEE_SKIP_M2") != nullptr); + // Terrain (opaque pass) if (terrainRenderer && camera && terrainEnabled) { auto terrainStart = std::chrono::steady_clock::now(); @@ -3217,7 +3226,7 @@ void Renderer::renderWorld(game::World* world, game::GameHandler* gameHandler) { } // WMO buildings (opaque, drawn before characters so selection circle sits on top) - if (wmoRenderer && camera) { + if (wmoRenderer && camera && !skipWMO) { auto wmoStart = std::chrono::steady_clock::now(); wmoRenderer->render(currentCmd, perFrameSet, *camera); lastWMORenderMs = std::chrono::duration( @@ -3228,12 +3237,12 @@ void Renderer::renderWorld(game::World* world, game::GameHandler* gameHandler) { renderSelectionCircle(view, projection); // Characters (after selection circle so units draw over the ring) - if (characterRenderer && camera) { + if (characterRenderer && camera && !skipChars) { characterRenderer->render(currentCmd, perFrameSet, *camera); } // M2 doodads, creatures, glow sprites, particles - if (m2Renderer && camera) { + if (m2Renderer && camera && !skipM2) { if (cameraController) { m2Renderer->setInsideInterior(cameraController->isInsideWMO()); m2Renderer->setOnTaxi(cameraController->isOnTaxi()); @@ -3393,21 +3402,21 @@ bool Renderer::loadTestTerrain(pipeline::AssetManager* assetManager, const std:: if (!wmoRenderer) { wmoRenderer = std::make_unique(); wmoRenderer->initialize(vkCtx, perFrameSetLayout, assetManager); + if (shadowRenderPass != VK_NULL_HANDLE) { + wmoRenderer->initializeShadow(shadowRenderPass); + } } - // Initialize shadow pipelines (Phase 7/8) - if (wmoRenderer && shadowRenderPass != VK_NULL_HANDLE) { - wmoRenderer->initializeShadow(shadowRenderPass); - } - if (m2Renderer && shadowRenderPass != VK_NULL_HANDLE) { + // Initialize shadow pipelines for M2 if not yet done + if (m2Renderer && shadowRenderPass != VK_NULL_HANDLE && !m2Renderer->hasShadowPipeline()) { m2Renderer->initializeShadow(shadowRenderPass); } if (!characterRenderer) { characterRenderer = std::make_unique(); characterRenderer->initialize(vkCtx, perFrameSetLayout, assetManager); - } - if (characterRenderer && shadowRenderPass != VK_NULL_HANDLE) { - characterRenderer->initializeShadow(shadowRenderPass); + if (shadowRenderPass != VK_NULL_HANDLE) { + characterRenderer->initializeShadow(shadowRenderPass); + } } // Create and initialize terrain manager @@ -3862,6 +3871,8 @@ void Renderer::renderReflectionPass() { } void Renderer::renderShadowPass() { + static const bool skipShadows = (std::getenv("WOWEE_SKIP_SHADOWS") != nullptr); + if (skipShadows) return; if (!shadowsEnabled || shadowDepthImage == VK_NULL_HANDLE) return; if (currentCmd == VK_NULL_HANDLE) return; diff --git a/src/rendering/swim_effects.cpp b/src/rendering/swim_effects.cpp index 06cc82f5..9a7ad119 100644 --- a/src/rendering/swim_effects.cpp +++ b/src/rendering/swim_effects.cpp @@ -557,7 +557,9 @@ void SwimEffects::update(const Camera& camera, const CameraController& cc, } // --- Bubble spawning --- - bool underwater = camWaterH && camPos.z < *camWaterH; + // Require swimming state to prevent spurious bubbles on login/teleport + // when camera may briefly appear below a water surface before grounding. + bool underwater = swimming && camWaterH && camPos.z < *camWaterH; if (underwater) { float bubbleRate = 20.0f; bubbleSpawnAccum += bubbleRate * deltaTime; diff --git a/src/rendering/wmo_renderer.cpp b/src/rendering/wmo_renderer.cpp index 0374c96d..e05b17e8 100644 --- a/src/rendering/wmo_renderer.cpp +++ b/src/rendering/wmo_renderer.cpp @@ -1059,6 +1059,11 @@ void WMORenderer::clearAll() { if (entry.texture) entry.texture->destroy(device, allocator); if (entry.normalHeightMap) entry.normalHeightMap->destroy(device, allocator); } + + // Reset descriptor pool so new allocations succeed after reload + if (materialDescPool_) { + vkResetDescriptorPool(device, materialDescPool_, 0); + } } loadedModels.clear();