07-使用反射API

本篇将看看如何使用Slang 提供的反射API。

我只浅显看过OpenGL,对Vulkan和DX12尚不理解,因此文中可能会出现错误的地方,还请大佬指正。待我工作后有时间的话,再学学这两个图形API吧。

唉,工作真难找。。。

使用反射API

获取反射信息

使用反射API前需要通过C++ API进行程序编译,然后使用getLayout()获取反射信息:

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

类型与变量

GPU着色器编程的一个特性是,相同类型的布局可能不同。因此这里先介绍如何反射类型和变量,然后介绍如何反射布局信息。

变量

VariableReflection代表输入程序中的一个变量声明,包含全局着色器参数,struct的成员变量,以及入口程序参数。

VariableReflection并不包含布局信息,因此可以简单反射如下:

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

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

类型

TypeReflection代表输入程序中的一些类型,包括数组,用户定义结构体,以及一些如int的内建类型。可通过TypeReflection::Kind获取类型枚举。

TypeReflection不包含布局信息,因此可以反射如下:

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

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

    // ...
}

那么目前给定一个float x;,应该能输出如下反射信息:

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

之前提到类型枚举,可以用这些枚举针对每个类型特化反射操作:

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

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

    // ...
    }
}

接下来看看一些常用到的类型枚举:

  • 标量类型:可通过TypeReflection::Kind::Scalar查看。

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

    其中,用getScalarType()可以获得更精确的标量类型枚举slang::ScalarType,例如slang::ScalarType::UInt64slang::ScalarType::Void等。

  • 结构体类型:可通过TypeReflection::Kind::Struct查看。一个结构体类型中可以有0个或多个成员,个数可由getFieldCount()获得;每个成员都有一个VariableReflection,可通过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;
  • 数组类型:对于像int[3]之类的数组类型,可由通过getElementCount()getElementType()获取相关反射信息。

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

    需要注意的是,有些数组没有定义容量,那么它getElementCount()结果就是size_t类型的最大值:

    void printPossiblyUnbounded(size_t value)
    {
        if (value == ~size_t(0))
        {
            printf("unbounded");
        }
        else
        {
            printf("%u", unsigned(value));
        }
    }
  • 向量:和数组类似。

    case slang::TypeReflection::Kind::Vector:
        {
            print("element count: ");
            printCount(type->getElementCount());
    
            print("element type: ");
            printType(type->getElementType());
        }
        break;
  • 矩阵:可通过getRowCount()获取行元素数;通过getColumnCount()获取列元素数;通过getElementType()获取元素类型信息。

    case slang::TypeReflection::Kind::Matrix:
        {
            print("row count: ");
            printCount(type->getRowCount());
    
            print("column count: ");
            printCount(type->getColumnCount());
    
            print("element type: ");
            printType(type->getElementType());
        }
        break;
  • 资源类型:有许多种类的资源,例如简单的TextureCubeStructuredBuffer<int>;也有复杂的RasterizerOrderedTexture2DArray<int4>AppendStructuredBuff<Stuff>。Slang的反射API将资源类型信息分为shapeaccessresult type三类。

    case slang::TypeReflection::Kind::Resource:
        {
            key("shape"); //输出 shape : 
            printResourceShape(type->getResourceShape());
    
            key("access");
            printResourceAccess(type->getResourceAccess());
    
            key("result type");
            printType(type->getResourceResultType());
        }
        break;

    资源的result type就是在该资源中读取数据的类型。例如StructuredBuffer<Thing>result type就是Thing,没有显式声明的Texture2Dresult type默认为float4

    资源的accessSlangResourceAccess类型)代表资源中的元素如何在着色器代码中被访问。对于Slang的资源类型,访问权限一般显式标注在资源类型的前缀上。例如,一个无前缀的Texture2D有只读权限(SLANG_RESOURCE_ACCESS_READ),而RWTexture2D有读写权限(SLANG_RESOURCE_ACCESS_READ_WRITE)。

    资源的shapeSlangResourceShape)类型描述该资源的秩/维度,以及该资源的索引方式。对于Slang的资源类型,在访问权限前缀后的就是资源的shape了。资源的shape由一个base shape和若干可能出现的前缀组成:

    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");
        // ...
    }
  • 单元素容器:像ConstantBuffer<T>ParameterBlock<T>这种类似数组或结构Buffer但只有一个元素的类型,可通过getElementType()获取元素类型信息。

    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;

