そろそろ触っておかないと不味い感じがしてきましたので,触ってみます。
まずは右も左もわからない状況なので,DirectXのサンプルにあるBasicCompute11を,少しいじってみるところから始めてみます。
今回は,地味なコンソールアプリです。
コンピュートシェーダの基本(1)
★ 1.はじめに…
★ 2.コンピュートシェーダって?
コンピュートシェーダは,描画パイプラインとは切り離された独立されたシェーダです。個人的にはグラフィックスの描画とは,関係が薄いので「シェーダ」って名前をつけるのはどうなのかなぁ~なんて思ったりするするのですが…。
まぁ,それはさておき… 「コンピュートシェーダは何がいいの?」って話なのですが… たまにリファレンス代わりに見ている「DirectX11 3Dプログラミング」という本にはこんなことが書いてあります。(p.332より引用)
個人的には,「描画パイプラインと無関係」という所と「出力リソースの任意の位置に書き込み可能」という所が一番大きい所です。今までは,頂点シェーダとピクセルシェーダの間にラスタライズの処理が入ってしまうので,ちょっとごにょごにょしないといけないとか制限があったりしました。例えば,Logarithmic Perspective Shadow Mapsなんかの実装では,対数ラスタライズしたいんだけどGPUは線形ラスタライズしてしまうので,相殺されるようにごにょごにょしていたりします。ピクセルシェーダは各ピクセルごとの処理になるので,自分が今書き込もうとしているピクセルの隣がどうなっているかの情報を取り出すことができないので,一度テクスチャに書き出して2パス目でごにょごにょするとか,ちと面倒くさい処理になったりします。
そういった,今までちょっとやっかいだったものがコンピュートシェーダを使うことにより緩和できる可能性があり,もしかしたら高速化できるかもしれない可能性があります。
まずは,そういった高速化など実践的な使い方に入る前に,基本な使い方をを抑えていきます。
まぁ,それはさておき… 「コンピュートシェーダは何がいいの?」って話なのですが… たまにリファレンス代わりに見ている「DirectX11 3Dプログラミング」という本にはこんなことが書いてあります。(p.332より引用)
【コンピュートシェーダの利点】 ・出力リソースの任意の位置に書き込み可能。 ・「データ共有」や「スレッド間同期」のメカニズム。 ・指定した数の「スレッド」を明示的に起動でき,パフォーマンスを最適化できる。 ・描画パイプラインと無関係なので,コードの保守が簡単。
個人的には,「描画パイプラインと無関係」という所と「出力リソースの任意の位置に書き込み可能」という所が一番大きい所です。今までは,頂点シェーダとピクセルシェーダの間にラスタライズの処理が入ってしまうので,ちょっとごにょごにょしないといけないとか制限があったりしました。例えば,Logarithmic Perspective Shadow Mapsなんかの実装では,対数ラスタライズしたいんだけどGPUは線形ラスタライズしてしまうので,相殺されるようにごにょごにょしていたりします。ピクセルシェーダは各ピクセルごとの処理になるので,自分が今書き込もうとしているピクセルの隣がどうなっているかの情報を取り出すことができないので,一度テクスチャに書き出して2パス目でごにょごにょするとか,ちと面倒くさい処理になったりします。
そういった,今までちょっとやっかいだったものがコンピュートシェーダを使うことにより緩和できる可能性があり,もしかしたら高速化できるかもしれない可能性があります。
まずは,そういった高速化など実践的な使い方に入る前に,基本な使い方をを抑えていきます。
★ 3. 演算の準備
コンピュートシェーダはその名通り,演算を行うシェーダです。演算するからには,演算の入力元と出力先が必要となります。
そこで,まず演算の入力用のデータと出力先データを整えることから始めていきます。
コンピュートシェーダの入出力用のデータですが,シェーダではこんな感じで書きます。
StructuredBufferとRWStructuredBufferの違いですが,RWなしのほうは入力のみに使えます。RWがあるほうはReadWriteの略なので読み書き,つまり入出力に使うことができます。
上のコードでは,StructuredBufferを入力データ,RWStructuredBufferを出力先として使用しています。
InputBufferを見ると"register( t0 )"となっています。つまり,入力はテクスチャとして用意しています。一方OutputBufferは"register( u0 )"となっています。MSDNでの説明をみると(http://msdn.microsoft.com/en-us/library/windows/desktop/dd607359(v=vs.85).aspx参照), Unordered Access Viewとなっています。日本語に無理やり訳すとすれば非順序アクセスビューといった感じでしょうか,その名の通りすきな場所にアクセスができます。これが上述した「出力リソースの任意の位置に書き込み可能」ということになります。
それでは,このInputBufferとOutBufferにあたる部分の作成処理について見ていきましょう。
まず使い方の流れですが,入力用のデータをバッファに格納して,構造化バッファを作成します。出力用のデータは初期データがいらないので,nllptrを引数で渡しています。
次にシェーダ内で使うためには,シェーダリソースビューとしてやる必要があるので,CreateBufferSRV()でシェーダリソースビューを作ります。
ちょうどテクスチャを使う場合は,
構造化バッファの実際の初期化処理は次のようになります。
これで,入力用データはそろいました。あとは,出力用データを作成します。
ここで注意してほしいのは,いつも使ってきたShaderResourceViewは読み込み専用の用途になります。シェーダで計算したデータを書き込むことはできません。
「それじゃ,書き込みどうするのさ?」という話になるのですが,シェーダ内で読み書きできる用途に使うのが前述したUnordered Access Viewになります。作り方は以下のような感じです。
さて,データの設定ができたので,あとはコンピュートシェーダを実行します。
メソッドについての詳細な説明はMSDNライブラリを参照して下さい。引数は実行スレッドの数を指定する数値になります。とりあえず今回のサンプルでは適当な数値にしました。この指定する数値ですが,シェーダファイル側の処理にも関係するので,実装者が『ええ感じ』に調整した方がよいかと思います。ちなみにどのように関するかというと下記のようになります。
さて,これで実行までできました。あとはコンピュートシェーダから演算結果を取得しましょう。
そのままさくっと取りたいのですが,GPUで演算するために作っているデータなので,簡単にはいきません。一度,CPUアクセスできるバッファを用意して,そのバッファにGPUで演算した結果をコピーして,コピーした結果をCPUでアクセスします。
そこで,まず演算の入力用のデータと出力先データを整えることから始めていきます。
コンピュートシェーダの入出力用のデータですが,シェーダではこんな感じで書きます。
StructuredBuffer<MyStruct> InputBuffer : register( t0 ); RWStructuredBuffer<MyStruct> OutputBuffer : register( u0 );ちなみに他の書き方も色々とできますが,とりあえず構造化バッファを使うコードにしてみました。
StructuredBufferとRWStructuredBufferの違いですが,RWなしのほうは入力のみに使えます。RWがあるほうはReadWriteの略なので読み書き,つまり入出力に使うことができます。
上のコードでは,StructuredBufferを入力データ,RWStructuredBufferを出力先として使用しています。
InputBufferを見ると"register( t0 )"となっています。つまり,入力はテクスチャとして用意しています。一方OutputBufferは"register( u0 )"となっています。MSDNでの説明をみると(http://msdn.microsoft.com/en-us/library/windows/desktop/dd607359(v=vs.85).aspx参照), Unordered Access Viewとなっています。日本語に無理やり訳すとすれば非順序アクセスビューといった感じでしょうか,その名の通りすきな場所にアクセスができます。これが上述した「出力リソースの任意の位置に書き込み可能」ということになります。
それでは,このInputBufferとOutBufferにあたる部分の作成処理について見ていきましょう。
00125: // バッファにデータを格納. 00126: for( int i=0; i<NUM_ELEMENTS; ++i ) 00127: { 00128: g_Buf0[ i ].s32 = i; 00129: g_Buf0[ i ].f32 = static_cast<float>( i ) * 0.25f; 00130: 00131: g_Buf1[ i ].s32 = i; 00132: g_Buf1[ i ].f32 = static_cast<float>( i ) * 0.75f; 00133: } 00134: 00135: 00136: // 構造化バッファを生成. 00137: hr = CreateStructuredBuffer( g_pDevice, sizeof(BufType), NUM_ELEMENTS, &g_Buf0[ 0 ], &g_pBuf0 ); 00138: assert( SUCCEEDED( hr ) ); 00139: hr = CreateStructuredBuffer( g_pDevice, sizeof(BufType), NUM_ELEMENTS, &g_Buf1[ 0 ], &g_pBuf1 ); 00140: assert( SUCCEEDED( hr ) ); 00141: hr = CreateStructuredBuffer( g_pDevice, sizeof(BufType), NUM_ELEMENTS, nullptr, &g_pBufResult ); 00142: assert( SUCCEEDED( hr ) ); 00143: 00144: 00145: // 入出力用のビューを生成. 00146: hr = CreateBufferSRV( g_pDevice, g_pBuf0, &g_pBufSRV0 ); 00147: assert( SUCCEEDED( hr ) ); 00148: hr = CreateBufferSRV( g_pDevice, g_pBuf1, &g_pBufSRV1 ); 00149: assert( SUCCEEDED( hr ) ); 00150: hr = CreateBufferUAV( g_pDevice, g_pBufResult, &g_pBufUAV ); 00151: assert( SUCCEEDED( hr ) );
まず使い方の流れですが,入力用のデータをバッファに格納して,構造化バッファを作成します。出力用のデータは初期データがいらないので,nllptrを引数で渡しています。
次にシェーダ内で使うためには,シェーダリソースビューとしてやる必要があるので,CreateBufferSRV()でシェーダリソースビューを作ります。
ちょうどテクスチャを使う場合は,
テクスチャ2Dを作る → シェーダリソースビューを作る…という流れでしたが,それと同じように
構造化バッファを作る → シェーダリソースビューを作る…という流れになっています。
構造化バッファの実際の初期化処理は次のようになります。
00358: //--------------------------------------------------------------------------------------------- 00359: // 構造化バッファを生成します. 00360: // ※ 頂点バッファやインデックスバッファとしては使用不可。 00361: //--------------------------------------------------------------------------------------------- 00362: HRESULT CreateStructuredBuffer 00363: ( 00364: ID3D11Device* pDevice, 00365: UINT elementSize, 00366: UINT count, 00367: void* pInitData, 00368: ID3D11Buffer** ppBufferOut 00369: ) 00370: { 00371: (*ppBufferOut) = nullptr; 00372: 00373: D3D11_BUFFER_DESC desc; 00374: memset( &desc, 0, sizeof(desc) ); 00375: 00376: desc.BindFlags = D3D11_BIND_UNORDERED_ACCESS | D3D11_BIND_SHADER_RESOURCE; 00377: desc.ByteWidth = elementSize * count; 00378: desc.MiscFlags = D3D11_RESOURCE_MISC_BUFFER_STRUCTURED; 00379: desc.StructureByteStride = elementSize; 00380: 00381: if ( pInitData ) 00382: { 00383: D3D11_SUBRESOURCE_DATA initData; 00384: initData.pSysMem = pInitData; 00385: 00386: return pDevice->CreateBuffer( &desc, &initData, ppBufferOut ); 00387: } 00388: 00389: return pDevice->CreateBuffer( &desc, nullptr, ppBufferOut ); 00390: } 00391:続いて,シェーダリソースビューの生成処理です。
00419: //--------------------------------------------------------------------------------------------- 00420: // シェーダリソースビューを生成します. 00421: //--------------------------------------------------------------------------------------------- 00422: HRESULT CreateBufferSRV( ID3D11Device* pDevice, ID3D11Buffer* pBuffer, ID3D11ShaderResourceView** ppSRVOut ) 00423: { 00424: D3D11_BUFFER_DESC desc; 00425: memset( &desc, 0, sizeof(desc) ); 00426: pBuffer->GetDesc( &desc ); 00427: 00428: D3D11_SHADER_RESOURCE_VIEW_DESC srvDesc; 00429: memset( &srvDesc, 0, sizeof(srvDesc) ); 00430: 00431: srvDesc.ViewDimension = D3D11_SRV_DIMENSION_BUFFEREX; 00432: srvDesc.BufferEx.FirstElement = 0; 00433: 00434: if ( desc.MiscFlags & D3D11_RESOURCE_MISC_BUFFER_ALLOW_RAW_VIEWS ) 00435: { 00436: srvDesc.Format = DXGI_FORMAT_R32_TYPELESS; 00437: srvDesc.BufferEx.Flags = D3D11_BUFFEREX_SRV_FLAG_RAW; 00438: srvDesc.BufferEx.NumElements = desc.ByteWidth / 4; 00439: } 00440: else if ( desc.MiscFlags & D3D11_RESOURCE_MISC_BUFFER_STRUCTURED ) 00441: { 00442: srvDesc.Format = DXGI_FORMAT_UNKNOWN; 00443: srvDesc.BufferEx.NumElements = desc.ByteWidth / desc.StructureByteStride; 00444: } 00445: else 00446: { 00447: return E_INVALIDARG; 00448: } 00449: 00450: return pDevice->CreateShaderResourceView( pBuffer, &srvDesc, ppSRVOut ); 00451: }構造化バッファの場合は,構造体の中身は当然ながらユーザーが決めるので,DXGI_FORMATにはないフォーマットになったりすると思います。構造化バッファの場合はDXGI_FORMAT_UNKNOWNをしてしておきましょう。
これで,入力用データはそろいました。あとは,出力用データを作成します。
ここで注意してほしいのは,いつも使ってきたShaderResourceViewは読み込み専用の用途になります。シェーダで計算したデータを書き込むことはできません。
「それじゃ,書き込みどうするのさ?」という話になるのですが,シェーダ内で読み書きできる用途に使うのが前述したUnordered Access Viewになります。作り方は以下のような感じです。
00453: //--------------------------------------------------------------------------------------------- 00454: // アンオーダードアクセスビューを生成します. 00455: //--------------------------------------------------------------------------------------------- 00456: HRESULT CreateBufferUAV( ID3D11Device* pDevice, ID3D11Buffer* pBuffer, ID3D11UnorderedAccessView** ppUAVOut ) 00457: { 00458: D3D11_BUFFER_DESC desc; 00459: memset( &desc, 0, sizeof(desc) ); 00460: pBuffer->GetDesc( &desc ); 00461: 00462: D3D11_UNORDERED_ACCESS_VIEW_DESC uavDesc; 00463: memset( &uavDesc, 0, sizeof(uavDesc ) ); 00464: 00465: uavDesc.ViewDimension = D3D11_UAV_DIMENSION_BUFFER; 00466: uavDesc.Buffer.FirstElement = 0; 00467: 00468: if ( desc.MiscFlags & D3D11_RESOURCE_MISC_BUFFER_ALLOW_RAW_VIEWS ) 00469: { 00470: uavDesc.Format = DXGI_FORMAT_R32_TYPELESS; 00471: uavDesc.Buffer.Flags = D3D11_BUFFER_UAV_FLAG_RAW; 00472: uavDesc.Buffer.NumElements = desc.ByteWidth / 4; 00473: } 00474: else if ( desc.MiscFlags & D3D11_RESOURCE_MISC_BUFFER_STRUCTURED ) 00475: { 00476: uavDesc.Format = DXGI_FORMAT_UNKNOWN; 00477: uavDesc.Buffer.NumElements = desc.ByteWidth / desc.StructureByteStride; 00478: } 00479: else 00480: { 00481: return E_INVALIDARG; 00482: } 00483: 00484: return pDevice->CreateUnorderedAccessView( pBuffer, &uavDesc, ppUAVOut ); 00485: }見て分かるように,ほとんどシェーダリソースビューの生成と変わらない感じです。
さて,データの設定ができたので,あとはコンピュートシェーダを実行します。
00154: // コンピュートシェーダを走らせる. 00155: ID3D11ShaderResourceView* pSRVs[ 2 ] = { g_pBufSRV0, g_pBufSRV1 }; 00156: RunComputeShader( g_pContext, g_pCS, 2, pSRVs, nullptr, nullptr, 0, g_pBufUAV, 32, 1, 1 );RunComputerShader()メソッドの実装ですが,下記のようになっています。
00513: //---------------------------------------------------------------------------------------------- 00514: // コンピュートシェーダを実行します. 00515: //---------------------------------------------------------------------------------------------- 00516: void RunComputeShader 00517: ( 00518: ID3D11DeviceContext* pContext, 00519: ID3D11ComputeShader* pComputeShader, 00520: UINT numViews, 00521: ID3D11ShaderResourceView** pSRVs, 00522: ID3D11Buffer* pCBCS, 00523: void* pCSData, 00524: DWORD numDataBytes, 00525: ID3D11UnorderedAccessView* pUAV, 00526: UINT x, 00527: UINT y, 00528: UINT z 00529: ) 00530: { 00531: pContext->CSSetShader( pComputeShader, nullptr, 0 ); 00532: pContext->CSSetShaderResources( 0, numViews, pSRVs ); 00533: pContext->CSSetUnorderedAccessViews( 0, 1, &pUAV, nullptr ); 00534: 00535: if ( pCBCS ) 00536: { 00537: D3D11_MAPPED_SUBRESOURCE res; 00538: 00539: pContext->Map( pCBCS, 0, D3D11_MAP_WRITE_DISCARD, 0, &res ); 00540: memcpy( res.pData, pCSData, numDataBytes ); 00541: pContext->Unmap( pCBCS, 0 ); 00542: 00543: ID3D11Buffer* ppCB[ 1 ] = { pCBCS }; 00544: pContext->CSSetConstantBuffers( 0, 1, ppCB ); 00545: } 00546: 00547: pContext->Dispatch( x, y, z ); 00548: 00549: ID3D11UnorderedAccessView* pNullUAVs[ 1 ] = { nullptr }; 00550: ID3D11ShaderResourceView* pNullSRVs[ 2 ] = { nullptr, nullptr }; 00551: ID3D11Buffer* pNullCBs [ 1 ] = { nullptr }; 00552: 00553: pContext->CSSetShader( nullptr, nullptr, 0 ); 00554: pContext->CSSetUnorderedAccessViews( 0, 1, pNullUAVs, nullptr ); 00555: pContext->CSSetShaderResources( 0, 2, pNullSRVs ); 00556: pContext->CSSetConstantBuffers( 0, 1, pNullCBs ); 00557: }いつもはコマンドの発行ににID3DDeviceContext::Draw()メソッドとかID3DDeviceContext::DrawIndexed()メソッドなどを使っているのですが,コンピュートシェーダではDraw()メソッドなどの代わりにDispatch()メソッドあるいはDispatchIndirect()メソッドを使用します。
メソッドについての詳細な説明はMSDNライブラリを参照して下さい。引数は実行スレッドの数を指定する数値になります。とりあえず今回のサンプルでは適当な数値にしました。この指定する数値ですが,シェーダファイル側の処理にも関係するので,実装者が『ええ感じ』に調整した方がよいかと思います。ちなみにどのように関するかというと下記のようになります。
00037: #define size_x 32 00038: #define size_y 1 00039: #define size_z 1 00040: 00041: 00042: //------------------------------------------------------------------------------------ 00043: // コンピュートシェーダのメインエントリーポイントです. 00044: //------------------------------------------------------------------------------------ 00045: [numthreads(size_x, size_y, size_z)] 00046: void CSFunc( const CSInput input ) 00047: { 00048: int index = input.dispatch.x; 00049: 00050: // 適当に演算させてみる. 00051: BufOut[ index ].i = BufIn0[ index ].i + BufIn1[ index ].i; 00052: BufOut[ index ].f = BufIn0[ index ].f * BufIn1[ index ].f; 00053: }このスレッド数ですが,まだコンピュートシェーダを使いはじめた状態なので,正直どういう場合にはどういう数値がいいかなどのノウハウがまだないです(だから誰か教えて!w)。今回のサンプルは超いい加減なので,サイズ1次元しか使っていないです。
さて,これで実行までできました。あとはコンピュートシェーダから演算結果を取得しましょう。
そのままさくっと取りたいのですが,GPUで演算するために作っているデータなので,簡単にはいきません。一度,CPUアクセスできるバッファを用意して,そのバッファにGPUで演算した結果をコピーして,コピーした結果をCPUでアクセスします。
00488: //--------------------------------------------------------------------------------------------- 00489: // バッファを生成し,内容をコピーします. 00490: //--------------------------------------------------------------------------------------------- 00491: ID3D11Buffer* CreateAndCopyToBuffer( ID3D11Device* pDevice, ID3D11DeviceContext* pContext, ID3D11Buffer* pBuffer ) 00492: { 00493: ID3D11Buffer* pCloneBuf = nullptr; 00494: 00495: D3D11_BUFFER_DESC desc; 00496: memset( &desc, 0, sizeof(desc) ); 00497: 00498: pBuffer->GetDesc( &desc ); 00499: desc.CPUAccessFlags = D3D11_CPU_ACCESS_READ; 00500: desc.Usage = D3D11_USAGE_STAGING; 00501: desc.BindFlags = 0; 00502: desc.MiscFlags = 0; 00503: 00504: if ( SUCCEEDED( pDevice->CreateBuffer( &desc, nullptr, &pCloneBuf ) ) ) 00505: { 00506: pContext->CopyResource( pCloneBuf, pBuffer ); 00507: } 00508: 00509: return pCloneBuf; 00510: }次に,CPUアクセスの仕方ですが,Map(), Unmap()を使って地味にアクセスしていきます。今回のサンプルではCPUでGPUで計算しているのと同じ処理を行って,CPUの演算結果とGPUの演算結果を比較します。当然ながら,浮動小数点計算は「==」とか「!=」で比較せず,誤差範囲内に入っているかどうかでチェックすべきなのですが,面倒なので「==」とか「!=」使っちゃっています。ちゃんとした実装をするときには,この実装のままだと絶対に一致しないので気を付けましょう。
00159: // GPU からの計算結果を読み戻して,CPUで計算した結果と同じであるか検証する. 00160: { 00161: // バッファを生成とコピー. 00162: ID3D11Buffer* pBufDbg = CreateAndCopyToBuffer( g_pDevice, g_pContext, g_pBufResult ); 00163: 00164: D3D11_MAPPED_SUBRESOURCE subRes; 00165: BufType* pBufType; 00166: 00167: // マップ. 00168: hr = g_pContext->Map( pBufDbg, 0, D3D11_MAP_READ, 0, &subRes ); 00169: assert( SUCCEEDED( hr ) ); 00170: 00171: pBufType = (BufType*)subRes.pData; 00172: 00173: ILOG( "Verifying against CPU result..." ); 00174: bool isSuccess = true; 00175: 00176: for( int i=0; i<NUM_ELEMENTS; ++i ) 00177: { 00178: // CPUで演算. 00179: int value0 = g_Buf0[i].s32 + g_Buf1[i].s32; 00180: float value1 = g_Buf0[i].f32 * g_Buf1[i].f32; 00181: 00182: // GPUの演算結果とCPUの演算結果を比較. 00183: if ( ( pBufType[i].s32 != value0 ) 00184: || ( pBufType[i].f32 != value1 ) ) 00185: { 00186: ILOG( "Failure." ); 00187: ILOG( " index = %d", i ); 00188: ILOG( " cpu value0 = %d, value1 = %f", value0, value1 ); 00189: ILOG( " gpu value0 = %d, value1 = %f", pBufType[i].s32, pBufType[i].f32 ); 00190: isSuccess = false; 00191: break; 00192: } 00193: } 00194: 00195: // CPUとGPUの結果がすべて一致したら成功のログを出力. 00196: if ( isSuccess ) 00197: { ILOG( "Succeded!!" ); } 00198: 00199: // アンマップ. 00200: g_pContext->Unmap( pBufDbg, 0 ); 00201: 00202: // 解放処理. 00203: ASDX_RELEASE( pBufDbg ); 00204: }今回は,コンピュートシェーダを軽く触ってみました。今回の内容では,実用できるレベルに至らないと思うのであと数回かけて基本を押さえていくことにします。
★ Download
本ソースコードおよびプログラムはMIT Licenseに準じます。
プログラムの作成にはMicrosoft Visual Studio 2012 Express Editionを用いています。
プログラムの作成にはMicrosoft Visual Studio 2012 Express Editionを用いています。