日別アーカイブ: 2008年11月11日

Direct3D11/DirectX11 (6) D3D11 の ComputeShader を使ってみる

ComputeShader の使い方はドキュメントも少なくまだよくわかっていません。
使用できる機能の共通性などから、PixelShader に近いか PixelShader の改良版
ではないかと予想していました。でも思ったより違いは多そうです。
Technical Preview (Beta) の段階なので、まだこれから仕様が変わる可能性もあります。

●作成と実行

シェーダーの生成はこれまでの PixelShader 等と同じです。
ShaderLinkage を指定できる点も同じ。

ID3D11Device*		iDevice;
ID3D11ComputeShader*	iCS;
iDevice->CreateComputeShader( blob, blobsize, NULL, &iCS );

ステート設定は CS~ 系の命令を使います。

ID3D11DeviceContext*	iContext;
iContext->CSSetShaderResources( 0, 1, srvlist );
iContext->CSSetShader( iCS, NULL, 0 );
UINT	uavcount= 256;
iContext->CSSetUnroderdAccessViews( 0, 1, &iUAV0, &uavcount );

他のシェーダーとの違いは、任意のアドレスに書き込み可能なリソース UAV を
設定する専用命令 CSSetUnroderdAccessViews() があることです。

UAV (UnorderdAccessView) は、RWBuffer や RWTexture2D 等の書き込み可能な
リソースへのアクセスを意味しています。

ComputeShader はストリーム出力を持たないので Shader は void 宣言されます。
値を返すには必ず出力先として UAV の設定が必要となるようです。

UAV は PixelShader でも使えるはずなのに SetUnorderdAccessView 相当の命令は
ありません。この辺の仕様は要調査です。
将来 10 世代 GPU 対応の ComputeShader 4.0/4.1 が使えるようになった場合も
出力先の扱いがどうなるか気になるところです。

ComputeShader の実行は Draw() ではなく専用命令 Dispatch() を使います。

iContext->Dispatch( 1, 1, 1 );

引数はマニュアルに載ってませんが、走らせるスレッドの数と考えられます。
おそらく CUDA や AMD Stream SDK の thread や block、domain 等に相当する
パラメータでしょう。

●ウィンドウレス実行

ComputeShader は描画とは関係なく実行できるはずです。
そこで Window も DXGI も使わずにシェーダーを走らせてみました。

D3D11CreateDevice(
		NULL,
		//D3D_DRIVER_TYPE_HARDWARE,
		//D3D_DRIVER_TYPE_WARP,
		D3D_DRIVER_TYPE_REFERENCE,
		NULL,
		D3D11_CREATE_DEVICE_DEBUG,
		NULL,
		0,
		D3D11_SDK_VERSION,
		&iDevice,
		NULL,
		&iContext
	);
// hlsl 読み込みは省略 → memory
ID3DBlob*	codeblob;
D3DCompile(
		memory,
		(size_t)size,
		fname,
		NULL,	// macro
		NULL,	// include
		"main",
		"cs_5_0",
		D3D10_SHADER_ENABLE_STRICTNESS,
		0,
		&codeblob,
		&errblob
	);
iDevice->CreateComputeShader(
		codeblob->GetBufferPointer(),
		codeblob->GetBufferSize(),
		NULL, &iCS );
codeblob->Release();


// 出力先
ID3D11Buffer*	iBufferC;
D3D11_BUFFER_DESC	bdesc;
bdesc.Usage= D3D11_USAGE_DEFAULT;
bdesc.BindFlags= D3D11_BIND_SHADER_RESOURCE|D3D11_BIND_UNORDERED_ACCESS;
bdesc.CPUAccessFlags= 0;
bdesc.MiscFlags= 0;
bdesc.ByteWidth= sizeof(float)*4*256;
bdesc.StructureByteStride= sizeof(float);
iDevice->CreateBuffer( &bdesc, NULL, &iBufferC );

