perf: eliminate ~70 unnecessary sqrt ops per frame, optimize caches and threading

Squared distance optimizations across 30 files:
- Convert glm::length() comparisons to glm::dot() (no sqrt)
- Use glm::inversesqrt() for check-then-normalize patterns (1 rsqrt vs 2 sqrt)
- Defer sqrt to after early-out checks in collision/movement code
- Hottest paths: camera_controller (21), weather particles, WMO collision,
  transport movement, creature interpolation, nameplate culling

Container and algorithm improvements:
- std::map<string> → std::unordered_map for asset/DBC/MPQ/warden caches
- std::mutex → std::shared_mutex for asset_manager and mpq_manager caches
- std::sort → std::partial_sort in lighting_manager (top-2 of N volumes)
- Double-lookup find()+operator[] → insert_or_assign in game_handler
- Add reserve() for per-frame vectors: weather, swim_effects, WMO/M2 collision

Threading and synchronization:
- Replace 1ms busy-wait polling with condition_variable in character_renderer
- Move timestamp capture before mutex in logger
- Use memory_order_acquire/release for normal map completion signaling

API additions:
- DBC getStringView()/getStringViewByOffset() for zero-copy string access
- Parse creature display IDs from SMSG_CREATURE_QUERY_SINGLE_RESPONSE
This commit is contained in:
Kelsi 2026-03-27 16:33:16 -07:00
parent cf0e2aa240
commit b0466e9029
29 changed files with 328 additions and 196 deletions

View file

