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リソースの更新を行う場合は、上記に留意し、プログラム側で調停の管理をする必要があるかもしれません。

広告