OpenGLのImmutable data sotreについて

OpenGLでアドレス固定のBufferObjectを作成する方法は、過去様々なExtensionがありましたが、近年では、ARB_buffer_storage拡張が4.3に対して作られ、それが、OpenGL4.4のCoreの仕様となったようです。
注目すべきは、この機能が既にCoreに取り込まれているということで、比較的先進的な機能と思われるものでありながら、GPUベンダー依存しないコードで実装が可能という点だと思います。

まずは、アドレス固定のBufferObjectを作成するAPIである、glBufferStorageの説明を確認してみたいと思います。


Name – 名称

glBufferStorage — (訳注:格納領域のアドレスが)不変的なデータの格納領域の作成と初期化

C Specification – C言語上の定義

void glBufferStorage( GLenum target,
GLsizeiptr size,
const GLvoid * data,
GLbitfield flags);

Parameters – パラメーター

target
ターゲットとなるバッファの種類を指定。以下の中から指定しなくてはならない。
GL_ARRAY_BUFFER, GL_ATOMIC_COUNTER_BUFFER, GL_COPY_READ_BUFFER, GL_COPY_WRITE_BUFFER, GL_DRAW_INDIRECT_BUFFER, GL_DISPATCH_INDIRECT_BUFFER, GL_ELEMENT_ARRAY_BUFFER, GL_PIXEL_PACK_BUFFER, GL_PIXEL_UNPACK_BUFFER, GL_QUERY_BUFFER, GL_SHADER_STORAGE_BUFFER, GL_TEXTURE_BUFFER, GL_TRANSFORM_FEEDBACK_BUFFER, or GL_UNIFORM_BUFFER.

size
作成するバッファのサイズをByte単位で指定

data
バッファにコピーされる、初期化データのポインタを指定。Nullならば初期化しない。

flags
バッファの想定される使用用途に基くフラグを指定。
以下のビットの組み合わせである必要がある。
GL_DYNAMIC_STORAGE_BIT, GL_MAP_READ_BIT GL_MAP_WRITE_BIT, GL_MAP_PERSISTENT_BIT, GL_MAP_COHERENT_BIT, and GL_CLIENT_STORAGE_BIT.

Description – 説明

glBufferStorageは、新たに不変的なデータの格納領域を、現在targetにバインドされているオブジェクトに作成する。
格納領域のサイズは、sizeで指定する。
初期化データがある場合、引数dataで指定する。初期化しないで、格納領域を作成する場合は、dataはNULLを指定する。
flagsは、格納領域の想定される使用用途の基く値を指定する。以下のビットの組み合わせである必要がある。

GL_DYNAMIC_STORAGE_BIT
バッファの内容は、glBufferSubDataの呼び出しで更新することが出来るようになります。
このBitがセットされていない場合は、クライアント(訳注:CPU側)から直接更新することは出来ません。
glBufferStorage()の引数で渡した初期化データは、このビットに関わらず初期化データとして使用されます。
このビットに関わらず、サーバーサイドGL関数(訳注:GPU側で処理が完結するGLの関数)の呼び出しによる更新(glCopyBufferSubData,glClearBufferSubData)は可能です。

訳注:このBitをセットしなくても、後述のGL_MAP_PERSISTENT_BITを用いた方法では、クライアント側からのデータ更新は可能です。

GL_MAP_READ_BIT
バッファは、読み出しのためにクライアント側でMapすることができ、読み出し可能な、クライアント側のアドレス空間のポインタが入手できます。

GL_MAP_WRITE_BIT
バッファは、書き出しのためにクライアント側でMapすることができ、書き出し可能な、クライアント側のアドレス空間のポインタが入手できます。

GL_MAP_PERSISTENT_BIT
バッファをMapしている間でも、サーバー(訳注:GPU側)に読み出しor書き出を含む処理をさせることが出来るようにします。
クライアント側のポインタ(訳注:Mapした際にCPU側で受け取るポインタ)はMapされている間は、たとえ描画の実行中であっても有効です。

