DXIL 資源處理

簡介

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

在 DXC 和 DXIL 中,靜態資源表示為 SRV(Shader Resource Views,著色器資源視圖)、UAV(Uniform Access Views,統一存取視圖)、CBV(Constant Buffer Views,常數緩衝區視圖)和 Sampler(採樣器)的列表。此元數據包含「資源記錄 ID」,其唯一識別資源和類型資訊。從著色器模型 6.6 開始,也存在動態資源,其放棄元數據,而是透過指令流中的 annotateHandle 操作來描述。

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

資源類型資訊與屬性

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

資源 ID

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

在 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(在需要時)。

注意

待辦事項: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)與用於 DXIL 的 RawBuffers 和 StructuredBuffers 的 rawBufferLoad / rawBufferStore 操作之間的差異。我們將後者稱為「RawBuffer」以匹配操作的命名,但它可以表示 Raw 和 Structured 變體。

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

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

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

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

表 105 緩衝區欄位

欄位

描述

元素類型

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

是否可寫入

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

IsROV

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

是否為有號數

整數元素類型是否為有號數(僅限「dx.TypedBuffer」)

資源操作

資源句柄

我們透過 llvm.dx.handle.* 內建函數在 IR 中提供幾種不同的資源實例化方式。這些內建函數在返回類型上重載,返回資源的適當句柄,並在內建函數的參數中表示綁定資訊。

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

我們與 DXIL 不同,從綁定的開始處而不是從綁定空間的開始處索引。這更清楚地匹配了語義,並避免了構成有效參數的非顯而易見的不變量。

表 106 @llvm.dx.resource.handlefrombinding

參數

類型

描述

傳回值

target() 類型

可以操作的句柄

%reg_space

1

i32

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

%lower_bound

2

i32

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

%range_size

3

i32

綁定的範圍大小。

%index

4

i32

從綁定開始處的索引。

%non-uniform

5

i1

如果資源索引可能是不一致的,則必須為 true

注意

待辦事項:我們可以刪除一致性位元嗎?我懷疑我們可以從一致性分析中推導出來…

範例

; RWBuffer<float4> Buf : register(u5, space3)
%buf = call target("dx.TypedBuffer", <4 x float>, 1, 0, 0)
     @llvm.dx.resource.handlefrombinding.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.resource.handlefrombinding.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.resource.handlefrombinding.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.resource.handlefrombinding.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.resource.handlefrombinding.tdx.RawBuffer_i8_0_0t(
        i32 1, i32 8, i32 1, i32 0, i1 false)

; RWBuffer<float4> Global[3] : register(u6, space5)
; RWBuffer<float4> Buf = Global[2];
%buf = call target("dx.TypedBuffer", <4 x float>, 1, 0, 0)
    @llvm.dx.resource.handlefrombinding.tdx.TypedBuffer_f32_1_0(
        i32 5, i32 6, i32 3, i32 2, i1 false)
表 107 @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)

以記憶體形式存取資源

相關類型:緩衝區和紋理

從資源載入和儲存通常在 LLVM 中使用只能透過句柄對象存取的記憶體上的操作來表示。給定一個句柄,llvm.dx.resource.getpointer 提供一個指針,可用於讀取和(取決於類型)寫入資源。

使用 llvm.dx.resource.getpointer 的存取在 DXILResourceAccess 通道中被替換為直接載入和儲存操作。這些直接載入和儲存稍後將在本文件中描述。

注意

目前,dx.resource.getpointer 返回的指針位於預設位址空間中,但這在未來可能會改變。

表 108 @llvm.dx.resource.getpointer

參數

類型

描述

傳回值

指針

指向緩衝區中物件的指針

%buffer

0

target(dx.TypedBuffer, ...)

要存取的緩衝區

%index

1

i32

%index

範例

%ptr = call ptr @llvm.dx.resource.getpointer.p0.tdx.TypedBuffer_v4f32_0_0_0t(
    target("dx.TypedBuffer", <4 x float>, 0, 0, 0) %buffer, i32 %index)

緩衝區中的索引

相關類型:緩衝區和紋理

載入、採樣與收集

DXIL 中的所有載入、採樣和收集操作都返回 ResRet 類型。這些類型是結構體,包含一些基本類型的 4 個元素,以及第 5 個元素,該元素由 CheckAccessFullyMapped 操作使用。其中一些操作,例如 RawBufferLoad,包含一個遮罩和/或對齊方式,告訴我們有關如何解釋這四個值的一些資訊。

在這些操作的 LLVM IR 表示形式中,我們改為返回純量或向量,但我們保留了只返回最多 4 個基本類型元素的要求。這避免了中間格式中一些不必要的轉換和結構操作,同時也保持了降級到 DXIL 的直接性。

