You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Add opt-in unified ArrayStride for SPIR-V resource descriptor heaps (#11723)
## Motivation
Fixes#11718.
`VK_EXT_descriptor_heap` / `spvDescriptorHeapEXT` packs every resource
descriptor
(images and buffers) into one runtime array — the "resource heap" — and
a separate
array for samplers. The host allocator stores each heap slot at a
**fixed stride**:
for the resource heap, the spec (§"Shader Model 6.6 - SamplerHeap and
ResourceHeap")
defines that stride as the canonical maximum
```
max(sizeof(image descriptor), sizeof(buffer descriptor))
```
so that `index * stride` lands on the same byte offset regardless of
which descriptor
category a given access happens to read.
Slang previously emitted a per-type `ArrayStrideIdEXT` derived only from
the descriptor
types a *single* shader referenced. That is wrong for a shared heap: an
image-only
shader and a buffer-only shader, indexing the **same** host-packed heap,
would advertise
two different strides and mis-index each other's descriptors. The
motivating case:
```hlsl
[shader("compute")][numthreads(1,1,1)]
void computeMain(
DescriptorHandle<Texture2D<float4>> tex,
DescriptorHandle<ConstantBuffer<MyStruct>> cbuffer,
RWStructuredBuffer<uint> output)
{
float4 texel = tex.Load(int3(0, 0, 0));
output[0] = (uint)texel.x + (*cbuffer).value;
}
```
Compiled with `-spirv-unified-descriptor-heap-stride`, both resource
arrays here — and a
buffer-only shader sharing the heap — must advertise the identical
image-vs-buffer max.
## Proposed solution
Add an **opt-in** option, `-spirv-unified-descriptor-heap-stride`, that
decorates every
image/buffer resource descriptor-heap runtime array with one shared,
**fixed** `ArrayStrideIdEXT`:
the canonical `max(sizeof(image descriptor), sizeof(buffer
descriptor))`. (Acceleration-structure
heap entries are unaffected — they keep their existing literal `uint64`
stride.)
The stride is **not adaptive to the shader's contents.** Because the
host heap layout is
independent of any one shader, the emitted stride is always the full
image-vs-buffer max —
even for a shader that touches only a buffer. A buffer-only shader still
emits the
placeholder image descriptor type and the full `Select` chain, so two
shaders over the same
heap can never disagree.
`OpConstantSizeOfEXT` is **device-defined** (the descriptor size is
decided by the driver,
not the compiler), so the maximum cannot be a compile-time literal. It
is expressed
symbolically as `max(a, b) = Select(UGreaterThan(a, b), a, b)` using
`OpSpecConstantOp`,
whose result is a constant `<id>` — a requirement for
`ArrayStrideIdEXT`.
(`OpExtInst GLSL.std.450 UMax` will not do: its result is not a constant
`<id>`.)
The unified stride is built lazily and memoized the first time a
resource array needs it
(`getUnifiedResourceHeapStride()`), so the constant `<id>` is emitted
**before** the first
array type — `ArrayStrideIdEXT` requires the stride `<id>` to precede
the array it decorates.
There is no separate finalize pass and no array reordering.
**Samplers are out of scope.** The spec uses two heaps — samplers are
never co-located with
resources — so the unified resource-heap stride folds only
image-vs-buffer. Sampler arrays
(`OpTypeSampler` element type) are excluded from the fold and keep their
existing
`-spirv-sampler-heap-stride` behavior.
**Mutual exclusivity.** A **non-zero** `-spirv-resource-heap-stride` and
the unified maximum
stride are mutually exclusive — the explicit literal and the
device-defined max are different
contracts for the same array, so combining them is a **hard error**
(`E57006`,
`SpirvConflictingDescriptorHeapStrideOptions`), not a silent "one wins."
An explicit
`-spirv-resource-heap-stride 0` selects the same default
`OpConstantSizeOfEXT` path the unified
option modifies and is therefore accepted (not a conflict).
### Placeholder image descriptor type — deviation from the proposal
The issue's literal proposal sketches the placeholder image descriptor
as
`OpTypeImage %void 2D 2 0 0 0 Unknown`. That is **invalid SPIR-V for
Vulkan**:
- `VUID-StandaloneSpirv-OpTypeImage-04656` — the sampled type must be a
32-bit int,
64-bit int, or 32-bit float scalar; `%void` is rejected.
- `VUID-StandaloneSpirv-OpTypeImage-04657` — `Sampled` must be `1` or
`2`, never `0`.
A descriptor's *device-defined size* is its heap-slot footprint and is
independent of the
image's sampled type and `Sampled` flag, so we use a spec-valid
placeholder
`OpTypeImage %float 2D 2 0 0 1 Unknown` (sampled type `float`, `Sampled
= 1`; depth operand
stays `Unknown = 2`). Both entry points of the regression test then pass
static SPIR-V
validation (see below). cc @csyonghe — flagging the placeholder
deviation explicitly.
## Change summary
| File | Change |
| --- | --- |
| `include/slang.h` | Append `SPIRVUnifiedDescriptorHeapStride = 153`
before the `CountOf` sentinel (additive, ABI-safe); doc notes mutual
exclusivity with a non-zero `-spirv-resource-heap-stride`. |
| `source/slang/slang-options.cpp` | Register the boolean CLI option
`-spirv-unified-descriptor-heap-stride` and add its case to the
boolean-option parse switch. |
| `source/slang/slang-emit-spirv.cpp` | `getUnifiedResourceHeapStride()`
lazily emits the fixed image-vs-buffer `Select` max (memoized).
`getDescriptorRuntimeArrayType()` decorates resource arrays with that
shared `ArrayStrideIdEXT` when the option is on; sampler arrays are
excluded. `diagnoseConflictingDescriptorHeapStrideOptions()` raises
`E57006` when the unified option and a non-zero
`-spirv-resource-heap-stride` are both set. |
| `source/slang/slang-diagnostics.lua` | Define
`SpirvConflictingDescriptorHeapStrideOptions` (`E57006`). |
| `tests/spirv/descriptor-heap-unified-stride.slang` | Four variants:
UNIFIED (shared max, two arrays one stride id), DEFAULT (control —
per-type size-of, no `Select`), CONFLICT (diagnostic — unified option +
a non-zero `-spirv-resource-heap-stride` → `E57006`), SINGLE
(buffer-only shader still emits the full image-vs-buffer max, proving
non-adaptivity). |
## Concepts and vocabulary
- **Resource heap vs sampler heap** — `spvDescriptorHeapEXT` uses two
separate runtime
arrays. The unified stride applies only to the resource heap (images +
buffers).
- **`ArrayStrideIdEXT`** — decorates a runtime array with a constant
`<id>` byte stride; that
`<id>` must be defined *before* the array type in the module.
- **`OpConstantSizeOfEXT`** — a device-defined (opaque, driver-decided)
constant giving a
descriptor type's heap-slot size. Not a compile-time literal.
- **Fixed / non-adaptive stride** — the emitted stride depends only on
the spec's canonical
image-vs-buffer max, never on which descriptor types the shader happens
to reference.
## Process report
- **`include/slang.h`** — new options need a `CompilerOptionName`
enumerator. Appended
`SPIRVUnifiedDescriptorHeapStride = 153` immediately before `CountOf`
with an explicit
value (never mid-enum), so existing ABI offsets are unchanged. `pr:
non-breaking`.
- **`source/slang/slang-options.cpp`** — the option is a pure boolean
(presence = on), so it
is registered with a `nullptr` value-spec and routed through the
existing boolean-option
parse case alongside `TraceCoverageBoolean` / `SkipSPIRVValidation`. No
new parsing
machinery.
- **`source/slang/slang-emit-spirv.cpp`** —
- `getUnifiedResourceHeapStride()` builds, in `ConstantsAndTypes`, the
placeholder image
descriptor type (`OpTypeImage %float 2D 2 0 0 1 Unknown`), the buffer
descriptor type
(`ensureDescriptorHeapBufferDescriptorType(SpvStorageClassUniform)`),
their two
`OpConstantSizeOfEXT`, `OpSpecConstantOp OpUGreaterThan`, and
`OpSpecConstantOp OpSelect`.
It is memoized on `m_unifiedResourceHeapStride` and emitted lazily so
the stride `<id>`
precedes the first decorated array (the `ArrayStrideIdEXT` ordering
rule).
- `getDescriptorRuntimeArrayType()` gates the fold on
`arrayStride == 0 && isResourceElement &&
isUnifiedResourceHeapStrideEnabled()`, where
`isResourceElement = descriptorElementType->opcode != SpvOpTypeSampler`.
When folding, it
skips the per-type `OpConstantSizeOfEXT` and decorates with the shared
`emitOpDecorateArrayStrideIdEXT`; otherwise it keeps the prior per-type
/ literal path.
*Input-shape check:* the per-array descriptor element type is the
correct, principled
input — the fold is a property of the **heap**, not of a malformed type,
so it lives at
the array-construction site rather than masking anything upstream.
- `diagnoseConflictingDescriptorHeapStrideOptions()` is called once in
`emitSPIRVFromIR`
(after the forward-declared-pointers loop) and raises `E57006` when the
unified option
and a non-zero `-spirv-resource-heap-stride` are both set. It fires on
the CLI
combination even when the module declares no resource heap (the user
supplied
contradictory intent), and emits no instructions.
- **`source/slang/slang-diagnostics.lua`** —
`SpirvConflictingDescriptorHeapStrideOptions`
(`E57006`) is a location-less hard error: "an explicit resource heap
stride and the unified
maximum stride are mutually exclusive." It is only emitted when
`-spirv-resource-heap-stride`
is non-zero (an explicit `0` is the default path and is accepted), so
the "explicit resource
heap stride" it names is always a non-zero one.
- **`tests/spirv/descriptor-heap-unified-stride.slang`** — the SINGLE
variant is the key
guardrail: a buffer-only shader compiled with the flag still emits the
placeholder
`OpTypeImage` and the full `Select` chain, demonstrating the stride is
fixed, not adaptive.
### Validation
Built `slang-test` (Debug); `descriptor-heap-unified-stride.slang`
passes all 4 static variants
(UNIFIED, DEFAULT, CONFLICT, SINGLE).
Static SPIR-V validation of both unified-stride entry points, under
`SLANG_RUN_SPIRV_VALIDATION=1`, exits 0:
```
$ SLANG_RUN_SPIRV_VALIDATION=1 slangc tests/spirv/descriptor-heap-unified-stride.slang \
-target spirv -capability spvDescriptorHeapEXT -spirv-unified-descriptor-heap-stride \
-entry computeMain -stage compute -o main.spv # EXIT=0
$ SLANG_RUN_SPIRV_VALIDATION=1 slangc tests/spirv/descriptor-heap-unified-stride.slang \
-target spirv -capability spvDescriptorHeapEXT -spirv-unified-descriptor-heap-stride \
-entry computeSingle -stage compute -o single.spv # EXIT=0
```
> **Caveat:** static `spirv-val` confirms the module is well-formed, but
the device driver is the
> final arbiter of the device-defined descriptor sizes and the resulting
stride — this opt-in path
> has not been exercised end-to-end on hardware. The runtime stack
(slang-rhi) does not yet support
> `VK_EXT_descriptor_heap`, so a runtime `COMPARE_COMPUTE` is not
meaningful for this path today.
> The two-heap assumption was confirmed by @chaoticbob on this PR.
<sub>🤖 Generated by an automated Slang coworker — may be inaccurate. A
human maintainer should verify.</sub>
---------
Co-authored-by: nv-slang-bot[bot] <274397474+nv-slang-bot[bot]@users.noreply.github.com>
Copy file name to clipboardExpand all lines: docs/command-line-slangc-reference.md
+5Lines changed: 5 additions & 0 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -644,6 +644,11 @@ Specify the byte stride for the resource descriptor heap when generating SPIRV w
644
644
Specify the byte stride for the sampler descriptor heap when generating SPIRV with spvDescriptorHeapEXT. Defaults to 0, which will use OpConstantSizeOfEXT(OpTypeSampler).
645
645
646
646
647
+
<aid="spirv-unified-descriptor-heap-stride"></a>
648
+
### -spirv-unified-descriptor-heap-stride
649
+
When generating SPIRV with spvDescriptorHeapEXT, emit each resource descriptor-heap runtime array's ArrayStride as the maximum of image and buffer descriptor sizes, so a single heap shared by buffers and images is indexed at the device's unified stride. Only affects the default OpConstantSizeOfEXT path (used when [-spirv-resource-heap-stride](#spirv-resource-heap-stride) is 0); mutually exclusive with a non-zero [-spirv-resource-heap-stride](#spirv-resource-heap-stride) (combining the two is an error). Does not affect the sampler heap or acceleration-structure entries.
650
+
651
+
647
652
<aid="separate-debug-info"></a>
648
653
### -separate-debug-info
649
654
Emit debug data to a separate file, and strip it from the main output file.
Copy file name to clipboardExpand all lines: docs/user-guide/08-compiling.md
+1Lines changed: 1 addition & 0 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -1091,6 +1091,7 @@ meanings of their `CompilerOptionValue` encodings.
1091
1091
| ValidateUniformity | When set will perform [uniformity analysis](a1-05-uniformity.md).|
1092
1092
| SPIRVResourceHeapStride | Specifies the byte stride for the resource descriptor heap when generating SPIR-V with `spvDescriptorHeapEXT`. `intValue0` encodes the stride in bytes; use 0 to emit `OpConstantSizeOfEXT(ResourceType)` as the default stride. For `RaytracingAccelerationStructure` entries, the 0 default emits a literal 8-byte `ArrayStride` for the `uint64` device address elements; explicit stride values still override these defaults, but must be at least 8 bytes for acceleration-structure entries. |
1093
1093
| SPIRVSamplerHeapStride | Specifies the byte stride for the sampler descriptor heap when generating SPIR-V with `spvDescriptorHeapEXT`. `intValue0` encodes the stride in bytes; use 0 to let the driver compute the stride via `OpConstantSizeOfEXT`. |
1094
+
| SPIRVUnifiedDescriptorHeapStride | When generating SPIR-V with `spvDescriptorHeapEXT`, emits each resource descriptor-heap runtime array's `ArrayStride` as the maximum of the image and buffer descriptor sizes, so a single heap shared by buffers and images is indexed at the device's unified stride. Only affects the default `OpConstantSizeOfEXT` path (used when `SPIRVResourceHeapStride` is 0); mutually exclusive with a non-zero `SPIRVResourceHeapStride` (combining the two is an error). Does not affect the sampler heap or acceleration-structure entries. `intValue0` specifies a bool value for the setting. |
1094
1095
| ForceDXLayout | When set forces the compiler to use DirectX-compatible (HLSL register packing) rules when laying out buffer struct fields during code generation. `intValue0` specifies a bool value for the setting. |
1095
1096
| ForceCLayout | When set forces the compiler to use C struct layout rules (natural alignment, no HLSL/GLSL padding) when laying out buffer struct fields during code generation. `intValue0` specifies a bool value for the setting. |
1096
1097
| DenormalModeFp16 | Specifies how 16-bit floating-point denormal values are handled. `intValue0` encodes a value from the `SlangFpDenormalMode` enum. |
Copy file name to clipboardExpand all lines: source/slang/slang-diagnostics.lua
+6Lines changed: 6 additions & 0 deletions
Original file line number
Diff line number
Diff line change
@@ -5547,6 +5547,12 @@ err(
5547
5547
"SPIR-V resource heap stride '~stride:Int' is too small for RaytracingAccelerationStructure descriptor heap entries; expected at least '~minimumStride:Int' bytes."
"'-spirv-resource-heap-stride' and '-spirv-unified-descriptor-heap-stride' cannot be used together; an explicit resource heap stride and the unified maximum stride are mutually exclusive."
0 commit comments