Direct2D と Direct3D 11 の共有方法

MSDN ライブラリ(英語版) の各ドキュメントには、 Direct2D と Direct3D 10.1 とのテクスチャ(サーフェイス)共有について書かれていますが、
Direct2D と Direct3D 11 とのテクスチャ(サーフェイス)共有の仕方は余り詳しく書かれていません。

なので、 Direct2D(以下 D2D) + Direct3D(以下 D3D) 11 で共有出来る方法を調べてみました。

まず、D2D は D3D 10.1 API を使用しているため、 D2D と D3D 10.1 は楽に共有できます。
(MSDN にもサンプルがあります)

さて、 D2D と D3D 11 を共有するためには、以下のような条件が必要となります。

  1. D3D 11 デバイスはハードウェアデバイスで無ければならない。(リファレンスや WARP では不可)
  2. テクスチャ(サーフェイス)を共有するには D3D 10.1 デバイスを介する必要がある。
  3. D3D 10.1 と D3D 11 のデバイスは同じアダプタで使用する必要がある。

最初の条件の WARP デバイスが使用できない部分については特に注意してください。

ここに実際に実行できるコードを置いておきます。
(以下で解説しているソースコードの全体文はこちらのファイルとなっています)
zip D3D2D 4.47 kB

VC++ でビルドする場合、空の Win32 プロジェクトをつくり、このファイルを追加してビルドするだけで出来ます。
これでまずは無事に実行できることを確認して下さい。
(環境によっては実行できないかもしれません)
正しく実行され、アニメーションされていることが出来たら無事に実行されている証拠です。

では、実装方法について説明していきましょう。
※ここでは必要となる要点のみ説明していきます。

まず、 D2D 、 D3D 10.1 、 D3D 11 の各デバイス(D2D はファクトリー)を作成します。
デバイスの作成順序は気にする必要はありませんが、
D3D 11 と D3D 10.1 のデバイスを作成する時は同じアダプタでなければなりません。
アダプタを指定するところに NULL を指定することでデフォルトのアダプタを指定することになりますが、
明示的に指定するべきです。

アダプタの取得は簡単です。


        // アダプタを取得
        {
            ResPtr<IDXGIFactory1> dxgiFactory;
            if FAILED(hr = CreateDXGIFactory1(__uuidof(IDXGIFactory1), (LPVOID*)&dxgiFactory))
            {
                return error_message_hr(L"CreateDXGIFactory1", hr);
            }
            if FAILED(hr = dxgiFactory->EnumAdapters1(0, &adapter))
            {
                return error_message_hr(L"IDXGIFactory1::EnumAdapters1", hr);
            }
        }

DXGI 1.1 のファクトリーを作成し、ファクトリーから EnumAdapters1 を呼び出してアダプタを取得するだけです。
ここでは 0 番目のアダプタを取得しています。
これはデフォルト(最初)のアダプタ、ということになり、
複数存在する場合は 1 番、 2 番…、と指定することで取得できます。

次に D3D 11 デバイスを作成します。
ここではスワップチェインと共に作成しています。


        // D3D 11 を スワップチェインと共に作成
        {
            DXGI_SWAP_CHAIN_DESC    scd;
            ZeroMemory(&scd, sizeof(scd));
            scd.BufferDesc.Format   = DXGI_FORMAT_B8G8R8A8_UNORM;
            scd.SampleDesc.Count    = 1;
            scd.BufferUsage         = DXGI_USAGE_RENDER_TARGET_OUTPUT;
            scd.BufferCount         = 1;
            scd.OutputWindow        = hWnd;
            scd.SwapEffect          = DXGI_SWAP_EFFECT_DISCARD;
            scd.Windowed            = TRUE;

            // D3D 11 デバイス生成時、 D3D_DRIVER_TYPE_UNKNOWN を指定しないと
            // うまくいかないことがある。
            // また、 D2D を使用する場合は D3D11_CREATE_DEVICE_BGRA_SUPPORT を
            // 指定する必要がある。
            hr = D3D11CreateDeviceAndSwapChain(
                adapter,
                D3D_DRIVER_TYPE_UNKNOWN,
                NULL,
                D3D11_CREATE_DEVICE_BGRA_SUPPORT,
                NULL,
                0,
                D3D11_SDK_VERSION,
                &scd,
                &swapchain,
                &device11,
                NULL,
                &immcontext
            );
            if FAILED(hr)
            {
                return error_message_hr(L"D3D11CreateDeviceAndSwapChain", hr);
            }
        }

