Fix city stuttering with incremental tile finalization and GPU optimizations

Replace monolithic finalizeTile() with a phased state machine that spreads
GPU upload work across multiple frames (TERRAIN→M2→WMO→WATER→AMBIENT→DONE).
Each advanceFinalization() call does one bounded unit of work within the
per-frame time budget, eliminating 50-300ms frame hitches when entering cities.

Additional performance improvements:
- Pre-allocate bone SSBOs at M2 instance creation instead of lazily during
  first render frame, preventing hitches when many skinned characters appear
- Enable WMO distance culling (800 units) with active-group exemption so
  the player's current floor/neighbors are never culled
- Add 4-tier adaptive M2 render distance (250/400/600/1000 based on count)
- Remove dead PendingM2Upload queue code superseded by incremental system

Fix tile re-enqueueing bug: keep tiles in pendingTiles until committed to
loadedTiles (not when moved to finalizingTiles_) so streamTiles() doesn't
re-enqueue tiles mid-finalization. Also handle unloadTile() for tiles in
the finalizingTiles_ deque to prevent orphaned water/M2/WMO resources.
This commit is contained in:
Kelsi 2026-02-25 02:36:23 -08:00
parent 8fe53171eb
commit d47ae2a110
6 changed files with 411 additions and 268 deletions

View file

@ -478,6 +478,7 @@ private:
// Helper to allocate descriptor sets
VkDescriptorSet allocateMaterialSet();
VkDescriptorSet allocateBoneSet();
void preallocateBoneBuffers(M2Instance& instance);
// Helper to destroy model GPU resources
void destroyModelGPU(M2ModelGPU& model);

View file

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

View file

@ -657,9 +657,9 @@ private:
bool wireframeMode = false;
bool frustumCulling = true;
bool portalCulling = false; // Disabled by default - needs debugging
bool distanceCulling = false; // Disabled - causes ground to disappear
float maxGroupDistance = 500.0f;
float maxGroupDistanceSq = 250000.0f; // maxGroupDistance^2
bool distanceCulling = true; // Enabled with active-group exemption to prevent floor disappearing
float maxGroupDistance = 800.0f;
float maxGroupDistanceSq = 640000.0f; // maxGroupDistance^2
uint32_t lastDrawCalls = 0;
mutable uint32_t lastPortalCulledGroups = 0;
mutable uint32_t lastDistanceCulledGroups = 0;