タグ別アーカイブ: OpenGL

OpenGL関連の話題

GL_NV_command_list 拡張について

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

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

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

OpenGLの機能拡張が分かりにくいワケ

先日、久しぶりにOpenGL Extension Registryを見てたら、glcorearb.hなるファイルがありました。(気づくの遅い)

OpenGLの関数群について

現在のOpenGLの関数群は、大きく分けて、3種類に分かれます。
まずは、Vendor拡張とEXT拡張と呼ばれる関数群があります。これ等は、特定のGPUベンダーや、OS/PCメーカーなどが追加した機能拡張です。EXT拡張は、複数の会社が合意して提出された機能拡張です。大抵のOpenGLの最新機能は、これ等の拡張で定義された関数群で実装されます。
次は、ARB/KHR拡張と呼ばれる機能です。ARBは、OpenGL Architecture Review Boardのことで、OpenGLの機能を決める評議会と言えると思います。ここで承認された機能拡張は、ARB拡張となります。大抵は、まずはじめにVendor拡張や、EXT拡張として実装された機能が、一定の有意性が見出された場合、ARBの承認を得ることで、ARB拡張となることが多いです。KHRはKhronos Groupが主導して導入された機能拡張のようですが、ARBと同一の機能拡張番号を使用しているので、実質的にARB拡張とみなして良いと思います。
最後に、Coreと呼ばれる機能です。これは、OpenGLの標準機能に属します。これは、OpenGLのバージョンが上がるたびに、機能が変わりますが、ひとたび特定のバージョンをサポートしたDriver/GPUがあれば、それは、そのバージョンで定義されている機能を全てサポートしている必要があります。サポートする機能は、バージョンが上がるたびに、基本的には追加という形で増えていきます。大抵は、ARB拡張で定義されている機能拡張が、Coreの機能として取り込まれることが多いです。
しかし、OpenGL3.1/3.2では、機能の削減が行われました。(具体的には、一部のCore機能をARB拡張に移しました。)したがって、後方互換性を保障するものではありません。

OpenGLの機能拡張が分かりにくいワケ

上記のように、とあるひとつの機能が、Coreに取り込まれるまでの過程で、Vendor/EXT/ARB/Coreと複数の定義を持つことが多々あります。
そのため同一の機能が、複数の関数やdefineを持つこととなり、OpenGLの機能の分かりにくさを助長していると思います。
不要な機能拡張の使用を避けるため、OpenGLのプログラムを書く際は、OpenGL Extension Registryにアップされている、最新バージョンのCoreProfileSpecificationに沿って、プログラムを書くのが一番だと思います。そして、Coreに無い機能を使う場合には、まずARB拡張を探し、次にEXT拡張、最後にVendor拡張を探すと良いと思います。

ここでは、ひとつの分かりにくい例として、Occlusion Queryの機能について調べてみたいと思います。
Occlusion Query関連は、

  • GL_HP_occlusion_test
  • GL_NV_occlusion_query
  • GL_ARB_occlusion_query
  • GL_ARB_occlusion_query2
  • OpenGL Core 1.5
  • OpenGL Core 3.3
  • OpenGL Core 4.3

で機能が追加されたり、Vendor->ARB->Coreへの変更などが行われています。下記にその内容を示します。

GL_HP_occlusion_test機能拡張

Occlusionの結果をBool値のみで取得可能。QueryがObject化されていないので、複数のOcclusionTestの結果を得るためには、一旦結果をCPU側で取得しなくてはならない。昔に作られた仕様。

GL_HP_occlusion_test

#ifndef GL_HP_occlusion_test
#define GL_HP_occlusion_test 1
#define GL_OCCLUSION_TEST_HP              0x8165
#define GL_OCCLUSION_TEST_RESULT_HP       0x8166
#endif /* GL_HP_occlusion_test */

使い方
glEnable(GL_OCCLUSION_TEST_HP)
//gl rendering calls
glDisable(GL_OCCLUSION_TEST_HP)
 
glGetBooleanv(GL_OCCLUSION_TEST_RESULT_HP, &result)

GL_NV_occlusion_query機能拡張

