Unity を検索

Async Upload Pipeline(AUP)でローディングのパフォーマンスを最適化する

2018年10月8日 カテゴリ: テクノロジー | 7 分 で読めます
取り上げているトピック
シェア

ローディング画面が好きという人は誰もいないでしょう。ロード時間は Async Upload Pipeline(AUP)パラメーターを簡単に調節するだけで大幅に改善できることをご存知でしたか?本記事では、AUP によるメッシュとテクスチャをローディングする仕組みを詳しくご紹介します。ロード時間を大幅に短縮する上で、この仕組みを理解しておくと役に立つと思います。これによって実際にパフォーマンスが 2 倍改善されたプロジェクトもあります。

AUP が機能する技術的な仕組みや、その効果を最大限に引き出すために使う API を知りたい方は、ぜひ以下を読み進めてください。

AUP をお試しください

最適化された最新版の AUP の実装は Unity 2018.3 ベータ版でご使用いただけます。

 

Unity 2018.3 ベータ版をダウンロード

 

まず最初に、AUP がどんな場合に使用されるか、およびローディング処理のプロセスについて、詳しくご紹介します。

AUP はどんな場合に使用されるか

バージョン 2018.3 以前は AUP はテクスチャのみを扱っていました。2018.3 ベータ版からはテクスチャとメッシュがロード可能になりましたが、一部例外もあります。読み込み/書き込み有効のテクスチャとメッシュ、および Compressed(圧縮)メッシュには AUP は使用できません。([注]バージョン 2018.2 で追加された Texture Mipmap Streaming も AUP を使用します。)

ローディング処理の流れ

ビルド処理中、テクスチャやメッシュオブジェクトはシリアライズされたファイルに書き込まれ、大きなバイナリデータ(テクスチャあるいは頂点データ)は付随の .resS ファイルに書き込まれます。このレイアウトはプレイヤーデータとアセットバンドルの両方に当てはまります。オブジェクトとバイナリデータを分けることにより(通常、小さなオブジェクトが入る)シリアライズされたファイルのより高速な読み込みが可能になり、大きなバイナリデータは .resS から効率的に読み込めるようになります。テクスチャオブジェクトやメッシュオブジェクトは、デシリアライズされた時に AUP のコマンドキューへコマンドをサブミットします。コマンドが完了した時点で、そのテクスチャあるいはメッシュデータは GPU へのアップロードされており、オブジェクトがメインスレッドに統合可能になっています。

図表:ビルド用にシリアライズされたメッシュおよびテクスチャデータのレイアウト

アップロード処理中、.resS ファイルからの大きなバイナリデータは固定サイズのリングバッファに読み込まれます。一旦メモリに入ったデータはレンダースレッド上でタイムスライス方式で GPU にアップロードされます。システムの挙動を変えるために変更できる 2 つのパラメーターは、リングバッファと、タイムスライスの長さです。

AUP では各コマンドごとに以下の処理が行われます。

  1. 必要な量のメモリがリングバッファ内で使用可能になるまで待機する
  2. ソースの .resS ファイルから、割り当てられたメモリ内にデータを読み込む
  3. ポストプロセッシング(テクスチャ解凍、メッシュコリジョン生成、プラットフォームごとの調整など)を実行する
  4. タイムスライス方式でレンダースレッドにアップロードする
  5. リングバッファメモリを解放する

複数のコマンドが同時に進行可能ですが、すべてのコマンドが 1 つの共有されたリングバッファにそれぞれ必要なメモリを割り当てる必要があります。リングバッファが一杯になると、新しいコマンドは待機することになります。この待機は非同期読み込み処理の速度を遅くしますが、メインスレッドのブロックを引き起こしたりフレームレートに影響を及ぼしたりすることはありません。

以下は、それぞれの特徴を簡単に整理したものです。

読み込みパイプラインの比較
AUP なしAUP効果
メモリ使用データをデフォルトヒープから読み出しながら割り当てる(ハイウォーターマーク)固定サイズのリングバッファハイウォーターマークの削減
アップロード処理データが使用可能になると同時にアップロード固定タイムスライスで少しずつアップロードヒッチのないアップロード
ポストプロセッシング読み込みスレッドで実行(読み込みスレッドがブロックされる)バックグラウンドのジョブで実行高速なローディング

ローディングパラメーターの調整に使用できるパブリック API

バージョン 2018.3 で AUP を最大限に活用するために、ランタイムで調整できる 3 つのパラメーターがあります。

  • QualitySettings.asyncUploadTimeSlice ― 各フレームで、レンダースレッドでテクスチャおよびメッシュデータの読み込みに使用される時間(ミリ秒単位)です。非同期ロード処理の進行中は、このサイズのタイムスライスを 2 つ実行します。デフォルトの値は 2 ms(2 ミリ秒)です。この値が小さすぎると、テクスチャやメッシュの GPU アップロードが遅くなる場合があります。逆に値が大きすぎるとフレームレートのヒッチが起きることがあります。
  • QualitySettings.asyncUploadBufferSize ― リングバッファのサイズ(メガバイト単位)です。各フレームでアップロードのタイムスライスが発生した場合に、タイムスライス全体を活用するための十分なデータ量がリングバッファ内に必要です。リングバッファが小さ過ぎると、アップロードタイムスライスが短縮されます。デフォルトは、Unity 2018.2 では 4MB でしたが Unity 2018.3 では 16MB に増加されました。
  • QualitySettings.asyncUploadPersistentBuffer ― Unity 2018.3 で搭載されるこのフラグは、すべての保留された読み出しが完了した時にアップロードリングバッファを解放するかどうかを設定します。このバッファの割り当てと解放はしばしばメモリの断片化を引き起こすことがあるので、通常はデフォルト(true)のままにしておくことが推奨されます。読み込み中以外でのメモリの再割り当てがどうしても必要な場合は、この値を false に設定することも可能です。

