DXIL 資源處理

簡介

DXIL 中的資源通過 LLVM IR 中的 TargetExtType 表示,並最終由 DirectX 後端降低為 DXIL 中的元數據。

在 DXC 和 DXIL 中,靜態資源表示為 SRV(著色器資源視圖)、UAV(無序訪問視圖)、CBV(常數緩衝區視圖)和採樣器的列表。此元數據包含一個「資源記錄 ID」,用於唯一識別資源和類型資訊。從著色器模型 6.6 開始,也有動態資源,它們放棄了元數據,而是通過指令流中的 annotateHandle 操作來描述。

在 LLVM 中,我們嘗試統一 DXC 中存在的某些替代表示形式,目的是使編譯器中間端的資源處理更簡單、更一致。

資源類型資訊和屬性

DXIL 中的資源與許多屬性相關聯。

資源 ID

每個資源類型(SRV、UAV 等)必須唯一的任意 ID。

在 LLVM 中,我們不費心表示它,而是在 DXIL 降低時生成它。

綁定資訊

關於資源來源的資訊。這可以是 (a) 一個寄存器空間、該空間中的下界和綁定大小,或 (b) 一個動態資源堆中的索引。

在 LLVM 中,我們在 句柄創建內聯函數 的參數中表示綁定資訊。生成 DXIL 時,我們會根據需要將這些調用轉換為元數據,dx.op.createHandledx.op.createHandleFromBindingdx.op.createHandleFromHeapdx.op.createHandleForLib

類型資訊

可通過資源訪問的數據類型。對於緩衝區和紋理,這可以是 floatfloat4 之類的簡單類型、結構或原始字節。對於常數緩衝區,這只是一個大小。對於採樣器,這是採樣器的類型。

在 LLVM 中,我們將此資訊作為資源 target() 類型上的參數嵌入。請參閱 資源類型

資源類型資訊

資源類型。在 HLSL 中,我們有 ByteAddressBufferRWTexture2DRasterizerOrderedStructuredBuffer 等資源。這些資源對應到一組 DXIL 類型,例如 RawBufferTexture2D,並帶有某些屬性的欄位,例如 IsUAVIsROV

在 LLVM 中,我們以 target() 類型表示。我們省略了可以從類型資訊中推導出的資訊,但我們確實有欄位可以在需要時編碼 IsWriteableIsROVSampleCount

備註

TODO:DXIL 中繼資料中有兩個欄位未表示為目標類型的一部分:IsGloballyCoherentHasCounter

由於這些是從分析中得出的,將它們儲存在類型上意味著我們需要在編譯器流程中更改類型。這實際上是不可行的。我不太清楚我們是否需要在編譯器流程中將這些資訊序列化到 IR 中——我們可能可以透過一個分析過程來在需要時計算資訊。

如果分析不足,我們將需要類似於 annotateHandle 的東西(但僅限於這兩個屬性)或在控點建立中對其進行編碼。

資源類型

我們定義了一組 TargetExtTypes,它類似於各種資源的 HLSL 表示,儘管有一些參數化。這與 DXIL 不同,因為將類型簡化為“dx.srv”和“dx.uav”之類的類型將意味著對這些類型的操作必須過於通用。

緩衝區

target("dx.TypedBuffer", ElementType, IsWriteable, IsROV, IsSigned)
target("dx.RawBuffer", ElementType, IsWriteable, IsROV)

我們需要兩種不同的緩衝區類型來解釋 16 位元組 bufferLoad / bufferStore 操作(作用於 DXIL 的 TypedBuffers)和 rawBufferLoad / rawBufferStore 操作(用於 DXIL 的 RawBuffers 和 StructuredBuffers)之間的差異。我們將後者稱為“RawBuffer”以匹配操作的名稱,但它可以表示 Raw 和 Structured 變體。

