Skip to content

Reflection API user guide

Hans-Kristian Arntzen edited this page Apr 30, 2018 · 19 revisions

The reflection API of SPIRV-Cross is quite comprehensive, but for more exotic use cases, it's not always obvious how to go about reflecting stuff. There are some idiosyncrasies with SPIR-V which are mirrored in the API to some degree.

Creating the reflection object

The first step you will always take is creating the reflection object.

#include "spirv_cross.hpp"
vector<uint32_t> spirv = load_spirv_from_file();
spirv_cross::Compiler comp(move(spirv)); // const uint32_t *, size_t interface is also available.

Gathering resources

The most common thing to do is to collect which resources are used in a SPIR-V module. Resources are things like images and buffer and the variations of these things. It also contains information about the input and output interfaces of your shaders.

Resources have decorations attached to them. These decorations tell where a resource is bound. The most common ones you want are:

  • DecorationDescriptorSet maps to layout(set = N) in Vulkan GLSL or register(spaceN) in HLSL.
  • DecorationBinding maps to layout(binding = N) in GLSL or : register(bN) in HLSL.
  • DecorationLocation maps to layout(location = N) in GLSL. This is used for in/out variables. The mapping to HLSL is not as obvious.

SPIRV-Cross has a convenient function for gathering all this information from a shader:

ShaderResources res = comp.get_shader_resources();

In this struct you will find arrays of all resource types.

Mapping from struct members to GLSL/HLSL types

