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 位哈希摘要,并由字节码验证器在二进制文件中进行编码。

MajorVersionMinorVersion 编码文件格式版本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 生成的部分标记为*。

  1. DXIL† - 存储 DXIL 字节码。

  2. HASH† - 存储着色器 MD5 哈希值。

  3. ILDB† - 存储包含嵌入在模块中的 LLVM 调试信息的 DXIL 字节码。

  4. ILDN† - 存储用于外部调试信息的着色器调试名称。

  5. ISG1 - 存储着色器模型 5.1+ 的输入签名。

  6. ISGN* - 存储着色器模型 4 及更早版本输入签名。

  7. OSG1 - 存储着色器模型 5.1+ 的输出签名。

  8. OSG5* - 存储着色器模型 5 的输出签名。

  9. OSGN* - 存储着色器模型 4 及更早版本输出签名。

  10. PCSG* - 存储着色器模型 5.1 及更早版本的补丁常量签名。

  11. PDBI† - 存储 PDB 信息。

  12. PRIV - 存储任意私有数据(FXC 或 DXC 均未编码)。

  13. PSG1 - 存储着色器模型 6+ 的补丁常量签名。

  14. PSV0 - 存储管道状态验证数据。

  15. RDAT† - 存储运行时数据。

  16. RDEF* - 存储资源定义。

  17. RTS0 - 存储已编译的根签名。

  18. SFI0 - 存储着色器功能标志。

  19. SHDR* - 存储已编译的 DXBC 字节码。

  20. SHEX* - 存储已编译的 DXBC 字节码。

  21. DXBC* - 存储已编译的 DXBC 字节码。

  22. SRCI† - 存储着色器源信息。

  23. STAT† - 存储着色器统计信息。

  24. VERS† - 存储着色器编译器版本信息。

DXIL 部分

DXIL 部分由三个数据结构组成:ProgramHeaderBitcodeHeader 和序列化为 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 字节处,写入ParamCountProgramSignatureElement 结构。在ProgramSignatureElements 后面是一个以 null 终止的字符串表,填充到 32 字节对齐。此字符串表与 LLVM 实现的 DWARF 字符串表格式匹配。

每个ProgramSignatureElement 编码一个NameOffset 值,该值指定字符串表中的偏移量。值为0 表示没有名称。此处编码的偏移量是从ProgramSignatureHeader 的开头,而不是字符串表的开头。

ProgramSignatureElement 包含几个枚举字段,这些字段在llvm/include/llvm/BinaryFormat/DXContainerConstants.def中定义。这些字段编码 D3D 系统值、数据类型及其精度要求。

PSV0 部分

管道状态验证数据编码版本化的运行时信息结构。这些结构使用一种方案,即不编码版本号,而是编码结构的大小,并且每个新版本的结构都是累加的。这允许读取器通过将编码的大小与已知结构的大小进行比较来推断结构的版本。如果编码的大小大于任何已知结构,则最大的已知结构可以有效地解析已知结构中表示的数据。

在 LLVM 中,我们使用llvm::dxbc::PSV 命名空间(例如v0v1)下的版本化命名空间来表示关联数据结构的版本。v0 命名空间中的每个结构都是基本版本,v1 命名空间中的结构继承自v0 命名空间,v2 结构继承自v1 结构,依此类推。

PSV 数据的高级结构为

  1. RuntimeInfo 结构

  2. 资源绑定

  3. 签名元素

  4. 掩码向量(输出、输入、输入补丁、补丁输出)

在 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个输出签名元素。这些元素是

注意

在下面的示例中,行与索引匹配是巧合,在具有多个语义的更复杂示例中并非如此。

  1. 索引0从第0行开始,包含4列,数据类型为float32。这表示源代码中的f1

  2. 索引1、2、3和4从第1行开始,包含两列,数据类型为float32。这表示源代码中的f2,并且它跨越第1-4行。

  3. 索引5从第5行开始,包含4列,数据类型为float32。这表示源代码中的f3

  4. 索引6从第6行开始,包含3列,数据类型为float32。这表示f4

  5. 索引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中定义。