GL_MAP_COHERENT_BIT
クライアント側で、glMapBufferRangeを用いてMapし、同時にクライアントとサーバーからアクセスした場合に、コヒーレントを維持します。これは、アプリケーションが特に何もしなくても、クライアント側、サーバー側からのストアが、即時に他方から見て反映されていることを意味します。

とくに、
* GL_MAP_COHERENT_BITをセットせずに、クライアント側からバッファを更新し、glMemoryBarrierをGL_CLIENT_MAPPED_BUFFER_BARRIER_BITで呼び出した場合、以後のGLのコマンドで、サーバー側は書き込み結果が反映された状態を使用することが出来ます。

* GL_MAP_COHERENT_BITをセットし、クライアント側からバッファを更新した場合、以後のGLのコマンドで、サーバー側は書き込み結果が反映された状態を使用することが出来ます。

* GL_MAP_COHERENT_BITをセットせずに、サーバーでバッファを更新し、その内容をクライアントから見る場合は、アプリケーションは、glMemoryBarrierをGL_CLIENT_MAPPED_BUFFER_BARRIER_BITで呼び出し、さらにglFenceSyncをGL_SYNC_GPU_COMMANDS_COMPLETEで呼び出す必要があります。

* GL_MAP_COHERENT_BITをセットし、サーバーでバッファを更新し、その内容をクライアントから見る場合は、glFenceSyncをGL_SYNC_GPU_COMMANDS_COMPLETEで呼び出す必要があります。
訳注:glFenceSyncが必ず必要というわけではなく、該当のバッファに関連するGL描画命令がGPU上で”実際に”完了している必要があるという意味と思われます。

GL_CLIENT_STORAGE_BIT
他のバッファ確保の条件が整っているとき、GLの実装によっては、サーバーにとってのローカル、もしくは、クライアントにとってのローカルで補助のバッファとして機能するかを決定するBitになります。

訳注:
おそらくこれを設定すると、条件とGLの実装によっては、クライアント側にバッファの実体を確保し、GPU側からCPUメモリを読み出す形になると思われます。COHERENT_BITがあるので、バッファをCPUとGPU両方に確保し、UnmapなどのコマンドをトリガーとしてGPU側へコピーすることは機能上不可能と思われます。(もしくはCOHERENT_BITと同時に使用できない)
(追記:おそらく実行環境に依存すると思われますが、このBitを設定しなくても、Client側にバッファが確保される事もあります)


といった感じです。
実際にこれを使用したサンプルとして、
John McDonaldさんが作成したGitHubのリポジトリにapitestというものがあります。
これはAPIのテストコードとして記述されたものの様で、実際に上記のAPIを用いて、頂点バッファをCPU側から動的に更新しつつ描画しているサンプルが含まれています。
https://github.com/nvMcJohn/apitest

VB転送テストのコードの概要

初期化処理で、ARRAY_BUFFERとして、24MBを確保。
6頂点の2D描画用3角形2枚分の頂点をCPU側から更新(計48Byte)し、これをTRIANGLESで描画。
この描画を16万回繰り返し、SwapBufferする。
1Frameあたり、合計で7.32MB分バッファが更新される。
バッファのサイズは3.2Frame分あり、FenceSyncは行っていない。(Overwrapされる前に、描画が終了する前提。)

バッファの確保と更新方法は3通りある。

ImmutableBuffer(COHERENT指定)を用いる方法

初期化
書き込み用の、coherent維持の、ImmutableBufferとして確保。MapBufferRangeしたまま描画を行う。

GLbitfield flags = GL_MAP_WRITE_BIT | GL_MAP_PERSISTENT_BIT | GL_MAP_COHERENT_BIT;
glBufferStorage(GL_ARRAY_BUFFER, DYN_VB_SIZE, NULL, flags);
m_dyn_vb_ptr = glMapBufferRange(GL_ARRAY_BUFFER, 0, DYN_VB_SIZE, flags);

描画
描画時は、MapBufferRangeの際に入手したポインタに直接memcpyして描画。同期処理などは全く無し。

memcpy(dst, vertices, count * sizeof(VertexPos2));
glDrawArrays( GL_TRIANGLES, vertex_offset, count);

BufferSubDataを用いる方法

初期化
通常の動的バッファの確保と同じ

glBufferData( GL_ARRAY_BUFFER, DYN_VB_SIZE , nullptr , GL_DYNAMIC_DRAW );

描画
BufferSubDataで更新し描画。

glBufferSubData( GL_ARRAY_BUFFER, byte_offset, size, vertices);
glDrawArrays( GL_TRIANGLES, vertex_offset, count);

MapBufferRangeを用いる方法

初期化
通常の動的バッファの確保と同じ

glBufferData( GL_ARRAY_BUFFER, DYN_VB_SIZE , nullptr , GL_DYNAMIC_DRAW );

描画
MapBufferRangeの際に、領域破棄と、書き込み、非同期処理用のフラグを指定。
バッファを更新後、UnmapBufferして描画。

void* dst = glMapBufferRange(GL_ARRAY_BUFFER, byte_offset, size,
      GL_MAP_WRITE_BIT | GL_MAP_INVALIDATE_RANGE_BIT | GL_MAP_UNSYNCHRONIZED_BIT);
memcpy(dst, vertices, count * sizeof(VertexPos2));
glUnmapBuffer(GL_ARRAY_BUFFER);
glDrawArrays( GL_TRIANGLES, vertex_offset, count);

処理の所要時間とGPUViewで取得されたログ

テスト環境は、Core-i7 3.4GHzとGTX680。

ImmutableBufferを用いた場合
13.6ms
1Frameあたり、約31個のDMA Packet。CPUからの頂点データ更新のメモリ転送はログに出ない。
immutable

BufferSubDataを用いた場合
450ms
subdata

MapBufferRangeを用いた場合
811ms
mapbuffer_range

考察

 16万回のDrawCallを、頂点バッファを逐次更新しつつ行うのは、従来では、ほぼ禁忌といって良い処理だったと思います。いまでも実際のアプリケーション内のシナリオとしては考えられない、非現実的な処理ですが、ImmutableBufferは現実的な速度でこれを処理しました。
 注意する点があるとすれば、GPUViewのログで確認できるように、ImmutableBufferの更新は、透過的に行われるため、処理として全く面に見えません。ただし、CPUの処理時間から察するに、ImmutableBufferへのmemcpyは、ローカルメモリ内のコピー処理よりはるかに時間が掛かるようです。これはCPUのスレッドが、描画中に動作し続けていることから確認できます。実際のアプリケーションでは、書き込み用のスレッドを分ける事も検討したほうが良いかも知れません。(追記:CPUのスレッドが描画中に動作し続けているのは、memcpy()の処理と、DrawCallの処理を行っているからで、特段にコピー処理が低速な訳ではありませんでした。)
 もうひとつは、描画コマンドと、バッファの更新処理の調停は、完全にアプリケーション側に委ねられるということです。COHERENTフラグを指定した描画では、memecpyの直後でDrawCallを行っても、バッファに結果が反映されているので、正しく描画できます。しかし、裏を返せば、このバッファの該当領域を使用した、過去の描画が、完全に終了しているように、アプリケーションをデザインするように求められているという事です。
 また、GPUViewを用いて、CPU/GPUのボトルネックの判定を行う場合、今後は一層注意する必要がありそうです。ImmutableBufferを用いた例は、一見するとCPU側の何らかの処理に時間が掛かり、GPUの描画処理が不足し、GPUがアイドルしている様なログに見えます。しかし実際は、バッファの更新処理をしつつ描画し続けている状態です。従来は、このような処理は、MapBufferとSubDataで見られるようなDMAパケットとして観測でき、容易に判断することが出来ました。今後はアプリケーションのコードと共に注意深く観察する必要がありそうです。