diff --git a/include/game/game_handler.hpp b/include/game/game_handler.hpp index 67c8f5f3..ef72d44e 100644 --- a/include/game/game_handler.hpp +++ b/include/game/game_handler.hpp @@ -3406,6 +3406,7 @@ private: std::vector trainerTabs_; void handleTrainerList(network::Packet& packet); void loadSpellNameCache() const; + void preloadDBCCaches() const; void categorizeTrainerSpells(); // Callbacks diff --git a/include/rendering/wmo_renderer.hpp b/include/rendering/wmo_renderer.hpp index 2431628e..2189909c 100644 --- a/include/rendering/wmo_renderer.hpp +++ b/include/rendering/wmo_renderer.hpp @@ -737,6 +737,8 @@ private: std::vector> cullFutures_; std::vector visibleInstances_; // reused per frame std::vector drawLists_; // reused per frame + std::vector portalVisibleGroups_; // reused per frame (portal culling scratch) + std::unordered_set portalVisibleGroupSet_; // reused per frame (portal culling scratch) // Collision query profiling (per frame). mutable double queryTimeMs = 0.0; diff --git a/src/game/game_handler.cpp b/src/game/game_handler.cpp index 1d7d80a3..ff0bcad7 100644 --- a/src/game/game_handler.cpp +++ b/src/game/game_handler.cpp @@ -8739,6 +8739,13 @@ void GameHandler::handleLoginVerifyWorld(network::Packet& packet) { } } + // Pre-load DBC name caches during world entry so the first packet that + // needs spell/title/achievement data doesn't stall mid-gameplay (the + // Spell.dbc cache alone is ~170ms on a cold load). + if (initialWorldEntry) { + preloadDBCCaches(); + } + // Fire PLAYER_ENTERING_WORLD — THE most important event for addon initialization. // Fires on initial login, teleports, instance transitions, and zone changes. if (addonEventCallback_) { @@ -21907,6 +21914,22 @@ void GameHandler::closeTrainer() { trainerTabs_.clear(); } +void GameHandler::preloadDBCCaches() const { + LOG_INFO("Pre-loading DBC caches during world entry..."); + auto t0 = std::chrono::steady_clock::now(); + + loadSpellNameCache(); // Spell.dbc — largest, ~170ms cold + loadTitleNameCache(); // CharTitles.dbc + loadFactionNameCache(); // Faction.dbc + loadAreaNameCache(); // WorldMapArea.dbc + loadMapNameCache(); // Map.dbc + loadLfgDungeonDbc(); // LFGDungeons.dbc + + auto elapsed = std::chrono::duration_cast( + std::chrono::steady_clock::now() - t0).count(); + LOG_INFO("DBC cache pre-load complete in ", elapsed, " ms"); +} + void GameHandler::loadSpellNameCache() const { if (spellNameCacheLoaded_) return; spellNameCacheLoaded_ = true; diff --git a/src/game/world_packets.cpp b/src/game/world_packets.cpp index bf0b0afe..1b5e23a4 100644 --- a/src/game/world_packets.cpp +++ b/src/game/world_packets.cpp @@ -975,21 +975,16 @@ bool UpdateObjectParser::parseMovementBlock(network::Packet& packet, UpdateBlock /*float finalAngle =*/ packet.readFloat(); } - // Spline data layout varies by expansion: - // Classic/Vanilla: timePassed(4)+duration(4)+splineId(4)+pointCount(4)+points+mode(1)+endPoint(12) - // WotLK: timePassed(4)+duration(4)+splineId(4)+durationMod(4)+durationModNext(4) - // +[ANIMATION(5)]+[PARABOLIC(8)]+pointCount(4)+points+mode(1)+endPoint(12) - // Since the parser has no expansion context, auto-detect by trying Classic first. - if (!bytesAvailable(16)) return false; // minimum: 12 common + 4 pointCount + // WotLK spline data layout: + // timePassed(4)+duration(4)+splineId(4)+durationMod(4)+durationModNext(4) + // +[ANIMATION(5)]+verticalAccel(4)+effectStartTime(4)+pointCount(4)+points+mode(1)+endPoint(12) + if (!bytesAvailable(12)) return false; /*uint32_t timePassed =*/ packet.readUInt32(); /*uint32_t duration =*/ packet.readUInt32(); /*uint32_t splineId =*/ packet.readUInt32(); - const size_t afterSplineId = packet.getReadPos(); // Helper: parse spline points + splineMode + endPoint. // WotLK uses compressed points by default (first=12 bytes, rest=4 bytes packed). - // Classic/Turtle uses all uncompressed (12 bytes each). - // The 'compressed' parameter selects which format. auto tryParseSplinePoints = [&](bool compressed, const char* tag) -> bool { if (!bytesAvailable(4)) return false; size_t prePointCount = packet.getReadPos(); @@ -1019,41 +1014,26 @@ bool UpdateObjectParser::parseMovementBlock(network::Packet& packet, UpdateBlock return true; }; - // --- Try 1: WotLK format (durationMod+durationModNext+parabolic+compressed points) --- - // Try WotLK first since this is a WotLK parser; Classic auto-detect can false-positive - // when durationMod bytes happen to look like a valid Classic pointCount. - bool splineParsed = false; - { - bool wotlkOk = bytesAvailable(8); // durationMod + durationModNext - if (wotlkOk) { - /*float durationMod =*/ packet.readFloat(); - /*float durationModNext =*/ packet.readFloat(); - if (splineFlags & 0x00400000) { // SPLINEFLAG_ANIMATION - if (!bytesAvailable(5)) { wotlkOk = false; } - else { packet.readUInt8(); packet.readUInt32(); } - } - } - // AzerothCore/ChromieCraft always writes verticalAcceleration(float) - // + effectStartTime(uint32) unconditionally — NOT gated by PARABOLIC flag. - if (wotlkOk) { - if (!bytesAvailable(8)) { wotlkOk = false; } - else { /*float vertAccel =*/ packet.readFloat(); /*uint32_t effectStart =*/ packet.readUInt32(); } - } - if (wotlkOk) { - // WotLK: compressed unless CYCLIC(0x80000) or ENTER_CYCLE(0x2000) set - bool useCompressed = (splineFlags & (0x00080000 | 0x00002000)) == 0; - splineParsed = tryParseSplinePoints(useCompressed, "wotlk-compressed"); - // Fallback: try uncompressed WotLK if compressed didn't work - if (!splineParsed) { - splineParsed = tryParseSplinePoints(false, "wotlk-uncompressed"); - } - } + // WotLK format: durationMod+durationModNext+[ANIMATION]+vertAccel+effectStart+points + if (!bytesAvailable(8)) return false; // durationMod + durationModNext + /*float durationMod =*/ packet.readFloat(); + /*float durationModNext =*/ packet.readFloat(); + if (splineFlags & 0x00400000) { // SPLINEFLAG_ANIMATION + if (!bytesAvailable(5)) return false; + packet.readUInt8(); packet.readUInt32(); } + // AzerothCore/ChromieCraft always writes verticalAcceleration(float) + // + effectStartTime(uint32) unconditionally -- NOT gated by PARABOLIC flag. + if (!bytesAvailable(8)) return false; + /*float vertAccel =*/ packet.readFloat(); + /*uint32_t effectStart =*/ packet.readUInt32(); - // --- Try 2: Classic format (uncompressed points immediately after splineId) --- + // WotLK: compressed unless CYCLIC(0x80000) or ENTER_CYCLE(0x2000) set + bool useCompressed = (splineFlags & (0x00080000 | 0x00002000)) == 0; + bool splineParsed = tryParseSplinePoints(useCompressed, "wotlk-compressed"); + // Fallback: try uncompressed WotLK if compressed didn't work if (!splineParsed) { - packet.setReadPos(afterSplineId); - splineParsed = tryParseSplinePoints(false, "classic"); + splineParsed = tryParseSplinePoints(false, "wotlk-uncompressed"); } } } diff --git a/src/rendering/wmo_renderer.cpp b/src/rendering/wmo_renderer.cpp index fdcfd3df..0688ae31 100644 --- a/src/rendering/wmo_renderer.cpp +++ b/src/rendering/wmo_renderer.cpp @@ -1395,21 +1395,21 @@ void WMORenderer::render(VkCommandBuffer cmd, VkDescriptorSet perFrameSet, const result.portalCulled = 0; result.distanceCulled = 0; - // Portal-based visibility — use a flat sorted vector instead of unordered_set - std::vector portalVisibleGroups; + // Portal-based visibility — reuse member scratch buffers (avoid per-frame alloc) + portalVisibleGroups_.clear(); bool usePortalCulling = doPortalCull && !model.portals.empty() && !model.portalRefs.empty(); if (usePortalCulling) { - std::unordered_set pvgSet; + portalVisibleGroupSet_.clear(); glm::vec4 localCamPos = instance.invModelMatrix * glm::vec4(portalViewerPos, 1.0f); getVisibleGroupsViaPortals(model, glm::vec3(localCamPos), frustum, - instance.modelMatrix, pvgSet); - portalVisibleGroups.assign(pvgSet.begin(), pvgSet.end()); - std::sort(portalVisibleGroups.begin(), portalVisibleGroups.end()); + instance.modelMatrix, portalVisibleGroupSet_); + portalVisibleGroups_.assign(portalVisibleGroupSet_.begin(), portalVisibleGroupSet_.end()); + std::sort(portalVisibleGroups_.begin(), portalVisibleGroups_.end()); } for (size_t gi = 0; gi < model.groups.size(); ++gi) { if (usePortalCulling && - !std::binary_search(portalVisibleGroups.begin(), portalVisibleGroups.end(), + !std::binary_search(portalVisibleGroups_.begin(), portalVisibleGroups_.end(), static_cast(gi))) { result.portalCulled++; continue;