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:
Kelsi 2026-05-05 07:07:33 -07:00
parent 1c58911da0
commit c60ddcfed4
5 changed files with 103 additions and 73 deletions

View file

@ -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;