映射到返回 ResRet 的操作的 LLVM 內建函數返回一個匿名結構體,其中 element-0 是純量或向量類型,element-1 是 CheckAccessFullyMapped 呼叫的 i1 結果。我們根本沒有單獨呼叫 CheckAccessFullyMapped,因為這是唯一可以對此值執行的操作。實際上,這可能意味著當 HLSL 原始碼中缺少檢查時,我們會插入一個 DXIL 操作,但這實際上符合 DXC 在實踐中的行為。

對於 TypedBuffer 和 Texture,我們直接從資源的包含類型映射到內建函數的傳回值。由於這些資源被限制為僅包含最多 4 個元素的純量和向量,因此降級到 DXIL 操作通常很簡單。我們在這裡的唯一例外是元素中的 double 類型是特殊的 - 這些類型在 LLVM 內建函數中是允許的,但對於 DXIL,它們被降級為 i32 對,後跟 MakeDouble 操作。

參數

類型

描述

傳回值

表 109 @llvm.dx.resource.load.typedbuffer

包含類型和檢查位元的結構體

%buffer

0

target(dx.TypedBuffer, ...)

從緩衝區載入的資料和檢查位元

%index

1

i32

%index

範例

%ret = call {<4 x float>, i1}
    @llvm.dx.resource.load.typedbuffer.v4f32.tdx.TypedBuffer_v4f32_0_0_0t(
        target("dx.TypedBuffer", <4 x float>, 0, 0, 0) %buffer, i32 %index)
%ret = call {float, i1}
    @llvm.dx.resource.load.typedbuffer.f32.tdx.TypedBuffer_f32_0_0_0t(
        target("dx.TypedBuffer", float, 0, 0, 0) %buffer, i32 %index)
%ret = call {<4 x i32>, i1}
    @llvm.dx.resource.load.typedbuffer.v4i32.tdx.TypedBuffer_v4i32_0_0_0t(
        target("dx.TypedBuffer", <4 x i32>, 0, 0, 0) %buffer, i32 %index)
%ret = call {<4 x half>, i1}
    @llvm.dx.resource.load.typedbuffer.v4f16.tdx.TypedBuffer_v4f16_0_0_0t(
        target("dx.TypedBuffer", <4 x half>, 0, 0, 0) %buffer, i32 %index)
%ret = call {<2 x double>, i1}
    @llvm.dx.resource.load.typedbuffer.v2f64.tdx.TypedBuffer_v2f64_0_0t(
        target("dx.TypedBuffer", <2 x double>, 0, 0, 0) %buffer, i32 %index)

%index

要從中載入的緩衝區

參數

類型

描述

傳回值

對於 RawBuffer,HLSL 載入操作可能會返回任意大小的結果,但我們仍然限制 LLVM 內建函數僅返回最多 4 個基本類型元素。這意味著較大的載入表示為一系列載入,這與 DXIL 匹配。與 RawBufferLoad 操作不同,我們不需要遮罩/類型大小和對齊方式的參數,因為我們可以從降級期間載入的返回類型計算這些參數。

包含類型和檢查位元的結構體

%buffer

0

表 110 @llvm.dx.resource.load.rawbuffer

從緩衝區載入的資料和檢查位元

%index

1

i32

%index

純量或向量和檢查位元的結構體

2

i32

target(dx.RawBuffer, ...)

範例

; float
%ret = call {float, i1}
    @llvm.dx.resource.load.rawbuffer.f32.tdx.RawBuffer_f32_0_0_0t(
        target("dx.RawBuffer", float, 0, 0, 0) %buffer,
        i32 %index,
        i32 0)
%ret = call {float, i1}
    @llvm.dx.resource.load.rawbuffer.f32.tdx.RawBuffer_i8_0_0_0t(
        target("dx.RawBuffer", i8, 0, 0, 0) %buffer,
        i32 %byte_offset,
        i32 0)

; float4
%ret = call {<4 x float>, i1}
    @llvm.dx.resource.load.rawbuffer.v4f32.tdx.RawBuffer_v4f32_0_0_0t(
        target("dx.RawBuffer", float, 0, 0, 0) %buffer,
        i32 %index,
        i32 0)
%ret = call {float, i1}
    @llvm.dx.resource.load.rawbuffer.v4f32.tdx.RawBuffer_i8_0_0_0t(
        target("dx.RawBuffer", i8, 0, 0, 0) %buffer,
        i32 %byte_offset,
        i32 0)

; struct S0 { float4 f; int4 i; };
%ret = call {<4 x float>, i1}
    @llvm.dx.resource.load.rawbuffer.v4f32.tdx.RawBuffer_sl_v4f32v4i32s_0_0t(
        target("dx.RawBuffer", {<4 x float>, <4 x i32>}, 0, 0, 0) %buffer,
        i32 %index,
        i32 0)