@ -5,6 +5,7 @@
#include <string> #include <string>
#include <memory> #include <memory>
#include <map> #include <map>
#include <unordered_map>
#include <functional> #include <functional>
// Forward declare unicorn types (will include in .cpp) // Forward declare unicorn types (will include in .cpp)
@ -148,7 +149,7 @@ private:
uint32_t apiStubBase_; // API stub base address uint32_t apiStubBase_; // API stub base address
// API hooks: DLL name -> Function name -> stub address // API hooks: DLL name -> Function name -> stub address
std::map<std::string, std::map<std::string, uint32_t>> apiAddresses_; std::unordered_map<std::string, std::unordered_map<std::string, uint32_t>> apiAddresses_;
// API stub dispatch: stub address -> {argCount, handler} // API stub dispatch: stub address -> {argCount, handler}
struct ApiHookEntry { struct ApiHookEntry {

View file

@ -1502,9 +1502,10 @@ struct CreatureQueryResponseData {
std::string subName; std::string subName;
std::string iconName; std::string iconName;
uint32_t typeFlags = 0; uint32_t typeFlags = 0;
uint32_t creatureType = 0; uint32_t creatureType = 0; // 1=Beast, 2=Dragonkin, 3=Demon, 4=Elemental, 5=Giant, 6=Undead, 7=Humanoid, ...
uint32_t family = 0; uint32_t family = 0;
uint32_t rank = 0; // 0=Normal, 1=Elite, 2=Rare Elite, 3=Boss, 4=Rare uint32_t rank = 0; // 0=Normal, 1=Elite, 2=Rare Elite, 3=Boss, 4=Rare
uint32_t displayId[4] = {}; // Up to 4 random display models (0 = unused)
bool isValid() const { return entry != 0 && !name.empty(); } bool isValid() const { return entry != 0 && !name.empty(); }
}; };

View file

@ -8,7 +8,9 @@
#include <string> #include <string>
#include <vector> #include <vector>
#include <map> #include <map>
#include <unordered_map>
#include <mutex> #include <mutex>
#include <shared_mutex>
namespace wowee { namespace wowee {
namespace pipeline { namespace pipeline {
@ -164,15 +166,15 @@ private:
*/ */
std::string resolveFile(const std::string& normalizedPath) const; std::string resolveFile(const std::string& normalizedPath) const;
mutable std::mutex cacheMutex; mutable std::shared_mutex cacheMutex;
std::map<std::string, std::shared_ptr<DBCFile>> dbcCache; std::unordered_map<std::string, std::shared_ptr<DBCFile>> dbcCache;
// File cache (LRU, dynamic budget based on system RAM) // 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;
}; };
mutable std::map<std::string, CachedFile> fileCache; mutable std::unordered_map<std::string, CachedFile> fileCache;
mutable size_t fileCacheTotalBytes = 0; mutable size_t fileCacheTotalBytes = 0;
mutable uint64_t fileCacheAccessCounter = 0; mutable uint64_t fileCacheAccessCounter = 0;
mutable size_t fileCacheHits = 0; mutable size_t fileCacheHits = 0;

View file

@ -3,6 +3,7 @@
#include <vector> #include <vector>
#include <map> #include <map>
#include <string> #include <string>
#include <string_view>
#include <cstdint> #include <cstdint>
#include <memory> #include <memory>
@ -92,6 +93,11 @@ public:
*/ */
std::string getString(uint32_t recordIndex, uint32_t fieldIndex) const; std::string getString(uint32_t recordIndex, uint32_t fieldIndex) const;
/**
* Get a string field as a view (no allocation; valid while this DBCFile lives)
*/
std::string_view getStringView(uint32_t recordIndex, uint32_t fieldIndex) const;
/** /**
* Get string by offset in string block * Get string by offset in string block
* @param offset Offset into string block * @param offset Offset into string block
@ -99,6 +105,11 @@ public:
*/ */
std::string getStringByOffset(uint32_t offset) const; std::string getStringByOffset(uint32_t offset) const;
/**
* Get string by offset as a view (no allocation; valid while this DBCFile lives)
*/
std::string_view getStringViewByOffset(uint32_t offset) const;
/** /**
* Find a record by ID (assumes first field is ID) * Find a record by ID (assumes first field is ID)
* @param id Record ID to find * @param id Record ID to find

View file

@ -8,6 +8,7 @@
#include <unordered_map> #include <unordered_map>
#include <unordered_set> #include <unordered_set>
#include <mutex> #include <mutex>
#include <shared_mutex>
// Forward declare StormLib handle // Forward declare StormLib handle
typedef void* HANDLE; typedef void* HANDLE;
@ -115,7 +116,7 @@ private:
// //
// Important: caching misses can blow up memory if the game probes many unique non-existent filenames. // Important: caching misses can blow up memory if the game probes many unique non-existent filenames.
// Miss caching is disabled by default and must be explicitly enabled. // Miss caching is disabled by default and must be explicitly enabled.
mutable std::mutex fileArchiveCacheMutex_; mutable std::shared_mutex fileArchiveCacheMutex_;
mutable std::unordered_map<std::string, HANDLE> fileArchiveCache_; mutable std::unordered_map<std::string, HANDLE> fileArchiveCache_;
size_t fileArchiveCacheMaxEntries_ = 500000; size_t fileArchiveCacheMaxEntries_ = 500000;
bool fileArchiveCacheMisses_ = false; bool fileArchiveCacheMisses_ = false;

View file

@ -13,6 +13,7 @@
#include <utility> #include <utility>
#include <future> #include <future>
#include <deque> #include <deque>
#include <condition_variable>
#include <mutex> #include <mutex>
#include <atomic> #include <atomic>
@ -325,6 +326,7 @@ private:
}; };
// Completed results ready for GPU upload (populated by background threads) // Completed results ready for GPU upload (populated by background threads)
std::mutex normalMapResultsMutex_; std::mutex normalMapResultsMutex_;
std::condition_variable normalMapDoneCV_; // signaled when pendingNormalMapCount_ reaches 0
std::deque<NormalMapResult> completedNormalMaps_; std::deque<NormalMapResult> completedNormalMaps_;
std::atomic<int> pendingNormalMapCount_{0}; // in-flight background tasks std::atomic<int> pendingNormalMapCount_{0}; // in-flight background tasks

View file

@ -1600,8 +1600,9 @@ void Application::update(float deltaTime) {
// Keep facing toward target and emit charge effect // Keep facing toward target and emit charge effect
glm::vec3 dir = chargeEndPos_ - chargeStartPos_; glm::vec3 dir = chargeEndPos_ - chargeStartPos_;
if (glm::length(dir) > 0.01f) { float dirLenSq = glm::dot(dir, dir);
dir = glm::normalize(dir); if (dirLenSq > 1e-4f) {
dir *= glm::inversesqrt(dirLenSq);
float yawDeg = glm::degrees(std::atan2(dir.x, dir.y)); float yawDeg = glm::degrees(std::atan2(dir.x, dir.y));
renderer->setCharacterYaw(yawDeg); renderer->setCharacterYaw(yawDeg);
renderer->emitChargeEffect(renderPos, dir); renderer->emitChargeEffect(renderPos, dir);
@ -1634,10 +1635,10 @@ void Application::update(float deltaTime) {
glm::vec3 targetCanonical(targetEntity->getX(), targetEntity->getY(), targetEntity->getZ()); glm::vec3 targetCanonical(targetEntity->getX(), targetEntity->getY(), targetEntity->getZ());
glm::vec3 targetRender = core::coords::canonicalToRender(targetCanonical); glm::vec3 targetRender = core::coords::canonicalToRender(targetCanonical);
glm::vec3 toTarget = targetRender - renderPos; glm::vec3 toTarget = targetRender - renderPos;
float d = glm::length(toTarget); float dSq = glm::dot(toTarget, toTarget);
if (d > 1.5f) { if (dSq > 2.25f) {
// Place us 1.5 units from target (well within 8-unit melee range) // Place us 1.5 units from target (well within 8-unit melee range)
glm::vec3 snapPos = targetRender - glm::normalize(toTarget) * 1.5f; glm::vec3 snapPos = targetRender - toTarget * (1.5f * glm::inversesqrt(dSq));
renderer->getCharacterPosition() = snapPos; renderer->getCharacterPosition() = snapPos;
glm::vec3 snapCanonical = core::coords::renderToCanonical(snapPos); glm::vec3 snapCanonical = core::coords::renderToCanonical(snapPos);
gameHandler->setPosition(snapCanonical.x, snapCanonical.y, snapCanonical.z); gameHandler->setPosition(snapCanonical.x, snapCanonical.y, snapCanonical.z);
@ -1925,13 +1926,14 @@ void Application::update(float deltaTime) {
creatureRenderPosCache_[guid] = renderPos; creatureRenderPosCache_[guid] = renderPos;
} else { } else {
const glm::vec3 prevPos = posIt->second; const glm::vec3 prevPos = posIt->second;
const glm::vec2 delta2(renderPos.x - prevPos.x, renderPos.y - prevPos.y); float ddx2 = renderPos.x - prevPos.x;
float planarDist = glm::length(delta2); float ddy2 = renderPos.y - prevPos.y;
float planarDistSq = ddx2 * ddx2 + ddy2 * ddy2;
float dz = std::abs(renderPos.z - prevPos.z); float dz = std::abs(renderPos.z - prevPos.z);
auto unitPtr = std::static_pointer_cast<game::Unit>(entity); auto unitPtr = std::static_pointer_cast<game::Unit>(entity);
const bool deadOrCorpse = unitPtr->getHealth() == 0; const bool deadOrCorpse = unitPtr->getHealth() == 0;
const bool largeCorrection = (planarDist > 6.0f) || (dz > 3.0f); const bool largeCorrection = (planarDistSq > 36.0f) || (dz > 3.0f);
// isEntityMoving() reflects server-authoritative move state set by // isEntityMoving() reflects server-authoritative move state set by
// startMoveTo() in handleMonsterMove, regardless of distance-cull. // startMoveTo() in handleMonsterMove, regardless of distance-cull.
// This correctly detects movement for distant creatures (> 150u) // This correctly detects movement for distant creatures (> 150u)
@ -1941,11 +1943,13 @@ void Application::update(float deltaTime) {
// destination, rather than persisting through the dead- // destination, rather than persisting through the dead-
// reckoning overrun window. // reckoning overrun window.
const bool entityIsMoving = entity->isActivelyMoving(); const bool entityIsMoving = entity->isActivelyMoving();
const bool isMovingNow = !deadOrCorpse && (entityIsMoving || planarDist > 0.03f || dz > 0.08f); constexpr float kMoveThreshSq = 0.03f * 0.03f;
const bool isMovingNow = !deadOrCorpse && (entityIsMoving || planarDistSq > kMoveThreshSq || dz > 0.08f);
if (deadOrCorpse || largeCorrection) { if (deadOrCorpse || largeCorrection) {
charRenderer->setInstancePosition(instanceId, renderPos); charRenderer->setInstancePosition(instanceId, renderPos);
} else if (planarDist > 0.03f || dz > 0.08f) { } else if (planarDistSq > kMoveThreshSq || dz > 0.08f) {
// Position changed in entity coords → drive renderer toward it. // Position changed in entity coords → drive renderer toward it.
float planarDist = std::sqrt(planarDistSq);
float duration = std::clamp(planarDist / 5.5f, 0.05f, 0.22f); float duration = std::clamp(planarDist / 5.5f, 0.05f, 0.22f);
charRenderer->moveInstanceTo(instanceId, renderPos, duration); charRenderer->moveInstanceTo(instanceId, renderPos, duration);
} }
@ -2045,19 +2049,22 @@ void Application::update(float deltaTime) {
creatureRenderPosCache_[guid] = renderPos; creatureRenderPosCache_[guid] = renderPos;
} else { } else {
const glm::vec3 prevPos = posIt->second; const glm::vec3 prevPos = posIt->second;
const glm::vec2 delta2(renderPos.x - prevPos.x, renderPos.y - prevPos.y); float ddx2 = renderPos.x - prevPos.x;
float planarDist = glm::length(delta2); float ddy2 = renderPos.y - prevPos.y;
float planarDistSq = ddx2 * ddx2 + ddy2 * ddy2;
float dz = std::abs(renderPos.z - prevPos.z); float dz = std::abs(renderPos.z - prevPos.z);
auto unitPtr = std::static_pointer_cast<game::Unit>(entity); auto unitPtr = std::static_pointer_cast<game::Unit>(entity);
const bool deadOrCorpse = unitPtr->getHealth() == 0; const bool deadOrCorpse = unitPtr->getHealth() == 0;
const bool largeCorrection = (planarDist > 6.0f) || (dz > 3.0f); const bool largeCorrection = (planarDistSq > 36.0f) || (dz > 3.0f);
const bool entityIsMoving = entity->isActivelyMoving(); const bool entityIsMoving = entity->isActivelyMoving();
const bool isMovingNow = !deadOrCorpse && (entityIsMoving || planarDist > 0.03f || dz > 0.08f); constexpr float kMoveThreshSq2 = 0.03f * 0.03f;
const bool isMovingNow = !deadOrCorpse && (entityIsMoving || planarDistSq > kMoveThreshSq2 || dz > 0.08f);
if (deadOrCorpse || largeCorrection) { if (deadOrCorpse || largeCorrection) {
charRenderer->setInstancePosition(instanceId, renderPos); charRenderer->setInstancePosition(instanceId, renderPos);
} else if (planarDist > 0.03f || dz > 0.08f) { } else if (planarDistSq > kMoveThreshSq2 || dz > 0.08f) {
float planarDist = std::sqrt(planarDistSq);
float duration = std::clamp(planarDist / 5.5f, 0.05f, 0.22f); float duration = std::clamp(planarDist / 5.5f, 0.05f, 0.22f);
charRenderer->moveInstanceTo(instanceId, renderPos, duration); charRenderer->moveInstanceTo(instanceId, renderPos, duration);
} }
@ -2810,9 +2817,10 @@ void Application::setupUICallbacks() {
// Compute direction and stop 2.0 units short (melee reach) // Compute direction and stop 2.0 units short (melee reach)
glm::vec3 dir = targetRender - startRender; glm::vec3 dir = targetRender - startRender;
float dist = glm::length(dir); float distSq = glm::dot(dir, dir);
if (dist < 3.0f) return; // Too close, nothing to do if (distSq < 9.0f) return; // Too close, nothing to do
glm::vec3 dirNorm = dir / dist; float invDist = glm::inversesqrt(distSq);
glm::vec3 dirNorm = dir * invDist;
glm::vec3 endRender = targetRender - dirNorm * 2.0f; glm::vec3 endRender = targetRender - dirNorm * 2.0f;
// Face toward target BEFORE starting charge // Face toward target BEFORE starting charge
@ -2827,7 +2835,7 @@ void Application::setupUICallbacks() {
// Set charge state // Set charge state
chargeActive_ = true; chargeActive_ = true;
chargeTimer_ = 0.0f; chargeTimer_ = 0.0f;
chargeDuration_ = std::max(dist / 25.0f, 0.3f); // ~25 units/sec chargeDuration_ = std::max(std::sqrt(distSq) / 25.0f, 0.3f); // ~25 units/sec
chargeStartPos_ = startRender; chargeStartPos_ = startRender;
chargeEndPos_ = endRender; chargeEndPos_ = endRender;
chargeTargetGuid_ = targetGuid; chargeTargetGuid_ = targetGuid;
@ -8859,7 +8867,7 @@ void Application::processPendingTransportRegistrations() {
pendingTransportMoves_.erase(moveIt); pendingTransportMoves_.erase(moveIt);
} }
if (glm::length(canonicalSpawnPos) < 1.0f) { if (glm::dot(canonicalSpawnPos, canonicalSpawnPos) < 1.0f) {
auto goData = gameHandler->getCachedGameObjectInfo(pending.entry); auto goData = gameHandler->getCachedGameObjectInfo(pending.entry);
if (goData && goData->type == 15 && goData->hasData && goData->data[0] != 0) { if (goData && goData->type == 15 && goData->hasData && goData->data[0] != 0) {
uint32_t taxiPathId = goData->data[0]; uint32_t taxiPathId = goData->data[0];

View file

@ -124,10 +124,11 @@ void Logger::log(LogLevel level, const std::string& message) {
return; return;
} }
// Capture timestamp before acquiring lock to minimize critical section
auto nowSteady = std::chrono::steady_clock::now();
std::lock_guard<std::mutex> lock(mutex); std::lock_guard<std::mutex> lock(mutex);
ensureFile(); ensureFile();
auto nowSteady = std::chrono::steady_clock::now();
if (dedupeEnabled_ && !lastMessage_.empty() && if (dedupeEnabled_ && !lastMessage_.empty() &&
level == lastLevel_ && message == lastMessage_) { level == lastLevel_ && message == lastMessage_) {
auto elapsedMs = std::chrono::duration_cast<std::chrono::milliseconds>(nowSteady - lastMessageTime_).count(); auto elapsedMs = std::chrono::duration_cast<std::chrono::milliseconds>(nowSteady - lastMessageTime_).count();

View file

@ -11264,8 +11264,7 @@ void GameHandler::applyUpdateObjectBlock(const UpdateBlock& block, bool& newItem
if (sock1EnchIt != block.fields.end()) info.socketEnchantIds[0] = sock1EnchIt->second; if (sock1EnchIt != block.fields.end()) info.socketEnchantIds[0] = sock1EnchIt->second;
if (sock2EnchIt != block.fields.end()) info.socketEnchantIds[1] = sock2EnchIt->second; if (sock2EnchIt != block.fields.end()) info.socketEnchantIds[1] = sock2EnchIt->second;
if (sock3EnchIt != block.fields.end()) info.socketEnchantIds[2] = sock3EnchIt->second; if (sock3EnchIt != block.fields.end()) info.socketEnchantIds[2] = sock3EnchIt->second;
bool isNew = (onlineItems_.find(block.guid) == onlineItems_.end()); auto [itemIt, isNew] = onlineItems_.insert_or_assign(block.guid, info);
onlineItems_[block.guid] = info;
if (isNew) newItemCreated = true; if (isNew) newItemCreated = true;
queryItemInfo(info.entry, block.guid); queryItemInfo(info.entry, block.guid);
} }
@ -22953,11 +22952,11 @@ void GameHandler::startClientTaxiPath(const std::vector<uint32_t>& pathNodes) {
// Set initial orientation to face the first non-degenerate flight segment. // Set initial orientation to face the first non-degenerate flight segment.
glm::vec3 start = taxiClientPath_[0]; glm::vec3 start = taxiClientPath_[0];
glm::vec3 dir(0.0f); glm::vec3 dir(0.0f);
float dirLen = 0.0f; float dirLenSq = 0.0f;
for (size_t i = 1; i < taxiClientPath_.size(); i++) { for (size_t i = 1; i < taxiClientPath_.size(); i++) {
dir = taxiClientPath_[i] - start; dir = taxiClientPath_[i] - start;
dirLen = glm::length(dir); dirLenSq = glm::dot(dir, dir);
if (dirLen >= 0.001f) { if (dirLenSq >= 1e-6f) {
break; break;
} }
} }
@ -22966,11 +22965,11 @@ void GameHandler::startClientTaxiPath(const std::vector<uint32_t>& pathNodes) {
float initialRenderYaw = movementInfo.orientation; float initialRenderYaw = movementInfo.orientation;
float initialPitch = 0.0f; float initialPitch = 0.0f;
float initialRoll = 0.0f; float initialRoll = 0.0f;
if (dirLen >= 0.001f) { if (dirLenSq >= 1e-6f) {
initialOrientation = std::atan2(dir.y, dir.x); initialOrientation = std::atan2(dir.y, dir.x);
glm::vec3 renderDir = core::coords::canonicalToRender(dir); glm::vec3 renderDir = core::coords::canonicalToRender(dir);
initialRenderYaw = std::atan2(renderDir.y, renderDir.x); initialRenderYaw = std::atan2(renderDir.y, renderDir.x);
glm::vec3 dirNorm = dir / dirLen; glm::vec3 dirNorm = dir * glm::inversesqrt(dirLenSq);
initialPitch = std::asin(std::clamp(dirNorm.z, -1.0f, 1.0f)); initialPitch = std::asin(std::clamp(dirNorm.z, -1.0f, 1.0f));
} }
@ -23056,12 +23055,13 @@ void GameHandler::updateClientTaxi(float deltaTime) {
start = taxiClientPath_[taxiClientIndex_]; start = taxiClientPath_[taxiClientIndex_];
end = taxiClientPath_[taxiClientIndex_ + 1]; end = taxiClientPath_[taxiClientIndex_ + 1];
dir = end - start; dir = end - start;
segmentLen = glm::length(dir); float segLenSq = glm::dot(dir, dir);
if (segmentLen < 0.01f) { if (segLenSq < 1e-4f) {
taxiClientIndex_++; taxiClientIndex_++;
continue; continue;
} }
segmentLen = std::sqrt(segLenSq);
if (remainingDistance >= segmentLen) { if (remainingDistance >= segmentLen) {
remainingDistance -= segmentLen; remainingDistance -= segmentLen;
@ -23099,13 +23099,13 @@ void GameHandler::updateClientTaxi(float deltaTime) {
2.0f * (2.0f * p0 - 5.0f * p1 + 4.0f * p2 - p3) * t + 2.0f * (2.0f * p0 - 5.0f * p1 + 4.0f * p2 - p3) * t +
3.0f * (-p0 + 3.0f * p1 - 3.0f * p2 + p3) * t2 3.0f * (-p0 + 3.0f * p1 - 3.0f * p2 + p3) * t2
); );
float tangentLen = glm::length(tangent); float tangentLenSq = glm::dot(tangent, tangent);
if (tangentLen < 0.0001f) { if (tangentLenSq < 1e-8f) {
tangent = dir; tangent = dir;
tangentLen = glm::length(tangent); tangentLenSq = glm::dot(tangent, tangent);
if (tangentLen < 0.0001f) { if (tangentLenSq < 1e-8f) {
tangent = glm::vec3(std::cos(movementInfo.orientation), std::sin(movementInfo.orientation), 0.0f); tangent = glm::vec3(std::cos(movementInfo.orientation), std::sin(movementInfo.orientation), 0.0f);
tangentLen = glm::length(tangent); tangentLenSq = 1.0f; // unit vector
} }
} }
@ -23113,7 +23113,7 @@ void GameHandler::updateClientTaxi(float deltaTime) {
float targetOrientation = std::atan2(tangent.y, tangent.x); float targetOrientation = std::atan2(tangent.y, tangent.x);
// Calculate pitch from vertical component (altitude change) // Calculate pitch from vertical component (altitude change)
glm::vec3 tangentNorm = tangent / std::max(tangentLen, 0.0001f); glm::vec3 tangentNorm = tangent * glm::inversesqrt(std::max(tangentLenSq, 1e-8f));
float pitch = std::asin(std::clamp(tangentNorm.z, -1.0f, 1.0f)); float pitch = std::asin(std::clamp(tangentNorm.z, -1.0f, 1.0f));
// Calculate roll (banking) from rate of yaw change // Calculate roll (banking) from rate of yaw change

View file

@ -66,7 +66,7 @@ void TransportManager::registerTransport(uint64_t guid, uint32_t wmoInstanceId,
// TransportAnimation paths are local offsets; first waypoint is expected near origin. // TransportAnimation paths are local offsets; first waypoint is expected near origin.
// Warn only if the local path itself looks suspicious. // Warn only if the local path itself looks suspicious.
glm::vec3 firstWaypoint = path.points[0].pos; glm::vec3 firstWaypoint = path.points[0].pos;
if (glm::length(firstWaypoint) > 10.0f) { if (glm::dot(firstWaypoint, firstWaypoint) > 100.0f) {
LOG_WARNING("Transport 0x", std::hex, guid, std::dec, " path ", pathId, LOG_WARNING("Transport 0x", std::hex, guid, std::dec, " path ", pathId,
": first local waypoint far from origin: (", ": first local waypoint far from origin: (",
firstWaypoint.x, ",", firstWaypoint.y, ",", firstWaypoint.z, ")"); firstWaypoint.x, ",", firstWaypoint.y, ",", firstWaypoint.z, ")");
@ -492,18 +492,18 @@ glm::quat TransportManager::orientationFromTangent(const TransportPath& path, ui
); );
// Normalize tangent // Normalize tangent
float tangentLength = glm::length(tangent); float tangentLenSq = glm::dot(tangent, tangent);
if (tangentLength < 0.001f) { if (tangentLenSq < 1e-6f) {
// Fallback to simple direction // Fallback to simple direction
tangent = p2 - p1; tangent = p2 - p1;
tangentLength = glm::length(tangent); tangentLenSq = glm::dot(tangent, tangent);
} }
if (tangentLength < 0.001f) { if (tangentLenSq < 1e-6f) {
return glm::quat(1.0f, 0.0f, 0.0f, 0.0f); // Identity return glm::quat(1.0f, 0.0f, 0.0f, 0.0f); // Identity
} }
tangent /= tangentLength; tangent *= glm::inversesqrt(tangentLenSq);
// Calculate rotation from forward direction // Calculate rotation from forward direction
glm::vec3 forward = tangent; glm::vec3 forward = tangent;
@ -565,7 +565,7 @@ void TransportManager::updateServerTransport(uint64_t guid, const glm::vec3& pos
const bool isWorldCoordPath = (hasPath && pathIt->second.worldCoords && pathIt->second.durationMs > 0); const bool isWorldCoordPath = (hasPath && pathIt->second.worldCoords && pathIt->second.durationMs > 0);
// Don't let (0,0,0) server updates override a TaxiPathNode world-coordinate path // Don't let (0,0,0) server updates override a TaxiPathNode world-coordinate path
if (isWorldCoordPath && glm::length(position) < 1.0f) { if (isWorldCoordPath && glm::dot(position, position) < 1.0f) {
transport->serverUpdateCount++; transport->serverUpdateCount++;
transport->lastServerUpdate = elapsedTime_; transport->lastServerUpdate = elapsedTime_;
transport->serverYaw = orientation; transport->serverYaw = orientation;
@ -583,12 +583,13 @@ void TransportManager::updateServerTransport(uint64_t guid, const glm::vec3& pos
if (isZOnlyPath || isWorldCoordPath) { if (isZOnlyPath || isWorldCoordPath) {
transport->useClientAnimation = true; transport->useClientAnimation = true;
} else if (transport->useClientAnimation && hasPath && pathIt->second.fromDBC) { } else if (transport->useClientAnimation && hasPath && pathIt->second.fromDBC) {
float posDelta = glm::length(position - transport->position); glm::vec3 pd = position - transport->position;
if (posDelta > 1.0f) { float posDeltaSq = glm::dot(pd, pd);
if (posDeltaSq > 1.0f) {
// Server sent a meaningfully different position — it's actively driving this transport // Server sent a meaningfully different position — it's actively driving this transport
transport->useClientAnimation = false; transport->useClientAnimation = false;
LOG_INFO("Transport 0x", std::hex, guid, std::dec, LOG_INFO("Transport 0x", std::hex, guid, std::dec,
" switching to server-driven (posDelta=", posDelta, ")"); " switching to server-driven (posDeltaSq=", posDeltaSq, ")");
} }
// Otherwise keep client animation (server just echoed spawn pos or sent small jitter) // Otherwise keep client animation (server just echoed spawn pos or sent small jitter)
} else if (!hasPath || !pathIt->second.fromDBC) { } else if (!hasPath || !pathIt->second.fromDBC) {
@ -632,16 +633,16 @@ void TransportManager::updateServerTransport(uint64_t guid, const glm::vec3& pos
const float dt = elapsedTime_ - prevUpdateTime; const float dt = elapsedTime_ - prevUpdateTime;
if (dt > 0.001f) { if (dt > 0.001f) {
glm::vec3 v = (position - prevPos) / dt; glm::vec3 v = (position - prevPos) / dt;
const float speed = glm::length(v); float speedSq = glm::dot(v, v);
constexpr float kMinAuthoritativeSpeed = 0.15f; constexpr float kMinAuthoritativeSpeed = 0.15f;
constexpr float kMaxSpeed = 60.0f; constexpr float kMaxSpeed = 60.0f;
if (speed >= kMinAuthoritativeSpeed) { if (speedSq >= kMinAuthoritativeSpeed * kMinAuthoritativeSpeed) {
// Auto-detect 180-degree yaw mismatch by comparing heading to movement direction. // Auto-detect 180-degree yaw mismatch by comparing heading to movement direction.
// Some transports appear to report yaw opposite their actual travel direction. // Some transports appear to report yaw opposite their actual travel direction.
glm::vec2 horizontalV(v.x, v.y); glm::vec2 horizontalV(v.x, v.y);
float hLen = glm::length(horizontalV); float hLenSq = glm::dot(horizontalV, horizontalV);
if (hLen > 0.2f) { if (hLenSq > 0.04f) {
horizontalV /= hLen; horizontalV *= glm::inversesqrt(hLenSq);
glm::vec2 heading(std::cos(transport->serverYaw), std::sin(transport->serverYaw)); glm::vec2 heading(std::cos(transport->serverYaw), std::sin(transport->serverYaw));
float alignDot = glm::dot(heading, horizontalV); float alignDot = glm::dot(heading, horizontalV);
@ -665,8 +666,8 @@ void TransportManager::updateServerTransport(uint64_t guid, const glm::vec3& pos
} }
} }
if (speed > kMaxSpeed) { if (speedSq > kMaxSpeed * kMaxSpeed) {
v *= (kMaxSpeed / speed); v *= (kMaxSpeed * glm::inversesqrt(speedSq));
} }
transport->serverLinearVelocity = v; transport->serverLinearVelocity = v;
@ -738,10 +739,10 @@ void TransportManager::updateServerTransport(uint64_t guid, const glm::vec3& pos
float dtSeg = static_cast<float>(t1 - t0) / 1000.0f; float dtSeg = static_cast<float>(t1 - t0) / 1000.0f;
if (dtSeg <= 0.001f) return; if (dtSeg <= 0.001f) return;
glm::vec3 v = seg / dtSeg; glm::vec3 v = seg / dtSeg;
float speed = glm::length(v); float speedSq = glm::dot(v, v);
if (speed < kMinBootstrapSpeed) return; if (speedSq < kMinBootstrapSpeed * kMinBootstrapSpeed) return;
if (speed > kMaxSpeed) { if (speedSq > kMaxSpeed * kMaxSpeed) {
v *= (kMaxSpeed / speed); v *= (kMaxSpeed * glm::inversesqrt(speedSq));
} }
transport->serverLinearVelocity = v; transport->serverLinearVelocity = v;
transport->serverAngularVelocity = 0.0f; transport->serverAngularVelocity = 0.0f;
@ -1136,7 +1137,7 @@ bool TransportManager::assignTaxiPathToTransport(uint32_t entry, uint32_t taxiPa
// Find transport(s) with matching entry that are at (0,0,0) // Find transport(s) with matching entry that are at (0,0,0)
for (auto& [guid, transport] : transports_) { for (auto& [guid, transport] : transports_) {
if (transport.entry != entry) continue; if (transport.entry != entry) continue;
if (glm::length(transport.position) > 1.0f) continue; // Already has real position if (glm::dot(transport.position, transport.position) > 1.0f) continue; // Already has real position
// Copy the taxi path into the main paths_ map (indexed by entry for this transport) // Copy the taxi path into the main paths_ map (indexed by entry for this transport)
TransportPath path = taxiIt->second; TransportPath path = taxiIt->second;

View file

@ -2724,11 +2724,26 @@ bool CreatureQueryResponseParser::parse(network::Packet& packet, CreatureQueryRe
data.family = packet.readUInt32(); data.family = packet.readUInt32();
data.rank = packet.readUInt32(); data.rank = packet.readUInt32();
// Skip remaining fields (kill credits, display IDs, modifiers, quest items, etc.) // killCredit[2] + displayId[4] = 6 × 4 = 24 bytes
// We've got what we need for display purposes if (!packet.hasRemaining(24)) {
LOG_WARNING("SMSG_CREATURE_QUERY_RESPONSE: truncated before displayIds (entry=", data.entry, ")");
LOG_DEBUG("Creature query response: ", data.name, " (type=", data.creatureType,
" rank=", data.rank, ")");
return true;
}
packet.readUInt32(); // killCredit[0]
packet.readUInt32(); // killCredit[1]
data.displayId[0] = packet.readUInt32();
data.displayId[1] = packet.readUInt32();
data.displayId[2] = packet.readUInt32();
data.displayId[3] = packet.readUInt32();
// Skip remaining fields (healthMultiplier, powerMultiplier, racialLeader, questItems, movementId)
LOG_DEBUG("Creature query response: ", data.name, " (type=", data.creatureType, LOG_DEBUG("Creature query response: ", data.name, " (type=", data.creatureType,
" rank=", data.rank, ")"); " rank=", data.rank, " displayIds=[", data.displayId[0], ",",
data.displayId[1], ",", data.displayId[2], ",", data.displayId[3], "])");
return true; return true;
} }

View file

@ -396,14 +396,15 @@ std::vector<uint8_t> AssetManager::readFile(const std::string& path) const {
std::string normalized = normalizePath(path); std::string normalized = normalizePath(path);
// Check cache first // Check cache first (shared lock allows concurrent reads)
{ {
std::lock_guard<std::mutex> cacheLock(cacheMutex); std::shared_lock<std::shared_mutex> cacheLock(cacheMutex);
auto it = fileCache.find(normalized); auto it = fileCache.find(normalized);
if (it != fileCache.end()) { if (it != fileCache.end()) {
it->second.lastAccessTime = ++fileCacheAccessCounter; auto data = it->second.data;
cacheLock.unlock();
fileCacheHits++; fileCacheHits++;
return it->second.data; return data;
} }
} }
@ -422,7 +423,7 @@ 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 < fileCacheBudget / 2) { if (fileSize > 0 && fileSize < fileCacheBudget / 2) {
std::lock_guard<std::mutex> cacheLock(cacheMutex); std::lock_guard<std::shared_mutex> cacheLock(cacheMutex);
// Evict old entries if needed (LRU) // Evict old entries if needed (LRU)
while (fileCacheTotalBytes + fileSize > fileCacheBudget && !fileCache.empty()) { while (fileCacheTotalBytes + fileSize > fileCacheBudget && !fileCache.empty()) {
auto lru = fileCache.begin(); auto lru = fileCache.begin();
@ -456,13 +457,13 @@ std::vector<uint8_t> AssetManager::readFileOptional(const std::string& path) con
} }
void AssetManager::clearDBCCache() { void AssetManager::clearDBCCache() {
std::lock_guard<std::mutex> lock(cacheMutex); std::lock_guard<std::shared_mutex> lock(cacheMutex);
dbcCache.clear(); dbcCache.clear();
LOG_INFO("Cleared DBC cache"); LOG_INFO("Cleared DBC cache");
} }
void AssetManager::clearCache() { void AssetManager::clearCache() {
std::lock_guard<std::mutex> lock(cacheMutex); std::lock_guard<std::shared_mutex> lock(cacheMutex);
dbcCache.clear(); dbcCache.clear();
fileCache.clear(); fileCache.clear();
fileCacheTotalBytes = 0; fileCacheTotalBytes = 0;

View file

@ -137,26 +137,32 @@ float DBCFile::getFloat(uint32_t recordIndex, uint32_t fieldIndex) const {
} }
std::string DBCFile::getString(uint32_t recordIndex, uint32_t fieldIndex) const { std::string DBCFile::getString(uint32_t recordIndex, uint32_t fieldIndex) const {
return std::string(getStringView(recordIndex, fieldIndex));
}
std::string_view DBCFile::getStringView(uint32_t recordIndex, uint32_t fieldIndex) const {
uint32_t offset = getUInt32(recordIndex, fieldIndex); uint32_t offset = getUInt32(recordIndex, fieldIndex);
return getStringByOffset(offset); return getStringViewByOffset(offset);
} }
std::string DBCFile::getStringByOffset(uint32_t offset) const { std::string DBCFile::getStringByOffset(uint32_t offset) const {
return std::string(getStringViewByOffset(offset));
}
std::string_view DBCFile::getStringViewByOffset(uint32_t offset) const {
if (!loaded || offset >= stringBlockSize) { if (!loaded || offset >= stringBlockSize) {
return ""; return {};
} }
// Find null terminator
const char* str = reinterpret_cast<const char*>(stringBlock.data() + offset); const char* str = reinterpret_cast<const char*>(stringBlock.data() + offset);
const char* end = reinterpret_cast<const char*>(stringBlock.data() + stringBlockSize); const char* end = reinterpret_cast<const char*>(stringBlock.data() + stringBlockSize);
// Find string length (up to null terminator or end of block)
size_t length = 0; size_t length = 0;
while (str + length < end && str[length] != '\0') { while (str + length < end && str[length] != '\0') {
length++; length++;
} }
return std::string(str, length); return std::string_view(str, length);
} }
int32_t DBCFile::findRecordById(uint32_t id) const { int32_t DBCFile::findRecordById(uint32_t id) const {

View file

@ -169,7 +169,7 @@ void MPQManager::shutdown() {
archives.clear(); archives.clear();
archiveNames.clear(); archiveNames.clear();
{ {
std::lock_guard<std::mutex> lock(fileArchiveCacheMutex_); std::lock_guard<std::shared_mutex> lock(fileArchiveCacheMutex_);
fileArchiveCache_.clear(); fileArchiveCache_.clear();
} }
{ {
@ -214,7 +214,7 @@ bool MPQManager::loadArchive(const std::string& path, int priority) {
// Archive set/priority changed, so cached filename -> archive mappings may be stale. // Archive set/priority changed, so cached filename -> archive mappings may be stale.
{ {
std::lock_guard<std::mutex> lock(fileArchiveCacheMutex_); std::lock_guard<std::shared_mutex> lock(fileArchiveCacheMutex_);
fileArchiveCache_.clear(); fileArchiveCache_.clear();
} }
@ -383,7 +383,7 @@ HANDLE MPQManager::findFileArchive(const std::string& filename) const {
#ifdef HAVE_STORMLIB #ifdef HAVE_STORMLIB
std::string cacheKey = normalizeVirtualFilenameForLookup(filename); std::string cacheKey = normalizeVirtualFilenameForLookup(filename);
{ {
std::lock_guard<std::mutex> lock(fileArchiveCacheMutex_); std::shared_lock<std::shared_mutex> lock(fileArchiveCacheMutex_);
auto it = fileArchiveCache_.find(cacheKey); auto it = fileArchiveCache_.find(cacheKey);
if (it != fileArchiveCache_.end()) { if (it != fileArchiveCache_.end()) {
return it->second; return it->second;
@ -416,7 +416,7 @@ HANDLE MPQManager::findFileArchive(const std::string& filename) const {
} }
{ {
std::lock_guard<std::mutex> lock(fileArchiveCacheMutex_); std::lock_guard<std::shared_mutex> lock(fileArchiveCacheMutex_);
if (fileArchiveCache_.size() >= fileArchiveCacheMaxEntries_) { if (fileArchiveCache_.size() >= fileArchiveCacheMaxEntries_) {
// Simple safety valve: clear the cache rather than allowing an unbounded growth. // Simple safety valve: clear the cache rather than allowing an unbounded growth.
LOG_WARNING("MPQ archive lookup cache cleared (size=", fileArchiveCache_.size(), LOG_WARNING("MPQ archive lookup cache cleared (size=", fileArchiveCache_.size(),

View file

@ -111,9 +111,11 @@ std::optional<float> CameraController::getCachedFloorHeight(float x, float y, fl
// Check cache validity (position within threshold and frame count) // Check cache validity (position within threshold and frame count)
glm::vec2 queryPos(x, y); glm::vec2 queryPos(x, y);
glm::vec2 cachedPos(lastFloorQueryPos.x, lastFloorQueryPos.y); glm::vec2 cachedPos(lastFloorQueryPos.x, lastFloorQueryPos.y);
float dist = glm::length(queryPos - cachedPos); glm::vec2 dq = queryPos - cachedPos;
float distSq = glm::dot(dq, dq);
constexpr float kFloorThresholdSq = FLOOR_QUERY_DISTANCE_THRESHOLD * FLOOR_QUERY_DISTANCE_THRESHOLD;
if (dist < FLOOR_QUERY_DISTANCE_THRESHOLD && floorQueryFrameCounter < FLOOR_QUERY_FRAME_INTERVAL) { if (distSq < kFloorThresholdSq && floorQueryFrameCounter < FLOOR_QUERY_FRAME_INTERVAL) {
floorQueryFrameCounter++; floorQueryFrameCounter++;
return cachedFloorHeight; return cachedFloorHeight;
} }
@ -194,7 +196,7 @@ void CameraController::update(float deltaTime) {
} }
// Smooth camera position // Smooth camera position
if (glm::length(smoothedCamPos) < 0.01f) { if (glm::dot(smoothedCamPos, smoothedCamPos) < 1e-4f) {
smoothedCamPos = actualCam; smoothedCamPos = actualCam;
} }
float camLerp = 1.0f - std::exp(-CAM_SMOOTH_SPEED * deltaTime); float camLerp = 1.0f - std::exp(-CAM_SMOOTH_SPEED * deltaTime);
@ -516,9 +518,9 @@ void CameraController::update(float deltaTime) {
}; };
glm::vec2 fwd2(forward.x, forward.y); glm::vec2 fwd2(forward.x, forward.y);
float fwdLen = glm::length(fwd2); float fwdLenSq = glm::dot(fwd2, fwd2);
if (fwdLen > 1e-4f) { if (fwdLenSq > 1e-8f) {
fwd2 /= fwdLen; fwd2 *= glm::inversesqrt(fwdLenSq);
std::optional<float> aheadFloor; std::optional<float> aheadFloor;
const float probeZ = targetPos.z + 2.0f; const float probeZ = targetPos.z + 2.0f;
const float dists[] = {0.45f, 0.90f, 1.25f}; const float dists[] = {0.45f, 0.90f, 1.25f};
@ -566,7 +568,7 @@ void CameraController::update(float deltaTime) {
} else { } else {
// Manual control: use camera's 3D direction (swim where you look) // Manual control: use camera's 3D direction (swim where you look)
swimForward = glm::normalize(forward3D); swimForward = glm::normalize(forward3D);
if (glm::length(swimForward) < 1e-4f) { if (glm::dot(swimForward, swimForward) < 1e-8f) {
swimForward = forward; swimForward = forward;
} }
} }
@ -583,8 +585,9 @@ void CameraController::update(float deltaTime) {
if (nowStrafeLeft) swimMove += swimRight; if (nowStrafeLeft) swimMove += swimRight;
if (nowStrafeRight) swimMove -= swimRight; if (nowStrafeRight) swimMove -= swimRight;
if (glm::length(swimMove) > 0.001f) { float swimMoveLenSq = glm::dot(swimMove, swimMove);
swimMove = glm::normalize(swimMove); if (swimMoveLenSq > 1e-6f) {
swimMove *= glm::inversesqrt(swimMoveLenSq);
// Use backward swim speed when moving backwards only (not when combining with strafe) // Use backward swim speed when moving backwards only (not when combining with strafe)
float applySpeed = (nowBackward && !nowForward) ? swimBackSpeed : swimSpeed; float applySpeed = (nowBackward && !nowForward) ? swimBackSpeed : swimSpeed;
targetPos += swimMove * applySpeed * physicsDeltaTime; targetPos += swimMove * applySpeed * physicsDeltaTime;
@ -623,10 +626,12 @@ void CameraController::update(float deltaTime) {
// Prevent sinking/clipping through world floor while swimming. // Prevent sinking/clipping through world floor while swimming.
// Cache floor queries (update every 3 frames or 1 unit movement) // Cache floor queries (update every 3 frames or 1 unit movement)
std::optional<float> floorH; std::optional<float> floorH;
float dist2D = glm::length(glm::vec2(targetPos.x - lastFloorQueryPos.x, float dx2D = targetPos.x - lastFloorQueryPos.x;
targetPos.y - lastFloorQueryPos.y)); float dy2D = targetPos.y - lastFloorQueryPos.y;
float dist2DSq = dx2D * dx2D + dy2D * dy2D;
constexpr float kFloorDistSq = FLOOR_QUERY_DISTANCE_THRESHOLD * FLOOR_QUERY_DISTANCE_THRESHOLD;
bool updateFloorCache = (floorQueryFrameCounter++ >= FLOOR_QUERY_FRAME_INTERVAL) || bool updateFloorCache = (floorQueryFrameCounter++ >= FLOOR_QUERY_FRAME_INTERVAL) ||
(dist2D > FLOOR_QUERY_DISTANCE_THRESHOLD); (dist2DSq > kFloorDistSq);
if (updateFloorCache) { if (updateFloorCache) {
floorQueryFrameCounter = 0; floorQueryFrameCounter = 0;
@ -685,10 +690,12 @@ void CameraController::update(float deltaTime) {
{ {
glm::vec3 swimFrom = *followTarget; glm::vec3 swimFrom = *followTarget;
glm::vec3 swimTo = targetPos; glm::vec3 swimTo = targetPos;
float swimMoveDist = glm::length(swimTo - swimFrom); glm::vec3 swimDelta = swimTo - swimFrom;
float swimMoveDistSq = glm::dot(swimDelta, swimDelta);
glm::vec3 stepPos = swimFrom; glm::vec3 stepPos = swimFrom;
if (swimMoveDist > 0.01f) { if (swimMoveDistSq > 1e-4f) {
float swimMoveDist = std::sqrt(swimMoveDistSq);
float swimStepSize = cachedInsideWMO ? 0.20f : 0.35f; float swimStepSize = cachedInsideWMO ? 0.20f : 0.35f;
int swimSteps = std::max(1, std::min(8, static_cast<int>(std::ceil(swimMoveDist / swimStepSize)))); int swimSteps = std::max(1, std::min(8, static_cast<int>(std::ceil(swimMoveDist / swimStepSize))));
glm::vec3 stepDelta = (swimTo - swimFrom) / static_cast<float>(swimSteps); glm::vec3 stepDelta = (swimTo - swimFrom) / static_cast<float>(swimSteps);
@ -746,7 +753,7 @@ void CameraController::update(float deltaTime) {
// Forward/back follows camera 3D direction (same as swim) // Forward/back follows camera 3D direction (same as swim)
glm::vec3 flyFwd = glm::normalize(forward3D); glm::vec3 flyFwd = glm::normalize(forward3D);
if (glm::length(flyFwd) < 1e-4f) flyFwd = forward; if (glm::dot(flyFwd, flyFwd) < 1e-8f) flyFwd = forward;
glm::vec3 flyMove(0.0f); glm::vec3 flyMove(0.0f);
if (nowForward) flyMove += flyFwd; if (nowForward) flyMove += flyFwd;
if (nowBackward) flyMove -= flyFwd; if (nowBackward) flyMove -= flyFwd;
@ -756,8 +763,9 @@ void CameraController::update(float deltaTime) {
bool flyDescend = !uiWantsKeyboard && xDown && mounted_; bool flyDescend = !uiWantsKeyboard && xDown && mounted_;
if (nowJump) flyMove.z += 1.0f; if (nowJump) flyMove.z += 1.0f;
if (flyDescend) flyMove.z -= 1.0f; if (flyDescend) flyMove.z -= 1.0f;
if (glm::length(flyMove) > 0.001f) { float flyMoveLenSq = glm::dot(flyMove, flyMove);
flyMove = glm::normalize(flyMove); if (flyMoveLenSq > 1e-6f) {
flyMove *= glm::inversesqrt(flyMoveLenSq);
float flyFwdSpeed = (flightSpeedOverride_ > 0.0f && flightSpeedOverride_ < 200.0f float flyFwdSpeed = (flightSpeedOverride_ > 0.0f && flightSpeedOverride_ < 200.0f
&& !std::isnan(flightSpeedOverride_)) && !std::isnan(flightSpeedOverride_))
? flightSpeedOverride_ : speed; ? flightSpeedOverride_ : speed;
@ -771,8 +779,9 @@ void CameraController::update(float deltaTime) {
// Skip all ground physics — go straight to collision/WMO sections // Skip all ground physics — go straight to collision/WMO sections
} else { } else {
if (glm::length(movement) > 0.001f) { float moveLenSq = glm::dot(movement, movement);
movement = glm::normalize(movement); if (moveLenSq > 1e-6f) {
movement *= glm::inversesqrt(moveLenSq);
targetPos += movement * speed * physicsDeltaTime; targetPos += movement * speed * physicsDeltaTime;
} }
@ -784,7 +793,7 @@ void CameraController::update(float deltaTime) {
float drag = std::exp(-KNOCKBACK_HORIZ_DRAG * physicsDeltaTime); float drag = std::exp(-KNOCKBACK_HORIZ_DRAG * physicsDeltaTime);
knockbackHorizVel_ *= drag; knockbackHorizVel_ *= drag;
// Once negligible, clear the flag so collision/grounding work normally. // Once negligible, clear the flag so collision/grounding work normally.
if (glm::length(knockbackHorizVel_) < 0.05f) { if (glm::dot(knockbackHorizVel_, knockbackHorizVel_) < 0.0025f) {
knockbackActive_ = false; knockbackActive_ = false;
knockbackHorizVel_ = glm::vec2(0.0f); knockbackHorizVel_ = glm::vec2(0.0f);
} }
@ -829,8 +838,9 @@ void CameraController::update(float deltaTime) {
// Refresh inside-WMO state before collision/grounding so we don't use stale // Refresh inside-WMO state before collision/grounding so we don't use stale
// terrain-first caches while entering enclosed tunnel/building spaces. // terrain-first caches while entering enclosed tunnel/building spaces.
if (wmoRenderer && !externalFollow_) { if (wmoRenderer && !externalFollow_) {
const float insideDist = glm::length(targetPos - lastInsideStateCheckPos_); glm::vec3 insideDelta = targetPos - lastInsideStateCheckPos_;
if (++insideStateCheckCounter_ >= 2 || insideDist > 0.35f) { float insideDistSq = glm::dot(insideDelta, insideDelta);
if (++insideStateCheckCounter_ >= 2 || insideDistSq > 0.1225f) {
insideStateCheckCounter_ = 0; insideStateCheckCounter_ = 0;
lastInsideStateCheckPos_ = targetPos; lastInsideStateCheckPos_ = targetPos;
@ -853,9 +863,11 @@ void CameraController::update(float deltaTime) {
{ {
glm::vec3 startPos = *followTarget; glm::vec3 startPos = *followTarget;
glm::vec3 desiredPos = targetPos; glm::vec3 desiredPos = targetPos;
float moveDist = glm::length(desiredPos - startPos); glm::vec3 moveDelta = desiredPos - startPos;
float moveDistSq = glm::dot(moveDelta, moveDelta);
if (moveDist > 0.01f) { if (moveDistSq > 1e-4f) {
float moveDist = std::sqrt(moveDistSq);
// Smaller step size when inside buildings for tighter collision // Smaller step size when inside buildings for tighter collision
float stepSize = cachedInsideWMO ? 0.20f : 0.35f; float stepSize = cachedInsideWMO ? 0.20f : 0.35f;
int sweepSteps = std::max(1, std::min(8, static_cast<int>(std::ceil(moveDist / stepSize)))); int sweepSteps = std::max(1, std::min(8, static_cast<int>(std::ceil(moveDist / stepSize))));
@ -909,9 +921,11 @@ void CameraController::update(float deltaTime) {
std::optional<float> centerM2H; std::optional<float> centerM2H;
{ {
// Collision cache: skip expensive checks if barely moved (15cm threshold) // Collision cache: skip expensive checks if barely moved (15cm threshold)
float distMoved = glm::length(glm::vec2(targetPos.x, targetPos.y) - float dmx = targetPos.x - lastCollisionCheckPos_.x;
glm::vec2(lastCollisionCheckPos_.x, lastCollisionCheckPos_.y)); float dmy = targetPos.y - lastCollisionCheckPos_.y;
bool useCached = grounded && hasCachedFloor_ && distMoved < COLLISION_CACHE_DISTANCE; float distMovedSq = dmx * dmx + dmy * dmy;
constexpr float kCollisionCacheDistSq = COLLISION_CACHE_DISTANCE * COLLISION_CACHE_DISTANCE;
bool useCached = grounded && hasCachedFloor_ && distMovedSq < kCollisionCacheDistSq;
if (useCached) { if (useCached) {
// Never trust cached ground while actively descending or when // Never trust cached ground while actively descending or when
// vertical drift from cached floor is meaningful. // vertical drift from cached floor is meaningful.
@ -1371,11 +1385,13 @@ void CameraController::update(float deltaTime) {
float mountedOffset = mounted_ ? mountHeightOffset_ : 0.0f; float mountedOffset = mounted_ ? mountHeightOffset_ : 0.0f;
float pivotLift = 0.0f; float pivotLift = 0.0f;
if (terrainManager && !externalFollow_ && !cachedInsideInteriorWMO) { if (terrainManager && !externalFollow_ && !cachedInsideInteriorWMO) {
float moved = glm::length(glm::vec2(targetPos.x - lastPivotLiftQueryPos_.x, float plx = targetPos.x - lastPivotLiftQueryPos_.x;
targetPos.y - lastPivotLiftQueryPos_.y)); float ply = targetPos.y - lastPivotLiftQueryPos_.y;
float movedSq = plx * plx + ply * ply;
constexpr float kPivotLiftPosSq = PIVOT_LIFT_POS_THRESHOLD * PIVOT_LIFT_POS_THRESHOLD;
float distDelta = std::abs(currentDistance - lastPivotLiftDistance_); float distDelta = std::abs(currentDistance - lastPivotLiftDistance_);
bool queryLift = (++pivotLiftQueryCounter_ >= PIVOT_LIFT_QUERY_INTERVAL) || bool queryLift = (++pivotLiftQueryCounter_ >= PIVOT_LIFT_QUERY_INTERVAL) ||
(moved >= PIVOT_LIFT_POS_THRESHOLD) || (movedSq >= kPivotLiftPosSq) ||
(distDelta >= PIVOT_LIFT_DIST_THRESHOLD); (distDelta >= PIVOT_LIFT_DIST_THRESHOLD);
if (queryLift) { if (queryLift) {
pivotLiftQueryCounter_ = 0; pivotLiftQueryCounter_ = 0;
@ -1421,8 +1437,9 @@ void CameraController::update(float deltaTime) {
// Limit max zoom when inside a WMO with a ceiling (building interior) // Limit max zoom when inside a WMO with a ceiling (building interior)
// Throttle: only recheck every 10 frames or when position changes >2 units. // Throttle: only recheck every 10 frames or when position changes >2 units.
if (wmoRenderer) { if (wmoRenderer) {
float distFromLastCheck = glm::length(targetPos - lastInsideWMOCheckPos); glm::vec3 wmoCheckDelta = targetPos - lastInsideWMOCheckPos;
if (++insideWMOCheckCounter >= 10 || distFromLastCheck > 2.0f) { float distFromLastCheckSq = glm::dot(wmoCheckDelta, wmoCheckDelta);
if (++insideWMOCheckCounter >= 10 || distFromLastCheckSq > 4.0f) {
wmoRenderer->updateActiveGroup(targetPos.x, targetPos.y, targetPos.z + 1.0f); wmoRenderer->updateActiveGroup(targetPos.x, targetPos.y, targetPos.z + 1.0f);
insideWMOCheckCounter = 0; insideWMOCheckCounter = 0;
lastInsideWMOCheckPos = targetPos; lastInsideWMOCheckPos = targetPos;
@ -1486,7 +1503,7 @@ void CameraController::update(float deltaTime) {
} }
// Smooth camera position to avoid jitter // Smooth camera position to avoid jitter
if (glm::length(smoothedCamPos) < 0.01f) { if (glm::dot(smoothedCamPos, smoothedCamPos) < 1e-4f) {
smoothedCamPos = actualCam; // Initialize smoothedCamPos = actualCam; // Initialize
} }
float camLerp = 1.0f - std::exp(-CAM_SMOOTH_SPEED * deltaTime); float camLerp = 1.0f - std::exp(-CAM_SMOOTH_SPEED * deltaTime);
@ -1503,9 +1520,10 @@ void CameraController::update(float deltaTime) {
std::optional<float> camWmoH; std::optional<float> camWmoH;
if (wmoRenderer) { if (wmoRenderer) {
// Skip expensive WMO floor query if camera barely moved // Skip expensive WMO floor query if camera barely moved
float camDelta = glm::length(glm::vec2(smoothedCamPos.x - lastCamFloorQueryPos.x, float cdx = smoothedCamPos.x - lastCamFloorQueryPos.x;
smoothedCamPos.y - lastCamFloorQueryPos.y)); float cdy = smoothedCamPos.y - lastCamFloorQueryPos.y;
if (camDelta < 0.3f && hasCachedCamFloor) { float camDeltaSq = cdx * cdx + cdy * cdy;
if (camDeltaSq < 0.09f && hasCachedCamFloor) {
camWmoH = cachedCamWmoFloor; camWmoH = cachedCamWmoFloor;
} else { } else {
float camFloorProbeZ = smoothedCamPos.z; float camFloorProbeZ = smoothedCamPos.z;
@ -1618,8 +1636,9 @@ void CameraController::update(float deltaTime) {
float waterSurfaceCamZ = waterH ? (*waterH - WATER_SURFACE_OFFSET + eyeHeight) : newPos.z; float waterSurfaceCamZ = waterH ? (*waterH - WATER_SURFACE_OFFSET + eyeHeight) : newPos.z;
bool diveIntent = nowForward && (forward3D.z < -0.28f); bool diveIntent = nowForward && (forward3D.z < -0.28f);
if (glm::length(movement) > 0.001f) { float movLenSq = glm::dot(movement, movement);
movement = glm::normalize(movement); if (movLenSq > 1e-6f) {
movement *= glm::inversesqrt(movLenSq);
newPos += movement * swimSpeed * physicsDeltaTime; newPos += movement * swimSpeed * physicsDeltaTime;
} }
@ -1652,8 +1671,9 @@ void CameraController::update(float deltaTime) {
} else { } else {
swimming = false; swimming = false;
if (glm::length(movement) > 0.001f) { float movLenSq2 = glm::dot(movement, movement);
movement = glm::normalize(movement); if (movLenSq2 > 1e-6f) {
movement *= glm::inversesqrt(movLenSq2);
newPos += movement * speed * physicsDeltaTime; newPos += movement * speed * physicsDeltaTime;
} }
@ -1680,9 +1700,11 @@ void CameraController::update(float deltaTime) {
if (wmoRenderer) { if (wmoRenderer) {
glm::vec3 startFeet = camera->getPosition() - glm::vec3(0, 0, eyeHeight); glm::vec3 startFeet = camera->getPosition() - glm::vec3(0, 0, eyeHeight);
glm::vec3 desiredFeet = newPos - glm::vec3(0, 0, eyeHeight); glm::vec3 desiredFeet = newPos - glm::vec3(0, 0, eyeHeight);
float moveDist = glm::length(desiredFeet - startFeet); glm::vec3 feetDelta = desiredFeet - startFeet;
float moveDistSq2 = glm::dot(feetDelta, feetDelta);
if (moveDist > 0.01f) { if (moveDistSq2 > 1e-4f) {
float moveDist = std::sqrt(moveDistSq2);
float stepSize = cachedInsideWMO ? 0.20f : 0.35f; float stepSize = cachedInsideWMO ? 0.20f : 0.35f;
int sweepSteps = std::max(1, std::min(8, static_cast<int>(std::ceil(moveDist / stepSize)))); int sweepSteps = std::max(1, std::min(8, static_cast<int>(std::ceil(moveDist / stepSize))));
glm::vec3 stepPos = startFeet; glm::vec3 stepPos = startFeet;

View file

@ -319,8 +319,11 @@ void CharacterRenderer::shutdown() {
" models=", models.size(), " override=", (void*)renderPassOverride_); " models=", models.size(), " override=", (void*)renderPassOverride_);
// Wait for any in-flight background normal map generation threads // Wait for any in-flight background normal map generation threads
while (pendingNormalMapCount_.load(std::memory_order_relaxed) > 0) { {
std::this_thread::sleep_for(std::chrono::milliseconds(1)); std::unique_lock<std::mutex> lock(normalMapResultsMutex_);
normalMapDoneCV_.wait(lock, [this] {
return pendingNormalMapCount_.load(std::memory_order_acquire) == 0;
});
} }
vkDeviceWaitIdle(vkCtx_->getDevice()); vkDeviceWaitIdle(vkCtx_->getDevice());
@ -407,8 +410,11 @@ void CharacterRenderer::clear() {
" models=", models.size()); " models=", models.size());
// Wait for any in-flight background normal map generation threads // Wait for any in-flight background normal map generation threads
while (pendingNormalMapCount_.load(std::memory_order_relaxed) > 0) { {
std::this_thread::sleep_for(std::chrono::milliseconds(1)); std::unique_lock<std::mutex> lock(normalMapResultsMutex_);
normalMapDoneCV_.wait(lock, [this] {
return pendingNormalMapCount_.load(std::memory_order_acquire) == 0;
});
} }
// Discard any completed results that haven't been uploaded // Discard any completed results that haven't been uploaded
{ {
@ -731,7 +737,9 @@ VkTexture* CharacterRenderer::loadTexture(const std::string& path) {
std::lock_guard<std::mutex> lock(self->normalMapResultsMutex_); std::lock_guard<std::mutex> lock(self->normalMapResultsMutex_);
self->completedNormalMaps_.push_back(std::move(result)); self->completedNormalMaps_.push_back(std::move(result));
} }
self->pendingNormalMapCount_.fetch_sub(1, std::memory_order_relaxed); if (self->pendingNormalMapCount_.fetch_sub(1, std::memory_order_release) == 1) {
self->normalMapDoneCV_.notify_one();
}
}).detach(); }).detach();
e.normalMapPending = true; e.normalMapPending = true;
} }
@ -2825,11 +2833,12 @@ void CharacterRenderer::moveInstanceTo(uint32_t instanceId, const glm::vec3& des
return 0; return 0;
}; };
float planarDist = glm::length(glm::vec2(destination.x - inst.position.x, float pdx = destination.x - inst.position.x;
destination.y - inst.position.y)); float pdy = destination.y - inst.position.y;
float planarDistSq = pdx * pdx + pdy * pdy;
bool synthesizedDuration = false; bool synthesizedDuration = false;
if (durationSeconds <= 0.0f) { if (durationSeconds <= 0.0f) {
if (planarDist < 0.01f) { if (planarDistSq < 1e-4f) {
// Stop at current location. // Stop at current location.
inst.position = destination; inst.position = destination;
inst.isMoving = false; inst.isMoving = false;
@ -2840,7 +2849,7 @@ void CharacterRenderer::moveInstanceTo(uint32_t instanceId, const glm::vec3& des
} }
// Some cores send movement-only deltas without spline duration. // Some cores send movement-only deltas without spline duration.
// Synthesize a tiny duration so movement anim/rotation still updates. // Synthesize a tiny duration so movement anim/rotation still updates.
durationSeconds = std::clamp(planarDist / 7.0f, 0.05f, 0.20f); durationSeconds = std::clamp(std::sqrt(planarDistSq) / 7.0f, 0.05f, 0.20f);
synthesizedDuration = true; synthesizedDuration = true;
} }
@ -2852,14 +2861,14 @@ void CharacterRenderer::moveInstanceTo(uint32_t instanceId, const glm::vec3& des
// Face toward destination (yaw around Z axis since Z is up) // Face toward destination (yaw around Z axis since Z is up)
glm::vec3 dir = destination - inst.position; glm::vec3 dir = destination - inst.position;
if (glm::length(glm::vec2(dir.x, dir.y)) > 0.001f) { if (dir.x * dir.x + dir.y * dir.y > 1e-6f) {
float angle = std::atan2(dir.y, dir.x); float angle = std::atan2(dir.y, dir.x);
inst.rotation.z = angle; inst.rotation.z = angle;
} }
// Play movement animation while moving. // Play movement animation while moving.
// Prefer run only when speed is clearly above normal walk pace. // Prefer run only when speed is clearly above normal walk pace.
float moveSpeed = planarDist / std::max(durationSeconds, 0.001f); float moveSpeed = std::sqrt(planarDistSq) / std::max(durationSeconds, 0.001f);
bool preferRun = (!synthesizedDuration && moveSpeed >= 4.5f); bool preferRun = (!synthesizedDuration && moveSpeed >= 4.5f);
uint32_t moveAnim = pickMoveAnim(preferRun); uint32_t moveAnim = pickMoveAnim(preferRun);
if (moveAnim != 0 && inst.currentAnimationId != moveAnim) { if (moveAnim != 0 && inst.currentAnimationId != moveAnim) {

View file

@ -449,8 +449,9 @@ void ChargeEffect::emit(const glm::vec3& position, const glm::vec3& direction) {
} }
// Only add a new trail point if we've moved enough // Only add a new trail point if we've moved enough
float dist = glm::length(position - lastEmitPos_); glm::vec3 emitDelta = position - lastEmitPos_;
if (dist >= TRAIL_SPAWN_DIST || trail_.empty()) { float distSq = glm::dot(emitDelta, emitDelta);
if (distSq >= TRAIL_SPAWN_DIST * TRAIL_SPAWN_DIST || trail_.empty()) {
// Ribbon is vertical: side vector points straight up // Ribbon is vertical: side vector points straight up
glm::vec3 side = glm::vec3(0.0f, 0.0f, 1.0f); glm::vec3 side = glm::vec3(0.0f, 0.0f, 1.0f);
@ -466,9 +467,10 @@ void ChargeEffect::emit(const glm::vec3& position, const glm::vec3& direction) {
// Spawn dust puffs at feet // Spawn dust puffs at feet
glm::vec3 horizDir = glm::vec3(direction.x, direction.y, 0.0f); glm::vec3 horizDir = glm::vec3(direction.x, direction.y, 0.0f);
float horizLen = glm::length(horizDir); float horizLenSq = glm::dot(horizDir, horizDir);
if (horizLen < 0.001f) return; if (horizLenSq < 1e-6f) return;
glm::vec3 backDir = -horizDir / horizLen; float invHorizLen = glm::inversesqrt(horizLenSq);
glm::vec3 backDir = -horizDir * invHorizLen;
glm::vec3 sideDir = glm::vec3(-backDir.y, backDir.x, 0.0f); glm::vec3 sideDir = glm::vec3(-backDir.y, backDir.x, 0.0f);
dustAccum_ += 30.0f * 0.016f; dustAccum_ += 30.0f * 0.016f;

View file

@ -63,10 +63,11 @@ void Frustum::extractFromMatrix(const glm::mat4& vp) {
} }
void Frustum::normalizePlane(Plane& plane) { void Frustum::normalizePlane(Plane& plane) {
float length = glm::length(plane.normal); float lenSq = glm::dot(plane.normal, plane.normal);
if (length > 0.0001f) { if (lenSq > 0.00000001f) {
plane.normal /= length; float invLen = glm::inversesqrt(lenSq);
plane.distance /= length; plane.normal *= invLen;
plane.distance *= invLen;
} }
} }

View file

@ -301,10 +301,11 @@ void LensFlare::render(VkCommandBuffer cmd, const Camera& camera, const glm::vec
// Sun billboard rendering is sky-locked (view translation removed), so anchor // Sun billboard rendering is sky-locked (view translation removed), so anchor
// flare projection to camera position along sun direction to avoid parallax drift. // flare projection to camera position along sun direction to avoid parallax drift.
glm::vec3 sunDir = sunPosition; glm::vec3 sunDir = sunPosition;
if (glm::length(sunDir) < 0.0001f) { float sunDirLenSq = glm::dot(sunDir, sunDir);
if (sunDirLenSq < 1e-8f) {
return; return;
} }
sunDir = glm::normalize(sunDir); sunDir *= glm::inversesqrt(sunDirLenSq);
glm::vec3 anchoredSunPos = camera.getPosition() + sunDir * 800.0f; glm::vec3 anchoredSunPos = camera.getPosition() + sunDir * 800.0f;
// Calculate sun visibility // Calculate sun visibility

View file

@ -305,8 +305,9 @@ void LightingManager::update(const glm::vec3& playerPos, uint32_t mapId,
} }
// Normalize blended direction // Normalize blended direction
if (glm::length(blendedDir) > 0.001f) { float blendedDirLenSq = glm::dot(blendedDir, blendedDir);
newParams.directionalDir = glm::normalize(blendedDir); if (blendedDirLenSq > 1e-6f) {
newParams.directionalDir = blendedDir * glm::inversesqrt(blendedDirLenSq);
} else { } else {
// Fallback if all directions cancelled out // Fallback if all directions cancelled out
newParams.directionalDir = glm::vec3(0.3f, -0.7f, 0.6f); newParams.directionalDir = glm::vec3(0.3f, -0.7f, 0.6f);
@ -371,14 +372,16 @@ std::vector<LightingManager::WeightedVolume> LightingManager::findLightVolumes(c
for (const auto& volume : volumes) { for (const auto& volume : volumes) {
// Apply coordinate scaling (test with 1.0f, try 36.0f if distances are off) // Apply coordinate scaling (test with 1.0f, try 36.0f if distances are off)
glm::vec3 scaledPos = volume.position * LIGHT_COORD_SCALE; glm::vec3 scaledPos = volume.position * LIGHT_COORD_SCALE;
float dist = glm::length(playerPos - scaledPos); glm::vec3 toPlayer = playerPos - scaledPos;
float distSq = glm::dot(toPlayer, toPlayer);
float weight = 0.0f; float weight = 0.0f;
if (dist <= volume.innerRadius) { if (distSq <= volume.innerRadius * volume.innerRadius) {
// Inside inner radius: full weight // Inside inner radius: full weight
weight = 1.0f; weight = 1.0f;
} else if (dist < volume.outerRadius) { } else if (distSq < volume.outerRadius * volume.outerRadius) {
// Between inner and outer: fade out with smoothstep // Between inner and outer: fade out with smoothstep (sqrt needed for interpolation)
float dist = std::sqrt(distSq);
float t = (dist - volume.innerRadius) / (volume.outerRadius - volume.innerRadius); float t = (dist - volume.innerRadius) / (volume.outerRadius - volume.innerRadius);
t = glm::clamp(t, 0.0f, 1.0f); t = glm::clamp(t, 0.0f, 1.0f);
weight = 1.0f - (t * t * (3.0f - 2.0f * t)); // Smoothstep weight = 1.0f - (t * t * (3.0f - 2.0f * t)); // Smoothstep
@ -389,7 +392,7 @@ std::vector<LightingManager::WeightedVolume> LightingManager::findLightVolumes(c
// Debug logging for first few volumes // Debug logging for first few volumes
if (weighted.size() <= 3) { if (weighted.size() <= 3) {
LOG_INFO("Light volume ", volume.lightId, ": dist=", dist, LOG_INFO("Light volume ", volume.lightId, ": distSq=", distSq,
" inner=", volume.innerRadius, " outer=", volume.outerRadius, " inner=", volume.innerRadius, " outer=", volume.outerRadius,
" weight=", weight); " weight=", weight);
} }
@ -400,15 +403,20 @@ std::vector<LightingManager::WeightedVolume> LightingManager::findLightVolumes(c
return {}; return {};
} }
// Sort by weight descending // Keep top N volumes by weight (partial sort is O(n) vs O(n log n) for full sort)
std::sort(weighted.begin(), weighted.end(),
[](const WeightedVolume& a, const WeightedVolume& b) {
return a.weight > b.weight;
});
// Keep top N volumes
if (weighted.size() > MAX_BLEND_VOLUMES) { if (weighted.size() > MAX_BLEND_VOLUMES) {
std::partial_sort(weighted.begin(),
weighted.begin() + MAX_BLEND_VOLUMES,
weighted.end(),
[](const WeightedVolume& a, const WeightedVolume& b) {
return a.weight > b.weight;
});
weighted.resize(MAX_BLEND_VOLUMES); weighted.resize(MAX_BLEND_VOLUMES);
} else {
std::sort(weighted.begin(), weighted.end(),
[](const WeightedVolume& a, const WeightedVolume& b) {
return a.weight > b.weight;
});
} }
// Normalize weights to sum to 1.0 // Normalize weights to sum to 1.0

View file

@ -947,6 +947,9 @@ void M2ModelGPU::CollisionMesh::getFloorTrisInRange(
int cxMax = std::clamp(static_cast<int>((maxX - gridOrigin.x) / CELL_SIZE), 0, gridCellsX - 1); int cxMax = std::clamp(static_cast<int>((maxX - gridOrigin.x) / CELL_SIZE), 0, gridCellsX - 1);
int cyMin = std::clamp(static_cast<int>((minY - gridOrigin.y) / CELL_SIZE), 0, gridCellsY - 1); int cyMin = std::clamp(static_cast<int>((minY - gridOrigin.y) / CELL_SIZE), 0, gridCellsY - 1);
int cyMax = std::clamp(static_cast<int>((maxY - gridOrigin.y) / CELL_SIZE), 0, gridCellsY - 1); int cyMax = std::clamp(static_cast<int>((maxY - gridOrigin.y) / CELL_SIZE), 0, gridCellsY - 1);
const size_t cellCount = static_cast<size_t>(cxMax - cxMin + 1) *
static_cast<size_t>(cyMax - cyMin + 1);
out.reserve(cellCount * 8);
for (int cy = cyMin; cy <= cyMax; cy++) { for (int cy = cyMin; cy <= cyMax; cy++) {
for (int cx = cxMin; cx <= cxMax; cx++) { for (int cx = cxMin; cx <= cxMax; cx++) {
const auto& cell = cellFloorTris[cy * gridCellsX + cx]; const auto& cell = cellFloorTris[cy * gridCellsX + cx];
@ -966,6 +969,9 @@ void M2ModelGPU::CollisionMesh::getWallTrisInRange(
int cxMax = std::clamp(static_cast<int>((maxX - gridOrigin.x) / CELL_SIZE), 0, gridCellsX - 1); int cxMax = std::clamp(static_cast<int>((maxX - gridOrigin.x) / CELL_SIZE), 0, gridCellsX - 1);
int cyMin = std::clamp(static_cast<int>((minY - gridOrigin.y) / CELL_SIZE), 0, gridCellsY - 1); int cyMin = std::clamp(static_cast<int>((minY - gridOrigin.y) / CELL_SIZE), 0, gridCellsY - 1);
int cyMax = std::clamp(static_cast<int>((maxY - gridOrigin.y) / CELL_SIZE), 0, gridCellsY - 1); int cyMax = std::clamp(static_cast<int>((maxY - gridOrigin.y) / CELL_SIZE), 0, gridCellsY - 1);
const size_t cellCount = static_cast<size_t>(cxMax - cxMin + 1) *
static_cast<size_t>(cyMax - cyMin + 1);
out.reserve(cellCount * 8);
for (int cy = cyMin; cy <= cyMax; cy++) { for (int cy = cyMin; cy <= cyMax; cy++) {
for (int cx = cxMin; cx <= cxMax; cx++) { for (int cx = cxMin; cx <= cxMax; cx++) {
const auto& cell = cellWallTris[cy * gridCellsX + cx]; const auto& cell = cellWallTris[cy * gridCellsX + cx];
@ -3227,8 +3233,8 @@ void M2Renderer::emitParticles(M2Instance& inst, const M2ModelGPU& gpu, float dt
dir.x += distN(particleRng_) * hRange; dir.x += distN(particleRng_) * hRange;
dir.y += distN(particleRng_) * hRange; dir.y += distN(particleRng_) * hRange;
dir.z += distN(particleRng_) * vRange; dir.z += distN(particleRng_) * vRange;
float len = glm::length(dir); float lenSq = glm::dot(dir, dir);
if (len > 0.001f) dir /= len; if (lenSq > 0.001f * 0.001f) dir *= glm::inversesqrt(lenSq);
// Transform direction by bone + model orientation (rotation only) // Transform direction by bone + model orientation (rotation only)
glm::mat3 rotMat = glm::mat3(inst.modelMatrix * boneXform); glm::mat3 rotMat = glm::mat3(inst.modelMatrix * boneXform);
@ -4715,7 +4721,7 @@ float M2Renderer::raycastBoundingBoxes(const glm::vec3& origin, const glm::vec3&
getTightCollisionBounds(model, localMin, localMax); getTightCollisionBounds(model, localMin, localMax);
// Skip tiny doodads for camera occlusion; they cause jitter and false hits. // Skip tiny doodads for camera occlusion; they cause jitter and false hits.
glm::vec3 extents = (localMax - localMin) * instance.scale; glm::vec3 extents = (localMax - localMin) * instance.scale;
if (glm::length(extents) < 0.75f) continue; if (glm::dot(extents, extents) < 0.5625f) continue;
glm::vec3 localOrigin = glm::vec3(instance.invModelMatrix * glm::vec4(origin, 1.0f)); glm::vec3 localOrigin = glm::vec3(instance.invModelMatrix * glm::vec4(origin, 1.0f));
glm::vec3 localDir = glm::normalize(glm::vec3(instance.invModelMatrix * glm::vec4(direction, 0.0f))); glm::vec3 localDir = glm::normalize(glm::vec3(instance.invModelMatrix * glm::vec4(direction, 0.0f)));

View file

@ -421,10 +421,11 @@ void Minimap::compositePass(VkCommandBuffer cmd, const glm::vec3& centerWorldPos
const auto now = std::chrono::steady_clock::now(); const auto now = std::chrono::steady_clock::now();
bool needsRefresh = !hasCachedFrame; bool needsRefresh = !hasCachedFrame;
if (!needsRefresh) { if (!needsRefresh) {
float moved = glm::length(glm::vec2(centerWorldPos.x - lastUpdatePos.x, float mdx = centerWorldPos.x - lastUpdatePos.x;
centerWorldPos.y - lastUpdatePos.y)); float mdy = centerWorldPos.y - lastUpdatePos.y;
float movedSq = mdx * mdx + mdy * mdy;
float elapsed = std::chrono::duration<float>(now - lastUpdateTime).count(); float elapsed = std::chrono::duration<float>(now - lastUpdateTime).count();
needsRefresh = (moved >= updateDistance) || (elapsed >= updateIntervalSec); needsRefresh = (movedSq >= updateDistance * updateDistance) || (elapsed >= updateIntervalSec);
} }
// Also refresh if player crossed a tile boundary // Also refresh if player crossed a tile boundary

View file

@ -3242,7 +3242,7 @@ void Renderer::update(float deltaTime) {
} else if (inCombat_ && targetPosition && !emoteActive && !isMounted()) { } else if (inCombat_ && targetPosition && !emoteActive && !isMounted()) {
// Face target when in combat and idle // Face target when in combat and idle
glm::vec3 toTarget = *targetPosition - characterPosition; glm::vec3 toTarget = *targetPosition - characterPosition;
if (glm::length(glm::vec2(toTarget.x, toTarget.y)) > 0.1f) { if (toTarget.x * toTarget.x + toTarget.y * toTarget.y > 0.01f) {
float targetYaw = glm::degrees(std::atan2(toTarget.y, toTarget.x)); float targetYaw = glm::degrees(std::atan2(toTarget.y, toTarget.x));
float diff = targetYaw - characterYaw; float diff = targetYaw - characterYaw;
while (diff > 180.0f) diff -= 360.0f; while (diff > 180.0f) diff -= 360.0f;
@ -6222,8 +6222,9 @@ glm::mat4 Renderer::computeLightSpaceMatrix() {
glm::vec3 sunDir = glm::normalize(glm::vec3(-0.3f, -0.7f, -0.6f)); glm::vec3 sunDir = glm::normalize(glm::vec3(-0.3f, -0.7f, -0.6f));
if (lightingManager) { if (lightingManager) {
const auto& lighting = lightingManager->getLightingParams(); const auto& lighting = lightingManager->getLightingParams();
if (glm::length(lighting.directionalDir) > 0.001f) { float ldirLenSq = glm::dot(lighting.directionalDir, lighting.directionalDir);
sunDir = glm::normalize(-lighting.directionalDir); if (ldirLenSq > 1e-6f) {
sunDir = -lighting.directionalDir * glm::inversesqrt(ldirLenSq);
} }
} }
// Shadow camera expects light rays pointing downward in render space (Z up). // Shadow camera expects light rays pointing downward in render space (Z up).

View file

@ -156,8 +156,9 @@ void SkySystem::render(VkCommandBuffer cmd, VkDescriptorSet perFrameSet,
} }
glm::vec3 SkySystem::getSunPosition(const SkyParams& params) const { glm::vec3 SkySystem::getSunPosition(const SkyParams& params) const {
glm::vec3 dir = glm::normalize(params.directionalDir); float dirLenSq = glm::dot(params.directionalDir, params.directionalDir);
if (glm::length(dir) < 0.0001f) { glm::vec3 dir = (dirLenSq > 1e-8f) ? params.directionalDir * glm::inversesqrt(dirLenSq) : glm::vec3(0.0f);
if (dirLenSq < 1e-8f) {
dir = glm::vec3(0.0f, 0.0f, -1.0f); dir = glm::vec3(0.0f, 0.0f, -1.0f);
} }
glm::vec3 sunDir = -dir; glm::vec3 sunDir = -dir;

View file

@ -542,7 +542,7 @@ void SwimEffects::update(const Camera& camera, const CameraController& cc,
// Compute movement direction from camera yaw // Compute movement direction from camera yaw
float yawRad = glm::radians(cc.getYaw()); float yawRad = glm::radians(cc.getYaw());
glm::vec3 moveDir(std::cos(yawRad), std::sin(yawRad), 0.0f); glm::vec3 moveDir(std::cos(yawRad), std::sin(yawRad), 0.0f);
if (glm::length(glm::vec2(moveDir)) > 0.001f) { if (moveDir.x * moveDir.x + moveDir.y * moveDir.y > 1e-6f) {
moveDir = glm::normalize(moveDir); moveDir = glm::normalize(moveDir);
} }
@ -676,6 +676,7 @@ void SwimEffects::update(const Camera& camera, const CameraController& cc,
// --- Build vertex data --- // --- Build vertex data ---
rippleVertexData.clear(); rippleVertexData.clear();
rippleVertexData.reserve(ripples.size() * 5);
for (const auto& p : ripples) { for (const auto& p : ripples) {
rippleVertexData.push_back(p.position.x); rippleVertexData.push_back(p.position.x);
rippleVertexData.push_back(p.position.y); rippleVertexData.push_back(p.position.y);
@ -685,6 +686,7 @@ void SwimEffects::update(const Camera& camera, const CameraController& cc,
} }
bubbleVertexData.clear(); bubbleVertexData.clear();
bubbleVertexData.reserve(bubbles.size() * 5);
for (const auto& p : bubbles) { for (const auto& p : bubbles) {
bubbleVertexData.push_back(p.position.x); bubbleVertexData.push_back(p.position.x);
bubbleVertexData.push_back(p.position.y); bubbleVertexData.push_back(p.position.y);
@ -694,6 +696,7 @@ void SwimEffects::update(const Camera& camera, const CameraController& cc,
} }
insectVertexData.clear(); insectVertexData.clear();
insectVertexData.reserve(insects.size() * 5);
for (const auto& p : insects) { for (const auto& p : insects) {
insectVertexData.push_back(p.position.x); insectVertexData.push_back(p.position.x);
insectVertexData.push_back(p.position.y); insectVertexData.push_back(p.position.y);

View file

@ -922,7 +922,8 @@ void WaterRenderer::loadFromWMO([[maybe_unused]] const pipeline::WMOLiquid& liqu
float stepXLen = glm::length(surface.stepX); float stepXLen = glm::length(surface.stepX);
float stepYLen = glm::length(surface.stepY); float stepYLen = glm::length(surface.stepY);
glm::vec3 planeN = glm::cross(surface.stepX, surface.stepY); glm::vec3 planeN = glm::cross(surface.stepX, surface.stepY);
float nz = (glm::length(planeN) > 1e-4f) ? std::abs(glm::normalize(planeN).z) : 0.0f; float planeNLenSq = glm::dot(planeN, planeN);
float nz = (planeNLenSq > 1e-8f) ? std::abs(planeN.z * glm::inversesqrt(planeNLenSq)) : 0.0f;
float spanX = stepXLen * static_cast<float>(surface.width); float spanX = stepXLen * static_cast<float>(surface.width);
float spanY = stepYLen * static_cast<float>(surface.height); float spanY = stepYLen * static_cast<float>(surface.height);
if (stepXLen < 0.2f || stepXLen > 12.0f || if (stepXLen < 0.2f || stepXLen > 12.0f ||

View file

@ -221,6 +221,7 @@ void Weather::update(const Camera& camera, float deltaTime) {
// Update position buffer // Update position buffer
particlePositions.clear(); particlePositions.clear();
particlePositions.reserve(particles.size());
for (const auto& particle : particles) { for (const auto& particle : particles) {
particlePositions.push_back(particle.position); particlePositions.push_back(particle.position);
} }
@ -232,9 +233,10 @@ void Weather::updateParticle(Particle& particle, const Camera& camera, float del
// Reset if lifetime exceeded or too far from camera // Reset if lifetime exceeded or too far from camera
glm::vec3 cameraPos = camera.getPosition(); glm::vec3 cameraPos = camera.getPosition();
float distance = glm::length(particle.position - cameraPos); glm::vec3 toCamera = particle.position - cameraPos;
float distSq = glm::dot(toCamera, toCamera);
if (particle.lifetime >= particle.maxLifetime || distance > SPAWN_VOLUME_SIZE || if (particle.lifetime >= particle.maxLifetime || distSq > SPAWN_VOLUME_SIZE * SPAWN_VOLUME_SIZE ||
particle.position.y < cameraPos.y - 20.0f) { particle.position.y < cameraPos.y - 20.0f) {
// Respawn at top // Respawn at top
particle.position = getRandomPosition(cameraPos); particle.position = getRandomPosition(cameraPos);

View file

@ -2101,6 +2101,7 @@ void WMORenderer::getVisibleGroupsViaPortals(const ModelData& model,
// BFS through portals from camera's group // BFS through portals from camera's group
std::vector<bool> visited(model.groups.size(), false); std::vector<bool> visited(model.groups.size(), false);
std::vector<uint32_t> queue; std::vector<uint32_t> queue;
queue.reserve(model.groups.size());
queue.push_back(static_cast<uint32_t>(cameraGroup)); queue.push_back(static_cast<uint32_t>(cameraGroup));
visited[cameraGroup] = true; visited[cameraGroup] = true;
outVisibleGroups.insert(static_cast<uint32_t>(cameraGroup)); outVisibleGroups.insert(static_cast<uint32_t>(cameraGroup));
@ -2731,6 +2732,11 @@ void WMORenderer::GroupResources::getTrianglesInRange(
if (cellMinX > cellMaxX || cellMinY > cellMaxY) return; if (cellMinX > cellMaxX || cellMinY > cellMaxY) return;
// Reserve estimate: cells queried * ~8 triangles per cell
const size_t cellCount = static_cast<size_t>(cellMaxX - cellMinX + 1) *
static_cast<size_t>(cellMaxY - cellMinY + 1);
out.reserve(cellCount * 8);
// Collect unique triangle indices using visited bitset (O(n) dedup) // Collect unique triangle indices using visited bitset (O(n) dedup)
bool multiCell = (cellMinX != cellMaxX || cellMinY != cellMaxY); bool multiCell = (cellMinX != cellMaxX || cellMinY != cellMaxY);
if (multiCell && !triVisited.empty()) { if (multiCell && !triVisited.empty()) {
@ -2776,6 +2782,10 @@ void WMORenderer::GroupResources::getFloorTrianglesInRange(
if (cellMinX > cellMaxX || cellMinY > cellMaxY) return; if (cellMinX > cellMaxX || cellMinY > cellMaxY) return;
const size_t cellCount = static_cast<size_t>(cellMaxX - cellMinX + 1) *
static_cast<size_t>(cellMaxY - cellMinY + 1);
out.reserve(cellCount * 8);
bool multiCell = (cellMinX != cellMaxX || cellMinY != cellMaxY); bool multiCell = (cellMinX != cellMaxX || cellMinY != cellMaxY);
if (multiCell && !triVisited.empty()) { if (multiCell && !triVisited.empty()) {
for (int cy = cellMinY; cy <= cellMaxY; ++cy) { for (int cy = cellMinY; cy <= cellMaxY; ++cy) {
@ -2819,6 +2829,10 @@ void WMORenderer::GroupResources::getWallTrianglesInRange(
if (cellMinX > cellMaxX || cellMinY > cellMaxY) return; if (cellMinX > cellMaxX || cellMinY > cellMaxY) return;
const size_t cellCount = static_cast<size_t>(cellMaxX - cellMinX + 1) *
static_cast<size_t>(cellMaxY - cellMinY + 1);
out.reserve(cellCount * 8);
bool multiCell = (cellMinX != cellMaxX || cellMinY != cellMaxY); bool multiCell = (cellMinX != cellMaxX || cellMinY != cellMaxY);
if (multiCell && !triVisited.empty()) { if (multiCell && !triVisited.empty()) {
for (int cy = cellMinY; cy <= cellMaxY; ++cy) { for (int cy = cellMinY; cy <= cellMaxY; ++cy) {
@ -3112,8 +3126,9 @@ bool WMORenderer::checkWallCollision(const glm::vec3& from, const glm::vec3& to,
bool blocked = false; bool blocked = false;
glm::vec3 moveDir = to - from; glm::vec3 moveDir = to - from;
float moveDist = glm::length(moveDir); float moveDistSq = glm::dot(moveDir, moveDir);
if (moveDist < 0.001f) return false; if (moveDistSq < 1e-6f) return false;
float moveDist = std::sqrt(moveDistSq);
// Player collision parameters — WoW-style horizontal cylinder // Player collision parameters — WoW-style horizontal cylinder
// Tighter radius when inside for more responsive indoor collision // Tighter radius when inside for more responsive indoor collision
@ -3246,10 +3261,10 @@ bool WMORenderer::checkWallCollision(const glm::vec3& from, const glm::vec3& to,
glm::vec3 safeLocal = hitPoint + normal * side * (PLAYER_RADIUS + 0.05f); glm::vec3 safeLocal = hitPoint + normal * side * (PLAYER_RADIUS + 0.05f);
glm::vec3 pushLocal(safeLocal.x - localTo.x, safeLocal.y - localTo.y, 0.0f); glm::vec3 pushLocal(safeLocal.x - localTo.x, safeLocal.y - localTo.y, 0.0f);
// Cap swept pushback so walls don't shove the player violently // Cap swept pushback so walls don't shove the player violently
float pushLen = glm::length(glm::vec2(pushLocal.x, pushLocal.y)); float pushLenSq = pushLocal.x * pushLocal.x + pushLocal.y * pushLocal.y;
const float MAX_SWEPT_PUSH = insideWMO ? 0.45f : 0.25f; const float MAX_SWEPT_PUSH = insideWMO ? 0.45f : 0.25f;
if (pushLen > MAX_SWEPT_PUSH) { if (pushLenSq > MAX_SWEPT_PUSH * MAX_SWEPT_PUSH) {
float scale = MAX_SWEPT_PUSH / pushLen; float scale = MAX_SWEPT_PUSH * glm::inversesqrt(pushLenSq);
pushLocal.x *= scale; pushLocal.x *= scale;
pushLocal.y *= scale; pushLocal.y *= scale;
} }
@ -3268,9 +3283,9 @@ bool WMORenderer::checkWallCollision(const glm::vec3& from, const glm::vec3& to,
// Horizontal cylinder collision: closest point + horizontal distance // Horizontal cylinder collision: closest point + horizontal distance
glm::vec3 closest = closestPointOnTriangle(localTo, v0, v1, v2); glm::vec3 closest = closestPointOnTriangle(localTo, v0, v1, v2);
glm::vec3 delta = localTo - closest; glm::vec3 delta = localTo - closest;
float horizDist = glm::length(glm::vec2(delta.x, delta.y)); float horizDistSq = delta.x * delta.x + delta.y * delta.y;
if (horizDist <= PLAYER_RADIUS) { if (horizDistSq <= PLAYER_RADIUS * PLAYER_RADIUS) {
// Skip floor-like surfaces — grounding handles them, not wall collision. // Skip floor-like surfaces — grounding handles them, not wall collision.
// Threshold matches MAX_WALK_SLOPE (cos 50° ≈ 0.6428): surfaces steeper // Threshold matches MAX_WALK_SLOPE (cos 50° ≈ 0.6428): surfaces steeper
// than 50° from horizontal must be tested as walls to prevent clip-through. // than 50° from horizontal must be tested as walls to prevent clip-through.
@ -3280,16 +3295,17 @@ bool WMORenderer::checkWallCollision(const glm::vec3& from, const glm::vec3& to,
const float SKIN = 0.005f; // small separation so we don't re-collide immediately const float SKIN = 0.005f; // small separation so we don't re-collide immediately
// Push must cover full penetration to prevent gradual clip-through // Push must cover full penetration to prevent gradual clip-through
const float MAX_PUSH = PLAYER_RADIUS; const float MAX_PUSH = PLAYER_RADIUS;
float horizDist = std::sqrt(horizDistSq);
float penetration = (PLAYER_RADIUS - horizDist); float penetration = (PLAYER_RADIUS - horizDist);
float pushDist = glm::clamp(penetration + SKIN, 0.0f, MAX_PUSH); float pushDist = glm::clamp(penetration + SKIN, 0.0f, MAX_PUSH);
glm::vec2 pushDir2; glm::vec2 pushDir2;
if (horizDist > 1e-4f) { if (horizDistSq > 1e-8f) {
pushDir2 = glm::normalize(glm::vec2(delta.x, delta.y)); pushDir2 = glm::vec2(delta.x, delta.y) * (1.0f / horizDist);
} else { } else {
glm::vec2 n2(normal.x, normal.y); glm::vec2 n2(normal.x, normal.y);
float n2Len = glm::length(n2); float n2LenSq = glm::dot(n2, n2);
if (n2Len < 1e-4f) continue; if (n2LenSq < 1e-8f) continue;
pushDir2 = n2 / n2Len; pushDir2 = n2 * glm::inversesqrt(n2LenSq);
} }
glm::vec3 pushLocal(pushDir2.x * pushDist, pushDir2.y * pushDist, 0.0f); glm::vec3 pushLocal(pushDir2.x * pushDist, pushDir2.y * pushDist, 0.0f);
@ -3524,8 +3540,12 @@ float WMORenderer::raycastBoundingBoxes(const glm::vec3& origin, const glm::vec3
} }
glm::vec3 center = (instance.worldBoundsMin + instance.worldBoundsMax) * 0.5f; glm::vec3 center = (instance.worldBoundsMin + instance.worldBoundsMax) * 0.5f;
float radius = glm::length(instance.worldBoundsMax - center); glm::vec3 halfExtent = instance.worldBoundsMax - center;
if (glm::length(center - origin) > (maxDistance + radius + 1.0f)) { float radiusSq = glm::dot(halfExtent, halfExtent);
glm::vec3 toCenter = center - origin;
float distSq = glm::dot(toCenter, toCenter);
float maxR = maxDistance + std::sqrt(radiusSq) + 1.0f;
if (distSq > maxR * maxR) {
continue; continue;
} }

View file

@ -2990,10 +2990,11 @@ void GameScreen::processTargetInput(game::GameHandler& gameHandler) {
if (leftClickWasPress_ && input.isMouseButtonJustReleased(SDL_BUTTON_LEFT)) { if (leftClickWasPress_ && input.isMouseButtonJustReleased(SDL_BUTTON_LEFT)) {
leftClickWasPress_ = false; leftClickWasPress_ = false;
glm::vec2 releasePos = input.getMousePosition(); glm::vec2 releasePos = input.getMousePosition();
float dragDist = glm::length(releasePos - leftClickPressPos_); glm::vec2 dragDelta = releasePos - leftClickPressPos_;
float dragDistSq = glm::dot(dragDelta, dragDelta);
constexpr float CLICK_THRESHOLD = 5.0f; // pixels constexpr float CLICK_THRESHOLD = 5.0f; // pixels
if (dragDist < CLICK_THRESHOLD) { if (dragDistSq < CLICK_THRESHOLD * CLICK_THRESHOLD) {
auto* renderer = core::Application::getInstance().getRenderer(); auto* renderer = core::Application::getInstance().getRenderer();
auto* camera = renderer ? renderer->getCamera() : nullptr; auto* camera = renderer ? renderer->getCamera() : nullptr;
auto* window = core::Application::getInstance().getWindow(); auto* window = core::Application::getInstance().getWindow();
@ -11557,9 +11558,10 @@ void GameScreen::renderNameplates(game::GameHandler& gameHandler) {
renderPos.z += 2.3f; renderPos.z += 2.3f;
// Cull distance: target or other players up to 40 units; NPC others up to 20 units // Cull distance: target or other players up to 40 units; NPC others up to 20 units
float dist = glm::length(renderPos - camPos); glm::vec3 nameDelta = renderPos - camPos;
float distSq = glm::dot(nameDelta, nameDelta);
float cullDist = (isTarget || isPlayer) ? 40.0f : 20.0f; float cullDist = (isTarget || isPlayer) ? 40.0f : 20.0f;
if (dist > cullDist) continue; if (distSq > cullDist * cullDist) continue;
// Project to clip space // Project to clip space
glm::vec4 clipPos = viewProj * glm::vec4(renderPos, 1.0f); glm::vec4 clipPos = viewProj * glm::vec4(renderPos, 1.0f);
@ -11576,7 +11578,9 @@ void GameScreen::renderNameplates(game::GameHandler& gameHandler) {
float sy = (ndc.y * 0.5f + 0.5f) * screenH; float sy = (ndc.y * 0.5f + 0.5f) * screenH;
// Fade out in the last 5 units of cull range // Fade out in the last 5 units of cull range
float alpha = dist < (cullDist - 5.0f) ? 1.0f : 1.0f - (dist - (cullDist - 5.0f)) / 5.0f; float fadeSq = (cullDist - 5.0f) * (cullDist - 5.0f);
float dist = std::sqrt(distSq);
float alpha = distSq < fadeSq ? 1.0f : 1.0f - (dist - (cullDist - 5.0f)) / 5.0f;
auto A = [&](int v) { return static_cast<int>(v * alpha); }; auto A = [&](int v) { return static_cast<int>(v * alpha); };
// Bar colour by hostility (grey for corpses) // Bar colour by hostility (grey for corpses)