mirror of
https://github.com/Kelsidavis/WoWee.git
synced 2026-04-17 17:43:52 +00:00
Stabilize streaming memory and parser handling; revert socket recv optimizations
This commit is contained in:
parent
c914295d20
commit
ae88b226b5
15 changed files with 591 additions and 161 deletions
|
|
@ -353,6 +353,7 @@ class TurtlePacketParsers : public ClassicPacketParsers {
|
||||||
public:
|
public:
|
||||||
uint8_t movementFlags2Size() const override { return 0; }
|
uint8_t movementFlags2Size() const override { return 0; }
|
||||||
bool parseMovementBlock(network::Packet& packet, UpdateBlock& block) override;
|
bool parseMovementBlock(network::Packet& packet, UpdateBlock& block) override;
|
||||||
|
bool parseMonsterMove(network::Packet& packet, MonsterMoveData& data) override;
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
||||||
|
|
@ -356,6 +356,8 @@ private:
|
||||||
|
|
||||||
uint32_t nextInstanceId = 1;
|
uint32_t nextInstanceId = 1;
|
||||||
uint32_t lastDrawCallCount = 0;
|
uint32_t lastDrawCallCount = 0;
|
||||||
|
size_t modelCacheLimit_ = 6000;
|
||||||
|
uint32_t modelLimitRejectWarnings_ = 0;
|
||||||
|
|
||||||
VkTexture* loadTexture(const std::string& path, uint32_t texFlags = 0);
|
VkTexture* loadTexture(const std::string& path, uint32_t texFlags = 0);
|
||||||
struct TextureCacheEntry {
|
struct TextureCacheEntry {
|
||||||
|
|
@ -371,6 +373,9 @@ private:
|
||||||
size_t textureCacheBytes_ = 0;
|
size_t textureCacheBytes_ = 0;
|
||||||
uint64_t textureCacheCounter_ = 0;
|
uint64_t textureCacheCounter_ = 0;
|
||||||
size_t textureCacheBudgetBytes_ = 2048ull * 1024 * 1024;
|
size_t textureCacheBudgetBytes_ = 2048ull * 1024 * 1024;
|
||||||
|
std::unordered_set<std::string> failedTextureCache_;
|
||||||
|
std::unordered_set<std::string> loggedTextureLoadFails_;
|
||||||
|
uint32_t textureBudgetRejectWarnings_ = 0;
|
||||||
std::unique_ptr<VkTexture> whiteTexture_;
|
std::unique_ptr<VkTexture> whiteTexture_;
|
||||||
std::unique_ptr<VkTexture> glowTexture_;
|
std::unique_ptr<VkTexture> glowTexture_;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,7 @@
|
||||||
#include <glm/glm.hpp>
|
#include <glm/glm.hpp>
|
||||||
#include <memory>
|
#include <memory>
|
||||||
#include <unordered_map>
|
#include <unordered_map>
|
||||||
|
#include <unordered_set>
|
||||||
#include <string>
|
#include <string>
|
||||||
|
|
||||||
namespace wowee {
|
namespace wowee {
|
||||||
|
|
@ -156,6 +157,9 @@ private:
|
||||||
size_t textureCacheBytes_ = 0;
|
size_t textureCacheBytes_ = 0;
|
||||||
uint64_t textureCacheCounter_ = 0;
|
uint64_t textureCacheCounter_ = 0;
|
||||||
size_t textureCacheBudgetBytes_ = 4096ull * 1024 * 1024;
|
size_t textureCacheBudgetBytes_ = 4096ull * 1024 * 1024;
|
||||||
|
std::unordered_set<std::string> failedTextureCache_;
|
||||||
|
std::unordered_set<std::string> loggedTextureLoadFails_;
|
||||||
|
uint32_t textureBudgetRejectWarnings_ = 0;
|
||||||
|
|
||||||
// Fallback textures
|
// Fallback textures
|
||||||
std::unique_ptr<VkTexture> whiteTexture;
|
std::unique_ptr<VkTexture> whiteTexture;
|
||||||
|
|
|
||||||
|
|
@ -587,12 +587,17 @@ private:
|
||||||
size_t textureCacheBytes_ = 0;
|
size_t textureCacheBytes_ = 0;
|
||||||
uint64_t textureCacheCounter_ = 0;
|
uint64_t textureCacheCounter_ = 0;
|
||||||
size_t textureCacheBudgetBytes_ = 2048ull * 1024 * 1024; // Default, overridden at init
|
size_t textureCacheBudgetBytes_ = 2048ull * 1024 * 1024; // Default, overridden at init
|
||||||
|
std::unordered_set<std::string> failedTextureCache_;
|
||||||
|
std::unordered_set<std::string> loggedTextureLoadFails_;
|
||||||
|
uint32_t textureBudgetRejectWarnings_ = 0;
|
||||||
|
|
||||||
// Default white texture
|
// Default white texture
|
||||||
std::unique_ptr<VkTexture> whiteTexture_;
|
std::unique_ptr<VkTexture> whiteTexture_;
|
||||||
|
|
||||||
// Loaded models (modelId -> ModelData)
|
// Loaded models (modelId -> ModelData)
|
||||||
std::unordered_map<uint32_t, ModelData> loadedModels;
|
std::unordered_map<uint32_t, ModelData> loadedModels;
|
||||||
|
size_t modelCacheLimit_ = 4000;
|
||||||
|
uint32_t modelLimitRejectWarnings_ = 0;
|
||||||
|
|
||||||
// Active instances
|
// Active instances
|
||||||
std::vector<WMOInstance> instances;
|
std::vector<WMOInstance> instances;
|
||||||
|
|
|
||||||
|
|
@ -320,15 +320,41 @@ void Application::run() {
|
||||||
auto t1 = std::chrono::steady_clock::now();
|
auto t1 = std::chrono::steady_clock::now();
|
||||||
|
|
||||||
// Update application state
|
// Update application state
|
||||||
update(deltaTime);
|
try {
|
||||||
|
update(deltaTime);
|
||||||
|
} catch (const std::bad_alloc& e) {
|
||||||
|
LOG_ERROR("OOM during Application::update (state=", static_cast<int>(state),
|
||||||
|
", dt=", deltaTime, "): ", e.what());
|
||||||
|
throw;
|
||||||
|
} catch (const std::exception& e) {
|
||||||
|
LOG_ERROR("Exception during Application::update (state=", static_cast<int>(state),
|
||||||
|
", dt=", deltaTime, "): ", e.what());
|
||||||
|
throw;
|
||||||
|
}
|
||||||
auto t2 = std::chrono::steady_clock::now();
|
auto t2 = std::chrono::steady_clock::now();
|
||||||
|
|
||||||
// Render
|
// Render
|
||||||
render();
|
try {
|
||||||
|
render();
|
||||||
|
} catch (const std::bad_alloc& e) {
|
||||||
|
LOG_ERROR("OOM during Application::render (state=", static_cast<int>(state), "): ", e.what());
|
||||||
|
throw;
|
||||||
|
} catch (const std::exception& e) {
|
||||||
|
LOG_ERROR("Exception during Application::render (state=", static_cast<int>(state), "): ", e.what());
|
||||||
|
throw;
|
||||||
|
}
|
||||||
auto t3 = std::chrono::steady_clock::now();
|
auto t3 = std::chrono::steady_clock::now();
|
||||||
|
|
||||||
// Swap buffers
|
// Swap buffers
|
||||||
window->swapBuffers();
|
try {
|
||||||
|
window->swapBuffers();
|
||||||
|
} catch (const std::bad_alloc& e) {
|
||||||
|
LOG_ERROR("OOM during swapBuffers: ", e.what());
|
||||||
|
throw;
|
||||||
|
} catch (const std::exception& e) {
|
||||||
|
LOG_ERROR("Exception during swapBuffers: ", e.what());
|
||||||
|
throw;
|
||||||
|
}
|
||||||
auto t4 = std::chrono::steady_clock::now();
|
auto t4 = std::chrono::steady_clock::now();
|
||||||
|
|
||||||
totalUpdateMs += std::chrono::duration<double, std::milli>(t2 - t1).count();
|
totalUpdateMs += std::chrono::duration<double, std::milli>(t2 - t1).count();
|
||||||
|
|
@ -533,7 +559,10 @@ void Application::logoutToLogin() {
|
||||||
}
|
}
|
||||||
|
|
||||||
void Application::update(float deltaTime) {
|
void Application::update(float deltaTime) {
|
||||||
|
const char* updateCheckpoint = "enter";
|
||||||
|
try {
|
||||||
// Update based on current state
|
// Update based on current state
|
||||||
|
updateCheckpoint = "state switch";
|
||||||
switch (state) {
|
switch (state) {
|
||||||
case AppState::AUTHENTICATION:
|
case AppState::AUTHENTICATION:
|
||||||
if (authHandler) {
|
if (authHandler) {
|
||||||
|
|
@ -563,20 +592,40 @@ void Application::update(float deltaTime) {
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case AppState::IN_GAME: {
|
case AppState::IN_GAME: {
|
||||||
|
const char* inGameStep = "begin";
|
||||||
|
try {
|
||||||
|
auto runInGameStage = [&](const char* stageName, auto&& fn) {
|
||||||
|
try {
|
||||||
|
fn();
|
||||||
|
} catch (const std::bad_alloc& e) {
|
||||||
|
LOG_ERROR("OOM during IN_GAME update stage '", stageName, "': ", e.what());
|
||||||
|
throw;
|
||||||
|
} catch (const std::exception& e) {
|
||||||
|
LOG_ERROR("Exception during IN_GAME update stage '", stageName, "': ", e.what());
|
||||||
|
throw;
|
||||||
|
}
|
||||||
|
};
|
||||||
// Application update profiling
|
// Application update profiling
|
||||||
|
updateCheckpoint = "in_game: profile init";
|
||||||
static int appProfileCounter = 0;
|
static int appProfileCounter = 0;
|
||||||
static float ghTime = 0.0f, worldTime = 0.0f, spawnTime = 0.0f;
|
static float ghTime = 0.0f, worldTime = 0.0f, spawnTime = 0.0f;
|
||||||
static float creatureQTime = 0.0f, goQTime = 0.0f, mountTime = 0.0f;
|
static float creatureQTime = 0.0f, goQTime = 0.0f, mountTime = 0.0f;
|
||||||
static float npcMgrTime = 0.0f, questMarkTime = 0.0f, syncTime = 0.0f;
|
static float npcMgrTime = 0.0f, questMarkTime = 0.0f, syncTime = 0.0f;
|
||||||
|
|
||||||
auto gh1 = std::chrono::high_resolution_clock::now();
|
auto gh1 = std::chrono::high_resolution_clock::now();
|
||||||
if (gameHandler) {
|
inGameStep = "gameHandler update";
|
||||||
gameHandler->update(deltaTime);
|
updateCheckpoint = "in_game: gameHandler update";
|
||||||
}
|
runInGameStage("gameHandler->update", [&] {
|
||||||
|
if (gameHandler) {
|
||||||
|
gameHandler->update(deltaTime);
|
||||||
|
}
|
||||||
|
});
|
||||||
auto gh2 = std::chrono::high_resolution_clock::now();
|
auto gh2 = std::chrono::high_resolution_clock::now();
|
||||||
ghTime += std::chrono::duration<float, std::milli>(gh2 - gh1).count();
|
ghTime += std::chrono::duration<float, std::milli>(gh2 - gh1).count();
|
||||||
|
|
||||||
// Always unsheath on combat engage.
|
// Always unsheath on combat engage.
|
||||||
|
inGameStep = "auto-unsheathe";
|
||||||
|
updateCheckpoint = "in_game: auto-unsheathe";
|
||||||
if (gameHandler) {
|
if (gameHandler) {
|
||||||
const bool autoAttacking = gameHandler->isAutoAttacking();
|
const bool autoAttacking = gameHandler->isAutoAttacking();
|
||||||
if (autoAttacking && !wasAutoAttacking_ && weaponsSheathed_) {
|
if (autoAttacking && !wasAutoAttacking_ && weaponsSheathed_) {
|
||||||
|
|
@ -587,6 +636,8 @@ void Application::update(float deltaTime) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Toggle weapon sheathe state with Z (ignored while UI captures keyboard).
|
// Toggle weapon sheathe state with Z (ignored while UI captures keyboard).
|
||||||
|
inGameStep = "weapon-toggle input";
|
||||||
|
updateCheckpoint = "in_game: weapon-toggle input";
|
||||||
{
|
{
|
||||||
const bool uiWantsKeyboard = ImGui::GetIO().WantCaptureKeyboard;
|
const bool uiWantsKeyboard = ImGui::GetIO().WantCaptureKeyboard;
|
||||||
auto& input = Input::getInstance();
|
auto& input = Input::getInstance();
|
||||||
|
|
@ -597,23 +648,33 @@ void Application::update(float deltaTime) {
|
||||||
}
|
}
|
||||||
|
|
||||||
auto w1 = std::chrono::high_resolution_clock::now();
|
auto w1 = std::chrono::high_resolution_clock::now();
|
||||||
if (world) {
|
inGameStep = "world update";
|
||||||
world->update(deltaTime);
|
updateCheckpoint = "in_game: world update";
|
||||||
}
|
runInGameStage("world->update", [&] {
|
||||||
|
if (world) {
|
||||||
|
world->update(deltaTime);
|
||||||
|
}
|
||||||
|
});
|
||||||
auto w2 = std::chrono::high_resolution_clock::now();
|
auto w2 = std::chrono::high_resolution_clock::now();
|
||||||
worldTime += std::chrono::duration<float, std::milli>(w2 - w1).count();
|
worldTime += std::chrono::duration<float, std::milli>(w2 - w1).count();
|
||||||
|
|
||||||
auto cq1 = std::chrono::high_resolution_clock::now();
|
auto cq1 = std::chrono::high_resolution_clock::now();
|
||||||
processPlayerSpawnQueue();
|
inGameStep = "spawn/equipment queues";
|
||||||
// Process deferred online creature spawns (throttled)
|
updateCheckpoint = "in_game: spawn/equipment queues";
|
||||||
processCreatureSpawnQueue();
|
runInGameStage("spawn/equipment queues", [&] {
|
||||||
// Process deferred equipment compositing (max 1 per frame to avoid stutter)
|
processPlayerSpawnQueue();
|
||||||
processDeferredEquipmentQueue();
|
// Process deferred online creature spawns (throttled)
|
||||||
|
processCreatureSpawnQueue();
|
||||||
|
// Process deferred equipment compositing (max 1 per frame to avoid stutter)
|
||||||
|
processDeferredEquipmentQueue();
|
||||||
|
});
|
||||||
auto cq2 = std::chrono::high_resolution_clock::now();
|
auto cq2 = std::chrono::high_resolution_clock::now();
|
||||||
creatureQTime += std::chrono::duration<float, std::milli>(cq2 - cq1).count();
|
creatureQTime += std::chrono::duration<float, std::milli>(cq2 - cq1).count();
|
||||||
|
|
||||||
// Self-heal missing creature visuals: if a nearby UNIT exists in
|
// Self-heal missing creature visuals: if a nearby UNIT exists in
|
||||||
// entity state but has no render instance, queue a spawn retry.
|
// entity state but has no render instance, queue a spawn retry.
|
||||||
|
inGameStep = "creature resync scan";
|
||||||
|
updateCheckpoint = "in_game: creature resync scan";
|
||||||
if (gameHandler) {
|
if (gameHandler) {
|
||||||
static float creatureResyncTimer = 0.0f;
|
static float creatureResyncTimer = 0.0f;
|
||||||
creatureResyncTimer += deltaTime;
|
creatureResyncTimer += deltaTime;
|
||||||
|
|
@ -659,13 +720,21 @@ void Application::update(float deltaTime) {
|
||||||
}
|
}
|
||||||
|
|
||||||
auto goq1 = std::chrono::high_resolution_clock::now();
|
auto goq1 = std::chrono::high_resolution_clock::now();
|
||||||
processGameObjectSpawnQueue();
|
inGameStep = "gameobject/transport queues";
|
||||||
processPendingTransportDoodads();
|
updateCheckpoint = "in_game: gameobject/transport queues";
|
||||||
|
runInGameStage("gameobject/transport queues", [&] {
|
||||||
|
processGameObjectSpawnQueue();
|
||||||
|
processPendingTransportDoodads();
|
||||||
|
});
|
||||||
auto goq2 = std::chrono::high_resolution_clock::now();
|
auto goq2 = std::chrono::high_resolution_clock::now();
|
||||||
goQTime += std::chrono::duration<float, std::milli>(goq2 - goq1).count();
|
goQTime += std::chrono::duration<float, std::milli>(goq2 - goq1).count();
|
||||||
|
|
||||||
auto m1 = std::chrono::high_resolution_clock::now();
|
auto m1 = std::chrono::high_resolution_clock::now();
|
||||||
processPendingMount();
|
inGameStep = "pending mount";
|
||||||
|
updateCheckpoint = "in_game: pending mount";
|
||||||
|
runInGameStage("processPendingMount", [&] {
|
||||||
|
processPendingMount();
|
||||||
|
});
|
||||||
auto m2 = std::chrono::high_resolution_clock::now();
|
auto m2 = std::chrono::high_resolution_clock::now();
|
||||||
mountTime += std::chrono::duration<float, std::milli>(m2 - m1).count();
|
mountTime += std::chrono::duration<float, std::milli>(m2 - m1).count();
|
||||||
|
|
||||||
|
|
@ -675,26 +744,33 @@ void Application::update(float deltaTime) {
|
||||||
|
|
||||||
auto qm1 = std::chrono::high_resolution_clock::now();
|
auto qm1 = std::chrono::high_resolution_clock::now();
|
||||||
// Update 3D quest markers above NPCs
|
// Update 3D quest markers above NPCs
|
||||||
updateQuestMarkers();
|
inGameStep = "quest markers";
|
||||||
|
updateCheckpoint = "in_game: quest markers";
|
||||||
|
runInGameStage("updateQuestMarkers", [&] {
|
||||||
|
updateQuestMarkers();
|
||||||
|
});
|
||||||
auto qm2 = std::chrono::high_resolution_clock::now();
|
auto qm2 = std::chrono::high_resolution_clock::now();
|
||||||
questMarkTime += std::chrono::duration<float, std::milli>(qm2 - qm1).count();
|
questMarkTime += std::chrono::duration<float, std::milli>(qm2 - qm1).count();
|
||||||
|
|
||||||
auto sync1 = std::chrono::high_resolution_clock::now();
|
auto sync1 = std::chrono::high_resolution_clock::now();
|
||||||
|
|
||||||
// Sync server run speed to camera controller
|
// Sync server run speed to camera controller
|
||||||
if (renderer && gameHandler && renderer->getCameraController()) {
|
inGameStep = "post-update sync";
|
||||||
renderer->getCameraController()->setRunSpeedOverride(gameHandler->getServerRunSpeed());
|
updateCheckpoint = "in_game: post-update sync";
|
||||||
}
|
runInGameStage("post-update sync", [&] {
|
||||||
|
if (renderer && gameHandler && renderer->getCameraController()) {
|
||||||
|
renderer->getCameraController()->setRunSpeedOverride(gameHandler->getServerRunSpeed());
|
||||||
|
}
|
||||||
|
|
||||||
bool onTaxi = gameHandler &&
|
bool onTaxi = gameHandler &&
|
||||||
(gameHandler->isOnTaxiFlight() ||
|
(gameHandler->isOnTaxiFlight() ||
|
||||||
gameHandler->isTaxiMountActive() ||
|
gameHandler->isTaxiMountActive() ||
|
||||||
gameHandler->isTaxiActivationPending());
|
gameHandler->isTaxiActivationPending());
|
||||||
bool onTransportNow = gameHandler && gameHandler->isOnTransport();
|
bool onTransportNow = gameHandler && gameHandler->isOnTransport();
|
||||||
if (worldEntryMovementGraceTimer_ > 0.0f) {
|
if (worldEntryMovementGraceTimer_ > 0.0f) {
|
||||||
worldEntryMovementGraceTimer_ -= deltaTime;
|
worldEntryMovementGraceTimer_ -= deltaTime;
|
||||||
}
|
}
|
||||||
if (renderer && renderer->getCameraController()) {
|
if (renderer && renderer->getCameraController()) {
|
||||||
const bool externallyDrivenMotion = onTaxi || onTransportNow || chargeActive_;
|
const bool externallyDrivenMotion = onTaxi || onTransportNow || chargeActive_;
|
||||||
// Keep physics frozen (externalFollow) during landing clamp when terrain
|
// Keep physics frozen (externalFollow) during landing clamp when terrain
|
||||||
// hasn't loaded yet — prevents gravity from pulling player through void.
|
// hasn't loaded yet — prevents gravity from pulling player through void.
|
||||||
|
|
@ -759,22 +835,22 @@ void Application::update(float deltaTime) {
|
||||||
} else if (!idleOrbit) {
|
} else if (!idleOrbit) {
|
||||||
idleYawned_ = false;
|
idleYawned_ = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (renderer) {
|
if (renderer) {
|
||||||
renderer->setTaxiFlight(onTaxi);
|
renderer->setTaxiFlight(onTaxi);
|
||||||
}
|
}
|
||||||
if (renderer && renderer->getTerrainManager()) {
|
if (renderer && renderer->getTerrainManager()) {
|
||||||
renderer->getTerrainManager()->setStreamingEnabled(true);
|
renderer->getTerrainManager()->setStreamingEnabled(true);
|
||||||
// Keep taxi streaming responsive so flight paths don't outrun terrain/model uploads.
|
// Keep taxi streaming responsive so flight paths don't outrun terrain/model uploads.
|
||||||
renderer->getTerrainManager()->setUpdateInterval(onTaxi ? 0.1f : 0.1f);
|
renderer->getTerrainManager()->setUpdateInterval(onTaxi ? 0.1f : 0.1f);
|
||||||
renderer->getTerrainManager()->setLoadRadius(onTaxi ? 3 : 4);
|
renderer->getTerrainManager()->setLoadRadius(onTaxi ? 3 : 4);
|
||||||
renderer->getTerrainManager()->setUnloadRadius(onTaxi ? 6 : 7);
|
renderer->getTerrainManager()->setUnloadRadius(onTaxi ? 6 : 7);
|
||||||
renderer->getTerrainManager()->setTaxiStreamingMode(onTaxi);
|
renderer->getTerrainManager()->setTaxiStreamingMode(onTaxi);
|
||||||
}
|
}
|
||||||
lastTaxiFlight_ = onTaxi;
|
lastTaxiFlight_ = onTaxi;
|
||||||
|
|
||||||
// Sync character render position ↔ canonical WoW coords each frame
|
// Sync character render position ↔ canonical WoW coords each frame
|
||||||
if (renderer && gameHandler) {
|
if (renderer && gameHandler) {
|
||||||
bool onTransport = gameHandler->isOnTransport();
|
bool onTransport = gameHandler->isOnTransport();
|
||||||
|
|
||||||
// Debug: Log transport state changes
|
// Debug: Log transport state changes
|
||||||
|
|
@ -922,11 +998,14 @@ void Application::update(float deltaTime) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
});
|
||||||
|
|
||||||
// Keep creature render instances aligned with authoritative entity positions.
|
// Keep creature render instances aligned with authoritative entity positions.
|
||||||
// This prevents desync where target circles move with server entities but
|
// This prevents desync where target circles move with server entities but
|
||||||
// creature models remain at stale spawn positions.
|
// creature models remain at stale spawn positions.
|
||||||
|
inGameStep = "creature render sync";
|
||||||
|
updateCheckpoint = "in_game: creature render sync";
|
||||||
if (renderer && gameHandler && renderer->getCharacterRenderer()) {
|
if (renderer && gameHandler && renderer->getCharacterRenderer()) {
|
||||||
auto* charRenderer = renderer->getCharacterRenderer();
|
auto* charRenderer = renderer->getCharacterRenderer();
|
||||||
static float npcWeaponRetryTimer = 0.0f;
|
static float npcWeaponRetryTimer = 0.0f;
|
||||||
|
|
@ -1061,16 +1140,26 @@ void Application::update(float deltaTime) {
|
||||||
|
|
||||||
// Log profiling every 60 frames
|
// Log profiling every 60 frames
|
||||||
if (++appProfileCounter >= 60) {
|
if (++appProfileCounter >= 60) {
|
||||||
LOG_DEBUG("APP UPDATE PROFILE (60 frames): gameHandler=", ghTime / 60.0f,
|
updateCheckpoint = "in_game: profile log";
|
||||||
"ms world=", worldTime / 60.0f, "ms spawn=", spawnTime / 60.0f,
|
if (core::Logger::getInstance().shouldLog(core::LogLevel::DEBUG)) {
|
||||||
"ms creatureQ=", creatureQTime / 60.0f, "ms goQ=", goQTime / 60.0f,
|
LOG_DEBUG("APP UPDATE PROFILE (60 frames): gameHandler=", ghTime / 60.0f,
|
||||||
"ms mount=", mountTime / 60.0f, "ms npcMgr=", npcMgrTime / 60.0f,
|
"ms world=", worldTime / 60.0f, "ms spawn=", spawnTime / 60.0f,
|
||||||
"ms questMark=", questMarkTime / 60.0f, "ms sync=", syncTime / 60.0f, "ms");
|
"ms creatureQ=", creatureQTime / 60.0f, "ms goQ=", goQTime / 60.0f,
|
||||||
|
"ms mount=", mountTime / 60.0f, "ms npcMgr=", npcMgrTime / 60.0f,
|
||||||
|
"ms questMark=", questMarkTime / 60.0f, "ms sync=", syncTime / 60.0f, "ms");
|
||||||
|
}
|
||||||
appProfileCounter = 0;
|
appProfileCounter = 0;
|
||||||
ghTime = worldTime = spawnTime = 0.0f;
|
ghTime = worldTime = spawnTime = 0.0f;
|
||||||
creatureQTime = goQTime = mountTime = 0.0f;
|
creatureQTime = goQTime = mountTime = 0.0f;
|
||||||
npcMgrTime = questMarkTime = syncTime = 0.0f;
|
npcMgrTime = questMarkTime = syncTime = 0.0f;
|
||||||
}
|
}
|
||||||
|
} catch (const std::bad_alloc& e) {
|
||||||
|
LOG_ERROR("OOM inside AppState::IN_GAME at step '", inGameStep, "': ", e.what());
|
||||||
|
throw;
|
||||||
|
} catch (const std::exception& e) {
|
||||||
|
LOG_ERROR("Exception inside AppState::IN_GAME at step '", inGameStep, "': ", e.what());
|
||||||
|
throw;
|
||||||
|
}
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1083,27 +1172,55 @@ void Application::update(float deltaTime) {
|
||||||
static int rendererProfileCounter = 0;
|
static int rendererProfileCounter = 0;
|
||||||
static float rendererTime = 0.0f, uiTime = 0.0f;
|
static float rendererTime = 0.0f, uiTime = 0.0f;
|
||||||
|
|
||||||
|
updateCheckpoint = "renderer update";
|
||||||
auto r1 = std::chrono::high_resolution_clock::now();
|
auto r1 = std::chrono::high_resolution_clock::now();
|
||||||
if (renderer && state == AppState::IN_GAME) {
|
if (renderer && state == AppState::IN_GAME) {
|
||||||
renderer->update(deltaTime);
|
try {
|
||||||
|
renderer->update(deltaTime);
|
||||||
|
} catch (const std::bad_alloc& e) {
|
||||||
|
LOG_ERROR("OOM during Application::update stage 'renderer->update': ", e.what());
|
||||||
|
throw;
|
||||||
|
} catch (const std::exception& e) {
|
||||||
|
LOG_ERROR("Exception during Application::update stage 'renderer->update': ", e.what());
|
||||||
|
throw;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
auto r2 = std::chrono::high_resolution_clock::now();
|
auto r2 = std::chrono::high_resolution_clock::now();
|
||||||
rendererTime += std::chrono::duration<float, std::milli>(r2 - r1).count();
|
rendererTime += std::chrono::duration<float, std::milli>(r2 - r1).count();
|
||||||
|
|
||||||
// Update UI
|
// Update UI
|
||||||
|
updateCheckpoint = "ui update";
|
||||||
auto u1 = std::chrono::high_resolution_clock::now();
|
auto u1 = std::chrono::high_resolution_clock::now();
|
||||||
if (uiManager) {
|
if (uiManager) {
|
||||||
uiManager->update(deltaTime);
|
try {
|
||||||
|
uiManager->update(deltaTime);
|
||||||
|
} catch (const std::bad_alloc& e) {
|
||||||
|
LOG_ERROR("OOM during Application::update stage 'uiManager->update': ", e.what());
|
||||||
|
throw;
|
||||||
|
} catch (const std::exception& e) {
|
||||||
|
LOG_ERROR("Exception during Application::update stage 'uiManager->update': ", e.what());
|
||||||
|
throw;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
auto u2 = std::chrono::high_resolution_clock::now();
|
auto u2 = std::chrono::high_resolution_clock::now();
|
||||||
uiTime += std::chrono::duration<float, std::milli>(u2 - u1).count();
|
uiTime += std::chrono::duration<float, std::milli>(u2 - u1).count();
|
||||||
|
|
||||||
|
updateCheckpoint = "renderer/ui profile log";
|
||||||
if (state == AppState::IN_GAME && ++rendererProfileCounter >= 60) {
|
if (state == AppState::IN_GAME && ++rendererProfileCounter >= 60) {
|
||||||
LOG_DEBUG("RENDERER/UI PROFILE (60 frames): renderer=", rendererTime / 60.0f,
|
if (core::Logger::getInstance().shouldLog(core::LogLevel::DEBUG)) {
|
||||||
"ms ui=", uiTime / 60.0f, "ms");
|
LOG_DEBUG("RENDERER/UI PROFILE (60 frames): renderer=", rendererTime / 60.0f,
|
||||||
|
"ms ui=", uiTime / 60.0f, "ms");
|
||||||
|
}
|
||||||
rendererProfileCounter = 0;
|
rendererProfileCounter = 0;
|
||||||
rendererTime = uiTime = 0.0f;
|
rendererTime = uiTime = 0.0f;
|
||||||
}
|
}
|
||||||
|
} catch (const std::bad_alloc& e) {
|
||||||
|
LOG_ERROR("OOM in Application::update checkpoint '", updateCheckpoint, "': ", e.what());
|
||||||
|
throw;
|
||||||
|
} catch (const std::exception& e) {
|
||||||
|
LOG_ERROR("Exception in Application::update checkpoint '", updateCheckpoint, "': ", e.what());
|
||||||
|
throw;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void Application::render() {
|
void Application::render() {
|
||||||
|
|
|
||||||
|
|
@ -1143,6 +1143,9 @@ void GameHandler::handlePacket(network::Packet& packet) {
|
||||||
}
|
}
|
||||||
|
|
||||||
uint16_t opcode = packet.getOpcode();
|
uint16_t opcode = packet.getOpcode();
|
||||||
|
try {
|
||||||
|
|
||||||
|
const bool allowVanillaAliases = isClassicLikeExpansion() || isActiveExpansion("tbc");
|
||||||
|
|
||||||
// Vanilla compatibility aliases:
|
// Vanilla compatibility aliases:
|
||||||
// - 0x006B: can be SMSG_COMPRESSED_MOVES on some vanilla-family servers
|
// - 0x006B: can be SMSG_COMPRESSED_MOVES on some vanilla-family servers
|
||||||
|
|
@ -1150,7 +1153,7 @@ void GameHandler::handlePacket(network::Packet& packet) {
|
||||||
// - 0x0103: SMSG_PLAY_MUSIC (some vanilla-family servers)
|
// - 0x0103: SMSG_PLAY_MUSIC (some vanilla-family servers)
|
||||||
//
|
//
|
||||||
// We gate these by payload shape so expansion-native mappings remain intact.
|
// We gate these by payload shape so expansion-native mappings remain intact.
|
||||||
if (opcode == 0x006B) {
|
if (allowVanillaAliases && opcode == 0x006B) {
|
||||||
// Try compressed movement batch first:
|
// Try compressed movement batch first:
|
||||||
// [u8 subSize][u16 subOpcode][subPayload...] ...
|
// [u8 subSize][u16 subOpcode][subPayload...] ...
|
||||||
// where subOpcode is typically SMSG_MONSTER_MOVE / SMSG_MONSTER_MOVE_TRANSPORT.
|
// where subOpcode is typically SMSG_MONSTER_MOVE / SMSG_MONSTER_MOVE_TRANSPORT.
|
||||||
|
|
@ -1198,7 +1201,7 @@ void GameHandler::handlePacket(network::Packet& packet) {
|
||||||
// Not weather-shaped: rewind and fall through to normal opcode table handling.
|
// Not weather-shaped: rewind and fall through to normal opcode table handling.
|
||||||
packet.setReadPos(0);
|
packet.setReadPos(0);
|
||||||
}
|
}
|
||||||
} else if (opcode == 0x0103) {
|
} else if (allowVanillaAliases && opcode == 0x0103) {
|
||||||
// Expected play-music payload: uint32 sound/music id
|
// Expected play-music payload: uint32 sound/music id
|
||||||
if (packet.getSize() - packet.getReadPos() == 4) {
|
if (packet.getSize() - packet.getReadPos() == 4) {
|
||||||
uint32_t soundId = packet.readUInt32();
|
uint32_t soundId = packet.readUInt32();
|
||||||
|
|
@ -2858,6 +2861,23 @@ void GameHandler::handlePacket(network::Packet& packet) {
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
} catch (const std::bad_alloc& e) {
|
||||||
|
LOG_ERROR("OOM while handling world opcode=0x", std::hex, opcode, std::dec,
|
||||||
|
" state=", worldStateName(state),
|
||||||
|
" size=", packet.getSize(),
|
||||||
|
" readPos=", packet.getReadPos(),
|
||||||
|
" what=", e.what());
|
||||||
|
if (socket && state == WorldState::IN_WORLD) {
|
||||||
|
disconnect();
|
||||||
|
fail("Out of memory while parsing world packet");
|
||||||
|
}
|
||||||
|
} catch (const std::exception& e) {
|
||||||
|
LOG_ERROR("Exception while handling world opcode=0x", std::hex, opcode, std::dec,
|
||||||
|
" state=", worldStateName(state),
|
||||||
|
" size=", packet.getSize(),
|
||||||
|
" readPos=", packet.getReadPos(),
|
||||||
|
" what=", e.what());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void GameHandler::handleAuthChallenge(network::Packet& packet) {
|
void GameHandler::handleAuthChallenge(network::Packet& packet) {
|
||||||
|
|
@ -8659,10 +8679,30 @@ void GameHandler::handleCompressedMoves(network::Packet& packet) {
|
||||||
|
|
||||||
void GameHandler::handleMonsterMove(network::Packet& packet) {
|
void GameHandler::handleMonsterMove(network::Packet& packet) {
|
||||||
MonsterMoveData data;
|
MonsterMoveData data;
|
||||||
|
auto logMonsterMoveParseFailure = [&](const std::string& msg) {
|
||||||
|
static uint32_t failCount = 0;
|
||||||
|
++failCount;
|
||||||
|
if (failCount <= 10 || (failCount % 100) == 0) {
|
||||||
|
LOG_WARNING(msg, " (occurrence=", failCount, ")");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
auto stripWrappedSubpacket = [&](const std::vector<uint8_t>& bytes, std::vector<uint8_t>& stripped) -> bool {
|
||||||
|
if (bytes.size() < 3) return false;
|
||||||
|
uint8_t subSize = bytes[0];
|
||||||
|
if (subSize < 2) return false;
|
||||||
|
size_t wrappedLen = static_cast<size_t>(subSize) + 1; // size byte + body
|
||||||
|
if (wrappedLen != bytes.size()) return false;
|
||||||
|
size_t payloadLen = static_cast<size_t>(subSize) - 2; // opcode(2) stripped
|
||||||
|
if (3 + payloadLen > bytes.size()) return false;
|
||||||
|
stripped.assign(bytes.begin() + 3, bytes.begin() + 3 + payloadLen);
|
||||||
|
return true;
|
||||||
|
};
|
||||||
// Turtle WoW (1.17+) compresses each SMSG_MONSTER_MOVE individually:
|
// Turtle WoW (1.17+) compresses each SMSG_MONSTER_MOVE individually:
|
||||||
// format: uint32 decompressedSize + zlib data (zlib magic = 0x78 ??)
|
// format: uint32 decompressedSize + zlib data (zlib magic = 0x78 ??)
|
||||||
const auto& rawData = packet.getData();
|
const auto& rawData = packet.getData();
|
||||||
bool isCompressed = rawData.size() >= 6 &&
|
const bool allowTurtleMoveCompression = isActiveExpansion("turtle");
|
||||||
|
bool isCompressed = allowTurtleMoveCompression &&
|
||||||
|
rawData.size() >= 6 &&
|
||||||
rawData[4] == 0x78 &&
|
rawData[4] == 0x78 &&
|
||||||
(rawData[5] == 0x01 || rawData[5] == 0x9C ||
|
(rawData[5] == 0x01 || rawData[5] == 0x9C ||
|
||||||
rawData[5] == 0xDA || rawData[5] == 0x5E);
|
rawData[5] == 0xDA || rawData[5] == 0x5E);
|
||||||
|
|
@ -8694,36 +8734,42 @@ void GameHandler::handleMonsterMove(network::Packet& packet) {
|
||||||
}
|
}
|
||||||
LOG_INFO("MonsterMove decomp[", destLen, "]: ", hex);
|
LOG_INFO("MonsterMove decomp[", destLen, "]: ", hex);
|
||||||
}
|
}
|
||||||
// Some Turtle WoW compressed move payloads include an inner
|
std::vector<uint8_t> stripped;
|
||||||
// sub-packet wrapper: uint8 size + uint16 opcode + payload.
|
bool hasWrappedForm = stripWrappedSubpacket(decompressed, stripped);
|
||||||
// Do not key this on expansion opcode mappings; strip by structure.
|
|
||||||
std::vector<uint8_t> parseBytes = decompressed;
|
|
||||||
if (destLen >= 3) {
|
|
||||||
uint8_t subSize = decompressed[0];
|
|
||||||
size_t wrappedLen = static_cast<size_t>(subSize) + 1; // size byte + subSize bytes
|
|
||||||
uint16_t innerOpcode = static_cast<uint16_t>(decompressed[1]) |
|
|
||||||
(static_cast<uint16_t>(decompressed[2]) << 8);
|
|
||||||
uint16_t monsterMoveWire = wireOpcode(Opcode::SMSG_MONSTER_MOVE);
|
|
||||||
bool looksLikeMonsterMoveWrapper =
|
|
||||||
(innerOpcode == 0x00DD) || (innerOpcode == monsterMoveWire);
|
|
||||||
// Strict case: one exact wrapped sub-packet in this decompressed blob.
|
|
||||||
if (subSize >= 2 && wrappedLen == destLen && looksLikeMonsterMoveWrapper) {
|
|
||||||
size_t payloadStart = 3;
|
|
||||||
size_t payloadLen = static_cast<size_t>(subSize) - 2;
|
|
||||||
parseBytes.assign(decompressed.begin() + payloadStart,
|
|
||||||
decompressed.begin() + payloadStart + payloadLen);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
network::Packet decompPacket(packet.getOpcode(), parseBytes);
|
// Try unwrapped payload first (common form), then wrapped-subpacket fallback.
|
||||||
|
network::Packet decompPacket(packet.getOpcode(), decompressed);
|
||||||
if (!packetParsers_->parseMonsterMove(decompPacket, data)) {
|
if (!packetParsers_->parseMonsterMove(decompPacket, data)) {
|
||||||
LOG_WARNING("Failed to parse vanilla SMSG_MONSTER_MOVE (decompressed ",
|
if (!hasWrappedForm) {
|
||||||
destLen, " bytes, parseBytes ", parseBytes.size(), " bytes)");
|
logMonsterMoveParseFailure("Failed to parse SMSG_MONSTER_MOVE (decompressed " +
|
||||||
return;
|
std::to_string(destLen) + " bytes)");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
network::Packet wrappedPacket(packet.getOpcode(), stripped);
|
||||||
|
if (!packetParsers_->parseMonsterMove(wrappedPacket, data)) {
|
||||||
|
logMonsterMoveParseFailure("Failed to parse SMSG_MONSTER_MOVE (decompressed " +
|
||||||
|
std::to_string(destLen) + " bytes, wrapped payload " +
|
||||||
|
std::to_string(stripped.size()) + " bytes)");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
LOG_WARNING("SMSG_MONSTER_MOVE parsed via wrapped-subpacket fallback");
|
||||||
}
|
}
|
||||||
} else if (!packetParsers_->parseMonsterMove(packet, data)) {
|
} else if (!packetParsers_->parseMonsterMove(packet, data)) {
|
||||||
LOG_WARNING("Failed to parse SMSG_MONSTER_MOVE");
|
// Some realms occasionally embed an extra [size|opcode] wrapper even when the
|
||||||
return;
|
// outer packet wasn't zlib-compressed. Retry with wrapper stripped by structure.
|
||||||
|
std::vector<uint8_t> stripped;
|
||||||
|
if (stripWrappedSubpacket(rawData, stripped)) {
|
||||||
|
network::Packet wrappedPacket(packet.getOpcode(), stripped);
|
||||||
|
if (packetParsers_->parseMonsterMove(wrappedPacket, data)) {
|
||||||
|
LOG_WARNING("SMSG_MONSTER_MOVE parsed via uncompressed wrapped-subpacket fallback");
|
||||||
|
} else {
|
||||||
|
logMonsterMoveParseFailure("Failed to parse SMSG_MONSTER_MOVE");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
logMonsterMoveParseFailure("Failed to parse SMSG_MONSTER_MOVE");
|
||||||
|
return;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update entity position in entity manager
|
// Update entity position in entity manager
|
||||||
|
|
|
||||||
|
|
@ -1192,6 +1192,24 @@ bool TurtlePacketParsers::parseMovementBlock(network::Packet& packet, UpdateBloc
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool TurtlePacketParsers::parseMonsterMove(network::Packet& packet, MonsterMoveData& data) {
|
||||||
|
// Turtle realms can emit both vanilla-like and WotLK-like monster move bodies.
|
||||||
|
// Try the canonical Turtle/vanilla parser first, then fall back to WotLK layout.
|
||||||
|
size_t start = packet.getReadPos();
|
||||||
|
if (MonsterMoveParser::parseVanilla(packet, data)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
packet.setReadPos(start);
|
||||||
|
if (MonsterMoveParser::parse(packet, data)) {
|
||||||
|
LOG_WARNING("[Turtle] SMSG_MONSTER_MOVE parsed via WotLK fallback layout");
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
packet.setReadPos(start);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
// Classic/Vanilla quest giver status
|
// Classic/Vanilla quest giver status
|
||||||
//
|
//
|
||||||
|
|
|
||||||
|
|
@ -376,83 +376,125 @@ bool TbcPacketParsers::parseCharEnum(network::Packet& packet, CharEnumResponse&
|
||||||
// (WotLK removed this field)
|
// (WotLK removed this field)
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
bool TbcPacketParsers::parseUpdateObject(network::Packet& packet, UpdateObjectData& data) {
|
bool TbcPacketParsers::parseUpdateObject(network::Packet& packet, UpdateObjectData& data) {
|
||||||
// Read block count
|
constexpr uint32_t kMaxReasonableUpdateBlocks = 4096;
|
||||||
data.blockCount = packet.readUInt32();
|
auto parseWithLayout = [&](bool withHasTransportByte, UpdateObjectData& out) -> bool {
|
||||||
|
out = UpdateObjectData{};
|
||||||
|
size_t start = packet.getReadPos();
|
||||||
|
if (packet.getSize() - start < 4) return false;
|
||||||
|
|
||||||
// TBC/Classic: has_transport byte (WotLK removed this)
|
out.blockCount = packet.readUInt32();
|
||||||
/*uint8_t hasTransport =*/ packet.readUInt8();
|
if (out.blockCount > kMaxReasonableUpdateBlocks) {
|
||||||
|
packet.setReadPos(start);
|
||||||
LOG_DEBUG("[TBC] SMSG_UPDATE_OBJECT: objectCount=", data.blockCount);
|
return false;
|
||||||
|
|
||||||
// Check for out-of-range objects first
|
|
||||||
if (packet.getReadPos() + 1 <= packet.getSize()) {
|
|
||||||
uint8_t firstByte = packet.readUInt8();
|
|
||||||
|
|
||||||
if (firstByte == static_cast<uint8_t>(UpdateType::OUT_OF_RANGE_OBJECTS)) {
|
|
||||||
uint32_t count = packet.readUInt32();
|
|
||||||
for (uint32_t i = 0; i < count; ++i) {
|
|
||||||
uint64_t guid = UpdateObjectParser::readPackedGuid(packet);
|
|
||||||
data.outOfRangeGuids.push_back(guid);
|
|
||||||
LOG_DEBUG(" Out of range: 0x", std::hex, guid, std::dec);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
packet.setReadPos(packet.getReadPos() - 1);
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
// Parse update blocks — dispatching movement via virtual parseMovementBlock()
|
if (withHasTransportByte) {
|
||||||
data.blocks.reserve(data.blockCount);
|
if (packet.getReadPos() >= packet.getSize()) {
|
||||||
for (uint32_t i = 0; i < data.blockCount; ++i) {
|
packet.setReadPos(start);
|
||||||
LOG_DEBUG("Parsing block ", i + 1, " / ", data.blockCount);
|
return false;
|
||||||
UpdateBlock block;
|
|
||||||
|
|
||||||
// Read update type
|
|
||||||
uint8_t updateTypeVal = packet.readUInt8();
|
|
||||||
block.updateType = static_cast<UpdateType>(updateTypeVal);
|
|
||||||
LOG_DEBUG("Update block: type=", (int)updateTypeVal);
|
|
||||||
|
|
||||||
bool ok = false;
|
|
||||||
switch (block.updateType) {
|
|
||||||
case UpdateType::VALUES: {
|
|
||||||
block.guid = UpdateObjectParser::readPackedGuid(packet);
|
|
||||||
ok = UpdateObjectParser::parseUpdateFields(packet, block);
|
|
||||||
break;
|
|
||||||
}
|
}
|
||||||
case UpdateType::MOVEMENT: {
|
/*uint8_t hasTransport =*/ packet.readUInt8();
|
||||||
block.guid = UpdateObjectParser::readPackedGuid(packet);
|
}
|
||||||
ok = this->parseMovementBlock(packet, block);
|
|
||||||
break;
|
if (packet.getReadPos() + 1 <= packet.getSize()) {
|
||||||
}
|
uint8_t firstByte = packet.readUInt8();
|
||||||
case UpdateType::CREATE_OBJECT:
|
if (firstByte == static_cast<uint8_t>(UpdateType::OUT_OF_RANGE_OBJECTS)) {
|
||||||
case UpdateType::CREATE_OBJECT2: {
|
if (packet.getReadPos() + 4 > packet.getSize()) {
|
||||||
block.guid = UpdateObjectParser::readPackedGuid(packet);
|
packet.setReadPos(start);
|
||||||
uint8_t objectTypeVal = packet.readUInt8();
|
return false;
|
||||||
block.objectType = static_cast<ObjectType>(objectTypeVal);
|
|
||||||
ok = this->parseMovementBlock(packet, block);
|
|
||||||
if (ok) {
|
|
||||||
ok = UpdateObjectParser::parseUpdateFields(packet, block);
|
|
||||||
}
|
}
|
||||||
break;
|
uint32_t count = packet.readUInt32();
|
||||||
|
if (count > kMaxReasonableUpdateBlocks) {
|
||||||
|
packet.setReadPos(start);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
for (uint32_t i = 0; i < count; ++i) {
|
||||||
|
if (packet.getReadPos() >= packet.getSize()) {
|
||||||
|
packet.setReadPos(start);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
uint64_t guid = UpdateObjectParser::readPackedGuid(packet);
|
||||||
|
out.outOfRangeGuids.push_back(guid);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
packet.setReadPos(packet.getReadPos() - 1);
|
||||||
}
|
}
|
||||||
case UpdateType::OUT_OF_RANGE_OBJECTS:
|
|
||||||
case UpdateType::NEAR_OBJECTS:
|
|
||||||
ok = true;
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
LOG_WARNING("Unknown update type: ", (int)updateTypeVal);
|
|
||||||
ok = false;
|
|
||||||
break;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!ok) {
|
out.blocks.reserve(out.blockCount);
|
||||||
LOG_WARNING("Failed to parse update block ", i + 1, " of ", data.blockCount,
|
for (uint32_t i = 0; i < out.blockCount; ++i) {
|
||||||
" — keeping ", data.blocks.size(), " parsed blocks");
|
if (packet.getReadPos() >= packet.getSize()) {
|
||||||
break;
|
packet.setReadPos(start);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
UpdateBlock block;
|
||||||
|
uint8_t updateTypeVal = packet.readUInt8();
|
||||||
|
if (updateTypeVal > static_cast<uint8_t>(UpdateType::NEAR_OBJECTS)) {
|
||||||
|
packet.setReadPos(start);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
block.updateType = static_cast<UpdateType>(updateTypeVal);
|
||||||
|
|
||||||
|
bool ok = false;
|
||||||
|
switch (block.updateType) {
|
||||||
|
case UpdateType::VALUES: {
|
||||||
|
block.guid = UpdateObjectParser::readPackedGuid(packet);
|
||||||
|
ok = UpdateObjectParser::parseUpdateFields(packet, block);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case UpdateType::MOVEMENT: {
|
||||||
|
block.guid = UpdateObjectParser::readPackedGuid(packet);
|
||||||
|
ok = this->parseMovementBlock(packet, block);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case UpdateType::CREATE_OBJECT:
|
||||||
|
case UpdateType::CREATE_OBJECT2: {
|
||||||
|
block.guid = UpdateObjectParser::readPackedGuid(packet);
|
||||||
|
if (packet.getReadPos() >= packet.getSize()) {
|
||||||
|
ok = false;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
uint8_t objectTypeVal = packet.readUInt8();
|
||||||
|
block.objectType = static_cast<ObjectType>(objectTypeVal);
|
||||||
|
ok = this->parseMovementBlock(packet, block);
|
||||||
|
if (ok) ok = UpdateObjectParser::parseUpdateFields(packet, block);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case UpdateType::OUT_OF_RANGE_OBJECTS:
|
||||||
|
case UpdateType::NEAR_OBJECTS:
|
||||||
|
ok = true;
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
ok = false;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!ok) {
|
||||||
|
packet.setReadPos(start);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
out.blocks.push_back(block);
|
||||||
}
|
}
|
||||||
data.blocks.push_back(block);
|
return true;
|
||||||
|
};
|
||||||
|
|
||||||
|
size_t startPos = packet.getReadPos();
|
||||||
|
UpdateObjectData parsed;
|
||||||
|
if (parseWithLayout(true, parsed)) {
|
||||||
|
data = std::move(parsed);
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
return true;
|
packet.setReadPos(startPos);
|
||||||
|
if (parseWithLayout(false, parsed)) {
|
||||||
|
LOG_WARNING("[TBC] SMSG_UPDATE_OBJECT parsed without has_transport byte fallback");
|
||||||
|
data = std::move(parsed);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
packet.setReadPos(startPos);
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
network::Packet TbcPacketParsers::buildAcceptQuestPacket(uint64_t npcGuid, uint32_t questId) {
|
network::Packet TbcPacketParsers::buildAcceptQuestPacket(uint64_t npcGuid, uint32_t questId) {
|
||||||
|
|
|
||||||
|
|
@ -1131,9 +1131,16 @@ bool UpdateObjectParser::parseUpdateBlock(network::Packet& packet, UpdateBlock&
|
||||||
}
|
}
|
||||||
|
|
||||||
bool UpdateObjectParser::parse(network::Packet& packet, UpdateObjectData& data) {
|
bool UpdateObjectParser::parse(network::Packet& packet, UpdateObjectData& data) {
|
||||||
|
constexpr uint32_t kMaxReasonableUpdateBlocks = 4096;
|
||||||
|
constexpr uint32_t kMaxReasonableOutOfRangeGuids = 16384;
|
||||||
|
|
||||||
// Read block count
|
// Read block count
|
||||||
data.blockCount = packet.readUInt32();
|
data.blockCount = packet.readUInt32();
|
||||||
|
if (data.blockCount > kMaxReasonableUpdateBlocks) {
|
||||||
|
LOG_ERROR("SMSG_UPDATE_OBJECT rejected: unreasonable blockCount=", data.blockCount,
|
||||||
|
" packetSize=", packet.getSize());
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
LOG_DEBUG("SMSG_UPDATE_OBJECT:");
|
LOG_DEBUG("SMSG_UPDATE_OBJECT:");
|
||||||
LOG_DEBUG(" objectCount = ", data.blockCount);
|
LOG_DEBUG(" objectCount = ", data.blockCount);
|
||||||
|
|
@ -1146,6 +1153,11 @@ bool UpdateObjectParser::parse(network::Packet& packet, UpdateObjectData& data)
|
||||||
if (firstByte == static_cast<uint8_t>(UpdateType::OUT_OF_RANGE_OBJECTS)) {
|
if (firstByte == static_cast<uint8_t>(UpdateType::OUT_OF_RANGE_OBJECTS)) {
|
||||||
// Read out-of-range GUID count
|
// Read out-of-range GUID count
|
||||||
uint32_t count = packet.readUInt32();
|
uint32_t count = packet.readUInt32();
|
||||||
|
if (count > kMaxReasonableOutOfRangeGuids) {
|
||||||
|
LOG_ERROR("SMSG_UPDATE_OBJECT rejected: unreasonable outOfRange count=", count,
|
||||||
|
" packetSize=", packet.getSize());
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
for (uint32_t i = 0; i < count; ++i) {
|
for (uint32_t i = 0; i < count; ++i) {
|
||||||
uint64_t guid = readPackedGuid(packet);
|
uint64_t guid = readPackedGuid(packet);
|
||||||
|
|
|
||||||
|
|
@ -147,8 +147,15 @@ BLPImage AssetManager::loadTexture(const std::string& path) {
|
||||||
std::vector<uint8_t> blpData = readFile(normalizedPath);
|
std::vector<uint8_t> blpData = readFile(normalizedPath);
|
||||||
if (blpData.empty()) {
|
if (blpData.empty()) {
|
||||||
static std::unordered_set<std::string> loggedMissingTextures;
|
static std::unordered_set<std::string> loggedMissingTextures;
|
||||||
if (loggedMissingTextures.insert(normalizedPath).second) {
|
static bool missingTextureLogSuppressed = false;
|
||||||
|
static constexpr size_t kMaxMissingTextureLogKeys = 20000;
|
||||||
|
if (loggedMissingTextures.size() < kMaxMissingTextureLogKeys &&
|
||||||
|
loggedMissingTextures.insert(normalizedPath).second) {
|
||||||
LOG_WARNING("Texture not found: ", normalizedPath);
|
LOG_WARNING("Texture not found: ", normalizedPath);
|
||||||
|
} else if (!missingTextureLogSuppressed && loggedMissingTextures.size() >= kMaxMissingTextureLogKeys) {
|
||||||
|
LOG_WARNING("Texture-not-found warning key cache reached ", kMaxMissingTextureLogKeys,
|
||||||
|
" entries; suppressing new unique texture-miss logs");
|
||||||
|
missingTextureLogSuppressed = true;
|
||||||
}
|
}
|
||||||
return BLPImage();
|
return BLPImage();
|
||||||
}
|
}
|
||||||
|
|
@ -156,8 +163,15 @@ BLPImage AssetManager::loadTexture(const std::string& path) {
|
||||||
BLPImage image = BLPLoader::load(blpData);
|
BLPImage image = BLPLoader::load(blpData);
|
||||||
if (!image.isValid()) {
|
if (!image.isValid()) {
|
||||||
static std::unordered_set<std::string> loggedDecodeFails;
|
static std::unordered_set<std::string> loggedDecodeFails;
|
||||||
if (loggedDecodeFails.insert(normalizedPath).second) {
|
static bool decodeFailLogSuppressed = false;
|
||||||
|
static constexpr size_t kMaxDecodeFailLogKeys = 8000;
|
||||||
|
if (loggedDecodeFails.size() < kMaxDecodeFailLogKeys &&
|
||||||
|
loggedDecodeFails.insert(normalizedPath).second) {
|
||||||
LOG_ERROR("Failed to load texture: ", normalizedPath);
|
LOG_ERROR("Failed to load texture: ", normalizedPath);
|
||||||
|
} else if (!decodeFailLogSuppressed && loggedDecodeFails.size() >= kMaxDecodeFailLogKeys) {
|
||||||
|
LOG_WARNING("Texture-decode warning key cache reached ", kMaxDecodeFailLogKeys,
|
||||||
|
" entries; suppressing new unique decode-failure logs");
|
||||||
|
decodeFailLogSuppressed = true;
|
||||||
}
|
}
|
||||||
return BLPImage();
|
return BLPImage();
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -250,7 +250,7 @@ bool CharacterRenderer::initialize(VkContext* ctx, VkDescriptorSetLayout perFram
|
||||||
}
|
}
|
||||||
|
|
||||||
// Diagnostics-only: cache lifetime is currently tied to renderer lifetime.
|
// Diagnostics-only: cache lifetime is currently tied to renderer lifetime.
|
||||||
textureCacheBudgetBytes_ = envSizeMBOrDefault("WOWEE_CHARACTER_TEX_CACHE_MB", 2048) * 1024ull * 1024ull;
|
textureCacheBudgetBytes_ = envSizeMBOrDefault("WOWEE_CHARACTER_TEX_CACHE_MB", 512) * 1024ull * 1024ull;
|
||||||
|
|
||||||
core::Logger::getInstance().info("Character renderer initialized (Vulkan)");
|
core::Logger::getInstance().info("Character renderer initialized (Vulkan)");
|
||||||
return true;
|
return true;
|
||||||
|
|
|
||||||
|
|
@ -40,6 +40,15 @@ bool envFlagEnabled(const char* key, bool defaultValue) {
|
||||||
return !(v == "0" || v == "false" || v == "off" || v == "no");
|
return !(v == "0" || v == "false" || v == "off" || v == "no");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
size_t envSizeMBOrDefault(const char* name, size_t defMb) {
|
||||||
|
const char* raw = std::getenv(name);
|
||||||
|
if (!raw || !*raw) return defMb;
|
||||||
|
char* end = nullptr;
|
||||||
|
unsigned long long mb = std::strtoull(raw, &end, 10);
|
||||||
|
if (end == raw || mb == 0) return defMb;
|
||||||
|
return static_cast<size_t>(mb);
|
||||||
|
}
|
||||||
|
|
||||||
static constexpr uint32_t kParticleFlagRandomized = 0x40;
|
static constexpr uint32_t kParticleFlagRandomized = 0x40;
|
||||||
static constexpr uint32_t kParticleFlagTiled = 0x80;
|
static constexpr uint32_t kParticleFlagTiled = 0x80;
|
||||||
|
|
||||||
|
|
@ -601,6 +610,11 @@ bool M2Renderer::initialize(VkContext* ctx, VkDescriptorSetLayout perFrameLayout
|
||||||
glowTexture_->upload(*vkCtx_, px.data(), SZ, SZ, VK_FORMAT_R8G8B8A8_UNORM);
|
glowTexture_->upload(*vkCtx_, px.data(), SZ, SZ, VK_FORMAT_R8G8B8A8_UNORM);
|
||||||
glowTexture_->createSampler(device, VK_FILTER_LINEAR, VK_FILTER_LINEAR, VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_EDGE);
|
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;
|
||||||
|
modelCacheLimit_ = envSizeMBOrDefault("WOWEE_M2_MODEL_LIMIT", 6000);
|
||||||
|
LOG_INFO("M2 texture cache budget: ", textureCacheBudgetBytes_ / (1024 * 1024), " MB");
|
||||||
|
LOG_INFO("M2 model cache limit: ", modelCacheLimit_);
|
||||||
|
|
||||||
LOG_INFO("M2 renderer initialized (Vulkan)");
|
LOG_INFO("M2 renderer initialized (Vulkan)");
|
||||||
initialized_ = true;
|
initialized_ = true;
|
||||||
|
|
@ -635,6 +649,9 @@ void M2Renderer::shutdown() {
|
||||||
textureCacheCounter_ = 0;
|
textureCacheCounter_ = 0;
|
||||||
textureHasAlphaByPtr_.clear();
|
textureHasAlphaByPtr_.clear();
|
||||||
textureColorKeyBlackByPtr_.clear();
|
textureColorKeyBlackByPtr_.clear();
|
||||||
|
failedTextureCache_.clear();
|
||||||
|
loggedTextureLoadFails_.clear();
|
||||||
|
textureBudgetRejectWarnings_ = 0;
|
||||||
whiteTexture_.reset();
|
whiteTexture_.reset();
|
||||||
glowTexture_.reset();
|
glowTexture_.reset();
|
||||||
|
|
||||||
|
|
@ -827,6 +844,14 @@ bool M2Renderer::loadModel(const pipeline::M2Model& model, uint32_t modelId) {
|
||||||
// Already loaded
|
// Already loaded
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
if (models.size() >= modelCacheLimit_) {
|
||||||
|
if (modelLimitRejectWarnings_ < 8 || (modelLimitRejectWarnings_ % 120) == 0) {
|
||||||
|
LOG_WARNING("M2 model cache full (", models.size(), "/", modelCacheLimit_,
|
||||||
|
"), skipping model load: id=", modelId, " name=", model.name);
|
||||||
|
}
|
||||||
|
++modelLimitRejectWarnings_;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
bool hasGeometry = !model.vertices.empty() && !model.indices.empty();
|
bool hasGeometry = !model.vertices.empty() && !model.indices.empty();
|
||||||
bool hasParticles = !model.particleEmitters.empty();
|
bool hasParticles = !model.particleEmitters.empty();
|
||||||
|
|
@ -1134,10 +1159,15 @@ bool M2Renderer::loadModel(const pipeline::M2Model& model, uint32_t modelId) {
|
||||||
VkTexture* texPtr = loadTexture(texPath, tex.flags);
|
VkTexture* texPtr = loadTexture(texPath, tex.flags);
|
||||||
bool failed = (texPtr == whiteTexture_.get());
|
bool failed = (texPtr == whiteTexture_.get());
|
||||||
if (failed) {
|
if (failed) {
|
||||||
static std::unordered_set<std::string> loggedModelTextureFails;
|
static uint32_t loggedModelTextureFails = 0;
|
||||||
std::string failKey = model.name + "|" + texKey;
|
static bool loggedModelTextureFailSuppressed = false;
|
||||||
if (loggedModelTextureFails.insert(failKey).second) {
|
if (loggedModelTextureFails < 250) {
|
||||||
LOG_WARNING("M2 model ", model.name, " texture[", ti, "] failed to load: ", texPath);
|
LOG_WARNING("M2 model ", model.name, " texture[", ti, "] failed to load: ", texPath);
|
||||||
|
++loggedModelTextureFails;
|
||||||
|
} else if (!loggedModelTextureFailSuppressed) {
|
||||||
|
LOG_WARNING("M2 model texture-failure warnings suppressed after ",
|
||||||
|
loggedModelTextureFails, " entries");
|
||||||
|
loggedModelTextureFailSuppressed = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (isInvisibleTrap) {
|
if (isInvisibleTrap) {
|
||||||
|
|
@ -3155,6 +3185,9 @@ VkTexture* M2Renderer::loadTexture(const std::string& path, uint32_t texFlags) {
|
||||||
it->second.lastUse = ++textureCacheCounter_;
|
it->second.lastUse = ++textureCacheCounter_;
|
||||||
return it->second.texture.get();
|
return it->second.texture.get();
|
||||||
}
|
}
|
||||||
|
if (failedTextureCache_.count(key)) {
|
||||||
|
return whiteTexture_.get();
|
||||||
|
}
|
||||||
|
|
||||||
auto containsToken = [](const std::string& haystack, const char* token) {
|
auto containsToken = [](const std::string& haystack, const char* token) {
|
||||||
return haystack.find(token) != std::string::npos;
|
return haystack.find(token) != std::string::npos;
|
||||||
|
|
@ -3175,13 +3208,28 @@ VkTexture* M2Renderer::loadTexture(const std::string& path, uint32_t texFlags) {
|
||||||
// Load BLP texture
|
// Load BLP texture
|
||||||
pipeline::BLPImage blp = assetManager->loadTexture(key);
|
pipeline::BLPImage blp = assetManager->loadTexture(key);
|
||||||
if (!blp.isValid()) {
|
if (!blp.isValid()) {
|
||||||
static std::unordered_set<std::string> loggedTextureLoadFails;
|
static constexpr size_t kMaxFailedTextureCache = 200000;
|
||||||
if (loggedTextureLoadFails.insert(key).second) {
|
if (failedTextureCache_.size() < kMaxFailedTextureCache) {
|
||||||
|
failedTextureCache_.insert(key);
|
||||||
|
}
|
||||||
|
if (loggedTextureLoadFails_.insert(key).second) {
|
||||||
LOG_WARNING("M2: Failed to load texture: ", path);
|
LOG_WARNING("M2: Failed to load texture: ", path);
|
||||||
}
|
}
|
||||||
return whiteTexture_.get();
|
return whiteTexture_.get();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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_) {
|
||||||
|
if (textureBudgetRejectWarnings_ < 8 || (textureBudgetRejectWarnings_ % 120) == 0) {
|
||||||
|
LOG_WARNING("M2 texture cache full (", textureCacheBytes_ / (1024 * 1024),
|
||||||
|
" MB / ", textureCacheBudgetBytes_ / (1024 * 1024),
|
||||||
|
" MB), rejecting texture: ", path);
|
||||||
|
}
|
||||||
|
++textureBudgetRejectWarnings_;
|
||||||
|
return whiteTexture_.get();
|
||||||
|
}
|
||||||
|
|
||||||
// Track whether the texture actually uses alpha (any pixel with alpha < 255).
|
// Track whether the texture actually uses alpha (any pixel with alpha < 255).
|
||||||
bool hasAlpha = false;
|
bool hasAlpha = false;
|
||||||
for (size_t i = 3; i < blp.data.size(); i += 4) {
|
for (size_t i = 3; i < blp.data.size(); i += 4) {
|
||||||
|
|
@ -3204,8 +3252,7 @@ VkTexture* M2Renderer::loadTexture(const std::string& path, uint32_t texFlags) {
|
||||||
|
|
||||||
TextureCacheEntry e;
|
TextureCacheEntry e;
|
||||||
e.texture = std::move(tex);
|
e.texture = std::move(tex);
|
||||||
size_t base = static_cast<size_t>(blp.width) * static_cast<size_t>(blp.height) * 4ull;
|
e.approxBytes = approxBytes;
|
||||||
e.approxBytes = base + (base / 3);
|
|
||||||
e.hasAlpha = hasAlpha;
|
e.hasAlpha = hasAlpha;
|
||||||
e.colorKeyBlack = colorKeyBlackHint;
|
e.colorKeyBlack = colorKeyBlackHint;
|
||||||
e.lastUse = ++textureCacheCounter_;
|
e.lastUse = ++textureCacheCounter_;
|
||||||
|
|
|
||||||
|
|
@ -2756,6 +2756,19 @@ void Renderer::update(float deltaTime) {
|
||||||
performanceHUD->update(deltaTime);
|
performanceHUD->update(deltaTime);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Periodic cache hygiene: drop model GPU data no longer referenced by active instances.
|
||||||
|
static float modelCleanupTimer = 0.0f;
|
||||||
|
modelCleanupTimer += deltaTime;
|
||||||
|
if (modelCleanupTimer >= 5.0f) {
|
||||||
|
if (wmoRenderer) {
|
||||||
|
wmoRenderer->cleanupUnusedModels();
|
||||||
|
}
|
||||||
|
if (m2Renderer) {
|
||||||
|
m2Renderer->cleanupUnusedModels();
|
||||||
|
}
|
||||||
|
modelCleanupTimer = 0.0f;
|
||||||
|
}
|
||||||
|
|
||||||
auto updateEnd = std::chrono::steady_clock::now();
|
auto updateEnd = std::chrono::steady_clock::now();
|
||||||
lastUpdateMs = std::chrono::duration<double, std::milli>(updateEnd - updateStart).count();
|
lastUpdateMs = std::chrono::duration<double, std::milli>(updateEnd - updateStart).count();
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -20,6 +20,17 @@
|
||||||
namespace wowee {
|
namespace wowee {
|
||||||
namespace rendering {
|
namespace rendering {
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
size_t envSizeMBOrDefault(const char* name, size_t defMb) {
|
||||||
|
const char* raw = std::getenv(name);
|
||||||
|
if (!raw || !*raw) return defMb;
|
||||||
|
char* end = nullptr;
|
||||||
|
unsigned long long mb = std::strtoull(raw, &end, 10);
|
||||||
|
if (end == raw || mb == 0) return defMb;
|
||||||
|
return static_cast<size_t>(mb);
|
||||||
|
}
|
||||||
|
} // namespace
|
||||||
|
|
||||||
// Matches set 1 binding 7 in terrain.frag.glsl
|
// Matches set 1 binding 7 in terrain.frag.glsl
|
||||||
struct TerrainParamsUBO {
|
struct TerrainParamsUBO {
|
||||||
int32_t layerCount;
|
int32_t layerCount;
|
||||||
|
|
@ -185,6 +196,9 @@ bool TerrainRenderer::initialize(VkContext* ctx, VkDescriptorSetLayout perFrameL
|
||||||
opaqueAlphaTexture->upload(*vkCtx, &opaqueAlpha, 1, 1, VK_FORMAT_R8_UNORM, false);
|
opaqueAlphaTexture->upload(*vkCtx, &opaqueAlpha, 1, 1, VK_FORMAT_R8_UNORM, false);
|
||||||
opaqueAlphaTexture->createSampler(device, VK_FILTER_LINEAR, VK_FILTER_LINEAR,
|
opaqueAlphaTexture->createSampler(device, VK_FILTER_LINEAR, VK_FILTER_LINEAR,
|
||||||
VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_EDGE);
|
VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_EDGE);
|
||||||
|
textureCacheBudgetBytes_ =
|
||||||
|
envSizeMBOrDefault("WOWEE_TERRAIN_TEX_CACHE_MB", 512) * 1024ull * 1024ull;
|
||||||
|
LOG_INFO("Terrain texture cache budget: ", textureCacheBudgetBytes_ / (1024 * 1024), " MB");
|
||||||
|
|
||||||
LOG_INFO("Terrain renderer initialized (Vulkan)");
|
LOG_INFO("Terrain renderer initialized (Vulkan)");
|
||||||
return true;
|
return true;
|
||||||
|
|
@ -287,6 +301,9 @@ void TerrainRenderer::shutdown() {
|
||||||
textureCache.clear();
|
textureCache.clear();
|
||||||
textureCacheBytes_ = 0;
|
textureCacheBytes_ = 0;
|
||||||
textureCacheCounter_ = 0;
|
textureCacheCounter_ = 0;
|
||||||
|
failedTextureCache_.clear();
|
||||||
|
loggedTextureLoadFails_.clear();
|
||||||
|
textureBudgetRejectWarnings_ = 0;
|
||||||
|
|
||||||
if (whiteTexture) { whiteTexture->destroy(device, allocator); whiteTexture.reset(); }
|
if (whiteTexture) { whiteTexture->destroy(device, allocator); whiteTexture.reset(); }
|
||||||
if (opaqueAlphaTexture) { opaqueAlphaTexture->destroy(device, allocator); opaqueAlphaTexture.reset(); }
|
if (opaqueAlphaTexture) { opaqueAlphaTexture->destroy(device, allocator); opaqueAlphaTexture.reset(); }
|
||||||
|
|
@ -425,10 +442,31 @@ VkTexture* TerrainRenderer::loadTexture(const std::string& path) {
|
||||||
it->second.lastUse = ++textureCacheCounter_;
|
it->second.lastUse = ++textureCacheCounter_;
|
||||||
return it->second.texture.get();
|
return it->second.texture.get();
|
||||||
}
|
}
|
||||||
|
if (failedTextureCache_.count(key)) {
|
||||||
|
return whiteTexture.get();
|
||||||
|
}
|
||||||
|
|
||||||
pipeline::BLPImage blp = assetManager->loadTexture(key);
|
pipeline::BLPImage blp = assetManager->loadTexture(key);
|
||||||
if (!blp.isValid()) {
|
if (!blp.isValid()) {
|
||||||
LOG_WARNING("Failed to load texture: ", path);
|
static constexpr size_t kMaxFailedTextureCache = 200000;
|
||||||
|
if (failedTextureCache_.size() < kMaxFailedTextureCache) {
|
||||||
|
failedTextureCache_.insert(key);
|
||||||
|
}
|
||||||
|
if (loggedTextureLoadFails_.insert(key).second) {
|
||||||
|
LOG_WARNING("Failed to load texture: ", path);
|
||||||
|
}
|
||||||
|
return whiteTexture.get();
|
||||||
|
}
|
||||||
|
|
||||||
|
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_) {
|
||||||
|
if (textureBudgetRejectWarnings_ < 8 || (textureBudgetRejectWarnings_ % 120) == 0) {
|
||||||
|
LOG_WARNING("Terrain texture cache full (", textureCacheBytes_ / (1024 * 1024),
|
||||||
|
" MB / ", textureCacheBudgetBytes_ / (1024 * 1024),
|
||||||
|
" MB), rejecting texture: ", path);
|
||||||
|
}
|
||||||
|
++textureBudgetRejectWarnings_;
|
||||||
return whiteTexture.get();
|
return whiteTexture.get();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -444,8 +482,7 @@ VkTexture* TerrainRenderer::loadTexture(const std::string& path) {
|
||||||
VkTexture* raw = tex.get();
|
VkTexture* raw = tex.get();
|
||||||
TextureCacheEntry e;
|
TextureCacheEntry e;
|
||||||
e.texture = std::move(tex);
|
e.texture = std::move(tex);
|
||||||
size_t base = static_cast<size_t>(blp.width) * static_cast<size_t>(blp.height) * 4ull;
|
e.approxBytes = approxBytes;
|
||||||
e.approxBytes = base + (base / 3);
|
|
||||||
e.lastUse = ++textureCacheCounter_;
|
e.lastUse = ++textureCacheCounter_;
|
||||||
textureCacheBytes_ += e.approxBytes;
|
textureCacheBytes_ += e.approxBytes;
|
||||||
textureCache[key] = std::move(e);
|
textureCache[key] = std::move(e);
|
||||||
|
|
|
||||||
|
|
@ -17,6 +17,7 @@
|
||||||
#include <algorithm>
|
#include <algorithm>
|
||||||
#include <chrono>
|
#include <chrono>
|
||||||
#include <cmath>
|
#include <cmath>
|
||||||
|
#include <cstdlib>
|
||||||
#include <filesystem>
|
#include <filesystem>
|
||||||
#include <fstream>
|
#include <fstream>
|
||||||
#include <future>
|
#include <future>
|
||||||
|
|
@ -27,6 +28,17 @@
|
||||||
namespace wowee {
|
namespace wowee {
|
||||||
namespace rendering {
|
namespace rendering {
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
size_t envSizeMBOrDefault(const char* name, size_t defMb) {
|
||||||
|
const char* raw = std::getenv(name);
|
||||||
|
if (!raw || !*raw) return defMb;
|
||||||
|
char* end = nullptr;
|
||||||
|
unsigned long long mb = std::strtoull(raw, &end, 10);
|
||||||
|
if (end == raw || mb == 0) return defMb;
|
||||||
|
return static_cast<size_t>(mb);
|
||||||
|
}
|
||||||
|
} // namespace
|
||||||
|
|
||||||
static void transformAABB(const glm::mat4& modelMatrix,
|
static void transformAABB(const glm::mat4& modelMatrix,
|
||||||
const glm::vec3& localMin,
|
const glm::vec3& localMin,
|
||||||
const glm::vec3& localMax,
|
const glm::vec3& localMax,
|
||||||
|
|
@ -214,6 +226,12 @@ bool WMORenderer::initialize(VkContext* ctx, VkDescriptorSetLayout perFrameLayou
|
||||||
whiteTexture_->upload(*vkCtx_, whitePixel, 1, 1, VK_FORMAT_R8G8B8A8_UNORM, false);
|
whiteTexture_->upload(*vkCtx_, whitePixel, 1, 1, VK_FORMAT_R8G8B8A8_UNORM, false);
|
||||||
whiteTexture_->createSampler(device, VK_FILTER_LINEAR, VK_FILTER_LINEAR,
|
whiteTexture_->createSampler(device, VK_FILTER_LINEAR, VK_FILTER_LINEAR,
|
||||||
VK_SAMPLER_ADDRESS_MODE_REPEAT);
|
VK_SAMPLER_ADDRESS_MODE_REPEAT);
|
||||||
|
textureCacheBudgetBytes_ =
|
||||||
|
envSizeMBOrDefault("WOWEE_WMO_TEX_CACHE_MB", 512) * 1024ull * 1024ull;
|
||||||
|
modelCacheLimit_ = envSizeMBOrDefault("WOWEE_WMO_MODEL_LIMIT", 4000);
|
||||||
|
core::Logger::getInstance().info("WMO texture cache budget: ",
|
||||||
|
textureCacheBudgetBytes_ / (1024 * 1024), " MB");
|
||||||
|
core::Logger::getInstance().info("WMO model cache limit: ", modelCacheLimit_);
|
||||||
|
|
||||||
core::Logger::getInstance().info("WMO renderer initialized (Vulkan)");
|
core::Logger::getInstance().info("WMO renderer initialized (Vulkan)");
|
||||||
initialized_ = true;
|
initialized_ = true;
|
||||||
|
|
@ -251,6 +269,9 @@ void WMORenderer::shutdown() {
|
||||||
textureCache.clear();
|
textureCache.clear();
|
||||||
textureCacheBytes_ = 0;
|
textureCacheBytes_ = 0;
|
||||||
textureCacheCounter_ = 0;
|
textureCacheCounter_ = 0;
|
||||||
|
failedTextureCache_.clear();
|
||||||
|
loggedTextureLoadFails_.clear();
|
||||||
|
textureBudgetRejectWarnings_ = 0;
|
||||||
|
|
||||||
// Free white texture
|
// Free white texture
|
||||||
if (whiteTexture_) { whiteTexture_->destroy(device, allocator); whiteTexture_.reset(); }
|
if (whiteTexture_) { whiteTexture_->destroy(device, allocator); whiteTexture_.reset(); }
|
||||||
|
|
@ -301,6 +322,14 @@ bool WMORenderer::loadModel(const pipeline::WMOModel& model, uint32_t id) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
static std::unordered_set<uint32_t> retryReloadedModels;
|
static std::unordered_set<uint32_t> retryReloadedModels;
|
||||||
|
static bool retryReloadedModelsCapped = false;
|
||||||
|
if (retryReloadedModels.size() > 8192) {
|
||||||
|
retryReloadedModels.clear();
|
||||||
|
if (!retryReloadedModelsCapped) {
|
||||||
|
core::Logger::getInstance().warning("WMO fallback-retry set exceeded 8192 entries; reset");
|
||||||
|
retryReloadedModelsCapped = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
if (!hasResolvedTexture && retryReloadedModels.insert(id).second) {
|
if (!hasResolvedTexture && retryReloadedModels.insert(id).second) {
|
||||||
core::Logger::getInstance().warning(
|
core::Logger::getInstance().warning(
|
||||||
"WMO model ", id,
|
"WMO model ", id,
|
||||||
|
|
@ -313,6 +342,15 @@ bool WMORenderer::loadModel(const pipeline::WMOModel& model, uint32_t id) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if (loadedModels.size() >= modelCacheLimit_) {
|
||||||
|
if (modelLimitRejectWarnings_ < 8 || (modelLimitRejectWarnings_ % 120) == 0) {
|
||||||
|
core::Logger::getInstance().warning("WMO model cache full (",
|
||||||
|
loadedModels.size(), "/", modelCacheLimit_,
|
||||||
|
"), skipping model load: id=", id);
|
||||||
|
}
|
||||||
|
++modelLimitRejectWarnings_;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
core::Logger::getInstance().debug("Loading WMO model ", id, " with ", model.groups.size(), " groups, ",
|
core::Logger::getInstance().debug("Loading WMO model ", id, " with ", model.groups.size(), " groups, ",
|
||||||
model.textures.size(), " textures...");
|
model.textures.size(), " textures...");
|
||||||
|
|
@ -1863,10 +1901,21 @@ VkTexture* WMORenderer::loadTexture(const std::string& path) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
std::vector<std::string> attemptedCandidates;
|
||||||
|
attemptedCandidates.reserve(uniqueCandidates.size());
|
||||||
|
for (const auto& c : uniqueCandidates) {
|
||||||
|
if (!failedTextureCache_.count(c)) {
|
||||||
|
attemptedCandidates.push_back(c);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (attemptedCandidates.empty()) {
|
||||||
|
return whiteTexture_.get();
|
||||||
|
}
|
||||||
|
|
||||||
// Try loading all candidates until one succeeds
|
// Try loading all candidates until one succeeds
|
||||||
pipeline::BLPImage blp;
|
pipeline::BLPImage blp;
|
||||||
std::string resolvedKey;
|
std::string resolvedKey;
|
||||||
for (const auto& c : uniqueCandidates) {
|
for (const auto& c : attemptedCandidates) {
|
||||||
blp = assetManager->loadTexture(c);
|
blp = assetManager->loadTexture(c);
|
||||||
if (blp.isValid()) {
|
if (blp.isValid()) {
|
||||||
resolvedKey = c;
|
resolvedKey = c;
|
||||||
|
|
@ -1874,7 +1923,15 @@ VkTexture* WMORenderer::loadTexture(const std::string& path) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (!blp.isValid()) {
|
if (!blp.isValid()) {
|
||||||
core::Logger::getInstance().warning("WMO: Failed to load texture: ", path);
|
static constexpr size_t kMaxFailedTextureCache = 200000;
|
||||||
|
for (const auto& c : attemptedCandidates) {
|
||||||
|
if (failedTextureCache_.size() < kMaxFailedTextureCache) {
|
||||||
|
failedTextureCache_.insert(c);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (loggedTextureLoadFails_.insert(key).second) {
|
||||||
|
core::Logger::getInstance().warning("WMO: Failed to load texture: ", path);
|
||||||
|
}
|
||||||
// Do not cache failures as white. MPQ reads can fail transiently
|
// Do not cache failures as white. MPQ reads can fail transiently
|
||||||
// during streaming/contention, and caching white here permanently
|
// during streaming/contention, and caching white here permanently
|
||||||
// poisons the texture for this session.
|
// poisons the texture for this session.
|
||||||
|
|
@ -1883,6 +1940,19 @@ VkTexture* WMORenderer::loadTexture(const std::string& path) {
|
||||||
|
|
||||||
core::Logger::getInstance().debug("WMO texture: ", path, " size=", blp.width, "x", blp.height);
|
core::Logger::getInstance().debug("WMO texture: ", path, " size=", blp.width, "x", blp.height);
|
||||||
|
|
||||||
|
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_) {
|
||||||
|
if (textureBudgetRejectWarnings_ < 8 || (textureBudgetRejectWarnings_ % 120) == 0) {
|
||||||
|
core::Logger::getInstance().warning(
|
||||||
|
"WMO texture cache full (", textureCacheBytes_ / (1024 * 1024),
|
||||||
|
" MB / ", textureCacheBudgetBytes_ / (1024 * 1024),
|
||||||
|
" MB), rejecting texture: ", path);
|
||||||
|
}
|
||||||
|
++textureBudgetRejectWarnings_;
|
||||||
|
return whiteTexture_.get();
|
||||||
|
}
|
||||||
|
|
||||||
// Create Vulkan texture
|
// Create Vulkan texture
|
||||||
auto texture = std::make_unique<VkTexture>();
|
auto texture = std::make_unique<VkTexture>();
|
||||||
if (!texture->upload(*vkCtx_, blp.data.data(), blp.width, blp.height,
|
if (!texture->upload(*vkCtx_, blp.data.data(), blp.width, blp.height,
|
||||||
|
|
@ -1896,8 +1966,7 @@ VkTexture* WMORenderer::loadTexture(const std::string& path) {
|
||||||
// Cache it
|
// Cache it
|
||||||
TextureCacheEntry e;
|
TextureCacheEntry e;
|
||||||
VkTexture* rawPtr = texture.get();
|
VkTexture* rawPtr = texture.get();
|
||||||
size_t base = static_cast<size_t>(blp.width) * static_cast<size_t>(blp.height) * 4ull;
|
e.approxBytes = approxBytes;
|
||||||
e.approxBytes = base + (base / 3);
|
|
||||||
e.lastUse = ++textureCacheCounter_;
|
e.lastUse = ++textureCacheCounter_;
|
||||||
e.texture = std::move(texture);
|
e.texture = std::move(texture);
|
||||||
textureCacheBytes_ += e.approxBytes;
|
textureCacheBytes_ += e.approxBytes;
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue