🤔

OpenGLとMetal/Vulkanってなにがちがうの?

2021/12/27に公開

5年前に書いとけや、って感じの記事を若干うろ覚えのまま書いてみます。主に初心者向けの記事です。実装面での話ではなくほぼ歴史の話になります。

Graphics API

OpenGLの話をしよう。

OpenGL

そもそも、OpenGLの黎明期は次のようなハードウェアーキテクチャが想定されていた。

要点としては次のとおり

  • 汎用計算(CPU)と描画演算(GPU)の処理部が物理的に離れている
  • しかも非同期通信
  • Graphics API(要するにOpenGL)は非同期を前提としたAPIである
    • というかならざるを得ない
  • OpenGLは良くも悪くもデバイスに依存しない最大公約数的なAPIである

容易に想像がつくように パフォーマンスがよろしくない

  • テクスチャと頂点(=めっちゃデータ量多い)がメインメモリとVRAMで二重管理される
  • 転送時間が問題になる(物理的に離れている上に同期的でない)

Unity等の描画において、draw callがボトルネックと呼ばれていた理由がこれでわかる。

OpenGL ES

OpenGL ESとはモバイルデバイス用のOpenGLサブセットである。API的にはサブセットだが、大きな違いがある。 ハードウェアーキテクチャが全然違う

モバイルデバイス(要するにスマホ)はその構造上、描画処理部分が物理的に離れていない。良くも悪くも1つの基盤に全部載せである。

この構造をしていると先述したパフォーマンスを改善できそうだ。ただ、OpenGL自体が古いAPIのままだと苦しい。GPU側がどれだけ頑張っても、OpenGL ESで ラップされている ため最大公約数的・非同期前提・メモリ転送という非効率さはなくなっていない。

Low-level Graphics API

そこで、MetalやVulkanである、もっというとDirectXも該当する。

モバイルデバイスのハードウェアーキテクチャを前提として

  • 転送オーバーヘッドなし
  • 最新のGPU(あるいはGraphics Driver)のパワーを直接引き出す

を行う。当然ながら、OSとGraphics Driverが対応している必要はある。我々は過去を忘れて未来に生きることにしたのだ。

GPU Native

Unityに触った始めの頃に誰もが思うことがある。「この画像フォーマットなに?」。

OpenGLの時代

アセットバンドルを使いつつ適当なテクスチャを表示させることを考えてみる。パフォーマンスを考えないと次のような順序でテクスチャが描画される。

大変そうだ。明らかに無駄っぽい感じに見えるのが

  • RGBA32(32bit × 縦サイズ × 横サイズ)という巨大データを展開する必要がある
    • CPUにもメモリにも優しくなさそう
  • テクスチャデータとGPUのキャッシュ戦略が噛み合っていない
    • 古典的な画像データはただ単にピクセルを1列に並べたものだが、GPUは画像には縦横があるものだと思っている
    • 気づいたかもしれない、これは行列演算(2次元配列)の最適化の話に似ている

Metalの時代

こうすると良さそうだ。

awesome! 無駄がないし美しい。単純計算を並列に行うのはGPUの得意とするところだ。

  • メモリ使用効率が圧倒的に良い
  • データ転送によるオーバーヘッドをなくす
  • CPUによるデコードのオーバーヘッドをなくす
  • GPUのキャッシングと並列計算を最大効率で利用する

このようなテクスチャを GPU native という。Unityが聞き慣れない画像フォーマットを使うのはこのためだ。

テクスチャの話をしたが頂点データも同様にオーバーヘッドを最小化できる。かつてdraw callがボトルネックだったのは転送によるオーバーヘッドが大きかったからだ。今、Unityでボトルネックというとset pass callが話題になることのほうが多いと思う。これはなんだろう?

GPUは

  • 256コアなら256並列計算できる
  • さて、並列計算時にコンテキストが同じならキャッシュも共有できるはずだ、実際できる
  • シェーダにおいてコンテキストとはなんだろうか、次の2つだ
    • プログラム(shader pass)
    • 変数(テクスチャを含むuniform変数)
  • この2つをまとめたものを我々は知っている、これはUnityのマテリアルだ
    • ちなみに頂点データは共有される変数ではなく入力の変数として扱う
    • 1コアは1スレッドを処理する、頂点シェーダなら1つの頂点あたり1スレッド、コンテキストを共有しているもの(つまりマテリアルが同じもの)をまとめて1つのスレッドプール、コンテキストが切り替わるときに別のスレッドプールをつくる、とこういう風に考えると良い
    • この話ではbatch renderingという単語が全く出てこないことに気づいただろうか?batch renderingは転送オーバーヘッドを 減らす ためのテクニックだから、昨今のGraphics APIだとこれをdynamicに行うのは逆にオーバーヘッドが大きくなりやすい(静的には当然まだ有用である、ただ昔に比べればチューニングの優先度は下がる)。

マテリアルが変わるとset pass callが増えるというのはこういう理屈になる。

ミラティブ Engineering

Discussion