布局信息

Slang反射API提供VariableLayoutReflectionTypeLayoutReflection代表变量和类型的布局信息。相同的类型可能有不同的布局信息。

变量的布局信息

VariableLayoutReflection表示提供由变量反射信息计算出来的布局。可通过getVariable()获取变量反射信息,此外还提供了一些访问重要属性的接口。

变量布局信息存储变量的偏移量,以及一个类型布局信息:

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

    printRelativeOffsets(varLayout);

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

其中,

  • 偏移量:由VariableLayoutReflection存储,是相对于最近结构体、作用域或其他上下文的。可通过getOffset()获取。

    void printOffset(
        slang::VariableLayoutReflection* varLayout,
        slang::ParameterCategory layoutUnit)
    {
        size_t offset = varLayout->getOffset(layoutUnit);
    
        print("value: "); print(offset);
        print("unit: "); printLayoutUnit(layoutUnit);
    
        // ...
    }

    其中,layoutUnit的个数和内容可通过getCategoryCount()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);
        }
    }
  • 空间/集合:对于平台的不同,可能还会额外包括空间/集合上的偏移信息。例如一个Vulkan/SPIR-V的Descriptor set,一个D3D12/DXIL的register space,或者一个WebGPU/WGSL的binding group。Slang将它们都统称为空间。

    空间上的偏移可通过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);    
        }
    }

    这里我还不大理解,等我接触学习了Vulkan,DX12后应该就能理解了。

类型的布局信息

TypeLayoutReflection表示由类型信息计算出来的布局。可通过getType()获取类型,也提供了访问常见类型布局信息的接口。

类型布局信息主要存储该类型的大小等信息:

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

    printSizes(typeLayout);

    // ...
}

其中,

  • 大小:和变量布局类似,通过getSize()获取类型的大小信息也需要layoutUnit帮忙。

    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);
        }
    
        // ...
    }
    
    void printSize(
        slang::TypeLayoutReflection* typeLayout,
        slang::ParameterCategory layoutUnit)
    {
        size_t size = typeLayout->getSize(layoutUnit);
    
        key("value"); printPossiblyUnbounded(size);
        key("unit"); writeLayoutUnit(layoutUnit);
    }

    需要注意的是,类型的大小可能是无限的,此时会返回size_t类型的最大值~size_t(0)

  • Alignment 和 Stride:类型布局也能反射类型的对齐量(Alignment),通过TypeLayoutReflection::getAlignment(),仅在布局单元是字节(slang::ParameterCategory::Uniform)时有效。

    类型布局还能反射它的跨度(Stride),用于标识两个相同元素间的”距离“。通过TypeLayoutReflection::getStride()获取。

    void printTypeLayout(slang::TypeLayoutReflection* typeLayout)
    {
        // ...
    
        if(typeLayout->getSize() != 0)
        {
            print("alignment in bytes: ");
            print(typeLayout->getAlignment());
    
            print("stride in bytes: ");
            print(typeLayout->getStride());
        }
    
        // ...
    }

    以上获取的大小,对齐量和跨度的单位均为字节。

  • 类型特定信息:和之前类型的反射类似,特定类型布局有存储特定的信息,需要分类处理:

    void printTypeLayout(slang::TypeLayoutReflection* typeLayout)
    {
        // ...
    
        switch(typeLayout->getKind())
        {
        default:
            break;
        
            // ...
        }
    }

    对于结构体类型,可以获得成员变量布局相关信息:

    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;

    结构体变量布局存储的偏移量相对于这个结构体的起始位置。

    对于数组类型,可以获得元素的个数和类型布局信息:

    case slang::TypeReflection::Kind::Array:
        {
            print("element count: ");
            printPossiblyUnbounded(typeLayout->getElementCount());
    
            print("element type layout: ");
            printTypeLayout(typeLayout->getElementTypeLayout());
        }
        break;

    对于 矩阵类型,可以获得它是行主序布局还是列主序布局(SlangMatrixLayoutMode):

    case slang::TypeReflection::Kind::Matrix:
        {
            // ...
    
            print("matrix layout mode: ");
            printMatrixLayoutMode(typeLayout->getMatrixLayoutMode());
        }
        break;

    需要注意的是,如果Slang反射一个行主序的矩阵,生成的SPIR-V代码中实际上是列主序的矩阵

    对于 单元素容器 类型(如ConstantBuffer<Thing>ParameterBlock<Thing>),包含了容器内元素的布局信息和容器本身的布局信息。其中,容器本身的布局信息被存储为一个变量布局,意义是哪些内存被分配了进而形成容器本身,通过getContainerVarLayout()获取;元素的布局信息也被存储为一个变量布局,意义是元素在容器内的布局,通过getElementVarLayout()获取。

    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;

    注意到这里不用刚刚编写的printVarLayout()输出容器和元素布局信息,这是因为它们的大多数信息都是无效的,主要信息就是偏移量。

