04. Mai 2022

DirectX- und Vulkan-Interop

In den letzten Wochen habe ich daran gearbeitet, Evergine Studio die Backends Vulkan und DirectX12 hinzuzufügen. Evergine unterstützt Vulkan und DirectX12 seit dem ersten offiziellen Release 2021.11.17.2-preview, doch das Problem beim Hinzufügen zu Evergine Studio ist, dass es sich um eine WPF-Anwendung handelt. WPF ist eine Benutzeroberflächentechnologie von Microsoft, die ihre Steuerelemente mit DirectX9 rendert. Wenn Sie also ein Evergine-Steuerelement in WPF erstellen möchten, muss es mit DirectX rendern. Zudem besteht die Architektur von Evergine Studio aus zwei Anwendungen: einer WPF-App ausschließlich für die UI und einem separaten Render-Prozess mit dem Backend. So können wir nur den Render-Prozess neu laden oder neu starten, ohne die WPF-App zu schließen.

Um dies zu erreichen, müssen Sie im Prozess der WPF-App den DirectX-Grafikkontext verwenden und eine gemeinsam genutzte Texturressource (Shared Resource) erstellen, in die Sie in Ihrem benutzerdefinierten WPF-Steuerelement zeichnen. DirectX Shared Resources sind ein spezieller Ressourcentyp, mit dem Sie eine Ressource mit einem anderen DirectX-Grafikkontext teilen können, sogar mit anderen Prozessen.

 

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);

 

Sobald Sie die gemeinsam genutzte DirectX-Textur haben, benötigen Sie ihr Shared Handle, um sie aus einem anderen Grafikkontext zu referenzieren.

 

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

var sharedHandle = resource.SharedHandle;

 

Nun lässt sich diese Textur ganz einfach aus anderen DirectX-11- oder DirectX-12-Grafikkontexten (zweite App) verwenden, indem Sie sie wie folgt öffnen.

 

var dxGraphicsContext = this.graphicsContext as DX11GraphicsContext;

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

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

So erhalten Sie die gemeinsam genutzte Textur aus der ersten App und können sie in Ihrer App wie eine gewöhnliche Textur verwenden.

Möchten Sie die Textur hingegen mit einem Vulkan-Kontext teilen, ist es etwas schwieriger. Ich habe einen interessanten Beitrag von James Jones und Mathias Schott von Nvidia darüber gefunden, wie man mit der Vulkan-Erweiterung VK_NV_external_memory eine gemeinsam genutzte DirectX11-Textur öffnet, und in Evergine eine Methode geschrieben, die dies nutzt.

 

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);

 

Der Hauptunterschied zur DirectX-API besteht darin, dass Sie Breite und Höhe der gemeinsam genutzten Textur angeben müssen, um eine Vulkan-Textur zu erstellen, deren Daten an die gemeinsam genutzte DirectX-Textur gebunden sind. Um Apps mit unterschiedlichen Rendering-Technologien zu unterstützen, werden also das Handle und die Größe der gemeinsam genutzten Textur benötigt.

Ich habe mit Evergine ein Beispiel erstellt, um diese Vulkan-Erweiterung zu testen. Es besteht aus zwei Apps: einer DX11-App, die eine gemeinsam genutzte Textur erstellt und deren Handle in der Konsole ausgibt (Hinweis: Die Größe ist in beiden Apps fest auf 1280×720 eingestellt), und einer Vulkan-App als Renderer, die die gemeinsam genutzte DirectX-Textur über die Erweiterung VK_NV_external_memory öffnet und eine sich drehende rote Teekanne zeichnet. Die Vulkan-App erhält als erstes Argument das Handle der gemeinsam genutzten Textur im Hexadezimalformat.

 

Quellcode: https://github.com/Jorgemagic/DirectX-Vulkan-interop

Das Beispiel testen

> ./DX11app.exe

Dx11 Shared Texture handle: 0x00000000c0001942

> ./Vulkan.exe 0x00000000c0001942

Und das Ergebnis:

Weitere Hinweise

An dieser Stelle können Sie die DX11-App nach dem Erstellen der gemeinsam genutzten Textur die Vulkan-App starten lassen, damit sich der Renderer automatisch öffnet – ein Vorgang, von dem die Nutzer nichts bemerken.

Referenzen

Jorge Canton
Autor
Jorge Cantón
Plain Concepts Research