[Linux/NVIDIA] Forever GI / Secondary Lighting GPU hang (Xid 109): cause isolated, shader-override workaround

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 >= 1 or graphicsLightMode >= 2 locks 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

  1. 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 == 0 was built and is untested.
  2. Why NVIDIA-on-Windows tolerates it is unknown.
  3. 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.
  4. The 16x1x1 siblings were patched because they share the walk code; leaving them unpatched is untested.
  5. 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.
  6. 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.
  7. 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.
  8. 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.

  1. 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.
  2. Find. spirv-dis each .spv; match OpEntryPoint GLCompute, LocalSizeId 4 4 4, the constant 536870911 (0x1FFFFFFF), and an OpTypeArray %uint %uint_256 in Workgroup storage. Expect two.
  3. 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.
  4. Apply. Put the patched files as <hash>.spv in a directory; launch with VKD3D_SHADER_OVERRIDE=<dir>. With VKD3D_DEBUG=info vkd3d logs Overriding 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.
  5. Optional GPU Features on switches to the SM6 shader family (Using shader family dx_6_0 in gx.log). Repeat steps 1–4 for those hashes.
  6. 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

  1. 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)
  2. Blizzard bug report: “[WoW Forever] NVIDIA Xid 109 GPU hangs on world load and failed renderer recovery”
  3. Blizzard bug report: “[Workaround Found] WoW Forever 69913: reproducible GPU hang after world load (Linux / Wine, NVIDIA RTX 4090)”
  4. Blizzard EU tech support: “Secondary lighting causing driver crashes in Wow Forever” (AMD, Windows)
  5. Blizzard beta discussion: “Mac Crashing Solution! Turn SECONDARY LIGHTING to FAIR”
  6. NVIDIA forum: “Open NVIDIA modules under Proton: temporary workarounds for Xid 109 CTX SWITCH TIMEOUT” (general, not WoW-specific)
7 Likes

You can’t expect blizzard to support use of Linux via bottles/lutris/wine/steam. Especially on a glass cannon OS, mutable or not.

FWIW I’ve been able to run it on fedora on an rtx3090 without issues. It has its inconveniences as expected depending on how you run it, but it runs and you wouldn’t know you’re on Linux on a full screen windowed single display setup.

Not a Linux support question.

The hang is a compiled shader shipped in the client, and the same setting produces driver timeouts on AMD/Windows and hard freezes on macOS. Both are officially supported platforms. See threads 4 and 5 at the bottom.

Since you have a working setup on Fedora, would you post the details: distro version, driver, Proton build, Secondary Lighting and GI values. That’s information that helps narrow down this clearly widespread issue.

3 Likes

Well they’ve fixed freeze for macOS (although still can’t see shadows but that’s a different story), the ironic part now is that global illumination + shadows work on mac if you run windows version of wow through crossover (which is basically a wine as well) with some performance to pay ofc

1 Like

It’s not just Linux.

OP:

I’m retired gamedev. Good data. I mainly handled physics and game systems and never got into rendering and shaders but I do know enough. Thanks for digging this deep. I legit hate looking at code now after retirement so I’m glad someone is doing it. Note: I’m not lazy I just hate it now. And retired. I ain’t gotta look at that stuff anymore. You’ll know what I mean when you get there. Keep grinding.

1 Like

Same here but I’ll admit it. I’m lazy af! :laughing:

1 Like

I am indeed one of the people experiencing AMD driver timeouts like in thread 4 over “Secondary lighting” and I’m using Windows. I do hope Blizzard sees this and takes action

2 Likes