DirectX 容器¶
概述¶
DirectX 容器 (DXContainer) 文件格式是为面向 DirectX 运行时的编译着色器而设计的二进制文件格式。该文件格式也称为 DXIL 容器或 DXBC 文件格式。由于该文件格式可用于包含 DXIL 或 DXBC 编译着色器,因此 LLVM 中的命名法简称为 DirectX 容器。
DirectX 容器文件由编译器和相关工具以及 DirectX 运行时、性能分析工具和其他用户读取。本文档作为 LLVM 中实现的补充,以更完整地记录该文件格式,供其众多用户使用。
基本结构¶
DXContainer 文件以文件头开始,然后是一系列“部件”,这些部件类似于目标文件节。每个部件都包含一个部件头,以及部件头后的一些字节的数据,这些数据采用定义的格式。
DX 容器数据结构在二进制文件中以小端字节序编码。
本文档中描述和/或引用的所有数据结构的 LLVM 版本都定义在 llvm/include/llvm/BinaryFormat/DXContainer.h 中。下面的一些伪代码块旨在帮助理解本文档,但结合头文件阅读将提供最清晰的理解。
文件头¶
struct Header {
uint8_t Magic[4];
uint8_t Digest[16];
uint16_t MajorVersion;
uint16_t MinorVersion;
uint32_t FileSize;
uint32_t PartCount;
};
DXContainer 文件头与上面的伪定义匹配。它以一个四字符代码(魔数)开始,其值为 DXBC
,用于表示文件格式。
Digest
是一个 128 位哈希摘要,使用专有算法计算,并由字节码验证器编码到二进制文件中。
MajorVersion
和 MinorVersion
编码文件格式版本 1.0
。
其余字段编码 32 位无符号整数,分别表示文件大小和部件数量。
部件头之后是一个 PartCount
个 32 位无符号整数的数组,指定每个部件头的偏移量。
部件数据¶
struct PartHeader {
uint8_t Name[4];
uint32_t Size;
}
每个部件都以一个部件头开始。部件头包括 4 字符的部件名称和一个 32 位无符号整数,用于指定部件数据的大小。部件头之后是 Size
字节的数据,构成该部件。该格式没有明确要求部件进行 32 位对齐,尽管 LLVM 在编写器代码中实现了此限制(因为这是一个好主意)。LLVM 对象读取器代码不假设输入已正确对齐,以避免由其他编译器生成的未对齐输入引起的未定义行为。
部件格式¶
部件名称指示部件数据的格式。DXC 和 FXC 使用 24 个部件头。并非所有编译后的着色器都包含所有部件。在下面的列表中,仅由 DXC 生成的部件标记为 †,仅由 FXC 生成的部件标记为 *。
DXIL† - 存储 DXIL 字节码。
HASH† - 存储着色器 MD5 哈希值。
ILDB† - 存储 DXIL 字节码,其中 LLVM 调试信息嵌入在模块中。
ILDN† - 存储着色器调试名称,用于外部调试信息。
ISG1 - 存储 Shader Model 5.1+ 的输入签名。
ISGN* - 存储 Shader Model 4 及更早版本的输入签名。
OSG1 - 存储 Shader Model 5.1+ 的输出签名。
OSG5* - 存储 Shader Model 5 的输出签名。
OSGN* - 存储 Shader Model 4 及更早版本的输出签名。
PCSG* - 存储 Shader Model 5.1 及更早版本的补丁常量签名。
PDBI† - 存储 PDB 信息。
PRIV - 存储任意私有数据(FXC 或 DXC 均未编码)。
PSG1 - 存储 Shader Model 6+ 的补丁常量签名。
PSV0 - 存储管线状态验证数据。
RDAT† - 存储运行时数据。
RDEF* - 存储资源定义。
RTS0 - 存储编译后的根签名。
SFI0 - 存储着色器特性标志。
SHDR* - 存储编译后的 DXBC 字节码。
SHEX* - 存储编译后的 DXBC 字节码。
DXBC* - 存储编译后的 DXBC 字节码。
SRCI† - 存储着色器源信息。
STAT† - 存储着色器统计信息。
VERS† - 存储着色器编译器版本信息。
DXIL 部件¶
DXIL 部件由三个数据结构组成:ProgramHeader
、BitcodeHeader
和位码序列化的 LLVM 3.7 IR 模块。
ProgramHeader
包含着色器模型版本和管线阶段枚举值。这标识了包含的着色器位码的目标配置文件。
BitcodeHeader
包含 DXIL 版本信息,并引用位码数据的起始位置。
HASH 部件¶
HASH 部件包含一个 32 位无符号整数(着色器哈希标志)和一个 128 位 MD5 哈希摘要。标志字段的值可以是 0
(表示没有标志)或 1
(表示文件哈希值是在包含生成二进制文件的源代码的情况下计算的)。
程序签名 (SG1) 部件¶
struct ProgramSignatureHeader {
uint32_t ParamCount;
uint32_t FirstParamOffset;
}
程序签名部件(ISG1、OSG1 和 PSG1)都使用相同的数据结构来编码输入、输出和补丁信息。ProgramSignatureHeader
包括两个 32 位无符号整数,用于指定签名参数的数量和第一个参数的偏移量。
从 ProgramSignatureHeader
的起始位置开始,偏移 FirstParamOffset
字节处,写入 ParamCount
个 ProgramSignatureElement
结构。在 ProgramSignatureElements
之后是一个字符串表,其中包含以 null 结尾的字符串,并填充到 32 字节对齐。此字符串表与 LLVM 实现的 DWARF 字符串表格式匹配。
每个 ProgramSignatureElement
编码一个 NameOffset
值,该值指定字符串表中的偏移量。值为 0
表示没有名称。此处编码的偏移量是从 ProgramSignatureHeader
的起始位置开始计算的,而不是从字符串表的起始位置开始计算的。
ProgramSignatureElement
包含多个枚举字段,这些字段在 llvm/include/llvm/BinaryFormat/DXContainerConstants.def 中定义。这些字段编码 D3D 系统值、数据类型及其精度要求。
PSV0 部件¶
管线状态验证数据编码版本化的运行时信息结构。这些结构使用一种方案,其中不编码版本号,而是编码结构的大小,并且结构的每个新版本都是累加式的。这允许读取器通过将编码的大小与已知结构的大小进行比较来推断结构的版本。如果编码的大小大于任何已知结构,则最大的已知结构可以有效地解析已知结构中表示的数据。
在 LLVM 中,我们使用 llvm::dxbc::PSV
命名空间下的版本化命名空间(例如 v0
、v1
)表示相关数据结构的版本。v0
命名空间中的每个结构都是基本版本,v1
命名空间中的结构继承自 v0
命名空间,v2
命名空间中的结构继承自 v1
结构,依此类推。
PSV 数据的高级结构是
RuntimeInfo
结构资源绑定
签名元素
掩码向量(输出、输入、输入补丁、补丁输出)
紧随 PSV0 部件的部件头之后是一个 32 位无符号整数,用于指定后续 RuntimeInfo
结构的大小。
紧随 RuntimeInfo
结构之后是一个 32 位无符号整数,用于指定资源绑定的数量。如果资源数量大于零,则会再跟随一个无符号 32 位整数,用于指定 ResourceBindInfo
结构的大小。之后是指定数量的指定大小的结构(这推断出结构的版本)。
对于数据版本 0,这结束了部件数据。
PSV0 签名元素¶
签名元素在概念上是一个单一的概念,但数据编码在三个不同的块中。第一个块是字符串表,第二个块是索引表,第三个块是元素本身,这些元素又按输入、输出和补丁常量或基元元素分隔。
签名元素捕获了 SG1 部件中捕获的许多相同数据。索引表的使用允许对数据进行去重,以获得更紧凑的最终表示形式。
字符串表以一个 32 位无符号整数开始,用于指定表大小。此字符串表使用 LLVM 中实现的 DXContainer 格式。此格式在字符串表前缀一个空字节,以便偏移量 0
是一个空字符串,并填充到 32 字节对齐。
索引表以一个 32 位无符号整数开始,用于指定表的大小,之后是多个 32 位无符号整数,表示该表。索引表可以去重重复序列,也可以不去重(DXC 和 Clang 都会去重)。索引表示签名元素描述的扁平化聚合表示形式中的索引。单个语义可能在此表中包含多个条目,以表示其成员的不同属性。
例如,给定以下代码
struct VSOut_1
{
float4 f3 : VOUT2;
float3 f4 : VOUT3;
};
struct VSOut
{
float4 f1 : VOUT0;
float2 f2[4] : VOUT1;
VSOut_1 s;
int4 f5 : VOUT4;
};
void main(out VSOut o1 : A) {
}
语义 A
扩展为 5 个输出签名元素。这些元素是
注意
在下面的示例中,行与索引匹配是一种巧合,在具有多个语义的更复杂示例中,情况并非如此。
索引 0 从第 0 行开始,包含 4 列,并且是 float32。这表示源代码中的
f1
。索引 1、2、3 和 4 从第 1 行开始,包含两列,并且是 float32。这表示源代码中的
f2
,它跨越第 1 - 4 行。索引 5 从第 5 行开始,包含 4 列,并且是 float32。这表示源代码中的
f3
。索引 6 从第 6 行开始,包含 3 列,并且是 float32。这表示
f4
。索引 7 从第 7 行开始,包含 4 列,并且是有符号 32 位整数。这表示源代码中的
f5
。
LLVM obj2yaml
工具可以从 PSV 中解析出此数据,并以人类可读的 YAML 格式呈现。对于上面的示例,它产生以下输出
SigOutputElements:
- Name: A
Indices: [ 0 ]
StartRow: 0
Cols: 4
StartCol: 0
Allocated: true
Kind: Arbitrary
ComponentType: Float32
Interpolation: Linear
DynamicMask: 0x0
Stream: 0
- Name: A
Indices: [ 1, 2, 3, 4 ]
StartRow: 1
Cols: 2
StartCol: 0
Allocated: true
Kind: Arbitrary
ComponentType: Float32
Interpolation: Linear
DynamicMask: 0x0
Stream: 0
- Name: A
Indices: [ 5 ]
StartRow: 5
Cols: 4
StartCol: 0
Allocated: true
Kind: Arbitrary
ComponentType: Float32
Interpolation: Linear
DynamicMask: 0x0
Stream: 0
- Name: A
Indices: [ 6 ]
StartRow: 6
Cols: 3
StartCol: 0
Allocated: true
Kind: Arbitrary
ComponentType: Float32
Interpolation: Linear
DynamicMask: 0x0
Stream: 0
- Name: A
Indices: [ 7 ]
StartRow: 7
Cols: 4
StartCol: 0
Allocated: true
Kind: Arbitrary
ComponentType: SInt32
Interpolation: Constant
DynamicMask: 0x0
Stream: 0
每种类型的签名元素数量编码在 llvm::dxbc::PSV::v1::RuntimeInfo
结构中。如果任何元素计数值非零,则接下来编码 ProgramSignatureElement
结构的大小,以允许该结构的版本控制。目前只有一个版本。在大小字段之后,是指定数量的签名元素,顺序为输入、输出,然后是补丁常量或基元。
在签名元素之后是一系列掩码向量,编码为一系列 32 位整数。掩码中的每个 32 位整数编码 8 个输入/输出/补丁或基元元素的值。掩码向量从最低有效位到最高有效位填充,每个添加的元素都会将之前的元素左移。读取器需要查阅 RuntimeInfo
结构中编码的向量总数,以了解如何读取掩码向量。
如果着色器在 RuntimeInfo
中启用了 UsesViewID
,则将包含输出掩码向量。输出掩码向量是四个 32 位无符号整数数组。四个数组中的每一个都对应于一个输出流。几何着色器最多有四个输出流,所有其他着色器阶段仅支持一个输出流。掩码向量中的每个位都标识来自输出签名的输出的一列是否依赖于 ViewID。
如果着色器启用了 UsesViewID
,它是外壳着色器,并且它具有补丁常量或基元向量元素,则将包含补丁常量或基元向量掩码。它在结构上与输出掩码向量相同。掩码向量中的每个位都标识补丁常量输出的一列是否依赖于 ViewID。
接下来的系列掩码向量在结构上与输出掩码向量相似,但它们包含一个额外的维度。
如果着色器具有输入和输出,则接下来编码输出/输入映射。输出/输入掩码编码每个输入的每一列影响哪些输出。每个掩码向量的大小是输出最大向量的大小 * 输入数量 * 4(对于每个组件)。掩码向量中的每个位都标识输出的一列和输入的一列。值为 1 表示输出受输入影响。
如果着色器是外壳着色器,并且它具有输入和补丁输出,则接下来将包含输入到补丁的映射。这在结构上与输出/输入映射相同。维度由补丁常量或基元向量掩码的大小 * 输入数量 * 4(对于每个组件)定义。掩码向量中的每个位都标识补丁常量输出的一列和输入的一列。值为 1 表示输出受输入影响。
如果着色器是域着色器,并且它具有输出和补丁输出,则接下来将包含输出补丁映射。这在结构上与输出/输入映射相同。维度由补丁常量或基元向量掩码的大小 * 输出数量 * 4(对于每个组件)定义。掩码向量中的每个位都标识补丁常量输入的一列和输出的一列。值为 1 表示输出受基元输入影响。
SFI0 部件¶
SFI0 部件编码 64 位无符号整数位掩码的特性标志。这表示着色器所需的哪些可选特性。标志值在 llvm/include/llvm/BinaryFormat/DXContainerConstants.def 中定义。