タグ別アーカイブ: OpenGL

OpenGL関連の話題

GL_NV_command_list 拡張について

GL_NV_command_list拡張は、描画リソースBindingと一部のRenderState変更を含んだ、一連のDrawCallを、少ないCPUオーバーヘッドで発行するための仕組みです。そのコンセプトは、MultiDrawIndirect系とは異なり、どちらかというとOpenGLに元来備わっているDisplayListに近いものとなります。なかなか大規模な拡張なので、順を追ってみていきたいと思います。
続きを読む

広告

Multi-Layer Alpha Blendingについて

順不同の半透明オブジェクトを描画する方法ひとつである、Multi-Layer Alpha Blendingを見てみたいと思います

参照
Multi-Layer Alpha Blending

正しくAlphaBlendingの計算を行うためには、ご存知のとおり、Z値の順番に沿って計算を実行していく必要があります。従来では、これを行うために、オブジェクト単位、プリミティブ単位のソートを行ってきました。
近年では、GPUによって、Fragment単位のソートを行う事により、順不同の半透明オブジェクトを正しく描画する方法が提案されています。しかし、これによって消費されるGPUの計算リソースと、メモリリソースは、描画する半透明オブジェクトの量によって大きく変動します。特に、メモリの使用量が変動するため、実際のアプリケーションで、簡単にこれを利用することは出来ませんでした。
本手法は、GPU上で順不同の半透明オブジェクトを描画する手法のひとつです。特に、メモリの使用量が固定できるのが大きな特長です。その代わり、AlphaBlendingのレンダリング結果は必ず正しいものになるとは限らず、使用するレイヤーの数と、描画順序の影響を受けます。この点を正しく理解して使う必要があります。

AlphaBlendingの式の一般化

まずは、AlphaBlendingの式の一般化を見てみます。参照論文のeq(4)の部分です。pre-multiplied alphaのカラー(A)と、透過率(t)と深度値(z)の式になります。この式は、AlphaBlendingの計算は、合成するFragmentの深度値の比較結果に応じて、2つの式に分かれることを示しています。深度値が小さいものに対して大きいものを合成する場合、また、その逆も正しく計算する方法があることを示しています。
しかし、この式によって、多数のFragmentを順不同に合成できるわけではありません。たとえば、1番目と2番目のFragmentを合成した後に、1番目と2番目の間の深度値を持つFragmentを正しく合成する術はありません。

この問題に対処するため、まず、1Pixel上で、合成するFragmentの数を、mとします。すると、AlphaBlendingを計算する式は、eq(5)のように書くことが出来ます。
この式を計算するためには、1Pixelあたりm個の半透明Fragment情報の配列を確保すればよいことになります。Fragmentを描画する際は、この配列に対して、挿入ソートを行えば、z値の手前からm個のFragmentを保存することが出来ます。
大きなmを設定すれば、レンダリングするシーン全体を正しく描画できるはずですが、これを行うと、GPUのメモリを多く消費することになります。

消費メモリの問題に対処するためには、配列の長さmは小さな値を使用する必要があります。しかし、小さなmを採用すれば、1Pixel上で多数のFragmentが重なると、Fragmentが配列から溢れることになります。
Fragmentが溢れた場合は、情報の喪失を最小限に留めるため、破棄するのではなく、隣接するFragmentを合成してマージします。

Fragmentのマージによるエラーの考察

Fragmentをマージすることにより、発生するエラーに関して考えます。エラーが発生する条件は、マージしたFragmentの中間の深度値に、新たなFragmentが描画された場合です。AlphaBlendingの式の一般化の項で説明したとおり、合成したFragmentの間のFragmentを後から合成する術はありません。これを配列に挿入すれば、本来のAlphaBlendingの結果と異なる結果になります。
論文中で、マージするFragmentに関して幾つかの方法が考察されていますが、空間的、時間的に、最も一貫した結果が得られるのが、視点から見て、最も透過率の低いFragmentを合成するものとされています。つまり、z値の最も大きな2つのFragmentを合成するということです。

実装

