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 位元無符號整數。部分標頭後面跟著 大小 位元組的數據,這些數據組成了該部分。該格式並未明確要求 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 之後是一個以空字元結尾的字串表,填充至 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 中。