N.Mです。最近DirectXのテクスチャが別プロセスで共有できることを知りました。今回の記事はDirectXのテクスチャ共有の話です。
はじめに
最近、MediaFoundationの仮想カメラでキャプチャしたウィンドウを映すことを試行錯誤していました。
MediaFoundationの仮想カメラで、キャプチャしたウィンドウを映してみた
その中で、あるプロセスからキャプチャしたウィンドウのデータを別プロセスに送る必要がありました。また送る先のプロセスには以下のような条件がありました。
- 送る先のプロセスはセッション0上にある
- LocalServiceのアカウントで動くサービスプロセスである
- LocalServiceの権限が弱いので、ファイルのやり取りはできなさそう
- セッション0であるので、ウィンドウハンドルを送っても無効になる。そのため、画像データとして送る必要がある
この条件をクリアできるプロセス間のやりとりのために、いろいろ調査したところDirectXのテクスチャ共有にたどり着きました。
この記事では、以下のことにフォーカスしていきます。
- テクスチャ共有でできること、メリット
- テクスチャ共有の注意点
この記事ではDirectXの詳細(デバイスの作成など)は割愛します。
サンプル
GitHub:NM_MFVCamSample
この記事で使用したサンプルを例に紹介していきます。アプリケーション側でキャプチャしたウィンドウの画像が、テクスチャとして仮想カメラに共有され、Windows標準のカメラアプリに映し出しています。このサンプルではDirectX11を使用しております。
テクスチャ共有でできること、メリット
- 別のプロセスでそのテクスチャを使用できます。セッション0のプロセスでも使用できるようになります
- テクスチャをメモリに見立てれば、画像に限らず、パラメタなどのデータのやり取りにも使用できます
- 通常の権限でテクスチャ共有ができます。1
テクスチャ共有の流れ
送信元、送信先共通
あらかじめ、DirectXデバイス(ID3D11Device
)やデバイスコンテキスト(ID3D11DeviceContext
)をD3D11CreateDevice
で作成し、テクスチャを作れる状態にしておきます。
共有するテクスチャはGPU上にあり、CPUから直接アクセスすることはできません。CPU上のデータをやり取りするためには、CPUからアクセス可能なテクスチャを別途作成する必要があります。デバイスコンテキストはこれら2つのテクスチャのコピーに使用されます。CPUからアクセス可能なテクスチャの詳細は後述する注意点5に記載します。
送信元のプロセス
- DirectXデバイスのテクスチャ
ID3D11Texture2D
を作成します -
ID3D11Texture2D
のQueryInterface
メソッドでIDXGIResource1
にします -
IDXGIResource1
のCreateSharedHandle
を呼び出し、共有ハンドルを作成します
こんな感じです。
com_ptr<ID3D11Device> device;
// 略: D3D11CreateDeviceでDirectXデバイスやデバイスコンテキストを作る。
com_ptr<ID3D11Texture2D> sharedTexture;
D3D11_TEXTURE2D_DESC sharedTextureDesc;
// 略: 共有するテクスチャsharedTextureのフラグ設定(注意点1)
// 1. DirectXデバイスのテクスチャを作成します
device->CreateTexture2D(&sharedTextureDesc, 0, sharedTexture.put());
// 2. ID3D11Texture2DをIDXGIResource1にします
com_ptr<IDXGIResource1> sharedTextureResource;
sharedTexture->QueryInterface(IID_PPV_ARGS(sharedTextureResource.put()));
SECURITY_ATTRIBUTES secAttr;
// 略: 共有ハンドルのアクセス権設定(注意点2)
// 3. IDXGIResource1のCreateSharedHandleを呼び出し、共有ハンドルを作成します
HANDLE sharedTextureHandle;
sharedTextureResource->CreateSharedHandle(&secAttr,
DXGI_SHARED_RESOURCE_READ,
TEXT("Global\\hogehoge"), // 注意点3
&sharedTextureHandle);
// 共有ハンドルの排他処理(注意点4)
com_ptr<IDXGIKeyedMutex> mutex;
sharedTexture.as(mutex);
mutex->AcquireSync(0, INFINITE);
mutex->ReleaseSync(1234);
送信先のプロセス
DirectXデバイスにあるメソッドOpenSharedResourceByName
を使用すると、共有ハンドルからテクスチャを得ることができます。OpenSharedResourceByName
はID3D11Device1
のメソッドであるので、QueryInterfece
でID3D11Device
からID3D11Device1
に変換しておく必要があります。
com_ptr<ID3D11Device> device;
// 略: D3D11CreateDeviceでDirectXデバイスやデバイスコンテキストを作る。
com_ptr<ID3D11Device1> device1;
device->QueryInterface(IID_PPV_ARGS(device1.put()));
com_ptr<ID3D11Texture2D> sharedTexture;
device1->OpenSharedResourceByName(TEXT("Global\\hogehoge"), // 注意点3
DXGI_SHARED_RESOURCE_READ, IID_PPV_ARGS(sharedTexture.put()));
テクスチャ共有の注意点
注意点としては、以下が挙げられます。
- テクスチャ作成時のフラグ設定
- 共有ハンドル作成時のアクセス権設定(セッション0限定)
- 共有ハンドル作成時の名前設定(セッション0限定)
- 共有ハンドルの排他処理
- データの入力、取得
- 送信元プロセス、送信先プロセスで使用するGPUの一致
注意点1: テクスチャ作成時のフラグ設定
共有するテクスチャは、以下のようにD3D11_TEXTURE2D_DESC
でフラグを設定する必要があります。
-
CPUAccessFlags
... 0に設定(CPUからアクセスできないようにする必要があります) -
MiscFlags
...D3D11_RESOURCE_MISC_SHARED_NTHANDLE | D3D11_RESOURCE_MISC_SHARED_KEYEDMUTEX
に設定 -
Usage
...D3D11_USAGE_DEFAULT
に設定
D3D11_TEXTURE2D_DESC bufferTextureDesc;
bufferTextureDesc.Width = MAX_SOURCE_WIDTH;
bufferTextureDesc.Height = MAX_SOURCE_HEIGHT;
bufferTextureDesc.Format = DXGI_FORMAT_B8G8R8A8_UNORM;
bufferTextureDesc.ArraySize = 1;
bufferTextureDesc.BindFlags = D3D11_BIND_SHADER_RESOURCE;
bufferTextureDesc.CPUAccessFlags = 0; // 説明1
bufferTextureDesc.MipLevels = 1;
bufferTextureDesc.MiscFlags
= D3D11_RESOURCE_MISC_SHARED_NTHANDLE | D3D11_RESOURCE_MISC_SHARED_KEYEDMUTEX; // 説明2
bufferTextureDesc.SampleDesc.Count = 1;
bufferTextureDesc.SampleDesc.Quality = 0;
bufferTextureDesc.Usage = D3D11_USAGE_DEFAULT; // 説明3
Tips: ID3D11Buffer
頂点バッファなどで使用されるID3D11Buffer
でも同じようにプロセス間で共有できないか試しましたが、MiscFlags
で D3D11_RESOURCE_MISC_SHARED_NTHANDLE | D3D11_RESOURCE_MISC_SHARED_KEYEDMUTEX
を指定すると、バッファ作成時に「引数が不正である」というエラーが発生してしまいました。
注意点2: 共有ハンドル作成時のアクセス権設定
セッション0のプロセスにテクスチャを共有する場合は、アクセス権の設定をしておく必要があります。 アクセス権を設定しておかないと、セッション0のプロセスでテクスチャを取得する際に0x8876086a
という謎のコードのエラーが発生します。2
このドキュメントを参考に、以下のようにSECURITY_ATTRIBUTES
を設定します。3
TEXT("D:(A;;GR;;;LS)")
というSDDL文字列でLocalServiceに読み取りのみのアクセス権を付与する設定になります。この書き方はこの記事が参考になりました。
// Session0であるFrameServerで共有テクスチャにアクセスできるようにするため、
// SECURITY_ATTRIBUTESを設定する必要がある。
SECURITY_ATTRIBUTES secAttr;
secAttr.nLength = sizeof(SECURITY_ATTRIBUTES);
secAttr.bInheritHandle = FALSE;
secAttr.lpSecurityDescriptor = NULL;
// Local Serviceにのみ読み取りのアクセス権(今回のFrameServerのパイプラインならこれで十分)
// Interactive User(現在のユーザ)に全アクセス権を付与するなら(A;;GA;;;IU)を追加する。
if (ConvertStringSecurityDescriptorToSecurityDescriptor(
TEXT("D:(A;;GR;;;LS)"),
SDDL_REVISION_1, &(secAttr.lpSecurityDescriptor), NULL)) {
HRESULT hr = sharedCaptureWindowResource->CreateSharedHandle(&secAttr,
DXGI_SHARED_RESOURCE_READ,
TEXT("Global\\NM_Capture_Window"),
&_sharedCaptureWindowHandle);
// 略
}
注意点3: 共有ハンドル作成時の名前設定
セッション0のプロセスにテクスチャを共有する場合は、共有ハンドルの名前の前にGlobal\
をつける必要があります。
古い英語の資料ですが、この資料のPotential Issuesのあたりに、Global\
をつける必要があると書かれております。
HRESULT hr = sharedCaptureWindowResource->CreateSharedHandle(&secAttr,
DXGI_SHARED_RESOURCE_READ,
TEXT("Global\\NM_Capture_Window"), // <- これ
&_sharedCaptureWindowHandle);
Tips: WinObj
実際にテクスチャの共有ハンドルが作られているかどうかは、Microsoftが公式で出しているWinObjというアプリケーションで確認できます。
今回のサンプル実行中にWinObjで見てみると、BaseNamedObjects
のところに、共有しているテクスチャのハンドルNM_Capture_Window
が表示されております。Global\
で指定しているため、このBaseNamedObjects
に配置されます。
注意点4: 共有ハンドルの排他処理
テクスチャ作成時、MiscFlags
にD3D11_RESOURCE_MISC_SHARED_KEYEDMUTEX
を設定しているので、テクスチャから排他処理のためのオブジェクトIDXGIKeyedMutex
を取得できます。このオブジェクトで排他処理しないと共有するテクスチャにコピーする際、エラーが発生し、コピーできません。
IDXGIKeyedMutex インターフェイス (dxgi.h)
共有ハンドル作成後にIDXGIKeyedMutex
で使用するキーを設定します。AcquireSync
の第1引数にキーを入力し、処理が通るとそのテクスチャを占有できます。最初のキーの値は0であり、その後は最後に呼ばれたReleaseSync
の引数に与えられた値がキーとなります。4 5 ReleaseSync
を呼ぶと占有を解除できます。
HRESULT hr = sharedCaptureWindowResource->CreateSharedHandle(&secAttr,
DXGI_SHARED_RESOURCE_READ,
TEXT("Global\\NM_Capture_Window"),
&_sharedCaptureWindowHandle);
com_ptr<IDXGIKeyedMutex> mutex;
_bufferTextureForCapture.as(mutex);
mutex->AcquireSync(0, INFINITE);
mutex->ReleaseSync(MUTEX_KEY);
コピーする場合など共有しているテクスチャに触れる場合は、このIDXGIKeyedMutex
でテクスチャを占有し、使い終わったら占有を解除します。
com_ptr<ID3D11Texture2D> texture2D = getDXGIInterfaceFromObject<::ID3D11Texture2D>(frame.Surface());
com_ptr<IDXGIKeyedMutex> mutex;
_bufferTextureForCapture.as(mutex);
mutex->AcquireSync(MUTEX_KEY, INFINITE); // <- ここで共有しているテクスチャ_bufferTextureForCaptureを占有
_deviceCtxForCapture->CopySubresourceRegion(_bufferTextureForCapture.get(), 0, 0, 0, 0, texture2D.get(), 0, &_capWinSizeInTexture);
_deviceCtxForCapture->Flush();
mutex->ReleaseSync(MUTEX_KEY); // <- ここでテクスチャの占有を解除
注意点5: データの入力、取得
先ほども触れたとおり、共有されたテクスチャはGPU上にあり、CPUからはアクセスできないので、CPUからアクセスできるテクスチャもあらかじめ作成しておく必要があります。共有されるテクスチャにデータを入力するか、取得するかでCPUからアクセス可能なテクスチャの設定が異なります。
共有するテクスチャにデータを入力する場合
CPUからGPUにデータを送れるようにテクスチャを設定する必要があります。特に以下のフラグを設定する必要があります。
-
CPUAccessFlags
...D3D11_CPU_ACCESS_WRITE
に設定 -
MiscFlags
... 0に設定 -
Usage
...D3D11_USAGE_DYNAMIC
に設定
// 説明のために一部変更しております。
// _cpuBufferParamsがCPUからアクセスできるテクスチャです。
bufferParamsDesc.Width = SHARED_PARAMS_BUF;
bufferParamsDesc.Height = SHARED_PARAMS_BUF;
bufferParamsDesc.Format = DXGI_FORMAT_B8G8R8A8_UNORM;
bufferParamsDesc.ArraySize = 1;
bufferParamsDesc.BindFlags = D3D11_BIND_SHADER_RESOURCE;
bufferParamsDesc.MipLevels = 1;
bufferParamsDesc.SampleDesc.Count = 1;
bufferParamsDesc.SampleDesc.Quality = 0;
bufferParamsDesc.CPUAccessFlags = D3D11_CPU_ACCESS_WRITE;
bufferParamsDesc.MiscFlags = 0;
bufferParamsDesc.Usage = D3D11_USAGE_DYNAMIC;
check_hresult(_d3dDevice->CreateTexture2D(&bufferParamsDesc, NULL, _cpuBufferParams.put()));
データを入力する際は、デバイスコンテキストのMap
メソッドでCPUからアクセス可能なテクスチャをCPU上のメモリにマッピングします。 6 データを書き込んだらUnmap
を呼び出し、マッピングを解除します。その後、CPUからアクセス可能なテクスチャを共有するテクスチャにコピーします。
今回はテクスチャの1行目のピクセルにあたる場所のみに書き込んでいるので、((char*)mapFromTexture.pData + memIdx)
という感じで書き込む場所を指定していますが、別の行の場所に書き込む場合は、行数×mapFromTexture.RowPitch
分ずらしたところに書き込む必要があります。
//_bufferParamsが共有するテクスチャです。
void NM_CaptureWindow::UpdateSharedParams() {
if (_bufferParams == nullptr) {
return;
}
D3D11_MAPPED_SUBRESOURCE mapFromTexture;
_deviceCtxForCapture->Map(_cpuBufferParams.get(), 0,
D3D11_MAP_WRITE_DISCARD, 0, &mapFromTexture);
int memIdx = 0;
CopyMemory((PVOID)((char*)mapFromTexture.pData + memIdx), (PVOID)(&_capWinSize.Width), sizeof(int32_t));
memIdx += sizeof(int32_t);
CopyMemory((PVOID)((char*)mapFromTexture.pData + memIdx), (PVOID)(&_capWinSize.Height), sizeof(int32_t));
memIdx += sizeof(int32_t);
_deviceCtxForCapture->Unmap(_cpuBufferParams.get(), 0);
com_ptr<IDXGIKeyedMutex> mutex;
_bufferParams.as(mutex);
mutex->AcquireSync(MUTEX_KEY, INFINITE);
_deviceCtxForCapture->CopyResource(_bufferParams.get(), _cpuBufferParams.get());
mutex->ReleaseSync(MUTEX_KEY);
}
共有するテクスチャからデータを取得する場合
入力のときと逆でGPUからCPUにデータを送れるようにテクスチャを設定する必要があります。
-
CPUAccessFlags
...D3D11_CPU_ACCESS_READ
に設定 -
MiscFlags
... 0に設定 -
Usage
...D3D11_USAGE_STAGING
に設定
// _cpuParamsTextureがCPUからアクセスできるテクスチャです。
void FrameGenerator::CreateSharedParamsTexture()
{
// 略
D3D11_TEXTURE2D_DESC bufferTextureDesc;
bufferTextureDesc.Width = SHARED_PARAMS_BUF;
bufferTextureDesc.Height = SHARED_PARAMS_BUF;
bufferTextureDesc.Format = DXGI_FORMAT_B8G8R8A8_UNORM;
bufferTextureDesc.ArraySize = 1;
bufferTextureDesc.BindFlags = 0;
bufferTextureDesc.MipLevels = 1;
bufferTextureDesc.SampleDesc.Count = 1;
bufferTextureDesc.SampleDesc.Quality = 0;
bufferTextureDesc.CPUAccessFlags = D3D11_CPU_ACCESS_READ;
bufferTextureDesc.MiscFlags = 0;
bufferTextureDesc.Usage = D3D11_USAGE_STAGING;
check_hresult(_textureDevice->CreateTexture2D(&bufferTextureDesc, 0, _cpuParamsTexture.put()));
// 略
}
データを取得する際は、まず共有テクスチャから、CPUからアクセス可能なテクスチャにコピーします。その後、CPUからアクセス可能なテクスチャをIDXGISurface
に変換し、Map
メソッドでCPU上のメモリにマッピングします。データ取得後はUnmap
でマッピングを解除します。
データ入力時と同様に、別の行の場所に書き込む場合は、行数×mapFromTexture.Pitch
分ずらしたところに書き込みます。
// _sharedParamsTextureは共有されているテクスチャです。
void FrameGenerator::GetParamsFromTexture()
{
if (_sharedParamsTexture == nullptr || _cpuParamsTexture == nullptr)
{
return;
}
com_ptr<IDXGIKeyedMutex> mutex;
_sharedParamsTexture.as(mutex);
mutex->AcquireSync(MUTEX_KEY, INFINITE);
_textureDeviceContext->CopyResource(_cpuParamsTexture.get(), _sharedParamsTexture.get());
mutex->ReleaseSync(MUTEX_KEY);
com_ptr<IDXGISurface> dxgiSurface;
_cpuParamsTexture->QueryInterface(IID_PPV_ARGS(dxgiSurface.put()));
DXGI_MAPPED_RECT mapFromTexture;
dxgiSurface->Map(&mapFromTexture, DXGI_MAP_READ);
int memIdx = 0;
CopyMemory((PVOID)&_captureWindowWidth, (PVOID)(mapFromTexture.pBits + memIdx), sizeof(int));
memIdx += sizeof(int32_t);
CopyMemory((PVOID)&_captureWindowHeight, (PVOID)(mapFromTexture.pBits + memIdx), sizeof(int));
memIdx += sizeof(int32_t);
dxgiSurface->Unmap();
}
注意点6: 送信元プロセス、送信先プロセスで使用するGPUの一致
それはそうと思うかもしれませんが、送信元プロセスと送信先プロセスで同じGPUを使用している必要があります。違うGPUを使用している場合は、送信先のプロセスでOpenSharedResourceByName
を呼び出したところで、「引数が不正である」(E_INVALIDARG
)といったエラーが返ってきます。
特にゲーミングPCだと、IntelさんのオンチップGPUとNVIDIAさんのGeForceの2つのGPUが載っているので、この点に注意が必要になります。7
まとめ
DirectXでプロセス間通信みたいなことができるのは驚きでした。(OpenGLとかでもできるのかな?)
今回の自分のケースに限らず、ローカルでの機械学習のデータ入力など、テクスチャ共有はいろいろな用途に使用できそうかなと思います。
-
プロセス間でデータを共有するのに、名前付き共有メモリというものもあります。しかし、こちらは管理者権限でアプリケーションを実行しないとセッション0のプロセスと共有できるメモリを作成できません。このドキュメントのようにC++から特権の設定ができるようですが、通常の権限からだとそもそも特権を取得できず、設定できませんでした。 ↩
-
自分はこのエラーに1か月くらいはまりました... ↩
-
SetSecurityDescriptorDacl
でアクセス権限の設定が空のものをSECURITY_ATTRIBUTES
のlpSecurityDescriptor
に設定することでも対応できるようです。しかし、全てのユーザにアクセス権を与える状態になっており、セキュリティ的に危険であるので、ちゃんとアクセス権を設定してあげましょう。 ↩ -
別のキーを与えると、そのキーで
ReleaseSync
が呼ばれるまで待機します。これを利用してプロセスごとに引数として与えるキーを変えておき、処理される順を制御するといったこともできるようです。今回は単純な排他処理ができれば良いので、同じキーの値を使用しております。 ↩ -
第2引数は待ち時間を指定できます。
INFINITE
と指定するとロックが取得できるまで待機できます。自分は間違えてINFINITY
と入力してしまいうまく行かなかったので、気を付けましょう。 ↩ -
データ取得時のように、IDXGISurfaceでMapメソッドを呼ぶと「引数が不正である」といったエラーが発生します。 ↩
-
基本的にはデフォルト設定でどちらかに固定しておけば大丈夫です。しかし、自分の環境では、デフォルトでNVIDIAさんのGeForceを使うよう設定したのにも関わらず、一部のアプリケーションがIntelさんのオンチップGPUを使っており、個別に使用するGPUを設定しないと失敗するケースがありました。 ↩