DirectX and Vulkan Interop
In the last weeks, I have been working on adding Vulkan and DirectX12 available backends to Evergine Studio. Evergine supports Vulkan and DirectX12 from its first official release 2021.11.17.2-preview, but the problem with adding them to Evergine Studio is that it is a WPF application. WPF is a Microsoft user interface technology that uses DirectX9 to render its controls, so if you want to create an Evergine control in WPF it must render with DirectX. Additionally, Evergine Studio architecture is compound by two applications, one WPF app only for UI purposes and another render process with the backend. That allows us to reload or restart only the render process without closing the WPF app.
To achieve this goal, you need to use the DirectX graphics context in WPF app process and create a texture shared resource that use to draw in your WPF custom control. DirectX shared resources are a special resource type that allows you to share a resource with another DirectX graphics context, even other processes.
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);
Once you have the DirectX shared texture, you need to get his shared handle to reference from another graphic context.
var resource = renderTarget.QueryInterface<IDXGIResource>();
var sharedHandle = resource.SharedHandle;
Now is easy to use this texture from other DirectX 11 or 12 graphic contexts (second app) and open the texture the following way.
var dxGraphicsContext = this.graphicsContext as DX11GraphicsContext;
var sharedNativeTexture = dxGraphicsContext.DXDevice.OpenSharedResource<ID3D11Texture2D>(surfaceHandle);
this.SharedTexture = DX11Texture.FromDirectXTexture(dxGraphicsContext, sharedNativeTexture);
So, you obtain the shared texture from the first app, and you can use it as a common texture in your app.
On the other hand, if you want to share the texture with a Vulkan context it is a little bit harder. I found an interesting post by James Jones and Mathias Schott from Nvidia about how to use the VK_NV_external_memory Vulkan extension to open a DirectX11 shared texture. And wrote a method in Evergine to do use it.
uint width = 1280;
uint height = 720;
var vkGraphicsContext = graphicsContext as VKGraphicsContext;
bool dedicateMemoryExtension = true; //extProperties.externalMemoryFeatures & VK_EXTERNAL_MEMORY_FEATURE_DEDICATED_ONLY_BIT_NV
// Creating the Vulkan Import Image
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);
// Binding Memory to the Vulkan Image
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);
// Create Framebuffer
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);
The main difference with DirectX API is that you need to specify the shared texture width and height to create a Vulkan texture where the data are binding by the DirectX shared texture. So, the shared texture handle and size are required to support different render technologies apps.
I have created a sample using Evergine to check this Vulkan extension. In the sample, there are two apps, a DX11 app that creates a shared texture and writes its texture handle in the console (Note. the size is fixed to 1280×720 in both apps). Vulkan app is the render that opens the DirectX shared texture using the VK_NV_external_memory extension and draws a red teapot spinning. The Vulkan app receives as the first argument the shared texture handle in hexadecimal.
Source code: https://github.com/Jorgemagic/DirectX-Vulkan-interop
Test the example
> ./DX11app.exe
Dx11 Shared Texture handle: 0x00000000c0001942
> ./Vulkan.exe 0x00000000c0001942
And the result:
Additional notes
At this point, you can do the DX11 app launch the Vulkan App after creating the shared texture for automatically opening the render, and that will be a silent process for the users.