Skip to content

Commit 6bd9a99

Browse files
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>
1 parent 2d6971c commit 6bd9a99

8 files changed

Lines changed: 246 additions & 6 deletions

File tree

docs/command-line-slangc-reference.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -644,6 +644,11 @@ Specify the byte stride for the resource descriptor heap when generating SPIRV w
644644
Specify the byte stride for the sampler descriptor heap when generating SPIRV with spvDescriptorHeapEXT. Defaults to 0, which will use OpConstantSizeOfEXT(OpTypeSampler).
645645

646646

647+
<a id="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+
647652
<a id="separate-debug-info"></a>
648653
### -separate-debug-info
649654
Emit debug data to a separate file, and strip it from the main output file.

docs/user-guide/03-convenience-features.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -706,6 +706,14 @@ from the `OpConstantSizeOfEXT` instruction. The user can override this behavior
706706
stride with the `-spirv-resource-heap-stride` or `-spirv-sampler-heap-stride` compiler options. For
707707
acceleration-structure entries, an explicit resource heap stride must be at least 8 bytes.
708708

709+
Alternatively, the `-spirv-unified-descriptor-heap-stride` option makes every resource descriptor-heap
710+
runtime array use a single shared stride equal to the maximum of the image and buffer descriptor sizes,
711+
so a heap that holds both buffers and images is indexed at the device's unified stride regardless of which
712+
descriptor type a particular shader accesses. This affects only the default `OpConstantSizeOfEXT` path
713+
(used when `-spirv-resource-heap-stride` is 0), so it is mutually exclusive with a non-zero
714+
`-spirv-resource-heap-stride` (combining the two is an error). The sampler heap and acceleration-structure
715+
entries are unaffected.
716+
709717
> **Note on `RaytracingAccelerationStructure`:** When the `spvDescriptorHeapEXT` capability is active and
710718
> a `DescriptorHandle<RaytracingAccelerationStructure>` is dereferenced, Slang loads a 64-bit device address
711719
> from the descriptor heap and converts it to an acceleration structure handle via

docs/user-guide/08-compiling.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1091,6 +1091,7 @@ meanings of their `CompilerOptionValue` encodings.
10911091
| ValidateUniformity | When set will perform [uniformity analysis](a1-05-uniformity.md).|
10921092
| 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. |
10931093
| 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. |
10941095
| 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. |
10951096
| 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. |
10961097
| DenormalModeFp16 | Specifies how 16-bit floating-point denormal values are handled. `intValue0` encodes a value from the `SlangFpDenormalMode` enum. |

include/slang.h

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1203,6 +1203,12 @@ typedef uint32_t SlangSizeT;
12031203
// of `1`, eliminating atomic contention (much faster, and avoids the GPU
12041204
// watchdog timeouts heavy coverage can trigger) at the cost of exact
12051205
// counts. Off by default.
1206+
SPIRVUnifiedDescriptorHeapStride =
1207+
153, // bool: when set, emit each SPIRV resource descriptor-heap runtime array's
1208+
// ArrayStride as the maximum of image and buffer descriptor sizes, so a
1209+
// single heap shared by buffers and images is indexed at the device's unified
1210+
// stride. Opt-in; mutually exclusive with a non-zero
1211+
// `-spirv-resource-heap-stride` (combining the two is an error).
12061212

12071213
// CLI-only query option `-<compiler>-version`: prints the version of the downstream
12081214
// <compiler> Slang would actually load for that pass-through (via

source/slang/slang-diagnostics.lua

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5547,6 +5547,12 @@ err(
55475547
"SPIR-V resource heap stride '~stride:Int' is too small for RaytracingAccelerationStructure descriptor heap entries; expected at least '~minimumStride:Int' bytes."
55485548
)
55495549

5550+
err(
5551+
"spirv-conflicting-descriptor-heap-stride-options",
5552+
57006,
5553+
"'-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."
5554+
)
5555+
55505556
-- GLSL Compatibility (58001-58003)
55515557