%ret = call {<4 x i32>, i1}
    @llvm.dx.resource.load.rawbuffer.v4i32.tdx.RawBuffer_sl_v4f32v4i32s_0_0t(
        target("dx.RawBuffer", {<4 x float>, <4 x i32>}, 0, 0, 0) %buffer,
        i32 %index,
        i32 1)

; struct Q { float4 f; int3 i; }
; struct R { int z; S x; }
%ret = call {i32, i1}
    @llvm.dx.resource.load.rawbuffer.i32(
        target("dx.RawBuffer", {i32, {<4 x float>, <3 x i32>}}, 0, 0, 0)
            %buffer, i32 %index, i32 0)
%ret = call {<4 x float>, i1}
    @llvm.dx.resource.load.rawbuffer.i32(
        target("dx.RawBuffer", {i32, {<4 x float>, <3 x i32>}}, 0, 0, 0)
            %buffer, i32 %index, i32 4)
%ret = call {<3 x i32>, i1}
    @llvm.dx.resource.load.rawbuffer.i32(
        target("dx.RawBuffer", {i32, {<4 x float>, <3 x i32>}}, 0, 0, 0)
            %buffer, i32 %index, i32 20)

; byteaddressbuf.Load<int64_t4>
%ret = call {<4 x i64>, i1}
    @llvm.dx.resource.load.rawbuffer.v4i64.tdx.RawBuffer_i8_0_0t(
        target("dx.RawBuffer", i8, 0, 0, 0) %buffer,
        i32 %byte_offset,
        i32 0)

%index

給定索引處結構體中的偏移量

儲存

相關類型:紋理和緩衝區

DXIL 操作 TextureStoreBufferStoreRawBufferStore 將四個組件寫入紋理或緩衝區。這些操作包含一個遮罩參數,當寫入少於 4 個組件時使用,但值得注意的是,這僅採用連續的 x、xy、xyz 和 xyzw 值。

範例

我們定義 LLVM 儲存內建函數以在儲存多個組件時接受向量,而不是使用 undef 和遮罩,但在其他方面與 DXIL 操作相當接近。

參數

類型

描述

傳回值

對於 TypedBuffer,我們只需要一個座標,並且我們必須始終寫入向量,因為不允許部分寫入。與上面描述的載入操作類似,我們特殊處理 64 位元類型,並且僅處理 2 元素向量而不是 4 元素向量。

%buffer

0

target(dx.TypedBuffer, ...)

表 111 @llvm.dx.resource.store.typedbuffer

%index

1

i32

%index

void

2

%buffer

要儲存到的緩衝區

範例

call void @llvm.dx.resource.store.typedbuffer.tdx.Buffer_v4f32_1_0_0t(
    target("dx.TypedBuffer", f32, 1, 0) %buf, i32 %index, <4 x f32> %data)
call void @llvm.dx.resource.store.typedbuffer.tdx.Buffer_v4f16_1_0_0t(
    target("dx.TypedBuffer", f16, 1, 0) %buf, i32 %index, <4 x f16> %data)
call void @llvm.dx.resource.store.typedbuffer.tdx.Buffer_v2f64_1_0_0t(
    target("dx.TypedBuffer", f64, 1, 0) %buf, i32 %index, <2 x f64> %data)

%data

範例

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

參數

類型

描述

傳回值

對於 TypedBuffer,我們只需要一個座標,並且我們必須始終寫入向量,因為不允許部分寫入。與上面描述的載入操作類似,我們特殊處理 64 位元類型,並且僅處理 2 元素向量而不是 4 元素向量。

%buffer

0

表 110 @llvm.dx.resource.load.rawbuffer

表 111 @llvm.dx.resource.store.typedbuffer

%index

1

i32

%index

純量或向量和檢查位元的結構體

2

i32

要儲存的資料

void

3

%index

要儲存到的緩衝區

範例

; float
call void @llvm.dx.resource.store.rawbuffer.tdx.RawBuffer_f32_1_0_0t.f32(
    target("dx.RawBuffer", float, 1, 0, 0) %buffer,
    i32 %index, i32 0, float %data)
call void @llvm.dx.resource.store.rawbuffer.tdx.RawBuffer_i8_1_0_0t.f32(
    target("dx.RawBuffer", i8, 1, 0, 0) %buffer,
    i32 %index, i32 0, float %data)

; float4
call void @llvm.dx.resource.store.rawbuffer.tdx.RawBuffer_v4f32_1_0_0t.v4f32(
    target("dx.RawBuffer", <4 x float>, 1, 0, 0) %buffer,
    i32 %index, i32 0, <4 x float> %data)
