Open7

ゲームのフレームペーシングいろいろ

S.PercentageS.Percentage

Androidの場合

API Level 16からであればChoreographerが使える。これはvsyncに合わせるのでvsync以外のフレームレートに合わせる必要がある場合は別の方法を考えないといけない(ANativeWindow_setFrameRateの影響を受けるのかは調査できていない)

ちなみに、Choreographerのコールバックはワンショットなので継続して呼んでほしい場合は再度Choreographer.postFrameCallbackを呼んでコールバックを登録してあげる必要がある。

NativeActivityの場合はネイティブラッパーであるAChoreographerを使うか、メインループがかけるので他のプラットフォームとほぼ同じようなやり方ができる(初期のPeridotはNativeActivityだったので、メインループで全力でぶん回す実装だった)

https://developer.android.com/games/sdk/frame-pacing?hl=ja
一応こういうのもあるらしいが......おそらくC++実装なのでRustから使うのは難しそうな気がする

S.PercentageS.Percentage

Mac/iOSの場合

CADisplayLinkを使う。以前はCVDisplayLinkという似たようなものがあったが、いつの間にかdeprecated?みたいな扱いになっていた。 どうやらCADisplayLinkはmacOS 14.0以降じゃないといけないらしく(iOSなら3.1以上で使える)、それ以前のmacにも対応するのであればCVDisplayLinkを使う。ドキュメント上はCVDisplayLink関係の全てのメソッドがdeprecatedになっているのは変わらずだが、2024年12月時点でmacOS13(Ventura)以下を足切りするのはあまりにも力技だと思うのでdeprecatedを承知の上で強い気持ちで対応していくしかない。

CADisplayLinkは任意のフレームレート範囲を指定できるように見えるので、vsyncにあわせる他にフレームレート指定で合わせるみたいな使い方もできるのかもしれない(これも例によって未調査 フレームレート変えたい需要が現時点でなさすぎる......) ちなみにCVDisplayLinkにそういうのはない

S.PercentageS.Percentage

Windowsの場合

少なくともWin32APIとしては垂直同期を取るAPIはないので(なんで?)自前でなんとかする必要がある。

メッセージがないときに全力でぶん回す

いちばん簡単な方法。スワップチェーンのPresentMode次第では次のvsyncまでブロックしてくれる(DX12/Vulkanであれば同期プリミティブ(Fenceなど)で待てる)のでこの方法でも一応vsyncに合わせるみたいなことはできる。ただし基本的には休憩無しでCPUを回すので電力消費的にはあまりよろしくないのと、ブロックする場合はその間そのスレッドは動かないので、レンダリングスレッドを分けるみたいなことをしていない場合はイベントハンドリングなども止まってレスポンスが悪くなる可能性がある。

とはいえレスポンスに関しては、更新フレーム内で都度入力デバイスの状態を取る形の実装も大抵のプラットフォームでは可能[1]なのであまり困ることはないかもしれない(Peridotで今進めているinput-system-v2はプラットフォームの生イベントをほぼそのままユーザーコードに流す実装にしようとしているので、そうなるとブロックされるのは困るようになる)

タイムアウトで待つ