Occlusionの結果を描画されたサンプル数で取得する。QueryがObject化されているので、複数のOcclusionQueryの結果を、非同期的に取得することができる。CPU側で、即時に結果が読めるかどうかのチェックも可能。DX9の機能とほぼ同等。

GL_NV_occlusion_query

#ifndef GL_NV_occlusion_query
#define GL_NV_occlusion_query 1
#define GL_PIXEL_COUNTER_BITS_NV          0x8864
#define GL_CURRENT_OCCLUSION_QUERY_ID_NV  0x8865
#define GL_PIXEL_COUNT_NV                 0x8866
#define GL_PIXEL_COUNT_AVAILABLE_NV       0x8867

#ifdef GL_GLEXT_PROTOTYPES
GLAPI void APIENTRY glGenOcclusionQueriesNV (GLsizei n, GLuint *ids);
GLAPI void APIENTRY glDeleteOcclusionQueriesNV (GLsizei n, const GLuint *ids);
GLAPI GLboolean APIENTRY glIsOcclusionQueryNV (GLuint id);
GLAPI void APIENTRY glBeginOcclusionQueryNV (GLuint id);
GLAPI void APIENTRY glEndOcclusionQueryNV (void);
GLAPI void APIENTRY glGetOcclusionQueryivNV (GLuint id, GLenum pname, GLint *params);
GLAPI void APIENTRY glGetOcclusionQueryuivNV (GLuint id, GLenum pname, GLuint *params);
#endif
#endif /* GL_NV_occlusion_query */

使い方
glBeginOcclusionQueryNV(occlusionQueries[i]);
// render 
glEndOcclusionQueryNV();

// you can check if the result is already available for reading on CPU side.
glGetOcclusionQueryuivNV(occlusionQueries[i], GL_PIXEL_COUNT_AVAILABLE_NV, &isAvailable);
// retrieve the pixel count
glGetOcclusionQueryuivNV(occlusionQueries[i], GL_PIXEL_COUNT_NV, &pixelCount);

GL_ARB_occlusion_query機能拡張

Vendor拡張がARB拡張になった典型的なケース。defineの名称や、関数名がARBに即した形になるが、同じ意味のdefineはNV拡張と同じ値。

GL_ARB_occlusion_query

#ifndef GL_ARB_occlusion_query
#define GL_ARB_occlusion_query 1
#define GL_QUERY_COUNTER_BITS_ARB         0x8864
#define GL_CURRENT_QUERY_ARB              0x8865
#define GL_QUERY_RESULT_ARB               0x8866
#define GL_QUERY_RESULT_AVAILABLE_ARB     0x8867
#define GL_SAMPLES_PASSED_ARB             0x8914

#ifdef GL_GLEXT_PROTOTYPES
GLAPI void APIENTRY glGenQueriesARB (GLsizei n, GLuint *ids);
GLAPI void APIENTRY glDeleteQueriesARB (GLsizei n, const GLuint *ids);
GLAPI GLboolean APIENTRY glIsQueryARB (GLuint id);
GLAPI void APIENTRY glBeginQueryARB (GLenum target, GLuint id);
GLAPI void APIENTRY glEndQueryARB (GLenum target);
GLAPI void APIENTRY glGetQueryivARB (GLenum target, GLenum pname, GLint *params);
GLAPI void APIENTRY glGetQueryObjectivARB (GLuint id, GLenum pname, GLint *params);
GLAPI void APIENTRY glGetQueryObjectuivARB (GLuint id, GLenum pname, GLuint *params);
#endif
#endif /* GL_ARB_occlusion_query */

GL_ARB_occlusion_query2機能拡張

ここで再びBool値でOcclusionの取得が可能になる。同時期にOpenGL Core3.3に取り込まれたため、本来ARBで定義されるべきdefineが無い。このように、Coreに取り込まれることが前提となるARB拡張では、ARBのpostfixがdefineや関数名についていないことがある。

GL_ARB_occlusion_query2

#ifndef GL_ARB_occlusion_query2
#define GL_ARB_occlusion_query2 1
#endif /* GL_ARB_occlusion_query2 */

GL_ANY_SAMPLES_PASSEDというdefineが新設されているが、Coreのdefineとして定義されている。

