From a63bce691834ca8202026cd2ccc41b293a4e9f34 Mon Sep 17 00:00:00 2001 From: Hans-Kristian Arntzen Date: Sun, 25 Feb 2024 13:23:56 +0100 Subject: [PATCH] Implement a DXGI interop swapchain. --- application/platforms/application_sdl3.cpp | 13 + vulkan/CMakeLists.txt | 4 + vulkan/wsi.cpp | 223 ++++++++- vulkan/wsi.hpp | 13 + vulkan/wsi_dxgi.cpp | 514 +++++++++++++++++++++ vulkan/wsi_dxgi.hpp | 83 ++++ 6 files changed, 836 insertions(+), 14 deletions(-) create mode 100644 vulkan/wsi_dxgi.cpp create mode 100644 vulkan/wsi_dxgi.hpp diff --git a/application/platforms/application_sdl3.cpp b/application/platforms/application_sdl3.cpp index ef69d49d..077cb789 100644 --- a/application/platforms/application_sdl3.cpp +++ b/application/platforms/application_sdl3.cpp @@ -259,6 +259,19 @@ struct WSIPlatformSDL : GraniteWSIPlatform }); } + uintptr_t get_native_window() override + { +#ifdef _WIN32 + SDL_PropertiesID props = SDL_GetWindowProperties(window); + SDL_LockProperties(props); + auto hwnd = static_cast(SDL_GetProperty(props, "SDL.window.win32.hwnd", nullptr)); + SDL_UnlockProperties(props); + return reinterpret_cast(hwnd); +#else + return 0; +#endif + } + void toggle_fullscreen() { bool is_fullscreen = (SDL_GetWindowFlags(window) & SDL_WINDOW_FULLSCREEN) != 0; diff --git a/vulkan/CMakeLists.txt b/vulkan/CMakeLists.txt index 214c7bc6..0b6fb998 100644 --- a/vulkan/CMakeLists.txt +++ b/vulkan/CMakeLists.txt @@ -29,6 +29,10 @@ add_granite_internal_lib(granite-vulkan query_pool.cpp query_pool.hpp texture/texture_format.cpp texture/texture_format.hpp) +if (WIN32) + target_sources(granite-vulkan PRIVATE wsi_dxgi.cpp wsi_dxgi.hpp) +endif() + target_include_directories(granite-vulkan PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}) if (GRANITE_RENDERDOC_CAPTURE) diff --git a/vulkan/wsi.cpp b/vulkan/wsi.cpp index a66517a7..627b5592 100644 --- a/vulkan/wsi.cpp +++ b/vulkan/wsi.cpp @@ -83,6 +83,11 @@ uintptr_t WSIPlatform::get_fullscreen_monitor() return 0; } +uintptr_t WSIPlatform::get_native_window() +{ + return 0; +} + const VkApplicationInfo *WSIPlatform::get_application_info() { return nullptr; @@ -158,6 +163,12 @@ bool WSI::init_device() device = Util::make_handle(); device->set_context(*context); platform->event_device_created(device.get()); + +#ifdef _WIN32 + dxgi.reset(new DXGIInteropSwapchain); + if (!dxgi->init_interop_device(*device)) + dxgi.reset(); +#endif return true; } @@ -166,15 +177,99 @@ bool WSI::init_device(DeviceHandle device_handle) VK_ASSERT(context); device = std::move(device_handle); platform->event_device_created(device.get()); + +#ifdef _WIN32 + dxgi.reset(new DXGIInteropSwapchain); + if (!dxgi->init_interop_device(*device)) + dxgi.reset(); +#endif return true; } +#ifdef _WIN32 +bool WSI::init_surface_swapchain_dxgi(unsigned width, unsigned height) +{ + if (!dxgi) + return false; + + // Anything fancy like compute present cannot use DXGI. + if (current_extra_usage) + return false; + + HWND hwnd = reinterpret_cast(platform->get_native_window()); + if (!hwnd) + return false; + + VkSurfaceFormatKHR format = {}; + switch (current_backbuffer_format) + { + case BackbufferFormat::UNORM: + format = { VK_FORMAT_B8G8R8A8_UNORM, VK_COLOR_SPACE_SRGB_NONLINEAR_KHR }; + break; + + case BackbufferFormat::sRGB: + format = { VK_FORMAT_B8G8R8A8_SRGB, VK_COLOR_SPACE_SRGB_NONLINEAR_KHR }; + break; + + case BackbufferFormat::HDR10: + format = { VK_FORMAT_A2B10G10R10_UNORM_PACK32, VK_COLOR_SPACE_HDR10_ST2084_EXT }; + break; + } + + constexpr unsigned num_images = 3; + + if (!dxgi->init_swapchain(hwnd, format, width, height, num_images)) + return false; + + LOGI("Initialized DXGI interop swapchain!\n"); + + swapchain_width = width; + swapchain_height = height; + swapchain_aspect_ratio = platform->get_aspect_ratio(); + swapchain_current_prerotate = VK_SURFACE_TRANSFORM_IDENTITY_BIT_KHR; + swapchain_surface_format = dxgi->get_current_surface_format(); + has_acquired_swapchain_index = false; + + const uint32_t queue_present_support = 1u << context->get_queue_info().family_indices[QUEUE_INDEX_GRAPHICS]; + device->set_swapchain_queue_family_support(queue_present_support); + + swapchain_images.clear(); + for (unsigned i = 0; i < num_images; i++) + swapchain_images.push_back(dxgi->get_vulkan_image(i)); + + device->init_swapchain(swapchain_images, swapchain_width, swapchain_height, + swapchain_surface_format.format, + swapchain_current_prerotate, + VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT); + platform->get_frame_timer().reset(); + + platform->event_swapchain_destroyed(); + platform->event_swapchain_created(device.get(), swapchain, swapchain_width, swapchain_height, + swapchain_aspect_ratio, num_images, + swapchain_surface_format.format, + swapchain_surface_format.colorSpace, + swapchain_current_prerotate); + + return true; +} +#endif + bool WSI::init_surface_swapchain() { VK_ASSERT(surface == VK_NULL_HANDLE); VK_ASSERT(context); VK_ASSERT(device); + unsigned width = platform->get_surface_width(); + unsigned height = platform->get_surface_height(); + +#ifdef _WIN32 + if (init_surface_swapchain_dxgi(width, height)) + return true; + else + dxgi.reset(); +#endif + surface = platform->create_surface(context->get_instance(), context->get_gpu()); if (surface == VK_NULL_HANDLE) { @@ -182,8 +277,6 @@ bool WSI::init_surface_swapchain() return false; } - unsigned width = platform->get_surface_width(); - unsigned height = platform->get_surface_height(); swapchain_aspect_ratio = platform->get_aspect_ratio(); VkBool32 supported = VK_FALSE; @@ -368,6 +461,11 @@ void WSI::drain_swapchain(bool in_tear_down) void WSI::tear_down_swapchain() { +#ifdef _WIN32 + // We only do explicit teardown on exit. + dxgi.reset(); +#endif + drain_swapchain(true); platform->event_swapchain_destroyed(); table->vkDestroySwapchainKHR(context->get_device(), swapchain, nullptr); @@ -468,6 +566,55 @@ void WSI::set_low_latency_mode(bool enable) low_latency_mode_enable = enable; } +#ifdef _WIN32 +bool WSI::begin_frame_dxgi() +{ + Semaphore acquire; + + while (!acquire) + { + if (!dxgi->acquire(acquire, swapchain_index)) + return false; + + acquire->signal_external(); + has_acquired_swapchain_index = true; + + // Poll after acquire as well for optimal latency. + platform->poll_input(); + + // Polling input may trigger a resize event. Trying to present in that situation without ResizeBuffers + // cause wonky issues on DXGI. + if (platform->should_resize()) + update_framebuffer(platform->get_surface_width(), platform->get_surface_height()); + + // If update_framebuffer caused a resize, we won't have an acquire index anymore, reacquire. + if (!has_acquired_swapchain_index) + acquire.reset(); + } + + auto wait_ts = device->write_calibrated_timestamp(); + if (!dxgi->wait_latency(present_frame_latency)) + { + LOGE("Failed to wait on latency handle.\n"); + return false; + } + device->register_time_interval("WSI", std::move(wait_ts), device->write_calibrated_timestamp(), + "DXGI wait latency"); + + auto frame_time = platform->get_frame_timer().frame(); + auto elapsed_time = platform->get_frame_timer().get_elapsed(); + + smooth_frame_time = frame_time; + smooth_elapsed_time = elapsed_time; + + platform->event_frame_tick(frame_time, elapsed_time); + platform->event_swapchain_index(device.get(), swapchain_index); + device->set_acquire_semaphore(swapchain_index, std::move(acquire)); + + return true; +} +#endif + bool WSI::begin_frame() { if (frame_is_external) @@ -478,14 +625,31 @@ bool WSI::begin_frame() #endif device->next_frame_context(); + external_release.reset(); #ifdef VULKAN_WSI_TIMING_DEBUG auto next_frame_end = Util::get_current_time_nsecs(); LOGI("Waited for vacant frame context for %.3f ms.\n", (next_frame_end - next_frame_start) * 1e-6); #endif - if (swapchain == VK_NULL_HANDLE || platform->should_resize() || swapchain_is_suboptimal) - update_framebuffer(platform->get_surface_width(), platform->get_surface_height()); +#ifdef _WIN32 + if (dxgi) + { + if (platform->should_resize()) + update_framebuffer(platform->get_surface_width(), platform->get_surface_height()); + + if (has_acquired_swapchain_index) + return true; + return begin_frame_dxgi(); + } + else +#endif + { + if (swapchain == VK_NULL_HANDLE || platform->should_resize() || swapchain_is_suboptimal) + update_framebuffer(platform->get_surface_width(), platform->get_surface_height()); + if (has_acquired_swapchain_index) + return true; + } if (swapchain == VK_NULL_HANDLE) { @@ -493,11 +657,6 @@ bool WSI::begin_frame() return false; } - if (has_acquired_swapchain_index) - return true; - - external_release.reset(); - VkResult result; do { @@ -597,6 +756,17 @@ bool WSI::begin_frame() return true; } +#ifdef _WIN32 +bool WSI::end_frame_dxgi() +{ + auto release = device->consume_release_semaphore(); + VK_ASSERT(release); + VK_ASSERT(release->is_signalled()); + VK_ASSERT(!release->is_pending_wait()); + return dxgi->present(std::move(release), current_present_mode == PresentMode::SyncToVBlank); +} +#endif + bool WSI::end_frame() { device->end_frame_context(); @@ -616,10 +786,16 @@ bool WSI::end_frame() has_acquired_swapchain_index = false; +#ifdef _WIN32 + if (dxgi) + return end_frame_dxgi(); +#endif + auto release = device->consume_release_semaphore(); VK_ASSERT(release); VK_ASSERT(release->is_signalled()); VK_ASSERT(!release->is_pending_wait()); + auto release_semaphore = release->get_semaphore(); VK_ASSERT(release_semaphore != VK_NULL_HANDLE); @@ -752,12 +928,22 @@ void WSI::update_framebuffer(unsigned width, unsigned height) { if (context && device) { - drain_swapchain(false); - if (blocking_init_swapchain(width, height)) +#ifdef _WIN32 + if (dxgi) { - device->init_swapchain(swapchain_images, swapchain_width, swapchain_height, swapchain_surface_format.format, - swapchain_current_prerotate, - current_extra_usage | VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT); + if (!init_surface_swapchain_dxgi(width, height)) + LOGE("Failed to resize DXGI swapchain.\n"); + } + else +#endif + { + drain_swapchain(false); + if (blocking_init_swapchain(width, height)) + { + device->init_swapchain(swapchain_images, swapchain_width, swapchain_height, + swapchain_surface_format.format, swapchain_current_prerotate, + current_extra_usage | VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT); + } } } @@ -770,6 +956,15 @@ bool WSI::update_active_presentation_mode(PresentMode mode) if (current_present_mode == mode) return true; +#ifdef _WIN32 + // We set this on Present time. + if (dxgi) + { + current_present_mode = mode; + return true; + } +#endif + for (auto m : present_mode_compat_group) { bool match = false; diff --git a/vulkan/wsi.hpp b/vulkan/wsi.hpp index 4ebb4179..15ee49c1 100644 --- a/vulkan/wsi.hpp +++ b/vulkan/wsi.hpp @@ -29,6 +29,11 @@ #include #include #include +#include + +#ifdef _WIN32 +#include "wsi_dxgi.hpp" +#endif namespace Granite { @@ -120,6 +125,7 @@ class WSIPlatform virtual void set_window_title(const std::string &title); virtual uintptr_t get_fullscreen_monitor(); + virtual uintptr_t get_native_window(); virtual const VkApplicationInfo *get_application_info(); @@ -359,5 +365,12 @@ class WSI void nonblock_delete_swapchains(); VkSurfaceFormatKHR find_suitable_present_format(const std::vector &formats, BackbufferFormat desired_format) const; + +#ifdef _WIN32 + std::unique_ptr dxgi; + bool init_surface_swapchain_dxgi(unsigned width, unsigned height); + bool begin_frame_dxgi(); + bool end_frame_dxgi(); +#endif }; } diff --git a/vulkan/wsi_dxgi.cpp b/vulkan/wsi_dxgi.cpp new file mode 100644 index 00000000..1b36290c --- /dev/null +++ b/vulkan/wsi_dxgi.cpp @@ -0,0 +1,514 @@ +/* Copyright (c) 2017-2023 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(unsigned index) const +{ + VK_ASSERT(index < backbuffers.size()); + return backbuffers[index].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_STUDIO_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, + unsigned width, unsigned height, VkFormat format) +{ + 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; + } + + ExternalHandle imported_image; + imported_image.memory_handle_type = VK_EXTERNAL_MEMORY_HANDLE_TYPE_D3D12_RESOURCE_BIT; + + if (FAILED(hr = device->CreateSharedHandle( + state.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); + image_info.initial_layout = VK_IMAGE_LAYOUT_UNDEFINED; + image_info.usage = VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT; + image_info.misc = IMAGE_MISC_EXTERNAL_MEMORY_BIT; + image_info.external = imported_image; + + state.vulkan_backbuffer = vk_device->create_image(image_info); + if (!state.vulkan_backbuffer) + { + LOGE("Failed to create shared Vulkan image, hr #%x.\n", unsigned(hr)); + return false; + } + state.vulkan_backbuffer->set_swapchain_layout(VK_IMAGE_LAYOUT_PRESENT_SRC_KHR); + + 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_STUDIO_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_STUDIO_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, width, height, format.format)) + return false; + + 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, uint32_t &index) +{ + index = swapchain->GetCurrentBackBufferIndex(); + auto &per_frame = backbuffers[index]; + 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); + + // Somewhat dubious idea, but if the barrier has completed, this should function as an acquire + // so that Vulkan can start rendering into the image. + 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_RENDER_TARGET; + list->ResourceBarrier(1, &barrier); + list->DiscardResource(per_frame.backbuffer.Get(), nullptr); + + 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; + + 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->image_barrier_release(*per_frame.vulkan_backbuffer, + VK_IMAGE_LAYOUT_PRESENT_SRC_KHR, VK_IMAGE_LAYOUT_GENERAL, + VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT, 0, + VK_QUEUE_FAMILY_EXTERNAL); + + 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); + + 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_RENDER_TARGET; + 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; +} +} diff --git a/vulkan/wsi_dxgi.hpp b/vulkan/wsi_dxgi.hpp new file mode 100644 index 00000000..85ff7e82 --- /dev/null +++ b/vulkan/wsi_dxgi.hpp @@ -0,0 +1,83 @@ +/* Copyright (c) 2017-2023 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. + */ + +#pragma once + +#include "device.hpp" +#include "image.hpp" +#include "d3d12.h" +#include "dxgi1_6.h" +#include "small_vector.hpp" +#include "dynamic_library.hpp" +#include + +namespace Vulkan +{ +template +using ComPtr = Microsoft::WRL::ComPtr; + +class DXGIInteropSwapchain +{ +public: + bool init_interop_device(Device &device); + ~DXGIInteropSwapchain(); + + bool init_swapchain(HWND hwnd, VkSurfaceFormatKHR format, unsigned width, unsigned height, unsigned count); + VkImage get_vulkan_image(unsigned index) const; + VkSurfaceFormatKHR get_current_surface_format() const; + + bool acquire(Semaphore &acquire_semaphore, uint32_t &index); + bool present(Semaphore release_semaphore, bool vsync); + bool wait_latency(unsigned latency_frames); + +private: + Device *vk_device = nullptr; + Util::DynamicLibrary d3d12_lib, dxgi_lib; + HWND hwnd = nullptr; + HANDLE latency_handle = nullptr; + ComPtr device; + ComPtr queue; + ComPtr dxgi_factory; + ComPtr swapchain; + ComPtr list; + ComPtr fence; + Semaphore vk_fence; + uint64_t fence_value = 0; + VkSurfaceFormatKHR surface_format = {}; + bool allow_tearing = false; + + struct PerFrameState + { + ComPtr allocator; + ComPtr backbuffer; + ImageHandle vulkan_backbuffer; + uint64_t wait_fence_value = 0; + }; + Util::SmallVector backbuffers; + + bool setup_per_frame_state(PerFrameState &state, unsigned index, unsigned width, unsigned height, VkFormat format); + void reset_backbuffer_state(); + + uint64_t completed_presents = 0; + uint64_t completed_waits = 0; +}; +}