Stabilize net parsing and reduce texture-cache churn

This commit is contained in:
Kelsi 2026-02-22 07:44:32 -08:00
parent ae88b226b5
commit 6d55c19987
7 changed files with 143 additions and 27 deletions

View file

@ -91,6 +91,12 @@ private:
// Receive buffer
std::vector<uint8_t> receiveBuffer;
// Optional reused packet queue (feature-gated) to reduce per-update allocations.
std::vector<Packet> 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

View file

@ -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) {

View file

@ -1965,11 +1965,22 @@ void GameHandler::handlePacket(network::Packet& packet) {
worldStateZoneId_ = packet.readUInt32();
uint16_t count = packet.readUInt16();
size_t needed = static_cast<size_t>(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<uint16_t>(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);

View file

@ -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<size_t>(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;
}

View file

@ -7,6 +7,8 @@
#include <sstream>
#include <cstdio>
#include <fstream>
#include <cstdlib>
#include <cstring>
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<size_t>(received);
receiveBuffer.insert(receiveBuffer.end(), buffer, buffer + received);
size_t receivedSize = static_cast<size_t>(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<Packet> parsedPackets;
parsedPackets.reserve(32);
std::vector<Packet> parsedPacketsLocal;
std::vector<Packet>* 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<Packet>().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<uint8_t> 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<uint8_t> 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);
}

View file

@ -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<size_t>(blp.width) * static_cast<size_t>(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),

View file

@ -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<size_t>(blp.width) * static_cast<size_t>(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),