次の方法で共有


HLSL でのリソース バインディング

このトピックでは、高レベル シェーダー言語 (HLSL) シェーダー モデル 5.1 とDirect3D 12を使用する特定の機能について説明します。 すべての Direct3D 12 ハードウェアは Shader Model 5.1 をサポートしているため、このモデルのサポートはハードウェアの機能レベルに依存しません。

リソースの種類と配列

シェーダー モデル 5 (SM5.0) リソース構文では、キーワード (keyword)をregister使用して、リソースに関する重要な情報を HLSL コンパイラに中継します。 たとえば、次のステートメントでは、スロット t3、t4、t5、t6 でバインドされた 4 つのテクスチャの配列を宣言しています。 t3 はこのステートメントの中で示される唯一のレジスタ スロットで、4 つから成る配列の最初の要素になります。

Texture2D<float4> tex1[4] : register(t3)

HLSL の Shader Model 5.1 (SM5.1) のリソース構文は、移植を容易にするために既存のレジスタ リソース構文に基づいています。 HLSL のDirect3D 12リソースは、論理レジスタ空間内の仮想レジスタにバインドされます。

  • t - シェーダー リソース ビュー (SRV) 用
  • s - サンプラー用
  • u - 順序指定されていないアクセス ビュー (UAV) 用
  • b - 定数バッファー ビュー (CBV) 用

シェーダーを参照するルート署名は、宣言されたレジスタ スロットと互換性がある必要があります。 たとえば、ルート署名の次の部分は、スロット t0 から t98 を含む記述子テーブルを記述しているため、テクスチャ スロット t3 から t6 の使用と互換性があります。

DescriptorTable( CBV(b1), SRV(t0,numDescriptors=99), CBV(b2) )

リソース宣言は、次のように、スカラー、1D 配列、または多次元配列になる場合があります。

Texture2D<float4> tex1 : register(t3,  space0)
Texture2D<float4> tex2[4] : register(t10)
Texture2D<float4> tex3[7][5][3] : register(t20, space1)

SM5.1 では、SM5.0 と同じリソースの種類と要素の種類が使用されます。 SM5.1 宣言の制限は柔軟性が高く、ランタイム/ハードウェアの制限によってのみ制約されます。 キーワード (keyword)はspace、宣言された変数がどの論理レジスタ領域にバインドされているかを指定します。 キーワード (keyword)をspace省略すると、既定のスペース インデックス 0 が暗黙的に範囲に割り当てられます (そのため、上記のtex2範囲は にありますspace0)。 register(t3, space0) は、t3 を register(t3, space1)含む可能性のある別のスペース内の配列と競合することはありません。

配列リソースには無制限のサイズが含まれる場合があります。これは、最初の次元を空にするか、0 を指定して宣言されます。

Texture2D<float4> tex1[] : register(t0)

一致する記述子テーブルは次のようになります。

DescriptorTable( CBV(b1), UAV(u0, numDescriptors = 4), SRV(t0, numDescriptors=unbounded) )

HLSL の無制限の配列は記述子テーブルの numDescriptors で設定された固定数と一致し、HLSL の固定サイズは記述子テーブルの unbounded 宣言と一致します。

多次元配列は許可されます。これには、サイズが無制限のものも含まれます。 このような多次元配列は、レジスタ空間内で平坦化されます。

Texture2D<float4> tex2[3000][10] : register(t0, space0); // t0-t29999 in space0
Texture2D<float4> tex3[0][5][3] : register(t5, space1)

リソース範囲のエイリアシングは許可されていません。 つまり、リソースの種類 (t、s、u、b) ごとに、宣言されたレジスタ範囲が重複しないようにする必要があります。 これには無制限の範囲も含まれます。 異なるレジスタ空間で宣言された範囲が重複することはありません。 無制限 tex2 (上記) は に space0存在し、無制限 tex3 は に space1存在し、重複しないように注意してください。

配列として宣言されているリソースへのアクセスは、インデックスを作成するのと同じくらい簡単です。

Texture2D<float4> tex1[400] : register(t3);
sampler samp[7] : register(s0);
tex1[myMaterialID].Sample(samp[samplerID], texCoords);

インデックス (myMaterialID および samplerID 上記のコード) の使用には、 ウェーブ内での変更が許可されないという点で、重要な既定の制限があります。 インスタンス化に基づくインデックスの変更でさえ、変更としてカウントされます。

インデックスの変更が必要な場合は、インデックスに NonUniformResourceIndex 修飾子を指定します。次に例を示します。

tex1[NonUniformResourceIndex(myMaterialID)].Sample(samp[NonUniformResourceIndex(samplerID)], texCoords);

一部のハードウェアでは、この修飾子を使用すると、(スレッド間を含めて) 正確性を確保するための追加コードが生成されますが、パフォーマンス コストの低下はわずかです。 この修飾子を使用せずに描画またはディスパッチ内でインデックスが変更されると、結果は未定義です。

記述子配列とテクスチャ配列

テクスチャ配列は DirectX 10 から使用可能になりました。 テクスチャ配列に必要な記述子は 1 つですが、すべての配列スライスは同じ形式、幅、高さ、および MIP 数を共有する必要があります。 また、この配列は、仮想アドレス空間内の連続する範囲を占める必要があります。 次のコードは、シェーダーからテクスチャ配列にアクセスする例を示します。

Texture2DArray<float4> myTex2DArray : register(t0); // t0
float3 myCoord(1.0f,1.4f,2.2f); // 2.2f is array index (rounded to int)
color = myTex2DArray.Sample(mySampler, myCoord);

テクスチャ配列では、NonUniformResourceIndex のような修飾子を必要とすることなく、インデックスを自由に変更できます。

同等の記述子配列は次のようになります。

Texture2D<float4> myArrayOfTex2D[] : register(t0); // t0+
float2 myCoord(1.0f, 1.4f);
color = myArrayOfTex2D[2].Sample(mySampler,myCoord); // 2 is index

配列インデックスには使いにくい float が myArrayOfTex2D[2] に置き換えられていることに注意してください。 また、記述子配列は、次元に関してより高い柔軟性を提供します。 この種類 (この例では Texture2D) は変更できませんが、形式、幅、高さ、および MIP 数はすべて、各記述子によって変更できます。

テクスチャ配列の記述子配列を使用しても問題ありません。

Texture2DArray<float4> myArrayOfTex2DArrays[2] : register(t0);

それぞれに記述子が含まれている構造体の配列を宣言するのは適切ではありません。たとえば、次のコードはサポートされません。

struct myStruct {
    Texture2D                    a; 
    Texture2D                    b;
    ConstantBuffer<myConstants>  c;
};
myStruct foo[10000] : register(....);

この場合、メモリ レイアウト abcabcabc.... は許可されますが、これは言語の制限であり、サポートされていません。 これを行うためのサポートされている方法の 1 つを次に示します。ただし、この場合のメモリ レイアウトは aaa...bbb...ccc... となります。

Texture2D                     a[10000] : register(t0);
Texture2D                     b[10000] : register(t10000);
ConstantBuffer<myConstants>   c[10000] : register(b0);

abcabcabc.... というメモリ レイアウトを実現するには、myStruct 構造体を使用せずに記述子テーブルを使用してください。

リソースのエイリアシング

HLSL シェーダーで指定されるリソース範囲は論理的な範囲です。 これらは、実行時にルート署名メカニズムを介して具体的なヒープ範囲にバインドされます。 通常、論理的な範囲は、他のヒープ範囲と重複しないヒープ範囲にマップされます。 ただし、ルート署名メカニズムにより、互換性のある型のヒープ範囲をエイリアス化 (重複) することができます。 たとえば、上の例の tex2tex3 の範囲は、同じ (または重複する) ヒープ範囲にマップされることがあります。これは、HLSL プログラムでテクスチャのエイリアシングの効果があります。 このようなエイリアシングが必要な場合は、エフェクト コンパイラ ツール (FXC) の /res_may_alias オプションを使用して設定される D3D10_SHADER_RESOURCES_MAY_ALIAS オプションを使用してシェーダーをコンパイルする必要があります。 このオプションは、リソースがエイリアス化される可能性があるという想定のもとで、特定のロードまたはストアの最適化を妨げることによって、コンパイラが適切なコードを生成するようにします。

分岐と派生物

SM5.1 では、リソース インデックス tex2[idx].Sample(…) に対する制限はありません。インデックス idx には、リテラル定数、cbuffer 定数、または補間された値を使用できます。 このプログラミング モデルは非常に高い柔軟性を提供しますが、注意すべき問題がいくつかあります。

  • クアッド全体でインデックスが分岐する場合、ハードウェアによって計算された派生物および LOD などの派生数は未定義になる可能性があります。 この場合、HLSL コンパイラはベスト エフォート方式で警告を出しますが、シェーダーのコンパイルを妨げません。 この動作は、分岐制御フローで派生物を計算することに似ています。
  • リソース インデックスが分岐する場合は、ハードウェアが複数のリソースに対して操作を実行する必要があるため、同一インデックスの場合と比較してパフォーマンスが低下します。 分岐する可能性のあるリソース インデックスは、HLSL コードの NonUniformResourceIndex 関数でマークする必要があります。 そうしないと、結果が未定義になります。

