凹みTips

C++、JavaScript、Unity、ガジェット等の Tips について雑多に書いています。

Unity で Windows のウィンドウを個別にキャプチャできる uWindowCapture の高速化をしてみた

はじめに

前回の記事で Windows Graphics Capture の解説を行いました。

tips.hecomi.com

記事中でも触れたのですが、調査の目的としては OVR Toolkit などにも使われている拙作の uWindowCapture にこの API を導入するためでした。従来の設計に合わせるのに少し時間がかかりましたが、ようやく安定して動くようになったので先程公開しました。これにより、従来のキャプチャの簡便性を保ちながら複数枚の 4K ウィンドウやデスクトップを簡単に切り替えながらキャプチャできるようになりました!本記事ではアップデートの解説と苦労話を少し書こうと思います。

uWindowCapture の基本については以前の記事をご参照ください。

tips.hecomi.com

tips.hecomi.com

デモ

ダウンロード / インストール

以下のページから最新バージョンをダウンロードし、ご自身のプロジェクトへ展開してください。

github.com

(2021/05/02 に安定版の v1.0.1 をリリースしましたので記事公開時の v1.0.0 を利用されている方はアップデートをお願いします)

Windows Graphics Capture のサポート

ユーザ側が意識する点は以下の Capture Mode のみです。従来の BitBltPrintWindow によるキャプチャ方式に加えて、Windows Graphics CaptureAuto が追加されました。

f:id:hecomi:20210430214038p:plain

Windows Graphics Capture

Windows Graphics Capture 方式でキャプチャを行います。キャプチャが開始されるとキャプチャされているウィンドウの周りに黄色い枠が現れます。キャプチャ方式を切り替えたり、Capture Request Timing が Only When Visible で画面外にウィンドウが出てしまった場合などキャプチャされない状態になると、自動で Windows Graphics Capture のセッションはクローズされ黄色い枠が消えます。

Auto

子ウィンドウなど種類によっては Windows Graphics Capture が使えないものがあります。これらに対しては従来の Print Window または BitBlt を行い、そうでないものには Windows Graphics Capture を使うというモードです。OS が対応していないバージョンの場合も旧 API へフォールバックされるので、こちらをデフォルトで使うことを推奨します。

パフォーマンス

例えば下記動画はデスクトップの配置を模擬して並べたシーンのものです。Youtube を再生した Chrome など、すべてのウィンドウがおおよそ 60 fps でキャプチャされています。

計測すると GTX 1080 環境で、キャプチャ用スレッドで Windows Graphics Capture でキャプチャされた 4K テクスチャを保持用の共有テクスチャにコピーするのが 300 us 程度、Unity のレンダリングスレッドでこの共有テクスチャを Unity のテクスチャへコピーするのが 100 us 程度です。並列で処理されるので 1 枚のキャプチャは 300 us 程度となるため、数十枚くらいキャプチャしても 60 fps で動くと思われます。

その他注意

Windows Graphics Capture モードの使用可否について

マニュアルでチェックしたい場合は、UwcManager.isWindowsGraphicsCaptureSupported から確認可能です。

カーソルのキャプチャについて

Windows Graphics Capture モードでカーソルのキャプチャが利用できるのは Windows 10 version 2004 以降になります。サポートしているかは UwcManager.isWindowsGraphicsCaptureCursorCaptureEnabledApiSupported で確認できるので、どうしてもカーソルが必要なアプリケーションの場合は、false の場合は旧 API(Print Window など)へフォールバックするようにしてください。

ストアアプリのアイコンについて

UWP アプリは最小化するとサスペンド状態になります。

f:id:hecomi:20210430220536p:plain

docs.microsoft.com

起動時に現在の制約でこの状態になっているアプリのアイコンは正しく取ってこれません(Windows デフォルトのアイコンになってしまう)。将来的に修正する予定です。

GetPixel について

Windows Graphics Capture は CPU メモリ上にテクスチャ情報を持ってこずに更新するため速い一方、特定のピクセルの情報を取ってくるといったことは出来ません。ピクセルの情報がほしいときは、ほしいタイミングでキャプチャモードを Print Window などの旧 API に切り替えてキャプチャしてください。

設計メモ

以下今回の設計のメモです。

Windows Graphics Capture の導入

従来のバージョンでは、Unity のメイン動作を阻害しない & 簡単に使える、というポリシーのもと、以下の 5 つのスレッドが動作していました。

