フレーム ペーシングを適切に行う

Swappy という名前でも知られる Android Frame Pacing ライブラリは、Android Game SDK の一部です。このライブラリは、OpenGL と Vulkan のゲームが Android でスムーズなレンダリングと正確なフレーム ペーシングを行えるようにサポートします。このドキュメントでは、フレーム ペーシングの定義を示し、フレーム ペーシングが必要になる状況について説明し、ライブラリがそのような状況に対処する仕組みを紹介します。フレーム ペーシングをゲームに直接実装する場合は、次のステップをご覧ください。

背景

「フレーム ペーシング」とは、ゲームのロジックおよびレンダリング ループを、OS のディスプレイ サブシステムおよび基盤となるディスプレイ ハードウェアと同期させることです。Android ディスプレイ サブシステムは、更新中にディスプレイ ハードウェアが新しいフレームに切り替える際に発生することがある視覚的アーティファクト(テアリングと呼ばれます)を回避するように設計されています。こうしたアーティファクトを回避するため、ディスプレイ サブシステムは次のことを行います。

  • 過去のフレームを内部的にバッファリングする
  • 遅延しているフレーム送信を検出する
  • 遅延しているフレームが検出されたときは過去のフレームの表示を繰り返す

ゲームは(eglSwapBuffers または vkQueuePresentKHR を呼び出すことによって)フレームに必要なすべての描画呼び出しを送信したことを、ディスプレイ サブシステム内のコンポジターである SurfaceFlinger に知らせます。SurfaceFlinger は、ラッチを使用してディスプレイ ハードウェアに利用できるフレームを知らせます。ディスプレイ ハードウェアは、指定されたフレームを表示します。ディスプレイ ハードウェアは一定のレート(60 Hz など)でティックし、ハードウェアがフレームを必要とするときに新しいフレームがない場合は、前のフレームを再度表示します。

ゲームのレンダリング ループがネイティブ ディスプレイ ハードウェアと異なるレートでレンダリングを行うと、フレーム時間の不整合が生じることがよくあります。30 FPS で実行されるゲームがネイティブに 60 FPS をサポートするデバイスでレンダリングを実行しようとした場合、ゲームのレンダリング ループは、繰り返されるフレームが 16 ミリ秒間余計に画面上に表示される動作を実現しません。この切断により、通常はフレーム時間に大きな不整合が生じます(49 ミリ秒、16 ミリ秒、33 ミリ秒など)。極度に複雑なシーンはフレームの欠落を生じさせるため、この問題をさらに複雑にします。

最適ではないソリューション

以前からあるゲームではフレーム ペーシングについて以下のソリューションが採用されており、フレーム時間の不整合と入力レイテンシの増加の原因になっています。

レンダリング API に可能な最大限の速さでフレームを送信する

このアプローチでは、ゲームを可変的な SurfaceFlinger アクティビティに結び付け、追加の遅延フレームを導入します。ディスプレイ パイプラインには、通常サイズ 2 のフレームのキューが含まれています。このキューは、ゲームが過度の速さでフレームを表示しようとすると一杯になります。キューに空きがない場合、ゲームループは(または少なくともレンダリング スレッドは)OpenGL または Vulkan の呼び出しによってブロックされます。その後、ゲームはディスプレイ ハードウェアがフレームを表示するまで待たされ、このバック プレッシャーにより 2 つのコンポーネントが同期されます。この状況は「バッファの詰め込み」または「キューの詰め込み」と呼ばれます。レンダラ プロセスは何が起こっているかを認識しないので、フレームレートの不整合が悪化します。ゲームがフレームの前に入力をサンプリングする場合、入力レイテンシも悪化します。

Android Choreographer を単独で使用する

ゲームは同期を行うために Android Choreographer も使用します。このコンポーネントは、Java では API 16 以降、C++ では API 24 以降で利用することができ、ディスプレイ サブシステムと同じ周波数で定期的なティックを配信します。このティックの配信が実際のハードウェア VSYNC とどれくらいずれているかは微妙な問題であり、そのオフセットはデバイスによって異なります。長いフレームでもバッファの詰め込みが発生することがあります。

Frame Pacing ライブラリの利点

Frame Pacing ライブラリは、同期に Android Choreographer を使用してティック配信の変動性に対処します。このライブラリは表示タイムスタンプを使用してフレームが適切なタイミングで表示されることを保証し、フェンスを同期してバッファの詰め込みを回避します。NDK Choreographer が利用できる場合はそれを使用し、利用できない場合は Java Choreographer にフォールバックします。

デバイスで複数のリフレッシュ レートがサポートされている場合は、ライブラリでそれらが処理されるため、ゲームはより柔軟にフレームを表示できます。たとえば、60 Hz と 90 Hz のリフレッシュ レートをサポートするデバイスの場合、60 FPS(フレーム毎秒)のレートを達成できないゲームは、スムーズな表示を維持するために 30 FPS ではなく 45 FPS にレートを下げることができます。ライブラリは、期待されるゲームのフレームレートを検出し、それに応じてフレームの表示時間を自動調整します。

仕組み

以下のセクションでは、Frame Pacing ライブラリが正確なフレーム ペーシングを達成するために長いゲームフレームと短いゲームフレームを処理する仕組みについて説明します。

30 Hz における正確なフレーム ペーシング

60 Hz デバイスにおいて 30 Hz でレンダリングする場合の Android の理想的な状況を図 1 に示します。SurfaceFlinger は、新しいグラフィック バッファがある場合はそれをラッチします(図中の NB は「バッファなし」を表し、前のバッファの表示が繰り返されることを示します)。

60 Hz デバイスにおける 30 Hz での理想的なフレーム ペーシング

