C++言語

ウィンドウ or モニタ画面全体を高速にキャプチャする方法

はじめに

長年続けている音ゲーのPC版がリリースされ、気になっていた判定基準を調べてみようと、
仕事とはほぼ無縁な Windows.Graphics.Capture を少し勉強したので、その時の備忘録。

簡単に言うと、HWNDHMONITOR から直接 ID3D11Texture2D を取得することができ、
DirectX のリソースを共有する仕組みである D3D11Interop が提供されている API です。

SANACHAN
SANACHAN
DirectX はゲーム業界でよく使われると思いますが、良い書籍は少ない感じだったので、
参考にした YouTube を貼っておきます。

 

DirectX とは?

DirectX は、ゲームや動画などのマルチメディアコンテンツを Windows 上で処理するために
Microsoft が開発した API の総称です。

Windows は、もともとゲーム用に作られた OS ではないため、GDI(Graphics Device Interface)
と呼ばれるグラフィック処理機構のみで、専用の機能は備わっていませんでした。

そこで、ゲームや動画といったマルチメディアを快適に処理できるように、
DirectX と呼ばれる仕組みが Windows に組み込まれました。

SANACHAN
SANACHAN
GDI は太古の技術で、ゲームや動画などの高速な処理を行うことには不向き。

DirectX は Microsoft が提供する Windows をはじめ、Microsoft が販売している
「Xboxシリーズ」、および数多くのPC版ゲームで広く活用されている技術です。

DirectX を使えば、動画や画像などの処理を効率よくできるようになります
簡単にまとめると、以下のようなことが提供されています。

  • グラフィックスや動画に関する開発環境・API の提供(開発者向け)
  • グラフィックス処理や動画再生のサポート(開発者・利用者向け)
  • サウンド関連のサポート(開発者・利用者向け)
  • ゲームなどの実行環境の提供(利用者向け)

 

Windows Graphics Capture

素の DirectX を使ってコードを記述すると非効率なため、今回は WinRT の一部として提供されている
Windows Graphics Capture を使ってキャプチャを取得することにしました。

Windows 10 April 2018 Update (1803) で追加された API で、画面をテクスチャ(ID3D11Texture2D
として取得できるのが特徴です。ウィンドウ単位(HWND)、モニタ単位(HMONITOR)での
取得をサポートしています。

ID3D11Texture2D を直接扱うことができれば GPU 上でコピーが完了するため、
かなり高速に処理を行うことができます。

 

使い方(サンプルコード)

↓↓↓↓↓クリックするとコード全体が表示されます。↓↓↓↓

GraphicsCapture.h

 GraphicsCapture.cpp

 

今回はモニタ指定でのキャプチャーとしていますが、GraphicsCapture::start() で呼び出す
IGraphicsCaptureItemInterop::CreateForMonitor() がモニタ指定となっています。
IGraphicsCaptureItemInterop::CreateForWindow() とすることでウィンドウ指定にもできます。

SANACHAN
SANACHAN
全画面ゲームのため、モニタ指定としています。

 

キャプチャの流れ

フレームの到着

Direct3D11CaptureFramePool のイベント FrameArrived にコールバックを登録することで、
フレームが来るたびにコールバックが呼び出されるようになります。

コールバックが呼ばれる仕組みとして、2通りの方式が用意されています。

SANACHAN
SANACHAN
使い方に注意が必要となります。

①FramePool 作成元スレッドから呼ぶ方式

Direct3D11CaptureFramePool::Create() で作成すると、この方式になります。

この場合、フレームが来るのは DispatchMessage() の中になるため、
たとえ指定したウィンドウが無くても、メッセージループが必要になります。

 

②専用スレッドから呼ぶ方式

Direct3D11CaptureFramePool::CreateFreeThreaded() で作成すると、この方式になります。

この場合、キャプチャ用の専用スレッドが起こされ、そこからコールバックが呼び出されます。
FramePool の終了や破棄は、作成元スレッドで行う必要があるため、
CreateFreeThreaded() のコールバックから終了させることはできません。

 

フレームの読出し

FrameArrived のイベントに登録したコールバックが呼び出された後、
テクスチャを読み出す処理を記述します。

 

Utils.cpp の一部

SANACHAN
SANACHAN
一般的な DMA でコピーする際の記述に似ていますね。

 

性能評価

以下のような処理を追加して、1フレームの読出しにかかる時間を計測してみました。

 

Utils.h

Utils.cpp

Main.cpp

 

コピーしたフレームを画像処理しやすい OpenCV の Mat 形式に変換した後、
コールバック内からメインスレッドへ通知を投げ、間隔を計測、表示するようにしています。

FHD 60Hz(=60 fps)の場合は、平均 16ms、
FHD 165Hz(=165 fps)の場合は、平均 6ms 程度でした。

SANACHAN
SANACHAN
リフレッシュレートに応じて、遅滞なく処理できていることが分かります。

 

ざっくり、実行環境は以下の通りです。

IDE Visual Studio 2022
CPU Intel Core i5 - 11400
GPU NVIDIA GTX 1650
MEM 16 GiB
解像度 1920 x 1080 (FHD)

 

まとめ

ReadTexture で、キャプチャ結果を staging にコピーし、
コピーの完了を待って Map() するまでは、約 3ms ほどかかっていました。

165 fps ぐらいをたたき出すゲームは、残り 3ms ほどでユーザ入力の処理をしているという
恐ろしい世界なんですね。性能を意識したプログラミングが強く求められるようです。

 

結局、何がしたかったのかというと、音ゲーで流れてきた譜面に従って操作/入力した後、
一番良い判定は、どのくらいのズレが許されているのかを確かめたかったのです。

SANACHAN
SANACHAN
結果は、±16ms でした。
60 fps のゲームのため、±1フレーム が許されているということが分かりました(笑)

 

参考

 

こちらの記事もよく読まれています

  • この記事を書いた人
  • 最新記事
SANACHAN

SANACHAN

「生涯一エンジニア」を掲げ、大手グローバル企業でSE/PGとして8年勤め、キャリアアップ転職した現役のエンジニアです。世にあるメジャーな全プログラム言語(コボル除く)を自由に扱えます。一児の父。自分のため、家族のため、日々勉強してます。システムエンジニア、プログラミングに関する情報を蓄積している雑記帳です。

-C++言語
-, , ,