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

最近のOpenGLにおける効率的なDrawCallを考える

過去数回にわたり、OpenGL4.4と幾つかのARB拡張の機能について勉強してきました。
これを用いて、効率的なDrawCallを考えてみたいと思います。

効率的なDrawCallとは?

ここで議論する効率的なDrawCallとは、一言で言えば、多数のDrawCallの呼び出しがあっても、CPU負荷が少なく、それによるCPU側の処理時間が少ないものを指します。GPUの絶対的な性能を向上させるものではありません。
しかし近年は、GPUのプリミティブの処理速度が高速になっていく一方で、CPU側から効率的にDrawCallを発行する方法がありませんでした。結果として、特にDX9などのAPIでは、CPUが描画コマンドを作成する部分に、レンダリング全体のボトルネックが発生することも少なくありません。(以下これを、DrawCallボトルネックと呼ぶ)
効率的なDrawCallを確立することが出来れば、パフォーマンスの向上だけでなく、現在はアーティスト作業の重荷になっている、オブジェクトのマージ作業や、テクスチャのアトラスの作成といった作業を軽減することが可能になるかもしれません。

効率的なDrawCallを考える

前提とする条件

OpenGL4.x
GL_ARB_shader_draw_parameters
GL_ARB_bindless_texture
を用いる環境と仮定します。

ModelViewProjectionマトリクスについて

DrawCallの呼び出しにおいて、ModelViewProjectionマトリクスを用意することは、ほぼ常識として考えられてきました。
しかし、近年のGPUの性能を鑑みると、マトリクス同士の乗算によって、消費されるALUの演算リソースよりも、メモリの入出力による遅延の方が、影響が大きい場合もあるようです。
ここで、Modelマトリクスと、ViewProjectionマトリクスを、VertexShader上で乗算するように変更すると、1フレームのレンダリング中に、ひとつのオブジェクトに対するModelマトリクスの更新は1度行えばよく、ShadowMapやDepth-Prepass, G-Bufferといった、各パスの描画のたびにマトリクスを演算し、UniformBufferを更新する作業を減らすことが出来ます。
一方で、GPU側のVertexShaderの計算量は増えますが、DrawCallボトルネックの状態の場合、確実にCPUの処理とAPIの呼び出し回数を軽減し、パフォーマンス向上につながることになります。

Modelマトリクスをどこに格納するか

Modelマトリクスを格納する場所として考えられるのは、UniformBufferObject(以下UBO)かShaderStorageBufferObject(以下SSBO)です。
BufferObjectひとつに対して、UBOが最低スペックの場合16KBまでしか確保できないのに対して、SSBOは最低でも16MBまでの領域が確保可能です。SSBOをModelマトリクスの格納場所として使用すれば、1フレーム分のDrawCallのマトリクスの格納場所として、十分な領域が確保可能と思われます。

DrawCallに対して配列のインデックスを渡す方法

上記のように、Modelマトリクスを単一のBufferObjectに配列で格納した場合、各DrawCallに対して、適切に配列のインデックスを通知する必要があります。
配列のインデックスを通知する方法として最も簡単な方法のひとつとして、BaseInstanceを用いる方法が挙げられると思います。

glDrawElementsInstancedBaseInstance(GL_TRIANGLES,
                                    nbPrim,
                                    GL_UNSIGNED_SHORT,
                                    NULL,
                                    1,
                                    (GLuint)arrayIndex);
#version 440
#extension GL_ARB_shader_draw_parameters : require
in int gl_BaseInstanceARB;

void main()
{
  int did = gl_BaseInstanceARB;
  mat4 World = Transforms[did];
  vec4 worldPos = World * vec4(In.v3Pos, 1);
  gl_Position = ViewProjection * worldPos;
}

