4
4

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

N.Mです。最近DirectXのテクスチャが別プロセスで共有できることを知りました。今回の記事はDirectXのテクスチャ共有の話です。

はじめに

最近、MediaFoundationの仮想カメラでキャプチャしたウィンドウを映すことを試行錯誤していました。

MediaFoundationの仮想カメラで、キャプチャしたウィンドウを映してみた

その中で、あるプロセスからキャプチャしたウィンドウのデータを別プロセスに送る必要がありました。また送る先のプロセスには以下のような条件がありました。

  • 送る先のプロセスはセッション0上にある
  • LocalServiceのアカウントで動くサービスプロセスである
  • LocalServiceの権限が弱いので、ファイルのやり取りはできなさそう
  • セッション0であるので、ウィンドウハンドルを送っても無効になる。そのため、画像データとして送る必要がある

この条件をクリアできるプロセス間のやりとりのために、いろいろ調査したところDirectXのテクスチャ共有にたどり着きました。

この記事では、以下のことにフォーカスしていきます。

  • テクスチャ共有でできること、メリット
  • テクスチャ共有の注意点

この記事ではDirectXの詳細(デバイスの作成など)は割愛します。

サンプル

NM_MFVCamSample_Result.gif

GitHub:NM_MFVCamSample

この記事で使用したサンプルを例に紹介していきます。アプリケーション側でキャプチャしたウィンドウの画像が、テクスチャとして仮想カメラに共有され、Windows標準のカメラアプリに映し出しています。このサンプルではDirectX11を使用しております。

テクスチャ共有でできること、メリット

  • 別のプロセスでそのテクスチャを使用できます。セッション0のプロセスでも使用できるようになります
  • テクスチャをメモリに見立てれば、画像に限らず、パラメタなどのデータのやり取りにも使用できます
  • 通常の権限でテクスチャ共有ができます。1

テクスチャ共有の流れ

送信元、送信先共通

あらかじめ、DirectXデバイス(ID3D11Device)やデバイスコンテキスト(ID3D11DeviceContext)をD3D11CreateDeviceで作成し、テクスチャを作れる状態にしておきます。

共有するテクスチャはGPU上にあり、CPUから直接アクセスすることはできません。CPU上のデータをやり取りするためには、CPUからアクセス可能なテクスチャを別途作成する必要があります。デバイスコンテキストはこれら2つのテクスチャのコピーに使用されます。CPUからアクセス可能なテクスチャの詳細は後述する注意点5に記載します。

送信元のプロセス

  1. DirectXデバイスのテクスチャID3D11Texture2Dを作成します
  2. ID3D11Texture2DQueryInterfaceメソッドでIDXGIResource1にします
  3. IDXGIResource1CreateSharedHandleを呼び出し、共有ハンドルを作成します

こんな感じです。

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を使用すると、共有ハンドルからテクスチャを得ることができます。OpenSharedResourceByNameID3D11Device1のメソッドであるので、QueryInterfeceID3D11Deviceから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()));

テクスチャ共有の注意点

注意点としては、以下が挙げられます。

  1. テクスチャ作成時のフラグ設定
  2. 共有ハンドル作成時のアクセス権設定(セッション0限定)
  3. 共有ハンドル作成時の名前設定(セッション0限定)
  4. 共有ハンドルの排他処理
  5. データの入力、取得
  6. 送信元プロセス、送信先プロセスで使用するGPUの一致

注意点1: テクスチャ作成時のフラグ設定

共有するテクスチャは、以下のようにD3D11_TEXTURE2D_DESCでフラグを設定する必要があります。

  1. CPUAccessFlags... 0に設定(CPUからアクセスできないようにする必要があります)
  2. MiscFlags... D3D11_RESOURCE_MISC_SHARED_NTHANDLE | D3D11_RESOURCE_MISC_SHARED_KEYEDMUTEXに設定
  3. Usage... D3D11_USAGE_DEFAULTに設定
NM_CaptureWindow/NM_CaptureWindow.cpp
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でも同じようにプロセス間で共有できないか試しましたが、MiscFlagsD3D11_RESOURCE_MISC_SHARED_NTHANDLE | D3D11_RESOURCE_MISC_SHARED_KEYEDMUTEXを指定すると、バッファ作成時に「引数が不正である」というエラーが発生してしまいました。

