
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.
