From c047446fb728ceef96523a755e274e433e5aa49c Mon Sep 17 00:00:00 2001 From: Kelsi Date: Sun, 8 Feb 2026 23:15:26 -0800 Subject: [PATCH] Add dynamic memory-based asset caching and aggressive loading - Add MemoryMonitor class for dynamic cache sizing based on available RAM - Increase terrain load radius to 8 tiles (17x17 grid, 289 tiles) - Scale worker threads to 75% of logical cores (no cap) - Increase cache budget to 80% of available RAM, max file size to 50% - Increase M2 render distance: 1200 units during taxi, 800 when >2000 instances - Fix camera positioning during taxi flights (external follow mode) - Add 2-second landing cooldown to prevent re-entering taxi mode on lag - Update interval reduced to 33ms for faster streaming responsiveness Optimized for high-memory systems while scaling gracefully to lower-end hardware. Cache and render distances now fully utilize available VRAM on minimum spec GPUs. --- CMakeLists.txt | 1 + include/core/memory_monitor.hpp | 47 ++++++++++++++++++++++++ include/game/game_handler.hpp | 1 + include/pipeline/asset_manager.hpp | 4 +-- include/rendering/terrain_manager.hpp | 8 ++--- src/core/application.cpp | 4 +++ src/core/memory_monitor.cpp | 51 +++++++++++++++++++++++++++ src/game/game_handler.cpp | 9 ++++- src/pipeline/asset_manager.cpp | 13 +++++-- src/rendering/camera_controller.cpp | 45 ++++++++++++++++++++++- src/rendering/m2_renderer.cpp | 8 ++--- src/rendering/terrain_manager.cpp | 26 +++++++++++--- 12 files changed, 198 insertions(+), 19 deletions(-) create mode 100644 include/core/memory_monitor.hpp create mode 100644 src/core/memory_monitor.cpp diff --git a/CMakeLists.txt b/CMakeLists.txt index 845e9e76..4ba50a37 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -69,6 +69,7 @@ set(WOWEE_SOURCES src/core/window.cpp src/core/input.cpp src/core/logger.cpp + src/core/memory_monitor.cpp # Network src/network/socket.cpp diff --git a/include/core/memory_monitor.hpp b/include/core/memory_monitor.hpp new file mode 100644 index 00000000..5bad537e --- /dev/null +++ b/include/core/memory_monitor.hpp @@ -0,0 +1,47 @@ +#pragma once + +#include +#include + +namespace wowee { +namespace core { + +/** + * Monitors system memory and provides dynamic cache sizing + */ +class MemoryMonitor { +public: + static MemoryMonitor& getInstance(); + + /** + * Initialize memory monitoring + */ + void initialize(); + + /** + * Get total system RAM in bytes + */ + size_t getTotalRAM() const { return totalRAM_; } + + /** + * Get currently available RAM in bytes + */ + size_t getAvailableRAM() const; + + /** + * Get recommended cache budget (30% of available RAM) + */ + size_t getRecommendedCacheBudget() const; + + /** + * Check if system is under memory pressure + */ + bool isMemoryPressure() const; + +private: + MemoryMonitor() = default; + size_t totalRAM_ = 0; +}; + +} // namespace core +} // namespace wowee diff --git a/include/game/game_handler.hpp b/include/game/game_handler.hpp index 3f795268..aee926a5 100644 --- a/include/game/game_handler.hpp +++ b/include/game/game_handler.hpp @@ -965,6 +965,7 @@ private: bool taxiActivatePending_ = false; float taxiActivateTimer_ = 0.0f; bool taxiClientActive_ = false; + float taxiLandingCooldown_ = 0.0f; // Prevent re-entering taxi right after landing size_t taxiClientIndex_ = 0; std::vector taxiClientPath_; float taxiClientSpeed_ = 18.0f; // Reduced from 32 to prevent loading hitches diff --git a/include/pipeline/asset_manager.hpp b/include/pipeline/asset_manager.hpp index c3f0ce14..3a0352ca 100644 --- a/include/pipeline/asset_manager.hpp +++ b/include/pipeline/asset_manager.hpp @@ -104,7 +104,7 @@ private: mutable std::mutex readMutex; std::map> dbcCache; - // Decompressed file cache (LRU, 1GB budget for modern RAM) + // Decompressed file cache (LRU, dynamic budget based on system RAM) struct CachedFile { std::vector data; uint64_t lastAccessTime; @@ -114,7 +114,7 @@ private: mutable uint64_t fileCacheAccessCounter = 0; mutable size_t fileCacheHits = 0; mutable size_t fileCacheMisses = 0; - static constexpr size_t FILE_CACHE_BUDGET = 1024 * 1024 * 1024; // 1GB + mutable size_t fileCacheBudget = 1024 * 1024 * 1024; // Dynamic, starts at 1GB /** * Normalize path for case-insensitive lookup diff --git a/include/rendering/terrain_manager.hpp b/include/rendering/terrain_manager.hpp index 43d731f2..f0919031 100644 --- a/include/rendering/terrain_manager.hpp +++ b/include/rendering/terrain_manager.hpp @@ -272,9 +272,9 @@ private: // Streaming parameters bool streamingEnabled = true; - int loadRadius = 2; // Load tiles within this radius (5x5 grid) - int unloadRadius = 3; // Unload tiles beyond this radius - float updateInterval = 0.1f; // Check streaming every 0.1 seconds + int loadRadius = 8; // Load tiles within this radius (17x17 grid) + int unloadRadius = 12; // Unload tiles beyond this radius + float updateInterval = 0.033f; // Check streaming every 33ms (~30 fps) float timeSinceLastUpdate = 0.0f; // Tile size constants (WoW ADT specifications) @@ -300,7 +300,7 @@ private: std::unordered_map tileCache_; std::list tileCacheLru_; size_t tileCacheBytes_ = 0; - size_t tileCacheBudgetBytes_ = 8ull * 1024 * 1024 * 1024; // 8GB for modern systems + size_t tileCacheBudgetBytes_ = 8ull * 1024 * 1024 * 1024; // Dynamic, set at init based on RAM std::mutex tileCacheMutex_; std::shared_ptr getCachedTile(const TileCoord& coord); diff --git a/src/core/application.cpp b/src/core/application.cpp index bf15f228..3df7783c 100644 --- a/src/core/application.cpp +++ b/src/core/application.cpp @@ -4,6 +4,7 @@ #include #include "core/spawn_presets.hpp" #include "core/logger.hpp" +#include "core/memory_monitor.hpp" #include "rendering/renderer.hpp" #include "rendering/camera.hpp" #include "rendering/camera_controller.hpp" @@ -79,6 +80,9 @@ Application::~Application() { bool Application::initialize() { LOG_INFO("Initializing Wowee Native Client"); + // Initialize memory monitoring for dynamic cache sizing + core::MemoryMonitor::getInstance().initialize(); + // Create window WindowConfig windowConfig; windowConfig.title = "Wowee"; diff --git a/src/core/memory_monitor.cpp b/src/core/memory_monitor.cpp new file mode 100644 index 00000000..1b38227a --- /dev/null +++ b/src/core/memory_monitor.cpp @@ -0,0 +1,51 @@ +#include "core/memory_monitor.hpp" +#include "core/logger.hpp" +#include +#include +#include + +namespace wowee { +namespace core { + +MemoryMonitor& MemoryMonitor::getInstance() { + static MemoryMonitor instance; + return instance; +} + +void MemoryMonitor::initialize() { + struct sysinfo info; + if (sysinfo(&info) == 0) { + totalRAM_ = static_cast(info.totalram) * info.mem_unit; + LOG_INFO("System RAM detected: ", totalRAM_ / (1024 * 1024 * 1024), " GB"); + } else { + // Fallback: assume 16GB + totalRAM_ = 16ull * 1024 * 1024 * 1024; + LOG_WARNING("Could not detect system RAM, assuming 16GB"); + } +} + +size_t MemoryMonitor::getAvailableRAM() const { + struct sysinfo info; + if (sysinfo(&info) == 0) { + // Available = free + buffers + cached + return static_cast(info.freeram) * info.mem_unit; + } + return totalRAM_ / 2; // Fallback: assume 50% available +} + +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; +} + +bool MemoryMonitor::isMemoryPressure() const { + size_t available = getAvailableRAM(); + // Memory pressure if < 20% RAM available + return available < (totalRAM_ * 20 / 100); +} + +} // namespace core +} // namespace wowee diff --git a/src/game/game_handler.cpp b/src/game/game_handler.cpp index 299f35dd..2b332ce8 100644 --- a/src/game/game_handler.cpp +++ b/src/game/game_handler.cpp @@ -182,6 +182,11 @@ void GameHandler::update(float deltaTime) { // Update combat text (Phase 2) updateCombatText(deltaTime); + // Update taxi landing cooldown + if (taxiLandingCooldown_ > 0.0f) { + taxiLandingCooldown_ -= deltaTime; + } + // Detect taxi flight landing: UNIT_FLAG_TAXI_FLIGHT (0x00000100) cleared if (onTaxiFlight_) { updateClientTaxi(deltaTime); @@ -194,6 +199,7 @@ void GameHandler::update(float deltaTime) { auto unit = std::static_pointer_cast(playerEntity); if ((unit->getUnitFlags() & 0x00000100) == 0) { onTaxiFlight_ = false; + taxiLandingCooldown_ = 2.0f; // 2 second cooldown to prevent re-entering if (taxiMountActive_ && mountCallback_) { mountCallback_(0); } @@ -1670,7 +1676,7 @@ void GameHandler::handleUpdateObject(network::Packet& packet) { } if (block.guid == playerGuid) { constexpr uint32_t UNIT_FLAG_TAXI_FLIGHT = 0x00000100; - if ((unit->getUnitFlags() & UNIT_FLAG_TAXI_FLIGHT) != 0 && !onTaxiFlight_) { + if ((unit->getUnitFlags() & UNIT_FLAG_TAXI_FLIGHT) != 0 && !onTaxiFlight_ && taxiLandingCooldown_ <= 0.0f) { onTaxiFlight_ = true; applyTaxiMountForCurrentNode(); } @@ -5242,6 +5248,7 @@ void GameHandler::updateClientTaxi(float deltaTime) { if (taxiClientIndex_ + 1 >= taxiClientPath_.size()) { taxiClientActive_ = false; onTaxiFlight_ = false; + taxiLandingCooldown_ = 2.0f; // 2 second cooldown to prevent re-entering if (taxiMountActive_ && mountCallback_) { mountCallback_(0); } diff --git a/src/pipeline/asset_manager.cpp b/src/pipeline/asset_manager.cpp index f174e899..22e0497c 100644 --- a/src/pipeline/asset_manager.cpp +++ b/src/pipeline/asset_manager.cpp @@ -1,5 +1,6 @@ #include "pipeline/asset_manager.hpp" #include "core/logger.hpp" +#include "core/memory_monitor.hpp" #include namespace wowee { @@ -25,8 +26,14 @@ bool AssetManager::initialize(const std::string& dataPath_) { return false; } + // Set dynamic file cache budget based on available RAM + auto& memMonitor = core::MemoryMonitor::getInstance(); + size_t recommendedBudget = memMonitor.getRecommendedCacheBudget(); + fileCacheBudget = recommendedBudget / 2; // Split budget: half for file cache, half for other caches + initialized = true; - LOG_INFO("Asset manager initialized successfully (1GB file cache enabled)"); + LOG_INFO("Asset manager initialized (dynamic file cache: ", + fileCacheBudget / (1024 * 1024), " MB, adjusts based on RAM)"); return true; } @@ -169,9 +176,9 @@ std::vector AssetManager::readFile(const std::string& path) const { // Add to cache if within budget size_t fileSize = data.size(); - if (fileSize > 0 && fileSize < FILE_CACHE_BUDGET / 10) { // Don't cache files > 100MB + if (fileSize > 0 && fileSize < fileCacheBudget / 2) { // Don't cache files > 50% of budget (very aggressive) // Evict old entries if needed (LRU) - while (fileCacheTotalBytes + fileSize > FILE_CACHE_BUDGET && !fileCache.empty()) { + while (fileCacheTotalBytes + fileSize > fileCacheBudget && !fileCache.empty()) { // Find least recently used entry auto lru = fileCache.begin(); for (auto it = fileCache.begin(); it != fileCache.end(); ++it) { diff --git a/src/rendering/camera_controller.cpp b/src/rendering/camera_controller.cpp index c2195985..c1cb3d6a 100644 --- a/src/rendering/camera_controller.cpp +++ b/src/rendering/camera_controller.cpp @@ -80,8 +80,51 @@ void CameraController::update(float deltaTime) { return; } - // Skip all collision/movement logic during taxi flights (position controlled externally) + // During taxi flights, skip input/movement logic but still position camera if (externalFollow_) { + // Mouse look (right mouse button) + if (rightMouseDown) { + int mouseDX, mouseDY; + SDL_GetRelativeMouseState(&mouseDX, &mouseDY); + yaw -= mouseDX * mouseSensitivity; + pitch -= mouseDY * mouseSensitivity; + pitch = glm::clamp(pitch, -89.0f, 89.0f); + camera->setRotation(yaw, pitch); + } + + // Position camera behind character during taxi + if (thirdPerson && followTarget) { + glm::vec3 targetPos = *followTarget; + glm::vec3 forward3D = camera->getForward(); + + // Pivot point at upper chest/neck + float mountedOffset = mounted_ ? mountHeightOffset_ : 0.0f; + glm::vec3 pivot = targetPos + glm::vec3(0.0f, 0.0f, PIVOT_HEIGHT + mountedOffset); + + // Camera direction from yaw/pitch + glm::vec3 camDir = -forward3D; + + // Use current distance + float actualDist = std::min(currentDistance, collisionDistance); + + // Compute camera position + glm::vec3 actualCam; + if (actualDist < MIN_DISTANCE + 0.1f) { + actualCam = pivot + forward3D * 0.1f; + } else { + actualCam = pivot + camDir * actualDist; + } + + // Smooth camera position + if (glm::length(smoothedCamPos) < 0.01f) { + smoothedCamPos = actualCam; + } + float camLerp = 1.0f - std::exp(-CAM_SMOOTH_SPEED * deltaTime); + smoothedCamPos += (actualCam - smoothedCamPos) * camLerp; + + camera->setPosition(smoothedCamPos); + } + return; } diff --git a/src/rendering/m2_renderer.cpp b/src/rendering/m2_renderer.cpp index 8b882a57..10544e1b 100644 --- a/src/rendering/m2_renderer.cpp +++ b/src/rendering/m2_renderer.cpp @@ -1379,7 +1379,7 @@ void M2Renderer::update(float deltaTime, const glm::vec3& cameraPos, const glm:: // Cache camera state for frustum-culling bone computation cachedCamPos_ = cameraPos; - const float maxRenderDistance = (instances.size() > 600) ? 320.0f : 2800.0f; + const float maxRenderDistance = (instances.size() > 2000) ? 800.0f : 2800.0f; cachedMaxRenderDistSq_ = maxRenderDistance * maxRenderDistance; // Build frustum for culling bones @@ -1643,9 +1643,9 @@ void M2Renderer::render(const Camera& camera, const glm::mat4& view, const glm:: lastDrawCallCount = 0; - // Adaptive render distance: keep longer tree/foliage visibility to reduce pop-in. - // During taxi, use very short render distance to prevent loading hitches - const float maxRenderDistance = onTaxi_ ? 150.0f : (instances.size() > 600) ? 320.0f : 2800.0f; + // Adaptive render distance: aggressive settings to utilize VRAM fully + // During taxi, render far to upload models/textures to VRAM cache + const float maxRenderDistance = onTaxi_ ? 1200.0f : (instances.size() > 2000) ? 800.0f : 2800.0f; const float maxRenderDistanceSq = maxRenderDistance * maxRenderDistance; const float fadeStartFraction = 0.75f; const glm::vec3 camPos = camera.getPosition(); diff --git a/src/rendering/terrain_manager.cpp b/src/rendering/terrain_manager.cpp index 014628be..df9d4d61 100644 --- a/src/rendering/terrain_manager.cpp +++ b/src/rendering/terrain_manager.cpp @@ -5,6 +5,7 @@ #include "rendering/wmo_renderer.hpp" #include "rendering/camera.hpp" #include "core/coordinates.hpp" +#include "core/memory_monitor.hpp" #include "pipeline/asset_manager.hpp" #include "pipeline/adt_loader.hpp" #include "pipeline/m2_loader.hpp" @@ -113,10 +114,21 @@ bool TerrainManager::initialize(pipeline::AssetManager* assets, TerrainRenderer* return false; } - // Start background worker pool + // Set dynamic tile cache budget (use other half of recommended budget) + auto& memMonitor = core::MemoryMonitor::getInstance(); + tileCacheBudgetBytes_ = memMonitor.getRecommendedCacheBudget() / 2; + LOG_INFO("Terrain tile cache budget: ", tileCacheBudgetBytes_ / (1024 * 1024), " MB (dynamic)"); + + // Start background worker pool (dynamic: scales with available cores) + // Use 75% of logical cores for decompression, leaving headroom for render/OS workerRunning.store(true); unsigned hc = std::thread::hardware_concurrency(); - workerCount = static_cast(hc > 0 ? std::min(4u, std::max(2u, hc - 1)) : 2u); + if (hc > 0) { + unsigned targetWorkers = std::max(6u, (hc * 3) / 4); // 75% of cores, minimum 6 + workerCount = static_cast(targetWorkers); + } else { + workerCount = 6; // Fallback + } workerThreads.reserve(workerCount); for (int i = 0; i < workerCount; i++) { workerThreads.emplace_back(&TerrainManager::workerLoop, this); @@ -917,10 +929,16 @@ void TerrainManager::unloadAll() { m2Renderer->clear(); } - // Restart worker threads so streaming can resume + // Restart worker threads so streaming can resume (dynamic: scales with available cores) + // Use 75% of logical cores for decompression, leaving headroom for render/OS workerRunning.store(true); unsigned hc = std::thread::hardware_concurrency(); - workerCount = static_cast(hc > 0 ? std::min(4u, std::max(2u, hc - 1)) : 2u); + if (hc > 0) { + unsigned targetWorkers = std::max(6u, (hc * 3) / 4); // 75% of cores, minimum 6 + workerCount = static_cast(targetWorkers); + } else { + workerCount = 6; // Fallback + } workerThreads.reserve(workerCount); for (int i = 0; i < workerCount; i++) { workerThreads.emplace_back(&TerrainManager::workerLoop, this);