はじめに
長年続けている音ゲーのPC版がリリースされ、気になっていた判定基準を調べてみようと、
仕事とはほぼ無縁な Windows.Graphics.Capture を少し勉強したので、その時の備忘録。
簡単に言うと、HWND や HMONITOR から直接 ID3D11Texture2D を取得することができ、
DirectX のリソースを共有する仕組みである D3D11Interop が提供されている API です。

参考にした YouTube を貼っておきます。
DirectX とは?
DirectX は、ゲームや動画などのマルチメディアコンテンツを Windows 上で処理するために
Microsoft が開発した API の総称です。
Windows は、もともとゲーム用に作られた OS ではないため、GDI(Graphics Device Interface)
と呼ばれるグラフィック処理機構のみで、専用の機能は備わっていませんでした。
そこで、ゲームや動画といったマルチメディアを快適に処理できるように、
DirectX と呼ばれる仕組みが Windows に組み込まれました。

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
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 |
#pragma once #pragma comment(lib, "windowsapp.lib") #include <vector> #include <functional> #include <mutex> #include <condition_variable> #include <windows.h> #include <d3d11.h> #include <d3d11_4.h> #include <winrt/Windows.Foundation.h> #include <winrt/Windows.System.h> #include <winrt/Windows.Graphics.Capture.h> #include <winrt/Windows.Graphics.DirectX.Direct3d11.h> #include <windows.graphics.directx.direct3d11.interop.h> #include <Windows.Graphics.Capture.Interop.h> using namespace winrt; using namespace winrt::Windows::Foundation; using namespace winrt::Windows::System; using namespace winrt::Windows::Graphics; using namespace winrt::Windows::Graphics::DirectX; using namespace winrt::Windows::Graphics::DirectX::Direct3D11; using namespace winrt::Windows::Graphics::Capture; class GraphicsCapture { public: using Callback = std::function<void(ID3D11Texture2D*, int, int)>; GraphicsCapture(); ~GraphicsCapture(); bool start(HMONITOR, const Callback&); void stop(); private: template<class CreateCaptureItem> bool startImpl(const Callback&, const CreateCaptureItem&); void onFrameArrived( winrt::Windows::Graphics::Capture::Direct3D11CaptureFramePool const&, winrt::Windows::Foundation::IInspectable const&); com_ptr<ID3D11Device> m_device; com_ptr<ID3D11DeviceContext> m_context; IDirect3DDevice m_device_rt{ nullptr }; Direct3D11CaptureFramePool m_frame_pool{ nullptr }; GraphicsCaptureItem m_capture_item{ nullptr }; GraphicsCaptureSession m_capture_session{ nullptr }; Direct3D11CaptureFramePool::FrameArrived_revoker m_frame_arrived; Callback m_callback; }; |
GraphicsCapture.cpp
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 |
#include "GraphicsCapture.h" GraphicsCapture::GraphicsCapture() { // create device UINT flags = D3D11_CREATE_DEVICE_BGRA_SUPPORT; flags |= D3D11_CREATE_DEVICE_DEBUG; ::D3D11CreateDevice(nullptr, D3D_DRIVER_TYPE_HARDWARE, nullptr, flags, nullptr, 0, D3D11_SDK_VERSION, m_device.put(), nullptr, nullptr); m_device->GetImmediateContext(m_context.put()); auto dxgi = m_device.as<IDXGIDevice>(); com_ptr<::IInspectable> device_rt; ::CreateDirect3D11DeviceFromDXGIDevice(dxgi.get(), device_rt.put()); m_device_rt = device_rt.as<IDirect3DDevice>(); } GraphicsCapture::~GraphicsCapture() { stop(); } void GraphicsCapture::stop() { m_frame_arrived.revoke(); m_capture_session = nullptr; if (m_frame_pool) { m_frame_pool.Close(); m_frame_pool = nullptr; } m_capture_item = nullptr; m_callback = { }; } template<class CreateCaptureItem> bool GraphicsCapture::startImpl(const Callback& callback, const CreateCaptureItem& cci) { stop(); m_callback = callback; // create capture item auto factory = get_activation_factory<GraphicsCaptureItem>(); auto interop = factory.as<IGraphicsCaptureItemInterop>(); cci(interop); if (m_capture_item) { // create frame pool auto size = m_capture_item.Size(); m_frame_pool = Direct3D11CaptureFramePool::CreateFreeThreaded( m_device_rt, DirectXPixelFormat::B8G8R8A8UIntNormalized, 1, size); m_frame_arrived = m_frame_pool.FrameArrived( auto_revoke, { this, &GraphicsCapture::onFrameArrived }); // capture start m_capture_session = m_frame_pool.CreateCaptureSession(m_capture_item); m_capture_session.StartCapture(); return true; } else { return false; } } bool GraphicsCapture::start(HMONITOR hmon, const Callback& callback) { return startImpl(callback, [&](auto interop) { interop->CreateForMonitor(hmon, guid_of<ABI::Windows::Graphics::Capture::IGraphicsCaptureItem>(), put_abi(m_capture_item)); }); } void GraphicsCapture::onFrameArrived( winrt::Windows::Graphics::Capture::Direct3D11CaptureFramePool const& sender, winrt::Windows::Foundation::IInspectable const& args) { auto frame = sender.TryGetNextFrame(); auto size = frame.ContentSize(); com_ptr<ID3D11Texture2D> surface; frame.Surface().as<::Windows::Graphics::DirectX::Direct3D11::IDirect3DDxgiInterfaceAccess>() ->GetInterface(guid_of<ID3D11Texture2D>(), surface.put_void()); m_callback(surface.get(), size.Width, size.Height); } |
今回はモニタ指定でのキャプチャーとしていますが、GraphicsCapture::start() で呼び出す
IGraphicsCaptureItemInterop::CreateForMonitor() がモニタ指定となっています。
IGraphicsCaptureItemInterop::CreateForWindow() とすることでウィンドウ指定にもできます。

キャプチャの流れ
フレームの到着
Direct3D11CaptureFramePool のイベント FrameArrived にコールバックを登録することで、
フレームが来るたびにコールバックが呼び出されるようになります。
コールバックが呼ばれる仕組みとして、2通りの方式が用意されています。

①FramePool 作成元スレッドから呼ぶ方式
Direct3D11CaptureFramePool::Create() で作成すると、この方式になります。
この場合、フレームが来るのは DispatchMessage() の中になるため、
たとえ指定したウィンドウが無くても、メッセージループが必要になります。
②専用スレッドから呼ぶ方式
Direct3D11CaptureFramePool::CreateFreeThreaded() で作成すると、この方式になります。
この場合、キャプチャ用の専用スレッドが起こされ、そこからコールバックが呼び出されます。
FramePool の終了や破棄は、作成元スレッドで行う必要があるため、
CreateFreeThreaded() のコールバックから終了させることはできません。
フレームの読出し
FrameArrived のイベントに登録したコールバックが呼び出された後、
テクスチャを読み出す処理を記述します。
Utils.cpp の一部
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 |
bool ReadTexture(ID3D11Texture2D* tex, int width, int height, const std::function<void(void*, int)>& callback) { com_ptr<ID3D11Device> device; com_ptr<ID3D11DeviceContext> ctx; tex->GetDevice(device.put()); device->GetImmediateContext(ctx.put()); // create query com_ptr<ID3D11Query> query_event; { D3D11_QUERY_DESC qdesc = { D3D11_QUERY_EVENT , 0 }; device->CreateQuery(&qdesc, query_event.put()); } // create staging texture com_ptr<ID3D11Texture2D> staging; { D3D11_TEXTURE2D_DESC tmp; tex->GetDesc(&tmp); D3D11_TEXTURE2D_DESC desc{ (UINT)width, (UINT)height, 1, 1, tmp.Format, { 1, 0 }, D3D11_USAGE_STAGING, 0, D3D11_CPU_ACCESS_READ, 0 }; device->CreateTexture2D(&desc, nullptr, staging.put()); } // dispatch copy { D3D11_BOX box{ }; box.right = width; box.bottom = height; box.back = 1; ctx->CopySubresourceRegion(staging.get(), 0, 0, 0, 0, tex, 0, &box); ctx->End(query_event.get()); ctx->Flush(); } // wait for copy to complete int wait_count = 0; while (ctx->GetData(query_event.get(), nullptr, 0, 0) == S_FALSE) { ++wait_count; // just for debug } // map D3D11_MAPPED_SUBRESOURCE mapped{ }; if (SUCCEEDED(ctx->Map(staging.get(), 0, D3D11_MAP_READ, 0, &mapped))) { D3D11_TEXTURE2D_DESC desc{ }; staging->GetDesc(&desc); callback(mapped.pData, mapped.RowPitch); ctx->Unmap(staging.get(), 0); return true; } return false; } |

性能評価
以下のような処理を追加して、1フレームの読出しにかかる時間を計測してみました。
Utils.h
1 2 3 4 5 6 7 8 |
#pragma once #include "GraphicsCapture.h" #include <opencv2/opencv.hpp> bool ReadTexture(ID3D11Texture2D*, int, int, const std::function<void(void*, int)>&); cv::Mat BuildBitmapData(void*, int, int, int); |
Utils.cpp
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 |
#include "Utils.h" cv::Mat BuildBitmapData(void* data, int width, int height, int src_stride) { cv::Mat img(height, width, CV_8UC4, data); return img; } bool ReadTexture(ID3D11Texture2D* tex, int width, int height, const std::function<void(void*, int)>& callback) { com_ptr<ID3D11Device> device; com_ptr<ID3D11DeviceContext> ctx; tex->GetDevice(device.put()); device->GetImmediateContext(ctx.put()); // create query com_ptr<ID3D11Query> query_event; { D3D11_QUERY_DESC qdesc = { D3D11_QUERY_EVENT , 0 }; device->CreateQuery(&qdesc, query_event.put()); } // create staging texture com_ptr<ID3D11Texture2D> staging; { D3D11_TEXTURE2D_DESC tmp; tex->GetDesc(&tmp); D3D11_TEXTURE2D_DESC desc{ (UINT)width, (UINT)height, 1, 1, tmp.Format, { 1, 0 }, D3D11_USAGE_STAGING, 0, D3D11_CPU_ACCESS_READ, 0 }; device->CreateTexture2D(&desc, nullptr, staging.put()); } // dispatch copy { D3D11_BOX box{ }; box.right = width; box.bottom = height; box.back = 1; ctx->CopySubresourceRegion(staging.get(), 0, 0, 0, 0, tex, 0, &box); ctx->End(query_event.get()); ctx->Flush(); } // wait for copy to complete int wait_count = 0; while (ctx->GetData(query_event.get(), nullptr, 0, 0) == S_FALSE) { ++wait_count; // just for debug } // map D3D11_MAPPED_SUBRESOURCE mapped{ }; if (SUCCEEDED(ctx->Map(staging.get(), 0, D3D11_MAP_READ, 0, &mapped))) { D3D11_TEXTURE2D_DESC desc{ }; staging->GetDesc(&desc); callback(mapped.pData, mapped.RowPitch); ctx->Unmap(staging.get(), 0); return true; } return false; } |
Main.cpp
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 |
#include "GraphicsCapture.h" #include "Utils.h" #include <iostream> static uint64_t NowNS() { using namespace std::chrono; return duration_cast<nanoseconds>(steady_clock::now().time_since_epoch()).count(); } int main() { HMONITOR target = ::MonitorFromPoint({ 0, 0 }, MONITOR_DEFAULTTOPRIMARY); GraphicsCapture capture; std::mutex mutex; std::condition_variable cond; // called from capture thread auto callback = [&](ID3D11Texture2D* surface, int w, int h) { ReadTexture(surface, w, h, [&](void* data, int stride) { cv::Mat img = BuildBitmapData(data, w, h, stride); }); cond.notify_one(); }; capture.start(target, callback); while (1) { uint64_t begin = NowNS(); std::unique_lock<std::mutex> lock(mutex); cond.wait(lock); float elapsed = (NowNS() - begin) / 1000000.0; std::cout << "[O] Elapsed:" << elapsed << " [ms]" << std::endl; } // stop() must be called from the thread created GraphicsCapture capture.stop(); return 0; } |
コピーしたフレームを画像処理しやすい OpenCV の Mat 形式に変換した後、
コールバック内からメインスレッドへ通知を投げ、間隔を計測、表示するようにしています。
FHD 60Hz(=60 fps)の場合は、平均 16ms、
FHD 165Hz(=165 fps)の場合は、平均 6ms 程度でした。

ざっくり、実行環境は以下の通りです。
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 ほどでユーザ入力の処理をしているという
恐ろしい世界なんですね。性能を意識したプログラミングが強く求められるようです。
結局、何がしたかったのかというと、音ゲーで流れてきた譜面に従って操作/入力した後、
一番良い判定は、どのくらいのズレが許されているのかを確かめたかったのです。

60 fps のゲームのため、±1フレーム が許されているということが分かりました(笑)
参考