Async humanoid NPC texture pipeline to eliminate 30-150ms main-thread stalls

Move all DBC lookups (CharSections, ItemDisplayInfo), texture path resolution,
and BLP decoding for humanoid NPCs to background threads. Only GPU texture
uploads remain on the main thread via pre-decoded BLP cache.
This commit is contained in:
Kelsi 2026-03-07 16:54:58 -08:00
parent 7ac990cff4
commit faca22ac5f
3 changed files with 703 additions and 327 deletions

View file

@ -220,6 +220,7 @@ private:
std::unordered_set<uint64_t> deadCreatureGuids_; // GUIDs that should spawn in corpse/death pose 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_map<uint32_t, uint32_t> displayIdModelCache_; // displayId → modelId (model caching)
std::unordered_set<uint32_t> displayIdTexturesApplied_; // displayIds with per-model textures applied std::unordered_set<uint32_t> displayIdTexturesApplied_; // displayIds with per-model textures applied
std::unordered_map<uint32_t, std::unordered_map<std::string, pipeline::BLPImage>> displayIdPredecodedTextures_; // displayId → pre-decoded skin textures
mutable std::unordered_set<uint32_t> warnedMissingDisplayDataIds_; // displayIds already warned mutable std::unordered_set<uint32_t> warnedMissingDisplayDataIds_; // displayIds already warned
mutable std::unordered_set<uint32_t> warnedMissingModelPathIds_; // modelIds/displayIds already warned mutable std::unordered_set<uint32_t> warnedMissingModelPathIds_; // modelIds/displayIds already warned
uint32_t nextCreatureModelId_ = 5000; // Model IDs for online creatures uint32_t nextCreatureModelId_ = 5000; // Model IDs for online creatures
@ -312,6 +313,49 @@ private:
// Deferred equipment compositing queue — processes max 1 per frame to avoid stutter // Deferred equipment compositing queue — processes max 1 per frame to avoid stutter
std::vector<std::pair<uint64_t, std::pair<std::array<uint32_t, 19>, std::array<uint8_t, 19>>>> deferredEquipmentQueue_; std::vector<std::pair<uint64_t, std::pair<std::array<uint32_t, 19>, std::array<uint8_t, 19>>>> deferredEquipmentQueue_;
void processDeferredEquipmentQueue(); void processDeferredEquipmentQueue();
// Async equipment texture pre-decode: BLP decode on background thread, composite on main thread
struct PreparedEquipmentUpdate {
uint64_t guid;
std::array<uint32_t, 19> displayInfoIds;
std::array<uint8_t, 19> inventoryTypes;
std::unordered_map<std::string, pipeline::BLPImage> predecodedTextures;
};
struct AsyncEquipmentLoad {
std::future<PreparedEquipmentUpdate> future;
};
std::vector<AsyncEquipmentLoad> asyncEquipmentLoads_;
void processAsyncEquipmentResults();
std::vector<std::string> resolveEquipmentTexturePaths(uint64_t guid,
const std::array<uint32_t, 19>& displayInfoIds,
const std::array<uint8_t, 19>& inventoryTypes) const;
// Deferred NPC texture setup — async DBC lookups + BLP pre-decode to avoid main-thread stalls
struct DeferredNpcComposite {
uint32_t modelId;
uint32_t displayId;
// Skin compositing (type-1 slots)
std::string basePath; // CharSections skin base texture
std::vector<std::string> overlayPaths; // face + underwear overlays
std::vector<std::pair<int, std::string>> regionLayers; // equipment region overlays
std::vector<uint32_t> skinTextureSlots; // model texture slots needing skin composite
bool hasComposite = false; // needs compositing (overlays or equipment regions)
bool hasSimpleSkin = false; // just base skin, no compositing needed
// Baked skin (type-1 slots)
std::string bakedSkinPath; // baked texture path (if available)
bool hasBakedSkin = false; // baked skin resolved successfully
// Hair (type-6 slots)
std::vector<uint32_t> hairTextureSlots; // model texture slots needing hair texture
std::string hairTexturePath; // resolved hair texture path
bool useBakedForHair = false; // bald NPC: use baked skin for type-6
};
struct PreparedNpcComposite {
DeferredNpcComposite info;
std::unordered_map<std::string, pipeline::BLPImage> predecodedTextures;
};
struct AsyncNpcCompositeLoad {
std::future<PreparedNpcComposite> future;
};
std::vector<AsyncNpcCompositeLoad> asyncNpcCompositeLoads_;
void processAsyncNpcCompositeResults();
// Cache base player model geometry by (raceId, genderId) // Cache base player model geometry by (raceId, genderId)
std::unordered_map<uint32_t, uint32_t> playerModelCache_; // key=(race<<8)|gender → modelId std::unordered_map<uint32_t, uint32_t> playerModelCache_; // key=(race<<8)|gender → modelId
struct PlayerTextureSlots { int skin = -1; int hair = -1; int underwear = -1; }; struct PlayerTextureSlots { int skin = -1; int hair = -1; int underwear = -1; };

File diff suppressed because it is too large Load diff

View file

