From 6d55c19987899d69179b25af4c8f8bb95236bf1c Mon Sep 17 00:00:00 2001 From: Kelsi Date: Sun, 22 Feb 2026 07:44:32 -0800 Subject: [PATCH] Stabilize net parsing and reduce texture-cache churn --- include/network/world_socket.hpp | 6 +++ src/core/application.cpp | 5 ++ src/game/game_handler.cpp | 21 ++++++-- src/game/world_packets.cpp | 35 ++++++++++---- src/network/world_socket.cpp | 83 +++++++++++++++++++++++++++----- src/rendering/m2_renderer.cpp | 8 ++- src/rendering/wmo_renderer.cpp | 12 ++++- 7 files changed, 143 insertions(+), 27 deletions(-) diff --git a/include/network/world_socket.hpp b/include/network/world_socket.hpp index eee99bda..f4d89930 100644 --- a/include/network/world_socket.hpp +++ b/include/network/world_socket.hpp @@ -91,6 +91,12 @@ private: // Receive buffer std::vector receiveBuffer; + // Optional reused packet queue (feature-gated) to reduce per-update allocations. + std::vector parsedPacketsScratch_; + + // Runtime-gated network optimization toggles (default off). + bool useFastRecvAppend_ = false; + bool useParseScratchQueue_ = false; // Track how many header bytes have been decrypted (0-4) // This prevents re-decrypting the same header when waiting for more data diff --git a/src/core/application.cpp b/src/core/application.cpp index f841544e..1d27b0f5 100644 --- a/src/core/application.cpp +++ b/src/core/application.cpp @@ -565,18 +565,21 @@ void Application::update(float deltaTime) { updateCheckpoint = "state switch"; switch (state) { case AppState::AUTHENTICATION: + updateCheckpoint = "auth: enter"; if (authHandler) { authHandler->update(deltaTime); } break; case AppState::REALM_SELECTION: + updateCheckpoint = "realm_selection: enter"; if (authHandler) { authHandler->update(deltaTime); } break; case AppState::CHARACTER_CREATION: + updateCheckpoint = "char_creation: enter"; if (gameHandler) { gameHandler->update(deltaTime); } @@ -586,12 +589,14 @@ void Application::update(float deltaTime) { break; case AppState::CHARACTER_SELECTION: + updateCheckpoint = "char_selection: enter"; if (gameHandler) { gameHandler->update(deltaTime); } break; case AppState::IN_GAME: { + updateCheckpoint = "in_game: enter"; const char* inGameStep = "begin"; try { auto runInGameStage = [&](const char* stageName, auto&& fn) { diff --git a/src/game/game_handler.cpp b/src/game/game_handler.cpp index 5f0fef5e..c7e893e1 100644 --- a/src/game/game_handler.cpp +++ b/src/game/game_handler.cpp @@ -1965,11 +1965,22 @@ void GameHandler::handlePacket(network::Packet& packet) { worldStateZoneId_ = packet.readUInt32(); uint16_t count = packet.readUInt16(); size_t needed = static_cast(count) * 8; - if (packet.getSize() - packet.getReadPos() < needed) { - LOG_WARNING("SMSG_INIT_WORLD_STATES truncated: expected ", needed, - " bytes of state pairs, got ", packet.getSize() - packet.getReadPos()); - packet.setReadPos(packet.getSize()); - break; + size_t available = packet.getSize() - packet.getReadPos(); + if (available < needed) { + // Be tolerant across expansion/private-core variants: if packet shape + // still looks like N*(key,val) dwords, parse what is present. + if ((available % 8) == 0) { + uint16_t adjustedCount = static_cast(available / 8); + LOG_WARNING("SMSG_INIT_WORLD_STATES count mismatch: header=", count, + " adjusted=", adjustedCount, " (available=", available, ")"); + count = adjustedCount; + needed = available; + } else { + LOG_WARNING("SMSG_INIT_WORLD_STATES truncated: expected ", needed, + " bytes of state pairs, got ", available); + packet.setReadPos(packet.getSize()); + break; + } } worldStates_.clear(); worldStates_.reserve(count); diff --git a/src/game/world_packets.cpp b/src/game/world_packets.cpp index da46dec5..767f8db7 100644 --- a/src/game/world_packets.cpp +++ b/src/game/world_packets.cpp @@ -532,29 +532,46 @@ bool LoginVerifyWorldParser::parse(network::Packet& packet, LoginVerifyWorldData } bool AccountDataTimesParser::parse(network::Packet& packet, AccountDataTimesData& data) { - // SMSG_ACCOUNT_DATA_TIMES format (WoW 3.3.5a): - // uint32 serverTime (Unix timestamp) - // uint8 unknown (always 1?) - // uint32[8] accountDataTimes (timestamps for each data slot) - - if (packet.getSize() < 37) { - LOG_ERROR("SMSG_ACCOUNT_DATA_TIMES packet too small: ", packet.getSize(), " bytes"); + // Common layouts seen in the wild: + // - WotLK-like: uint32 serverTime, uint8 unk, uint32 mask, uint32[up to 8] slotTimes + // - Older/variant: uint32 serverTime, uint8 unk, uint32[up to 8] slotTimes + // Some servers only send a subset of slots. + if (packet.getSize() < 5) { + LOG_ERROR("SMSG_ACCOUNT_DATA_TIMES packet too small: ", packet.getSize(), + " bytes (need at least 5)"); return false; } + for (uint32_t& t : data.accountDataTimes) { + t = 0; + } data.serverTime = packet.readUInt32(); data.unknown = packet.readUInt8(); + size_t remaining = packet.getSize() - packet.getReadPos(); + uint32_t mask = 0xFF; + if (remaining >= 4 && ((remaining - 4) % 4) == 0) { + // Treat first dword as slot mask when payload shape matches. + mask = packet.readUInt32(); + } + remaining = packet.getSize() - packet.getReadPos(); + size_t slotWords = std::min(8, remaining / 4); + LOG_DEBUG("Parsed SMSG_ACCOUNT_DATA_TIMES:"); LOG_DEBUG(" Server time: ", data.serverTime); LOG_DEBUG(" Unknown: ", (int)data.unknown); + LOG_DEBUG(" Mask: 0x", std::hex, mask, std::dec, " slotsInPacket=", slotWords); - for (int i = 0; i < 8; ++i) { + for (size_t i = 0; i < slotWords; ++i) { data.accountDataTimes[i] = packet.readUInt32(); - if (data.accountDataTimes[i] != 0) { + if (data.accountDataTimes[i] != 0 || ((mask & (1u << i)) != 0)) { LOG_DEBUG(" Data slot ", i, ": ", data.accountDataTimes[i]); } } + if (packet.getReadPos() != packet.getSize()) { + LOG_DEBUG(" AccountDataTimes trailing bytes: ", packet.getSize() - packet.getReadPos()); + packet.setReadPos(packet.getSize()); + } return true; } diff --git a/src/network/world_socket.cpp b/src/network/world_socket.cpp index 349eea7f..10d7f950 100644 --- a/src/network/world_socket.cpp +++ b/src/network/world_socket.cpp @@ -7,6 +7,8 @@ #include #include #include +#include +#include namespace { constexpr size_t kMaxReceiveBufferBytes = 8 * 1024 * 1024; @@ -40,6 +42,13 @@ inline bool isLoginPipelineCmsg(uint16_t opcode) { return false; } } + +inline bool envFlagEnabled(const char* key, bool defaultValue = false) { + const char* raw = std::getenv(key); + if (!raw || !*raw) return defaultValue; + return !(raw[0] == '0' || raw[0] == 'f' || raw[0] == 'F' || + raw[0] == 'n' || raw[0] == 'N'); +} } // namespace namespace wowee { @@ -58,6 +67,19 @@ static const uint8_t DECRYPT_KEY[] = { WorldSocket::WorldSocket() { net::ensureInit(); + // Always reserve baseline receive capacity (safe, behavior-preserving). + receiveBuffer.reserve(64 * 1024); + useFastRecvAppend_ = envFlagEnabled("WOWEE_NET_FAST_RECV_APPEND", false); + useParseScratchQueue_ = envFlagEnabled("WOWEE_NET_PARSE_SCRATCH", false); + if (useParseScratchQueue_) { + LOG_WARNING("WOWEE_NET_PARSE_SCRATCH is temporarily disabled (known unstable); forcing off"); + useParseScratchQueue_ = false; + } + if (useParseScratchQueue_) { + parsedPacketsScratch_.reserve(64); + } + LOG_INFO("WorldSocket net opts: fast_recv_append=", useFastRecvAppend_ ? "on" : "off", + " parse_scratch=", useParseScratchQueue_ ? "on" : "off"); } WorldSocket::~WorldSocket() { @@ -118,6 +140,7 @@ void WorldSocket::disconnect() { encryptionEnabled = false; useVanillaCrypt = false; receiveBuffer.clear(); + parsedPacketsScratch_.clear(); headerBytesDecrypted = 0; LOG_INFO("Disconnected from world server"); } @@ -270,8 +293,22 @@ void WorldSocket::update() { if (received > 0) { receivedAny = true; ++readOps; - bytesReadThisTick += static_cast(received); - receiveBuffer.insert(receiveBuffer.end(), buffer, buffer + received); + size_t receivedSize = static_cast(received); + bytesReadThisTick += receivedSize; + if (useFastRecvAppend_) { + size_t oldSize = receiveBuffer.size(); + if (oldSize > kMaxReceiveBufferBytes || receivedSize > (kMaxReceiveBufferBytes - oldSize)) { + LOG_ERROR("World socket receive buffer would overflow (old=", oldSize, + " incoming=", receivedSize, " max=", kMaxReceiveBufferBytes, + "). Disconnecting to recover framing."); + disconnect(); + return; + } + receiveBuffer.resize(oldSize + receivedSize); + std::memcpy(receiveBuffer.data() + oldSize, buffer, receivedSize); + } else { + receiveBuffer.insert(receiveBuffer.end(), buffer, buffer + received); + } if (receiveBuffer.size() > kMaxReceiveBufferBytes) { LOG_ERROR("World socket receive buffer overflow (", receiveBuffer.size(), " bytes). Disconnecting to recover framing."); @@ -327,8 +364,21 @@ void WorldSocket::tryParsePackets() { int parsedThisTick = 0; size_t parseOffset = 0; size_t localHeaderBytesDecrypted = headerBytesDecrypted; - std::vector parsedPackets; - parsedPackets.reserve(32); + std::vector parsedPacketsLocal; + std::vector* parsedPackets = &parsedPacketsLocal; + if (useParseScratchQueue_) { + parsedPacketsScratch_.clear(); + // Keep a warm queue to reduce steady-state allocations, but avoid + // retaining pathological capacity after burst/misaligned streams. + if (parsedPacketsScratch_.capacity() > 1024) { + std::vector().swap(parsedPacketsScratch_); + } else if (parsedPacketsScratch_.capacity() < 64) { + parsedPacketsScratch_.reserve(64); + } + parsedPackets = &parsedPacketsScratch_; + } else { + parsedPacketsLocal.reserve(32); + } while ((receiveBuffer.size() - parseOffset) >= 4 && parsedThisTick < kMaxParsedPacketsPerUpdate) { uint8_t rawHeader[4] = {0, 0, 0, 0}; std::memcpy(rawHeader, receiveBuffer.data() + parseOffset, 4); @@ -408,12 +458,23 @@ void WorldSocket::tryParsePackets() { break; } - // Extract payload (skip header) - std::vector packetData(receiveBuffer.begin() + parseOffset + 4, - receiveBuffer.begin() + parseOffset + totalSize); - - // Queue packet; callbacks run after buffer state is finalized. - parsedPackets.emplace_back(opcode, std::move(packetData)); + // Extract payload (skip header). Guard allocation failures so malformed + // streams cannot unwind into application-level OOM crashes. + try { + std::vector packetData(payloadLen); + if (payloadLen > 0) { + std::memcpy(packetData.data(), receiveBuffer.data() + parseOffset + 4, payloadLen); + } + // Queue packet; callbacks run after buffer state is finalized. + parsedPackets->emplace_back(opcode, std::move(packetData)); + } catch (const std::bad_alloc& e) { + LOG_ERROR("OOM while queuing world packet opcode=0x", std::hex, opcode, std::dec, + " payload=", payloadLen, " buffered=", receiveBuffer.size(), + " parseOffset=", parseOffset, " what=", e.what(), + ". Disconnecting to recover."); + disconnect(); + return; + } parseOffset += totalSize; localHeaderBytesDecrypted = 0; ++parsedThisTick; @@ -425,7 +486,7 @@ void WorldSocket::tryParsePackets() { headerBytesDecrypted = localHeaderBytesDecrypted; if (packetCallback) { - for (const auto& packet : parsedPackets) { + for (const auto& packet : *parsedPackets) { if (!connected) break; packetCallback(packet); } diff --git a/src/rendering/m2_renderer.cpp b/src/rendering/m2_renderer.cpp index 2fc61bb7..5f9aafda 100644 --- a/src/rendering/m2_renderer.cpp +++ b/src/rendering/m2_renderer.cpp @@ -611,7 +611,7 @@ bool M2Renderer::initialize(VkContext* ctx, VkDescriptorSetLayout perFrameLayout glowTexture_->createSampler(device, VK_FILTER_LINEAR, VK_FILTER_LINEAR, VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_EDGE); } textureCacheBudgetBytes_ = - envSizeMBOrDefault("WOWEE_M2_TEX_CACHE_MB", 512) * 1024ull * 1024ull; + envSizeMBOrDefault("WOWEE_M2_TEX_CACHE_MB", 1024) * 1024ull * 1024ull; modelCacheLimit_ = envSizeMBOrDefault("WOWEE_M2_MODEL_LIMIT", 6000); LOG_INFO("M2 texture cache budget: ", textureCacheBudgetBytes_ / (1024 * 1024), " MB"); LOG_INFO("M2 model cache limit: ", modelCacheLimit_); @@ -3221,6 +3221,12 @@ VkTexture* M2Renderer::loadTexture(const std::string& path, uint32_t texFlags) { size_t base = static_cast(blp.width) * static_cast(blp.height) * 4ull; size_t approxBytes = base + (base / 3); if (textureCacheBytes_ + approxBytes > textureCacheBudgetBytes_) { + static constexpr size_t kMaxFailedTextureCache = 200000; + if (failedTextureCache_.size() < kMaxFailedTextureCache) { + // Cache budget-rejected keys too; without this we repeatedly decode/load + // the same textures every frame once budget is saturated. + failedTextureCache_.insert(key); + } if (textureBudgetRejectWarnings_ < 8 || (textureBudgetRejectWarnings_ % 120) == 0) { LOG_WARNING("M2 texture cache full (", textureCacheBytes_ / (1024 * 1024), " MB / ", textureCacheBudgetBytes_ / (1024 * 1024), diff --git a/src/rendering/wmo_renderer.cpp b/src/rendering/wmo_renderer.cpp index a8e50922..a2f97a24 100644 --- a/src/rendering/wmo_renderer.cpp +++ b/src/rendering/wmo_renderer.cpp @@ -227,7 +227,7 @@ bool WMORenderer::initialize(VkContext* ctx, VkDescriptorSetLayout perFrameLayou whiteTexture_->createSampler(device, VK_FILTER_LINEAR, VK_FILTER_LINEAR, VK_SAMPLER_ADDRESS_MODE_REPEAT); textureCacheBudgetBytes_ = - envSizeMBOrDefault("WOWEE_WMO_TEX_CACHE_MB", 512) * 1024ull * 1024ull; + envSizeMBOrDefault("WOWEE_WMO_TEX_CACHE_MB", 1024) * 1024ull * 1024ull; modelCacheLimit_ = envSizeMBOrDefault("WOWEE_WMO_MODEL_LIMIT", 4000); core::Logger::getInstance().info("WMO texture cache budget: ", textureCacheBudgetBytes_ / (1024 * 1024), " MB"); @@ -1943,6 +1943,16 @@ VkTexture* WMORenderer::loadTexture(const std::string& path) { size_t base = static_cast(blp.width) * static_cast(blp.height) * 4ull; size_t approxBytes = base + (base / 3); if (textureCacheBytes_ + approxBytes > textureCacheBudgetBytes_) { + static constexpr size_t kMaxFailedTextureCache = 200000; + if (failedTextureCache_.size() < kMaxFailedTextureCache) { + // Cache budget-rejected keys too; once saturated, repeated attempts + // cause pointless decode churn and transient allocations. + if (!resolvedKey.empty()) { + failedTextureCache_.insert(resolvedKey); + } else { + failedTextureCache_.insert(key); + } + } if (textureBudgetRejectWarnings_ < 8 || (textureBudgetRejectWarnings_ % 120) == 0) { core::Logger::getInstance().warning( "WMO texture cache full (", textureCacheBytes_ / (1024 * 1024),