From 19eb7a1fb793dfa918a5a7cb32389ba97f4a7529 Mon Sep 17 00:00:00 2001 From: Kelsi Date: Tue, 10 Mar 2026 22:26:50 -0700 Subject: [PATCH] fix: animation stutter, resolution crash, memory cap, spell tooltip hints, GO collision MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Animation stutter: skip playAnimation(Run) for the local player in the server movement callback — the player renderer state machine already manages it; resetting animTime on every movement packet caused visible stutter - Resolution crash: reorder swapchain recreation so old swapchain is only destroyed after confirming the new build succeeded; add null-swapchain guard in beginFrame to survive the retry window - Memory cap: reduce cache budget from 80% uncapped to 50% hard-capped at 16 GB to prevent excessive RAM use on high-memory systems - Spell tooltip: suppress "Drag to action bar / Double-click to cast" hints when the tooltip is shown from the action bar (showUsageHints=false) - M2 collision: add watermelon/melon/squash/gourd to foliage (no-collision); exclude chair/bench/stool/seat/throne from smallSolidProp so invisible chair bounding boxes no longer trap the player --- include/ui/spellbook_screen.hpp | 4 ++-- src/core/application.cpp | 30 ++++++++++++++++++++---------- src/core/memory_monitor.cpp | 12 ++++++------ src/pipeline/asset_manager.cpp | 2 +- src/rendering/m2_renderer.cpp | 14 +++++++++++++- src/rendering/vk_context.cpp | 20 ++++++++++++++------ src/ui/spellbook_screen.cpp | 8 ++++---- 7 files changed, 60 insertions(+), 30 deletions(-) diff --git a/include/ui/spellbook_screen.hpp b/include/ui/spellbook_screen.hpp index 77f1c2d6..470cb233 100644 --- a/include/ui/spellbook_screen.hpp +++ b/include/ui/spellbook_screen.hpp @@ -94,8 +94,8 @@ private: VkDescriptorSet getSpellIcon(uint32_t iconId, pipeline::AssetManager* assetManager); const SpellInfo* getSpellInfo(uint32_t spellId) const; - // Tooltip rendering helper - void renderSpellTooltip(const SpellInfo* info, game::GameHandler& gameHandler); + // Tooltip rendering helper (showUsageHints=false when called from action bar) + void renderSpellTooltip(const SpellInfo* info, game::GameHandler& gameHandler, bool showUsageHints = true); }; } // namespace ui diff --git a/src/core/application.cpp b/src/core/application.cpp index b265d45c..8a7b51e4 100644 --- a/src/core/application.cpp +++ b/src/core/application.cpp @@ -2553,13 +2553,19 @@ void Application::setupUICallbacks() { // Don't override Death animation (1). The per-frame sync loop will return to // Stand when movement stops. if (durationMs > 0) { - uint32_t curAnimId = 0; float curT = 0.0f, curDur = 0.0f; - auto* cr = renderer->getCharacterRenderer(); - bool gotState = cr->getAnimationState(instanceId, curAnimId, curT, curDur); - if (!gotState || curAnimId != 1 /*Death*/) { - cr->playAnimation(instanceId, 5u, /*loop=*/true); + // Player animation is managed by the local renderer state machine — + // don't reset it here or every server movement packet restarts the + // run cycle from frame 0, causing visible stutter. + if (!isPlayer) { + uint32_t curAnimId = 0; float curT = 0.0f, curDur = 0.0f; + auto* cr = renderer->getCharacterRenderer(); + bool gotState = cr->getAnimationState(instanceId, curAnimId, curT, curDur); + // Only start Run if not already running and not in Death animation. + if (!gotState || (curAnimId != 1 /*Death*/ && curAnimId != 5u /*Run*/)) { + cr->playAnimation(instanceId, 5u, /*loop=*/true); + } + creatureWasMoving_[guid] = true; } - if (!isPlayer) creatureWasMoving_[guid] = true; } } }); @@ -8701,17 +8707,21 @@ void Application::updateQuestMarkers() { int markerType = -1; // -1 = no marker using game::QuestGiverStatus; + float markerGrayscale = 0.0f; // 0 = colour, 1 = grey (trivial quests) switch (status) { case QuestGiverStatus::AVAILABLE: + markerType = 0; // Yellow ! + break; case QuestGiverStatus::AVAILABLE_LOW: - markerType = 0; // Available (yellow !) + markerType = 0; // Grey ! (same texture, desaturated in shader) + markerGrayscale = 1.0f; break; case QuestGiverStatus::REWARD: case QuestGiverStatus::REWARD_REP: - markerType = 1; // Turn-in (yellow ?) + markerType = 1; // Yellow ? break; case QuestGiverStatus::INCOMPLETE: - markerType = 2; // Incomplete (grey ?) + markerType = 2; // Grey ? break; default: break; @@ -8745,7 +8755,7 @@ void Application::updateQuestMarkers() { } // Set the marker (renderer will handle positioning, bob, glow, etc.) - questMarkerRenderer->setMarker(guid, renderPos, markerType, boundingHeight); + questMarkerRenderer->setMarker(guid, renderPos, markerType, boundingHeight, markerGrayscale); markersAdded++; } diff --git a/src/core/memory_monitor.cpp b/src/core/memory_monitor.cpp index 913240fd..080a1ef6 100644 --- a/src/core/memory_monitor.cpp +++ b/src/core/memory_monitor.cpp @@ -109,16 +109,16 @@ size_t MemoryMonitor::getAvailableRAM() const { size_t MemoryMonitor::getRecommendedCacheBudget() const { size_t available = getAvailableRAM(); - // Use 80% of available RAM for caches (very aggressive), but cap at 90% of total - size_t budget = available * 80 / 100; - size_t maxBudget = totalRAM_ * 90 / 100; - return budget < maxBudget ? budget : maxBudget; + // Use 50% of available RAM for caches, hard-capped at 16 GB. + static constexpr size_t kHardCapBytes = 16ull * 1024 * 1024 * 1024; // 16 GB + size_t budget = available * 50 / 100; + return budget < kHardCapBytes ? budget : kHardCapBytes; } bool MemoryMonitor::isMemoryPressure() const { size_t available = getAvailableRAM(); - // Memory pressure if < 20% RAM available - return available < (totalRAM_ * 20 / 100); + // Memory pressure if < 10% RAM available + return available < (totalRAM_ * 10 / 100); } } // namespace core diff --git a/src/pipeline/asset_manager.cpp b/src/pipeline/asset_manager.cpp index 89b063c5..469df669 100644 --- a/src/pipeline/asset_manager.cpp +++ b/src/pipeline/asset_manager.cpp @@ -92,7 +92,7 @@ void AssetManager::setupFileCacheBudget() { const size_t envMaxMB = parseEnvSizeMB("WOWEE_FILE_CACHE_MAX_MB"); const size_t minBudgetBytes = 256ull * 1024ull * 1024ull; - const size_t defaultMaxBudgetBytes = 32768ull * 1024ull * 1024ull; + const size_t defaultMaxBudgetBytes = 12288ull * 1024ull * 1024ull; // 12 GB max for file cache const size_t maxBudgetBytes = (envMaxMB > 0) ? (envMaxMB * 1024ull * 1024ull) : defaultMaxBudgetBytes; diff --git a/src/rendering/m2_renderer.cpp b/src/rendering/m2_renderer.cpp index 2c25cd77..48b2d346 100644 --- a/src/rendering/m2_renderer.cpp +++ b/src/rendering/m2_renderer.cpp @@ -979,8 +979,16 @@ bool M2Renderer::loadModel(const pipeline::M2Model& model, uint32_t modelId) { (lowerName.find("monument") != std::string::npos) || (lowerName.find("sculpture") != std::string::npos); gpuModel.collisionStatue = statueName; + // Sittable furniture: chairs/benches/stools cause players to get stuck against + // invisible bounding boxes; WMOs already handle room collision. + bool sittableFurnitureName = + (lowerName.find("chair") != std::string::npos) || + (lowerName.find("bench") != std::string::npos) || + (lowerName.find("stool") != std::string::npos) || + (lowerName.find("seat") != std::string::npos) || + (lowerName.find("throne") != std::string::npos); bool smallSolidPropName = - statueName || + (statueName && !sittableFurnitureName) || (lowerName.find("crate") != std::string::npos) || (lowerName.find("box") != std::string::npos) || (lowerName.find("chest") != std::string::npos) || @@ -1023,6 +1031,10 @@ bool M2Renderer::loadModel(const pipeline::M2Model& model, uint32_t modelId) { (lowerName.find("bamboo") != std::string::npos) || (lowerName.find("banana") != std::string::npos) || (lowerName.find("coconut") != std::string::npos) || + (lowerName.find("watermelon") != std::string::npos) || + (lowerName.find("melon") != std::string::npos) || + (lowerName.find("squash") != std::string::npos) || + (lowerName.find("gourd") != std::string::npos) || (lowerName.find("canopy") != std::string::npos) || (lowerName.find("hedge") != std::string::npos) || (lowerName.find("cactus") != std::string::npos) || diff --git a/src/rendering/vk_context.cpp b/src/rendering/vk_context.cpp index dc4144fa..fdd07d8e 100644 --- a/src/rendering/vk_context.cpp +++ b/src/rendering/vk_context.cpp @@ -1051,14 +1051,21 @@ bool VkContext::recreateSwapchain(int width, int height) { auto swapRet = builder.build(); - if (oldSwapchain) { - vkDestroySwapchainKHR(device, oldSwapchain, nullptr); + if (!swapRet) { + // Destroy old swapchain now that we failed (it can't be used either) + if (oldSwapchain) { + vkDestroySwapchainKHR(device, oldSwapchain, nullptr); + swapchain = VK_NULL_HANDLE; + } + LOG_ERROR("Failed to recreate swapchain: ", swapRet.error().message()); + // Keep swapchainDirty=true so the next frame retries + swapchainDirty = true; + return false; } - if (!swapRet) { - LOG_ERROR("Failed to recreate swapchain: ", swapRet.error().message()); - swapchain = VK_NULL_HANDLE; - return false; + // Success — safe to retire the old swapchain + if (oldSwapchain) { + vkDestroySwapchainKHR(device, oldSwapchain, nullptr); } auto vkbSwap = swapRet.value(); @@ -1322,6 +1329,7 @@ bool VkContext::recreateSwapchain(int width, int height) { VkCommandBuffer VkContext::beginFrame(uint32_t& imageIndex) { if (deviceLost_) return VK_NULL_HANDLE; + if (swapchain == VK_NULL_HANDLE) return VK_NULL_HANDLE; // Swapchain lost; recreate pending auto& frame = frames[currentFrame]; diff --git a/src/ui/spellbook_screen.cpp b/src/ui/spellbook_screen.cpp index f90090f7..0a355ff3 100644 --- a/src/ui/spellbook_screen.cpp +++ b/src/ui/spellbook_screen.cpp @@ -189,7 +189,7 @@ bool SpellbookScreen::renderSpellInfoTooltip(uint32_t spellId, game::GameHandler if (!dbcLoadAttempted) loadSpellDBC(assetManager); const SpellInfo* info = getSpellInfo(spellId); if (!info) return false; - renderSpellTooltip(info, gameHandler); + renderSpellTooltip(info, gameHandler, /*showUsageHints=*/false); return true; } @@ -446,7 +446,7 @@ const SpellInfo* SpellbookScreen::getSpellInfo(uint32_t spellId) const { return (it != spellData.end()) ? &it->second : nullptr; } -void SpellbookScreen::renderSpellTooltip(const SpellInfo* info, game::GameHandler& gameHandler) { +void SpellbookScreen::renderSpellTooltip(const SpellInfo* info, game::GameHandler& gameHandler, bool showUsageHints) { ImGui::BeginTooltip(); ImGui::PushTextWrapPos(320.0f); @@ -551,8 +551,8 @@ void SpellbookScreen::renderSpellTooltip(const SpellInfo* info, game::GameHandle ImGui::TextWrapped("%s", info->description.c_str()); } - // Usage hints - if (!info->isPassive()) { + // Usage hints — only shown when browsing the spellbook, not on action bar hover + if (!info->isPassive() && showUsageHints) { ImGui::Spacing(); ImGui::TextColored(ImVec4(0.3f, 1.0f, 0.3f, 1.0f), "Drag to action bar"); ImGui::TextColored(ImVec4(0.3f, 1.0f, 0.3f, 1.0f), "Double-click to cast");