f:id:hecomi:20180826223648p:plain

  • Window の情報を集めるスレッド
    • ウィンドウハンドルをかき集めてウィンドウの大きさやタイトルなどを更新するスレッド
  • メインスレッド
    • Unity のメインスレッドで、上の情報を取得して欲しいウィンドウのキャプチャのリクエストを行う
  • キャプチャスレッド
    • PrintWindow や BitBlt を使って CPU 上でキャプチャを行いバッファを作成
  • アップロードスレッド
    • キャプチャされたバッファを GPU 上の共有テクスチャへアップロードする
  • レンダリングスレッド
    • Unity のレンダリングスレッドで共有テクスチャを Unity で確保したテクスチャへコピー

このようにリクエストを受け取りワンショットでキャプチャリクエストを出す方式でした。しかしながら Windows Graphics Capture はセッションを開いてテクスチャが更新されてきたタイミングでコールバックが呼ばれ、キャプチャが不要になったらセッションを閉じる、という設計になっています。この関係で、そのままではこれまでの設計とは相性が良くありませんでした。が、ウンウンと考えて従来の仕組みに収まるよう次のような設計にしました。

  • リクエストが来たらキャプチャスレッドでは(セッションが開始されていなかったら)セッションの開始を行う
  • 1 秒間リクエストが来なかったら自動的にキャプチャスレッド上でセッションのクローズを行う
  • コールバックを用いずに、アップロードスレッドでキャプチャプールをポーリング、新規テクスチャがあった場合はそれを共有テクスチャへコピー

これにより、新規部分以外は従来の仕組みをそのまま使える設計に出来ました。

UWP アプリのアイコンのキャプチャ

Window List サンプルを見ていたら UWP アプリのアイコンがキャプチャ出来ていないのに気づきました。従来のアプリは GetClassLongPtr() および GCLP_HICON の組み合わせか、SendMessage()WM_GETICON の組み合わせにより、ウィンドウハンドル(HWND)に紐づくアイコンハンドル(HICON)を取得することが出来ていました。しかしながら、UWP アプリはこれでは取得できず...、exe 同階層にある AppxManifest.xml に記述されている情報からロゴ(.png など)のパスをパースして取ってくる方式しかなさそうでした。そこで仕方なく次のような道程を経て取得する方式にしました。

  • HWND からプロセス ID を取得
  • プロセス ID からプロセスハンドルを取得して QueryFullProcessImageName() で exe のパスを取得
    • ただし ApplicationFrameHost.exe(ブローカー)を介して起動しているアプリは exe がこれになってしまうので、子プロセスを EnumChildWindows() 経由で検索して欲しいプロセス ID を頑張って探す
  • exe のパスから AppxManifest.xml のパスを作る
  • この XML を開いてパースし、 タグの中身からロゴに使われている .png のパスを取得
    • ただし、Assets/Logo.png となっていても実際は Assets/Logo.scale-200.png のようにスケールサフィックスのついた名前になっている
    • なので、Assets/Logo と .png に分解し、.scale-xxx で xxx が最も大きいファイルを探す
  • パスが出来たら Gdiplus::Bitmap::FromFile() でこれを開く
  • ここからアイコンハンドル(HICON)やアイコンサイズが取得できる

といった長い道のりを経てようやくアイコンが取得できました。。

f:id:hecomi:20210430223711p:plain

パフォーマンス改善について

Windows Graphics Capture でも大量に開くとパフォーマンスが落ちていて、計測してみてみると、GraphicsCaptureSessionIsCursorCaptureEnabled(bool) の呼び出しに時間がかかっているようでした(2 ~ 3 ms)。そこでフラグが切り替わったときのみ呼び出すよう修正しました。

また、アップロードスレッドがこれまで Unity のレンダリングスレッドと不要な同期をしていたのですが意味がなかったので同期を切り、常に裏で動くよう修正しました。

初期化やアップデートコストについて

現在は、初期化時に全てのウィンドウの様々な情報(タイトルやプロセスなど)を取得したり、また Windows Graphics API が利用可能かチェックや、上記アイコンのパースなどが動いています。初期化時且つバックグラウンドスレッドで動くので影響は大きくないとは思っているのですが、マネージャ経由などでこれらを行うかどうかの細かい設定が行えるようにしたいと考えています。

おわりに

数年前に欲しかった環境が出来ました!かなりキャプチャが速くなったので場合によっては uDesktopDuplication が要らなくなる程かもしれません…。また、以前作成した VR デスクトップのコンセプトのこちらも実用に耐えうるものになるかもしれません(今はあまりこちらのアプリを開発再開するモチベーションはないですが...):

MIT ライセンスで配布してますので、VTuber の方々や VR アプリの開発者の方々も使いやすいかと思いますので、ぜひご活用いただけると嬉しいです(ご使用の際は Twitter などでご一報いただけるととても嬉しいです!)