HLSL 的 Buffer 和 RWBuffer 表示為 TypedBuffer,其元素類型是標量整數或浮點數類型,或者是最多 4 個此類類型的向量。HLSL 的 ByteAddressBuffer 是一個 RawBuffer,其元素類型為 i8。HLSL 的 StructuredBuffers 是 RawBuffer,其元素類型為結構體、向量或標量類型。

這裡一個不幸的必要條件是 TypedBuffer 需要一個額外的參數來區分有符號和無符號整數。這是因為在 LLVM IR 中,int 類型沒有符號,因此要保留此資訊,我們需要一個側通道。

這些類型通常由 BufferLoad 和 BufferStore 操作以及原子操作使用。

有一些欄位用於描述所有這些類型的變體

表 102 緩衝區欄位

欄位

描述

元素類型

單一元素的類型,例如 i8v4f32 或結構類型。

可寫入性

欄位是否可寫入。這區分了 SRV(不可寫入)和 UAV(可寫入)。

是否為 ROV

UAV 是否為光柵化順序視圖。對於 SRV,始終為 0

是否為帶符號

int 元素類型是否為帶符號的(僅限“dx.TypedBuffer”)

資源操作

資源控點

我們提供了幾種不同的方法,可以透過 llvm.dx.handle.* 內建函數在 IR 中實例化資源。這些內建函數會根據返回類型進行重載,返回資源的適當控點,並在內建函數的參數中表示綁定資訊。

我們需要的三個操作是 llvm.dx.handle.fromBindingllvm.dx.handle.fromHeapllvm.dx.handle.fromPointer。這些操作大致等效於 DXIL 操作 dx.op.createHandleFromBindingdx.op.createHandleFromHeapdx.op.createHandleForLib,但它們會折疊後續的 dx.op.annotateHandle 操作。請注意,我們沒有 dx.op.createHandle 的類似物,因為 dx.op.createHandleFromBinding 包含了它。

為了簡化降級,我們與 DXIL 相匹配,使用從綁定空間開頭算起的索引,而不是從綁定本身的下限算起的索引。

表 103 @llvm.dx.handle.fromBinding

參數

類型

描述

返回值

一個 target() 類型

一個可以操作的控點

%reg_space

1

i32

此資源在根簽章中的暫存器空間 ID。

%lower_bound

2

i32

綁定在其暫存器空間中的下限。

%range_size

3

i32

綁定的範圍大小。

%index

4

i32

從綁定空間開頭算起要訪問的索引。

%non-uniform

5

i1

如果資源索引可能是非均勻的,則必須為 true

備註

TODO:我們可以刪除均勻性位元嗎?我懷疑我們可以從均勻性分析中推導出來...

範例

; RWBuffer<float4> Buf : register(u5, space3)
%buf = call target("dx.TypedBuffer", <4 x float>, 1, 0, 0)
            @llvm.dx.handle.fromBinding.tdx.TypedBuffer_f32_1_0(
                i32 3, i32 5, i32 1, i32 0, i1 false)

; RWBuffer<int> Buf : register(u7, space2)
%buf = call target("dx.TypedBuffer", i32, 1, 0, 1)
            @llvm.dx.handle.fromBinding.tdx.TypedBuffer_i32_1_0t(
                i32 2, i32 7, i32 1, i32 0, i1 false)

; Buffer<uint4> Buf[24] : register(t3, space5)
%buf = call target("dx.TypedBuffer", <4 x i32>, 0, 0, 0)
            @llvm.dx.handle.fromBinding.tdx.TypedBuffer_i32_0_0t(
                i32 2, i32 7, i32 24, i32 0, i1 false)

; struct S { float4 a; uint4 b; };
; StructuredBuffer<S> Buf : register(t2, space4)
%buf = call target("dx.RawBuffer", {<4 x float>, <4 x i32>}, 0, 0)
            @llvm.dx.handle.fromBinding.tdx.RawBuffer_sl_v4f32v4i32s_0_0t(
                i32 4, i32 2, i32 1, i32 0, i1 false)

