SPIR-V Reflection: Descriptor Sets and Locations

Shader reflection is the process of inspecting compiled shader binaries to automatically deduce their resource interface without relying on hardcoded application bindings. When GLSL is compiled into Standard Portable Intermediate Representation (SPIR-V), metadata such as descriptor set indices, binding slots, and input/output locations are retained as explicit decorations attached to internal IDs. By traversing the linear stream of 32-bit words comprising a SPIR-V module, reflection tools map variables to their storage types, parse execution interfaces, and reconstruct the exact pipeline layout required by graphics APIs like Vulkan.

The Anatomy of SPIR-V Instructions

A SPIR-V binary is an ordered stream of 32-bit unsigned integer words. The module begins with a five-word header defining the magic number (0x07230203), the version specification, generator ID, bound (the maximum ID value plus one), and schema information.

Following the header, the binary consists entirely of instructions. Each instruction is packed into words where the high 16 bits define the word count (length of the instruction), and the low 16 bits represent the opcode (OpCode). Reflection parsers read this stream sequentially, storing intermediate values based on numeric IDs assigned by the compiler to types, constants, variables, and decorations.

Extracting Metadata via OpDecorate

GLSL layout qualifiers are converted by compilers (such as glslang) into specific SPIR-V decoration instructions:

  • layout(set = S, binding = B) becomes two separate instructions: OpDecorate %id DescriptorSet S and OpDecorate %id Binding B.
  • layout(location = L) becomes OpDecorate %id Location L.
  • Flat or non-interpolated qualifiers map to decorations like OpDecorate %id Flat.

Reflection engines maintain a lookup table indexed by ID. When the parser encounters an OpDecorate or OpMemberDecorate opcode, it reads the target ID, the decoration enum (such as Binding = 33 or DescriptorSet = 34), and any associated literal parameters. The extracted values are stored in the entry corresponding to that ID for later cross-referencing.

Resolving Variables and Types

Decorations alone do not provide complete context; they must be paired with concrete variable definitions. The parser locates OpVariable instructions to identify distinct resources:

  1. Storage Class Inspection: Every OpVariable declares a Storage Class (such as Uniform, StorageBuffer, Image, UniformConstant, Input, or Output). Variables marked as Input or Output represent stage interface attributes, while Uniform and UniformConstant typically represent descriptor-backed resources.
  2. Type Pointer Resolution: The OpVariable instruction points to a result type defined by an OpTypePointer. The reflection tool unwraps this pointer to find the underlying base type.
  3. Descriptor Classification:
    • If the type resolves to OpTypeImage or OpTypeSampledImage, the resource is a texture or combined image sampler.
    • If the type resolves to OpTypeSampler, it represents a separate sampler state.
    • If the type resolves to OpTypeStruct, the parser determines whether it is an explicit Uniform Buffer Object (UBO) or Shader Storage Buffer Object (SSBO) based on whether the struct is decorated with Block or BufferBlock.

Associating Locations and Sets with Variables

Once types and variables are identified, the reflection engine reconciles them with the collected decoration map:

  • Descriptor Sets and Bindings: The variable's ID is queried against the decoration table. The parser extracts the values stored under DescriptorSet and Binding. If a variable lacks an explicit DescriptorSet decoration, the SPIR-V specification defaults the set index to 0.
  • Pipeline Locations: For Input and Output storage classes, the variable's ID is checked for a Location decoration. In the case of struct-based interfaces or array inputs (such as tessellation or geometry stages), members may instead be decorated with individual Location offsets via OpMemberDecorate.

Handling Active Interface Pruning

A compiled SPIR-V module may contain declarations for resources that are not actively used by every entry point. Reflection tools read the OpEntryPoint instruction, which specifies the execution model (e.g., Vertex, Fragment), the entry point name (e.g., "main"), and an interface list containing the IDs of all global variables accessed by that entry point. Robust reflection engines filter their output against this interface list to ensure only resources actively referenced by the specified shader stage are exposed to the pipeline layout generator.