SPIR-V OpTypeRuntimeArray for GLSL SSBO Unsized Arrays

In GLSL, Shader Storage Buffer Objects (SSBOs) frequently conclude with an unsized array whose dimension is resolved dynamically based on the bound buffer's memory allocation. In SPIR-V, this construct is represented using the OpTypeRuntimeArray type definition embedded as the final member of an OpTypeStruct. This article explains how SPIR-V declares and structures OpTypeRuntimeArray, the decorating rules required for memory layout, and how runtime sizing and indexing are handled via SPIR-V instructions.

Declaring the Runtime Array Type

In GLSL, a storage buffer with an unsized array is written as:

layout(std430, binding = 0) buffer StorageBlock {
    uint count;
    float values[];
};

In SPIR-V, fixed-size arrays rely on OpTypeArray, which pairs an element type with an OpConstant defining its length. Because an SSBO's trailing array has no compile-time size, SPIR-V uses OpTypeRuntimeArray instead.

Its declaration syntax requires only an ID and the element type:

%float = OpTypeFloat 32
%runtime_array = OpTypeRuntimeArray %float

Because the array length is omitted from the type definition, its size remains unbounded until the underlying buffer is mapped at runtime.

Struct Integration and SPIR-V Rules

OpTypeRuntimeArray cannot exist as an independent variable or a function parameter; it must be wrapped inside a structure. SPIR-V and graphics API specifications (such as Vulkan) enforce strict architectural rules on its placement:

  1. Final Member Only: OpTypeRuntimeArray must appear strictly as the last member of an OpTypeStruct. Any struct containing a runtime array cannot contain additional fields after it.
  2. No Nesting: A structure containing an OpTypeRuntimeArray cannot be embedded inside another array or another composite type.
  3. Block Decoration: The enclosing structure must be decorated with Block (or BufferBlock in older SPIR-V/Vulkan environments) to indicate standard buffer-backed storage.

The corresponding SPIR-V structure definition looks like:

%uint = OpTypeInt 32 0
%StorageBlock = OpTypeStruct %uint %runtime_array

Layout Decorations and ArrayStride

Hardware drivers require explicit byte-offset calculations to locate elements within memory. When OpTypeRuntimeArray is declared, it must be assigned an ArrayStride decoration via OpDecorate.

OpDecorate %runtime_array ArrayStride 4

The ArrayStride specifies the distance in bytes from the start of one array element to the start of the next. In an std430 layout for scalar types like a 32-bit float, the stride is 4 bytes. For vector types such as vec3 (12 bytes), std430 alignment rules dictate an ArrayStride of 16 bytes.

Querying Array Bounds with OpArrayLength

When GLSL code calls values.length(), the compiler translates this operation into the SPIR-V instruction OpArrayLength.

OpArrayLength does not inspect static metadata; instead, it determines the element count at runtime based on the actual memory bound to the descriptor:

%result = OpArrayLength %uint %block_pointer 1
  • %block_pointer is an OpVariable pointing to the storage buffer structure.
  • The literal integer 1 specifies the index of the structure member, pointing directly to the field using OpTypeRuntimeArray.

The execution environment calculates the return value dynamically by taking the total byte size of the buffer bound to the descriptor, subtracting the byte offset of the runtime array within the struct, and dividing the remaining bytes by the specified ArrayStride.

Accessing Array Elements

Reading from or writing to elements within an OpTypeRuntimeArray uses standard SPIR-V pointer navigation via OpAccessChain or OpInBoundsAccessChain. The instruction takes the base pointer of the SSBO struct, followed by the member index (e.g., 1), and then the dynamic element index. SPIR-V yields a pointer directly to the requested element, permitting normal OpLoad and OpStore instructions.