アルゴリズムは、参照論文のListing 1 を参照して下さい。pseudocodeが書かれています。バブルソートを行い、最後尾のFragmentをマージして格納するだけです。
問題は、GPU上のPixelShaderでこれを正しく実行する方法です。既存のGPUでは、AlphaBlend描画時は、PixelShaderの出力順序は、描画APIの呼び出し順序と一致するはずでが、PixelShader上で、メモリに対して、Read/Modify/Writeを行なった場合に、これがAPIの呼び出し順序と一致する保障はありません。加えて、Read/Modify/Writeを行う最中に、他のPixelShaderがバッファに対してアクセスしないようにするには、Atomicを用いる必要があります。しかし、ピクセル単位でAtomic処理を用いるのは、パフォーマンスの側面で好ましくありません。

GL_INTEL_fragment_shader_ordering拡張について

このOpenGLの機能拡張は、上記の処理を行うために、GLSLシェーダー内に、同一Pixelを処理するPixelShader同士のインターロックを設定する事が出来ます。これを用いることによって、Atomic処理を用いることなく、メモリに対して、read/modify/writeの処理を、描画APIの呼び出し順序を守って実行する事が可能となります。具体的には、シェーダーコード内で、下記の組み込み関数を呼び出すことで、この呼び出し以降の処理は、同一Pixelを対象とした、PixelShaderの処理はブロックされます。

void beginFragmentShaderOrderingINTEL();

endFragmentShaderOrderingINTEL()に相当する関数は今のところ存在しません。PixelShaderの処理の終了を以って、ブロックが解除されるようです。

考察

まず、この手法は、必ずしも正しいAlphaBlendingの結果をもたらすものではありません。ただし、エラーが発生する条件を理解すれば、誤差を最小限に留めつつ描画する方法が自然と分かってきます。
先ほども述べたとおり、誤差を発生させるには、マージしたFragmentの中間の深度のFragmentを描画する必要があります。加えて、マージされるFragmentは、z値が一番大きな2つのFragmentです。
もし、近景を先に描画して、近景で配列を充填した後に、遠景を描画すれば、自ずと遠景のFragment同士でマージされ、遠景のFragment同士をソートする機会が失われます。
もし、遠景を先に描画して、遠景で配列を充填した後に、近景を描画すれば、配列内でソートされた、遠景のFragmentから順にマージされ、近景のFragmentが配列に充填されることになります。
したがって、精密なソートは行わないとしても、なるべく遠景より描画すれば、この手法の誤差は少なく保つことが出来るはずです。
また、この手法では、深度値の比較を行って、処理を変更しているため、MSAAとの相性は良くないです。SubSample単位でこの処理を行えば実装可能ですが、メモリの使用量や、PixelShaderの負荷的に、相当のGPUリソースを必要とすると思われます。
最後に、これを実装するにあたって、GPUに、PixelShaderの同期機構が備わっていることが必要だと思います。Atomicを用いてこれを実装することも可能と思われますが、Atomic処理によって、ブロックされる処理は長く、パフォーマンスを考慮すると、あまりお勧めできません。

OpenGLのDrawCallについて

OpenGL4.4のDrawCallについて、あたらめて勉強してみたいと思います。
加えて、DrawCallに深く関連する、頂点データの指定方法と、VertexShaderで参照可能なシステム変数についても見てみたいと思います。

頂点データ配列の指定について

頂点データ配列は、DrawCallの呼び出しによって、起動されるVertexShaderの入力として使用されるデータの配列です。たとえば、頂点の位置や法線、色、UVなどがこれに相当します。これらの、OpenGLにおける指定方法を見てみたいと思います。

VertexAttributeとVertexAttributeBindingについて

VertexAttribute(以下VA)は、VertexShaderの入力となるin変数と対応付けされるデータのフォーマットを指定するものとなります。具体的なVertexAttributeの例として、頂点の位置や法線、色、UVなどが挙げられると思います。
一方で、VertexAttributeBinding(以下VAB)は、どのVAが、どの頂点データ配列(VertexBufferObject。以下VBO)を使用するかを対応付けるものになります。たとえば、頂点と法線がインターリーブで配置された配列があれば、この配列を、頂点と法線のVAにBindingします。
このように、頂点フォーマットの指定に相当するVAと、VBOの指定に相当するVABを設定するAPIが用意されています。

VertexBindingDivisorについて

VertexBindingDivisorは、端的にいえば、インスタンス描画で用いる、インスタンスごとの頂点データ配列を指定する方法です。
APIは、

GLvoid glVertexBindingDivisor(GLuint bindingindex, GLuint divisor);

となっており、VABのインデックスとdivisorの値を指定します。divisorの値に0を指定した場合は、通常の頂点データ配列と同じように、頂点ごとに頂点データを読み進めます。
divisorの値に0以外の値を指定した場合は、指定した数のインスタンスごとに、頂点データを読み進めます。たとえば、1を指定すれば、インスタンスごとに頂点データが指定できるようになります。

VertexAttributeObjectについて

VertexAttributeObject(以下VAO)には、上記のVAおよびVABに関する設定の全てが保存されるオブジェクトとなります。
VAOを作成することで、VAおよびVABに関する煩雑な設定を、VAOのBindという1つのAPI呼び出しで置き換えることが出来ます。
殆どの描画では、同一のオブジェクトに対する、VAおよびVABに関する設定が、描画フレームごとに変化することはありません。従って、VAOを用いてオブジェクト化しておくと、後の描画処理がシンプルに記述できます。
ただし、頂点データに関連する一部の設定がVAOに格納されないので注意が必要です。具体的には以下の項目が格納されません。

  • GL_ARRAY_BUFFERと GL_DRAW_INDIRECT_BUFFER(後述)のBinding情報
  • ストリップを用いた描画で用いる、Primitive Restartに関する設定

Primitive Restartを使用する場合は、

glEnable(GL_PRIMITIVE_RESTART_FIXED_INDEX);

を呼び出すことで、インデックスバッファ(GL_ELEMENT_ARRAY_BUFFER)で用いる変数型の最大値が、RestartIndexとして扱われるようになります。こちらを呼び出し、以後はPrimitive Restartに関する設定を変更しないような利用方法が望ましいです。

具体的な例

下記の例では、3つのVBOに、(position, normal), (uv0, uv1), (color)と格納されているデータを、VABとVAに設定している例です。
(color)に関しては、描画インスタンスごとの値となるように設定します。さらにこれ等の設定をVAOに保存しています。

GLuint buffer0, buffer1, buffer2, buffer3;
-- create and initialize buffers --

// create and bind VAO
GLuint vao;
glGenVertexArrays(1, &vao);
glBindVertexArray(vao);

// Set up formats and relative offsets within the interleaved data.
// position float3, unnormalized, offset 0 byte
glVertexAttribFormat(0, 3, GL_FLOAT, GL_FALSE, 0);
// normal float3, unnormalized, offset 12 byte
glVertexAttribFormat(1, 3, GL_FLOAT, GL_FALSE, 12);
// uv0 float2, unnormalized, offset 0 byte
glVertexAttribFormat(2, 2, GL_FLOAT, GL_FALSE, 0);
// uv1 float2, unnormalized, offset 8 byte
glVertexAttribFormat(3, 2, GL_FLOAT, GL_FALSE, 8);
// color float4, unnormalized, offset 0 byte
glVertexAttribFormat(4, 4, GL_FLOAT, GL_FALSE, 0);

// Bind the vertex buffers to VAB slots.
// set buffer0 to VAB slot: 0, offset 0  byte, stride 24 byte
glBindVertexBuffer(0, buffer0, 0, 24);
// set buffer1 to VAB slot: 1, offset 0 byte, stride 16 byte
glBindVertexBuffer(1, buffer1, 0, 16);
// set buffer2 to VAB slot: 2, offset 0 byte, stride 12 byte
glBindVertexBuffer(2, buffer2, 0, 12);

// Set up attrib->binding mapping
glVertexAttribBinding(0, 0); // set VA slot:0 to VAB slot:0
glVertexAttribBinding(1, 0); // set VA slot:1 to VAB slot:0
glVertexAttribBinding(2, 1); // set VA slot:2 to VAB slot:1
glVertexAttribBinding(3, 1); // set VA slot:3 to VAB slot:1
glVertexAttribBinding(4, 2); // set VA slot:4 to VAB slot:2

// Set divisor to per-instance array
// set color array as per-instance data.
glVertexBindingDivisor(2, 1);

// Set a buffer as index array buffer
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, buffer3);

glBindVertexArray(0);

DrawCallについて

次に、DrawCallの呼び出し手順についても見ていきたいと思います。
DrawCallの種類は多岐にわたりますので、主だったものを順を追って確認してみたいと思います。

通常の描画

GLvoid glDrawElements(GLenum mode,
                      GLsizei count,
                      GLenum type,
                      const GLvoid * indices);

頂点バッファとインデックスバッファを用いたプリミティブの描画。

  • mode
    プリミティブの種類(POINT,LINE,TRIANGLE)
  • count
    indices配列の長さ
  • type
    インデックス配列の値の種類(UBYTE, USHORT, UINT)
  • indices
    null (インデックスバッファ(GL_ELEMENT_ARRAY_BUFFER)がBindされている場合)

頂点インデックスを使用したDrawCallの基本形です。
当然ですが、DrawCallの呼び出す前に、VAO,VBO等が正しくBindされている必要があります。

インスタンス描画

GLvoid glDrawElementsInstanced(GLenum mode,
                               GLsizei count,
                               GLenum type,
                               const GLvoid * indices,
                               GLsizei primcount);
  • primcount
    描画するインスタンスの数

glDrawElementsのインスタンス描画版です。primcountに指定した数だけインスタンス描画を行います。
VertexShader内で、gl_InstanceIDシステム変数(後述)を参照することで、描画しているインスタンスのインデックスを確認することができます。
また、divisorを設定した頂点配列を用いることで、インスタンスごとの頂点属性データを指定できます。

配列オフセット付き描画

GLvoid glDrawElementsBaseVertex(GLenum mode,
                                GLsizei count,
                                GLenum type,
                                GLvoid *indices,
                                GLint basevertex);

GLvoid glDrawElementsInstancedBaseInstance(GLenum mode,
                                           GLsizei count,
                                           GLenum type,
                                           const GLvoid *indices,
                                           GLsizei instancecount,
                                           GLuint baseinstance);

GLvoid glDrawElementsInstancedBaseVertex(GLenum mode,
                                         GLsizei count,
                                         GLenum type,
                                         const GLvoid *indices,
                                         GLsizei instancecount,
                                         GLint basevertex);

GLvoid glDrawElementsInstancedBaseVertexBaseInstance(GLenum mode,
                                                     GLsizei count,
                                                     GLenum type,
                                                     const GLvoid *indices,
                                                     GLsizei instancecount,
                                                     GLint basevertex,
                                                     GLuint baseinstance);
  • basevertex
    頂点インデックスに対して適用するオフセット値
  • baseinstance
    divisorを設定した頂点配列にアクセスするインデックスに対して適用するオフセット値

これ等のAPIは、いずれもglDrawElements()とglDrawElementsInstanced()に、頂点データ配列、もしくはdivisorを指定したインスタンスごとのデータ配列にアクセスする際のオフセット値を指定できるようにしたものです。
特に、インスタンス描画に対しては、baseinstanceの値とdivisorの値によって、次のように配列のインデックスが計算されます。

((current instance No) / divisor) + baseinstance

GPU側のデータを用いた描画

GLvoid glDrawElementsIndirect(GLenum mode,
                              GLenum type,
                              const GLvoid *indirect);
  • mode
    プリミティブの種類(POINT,LINE,TRIANGLE)
  • type
    インデックス配列の値の種類(UBYTE, USHORT, UINT)
  • indirect
    DrawElementsIndirectCommand構造体へのポインタ(ただし必ずnullを指定)

DrawElementsIndirectCommand構造体は以下の内容となっています。

typedef struct {
  GLuint  count;
  GLuint  primCount;
  GLuint  firstIndex;
  GLuint  baseVertex;
  GLuint  baseInstance;
} DrawElementsIndirectCommand;

glDrawElementsInstancedBaseVertexBaseInstance()の呼び出しを、構造体を通して行えるようにしたものです。一見すると、Client側のメモリに配置した構造体に対応しているように見えますが、indirectポインタはnullである必要があります。
実際の描画に用いる構造体は、GL_DRAW_INDIRECT_BUFFERという、描画命令バッファに格納されている必要があり、かつ、DrawCallの呼び出し時にBindされている必要があります。

GPU側のデータを用いた複数の描画

GLvoid glMultiDrawElementsIndirect(GLenum mode,
                                   GLenum type,
                                   const GLvoid *indirect,
                                   GLsizei drawcount,
                                   GLsizei stride);
  • drawcount
    DrawElementsIndirectCommand構造体配列の長さ
  • stride
    DrawElementsIndirectCommand構造体配列のサイズ(ポインタのインクリメント量)

glDrawElementsIndirect()を複数の構造体に対応させたものです。
構造体1つの描画ごとに、VertexShaderで参照できる、gl_DrawIDARBシステム変数(後述)がインクリメントされます。この変数を参照することで、VertexShader内で何番目の構造体による描画なのかを知ることが出来ます。

頂点シェーダーで参照できるシステム変数について

GLSL4.4では頂点シェーダーで以下の変数が参照可能です。

in int gl_VertexID;
in int gl_InstanceID;

加えて、ARB_shader_draw_parameters拡張を使用することで、以下のシステム変数が使用可能になります。

#extension GL_ARB_shader_draw_parameters : required

in int gl_DrawIDARB;
in int gl_BaseVertexARB;
in int gl_BaseInstanceARB;

それぞれが格納する値は以下の様になっています。

  • gl_VertexID
    頂点のインデックスが格納される。basevertexを用いて頂点インデックスに対してオフセットを適用した場合は、そのオフセットが加算された値になる。
  • gl_InstanceID
    インスタンスのインデックスが格納される。baseinstanceを用いてインスタンスのインデックスに対してオフセットを適用した場合でも、オフセットが加算されていない値になる。
  • gl_DrawIDARB
    MultiDraw系のDrawCallで描画された際に、描画された構造体のインデックスが格納される。
  • gl_BaseVertexARB
    basevertexを用いて頂点インデックスに対してオフセットを適用した場合に、そのオフセット値が格納される。
  • gl_BaseInstanceARB
    baseinstanceを用いてインスタンスのインデックスに対してオフセットを適用した場合に、そのオフセット値が格納される。

gl_VertexIDと、gl_InstanceIDが格納する値に一貫性が無いので注意が必要です。
なんとなくですが、basevertexはGPU上でインデックスをオフセットして、baseinstanceはドライバがポインタをオフセットしているためだと思われます。
gl_DrawIDARBは、MultiDraw系のDrawCallを使用する場合はほぼ必須の変数になると思われます。

最後に

当初は、DrawCallの説明だけ記事を書くつもりではなかったのですが、長くなったので、記事にしました。

OpenGLのImmutable data sotreについて(2)

先日Immutable data sotreへの書き込みは遅いので、スレッドを分けたほうが良いのではないかという記事を書きましたが、誤りでした。

Immutable data sotreに対するアクセス速度

まずはじめに、Immutable data storeで確保した24MByteのメモリ領域に、memcpy()を用いてデータを転送し、ローカルメモリ同士の場合と、転送速度を計測しました。

結果は、
CPU Mem(alloc()) -> Immutable data sotre 8068 MB/sec
CPU Mem(alloc()) -> CPU Mem(alloc()) 7995 MB/sec
計測誤差を鑑みると、ほぼ差がありません。

メモリはDDR3 1.33GHz DualChannelなので、実効帯域としては、この程度なのかも知れません。私のマシンでは、Immutable bufferはメインメモリ上に確保されているようです。
GPUViewのMemoryViewerで該当領域を確認したところ、やはりApertureのセグメントに確保されていました。

imm_write

imm_read_write

確保されるセグメントは、GL_MAP_WRITE_BITのみを設定した場合は、キャッシュされないApertureセグメントに確保されました。
GL_MAP_WRITE_BIT | GL_MAP_READ_BITを設定した場合は、CachedのApertureセグメントに確保されました。
確保時に設定したフラグによって、確保されるセグメントが異なります。
ためしに、GL_MAP_WRITE_BITのみを設定したセグメントを転送元として、memcpy()を実行したところ、約180 MB/secでした。GL_MAP_READ_BITを設定していないので、読み出し自体が許されていない領域ですが、転送速度は著しく遅いです。
GL_MAP_WRITE_BIT | GL_MAP_READ_BITを設定した場合は、約7000 MB/secで読み出すことが出来ました。書き込みに比べると若干遅いですが、問題になるほどではありません。

まとめ

少なくとも、私のマシンでは、Immutable data storeは、GL_CLIENT_STORAGE_BITを設定しなくても、CPU側のメモリに確保されるようです。また、GL_MAP_READ_BITをつけると、CachedのAperture領域を確保して、読み出しを高速に行えるように配慮されます。
DeviceLost(画面解像度の変更などで)を発生させても、バッファの内容は保持されていました。特別何かを意識する必要なく、普通に確保したメモリと同様に扱っても不都合は無いように思えました。
一方で、GPU側からImmutable data storeのバッファにアクセスする際は、PCIeバスを経由したアクセスとなるはずです。帯域や遅延の問題が考えられます。なるべく、GPU側のキャッシュのヒット率が高い状態になるようなアクセスパターンが求められると思われます。(今後機会があれば検証してみたいと思います。)

OpenGLの同期APIについて

OpenGLのAPIには、コマンドバッファの制御と、コマンド実行の同期を取る方法として、幾つかのAPIが用意されていますが、ややこしいので少しまとめてみます。

GLvoid glFlush(GLvoid)

OpenGLのAPIによって生成される、各種描画命令は、通常はディスプレイドライバー側で、ある程度まとめてからGPUに転送されます。これをコマンドキューと呼びます。
この命令は、コマンドキューに蓄積されている描画命令を、その量に関わらずGPU側に転送するものです。通常のレンダリングでは、特に呼び出す必要はありません。
Queryオブジェクトを使用した場合や、コマンドキューへの描画命令の蓄積による遅延が気になる時などに使用します。ただし、あまり頻繁に呼び出すと、転送命令のオーバーヘッドが大きくなるため性能低下を招くこともあります。

GLvoid glFinish(GLvoid)

glFinish()は、直前まで呼び出されたGLのAPIによる描画が、全て終了するまで関数をブロックします。逆を言えば、glFinish()が呼び出された後は、全てのGPU処理が終了していることを意味します。
たまに、glFinish()をSwapBuffer()の直前に呼び出しているプログラムを見かけることがありますが、意図したもので無い限りお勧めできません。
GPUに対する描画命令は、その実行に対して先行してコマンドキューに蓄積されるのが理想的です。glFinish()を待つことは、これを放棄することになります。glFinish()の呼び出しは、デバッグ時の確認等に限られると思います。

GLsync glFenceSync(GLenum condition,GLbitfield flags)

condition: GL_SYNC_GPU_COMMANDS_COMPLETE
flgas: 0

このAPIの呼び出しにより、現在のGLのコンテキストに、同期オブジェクトを設定します。言い換えれば、直前までのGLの描画で、何らかの同期を取りたい場合のマーカーとしてAPIを呼び出します。戻り値で入手できる値が、該当の同期オブジェクトになります。
同期オブジェクトは同時に複数使用可能なので、必要に応じて呼び出します。
現在(OpenGL4.3 core profile)の仕様では、上記の引数しか認められていません。従って、引数は事実上意味を成しません。

void glDeleteSync(GLsync sync )

glFenceSync()で入手した同期オブジェクトを破棄します。GLの同期オブジェクトは、使い捨ての構造になっており、同期オブジェクトを設定するたびに新たなオブジェクトが手に入ります。
従って、同期の監視が終了したら、glDeleteSync()でオブジェクトを破棄する必要があります。
glDeleteSync()は同期オブジェクトの状態に関わらず呼び出し可能なので、必要がなくなった時点で呼び出し可能です。

GLenum glClientWaitSync( GLsync sync, GLbitfield flags,GLuint64 timeout )

sync: 監視する同期オブジェクト
flags: GL_SYNC_FLUSH_COMMANDS_BIT もしくは 0
timeout: タイムアウト(nanosecond)

引数に指定した、同期オブジェクトがSignal状態(同期オブジェクト以前までの、GLのAPI呼び出しの実行が完了している状態)になるか、timeout時間が経過するまで、関数をブロックします。
flgasにGL_SYNC_FLUSH_COMMANDS_BITを指定すると、直前までのコマンドキューをGPUに転送します。glFlush()と同じ効果が得られます。

返されるenum値は、

  • GL_ALREADY_SIGNALED
    glClientWaitSync()を呼び出した時点で、既に同期オブジェクトがSignal状態(GL命令の実行が完了している状態)だった。

  • GL_CONDITION_SATISFIED
    timeoutになる前に、同期オブジェクトがSignal状態になった。

  • GL_TIMEOUT_EXPIRED
    timeoutした。

  • GL_WAIT_FAILED
    エラーによって、同期オブジェクトの監視に失敗した。

  • GL_INVALID_VALUE
    関数の呼び出し自体に失敗した

となっています。
Signal状態では、状況によって、GL_ALREADY_SIGNALEDもしくは、GL_CONDITION_SATISFIEDが返されるので注意が必要です。
timeoutに0を指定することも可能です。その場合は、即時に同期オブジェクトをチェックして、enum値が返されます。

この関数は、glFinish()の機能を、よりフレキシブルなものにしたものだと考えることが出来ます。
使い道は、コマンドキューで先行する描画フレーム数に制限をかけたり、以前の描画で使用したリソースを再利用し、Client側から新たに描画リソースの転送を行いたい場合の同期などです。
具体的には、Immutable buffer storageや、GL_MAP_UNSYNCHRONIZED_BITを用いたMap()/Unmap()によるリソースの更新が、描画を追い越さないようにする場合などです。

GLvoid glWaitSync(GLsync sync, GLbitfield flags, GLuint64 timeout )

sync: 監視する同期オブジェクト
flag: 0
timeout: GL_TIMEOUT_IGNORED

このAPIの呼び出しにより、現在のGLのコンテキストに同期オブジェクトの監視を設定します。このAPIの呼び出しは、即時にリターンし、Client側(CPU側)では何も待ちません。
この呼び出しにより、GPUがGPUの処理の終了を待つことになります。

GPUの描画命令は、基本的に順序を入れ替えることなく実行されるので、glWaitSync()による同期は、一部の場合を除いて必要ありません。
また、従来のレンダリングにおいて、前後に依存関係がある場合では、ドライバが必要に応じて同期を取るようにGPUに指示することで、解決されてきました。
一見すると、使い道が思いつかないこの命令が必要なケースとして考えられるのは、マルチスレッド環境下でのGLの使用です。近年のGLではマルチスレッド環境下で、更新データをCPU側から転送しつつ、GPU側にレンダリングを指示することが出来ます。
特にNVIDIA製のQuadroなどは、PCIExpressを経由するDMAチャンネル(CopyEngine)を2つ持っており、GPUに対するPBO等のリソース転送と描画命令の発行を同時に行うことが出来ます。
この際に、PBOの転送完了とそれを用いた描画の開始で同期を取る必要があり、CPU側のスレッド間の同期に加え、GPU側の同期が必要となり、glWaitSync()を用いる必要が発生します。
もうひとつ、この命令が必要なケースとして考えられるのが、近年のBindlessリソースを用いたレンダリングだと思われます。
Bindlessリソースは、従来のように、GLのオブジェクトをBindして利用する代わりに、ポインタを用いてリソースを使用します。こうすることで、Bindによるオーバーヘッドと、同時に使用できるリソース数の制限に縛られることなくレンダリングが可能となります。
その代わりに、従来はドライバーが行っていた、レンダリングにおけるリソースの依存関係の監視が、事実上不可能になります。GPUは可能な限り、描画命令を並列で実行しようとするので、RAWハザードなどに陥る可能性があると思われます。
現在のところは、ComputeShaderを用いてレンダリングリソースを更新する際は、ComputeとGraphicsの切り替え時に同期されるので、あまりこのようなケースは見かけませんが、Graphicsのパイプラインを用いて、Bindlessリソースの更新を行う場合は、上記に留意し、プログラム側で調停の管理をする必要があるかもしれません。