Yuniframe: デスクトップアプリ作成の準備
とりあえずWebGL1相当でSchemeから絵を出せるレベルにはなったので、これを使ってゲームのレベルエディタとかを作りたい。ただ、今はEmscriptenのプログラムを動かすのに必要十分しか実装していないので、機能を相当に追加してやる必要がある。
必要そうな機能
ぱっと思いつくものとしては:
-
フレームレート制限機構 。Emscriptenを想定してフレームワークを作っていたので、
requestAnimationFrame
に相当する機能が外部に必要になっている。できればスクリプト側で書き分けしないで実現できる方式を考えたいところ。。モード分けするしかないかなぁ。。 - リサイズ可能なWindow 。リサイズイベントがあったらRender bufferを作りなおす とかはアプリ側にやらせても良いと思う。2枚以上Windowを開く機能は今のところいらない(電話をサポートできないので)。
- 非同期タスクキュー 。SDLのイベントスレッドはmain threadでしか待合せられない(macOSの制限)ので、他のI/Oフレームワーク(= ネットワーク関連)の待合せは別のスレッドでやる必要がある。Scheme処理系側にはスレッドが無い可能性があるので、Yuniframe側に機能性として用意してあげるのが妥当かな。
- IME統合 。これはSDL2にあるけどメンテとかを考えると真面目に取り込むのはSDL3に上げてからかな。。
... クリップボード と ドラッグアンドドロップ を忘れてた。。ファイル保存ダイアログとかも該当する。これらもめっちゃ難しい。(特にPC以外で)
描画モード
アプリケーション自体はEmscriptenの作法で書く。つまり、 SDLイベントキューの待合せは常に非blockとなる 。コールバックを呼び出すロジック自体はSchemeのライブラリに隠してしまうことにする。
ユーザーが記述するべきはイベント処理コールバック手続き(以後 描画コールバック )で、それをライブラリに預ける形になる。ライブラリは、プラットフォームがWebかネイティブアプリかを判別し、適切なコールバック手法を選択する。
アプリケーションは "描画モード" を持つ:
- アニメーションモード はゲームのように常時アニメーションが更新される状態を想定している。
- イベントモード はイベント待ちのあと描画する。
2つのモードの違いは、単に "何もイベントが無いときに描画コールバックを呼ぶか" でしかない。ブラウザ上では、
- アニメーションモードのときは
requestAnimationFrame
を常に実施する - イベントモードのときは、関心のあるDOMイベントが発生したら初めて
requestAnimationFrame
する
という挙動差になる。描画モードは単なるグローバル変数で、描画コールバックを抜けたタイミングで評価される。
フレームペーシング問題
デスクトップ上で requestAnimationFrame
に相当する機能を実現する方法は自明ではない。
OpenGL ES + EGLは自身のコンテキストがダブルバッファなのかトリプルバッファなのか より多いバッファを持っているのか についても隠蔽しているので、 VSYNCを有効にしているからといって自由に描画して良いわけではない 。さもないと必要以上の描画遅延を発生させることになり、お絵描きソフトでは致命的になる。
AndroidのFrame pacingライブラリはこの問題をよく抽象化している
Frame pacingは:
- 描画スレッドで
eglCreateSyncKHR
してfence syncを挿入 - 普通に
eglSwapBuffers
する - 別のスレッドで
eglClientWaitSyncKHR
してフレーム時間を計測するとともに、フレーム完了を観測
(1 と 3 は別々のスレッドで行われるが、EGLはOpenGLのようなクライアントAPIと異なりスレッドセーフ性が保証されている。)
この方法はVulkanやMetal上のANGLEでも可能なはずだが、DirectX上のANGLEには無い。DirectX上のANGLEでは直接swapchainを処理する必要がある(多分 -- どっちにせよdesktop compositorとの統合が必要になる)。
... もっとも、最悪のケース(ソフトウェアレンダリングしていてVNC描画するみたいなケース)ではタイマ以外に方法が無いので最初はタイマで実装する。つまり、
- アニメーションモードでは1/60秒に一度タイマイベントを発火させることでフレームを進める
- イベントモードのときはタイマを止める
そもそも VSYNC 待ちに簡単な方法は無い
伝統的にLinux等のフリーUNIXはその辺にあまり興味を持たれてこなかったので、VSYNC待ちを行う簡単な方法はプラットフォームになかった。Vulkanの拡張には VK_GOOGLE_display_timing
等相当する機能性がある。
低遅延のためのGPUソリューション
Windowサイズの問題
フルスクリーン固定サイズで良いゲームとは異なり、デスクトップアプリではWindowサイズの制御が難しい問題となる。
フレームペーシング同様、こちらも簡単な解は無いので、一旦:
- 初期化時のフラグでリサイズ可能性を指定する
- Resizedイベントを送信してアプリケーションに描画領域をリサイズさせる
という方向性で。。
High DPI
macOSではスクリーン上のピクセルとWindow内のピクセルのサイズが異なる。
2560x1440 を選んでいるように見えるが、実際の(HDMI的な)出力解像度は3840x2160で、それぞれ1.5倍になっている。
サイズ変更
SDL2とSDL3ではこの辺の挙動が異なっている:
- https://wiki.libsdl.org/SDL2/SDL_SetWindowSize - SDL2はリクエストはその場で実行される
-
https://wiki.libsdl.org/SDL3/SDL_SetWindowSize - SDL3はリクエストは非同期で実行され、
SDL_EVENT_WINDOW_RESIZED
イベントで完了が通知される
さらに、SDL2には サイズ変更中はイベントループが回らないという構造的な問題 があり、超長いディスカッションの末:
アプリケーションのイベントループ(のhook)を外部で書けるようにする方向 https://wiki.libsdl.org/SDL3/SDL_MAIN_USE_CALLBACKS で解決が図られた。
ユーザーによるリサイズ要求のハンドリングも難しい問題となる: WindowsではWindowサイズの変更そのものをhookしてコールバック中にリサイズのリクエストを書き換えることで固定アスペクト比やN倍等を強制できる。逆に、Webではどうしようもない(アプリケーションが自発的にリサイズできるのはfull screenのみ)。
部分アップデート
画面のごく一部のみアップデートするようなケース(画面上のメーターとか)でメモリ転送量を減らすことは消費電力的に重要と言える。が、これがなかなか難しい。。
EGLでは EGL_EXT_swap_buffers_with_damage
EGL_KHR_partial_update
と EGL_EXT_buffer_age
の3つが関連する。このうち Androidで必須とされているのは EGL_EXT_swap_buffers_with_damage
のみで、 EGL_KHR_partial_update
は推奨、 EGL_EXT_buffer_age
は言及なしとなっている。
スーパーややこしいことに、これらの拡張は守備範囲が異なる。 EGL_EXT_swap_buffers_with_damage
はコンポジタにヒントを与える。あくまでヒントなので、他の拡張を組合せないかぎりアプリケーションは全体を描画する必要がある。 EGL_KHR_partial_update
や EGL_EXT_buffer_age
は通常利用が忌避されるバックバッファの内容preserve挙動を拡張し、過去に描画した内容を再利用できるようにする。
Android以外のプラットフォームにはこういう考察は無い気がする。 Appleは多分自社のUIフレームワークではやっているが、Metalはゲームやビジュアライズ用と割り切っているんだろう。