; ByteAddressBuffer Buf : register(t8, space1)
%buf = call target("dx.RawBuffer", i8, 0, 0)
            @llvm.dx.handle.fromBinding.tdx.RawBuffer_i8_0_0t(
                i32 1, i32 8, i32 1, i32 0, i1 false)
表 104 @llvm.dx.handle.fromHeap

參數

類型

描述

返回值

一個 target() 類型

一個可以操作的控點

%index

0

i32

要訪問的資源的索引。

%non-uniform

1

i1

如果資源索引可能是非均勻的,則必須為 true

範例

; RWStructuredBuffer<float4> Buf = ResourceDescriptorHeap[2];
declare
  target("dx.RawBuffer", <4 x float>, 1, 0)
  @llvm.dx.handle.fromHeap.tdx.RawBuffer_v4f32_1_0(
      i32 %index, i1 %non_uniform)
; ...
%buf = call target("dx.RawBuffer", <4 x f32>, 1, 0)
            @llvm.dx.handle.fromHeap.tdx.RawBuffer_v4f32_1_0(
                i32 2, i1 false)

緩衝區載入和儲存

相關類型:緩衝區

我們需要分別處理來自“dx.TypedBuffer”和“dx.RawBuffer”的緩衝區載入和儲存。對於 TypedBuffer,我們有 llvm.dx.typedBufferLoadllvm.dx.typedBufferStore,它們透過簡單的索引載入和儲存 16 位元組的數據“行”。對於 RawBuffer,我們有 llvm.dx.rawBufferPtr,它返回一個可以根據需要進行索引、載入和儲存的指標。

類型化載入和存放操作始終對正好 16 個位元組的資料進行操作,因此只有幾個有效的過載。對於 32 位元或更小的類型,我們對 4 元素向量進行操作,例如 <4 x i32><4 x float><4 x half>。請注意,在 16 位元情況下,每個 16 位元值佔用 32 位元的存放空間。對於 64 位元類型,我們對 2 元素向量進行操作 - <2 x double><2 x i64>。當在 HLSL 層級使用 Buffer<float> 之類的類型時,預期它將在每個 16 位元組列中對單個浮點數進行操作 - 也就是說,載入將使用 <4 x float> 變體,然後提取第一個元素。

備註

在 DXC 中,嘗試對 Buffer<double4> 進行操作會導致編譯器當機。我們應該在前段就拒絕這種情況。

TypedBuffer 內建函數會降級為 bufferLoadbufferStore 操作,而對 RawBufferPtr 存取的記憶體進行的操作會降級為 rawBufferLoadrawBufferStore。請注意,如果我們要支援 1.2 之前的 DXIL 版本,我們也需要將 RawBuffer 載入和存放降級為非原始操作。

備註

TODO:我們需要在此處考慮 CheckAccessFullyMapped

在 DXIL 中,載入操作始終返回 i32 狀態值,但在不使用時,這不太符合人體工學。我們可以 (1) 忍痛讓載入始終返回 {%ret_type, %i32},(2) 建立變體或僅在使用狀態時更新簽章,或 (3) 將其隱藏在某個側邊通道中。我傾向於 (2),但可能會被說服 (1) 的醜陋是值得的簡單性。

表 105 @llvm.dx.typedBufferLoad

參數

類型

描述

返回值

緩衝區類型的 4 元素或 2 元素向量

從緩衝區載入的資料

%buffer

0

target(dx.TypedBuffer, ...)

要載入的緩衝區

%index

1

i32

緩衝區中的索引

範例

%ret = call <4 x float> @llvm.dx.typedBufferLoad.tdx.TypedBuffer_f32_0_0t(
    target("dx.TypedBuffer", f32, 0, 0) %buffer, i32 %index)
