凹みTips

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

Unity で Windows のウィンドウをキャプチャできる uWindowCapture v0.6.0 をリリースしました

はじめに

Unity 内で Windows でウィンドウを個別にキャプチャするアセットの uWindowCapture v.0.6.0 へ更新しましたので内容の紹介です(≒ 今月はネタがないのでバグ修正報告です)。

uWindowCapture は Unity のメインスレッドを邪魔することなく複数のウィンドウをキャプチャして Unity 内でテクスチャ化してくれるアセットです。ウィンドウが別のウィンドウに背後に隠れていてもキャプチャでき、VR 内で独自のウィンドウシステムを作れたり、デスクトップのキャプチャも(高速ではないですが)できるので、uDesktopDuplication のフォールバックとしても使えます。

tips.hecomi.com

ダウンロード

github.com

更新内容

以下の内容の更新を行いました。

  • 更新
    • キャプチャ対象のウィンドウが UWP かどうか調べる isUWP を追加
    • isStoreAppisApplicationFrameWindow へ変更
  • バグ修正
    • Spotify のようなアプリでキャプチャのブロッキング(止まって更新されなくなる)される
    • 異なる DPI のモニタを 2 枚以上使用していた際に、ウィンドウを移動させると表示領域が変にクロッピングされる
    • デスクトップキャプチャ時に複数のモニタがある場合サブモニタ側でカーソルが描画されない
    • 最大化時に画面端が見切れてしまう場合がある

バグの方は結構前に頂いていたのに修正が遅くなってしまいました...、すみません。この中からいくつか内容や関連する話題を紹介します。

更新内容詳細

UWP 判別

動いているアプリが UWP かどうかによって処理の判別を行いたい場合のために isUWP フラグを UwcWindow に追加しました。これまでは isStoreApp というフラグを用意してこれで判断できると思っていたのですが、こちらは HWND に対して GetWindowClassName() を行った結果得られるウィンドウのクラス名が ApplicationFrameWindow かどうか調べる、という手法だったのですが、設定ウィンドウなどはこれで問題ないのですが実際にストアアプリを調べてみると該当しないことが分かりました。そこで以下の stackoverflow のスレッドを参考に UWP の判断を行う方法に切り替えました:

stackoverflow.com

こちらは kernel32.dll にある GetPackageFamilyName() で該当プロセスを調べて ERROR_INSUFFICIENT_BUFFER だった場合に UWP と判断する手法のようです。ちょっと調べ方は抜けもありそうな気もしますが一応調査した範囲内ではうまく動いていたのでこれで行くことにしました。

bool IsUWP(DWORD pid)
{
    using GetPackageFamilyNameType = LONG (WINAPI*)(HANDLE, UINT32*, PWSTR);

    const auto hKernel32 = ::GetModuleHandleA("kernel32.dll");
    if (!hKernel32) return false;

    const auto GetPackageFamilyName = (GetPackageFamilyNameType)::GetProcAddress(hKernel32, "GetPackageFamilyName");
    if (!GetPackageFamilyName) return false;

    auto process = ::OpenProcess(PROCESS_QUERY_INFORMATION, FALSE, pid);
    ScopedReleaser releaser([&] { ::CloseHandle(process); });

    UINT32 len = 0;
    const auto res = GetPackageFamilyName(process, &len, NULL);

    return res == ERROR_INSUFFICIENT_BUFFER;
}

Alt-Tab 判別

今回の内容とは関係ないですが、Alt-Tab で切替えられるウィンドウかどうかのフラグの isAltTabWindow についても UWP アプリ判断のついでに書いておきます。uWindowCapture では右クリックのメニューやブラウザでリンクにカーソルを乗せたときのポップアップなども含むすべてのウィンドウを管理しています。ここから該当のウィンドウを探すのは結構大変ですが、Alt-Tab で切替えられるウィンドウを探すユースケースが大半だと思いますので、このフラグが設定できれば便利だと思い追加しました(ウィンドウ名による検索の追加条件としても使えます)。

ただ、これを直接調べる API はないので色々なチェックを組み合わせる必要があります。今の所の試行錯誤では以下のような形で概ねうまく行っているのでは...と思います。

