Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 21 additions & 0 deletions source/slang/slang-ir-byte-address-legalize.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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<void>` (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<IRVoidType>(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.
Expand Down Expand Up @@ -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<IRVoidType>(fieldType))
continue;

IRIntegerValue fieldOffset;
SLANG_RETURN_ON_FAIL(getOffset(m_targetProgram, field, &fieldOffset));

Expand Down
63 changes: 63 additions & 0 deletions tests/bugs/byte-address-load-empty-field.slang
Original file line number Diff line number Diff line change
@@ -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<void>` (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<ItemFirst>(0);
g_out.Store<ItemFirst>(0, f);
ItemMid m = g_in.Load<ItemMid>(16);
g_out.Store<ItemMid>(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<void>`); 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<float
// HLSL: Load<float >(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<void>` form to forbid; the store path is covered by successful compilation plus the
// positive store check above.
// HLSLNOVOID-NOT: Load<void
Loading