Storm weather (wType==3 from SMSG_WEATHER) previously rendered no
visual particles and no audio. Map it to RAIN in the weather system so
thunderstorms produce rain particles at the server-sent intensity level,
and the ambient sound manager picks up rain_heavy/medium/light audio
from the same intensity logic already used for plain rain.
This pairs with the lightning commit — storms now have both rain
particles and lightning flashes for a complete thunderstorm experience.
The lightning system (lightning.hpp/cpp) was fully implemented but never
wired into the renderer. Connect it now:
- Enable lightning during server storm weather (wType==3, intensity>0.1)
and heavy rain (wType==1, intensity>0.7) as a bonus visual
- Scale lightning intensity proportionally to weather intensity
- Render in both parallel (SEC_POST) and fallback rendering paths
- Update and shutdown alongside the weather system
- Show active lightning info in the performance HUD weather section
gameObjectDisplayIdWmoCache_ was not cleared on world unload/transition,
causing stale WMO model IDs (e.g. 40006, 40003) to be looked up after
the renderer cleared its model list, resulting in "Cannot create instance
of unloaded WMO model" errors on zone re-entry.
Changes:
- Clear gameObjectDisplayIdWmoCache_ alongside other GO caches on world reset
- Add WMORenderer::isModelLoaded() for cache-hit validation
- Inline GO WMO path now verifies cached model is still renderer-resident
before using it; evicts stale entries and falls back to reload
When unloadTile() was called for a tile still in finalizingTiles_
(mid-incremental-finalization), terrain chunks already uploaded to the
GPU (terrainMeshDone=true) were not being cleaned up. The early-return
path correctly removed water and M2/WMO instances but missed calling
terrainRenderer->removeTile(), causing descriptor sets to leak.
After ~20 minutes of play the VkDescriptorPool (MAX_MATERIAL_SETS=16384)
filled up, causing all subsequent terrain material allocations to fail
and the log to flood with "failed to allocate material descriptor set".
Fix: check fit->terrainMeshDone before the early return and call
terrainRenderer->removeTile() to free those descriptor sets.
wmo_renderer: when portal BFS starts from a group with no portal refs
(utility/transition group), the rest of the WMO becomes invisible because
BFS only adds the starting group. Fix: if cameraGroup has portalCount==0,
fall back to marking all groups visible (same as camera-outside behavior).
renderer: minimap player-orientation arrow was pointing the wrong direction.
The shader convention is arrowRotation=0→North, positive→clockwise (West),
negative→East. The correct mapping from canonical yaw is arrowRotation =
-canonical_yaw. Fixed both render paths (cameraController and gameHandler)
in both the FXAA and non-FXAA minimap render calls.
World-map W-key: already corrected in prior commit (13c096f); users with
stale ~/.wowee/settings.cfg should rebind via Settings > Controls.
Two follow-up fixes for the ribbon emitter implementation and the
transport-doodad stall fix:
1. loadModel() rejected any M2 with no vertices AND no particles, but
ribbon-only spell-effect models (e.g. weapon trail or aura ribbons)
have neither. These models were silently invisible even though the
ribbon rendering pipeline added in 1108aa9 is fully capable of
rendering them. Extended the guard to also accept models that have
ribbon emitters, matching the particle-emitter precedent.
2. processPendingTransportDoodads() ignored the bool return of
loadModel(), calling createInstance() even when the model was
rejected, generating spurious "Cannot create instance: model X not
loaded" warnings for every failed doodad path. Check the return
value and continue to the next doodad on failure.
Three root causes identified from wowee.log crash at frame 134368:
1. processPendingTransportDoodads() was doing N separate synchronous
GPU uploads (vkQueueSubmit + vkWaitForFences per texture per doodad).
With 30+ doodads × multiple textures, this caused the 489ms stall in
the 'gameobject/transport queues' update stage. Fixed by wrapping the
entire batch in beginUploadBatch()/endUploadBatch() so all texture
layout transitions are submitted in a single async command buffer.
2. Game objects whose M2 model has no geometry/particles (empty or
unsupported format) were retried every frame because loadModel()
returns false without adding to gameObjectDisplayIdModelCache_.
Added gameObjectDisplayIdFailedCache_ to permanently skip these
display IDs after the first failure, stopping the per-frame spam.
3. renderM2Ribbons() only checked ribbonPipeline_ != null, not
ribbonAdditivePipeline_. If additive pipeline creation failed, any
ribbon with additive blending would call vkCmdBindPipeline with
VK_NULL_HANDLE, causing VK_ERROR_DEVICE_LOST on the GPU side.
Extended the early-return guard to cover both ribbon pipelines.
- TOGGLE_QUEST_LOG: change default from Q to None — Q conflicts with
strafe-left in camera_controller; quest log already accessible via
TOGGLE_QUESTS (L, the standard WoW binding)
- Equipment Set Manager: remove hardcoded SDL_SCANCODE_GRAVE shortcut
(~` should not be used for this)
- World map M key: remove duplicate SDL_SCANCODE_M self-handler from
world_map.cpp::render() that was desync-ing with game_screen's
TOGGLE_WORLD_MAP binding; game_screen now owns open/close, render()
handles initial zone load and ESC-close signalling via isOpen()
Parse M2RibbonEmitter data (WotLK format) from M2 files — bone index,
position, color/alpha/height tracks, edgesPerSecond, edgeLifetime,
gravity. Add CPU-side trail simulation per instance (edge birth at bone
world position, lifetime expiry, gravity droop). New m2_ribbon.vert/frag
shaders render a triangle-strip quad per emitter using the existing
particleTexLayout_ descriptor set. Supports both alpha-blend and additive
pipeline variants based on material blend mode. Fixes invisible spell
trail effects (~5-10%% of spell visuals) that were silently skipped.
- FXAA path: repurpose _pad field as 'desaturate' push constant; when
ghostMode_ is true, convert final pixel to grayscale with slight cool
blue tint using luma(0.299,0.587,0.114) mix
- Non-FXAA path: apply a high-opacity gray overlay (rgba 0.5,0.5,0.55,0.82)
over the scene for a washed-out look
- Both parallel (SEC_POST) and single-threaded render paths covered
- ghostMode_ flag set each frame from gameHandler->isPlayerGhost()
- Post-FXAA unsharp mask: when FSR2 is active alongside FXAA, forward
the FSR2 sharpness value (0–2) to the FXAA fragment shader via a new
vec4 push constant. A contrast-adaptive sharpening step (unsharp mask
scaled to 0–0.3) is applied after FXAA blending, recovering the
crispness that FXAA's sub-pixel blend removes. At sharpness=2.0 the
output matches RCAS quality; at sharpness=0 the step is a no-op.
- MSAA guard: setFXAAEnabled() refuses to activate FXAA when hardware
MSAA is in use. FXAA's role is to supplement FSR temporal AA, not to
stack on top of MSAA which already resolves jaggies during the scene
render pass.
FSR EASU and FSR2 sharpen fragment shaders had a manual Y-flip to undo
the now-removed postprocess.vert flip. Strip those since the vertex
shader no longer flips, making all postprocess paths consistent.
Also flip the default mouse Y-axis to match user expectation (mouse
down = look up / flight-sim style) and make FSR1 disable MSAA on
enable, matching FSR2 behaviour (FSR provides its own spatial AA).
SDL yrel > 0 means the mouse moved downward. In WoW, moving the mouse
down should decrease pitch (look down), but the previous code did
+= yrel which increased pitch (look up). This made the camera appear
inverted — moving the mouse down tilted the view upward. The invertMouse
option accidentally produced the correct WoW-default behaviour.
Fix: negate the default invert factor so mouse-down = look down without
InvertMouse, and mouse-down = look up when InvertMouse is enabled.
- Add triggerShake(magnitude, frequency, duration) to CameraController
- Apply envelope-decaying sinusoidal XYZ offset to camera in update()
- Handle SMSG_CAMERA_SHAKE opcode in GameHandler dispatch
- Translate shakeId to magnitude (minor <50: 0.04, larger: 0.08 world units)
- Wire CameraShakeCallback from GameHandler through to CameraController
- Shake uses 18Hz oscillation with 30% fade-out envelope at end of duration
- Remove fsr2Active guard that prevented FXAA when FSR3 was active
- FXAA checkbox now always enabled; tooltip adapts to explain FSR3+FXAA combo
when FSR3 is active ('recommended ultra-quality combination')
- Performance HUD shows 'FXAA: ON (FSR3+FXAA combined)' when both active
- Ultra graphics preset now enables FXAA (8x MSAA + FXAA for max smoothness)
- Preset detection updated to require FXAA for Ultra match
cleanupUnusedModels() runs every 5 seconds and freed vertex/index buffers
without waiting for the GPU to finish the previous frame's command buffer.
This caused VK_ERROR_DEVICE_LOST (-4) after extended gameplay when tiles
stream out and their models are freed mid-render.
Add vkDeviceWaitIdle() before the buffer destroy loop in both M2Renderer
and WMORenderer cleanupUnusedModels(). The wait only happens when there are
models to remove, so quiet sessions have no overhead.
- Remove !fsr_.enabled / !fsr2_.enabled guards that blocked FXAA init
- FXAA can now coexist with FSR1 and FSR3 simultaneously
- Priority: FSR3 > FXAA > FSR1
- FSR3 + FXAA: scene renders at FSR3 internal res, temporal AA runs,
then FXAA reads FSR3 history and applies spatial AA to swapchain
(replaces RCAS sharpening for ultra-quality native mode)
- FXAA + FSR1: scene renders at native res, FXAA post-processes;
FSR1 resources exist but are idle (FXAA wins for better quality)
- FSR3 only / FSR1 only: unchanged paths
- Fix missing fxaa.frag.spv: shader was present but uncompiled; the
CMake compile_shaders() function will now pick it up on next build
- Draw zone name text centered in each zone rect on the continent view;
only rendered when the rect is large enough to fit the label without
crowding (explored zones get gold text, unexplored get dim grey)
- Show WoW coordinates under the cursor when hovering the map image in
continent or zone view, bottom-right corner of the map panel
Persist damage_flash to settings.cfg; checkbox in Interface > Screen Effects.
Fix world map fog: trust server exploration mask unconditionally when present,
always reveal the current zone immediately regardless of server mask state.
Floating-point fmod() loses precision with large accumulated time values, causing
subtle jumps/hitches in animation loops. Replace with iterative duration subtraction
to keep animationTime bounded and maintain precision, consistent with the fix
applied to character_renderer.cpp.
Applies to:
- M2 creature/object animation loops (main update)
- M2 particle-only instance wrapping (3333ms limit)
- M2 global sequence timing resolution
- M2 animated particle tile indexing
- Mount bobbing motion (sinusoidal rider motion)
- Character footstep trigger timing
- Mount footstep trigger timing
All timing computations now use the same precision-preserving approach.
Addresses sparseness in lava/magma effects noted in status documentation.
Higher emission rate (48 vs 32 per second) makes lava/slime areas visually
denser and more immersive while staying within GPU budget constraints.
Replace floating-point fmod() with iterative duration subtraction to preserve precision.
When animation time accumulates over many loops, fmod() loses precision with large values,
causing subtle jumps/hitches in looping animations. Subtracting the duration instead keeps
animationTime bounded in [0, duration) and avoids precision loss.
Double the smoke particle emission rate to create visually richer lava and magma
effects. Current implementation emitted only 16 particles/sec per emitter (~88 in
steady state), which appeared sparse especially in multi-emitter lava areas.
Increasing to 32/sec provides denser steam/smoke effects (~176 in steady state)
while remaining well under the 1000 particle cap. This tuning opportunity was
documented in status.md as a known gap in visual completeness.
Add view-frustum intersection testing to QuestMarkerRenderer::render() using
Frustum::intersectsSphere(), bringing quest marker culling in line with the
character instance and WMO group frustum culling improvements. Reduces marker
visibility testing overhead in scenes with many off-screen quest givers.
Replace ad-hoc cone-based backface culling with proper view-frustum intersection
testing using Frustum::intersectsSphere(). Characters are now culled based on
visibility within the view frustum, improving accuracy in complex scenes and
reducing overdraw. Maintains distance-based culling for broad radius filtering.
Replace the basic forward-vector culling (which only culls when all AABB
corners are behind the camera) with proper frustum-AABB intersection testing
for more accurate and aggressive visibility culling. This reduces overdraw
and improves rendering performance in WMO-heavy scenes (dungeons, buildings).
Enable shadows in character preview with 0.5 strength for a subtle
lighting effect that improves visual accuracy. Removes clearShadowMap()
call and enables shadowParams in preview UBO. Enhances character
appearance fidelity when viewing equipment and customization options.
Move ShadowParamsUBO from 5 separate shadow rendering functions (2 in
m2_renderer, 1 in terrain_renderer, 1 in wmo_renderer) into shared
vk_frame_data.hpp header. Eliminates 5 identical local struct definitions
and improves consistency across all shadow pass implementations. Structure
layout matches shader std140 uniform buffer requirements.
Move envSizeMBOrDefault and envSizeOrDefault from 4 separate rendering
modules (character_renderer, m2_renderer, terrain_renderer, wmo_renderer)
into shared vk_utils.hpp header as inline functions. Use the most robust
version which includes overflow checking for MB-to-bytes conversion. This
eliminates 7 identical local function definitions and improves consistency
across all rendering modules.
Move ShadowPush from 4 separate rendering modules (character_renderer,
m2_renderer, terrain_renderer, wmo_renderer) into shared vk_frame_data.hpp
header. This eliminates 4 identical local struct definitions and ensures
consistency across all shadow rendering passes. Add vk_frame_data.hpp include
to character_renderer.cpp.
Consolidated duplicate attachment point resolution code used by both
attachWeapon() and getAttachmentTransform(). New findAttachmentBone()
helper encapsulates the complete lookup chain: attachment by ID, fallback
scan, key-bone fallback, and validation. Eliminates ~55 lines of duplicate
code while improving maintainability and consistency.
Consolidated identical key-bone lookup logic that appeared at lines 3076
and 3099. Both performed the same search for weapon attachment points
(ID 1/2 for right/left hand). Removed duplication while preserving
behavior and improving code clarity with better comments.
Add fallback logic to use bone 0 for head attachment point (ID 11) when models
don't have it explicitly defined. This improves helmet rendering compatibility
on humanoid NPC models that lack explicit attachment 11 definitions. Re-enable
helmet attachments now that the fallback logic is in place.
Allow R key to reset camera position/rotation when chat input is not active.
Previously disabled due to conflict with chat reply command. Now uses the same
safety check as movement keys (ImGui::GetIO().WantTextInput).
Implements edge-triggered reset on R key press, matching X key (sit) pattern.
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.
- character_renderer: playAnimation now prefers the primary variation
(variationIndex==0) when multiple sequences share the same animation ID;
this fixes hitching on human female run where a variation sequence was
selected first before the base cycle
- character_renderer: move the compositeWithRegions size-mismatch warning
inside the else branch so it only fires when sizes genuinely don't match,
not for every successful 1:1 or scaled blit
- terrain_renderer: add FREE_DESCRIPTOR_SET_BIT flag and vkFreeDescriptorSets
in destroyChunkGPU so material descriptor sets are returned to the pool;
prevents GPU device lost from pool exhaustion near populated areas
- game_screen: fix projectToMinimap to use the exact inverse of the minimap
shader transform so quest objective markers appear at the correct position
and orientation regardless of camera bearing
- inventory_screen: fix item comparison tooltip to not compare equipped items
against themselves (character screen); add item level diff line; show (=)
indicator when stats are equal rather than bare value which looked identical
to the item's own tooltip
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.
- Animation stutter: skip playAnimation(Run) for the local player in the
server movement callback — the player renderer state machine already manages
it; resetting animTime on every movement packet caused visible stutter
- Resolution crash: reorder swapchain recreation so old swapchain is only
destroyed after confirming the new build succeeded; add null-swapchain
guard in beginFrame to survive the retry window
- Memory cap: reduce cache budget from 80% uncapped to 50% hard-capped at
16 GB to prevent excessive RAM use on high-memory systems
- Spell tooltip: suppress "Drag to action bar / Double-click to cast" hints
when the tooltip is shown from the action bar (showUsageHints=false)
- M2 collision: add watermelon/melon/squash/gourd to foliage (no-collision);
exclude chair/bench/stool/seat/throne from smallSolidProp so invisible chair
bounding boxes no longer trap the player
On Windows arm64, wchar_t is unsigned so 'wc >= 0' is always true
and GCC/Clang emit -Wtype-limits. Drop the redundant lower bound
check — only the upper bound 'wc <= 0x7f' is needed.
- WotLK opcode 0x21E is aliased to both SMSG_SET_REST_START and
SMSG_QUEST_FORCE_REMOVE. In WotLK, treat as SET_REST_START (non-zero
= entering rest area, zero = leaving); Classic/TBC treat as quest removal.
- PLAYER_BYTES_2 rest state byte: change from `& 0x01` to `!= 0` to also
detect REST_TYPE_IN_CITY (value 2), not just REST_TYPE_IN_TAVERN (1).
- Minimap arrow: server orientation (π/2=North) needed conversion to
minimap arrow space (0=North). Subtract π/2 in both render paths so
arrow points North when player faces North.
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
- renderer: construct QuestMarkerRenderer via make_unique (was never
instantiated, causing getQuestMarkerRenderer() to always return null
and all quest-marker updates to be silently skipped)
- m2_renderer: add "levelup" to effectByName so LevelUp.m2 is treated
as a spell effect (additive blend, no collision, particle-dominated)
- renderer: auto-cancel non-looping emote animations when they reach
end-of-sequence, transitioning player back to IDLE state