diff --git a/include/rendering/terrain_manager.hpp b/include/rendering/terrain_manager.hpp index 46a39962..7d4d21b7 100644 --- a/include/rendering/terrain_manager.hpp +++ b/include/rendering/terrain_manager.hpp @@ -123,41 +123,6 @@ struct PendingTile { std::unordered_map preloadedTextures; }; -/** - * Phases for incremental tile finalization (one bounded unit of work per call) - */ -enum class FinalizationPhase { - TERRAIN, // Upload terrain mesh + textures - M2_MODELS, // Upload ONE M2 model per call - M2_INSTANCES, // Create all M2 instances (lightweight struct allocation) - WMO_MODELS, // Upload ONE WMO model per call - WMO_INSTANCES, // Create all WMO instances + load WMO liquids - WMO_DOODADS, // Upload ONE WMO doodad M2 per call - WATER, // Upload water from terrain - AMBIENT, // Register ambient emitters - DONE // Commit to loadedTiles -}; - -/** - * In-progress tile finalization state — tracks progress across frames - */ -struct FinalizingTile { - std::shared_ptr pending; - FinalizationPhase phase = FinalizationPhase::TERRAIN; - - // Progress indices within current phase - size_t m2ModelIndex = 0; // Next M2 model to upload - size_t wmoModelIndex = 0; // Next WMO model to upload - size_t wmoDoodadIndex = 0; // Next WMO doodad to upload - - // Accumulated results (built up across phases) - std::vector m2InstanceIds; - std::vector wmoInstanceIds; - std::vector tileUniqueIds; - std::vector tileWmoUniqueIds; - std::unordered_set uploadedM2ModelIds; -}; - /** * Terrain manager for multi-tile terrain streaming * @@ -254,8 +219,8 @@ public: int getLoadedTileCount() const { return static_cast(loadedTiles.size()); } int getPendingTileCount() const { return static_cast(pendingTiles.size()); } int getReadyQueueCount() const { return static_cast(readyQueue.size()); } - /** Total unfinished tiles (worker threads + ready queue + finalizing) */ - int getRemainingTileCount() const { return static_cast(pendingTiles.size() + readyQueue.size() + finalizingTiles_.size()); } + /** Total unfinished tiles (worker threads + ready queue) */ + int getRemainingTileCount() const { return static_cast(pendingTiles.size() + readyQueue.size()); } TileCoord getCurrentTile() const { return currentTile; } /** Process all ready tiles immediately (use during loading screens) */ @@ -289,10 +254,9 @@ private: std::shared_ptr prepareTile(int x, int y); /** - * Advance incremental finalization of a tile (one bounded unit of work). - * Returns true when the tile is fully finalized (phase == DONE). + * Main thread: upload prepared tile data to GPU */ - bool advanceFinalization(FinalizingTile& ft); + void finalizeTile(const std::shared_ptr& pending); /** * Background worker thread loop @@ -377,8 +341,16 @@ private: // Dedup set for WMO placements across tile boundaries (prevents rendering Stormwind 16x) std::unordered_set placedWmoIds; - // Tiles currently being incrementally finalized across frames - std::deque finalizingTiles_; + // Progressive M2 upload queue (spread heavy uploads across frames) + struct PendingM2Upload { + uint32_t modelId; + pipeline::M2Model model; + std::string path; + }; + std::queue m2UploadQueue_; + static constexpr int MAX_M2_UPLOADS_PER_FRAME = 5; // Upload up to 5 models per frame + + void processM2UploadQueue(); struct GroundEffectEntry { std::array doodadIds{{0, 0, 0, 0}}; diff --git a/src/rendering/terrain_manager.cpp b/src/rendering/terrain_manager.cpp index be2e36f0..da92981a 100644 --- a/src/rendering/terrain_manager.cpp +++ b/src/rendering/terrain_manager.cpp @@ -226,9 +226,7 @@ bool TerrainManager::loadTile(int x, int y) { return false; } - FinalizingTile ft; - ft.pending = std::move(pending); - while (!advanceFinalization(ft)) {} + finalizeTile(pending); return true; } @@ -650,160 +648,176 @@ void TerrainManager::logMissingAdtOnce(const std::string& adtPath) { } } -bool TerrainManager::advanceFinalization(FinalizingTile& ft) { - auto& pending = ft.pending; +void TerrainManager::finalizeTile(const std::shared_ptr& pending) { int x = pending->coord.x; int y = pending->coord.y; TileCoord coord = pending->coord; - switch (ft.phase) { + LOG_DEBUG("Finalizing tile [", x, ",", y, "] (GPU upload)"); - case FinalizationPhase::TERRAIN: { - // Check if tile was already loaded or failed - if (loadedTiles.find(coord) != loadedTiles.end() || failedTiles.find(coord) != failedTiles.end()) { - { - std::lock_guard lock(queueMutex); - pendingTiles.erase(coord); - } - ft.phase = FinalizationPhase::DONE; - return true; - } - - LOG_DEBUG("Finalizing tile [", x, ",", y, "] (incremental)"); - - // Upload pre-loaded textures - if (!pending->preloadedTextures.empty()) { - terrainRenderer->uploadPreloadedTextures(pending->preloadedTextures); - } - - // Upload terrain mesh to GPU - if (!terrainRenderer->loadTerrain(pending->mesh, pending->terrain.textures, x, y)) { - LOG_ERROR("Failed to upload terrain to GPU for tile [", x, ",", y, "]"); - failedTiles[coord] = true; - { - std::lock_guard lock(queueMutex); - pendingTiles.erase(coord); - } - ft.phase = FinalizationPhase::DONE; - return true; - } - - // Load water immediately after terrain (same frame) — water vertex positions - // depend on terrain chunk data and must be uploaded before the terrain renders - // without water, otherwise vertices appear at origin. - if (waterRenderer) { - waterRenderer->loadFromTerrain(pending->terrain, true, x, y); - } - - // Ensure M2 renderer has asset manager - if (m2Renderer && assetManager) { - m2Renderer->initialize(nullptr, VK_NULL_HANDLE, assetManager); - } - - ft.phase = FinalizationPhase::M2_MODELS; - return false; + // Check if tile was already loaded (race condition guard) or failed + if (loadedTiles.find(coord) != loadedTiles.end()) { + return; + } + if (failedTiles.find(coord) != failedTiles.end()) { + return; } - case FinalizationPhase::M2_MODELS: { - // Upload ONE M2 model per call - if (m2Renderer && ft.m2ModelIndex < pending->m2Models.size()) { - auto& m2Ready = pending->m2Models[ft.m2ModelIndex]; - if (m2Renderer->loadModel(m2Ready.model, m2Ready.modelId)) { - ft.uploadedM2ModelIds.insert(m2Ready.modelId); - } - ft.m2ModelIndex++; - // Stay in this phase until all models uploaded - if (ft.m2ModelIndex < pending->m2Models.size()) { - return false; - } - } - if (!ft.uploadedM2ModelIds.empty()) { - LOG_DEBUG(" Uploaded ", ft.uploadedM2ModelIds.size(), " M2 models for tile [", x, ",", y, "]"); - } - ft.phase = FinalizationPhase::M2_INSTANCES; - return false; + // Upload pre-loaded textures to the GL cache so loadTerrain avoids file I/O + if (!pending->preloadedTextures.empty()) { + terrainRenderer->uploadPreloadedTextures(pending->preloadedTextures); } - case FinalizationPhase::M2_INSTANCES: { - // Create all M2 instances (lightweight struct allocation, no GPU work) - if (m2Renderer) { - int loadedDoodads = 0; - int skippedDedup = 0; - for (const auto& p : pending->m2Placements) { - if (p.uniqueId != 0 && placedDoodadIds.count(p.uniqueId)) { - skippedDedup++; - continue; - } - uint32_t instId = m2Renderer->createInstance(p.modelId, p.position, p.rotation, p.scale); - if (instId) { - ft.m2InstanceIds.push_back(instId); - if (p.uniqueId != 0) { - placedDoodadIds.insert(p.uniqueId); - ft.tileUniqueIds.push_back(p.uniqueId); + // Upload terrain to GPU + if (!terrainRenderer->loadTerrain(pending->mesh, pending->terrain.textures, x, y)) { + LOG_ERROR("Failed to upload terrain to GPU for tile [", x, ",", y, "]"); + failedTiles[coord] = true; + return; + } + + // Load water + if (waterRenderer) { + waterRenderer->loadFromTerrain(pending->terrain, true, x, y); + } + + // Register water surface ambient sound emitters + if (ambientSoundManager) { + // Scan ADT water data for water surfaces + int waterEmitterCount = 0; + for (size_t chunkIdx = 0; chunkIdx < pending->terrain.waterData.size(); chunkIdx++) { + const auto& chunkWater = pending->terrain.waterData[chunkIdx]; + if (!chunkWater.hasWater()) continue; + + // Calculate chunk position in world coordinates + int chunkX = chunkIdx % 16; + int chunkY = chunkIdx / 16; + + // WoW coordinates: Each ADT tile is 533.33 units, each chunk is 533.33/16 = 33.333 units + // Tile origin in GL space + float tileOriginX = (32.0f - x) * 533.33333f; + float tileOriginY = (32.0f - y) * 533.33333f; + + // Chunk center position + float chunkCenterX = tileOriginX + (chunkX + 0.5f) * 33.333333f; + float chunkCenterY = tileOriginY + (chunkY + 0.5f) * 33.333333f; + + // Use first layer for height and type detection + if (!chunkWater.layers.empty()) { + const auto& layer = chunkWater.layers[0]; + float waterHeight = layer.minHeight; + + // Determine water type and register appropriate emitter + // liquidType: 0=water/lake, 1=ocean, 2=magma, 3=slime + if (layer.liquidType == 0) { + // Lake/river water - add water surface emitter every 32 chunks to avoid spam + if (chunkIdx % 32 == 0) { + PendingTile::AmbientEmitter emitter; + emitter.position = glm::vec3(chunkCenterX, chunkCenterY, waterHeight); + emitter.type = 4; // WATER_SURFACE + pending->ambientEmitters.push_back(emitter); + waterEmitterCount++; + } + } else if (layer.liquidType == 1) { + // Ocean - add ocean emitter every 64 chunks (oceans are very large) + if (chunkIdx % 64 == 0) { + PendingTile::AmbientEmitter emitter; + emitter.position = glm::vec3(chunkCenterX, chunkCenterY, waterHeight); + emitter.type = 4; // WATER_SURFACE (could add separate OCEAN type later) + pending->ambientEmitters.push_back(emitter); + waterEmitterCount++; } - loadedDoodads++; - } - } - LOG_DEBUG(" Loaded doodads for tile [", x, ",", y, "]: ", - loadedDoodads, " instances (", ft.uploadedM2ModelIds.size(), " new models, ", - skippedDedup, " dedup skipped)"); - } - ft.phase = FinalizationPhase::WMO_MODELS; - return false; - } - - case FinalizationPhase::WMO_MODELS: { - // Upload ONE WMO model per call - if (wmoRenderer && assetManager) { - wmoRenderer->initialize(nullptr, VK_NULL_HANDLE, assetManager); - - if (ft.wmoModelIndex < pending->wmoModels.size()) { - auto& wmoReady = pending->wmoModels[ft.wmoModelIndex]; - // Deduplicate - if (wmoReady.uniqueId != 0 && placedWmoIds.count(wmoReady.uniqueId)) { - // Skip this one, advance - ft.wmoModelIndex++; - if (ft.wmoModelIndex < pending->wmoModels.size()) return false; - } else { - wmoRenderer->loadModel(wmoReady.model, wmoReady.modelId); - ft.wmoModelIndex++; - if (ft.wmoModelIndex < pending->wmoModels.size()) return false; } + // Skip magma and slime for now (no ambient sounds for those) } } - ft.wmoModelIndex = 0; // Reset for WMO_INSTANCES phase iteration - ft.phase = FinalizationPhase::WMO_INSTANCES; - return false; + if (waterEmitterCount > 0) { + } } - case FinalizationPhase::WMO_INSTANCES: { - // Create all WMO instances + load WMO liquids - if (wmoRenderer) { - int loadedWMOs = 0; - int loadedLiquids = 0; - int skippedWmoDedup = 0; - for (auto& wmoReady : pending->wmoModels) { - if (wmoReady.uniqueId != 0 && placedWmoIds.count(wmoReady.uniqueId)) { - skippedWmoDedup++; - continue; - } + std::vector m2InstanceIds; + std::vector wmoInstanceIds; + std::vector tileUniqueIds; + std::vector tileWmoUniqueIds; + // Upload M2 models to GPU and create instances + if (m2Renderer && assetManager) { + // Always pass the latest asset manager. initialize() is idempotent and updates + // the pointer even when the renderer was initialized earlier without assets. + m2Renderer->initialize(nullptr, VK_NULL_HANDLE, assetManager); + + // Upload M2 models immediately (batching was causing hangs) + // The 5ms time budget in processReadyTiles() limits the spike + std::unordered_set uploadedModelIds; + for (auto& m2Ready : pending->m2Models) { + if (m2Renderer->loadModel(m2Ready.model, m2Ready.modelId)) { + uploadedModelIds.insert(m2Ready.modelId); + } + } + if (!uploadedModelIds.empty()) { + LOG_DEBUG(" Uploaded ", uploadedModelIds.size(), " M2 models for tile [", x, ",", y, "]"); + } + + // Create instances (deduplicate by uniqueId across tile boundaries) + int loadedDoodads = 0; + int skippedDedup = 0; + for (const auto& p : pending->m2Placements) { + // Skip if this doodad was already placed by a neighboring tile + if (p.uniqueId != 0 && placedDoodadIds.count(p.uniqueId)) { + skippedDedup++; + continue; + } + uint32_t instId = m2Renderer->createInstance(p.modelId, p.position, p.rotation, p.scale); + if (instId) { + m2InstanceIds.push_back(instId); + if (p.uniqueId != 0) { + placedDoodadIds.insert(p.uniqueId); + tileUniqueIds.push_back(p.uniqueId); + } + loadedDoodads++; + } + } + + LOG_DEBUG(" Loaded doodads for tile [", x, ",", y, "]: ", + loadedDoodads, " instances (", uploadedModelIds.size(), " new models, ", + skippedDedup, " dedup skipped)"); + } + + // Upload WMO models to GPU and create instances + if (wmoRenderer && assetManager) { + // WMORenderer may be initialized before assets are ready; always re-pass assets. + wmoRenderer->initialize(nullptr, VK_NULL_HANDLE, assetManager); + + int loadedWMOs = 0; + int loadedLiquids = 0; + int skippedWmoDedup = 0; + for (auto& wmoReady : pending->wmoModels) { + // Deduplicate by placement uniqueId when available. + // Some ADTs use uniqueId=0, which is not safe for dedup. + if (wmoReady.uniqueId != 0 && placedWmoIds.count(wmoReady.uniqueId)) { + skippedWmoDedup++; + continue; + } + + if (wmoRenderer->loadModel(wmoReady.model, wmoReady.modelId)) { uint32_t wmoInstId = wmoRenderer->createInstance(wmoReady.modelId, wmoReady.position, wmoReady.rotation); if (wmoInstId) { - ft.wmoInstanceIds.push_back(wmoInstId); + wmoInstanceIds.push_back(wmoInstId); if (wmoReady.uniqueId != 0) { placedWmoIds.insert(wmoReady.uniqueId); - ft.tileWmoUniqueIds.push_back(wmoReady.uniqueId); + tileWmoUniqueIds.push_back(wmoReady.uniqueId); } loadedWMOs++; // Load WMO liquids (canals, pools, etc.) if (waterRenderer) { + // Compute the same model matrix as WMORenderer uses glm::mat4 modelMatrix = glm::mat4(1.0f); modelMatrix = glm::translate(modelMatrix, wmoReady.position); modelMatrix = glm::rotate(modelMatrix, wmoReady.rotation.z, glm::vec3(0.0f, 0.0f, 1.0f)); modelMatrix = glm::rotate(modelMatrix, wmoReady.rotation.y, glm::vec3(0.0f, 1.0f, 0.0f)); modelMatrix = glm::rotate(modelMatrix, wmoReady.rotation.x, glm::vec3(1.0f, 0.0f, 0.0f)); + + // Load liquids from each WMO group for (const auto& group : wmoReady.model.groups) { if (group.liquid.hasLiquid()) { waterRenderer->loadFromWMO(group.liquid, modelMatrix, wmoInstId); @@ -813,109 +827,57 @@ bool TerrainManager::advanceFinalization(FinalizingTile& ft) { } } } - if (loadedWMOs > 0 || skippedWmoDedup > 0) { - LOG_DEBUG(" Loaded WMOs for tile [", x, ",", y, "]: ", - loadedWMOs, " instances, ", skippedWmoDedup, " dedup skipped"); - } - if (loadedLiquids > 0) { - LOG_DEBUG(" Loaded WMO liquids for tile [", x, ",", y, "]: ", loadedLiquids); - } } - ft.phase = FinalizationPhase::WMO_DOODADS; - return false; - } - - case FinalizationPhase::WMO_DOODADS: { - // Upload ONE WMO doodad M2 per call - if (m2Renderer && ft.wmoDoodadIndex < pending->wmoDoodads.size()) { - auto& doodad = pending->wmoDoodads[ft.wmoDoodadIndex]; - m2Renderer->loadModel(doodad.model, doodad.modelId); - uint32_t wmoDoodadInstId = m2Renderer->createInstanceWithMatrix( - doodad.modelId, doodad.modelMatrix, doodad.worldPosition); - if (wmoDoodadInstId) ft.m2InstanceIds.push_back(wmoDoodadInstId); - ft.wmoDoodadIndex++; - if (ft.wmoDoodadIndex < pending->wmoDoodads.size()) return false; + if (loadedWMOs > 0 || skippedWmoDedup > 0) { + LOG_DEBUG(" Loaded WMOs for tile [", x, ",", y, "]: ", + loadedWMOs, " instances, ", skippedWmoDedup, " dedup skipped"); + } + if (loadedLiquids > 0) { + LOG_DEBUG(" Loaded WMO liquids for tile [", x, ",", y, "]: ", loadedLiquids); } - ft.phase = FinalizationPhase::WATER; - return false; - } - case FinalizationPhase::WATER: { - // Terrain water was already loaded in TERRAIN phase. - // Generate water ambient emitters here. - if (ambientSoundManager) { - for (size_t chunkIdx = 0; chunkIdx < pending->terrain.waterData.size(); chunkIdx++) { - const auto& chunkWater = pending->terrain.waterData[chunkIdx]; - if (!chunkWater.hasWater()) continue; - - int chunkX = chunkIdx % 16; - int chunkY = chunkIdx / 16; - float tileOriginX = (32.0f - x) * 533.33333f; - float tileOriginY = (32.0f - y) * 533.33333f; - float chunkCenterX = tileOriginX + (chunkX + 0.5f) * 33.333333f; - float chunkCenterY = tileOriginY + (chunkY + 0.5f) * 33.333333f; - - if (!chunkWater.layers.empty()) { - const auto& layer = chunkWater.layers[0]; - float waterHeight = layer.minHeight; - if (layer.liquidType == 0 && chunkIdx % 32 == 0) { - PendingTile::AmbientEmitter emitter; - emitter.position = glm::vec3(chunkCenterX, chunkCenterY, waterHeight); - emitter.type = 4; - pending->ambientEmitters.push_back(emitter); - } else if (layer.liquidType == 1 && chunkIdx % 64 == 0) { - PendingTile::AmbientEmitter emitter; - emitter.position = glm::vec3(chunkCenterX, chunkCenterY, waterHeight); - emitter.type = 4; - pending->ambientEmitters.push_back(emitter); - } - } + // Upload WMO doodad M2 models + if (m2Renderer) { + for (auto& doodad : pending->wmoDoodads) { + m2Renderer->loadModel(doodad.model, doodad.modelId); + uint32_t wmoDoodadInstId = m2Renderer->createInstanceWithMatrix( + doodad.modelId, doodad.modelMatrix, doodad.worldPosition); + if (wmoDoodadInstId) m2InstanceIds.push_back(wmoDoodadInstId); } } - ft.phase = FinalizationPhase::AMBIENT; - return false; - } - - case FinalizationPhase::AMBIENT: { - // Register ambient sound emitters - if (ambientSoundManager && !pending->ambientEmitters.empty()) { - for (const auto& emitter : pending->ambientEmitters) { - auto type = static_cast(emitter.type); - ambientSoundManager->addEmitter(emitter.position, type); - } + if (loadedWMOs > 0) { + LOG_DEBUG(" Loaded WMOs for tile [", x, ",", y, "]: ", loadedWMOs); } + } - // Commit tile to loadedTiles - auto tile = std::make_unique(); - tile->coord = coord; - tile->terrain = std::move(pending->terrain); - tile->mesh = std::move(pending->mesh); - tile->loaded = true; - tile->m2InstanceIds = std::move(ft.m2InstanceIds); - tile->wmoInstanceIds = std::move(ft.wmoInstanceIds); - tile->wmoUniqueIds = std::move(ft.tileWmoUniqueIds); - tile->doodadUniqueIds = std::move(ft.tileUniqueIds); - getTileBounds(coord, tile->minX, tile->minY, tile->maxX, tile->maxY); - loadedTiles[coord] = std::move(tile); - putCachedTile(pending); - - // Now safe to remove from pendingTiles (tile is in loadedTiles) - { - std::lock_guard lock(queueMutex); - pendingTiles.erase(coord); + // Register ambient sound emitters with ambient sound manager + if (ambientSoundManager && !pending->ambientEmitters.empty()) { + for (const auto& emitter : pending->ambientEmitters) { + // Cast uint32_t type to AmbientSoundManager::AmbientType enum + auto type = static_cast(emitter.type); + ambientSoundManager->addEmitter(emitter.position, type); } - - LOG_DEBUG(" Finalized tile [", x, ",", y, "]"); - - ft.phase = FinalizationPhase::DONE; - return true; } - case FinalizationPhase::DONE: - return true; - } - return true; + // Create tile entry + auto tile = std::make_unique(); + tile->coord = coord; + tile->terrain = std::move(pending->terrain); + tile->mesh = std::move(pending->mesh); + tile->loaded = true; + tile->m2InstanceIds = std::move(m2InstanceIds); + tile->wmoInstanceIds = std::move(wmoInstanceIds); + tile->wmoUniqueIds = std::move(tileWmoUniqueIds); + tile->doodadUniqueIds = std::move(tileUniqueIds); + + // Calculate world bounds + getTileBounds(coord, tile->minX, tile->minY, tile->maxX, tile->maxY); + + loadedTiles[coord] = std::move(tile); + putCachedTile(pending); + + LOG_DEBUG(" Finalized tile [", x, ",", y, "]"); } void TerrainManager::workerLoop() { @@ -965,59 +927,80 @@ void TerrainManager::processReadyTiles() { // Taxi mode gets a slightly larger budget to avoid visible late-pop terrain/models. const float timeBudgetMs = taxiStreamingMode_ ? 8.0f : 5.0f; auto startTime = std::chrono::high_resolution_clock::now(); + int processed = 0; - // Move newly ready tiles into the finalizing deque. - // Keep them in pendingTiles so streamTiles() won't re-enqueue them. - { - std::lock_guard lock(queueMutex); - while (!readyQueue.empty()) { - auto pending = readyQueue.front(); - readyQueue.pop(); - if (pending) { - FinalizingTile ft; - ft.pending = std::move(pending); - finalizingTiles_.push_back(std::move(ft)); + while (true) { + std::shared_ptr pending; + + { + std::lock_guard lock(queueMutex); + if (readyQueue.empty()) { + break; } + pending = readyQueue.front(); + readyQueue.pop(); } - } - // Finalize one complete tile per iteration, check budget between tiles. - // Each tile runs through all phases before moving to the next — this - // avoids subtle state issues from spreading GPU uploads across frames. - while (!finalizingTiles_.empty()) { - auto& ft = finalizingTiles_.front(); - while (!advanceFinalization(ft)) {} - finalizingTiles_.pop_front(); + if (pending) { + TileCoord coord = pending->coord; - auto now = std::chrono::high_resolution_clock::now(); - float elapsedMs = std::chrono::duration(now - startTime).count(); - if (elapsedMs >= timeBudgetMs) { - break; + finalizeTile(pending); + + auto now = std::chrono::high_resolution_clock::now(); + + { + std::lock_guard lock(queueMutex); + pendingTiles.erase(coord); + } + processed++; + + // Check if we've exceeded time budget + float elapsedMs = std::chrono::duration(now - startTime).count(); + if (elapsedMs >= timeBudgetMs) { + if (processed > 1) { + LOG_DEBUG("Processed ", processed, " tiles in ", elapsedMs, "ms (budget: ", timeBudgetMs, "ms)"); + } + break; + } } } } +void TerrainManager::processM2UploadQueue() { + // Upload up to MAX_M2_UPLOADS_PER_FRAME models per frame + int uploaded = 0; + while (!m2UploadQueue_.empty() && uploaded < MAX_M2_UPLOADS_PER_FRAME) { + auto& upload = m2UploadQueue_.front(); + if (m2Renderer) { + m2Renderer->loadModel(upload.model, upload.modelId); + } + m2UploadQueue_.pop(); + uploaded++; + } + + if (uploaded > 0) { + LOG_DEBUG("Uploaded ", uploaded, " M2 models (", m2UploadQueue_.size(), " remaining in queue)"); + } +} + void TerrainManager::processAllReadyTiles() { - // Move all ready tiles into finalizing deque - // Keep in pendingTiles until committed (same as processReadyTiles) - { - std::lock_guard lock(queueMutex); - while (!readyQueue.empty()) { - auto pending = readyQueue.front(); + while (true) { + std::shared_ptr pending; + { + std::lock_guard lock(queueMutex); + if (readyQueue.empty()) break; + pending = readyQueue.front(); readyQueue.pop(); - if (pending) { - FinalizingTile ft; - ft.pending = std::move(pending); - finalizingTiles_.push_back(std::move(ft)); + } + if (pending) { + TileCoord coord = pending->coord; + finalizeTile(pending); + { + std::lock_guard lock(queueMutex); + pendingTiles.erase(coord); } } } - // Finalize all tiles completely (no time budget — used for loading screens) - while (!finalizingTiles_.empty()) { - auto& ft = finalizingTiles_.front(); - while (!advanceFinalization(ft)) {} - finalizingTiles_.pop_front(); - } } std::shared_ptr TerrainManager::getCachedTile(const TileCoord& coord) { @@ -1116,31 +1099,6 @@ void TerrainManager::unloadTile(int x, int y) { pendingTiles.erase(coord); } - // Remove from finalizingTiles_ if it's being incrementally finalized. - // Water may have already been loaded in TERRAIN phase, so clean it up. - for (auto fit = finalizingTiles_.begin(); fit != finalizingTiles_.end(); ++fit) { - if (fit->pending && fit->pending->coord == coord) { - // If past TERRAIN phase, water was already loaded — remove it - if (fit->phase != FinalizationPhase::TERRAIN && waterRenderer) { - waterRenderer->removeTile(x, y); - } - // Clean up any M2/WMO instances that were already created - if (m2Renderer && !fit->m2InstanceIds.empty()) { - m2Renderer->removeInstances(fit->m2InstanceIds); - } - if (wmoRenderer && !fit->wmoInstanceIds.empty()) { - for (uint32_t id : fit->wmoInstanceIds) { - if (waterRenderer) waterRenderer->removeWMO(id); - } - wmoRenderer->removeInstances(fit->wmoInstanceIds); - } - for (uint32_t uid : fit->tileUniqueIds) placedDoodadIds.erase(uid); - for (uint32_t uid : fit->tileWmoUniqueIds) placedWmoIds.erase(uid); - finalizingTiles_.erase(fit); - return; - } - } - auto it = loadedTiles.find(coord); if (it == loadedTiles.end()) { return; @@ -1209,7 +1167,6 @@ void TerrainManager::unloadAll() { while (!readyQueue.empty()) readyQueue.pop(); } pendingTiles.clear(); - finalizingTiles_.clear(); placedDoodadIds.clear(); LOG_INFO("Unloading all terrain tiles");