上記の例では、インスタンス描画用のAPIを用いていますが、インスタンス数は1で描画しますので、実際にはインスタンス描画ではありません。ここで、BaseInstanceに配列のインデックスを渡すことで、VertexShader上で、配列のインデックスを得ることが出来ます。
この方法では、頂点配列をマージする必要もないので、簡単に試すことが出来ます。DrawCallのパフォーマンスを向上させたい場合の、最初の一手として試す価値があるかもしれません。
一点、短所として挙げられるのは、この方法は、本当のインスタンス描画とシェーダーを共有することが難しいということです。
インスタンス描画で、divisorを用いた頂点配列を用いたケースでは、BaseInstanceは0、もしくは適切な値を設定する必要があります。ただし、一般的にインスタンス描画を行う場合は、専用のシェーダーを用意するケースが多いと思われるので、この短所はあまり致命的ではないと思われます。

配列に格納可能な他の要素

DrawCallに関連する、他の要素として、各種Shaderのパラメーターや、Textureなどが考えられます。もし、これ等を配列に格納することが出来れば、上記と同様の手法でShaderからアクセスすることが可能になります。これによって、従来では、DrawCallごとに頻繁に発生していた、BufferObjectの更新や、各種Stateの変更を避けることが可能になります。
Textureを配列に格納する方法は、幾つか考えられますが、最も自由度が高いのが、BindlessTextureを用いる方法です。これを用いれば、各Textureの解像度やチャンネル数、フォーマットなどが異なった場合でも、同一の配列に格納可能です。
Shaderに関連するパラメーターは、各Shaderによって異なることが考えられますが、最大公約数的な構造体を作成することで、同一の構造体配列を、複数のShaderで扱うことが可能だと思われます。
これ等を、SSBOバッファに用意した配列に格納することで、Modelマトリクスの例と同様の手法を適用することが可能となります。
下記の例では、Bindlessテクスチャを用いて、PixelShader上でTexture配列にアクセスする例です。PixelShaderでは、VertexShaderより配列のインデックスを取得することになります。

#version 440
#extension GL_ARB_separate_shader_objects : require
#extension GL_ARB_bindless_texture : require

layout (std430, binding = 0) readonly buffer CB1
{
  uint64_t texHandle[];
};

layout(location=0) in struct {
  vec2 v2TexCoord;
  flat int iDrawID;
} In;

layout(location=0) out struct {
  vec4 v4Color;
} Out;

void main()
{
  Out.v4Color = vec4(texture(sampler2D(texAddress[In.iDrawID]),
                             In.v2TexCoord).xyz,  1);
}

MultiDraw系を用いる

MultiDraw系のAPIを用いることで、複数のDrawCallをマージし、1回のAPIの呼び出しにまとめることが出来ます。
ただし、マージするための条件として、複数のDrawCallが、同一のシェーダーで描画され、さらに同一の頂点フォーマットであるという制限があります。もちろんですが、描画中にRenderStateの変更などは出来ません。これ等の制限に適合するオブジェクトが多数あるケースでは、MultiDraw系を用いることで、さらにDrawCallの呼び出しを最適化することが出来ると思われます。
注意点として、MultiDraw系を用いる場合は、上記で述べた、BaseInstanceによる配列インデックスの指定とDrawIDを混同しないようにする必要があります。MultiDraw系のAPIで描画された際に、何番目のDrawCallであるかを知るには、gl_DrawIDARBにアクセスしますが、BaseInstanceによる配列インデックスを用いている場合は、この値はあまり意味がありません。代わりに、描画コマンド構造体の、baseInstanceに適切な値を設定する必要があります。

in int gl_DrawIDARB;
in int gl_BaseInstanceARB;
for (int i = 0; i < count; ++i)
{
  DrawElementsIndirectCommand *cmd = &commands[i];
  cmd->count = nbPrims[i];
  cmd->instanceCount = 1;
  cmd->firstIndex = 0;
  cmd->baseVertex = 0;
  cmd->baseInstance = arrayIdcs[i];
}

//注:Indirect系の描画コマンド配列は、CoreProfileの場合はDRAW_INDIRECT_BUFFERを通じて指定する必要があります。
glMultiDrawElementsIndirect(GL_TRIANGLES, GL_UNSIGNED_SHORT, commands, count, 0);