55525558
err(

source/slang/slang-emit-spirv.cpp

Lines changed: 110 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -6994,6 +6994,15 @@ struct SPIRVEmitContext : public SourceEmitterBase, public SPIRVEmitSharedContex
69946994
Dictionary<DescriptorRuntimeArrayKey, SpvInst*> m_descriptorHeapRuntimeArrayTypes;
69956995
bool m_didDiagnoseAccelerationStructureDescriptorHeapStrideTooSmall = false;
69966996

6997+
// The single fixed `ArrayStrideIdEXT` stride shared by every resource descriptor-heap runtime
6998+
// array when `-spirv-unified-descriptor-heap-stride` is set. It is the canonical
6999+
// `max(sizeof(image descriptor), sizeof(buffer descriptor))` sequence from the
7000+
// SPV_EXT_descriptor_heap proposal (built by `getUnifiedResourceHeapStride`), not a value
7001+
// derived from the descriptor types a given shader happens to use: the host packs the resource
7002+
// heap at this stride over every resource descriptor category, so a shader that references only
7003+
// one category must still advertise the same stride. Emitted lazily on first use and memoized.
7004+
SpvInst* m_unifiedResourceHeapStride = nullptr;
7005+
69977006

69987007
bool isInstUsedInStage(IRInst* inst, Stage s)
69997008
{
@@ -7165,6 +7174,20 @@ struct SPIRVEmitContext : public SourceEmitterBase, public SPIRVEmitSharedContex
71657174
return type;
71667175
}
71677176

7177+
// Returns whether resource descriptor-heap runtime arrays should advertise a single unified
7178+
// maximum `ArrayStride` (opt-in via `-spirv-unified-descriptor-heap-stride`). This only affects
7179+
// the auto stride path, i.e. when `-spirv-resource-heap-stride` is 0. Combining it with a
7180+
// non-zero `-spirv-resource-heap-stride` is a conflict (the two express contradictory strides),
7181+
// rejected up front during option processing for the CLI (`OptionsParser` in
7182+
// `slang-options.cpp`) and re-checked here by `diagnoseConflictingDescriptorHeapStrideOptions`
7183+
// for the compile-API path; an explicit stride of 0 selects that same auto path, not a
7184+
// conflict.
7185+
bool isUnifiedResourceHeapStrideEnabled()
7186+
{
7187+
return m_targetProgram->getOptionSet().getBoolOption(
7188+
CompilerOptionName::SPIRVUnifiedDescriptorHeapStride);
7189+
}
7190+
71687191
// Selects the configured heap stride for a non-acceleration-structure descriptor element.
71697192
// Keeping this lookup at the call site makes `getDescriptorRuntimeArrayType` consume only a
71707193
// caller-chosen stride; for example, sampler heaps use `SPIRVSamplerHeapStride`, while
@@ -7181,10 +7204,62 @@ struct SPIRVEmitContext : public SourceEmitterBase, public SPIRVEmitSharedContex
71817204
CompilerOptionName::SPIRVResourceHeapStride);
71827205
}
71837206