ShaderResources member GLSL type HLSL type
uniform_buffers uniform UBO { ... } ubo; cbuffer cbuf { ... }; ConstantBuffer<T> Buf;
storage_buffers buffer SSBO { ... } ssbo; UAV buffers, i.e. RWByteAddressBuffer, ByteAddressBuffer, StructuredBuffer, etc (it's complicated, read on)
stage_inputs in vec4 foo; void main(float4 pos : POSITION) { ... }
stage_outputs out vec4 foo; float4 main() : TEXCOORD0 { ... }
subpass_inputs uniform subpassInput foo; N/A
storage_images uniform image2D foo; uniform imageBuffer buf; RWTexture2D<float4> UAVImage; RWBuffer<float4> UAVBuffer;
sampled_images uniform sampler2D uCombined; N/A, tex2D and friends in DX9 HLSL and older
atomic_counters uniform atomic_uint counter; N/A
push_constant_buffers layout(push_constant) uniform Push { ... } push; N/A
separate_images uniform texture2D uTexture; uniform samplerBuffer Buf; Texture2D<float4> SRV; Buffer<float4> SRVBuffer;
separate_samplers uniform sampler uSamp; SamplerState Samp; SamplerComparisonState CompareSampler;

Querying statically accessed resources

In certain cases, you might want to only check for resources which are statically used by the SPIR-V module. I.e., you don't care about textures or buffers which are never accessed by the shader. Use the following:

auto active = comp.get_active_interface_variables();
ShaderResources = comp.get_shader_resources(active);
comp.set_enabled_interface_variables(move(active));

Inspecting a resource

For each resource, there are usually four things a user will care about:

  • Name Resource::name, as declared with OpName in the SPIR-V module. Do note that this name has zero significance on Vulkan, so normally, you should not care about Name here unless your backend assigns bindings based on name, or for debugging purposes.
  • Descriptor sets and bindings Compiler::get_decoration(Resource::id, spv::DecorationDescriptorSet). These can be used to automatically deduce a vkDescriptorSetLayout (and vkPipelineLayout) for Vulkan if you combine information from all shader stages.
  • The type Resource::base_type_id and Resource::type_id. In most cases, this does not matter, since the type is sorted out for you in ShaderResources already, but in certain scenarios you need more information. How to query and deal with types will be discussed in greater detail later.
  • The ID. This ID represents the resource variable, and can be used with other flags in the API.

Querying decorations

Use Compiler::get_decoration(id, decoration).

ShaderResources res = ...;
for (const Resource &resource : res.sampled_images)
{
    unsigned set = comp.get_decoration(resource.id, spv::DecorationDescriptorSet);
    unsigned binding = comp.get_decoration(resource.id, spv::DecorationBinding);
}

for (const Resource &resource : res.subpass_inputs)
{
    unsigned attachment_index = comp.get_decoration(resource.id, spv::DecorationInputAttachmentIndex);
}

A note on names

In general, the name of an object maps directly to how it is declared in the shader. uniform sampler2D MySampler; has a name MySampler assuming your toolchain emits debug information.

However, block types are more interesting, as they have "two" names, e.g. for GLSL:

  • layout(std140) uniform UBO { ... } ubo;. Which name to use here?
  • layout(std430) buffer SSBO { ... } ssbo;. Which name to use here?
  • layout(push_constant) uniform Push { ... } push;.

and for HLSL:

  • RWStructuredBuffer<T> UAV;
  • cbuffer cbuf { ... };

The name returned by SPIRV-Cross aims to return the most significant name. However, this leads to some weirdness in some cases.

  • GLSL UBO returns the block name, i.e. "UBO" in the above list.
  • GLSL SSBO returns the block name, i.e. "SSBO".
  • GLSL push constant returns the instance name, i.e. "push". The rationale here is that push constants will be translated to uniform Push push; in GLSL, and thus "push" becomes the relevant name for purposes of dealing with glUniform*() interfaces in GL backends.
  • RWStructuredBuffer returns a meaningless name, or misleading name. HLSL as emitted by at least glslang does not really care much for the Block names, the instance name will be UAV ...
  • cbuffer cbuf { ... } returns "cbuf" here.

If you want to see "UAV" for RWStructuredBuffer you need to look at the instance name, and you can use the direct name querying interface for this:

const string &uav_name = compiler.get_name(res.storage_buffers[0].id);

You can ignore Resource::name and just use the get_name() API instead if you know better what kind of naming convention the SPIR-V is using. To query the block name, you would use:

const string &block_name = compiler.get_name(res.storage_buffers[0].base_type_id);

Type system

Eventually, you might want to start inspecting types.

In the Resource struct, you will find base_type_id and type_id, why two? This is one of the larger idiosyncrasies with SPIR-V. The type system is built up hierarchically. The way it works is that you start with a base type and build up new types by adding modifiers to other types, here in pseudo-asm:

%block = TypeStruct %float %int                     <-- base_type_id
MemberName %block 0 "my_float"                      <-- Debug information like names apply to base_type_id
MemberName %block 1 "my_int"
MemberDecorate Offset %block 0 0                    <-- Buffer layout information as well
MemberDecorate Offset %block 0 4

%array-block = TypeArray %block 10                  <-- Information like arrays apply to type_id
%ptr-array-block = TypePointer Uniform %array-block <-- type_id
%variable = Variable %ptr-array-block

For this reason, there are times you will want to use one or the other. You can get an internal representation of a type using:

const SPIRType &base_type = comp.get_type(resource.base_type_id);
const SPIRType &type = comp.get_type(resource.type_id);

SPIRType is defined in spirv_common.hpp.

Querying fundamental types

SPIRType::basetype contains the basic type. Here you can check for things like:

  • Boolean
  • Int
  • UInt
  • Float
  • Struct
  • Image texture2D or Texture2D<T>
  • SampledImage sampler2D
  • Sampler sampler or SamplerState

For vectors and matrices, look at SPIRType::vecsize and SPIRType::columns. 1 column means it's a vector.

Querying array types

SPIRType::array is an array, where each element denotes size of each dimension. For non-array types, this vector will be empty. In SPIR-V, array size can come from specialization constants, so there's an equally large array array_size_literal which tells if each element is a specialization constant ID or not. Except for really odd scenarios, this will always just contain false. Unsized arrays will have 0 as their size.

This is mostly used for querying arrays of resources, e.g.:

uniform sampler2D uSampler[10];
for (const Resource &resource : res.sampled_images)
{
    const SPIRType &type = comp.get_type(resource.type_id); // Notice how we're using type_id here because we need the array information and not decoration information.
    print(type.array.size()); // 1, because it's one dimension.
    print(type.array[0]); // 10
    print(type.array_size_literal[0]); // true
}

Arrays of arrays

E.g. a declaration like int a[4][6], will be declared like:

array = { 6, 4 };
array_size_literal = { true, true };

i.e. backwards.

C-style array declarations are a bit backward in this sense. The way this would be declared in pseudo-SPIR-V is:

%int = OpTypeInt
%int6 = OpTypeArray %int 6
%int4 = OpTypeArray %int6 4

Querying structs

If SPIRType::basetype is SPIRType::Struct, the member types are found in SPIRType::member_types. Use comp.get_type(type.member_types[i]) to dig deeper.`

Querying image types

If SPIRType::basetype is Image or SampledImage, you can peek at SPIRType::image for more information about the image. This struct closely mirrors SPIR-V.

  • type: This is the type ID returned when the image is sampled or read from. E.g. Texture2D<float4>. IIRC, it will always have 4 components.
  • dim: Dimensionality. Here you can check for 1D, 2D, 3D, Cube, Buffer, etc.
  • depth: If the image will be used for comparison sampling. Unfortunately, this information is not very reliable for separate images, only SampledImage.
  • arrayed: sampler2DArray vs sampler2D, etc.
  • ms: sampler2DMS vs sampler2D
  • sampled: 1 means that the image can be sampled from, e.g. sampler*, texture*, (or SRV in HLSL). 2 means image load/store (UAV in HLSL). 0 means nothing. Unfortunately, there is no convenient enum for this in SPIR-V headers.
  • format: For image load/store (UAV) images. This is the format declared in the layout, e.g.: layout(r32f, ...) uniform image2D Image;. HLSL doesn't really have this, so the format will generally be undefined.
  • access: Irrelevant.

Using this information you can distinguish between some confusing types:

  • samplerBuffer vs sampler2D (sampled = 1, dim = DimBuffer vs Dim2D) separate_images
  • imageBuffer vs image2D (sampled = 2, dim = DimBuffer vs Dim2D) storage_images
  • Buffer vs Texture2D (sampled = 1, dim = DimBuffer vs Dim2D) separate_images
  • RWBuffer vs RWTexture2D (sampled = 2, dim = DimBuffer vs Dim2D) storage_images

Read-write vs read-only resources for HLSL

SPIR-V doesn't clearly make a distinction between read-write and read-only types, unlike HLSL. HLSL for example as variants like: RWStructuredBuffer vs StructuredBuffer, RWBuffer vs Buffer. In SPIR-V, these are decorations on the block rather than being part of the type system.

For RWStructuredBuffer vs StructuredBuffer and friends:

Bitset buffer_flags = comp.get_buffer_block_flags(res.storage_buffers[0].id);
if (buffer_flags.get(spv::DecorationNonWritable))
    print("StructuredBuffer");
else
    print("RWStructuredBuffer");

RWBuffer vs Buffer is quite different, because they are actually different types in SPIR-V. RWBuffer is placed in the storage_images vector. The type is OpTypeImage where sampled = 1, and dim = DimBuffer. It is essentially the sibling type of RWTexture2D.

Buffer is an SRV, so it will be found in separate_images. To distinguish Buffer from Texture2D, check for dim = DimBuffer.

Counter buffers in HLSL

Querying size for buffer blocks

Clone this wiki locally