Vulkanで描いてDirectCompositionで半透明合成する

![cropped.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/94115/01f6fe95-8abb-3cad-bd32-993a37aa023a.gif)

リポジトリはこちら: https://github.com/Pctg-x8/vk-noredirect-render

# これはなに？

VulkanでレンダリングしたものをDirectCompositionで直接デスクトップに合成しています。
DirectCompositionではDirectX（というよりDXGI）のスワップチェーンをそのまま合成に利用できる仕組みがあり[^dxgi-interop]、これを用いることでウィンドウ自体がバックバッファを持たなくてもウィンドウに対して描画を行うことが可能です。
ウィンドウがバックバッファを持たない場合、このウィンドウのクライアントエリアは透過したものとして扱われます。そこにDirectCompositionで合成を行うと、スワップチェーンの半透明情報をそのまま利用して合成されます。冒頭の三角形では一部の頂点を半透明にしており、ウィンドウの後ろにあるVisual Studio Codeの内容が一部合成されていることがわかります。

ただし、DirectCompositionが相互利用を提供しているのはDXGIを利用するDirectX APIのみです。VulkanはDXGIを利用していないため（たぶん）、VulkanのイメージやスワップチェーンをDirectCompositionで合成することはできません。
Vulkanのリソースをそのまま利用することはできませんが、DXGIもVulkanも他APIとの相互利用APIを提供しています。今回はコレを利用して、DXGI（DirectX12）のリソースに対して直接Vulkanで描画を行って合成をしています。

# DirectX12-Vulkan Interop

