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::UInt64
,slang::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;
资源类型:有许多种类的资源,例如简单的
TextureCube
和StructuredBuffer<int>
;也有复杂的RasterizerOrderedTexture2DArray<int4>
和AppendStructuredBuff<Stuff>
。Slang的反射API将资源类型信息分为shape
,access
和result 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
,没有显式声明的Texture2D
的result type
默认为float4
。资源的
access
(SlangResourceAccess
类型)代表资源中的元素如何在着色器代码中被访问。对于Slang的资源类型,访问权限一般显式标注在资源类型的前缀上。例如,一个无前缀的Texture2D
有只读权限(SLANG_RESOURCE_ACCESS_READ
),而RWTexture2D
有读写权限(SLANG_RESOURCE_ACCESS_READ_WRITE
)。资源的
shape
(SlangResourceShape
)类型描述该资源的秩/维度,以及该资源的索引方式。对于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提供VariableLayoutReflection
和TypeLayoutReflection
代表变量和类型的布局信息。相同的类型可能有不同的布局信息。
变量的布局信息
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
已经被显式绑定binding
1到空间(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());
}
// ...
}
计算累计偏移量
目前我们只得到变量布局的相对偏移量反射。对于结构体成员来说,偏移量相对于结构体本身。对于顶层参数来说,偏移量相对于它们的作用域,或包裹它们的ConstantBuffer
或ParameterBlock
。
有必要为一些参数计算累计偏移量(或称绝对偏移量)。例如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;
单元素容器
这里没搞明白,还是看官方源码理解吧。
对于ConstantBuffer
,ParameterBlock
这类单元素容器,代码拓展起来比较复杂。
首先,当计算单元素容器内变量的累计字节偏移时,不去累计计算当前访问路径的总贡献很重要。例如:
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
进行计算。
为了获得当前路径可能存在的最深ConstantBuffer
或ParameterBlock
,需要为访问路径结构体额外添加相关信息:
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