ピクセル シェーダーの UAV

SM5.0 の場合と同様、SM5.1 では、ピクセル シェーダー内の UAV 範囲に対する制約はありません。

定数バッファー

SM5.1 の定数バッファー (cbuffer) の構文は SM5.0 から変更され、開発者が定数バッファーにインデックスを作成できるようになりました。 インデックスの作成可能な定数バッファーを有効にするために、SM5.1 では ConstantBuffer "テンプレート" コンストラクトが導入されています。

struct Foo
{
    float4 a;
    int2 b;
};
ConstantBuffer<Foo> myCB1[2][3] : register(b2, space1);
ConstantBuffer<Foo> myCB2 : register(b0, space1);

上記のコードでは、Foo 型でサイズ 6 の定数バッファー変数 myCB1、およびスカラー型の定数バッファー変数 myCB2 を宣言しています。 定数バッファー変数には、シェーダー内で次のようにインデックスを作成できるようになりました。

myCB1[i][j].a.xyzw
myCB2.b.yy

フィールド "a" と "b" はグローバル変数にはなりませんが、フィールドとして扱う必要があります。 下位互換性のために、SM5.1 では、スカラー cbuffer に対して古い cbuffer の概念をサポートしています。 次のステートメントでは、SM5.0 と同様に、"a" および "b" はグローバルな読み取り専用変数になります。 ただし、このような古いスタイルの cbuffer にはインデックスを作成できません。

cbuffer : register(b1)
{
    float4 a;
    int2 b;
};

現在、シェーダー コンパイラは、ユーザー定義の構造体に対してのみ ConstantBuffer テンプレートをサポートしています。

互換性上の理由から、HLSL コンパイラは space0 で宣言された範囲にリソース レジスタを自動的に割り当てる可能性があります。 register 句で "space" が省略されている場合は、既定の space0 が使用されます。 コンパイラは、first-hole-fits ヒューリスティックを使用してレジスタを割り当てます。 この割り当ては、リフレクション API を介して取得できます。この API は、空間用の Space フィールドを追加するよう拡張されているのに対し、BindPoint フィールドは、リソース レジスタ範囲の下限を示します。

SM5.1 におけるバイトコードの変更

SM5.1 では、命令内でのリソース レジスタの宣言方法と参照方法が変更されています。 この構文には、グループ共有メモリ レジスタの場合と同様に、レジスタ "変数" の宣言が含まれます。

Texture2D<float4> tex0          : register(t5,  space0);
Texture2D<float4> tex1[][5][3]  : register(t10, space0);
Texture2D<float4> tex2[8]       : register(t0,  space1);
SamplerState samp0              : register(s5, space0);

float4 main(float4 coord : COORD) : SV_TARGET
{
    float4 r = coord;
    r += tex0.Sample(samp0, r.xy);
    r += tex2[r.x].Sample(samp0, r.xy);
    r += tex1[r.x][r.y][r.z].Sample(samp0, r.xy);
    return r;
}

これを逆アセンブルすると、次のようになります。

// Resource Bindings:
//
// Name                                 Type  Format         Dim    ID   HLSL Bind     Count
// ------------------------------ ---------- ------- ----------- -----   --------- ---------
// samp0                             sampler      NA          NA     S0    a5            1
// tex0                              texture  float4          2d     T0    t5            1
// tex1[0][5][3]                     texture  float4          2d     T1   t10        unbounded
// tex2[8]                           texture  float4          2d     T2    t0.space1     8
//
//
//
// Input signature:
//
// Name                 Index   Mask Register SysValue  Format   Used
// -------------------- ----- ------ -------- -------- ------- ------
// COORD                    0   xyzw        0     NONE   float   xyzw
//
//
// Output signature:
//
// Name                 Index   Mask Register SysValue  Format   Used
// -------------------- ----- ------ -------- -------- ------- ------
// SV_TARGET                0   xyzw        0   TARGET   float   xyzw
//
ps_5_1
dcl_globalFlags refactoringAllowed
dcl_sampler s0[5:5], mode_default, space=0
dcl_resource_texture2d (float,float,float,float) t0[5:5], space=0
dcl_resource_texture2d (float,float,float,float) t1[10:*], space=0
dcl_resource_texture2d (float,float,float,float) t2[0:7], space=1
dcl_input_ps linear v0.xyzw
dcl_output o0.xyzw
dcl_temps 2
sample r0.xyzw, v0.xyxx, t0[0].xyzw, s0[5]
add r0.xyzw, r0.xyzw, v0.xyzw
ftou r1.x, r0.x
sample r1.xyzw, r0.xyxx, t2[r1.x + 0].xyzw, s0[5]
add r0.xyzw, r0.xyzw, r1.xyzw
ftou r1.xyz, r0.zyxz
imul null, r1.yz, r1.zzyz, l(0, 15, 3, 0)
iadd r1.y, r1.z, r1.y
iadd r1.x, r1.x, r1.y
sample r1.xyzw, r0.xyxx, t1[r1.x + 10].xyzw, s0[5]
add o0.xyzw, r0.xyzw, r1.xyzw
ret
// Approximately 12 instruction slots are used.

