Wednesday, November 2, 2016

Vertex Array Object(VAO)を使わない理由

OpenGL ES 3.xではVertex Array Object(VAO)が導入され、APIコールが減って効率的ということになっています。ところが、コーディングの特殊さによるVAOの弊害が多く、多分実行速度も変わらない気がしたので、VAOを使わなくてもいい理由を書いてみようと思いました。(ちなみに、Unreal Engine 4もVAOを使っていないようです)プラットフォームは主にAndroidを想定しています。

1. VAOが頂点バッファと結びつくのが困る


頂点バッファをシェーダに結びつけるいわゆる"Input Layout"はAPI毎に格納されるオブジェクトが違います。DX11ではID3D11InputLayout、DX12やVulkanではPSO(pipeline state object)、ES 3.xはVAOに格納されます。

そのうちVAOだけの厄介な点は特定の頂点バッファと結びつく仕様になっていることです。実務でもありそうな例として、モンスターA、B、Cを、Gバッファ生成シェーダー、シャドウマップ生成シェーダ、光学迷彩シェーダで描画するとします。頂点バッファから送るのはGバッファは全情報、シャドウマップは座標だけ送れば十分、光学迷彩はテクスチャマッピングを省くことにします。

VAOを使って必要な情報のみ必要なシェーダに送るためには、モンスター3種とシェーダー3種、3x3=9個のVAOが必要になります。さすがにこのような煩雑なことはしたくないので、実際にはモンスター毎に1つずつVAOを作って、それぞれ頂点の全情報を送るように設定し、あとはドライバレベルでの最適化に託す、という書き方になりそうです。

2. わかりにくいバグを作りやすい


経験上、描画が終わったらglBindVertexArray(0)でVAOのバインドを忘れず解除すべきです。なぜなら、バインドしっぱなしにすると他の描画モジュールがバインド中のVAOを書き換えることが出来てしまい、どこでどう地雷を踏んでいるかわからない難解なバグとなります。

描画の為のglBindBufferやglVertexAttribPointerのみならず、glBindBufferの周辺はVAOを書き換える可能性に気を使います。例えばインデックスバッファを生成したり書き換えたりする時にglBindBufferを呼びますが、これがよそのVAOを壊してしまうかもしれません。


3. VAOでAPIコールを減らしても(多分)速くならない


VAOを採用する動機は、なんとなく速そうという期待ではないでしょうか。しかし、本当に速くなるでしょうか。VAOが無いES2.0は、頂点バッファを切り替える度にglBindBufferとglVertexAttribPointerの複数回に及ぶコールで毎回”Input Layout"に該当する情報をドライバに伝えます。それが省ける点はVAOが有利に見えます。

ところが、DX12やVulkanを見るとVAOは少なくとも最近のハードウェアの実装からかけ離れている事が想像できます。バッファはコマンドリストに乗るGPUアドレスに過ぎず、Input LayoutはPSOの一部に過ぎません。この2つを取り出して1つにまとめる事そのものに最適化的な利点は期待できなさそうです。

OpenGLのドライバの実装を想像してみます。GLも内部でPSOのような物を持っているはずです。PSOの生成は重い処理なので、一回作ったらハッシュ値などで探せるようにして実体をキャッシュしておくでしょう。他のステートが決まらないとPSOが確定できないので、ハッシュ値の計算もPSOの生成もすぐには行われず、ドローコール(glDrawElements等)のタイミングで行われるはずです。

こう考えると、glVertexAttribPointerにしろ、glBindVertexArrayにしろ、ハッシュ値を計算するための元データの提供に過ぎず、バッファのバインドを除いてCPU内で完結しています。VAOを使ったほうがAPIの呼び出しは減るかもしれませんが、無理にVAOを使って得られるものがあるわけではなさそうです。

別の視点から見てみると、また、現状多くのゲームが互換性のためにES2.0で実装されています。ドライバ開発者の立場としても、VAOありきで最適化できないと思われます。

4. ES2.0がまだまだ現役、そしてVulkanの登場


従来はES2.0をベースとして上位機種はES3.x採用という戦略がありました。今後は上位機種はVulkanで互換性のためにES2.0、という組み合わせが増えると思います。対応機種を狭める上に将来的にVulkanに置き換えられる運命にある、ES3.xの必要性が薄れてきました。

現在のようにAPIが移行期にある中では特定APIに依存しないように抽象化を試みることも多いと思いますが、VAOはその特殊さゆえに抽象化がとても難しいです。ES2.0のglVertexAttribPointerも非常に変則的ですが、使うシェーダが決まるまで頂点レイアウトの決定を保留できる分、VAOよりは抽象化はしやすいと言えるかもしれません。