図 1. 60 Hz デバイスにおける 30 Hz での理想的なフレーム ペーシング

短いゲームフレームがスタッタリングを発生させる

最近のほとんどのデバイスでは、ゲームエンジンはティックを配信するプラットフォームのコリオグラファを頼りにフレームの送信を制御します。ただし、図 2 に示すように、短いフレームが原因でフレーム ペーシングが不正確になることもあります。短いフレームの後に長いフレームが続くと、プレーヤーはスタッタリングが生じていると感じます。

短いゲームフレーム

図 2. 短いゲームフレーム C が原因でフレーム B が 1 つのフレームのみに表示され、その後複数のフレーム C が表示される

Frame Pacing ライブラリは、表示タイムスタンプを使用してこの問題を解決します。ライブラリは、表示タイムスタンプ拡張 EGL_ANDROID_presentation_time および VK_GOOGLE_display_timing を使用して、図 3 に示すように、フレームの早すぎる表示を防ぎます。

表示タイムスタンプ

図 3. スムーズに見えるようにゲームフレーム B が 2 回表示される

長いフレームがスタッタリングとレイテンシを発生させる

ディスプレイ ワークロードがアプリ ワークロードより長くかかると、追加フレームがキューに投入されます。これもまたスタッタリングを発生させ、バッファの詰め込みが原因で追加の遅延フレームを発生させる可能性があります(図 4 を参照)。ライブラリは、スタッタリングと追加の遅延フレームの両方を取り除きます。

長いゲームフレーム

図 4. 長いフレーム B が 2 つのフレーム(A と B)で不正確なペーシングを生じさせる

ライブラリはこれを解決するために、同期フェンス(EGL_KHR_fence_syncVkFence)を使用して、バック プレッシャーを増大させるのではなく、ディスプレイ パイプラインが追いつけるように待機をアプリに注入します。図 5 に示すように、フレーム A はやはり追加フレームを表示しますが、フレーム B は正しく表示されるようになります。

アプリレイヤに待機が追加される

図 5. フレーム C とフレーム D が表示を待機する

サポートされている動作モード

Frame Pacing ライブラリは、次の 3 つのモードで動作するように構成できます。

  • 自動モードオフ + パイプライン
  • 自動モードオン + パイプライン
  • 自動モードオン + 自動パイプライン モード(パイプライン / 非パイプライン)

自動モード + パイプライン モードを試すこともできますが、最初はそれらを無効にして、Swappy を初期化した後に以下を使用することをおすすめします。

  swappyAutoSwapInterval(false);
  swappyAutoPipelineMode(false);
  swappyEnableStats(false);
  swappySwapIntervalNS(1000000000L/yourPreferredFrameRateInHz);

パイプライン モード

通常、ライブラリはエンジンのワークロードを調整するために、VSYNC 境界を越えて CPU と GPU のワークロードを分離するパイプライン モデルを使用します。

パイプライン モード

図 6. パイプライン モード

非パイプライン モード

一般的に、このアプローチを採用すると、入力画面のレイテンシが短くなり、予測しやすくなります。ゲームのフレーム時間が非常に短ければ、CPU と GPU の両方のワークロードが単一のスワップ間隔に収まる可能性があります。このようなケースでは、非パイプライン モードのアプローチを使用すると、実際に入力画面のレイテンシが短くなります。

非パイプライン モード

図 7. 非パイプライン モード

自動モード

ほとんどのゲームは、スワップ間隔を選択する手段を備えていません。スワップ間隔とは、個々のフレームが表示される期間(たとえば、30 Hz の場合は 33.3 ミリ秒)です。ゲームは、一部のデバイスでは 60 FPS でレンダリングする一方で、別のデバイスではレートをもっと低い値に下げる場合があります。自動モードでは、以下を行うために CPU 時間と GPU 時間が測定されます。

  • スワップ間隔を自動的に選択する: 一部のシーンでは 30 Hz、別のシーンでは 60 Hz のレートを提供するゲームでは、ライブラリがこの間隔を動的に調整できます。
  • 超高速フレームのパイプライン処理を無効にする: あらゆる場合に最適な入力画面レイテンシを提供します。

複数のリフレッシュ レート

複数のリフレッシュ レートをサポートするデバイスでは、次のように、スムーズに見えるスワップ間隔をより柔軟に選択できます。

  • 60 Hz デバイスの場合: 60 FPS / 30 FPS / 20 FPS
  • 60 Hz + 90 Hz デバイスの場合: 90 FPS / 60 FPS / 45 FPS / 30 FPS
  • 60 Hz + 90 Hz + 120 Hz デバイスの場合: 120 FPS / 90 FPS / 60 FPS / 45 FPS / 40 FPS / 30 FPS

ライブラリは、ゲームフレームの実際のレンダリング時間に最も適したリフレッシュ レートを選択します。それにより、視覚的なエクスペリエンスが向上します。

複数のリフレッシュ レートのフレーム ペーシングの詳細については、ブログ投稿 Android での高リフレッシュ レートのレンダリングをご覧ください。

フレーム統計情報

Frame Pacing ライブラリは、デバッグとプロファイリング用に次の統計情報を提供します。

  • レンダリングの完了後にコンポジター キューでフレームが待機した画面リフレッシュ回数のヒストグラム。
  • リクエストされた表示時間から実際の表示時間までに経た画面リフレッシュ回数のヒストグラム。
  • 2 つの連続するフレームの間に経た画面リフレッシュ回数のヒストグラム。
  • このフレームの CPU 処理の開始時間から実際の表示時間までに経た画面リフレッシュ回数のヒストグラム。

次のステップ

Android Frame Pacing ライブラリをゲームに統合する方法については、以下のいずれかのガイドをご覧ください。