Kelsidavis-WoWee/include/rendering/terrain_manager.hpp
Pavel Okhlopkov 2e8856bacd memory, threading, network hardening
Signed-off-by: Pavel Okhlopkov <pavel.okhlopkov@flant.com>
2026-04-06 21:19:37 +03:00

434 lines
15 KiB
C++
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#pragma once
#include "pipeline/adt_loader.hpp"
#include "pipeline/terrain_mesh.hpp"
#include "pipeline/m2_loader.hpp"
#include "pipeline/wmo_loader.hpp"
#include "pipeline/blp_loader.hpp"
#include <string>
#include <unordered_map>
#include <unordered_set>
#include <memory>
#include <optional>
#include <thread>
#include <mutex>
#include <atomic>
#include <queue>
#include <list>
#include <vector>
#include <condition_variable>
#include <deque>
#include <glm/glm.hpp>
#include <array>
namespace wowee {
namespace pipeline { class AssetManager; }
namespace audio { class AmbientSoundManager; }
namespace rendering { class TerrainRenderer; class Camera; class WaterRenderer; class M2Renderer; class WMORenderer; }
namespace rendering {
/**
* Terrain tile coordinates
*/
struct TileCoord {
int x;
int y;
bool operator==(const TileCoord& other) const {
return x == other.x && y == other.y;
}
struct Hash {
size_t operator()(const TileCoord& coord) const {
return std::hash<int>()(coord.x) ^ (std::hash<int>()(coord.y) << 1);
}
};
};
/**
* Loaded terrain tile data
*/
struct TerrainTile {
TileCoord coord;
pipeline::ADTTerrain terrain;
pipeline::TerrainMesh mesh;
bool loaded = false;
// Tile bounds in world coordinates
float minX, minY, maxX, maxY;
// Instance IDs for cleanup on unload
std::vector<uint32_t> wmoInstanceIds;
std::vector<uint32_t> wmoUniqueIds; // For WMO dedup cleanup on unload
std::vector<uint32_t> m2InstanceIds;
std::vector<uint32_t> doodadUniqueIds; // For dedup cleanup on unload
};
/**
* Pre-processed tile data ready for GPU upload (produced by background thread)
*/
struct PendingTile {
TileCoord coord;
pipeline::ADTTerrain terrain;
pipeline::TerrainMesh mesh;
// Pre-loaded M2 data
struct M2Ready {
uint32_t modelId;
pipeline::M2Model model;
std::string path;
};
std::vector<M2Ready> m2Models;
// M2 instance placement data (references modelId from m2Models)
struct M2Placement {
uint32_t modelId;
uint32_t uniqueId;
glm::vec3 position;
glm::vec3 rotation;
float scale;
};
std::vector<M2Placement> m2Placements;
// Pre-loaded WMO data
struct WMOReady {
uint32_t modelId;
uint32_t uniqueId;
pipeline::WMOModel model;
glm::vec3 position;
glm::vec3 rotation;
};
std::vector<WMOReady> wmoModels;
// WMO doodad M2 models (M2s placed inside WMOs)
struct WMODoodadReady {
uint32_t modelId;
pipeline::M2Model model;
glm::vec3 worldPosition; // For frustum culling
glm::mat4 modelMatrix; // Pre-computed world transform
};
std::vector<WMODoodadReady> wmoDoodads;
// Ambient sound emitters (detected from doodads)
struct AmbientEmitter {
glm::vec3 position;
uint32_t type; // Maps to AmbientSoundManager::AmbientType
};
std::vector<AmbientEmitter> ambientEmitters;
// Pre-loaded terrain texture BLP data (loaded on background thread to avoid
// blocking file I/O on the main thread during finalizeTile)
std::unordered_map<std::string, pipeline::BLPImage> preloadedTextures;
// Pre-decoded M2 model textures (decoded on background thread)
std::unordered_map<std::string, pipeline::BLPImage> preloadedM2Textures;
// Pre-decoded WMO textures (decoded on background thread)
std::unordered_map<std::string, pipeline::BLPImage> preloadedWMOTextures;
};
/**
* Phases for incremental tile finalization (one bounded unit of work per call)
*/
enum class FinalizationPhase {
TERRAIN, // Upload terrain mesh + textures + water
M2_MODELS, // Upload ONE M2 model per call
M2_INSTANCES, // Create all M2 instances (lightweight struct allocation)
WMO_MODELS, // Upload ONE WMO model per call
WMO_INSTANCES, // Create all WMO instances + load WMO liquids
WMO_DOODADS, // Upload ONE WMO doodad M2 per call
WATER, // Generate water ambient emitters
AMBIENT, // Register ambient emitters + commit tile
DONE // Fully finalized
};
/**
* In-progress tile finalization state — tracks progress across frames
*/
struct FinalizingTile {
std::shared_ptr<PendingTile> pending;
FinalizationPhase phase = FinalizationPhase::TERRAIN;
// Progress indices within current phase
size_t m2ModelIndex = 0; // Next M2 model to upload
size_t m2InstanceIndex = 0; // Next M2 placement to instantiate
size_t wmoModelIndex = 0; // Next WMO model to upload
size_t wmoInstanceIndex = 0; // Next WMO placement to instantiate
size_t wmoDoodadIndex = 0; // Next WMO doodad to upload
size_t wmoLiquidGroupIndex = 0; // Next liquid group within current WMO instance
// Incremental terrain upload state (splits TERRAIN phase across frames)
bool terrainPreloaded = false; // True after preloaded textures uploaded
int terrainChunkNext = 0; // Next chunk index to upload (0-255, row-major)
bool terrainMeshDone = false; // True when all chunks uploaded
// Accumulated results (built up across phases)
std::vector<uint32_t> m2InstanceIds;
std::vector<uint32_t> wmoInstanceIds;
std::vector<uint32_t> tileUniqueIds;
std::vector<uint32_t> tileWmoUniqueIds;
std::unordered_set<uint32_t> uploadedM2ModelIds;
};
/**
* Terrain manager for multi-tile terrain streaming
*
* Handles loading and unloading terrain tiles based on camera position
*/
class TerrainManager {
public:
TerrainManager();
~TerrainManager();
/**
* Initialize terrain manager
* @param assetManager Asset manager for loading files
* @param terrainRenderer Terrain renderer for GPU upload
*/
bool initialize(pipeline::AssetManager* assetManager, TerrainRenderer* terrainRenderer);
/**
* Update terrain streaming based on camera position
* @param camera Current camera
* @param deltaTime Time since last update
*/
void update(const Camera& camera, float deltaTime);
/**
* Set map name
* @param mapName Map name (e.g., "Azeroth", "Kalimdor")
*/
void setMapName(const std::string& mapName) { this->mapName = mapName; }
/**
* Load a single tile
* @param x Tile X coordinate (0-63)
* @param y Tile Y coordinate (0-63)
* @return true if loaded successfully
*/
bool loadTile(int x, int y);
/**
* Enqueue a tile for async loading (returns false if previously failed).
*/
bool enqueueTile(int x, int y);
/**
* Unload a tile
* @param x Tile X coordinate
* @param y Tile Y coordinate
*/
void unloadTile(int x, int y);
/**
* Unload all tiles
*/
void unloadAll();
void stopWorkers(); // Stop worker threads without restarting (for shutdown)
void softReset(); // Clear tile data without stopping worker threads (non-blocking)
/**
* Precache a set of tiles (for taxi routes, etc.)
* @param tiles Vector of (x, y) tile coordinates to preload
*/
void precacheTiles(const std::vector<std::pair<int, int>>& tiles);
/**
* Set streaming parameters
*/
void setLoadRadius(int radius) { loadRadius = radius; }
void setUnloadRadius(int radius) { unloadRadius = radius; }
void setStreamingEnabled(bool enabled) { streamingEnabled = enabled; }
void setUpdateInterval(float seconds) { updateInterval = seconds; }
void setTaxiStreamingMode(bool enabled) { taxiStreamingMode_ = enabled; }
void setGroundClutterDensityScale(float scale) { groundClutterDensityScale_ = glm::clamp(scale, 0.0f, 1.5f); }
float getGroundClutterDensityScale() const { return groundClutterDensityScale_; }
void setWaterRenderer(WaterRenderer* renderer) { waterRenderer = renderer; }
void setM2Renderer(M2Renderer* renderer) { m2Renderer = renderer; }
void setWMORenderer(WMORenderer* renderer) { wmoRenderer = renderer; }
void setAmbientSoundManager(audio::AmbientSoundManager* manager) { ambientSoundManager = manager; }
/**
* Get terrain height at GL coordinates
* @param glX GL X position
* @param glY GL Y position
* @return Height (GL Z) if terrain loaded at that position, empty otherwise
*/
std::optional<float> getHeightAt(float glX, float glY) const;
/**
* Get dominant terrain texture name at a GL position.
* Returns empty if terrain is not loaded at that position.
*/
std::optional<std::string> getDominantTextureAt(float glX, float glY) const;
/**
* Get statistics
*/
int getLoadedTileCount() const { return static_cast<int>(loadedTiles.size()); }
int getPendingTileCount() const { return static_cast<int>(pendingTiles.size()); }
int getReadyQueueCount() const { return static_cast<int>(readyQueue.size()); }
/** Total unfinished tiles (worker threads + ready queue + finalizing) */
int getRemainingTileCount() const { return static_cast<int>(pendingTiles.size() + readyQueue.size() + finalizingTiles_.size()); }
TileCoord getCurrentTile() const { return currentTile; }
/** Process all ready tiles immediately (use during loading screens) */
void processAllReadyTiles();
/** Process one ready tile (for loading screens with per-tile progress updates) */
void processOneReadyTile();
/** Process a bounded batch of ready tiles with async GPU upload (no sync wait) */
void processReadyTiles();
private:
/**
* Get tile coordinates from GL world position
*/
TileCoord worldToTile(float worldX, float worldY) const;
/**
* Get world bounds for a tile
*/
void getTileBounds(const TileCoord& coord, float& minX, float& minY,
float& maxX, float& maxY) const;
/**
* Build ADT file path
*/
std::string getADTPath(const TileCoord& coord) const;
/**
* Load tiles in radius around current tile
*/
void streamTiles();
/**
* Background thread: prepare tile data (CPU work only, no OpenGL)
*/
std::shared_ptr<PendingTile> prepareTile(int x, int y);
/**
* Advance incremental finalization of a tile (one bounded unit of work).
* Returns true when the tile is fully finalized (phase == DONE).
*/
bool advanceFinalization(FinalizingTile& ft);
/**
* Background worker thread loop
*/
void workerLoop();
void ensureGroundEffectTablesLoaded();
void generateGroundClutterPlacements(std::shared_ptr<PendingTile>& pending,
std::unordered_set<uint32_t>& preparedModelIds);
pipeline::AssetManager* assetManager = nullptr;
TerrainRenderer* terrainRenderer = nullptr;
WaterRenderer* waterRenderer = nullptr;
M2Renderer* m2Renderer = nullptr;
WMORenderer* wmoRenderer = nullptr;
audio::AmbientSoundManager* ambientSoundManager = nullptr;
std::string mapName = "Azeroth";
// Loaded tiles (keyed by coordinate)
std::unordered_map<TileCoord, std::unique_ptr<TerrainTile>, TileCoord::Hash> loadedTiles;
// Tiles that failed to load (don't retry)
std::unordered_map<TileCoord, bool, TileCoord::Hash> failedTiles;
// Current tile (where camera is)
TileCoord currentTile = {-1, -1};
TileCoord lastStreamTile = {-1, -1};
// Streaming parameters
bool streamingEnabled = true;
int loadRadius = 6; // Load tiles within this radius (13x13 grid = 169 tiles)
int unloadRadius = 9; // Unload tiles beyond this radius
float updateInterval = 0.033f; // Check streaming every 33ms (~30 fps)
float timeSinceLastUpdate = 0.0f;
float proactiveStreamTimer_ = 0.0f;
bool taxiStreamingMode_ = false;
// Tile size constants (WoW ADT specifications)
// A tile (ADT) = 16x16 chunks = 533.33 units across
// A chunk = 8x8 vertex quads = 33.33 units across
static constexpr float TILE_SIZE = 533.33333f; // One tile = 533.33 units
static constexpr float CHUNK_SIZE = 33.33333f; // One chunk = 33.33 units
// Background loading worker pool
std::vector<std::thread> workerThreads;
int workerCount = 0;
// THREAD-SAFE: guards loadQueue, readyQueue, and pendingTiles.
// Workers wait on queueCV; main thread signals when new tiles are enqueued
// or when readyQueue drains below maxReadyQueueSize_.
std::mutex queueMutex;
std::condition_variable queueCV;
std::deque<TileCoord> loadQueue; // THREAD-SAFE: protected by queueMutex
std::queue<std::shared_ptr<PendingTile>> readyQueue; // THREAD-SAFE: protected by queueMutex
// Maximum number of prepared-but-not-finalized tiles in readyQueue.
// Each prepared tile can hold 100500 MB of decoded textures in RAM.
// Workers sleep when this limit is reached, letting the main thread
// finalize (GPU-upload + free) before more tiles are prepared.
static constexpr size_t maxReadyQueueSize_ = 3;
// In-RAM tile cache (LRU) to avoid re-reading from disk
struct CachedTile {
std::shared_ptr<PendingTile> tile;
size_t bytes = 0;
std::list<TileCoord>::iterator lruIt;
};
// THREAD-SAFE: protected by tileCacheMutex_.
std::unordered_map<TileCoord, CachedTile, TileCoord::Hash> tileCache_;
std::list<TileCoord> tileCacheLru_;
size_t tileCacheBytes_ = 0;
size_t tileCacheBudgetBytes_ = 8ull * 1024 * 1024 * 1024; // Dynamic, set at init based on RAM
std::mutex tileCacheMutex_;
std::shared_ptr<PendingTile> getCachedTile(const TileCoord& coord);
void putCachedTile(const std::shared_ptr<PendingTile>& tile);
size_t estimatePendingTileBytes(const PendingTile& tile) const;
void logMissingAdtOnce(const std::string& adtPath);
std::atomic<bool> workerRunning{false};
// Track tiles currently queued or being processed to avoid duplicates
std::unordered_map<TileCoord, bool, TileCoord::Hash> pendingTiles; // THREAD-SAFE: protected by queueMutex
std::unordered_set<std::string> missingAdtWarnings_; // THREAD-SAFE: protected by missingAdtWarningsMutex_
std::mutex missingAdtWarningsMutex_;
// Thread-safe set of M2 model IDs already uploaded to GPU
// (checked by workers to skip redundant file I/O + parsing)
std::unordered_set<uint32_t> uploadedM2Ids_;
std::mutex uploadedM2IdsMutex_;
// Cross-tile dedup for WMO doodad preparation on background workers
// (prevents re-parsing thousands of doodads when same WMO spans multiple tiles)
std::unordered_set<uint32_t> preparedWmoUniqueIds_;
std::mutex preparedWmoUniqueIdsMutex_;
// MAIN-THREAD-ONLY: checked and modified in processReadyTiles() and unloadDistantTiles(),
// both of which run exclusively on the main thread.
std::unordered_set<uint32_t> placedDoodadIds;
// MAIN-THREAD-ONLY: same contract as placedDoodadIds.
std::unordered_set<uint32_t> placedWmoIds;
// Tiles currently being incrementally finalized across frames
std::deque<FinalizingTile> finalizingTiles_;
struct GroundEffectEntry {
std::array<uint32_t, 4> doodadIds{{0, 0, 0, 0}};
std::array<uint32_t, 4> weights{{0, 0, 0, 0}};
uint32_t density = 0;
};
bool groundEffectsLoaded_ = false;
std::unordered_map<uint32_t, GroundEffectEntry> groundEffectById_; // effectId -> config
std::unordered_map<uint32_t, std::string> groundDoodadModelById_; // doodadId -> model path
float groundClutterDensityScale_ = 1.0f;
};
} // namespace rendering
} // namespace wowee