Windows.Graphics.Capture
自分のプロセスから参照できるウィンドウだけでなく、OS上で表示されている他のプロセスのウィンドウの内容をキャプチャしたい場合、従来であれば太古のGDI関数(もはやまともに動くか怪しい)や、DwmGetDxSharedSurface
(隠しAPIなのでお行儀が悪い上にDWM内で同期が取れていないのか取得できるイメージが中途半端)などが利用されてきたようですが、Windows 10 version 1803でキャプチャ専用のAPIがWinRTに用意されました。
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が提供されていましたが、名前の通り取得できるのはデスクトップ丸ごとのイメージのみでした。
渡されるイメージもDwmGetDxSharedSurface
とは異なりDirect3DやOpenGLの結果が正常に反映されており、アクリル効果やSetWindowCompositionAttribute
の効果が反映された後のイメージで取得されます。
タスクビューやピークで表示されるサムネイルのオリジナルと同等のイメージが取得できると思って良さそうです(いくつかの手法で実装される非矩形ウィンドウがどのようにキャプチャされるかは今回試してませんが)。
注意点として、キャプチャ対象がウィンドウの場合は例えキャプチャしているウィンドウ上に表示されていてもポップアップウィンドウはキャプチャされません。よってポップアップメニューやツールチップの類は基本的にキャプチャされません。また、プッシュ式でイメージが流れてくるためエンコードや配信用途で扱いやすく、スナップショットのみの用途では若干扱いにくい構成になっています。
C++デスクトップアプリでの実装
Windows.Graphics.Capture
はWinRTのAPIとして提供されています。C++言語でWinRTのAPIを呼び出すにはいくつか方法がありますが、ここでは最新の手法となるC++/WinRTを使用します。
用意したサンプルはこちら。
公式ドキュメントには基本的にUWPアプリでの手順で書かれており、デスクトップアプリでも流れは変わりませんが、いくつか専用の手続きや注意点が必要になります。
スレッドアパートメントの初期化
メインとなるUIスレッドをSTAとして設定します。STAにしないと後述するピッカーが適切に動作しない、co_await
で元のスレッドに戻ってこないなどいつくかの不都合が生じます。
init_apartment(apartment_type::single_threaded);
GraphicsCaptureItem
の作成
まずキャプチャ対象となるウィンドウあるいはモニタを指定したGraphicsCaptureItem
オブジェクトを作成します。これには専用ピッカーであるGraphicsCapturePicker
を起動して取得するのが一番簡単です。これはアプリケーションウィンドウ及びモニタの一覧がモーダルダイアログで表示され、ユーザー自身に選択を促すことができます。
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
を作成することもできます。
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
がそれにあたります。
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
が作成できます。
_framePool = Direct3D11CaptureFramePool::Create(
_device,
DirectXPixelFormat::B8G8R8A8UIntNormalized,
2,
item.Size());
_frameArrived = _framePool.FrameArrived(auto_revoke, { this, &CaptureView::OnFrameArrived });
バッファのサイズは自由に決められますが、キャプチャ対象となるウィンドウより小さくてもまず意味がないのでGraphicsCaptureItem.Size
プロパティと同じにすれば良いでしょう。
Direct3D11CaptureFramePool.FrameArrived
の部分はC++/WinRT流のイベント登録です。トークンを返すパターンとこのリボーカーを返すパターンがありますが、リボーカーの方が結合が緩くデストラクタで接続解除してくれるのでお勧めです。
キャプチャ開始
_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
メソッドも存在します。
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
のバッファを再作成して追従させる必要があります。
// 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年以上経過した今、ようやく素直にウィンドウのサーフェイスを差し出してくれるまでにデレてくれた感がありますねー。