日別アーカイブ: 2009年6月12日

Direct3D11/DirectX11 ComputeShader 4.0 を使う

DirectX11 の ComputeShader が実際に動くようになりました。

Direct3D11 GeForce driver 186.08 beta

ComputeShader 4.0 は FeatureLevel 10.0。つまり 1世代前の GPU で動作します。
cs4.0/4.1 の存在自体は November 2008 SDK でも確認できました。
しかしながら実際に使うためにはドライバの対応を待つ必要があったわけです。

本来の ComputeShader (cs5.0) の仕様に対して cs4.0 はサブセットとなります。
機能制限のある cs4.0 は、ぱっと見 PixelShader とほとんど違わないし
どこにメリットがあるのかあまりわからないかもしれません。
とりあえず目に付いた ComputeShader4.0 の利点は次の通り。

・シェーダーがバッファに書き込みできる
・共有メモリへアクセスできる

描画パイプラインに組み込まれたシェーダーの場合、ストリームとして動作するため
出力位置は固定でした。
ComputeShader は戻り値を持たず、代わりに UnorderedAccessView を通して
リソースの任意の位置にデータを書き込むことができます。

共有メモリはスレッド間でデータの受け渡しができるプロセッサ内部のローカルメモリです。
高速アクセス可能で他のスレッドが書き込んだ値を参照することもできます。

VertexShader 等を通さずに直接シェーダーのみ実行できるのも ComputeShader
の特徴となります。

●実行とデータの受け渡し

ComputeShader を実行する命令は Dispatch() です。

iDeviceContext->CSSetShader( iCS, NULL, 0 );
UINT	v= 0;
iDeviceContext->CSSetUnorderedAccessViews( 0, 1, &iOutputUAView, &v );
iDeviceContext->CSSetShaderResources( 0, 1, &iInputView );
iDeviceContext->Dispatch( 1, 1, 1 );

この例では入力が ShaderResourceView (SRV)、出力は
UnroderedAccessView (UAV) です。

入力データは何らかの形で SRV のリソースに書き込んでおきます。
ConstantBuffer や Texture へ書き込む手順と変わらないので難しくありません。

実行結果は UAV で受け取ります。
UAV のリソースは RenderTarget 同様 GPU が書き込むため D3D11_USAGE_DEFAULT
を指定しておきます。
デバッグ時など、結果を何らかの形で CPU で受け取りたい場合は
D3D11_USAGE_STAGING のバッファにコピーしなければなりません。

STAGING リソースから読み込む場合 Map() を使いますが、STAGING への Map は
ChildContext で実行することができませんでした。
STAGING は直接メモリアクセスしているのに、コマンドをバッファにためるだけでは
意味がないので当然かもしれません。
スレッド対応のためにすべて Deferred Context 化している場合は注意が必要です。
Immediate Context を使います。

(1) SRV リソースへ書き込み
  ・UpdateSubresource() → USAGE_DEFAULT
  ・Map() → USAGE_STAGING → CopyResource() → USAGE_DEFAULT
  ・Map() → USAGE_DYNAMIC
(2) 実行 Dispatch()
(3) UAV から結果受け取り
  ・USAGE_DEFAULT → CopyResource() → USAGE_STAGING → Map()

このように、ComputeShader の場合 CPU とのデータやりとりも一般のシェーダーと
同じ手順が必要になります。
CUDA とか ATI Stream (CAL) より若干手間がいるかもしれません。

ComputeShader 4.x の場合、UAV は RAW または Structured Buffer のみ使用できます。
同時に与えられる UAV も 1つだけです。

● ComputeShader の実行回数と ID

PixelShader なら 2次元の面積で描画するピクセル数が決定します。
このピクセルの数だけ Shader を実行することになります。

ComputeShader の場合 3次元の体積で実行するスレッドの数を指定できます。

例えば XYZ= ( 2, 3, 1 ) の場合、2 x 3 x 1 = 6 スレッド走ります。
各スレッドは自分がどの座標に対応しているのか 3次元の id 番号を
受け取ることができるわけです。

実行するスレッドの個数の指定は二カ所で行います。

1. Dispatch() の引数
2. ComputeShader のアトリビュート numthreads 指定