結果をBool値で欲しいとき(Bool値で十分なとき)は、GL_ANY_SAMPLES_PASSEDをQueryのBegin/Endで使用し、結果をBool値で受け取る。

OpenGL Core 1.5

GL_ARB_occlusion_queryに相当する機能がCore1.5で追加された。ARBのpostfixが取れている。

OpenGL Core 1.5
#ifndef GL_VERSION_1_5
#define GL_VERSION_1_5 1
#define GL_QUERY_COUNTER_BITS             0x8864
#define GL_CURRENT_QUERY                  0x8865
#define GL_QUERY_RESULT                   0x8866
#define GL_QUERY_RESULT_AVAILABLE         0x8867
#define GL_SAMPLES_PASSED                 0x8914

#ifdef GL_GLEXT_PROTOTYPES
GLAPI void APIENTRY glGenQueries (GLsizei n, GLuint *ids);
GLAPI void APIENTRY glDeleteQueries (GLsizei n, const GLuint *ids);
GLAPI GLboolean APIENTRY glIsQuery (GLuint id);
GLAPI void APIENTRY glBeginQuery (GLenum target, GLuint id);
GLAPI void APIENTRY glEndQuery (GLenum target);
GLAPI void APIENTRY glGetQueryiv (GLenum target, GLenum pname, GLint *params);
GLAPI void APIENTRY glGetQueryObjectiv (GLuint id, GLenum pname, GLint *params);
GLAPI void APIENTRY glGetQueryObjectuiv (GLuint id, GLenum pname, GLuint *params);
#endif
#endif /* GL_VERSION_1_5 */

OpenGL Core 3.3

GL_ARB_occlusion_query2に相当する機能が追加された。必要なdefineが定義されている。

OpenGL Core 3.3

#ifndef GL_VERSION_3_3
#define GL_VERSION_3_3 1
#define GL_ANY_SAMPLES_PASSED             0x8C2F
#endif /* GL_VERSION_3_3 */

OpenGL Core 4.3

MultiSample使用時のQueryのパフォーマンス向上のための機能追加が行われた。前提となる拡張が無く、いきなりCoreに取り込まれた。

OpenGL Core 4.3

#ifndef GL_VERSION_4_3
#define GL_VERSION_4_3 1
#define GL_ANY_SAMPLES_PASSED_CONSERVATIVE 0x8D6A
#endif /* GL_VERSION_4_3 */

追加されたdefineは、MutisampleのRenderTargetを使用している時の、Ccclusionテストの高速化を実現するためのもの。ただし、保守的な方向で、実際とは異なる結果を返す可能性がある。(全てのsampleがOccludeされているにも関わらず、されていないと返す可能性があるが、はやくなる(かもしれない))

gl_corearb.hについて

上記で示したとおり、OpenGLの機能拡張で定義された関数やdefineを全て含んだ glext.h というヘッダファイルは、いわば二重定義の嵐のような状況になっています。
OpenGLの機能拡張に精通していれば、それほど迷うことは無いのですが、初めてOpenGLを学んだ方には、まさにカオティックな内容になっています。
また、OpenGL3.2で追加されたCoreProfileとCompatibilityProfileでは、使用可能な機能に違いがあるのですが、同一のヘッダファイルとライブラリファイルを使用するため、コンパイルは通るけど、使ってはいけない関数というものが存在していました。
この状況下で、OpenGLのCoreとARB拡張のみを取り出した、gl_corearb.h を新設したのはすばらしい事だと思います。gl_corearb.hに定義されるARB拡張は、OpenGLのCoreProfileと互換性のあるもののみで、CompatibilityProfileでのみ使用可能なARB拡張は定義されないそうです。詳しくは、OpenGL 4.3 Core Profile SpecificationのAppendix G.2に記載されています。
ちなみに、OcclusionQuery関連について、gl_corearb.hをチェックすると、GL_ARB_occlusion_queryはgl_corearb.hに入っていませんでした。しかし、GL_ARB_occlusion_query2は残っていました。なんだか上記の説明と異なる気もしますが、CoreProfileが定義されるOpenGL3.2以前にCoreに統合されたARB拡張も、削除されていると考えると納得できます。

WindowsでGLを御使用の方へ

