From acb63d4f6e7976369efbefdadba11257f09ea9d9 Mon Sep 17 00:00:00 2001 From: Kelsi Date: Fri, 20 Feb 2026 17:29:09 -0800 Subject: [PATCH] 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. --- include/core/application.hpp | 1 + src/core/application.cpp | 73 +++++++++++++++++++++++++++++++----- src/network/world_socket.cpp | 10 ++++- 3 files changed, 74 insertions(+), 10 deletions(-) diff --git a/include/core/application.hpp b/include/core/application.hpp index e172a339..26e3d6cf 100644 --- a/include/core/application.hpp +++ b/include/core/application.hpp @@ -218,6 +218,7 @@ private: std::unordered_map pendingTransportMoves_; // guid -> latest pre-registration move uint32_t nextGameObjectModelId_ = 20000; uint32_t nextGameObjectWmoModelId_ = 40000; + bool testTransportSetup_ = false; bool gameObjectLookupsBuilt_ = false; // Mount model tracking diff --git a/src/core/application.cpp b/src/core/application.cpp index 656edb9d..788492be 100644 --- a/src/core/application.cpp +++ b/src/core/application.cpp @@ -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(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(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); diff --git a/src/network/world_socket.cpp b/src/network/world_socket.cpp index a9ae6b99..fcee6a92 100644 --- a/src/network/world_socket.cpp +++ b/src/network/world_socket.cpp @@ -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"); } }