mirror of
https://github.com/Kelsidavis/WoWee.git
synced 2026-05-06 00:53:52 +00:00
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)
This commit is contained in:
parent
1c58911da0
commit
c60ddcfed4
5 changed files with 103 additions and 73 deletions
|
|
@ -98,13 +98,19 @@ void EditorApp::run() {
|
||||||
// Refresh dirty terrain chunks
|
// Refresh dirty terrain chunks
|
||||||
refreshDirtyChunks();
|
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();
|
size_t objCount = objectPlacer_.objectCount() + npcSpawner_.spawnCount();
|
||||||
if (objectsDirty_ || objCount != lastObjectCount_) {
|
if (objectsDirty_ || objCount != lastObjectCount_) {
|
||||||
objectsDirty_ = false;
|
objectsDirty_ = false;
|
||||||
|
bool countChanged = (objCount != lastObjectCount_);
|
||||||
lastObjectCount_ = objCount;
|
lastObjectCount_ = objCount;
|
||||||
vkDeviceWaitIdle(window_->getVkContext()->getDevice());
|
// Only update NPC position markers (always cheap)
|
||||||
viewport_.rebuildObjects(objectPlacer_.getObjects(), npcSpawner_.getSpawns());
|
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
|
// 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) {
|
void EditorApp::createNewTerrain(const std::string& mapName, int tileX, int tileY, float baseHeight, Biome biome) {
|
||||||
terrain_ = TerrainEditor::createBlankTerrain(tileX, tileY, baseHeight, biome);
|
terrain_ = TerrainEditor::createBlankTerrain(tileX, tileY, baseHeight, biome);
|
||||||
// Clear previous state
|
// Clear previous state
|
||||||
objectPlacer_.clearAll();
|
clearAllObjects();
|
||||||
npcSpawner_.clearSelection();
|
|
||||||
npcSpawner_.getSpawns().clear();
|
|
||||||
viewport_.clearObjects();
|
|
||||||
|
|
||||||
terrainEditor_.setTerrain(&terrain_);
|
terrainEditor_.setTerrain(&terrain_);
|
||||||
terrainEditor_.history().clear();
|
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() {
|
void EditorApp::centerOnTerrain() {
|
||||||
if (!terrain_.isLoaded()) return;
|
if (!terrain_.isLoaded()) return;
|
||||||
float centerX = (32.0f - loadedTileY_) * 533.33333f - 8.0f * 533.33333f / 16.0f;
|
float centerX = (32.0f - loadedTileY_) * 533.33333f - 8.0f * 533.33333f / 16.0f;
|
||||||
|
|
|
||||||
|
|
@ -76,6 +76,7 @@ public:
|
||||||
void setSkyPreset(int preset); // 0=day, 1=dusk, 2=night
|
void setSkyPreset(int preset); // 0=day, 1=dusk, 2=night
|
||||||
void snapSelectedToGround();
|
void snapSelectedToGround();
|
||||||
void flyToSelected();
|
void flyToSelected();
|
||||||
|
void clearAllObjects();
|
||||||
void centerOnTerrain();
|
void centerOnTerrain();
|
||||||
|
|
||||||
// Multi-tile support
|
// Multi-tile support
|
||||||
|
|
|
||||||
|
|
@ -103,12 +103,7 @@ void EditorUI::renderMenuBar(EditorApp& app) {
|
||||||
ImGui::EndMenu();
|
ImGui::EndMenu();
|
||||||
}
|
}
|
||||||
if (ImGui::MenuItem("Clear All Objects/NPCs", nullptr, false, app.hasTerrainLoaded())) {
|
if (ImGui::MenuItem("Clear All Objects/NPCs", nullptr, false, app.hasTerrainLoaded())) {
|
||||||
app.getObjectPlacer().clearAll();
|
app.clearAllObjects();
|
||||||
app.getNpcSpawner().clearSelection();
|
|
||||||
app.getNpcSpawner().getSpawns().clear();
|
|
||||||
app.getTerrainEditor().history().clear();
|
|
||||||
app.markObjectsDirty();
|
|
||||||
app.showToast("All objects and NPCs cleared");
|
|
||||||
}
|
}
|
||||||
ImGui::Separator();
|
ImGui::Separator();
|
||||||
if (ImGui::MenuItem("Quick Save", "Ctrl+S", false, app.hasTerrainLoaded()))
|
if (ImGui::MenuItem("Quick Save", "Ctrl+S", false, app.hasTerrainLoaded()))
|
||||||
|
|
|
||||||
|
|
@ -144,15 +144,27 @@ void EditorViewport::rebuildObjects(const std::vector<PlacedObject>& objects,
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Ensure boundRadius is reasonable for culling
|
|
||||||
if (model.boundRadius < 1.0f) model.boundRadius = 50.0f;
|
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++;
|
modelId = nextModelId++;
|
||||||
if (!m2Renderer_->loadModel(model, modelId)) {
|
if (!m2Renderer_->loadModel(model, modelId)) {
|
||||||
LOG_WARNING("M2 failed to upload to GPU: ", obj.path);
|
LOG_WARNING("M2 failed to upload to GPU: ", obj.path);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
// Wait for async texture uploads to complete before rendering
|
|
||||||
vkCtx_->waitAllUploads();
|
vkCtx_->waitAllUploads();
|
||||||
vkCtx_->pollUploadBatches();
|
vkCtx_->pollUploadBatches();
|
||||||
LOG_INFO("M2 loaded: ", obj.path, " (modelId=", modelId, ", ",
|
LOG_INFO("M2 loaded: ", obj.path, " (modelId=", modelId, ", ",
|
||||||
|
|
@ -234,6 +246,14 @@ void EditorViewport::rebuildObjects(const std::vector<PlacedObject>& objects,
|
||||||
}
|
}
|
||||||
if (!model.isValid()) continue;
|
if (!model.isValid()) continue;
|
||||||
if (model.boundRadius < 1.0f) model.boundRadius = 50.0f;
|
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++;
|
modelId = nextModelId++;
|
||||||
if (!m2Renderer_->loadModel(model, modelId)) continue;
|
if (!m2Renderer_->loadModel(model, modelId)) continue;
|
||||||
vkCtx_->waitAllUploads();
|
vkCtx_->waitAllUploads();
|
||||||
|
|
@ -248,64 +268,8 @@ void EditorViewport::rebuildObjects(const std::vector<PlacedObject>& objects,
|
||||||
vkCtx_->waitAllUploads();
|
vkCtx_->waitAllUploads();
|
||||||
vkCtx_->pollUploadBatches();
|
vkCtx_->pollUploadBatches();
|
||||||
|
|
||||||
// Build NPC position markers (always visible, renders as colored discs)
|
// Update NPC markers via dedicated method
|
||||||
if (npcMarkerVB_) {
|
updateNpcMarkers(npcs);
|
||||||
vmaDestroyBuffer(vkCtx_->getAllocator(), npcMarkerVB_, npcMarkerVBAlloc_);
|
|
||||||
npcMarkerVB_ = VK_NULL_HANDLE;
|
|
||||||
npcMarkerVertCount_ = 0;
|
|
||||||
}
|
|
||||||
if (!npcs.empty()) {
|
|
||||||
struct MV { float pos[3]; float color[4]; };
|
|
||||||
std::vector<MV> 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<uint32_t>(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));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void EditorViewport::setBrushIndicator(const glm::vec3& center, float radius, bool active) {
|
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<CreatureSpawn>& 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<MV> 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<uint32_t>(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) {
|
void EditorViewport::update(float deltaTime) {
|
||||||
if (m2Renderer_)
|
if (m2Renderer_)
|
||||||
m2Renderer_->update(deltaTime, camera_->getPosition(), camera_->getViewProjectionMatrix());
|
m2Renderer_->update(deltaTime, camera_->getPosition(), camera_->getViewProjectionMatrix());
|
||||||
|
|
|
||||||
|
|
@ -35,6 +35,7 @@ public:
|
||||||
|
|
||||||
void updateWater(const pipeline::ADTTerrain& terrain, int tileX, int tileY);
|
void updateWater(const pipeline::ADTTerrain& terrain, int tileX, int tileY);
|
||||||
void updateMarkers(const std::vector<PlacedObject>& objects);
|
void updateMarkers(const std::vector<PlacedObject>& objects);
|
||||||
|
void updateNpcMarkers(const std::vector<CreatureSpawn>& npcs);
|
||||||
void placeM2(const std::string& path, const glm::vec3& pos, const glm::vec3& rot, float scale);
|
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 placeWMO(const std::string& path, const glm::vec3& pos, const glm::vec3& rot);
|
||||||
void clearObjects();
|
void clearObjects();
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue