DirectXでテクスチャを別プロセスに共有する


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

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

[MediaFoundationの仮想カメラで、キャプチャしたウィンドウを映してみた](https://qiita.com/HexagramNM/items/52f5442f26dce89d3502)

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

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

この記事では、以下のことにフォーカスしていきます。
- **テクスチャ共有でできること、メリット**
- **テクスチャ共有の注意点**

::: note warn
この記事ではDirectXの詳細（デバイスの作成など）は割愛します。
:::


# サンプル
![NM_MFVCamSample_Result.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/152791/3a33ec0c-8e36-06fc-89fe-f3e4de63269f.gif)

GitHub：[NM_MFVCamSample](https://github.com/HexagramNM/NM_MFVcamSample)

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


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

[^1]: プロセス間でデータを共有するのに、名前付き共有メモリというものもあります。しかし、こちらは管理者権限でアプリケーションを実行しないとセッション0のプロセスと共有できるメモリを作成できません。[このドキュメント](https://learn.microsoft.com/ja-jp/windows/win32/secauthz/enabling-and-disabling-privileges-in-c--)のようにC++から特権の設定ができるようですが、通常の権限からだとそもそも特権を取得できず、設定できませんでした。


# テクスチャ共有の流れ

## 送信元、送信先共通
あらかじめ、DirectXデバイス（`ID3D11Device`）やデバイスコンテキスト（`ID3D11DeviceContext`）を`D3D11CreateDevice`で作成し、テクスチャを作れる状態にしておきます。

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

## 送信元のプロセス
1. DirectXデバイスのテクスチャ`ID3D11Texture2D`を作成します
2. `ID3D11Texture2D`の`QueryInterface`メソッドで`IDXGIResource1`にします
3. `IDXGIResource1`の`CreateSharedHandle`を呼び出し、共有ハンドルを作成します

こんな感じです。
```C++:
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`に変換しておく必要があります。

```C++:
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`に設定

```C++: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`でも同じようにプロセス間で共有できないか試しましたが、`MiscFlags`で `D3D11_RESOURCE_MISC_SHARED_NTHANDLE | D3D11_RESOURCE_MISC_SHARED_KEYEDMUTEX`を指定すると、バッファ作成時に「引数が不正である」というエラーが発生してしまいました。

## 注意点2: 共有ハンドル作成時のアクセス権設定
**セッション0のプロセスにテクスチャを共有する場合は、アクセス権の設定をしておく必要があります。** アクセス権を設定しておかないと、セッション0のプロセスでテクスチャを取得する際に`0x8876086a`という謎のコードのエラーが発生します。[^2]

[このドキュメント](https://learn.microsoft.com/ja-jp/windows-hardware/drivers/image/wia-security-and-security-descriptors)を参考に、以下のように`SECURITY_ATTRIBUTES`を設定します。[^3]

`TEXT("D:(A;;GR;;;LS)")`というSDDL文字列でLocalServiceに読み取りのみのアクセス権を付与する設定になります。この書き方は[この記事](https://atmarkit.itmedia.co.jp/ait/articles/0603/25/news016.html)が参考になりました。

```C++: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);

  // 略
}
```

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

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

## 注意点3: 共有ハンドル作成時の名前設定
**セッション0のプロセスにテクスチャを共有する場合は、共有ハンドルの名前の前に`Global\`をつける必要があります。**

古い英語の資料ですが、[この資料](https://www.coretechnologies.com/WindowsServices/Microsoft-Impact-of-Session-0-Isolation-on-Services-and-Drivers-in-Windows-Vista.pdf)のPotential Issuesのあたりに、`Global\`をつける必要があると書かれております。

```C++:NM_CaptureWindow/NM_CaptureWindow.cpp
HRESULT hr = sharedCaptureWindowResource->CreateSharedHandle(&secAttr,
  DXGI_SHARED_RESOURCE_READ,
  TEXT("Global\\NM_Capture_Window"), // <- これ
  &_sharedCaptureWindowHandle);
```

### Tips: WinObj
実際にテクスチャの共有ハンドルが作られているかどうかは、Microsoftが公式で出している[WinObj](https://learn.microsoft.com/ja-jp/sysinternals/downloads/winobj)というアプリケーションで確認できます。

今回のサンプル実行中にWinObjで見てみると、`BaseNamedObjects`のところに、共有しているテクスチャのハンドル`NM_Capture_Window`が表示されております。`Global\`で指定しているため、この`BaseNamedObjects`に配置されます。

![WinObj.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/152791/c3dc69da-dc82-1864-9248-218d17debcd6.png)

## 注意点4: 共有ハンドルの排他処理
テクスチャ作成時、`MiscFlags`に`D3D11_RESOURCE_MISC_SHARED_KEYEDMUTEX`を設定しているので、テクスチャから排他処理のためのオブジェクト`IDXGIKeyedMutex`を取得できます。このオブジェクトで排他処理しないと共有するテクスチャにコピーする際、エラーが発生し、コピーできません。

[IDXGIKeyedMutex インターフェイス (dxgi.h)](https://learn.microsoft.com/ja-jp/windows/win32/api/dxgi/nn-dxgi-idxgikeyedmutex)

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

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

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

```C++: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`でテクスチャを占有し、使い終わったら占有を解除します。

```C++: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`に設定

```C++: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からアクセス可能なテクスチャを共有するテクスチャにコピーします。

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

::: note warn
今回はテクスチャの1行目のピクセルにあたる場所のみに書き込んでいるので、`((char*)mapFromTexture.pData + memIdx)`という感じで書き込む場所を指定していますが、別の行の場所に書き込む場合は、行数×`mapFromTexture.RowPitch`分ずらしたところに書き込む必要があります。
:::

```C++: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`に設定

```C++: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`でマッピングを解除します。

::: note warn
データ入力時と同様に、別の行の場所に書き込む場合は、行数×`mapFromTexture.Pitch`分ずらしたところに書き込みます。
:::

```C++: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のデバイスを適切に選択することで対応ができます。詳しくは[補足記事：共有テクスチャ取得用として適切なDirectXデバイスを検索する](https://qiita.com/HexagramNM/items/7d70f12dd1f04a32abb3)をご覧ください。

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


# まとめ
DirectXでプロセス間通信みたいなことができるのは驚きでした。（OpenGLとかでもできるのかな？）

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