開発

高度なGPUパイプラインと読み込み、保存、パス

更新日時: 2026/04/22
このトピックでは、OpenGLおよびVulkanのさまざまなモバイルGPUレンダリングアーキテクチャの概要、それらを適切に設定およびプロファイリングする方法、およびこれらのアーキテクチャにおける固定中心窩レンダリング(FFR)の動作について説明します。VulkanはMeta Quest開発に必須のグラフィックAPIです。このガイドのOpenGL ESコンテンツは、履歴を参照するため、およびGPU動作に関する追加のコンテキストを提供するために維持されています。以下のセクションは、モバイルGPUレンダラーを担当するすべての人にとって重要な知識です。

概要

ラスタライズベースのGPUでは、画面上のすべての三角形が、深度バッファ内のピクセル位置における現在の値に対して深度テストされるピクセルを出力します。深度テストに合格すると、フレーム内のすべての三角形がレンダリングされるまで、新しい色と深度値を色/深度アタッチメントに繰り返し書き込みます。
つまり、各三角形の各ピクセルに対して、GPUは少なくとも1回の読み取り(深度テスト用)と、おそらく複数回の書き込み(色と深度用)を実行する必要があります。カラーブレンドの場合は、最終的な色の値を計算するために、さらに色の読み取りも必要になります。重なるジオメトリが多いほど、読み取りと書き込みの回数も増加します。

タイル式レンダラー

タイル式レンダラーは、以下を実現することによって読み取りと書き込みの速度を最適化します。
  1. 特定のピクセルは、フレーム内のほかのピクセルの値に依存しません。
  2. フレームの途中でカラーや深度の値を表示する必要はありません。
タイル型レンダラーは画面をタイルに分割し、それぞれを順番にレンダリングします。各タイルは、高速で小型のキャッシュ(gmem/tilemem)を使って読み書きを行い、最終的なピクセル値が計算されるまで処理します。最終的な値は、今後の使用のためにRAM (sysmem)に保存されます。
Meta Quest (オリジナル)、Meta Quest 2、Meta Quest 3/Meta Quest 3Sの動力源となるのは、それぞれQualcomm Snapdragon 835、Snapdragon XR2、Snapdragon XR2 Gen 2チップセットです。Snapdragon 835 (Adreno 540)とXR2 (Adreno 650)では、どちらもタイルメモリ(gmem)が1MBですが、XR2 Gen 2 (Adreno 740)では約2MBです。Meta Quest (オリジナル)は廃止され、SDKアップデートが提供されなくなっています。XR2では、マルチビューレンダリングパイプラインで1つのタイルに左目と右目の両方のビューが含まれるため、4xMSAA、32ビットカラー、24/8の深度/ステンシルバッファを実行するアプリでは、96x176のタイルが使用されます。1つのピクセルに(4色+4深度)x4-MSAA = 32バイトの情報が含まれているため、96×176×2(マルチビュー)×32 = 約1MBとなります。MSAAなどのピクセル単位の設定を変更すると、タイルサイズが変わります。これは、GPUドライバーが、タイルメモリ量の範囲に収めつつ、(画面上のタイルの総数を減らしてキャッシュを最大限に利用するために)タイルに含まれるピクセル数を最大化しようとするためです。

読み込みと保存

タイル式レンダラーのアーキテクチャについては説明したとおりです。これに基づいて、タイルごとのワークフローは次のようになります。
  1. 既存の深度と色のバッファデータをRAMからタイルメモリに読み込む。
  2. すべての三角形/フラグメントをタイルメモリにレンダリングする。
  3. 最終的な深度と色のバッファを、タイルメモリからRAMに保存する。