bool IsAltTabWindow(HWND hWnd)
{
    if (!::IsWindowVisible(hWnd)) return false;

    // Ref: https://blogs.msdn.microsoft.com/oldnewthing/20071008-00/?p=24863/
    HWND hWndWalk = ::GetAncestor(hWnd, GA_ROOTOWNER);
    HWND hWndTry;
    while ((hWndTry = ::GetLastActivePopup(hWndWalk)) != hWndTry) 
    {
        if (::IsWindowVisible(hWndTry)) break;
        hWndWalk = hWndTry;
    }
    if (hWndWalk != hWnd)
    {
        return false;
    }

    // Exclude tool windows
    if (::GetWindowLong(hWnd, GWL_EXSTYLE) & WS_EX_TOOLWINDOW)
    {
        return false;
    }

    // Include fullscreen
    if (IsFullScreenWindow(hWnd))
    {
        return true;
    }

    // Exclude task tray programs
    TITLEBARINFO titleBar;
    titleBar.cbSize = sizeof(TITLEBARINFO);
    ::GetTitleBarInfo(hWnd, &titleBar);
    if (titleBar.rgstate[0] & STATE_SYSTEM_INVISIBLE)
    {
        return false;
    }

    return true;
}

最初は以下のエントリを参考にしています。

blogs.msdn.microsoft.com

ここにツールウィンドウとタスクトレイアプリの除外を行っています。これでも駄目な点が見つかったり効率的な調べ方をご存知の際はぜひご連絡ください。

ウィンドウのキャプチャ領域判断について

キャプチャの領域とウィンドウの領域は異なるケースが結構あります。この大きな原因の2大巨頭が DPI スケーリングとウィンドウ影です。そしてこれ以外にも色々な要因があります。

DPI スケーリング

うちでは 27 インチの 4K モニタを使っているのですが、ドットバイドットだと文字やアイコンが小さすぎるので 150% のスケーリングをしています。

f:id:hecomi:20191130104110p:plain

この設定をすると対応しているアプリはキレイに表示されるのですがそうでない場合は描画は以前のままスケールアップされた(解像度の低くなったような)表示になります。また、ヘテロなディスプレイ構成でディスプレイ毎に異なる DPI 設定を行っている場合は、ウィンドウが所属するディスプレイが切り替わった際に、DPI の設定がうまく設定されていればキレイに切り替わります。

ウィンドウ影

f:id:hecomi:20191130104208p:plain

こういったウィンドウ影があると描画領域にここも含まれてしまい、キャプチャした際は影エリアが真っ黒になって太い縁のようになってしまいます。Unity でのユースケースとしては影は邪魔なので取り除いてしまいたいです。

コード

このため色々な登場人物がいることになります。

  • キャプチャに必要なサイズ
  • キャプチャから除外するオフセット
  • 実際にテクスチャ化するサイズ

ここに BitBltPrintWindow の場合分けが入ります。また、最大化時のクロッピングエリアのバグ報告を受けて調べてみたところ、最大化したときは (0,0) - (3840, 2160) とかならずに (-11, -11) - (3855, 2175) とかになる場合、ならない場合があり、またタスクバーの位置サイズによって最大化サイズも変わり、DPI 設定でも変わる。みたいなことを考慮していないのが原因でした

これらを踏まえて以下のようなコードになっています。

毎回メンテのたびに何していたか忘れて白目になるので整理したいところです。。

今後の展望

メインスレッドでないといって IsUwp() のような処理を情報収集スレッドの方に追いやってしまっているので、それなりに CPU が回ってしまっています。キャプチャ自体はリクエストのあったものだけ行うのですが、情報収集に関してはすべてのウィンドウに対して行っているのでそれなりの重さがあります。ここはウィンドウ単位で収集する情報をチューニングできたり、もしくは全体の設定としてどの情報を収集するかをセットできるようにしたり、といったことをしたいです。ウィンドウタイトル取得はかなり重いのでこの対応をしていますが、これも含めた形の仕組みにしたいですね。

また、テクスチャの生成は現在 Unity 側で行っているのですが、ウィンドウのサイズ変更があった際にサイズ変更通知を DLL から Unity へ投げ、サイズが変更されたら DLL 側でキャプチャを再開する、という流れになっているのでスケーリング時に白画が見えてしまったりしてイケていません。ここは DLL 側で ID3D11Texture2D を生成する方式に変更したいなぁ、と思っています。

おわりに

今月はネタがなかったので細かい話になってしまいました...。

uWindowCapture は開発期間の比較的長いアセットでしたので結構愛着があります。Seg さんの VaNiiMenu やおぐらさんの VDRAW のように使用例も色々と出てきて下さり嬉しい限りです。

sabowl.sakura.ne.jp

sites.google.com

今回のように issue を頂ければ(修正可能範囲内で)対応しますので問題があればご報告ください。また、プルリクもお待ちしています。