- Windows: SetThreadAffinityMask to pin main thread to core 0 and
exclude workers from core 0
- macOS: thread_policy_set with THREAD_AFFINITY_POLICY tags to hint
scheduler separation (tag 1 for main, tag 2 for workers)
Water deduplication: merge per-chunk water surfaces into per-tile surfaces
to reduce Vulkan descriptor set usage from ~8900 to ~100-200. Uses hybrid
approach — groups with ≤4 chunks stay per-chunk (preserving shore detail),
larger groups merge into 128×128 tile-wide surfaces.
Re-add incremental tile finalization state machine (reverted in 9b90ab0)
to spread GPU uploads across frames and prevent city stuttering.
Pin main thread to CPU core 0 and exclude worker threads from core 0
to reduce scheduling jitter on the render/game loop.
The incremental advanceFinalization state machine broke water rendering
in ways that couldn't be resolved. Reverted to the original monolithic
finalizeTile approach. The other performance optimizations (bone SSBO
pre-allocation, WMO distance culling, M2 adaptive distance tiers)
are kept.
Phase-splitting across frames caused water surfaces to not render
correctly. Changed processReadyTiles to run all phases for each tile
before moving to the next, with time budget checked between tiles.
Replace monolithic finalizeTile() with a phased state machine that spreads
GPU upload work across multiple frames (TERRAIN→M2→WMO→WATER→AMBIENT→DONE).
Each advanceFinalization() call does one bounded unit of work within the
per-frame time budget, eliminating 50-300ms frame hitches when entering cities.
Additional performance improvements:
- Pre-allocate bone SSBOs at M2 instance creation instead of lazily during
first render frame, preventing hitches when many skinned characters appear
- Enable WMO distance culling (800 units) with active-group exemption so
the player's current floor/neighbors are never culled
- Add 4-tier adaptive M2 render distance (250/400/600/1000 based on count)
- Remove dead PendingM2Upload queue code superseded by incremental system
Fix tile re-enqueueing bug: keep tiles in pendingTiles until committed to
loadedTiles (not when moved to finalizingTiles_) so streamTiles() doesn't
re-enqueue tiles mid-finalization. Also handle unloadTile() for tiles in
the finalizingTiles_ deque to prevent orphaned water/M2/WMO resources.
- Fix StormLib package name: libstormlib-dev → libstorm-dev (correct
Ubuntu package name) across all CI workflows and extract_assets.sh
- Build StormLib from source on Windows CI (no MSYS2 package exists),
ensuring asset_extract.exe is included in release archives
- Update extract_assets.sh/.ps1 to prefer pre-built asset_extract
binary next to the script (release archives) before trying build dir
- Move ADTTerrain allocations from stack to heap in prepareTile() to
fix stack overflow on macOS (worker threads default to 512 KB stack,
two ADTTerrain structs ≈ 560 KB exceeded that)
- reduce per-tile ground clutter generation pressure and enforce tighter caps to avoid spikes
- remove expensive detail dedupe scans from the hot render path
- add progressive/lazy clutter updates around player movement to smooth frame pacing
- lower noisy runtime INFO logging to DEBUG/throttled paths
- keep terrain/game screen updates responsive while preserving existing behavior
Vanilla (v256) and TBC (v263) M2 files embed skin data directly, parsed
during M2Loader::load(). The code unconditionally loaded external .skin
files afterwards, which resolved to WotLK-format .skin files (48-byte
submeshes) from the base manifest — overwriting the correctly parsed
embedded skin (32-byte submeshes) and causing mesh corruption on all
character models. Guard all 13 loadSkin() call sites with version >= 264
so external .skin files are only loaded for WotLK M2s that need them.
Mount Animation System:
- Property-based jump animation discovery using sequence metadata
- Chain linkage scoring (nextAnimation/aliasNext) for accurate detection
- Correct loop detection: flags & 0x01 == 0 means looping
- Avoids brake/stop animations via blendTime penalties
- Works on any mount model without hardcoded animation IDs
Mount Physics:
- Physics-based jump height: vz = sqrt(2 * g * h)
- Configurable MOUNT_JUMP_HEIGHT constant (1.0m default)
- Procedural lean into turns for ground mounts
- Smooth roll based on turn rate (±14° max, 6x/sec blend)
Audio Improvements:
- State-machine driven mount sounds (jump, land, rear-up)
- Semantic sound methods (no animation ID dependencies)
- Debug logging for missing sound files
Bug Fixes:
- Fixed mount animation sequencing (JumpStart → JumpLoop → JumpEnd)
- Fixed animation loop flag interpretation (0x20 vs 0x21)
- Rider bone attachment working correctly during all mount actions
Added deduplication for WMO instances based on uniqueId, matching the
existing M2 doodad deduplication logic. This prevents creating multiple
instances of the same WMO when it's referenced from multiple ADT tiles.
Before: STORMWIND.WMO (uniqueId=10047) was being rendered 16 times
(one instance per ADT tile that references it)
After: Only 1 instance is created and shared across all tiles
Changes:
- Added placedWmoIds set to TerrainManager (like placedDoodadIds)
- Check uniqueId before creating WMO instance
- Skip duplicate WMO placements across tile boundaries
- Log dedup statistics: 'X instances, Y dedup skipped'
This should fix the floating cathedral visual issue if it was caused by
rendering artifacts from 16x overdraw, and will massively improve
performance in Stormwind.
- Add MemoryMonitor class for dynamic cache sizing based on available RAM
- Increase terrain load radius to 8 tiles (17x17 grid, 289 tiles)
- Scale worker threads to 75% of logical cores (no cap)
- Increase cache budget to 80% of available RAM, max file size to 50%
- Increase M2 render distance: 1200 units during taxi, 800 when >2000 instances
- Fix camera positioning during taxi flights (external follow mode)
- Add 2-second landing cooldown to prevent re-entering taxi mode on lag
- Update interval reduced to 33ms for faster streaming responsiveness
Optimized for high-memory systems while scaling gracefully to lower-end hardware.
Cache and render distances now fully utilize available VRAM on minimum spec GPUs.
Modern GPUs have 8-16GB VRAM - leverage this to cache all M2 models permanently.
Changes:
- Disabled cleanupUnusedModels() call when tiles unload
- Models now stay in VRAM after initial load, even when tiles unload
- Increased taxi mounting delay from 3s to 5s for more precache time
- Added logging: M2 model count, instance count, and GPU upload duration
- Added debug logging when M2 models are uploaded per tile
This fixes the "building pops up then pause" issue - models were being:
1. Loaded when tile loads
2. Unloaded when tile unloads (behind taxi)
3. Re-loaded when flying through again (causing hitch)
Now models persist in VRAM permanently (few hundred MB for typical session).
First pass loads to VRAM, subsequent passes are instant.
Major improvements:
- Load TaxiPathNode.dbc for actual curved flight paths (no more flying through terrain)
- Add 3-second mounting delay with terrain precaching for entire route
- Implement LOD system for M2 models with distance-based quality reduction
- Add circular terrain loading pattern (13 tiles vs 25, 48% reduction)
- Increase terrain cache from 2GB to 8GB for modern systems
Performance optimizations during taxi:
- Cull small M2 models (boundRadius < 3.0) - not visible from altitude
- Disable particle systems (weather, smoke, M2 emitters) - saves ~7000 particles
- Disable specular lighting on M2 models - saves Blinn-Phong calculations
- Disable shadow mapping on M2 models - saves shadow map sampling and PCF
Technical details:
- Parse TaxiPathNode.dbc spline waypoints for curved paths around terrain
- Build full path from node pairs using TaxiPathEdge lookup
- Precache callback triggers during mounting delay for smooth takeoff
- Circular tile loading uses Euclidean distance check (dx²+dy² <= r²)
- LOD fallback to base mesh when higher LODs unavailable
Result: Buttery smooth taxi flights with no terrain clipping or performance hitches
Load BLP texture data during prepareTile() and upload to GL cache in
finalizeTile(), eliminating file I/O stalls on the main thread. Reduce
ready tiles per frame to 1. Fix camera sweep to snap Z to ramp surfaces.
Change hearthstone action bar slot from spell to item.
Guard pendingTiles.erase() with queueMutex in processReadyTiles and
unloadTile to prevent data race with worker threads. Add defensive null
checks in M2/WMO render and animation paths. Move cleanupUnusedModels
out of per-tile unload loop to run once after all tiles are removed.
Enrich online inventory from local DB when server data is incomplete, add
resolveOnlineItemGuid fallback for sell/equip/use, use async enqueueTile for
initial terrain load, improve walk/run animation fallbacks, clear target on
loot close, and broaden equipability detection to include armor/subclass.
Use getRemainingTileCount (pending + readyQueue) and processAllReadyTiles
to prevent loading screen from exiting before tiles are finalized. Auto-select
realm and character when only one is available.
Add world entry callback that triggers terrain loading when receiving
SMSG_LOGIN_VERIFY_WORLD. Fix coordinate conversion by applying
serverToCanonical() to properly swap X/Y axes from server format.
Teleporter panel (T key) lets the player teleport between Goldshire,
Stormwind Gate, Ironforge, and Westfall in single-player mode. Adds
serverToCanonical/canonicalToServer conversion at the network packet
boundary so positions are compatible with TrinityCore/MaNGOS/AzerothCore
emulator servers.
Centralizes all coordinate conversions in core/coordinates.hpp with
proper canonical WoW coords (+X=North, +Y=West, +Z=Up). Fixes critical
tile calculation bug that was loading wrong surrounding tiles during
terrain streaming, and fixes position sync sending ADT-raw format
instead of canonical coordinates to the server.
- Loading screen stays visible until all terrain tiles finish streaming;
character spawns only after terrain is loaded and Z-snapped to ground
- Reduce tree trunk collision bounds (5% of canopy, capped at 5.0) and
make all small/medium trees, bushes, lily pads, and foliage walkthrough
- Add jump input buffering (150ms) and coyote time (100ms) for responsive jumps
- Fix fence orientation by adding +180° heading rotation
- Increase terrain load radius from 1 to 2 (5x5 tile grid)
- Add hearthstone callback for single-player camera reset
- Add spellbook screen (P key) with Spell.dbc name lookup and action bar assignment
- Default Attack and Hearthstone spells available in single player
- Fix WMO floor clipping (gryphon roost) by tightening ceiling rejection threshold
- Darken ocean water, increase wave motion and opacity
- Add M2 model distance fade-in to prevent pop-in
- Reposition chat window, add slash/enter key focus
- Remove debug key commands (keep only F1 perf HUD, N minimap)
- Performance: return chat history by const ref, use deque for O(1) pop_front
- Add loading screen system with stb_image for JPEG loading
- Two loading screen images (orc and dwarf) randomly selected
- Display loading screen while terrain data loads
- Cache WMO inverse matrices to reduce per-frame computation
- Stub WMO liquid rendering (needs coordinate system fix)
- Update spawn point to Stormwind Trade District
- Spawn position changed to (-9080, -100, 100) which is on actual terrain
- The terrain mesh uses WoW coordinates from ADT files directly
- Camera/spawn position must use same coordinate system as terrain
- Cleaned up getHeightAt comments to clarify coordinate system
- Removed debug logging from WMO floor detection