LLVM 中 DXIL 支持的架构和设计¶
简介¶
LLVM 支持读取和写入 DirectX 中间语言,或 DXIL。DXIL 本质上是 LLVM 3.7 时代的位代码,带有一些限制以及各种语义上重要的操作和元数据。
LLVM 对 DXIL 支持的实现理念是尽可能将 DXIL 仅仅视为一种表示格式。读取 DXIL 时,我们应尽可能将所有内容转换为通用的 LLVM 构造。同样,我们应在降低格式的过程中尽可能晚地引入 DXIL 特定的构造。
在 LLVM 中有三个地方可以查找与 DXIL 相关的代码:用于写入 DXIL 的 DirectX 后端;用于读取的 DXILUpgrade pass;以及在读写之间共享的库代码。我们将按相反的顺序描述这些内容。
读写通用代码¶
为了避免代码重复,读写 DXIL 之间需要共享相当多的逻辑。虽然我们没有关于此类代码应放在何处的硬性规定,但通常有三个明智的位置。必须保持固定以匹配 DXIL ABI 的枚举和值的简单定义可以在 Support/DXILABI.h 中找到,在 DXIL 和现代 LLVM 构造之间双向转换的实用程序位于 lib/Transforms/Utils 中,而导出或保留信息所需的更多分析则作为典型的 lib/Analysis pass 实现。
DXILUpgrade Pass¶
将 DXIL 转换为 LLVM IR 利用了 DXIL 与 LLVM 3.7 位代码兼容的事实,以及现代 LLVM 能够将旧位代码“升级”为现代 IR 的能力。但是,仅依靠位代码升级过程是不够的,因为它会留下许多 DXIL 特定的构造。因此,我们有 DXILUpgrade pass 来将 DXIL 操作转换为 LLVM 操作,并消除元数据表示方面的差异。我们将此 pass 称为“升级”,以反映它遵循 LLVM 的标准位代码升级过程,并且只是完成了 DXIL 构造的工作 - 虽然“读取器”或“提升”也可能是合理的名称,但它们可能会有点误导。
DXILUpgrade pass 本身相当轻量级。它主要依赖于上面“通用代码”中描述的实用程序,以便尽可能与 DirectX 后端和 Clang 的 HLSL 代码生成共享逻辑。
DirectX Intrinsic Expansion Pass¶
有些 intrinsic 不能直接映射到 DXIL Ops。在某些情况下,intrinsic 需要扩展为一组 LLVM IR 指令。在其他情况下,intrinsic 需要修改 DXIL Op 的参数或返回值。DXILIntrinsicExpansion pass 处理我们 intrinsic 没有一对一映射的所有情况。当扩展特定于 DXIL 以将实现细节排除在 CodeGen 之外时,也可以使用此 pass。最后,期望我们在整个 pass 中维护向量类型。因此,最佳实践是在此 pass 中避免标量化。
DirectX 后端¶
DirectX 后端将 LLVM IR 降低为 DXIL。由于我们正在转换为中间格式而不是特定的 ISA,因此此后端不遵循您可能从其他后端熟悉的指令选择模式。降低 DXIL 有两个部分 - 一组 pass,它们将各种构造突变为与 DXIL 表示这些构造的方式匹配的形式,然后是一个有限的位代码“降级器 pass”。
在发出 DXIL 之前,DirectX 后端需要修改 LLVM IR,以便以 DXIL 期望的方式表示外部操作、类型和元数据。例如,DXILOpLowering 将 intrinsic 转换为 dx.op 调用。这些 pass 本质上是 DXILUpgrade pass 的逆过程。最好在可能的情况下将此降级过程作为 IR 到 IR 的 pass 来完成,因为这意味着可以使用 opt 和 FileCheck 轻松地对其进行测试,而无需外部工具。
DXIL 发射的第二部分或多或少是 LLVM 位代码降级器。我们需要发出与 LLVM 3.7 表示匹配的位代码。为此,我们有 DXILWriter,它是 LLVM 的 BitcodeWriter 的替代版本。目前,它可以利用 LLVM 当前的位代码库来完成大量工作,但未来某个时候可能需要完全分离,因为现代 LLVM 位代码不断发展。
DirectX 后端流程¶
DXIL 的代码生成流程分为一系列 pass。这些 pass 分为两个流程
生成 DXIL IR。
生成 DXIL 二进制文件。
生成 DXIL IR 的 pass 遵循以下流程
DXILOpLowering -> DXILPrepare -> DXILTranslateMetadata
这些 pass 中的每一个都有明确的职责
DXILOpLowering 将 LLVM intrinsic 调用转换为 dx.op 调用。
DXILPrepare 转换 DXIL IR 以与 LLVM 3.7 兼容,并插入位转换以允许插入类型化指针。
DXILTranslateMetadata 发出 DXIL 元数据结构。
在 DX 容器中将 DXIL 编码为二进制文件的 pass 遵循以下流程
DXILEmbedder -> DXContainerGlobals -> AsmPrinter
这些 pass 中的每一个都有以下明确的职责
DXILEmbedder 运行 DXIL 位代码编写器以生成位代码流,并将二进制数据嵌入到原始模块的全局变量中。
DXContainerGlobals 基于计算的分析 pass 为其他 DX 容器部件生成二进制数据全局变量。
AsmPrinter 是用于发出目标文件的标准 LLVM 基础设施。
当将 DXIL 发射到 DX 容器文件时,MC 层的使用方式类似于 Clang -fembed-bitcode
选项的操作方式。DX 容器对象编写器知道如何构造容器的标头和结构字段,并从模块中读取全局变量以填充剩余的部分数据。
DirectX 容器¶
DirectX 容器格式在 LLVM 中被视为对象文件格式。读取在 BinaryFormat 和 Object 库之间实现,写入在 MC 层实现。附加的测试和检查支持在 ObjectYAML 库和工具中实现。
测试¶
许多 DXIL 测试可以使用典型的 IR 到 IR 测试,使用 opt 和 FileCheck 完成,因为许多支持是在 IR 级别 pass 中实现的,如前几节所述。您可以在 llvm/test/CodeGen/DirectX 以及 llvm/test/Transforms/DXILUpgrade 中看到此类示例,并且应尽可能多地利用这种类型的测试。
但是,当涉及到测试 DXIL 格式本身时,IR pass 不足以进行测试。目前,我们可用的最佳选择是使用 DXC 项目的工具进行往返测试。这些测试目前在 test/tools/dxil-dis 中找到,并且仅在设置 LLVM_INCLUDE_DXIL_TESTS cmake 选项时可用。请注意,我们目前没有为 DXIL 读取路径设置等效的测试。
一旦我们能够做到,我们还将希望使用 DXIL 写入和读取路径进行往返测试,以确保自身一致性,并在 dxil-dis 不可用时获得测试覆盖率。