ピクセルシェーダーにおけるAtomic加算

DX11からPixelShader内でのAtomicOperationが可能になりました。一方でOpenGLもARB_shader_atomic_counters拡張によって、すべてのシェーダーステージでAtomicOperation(Query/Increment/Decrementのみ)が可能になっています。

ARB_shader_atomic_counters拡張について

このOpenGLの機能拡張によって、すべてのシェーダーステージでAtomicCounterが使用可能になります。使用するためには専用のバッファをGL_ATOMIC_COUNTER_BUFFERにBindする必要があります。あとはGLSLの組み込み関数でAtomicOperationが実行できます。以下は端的ですが、uintのカウンタを用意して、0に初期化してシェーダー内でIncrementCounterとして使用した例になります。

バッファの作成

glGenBuffers(1, &g_atomicCounterBuffer);
glBindBuffer(GL_ATOMIC_COUNTER_BUFFER, g_atomicCounterBuffer);
glBufferData(GL_ATOMIC_COUNTER_BUFFER, sizeof(GLuint), NULL, GL_DYNAMIC_DRAW);
glBindBuffer(GL_ATOMIC_COUNTER_BUFFER, 0);

バッファの初期化とBind

glBindBuffer(GL_ATOMIC_COUNTER_BUFFER, g_atomicCounterBuffer);
GLuint  cnt = 0;
glBufferSubData(GL_ATOMIC_COUNTER_BUFFER, 0, sizeof(GLuint), &cnt); 
glBindBuffer(GL_ATOMIC_COUNTER_BUFFER, 0);

glBindBufferBase(GL_ATOMIC_COUNTER_BUFFER, 0, g_atomicCounterBuffer);

GLSLでの宣言とAtomicOperation

#version 420 compatibility
layout(binding=0, offset=0) uniform atomic_uint atomicCounter;

uint oldIdx = atomicCounterIncrement(atomicCounter);

同様のことをDX11で行う

こちらに関しては数多解説があると思いますが、対応付けて見たほうがわかりやすいので一応例を示します。
PixelShaderでUAVを使用する場合に気をつけたほうがいいことがあります。[earlydepthstencil]属性をピクセルシェーダーにつけると、実際に描画されるピクセル(深度とステンシルテストにパスしたピクセル)のみでピクセルシェーダーが動作し、結果としてUAVに対する操作も行われます。この属性をつけない場合は、テストの結果にかかわらずピクセルシェーダーが動作します。ちなみにBackfaceCullingのテストも無視して動作するようです。PixelShaderでUAVを使用する場合のほとんどのケースでは、[earlydepthstencil]属性が必要になると思います。

バッファの作成

ID3D11Buffer              *g_pRWBuffer = NULL;
ID3D11UnorderedAccessView *g_pUAV = NULL;

D3D11_BUFFER_DESC bd;
ZeroMemory( &bd, sizeof(bd) );
bd.Usage = D3D11_USAGE_DEFAULT;
bd.ByteWidth = sizeof(UINT);
bd.BindFlags = D3D11_BIND_UNORDERED_ACCESS;
bd.CPUAccessFlags = 0;
bd.MiscFlags  = D3D11_RESOURCE_MISC_BUFFER_ALLOW_RAW_VIEWS;
g_pd3dDevice->CreateBuffer( &bd, NULL, &g_pRWBuffer );

D3D11_UNORDERED_ACCESS_VIEW_DESC ud;
ZeroMemory( &ud, sizeof(ud) );
ud.Format = DXGI_FORMAT_R32_TYPELESS;
ud.ViewDimension = D3D11_UAV_DIMENSION_BUFFER;
ud.Buffer.FirstElement = 0;
ud.Buffer.NumElements = 1;
ud.Buffer.Flags = D3D11_BUFFER_UAV_FLAG_RAW;
g_pd3dDevice->CreateUnorderedAccessView(g_pRWBuffer, &ud, &g_pUAV);

バッファの初期化とBind

UINT clVal[4] = {0, 0, 0, 0};
g_pImmediateContext->ClearUnorderedAccessViewUint( g_pUAV, clVal);

