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.
This commit is contained in:
Kelsi 2026-02-08 23:15:26 -08:00
parent 27d0496894
commit c047446fb7
12 changed files with 198 additions and 19 deletions

View file

@ -69,6 +69,7 @@ set(WOWEE_SOURCES
src/core/window.cpp src/core/window.cpp
src/core/input.cpp src/core/input.cpp
src/core/logger.cpp src/core/logger.cpp
src/core/memory_monitor.cpp
# Network # Network
src/network/socket.cpp src/network/socket.cpp

View file

@ -0,0 +1,47 @@
#pragma once
#include <cstddef>
#include <cstdint>
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

View file

@ -965,6 +965,7 @@ private:
bool taxiActivatePending_ = false; bool taxiActivatePending_ = false;
float taxiActivateTimer_ = 0.0f; float taxiActivateTimer_ = 0.0f;
bool taxiClientActive_ = false; bool taxiClientActive_ = false;
float taxiLandingCooldown_ = 0.0f; // Prevent re-entering taxi right after landing
size_t taxiClientIndex_ = 0; size_t taxiClientIndex_ = 0;
std::vector<glm::vec3> taxiClientPath_; std::vector<glm::vec3> taxiClientPath_;
float taxiClientSpeed_ = 18.0f; // Reduced from 32 to prevent loading hitches float taxiClientSpeed_ = 18.0f; // Reduced from 32 to prevent loading hitches

View file

@ -104,7 +104,7 @@ private:
mutable std::mutex readMutex; mutable std::mutex readMutex;
std::map<std::string, std::shared_ptr<DBCFile>> dbcCache; std::map<std::string, std::shared_ptr<DBCFile>> dbcCache;
// Decompressed file cache (LRU, 1GB budget for modern RAM) // Decompressed file cache (LRU, dynamic budget based on system RAM)
struct CachedFile { struct CachedFile {
std::vector<uint8_t> data; std::vector<uint8_t> data;
uint64_t lastAccessTime; uint64_t lastAccessTime;
@ -114,7 +114,7 @@ private:
mutable uint64_t fileCacheAccessCounter = 0; mutable uint64_t fileCacheAccessCounter = 0;
mutable size_t fileCacheHits = 0; mutable size_t fileCacheHits = 0;
mutable size_t fileCacheMisses = 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 * Normalize path for case-insensitive lookup

View file

@ -272,9 +272,9 @@ private:
// Streaming parameters // Streaming parameters
bool streamingEnabled = true; bool streamingEnabled = true;
int loadRadius = 2; // Load tiles within this radius (5x5 grid) int loadRadius = 8; // Load tiles within this radius (17x17 grid)
int unloadRadius = 3; // Unload tiles beyond this radius int unloadRadius = 12; // Unload tiles beyond this radius
float updateInterval = 0.1f; // Check streaming every 0.1 seconds float updateInterval = 0.033f; // Check streaming every 33ms (~30 fps)
float timeSinceLastUpdate = 0.0f; float timeSinceLastUpdate = 0.0f;
// Tile size constants (WoW ADT specifications) // Tile size constants (WoW ADT specifications)
@ -300,7 +300,7 @@ private:
std::unordered_map<TileCoord, CachedTile, TileCoord::Hash> tileCache_; std::unordered_map<TileCoord, CachedTile, TileCoord::Hash> tileCache_;
std::list<TileCoord> tileCacheLru_; std::list<TileCoord> tileCacheLru_;
size_t tileCacheBytes_ = 0; 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::mutex tileCacheMutex_;
std::shared_ptr<PendingTile> getCachedTile(const TileCoord& coord); std::shared_ptr<PendingTile> getCachedTile(const TileCoord& coord);

View file

@ -4,6 +4,7 @@
#include <cmath> #include <cmath>
#include "core/spawn_presets.hpp" #include "core/spawn_presets.hpp"
#include "core/logger.hpp" #include "core/logger.hpp"
#include "core/memory_monitor.hpp"
#include "rendering/renderer.hpp" #include "rendering/renderer.hpp"
#include "rendering/camera.hpp" #include "rendering/camera.hpp"
#include "rendering/camera_controller.hpp" #include "rendering/camera_controller.hpp"
@ -79,6 +80,9 @@ Application::~Application() {
bool Application::initialize() { bool Application::initialize() {
LOG_INFO("Initializing Wowee Native Client"); LOG_INFO("Initializing Wowee Native Client");
// Initialize memory monitoring for dynamic cache sizing
core::MemoryMonitor::getInstance().initialize();
// Create window // Create window
WindowConfig windowConfig; WindowConfig windowConfig;
windowConfig.title = "Wowee"; windowConfig.title = "Wowee";

View file

@ -0,0 +1,51 @@
#include "core/memory_monitor.hpp"
#include "core/logger.hpp"
#include <fstream>
#include <string>
#include <sys/sysinfo.h>
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<size_t>(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<size_t>(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

View file

@ -182,6 +182,11 @@ void GameHandler::update(float deltaTime) {
// Update combat text (Phase 2) // Update combat text (Phase 2)
updateCombatText(deltaTime); updateCombatText(deltaTime);
// Update taxi landing cooldown
if (taxiLandingCooldown_ > 0.0f) {
taxiLandingCooldown_ -= deltaTime;
}
// Detect taxi flight landing: UNIT_FLAG_TAXI_FLIGHT (0x00000100) cleared // Detect taxi flight landing: UNIT_FLAG_TAXI_FLIGHT (0x00000100) cleared
if (onTaxiFlight_) { if (onTaxiFlight_) {
updateClientTaxi(deltaTime); updateClientTaxi(deltaTime);
@ -194,6 +199,7 @@ void GameHandler::update(float deltaTime) {
auto unit = std::static_pointer_cast<Unit>(playerEntity); auto unit = std::static_pointer_cast<Unit>(playerEntity);
if ((unit->getUnitFlags() & 0x00000100) == 0) { if ((unit->getUnitFlags() & 0x00000100) == 0) {
onTaxiFlight_ = false; onTaxiFlight_ = false;
taxiLandingCooldown_ = 2.0f; // 2 second cooldown to prevent re-entering
if (taxiMountActive_ && mountCallback_) { if (taxiMountActive_ && mountCallback_) {
mountCallback_(0); mountCallback_(0);
} }
@ -1670,7 +1676,7 @@ void GameHandler::handleUpdateObject(network::Packet& packet) {
} }
if (block.guid == playerGuid) { if (block.guid == playerGuid) {
constexpr uint32_t UNIT_FLAG_TAXI_FLIGHT = 0x00000100; 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; onTaxiFlight_ = true;
applyTaxiMountForCurrentNode(); applyTaxiMountForCurrentNode();
} }
@ -5242,6 +5248,7 @@ void GameHandler::updateClientTaxi(float deltaTime) {
if (taxiClientIndex_ + 1 >= taxiClientPath_.size()) { if (taxiClientIndex_ + 1 >= taxiClientPath_.size()) {
taxiClientActive_ = false; taxiClientActive_ = false;
onTaxiFlight_ = false; onTaxiFlight_ = false;
taxiLandingCooldown_ = 2.0f; // 2 second cooldown to prevent re-entering
if (taxiMountActive_ && mountCallback_) { if (taxiMountActive_ && mountCallback_) {
mountCallback_(0); mountCallback_(0);
} }

View file

@ -1,5 +1,6 @@
#include "pipeline/asset_manager.hpp" #include "pipeline/asset_manager.hpp"
#include "core/logger.hpp" #include "core/logger.hpp"
#include "core/memory_monitor.hpp"
#include <algorithm> #include <algorithm>
namespace wowee { namespace wowee {
@ -25,8 +26,14 @@ bool AssetManager::initialize(const std::string& dataPath_) {
return false; 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; 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; return true;
} }
@ -169,9 +176,9 @@ std::vector<uint8_t> AssetManager::readFile(const std::string& path) const {
// Add to cache if within budget // Add to cache if within budget
size_t fileSize = data.size(); 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) // 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 // Find least recently used entry
auto lru = fileCache.begin(); auto lru = fileCache.begin();
for (auto it = fileCache.begin(); it != fileCache.end(); ++it) { for (auto it = fileCache.begin(); it != fileCache.end(); ++it) {

View file

@ -80,8 +80,51 @@ void CameraController::update(float deltaTime) {
return; 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_) { 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; return;
} }

View file

@ -1379,7 +1379,7 @@ void M2Renderer::update(float deltaTime, const glm::vec3& cameraPos, const glm::
// Cache camera state for frustum-culling bone computation // Cache camera state for frustum-culling bone computation
cachedCamPos_ = cameraPos; 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; cachedMaxRenderDistSq_ = maxRenderDistance * maxRenderDistance;
// Build frustum for culling bones // Build frustum for culling bones
@ -1643,9 +1643,9 @@ void M2Renderer::render(const Camera& camera, const glm::mat4& view, const glm::
lastDrawCallCount = 0; lastDrawCallCount = 0;
// Adaptive render distance: keep longer tree/foliage visibility to reduce pop-in. // Adaptive render distance: aggressive settings to utilize VRAM fully
// During taxi, use very short render distance to prevent loading hitches // During taxi, render far to upload models/textures to VRAM cache
const float maxRenderDistance = onTaxi_ ? 150.0f : (instances.size() > 600) ? 320.0f : 2800.0f; const float maxRenderDistance = onTaxi_ ? 1200.0f : (instances.size() > 2000) ? 800.0f : 2800.0f;
const float maxRenderDistanceSq = maxRenderDistance * maxRenderDistance; const float maxRenderDistanceSq = maxRenderDistance * maxRenderDistance;
const float fadeStartFraction = 0.75f; const float fadeStartFraction = 0.75f;
const glm::vec3 camPos = camera.getPosition(); const glm::vec3 camPos = camera.getPosition();

View file

@ -5,6 +5,7 @@
#include "rendering/wmo_renderer.hpp" #include "rendering/wmo_renderer.hpp"
#include "rendering/camera.hpp" #include "rendering/camera.hpp"
#include "core/coordinates.hpp" #include "core/coordinates.hpp"
#include "core/memory_monitor.hpp"
#include "pipeline/asset_manager.hpp" #include "pipeline/asset_manager.hpp"
#include "pipeline/adt_loader.hpp" #include "pipeline/adt_loader.hpp"
#include "pipeline/m2_loader.hpp" #include "pipeline/m2_loader.hpp"
@ -113,10 +114,21 @@ bool TerrainManager::initialize(pipeline::AssetManager* assets, TerrainRenderer*
return false; 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); workerRunning.store(true);
unsigned hc = std::thread::hardware_concurrency(); unsigned hc = std::thread::hardware_concurrency();
workerCount = static_cast<int>(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<int>(targetWorkers);
} else {
workerCount = 6; // Fallback
}
workerThreads.reserve(workerCount); workerThreads.reserve(workerCount);
for (int i = 0; i < workerCount; i++) { for (int i = 0; i < workerCount; i++) {
workerThreads.emplace_back(&TerrainManager::workerLoop, this); workerThreads.emplace_back(&TerrainManager::workerLoop, this);
@ -917,10 +929,16 @@ void TerrainManager::unloadAll() {
m2Renderer->clear(); 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); workerRunning.store(true);
unsigned hc = std::thread::hardware_concurrency(); unsigned hc = std::thread::hardware_concurrency();
workerCount = static_cast<int>(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<int>(targetWorkers);
} else {
workerCount = 6; // Fallback
}
workerThreads.reserve(workerCount); workerThreads.reserve(workerCount);
for (int i = 0; i < workerCount; i++) { for (int i = 0; i < workerCount; i++) {
workerThreads.emplace_back(&TerrainManager::workerLoop, this); workerThreads.emplace_back(&TerrainManager::workerLoop, this);