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:
- Final Member Only:
OpTypeRuntimeArraymust appear strictly as the last member of anOpTypeStruct. Any struct containing a runtime array cannot contain additional fields after it. - No Nesting: A structure containing an
OpTypeRuntimeArraycannot be embedded inside another array or another composite type. - Block Decoration: The enclosing structure must be
decorated with
Block(orBufferBlockin 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_pointeris anOpVariablepointing to the storage buffer structure.- The literal integer
1specifies the index of the structure member, pointing directly to the field usingOpTypeRuntimeArray.
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.