LLVM 中 DXIL 支持的架构和设计

简介

LLVM 支持读取和写入 DirectX 中间语言,或 DXIL。DXIL 本质上是 LLVM 3.7 时代的比特码,带有一些限制以及各种语义上重要的操作和元数据。

LLVM 对 DXIL 支持的实现理念是尽可能地将 DXIL 视为一种表示格式。在读取 DXIL 时,我们应该尽可能地将所有内容转换为通用的 LLVM 构造。类似地,我们应该在降低到该格式的过程中尽可能晚地引入 DXIL 特定的构造。

在 LLVM 中有三个地方可以找到与 DXIL 相关的代码:用于写入 DXIL 的 DirectX 后端;用于读取的 DXILUpgrade 传递;以及在读取和写入之间共享的库代码。我们将按相反的顺序描述这些内容。

读取和写入的通用代码

为了避免代码重复,在读取和写入 DXIL 之间需要共享相当多的逻辑。虽然我们没有关于此类代码应该放在何处的硬性规定,但通常有三个合理的位置。可以在 Support/DXILABI.h 中找到必须保持固定以匹配 DXIL 的 ABI 的枚举和值的简单定义,在 lib/Transforms/Utils 中找到在 DXIL 和现代 LLVM 构造之间双向转换的实用程序,以及为导出或保留信息而实现的更多分析作为典型的 lib/Analysis 传递。

DXILUpgrade 传递

将 DXIL 转换为 LLVM IR 利用了 DXIL 与 LLVM 3.7 比特码兼容这一事实,以及现代 LLVM 能够将旧的比特码“升级”为现代 IR。但是,仅仅依靠比特码升级过程是不够的,因为这会在周围留下许多 DXIL 特定的构造。因此,我们有 DXILUpgrade 传递来将 DXIL 操作转换为 LLVM 操作并消除元数据表示中的差异。我们称此传递为“升级”,以反映它遵循 LLVM 的标准比特码升级过程并仅完成 DXIL 构造的工作——虽然“读取器”或“提升”也可能是合理的名称,但它们可能会有点误导性。

DXILUpgrade 传递本身相当轻量级。它主要依赖于上面“通用代码”中描述的实用程序,以便尽可能地与 DirectX 后端和 Clang 的 HLSL 支持代码生成共享逻辑。

DirectX 内联函数扩展传递

有一些内联函数无法直接映射到 DXIL 操作。在某些情况下,需要将内联函数扩展为一组 LLVM IR 指令。在其他情况下,内联函数需要修改 DXIL 操作的参数或返回值。DXILIntrinsicExpansion 传递处理我们所有内联函数没有一对一映射的情况。当扩展特定于 DXIL 时,此传递也可能被使用,以将实现细节排除在 CodeGen 之外。最后,我们期望在此传递中维护向量类型。因此,最佳实践是在此传递中避免标量化。

DirectX 后端

DirectX 后端将 LLVM IR 降低到 DXIL。由于我们正在转换为中间格式而不是特定的 ISA,因此此后端不会遵循您可能从其他后端熟悉的指令选择模式。降低 DXIL 包括两个部分——一组将各种构造体更改为与 DXIL 表示这些构造体的方式相匹配的形式的传递,以及一个有限的比特码“降级传递”。

在发出 DXIL 之前,DirectX 后端需要修改 LLVM IR,以便以 DXIL 期望的方式表示外部操作、类型和元数据。例如,DXILOpLowering 将内联函数转换为 dx.op 调用。这些传递本质上是 DXILUpgrade 传递的反向过程。如果可能,最好将此降级过程作为 IR 到 IR 传递来完成,因为这意味着它们可以使用 optFileCheck 轻松测试,而无需外部工具。

DXIL 发出的第二部分或多或少是一个 LLVM 比特码降级器。我们需要发出与 LLVM 3.7 表示相匹配的比特码。为此,我们有 DXILWriter,它是 LLVM 的 BitcodeWriter 的替代版本。目前,它能够利用 LLVM 当前的比特码库来完成很多工作,但将来可能需要完全分离,因为现代 LLVM 比特码在不断发展。

DirectX 后端流程

DXIL 的代码生成流程被分解为一系列传递。这些传递被分为两个流程

  1. 生成 DXIL IR。

  2. 生成 DXIL 二进制文件。

生成 DXIL IR 的传递遵循以下流程

DXILOpLowering -> DXILPrepare -> DXILTranslateMetadata

每个传递都有一个明确的职责

  1. DXILOpLowering 将 LLVM 内联函数调用转换为 dx.op 调用。

  2. DXILPrepare 将 DXIL IR 转换为与 LLVM 3.7 兼容,并插入位转换以允许插入类型化的指针。

  3. DXILTranslateMetadata 发出 DXIL 元数据结构。

将 DXIL 编码为 DX 容器中的二进制文件的传递遵循以下流程

DXILEmbedder -> DXContainerGlobals -> AsmPrinter

每个传递都有以下定义的职责

  1. DXILEmbedder 运行 DXIL 比特码编写器以生成比特码流,并将二进制数据嵌入到原始模块中的全局变量中。

  2. DXContainerGlobals 基于计算的分析传递为其他 DX 容器部分生成二进制数据全局变量。

  3. AsmPrinter 是 LLVM 用于发出目标文件的标准基础设施。

当将 DXIL 发射到 DX 容器文件中时,MC 层的使用方式类似于 Clang 的 -fembed-bitcode 选项的操作方式。DX 容器对象编写器知道如何构造容器的标头和结构字段,并从模块中读取全局变量以填充其余部分数据。

DirectX 容器

在 LLVM 中,DirectX 容器格式被视为一种目标文件格式。读取在 BinaryFormat 和 Object 库之间实现,写入在 MC 层实现。其他测试和检查支持在 ObjectYAML 库和工具中实现。

测试

可以使用典型的 IR 到 IR 测试(使用 optFileCheck)完成许多 DXIL 测试,因为许多支持都是根据前面部分中描述的 IR 级传递实现的。您可以在 llvm/test/CodeGen/DirectX 以及 llvm/test/Transforms/DXILUpgrade 中看到此示例,并且应尽可能地利用此类测试。

但是,当涉及到测试 DXIL 格式本身时,IR 传递不足以进行测试。目前,我们可用的最佳选择是使用 DXC 项目的工具进行往返测试。这些测试目前位于 test/tools/dxil-dis 中,并且仅在设置了 LLVM_INCLUDE_DXIL_TESTS cmake 选项时才可用。请注意,我们目前还没有为 DXIL 读取路径设置等效的测试。

一旦我们能够做到,我们还希望使用 DXIL 写入和读取路径进行往返测试,以确保自一致性和在 dxil-dis 不可用时获得测试覆盖率。