【iOS】Metal Best Practicesの解説(8) フレームレート
Metal Best Practicesは、iOS/MacOS/tvOSのAPIであるMetalを用いた設計のベストプラクティスガイドです。
本稿では、何回かに分けてこのガイドを読み解き、コード上での実験を交えて解説していきます。
読んでそのまま理解できそうなところは飛ばしますので、原文を読みながら原文のガイドとしてご利用下さい。
また、iOSの記事なので他のOS(MacOS, tvOS)についての記載は割愛します。
他の記事の一覧は、初回記事よりご覧下さい。
Frame Rate (iOS and tvOS)(フレームレート)
ベストプラクティス:一貫した安定したフレームレートでドローアブルを表示します。
アプリは一定したフレームレートを維持できるようにしましょう、という話。フレームレートが一定でない場合Jitterと呼ばれる現象が起きます。Jitterは遅延の変動のことで、これが起きると画面のモーションにカクつきが出るためユーザーに不快な印象を与えます。シューティングゲームでこれがあると操作しづらいでしょうね😅
アプリが、60FPSを維持できるような処理速度であれば何も問題はありません。ただ、それが難しいときはフレームレートを下げる必要があります。
iOSのフレームレートはUIScreenのmaximumFramesPerSecondプロパティで参照でき、通常60FPSです。
MTKViewは、フレームレートの調整がしやすくなっていて、preferredFramesPerSecondプロパティを使うことで、指定したFPSにすることができます。
ドローアブルの表示時間の固定
preferredFramesPerSecondに30FPSなど低いフレームレートを指定して、アプリのレンダリングループを遅くしても、ディスプレイのリフレッシュレートは60FPSのままなので、タイミングによってはアプリの処理が60FPSの間隔(16.67ms)に間に合ってしまうことがります。これによりフレームレートが一定せずJitterが起きます。
そこで、presentDrawableのafterMinimumDuration引数を指定すると、ドローアブルの最小表示時間を指定できます。この引数に、1.0/preferredFramesPerSecondを指定すると、preferredFramesPerSecondのフレームレートにあわせたドロアーブルの表示間隔を保ってくれるので、アプリのレンダリングループとドローアブルの表示間隔が同期し、Jitterの問題を回避できます。
コードで検証してみる
何も指定しない場合と、フレームレートを固定した場合、ドロアーブルの表示間隔をフレームレートにあわせた場合をコードで比較してみます。
サンプルコードはこちらにあるコードを改変して作ります。
実行すると、パーティクルが上から下に流れてきます。
(実行イメージ)
検証のためにパーティクルの数を10万に増やします。
class Coordinator : NSObject, MTKViewDelegate {
static let numberOfParticles = 100000
そのまま実行した場合
サンプルコードをそのまま実行してみます。
FPSは46〜48FPSの間で変動していました。パーティクルの流れは滑らかさに欠ける感じがあります。
(スクリーンキャプチャしてGIFアニメするとGIFのカクつきの方が大きかったので、キャプチャは省略します)
Metal System Traceを使って実行中の状態を取得してみます。
このツールの使い方はこちらの記事をご覧ください。
(実行中の状態)
このコードは、パーティクルを動かす処理をCPU側でやっているので、遅延の原因の殆どがCPUの処理によるものです。
ディスプレイのリフレッシュ間隔が49msになったり、16msになったりと一定していないようです。平均すると21ms(=46FPS)ぐらいです。
MTKViewDelegateのdrawメソッドは内部タイマーに基づいて呼び出されますが、呼び出しの間隔は21ms〜28msの間で変化していました。
60FPSに間に合っていても間に合わなくても、準備ができたらすぐに描画、といった感じでしょうか。
フレームレートを固定した場合
MTKViewのpreferredFramesPerSecondを使ってフレームレートを30FPSに固定してみます。
次のようにコードを変更します。
func makeUIView(context: Context) -> MTKView {
// 〜中略〜
mtkView.drawableSize = mtkView.frame.size
mtkView.colorPixelFormat = .bgra8Unorm_srgb
mtkView.preferredFramesPerSecond = 30 // 👈これを追加
return mtkView
}
実行するとFPSは30で一定のままになりました。パーティクルのカクつきはほとんどなくなったように見えます。
(実行中の状態)
drawメソッドはの呼び出しの間隔も33msで一定でしたが、たまに処理がディスプレイのリフレッシュ間隔(16.67ms)に間に合うことがあるようで、その場合は16ms間隔でリフレッシュされ、その後33msに戻ったりと一定のリフレッシュ間隔にはなっていません。
この結果からの推測ですが、preferredFramesPerSecondは、おそらくMTKViewDelegateのdrawメソッドの更新間隔を決定しているものと思われます。preferredFramesPerSecondで30FPSを指定すると、33msの間隔でdrawメソッドは呼ばれますが、ディスプレイのリフレッシュレートは60FSPのままなので、タイミングによっては、16msの間隔で画面が更新されるようです。
フレームレートを固定 + ドロアーブルの表示間隔を固定した場合
フレームレートの固定に加え、ドローアブルの表示間隔も固定してみます。
func draw(in view: MTKView) {
renderEncoder.drawPrimitives(type: .point, vertexStart: 0, vertexCount: Coordinator.numberOfParticles)
renderEncoder.endEncoding()
// 👇引数afterMinimumDurationを追加
commandBuffer.present(drawable, afterMinimumDuration: 1.0/Double(parent.mtkView.preferredFramesPerSecond))
commandBuffer.addCompletedHandler {[weak self] _ in
self?.semaphore.signal()
}
commandBuffer.commit()
}
}
こちらも実行すると30FPSで一定になります。パーティクルのカクつきは全くなく、とても滑らかになったように見えます。(目視なので気のせいかもしれませんが。。)
(実行中の状態)
drawメソッドの呼び出しの間隔は33msで一定しており、ディスプレイのリフレッシュ間隔も33msで一定したまま続いているのがわかります。
結論
今回は、アプリが60FPSを守れない場合の対策、のような感じになりましたが、30FPSなどフレームレートを下げても滑らかな動きを実現するためには、preferredFramesPerSecondでフレームレートを固定し、presentDrawableのafterMinimumDurationでドローアブルの表示間隔を同期させる必要がありそうです。
最後に
iOSを使った3D処理やAR、ML、音声処理などの作品やサンプル、技術情報を発信しています。
作品ができたらTwitterで発信していきますのでフォローをお願いします🙏
Twitterは作品や記事のリンクを貼っています。
https://twitter.com/jugemjugemjugem
Qiitaは、iOS開発、とくにARや機械学習、グラフィックス処理、音声処理について発信しています。
https://qiita.com/TokyoYoshida
Noteでは、連載記事を書いています。
https://note.com/tokyoyoshida
Zennは機械学習が多めです。
https://zenn.dev/tokyoyoshida
Author And Source
この問題について(【iOS】Metal Best Practicesの解説(8) フレームレート), 我々は、より多くの情報をここで見つけました https://qiita.com/TokyoYoshida/items/493194825460c3b1d6b2著者帰属:元の著者の情報は、元のURLに含まれています。著作権は原作者に属する。
Content is automatically searched and collected through network algorithms . If there is a violation . Please contact us . We will adjust (correct author information ,or delete content ) as soon as possible .