コード内のコメントに書かれている通り、アダプタを指定する時は D3D_DRIVER_TYPE_UNKNOWN を指定しないとうまく動作しません(呼び出しに失敗します)。
そして、 D2D との連携をするために、 D3D11_CREATE_DEVICE_BGRA_SUPPORT フラグを指定します。

ここでの注意点はそこのみです。

次に D3D 10.1 デバイスの作成です。


        // D3D 10.1 を作成
        {
            // D3D10_DRIVER_TYPE_HARDWARE と D3D10_CREATE_DEVICE_BGRA_SUPPORT は必ず指定。
            // なお、こちらの環境では D3D10_FEATURE_LEVEL_9_3 にしないと動作しませんでした。
            hr = D3D10CreateDevice1(
                adapter,
                D3D10_DRIVER_TYPE_HARDWARE,
                NULL,
                D3D10_CREATE_DEVICE_BGRA_SUPPORT,
                D3D10_FEATURE_LEVEL_9_3,
                D3D10_1_SDK_VERSION,
                &device101
            );
            if FAILED(hr)
            {
                return error_message_hr(L"D3D10CreateDevice1", hr);
            }
        }

こちらは D3D10_CREATE_DEVICE_BGRA_SUPPORT と D3D10_FEATURE_LEVEL_9_3 を指定すること以外、
特筆することがありません。
ちなみに D3D10_FEATURE_LEVEL_10_0 で生成して試すと、 D3D 10.1 側での共有サーフェイスの準備で失敗してしまいます。

D2D の方はファクトリーを生成するだけなので省きます。

ちなみに、各デバイスのフラグにはマルチスレッド(シングルスレッド)を指定するフラグがありますが、
どちらを指定しても動作します。

ここから、共有させるためのテクスチャを作成していきます。
まず、 D3D 11 のテクスチャを作成します。
ここではサイズはバックバッファと同じサイズにしています。


            // 作成するテクスチャ情報の設定。
            // ・DXGI_FORMAT_B8G8R8A8_UNORM は固定。
            // ・D3D11_BIND_RENDER_TARGET は D2D での描画対象とするために必須。
            // ・D3D11_RESOURCE_MISC_SHARED_KEYEDMUTEX はテクスチャを共有するのに必須。
            ZeroMemory(&std, sizeof(std));
            std.Width               = bbd.Width;
            std.Height              = bbd.Height;
            std.MipLevels           = 1;
            std.ArraySize           = 1;
            std.Format              = DXGI_FORMAT_B8G8R8A8_UNORM;
            std.SampleDesc.Count    = 1;
            std.Usage               = D3D11_USAGE_DEFAULT;
            std.BindFlags           = D3D11_BIND_SHADER_RESOURCE | D3D11_BIND_RENDER_TARGET;
            std.MiscFlags           = D3D11_RESOURCE_MISC_SHARED_KEYEDMUTEX;
            if FAILED(device11->CreateTexture2D(&std, NULL, &texture11))
            {
                return error_message_hr(L"ID3D11Texture2D::CreateTexture2D", hr);
            }

必要な要点はコード内のコメントの通りです。

作成されたテクスチャから IDXGIKeyedMutex インターフェイスを取得し、
(D3D11_RESOURCE_MISC_SHARED_KEYEDMUTEX を指定しないと取得できません)
さらに共有するための共有ハンドルも取得します。


        // 共有するための D3D 11 のキーミューテックスを取得
        if FAILED(texture11->QueryInterface(__uuidof(IDXGIKeyedMutex), (LPVOID*)&keyedmutex11))
        {
            return error_message_hr(L"ID3D11Texture2D::QueryInterface(IDXGIResource)", hr);
        }
        // 共有のためのハンドルを取得。
        {
            ResPtr<IDXGIResource> resource11;
            if FAILED(texture11->QueryInterface(__uuidof(IDXGIResource), (LPVOID*)&resource11))
            {
                return error_message_hr(L"ID3D11Texture2D::QueryInterface(IDXGIResource)", hr);
            }
            if FAILED(resource11->GetSharedHandle(&sharedHandle))
            {
                return error_message_hr(L"IDXGIResource::GetSharedHandle", hr);
            }
        }

