yum-mirror/slang

Making it easier to work with shaders

git clone https://git.yummers.dev/yum-mirror/slang

aidanfnvFix broken links in User Guide (#7938)6d399804a

master
58.1 KiB1632 linesraw

layout: user-guide permalink: /user-guide/reflection

Using the Reflection API

This chapter provides an introduction to the Slang reflection API. Our goals in this chapter are to:

  • Demonstrate the recommended types and operations to use for the most common reflection scenarios

  • Provide an underlying mental model for how Slang's reflection information represents the structure of a program

We will describe the structure of a program that traverses all of the parameters of a shader program and prints information (including binding locations) for them. The code shown here is derived from the reflection-api example that is included in the Slang repository. Readers may find it helpful to follow along with that code, to see a more complete picture of what is presented here.

Compiling a Program

The first step in reflecting a shader program is, unsurprisingly, to compile it. Currently reflection information cannot be queried from code compiled via the command-line slangc tool, so applications that want to perform reflection on Slang shader code should use the compilation API to compile a program, and then use getLayout() to extract reflection information:

slang::IComponentType* program = ...;
slang::ProgramLayout* programLayout = program->getLayout(targetIndex);

For more information, see the relevant section of the chapter on compilation.

Types and Variables

We start our discussion of the reflection API with two of the fundamental building blocks used to represent the structure of a program: types and variables.

A key property of GPU shader programming is that the same type may be laid out differently, depending on how it is used. For example, a user-defined struct type Stuff will often be laid out differently if it is used in a ConstantBuffer<Stuff> than in a StructuredBuffer<Stuff>.

Because the same thing can be laid out in multiple ways (even within the same program), the Slang reflection API represents types and variables as distinct things from the layouts applied to them. This section focuses only on the underlying types/variables, while later sections will build on these concepts to show how layouts can be reflected.

Variables

A VariableReflection represents a variable declaration in the input program. Variables include global shader parameters, fields of struct types, and entry-point parameters.

Because a VariableReflection does not include layout information, the main things that can be queried on it are just its name and type:

void printVariable(
    slang::VariableReflection* variable)
{
    const char* name = variable->getName();
    slang::TypeReflection* type = variable->getType();

    print("name: ");    printQuotedString(name);
    print("type: ");    printType(type);
}

Types

A TypeReflection represents some type in the input program. There are various different kinds of types, such as arrays, user-defined struct types, and built-in types like int. The reflection API represents these different cases with the TypeReflection::Kind enumeration.

On its own, a TypeReflection does not include layout information.

We will now start building a function for printing information about types:

void printType(slang::TypeReflection* type)
{
    const char* name = type->getName();
    slang::TypeReflection::Kind kind = type->getKind();

    print("name: ");    printQuotedString(name);
    print("kind: ");    printTypeKind(kind);

    // ...
}

Given what has been presented so far, if we have a Slang variable declaration like the following:

float x;

then applying printVariable() to a VariableReflection for x would yield:

name: "x"
type:
  name: "float"
  kind: Scalar

Additional information can be queried from a TypeReflection, depending on its kind:

void printType(slang::TypeReflection* type)
{
    // ...

    switch(type->getKind())
    {
    default:
        break;

    // ...
    }
}

The following subsections will show examples of what can be queried for various kinds of types.

Scalar Types

Scalar types store an additional enumerant to indicate which of the built-in scalar types is being represented:

case slang::TypeReflection::Kind::Scalar:
    {
        print("scalar type: ");
        printScalarType(type->getScalarType());
    }
    break;

The slang::ScalarType enumeration includes cases for the built-in integer and floating-point types (for example, slang::ScalarType::UInt64 and slang::ScalarType::Float16), as well as the basic bool type (slang::ScalarType::Bool). The void type is also considered a scalar type (slang::ScalarType::Void);

Structure Types

A structure type may have zero or more fields. Each field is represented as a VariableReflection. A TypeReflection allows the fields to be enumerated using getFieldCount() and getFieldByIndex().

case slang::TypeReflection::Kind::Struct:
    {
        print("fields:");
        int fieldCount = type->getFieldCount();
        for (int f = 0; f < fieldCount; f++)
        {
            print("- ");
            slang::VariableReflection* field =
                type->getFieldByIndex(f);
            printVariable(field);
        }
    }
    break;

For the purposes of the reflection API, the fields of a struct type are its non-static members (both public and non-public).

Given Slang code like the following:

struct S
{
    int a;
    float b;
}

Reflection on type S would yield:

name: "S"
kind: Struct
fields:
  - name: "a"
    type:
      name: "int"
      kind: Scalar
  - name: "b"
    type:
      name: "float"
      kind: Scalar

Arrays

An array type like int[3] is defined by the number and type of elements in the array, which can be queried with getElementCount() and getElementType, respectively:

case slang::TypeReflection::Kind::Array:
    {
        print("element count: ");
        printPossiblyUnbounded(type->getElementCount());

        print("element type: ");
        printType(type->getElementType());
    }
    break;

Some array types, like Stuff[], have unbounded size. The Slang reflection API represents this case using the maximum value possible for the size_t result from getElementCount():

void printPossiblyUnbounded(size_t value)
{
    if (value == ~size_t(0))
    {
        printf("unbounded");
    }
    else
    {
        printf("%u", unsigned(value));
    }
}

Vectors

Vector types like int3 are similar to arrays, in that they are defined by their element type and number of elements:

case slang::TypeReflection::Kind::Vector:
    {
        print("element count: ");
        printCount(type->getElementCount());

        print("element type: ");
        printType(type->getElementType());
    }
    break;

Matrices

Matrix types like float3x4 are defined by the number of rows, the number of columns, and the element type:

case slang::TypeReflection::Kind::Matrix:
    {
        print("row count: ");
        printCount(type->getRowCount());

        print("column count: ");
        printCount(type->getColumnCount());

        print("element type: ");
        printType(type->getElementType());
    }
    break;

Resources

There are a wide range of resource types, including simple cases like TextureCube and StructuredBuffer<int>, as well as quite complicated ones like RasterizerOrderedTexture2DArray<int4> and AppendStructuredBuffer<Stuff>.

The Slang reflection API breaks down the properties of a resource type into its shape, access, and result type:

case slang::TypeReflection::Kind::Resource:
    {
        key("shape");
        printResourceShape(type->getResourceShape());

        key("access");
        printResourceAccess(type->getResourceAccess());

        key("result type");
        printType(type->getResourceResultType());
    }
    break;

The result type of a resource is simply whatever would be returned by a basic read operation on that resource. For resource types in Slang code, the result type is typically written as a generic type parameter after the type name. For a StructuredBuffer<Thing> the result type is Thing, while for a Texture2D<int3> it is int3. A texture type like Texture2D that does not give an explicit result type has a default result type of float4.

The access of a resource (SlangResourceAccess) represents how the elements of the resource may be accessed by shader code. For Slang resource types, access is typically encoded as a prefix on the type name. For example, an unprefixed Texture2D has read-only access (SLANG_RESOURCE_ACCESS_READ), while a RWTexture2D has read-write access (SLANG_RESOURCE_ACCESS_READ_WRITE).

The shape of a resource (SlangResourceShape) represents the conceptual rank/dimensionality of the resource and how it is indexed. For Slang resource type names, everything after the access prefix is typically part of the shape.

A resource shape breaks down into a base shape along with a few possible suffixes like array-ness:

void printResourceShape(SlangResourceShape shape)
{
    print("base shape:");
    switch(shape & SLANG_BASE_SHAPE_MASK)
    {
    case SLANG_TEXTURE1D: printf("TEXTURE1D"); break;
    case SLANG_TEXTURE2D: printf("TEXTURE2D"); break;
    // ...
    }

    if(shape & SLANG_TEXTURE_ARRAY_FLAG) printf("ARRAY");
    if(shape & SLANG_TEXTURE_MULTISAMPLE_FLAG) printf("MULTISAMPLE");
    // ...
}

Single-Element Containers

Types like ConstantBuffer<T> and ParameterBlock<T> represent a grouping of parameter data, and behave like an array or structured buffer with only a single element:

case slang::TypeReflection::Kind::ConstantBuffer:
case slang::TypeReflection::Kind::ParameterBlock:
case slang::TypeReflection::Kind::TextureBuffer:
case slang::TypeReflection::Kind::ShaderStorageBuffer:
    {
        key("element type");
        printType(type->getElementType());
    }
    break;

Layout for Types and Variables

The Slang reflection API provides VariableLayoutReflection and TypeLayoutReflection to represent a layout of a given variable or type. As discussed earlier, the same type might have multiple different layouts used for it in the same program.

Layout Units

A key challenge that the Slang reflection API has to address is how to represent the offset of a variable (or struct field, etc.) or the size of a type when struct types are allowed to mix various kinds of data together.

For example, consider the following Slang code:

struct Material
{
    Texture2D albedoMap;
    SamplerState sampler;
    float2 uvScale;
    float2 uvBias;
}
struct Uniforms
{
    TextureCube environmentMap;
    SamplerState environmentSampler;
    float3 sunLightDirection;
    float3 sunLightIntensity;
    Material material;
    // ...
}
ParameterBlock<Uniforms> uniforms;

When laid out in the given parameter block, what is the offset of the field Uniforms::material? What is the size of the Material type?

The key insight is that layout is multi-dimensional: the same type can have a size in multiple distinct units. For example, when compiling the above code for D3D12/DXIL, the answer is that the Uniforms::material has an offset of one t register, one s register, and 32 bytes. Similarly, the size of the Material type is one t register, one s register, and 16 bytes.

We refer to these distinct units of measure used in layouts (including bytes, t registers, and s registers) as layout units. Layout units are represented in the Slang reflection API with the slang::ParameterCategory enumeration. (We will avoid the term "parameter category," despite that being the name currently exposed in the public API; that name has turned out to be a less-than-ideal choice).

Variable Layouts

A VariableLayoutReflection represents a layout computed for a given variable (itself a VariableReflection). The underlying variable can be accessed with getVariable(), but the variable layout also provides accessors for the most important properties.

A variable layout stores the offsets of that variable (possibly in multiple layout units), and also a type layout for the data stored in the variable.

void printVarLayout(slang::VariableLayoutReflection* varLayout)
{
    print("name"); printQuotedString(varLayout->getName());

    printRelativeOffsets(varLayout);

    key("type layout");
    printTypeLayout(varLayout->getTypeLayout());
}

Offsets

The offsets stored by a VariableLayoutReflection are always relative to the enclosing struct type, scope, or other context that surrounds the variable.

The VariableLayoutReflection::getOffset method can be used to query the relative offset of a variable for any given layout unit:

void printOffset(
    slang::VariableLayoutReflection* varLayout,
    slang::ParameterCategory layoutUnit)
{
    size_t offset = varLayout->getOffset(layoutUnit);

    print("value: "); print(offset);
    print("unit: "); printLayoutUnit(layoutUnit);

    // ...
}

If an application knows what unit(s) it expects a variable to be laid out in, it can directly query those. However, in a case like our systematic traversal of all shader parameters, it is not always possible to know what units a given variable uses.

The Slang reflection API can be used to query layout units used by a given variable layout with getCategoryCount() and getCategoryByIndex():

void printRelativeOffsets(
    slang::VariableLayoutReflection* varLayout)
{
    print("relative offset: ");
    int usedLayoutUnitCount = varLayout->getCategoryCount();
    for (int i = 0; i < usedLayoutUnitCount; ++i)
    {
        auto layoutUnit = varLayout->getCategoryByIndex(i);
        printOffset(varLayout, layoutUnit);
    }
}

Spaces / Sets

For certain target platforms and layout units, the offset of a variable for that unit might include an additional dimension that represents a Vulkan/SPIR-V descriptor set, D3D12/DXIL register space, or a WebGPU/WGSL binding group. In this chapter, we will uniformly refer to all of these concepts as spaces.

The relative space offset of a variable layout for a given layout unit can be queried with getBindingSpace():

void printOffset(
    slang::VariableLayoutReflection* varLayout,
    slang::ParameterCategory layoutUnit)
{
    // ...

    size_t spaceOffset = varLayout->getBindingSpace(layoutUnit);

    switch(layoutUnit)
    {
    default:
        break;

    case slang::ParameterCategory::ConstantBuffer:
    case slang::ParameterCategory::ShaderResource:
    case slang::ParameterCategory::UnorderedAccess:
    case slang::ParameterCategory::SamplerState:
    case slang::ParameterCategory::DescriptorTableSlot:
        print("space: "); print(spaceOffset);    
    }
}

The code above only prints the space offset for the layout units where a space is semantically possible and meaningful.

Type Layouts

A TypeLayoutReflection represents a layout computed for a type. The underlying type that layout was computed for can be accessed using TypeLayoutReflection::getType(), but accessors are provided so that the most common properties of types can be queried on type layouts.

The main thing that a type layout stores is the size of the type:

void printTypeLayout(slang::TypeLayoutReflection* typeLayout)
{
    print("name: "); printQuotedString(typeLayout->getName());
    print("kind: "); printTypeKind(typeLayout->getKind());

    printSizes(typeLayout);

    // ...
}

Size

Similarly to variable layouts, the size of a type layout can be queried given a chosen layout unit:

void printSize(
    slang::TypeLayoutReflection* typeLayout,
    slang::ParameterCategory layoutUnit)
{
    size_t size = typeLayout->getSize(layoutUnit);

    key("value"); printPossiblyUnbounded(size);
    key("unit"); writeLayoutUnit(layoutUnit);
}

Note that the size of a type may be unbounded for a particular layout unit; this case is encoded just like the unbounded case for the element count of an array type (~size_t(0)).

The layout units used by a particular type layout can be iterated over using getCategoryCount() and getCategoryByIndex():

void printSizes(slang::TypeLayoutReflection* typeLayout)
{
    print("size: ");
    int usedLayoutUnitCount = typeLayout->getCategoryCount();
    for (int i = 0; i < usedLayoutUnitCount; ++i)
    {
        auto layoutUnit = typeLayout->getCategoryByIndex(i);
        print("- "); printSize(typeLayout, layoutUnit);
    }

    // ...
}

Alignment and Stride

For any given layout unit, a type layout can also reflect the alignment of the type for that unit with TypeLayoutReflection::getAlignment(). Alignment is typically only interesting when the layout unit is bytes (slang::ParameterCategory::Uniform).

Note that, unlike in C/C++, a type layout in Slang may have a size that is not a multiple of its alignment. The stride of a type layout (for a given layout unit) is its size rounded up to its alignment, and is used as the distance between consecutive elements in arrays. The stride of a type layout can be queried for any chosen layout unit with TypeLayoutReflection::getStride().

Note that all of the TypeLayoutReflection methods getSize(), getAlignment(), and getStride() default to returning information in bytes, if a layout unit is not specified. The same is true of the VariableLayoutReflection::getOffset() method.

The alignment and stride of a type layout can be reflected when it is relevant with code like:

void printTypeLayout(slang::TypeLayoutReflection* typeLayout)
{
    // ...

    if(typeLayout->getSize() != 0)
    {
        print("alignment in bytes: ");
        print(typeLayout->getAlignment());

        print("stride in bytes: ");
        print(typeLayout->getStride());
    }

    // ...
}

Kind-Specific Information

Just as with the underlying types, a type layout may store additional information depending on the kind of type:

void printTypeLayout(slang::TypeLayoutReflection* typeLayout)
{
    // ...

    switch(typeLayout->getKind())
    {
    default:
        break;
    
        // ...
    }
}

The following subsections will cover the important kinds to handle when reflecting type layouts.

Structure Type Layouts

A type layout for a struct type provides access to the fields of the struct, with each field represented as a variable layout:

case slang::TypeReflection::Kind::Struct:
    {
        print("fields: ");

        int fieldCount = typeLayout->getFieldCount();
        for (int f = 0; f < fieldCount; f++)
        {
            auto field = typeLayout->getFieldByIndex(f);
            printVarLayout(field);
        }
    }
    break;

The offset information stored on the type layout for each field will always be relative to the start of the struct type.

Array Type Layouts

Array type layouts store a layout for the element type of the array, which can be accessed with getElementTypeLayout():

case slang::TypeReflection::Kind::Array:
    {
        print("element count: ");
        printPossiblyUnbounded(typeLayout->getElementCount());

        print("element type layout: ");
        printTypeLayout(typeLayout->getElementTypeLayout());
    }
    break;

Matrix Type Layouts

A layout for a matrix type stores a matrix layout mode (SlangMatrixLayoutMode) to record whether the type was laid out in row-major or column-major layout:

case slang::TypeReflection::Kind::Matrix:
    {
        // ...

        print("matrix layout mode: ");
        printMatrixLayoutMode(typeLayout->getMatrixLayoutMode());
    }
    break;

Note that the concepts of "row" and "column" as employed by Slang are the opposite of how Vulkan, SPIR-V, GLSL, and OpenGL use those terms. When Slang reflects a matrix as using row-major layout, the corresponding matrix in generated SPIR-V will have a ColMajor decoration. For an explanation of why these conventions differ, please see the relevant appendix.

Single-Element Containers

Constant buffers, parameter blocks, and other types representing grouping of parameters are the most subtle cases to handle for reflection. The Slang reflection API aspires to provide complete and accurate information for these cases, but understanding why the provided data is what it is requires an appropriate mental model.

Simple Cases

In simple cases, a constant buffer has only ordinary data in it (things where the only used layout unit is bytes):

struct DirectionalLight
{
    float3 direction;
    float3 intensity;
}
ConstantBuffer<DirectionalLight> light;

When this case is laid out for D3D12, the DirectionalLight type will consume 28 bytes, but the light parameter will instead consume one b register. We thus see that the ConstantBuffer<> type effectively "hides" the number of bytes used by its element.

Similarly, when a parameter block only has opaque types in it:

struct Material
{
    Texture2D albedoMap;
    Texture2D glossMap;
    SamplerState sampler;
}
ParameterBlock<Material> material;

When this is laid out for Vulkan, the Material type will consume 3 bindings, but the material parameter will instead consume one space. A ParameterBLock<> type hides the bindings/registers/slots used by its element.

When Things Leak

If the element type of a constant buffer includes any data that isn't just measured in bytes, that usage will "leak" into the size of the constant buffer. For example:

struct ViewParams
{
    float3 cameraPos;
    float3 cameraDir;
    TextureCube envMap;
}
ConstantBuffer<ViewParams> view;

If this example is laid out for D3D12, the ViewParams type will have a size of 28 bytes (according to D3D constant buffer layout rules) and one t register. The size of the view parameter will be one b register and one t register. The ConstantBuffer<> type can hide the bytes used by ViewParams, but the used t register leaks out and becomes part of the size of view.

If the same example is laid out for Vulkan, the ViewParams type will have a size of 28 bytes (according to std140 layout rules) and one binding. The size of the view parameter will be two bindings.

An important question a user might have in the Vulkan case, is whether the binding for view comes before that for view.envMap, or the other way around. The answer is that the Slang compiler always lays out the "container" part of a parameter like view (the constant buffer) before the element, but a client of the reflection API shouldn't have to know such things to understand the information that gets reflected.

Note that in the Vulkan case, the offset of the envMap field within ViewParams is zero bindings, but the offset of view.envMap field relative to view is one binding. Computing the cumulative offset of view.envMap requires more information than just that available on the variable layouts for view and view.envMap.

Similar cases of usage leaking can occur for parameter blocks, when one parameter block is nested within another.

A ConstantBuffer<> Without a Constant Buffer

While it is an uncommon case, it is possible to use a ConstantBuffer<> with an element type that contains no ordinary data (nothing with a layout unit of bytes):

struct Material
{
    Texture2D albedoMap;
    Texture2D glossMap;
    SamplerState sampler;
}
ConstantBuffer<Material> material;

If this case is compiled for Vulkan, the material parameter will consume 3 bindings, but none of those will be for a constant buffer. In this case, unlike in the preceding example with view.envMap, the offset of material.albedoMap relative to material will be zero bindings.

Implicitly-Allocated Constant Buffers

A common use case for parameter blocks is to wrap up all of the parameters of a shader, or of some subsystem. In such cases, there are likely to be both ordinary-type and opaque-type fields:

struct PointLight
{
    float3 position;
    float3 intensity;
}
struct LightingEnvironment
{
    TextureCube envMap;
    PointLight pointLights[10];
}
ParameterBlock<LightingEnvironment> lightEnv;

If this example is compiled for Vulkan, the LightingEnvironment type uses 316 bytes and one binding (ParameterCategory::DescriptorTableSlot), while lightEnv uses one descriptor set (ParameterCategory::SubElementRegisterSpace).

What is not clear in the above description, however, is that because LightingEnvironment uses ordinary bytes, the Slang compiler will have to implicitly allocate a binding for a constant buffer to hold those bytes. Conceptually, the layout is similar to what would be produced for ParameterBlock<ConstantBuffer<LightingEnvironment>>.

Furthermore, that constant buffer binding will be the first binding within the descriptor set for lightEnv, so that the cumulative binding offset for lightEnv.envMap will be one binding (even though LightingEnvironment::envMap has a relative offset of zero bindings).

Container and Element

In order to properly handle all of the nuances described here, the layout for a type like ConstantBuffer<Thing> or ParameterBlock<Thing> includes both layout information for the element of the container (a Thing) as well as layout information for the container itself. Furthermore, the layout information for both the element and container need to support storing offset information (not just size), relative to the overall ConstantBuffer<> or ParameterBlock<>.

The breakdown is thus:

  • The size information for the complete container type layout reflects whatever usage "leaks" out, such that it would need to be accounted for when further aggregating the overall type.

  • Information about the allocated container is stored as a variable layout, queried with getContainerVarLayout()

    • The type layout for that variable layout shows what was allocated to represent the container itself, including any implicitly-allocated constant buffer

    • The offsets of that variable layout show where the container is situated relative to the overall type. With the current layout strategies used by the Slang compiler, all of these offsets will be zero.

  • Information about the element is stored as a variable layout, queried with getElementVarLayout()

    • The type layout of that variable layout shows how the element type is laid out inside container.

    • The offsets on that variable layout show where the element is situated relative to the overall type. These offsets will be non-zero in cases where there is some layout unit used by both the element type and the container itself.

Given this understanding, we can now look at the logic to reflect a type layout for a constant buffer, parameter block, or similar type.

case slang::TypeReflection::Kind::ConstantBuffer:
case slang::TypeReflection::Kind::ParameterBlock:
case slang::TypeReflection::Kind::TextureBuffer:
case slang::TypeReflection::Kind::ShaderStorageBuffer:
    {
        print("container: ");
        printOffsets(typeLayout->getContainerVarLayout());
    
        auto elementVarLayout = typeLayout->getElementVarLayout();
        print("element: ");
        printOffsets(elementVarLayout);

        print("type layout: ");
        printTypeLayout(
            elementVarLayout->getTypeLayout();
    }
    break;

Note that the application logic here does not simply make use of printVarLayout() on the results of both getContainerVarLayout() and getElementVarLayout(), even though it technically could. While these sub-parts of the overall type layout are each represented as a VariableLayoutReflection, many of the properties of those variable layouts are uninteresting or null; they primarily exist to convey offset information.

Example

Given input code like the following:

struct Material
{
    Texture2D albedoMap;
    SamplerState sampler;
    float2 uvScale;
    float2 uvBias;
}

struct FrameParams
{
    ConstantBuffer<Material> material;

    float3 cameraPos;
    float3 cameraDir;

    TextureCube envMap;
    float3 sunLightDir;
    float3 sunLightIntensity;

    Texture2D shadowMap;
    SamplerComparisonState shadowMapSampler;
}

ParameterBlock<FrameParams> params;

We will look at the kind of output our example application prints for params when compiling for Vulkan. The basic information for the variable and its type layout looks like:

- name: "params"
  offset:
    relative:
    - value: 1
      unit: SubElementRegisterSpace # register spaces / descriptor sets
  type layout:
    name: "ParameterBlock"
    kind: ParameterBlock
    size:
      - value: 1
        unit: SubElementRegisterSpace # register spaces / descriptor sets

As we would expect, the size of the parameter block is one register space (aka Vulkan descriptor set). In this case, the Slang compiler has assigned params to have a space offset of 1 (set=1 in GLSL terms).

The offset information for the container part of params is the following:

container:
offset:
  relative:
    - value: 0
      unit: DescriptorTableSlot # bindings
      space: 0
    - value: 0
      unit: SubElementRegisterSpace # register spaces / descriptor sets

We can see from this information that the ParameterBlock<> container had two things allocated to it: a descriptor set (ParameterCategory::SubElementRegisterSpace), and a binding within that descriptor set (ParameterCategory::DescriptorTableSlot) for the automatically-introduced constant buffer. That automatically-introduced buffer has an offset of 0 bindings from the start of the descriptor set.

The layout for the element part of the parameter block is as follows:

element:
  offset:
    relative:
      - value: 1
        unit: DescriptorTableSlot # bindings
        space: 0
      - value: 0
        unit: Uniform # bytes
  type layout:
    name: "FrameParams"
    kind: Struct
    size:
      - value: 6
        unit: DescriptorTableSlot # bindings
      - value: 64
        unit: Uniform # bytes
    alignment in bytes: 16
    stride in bytes: 64
    fields:                  
      - name: "material"
        offset:
          relative:
            - value: 0
              unit: DescriptorTableSlot # bindings
              space: 0
      ...

We see here that the type layout for the element is as expected of a layout for the FrameParams type. In particular, note how the material field has a relative offset of zero bindings from the start of the struct, as is expected for the first field. In order to account for the automatically-introduced constant buffer that is used by the container part of the layout, the element variable layout includes a relative offset of one binding (ParameterCategory::DescriptorTableSlot).

In a later section we will discuss how to easily sum up the various relative offsets shown in an example like this, when an application wants to compute a cumulative offset for a field like params.material.sampler.

Pitfalls to Avoid

It is a common mistake for users to apply getElementTypeLayout() on a single-element container, instead of using getElementVarLayout() as we advise here. The implementation of the reflection API makes an effort to ensure that the type layout returned by getElementTypeLayout() automatically bakes in the additional offsets that are needed, but the results can still be unintuitive.

Programs and Scopes

So far, our presentation has largely been bottom-up: we have shown how to recursively perform reflection on types, variables, and their layouts, but we have not yet shown how how to get this recursive traversal started. We will now proceed top-down for a bit, and look at how to reflect the top-level parameters of a program.

A ProgramLayout is typically obtained using IComponentType::getLayout() after compiling and linking a Slang program. A program layout primarily comprises the global scope, and zero or more entry points:

void printProgramLayout(
    slang::ProgramLayout* programLayout)
{
    print("global scope: ");
    printScope(programLayout->getGlobalParamsVarLayout());

    print("entry points: ");
    int entryPointCount = programLayout->getEntryPointCount();
    for (int i = 0; i < entryPointCount; ++i)
    {
        print("- ");
        printEntryPointLayout(
            programLayout->getEntryPointByIndex(i));
    }
}

The global scope and entry points are each an example of a scope where top-level shader parameters can be declared. Scopes are represented in the reflection API using VariableLayoutReflections. We will now discuss the details of reflection for scopes, starting with the global scope as an example.

Global Scope

In order to understand how the Slang reflection API exposes the global scope, it is valuable to think of the steps (some of them optional) that the Slang compiler applies to global-scope shader parameter declarations as part of compilation.

Parameters are Grouped Into a Structure

If a shader program declares global-scope parameters like the following:

Texture2D diffuseMap;
TextureCube envMap;
SamplerState sampler;

The Slang compiler will conceptually group all of those distinct global-scope parameter declarations into a struct type and then have only a single global-scope parameter of that type:

struct Globals
{
    Texture2D diffuseMap;
    TextureCube envMap;
    SamplerState sampler;
}
uniform Globals globals;

In this simple kind of case, the scope will be reflected as a variable layout with a struct type layout, with one field for each parameter declared in that scope:

void printScope(
    slang::VariableLayoutReflection*    scopeVarLayout)
{
    auto scopeTypeLayout = scopeVarLayout->getTypeLayout();
    switch (scopeTypeLayout->getKind())
    {
    case slang::TypeReflection::Kind::Struct:
        {
            print("parameters: ");

            int paramCount = scopeTypeLayout->getFieldCount();
            for (int i = 0; i < paramCount; i++)
            {
                print("- ");

                auto param = scopeTypeLayout->getFieldByIndex(i);
                printVarLayout(param, &scopeOffsets);
            }
        }
        break;

        // ...
    }
}

Wrapped in a Constant Buffer If Needed

In existing shader code that was originally authored for older APIs (such as D3D9) it is common to find a mixture of opaque and ordinary types appearing as global-scope shader parameters:

Texture2D diffuseMap;
TextureCube envMap;
SamplerState sampler;

uniform float3 cameraPos;
uniform float3 cameraDir;

In these cases, when the Slang compiler groups the parameters into a single struct:

struct Globals
{
    Texture2D diffuseMap;
    TextureCube envMap;
    SamplerState sampler;

    float3 cameraPos;
    float3 cameraDir;
}

it finds that the resulting struct consumes a non-zero number of bytes and, for most compilation targets, it will automatically wrap that structure in a ConstantBuffer<> before declaring the single shader parameter that represents the global scope:

ConstantBuffer<Globals> globals

This case shows up in the Slang reflection API as the scope having a type layout with the constant-buffer kind:

case slang::TypeReflection::Kind::ConstantBuffer:
    print("automatically-introduced constant buffer: ");

    printOffsets(scopeTypeLayout->getContainerVarLayout());

    printScope(scopeTypeLayout->getElementVarLayout());
    break;

In this case, the container variable layout reflects the relative offsets for where the automatically-introduced constant buffer is bound, and the element variable layout reflects the global scope parameters that were wrapped in this way.

Wrapped in a Parameter Block If Needed

For targets like D3D12/DXIL, Vulkan/SPIR-V, and WebGPU/WGSL, most shader parameters must be bound via the target-specific grouping mechanism (descriptor tables, descriptor sets, or binding groups, respectively). If the Slang compiler is compiling for such a target and detects that there are global-scope parameters that do not specify an explicit space, then it will conceptually wrap the global-scope declarations in a ParameterBlock<> that provides a default space.

For example, if compiling this code to Vulkan:

Texture2D diffuseMap;
[[vk::binding(1,0)]] TextureCube envMap;
SamplerState sampler;

the Slang compiler will detect that envMap is explicitly bound to binding 1 in space (aka descriptor set) 0, and that neither diffuseMap nor sampler has been explicitly bound. Both of the unbound parameters need to be passed inside of some space, so the compiler will allocate space 1 for that purpose (as space 0 was already claimed by explicit bindings). In simplistic terms, the compiler will behave as if the global-scope parameters are wrapped up in a struct and then further wrapped up into a ParameterBlock<>.

This case shows up in the Slang reflection API as the scope having a type layout with the parameter-block kind:

case slang::TypeReflection::Kind::ParameterBlock:
    print("automatically-introduced parameter block: ");

    printOffsets(scopeTypeLayout->getContainerVarLayout());

    printScope(scopeTypeLayout->getElementVarLayout());
    break;

In cases where the parameters in a scope require both a constant buffer and a parameter block to be automatically introduced, the scope is reflected as if things were wrapped with ParameterBlock<...> and not ParameterBlock<ConstantBuffer<...>>. That is, the binding information for the implicit constant buffer will be found as part of the container variable layout for the parameter block.

Pitfalls to Avoid

The ProgramLayout type has the appealingly-named getParameterCount and getParameterByIndex() methods, which seem to be the obvious way to navigate the global-scope parameters of a shader. However, we recommend against using these functions in applications that want to be able to systematically and robustly reflect any possible input shader code.

While the reflection API implementation makes an effort to ensure that the information returned by getParameterByIndex() is not incorrect, it is very difficult when using those functions to account for how global-scope parameters might have been grouped into an automatically-introduced constant buffer or parameter block. The getGlobalConstantBufferBinding() and getGlobalConstantBufferSize() methods can be used in some scenarios, but aren't the best way to get the relevant information.

While it would only matter in corner cases, we still recommend that applications use getGlobalParamsVarLayout() instead of getGlobalParamsTypeLayout(), to account for cases where the global-scope might have offsets applied to it (and also to handle the global scope and entry-point scopes more uniformly).

Entry Points

An EntryPointReflection provides information on an entry point. This includes the stage that the entry point was compiled for:

void printEntryPointLayout(slang::EntryPointReflection* entryPointLayout)
{
    print("stage: "); printStage(entryPointLayout->getStage());

    // ...
}

Entry Point Parameters

An entry point acts as a scope for top-level shader parameters, much like the global scope. Entry-point parameters are grouped into a struct, and then automatically wrapped in a constant buffer or parameter block if needed. The main additional consideration, compared to the global scope, is that an entry-point function may also declare a result type. When present, the function result acts more or less as an additional out parameter.

The parameter scope and result of an entry point can be reflected with logic like:

void printEntryPointLayout(slang::EntryPointReflection* entryPointLayout)
{
    // ...
    printScope(entryPointLayout->getVarLayout());

    auto resultVarLayout = entryPointLayout->getResultVarLayout();
    if (resultVarLayout->getTypeLayout()->getKind() != slang::TypeReflection::Kind::None)
    {
        key("result"); printVarLayout(resultVarLayout);
    }
}
Pitfalls to Avoid

Similarly to the case for the global scope, we recommend against using the getParameterCount() and getParameterByIndex() methods on EntryPointReflection, since they make it harder to handle cases where the entry-point scope might have been allocated as a constant buffer (although the hasDefaultConstantBuffer() method is provided to try to support older applications that still use getParameterByIndex()). Applications are also recommended to use EntryPointReflection::getVarLayout() instead of ::getTypeLayout(), to more properly reflect the way that offsets are computed and applied to the parameters of an entry point.

Stage-Specific Information

Depending on the stage that an entry point was compiled for, it may provide additional information that an application can query:

void printEntryPointLayout(slang::EntryPointReflection* entryPointLayout)
{
    // ...
    switch (entryPointLayout->getStage())
    {
    default:
        break;

        // ...
    }
    // ...
}

For example, compute entry points store the thread-group dimensions:

case SLANG_STAGE_COMPUTE:
    {
        SlangUInt sizes[3];
        entryPointLayout->getComputeThreadGroupSize(3, sizes);

        print("thread group size: ");
        print("x: "); print(sizes[0]);
        print("y: "); print(sizes[1]);
        print("z: "); print(sizes[2]);
    }
    break;

Varying Parameters

So far we have primarily been talking about the uniform shader parameters of a program: those that can be passed in from application code to shader code. Slang's reflection API also reflects the varying shader parameters that appear are passed between stages of a pipeline.

Variable and type layouts for varying shader parameters will typically show usage of:

  • Varying input slots (slang::ParameterCategory::VaryingInput) for stage inputs
  • Varying output slots (slang::ParameterCategory::VaryingOutput) for out parameters and the entry-point result
  • Both (slang::ParameterCategory::VaryingInput and ::VaryingOutput) for inout parameters
  • Nothing (no usage for any unit) for system value parameters (typically using an SV_* semantic)

For user-defined varying parameters, some GPU APIs care about the semantic that has been applied to the parameter. For example, given this shader code:

[shader("vertex")]
float4 vertexMain(
    float3 position : POSITION,
    float3 normal : NORMAL,
    float3 uv : TEXCOORD,
    // ...
    )
    : SV_Position
{
    // ...
}

the shader parameter normal of vertexMain has a semantic of NORMAL.

Semantics are only relevant for shader parameters that became part of the varying input/output interface of an entry point for some stage, in which case the VariableLayoutReflection::getStage() method will return that stage. A semantic is decomposed into both a name and an index (e.g., TEXCOORD5 has a name of "TEXCOORD" and an index of 5). This information can be reflected with getSemanticName() and getSemanticIndex():

void printVarLayout(slang::VariableLayoutReflection* varLayout)
{
    // ...
    if (varLayout->getStage() != SLANG_STAGE_NONE)
    {
        print("semantic: ");
        print("name: "); printQuotedString(varLayout->getSemanticName());
        print("index: "); print(varLayout->getSemanticIndex());
    }
    // ...
}

Calculating Cumulative Offsets

All of the code so far has only extracted the relative offsets of variable layouts. Offsets for fields have been relative to the struct that contains them. Offsets for top-level parameters have been relative to the scope that contains them, or even to a constant buffer or parameter block that was introduced for that scope.

There are many cases where an application needs to calculate a cumulative offset (or even an absolute offset) for some parameter, even down to the granularity of individual struct fields. As a notable example, allocation of D3D root signatures and Vulkan pipeline layouts for a program requires being able to enumerate the absolute offsets of all bindings in all descriptor tables/sets.

Because offsets for certain layout units include an additional dimension for a space, our example application will define a simple struct to represent a cumulative offset:

struct CumulativeOffset
{
    int value; // the actual offset
    int space; // the associated space
};

Access Paths

There are multiple ways to track and calculate cumulative offsets. Here we will present a solution that is both simple and reasonably efficient, while still yielding correct results even in complicated scenarios.

If all we had to do was calculate the byte offsets of things, a single size_t would be enough to represent a cumulative offset. However, we have already seen that in the context of a GPU language like Slang, we can have offsets measured in multiple different layout units. A naive implementation might try to represent a cumulative offset as a vector or dictionary of scalar offsets, with (up to) one for each layout unit. The sheer number of layout units (the cases of the slang::ParameterCategory enumeration) makes such an approach unwieldy.

Instead we focus on the intuition that the cumulative offset of a variable layout, for any given layout unit, can be computed by summing up all the relative offsets along the access path to that variable. For example, given code like:

struct Material
{
    Texture2D albedoMap;
    Texture2D glossMap;
    SamplerState sampler;
}
struct LightingEnvironment
{
    TextureCube environmentMap;
    float3 sunLightDir;
    float3 sunLightIntensity;
}
struct Params
{
    LightingEnvironment lights;
    Material material;
}
uniform Params params;

we expect that the cumulative offset of params.material.glossMap in units of Vulkan bindings can be computed by summing up the offsets in that unit of params (0), material (1), and glossMap (1).

When recursively traversing the parameters of a shader, out example application will track an access path as a singly-linked list of variable layouts that points up the stack, from the deepest variable to the shallowest:

struct AccessPathNode
{
    slang::VariableLayoutReflection* varLayout;
    AccessPathNode* outer;
};

struct AccessPath
{
    AccessPathNode* leafNode = nullptr;
};

For the example code above, if our recursive traversal is at params.material.glossMap, then the access path will start with a node for glossMap which points to a node for material, which points to a node for glossMap.

For many layout units, we can calculate a cumulative offset simply by summing up contributions along the entire access path, with logic like the following:

CumulativeOffset calculateCumulativeOffset(slang::ParameterCategory layoutUnit, AccessPath accessPath)
{
    // ...
    for(auto node = accessPath.leafNode; node != nullptr; node = node->outer)
    {
        result.value += node->varLayout->getOffset(layoutUnit);
        result.space += node->varLayout->getBindingSpace(layoutUnit);
    }
    // ...
}

Once our example application is properly tracking access paths, we will be able to use them to calculate and print the cumulative offsets of variable layouts:

void printOffsets(
    slang::VariableLayoutReflection* varLayout,
    AccessPath accessPath)
{
    // ...

    print("cumulative:");
    for (int i = 0; i < usedLayoutUnitCount; ++i)
    {
        print("- ");
        auto layoutUnit = varLayout->getCategoryByIndex(i);
        printCumulativeOffset(varLayout, layoutUnit, accessPath);
    }
}

Printing the cumulative offset of a variable layout requires adding the offset information for the variable itself to the offset calculated from its access path:

void printCumulativeOffset(
    slang::VariableLayoutReflection* varLayout,
    slang::ParameterCategory layoutUnit,
    AccessPath accessPath)
{
    CumulativeOffset cumulativeOffset = calculateCumulativeOffset(layoutUnit, accessPath);

    cumulativeOffset.offset += varLayout->getOffset(layoutUnit);
    cumulativeOffset.space += varLayout->getBindingSpace(layoutUnit);

    printOffset(layoutUnit, cumulativeOffset.offset, cumulativeOffset.space);
}

Tracking Access Paths

In order to support calculation of cumulative offsets, the various functions we've presented so far like printVarLayout() and printTypeLayout() need to be extended with an additional parameter for an AccessPath. For example, the signature of printTypeLayout() becomes:

void printTypeLayout(slang::TypeLayoutReflection* typeLayout, AccessPath accessPath)
{
    // ...
}

Variable Layouts

When traversing a variable layout, we then need to extend the access path to include the additional variable layout, before traversing down into its type layout:

void printVarLayout(slang::VariableLayoutReflection* typeLayout, AccessPath accessPath)
{
    // ...

    ExtendedAccessPath varAccessPath(accessPath, varLayout);

    print("type layout: ");
    printTypeLayout(varLayout->getTypeLayout(), varAccessPath);
}

Scopes

Similar logic is needed within printScope() in our example program:

void printScope(
    slang::VariableLayoutReflection* scopeVarLayout,
    AccessPath                       accessPath)
{
    ExtendedAccessPath scopeAccessPath(accessPath, scopeVarLayout);

    // ...
}

The calls to printOffsets(), printTypeLayout(), etc. inside of printScope() will then pass along the extended access path.

Array-Like Types

When the traversing an array, matrix, or vector type, it is impossible to compute a single cumulative offset that is applicable to all elements of the type. The recursive calls to printTypeLayout() in these cases will simply pass in an empty AccessPath. For example:

case slang::TypeReflection::Kind::Array:
    {
        // ...

        print("element type layout: ");
        printTypeLayout(
            typeLayout->getElementTypeLayout(),
            AccessPath());
    }
    break;

Handling Single-Element Containers

Types like constant buffers and parameter blocks add complexity that requires additions to our representation and handling of access paths.

First, when calculating the cumulative byte offset of variables inside a constant buffer (or any of these single-element container types), it is important not to sum contributions too far up the access path. Consider this example:

struct A
{
    float4 x;
    Texture2D t;
}
struct B
{
    float4 y;
    ConstantBuffer<Inner> a;
}
struct C
{
    float4 z;
    Texture2D t;
    B b;
}
uniform C c;

When compiling for D3D12, the cumulative byte offset of c.b is 16, but the cumulative byte offset of c.b.a.x needs to be zero, because its byte offset should be measured relative to the enclosing constant buffer c.b.a. In contrast, the cumulative of offset of c.b in t registers is one, and the cumulative offset of c.b.a.t needs to be two.

Similarly, when calculating the cumulative offsets of variables inside a parameter block (for targets that can allocate each parameter block its own space), it is important not to sum contributions past an enclosing parameter block.

We can account for these subtleties by extending the representation of access paths in our example application to record the node corresponding to the deepest constant buffer or parameter block along the path:

struct AccessPath
{
    AccessPathNode* leaf = nullptr;
    AccessPathNode* deepestConstantBuffer = nullptr;
    AccessPathNode* deepestParameterBlock = nullptr;
};

Now when traversing a single-element container type in printTypeLayout, we can make a copy of the current access path and modify its deepestConstantBuffer to account for the container:

case slang::TypeReflection::Kind::ConstantBuffer:
case slang::TypeReflection::Kind::ParameterBlock:
case slang::TypeReflection::Kind::TextureBuffer:
case slang::TypeReflection::Kind::ShaderStorageBuffer:
    {
        // ...

        AccumulatedOffsets innerAccessPath = accessPath;
        innerAccessPath.deepestConstantBuffer = innerAccessPath.leaf;

        // ...
    }
    break;

Further, if the container had a full space allocated to it, then we also update the deepestParameterBlock:

// ...
if (containerVarLayout->getTypeLayout()->getSize(
    slang::ParameterCategory::SubElementRegisterSpace) != 0)
{
    innerAccessPath.deepestParameterBlock = innerAccessPath.leaf;
}
// ...

Finally, when traversing the element of the container, we need to use this new innerAccessPath, and also extend the access path when traversing into the type layout of the element:

print("element: ");
printOffsets(elementVarLayout, innerAccessPath);

ExtendedAccessPath elementAccessPath(innerAccessPath, elementVarLayout);

print("type layout: ");
printTypeLayout(
    elementVarLayout->getTypeLayout(),
    elementAccessPath);

Accumulating Offsets Along An Access Path

We now understand that the proper way to calculate a cumulative offset depends on the layout unit:

CumulativeOffset calculateCumulativeOffset(
    slang::ParameterCategory layoutUnit,
    AccessPath               accessPath)
{
    switch(layoutUnit)
    {
    // ...
    }
}

Layout Units That Don't Require Special Handling

By default, relative offsets will be summed for all nodes along the access path:

default:
    for (auto node = accessPath.leaf; node != nullptr; node = node->outer)
    {
        result.offset += node->varLayout->getOffset(layoutUnit);
    }
    break;

Bytes

When a byte offset is being computed, relative offsets will only be summed up to the deepest enclosing constant buffer, if any:

case slang::ParameterCategory::Uniform:
    for (auto node = accessPath.leaf; node != accessPath.deepestConstantBuffer; node = node->outer)
    {
        result.offset += node->varLayout->getOffset(layoutUnit);
    }
    break;

Layout Units That Care About Spaces

Finally, we need to handle the layout units that care about spaces:

case slang::ParameterCategory::ConstantBuffer:
case slang::ParameterCategory::ShaderResource:
case slang::ParameterCategory::UnorderedAccess:
case slang::ParameterCategory::SamplerState:
case slang::ParameterCategory::DescriptorTableSlot:
    // ...
    break;

Relative offsets, including space offsets, need to be summed along the access path up to the deepest enclosing parameter block, if any:

for (auto node = accessPath.leaf; node != accessPath.deepestParameterBlock; node = node->outer)
{
    result.offset += node->varLayout->getOffset(layoutUnit);
    result.space += node->varLayout->getBindingSpace(layoutUnit);
}

Additionally, the offset of the enclosing parameter block in spaces needs to be added to the space of the cumulative offset:

for (auto node = accessPath.deepestParameterBlock; node != nullptr; node = node->outer)
{
    result.space += node->varLayout->getOffset(slang::ParameterCategory::SubElementRegisterSpace);
}

Determining Whether Parameters Are Used

Some application architectures make use of shader code that declares a large number of shader parameters at global scope, but only uses a small fraction of those parameters at runtime. Similarly, shader parameters may be declared at global scope even if they are only used by a single entry point in a pipeline. These kinds of architectures are not ideal, but they are pervasive.

Slang's base reflection API intentionally does not provide information about which shader parameters are or are not used by a program, or specific entry points. This choice ensures that applications using the reflection API can robustly re-use data structures built from reflection data across hot reloads of shaders, or switches between variants of a program.

Applications that need to know which parameters are used (and by which entry points or stages) need to query for additional metadata connected to the entry points of their compiled program using IComponentType::getEntryPointMetadata():

slang::IComponentType* program = ...;
slang::IMetadata* entryPointMetadata;
program->getEntryPointMetadata(
        entryPointIndex,
        0, // target index
        &entryPointMetadata);

When traversal of reflection data reaches a leaf parameter, the application can use IMetadata::isParameterLocationUsed() with the absolute location of that parameter for a given layout unit:

unsigned calculateParameterStageMask(
    slang::ParameterCategory layoutUnit,
    CumulativeOffset offset)
{
    unsigned mask = 0;
    for(int i = 0; i < entryPointCount; ++i)
    {
        bool isUsed = false;
        entryPoints[i].metadata->isParameterLocationUsed(
            layoutUnit, offset.space, offset.value, isUsed);
        if(isUsed)
        {
            mask |= 1 << unsigned(entryPoints[i].stage);
        }
    }
    return mask;
}

The application can then incorporate this logic into a loop over the layout units consumed by a parameter:

unsigned calculateParameterStageMask(
    slang::VariableLayoutReflection* varLayout,
    AccessPath accessPath)
{
    unsigned mask = 0;

    int usedLayoutUnitCount = varLayout->getCategoryCount();
    for (int i = 0; i < usedLayoutUnitCount; ++i)
    {
        auto layoutUnit = varLayout->getCategoryByIndex(i);
        auto offset = calculateCumulativeOffset(
            varLayout, layoutUnit, accessPath);
        
        mask |= calculateStageMask(
            layoutUnit, offset);
    }

    return mask;
}

Finally, we can wrap all this up into logic to print which stage(s) use a given parameter, based on the information in the per-entry-point metadata:

void printVarLayout(
    slang::VariableLayoutReflection* varLayout,
    AccessPath accessPath)
{
    //...
    unsigned stageMask = calculateStageMask(
        varLayout, accessPath);

    print("used by stages: ");
    for(int i = 0; i < SLANG_STAGE_COUNT; i++)
    {
        if(stageMask & (1 << i))
        {
            print("- ");
            printStage(SlangStage(i));
        }
    }
    // ...
}

Conclusion

At this point we have provided a comprehensive example of how to robustly traverse the information provided by the Slang reflection API to get a complete picture of the shader parameters of a program, and what target-specific locations they were bound to. We hope that along the way we have also imparted some key parts of the mental model that exists behind the reflection API and its representations.

1---
2layout: user-guide
3permalink: /user-guide/reflection
4---
5
6Using the Reflection API
7=========================
8
9This chapter provides an introduction to the Slang reflection API.
10Our goals in this chapter are to:
11
12* Demonstrate the recommended types and operations to use for the most common reflection scenarios
13
14* Provide an underlying mental model for how Slang's reflection information represents the structure of a program
15
16We will describe the structure of a program that traverses all of the parameters of a shader program and prints information (including binding locations) for them.
17The code shown here is derived from the [reflection-api](https://github.com/shader-slang/slang/tree/master/examples/reflection-api) example that is included in the Slang repository.
18Readers may find it helpful to follow along with that code, to see a more complete picture of what is presented here.
19
20Compiling a Program
21-------------------
22
23The first step in reflecting a shader program is, unsurprisingly, to compile it.
24Currently reflection information cannot be queried from code compiled via the command-line `slangc` tool, so applications that want to perform reflection on Slang shader code should use the [compilation API](08-compiling.md#using-the-compilation-api) to compile a program, and then use `getLayout()` to extract reflection information:
25
26```c++
27slang::IComponentType* program = ...;
28slang::ProgramLayout* programLayout = program->getLayout(targetIndex);
29```
30
31For more information, see the [relevant section](08-compiling.md#layout-and-reflection) of the chapter on compilation.
32
33Types and Variables
34-------------------
35
36We start our discussion of the reflection API with two of the fundamental building blocks used to represent the structure of a program: types and variables.
37
38A key property of GPU shader programming is that the same type may be laid out differently, depending on how it is used.
39For example, a user-defined `struct` type `Stuff` will often be laid out differently if it is used in a `ConstantBuffer<Stuff>` than in a `StructuredBuffer<Stuff>`.
40
41Because the same thing can be laid out in multiple ways (even within the same program), the Slang reflection API represents types and variables as distinct things from the *layouts* applied to them.
42This section focuses only on the underlying types/variables, while later sections will build on these concepts to show how layouts can be reflected.
43
44### Variables
45
46A `VariableReflection` represents a variable declaration in the input program.
47Variables include global shader parameters, fields of `struct` types, and entry-point parameters.
48
49Because a `VariableReflection` does not include layout information, the main things that can be queried on it are just its name and type:
50
51```c++
52void printVariable(
53    slang::VariableReflection* variable)
54{
55    const char* name = variable->getName();
56    slang::TypeReflection* type = variable->getType();
57
58    print("name: ");    printQuotedString(name);
59    print("type: ");    printType(type);
60}
61```
62
63### Types
64
65A `TypeReflection` represents some type in the input program.
66There are various different *kinds* of types, such as arrays, user-defined `struct` types, and built-in types like `int`.
67The reflection API represents these different cases with the `TypeReflection::Kind` enumeration.
68
69On its own, a `TypeReflection` does not include layout information.
70
71We will now start building a function for printing information about types:
72
73```c++
74void printType(slang::TypeReflection* type)
75{
76    const char* name = type->getName();
77    slang::TypeReflection::Kind kind = type->getKind();
78
79    print("name: ");    printQuotedString(name);
80    print("kind: ");    printTypeKind(kind);
81
82    // ...
83}
84```
85
86Given what has been presented so far, if we have a Slang variable declaration like the following:
87
88```hlsl
89float x;
90```
91
92then applying `printVariable()` to a `VariableReflection` for `x` would yield:
93
94```
95name: "x"
96type:
97  name: "float"
98  kind: Scalar
99```
100
101Additional information can be queried from a `TypeReflection`, depending on its kind:
102
103```c++
104void printType(slang::TypeReflection* type)
105{
106    // ...
107
108    switch(type->getKind())
109    {
110    default:
111        break;
112
113    // ...
114    }
115}
116```
117
118The following subsections will show examples of what can be queried for various kinds of types.
119
120#### Scalar Types
121
122Scalar types store an additional enumerant to indicate which of the built-in scalar types is being represented:
123
124```c++
125case slang::TypeReflection::Kind::Scalar:
126    {
127        print("scalar type: ");
128        printScalarType(type->getScalarType());
129    }
130    break;
131```
132
133The `slang::ScalarType` enumeration includes cases for the built-in integer and floating-point types (for example, `slang::ScalarType::UInt64` and `slang::ScalarType::Float16`), as well as the basic `bool` type (`slang::ScalarType::Bool`).
134The `void` type is also considered a scalar type (`slang::ScalarType::Void`);
135
136#### Structure Types
137
138A structure type may have zero or more *fields*.
139Each field is represented as a `VariableReflection`.
140A `TypeReflection` allows the fields to be enumerated using `getFieldCount()` and `getFieldByIndex()`.
141
142```c++
143case slang::TypeReflection::Kind::Struct:
144    {
145        print("fields:");
146        int fieldCount = type->getFieldCount();
147        for (int f = 0; f < fieldCount; f++)
148        {
149            print("- ");
150            slang::VariableReflection* field =
151                type->getFieldByIndex(f);
152            printVariable(field);
153        }
154    }
155    break;
156```
157
158For the purposes of the reflection API, the fields of a `struct` type are its non-static members (both `public` and non-`public`).
159
160Given Slang code like the following:
161
162```hlsl
163struct S
164{
165    int a;
166    float b;
167}
168```
169
170Reflection on type `S` would yield:
171
172```
173name: "S"
174kind: Struct
175fields:
176  - name: "a"
177    type:
178      name: "int"
179      kind: Scalar
180  - name: "b"
181    type:
182      name: "float"
183      kind: Scalar
184```
185
186#### Arrays
187
188An array type like `int[3]` is defined by the number and type of elements in the array, which can be queried with `getElementCount()` and `getElementType`, respectively:
189
190```c++
191case slang::TypeReflection::Kind::Array:
192    {
193        print("element count: ");
194        printPossiblyUnbounded(type->getElementCount());
195
196        print("element type: ");
197        printType(type->getElementType());
198    }
199    break;
200```
201
202Some array types, like `Stuff[]`, have *unbounded* size.
203The Slang reflection API represents this case using the maximum value possible for the `size_t` result from `getElementCount()`:
204
205```c++
206void printPossiblyUnbounded(size_t value)
207{
208    if (value == ~size_t(0))
209    {
210        printf("unbounded");
211    }
212    else
213    {
214        printf("%u", unsigned(value));
215    }
216}
217```
218
219#### Vectors
220
221Vector types like `int3` are similar to arrays, in that they are defined by their element type and number of elements:
222
223```c++
224case slang::TypeReflection::Kind::Vector:
225    {
226        print("element count: ");
227        printCount(type->getElementCount());
228
229        print("element type: ");
230        printType(type->getElementType());
231    }
232    break;
233```
234
235#### Matrices
236
237Matrix types like `float3x4` are defined by the number of rows, the number of columns, and the element type:
238
239```c++
240case slang::TypeReflection::Kind::Matrix:
241    {
242        print("row count: ");
243        printCount(type->getRowCount());
244
245        print("column count: ");
246        printCount(type->getColumnCount());
247
248        print("element type: ");
249        printType(type->getElementType());
250    }
251    break;
252```
253
254#### Resources
255
256There are a wide range of resource types, including simple cases like `TextureCube` and `StructuredBuffer<int>`, as well as quite complicated ones like `RasterizerOrderedTexture2DArray<int4>` and `AppendStructuredBuffer<Stuff>`.
257
258The Slang reflection API breaks down the properties of a resource type into its shape, access, and result type:
259
260```c++
261case slang::TypeReflection::Kind::Resource:
262    {
263        key("shape");
264        printResourceShape(type->getResourceShape());
265
266        key("access");
267        printResourceAccess(type->getResourceAccess());
268
269        key("result type");
270        printType(type->getResourceResultType());
271    }
272    break;
273```
274
275The *result type* of a resource is simply whatever would be returned by a basic read operation on that resource.
276For resource types in Slang code, the result type is typically written as a generic type parameter after the type name.
277For a `StructuredBuffer<Thing>` the result type is `Thing`, while for a `Texture2D<int3>` it is `int3`.
278A texture type like `Texture2D` that does not give an explicit result type has a default result type of `float4`.
279
280The *access* of a resource (`SlangResourceAccess`) represents how the elements of the resource may be accessed by shader code.
281For Slang resource types, access is typically encoded as a prefix on the type name.
282For example, an unprefixed `Texture2D` has read-only access (`SLANG_RESOURCE_ACCESS_READ`), while a `RWTexture2D` has read-write access (`SLANG_RESOURCE_ACCESS_READ_WRITE`).
283
284The *shape* of a resource (`SlangResourceShape`) represents the conceptual rank/dimensionality of the resource and how it is indexed.
285For Slang resource type names, everything after the access prefix is typically part of the shape.
286
287A resource shape breaks down into a *base shape* along with a few possible suffixes like array-ness:
288
289```c++
290void printResourceShape(SlangResourceShape shape)
291{
292    print("base shape:");
293    switch(shape & SLANG_BASE_SHAPE_MASK)
294    {
295    case SLANG_TEXTURE1D: printf("TEXTURE1D"); break;
296    case SLANG_TEXTURE2D: printf("TEXTURE2D"); break;
297    // ...
298    }
299
300    if(shape & SLANG_TEXTURE_ARRAY_FLAG) printf("ARRAY");
301    if(shape & SLANG_TEXTURE_MULTISAMPLE_FLAG) printf("MULTISAMPLE");
302    // ...
303}
304```
305
306#### Single-Element Containers
307
308Types like `ConstantBuffer<T>` and `ParameterBlock<T>` represent a grouping of parameter data, and behave like an array or structured buffer with only a single element:
309
310```c++
311case slang::TypeReflection::Kind::ConstantBuffer:
312case slang::TypeReflection::Kind::ParameterBlock:
313case slang::TypeReflection::Kind::TextureBuffer:
314case slang::TypeReflection::Kind::ShaderStorageBuffer:
315    {
316        key("element type");
317        printType(type->getElementType());
318    }
319    break;
320```
321
322Layout for Types and Variables
323------------------------------
324
325The Slang reflection API provides `VariableLayoutReflection` and `TypeLayoutReflection` to represent a *layout* of a given variable or type.
326As discussed earlier, the same type might have multiple different layouts used for it in the same program.
327
328### Layout Units
329
330A key challenge that the Slang reflection API has to address is how to represent the offset of a variable (or struct field, etc.) or the size of a type when `struct` types are allowed to mix various kinds of data together.
331
332For example, consider the following Slang code:
333
334```hlsl
335struct Material
336{
337    Texture2D albedoMap;
338    SamplerState sampler;
339    float2 uvScale;
340    float2 uvBias;
341}
342struct Uniforms
343{
344    TextureCube environmentMap;
345    SamplerState environmentSampler;
346    float3 sunLightDirection;
347    float3 sunLightIntensity;
348    Material material;
349    // ...
350}
351ParameterBlock<Uniforms> uniforms;
352```
353
354When laid out in the given parameter block, what is the offset of the field `Uniforms::material`? What is the size of the `Material` type?
355
356The key insight is that layout is multi-dimensional: the same type can have a size in multiple distinct units.
357For example, when compiling the above code for D3D12/DXIL, the answer is that the `Uniforms::material` has an offset of one `t` register, one `s` register, and 32 bytes.
358Similarly, the size of the `Material` type is one `t` register, one `s` register, and 16 bytes.
359
360We refer to these distinct units of measure used in layouts (including bytes, `t` registers, and `s` registers) as *layout units*.
361Layout units are represented in the Slang reflection API with the `slang::ParameterCategory` enumeration.
362(We will avoid the term "parameter category," despite that being the name currently exposed in the public API; that name has turned out to be a less-than-ideal choice).
363
364### Variable Layouts
365
366A `VariableLayoutReflection` represents a layout computed for a given variable (itself a `VariableReflection`).
367The underlying variable can be accessed with `getVariable()`, but the variable layout also provides accessors for the most important properties.
368
369A variable layout stores the offsets of that variable (possibly in multiple layout units), and also a type layout for the data stored in the variable.
370
371```c++
372void printVarLayout(slang::VariableLayoutReflection* varLayout)
373{
374    print("name"); printQuotedString(varLayout->getName());
375
376    printRelativeOffsets(varLayout);
377
378    key("type layout");
379    printTypeLayout(varLayout->getTypeLayout());
380}
381```
382
383#### Offsets
384
385The offsets stored by a `VariableLayoutReflection` are always *relative* to the enclosing `struct` type, scope, or other context that surrounds the variable.
386
387The `VariableLayoutReflection::getOffset` method can be used to query the relative offset of a variable for any given layout unit:
388
389```c++
390void printOffset(
391    slang::VariableLayoutReflection* varLayout,
392    slang::ParameterCategory layoutUnit)
393{
394    size_t offset = varLayout->getOffset(layoutUnit);
395
396    print("value: "); print(offset);
397    print("unit: "); printLayoutUnit(layoutUnit);
398
399    // ...
400}
401```
402
403If an application knows what unit(s) it expects a variable to be laid out in, it can directly query those.
404However, in a case like our systematic traversal of all shader parameters, it is not always possible to know what units a given variable uses.
405
406The Slang reflection API can be used to query layout units used by a given variable layout with `getCategoryCount()` and `getCategoryByIndex()`:
407
408```c++
409void printRelativeOffsets(
410    slang::VariableLayoutReflection* varLayout)
411{
412    print("relative offset: ");
413    int usedLayoutUnitCount = varLayout->getCategoryCount();
414    for (int i = 0; i < usedLayoutUnitCount; ++i)
415    {
416        auto layoutUnit = varLayout->getCategoryByIndex(i);
417        printOffset(varLayout, layoutUnit);
418    }
419}
420```
421
422#### Spaces / Sets
423
424For certain target platforms and layout units, the offset of a variable for that unit might include an additional dimension that represents a Vulkan/SPIR-V descriptor set, D3D12/DXIL register space, or a WebGPU/WGSL binding group.
425In this chapter, we will uniformly refer to all of these concepts as *spaces*.
426
427The relative space offset of a variable layout for a given layout unit can be queried with `getBindingSpace()`:
428
429```c++
430void printOffset(
431    slang::VariableLayoutReflection* varLayout,
432    slang::ParameterCategory layoutUnit)
433{
434    // ...
435
436    size_t spaceOffset = varLayout->getBindingSpace(layoutUnit);
437
438    switch(layoutUnit)
439    {
440    default:
441        break;
442
443    case slang::ParameterCategory::ConstantBuffer:
444    case slang::ParameterCategory::ShaderResource:
445    case slang::ParameterCategory::UnorderedAccess:
446    case slang::ParameterCategory::SamplerState:
447    case slang::ParameterCategory::DescriptorTableSlot:
448        print("space: "); print(spaceOffset);    
449    }
450}
451```
452
453The code above only prints the space offset for the layout units where a space is semantically possible and meaningful.
454
455### Type Layouts
456
457A `TypeLayoutReflection` represents a layout computed for a type.
458The underlying type that layout was computed for can be accessed using `TypeLayoutReflection::getType()`, but accessors are provided so that the most common properties of types can be queried on type layouts.
459
460The main thing that a type layout stores is the size of the type:
461
462```c++
463void printTypeLayout(slang::TypeLayoutReflection* typeLayout)
464{
465    print("name: "); printQuotedString(typeLayout->getName());
466    print("kind: "); printTypeKind(typeLayout->getKind());
467
468    printSizes(typeLayout);
469
470    // ...
471}
472```
473
474#### Size
475
476Similarly to variable layouts, the size of a type layout can be queried given a chosen layout unit:
477
478```c++
479void printSize(
480    slang::TypeLayoutReflection* typeLayout,
481    slang::ParameterCategory layoutUnit)
482{
483    size_t size = typeLayout->getSize(layoutUnit);
484
485    key("value"); printPossiblyUnbounded(size);
486    key("unit"); writeLayoutUnit(layoutUnit);
487}
488```
489
490Note that the size of a type may be *unbounded* for a particular layout unit; this case is encoded just like the unbounded case for the element count of an array type (`~size_t(0)`).
491
492The layout units used by a particular type layout can be iterated over using `getCategoryCount()` and `getCategoryByIndex()`:
493
494```c++
495void printSizes(slang::TypeLayoutReflection* typeLayout)
496{
497    print("size: ");
498    int usedLayoutUnitCount = typeLayout->getCategoryCount();
499    for (int i = 0; i < usedLayoutUnitCount; ++i)
500    {
501        auto layoutUnit = typeLayout->getCategoryByIndex(i);
502        print("- "); printSize(typeLayout, layoutUnit);
503    }
504
505    // ...
506}
507```
508
509#### Alignment and Stride
510
511For any given layout unit, a type layout can also reflect the alignment of the type for that unit with `TypeLayoutReflection::getAlignment()`.
512Alignment is typically only interesting when the layout unit is bytes (`slang::ParameterCategory::Uniform`).
513
514Note that, unlike in C/C++, a type layout in Slang may have a size that is not a multiple of its alignment.
515The *stride* of a type layout (for a given layout unit) is its size rounded up to its alignment, and is used as the distance between consecutive elements in arrays.
516The stride of a type layout can be queried for any chosen layout unit with `TypeLayoutReflection::getStride()`.
517
518Note that all of the `TypeLayoutReflection` methods `getSize()`, `getAlignment()`, and `getStride()` default to returning information in bytes, if a layout unit is not specified.
519The same is true of the `VariableLayoutReflection::getOffset()` method.
520
521The alignment and stride of a type layout can be reflected when it is relevant with code like:
522
523```c++
524void printTypeLayout(slang::TypeLayoutReflection* typeLayout)
525{
526    // ...
527
528    if(typeLayout->getSize() != 0)
529    {
530        print("alignment in bytes: ");
531        print(typeLayout->getAlignment());
532
533        print("stride in bytes: ");
534        print(typeLayout->getStride());
535    }
536
537    // ...
538}
539```
540
541#### Kind-Specific Information
542
543Just as with the underlying types, a type layout may store additional information depending on the kind of type:
544
545```c++
546void printTypeLayout(slang::TypeLayoutReflection* typeLayout)
547{
548    // ...
549
550    switch(typeLayout->getKind())
551    {
552    default:
553        break;
554    
555        // ...
556    }
557}
558```
559
560The following subsections will cover the important kinds to handle when reflecting type layouts.
561
562#### Structure Type Layouts
563
564A type layout for a `struct` type provides access to the fields of the `struct`, with each field represented as a variable layout:
565
566```c++
567case slang::TypeReflection::Kind::Struct:
568    {
569        print("fields: ");
570
571        int fieldCount = typeLayout->getFieldCount();
572        for (int f = 0; f < fieldCount; f++)
573        {
574            auto field = typeLayout->getFieldByIndex(f);
575            printVarLayout(field);
576        }
577    }
578    break;
579```
580
581The offset information stored on the type layout for each field will always be relative to the start of the `struct` type.
582
583#### Array Type Layouts
584
585Array type layouts store a layout for the element type of the array, which can be accessed with `getElementTypeLayout()`:
586
587```c++
588case slang::TypeReflection::Kind::Array:
589    {
590        print("element count: ");
591        printPossiblyUnbounded(typeLayout->getElementCount());
592
593        print("element type layout: ");
594        printTypeLayout(typeLayout->getElementTypeLayout());
595    }
596    break;
597```
598
599#### Matrix Type Layouts
600
601A layout for a matrix type stores a matrix layout *mode* (`SlangMatrixLayoutMode`) to record whether the type was laid out in row-major or column-major layout:
602
603```c++
604case slang::TypeReflection::Kind::Matrix:
605    {
606        // ...
607
608        print("matrix layout mode: ");
609        printMatrixLayoutMode(typeLayout->getMatrixLayoutMode());
610    }
611    break;
612```
613
614Note that the concepts of "row" and "column" as employed by Slang are the opposite of how Vulkan, SPIR-V, GLSL, and OpenGL use those terms.
615When Slang reflects a matrix as using row-major layout, the corresponding matrix in generated SPIR-V will have a `ColMajor` decoration.
616For an explanation of why these conventions differ, please see the relevant [appendix](./a1-01-matrix-layout.md).
617
618#### Single-Element Containers
619
620Constant buffers, parameter blocks, and other types representing grouping of parameters are the most subtle cases to handle for reflection.
621The Slang reflection API aspires to provide complete and accurate information for these cases, but understanding *why* the provided data is what it is requires an appropriate mental model.
622
623##### Simple Cases
624
625In simple cases, a constant buffer has only ordinary data in it (things where the only used layout unit is bytes):
626
627```
628struct DirectionalLight
629{
630    float3 direction;
631    float3 intensity;
632}
633ConstantBuffer<DirectionalLight> light;
634```
635
636When this case is laid out for D3D12, the `DirectionalLight` type will consume 28 bytes, but the `light` parameter will instead consume one `b` register.
637We thus see that the `ConstantBuffer<>` type effectively "hides" the number of bytes used by its element.
638
639Similarly, when a parameter block only has opaque types in it:
640
641```
642struct Material
643{
644    Texture2D albedoMap;
645    Texture2D glossMap;
646    SamplerState sampler;
647}
648ParameterBlock<Material> material;
649```
650
651When this is laid out for Vulkan, the `Material` type will consume 3 bindings, but the `material` parameter will instead consume one space.
652A `ParameterBLock<>` type hides the bindings/registers/slots used by its element.
653
654##### When Things Leak
655
656If the element type of a constant buffer includes any data that isn't just measured in bytes, that usage will "leak" into the size of the constant buffer.
657For example:
658
659```
660struct ViewParams
661{
662    float3 cameraPos;
663    float3 cameraDir;
664    TextureCube envMap;
665}
666ConstantBuffer<ViewParams> view;
667```
668
669If this example is laid out for D3D12, the `ViewParams` type will have a size of 28 bytes (according to D3D constant buffer layout rules) and one `t` register.
670The size of the `view` parameter will be one `b` register and one `t` register.
671The `ConstantBuffer<>` type can hide the bytes used by `ViewParams`, but the used `t` register leaks out and becomes part of the size of `view`.
672
673If the same example is laid out for Vulkan, the `ViewParams` type will have a size of 28 bytes (according to `std140` layout rules) and one `binding`.
674The size of the `view` parameter will be two `binding`s.
675
676An important question a user might have in the Vulkan case, is whether the `binding` for `view` comes before that for `view.envMap`, or the other way around.
677The answer is that the Slang compiler always lays out the "container" part of a parameter like `view` (the constant buffer) before the element, but a client of the reflection API shouldn't have to know such things to understand the information that gets reflected.
678
679Note that in the Vulkan case, the offset of the `envMap` field within `ViewParams` is zero `binding`s, but the offset of `view.envMap` field relative to `view` is one `binding`.
680Computing the cumulative offset of `view.envMap` requires more information than just that available on the variable layouts for `view` and `view.envMap`.
681
682Similar cases of usage leaking can occur for parameter blocks, when one parameter block is nested within another.
683
684##### A `ConstantBuffer<>` Without a Constant Buffer
685
686While it is an uncommon case, it is possible to use a `ConstantBuffer<>` with an element type that contains no ordinary data (nothing with a layout unit of bytes):
687
688```
689struct Material
690{
691    Texture2D albedoMap;
692    Texture2D glossMap;
693    SamplerState sampler;
694}
695ConstantBuffer<Material> material;
696```
697
698If this case is compiled for Vulkan, the `material` parameter will consume 3 `binding`s, but none of those will be for a constant buffer.
699In this case, unlike in the preceding example with `view.envMap`, the offset of `material.albedoMap` relative to `material` will be zero `binding`s.
700
701##### Implicitly-Allocated Constant Buffers
702
703A common use case for parameter blocks is to wrap up all of the parameters of a shader, or of some subsystem.
704In such cases, there are likely to be both ordinary-type and opaque-type fields:
705
706```
707struct PointLight
708{
709    float3 position;
710    float3 intensity;
711}
712struct LightingEnvironment
713{
714    TextureCube envMap;
715    PointLight pointLights[10];
716}
717ParameterBlock<LightingEnvironment> lightEnv;
718```
719
720If this example is compiled for Vulkan, the `LightingEnvironment` type uses 316 bytes and one `binding` (ParameterCategory::DescriptorTableSlot), while `lightEnv` uses one descriptor `set`  (ParameterCategory::SubElementRegisterSpace).
721
722What is not clear in the above description, however, is that because `LightingEnvironment` uses ordinary bytes, the Slang compiler will have to implicitly allocate a `binding` for a constant buffer to hold those bytes.
723Conceptually, the layout is similar to what would be produced for `ParameterBlock<ConstantBuffer<LightingEnvironment>>`.
724
725Furthermore, that constant buffer `binding` will be the first binding within the descriptor `set` for `lightEnv`, so that the cumulative `binding` offset for `lightEnv.envMap` will be one `binding` (even though `LightingEnvironment::envMap` has a relative offset of zero `binding`s).
726
727##### Container and Element
728
729In order to properly handle all of the nuances described here, the layout for a type like `ConstantBuffer<Thing>` or `ParameterBlock<Thing>` includes both layout information for the element of the container (a `Thing`) as well as layout information for the *container* itself.
730Furthermore, the layout information for both the element and container need to support storing offset information (not just size), relative to the overall `ConstantBuffer<>` or `ParameterBlock<>`.
731
732The breakdown is thus:
733
734* The size information for the complete container type layout reflects whatever usage "leaks" out, such that it would need to be accounted for when further aggregating the overall type.
735
736* Information about the allocated container is stored as a variable layout, queried with `getContainerVarLayout()`
737
738  * The type layout for that variable layout shows what was allocated to represent the container itself, including any implicitly-allocated constant buffer
739
740  * The offsets of that variable layout show where the container is situated relative to the overall type.
741  With the current layout strategies used by the Slang compiler, all of these offsets will be zero.
742
743* Information about the element is stored as a variable layout, queried with `getElementVarLayout()`
744
745  * The type layout of that variable layout shows how the element type is laid out inside container.
746
747  * The offsets on that variable layout show where the element is situated relative to the overall type.
748  These offsets will be non-zero in cases where there is some layout unit used by both the element type and the container itself.
749
750Given this understanding, we can now look at the logic to reflect a type layout for a constant buffer, parameter block, or similar type.
751
752```c++
753case slang::TypeReflection::Kind::ConstantBuffer:
754case slang::TypeReflection::Kind::ParameterBlock:
755case slang::TypeReflection::Kind::TextureBuffer:
756case slang::TypeReflection::Kind::ShaderStorageBuffer:
757    {
758        print("container: ");
759        printOffsets(typeLayout->getContainerVarLayout());
760    
761        auto elementVarLayout = typeLayout->getElementVarLayout();
762        print("element: ");
763        printOffsets(elementVarLayout);
764
765        print("type layout: ");
766        printTypeLayout(
767            elementVarLayout->getTypeLayout();
768    }
769    break;
770```
771
772Note that the application logic here does not simply make use of `printVarLayout()` on the results of both `getContainerVarLayout()` and `getElementVarLayout()`, even though it technically could.
773While these sub-parts of the overall type layout are each represented as a `VariableLayoutReflection`, many of the properties of those variable layouts are uninteresting or null; they primarily exist to convey offset information.
774
775##### Example
776
777Given input code like the following:
778
779```hlsl
780struct Material
781{
782    Texture2D albedoMap;
783    SamplerState sampler;
784    float2 uvScale;
785    float2 uvBias;
786}
787
788struct FrameParams
789{
790    ConstantBuffer<Material> material;
791
792    float3 cameraPos;
793    float3 cameraDir;
794
795    TextureCube envMap;
796    float3 sunLightDir;
797    float3 sunLightIntensity;
798
799    Texture2D shadowMap;
800    SamplerComparisonState shadowMapSampler;
801}
802
803ParameterBlock<FrameParams> params;
804```
805
806We will look at the kind of output our example application prints for `params` when compiling for Vulkan.
807The basic information for the variable and its type layout looks like:
808
809```
810- name: "params"
811  offset:
812    relative:
813    - value: 1
814      unit: SubElementRegisterSpace # register spaces / descriptor sets
815  type layout:
816    name: "ParameterBlock"
817    kind: ParameterBlock
818    size:
819      - value: 1
820        unit: SubElementRegisterSpace # register spaces / descriptor sets
821```
822
823As we would expect, the size of the parameter block is one register space (aka Vulkan descriptor `set`).
824In this case, the Slang compiler has assigned `params` to have a space offset of 1 (`set=1` in GLSL terms).
825
826The offset information for the container part of `params` is the following:
827
828```
829container:
830offset:
831  relative:
832    - value: 0
833      unit: DescriptorTableSlot # bindings
834      space: 0
835    - value: 0
836      unit: SubElementRegisterSpace # register spaces / descriptor sets
837```
838
839We can see from this information that the `ParameterBlock<>` container had two things allocated to it: a descriptor set (`ParameterCategory::SubElementRegisterSpace`), and a binding within that descriptor set (`ParameterCategory::DescriptorTableSlot`) for the automatically-introduced constant buffer.
840That automatically-introduced buffer has an offset of 0 bindings from the start of the descriptor set.
841
842The layout for the element part of the parameter block is as follows:
843
844```
845element:
846  offset:
847    relative:
848      - value: 1
849        unit: DescriptorTableSlot # bindings
850        space: 0
851      - value: 0
852        unit: Uniform # bytes
853  type layout:
854    name: "FrameParams"
855    kind: Struct
856    size:
857      - value: 6
858        unit: DescriptorTableSlot # bindings
859      - value: 64
860        unit: Uniform # bytes
861    alignment in bytes: 16
862    stride in bytes: 64
863    fields:                  
864      - name: "material"
865        offset:
866          relative:
867            - value: 0
868              unit: DescriptorTableSlot # bindings
869              space: 0
870      ...
871```
872
873We see here that the type layout for the element is as expected of a layout for the `FrameParams` type.
874In particular, note how the `material` field has a relative offset of zero bindings from the start of the `struct`, as is expected for the first field.
875In order to account for the automatically-introduced constant buffer that is used by the container part of the layout, the element variable layout includes a relative offset of one binding (`ParameterCategory::DescriptorTableSlot`).
876
877In a later section we will discuss how to easily sum up the various relative offsets shown in an example like this, when an application wants to compute a *cumulative* offset for a field like `params.material.sampler`.
878
879
880##### Pitfalls to Avoid
881
882It is a common mistake for users to apply `getElementTypeLayout()` on a single-element container, instead of using `getElementVarLayout()` as we advise here.
883The implementation of the reflection API makes an effort to ensure that the type layout returned by `getElementTypeLayout()` automatically bakes in the additional offsets that are needed, but the results can still be unintuitive.
884
885Programs and Scopes
886-------------------
887
888So far, our presentation has largely been bottom-up: we have shown how to recursively perform reflection on types, variables, and their layouts, but we have not yet shown how how to get this recursive traversal started.
889We will now proceed top-down for a bit, and look at how to reflect the top-level parameters of a program.
890
891A `ProgramLayout` is typically obtained using `IComponentType::getLayout()` after compiling and linking a Slang program.
892A program layout primarily comprises the global scope, and zero or more entry points:
893
894```c++
895void printProgramLayout(
896    slang::ProgramLayout* programLayout)
897{
898    print("global scope: ");
899    printScope(programLayout->getGlobalParamsVarLayout());
900
901    print("entry points: ");
902    int entryPointCount = programLayout->getEntryPointCount();
903    for (int i = 0; i < entryPointCount; ++i)
904    {
905        print("- ");
906        printEntryPointLayout(
907            programLayout->getEntryPointByIndex(i));
908    }
909}
910```
911
912The global scope and entry points are each an example of a *scope* where top-level shader parameters can be declared.
913Scopes are represented in the reflection API using `VariableLayoutReflection`s.
914We will now discuss the details of reflection for scopes, starting with the global scope as an example.
915
916### Global Scope
917
918In order to understand how the Slang reflection API exposes the global scope, it is valuable to think of the steps (some of them optional) that the Slang compiler applies to global-scope shader parameter declarations as part of compilation.
919
920#### Parameters are Grouped Into a Structure
921
922If a shader program declares global-scope parameters like the following:
923
924```hlsl
925Texture2D diffuseMap;
926TextureCube envMap;
927SamplerState sampler;
928```
929
930The Slang compiler will conceptually group all of those distinct global-scope parameter declarations into a `struct` type and then have only a single global-scope parameter of that type:
931
932```hlsl
933struct Globals
934{
935    Texture2D diffuseMap;
936    TextureCube envMap;
937    SamplerState sampler;
938}
939uniform Globals globals;
940```
941
942In this simple kind of case, the scope will be reflected as a variable layout with a `struct` type layout, with one field for each parameter declared in that scope:
943
944```c++
945void printScope(
946    slang::VariableLayoutReflection*    scopeVarLayout)
947{
948    auto scopeTypeLayout = scopeVarLayout->getTypeLayout();
949    switch (scopeTypeLayout->getKind())
950    {
951    case slang::TypeReflection::Kind::Struct:
952        {
953            print("parameters: ");
954
955            int paramCount = scopeTypeLayout->getFieldCount();
956            for (int i = 0; i < paramCount; i++)
957            {
958                print("- ");
959
960                auto param = scopeTypeLayout->getFieldByIndex(i);
961                printVarLayout(param, &scopeOffsets);
962            }
963        }
964        break;
965
966        // ...
967    }
968}
969```
970
971#### Wrapped in a Constant Buffer If Needed
972
973In existing shader code that was originally authored for older APIs (such as D3D9) it is common to find a mixture of opaque and ordinary types appearing as global-scope shader parameters:
974
975```hlsl
976Texture2D diffuseMap;
977TextureCube envMap;
978SamplerState sampler;
979
980uniform float3 cameraPos;
981uniform float3 cameraDir;
982```
983
984In these cases, when the Slang compiler groups the parameters into a single `struct`:
985
986```hlsl
987struct Globals
988{
989    Texture2D diffuseMap;
990    TextureCube envMap;
991    SamplerState sampler;
992
993    float3 cameraPos;
994    float3 cameraDir;
995}
996```
997
998it finds that the resulting `struct` consumes a non-zero number of bytes and, for most compilation targets, it will automatically wrap that structure in a `ConstantBuffer<>` before declaring the single shader parameter that represents the global scope:
999
1000```hlsl
1001ConstantBuffer<Globals> globals
1002```
1003
1004This case shows up in the Slang reflection API as the scope having a type layout with the constant-buffer kind:
1005
1006```c++
1007case slang::TypeReflection::Kind::ConstantBuffer:
1008    print("automatically-introduced constant buffer: ");
1009
1010    printOffsets(scopeTypeLayout->getContainerVarLayout());
1011
1012    printScope(scopeTypeLayout->getElementVarLayout());
1013    break;
1014```
1015
1016In this case, the container variable layout reflects the relative offsets for where the automatically-introduced constant buffer is bound, and the element variable layout reflects the global scope parameters that were wrapped in this way.
1017
1018#### Wrapped in a Parameter Block If Needed
1019
1020For targets like D3D12/DXIL, Vulkan/SPIR-V, and WebGPU/WGSL, most shader parameters must be bound via the target-specific grouping mechanism (descriptor tables, descriptor sets, or binding groups, respectively).
1021If the Slang compiler is compiling for such a target and detects that there are global-scope parameters that do not specify an explicit space, then it will conceptually wrap the global-scope declarations in a `ParameterBlock<>` that provides a default space.
1022
1023For example, if compiling this code to Vulkan:
1024
1025```hlsl
1026Texture2D diffuseMap;
1027[[vk::binding(1,0)]] TextureCube envMap;
1028SamplerState sampler;
1029```
1030
1031the Slang compiler will detect that `envMap` is explicitly bound to `binding` 1 in space (aka descriptor `set`) 0, and that neither `diffuseMap` nor `sampler` has been explicitly bound.
1032Both of the unbound parameters need to be passed inside of some space, so the compiler will allocate space 1 for that purpose (as space 0 was already claimed by explicit bindings).
1033In simplistic terms, the compiler will behave *as if* the global-scope parameters are wrapped up in a `struct` and then further wrapped up into a `ParameterBlock<>`.
1034
1035This case shows up in the Slang reflection API as the scope having a type layout with the parameter-block kind:
1036
1037```c++
1038case slang::TypeReflection::Kind::ParameterBlock:
1039    print("automatically-introduced parameter block: ");
1040
1041    printOffsets(scopeTypeLayout->getContainerVarLayout());
1042
1043    printScope(scopeTypeLayout->getElementVarLayout());
1044    break;
1045```
1046
1047In cases where the parameters in a scope require *both* a constant buffer and a parameter block to be automatically introduced, the scope is reflected as if things were wrapped with `ParameterBlock<...>` and not `ParameterBlock<ConstantBuffer<...>>`.
1048That is, the binding information for the implicit constant buffer will be found as part of the container variable layout for the parameter block.
1049
1050#### Pitfalls to Avoid
1051
1052The `ProgramLayout` type has the appealingly-named `getParameterCount` and `getParameterByIndex()` methods, which seem to be the obvious way to navigate the global-scope parameters of a shader.
1053However, we recommend *against* using these functions in applications that want to be able to systematically and robustly reflect any possible input shader code.
1054
1055While the reflection API implementation makes an effort to ensure that the information returned by `getParameterByIndex()` is not incorrect, it is very difficult when using those functions to account for how global-scope parameters might have been grouped into an automatically-introduced constant buffer or parameter block.
1056The `getGlobalConstantBufferBinding()` and `getGlobalConstantBufferSize()` methods can be used in some scenarios, but aren't the best way to get the relevant information.
1057
1058While it would only matter in corner cases, we still recommend that applications use `getGlobalParamsVarLayout()` instead of `getGlobalParamsTypeLayout()`, to account for cases where the global-scope might have offsets applied to it (and also to handle the global scope and entry-point scopes more uniformly).
1059
1060### Entry Points
1061
1062An `EntryPointReflection` provides information on an entry point.
1063This includes the stage that the entry point was compiled for:
1064
1065```c++
1066void printEntryPointLayout(slang::EntryPointReflection* entryPointLayout)
1067{
1068    print("stage: "); printStage(entryPointLayout->getStage());
1069
1070    // ...
1071}
1072```
1073
1074#### Entry Point Parameters
1075
1076An entry point acts as a scope for top-level shader parameters, much like the global scope.
1077Entry-point parameters are grouped into a `struct`, and then automatically wrapped in a constant buffer or parameter block if needed.
1078The main additional consideration, compared to the global scope, is that an entry-point function may also declare a result type.
1079When present, the function result acts more or less as an additional `out` parameter.
1080
1081The parameter scope and result of an entry point can be reflected with logic like:
1082
1083```c++
1084void printEntryPointLayout(slang::EntryPointReflection* entryPointLayout)
1085{
1086    // ...
1087    printScope(entryPointLayout->getVarLayout());
1088
1089    auto resultVarLayout = entryPointLayout->getResultVarLayout();
1090    if (resultVarLayout->getTypeLayout()->getKind() != slang::TypeReflection::Kind::None)
1091    {
1092        key("result"); printVarLayout(resultVarLayout);
1093    }
1094}
1095```
1096
1097##### Pitfalls to Avoid
1098
1099Similarly to the case for the global scope, we recommend against using the `getParameterCount()` and `getParameterByIndex()` methods on `EntryPointReflection`, since they make it harder to handle cases where the entry-point scope might have been allocated as a constant buffer (although the `hasDefaultConstantBuffer()` method is provided to try to support older applications that still use `getParameterByIndex()`).
1100Applications are also recommended to use `EntryPointReflection::getVarLayout()` instead of `::getTypeLayout()`, to more properly reflect the way that offsets are computed and applied to the parameters of an entry point.
1101
1102#### Stage-Specific Information
1103
1104Depending on the stage that an entry point was compiled for, it may provide additional information that an application can query:
1105
1106```c++
1107void printEntryPointLayout(slang::EntryPointReflection* entryPointLayout)
1108{
1109    // ...
1110    switch (entryPointLayout->getStage())
1111    {
1112    default:
1113        break;
1114
1115        // ...
1116    }
1117    // ...
1118}
1119```
1120
1121For example, compute entry points store the thread-group dimensions:
1122
1123```c++
1124case SLANG_STAGE_COMPUTE:
1125    {
1126        SlangUInt sizes[3];
1127        entryPointLayout->getComputeThreadGroupSize(3, sizes);
1128
1129        print("thread group size: ");
1130        print("x: "); print(sizes[0]);
1131        print("y: "); print(sizes[1]);
1132        print("z: "); print(sizes[2]);
1133    }
1134    break;
1135```
1136
1137#### Varying Parameters
1138
1139So far we have primarily been talking about the *uniform* shader parameters of a program: those that can be passed in from application code to shader code.
1140Slang's reflection API also reflects the *varying* shader parameters that appear are passed between stages of a pipeline.
1141
1142Variable and type layouts for varying shader parameters will typically show usage of:
1143
1144* Varying input slots (`slang::ParameterCategory::VaryingInput`) for stage inputs
1145* Varying output slots (`slang::ParameterCategory::VaryingOutput`) for `out` parameters and the entry-point result
1146* Both (`slang::ParameterCategory::VaryingInput` *and* `::VaryingOutput`) for `inout` parameters
1147* Nothing (no usage for any unit) for *system value* parameters (typically using an `SV_*` semantic)
1148
1149For user-defined varying parameters, some GPU APIs care about the *semantic* that has been applied to the parameter.
1150For example, given this shader code:
1151
1152```hlsl
1153[shader("vertex")]
1154float4 vertexMain(
1155    float3 position : POSITION,
1156    float3 normal : NORMAL,
1157    float3 uv : TEXCOORD,
1158    // ...
1159    )
1160    : SV_Position
1161{
1162    // ...
1163}
1164```
1165
1166the shader parameter `normal` of `vertexMain` has a semantic of `NORMAL`.
1167
1168Semantics are only relevant for shader parameters that became part of the varying input/output interface of an entry point for some stage, in which case the `VariableLayoutReflection::getStage()` method will return that stage.
1169A semantic is decomposed into both a name and an index (e.g., `TEXCOORD5` has a name of `"TEXCOORD"` and an index of `5`).
1170This information can be reflected with `getSemanticName()` and `getSemanticIndex()`:
1171
1172
1173```c++
1174void printVarLayout(slang::VariableLayoutReflection* varLayout)
1175{
1176    // ...
1177    if (varLayout->getStage() != SLANG_STAGE_NONE)
1178    {
1179        print("semantic: ");
1180        print("name: "); printQuotedString(varLayout->getSemanticName());
1181        print("index: "); print(varLayout->getSemanticIndex());
1182    }
1183    // ...
1184}
1185```
1186
1187Calculating Cumulative Offsets
1188------------------------------
1189
1190All of the code so far has only extracted the *relative* offsets of variable layouts.
1191Offsets for fields have been relative to the `struct` that contains them.
1192Offsets for top-level parameters have been relative to the scope that contains them, or even to a constant buffer or parameter block that was introduced for that scope.
1193
1194There are many cases where an application needs to calculate a *cumulative* offset (or even an absolute offset) for some parameter, even down to the granularity of individual `struct` fields.
1195As a notable example, allocation of D3D root signatures and Vulkan pipeline layouts for a program requires being able to enumerate the absolute offsets of all bindings in all descriptor tables/sets.
1196
1197Because offsets for certain layout units include an additional dimension for a space, our example application will define a simple `struct` to represent a cumulative offset:
1198
1199```c++
1200struct CumulativeOffset
1201{
1202    int value; // the actual offset
1203    int space; // the associated space
1204};
1205```
1206
1207### Access Paths
1208
1209There are multiple ways to track and calculate cumulative offsets.
1210Here we will present a solution that is both simple and reasonably efficient, while still yielding correct results even in complicated scenarios.
1211
1212If all we had to do was calculate the byte offsets of things, a single `size_t` would be enough to represent a cumulative offset.
1213However, we have already seen that in the context of a GPU language like Slang, we can have offsets measured in multiple different layout units.
1214A naive implementation might try to represent a cumulative offset as a vector or dictionary of scalar offsets, with (up to) one for each layout unit.
1215The sheer number of layout units (the cases of the `slang::ParameterCategory` enumeration) makes such an approach unwieldy.
1216
1217Instead we focus on the intuition that the cumulative offset of a variable layout, for any given layout unit, can be computed by summing up all the relative offsets along the *access path* to that variable.
1218For example, given code like:
1219
1220```hlsl
1221struct Material
1222{
1223    Texture2D albedoMap;
1224    Texture2D glossMap;
1225    SamplerState sampler;
1226}
1227struct LightingEnvironment
1228{
1229    TextureCube environmentMap;
1230    float3 sunLightDir;
1231    float3 sunLightIntensity;
1232}
1233struct Params
1234{
1235    LightingEnvironment lights;
1236    Material material;
1237}
1238uniform Params params;
1239```
1240
1241we expect that the cumulative offset of `params.material.glossMap` in units of Vulkan `binding`s can be computed by summing up the offsets in that unit of `params` (0), `material` (1), and `glossMap` (1).
1242
1243When recursively traversing the parameters of a shader, out example application will track an access path as a singly-linked list of variable layouts that points up the stack, from the deepest variable to the shallowest:
1244
1245```c++
1246struct AccessPathNode
1247{
1248    slang::VariableLayoutReflection* varLayout;
1249    AccessPathNode* outer;
1250};
1251
1252struct AccessPath
1253{
1254    AccessPathNode* leafNode = nullptr;
1255};
1256```
1257
1258For the example code above, if our recursive traversal is at `params.material.glossMap`, then the access path will start with a node for `glossMap` which points to a node for `material`, which points to a node for `glossMap`.
1259
1260For many layout units, we can calculate a cumulative offset simply by summing up contributions along the entire access path, with logic like the following:
1261
1262```c++
1263CumulativeOffset calculateCumulativeOffset(slang::ParameterCategory layoutUnit, AccessPath accessPath)
1264{
1265    // ...
1266    for(auto node = accessPath.leafNode; node != nullptr; node = node->outer)
1267    {
1268        result.value += node->varLayout->getOffset(layoutUnit);
1269        result.space += node->varLayout->getBindingSpace(layoutUnit);
1270    }
1271    // ...
1272}
1273```
1274
1275Once our example application is properly tracking access paths, we will be able to use them to calculate and print the cumulative offsets of variable layouts:
1276
1277```c++
1278void printOffsets(
1279    slang::VariableLayoutReflection* varLayout,
1280    AccessPath accessPath)
1281{
1282    // ...
1283
1284    print("cumulative:");
1285    for (int i = 0; i < usedLayoutUnitCount; ++i)
1286    {
1287        print("- ");
1288        auto layoutUnit = varLayout->getCategoryByIndex(i);
1289        printCumulativeOffset(varLayout, layoutUnit, accessPath);
1290    }
1291}
1292```
1293
1294Printing the cumulative offset of a variable layout requires adding the offset information for the variable itself to the offset calculated from its access path:
1295
1296```c++
1297void printCumulativeOffset(
1298    slang::VariableLayoutReflection* varLayout,
1299    slang::ParameterCategory layoutUnit,
1300    AccessPath accessPath)
1301{
1302    CumulativeOffset cumulativeOffset = calculateCumulativeOffset(layoutUnit, accessPath);
1303
1304    cumulativeOffset.offset += varLayout->getOffset(layoutUnit);
1305    cumulativeOffset.space += varLayout->getBindingSpace(layoutUnit);
1306
1307    printOffset(layoutUnit, cumulativeOffset.offset, cumulativeOffset.space);
1308}
1309```
1310
1311### Tracking Access Paths
1312
1313In order to support calculation of cumulative offsets, the various functions we've presented so far like `printVarLayout()` and `printTypeLayout()` need to be extended with an additional parameter for an `AccessPath`.
1314For example, the signature of `printTypeLayout()` becomes:
1315
1316```c++
1317void printTypeLayout(slang::TypeLayoutReflection* typeLayout, AccessPath accessPath)
1318{
1319    // ...
1320}
1321```
1322
1323#### Variable Layouts
1324
1325When traversing a variable layout, we then need to extend the access path to include the additional variable layout, before traversing down into its type layout:
1326
1327```c++
1328void printVarLayout(slang::VariableLayoutReflection* typeLayout, AccessPath accessPath)
1329{
1330    // ...
1331
1332    ExtendedAccessPath varAccessPath(accessPath, varLayout);
1333
1334    print("type layout: ");
1335    printTypeLayout(varLayout->getTypeLayout(), varAccessPath);
1336}
1337```
1338
1339#### Scopes
1340
1341Similar logic is needed within `printScope()` in our example program:
1342
1343```c++
1344void printScope(
1345    slang::VariableLayoutReflection* scopeVarLayout,
1346    AccessPath                       accessPath)
1347{
1348    ExtendedAccessPath scopeAccessPath(accessPath, scopeVarLayout);
1349
1350    // ...
1351}
1352```
1353
1354The calls to `printOffsets()`, `printTypeLayout()`, etc. inside of `printScope()` will then pass along the extended access path.
1355
1356#### Array-Like Types
1357
1358When the traversing an array, matrix, or vector type, it is impossible to compute a single cumulative offset that is applicable to all elements of the type.
1359The recursive calls to `printTypeLayout()` in these cases will simply pass in an empty `AccessPath`.
1360For example:
1361
1362```c++
1363case slang::TypeReflection::Kind::Array:
1364    {
1365        // ...
1366
1367        print("element type layout: ");
1368        printTypeLayout(
1369            typeLayout->getElementTypeLayout(),
1370            AccessPath());
1371    }
1372    break;
1373```
1374
1375### Handling Single-Element Containers
1376
1377Types like constant buffers and parameter blocks add complexity that requires additions to our representation and handling of access paths.
1378
1379First, when calculating the cumulative byte offset of variables inside a constant buffer (or any of these single-element container types), it is important not to sum contributions too far up the access path.
1380Consider this example:
1381
1382```c++
1383struct A
1384{
1385    float4 x;
1386    Texture2D t;
1387}
1388struct B
1389{
1390    float4 y;
1391    ConstantBuffer<Inner> a;
1392}
1393struct C
1394{
1395    float4 z;
1396    Texture2D t;
1397    B b;
1398}
1399uniform C c;
1400```
1401
1402When compiling for D3D12, the cumulative byte offset of `c.b` is 16, but the cumulative byte offset of `c.b.a.x` needs to be zero, because its byte offset should be measured relative to the enclosing constant buffer `c.b.a`.
1403In contrast, the cumulative of offset of `c.b` in `t` registers is one, and the cumulative offset of `c.b.a.t` needs to be two.
1404
1405Similarly, when calculating the cumulative offsets of variables inside a parameter block (for targets that can allocate each parameter block its own space), it is important not to sum contributions past an enclosing parameter block.
1406
1407We can account for these subtleties by extending the representation of access paths in our example application to record the node corresponding to the deepest constant buffer or parameter block along the path:
1408
1409```c++
1410struct AccessPath
1411{
1412    AccessPathNode* leaf = nullptr;
1413    AccessPathNode* deepestConstantBuffer = nullptr;
1414    AccessPathNode* deepestParameterBlock = nullptr;
1415};
1416```
1417
1418Now when traversing a single-element container type in `printTypeLayout`, we can make a copy of the current access path and modify its `deepestConstantBuffer` to account for the container:
1419
1420```c++
1421case slang::TypeReflection::Kind::ConstantBuffer:
1422case slang::TypeReflection::Kind::ParameterBlock:
1423case slang::TypeReflection::Kind::TextureBuffer:
1424case slang::TypeReflection::Kind::ShaderStorageBuffer:
1425    {
1426        // ...
1427
1428        AccumulatedOffsets innerAccessPath = accessPath;
1429        innerAccessPath.deepestConstantBuffer = innerAccessPath.leaf;
1430
1431        // ...
1432    }
1433    break;
1434```
1435
1436Further, if the container had a full space allocated to it, then we also update the `deepestParameterBlock`:
1437
1438```c++
1439// ...
1440if (containerVarLayout->getTypeLayout()->getSize(
1441    slang::ParameterCategory::SubElementRegisterSpace) != 0)
1442{
1443    innerAccessPath.deepestParameterBlock = innerAccessPath.leaf;
1444}
1445// ...
1446```
1447
1448Finally, when traversing the element of the container, we need to use this new `innerAccessPath`, and also extend the access path when traversing into the type layout of the element:
1449
1450```c++
1451print("element: ");
1452printOffsets(elementVarLayout, innerAccessPath);
1453
1454ExtendedAccessPath elementAccessPath(innerAccessPath, elementVarLayout);
1455
1456print("type layout: ");
1457printTypeLayout(
1458    elementVarLayout->getTypeLayout(),
1459    elementAccessPath);
1460```
1461
1462### Accumulating Offsets Along An Access Path
1463
1464We now understand that the proper way to calculate a cumulative offset depends on the layout unit:
1465
1466```c++
1467CumulativeOffset calculateCumulativeOffset(
1468    slang::ParameterCategory layoutUnit,
1469    AccessPath               accessPath)
1470{
1471    switch(layoutUnit)
1472    {
1473    // ...
1474    }
1475}
1476```
1477
1478#### Layout Units That Don't Require Special Handling
1479
1480By default, relative offsets will be summed for all nodes along the access path:
1481
1482```c++
1483default:
1484    for (auto node = accessPath.leaf; node != nullptr; node = node->outer)
1485    {
1486        result.offset += node->varLayout->getOffset(layoutUnit);
1487    }
1488    break;
1489```
1490
1491#### Bytes
1492
1493When a byte offset is being computed, relative offsets will only be summed up to the deepest enclosing constant buffer, if any:
1494
1495```c++
1496case slang::ParameterCategory::Uniform:
1497    for (auto node = accessPath.leaf; node != accessPath.deepestConstantBuffer; node = node->outer)
1498    {
1499        result.offset += node->varLayout->getOffset(layoutUnit);
1500    }
1501    break;
1502```
1503
1504#### Layout Units That Care About Spaces
1505
1506Finally, we need to handle the layout units that care about spaces:
1507
1508```c++
1509case slang::ParameterCategory::ConstantBuffer:
1510case slang::ParameterCategory::ShaderResource:
1511case slang::ParameterCategory::UnorderedAccess:
1512case slang::ParameterCategory::SamplerState:
1513case slang::ParameterCategory::DescriptorTableSlot:
1514    // ...
1515    break;
1516```
1517
1518Relative offsets, including space offsets, need to be summed along the access path up to the deepest enclosing parameter block, if any:
1519
1520```c++
1521for (auto node = accessPath.leaf; node != accessPath.deepestParameterBlock; node = node->outer)
1522{
1523    result.offset += node->varLayout->getOffset(layoutUnit);
1524    result.space += node->varLayout->getBindingSpace(layoutUnit);
1525}
1526```
1527
1528Additionally, the offset of the enclosing parameter block in spaces needs to be added to the space of the cumulative offset:
1529
1530```c++
1531for (auto node = accessPath.deepestParameterBlock; node != nullptr; node = node->outer)
1532{
1533    result.space += node->varLayout->getOffset(slang::ParameterCategory::SubElementRegisterSpace);
1534}
1535```
1536
1537Determining Whether Parameters Are Used
1538---------------------------------------
1539
1540Some application architectures make use of shader code that declares a large number of shader parameters at global scope, but only uses a small fraction of those parameters at runtime.
1541Similarly, shader parameters may be declared at global scope even if they are only used by a single entry point in a pipeline.
1542These kinds of architectures are not ideal, but they are pervasive.
1543
1544Slang's base reflection API *intentionally* does not provide information about which shader parameters are or are not used by a program, or specific entry points.
1545This choice ensures that applications using the reflection API can robustly re-use data structures built from reflection data across hot reloads of shaders, or switches between variants of a program.
1546
1547Applications that need to know which parameters are used (and by which entry points or stages) need to query for additional metadata connected to the entry points of their compiled program using `IComponentType::getEntryPointMetadata()`:
1548
1549```c++
1550slang::IComponentType* program = ...;
1551slang::IMetadata* entryPointMetadata;
1552program->getEntryPointMetadata(
1553        entryPointIndex,
1554        0, // target index
1555        &entryPointMetadata);
1556```
1557
1558When traversal of reflection data reaches a leaf parameter, the application can use `IMetadata::isParameterLocationUsed()` with the absolute location of that parameter for a given layout unit:
1559
1560```c++
1561unsigned calculateParameterStageMask(
1562    slang::ParameterCategory layoutUnit,
1563    CumulativeOffset offset)
1564{
1565    unsigned mask = 0;
1566    for(int i = 0; i < entryPointCount; ++i)
1567    {
1568        bool isUsed = false;
1569        entryPoints[i].metadata->isParameterLocationUsed(
1570            layoutUnit, offset.space, offset.value, isUsed);
1571        if(isUsed)
1572        {
1573            mask |= 1 << unsigned(entryPoints[i].stage);
1574        }
1575    }
1576    return mask;
1577}
1578```
1579
1580The application can then incorporate this logic into a loop over the layout units consumed by a parameter:
1581
1582```c++
1583unsigned calculateParameterStageMask(
1584    slang::VariableLayoutReflection* varLayout,
1585    AccessPath accessPath)
1586{
1587    unsigned mask = 0;
1588
1589    int usedLayoutUnitCount = varLayout->getCategoryCount();
1590    for (int i = 0; i < usedLayoutUnitCount; ++i)
1591    {
1592        auto layoutUnit = varLayout->getCategoryByIndex(i);
1593        auto offset = calculateCumulativeOffset(
1594            varLayout, layoutUnit, accessPath);
1595        
1596        mask |= calculateStageMask(
1597            layoutUnit, offset);
1598    }
1599
1600    return mask;
1601}
1602```
1603
1604Finally, we can wrap all this up into logic to print which stage(s) use a given parameter, based on the information in the per-entry-point metadata:
1605
1606```c++
1607void printVarLayout(
1608    slang::VariableLayoutReflection* varLayout,
1609    AccessPath accessPath)
1610{
1611    //...
1612    unsigned stageMask = calculateStageMask(
1613        varLayout, accessPath);
1614
1615    print("used by stages: ");
1616    for(int i = 0; i < SLANG_STAGE_COUNT; i++)
1617    {
1618        if(stageMask & (1 << i))
1619        {
1620            print("- ");
1621            printStage(SlangStage(i));
1622        }
1623    }
1624    // ...
1625}
1626```
1627
1628Conclusion
1629----------
1630
1631At this point we have provided a comprehensive example of how to robustly traverse the information provided by the Slang reflection API to get a complete picture of the shader parameters of a program, and what target-specific locations they were bound to.
1632We hope that along the way we have also imparted some key parts of the mental model that exists behind the reflection API and its representations.