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 SandOpDecorate %id Binding B.layout(location = L)becomesOpDecorate %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:
- Storage Class Inspection: Every
OpVariabledeclares a Storage Class (such asUniform,StorageBuffer,Image,UniformConstant,Input, orOutput). Variables marked asInputorOutputrepresent stage interface attributes, whileUniformandUniformConstanttypically represent descriptor-backed resources. - Type Pointer Resolution: The
OpVariableinstruction points to a result type defined by anOpTypePointer. The reflection tool unwraps this pointer to find the underlying base type. - Descriptor Classification:
- If the type resolves to
OpTypeImageorOpTypeSampledImage, 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 withBlockorBufferBlock.
- If the type resolves to
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
DescriptorSetandBinding. If a variable lacks an explicitDescriptorSetdecoration, the SPIR-V specification defaults the set index to0. - Pipeline Locations: For
InputandOutputstorage classes, the variable's ID is checked for aLocationdecoration. In the case of struct-based interfaces or array inputs (such as tessellation or geometry stages), members may instead be decorated with individualLocationoffsets viaOpMemberDecorate.
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.