しかし、このワークフローは最適化されていません。ステップ1では、前のフレームのコンテンツがすべてクリアされたり、新しいフレームで上書きされたりするため、多くの場合、不要な帯域幅の転送が発生します。そこで、GPUドライバーに前フレームのコンテンツをクリアまたは無効にするよう指示することで、ステップ1を回避できます。OpenGLでは、フレームバッファをバインドした後、最初の描画呼び出しの前にglClearまたはglInvalidateFramebufferを使います。かなり明示的なシステムを持つVulkanでは、レンダーパス構成にloadOpstoreOp属性が含まれており、以下のようになります。
loadOp and storeOp attributes
QCOM GPUはクリアのコストを他の必要なセットアップ作業で隠そうとするため、ここでのクリアと無効化の差はほんのわずかですが、状況によってはこの差が大きくなることがあります。ただし、ここで上記いずれかの措置を必ず行う必要があります。クリアの回避はPCのGPUでは標準的な最適化ですが、これらのチップセット上では、GPUがフレームごとに前フレームのデータをRAMからタイルメモリに読み込む必要があるため、パフォーマンスに悪影響を及ぼします。
また、ステップ3でも、フレーム終了後に一部のアタッチメントが必要とされない場合には、不要な帯域幅の転送が発生します。例えば、VulkanにおけるMSAAのアタッチメントや、OpenGLとVulkanの両方における深度のアタッチメントは、フレーム終了後の保存が不要となる場合が非常によくあります。そして、これらのアタッチメントを無効にすることで、GPUドライバーに「これらのコンテンツはタイルメモリからRAMに保存しない(次のタイル実行時に上書きされるので必要はない)」ことを伝えることができ、非常に有用です。ここでは、OpenGLのglInvalidateFramebuffer関数と同じものを使用できますが、ステップ1とは全く異なる機能を果たすので、両方のバージョンが必要であることを必ず理解しておいてください。ステップ1でバッファを無効にするのは、レンダリング前にRAMからタイルメモリへの読み込み操作が実行されるのを避けるためであり、ステップ3では、すべてがレンダリングされた後にタイルメモリからRAMへの保存操作が実行されるのを避けるためです。Vulkanでは、storeOpレンダーパス属性にVK_ATTACHMENT_STORE_OP_DONT_CAREを指定することでこれを行います。
OpenGLでは、Vulkanのような明示的なstoreOpとフラッシング命令がないので、GPUが当該フレームバッファのコンテンツの実行を決定する前にglInvalidateFramebufferを呼び出すことが重要です。呼び出さなかった場合、無効化が考慮されなくなります。glFlushの前に無効化関数を呼び出す必要がありますが、例えば、タイマークエリを挿入すると強制的にフラッシュされるので(フラッシュしないとある時点までの操作時間を測定できないため)、無効化の前にタイマークエリ操作を挿入すると、無効化が考慮されず、深度バッファを解決することになるというのは興味深いことです。
Vulkanには明示的なMSAAアタッチメントがあり(MSAAフレームバッファがあってもテクスチャーが非MSAAであるOpenGLとは異なります)、すべてのMSAAサブサンプルをRAMに保存しないことが重要です(保存してしまうと、MSAAレベルに応じて帯域幅が線形に増加するという不要な事態が発生します)。そのため、最後のサブパスの添付pResolveAttachment属性を使用して、MSAAではないアタッチメントをバインドし、そのアタッチメントのみを保存することが重要です。下のスクリーンショットでは、4xMSAAアタッチメントにSTORE_OP_DONT_CARE属性が付き、1xMSAAアタッチメントに、STORE_OP_STORE属性が付いていることが分かります。
Comparing MSAA attributes
GPUにはMSAAの解決を行うハードウェアアクセラレーションのチップがあるので、ぜひ活用してください。

ツール

