mirror of
https://github.com/Kelsidavis/WoWee.git
synced 2026-03-22 23:30:14 +00:00
Stabilize net parsing and reduce texture-cache churn
This commit is contained in:
parent
ae88b226b5
commit
6d55c19987
7 changed files with 143 additions and 27 deletions
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue