月別アーカイブ: 2014年1月

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パケットとして観測でき、容易に判断することが出来ました。今後はアプリケーションのコードと共に注意深く観察する必要がありそうです。

広告

Adaptive Depth Bias for Shadow Mapsについて

例によって概要だけですが。ShadowMapのBiasに関する論文です。
[参照]
Adaptive Depth Bias for Shadow Maps

Slope Scale Depth Biasと比較して違うところ

本手法も、FragmentにおけるTangentに基くbiasを適用しますが、そのbiasの量の計算方法は異なります。
まず、対象FragmentのPixelが参照するShadowMap(以下SM)のTexelの中心位置と、Lightの位置を結ぶVectorを計算します。
一方で、FragmentのDepthとNormalより、Fragmentの三次元上での平面を求め、この平面と先のVectorの交点を計算します。
Lit/Shadowの判定は、Lightから交点までの距離と、SMのDepth値との比較によって判定します。
つまり、従来の手法では、Lit/Shadowの判定をFragmentのPixelの中心で行っていましたが、本手法では、SMのTexelの中心で行う様に計算するのが大きな違いです。従って、このBiasはDepthにおける奥側にも手前側にも作用します。
描画されたSMのTexelと、描画するFragmentのPixelが、同一平面であった場合は、この計算より求まる交点のLightからの距離は、SMのTexelに格納されたDepthの値と正確に一致するはずです。
このままではBiasはゼロなので、必要最低限のBias(以下Epsilon)を適用して判定します。
Epsilonは、いわゆるShadow acne/detachment等の現象を避けるためのものでなく、主にDepth Bufferの精度とFloatの精度によって生じる誤差を回避するためのものとなります。
Epsilonの計算は、DepthBufferの精度に基いて、カメラからの距離に応じて最適な値を計算します。(参照論文中の eq.3)

利点

おそらくですが、現状で最も最適化されたShadowMapのBiasと思われます。従って、shadow acne, shadow detachmentの問題が最も少ない結果が期待できます。
また、他のWarp系の手法(Dual Paraboloid、Perspective SM)にも正しく応用することができ、 良好なBias値の導出が可能です。
実行時間に関しても、slope scale depth biasを使用している状況下と比較するならば、軽微な増加と考えることができると思います。詳しくは論文内にデータがあるので参照してください。

残る問題点

FragmentのPixelとSamplingされるSMのTexelが、同一平面上でなかった場合は誤った結果になる可能性があります。
特に、FragmentのTangentがLightのVectorに対して平行に近づくケースでは、誤差が増大すると思われます。この誤差によって、判定を誤るケースが存在しますが、slope scale depth biasの場合も、同様の問題を抱えていると思われます。
もうひとつは、Percentage Closer Filterを代表とする、SMの隣接する他のTexelを参照する、Filter系の手法では、この手法を適用しても良い結果を得るのは難しいと思われます。
中心となるSMのTexelに対して、タイトにBiasを計算しても、SMの周辺の値を参照する手法では、あまり意味を成さないと思われます。
唯一PCFに関しては、Samplingした個々のSMのTexelの中心へのVectorを、平面に投影することで、この手法を正確に適用することは可能ですが、本来の計算対象である、FragmentのPixel位置より大きく離れた場所で、タイトにBiasを計算することに、大きな意味は無いと思われます。
従って、本手法を用いる際は、SMの判定が終わった後のパスで、Screen Spaceで、Cross-BilateralFilterを適用するなどの方法を考える必要があると思われます。

※VSVに関する部分は割愛させて頂きました。基本的には同じ考え方を適用していると思います。