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.createHandle
、dx.op.createHandleFromBinding
、dx.op.createHandleFromHeap
和dx.op.createHandleForLib
。- 類型資訊
可通過資源訪問的數據類型。對於緩衝區和紋理,這可以是
float
或float4
之類的簡單類型、結構或原始字節。對於常數緩衝區,這只是一個大小。對於採樣器,這是採樣器的類型。在 LLVM 中,我們將此資訊作為資源
target()
類型上的參數嵌入。請參閱 資源類型。- 資源類型資訊
資源類型。在 HLSL 中,我們有
ByteAddressBuffer
、RWTexture2D
和RasterizerOrderedStructuredBuffer
等資源。這些資源對應到一組 DXIL 類型,例如RawBuffer
和Texture2D
,並帶有某些屬性的欄位,例如IsUAV
和IsROV
。在 LLVM 中,我們以
target()
類型表示。我們省略了可以從類型資訊中推導出的資訊,但我們確實有欄位可以在需要時編碼IsWriteable
、IsROV
和SampleCount
。
備註
TODO:DXIL 中繼資料中有兩個欄位未表示為目標類型的一部分:IsGloballyCoherent
和 HasCounter
。
由於這些是從分析中得出的,將它們儲存在類型上意味著我們需要在編譯器流程中更改類型。這實際上是不可行的。我不太清楚我們是否需要在編譯器流程中將這些資訊序列化到 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 操作以及原子操作使用。
有一些欄位用於描述所有這些類型的變體
欄位 |
描述 |
---|---|
元素類型 |
單一元素的類型,例如 |
可寫入性 |
欄位是否可寫入。這區分了 SRV(不可寫入)和 UAV(可寫入)。 |
是否為 ROV |
UAV 是否為光柵化順序視圖。對於 SRV,始終為 |
是否為帶符號 |
int 元素類型是否為帶符號的(僅限“dx.TypedBuffer”) |
資源操作¶
資源控點¶
我們提供了幾種不同的方法,可以透過 llvm.dx.handle.*
內建函數在 IR 中實例化資源。這些內建函數會根據返回類型進行重載,返回資源的適當控點,並在內建函數的參數中表示綁定資訊。
我們需要的三個操作是 llvm.dx.handle.fromBinding
、llvm.dx.handle.fromHeap
和 llvm.dx.handle.fromPointer
。這些操作大致等效於 DXIL 操作 dx.op.createHandleFromBinding
、dx.op.createHandleFromHeap
和 dx.op.createHandleForLib
,但它們會折疊後續的 dx.op.annotateHandle
操作。請注意,我們沒有 dx.op.createHandle 的類似物,因為 dx.op.createHandleFromBinding
包含了它。
為了簡化降級,我們與 DXIL 相匹配,使用從綁定空間開頭算起的索引,而不是從綁定本身的下限算起的索引。
參數 |
類型 |
描述 |
|
---|---|---|---|
返回值 |
一個 |
一個可以操作的控點 |
|
|
1 |
|
此資源在根簽章中的暫存器空間 ID。 |
|
2 |
|
綁定在其暫存器空間中的下限。 |
|
3 |
|
綁定的範圍大小。 |
|
4 |
|
從綁定空間開頭算起要訪問的索引。 |
|
5 |
i1 |
如果資源索引可能是非均勻的,則必須為 |
備註
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)
參數 |
類型 |
描述 |
|
---|---|---|---|
返回值 |
一個 |
一個可以操作的控點 |
|
|
0 |
|
要訪問的資源的索引。 |
|
1 |
i1 |
如果資源索引可能是非均勻的,則必須為 |
範例
; 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.typedBufferLoad
和 llvm.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 內建函數會降級為 bufferLoad 和 bufferStore 操作,而對 RawBufferPtr 存取的記憶體進行的操作會降級為 rawBufferLoad 和 rawBufferStore。請注意,如果我們要支援 1.2 之前的 DXIL 版本,我們也需要將 RawBuffer 載入和存放降級為非原始操作。
備註
TODO:我們需要在此處考慮 CheckAccessFullyMapped。
在 DXIL 中,載入操作始終返回 i32
狀態值,但在不使用時,這不太符合人體工學。我們可以 (1) 忍痛讓載入始終返回 {%ret_type, %i32},(2) 建立變體或僅在使用狀態時更新簽章,或 (3) 將其隱藏在某個側邊通道中。我傾向於 (2),但可能會被說服 (1) 的醜陋是值得的簡單性。
參數 |
類型 |
描述 |
|
---|---|---|---|
返回值 |
緩衝區類型的 4 元素或 2 元素向量 |
從緩衝區載入的資料 |
|
|
0 |
|
要載入的緩衝區 |
|
1 |
|
緩衝區中的索引 |
範例
%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)
參數 |
類型 |
描述 |
|
---|---|---|---|
返回值 |
|
||
|
0 |
|
要存放到的緩衝區 |
|
1 |
|
緩衝區中的索引 |
|
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)
參數 |
類型 |
描述 |
|
---|---|---|---|
返回值 |
|
指向緩衝區元素的指標 |
|
|
0 |
|
要載入的緩衝區 |
|
1 |
|
緩衝區中的索引 |
範例
; 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