From dc2aab5e906adc1cd7cfea4ae2d2dd605f65d10a Mon Sep 17 00:00:00 2001 From: Kelsi Date: Tue, 10 Mar 2026 06:47:33 -0700 Subject: [PATCH] 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). --- include/core/application.hpp | 2 +- src/core/application.cpp | 14 ++++++++++++-- 2 files changed, 13 insertions(+), 3 deletions(-) diff --git a/include/core/application.hpp b/include/core/application.hpp index 7587fb7b..d2ef3f36 100644 --- a/include/core/application.hpp +++ b/include/core/application.hpp @@ -361,7 +361,7 @@ private: std::future future; }; std::vector asyncNpcCompositeLoads_; - void processAsyncNpcCompositeResults(); + void processAsyncNpcCompositeResults(bool unlimited = false); // Cache base player model geometry by (raceId, genderId) std::unordered_map playerModelCache_; // key=(race<<8)|gender → modelId struct PlayerTextureSlots { int skin = -1; int hair = -1; int underwear = -1; }; diff --git a/src/core/application.cpp b/src/core/application.cpp index 9f32c66b..9ffc1302 100644 --- a/src/core/application.cpp +++ b/src/core/application.cpp @@ -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 // and spawns happen in bulk while the loading screen is still visible. processCreatureSpawnQueue(true); - processAsyncNpcCompositeResults(); + processAsyncNpcCompositeResults(true); // Process equipment queue more aggressively during warmup (multiple per iteration) for (int i = 0; i < 8 && (!deferredEquipmentQueue_.empty() || !asyncEquipmentLoads_.empty()); i++) { processDeferredEquipmentQueue(); @@ -7072,11 +7072,21 @@ void Application::processAsyncCreatureResults(bool unlimited) { } } -void Application::processAsyncNpcCompositeResults() { +void Application::processAsyncNpcCompositeResults(bool unlimited) { auto* charRenderer = renderer ? renderer->getCharacterRenderer() : nullptr; 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(); ) { + if (!unlimited) { + float elapsed = std::chrono::duration( + std::chrono::steady_clock::now() - startTime).count(); + if (elapsed >= kCompositeBudgetMs) break; + } if (!it->future.valid() || it->future.wait_for(std::chrono::milliseconds(0)) != std::future_status::ready) { ++it;