Windows上でのOpenGL Extensionを使用する場合は、Perl等を使って、gl_ext.hを書き換えれば簡単に使用できる旨を以前の[記事]で書きましたが、gl_corearb.hも同様に、Perl等を使って加工することで簡単に使えると思います。
ただし、gl_corearb.hには、OpenGL1.0 Coreについて定義があり、ここで定義されている関数は既に存在します。一方その他の関数はwglGetProcAddress()で取得する必要があるので、OpenGL1.0 Coreの定義の部分だけ、気をつけて処理する必要があります。
一応2013/10現在のgl_ext.h, wgl_ext.h, gl_corearb.hをPerlで加工したものをUploadしておきます。
glcorearb_win-zip.jpg(保存して拡張子変えてください)

使い方は、プログラム中で一箇所のみ、関数とポインタの実体を作る必要があるので、以下のように記述します。

#define WINDOWS_GL_EXT_DEFINE_FUNCTIONPTR 1
#define WINDOWS_GL_EXT_CREATE_FUNCTIONPTR 1
#include "glcorearb_win.h"
#include "wglext_win.h"
#undef WINDOWS_GL_EXT_CREATE_FUNCTIONPTR
#undef WINDOWS_GL_EXT_DEFINE_FUNCTIONPTR

それ以外の箇所で外部参照する際は、下記の様に記述します。

#define WINDOWS_GL_EXT_DEFINE_FUNCTIONPTR 1
#include "glcorearb_win.h"
#include "wglext_win.h"
#undef WINDOWS_GL_EXT_DEFINE_FUNCTIONPTR

注:上記ファイルは、余り(殆ど)チェックをしていないので、なにか不具合があるかもしれません。

DrawCallが9倍早くなるワケ

地表で生きるものとして、Windows上でのDrawCallの仕組みを見てみます。

DrawCallがCPU処理時間を消費する理由

DrawCallがプロセス側から発行されると、その時点で、描画に使用するリソース(VB,IB,Tex,CBなど)が、存在するかどうか、大きさは適切であるかなどの、妥当性のチェックが行われると同時に、リソースがGPU側のデバイスメモリに転送されているか、転送されていればそのアドレスはどこなのか、などの問題が解決され、GPU用の描画命令コマンドとして、専用のバッファに蓄積されます。
ここまでが、大まかですが、DrawCallの発行によって、CPU側で処理される内容です。
リソースの生成を伴わない、通常のDrawCallであれば、専用のバッファへの蓄積までは、UMD(User Mode Driver)で処理されます。UMDは、皆さんのプロセスと同じアドレス空間を用いて動作するもので、要はDLLのライブラリ等と同じです。KMDはこのケースでは動作することは殆どありません。(KMDについては後述)
では、何がCPU処理時間を消費しているのでしょうか。
上記で述べた、使用リソースの妥当性チェックと、リソースのオブジェクト名からアドレスへの変換と、API呼び出しのオーバーヘッドです。

ちなみに、DXでは、DXのAPIが呼び出されると、DXのレイヤーの処理に加え、必要に応じてそれに対応するUMDの関数が呼び出されますが、OGLの場合は、その処理のほとんどが、UMDに相当するレイヤーで行われます。従って、OGLの方が、全般的にAPIレイヤーのオーバーヘッドが少ないと思われます。
下記のリンクは、この違いが、実際の性能に現れたケースだと思われます。
[EXTREME TECH: Valve: OpenGL is faster than DirectX — even on Windows]
ただし、全てにおいてDXよりOGLの方が速いとは限りません。DXは多数のゲームタイトルで使用され、最適化も進んでいます。加えてOGLのように太古の昔からの互換性もありません。

KMDについて

