LLVM 中 DXIL 支援的架構與設計

簡介

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

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

在 LLVM 中有三個地方可以尋找與 DXIL 相關的程式碼:用於寫入 DXIL 的 DirectX 後端;用於讀取的 DXILUpgrade pass;以及在寫入和讀取之間共用的程式庫程式碼。我們將以相反的順序描述這些。

讀取和寫入的通用程式碼

為了避免程式碼重複,讀取和寫入 DXIL 之間需要共用相當多的邏輯。雖然我們沒有一個硬性規定,說明此類程式碼應該放在哪裡,但通常有三個合理的地方。可以在 Support/DXILABI.h 中找到必須保持固定以匹配 DXIL ABI 的列舉和值的簡單定義,用於在 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」傳遞本身相當輕量級。 它主要依賴上面「通用代碼」中描述的工具程式,以便盡可能與 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 傳遞來執行,因為這意味著可以使用「opt」和「FileCheck」輕鬆測試它們,而無需外部工具。

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 中繼資料結構。

在 DX 容器中將 DXIL 編碼為二進位檔案的傳遞遵循以下流程

DXILEmbedder -> DXContainerGlobals -> AsmPrinter

這些傳遞中的每一個都具有以下定義的職責

  1. DXILEmbedder 運行 DXIL 位元碼編寫器以生成位元碼流,並將二進位資料嵌入原始模組中的全域變數內。

  2. DXContainerGlobals 根據計算的分析傳遞為其他 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 不可用時獲得測試覆蓋率。