Incremental terrain upload + M2 instance dedup hash for city stutter

Terrain finalization was uploading all 256 chunks (GPU fence waits) in one
atomic advanceFinalization call that couldn't be interrupted by the 5ms time
budget. Now split into incremental batches of 16 chunks per call, allowing
the time budget to yield between batches.

M2 instance creation had O(N) dedup scans iterating ALL instances to check
for duplicates. In cities with 5000+ doodads, this caused O(N²) total work
during tile loading. Replaced with hash-based DedupKey map for O(1) lookups.

Changes:
- TerrainRenderer::loadTerrainIncremental: uploads N chunks per call
- FinalizingTile tracks terrainChunkNext for cross-frame progress
- TERRAIN phase yields after preload and after each chunk batch
- M2Renderer::DedupKey hash map replaces linear scan in createInstance
  and createInstanceWithMatrix
- Dedup map maintained through rebuildSpatialIndex and clear paths
This commit is contained in:
Kelsi 2026-03-07 11:59:19 -08:00
parent f9410cc4bd
commit 884b72bc1c
6 changed files with 193 additions and 35 deletions

View file

@ -695,27 +695,39 @@ bool TerrainManager::advanceFinalization(FinalizingTile& ft) {
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<std::mutex> lock(queueMutex);
pendingTiles.erase(coord);
// Upload pre-loaded textures (once)
if (!ft.terrainPreloaded) {
LOG_DEBUG("Finalizing tile [", x, ",", y, "] (incremental)");
if (!pending->preloadedTextures.empty()) {
terrainRenderer->uploadPreloadedTextures(pending->preloadedTextures);
}
ft.phase = FinalizationPhase::DONE;
return true;
ft.terrainPreloaded = true;
// Yield after preload to give time budget a chance to interrupt
return false;
}
// Load water immediately after terrain (same frame) — water is now
// deduplicated to ~1-2 merged surfaces per tile, so this is fast.
// Upload terrain chunks incrementally (16 per call to spread across frames)
if (!ft.terrainMeshDone) {
if (pending->mesh.validChunkCount == 0) {
LOG_ERROR("Failed to upload terrain to GPU for tile [", x, ",", y, "]");
failedTiles[coord] = true;
{
std::lock_guard<std::mutex> lock(queueMutex);
pendingTiles.erase(coord);
}
ft.phase = FinalizationPhase::DONE;
return true;
}
bool allDone = terrainRenderer->loadTerrainIncremental(
pending->mesh, pending->terrain.textures, x, y,
ft.terrainChunkNext, 16);
if (!allDone) {
return false; // More chunks remain — yield to time budget
}
ft.terrainMeshDone = true;
}
// Load water after all terrain chunks are uploaded
if (waterRenderer) {
size_t beforeSurfaces = waterRenderer->getSurfaceCount();
waterRenderer->loadFromTerrain(pending->terrain, true, x, y);