私はWindowsのドライバの中を見たことも書いたことが無いので、以下は正確性を欠いているかもしれません。
KMDはKernel Mode Driverで、その名の通り、KernelModeで動作しているものです。KMDのインスタンスはシステム上で1つのみロードされており、GPUとの直接の通信を担当します。
対してUMDはプロセスごとにロードされるため、使用しているプロセスごとに、複数個存在する前提となっています。
KMDはGPUとの唯一の窓口であるため、GPU側のメモリの管理や、Apertureと呼ばれる、GPU側から見えるHOST側のメモリの管理を行っています。
KMDはUMDの要求に応じて、GPUのメモリやApertureの領域を割り当てます。UMDはそれを、自分のリソースとして管理しているため、GPU側の、自分の保持しているリソースのアドレスを知ることが出来ます。GPU側のメモリやAperture領域の確保は、UMD-KMD間のやり取りを必要とするため、非常にコストが高いです。
従って、小さなサイズのリソースに関しては、UMDがある程度の大きさでKMDよりメモリ空間を取得し、それをUMD側で分割して割り当てていることがあります。
つまり、グラフィックスAPI上で新規にリソースを作成したからといって、直ちにKMDでGPU側のメモリ確保を行っているとは限りません。
もうひとつKMDの大きな役割として、GPU処理命令のブロック(DMAパケット)のGPU側への送出があります。DMAパケットの生成の殆どはUMD側で行いますが、実際にGPUに送る作業はKMDが行います。KMDはGPUを使用するプロセスが複数存在する場合は、そのスケジューリングも行います。スケジューリングの粒度は、このDMAパケットになります。
ひとつのDMAパケットがどの程度のGPU処理時間を使用するかは分からないので、プロセス間でGPUの競合が起きた場合の切り替わりの速度は、このDMAパケットの処理時間に依存します。
このあたりの事情はWDDMが2.xになると大きく変わると思われますが、Windows8.1でもWDDMは1.3なので、今でも大枠でこの仕組みは変わっていないと思われます。
また、ディスプレイの解像度変更など、システム全体に影響のある処理はKMDが担当しています。

話を元に戻します

OGLでDrawCallのCPU処理時間について詳細に扱ったセッションが、GTC2013でありました。
[GTC2013 Advanced Scenegraph Rendering Pipeline]

このスライドの30ページの所に注目です。2400回のDrawCallのCPU側の処理時間を比較しています。
VBOと示されるのは、通常の頂点バッファを用いたDrawCallです。これを基準として考えます。
VABは頂点フォーマットの設定をオブジェクト化してBindできるようにしたものです。OGLでは通常は、頂点フォーマットの設定は多数のGL関数の呼び出しを伴いますが、これを用いると、頂点フォーマットの設定をオブジェクト化でき、一度のAPI呼び出しで設定できるようになります。DXで言うところの、FVFからCreateVertexDeclaration()への変更といったところでしょうか。これによって処理時間は、10%ほど短縮されています。
VBUMは、頂点バッファのアドレスを、プロセス側で先に取得しておき、DrawCallの発行時には、GPU上のアドレスを直接指定する方法です。これにより、DrawCallごとに発生していた、頂点バッファのオブジェクト名からGPU上のアドレスへの変換作業が省略することができ、50%以上CPU処理時間が短縮されています。この短縮された時間には、アドレス変換だけではなく、リソースの妥当性検証部分も含まれていると思われます。なぜならば、GPU側のアドレスを直接指定するため、UMDレベルでは、リソースの妥当性検証は不可能に近いので、行われていないと考えるのが自然でだからです。
BINDLESS INDIRECT HOSTは、CPU側で、頂点バッファと頂点フォーマットに相当するアドレス情報と、インデックスバッファのアドレスを指定した、DrawCallに相当する構造体の配列を作成し、これにより複数のDrawCallを一回のAPI呼び出しで処理するものです。これを用いることで90%近い処理時間の短縮が達成されています。
つまり、通常の手順でDrawCallを呼び出す代わりに、DrawCallの使用するリソースの指定にGPU側のアドレスを直接指定し、一時的なバッファに描画命令を蓄積して、DrawCallをある程度まとめて発行すれば、DrawCallの処理が9倍早くなるのも妥当だと思われます。
BINDLESS INDIRECT GPUは、DrawCallの内容を指定した構造体配列が、GPU側のメモリに格納されているケースです。こちらは、もはやCPUの処理時間を殆ど消費しません。
このように既存のAPIである、OpenGLでも、GPUベンダーによる拡張を使用することを前提にすると、Textureなどの他のリソースも殆どがGPU側のアドレスを取得でき、描画リソースを指定する際に使用できるようになっています。これ等を積極的に用いると、DrawCallのCPU側の処理時間を数倍オーダーで高速化することは十分可能です。