Offset in struct after array with specialization constant size


I recently came across an odd phenomenon when using specialization constants as array sizes.
See this GLSL example for a compute shader:

#version 460

layout(constant_id=0) const uint size=16;

const uint broken_size = size;

layout (set = 0, binding = 0) buffer StorageBuffer
    int arr[broken_size];
    int val;

void main()
    val = 42;

In the SPIR-V code that is generated from this code, the offset of arr and var are specified via the OpMemberDecorate instruction.
Depending on whether broken_size or size are used, the shader compiler (glslc) produces an offset of 4 or 64, respectively.

   OpMemberDecorate %StorageBuffer 1 Offset 4  // when `broken_size` was used.
   OpMemberDecorate %StorageBuffer 1 Offset 64 // when `size` was used.

Initially I thought this is a bug in the shader compiler, which it is, but the issue is much more extensive.
I coud fix the shader compiler, to produce the correct offset for var for the given default value for the specialization constant, but what if I modify the specialization constant at application runtime?
The offset used for var is independent of the specified value for size, and I don’t see a way in which to specify this in SPIRV…
I am missing the ability to express someting like this in SPIR-V:

       %uint = OpTypeInt 32 0
      %int_0 = OpConstant %uint 0
       %size = OpSpecConstant %uint 16
               OpMemberDecorate %StorageBuffer 0 Offset %int_0
               OpMemberDecorate %StorageBuffer 1 Offset %size

Is there some way of working around this issue other than actually modifying the SPIR-V code when modifying the specialization constant value?


Generated SPIR-V code:

; Version: 1.0
; Generator: Google Shaderc over Glslang; 10
; Bound: 17
; Schema: 0
               OpCapability Shader
          %1 = OpExtInstImport "GLSL.std.450"
               OpMemoryModel Logical GLSL450
               OpEntryPoint GLCompute %main "main"
               OpExecutionMode %main LocalSize 1 1 1
               OpSource GLSL 460
               OpSourceExtension "GL_GOOGLE_cpp_style_line_directive"
               OpSourceExtension "GL_GOOGLE_include_directive"
               OpName %main "main"
               OpName %size "size"
               OpName %size "broken_size"
               OpName %StorageBuffer "StorageBuffer"
               OpMemberName %StorageBuffer 0 "arr"
               OpMemberName %StorageBuffer 1 "val"
               OpName %_ ""
               OpDecorate %size SpecId 0
               OpDecorate %_arr_int_size ArrayStride 4
               OpMemberDecorate %StorageBuffer 0 Offset 0
               OpMemberDecorate %StorageBuffer 1 Offset 4
               OpDecorate %StorageBuffer BufferBlock
               OpDecorate %_ DescriptorSet 0
               OpDecorate %_ Binding 0
       %void = OpTypeVoid
          %3 = OpTypeFunction %void
        %int = OpTypeInt 32 1
       %uint = OpTypeInt 32 0
       %size = OpSpecConstant %uint 16
%_arr_int_size = OpTypeArray %int %size
%StorageBuffer = OpTypeStruct %_arr_int_size %int
%_ptr_Uniform_StorageBuffer = OpTypePointer Uniform %StorageBuffer
          %_ = OpVariable %_ptr_Uniform_StorageBuffer Uniform
      %int_1 = OpConstant %int 1
     %int_42 = OpConstant %int 42
%_ptr_Uniform_int = OpTypePointer Uniform %int
       %main = OpFunction %void None %3
          %5 = OpLabel
         %16 = OpAccessChain %_ptr_Uniform_int %_ %int_1
               OpStore %16 %int_42

The Offset decoration uses a literal integer, not a specialization constant. As such, if a struct member has an array size is anything other than a compile-time constant, nothing is permitted to follow that member.

That being said, since you’re already using an SSBO, just use an unbounded array. After all, you have to make it the last member of the block anyway, so there’s no reason to bother with an explicit bounds.

Thanks for the quick response!

So if a specialization constant does not count as a compile-time constant, the GLSL code should not compile, right?

Making the offset decoration except not just literal integers would be the “fix” that I could see to make this case work properly.

The direct use of a storage buffer in the example is rather accidental.
The same applies if I actually use structs.
In this example a runtime array would not work as there can only be one per storage buffer.

#version 460

layout(constant_id=0) const uint size=16;

const uint broken_size = size;

struct Foo
    int arr[broken_size];
    int val;

layout (set = 0, binding = 0) buffer StorageBuffer
    Foo f1;
    Foo f2;

void main()
    f1.val = 13;
    f2.val = 42;

But if such things are not supposed to compile and only do so on accident, that’s also fine with me :slight_smile:

Um, the limitation in question is a SPIR-V limitation, not a GLSL limitation. It compiles to SPIR-V that is invalid.

That’s probably not something you should be fine with.

Do you know if this is mentioned in the SPIR-V specification?
I couldn’t find anything regarding this. Note that the GLSL code compiles the array to OpTypeArray and not to OpTypeRuntimeArray.

Regarding the Offset decoration it says

It must not cause any overlap of the structure’s members

And in the OpTypeArray description it says

Length must come from a constant instruction of an integer-type scalar whose value is at least 1.


Constant Instruction: Either a specialization-constant instruction or a non-specialization constant instruction: Instructions that start “OpConstant” or “OpSpec”.

It’s the part where Offset is a Literal. Literals are compile-time constants. Therefore the offset of any particular member of a struct cannot be based on specialization constants.

Since every member of a block struct (recursively) must have an explicit Offset, and members cannot overlap their storage, if a member comes after a specialization-sized array, then the Offset must be computed based on that specialization constant. But since Offset cannot be based on specialization constants… there can be no member coming after a specialization-sized array.

OK technically, it is not impossible. You simply must set a static Offset for subsequent members such that any specialization size for the array never overlaps with those members.

But that means the buffer always takes up the largest possible size. So you may as well just specify that size to begin with.