Hide post-login world hitch behind loading screen

- keep character-selection state until world entry load/finalize completes
- move expensive post-load setup (test transport + creature callback prep) before loading screen shutdown
- add bounded world warmup pass under loading screen to drain initial network/spawn backlog
- start intro camera pan after warmup so rotation begins when gameplay becomes visible
- guard test transport setup so it runs once per session
- add per-update world socket parse budget to prevent single-frame packet-drain stalls

This reduces visible 3-4s stutter after login by shifting startup work behind the loading screen and time-slicing packet processing.
This commit is contained in:
Kelsi 2026-02-20 17:29:09 -08:00
parent 954edc91b8
commit acb63d4f6e
3 changed files with 74 additions and 10 deletions

View file

@ -218,6 +218,7 @@ private:
std::unordered_map<uint64_t, PendingTransportMove> pendingTransportMoves_; // guid -> latest pre-registration move
uint32_t nextGameObjectModelId_ = 20000;
uint32_t nextGameObjectWmoModelId_ = 40000;
bool testTransportSetup_ = false;
bool gameObjectLookupsBuilt_ = false;
// Mount model tracking

View file

@ -1181,8 +1181,8 @@ void Application::setupUICallbacks() {
if (gameHandler) {
gameHandler->setActiveCharacterGuid(characterGuid);
}
// Online mode - login will be handled by world entry callback
setState(AppState::IN_GAME);
// Keep CHARACTER_SELECTION active until world entry is fully loaded.
// This avoids exposing pre-load hitching before the loading screen/intro.
});
// Character create screen callbacks
@ -2827,7 +2827,6 @@ void Application::loadOnlineWorldTerrain(uint32_t mapId, float x, float y, float
renderer->getCameraController()->setOnlineMode(true);
renderer->getCameraController()->setDefaultSpawn(spawnRender, 0.0f, -15.0f);
renderer->getCameraController()->reset();
renderer->getCameraController()->startIntroPan(2.8f, 140.0f);
}
// Set map name for WMO renderer
@ -3021,16 +3020,12 @@ void Application::loadOnlineWorldTerrain(uint32_t mapId, float x, float y, float
renderer->getCameraController()->reset();
}
showProgress("Entering world...", 1.0f);
if (loadingScreenOk) {
loadingScreen.shutdown();
}
// Set up test transport (development feature)
showProgress("Finalizing world...", 0.94f);
setupTestTransport();
// Set up NPC animation callbacks (for online creatures)
showProgress("Preparing creatures...", 0.97f);
if (gameHandler && renderer && renderer->getCharacterRenderer()) {
auto* cr = renderer->getCharacterRenderer();
auto* app = this;
@ -3059,6 +3054,64 @@ void Application::loadOnlineWorldTerrain(uint32_t mapId, float x, float y, float
});
}
// Hide first-login hitch by draining initial world packets/spawn queues before
// dropping the loading screen. Keep this bounded so we don't stall indefinitely.
{
const float kWarmupMaxSeconds = 2.5f;
const auto warmupStart = std::chrono::high_resolution_clock::now();
while (true) {
SDL_Event event;
while (SDL_PollEvent(&event)) {
if (event.type == SDL_QUIT) {
window->setShouldClose(true);
if (loadingScreenOk) loadingScreen.shutdown();
return;
}
if (event.type == SDL_WINDOWEVENT &&
event.window.event == SDL_WINDOWEVENT_RESIZED) {
int w = event.window.data1;
int h = event.window.data2;
window->setSize(w, h);
glViewport(0, 0, w, h);
if (renderer && renderer->getCamera()) {
renderer->getCamera()->setAspectRatio(static_cast<float>(w) / h);
}
}
}
// Drain network and process deferred spawn/composite queues while hidden.
if (gameHandler) gameHandler->update(1.0f / 60.0f);
if (world) world->update(1.0f / 60.0f);
processPlayerSpawnQueue();
processCreatureSpawnQueue();
processDeferredEquipmentQueue();
processGameObjectSpawnQueue();
processPendingMount();
updateQuestMarkers();
const auto now = std::chrono::high_resolution_clock::now();
const float elapsed = std::chrono::duration<float>(now - warmupStart).count();
const float t = std::clamp(elapsed / kWarmupMaxSeconds, 0.0f, 1.0f);
showProgress("Finalizing world sync...", 0.97f + t * 0.025f);
if (elapsed >= kWarmupMaxSeconds) {
break;
}
SDL_Delay(16);
}
}
// Start intro pan right before entering gameplay so it's visible after loading.
if (renderer->getCameraController()) {
renderer->getCameraController()->startIntroPan(2.8f, 140.0f);
}
showProgress("Entering world...", 1.0f);
if (loadingScreenOk) {
loadingScreen.shutdown();
}
// Set game state
setState(AppState::IN_GAME);
}
@ -5458,6 +5511,7 @@ void Application::updateQuestMarkers() {
}
void Application::setupTestTransport() {
if (testTransportSetup_) return;
if (!gameHandler || !renderer || !assetManager) return;
auto* transportManager = gameHandler->getTransportManager();
@ -5584,6 +5638,7 @@ void Application::setupTestTransport() {
glm::vec3(-15.0f, -30.0f, 0.0f),
glm::vec3(15.0f, 30.0f, 10.0f));
testTransportSetup_ = true;
LOG_INFO("========================================");
LOG_INFO("Test transport registered:");
LOG_INFO(" GUID: 0x", std::hex, transportGuid, std::dec);

View file

@ -10,6 +10,7 @@
namespace {
constexpr size_t kMaxReceiveBufferBytes = 8 * 1024 * 1024;
constexpr int kMaxParsedPacketsPerUpdate = 220;
inline bool isLoginPipelineSmsg(uint16_t opcode) {
switch (opcode) {
@ -323,7 +324,8 @@ void WorldSocket::update() {
void WorldSocket::tryParsePackets() {
// World server packets have 4-byte incoming header: size(2) + opcode(2)
while (receiveBuffer.size() >= 4) {
int parsedThisTick = 0;
while (receiveBuffer.size() >= 4 && parsedThisTick < kMaxParsedPacketsPerUpdate) {
uint8_t rawHeader[4] = {0, 0, 0, 0};
std::memcpy(rawHeader, receiveBuffer.data(), 4);
@ -417,6 +419,12 @@ void WorldSocket::tryParsePackets() {
if (packetCallback) {
packetCallback(packet);
}
++parsedThisTick;
}
if (parsedThisTick >= kMaxParsedPacketsPerUpdate && receiveBuffer.size() >= 4) {
LOG_DEBUG("World socket parse budget reached (", parsedThisTick,
" packets); deferring remaining buffered data=", receiveBuffer.size(), " bytes");
}
}