まとめ。考察

 まず、今回は、実際にこれ等を実践することで得られる、パフォーマンスの向上について検証していません。できれば、実際のゲームのシーンで、従来のDrawCallの手順と、上記を実践した場合に得られるパフォーマンスの差を検証してみたいのですが、簡単に手に入るアセットでは、なかなか適当なものがありません。
たとえ実際のゲームのシーンを入手できても、既にテクスチャアトラスが作られ、マテリアルなどがマージされ、DrawCallの数自体を抑えてあるものでは、上記を実践しても効果が薄いかもしれません。
 また、今回割愛しましたが、同一解像度の、非常に多数のTextureを使用する場合は、BindlessによるSSBO配列よりも、Texture2DArrayを用いた方が好ましいと思われます。理由は、Bindlessを使用した場合では、用いていない場合に比べ、TextureHandle1つあたり、32Bitレジスタ2本が消費されます。3つのTextureを参照すれば、6本となり、これは無視できない数字となります。また、Bindlessテクスチャは、基本的には、個々のOpenGLのTextureObjectなので、各々が独立したSamplerStateを保持しています。つまり、GPUはSamplerStateの切り替えがあると前提します。一方でTexture2DArrayで、別のSliceにアクセスしても、同一のTextureUnitが別のメモリにアクセスするだけです。特に、P-texやパーティクルなどで、プリミティブごとに異なるTextureにアクセスするようなケースでは、Texture2DArrayを用いることを検討する必要があると思います。

OpenGLのTextureについて

OpenGLのTexture周りの機能について、改めて勉強したいと思います。

TextureObjectとSamplerObjectについて

OpenGLのTextureObjectには、Textureのフォーマット情報とデータの実体に加えて、サンプリング時に使用される、Samplerの設定が格納されます。これに対して、SamplerObjectに格納されるのは、Samplerの情報のみが格納されます。
TextureObjectとSamplerObjectは、同時に、同じTextureUnitにBindすることが出来ます。この場合は、SamplerObjectのSamplerの設定が使用されます。
SamplerObjectに0をBindすることで、SamplerObjectをUnBindできます。この場合は、TextureObjectに設定されたSamplerの設定が使用されます。

SmaplerObjectに格納されるのは、

GL_TEXTURE_WRAP_S
GL_TEXTURE_WRAP_T
GL_TEXTURE_WRAP_R
GL_TEXTURE_MIN_FILTER
GL_TEXTURE_MAG_FILTER
GL_TEXTURE_BORDER_COLOR
GL_TEXTURE_MIN_LOD
GL_TEXTURE_MAX_LOD
GL_TEXTURE_LOD_BIAS
GL_TEXTURE_COMPARE_MODE
GL_TEXTURE_COMPARE_FUNC

に関する情報です。

また、TextureObjectでは、同様のパラメーターに関しては

glTexParameter*();

を用いて設定していましたが、SampleObjectでは、

glSamplerParameter*()

を用いて設定します。APIが異なるので、注意が必要です。

ARB_bindless_texture拡張について

従来のOpenGLでは、ShaderからTextureを参照するには、TextureObjectをTextureUnitにBindする必要がありました。TextureUnitの数には限りがあり、一般的なGPUでは16個程度のTextureUnitにTextureをBindして利用していました。
一方で、BindlessTextureの機能を用いた場合、ShaderへTextureObjectのHandleを、Uniformや頂点Attributeなどの何らかの方法で伝え、Shader側はそのHandleよりTextureを参照します。従って、BindlessTextureの場合は、同時参照できるTextureの数に明示的な上限は存在しないのが大きな特徴です。

Bindless用のHandleの取得

Bindless用のHandleは、従来のTextureObjectのIDとして使用されていた、GLuint型ではなく、GLuint64型となります。取得には下記のAPIを使います。

GLuint64 glGetTextureHandleARB(GLuint texture);
GLuint64 glGetTextureSamplerHandleARB(GLuint texture, GLuint sampler);

