ComputeShader はかなり便利なことがわかってきました。
ポストフィルタなどのピクセル処理だけでなく、VertexShader の前段に頂点演算として
挿入したり、頂点シェーダーの代わりに使ったりも出来ます。
標準描画パイプラインへのちょっとした機能追加が簡単にできるようになった印象です。
これまでは Stream Output や PixelShader を使っていた処理が ComputeShader
だけで済むわけです。
●ここが簡単
・読み込み位置、書き込み位置が固定されない
与えるリソースはどれも任意アドレスに対してアクセス可能で、特別扱いする
ものがありません。
・設定が項目が少ない
ConstantBuffer, ShaderResourceView, UnorderedAccessView のわずか 3つだけ。
サンプラーを使う場合は + 1。
・設定が独立している
描画用のステートをいじる必要がないので、パラメータを保存したり復帰させなくて
済みます。たとえば Viewport とか、RenderTarget とか、
DepthStencilState とか!! 描画に影響与えないし、戻さなくてもいいんです。
特に 3番目。いちいち DepthStencilState を作ったり、Depth を disable にしたり
しなくても良いだけで CS ありがとう、といった感じ。
● CS の制限
今までの Shader から見れば制限無しに扱いやすい ComputeShader ですが、
VertexShader 代わりに使おうとするといくつか制限も生じます。
・グループ内の実行スレッド数はシェーダー内に記述し、実行時に書き換えできない。
・Dispatch() に与えられる実行回数は x,y,z それぞれ 65535 まで。
・グループ内のスレッド数は 1024 まで。
よって VertexShader のように、1~100万回 など任意の実行回数が与えられた場合に
どのように Compute Shader を呼び出せばよいのか悩みます。
1. 65535 回を超える場合
x, y, z に分けるにしろ、CS 内のアトリビュートで Group 内スレッド数を増やすにしろ、
実行回数が常に定数で割り切れるとは限らない。
2. 速度
ある程度グループ内のスレッド数を大きめの値にしなければ ComputeShader の実行
速度が落ちます。65535 回未満だからと言って、Dispatch() だけで回数を指定して
下記のようなシェーダーを走らせると非常に低速になります。
[numthreads(1,1,1)] void cmain_loop1( uint3 threadid : SV_DispatchThreadID ) { lmain( threadid.x ); }
●解決案
二通りの手段を考えてみました。
(1) 複数のシェーダーに分ける
numthreads = 32 などグループスレッド数を増やしたものと、numthreads = 1 の
端数を処理するスレッドに分けて実行します。
下のプログラムは 32 で割り切れる回数分 cmain_loop32 を実行し、端数を
cmain_loop1 で処理しています。
例えば 75 個のデータを処理するなら、cmain_loop32 を 2回、cmain_loop1 を 11回分
実行します。
// Compute Shader 5.0 cbuffer offset_T : register( b0 ) { uint threadid_offset; uint thread_total; uint r0; uint r1; }; [numthreads(32,1,1)] void cmain_loop32( uint3 threadid : SV_DispatchThreadID) { lmain( threadid.x ); } [numthreads(1,1,1)] void cmain_loop1( uint3 threadid : SV_DispatchThreadID ) { lmain( threadid.x + threadid_offset ); }
// C++ const int ThreadGroup= 32; int dcount1= data_count/ThreadGroup; int offset= dcount1*ThreadGroup; int dcount2= dcount - offset; offset_T cparam; cparam.threadid_offset= offset; cparam.thread_total= data_count; context.UpdateSubresource( CB_Offset.iBuffer, 0, NULL, &cparam, 0, 0 ); context.CSSetConstantBuffers( 0, 1, &CB_Offset.iBuffer ); if( dcount1 > 0 ){ context.CSSetShader( LoopShader_1.iCS, NULL, 0 ); context.Dispatch( dcount1, 1, 1 ); } if( dcount2 > 0 ){ context.CSSetShader( LoopShader_32.iCS, NULL, 0 ); context.Dispatch( dcount2, 1, 1 ); }
(2) 動的分岐を用いる
端数込みで 32 の倍数分実行します。スレッド番号が実行したい回数より多ければ、
動的分岐で処理を省きます。
例えば 75 個のデータを処理するなら、cmain_loop_dis を 3 回 (96回分) 実行し、
id が 75 以上なら何もしないで終了します。
// Compute Shader 5.0 cbuffer offset_T : register( b0 ) { uint threadid_offset; uint thread_total; uint r0; uint r1; }; [numthreads(32,1,1)] void cmain_loop_dis( uint3 threadid : SV_DispatchThreadID ) { if( threadid.x < thread_total ){ lmain( threadid.x ); } }
// C++ const int ThreadGroup= 32; int dcount1= (data_count+ThreadGroup-1)/ThreadGroup; offset_T cparam; cparam.threadid_offset= 0; cparam.thread_total= data_count; context.UpdateSubresource( CB_Offset.iBuffer, 0, NULL, &cparam, 0, 0 ); context.CSSetConstantBuffers( 0, 1, &CB_Offset.iBuffer ); context.CSSetShader( CSSubDShaderDis.iCS, NULL, 0 ); context.Dispatch( dcount1, 1, 1 );
●実行結果
RADEON HD 5870 で試してみました。
1 フレームあたり 7セット Compute Shader の実行を繰り返しています。
それ以外の描画は fps などのフォントのみ。
上のプログラムはグループ内スレッド数 32 固定でしたが、16~320 まで変更して
試しています。
GroupThread= 16 (1) 476 fps (2) 482 fps GroupThread= 32 (1) 840 fps (2) 852 fps GroupThread= 48 (1) 973 fps (2) 1006 fps GroupThread= 64 (1) 1102 fps (2) 1145 fps GroupThread= 96 (1) 984 fps (2) 1026 fps GroupThread= 128 (1) 1090 fps (2) 1140 fps GroupThread= 256 (1) 1083 fps (2) 1128 fps GroupThread= 320 (1) 1009 fps (2) 1065 fps
GroupThread が小さいと低速です。あまり小さいと Dispatch() の 65535 制限にも
ひっかかります。
どのケースでも、分岐を用いた (2) の方が高速でした。
端数分とはいえ GroupThread=1 で実行しているスレッドがあるため効率が悪い、
7セットの実行中に毎回シェーダー切り替えが発生しているから、切り替えないで済む
(2) の方が条件的に有利、等の理由が考えられます。
関連エントリ
・DirectX 11 / Direct3D 11 と RADEON HD 5870 の caps
・Direct3D11/DirectX11 ComputeShader 4.0 を使う
・Direct3D11/DirectX11 (6) D3D11 の ComputeShader を使ってみる