2026-02-02 12:24:50 -08:00
|
|
|
#include "rendering/terrain_manager.hpp"
|
|
|
|
|
#include "rendering/terrain_renderer.hpp"
|
2026-03-07 12:32:39 -08:00
|
|
|
#include "rendering/vk_context.hpp"
|
2026-02-02 12:24:50 -08:00
|
|
|
#include "rendering/water_renderer.hpp"
|
|
|
|
|
#include "rendering/m2_renderer.hpp"
|
|
|
|
|
#include "rendering/wmo_renderer.hpp"
|
|
|
|
|
#include "rendering/camera.hpp"
|
2026-02-09 14:50:14 -08:00
|
|
|
#include "audio/ambient_sound_manager.hpp"
|
2026-02-04 17:37:28 -08:00
|
|
|
#include "core/coordinates.hpp"
|
2026-02-08 23:15:26 -08:00
|
|
|
#include "core/memory_monitor.hpp"
|
2026-02-02 12:24:50 -08:00
|
|
|
#include "pipeline/asset_manager.hpp"
|
|
|
|
|
#include "pipeline/adt_loader.hpp"
|
|
|
|
|
#include "pipeline/m2_loader.hpp"
|
|
|
|
|
#include "pipeline/wmo_loader.hpp"
|
|
|
|
|
#include "pipeline/terrain_mesh.hpp"
|
|
|
|
|
#include "core/logger.hpp"
|
|
|
|
|
#include <glm/gtc/matrix_transform.hpp>
|
|
|
|
|
#include <glm/gtc/quaternion.hpp>
|
|
|
|
|
#include <glm/gtx/euler_angles.hpp>
|
|
|
|
|
#include <cmath>
|
|
|
|
|
#include <cctype>
|
2026-02-22 08:12:08 -08:00
|
|
|
#include <cstdlib>
|
2026-02-02 12:24:50 -08:00
|
|
|
#include <functional>
|
|
|
|
|
#include <unordered_set>
|
|
|
|
|
|
2026-02-25 03:39:45 -08:00
|
|
|
#ifdef __linux__
|
|
|
|
|
#include <sched.h>
|
|
|
|
|
#include <pthread.h>
|
2026-02-25 03:41:18 -08:00
|
|
|
#elif defined(_WIN32)
|
|
|
|
|
#ifndef WIN32_LEAN_AND_MEAN
|
|
|
|
|
#define WIN32_LEAN_AND_MEAN
|
|
|
|
|
#endif
|
|
|
|
|
#include <windows.h>
|
|
|
|
|
#elif defined(__APPLE__)
|
|
|
|
|
#include <mach/mach.h>
|
|
|
|
|
#include <mach/thread_policy.h>
|
|
|
|
|
#include <pthread.h>
|
2026-02-25 03:39:45 -08:00
|
|
|
#endif
|
|
|
|
|
|
2026-02-02 12:24:50 -08:00
|
|
|
namespace wowee {
|
|
|
|
|
namespace rendering {
|
|
|
|
|
|
2026-02-03 14:55:32 -08:00
|
|
|
namespace {
|
|
|
|
|
|
2026-02-22 08:12:08 -08:00
|
|
|
int computeTerrainWorkerCount() {
|
|
|
|
|
const char* raw = std::getenv("WOWEE_TERRAIN_WORKERS");
|
|
|
|
|
if (raw && *raw) {
|
|
|
|
|
char* end = nullptr;
|
|
|
|
|
unsigned long long forced = std::strtoull(raw, &end, 10);
|
|
|
|
|
if (end != raw && forced > 0) {
|
|
|
|
|
return static_cast<int>(forced);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
unsigned hc = std::thread::hardware_concurrency();
|
|
|
|
|
if (hc > 0) {
|
2026-03-07 12:32:39 -08:00
|
|
|
// Use most cores for loading — leave 1-2 for render/update threads.
|
|
|
|
|
const unsigned reserved = (hc >= 8u) ? 2u : 1u;
|
|
|
|
|
const unsigned targetWorkers = std::max(4u, hc - reserved);
|
2026-02-22 08:12:08 -08:00
|
|
|
return static_cast<int>(targetWorkers);
|
|
|
|
|
}
|
2026-03-07 12:32:39 -08:00
|
|
|
return 4; // Fallback
|
2026-02-22 08:12:08 -08:00
|
|
|
}
|
|
|
|
|
|
2026-02-03 14:55:32 -08:00
|
|
|
bool decodeLayerAlpha(const pipeline::MapChunk& chunk, size_t layerIdx, std::vector<uint8_t>& outAlpha) {
|
|
|
|
|
if (layerIdx >= chunk.layers.size()) return false;
|
|
|
|
|
const auto& layer = chunk.layers[layerIdx];
|
|
|
|
|
if (!layer.useAlpha() || layer.offsetMCAL >= chunk.alphaMap.size()) return false;
|
|
|
|
|
|
|
|
|
|
size_t offset = layer.offsetMCAL;
|
|
|
|
|
size_t layerSize = chunk.alphaMap.size() - offset;
|
|
|
|
|
for (size_t j = layerIdx + 1; j < chunk.layers.size(); j++) {
|
|
|
|
|
if (chunk.layers[j].useAlpha()) {
|
|
|
|
|
layerSize = chunk.layers[j].offsetMCAL - offset;
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
outAlpha.assign(4096, 255);
|
|
|
|
|
|
|
|
|
|
if (layer.compressedAlpha()) {
|
|
|
|
|
size_t readPos = offset;
|
|
|
|
|
size_t writePos = 0;
|
|
|
|
|
while (writePos < 4096 && readPos < chunk.alphaMap.size()) {
|
|
|
|
|
uint8_t cmd = chunk.alphaMap[readPos++];
|
|
|
|
|
bool fill = (cmd & 0x80) != 0;
|
|
|
|
|
int count = (cmd & 0x7F) + 1;
|
|
|
|
|
|
|
|
|
|
if (fill) {
|
|
|
|
|
if (readPos >= chunk.alphaMap.size()) break;
|
|
|
|
|
uint8_t val = chunk.alphaMap[readPos++];
|
|
|
|
|
for (int i = 0; i < count && writePos < 4096; i++) {
|
|
|
|
|
outAlpha[writePos++] = val;
|
|
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
for (int i = 0; i < count && writePos < 4096 && readPos < chunk.alphaMap.size(); i++) {
|
|
|
|
|
outAlpha[writePos++] = chunk.alphaMap[readPos++];
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (layerSize >= 4096) {
|
|
|
|
|
std::copy(chunk.alphaMap.begin() + offset, chunk.alphaMap.begin() + offset + 4096, outAlpha.begin());
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (layerSize >= 2048) {
|
|
|
|
|
for (size_t i = 0; i < 2048; i++) {
|
|
|
|
|
uint8_t v = chunk.alphaMap[offset + i];
|
|
|
|
|
outAlpha[i * 2] = (v & 0x0F) * 17;
|
|
|
|
|
outAlpha[i * 2 + 1] = (v >> 4) * 17;
|
|
|
|
|
}
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-21 01:26:16 -08:00
|
|
|
std::string toLowerCopy(std::string v) {
|
|
|
|
|
std::transform(v.begin(), v.end(), v.begin(),
|
|
|
|
|
[](unsigned char c) { return static_cast<char>(std::tolower(c)); });
|
|
|
|
|
return v;
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-03 14:55:32 -08:00
|
|
|
} // namespace
|
|
|
|
|
|
2026-02-02 12:24:50 -08:00
|
|
|
TerrainManager::TerrainManager() {
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
TerrainManager::~TerrainManager() {
|
2026-02-26 13:38:29 -08:00
|
|
|
stopWorkers();
|
2026-02-02 12:24:50 -08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
bool TerrainManager::initialize(pipeline::AssetManager* assets, TerrainRenderer* renderer) {
|
|
|
|
|
assetManager = assets;
|
|
|
|
|
terrainRenderer = renderer;
|
|
|
|
|
|
|
|
|
|
if (!assetManager) {
|
|
|
|
|
LOG_ERROR("Asset manager is null");
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (!terrainRenderer) {
|
|
|
|
|
LOG_ERROR("Terrain renderer is null");
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-11 19:28:15 -08:00
|
|
|
// Set dynamic tile cache budget.
|
|
|
|
|
// Keep this lower so decompressed MPQ file cache can stay very aggressive.
|
2026-02-08 23:15:26 -08:00
|
|
|
auto& memMonitor = core::MemoryMonitor::getInstance();
|
2026-02-11 19:28:15 -08:00
|
|
|
tileCacheBudgetBytes_ = memMonitor.getRecommendedCacheBudget() / 4;
|
2026-02-08 23:15:26 -08:00
|
|
|
LOG_INFO("Terrain tile cache budget: ", tileCacheBudgetBytes_ / (1024 * 1024), " MB (dynamic)");
|
|
|
|
|
|
|
|
|
|
// Start background worker pool (dynamic: scales with available cores)
|
2026-02-22 08:12:08 -08:00
|
|
|
// Keep defaults moderate; env override can increase if streaming is bottlenecked.
|
2026-02-02 12:24:50 -08:00
|
|
|
workerRunning.store(true);
|
2026-02-22 08:12:08 -08:00
|
|
|
workerCount = computeTerrainWorkerCount();
|
2026-02-03 17:21:04 -08:00
|
|
|
workerThreads.reserve(workerCount);
|
|
|
|
|
for (int i = 0; i < workerCount; i++) {
|
|
|
|
|
workerThreads.emplace_back(&TerrainManager::workerLoop, this);
|
|
|
|
|
}
|
2026-02-02 12:24:50 -08:00
|
|
|
|
|
|
|
|
LOG_INFO("Terrain manager initialized (async loading enabled)");
|
|
|
|
|
LOG_INFO(" Map: ", mapName);
|
|
|
|
|
LOG_INFO(" Load radius: ", loadRadius, " tiles");
|
|
|
|
|
LOG_INFO(" Unload radius: ", unloadRadius, " tiles");
|
2026-02-03 17:21:04 -08:00
|
|
|
LOG_INFO(" Workers: ", workerCount);
|
2026-02-02 12:24:50 -08:00
|
|
|
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void TerrainManager::update(const Camera& camera, float deltaTime) {
|
|
|
|
|
if (!streamingEnabled || !assetManager || !terrainRenderer) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Always process ready tiles each frame (GPU uploads from background thread)
|
2026-02-10 19:30:45 -08:00
|
|
|
// Time budget prevents frame spikes from heavy tiles
|
2026-02-02 12:24:50 -08:00
|
|
|
processReadyTiles();
|
|
|
|
|
|
|
|
|
|
timeSinceLastUpdate += deltaTime;
|
|
|
|
|
|
|
|
|
|
// Only update streaming periodically (not every frame)
|
|
|
|
|
if (timeSinceLastUpdate < updateInterval) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
timeSinceLastUpdate = 0.0f;
|
|
|
|
|
|
2026-02-04 17:37:28 -08:00
|
|
|
// Get current tile from camera position.
|
2026-02-02 12:24:50 -08:00
|
|
|
glm::vec3 camPos = camera.getPosition();
|
2026-02-04 17:37:28 -08:00
|
|
|
TileCoord newTile = worldToTile(camPos.x, camPos.y);
|
2026-02-02 12:24:50 -08:00
|
|
|
|
|
|
|
|
// Check if we've moved to a different tile
|
|
|
|
|
if (newTile.x != currentTile.x || newTile.y != currentTile.y) {
|
|
|
|
|
LOG_DEBUG("Camera moved to tile [", newTile.x, ",", newTile.y, "]");
|
|
|
|
|
currentTile = newTile;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Stream tiles if we've moved significantly or initial load
|
|
|
|
|
if (newTile.x != lastStreamTile.x || newTile.y != lastStreamTile.y) {
|
2026-02-03 17:21:04 -08:00
|
|
|
LOG_DEBUG("Streaming: cam=(", camPos.x, ",", camPos.y, ",", camPos.z,
|
2026-02-02 12:24:50 -08:00
|
|
|
") tile=[", newTile.x, ",", newTile.y,
|
|
|
|
|
"] loaded=", loadedTiles.size());
|
|
|
|
|
streamTiles();
|
|
|
|
|
lastStreamTile = newTile;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Synchronous fallback for initial tile loading (before worker thread is useful)
|
|
|
|
|
bool TerrainManager::loadTile(int x, int y) {
|
|
|
|
|
TileCoord coord = {x, y};
|
|
|
|
|
|
|
|
|
|
// Check if already loaded
|
|
|
|
|
if (loadedTiles.find(coord) != loadedTiles.end()) {
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Don't retry tiles that already failed
|
|
|
|
|
if (failedTiles.find(coord) != failedTiles.end()) {
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
LOG_INFO("Loading terrain tile [", x, ",", y, "] (synchronous)");
|
|
|
|
|
|
|
|
|
|
auto pending = prepareTile(x, y);
|
|
|
|
|
if (!pending) {
|
|
|
|
|
failedTiles[coord] = true;
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-25 03:39:45 -08:00
|
|
|
FinalizingTile ft;
|
|
|
|
|
ft.pending = std::move(pending);
|
|
|
|
|
while (!advanceFinalization(ft)) {}
|
2026-02-02 12:24:50 -08:00
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-06 18:52:28 -08:00
|
|
|
bool TerrainManager::enqueueTile(int x, int y) {
|
|
|
|
|
TileCoord coord = {x, y};
|
|
|
|
|
if (loadedTiles.find(coord) != loadedTiles.end()) {
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
if (pendingTiles.find(coord) != pendingTiles.end()) {
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
if (failedTiles.find(coord) != failedTiles.end()) {
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
{
|
|
|
|
|
std::lock_guard<std::mutex> lock(queueMutex);
|
2026-02-11 19:28:15 -08:00
|
|
|
loadQueue.push_back(coord);
|
2026-02-06 18:52:28 -08:00
|
|
|
pendingTiles[coord] = true;
|
|
|
|
|
}
|
|
|
|
|
queueCV.notify_all();
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-08 03:24:12 -08:00
|
|
|
std::shared_ptr<PendingTile> TerrainManager::prepareTile(int x, int y) {
|
2026-02-02 12:24:50 -08:00
|
|
|
TileCoord coord = {x, y};
|
2026-02-08 03:24:12 -08:00
|
|
|
if (auto cached = getCachedTile(coord)) {
|
|
|
|
|
LOG_DEBUG("Using cached tile [", x, ",", y, "]");
|
|
|
|
|
return cached;
|
|
|
|
|
}
|
2026-02-02 12:24:50 -08:00
|
|
|
|
2026-02-03 17:21:04 -08:00
|
|
|
LOG_DEBUG("Preparing tile [", x, ",", y, "] (CPU work)");
|
2026-02-02 12:24:50 -08:00
|
|
|
|
2026-02-26 13:38:29 -08:00
|
|
|
// Early-exit check — worker should bail fast during shutdown
|
|
|
|
|
if (!workerRunning.load()) return nullptr;
|
|
|
|
|
|
2026-02-02 12:24:50 -08:00
|
|
|
// Load ADT file
|
|
|
|
|
std::string adtPath = getADTPath(coord);
|
|
|
|
|
auto adtData = assetManager->readFile(adtPath);
|
|
|
|
|
|
|
|
|
|
if (adtData.empty()) {
|
2026-02-11 22:27:02 -08:00
|
|
|
logMissingAdtOnce(adtPath);
|
2026-02-02 12:24:50 -08:00
|
|
|
return nullptr;
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-25 01:55:16 -08:00
|
|
|
// Parse ADT — allocate on heap to avoid stack overflow on macOS
|
|
|
|
|
// (ADTTerrain contains std::array<MapChunk,256> ≈ 280 KB; macOS worker
|
|
|
|
|
// threads default to 512 KB stack, so two on-stack copies would overflow)
|
|
|
|
|
auto terrainPtr = std::make_unique<pipeline::ADTTerrain>(pipeline::ADTLoader::load(adtData));
|
|
|
|
|
if (!terrainPtr->isLoaded()) {
|
2026-02-02 12:24:50 -08:00
|
|
|
LOG_ERROR("Failed to parse ADT terrain: ", adtPath);
|
|
|
|
|
return nullptr;
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-26 13:38:29 -08:00
|
|
|
if (!workerRunning.load()) return nullptr;
|
|
|
|
|
|
2026-02-21 01:26:16 -08:00
|
|
|
// WotLK split ADTs can store placements in *_obj0.adt.
|
|
|
|
|
// Merge object chunks so doodads/WMOs (including ground clutter) are available.
|
|
|
|
|
std::string objPath = "World\\Maps\\" + mapName + "\\" + mapName + "_" +
|
|
|
|
|
std::to_string(coord.x) + "_" + std::to_string(coord.y) + "_obj0.adt";
|
|
|
|
|
auto objData = assetManager->readFile(objPath);
|
|
|
|
|
if (!objData.empty()) {
|
2026-02-25 01:55:16 -08:00
|
|
|
auto objTerrain = std::make_unique<pipeline::ADTTerrain>(pipeline::ADTLoader::load(objData));
|
|
|
|
|
if (objTerrain->isLoaded()) {
|
|
|
|
|
const uint32_t doodadNameBase = static_cast<uint32_t>(terrainPtr->doodadNames.size());
|
|
|
|
|
const uint32_t wmoNameBase = static_cast<uint32_t>(terrainPtr->wmoNames.size());
|
2026-02-21 01:26:16 -08:00
|
|
|
|
2026-02-25 01:55:16 -08:00
|
|
|
terrainPtr->doodadNames.insert(terrainPtr->doodadNames.end(),
|
|
|
|
|
objTerrain->doodadNames.begin(), objTerrain->doodadNames.end());
|
|
|
|
|
terrainPtr->wmoNames.insert(terrainPtr->wmoNames.end(),
|
|
|
|
|
objTerrain->wmoNames.begin(), objTerrain->wmoNames.end());
|
2026-02-21 01:26:16 -08:00
|
|
|
|
|
|
|
|
std::unordered_set<uint32_t> existingDoodadUniqueIds;
|
2026-02-25 01:55:16 -08:00
|
|
|
existingDoodadUniqueIds.reserve(terrainPtr->doodadPlacements.size());
|
|
|
|
|
for (const auto& p : terrainPtr->doodadPlacements) {
|
2026-02-21 01:26:16 -08:00
|
|
|
if (p.uniqueId != 0) existingDoodadUniqueIds.insert(p.uniqueId);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
size_t mergedDoodads = 0;
|
2026-02-25 01:55:16 -08:00
|
|
|
for (auto placement : objTerrain->doodadPlacements) {
|
|
|
|
|
if (placement.nameId >= objTerrain->doodadNames.size()) continue;
|
2026-02-21 01:26:16 -08:00
|
|
|
placement.nameId += doodadNameBase;
|
|
|
|
|
if (placement.uniqueId != 0 && !existingDoodadUniqueIds.insert(placement.uniqueId).second) {
|
|
|
|
|
continue;
|
|
|
|
|
}
|
2026-02-25 01:55:16 -08:00
|
|
|
terrainPtr->doodadPlacements.push_back(placement);
|
2026-02-21 01:26:16 -08:00
|
|
|
mergedDoodads++;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
std::unordered_set<uint32_t> existingWmoUniqueIds;
|
2026-02-25 01:55:16 -08:00
|
|
|
existingWmoUniqueIds.reserve(terrainPtr->wmoPlacements.size());
|
|
|
|
|
for (const auto& p : terrainPtr->wmoPlacements) {
|
2026-02-21 01:26:16 -08:00
|
|
|
if (p.uniqueId != 0) existingWmoUniqueIds.insert(p.uniqueId);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
size_t mergedWmos = 0;
|
2026-02-25 01:55:16 -08:00
|
|
|
for (auto placement : objTerrain->wmoPlacements) {
|
|
|
|
|
if (placement.nameId >= objTerrain->wmoNames.size()) continue;
|
2026-02-21 01:26:16 -08:00
|
|
|
placement.nameId += wmoNameBase;
|
|
|
|
|
if (placement.uniqueId != 0 && !existingWmoUniqueIds.insert(placement.uniqueId).second) {
|
|
|
|
|
continue;
|
|
|
|
|
}
|
2026-02-25 01:55:16 -08:00
|
|
|
terrainPtr->wmoPlacements.push_back(placement);
|
2026-02-21 01:26:16 -08:00
|
|
|
mergedWmos++;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (mergedDoodads > 0 || mergedWmos > 0) {
|
|
|
|
|
LOG_DEBUG("Merged obj0 tile [", x, ",", y, "]: +", mergedDoodads,
|
|
|
|
|
" doodads, +", mergedWmos, " WMOs");
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-02 12:24:50 -08:00
|
|
|
// Set tile coordinates so mesh knows where to position this tile in world
|
2026-02-25 01:55:16 -08:00
|
|
|
terrainPtr->coord.x = x;
|
|
|
|
|
terrainPtr->coord.y = y;
|
2026-02-02 12:24:50 -08:00
|
|
|
|
|
|
|
|
// Generate mesh
|
2026-02-25 01:55:16 -08:00
|
|
|
pipeline::TerrainMesh mesh = pipeline::TerrainMeshGenerator::generate(*terrainPtr);
|
2026-02-02 12:24:50 -08:00
|
|
|
if (mesh.validChunkCount == 0) {
|
|
|
|
|
LOG_ERROR("Failed to generate terrain mesh: ", adtPath);
|
|
|
|
|
return nullptr;
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-26 13:38:29 -08:00
|
|
|
if (!workerRunning.load()) return nullptr;
|
|
|
|
|
|
2026-02-08 03:24:12 -08:00
|
|
|
auto pending = std::make_shared<PendingTile>();
|
2026-02-02 12:24:50 -08:00
|
|
|
pending->coord = coord;
|
2026-02-25 01:55:16 -08:00
|
|
|
pending->terrain = std::move(*terrainPtr);
|
2026-02-02 12:24:50 -08:00
|
|
|
pending->mesh = std::move(mesh);
|
|
|
|
|
|
2026-02-21 01:26:16 -08:00
|
|
|
std::unordered_set<uint32_t> preparedModelIds;
|
|
|
|
|
auto ensureModelPrepared = [&](const std::string& m2Path,
|
|
|
|
|
uint32_t modelId,
|
|
|
|
|
int& skippedFileNotFound,
|
|
|
|
|
int& skippedInvalid,
|
|
|
|
|
int& skippedSkinNotFound) -> bool {
|
|
|
|
|
if (preparedModelIds.find(modelId) != preparedModelIds.end()) return true;
|
|
|
|
|
|
2026-03-07 12:32:39 -08:00
|
|
|
// Skip file I/O + parsing for models already uploaded to GPU from previous tiles
|
|
|
|
|
{
|
|
|
|
|
std::lock_guard<std::mutex> lock(uploadedM2IdsMutex_);
|
|
|
|
|
if (uploadedM2Ids_.count(modelId)) {
|
|
|
|
|
preparedModelIds.insert(modelId);
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-21 01:26:16 -08:00
|
|
|
std::vector<uint8_t> m2Data = assetManager->readFile(m2Path);
|
|
|
|
|
if (m2Data.empty()) {
|
|
|
|
|
skippedFileNotFound++;
|
|
|
|
|
LOG_WARNING("M2 file not found: ", m2Path);
|
|
|
|
|
return false;
|
|
|
|
|
}
|
2026-02-02 12:24:50 -08:00
|
|
|
|
2026-02-21 01:26:16 -08:00
|
|
|
pipeline::M2Model m2Model = pipeline::M2Loader::load(m2Data);
|
|
|
|
|
if (m2Model.name.empty()) {
|
|
|
|
|
m2Model.name = m2Path;
|
|
|
|
|
}
|
|
|
|
|
std::string skinPath = m2Path.substr(0, m2Path.size() - 3) + "00.skin";
|
|
|
|
|
std::vector<uint8_t> skinData = assetManager->readFileOptional(skinPath);
|
|
|
|
|
if (!skinData.empty() && m2Model.version >= 264) {
|
|
|
|
|
pipeline::M2Loader::loadSkin(skinData, m2Model);
|
|
|
|
|
} else if (skinData.empty() && m2Model.version >= 264) {
|
|
|
|
|
skippedSkinNotFound++;
|
|
|
|
|
}
|
2026-02-02 12:24:50 -08:00
|
|
|
|
2026-02-21 01:26:16 -08:00
|
|
|
if (!m2Model.isValid()) {
|
|
|
|
|
skippedInvalid++;
|
|
|
|
|
LOG_DEBUG("M2 model invalid (no verts/indices): ", m2Path);
|
|
|
|
|
return false;
|
|
|
|
|
}
|
2026-02-02 12:24:50 -08:00
|
|
|
|
2026-02-21 01:26:16 -08:00
|
|
|
PendingTile::M2Ready ready;
|
|
|
|
|
ready.modelId = modelId;
|
|
|
|
|
ready.model = std::move(m2Model);
|
|
|
|
|
ready.path = m2Path;
|
|
|
|
|
pending->m2Models.push_back(std::move(ready));
|
|
|
|
|
preparedModelIds.insert(modelId);
|
|
|
|
|
return true;
|
|
|
|
|
};
|
2026-02-02 12:24:50 -08:00
|
|
|
|
2026-02-21 01:26:16 -08:00
|
|
|
// Pre-load M2 doodads (CPU: read files, parse models)
|
|
|
|
|
int skippedNameId = 0, skippedFileNotFound = 0, skippedInvalid = 0, skippedSkinNotFound = 0;
|
|
|
|
|
for (const auto& placement : pending->terrain.doodadPlacements) {
|
2026-02-26 13:38:29 -08:00
|
|
|
if (!workerRunning.load()) return nullptr;
|
2026-02-21 01:26:16 -08:00
|
|
|
if (placement.nameId >= pending->terrain.doodadNames.size()) {
|
|
|
|
|
skippedNameId++;
|
|
|
|
|
continue;
|
|
|
|
|
}
|
2026-02-02 12:24:50 -08:00
|
|
|
|
2026-02-21 01:26:16 -08:00
|
|
|
std::string m2Path = pending->terrain.doodadNames[placement.nameId];
|
|
|
|
|
if (m2Path.size() > 4) {
|
|
|
|
|
std::string ext = toLowerCopy(m2Path.substr(m2Path.size() - 4));
|
|
|
|
|
if (ext == ".mdx") {
|
|
|
|
|
m2Path = m2Path.substr(0, m2Path.size() - 4) + ".m2";
|
2026-02-02 12:24:50 -08:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-21 01:26:16 -08:00
|
|
|
uint32_t modelId = static_cast<uint32_t>(std::hash<std::string>{}(m2Path));
|
|
|
|
|
if (!ensureModelPrepared(m2Path, modelId, skippedFileNotFound, skippedInvalid, skippedSkinNotFound)) {
|
|
|
|
|
continue;
|
2026-02-02 12:24:50 -08:00
|
|
|
}
|
2026-02-21 01:26:16 -08:00
|
|
|
|
|
|
|
|
float wowX = placement.position[0];
|
|
|
|
|
float wowY = placement.position[1];
|
|
|
|
|
float wowZ = placement.position[2];
|
|
|
|
|
glm::vec3 glPos = core::coords::adtToWorld(wowX, wowY, wowZ);
|
|
|
|
|
|
|
|
|
|
PendingTile::M2Placement p;
|
|
|
|
|
p.modelId = modelId;
|
|
|
|
|
p.uniqueId = placement.uniqueId;
|
|
|
|
|
p.position = glPos;
|
|
|
|
|
p.rotation = glm::vec3(
|
|
|
|
|
-placement.rotation[2] * 3.14159f / 180.0f,
|
|
|
|
|
-placement.rotation[0] * 3.14159f / 180.0f,
|
|
|
|
|
(placement.rotation[1] + 180.0f) * 3.14159f / 180.0f
|
|
|
|
|
);
|
|
|
|
|
p.scale = placement.scale / 1024.0f;
|
|
|
|
|
pending->m2Placements.push_back(p);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (skippedNameId > 0 || skippedFileNotFound > 0 || skippedInvalid > 0 || skippedSkinNotFound > 0) {
|
|
|
|
|
LOG_DEBUG("Tile [", x, ",", y, "] doodad issues: ",
|
|
|
|
|
skippedNameId, " bad nameId, ",
|
|
|
|
|
skippedFileNotFound, " file not found, ",
|
|
|
|
|
skippedInvalid, " invalid model, ",
|
|
|
|
|
skippedSkinNotFound, " skin not found");
|
2026-02-02 12:24:50 -08:00
|
|
|
}
|
|
|
|
|
|
2026-02-21 01:26:16 -08:00
|
|
|
// Procedural ground clutter from terrain layer effectId -> GroundEffectTexture/Doodad DBCs.
|
|
|
|
|
ensureGroundEffectTablesLoaded();
|
|
|
|
|
generateGroundClutterPlacements(pending, preparedModelIds);
|
|
|
|
|
|
2026-02-26 13:38:29 -08:00
|
|
|
if (!workerRunning.load()) return nullptr;
|
|
|
|
|
|
2026-02-02 12:24:50 -08:00
|
|
|
// Pre-load WMOs (CPU: read files, parse models and groups)
|
|
|
|
|
if (!pending->terrain.wmoPlacements.empty()) {
|
|
|
|
|
for (const auto& placement : pending->terrain.wmoPlacements) {
|
2026-02-26 13:38:29 -08:00
|
|
|
if (!workerRunning.load()) return nullptr;
|
2026-02-02 12:24:50 -08:00
|
|
|
if (placement.nameId >= pending->terrain.wmoNames.size()) continue;
|
|
|
|
|
|
|
|
|
|
const std::string& wmoPath = pending->terrain.wmoNames[placement.nameId];
|
|
|
|
|
std::vector<uint8_t> wmoData = assetManager->readFile(wmoPath);
|
|
|
|
|
if (wmoData.empty()) continue;
|
|
|
|
|
|
|
|
|
|
pipeline::WMOModel wmoModel = pipeline::WMOLoader::load(wmoData);
|
|
|
|
|
if (wmoModel.nGroups > 0) {
|
|
|
|
|
std::string basePath = wmoPath;
|
|
|
|
|
std::string extension;
|
|
|
|
|
if (basePath.size() > 4) {
|
|
|
|
|
extension = basePath.substr(basePath.size() - 4);
|
|
|
|
|
std::string extLower = extension;
|
|
|
|
|
for (char& c : extLower) c = std::tolower(c);
|
|
|
|
|
if (extLower == ".wmo") {
|
|
|
|
|
basePath = basePath.substr(0, basePath.size() - 4);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
for (uint32_t gi = 0; gi < wmoModel.nGroups; gi++) {
|
|
|
|
|
char groupSuffix[16];
|
|
|
|
|
snprintf(groupSuffix, sizeof(groupSuffix), "_%03u%s", gi, extension.c_str());
|
|
|
|
|
std::string groupPath = basePath + groupSuffix;
|
|
|
|
|
std::vector<uint8_t> groupData = assetManager->readFile(groupPath);
|
|
|
|
|
if (groupData.empty()) {
|
|
|
|
|
snprintf(groupSuffix, sizeof(groupSuffix), "_%03u.wmo", gi);
|
|
|
|
|
groupData = assetManager->readFile(basePath + groupSuffix);
|
|
|
|
|
}
|
|
|
|
|
if (groupData.empty()) {
|
|
|
|
|
snprintf(groupSuffix, sizeof(groupSuffix), "_%03u.WMO", gi);
|
|
|
|
|
groupData = assetManager->readFile(basePath + groupSuffix);
|
|
|
|
|
}
|
|
|
|
|
if (!groupData.empty()) {
|
|
|
|
|
pipeline::WMOLoader::loadGroup(groupData, wmoModel, gi);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (!wmoModel.groups.empty()) {
|
2026-02-04 17:37:28 -08:00
|
|
|
glm::vec3 pos = core::coords::adtToWorld(placement.position[0],
|
|
|
|
|
placement.position[1],
|
|
|
|
|
placement.position[2]);
|
2026-02-02 12:24:50 -08:00
|
|
|
|
|
|
|
|
glm::vec3 rot(
|
|
|
|
|
-placement.rotation[2] * 3.14159f / 180.0f,
|
|
|
|
|
-placement.rotation[0] * 3.14159f / 180.0f,
|
|
|
|
|
(placement.rotation[1] + 180.0f) * 3.14159f / 180.0f
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
// Pre-load WMO doodads (M2 models inside WMO)
|
2026-02-26 13:38:29 -08:00
|
|
|
if (!workerRunning.load()) return nullptr;
|
2026-02-02 12:24:50 -08:00
|
|
|
if (!wmoModel.doodadSets.empty() && !wmoModel.doodads.empty()) {
|
|
|
|
|
glm::mat4 wmoMatrix(1.0f);
|
|
|
|
|
wmoMatrix = glm::translate(wmoMatrix, pos);
|
|
|
|
|
wmoMatrix = glm::rotate(wmoMatrix, rot.z, glm::vec3(0, 0, 1));
|
|
|
|
|
wmoMatrix = glm::rotate(wmoMatrix, rot.y, glm::vec3(0, 1, 0));
|
|
|
|
|
wmoMatrix = glm::rotate(wmoMatrix, rot.x, glm::vec3(1, 0, 0));
|
|
|
|
|
|
2026-03-04 19:47:01 -08:00
|
|
|
// Load doodads from set 0 (global) + placement-specific set
|
|
|
|
|
std::vector<uint32_t> setsToLoad = {0};
|
|
|
|
|
if (placement.doodadSet > 0 && placement.doodadSet < wmoModel.doodadSets.size()) {
|
|
|
|
|
setsToLoad.push_back(placement.doodadSet);
|
|
|
|
|
}
|
|
|
|
|
std::unordered_set<uint32_t> loadedDoodadIndices;
|
|
|
|
|
for (uint32_t setIdx : setsToLoad) {
|
|
|
|
|
const auto& doodadSet = wmoModel.doodadSets[setIdx];
|
2026-02-02 12:24:50 -08:00
|
|
|
for (uint32_t di = 0; di < doodadSet.count; di++) {
|
|
|
|
|
uint32_t doodadIdx = doodadSet.startIndex + di;
|
|
|
|
|
if (doodadIdx >= wmoModel.doodads.size()) break;
|
2026-03-04 19:47:01 -08:00
|
|
|
if (!loadedDoodadIndices.insert(doodadIdx).second) continue;
|
2026-02-02 12:24:50 -08:00
|
|
|
|
|
|
|
|
const auto& doodad = wmoModel.doodads[doodadIdx];
|
|
|
|
|
auto nameIt = wmoModel.doodadNames.find(doodad.nameIndex);
|
|
|
|
|
if (nameIt == wmoModel.doodadNames.end()) continue;
|
|
|
|
|
|
|
|
|
|
std::string m2Path = nameIt->second;
|
|
|
|
|
if (m2Path.empty()) continue;
|
|
|
|
|
|
|
|
|
|
if (m2Path.size() > 4) {
|
|
|
|
|
std::string ext = m2Path.substr(m2Path.size() - 4);
|
|
|
|
|
for (char& c : ext) c = std::tolower(c);
|
|
|
|
|
if (ext == ".mdx" || ext == ".mdl") {
|
|
|
|
|
m2Path = m2Path.substr(0, m2Path.size() - 4) + ".m2";
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
uint32_t doodadModelId = static_cast<uint32_t>(std::hash<std::string>{}(m2Path));
|
|
|
|
|
|
2026-03-07 12:32:39 -08:00
|
|
|
// Skip file I/O if model already uploaded from a previous tile
|
|
|
|
|
bool modelAlreadyUploaded = false;
|
|
|
|
|
{
|
|
|
|
|
std::lock_guard<std::mutex> lock(uploadedM2IdsMutex_);
|
|
|
|
|
modelAlreadyUploaded = uploadedM2Ids_.count(doodadModelId) > 0;
|
2026-02-21 01:26:16 -08:00
|
|
|
}
|
2026-03-07 12:32:39 -08:00
|
|
|
|
|
|
|
|
pipeline::M2Model m2Model;
|
|
|
|
|
if (!modelAlreadyUploaded) {
|
|
|
|
|
std::vector<uint8_t> m2Data = assetManager->readFile(m2Path);
|
|
|
|
|
if (m2Data.empty()) continue;
|
|
|
|
|
|
|
|
|
|
m2Model = pipeline::M2Loader::load(m2Data);
|
|
|
|
|
if (m2Model.name.empty()) {
|
|
|
|
|
m2Model.name = m2Path;
|
|
|
|
|
}
|
|
|
|
|
std::string skinPath = m2Path.substr(0, m2Path.size() - 3) + "00.skin";
|
|
|
|
|
std::vector<uint8_t> skinData = assetManager->readFile(skinPath);
|
|
|
|
|
if (!skinData.empty() && m2Model.version >= 264) {
|
|
|
|
|
pipeline::M2Loader::loadSkin(skinData, m2Model);
|
|
|
|
|
}
|
|
|
|
|
if (!m2Model.isValid()) continue;
|
2026-02-02 12:24:50 -08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Build doodad's local transform (WoW coordinates)
|
|
|
|
|
// WMO doodads use quaternion rotation
|
2026-03-06 18:48:12 -08:00
|
|
|
glm::quat fixedRotation(doodad.rotation.w, doodad.rotation.x, doodad.rotation.y, doodad.rotation.z);
|
2026-02-09 00:41:19 -08:00
|
|
|
|
2026-02-02 12:24:50 -08:00
|
|
|
glm::mat4 doodadLocal(1.0f);
|
|
|
|
|
doodadLocal = glm::translate(doodadLocal, doodad.position);
|
2026-02-09 00:41:19 -08:00
|
|
|
doodadLocal *= glm::mat4_cast(fixedRotation);
|
2026-02-02 12:24:50 -08:00
|
|
|
doodadLocal = glm::scale(doodadLocal, glm::vec3(doodad.scale));
|
|
|
|
|
|
|
|
|
|
// Full world transform = WMO world transform * doodad local transform
|
|
|
|
|
glm::mat4 worldMatrix = wmoMatrix * doodadLocal;
|
|
|
|
|
|
|
|
|
|
// Extract world position for frustum culling
|
|
|
|
|
glm::vec3 worldPos = glm::vec3(worldMatrix[3]);
|
|
|
|
|
|
2026-02-09 14:50:14 -08:00
|
|
|
// Detect ambient sound emitters from doodad model path
|
|
|
|
|
std::string m2PathLower = m2Path;
|
|
|
|
|
std::transform(m2PathLower.begin(), m2PathLower.end(), m2PathLower.begin(), ::tolower);
|
|
|
|
|
|
|
|
|
|
// Debug: Log all doodad paths to help identify fire-related models
|
|
|
|
|
static int doodadLogCount = 0;
|
|
|
|
|
if (doodadLogCount < 50) { // Limit logging to first 50 doodads
|
|
|
|
|
LOG_DEBUG("WMO doodad: ", m2Path);
|
|
|
|
|
doodadLogCount++;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (m2PathLower.find("fire") != std::string::npos ||
|
|
|
|
|
m2PathLower.find("brazier") != std::string::npos ||
|
|
|
|
|
m2PathLower.find("campfire") != std::string::npos) {
|
|
|
|
|
// Fireplace/brazier emitter
|
|
|
|
|
PendingTile::AmbientEmitter emitter;
|
|
|
|
|
emitter.position = worldPos;
|
|
|
|
|
if (m2PathLower.find("small") != std::string::npos || m2PathLower.find("campfire") != std::string::npos) {
|
|
|
|
|
emitter.type = 0; // FIREPLACE_SMALL
|
|
|
|
|
} else {
|
|
|
|
|
emitter.type = 1; // FIREPLACE_LARGE
|
|
|
|
|
}
|
|
|
|
|
pending->ambientEmitters.push_back(emitter);
|
|
|
|
|
} else if (m2PathLower.find("torch") != std::string::npos) {
|
|
|
|
|
// Torch emitter
|
|
|
|
|
PendingTile::AmbientEmitter emitter;
|
|
|
|
|
emitter.position = worldPos;
|
|
|
|
|
emitter.type = 2; // TORCH
|
|
|
|
|
pending->ambientEmitters.push_back(emitter);
|
|
|
|
|
} else if (m2PathLower.find("fountain") != std::string::npos) {
|
|
|
|
|
// Fountain emitter
|
|
|
|
|
PendingTile::AmbientEmitter emitter;
|
|
|
|
|
emitter.position = worldPos;
|
|
|
|
|
emitter.type = 3; // FOUNTAIN
|
|
|
|
|
pending->ambientEmitters.push_back(emitter);
|
|
|
|
|
} else if (m2PathLower.find("waterfall") != std::string::npos) {
|
|
|
|
|
// Waterfall emitter
|
|
|
|
|
PendingTile::AmbientEmitter emitter;
|
|
|
|
|
emitter.position = worldPos;
|
|
|
|
|
emitter.type = 6; // WATERFALL
|
|
|
|
|
pending->ambientEmitters.push_back(emitter);
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-02 12:24:50 -08:00
|
|
|
PendingTile::WMODoodadReady doodadReady;
|
|
|
|
|
doodadReady.modelId = doodadModelId;
|
|
|
|
|
doodadReady.model = std::move(m2Model);
|
|
|
|
|
doodadReady.worldPosition = worldPos;
|
|
|
|
|
doodadReady.modelMatrix = worldMatrix;
|
|
|
|
|
pending->wmoDoodads.push_back(std::move(doodadReady));
|
|
|
|
|
}
|
2026-03-04 19:47:01 -08:00
|
|
|
}
|
2026-02-02 12:24:50 -08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
PendingTile::WMOReady ready;
|
2026-02-18 22:36:34 -08:00
|
|
|
// Cache WMO model uploads by path; placement dedup uses uniqueId separately.
|
|
|
|
|
ready.modelId = static_cast<uint32_t>(std::hash<std::string>{}(wmoPath));
|
|
|
|
|
if (ready.modelId == 0) ready.modelId = 1;
|
|
|
|
|
ready.uniqueId = placement.uniqueId;
|
2026-02-02 12:24:50 -08:00
|
|
|
ready.model = std::move(wmoModel);
|
|
|
|
|
ready.position = pos;
|
|
|
|
|
ready.rotation = rot;
|
|
|
|
|
pending->wmoModels.push_back(std::move(ready));
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-26 13:38:29 -08:00
|
|
|
if (!workerRunning.load()) return nullptr;
|
|
|
|
|
|
2026-02-08 01:16:23 -08:00
|
|
|
// Pre-load terrain texture BLP data on background thread so finalizeTile
|
|
|
|
|
// doesn't block the main thread with file I/O.
|
|
|
|
|
for (const auto& texPath : pending->terrain.textures) {
|
|
|
|
|
if (pending->preloadedTextures.find(texPath) != pending->preloadedTextures.end()) continue;
|
|
|
|
|
pending->preloadedTextures[texPath] = assetManager->loadTexture(texPath);
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-03 17:21:04 -08:00
|
|
|
LOG_DEBUG("Prepared tile [", x, ",", y, "]: ",
|
2026-02-02 12:24:50 -08:00
|
|
|
pending->m2Models.size(), " M2 models, ",
|
|
|
|
|
pending->m2Placements.size(), " M2 placements, ",
|
|
|
|
|
pending->wmoModels.size(), " WMOs, ",
|
2026-02-08 01:16:23 -08:00
|
|
|
pending->wmoDoodads.size(), " WMO doodads, ",
|
|
|
|
|
pending->preloadedTextures.size(), " textures");
|
2026-02-02 12:24:50 -08:00
|
|
|
|
|
|
|
|
return pending;
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-11 22:27:02 -08:00
|
|
|
void TerrainManager::logMissingAdtOnce(const std::string& adtPath) {
|
|
|
|
|
std::string normalized = adtPath;
|
|
|
|
|
std::transform(normalized.begin(), normalized.end(), normalized.begin(),
|
|
|
|
|
[](unsigned char c) { return static_cast<char>(std::tolower(c)); });
|
|
|
|
|
|
|
|
|
|
std::lock_guard<std::mutex> lock(missingAdtWarningsMutex_);
|
|
|
|
|
if (missingAdtWarnings_.insert(normalized).second) {
|
|
|
|
|
LOG_WARNING("Failed to load ADT file: ", adtPath);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-25 03:39:45 -08:00
|
|
|
bool TerrainManager::advanceFinalization(FinalizingTile& ft) {
|
|
|
|
|
auto& pending = ft.pending;
|
2026-02-02 12:24:50 -08:00
|
|
|
int x = pending->coord.x;
|
|
|
|
|
int y = pending->coord.y;
|
|
|
|
|
TileCoord coord = pending->coord;
|
|
|
|
|
|
2026-02-25 03:39:45 -08:00
|
|
|
switch (ft.phase) {
|
2026-02-02 12:24:50 -08:00
|
|
|
|
2026-02-25 03:39:45 -08:00
|
|
|
case FinalizationPhase::TERRAIN: {
|
|
|
|
|
// Check if tile was already loaded or failed
|
|
|
|
|
if (loadedTiles.find(coord) != loadedTiles.end() || failedTiles.find(coord) != failedTiles.end()) {
|
|
|
|
|
{
|
|
|
|
|
std::lock_guard<std::mutex> lock(queueMutex);
|
|
|
|
|
pendingTiles.erase(coord);
|
|
|
|
|
}
|
|
|
|
|
ft.phase = FinalizationPhase::DONE;
|
|
|
|
|
return true;
|
|
|
|
|
}
|
2026-02-02 12:24:50 -08:00
|
|
|
|
2026-03-07 11:59:19 -08:00
|
|
|
// Upload pre-loaded textures (once)
|
|
|
|
|
if (!ft.terrainPreloaded) {
|
|
|
|
|
LOG_DEBUG("Finalizing tile [", x, ",", y, "] (incremental)");
|
|
|
|
|
if (!pending->preloadedTextures.empty()) {
|
|
|
|
|
terrainRenderer->uploadPreloadedTextures(pending->preloadedTextures);
|
|
|
|
|
}
|
|
|
|
|
ft.terrainPreloaded = true;
|
|
|
|
|
// Yield after preload to give time budget a chance to interrupt
|
|
|
|
|
return false;
|
2026-02-25 03:39:45 -08:00
|
|
|
}
|
2026-02-02 12:24:50 -08:00
|
|
|
|
2026-03-07 11:59:19 -08:00
|
|
|
// 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;
|
2026-02-09 14:50:14 -08:00
|
|
|
}
|
2026-03-07 11:59:19 -08:00
|
|
|
bool allDone = terrainRenderer->loadTerrainIncremental(
|
|
|
|
|
pending->mesh, pending->terrain.textures, x, y,
|
2026-03-07 12:32:39 -08:00
|
|
|
ft.terrainChunkNext, 64);
|
2026-03-07 11:59:19 -08:00
|
|
|
if (!allDone) {
|
|
|
|
|
return false; // More chunks remain — yield to time budget
|
|
|
|
|
}
|
|
|
|
|
ft.terrainMeshDone = true;
|
2026-02-09 14:50:14 -08:00
|
|
|
}
|
2026-02-25 03:39:45 -08:00
|
|
|
|
2026-03-07 11:59:19 -08:00
|
|
|
// Load water after all terrain chunks are uploaded
|
2026-02-25 03:39:45 -08:00
|
|
|
if (waterRenderer) {
|
2026-02-25 13:26:08 -08:00
|
|
|
size_t beforeSurfaces = waterRenderer->getSurfaceCount();
|
2026-02-25 03:39:45 -08:00
|
|
|
waterRenderer->loadFromTerrain(pending->terrain, true, x, y);
|
2026-02-25 13:26:08 -08:00
|
|
|
size_t afterSurfaces = waterRenderer->getSurfaceCount();
|
|
|
|
|
if (afterSurfaces > beforeSurfaces) {
|
|
|
|
|
LOG_INFO("Water: tile [", x, ",", y, "] added ", afterSurfaces - beforeSurfaces,
|
|
|
|
|
" surfaces (total: ", afterSurfaces, ")");
|
|
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
LOG_WARNING("Water: waterRenderer is null during tile [", x, ",", y, "] finalization!");
|
2026-02-09 14:50:14 -08:00
|
|
|
}
|
|
|
|
|
|
2026-02-25 03:39:45 -08:00
|
|
|
// Ensure M2 renderer has asset manager
|
|
|
|
|
if (m2Renderer && assetManager) {
|
|
|
|
|
m2Renderer->initialize(nullptr, VK_NULL_HANDLE, assetManager);
|
|
|
|
|
}
|
2026-02-02 12:24:50 -08:00
|
|
|
|
2026-02-25 03:39:45 -08:00
|
|
|
ft.phase = FinalizationPhase::M2_MODELS;
|
|
|
|
|
return false;
|
|
|
|
|
}
|
2026-02-02 12:24:50 -08:00
|
|
|
|
2026-02-25 03:39:45 -08:00
|
|
|
case FinalizationPhase::M2_MODELS: {
|
2026-03-07 12:32:39 -08:00
|
|
|
// Upload multiple M2 models per call (batched GPU uploads)
|
2026-02-25 03:39:45 -08:00
|
|
|
if (m2Renderer && ft.m2ModelIndex < pending->m2Models.size()) {
|
2026-03-07 12:32:39 -08:00
|
|
|
constexpr size_t kModelsPerStep = 8;
|
|
|
|
|
size_t uploaded = 0;
|
|
|
|
|
while (ft.m2ModelIndex < pending->m2Models.size() && uploaded < kModelsPerStep) {
|
|
|
|
|
auto& m2Ready = pending->m2Models[ft.m2ModelIndex];
|
|
|
|
|
if (m2Renderer->loadModel(m2Ready.model, m2Ready.modelId)) {
|
|
|
|
|
ft.uploadedM2ModelIds.insert(m2Ready.modelId);
|
|
|
|
|
// Track uploaded model IDs so background threads can skip re-reading
|
|
|
|
|
std::lock_guard<std::mutex> lock(uploadedM2IdsMutex_);
|
|
|
|
|
uploadedM2Ids_.insert(m2Ready.modelId);
|
|
|
|
|
}
|
|
|
|
|
ft.m2ModelIndex++;
|
|
|
|
|
uploaded++;
|
2026-02-25 03:39:45 -08:00
|
|
|
}
|
|
|
|
|
// Stay in this phase until all models uploaded
|
|
|
|
|
if (ft.m2ModelIndex < pending->m2Models.size()) {
|
|
|
|
|
return false;
|
2026-02-02 12:24:50 -08:00
|
|
|
}
|
|
|
|
|
}
|
2026-02-25 03:39:45 -08:00
|
|
|
if (!ft.uploadedM2ModelIds.empty()) {
|
|
|
|
|
LOG_DEBUG(" Uploaded ", ft.uploadedM2ModelIds.size(), " M2 models for tile [", x, ",", y, "]");
|
2026-02-08 22:08:42 -08:00
|
|
|
}
|
2026-02-25 03:39:45 -08:00
|
|
|
ft.phase = FinalizationPhase::M2_INSTANCES;
|
|
|
|
|
return false;
|
|
|
|
|
}
|
2026-02-02 12:24:50 -08:00
|
|
|
|
2026-02-25 03:39:45 -08:00
|
|
|
case FinalizationPhase::M2_INSTANCES: {
|
|
|
|
|
// Create all M2 instances (lightweight struct allocation, no GPU work)
|
|
|
|
|
if (m2Renderer) {
|
|
|
|
|
int loadedDoodads = 0;
|
|
|
|
|
int skippedDedup = 0;
|
|
|
|
|
for (const auto& p : pending->m2Placements) {
|
|
|
|
|
if (p.uniqueId != 0 && placedDoodadIds.count(p.uniqueId)) {
|
|
|
|
|
skippedDedup++;
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
uint32_t instId = m2Renderer->createInstance(p.modelId, p.position, p.rotation, p.scale);
|
|
|
|
|
if (instId) {
|
|
|
|
|
ft.m2InstanceIds.push_back(instId);
|
|
|
|
|
if (p.uniqueId != 0) {
|
|
|
|
|
placedDoodadIds.insert(p.uniqueId);
|
|
|
|
|
ft.tileUniqueIds.push_back(p.uniqueId);
|
|
|
|
|
}
|
|
|
|
|
loadedDoodads++;
|
2026-02-25 02:36:23 -08:00
|
|
|
}
|
2026-02-09 18:38:45 -08:00
|
|
|
}
|
2026-02-25 03:39:45 -08:00
|
|
|
LOG_DEBUG(" Loaded doodads for tile [", x, ",", y, "]: ",
|
|
|
|
|
loadedDoodads, " instances (", ft.uploadedM2ModelIds.size(), " new models, ",
|
|
|
|
|
skippedDedup, " dedup skipped)");
|
2026-02-25 02:36:23 -08:00
|
|
|
}
|
2026-02-25 03:39:45 -08:00
|
|
|
ft.phase = FinalizationPhase::WMO_MODELS;
|
|
|
|
|
return false;
|
|
|
|
|
}
|
2026-02-25 02:36:23 -08:00
|
|
|
|
2026-02-25 03:39:45 -08:00
|
|
|
case FinalizationPhase::WMO_MODELS: {
|
2026-03-07 12:32:39 -08:00
|
|
|
// Upload multiple WMO models per call (batched GPU uploads)
|
2026-02-25 03:39:45 -08:00
|
|
|
if (wmoRenderer && assetManager) {
|
|
|
|
|
wmoRenderer->initialize(nullptr, VK_NULL_HANDLE, assetManager);
|
|
|
|
|
|
2026-03-07 12:32:39 -08:00
|
|
|
constexpr size_t kWmosPerStep = 4;
|
|
|
|
|
size_t uploaded = 0;
|
|
|
|
|
while (ft.wmoModelIndex < pending->wmoModels.size() && uploaded < kWmosPerStep) {
|
2026-02-25 03:39:45 -08:00
|
|
|
auto& wmoReady = pending->wmoModels[ft.wmoModelIndex];
|
|
|
|
|
if (wmoReady.uniqueId != 0 && placedWmoIds.count(wmoReady.uniqueId)) {
|
|
|
|
|
ft.wmoModelIndex++;
|
|
|
|
|
} else {
|
|
|
|
|
wmoRenderer->loadModel(wmoReady.model, wmoReady.modelId);
|
|
|
|
|
ft.wmoModelIndex++;
|
2026-03-07 12:32:39 -08:00
|
|
|
uploaded++;
|
2026-02-25 03:39:45 -08:00
|
|
|
}
|
2026-02-25 02:50:36 -08:00
|
|
|
}
|
2026-03-07 12:32:39 -08:00
|
|
|
if (ft.wmoModelIndex < pending->wmoModels.size()) return false;
|
2026-02-25 03:39:45 -08:00
|
|
|
}
|
|
|
|
|
ft.phase = FinalizationPhase::WMO_INSTANCES;
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
case FinalizationPhase::WMO_INSTANCES: {
|
|
|
|
|
// Create all WMO instances + load WMO liquids
|
|
|
|
|
if (wmoRenderer) {
|
|
|
|
|
int loadedWMOs = 0;
|
|
|
|
|
int loadedLiquids = 0;
|
|
|
|
|
int skippedWmoDedup = 0;
|
|
|
|
|
for (auto& wmoReady : pending->wmoModels) {
|
|
|
|
|
if (wmoReady.uniqueId != 0 && placedWmoIds.count(wmoReady.uniqueId)) {
|
|
|
|
|
skippedWmoDedup++;
|
|
|
|
|
continue;
|
|
|
|
|
}
|
2026-02-09 18:38:45 -08:00
|
|
|
|
2026-02-02 12:24:50 -08:00
|
|
|
uint32_t wmoInstId = wmoRenderer->createInstance(wmoReady.modelId, wmoReady.position, wmoReady.rotation);
|
|
|
|
|
if (wmoInstId) {
|
2026-02-25 03:39:45 -08:00
|
|
|
ft.wmoInstanceIds.push_back(wmoInstId);
|
2026-02-18 22:36:34 -08:00
|
|
|
if (wmoReady.uniqueId != 0) {
|
|
|
|
|
placedWmoIds.insert(wmoReady.uniqueId);
|
2026-02-25 03:39:45 -08:00
|
|
|
ft.tileWmoUniqueIds.push_back(wmoReady.uniqueId);
|
2026-02-18 22:36:34 -08:00
|
|
|
}
|
2026-02-02 12:24:50 -08:00
|
|
|
loadedWMOs++;
|
2026-02-03 13:33:31 -08:00
|
|
|
|
|
|
|
|
// Load WMO liquids (canals, pools, etc.)
|
|
|
|
|
if (waterRenderer) {
|
|
|
|
|
glm::mat4 modelMatrix = glm::mat4(1.0f);
|
|
|
|
|
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.y, glm::vec3(0.0f, 1.0f, 0.0f));
|
|
|
|
|
modelMatrix = glm::rotate(modelMatrix, wmoReady.rotation.x, glm::vec3(1.0f, 0.0f, 0.0f));
|
2026-03-07 00:49:11 -08:00
|
|
|
for (const auto& group : wmoReady.model.groups) {
|
2026-03-05 15:12:51 -08:00
|
|
|
if (!group.liquid.hasLiquid()) continue;
|
2026-03-07 00:48:04 -08:00
|
|
|
// Skip interior water/ocean but keep magma/slime (e.g. Ironforge lava)
|
2026-03-07 00:49:11 -08:00
|
|
|
if (group.flags & 0x2000) {
|
|
|
|
|
uint16_t lt = group.liquid.materialId;
|
|
|
|
|
uint8_t basicType = (lt == 0) ? 0 : ((lt - 1) % 4);
|
|
|
|
|
if (basicType < 2) continue;
|
2026-03-07 00:48:04 -08:00
|
|
|
}
|
2026-03-05 15:12:51 -08:00
|
|
|
waterRenderer->loadFromWMO(group.liquid, modelMatrix, wmoInstId);
|
|
|
|
|
loadedLiquids++;
|
2026-02-03 13:33:31 -08:00
|
|
|
}
|
|
|
|
|
}
|
2026-02-02 12:24:50 -08:00
|
|
|
}
|
|
|
|
|
}
|
2026-02-25 03:39:45 -08:00
|
|
|
if (loadedWMOs > 0 || skippedWmoDedup > 0) {
|
|
|
|
|
LOG_DEBUG(" Loaded WMOs for tile [", x, ",", y, "]: ",
|
|
|
|
|
loadedWMOs, " instances, ", skippedWmoDedup, " dedup skipped");
|
|
|
|
|
}
|
|
|
|
|
if (loadedLiquids > 0) {
|
|
|
|
|
LOG_DEBUG(" Loaded WMO liquids for tile [", x, ",", y, "]: ", loadedLiquids);
|
|
|
|
|
}
|
2026-02-02 12:24:50 -08:00
|
|
|
}
|
2026-02-25 03:39:45 -08:00
|
|
|
ft.phase = FinalizationPhase::WMO_DOODADS;
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
case FinalizationPhase::WMO_DOODADS: {
|
2026-03-07 12:32:39 -08:00
|
|
|
// Upload multiple WMO doodad M2s per call (batched GPU uploads)
|
2026-02-25 03:39:45 -08:00
|
|
|
if (m2Renderer && ft.wmoDoodadIndex < pending->wmoDoodads.size()) {
|
2026-03-07 12:32:39 -08:00
|
|
|
constexpr size_t kDoodadsPerStep = 16;
|
|
|
|
|
size_t uploaded = 0;
|
|
|
|
|
while (ft.wmoDoodadIndex < pending->wmoDoodads.size() && uploaded < kDoodadsPerStep) {
|
|
|
|
|
auto& doodad = pending->wmoDoodads[ft.wmoDoodadIndex];
|
|
|
|
|
if (m2Renderer->loadModel(doodad.model, doodad.modelId)) {
|
|
|
|
|
std::lock_guard<std::mutex> lock(uploadedM2IdsMutex_);
|
|
|
|
|
uploadedM2Ids_.insert(doodad.modelId);
|
|
|
|
|
}
|
|
|
|
|
uint32_t wmoDoodadInstId = m2Renderer->createInstanceWithMatrix(
|
|
|
|
|
doodad.modelId, doodad.modelMatrix, doodad.worldPosition);
|
|
|
|
|
if (wmoDoodadInstId) {
|
|
|
|
|
m2Renderer->setSkipCollision(wmoDoodadInstId, true);
|
|
|
|
|
ft.m2InstanceIds.push_back(wmoDoodadInstId);
|
|
|
|
|
}
|
|
|
|
|
ft.wmoDoodadIndex++;
|
|
|
|
|
uploaded++;
|
2026-03-06 12:26:17 -08:00
|
|
|
}
|
2026-02-25 03:39:45 -08:00
|
|
|
if (ft.wmoDoodadIndex < pending->wmoDoodads.size()) return false;
|
2026-02-03 13:33:31 -08:00
|
|
|
}
|
2026-02-25 03:39:45 -08:00
|
|
|
ft.phase = FinalizationPhase::WATER;
|
|
|
|
|
return false;
|
|
|
|
|
}
|
2026-02-02 12:24:50 -08:00
|
|
|
|
2026-02-25 03:39:45 -08:00
|
|
|
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);
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-02-02 12:24:50 -08:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-25 03:39:45 -08:00
|
|
|
ft.phase = FinalizationPhase::AMBIENT;
|
|
|
|
|
return false;
|
2026-02-02 12:24:50 -08:00
|
|
|
}
|
|
|
|
|
|
2026-02-25 03:39:45 -08:00
|
|
|
case FinalizationPhase::AMBIENT: {
|
|
|
|
|
// Register ambient sound emitters
|
|
|
|
|
if (ambientSoundManager && !pending->ambientEmitters.empty()) {
|
|
|
|
|
for (const auto& emitter : pending->ambientEmitters) {
|
|
|
|
|
auto type = static_cast<audio::AmbientSoundManager::AmbientType>(emitter.type);
|
|
|
|
|
ambientSoundManager->addEmitter(emitter.position, type);
|
|
|
|
|
}
|
2026-02-09 14:50:14 -08:00
|
|
|
}
|
|
|
|
|
|
2026-02-25 03:39:45 -08:00
|
|
|
// Commit tile to loadedTiles
|
|
|
|
|
auto tile = std::make_unique<TerrainTile>();
|
|
|
|
|
tile->coord = coord;
|
2026-03-02 10:15:01 -08:00
|
|
|
tile->terrain = std::move(pending->terrain);
|
|
|
|
|
tile->mesh = std::move(pending->mesh);
|
2026-02-25 03:39:45 -08:00
|
|
|
tile->loaded = true;
|
|
|
|
|
tile->m2InstanceIds = std::move(ft.m2InstanceIds);
|
|
|
|
|
tile->wmoInstanceIds = std::move(ft.wmoInstanceIds);
|
|
|
|
|
tile->wmoUniqueIds = std::move(ft.tileWmoUniqueIds);
|
|
|
|
|
tile->doodadUniqueIds = std::move(ft.tileUniqueIds);
|
|
|
|
|
getTileBounds(coord, tile->minX, tile->minY, tile->maxX, tile->maxY);
|
|
|
|
|
loadedTiles[coord] = std::move(tile);
|
2026-03-02 10:15:01 -08:00
|
|
|
// NOTE: Don't cache pending here — std::move above empties terrain/mesh,
|
|
|
|
|
// so the cached tile would have 0 valid chunks on reuse. Tiles are
|
|
|
|
|
// re-parsed from ADT files (file-cache hit) when they re-enter range.
|
2026-02-25 03:39:45 -08:00
|
|
|
|
|
|
|
|
// Now safe to remove from pendingTiles (tile is in loadedTiles)
|
|
|
|
|
{
|
|
|
|
|
std::lock_guard<std::mutex> lock(queueMutex);
|
|
|
|
|
pendingTiles.erase(coord);
|
|
|
|
|
}
|
2026-02-02 12:24:50 -08:00
|
|
|
|
2026-02-25 03:39:45 -08:00
|
|
|
LOG_DEBUG(" Finalized tile [", x, ",", y, "]");
|
2026-02-02 12:24:50 -08:00
|
|
|
|
2026-02-25 03:39:45 -08:00
|
|
|
ft.phase = FinalizationPhase::DONE;
|
|
|
|
|
return true;
|
|
|
|
|
}
|
2026-02-02 12:24:50 -08:00
|
|
|
|
2026-02-25 03:39:45 -08:00
|
|
|
case FinalizationPhase::DONE:
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
return true;
|
2026-02-02 12:24:50 -08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void TerrainManager::workerLoop() {
|
2026-02-25 03:39:45 -08:00
|
|
|
// Keep worker threads off core 0 (reserved for main thread)
|
|
|
|
|
{
|
|
|
|
|
int numCores = static_cast<int>(std::thread::hardware_concurrency());
|
|
|
|
|
if (numCores >= 2) {
|
2026-02-25 03:41:18 -08:00
|
|
|
#ifdef __linux__
|
2026-02-25 03:39:45 -08:00
|
|
|
cpu_set_t cpuset;
|
|
|
|
|
CPU_ZERO(&cpuset);
|
|
|
|
|
for (int i = 1; i < numCores; i++) {
|
|
|
|
|
CPU_SET(i, &cpuset);
|
|
|
|
|
}
|
|
|
|
|
pthread_setaffinity_np(pthread_self(), sizeof(cpu_set_t), &cpuset);
|
2026-02-25 03:41:18 -08:00
|
|
|
#elif defined(_WIN32)
|
|
|
|
|
DWORD_PTR mask = 0;
|
|
|
|
|
for (int i = 1; i < numCores && i < 64; i++) {
|
|
|
|
|
mask |= (static_cast<DWORD_PTR>(1) << i);
|
|
|
|
|
}
|
|
|
|
|
SetThreadAffinityMask(GetCurrentThread(), mask);
|
|
|
|
|
#elif defined(__APPLE__)
|
|
|
|
|
// Use affinity tag 2 for workers (separate from main thread tag 1)
|
|
|
|
|
thread_affinity_policy_data_t policy = { 2 };
|
|
|
|
|
thread_policy_set(
|
|
|
|
|
pthread_mach_thread_np(pthread_self()),
|
|
|
|
|
THREAD_AFFINITY_POLICY,
|
|
|
|
|
reinterpret_cast<thread_policy_t>(&policy),
|
|
|
|
|
THREAD_AFFINITY_POLICY_COUNT);
|
|
|
|
|
#endif
|
2026-02-25 03:39:45 -08:00
|
|
|
}
|
|
|
|
|
}
|
2026-02-02 12:24:50 -08:00
|
|
|
LOG_INFO("Terrain worker thread started");
|
|
|
|
|
|
|
|
|
|
while (workerRunning.load()) {
|
|
|
|
|
TileCoord coord;
|
|
|
|
|
bool hasWork = false;
|
|
|
|
|
|
|
|
|
|
{
|
|
|
|
|
std::unique_lock<std::mutex> lock(queueMutex);
|
|
|
|
|
queueCV.wait(lock, [this]() {
|
|
|
|
|
return !loadQueue.empty() || !workerRunning.load();
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
if (!workerRunning.load()) {
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (!loadQueue.empty()) {
|
|
|
|
|
coord = loadQueue.front();
|
2026-02-11 19:28:15 -08:00
|
|
|
loadQueue.pop_front();
|
2026-02-02 12:24:50 -08:00
|
|
|
hasWork = true;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (hasWork) {
|
|
|
|
|
auto pending = prepareTile(coord.x, coord.y);
|
|
|
|
|
|
|
|
|
|
std::lock_guard<std::mutex> lock(queueMutex);
|
|
|
|
|
if (pending) {
|
2026-02-08 03:24:12 -08:00
|
|
|
readyQueue.push(pending);
|
2026-02-02 12:24:50 -08:00
|
|
|
} else {
|
|
|
|
|
// Mark as failed so we don't re-enqueue
|
|
|
|
|
// We'll set failedTiles on the main thread in processReadyTiles
|
|
|
|
|
// For now, just remove from pending tracking
|
|
|
|
|
pendingTiles.erase(coord);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
LOG_INFO("Terrain worker thread stopped");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void TerrainManager::processReadyTiles() {
|
2026-02-10 19:30:45 -08:00
|
|
|
// Process tiles with time budget to avoid frame spikes
|
2026-02-11 22:27:02 -08:00
|
|
|
// Taxi mode gets a slightly larger budget to avoid visible late-pop terrain/models.
|
2026-03-07 13:44:09 -08:00
|
|
|
const float timeBudgetMs = taxiStreamingMode_ ? 8.0f : 3.0f;
|
2026-02-10 19:30:45 -08:00
|
|
|
auto startTime = std::chrono::high_resolution_clock::now();
|
2026-02-25 02:50:36 -08:00
|
|
|
|
2026-02-25 03:39:45 -08:00
|
|
|
// 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);
|
|
|
|
|
while (!readyQueue.empty()) {
|
|
|
|
|
auto pending = readyQueue.front();
|
2026-02-02 12:24:50 -08:00
|
|
|
readyQueue.pop();
|
2026-02-25 03:39:45 -08:00
|
|
|
if (pending) {
|
|
|
|
|
FinalizingTile ft;
|
|
|
|
|
ft.pending = std::move(pending);
|
|
|
|
|
finalizingTiles_.push_back(std::move(ft));
|
2026-02-10 19:30:45 -08:00
|
|
|
}
|
2026-02-02 12:24:50 -08:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-07 12:32:39 -08:00
|
|
|
// Outer upload batch: all GPU uploads across all advanceFinalization calls
|
|
|
|
|
// this frame share a single command buffer submission + fence wait.
|
|
|
|
|
VkContext* vkCtx = terrainRenderer ? terrainRenderer->getVkContext() : nullptr;
|
|
|
|
|
if (vkCtx) vkCtx->beginUploadBatch();
|
|
|
|
|
|
2026-02-25 03:39:45 -08:00
|
|
|
// Drive incremental finalization within time budget
|
|
|
|
|
while (!finalizingTiles_.empty()) {
|
|
|
|
|
auto& ft = finalizingTiles_.front();
|
|
|
|
|
bool done = advanceFinalization(ft);
|
|
|
|
|
|
|
|
|
|
if (done) {
|
|
|
|
|
finalizingTiles_.pop_front();
|
2026-02-25 02:36:23 -08:00
|
|
|
}
|
2026-02-25 02:50:36 -08:00
|
|
|
|
2026-02-25 03:39:45 -08:00
|
|
|
auto now = std::chrono::high_resolution_clock::now();
|
|
|
|
|
float elapsedMs = std::chrono::duration<float, std::milli>(now - startTime).count();
|
|
|
|
|
if (elapsedMs >= timeBudgetMs) {
|
|
|
|
|
break;
|
|
|
|
|
}
|
2026-02-10 19:30:45 -08:00
|
|
|
}
|
2026-03-07 12:32:39 -08:00
|
|
|
|
|
|
|
|
if (vkCtx) vkCtx->endUploadBatch();
|
2026-02-10 19:30:45 -08:00
|
|
|
}
|
|
|
|
|
|
2026-02-06 14:56:26 -08:00
|
|
|
void TerrainManager::processAllReadyTiles() {
|
2026-02-25 03:39:45 -08:00
|
|
|
// Move all ready tiles into finalizing deque
|
|
|
|
|
// Keep in pendingTiles until committed (same as processReadyTiles)
|
|
|
|
|
{
|
|
|
|
|
std::lock_guard<std::mutex> lock(queueMutex);
|
|
|
|
|
while (!readyQueue.empty()) {
|
|
|
|
|
auto pending = readyQueue.front();
|
2026-02-06 14:56:26 -08:00
|
|
|
readyQueue.pop();
|
2026-02-25 03:39:45 -08:00
|
|
|
if (pending) {
|
|
|
|
|
FinalizingTile ft;
|
|
|
|
|
ft.pending = std::move(pending);
|
|
|
|
|
finalizingTiles_.push_back(std::move(ft));
|
2026-02-07 18:57:34 -08:00
|
|
|
}
|
2026-02-06 14:56:26 -08:00
|
|
|
}
|
|
|
|
|
}
|
2026-03-07 12:32:39 -08:00
|
|
|
|
|
|
|
|
// Batch all GPU uploads across all tiles into a single submission
|
|
|
|
|
VkContext* vkCtx = terrainRenderer ? terrainRenderer->getVkContext() : nullptr;
|
|
|
|
|
if (vkCtx) vkCtx->beginUploadBatch();
|
|
|
|
|
|
2026-02-25 03:39:45 -08:00
|
|
|
// Finalize all tiles completely (no time budget — used for loading screens)
|
|
|
|
|
while (!finalizingTiles_.empty()) {
|
|
|
|
|
auto& ft = finalizingTiles_.front();
|
|
|
|
|
while (!advanceFinalization(ft)) {}
|
|
|
|
|
finalizingTiles_.pop_front();
|
|
|
|
|
}
|
2026-03-07 12:32:39 -08:00
|
|
|
|
|
|
|
|
if (vkCtx) vkCtx->endUploadBatch();
|
2026-02-06 14:56:26 -08:00
|
|
|
}
|
|
|
|
|
|
2026-02-26 13:38:29 -08:00
|
|
|
void TerrainManager::processOneReadyTile() {
|
|
|
|
|
// Move ready tiles into finalizing deque
|
|
|
|
|
{
|
|
|
|
|
std::lock_guard<std::mutex> lock(queueMutex);
|
|
|
|
|
while (!readyQueue.empty()) {
|
|
|
|
|
auto pending = readyQueue.front();
|
|
|
|
|
readyQueue.pop();
|
|
|
|
|
if (pending) {
|
|
|
|
|
FinalizingTile ft;
|
|
|
|
|
ft.pending = std::move(pending);
|
|
|
|
|
finalizingTiles_.push_back(std::move(ft));
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
// Finalize ONE tile completely, then return so caller can update the screen
|
|
|
|
|
if (!finalizingTiles_.empty()) {
|
2026-03-07 12:32:39 -08:00
|
|
|
VkContext* vkCtx = terrainRenderer ? terrainRenderer->getVkContext() : nullptr;
|
|
|
|
|
if (vkCtx) vkCtx->beginUploadBatch();
|
|
|
|
|
|
2026-02-26 13:38:29 -08:00
|
|
|
auto& ft = finalizingTiles_.front();
|
|
|
|
|
while (!advanceFinalization(ft)) {}
|
|
|
|
|
finalizingTiles_.pop_front();
|
2026-03-07 12:32:39 -08:00
|
|
|
|
|
|
|
|
if (vkCtx) vkCtx->endUploadBatch();
|
2026-02-26 13:38:29 -08:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-08 03:24:12 -08:00
|
|
|
std::shared_ptr<PendingTile> TerrainManager::getCachedTile(const TileCoord& coord) {
|
|
|
|
|
std::lock_guard<std::mutex> lock(tileCacheMutex_);
|
|
|
|
|
auto it = tileCache_.find(coord);
|
|
|
|
|
if (it == tileCache_.end()) return nullptr;
|
|
|
|
|
tileCacheLru_.erase(it->second.lruIt);
|
|
|
|
|
tileCacheLru_.push_front(coord);
|
|
|
|
|
it->second.lruIt = tileCacheLru_.begin();
|
|
|
|
|
return it->second.tile;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void TerrainManager::putCachedTile(const std::shared_ptr<PendingTile>& tile) {
|
|
|
|
|
if (!tile) return;
|
|
|
|
|
std::lock_guard<std::mutex> lock(tileCacheMutex_);
|
|
|
|
|
TileCoord coord = tile->coord;
|
|
|
|
|
|
|
|
|
|
auto it = tileCache_.find(coord);
|
|
|
|
|
if (it != tileCache_.end()) {
|
|
|
|
|
tileCacheLru_.erase(it->second.lruIt);
|
|
|
|
|
tileCacheBytes_ -= it->second.bytes;
|
|
|
|
|
tileCache_.erase(it);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
size_t bytes = estimatePendingTileBytes(*tile);
|
|
|
|
|
tileCacheLru_.push_front(coord);
|
|
|
|
|
tileCache_[coord] = CachedTile{tile, bytes, tileCacheLru_.begin()};
|
|
|
|
|
tileCacheBytes_ += bytes;
|
|
|
|
|
|
|
|
|
|
// Evict least-recently used tiles until under budget
|
|
|
|
|
while (tileCacheBytes_ > tileCacheBudgetBytes_ && !tileCacheLru_.empty()) {
|
|
|
|
|
TileCoord evictCoord = tileCacheLru_.back();
|
|
|
|
|
auto eit = tileCache_.find(evictCoord);
|
|
|
|
|
if (eit != tileCache_.end()) {
|
|
|
|
|
tileCacheBytes_ -= eit->second.bytes;
|
|
|
|
|
tileCache_.erase(eit);
|
|
|
|
|
}
|
|
|
|
|
tileCacheLru_.pop_back();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
size_t TerrainManager::estimatePendingTileBytes(const PendingTile& tile) const {
|
|
|
|
|
size_t bytes = 0;
|
|
|
|
|
bytes += sizeof(PendingTile);
|
|
|
|
|
bytes += tile.terrain.textures.size() * 64;
|
|
|
|
|
bytes += tile.terrain.doodadNames.size() * 64;
|
|
|
|
|
bytes += tile.terrain.wmoNames.size() * 64;
|
|
|
|
|
bytes += tile.terrain.doodadPlacements.size() * sizeof(pipeline::ADTTerrain::DoodadPlacement);
|
|
|
|
|
bytes += tile.terrain.wmoPlacements.size() * sizeof(pipeline::ADTTerrain::WMOPlacement);
|
|
|
|
|
|
|
|
|
|
for (const auto& chunk : tile.terrain.chunks) {
|
|
|
|
|
bytes += sizeof(chunk);
|
|
|
|
|
bytes += chunk.layers.size() * sizeof(pipeline::TextureLayer);
|
|
|
|
|
bytes += chunk.alphaMap.size();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
for (const auto& cm : tile.mesh.chunks) {
|
|
|
|
|
bytes += cm.vertices.size() * sizeof(pipeline::TerrainVertex);
|
|
|
|
|
bytes += cm.indices.size() * sizeof(pipeline::TerrainIndex);
|
|
|
|
|
for (const auto& layer : cm.layers) {
|
|
|
|
|
bytes += layer.alphaData.size();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
for (const auto& ready : tile.m2Models) {
|
|
|
|
|
bytes += ready.model.vertices.size() * sizeof(pipeline::M2Vertex);
|
|
|
|
|
bytes += ready.model.indices.size() * sizeof(uint16_t);
|
|
|
|
|
bytes += ready.model.textures.size() * sizeof(pipeline::M2Texture);
|
|
|
|
|
}
|
|
|
|
|
bytes += tile.m2Placements.size() * sizeof(PendingTile::M2Placement);
|
|
|
|
|
|
|
|
|
|
for (const auto& ready : tile.wmoModels) {
|
|
|
|
|
for (const auto& group : ready.model.groups) {
|
|
|
|
|
bytes += group.vertices.size() * sizeof(pipeline::WMOVertex);
|
|
|
|
|
bytes += group.indices.size() * sizeof(uint16_t);
|
|
|
|
|
bytes += group.batches.size() * sizeof(pipeline::WMOBatch);
|
|
|
|
|
bytes += group.portalVertices.size() * sizeof(glm::vec3);
|
|
|
|
|
bytes += group.portals.size() * sizeof(pipeline::WMOPortal);
|
|
|
|
|
bytes += group.bspNodes.size();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
bytes += tile.wmoDoodads.size() * sizeof(PendingTile::WMODoodadReady);
|
|
|
|
|
|
|
|
|
|
for (const auto& [_, img] : tile.preloadedTextures) {
|
|
|
|
|
bytes += img.data.size();
|
|
|
|
|
}
|
|
|
|
|
return bytes;
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-02 12:24:50 -08:00
|
|
|
void TerrainManager::unloadTile(int x, int y) {
|
|
|
|
|
TileCoord coord = {x, y};
|
|
|
|
|
|
|
|
|
|
// Also remove from pending if it was queued but not yet loaded
|
2026-02-07 18:57:34 -08:00
|
|
|
{
|
|
|
|
|
std::lock_guard<std::mutex> lock(queueMutex);
|
|
|
|
|
pendingTiles.erase(coord);
|
|
|
|
|
}
|
2026-02-02 12:24:50 -08:00
|
|
|
|
2026-02-25 03:39:45 -08:00
|
|
|
// 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;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-02 12:24:50 -08:00
|
|
|
auto it = loadedTiles.find(coord);
|
|
|
|
|
if (it == loadedTiles.end()) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
LOG_INFO("Unloading terrain tile [", x, ",", y, "]");
|
|
|
|
|
|
|
|
|
|
const auto& tile = it->second;
|
|
|
|
|
|
|
|
|
|
// Remove doodad unique IDs from dedup set
|
|
|
|
|
for (uint32_t uid : tile->doodadUniqueIds) {
|
|
|
|
|
placedDoodadIds.erase(uid);
|
|
|
|
|
}
|
2026-02-18 22:36:34 -08:00
|
|
|
for (uint32_t uid : tile->wmoUniqueIds) {
|
|
|
|
|
placedWmoIds.erase(uid);
|
|
|
|
|
}
|
2026-02-02 12:24:50 -08:00
|
|
|
|
|
|
|
|
// Remove M2 doodad instances
|
|
|
|
|
if (m2Renderer) {
|
2026-02-11 22:27:02 -08:00
|
|
|
m2Renderer->removeInstances(tile->m2InstanceIds);
|
2026-02-02 12:24:50 -08:00
|
|
|
LOG_DEBUG(" Removed ", tile->m2InstanceIds.size(), " M2 instances");
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-03 13:33:31 -08:00
|
|
|
// Remove WMO instances and their liquids
|
2026-02-02 12:24:50 -08:00
|
|
|
if (wmoRenderer) {
|
|
|
|
|
for (uint32_t id : tile->wmoInstanceIds) {
|
2026-02-03 13:33:31 -08:00
|
|
|
// Remove WMO liquids associated with this instance
|
|
|
|
|
if (waterRenderer) {
|
|
|
|
|
waterRenderer->removeWMO(id);
|
|
|
|
|
}
|
2026-02-02 12:24:50 -08:00
|
|
|
}
|
2026-02-11 22:27:02 -08:00
|
|
|
wmoRenderer->removeInstances(tile->wmoInstanceIds);
|
2026-02-02 12:24:50 -08:00
|
|
|
LOG_DEBUG(" Removed ", tile->wmoInstanceIds.size(), " WMO instances");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Remove terrain chunks for this tile
|
|
|
|
|
if (terrainRenderer) {
|
|
|
|
|
terrainRenderer->removeTile(x, y);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Remove water surfaces for this tile
|
|
|
|
|
if (waterRenderer) {
|
|
|
|
|
waterRenderer->removeTile(x, y);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
loadedTiles.erase(it);
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-26 13:38:29 -08:00
|
|
|
void TerrainManager::stopWorkers() {
|
|
|
|
|
if (!workerRunning.load()) {
|
|
|
|
|
LOG_WARNING("stopWorkers: already stopped");
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
LOG_WARNING("stopWorkers: signaling ", workerThreads.size(), " workers to stop...");
|
|
|
|
|
workerRunning.store(false);
|
|
|
|
|
queueCV.notify_all();
|
|
|
|
|
|
|
|
|
|
// Workers check workerRunning at each I/O point in prepareTile() and bail
|
|
|
|
|
// out quickly. Use plain join() which is safe with std::thread — no
|
|
|
|
|
// pthread_timedjoin_np (which silently joins the pthread but leaves the
|
|
|
|
|
// std::thread object thinking it's still joinable → std::terminate on dtor).
|
|
|
|
|
for (size_t i = 0; i < workerThreads.size(); i++) {
|
|
|
|
|
if (workerThreads[i].joinable()) {
|
|
|
|
|
LOG_WARNING("stopWorkers: joining worker ", i, "...");
|
|
|
|
|
workerThreads[i].join();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
workerThreads.clear();
|
|
|
|
|
LOG_WARNING("stopWorkers: done");
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-02 12:24:50 -08:00
|
|
|
void TerrainManager::unloadAll() {
|
2026-02-25 13:42:58 -08:00
|
|
|
// Signal worker threads to stop and wait briefly for them to finish.
|
|
|
|
|
// Workers may be mid-prepareTile (reading MPQ / parsing ADT) which can
|
|
|
|
|
// take seconds, so use a short deadline and detach any stragglers.
|
2026-02-02 12:24:50 -08:00
|
|
|
if (workerRunning.load()) {
|
|
|
|
|
workerRunning.store(false);
|
|
|
|
|
queueCV.notify_all();
|
2026-02-25 13:42:58 -08:00
|
|
|
|
2026-02-03 17:21:04 -08:00
|
|
|
for (auto& t : workerThreads) {
|
2026-02-26 13:38:29 -08:00
|
|
|
if (t.joinable()) t.join();
|
2026-02-02 12:24:50 -08:00
|
|
|
}
|
2026-02-03 17:21:04 -08:00
|
|
|
workerThreads.clear();
|
2026-02-02 12:24:50 -08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Clear queues
|
|
|
|
|
{
|
|
|
|
|
std::lock_guard<std::mutex> lock(queueMutex);
|
2026-02-11 19:28:15 -08:00
|
|
|
while (!loadQueue.empty()) loadQueue.pop_front();
|
2026-02-02 12:24:50 -08:00
|
|
|
while (!readyQueue.empty()) readyQueue.pop();
|
|
|
|
|
}
|
|
|
|
|
pendingTiles.clear();
|
2026-02-25 03:39:45 -08:00
|
|
|
finalizingTiles_.clear();
|
2026-02-02 12:24:50 -08:00
|
|
|
placedDoodadIds.clear();
|
2026-03-04 19:47:01 -08:00
|
|
|
placedWmoIds.clear();
|
2026-03-07 12:32:39 -08:00
|
|
|
{
|
|
|
|
|
std::lock_guard<std::mutex> lock(uploadedM2IdsMutex_);
|
|
|
|
|
uploadedM2Ids_.clear();
|
|
|
|
|
}
|
2026-02-02 12:24:50 -08:00
|
|
|
|
|
|
|
|
LOG_INFO("Unloading all terrain tiles");
|
|
|
|
|
loadedTiles.clear();
|
|
|
|
|
failedTiles.clear();
|
|
|
|
|
|
2026-02-04 18:27:52 -08:00
|
|
|
// Reset tile tracking so streaming re-triggers at the new location
|
|
|
|
|
currentTile = {-1, -1};
|
|
|
|
|
lastStreamTile = {-1, -1};
|
|
|
|
|
|
2026-02-02 12:24:50 -08:00
|
|
|
// Clear terrain renderer
|
|
|
|
|
if (terrainRenderer) {
|
|
|
|
|
terrainRenderer->clear();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Clear water
|
|
|
|
|
if (waterRenderer) {
|
|
|
|
|
waterRenderer->clear();
|
|
|
|
|
}
|
2026-02-04 18:27:52 -08:00
|
|
|
|
|
|
|
|
// Clear WMO and M2 renderers so old-location geometry doesn't persist
|
|
|
|
|
if (wmoRenderer) {
|
|
|
|
|
wmoRenderer->clearInstances();
|
|
|
|
|
}
|
|
|
|
|
if (m2Renderer) {
|
|
|
|
|
m2Renderer->clear();
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-08 23:15:26 -08:00
|
|
|
// Restart worker threads so streaming can resume (dynamic: scales with available cores)
|
|
|
|
|
// Use 75% of logical cores for decompression, leaving headroom for render/OS
|
2026-02-04 18:27:52 -08:00
|
|
|
workerRunning.store(true);
|
2026-02-22 08:12:08 -08:00
|
|
|
workerCount = computeTerrainWorkerCount();
|
2026-02-04 18:27:52 -08:00
|
|
|
workerThreads.reserve(workerCount);
|
|
|
|
|
for (int i = 0; i < workerCount; i++) {
|
|
|
|
|
workerThreads.emplace_back(&TerrainManager::workerLoop, this);
|
|
|
|
|
}
|
2026-02-02 12:24:50 -08:00
|
|
|
}
|
|
|
|
|
|
2026-02-25 13:37:09 -08:00
|
|
|
void TerrainManager::softReset() {
|
|
|
|
|
// Clear queues (workers may still be running — they'll find empty queues)
|
|
|
|
|
{
|
|
|
|
|
std::lock_guard<std::mutex> lock(queueMutex);
|
|
|
|
|
loadQueue.clear();
|
|
|
|
|
while (!readyQueue.empty()) readyQueue.pop();
|
|
|
|
|
}
|
|
|
|
|
pendingTiles.clear();
|
|
|
|
|
finalizingTiles_.clear();
|
|
|
|
|
placedDoodadIds.clear();
|
2026-03-04 19:47:01 -08:00
|
|
|
placedWmoIds.clear();
|
2026-03-07 12:32:39 -08:00
|
|
|
{
|
|
|
|
|
std::lock_guard<std::mutex> lock(uploadedM2IdsMutex_);
|
|
|
|
|
uploadedM2Ids_.clear();
|
|
|
|
|
}
|
2026-02-25 13:37:09 -08:00
|
|
|
|
2026-03-02 09:52:09 -08:00
|
|
|
// Clear tile cache — keys are (x,y) without map name, so stale entries from
|
|
|
|
|
// a different map with overlapping coordinates would produce wrong geometry.
|
|
|
|
|
{
|
|
|
|
|
std::lock_guard<std::mutex> lock(tileCacheMutex_);
|
|
|
|
|
tileCache_.clear();
|
|
|
|
|
tileCacheLru_.clear();
|
|
|
|
|
tileCacheBytes_ = 0;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
LOG_INFO("Soft-resetting terrain (clearing tiles + water + cache, workers stay alive)");
|
2026-02-25 13:37:09 -08:00
|
|
|
loadedTiles.clear();
|
|
|
|
|
failedTiles.clear();
|
|
|
|
|
|
|
|
|
|
currentTile = {-1, -1};
|
|
|
|
|
lastStreamTile = {-1, -1};
|
|
|
|
|
|
|
|
|
|
if (terrainRenderer) {
|
|
|
|
|
terrainRenderer->clear();
|
|
|
|
|
}
|
|
|
|
|
if (waterRenderer) {
|
|
|
|
|
waterRenderer->clear();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-04 17:37:28 -08:00
|
|
|
TileCoord TerrainManager::worldToTile(float glX, float glY) const {
|
|
|
|
|
auto [tileX, tileY] = core::coords::worldToTile(glX, glY);
|
2026-02-02 12:24:50 -08:00
|
|
|
return {tileX, tileY};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void TerrainManager::getTileBounds(const TileCoord& coord, float& minX, float& minY,
|
|
|
|
|
float& maxX, float& maxY) const {
|
|
|
|
|
// Calculate world bounds for this tile
|
|
|
|
|
// Tile (32, 32) is at origin
|
|
|
|
|
float offsetX = (32 - coord.x) * TILE_SIZE;
|
|
|
|
|
float offsetY = (32 - coord.y) * TILE_SIZE;
|
|
|
|
|
|
|
|
|
|
minX = offsetX - TILE_SIZE;
|
|
|
|
|
minY = offsetY - TILE_SIZE;
|
|
|
|
|
maxX = offsetX;
|
|
|
|
|
maxY = offsetY;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
std::string TerrainManager::getADTPath(const TileCoord& coord) const {
|
|
|
|
|
// Format: World\Maps\{MapName}\{MapName}_{X}_{Y}.adt
|
|
|
|
|
return "World\\Maps\\" + mapName + "\\" + mapName + "_" +
|
|
|
|
|
std::to_string(coord.x) + "_" + std::to_string(coord.y) + ".adt";
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-21 01:26:16 -08:00
|
|
|
void TerrainManager::ensureGroundEffectTablesLoaded() {
|
|
|
|
|
if (groundEffectsLoaded_ || !assetManager) return;
|
|
|
|
|
groundEffectsLoaded_ = true;
|
|
|
|
|
|
|
|
|
|
auto groundEffectTex = assetManager->loadDBC("GroundEffectTexture.dbc");
|
|
|
|
|
auto groundEffectDoodad = assetManager->loadDBC("GroundEffectDoodad.dbc");
|
|
|
|
|
if (!groundEffectTex || !groundEffectDoodad) {
|
|
|
|
|
LOG_WARNING("Ground clutter DBCs missing; skipping procedural ground effects");
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// GroundEffectTexture: id + 4 doodad IDs + 4 weights + density + sound
|
|
|
|
|
for (uint32_t i = 0; i < groundEffectTex->getRecordCount(); ++i) {
|
|
|
|
|
uint32_t effectId = groundEffectTex->getUInt32(i, 0);
|
|
|
|
|
if (effectId == 0) continue;
|
|
|
|
|
|
|
|
|
|
GroundEffectEntry e;
|
|
|
|
|
e.doodadIds[0] = groundEffectTex->getUInt32(i, 1);
|
|
|
|
|
e.doodadIds[1] = groundEffectTex->getUInt32(i, 2);
|
|
|
|
|
e.doodadIds[2] = groundEffectTex->getUInt32(i, 3);
|
|
|
|
|
e.doodadIds[3] = groundEffectTex->getUInt32(i, 4);
|
|
|
|
|
e.weights[0] = groundEffectTex->getUInt32(i, 5);
|
|
|
|
|
e.weights[1] = groundEffectTex->getUInt32(i, 6);
|
|
|
|
|
e.weights[2] = groundEffectTex->getUInt32(i, 7);
|
|
|
|
|
e.weights[3] = groundEffectTex->getUInt32(i, 8);
|
|
|
|
|
e.density = groundEffectTex->getUInt32(i, 9);
|
|
|
|
|
groundEffectById_[effectId] = e;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// GroundEffectDoodad: id + modelName(offset) + flags
|
|
|
|
|
for (uint32_t i = 0; i < groundEffectDoodad->getRecordCount(); ++i) {
|
|
|
|
|
uint32_t doodadId = groundEffectDoodad->getUInt32(i, 0);
|
|
|
|
|
std::string modelName = groundEffectDoodad->getString(i, 1);
|
|
|
|
|
if (doodadId == 0 || modelName.empty()) continue;
|
|
|
|
|
|
|
|
|
|
std::string lower = toLowerCopy(modelName);
|
|
|
|
|
if (lower.size() > 4 && lower.substr(lower.size() - 4) == ".mdl") {
|
|
|
|
|
lower = lower.substr(0, lower.size() - 4) + ".m2";
|
|
|
|
|
}
|
|
|
|
|
if (lower.find('\\') != std::string::npos || lower.find('/') != std::string::npos) {
|
|
|
|
|
groundDoodadModelById_[doodadId] = lower;
|
|
|
|
|
} else {
|
|
|
|
|
groundDoodadModelById_[doodadId] = "World\\NoDXT\\Detail\\" + lower;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
LOG_INFO("Ground clutter tables loaded: ", groundEffectById_.size(),
|
|
|
|
|
" effects, ", groundDoodadModelById_.size(), " doodad models");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void TerrainManager::generateGroundClutterPlacements(std::shared_ptr<PendingTile>& pending,
|
|
|
|
|
std::unordered_set<uint32_t>& preparedModelIds) {
|
|
|
|
|
if (taxiStreamingMode_) return; // Skip clutter while on taxi flights.
|
|
|
|
|
if (!pending || groundEffectById_.empty() || groundDoodadModelById_.empty()) return;
|
|
|
|
|
|
|
|
|
|
static const std::string kGroundClutterProxyModel = "World\\NoDXT\\Detail\\ElwGra01.m2";
|
|
|
|
|
static bool loggedProxy = false;
|
|
|
|
|
if (!loggedProxy) {
|
|
|
|
|
LOG_INFO("Ground clutter: forcing proxy model ", kGroundClutterProxyModel);
|
|
|
|
|
loggedProxy = true;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
size_t modelMissing = 0;
|
|
|
|
|
size_t modelInvalid = 0;
|
|
|
|
|
auto ensureModelPrepared = [&](const std::string& m2Path, uint32_t modelId) -> bool {
|
|
|
|
|
if (preparedModelIds.count(modelId)) return true;
|
|
|
|
|
|
|
|
|
|
std::vector<uint8_t> m2Data = assetManager->readFile(m2Path);
|
|
|
|
|
if (m2Data.empty()) {
|
|
|
|
|
modelMissing++;
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pipeline::M2Model m2Model = pipeline::M2Loader::load(m2Data);
|
|
|
|
|
if (m2Model.name.empty()) {
|
|
|
|
|
m2Model.name = m2Path;
|
|
|
|
|
}
|
|
|
|
|
std::string skinPath = m2Path.substr(0, m2Path.size() - 3) + "00.skin";
|
|
|
|
|
std::vector<uint8_t> skinData = assetManager->readFileOptional(skinPath);
|
|
|
|
|
if (!skinData.empty() && m2Model.version >= 264) {
|
|
|
|
|
pipeline::M2Loader::loadSkin(skinData, m2Model);
|
|
|
|
|
}
|
|
|
|
|
if (!m2Model.isValid()) {
|
|
|
|
|
modelInvalid++;
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
PendingTile::M2Ready ready;
|
|
|
|
|
ready.modelId = modelId;
|
|
|
|
|
ready.model = std::move(m2Model);
|
|
|
|
|
ready.path = m2Path;
|
|
|
|
|
pending->m2Models.push_back(std::move(ready));
|
|
|
|
|
preparedModelIds.insert(modelId);
|
|
|
|
|
return true;
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
constexpr float unitSize = CHUNK_SIZE / 8.0f;
|
|
|
|
|
constexpr float pi = 3.1415926535f;
|
|
|
|
|
constexpr size_t kBaseMaxGroundClutterPerTile = 220;
|
|
|
|
|
constexpr uint32_t kBaseMaxAttemptsPerLayer = 4;
|
|
|
|
|
const float densityScaleRaw = glm::clamp(groundClutterDensityScale_, 0.0f, 1.5f);
|
|
|
|
|
// Keep runtime density bounded to avoid large streaming spikes in dense tiles.
|
|
|
|
|
const float densityScale = std::min(densityScaleRaw, 1.0f);
|
|
|
|
|
const size_t kMaxGroundClutterPerTile = std::max<size_t>(
|
|
|
|
|
0, static_cast<size_t>(std::lround(static_cast<float>(kBaseMaxGroundClutterPerTile) * densityScale)));
|
|
|
|
|
const uint32_t kMaxAttemptsPerLayer = std::max<uint32_t>(
|
|
|
|
|
1u, static_cast<uint32_t>(std::lround(static_cast<float>(kBaseMaxAttemptsPerLayer) * densityScale)));
|
|
|
|
|
std::vector<uint8_t> alphaScratch;
|
|
|
|
|
std::vector<uint8_t> alphaScratchTex;
|
|
|
|
|
size_t added = 0;
|
|
|
|
|
size_t attemptsTotal = 0;
|
|
|
|
|
size_t alphaRejected = 0;
|
|
|
|
|
size_t roadRejected = 0;
|
|
|
|
|
size_t noEffectMatch = 0;
|
|
|
|
|
size_t textureIdFallbackMatch = 0;
|
|
|
|
|
size_t noDoodadModel = 0;
|
|
|
|
|
std::array<uint16_t, 256> perChunkAdded{};
|
|
|
|
|
|
|
|
|
|
auto isRoadLikeTexture = [](const std::string& texPath) -> bool {
|
|
|
|
|
std::string t = toLowerCopy(texPath);
|
|
|
|
|
return (t.find("road") != std::string::npos) ||
|
|
|
|
|
(t.find("cobble") != std::string::npos) ||
|
|
|
|
|
(t.find("path") != std::string::npos) ||
|
|
|
|
|
(t.find("street") != std::string::npos) ||
|
|
|
|
|
(t.find("pavement") != std::string::npos) ||
|
|
|
|
|
(t.find("brick") != std::string::npos);
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
auto layerWeightAt = [&](const pipeline::MapChunk& chunk, size_t layerIdx, int alphaIndex) -> int {
|
|
|
|
|
if (layerIdx >= chunk.layers.size()) return 0;
|
|
|
|
|
if (layerIdx == 0) {
|
|
|
|
|
int accum = 0;
|
|
|
|
|
size_t numLayers = std::min(chunk.layers.size(), static_cast<size_t>(4));
|
|
|
|
|
for (size_t i = 1; i < numLayers; ++i) {
|
|
|
|
|
int a = 0;
|
|
|
|
|
if (decodeLayerAlpha(chunk, i, alphaScratchTex) &&
|
|
|
|
|
alphaIndex >= 0 &&
|
|
|
|
|
alphaIndex < static_cast<int>(alphaScratchTex.size())) {
|
|
|
|
|
a = alphaScratchTex[alphaIndex];
|
|
|
|
|
}
|
|
|
|
|
accum += a;
|
|
|
|
|
}
|
|
|
|
|
return glm::clamp(255 - accum, 0, 255);
|
|
|
|
|
}
|
|
|
|
|
if (decodeLayerAlpha(chunk, layerIdx, alphaScratchTex) &&
|
|
|
|
|
alphaIndex >= 0 &&
|
|
|
|
|
alphaIndex < static_cast<int>(alphaScratchTex.size())) {
|
|
|
|
|
return alphaScratchTex[alphaIndex];
|
|
|
|
|
}
|
|
|
|
|
return 0;
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
auto hasRoadLikeTextureAt = [&](const pipeline::MapChunk& chunk, float fracX, float fracY) -> bool {
|
|
|
|
|
if (chunk.layers.empty()) return false;
|
|
|
|
|
int alphaX = glm::clamp(static_cast<int>((fracX / 8.0f) * 63.0f), 0, 63);
|
|
|
|
|
int alphaY = glm::clamp(static_cast<int>((fracY / 8.0f) * 63.0f), 0, 63);
|
|
|
|
|
int alphaIndex = alphaY * 64 + alphaX;
|
|
|
|
|
|
|
|
|
|
size_t numLayers = std::min(chunk.layers.size(), static_cast<size_t>(4));
|
|
|
|
|
for (size_t layerIdx = 0; layerIdx < numLayers; ++layerIdx) {
|
|
|
|
|
uint32_t texId = chunk.layers[layerIdx].textureId;
|
|
|
|
|
if (texId >= pending->terrain.textures.size()) continue;
|
|
|
|
|
const std::string& texPath = pending->terrain.textures[texId];
|
|
|
|
|
if (!isRoadLikeTexture(texPath)) continue;
|
|
|
|
|
// Treat meaningful blend contribution as road occupancy.
|
|
|
|
|
int w = layerWeightAt(chunk, layerIdx, alphaIndex);
|
|
|
|
|
if (w >= 24) return true;
|
|
|
|
|
}
|
|
|
|
|
return false;
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
for (int cy = 0; cy < 16; ++cy) {
|
|
|
|
|
if (added >= kMaxGroundClutterPerTile) break;
|
|
|
|
|
for (int cx = 0; cx < 16; ++cx) {
|
|
|
|
|
if (added >= kMaxGroundClutterPerTile) break;
|
|
|
|
|
const auto& chunk = pending->terrain.getChunk(cx, cy);
|
|
|
|
|
if (!chunk.hasHeightMap() || chunk.layers.empty()) continue;
|
|
|
|
|
|
|
|
|
|
for (size_t layerIdx = 0; layerIdx < chunk.layers.size(); ++layerIdx) {
|
|
|
|
|
if (added >= kMaxGroundClutterPerTile) break;
|
|
|
|
|
const auto& layer = chunk.layers[layerIdx];
|
|
|
|
|
if (layer.effectId == 0) continue;
|
|
|
|
|
|
|
|
|
|
auto geIt = groundEffectById_.find(layer.effectId);
|
|
|
|
|
if (geIt == groundEffectById_.end() && layer.textureId != 0) {
|
|
|
|
|
geIt = groundEffectById_.find(layer.textureId);
|
|
|
|
|
if (geIt != groundEffectById_.end()) {
|
|
|
|
|
textureIdFallbackMatch++;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
if (geIt == groundEffectById_.end()) {
|
|
|
|
|
noEffectMatch++;
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
const GroundEffectEntry& ge = geIt->second;
|
|
|
|
|
|
|
|
|
|
uint32_t totalWeight = ge.weights[0] + ge.weights[1] + ge.weights[2] + ge.weights[3];
|
|
|
|
|
if (totalWeight == 0) totalWeight = 4;
|
|
|
|
|
|
|
|
|
|
uint32_t density = std::min<uint32_t>(ge.density, 16u);
|
|
|
|
|
density = static_cast<uint32_t>(std::lround(static_cast<float>(density) * densityScale));
|
|
|
|
|
if (density == 0) continue;
|
|
|
|
|
uint32_t attempts = std::max<uint32_t>(3u, density * 2u);
|
|
|
|
|
attempts = std::min<uint32_t>(attempts, kMaxAttemptsPerLayer);
|
|
|
|
|
attemptsTotal += attempts;
|
|
|
|
|
|
|
|
|
|
bool hasAlpha = decodeLayerAlpha(chunk, layerIdx, alphaScratch);
|
|
|
|
|
uint32_t seed = static_cast<uint32_t>(
|
|
|
|
|
((pending->coord.x & 0xFF) << 24) ^
|
|
|
|
|
((pending->coord.y & 0xFF) << 16) ^
|
|
|
|
|
((cx & 0x1F) << 8) ^
|
|
|
|
|
((cy & 0x1F) << 3) ^
|
|
|
|
|
(layerIdx & 0x7));
|
|
|
|
|
auto nextRand = [&seed]() -> uint32_t {
|
|
|
|
|
seed = seed * 1664525u + 1013904223u;
|
|
|
|
|
return seed;
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
for (uint32_t a = 0; a < attempts; ++a) {
|
|
|
|
|
float fracX = (nextRand() & 0xFFFFu) / 65535.0f * 8.0f;
|
|
|
|
|
float fracY = (nextRand() & 0xFFFFu) / 65535.0f * 8.0f;
|
|
|
|
|
|
|
|
|
|
if (hasAlpha && !alphaScratch.empty()) {
|
|
|
|
|
int alphaX = glm::clamp(static_cast<int>((fracX / 8.0f) * 63.0f), 0, 63);
|
|
|
|
|
int alphaY = glm::clamp(static_cast<int>((fracY / 8.0f) * 63.0f), 0, 63);
|
|
|
|
|
int alphaIndex = alphaY * 64 + alphaX;
|
|
|
|
|
if (alphaIndex < 0 || alphaIndex >= static_cast<int>(alphaScratch.size())) continue;
|
|
|
|
|
if (alphaScratch[alphaIndex] < 64) {
|
|
|
|
|
alphaRejected++;
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (hasRoadLikeTextureAt(chunk, fracX, fracY)) {
|
|
|
|
|
roadRejected++;
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
uint32_t roll = nextRand() % totalWeight;
|
|
|
|
|
int pick = 0;
|
|
|
|
|
uint32_t acc = 0;
|
|
|
|
|
for (int i = 0; i < 4; ++i) {
|
|
|
|
|
uint32_t w = ge.weights[i] > 0 ? ge.weights[i] : 1;
|
|
|
|
|
acc += w;
|
|
|
|
|
if (roll < acc) { pick = i; break; }
|
|
|
|
|
}
|
|
|
|
|
uint32_t doodadId = ge.doodadIds[pick];
|
|
|
|
|
if (doodadId == 0) continue;
|
|
|
|
|
|
|
|
|
|
auto doodadIt = groundDoodadModelById_.find(doodadId);
|
|
|
|
|
if (doodadIt == groundDoodadModelById_.end()) {
|
|
|
|
|
noDoodadModel++;
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
const std::string& doodadModelPath = doodadIt->second;
|
|
|
|
|
uint32_t modelId = static_cast<uint32_t>(std::hash<std::string>{}(doodadModelPath));
|
|
|
|
|
if (!ensureModelPrepared(doodadModelPath, modelId)) {
|
|
|
|
|
modelId = static_cast<uint32_t>(std::hash<std::string>{}(kGroundClutterProxyModel));
|
|
|
|
|
if (!ensureModelPrepared(kGroundClutterProxyModel, modelId)) {
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
float worldX = chunk.position[0] - fracY * unitSize;
|
|
|
|
|
float worldY = chunk.position[1] - fracX * unitSize;
|
|
|
|
|
|
|
|
|
|
int gx0 = glm::clamp(static_cast<int>(std::floor(fracX)), 0, 8);
|
|
|
|
|
int gy0 = glm::clamp(static_cast<int>(std::floor(fracY)), 0, 8);
|
|
|
|
|
int gx1 = std::min(gx0 + 1, 8);
|
|
|
|
|
int gy1 = std::min(gy0 + 1, 8);
|
|
|
|
|
float tx = fracX - static_cast<float>(gx0);
|
|
|
|
|
float ty = fracY - static_cast<float>(gy0);
|
|
|
|
|
float h00 = chunk.heightMap.getHeight(gx0, gy0);
|
|
|
|
|
float h10 = chunk.heightMap.getHeight(gx1, gy0);
|
|
|
|
|
float h01 = chunk.heightMap.getHeight(gx0, gy1);
|
|
|
|
|
float h11 = chunk.heightMap.getHeight(gx1, gy1);
|
|
|
|
|
float worldZ = chunk.position[2] +
|
|
|
|
|
(h00 * (1 - tx) * (1 - ty) +
|
|
|
|
|
h10 * tx * (1 - ty) +
|
|
|
|
|
h01 * (1 - tx) * ty +
|
|
|
|
|
h11 * tx * ty);
|
|
|
|
|
|
|
|
|
|
PendingTile::M2Placement p;
|
|
|
|
|
p.modelId = modelId;
|
|
|
|
|
p.uniqueId = 0;
|
|
|
|
|
// MCNK chunk.position is already in terrain/render world space.
|
|
|
|
|
// Do not convert via ADT placement mapping (that is for MDDF/MODF records).
|
|
|
|
|
p.rotation = glm::vec3(0.0f, 0.0f, (nextRand() & 0xFFFFu) / 65535.0f * (2.0f * pi));
|
|
|
|
|
p.scale = 0.80f + ((nextRand() & 0xFFFFu) / 65535.0f) * 0.35f;
|
|
|
|
|
// Snap directly to sampled terrain height.
|
|
|
|
|
p.position = glm::vec3(worldX, worldY, worldZ + 0.01f);
|
|
|
|
|
pending->m2Placements.push_back(p);
|
|
|
|
|
added++;
|
|
|
|
|
perChunkAdded[cy * 16 + cx]++;
|
|
|
|
|
if (added >= kMaxGroundClutterPerTile) break;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
size_t fallbackAdded = 0;
|
|
|
|
|
const size_t kMinGroundClutterPerTile = static_cast<size_t>(std::lround(40.0f * densityScale));
|
|
|
|
|
size_t fallbackNeeded = (added < kMinGroundClutterPerTile) ? (kMinGroundClutterPerTile - added) : 0;
|
|
|
|
|
if (fallbackNeeded > 0) {
|
|
|
|
|
const uint32_t proxyModelId = static_cast<uint32_t>(std::hash<std::string>{}(kGroundClutterProxyModel));
|
|
|
|
|
if (ensureModelPrepared(kGroundClutterProxyModel, proxyModelId)) {
|
|
|
|
|
constexpr uint32_t kFallbackPerChunk = 2;
|
|
|
|
|
for (int cy = 0; cy < 16; ++cy) {
|
|
|
|
|
for (int cx = 0; cx < 16; ++cx) {
|
|
|
|
|
if (fallbackAdded >= fallbackNeeded || added >= kMaxGroundClutterPerTile) break;
|
|
|
|
|
const auto& chunk = pending->terrain.getChunk(cx, cy);
|
|
|
|
|
if (!chunk.hasHeightMap()) continue;
|
|
|
|
|
|
|
|
|
|
for (uint32_t i = 0; i < kFallbackPerChunk; ++i) {
|
|
|
|
|
if (fallbackAdded >= fallbackNeeded || added >= kMaxGroundClutterPerTile) break;
|
|
|
|
|
// Deterministic scatter so the tile stays visually stable.
|
|
|
|
|
uint32_t seed = static_cast<uint32_t>(
|
|
|
|
|
((pending->coord.x & 0xFF) << 24) ^
|
|
|
|
|
((pending->coord.y & 0xFF) << 16) ^
|
|
|
|
|
((cx & 0x1F) << 8) ^
|
|
|
|
|
((cy & 0x1F) << 3) ^
|
|
|
|
|
(i & 0x7));
|
|
|
|
|
auto nextRand = [&seed]() -> uint32_t {
|
|
|
|
|
seed = seed * 1664525u + 1013904223u;
|
|
|
|
|
return seed;
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
float fracX = (nextRand() & 0xFFFFu) / 65535.0f * 8.0f;
|
|
|
|
|
float fracY = (nextRand() & 0xFFFFu) / 65535.0f * 8.0f;
|
|
|
|
|
if (hasRoadLikeTextureAt(chunk, fracX, fracY)) {
|
|
|
|
|
roadRejected++;
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
float worldX = chunk.position[0] - fracY * unitSize;
|
|
|
|
|
float worldY = chunk.position[1] - fracX * unitSize;
|
|
|
|
|
|
|
|
|
|
int gx0 = glm::clamp(static_cast<int>(std::floor(fracX)), 0, 8);
|
|
|
|
|
int gy0 = glm::clamp(static_cast<int>(std::floor(fracY)), 0, 8);
|
|
|
|
|
int gx1 = std::min(gx0 + 1, 8);
|
|
|
|
|
int gy1 = std::min(gy0 + 1, 8);
|
|
|
|
|
float tx = fracX - static_cast<float>(gx0);
|
|
|
|
|
float ty = fracY - static_cast<float>(gy0);
|
|
|
|
|
float h00 = chunk.heightMap.getHeight(gx0, gy0);
|
|
|
|
|
float h10 = chunk.heightMap.getHeight(gx1, gy0);
|
|
|
|
|
float h01 = chunk.heightMap.getHeight(gx0, gy1);
|
|
|
|
|
float h11 = chunk.heightMap.getHeight(gx1, gy1);
|
|
|
|
|
float worldZ = chunk.position[2] +
|
|
|
|
|
(h00 * (1 - tx) * (1 - ty) +
|
|
|
|
|
h10 * tx * (1 - ty) +
|
|
|
|
|
h01 * (1 - tx) * ty +
|
|
|
|
|
h11 * tx * ty);
|
|
|
|
|
|
|
|
|
|
PendingTile::M2Placement p;
|
|
|
|
|
p.modelId = proxyModelId;
|
|
|
|
|
p.uniqueId = 0;
|
|
|
|
|
p.rotation = glm::vec3(0.0f, 0.0f, (nextRand() & 0xFFFFu) / 65535.0f * (2.0f * pi));
|
|
|
|
|
p.scale = 0.75f + ((nextRand() & 0xFFFFu) / 65535.0f) * 0.40f;
|
|
|
|
|
p.position = glm::vec3(worldX, worldY, worldZ + 0.01f);
|
|
|
|
|
pending->m2Placements.push_back(p);
|
|
|
|
|
fallbackAdded++;
|
|
|
|
|
added++;
|
|
|
|
|
perChunkAdded[cy * 16 + cx]++;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
if (fallbackAdded >= fallbackNeeded || added >= kMaxGroundClutterPerTile) break;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Baseline pass disabled: one-per-chunk fill caused large instance spikes and hitches
|
|
|
|
|
// when streaming tiles around the player.
|
|
|
|
|
size_t baselineAdded = 0;
|
|
|
|
|
|
|
|
|
|
if (added > 0) {
|
|
|
|
|
static int clutterLogCount = 0;
|
|
|
|
|
if (clutterLogCount < 12) {
|
|
|
|
|
LOG_INFO("Ground clutter tile [", pending->coord.x, ",", pending->coord.y,
|
|
|
|
|
"] added=", added, " attempts=", attemptsTotal,
|
|
|
|
|
" fallbackAdded=", fallbackAdded,
|
|
|
|
|
" baselineAdded=", baselineAdded,
|
|
|
|
|
" roadRejected=", roadRejected);
|
|
|
|
|
clutterLogCount++;
|
|
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
static int noClutterLogCount = 0;
|
|
|
|
|
if (noClutterLogCount < 8) {
|
|
|
|
|
LOG_INFO("Ground clutter tile [", pending->coord.x, ",", pending->coord.y,
|
|
|
|
|
"] added=0 attempts=", attemptsTotal,
|
|
|
|
|
" alphaRejected=", alphaRejected,
|
|
|
|
|
" roadRejected=", roadRejected,
|
|
|
|
|
" noEffect=", noEffectMatch,
|
|
|
|
|
" textureFallback=", textureIdFallbackMatch,
|
|
|
|
|
" noDoodadModel=", noDoodadModel,
|
|
|
|
|
" modelMissing=", modelMissing,
|
|
|
|
|
" modelInvalid=", modelInvalid);
|
|
|
|
|
noClutterLogCount++;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-02 12:24:50 -08:00
|
|
|
std::optional<float> TerrainManager::getHeightAt(float glX, float glY) const {
|
2026-02-02 23:36:49 -08:00
|
|
|
// Terrain mesh vertices use chunk.position directly (WoW coordinates)
|
|
|
|
|
// But camera is in GL coordinates. We query using the mesh coordinates directly
|
|
|
|
|
// since terrain is rendered without model transformation.
|
2026-02-02 12:24:50 -08:00
|
|
|
//
|
2026-02-02 23:36:49 -08:00
|
|
|
// The terrain mesh generation puts vertices at:
|
|
|
|
|
// vertex.position[0] = chunk.position[0] - (offsetY * unitSize)
|
|
|
|
|
// vertex.position[1] = chunk.position[1] - (offsetX * unitSize)
|
|
|
|
|
// vertex.position[2] = chunk.position[2] + height
|
|
|
|
|
//
|
|
|
|
|
// So chunk spans:
|
2026-02-02 12:24:50 -08:00
|
|
|
// X: [chunk.position[0] - 8*unitSize, chunk.position[0]]
|
|
|
|
|
// Y: [chunk.position[1] - 8*unitSize, chunk.position[1]]
|
|
|
|
|
|
|
|
|
|
const float unitSize = CHUNK_SIZE / 8.0f;
|
|
|
|
|
|
2026-02-03 17:21:04 -08:00
|
|
|
auto sampleTileHeight = [&](const TerrainTile* tile) -> std::optional<float> {
|
|
|
|
|
if (!tile || !tile->loaded) return std::nullopt;
|
2026-02-02 12:24:50 -08:00
|
|
|
|
2026-02-03 17:21:04 -08:00
|
|
|
auto sampleChunk = [&](int cx, int cy) -> std::optional<float> {
|
|
|
|
|
if (cx < 0 || cx >= 16 || cy < 0 || cy >= 16) return std::nullopt;
|
|
|
|
|
const auto& chunk = tile->terrain.getChunk(cx, cy);
|
|
|
|
|
if (!chunk.hasHeightMap()) return std::nullopt;
|
2026-02-02 12:24:50 -08:00
|
|
|
|
2026-02-03 17:21:04 -08:00
|
|
|
float chunkMaxX = chunk.position[0];
|
|
|
|
|
float chunkMinX = chunk.position[0] - 8.0f * unitSize;
|
|
|
|
|
float chunkMaxY = chunk.position[1];
|
|
|
|
|
float chunkMinY = chunk.position[1] - 8.0f * unitSize;
|
2026-02-02 12:24:50 -08:00
|
|
|
|
2026-02-03 17:21:04 -08:00
|
|
|
if (glX < chunkMinX || glX > chunkMaxX ||
|
|
|
|
|
glY < chunkMinY || glY > chunkMaxY) {
|
|
|
|
|
return std::nullopt;
|
|
|
|
|
}
|
2026-02-02 12:24:50 -08:00
|
|
|
|
2026-02-03 17:21:04 -08:00
|
|
|
// Fractional position within chunk (0-8 range)
|
|
|
|
|
float fracY = (chunk.position[0] - glX) / unitSize; // maps to offsetY
|
|
|
|
|
float fracX = (chunk.position[1] - glY) / unitSize; // maps to offsetX
|
|
|
|
|
|
|
|
|
|
fracX = glm::clamp(fracX, 0.0f, 8.0f);
|
|
|
|
|
fracY = glm::clamp(fracY, 0.0f, 8.0f);
|
|
|
|
|
|
|
|
|
|
// Bilinear interpolation on 9x9 outer grid
|
|
|
|
|
int gx0 = static_cast<int>(std::floor(fracX));
|
|
|
|
|
int gy0 = static_cast<int>(std::floor(fracY));
|
|
|
|
|
int gx1 = std::min(gx0 + 1, 8);
|
|
|
|
|
int gy1 = std::min(gy0 + 1, 8);
|
|
|
|
|
|
|
|
|
|
float tx = fracX - gx0;
|
|
|
|
|
float ty = fracY - gy0;
|
|
|
|
|
|
|
|
|
|
float h00 = chunk.heightMap.heights[gy0 * 17 + gx0];
|
|
|
|
|
float h10 = chunk.heightMap.heights[gy0 * 17 + gx1];
|
|
|
|
|
float h01 = chunk.heightMap.heights[gy1 * 17 + gx0];
|
|
|
|
|
float h11 = chunk.heightMap.heights[gy1 * 17 + gx1];
|
|
|
|
|
|
|
|
|
|
float h = h00 * (1 - tx) * (1 - ty) +
|
|
|
|
|
h10 * tx * (1 - ty) +
|
|
|
|
|
h01 * (1 - tx) * ty +
|
|
|
|
|
h11 * tx * ty;
|
|
|
|
|
|
|
|
|
|
return chunk.position[2] + h;
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// Fast path: infer likely chunk index and probe 3x3 neighborhood.
|
|
|
|
|
int guessCy = glm::clamp(static_cast<int>(std::floor((tile->maxX - glX) / CHUNK_SIZE)), 0, 15);
|
|
|
|
|
int guessCx = glm::clamp(static_cast<int>(std::floor((tile->maxY - glY) / CHUNK_SIZE)), 0, 15);
|
|
|
|
|
for (int dy = -1; dy <= 1; dy++) {
|
|
|
|
|
for (int dx = -1; dx <= 1; dx++) {
|
|
|
|
|
auto h = sampleChunk(guessCx + dx, guessCy + dy);
|
|
|
|
|
if (h) return h;
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-02-02 12:24:50 -08:00
|
|
|
|
2026-02-03 17:21:04 -08:00
|
|
|
// Fallback full scan for robustness at seams/unusual coords.
|
|
|
|
|
for (int cy = 0; cy < 16; cy++) {
|
|
|
|
|
for (int cx = 0; cx < 16; cx++) {
|
|
|
|
|
auto h = sampleChunk(cx, cy);
|
|
|
|
|
if (h) {
|
|
|
|
|
return h;
|
|
|
|
|
}
|
2026-02-02 12:24:50 -08:00
|
|
|
}
|
|
|
|
|
}
|
2026-02-03 17:21:04 -08:00
|
|
|
return std::nullopt;
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// Fast path: sample the expected containing tile first.
|
|
|
|
|
TileCoord tc = worldToTile(glX, glY);
|
|
|
|
|
auto it = loadedTiles.find(tc);
|
|
|
|
|
if (it != loadedTiles.end()) {
|
|
|
|
|
auto h = sampleTileHeight(it->second.get());
|
|
|
|
|
if (h) return h;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Fallback: check all loaded tiles (handles seam/edge coordinate ambiguity).
|
|
|
|
|
for (const auto& [coord, tile] : loadedTiles) {
|
|
|
|
|
if (coord == tc) continue;
|
|
|
|
|
auto h = sampleTileHeight(tile.get());
|
|
|
|
|
if (h) return h;
|
2026-02-02 12:24:50 -08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return std::nullopt;
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-03 14:55:32 -08:00
|
|
|
std::optional<std::string> TerrainManager::getDominantTextureAt(float glX, float glY) const {
|
|
|
|
|
const float unitSize = CHUNK_SIZE / 8.0f;
|
|
|
|
|
std::vector<uint8_t> alphaScratch;
|
2026-02-03 17:21:04 -08:00
|
|
|
auto sampleTileTexture = [&](const TerrainTile* tile) -> std::optional<std::string> {
|
|
|
|
|
if (!tile || !tile->loaded) return std::nullopt;
|
|
|
|
|
|
|
|
|
|
auto sampleChunkTexture = [&](int cx, int cy) -> std::optional<std::string> {
|
|
|
|
|
if (cx < 0 || cx >= 16 || cy < 0 || cy >= 16) return std::nullopt;
|
|
|
|
|
const auto& chunk = tile->terrain.getChunk(cx, cy);
|
|
|
|
|
if (!chunk.hasHeightMap() || chunk.layers.empty()) return std::nullopt;
|
|
|
|
|
|
|
|
|
|
float chunkMaxX = chunk.position[0];
|
|
|
|
|
float chunkMinX = chunk.position[0] - 8.0f * unitSize;
|
|
|
|
|
float chunkMaxY = chunk.position[1];
|
|
|
|
|
float chunkMinY = chunk.position[1] - 8.0f * unitSize;
|
|
|
|
|
if (glX < chunkMinX || glX > chunkMaxX || glY < chunkMinY || glY > chunkMaxY) {
|
|
|
|
|
return std::nullopt;
|
|
|
|
|
}
|
2026-02-03 14:55:32 -08:00
|
|
|
|
2026-02-03 17:21:04 -08:00
|
|
|
float fracY = (chunk.position[0] - glX) / unitSize;
|
|
|
|
|
float fracX = (chunk.position[1] - glY) / unitSize;
|
|
|
|
|
fracX = glm::clamp(fracX, 0.0f, 8.0f);
|
|
|
|
|
fracY = glm::clamp(fracY, 0.0f, 8.0f);
|
|
|
|
|
|
|
|
|
|
int alphaX = glm::clamp(static_cast<int>((fracX / 8.0f) * 63.0f), 0, 63);
|
|
|
|
|
int alphaY = glm::clamp(static_cast<int>((fracY / 8.0f) * 63.0f), 0, 63);
|
|
|
|
|
int alphaIndex = alphaY * 64 + alphaX;
|
|
|
|
|
|
2026-02-04 11:31:08 -08:00
|
|
|
int weights[4] = {0, 0, 0, 0};
|
|
|
|
|
size_t numLayers = std::min(chunk.layers.size(), static_cast<size_t>(4));
|
2026-02-03 17:21:04 -08:00
|
|
|
int accum = 0;
|
2026-02-04 11:31:08 -08:00
|
|
|
for (size_t layerIdx = 1; layerIdx < numLayers; layerIdx++) {
|
2026-02-03 17:21:04 -08:00
|
|
|
int alpha = 0;
|
|
|
|
|
if (decodeLayerAlpha(chunk, layerIdx, alphaScratch) && alphaIndex < static_cast<int>(alphaScratch.size())) {
|
|
|
|
|
alpha = alphaScratch[alphaIndex];
|
2026-02-03 14:55:32 -08:00
|
|
|
}
|
2026-02-03 17:21:04 -08:00
|
|
|
weights[layerIdx] = alpha;
|
|
|
|
|
accum += alpha;
|
|
|
|
|
}
|
|
|
|
|
weights[0] = glm::clamp(255 - accum, 0, 255);
|
|
|
|
|
|
|
|
|
|
size_t bestLayer = 0;
|
|
|
|
|
int bestWeight = weights[0];
|
2026-02-04 11:31:08 -08:00
|
|
|
for (size_t i = 1; i < numLayers; i++) {
|
2026-02-03 17:21:04 -08:00
|
|
|
if (weights[i] > bestWeight) {
|
|
|
|
|
bestWeight = weights[i];
|
|
|
|
|
bestLayer = i;
|
2026-02-03 14:55:32 -08:00
|
|
|
}
|
2026-02-03 17:21:04 -08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
uint32_t texId = chunk.layers[bestLayer].textureId;
|
|
|
|
|
if (texId < tile->terrain.textures.size()) {
|
|
|
|
|
return tile->terrain.textures[texId];
|
|
|
|
|
}
|
|
|
|
|
return std::nullopt;
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
int guessCy = glm::clamp(static_cast<int>(std::floor((tile->maxX - glX) / CHUNK_SIZE)), 0, 15);
|
|
|
|
|
int guessCx = glm::clamp(static_cast<int>(std::floor((tile->maxY - glY) / CHUNK_SIZE)), 0, 15);
|
|
|
|
|
for (int dy = -1; dy <= 1; dy++) {
|
|
|
|
|
for (int dx = -1; dx <= 1; dx++) {
|
|
|
|
|
auto tex = sampleChunkTexture(guessCx + dx, guessCy + dy);
|
|
|
|
|
if (tex) return tex;
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-02-03 14:55:32 -08:00
|
|
|
|
2026-02-03 17:21:04 -08:00
|
|
|
for (int cy = 0; cy < 16; cy++) {
|
|
|
|
|
for (int cx = 0; cx < 16; cx++) {
|
|
|
|
|
auto tex = sampleChunkTexture(cx, cy);
|
|
|
|
|
if (tex) {
|
|
|
|
|
return tex;
|
2026-02-03 14:55:32 -08:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-02-03 17:21:04 -08:00
|
|
|
return std::nullopt;
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// Fast path: check expected containing tile first.
|
|
|
|
|
TileCoord tc = worldToTile(glX, glY);
|
|
|
|
|
auto it = loadedTiles.find(tc);
|
|
|
|
|
if (it != loadedTiles.end()) {
|
|
|
|
|
auto tex = sampleTileTexture(it->second.get());
|
|
|
|
|
if (tex) return tex;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Fallback: seam/edge case.
|
|
|
|
|
for (const auto& [coord, tile] : loadedTiles) {
|
|
|
|
|
if (coord == tc) continue;
|
|
|
|
|
auto tex = sampleTileTexture(tile.get());
|
|
|
|
|
if (tex) return tex;
|
2026-02-03 14:55:32 -08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return std::nullopt;
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-02 12:24:50 -08:00
|
|
|
void TerrainManager::streamTiles() {
|
2026-02-11 22:27:02 -08:00
|
|
|
auto shouldSkipMissingAdt = [this](const TileCoord& coord) -> bool {
|
|
|
|
|
if (!assetManager) return false;
|
|
|
|
|
if (failedTiles.find(coord) != failedTiles.end()) return true;
|
|
|
|
|
const std::string adtPath = getADTPath(coord);
|
|
|
|
|
if (!assetManager->fileExists(adtPath)) {
|
|
|
|
|
// Mark permanently failed so future stream/precache passes do not retry.
|
|
|
|
|
failedTiles[coord] = true;
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
return false;
|
|
|
|
|
};
|
|
|
|
|
|
2026-02-02 12:24:50 -08:00
|
|
|
// Enqueue tiles in radius around current tile for async loading
|
|
|
|
|
{
|
|
|
|
|
std::lock_guard<std::mutex> lock(queueMutex);
|
|
|
|
|
|
|
|
|
|
for (int dy = -loadRadius; dy <= loadRadius; dy++) {
|
|
|
|
|
for (int dx = -loadRadius; dx <= loadRadius; dx++) {
|
|
|
|
|
int tileX = currentTile.x + dx;
|
|
|
|
|
int tileY = currentTile.y + dy;
|
|
|
|
|
|
|
|
|
|
// Check valid range
|
|
|
|
|
if (tileX < 0 || tileX > 63 || tileY < 0 || tileY > 63) {
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-08 21:32:38 -08:00
|
|
|
// Circular pattern: skip corner tiles beyond radius (Euclidean distance)
|
|
|
|
|
if (dx*dx + dy*dy > loadRadius*loadRadius) {
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-02 12:24:50 -08:00
|
|
|
TileCoord coord = {tileX, tileY};
|
|
|
|
|
|
|
|
|
|
// Skip if already loaded, pending, or failed
|
|
|
|
|
if (loadedTiles.find(coord) != loadedTiles.end()) continue;
|
|
|
|
|
if (pendingTiles.find(coord) != pendingTiles.end()) continue;
|
|
|
|
|
if (failedTiles.find(coord) != failedTiles.end()) continue;
|
2026-02-11 22:27:02 -08:00
|
|
|
if (shouldSkipMissingAdt(coord)) continue;
|
2026-02-02 12:24:50 -08:00
|
|
|
|
2026-02-11 19:28:15 -08:00
|
|
|
loadQueue.push_back(coord);
|
2026-02-02 12:24:50 -08:00
|
|
|
pendingTiles[coord] = true;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-03 17:21:04 -08:00
|
|
|
// Notify workers that there's work
|
|
|
|
|
queueCV.notify_all();
|
2026-02-02 12:24:50 -08:00
|
|
|
|
|
|
|
|
// Unload tiles beyond unload radius (well past the camera far clip)
|
|
|
|
|
std::vector<TileCoord> tilesToUnload;
|
|
|
|
|
|
|
|
|
|
for (const auto& pair : loadedTiles) {
|
|
|
|
|
const TileCoord& coord = pair.first;
|
|
|
|
|
|
2026-02-08 21:32:38 -08:00
|
|
|
int dx = coord.x - currentTile.x;
|
|
|
|
|
int dy = coord.y - currentTile.y;
|
2026-02-02 12:24:50 -08:00
|
|
|
|
2026-02-08 21:32:38 -08:00
|
|
|
// Circular pattern: unload beyond radius (Euclidean distance)
|
|
|
|
|
if (dx*dx + dy*dy > unloadRadius*unloadRadius) {
|
2026-02-02 12:24:50 -08:00
|
|
|
tilesToUnload.push_back(coord);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
for (const auto& coord : tilesToUnload) {
|
|
|
|
|
unloadTile(coord.x, coord.y);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (!tilesToUnload.empty()) {
|
2026-02-08 22:08:42 -08:00
|
|
|
// Don't clean up models during streaming - keep them in VRAM for performance
|
|
|
|
|
// Modern GPUs have 8-16GB VRAM, models are only ~hundreds of MB
|
|
|
|
|
// Cleanup can be done manually when memory pressure is detected
|
|
|
|
|
// NOTE: Disabled permanent model cleanup to leverage modern VRAM capacity
|
|
|
|
|
// if (m2Renderer) {
|
|
|
|
|
// m2Renderer->cleanupUnusedModels();
|
|
|
|
|
// }
|
|
|
|
|
// if (wmoRenderer) {
|
|
|
|
|
// wmoRenderer->cleanupUnusedModels();
|
|
|
|
|
// }
|
2026-02-07 18:57:34 -08:00
|
|
|
|
2026-02-02 12:24:50 -08:00
|
|
|
LOG_INFO("Unloaded ", tilesToUnload.size(), " distant tiles, ",
|
2026-02-08 22:08:42 -08:00
|
|
|
loadedTiles.size(), " remain (models kept in VRAM)");
|
2026-02-02 12:24:50 -08:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-08 21:32:38 -08:00
|
|
|
void TerrainManager::precacheTiles(const std::vector<std::pair<int, int>>& tiles) {
|
|
|
|
|
std::lock_guard<std::mutex> lock(queueMutex);
|
|
|
|
|
|
|
|
|
|
for (const auto& [x, y] : tiles) {
|
2026-02-11 22:27:02 -08:00
|
|
|
if (x < 0 || x > 63 || y < 0 || y > 63) continue;
|
|
|
|
|
|
2026-02-08 21:32:38 -08:00
|
|
|
TileCoord coord = {x, y};
|
|
|
|
|
|
|
|
|
|
// Skip if already loaded, pending, or failed
|
|
|
|
|
if (loadedTiles.find(coord) != loadedTiles.end()) continue;
|
|
|
|
|
if (pendingTiles.find(coord) != pendingTiles.end()) continue;
|
|
|
|
|
if (failedTiles.find(coord) != failedTiles.end()) continue;
|
2026-02-11 22:27:02 -08:00
|
|
|
if (assetManager && !assetManager->fileExists(getADTPath(coord))) {
|
|
|
|
|
failedTiles[coord] = true;
|
|
|
|
|
continue;
|
|
|
|
|
}
|
2026-02-08 21:32:38 -08:00
|
|
|
|
2026-02-11 19:28:15 -08:00
|
|
|
// Precache work is prioritized so taxi-route tiles are prepared before
|
|
|
|
|
// opportunistic radius streaming tiles.
|
|
|
|
|
loadQueue.push_front(coord);
|
2026-02-08 21:32:38 -08:00
|
|
|
pendingTiles[coord] = true;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Notify workers to start loading
|
|
|
|
|
queueCV.notify_all();
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-02 12:24:50 -08:00
|
|
|
} // namespace rendering
|
|
|
|
|
} // namespace wowee
|