From 2af3594ce8f8e67577a141a25b8bff4422f61347 Mon Sep 17 00:00:00 2001 From: Kelsi Date: Fri, 27 Mar 2026 18:21:47 -0700 Subject: [PATCH] perf: eliminate per-frame heap allocs in M2 renderer; UI polish and report M2 renderer: move 3 per-frame local containers to member variables: - particleGroups_ (unordered_map): reuse bucket structure across frames - ribbonDraws_ (vector): reuse draw call buffer - shadowTexSetCache_ (unordered_map): reuse descriptor cache Eliminates ~3 heap allocations per frame in particle/ribbon/shadow passes. UI polish: - Nameplate hover tooltip showing level, class (players), guild name - Bag window titles show slot counts: "Backpack (12/16)" Player report: CMSG_COMPLAIN packet builder and reportPlayer() method. "Report Player" option in target frame right-click menu for other players. Server response handler (SMSG_COMPLAIN_RESULT) was already implemented. --- include/game/game_handler.hpp | 1 + include/game/world_packets.hpp | 6 ++++ include/rendering/m2_renderer.hpp | 43 +++++++++++++++++++++++++++ src/game/game_handler.cpp | 17 +++++++++++ src/game/world_packets.cpp | 13 ++++++++ src/rendering/m2_renderer.cpp | 49 +++++++------------------------ src/ui/game_screen.cpp | 13 ++++++++ src/ui/inventory_screen.cpp | 19 ++++++++---- 8 files changed, 117 insertions(+), 44 deletions(-) diff --git a/include/game/game_handler.hpp b/include/game/game_handler.hpp index 7b5a3775..915b8b54 100644 --- a/include/game/game_handler.hpp +++ b/include/game/game_handler.hpp @@ -718,6 +718,7 @@ public: // Combat and Trade void proposeDuel(uint64_t targetGuid); void initiateTrade(uint64_t targetGuid); + void reportPlayer(uint64_t targetGuid, const std::string& reason); void stopCasting(); // ---- Phase 1: Name queries ---- diff --git a/include/game/world_packets.hpp b/include/game/world_packets.hpp index db66a9fe..a8be1060 100644 --- a/include/game/world_packets.hpp +++ b/include/game/world_packets.hpp @@ -904,6 +904,12 @@ public: static network::Packet build(uint64_t ignoreGuid); }; +/** CMSG_COMPLAIN packet builder (player report) */ +class ComplainPacket { +public: + static network::Packet build(uint64_t targetGuid, const std::string& reason); +}; + // ============================================================ // Logout Commands // ============================================================ diff --git a/include/rendering/m2_renderer.hpp b/include/rendering/m2_renderer.hpp index ac99c5df..24d72247 100644 --- a/include/rendering/m2_renderer.hpp +++ b/include/rendering/m2_renderer.hpp @@ -550,6 +550,49 @@ private: }; std::vector glowSprites_; // Reused each frame + // Shadow-pass texture descriptor cache (reused each frame, cleared via pool reset) + std::unordered_map shadowTexSetCache_; + + // Ribbon draw-call list (reused each frame) + struct RibbonDrawCall { + VkDescriptorSet texSet; + VkPipeline pipeline; + uint32_t firstVertex; + uint32_t vertexCount; + }; + std::vector ribbonDraws_; + + // Particle group structures (reused each frame) + struct ParticleGroupKey { + VkTexture* texture; + uint8_t blendType; + uint16_t tilesX; + uint16_t tilesY; + bool operator==(const ParticleGroupKey& other) const { + return texture == other.texture && + blendType == other.blendType && + tilesX == other.tilesX && + tilesY == other.tilesY; + } + }; + struct ParticleGroupKeyHash { + size_t operator()(const ParticleGroupKey& key) const { + size_t h1 = std::hash{}(reinterpret_cast(key.texture)); + size_t h2 = std::hash{}((static_cast(key.tilesX) << 16) | key.tilesY); + size_t h3 = std::hash{}(key.blendType); + return h1 ^ (h2 * 0x9e3779b9u) ^ (h3 * 0x85ebca6bu); + } + }; + struct ParticleGroup { + VkTexture* texture; + uint8_t blendType; + uint16_t tilesX; + uint16_t tilesY; + VkDescriptorSet preAllocSet = VK_NULL_HANDLE; + std::vector vertexData; + }; + std::unordered_map particleGroups_; + // Animation update buffers (avoid per-frame allocation) std::vector boneWorkIndices_; // Reused each frame std::vector> animFutures_; // Reused each frame diff --git a/src/game/game_handler.cpp b/src/game/game_handler.cpp index e928b4cf..58a7ecbe 100644 --- a/src/game/game_handler.cpp +++ b/src/game/game_handler.cpp @@ -13784,6 +13784,23 @@ void GameHandler::initiateTrade(uint64_t targetGuid) { LOG_INFO("Initiated trade with target: 0x", std::hex, targetGuid, std::dec); } +void GameHandler::reportPlayer(uint64_t targetGuid, const std::string& reason) { + if (!isInWorld()) { + LOG_WARNING("Cannot report player: not in world or not connected"); + return; + } + + if (targetGuid == 0) { + addSystemChatMessage("You must target a player to report."); + return; + } + + auto packet = ComplainPacket::build(targetGuid, reason); + socket->send(packet); + addSystemChatMessage("Player report submitted."); + LOG_INFO("Reported player: 0x", std::hex, targetGuid, std::dec, " reason=", reason); +} + void GameHandler::stopCasting() { if (!isInWorld()) { LOG_WARNING("Cannot stop casting: not in world or not connected"); diff --git a/src/game/world_packets.cpp b/src/game/world_packets.cpp index 82214d21..bda7b163 100644 --- a/src/game/world_packets.cpp +++ b/src/game/world_packets.cpp @@ -1908,6 +1908,19 @@ network::Packet DelIgnorePacket::build(uint64_t ignoreGuid) { return packet; } +network::Packet ComplainPacket::build(uint64_t targetGuid, const std::string& reason) { + network::Packet packet(wireOpcode(Opcode::CMSG_COMPLAIN)); + packet.writeUInt8(1); // complaintType: 1 = spam + packet.writeUInt64(targetGuid); + packet.writeUInt32(0); // unk + packet.writeUInt32(0); // messageType + packet.writeUInt32(0); // channelId + packet.writeUInt32(static_cast(time(nullptr))); // timestamp + packet.writeString(reason); + LOG_DEBUG("Built CMSG_COMPLAIN: target=0x", std::hex, targetGuid, std::dec, " reason=", reason); + return packet; +} + // ============================================================ // Logout Commands // ============================================================ diff --git a/src/rendering/m2_renderer.cpp b/src/rendering/m2_renderer.cpp index 916b0ff0..8f134a75 100644 --- a/src/rendering/m2_renderer.cpp +++ b/src/rendering/m2_renderer.cpp @@ -2998,7 +2998,9 @@ void M2Renderer::renderShadow(VkCommandBuffer cmd, const glm::mat4& lightSpaceMa vkResetDescriptorPool(vkCtx_->getDevice(), shadowTexPool_, 0); } // Cache: texture imageView -> allocated descriptor set (avoids duplicates within frame) - std::unordered_map texSetCache; + // Reuse persistent map — pool reset already invalidated the sets. + shadowTexSetCache_.clear(); + auto& texSetCache = shadowTexSetCache_; auto getTexDescSet = [&](VkTexture* tex) -> VkDescriptorSet { VkImageView iv = tex->getImageView(); @@ -3425,13 +3427,8 @@ void M2Renderer::renderM2Ribbons(VkCommandBuffer cmd, VkDescriptorSet perFrameSe float* dst = static_cast(ribbonVBMapped_); size_t written = 0; - struct DrawCall { - VkDescriptorSet texSet; - VkPipeline pipeline; - uint32_t firstVertex; - uint32_t vertexCount; - }; - std::vector draws; + ribbonDraws_.clear(); + auto& draws = ribbonDraws_; for (const auto& inst : instances) { if (!inst.cachedModel) continue; @@ -3539,36 +3536,12 @@ void M2Renderer::renderM2Particles(VkCommandBuffer cmd, VkDescriptorSet perFrame if (!particlePipeline_ || !m2ParticleVB_) return; // Collect all particles from all instances, grouped by texture+blend - struct ParticleGroupKey { - VkTexture* texture; - uint8_t blendType; - uint16_t tilesX; - uint16_t tilesY; - - bool operator==(const ParticleGroupKey& other) const { - return texture == other.texture && - blendType == other.blendType && - tilesX == other.tilesX && - tilesY == other.tilesY; - } - }; - struct ParticleGroupKeyHash { - size_t operator()(const ParticleGroupKey& key) const { - size_t h1 = std::hash{}(reinterpret_cast(key.texture)); - size_t h2 = std::hash{}((static_cast(key.tilesX) << 16) | key.tilesY); - size_t h3 = std::hash{}(key.blendType); - return h1 ^ (h2 * 0x9e3779b9u) ^ (h3 * 0x85ebca6bu); - } - }; - struct ParticleGroup { - VkTexture* texture; - uint8_t blendType; - uint16_t tilesX; - uint16_t tilesY; - VkDescriptorSet preAllocSet = VK_NULL_HANDLE; // Pre-allocated stable set, avoids per-frame alloc - std::vector vertexData; // 9 floats per particle - }; - std::unordered_map groups; + // Reuse persistent map — clear each group's vertex data but keep bucket structure. + for (auto& [k, g] : particleGroups_) { + g.vertexData.clear(); + g.preAllocSet = VK_NULL_HANDLE; + } + auto& groups = particleGroups_; size_t totalParticles = 0; diff --git a/src/ui/game_screen.cpp b/src/ui/game_screen.cpp index 488d7d30..dd436e16 100644 --- a/src/ui/game_screen.cpp +++ b/src/ui/game_screen.cpp @@ -4271,6 +4271,8 @@ void GameScreen::renderTargetFrame(game::GameHandler& gameHandler) { gameHandler.addFriend(name); if (ImGui::MenuItem("Ignore")) gameHandler.addIgnore(name); + if (ImGui::MenuItem("Report Player")) + gameHandler.reportPlayer(tGuid, "Reported via UI"); } ImGui::Separator(); if (ImGui::BeginMenu("Set Raid Mark")) { @@ -12181,6 +12183,17 @@ void GameScreen::renderNameplates(game::GameHandler& gameHandler) { if (mouse.x >= nx0 && mouse.x <= nx1 && mouse.y >= ny0 && mouse.y <= ny1) { // Track mouseover for [target=mouseover] macro conditionals gameHandler.setMouseoverGuid(guid); + // Hover tooltip: name, level/class, guild + ImGui::BeginTooltip(); + ImGui::TextUnformatted(unitName.c_str()); + if (isPlayer) { + uint8_t cid = entityClassId(unit); + ImGui::Text("Level %u %s", level, classNameStr(cid)); + } else if (level > 0) { + ImGui::Text("Level %u", level); + } + if (!subLabel.empty()) ImGui::TextColored(ImVec4(0.7f,0.7f,0.7f,1.0f), "%s", subLabel.c_str()); + ImGui::EndTooltip(); if (ImGui::IsMouseClicked(ImGuiMouseButton_Left)) { gameHandler.setTarget(guid); } else if (ImGui::IsMouseClicked(ImGuiMouseButton_Right)) { diff --git a/src/ui/inventory_screen.cpp b/src/ui/inventory_screen.cpp index a7550a39..b0f69eb2 100644 --- a/src/ui/inventory_screen.cpp +++ b/src/ui/inventory_screen.cpp @@ -985,10 +985,15 @@ void InventoryScreen::renderSeparateBags(game::Inventory& inventory, uint64_t mo // Backpack window (bottom of stack) if (backpackOpen_) { - int bpRows = (inventory.getBackpackSize() + columns - 1) / columns; + int bpTotal = inventory.getBackpackSize(); + int bpUsed = 0; + for (int i = 0; i < bpTotal; ++i) if (!inventory.getBackpackSlot(i).empty()) ++bpUsed; + char bpTitle[64]; + snprintf(bpTitle, sizeof(bpTitle), "Backpack (%d/%d)", bpUsed, bpTotal); + int bpRows = (bpTotal + columns - 1) / columns; float bpH = bpRows * (slotSize + 4.0f) + 80.0f; // header + money + padding float defaultY = stackBottom - bpH; - renderBagWindow("Backpack", backpackOpen_, inventory, -1, stackX, defaultY, moneyCopper); + renderBagWindow(bpTitle, backpackOpen_, inventory, -1, stackX, defaultY, moneyCopper); stackBottom = defaultY - stackGap; } @@ -1010,14 +1015,16 @@ void InventoryScreen::renderSeparateBags(game::Inventory& inventory, uint64_t mo float defaultY = stackBottom - bagH; stackBottom = defaultY - stackGap; - // Build title from equipped bag item name - char title[64]; + // Build title from equipped bag item name, with used/total slot counts + int bagUsed = 0; + for (int si = 0; si < bagSize; ++si) if (!inventory.getBagSlot(bag, si).empty()) ++bagUsed; + char title[96]; game::EquipSlot bagSlot = static_cast(static_cast(game::EquipSlot::BAG1) + bag); const auto& bagItem = inventory.getEquipSlot(bagSlot); if (!bagItem.empty() && !bagItem.item.name.empty()) { - snprintf(title, sizeof(title), "%s##bag%d", bagItem.item.name.c_str(), bag); + snprintf(title, sizeof(title), "%s (%d/%d)##bag%d", bagItem.item.name.c_str(), bagUsed, bagSize, bag); } else { - snprintf(title, sizeof(title), "Bag Slot %d##bag%d", bag + 1, bag); + snprintf(title, sizeof(title), "Bag Slot %d (%d/%d)##bag%d", bag + 1, bagUsed, bagSize, bag); } renderBagWindow(title, bagOpen_[bag], inventory, bag, stackX, defaultY, 0);