IDXGIKeyedMutex インターフェイスは D3D 11 と D3D 10.1 双方のデバイスの同調させるのに必要です。
共有ハンドルは 後述する共有サーフェイスを取得するのに使用します。


        // D3D 10.1 で共有サーフェイスを生成。
        if FAILED(hr = device101->OpenSharedResource(sharedHandle, __uuidof(IDXGISurface1), (LPVOID*)&surface10))
        {
            return error_message_hr(L"ID3D10Device1::OpenSharedResource(IDXGISurface1)", hr);
        }

そして、共有サーフェイスからもキーミューテックスを取得します。


        // 共有するための D3D 10.1 のキーミューテックスを取得
        if FAILED(surface10->QueryInterface(__uuidof(IDXGIKeyedMutex), (LPVOID*)&keyedmutex10))
        {
            return error_message_hr(L"IDXGISurface1::QueryInterface(IDXGIKeyedMutex)", hr);
        }

この二つのキーミューテックスを、それぞれのデバイス上で操作するときにそれぞれのキーミューテックスを使用します。
詳しい使用方法は最後に纏めていますのでここでは割愛します。

D3D 10.1 で用意した共有サーフェイスから D2D のレンダーターゲットを生成します。


        // D2D のレンダーターゲットを D3D 10.1 の共有サーフェイスから生成。
        {
            D2D1_RENDER_TARGET_PROPERTIES    rtp;
            ZeroMemory(&rtp, sizeof(rtp));
            rtp.type        = D2D1_RENDER_TARGET_TYPE_HARDWARE;
            rtp.pixelFormat    = D2D1::PixelFormat(DXGI_FORMAT_UNKNOWN, D2D1_ALPHA_MODE_PREMULTIPLIED);
            if FAILED(d2dfactory->CreateDxgiSurfaceRenderTarget(surface10, &rtp, &rendertarget))
            {
                return error_message_hr(L"ID2D1Factory::CreateDxgiSurfaceRenderTarget", hr);
            }
        }

ここまで成功すれば、 D2D と D3D 11 との共有サーフェイス(テクスチャ)が作成されたことになります。

そして、それぞれの描画の仕方ですが、使用する前と後でキーミューテックスの AcquireSync と ReleaseSync を呼ぶこと以外に変更はありません。

キーミューテックスインターフェイスの呼び出しの仕方には法則があります。

  1. (生成直後など)一番最初に AcquireSync 関数を呼び出す場合のキー値は 0 である必要がある。
  2. ReleaseSync 関数のキー値は次にアクセスを許可するキー値を指定する。

    (これによりそのキー値で AcquireSync 関数を呼び出した所にアクセス権が移る)

  3. AcquireSync 関数を呼び出した後、 ReleaseSync 関数を呼び出す前に AcquireSync 関数を同じキー値で呼び出してはならない。
  4. 複数のスレッドで同じキー値を指定して AcquireSync 関数を呼び出した時、アクセス権が移る時の順序は不定(未定義)。

これは、ちょうど回覧板のような仕組みを示しています。
共有サーフェイスを回覧板、各デバイス(D3D 10.1 と D3D 11)を受け取る人と例えると、以下のような感じになります。

最初、A さん(D3D 10.1 デバイス)は、 0 番(キー値)と書いた回覧板を受け取りにいきます。 – keyedmutex10->AcquireSync(0, INFINITE)
このとき、回覧板に 0 番と書かれていなければ、 A さんは 0 番と書かれるまでその場で待ちます。
そして、 A さんはその回覧板に書き込みをし、
番号を 1 番に書き換えて戻します。 – keyedmutex10->ReleaseSync(1)

次に B さん(D3D 11 デバイス)が 1 番と書いた回覧板を受け取りにいきます。 – keyedmutex11->AcquireSync(0, INFINITE)
このとき、回覧板に 1 番と書かれていなければ、 B さんは 1 番と書かれるまでその場で待ちます。
そして、 B さんはその回覧板に書き込みをし、
番号を 0 番に書き換えて戻します。 – keyedmutex11->ReleaseSync(0)

(そして最初の方に戻る)

このような感じで共有サーフェイス(回覧板)へのアクセス権を同期させます。
これを怠ると、片方の作業中にもう片方が作業しようとしてしまい、
おかしな結果になってしまいます。

なお、触れなかった AcquireSync 関数の第二引数ですが、これはアクセス権を受け取るために待つ時間(ms)です。
INFINITE を指定することで無限に待つことを意味します。
それ以外を指定し、且つタイムアウトした(待機時間以内にアクセス権が受け取れなかった)場合は WAIT_TIMEOUT が返ってきます。