Vulkanには外部APIのメモリを取り扱えるようにする拡張として `VK_KHR_external_memory` という拡張が存在しました。今でも指定自体は多分できるんですが、この拡張はVulkan 1.1で標準機能にPromoteされており現在では特に指定なしで使えるようになっています。
ただし、この拡張は外部メモリ取り扱いの共通部分しか提供しておらず、実際にプラットフォーム/APIごとのメモリ共有はいまだに拡張機能となっています。
VulkanとWindows（DXGI, DirectX含む）とのメモリ共有には [`VK_KHR_external_memory_win32`](https://www.khronos.org/registry/vulkan/specs/1.2-extensions/man/html/VK_KHR_external_memory_win32.html) を使用します。この拡張はデバイス拡張となっているので、デバイスの作成時に指定します。

```rust:src/main.rs（243行目〜）
    let device_extensions = &[b"VK_KHR_external_memory_win32\0".as_ptr() as _];
    let device_cinfo = br::vk::VkDeviceCreateInfo {
        sType: br::vk::VK_STRUCTURE_TYPE_DEVICE_CREATE_INFO,
        pNext: std::ptr::null(),
        flags: 0,
        ppEnabledLayerNames: std::ptr::null(),
        enabledLayerCount: 0,
        ppEnabledExtensionNames: device_extensions.as_ptr(),
        enabledExtensionCount: device_extensions.len() as _,
        pQueueCreateInfos: &queue_create_info,
        queueCreateInfoCount: 1,
        pEnabledFeatures: std::ptr::null()
    };
```

この拡張を使用することにより、Windowsのハンドルを経由して他APIのメモリをVulkanから触ることができるようになります。
DirectXの場合はCOMなのでリソースへのハンドルと言われてもどれのことかさっぱりという感じだと思いますが、DirectX11/12でも外部とのメモリの共有を行うためにハンドルを経由したAPIが提供されています。クロスアダプタとか今回みたいにAPI跨ぎとかやらない限りは触る場面は全然ないマイナーAPIです。

今回はスワップチェーンの、バックバッファリソースを共有したいためバックバッファを取得したのち各種ハンドル取得APIを呼びます。今回はDirectX12でドライブするようにスワップチェーンを作ったので、 `ID3D12Device::CreateSharedHandle` を使用してハンドルを取得します。

```rust:src/main.rs（674行目〜）
        let mut res = std::ptr::null_mut();
        let hr = unsafe { sc.GetBuffer(n as _, &winapi::um::d3d12::ID3D12Resource::uuidof(), &mut res) };
        hr_to_ioresult(hr).expect("SwapChain GetBuffer failed");
        let res = ComPtr::from(res as *mut winapi::um::d3d12::ID3D12Resource);
        let mut sh = std::ptr::null_mut();
        let name = widestring::WideCString::from_str(format!("LocalSharedBackBufferResource{}", n)).expect("WideCString encoding failed");
        let hr = unsafe { device12.CreateSharedHandle(res.as_ptr() as _, std::ptr::null(), winapi::um::winnt::GENERIC_ALL, name.as_ptr(), &mut sh) };
        hr_to_ioresult(hr).expect("D3D12 CreateSharedHandle failed");
        let sh = UniqueObject(sh, |p| unsafe { winapi::um::handleapi::CloseHandle(p); });
```

ハンドルを作成したら、いつもどおり `VkImage` を作成してバッキングメモリを確保してBindしてあげます。ここで拡張情報をそれぞれ追加で渡してあげる必要があるため、それぞれ `pNext` に構造体へのポインタを指定します。
`VkImageCreateInfo` には `VkExternalMemoryImageCreateInfo` を、 `VkMemoryAllocateInfo` には `VkImportMemoryWin32HandleInfoKHR` を使用します。

```rust:src/main.rs（684行目〜）
        let image_extmem_info = br::vk::VkExternalMemoryImageCreateInfo {
            sType: br::vk::VK_STRUCTURE_TYPE_EXTERNAL_MEMORY_IMAGE_CREATE_INFO,
            pNext: std::ptr::null(),
            handleTypes: br::vk::VK_EXTERNAL_MEMORY_HANDLE_TYPE_D3D12_RESOURCE_BIT
        };
        let image_cinfo = br::vk::VkImageCreateInfo {
            sType: br::vk::VK_STRUCTURE_TYPE_IMAGE_CREATE_INFO,
            pNext: &image_extmem_info as *const _ as _,
            imageType: br::vk::VK_IMAGE_TYPE_2D,
            format: br::vk::VK_FORMAT_R8G8B8A8_UNORM,
            extent: br::vk::VkExtent3D { width: 640, height: 480, depth: 1 },
            mipLevels: 1,
            arrayLayers: 1,
            samples: br::vk::VK_SAMPLE_COUNT_1_BIT,
            tiling: br::vk::VK_IMAGE_TILING_OPTIMAL,
            usage: br::vk::VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT,
            sharingMode: br::vk::VK_SHARING_MODE_EXCLUSIVE,
            queueFamilyIndexCount: 0,
            pQueueFamilyIndices: std::ptr::null(),
            initialLayout: br::vk::VK_IMAGE_LAYOUT_PREINITIALIZED,
            flags: 0
        };
        let mut image = br::vk::VK_NULL_HANDLE as _;
        let r = unsafe { br::vk::vkCreateImage(vk_device.as_ptr(), &image_cinfo, std::ptr::null(), &mut image) };
        vk_to_result(r).expect("vkCreateImage failed");
        let image = UniqueObject(image, |o| unsafe { br::vk::vkDestroyImage(vk_device.as_ptr(), o, std::ptr::null()); });
        let mut img_requirements = std::mem::MaybeUninit::uninit();
        unsafe { br::vk::vkGetImageMemoryRequirements(vk_device.as_ptr(), image.as_ptr(), img_requirements.as_mut_ptr()) };
        let img_requirements = unsafe { img_requirements.assume_init() };

        let mut props = br::vk::VkMemoryWin32HandlePropertiesKHR {
            sType: br::vk::VK_STRUCTURE_TYPE_MEMORY_WIN32_HANDLE_PROPERTIES_KHR,
            pNext: std::ptr::null_mut(),
            .. unsafe { std::mem::MaybeUninit::uninit().assume_init() }
        };
        let r = (vk_get_memory_win32_handle_properties_khr)(vk_device.as_ptr(), br::vk::VK_EXTERNAL_MEMORY_HANDLE_TYPE_D3D12_RESOURCE_BIT, sh.as_ptr(), &mut props);
        vk_to_result(r).expect("vkGetMemoryWin32HandlePropertiesKHR failed");
        let memory_type_index = memory_properties.memoryTypes[..memory_properties.memoryTypeCount as usize].iter().enumerate()
            .position(|(n, t)|
                (props.memoryTypeBits & (1 << n)) != 0 &&
                (img_requirements.memoryTypeBits & (1 << n)) != 0 &&
                (t.propertyFlags & br::vk::VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT) != 0
            ).expect("no matching memory type index");
        let import_memory_info = br::vk::VkImportMemoryWin32HandleInfoKHR {
            sType: br::vk::VK_STRUCTURE_TYPE_IMPORT_MEMORY_WIN32_HANDLE_INFO_KHR,
            pNext: std::ptr::null(),
            handleType: br::vk::VK_EXTERNAL_MEMORY_HANDLE_TYPE_D3D12_RESOURCE_BIT,
            handle: sh.as_ptr(),
            name: name.as_ptr()
        };
        let memory_ainfo = br::vk::VkMemoryAllocateInfo {
            sType: br::vk::VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO,
            pNext: &import_memory_info as *const _ as _,
            allocationSize: img_requirements.size,  // ignored
            memoryTypeIndex: memory_type_index as _
        };
        let mut mem = br::vk::VK_NULL_HANDLE as _;
        let r = unsafe { br::vk::vkAllocateMemory(vk_device.as_ptr(), &memory_ainfo, std::ptr::null(), &mut mem) };
        vk_to_result(r).expect("vkAllocateMemory failed");
        let mem = UniqueObject(mem, |p| unsafe { br::vk::vkFreeMemory(vk_device.as_ptr(), p, std::ptr::null()); });
        let r = unsafe { br::vk::vkBindImageMemory(vk_device.as_ptr(), image.as_ptr(), mem.as_ptr(), 0) };
        vk_to_result(r).expect("vkBindImageMemory failed");
```

`VkMemoryAllocateInfo::allocationSize` のところに `// ignored` と書いてある通り、spec[^alloc-spec]上はここで指定したアロケーションサイズは無視され、Vulkanの実装側が適切なサイズを内部的に取得して適用するようになっています。だからといってここを0にするとAPI Validationに引っかかるため、無視されるけど0以上の値を指定しておく必要があります。

`VkImage` さえできてしまえば残りは通常のWSIを使用したVulkanプログラムと同じになります。

一つ違うのはPresentationの時で、ここはVulkanのSwapchainではないためDXGI側のPresentを呼び出す必要があります。
DXGI側のPresentを呼び出すということは、同期機構もDXGI/DirectX12側の物を使用する必要があります。そのため、相互利用を行うプログラムでは同期機構が2系統になります。ここをどううまく合わせるかですが、今回は特に何も考えずにDirectX12側の同期はブロッキングで行うようになっています。真面目なお話をするのであればちゃんとうまくフラグ管理するなりしてノンブロッキングでやってあげた方が良いと思います。

```rust:src/main.rs（1057行目〜）
            unsafe {
                let handles = &[sc_waitable, fence_event];
                winapi::um::synchapi::WaitForMultipleObjectsEx(handles.len() as _, handles.as_ptr(), true as _, winapi::um::winbase::INFINITE, false as _)
            };
            let hr = unsafe { sc.Present(0, 0) };
            hr_to_ioresult(hr).expect("SwapChain Present failed");
            let hr = unsafe { cq.Signal(fence12.as_ptr(), fence_value) };
            hr_to_ioresult(hr).expect("Fence signaling failed");
            let hr = unsafe { fence12.SetEventOnCompletion(fence_value, fence_event) };
            hr_to_ioresult(hr).expect("Fence Event Setting failed");
            fence_value += 1;
```

サンプルではスワップチェーンの遅延低減用のオブジェクト[^latency-waitable]も待っていますが、これは必須ではないと思います。

以上で、DirectX12とVulkanの相互利用の部分だけの解説は終わりです。他は特に相互利用しない場合のコードとほぼ変わらないのでリポジトリの方を見てください。

DirectCompositionで直接レンダリングできると仕組み上Presentのコストが下がるのと、うまくやればデスクトップアクセサリ（ガジェット）みたいなのもVulkanで作れるようになるので、そのうちPeridot[^peridot]にも載せたいなと思ってます。

[^dxgi-interop]: https://docs.microsoft.com/ja-jp/archive/msdn-magazine/2014/june/windows-with-c-high-performance-window-layering-using-the-windows-composition-engine
[^alloc-spec]: https://www.khronos.org/registry/vulkan/specs/1.2-extensions/man/html/VkMemoryAllocateInfo.html
[^latency-waitable]: https://docs.microsoft.com/en-us/windows/win32/api/dxgi1_3/nf-dxgi1_3-idxgiswapchain2-getframelatencywaitableobject
[^peridot]: https://github.com/Pctg-x8/peridot