ゲームのフレームペーシングいろいろ
Androidの場合
API Level 16からであればChoreographer
が使える。これはvsyncに合わせるのでvsync以外のフレームレートに合わせる必要がある場合は別の方法を考えないといけない(ANativeWindow_setFrameRate
の影響を受けるのかは調査できていない)
ちなみに、Choreographer
のコールバックはワンショットなので継続して呼んでほしい場合は再度Choreographer.postFrameCallback
を呼んでコールバックを登録してあげる必要がある。
NativeActivityの場合はネイティブラッパーであるAChoreographer
を使うか、メインループがかけるので他のプラットフォームとほぼ同じようなやり方ができる(初期のPeridotはNativeActivityだったので、メインループで全力でぶん回す実装だった)
一応こういうのもあるらしいが......おそらくC++実装なのでRustから使うのは難しそうな気がする
Mac/iOSの場合
CADisplayLink
を使う。以前は どうやらCVDisplayLink
という似たようなものがあったが、いつの間にかdeprecated?みたいな扱いになっていた。CADisplayLink
はmacOS 14.0以降じゃないといけないらしく(iOSなら3.1以上で使える)、それ以前のmacにも対応するのであればCVDisplayLink
を使う。ドキュメント上はCVDisplayLink関係の全てのメソッドがdeprecatedになっているのは変わらずだが、2024年12月時点でmacOS13(Ventura)以下を足切りするのはあまりにも力技だと思うのでdeprecatedを承知の上で強い気持ちで対応していくしかない。
CADisplayLink
は任意のフレームレート範囲を指定できるように見えるので、vsyncにあわせる他にフレームレート指定で合わせるみたいな使い方もできるのかもしれない(これも例によって未調査 フレームレート変えたい需要が現時点でなさすぎる......) ちなみにCVDisplayLink
にそういうのはない
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
の引数に指定する。
-
Windows/Xboxの入力デバイスAPIのひとつであるXInputはむしろ入力の状態を都度取得する方法しかない(はず)ので、これに関してはブロックされたとて全く影響はないといえる ↩︎
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が返ってくるよ」「実際に試したけど使えないが???」という雰囲気のやりとりがなされている)
DRM(Direct Rendering Manager)の場合
drmHandleEvent
でpage_flip_handler
を指定できるので、これで取得できる(らしい)。
生drmでの描画はまだ成功したことがないので(過去にEGLStreamsとBltで無理やり実現したことはある[1]が)実際にはまだ使ったことがない
-
これは余談だが、最近NVIDIAのプロプライエタリドライバでも
VK_EXT_external_memory_dma_buf
が使えるようになったような記憶があるので今やったらリベンジできるかもしれない ↩︎
Fence(VkFence)でvsyncを待つパターンのTips
プラットフォームごとの最適なのやり方ではないが念の為書き置き
VkFence
をプラットフォーム固有の同期プリミティブ(HEVENTとかselect可能なFile Descriptorとか)で待つ方法は残念ながら(これを書いた時点では)存在しないので、メインスレッド/レンダリングスレッドをブロックせずに待つ場合は別途待機用のスレッドを立てて待つ必要がある。
参考までにPeridotでのFenceReactorThread
の実装をおいておく
FenceReactorThread::new
内のthread_handle
のspawn
している内容が実際の監視スレッドの処理内容
スレッド外から非同期にFenceを突っ込めるようにしているためループ先頭がちょっとだけ複雑だが、それ以外は単純に「現在監視対象のFenceに対してvkGetFenceStatus
で状態を確認し、待機状態でなくなった場合は登録元のFutureをwakeする」といった実装になっている。
ここでvkWaitFences
ですべてのFenceを待つ形にしていないのは、これで待ってしまうと待っている間に別のFence監視要求が来たときに待機を解除してそっちに処理を回すことができないため。このスレッドだけで見ればビジーループみたいな感じにはなってしまうがおそらく現状のVulkanではこれ以外のやりようがない
Webブラウザの場合
Peridotではまだこのプラットフォームへの対応はないが一応
Webブラウザ向けにはrequestAnimationFrame
が使える。これは「ブラウザが再描画を行うとき」に呼び出されるコールバックを登録するものなので必ずしもvsyncと同期するわけでは無いが、おおよそvsync(ディスプレイのリフレッシュレート)に合わせて呼び出される。
なお、この関数で呼び出されるコールバックはワンショットなので、継続して何回も呼び出してもらう(ループを構成する)にはrequestAnimationFrame
のコールバックの最後で再び同じコールバックをrequestAnimationFrame
に登録する必要がある(AndroidのChoreographer
と同じ仕様)。
ちなみにこれも任意のフレームレートに合わせる事はできないので、その場合は別の方法を取る必要がある。
setTimeout
やsetInterval
を使う方法もあるが、これも例によって精度がそこまで保証されていないのでそれを許容できるかどうかはゲームジャンル次第となる。
あるいは https://blog.oimo.io/2021/06/06/adjust-fps/ こういう方法もあるらしい