From 63efac9fa66b956bc2ac4ab7cbc0fd81244f8ca0 Mon Sep 17 00:00:00 2001 From: Kelsi Date: Sat, 7 Mar 2026 17:31:47 -0800 Subject: [PATCH] Unlimited creature model uploads during load screen, remove duplicate code Loading screen now calls processCreatureSpawnQueue(unlimited=true) which removes the 1-upload-per-frame cap and 2ms time budget, allowing all pending creature models to upload to GPU in bulk. Also increases concurrent async background loads from 4 to 16 during load screen. Replaces 40-line inline duplicate of processAsyncCreatureResults with the shared function. --- include/core/application.hpp | 4 +-- src/core/application.cpp | 68 +++++++----------------------------- 2 files changed, 14 insertions(+), 58 deletions(-) diff --git a/include/core/application.hpp b/include/core/application.hpp index 84b89f32..165d11bb 100644 --- a/include/core/application.hpp +++ b/include/core/application.hpp @@ -215,7 +215,7 @@ private: std::future future; }; std::vector asyncCreatureLoads_; - void processAsyncCreatureResults(); + void processAsyncCreatureResults(bool unlimited = false); static constexpr int MAX_ASYNC_CREATURE_LOADS = 4; // concurrent background loads std::unordered_set deadCreatureGuids_; // GUIDs that should spawn in corpse/death pose std::unordered_map displayIdModelCache_; // displayId → modelId (model caching) @@ -373,7 +373,7 @@ private: std::unordered_set pendingPlayerSpawnGuids_; void processPlayerSpawnQueue(); std::unordered_set creaturePermanentFailureGuids_; - void processCreatureSpawnQueue(); + void processCreatureSpawnQueue(bool unlimited = false); struct PendingGameObjectSpawn { uint64_t guid; diff --git a/src/core/application.cpp b/src/core/application.cpp index 1a239d8a..23b2c15c 100644 --- a/src/core/application.cpp +++ b/src/core/application.cpp @@ -4207,53 +4207,8 @@ void Application::loadOnlineWorldTerrain(uint32_t mapId, float x, float y, float processPlayerSpawnQueue(); // During load screen warmup: lift per-frame budgets so GPU uploads - // happen in bulk while the loading screen is still visible. - // Process ALL async creature model uploads (no 3-per-frame cap). - { - for (auto it = asyncCreatureLoads_.begin(); it != asyncCreatureLoads_.end(); ) { - if (!it->future.valid() || - it->future.wait_for(std::chrono::milliseconds(0)) != std::future_status::ready) { - ++it; - continue; - } - auto result = it->future.get(); - it = asyncCreatureLoads_.erase(it); - if (result.permanent_failure) { - nonRenderableCreatureDisplayIds_.insert(result.displayId); - creaturePermanentFailureGuids_.insert(result.guid); - pendingCreatureSpawnGuids_.erase(result.guid); - creatureSpawnRetryCounts_.erase(result.guid); - continue; - } - if (!result.valid || !result.model) { - pendingCreatureSpawnGuids_.erase(result.guid); - creatureSpawnRetryCounts_.erase(result.guid); - continue; - } - auto* charRenderer = renderer ? renderer->getCharacterRenderer() : nullptr; - if (!charRenderer) { pendingCreatureSpawnGuids_.erase(result.guid); continue; } - if (!charRenderer->loadModel(*result.model, result.modelId)) { - nonRenderableCreatureDisplayIds_.insert(result.displayId); - creaturePermanentFailureGuids_.insert(result.guid); - pendingCreatureSpawnGuids_.erase(result.guid); - creatureSpawnRetryCounts_.erase(result.guid); - continue; - } - displayIdModelCache_[result.displayId] = result.modelId; - pendingCreatureSpawnGuids_.erase(result.guid); - creatureSpawnRetryCounts_.erase(result.guid); - if (!creatureInstances_.count(result.guid) && - !creaturePermanentFailureGuids_.count(result.guid)) { - PendingCreatureSpawn s{}; - s.guid = result.guid; s.displayId = result.displayId; - s.x = result.x; s.y = result.y; s.z = result.z; - s.orientation = result.orientation; - pendingCreatureSpawns_.push_back(s); - pendingCreatureSpawnGuids_.insert(result.guid); - } - } - } - processCreatureSpawnQueue(); + // and spawns happen in bulk while the loading screen is still visible. + processCreatureSpawnQueue(true); // unlimited: no model upload cap, no time budget processAsyncNpcCompositeResults(); processDeferredEquipmentQueue(); if (auto* cr = renderer ? renderer->getCharacterRenderer() : nullptr) { @@ -6804,9 +6759,10 @@ void Application::spawnOnlineGameObject(uint64_t guid, uint32_t entry, uint32_t " displayId=", displayId, " at (", x, ", ", y, ", ", z, ")"); } -void Application::processAsyncCreatureResults() { +void Application::processAsyncCreatureResults(bool unlimited) { // Check completed async model loads and finalize on main thread (GPU upload + instance creation). // Limit GPU model uploads per frame to avoid spikes, but always drain cheap bookkeeping. + // In unlimited mode (load screen), process all pending uploads without cap. static constexpr int kMaxModelUploadsPerFrame = 1; int modelUploads = 0; @@ -6819,9 +6775,7 @@ void Application::processAsyncCreatureResults() { // Peek: if this result needs a NEW model upload (not cached) and we've hit // the upload budget, defer to next frame without consuming the future. - if (modelUploads >= kMaxModelUploadsPerFrame) { - // Check if this displayId already has a cached model (cheap spawn, no GPU upload). - // We can't peek the displayId without getting the future, so just break. + if (!unlimited && modelUploads >= kMaxModelUploadsPerFrame) { break; } @@ -6967,13 +6921,14 @@ void Application::processAsyncNpcCompositeResults() { } } -void Application::processCreatureSpawnQueue() { +void Application::processCreatureSpawnQueue(bool unlimited) { auto startTime = std::chrono::steady_clock::now(); // Budget: max 2ms per frame for creature spawning to prevent stutter. + // In unlimited mode (load screen), process everything without budget cap. static constexpr float kSpawnBudgetMs = 2.0f; // First, finalize any async model loads that completed on background threads. - processAsyncCreatureResults(); + processAsyncCreatureResults(unlimited); { auto now = std::chrono::steady_clock::now(); float asyncMs = std::chrono::duration(now - startTime).count(); @@ -6992,11 +6947,11 @@ void Application::processCreatureSpawnQueue() { int asyncLaunched = 0; size_t rotationsLeft = pendingCreatureSpawns_.size(); while (!pendingCreatureSpawns_.empty() && - processed < MAX_SPAWNS_PER_FRAME && + (unlimited || processed < MAX_SPAWNS_PER_FRAME) && rotationsLeft > 0) { // Check time budget every iteration (including first — async results may // have already consumed the budget via GPU model uploads). - { + if (!unlimited) { auto now = std::chrono::steady_clock::now(); float elapsedMs = std::chrono::duration(now - startTime).count(); if (elapsedMs >= kSpawnBudgetMs) break; @@ -7017,7 +6972,8 @@ void Application::processCreatureSpawnQueue() { // For new models: launch async load on background thread instead of blocking. if (needsNewModel) { - if (static_cast(asyncCreatureLoads_.size()) + asyncLaunched >= MAX_ASYNC_CREATURE_LOADS) { + const int maxAsync = unlimited ? (MAX_ASYNC_CREATURE_LOADS * 4) : MAX_ASYNC_CREATURE_LOADS; + if (static_cast(asyncCreatureLoads_.size()) + asyncLaunched >= maxAsync) { // Too many in-flight — defer to next frame pendingCreatureSpawns_.push_back(s); rotationsLeft--;