DirectX 容器

總覽

DirectX 容器 (DXContainer) 檔案格式是用於編譯後的著色器,目標為 DirectX 執行階段的二進位檔案格式。此檔案格式也稱為 DXIL 容器或 DXBC 檔案格式。由於此檔案格式可用於包含 DXIL 或 DXBC 編譯後的著色器,因此 LLVM 中的命名僅為 DirectX 容器。

DirectX 容器檔案由編譯器和相關工具以及 DirectX 執行階段、效能分析工具和其他使用者讀取。本文檔作為 LLVM 中實作的配套文件,更完整地記錄檔案格式,以供眾多使用者參考。

基本結構

DXContainer 檔案以標頭開始,然後接著一系列「parts」,這些 parts 類似於物件檔案區段。每個 part 包含一個 part 標頭,以及標頭後面的若干位元組的資料,這些資料採用定義的格式。

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 位元無號整數,用於檔案大小和 part 數量。

在 part 標頭之後是一個 PartCount 個 32 位元無號整數的陣列,指定每個 part 標頭的偏移量。

Part 資料

struct PartHeader {
  uint8_t Name[4];
  uint32_t Size;
}

每個 part 都以一個 part 標頭開始。part 標頭包含 4 字元的 part 名稱,以及一個 32 位元無號整數,指定 part 資料的大小。part 標頭後面接著 Size 位元組的資料,構成 part。格式沒有明確要求 part 的 32 位元對齊,儘管 LLVM 在寫入器程式碼中實作了此限制(因為這是個好主意)。LLVM 物件讀取器程式碼不假設輸入已正確對齊,以避免其他編譯器產生的未對齊輸入導致未定義的行為。

Part 格式

part 名稱指示 part 資料的格式。DXC 和 FXC 使用 24 個 part 標頭。並非所有編譯後的著色器都包含所有 parts。在下面的列表中,僅由 DXC 產生的 parts 標記為 †,僅由 FXC 產生的 parts 標記為 *。

  1. DXIL† - 儲存 DXIL 位元組碼。

  2. HASH† - 儲存著色器 MD5 雜湊值。

  3. ILDB† - 儲存 DXIL 位元組碼,其中 LLVM 偵錯資訊嵌入在模組中。

  4. ILDN† - 儲存外部偵錯資訊的著色器偵錯名稱。

  5. ISG1 - 儲存 Shader Model 5.1+ 的輸入簽章。

  6. ISGN* - 儲存 Shader Model 4 及更早版本的輸入簽章。

  7. OSG1 - 儲存 Shader Model 5.1+ 的輸出簽章。

  8. OSG5* - 儲存 Shader Model 5 的輸出簽章。

  9. OSGN* - 儲存 Shader Model 4 及更早版本的輸出簽章。

  10. PCSG* - 儲存 Shader Model 5.1 及更早版本的修補常數簽章。

  11. PDBI† - 儲存 PDB 資訊。

  12. PRIV - 儲存任意私有資料(FXC 或 DXC 均未編碼)。

  13. PSG1 - 儲存 Shader Model 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 Part

DXIL part 由三個資料結構組成:ProgramHeaderBitcodeHeader 和位元碼序列化的 LLVM 3.7 IR 模組。

ProgramHeader 包含著色器模型版本和管線階段列舉值。這識別了包含的著色器位元碼的目標設定檔。

BitcodeHeader 包含 DXIL 版本資訊,並指向位元碼資料的起始位置。

HASH Part

HASH part 包含一個 32 位元無號整數,其中包含著色器雜湊旗標,以及一個 128 位元 MD5 雜湊摘要。旗標欄位的值可以是 0,表示沒有旗標,也可以是 1,表示檔案雜湊是包含產生二進位檔案的原始碼計算而得。

程式簽章 (SG1) Parts

struct ProgramSignatureHeader {
  uint32_t ParamCount;
  uint32_t FirstParamOffset;
}

程式簽章 parts (ISG1、OSG1 和 PSG1) 都使用相同的資料結構來編碼輸入、輸出和修補資訊。ProgramSignatureHeader 包含兩個 32 位元無號整數,用於指定簽章參數的數量和第一個參數的偏移量。

ProgramSignatureHeader 的開頭算起,FirstParamOffset 位元組處開始,寫入 ParamCountProgramSignatureElement 結構。在 ProgramSignatureElements 之後是一個字串表,其中包含以 null 終止的字串,並填補到 32 位元組對齊。此字串表與 LLVM 實作的 DWARF 字串表格式相符。

