mirror of
https://github.com/Kelsidavis/WoWee.git
synced 2026-03-22 23:30:14 +00:00
Fix city NPC stuttering: async model loading, CharSections cache, frame budgets
- Async creature model loading: M2 file I/O and parsing on background threads via std::async, GPU upload on main thread when ready (MAX_ASYNC_CREATURE_LOADS=4) - CharSections.dbc lookup cache: O(1) hash lookup instead of O(N) full DBC scan per humanoid NPC spawn (was scanning thousands of records twice per spawn) - Frame time budget: 4ms cap on creature spawn processing per frame - Wolf/worg model name check cached per modelId (was doing tolower+find per hostile creature per frame) - Weapon attach throttle: max 2 per 1s tick (was attempting all unweaponized NPCs) - Separate texture application tracking (displayIdTexturesApplied_) so async-loaded models still get skin/equipment textures applied correctly
This commit is contained in:
parent
f374e19239
commit
f9410cc4bd
2 changed files with 361 additions and 102 deletions
|
|
@ -10,6 +10,8 @@
|
|||
#include <unordered_set>
|
||||
#include <array>
|
||||
#include <optional>
|
||||
#include <future>
|
||||
#include <mutex>
|
||||
|
||||
namespace wowee {
|
||||
|
||||
|
|
@ -18,7 +20,7 @@ namespace rendering { class Renderer; }
|
|||
namespace ui { class UIManager; }
|
||||
namespace auth { class AuthHandler; }
|
||||
namespace game { class GameHandler; class World; class ExpansionRegistry; }
|
||||
namespace pipeline { class AssetManager; class DBCLayout; }
|
||||
namespace pipeline { class AssetManager; class DBCLayout; struct M2Model; }
|
||||
namespace audio { enum class VoiceType; }
|
||||
|
||||
namespace core {
|
||||
|
|
@ -90,6 +92,7 @@ private:
|
|||
static const char* mapIdToName(uint32_t mapId);
|
||||
void loadOnlineWorldTerrain(uint32_t mapId, float x, float y, float z);
|
||||
void buildFactionHostilityMap(uint8_t playerRace);
|
||||
pipeline::M2Model loadCreatureM2Sync(const std::string& m2Path);
|
||||
void spawnOnlineCreature(uint64_t guid, uint32_t displayId, float x, float y, float z, float orientation);
|
||||
void despawnOnlineCreature(uint64_t guid);
|
||||
bool tryAttachCreatureVirtualWeapons(uint64_t guid, uint32_t instanceId);
|
||||
|
|
@ -181,8 +184,37 @@ private:
|
|||
std::unordered_map<uint64_t, glm::vec3> creatureRenderPosCache_; // guid -> last synced render position
|
||||
std::unordered_set<uint64_t> creatureWeaponsAttached_; // guid set when NPC virtual weapons attached
|
||||
std::unordered_map<uint64_t, uint8_t> creatureWeaponAttachAttempts_; // guid -> attach attempts
|
||||
std::unordered_map<uint32_t, bool> modelIdIsWolfLike_; // modelId → cached wolf/worg check
|
||||
static constexpr int MAX_WEAPON_ATTACHES_PER_TICK = 2; // limit weapon attach work per 1s tick
|
||||
|
||||
// CharSections.dbc lookup cache to avoid O(N) DBC scan per NPC spawn.
|
||||
// Key: (race<<24)|(sex<<16)|(section<<12)|(variation<<8)|color → texture path
|
||||
std::unordered_map<uint64_t, std::string> charSectionsCache_;
|
||||
bool charSectionsCacheBuilt_ = false;
|
||||
void buildCharSectionsCache();
|
||||
std::string lookupCharSection(uint8_t race, uint8_t sex, uint8_t section,
|
||||
uint8_t variation, uint8_t color, int texIndex = 0) const;
|
||||
|
||||
// Async creature model loading: file I/O + M2 parsing on background thread,
|
||||
// GPU upload + instance creation on main thread.
|
||||
struct PreparedCreatureModel {
|
||||
uint64_t guid;
|
||||
uint32_t displayId;
|
||||
uint32_t modelId;
|
||||
float x, y, z, orientation;
|
||||
std::shared_ptr<pipeline::M2Model> model; // parsed on background thread
|
||||
bool valid = false;
|
||||
bool permanent_failure = false;
|
||||
};
|
||||
struct AsyncCreatureLoad {
|
||||
std::future<PreparedCreatureModel> future;
|
||||
};
|
||||
std::vector<AsyncCreatureLoad> asyncCreatureLoads_;
|
||||
void processAsyncCreatureResults();
|
||||
static constexpr int MAX_ASYNC_CREATURE_LOADS = 4; // concurrent background loads
|
||||
std::unordered_set<uint64_t> deadCreatureGuids_; // GUIDs that should spawn in corpse/death pose
|
||||
std::unordered_map<uint32_t, uint32_t> displayIdModelCache_; // displayId → modelId (model caching)
|
||||
std::unordered_set<uint32_t> displayIdTexturesApplied_; // displayIds with per-model textures applied
|
||||
mutable std::unordered_set<uint32_t> warnedMissingDisplayDataIds_; // displayIds already warned
|
||||
mutable std::unordered_set<uint32_t> warnedMissingModelPathIds_; // modelIds/displayIds already warned
|
||||
uint32_t nextCreatureModelId_ = 5000; // Model IDs for online creatures
|
||||
|
|
|
|||
|
|
@ -734,6 +734,16 @@ void Application::logoutToLogin() {
|
|||
deadCreatureGuids_.clear();
|
||||
nonRenderableCreatureDisplayIds_.clear();
|
||||
creaturePermanentFailureGuids_.clear();
|
||||
modelIdIsWolfLike_.clear();
|
||||
displayIdTexturesApplied_.clear();
|
||||
charSectionsCache_.clear();
|
||||
charSectionsCacheBuilt_ = false;
|
||||
|
||||
// Wait for any in-flight async creature loads before clearing state
|
||||
for (auto& load : asyncCreatureLoads_) {
|
||||
if (load.future.valid()) load.future.wait();
|
||||
}
|
||||
asyncCreatureLoads_.clear();
|
||||
|
||||
// --- Creature spawn queues ---
|
||||
pendingCreatureSpawns_.clear();
|
||||
|
|
@ -1285,6 +1295,7 @@ void Application::update(float deltaTime) {
|
|||
npcWeaponRetryTimer += deltaTime;
|
||||
const bool npcWeaponRetryTick = (npcWeaponRetryTimer >= 1.0f);
|
||||
if (npcWeaponRetryTick) npcWeaponRetryTimer = 0.0f;
|
||||
int weaponAttachesThisTick = 0;
|
||||
glm::vec3 playerPos(0.0f);
|
||||
glm::vec3 playerRenderPos(0.0f);
|
||||
bool havePlayerPos = false;
|
||||
|
|
@ -1304,11 +1315,14 @@ void Application::update(float deltaTime) {
|
|||
auto entity = gameHandler->getEntityManager().getEntity(guid);
|
||||
if (!entity || entity->getType() != game::ObjectType::UNIT) continue;
|
||||
|
||||
if (npcWeaponRetryTick && !creatureWeaponsAttached_.count(guid)) {
|
||||
if (npcWeaponRetryTick &&
|
||||
weaponAttachesThisTick < MAX_WEAPON_ATTACHES_PER_TICK &&
|
||||
!creatureWeaponsAttached_.count(guid)) {
|
||||
uint8_t attempts = 0;
|
||||
auto itAttempts = creatureWeaponAttachAttempts_.find(guid);
|
||||
if (itAttempts != creatureWeaponAttachAttempts_.end()) attempts = itAttempts->second;
|
||||
if (attempts < 30) {
|
||||
weaponAttachesThisTick++;
|
||||
if (tryAttachCreatureVirtualWeapons(guid, instanceId)) {
|
||||
creatureWeaponsAttached_.insert(guid);
|
||||
creatureWeaponAttachAttempts_.erase(guid);
|
||||
|
|
@ -1355,14 +1369,21 @@ void Application::update(float deltaTime) {
|
|||
// often put head/torso inside the player capsule).
|
||||
auto mit = creatureModelIds_.find(guid);
|
||||
if (mit != creatureModelIds_.end()) {
|
||||
if (const auto* md = charRenderer->getModelData(mit->second)) {
|
||||
std::string modelName = md->name;
|
||||
std::transform(modelName.begin(), modelName.end(), modelName.begin(),
|
||||
[](unsigned char c) { return static_cast<char>(std::tolower(c)); });
|
||||
if (modelName.find("wolf") != std::string::npos ||
|
||||
modelName.find("worg") != std::string::npos) {
|
||||
minSep = std::max(minSep, 2.45f);
|
||||
uint32_t mid = mit->second;
|
||||
auto wolfIt = modelIdIsWolfLike_.find(mid);
|
||||
if (wolfIt == modelIdIsWolfLike_.end()) {
|
||||
bool isWolf = false;
|
||||
if (const auto* md = charRenderer->getModelData(mid)) {
|
||||
std::string modelName = md->name;
|
||||
std::transform(modelName.begin(), modelName.end(), modelName.begin(),
|
||||
[](unsigned char c) { return static_cast<char>(std::tolower(c)); });
|
||||
isWolf = (modelName.find("wolf") != std::string::npos ||
|
||||
modelName.find("worg") != std::string::npos);
|
||||
}
|
||||
wolfIt = modelIdIsWolfLike_.emplace(mid, isWolf).first;
|
||||
}
|
||||
if (wolfIt->second) {
|
||||
minSep = std::max(minSep, 2.45f);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -3465,6 +3486,14 @@ void Application::loadOnlineWorldTerrain(uint32_t mapId, float x, float y, float
|
|||
deadCreatureGuids_.clear();
|
||||
nonRenderableCreatureDisplayIds_.clear();
|
||||
creaturePermanentFailureGuids_.clear();
|
||||
modelIdIsWolfLike_.clear();
|
||||
displayIdTexturesApplied_.clear();
|
||||
charSectionsCache_.clear();
|
||||
charSectionsCacheBuilt_ = false;
|
||||
for (auto& load : asyncCreatureLoads_) {
|
||||
if (load.future.valid()) load.future.wait();
|
||||
}
|
||||
asyncCreatureLoads_.clear();
|
||||
|
||||
playerInstances_.clear();
|
||||
onlinePlayerAppearance_.clear();
|
||||
|
|
@ -4140,6 +4169,55 @@ void Application::loadOnlineWorldTerrain(uint32_t mapId, float x, float y, float
|
|||
setState(AppState::IN_GAME);
|
||||
}
|
||||
|
||||
void Application::buildCharSectionsCache() {
|
||||
if (charSectionsCacheBuilt_ || !assetManager || !assetManager->isInitialized()) return;
|
||||
auto dbc = assetManager->loadDBC("CharSections.dbc");
|
||||
if (!dbc) return;
|
||||
const auto* csL = pipeline::getActiveDBCLayout()
|
||||
? pipeline::getActiveDBCLayout()->getLayout("CharSections") : nullptr;
|
||||
uint32_t raceF = csL ? (*csL)["RaceID"] : 1;
|
||||
uint32_t sexF = csL ? (*csL)["SexID"] : 2;
|
||||
uint32_t secF = csL ? (*csL)["BaseSection"] : 3;
|
||||
uint32_t varF = csL ? (*csL)["VariationIndex"] : 4;
|
||||
uint32_t colF = csL ? (*csL)["ColorIndex"] : 5;
|
||||
uint32_t tex1F = csL ? (*csL)["Texture1"] : 6;
|
||||
for (uint32_t r = 0; r < dbc->getRecordCount(); r++) {
|
||||
uint32_t race = dbc->getUInt32(r, raceF);
|
||||
uint32_t sex = dbc->getUInt32(r, sexF);
|
||||
uint32_t section = dbc->getUInt32(r, secF);
|
||||
uint32_t variation = dbc->getUInt32(r, varF);
|
||||
uint32_t color = dbc->getUInt32(r, colF);
|
||||
// We only cache sections 0 (skin), 1 (face), 3 (hair), 4 (underwear)
|
||||
if (section != 0 && section != 1 && section != 3 && section != 4) continue;
|
||||
for (int ti = 0; ti < 3; ti++) {
|
||||
std::string tex = dbc->getString(r, tex1F + ti);
|
||||
if (tex.empty()) continue;
|
||||
// Key: race(8)|sex(4)|section(4)|variation(8)|color(8)|texIndex(2) packed into 64 bits
|
||||
uint64_t key = (static_cast<uint64_t>(race) << 26) |
|
||||
(static_cast<uint64_t>(sex & 0xF) << 22) |
|
||||
(static_cast<uint64_t>(section & 0xF) << 18) |
|
||||
(static_cast<uint64_t>(variation & 0xFF) << 10) |
|
||||
(static_cast<uint64_t>(color & 0xFF) << 2) |
|
||||
static_cast<uint64_t>(ti);
|
||||
charSectionsCache_.emplace(key, tex);
|
||||
}
|
||||
}
|
||||
charSectionsCacheBuilt_ = true;
|
||||
LOG_INFO("CharSections cache built: ", charSectionsCache_.size(), " entries");
|
||||
}
|
||||
|
||||
std::string Application::lookupCharSection(uint8_t race, uint8_t sex, uint8_t section,
|
||||
uint8_t variation, uint8_t color, int texIndex) const {
|
||||
uint64_t key = (static_cast<uint64_t>(race) << 26) |
|
||||
(static_cast<uint64_t>(sex & 0xF) << 22) |
|
||||
(static_cast<uint64_t>(section & 0xF) << 18) |
|
||||
(static_cast<uint64_t>(variation & 0xFF) << 10) |
|
||||
(static_cast<uint64_t>(color & 0xFF) << 2) |
|
||||
static_cast<uint64_t>(texIndex);
|
||||
auto it = charSectionsCache_.find(key);
|
||||
return (it != charSectionsCache_.end()) ? it->second : std::string();
|
||||
}
|
||||
|
||||
void Application::buildCreatureDisplayLookups() {
|
||||
if (creatureLookupsBuilt_ || !assetManager || !assetManager->isInitialized()) return;
|
||||
|
||||
|
|
@ -4479,6 +4557,47 @@ bool Application::getRenderFootZForGuid(uint64_t guid, float& outFootZ) const {
|
|||
return renderer->getCharacterRenderer()->getInstanceFootZ(instanceId, outFootZ);
|
||||
}
|
||||
|
||||
pipeline::M2Model Application::loadCreatureM2Sync(const std::string& m2Path) {
|
||||
auto m2Data = assetManager->readFile(m2Path);
|
||||
if (m2Data.empty()) {
|
||||
LOG_WARNING("Failed to read creature M2: ", m2Path);
|
||||
return {};
|
||||
}
|
||||
|
||||
pipeline::M2Model model = pipeline::M2Loader::load(m2Data);
|
||||
if (model.vertices.empty()) {
|
||||
LOG_WARNING("Failed to parse creature M2: ", m2Path);
|
||||
return {};
|
||||
}
|
||||
|
||||
// Load skin file (only for WotLK M2s - vanilla has embedded skin)
|
||||
if (model.version >= 264) {
|
||||
std::string skinPath = m2Path.substr(0, m2Path.size() - 3) + "00.skin";
|
||||
auto skinData = assetManager->readFile(skinPath);
|
||||
if (!skinData.empty()) {
|
||||
pipeline::M2Loader::loadSkin(skinData, model);
|
||||
} else {
|
||||
LOG_WARNING("Missing skin file for WotLK creature M2: ", skinPath);
|
||||
}
|
||||
}
|
||||
|
||||
// Load external .anim files for sequences without flag 0x20
|
||||
std::string basePath = m2Path.substr(0, m2Path.size() - 3);
|
||||
for (uint32_t si = 0; si < model.sequences.size(); si++) {
|
||||
if (!(model.sequences[si].flags & 0x20)) {
|
||||
char animFileName[256];
|
||||
snprintf(animFileName, sizeof(animFileName), "%s%04u-%02u.anim",
|
||||
basePath.c_str(), model.sequences[si].id, model.sequences[si].variationIndex);
|
||||
auto animData = assetManager->readFileOptional(animFileName);
|
||||
if (!animData.empty()) {
|
||||
pipeline::M2Loader::loadAnimFile(m2Data, animData, si, model);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return model;
|
||||
}
|
||||
|
||||
void Application::spawnOnlineCreature(uint64_t guid, uint32_t displayId, float x, float y, float z, float orientation) {
|
||||
if (!renderer || !renderer->getCharacterRenderer() || !assetManager) return;
|
||||
|
||||
|
|
@ -4525,47 +4644,13 @@ void Application::spawnOnlineCreature(uint64_t guid, uint32_t displayId, float x
|
|||
// Load model from disk (only once per displayId)
|
||||
modelId = nextCreatureModelId_++;
|
||||
|
||||
auto m2Data = assetManager->readFile(m2Path);
|
||||
if (m2Data.empty()) {
|
||||
LOG_WARNING("Failed to read creature M2: ", m2Path);
|
||||
pipeline::M2Model model = loadCreatureM2Sync(m2Path);
|
||||
if (!model.isValid()) {
|
||||
nonRenderableCreatureDisplayIds_.insert(displayId);
|
||||
creaturePermanentFailureGuids_.insert(guid);
|
||||
return;
|
||||
}
|
||||
|
||||
pipeline::M2Model model = pipeline::M2Loader::load(m2Data);
|
||||
if (model.vertices.empty()) {
|
||||
LOG_WARNING("Failed to parse creature M2: ", m2Path);
|
||||
nonRenderableCreatureDisplayIds_.insert(displayId);
|
||||
creaturePermanentFailureGuids_.insert(guid);
|
||||
return;
|
||||
}
|
||||
|
||||
// Load skin file (only for WotLK M2s - vanilla has embedded skin)
|
||||
if (model.version >= 264) {
|
||||
std::string skinPath = m2Path.substr(0, m2Path.size() - 3) + "00.skin";
|
||||
auto skinData = assetManager->readFile(skinPath);
|
||||
if (!skinData.empty()) {
|
||||
pipeline::M2Loader::loadSkin(skinData, model);
|
||||
} else {
|
||||
LOG_WARNING("Missing skin file for WotLK creature M2: ", skinPath);
|
||||
}
|
||||
}
|
||||
|
||||
// Load external .anim files for sequences without flag 0x20
|
||||
std::string basePath = m2Path.substr(0, m2Path.size() - 3);
|
||||
for (uint32_t si = 0; si < model.sequences.size(); si++) {
|
||||
if (!(model.sequences[si].flags & 0x20)) {
|
||||
char animFileName[256];
|
||||
snprintf(animFileName, sizeof(animFileName), "%s%04u-%02u.anim",
|
||||
basePath.c_str(), model.sequences[si].id, model.sequences[si].variationIndex);
|
||||
auto animData = assetManager->readFileOptional(animFileName);
|
||||
if (!animData.empty()) {
|
||||
pipeline::M2Loader::loadAnimFile(m2Data, animData, si, model);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!charRenderer->loadModel(model, modelId)) {
|
||||
LOG_WARNING("Failed to load creature model: ", m2Path);
|
||||
nonRenderableCreatureDisplayIds_.insert(displayId);
|
||||
|
|
@ -4576,9 +4661,13 @@ void Application::spawnOnlineCreature(uint64_t guid, uint32_t displayId, float x
|
|||
displayIdModelCache_[displayId] = modelId;
|
||||
}
|
||||
|
||||
// Apply skin textures from CreatureDisplayInfo.dbc (only for newly loaded models)
|
||||
// Apply skin textures from CreatureDisplayInfo.dbc (only once per displayId model).
|
||||
// Track separately from model cache because async loading may upload the model
|
||||
// before textures are applied.
|
||||
auto itDisplayData = displayDataMap_.find(displayId);
|
||||
if (!modelCached && itDisplayData != displayDataMap_.end()) {
|
||||
bool needsTextures = (displayIdTexturesApplied_.find(displayId) == displayIdTexturesApplied_.end());
|
||||
if (needsTextures && itDisplayData != displayDataMap_.end()) {
|
||||
displayIdTexturesApplied_.insert(displayId);
|
||||
const auto& dispData = itDisplayData->second;
|
||||
|
||||
// Get model directory for texture path construction
|
||||
|
|
@ -5058,7 +5147,9 @@ void Application::spawnOnlineCreature(uint64_t guid, uint32_t displayId, float x
|
|||
|
||||
// Per-instance hair/skin texture overrides — runs for ALL NPCs (including cached models)
|
||||
// so that each NPC gets its own hair/skin color regardless of model sharing.
|
||||
// Uses pre-built CharSections cache (O(1) lookup instead of O(N) DBC scan).
|
||||
{
|
||||
if (!charSectionsCacheBuilt_) buildCharSectionsCache();
|
||||
auto itDD = displayDataMap_.find(displayId);
|
||||
if (itDD != displayDataMap_.end() && itDD->second.extraDisplayId != 0) {
|
||||
auto itExtra2 = humanoidExtraMap_.find(itDD->second.extraDisplayId);
|
||||
|
|
@ -5066,37 +5157,19 @@ void Application::spawnOnlineCreature(uint64_t guid, uint32_t displayId, float x
|
|||
const auto& extra = itExtra2->second;
|
||||
const auto* md = charRenderer->getModelData(modelId);
|
||||
if (md) {
|
||||
auto charSectionsDbc2 = assetManager->loadDBC("CharSections.dbc");
|
||||
if (charSectionsDbc2) {
|
||||
const auto* csL = pipeline::getActiveDBCLayout()
|
||||
? pipeline::getActiveDBCLayout()->getLayout("CharSections") : nullptr;
|
||||
uint32_t tgtRace = static_cast<uint32_t>(extra.raceId);
|
||||
uint32_t tgtSex = static_cast<uint32_t>(extra.sexId);
|
||||
|
||||
// Look up hair texture (section 3)
|
||||
// Look up hair texture (section 3) via cache
|
||||
rendering::VkTexture* whiteTex = charRenderer->loadTexture("");
|
||||
for (uint32_t r = 0; r < charSectionsDbc2->getRecordCount(); r++) {
|
||||
uint32_t rId = charSectionsDbc2->getUInt32(r, csL ? (*csL)["RaceID"] : 1);
|
||||
uint32_t sId = charSectionsDbc2->getUInt32(r, csL ? (*csL)["SexID"] : 2);
|
||||
if (rId != tgtRace || sId != tgtSex) continue;
|
||||
uint32_t sec = charSectionsDbc2->getUInt32(r, csL ? (*csL)["BaseSection"] : 3);
|
||||
if (sec != 3) continue;
|
||||
uint32_t var = charSectionsDbc2->getUInt32(r, csL ? (*csL)["VariationIndex"] : 4);
|
||||
uint32_t col = charSectionsDbc2->getUInt32(r, csL ? (*csL)["ColorIndex"] : 5);
|
||||
if (var != static_cast<uint32_t>(extra.hairStyleId)) continue;
|
||||
if (col != static_cast<uint32_t>(extra.hairColorId)) continue;
|
||||
std::string hairPath = charSectionsDbc2->getString(r, csL ? (*csL)["Texture1"] : 6);
|
||||
if (!hairPath.empty()) {
|
||||
rendering::VkTexture* hairTex = charRenderer->loadTexture(hairPath);
|
||||
if (hairTex && hairTex != whiteTex) {
|
||||
for (size_t ti = 0; ti < md->textures.size(); ti++) {
|
||||
if (md->textures[ti].type == 6) {
|
||||
charRenderer->setTextureSlotOverride(instanceId, static_cast<uint16_t>(ti), hairTex);
|
||||
}
|
||||
std::string hairPath = lookupCharSection(
|
||||
extra.raceId, extra.sexId, 3, extra.hairStyleId, extra.hairColorId, 0);
|
||||
if (!hairPath.empty()) {
|
||||
rendering::VkTexture* hairTex = charRenderer->loadTexture(hairPath);
|
||||
if (hairTex && hairTex != whiteTex) {
|
||||
for (size_t ti = 0; ti < md->textures.size(); ti++) {
|
||||
if (md->textures[ti].type == 6) {
|
||||
charRenderer->setTextureSlotOverride(instanceId, static_cast<uint16_t>(ti), hairTex);
|
||||
}
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
// Look up skin texture (section 0) for per-instance skin color.
|
||||
|
|
@ -5108,30 +5181,20 @@ void Application::spawnOnlineCreature(uint64_t guid, uint32_t displayId, float x
|
|||
if (extra.equipDisplayId[s] != 0) hasEquipOrBake = true;
|
||||
}
|
||||
if (!hasEquipOrBake) {
|
||||
for (uint32_t r = 0; r < charSectionsDbc2->getRecordCount(); r++) {
|
||||
uint32_t rId = charSectionsDbc2->getUInt32(r, csL ? (*csL)["RaceID"] : 1);
|
||||
uint32_t sId = charSectionsDbc2->getUInt32(r, csL ? (*csL)["SexID"] : 2);
|
||||
if (rId != tgtRace || sId != tgtSex) continue;
|
||||
uint32_t sec = charSectionsDbc2->getUInt32(r, csL ? (*csL)["BaseSection"] : 3);
|
||||
if (sec != 0) continue;
|
||||
uint32_t col = charSectionsDbc2->getUInt32(r, csL ? (*csL)["ColorIndex"] : 5);
|
||||
if (col != static_cast<uint32_t>(extra.skinId)) continue;
|
||||
std::string skinPath = charSectionsDbc2->getString(r, csL ? (*csL)["Texture1"] : 6);
|
||||
if (!skinPath.empty()) {
|
||||
rendering::VkTexture* skinTex = charRenderer->loadTexture(skinPath);
|
||||
if (skinTex) {
|
||||
for (size_t ti = 0; ti < md->textures.size(); ti++) {
|
||||
uint32_t tt = md->textures[ti].type;
|
||||
if (tt == 1 || tt == 11) {
|
||||
charRenderer->setTextureSlotOverride(instanceId, static_cast<uint16_t>(ti), skinTex);
|
||||
}
|
||||
std::string skinPath = lookupCharSection(
|
||||
extra.raceId, extra.sexId, 0, 0, extra.skinId, 0);
|
||||
if (!skinPath.empty()) {
|
||||
rendering::VkTexture* skinTex = charRenderer->loadTexture(skinPath);
|
||||
if (skinTex) {
|
||||
for (size_t ti = 0; ti < md->textures.size(); ti++) {
|
||||
uint32_t tt = md->textures[ti].type;
|
||||
if (tt == 1 || tt == 11) {
|
||||
charRenderer->setTextureSlotOverride(instanceId, static_cast<uint16_t>(ti), skinTex);
|
||||
}
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -6692,19 +6755,94 @@ void Application::spawnOnlineGameObject(uint64_t guid, uint32_t entry, uint32_t
|
|||
" displayId=", displayId, " at (", x, ", ", y, ", ", z, ")");
|
||||
}
|
||||
|
||||
void Application::processAsyncCreatureResults() {
|
||||
// Check completed async model loads and finalize on main thread (GPU upload + instance creation).
|
||||
for (auto it = asyncCreatureLoads_.begin(); it != asyncCreatureLoads_.end(); ) {
|
||||
if (!it->future.valid() ||
|
||||
it->future.wait_for(std::chrono::milliseconds(0)) != std::future_status::ready) {
|
||||
++it;
|
||||
continue;
|
||||
}
|
||||
auto result = it->future.get();
|
||||
it = asyncCreatureLoads_.erase(it);
|
||||
|
||||
if (result.permanent_failure) {
|
||||
nonRenderableCreatureDisplayIds_.insert(result.displayId);
|
||||
creaturePermanentFailureGuids_.insert(result.guid);
|
||||
pendingCreatureSpawnGuids_.erase(result.guid);
|
||||
creatureSpawnRetryCounts_.erase(result.guid);
|
||||
continue;
|
||||
}
|
||||
if (!result.valid || !result.model) {
|
||||
pendingCreatureSpawnGuids_.erase(result.guid);
|
||||
creatureSpawnRetryCounts_.erase(result.guid);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Model parsed on background thread — upload to GPU on main thread.
|
||||
auto* charRenderer = renderer ? renderer->getCharacterRenderer() : nullptr;
|
||||
if (!charRenderer) {
|
||||
pendingCreatureSpawnGuids_.erase(result.guid);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Upload model to GPU (must happen on main thread)
|
||||
if (!charRenderer->loadModel(*result.model, result.modelId)) {
|
||||
nonRenderableCreatureDisplayIds_.insert(result.displayId);
|
||||
creaturePermanentFailureGuids_.insert(result.guid);
|
||||
pendingCreatureSpawnGuids_.erase(result.guid);
|
||||
creatureSpawnRetryCounts_.erase(result.guid);
|
||||
continue;
|
||||
}
|
||||
displayIdModelCache_[result.displayId] = result.modelId;
|
||||
|
||||
pendingCreatureSpawnGuids_.erase(result.guid);
|
||||
creatureSpawnRetryCounts_.erase(result.guid);
|
||||
|
||||
// Re-queue as a normal pending spawn — model is now cached, so sync spawn is fast
|
||||
// (only creates instance + applies textures, no file I/O).
|
||||
if (!creatureInstances_.count(result.guid) &&
|
||||
!creaturePermanentFailureGuids_.count(result.guid)) {
|
||||
PendingCreatureSpawn s{};
|
||||
s.guid = result.guid;
|
||||
s.displayId = result.displayId;
|
||||
s.x = result.x;
|
||||
s.y = result.y;
|
||||
s.z = result.z;
|
||||
s.orientation = result.orientation;
|
||||
pendingCreatureSpawns_.push_back(s);
|
||||
pendingCreatureSpawnGuids_.insert(result.guid);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void Application::processCreatureSpawnQueue() {
|
||||
// First, finalize any async model loads that completed on background threads.
|
||||
processAsyncCreatureResults();
|
||||
|
||||
if (pendingCreatureSpawns_.empty()) return;
|
||||
if (!creatureLookupsBuilt_) {
|
||||
buildCreatureDisplayLookups();
|
||||
if (!creatureLookupsBuilt_) return;
|
||||
}
|
||||
|
||||
auto startTime = std::chrono::steady_clock::now();
|
||||
// Budget: max 4ms per frame for creature spawning to prevent stutter.
|
||||
static constexpr float kSpawnBudgetMs = 4.0f;
|
||||
|
||||
int processed = 0;
|
||||
int newModelLoads = 0;
|
||||
int asyncLaunched = 0;
|
||||
size_t rotationsLeft = pendingCreatureSpawns_.size();
|
||||
while (!pendingCreatureSpawns_.empty() &&
|
||||
processed < MAX_SPAWNS_PER_FRAME &&
|
||||
rotationsLeft > 0) {
|
||||
// Check time budget after each spawn (not for the first one, always process at least 1)
|
||||
if (processed > 0) {
|
||||
auto now = std::chrono::steady_clock::now();
|
||||
float elapsedMs = std::chrono::duration<float, std::milli>(now - startTime).count();
|
||||
if (elapsedMs >= kSpawnBudgetMs) break;
|
||||
}
|
||||
|
||||
PendingCreatureSpawn s = pendingCreatureSpawns_.front();
|
||||
pendingCreatureSpawns_.erase(pendingCreatureSpawns_.begin());
|
||||
|
||||
|
|
@ -6717,14 +6855,106 @@ void Application::processCreatureSpawnQueue() {
|
|||
}
|
||||
|
||||
const bool needsNewModel = (displayIdModelCache_.find(s.displayId) == displayIdModelCache_.end());
|
||||
if (needsNewModel && newModelLoads >= MAX_NEW_CREATURE_MODELS_PER_FRAME) {
|
||||
// Defer additional first-time model/texture loads to later frames so
|
||||
// movement stays responsive in dense areas.
|
||||
pendingCreatureSpawns_.push_back(s);
|
||||
rotationsLeft--;
|
||||
|
||||
// For new models: launch async load on background thread instead of blocking.
|
||||
if (needsNewModel) {
|
||||
if (static_cast<int>(asyncCreatureLoads_.size()) + asyncLaunched >= MAX_ASYNC_CREATURE_LOADS) {
|
||||
// Too many in-flight — defer to next frame
|
||||
pendingCreatureSpawns_.push_back(s);
|
||||
rotationsLeft--;
|
||||
continue;
|
||||
}
|
||||
|
||||
std::string m2Path = getModelPathForDisplayId(s.displayId);
|
||||
if (m2Path.empty()) {
|
||||
nonRenderableCreatureDisplayIds_.insert(s.displayId);
|
||||
creaturePermanentFailureGuids_.insert(s.guid);
|
||||
pendingCreatureSpawnGuids_.erase(s.guid);
|
||||
creatureSpawnRetryCounts_.erase(s.guid);
|
||||
processed++;
|
||||
rotationsLeft = pendingCreatureSpawns_.size();
|
||||
continue;
|
||||
}
|
||||
|
||||
// Check for invisible stalkers
|
||||
{
|
||||
std::string lowerPath = m2Path;
|
||||
std::transform(lowerPath.begin(), lowerPath.end(), lowerPath.begin(),
|
||||
[](unsigned char c) { return static_cast<char>(std::tolower(c)); });
|
||||
if (lowerPath.find("invisiblestalker") != std::string::npos ||
|
||||
lowerPath.find("invisible_stalker") != std::string::npos) {
|
||||
nonRenderableCreatureDisplayIds_.insert(s.displayId);
|
||||
creaturePermanentFailureGuids_.insert(s.guid);
|
||||
pendingCreatureSpawnGuids_.erase(s.guid);
|
||||
processed++;
|
||||
rotationsLeft = pendingCreatureSpawns_.size();
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
// Launch async M2 load — file I/O and parsing happen off the main thread.
|
||||
uint32_t modelId = nextCreatureModelId_++;
|
||||
auto* am = assetManager.get();
|
||||
AsyncCreatureLoad load;
|
||||
load.future = std::async(std::launch::async,
|
||||
[am, m2Path, modelId, s]() -> PreparedCreatureModel {
|
||||
PreparedCreatureModel result;
|
||||
result.guid = s.guid;
|
||||
result.displayId = s.displayId;
|
||||
result.modelId = modelId;
|
||||
result.x = s.x;
|
||||
result.y = s.y;
|
||||
result.z = s.z;
|
||||
result.orientation = s.orientation;
|
||||
|
||||
auto m2Data = am->readFile(m2Path);
|
||||
if (m2Data.empty()) {
|
||||
result.permanent_failure = true;
|
||||
return result;
|
||||
}
|
||||
|
||||
auto model = std::make_shared<pipeline::M2Model>(pipeline::M2Loader::load(m2Data));
|
||||
if (model->vertices.empty()) {
|
||||
result.permanent_failure = true;
|
||||
return result;
|
||||
}
|
||||
|
||||
// Load skin file
|
||||
if (model->version >= 264) {
|
||||
std::string skinPath = m2Path.substr(0, m2Path.size() - 3) + "00.skin";
|
||||
auto skinData = am->readFile(skinPath);
|
||||
if (!skinData.empty()) {
|
||||
pipeline::M2Loader::loadSkin(skinData, *model);
|
||||
}
|
||||
}
|
||||
|
||||
// Load external .anim files
|
||||
std::string basePath = m2Path.substr(0, m2Path.size() - 3);
|
||||
for (uint32_t si = 0; si < model->sequences.size(); si++) {
|
||||
if (!(model->sequences[si].flags & 0x20)) {
|
||||
char animFileName[256];
|
||||
snprintf(animFileName, sizeof(animFileName), "%s%04u-%02u.anim",
|
||||
basePath.c_str(), model->sequences[si].id, model->sequences[si].variationIndex);
|
||||
auto animData = am->readFileOptional(animFileName);
|
||||
if (!animData.empty()) {
|
||||
pipeline::M2Loader::loadAnimFile(m2Data, animData, si, *model);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
result.model = std::move(model);
|
||||
result.valid = true;
|
||||
return result;
|
||||
});
|
||||
asyncCreatureLoads_.push_back(std::move(load));
|
||||
asyncLaunched++;
|
||||
// Don't erase from pendingCreatureSpawnGuids_ — the async result handler will do it
|
||||
rotationsLeft = pendingCreatureSpawns_.size();
|
||||
processed++;
|
||||
continue;
|
||||
}
|
||||
|
||||
// Cached model — spawn is fast (no file I/O, just instance creation + texture setup)
|
||||
spawnOnlineCreature(s.guid, s.displayId, s.x, s.y, s.z, s.orientation);
|
||||
pendingCreatureSpawnGuids_.erase(s.guid);
|
||||
|
||||
|
|
@ -6752,9 +6982,6 @@ void Application::processCreatureSpawnQueue() {
|
|||
} else {
|
||||
creatureSpawnRetryCounts_.erase(s.guid);
|
||||
}
|
||||
if (needsNewModel) {
|
||||
newModelLoads++;
|
||||
}
|
||||
rotationsLeft = pendingCreatureSpawns_.size();
|
||||
processed++;
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue