Pre-allocate one stable VkDescriptorSet per particle emitter at model
upload time (particleTexSets[]) instead of allocating a new set from
materialDescPool_ every frame for each particle group. The per-frame
path exhausted the 8192-set pool in ~14 s at 60 fps with 10 active
particle emitters, causing GPU device-lost crashes. The old path is
kept as an explicit fallback but should never be reached in practice.
When the server has not sent SMSG_INIT_WORLD_STATES or the mask is
empty, fall back to locally-accumulated explored zones tracked by
player position. The local set is cleared when a real server mask
arrives so it doesn't persist stale data.
Trivial/low-level quests now show gray '!' / '?' markers instead of
yellow, matching the in-game distinction between available and trivial
quests. Add grayscale parameter to QuestMarkerRenderer::setMarker and
the push-constant block; application sets grayscale=1.0 for trivial
markers and 0.0 for all others.
Camera controller / sitting:
- Any movement key (WASD/QE/Space) pressed while sitting now clears the
sitting flag immediately, matching WoW's sit-to-stand-on-move behaviour
- Added StandUpCallback: when the player stands up via local input the
callback fires setStandState(0) → CMSG_STAND_STATE_CHANGE(STAND) so
the server releases the sit lock and restores normal movement
- Fixes character getting stuck in sit state after accidentally
right-clicking a chair GO in Goldshire Inn (or similar)
Nameplates:
- Use getRenderPositionForGuid() (renderer visual position) as primary
source for nameplate anchor, falling back to entity X/Y/Z only when
no render instance exists yet; keeps health bars in sync with the
rendered model instead of the parallel entity interpolator
- wmo_renderer: pass character position (not camera position) to portal
visibility traversal — the 3rd-person camera can orbit outside a WMO
while the character is inside, causing interior groups to cull; render()
now accepts optional viewerPos that defaults to camPos for compatibility
- renderer: pass &characterPosition to wmoRenderer->render() at both
main and single-threaded call sites; reflection pass keeps camPos
- renderer: apply mount pitch/roll to rider during all flight, not just
taxiFlight_ (fixes zero rider tilt during player-controlled flying)
- game_screen: format SAY/YELL/WHISPER/EMOTE using WoW-style "Name says:"
instead of "[SAY] Name:" bracket prefix
When flyingActive_, detect Space/X key transitions and emit proper flight
vertical movement opcodes so the server (and other players) see the
correct ascending/descending animation state:
- MSG_MOVE_START_ASCEND (Space pressed while flying) → sets ASCENDING flag
- MSG_MOVE_STOP_ASCEND (Space released while flying) → clears ASCENDING flag
- MSG_MOVE_START_DESCEND (X pressed while flying) → clears ASCENDING flag
- MSG_MOVE_STOP_ASCEND (X released while flying) → clears vertical state
Track wasAscending_/wasDescending_ member state to detect transitions.
Also clear lingering vertical state when leaving flight mode.
- Add getServerTurnRate() accessor and turnRateOverride_ field so the
keyboard turn speed respects SMSG_FORCE_TURN_RATE_CHANGE from server
- Convert rad/s → deg/s before applying to camera yaw logic
- Fix SMSG_SPLINE_SET_RUN_BACK/SWIM/FLIGHT/FLIGHT_BACK/SWIM_BACK/WALK/
TURN_RATE handlers: all previously discarded the value; now update the
corresponding serverXxxSpeed_ / serverTurnRate_ field when GUID matches
playerGuid (camera controller syncs these every frame)
SMSG_FORCE_FLIGHT_BACK_SPEED_CHANGE was already ACK'd and stored in
serverFlightBackSpeed_, but the value was never accessible or synced
to the CameraController. Backward flight movement always used forward
flight speed (flightSpeedOverride_), making it faster than the server
intended.
- Add getServerFlightBackSpeed() accessor in GameHandler
- Add flightBackSpeedOverride_ field and setter in CameraController
- Apply it in the fly movement block: backward-only flight uses the
back speed; forward or strafing uses the forward speed as WoW does
- Fallback: 50% of forward flight speed when override is unset
- Sync per-frame in application.cpp alongside the other speed overrides
Backward swimming was using 50% of forward swim speed as a hardcoded
fallback. Wire up the server-authoritative swim back speed so Warlock
Dark Pact, buffs, and server-forced speed changes all apply correctly
when swimming backward.
- game_handler.hpp: add getServerSwimBackSpeed() accessor
- camera_controller.hpp: add swimBackSpeedOverride_ field + setter
- camera_controller.cpp: apply swimBackSpeedOverride_ when player
swims backward without forward input; fall back to 50% of swim speed
- application.cpp: sync swim back speed each frame
When the server sets MovementFlags::HOVER (SMSG_MOVE_SET_HOVER), the
player now floats 4 yards above the nearest ground surface instead of
standing on it. Uses the existing floor-snap path with a HOVER_HEIGHT
offset applied to the snap target.
- game_handler.hpp: add isHovering() accessor (reads HOVER flag from
movementInfo.flags, which is already set by handleForceMoveFlagChange)
- camera_controller.hpp: add hoverActive_ field and setHoverActive()
- camera_controller.cpp: apply HOVER_HEIGHT = 4.0f offset at floor snap
- application.cpp: sync hover state each frame alongside other movement
states (gravity, feather fall, water walk, flying)
Previously only run speed was synced. Now all server-driven movement
speeds are forwarded to the camera controller each frame:
- runSpeedOverride_: server run speed (existing)
- walkSpeedOverride_: server walk speed (Ctrl key movement)
- swimSpeedOverride_: swim speed (Swim Form, Engineering fins)
- flightSpeedOverride_: flight speed (epic vs normal flying mounts)
- runBackSpeedOverride_: back-pedal speed
Each uses the server value when non-zero/sane, falling back to the
hardcoded WoW default constant otherwise.
serverFlightSpeed_ (from SMSG_FORCE_FLIGHT_SPEED_CHANGE) was stored but
never synced to CameraController. Add getServerFlightSpeed() accessor,
flightSpeedOverride_ field, and use it in the flying physics path so
normal vs epic flying mounts actually move at their correct speeds.
When CAN_FLY + FLYING movement flags are both set (flying mounts, Druid
Flight Form), the CameraController now uses 3D pitch-following movement
instead of ground physics:
- Forward/back follows the camera's 3D look direction (ascend when
looking up, descend when looking down)
- Space = ascend vertically, X (while mounted) = descend
- No gravity, no grounding, no jump coyote time
- Fall-damage checks suppressed (grounded=true)
Also wire up all remaining server movement state flags to CameraController:
- Feather Fall: cap terminal velocity at -2 m/s
- Water Walk: clamp to water surface, skip swim entry
- Flying: 3D movement with no gravity
All states synced each frame from GameHandler via isPlayerFlying(),
isFeatherFalling(), isWaterWalking(), isGravityDisabled().
SMSG_MOVE_WATER_WALK / SMSG_MOVE_LAND_WALK now correctly set/clear
WATER_WALK (0x00008000) in movementInfo.flags, ensuring the flag is
included in movement ACKs sent to the server.
In CameraController, when waterWalkActive_ is set and the player is
at or above the water surface (within 0.5 units), clamp them to the
water surface and mark as grounded — preventing water entry and allowing
them to walk across the water surface as the spell intends.
Feather Fall (SMSG_MOVE_FEATHER_FALL / SMSG_MOVE_NORMAL_FALL):
- Add FEATHER_FALL = 0x00004000 to MovementFlags enum
- Fix handlers to set/clear the flag instead of passing flag=0
- Cap downward terminal velocity at -2.0 m/s in CameraController when
feather fall is active (Slow Fall, Parachute, etc.)
All three handlers now correctly propagate server movement state flags
that were previously acknowledged without updating any local state.
serverWalkSpeed_ and serverSwimSpeed_ were stored in GameHandler but
never exposed or synced to the camera controller. The controller used
hardcoded WOW_WALK_SPEED and speed*SWIM_SPEED_FACTOR regardless of
server-sent speed changes.
Add getServerWalkSpeed()/getServerSwimSpeed() accessors, walkSpeedOverride_
and swimSpeedOverride_ fields in CameraController, and sync all three
server speeds each frame. Both swim speed sites (main and camera-collision
path) now use the override when set. This makes Slow debuffs (walk speed),
Swim Form, and Engineering fins actually affect movement speed.
SMSG_MOVE_GRAVITY_DISABLE/ENABLE now correctly set/clear the LEVITATING
movement flag instead of passing flag=0. GameHandler::isGravityDisabled()
reads the LEVITATING bit and is synced to CameraController each frame.
When gravity is disabled the physics loop bleeds off downward velocity
and skips gravity accumulation, so Levitate and similar effects actually
float the player rather than letting them fall through the world.
When SMSG_FORCE_MOVE_ROOT sets ROOT in movementInfo.flags, the
camera controller was not aware and continued to accept directional
input. This caused position desync (client moves, server sees player
as rooted).
- Add movementRooted_ flag to CameraController with setter/getter.
- Block nowForward/nowBackward/nowStrafe when movementRooted_ is set.
- Sync isPlayerRooted() from GameHandler to CameraController each
frame alongside the existing run-speed sync in application.cpp.
- Add GameHandler::isPlayerRooted() convenience accessor.
Previously the handler ACKed with current position and ignored the
velocity fields entirely (vcos/vsin/hspeed/vspeed were [[maybe_unused]]).
The server expects the client to fly through the air on knockback — without
simulation the player stays in place while the server models them as airborne,
causing position desync and rubberbanding.
Changes:
- CameraController: add applyKnockBack(vcos, vsin, hspeed, vspeed)
that sets knockbackHorizVel_ and launches verticalVelocity = -vspeed
(server sends vspeed as negative for upward launches, matching TrinityCore)
- Physics loop: each tick adds knockbackHorizVel_ to targetPos then applies
exponential drag (KNOCKBACK_HORIZ_DRAG=4.5/s) until velocity < 0.05 u/s
- GameHandler: parse all four fields, add KnockBackCallback, call it for
the local player so the camera controller receives the impulse
- Application: register the callback — routes server knockback to physics
The existing ACK path is unchanged; the server gets position confirmation
as before while the client now actually simulates the trajectory.
The AABB transform bug (direct min/max transform was wrong for rotated
WMOs) was fixed in a prior commit. Portal culling now uses the correct
world-space AABB computed from all 8 corners, so frustum intersection
is valid.
The AABB-based test is conservative (no portal plane-side check): a
visible portal can only be incorrectly INCLUDED, never EXCLUDED. This
means no geometry can disappear, and any overdraw is handled by the
z-buffer. Enable by default to get the performance benefit inside WMOs
and dungeons.
The renderer's CharAnimState machine already drives player character
animations (Run=5, Walk=4, Jump, Swim, etc.) — remove the conflicting
camera controller code added in the previous commit.
Fix creature movement animations to use the correct WoW M2 IDs:
4=Walk, 5=Run. Both the per-frame sync loop and the SMSG_MONSTER_MOVE
spline callback now use Run (5) for NPC movement.
CameraController now transitions the player character to Run (anim 4)
on movement start and back to Stand (anim 0) on stop, guarded by a
prevPlayerMoving_ flag so animation time is not reset every frame.
Death animation (anim 1) is never overridden.
Application creature sync similarly switches creature models to Run (4)
when they move between server positions and Stand (0) when they stop,
with per-guid creatureWasMoving_ tracking to avoid per-frame resets.
Add GhostStateCallback to GameHandler, fired when PLAYER_FLAGS_GHOST
transitions on or off in UPDATE_OBJECT / login detection. Add
setInstanceOpacity() to CharacterRenderer to directly set opacity
without disturbing fade-in state. Application wires the callback to
set opacity 0.5 on ghost entry and 1.0 on resurrect.
Add CameraController::setSitting() and call it from the StandStateCallback
so the camera blocks movement when the server confirms the player is
sitting or kneeling (stand states 1-6, 8). This prevents the player
from sliding across the ground after sitting.
Death (state 7) deliberately leaves sitting=false so the player can
still respawn/move after death without input being blocked.
Previously disabled because the per-frame raycast caused erratic zoom
snapping at doorway transitions. Re-enable using an asymmetrically-
smoothed collision limit: pull-in reacts quickly (τ≈60 ms) to prevent
the camera from ever visibly clipping through walls, while recovery is
slow (τ≈400 ms) so walking through a doorway zooms back out gradually
instead of snapping.
Uses wmoRenderer->raycastBoundingBoxes() which already has strict wall
filters (|normal.z|<0.20, surface-alignment check, ±0.9 height band)
to ignore floors, ramps, and arch geometry.
The selection circle was positioned using the entity's game-logic
interpolator (entity->getX/Y/Z), while the actual M2 model is
positioned by CharacterRenderer's independent interpolator (moveInstanceTo).
These two systems can drift apart during movement, causing the circle
to appear under the wrong position relative to the visible model.
Fix: add CharacterRenderer::getInstancePosition / Application::getRenderPositionForGuid
and use the renderer's inst.position for XY (with footZ override for Z)
so the circle always tracks the rendered model exactly. Falls back to
the entity game-logic position when no CharacterRenderer instance exists.
Single shadow depth image shared across MAX_FRAMES=2 in-flight GPU frames
caused a race: frame N's main pass reads shadow map while frame N+1's
shadow pass clears and writes it, producing visible flashing standing
still and while moving.
Fix: give each in-flight frame its own VkImage, VmaAllocation, VkImageView,
and VkFramebuffer for the shadow depth attachment. renderShadowPass() now
indexes all shadow resources by getCurrentFrame(), and layout transitions
track per-frame state in shadowDepthLayout_[frame]. Cleanup loops over
MAX_FRAMES=2. Descriptor sets already written per-frame; updated shadow
image view binding to use the matching per-frame view.
Read the ambient color from the MOHD chunk (BGRA uint32) and store it
on WMOModel as a normalized RGB vec3. Pass it through ModelData into
the per-batch WMOMaterialUBO (replacing the unused pad[3] bytes, keeping
the struct at 64 bytes). The GLSL interior branch now floors vertex
colors against the WMO ambient instead of a hardcoded 0.5, so dungeon
interiors respect the artist-specified ambient tint from the WMO root
rather than always clamping to grey.
Hang/GPU device lost fix:
- M2_INSTANCES and WMO_INSTANCES finalization phases now create instances
incrementally (32 per step / 4 per step) instead of all at once, eliminating
the >1s main-thread stalls that caused GPU fence timeouts and device loss
M2 two-pass transparent rendering:
- Opaque/alpha-test batches render in pass 1, transparent/additive in pass 2
(back-to-front sorted) to fix wing transparency showing terrain instead of
trees — adds hasTransparentBatches flag to skip models with no transparency
Tile streaming improvements:
- Sort new load queue entries nearest-first so critical tiles load before
distant ones during fast taxi flight
- Increase taxi load radius 6→8 tiles, unload 9→12 for better coverage
Water refraction gated on FSR:
- Disable water refraction when FSR is not active (bugged without upscaling)
- Auto-disable refraction if FSR is turned off while refraction was on
Add initializeShadow() to TerrainRenderer that creates a depth-only
shadow pipeline reusing the existing shadow.vert/frag shaders (same
path as WMO/M2/character renderers). renderShadow() draws all terrain
chunks with sphere culling against the shadow coverage radius. Wire
both init and draw calls into Renderer so terrain now casts shadows
alongside buildings and NPCs.
The boneDescPool_ had MAX_BONE_SETS=2048 but sets were never freed when
instances were removed (only when clear() reset the whole pool on map load).
As tiles streamed in/out, each new animated instance consumed 2 pool slots
(one per frame index) permanently. After ~1024 animated instances created
total, vkAllocateDescriptorSets began failing silently and returning
VK_NULL_HANDLE. render() skips instances with null boneSet[frameIndex],
making them invisible — appearing as per-frame flicker as the culling pass
included them but the render pass excluded them.
Fix: destroyInstanceBones() now calls vkFreeDescriptorSets() for each
non-null boneSet before destroying the bone SSBO. The pool already had
VK_DESCRIPTOR_POOL_CREATE_FREE_DESCRIPTOR_SET_BIT set for this purpose.
Also increased MAX_BONE_SETS from 2048 to 8192 for extra headroom.
texture.hpp / texture.cpp implemented an unfinished OpenGL texture loader
(loadFromFile was a TODO stub) that had no callers — the project's texture
loading is entirely handled by VkTexture (vk_texture.hpp/cpp) after the
Vulkan migration. Remove both files and their CMakeLists entries.
- AmdFsr3Runtime now probes both the legacy ffxFsr3* API and the newer
generic ffxCreateContext/ffxDispatch API; selects whichever the loaded
runtime library exports (GenericApi takes priority fallback)
- Generic API path implements full upscale + frame-generation context
creation, configure, dispatch, and destroy lifecycle
- dlopen error captured and surfaced in lastError_ on Linux so runtime
initialization failures are actionable
- FSR3 runtime init failure log now includes path kind, error string,
and loaded library path for easier debugging
- tools/generate_ffx_sdk_vk_permutations.sh added: auto-bootstraps
missing VK permutation headers; DXC auto-downloaded on Linux/Windows
MSYS2; macOS reads from PATH (CI installs via brew dxc)
- CMakeLists: add upscalers/include to probe include dirs, invoke
permutation script before SDK build, scope FFX pragma/ODR warning
suppressions to affected TUs, add runtime-copy dependency on wowee
- UI labels updated from "FSR2" → "FSR3" in settings, tuning panel,
performance HUD, and combo boxes
- CI macOS job now installs dxc via Homebrew for permutation codegen