DXIL リソース処理¶
はじめに¶
DXIL のリソースは、LLVM IR では TargetExtType
を介して表現され、最終的には DirectX バックエンドによって DXIL のメタデータに Lowering されます。
DXC および DXIL では、静的リソースは SRV(シェーダーリソースビュー)、UAV(均一アクセスビュー)、CBV(定数バッファビュー)、およびサンプラーのリストとして表現されます。このメタデータは、リソースを一意に識別する「リソースレコード ID」とタイプ情報で構成されます。シェーダーモデル 6.6 の時点では、動的リソースもあり、メタデータを使用せず、代わりに命令ストリームの annotateHandle
操作によって記述されます。
LLVM では、DXC に存在する代替表現のいくつかを統合しようとしています。これは、コンパイラの中間エンドでのリソースの処理をよりシンプルで一貫性のあるものにすることを目的としています。
リソースの種類の情報とプロパティ¶
DXIL のリソースには、多くのプロパティが関連付けられています。
- リソース ID
リソースの種類(SRV、UAV など)ごとに一意である必要がある任意の ID。
LLVM では、これを表現するのではなく、DXIL Lowering 時に生成することを選択しています。
- バインディング情報
リソースの出所に関する情報。これは、(a) レジスタ空間、その空間の下限、およびバインディングのサイズ、または (b) 動的リソースヒープへのインデックスのいずれかです。
LLVM では、ハンドル作成組み込み関数 の引数にバインディング情報を表現します。 DXIL を生成するとき、これらの呼び出しをメタデータ、
dx.op.createHandle
、dx.op.createHandleFromBinding
、dx.op.createHandleFromHeap
、およびdx.op.createHandleForLib
に必要に応じて変換します。- タイプ情報
リソースを介してアクセスできるデータのタイプ。バッファとテクスチャの場合、これは
float
やfloat4
などの単純なタイプ、構造体、または生のバイトにすることができます。定数バッファの場合、これは単なるサイズです。サンプラーの場合、これはサンプラーの種類です。LLVM では、この情報をリソースの
target()
タイプのパラメータとして埋め込みます。リソースの種類 を参照してください。- リソースの種類の情報
リソースの種類。HLSL には、
ByteAddressBuffer
、RWTexture2D
、RasterizerOrderedStructuredBuffer
などがあります。これらは、IsUAV
やIsROV
などの特定のプロパティのフィールドを持つRawBuffer
やTexture2D
などの DXIL の種類にマップされます。LLVM では、これを
target()
タイプで表現します。タイプ情報から派生可能な情報は省略しますが、必要に応じてIsWriteable
、IsROV
、およびSampleCount
をエンコードするためのフィールドがあります。
注記
TODO: DXIL メタデータには、ターゲットタイプの一部として表現されていない 2 つのフィールド、IsGloballyCoherent
と HasCounter
があります。
これらは分析から派生しているため、タイプに格納すると、コンパイラパイプライン中にタイプを変更する必要があります。それは現実的ではありません。コンパイラパイプライン中にこの情報を IR にシリアル化する必要があるかどうかは、私には完全には明らかではありません。おそらく、必要なときに情報を計算できる分析パスで済ませることができるでしょう。
分析が不十分な場合は、annotateHandle
に似たもの(ただし、これら 2 つのプロパティに限定される)か、ハンドル作成でこれらをエンコードする必要があります。
リソースの種類¶
いくつかのことがパラメータ化されていますが、さまざまなリソースの HLSL 表現と同様の TargetExtTypes
のセットを定義します。これは DXIL とは異なり、「dx.srv」や「dx.uav」などのタイプに簡略化すると、これらのタイプの操作が過度に一般的になる必要があることを意味します。
バッファ¶
target("dx.TypedBuffer", ElementType, IsWriteable, IsROV, IsSigned)
target("dx.RawBuffer", ElementType, IsWriteable, IsROV)
DXIL の TypedBuffer で動作する 16 バイトの bufferLoad / bufferStore 操作と、DXIL の RawBuffer および StructuredBuffer に使用される rawBufferLoad / rawBufferStore 操作の違いを考慮するには、2 つの別々のバッファタイプが必要です。後者を操作の名前に合わせて「RawBuffer」と呼びますが、Raw と Structured の両方のバリアントを表すことができます。
HLSL の Buffer および RWBuffer は、スカラー整数または浮動小数点型、または最大 4 つのそのような型のベクトルである要素型を持つ TypedBuffer として表されます。HLSL の ByteAddressBuffer は、i8 要素型を持つ RawBuffer です。HLSL の StructuredBuffer は、構造体、ベクトル、またはスカラー型を持つ RawBuffer です。
ここでの 1 つの残念な必要性は、TypedBuffer が符号付き整数と符号なし整数を区別するために追加のパラメータを必要とすることです。これは、LLVM IR の int 型に符号がないため、この情報を保持するにはサイドチャネルが必要になるためです。
これらのタイプは、一般に BufferLoad および BufferStore 操作、およびアトミックで使用されます。
これらのすべてのタイプのバリアントを記述するためのフィールドがいくつかあります
フィールド |
説明 |
---|---|
ElementType |
|
IsWriteable |
フィールドが書き込み可能かどうか。これは、SRV(書き込み不可)と UAV(書き込み可能)を区別します。 |
IsROV |
UAV がラスタライザー順序ビューであるかどうか。SRV の場合は常に |
IsSigned |
int 要素型が符号付きかどうか(「dx.TypedBuffer」のみ) |
リソース操作¶
リソースハンドル¶
llvm.dx.handle.*
組み込み関数を使用して、IR でリソースをインスタンス化するいくつかの異なる方法を提供します。これらの組み込み関数は戻り値の型でオーバーロードされ、リソースの適切なハンドルを返し、組み込み関数の引数にバインディング情報を表します。
必要な 3 つの操作は、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
がそれを包含しているためです。
Lowering を簡単にするために、バインディング自体の LowerBound からのインデックスではなく、バインディング空間の先頭からのインデックスを使用するという点で DXIL と一致させます。
引数 |
タイプ |
説明 |
|
---|---|---|---|
戻り値 |
|
操作可能なハンドル |
|
|
1 |
|
このリソースのルート署名内のレジスタ空間 ID。 |
|
2 |
|
i32 |
|
3 |
|
%range_size |
|
4 |
|
バインディングの範囲サイズ。 |
|
5 |
i32 |
アクセスするバインディング空間の先頭からのインデックス。 |
注記
%non-uniform
i1
; 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 |
|
TODO: 均一性ビットを削除できますか?均一性分析から導き出せると思います… |
|
1 |
i32 |
アクセスするバインディング空間の先頭からのインデックス。 |
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の場合、単純なインデックスを介して16バイトの「行」のデータを読み書きする`llvm.dx.typedBufferLoad`と`llvm.dx.typedBufferStore`があります。RawBufferの場合、必要に応じてインデックス付け、ロード、およびストアできるポインタを返す`llvm.dx.rawBufferPtr`があります。
型指定されたロードおよびストア操作は、常に正確に16バイトのデータを操作するため、有効なオーバーロードはわずかです。32ビット以下の型の場合、`<4 x i32>`、`<4 x float>`、または`<4 x half>`などの4要素ベクトルを操作します。16ビットの場合、各16ビット値は32ビットのストレージを占有することに注意してください。64ビット型の場合、2要素ベクトル(`<2 x double>`または`<2 x i64>`)を操作します。HLSLレベルで`Buffer
注記
DXCでは、`Buffer
TypedBuffer組み込み関数は、bufferLoadおよびbufferStore操作に Lowering され、RawBufferPtrによってアクセスされるメモリに対する操作は、rawBufferLoadおよびrawBufferStoreに Lowering されます。1.2より前のDXILバージョンをサポートする場合、RawBufferのロードとストアを非raw操作にも Lowering する必要があることに注意してください。
注記
TODO: ここでCheckAccessFullyMappedを考慮する必要があります。
DXILでは、ロード操作は常に`i32`ステータス値を返しますが、これは使用されていない場合、あまり人間工学に基づいていません。(1) いさぎよくロードに常に`{%ret_type, %i32}`を返させる、(2) ステータスが使用されている場合にのみバリアントを作成するかシグネチャを更新する、(3) これをサイドバンドチャネルのどこかに隠す、のいずれかを実行できます。(2)に傾いていますが、(1)の醜さがシンプルさを上回ると納得させられるかもしれません。
引数 |
タイプ |
説明 |
|
---|---|---|---|
戻り値 |
バッファの型の4要素または2要素ベクトル |
バッファからロードされたデータ |
|
|
0 |
|
ロード元のバッファ |
|
1 |
|
バッファへのインデックス |
i1
%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要素ベクトル |
ストアするデータ |
i1
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 |
|
バッファへのインデックス |
i1
; 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