VK_EXT_descriptor_heap: fragment shader reads the wrong descriptor for any heap index other than 0
Reading a layout(descriptor_heap) array from a fragment shader at any index other than 0 returns the wrong descriptor on NVIDIA. Index 0 is correct. A literal constant index of 1 is enough to trigger it — there is no push data, no dynamic index and no nonuniformEXT involved.
Reproduced identically on two driver branches: 610.57.04 and 615.71.09.
The same heap, same descriptors and equivalent shader logic read correctly at every index from a compute shader on the same device, so this appears to be specific to the fragment stage.
Mesa RADV 26.2.3 renders every case correctly from the same SPIR-V and the same API calls.
Environment
| GPU | NVIDIA GeForce RTX 4060 |
| Driver | 615.71.09 (driverVersion 615.284.576, device API 1.4.351) — also reproduced on 610.57.04 (driverVersion 610.228.256, device API 1.4.341) |
| OS | Linux (CachyOS, kernel 7.2.6) |
| Loader | 1.4.357 |
| App API version | Vulkan 1.4 |
| Extensions | VK_EXT_descriptor_heap (spec v1 on both branches), VK_KHR_shader_untyped_pointers |
| Comparison device | AMD Radeon 7700X iGPU, RADV, Mesa 26.2.3 — all cases pass |
Minimum Repro
See README.md for further details.
Observed output
Nvidia driver:
./build/descriptor_heap_repro
================================================================
Part 1 of 2: CONTROL CASES (compute)
These are expected to PASS on every driver.
================================================================
Device: NVIDIA GeForce RTX 4060
Driver: 615.284.576
API version: 1.4.351
storage buffer descriptor size: 16 bytes
heap reserved range: 96768 bytes at offset 128
constant index : PASS
dynamic index (buffer) : PASS
pushdata index = 0 : PASS
pushdata index = 1 : PASS
RESULT: all cases passed.
================================================================
Part 2 of 2: THE REPRO (fragment stage)
Expected to FAIL on affected NVIDIA drivers, pass elsewhere.
================================================================
Device: NVIDIA GeForce RTX 4060
Driver: 615.284.576
API version: 1.4.351
[A] constant heap index 0 (expect red every frame):
-> 40/40 frames correct
[B] constant heap index 1 (expect green every frame):
frame 0: pushed index 0, expected (0,255,0,255), got (0,0,0,0)
frame 1: pushed index 1, expected (0,255,0,255), got (0,0,0,0)
frame 2: pushed index 0, expected (0,255,0,255), got (0,0,0,0)
frame 3: pushed index 1, expected (0,255,0,255), got (0,0,0,0)
frame 4: pushed index 0, expected (0,255,0,255), got (0,0,0,0)
frame 5: pushed index 1, expected (0,255,0,255), got (0,0,0,0)
... and 34 more
-> 0/40 frames correct
[C] heap index 0, pushed value selects inside the buffer:
-> 40/40 frames correct
RESULT: heap slot 0 reads correctly, but a CONSTANT heap index of 1
reads the wrong descriptor (B failed 40/40 frames).
No push data or dynamic indexing is involved in B at all.
Keeping the heap index at 0 and selecting inside the buffer
works (C passed), which is a viable workaround.
================================================================
SUMMARY
control cases (compute) : PASS
repro (fragment) : FAIL <-- the bug
================================================================
The controls all pass, so descriptor heaps work fine in compute at
every index. Only the fragment stage is affected, and only for heap
indices other than 0 -- even a literal constant 1. This is the bug.
AMD driver:
VK_DRIVER_FILES=/usr/share/vulkan/icd.d/radeon_icd.json ./build/descriptor_heap_repro
================================================================
Part 1 of 2: CONTROL CASES (compute)
These are expected to PASS on every driver.
================================================================
Device: AMD Ryzen 7 7700X 8-Core Processor (RADV RAPHAEL_MENDOCINO)
Driver: 26.2.3
API version: 1.4.354
storage buffer descriptor size: 16 bytes
heap reserved range: 0 bytes at offset 96
constant index : PASS
dynamic index (buffer) : PASS
pushdata index = 0 : PASS
pushdata index = 1 : PASS
RESULT: all cases passed.
================================================================
Part 2 of 2: THE REPRO (fragment stage)
Expected to FAIL on affected NVIDIA drivers, pass elsewhere.
================================================================
Device: AMD Ryzen 7 7700X 8-Core Processor (RADV RAPHAEL_MENDOCINO)
Driver: 26.2.3
API version: 1.4.354
[A] constant heap index 0 (expect red every frame):
-> 40/40 frames correct
[B] constant heap index 1 (expect green every frame):
-> 40/40 frames correct
[C] heap index 0, pushed value selects inside the buffer:
-> 40/40 frames correct
RESULT: all cases correct.
================================================================
SUMMARY
control cases (compute) : PASS
repro (fragment) : PASS
================================================================
No failure observed on this driver.
The heap has two slots, both written with vkWriteResourceDescriptorsEXT at init: slot 0 holds a uniform buffer with two colours (red, green), slot 1 holds a uniform buffer with one colour (green).
- [A] reads slot 0. Correct on both vendors.
- [B] reads slot 1 via
tints[1].color, a literal constant. Expected green; NVIDIA returns something else. This is the bug in its smallest form. - [C] reads slot 0 but selects between the two colours inside that buffer using a
vkCmdPushDataEXTvalue. Correct on both vendors, so ordinary array indexing within a buffer is unaffected — it is specifically descriptor-heap indexing that breaks.
The incorrect value returned by [B] varies with heap layout: with this layout it is (0,0,0,0), and with a three-slot layout it was slot 0’s colour instead. That suggests a wrong address computation rather than the index simply being clamped to zero.
What works and what does not
| Stage | Index kind | Index value | NVIDIA (610 & 615) | RADV |
|---|---|---|---|---|
| compute | constant | 0–3 | pass | pass |
| compute | dynamic, from storage buffer | 0–3 | pass | pass |
| compute | dynamic, from push data | 0 and 1 | pass | pass |
| fragment | constant | 0 | pass | pass |
| fragment | constant | 1 | fail | pass |
| fragment | dynamic, from push data | 0 | pass | pass |
| fragment | dynamic, from push data | 1 | fail | pass |
Edit: I’ve reproduced this on RTX PRO 4000 Blackwell too
./build/descriptor_heap_repro
================================================================
Part 1 of 2: CONTROL CASES (compute)
These are expected to PASS on every driver.
================================================================
Device: NVIDIA RTX PRO 4000 Blackwell
Driver: 610.228.256
API version: 1.4.341
storage buffer descriptor size: 16 bytes
heap reserved range: 96768 bytes at offset 96
constant index : PASS
dynamic index (buffer) : PASS
pushdata index = 0 : PASS
pushdata index = 1 : PASS
RESULT: all cases passed.
================================================================
Part 2 of 2: THE REPRO (fragment stage)
Expected to FAIL on affected NVIDIA drivers, pass elsewhere.
================================================================
Device: NVIDIA RTX PRO 4000 Blackwell
Driver: 610.228.256
API version: 1.4.341
[A] constant heap index 0 (expect red every frame):
-> 40/40 frames correct
[B] constant heap index 1 (expect green every frame):
frame 0: pushed index 0, expected (0,255,0,255), got (0,0,0,0)
frame 1: pushed index 1, expected (0,255,0,255), got (0,0,0,0)
frame 2: pushed index 0, expected (0,255,0,255), got (0,0,0,0)
frame 3: pushed index 1, expected (0,255,0,255), got (0,0,0,0)
frame 4: pushed index 0, expected (0,255,0,255), got (0,0,0,0)
frame 5: pushed index 1, expected (0,255,0,255), got (0,0,0,0)
... and 34 more
-> 0/40 frames correct
[C] heap index 0, pushed value selects inside the buffer:
-> 40/40 frames correct
RESULT: heap slot 0 reads correctly, but a CONSTANT heap index of 1
reads the wrong descriptor (B failed 40/40 frames).
No push data or dynamic indexing is involved in B at all.
Keeping the heap index at 0 and selecting inside the buffer
works (C passed), which is a viable workaround.
================================================================
SUMMARY
control cases (compute) : PASS
repro (fragment) : FAIL <-- the bug
================================================================
The controls all pass, so descriptor heaps work fine in compute at
every index. Only the fragment stage is affected, and only for heap
indices other than 0 -- even a literal constant 1. This is the bug.