每個 ProgramSignatureElement 都編碼一個 NameOffset 值,該值指定字串表中的偏移量。0 值表示沒有名稱。此處編碼的偏移量是從 ProgramSignatureHeader 的開頭算起,而不是字串表的開頭。

ProgramSignatureElement 包含多個列舉欄位,這些欄位在 llvm/include/llvm/BinaryFormat/DXContainerConstants.def 中定義。這些欄位編碼 D3D 系統值、資料類型及其精確度要求。

PSV0 Part

管線狀態驗證資料編碼版本化的執行階段資訊結構。這些結構使用一種方案,其中不編碼版本號碼,而是編碼結構的大小,並且結構的每個新版本都是累加式的。這允許讀取器通過將編碼的大小與已知結構的大小進行比較來推斷結構的版本。如果編碼的大小大於任何已知結構,則最大的已知結構可以有效地解析已知結構中表示的資料。

在 LLVM 中,我們使用 llvm::dxbc::PSV 命名空間下的版本化命名空間(例如 v0v1)表示相關資料結構的版本。v0 命名空間中的每個結構都是基本版本,v1 命名空間中的結構繼承自 v0 命名空間,而 v2 結構繼承自 v1 結構,依此類推。

PSV 資料的高階結構為

  1. RuntimeInfo 結構

  2. 資源綁定

  3. 簽章元素

  4. 遮罩向量(輸出、輸入、輸入修補、修補輸出)

緊接著 PSV0 part 的 part 標頭之後是一個 32 位元無號整數,指定隨後的 RuntimeInfo 結構的大小。

緊接著 RuntimeInfo 結構之後是一個 32 位元無號整數,指定資源綁定的數量。如果資源數量大於零,則另一個無號 32 位元整數會隨後指定 ResourceBindInfo 結構的大小。之後是指定數量的指定大小的結構(從而推斷結構的版本)。

對於資料的版本 0,這結束了 part 資料。

PSV0 簽章元素

簽章元素在概念上是一個單一概念,但資料編碼在三個不同的區塊中。第一個區塊是字串表,第二個區塊是索引表,第三個區塊是元素本身,這些元素又按輸入、輸出和修補常數或原始元素分隔。

簽章元素捕獲了與 SG1 parts 中捕獲的許多相同資料。索引表的使用允許重複資料刪除,以實現更緊湊的最終表示形式。

字串表以一個 32 位元無號整數開始,指定表的大小。此字串表使用 LLVM 中實作的 DXContainer 格式。此格式在字串表前面加上一個 null 位元組,以便偏移量 0 為 null 字串,並填補到 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,它是 hull 著色器,並且它具有修補常數或原始向量元素,則將包含修補常數或原始向量遮罩。它的結構與輸出遮罩向量相同。遮罩向量中的每個位元都識別修補常數輸出的一列,該列取決於 ViewID。

接下來的一系列遮罩向量在結構上與輸出遮罩向量相似,但它們包含一個額外的維度。

如果著色器具有輸入和輸出,則接下來會編碼輸出/輸入映射。輸出/輸入遮罩編碼每個輸入的每一列影響哪些輸出。每個遮罩向量的大小為輸出最大向量的大小 * 輸入數量 * 4(每個元件)。遮罩向量中的每個位元都識別輸出的一列和輸入的一列。值為 1 表示輸出受到輸入的影響。

如果著色器是 hull 著色器,並且它具有輸入和修補輸出,則接下來將包含輸入到修補映射。這在結構上與輸出/輸入映射相同。維度由修補常數或原始向量遮罩的大小 * 輸入數量 * 4(每個元件)定義。遮罩向量中的每個位元都識別修補常數輸出的一列和輸入的一列。值為 1 表示輸出受到輸入的影響。

如果著色器是 domain 著色器,並且它具有輸出和修補輸出,則接下來將包含輸出修補映射。這在結構上與輸出/輸入映射相同。維度由修補常數或原始向量遮罩的大小 * 輸出數量 * 4(每個元件)定義。遮罩向量中的每個位元都識別修補常數輸入的一列和輸出的一列。值為 1 表示輸出受到原始輸入的影響。

SFI0 Part

SFI0 part 編碼 64 位元無號整數的特徵旗標位元遮罩。這表示著色器需要哪些可選功能。旗標值在 llvm/include/llvm/BinaryFormat/DXContainerConstants.def 中定義。