FAULT 01 · descriptors
graphics pipeline failures → 0
Polaris cannot compile a graphics pipeline that uses a mutable descriptor type
vkd3d-proton reaches for VK_EXT_mutable_descriptor_type because it collapses descriptor-heap emulation into one binding. amdvlk64 advertises it. On Polaris the compiler then fails every graphics pipeline that touches it, and D3D12 surfaces that as an opaque VK_ERROR_UNKNOWN.
Disabling both the EXT and VALVE forms took graphics-pipeline creation failures to zero in the live game. This was the first fault found and the one that made every later measurement possible — until it was gone, every log was dominated by it.
Where: device extension filter in the shim.
FAULT 02 · shader compiler
11 of 11 crashers fixed · 0 regressions across 402 modules
A gl_HelperInvocation load storm faults the driver's use-list walk
Some fragment shaders load the HelperInvocation builtin more than a hundred times. Polaris' shader compiler walks the use-list of that value and either takes an access violation that gets swallowed, or never returns at all — the same defect wearing two faces, which is why it read as two unrelated bugs for a day.
The fix is one line of intent: load the builtin once, reuse the result. Collapsing the loads to a single value fixed all eleven known crashers with zero regressions measured over 402 modules.
Getting there required abandoning hypothesis-driven debugging entirely. Every hand-picked theory about those shaders was refuted; the shader's declarations were innocent and its code was guilty. Delta-debugging the module found it in minutes after hours of reasoning had not.
Where: SPIR-V post-pass, vkCreateShaderModule hook.
FAULT 03 · shader compiler
second encoding · opcode 5381
The same builtin has a second form, and rewriting it grows the module
OpIsHelperInvocationEXT (opcode 5381) is the other way that value reaches the compiler. Handling it is not symmetric with fault 02: the rewrite needs spare space in the binary, the SPIR-V header length written back afterwards, and an anchor value that resets whenever a demote happens or a call that can reach a demote is made.
A CFG-aware version of that anchor collapse was designed, measured, and deliberately not written: it gained between 0% and 68% depending on the shader, the worst-case shader stayed above the fault threshold anyway, and the demote-dominance const-true rule gained exactly nothing. Measuring a refactor before writing it is cheaper than writing it.
Where: SPIR-V post-pass.
FAULT 04 · pipeline creation
refusals 9 → 0 · VkResult −3: 51 → 0 · E_OUTOFMEMORY 16 → 0
A dead FragmentShadingRateKHR declaration was refusing pipelines
Shaders declared variable-rate-shading capability on a card that has no VRS. The capability was never used — the declaration alone was enough for the driver to refuse the pipeline with VkResult = -3, which vkd3d then relabelled as E_OUTOFMEMORY at state.c:5728. That mistranslation sent the investigation after memory pressure for hours. The box did have only 0.3 GB free — true, and irrelevant.
Stripping the unused declaration took all three counters to zero on the same launch.
Four hypotheses died here first: the pipeline layout was not the discriminator (the failing layout was the same object graph as a passing one, and swapping them changed nothing). It was not helper-invocation related (a module collapsed to fanout 27 and accepted by vkCreateShaderModule was still refused, and three of the modules were helper-op-free vertex shaders). It survived dropping the fragment stage entirely. And it was not memory — peak commit 5.6 GB against a 16.4 GB limit.
Where: SPIR-V post-pass.
FAULT 05 · subgroups
validation admits WaveSize(32)
[WaveSize(32)] on a wave64 card
Shaders annotated [WaveSize(32)] are rejected outright: GCN4 is wave64 and the driver reports a fixed subgroup size of 64. Vulkan validation only requires min ≤ 32 ≤ max, so forging minSubgroupSize down to 16 satisfies it without lying about anything the shader can observe.
The trap in this fix
vkd3d-proton branches on the executable name CrimsonDesert.exe to apply an RDNA1 compatibility profile — and that branch is gated on minSubgroupSize == 32. So 32 is a uniquely dangerous value to forge here: it silently activates a profile written for different hardware. 16 is correct precisely because it is not 32.
Where: capability forge. Each mode change costs one cold-cache launch to evaluate.
FAULT 06 · fp16
337 fragment modules made invalid
The fp16 graphics demote emitted invalid SPIR-V
Having forged native fp16 support, the fp16 arithmetic then has to go somewhere. The graphics demote path rewrote %v2half to %v2float inside a bitcast, producing an OpBitcast between mismatched widths. 337 fragment modules became invalid SPIR-V. Compute shaders kept fp16 untouched and had zero failures — the split was perfectly clean along the Fragment boundary, which is what identified it.
Worth recording that fp16 was also ruled out as the cause of an earlier failure, twice: fp16 arithmetic and fp16 attachment formats were both validly eliminated. The fp16-versus-int16 distinction mattered, and this build compiles out WARN and TRACE, which is why that took as long as it did.
Where: SPIR-V post-pass. Currently disabled — DXIL_SPIRV_GRAPHICS_DISABLE_NATIVE_FP16 = 0.
FAULT 07 · command stream
indirect draws 14 → 255 per frame
Missing DGC silently discarded most indirect draws
This is the fault that was hiding the world. vkd3d's ExecuteIndirect path returns early when a command signature carries root arguments and device-generated commands are unavailable. It returns without drawing and without complaining. Of 5,284 command signatures the engine created, 5,281 carried root arguments.
Falling through to a manual path at command.c:17159 took indirect draws from 14 to 255 per frame. This engine is GPU-driven: its real batches carry roughly 53 draws each, and an earlier reading of maxDrawCount = 1 was survivorship bias — the batches that reached the log were the trivial ones.
Where: vkd3d-proton libs/vkd3d/command.c, +557 / −4.
FAULT 08 · command stream
450,000 calls handled · 0 rejected · 0 refusals
Per-command root constants, emulated without DGC
Fault 07's fallback then needs each unrolled command to see its own root constants. Two changes: force push-UBO mode, and unroll the argument buffer in a compute pass. 450,000 calls were handled with none rejected and no pipeline refusals introduced.
The PSO cache cannot detect this. Changing the root-argument interface changes shader behaviour without changing anything the pipeline cache hashes. A stale cache will serve pipelines built for the old interface. The cache has to be moved aside by hand when this lands — a silent-wrong-answer failure mode, not a crash.
Where: vkd3d-proton meta.c +51, vkd3d_private.h +56.
FAULT 09 · host memory
peak commit 30,916 → 8,768 MiB
The endless "Compiling Shaders" screen was a 4 GB AMD DxcCache
The single most user-visible fault, and it was not in any code. AMD's shader cache at %LOCALAPPDATA%\AMD\DxcCache had grown to 4.0 GB. The game then committed 30,916 MB against a 32,318 MB Windows commit limit — 805 MB of headroom. The compile worker died silently and the game sat on its compile screen forever, at 82 fps, showing a black frame.
The tell was equal working set with 2.7× the commit. That signature is memory that is reserved but never touched, which points at a mapped file, not at leaked allocations. Emptying the cache dropped peak commit to 8,768 MiB and the game reached gameplay.
Three other explanations for the same screen were measured and refuted first: the DGC unroll bound, bad shaders, and shader-compile concurrency.
Where: outside the process entirely. Now guarded at launch by Play Crimson Desert (RX 580).cmd, which never touches DxCache — no second 'c' — because the Radeon service holds that one open.
FAULT 10 · process
refusals 41 → 0 on revert
A shim build I had made myself was the regression
A compile-screen failure that looked like a game bug was a binary I had built thirteen minutes earlier. Restoring the previous build took refusals from 41 to zero and reached the menu. The cause was building by hand instead of with build_shim.py — the script strips the binary and a hand-rolled g++ line does not. The one time that was done, launch success went from 12/12 to 2/12.
The lasting change is procedural, and it is the most valuable single line in this report:
Hash every file you wrote against its backup before calling a revert complete. A revert that checked three files out of four cost a working build and an evening. Checking three of four is not a revert; it is a coin toss with extra steps.
Where: build procedure. backup-previous\d3d12.dll is the instant escape hatch.
FAULT 11 · capability
reporting gap closed
shaderFloat16 reporting, and the denorm modes underneath it
Forging fp16 support is not one boolean. The reported capability, the DenormPreserve / DenormFlushToZero execution modes, and what the shader actually does with a half have to agree, or validation rejects the module while the capability query says everything is fine. Three distinct modes were implemented and each has a different failure surface.
Where: capability forge; vkd3d_shaders.h, include/vkd3d_shader.h.
FAULT 12 · presentation
swapchain reached
The DXGI swapchain returned E_NOTIMPL
All the shader work in the world is invisible without a surface to present to. A companion dxgi.dll (1,272 lines) answers the swapchain creation path that was returning E_NOTIMPL. Combined with fault 02's change of substitution strategy — hooking below vkd3d's cache rather than through its override, which forces pipeline_library_ignore_spirv and kills caching — this produced the first swapchain the project ever reached.
Where: src/dxgishim, 6 files.