7207+
// Emits (once, memoized) the fixed unified resource descriptor-heap stride from the
7208+
// SPV_EXT_descriptor_heap proposal: the canonical
7209+
// `max(sizeof(image descriptor), sizeof(buffer descriptor))`, emitted regardless of which
7210+
// descriptor categories the shader uses. `OpConstantSizeOfEXT` is device-defined, so the
7211+
// maximum is symbolic: `max(a, b) = Select(UGreaterThan(a, b), a, b)`.
7212+
SpvInst* getUnifiedResourceHeapStride()
7213+
{
7214+
if (m_unifiedResourceHeapStride)
7215+
return m_unifiedResourceHeapStride;
7216+
7217+
IRBuilder builder(m_irModule);
7218+
builder.setInsertInto(m_irModule->getModuleInst());
7219+
auto uintType = builder.getUIntType();
7220+
auto boolType = builder.getBoolType();
7221+
7222+
auto imageType = emitOpTypeImage(
7223+
nullptr,
7224+
builder.getFloatType(),
7225+
SpvDim2D,
7226+
SpvLiteralInteger::from32(ImageOpConstants::unknownDepthImage),
7227+
SpvLiteralInteger::from32(ImageOpConstants::notArrayed),
7228+
SpvLiteralInteger::from32(ImageOpConstants::notMultisampled),
7229+
SpvLiteralInteger::from32(ImageOpConstants::sampledImage),
7230+
SpvImageFormatUnknown);
7231+
auto bufferType = ensureDescriptorHeapBufferDescriptorType(SpvStorageClassUniform);
7232+
7233+
auto imageSize = emitOpConstantSizeOfEXT(nullptr, uintType, imageType);
7234+
auto bufferSize = emitOpConstantSizeOfEXT(nullptr, uintType, bufferType);
7235+
auto imageIsBigger = emitInst(
7236+
getSection(SpvLogicalSectionID::ConstantsAndTypes),
7237+
nullptr,
7238+
SpvOpSpecConstantOp,
7239+
boolType,
7240+
kResultID,
7241+
SpvOpUGreaterThan,
7242+
imageSize,
7243+
bufferSize);
7244+
m_unifiedResourceHeapStride = emitInst(
7245+
getSection(SpvLogicalSectionID::ConstantsAndTypes),
7246+
nullptr,
7247+
SpvOpSpecConstantOp,
7248+
uintType,
7249+
kResultID,
7250+
SpvOpSelect,
7251+
imageIsBigger,
7252+
imageSize,
7253+
bufferSize);
7254+
return m_unifiedResourceHeapStride;
7255+
}
7256+
71847257
// Builds or reuses the descriptor runtime array for a specific element type and stride.
7185-
// A zero stride emits an `ArrayStrideIdEXT` from `OpConstantSizeOfEXT` for descriptor-typed
7186-
// resources, while acceleration-structure heap entries pass the explicit `uint64` stride
7187-
// computed by `getAccelerationStructureDescriptorHeapStride`.
7258+
// A zero stride emits an `ArrayStrideIdEXT`: in unified mode
7259+
// (`-spirv-unified-descriptor-heap-stride`) every resource heap array shares the single fixed
7260+
// stride from `getUnifiedResourceHeapStride`; otherwise the stride is this element type's own
7261+
// `OpConstantSizeOfEXT`. Acceleration-structure heap entries instead pass the explicit `uint64`
7262+
// stride computed by `getAccelerationStructureDescriptorHeapStride`.
71887263
SpvInst* getDescriptorRuntimeArrayType(SpvInst* descriptorElementType, int arrayStride)
71897264
{
71907265
SLANG_RELEASE_ASSERT(
@@ -7197,8 +7272,17 @@ struct SPIRVEmitContext : public SourceEmitterBase, public SPIRVEmitSharedContex
71977272
if (auto found = m_descriptorHeapRuntimeArrayTypes.tryGetValue(key))
71987273
return *found;
71997274

7275+
// In unified mode, every resource descriptor-heap array (anything other than the separate
7276+
// sampler heap; acceleration-structure heaps never reach the auto path because they pass a
7277+
// non-zero stride) shares the one fixed maximum stride. Build it before the array so its
7278+
// `<id>` precedes the array, satisfying the `ArrayStrideIdEXT` operand-ordering rule.
7279+
const bool isResourceElement = descriptorElementType->opcode != SpvOpTypeSampler;
7280+
const bool useUnifiedStride =
7281+
arrayStride == 0 && isResourceElement && isUnifiedResourceHeapStrideEnabled();
7282+
SpvInst* unifiedStride = useUnifiedStride ? getUnifiedResourceHeapStride() : nullptr;
7283+
72007284
SpvInst* stride = nullptr;
7201-
if (arrayStride == 0)
7285+
if (arrayStride == 0 && !useUnifiedStride)
72027286
{
72037287
IRBuilder builder(m_irModule);
72047288
builder.setInsertInto(m_irModule->getModuleInst());
@@ -7209,13 +7293,14 @@ struct SPIRVEmitContext : public SourceEmitterBase, public SPIRVEmitSharedContex
72097293
auto runtimeArrayType = emitOpTypeRuntimeArray(nullptr, descriptorElementType);
72107294
m_descriptorHeapRuntimeArrayTypes[key] = runtimeArrayType;
72117295

7212-
if (stride)
7296+
SpvInst* strideId = useUnifiedStride ? unifiedStride : stride;
7297+
if (strideId)
72137298
{
72147299
emitOpDecorateArrayStrideIdEXT(
72157300
getSection(SpvLogicalSectionID::Annotations),
72167301
nullptr,
72177302
runtimeArrayType,
7218-
stride);
7303+
strideId);
72197304
}
72207305
else
72217306
{
@@ -7228,6 +7313,23 @@ struct SPIRVEmitContext : public SourceEmitterBase, public SPIRVEmitSharedContex
72287313
return runtimeArrayType;
72297314
}
72307315

7316+
// Diagnoses the `-spirv-resource-heap-stride` / `-spirv-unified-descriptor-heap-stride`
7317+
// conflict for the compile-API path. The CLI rejects this at option-parse time (`OptionsParser`
7318+
// in `slang-options.cpp`), which aborts before emission; but options set directly through the
7319+
// public compile API do not flow through that parser, so this re-checks at emit time and fails
7320+
// loudly rather than silently honoring the explicit stride. The condition matches the parser:
7321+
// unified enabled and a non-zero `SPIRVResourceHeapStride` (an explicit 0 selects the default
7322+
// `OpConstantSizeOfEXT` path the unified option modifies, so it is not a conflict).
7323+
void diagnoseConflictingDescriptorHeapStrideOptions()
7324+
{
7325+
if (isUnifiedResourceHeapStrideEnabled() &&
7326+
m_targetProgram->getOptionSet().getIntOption(
7327+
CompilerOptionName::SPIRVResourceHeapStride) != 0)
7328+
{
7329+
m_sink->diagnose(Diagnostics::SpirvConflictingDescriptorHeapStrideOptions{});
7330+
}
7331+
}
7332+
72317333
int getAccelerationStructureDescriptorHeapStride()
72327334
{
72337335
static const int kAccelerationStructureDescriptorHeapStride = 8;
@@ -11877,6 +11979,8 @@ SlangResult emitSPIRVFromIR(
1187711979
}
1187811980
} while (context.m_forwardDeclaredPointers.getCount() != 0);
1187911981

11982+
context.diagnoseConflictingDescriptorHeapStrideOptions();
11983+
1188011984
// Emit extensions and capabilities for which there are multiple options available.
1188111985
// This is delayed to avoid emitting unnecessary extensions and capabilities if
1188211986
// one of the options is already required by some other op.

source/slang/slang-options.cpp

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -926,6 +926,15 @@ void initCommandOptions(CommandOptions& options)
926926
"-spirv-sampler-heap-stride <stride>",
927927
"Specify the byte stride for the sampler descriptor heap when generating SPIRV with "
928928
"spvDescriptorHeapEXT. Defaults to 0, which will use OpConstantSizeOfEXT(OpTypeSampler)."},
929+
{OptionKind::SPIRVUnifiedDescriptorHeapStride,
930+
"-spirv-unified-descriptor-heap-stride",
931+
nullptr,
932+
"When generating SPIRV with spvDescriptorHeapEXT, emit each resource descriptor-heap "
933+
"runtime array's ArrayStride as the maximum of image and buffer descriptor sizes, so "
934+
"a single heap shared by buffers and images is indexed at the device's unified stride. "
935+
"Only affects the default OpConstantSizeOfEXT path (used when -spirv-resource-heap-stride "
936+
"is 0); mutually exclusive with a non-zero -spirv-resource-heap-stride (combining the two "
937+
"is an error). Does not affect the sampler heap or acceleration-structure entries."},
929938
{OptionKind::EmitSeparateDebug,
930939
"-separate-debug-info",
931940
nullptr,
@@ -2746,6 +2755,7 @@ SlangResult OptionsParser::_parse(int argc, char const* const* argv)
27462755
case OptionKind::TraceFunctionCoverage:
27472756
case OptionKind::TraceBranchCoverage:
27482757
case OptionKind::TraceCoverageBoolean:
2758+
case OptionKind::SPIRVUnifiedDescriptorHeapStride:
27492759
case OptionKind::SkipSPIRVValidation:
27502760
case OptionKind::DisableSpecialization:
27512761
case OptionKind::DisableDynamicDispatch:
@@ -4408,6 +4418,18 @@ SlangResult OptionsParser::_parse(int argc, char const* const* argv)
44084418
}
44094419
}
44104420

4421+
// `-spirv-unified-descriptor-heap-stride` and an explicit non-zero
4422+
// `-spirv-resource-heap-stride` express contradictory strides for the same resource heap,
4423+
// so reject the combination here, as soon as the options are parsed, rather than waiting
4424+
// until SPIR-V emission. An explicit stride of 0 selects the default `OpConstantSizeOfEXT`
4425+
// path that the unified option modifies and is therefore not a conflict.
4426+
if (linkage->m_optionSet.getBoolOption(
4427+
CompilerOptionName::SPIRVUnifiedDescriptorHeapStride) &&
4428+
linkage->m_optionSet.getIntOption(CompilerOptionName::SPIRVResourceHeapStride) != 0)
4429+
{
4430+
m_sink->diagnose(Diagnostics::SpirvConflictingDescriptorHeapStrideOptions{});
4431+
}
4432+
44114433
// TODO: do we need to require that a target must have a profile specified,
44124434
// or will we continue to allow the profile to be inferred from the target?
44134435

0 commit comments

Comments
 (0)