为了方便理解,最后来个大例子总结一下,假设要反射的代码如下:

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;

首先是变量params的基础信息(目标平台为Vulkan):

- 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

param的大小是一个Register Space(Vulkan中为一个Descriptor Set),因此空间偏移是1。

接下来是容器ParameterBlock<>的偏移反射信息:

这里我还不大理解,等我接触学习了Vulkan,DX12后应该就能理解了。

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

可以发现,容器内分配了两个东西:一个Descriptor Set(ParameterCategory::SubElementRegisterSpace)和一个与它绑定的值(ParameterCategory::DescriptorTableSlot),为了自动引入ConstantBuffer<Material>

最后是元素类型的FrameParams的反射信息:

这里我还不大理解,等我接触学习了Vulkan,DX12后应该就能理解了。

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
      ...

程序和作用域

在上面的内容中,我们初步了解如何递归地获取Slang代码的反射信息,但我们还不知道怎么开始递归。接下来看看如何开始反射程序的顶层参数。

编译和链接完 一个Slang程序后,就能使用IComponentType::getLayout()获取到ProgramLayout类型的反射信息。其中包含全局作用域,以及0个或多个程序入口点:

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));
    }
}

在反射API中,通过VariableLayoutReflection代表作用域。

全局作用域

为了了解Slang反射API是如何暴露全局作用域的,有必要去看看Slang编译器对待全局作用域的着色器参数是怎么处理的。

包装参数到结构体中

如果一个Slang程序声明如下独立的全局参数:

Texture2D diffuseMap;
TextureCube envMap;
SamplerState sampler;

那么编译器会将它们放到一个结构体中,并额外声明单独的结构体类型全局参数:

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

在这种简单的情况下,printScope()可以这样写:

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;

        // ...
    }
}

如有必要,包装Constant Buffer

对于如下着色器参数:

Texture2D diffuseMap;
TextureCube envMap;
SamplerState sampler;

uniform float3 cameraPos;
uniform float3 cameraDir;

编译器将其包装到单独结构体中:

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

    float3 cameraPos;
    float3 cameraDir;
}

发现结构体占用的字节数不是0,对于大多数编译目标来说,编译器将会自动为结构体包一层ConstantBuffer<>

ConstantBuffer<Globals> globals

这种情况下,代码可以这样写:

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

    printOffsets(scopeTypeLayout->getContainerVarLayout());

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

