共有テクスチャ取得用として適切なDirectXデバイスを検索する

::: note
以前書いた記事：[DirectXでテクスチャを別プロセスに共有する](https://qiita.com/HexagramNM/items/5fa74f59f0b7bb4b80e9)の補足記事です。
:::

N.Mです。以前の記事でDirectXのテクスチャを共有した際、注意点として以下のことを挙げました。

> それはそうと思うかもしれませんが、送信元プロセスと送信先プロセスで同じGPUを使用している必要があります。違うGPUを使用している場合は、送信先のプロセスで`OpenSharedResourceByName`を呼び出したところで、「引数が不正である」(`E_INVALIDARG`)といったエラーが返ってきます。

複数のGPUを積んでいるPCで起こりうる問題ですが、実際のところWindows側で優先するGPUを自動選択する設定もできるので、この部分を対策しないと一部の環境に対応できないことがわかりました。[^1]

[^1]: NVIDIAコントロールパネルとかで一括で優先するGPUを設定しても、アプリケーションごとに優先するGPUが異なるケースもあり、ユーザに設定を見直すように指示するのは厳しいと思います。

ウィンドウをキャプチャし、仮想カメラに送る自作ツール: [NM_WindowCaptureVirtualCamera](https://github.com/HexagramNM/NM_WindowCaptureVirtualCamera)[^2]でも修正対応を行ったので、その対応を紹介したいと思います。

[^2]: これまで書いてきた仮想カメラやウィンドウキャプチャの知見をまとめた集大成として開発し、2025年1月ごろにリリースしました。散らかってしまった知見をまとめた[GitHub wiki](https://github.com/HexagramNM/NM_WindowCaptureVirtualCamera/wiki)も書いてみたので、ぜひご覧ください。


# 対応
共有テクスチャを受け取る送信先のプロセスでDirectXのデバイスを作成する際に、以下を行いました。

1. 一度使用可能なすべてのGPUに対してデバイスを走査する
2. デバイスを一つずつ見ていき、共有テクスチャの取得に成功するか確認する。取得に成功したデバイスを以降の処理で使用するようにする

::: note
共有テクスチャを作る送信元のプロセスは、`D3D11CreateDevice`でデフォルトのデバイスを作成すれば大丈夫です。
:::

1のすべてのGPUを走査する方法ですが、dxgiの機能を利用すれば実現できました。その際に、以下の記事を参考にしています。

- [アダプターを列挙する](https://learn.microsoft.com/ja-jp/windows/win32/direct3d11/overviews-direct3d-11-devices-enum)
- [DirectX11入門編 #1　初期化・ポリゴン表示](https://qiita.com/harayuu10/items/3ae4de5fe7fb19847152)


# サンプルコード
::: warning
dxgiの機能を使用するため、以下の対応が必要です
- `dxgi.h`のインクルード
- `dxgi.lib`のリンク（`#pragma comment(lib, "dxgi.lib")`）
:::

```C++
// winrt::com_ptrでポインタを管理することを想定しています。
// 引数についてはcom_ptrのputメソッドで取得できるダブルポインタを渡すことを想定しています。

void findDeviceAndGetSharedTexture(ID3D11Device1** i_dxDevice,
    ID3D11DeviceContext** i_dxDeviceContext
    ID3D11Texture2D** i_sharedTexture)
{
    // 共有テクスチャを取得できるデバイスを探し、見つかればテクスチャやデバイスコンテキストを作成
    UINT createDeviceFlags = D3D11_CREATE_DEVICE_BGRA_SUPPORT;
#ifdef _DEBUG
    createDeviceFlags |= D3D11_CREATE_DEVICE_DEBUG;
#endif

    com_ptr<IDXGIFactory1> factory(nullptr);
    com_ptr<IDXGIAdapter1> adapter(nullptr);
    com_ptr<ID3D11Device> device(nullptr);
    D3D_FEATURE_LEVEL d3dFeatures[7] = {
        D3D_FEATURE_LEVEL_11_1
    };

    UINT adapterIdx = 0;
    HRESULT hr = S_OK;
    if (CreateDXGIFactory1(IID_PPV_ARGS(factory.put())) != S_OK)
    {
        return;
    }
    while (factory->EnumAdapters1(adapterIdx, adapter.put()) != DXGI_ERROR_NOT_FOUND)
    {
        // adapterIdxのインデックスに割り当てられているアダプタに対して、デバイスを作成する
        hr = D3D11CreateDevice(adapter.get(), D3D_DRIVER_TYPE_UNKNOWN,
            nullptr, createDeviceFlags, d3dFeatures, 1, D3D11_SDK_VERSION,
            device.put(), nullptr, i_dxDeviceContext);
        if (hr != S_OK)
        {
            adapterIdx++;
            continue;
        }

        hr = device->QueryInterface(IID_PPV_ARGS(i_dxDevice));
        if (hr != S_OK)
        {
            adapterIdx++;
            continue;
        }

        // 作成されたデバイスで共有テクスチャの取得を試み、成功した場合はそのDirectXのデバイスを以降使用する
        // SHARED_CAPTURE_WINDOW_TEXTURE_PATHには共有テクスチャのハンドル名（"Global\\****"）を入れています。
        hr = _dxDevice->OpenSharedResourceByName(SHARED_CAPTURE_WINDOW_TEXTURE_PATH,
            DXGI_SHARED_RESOURCE_READ, IID_PPV_ARGS(i_sharedTexture));
        if (hr != S_OK)
        {
            adapterIdx++;
            continue;
        }

        break;
    }
}
```


# 解説

使用可能なDirectXのデバイスは`IDXGIFactory1`の`EnumAdapters1`メソッドで走査できます。`IDXGIFactory1`は`CreateDXGIFactory1`でそのインスタンスを作成できます。

`EnumAdapters1`は第1引数で渡すインデックスに対応したアダプタ`IDXGIAdapter1`を取得できます。インデックスを0から順に1ずつ増やすことで、使用可能なGPUに紐づいたアダプタすべてを走査できます。走査が完了すると`EnumAdapters1`は`DXGI_ERROR_NOT_FOUND`を返します。

ここで取得された`IDXGIAdapter1`は`D3D11CreateDevice`の第1引数に渡すことができ、渡すことでアダプタに紐づいたGPUを使用するDirectXのデバイス（`ID3D11Device`）を作成できます。このように作成されたDirectXのデバイスは、一度`QueryInterface`で`ID3D11Device1`に変換し、`OpenSharedResourceByName`メソッドで共有テクスチャの取得を試みます。

エラーが発生せずに取得できればwhile文を抜け、その時点で作成、取得したデバイスやデバイスコンテキスト、共有テクスチャを以降の処理で使用するようにします。エラーが発生した場合はインデックスを更新し、continueで次のアダプタに対して同様の確認を行います。


# 注意点

### 最後に1のつく`IDXGIFactory1`や`EnumAdapters1`などを使用する

似たものとして`IDXGIFactory`や`EnumAdapters`といったものがあります。これらはDXGI 1.0の機能を使用したものです。共有テクスチャはDXGI 1.1から使用できるようになった機能なので、`IDXGIFactory`や`EnumAdapters`を使用すると、共有テクスチャ使用時に必ずエラーが発生します。

そのため、DXGI 1.1に対応した`IDXGIFactory1`や`EnumAdapters1`などを使用する必要があります。


### `D3D11CreateDevice`の引数に`D3D_DRIVER_TYPE_UNKNOWN`を渡す

アダプタを指定せず、デフォルトのGPUを使用するデバイスを作成する場合、`D3D11CreateDevice`の第2引数を`D3D_DRIVER_TYPE_HARDWARE`と設定します。しかし、アダプタを指定してデバイスを生成する場合は、この引数だとエラーになります。そのため、第2引数を`D3D_DRIVER_TYPE_UNKNOWN`にする必要があります。


# まとめ

共有テクスチャを受け取る送信先のプロセス側を修正することで、優先するGPUの設定に依存せず、共有テクスチャのやり取りができるようになります。

`IDXGIFactory1`の`EnumAdapters1`メソッドによる走査は使用可能なGPUの情報を集めるのにも使用でき、環境によらずDirectXを使用するプログラムを動作させるためには、このような確認も考慮に入れる必要がありそうです。