これらの設定はスクリプティング API を使うか、または Quality Settings(品質設定)メニューから調整可能です。

ワークフローの例

大量のテクスチャとメッシュを、AUP でデフォルトのタイムスライス(2 ms)およびリングバッファ(4 MB)でアップロードする場合の負荷を見てみましょう。読み込みが行われているので、1 レンダーフレームごとに 2 回のタイムスライスが入るため、アップロード時間は 4 ms 取られるはずです。しかし、プロファイラーのデータを見ると、約 1.5 ms しか使われていません。また、リングバッファ内でメモリが使用可能になっているので、アップロード直後に新しい読み出し処理が発行されているのも確認できます。このことから、より大きなリングバッファが必要であることが分かります。

試しにリングバッファを増加させてみましょう。ローディング画面なので、アップロードタイムスライスも増加させた方が良いでしょう。リングバッファを 16 MB、タイムスライスを 4 ms とすると、以下のようになります。

今度は、ほぼすべてのレンダースレッドの時間がアップロードに使用されており、アップロードとアップロードの間のわずかな時間がレンダリングに使用されていることが分かります。

以下は、この負荷サンプルでアップロードタイムスライスとリングバッファサイズの設定を変えた場合の、それぞれのロード時間を比較したグラフです。テストは MacBook Pro(2.8GHz Intel Core i7 搭載、OS X El Capitan)で実行されました。アップロード速度と入出力速度はプラットフォームやデバイスによって異なります。負荷サンプルとして使用したのは、Unity 社内でパフォーマンステストに使用されるサンプルプロジェクト『Viking Village』のサブセットです。他に読み込まれているオブジェクトがあるので、値を変更したときにどの程度パフォーマンスが改善したかは正確に確認できません。しかし、このサンプルの場合、4 MB・2 ms から 16 MB・4 ms に変更すると、テクスチャとメッシュの読み込みが少なくとも 2 倍以上速くなることは確実です。

各パラメーターを変更してみると、以下のような結果になります。

したがって、このサンプルプロジェクトでロード時間を最適化できる設定は以下のようになります。

QualitySettings.asyncUploadTimeSlice = 4
QualitySettings.asyncUploadBufferSize = 16
QualitySettings.asyncUploadPersistentBuffer = true

まとめとヒント

テクスチャとメッシュの読み込み速度を最適化するときの推奨事項は下記のとおりです。

  • フレーム落ちを発生させない範囲で、最も大きい QualitySettings.asyncUploadTimeSlice を設定する。
  • ローディング画面中、一時的に QualitySettings.asyncUploadTimeSlice を増加させる。
  • プロファイラーでタイムスライスの使用状況を調査する。タイムスライスはプロファイラー上で AsyncUploadManager.AsyncResourceUpload として表示される。タイムスライスがフル活用されていない場合は QualitySettings.asyncUploadBufferSize を増加させる。
  • 基本的に QualitySettings.asyncUploadBufferSize が大きいほうが速度が上がるので、メモリが十分にある場合は 16 MB から 32 MB に増加させる。
  • ローディング中以外にどうしてもランタイム使用メモリを削減しなければならない特別な理由がなければ、QualitySettings.asyncUploadPersistentBuffer の設定を true のままにする。

よくある質問

Q: タイムスライスされたアップロードがレンダースレッドで起こる頻度はどの程度ですか?

  • タイムスライスされたアップロードは、レンダーフレームごとに 1 回、あるいは 1 回の非同期ロード処理中に 2 回発生します。VSync はこのパイプラインに影響を及ぼします。レンダースレッドが VSync を待機している間にアップロードを行えます。各フレーム 16 ms で実行していて、あるフレームが長く(例えば 17 ms に)なった場合、Vsync の待機時間は 15 ms になります。基本的に、フレームレートが高いほどアップロードタイムスライスの発生頻度が高くなります。

Q: AUP 経由で読み込まれるものは何ですか?

  • 読み込み/書き込み有効でないテクスチャが AUP でアップロードされます。
  • バージョン 2018.2 の時点では、テクスチャミップマップが AUP 経由でストリーミングされます。
  • バージョン 2018.3 では、メッシュも(非圧縮で、かつ読み込み/書き込み有効でない場合のみ)AUP 経由でアップロードされます。

Q: 非常に大きなテクスチャをアップロードする場合など、アップロードされるデータに対して、リングバッファの大きさが足りない場合はどうなりますか?

  • リングバッファより大きなアップロードコマンドは、リングバッファが完全に消費されるまで待機します。その後、必要な大きさに合わせてリングバッファが再度割り当てされます。アップロード完了後、リングバッファが元のサイズに再度割り当てられます。

Q: 同期ロード API(Resources.Load, AssetBundle.LoadAsset など)はどのように機能しますか?

  • 同期ローディングコールは AUP を使用し、非同期アップロード処理が完了するまで実質的にメインスレッドをブロックします。使用される読み込み API の種類は関係ありません。

ご意見をお寄せください

皆様のフィードバックをお待ちしています。本ページのコメント欄か Unity 2018.3 ベータ版のフォーラムから、ぜひご意見をお寄せください!

2018年10月8日 カテゴリ: テクノロジー | 7 分 で読めます
取り上げているトピック