其中,容器变量布局反射了自动引入的ConstantBuffer的相对偏移量;元素变量布局反射了被包裹的全局作用域中的参数。

如有必要,包装Parameter Block

这里我还不大理解,等我接触学习了Vulkan,DX12后应该就能理解了。

对于像D3D12/DXIL,Vulkan/SPIR-V以及WebGPU/WGSL这样的目标平台,大多数着色器参数必须通过一些方式进行绑定(如Descriptor tables,Descriptor sets,Binding groups等)。如果Slang编译器正在对上述平台编译,并且发现一些着色器全局参数没有被显式绑定,就会为其包装一层ParameterBlock<>以提供默认绑定空间。

例如编译如下代码到Vulkan:

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

Slang编译器会发现,envMap已经被显式绑定binding1到空间(Descriptor Set)0中,剩下两个均没有显式绑定。Slang编译器会为剩下两个分配空间1。简单来说,编译器的行为就像是给全局作用域参数包装到结构体中,并再包装到ParameterBlock<>中。

这种情况下,代码可以这样写:

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

    printOffsets(scopeTypeLayout->getContainerVarLayout());

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

程序入口点

EntryPointReflection提供程序入口点的反射信息,包括当前着色器的阶段:

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

    // ...
}

程序入口点参数

一个程序入口点就像一个顶层的着色器参数全局作用域。程序入口点的参数被包裹在一个结构体中,如有需要会再自动包裹到ConstantBuffer<>ParameterBlock<>中。此外,程序入口点还可能额外声明了一个返回类型。如果存在,函数的结果会多一个out参数。

反射代码可以这样写:

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

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

着色器阶段特化

着色器阶段不同,它携带的额外信息也不同,因此可以这样处理:

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

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

例如计算着色器:

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参数

Slang的反射API也支持反射varying参数,这种参数是用来在渲染管线中,着色器不同阶段的传参。

varying着色器参数的变量和类型布局主要有如下用法:

  • 为阶段输入的 Varying 输入槽(slang::ParameterCategory::VaryingInput);
  • out输出参数和程序入口点结果的 Varying 输出槽(slang::ParameterCategory::VaryingOutput);
  • 以上两者均有的inout参数;
  • 无用法,对于系统定义的值(SV_xxxx);

对于用户定义的Varying参数,一些GPU API关心参数的语义。例如下列代码:

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

着色器参数normal有语义NORMAL。语义只与着色器阶段中,用于输入/输出的Varying参数有关。它可以分解为一个名字和一个索引(例如TEXCOORD5的名字是TEXCOORD,索引是5)。语义信息可以通过getSemanticName()getSemanticIndex()反射得到。

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

计算累计偏移量

目前我们只得到变量布局的相对偏移量反射。对于结构体成员来说,偏移量相对于结构体本身。对于顶层参数来说,偏移量相对于它们的作用域,或包裹它们的ConstantBufferParameterBlock

有必要为一些参数计算累计偏移量(或称绝对偏移量)。例如D3D的root signatures,Vulkan的管线布局都需要枚举所有Descriptor Set/Table中绑定的绝对偏移量。

因为一些特定布局单元的偏移量存在额外的绑定空间信息,可以定义累计偏移量结构体如下:

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

访问路径

有多种方式可以跟踪与计算累计偏移量。这里使用较为简单高效的方法,即使在复杂场景中也能产生正确结果。当递归访问着色器参数时,访问的路径就像树一样,因此可以设计数据结构如下:

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

struct AccessPath
{
    AccessPathNode* leafNode = nullptr;
};

在遍历完所有反射信息后,可以按如下方式获取一个layoutUnit的累计偏移量:

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);
    }
    // ...
}

那么varLayout中所有layoutUnit的累计偏移量如下:

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);
    }
}

varLayout本身的累计偏移量如下:

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);
}

代码拓展

接下来拓展代码,让之前的函数添加AccessPath参数。例如printTypeLayout()的函数签名应该是:

