Apart of #8517
Description
Under -fspv-use-descriptor-heap, a local resource variable initialized from ResourceDescriptorHeap is not lowered to a stored descriptor handle. Instead the variable's VarDecl is recorded in a compile-time alias map (descriptorHeapImageAliasVars for image-like resources, descriptorHeapBufferAliasVars for buffer-like resources, and registerFnVarAlias for RaytracingAccelerationStructure), and later uses of that variable are re-lowered as a fresh OpUntypedAccessChainKHR over the recorded heap index.
The recorded index is materialized into a function variable named <var>.descriptor.index, created without an initializer (SpirvEmitter::createDescriptorHeapIndexVar, SpirvEmitter.cpp:5267), and stored at each heap assignment (tryToAssignDescriptorHeapImageAlias, :5329; tryToAssignDescriptorHeapBufferAlias, :5368). That index variable is a normal SSA promotion candidate, so merging two heap indices across control flow already works. What has no representation is a path on which no heap index was ever stored, and a non-heap assignment that should retire the alias.
This map is keyed on the declaration and has no notion of control flow or of being invalidated. It is only ever inserted into, never cleared. That makes the lowering unsound whenever a variable does not hold a heap descriptor on every path reaching the use:
- Conditional assignment: if only one branch assigns from the heap, the alias is recorded unconditionally and the merged heap index is undefined on the other path.
- Reassignment to a bound resource: assigning a bound resource over a heap-aliased variable does not remove the alias entry, so later uses still index the heap and the bound resource is dropped from the module entirely.
- Assignment inside a loop: the alias is recorded from the loop body, but the heap index is undefined when the body never executes.
Where the alias is consumed differs by resource class, and so does the blast radius:
- Image-like resources (
RWTexture*, and RWBuffer via the same predicate at :5344): consumed only by the Interlocked* lowering (emitDescriptorHeapImageTexelPointer, :5428). Non-atomic reads and writes still go through the variable's own function variable.
- Buffer-like resources (
(RW)StructuredBuffer, (RW)ByteAddressBuffer, ConstantBuffer, TextureBuffer): consumed at every DeclRefExpr for the variable (doExpr, :1299-1301), so all loads and stores are affected, not just atomics.
RaytracingAccelerationStructure: consumed at every use via registerFnVarAlias in DeclResultIdMapper; the alias cannot be updated after initialization.
Root cause: the alias is compile-time state attached to a VarDecl rather than an SSA value that participates in control-flow merges. SpirvEmitter::doBinaryOperator calls tryToAssignToDescriptorHeapBuffer and tryToAssignDescriptorHeapImageAlias on every BO_Assign with no path sensitivity and no invalidation on non-heap assignment. Note that the index variable itself does merge correctly; the missing piece is that a bound resource has no index to merge, and that a non-heap store leaves the stale alias in place.
Not affected (verified against this build): heap-to-heap reassignment, including under a conditional (the two indices merge into an OpSelect), inside a loop, and with non-constant indices; every Interlocked* opcode, the two-argument form, and swizzled destinations; shaders compiled without -fspv-use-descriptor-heap, where the same source lowers the heap access into the variable's own storage; and the DXIL backend, which shares none of this code.
Steps to Reproduce
Compiled with #8517 branch.
Defect 1: conditional assignment
// dxc -T cs_6_6 -E main -fspv-use-descriptor-heap -fspv-target-env=vulkan1.3 -spirv repro.hlsl
RWByteAddressBuffer outputBytes : register(u0);
RWTexture2D<uint> boundTex : register(u1);
[numthreads(1, 1, 1)]
void main(uint3 tid : SV_DispatchThreadID) {
uint original;
RWTexture2D<uint> mixed = boundTex;
if (tid.x == 0)
mixed = ResourceDescriptorHeap[1];
InterlockedAdd(mixed[tid.xy], 1, original);
outputBytes.Store(0, original);
}
Expected: either correct code for both paths, or a diagnostic.
Defect 2: reassignment to a bound resource
RWTexture2D<uint> mixed = ResourceDescriptorHeap[2];
mixed = boundTex;
InterlockedAdd(mixed[tid.xy], 2, original);
Expected: the atomic targets boundTex. No control flow is involved; this miscompiles in straight-line code.
Defect 3: assignment inside a loop
RWTexture2D<uint> mixed = boundTex;
for (uint i = 0; i < tid.y; ++i)
mixed = ResourceDescriptorHeap[i];
InterlockedAdd(mixed[tid.xy], 3, original);
Expected: boundTex when the loop body never runs.
Defect 4: buffer reassigned from heap to a bound resource
StructuredBuffer<uint> boundBuf : register(t0);
...
StructuredBuffer<uint> mixedBuf = ResourceDescriptorHeap[2];
mixedBuf = boundBuf;
uint value = mixedBuf.Load(0);
Expected: the load targets boundBuf.
Actual Behavior (current build)
All four defects above are now diagnosed at compile time:
error: mixing bound and descriptor heap resources in the same variable is not
supported with SPV_EXT_descriptor_heap
Defects 1–3 (any assignment sequence that mixes a bound-resource assignment and a heap-resource assignment to the same variable, in any order) are caught by diagnoseDescriptorHeapAliasMixing, which tracks per-variable state in descriptorHeapVarState. Defect 4 (buffer reassignment) is caught by the same diagnostic and no longer produces an ICE.
Remaining unsound case (not yet diagnosed)
A heap-only conditional assignment with no bound-resource counterpart is still not caught:
RWTexture2D<uint> mixed;
if (cond)
mixed = ResourceDescriptorHeap[1];
InterlockedAdd(mixed[uint2(0, 0)], 1, original); // undefined index on else path
Here wasBound is never set, so the mixing check does not fire. The alias is registered inside the branch and is applied unconditionally on uses outside it. For image-like resources this only affects Interlocked* uses; for buffer-like resources all loads and stores are affected. This requires dataflow analysis to diagnose correctly.
Note on scope for image-like resources: the silent window for this remaining case is a heap-aliased variable whose only uses are Interlocked*. Adding an ordinary read or write of the same variable keeps its image function variable live, which then hits a separate, pre-existing limitation (fatal error: generated SPIR-V is invalid: Result type cannot be OpTypeImage on an OpPhi of image type). That failure is unrelated to descriptor heaps, it reproduces with a conditional between two bound textures and no heap flag, but it is worth knowing that the alias path is what converts that loud failure into a quiet one, by making the image variable dead.
Workaround: use separate variables for bound and heap-loaded resources, and hoist the heap access so the variable is assigned from the heap on every path.
A full fix cannot reuse the existing index variable alone, since it already participates in phi nodes and a bound resource has no index to contribute. It requires either carrying a heap-vs-bound discriminant alongside the index and selecting the access form per path, or deriving the access from the resource variable itself after mem2reg, so that bound and heap descriptors merge as one value.
Apart of #8517
Description
Under
-fspv-use-descriptor-heap, a local resource variable initialized fromResourceDescriptorHeapis not lowered to a stored descriptor handle. Instead the variable'sVarDeclis recorded in a compile-time alias map (descriptorHeapImageAliasVarsfor image-like resources,descriptorHeapBufferAliasVarsfor buffer-like resources, andregisterFnVarAliasforRaytracingAccelerationStructure), and later uses of that variable are re-lowered as a freshOpUntypedAccessChainKHRover the recorded heap index.The recorded index is materialized into a function variable named
<var>.descriptor.index, created without an initializer (SpirvEmitter::createDescriptorHeapIndexVar,SpirvEmitter.cpp:5267), and stored at each heap assignment (tryToAssignDescriptorHeapImageAlias,:5329;tryToAssignDescriptorHeapBufferAlias,:5368). That index variable is a normal SSA promotion candidate, so merging two heap indices across control flow already works. What has no representation is a path on which no heap index was ever stored, and a non-heap assignment that should retire the alias.This map is keyed on the declaration and has no notion of control flow or of being invalidated. It is only ever inserted into, never cleared. That makes the lowering unsound whenever a variable does not hold a heap descriptor on every path reaching the use:
Where the alias is consumed differs by resource class, and so does the blast radius:
RWTexture*, andRWBuffervia the same predicate at:5344): consumed only by theInterlocked*lowering (emitDescriptorHeapImageTexelPointer,:5428). Non-atomic reads and writes still go through the variable's own function variable.(RW)StructuredBuffer,(RW)ByteAddressBuffer,ConstantBuffer,TextureBuffer): consumed at everyDeclRefExprfor the variable (doExpr,:1299-1301), so all loads and stores are affected, not just atomics.RaytracingAccelerationStructure: consumed at every use viaregisterFnVarAliasinDeclResultIdMapper; the alias cannot be updated after initialization.Root cause: the alias is compile-time state attached to a
VarDeclrather than an SSA value that participates in control-flow merges.SpirvEmitter::doBinaryOperatorcallstryToAssignToDescriptorHeapBufferandtryToAssignDescriptorHeapImageAliason everyBO_Assignwith no path sensitivity and no invalidation on non-heap assignment. Note that the index variable itself does merge correctly; the missing piece is that a bound resource has no index to merge, and that a non-heap store leaves the stale alias in place.Not affected (verified against this build): heap-to-heap reassignment, including under a conditional (the two indices merge into an
OpSelect), inside a loop, and with non-constant indices; everyInterlocked*opcode, the two-argument form, and swizzled destinations; shaders compiled without-fspv-use-descriptor-heap, where the same source lowers the heap access into the variable's own storage; and the DXIL backend, which shares none of this code.Steps to Reproduce
Compiled with #8517 branch.
Defect 1: conditional assignment
Expected: either correct code for both paths, or a diagnostic.
Defect 2: reassignment to a bound resource
Expected: the atomic targets
boundTex. No control flow is involved; this miscompiles in straight-line code.Defect 3: assignment inside a loop
Expected:
boundTexwhen the loop body never runs.Defect 4: buffer reassigned from heap to a bound resource
Expected: the load targets
boundBuf.Actual Behavior (current build)
All four defects above are now diagnosed at compile time:
Defects 1–3 (any assignment sequence that mixes a bound-resource assignment and a heap-resource assignment to the same variable, in any order) are caught by
diagnoseDescriptorHeapAliasMixing, which tracks per-variable state indescriptorHeapVarState. Defect 4 (buffer reassignment) is caught by the same diagnostic and no longer produces an ICE.Remaining unsound case (not yet diagnosed)
A heap-only conditional assignment with no bound-resource counterpart is still not caught:
Here
wasBoundis never set, so the mixing check does not fire. The alias is registered inside the branch and is applied unconditionally on uses outside it. For image-like resources this only affectsInterlocked*uses; for buffer-like resources all loads and stores are affected. This requires dataflow analysis to diagnose correctly.Note on scope for image-like resources: the silent window for this remaining case is a heap-aliased variable whose only uses are
Interlocked*. Adding an ordinary read or write of the same variable keeps its image function variable live, which then hits a separate, pre-existing limitation (fatal error: generated SPIR-V is invalid: Result type cannot be OpTypeImageon anOpPhiof image type). That failure is unrelated to descriptor heaps, it reproduces with a conditional between two bound textures and no heap flag, but it is worth knowing that the alias path is what converts that loud failure into a quiet one, by making the image variable dead.Workaround: use separate variables for bound and heap-loaded resources, and hoist the heap access so the variable is assigned from the heap on every path.
A full fix cannot reuse the existing index variable alone, since it already participates in phi nodes and a bound resource has no index to contribute. It requires either carrying a heap-vs-bound discriminant alongside the index and selecting the access form per path, or deriving the access from the resource variable itself after mem2reg, so that bound and heap descriptors merge as one value.