TL;DR: I was able to get the game to render full shadow and ligthing effects by altering a compiled shader shipped in the game client. Game went from crashing on world boot, to running 144+ FPS with full shadows/lighting/shaders and is indiscernible from running the game on windows with the same hardware.
A problem I haven’t tracked down yet is another discussed issue: FPS slowly degrades to ~30 over a session and stabilizes there. Spitball is a VRAM leak rather than system memory, since the GPU holds full clocks and 70–85 % utilization while it drops.
Disclaimer: I’m a software engineer with powerful tools at my disposal, not a game developer. Shader internals below come from disassembling the compiled SPIR-V; the HLSL source was never seen. Names given to loops and buffers are just descriptive. I’ve documented my workaround, but I don’t suspect the average person will be able to apply the workaround. Because of this, I’ve tried to be as technical as possible so the team at Blizzard can hopefully glean some useful information.
I came up with this work around over the weekend, and it’s possible a patch has already been cut. Figured it was worth posting just in case. Cheers.
Build: 1.60.1.69913 (isolation and fix); workaround still applied on 1.60.1.69977
Host: CachyOS, kernel 7.2.6, KDE Plasma Wayland, RTX 4080 SUPER, NVIDIA 615.71.09 open kernel modules, GE-Proton11-7 via Lutris/umu (DXVK 3.1, vkd3d-proton)
Result: GI 3, Secondary Lighting High, Optional GPU Features on, four days of play, zero GPU faults with the workaround. Same configuration without it: GPU lock within seconds of world load, every time.
In plain terms
- What happens: with Global Illumination on, or Secondary Lighting above Fair, the game asks the graphics card to run a small program (a shader) that, on Linux, never finishes on its first run. The graphics card stalls, the driver gives up after about four seconds, and the game freezes with audio still playing.
- Whose bug it is: High confidence its the game’s. The shader is shipped inside WoW’s data files. Nothing in the NVIDIA driver, Proton, or the Linux setup is broken; they all behave correctly and were each tested in isolation. Windows on NVIDIA happens to get away with it; AMD on Windows and Macs do not.
- Where the workaround lives: in neither the game files nor the driver. Proton includes a translation layer (vkd3d-proton) that converts the game’s DirectX 12 shaders into Vulkan shaders the Linux driver understands. That layer has a built-in option to swap any shader for a replacement file at load time. The workaround is a folder containing corrected copies of the shader, plus one environment variable that points the layer at that folder. The game install is untouched, the driver is untouched, and removing the variable removes the fix.
- What the correction does: the shader has a loop that reads its iteration count from memory that is not ready yet on the first frame. The corrected copy stops that loop after 128 passes instead of running forever. On normal frames the count never gets near 128, so the picture is unchanged.
- What it needs: the game set to DirectX 12, an NVIDIA card, and a few minutes with the command line. Steps are in the workaround section below. Every game patch can invalidate it, because the replacement is matched to the exact shader.
- The real fix is a one-line change on Blizzard’s side: initialize that value before the shader first runs, or put a bound on the loop.
Summary
giQuality >= 1orgraphicsLightMode >= 2locks the GPU 3–10 s after “Render Settings Changed” (cold world load or any in-world graphics change).- Cause: one compute shader, the GI probe update. Its two outer loops run to a count read from a constant buffer (
cb0[2] / cb0[3]). On the first dispatch after GI resources are created or recreated, that count is not valid. The loops have no other bound. Xid 109 is the driver timing out a context that never yields. - Fix that works: substitute the shader with a copy whose outer loops are capped at 128 iterations (
VKD3D_SHADER_OVERRIDE, D3D12 only). Capping only the inner tree-walk loops does not work. - Not the driver, kernel, firmware, Proton, DXVK, vkd3d, prefix, caches, NVAPI, Reflex, thermals, or any other setting. The same setting produces driver timeouts on AMD/Windows and hard freezes on macOS (threads below), so it is a client defect that NVIDIA-on-Windows tolerates.
Failure signature
gx.log: Render Settings Changed -> 3-10 s -> Failure in WaitForFence
Device Removed Reason: GPU Hung. Timeout when waiting for queue: Graphics (0x80070102)
dmesg: NVRM: Xid (PCI:0000:01:00): 109, pid=..., name=WowB.exe, channel 0x1f, errorString CTX SWITCH TIMEOUT
Audio continues, client stays alive, render thread blocks on a fence that never signals, GPU parks at 210 MHz, recovery never completes. The IndirectTextureArray.cpp:229 assert (“Expected size 128x128, got 1x1”) that sometimes follows is a consequence of device loss, not a cause. Identical across 20+ occurrences.
Settings matrix (D3D11, clean boot, no driver parameters)
| cvar | 0 | 1 | 2 | 3 |
|---|---|---|---|---|
graphicsLightMode |
OK | OK (Fair) | FAIL | — |
giQuality |
OK | FAIL | — | FAIL |
Textures, view distance, shadows, SSAO, particles, compute effects: all stable at maximum.
The shader
Identified by dumping every compiled shader (DXVK_SHADER_DUMP_PATH on D3D11, VKD3D_SHADER_DUMP_PATH on D3D12) and matching the shape reported by a GFXReconstruct capture on the NVIDIA forum (thread 1 below). Two 4x4x4 compute shaders match, one with a sky cube-map sample and one without.
| API / shader family | Hashes |
|---|---|
| D3D11, DXBC (DXVK names) | cs.32ff2d97c04db77fb1db038246404e95, cs.5384147526647f6e664b68e06f2cb030 |
| D3D12, DXIL, SM5 family | d376cd76654c2b2d, afcdd7438ea5e893 |
| D3D12, DXIL, SM6 family (Optional GPU Features on) | 2d79ed5755446f2f, bfbc72e5391a2095 |
| 16x1x1 siblings sharing the tree-walk code | 47454c97a7d4e365, ae144d0f2294d8b1 (SM5); 1f15da409bded3b7, f38fe78c975efc19 (SM6) |
Resources: constant buffer cb0; four structured buffers (strides 32, 256, 32, 32 bytes); an R32ui buffer UAV; a RWTexture2D with six writes; two groupshared uint[256] arrays. Loop structure from the SPIR-V control flow, identical in both families:
L1: for (i = 0; i < udiv(cb0[2], cb0[3]); i++) // count from the constant buffer
L2: for (j = 0; j < udiv(cb0[2], cb0[3]); j++)
L3: while (node != 0x1FFFFFFF && stackPtr > 0) // tree walk, explicit groupshared stack
L4: while (node != 0x1FFFFFFF && step < 16 ...) // inner walk
The compiler-emitted udiv helper returns 0xFFFFFFFF on a zero divisor (verified in both the SM5 and SM6 disassembly). Nothing bounds L1/L2 except that count.
Tested, in order
| Change | Result |
|---|---|
| NVIDIA 595 → 615, kernel 7.2.5 / 7.2.6, new GSP firmware | hangs |
RmEngineContextSwitchTimeoutUs 4.3 s → 17 s |
hangs; Xid 109 becomes Xid 8 + RC watchdog; GPU clocks drop ~60 %. Do not use. |
| D3D11/DXVK and D3D12/vkd3d; DXVK 2.6 → 3.1; vkd3d 2.14 → 3.x | hangs on both |
VKD3D_CONFIG=single_queue; NVAPI and Reflex on/off; HDR on/off; second monitor off; two fresh prefixes; all shader caches cleared; fresh character |
hangs |
| Test 1: override the two probe shaders, L3 + L4 capped at 4096 | hangs, 3.5 s after pipeline creation. Override loads confirmed in the vkd3d log. |
| Test 2: same, plus L1 + L2 capped at 128, L3 + L4 at 1024, siblings capped | stable: 60+ min from cold load, in-world setting changes, then days of play |
| Test 3: Optional GPU Features enabled in-game | hangs once: engine recompiles to the SM6 family, new hashes, nothing overridden. Same patch on those four: stable, setting stays on. |
Test 1 and test 2 differ only in whether L1/L2 are bounded.
Other data points: a single 24-hour clean run with giQuality 3 on 2026-09-18 that never reproduced. One apparent success that was a Lutris launch without the override in its environment; every result above was confirmed by the Overriding shader hash line in the vkd3d log.
Assumptions and open items
- Why the count is invalid on the first dispatch is inferred, not observed. Constant buffer contents were never captured. Zero divisor (→ 2^32 iterations), unwritten/stale buffer, and upload race are all consistent with the evidence. A variant guarding only
divisor == 0was built and is untested. - Why NVIDIA-on-Windows tolerates it is unknown.
- The 128 cap not truncating real work is arithmetic, not measurement: N² samples × 32 768 probes × 8 dispatches per frame fits a frame budget only for N ≤ 16.
- The 16x1x1 siblings were patched because they share the walk code; leaving them unpatched is untested.
- The NVIDIA-forum capture attributed the hang to the tree walk (L3/L4). Test 1 contradicts that on this build. Their 4096 cap on “two loops” fixed their replay; which two loops is the likely difference. Same shader either way.
- Build 69977: four days clean with the override still active in the launcher. Whether 69977 changed this shader is untested (no dump run since the update). If the hashes changed, the override is inert and the bug is fixed upstream. A dump run will settle it.
- Unrelated to the shader, both unverified: brightness flicker at high lighting settings, present on loading screens where GI does not run, with panel VRR set to Automatic (under evaluation with VRR off); an fps stall that clears on alt-tab, apparently Reflex pacing through vkd3d’s swapchain.
- Performance degradation over a session, believed to be a separate issue. A fresh boot holds the 144 fps target. Over the course of a play session frame rate deteriorates, most noticeably in Ironforge and Stormwind. GPU stays at full clocks and 70–85 % utilization during the drop, no throttling, no new shader compiles with the runaway signature. Other threads report the same zone-specific fps loss on build 69913 on Linux regardless of lighting settings (thread 2 below), so it is not attributed to this shader or to the override.
Workaround, step by step (Linux, NVIDIA, Proton)
Requires D3D12: SET GxApi "D3D12" in Config.wtf (edit with the game closed; WoW rewrites the file on clean exit). vkd3d-proton has VKD3D_SHADER_OVERRIDE. DXVK 3.x builds compute pipelines with a descriptor-heap pNext that the pyroveil shader layer cannot copy, so D3D11 compute shaders cannot be replaced this way.
- Dump. Launch once with
VKD3D_SHADER_DUMP_PATH=/some/dir VKD3D_SHADER_CACHE_PATH=0, enter the world on a stable config (GI off; the probe shaders compile regardless), exit. Each shader lands as<hash>.dxbc+<hash>.spv. - Find.
spirv-diseach.spv; matchOpEntryPoint GLCompute,LocalSizeId 4 4 4, the constant536870911(0x1FFFFFFF), and anOpTypeArray %uint %uint_256inWorkgroupstorage. Expect two. - Patch. In each, add an iteration counter to the outer two loops (phi in the loop header, increment in the continue block, OR into the exit condition) and force exit at 128; cap the walk loops at 1024. Assemble with
spirv-as --preserve-numeric-ids --target-env spv<module version>(SM6 output is SPIR-V 1.3; assembling as 1.6 fails interface validation). Validate:spirv-val --target-env vulkan1.3 --scalar-block-layout. - Apply. Put the patched files as
<hash>.spvin a directory; launch withVKD3D_SHADER_OVERRIDE=<dir>. WithVKD3D_DEBUG=infovkd3d logsOverriding shader hash <hash> with alternative SPIR-V module. No such line means no fix is active. vkd3d ignores cached SPIR-V automatically when the override variable is set. - Optional GPU Features on switches to the SM6 shader family (
Using shader family dx_6_0ingx.log). Repeat steps 1–4 for those hashes. - Any client patch may change the hashes. Re-dump after updates. Add the override variable to the same launcher Lutris uses; a launch without it, with GI enabled, is a guaranteed lock.
Patcher and finder scripts (Python over spirv-dis/spirv-as) available on request.
For the dev team
The GI probe shader dispatches in the first frame after GI resources are created with a loop count derived from cb0[2] / cb0[3] that is not yet valid. Initializing that constant buffer before the first dispatch, clamping the count, or bounding L1/L2 each prevents the hang. Reproduces in a GFXReconstruct replay (thread 1), on D3D11 and D3D12, and the same setting produces driver timeouts on AMD/Windows (Adrenalin 26.9.1; 9070 XT, 7800 XT, 6600) and hard freezes on macOS. Appeared between builds 69893 and 69913.
Threads referenced during debugging
- NVIDIA forum: “615.71.09 / RTX 5090: WoW DX12 freezes with ray tracing enabled. Xid 109 / VK_ERROR_DEVICE_LOST” (GFXReconstruct capture, replay reproduces, 4096-iteration cap on two loops fixes the replay)
- Blizzard bug report: “[WoW Forever] NVIDIA Xid 109 GPU hangs on world load and failed renderer recovery”
- Blizzard bug report: “[Workaround Found] WoW Forever 69913: reproducible GPU hang after world load (Linux / Wine, NVIDIA RTX 4090)”
- Blizzard EU tech support: “Secondary lighting causing driver crashes in Wow Forever” (AMD, Windows)
- Blizzard beta discussion: “Mac Crashing Solution! Turn SECONDARY LIGHTING to FAIR”
- NVIDIA forum: “Open NVIDIA modules under Proton: temporary workarounds for Xid 109 CTX SWITCH TIMEOUT” (general, not WoW-specific)