diff --git a/include/rendering/terrain_manager.hpp b/include/rendering/terrain_manager.hpp index 237c1f8c..7ef03322 100644 --- a/include/rendering/terrain_manager.hpp +++ b/include/rendering/terrain_manager.hpp @@ -4,6 +4,7 @@ #include "pipeline/terrain_mesh.hpp" #include "pipeline/m2_loader.hpp" #include "pipeline/wmo_loader.hpp" +#include "pipeline/blp_loader.hpp" #include #include #include @@ -103,6 +104,10 @@ struct PendingTile { glm::mat4 modelMatrix; // Pre-computed world transform }; std::vector wmoDoodads; + + // Pre-loaded terrain texture BLP data (loaded on background thread to avoid + // blocking file I/O on the main thread during finalizeTile) + std::unordered_map preloadedTextures; }; /** diff --git a/include/rendering/terrain_renderer.hpp b/include/rendering/terrain_renderer.hpp index 4e2a1fbf..4e39922a 100644 --- a/include/rendering/terrain_renderer.hpp +++ b/include/rendering/terrain_renderer.hpp @@ -1,6 +1,7 @@ #pragma once #include "pipeline/terrain_mesh.hpp" +#include "pipeline/blp_loader.hpp" #include "rendering/shader.hpp" #include "rendering/texture.hpp" #include "rendering/camera.hpp" @@ -87,6 +88,12 @@ public: */ void removeTile(int tileX, int tileY); + /** + * Upload pre-loaded BLP textures to the GL texture cache. + * Called before loadTerrain() so texture loading avoids file I/O. + */ + void uploadPreloadedTextures(const std::unordered_map& textures); + /** * Render loaded terrain * @param camera Camera for view/projection matrices diff --git a/src/game/game_handler.cpp b/src/game/game_handler.cpp index 07fadad2..e0316776 100644 --- a/src/game/game_handler.cpp +++ b/src/game/game_handler.cpp @@ -30,13 +30,12 @@ GameHandler::GameHandler() { // Default spells always available knownSpells.push_back(6603); // Attack - knownSpells.push_back(8690); // Hearthstone // Default action bar layout actionBar[0].type = ActionBarSlot::SPELL; actionBar[0].id = 6603; // Attack in slot 1 - actionBar[11].type = ActionBarSlot::SPELL; - actionBar[11].id = 8690; // Hearthstone in slot 12 + actionBar[11].type = ActionBarSlot::ITEM; + actionBar[11].id = 6948; // Hearthstone item in slot 12 } GameHandler::~GameHandler() { @@ -3788,13 +3787,10 @@ void GameHandler::handleInitialSpells(network::Packet& packet) { knownSpells = data.spellIds; - // Ensure Attack (6603) and Hearthstone (8690) are always present + // Ensure Attack (6603) is always present if (std::find(knownSpells.begin(), knownSpells.end(), 6603u) == knownSpells.end()) { knownSpells.insert(knownSpells.begin(), 6603u); } - if (std::find(knownSpells.begin(), knownSpells.end(), 8690u) == knownSpells.end()) { - knownSpells.push_back(8690u); - } // Set initial cooldowns for (const auto& cd : data.cooldowns) { @@ -3803,11 +3799,11 @@ void GameHandler::handleInitialSpells(network::Packet& packet) { } } - // Load saved action bar or use defaults (Attack slot 1, Hearthstone slot 12) + // Load saved action bar or use defaults (Attack slot 1, Hearthstone item slot 12) actionBar[0].type = ActionBarSlot::SPELL; actionBar[0].id = 6603; // Attack - actionBar[11].type = ActionBarSlot::SPELL; - actionBar[11].id = 8690; // Hearthstone + actionBar[11].type = ActionBarSlot::ITEM; + actionBar[11].id = 6948; // Hearthstone item loadCharacterConfig(); LOG_INFO("Learned ", knownSpells.size(), " spells"); diff --git a/src/rendering/camera_controller.cpp b/src/rendering/camera_controller.cpp index 7a891cb4..3cd9f840 100644 --- a/src/rendering/camera_controller.cpp +++ b/src/rendering/camera_controller.cpp @@ -465,6 +465,11 @@ void CameraController::update(float deltaTime) { if (!walkable) { candidate.x = adjusted.x; candidate.y = adjusted.y; + } else if (floorH && *floorH > candidate.z) { + // Snap Z to ramp surface so subsequent sweep + // steps measure feetZ from the ramp, not the + // starting position. + candidate.z = *floorH; } } } diff --git a/src/rendering/terrain_manager.cpp b/src/rendering/terrain_manager.cpp index 12db5c6f..2eac5483 100644 --- a/src/rendering/terrain_manager.cpp +++ b/src/rendering/terrain_manager.cpp @@ -465,11 +465,19 @@ std::unique_ptr TerrainManager::prepareTile(int x, int y) { } } + // Pre-load terrain texture BLP data on background thread so finalizeTile + // doesn't block the main thread with file I/O. + for (const auto& texPath : pending->terrain.textures) { + if (pending->preloadedTextures.find(texPath) != pending->preloadedTextures.end()) continue; + pending->preloadedTextures[texPath] = assetManager->loadTexture(texPath); + } + LOG_DEBUG("Prepared tile [", x, ",", y, "]: ", pending->m2Models.size(), " M2 models, ", pending->m2Placements.size(), " M2 placements, ", pending->wmoModels.size(), " WMOs, ", - pending->wmoDoodads.size(), " WMO doodads"); + pending->wmoDoodads.size(), " WMO doodads, ", + pending->preloadedTextures.size(), " textures"); return pending; } @@ -489,6 +497,11 @@ void TerrainManager::finalizeTile(std::unique_ptr pending) { return; } + // Upload pre-loaded textures to the GL cache so loadTerrain avoids file I/O + if (!pending->preloadedTextures.empty()) { + terrainRenderer->uploadPreloadedTextures(pending->preloadedTextures); + } + // Upload terrain to GPU if (!terrainRenderer->loadTerrain(pending->mesh, pending->terrain.textures, x, y)) { LOG_ERROR("Failed to upload terrain to GPU for tile [", x, ",", y, "]"); @@ -657,9 +670,9 @@ void TerrainManager::workerLoop() { } void TerrainManager::processReadyTiles() { - // Process up to 2 ready tiles per frame to spread GPU work + // Process up to 1 ready tile per frame to avoid main-thread stalls int processed = 0; - const int maxPerFrame = 2; + const int maxPerFrame = 1; while (processed < maxPerFrame) { std::unique_ptr pending; diff --git a/src/rendering/terrain_renderer.cpp b/src/rendering/terrain_renderer.cpp index b5b62e7b..cb6b2ca4 100644 --- a/src/rendering/terrain_renderer.cpp +++ b/src/rendering/terrain_renderer.cpp @@ -252,6 +252,33 @@ GLuint TerrainRenderer::loadTexture(const std::string& path) { return textureID; } +void TerrainRenderer::uploadPreloadedTextures(const std::unordered_map& textures) { + for (const auto& [path, blp] : textures) { + // Skip if already cached + if (textureCache.find(path) != textureCache.end()) continue; + if (!blp.isValid()) { + textureCache[path] = whiteTexture; + continue; + } + + GLuint textureID; + glGenTextures(1, &textureID); + glBindTexture(GL_TEXTURE_2D, textureID); + glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, + blp.width, blp.height, 0, + GL_RGBA, GL_UNSIGNED_BYTE, blp.data.data()); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR_MIPMAP_LINEAR); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_REPEAT); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_REPEAT); + glGenerateMipmap(GL_TEXTURE_2D); + applyAnisotropicFiltering(); + glBindTexture(GL_TEXTURE_2D, 0); + + textureCache[path] = textureID; + } +} + GLuint TerrainRenderer::createAlphaTexture(const std::vector& alphaData) { if (alphaData.empty()) { return 0;