diff --git a/src/rendering/m2_renderer.cpp b/src/rendering/m2_renderer.cpp index cd080a22..273def3a 100644 --- a/src/rendering/m2_renderer.cpp +++ b/src/rendering/m2_renderer.cpp @@ -1318,9 +1318,11 @@ void M2Renderer::update(float deltaTime, const glm::vec3& cameraPos, const glm:: if (animCount < 32 || numAnimThreads_ <= 1) { // Sequential — not enough work to justify thread overhead for (size_t i : boneWorkIndices) { + if (i >= instances.size()) continue; auto& inst = instances[i]; - const auto& mdl = models.find(inst.modelId)->second; - computeBoneMatrices(mdl, inst); + auto mdlIt = models.find(inst.modelId); + if (mdlIt == models.end()) continue; + computeBoneMatrices(mdlIt->second, inst); } } else { // Parallel — dispatch across worker threads @@ -1338,9 +1340,11 @@ void M2Renderer::update(float deltaTime, const glm::vec3& cameraPos, const glm:: [this, &boneWorkIndices, start, end]() { for (size_t j = start; j < end; ++j) { size_t idx = boneWorkIndices[j]; + if (idx >= instances.size()) continue; auto& inst = instances[idx]; - const auto& mdl = models.find(inst.modelId)->second; - computeBoneMatrices(mdl, inst); + auto mdlIt = models.find(inst.modelId); + if (mdlIt == models.end()) continue; + computeBoneMatrices(mdlIt->second, inst); } })); start = end; @@ -1354,8 +1358,11 @@ void M2Renderer::update(float deltaTime, const glm::vec3& cameraPos, const glm:: // Phase 3: Particle update (sequential — uses RNG, not thread-safe) for (size_t idx : boneWorkIndices) { + if (idx >= instances.size()) continue; auto& instance = instances[idx]; - const auto& model = models.find(instance.modelId)->second; + auto mdlIt = models.find(instance.modelId); + if (mdlIt == models.end()) continue; + const auto& model = mdlIt->second; if (!model.particleEmitters.empty()) { emitParticles(instance, model, deltaTime); updateParticles(instance, deltaTime); @@ -1471,13 +1478,16 @@ void M2Renderer::render(const Camera& camera, const glm::mat4& view, const glm:: const M2ModelGPU* currentModel = nullptr; for (const auto& entry : sortedVisible) { + if (entry.index >= instances.size()) continue; const auto& instance = instances[entry.index]; // Bind VAO once per model group if (entry.modelId != currentModelId) { if (currentModel) glBindVertexArray(0); currentModelId = entry.modelId; - currentModel = &models.find(currentModelId)->second; + auto mdlIt = models.find(currentModelId); + if (mdlIt == models.end()) continue; + currentModel = &mdlIt->second; glBindVertexArray(currentModel->vao); } diff --git a/src/rendering/terrain_manager.cpp b/src/rendering/terrain_manager.cpp index 94c097b5..12db5c6f 100644 --- a/src/rendering/terrain_manager.cpp +++ b/src/rendering/terrain_manager.cpp @@ -676,7 +676,10 @@ void TerrainManager::processReadyTiles() { if (pending) { TileCoord coord = pending->coord; finalizeTile(std::move(pending)); - pendingTiles.erase(coord); + { + std::lock_guard lock(queueMutex); + pendingTiles.erase(coord); + } processed++; } } @@ -694,7 +697,10 @@ void TerrainManager::processAllReadyTiles() { if (pending) { TileCoord coord = pending->coord; finalizeTile(std::move(pending)); - pendingTiles.erase(coord); + { + std::lock_guard lock(queueMutex); + pendingTiles.erase(coord); + } } } } @@ -703,7 +709,10 @@ void TerrainManager::unloadTile(int x, int y) { TileCoord coord = {x, y}; // Also remove from pending if it was queued but not yet loaded - pendingTiles.erase(coord); + { + std::lock_guard lock(queueMutex); + pendingTiles.erase(coord); + } auto it = loadedTiles.find(coord); if (it == loadedTiles.end()) { @@ -750,14 +759,6 @@ void TerrainManager::unloadTile(int x, int y) { } loadedTiles.erase(it); - - // Clean up any models that are no longer referenced - if (m2Renderer) { - m2Renderer->cleanupUnusedModels(); - } - if (wmoRenderer) { - wmoRenderer->cleanupUnusedModels(); - } } void TerrainManager::unloadAll() { @@ -1091,6 +1092,14 @@ void TerrainManager::streamTiles() { } if (!tilesToUnload.empty()) { + // Clean up models that lost all instances (once, after all tiles removed) + if (m2Renderer) { + m2Renderer->cleanupUnusedModels(); + } + if (wmoRenderer) { + wmoRenderer->cleanupUnusedModels(); + } + LOG_INFO("Unloaded ", tilesToUnload.size(), " distant tiles, ", loadedTiles.size(), " remain"); } diff --git a/src/rendering/wmo_renderer.cpp b/src/rendering/wmo_renderer.cpp index 02988e84..c043965a 100644 --- a/src/rendering/wmo_renderer.cpp +++ b/src/rendering/wmo_renderer.cpp @@ -852,8 +852,11 @@ void WMORenderer::render(const Camera& camera, const glm::mat4& view, const glm: bool doFrustumCull = frustumCulling; auto cullInstance = [&](size_t instIdx) -> InstanceDrawList { + if (instIdx >= instances.size()) return InstanceDrawList{}; const auto& instance = instances[instIdx]; - const ModelData& model = loadedModels.find(instance.modelId)->second; + auto mdlIt = loadedModels.find(instance.modelId); + if (mdlIt == loadedModels.end()) return InstanceDrawList{}; + const ModelData& model = mdlIt->second; InstanceDrawList result; result.instanceIndex = instIdx; @@ -942,8 +945,11 @@ void WMORenderer::render(const Camera& camera, const glm::mat4& view, const glm: // ── Phase 2: Sequential GL draw ──────────────────────────────── for (const auto& dl : drawLists) { + if (dl.instanceIndex >= instances.size()) continue; const auto& instance = instances[dl.instanceIndex]; - const ModelData& model = loadedModels.find(instance.modelId)->second; + auto modelIt = loadedModels.find(instance.modelId); + if (modelIt == loadedModels.end()) continue; + const ModelData& model = modelIt->second; // Occlusion query pre-pass (GL calls — must be main thread) if (occlusionCulling && occlusionShader && bboxVao != 0) {