From 1218c6b31f2e3fe7c29a137f663e3d2930c49b5f Mon Sep 17 00:00:00 2001 From: "nv-slang-bot[bot]" <274397474+nv-slang-bot[bot]@users.noreply.github.com> Date: Tue, 25 Aug 2026 01:56:59 +0000 Subject: [PATCH] Fix shader-slang/slang#12711: skip void fields in byte-address struct load/store An empty/zero-sized struct field is turned into a void-typed field upstream by legalizeResourceTypes to keep field-count alignment. The byte-address-buffer legalization pass had no skip for it, so it emitted a per-field access of the void field: a Load on HLSL/DXIL (invalid intrinsic) and, for SPIR-V/GLSL, offset / stride where stride(void) == 0, i.e. a divide by zero (E30002). Skip void-typed fields in emitLegalLoad and emitLegalStore before recursing, so no load/store is emitted for them and they are omitted from the reconstructed makeStruct; the later cleanUpVoidType pass drops the matching void field from the struct type, keeping operand and field counts consistent. Mirrors the existing void-field skip in slang-ir-lower-buffer-element-type.cpp and slang-ir-cleanup-void.cpp. --- .../slang/slang-ir-byte-address-legalize.cpp | 21 +++++++ .../bugs/byte-address-load-empty-field.slang | 63 +++++++++++++++++++ 2 files changed, 84 insertions(+) create mode 100644 tests/bugs/byte-address-load-empty-field.slang diff --git a/source/slang/slang-ir-byte-address-legalize.cpp b/source/slang/slang-ir-byte-address-legalize.cpp index 928ea61b481..356c8794902 100644 --- a/source/slang/slang-ir-byte-address-legalize.cpp +++ b/source/slang/slang-ir-byte-address-legalize.cpp @@ -437,6 +437,21 @@ struct ByteAddressBufferLegalizationContext { auto fieldType = field->getFieldType(); + // An empty/zero-sized field is turned into a `void`-typed field upstream by + // `legalizeResourceTypes` (slang-legalize-types.cpp assigns `getVoidType()` to + // keep the field count aligned). Such a field has zero stride, so recursing here + // would emit a `Load` (invalid on HLSL/DXIL) or divide the offset by a zero + // stride (SPIR-V, E30002). It carries no data, so we skip it and omit it from the + // `makeStruct` below; the later `cleanUpVoidType` pass drops the matching void + // field from the struct type, keeping operand and field counts consistent. + // + // The guard is specifically `void`, not "any zero-sized field": `cleanUpVoidType` + // reconciles the `makeStruct` by dropping `void` operands, so only a `void` field + // may be omitted here. An empty-*struct* field is instead handled by the recursion + // below (its zero-field loop emits an empty `makeStruct`) and must not be skipped. + if (as(fieldType)) + continue; + // The relative offset of each field is calculated using // the IR-based layout subsystem, which works with the // "natural" in-memory layout of types. @@ -1430,6 +1445,12 @@ struct ByteAddressBufferLegalizationContext { auto fieldType = field->getFieldType(); + // Skip the `void`-typed field synthesized for an empty/zero-sized field, for the + // same reason as in `emitLegalLoad` above: it carries no data and its zero stride + // would otherwise produce a store of `void` / a divide by zero. + if (as(fieldType)) + continue; + IRIntegerValue fieldOffset; SLANG_RETURN_ON_FAIL(getOffset(m_targetProgram, field, &fieldOffset)); diff --git a/tests/bugs/byte-address-load-empty-field.slang b/tests/bugs/byte-address-load-empty-field.slang new file mode 100644 index 00000000000..594318e5ee9 --- /dev/null +++ b/tests/bugs/byte-address-load-empty-field.slang @@ -0,0 +1,63 @@ +// A void placeholder field (synthesized for an empty/zero-sized struct field) must not generate a +// byte-address buffer access: its zero stride otherwise produces a `Load` (invalid on +// HLSL/DXIL) or a divide by zero on SPIR-V/GLSL. See shader-slang/slang#12711. +// +// The empty field is placed both first (`ItemFirst`) and in the middle (`ItemMid`) so a regression +// is caught regardless of position, and `ItemMid`'s trailing `float c` proves the following real +// field keeps its natural offset (base + 4, i.e. 20U) across the skipped field. The forbidden void +// access is checked by a dedicated `HLSLNOVOID` prefix whose only directive is a `CHECK-NOT`, so it +// scans the entire output (a `CHECK-NOT` interleaved with positive checks would only scan the region +// between them and could miss a first-position regression). + +//TEST:SIMPLE(filecheck=SPIRV): -target spirv -entry main -stage compute +//TEST:SIMPLE(filecheck=HLSL): -target hlsl -profile sm_6_5 -entry main -stage compute +//TEST:SIMPLE(filecheck=HLSLNOVOID): -target hlsl -profile sm_6_5 -entry main -stage compute +//TEST:SIMPLE(filecheck=GLSL): -target glsl -entry main -stage compute + +struct Empty {} + +struct ItemFirst +{ + Empty a; + float b; +} + +struct ItemMid +{ + float a; + Empty b; + float c; +} + +ByteAddressBuffer g_in; +RWByteAddressBuffer g_out; + +[numthreads(1, 1, 1)] +void main() +{ + ItemFirst f = g_in.Load(0); + g_out.Store(0, f); + ItemMid m = g_in.Load(16); + g_out.Store(16, m); +} + +// The divide-by-zero over a void field's zero stride aborts SPIR-V/GLSL codegen before emission, so +// on those targets reaching entry-point emission at all is the guard against a regression. + +// SPIRV: OpEntryPoint + +// GLSL emits shader-storage-buffer element accesses (no `.Load`); reaching `void main()` +// means legalization did not divide by the void field's zero stride. +// GLSL: void main + +// The real `float` fields must still load and store; `ItemMid.c` at offset 20U proves the field +// after the skipped empty field keeps its natural offset. +// HLSL: Load(20U) +// HLSL: Store( + +// No load of `void` may appear anywhere in the HLSL output (whole-output scan; see header note). +// A byte-address store emits `.Store(offset, value)` with no template argument, so there is no +// `Store` form to forbid; the store path is covered by successful compilation plus the +// positive store check above. +// HLSLNOVOID-NOT: Load