If the capability probe ran before the model was fully loaded, all melee
animation IDs would be 0 and auto-attack swings would silently fall back
to STAND (no visible animation). Now re-probes when a melee swing fires
but hasMelee is false.
Added WARNING-level logging to triggerMeleeSwing and CombatFSM to
diagnose the night elf stationary combat animation issue.
Emote animations: fix DBC chain for /laugh, /flirt, /sleep, /fart, /stink.
Previously all emotes with AnimID=0 used emoteRef as animId (wrong DBC
record IDs). Now resolves through Emotes.dbc properly, with per-emote
overrides for emotes whose DBC chain yields 0. Adds Emotes.dbc load
failure warning and diagnostic logging.
WMO culling: skip portal culling when camera is outside all groups (fixes
vanishing Stormwind ground tiles). Also handle indoor/outdoor AABB overlap
by showing all groups when position is in both indoor and outdoor AABBs.
Transport: clear ONTRANSPORT flag and transport state when transport not
found, preventing stale transport data from teleporting player to map
origin. Add area trigger safety net near (0,0,0) on Eastern Kingdoms.
MAX_CULL_INSTANCES was 16384 but game object instances were allocated
at indices 20000+, beyond the cull buffer. Increased to 65536.
Also compute fallback boundRadius from vertex extents when M2 header
reports 0, and seed bones for global-sequence-only animated models.
M2 GPU instancing
- M2InstanceGPU SSBO (96 B/entry, double-buffered, 16384 max)
- Group opaque instances by (modelId, LOD); single vkCmdDrawIndexed per group
- boneBase field indexes into mega bone SSBO via gl_InstanceIndex
Indirect terrain drawing
- 24 MB mega index buffer (6M uint32) + 64 MB mega vertex buffer
- CPU builds VkDrawIndexedIndirectCommand per visible chunk
- Single VB/IB bind per frame; shadow pass reuses mega buffers
- Replaced vkCmdDrawIndexedIndirect with direct vkCmdDrawIndexed to fix
host-mapped buffer race condition that caused terrain flickering
GPU frustum culling (compute shader)
- m2_cull.comp.glsl: 64-thread workgroups, sphere-vs-6-planes + distance cull
- CullInstanceGPU SSBO input, uint visibility[] output, double-buffered
- dispatchCullCompute() runs before main pass via render graph node
Consolidated bone matrix SSBOs
- 16 MB double-buffered mega bone SSBO (2048 instances × 128 bones)
- Eliminated per-instance descriptor sets; one megaBoneSet_ per frame
- prepareRender() packs bone matrices consecutively into current frame slot
Render graph / frame graph
- RenderGraph: RGResource handles, RGPass nodes, Kahn topological sort
- Automatic VkImageMemoryBarrier/VkBufferMemoryBarrier between passes
- Passes: minimap_composite, worldmap_composite, preview_composite,
shadow_pass, reflection_pass, compute_cull
- beginFrame() uses buildFrameGraph() + renderGraph_->execute(cmd)
Pipeline derivatives
- PipelineBuilder::setFlags/setBasePipeline for VK_PIPELINE_CREATE_DERIVATIVE_BIT
- M2 opaque = base; alphaTest/alpha/additive are derivatives
- Applied to terrain (wireframe) and WMO (alpha-test) renderers
Rendering bug fixes:
- fix(shadow): compute lightSpaceMatrix before updatePerFrameUBO to eliminate
one-frame lag that caused shadow trails and flicker on moving objects
- fix(shadow): scale depth bias with shadowDistance_ instead of hardcoded 0.8f
to prevent acne at close range and gaps at far range
- fix(visibility): WMO group distance threshold 500u → 1200u to match terrain
view distance; buildings were disappearing on the horizon
- fix(precision): camera near plane 0.05 → 0.5 (ratio 600K:1 → 60K:1),
eliminating Z-fighting and improving frustum plane extraction stability
- fix(streaming): terrain load radius 4 → 6 tiles (~2133u → ~3200u) to exceed
M2 render distance (2800u) and eliminate pop-in when camera turns;
unload radius 7 → 9; spawn radius 3 → 4
- fix(visibility): ground-detail M2 distance multiplier 0.75 → 0.9 to reduce
early pop of grass and debris
- Enable alpha-to-coverage on alphaTestPipeline for smooth hair edges
when MSAA is active (both init and recreatePipelines paths)
- Shader uses fwidth()-based alpha rescaling for clean coverage
- Skip group 17/18 geosets (DK/NE eye glow) when no geoset filter is
set — prevents blue eye glow on all NPCs
- Add missing <libgen.h> include for dirname() on Linux
- alphaTestPipeline_ uses blendDisabled() so surviving pixels are fully
opaque (was blendAlpha, causing hair to blend with background)
- Remove alphaCutout from alphaTest condition — opaque materials like
capes no longer alpha-test just because their texture has an alpha
channel
- Two-pass batch rendering: opaque (blendMode 0) draws first to
establish depth, then alpha-key/blend draws on top
Bone SSBO buffers were allocated for MAX_BONES (240) entries but only
the first numBones were written. Uninitialized GPU memory in the
remaining slots caused vertex spikes when any bone index exceeded the
model's actual bone count.
Also send MSG_MOVE_STOP before spell casts so the server doesn't reject
cast-time spells (e.g. hearthstone) with "can't do that while moving".
Global sequence bones (hair, cape, physics) need time values spanning
their full duration (up to ~968733ms), but animationTime wraps at the
current animation's sequence duration (~2000ms for walk). This caused
vertex spikes projecting from fingers/neck/ponytail as bones got stuck
in the first ~2s of their loop. Add a separate globalSequenceTime
accumulator that is not wrapped at the animation duration.
Hair, cape, and other physics bones use global sequences (continuously
looping timers independent of the character's current animation). The
character renderer was ignoring globalSequence entirely, causing these
bones to fall back to identity transforms and produce deformed/spiked
hair geometry. Added resolveTrackTime() to wrap global sequence time
correctly, matching the M2 renderer's existing behavior.
M2 vertex bone indices are indices into boneLookupTable, not direct bone
array indices. Without remapping, vertices weighted to higher bone
indices (cloak, cape, hair) get the wrong bone transform, causing
vertices to project wildly outward from the character.
Wait on in-flight upload batch fences at the start of each frame and
insert a memory barrier (transfer→fragment shader) so the graphics
queue sees completed layout transitions from the transfer queue.
Fixes VK_IMAGE_LAYOUT_UNDEFINED validation errors for freshly loaded
textures.
Revert static JSON layout changes (15-22 back to 14-21) since WotLK
loads the Classic 23-field DBC. Add getItemDisplayInfoTextureFields()
helper that detects field count at runtime and adjusts the texture
base index accordingly (14 for 23-field, 15 for 25-field).
When saved settings loaded MSAA before FSR2 on startup, the pending MSAA
change (e.g. 8x) was queued before FSR2 was enabled. Since FSR2 checked
the current MSAA (still 1x), it didn't override the pending change.
On the next frame, applyMsaaChange created a 4-attachment MSAA render pass,
then FSR2 lazily created a 2-attachment framebuffer against it — SIGSEGV.
Add guards in both applyMsaaChange (force 1x if FSR2 is blocking) and
setFSR2Enabled (override any pending MSAA >1x when enabling FSR2).
AMD RADV validation flagged missing shaderStorageImageWriteWithoutFormat,
shaderInt16, shaderFloat16, and deviceCoherentMemory. The first two are
now required device features; shaderFloat16 is optionally enabled via
Vulkan 1.2 feature query; AMD device coherent memory extension and
feature are enabled when available to prevent VMA memory type errors.
executePostProcessing() only set inlineMode=true in the FSR2 path.
The FXAA and FSR1 paths both start INLINE render passes but returned
false, causing endFrame() to record ImGui into a secondary command
buffer and execute it inside the INLINE pass — validation errors and
UI disappearing on AMD RADV when FSR1+MSAA are both enabled.
Instance was created with Vulkan 1.1 but depthResolveSupported_ was gated
on the physical device's API version (1.2+ on RADV). This caused
vkCreateRenderPass2 (core 1.2) to dispatch through a null function pointer
when MSAA was enabled. Now requests 1.2 instance with 1.1 minimum fallback
and gates depth resolve on the actual instance API version. Also removes
all diagnostic crash-phase instrumentation from the previous investigation.
AMD crash is caused by msaaChangePending_ flipping true from saved settings.
applyMsaaChange() then crashes with faultAddr=(nil). Add LOG_WARNING markers
between pipeline recreation groups to identify the failing call.
Add per-frame LOG_WARNING with this/vkCtx/camera/postProcess pointers and
msaaChangePending state. If the log prints before crash, the pointer values
tell us what's corrupt. If it doesn't print, crash is in the log itself
(meaning this or vkCtx is corrupt).
Previous markers showed crash before bf:ubo. Add markers at bf:msaa,
bf:pp, bf:swap, bf:acquire, bf:jitter to isolate which early beginFrame
call does the NULL deref.
AMD RADV crash is renderPhase=beginFrame with faultAddr=(nil) — a NULL
pointer dereference somewhere in the pre-pass chain. Add granular markers
(bf:ubo, bf:minimap, bf:worldmap, bf:preview, bf:shadow, bf:reflection,
bf:renderpass) to pinpoint the exact call.
Add signal-safe render-phase markers throughout GameScreen::render() and
Application::render() so the crash handler can report which render call was
active when a SIGSEGV occurs. The AMD RADV crash backtrace only shows 2
frames due to missing frame pointers, making it impossible to identify the
actual crash site.
Changes:
- Add volatile g_crashRenderPhase marker updated before each major render call
- Upgrade Linux signal handler to sigaction with SA_SIGINFO for faulting address
- Set ImGui CheckVkResultFn to log silent Vulkan errors in ImGui backend
- Enable -fno-omit-frame-pointer in all build configs (not just Debug/RelWithDebInfo)
destroyInstanceBones loops over both frame slots (i=0,1), deferring
both boneSet[0] and boneSet[1] to the current frame's fence. When
currentFrame=0, boneSet[1] is freed after only slot 0's fence completes
while slot 1's command buffer may still be using it.
Switch both M2Renderer and CharacterRenderer bone destruction from
deferAfterFrameFence to deferAfterAllFrameFences to ensure all
in-flight frames have completed before freeing cross-slot resources.
deferAfterFrameFence only waits for one frame slot's fence, but shared
resources (material descriptor sets, vertex/index buffers) are bound by
both in-flight frames' command buffers. On AMD RADV this caused
vkFreeDescriptorSets errors and eventual SIGSEGV.
Add deferAfterAllFrameFences: queues to every frame slot with a shared
counter so cleanup runs exactly once, after the last slot is fenced.
Use it for WMO, terrain, water, and character model shared resources.
Per-frame bone sets keep using deferAfterFrameFence (already correct).
Also fix character renderer vertex format: R8G8B8A8_UINT -> _SINT to
match shader's ivec4 input (RADV validation rejects the mismatch).
CharacterRenderer::destroyModelGPU now defers vertex/index buffer
destruction when replacing models mid-stream, preventing use-after-free
on AMD RADV. FXAA descriptor sets are now per-frame to eliminate
write-read races between in-flight command buffers. Water reflection
descriptor update narrowed to current frame only.
CharacterRenderer::destroyInstanceBones had the same immediate-free bug
as M2Renderer — freeing bone descriptor sets and buffers while in-flight
command buffers still reference them. Applies the same deferred pattern
via deferAfterFrameFence for the removeInstance streaming path.
M2 destroyInstanceBones and WMO destroyGroupGPU freed descriptor sets
and buffers immediately during tile streaming, while in-flight command
buffers still referenced them — causing DEVICE_LOST on AMD RADV.
Now defers GPU resource destruction via deferAfterFrameFence in streaming
paths (removeInstance, removeInstances, unloadModel). Immediate
destruction preserved for shutdown/clear paths that vkDeviceWaitIdle
first.
Also: vkDeviceWaitIdle before WMO backfillNormalMaps descriptor rebinds,
and fillModeNonSolid added to required device features for wireframe
pipelines on AMD.
The pool was exhausted by cached spell/item/talent icon textures,
causing vkAllocateDescriptorSets to fail inside ImGui_ImplVulkan_AddTexture.
The NVIDIA driver crashed on the subsequent invalid descriptor write.
Also add a null-check on the returned descriptor set so pool exhaustion
gracefully returns VK_NULL_HANDLE instead of crashing.
During shutdown, VkContext::runDeferredCleanup() was executing lambdas
that called vkFreeDescriptorSets on descriptor pools already destroyed
by Renderer::shutdown(). This corrupted the validation layer's internal
state, causing a SIGSEGV during process exit on AMD RADV.
Clear the deferred queues without executing them — vkDestroyDevice
reclaims all device-child resources anyway. Also guard against the
double shutdown() call (explicit + destructor).
Avoid semaphore reuse while the presentation engine still holds a
reference by switching from per-frame-slot to per-swapchain-image
semaphores with a rotating free semaphore for acquire.
Replace the R8G8B8A8_UNORM dummy white texture in CharacterPreview
with a proper D16_UNORM depth texture cleared to 1.0, matching the
sampler2DShadow expectation in shaders. AMD RADV enforces strict
format/sampler type compatibility.
Three bugs found via AMD RADV crash log:
1. Water reflection render pass used BOTTOM_OF_PIPE as srcStageMask but
pipelines were created against the main pass (EARLY_FRAGMENT_TESTS |
COLOR_ATTACHMENT_OUTPUT). AMD enforces strict render pass compatibility
→ SIGSEGV when scene renders into reflection texture.
2. samplerAnisotropy was never enabled during device creation despite being
used in sampler creation — now requested via PhysicalDeviceSelector.
3. Shadow texture descriptor pool was reset each frame while prior frame's
command buffers might still reference it. Split into per-frame-slot pools
so each reset is fence-guarded.
AMD RDNA4 (9070XT) crashes with SIGSEGV when MSAA is enabled because the
driver optimizes TRANSIENT images for tile-only storage. Without lazily
allocated memory backing, the MSAA resolve reads unbacked memory. Now we
only set TRANSIENT+LAZILY_ALLOCATED when the device actually exposes that
memory type.
M2 particle/ribbon/batch, terrain layer, and WMO material texture
resolution paths were silently falling back to white textures when
indices were out of range — making missing texture issues hard to
diagnose. Add LOG_WARNING at each silent failure point with model
name, index details, and array sizes.
Remove all OpenGL/GLEW code and dependencies. The Vulkan renderer has
been the sole active backend for months; these files were dead code.
Deleted (8 files, 641 lines):
- rendering/mesh.cpp+hpp: OpenGL VAO/VBO/EBO wrapper (never instantiated)
- rendering/shader.cpp+hpp: OpenGL GLSL compiler (replaced by VkShaderModule)
- rendering/scene.cpp+hpp: Scene graph holding Mesh objects (created but
never populated — all rendering uses Vulkan sub-renderers directly)
- rendering/video_player.cpp+hpp: FFmpeg+GL texture uploader (never
included by any other file — login video feature can be re-implemented
with VkTexture when needed)
Cleaned up:
- renderer.hpp: remove Scene forward-decl, getScene() accessor, scene member
- renderer.cpp: remove scene.hpp/shader.hpp includes, Scene create/destroy
- application.cpp: remove stale "GL/glew.h removed" comment
- CMakeLists.txt: remove find_package(OpenGL/GLEW), source/header entries,
and target_link_libraries for OpenGL::GL and GLEW::GLEW
- PKGBUILD: remove glew dependency
- BUILD_INSTRUCTIONS.md: remove glew from all platform install commands
- Explain M2 version 264 threshold (WotLK stores submesh/bone data
in external .skin files; Classic/TBC embed it in the M2)
- Explain M2 texture types 1 and 6 (skin and hair/scalp; empty
filenames resolved via CharSections.dbc at runtime)
- Explain 0x20 anim flag (embedded data; when clear, keyframes live
in external {Model}{SeqID}-{Var}.anim files)
- Explain geoset ID encoding (group × 100 + variant from
ItemDisplayInfo.dbc; e.g. 801 = sleeves variant 1)
- inventory_screen: extract renderClassRestriction() and
renderRaceRestriction() from two identical 40-line blocks in quest
info and item info tooltips. Both used identical bitmask logic,
strncat formatting, and player-class/race validation (-49 lines net)
- world_map: add why-comment on AreaTable.dbc fallback field indices —
explains that incorrect indices silently return wrong data and why
the WotLK stock layout (ID=0, Parent=2, ExploreFlag=3) is chosen
as the safest default
- Name kHalfMinutesPerDay (2880) replacing 8 bare literals across
time conversion, modulo clamping, and midnight wrap arithmetic.
Add why-comment: Light.dbc stores time-of-day as half-minutes
(24h × 60m × 2 = 2880 ticks per day cycle)
- Replace hardcoded 3.14159f with glm::two_pi<float>() in sun
direction angle calculations (2 occurrences)
- terrain_manager: extract kRand16Max (65535.0f) from 8 duplicated
random normalization expressions — 16-bit mask to [0..1] float
- terrain_manager: add static_assert verifying packed alpha unpacks
to full alpha map size (ALPHA_MAP_PACKED * 2 == ALPHA_MAP_SIZE)
- camera_controller: name kCameraClipEpsilon (0.1f) with why-comment
preventing character model clipping at near-minimum distance
- vk_pipeline: extract kColorWriteAll constant from 4 duplicated RGBA
bitmask expressions across blend mode functions, with why-comment
- frustum: name kMinNormalLenSq epsilon (1e-8) with why-comment —
prevents division by zero on degenerate planes
- dbc_loader: add why-comment on DBC field width validation — all
fields are fixed 4-byte uint32 per format spec
- pin_auth: replace 0x30 hex literal with '0' char constant, add
why-comment on ASCII encoding for server HMAC compatibility
- 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)
- weather: remove duplicate setZoneWeather(15) for Dustwallow Marsh —
second call silently overwrote the first with different parameters
- weather: replace duplicate static RNG in getRandomPosition() with
shared weatherRng() to avoid redundant generator state
- starfield: extract day/night cycle thresholds into named constants
(kDuskStart/kNightStart/kDawnStart/kDawnEnd/kFadeDuration)
- skybox: replace while-loop time wrapping with std::fmod — avoids
O(n) iterations on large time jumps