2022年5月4日

DirectXとVulkanの相互運用

ここ数週間、Evergine StudioにVulkanとDirectX12の利用可能なバックエンドを追加する作業に取り組んできました。Evergineは最初の公式リリース2021.11.17.2-previewからVulkanとDirectX12をサポートしていますが、Evergine Studioにこれらを追加する際の問題は、Evergine StudioがWPFアプリケーションであることです。WPFは、そのコントロールのレンダリングにDirectX9を使用するMicrosoftのユーザーインターフェース技術であるため、WPFでEvergineコントロールを作成する場合は、DirectXでレンダリングする必要があります。さらに、Evergine Studioのアーキテクチャは2つのアプリケーションで構成されており、1つはUI専用のWPFアプリ、もう1つはバックエンドを持つレンダリングプロセスです。これにより、WPFアプリを閉じることなく、レンダリングプロセスのみをリロードまたは再起動できます。

この目標を達成するには、WPFアプリプロセスでDirectXグラフィックスコンテキストを使用し、WPFカスタムコントロールに描画するために使用するテクスチャ共有リソースを作成する必要があります。DirectX共有リソースは、別のDirectXグラフィックスコンテキスト、さらには別のプロセスとリソースを共有できる特別なリソースタイプです。

 

var renderTargetDescription = new Texture2DDescription

{

CpuAccessFlags = CpuAccessFlags.None,

Width = (int)width,

Height = (int)height,

Usage = Vortice.Direct3D11.ResourceUsage.Default,

Format = Vortice.DXGI.Format.B8G8R8A8_UNorm,

ArraySize = 1,

BindFlags = BindFlags.RenderTarget,

OptionFlags = ResourceOptionFlags.Shared,

MipLevels = 1,

SampleDescription = new SampleDescription(1, 0),

};

 

renderTarget = ((DX11GraphicsContext)dx11GraphicsContext).DXDevice.CreateTexture2D(renderTargetDescription);

 

DirectX共有テクスチャを取得したら、別のグラフィックスコンテキストから参照するために、その共有ハンドルを取得する必要があります。

 

var resource = renderTarget.QueryInterface<IDXGIResource>();

var sharedHandle = resource.SharedHandle;

 

これで、このテクスチャを他のDirectX 11または12グラフィックスコンテキスト(2番目のアプリ)から使用し、次の方法でテクスチャを開くことが簡単になります。

 

var dxGraphicsContext = this.graphicsContext as DX11GraphicsContext;

var sharedNativeTexture = dxGraphicsContext.DXDevice.OpenSharedResource<ID3D11Texture2D>(surfaceHandle);

this.SharedTexture = DX11Texture.FromDirectXTexture(dxGraphicsContext, sharedNativeTexture);

これにより、最初のアプリから共有テクスチャを取得し、アプリ内で通常のテクスチャとして使用できます。

一方、Vulkanコンテキストとテクスチャを共有する場合は、少し難しくなります。NvidiaのJames Jones氏とMathias Schott氏による、DirectX11共有テクスチャを開くためのVK_NV_external_memory Vulkan拡張の使用方法に関する興味深い投稿を見つけました。そして、Evergineでそれを使用するためのメソッドを作成しました。

 

uint width = 1280;

uint height = 720;

 

var vkGraphicsContext = graphicsContext as VKGraphicsContext;

 

bool dedicateMemoryExtension = true; //extProperties.externalMemoryFeatures & VK_EXTERNAL_MEMORY_FEATURE_DEDICATED_ONLY_BIT_NV

 

// Vulkanインポートイメージの作成

VkExternalMemoryImageCreateInfoNV extMemoryImageInfo = new VkExternalMemoryImageCreateInfoNV();

extMemoryImageInfo.sType = VkStructureType.VK_STRUCTURE_TYPE_EXTERNAL_MEMORY_IMAGE_CREATE_INFO_NV;

extMemoryImageInfo.handleTypes = VkExternalMemoryHandleTypeFlagsNV.VK_EXTERNAL_MEMORY_HANDLE_TYPE_D3D11_IMAGE_KMT_BIT_NV;

 

VkDedicatedAllocationImageCreateInfoNV dedicatedImageCreateInfo = new VkDedicatedAllocationImageCreateInfoNV();

dedicatedImageCreateInfo.sType = VkStructureType.VK_STRUCTURE_TYPE_DEDICATED_ALLOCATION_IMAGE_CREATE_INFO_NV;

dedicatedImageCreateInfo.dedicatedAllocation = false;

 

if (dedicateMemoryExtension)

{

extMemoryImageInfo.pNext = &dedicatedImageCreateInfo;

dedicatedImageCreateInfo.dedicatedAllocation = true;

}

 

VkImageCreateInfo imageCreateInfo = new VkImageCreateInfo();

imageCreateInfo.sType = VkStructureType.VK_STRUCTURE_TYPE_IMAGE_CREATE_INFO;

imageCreateInfo.pNext = &extMemoryImageInfo;

 

imageCreateInfo.imageType = VkImageType.VK_IMAGE_TYPE_2D;

imageCreateInfo.format = VkFormat.VK_FORMAT_B8G8R8A8_UNORM;

imageCreateInfo.extent.width = width;

imageCreateInfo.extent.height = height;

imageCreateInfo.extent.depth = 1;

