refactor: name FNV-1a/transport constants, fix dead code, add comments

- vk_context: name FNV-1a hash constants (kFnv1aOffsetBasis/kFnv1aPrime)
  with why-comment on algorithm choice for sampler cache
- transport_manager: collapse redundant if/else that both set
  looping=false into single unconditional assignment, add why-comment
  explaining the time-closed path design
- transport_manager: hoist duplicate kMinFallbackZOffset constants out
  of separate if-blocks, add why-comment on icebreaker Z clamping
- entity: expand velocity smoothing comment — explain 65/35 EMA ratio
  and its tradeoff (jitter suppression vs direction change lag)
This commit is contained in:
Kelsi 2026-03-30 14:48:06 -07:00
parent a940859e6a
commit 8c7db3e6c8
3 changed files with 21 additions and 13 deletions

View file

@ -93,7 +93,9 @@ public:
float impliedVX = (destX - fromX) / durationSec; float impliedVX = (destX - fromX) / durationSec;
float impliedVY = (destY - fromY) / durationSec; float impliedVY = (destY - fromY) / durationSec;
float impliedVZ = (destZ - fromZ) / durationSec; float impliedVZ = (destZ - fromZ) / durationSec;
// Exponentially smooth velocity so jittery packet timing doesn't snap speed. // Exponential moving average on velocity — 65% new sample, 35% previous.
// Smooths out jitter from irregular server update intervals (~200-600ms)
// without introducing visible lag on direction changes.
const float alpha = 0.65f; const float alpha = 0.65f;
velX_ = alpha * impliedVX + (1.0f - alpha) * velX_; velX_ = alpha * impliedVX + (1.0f - alpha) * velX_;
velY_ = alpha * impliedVY + (1.0f - alpha) * velY_; velY_ = alpha * impliedVY + (1.0f - alpha) * velY_;

View file

@ -203,16 +203,17 @@ void TransportManager::loadPathFromNodes(uint32_t pathId, const std::vector<glm:
path.points.push_back({cumulativeMs, waypoints[i]}); path.points.push_back({cumulativeMs, waypoints[i]});
} }
// Add explicit wrap segment (last → first) for looping paths // Add explicit wrap segment (last → first) for looping paths.
// By duplicating the first point at the end with cumulative time, the path
// becomes time-closed and evalTimedCatmullRom handles wrap via modular time
// instead of index wrapping — so looping is always false after construction.
if (looping) { if (looping) {
float wrapDist = glm::distance(waypoints.back(), waypoints.front()); float wrapDist = glm::distance(waypoints.back(), waypoints.front());
uint32_t wrapMs = glm::max(1u, segMsFromDist(wrapDist)); uint32_t wrapMs = glm::max(1u, segMsFromDist(wrapDist));
cumulativeMs += wrapMs; cumulativeMs += wrapMs;
path.points.push_back({cumulativeMs, waypoints.front()}); // Duplicate first point path.points.push_back({cumulativeMs, waypoints.front()});
path.looping = false; // Time-closed path, no need for index wrapping
} else {
path.looping = false;
} }
path.looping = false;
path.durationMs = cumulativeMs; path.durationMs = cumulativeMs;
paths_[pathId] = path; paths_[pathId] = path;
@ -301,14 +302,16 @@ void TransportManager::updateTransportMovement(ActiveTransport& transport, float
// Guard against bad fallback Z curves on some remapped transport paths (notably icebreakers), // Guard against bad fallback Z curves on some remapped transport paths (notably icebreakers),
// where path offsets can sink far below sea level when we only have spawn-time data. // where path offsets can sink far below sea level when we only have spawn-time data.
// Skip Z clamping for world-coordinate paths (TaxiPathNode) where values are absolute positions. // Skip Z clamping for world-coordinate paths (TaxiPathNode) where values are absolute positions.
// Clamp fallback Z offsets for non-world-coordinate paths to prevent transport
// models from sinking below sea level on paths derived only from spawn-time data
// (notably icebreaker routes where the DBC path has steep vertical curves).
constexpr float kMinFallbackZOffset = -2.0f;
constexpr float kMaxFallbackZOffset = 8.0f;
if (!path.worldCoords) { if (!path.worldCoords) {
if (transport.useClientAnimation && transport.serverUpdateCount <= 1) { if (transport.useClientAnimation && transport.serverUpdateCount <= 1) {
constexpr float kMinFallbackZOffset = -2.0f;
pathOffset.z = glm::max(pathOffset.z, kMinFallbackZOffset); pathOffset.z = glm::max(pathOffset.z, kMinFallbackZOffset);
} }
if (!transport.useClientAnimation && !transport.hasServerClock) { if (!transport.useClientAnimation && !transport.hasServerClock) {
constexpr float kMinFallbackZOffset = -2.0f;
constexpr float kMaxFallbackZOffset = 8.0f;
pathOffset.z = glm::clamp(pathOffset.z, kMinFallbackZOffset, kMaxFallbackZOffset); pathOffset.z = glm::clamp(pathOffset.z, kMinFallbackZOffset, kMaxFallbackZOffset);
} }
} }

View file

@ -16,13 +16,16 @@ namespace rendering {
VkContext* VkContext::sInstance_ = nullptr; VkContext* VkContext::sInstance_ = nullptr;
// Hash a VkSamplerCreateInfo into a 64-bit key for the sampler cache. // Hash a VkSamplerCreateInfo into a 64-bit key for the sampler cache.
// FNV-1a chosen for speed and low collision rate on small structured data.
// Constants from: http://www.isthe.com/chongo/tech/comp/fnv/
static constexpr uint64_t kFnv1aOffsetBasis = 14695981039346656037ULL;
static constexpr uint64_t kFnv1aPrime = 1099511628211ULL;
static uint64_t hashSamplerCreateInfo(const VkSamplerCreateInfo& s) { static uint64_t hashSamplerCreateInfo(const VkSamplerCreateInfo& s) {
// Pack the relevant fields into a deterministic hash. uint64_t h = kFnv1aOffsetBasis;
// FNV-1a 64-bit on the raw config values.
uint64_t h = 14695981039346656037ULL;
auto mix = [&](uint64_t v) { auto mix = [&](uint64_t v) {
h ^= v; h ^= v;
h *= 1099511628211ULL; h *= kFnv1aPrime;
}; };
mix(static_cast<uint64_t>(s.minFilter)); mix(static_cast<uint64_t>(s.minFilter));
mix(static_cast<uint64_t>(s.magFilter)); mix(static_cast<uint64_t>(s.magFilter));