カスタムエンジンを作成している場合でも、Unityでシーンを構築している場合でも、読み込み、保存、レンダーパスの設定などを表示するツールを使って、GPUが期待どおりに動作していることを確認することが重要です。
ovrgpuprofilerは、レンダリングステージトレーシングツールで、特にこの情報を表示するために設計されています。ovrgpuprofilerは、信頼性が高くスムーズに使えるadbシェルツールで、数秒で実行できます。ステップ1ステップ3を正しく実行していない1216x1344レンダーパスの出力例をご覧ください。
Surface 1    | 1216x1344 | color 32bit, depth 24bit, stencil 8 bit, MSAA 2 | 28  320x192 bins | 10.62 ms | 171 stages :  Binning : 0.305ms LoadColor : 0.71ms Render : 2.926ms StoreColor : 1.525ms Preempt : 2.964ms LoadDepthStencil : 0.828ms StoreDepthStencil : 0.871ms
LoadColorLoadDepthStencil (合計10msのうち1.5ms)に加えて、StoreDepthStencilの時間も表示されていますが、これは大半のケースでは必要ないはずです。

マルチパス: パート1 - 別々に実行

多くのMeta Questタイトルでは、シンプルなシングルパスのフォワードレンダラーを使用しています。Meta Questハードウェアが世代を重ねて進化するにつれ、GPUの性能も大幅に向上しており、複雑なGPUレンダリングパイプラインの実行を検討する開発者が増えています。しかし、マルチパスパイプラインに取り組む際には、いくつかの重要な考慮事項があります。
OpenGLとVulkanの両方において、マルチパスを行う基本的な方法は、メイン/フォワードパスがすべてのレンダリングを行い、カラーバッファをRAMにコピーします。そのカラーバッファをテクスチャー入力として2つ目のパスにバインドし、2つ目のパスが何らかの効果(トーンマッピングなど)を適用して、最終的にコンポジター割り当て済みのスワップチェーンを生成します。これにより、RAMへの保存を含むパスが1つ追加されます(これは複雑さに依存せず、解像度/帯域幅の1つの要因にすぎません。新しいパスが単発のドローコールであることは全く問題ありません。保存操作はテクスチャーの解像度にのみ依存する固定のオーバーヘッドです)。標準的なフォワードレンダラーの視覚的品質に比べれば、特にMeta Quest 2以降のハードウェアで開発者が必要とする場合には、妥当なトレードオフと言えます。
しかし、OpenGLでは、1つの厳しい制限が課されます。FFRはテクスチャードリブンで、アプリではなくコンポジターによって適用され、コンポジタースワップチェーンへのフレームバッファレンダリング(ここでは2番目のパス)にのみ影響を与えます。そのため、開発者がFFRを有効にした場合、フラグメント負荷の高いメインパスでは中心窩形成がなされず、中心窩形成されないピクセルがRAMに保存され、その後に安価なトーンマッピングパスでそれらのピクセルを中心窩形成することになります(その過程で、苦労して得た精度が失われます)。OpenGLではこの問題に対する明確な解決策がなく、コンポジターがQCOM_texture_foveatedの呼び出しを通じてメインパスのFFR設定を駆動することはできません。しかし、Vulkanでは、RG8_UNORMの中心窩形成制御テクスチャーを通じて開発者に中心窩形成パラメーターが提供されます。このテクスチャーのコンテンツはコンポジターによって制御され、開発者は任意のレンダーパス(最終、メイン、その他)にこれをバインドすることができます。
タイル式レンダラーの役割についての説明を思い出してください。その目標は、最終的なピクセルが計算されるまでの間になされる、色と深度のバッファに対するピクセルごとの複数の読み書き操作を最適化することです。フルスクリーンエフェクトが深度バッファなし、MSAAなしでフルスクリーンクワッドをレンダリングするという具体的なケースでは、GPUは実際にはループで読み書きを行わず、唯一計算されるフラグメントが最終的なピクセルのカラーとなります。この場合、GPUコアからタイルメモリ、そしてRAMへと進むのはほとんど意味がないので、QCOM GPUはヒューリスティックを使ってその動作を検出し、GPUコアからRAMに直接進み、タイルGPUとしてではなく即時モードGPUとして動作します。これはダイレクトモードレンダリングと呼ばれ、Unityのトーンマップはこの方法で実行されています。GPUのFFRはタイルごとの効果である(解像度がタイルごとに変更される)ため、サーフェスがダイレクトモードで実行されるとFFRは無効になります。このため、2パスのOpenGL Unityレンダリングでは、たとえプロジェクト設定でFFRが有効になっていても、FFRの効果が全く得られません。
  • パス1はコンポジタースワップチェーンにレンダリングしていないのでFFRが得られません。
  • パス2は、コンポジタースワップチェーンにレンダリングしているにもかかわらず、深度バッファなしのフルスクリーンパスがダイレクトモードとして実行されて、FFRが無効になるため、FFRが得られません。