imageCreateInfo.mipLevels = 1;

imageCreateInfo.arrayLayers = 1;

imageCreateInfo.samples = VkSampleCountFlags.VK_SAMPLE_COUNT_1_BIT;

imageCreateInfo.tiling = VkImageTiling.VK_IMAGE_TILING_OPTIMAL;

imageCreateInfo.usage = VkImageUsageFlags.VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT;

imageCreateInfo.flags = VkImageCreateFlags.None;

imageCreateInfo.sharingMode = VkSharingMode.VK_SHARING_MODE_EXCLUSIVE;

 

VkImage image;

VkResult result = VulkanNative.vkCreateImage(vkGraphicsContext.VkDevice, &imageCreateInfo, null, &image);

VKHelpers.CheckErrors(vkGraphicsContext, result);

 

// Vulkanイメージへのメモリバインド

VkMemoryRequirements memoryRequirements;

VulkanNative.vkGetImageMemoryRequirements(vkGraphicsContext.VkDevice, image, &memoryRequirements);

 

var memoryType = VKHelpers.FindMemoryType(vkGraphicsContext, memoryRequirements.memoryTypeBits, VkMemoryPropertyFlags.VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT);

 

VkImportMemoryWin32HandleInfoNV importMemInfoNV = new VkImportMemoryWin32HandleInfoNV();

importMemInfoNV.sType = VkStructureType.VK_STRUCTURE_TYPE_IMPORT_MEMORY_WIN32_HANDLE_INFO_NV;

importMemInfoNV.pNext = null;

importMemInfoNV.handleType = VkExternalMemoryHandleTypeFlagsNV.VK_EXTERNAL_MEMORY_HANDLE_TYPE_D3D11_IMAGE_KMT_BIT_NV;

importMemInfoNV.handle = surfaceHandle;

 

VkMemoryAllocateInfo allocInfo = new VkMemoryAllocateInfo();

allocInfo.sType = VkStructureType.VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO;

allocInfo.pNext = &importMemInfoNV;

allocInfo.allocationSize = memoryRequirements.size;

 

if (memoryType == -1)

{

vkGraphicsContext.ValidationLayer?.Notify(“Vulkan”, “No suitable memory type.”);

}

 

allocInfo.memoryTypeIndex = (uint)memoryType;

 

if (dedicateMemoryExtension)

{

 

VkDedicatedAllocationMemoryAllocateInfoNV dedicatedAllocationInfo = new VkDedicatedAllocationMemoryAllocateInfoNV();

dedicatedAllocationInfo.sType = VkStructureType.VK_STRUCTURE_TYPE_DEDICATED_ALLOCATION_MEMORY_ALLOCATE_INFO_NV;

dedicatedAllocationInfo.image = image;

 

importMemInfoNV.pNext = &dedicatedAllocationInfo;

}

 

VkDeviceMemory deviceMemory;

result = VulkanNative.vkAllocateMemory(vkGraphicsContext.VkDevice, &allocInfo, null, &deviceMemory);

VKHelpers.CheckErrors(vkGraphicsContext, result);

 

result = VulkanNative.vkBindImageMemory(vkGraphicsContext.VkDevice, image, deviceMemory, 0);

VKHelpers.CheckErrors(vkGraphicsContext, result);

 

// フレームバッファを作成

TextureDescription textureDescription = new TextureDescription()

{

CpuAccess = ResourceCpuAccess.None,

Width = width,

Height = height,

Depth = 1,

Usage = Evergine.Common.Graphics.ResourceUsage.Default,

Format = PixelFormat.B8G8R8A8_UNorm,

ArraySize = 1,

Faces = 1,

MipLevels = 1,

SampleCount = TextureSampleCount.None,

Flags = TextureFlags.RenderTarget,

};

var SharedTexture = VKTexture.FromVulkanImage(vkGraphicsContext, ref textureDescription, image);

 

DirectX APIとの主な違いは、DirectX共有テクスチャによってデータがバインドされるVulkanテクスチャを作成するために、共有テクスチャの幅と高さを指定する必要がある点です。そのため、異なるレンダリング技術のアプリをサポートするには、共有テクスチャハンドルとサイズが必要になります。

このVulkan拡張を確認するために、Evergineを使用したサンプルを作成しました。サンプルには2つのアプリがあり、DX11アプリは共有テクスチャを作成し、そのテクスチャハンドルをコンソールに書き込みます(注:サイズは両方のアプリで1280×720に固定されています)。Vulkanアプリは、VK_NV_external_memory拡張を使用してDirectX共有テクスチャを開き、回転する赤いティーポットを描画するレンダラーです。Vulkanアプリは、最初の引数として16進数の共有テクスチャハンドルを受け取ります。

 

ソースコード: https://github.com/Jorgemagic/DirectX-Vulkan-interop

サンプルをテストする

> ./DX11app.exe

Dx11 共有テクスチャハンドル: 0x00000000c0001942

> ./Vulkan.exe 0x00000000c0001942

そして結果:

補足事項

この時点で、DX11アプリが共有テクスチャを作成した後にVulkanアプリを起動して自動的にレンダリングを開くようにすれば、ユーザーにとってはサイレントなプロセスになります。

参考資料

Jorge Canton
著者
Jorge Cantón
Plain Concepts Research