- 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
- Fix move constructor and move assignment: set other.ownsSampler_ to
false after transfer (was incorrectly set to true, leaving moved-from
object claiming ownership of a null sampler)
- Fix destroy(): reset ownsSampler_ to false after clearing sampler
handle (was set to true, inconsistent with null handle state)
- Extract finalizeSampler() from 3 duplicated cache-or-create blocks
in createSampler() overloads and createShadowSampler() (-24 lines)
- Add SPIR-V alignment why-comment in vk_shader.cpp
- Name portal spin wrap value as kTwoPi constant
- Name particle animTime wrap as kParticleWrapMs (3333ms) with
why-comment: covers longest known emission cycle (~3s torch/campfire)
while preventing float precision loss over hours of runtime
- Add FBlock interpolation documentation: explain what FBlocks are
(particle lifetime curves) and note that float/vec3 variants share
identical logic and must be updated together
- renderer: remove no-op assignment (mountAnims_.stand = 0 when already 0)
- renderer: add why-comments on blacksmith WMO ID 96048 (ambient forge
sounds) with TODO for other smithy buildings
- terrain_renderer: replace 1e30f sentinel with numeric_limits::max(),
name terrain view distance constant (1200 units ≈ 9 ADT tiles)
- social_handler: add missing LFG case 15, document case 0 nullptr
return (success = no error message), add enum name comments
- character_renderer: extract duplicated fallback texture creation
(white/transparent/flat-normal) into createFallbackTextures() — was
copy-pasted between initialize() and clear()
- wmo_renderer: replace magic 8192 with kMaxRetryTracked constant,
add why-comment explaining the fallback-retry set cap (Dalaran has
2000+ unique WMO groups)
- quest_handler: add why-comment on reqCount=0 fallback — escort/event
quests can report kill credit without objective counts in query response
- spell_handler.cpp: replace goto-done with do/while(false) for pet
spell packet parsing — bail on truncated data while always firing
events afterward
- water_renderer.cpp: replace goto-found_neighbor with immediately
invoked lambda to break out of nested neighbor search loops
The while(true) loop retried av_read_frame after seeking to the start
on error. A corrupt file where read fails but seek succeeds would loop
forever, blocking the main thread. Bounded to 500 attempts with a
warning log on exhaustion.
If generateNormalHeightMapCPU threw (e.g., bad_alloc), the pending
counter was never decremented, causing shutdown() to block forever
waiting for a count that would never reach zero. Added try-catch to
guarantee the decrement. Also strengthened the increment from relaxed
to acq_rel so shutdown()'s acquire load sees the count before the
thread body begins executing.
std::future::get() re-throws any exception from the async task. The 6
call sites in the render pipeline (terrain/WMO/M2 workers + animation
worker) and 2 floor-query sites in camera_controller were unguarded,
so a single bad_alloc in any worker would terminate the process with
no recovery. Now wrapped in try-catch with error logging.
8 rand() calls in weather.cpp used C rand() which defaults to seed 1.
Weather intensity rolls, cycle durations, and particle Y positions were
identical on every launch. Replaced with a file-local mt19937 seeded
from random_device, matching the RNG already present in getRandomPosition.