%ret = call <4 x i32> @llvm.dx.typedBufferLoad.tdx.TypedBuffer_i32_0_0t(
    target("dx.TypedBuffer", i32, 0, 0) %buffer, i32 %index)
%ret = call <4 x half> @llvm.dx.typedBufferLoad.tdx.TypedBuffer_f16_0_0t(
    target("dx.TypedBuffer", f16, 0, 0) %buffer, i32 %index)
%ret = call <2 x double> @llvm.dx.typedBufferLoad.tdx.TypedBuffer_f64_0_0t(
    target("dx.TypedBuffer", double, 0, 0) %buffer, i32 %index)
表 106 @llvm.dx.typedBufferStore

參數

類型

描述

返回值

void

%buffer

0

target(dx.TypedBuffer, ...)

要存放到的緩衝區

%index

1

i32

緩衝區中的索引

%data

2

緩衝區類型的 4 元素或 2 元素向量

要存放的資料

範例

call void @llvm.dx.bufferStore.tdx.Buffer_f32_1_0t(
    target("dx.TypedBuffer", f32, 1, 0) %buf, i32 %index, <4 x f32> %data)
call void @llvm.dx.bufferStore.tdx.Buffer_f16_1_0t(
    target("dx.TypedBuffer", f16, 1, 0) %buf, i32 %index, <4 x f16> %data)
call void @llvm.dx.bufferStore.tdx.Buffer_f64_1_0t(
    target("dx.TypedBuffer", f64, 1, 0) %buf, i32 %index, <2 x f64> %data)
表 107 @llvm.dx.rawBufferPtr

參數

類型

描述

返回值

ptr

指向緩衝區元素的指標

%buffer

0

target(dx.RawBuffer, ...)

要載入的緩衝區

%index

1

i32

緩衝區中的索引

範例

; Load a float4 from a buffer
%buf = call ptr @llvm.dx.rawBufferPtr.tdx.RawBuffer_v4f32_0_0t(
    target("dx.RawBuffer", <4 x f32>, 0, 0) %buffer, i32 %index)
%val = load <4 x float>, ptr %buf, align 16

; Load the double from a struct containing an int, a float, and a double
%buf = call ptr @llvm.dx.rawBufferPtr.tdx.RawBuffer_sl_i32f32f64s_0_0t(
    target("dx.RawBuffer", {i32, f32, f64}, 0, 0) %buffer, i32 %index)
%val = getelementptr inbounds {i32, f32, f64}, ptr %buf, i32 0, i32 2
%d = load double, ptr %val, align 8

; Load a float from a byte address buffer
%buf = call ptr @llvm.dx.rawBufferPtr.tdx.RawBuffer_i8_0_0t(
    target("dx.RawBuffer", i8, 0, 0) %buffer, i32 %index)
%val = getelementptr inbounds float, ptr %buf, i64 0
%f = load float, ptr %val, align 4

; Store to a buffer containing float4
%addr = call ptr @llvm.dx.rawBufferPtr.tdx.RawBuffer_v4f32_0_0t(
    target("dx.RawBuffer", <4 x f32>, 0, 0) %buffer, i32 %index)
store <4 x float> %val, ptr %addr

; Store the double in a struct containing an int, a float, and a double
%buf = call ptr @llvm.dx.rawBufferPtr.tdx.RawBuffer_sl_i32f32f64s_0_0t(
    target("dx.RawBuffer", {i32, f32, f64}, 0, 0) %buffer, i32 %index)
%addr = getelementptr inbounds {i32, f32, f64}, ptr %buf, i32 0, i32 2
store double %d, ptr %addr

; Store a float into a byte address buffer
%buf = call ptr @llvm.dx.rawBufferPtr.tdx.RawBuffer_i8_0_0t(
    target("dx.RawBuffer", i8, 0, 0) %buffer, i32 %index)
%addr = getelementptr inbounds float, ptr %buf, i64 0
store float %f, ptr %val