g_pImmediateContext->OMSetRenderTargetsAndUnorderedAccessViews(
  1, &g_pRenderTargetView, g_pDepthStencilView,
  1, 1, &g_pUAV, NULL);

HSLSL内での宣言とAtomicOperation

RWByteAddressBuffer    rwBuffer0 : register( u1 );

[earlydepthstencil]
float4 PS( PS_INPUT input) : SV_Target
.
.
rwBuffer0.InterlockedAdd(0, 1, oldIdx);

実行してみる

ピクセルシェーダー内でInterlockedAddを行い、そのインデックスでマンセル色環を回るようにレンダリングしてみました。さらにマンセル色環の8倍周期で輝度が1から0になるようにシェーダーを書いてみました。このシェーダーを用いて720pのbackbufferにFullScreenQuadを描画しました。このレンダリング結果で、ピクセルシェーダーがスクリーン上でどのような順番で呼び出されているかを、大まかな精度で知ることが出来ます。
以下にそのレンダリング結果を示します。

GeFroceGTX680ではスクリーン左上から16ラインごとに右方向に呼び出されるようです。この16ラインは16×16ごとに呼び出され、さらに上下2つに分かれて、4×8のブロックごとに呼び出されているようです。さらにこの4×8のブロックは2×8に分かれているように見えます(この2×8のブロックはシェーダーを変更すると簡単に確認できます)。

一方RadeonHD7970では、スクリーン右上から呼び出して、次のプリミティブでは左下から呼び出しています。おそらくですが、RadeonはFullScreenQuadを検出して、最適な呼び出し順序になるように工夫しているのかも知れません。もしくはプリミティブに投入された頂点順序に依存しているのか、プリミティブごとにラスタライズの順番を反転しているかです。基本的には32×32のブロックごとに横方向に呼び出しているようですが、途中で折り返しているのが見えます。この位置はスクリーンにおける512pxごとの位置になります。したがって、右から走査を始めますが、最初の折り返し位置は左から数えて1024pxの位置です。さらに32×32のブロックの中には、場所によって8×16のブロックが見て取れます。

OpenGLの実行結果

以上はDX11での実行結果ですが、OGLでの実行結果は以下になります。
OGLでも基本的にはDX11と同じ挙動を示しますが、Atomicの実装方法が違うのか、レンダリング結果は異なります。この結果から見て取れるのは、両者ともDX11に比べると色の乱れが大きいです。これはInterlockによるstallが大きくなったことを示していると思います。これが何に由来するものかは明確にわかりませんが、OGL側の仕様ではすべてのシェーダーステージでAtomicが使用できるようになっています。ちょうどDX11.1の機能を先取りした形になります。そのためこの様な結果になっているのかも知れませんが、詳細は不明です。

GeForce GTX680

Radeon HD7970

実行時間

DX11とOpenGLの実行時間をとってみました。下記はGTX680で720pを100回描画した際のレンダリング時間になります。当初は、専用のバッファをBindしてIncrementとDecrementに特化していたので、OpenGLの方が速いと予想していましたが、レンダリング結果が示すとおり、Interlockによるストールが大きいのかOGLの方が遅いという結果になりました。
ちなみに、この二つは機能面で異なるので一概にDX11の方が速いという結果ではありません。

DX11 OGL
GTX680 5.3ms 8.8ms

[追記 2013/03/14]
現時点での最新のドライバーである、バージョン314.07を用いて同様のテストを行ってみました。下記の通り、OGLの方でパフォーマンスの向上があったのが見て取れます。

DX11 OGL
GTX680 5.3ms 6.0ms

ソース

MicrosoftのDirectXのTutorial07のコードを元に作成しました。
AtomicTest.(拡張子をzipに変更してください)

コメントを残す

以下に詳細を記入するか、アイコンをクリックしてログインしてください。

WordPress.com ロゴ

WordPress.com アカウントを使ってコメントしています。 ログアウト / 変更 )

Twitter 画像

Twitter アカウントを使ってコメントしています。 ログアウト / 変更 )

Facebook の写真

Facebook アカウントを使ってコメントしています。 ログアウト / 変更 )

Google+ フォト

Google+ アカウントを使ってコメントしています。 ログアウト / 変更 )

%s と連携中