diff --git a/include/core/application.hpp b/include/core/application.hpp index c97bfaf6..84b89f32 100644 --- a/include/core/application.hpp +++ b/include/core/application.hpp @@ -220,6 +220,7 @@ private: std::unordered_set deadCreatureGuids_; // GUIDs that should spawn in corpse/death pose std::unordered_map displayIdModelCache_; // displayId → modelId (model caching) std::unordered_set displayIdTexturesApplied_; // displayIds with per-model textures applied + std::unordered_map> displayIdPredecodedTextures_; // displayId → pre-decoded skin textures mutable std::unordered_set warnedMissingDisplayDataIds_; // displayIds already warned mutable std::unordered_set warnedMissingModelPathIds_; // modelIds/displayIds already warned 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 std::vector, std::array>>> deferredEquipmentQueue_; void processDeferredEquipmentQueue(); + // Async equipment texture pre-decode: BLP decode on background thread, composite on main thread + struct PreparedEquipmentUpdate { + uint64_t guid; + std::array displayInfoIds; + std::array inventoryTypes; + std::unordered_map predecodedTextures; + }; + struct AsyncEquipmentLoad { + std::future future; + }; + std::vector asyncEquipmentLoads_; + void processAsyncEquipmentResults(); + std::vector resolveEquipmentTexturePaths(uint64_t guid, + const std::array& displayInfoIds, + const std::array& 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 overlayPaths; // face + underwear overlays + std::vector> regionLayers; // equipment region overlays + std::vector 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 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 predecodedTextures; + }; + struct AsyncNpcCompositeLoad { + std::future future; + }; + std::vector asyncNpcCompositeLoads_; + void processAsyncNpcCompositeResults(); // Cache base player model geometry by (raceId, genderId) std::unordered_map playerModelCache_; // key=(race<<8)|gender → modelId struct PlayerTextureSlots { int skin = -1; int hair = -1; int underwear = -1; }; diff --git a/src/core/application.cpp b/src/core/application.cpp index f4712613..b003af53 100644 --- a/src/core/application.cpp +++ b/src/core/application.cpp @@ -913,11 +913,24 @@ void Application::update(float deltaTime) { inGameStep = "spawn/equipment queues"; updateCheckpoint = "in_game: spawn/equipment queues"; runInGameStage("spawn/equipment queues", [&] { + auto t0 = std::chrono::steady_clock::now(); processPlayerSpawnQueue(); - // Process deferred online creature spawns (throttled) + auto t1 = std::chrono::steady_clock::now(); processCreatureSpawnQueue(); - // Process deferred equipment compositing (max 1 per frame to avoid stutter) + auto t2 = std::chrono::steady_clock::now(); + processAsyncNpcCompositeResults(); + auto t3 = std::chrono::steady_clock::now(); processDeferredEquipmentQueue(); + auto t4 = std::chrono::steady_clock::now(); + float pMs = std::chrono::duration(t1 - t0).count(); + float cMs = std::chrono::duration(t2 - t1).count(); + float nMs = std::chrono::duration(t3 - t2).count(); + float eMs = std::chrono::duration(t4 - t3).count(); + float total = pMs + cMs + nMs + eMs; + if (total > 4.0f) { + LOG_WARNING("spawn/equip breakdown: player=", pMs, "ms creature=", cMs, + "ms npcComposite=", nMs, "ms equip=", eMs, "ms"); + } }); // Self-heal missing creature visuals: if a nearby UNIT exists in // entity state but has no render instance, queue a spawn retry. @@ -4235,6 +4248,7 @@ void Application::loadOnlineWorldTerrain(uint32_t mapId, float x, float y, float } } processCreatureSpawnQueue(); + processAsyncNpcCompositeResults(); processDeferredEquipmentQueue(); // Process ALL pending game object spawns (no 1-per-frame cap during load screen). @@ -4792,9 +4806,17 @@ void Application::spawnOnlineCreature(uint64_t guid, uint32_t displayId, float x auto itDisplayData = displayDataMap_.find(displayId); bool needsTextures = (displayIdTexturesApplied_.find(displayId) == displayIdTexturesApplied_.end()); if (needsTextures && itDisplayData != displayDataMap_.end()) { + auto texStart = std::chrono::steady_clock::now(); displayIdTexturesApplied_.insert(displayId); const auto& dispData = itDisplayData->second; + // Use pre-decoded textures from async creature load (if available) + auto itPreDec = displayIdPredecodedTextures_.find(displayId); + bool hasPreDec = (itPreDec != displayIdPredecodedTextures_.end()); + if (hasPreDec) { + charRenderer->setPredecodedBLPCache(&itPreDec->second); + } + // Get model directory for texture path construction std::string modelDir; size_t lastSlash = m2Path.find_last_of("\\/"); @@ -4827,336 +4849,217 @@ void Application::spawnOnlineCreature(uint64_t guid, uint32_t displayId, float x LOG_DEBUG(" Found humanoid extra: raceId=", (int)extra.raceId, " sexId=", (int)extra.sexId, " hairStyle=", (int)extra.hairStyleId, " hairColor=", (int)extra.hairColorId, " bakeName='", extra.bakeName, "'"); - LOG_DEBUG("NPC equip: chest=", extra.equipDisplayId[3], - " legs=", extra.equipDisplayId[5], - " feet=", extra.equipDisplayId[6], - " hands=", extra.equipDisplayId[8], - " bake='", extra.bakeName, "'"); - // Build equipment texture region layers from NPC equipment display IDs - // (texture-only compositing — no geoset changes to avoid invisibility bugs) - std::vector> npcRegionLayers; - std::string npcCapeTexturePath; - auto npcItemDisplayDbc = assetManager->loadDBC("ItemDisplayInfo.dbc"); - if (npcItemDisplayDbc) { - static const char* npcComponentDirs[] = { - "ArmUpperTexture", "ArmLowerTexture", "HandTexture", - "TorsoUpperTexture", "TorsoLowerTexture", - "LegUpperTexture", "LegLowerTexture", "FootTexture", - }; - const auto* idiL = pipeline::getActiveDBCLayout() - ? pipeline::getActiveDBCLayout()->getLayout("ItemDisplayInfo") : nullptr; - // Texture component region fields (8 regions: ArmUpper..Foot) - // Binary DBC (23 fields) has textures at 14+ - const uint32_t texRegionFields[8] = { - idiL ? (*idiL)["TextureArmUpper"] : 14u, - idiL ? (*idiL)["TextureArmLower"] : 15u, - idiL ? (*idiL)["TextureHand"] : 16u, - idiL ? (*idiL)["TextureTorsoUpper"]: 17u, - idiL ? (*idiL)["TextureTorsoLower"]: 18u, - idiL ? (*idiL)["TextureLegUpper"] : 19u, - idiL ? (*idiL)["TextureLegLower"] : 20u, - idiL ? (*idiL)["TextureFoot"] : 21u, - }; - const bool npcIsFemale = (extra.sexId == 1); - const bool npcHasArmArmor = (extra.equipDisplayId[7] != 0 || extra.equipDisplayId[8] != 0); - - auto regionAllowedForNpcSlot = [](int eqSlot, int region) -> bool { - // Regions: 0 ArmUpper, 1 ArmLower, 2 Hand, 3 TorsoUpper, 4 TorsoLower, - // 5 LegUpper, 6 LegLower, 7 Foot - switch (eqSlot) { - case 2: // shirt - case 3: // chest - return region <= 4; - case 4: // belt - // TODO(#npc-belt-region): belt torso-lower overlay can - // cut out male abdomen on some humanoid NPCs. - // Keep disabled until region compositing is fixed. - return false; - case 5: // legs - return region == 5 || region == 6; - case 6: // feet - return region == 7; - case 7: // wrist - // Bracer overlays on NPCs often produce bad arm artifacts. - // Keep disabled until slot-accurate arm compositing is implemented. - return false; - case 8: // hands - // Keep glove textures to hand region only; arm regions from glove - // items can produce furry/looping forearm artifacts on some NPCs. - return region == 2; - case 9: // tabard - return region == 3 || region == 4; - default: - return false; - } - }; - auto regionAllowedForNpcSlotCtx = [&](int eqSlot, int region) -> bool { - // Shirt (slot 2) without arm armor: restrict to torso only - // to avoid bare-skin shirt textures bleeding onto arms. - // Chest (slot 3) always paints arms — plate/mail chest armor - // must cover the full upper body even without separate gloves. - if (eqSlot == 2 && !npcHasArmArmor) { - return (region == 3 || region == 4); - } - return regionAllowedForNpcSlot(eqSlot, region); - }; - - // Iterate all 11 NPC equipment slots; use slot-aware region filtering - for (int eqSlot = 0; eqSlot < 11; eqSlot++) { - uint32_t did = extra.equipDisplayId[eqSlot]; - if (did == 0) continue; - int32_t recIdx = npcItemDisplayDbc->findRecordById(did); - if (recIdx < 0) continue; - - for (int region = 0; region < 8; region++) { - if (!regionAllowedForNpcSlotCtx(eqSlot, region)) continue; - std::string texName = npcItemDisplayDbc->getString( - static_cast(recIdx), texRegionFields[region]); - if (texName.empty()) continue; - - std::string base = "Item\\TextureComponents\\" + - std::string(npcComponentDirs[region]) + "\\" + texName; - std::string genderPath = base + (npcIsFemale ? "_F.blp" : "_M.blp"); - std::string unisexPath = base + "_U.blp"; - std::string basePath = base + ".blp"; - std::string fullPath; - if (assetManager->fileExists(genderPath)) fullPath = genderPath; - else if (assetManager->fileExists(unisexPath)) fullPath = unisexPath; - else if (assetManager->fileExists(basePath)) fullPath = basePath; - else continue; - - npcRegionLayers.emplace_back(region, fullPath); - } - } - - // Cloak/cape texture is separate from the body atlas. - // Read equipped cape displayId (slot 10) and resolve the best cape texture path. - uint32_t capeDisplayId = extra.equipDisplayId[10]; - if (capeDisplayId != 0) { - int32_t capeRecIdx = npcItemDisplayDbc->findRecordById(capeDisplayId); - if (capeRecIdx >= 0) { - const uint32_t leftTexField = idiL ? (*idiL)["LeftModelTexture"] : 3u; - const uint32_t rightTexField = leftTexField + 1u; // modelTexture_2 in 3.3.5a - - std::vector capeNames; - auto addName = [&](const std::string& n) { - if (!n.empty() && std::find(capeNames.begin(), capeNames.end(), n) == capeNames.end()) { - capeNames.push_back(n); - } - }; - std::string leftName = npcItemDisplayDbc->getString( - static_cast(capeRecIdx), leftTexField); - std::string rightName = npcItemDisplayDbc->getString( - static_cast(capeRecIdx), rightTexField); - // Female models often prefer modelTexture_2. - if (npcIsFemale) { - addName(rightName); - addName(leftName); - } else { - addName(leftName); - addName(rightName); - } - - auto hasBlpExt = [](const std::string& p) { - if (p.size() < 4) return false; - std::string ext = p.substr(p.size() - 4); - std::transform(ext.begin(), ext.end(), ext.begin(), - [](unsigned char c) { return static_cast(std::tolower(c)); }); - return ext == ".blp"; - }; - - std::vector capeCandidates; - auto addCapeCandidate = [&](const std::string& p) { - if (p.empty()) return; - if (std::find(capeCandidates.begin(), capeCandidates.end(), p) == capeCandidates.end()) { - capeCandidates.push_back(p); - } - }; - - for (const auto& nameRaw : capeNames) { - std::string name = nameRaw; - std::replace(name.begin(), name.end(), '/', '\\'); - bool hasDir = (name.find('\\') != std::string::npos); - bool hasExt = hasBlpExt(name); - if (hasDir) { - addCapeCandidate(name); - if (!hasExt) addCapeCandidate(name + ".blp"); - } else { - std::string base = "Item\\ObjectComponents\\Cape\\" + name; - addCapeCandidate(base); - if (!hasExt) addCapeCandidate(base + ".blp"); - // Some data sets use gender/unisex suffix variants. - addCapeCandidate(base + (npcIsFemale ? "_F.blp" : "_M.blp")); - addCapeCandidate(base + "_U.blp"); - } - } - - for (const auto& candidate : capeCandidates) { - if (assetManager->fileExists(candidate)) { - npcCapeTexturePath = candidate; - break; - } - } - } - } - } - - // Use baked texture for body skin (types 1, 2) - // Type 6 (hair) needs its own texture from CharSections.dbc - const bool allowNpcRegionComposite = true; - rendering::VkTexture* bakedSkinTex = nullptr; - if (!extra.bakeName.empty()) { - std::string bakePath = "Textures\\BakedNpcTextures\\" + extra.bakeName; - rendering::VkTexture* finalTex = charRenderer->loadTexture(bakePath); - bakedSkinTex = finalTex; - if (finalTex && modelData) { - for (size_t ti = 0; ti < modelData->textures.size(); ti++) { - uint32_t texType = modelData->textures[ti].type; - if (texType == 1) { - charRenderer->setModelTexture(modelId, static_cast(ti), finalTex); - hasHumanoidTexture = true; - LOG_DEBUG("NPC baked type1 slot=", ti, " modelId=", modelId, - " tex=", bakePath); - } - } - } - } - // Fallback: if baked texture failed or bakeName was empty, build from CharSections - if (!hasHumanoidTexture) { - LOG_DEBUG(" Trying CharSections fallback for NPC skin"); - - // Build skin texture from CharSections.dbc (same as player character) - auto csFallbackDbc = assetManager->loadDBC("CharSections.dbc"); - if (csFallbackDbc) { - const auto* csFL = pipeline::getActiveDBCLayout() - ? pipeline::getActiveDBCLayout()->getLayout("CharSections") : nullptr; - uint32_t npcRace = static_cast(extra.raceId); - uint32_t npcSex = static_cast(extra.sexId); - uint32_t npcSkin = static_cast(extra.skinId); - uint32_t npcFace = static_cast(extra.faceId); - std::string npcSkinPath, npcFaceLower, npcFaceUpper; - std::vector npcUnderwear; - - for (uint32_t r = 0; r < csFallbackDbc->getRecordCount(); r++) { - uint32_t rId = csFallbackDbc->getUInt32(r, csFL ? (*csFL)["RaceID"] : 1); - uint32_t sId = csFallbackDbc->getUInt32(r, csFL ? (*csFL)["SexID"] : 2); - if (rId != npcRace || sId != npcSex) continue; - - uint32_t section = csFallbackDbc->getUInt32(r, csFL ? (*csFL)["BaseSection"] : 3); - uint32_t variation = csFallbackDbc->getUInt32(r, csFL ? (*csFL)["VariationIndex"] : 8); - uint32_t color = csFallbackDbc->getUInt32(r, csFL ? (*csFL)["ColorIndex"] : 9); - uint32_t tex1F = csFL ? (*csFL)["Texture1"] : 4; - - // Section 0 = skin: match colorIndex = skinId - if (section == 0 && npcSkinPath.empty() && color == npcSkin) { - npcSkinPath = csFallbackDbc->getString(r, tex1F); - } - // Section 1 = face: match variation=faceId, color=skinId - else if (section == 1 && npcFaceLower.empty() && - variation == npcFace && color == npcSkin) { - npcFaceLower = csFallbackDbc->getString(r, tex1F); - npcFaceUpper = csFallbackDbc->getString(r, tex1F + 1); - } - // Section 4 = underwear: match color=skinId - else if (section == 4 && npcUnderwear.empty() && color == npcSkin) { - for (uint32_t f = tex1F; f <= tex1F + 2; f++) { - std::string tex = csFallbackDbc->getString(r, f); - if (!tex.empty()) npcUnderwear.push_back(tex); - } - } - } - - LOG_DEBUG("NPC CharSections lookup: race=", npcRace, " sex=", npcSex, - " skin=", npcSkin, " face=", npcFace, - " skinPath='", npcSkinPath, "' faceLower='", npcFaceLower, "'"); - if (!npcSkinPath.empty()) { - // Composite skin + face + underwear - std::vector skinLayers; - skinLayers.push_back(npcSkinPath); - if (!npcFaceLower.empty()) skinLayers.push_back(npcFaceLower); - if (!npcFaceUpper.empty()) skinLayers.push_back(npcFaceUpper); - for (const auto& uw : npcUnderwear) skinLayers.push_back(uw); - - rendering::VkTexture* npcSkinTex = nullptr; - if (allowNpcRegionComposite && !npcRegionLayers.empty()) { - npcSkinTex = charRenderer->compositeWithRegions(npcSkinPath, - std::vector(skinLayers.begin() + 1, skinLayers.end()), - npcRegionLayers); - } else if (skinLayers.size() > 1) { - npcSkinTex = charRenderer->compositeTextures(skinLayers); - } else { - npcSkinTex = charRenderer->loadTexture(npcSkinPath); - } - - if (npcSkinTex && modelData) { - int slotsSet = 0; - for (size_t ti = 0; ti < modelData->textures.size(); ti++) { - uint32_t texType = modelData->textures[ti].type; - if (texType == 1 || texType == 11 || texType == 12 || texType == 13) { - charRenderer->setModelTexture(modelId, static_cast(ti), npcSkinTex); - hasHumanoidTexture = true; - slotsSet++; - } - } - LOG_DEBUG("NPC CharSections: skin='", npcSkinPath, "' regions=", - npcRegionLayers.size(), " applied=", hasHumanoidTexture, - " slots=", slotsSet, - " modelId=", modelId, " texCount=", modelData->textures.size()); - } - } + // Collect model texture slot info (type 1 = skin, type 6 = hair) + std::vector skinSlots, hairSlots; + if (modelData) { + for (size_t ti = 0; ti < modelData->textures.size(); ti++) { + uint32_t texType = modelData->textures[ti].type; + if (texType == 1 || texType == 11 || texType == 12 || texType == 13) + skinSlots.push_back(static_cast(ti)); + if (texType == 6) + hairSlots.push_back(static_cast(ti)); } } - // Load hair texture from CharSections.dbc (section 3) - auto charSectionsDbc = assetManager->loadDBC("CharSections.dbc"); - if (charSectionsDbc) { - const auto* csL2 = pipeline::getActiveDBCLayout() ? pipeline::getActiveDBCLayout()->getLayout("CharSections") : nullptr; - uint32_t targetRace = static_cast(extra.raceId); - uint32_t targetSex = static_cast(extra.sexId); - std::string hairTexPath; + // Copy extra data for the async task (avoid dangling reference) + HumanoidDisplayExtra extraCopy = extra; - for (uint32_t r = 0; r < charSectionsDbc->getRecordCount(); r++) { - uint32_t raceId = charSectionsDbc->getUInt32(r, csL2 ? (*csL2)["RaceID"] : 1); - uint32_t sexId = charSectionsDbc->getUInt32(r, csL2 ? (*csL2)["SexID"] : 2); - uint32_t section = charSectionsDbc->getUInt32(r, csL2 ? (*csL2)["BaseSection"] : 3); - uint32_t variation = charSectionsDbc->getUInt32(r, csL2 ? (*csL2)["VariationIndex"] : 4); - uint32_t colorIdx = charSectionsDbc->getUInt32(r, csL2 ? (*csL2)["ColorIndex"] : 5); + // Launch async task: ALL DBC lookups, path resolution, and BLP pre-decode + // happen on a background thread. Only GPU texture upload runs on main thread + // (in processAsyncNpcCompositeResults). + auto* am = assetManager.get(); + AsyncNpcCompositeLoad load; + load.future = std::async(std::launch::async, + [am, extraCopy, skinSlots = std::move(skinSlots), + hairSlots = std::move(hairSlots), modelId, displayId]() mutable -> PreparedNpcComposite { + PreparedNpcComposite result; + DeferredNpcComposite& def = result.info; + def.modelId = modelId; + def.displayId = displayId; + def.skinTextureSlots = std::move(skinSlots); + def.hairTextureSlots = std::move(hairSlots); - if (raceId != targetRace || sexId != targetSex) continue; - if (section != 3) continue; // Section 3 = hair - if (variation != static_cast(extra.hairStyleId)) continue; - if (colorIdx != static_cast(extra.hairColorId)) continue; + std::vector allPaths; // paths to pre-decode - hairTexPath = charSectionsDbc->getString(r, csL2 ? (*csL2)["Texture1"] : 6); - break; - } + // --- Baked skin texture --- + if (!extraCopy.bakeName.empty()) { + def.bakedSkinPath = "Textures\\BakedNpcTextures\\" + extraCopy.bakeName; + def.hasBakedSkin = true; + allPaths.push_back(def.bakedSkinPath); + } - if (!hairTexPath.empty()) { - rendering::VkTexture* hairTex = charRenderer->loadTexture(hairTexPath); - rendering::VkTexture* whTex = charRenderer->loadTexture(""); - if (hairTex && hairTex != whTex && modelData) { - for (size_t ti = 0; ti < modelData->textures.size(); ti++) { - if (modelData->textures[ti].type == 6) { - charRenderer->setModelTexture(modelId, static_cast(ti), hairTex); + // --- CharSections fallback (skin/face/underwear) --- + if (!def.hasBakedSkin) { + auto csDbc = am->loadDBC("CharSections.dbc"); + if (csDbc) { + const auto* csL = pipeline::getActiveDBCLayout() + ? pipeline::getActiveDBCLayout()->getLayout("CharSections") : nullptr; + uint32_t npcRace = static_cast(extraCopy.raceId); + uint32_t npcSex = static_cast(extraCopy.sexId); + uint32_t npcSkin = static_cast(extraCopy.skinId); + uint32_t npcFace = static_cast(extraCopy.faceId); + std::string npcFaceLower, npcFaceUpper; + std::vector npcUnderwear; + + for (uint32_t r = 0; r < csDbc->getRecordCount(); r++) { + uint32_t rId = csDbc->getUInt32(r, csL ? (*csL)["RaceID"] : 1); + uint32_t sId = csDbc->getUInt32(r, csL ? (*csL)["SexID"] : 2); + if (rId != npcRace || sId != npcSex) continue; + + uint32_t section = csDbc->getUInt32(r, csL ? (*csL)["BaseSection"] : 3); + uint32_t variation = csDbc->getUInt32(r, csL ? (*csL)["VariationIndex"] : 4); + uint32_t color = csDbc->getUInt32(r, csL ? (*csL)["ColorIndex"] : 5); + uint32_t tex1F = csL ? (*csL)["Texture1"] : 6; + + if (section == 0 && def.basePath.empty() && color == npcSkin) { + def.basePath = csDbc->getString(r, tex1F); + } else if (section == 1 && npcFaceLower.empty() && + variation == npcFace && color == npcSkin) { + npcFaceLower = csDbc->getString(r, tex1F); + npcFaceUpper = csDbc->getString(r, tex1F + 1); + } else if (section == 4 && npcUnderwear.empty() && color == npcSkin) { + for (uint32_t f = tex1F; f <= tex1F + 2; f++) { + std::string tex = csDbc->getString(r, f); + if (!tex.empty()) npcUnderwear.push_back(tex); + } + } + } + + if (!def.basePath.empty()) { + allPaths.push_back(def.basePath); + if (!npcFaceLower.empty()) { def.overlayPaths.push_back(npcFaceLower); allPaths.push_back(npcFaceLower); } + if (!npcFaceUpper.empty()) { def.overlayPaths.push_back(npcFaceUpper); allPaths.push_back(npcFaceUpper); } + for (const auto& uw : npcUnderwear) { def.overlayPaths.push_back(uw); allPaths.push_back(uw); } } } } - } - // Bald NPCs (hairStyle=0 or no CharSections match): set type-6 to - // the skin/baked texture so the scalp cap renders with skin color. - if (hairTexPath.empty() && bakedSkinTex && modelData) { - for (size_t ti = 0; ti < modelData->textures.size(); ti++) { - if (modelData->textures[ti].type == 6) { - charRenderer->setModelTexture(modelId, static_cast(ti), bakedSkinTex); + + // --- Equipment region layers (ItemDisplayInfo DBC) --- + auto idiDbc = am->loadDBC("ItemDisplayInfo.dbc"); + if (idiDbc) { + static const char* componentDirs[] = { + "ArmUpperTexture", "ArmLowerTexture", "HandTexture", + "TorsoUpperTexture", "TorsoLowerTexture", + "LegUpperTexture", "LegLowerTexture", "FootTexture", + }; + const auto* idiL = pipeline::getActiveDBCLayout() + ? pipeline::getActiveDBCLayout()->getLayout("ItemDisplayInfo") : nullptr; + const uint32_t texRegionFields[8] = { + idiL ? (*idiL)["TextureArmUpper"] : 14u, + idiL ? (*idiL)["TextureArmLower"] : 15u, + idiL ? (*idiL)["TextureHand"] : 16u, + idiL ? (*idiL)["TextureTorsoUpper"]: 17u, + idiL ? (*idiL)["TextureTorsoLower"]: 18u, + idiL ? (*idiL)["TextureLegUpper"] : 19u, + idiL ? (*idiL)["TextureLegLower"] : 20u, + idiL ? (*idiL)["TextureFoot"] : 21u, + }; + const bool npcIsFemale = (extraCopy.sexId == 1); + const bool npcHasArmArmor = (extraCopy.equipDisplayId[7] != 0 || extraCopy.equipDisplayId[8] != 0); + + auto regionAllowedForNpcSlot = [](int eqSlot, int region) -> bool { + switch (eqSlot) { + case 2: case 3: return region <= 4; + case 4: return false; + case 5: return region == 5 || region == 6; + case 6: return region == 7; + case 7: return false; + case 8: return region == 2; + case 9: return region == 3 || region == 4; + default: return false; + } + }; + + for (int eqSlot = 0; eqSlot < 11; eqSlot++) { + uint32_t did = extraCopy.equipDisplayId[eqSlot]; + if (did == 0) continue; + int32_t recIdx = idiDbc->findRecordById(did); + if (recIdx < 0) continue; + + for (int region = 0; region < 8; region++) { + if (!regionAllowedForNpcSlot(eqSlot, region)) continue; + if (eqSlot == 2 && !npcHasArmArmor && !(region == 3 || region == 4)) continue; + std::string texName = idiDbc->getString( + static_cast(recIdx), texRegionFields[region]); + if (texName.empty()) continue; + + std::string base = "Item\\TextureComponents\\" + + std::string(componentDirs[region]) + "\\" + texName; + std::string genderPath = base + (npcIsFemale ? "_F.blp" : "_M.blp"); + std::string unisexPath = base + "_U.blp"; + std::string basePath = base + ".blp"; + std::string fullPath; + if (am->fileExists(genderPath)) fullPath = genderPath; + else if (am->fileExists(unisexPath)) fullPath = unisexPath; + else if (am->fileExists(basePath)) fullPath = basePath; + else continue; + + def.regionLayers.emplace_back(region, fullPath); + allPaths.push_back(fullPath); + } } } - } - } - // Do not apply cape textures at model scope here. Type-2 texture slots are - // shared per model and this can leak cape textures/white fallbacks onto - // unrelated humanoid NPCs that use the same modelId. + // Determine compositing mode + if (!def.basePath.empty()) { + bool needsComposite = !def.overlayPaths.empty() || !def.regionLayers.empty(); + if (needsComposite && !def.skinTextureSlots.empty()) { + def.hasComposite = true; + } else if (!def.skinTextureSlots.empty()) { + def.hasSimpleSkin = true; + } + } + + // --- Hair texture from CharSections (section 3) --- + { + auto csDbc = am->loadDBC("CharSections.dbc"); + if (csDbc) { + const auto* csL = pipeline::getActiveDBCLayout() + ? pipeline::getActiveDBCLayout()->getLayout("CharSections") : nullptr; + uint32_t targetRace = static_cast(extraCopy.raceId); + uint32_t targetSex = static_cast(extraCopy.sexId); + + for (uint32_t r = 0; r < csDbc->getRecordCount(); r++) { + uint32_t raceId = csDbc->getUInt32(r, csL ? (*csL)["RaceID"] : 1); + uint32_t sexId = csDbc->getUInt32(r, csL ? (*csL)["SexID"] : 2); + if (raceId != targetRace || sexId != targetSex) continue; + uint32_t section = csDbc->getUInt32(r, csL ? (*csL)["BaseSection"] : 3); + if (section != 3) continue; + uint32_t variation = csDbc->getUInt32(r, csL ? (*csL)["VariationIndex"] : 4); + uint32_t colorIdx = csDbc->getUInt32(r, csL ? (*csL)["ColorIndex"] : 5); + if (variation != static_cast(extraCopy.hairStyleId)) continue; + if (colorIdx != static_cast(extraCopy.hairColorId)) continue; + def.hairTexturePath = csDbc->getString(r, csL ? (*csL)["Texture1"] : 6); + break; + } + + if (!def.hairTexturePath.empty()) { + allPaths.push_back(def.hairTexturePath); + } else if (def.hasBakedSkin && !def.hairTextureSlots.empty()) { + def.useBakedForHair = true; + // bakedSkinPath already in allPaths + } + } + } + + // --- Pre-decode all BLP textures on this background thread --- + for (const auto& path : allPaths) { + std::string key = path; + std::replace(key.begin(), key.end(), '/', '\\'); + std::transform(key.begin(), key.end(), key.begin(), + [](unsigned char c) { return static_cast(std::tolower(c)); }); + if (result.predecodedTextures.count(key)) continue; + auto blp = am->loadTexture(key); + if (blp.isValid()) { + result.predecodedTextures[key] = std::move(blp); + } + } + + return result; + }); + asyncNpcCompositeLoads_.push_back(std::move(load)); + hasHumanoidTexture = true; // skip non-humanoid skin block } else { LOG_WARNING(" extraDisplayId ", dispData.extraDisplayId, " not found in humanoidExtraMap"); } @@ -5235,6 +5138,18 @@ void Application::spawnOnlineCreature(uint64_t guid, uint32_t displayId, float x } } } + + // Clear pre-decoded cache after applying all display textures + charRenderer->setPredecodedBLPCache(nullptr); + displayIdPredecodedTextures_.erase(displayId); + { + auto texEnd = std::chrono::steady_clock::now(); + float texMs = std::chrono::duration(texEnd - texStart).count(); + if (texMs > 3.0f) { + LOG_WARNING("spawnCreature texture setup took ", texMs, "ms displayId=", displayId, + " hasPreDec=", hasPreDec, " extra=", dispData.extraDisplayId); + } + } } // Use the entity's latest server-authoritative position rather than the stale spawn @@ -6926,6 +6841,7 @@ void Application::processAsyncCreatureResults() { // Upload model to GPU (must happen on main thread) // Use pre-decoded BLP cache to skip main-thread texture decode + auto uploadStart = std::chrono::steady_clock::now(); charRenderer->setPredecodedBLPCache(&result.predecodedTextures); if (!charRenderer->loadModel(*result.model, result.modelId)) { charRenderer->setPredecodedBLPCache(nullptr); @@ -6936,6 +6852,18 @@ void Application::processAsyncCreatureResults() { continue; } charRenderer->setPredecodedBLPCache(nullptr); + { + auto uploadEnd = std::chrono::steady_clock::now(); + float uploadMs = std::chrono::duration(uploadEnd - uploadStart).count(); + if (uploadMs > 3.0f) { + LOG_WARNING("charRenderer->loadModel took ", uploadMs, "ms displayId=", result.displayId, + " preDecoded=", result.predecodedTextures.size()); + } + } + // Save remaining pre-decoded textures (display skins) for spawnOnlineCreature + if (!result.predecodedTextures.empty()) { + displayIdPredecodedTextures_[result.displayId] = std::move(result.predecodedTextures); + } displayIdModelCache_[result.displayId] = result.modelId; modelUploads++; @@ -6959,6 +6887,77 @@ void Application::processAsyncCreatureResults() { } } +void Application::processAsyncNpcCompositeResults() { + auto* charRenderer = renderer ? renderer->getCharacterRenderer() : nullptr; + if (!charRenderer) return; + + for (auto it = asyncNpcCompositeLoads_.begin(); it != asyncNpcCompositeLoads_.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 = asyncNpcCompositeLoads_.erase(it); + + const auto& info = result.info; + + // Set pre-decoded cache so texture loads skip synchronous BLP decode + charRenderer->setPredecodedBLPCache(&result.predecodedTextures); + + // --- Apply skin to type-1 slots --- + rendering::VkTexture* skinTex = nullptr; + + if (info.hasBakedSkin) { + // Baked skin: load from pre-decoded cache + skinTex = charRenderer->loadTexture(info.bakedSkinPath); + } + + if (info.hasComposite) { + // Composite with face/underwear/equipment regions on top of base skin + rendering::VkTexture* compositeTex = nullptr; + if (!info.regionLayers.empty()) { + compositeTex = charRenderer->compositeWithRegions(info.basePath, + info.overlayPaths, info.regionLayers); + } else if (!info.overlayPaths.empty()) { + std::vector skinLayers; + skinLayers.push_back(info.basePath); + for (const auto& op : info.overlayPaths) skinLayers.push_back(op); + compositeTex = charRenderer->compositeTextures(skinLayers); + } + if (compositeTex) skinTex = compositeTex; + } else if (info.hasSimpleSkin) { + // Simple skin: just base texture, no compositing + auto* baseTex = charRenderer->loadTexture(info.basePath); + if (baseTex) skinTex = baseTex; + } + + if (skinTex) { + for (uint32_t slot : info.skinTextureSlots) { + charRenderer->setModelTexture(info.modelId, slot, skinTex); + } + } + + // --- Apply hair texture to type-6 slots --- + if (!info.hairTexturePath.empty()) { + rendering::VkTexture* hairTex = charRenderer->loadTexture(info.hairTexturePath); + rendering::VkTexture* whTex = charRenderer->loadTexture(""); + if (hairTex && hairTex != whTex) { + for (uint32_t slot : info.hairTextureSlots) { + charRenderer->setModelTexture(info.modelId, slot, hairTex); + } + } + } else if (info.useBakedForHair && skinTex) { + // Bald NPC: use skin/baked texture for scalp cap + for (uint32_t slot : info.hairTextureSlots) { + charRenderer->setModelTexture(info.modelId, slot, skinTex); + } + } + + charRenderer->setPredecodedBLPCache(nullptr); + } +} + void Application::processCreatureSpawnQueue() { auto startTime = std::chrono::steady_clock::now(); // Budget: max 2ms per frame for creature spawning to prevent stutter. @@ -6966,6 +6965,13 @@ void Application::processCreatureSpawnQueue() { // First, finalize any async model loads that completed on background threads. processAsyncCreatureResults(); + { + auto now = std::chrono::steady_clock::now(); + float asyncMs = std::chrono::duration(now - startTime).count(); + if (asyncMs > 3.0f) { + LOG_WARNING("processAsyncCreatureResults took ", asyncMs, "ms"); + } + } if (pendingCreatureSpawns_.empty()) return; if (!creatureLookupsBuilt_) { @@ -7039,9 +7045,136 @@ void Application::processCreatureSpawnQueue() { // Launch async M2 load — file I/O and parsing happen off the main thread. uint32_t modelId = nextCreatureModelId_++; auto* am = assetManager.get(); + + // Collect display skin texture paths for background pre-decode + std::vector displaySkinPaths; + { + auto itDD = displayDataMap_.find(s.displayId); + if (itDD != displayDataMap_.end()) { + std::string modelDir; + size_t lastSlash = m2Path.find_last_of("\\/"); + if (lastSlash != std::string::npos) modelDir = m2Path.substr(0, lastSlash + 1); + + auto resolveForAsync = [&](const std::string& skinField) { + if (skinField.empty()) return; + std::string raw = skinField; + std::replace(raw.begin(), raw.end(), '/', '\\'); + while (!raw.empty() && std::isspace(static_cast(raw.front()))) raw.erase(raw.begin()); + while (!raw.empty() && std::isspace(static_cast(raw.back()))) raw.pop_back(); + if (raw.empty()) return; + bool hasExt = raw.size() >= 4 && raw.substr(raw.size()-4) == ".blp"; + bool hasDir = raw.find('\\') != std::string::npos; + std::vector candidates; + if (hasDir) { + candidates.push_back(raw); + if (!hasExt) candidates.push_back(raw + ".blp"); + } else { + candidates.push_back(modelDir + raw); + if (!hasExt) candidates.push_back(modelDir + raw + ".blp"); + candidates.push_back(raw); + if (!hasExt) candidates.push_back(raw + ".blp"); + } + for (const auto& c : candidates) { + if (am->fileExists(c)) { displaySkinPaths.push_back(c); return; } + } + }; + resolveForAsync(itDD->second.skin1); + resolveForAsync(itDD->second.skin2); + resolveForAsync(itDD->second.skin3); + + // Pre-decode humanoid NPC textures (bake, skin, face, underwear, hair, equipment) + if (itDD->second.extraDisplayId != 0) { + auto itHE = humanoidExtraMap_.find(itDD->second.extraDisplayId); + if (itHE != humanoidExtraMap_.end()) { + const auto& he = itHE->second; + // Baked texture + if (!he.bakeName.empty()) { + displaySkinPaths.push_back("Textures\\BakedNpcTextures\\" + he.bakeName); + } + // CharSections: skin, face, underwear + auto csDbc = am->loadDBC("CharSections.dbc"); + if (csDbc) { + const auto* csL = pipeline::getActiveDBCLayout() + ? pipeline::getActiveDBCLayout()->getLayout("CharSections") : nullptr; + uint32_t nRace = static_cast(he.raceId); + uint32_t nSex = static_cast(he.sexId); + uint32_t nSkin = static_cast(he.skinId); + uint32_t nFace = static_cast(he.faceId); + for (uint32_t r = 0; r < csDbc->getRecordCount(); r++) { + uint32_t rId = csDbc->getUInt32(r, csL ? (*csL)["RaceID"] : 1); + uint32_t sId = csDbc->getUInt32(r, csL ? (*csL)["SexID"] : 2); + if (rId != nRace || sId != nSex) continue; + uint32_t section = csDbc->getUInt32(r, csL ? (*csL)["BaseSection"] : 3); + uint32_t variation = csDbc->getUInt32(r, csL ? (*csL)["VariationIndex"] : 4); + uint32_t color = csDbc->getUInt32(r, csL ? (*csL)["ColorIndex"] : 5); + uint32_t tex1F = csL ? (*csL)["Texture1"] : 6; + if (section == 0 && color == nSkin) { + std::string t = csDbc->getString(r, tex1F); + if (!t.empty()) displaySkinPaths.push_back(t); + } else if (section == 1 && variation == nFace && color == nSkin) { + std::string t1 = csDbc->getString(r, tex1F); + std::string t2 = csDbc->getString(r, tex1F + 1); + if (!t1.empty()) displaySkinPaths.push_back(t1); + if (!t2.empty()) displaySkinPaths.push_back(t2); + } else if (section == 3 && variation == static_cast(he.hairStyleId) + && color == static_cast(he.hairColorId)) { + std::string t = csDbc->getString(r, tex1F); + if (!t.empty()) displaySkinPaths.push_back(t); + } else if (section == 4 && color == nSkin) { + for (uint32_t f = tex1F; f <= tex1F + 2; f++) { + std::string t = csDbc->getString(r, f); + if (!t.empty()) displaySkinPaths.push_back(t); + } + } + } + } + // Equipment region textures + auto idiDbc = am->loadDBC("ItemDisplayInfo.dbc"); + if (idiDbc) { + static const char* compDirs[] = { + "ArmUpperTexture", "ArmLowerTexture", "HandTexture", + "TorsoUpperTexture", "TorsoLowerTexture", + "LegUpperTexture", "LegLowerTexture", "FootTexture", + }; + const auto* idiL = pipeline::getActiveDBCLayout() + ? pipeline::getActiveDBCLayout()->getLayout("ItemDisplayInfo") : nullptr; + const uint32_t trf[8] = { + idiL ? (*idiL)["TextureArmUpper"] : 14u, + idiL ? (*idiL)["TextureArmLower"] : 15u, + idiL ? (*idiL)["TextureHand"] : 16u, + idiL ? (*idiL)["TextureTorsoUpper"]: 17u, + idiL ? (*idiL)["TextureTorsoLower"]: 18u, + idiL ? (*idiL)["TextureLegUpper"] : 19u, + idiL ? (*idiL)["TextureLegLower"] : 20u, + idiL ? (*idiL)["TextureFoot"] : 21u, + }; + const bool isFem = (he.sexId == 1); + for (int eq = 0; eq < 11; eq++) { + uint32_t did = he.equipDisplayId[eq]; + if (did == 0) continue; + int32_t recIdx = idiDbc->findRecordById(did); + if (recIdx < 0) continue; + for (int region = 0; region < 8; region++) { + std::string texName = idiDbc->getString(static_cast(recIdx), trf[region]); + if (texName.empty()) continue; + std::string base = "Item\\TextureComponents\\" + + std::string(compDirs[region]) + "\\" + texName; + std::string gp = base + (isFem ? "_F.blp" : "_M.blp"); + std::string up = base + "_U.blp"; + if (am->fileExists(gp)) displaySkinPaths.push_back(gp); + else if (am->fileExists(up)) displaySkinPaths.push_back(up); + else displaySkinPaths.push_back(base + ".blp"); + } + } + } + } + } + } + } + AsyncCreatureLoad load; load.future = std::async(std::launch::async, - [am, m2Path, modelId, s]() -> PreparedCreatureModel { + [am, m2Path, modelId, s, skinPaths = std::move(displaySkinPaths)]() -> PreparedCreatureModel { PreparedCreatureModel result; result.guid = s.guid; result.displayId = s.displayId; @@ -7100,6 +7233,19 @@ void Application::processCreatureSpawnQueue() { } } + // Pre-decode display skin textures (skin1/skin2/skin3 from CreatureDisplayInfo) + for (const auto& sp : skinPaths) { + std::string key = sp; + std::replace(key.begin(), key.end(), '/', '\\'); + std::transform(key.begin(), key.end(), key.begin(), + [](unsigned char c) { return static_cast(std::tolower(c)); }); + if (result.predecodedTextures.count(key)) continue; + auto blp = am->loadTexture(key); + if (blp.isValid()) { + result.predecodedTextures[key] = std::move(blp); + } + } + result.model = std::move(model); result.valid = true; return result; @@ -7113,7 +7259,15 @@ void Application::processCreatureSpawnQueue() { } // 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); + { + auto spawnStart = std::chrono::steady_clock::now(); + spawnOnlineCreature(s.guid, s.displayId, s.x, s.y, s.z, s.orientation); + auto spawnEnd = std::chrono::steady_clock::now(); + float spawnMs = std::chrono::duration(spawnEnd - spawnStart).count(); + if (spawnMs > 3.0f) { + LOG_WARNING("spawnOnlineCreature took ", spawnMs, "ms displayId=", s.displayId); + } + } pendingCreatureSpawnGuids_.erase(s.guid); // If spawn still failed, retry for a limited number of frames. @@ -7172,12 +7326,130 @@ void Application::processPlayerSpawnQueue() { } } +std::vector Application::resolveEquipmentTexturePaths(uint64_t guid, + const std::array& displayInfoIds, + const std::array& /*inventoryTypes*/) const { + std::vector paths; + + auto it = onlinePlayerAppearance_.find(guid); + if (it == onlinePlayerAppearance_.end()) return paths; + const OnlinePlayerAppearanceState& st = it->second; + + // Add base skin + underwear paths + if (!st.bodySkinPath.empty()) paths.push_back(st.bodySkinPath); + for (const auto& up : st.underwearPaths) { + if (!up.empty()) paths.push_back(up); + } + + // Resolve equipment region texture paths (same logic as setOnlinePlayerEquipment) + auto displayInfoDbc = assetManager->loadDBC("ItemDisplayInfo.dbc"); + if (!displayInfoDbc) return paths; + const auto* idiL = pipeline::getActiveDBCLayout() + ? pipeline::getActiveDBCLayout()->getLayout("ItemDisplayInfo") : nullptr; + + static const char* componentDirs[] = { + "ArmUpperTexture", "ArmLowerTexture", "HandTexture", + "TorsoUpperTexture", "TorsoLowerTexture", + "LegUpperTexture", "LegLowerTexture", "FootTexture", + }; + const uint32_t texRegionFields[8] = { + idiL ? (*idiL)["TextureArmUpper"] : 14u, + idiL ? (*idiL)["TextureArmLower"] : 15u, + idiL ? (*idiL)["TextureHand"] : 16u, + idiL ? (*idiL)["TextureTorsoUpper"]: 17u, + idiL ? (*idiL)["TextureTorsoLower"]: 18u, + idiL ? (*idiL)["TextureLegUpper"] : 19u, + idiL ? (*idiL)["TextureLegLower"] : 20u, + idiL ? (*idiL)["TextureFoot"] : 21u, + }; + const bool isFemale = (st.genderId == 1); + + for (int s = 0; s < 19; s++) { + uint32_t did = displayInfoIds[s]; + if (did == 0) continue; + int32_t recIdx = displayInfoDbc->findRecordById(did); + if (recIdx < 0) continue; + for (int region = 0; region < 8; region++) { + std::string texName = displayInfoDbc->getString( + static_cast(recIdx), texRegionFields[region]); + if (texName.empty()) continue; + std::string base = "Item\\TextureComponents\\" + + std::string(componentDirs[region]) + "\\" + texName; + std::string genderPath = base + (isFemale ? "_F.blp" : "_M.blp"); + std::string unisexPath = base + "_U.blp"; + if (assetManager->fileExists(genderPath)) paths.push_back(genderPath); + else if (assetManager->fileExists(unisexPath)) paths.push_back(unisexPath); + else paths.push_back(base + ".blp"); + } + } + return paths; +} + +void Application::processAsyncEquipmentResults() { + for (auto it = asyncEquipmentLoads_.begin(); it != asyncEquipmentLoads_.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 = asyncEquipmentLoads_.erase(it); + + auto* charRenderer = renderer ? renderer->getCharacterRenderer() : nullptr; + if (!charRenderer) continue; + + // Set pre-decoded cache so compositeWithRegions skips synchronous BLP decode + charRenderer->setPredecodedBLPCache(&result.predecodedTextures); + setOnlinePlayerEquipment(result.guid, result.displayInfoIds, result.inventoryTypes); + charRenderer->setPredecodedBLPCache(nullptr); + } +} + void Application::processDeferredEquipmentQueue() { + // First, finalize any completed async pre-decodes + processAsyncEquipmentResults(); + if (deferredEquipmentQueue_.empty()) return; - // Process at most 1 per frame — compositeWithRegions is expensive + // Limit in-flight async equipment loads + if (asyncEquipmentLoads_.size() >= 2) return; + auto [guid, equipData] = deferredEquipmentQueue_.front(); deferredEquipmentQueue_.erase(deferredEquipmentQueue_.begin()); - setOnlinePlayerEquipment(guid, equipData.first, equipData.second); + + // Resolve all texture paths that compositeWithRegions will need + auto texturePaths = resolveEquipmentTexturePaths(guid, equipData.first, equipData.second); + + if (texturePaths.empty()) { + // No textures to pre-decode — just apply directly (fast path) + setOnlinePlayerEquipment(guid, equipData.first, equipData.second); + return; + } + + // Launch background BLP pre-decode + auto* am = assetManager.get(); + auto displayInfoIds = equipData.first; + auto inventoryTypes = equipData.second; + AsyncEquipmentLoad load; + load.future = std::async(std::launch::async, + [am, guid, displayInfoIds, inventoryTypes, paths = std::move(texturePaths)]() -> PreparedEquipmentUpdate { + PreparedEquipmentUpdate result; + result.guid = guid; + result.displayInfoIds = displayInfoIds; + result.inventoryTypes = inventoryTypes; + for (const auto& path : paths) { + std::string key = path; + std::replace(key.begin(), key.end(), '/', '\\'); + std::transform(key.begin(), key.end(), key.begin(), + [](unsigned char c) { return static_cast(std::tolower(c)); }); + if (result.predecodedTextures.count(key)) continue; + auto blp = am->loadTexture(key); + if (blp.isValid()) { + result.predecodedTextures[key] = std::move(blp); + } + } + return result; + }); + asyncEquipmentLoads_.push_back(std::move(load)); } void Application::processAsyncGameObjectResults() { diff --git a/src/rendering/character_renderer.cpp b/src/rendering/character_renderer.cpp index 040a301d..2031a7b4 100644 --- a/src/rendering/character_renderer.cpp +++ b/src/rendering/character_renderer.cpp @@ -836,7 +836,19 @@ VkTexture* CharacterRenderer::compositeTextures(const std::vector& } // 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(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()) { core::Logger::getInstance().warning("Composite: failed to load base layer: ", layerPaths[0]); return whiteTexture_.get(); @@ -877,7 +889,19 @@ VkTexture* CharacterRenderer::compositeTextures(const std::vector& for (size_t layer = 1; layer < layerPaths.size(); layer++) { 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(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()) { core::Logger::getInstance().warning("Composite: FAILED to load overlay: ", layerPaths[layer]); continue; @@ -1054,7 +1078,19 @@ VkTexture* CharacterRenderer::compositeWithRegions(const std::string& basePath, 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(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()) { return whiteTexture_.get(); } @@ -1093,7 +1129,19 @@ VkTexture* CharacterRenderer::compositeWithRegions(const std::string& basePath, bool upscaled = (base.width == 256 && base.height == 256 && width == 512); for (const auto& ul : baseLayers) { 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(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.width == width && overlay.height == height) { @@ -1171,7 +1219,19 @@ VkTexture* CharacterRenderer::compositeWithRegions(const std::string& basePath, int regionIdx = rl.first; 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(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()) { core::Logger::getInstance().warning("compositeWithRegions: failed to load ", rl.second); continue;