ツール側では、ダイレクトモードでレンダリングされているかどうかを判断するのは簡単です。サーフェス全体が「シングルタイル」としてレンダリングされます。
Surface 1 | 1216x1344 | color 32bit, depth 0bit, stencil 0 bit, MSAA 1 | 1 1216x1344 bins | 2.01 ms | 1 stages : Render 2.01ms

マルチパス: パート2 - サブパス理論

Vulkanでは、マルチパスレンダリングを実行するために、タイルにより適した方法としてサブパスを導入しました。ARM_framebuffer_fetchのように、OpenGL ESでの動作をシミュレートしようとする拡張機能もありますが、特にMSAAではその使用は推奨されません。
パート1では、パスを実行する「標準的な」方法について説明しました。GPUは、パス1をすべて実行してRAMに保存した後、パス1のRAMの内容を読み取ってパス2の出力をRAMに保存することで、パス2をすべて実行します。しかし、パス2のピクセルが、パス1の出力から自分のピクセル座標を検索するだけの場合には、パス間の保存と読み込みを省略し、タイルメモリ内で順次実行することで、パス2の出力をRAMに保存するだけで済みます。これは、トーンマッピングやビネットなどのフルスクリーン効果には当てはまりますが、ブルームや被写界深度には当てはまりません(これらの効果は、任意のピクセルを着色するために周囲のピクセルの値に依存するからです)。ovrgpuprofiler -t -vの出力を非常に大まかに表すと、以下のように変わります。
Surface1 (Pass1)
  render
  store
  render
  store

Surface2 (Pass2)
  render
  store
  render
  store
変更後:
Surface1
  render
  render
  store
  render
  render
  store
これがサブパスの目的です。つまり、1つのサーフェスの実行内にとどまり、タイルメモリ内の依存関係を連続的に維持します。これは、AppleのMetal APIのタイルシェーディングに相当します。これを使って、タイルメモリ内トーンマッピングレンダラーを行うことができます。サブパス0はトーンマップ前のカラーバッファを出力し、サブパス1がこれを(タイルメモリ内に)読み込んでトーンマッピングを行い、その結果をコンポジタースワップチェーンに保存します。ここでサブパス0の出力は、サブパス1への入力アタッチメントと呼ばれます。この場合、トーンマップ前のカラーバッファはRAMに保存されないため、パフォーマンスに大きなメリットがあり、トーンマップパスは入力アタッチメントをRAMからではなく、高速タイルメモリから読み込むことになります。
Unreal Engine 4.25以降(UE5を含む)では、サブパスを使用して、半透明シェーダーに不透明パスの深度を読み取るオプションを与えています。このエンジンでは不透明なオブジェクトと半透明なオブジェクトを2つのサブパスに分けてレンダリングします。まず不透明なオブジェクトがレンダリングされ、次に不透明なサブパスの深度バッファが入力アタッチメントとして半透明なサブパスにバインドされ、半透明オブジェクトがピクセルシェーダーでそこから読み込んで安価な深度ベースのエフェクトを実現します。これにより、動的に有効または無効にすることができるカスタムのサブパスベースのトーンマッパーを開発することができます。

マルチパス: パート3 - サブパスを非常に遅くする例