注意点2: 共有ハンドル作成時のアクセス権設定

セッション0のプロセスにテクスチャを共有する場合は、アクセス権の設定をしておく必要があります。 アクセス権を設定しておかないと、セッション0のプロセスでテクスチャを取得する際に0x8876086aという謎のコードのエラーが発生します。2

このドキュメントを参考に、以下のようにSECURITY_ATTRIBUTESを設定します。3

TEXT("D:(A;;GR;;;LS)")というSDDL文字列でLocalServiceに読み取りのみのアクセス権を付与する設定になります。この書き方はこの記事が参考になりました。

NM_CaptureWindow/NM_CaptureWindow.cpp
// 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\をつける必要があると書かれております。

NM_CaptureWindow/NM_CaptureWindow.cpp
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に配置されます。

WinObj.png

注意点4: 共有ハンドルの排他処理

テクスチャ作成時、MiscFlagsD3D11_RESOURCE_MISC_SHARED_KEYEDMUTEXを設定しているので、テクスチャから排他処理のためのオブジェクトIDXGIKeyedMutexを取得できます。このオブジェクトで排他処理しないと共有するテクスチャにコピーする際、エラーが発生し、コピーできません。

IDXGIKeyedMutex インターフェイス (dxgi.h)

共有ハンドル作成後にIDXGIKeyedMutexで使用するキーを設定します。AcquireSyncの第1引数にキーを入力し、処理が通るとそのテクスチャを占有できます。最初のキーの値は0であり、その後は最後に呼ばれたReleaseSyncの引数に与えられた値がキーとなります。4 5 ReleaseSyncを呼ぶと占有を解除できます。

NM_CaptureWindow/NM_CaptureWindow.cpp
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でテクスチャを占有し、使い終わったら占有を解除します。

NM_CaptureWindow/NM_CaptureWindow.cpp
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に設定
NM_CaptureWindow/NM_CaptureWindow.cpp
// 説明のために一部変更しております。
// _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分ずらしたところに書き込む必要があります。

NM_CaptureWindow/NM_CaptureWindow.cpp
//_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に設定
VCamSampleSource/FrameGenerator.cpp
// _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分ずらしたところに書き込みます。

VCamSampleSource/FrameGenerator.cpp
// _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とかでもできるのかな?)

今回の自分のケースに限らず、ローカルでの機械学習のデータ入力など、テクスチャ共有はいろいろな用途に使用できそうかなと思います。

  1. プロセス間でデータを共有するのに、名前付き共有メモリというものもあります。しかし、こちらは管理者権限でアプリケーションを実行しないとセッション0のプロセスと共有できるメモリを作成できません。このドキュメントのようにC++から特権の設定ができるようですが、通常の権限からだとそもそも特権を取得できず、設定できませんでした。

  2. 自分はこのエラーに1か月くらいはまりました...

  3. SetSecurityDescriptorDaclでアクセス権限の設定が空のものをSECURITY_ATTRIBUTESlpSecurityDescriptorに設定することでも対応できるようです。しかし、全てのユーザにアクセス権を与える状態になっており、セキュリティ的に危険であるので、ちゃんとアクセス権を設定してあげましょう。

  4. 別のキーを与えると、そのキーでReleaseSyncが呼ばれるまで待機します。これを利用してプロセスごとに引数として与えるキーを変えておき、処理される順を制御するといったこともできるようです。今回は単純な排他処理ができれば良いので、同じキーの値を使用しております。

  5. 第2引数は待ち時間を指定できます。INFINITEと指定するとロックが取得できるまで待機できます。自分は間違えてINFINITYと入力してしまいうまく行かなかったので、気を付けましょう。

  6. データ取得時のように、IDXGISurfaceでMapメソッドを呼ぶと「引数が不正である」といったエラーが発生します。

  7. 基本的にはデフォルト設定でどちらかに固定しておけば大丈夫です。しかし、自分の環境では、デフォルトでNVIDIAさんのGeForceを使うよう設定したのにも関わらず、一部のアプリケーションがIntelさんのオンチップGPUを使っており、個別に使用するGPUを設定しないと失敗するケースがありました。

4
4
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
4
4

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?