void printTypeLayout(slang::TypeLayoutReflection* typeLayout, AccessPath accessPath);

变量布局

当遍历变量布局时,我们需要扩展访问路径以包含额外的变量布局,然后再向下遍历到其类型布局:

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

    ExtendedAccessPath varAccessPath(accessPath, varLayout);

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

作用域

printScope()的拓展逻辑类似:

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

    // ...
}

在此函数内的其他函数(如printOffsets()printTypeLayout())会传递拓展后的访问路径。

类数组类型

当遍历数组、矩阵或向量类型时,无法计算适用于该类型的所有元素的单个累积偏移量。因此这种类型的printTypeLayout()得传入空的AccessPath(),防止继续拓展下去:

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

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

单元素容器

这里没搞明白,还是看官方源码理解吧。

对于ConstantBufferParameterBlock这类单元素容器,代码拓展起来比较复杂。

首先,当计算单元素容器内变量的累计字节偏移时,不去累计计算当前访问路径的总贡献很重要。例如:

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

在D3D12环境下,c.b的累计字节偏移是16,但c.b.a.x的累计字节偏移是0,因为它的字节偏移应该相对于c.b.a进行计算。

为了获得当前路径可能存在的最深ConstantBufferParameterBlock,需要为访问路径结构体额外添加相关信息:

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

并在printTypeLayout()中对单元素容器进行拓展:

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;

如果容器内还存在容器(套娃),要再次更新相关内容:

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

最后,在遍历容器内元素时,需要使用新的 innerAccessPath进行累计偏移计算,从头遍历到现在的accessPath则用于更新访问路径:

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

ExtendedAccessPath elementAccessPath(innerAccessPath, elementVarLayout);

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

积累一条路径上的偏移

现在我们知道通过layoutUnit正确计算累计偏移的方法:

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

接下来看看switch()里头的逻辑:

  • default:默认来说,相对偏移总是路径上所有节点的偏移之和:

    default:
        for (auto node = accessPath.leaf; node != nullptr; node = node->outer)
        {
            result.offset += node->varLayout->getOffset(layoutUnit);
        }
        break;
  • Bytes:当一个字节偏移正在被计算,相对偏移只会是路径上到达deepestconstantBuffer节点的偏移之和:

    case slang::ParameterCategory::Uniform:
        for (auto node = accessPath.leaf; node != accessPath.deepestConstantBuffer; node = node->outer)
        {
            result.offset += node->varLayout->getOffset(layoutUnit);
        }
        break;
  • 带有绑定空间:此外还有带有绑定空间的ParameterCategory

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

    在相对偏移的基础上,还需要计算绑定空间:

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

    此外,对于ParameterBlock,它的累计绑定空间是全局的,得加上所有路径:

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

决定被使用的参数

一些应用程序声明了一大堆全局作用域的着色器参数,但在运行时只用小部分。类似的,着色器参数可以在全局范围内声明,即使它们仅由管道中的单个入口点使用。 这些类型的架构并不理想,但它们无处不在。

Slang的基础反射API没有提供当前程序中哪些参数被使用的功能,因为要保证重用性。

如果应用想要了解哪些着色器参数(在特定程序入口点或阶段)被使用,需要询问连接在编译后程序入口点的IComponentType::getEntryPointMetadata()

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

在遍历着色器参数信息时,可用IMetadata::isParameterLocationUsed()获取该参数是否被使用,这需要提供layoutUnit和参数的绝对位置信息作为参数:

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;
}

接下来对其进行二次包装,通过循环的方式获取VariableLayoutReflection中所有参数变量的使用情况:

unsigned calculateStageMask(
    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 |= calculateParameterStageMask(
            layoutUnit, offset);
    }

    return mask;
}

最后将其集成至printVarLayout()中:

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));
        }
    }
    // ...
}

PS:全部程序详见官方反射示例

参考资料

  • shader-slang/slang: Making it easier to work with shaders (github.com)
  • Using the Reflection API | slang