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