/* Copyright (c) 2017-2026 Hans-Kristian Arntzen * * Permission is hereby granted, free of charge, to any person obtaining * a copy of this software and associated documentation files (the * "Software"), to deal in the Software without restriction, including * without limitation the rights to use, copy, modify, merge, publish, * distribute, sublicense, and/or sell copies of the Software, and to * permit persons to whom the Software is furnished to do so, subject to * the following conditions: * * The above copyright notice and this permission notice shall be * included in all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. * IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY * CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, * TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ #include "wsi_dxgi.hpp" #include namespace Vulkan { DXGIInteropSwapchain::~DXGIInteropSwapchain() { // Wait-for-idle before teardown. if (fence) fence->SetEventOnCompletion(fence_value, nullptr); if (latency_handle) CloseHandle(latency_handle); } static bool is_running_on_wine() { // If we're running in Wine for whatever reason, interop like this is completely useless. HMODULE ntdll = GetModuleHandleA("ntdll.dll"); return !ntdll || GetProcAddress(ntdll, "wine_get_version"); } static bool is_running_in_tool(Device &device) { auto &ext = device.get_device_features(); if (ext.supports_tooling_info && vkGetPhysicalDeviceToolPropertiesEXT) { auto gpu = device.get_physical_device(); uint32_t count = 0; vkGetPhysicalDeviceToolPropertiesEXT(gpu, &count, nullptr); Util::SmallVector tool_props(count); for (auto &t : tool_props) t = { VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_TOOL_PROPERTIES_EXT }; vkGetPhysicalDeviceToolPropertiesEXT(gpu, &count, tool_props.data()); // It's okay for validation to not force this path. We're mostly concerned with RenderDoc, RGP and Nsight. for (auto &t : tool_props) if (t.purposes & (VK_TOOL_PURPOSE_PROFILING_BIT | VK_TOOL_PURPOSE_TRACING_BIT)) return true; } return false; } bool DXGIInteropSwapchain::init_interop_device(Device &vk_device_) { vk_device = &vk_device_; // If we're running in Wine for whatever reason, interop like this is more harmful than good. if (is_running_on_wine()) return false; // If we're running in some capture tool, we need to use Vulkan WSI to avoid confusing it. if (is_running_in_tool(*vk_device)) return false; if (!vk_device->get_device_features().vk11_props.deviceLUIDValid) return false; d3d12_lib = Util::DynamicLibrary("d3d12.dll"); dxgi_lib = Util::DynamicLibrary("dxgi.dll"); if (!d3d12_lib) { LOGE("Failed to find d3d12.dll. Ignoring interop device.\n"); return false; } if (!dxgi_lib) { LOGE("Failed to find dxgi.dll. Ignoring interop device.\n"); return false; } auto pfn_CreateDXGIFactory1 = dxgi_lib.get_symbol("CreateDXGIFactory1"); auto pfn_D3D12CreateDevice = d3d12_lib.get_symbol("D3D12CreateDevice"); if (!pfn_CreateDXGIFactory1 || !pfn_D3D12CreateDevice) { LOGE("Failed to find entry points.\n"); return false; } HRESULT hr; if (FAILED(hr = pfn_CreateDXGIFactory1(IID_PPV_ARGS(&dxgi_factory)))) { LOGE("Failed to create DXGI factory, hr #%x.\n", unsigned(hr)); return false; } LUID luid = {}; ComPtr adapter; memcpy(&luid, vk_device->get_device_features().vk11_props.deviceLUID, VK_LUID_SIZE); if (FAILED(hr = dxgi_factory->EnumAdapterByLuid(luid, IID_PPV_ARGS(&adapter)))) { LOGE("Failed to enumerate DXGI adapter by LUID.\n"); return false; } if (FAILED(hr = pfn_D3D12CreateDevice(adapter.Get(), D3D_FEATURE_LEVEL_11_0, IID_PPV_ARGS(&device)))) { LOGE("Failed to create D3D12Device, hr #%x.\n", unsigned(hr)); return false; } D3D12_COMMAND_QUEUE_DESC queue_desc = {}; queue_desc.Type = D3D12_COMMAND_LIST_TYPE_DIRECT; if (FAILED(hr = device->CreateCommandQueue(&queue_desc, IID_PPV_ARGS(&queue)))) { LOGE("Failed to create command queue, hr #%x.\n", unsigned(hr)); return false; } if (FAILED(hr = device->CreateCommandList1( 0, D3D12_COMMAND_LIST_TYPE_DIRECT, D3D12_COMMAND_LIST_FLAG_NONE, IID_PPV_ARGS(&list)))) { LOGE("Failed to create command list, hr #%x.\n", unsigned(hr)); return false; } if (FAILED(hr = device->CreateFence(0, D3D12_FENCE_FLAG_SHARED, IID_PPV_ARGS(&fence)))) { LOGE("Failed to create shared fence, hr #%x.\n", unsigned(hr)); return false; } // Import D3D12 timeline into Vulkan. // Other way around is not as well-supported. vk_fence = vk_device->request_semaphore_external( VK_SEMAPHORE_TYPE_TIMELINE, VK_EXTERNAL_SEMAPHORE_HANDLE_TYPE_D3D12_FENCE_BIT); if (!vk_fence) { LOGE("Failed to create timeline.\n"); return EXIT_FAILURE; } ExternalHandle fence_handle; fence_handle.semaphore_handle_type = VK_EXTERNAL_SEMAPHORE_HANDLE_TYPE_D3D12_FENCE_BIT; if (FAILED(device->CreateSharedHandle(fence.Get(), nullptr, GENERIC_ALL, nullptr, &fence_handle.handle))) { LOGE("Failed to create shared fence handle.\n"); return EXIT_FAILURE; } if (!vk_fence->import_from_handle(fence_handle)) { LOGE("Failed to import timeline.\n"); CloseHandle(fence_handle.handle); return false; } return true; } VkImage DXGIInteropSwapchain::get_vulkan_image() const { return vulkan_backbuffer->get_image(); } static DXGI_FORMAT convert_vk_format(VkFormat fmt) { switch (fmt) { case VK_FORMAT_R8G8B8A8_UNORM: case VK_FORMAT_R8G8B8A8_SRGB: // D3D12 fails to create SRGB swapchain for some reason. // We'll import the memory as sRGB however, and it works fine ... return DXGI_FORMAT_R8G8B8A8_UNORM; case VK_FORMAT_B8G8R8A8_UNORM: case VK_FORMAT_B8G8R8A8_SRGB: return DXGI_FORMAT_B8G8R8A8_UNORM; case VK_FORMAT_A2B10G10R10_UNORM_PACK32: return DXGI_FORMAT_R10G10B10A2_UNORM; case VK_FORMAT_R16G16B16A16_SFLOAT: return DXGI_FORMAT_R16G16B16A16_FLOAT; default: return DXGI_FORMAT_UNKNOWN; } } static DXGI_COLOR_SPACE_TYPE convert_vk_color_space(VkColorSpaceKHR colspace) { switch (colspace) { case VK_COLOR_SPACE_EXTENDED_SRGB_LINEAR_EXT: return DXGI_COLOR_SPACE_RGB_FULL_G10_NONE_P709; case VK_COLOR_SPACE_HDR10_ST2084_EXT: return DXGI_COLOR_SPACE_RGB_FULL_G2084_NONE_P2020; case VK_COLOR_SPACE_SRGB_NONLINEAR_KHR: return DXGI_COLOR_SPACE_RGB_FULL_G22_NONE_P709; default: return DXGI_COLOR_SPACE_RESERVED; } } void DXGIInteropSwapchain::reset_backbuffer_state() { for (auto &buf : backbuffers) if (fence) fence->SetEventOnCompletion(buf.wait_fence_value, nullptr); backbuffers.clear(); } bool DXGIInteropSwapchain::setup_per_frame_state(PerFrameState &state, unsigned index) { HRESULT hr; if (FAILED(hr = swapchain->GetBuffer(index, IID_PPV_ARGS(&state.backbuffer)))) { LOGE("Failed to get backbuffer, hr #%x.\n", unsigned(hr)); return false; } if (FAILED(hr = device->CreateCommandAllocator(D3D12_COMMAND_LIST_TYPE_DIRECT, IID_PPV_ARGS(&state.allocator)))) { LOGE("Failed to create command allocator, hr #%x.\n", unsigned(hr)); return false; } return true; } bool DXGIInteropSwapchain::init_swapchain(HWND hwnd_, VkSurfaceFormatKHR format, unsigned width, unsigned height, unsigned count) { if (hwnd && hwnd_ != hwnd) { reset_backbuffer_state(); swapchain.Reset(); } hwnd = hwnd_; DXGI_SWAP_CHAIN_DESC1 desc = {}; desc.Width = width; desc.Height = height; desc.BufferCount = count; desc.SwapEffect = DXGI_SWAP_EFFECT_FLIP_DISCARD; desc.SampleDesc.Count = 1; desc.BufferUsage = DXGI_USAGE_RENDER_TARGET_OUTPUT; desc.Format = convert_vk_format(format.format); if (!desc.Format) return false; auto color_space = convert_vk_color_space(format.colorSpace); if (color_space == DXGI_COLOR_SPACE_RESERVED) return false; BOOL allow_tear = FALSE; if (SUCCEEDED(dxgi_factory->CheckFeatureSupport( DXGI_FEATURE_PRESENT_ALLOW_TEARING, &allow_tear, sizeof(allow_tear)) && allow_tear)) { desc.Flags = DXGI_SWAP_CHAIN_FLAG_ALLOW_TEARING; allow_tearing = true; } desc.Flags |= DXGI_SWAP_CHAIN_FLAG_FRAME_LATENCY_WAITABLE_OBJECT; ComPtr swap; HRESULT hr; reset_backbuffer_state(); // If we already have a swapchain we can just use ResizeBuffers. if (!swapchain) { if (FAILED(hr = dxgi_factory->CreateSwapChainForHwnd( queue.Get(), hwnd, &desc, nullptr, nullptr, &swap))) { LOGE("Failed to create swapchain, hr #%x.\n", unsigned(hr)); return false; } completed_presents = 0; completed_waits = 0; if (FAILED(swap.As(&swapchain))) { LOGE("Failed to query swapchain interface.\n"); return false; } if (latency_handle) CloseHandle(latency_handle); latency_handle = swapchain->GetFrameLatencyWaitableObject(); if (!latency_handle) { LOGE("Failed to query latency handle.\n"); return false; } // Drop semaphore to 0 right away to make code less awkward later. if (WaitForSingleObject(latency_handle, INFINITE) != WAIT_OBJECT_0) { LOGE("Failed to wait for latency object.\n"); return false; } } else { if (FAILED(hr = swapchain->ResizeBuffers(count, width, height, desc.Format, desc.Flags))) { LOGE("Failed to resize buffers, hr #%x.\n", unsigned(hr)); return false; } } if (FAILED(dxgi_factory->MakeWindowAssociation(hwnd, DXGI_MWA_NO_ALT_ENTER | DXGI_MWA_NO_WINDOW_CHANGES))) { LOGE("Failed to make window association.\n"); return false; } surface_format = format; UINT space_support = 0; if (FAILED(swapchain->CheckColorSpaceSupport(color_space, &space_support)) || ((space_support & DXGI_SWAP_CHAIN_COLOR_SPACE_SUPPORT_FLAG_PRESENT) == 0)) { // Fallback to SDR if HDR doesn't pass check. if (FAILED(swapchain->CheckColorSpaceSupport(DXGI_COLOR_SPACE_RGB_FULL_G22_NONE_P709, &space_support)) || ((space_support & DXGI_SWAP_CHAIN_COLOR_SPACE_SUPPORT_FLAG_PRESENT) == 0)) { return false; } LOGW("HDR10 not supported by DXGI swapchain, falling back to SDR.\n"); surface_format.colorSpace = VK_COLOR_SPACE_SRGB_NONLINEAR_KHR; color_space = DXGI_COLOR_SPACE_RGB_FULL_G22_NONE_P709; } if (FAILED(swapchain->SetColorSpace1(color_space))) { LOGE("Failed to set color space.\n"); return false; } backbuffers.resize(desc.BufferCount); for (unsigned i = 0; i < desc.BufferCount; i++) if (!setup_per_frame_state(backbuffers[i], i)) return false; ExternalHandle imported_image; imported_image.memory_handle_type = VK_EXTERNAL_MEMORY_HANDLE_TYPE_D3D12_RESOURCE_BIT; D3D12_RESOURCE_DESC blit_desc = {}; blit_desc.Width = width; blit_desc.Height = height; blit_desc.Format = desc.Format; blit_desc.Flags = D3D12_RESOURCE_FLAG_ALLOW_RENDER_TARGET; blit_desc.SampleDesc.Count = 1; blit_desc.DepthOrArraySize = 1; blit_desc.MipLevels = 1; blit_desc.Layout = D3D12_TEXTURE_LAYOUT_UNKNOWN; blit_desc.Dimension = D3D12_RESOURCE_DIMENSION_TEXTURE2D; D3D12_HEAP_PROPERTIES heap_props = {}; heap_props.Type = D3D12_HEAP_TYPE_DEFAULT; blit_backbuffer.Reset(); if (FAILED(hr = device->CreateCommittedResource(&heap_props, D3D12_HEAP_FLAG_SHARED, &blit_desc, D3D12_RESOURCE_STATE_COMMON, nullptr, IID_PPV_ARGS(&blit_backbuffer)))) { LOGE("Failed to create blit render target, hr #%x.\n", unsigned(hr)); return false; } if (FAILED(hr = device->CreateSharedHandle(blit_backbuffer.Get(), nullptr, GENERIC_ALL, nullptr, &imported_image.handle))) { LOGE("Failed to create shared handle, hr #%x.\n", unsigned(hr)); return false; } auto image_info = ImageCreateInfo::render_target(width, height, format.format); image_info.initial_layout = VK_IMAGE_LAYOUT_UNDEFINED; image_info.usage = VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT | VK_IMAGE_USAGE_TRANSFER_SRC_BIT; image_info.misc = IMAGE_MISC_EXTERNAL_MEMORY_BIT; image_info.external = imported_image; vulkan_backbuffer = vk_device->create_image(image_info); if (!vulkan_backbuffer) { LOGE("Failed to create shared Vulkan image, hr #%x.\n", unsigned(hr)); return false; } vulkan_backbuffer->set_swapchain_layout(VK_IMAGE_LAYOUT_PRESENT_SRC_KHR); return true; } VkSurfaceFormatKHR DXGIInteropSwapchain::get_current_surface_format() const { return surface_format; } bool DXGIInteropSwapchain::wait_latency(unsigned latency_frames) { uint64_t target_wait_count = completed_presents - latency_frames; if (latency_handle && (target_wait_count & (1ull << 63)) == 0) { while (completed_waits < target_wait_count) { if (WaitForSingleObject(latency_handle, INFINITE) != WAIT_OBJECT_0) { LOGE("Failed to wait for latency object.\n"); return false; } completed_waits++; } } return true; } bool DXGIInteropSwapchain::acquire(Semaphore &acquire_semaphore) { // AMD workaround. Driver freaks out if trying to wait for D3D12 timeline value of 0. queue->Signal(fence.Get(), ++fence_value); acquire_semaphore = vk_device->request_timeline_semaphore_as_binary(*vk_fence, fence_value); return true; } bool DXGIInteropSwapchain::present(Vulkan::Semaphore release_semaphore, bool vsync) { unsigned index = swapchain->GetCurrentBackBufferIndex(); auto &per_frame = backbuffers[index]; vk_device->add_wait_semaphore(CommandBuffer::Type::Generic, std::move(release_semaphore), VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT, true); auto cmd = vk_device->request_command_buffer(); cmd->release_image_barrier(*vulkan_backbuffer, VK_IMAGE_LAYOUT_PRESENT_SRC_KHR, VK_IMAGE_LAYOUT_GENERAL, VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT, 0); vk_device->submit(cmd); auto timeline_signal = vk_device->request_timeline_semaphore_as_binary(*vk_fence, ++fence_value); vk_device->submit_empty(CommandBuffer::Type::Generic, nullptr, timeline_signal.get()); queue->Wait(fence.Get(), fence_value); fence->SetEventOnCompletion(per_frame.wait_fence_value, nullptr); if (FAILED(per_frame.allocator->Reset())) { LOGE("Failed to reset command allocator.\n"); return false; } list->Reset(per_frame.allocator.Get(), nullptr); D3D12_RESOURCE_BARRIER barrier = {}; barrier.Type = D3D12_RESOURCE_BARRIER_TYPE_TRANSITION; barrier.Transition.Subresource = D3D12_RESOURCE_BARRIER_ALL_SUBRESOURCES; barrier.Transition.pResource = per_frame.backbuffer.Get(); barrier.Transition.StateBefore = D3D12_RESOURCE_STATE_PRESENT; barrier.Transition.StateAfter = D3D12_RESOURCE_STATE_COPY_DEST; list->ResourceBarrier(1, &barrier); D3D12_TEXTURE_COPY_LOCATION dst = {}, src = {}; dst.Type = D3D12_TEXTURE_COPY_TYPE_SUBRESOURCE_INDEX; src.Type = D3D12_TEXTURE_COPY_TYPE_SUBRESOURCE_INDEX; dst.pResource = per_frame.backbuffer.Get(); src.pResource = blit_backbuffer.Get(); list->CopyTextureRegion(&dst, 0, 0, 0, &src, nullptr); barrier.Transition.StateBefore = D3D12_RESOURCE_STATE_COPY_DEST; barrier.Transition.StateAfter = D3D12_RESOURCE_STATE_PRESENT; list->ResourceBarrier(1, &barrier); if (FAILED(list->Close())) { LOGE("Failed to close command list.\n"); return false; } ID3D12CommandList *cmdlist = list.Get(); queue->ExecuteCommandLists(1, &cmdlist); queue->Signal(fence.Get(), ++fence_value); per_frame.wait_fence_value = fence_value; HRESULT hr = swapchain->Present(vsync ? 1 : 0, !vsync && allow_tearing ? DXGI_PRESENT_ALLOW_TEARING : 0); if (FAILED(hr)) { LOGE("Failed to present, hr #%x.\n", unsigned(hr)); return false; } completed_presents++; return true; } }