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.createHandledx.op.createHandleFromBindingdx.op.createHandleFromHeap、および dx.op.createHandleForLib に必要に応じて変換します。

タイプ情報

リソースを介してアクセスできるデータのタイプ。バッファとテクスチャの場合、これは floatfloat4 などの単純なタイプ、構造体、または生のバイトにすることができます。定数バッファの場合、これは単なるサイズです。サンプラーの場合、これはサンプラーの種類です。

LLVM では、この情報をリソースの target() タイプのパラメータとして埋め込みます。リソースの種類 を参照してください。

リソースの種類の情報

リソースの種類。HLSL には、ByteAddressBufferRWTexture2DRasterizerOrderedStructuredBuffer などがあります。これらは、IsUAVIsROV などの特定のプロパティのフィールドを持つ RawBufferTexture2D などの DXIL の種類にマップされます。

LLVM では、これを target() タイプで表現します。タイプ情報から派生可能な情報は省略しますが、必要に応じて IsWriteableIsROV、および SampleCount をエンコードするためのフィールドがあります。

注記

TODO: DXIL メタデータには、ターゲットタイプの一部として表現されていない 2 つのフィールド、IsGloballyCoherentHasCounter があります。

これらは分析から派生しているため、タイプに格納すると、コンパイラパイプライン中にタイプを変更する必要があります。それは現実的ではありません。コンパイラパイプライン中にこの情報を 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 操作、およびアトミックで使用されます。

これらのすべてのタイプのバリアントを記述するためのフィールドがいくつかあります

表 102 バッファフィールド

フィールド

説明

ElementType

i8v4f32、または構造体型などの単一要素の型。

IsWriteable

フィールドが書き込み可能かどうか。これは、SRV(書き込み不可)と UAV(書き込み可能)を区別します。

IsROV

UAV がラスタライザー順序ビューであるかどうか。SRV の場合は常に 0 です。

IsSigned

int 要素型が符号付きかどうか(「dx.TypedBuffer」のみ)

リソース操作

リソースハンドル

llvm.dx.handle.* 組み込み関数を使用して、IR でリソースをインスタンス化するいくつかの異なる方法を提供します。これらの組み込み関数は戻り値の型でオーバーロードされ、リソースの適切なハンドルを返し、組み込み関数の引数にバインディング情報を表します。

必要な 3 つの操作は、llvm.dx.handle.fromBindingllvm.dx.handle.fromHeap、および llvm.dx.handle.fromPointer です。これらは、DXIL 操作の dx.op.createHandleFromBindingdx.op.createHandleFromHeap、および dx.op.createHandleForLib とほぼ同等ですが、後続の dx.op.annotateHandle 操作を折りたたんでいます。 dx.op.createHandle のアナログがないことに注意してください。 dx.op.createHandleFromBinding がそれを包含しているためです。

Lowering を簡単にするために、バインディング自体の LowerBound からのインデックスではなく、バインディング空間の先頭からのインデックスを使用するという点で DXIL と一致させます。

表 103 @llvm.dx.handle.fromBinding

引数

タイプ

説明

戻り値

target() タイプ

操作可能なハンドル

%reg_space

1

i32

このリソースのルート署名内のレジスタ空間 ID。

%lower_bound

2

i32

i32

レジスタ空間内のバインディングの下限。

3

i32

%range_size

i32

4

i32

バインディングの範囲サイズ。

%index

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)
リソースインデックスが不均一になる可能性がある場合は、true である必要があります。

引数

タイプ

説明

戻り値

target() タイプ

操作可能なハンドル

i32

0

i32

TODO: 均一性ビットを削除できますか?均一性分析から導き出せると思います…

%index

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`のような型が使用される場合、これは各16バイト行の単一の浮動小数点数を操作することが想定されています。つまり、ロードは`<4 x float>`バリアントを使用して最初の要素を抽出します。

注記

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)の醜さがシンプルさを上回ると納得させられるかもしれません。

表 105 `@llvm.dx.typedBufferLoad`

引数

タイプ

説明

戻り値

バッファの型の4要素または2要素ベクトル

バッファからロードされたデータ

%buffer

0

`target(dx.TypedBuffer, ...)`

ロード元のバッファ

i32

1

i32

バッファへのインデックス

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)
表 106 `@llvm.dx.typedBufferStore`

引数

タイプ

説明

戻り値

void

%buffer

0

`target(dx.TypedBuffer, ...)`

ストア先バッファ

i32

1

i32

バッファへのインデックス

%data

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)
表 107 `@llvm.dx.rawBufferPtr`

引数

タイプ

説明

戻り値

ptr

バッファの要素へのポインタ

%buffer

0

`target(dx.RawBuffer, ...)`

ロード元のバッファ

i32

1

i32

バッファへのインデックス

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