LLVM 中 DXIL 支援的架構與設計

簡介

LLVM 支援讀取和寫入 DirectX 中繼語言,或稱 DXIL。DXIL 本質上是 LLVM 3.7 時代的位元碼,帶有一些限制以及各種語義上重要的操作和元數據。

LLVM 實作 DXIL 支援的哲學是盡可能將 DXIL 僅視為一種表示格式。在讀取 DXIL 時,我們應該盡可能將所有內容轉換為通用的 LLVM 結構。同樣地,我們應該在降低格式的過程中盡可能晚地引入 DXIL 特定的結構。

在 LLVM 中有三個地方可以找到與 DXIL 相關的程式碼:DirectX 後端(用於寫入 DXIL)、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 有兩個部分 - 一組將各種結構變異為符合 DXIL 表示方式的 pass,然後是一個有限的位元碼「降級器 pass」。

在發射 DXIL 之前,DirectX 後端需要修改 LLVM IR,以便外部操作、類型和元數據以 DXIL 期望的方式表示。例如,DXILOpLowering 將 intrinsic 轉換為 dx.op 呼叫。這些 pass 本質上是 DXILUpgrade pass 的逆過程。最好在可能的情況下將此降級過程作為 IR 到 IR 的 pass 進行,因為這意味著它們可以使用 optFileCheck 輕鬆測試,而無需外部工具。

DXIL 發射的第二部分或多或少是一個 LLVM 位元碼降級器。我們需要發射符合 LLVM 3.7 表示的位元碼。為此,我們有 DXILWriter,它是 LLVM 的 BitcodeWriter 的替代版本。目前,這能夠利用 LLVM 當前的位元碼程式庫來完成許多工作,但未來某個時候可能需要完全分離,因為現代 LLVM 位元碼會不斷發展。

DirectX 後端流程

DXIL 的程式碼生成流程分為一系列 pass。這些 pass 分為兩個流程:

  1. 生成 DXIL IR。

  2. 生成 DXIL 二進制檔。

生成 DXIL IR 的 pass 遵循以下流程:

DXILOpLowering -> DXILPrepare -> DXILTranslateMetadata

每個 pass 都有明確的責任:

  1. DXILOpLowering 將 LLVM intrinsic 呼叫轉換為 dx.op 呼叫。

  2. DXILPrepare 轉換 DXIL IR 以與 LLVM 3.7 相容,並插入位元轉換以允許插入類型化的指標。

  3. DXILTranslateMetadata 發射 DXIL 元數據結構。

在 DX 容器中將 DXIL 編碼為二進制檔的 pass 遵循以下流程:

DXILEmbedder -> DXContainerGlobals -> AsmPrinter

每個 pass 都有以下明確的責任:

  1. DXILEmbedder 執行 DXIL 位元碼寫入器以生成位元碼流,並將二進制數據嵌入原始模組的全局變數中。

  2. DXContainerGlobals 基於計算分析 pass 為其他 DX 容器部分生成二進制數據全局變數。

  3. AsmPrinter 是用於發射物件檔案的標準 LLVM 基礎架構。

當將 DXIL 發射到 DX 容器檔案中時,MC 層的使用方式與 Clang -fembed-bitcode 選項的操作方式類似。DX 容器物件寫入器知道如何建構容器的標頭和結構欄位,並從模組中讀取全局變數以填寫剩餘的部分數據。

DirectX 容器

DirectX 容器格式在 LLVM 中被視為物件檔案格式。讀取實作在 BinaryFormat 和 Object 程式庫之間,而寫入實作在 MC 層中。額外的測試和檢查支援在 ObjectYAML 程式庫和工具中實作。

測試

許多 DXIL 測試可以使用典型的 IR 到 IR 測試,使用 optFileCheck 完成,因為許多支援都是以 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 不可用時獲得測試覆蓋率。