これはMsgWaitForMultipleObjectなどのメッセージ待機関数に指定できるタイムアウト値を使って、イベントを処理しつつ待つ方法。
この手の関数のタイムアウトはあまり精度が良くない( https://learn.microsoft.com/ja-jp/windows/win32/sync/wait-functions#wait-functions-and-time-out-intervals )ため、タイムアウトを全面的に信用するのはよくないかも(タイムアウトはちょっと短めにして、残りの超短時間をビジーループで高精度に待つなどといった工夫が必要)。

タイムアウトの精度をあげるにはMultimedia Class Scheduler ServiceのAPIでスレッドが特定のタスクを受け持っていることを宣言する。以前はMultimedia TimerのAPIでtimeBeginPeriodとかを使って制御していたのがWindows10/11からこれに変わった形。
ゲームであればGamesがすべてのWindowsに最初から入っているので、これをAvSetMmThreadCharacteristicsの引数に指定する。

脚注
  1. Windows/Xboxの入力デバイスAPIのひとつであるXInputはむしろ入力の状態を都度取得する方法しかない(はず)ので、これに関してはブロックされたとて全く影響はないといえる ↩︎

S.PercentageS.Percentage

Waylandの場合

おそらく存在しない

一応tearing-control-v1というのはあるが、これは説明読む感じグラフィックスAPIがWSIの実装として使うのを想定しているっぽくてそれとの併用が不可能なように見える

あとはpresentation-time(wp_presentation/wp_presentation_feedback)もそれっぽい拡張に見えるが、これはおそらくwl_surface.commitに対する表示結果のフィードバック機構に見えるのでゲーム用途にも使えるのかは未調査(ただしフィードバック内容に「次のrefreshまでのナノ秒」みたいな項目があるので、これを使うことで正しく待機できるのかも?)

なので、現時点では全力でループを回すかしかなさそう

ちなみに、VulkanにはFenceのFile Descriptorを取得する拡張VK_KHR_external_fence_fdがあって、これを使うとepollとかでポーリングできそうなFDを取得してWindows/DXGIなどと同じように賢く待機できるように見えるが、実際にはSYNC_FDを取得してもepollは制御を返さない( https://github.com/KhronosGroup/Vulkan-Docs/issues/310 このあたりでも「SYNC_FDはepollで使えるFDが返ってくるよ」「実際に試したけど使えないが???」という雰囲気のやりとりがなされている)

S.PercentageS.Percentage

DRM(Direct Rendering Manager)の場合

drmHandleEventpage_flip_handlerを指定できるので、これで取得できる(らしい)。
生drmでの描画はまだ成功したことがないので(過去にEGLStreamsとBltで無理やり実現したことはある[1]が)実際にはまだ使ったことがない

脚注
  1. これは余談だが、最近NVIDIAのプロプライエタリドライバでもVK_EXT_external_memory_dma_bufが使えるようになったような記憶があるので今やったらリベンジできるかもしれない ↩︎

S.PercentageS.Percentage

Fence(VkFence)でvsyncを待つパターンのTips

プラットフォームごとの最適なのやり方ではないが念の為書き置き

VkFenceをプラットフォーム固有の同期プリミティブ(HEVENTとかselect可能なFile Descriptorとか)で待つ方法は残念ながら(これを書いた時点では)存在しないので、メインスレッド/レンダリングスレッドをブロックせずに待つ場合は別途待機用のスレッドを立てて待つ必要がある。

参考までにPeridotでのFenceReactorThreadの実装をおいておく
https://github.com/Pctg-x8/peridot/blob/9315313b9d948558b335e1620d9ac26c2b71784f/base/src/graphics/async_fence_driver.rs#L16
FenceReactorThread::new内のthread_handlespawnしている内容が実際の監視スレッドの処理内容
スレッド外から非同期にFenceを突っ込めるようにしているためループ先頭がちょっとだけ複雑だが、それ以外は単純に「現在監視対象のFenceに対してvkGetFenceStatusで状態を確認し、待機状態でなくなった場合は登録元のFutureをwakeする」といった実装になっている。

ここでvkWaitFencesですべてのFenceを待つ形にしていないのは、これで待ってしまうと待っている間に別のFence監視要求が来たときに待機を解除してそっちに処理を回すことができないため。このスレッドだけで見ればビジーループみたいな感じにはなってしまうがおそらく現状のVulkanではこれ以外のやりようがない

S.PercentageS.Percentage

Webブラウザの場合

Peridotではまだこのプラットフォームへの対応はないが一応

Webブラウザ向けにはrequestAnimationFrameが使える。これは「ブラウザが再描画を行うとき」に呼び出されるコールバックを登録するものなので必ずしもvsyncと同期するわけでは無いが、おおよそvsync(ディスプレイのリフレッシュレート)に合わせて呼び出される。

なお、この関数で呼び出されるコールバックはワンショットなので、継続して何回も呼び出してもらう(ループを構成する)にはrequestAnimationFrameのコールバックの最後で再び同じコールバックをrequestAnimationFrameに登録する必要がある(AndroidのChoreographerと同じ仕様)。

ちなみにこれも任意のフレームレートに合わせる事はできないので、その場合は別の方法を取る必要がある。
setTimeoutsetIntervalを使う方法もあるが、これも例によって精度がそこまで保証されていないのでそれを許容できるかどうかはゲームジャンル次第となる。
あるいは https://blog.oimo.io/2021/06/06/adjust-fps/ こういう方法もあるらしい