- Wire WOB buildings into WMO render pipeline (loads→converts→renders)
- Implement JSON DBC loading in DBCFile::loadJSON() with nlohmann/json
- Wire JSON DBC override into AssetManager (custom_zones/output scan)
- Add WMO→WOB conversion with full geometry (fromWMO)
- Replace placeholder WOB export with real WMO→WOB conversion in editor
- Add --convert-wmo CLI flag for batch WMO→WOB conversion
- Store discovered custom zones on Renderer with getCustomZones() accessor
- Add isCustomZone_ member to TerrainManager
All 6 Blizzard format replacements now fully load in the client:
ADT→WOT/WHM, WDT→zone.json, BLP→PNG, DBC→JSON, M2→WOM, WMO→WOB
- Renderer::initializeRenderers() now scans custom_zones/ and output/
for zone.json manifests on first initialization
- Logs all discovered custom zones with name, directory, and
NPC/quest availability indicators
- One-time scan (cached after first run)
- Foundation for custom zone selection menu in client UI
Client open format support:
- FULL: WOT/WHM terrain, PNG textures, WOM models (load + render)
- DETECT: zone.json (scanned at startup), WOB buildings, JSON DBC
- Mark swapchain dirty in Application's SDL resize handler (was only done
in Window::pollEvents which is never called)
- Skip swapchain recreation when window is minimized (0×0 extent violates
Vulkan spec and crashes vmaCreateImage)
- Guard aspect ratio division by zero when height is 0
Signed-off-by: Pavel Okhlopkov <pavel.okhlopkov@flant.com>
Add complete spell visual pipeline resolving the DBC chain
(Spell → SpellVisual → SpellVisualKit → SpellVisualEffectName → M2)
with precast/cast/impact phases, bone-attached positioning, and
automatic dual-hand mirroring.
Ribbon rendering fixes:
- Parse visibility track as uint8 (was read as float, suppressing
all ribbon edges due to ~1.4e-45 failing the >0.5 check)
- Filter garbage emitters with bone=UINT_MAX unconditionally
- Guard against NaN spine positions from corrupt bone data
- Resolve ribbon textures via direct index, not textureLookup table
- Fall back to bone 0 when ribbon bone index is out of range
Particle rendering fixes:
- Reduce spell particle scale from 5x to 1.5x (was oversized)
- Exempt spell effect instances from position-based deduplication
Spell handler integration:
- Trigger precast visuals on SMSG_SPELL_START with server castTimeMs
- Trigger cast/impact visuals on SMSG_SPELL_GO
- Cancel precast visuals on cast interrupt/failure/movement
M2 classifier expansion:
- Add AmbientEmitterType enum for sound system integration
- Add 20+ foliage tokens, 4 spell effect tokens, isSmallFoliage flag
- Add markModelAsSpellEffect() to override disableAnimation
DBC layouts:
- Add SpellVisualID field to Spell.dbc for all expansion configs
Signed-off-by: Pavel Okhlopkov <pavel.okhlopkov@flant.com>
HiZ occlusion culling built ~11 mip levels with per-level barriers
behind a blocking vkWaitForFences every frame — the main frame-rate
bottleneck. Disable the pyramid build and fall back to GPU frustum-
only culling which is nearly free behind the fence.
WMO interiors now receive full-strength directional shadows but clamp
minimum brightness at 0.45 with a 0.35 ambient floor, so interiors
get real shadow contrast without going too dark.
Implement GPU-driven Hierarchical-Z occlusion culling for M2 doodads
using a depth pyramid built from the previous frame's depth buffer.
The cull shader projects bounding spheres via prevViewProj (temporal
reprojection) and samples the HiZ pyramid to reject hidden objects
before the main render pass.
Key implementation details:
- Separate early compute submission (beginSingleTimeCommands + fence
wait) eliminates 2-frame visibility staleness
- Conservative safeguards prevent false culls: screen-edge guard,
full VP row-vector AABB projection (Cauchy-Schwarz), 50% sphere
inflation, depth bias, mip+1, min screen size threshold, camera
motion dampening (auto-disable on fast rotations), and per-instance
previouslyVisible flag tracking
- Graceful fallback to frustum-only culling if HiZ init fails
Fix dark WMO interiors by gating shadow map sampling on isInterior==0
in the WMO fragment shader. Interior groups (flag 0x2000) now rely
solely on pre-baked MOCV vertex-color lighting + MOHD ambient color.
Disable interiorDarken globally (was incorrectly darkening outdoor M2s
when camera was inside a WMO). Use isInsideInteriorWMO() instead of
isInsideWMO() for correct indoor detection.
New files:
- hiz_system.hpp/cpp: pyramid image management, compute pipeline,
descriptors, mip-chain build dispatch, resize handling
- hiz_build.comp.glsl: MAX-depth 2x2 reduction compute shader
- m2_cull_hiz.comp.glsl: frustum + HiZ occlusion cull compute shader
- test_indoor_shadows.cpp: 14 unit tests for shadow/interior contracts
Modified:
- CullUniformsGPU expanded 128->272 bytes (HiZ params, viewProj,
prevViewProj)
- Depth buffer images gain VK_IMAGE_USAGE_SAMPLED_BIT for HiZ reads
- wmo.frag.glsl: interior branch before unlit, shadow skip for 0x2000
- Render graph: hiz_build + compute_cull disabled (run in early compute)
- .gitignore: ignore compiled .spv binaries
- MEGA_BONE_MAX_INSTANCES: 2048 -> 4096
Signed-off-by: Pavel Okhlopkov <pavel.okhlopkov@flant.com>
Demote 44 more LOG_WARNING messages to LOG_DEBUG: warden module chunk
progress, entire shutdown/teardown sequence, transport manager connect,
inventory right-click slot, and warden handshake diagnostics. Keeps real
warnings (texture not found, slow handlers, stalls, integrity hash)
visible in the log.
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
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).
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)
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
- 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
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.
All 8 rand() calls for animation time offsets and variation timers in
m2_renderer.cpp used C rand() which defaults to seed 1 without srand(),
producing identical sequences every launch. Trees, torches, and grass
all swayed in sync. Replaced with std::mt19937 seeded from
random_device. Same fix for 4 mount idle fidget/sound timer sites in
renderer.cpp which mixed rand() with the mt19937 already present.
tavernTrackIndex was initialized to 0 but never modified, so the player
always heard TavernAlliance01.mp3. Added post-increment to rotate
through the 3 available tracks on each tavern entry.
- Hoist DBC field index lookups before loops in game_handler (7 DBC iteration loops)
- Cache getSkybox()/getPosition() calls instead of redundant per-frame queries
- Merge textureHasAlphaByPtr_ + textureColorKeyBlackByPtr_ into single map
- Add constexpr for DEG_TO_RAD, reciprocal constants, physics delta
- Add reserve() for WMO/M2 collision grid queries and portal BFS
- Frustum plane normalize: inversesqrt instead of length+divide
- M2 particle emission: inversesqrt for direction normalization
- Parse creature display IDs from query response
- UI: show spell names/IDs as fallback instead of "Unknown"
Squared distance optimizations across 30 files:
- Convert glm::length() comparisons to glm::dot() (no sqrt)
- Use glm::inversesqrt() for check-then-normalize patterns (1 rsqrt vs 2 sqrt)
- Defer sqrt to after early-out checks in collision/movement code
- Hottest paths: camera_controller (21), weather particles, WMO collision,
transport movement, creature interpolation, nameplate culling
Container and algorithm improvements:
- std::map<string> → std::unordered_map for asset/DBC/MPQ/warden caches
- std::mutex → std::shared_mutex for asset_manager and mpq_manager caches
- std::sort → std::partial_sort in lighting_manager (top-2 of N volumes)
- Double-lookup find()+operator[] → insert_or_assign in game_handler
- Add reserve() for per-frame vectors: weather, swim_effects, WMO/M2 collision
Threading and synchronization:
- Replace 1ms busy-wait polling with condition_variable in character_renderer
- Move timestamp capture before mutex in logger
- Use memory_order_acquire/release for normal map completion signaling
API additions:
- DBC getStringView()/getStringViewByOffset() for zero-copy string access
- Parse creature display IDs from SMSG_CREATURE_QUERY_SINGLE_RESPONSE
The findMemType/findMemoryType helper in auth_screen, loading_screen,
and vk_context returned 0 on failure — a valid memory type index.
Changed to return UINT32_MAX and log an error, so vkAllocateMemory
receives an invalid index and fails cleanly rather than silently
using the wrong memory type.
Add [[nodiscard]] to VkBuffer::uploadToGPU/createMapped and
VkContext::initialize/recreateSwapchain so callers that ignore
failure are flagged at compile time. Suppress with (void) cast at
3 call sites where failure is non-actionable (resize best-effort).
Wrap string-to-number conversions in try-catch where input comes from
external sources (realm address port, last_world.cfg, keybinding config,
ADT tile filenames) to prevent crashes on malformed data.
- Extract guidToUnitId(), getQuestTitle(), findQuestLogEntry() helpers
to replace 14 duplicated GUID-to-unitId patterns and 7 quest log
search patterns in game_handler.cpp
- Remove duplicate #include in renderer.cpp
- Remove commented-out model cleanup code in terrain_manager.cpp
- Replace C-style casts with static_cast in auth and transport code
When parallel recording is active, the scene pass uses
VK_SUBPASS_CONTENTS_SECONDARY_COMMAND_BUFFERS. Post-processing paths
(FSR/FXAA) end the scene pass and begin a new INLINE render pass for
the swapchain output. ImGui rendering must use the correct mode —
secondary buffers for SECONDARY passes, direct calls for INLINE.
Previously the check used a static condition based on enabled features
(!fsr && !fsr2 && !fxaa && parallel), which could mismatch if a
feature was enabled but initialization failed. Replace with
endFrameInlineMode_ flag that tracks the actual current render pass
mode at runtime, eliminating the validation error
VUID-vkCmdDrawIndexed-commandBuffer-recording that caused intermittent
NVIDIA driver crashes.
Validation layers revealed 9965 VkSamplers allocated against a device
limit of 4000 — every VkTexture created its own sampler even when
configurations were identical. This exhausted NVIDIA's sampler pool
and caused intermittent SIGSEGV in vkCmdBeginRenderPass.
Add a thread-safe sampler cache in VkContext that deduplicates samplers
by FNV-1a hash of all 14 VkSamplerCreateInfo fields. All texture,
render target, renderer, water, and loading screen sampler creation
now goes through getOrCreateSampler(). Textures set ownsSampler_=false
so shared samplers aren't double-freed.
Also auto-disable anisotropy in the cache when the physical device
doesn't support the samplerAnisotropy feature, fixing the validation
error VUID-VkSamplerCreateInfo-anisotropyEnable-01070.
The AMD FidelityFX FSR3 runtime corrupts Vulkan driver state when
context creation fails on NVIDIA GPUs, causing vkCmdBeginRenderPass
to SIGSEGV inside libnvidia-glcore. Gate FSR3 frame gen initialization
behind isAmdGpu() check — FSR2 upscaling still works on all GPUs.
Create a VkPipelineCache at device init, loaded from disk if available.
All 65 pipeline creation calls across 19 renderer files now use the
shared cache. On shutdown, the cache is serialized to disk so subsequent
launches skip redundant shader compilation.
Cache path: ~/.local/share/wowee/pipeline_cache.bin (Linux),
~/Library/Caches/wowee/ (macOS), %APPDATA%\wowee\ (Windows).
Stale/corrupt caches are handled gracefully (fallback to empty cache).
Add Weather::Type::STORM enum value and wire it from SMSG_WEATHER type 3.
Storm particles are faster (70 units/s vs rain's 50), wind-angled at 15+
units lateral velocity with gusty turbulence, darker blue-grey tint, and
shorter lifetime. Previously storms rendered identically to rain.
The spell visual failed-model cache was never cleared across world
changes, so models that failed to load during initial asset loading
(before MPQ/CASC data was fully indexed) would never retry. Now clears
spellVisualFailedModels_ in resetCombatVisualState() alongside the
active spell visual cleanup, giving failed models a fresh attempt on
each world entry.
Active spell visual M2 instances were never cleaned up when the player
teleported to a different map or re-entered the world. Orphaned effects
could linger visually from the previous combat session. Now properly
removes all active spell visual instances in resetCombatVisualState(),
which is called on every world entry.
Add indoor/outdoor state macro conditionals using the renderer's WMO
interior detection. Essential for mount macros that need to select
ground mounts indoors vs flying mounts outdoors. The Renderer now
caches the insideWmo state in playerIndoors_ and exposes it via
isPlayerIndoors(). Updated /macrohelp to list the new conditionals.
Spell visual M2 models that fail to load (missing file, empty model,
or GPU upload failure) were re-attempted on every subsequent spell cast,
causing repeated file I/O during combat. Now caches failed model IDs in
spellVisualFailedModels_ so they are skipped on subsequent attempts.
Spell visual effects previously used a fixed 3.5s duration for all
effects, causing some to linger too long and overlap during combat.
Now queries the M2 model's default animation duration via the new
getInstanceAnimDuration() method and clamps it to 0.5-5s. Effects
without animations fall back to a 2s default. This makes spell impacts
feel more responsive and reduces visual clutter.
Captures the Vulkan swapchain image to PNG via stb_image_write.
Screenshots saved to ~/.wowee/screenshots/ with timestamped filenames.
Cross-platform: BGRA→RGBA swizzle, localtime_r/localtime_s.
loadSpellVisualDbc() now builds two distinct maps:
spellVisualCastPath_ — visualId → M2 via SpellVisual.CastKit chain
spellVisualImpactPath_ — visualId → M2 via SpellVisual.ImpactKit chain
playSpellVisual() accepts useImpactKit=false (default, cast) / true (impact).
SMSG_PLAY_SPELL_IMPACT passes useImpactKit=true so impact effects (explosions,
debuff indicators) use the ImpactKit model instead of the CastKit model.
Added ImpactKit field to all four dbc_layouts.json files.
Parse SMSG_PLAY_SPELL_VISUAL (casterGuid + visualId) and spawn a
transient M2 spell effect at the caster's world position.
DBC chain: SpellVisual.dbc → SpellVisualKit.dbc → SpellVisualEffectName.dbc
Lookup priority: CastKit.SpecialEffect0, fallback to MissileModel.
Models are lazy-loaded and cached by path; instances auto-expire after 3.5s.
DBC layouts added to all four expansion layout files (Classic/TBC/WotLK/Turtle).
Apply a cold blue-grey fullscreen overlay when the player is in ghost
form, creating a desaturated, muted appearance that clearly signals the
death state. Uses the existing overlay pipeline infrastructure. Applied
in both parallel and non-parallel rendering paths, after underwater tint
but before brightness adjustment so UI elements remain unaffected.
Move 5-second brute-force HMAC-SHA1 code pattern searches to a
background thread via std::async. The main loop now detects PAGE_A/B
checks, launches the response builder async, and drains the result
in update() — encrypting and sending on the main thread to keep
wardenCrypto_ RC4 state thread-safe.
Also adds Turtle WoW PE binary support (isTurtle flag, dedicated exe
search, runtime patches), searchCodePattern with result caching,
writeLE32 public API, and Warden scan entry verification.