glGetTextureHandleARB()は、単に指定されたTextureObjectのHandleを返します。
glGetTextureSamplerHandleARB()は、Samplerに関する指定に、引数samplerで指定されたSamplerObjectを利用します。
glGetTextureSamplerHandleARB()で参照されたSamplerObjectは、各ステートの変更が出来なくなるので、注意が必要です。

Residentの指定

Handleを取得後、実際に、Shader内でHandleを通じてTextureにアクセスするには、そのTextureがResident(GPUのシェーダーユニットよりアクセス可能なメモリに配置されている状態)である必要があります。
これを指定するAPIが以下になります。

GLvoid glMakeTextureHandleResidentARB(GLuint64 handle);
GLvoid glMakeTextureHandleNonResidentARB(GLuint64 handle);
GLboolean glIsTextureHandleResidentARB(GLuint64 handle);

glMakeTextureHandleResidentARB()でTextureObjectをResidentに指定し、glMakeTextureHandleNonResidentARB()で、その指定を解除します。
glIsTextureHandleResidentARB()で、現在のResident指定の状態を取得できます。
glMakeTextureHandleResidentARB()のAPIは、現在のところ、メモリ不足によるResident指定の失敗を定義していません。しかし、Resident指定は、実質的にTextureObjectをGPU側のメモリに固定することになると思います。従って、GPUメモリの使用量に関しては注意が必要と思われます。

また、

GLboolean glAreTexturesResident(GLsizei n, const GLuint *textures, GLboolean *residences );

というAPIがOpenGLのCompatibility Profileに存在しますが、BindlessTextureと直接の関係はありません。

Shaderからの参照方法

ShaderからBindlessTextureを参照するには、とにかく64bit型のHandleの値を入手すれば良い訳です。その手段は問われません。
ただし、今のところ最も一般的な手法としては、GL_SHADER_STORAGE_BUFFERに、Handleの配列を格納しておく手法があげられると思います。
Handleの配列を用意しておくと、Textureのインデックス値を何らかの方法でShaderに渡すだけで、Shaderは、この配列を通じでTextureを参照することが出来ます。
UniformBufferObjectでも同様の手法を取ることができます。しかし、GL_SHADER_STORAGE_BUFFERの方が、ランダムアクセスを想定されており、サイズの上限も大きいです。
ひとつ、注意点として挙げられるのが、BindlessTextureへアクセスする際にはHandleの値が必要で、これを格納するために、Shader内で通常の演算などにも利用されるレジスターが消費されるということです。しかも64Bitの値なので、1枚のTextureへのHandleを保持するだけで、2つのレジスターを消費することになります。レジスター消費に関しては常に意識しながらShaderを書く必要があります。

glGenBuffers(1, &buf);
glBindBuffer(GL_SHADER_STORAGE_BUFFER, buf);
glBufferData(GL_SHADER_STORAGE_BUFFER, TEX_COUNT * sizeof(GLuint64), handleArray, GL_STATIC_DRAW);
glBindBuffer(GL_SHADER_STORAGE_BUFFER, 0);

glBindBufferBase(GL_SHADER_STORAGE_BUFFER, 0, buf);
#extension GL_ARB_bindless_texture : require

layout (std430, binding = 0) readonly
buffer CB0
{
uint64_t texHandles[];
};

void main()
{
Out.v4Color = texture(sampler2D(texHandles[In.DrawID]), In.v2TexCoord));
}

考察

SamplerObjectとBindlessTextureの機能について見てみました。
OepnGLのSamplerObjectには、現在のところ、Shader内で明示的にアクセスして、TextureとSamplerをそれぞれ組み合わせて使う機能が提供されていません。
また、現状のSamplerObjectは、一度BindlessTextureのHandleに参照されると、値の変更が出来なくなります。これは、実際には、TextureObjectにSamplerの状態が管理されていることを意味するのかも知れません。
この点はDX11と異なる点です。OpenGLではTextureObjectがSamplerに関する情報を保持している限り、この制限は無くならないかもしれません。

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