perf: limit NPC composite texture processing to 2ms per frame

processAsyncNpcCompositeResults() had no per-frame budget cap, so when
many NPCs finished async skin compositing simultaneously (e.g. right
after world load), all results were finalized in a single frame causing
up to 284ms frame stalls. Apply the same 2ms budget pattern used by
processAsyncCreatureResults. Load screen still processes all pending
composites without the cap (unlimited=true).
This commit is contained in:
Kelsi 2026-03-10 06:47:33 -07:00
parent ac0fe1bd61
commit dc2aab5e90
2 changed files with 13 additions and 3 deletions

View file

@ -361,7 +361,7 @@ private:
std::future<PreparedNpcComposite> future; std::future<PreparedNpcComposite> future;
}; };
std::vector<AsyncNpcCompositeLoad> asyncNpcCompositeLoads_; std::vector<AsyncNpcCompositeLoad> asyncNpcCompositeLoads_;
void processAsyncNpcCompositeResults(); void processAsyncNpcCompositeResults(bool unlimited = false);
// Cache base player model geometry by (raceId, genderId) // Cache base player model geometry by (raceId, genderId)
std::unordered_map<uint32_t, uint32_t> playerModelCache_; // key=(race<<8)|gender → modelId std::unordered_map<uint32_t, uint32_t> playerModelCache_; // key=(race<<8)|gender → modelId
struct PlayerTextureSlots { int skin = -1; int hair = -1; int underwear = -1; }; struct PlayerTextureSlots { int skin = -1; int hair = -1; int underwear = -1; };

View file

@ -4397,7 +4397,7 @@ void Application::loadOnlineWorldTerrain(uint32_t mapId, float x, float y, float
// During load screen warmup: lift per-frame budgets so GPU uploads // During load screen warmup: lift per-frame budgets so GPU uploads
// and spawns happen in bulk while the loading screen is still visible. // and spawns happen in bulk while the loading screen is still visible.
processCreatureSpawnQueue(true); processCreatureSpawnQueue(true);
processAsyncNpcCompositeResults(); processAsyncNpcCompositeResults(true);
// Process equipment queue more aggressively during warmup (multiple per iteration) // Process equipment queue more aggressively during warmup (multiple per iteration)
for (int i = 0; i < 8 && (!deferredEquipmentQueue_.empty() || !asyncEquipmentLoads_.empty()); i++) { for (int i = 0; i < 8 && (!deferredEquipmentQueue_.empty() || !asyncEquipmentLoads_.empty()); i++) {
processDeferredEquipmentQueue(); processDeferredEquipmentQueue();
@ -7072,11 +7072,21 @@ void Application::processAsyncCreatureResults(bool unlimited) {
} }
} }
void Application::processAsyncNpcCompositeResults() { void Application::processAsyncNpcCompositeResults(bool unlimited) {
auto* charRenderer = renderer ? renderer->getCharacterRenderer() : nullptr; auto* charRenderer = renderer ? renderer->getCharacterRenderer() : nullptr;
if (!charRenderer) return; if (!charRenderer) return;
// Budget: 2ms per frame to avoid stalling when many NPCs complete skin compositing
// simultaneously. In unlimited mode (load screen), process everything without cap.
static constexpr float kCompositeBudgetMs = 2.0f;
auto startTime = std::chrono::steady_clock::now();
for (auto it = asyncNpcCompositeLoads_.begin(); it != asyncNpcCompositeLoads_.end(); ) { for (auto it = asyncNpcCompositeLoads_.begin(); it != asyncNpcCompositeLoads_.end(); ) {
if (!unlimited) {
float elapsed = std::chrono::duration<float, std::milli>(
std::chrono::steady_clock::now() - startTime).count();
if (elapsed >= kCompositeBudgetMs) break;
}
if (!it->future.valid() || if (!it->future.valid() ||
it->future.wait_for(std::chrono::milliseconds(0)) != std::future_status::ready) { it->future.wait_for(std::chrono::milliseconds(0)) != std::future_status::ready) {
++it; ++it;