それぞれのシェーダー リソース範囲は、シェーダー バイトコードに固有の ID (名前) を持つようになりました。 たとえば、tex1 (t10) テクスチャ配列は、シェーダー バイトコードでは "T1" になります。 各リソース範囲に一意の ID を付与すると、次の 2 つのことが可能になります。

  • 命令でインデックスが作成されているリソース範囲 (dcl_resource_texture2dを参照) を明確に特定します (サンプル命令を参照)。
  • 要素の種類、ストライド サイズ、ラスター操作モードなど、一連の属性を宣言にアタッチする。

範囲の ID は、HLSL の下限宣言とは関連していないことに注意してください。

リフレクション リソース バインドの順序 (上部のリスト) とシェーダー宣言命令 (dcl_*) は、HLSL 変数とバイトコード ID の対応関係を識別するのに役立ちます。

SM5.1 の各宣言命令では、3D オペランドを使用して、範囲 ID、下限値、上限値を定義します。 レジスタ空間を指定するために、1 つの追加のトークンが出力されます。 範囲の追加のプロパティを伝達するために他のトークンが出力される場合もあります。たとえば、cbuffer または構造化バッファー宣言命令は、cbuffer または構造体のサイズを出力します。 エンコードの詳細は、d3d12TokenizedProgramFormat.h および D3D10ShaderBinary::CShaderCodeParser で確認できます。

SM5.1 命令では、(SM5.0 の場合のように) 命令の一部として追加のリソース オペランド情報を出力しません。 現在、この情報は、宣言命令に含まれます。 SM5.0 では、インデックスの作成により宣言との関連付けがわかりにくくなったため、リソースにインデックスを作成する命令では、拡張オペコード トークンにリソースの属性を記述する必要がありました。 SM5.1 では、各 ID ("t1" など) は、必要なリソース情報を記述する単一の宣言に明確に関連付けられます。 したがって、リソース情報を記述するための命令で使用されていた拡張オペコード トークンは出力されなくなりました。

宣言以外の命令では、サンプラー、SRV、および UAV のリソース オペランドは 2D オペランドです。 最初のインデックスは、範囲 ID を指定するリテラル定数です。 2 番目のインデックスは、インデックスの線形化された値を表します。 この値は、ルート署名との相関性を高め、インデックスを調整するドライバー コンパイラの負担を軽減するために、(論理的な範囲の先頭ではなく) 対応するレジスタ空間の先頭を基準に計算されます。

CBV のリソース オペランドは 3D オペランドで、範囲のリテラル ID、定数バッファーのインデックス、定数バッファーの特定のインスタンスへのオフセットを含みます。

HLSL の宣言の例

HLSL プログラムは、ルート署名についてすべてを把握する必要はありません。 仮想 "レジスタ" バインド空間、SRV の場合は t#、UAV の場合は u#、CBV の場合は b#、サンプラーの場合は s# にバインドを割り当てたり、コンパイラに依存して割り当てを選択したり (後でシェーダー リフレクションを使用して結果のマッピングにクエリを実行したりします)。 ルート署名は、記述子テーブル、ルート記述子、およびルート定数をこの仮想レジスタ空間にマップします。

HLSL シェーダーに設定されている可能性のある宣言の例を次に示します。 ルート署名または記述子テーブルへの参照がないことに注意してください。

Texture2D foo[5] : register(t2);
Buffer bar : register(t7);
RWBuffer dataLog : register(u1);

Sampler samp : register(s0);

struct Data
{
    UINT index;
    float4 color;
};
ConstantBuffer<Data> myData : register(b0);

Texture2D terrain[] : register(t8); // Unbounded array
Texture2D misc[] : register(t0,space1); // Another unbounded array 
                                        // space1 avoids overlap with above t#

struct MoreData
{
    float4x4 xform;
};
ConstantBuffer<MoreData> myMoreData : register(b1);

struct Stuff
{
    float2 factor;
    UINT drawID;
};
ConstantBuffer<Stuff> myStuff[][3][8]  : register(b2, space3)