ここで認識しておく必要があるのは、設定したサブパスを順次実行するのではなく別々のパスとしてGPUドライバーに強制実行させることにより、理論上の性能向上をすべて台無しにしてしまう設定が数多くあるということです。場合によっては、実際の速度がかなり遅くなってしまうこともあります。開発者が遭遇する3つの大きな落とし穴には次のものがあります。
MSAAから非MSAAへの解決のためにpResolveAttachmentエントリーがある中間の(最終ではない)サブパス
GPUのHWアクセラレーションによるMSAA解決チップは、タイルメモリからRAMへの保存パイプラインに統合されています。中間MSAAサブパスにpResolveAttachment属性を使ってコンテンツを非MSAAとして解決するように指示すると、そのサブパスはコンテンツを強制的にRAMに保存します(通常のレンダーパスの場合と同じです)。その次のサブパスも別のパスとして実行しなければならず、通常はタイルメモリ内にある入力アタッチメントをすべてRAMから読み込み直すことになります。
この場合、次のサブパスに、subpassLoad(input, sample) GLSL関数を使って未解決のMSAA入力アタッチメントを読み込ませ、サブサンプルごとに1回呼び出して、手動でMSAA解決を行う必要があります。なお、MSAAの設定によっては、パフォーマンスに影響することがあるので注意が必要です。Adreno 540 (Meta Quest)およびAdreno 650 (Meta Quest 2)のGPUでは、サブパスは2つの入力アタッチメントサブサンプルを並行して読み取れますが、4つのサブサンプルを並行処理することはできません。したがって、2xMSAAシェーダーの読み取りは「自由」ですが、4xMSAAシェーダーの読み取りは実際には自由ではありません。Adreno 740 (Meta Quest 3/Meta Quest 3S)は、異なる並行読み取り特性に対応している可能性があります。デバイス固有の機能については、Qualcomm Adreno 740のドキュメントをご覧ください。
保存操作アタッチメントを使用した中間サブパス
サブパスで重要なことは、最後のサブパスだけがその内容をRAMに保存するようにすることです。すべてのアタッチメントに目を通し、最後のサブパスのアタッチメント(理想的には非MSAAカラーアタッチまたは解決アタッチメント)だけが、STORE_OP_STORE属性を持つようにすることを忘れないでください。これらの属性は、レンダーパスの実行終了時にRAMに保存したいものだけを対象とし、サブパス間の依存関係は対象外です。MSAAカラーバッファがサブパス0とサブパス1の間で有効であり続けなければならない場合、保存操作の必要はありません。
過度に保守的なpSubpassDependencies
Vulkanでは、サブパス間の依存関係を定義し、並列実行できるものとできないものを区別できなければなりません。例えば、サブパスの依存関係としてdstAccessMaskVK_ACCESS_SHADER_READ_BITを指定すると、GPUドライバーに「サブパス0の出力は、サブパス1によりディスクリプターセットを使ってシェーダーで読めるようにする必要がある」と指示することになります。これはごく普通のことに見えますが、実際にはサブパスモードを破壊しています。サブパス1がテクスチャーサンプラーを使ってサブパス0の出力を読み取れる場合、サブパス0の出力のすべてのテクセルを読めることになり、全く別のタイルのものも読めてしまいます。そうなると、サブパス0とサブパス1はタイルメモリ内で連続実行されなくなり、別々のレンダーパスとして実行されます。ここでの正しいマスクはVK_ACCESS_INPUT_ATTACHMENT_READ_BITです。入力アタッチメントの読み取り関数subpassLoadにUVパラメーターがないため、依存関係は強制的に同じピクセルにのみ存在することになります。
このようなコードを書く開発者は、ovrgpuprofilerなどのレンダーステージトレーシングツールを使ってレンダラーをプロファイリングして、レンダービンに注目し、連続で実行されているか、読み込みと保存操作が異なるサーフェスで実行されているかを確認する必要があります。これは、パフォーマンスを確認する唯一の方法です。
ナビゲーションロゴ
日本語
© 2026 Meta