From c60ddcfed4eadb6933603310b95f3f40eaca0912 Mon Sep 17 00:00:00 2001 From: Kelsi Date: Tue, 5 May 2026 07:07:33 -0700 Subject: [PATCH] fix(editor): stop destructive M2 rebuild on every NPC click, fix Clear All Root cause of GPU crashes (VK_ERROR_DEVICE_LOST): every NPC placement triggered a full clear+reload of ALL M2 models. After several cycles the GPU state corrupted, causing vertex explosions and device lost. Fixes: - NPC placement now only updates cheap marker geometry (no M2 reload) - Full M2 rebuild only happens when object COUNT changes (not every click) - clearAllObjects() properly resets viewport, placer, spawner, markers, and history in one call with vkDeviceWaitIdle fence - New Terrain uses clearAllObjects() for consistent reset - Clear All menu item calls clearAllObjects() - M2 vertex validation: rejects models with NaN/infinite/extreme vertex positions before GPU upload (prevents vertex explosions) - NPC marker building extracted to updateNpcMarkers() method (can be called independently without M2 rebuild) --- tools/editor/editor_app.cpp | 30 +++++-- tools/editor/editor_app.hpp | 1 + tools/editor/editor_ui.cpp | 7 +- tools/editor/editor_viewport.cpp | 137 +++++++++++++++++-------------- tools/editor/editor_viewport.hpp | 1 + 5 files changed, 103 insertions(+), 73 deletions(-) diff --git a/tools/editor/editor_app.cpp b/tools/editor/editor_app.cpp index 938125b2..277ea4ae 100644 --- a/tools/editor/editor_app.cpp +++ b/tools/editor/editor_app.cpp @@ -98,13 +98,19 @@ void EditorApp::run() { // Refresh dirty terrain chunks refreshDirtyChunks(); - // Rebuild object visuals when object list changes + // Update NPC markers (cheap — just vertex buffer, no M2 reload) size_t objCount = objectPlacer_.objectCount() + npcSpawner_.spawnCount(); if (objectsDirty_ || objCount != lastObjectCount_) { objectsDirty_ = false; + bool countChanged = (objCount != lastObjectCount_); lastObjectCount_ = objCount; - vkDeviceWaitIdle(window_->getVkContext()->getDevice()); - viewport_.rebuildObjects(objectPlacer_.getObjects(), npcSpawner_.getSpawns()); + // Only update NPC position markers (always cheap) + viewport_.updateNpcMarkers(npcSpawner_.getSpawns()); + // Full M2 rebuild only when explicitly requested (not on every click) + if (countChanged && objCount > 0) { + vkDeviceWaitIdle(vkCtx->getDevice()); + viewport_.rebuildObjects(objectPlacer_.getObjects(), npcSpawner_.getSpawns()); + } } // Show gizmo arrows on selected object @@ -602,10 +608,7 @@ void EditorApp::loadADT(const std::string& mapName, int tileX, int tileY) { void EditorApp::createNewTerrain(const std::string& mapName, int tileX, int tileY, float baseHeight, Biome biome) { terrain_ = TerrainEditor::createBlankTerrain(tileX, tileY, baseHeight, biome); // Clear previous state - objectPlacer_.clearAll(); - npcSpawner_.clearSelection(); - npcSpawner_.getSpawns().clear(); - viewport_.clearObjects(); + clearAllObjects(); terrainEditor_.setTerrain(&terrain_); terrainEditor_.history().clear(); @@ -804,6 +807,19 @@ void EditorApp::flyToSelected() { } } +void EditorApp::clearAllObjects() { + vkDeviceWaitIdle(window_->getVkContext()->getDevice()); + objectPlacer_.clearAll(); + npcSpawner_.clearSelection(); + npcSpawner_.getSpawns().clear(); + viewport_.clearObjects(); + viewport_.updateNpcMarkers({}); + terrainEditor_.history().clear(); + lastObjectCount_ = 0; + objectsDirty_ = false; + showToast("All objects and NPCs cleared"); +} + void EditorApp::centerOnTerrain() { if (!terrain_.isLoaded()) return; float centerX = (32.0f - loadedTileY_) * 533.33333f - 8.0f * 533.33333f / 16.0f; diff --git a/tools/editor/editor_app.hpp b/tools/editor/editor_app.hpp index b1a93717..691f1084 100644 --- a/tools/editor/editor_app.hpp +++ b/tools/editor/editor_app.hpp @@ -76,6 +76,7 @@ public: void setSkyPreset(int preset); // 0=day, 1=dusk, 2=night void snapSelectedToGround(); void flyToSelected(); + void clearAllObjects(); void centerOnTerrain(); // Multi-tile support diff --git a/tools/editor/editor_ui.cpp b/tools/editor/editor_ui.cpp index 95c414ae..b4bd38e8 100644 --- a/tools/editor/editor_ui.cpp +++ b/tools/editor/editor_ui.cpp @@ -103,12 +103,7 @@ void EditorUI::renderMenuBar(EditorApp& app) { ImGui::EndMenu(); } if (ImGui::MenuItem("Clear All Objects/NPCs", nullptr, false, app.hasTerrainLoaded())) { - app.getObjectPlacer().clearAll(); - app.getNpcSpawner().clearSelection(); - app.getNpcSpawner().getSpawns().clear(); - app.getTerrainEditor().history().clear(); - app.markObjectsDirty(); - app.showToast("All objects and NPCs cleared"); + app.clearAllObjects(); } ImGui::Separator(); if (ImGui::MenuItem("Quick Save", "Ctrl+S", false, app.hasTerrainLoaded())) diff --git a/tools/editor/editor_viewport.cpp b/tools/editor/editor_viewport.cpp index 2440d42f..bbb35f1d 100644 --- a/tools/editor/editor_viewport.cpp +++ b/tools/editor/editor_viewport.cpp @@ -144,15 +144,27 @@ void EditorViewport::rebuildObjects(const std::vector& objects, continue; } - // Ensure boundRadius is reasonable for culling if (model.boundRadius < 1.0f) model.boundRadius = 50.0f; + // Validate vertex data to prevent GPU crashes + bool vertexOk = true; + for (const auto& vert : model.vertices) { + if (!std::isfinite(vert.position.x) || !std::isfinite(vert.position.y) || + !std::isfinite(vert.position.z) || std::abs(vert.position.x) > 100000.0f) { + vertexOk = false; + break; + } + } + if (!vertexOk) { + LOG_WARNING("M2 has invalid vertex data, skipping: ", obj.path); + continue; + } + modelId = nextModelId++; if (!m2Renderer_->loadModel(model, modelId)) { LOG_WARNING("M2 failed to upload to GPU: ", obj.path); continue; } - // Wait for async texture uploads to complete before rendering vkCtx_->waitAllUploads(); vkCtx_->pollUploadBatches(); LOG_INFO("M2 loaded: ", obj.path, " (modelId=", modelId, ", ", @@ -234,6 +246,14 @@ void EditorViewport::rebuildObjects(const std::vector& objects, } if (!model.isValid()) continue; if (model.boundRadius < 1.0f) model.boundRadius = 50.0f; + // Validate vertex data + bool ok = true; + for (const auto& vert : model.vertices) { + if (!std::isfinite(vert.position.x) || std::abs(vert.position.x) > 100000.0f) { + ok = false; break; + } + } + if (!ok) { LOG_WARNING("NPC M2 bad vertices: ", npc.modelPath); continue; } modelId = nextModelId++; if (!m2Renderer_->loadModel(model, modelId)) continue; vkCtx_->waitAllUploads(); @@ -248,64 +268,8 @@ void EditorViewport::rebuildObjects(const std::vector& objects, vkCtx_->waitAllUploads(); vkCtx_->pollUploadBatches(); - // Build NPC position markers (always visible, renders as colored discs) - if (npcMarkerVB_) { - vmaDestroyBuffer(vkCtx_->getAllocator(), npcMarkerVB_, npcMarkerVBAlloc_); - npcMarkerVB_ = VK_NULL_HANDLE; - npcMarkerVertCount_ = 0; - } - if (!npcs.empty()) { - struct MV { float pos[3]; float color[4]; }; - std::vector verts; - for (const auto& npc : npcs) { - float s = 5.0f; - float x = npc.position.x, y = npc.position.y, z = npc.position.z; - float r = npc.hostile ? 1.0f : 0.1f; - float g = npc.hostile ? 0.15f : 0.9f; - float b = 0.1f, a = 0.9f; - - // Large base circle (8 triangles forming octagon) - MV v; v.color[0]=r; v.color[1]=g; v.color[2]=b; v.color[3]=a; - for (int seg = 0; seg < 8; seg++) { - float a0 = seg * 0.7854f, a1 = (seg+1) * 0.7854f; - v.pos[0]=x; v.pos[1]=y; v.pos[2]=z+0.3f; verts.push_back(v); - v.pos[0]=x+std::cos(a0)*s; v.pos[1]=y+std::sin(a0)*s; v.pos[2]=z+0.3f; verts.push_back(v); - v.pos[0]=x+std::cos(a1)*s; v.pos[1]=y+std::sin(a1)*s; v.pos[2]=z+0.3f; verts.push_back(v); - } - - // Tall pole (2 triangles forming thin quad, 30 units high) - float pw = 0.8f, ph = 30.0f; - v.color[3] = 0.8f; - v.pos[0]=x-pw; v.pos[1]=y; v.pos[2]=z; verts.push_back(v); - v.pos[0]=x+pw; v.pos[1]=y; v.pos[2]=z; verts.push_back(v); - v.pos[0]=x; v.pos[1]=y; v.pos[2]=z+ph; verts.push_back(v); - v.pos[0]=x; v.pos[1]=y-pw; v.pos[2]=z; verts.push_back(v); - v.pos[0]=x; v.pos[1]=y+pw; v.pos[2]=z; verts.push_back(v); - v.pos[0]=x; v.pos[1]=y; v.pos[2]=z+ph; verts.push_back(v); - - // Top diamond (visible from above) - float ts = 3.0f; - float tz = z + ph; - v.color[0]=1; v.color[1]=1; v.color[2]=0.3f; v.color[3]=0.95f; - v.pos[0]=x+ts; v.pos[1]=y; v.pos[2]=tz; verts.push_back(v); - v.pos[0]=x; v.pos[1]=y+ts; v.pos[2]=tz; verts.push_back(v); - v.pos[0]=x-ts; v.pos[1]=y; v.pos[2]=tz; verts.push_back(v); - v.pos[0]=x+ts; v.pos[1]=y; v.pos[2]=tz; verts.push_back(v); - v.pos[0]=x-ts; v.pos[1]=y; v.pos[2]=tz; verts.push_back(v); - v.pos[0]=x; v.pos[1]=y-ts; v.pos[2]=tz; verts.push_back(v); - } - npcMarkerVertCount_ = static_cast(verts.size()); - LOG_INFO("NPC markers: ", npcs.size(), " npcs -> ", npcMarkerVertCount_, " verts"); - VkBufferCreateInfo bi{VK_STRUCTURE_TYPE_BUFFER_CREATE_INFO}; - bi.size = verts.size() * sizeof(MV); - bi.usage = VK_BUFFER_USAGE_VERTEX_BUFFER_BIT; - VmaAllocationCreateInfo ai{}; ai.usage = VMA_MEMORY_USAGE_CPU_TO_GPU; - ai.flags = VMA_ALLOCATION_CREATE_MAPPED_BIT; - VmaAllocationInfo mi{}; - if (vmaCreateBuffer(vkCtx_->getAllocator(), &bi, &ai, - &npcMarkerVB_, &npcMarkerVBAlloc_, &mi) == VK_SUCCESS) - std::memcpy(mi.pMappedData, verts.data(), verts.size() * sizeof(MV)); - } + // Update NPC markers via dedicated method + updateNpcMarkers(npcs); } void EditorViewport::setBrushIndicator(const glm::vec3& center, float radius, bool active) { @@ -362,6 +326,59 @@ void EditorViewport::setBrushIndicator(const glm::vec3& center, float radius, bo } } +void EditorViewport::updateNpcMarkers(const std::vector& npcs) { + if (npcMarkerVB_) { + vmaDestroyBuffer(vkCtx_->getAllocator(), npcMarkerVB_, npcMarkerVBAlloc_); + npcMarkerVB_ = VK_NULL_HANDLE; + npcMarkerVertCount_ = 0; + } + if (npcs.empty()) return; + + struct MV { float pos[3]; float color[4]; }; + std::vector verts; + for (const auto& npc : npcs) { + float s = 5.0f; + float x = npc.position.x, y = npc.position.y, z = npc.position.z; + float r = npc.hostile ? 1.0f : 0.1f; + float g = npc.hostile ? 0.15f : 0.9f; + float b = 0.1f, a = 0.9f; + + MV v; v.color[0]=r; v.color[1]=g; v.color[2]=b; v.color[3]=a; + for (int seg = 0; seg < 8; seg++) { + float a0 = seg * 0.7854f, a1 = (seg+1) * 0.7854f; + v.pos[0]=x; v.pos[1]=y; v.pos[2]=z+0.3f; verts.push_back(v); + v.pos[0]=x+std::cos(a0)*s; v.pos[1]=y+std::sin(a0)*s; v.pos[2]=z+0.3f; verts.push_back(v); + v.pos[0]=x+std::cos(a1)*s; v.pos[1]=y+std::sin(a1)*s; v.pos[2]=z+0.3f; verts.push_back(v); + } + float pw = 0.8f, ph = 30.0f; + v.color[3] = 0.8f; + v.pos[0]=x-pw; v.pos[1]=y; v.pos[2]=z; verts.push_back(v); + v.pos[0]=x+pw; v.pos[1]=y; v.pos[2]=z; verts.push_back(v); + v.pos[0]=x; v.pos[1]=y; v.pos[2]=z+ph; verts.push_back(v); + v.pos[0]=x; v.pos[1]=y-pw; v.pos[2]=z; verts.push_back(v); + v.pos[0]=x; v.pos[1]=y+pw; v.pos[2]=z; verts.push_back(v); + v.pos[0]=x; v.pos[1]=y; v.pos[2]=z+ph; verts.push_back(v); + float ts = 3.0f, tz = z + ph; + v.color[0]=1; v.color[1]=1; v.color[2]=0.3f; v.color[3]=0.95f; + v.pos[0]=x+ts; v.pos[1]=y; v.pos[2]=tz; verts.push_back(v); + v.pos[0]=x; v.pos[1]=y+ts; v.pos[2]=tz; verts.push_back(v); + v.pos[0]=x-ts; v.pos[1]=y; v.pos[2]=tz; verts.push_back(v); + v.pos[0]=x+ts; v.pos[1]=y; v.pos[2]=tz; verts.push_back(v); + v.pos[0]=x-ts; v.pos[1]=y; v.pos[2]=tz; verts.push_back(v); + v.pos[0]=x; v.pos[1]=y-ts; v.pos[2]=tz; verts.push_back(v); + } + npcMarkerVertCount_ = static_cast(verts.size()); + VkBufferCreateInfo bi{VK_STRUCTURE_TYPE_BUFFER_CREATE_INFO}; + bi.size = verts.size() * sizeof(MV); + bi.usage = VK_BUFFER_USAGE_VERTEX_BUFFER_BIT; + VmaAllocationCreateInfo ai{}; ai.usage = VMA_MEMORY_USAGE_CPU_TO_GPU; + ai.flags = VMA_ALLOCATION_CREATE_MAPPED_BIT; + VmaAllocationInfo mi{}; + if (vmaCreateBuffer(vkCtx_->getAllocator(), &bi, &ai, + &npcMarkerVB_, &npcMarkerVBAlloc_, &mi) == VK_SUCCESS) + std::memcpy(mi.pMappedData, verts.data(), verts.size() * sizeof(MV)); +} + void EditorViewport::update(float deltaTime) { if (m2Renderer_) m2Renderer_->update(deltaTime, camera_->getPosition(), camera_->getViewProjectionMatrix()); diff --git a/tools/editor/editor_viewport.hpp b/tools/editor/editor_viewport.hpp index d71a56d2..62047054 100644 --- a/tools/editor/editor_viewport.hpp +++ b/tools/editor/editor_viewport.hpp @@ -35,6 +35,7 @@ public: void updateWater(const pipeline::ADTTerrain& terrain, int tileX, int tileY); void updateMarkers(const std::vector& objects); + void updateNpcMarkers(const std::vector& npcs); void placeM2(const std::string& path, const glm::vec3& pos, const glm::vec3& rot, float scale); void placeWMO(const std::string& path, const glm::vec3& pos, const glm::vec3& rot); void clearObjects();