syghの新フラグメント置き場

プログラミングTipsやコード断片の保管場所です。お絵描きもときどき載せます。

OpenGLコンピュートシェーダー

(これは2013-01-30に書いた故OCNブログの記事を移植したものです)

2013-01-05に公開されたNVIDIAのリテールドライバー310.90で、ようやくOpenGL 4.3が正式に使えるようになりました。最新世代のKeplerのほか、旧世代のFermiアーキテクチャのグラフィックスカードでもフルサポートされてるのが嬉しいところです。

OpenGL 4.3の目玉はなんといってもコンピュートシェーダー (Compute Shader) でしょう。コイツはDirectXにおいてはバージョン11からDirectComputeとして搭載されていたGPGPU用シェーダーなんですが、OpenGLでもこのたびついに同様のシェーダーが搭載されることになりました。

早速サンプルコードを書いて実行してみました。

https://sygh-jp.github.io/content_hosting/my_program_ss/gl_cs_simple_test_ss02.png

といってもこのサンプル、実はココで公開されてるX Windowシステム用のコードを、FreeGLUTGLEWを使って書き直しただけです。ちなみに終了時のGLリソース解放コードとかは一切書いてないのでご注意を。なお、GLUTに関しては、Radeon環境やMac OS環境だとOpenGL 4対応の互換コンテキストを明示的に作成するようにする修正作業が必要かもしれません。コンテキストまわりを設定するならばGLUT系よりもGLFWGLUSのほうが良いと思います。

コンパイル・動作検証は

でやってます。FreeGLUTとGLEWは最新版を一式ダウンロード&インストールしてパスを通しておいてください。なお、GLEWはビルド済みバイナリが提供されていますが、FreeGLUTのほうは自前でビルドする必要があります。

サンプルのメインはgenComputeProg()グローバルメソッド内に文字列として記述されているGLSLコンピュートシェーダーのコード(CUDAやOpenCLでおなじみのいわゆるカーネル関数に相当)なんですが、何をやってるかっていうとカーネル関数に渡されたスレッドIDとかグループIDとかに適当な係数とフレーム時刻を乗じてOpenGLテクスチャに書き出し(ランダムアクセス)、それをポリゴンに張り付けてるだけの代物です。ちなみにimageLoad()/imageStore()によるテクスチャへのランダムアクセスはDirectCompute(HLSL)でいうTexture2D/RWTexture2Dのインデクサに相当する機能(StructuredBuffer/RWStructuredBufferではなく)のはずです。OpenGL 4.3のコンピュートシェーダーはDirectX 11のコンピュートシェーダーとは違い、DirectX 10世代のダウンレベルGPUでは機能制限どころではなくそもそも動かないものと思われます(現にFermiよりも古い世代ではドライバーが対応していない)。つまり、動作には必ずDirectX 11世代(シェーダーモデル5以上)のGPUが必須になります。
サンプルで使っているテクスチャはR32形式の浮動小数バッファ(GL_R32F)なので、RGBAのバックバッファに描画するとコンピュートシェーダーの計算結果がゼロ(R輝度がゼロ)の部分が補色のシアンになります(説明間違ってないよね?)。

なお、StructuredBuffer/RWStructuredBuffer (SB) 相当の機能はShader Storage Buffer Object (SSBO) と呼ばれるものなのですが、今回は省略します。あとAppendStructuredBufferやConsumeStructuredBufferに相当するものは見あたらないのですが、OpenGL 4.2のアトミックカウンターを使って各自なんとかしろ、ということかもしれません。また、ByteAddressBuffer/RWByteAddressBuffer (BAB) に関しても相当機能がなさげなんですが、もともとBABは頂点バッファやインデックスバッファ、Indirect系メソッドの引数用バッファなどをシェーダーで直接読み書きできるようにする類のビューです(D3D11_BIND_VERTEX_BUFFER, D3D11_BIND_INDEX_BUFFERやD3D11_RESOURCE_MISC_DRAWINDIRECT_ARGS, D3D11_RESOURCE_MISC_BUFFER_ALLOW_RAW_VIEWSとは組み合わせられるが、D3D11_RESOURCE_MISC_BUFFER_STRUCTUREDとは組み合わせられない)。一方glDispatchComputeIndirect()用に専用のバッファGL_DISPATCH_INDIRECT_BUFFERを持ち、さらにVBOもSSBOとしてglBindBufferBase()で直接バインドできるOpenGLではどうやらBABは不要のようです。実際BABはかなり使いどころが難しいというかできれば使いたくないです……

#version 430

uniform float roll;
uniform image2D destTex;
layout (local_size_x = 16, local_size_y = 16) in;
void main() {
    ivec2 storePos = ivec2(gl_GlobalInvocationID.xy);
    float localCoef = length(vec2(ivec2(gl_LocalInvocationID.xy) - 8) / 8.0);
    float globalCoef = sin(float(gl_WorkGroupID.x + gl_WorkGroupID.y) * 0.1 + roll) * 0.5;
    imageStore(destTex, storePos, vec4(1.0 - globalCoef * localCoef, 0.0, 0.0, 0.0)); // Unordered access
}

ちなみにサンプルコード中ではGLSLのlayout修飾子を使ってローカルグループサイズ(HLSLでいうnumthreads属性)を指定していますが、実はOpenGLのコンピュートシェーダーにはGL_ARB_compute_variable_group_size(ARB_compute_variable_group_size)という拡張があって、GLSLコンパイル時だけでなくglDispatchComputeGroupSizeARB()によって実行時にもC/C++ホストプログラム側でローカルグループサイズを制御できるようになっているらしいです。この機能はDirectX 11のコンピュートシェーダー(DirectCompute)にはまだないのですが、CUDAやOpenCLには普通に同等機能が実装されていて、実行効率の向上やデータサイズに対する柔軟性を向上させるのに重要な機能なので、OpenGL 4アドバンテージの1つになっている模様。ダテに「DirectXを超えた」と謳っているわけではなさそうです。あと、シェーダー側でディスパッチグループ数・ローカルグループサイズを取得する場合、CUDAの gridDim, blockDim 組み込み変数や、OpenCL の get_num_groups(), get_local_size() 組み込み関数に相当するセマンティクスがHLSLにはなく、定数バッファなどを使って自前で通知するしかないのですが、OpenGLコンピュートシェーダーにはちゃんと gl_NumWorkGroups, gl_WorkGroupSize として実装されているのはありがたいです。まあAPIトータルで見ればDirectXのほうがきれいな設計になっていて圧倒的に開発しやすいんですが、こうして見るとDirectX 11もまだ欠点や機能不足を多数抱えていますね……なお、ホスト側でローカルグループサイズを指定する場合、シェーダープログラムの先頭に

#extension GL_ARB_compute_variable_group_size : enable

を記述し、さらにlayout(local_size_variable)を指定する必要があるようです。

あと記事を書いてて気付いたんですが、テクスチャのサイズは512x512なのにウィンドウクライアント領域のサイズが512x512じゃない……GLUTのglutInitWindowSize()はタイトルバーを考慮しないウィンドウ自体の横幅・縦幅を指定するものでしたorz

2015-02-21追記:
NVIDIAOpenGL/OpenGL ESのコンピュートシェーダーを使ったサンプルをいくつか公開しています。

AMDは2015年2月現在、OpenGL 4.3を一応サポートしているものの、依然としてOpenGLには真面目に取り組むつもりはないようです。MantleやDirectX 12がある以上、今更OpenGLに注力する価値はないと判断しているのでしょう(Windows以外は二の次)。IntelはHaswell/Broadwell世代のGPUOpenGL 4.3をサポートしているようです。

2018-05-26追記:
Visual Studio 2015とGLFWを使ってサンプルコードを書き直しました。ウィンドウクライアント領域のサイズも512x512に設定しています。

なお、GeForceドライバー388.13とGTX 760で試したところ、コンピュートシェーダー内で使用しているimage2Dオブジェクトには、writeonly修飾子を付けて定義しないとコンパイルエラーになりました。古いQuadroドライバー310.90はGLSL 4.3規格に正しく準拠していなかったのかもしれません。しかし相変わらずGLSLの文法は微妙な感じです。

*1:NVIDIAの310.90ドライバーはどうもBuggyらしいです。自分が確認しただけでも、MFC Feature Pack(というかBCGSoft製ライブラリ?)のGDIコントロール再描画がおかしくなるという現象が発生します。311系列以降を使うようにしたほうがよいです。