[numthreads(3,2,1)]  // ←これ
void cmain1(
		uint3 did : SV_DispatchThreadID,
		uint3 gid: SV_GroupThreadID,
		uint3 group: SV_GroupID,
		uint gindex : SV_GroupIndex
				)
{
	int	index= did.x + did.y * 10;
~

両者の指定は似ていますが別物です。
6次元の id 値を持っているといえるかもしれません。

・1グループ内のスレッド実行回数の定義

[numthreads()] で指定するのは 1グループ内で実行するスレッドの数です。
シェーダーのコンパイル時に決定するので static な値となります。

  uint3 SV_GroupThreadID // グループ内のスレッド番号

semantic SV_GroupThreadID はグループ内の id 番号を受け取ることができます。
[numthreads(3,2,1)] は次の 6 通り。

(0,0,0) (1,0,0) (2,0,0) (0,1,0), (1,1,0), (2,1,0)

  int SV_GroupIndex // グループ内の通し番号

SV_GroupIndex はグループ内の連番です。
[numthreads(3,2,1)] の場合 0~5 の連番を受け取ります。

・グループ自体を何回実行するか指定する

Dispatch() の引数は、シェーダーで定義したスレッド Group をさらに何回実行するか
指定します。例えば [numthreads(3,2,1)] を Dispatch( 2, 1, 1 ) で呼び出すと
合計 12スレッドプログラムが走ることになります。

  uint3 SV_GroupID // グループの番号

SV_GroupID はグループにつけられた番号です。
例えば Dispatch( 10, 2, 1 ) で呼び出すと (0,0,0)~(9,1,0) まで。
同じグループの中のスレッドはすべて同じ値です。

  uint3 SV_DispatchThreadID // 3次元の通し番号

SV_DispatchThreadID は numthreads も Dispatch も区別無く、3次元に展開した
スレッドの通し番号になります。
例えば [numthreads(3,2,1)] を Dispatch( 4, 9, 1 ) で呼び出すと 216スレッド。
(3*4, 2*9, 1*1) の範囲なので (0,0,0)~(11,17,0) までの値を取ります。

[numthreads(3,2,1)] を Dispatch( 2, 1, 1 ) で呼び出した場合の ID 一覧。

SV_GroupID  SV_GroupThreadID  SV_GroupIndex  SV_DispatchThreadID
(0,0,0)     (0,0,0)           0              (0,0,0)
(0,0,0)     (1,0,0)           1              (1,0,0)
(0,0,0)     (2,0,0)           2              (2,0,0)
(0,0,0)     (0,1,0)           3              (0,1,0)
(0,0,0)     (1,1,0)           4              (1,1,0)
(0,0,0)     (2,1,0)           5              (2,1,0)
(1,0,0)     (0,0,0)           0              (3,0,0)
(1,0,0)     (1,0,0)           1              (4,0,0)
(1,0,0)     (2,0,0)           2              (5,0,0)
(1,0,0)     (0,1,0)           3              (3,1,0)
(1,0,0)     (1,1,0)           4              (4,1,0)
(1,0,0)     (2,1,0)           5              (5,1,0)

ComputeShader4.x の場合スレッド数の 3番目の値は必ず 1 でなければなりません。
つまり PixelShader 同様 2次元の id 指定に制限されます。

● Group

上記のようにスレッドの実行にはグループという概念があります。
プログラムの実行はこのグループ単位に行われているようです。

・スレッド共有メモリは group 毎に確保される
・共有メモリへのアクセスは group 単位で同期できる

●共有メモリ

ComputeShader はローカルな共有メモリにアクセスすることができます。
メモリ空間にマップされるわけではないので Buffer より高速です。
ShaderResource と違い読み書きも可能です。
同一グループ内の他のスレッドと共有できます。他のスレッドでも書き込んだ値を参照可能です。

・レジスタ = スレッド固有
・共有メモリ = グループ内のみ共有
・リソース(Buffer)

Shader プログラム内で groupshared 宣言すると共有メモリになります。

ComputeShader 4.x ではいくつか制限があります。
容量は 16KB までです。
また共有メモリの宣言はグループ内のスレッド数と同数でなければならず、
書き込めるのは自分の SV_GroupIndex に対応するメモリだけ。
宣言できる共有メモリも1つだけです。
その代わり Structured のように構造体を与えることが可能です。

struct SharedMem {
	float2	fl;
	uint	id;
};

groupshared SharedMem	buf[6]; // 3x2

[numthreads(3,2,1)]
void cmain(
		uint3 did : SV_DispatchThreadID,
		uint3 gid: SV_GroupThreadID,
		uint3 group: SV_GroupID,
		uint gindex : SV_GroupIndex
				)
{
	buf[  gindex ].fl.x= gindex;
~

groupshared 指定は、アセンブラコードだと下記の宣言になります。
g0 が共有メモリです。

dcl_tgsm_structured g0, 32, 6

●同期

ComputeShader 4.x の場合 Interlock 系の Atomic なオペレーション命令は
ありませんが、メモリアクセスに対する Barrier 同期は使えるようです。

GroupMemoryBarrierWithGroupSync();
GroupMemoryBarrier();
AllMemoryBarrierWithGroupSync();
AllMemoryBarrier();

GeForce 用ドライバ 186.08 beta ではこれらの命令が動作しませんでした。
命令のコンパイル自体は可能だし DebugLayer も無言だったので、ドライバがまだ
対応していないだけかもしれません。
Reference Driver を使うと Shader4.0 でも動作しました。

アセンブラでは sync 命令が挿入されています。

sync_g              GroupMemoryBarrier()
sync_g_t            GroupMemoryBarrierWithGroupSync()
sync_uglobal_g      AllMemoryBarrier()
sync_uglobal_g_t    AllMemoryBarrierWithGroupSync()

関連エントリ
Direct3D11/DirectX11 (6) D3D11 の ComputeShader を使ってみる
Direct3D11/DirectX11 (4) FeatureLevel と旧 GPU の互換性、テクスチャ形式など
Direct3D11 (DirectX11) シェーダーの書き込み RWBuffer 他