Revert terrain_manager to original finalizeTile to fix water rendering

The incremental advanceFinalization state machine broke water rendering
in ways that couldn't be resolved. Reverted to the original monolithic
finalizeTile approach. The other performance optimizations (bone SSBO
pre-allocation, WMO distance culling, M2 adaptive distance tiers)
are kept.
This commit is contained in:
Kelsi 2026-02-25 02:50:36 -08:00
parent 7dd78e2c0a
commit 9b90ab0429
2 changed files with 255 additions and 326 deletions

View file

@ -123,41 +123,6 @@ struct PendingTile {
std::unordered_map<std::string, pipeline::BLPImage> preloadedTextures; std::unordered_map<std::string, pipeline::BLPImage> preloadedTextures;
}; };
/**
* Phases for incremental tile finalization (one bounded unit of work per call)
*/
enum class FinalizationPhase {
TERRAIN, // Upload terrain mesh + textures
M2_MODELS, // Upload ONE M2 model per call
M2_INSTANCES, // Create all M2 instances (lightweight struct allocation)
WMO_MODELS, // Upload ONE WMO model per call
WMO_INSTANCES, // Create all WMO instances + load WMO liquids
WMO_DOODADS, // Upload ONE WMO doodad M2 per call
WATER, // Upload water from terrain
AMBIENT, // Register ambient emitters
DONE // Commit to loadedTiles
};
/**
* In-progress tile finalization state tracks progress across frames
*/
struct FinalizingTile {
std::shared_ptr<PendingTile> pending;
FinalizationPhase phase = FinalizationPhase::TERRAIN;
// Progress indices within current phase
size_t m2ModelIndex = 0; // Next M2 model to upload
size_t wmoModelIndex = 0; // Next WMO model to upload
size_t wmoDoodadIndex = 0; // Next WMO doodad to upload
// Accumulated results (built up across phases)
std::vector<uint32_t> m2InstanceIds;
std::vector<uint32_t> wmoInstanceIds;
std::vector<uint32_t> tileUniqueIds;
std::vector<uint32_t> tileWmoUniqueIds;
std::unordered_set<uint32_t> uploadedM2ModelIds;
};
/** /**
* Terrain manager for multi-tile terrain streaming * Terrain manager for multi-tile terrain streaming
* *
@ -254,8 +219,8 @@ public:
int getLoadedTileCount() const { return static_cast<int>(loadedTiles.size()); } int getLoadedTileCount() const { return static_cast<int>(loadedTiles.size()); }
int getPendingTileCount() const { return static_cast<int>(pendingTiles.size()); } int getPendingTileCount() const { return static_cast<int>(pendingTiles.size()); }
int getReadyQueueCount() const { return static_cast<int>(readyQueue.size()); } int getReadyQueueCount() const { return static_cast<int>(readyQueue.size()); }
/** Total unfinished tiles (worker threads + ready queue + finalizing) */ /** Total unfinished tiles (worker threads + ready queue) */
int getRemainingTileCount() const { return static_cast<int>(pendingTiles.size() + readyQueue.size() + finalizingTiles_.size()); } int getRemainingTileCount() const { return static_cast<int>(pendingTiles.size() + readyQueue.size()); }
TileCoord getCurrentTile() const { return currentTile; } TileCoord getCurrentTile() const { return currentTile; }
/** Process all ready tiles immediately (use during loading screens) */ /** Process all ready tiles immediately (use during loading screens) */
@ -289,10 +254,9 @@ private:
std::shared_ptr<PendingTile> prepareTile(int x, int y); std::shared_ptr<PendingTile> prepareTile(int x, int y);
/** /**
* Advance incremental finalization of a tile (one bounded unit of work). * Main thread: upload prepared tile data to GPU
* Returns true when the tile is fully finalized (phase == DONE).
*/ */
bool advanceFinalization(FinalizingTile& ft); void finalizeTile(const std::shared_ptr<PendingTile>& pending);
/** /**
* Background worker thread loop * Background worker thread loop
@ -377,8 +341,16 @@ private:
// Dedup set for WMO placements across tile boundaries (prevents rendering Stormwind 16x) // Dedup set for WMO placements across tile boundaries (prevents rendering Stormwind 16x)
std::unordered_set<uint32_t> placedWmoIds; std::unordered_set<uint32_t> placedWmoIds;
// Tiles currently being incrementally finalized across frames // Progressive M2 upload queue (spread heavy uploads across frames)
std::deque<FinalizingTile> finalizingTiles_; struct PendingM2Upload {
uint32_t modelId;
pipeline::M2Model model;
std::string path;
};
std::queue<PendingM2Upload> m2UploadQueue_;
static constexpr int MAX_M2_UPLOADS_PER_FRAME = 5; // Upload up to 5 models per frame
void processM2UploadQueue();
struct GroundEffectEntry { struct GroundEffectEntry {
std::array<uint32_t, 4> doodadIds{{0, 0, 0, 0}}; std::array<uint32_t, 4> doodadIds{{0, 0, 0, 0}};

View file

@ -226,9 +226,7 @@ bool TerrainManager::loadTile(int x, int y) {
return false; return false;
} }
FinalizingTile ft; finalizeTile(pending);
ft.pending = std::move(pending);
while (!advanceFinalization(ft)) {}
return true; return true;
} }
@ -650,160 +648,176 @@ void TerrainManager::logMissingAdtOnce(const std::string& adtPath) {
} }
} }
bool TerrainManager::advanceFinalization(FinalizingTile& ft) { void TerrainManager::finalizeTile(const std::shared_ptr<PendingTile>& pending) {
auto& pending = ft.pending;
int x = pending->coord.x; int x = pending->coord.x;
int y = pending->coord.y; int y = pending->coord.y;
TileCoord coord = pending->coord; TileCoord coord = pending->coord;
switch (ft.phase) { LOG_DEBUG("Finalizing tile [", x, ",", y, "] (GPU upload)");
case FinalizationPhase::TERRAIN: { // Check if tile was already loaded (race condition guard) or failed
// Check if tile was already loaded or failed if (loadedTiles.find(coord) != loadedTiles.end()) {
if (loadedTiles.find(coord) != loadedTiles.end() || failedTiles.find(coord) != failedTiles.end()) { return;
{
std::lock_guard<std::mutex> lock(queueMutex);
pendingTiles.erase(coord);
} }
ft.phase = FinalizationPhase::DONE; if (failedTiles.find(coord) != failedTiles.end()) {
return true; return;
} }
LOG_DEBUG("Finalizing tile [", x, ",", y, "] (incremental)"); // Upload pre-loaded textures to the GL cache so loadTerrain avoids file I/O
// Upload pre-loaded textures
if (!pending->preloadedTextures.empty()) { if (!pending->preloadedTextures.empty()) {
terrainRenderer->uploadPreloadedTextures(pending->preloadedTextures); terrainRenderer->uploadPreloadedTextures(pending->preloadedTextures);
} }
// Upload terrain mesh to GPU // Upload terrain to GPU
if (!terrainRenderer->loadTerrain(pending->mesh, pending->terrain.textures, x, y)) { if (!terrainRenderer->loadTerrain(pending->mesh, pending->terrain.textures, x, y)) {
LOG_ERROR("Failed to upload terrain to GPU for tile [", x, ",", y, "]"); LOG_ERROR("Failed to upload terrain to GPU for tile [", x, ",", y, "]");
failedTiles[coord] = true; failedTiles[coord] = true;
{ return;
std::lock_guard<std::mutex> lock(queueMutex);
pendingTiles.erase(coord);
}
ft.phase = FinalizationPhase::DONE;
return true;
} }
// Load water immediately after terrain (same frame) — water vertex positions // Load water
// depend on terrain chunk data and must be uploaded before the terrain renders
// without water, otherwise vertices appear at origin.
if (waterRenderer) { if (waterRenderer) {
waterRenderer->loadFromTerrain(pending->terrain, true, x, y); waterRenderer->loadFromTerrain(pending->terrain, true, x, y);
} }
// Ensure M2 renderer has asset manager // Register water surface ambient sound emitters
if (ambientSoundManager) {
// Scan ADT water data for water surfaces
int waterEmitterCount = 0;
for (size_t chunkIdx = 0; chunkIdx < pending->terrain.waterData.size(); chunkIdx++) {
const auto& chunkWater = pending->terrain.waterData[chunkIdx];
if (!chunkWater.hasWater()) continue;
// Calculate chunk position in world coordinates
int chunkX = chunkIdx % 16;
int chunkY = chunkIdx / 16;
// WoW coordinates: Each ADT tile is 533.33 units, each chunk is 533.33/16 = 33.333 units
// Tile origin in GL space
float tileOriginX = (32.0f - x) * 533.33333f;
float tileOriginY = (32.0f - y) * 533.33333f;
// Chunk center position
float chunkCenterX = tileOriginX + (chunkX + 0.5f) * 33.333333f;
float chunkCenterY = tileOriginY + (chunkY + 0.5f) * 33.333333f;
// Use first layer for height and type detection
if (!chunkWater.layers.empty()) {
const auto& layer = chunkWater.layers[0];
float waterHeight = layer.minHeight;
// Determine water type and register appropriate emitter
// liquidType: 0=water/lake, 1=ocean, 2=magma, 3=slime
if (layer.liquidType == 0) {
// Lake/river water - add water surface emitter every 32 chunks to avoid spam
if (chunkIdx % 32 == 0) {
PendingTile::AmbientEmitter emitter;
emitter.position = glm::vec3(chunkCenterX, chunkCenterY, waterHeight);
emitter.type = 4; // WATER_SURFACE
pending->ambientEmitters.push_back(emitter);
waterEmitterCount++;
}
} else if (layer.liquidType == 1) {
// Ocean - add ocean emitter every 64 chunks (oceans are very large)
if (chunkIdx % 64 == 0) {
PendingTile::AmbientEmitter emitter;
emitter.position = glm::vec3(chunkCenterX, chunkCenterY, waterHeight);
emitter.type = 4; // WATER_SURFACE (could add separate OCEAN type later)
pending->ambientEmitters.push_back(emitter);
waterEmitterCount++;
}
}
// Skip magma and slime for now (no ambient sounds for those)
}
}
if (waterEmitterCount > 0) {
}
}
std::vector<uint32_t> m2InstanceIds;
std::vector<uint32_t> wmoInstanceIds;
std::vector<uint32_t> tileUniqueIds;
std::vector<uint32_t> tileWmoUniqueIds;
// Upload M2 models to GPU and create instances
if (m2Renderer && assetManager) { if (m2Renderer && assetManager) {
// Always pass the latest asset manager. initialize() is idempotent and updates
// the pointer even when the renderer was initialized earlier without assets.
m2Renderer->initialize(nullptr, VK_NULL_HANDLE, assetManager); m2Renderer->initialize(nullptr, VK_NULL_HANDLE, assetManager);
}
ft.phase = FinalizationPhase::M2_MODELS; // Upload M2 models immediately (batching was causing hangs)
return false; // The 5ms time budget in processReadyTiles() limits the spike
} std::unordered_set<uint32_t> uploadedModelIds;
for (auto& m2Ready : pending->m2Models) {
case FinalizationPhase::M2_MODELS: {
// Upload ONE M2 model per call
if (m2Renderer && ft.m2ModelIndex < pending->m2Models.size()) {
auto& m2Ready = pending->m2Models[ft.m2ModelIndex];
if (m2Renderer->loadModel(m2Ready.model, m2Ready.modelId)) { if (m2Renderer->loadModel(m2Ready.model, m2Ready.modelId)) {
ft.uploadedM2ModelIds.insert(m2Ready.modelId); uploadedModelIds.insert(m2Ready.modelId);
}
ft.m2ModelIndex++;
// Stay in this phase until all models uploaded
if (ft.m2ModelIndex < pending->m2Models.size()) {
return false;
} }
} }
if (!ft.uploadedM2ModelIds.empty()) { if (!uploadedModelIds.empty()) {
LOG_DEBUG(" Uploaded ", ft.uploadedM2ModelIds.size(), " M2 models for tile [", x, ",", y, "]"); LOG_DEBUG(" Uploaded ", uploadedModelIds.size(), " M2 models for tile [", x, ",", y, "]");
}
ft.phase = FinalizationPhase::M2_INSTANCES;
return false;
} }
case FinalizationPhase::M2_INSTANCES: { // Create instances (deduplicate by uniqueId across tile boundaries)
// Create all M2 instances (lightweight struct allocation, no GPU work)
if (m2Renderer) {
int loadedDoodads = 0; int loadedDoodads = 0;
int skippedDedup = 0; int skippedDedup = 0;
for (const auto& p : pending->m2Placements) { for (const auto& p : pending->m2Placements) {
// Skip if this doodad was already placed by a neighboring tile
if (p.uniqueId != 0 && placedDoodadIds.count(p.uniqueId)) { if (p.uniqueId != 0 && placedDoodadIds.count(p.uniqueId)) {
skippedDedup++; skippedDedup++;
continue; continue;
} }
uint32_t instId = m2Renderer->createInstance(p.modelId, p.position, p.rotation, p.scale); uint32_t instId = m2Renderer->createInstance(p.modelId, p.position, p.rotation, p.scale);
if (instId) { if (instId) {
ft.m2InstanceIds.push_back(instId); m2InstanceIds.push_back(instId);
if (p.uniqueId != 0) { if (p.uniqueId != 0) {
placedDoodadIds.insert(p.uniqueId); placedDoodadIds.insert(p.uniqueId);
ft.tileUniqueIds.push_back(p.uniqueId); tileUniqueIds.push_back(p.uniqueId);
} }
loadedDoodads++; loadedDoodads++;
} }
} }
LOG_DEBUG(" Loaded doodads for tile [", x, ",", y, "]: ", LOG_DEBUG(" Loaded doodads for tile [", x, ",", y, "]: ",
loadedDoodads, " instances (", ft.uploadedM2ModelIds.size(), " new models, ", loadedDoodads, " instances (", uploadedModelIds.size(), " new models, ",
skippedDedup, " dedup skipped)"); skippedDedup, " dedup skipped)");
} }
ft.phase = FinalizationPhase::WMO_MODELS;
return false;
}
case FinalizationPhase::WMO_MODELS: { // Upload WMO models to GPU and create instances
// Upload ONE WMO model per call
if (wmoRenderer && assetManager) { if (wmoRenderer && assetManager) {
// WMORenderer may be initialized before assets are ready; always re-pass assets.
wmoRenderer->initialize(nullptr, VK_NULL_HANDLE, assetManager); wmoRenderer->initialize(nullptr, VK_NULL_HANDLE, assetManager);
if (ft.wmoModelIndex < pending->wmoModels.size()) {
auto& wmoReady = pending->wmoModels[ft.wmoModelIndex];
// Deduplicate
if (wmoReady.uniqueId != 0 && placedWmoIds.count(wmoReady.uniqueId)) {
// Skip this one, advance
ft.wmoModelIndex++;
if (ft.wmoModelIndex < pending->wmoModels.size()) return false;
} else {
wmoRenderer->loadModel(wmoReady.model, wmoReady.modelId);
ft.wmoModelIndex++;
if (ft.wmoModelIndex < pending->wmoModels.size()) return false;
}
}
}
ft.wmoModelIndex = 0; // Reset for WMO_INSTANCES phase iteration
ft.phase = FinalizationPhase::WMO_INSTANCES;
return false;
}
case FinalizationPhase::WMO_INSTANCES: {
// Create all WMO instances + load WMO liquids
if (wmoRenderer) {
int loadedWMOs = 0; int loadedWMOs = 0;
int loadedLiquids = 0; int loadedLiquids = 0;
int skippedWmoDedup = 0; int skippedWmoDedup = 0;
for (auto& wmoReady : pending->wmoModels) { for (auto& wmoReady : pending->wmoModels) {
// Deduplicate by placement uniqueId when available.
// Some ADTs use uniqueId=0, which is not safe for dedup.
if (wmoReady.uniqueId != 0 && placedWmoIds.count(wmoReady.uniqueId)) { if (wmoReady.uniqueId != 0 && placedWmoIds.count(wmoReady.uniqueId)) {
skippedWmoDedup++; skippedWmoDedup++;
continue; continue;
} }
if (wmoRenderer->loadModel(wmoReady.model, wmoReady.modelId)) {
uint32_t wmoInstId = wmoRenderer->createInstance(wmoReady.modelId, wmoReady.position, wmoReady.rotation); uint32_t wmoInstId = wmoRenderer->createInstance(wmoReady.modelId, wmoReady.position, wmoReady.rotation);
if (wmoInstId) { if (wmoInstId) {
ft.wmoInstanceIds.push_back(wmoInstId); wmoInstanceIds.push_back(wmoInstId);
if (wmoReady.uniqueId != 0) { if (wmoReady.uniqueId != 0) {
placedWmoIds.insert(wmoReady.uniqueId); placedWmoIds.insert(wmoReady.uniqueId);
ft.tileWmoUniqueIds.push_back(wmoReady.uniqueId); tileWmoUniqueIds.push_back(wmoReady.uniqueId);
} }
loadedWMOs++; loadedWMOs++;
// Load WMO liquids (canals, pools, etc.) // Load WMO liquids (canals, pools, etc.)
if (waterRenderer) { if (waterRenderer) {
// Compute the same model matrix as WMORenderer uses
glm::mat4 modelMatrix = glm::mat4(1.0f); glm::mat4 modelMatrix = glm::mat4(1.0f);
modelMatrix = glm::translate(modelMatrix, wmoReady.position); modelMatrix = glm::translate(modelMatrix, wmoReady.position);
modelMatrix = glm::rotate(modelMatrix, wmoReady.rotation.z, glm::vec3(0.0f, 0.0f, 1.0f)); modelMatrix = glm::rotate(modelMatrix, wmoReady.rotation.z, glm::vec3(0.0f, 0.0f, 1.0f));
modelMatrix = glm::rotate(modelMatrix, wmoReady.rotation.y, glm::vec3(0.0f, 1.0f, 0.0f)); modelMatrix = glm::rotate(modelMatrix, wmoReady.rotation.y, glm::vec3(0.0f, 1.0f, 0.0f));
modelMatrix = glm::rotate(modelMatrix, wmoReady.rotation.x, glm::vec3(1.0f, 0.0f, 0.0f)); modelMatrix = glm::rotate(modelMatrix, wmoReady.rotation.x, glm::vec3(1.0f, 0.0f, 0.0f));
// Load liquids from each WMO group
for (const auto& group : wmoReady.model.groups) { for (const auto& group : wmoReady.model.groups) {
if (group.liquid.hasLiquid()) { if (group.liquid.hasLiquid()) {
waterRenderer->loadFromWMO(group.liquid, modelMatrix, wmoInstId); waterRenderer->loadFromWMO(group.liquid, modelMatrix, wmoInstId);
@ -813,6 +827,7 @@ bool TerrainManager::advanceFinalization(FinalizingTile& ft) {
} }
} }
} }
}
if (loadedWMOs > 0 || skippedWmoDedup > 0) { if (loadedWMOs > 0 || skippedWmoDedup > 0) {
LOG_DEBUG(" Loaded WMOs for tile [", x, ",", y, "]: ", LOG_DEBUG(" Loaded WMOs for tile [", x, ",", y, "]: ",
loadedWMOs, " instances, ", skippedWmoDedup, " dedup skipped"); loadedWMOs, " instances, ", skippedWmoDedup, " dedup skipped");
@ -820,102 +835,49 @@ bool TerrainManager::advanceFinalization(FinalizingTile& ft) {
if (loadedLiquids > 0) { if (loadedLiquids > 0) {
LOG_DEBUG(" Loaded WMO liquids for tile [", x, ",", y, "]: ", loadedLiquids); LOG_DEBUG(" Loaded WMO liquids for tile [", x, ",", y, "]: ", loadedLiquids);
} }
}
ft.phase = FinalizationPhase::WMO_DOODADS;
return false;
}
case FinalizationPhase::WMO_DOODADS: { // Upload WMO doodad M2 models
// Upload ONE WMO doodad M2 per call if (m2Renderer) {
if (m2Renderer && ft.wmoDoodadIndex < pending->wmoDoodads.size()) { for (auto& doodad : pending->wmoDoodads) {
auto& doodad = pending->wmoDoodads[ft.wmoDoodadIndex];
m2Renderer->loadModel(doodad.model, doodad.modelId); m2Renderer->loadModel(doodad.model, doodad.modelId);
uint32_t wmoDoodadInstId = m2Renderer->createInstanceWithMatrix( uint32_t wmoDoodadInstId = m2Renderer->createInstanceWithMatrix(
doodad.modelId, doodad.modelMatrix, doodad.worldPosition); doodad.modelId, doodad.modelMatrix, doodad.worldPosition);
if (wmoDoodadInstId) ft.m2InstanceIds.push_back(wmoDoodadInstId); if (wmoDoodadInstId) m2InstanceIds.push_back(wmoDoodadInstId);
ft.wmoDoodadIndex++;
if (ft.wmoDoodadIndex < pending->wmoDoodads.size()) return false;
}
ft.phase = FinalizationPhase::WATER;
return false;
}
case FinalizationPhase::WATER: {
// Terrain water was already loaded in TERRAIN phase.
// Generate water ambient emitters here.
if (ambientSoundManager) {
for (size_t chunkIdx = 0; chunkIdx < pending->terrain.waterData.size(); chunkIdx++) {
const auto& chunkWater = pending->terrain.waterData[chunkIdx];
if (!chunkWater.hasWater()) continue;
int chunkX = chunkIdx % 16;
int chunkY = chunkIdx / 16;
float tileOriginX = (32.0f - x) * 533.33333f;
float tileOriginY = (32.0f - y) * 533.33333f;
float chunkCenterX = tileOriginX + (chunkX + 0.5f) * 33.333333f;
float chunkCenterY = tileOriginY + (chunkY + 0.5f) * 33.333333f;
if (!chunkWater.layers.empty()) {
const auto& layer = chunkWater.layers[0];
float waterHeight = layer.minHeight;
if (layer.liquidType == 0 && chunkIdx % 32 == 0) {
PendingTile::AmbientEmitter emitter;
emitter.position = glm::vec3(chunkCenterX, chunkCenterY, waterHeight);
emitter.type = 4;
pending->ambientEmitters.push_back(emitter);
} else if (layer.liquidType == 1 && chunkIdx % 64 == 0) {
PendingTile::AmbientEmitter emitter;
emitter.position = glm::vec3(chunkCenterX, chunkCenterY, waterHeight);
emitter.type = 4;
pending->ambientEmitters.push_back(emitter);
}
}
} }
} }
ft.phase = FinalizationPhase::AMBIENT; if (loadedWMOs > 0) {
return false; LOG_DEBUG(" Loaded WMOs for tile [", x, ",", y, "]: ", loadedWMOs);
}
} }
case FinalizationPhase::AMBIENT: { // Register ambient sound emitters with ambient sound manager
// Register ambient sound emitters
if (ambientSoundManager && !pending->ambientEmitters.empty()) { if (ambientSoundManager && !pending->ambientEmitters.empty()) {
for (const auto& emitter : pending->ambientEmitters) { for (const auto& emitter : pending->ambientEmitters) {
// Cast uint32_t type to AmbientSoundManager::AmbientType enum
auto type = static_cast<audio::AmbientSoundManager::AmbientType>(emitter.type); auto type = static_cast<audio::AmbientSoundManager::AmbientType>(emitter.type);
ambientSoundManager->addEmitter(emitter.position, type); ambientSoundManager->addEmitter(emitter.position, type);
} }
} }
// Commit tile to loadedTiles // Create tile entry
auto tile = std::make_unique<TerrainTile>(); auto tile = std::make_unique<TerrainTile>();
tile->coord = coord; tile->coord = coord;
tile->terrain = std::move(pending->terrain); tile->terrain = std::move(pending->terrain);
tile->mesh = std::move(pending->mesh); tile->mesh = std::move(pending->mesh);
tile->loaded = true; tile->loaded = true;
tile->m2InstanceIds = std::move(ft.m2InstanceIds); tile->m2InstanceIds = std::move(m2InstanceIds);
tile->wmoInstanceIds = std::move(ft.wmoInstanceIds); tile->wmoInstanceIds = std::move(wmoInstanceIds);
tile->wmoUniqueIds = std::move(ft.tileWmoUniqueIds); tile->wmoUniqueIds = std::move(tileWmoUniqueIds);
tile->doodadUniqueIds = std::move(ft.tileUniqueIds); tile->doodadUniqueIds = std::move(tileUniqueIds);
// Calculate world bounds
getTileBounds(coord, tile->minX, tile->minY, tile->maxX, tile->maxY); getTileBounds(coord, tile->minX, tile->minY, tile->maxX, tile->maxY);
loadedTiles[coord] = std::move(tile); loadedTiles[coord] = std::move(tile);
putCachedTile(pending); putCachedTile(pending);
// Now safe to remove from pendingTiles (tile is in loadedTiles)
{
std::lock_guard<std::mutex> lock(queueMutex);
pendingTiles.erase(coord);
}
LOG_DEBUG(" Finalized tile [", x, ",", y, "]"); LOG_DEBUG(" Finalized tile [", x, ",", y, "]");
ft.phase = FinalizationPhase::DONE;
return true;
}
case FinalizationPhase::DONE:
return true;
}
return true;
} }
void TerrainManager::workerLoop() { void TerrainManager::workerLoop() {
@ -965,59 +927,80 @@ void TerrainManager::processReadyTiles() {
// Taxi mode gets a slightly larger budget to avoid visible late-pop terrain/models. // Taxi mode gets a slightly larger budget to avoid visible late-pop terrain/models.
const float timeBudgetMs = taxiStreamingMode_ ? 8.0f : 5.0f; const float timeBudgetMs = taxiStreamingMode_ ? 8.0f : 5.0f;
auto startTime = std::chrono::high_resolution_clock::now(); auto startTime = std::chrono::high_resolution_clock::now();
int processed = 0;
while (true) {
std::shared_ptr<PendingTile> pending;
// Move newly ready tiles into the finalizing deque.
// Keep them in pendingTiles so streamTiles() won't re-enqueue them.
{ {
std::lock_guard<std::mutex> lock(queueMutex); std::lock_guard<std::mutex> lock(queueMutex);
while (!readyQueue.empty()) { if (readyQueue.empty()) {
auto pending = readyQueue.front(); break;
}
pending = readyQueue.front();
readyQueue.pop(); readyQueue.pop();
if (pending) {
FinalizingTile ft;
ft.pending = std::move(pending);
finalizingTiles_.push_back(std::move(ft));
}
}
} }
// Finalize one complete tile per iteration, check budget between tiles. if (pending) {
// Each tile runs through all phases before moving to the next — this TileCoord coord = pending->coord;
// avoids subtle state issues from spreading GPU uploads across frames.
while (!finalizingTiles_.empty()) { finalizeTile(pending);
auto& ft = finalizingTiles_.front();
while (!advanceFinalization(ft)) {}
finalizingTiles_.pop_front();
auto now = std::chrono::high_resolution_clock::now(); auto now = std::chrono::high_resolution_clock::now();
{
std::lock_guard<std::mutex> lock(queueMutex);
pendingTiles.erase(coord);
}
processed++;
// Check if we've exceeded time budget
float elapsedMs = std::chrono::duration<float, std::milli>(now - startTime).count(); float elapsedMs = std::chrono::duration<float, std::milli>(now - startTime).count();
if (elapsedMs >= timeBudgetMs) { if (elapsedMs >= timeBudgetMs) {
if (processed > 1) {
LOG_DEBUG("Processed ", processed, " tiles in ", elapsedMs, "ms (budget: ", timeBudgetMs, "ms)");
}
break; break;
} }
} }
}
}
void TerrainManager::processM2UploadQueue() {
// Upload up to MAX_M2_UPLOADS_PER_FRAME models per frame
int uploaded = 0;
while (!m2UploadQueue_.empty() && uploaded < MAX_M2_UPLOADS_PER_FRAME) {
auto& upload = m2UploadQueue_.front();
if (m2Renderer) {
m2Renderer->loadModel(upload.model, upload.modelId);
}
m2UploadQueue_.pop();
uploaded++;
}
if (uploaded > 0) {
LOG_DEBUG("Uploaded ", uploaded, " M2 models (", m2UploadQueue_.size(), " remaining in queue)");
}
} }
void TerrainManager::processAllReadyTiles() { void TerrainManager::processAllReadyTiles() {
// Move all ready tiles into finalizing deque while (true) {
// Keep in pendingTiles until committed (same as processReadyTiles) std::shared_ptr<PendingTile> pending;
{ {
std::lock_guard<std::mutex> lock(queueMutex); std::lock_guard<std::mutex> lock(queueMutex);
while (!readyQueue.empty()) { if (readyQueue.empty()) break;
auto pending = readyQueue.front(); pending = readyQueue.front();
readyQueue.pop(); readyQueue.pop();
}
if (pending) { if (pending) {
FinalizingTile ft; TileCoord coord = pending->coord;
ft.pending = std::move(pending); finalizeTile(pending);
finalizingTiles_.push_back(std::move(ft)); {
std::lock_guard<std::mutex> lock(queueMutex);
pendingTiles.erase(coord);
} }
} }
} }
// Finalize all tiles completely (no time budget — used for loading screens)
while (!finalizingTiles_.empty()) {
auto& ft = finalizingTiles_.front();
while (!advanceFinalization(ft)) {}
finalizingTiles_.pop_front();
}
} }
std::shared_ptr<PendingTile> TerrainManager::getCachedTile(const TileCoord& coord) { std::shared_ptr<PendingTile> TerrainManager::getCachedTile(const TileCoord& coord) {
@ -1116,31 +1099,6 @@ void TerrainManager::unloadTile(int x, int y) {
pendingTiles.erase(coord); pendingTiles.erase(coord);
} }
// Remove from finalizingTiles_ if it's being incrementally finalized.
// Water may have already been loaded in TERRAIN phase, so clean it up.
for (auto fit = finalizingTiles_.begin(); fit != finalizingTiles_.end(); ++fit) {
if (fit->pending && fit->pending->coord == coord) {
// If past TERRAIN phase, water was already loaded — remove it
if (fit->phase != FinalizationPhase::TERRAIN && waterRenderer) {
waterRenderer->removeTile(x, y);
}
// Clean up any M2/WMO instances that were already created
if (m2Renderer && !fit->m2InstanceIds.empty()) {
m2Renderer->removeInstances(fit->m2InstanceIds);
}
if (wmoRenderer && !fit->wmoInstanceIds.empty()) {
for (uint32_t id : fit->wmoInstanceIds) {
if (waterRenderer) waterRenderer->removeWMO(id);
}
wmoRenderer->removeInstances(fit->wmoInstanceIds);
}
for (uint32_t uid : fit->tileUniqueIds) placedDoodadIds.erase(uid);
for (uint32_t uid : fit->tileWmoUniqueIds) placedWmoIds.erase(uid);
finalizingTiles_.erase(fit);
return;
}
}
auto it = loadedTiles.find(coord); auto it = loadedTiles.find(coord);
if (it == loadedTiles.end()) { if (it == loadedTiles.end()) {
return; return;
@ -1209,7 +1167,6 @@ void TerrainManager::unloadAll() {
while (!readyQueue.empty()) readyQueue.pop(); while (!readyQueue.empty()) readyQueue.pop();
} }
pendingTiles.clear(); pendingTiles.clear();
finalizingTiles_.clear();
placedDoodadIds.clear(); placedDoodadIds.clear();
LOG_INFO("Unloading all terrain tiles"); LOG_INFO("Unloading all terrain tiles");