ID3D11UnorderedAccessView*	iUAView;
D3D11_UNORDERED_ACCESS_VIEW_DESC	uavdesc;
uavdesc.Format= DXGI_FORMAT_R32_UINT;
uavdesc.ViewDimension= D3D11_UAV_DIMENSION_BUFFER;
uavdesc.Buffer.FirstElement= 0;
uavdesc.Buffer.NumElements= 256;
uavdesc.Buffer.Flags= 0;
iDevice->CreateUnorderedAccessView( iBufferC, &uavdesc, &iUAView );

// CPUアクセス用バッファ
ID3D11Buffer*	iBufferC2;
D3D11_BUFFER_DESC	bdesc;
bdesc.Usage= D3D11_USAGE_STAGING;
bdesc.BindFlags= 0;
bdesc.CPUAccessFlags= D3D11_CPU_ACCESS_WRITE|D3D11_CPU_ACCESS_READ;
bdesc.MiscFlags= 0;
bdesc.ByteWidth= sizeof(float)*4*256;
bdesc.StructureByteStride= sizeof(float);
iDevice->CreateBuffer( &bdesc, NULL, &iBufferC2 );

// 初期データ書き込み
D3D11_MAPPED_SUBRESOURCE	mapres;
iContext->Map( iBufferC2, 0, D3D11_MAP_WRITE, 0, &mapres );
unsigned int*	ptr= reinterpret_cast( mapres.pData );
ptr[0]= 0;
iContext->Unmap( iBufferC2, 0 );
iContext->CopyResource( iBufferC, iBufferC2 );

// 実行
for( int i= 0 ; i< 4 ; i++ ){
	iContext->CSSetShader( iCS, NULL, 0 );
	UINT	uavcount= 256;
	iContext->CSSetUnorderedAccessViews( 0, 1, &iUAView, &uavcount );
	iContext->Dispatch( 10, 2, 1 );
}

// 結果読み込みと表示
iContext->CopyResource( iBufferC2, iBufferC );
D3D11_MAPPED_SUBRESOURCE	mapres;
iContext->Map( iBufferC2, 0, D3D11_MAP_READ, 0, &mapres );
unsigned int*	ptr= reinterpret_cast( mapres.pData );
printf( "%d\n", ptr[0] );
iContext->Unmap( iBufferC2, 0 );

下記のシェーダーを実行すると結果は「360」。(45 x 2 x 4)
Windows や Present() とか無しにシェーダーが走りました。

// cs.hlsl
RWBuffer	a;

[numthreads(1,1,1)]
void main( uint3 tid : SV_DispatchThreadID )
{
	InterlockedAdd( a[0], tid.x );
}

Reference では InterlockedAdd でなく直接 a[0]+= tid.x と書いても同じ値に
なってしまいます。要注意です。
実際は Query 等を使って GPU の実行を待つ必要があるかもしれません。

●リソースの受け渡し

GPU が書き込めるリソースは D3D11_USAGE_DEFAULT だけなので、UAV は全部
D3D11_USAGE_DEFAULT になります。D3D11_USAGE_DEFAULT だと CPU で直接
読み出せないため、システム側に D3D11_USAGE_STAGING のミラーバッファを用意し
アクセスの度に CopyResource() しています。
初期化手順も多く、あまり簡単とはいえません。

CPU 側との連携やデータの読み書きの容易さを考えると、CUDA や AMD Stream SDK
を直接使った方が便利だと思います。おそらく CopyResource 相当の転送も自動で
やってくれています。

ComputeShader を使うメリットは、GPU 毎の SDK の違いを吸収できることと
Direct3D との連携の容易さでしょう。

Structured Buffer や Append Buffer も試そうとしましたが、フラグ設定や
組み合わせが複雑でなかなかうまくいきませんでした。

関連エントリ
Direct3D11/DirectX11 (5) WARP の試し方、Dynamic Shader Linkage
Direct3D11/DirectX11 (4) FeatureLevel と旧 GPU の互換性、テクスチャ形式など
Direct3D11 (DirectX11) シェーダーの書き込み RWBuffer 他
Direct3D11 の遅延描画、スレッド対応機能、シェーダー命令
Direct3D11 Technical Preview D3D11の互換性、WARP Driver