Throttle player equipment compositing and spawning to fix walking stutter

Defer equipment texture compositing to a queue processed max 1 per frame
instead of compositing immediately on callback (each compositeWithRegions
call does file I/O + CPU blit + GPU upload on the main thread). Reduce
MAX_SPAWNS_PER_FRAME from 96 to 8 and increase inspect rate limit from
0.75s to 2s. Demote noisy per-frame logs to DEBUG level.
This commit is contained in:
Kelsi 2026-02-16 00:51:59 -08:00
parent d87a86e35c
commit 35034ca544
3 changed files with 23 additions and 7 deletions

View file

@ -224,7 +224,7 @@ private:
float x, y, z, orientation;
};
std::vector<PendingCreatureSpawn> pendingCreatureSpawns_;
static constexpr int MAX_SPAWNS_PER_FRAME = 96;
static constexpr int MAX_SPAWNS_PER_FRAME = 8;
static constexpr uint16_t MAX_CREATURE_SPAWN_RETRIES = 300;
std::unordered_set<uint64_t> pendingCreatureSpawnGuids_;
std::unordered_map<uint64_t, uint16_t> creatureSpawnRetryCounts_;
@ -244,6 +244,9 @@ private:
};
std::unordered_map<uint64_t, OnlinePlayerAppearanceState> onlinePlayerAppearance_;
std::unordered_map<uint64_t, std::pair<std::array<uint32_t, 19>, std::array<uint8_t, 19>>> pendingOnlinePlayerEquipment_;
// Deferred equipment compositing queue — processes max 1 per frame to avoid stutter
std::vector<std::pair<uint64_t, std::pair<std::array<uint32_t, 19>, std::array<uint8_t, 19>>>> deferredEquipmentQueue_;
void processDeferredEquipmentQueue();
// Cache base player model geometry by (raceId, genderId)
std::unordered_map<uint32_t, uint32_t> playerModelCache_; // key=(race<<8)|gender → modelId
struct PlayerTextureSlots { int skin = -1; int hair = -1; int underwear = -1; };

View file

@ -584,6 +584,8 @@ void Application::update(float deltaTime) {
processPlayerSpawnQueue();
// Process deferred online creature spawns (throttled)
processCreatureSpawnQueue();
// Process deferred equipment compositing (max 1 per frame to avoid stutter)
processDeferredEquipmentQueue();
auto cq2 = std::chrono::high_resolution_clock::now();
creatureQTime += std::chrono::duration<float, std::milli>(cq2 - cq1).count();
@ -1209,7 +1211,10 @@ void Application::setupUICallbacks() {
gameHandler->setPlayerEquipmentCallback([this](uint64_t guid,
const std::array<uint32_t, 19>& displayInfoIds,
const std::array<uint8_t, 19>& inventoryTypes) {
setOnlinePlayerEquipment(guid, displayInfoIds, inventoryTypes);
// Queue equipment compositing instead of doing it immediately —
// compositeWithRegions is expensive (file I/O + CPU blit + GPU upload)
// and causes frame stutters if multiple players update at once.
deferredEquipmentQueue_.push_back({guid, {displayInfoIds, inventoryTypes}});
});
// Creature despawn callback (online mode) - remove creature models
@ -4496,13 +4501,21 @@ void Application::processPlayerSpawnQueue() {
// Apply any equipment updates that arrived before the player was spawned.
auto pit = pendingOnlinePlayerEquipment_.find(s.guid);
if (pit != pendingOnlinePlayerEquipment_.end()) {
setOnlinePlayerEquipment(s.guid, pit->second.first, pit->second.second);
deferredEquipmentQueue_.push_back({s.guid, pit->second});
pendingOnlinePlayerEquipment_.erase(pit);
}
processed++;
}
}
void Application::processDeferredEquipmentQueue() {
if (deferredEquipmentQueue_.empty()) return;
// Process at most 1 per frame — compositeWithRegions is expensive
auto [guid, equipData] = deferredEquipmentQueue_.front();
deferredEquipmentQueue_.erase(deferredEquipmentQueue_.begin());
setOnlinePlayerEquipment(guid, equipData.first, equipData.second);
}
void Application::processGameObjectSpawnQueue() {
if (pendingGameObjectSpawns_.empty()) return;

View file

@ -314,8 +314,8 @@ void GameHandler::update(float deltaTime) {
if (guid != 0 && guid != playerGuid && entityManager.hasEntity(guid)) {
auto pkt = InspectPacket::build(guid);
socket->send(pkt);
inspectRateLimit_ = 0.75f; // keep it gentle
LOG_INFO("Sent CMSG_INSPECT for player 0x", std::hex, guid, std::dec);
inspectRateLimit_ = 2.0f; // throttle to avoid compositing stutter
LOG_DEBUG("Sent CMSG_INSPECT for player 0x", std::hex, guid, std::dec);
}
}
@ -4040,7 +4040,7 @@ void GameHandler::handleDestroyObject(network::Packet& packet) {
LOG_INFO("Destroyed entity: 0x", std::hex, data.guid, std::dec,
" (", (data.isDeath ? "death" : "despawn"), ")");
} else {
LOG_WARNING("Destroy object for unknown entity: 0x", std::hex, data.guid, std::dec);
LOG_DEBUG("Destroy object for unknown entity: 0x", std::hex, data.guid, std::dec);
}
// Clean up auto-attack and target if destroyed entity was our target
@ -6033,7 +6033,7 @@ void GameHandler::updateOtherPlayerVisibleItems(uint64_t guid, const std::map<ui
// Layout not detected yet — queue this player for inspect as fallback.
if (socket && state == WorldState::IN_WORLD) {
pendingAutoInspect_.insert(guid);
LOG_INFO("Queued player 0x", std::hex, guid, std::dec, " for auto-inspect (layout not detected)");
LOG_DEBUG("Queued player 0x", std::hex, guid, std::dec, " for auto-inspect (layout not detected)");
}
return;
}