@ -836,7 +836,19 @@ VkTexture* CharacterRenderer::compositeTextures(const std::vector<std::string>&
} }
// Load base layer // Load base layer
auto base = assetManager->loadTexture(layerPaths[0]); pipeline::BLPImage base;
if (predecodedBLPCache_) {
std::string key = layerPaths[0];
std::replace(key.begin(), key.end(), '/', '\\');
std::transform(key.begin(), key.end(), key.begin(),
[](unsigned char c) { return static_cast<char>(std::tolower(c)); });
auto pit = predecodedBLPCache_->find(key);
if (pit != predecodedBLPCache_->end()) {
base = std::move(pit->second);
predecodedBLPCache_->erase(pit);
}
}
if (!base.isValid()) base = assetManager->loadTexture(layerPaths[0]);
if (!base.isValid()) { if (!base.isValid()) {
core::Logger::getInstance().warning("Composite: failed to load base layer: ", layerPaths[0]); core::Logger::getInstance().warning("Composite: failed to load base layer: ", layerPaths[0]);
return whiteTexture_.get(); return whiteTexture_.get();
@ -877,7 +889,19 @@ VkTexture* CharacterRenderer::compositeTextures(const std::vector<std::string>&
for (size_t layer = 1; layer < layerPaths.size(); layer++) { for (size_t layer = 1; layer < layerPaths.size(); layer++) {
if (layerPaths[layer].empty()) continue; if (layerPaths[layer].empty()) continue;
auto overlay = assetManager->loadTexture(layerPaths[layer]); pipeline::BLPImage overlay;
if (predecodedBLPCache_) {
std::string key = layerPaths[layer];
std::replace(key.begin(), key.end(), '/', '\\');
std::transform(key.begin(), key.end(), key.begin(),
[](unsigned char c) { return static_cast<char>(std::tolower(c)); });
auto pit = predecodedBLPCache_->find(key);
if (pit != predecodedBLPCache_->end()) {
overlay = std::move(pit->second);
predecodedBLPCache_->erase(pit);
}
}
if (!overlay.isValid()) overlay = assetManager->loadTexture(layerPaths[layer]);
if (!overlay.isValid()) { if (!overlay.isValid()) {
core::Logger::getInstance().warning("Composite: FAILED to load overlay: ", layerPaths[layer]); core::Logger::getInstance().warning("Composite: FAILED to load overlay: ", layerPaths[layer]);
continue; continue;
@ -1054,7 +1078,19 @@ VkTexture* CharacterRenderer::compositeWithRegions(const std::string& basePath,
return whiteTexture_.get(); return whiteTexture_.get();
} }
auto base = assetManager->loadTexture(basePath); pipeline::BLPImage base;
if (predecodedBLPCache_) {
std::string key = basePath;
std::replace(key.begin(), key.end(), '/', '\\');
std::transform(key.begin(), key.end(), key.begin(),
[](unsigned char c) { return static_cast<char>(std::tolower(c)); });
auto pit = predecodedBLPCache_->find(key);
if (pit != predecodedBLPCache_->end()) {
base = std::move(pit->second);
predecodedBLPCache_->erase(pit);
}
}
if (!base.isValid()) base = assetManager->loadTexture(basePath);
if (!base.isValid()) { if (!base.isValid()) {
return whiteTexture_.get(); return whiteTexture_.get();
} }
@ -1093,7 +1129,19 @@ VkTexture* CharacterRenderer::compositeWithRegions(const std::string& basePath,
bool upscaled = (base.width == 256 && base.height == 256 && width == 512); bool upscaled = (base.width == 256 && base.height == 256 && width == 512);
for (const auto& ul : baseLayers) { for (const auto& ul : baseLayers) {
if (ul.empty()) continue; if (ul.empty()) continue;
auto overlay = assetManager->loadTexture(ul); pipeline::BLPImage overlay;
if (predecodedBLPCache_) {
std::string key = ul;
std::replace(key.begin(), key.end(), '/', '\\');
std::transform(key.begin(), key.end(), key.begin(),
[](unsigned char c) { return static_cast<char>(std::tolower(c)); });
auto pit = predecodedBLPCache_->find(key);
if (pit != predecodedBLPCache_->end()) {
overlay = std::move(pit->second);
predecodedBLPCache_->erase(pit);
}
}
if (!overlay.isValid()) overlay = assetManager->loadTexture(ul);
if (!overlay.isValid()) continue; if (!overlay.isValid()) continue;
if (overlay.width == width && overlay.height == height) { if (overlay.width == width && overlay.height == height) {
@ -1171,7 +1219,19 @@ VkTexture* CharacterRenderer::compositeWithRegions(const std::string& basePath,
int regionIdx = rl.first; int regionIdx = rl.first;
if (regionIdx < 0 || regionIdx >= 8) continue; if (regionIdx < 0 || regionIdx >= 8) continue;
auto overlay = assetManager->loadTexture(rl.second); pipeline::BLPImage overlay;
if (predecodedBLPCache_) {
std::string key = rl.second;
std::replace(key.begin(), key.end(), '/', '\\');
std::transform(key.begin(), key.end(), key.begin(),
[](unsigned char c) { return static_cast<char>(std::tolower(c)); });
auto pit = predecodedBLPCache_->find(key);
if (pit != predecodedBLPCache_->end()) {
overlay = std::move(pit->second);
predecodedBLPCache_->erase(pit);
}
}
if (!overlay.isValid()) overlay = assetManager->loadTexture(rl.second);
if (!overlay.isValid()) { if (!overlay.isValid()) {
core::Logger::getInstance().warning("compositeWithRegions: failed to load ", rl.second); core::Logger::getInstance().warning("compositeWithRegions: failed to load ", rl.second);
continue; continue;