/* 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. */ #define NOMINMAX #include "wsi.hpp" #include "environment.hpp" #include #ifdef _WIN32 #define WIN32_LEAN_AND_MEAN #include #endif #if defined(ANDROID) && defined(HAVE_SWAPPY) #include "swappy/swappyVk.h" #endif static constexpr uint32_t PresentTimingQueueSize = 16; namespace Vulkan { WSI::WSI() { // With frame latency of 1, we get the ideal latency where // we present, and then wait for the previous present to complete. // Once this unblocks, it means that the present we just queued up is scheduled to complete next vblank, // and the next frame to be recorded will have to be ready in 2 frames. // This is ideal, since worst case for full performance, we will have a pipeline of CPU -> GPU, // where CPU can spend 1 frame's worth of time, and GPU can spend one frame's worth of time. // For mobile, opt for 2 frames of latency, since TBDR likes deeper pipelines and we can absorb more // surfaceflinger jank. #ifdef ANDROID present_frame_latency = 2; #else present_frame_latency = 1; #endif present_frame_latency = Util::get_environment_uint("GRANITE_VULKAN_PRESENT_WAIT_LATENCY", present_frame_latency); LOGI("Targeting VK_KHR_present_wait latency to %u frames.\n", present_frame_latency); // Primaries are ST.2020 with D65 whitepoint as specified. hdr_metadata.displayPrimaryRed = { 0.708f, 0.292f }; hdr_metadata.displayPrimaryGreen = { 0.170f, 0.797f }; hdr_metadata.displayPrimaryBlue = { 0.131f, 0.046f }; hdr_metadata.whitePoint = { 0.3127f, 0.3290f }; // HDR10 range? Just arbitrary values, user can override later. hdr_metadata.minLuminance = 0.01f; hdr_metadata.maxLuminance = 1000.0f; hdr_metadata.maxContentLightLevel = 1000.0f; hdr_metadata.maxFrameAverageLightLevel = 200.0f; } void WSI::set_present_wait_latency(uint32_t latency) { present_frame_latency = latency; } void WSI::set_hdr_metadata(const VkHdrMetadataEXT &hdr) { hdr_metadata = hdr; valid_hdr_metadata = true; if (swapchain && swapchain_surface_format.colorSpace == VK_COLOR_SPACE_HDR10_ST2084_EXT && device->get_device_features().supports_hdr_metadata) { table->vkSetHdrMetadataEXT(device->get_device(), 1, &swapchain, &hdr_metadata); } } void WSIPlatform::set_window_title(const std::string &) { } void WSIPlatform::destroy_surface(VkInstance instance, VkSurfaceKHR surface) { vkDestroySurfaceKHR(instance, surface, nullptr); } uintptr_t WSIPlatform::get_fullscreen_monitor() { return 0; } uintptr_t WSIPlatform::get_native_window() { return 0; } const VkApplicationInfo *WSIPlatform::get_application_info() { return nullptr; } void WSI::set_window_title(const std::string &title) { if (platform) platform->set_window_title(title); } double WSI::get_smooth_elapsed_time() const { return smooth_elapsed_time; } double WSI::get_smooth_frame_time() const { return smooth_frame_time; } bool WSI::init_from_existing_context(ContextHandle existing_context) { VK_ASSERT(platform); if (platform && device) platform->event_device_destroyed(); device.reset(); context = std::move(existing_context); table = &context->get_device_table(); return true; } bool WSI::init_external_swapchain(std::vector swapchain_images_) { VK_ASSERT(context); VK_ASSERT(device); swapchain_width = platform->get_surface_width(); swapchain_height = platform->get_surface_height(); swapchain_aspect_ratio = platform->get_aspect_ratio(); external_swapchain_images = std::move(swapchain_images_); swapchain_width = external_swapchain_images.front()->get_width(); swapchain_height = external_swapchain_images.front()->get_height(); swapchain_surface_format = { external_swapchain_images.front()->get_format(), VK_COLOR_SPACE_SRGB_NONLINEAR_KHR }; LOGI("Created swapchain %u x %u (fmt: %u).\n", swapchain_width, swapchain_height, static_cast(swapchain_surface_format.format)); platform->event_swapchain_destroyed(); platform->event_swapchain_created(device.get(), VK_NULL_HANDLE, swapchain_width, swapchain_height, swapchain_aspect_ratio, external_swapchain_images.size(), swapchain_surface_format.format, swapchain_surface_format.colorSpace, swapchain_current_prerotate); device->init_external_swapchain(this->external_swapchain_images); platform->get_frame_timer().reset(); external_acquire.reset(); external_release.reset(); return true; } void WSI::set_platform(WSIPlatform *platform_) { platform = platform_; } bool WSI::init_device() { VK_ASSERT(context); VK_ASSERT(!device); device = Util::make_handle(); device->set_context(*context); platform->event_device_created(device.get()); #ifdef HAVE_WSI_DXGI_INTEROP dxgi.reset(new DXGIInteropSwapchain); if (!dxgi->init_interop_device(*device)) dxgi.reset(); else platform->get_frame_timer().reset(); #endif return true; } bool WSI::init_device(DeviceHandle device_handle) { VK_ASSERT(context); device = std::move(device_handle); platform->event_device_created(device.get()); #ifdef HAVE_WSI_DXGI_INTEROP dxgi.reset(new DXGIInteropSwapchain); if (!dxgi->init_interop_device(*device)) dxgi.reset(); else platform->get_frame_timer().reset(); #endif return true; } #ifdef HAVE_WSI_DXGI_INTEROP 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_A2B10G10R10_UNORM_PACK32, 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; case BackbufferFormat::scRGB: format = { VK_FORMAT_R16G16B16A16_SFLOAT, VK_COLOR_SPACE_EXTENDED_SRGB_LINEAR_EXT }; break; default: return false; } 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 = { dxgi->get_vulkan_image() }; device->init_swapchain(swapchain_images, swapchain_width, swapchain_height, swapchain_surface_format.format, swapchain_current_prerotate, VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT | VK_IMAGE_USAGE_TRANSFER_SRC_BIT); 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 HAVE_WSI_DXGI_INTEROP 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) { LOGE("Failed to create VkSurfaceKHR.\n"); return false; } swapchain_aspect_ratio = platform->get_aspect_ratio(); VkBool32 supported = VK_FALSE; uint32_t queue_present_support = 0; // TODO: Ideally we need to create surface earlier and negotiate physical device based on that support. for (auto &index : context->get_queue_info().family_indices) { if (index != VK_QUEUE_FAMILY_IGNORED) { if (vkGetPhysicalDeviceSurfaceSupportKHR(context->get_gpu(), index, surface, &supported) == VK_SUCCESS && supported) { queue_present_support |= 1u << index; } } } if ((queue_present_support & (1u << context->get_queue_info().family_indices[QUEUE_INDEX_GRAPHICS])) == 0) { LOGE("No presentation queue found for GPU. Is it connected to a display?\n"); return false; } device->set_swapchain_queue_family_support(queue_present_support); if (!blocking_init_swapchain(width, height)) { LOGE("Failed to create swapchain.\n"); return false; } 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); platform->get_frame_timer().reset(); return true; } bool WSI::init_simple(unsigned num_thread_indices, const Context::SystemHandles &system_handles) { if (!init_context_from_platform(num_thread_indices, system_handles)) return false; if (!init_device()) return false; if (!init_surface_swapchain()) return false; return true; } bool WSI::init_context_from_platform(unsigned num_thread_indices, const Context::SystemHandles &system_handles) { VK_ASSERT(platform); auto instance_ext = platform->get_instance_extensions(); auto device_ext = platform->get_device_extensions(); auto new_context = Util::make_handle(); #ifdef HAVE_FFMPEG_VULKAN constexpr ContextCreationFlags video_context_flags = CONTEXT_CREATION_ENABLE_VIDEO_DECODE_BIT | CONTEXT_CREATION_ENABLE_VIDEO_ENCODE_BIT | CONTEXT_CREATION_ENABLE_VIDEO_H264_BIT | CONTEXT_CREATION_ENABLE_VIDEO_H265_BIT; #else constexpr ContextCreationFlags video_context_flags = 0; #endif new_context->set_application_info(platform->get_application_info()); new_context->set_num_thread_indices(num_thread_indices); new_context->set_system_handles(system_handles); constexpr ContextCreationFlags context_flags = CONTEXT_CREATION_ENABLE_ADVANCED_WSI_BIT | CONTEXT_CREATION_ENABLE_PUSH_DESCRIPTOR_BIT | CONTEXT_CREATION_ENABLE_DESCRIPTOR_BUFFER_BIT | CONTEXT_CREATION_ENABLE_DESCRIPTOR_HEAP_BIT | #ifdef GRANITE_VULKAN_SYSTEM_HANDLES //CONTEXT_CREATION_ENABLE_PIPELINE_BINARY_BIT | #endif video_context_flags; if (!new_context->init_instance( instance_ext.data(), instance_ext.size(), context_flags)) { LOGE("Failed to create Vulkan instance.\n"); return false; } VkSurfaceKHR tmp_surface = platform->create_surface(new_context->get_instance(), VK_NULL_HANDLE); bool ret = new_context->init_device( VK_NULL_HANDLE, tmp_surface, device_ext.data(), device_ext.size(), context_flags); if (tmp_surface) platform->destroy_surface(new_context->get_instance(), tmp_surface); if (!ret) { LOGE("Failed to create Vulkan device.\n"); return false; } return init_from_existing_context(std::move(new_context)); } void WSI::reinit_surface_and_swapchain(VkSurfaceKHR new_surface) { LOGI("init_surface_and_swapchain()\n"); if (new_surface != VK_NULL_HANDLE) { VK_ASSERT(surface == VK_NULL_HANDLE); surface = new_surface; } swapchain_width = platform->get_surface_width(); swapchain_height = platform->get_surface_height(); update_framebuffer(swapchain_width, swapchain_height); } VkResult WSI::wait_for_present(uint64_t id, uint64_t timeout) { if (!swapchain) return VK_SUCCESS; if (id > present_last_id) return VK_NOT_READY; bool timeout_is_fault = false; if (device->get_device_features().supports_post_mortem && timeout == UINT64_MAX) { timeout = PostMortemTimeout; timeout_is_fault = true; } VkResult vr; if (supports_present_wait2 && device->get_device_features().present_wait2_features.presentWait2) { VkPresentWait2InfoKHR wait_info = { VK_STRUCTURE_TYPE_PRESENT_WAIT_2_INFO_KHR }; wait_info.presentId = id; wait_info.timeout = timeout; vr = table->vkWaitForPresent2KHR(context->get_device(), swapchain, &wait_info); } else if (device->get_device_features().present_wait_features.presentWait) vr = table->vkWaitForPresentKHR(context->get_device(), swapchain, id, timeout); else return VK_NOT_READY; if (timeout_is_fault && (vr == VK_TIMEOUT || vr == VK_ERROR_DEVICE_LOST)) device->managers.breadcrumbs.notify_device_hung(); return vr; } void WSI::nonblock_delete_swapchain_resources() { // If we can help it, don't try to destroy swapchains until we know the new swapchain has presented at least one frame on screen. if (swapchain != VK_NULL_HANDLE && wait_for_present(1, 0) != VK_SUCCESS) return; Util::SmallVector keep; size_t pending = deferred_swapchains.size(); for (auto &swap : deferred_swapchains) { if (!swap.fence || swap.fence->wait_timeout(0)) { platform->destroy_swapchain_resources(swap.swapchain); table->vkDestroySwapchainKHR(device->get_device(), swap.swapchain, nullptr); } else if (pending >= 2) { swap.fence->wait(); platform->destroy_swapchain_resources(swap.swapchain); table->vkDestroySwapchainKHR(device->get_device(), swap.swapchain, nullptr); } else keep.push_back(std::move(swap)); pending--; } deferred_swapchains = std::move(keep); auto itr = std::remove_if(deferred_semaphore.begin(), deferred_semaphore.end(), [](DeferredDeletionSemaphore &sem) { return !sem.fence || sem.fence->wait_timeout(0); }); deferred_semaphore.erase(itr, deferred_semaphore.end()); } void WSI::drain_swapchain(bool in_tear_down) { release_semaphores.clear(); device->set_acquire_semaphore(0, Semaphore{}); device->consume_release_semaphore(); if (device->get_device_features().swapchain_maintenance1_features.swapchainMaintenance1) { // If we're just resizing, there's no need to block, defer deletions for later. if (in_tear_down) { if (last_present_fence) { last_present_fence->wait(); last_present_fence.reset(); } for (auto &old_swap : deferred_swapchains) { if (old_swap.fence) old_swap.fence->wait(); platform->destroy_swapchain_resources(old_swap.swapchain); table->vkDestroySwapchainKHR(context->get_device(), old_swap.swapchain, nullptr); } deferred_swapchains.clear(); deferred_semaphore.clear(); } } else if (swapchain != VK_NULL_HANDLE) { wait_for_present(present_last_id); device->external_queue_lock(); table->vkDeviceWaitIdle(device->get_device()); device->external_queue_unlock(); } } void WSI::tear_down_swapchain() { #ifdef HAVE_WSI_DXGI_INTEROP // We only do explicit teardown on exit. dxgi.reset(); #endif drain_swapchain(true); platform->event_swapchain_destroyed(); platform->destroy_swapchain_resources(swapchain); table->vkDestroySwapchainKHR(context->get_device(), swapchain, nullptr); swapchain = VK_NULL_HANDLE; has_acquired_swapchain_index = false; next_present_id = 1; present_last_id = 0; device->set_present_id(VK_NULL_HANDLE, 0); } void WSI::deinit_surface_and_swapchain() { LOGI("deinit_surface_and_swapchain()\n"); tear_down_swapchain(); if (surface != VK_NULL_HANDLE) { platform->destroy_surface(context->get_instance(), surface); surface = VK_NULL_HANDLE; } } void WSI::set_external_frame(unsigned index, Semaphore acquire_semaphore, double frame_time) { external_frame_index = index; external_acquire = std::move(acquire_semaphore); frame_is_external = true; external_frame_time = frame_time; } bool WSI::begin_frame_external() { device->next_frame_context(); // Need to handle this stuff from outside. if (has_acquired_swapchain_index) return false; auto frame_time = platform->get_frame_timer().frame(external_frame_time); auto elapsed_time = platform->get_frame_timer().get_elapsed(); // Assume we have been given a smooth frame pacing. smooth_frame_time = frame_time; smooth_elapsed_time = elapsed_time; // Poll after acquire as well for optimal latency. platform->poll_input(); swapchain_index = external_frame_index; platform->event_frame_tick(frame_time, elapsed_time); platform->event_swapchain_index(device.get(), swapchain_index); device->set_acquire_semaphore(swapchain_index, external_acquire); external_acquire.reset(); return true; } Semaphore WSI::consume_external_release_semaphore() { Semaphore sem; std::swap(external_release, sem); return sem; } //#define VULKAN_WSI_TIMING_DEBUG void WSI::wait_swapchain_latency() { unsigned effective_latency = low_latency_mode_enable_present ? 0 : present_frame_latency; if (device->get_device_features().supports_low_latency2_nv && swapchain && low_latency_mode_enable_gpu_submit) { if (!low_latency_semaphore) low_latency_semaphore = device->request_semaphore(VK_SEMAPHORE_TYPE_TIMELINE); auto wait_ts = device->write_calibrated_timestamp(); VkLatencySleepInfoNV sleep_info = { VK_STRUCTURE_TYPE_LATENCY_SLEEP_INFO_NV }; sleep_info.signalSemaphore = low_latency_semaphore->get_semaphore(); sleep_info.value = ++low_latency_semaphore_value; if (device->get_device_table().vkLatencySleepNV(device->get_device(), swapchain, &sleep_info) == VK_SUCCESS) low_latency_semaphore->wait_timeline(low_latency_semaphore_value); else LOGE("Failed to call vkLatencySleepNV.\n"); device->register_time_interval("WSI", std::move(wait_ts), device->write_calibrated_timestamp(), "low_latency_sleep"); VkSetLatencyMarkerInfoNV latency_marker_info = { VK_STRUCTURE_TYPE_SET_LATENCY_MARKER_INFO_NV }; latency_marker_info.marker = VK_LATENCY_MARKER_INPUT_SAMPLE_NV; latency_marker_info.presentID = next_present_id; device->get_device_table().vkSetLatencyMarkerNV(device->get_device(), swapchain, &latency_marker_info); latency_marker_info.marker = VK_LATENCY_MARKER_SIMULATION_START_NV; device->get_device_table().vkSetLatencyMarkerNV(device->get_device(), swapchain, &latency_marker_info); // Avoid conflicting wait cycles when doing reflex style latency limiting. effective_latency = std::max(effective_latency, 2); } else if (device->get_device_features().anti_lag_features.antiLag) { auto wait_ts = device->write_calibrated_timestamp(); VkAntiLagDataAMD anti_lag = { VK_STRUCTURE_TYPE_ANTI_LAG_DATA_AMD }; VkAntiLagPresentationInfoAMD present_info = { VK_STRUCTURE_TYPE_ANTI_LAG_PRESENTATION_INFO_AMD }; anti_lag.pPresentationInfo = &present_info; present_info.stage = VK_ANTI_LAG_STAGE_INPUT_AMD; present_info.frameIndex = ++low_latency_semaphore_value; anti_lag.mode = low_latency_mode_enable_gpu_submit ? VK_ANTI_LAG_MODE_ON_AMD : VK_ANTI_LAG_MODE_OFF_AMD; device->get_device_table().vkAntiLagUpdateAMD(device->get_device(), &anti_lag); low_latency_anti_lag_present_valid = low_latency_mode_enable_gpu_submit; device->register_time_interval("WSI", std::move(wait_ts), device->write_calibrated_timestamp(), "low_latency_sleep"); if (low_latency_mode_enable_gpu_submit) { // Avoid conflicting wait cycles when doing reflex style latency limiting. effective_latency = std::max(effective_latency, 2); } } // If we're using duped frames, make sure we're waiting for the previous "real" frame, // instead of a duped one. // E.g. when doing frame dupes: // 0, 1, 2, 3, 4, 5 ... // real, dup, real, dup, real, dup ... // With present frame latency of 1 (default), after presenting 2 // we will wait for 0 to be done rather than 1. // Similarly, after presenting 3 we'll still wait for 0 to be done, so we can get on submitting work // for the next real frame, 4 before the GPU drains of work. effective_latency += last_duplicated_frames; if ((device->get_device_features().present_wait_features.presentWait || supports_present_wait2) && present_last_id > effective_latency && current_present_mode == PresentMode::SyncToVBlank) { // The effective latency is more like present_frame_latency + 1. // If 0, we wait for vblank, and we must do CPU work and GPU work in one frame // to hit next vblank. uint64_t target = present_last_id - effective_latency; #ifdef VULKAN_WSI_TIMING_DEBUG auto begin_wait = Util::get_current_time_nsecs(); #endif auto wait_ts = device->write_calibrated_timestamp(); auto wait_result = wait_for_present(target); device->register_time_interval("WSI", std::move(wait_ts), device->write_calibrated_timestamp(), "wait_frame_latency"); if (wait_result < 0) LOGE("vkWaitForPresentKHR failed, vr %d.\n", wait_result); #ifdef VULKAN_WSI_TIMING_DEBUG auto end_wait = Util::get_current_time_nsecs(); LOGI("WaitForPresentKHR took %.3f ms.\n", 1e-6 * double(end_wait - begin_wait)); #endif } } void WSI::emit_end_of_frame_markers() { if (device->get_device_features().supports_low_latency2_nv && swapchain && low_latency_mode_enable_gpu_submit) { VkSetLatencyMarkerInfoNV latency_marker_info = { VK_STRUCTURE_TYPE_SET_LATENCY_MARKER_INFO_NV }; latency_marker_info.marker = VK_LATENCY_MARKER_SIMULATION_END_NV; latency_marker_info.presentID = next_present_id; device->get_device_table().vkSetLatencyMarkerNV(device->get_device(), swapchain, &latency_marker_info); latency_marker_info.marker = VK_LATENCY_MARKER_RENDERSUBMIT_END_NV; device->get_device_table().vkSetLatencyMarkerNV(device->get_device(), swapchain, &latency_marker_info); } } void WSI::emit_marker_pre_present() { if (device->get_device_features().supports_low_latency2_nv && swapchain && low_latency_mode_enable_gpu_submit) { VkSetLatencyMarkerInfoNV latency_marker_info = { VK_STRUCTURE_TYPE_SET_LATENCY_MARKER_INFO_NV }; latency_marker_info.marker = VK_LATENCY_MARKER_PRESENT_START_NV; latency_marker_info.presentID = next_present_id; device->get_device_table().vkSetLatencyMarkerNV(device->get_device(), swapchain, &latency_marker_info); } else if (device->get_device_features().anti_lag_features.antiLag && low_latency_anti_lag_present_valid) { VkAntiLagDataAMD anti_lag = { VK_STRUCTURE_TYPE_ANTI_LAG_DATA_AMD }; VkAntiLagPresentationInfoAMD present_info = { VK_STRUCTURE_TYPE_ANTI_LAG_PRESENTATION_INFO_AMD }; anti_lag.pPresentationInfo = &present_info; present_info.stage = VK_ANTI_LAG_STAGE_PRESENT_AMD; present_info.frameIndex = low_latency_semaphore_value; anti_lag.mode = low_latency_mode_enable_gpu_submit ? VK_ANTI_LAG_MODE_ON_AMD : VK_ANTI_LAG_MODE_OFF_AMD; device->get_device_table().vkAntiLagUpdateAMD(device->get_device(), &anti_lag); low_latency_anti_lag_present_valid = false; } } void WSI::emit_marker_post_present() { if (device->get_device_features().supports_low_latency2_nv && swapchain && low_latency_mode_enable_gpu_submit) { VkSetLatencyMarkerInfoNV latency_marker_info = { VK_STRUCTURE_TYPE_SET_LATENCY_MARKER_INFO_NV }; latency_marker_info.marker = VK_LATENCY_MARKER_PRESENT_END_NV; latency_marker_info.presentID = next_present_id; device->get_device_table().vkSetLatencyMarkerNV(device->get_device(), swapchain, &latency_marker_info); } } void WSI::set_present_low_latency_mode(bool enable) { low_latency_mode_enable_present = enable; } void WSI::set_gpu_submit_low_latency_mode(bool enable) { if (device && device->get_device_features().supports_low_latency2_nv && swapchain && low_latency_mode_enable_gpu_submit != enable) { VkLatencySleepModeInfoNV sleep_mode_info = { VK_STRUCTURE_TYPE_LATENCY_SLEEP_MODE_INFO_NV }; sleep_mode_info.lowLatencyBoost = enable; sleep_mode_info.lowLatencyMode = enable; if (table->vkSetLatencySleepModeNV(context->get_device(), swapchain, &sleep_mode_info) != VK_SUCCESS) LOGE("Failed to set low latency sleep mode.\n"); } low_latency_mode_enable_gpu_submit = enable; } #ifdef HAVE_WSI_DXGI_INTEROP bool WSI::begin_frame_dxgi() { Semaphore acquire; while (!acquire) { if (!dxgi->acquire(acquire)) return false; swapchain_index = 0; 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 void WSI::update_present_timing_properties() { VkSwapchainTimingPropertiesEXT props = { VK_STRUCTURE_TYPE_SWAPCHAIN_TIMING_PROPERTIES_EXT }; uint64_t counter = 0; if (table->vkGetSwapchainTimingPropertiesEXT(context->get_device(), swapchain, &props, &counter) != VK_SUCCESS) return; if (counter == present_timing.refresh_counter && present_timing.has_refresh_feedback) return; present_timing.refresh_counter = counter; present_timing.refresh_duration = props.refreshDuration; present_timing.refresh_interval = props.refreshInterval; const char *refresh_mode = "Unknown"; if (props.refreshInterval == 0) { // Wayland issue: It cannot figure out VRR vs FRR. Seems like we will need presentation-timing v3. present_timing.refresh_mode = RefreshMode::Unknown; } else if (props.refreshInterval == UINT64_MAX) { present_timing.refresh_mode = RefreshMode::VRR; refresh_mode = "VRR"; } else { present_timing.refresh_mode = RefreshMode::FRR; refresh_mode = "FRR"; } if (present_timing.refresh_duration) { (void)refresh_mode; #ifdef VULKAN_DEBUG LOGI("Present timing (count %llu): detected refresh duration of %llu nsec (%.6f Hz), mode %s.\n", static_cast(counter), static_cast(present_timing.refresh_duration), 1e9 / double(present_timing.refresh_duration), refresh_mode); #endif } present_timing.has_refresh_feedback = true; } void WSI::recalibrate_present_timing_domains() { if (!context->get_enabled_device_features().supports_calibrated_timestamps) return; present_timing.calibration.clear(); uint32_t count; vkGetPhysicalDeviceCalibrateableTimeDomainsKHR(context->get_gpu(), &count, nullptr); Util::SmallVector domains(count); vkGetPhysicalDeviceCalibrateableTimeDomainsKHR(context->get_gpu(), &count, domains.data()); domains.resize(count); #ifdef _WIN32 constexpr auto host_domain = VK_TIME_DOMAIN_QUERY_PERFORMANCE_COUNTER_KHR; #else constexpr auto host_domain = VK_TIME_DOMAIN_CLOCK_MONOTONIC_KHR; #endif if (std::find(domains.begin(), domains.end(), host_domain) == domains.end()) { LOGW("Cannot calibrate timestamp domain %u\n", host_domain); return; } present_timing.calibration.resize(present_timing.time_domains.size()); for (size_t i = 0, n = present_timing.time_domains.size(); i < n; i++) { if (present_timing.time_domains[i] == host_domain) { present_timing.calibration[i] = { 1, { 1, 1, 1, 1 } }; continue; } if (std::find(domains.begin(), domains.end(), present_timing.time_domains[i]) == domains.end()) { LOGW("Cannot calibrate timestamp domain %u\n", host_domain); return; } VkCalibratedTimestampInfoKHR infos[2] = {}; infos[0].sType = VK_STRUCTURE_TYPE_CALIBRATED_TIMESTAMP_INFO_KHR; infos[1].sType = VK_STRUCTURE_TYPE_CALIBRATED_TIMESTAMP_INFO_KHR; infos[0].timeDomain = host_domain; infos[1].timeDomain = present_timing.time_domains[i]; uint64_t timestamps[2] = {}; uint64_t max_deviation; VkSwapchainCalibratedTimestampInfoEXT swapchain_info = { VK_STRUCTURE_TYPE_SWAPCHAIN_CALIBRATED_TIMESTAMP_INFO_EXT }; if (present_timing.time_domains[i] == VK_TIME_DOMAIN_PRESENT_STAGE_LOCAL_EXT) { // Every present stage has a unique time domain, query each stage one-by-one. infos[1].pNext = &swapchain_info; swapchain_info.timeDomainId = present_timing.time_domain_ids[i]; swapchain_info.swapchain = swapchain; Util::for_each_bit(supports_present_timing.feedback, [&](unsigned bit) { swapchain_info.presentStage = 1u << bit; if (table->vkGetCalibratedTimestampsKHR(context->get_device(), 2, infos, timestamps, &max_deviation) != VK_SUCCESS) { LOGE("Failed to get calibrated timestamps.\n"); return; } present_timing.calibration[i].host_time = timestamps[0]; present_timing.calibration[i].stage_times[bit] = timestamps[1]; }); } else if (present_timing.time_domains[i] == VK_TIME_DOMAIN_SWAPCHAIN_LOCAL_EXT) { infos[1].pNext = &swapchain_info; swapchain_info.timeDomainId = present_timing.time_domain_ids[i]; swapchain_info.swapchain = swapchain; if (table->vkGetCalibratedTimestampsKHR(context->get_device(), 2, infos, timestamps, &max_deviation) != VK_SUCCESS) { LOGE("Failed to get calibrated timestamps.\n"); return; } present_timing.calibration[i].host_time = timestamps[0]; Util::for_each_bit(supports_present_timing.feedback, [&](unsigned bit) { present_timing.calibration[i].stage_times[bit] = timestamps[1]; }); } else { // This probably needs spec clarification. // We assume it's normal nanoseconds for now. if (infos[1].timeDomain == VK_TIME_DOMAIN_DEVICE_KHR && (context->get_gpu_props().limits.timestampPeriod != 1.0f || context->get_queue_info().timestamp_valid_bits != 64)) { LOGW("Implementation reports DEVICE domain timestamps, " "but it's unclear how to deal with non-trivial periods and valid bits.\n"); } if (table->vkGetCalibratedTimestampsKHR(context->get_device(), 2, infos, timestamps, &max_deviation) != VK_SUCCESS) { LOGE("Failed to get calibrated timestamps.\n"); return; } present_timing.calibration[i].host_time = timestamps[0]; Util::for_each_bit(supports_present_timing.feedback, [&](unsigned bit) { present_timing.calibration[i].stage_times[bit] = timestamps[1]; }); } } #ifdef _WIN32 LARGE_INTEGER freq; QueryPerformanceFrequency(&freq); for (auto &calibration : present_timing.calibration) calibration.host_time = uint64_t(1e9 * calibration.host_time / double(freq.QuadPart)); #endif if (present_timing.present_stage == 0) { // Make sure that our requests use a supported time domain, otherwise NV driver gets confused. for (size_t i = 0, n = present_timing.calibration.size(); i < n; i++) { auto &cal = present_timing.calibration[i]; if (cal.host_time && std::any_of(std::begin(cal.stage_times), std::end(cal.stage_times), [](uint64_t v) { return v != 0; })) { present_timing.time_domain = present_timing.time_domains[i]; present_timing.time_domain_id = present_timing.time_domain_ids[i]; break; } } } } void WSI::update_time_domain_properties() { VkSwapchainTimeDomainPropertiesEXT time_domain_properties = { VK_STRUCTURE_TYPE_SWAPCHAIN_TIME_DOMAIN_PROPERTIES_EXT }; time_domain_properties.timeDomainCount = 1; // Workaround VVL. if (table->vkGetSwapchainTimeDomainPropertiesEXT(context->get_device(), swapchain, &time_domain_properties, nullptr) != VK_SUCCESS) { LOGE("Failed to query time domain properties.\n"); return; } present_timing.time_domains.resize(time_domain_properties.timeDomainCount); present_timing.time_domain_ids.resize(time_domain_properties.timeDomainCount); time_domain_properties.pTimeDomains = present_timing.time_domains.data(); time_domain_properties.pTimeDomainIds = present_timing.time_domain_ids.data(); if (table->vkGetSwapchainTimeDomainPropertiesEXT(context->get_device(), swapchain, &time_domain_properties, &present_timing.time_domain_counter) != VK_SUCCESS) { LOGE("Failed to query time domain properties.\n"); return; } #ifdef VULKAN_DEBUG for (uint32_t i = 0; i < time_domain_properties.timeDomainCount; i++) { LOGI("Got time domain %u, ID %llu\n", present_timing.time_domains[i], static_cast(present_timing.time_domain_ids[i])); } #endif present_timing.has_time_domain_props = true; present_timing.need_recalibration = true; } void WSI::poll_present_timing_feedback() { VkPastPresentationTimingEXT timings[PresentTimingQueueSize] = {}; VkPresentStageTimeEXT stage_time[PresentTimingQueueSize][4] = {}; for (uint32_t i = 0; i < PresentTimingQueueSize; i++) { auto &t = timings[i]; t.sType = VK_STRUCTURE_TYPE_PAST_PRESENTATION_TIMING_EXT; t.pPresentStages = stage_time[i]; t.presentStageCount = 4; // VVL workaround for (auto &s : stage_time[i]) s.stage = VK_PRESENT_STAGE_QUEUE_OPERATIONS_END_BIT_EXT; } VkPastPresentationTimingInfoEXT info = { VK_STRUCTURE_TYPE_PAST_PRESENTATION_TIMING_INFO_EXT }; VkPastPresentationTimingPropertiesEXT props = { VK_STRUCTURE_TYPE_PAST_PRESENTATION_TIMING_PROPERTIES_EXT }; props.presentationTimingCount = PresentTimingQueueSize; props.pPresentationTimings = timings; info.swapchain = swapchain; if (table->vkGetPastPresentationTimingEXT(context->get_device(), &info, &props) != VK_SUCCESS) return; if (props.presentationTimingCount == 0) return; if (props.timingPropertiesCounter != present_timing.refresh_counter) update_present_timing_properties(); // NV bug on X11: props.timingPropertiesCounter is always 0, even if TimingPropertiesEXT returns 1. if (props.timingPropertiesCounter != 0 && props.timingPropertiesCounter != present_timing.refresh_counter) { LOGW("Got presentation timing counter (%llu) which does not map to current state of swapchain (%llu).\n", static_cast(props.timingPropertiesCounter), static_cast(present_timing.refresh_counter)); } // NV bug on X11: timeDomainsCount remains 0. if ((props.timeDomainsCounter != 0 && props.timeDomainsCounter != present_timing.time_domain_counter) || !present_timing.has_time_domain_props) { update_time_domain_properties(); } auto current_time = Util::get_current_time_nsecs(); if (present_timing.last_recalibration_time + 1000000000 < current_time) { present_timing.need_recalibration = true; present_timing.last_recalibration_time = current_time; } if (present_timing.need_recalibration) { recalibrate_present_timing_domains(); present_timing.need_recalibration = false; } for (uint32_t i = 0; i < props.presentationTimingCount; i++) { // By default, these reports must be sorted based on QueuePresent(). auto &timing = props.pPresentationTimings[i]; if (!timing.reportComplete) { // This should never happen since we don't request partial timestamps. LOGE("Implementation does not report complete timestamps?\n"); continue; } #ifdef VULKAN_DEBUG LOGI("Timing for presentID %llu, time domain %u, time domain ID %llu:\n", static_cast(timing.presentId), timing.timeDomain, static_cast(timing.timeDomainId)); #endif present_timing.present_stage = 0; present_timing.reference_time = 0; present_timing.present_id = timing.presentId; present_timing.time_domain = timing.timeDomain; present_timing.time_domain_id = timing.timeDomainId; // Try to calibrate the timestamp so we can report them in host time domain. const CalibratedTimestamp *calibrated = nullptr; for (size_t j = 0, n = present_timing.time_domain_ids.size(); j < n; j++) { if (present_timing.time_domain_ids[j] == timing.timeDomainId) { if (j < present_timing.calibration.size()) calibrated = &present_timing.calibration[j]; break; } } if (calibrated && calibrated->host_time == 0) calibrated = nullptr; std::sort(timing.pPresentStages, timing.pPresentStages + timing.presentStageCount, [](const VkPresentStageTimeEXT &a, const VkPresentStageTimeEXT &b) { return a.stage < b.stage; }); present_timing.present_done_host_time = 0; present_timing.gpu_done_host_time = 0; for (uint32_t stage_index = 0; stage_index < timing.presentStageCount; stage_index++) { auto &stage = timing.pPresentStages[stage_index]; // Safety. Shouldn't happen since we don't request it. // Time of 0 can happen according to spec and means timing information is not available. // Can happen for e.g. discarded, occluded surfaces on WL. Just skip the update. if (stage.stage > VK_PRESENT_STAGE_IMAGE_FIRST_PIXEL_VISIBLE_BIT_EXT || stage.stage == 0 || stage.time == 0) continue; uint64_t calibrated_stage_time = calibrated ? calibrated->stage_times[Util::trailing_zeroes(stage.stage)] : 0; #ifdef VULKAN_DEBUG static const char *stage_tags[] = { "QueueOperations", "Dequeued", "FirstPixelOut", "FirstPixelVisible" }; const char *stage_tag = stage_tags[Util::trailing_zeroes(stage.stage)]; #endif if (calibrated) { uint64_t calibrated_ts = calibrated->host_time + (stage.time - calibrated_stage_time); if (stage.stage == VK_PRESENT_STAGE_QUEUE_OPERATIONS_END_BIT_EXT) present_timing.gpu_done_host_time = calibrated_ts; else present_timing.present_done_host_time = calibrated_ts; #ifdef VULKAN_DEBUG LOGI(" %s: %.3f s (calibrated)\n", stage_tag, calibrated_ts * 1e-9); #endif } #ifdef VULKAN_DEBUG else { LOGI(" %s: %llu (raw ns)\n", stage_tag, static_cast(stage.time)); } #endif present_timing.present_stage = stage.stage; present_timing.reference_time = stage.time; } auto itr = std::find_if( present_timing.error_stats.begin(), present_timing.error_stats.end(), [&](const ErrorStats &err) { return err.present_id == timing.presentId; }); present_timing.presentation_time_error = 0; if (itr != present_timing.error_stats.end()) { // Retire the pending compensation. present_timing.pending_compensation -= itr->compensation; if (present_timing.present_done_host_time && itr->target_absolute) { auto err = int64_t(present_timing.present_done_host_time) - int64_t(itr->target_absolute); present_timing.presentation_time_error = err; #ifdef VULKAN_DEBUG LOGI(" Error: %.3f ms\n", 1e-6 * double(err)); #endif } present_timing.error_stats.erase(itr); } frr_pacer.set_frame_time_ns(present_timing.refresh_duration); if (present_timing.present_done_host_time && present_timing.gpu_done_host_time) { frr_pacer.update_feedback(present_timing.present_id, present_timing.gpu_done_host_time, present_timing.present_done_host_time); } } } void WSI::set_present_timing_request(VkPresentTimingInfoEXT &timing) { // No stable way to set targets yet. if (present_timing.refresh_duration == 0 || present_timing.reference_time == 0 || present_timing.present_done_host_time == 0) return; // Presentation timing is only meaningful for FIFO. if (active_present_mode != VK_PRESENT_MODE_FIFO_KHR && active_present_mode != VK_PRESENT_MODE_FIFO_RELAXED_KHR && active_present_mode != VK_PRESENT_MODE_FIFO_LATEST_READY_KHR) return; // Not supported. if (!supports_present_timing.absolute && !supports_present_timing.relative) return; // No request to set time. if (present_timing.target_absolute_time == 0 && present_timing.target_relative_time == 0) return; uint64_t relative_time = present_timing.target_relative_time; if (relative_time && !present_timing.force_vrr && present_timing.refresh_mode != RefreshMode::VRR) { // If we keep accumulating relative time in a non-locked way, we'll get terrible pacing. // Realign the relative time to boundary unless we're in VRR mode. uint64_t interval = present_timing.refresh_duration; relative_time = std::max(relative_time, interval); // The refresh interval represents the alignment of a refresh cycle. // Duration is a multiple of interval. if (present_timing.refresh_interval) interval = present_timing.refresh_interval; auto cycles = (relative_time + interval - 1) / interval; relative_time = interval * cycles; } // VRR does not have to align to boundaries, so rounding is somewhat meaningless. if (present_timing.refresh_mode != RefreshMode::VRR && !present_timing.force_vrr) timing.flags |= VK_PRESENT_TIMING_INFO_PRESENT_AT_NEAREST_REFRESH_CYCLE_BIT_EXT; if (present_timing.time_domain == VK_TIME_DOMAIN_PRESENT_STAGE_LOCAL_EXT) timing.targetTimeDomainPresentStage = present_timing.present_stage; uint64_t minimum_interval = present_timing.refresh_duration; if (supports_present_timing.relative && relative_time) { // Relative time is very nice, since it's not our job to align frames :) timing.flags |= VK_PRESENT_TIMING_INFO_PRESENT_AT_RELATIVE_TIME_BIT_EXT; timing.targetTime = relative_time; // If we're using pure relative timing, we don't care about error accumulation and compensation, // since it's expected that application will be driven by feedback rather than opposite. } else { if (!supports_present_timing.absolute) { // Ensure that we don't attempt to compensate for errors when absolute times are very tight. // The emulated relative time must never be smaller than duration. relative_time = std::max(relative_time, present_timing.refresh_duration); } // If presentations get sufficiently delayed, we will need to catch up with our internal accumulator. // If we're using present interval > 1, we'll catch up in a few frames. uint64_t estimated_minimum_time = present_timing.present_done_host_time + (present_last_id - present_timing.present_id) * minimum_interval; present_timing.last_absolute_target_time = std::max(present_timing.last_absolute_target_time, estimated_minimum_time); // Compute the target absolute timing. uint64_t next_absolute_time = std::max( present_timing.last_absolute_target_time + relative_time, present_timing.target_absolute_time); int64_t compensation = 0; if (supports_present_timing.absolute) { timing.targetTime = next_absolute_time; } else { if (present_timing.last_absolute_target_time) { // This is kinda crude. We're emulating absolute timestamp with relative. timing.targetTime = std::max(next_absolute_time, present_timing.last_absolute_target_time) - present_timing.last_absolute_target_time; // Safety against deadlocks. timing.targetTime = std::min(timing.targetTime, 1000 * 1000 * 1000); // If we lost frames, relative timing will remain delayed, so pull back the relative timing // to realign with absolute time. // The value can be negative in theory if we got back frames too early, // but that shouldn't really happen. int64_t in_flight_error = present_timing.presentation_time_error - present_timing.pending_compensation; VK_ASSERT(timing.targetTime >= present_timing.refresh_duration); // Don't aim to compensate all error in one go. That seems to create some unfortunate feedback loop effects, // especially on Windows. We'll accept some error as long as it means more stable pacing. compensation = std::min(timing.targetTime - present_timing.refresh_duration, in_flight_error / 2); timing.targetTime -= compensation; #ifdef _WIN32 if (device->get_device_features().driver_id == VK_DRIVER_ID_NVIDIA_PROPRIETARY && (timing.flags & VK_PRESENT_TIMING_INFO_PRESENT_AT_NEAREST_REFRESH_CYCLE_BIT_EXT) != 0) { // The driver seems very temperamental here, and it seems to round down the relative time // to DXGI present intervals or something (which is a bug) ... Realign the next_absolute_time exactly. uint64_t interval = present_timing.refresh_interval ? present_timing.refresh_interval : present_timing.refresh_duration; auto cycles = (timing.targetTime + interval / 2) / interval; auto new_relative_time = cycles * interval; auto adj = new_relative_time - timing.targetTime; next_absolute_time += adj; compensation -= adj; timing.targetTime = new_relative_time; } #endif present_timing.pending_compensation += compensation; #ifdef VULKAN_DEBUG LOGI("Relative target time: %.3f ms.\n", timing.targetTime * 1e-6); LOGI(" Abs target time: %.3f ms.\n", next_absolute_time * 1e-6); LOGI(" Compensation offset: %.3f ms\n", compensation * 1e-6); #endif } timing.flags |= VK_PRESENT_TIMING_INFO_PRESENT_AT_RELATIVE_TIME_BIT_EXT; } present_timing.last_absolute_target_time = next_absolute_time; present_timing.error_stats.push_back({ next_present_id, next_absolute_time, compensation }); // If we keep accumulating time through relative time emulation, we need to account for clock drift. if (supports_present_timing.absolute && relative_time && present_timing.refresh_mode != RefreshMode::VRR && !present_timing.force_vrr) { uint64_t interval = present_timing.refresh_interval ? present_timing.refresh_interval : present_timing.refresh_duration; uint64_t align = (present_timing.last_absolute_target_time - present_timing.present_done_host_time) % interval; // Try to quickly-ish align to a refresh cycle. if (align > interval / 2) present_timing.last_absolute_target_time += (interval - align) / 8; else present_timing.last_absolute_target_time -= align / 8; } } if ((timing.flags & VK_PRESENT_TIMING_INFO_PRESENT_AT_RELATIVE_TIME_BIT_EXT) == 0) { bool has_calibrated_time = false; size_t n = std::min(present_timing.time_domain_ids.size(), present_timing.calibration.size()); for (size_t i = 0; i < n; i++) { if (present_timing.time_domain_ids[i] == timing.timeDomainId) { auto &c = present_timing.calibration[i]; // Spec seems to suggest that targetTime is always in terms of FIRST_PIXEL_VISIBLE, // but we can only use timestamps based on the latest present stage we get feedback for. uint64_t stage_time = c.stage_times[Util::trailing_zeroes(present_timing.present_stage)]; if (stage_time == 0) { LOGW("Cannot compute targetTime.\n"); } else { // Recompute the absolute time to target the domain. timing.targetTime = (timing.targetTime - c.host_time) + stage_time; if (timing.targetTime >= (1ull << 63)) LOGW("Detected strange overflow, ignoring time request.\n"); else has_calibrated_time = true; } break; } } if (!has_calibrated_time) timing.targetTime = 0; } // Completely meaningless to keep targeting absolute. present_timing.target_absolute_time = 0; } bool WSI::get_presentation_stats(PresentationStats &stats) const { if (!present_timing.reference_time) return false; stats.feedback_present_id = present_timing.present_id; stats.gpu_done_ts = present_timing.gpu_done_host_time; stats.present_done_ts = present_timing.present_done_host_time; stats.error = present_timing.presentation_time_error; return true; } bool WSI::get_refresh_rate_info(RefreshRateInfo &info) const { if (present_timing.refresh_duration == 0) return false; info.refresh_duration = present_timing.refresh_duration; info.refresh_interval = present_timing.refresh_interval; info.mode = present_timing.refresh_mode; return true; } uint64_t WSI::get_last_submitted_present_id() const { return present_last_id; } bool WSI::set_target_presentation_time(uint64_t absolute_time_ns, uint64_t relative_time_ns, bool force_vrr) { present_timing.target_absolute_time = absolute_time_ns; present_timing.target_relative_time = relative_time_ns; present_timing.force_vrr = force_vrr; if (absolute_time_ns || relative_time_ns) present_feedback_enable = true; if (active_present_mode != VK_PRESENT_MODE_FIFO_KHR && active_present_mode != VK_PRESENT_MODE_FIFO_RELAXED_KHR && active_present_mode != VK_PRESENT_MODE_FIFO_LATEST_READY_KHR) return false; return present_timing.refresh_duration != 0 && present_timing.reference_time != 0 && present_timing.present_done_host_time != 0 && (supports_present_timing.relative || supports_present_timing.absolute); } void WSI::set_enable_timing_feedback(bool enable) { present_feedback_enable = enable; } void WSI::set_fixed_rate_low_latency_pacer(bool enable) { frr_pacer_enable = enable; if (!frr_pacer_enable) frr_pacer.reset(); } FixedRefreshRatePacer &WSI::get_fixed_rate_pacer() { return frr_pacer; } bool WSI::begin_frame() { if (frame_is_external) return begin_frame_external(); #ifdef VULKAN_WSI_TIMING_DEBUG auto next_frame_start = Util::get_current_time_nsecs(); #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 #ifdef HAVE_WSI_DXGI_INTEROP 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) { LOGE("Completely lost swapchain. Cannot continue.\n"); return false; } VkResult result; do { auto acquire = device->request_semaphore(VK_SEMAPHORE_TYPE_BINARY); #ifdef VULKAN_WSI_TIMING_DEBUG auto acquire_start = Util::get_current_time_nsecs(); #endif Fence fence; // TODO: Improve this with fancier approaches as needed. if (low_latency_mode_enable_present && !device->get_device_features().present_wait_features.presentWait && !supports_present_wait2 && current_present_mode == PresentMode::SyncToVBlank) { fence = device->request_legacy_fence(); } auto acquire_ts = device->write_calibrated_timestamp(); result = table->vkAcquireNextImageKHR(context->get_device(), swapchain, UINT64_MAX, acquire->get_semaphore(), fence ? fence->get_fence() : VK_NULL_HANDLE, &swapchain_index); device->register_time_interval("WSI", std::move(acquire_ts), device->write_calibrated_timestamp(), "acquire"); if (fence) fence->wait(); #if defined(ANDROID) // Android 10 can return suboptimal here, only because of pre-transform. // We don't care about that, and treat this as success. if (result == VK_SUBOPTIMAL_KHR && !support_prerotate) result = VK_SUCCESS; #endif if (result == VK_ERROR_FULL_SCREEN_EXCLUSIVE_MODE_LOST_EXT) { LOGE("Lost exclusive full-screen ...\n"); } #ifdef VULKAN_WSI_TIMING_DEBUG auto acquire_end = Util::get_current_time_nsecs(); LOGI("vkAcquireNextImageKHR took %.3f ms.\n", (acquire_end - acquire_start) * 1e-6); #endif if (result == VK_SUBOPTIMAL_KHR) { #ifdef VULKAN_DEBUG LOGI("AcquireNextImageKHR is suboptimal, will recreate.\n"); #endif swapchain_is_suboptimal = true; LOGW("Swapchain suboptimal.\n"); } if (result >= 0) { has_acquired_swapchain_index = true; acquire->signal_external(); // WSI signals this, which exists outside the domain of our Vulkan queues. acquire->set_signal_is_foreign_queue(); wait_swapchain_latency(); if (supports_present_timing.feedback) { update_present_timing_properties(); poll_present_timing_feedback(); } if (frr_pacer_enable) { auto pace_start_ts = device->write_calibrated_timestamp(); frr_pacer.begin_frame_submission(next_present_id); device->register_time_interval( "WSI", std::move(pace_start_ts), device->write_calibrated_timestamp(), "pacer-wait"); } 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; // Poll after acquire as well for optimal latency. platform->poll_input(); platform->event_frame_tick(frame_time, elapsed_time); platform->event_swapchain_index(device.get(), swapchain_index); device->set_acquire_semaphore(swapchain_index, acquire); if (device->get_device_features().present_id_features.presentId || device->get_device_features().present_id2_features.presentId2) { device->set_present_id(swapchain, next_present_id); } } else if (result == VK_ERROR_OUT_OF_DATE_KHR || result == VK_ERROR_FULL_SCREEN_EXCLUSIVE_MODE_LOST_EXT) { LOGW("Swapchain out of date.\n"); VK_ASSERT(swapchain_width != 0); VK_ASSERT(swapchain_height != 0); tear_down_swapchain(); if (!blocking_init_swapchain(swapchain_width, swapchain_height)) return false; 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); } else { return false; } } while (result < 0); return true; } #ifdef HAVE_WSI_DXGI_INTEROP 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 void WSI::set_frame_duplication_aware(bool enable, uint32_t target_image_count) { frame_dupe_aware = enable; frame_dupe_target_images = target_image_count; if (!has_acquired_swapchain_index && (current_frame_dupe_aware != frame_dupe_aware || current_frame_dupe_target_images != frame_dupe_target_images)) { current_frame_dupe_aware = frame_dupe_aware; current_frame_dupe_target_images = frame_dupe_target_images; update_framebuffer(swapchain_width, swapchain_height); } } void WSI::set_next_present_is_duplicated() { next_present_is_dupe = true; } bool WSI::end_frame() { device->end_frame_context(); // Take ownership of the release semaphore so that the external user can use it. if (frame_is_external) { // If we didn't render into the swapchain this frame, we will return a blank semaphore. external_release = device->consume_release_semaphore(); VK_ASSERT(!external_release || external_release->is_signalled()); frame_is_external = false; } else { if (!device->swapchain_touched()) return true; emit_end_of_frame_markers(); has_acquired_swapchain_index = false; #ifdef HAVE_WSI_DXGI_INTEROP 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); VkResult result = VK_SUCCESS; VkPresentInfoKHR info = { VK_STRUCTURE_TYPE_PRESENT_INFO_KHR }; info.waitSemaphoreCount = 1; info.pWaitSemaphores = &release_semaphore; info.swapchainCount = 1; info.pSwapchains = &swapchain; info.pImageIndices = &swapchain_index; info.pResults = &result; VkSwapchainPresentFenceInfoKHR present_fence = { VK_STRUCTURE_TYPE_SWAPCHAIN_PRESENT_FENCE_INFO_KHR }; VkSwapchainPresentModeInfoKHR present_mode_info = { VK_STRUCTURE_TYPE_SWAPCHAIN_PRESENT_MODE_INFO_KHR }; VkPresentIdKHR present_id_info = { VK_STRUCTURE_TYPE_PRESENT_ID_KHR }; VkPresentId2KHR present_id2_info = { VK_STRUCTURE_TYPE_PRESENT_ID_2_KHR }; VkPresentTimingsInfoEXT timings_info = { VK_STRUCTURE_TYPE_PRESENT_TIMINGS_INFO_EXT }; VkPresentTimingInfoEXT timing_info = { VK_STRUCTURE_TYPE_PRESENT_TIMING_INFO_EXT }; if (supports_present_wait2) { present_id2_info.swapchainCount = 1; present_id2_info.pPresentIds = &next_present_id; present_id2_info.pNext = info.pNext; info.pNext = &present_id2_info; } else if (device->get_device_features().present_id_features.presentId) { present_id_info.swapchainCount = 1; present_id_info.pPresentIds = &next_present_id; present_id_info.pNext = info.pNext; info.pNext = &present_id_info; } // If we can, just promote the new presentation mode right away. update_active_presentation_mode(present_mode); if (device->get_device_features().swapchain_maintenance1_features.swapchainMaintenance1) { if (!device->get_workarounds().broken_present_fence) { last_present_fence = device->request_legacy_fence(); present_fence.swapchainCount = 1; present_fence.pFences = &last_present_fence->get_fence(); present_fence.pNext = const_cast(info.pNext); info.pNext = &present_fence; } present_mode_info.swapchainCount = 1; present_mode_info.pPresentModes = &active_present_mode; present_mode_info.pNext = const_cast(info.pNext); info.pNext = &present_mode_info; } if (supports_present_timing.feedback && present_feedback_enable) { timing_info.presentStageQueries = supports_present_timing.feedback; timing_info.timeDomainId = present_timing.time_domain_id; set_present_timing_request(timing_info); timings_info.swapchainCount = 1; timings_info.pTimingInfos = &timing_info; timings_info.pNext = info.pNext; info.pNext = &timings_info; } #ifdef VULKAN_WSI_TIMING_DEBUG auto present_start = Util::get_current_time_nsecs(); #endif auto present_ts = device->write_calibrated_timestamp(); device->external_queue_lock(); emit_marker_pre_present(); #if defined(ANDROID) && defined(HAVE_SWAPPY) VkResult overall = SwappyVk_queuePresent(device->get_current_present_queue(), &info); #else VkResult overall = table->vkQueuePresentKHR(device->get_current_present_queue(), &info); #endif emit_marker_post_present(); device->external_queue_unlock(); device->register_time_interval("WSI", std::move(present_ts), device->write_calibrated_timestamp(), "present"); #if defined(ANDROID) // Android 10 can return suboptimal here, only because of pre-transform. // We don't care about that, and treat this as success. if (overall == VK_SUBOPTIMAL_KHR && !support_prerotate) overall = VK_SUCCESS; if (result == VK_SUBOPTIMAL_KHR && !support_prerotate) result = VK_SUCCESS; #endif if (overall == VK_ERROR_FULL_SCREEN_EXCLUSIVE_MODE_LOST_EXT || result == VK_ERROR_FULL_SCREEN_EXCLUSIVE_MODE_LOST_EXT) { LOGE("Lost exclusive full-screen ...\n"); } #ifdef VULKAN_WSI_TIMING_DEBUG auto present_end = Util::get_current_time_nsecs(); LOGI("vkQueuePresentKHR took %.3f ms.\n", (present_end - present_start) * 1e-6); #endif bool dupes_frame = next_present_is_dupe && current_frame_dupe_aware && !low_latency_mode_enable_present; // The presentID only seems to get updated if QueuePresent returns success. // This makes sense I guess. Record the latest present ID which was successfully presented // so we don't risk deadlock. if ((result == VK_SUCCESS || result == VK_SUBOPTIMAL_KHR) && (device->get_device_features().present_id_features.presentId || supports_present_wait2) && !dupes_frame) { present_last_id = next_present_id; } next_present_id++; next_present_is_dupe = false; if (dupes_frame) { duplicated_frames++; } else { last_duplicated_frames = duplicated_frames; duplicated_frames = 0; } if (overall == VK_SUBOPTIMAL_KHR || result == VK_SUBOPTIMAL_KHR) { #ifdef VULKAN_DEBUG LOGI("QueuePresent is suboptimal, will recreate.\n"); #endif swapchain_is_suboptimal = true; } // The present semaphore is consumed even on OUT_OF_DATE, etc. release->wait_external(); if (!device->get_workarounds().broken_present_fence && device->get_device_features().swapchain_maintenance1_features.swapchainMaintenance1) { deferred_semaphore.push_back({ std::move(release), last_present_fence }); release = {}; } if (overall < 0 || result < 0) { LOGE("vkQueuePresentKHR failed.\n"); release.reset(); tear_down_swapchain(); return false; } else { // Cannot release the WSI wait semaphore until we observe that the image has been // waited on again. // Could make this a bit tighter with swapchain_maintenance1, but not that important here. release_semaphores[swapchain_index] = std::move(release); } // Re-init swapchain. if (present_mode != current_present_mode || has_backbuffer_format_delta() || extra_usage != current_extra_usage || compression.type != current_compression.type || compression.fixed_rates != current_compression.fixed_rates || frame_dupe_aware != current_frame_dupe_aware || frame_dupe_target_images != current_frame_dupe_target_images) { current_present_mode = present_mode; current_backbuffer_format = backbuffer_format; current_extra_usage = extra_usage; current_compression = compression; current_custom_backbuffer_format = custom_backbuffer_format; current_frame_dupe_aware = frame_dupe_aware; current_frame_dupe_target_images = frame_dupe_target_images; update_framebuffer(swapchain_width, swapchain_height); } } nonblock_delete_swapchain_resources(); return true; } bool WSI::has_backbuffer_format_delta() const { bool has_format_delta = backbuffer_format != current_backbuffer_format; if (!has_format_delta && backbuffer_format == BackbufferFormat::Custom) { has_format_delta = current_custom_backbuffer_format.format != custom_backbuffer_format.format || current_custom_backbuffer_format.colorSpace != custom_backbuffer_format.colorSpace; } return has_format_delta; } void WSI::update_framebuffer(unsigned width, unsigned height) { if (context && device) { #ifdef HAVE_WSI_DXGI_INTEROP if (dxgi) { 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); } } } if (platform) platform->notify_current_swapchain_dimensions(swapchain_width, swapchain_height); } bool WSI::update_active_presentation_mode(PresentMode mode) { if (current_present_mode == mode) return true; #ifdef HAVE_WSI_DXGI_INTEROP // 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; switch (m) { case VK_PRESENT_MODE_FIFO_KHR: match = mode == PresentMode::SyncToVBlank; break; case VK_PRESENT_MODE_IMMEDIATE_KHR: match = mode == PresentMode::UnlockedMaybeTear || mode == PresentMode::UnlockedForceTearing; break; case VK_PRESENT_MODE_MAILBOX_KHR: match = mode == PresentMode::UnlockedNoTearing || mode == PresentMode::UnlockedMaybeTear; break; default: break; } if (match) { active_present_mode = m; current_present_mode = mode; return true; } } return false; } void WSI::set_present_mode(PresentMode mode) { present_mode = mode; if (!has_acquired_swapchain_index && present_mode != current_present_mode) { if (!update_active_presentation_mode(present_mode)) { current_present_mode = present_mode; update_framebuffer(swapchain_width, swapchain_height); } } } void WSI::set_extra_usage_flags(VkImageUsageFlags usage) { extra_usage = usage; if (!has_acquired_swapchain_index && extra_usage != current_extra_usage) { current_extra_usage = extra_usage; update_framebuffer(swapchain_width, swapchain_height); } } void WSI::set_backbuffer_format(BackbufferFormat format) { backbuffer_format = format; if (!has_acquired_swapchain_index && has_backbuffer_format_delta()) { current_backbuffer_format = backbuffer_format; current_custom_backbuffer_format = custom_backbuffer_format; update_framebuffer(swapchain_width, swapchain_height); } } void WSI::set_image_compression_control(const ImageCompression &comp) { if (device && !device->get_device_features().image_compression_control_swapchain_features.imageCompressionControlSwapchain) return; compression = comp; if (!has_acquired_swapchain_index && (compression.type != current_compression.type || compression.fixed_rates != current_compression.fixed_rates)) { current_compression = compression; update_framebuffer(swapchain_width, swapchain_height); } } void WSI::set_custom_backbuffer_format(VkSurfaceFormatKHR format) { custom_backbuffer_format = format; set_backbuffer_format(BackbufferFormat::Custom); } void WSI::set_backbuffer_srgb(bool enable) { set_backbuffer_format(enable ? BackbufferFormat::sRGB : BackbufferFormat::UNORM); } void WSI::teardown() { low_latency_semaphore.reset(); if (platform) platform->release_resources(); if (context) tear_down_swapchain(); if (surface != VK_NULL_HANDLE) { platform->destroy_surface(context->get_instance(), surface); surface = VK_NULL_HANDLE; } if (platform) platform->event_device_destroyed(); external_release.reset(); external_acquire.reset(); external_swapchain_images.clear(); device.reset(); context.reset(); } bool WSI::blocking_init_swapchain(unsigned width, unsigned height) { SwapchainError err; unsigned retry_counter = 0; do { swapchain_aspect_ratio = platform->get_aspect_ratio(); err = init_swapchain(width, height); if (err != SwapchainError::None) platform->notify_current_swapchain_dimensions(0, 0); if (err == SwapchainError::Error) { if (++retry_counter > 3) return false; // Try to not reuse the swapchain. tear_down_swapchain(); } else if (err == SwapchainError::NoSurface) { LOGW("WSI cannot make forward progress due to minimization, blocking ...\n"); device->set_enable_async_thread_frame_context(true); platform->block_until_wsi_forward_progress(*this); device->set_enable_async_thread_frame_context(false); LOGW("Woke up!\n"); } } while (err != SwapchainError::None); frr_pacer.reset(); return swapchain != VK_NULL_HANDLE; } VkSurfaceFormatKHR WSI::find_suitable_present_format(const std::vector &formats, BackbufferFormat desired_format) const { size_t format_count = formats.size(); VkSurfaceFormatKHR format = { VK_FORMAT_UNDEFINED }; VkFormatFeatureFlags features = VK_FORMAT_FEATURE_COLOR_ATTACHMENT_BIT | VK_FORMAT_FEATURE_COLOR_ATTACHMENT_BLEND_BIT; if ((current_extra_usage & VK_IMAGE_USAGE_STORAGE_BIT) != 0) features |= VK_FORMAT_FEATURE_STORAGE_IMAGE_BIT; if (format_count == 0) { LOGE("Surface has no formats?\n"); return format; } for (size_t i = 0; i < format_count; i++) { if (!device->image_format_is_supported(formats[i].format, features)) continue; if (desired_format == BackbufferFormat::Custom) { if (formats[i].colorSpace == current_custom_backbuffer_format.colorSpace && formats[i].format == current_custom_backbuffer_format.format) { format = formats[i]; break; } } else if (desired_format == BackbufferFormat::DisplayP3) { if (formats[i].colorSpace == VK_COLOR_SPACE_DISPLAY_P3_NONLINEAR_EXT && (formats[i].format == VK_FORMAT_A2B10G10R10_UNORM_PACK32 || formats[i].format == VK_FORMAT_A2R10G10B10_UNORM_PACK32)) { format = formats[i]; break; } } else if (desired_format == BackbufferFormat::UNORMPassthrough) { if (formats[i].colorSpace == VK_COLOR_SPACE_PASS_THROUGH_EXT && (formats[i].format == VK_FORMAT_R8G8B8A8_UNORM || formats[i].format == VK_FORMAT_B8G8R8A8_UNORM || formats[i].format == VK_FORMAT_A2B10G10R10_UNORM_PACK32 || formats[i].format == VK_FORMAT_A2R10G10B10_UNORM_PACK32)) { format = formats[i]; break; } } else if (desired_format == BackbufferFormat::HDR10) { if (formats[i].colorSpace == VK_COLOR_SPACE_HDR10_ST2084_EXT && (formats[i].format == VK_FORMAT_A2B10G10R10_UNORM_PACK32 || formats[i].format == VK_FORMAT_A2R10G10B10_UNORM_PACK32)) { format = formats[i]; break; } } else if (desired_format == BackbufferFormat::scRGB) { if (formats[i].colorSpace == VK_COLOR_SPACE_EXTENDED_SRGB_LINEAR_EXT && formats[i].format == VK_FORMAT_R16G16B16A16_SFLOAT) { format = formats[i]; break; } } else if (desired_format == BackbufferFormat::sRGB) { if (formats[i].colorSpace == VK_COLOR_SPACE_SRGB_NONLINEAR_KHR && (formats[i].format == VK_FORMAT_R8G8B8A8_SRGB || formats[i].format == VK_FORMAT_B8G8R8A8_SRGB || formats[i].format == VK_FORMAT_A8B8G8R8_SRGB_PACK32)) { format = formats[i]; break; } } else { if (formats[i].colorSpace == VK_COLOR_SPACE_SRGB_NONLINEAR_KHR && (formats[i].format == VK_FORMAT_R8G8B8A8_UNORM || formats[i].format == VK_FORMAT_B8G8R8A8_UNORM || formats[i].format == VK_FORMAT_A2B10G10R10_UNORM_PACK32 || formats[i].format == VK_FORMAT_A2R10G10B10_UNORM_PACK32 || formats[i].format == VK_FORMAT_A8B8G8R8_UNORM_PACK32)) { format = formats[i]; break; } } } return format; } struct SurfaceInfo { VkPhysicalDeviceSurfaceInfo2KHR surface_info; VkSurfacePresentModeKHR present_mode; VkSurfaceCapabilitiesKHR surface_capabilities; std::vector formats; VkSwapchainPresentModesCreateInfoKHR present_modes_info; VkImageCompressionControlEXT compression_control; VkImageCompressionFixedRateFlagsEXT compression_control_fixed_rates; VkSurfaceCapabilitiesPresentId2KHR present_id2; VkSurfaceCapabilitiesPresentWait2KHR present_wait2; VkPresentTimingSurfaceCapabilitiesEXT present_timing; std::vector present_mode_compat_group; const void *swapchain_pnext; VkSwapchainLatencyCreateInfoNV latency_create_info; #ifdef _WIN32 VkSurfaceFullScreenExclusiveInfoEXT exclusive_info; VkSurfaceFullScreenExclusiveWin32InfoEXT exclusive_info_win32; #endif }; static bool init_surface_info(Device &device, WSIPlatform &platform, VkSurfaceKHR surface, BackbufferFormat format, const WSI::ImageCompression &compression, PresentMode present_mode, SurfaceInfo &info, bool low_latency_mode_enable) { if (surface == VK_NULL_HANDLE) { LOGE("Cannot create swapchain with surface == VK_NULL_HANDLE.\n"); return false; } info.surface_info = { VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_SURFACE_INFO_2_KHR }; info.surface_info.surface = surface; info.swapchain_pnext = nullptr; auto &ext = device.get_device_features(); #ifdef _WIN32 if (ext.supports_full_screen_exclusive) { info.exclusive_info = { VK_STRUCTURE_TYPE_SURFACE_FULL_SCREEN_EXCLUSIVE_INFO_EXT }; auto monitor = reinterpret_cast(platform.get_fullscreen_monitor()); info.swapchain_pnext = &info.exclusive_info; info.surface_info.pNext = &info.exclusive_info; if (monitor != nullptr) { info.exclusive_info_win32 = { VK_STRUCTURE_TYPE_SURFACE_FULL_SCREEN_EXCLUSIVE_WIN32_INFO_EXT }; info.exclusive_info.pNext = &info.exclusive_info_win32; info.exclusive_info_win32.hmonitor = monitor; LOGI("Win32: Got a full-screen monitor.\n"); } else LOGI("Win32: Not running full-screen.\n"); bool prefer_exclusive = Util::get_environment_bool("GRANITE_EXCLUSIVE_FULL_SCREEN", false) || low_latency_mode_enable; if (ext.driver_id == VK_DRIVER_ID_INTEL_PROPRIETARY_WINDOWS) prefer_exclusive = false; // Broken on Intel Windows if (ext.driver_id == VK_DRIVER_ID_AMD_PROPRIETARY && (format == BackbufferFormat::HDR10 || format == BackbufferFormat::scRGB)) { LOGI("Win32: HDR requested on AMD Windows. Forcing exclusive fullscreen, or HDR will not work properly.\n"); prefer_exclusive = true; } if (prefer_exclusive && monitor != nullptr) { LOGI("Win32: Opting in to exclusive full-screen!\n"); info.exclusive_info.fullScreenExclusive = VK_FULL_SCREEN_EXCLUSIVE_ALLOWED_EXT; // Try to promote this to application controlled exclusive. VkSurfaceCapabilities2KHR surface_capabilities2 = { VK_STRUCTURE_TYPE_SURFACE_CAPABILITIES_2_KHR }; VkSurfaceCapabilitiesFullScreenExclusiveEXT capability_full_screen_exclusive = { VK_STRUCTURE_TYPE_SURFACE_CAPABILITIES_FULL_SCREEN_EXCLUSIVE_EXT }; surface_capabilities2.pNext = &capability_full_screen_exclusive; if (vkGetPhysicalDeviceSurfaceCapabilities2KHR(device.get_physical_device(), &info.surface_info, &surface_capabilities2) != VK_SUCCESS) return false; if (capability_full_screen_exclusive.fullScreenExclusiveSupported) { LOGI("Win32: Opting for exclusive fullscreen access.\n"); info.exclusive_info.fullScreenExclusive = VK_FULL_SCREEN_EXCLUSIVE_APPLICATION_CONTROLLED_EXT; } } else { LOGI("Win32: Opting out of exclusive full-screen!\n"); info.exclusive_info.fullScreenExclusive = prefer_exclusive ? VK_FULL_SCREEN_EXCLUSIVE_ALLOWED_EXT : VK_FULL_SCREEN_EXCLUSIVE_DISALLOWED_EXT; } } #else (void)platform; (void)format; #endif std::vector present_modes; uint32_t num_present_modes = 0; auto gpu = device.get_physical_device(); #ifdef _WIN32 if (ext.supports_surface_capabilities2 && ext.supports_full_screen_exclusive) { if (vkGetPhysicalDeviceSurfacePresentModes2EXT(gpu, &info.surface_info, &num_present_modes, nullptr) != VK_SUCCESS) { return false; } present_modes.resize(num_present_modes); if (vkGetPhysicalDeviceSurfacePresentModes2EXT(gpu, &info.surface_info, &num_present_modes, present_modes.data()) != VK_SUCCESS) { return false; } } else #endif { if (vkGetPhysicalDeviceSurfacePresentModesKHR(gpu, surface, &num_present_modes, nullptr) != VK_SUCCESS) return false; present_modes.resize(num_present_modes); if (vkGetPhysicalDeviceSurfacePresentModesKHR(gpu, surface, &num_present_modes, present_modes.data()) != VK_SUCCESS) return false; } auto swapchain_present_mode = VK_PRESENT_MODE_FIFO_KHR; bool use_vsync = present_mode == PresentMode::SyncToVBlank; if (!use_vsync) { bool allow_mailbox = present_mode != PresentMode::UnlockedForceTearing; bool allow_immediate = present_mode != PresentMode::UnlockedNoTearing; #ifdef _WIN32 // If we're trying to go exclusive full-screen, // we need to ban certain types of present modes which apparently do not work as we expect. if (info.exclusive_info.fullScreenExclusive == VK_FULL_SCREEN_EXCLUSIVE_APPLICATION_CONTROLLED_EXT) allow_mailbox = false; #endif for (auto &mode : present_modes) { if ((allow_immediate && mode == VK_PRESENT_MODE_IMMEDIATE_KHR) || (allow_mailbox && mode == VK_PRESENT_MODE_MAILBOX_KHR)) { swapchain_present_mode = mode; break; } } } if (swapchain_present_mode == VK_PRESENT_MODE_FIFO_KHR && low_latency_mode_enable) for (auto mode : present_modes) if (mode == VK_PRESENT_MODE_FIFO_RELAXED_KHR) swapchain_present_mode = mode; LOGI("Using present mode: %u.\n", swapchain_present_mode); // First, query minImageCount without any present mode. // Avoid opting for present mode compat that is pathological in nature, // e.g. Xorg MAILBOX where minImageCount shoots up to 5 for stupid reasons. if (ext.supports_surface_capabilities2) { VkSurfaceCapabilities2KHR surface_capabilities2 = { VK_STRUCTURE_TYPE_SURFACE_CAPABILITIES_2_KHR }; if (ext.present_id2_features.presentId2 && ext.present_wait2_features.presentWait2) { info.present_wait2 = { VK_STRUCTURE_TYPE_SURFACE_CAPABILITIES_PRESENT_WAIT_2_KHR }; info.present_id2 = { VK_STRUCTURE_TYPE_SURFACE_CAPABILITIES_PRESENT_ID_2_KHR }; info.present_wait2.pNext = &info.present_id2; surface_capabilities2.pNext = &info.present_wait2; } if (ext.present_timing_features.presentTiming) { info.present_timing = { VK_STRUCTURE_TYPE_PRESENT_TIMING_SURFACE_CAPABILITIES_EXT }; info.present_timing.pNext = surface_capabilities2.pNext; surface_capabilities2.pNext = &info.present_timing; } if (vkGetPhysicalDeviceSurfaceCapabilities2KHR(gpu, &info.surface_info, &surface_capabilities2) != VK_SUCCESS) return false; info.surface_capabilities = surface_capabilities2.surfaceCapabilities; } else { if (vkGetPhysicalDeviceSurfaceCapabilitiesKHR(gpu, surface, &info.surface_capabilities) != VK_SUCCESS) return false; } // Make sure we query surface caps tied to the present mode for correct results. if (ext.swapchain_maintenance1_features.swapchainMaintenance1 && ext.supports_surface_capabilities2) { VkSurfaceCapabilities2KHR surface_capabilities2 = { VK_STRUCTURE_TYPE_SURFACE_CAPABILITIES_2_KHR }; VkSurfacePresentModeCompatibilityKHR present_mode_caps = { VK_STRUCTURE_TYPE_SURFACE_PRESENT_MODE_COMPATIBILITY_KHR }; std::vector present_mode_compat_group; present_mode_compat_group.resize(32); present_mode_caps.presentModeCount = present_mode_compat_group.size(); present_mode_caps.pPresentModes = present_mode_compat_group.data(); info.present_mode.pNext = const_cast(info.surface_info.pNext); info.surface_info.pNext = &info.present_mode; info.present_mode = { VK_STRUCTURE_TYPE_SURFACE_PRESENT_MODE_KHR }; info.present_mode.presentMode = swapchain_present_mode; surface_capabilities2.pNext = &present_mode_caps; if (vkGetPhysicalDeviceSurfaceCapabilities2KHR(gpu, &info.surface_info, &surface_capabilities2) != VK_SUCCESS) return false; surface_capabilities2.pNext = present_mode_caps.pNext; info.surface_capabilities.minImageCount = surface_capabilities2.surfaceCapabilities.minImageCount; present_mode_compat_group.resize(present_mode_caps.presentModeCount); info.present_mode_compat_group.reserve(present_mode_caps.presentModeCount); info.present_mode_compat_group.push_back(swapchain_present_mode); for (auto mode : present_mode_compat_group) { if (mode == swapchain_present_mode) continue; // Only allow sensible present modes that we know of. if (mode != VK_PRESENT_MODE_FIFO_KHR && mode != VK_PRESENT_MODE_FIFO_RELAXED_KHR && mode != VK_PRESENT_MODE_IMMEDIATE_KHR && mode != VK_PRESENT_MODE_MAILBOX_KHR) { continue; } info.present_mode.presentMode = mode; if (vkGetPhysicalDeviceSurfaceCapabilities2KHR(gpu, &info.surface_info, &surface_capabilities2) != VK_SUCCESS) return false; // Accept the present mode if it does not modify minImageCount. // If image count changes, we should probably recreate the swapchain. // If we have present wait we're at no risk of adding more latency, so just go ahead. if (surface_capabilities2.surfaceCapabilities.minImageCount == info.surface_capabilities.minImageCount || device.get_device_features().present_wait_features.presentWait || info.present_wait2.presentWait2Supported) { info.present_mode_compat_group.push_back(mode); info.surface_capabilities.minImageCount = std::max(info.surface_capabilities.minImageCount, surface_capabilities2.surfaceCapabilities.minImageCount); } } } uint32_t format_count = 0; if (ext.supports_surface_capabilities2) { if (vkGetPhysicalDeviceSurfaceFormats2KHR(device.get_physical_device(), &info.surface_info, &format_count, nullptr) != VK_SUCCESS) { return false; } std::vector formats2(format_count); for (auto &f : formats2) { f = {}; f.sType = VK_STRUCTURE_TYPE_SURFACE_FORMAT_2_KHR; } if (vkGetPhysicalDeviceSurfaceFormats2KHR(gpu, &info.surface_info, &format_count, formats2.data()) != VK_SUCCESS) return false; info.formats.reserve(format_count); for (auto &f : formats2) info.formats.push_back(f.surfaceFormat); } else { if (vkGetPhysicalDeviceSurfaceFormatsKHR(gpu, surface, &format_count, nullptr) != VK_SUCCESS) return false; info.formats.resize(format_count); if (vkGetPhysicalDeviceSurfaceFormatsKHR(gpu, surface, &format_count, info.formats.data()) != VK_SUCCESS) return false; } // Ensure that 10-bit formats come before other formats. std::sort(info.formats.begin(), info.formats.end(), [](const VkSurfaceFormatKHR &a, const VkSurfaceFormatKHR &b) { const auto qual = [](VkFormat fmt) { // Prefer a consistent ordering so Fossilize caches are more effective. if (fmt == VK_FORMAT_A2B10G10R10_UNORM_PACK32) return 3; else if (fmt == VK_FORMAT_A2R10G10B10_UNORM_PACK32) return 2; else if (fmt == VK_FORMAT_B8G8R8A8_UNORM) return 1; else return 0; }; return qual(a.format) > qual(b.format); }); // Allow for seamless toggle between presentation modes. if (ext.swapchain_maintenance1_features.swapchainMaintenance1) { info.present_modes_info = { VK_STRUCTURE_TYPE_SWAPCHAIN_PRESENT_MODES_CREATE_INFO_KHR }; info.present_modes_info.pNext = const_cast(info.swapchain_pnext); info.present_modes_info.presentModeCount = info.present_mode_compat_group.size(); info.present_modes_info.pPresentModes = info.present_mode_compat_group.data(); info.swapchain_pnext = &info.present_modes_info; } info.present_mode.presentMode = swapchain_present_mode; if (ext.image_compression_control_swapchain_features.imageCompressionControlSwapchain && compression.type != VK_IMAGE_COMPRESSION_DEFAULT_EXT) { // There is no VU that we cannot just pass in whatever we want here, // but we might not be honored if we pass in something unsupported. // That's fine for now. info.compression_control = { VK_STRUCTURE_TYPE_IMAGE_COMPRESSION_CONTROL_EXT }; info.compression_control.pNext = info.swapchain_pnext; info.compression_control.flags = compression.type; if (compression.type == VK_IMAGE_COMPRESSION_FIXED_RATE_EXPLICIT_EXT) { info.compression_control_fixed_rates = compression.fixed_rates; info.compression_control.pFixedRateFlags = &info.compression_control_fixed_rates; info.compression_control.compressionControlPlaneCount = 1; LOGI("Using fixed-rate compression for swapchain (flags #%08x).\n", compression.fixed_rates); } else if (compression.type == VK_IMAGE_COMPRESSION_FIXED_RATE_DEFAULT_EXT) LOGI("Using default fixed-rate compression for swapchain.\n"); else if (compression.type == VK_IMAGE_COMPRESSION_DISABLED_EXT) LOGI("Disabling compression for swapchain.\n"); info.swapchain_pnext = &info.compression_control; } if (ext.supports_low_latency2_nv) { info.latency_create_info = { VK_STRUCTURE_TYPE_SWAPCHAIN_LATENCY_CREATE_INFO_NV }; info.latency_create_info.latencyModeEnable = VK_TRUE; info.latency_create_info.pNext = info.swapchain_pnext; info.swapchain_pnext = &info.latency_create_info; } return true; } WSI::SwapchainError WSI::init_swapchain(unsigned width, unsigned height) { SurfaceInfo surface_info = {}; if (!init_surface_info(*device, *platform, surface, current_backbuffer_format, current_compression, current_present_mode, surface_info, low_latency_mode_enable_present)) { return SwapchainError::Error; } const auto &caps = surface_info.surface_capabilities; // Happens on Windows when you minimize a window. if (caps.maxImageExtent.width == 0 && caps.maxImageExtent.height == 0) return SwapchainError::NoSurface; if (current_extra_usage && support_prerotate) { LOGW("Disabling prerotate support due to extra usage flags in swapchain.\n"); support_prerotate = false; } if (current_extra_usage & ~caps.supportedUsageFlags) { LOGW("Attempting to use unsupported usage flags 0x%x for swapchain.\n", current_extra_usage); current_extra_usage &= caps.supportedUsageFlags; extra_usage = current_extra_usage; } auto attempt_backbuffer_format = current_backbuffer_format; auto surface_format = find_suitable_present_format(surface_info.formats, attempt_backbuffer_format); if (surface_format.format == VK_FORMAT_UNDEFINED && (attempt_backbuffer_format == BackbufferFormat::HDR10 || attempt_backbuffer_format == BackbufferFormat::scRGB || attempt_backbuffer_format == BackbufferFormat::DisplayP3 || attempt_backbuffer_format == BackbufferFormat::UNORMPassthrough || attempt_backbuffer_format == BackbufferFormat::Custom)) { LOGW("Could not find suitable present format for HDR. Attempting fallback to UNORM.\n"); attempt_backbuffer_format = BackbufferFormat::UNORM; surface_format = find_suitable_present_format(surface_info.formats, attempt_backbuffer_format); } if (surface_format.format == VK_FORMAT_UNDEFINED) { LOGW("Could not find supported format for swapchain usage flags 0x%x.\n", current_extra_usage); current_extra_usage = 0; extra_usage = 0; surface_format = find_suitable_present_format(surface_info.formats, attempt_backbuffer_format); } if (surface_format.format == VK_FORMAT_UNDEFINED) { LOGE("Failed to find any suitable format for swapchain.\n"); return SwapchainError::Error; } static const char *transform_names[] = { "IDENTITY_BIT_KHR", "ROTATE_90_BIT_KHR", "ROTATE_180_BIT_KHR", "ROTATE_270_BIT_KHR", "HORIZONTAL_MIRROR_BIT_KHR", "HORIZONTAL_MIRROR_ROTATE_90_BIT_KHR", "HORIZONTAL_MIRROR_ROTATE_180_BIT_KHR", "HORIZONTAL_MIRROR_ROTATE_270_BIT_KHR", "INHERIT_BIT_KHR", }; LOGI("Current transform is enum 0x%x.\n", unsigned(caps.currentTransform)); for (unsigned i = 0; i <= 8; i++) { if (caps.supportedTransforms & (1u << i)) LOGI("Supported transform 0x%x: %s.\n", 1u << i, transform_names[i]); } VkSurfaceTransformFlagBitsKHR pre_transform; if (!support_prerotate && (caps.supportedTransforms & VK_SURFACE_TRANSFORM_IDENTITY_BIT_KHR) != 0) pre_transform = VK_SURFACE_TRANSFORM_IDENTITY_BIT_KHR; else { // Only attempt to use prerotate if we can deal with it purely using a XY clip fixup. // For horizontal flip we need to start flipping front-face as well ... if ((caps.currentTransform & ( VK_SURFACE_TRANSFORM_ROTATE_90_BIT_KHR | VK_SURFACE_TRANSFORM_ROTATE_180_BIT_KHR | VK_SURFACE_TRANSFORM_ROTATE_270_BIT_KHR)) != 0) pre_transform = caps.currentTransform; else pre_transform = VK_SURFACE_TRANSFORM_IDENTITY_BIT_KHR; } if (pre_transform != caps.currentTransform) { LOGW("surfaceTransform (0x%x) != currentTransform (0x%u). Might get performance penalty.\n", unsigned(pre_transform), unsigned(caps.currentTransform)); } swapchain_current_prerotate = pre_transform; VkExtent2D swapchain_size; LOGI("Swapchain current extent: %d x %d\n", int(caps.currentExtent.width), int(caps.currentExtent.height)); if (width == 0) { if (caps.currentExtent.width != ~0u) width = caps.currentExtent.width; else width = 1280; LOGI("Auto selected width = %u.\n", width); } if (height == 0) { if (caps.currentExtent.height != ~0u) height = caps.currentExtent.height; else height = 720; LOGI("Auto selected height = %u.\n", height); } // Try to match the swapchain size up with what we expect w.r.t. aspect ratio. float target_aspect_ratio = float(width) / float(height); if ((swapchain_aspect_ratio > 1.0f && target_aspect_ratio < 1.0f) || (swapchain_aspect_ratio < 1.0f && target_aspect_ratio > 1.0f)) { std::swap(width, height); } // If we are using pre-rotate of 90 or 270 degrees, we need to flip width and height again. if (swapchain_current_prerotate & (VK_SURFACE_TRANSFORM_ROTATE_90_BIT_KHR | VK_SURFACE_TRANSFORM_ROTATE_270_BIT_KHR | VK_SURFACE_TRANSFORM_HORIZONTAL_MIRROR_ROTATE_90_BIT_KHR | VK_SURFACE_TRANSFORM_HORIZONTAL_MIRROR_ROTATE_270_BIT_KHR)) { std::swap(width, height); } // Clamp the target width, height to boundaries. swapchain_size.width = std::max(std::min(width, caps.maxImageExtent.width), caps.minImageExtent.width); swapchain_size.height = std::max(std::min(height, caps.maxImageExtent.height), caps.minImageExtent.height); uint32_t desired_swapchain_images = low_latency_mode_enable_present && current_present_mode == PresentMode::SyncToVBlank ? 2 : 3; supports_present_wait2 = surface_info.present_id2.presentId2Supported && surface_info.present_wait2.presentWait2Supported; // Need a deeper swapchain to avoid potential stalls when duping frames. // We only do this when present wait is supported, so latency should not be compromised. if (current_frame_dupe_aware && (device->get_device_features().present_wait_features.presentWait || supports_present_wait2)) desired_swapchain_images = current_frame_dupe_target_images; desired_swapchain_images = Util::get_environment_uint("GRANITE_VULKAN_SWAPCHAIN_IMAGES", desired_swapchain_images); LOGI("Targeting %u swapchain images.\n", desired_swapchain_images); if (desired_swapchain_images < caps.minImageCount) desired_swapchain_images = caps.minImageCount; if ((caps.maxImageCount > 0) && (desired_swapchain_images > caps.maxImageCount)) desired_swapchain_images = caps.maxImageCount; VkCompositeAlphaFlagBitsKHR composite_mode = VK_COMPOSITE_ALPHA_OPAQUE_BIT_KHR; if (caps.supportedCompositeAlpha & VK_COMPOSITE_ALPHA_OPAQUE_BIT_KHR) composite_mode = VK_COMPOSITE_ALPHA_OPAQUE_BIT_KHR; else if (caps.supportedCompositeAlpha & VK_COMPOSITE_ALPHA_INHERIT_BIT_KHR) composite_mode = VK_COMPOSITE_ALPHA_INHERIT_BIT_KHR; else if (caps.supportedCompositeAlpha & VK_COMPOSITE_ALPHA_POST_MULTIPLIED_BIT_KHR) composite_mode = VK_COMPOSITE_ALPHA_POST_MULTIPLIED_BIT_KHR; else if (caps.supportedCompositeAlpha & VK_COMPOSITE_ALPHA_PRE_MULTIPLIED_BIT_KHR) composite_mode = VK_COMPOSITE_ALPHA_PRE_MULTIPLIED_BIT_KHR; else LOGW("No sensible composite mode supported?\n"); VkSwapchainKHR old_swapchain = swapchain; VkSwapchainCreateInfoKHR info = { VK_STRUCTURE_TYPE_SWAPCHAIN_CREATE_INFO_KHR }; info.surface = surface; info.pNext = surface_info.swapchain_pnext; info.minImageCount = desired_swapchain_images; info.imageFormat = surface_format.format; info.imageColorSpace = surface_format.colorSpace; info.imageExtent.width = swapchain_size.width; info.imageExtent.height = swapchain_size.height; info.imageArrayLayers = 1; info.imageUsage = VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT | current_extra_usage; info.imageSharingMode = VK_SHARING_MODE_EXCLUSIVE; info.preTransform = pre_transform; info.compositeAlpha = composite_mode; info.presentMode = surface_info.present_mode.presentMode; info.clipped = VK_TRUE; info.oldSwapchain = old_swapchain; // Defer the deletion instead. if (device->get_device_features().swapchain_maintenance1_features.swapchainMaintenance1 && old_swapchain != VK_NULL_HANDLE && !device->get_workarounds().broken_present_fence) { deferred_swapchains.push_back({ old_swapchain, last_present_fence }); old_swapchain = VK_NULL_HANDLE; } platform->event_swapchain_destroyed(); supports_present_timing.feedback = surface_info.present_timing.presentTimingSupported ? surface_info.present_timing.presentStageQueries : 0; // Only ackknowledge present stages we understand in case there are future extensions. supports_present_timing.feedback &= VK_PRESENT_STAGE_QUEUE_OPERATIONS_END_BIT_EXT | VK_PRESENT_STAGE_REQUEST_DEQUEUED_BIT_EXT | VK_PRESENT_STAGE_IMAGE_FIRST_PIXEL_OUT_BIT_EXT | VK_PRESENT_STAGE_IMAGE_FIRST_PIXEL_VISIBLE_BIT_EXT; supports_present_timing.absolute = surface_info.present_timing.presentTimingSupported && surface_info.present_timing.presentAtAbsoluteTimeSupported; supports_present_timing.relative = surface_info.present_timing.presentTimingSupported && surface_info.present_timing.presentAtRelativeTimeSupported; if (supports_present_wait2) info.flags |= VK_SWAPCHAIN_CREATE_PRESENT_ID_2_BIT_KHR | VK_SWAPCHAIN_CREATE_PRESENT_WAIT_2_BIT_KHR; else supports_present_timing.feedback = 0; if (supports_present_timing.feedback) { info.flags |= VK_SWAPCHAIN_CREATE_PRESENT_TIMING_BIT_EXT; LOGI("Enabling present timing, queries #%x, absolute %u, relative %u.\n", supports_present_timing.feedback, supports_present_timing.absolute, supports_present_timing.relative); } auto res = table->vkCreateSwapchainKHR(context->get_device(), &info, nullptr, &swapchain); if (res < 0) swapchain = VK_NULL_HANDLE; if (res == VK_SUCCESS && supports_present_timing.feedback) { table->vkSetSwapchainPresentTimingQueueSizeEXT(context->get_device(), swapchain, PresentTimingQueueSize); present_timing = {}; update_present_timing_properties(); update_time_domain_properties(); recalibrate_present_timing_domains(); } platform->destroy_swapchain_resources(old_swapchain); table->vkDestroySwapchainKHR(context->get_device(), old_swapchain, nullptr); has_acquired_swapchain_index = false; next_present_id = 1; present_last_id = 0; device->set_present_id(VK_NULL_HANDLE, 0); if (res == VK_SUCCESS && device->get_device_features().supports_low_latency2_nv) { VkLatencySleepModeInfoNV sleep_mode_info = { VK_STRUCTURE_TYPE_LATENCY_SLEEP_MODE_INFO_NV }; sleep_mode_info.lowLatencyBoost = low_latency_mode_enable_gpu_submit; sleep_mode_info.lowLatencyMode = low_latency_mode_enable_gpu_submit; if (table->vkSetLatencySleepModeNV(context->get_device(), swapchain, &sleep_mode_info) != VK_SUCCESS) LOGE("Failed to set low latency sleep mode.\n"); } active_present_mode = info.presentMode; present_mode_compat_group = std::move(surface_info.present_mode_compat_group); #ifdef _WIN32 if (surface_info.exclusive_info.fullScreenExclusive == VK_FULL_SCREEN_EXCLUSIVE_APPLICATION_CONTROLLED_EXT) { bool success = vkAcquireFullScreenExclusiveModeEXT(context->get_device(), swapchain) == VK_SUCCESS; if (success) LOGI("Successfully acquired exclusive full-screen.\n"); else LOGI("Failed to acquire exclusive full-screen. Using borderless windowed.\n"); } #endif if (res != VK_SUCCESS) { LOGE("Failed to create swapchain (code: %d)\n", int(res)); swapchain = VK_NULL_HANDLE; return SwapchainError::Error; } swapchain_width = swapchain_size.width; swapchain_height = swapchain_size.height; swapchain_surface_format = surface_format; swapchain_is_suboptimal = false; LOGI("Created swapchain %u x %u (fmt: %u, transform: %u).\n", swapchain_width, swapchain_height, unsigned(swapchain_surface_format.format), unsigned(swapchain_current_prerotate)); uint32_t image_count; if (table->vkGetSwapchainImagesKHR(context->get_device(), swapchain, &image_count, nullptr) != VK_SUCCESS) return SwapchainError::Error; swapchain_images.resize(image_count); release_semaphores.resize(image_count); if (table->vkGetSwapchainImagesKHR(context->get_device(), swapchain, &image_count, swapchain_images.data()) != VK_SUCCESS) return SwapchainError::Error; LOGI("Got %u swapchain images.\n", image_count); platform->event_swapchain_created(device.get(), swapchain, swapchain_width, swapchain_height, swapchain_aspect_ratio, image_count, swapchain_surface_format.format, swapchain_surface_format.colorSpace, swapchain_current_prerotate); if (swapchain_surface_format.colorSpace == VK_COLOR_SPACE_HDR10_ST2084_EXT && valid_hdr_metadata && device->get_device_features().supports_hdr_metadata) { table->vkSetHdrMetadataEXT(device->get_device(), 1, &swapchain, &hdr_metadata); } return SwapchainError::None; } void WSI::set_support_prerotate(bool enable) { support_prerotate = enable; } VkSurfaceTransformFlagBitsKHR WSI::get_current_prerotate() const { return swapchain_current_prerotate; } CommandBuffer::Type WSI::get_current_present_queue_type() const { return device->get_current_present_queue_type(); } WSI::~WSI() { teardown(); } void WSIPlatform::event_device_created(Device *) {} void WSIPlatform::event_device_destroyed() {} void WSIPlatform::event_swapchain_created(Device *, VkSwapchainKHR, unsigned, unsigned, float, size_t, VkFormat, VkColorSpaceKHR, VkSurfaceTransformFlagBitsKHR) {} void WSIPlatform::event_swapchain_destroyed() {} void WSIPlatform::destroy_swapchain_resources(VkSwapchainKHR) {} void WSIPlatform::event_frame_tick(double, double) {} void WSIPlatform::event_swapchain_index(Device *, unsigned) {} void WSIPlatform::begin_drop_event() {} void WSIPlatform::begin_soft_keyboard(const std::string &) {} void WSIPlatform::end_soft_keyboard() {} void WSIPlatform::show_message_box(const std::string &, Vulkan::WSIPlatform::MessageType) {} }