call void @llvm.dx.resource.store.rawbuffer.tdx.RawBuffer_i8_1_0_0t.v4f32(
    target("dx.RawBuffer", i8, 1, 0, 0) %buffer,
    i32 %index, i32 0, <4 x float> %data)

; struct S0 { float4 f; int4 i; }
call void @llvm.dx.resource.store.rawbuffer.v4f32(
    target("dx.RawBuffer", { <4 x float>, <4 x i32> }, 1, 0, 0) %buffer,
    i32 %index, i32 0, <4 x float> %data0)
call void @llvm.dx.resource.store.rawbuffer.v4i32(
    target("dx.RawBuffer", { <4 x float>, <4 x i32> }, 1, 0, 0) %buffer,
    i32 %index, i32 16, <4 x i32> %data1)

; struct Q { float4 f; int3 i; }
; struct R { int z; S x; }
call void @llvm.dx.resource.store.rawbuffer.i32(
    target("dx.RawBuffer", {i32, {<4 x float>, <3 x half>}}, 1, 0, 0)
        %buffer,
    i32 %index, i32 0, i32 %data0)
call void @llvm.dx.resource.store.rawbuffer.v4f32(
    target("dx.RawBuffer", {i32, {<4 x float>, <3 x half>}}, 1, 0, 0)
        %buffer,
    i32 %index, i32 4, <4 x float> %data1)
call void @llvm.dx.resource.store.rawbuffer.v3f16(
    target("dx.RawBuffer", {i32, {<4 x float>, <3 x half>}}, 1, 0, 0)
        %buffer,
    i32 %index, i32 20, <3 x half> %data2)

; byteaddressbuf.Store<int64_t4>
call void @llvm.dx.resource.store.rawbuffer.tdx.RawBuffer_i8_1_0_0t.v4f64(
    target("dx.RawBuffer", i8, 1, 0, 0) %buffer,
    i32 %index, i32 0, <4 x double> %data)

對於 RawBuffer,我們需要兩個索引,並且我們接受純量和 4 個或更少元素的向量。請注意,這裡我們允許 4 個 64 位元元素的向量。

表 112 @llvm.dx.resource.store.rawbuffer

%offset

結構化緩衝區元素中的位元組偏移量

%data

參數

類型

描述

傳回值

純量或向量

%buffer

%buffer

0

常數緩衝區載入

從緩衝區載入的資料和檢查位元

%index

1

i32

%index

範例

%ret = call {float, float, float, float}
    @llvm.dx.resource.load.cbufferrow.4(
        target("dx.CBuffer", target("dx.Layout", {float}, 4, 0)) %buffer,
        i32 %index)
%ret = call {i32, i32, i32, i32}
    @llvm.dx.resource.load.cbufferrow.4(
        target("dx.CBuffer", target("dx.Layout", {i32}, 4, 0)) %buffer,
        i32 %index)
相關類型:CBuffers

參數

類型

描述

傳回值

操作 CBufferLoadLegacy,儘管名稱如此,但它是任何 DXIL 版本中從 cbuffer 載入的唯一支援方式,它載入 cbuffer 的單個「行」,正好是 16 個位元組。操作的傳回值由 CBufRet 類型表示,該類型具有 2 個 64 位元值、4 個 32 位元值和 8 個 16 位元值的變體。

我們在 LLVM IR 中使用 3 個獨立的操作來表示這些,它們分別返回 2 元素、4 元素或 8 元素結構體。

%buffer

0

常數緩衝區載入

從緩衝區載入的資料和檢查位元

%index

1

i32

%index

範例

%ret = call {double, double}
    @llvm.dx.resource.load.cbufferrow.2(
        target("dx.CBuffer", target("dx.Layout", {double}, 8, 0)) %buffer,
        i32 %index)
%ret = call {i64, i64}
    @llvm.dx.resource.load.cbufferrow.2(
        target("dx.CBuffer", target("dx.Layout", {i64}, 4, 0)) %buffer,
        i32 %index)
表 113 @llvm.dx.resource.load.cbufferrow.4

參數

類型

描述

傳回值

4 個 32 位元值的結構體

cbuffer 的單個行,解釋為 4 個 32 位元值

%buffer

0

常數緩衝區載入

從緩衝區載入的資料和檢查位元

%index

1

i32

%index

範例

%ret = call {half, half, half, half, half, half, half, half}
    @llvm.dx.resource.load.cbufferrow.8(
        target("dx.CBuffer", target("dx.Layout", {half}, 2, 0)) %buffer,
        i32 %index)
%ret = call {i16, i16, i16, i16, i16, i16, i16, i16}
    @llvm.dx.resource.load.cbufferrow.8(
        target("dx.CBuffer", target("dx.Layout", {i16}, 2, 0)) %buffer,
        i32 %index)