Windows 10のウィンドウキャプチャAPI

## `Windows.Graphics.Capture`

自分のプロセスから参照できるウィンドウだけでなく、OS上で表示されている他のプロセスのウィンドウの内容をキャプチャしたい場合、従来であれば[太古のGDI関数](https://docs.microsoft.com/en-us/windows/win32/gdi/capturing-an-image)（もはやまともに動くか怪しい）や、`DwmGetDxSharedSurface`（隠しAPIなのでお行儀が悪い上にDWM内で同期が取れていないのか取得できるイメージが中途半端）などが利用されてきたようですが、Windows 10 version 1803でキャプチャ専用のAPIがWinRTに用意されました。

> [**Screen capture**](https://docs.microsoft.com/en-us/windows/uwp/audio-video-camera/screen-capture)
 Starting in Windows 10, version 1803, the Windows.Graphics.Capture namespace provides APIs to acquire frames from a display or application window, to create video streams or snapshots to build collaborative and interactive experiences.

`Windows.Graphics.Capture`が1803で実装された当初は、Win32的なデスクトップアプリで動作しない、WindowsのエディションがProやEnterpriseを要求されるなど、ちょっと意味のわからない制限がありましたが、それらも緩和されているのでようやく使い物になりそうです。

このAPIの最大のメリットはウィンドウのイメージをGPUリソース（`::ID3D11Texture2D`）のまま渡してくれるところでしょうか。例えばキャプチャイメージを3Dシーンに張り付けたり、ハードウェアエンコーダに渡したりといった処理が低負荷で行える可能性が広がります。

キャプチャAPI自体も低負荷で動作するので、今までDWMでプライベートに管理されていたウィンドウ毎のサーフェイスをGPUのみを介してアプリケーション側に転送しているものと思われます。

キャプチャされたイメージをGPUリソースのまま取得できる標準APIはWin8の時点で[Desktop Duplication API](https://docs.microsoft.com/en-us/windows/win32/direct3ddxgi/desktop-dup-api)が提供されていましたが、名前の通り取得できるのはデスクトップ丸ごとのイメージのみでした。


渡されるイメージも`DwmGetDxSharedSurface`とは異なりDirect3DやOpenGLの結果が正常に反映されており、アクリル効果や`SetWindowCompositionAttribute`の効果が反映された後のイメージで取得されます。
![2019-11-09 221522.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/36830/9696ef3e-9743-85c9-2b32-d1701f56896c.png)
タスクビューやピークで表示されるサムネイルのオリジナルと同等のイメージが取得できると思って良さそうです（いくつかの手法で実装される非矩形ウィンドウがどのようにキャプチャされるかは今回試してませんが）。

注意点として、キャプチャ対象がウィンドウの場合は例えキャプチャしているウィンドウ上に表示されていてもポップアップウィンドウはキャプチャされません。よってポップアップメニューやツールチップの類は基本的にキャプチャされません。また、プッシュ式でイメージが流れてくるためエンコードや配信用途で扱いやすく、スナップショットのみの用途では若干扱いにくい構成になっています。

##C++デスクトップアプリでの実装

`Windows.Graphics.Capture`はWinRTのAPIとして提供されています。C++言語でWinRTのAPIを呼び出すにはいくつか方法がありますが、ここでは最新の手法となるC++/WinRTを使用します。

用意したサンプルは[**こちら**](https://github.com/opysky/examples/tree/master/winrt/GraphicsCapture/CapturePreview)。

公式ドキュメントには基本的にUWPアプリでの手順で書かれており、デスクトップアプリでも流れは変わりませんが、いくつか専用の手続きや注意点が必要になります。

###スレッドアパートメントの初期化

メインとなるUIスレッドをSTAとして設定します。STAにしないと後述するピッカーが適切に動作しない、`co_await`で元のスレッドに戻ってこないなどいつくかの不都合が生じます。

```cpp:main.cpp
init_apartment(apartment_type::single_threaded);
```

###`GraphicsCaptureItem`の作成

まずキャプチャ対象となるウィンドウあるいはモニタを指定した`GraphicsCaptureItem`オブジェクトを作成します。これには専用ピッカーである`GraphicsCapturePicker`を起動して取得するのが一番簡単です。これはアプリケーションウィンドウ及びモニタの一覧がモーダルダイアログで表示され、ユーザー自身に選択を促すことができます。
![2019-11-09 222224.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/36830/284ee8f3-37e3-f6a4-7a57-4331f876f197.png)

```cpp:main.cpp
fire_and_forget OnButtonClicked(UINT, CPoint const&) {
  GraphicsCapturePicker picker;
  auto interop = picker.as<::IInitializeWithWindow>();
  // オーナーとなるウィンドウのハンドルを渡す
  interop->Initialize(m_hWnd); 

  auto item = co_await picker.PickSingleItemAsync();
  if (!!item) {
    _captureView.StartCapture(item);
  }
}
```
UWPアプリでは単純に既定のコンストラクタからインスタンスを作成するだけで機能しますが、デスクトップアプリの場合は`GraphicsCapturePicker.PickSingleItemAsync`メソッドを呼び出す前に`::IInitializeWithWindow`というインターフェイスを介してオーナーウィンドウを手動で設定する必要があります。

`PickSingleItemAsync`は名前の通りWinRTの非同期関数です。これらは戻り値が非同期操作の型でラップされていて、実際の値を取得するにはC++/CXならば`concurrency::task`の継続チェーンなどで記述する必要がありましたが、C++/WinRTではC++20のコルーチン拡張に則ったアダプタなどが実装されており、良い感じにイマドキのasync/await風に記述できるようになっています。

デスクトップアプリの場合、ウィンドウハンドルやモニタのハンドルを渡して直接`GraphicsCaptureItem`を作成することもできます。

```cpp:CaptureView.cpp
auto CreateCaptureItemForWindow(HWND hwnd)
{
  namespace abi = ABI::Windows::Graphics::Capture;

  auto factory = get_activation_factory<GraphicsCaptureItem>();
  auto interop = factory.as<::IGraphicsCaptureItemInterop>();
  GraphicsCaptureItem item{ nullptr };
  check_hresult(interop->CreateForWindow(hwnd, guid_of<abi::IGraphicsCaptureItem>(), reinterpret_cast<void**>(put_abi(item))));
  return item;
}
```

正直C++/WinRTの情報自体が公式ドキュメントにすら少ないので、このへんの手続きはMicrosoftのサンプルだけが頼りですねー。

###`Direct3D11CaptureFramePool`の作成

OSからキャプチャイメージを受け取り、アプリ側に渡してくれるのが`Direct3D11CaptureFramePool`です。名前の通りDirect3D 11を基盤としており、Direct3D 11のデバイスとバッファのフォーマットやサイズを指定して作成します。

ここでC++からDirect3Dデバイスを渡す方法ですが、あらゆる言語から呼び出せることを目的としたWinRTには例えばインターフェイスポインタを`IntPtr`に割り当てて引数として渡すような雑な（？）シグネチャはありません。代わりに相互運用の為の`Windows.Graphics.DirectX.Direct3D11.IDirect3DDevice`という型がWinRTに定義されているのでこれを仲介します。

ネイティブの`::ID3D11Device`と`Windows.Graphics.DirectX.Direct3D11.IDirect3DDevice`の相互変換にはヘルパーとなるインターフェイスや関数がWindows SDKに定義されています。`::CreateDirect3D11DeviceFromDXGIDevice`と`::Windows::Graphics::DirectX::Direct3D11::IDirect3DDxgiInterfaceAccess`がそれにあたります。

```cpp:Direct3DHelper.h
auto CreateDirect3DDevice()
{
    UINT createDeviceFlags = D3D11_CREATE_DEVICE_BGRA_SUPPORT;
#ifdef _DEBUG
    createDeviceFlags |= D3D11_CREATE_DEVICE_DEBUG;
#endif
    com_ptr<ID3D11Device> d3dDevice;
    check_hresult(D3D11CreateDevice(
        nullptr, 
        D3D_DRIVER_TYPE_HARDWARE,
        nullptr, 
        createDeviceFlags, 
        nullptr, 
        0, 
        D3D11_SDK_VERSION, 
        d3dDevice.put(), 
        nullptr, 
        nullptr));

    // ID3D11DeviceからWinRTのIDirect3DDeviceを作成
    auto dxgiDevice = d3dDevice.as<IDXGIDevice>();
    com_ptr<::IInspectable> device;
    check_hresult(::CreateDirect3D11DeviceFromDXGIDevice(dxgiDevice.get(), device.put()));
    return device.as<winrt::Windows::Graphics::DirectX::Direct3D11::IDirect3DDevice>();
}

template <typename T>
auto GetDXGIInterfaceFromObject(winrt::Windows::Foundation::IInspectable const& object)
{
    // WinRTの型からネイティブのインターフェイスを取得
    auto access = object.as<::Windows::Graphics::DirectX::Direct3D11::IDirect3DDxgiInterfaceAccess>();
    com_ptr<T> result;
    check_hresult(access->GetInterface(guid_of<T>(), result.put_void()));
    return result;
}
```

なんだかもうWin32の型なのかWinRTの型なのかC++/WinRTの型なのかワケがわからなくなってきますねー。とまれ、`Windows.Graphics.DirectX.Direct3D11.IDirect3DDevice`が入手できれば`Direct3D11CaptureFramePool`が作成できます。

```cpp:CaptureView.cpp
    _framePool = Direct3D11CaptureFramePool::Create(
        _device,
        DirectXPixelFormat::B8G8R8A8UIntNormalized,
        2,
        item.Size());

    _frameArrived = _framePool.FrameArrived(auto_revoke, { this, &CaptureView::OnFrameArrived });
```

バッファのサイズは自由に決められますが、キャプチャ対象となるウィンドウより小さくてもまず意味がないので`GraphicsCaptureItem.Size`プロパティと同じにすれば良いでしょう。

`Direct3D11CaptureFramePool.FrameArrived`の部分はC++/WinRT流のイベント登録です。トークンを返すパターンとこのリボーカーを返すパターンがありますが、リボーカーの方が結合が緩くデストラクタで接続解除してくれるのでお勧めです。

###キャプチャ開始

```cpp:CaptureView.cpp
    _captureSession = _framePool.CreateCaptureSession(item);
    _captureSession.StartCapture();
```

`Direct3D11CaptureFramePool.CreateSession`に`GraphicsCaptureItem`を渡し`GraphicsCaptureSession`を取得、`GraphicsCaptureSession.StartCapture`メソッドで実際のキャプチャが開始されます。

###`Direct3D11CaptureFramePool.FrameArrived`イベント

キャプチャを開始すると、`Direct3D11CaptureFramePool`にキャプチャフレームが届いて利用可能になったタイミングで`Direct3D11CaptureFramePool.FrameArrived`イベントが発行されます。試行した環境下では対象となるウィンドウ自身のレンダリング頻度に関わらずvsyncの間隔で発行されました。

ちなみに、`Direct3D11CaptureFramePool.Create`メソッドで作成した場合`FrameArrived`イベントは作成元と同じスレッド（UIスレッド）から呼び出されます。独自のスレッドを作ってそこから`FrameArrived`イベントが発行される`Direct3D11CaptureFramePool.CreateFreeThreaded`メソッドも存在します。

```cpp:CaptureView.cpp
void CaptureView::OnFrameArrived(
    Direct3D11CaptureFramePool const& sender, 
    winrt::Windows::Foundation::IInspectable const& args)
{
    auto frame = sender.TryGetNextFrame();
    auto frameSurface = GetDXGIInterfaceFromObject<::ID3D11Texture2D>(frame.Surface());
    auto contentSize = frame.ContentSize();
```

`OnFrameArrived`イベントにも引数はあるのですが、なぜか目的のイメージは発行元の`Direct3D11CaptureFramePool`の`TryGetNextFrame`メソッドから取得する流れになるようです。`Direct3D11CaptureFrame.Surface`プロパティから`Windows.Graphics.DirectX.Direct3D11.IDirect3DSurface`を取得し、前述のヘルパーを介して`::ID3D11Texture2D`としてアクセスできます。`D3D11_BIND_SHADER_RESOURCE`も割り当てられているのでシェーダの入力として扱うことが可能です。

キャプチャ中に対象となるウィンドウのサイズが変更されたらどうなるか？　という点ですが、これは手動で`Direct3D11CaptureFramePool`のバッファを再作成して追従させる必要があります。

```cpp:CaptureView.cpp
    // Direct3D11CaptureFramePoolは作成時のパラメータを取得するプロパティが無いのでフレームから取得する
    auto surfaceDesc = frame.Surface().Description();
    auto itemSize = _captureItem.Size();
    if (itemSize.Width != surfaceDesc.Width || itemSize.Height != surfaceDesc.Height) {
        SizeInt32 size;
        size.Width = std::max(itemSize.Width, 1);
        size.Height = std::max(itemSize.Height, 1);
        _framePool.Recreate(_device, DirectXPixelFormat::B8G8R8A8UIntNormalized, 2, size);
    }
```

再作成用の`Direct3D11CaptureFramePool.Recreate`メソッドがあるのでキャプチャのセッションを停止せずにバッファサイズを変更することができます。

## おしまい

VistaからDWMが導入されて10年以上経過した今、ようやく素直にウィンドウのサーフェイスを差し出してくれるまでにデレてくれた感がありますねー。