From af04b3e04cc1ff46d2ece0152f6e98ff2cc3d73b Mon Sep 17 00:00:00 2001 From: Lionel Duc Date: Thu, 29 Jan 2026 23:15:45 +0100 Subject: [PATCH 1/3] Add simple API sample for VK_EXT_present_timing. --- antora/modules/ROOT/nav.adoc | 1 + samples/CMakeLists.txt | 7 +- samples/api/README.adoc | 8 +- .../swapchain_present_timing/CMakeLists.txt | 37 + .../api/swapchain_present_timing/README.adoc | 102 ++ .../swapchain_present_timing.cpp | 1007 +++++++++++++++++ .../swapchain_present_timing.h | 183 +++ .../swapchain_present_timing/glsl/circle.frag | 46 + .../glsl/circle.frag.spv | Bin 0 -> 1712 bytes .../glsl/fullscreen_triangle.vert | 27 + .../glsl/fullscreen_triangle.vert.spv | Bin 0 -> 1208 bytes .../hlsl/circle.frag.hlsl | 49 + .../hlsl/circle.frag.spv | Bin 0 -> 1344 bytes .../hlsl/fullscreen_triangle.vert.hlsl | 34 + .../hlsl/fullscreen_triangle.vert.spv | Bin 0 -> 656 bytes .../slang/circle.frag.slang | 51 + .../slang/circle.frag.spv | Bin 0 -> 1296 bytes .../slang/fullscreen_triangle.vert.slang | 35 + .../slang/fullscreen_triangle.vert.spv | Bin 0 -> 736 bytes 19 files changed, 1582 insertions(+), 5 deletions(-) create mode 100644 samples/api/swapchain_present_timing/CMakeLists.txt create mode 100644 samples/api/swapchain_present_timing/README.adoc create mode 100644 samples/api/swapchain_present_timing/swapchain_present_timing.cpp create mode 100644 samples/api/swapchain_present_timing/swapchain_present_timing.h create mode 100644 shaders/swapchain_present_timing/glsl/circle.frag create mode 100644 shaders/swapchain_present_timing/glsl/circle.frag.spv create mode 100644 shaders/swapchain_present_timing/glsl/fullscreen_triangle.vert create mode 100644 shaders/swapchain_present_timing/glsl/fullscreen_triangle.vert.spv create mode 100644 shaders/swapchain_present_timing/hlsl/circle.frag.hlsl create mode 100644 shaders/swapchain_present_timing/hlsl/circle.frag.spv create mode 100644 shaders/swapchain_present_timing/hlsl/fullscreen_triangle.vert.hlsl create mode 100644 shaders/swapchain_present_timing/hlsl/fullscreen_triangle.vert.spv create mode 100644 shaders/swapchain_present_timing/slang/circle.frag.slang create mode 100644 shaders/swapchain_present_timing/slang/circle.frag.spv create mode 100644 shaders/swapchain_present_timing/slang/fullscreen_triangle.vert.slang create mode 100644 shaders/swapchain_present_timing/slang/fullscreen_triangle.vert.spv diff --git a/antora/modules/ROOT/nav.adoc b/antora/modules/ROOT/nav.adoc index 8ead15b3da..149f569c44 100644 --- a/antora/modules/ROOT/nav.adoc +++ b/antora/modules/ROOT/nav.adoc @@ -50,6 +50,7 @@ *** xref:samples/api/hpp_oit_linked_lists/README.adoc[OIT linked lists (Vulkan-Hpp)] ** xref:samples/api/oit_depth_peeling/README.adoc[OIT depth peeling] *** xref:samples/api/hpp_oit_depth_peeling/README.adoc[OIT depth peeling (Vulkan-Hpp)] +** xref:samples/api/swapchain_present_timing/README.adoc[Swapchain present timing] * xref:samples/extensions/README.adoc[Extension usage samples] ** xref:samples/extensions/buffer_device_address/README.adoc[Buffer device address] ** xref:samples/extensions/calibrated_timestamps/README.adoc[Calibrated timestamps] diff --git a/samples/CMakeLists.txt b/samples/CMakeLists.txt index 684966fe80..5cb67099a7 100644 --- a/samples/CMakeLists.txt +++ b/samples/CMakeLists.txt @@ -59,6 +59,7 @@ set(ORDER_LIST "terrain_tessellation" "oit_linked_lists" "oit_depth_peeling" + "swapchain_present_timing" #Extension Samples "dynamic_rendering" @@ -136,16 +137,16 @@ set(ORDER_LIST "hpp_terrain_tessellation" "hpp_texture_loading" "hpp_texture_mipmap_generation" - + #HPP Extension Samples "hpp_mesh_shading" "hpp_push_descriptors" - + #HPP Performance Samples "hpp_pipeline_cache" "hpp_swapchain_images" "hpp_texture_compression_comparison" - + #General Samples "mobile_nerf") diff --git a/samples/api/README.adoc b/samples/api/README.adoc index cdb13a916a..c866186cb4 100644 --- a/samples/api/README.adoc +++ b/samples/api/README.adoc @@ -1,5 +1,5 @@ //// -- Copyright (c) 2021-2025, The Khronos Group +- Copyright (c) 2021-2026, The Khronos Group - - SPDX-License-Identifier: Apache-2.0 - @@ -129,4 +129,8 @@ A sample that implements an order-independent transparency algorithm using per-p === xref:./{api_samplespath}oit_depth_peeling/README.adoc[Order-independent transparency with depth peeling] -A sample that implements order-independent transparency with depth peeling. \ No newline at end of file +A sample that implements order-independent transparency with depth peeling. + +=== xref:./{api_samplespath}swapchain_present_timing/README.adoc[Swapchain present timing] + +A sample that demonstrates basic usage of presentation timing features. diff --git a/samples/api/swapchain_present_timing/CMakeLists.txt b/samples/api/swapchain_present_timing/CMakeLists.txt new file mode 100644 index 0000000000..17114feca5 --- /dev/null +++ b/samples/api/swapchain_present_timing/CMakeLists.txt @@ -0,0 +1,37 @@ +# Copyright (c) 2026, NVIDIA CORPORATION +# +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 the "License"; +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +get_filename_component(FOLDER_NAME ${CMAKE_CURRENT_LIST_DIR} NAME) +get_filename_component(PARENT_DIR ${CMAKE_CURRENT_LIST_DIR} PATH) +get_filename_component(CATEGORY_NAME ${PARENT_DIR} NAME) + +add_sample( + ID ${FOLDER_NAME} + CATEGORY ${CATEGORY_NAME} + AUTHOR "NVIDIA" + NAME "Swapchain Present Timing" + DESCRIPTION "Presentation using VK_EXT_present_timing" + SHADER_FILES_GLSL + "${FOLDER_NAME}/glsl/fullscreen_triangle.vert" + "${FOLDER_NAME}/glsl/circle.frag" + SHADER_FILES_HLSL + "${FOLDER_NAME}/hlsl/fullscreen_triangle.vert.hlsl" + "${FOLDER_NAME}/hlsl/circle.frag.hlsl" + SHADER_FILES_SLANG + "${FOLDER_NAME}/slang/fullscreen_triangle.vert.slang" + "${FOLDER_NAME}/slang/circle.frag.slang") + diff --git a/samples/api/swapchain_present_timing/README.adoc b/samples/api/swapchain_present_timing/README.adoc new file mode 100644 index 0000000000..73547351a7 --- /dev/null +++ b/samples/api/swapchain_present_timing/README.adoc @@ -0,0 +1,102 @@ +//// +- Copyright (c) 2026, NVIDIA CORPORATION +- +- SPDX-License-Identifier: Apache-2.0 +- +- Licensed under the Apache License, Version 2.0 the "License"; +- you may not use this file except in compliance with the License. +- You may obtain a copy of the License at +- +- http://www.apache.org/licenses/LICENSE-2.0 +- +- Unless required by applicable law or agreed to in writing, software +- distributed under the License is distributed on an "AS IS" BASIS, +- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +- See the License for the specific language governing permissions and +- limitations under the License. +- +//// + += Swapchain Present Timing + +ifdef::site-gen-antora[] +TIP: The source for this sample can be found in the https://github.com/KhronosGroup/Vulkan-Samples/tree/main/samples/api/swapchain_present_timing[Khronos Vulkan samples github repository]. +endif::[] + +Frame pacing is an important aspect of achieving smooth rendering and synchronization in real-time applications. This sample demonstrates how to use the VK_EXT_present_timing extension to implement frame pacing with time-based presentation. + +The sample renders a circle moving linearly across the viewport, which is a good visual test for detecting micro-stutters and verifying smooth presentation. It queries past presentation timing information and uses the last known display time along with the swapchain's refresh cycle duration to predict when the next frame should be presented. + +Press 'P' on the keyboard to toggle the use of present timing feature. When present timing is disabled, animations will use the current CPU time during the application's update routine to simulate geometry placement. This is a common approach in realtime applications and can exhibit visual artifacts when frame times are inconsistent, e.g. when rendering complex very dynamic scenes. + +== Extension Features + +VK_EXT_present_timing exposes 3 features at the physical device level: +* `presentTiming` is required for the extension to be exposed, and allows the application to query past presentation timings. +* `presentAtAbsoluteTime` and `presentAtRelativeTime` allow applications to precisely control the time or duration of a presentation request. + +However, hardware support is only one part of the requirements to access those features. Presentation capabilities can vary greatly depending on the kind of VkSurfaceKHR used, since they are in large part also dictated by the system APIs available to the implementation. Surface capabilities may then only expose a subset of the physical device's and need to be queried as well to make sure the implementation can provide present timing features in its runtime environment. + +In addition to the "present-at" features, surfaces capabilities also expose the present stages the implementation is able to provide timing information for. Present stages represent steps in common presentation pipelines, so that different systems which provide time measurements for different things under a vague "display time" term can still express them in a common framework. For example, some systems might be able to accurately measure the time at which the display actually lit pixels, while others can only report when the system made the image available for the display. Having access to timing information from multiple present stages can also be useful to figure out the latency of the presentation engine. + +== Swapchain Setup + +After creating a swapchain with a capable `VkSurfaceKHR` and the `VK_SWAPCHAIN_CREATE_PRESENT_TIMING_BIT_EXT` flag bit, applications should first query important properties about the swapchain's timing. + +=== Timing Properties + +`VkSwapchainTimingPropertiesEXT` exposes a `refreshDuration` and a `refreshInterval` value. These two fields put together describe the behavior of the presentation engine: +* If both values are equal, the presentation engine is operating in a fixed refresh rate mode (FRR), and the value indicates the length of a refresh cycle in nanoseconds. +* If `refreshInterval` is `UINT64_MAX`, it means variable refresh rate (VRR) is active, and `refreshDuration` is the minimum duration of a refresh cycle achievable (i.e. the maximum framerate). +* If both values are different (but non-zero), then the presentation engine is operating in FRR mode, but with the ability to adjust its refresh duration by a factor of `refreshInterval` nanoseconds, sometimes referred to as Adaptive Refresh Rate (ARR). +* Finally, any value of zero means the implementation was not able to determine how the presentation engine operates. + +In this example which has trivial rendering, we simply use the `refreshDuration` value as a fixed time interval, or default to a 60Hz refresh rate if that is not available. + +=== Time Domains + +The time values are all expressed in a time domain chosen by the application among a list of candidates exposed by the swapchain. `VK_EXT_present_timing` introduces new opaque times domains that are local to a given swapchain: `VK_TIME_DOMAIN_PRESENT_STAGE_LOCAL_EXT`, which all implementations must support, and `VK_TIME_DOMAIN_SWAPCHAIN_LOCAL_EXT`. + +Because these are opaque time domains, it is possible for an implementation to expose more than one of the same kind, for example when a window is moved from one display to another. For this reason, time domains are also assigned a unique id by the implementation. + +* `VK_TIME_DOMAIN_SWAPCHAIN_LOCAL_EXT` refers to a time domain that is specific to a swapchain, but common across all present stages. +* `VK_TIME_DOMAIN_PRESENT_STAGE_LOCAL_EXT` must be associated with both a swapchain and a present stage. This is useful on platforms where present stages might be handled by different hardware, each with their own time domain. + +If available, choosing a wider time domain such as `VK_TIME_DOMAIN_DEVICE_KHR` can simplify the usage of the extension's API. + +=== Timing Results Queue + +Finally, the swapchain must be given the opportunity to allocate internal resources that are used to store the timing results until the application can collect them. This is done with `vkSetSwapchainPresentTimingQueueSizeEXT`. A common question is figuring out how much space to allocate. There is unfortunately no good way of figuring this out other than trying, as it depends on the latency of the presentation engine to fill those results, which cannot be known beforehand. In this example, we choose a multiple of the swapchain's image count, betting on results to be available within a few frames. + +If the internal timing results queue cannot hold any more data, calling `vkQueuePresentKHR` and requesting timing results returns a new error, `VK_ERROR_PRESENT_TIMING_QUEUE_FULL_EXT`. Applications can recover from this error by allocating more space in the queue, or stop results requests until more space has been made available. + +== Collecting Presentation Timings Data + +Timing results are retrieved by the application by calling `vkGetPastPresentationTimingEXT`. + +Results can be correlated to every `vkQueuePresentKHR` by using present IDs assigned with `VK_KHR_present_id2`. This allows applications to build a history of present timing statistics which can then be used to drive their frame pacing strategy. This sample shows an example of collecting those statistics, though it does not try to use them. + +Unexpected system events, such as those triggered by power management-related features, can cause the presentation engine to change its behavior, for example by throttling the presentation rate. Such changes are communicated to the application via the `VkPastPresentationTimingPropertiesEXT::timingPropertiesCounter` when they are related to timing properties, and `VkPastPresentationTimingPropertiesEXT::timeDomainsCounter` when time domains are affected. These "counter" values should be checked against the last known value returned from `vkGetSwapchainTimingPropertiesEXT` and `vkGetSwapchainTimeDomainPropertiesEXT` respectively. If they do not match, applications should query those properties again. + +== Rendering & Presentation + +=== Absolute vs. Relative Present Time + +`VK_EXT_present_timing` supports two methods for specifying when a frame should be presented: + +* Absolute Time (`presentAtAbsoluteTime`): `VkPresentTimingInfoEXT::targetTime` represents a timestamp in the selected time domain at which the image should be displayed. + +* Relative Time (`presentAtRelativeTime`): The `VkPresentTimingInfoEXT::targetTime` is the minimum duration the image should be visible. + +This sample provides examples for both methods. To compute the absolute time, it adds the current refresh duration to the last available display time. For relative time, the sample simply uses the current refresh duration as well. + +The `VK_PRESENT_TIMING_INFO_PRESENT_AT_NEAREST_REFRESH_CYCLE_BIT_EXT` flag instructs the implementation to align the requested time or duration to the nearest refresh cycle boundary. This is useful when working with fixed refresh rates, so that small errors in calculations won't cause a presentation request to miss a full refresh cycle, causing in turn micro-stutters. + +=== Adaptive Frame Pacing + +While this sample uses a fixed target duration, production applications can implement more sophisticated strategies by analyzing the present statistics: + +* Latency detection: Compare `present_ready_time` (when GPU finished the queue operations enqueued by `vkQueuePresentKHR`) with `present_display_time` to measure the full presentation latency from the device's perspective. +* Missed deadlines: Detect when `targetTime` is earlier than `present_display_time` by more than a full refresh cycle, indicating the frame missed its target. +* Headroom: Compare `present_ready_time` with `present_dequeued_time` to measure how much headroom is available. If consistently finishing early with enough headroom, the application may target a higher framerate. +* Micro-stutter Detection: Look for irregular spacing between consecutive `present_display_time` values diff --git a/samples/api/swapchain_present_timing/swapchain_present_timing.cpp b/samples/api/swapchain_present_timing/swapchain_present_timing.cpp new file mode 100644 index 0000000000..b6730753e2 --- /dev/null +++ b/samples/api/swapchain_present_timing/swapchain_present_timing.cpp @@ -0,0 +1,1007 @@ +/* Copyright (c) 2026, NVIDIA CORPORATION + * + * SPDX-License-Identifier: Apache-2.0 + * + * Licensed under the Apache License, Version 2.0 the "License"; + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "swapchain_present_timing.h" + +#include "common/vk_common.h" +#include "core/util/logging.hpp" +#include "filesystem/legacy.h" + +/** + * @brief Get a graphics queue suitable for presentation. + */ +void SwapchainPresentTiming::get_queue() +{ + queue = &get_device().get_queue_by_flags(VK_QUEUE_GRAPHICS_BIT, 0); + + // Make sure presentation is supported on this queue. This is practically always the case; + // if a platform/driver is found where this is not true, all queues supporting + // VK_QUEUE_GRAPHICS_BIT need to be queried and one that supports presentation picked. + VkBool32 supports_present = VK_FALSE; + vkGetPhysicalDeviceSurfaceSupportKHR(get_gpu_handle(), queue->get_family_index(), get_surface(), &supports_present); + + if (!supports_present) + { + throw std::runtime_error("Default graphics queue does not support present."); + } +} + +/** + * @brief Selects the swapchain surface format + */ +void SwapchainPresentTiming::select_surface_format() +{ + surface_format = vkb::select_surface_format(get_gpu_handle(), get_surface()); +} + +/** + * @brief Queries surface capabilities and present timing support + */ +void SwapchainPresentTiming::query_surface_capabilities() +{ + VkSurfaceCapabilities2KHR surface_capabilities{VK_STRUCTURE_TYPE_SURFACE_CAPABILITIES_2_KHR}; + VkPresentTimingSurfaceCapabilitiesEXT present_timing_capabilities{VK_STRUCTURE_TYPE_PRESENT_TIMING_SURFACE_CAPABILITIES_EXT}; + VkPhysicalDeviceSurfaceInfo2KHR surface_info{VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_SURFACE_INFO_2_KHR}; + + surface_info.surface = get_surface(); + + surface_capabilities.pNext = &present_timing_capabilities; + + VK_CHECK(vkGetPhysicalDeviceSurfaceCapabilities2KHR(get_gpu_handle(), &surface_info, &surface_capabilities)); + + if (surface_capabilities.surfaceCapabilities.currentExtent.width == 0xFFFFFFFF) + { + swapchain_extents = VkExtent2D{400, 300}; + } + else + { + swapchain_extents = surface_capabilities.surfaceCapabilities.currentExtent; + } + + // Do triple-buffering when possible. This is clamped to the min and max image count limits. + desired_swapchain_images = std::max(surface_capabilities.surfaceCapabilities.minImageCount, 3u); + if (surface_capabilities.surfaceCapabilities.maxImageCount > 0) + { + desired_swapchain_images = std::min(desired_swapchain_images, surface_capabilities.surfaceCapabilities.maxImageCount); + } + + // Find a supported composite type. + composite = VK_COMPOSITE_ALPHA_OPAQUE_BIT_KHR; + + if (surface_capabilities.surfaceCapabilities.supportedCompositeAlpha & VK_COMPOSITE_ALPHA_OPAQUE_BIT_KHR) + { + composite = VK_COMPOSITE_ALPHA_OPAQUE_BIT_KHR; + } + else if (surface_capabilities.surfaceCapabilities.supportedCompositeAlpha & VK_COMPOSITE_ALPHA_INHERIT_BIT_KHR) + { + composite = VK_COMPOSITE_ALPHA_INHERIT_BIT_KHR; + } + else if (surface_capabilities.surfaceCapabilities.supportedCompositeAlpha & VK_COMPOSITE_ALPHA_PRE_MULTIPLIED_BIT_KHR) + { + composite = VK_COMPOSITE_ALPHA_PRE_MULTIPLIED_BIT_KHR; + } + else if (surface_capabilities.surfaceCapabilities.supportedCompositeAlpha & VK_COMPOSITE_ALPHA_POST_MULTIPLIED_BIT_KHR) + { + composite = VK_COMPOSITE_ALPHA_POST_MULTIPLIED_BIT_KHR; + } + + // Determine which present stages and presentAt method to use. + // We interpret the "display" present stage as the closest possible to the FIRST_PIXEL_VISIBLE stage. + // Ignore VK_PRESENT_STAGE_QUEUE_OPERATIONS_END_BIT_EXT as it might be too far from display to be useful. + can_present_at_absolute_time = present_timing_capabilities.presentAtAbsoluteTimeSupported; + display_present_stage = 0; + + if (present_timing_capabilities.presentStageQueries & VK_PRESENT_STAGE_IMAGE_FIRST_PIXEL_VISIBLE_BIT_EXT) + { + display_present_stage = VK_PRESENT_STAGE_IMAGE_FIRST_PIXEL_VISIBLE_BIT_EXT; + } + else if (present_timing_capabilities.presentStageQueries & VK_PRESENT_STAGE_IMAGE_FIRST_PIXEL_OUT_BIT_EXT) + { + display_present_stage = VK_PRESENT_STAGE_IMAGE_FIRST_PIXEL_OUT_BIT_EXT; + } + else if (present_timing_capabilities.presentStageQueries & VK_PRESENT_STAGE_REQUEST_DEQUEUED_BIT_EXT) + { + display_present_stage = VK_PRESENT_STAGE_REQUEST_DEQUEUED_BIT_EXT; + } + + // We'll calculate the animation time based on the display_present_stage's time measurements, so + // don't try to use present timing if we don't have one. + if (display_present_stage && + (present_timing_capabilities.presentAtRelativeTimeSupported || + present_timing_capabilities.presentAtAbsoluteTimeSupported)) + { + can_use_present_timing = true; + } + else + { + can_use_present_timing = false; + } +} + +/** + * @brief Selects the swapchain-local time domain used for present timing. + */ +void SwapchainPresentTiming::select_swapchain_time_domain() +{ + VkSwapchainTimeDomainPropertiesEXT time_domain_properties{VK_STRUCTURE_TYPE_SWAPCHAIN_TIME_DOMAIN_PROPERTIES_EXT}; + time_domain_properties.pTimeDomains = nullptr; + + VK_CHECK(vkGetSwapchainTimeDomainPropertiesEXT(get_device_handle(), swapchain, &time_domain_properties, &time_domains_counter)); + + std::vector domains(time_domain_properties.timeDomainCount); + std::vector domain_ids(time_domain_properties.timeDomainCount); + + time_domain_properties.pTimeDomains = domains.data(); + time_domain_properties.pTimeDomainIds = domain_ids.data(); + VK_CHECK(vkGetSwapchainTimeDomainPropertiesEXT(get_device_handle(), swapchain, &time_domain_properties, &time_domains_counter)); + + // Associate a VkTimeDomainKHR with its time domain ID returned by the swapchain. + auto find_time_domain = [time_domain_properties](VkTimeDomainKHR time_domain) -> SwapchainTimeDomain { + for (uint32_t i = 0; i < time_domain_properties.timeDomainCount; i++) + { + if (time_domain_properties.pTimeDomains[i] == time_domain) + { + return SwapchainTimeDomain{time_domain_properties.pTimeDomains[i], time_domain_properties.pTimeDomainIds[i]}; + } + } + return SwapchainTimeDomain{VK_TIME_DOMAIN_MAX_ENUM_KHR, 0}; + }; + + // Any swapchain-local time domain will do. + time_domain = find_time_domain(VK_TIME_DOMAIN_SWAPCHAIN_LOCAL_EXT); + if (time_domain.time_domain == VK_TIME_DOMAIN_MAX_ENUM_KHR) + { + time_domain = find_time_domain(VK_TIME_DOMAIN_PRESENT_STAGE_LOCAL_EXT); + } +} + +void SwapchainPresentTiming::query_swapchain_timing_properties() +{ + timing_properties.sType = VK_STRUCTURE_TYPE_SWAPCHAIN_TIMING_PROPERTIES_EXT; + timing_properties.pNext = nullptr; + VkResult result = vkGetSwapchainTimingPropertiesEXT(get_device_handle(), swapchain, &timing_properties, &timing_properties_counter); + + if (result != VK_SUCCESS) + { + timing_properties.refreshDuration = 0; + timing_properties.refreshInterval = 0; + } + + LOGI("New timing properties: refreshDuration={} refreshInterval={}", + timing_properties.refreshDuration, timing_properties.refreshInterval); +} + +/** + * @brief Sets the target present duration from timing properties (or default 60 Hz). + */ +void SwapchainPresentTiming::select_target_present_duration() +{ + static constexpr uint64_t default_refresh_duration_ns = 16'666'666; + + if (timing_properties.refreshDuration > 0) + { + // Match the presentation engine's refresh rate. + target_present_duration = timing_properties.refreshDuration; + } + else + { + // Default to 60Hz if timing_properties are not available yet. + target_present_duration = default_refresh_duration_ns; + } + + // Here, timing_history could be analyzed to detect latency, skipped frames, + // micro-stutters, or even pick a new present duration that is optimal + // for the current workload. +} + +/** + * @brief Invalidates the current timing statistics after recreating a swapchain or changing time domain. + */ +void SwapchainPresentTiming::invalidate_timing_history() +{ + timing_history_size = 0; + display_time_present_id = 0; + display_time_ns = 0; + display_time_ns_base = 0; +} + +/** + * @brief Retrieves past presentation timing from the driver and updates timing history. + */ +void SwapchainPresentTiming::query_past_presentation_timing() +{ + VkPastPresentationTimingPropertiesEXT past_presentation_properties{VK_STRUCTURE_TYPE_PAST_PRESENTATION_TIMING_PROPERTIES_EXT}; + VkPastPresentationTimingInfoEXT past_presentation_info{VK_STRUCTURE_TYPE_PAST_PRESENTATION_TIMING_INFO_EXT}; + + past_presentation_info.swapchain = swapchain; + + past_presentation_properties.pPresentationTimings = past_presentation_timings.data(); + past_presentation_properties.presentationTimingCount = past_presentation_timings.size(); + + VK_CHECK(vkGetPastPresentationTimingEXT(get_device_handle(), &past_presentation_info, &past_presentation_properties)); + + for (uint32_t i = 0; i < past_presentation_properties.presentationTimingCount; i++) + { + VkPastPresentationTimingEXT &timing = past_presentation_timings[i]; + size_t index = timing_history_size % history_buffer_size; + + timing_history[index].present_id = timing.presentId; + timing_history[index].target_time = timing.targetTime; + + for (uint32_t j = 0; j < timing.presentStageCount; j++) + { + if (timing.pPresentStages[j].stage == VK_PRESENT_STAGE_QUEUE_OPERATIONS_END_BIT_EXT) + { + timing_history[index].present_ready_time = timing.pPresentStages[j].time; + } + if (timing.pPresentStages[j].stage == VK_PRESENT_STAGE_REQUEST_DEQUEUED_BIT_EXT) + { + timing_history[index].present_dequeued_time = timing.pPresentStages[j].time; + } + if (timing.pPresentStages[j].stage == display_present_stage) + { + timing_history[index].present_display_time = timing.pPresentStages[j].time; + } + } + + timing_history_size++; + + // Save the last known display time and present id, which we'll use as a + // reference to predict the next presentation time. + // + // Since we did not allow out of order or incomplete results, these are + // guaranteed to be in increasing order. + if (timing_history[index].present_display_time) + { + display_time_present_id = timing.presentId; + display_time_ns = timing_history[index].present_display_time; + + // We save the first display time as a base to increase the + // floating point accuracy when converting the time to + // seconds for the animation. + if (display_time_ns_base == 0) + { + display_time_ns_base = display_time_ns; + } + } + + // Reset presentStageCount for the next vkGetPastPresentationTimingEXT call. + timing.presentStageCount = max_present_stages; + } + + if (past_presentation_properties.timingPropertiesCounter != timing_properties_counter) + { + query_swapchain_timing_properties(); + select_target_present_duration(); + timing_properties_counter = past_presentation_properties.timingPropertiesCounter; + } + + if (past_presentation_properties.timeDomainsCounter != time_domains_counter) + { + const uint64_t current_time_domain_id = time_domain.time_domain_id; + + select_swapchain_time_domain(); + + // If we changed time domain, invalidate our present timing history. + if (current_time_domain_id != time_domain.time_domain_id) + { + invalidate_timing_history(); + } + + time_domains_counter = past_presentation_properties.timeDomainsCounter; + } +} + +/** + * @brief Creates the graphics pipeline which is a basic vertex shader passthrough and a frament shader drawing a circle. + */ +void SwapchainPresentTiming::create_pipeline() +{ + auto vert_module = vkb::load_shader("swapchain_present_timing/" + get_shader_folder() + "/fullscreen_triangle.vert.spv", get_device_handle(), VK_SHADER_STAGE_VERTEX_BIT); + auto frag_module = vkb::load_shader("swapchain_present_timing/" + get_shader_folder() + "/circle.frag.spv", get_device_handle(), VK_SHADER_STAGE_FRAGMENT_BIT); + + VkPushConstantRange push_constant_range{}; + push_constant_range.stageFlags = VK_SHADER_STAGE_FRAGMENT_BIT; + push_constant_range.offset = 0; + push_constant_range.size = sizeof(PushConstants); + + VkPipelineLayoutCreateInfo layout_info{VK_STRUCTURE_TYPE_PIPELINE_LAYOUT_CREATE_INFO}; + layout_info.pushConstantRangeCount = 1; + layout_info.pPushConstantRanges = &push_constant_range; + + VK_CHECK(vkCreatePipelineLayout(get_device_handle(), &layout_info, nullptr, &pipeline_layout)); + + VkPipelineShaderStageCreateInfo stages[2] = {}; + + stages[0].sType = VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFO; + stages[0].stage = VK_SHADER_STAGE_VERTEX_BIT; + stages[0].module = vert_module; + stages[0].pName = "main"; + + stages[1].sType = VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFO; + stages[1].stage = VK_SHADER_STAGE_FRAGMENT_BIT; + stages[1].module = frag_module; + stages[1].pName = "main"; + + VkPipelineVertexInputStateCreateInfo vertex_input_state{VK_STRUCTURE_TYPE_PIPELINE_VERTEX_INPUT_STATE_CREATE_INFO}; + + VkPipelineInputAssemblyStateCreateInfo input_assembly_state{VK_STRUCTURE_TYPE_PIPELINE_INPUT_ASSEMBLY_STATE_CREATE_INFO}; + input_assembly_state.topology = VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST; + + VkPipelineViewportStateCreateInfo viewport_state{VK_STRUCTURE_TYPE_PIPELINE_VIEWPORT_STATE_CREATE_INFO}; + viewport_state.viewportCount = 1; + viewport_state.scissorCount = 1; + + VkPipelineRasterizationStateCreateInfo rasterization_state{VK_STRUCTURE_TYPE_PIPELINE_RASTERIZATION_STATE_CREATE_INFO}; + rasterization_state.lineWidth = 1.0f; + + VkPipelineMultisampleStateCreateInfo multisample_state{VK_STRUCTURE_TYPE_PIPELINE_MULTISAMPLE_STATE_CREATE_INFO}; + multisample_state.rasterizationSamples = VK_SAMPLE_COUNT_1_BIT; + + VkPipelineColorBlendAttachmentState blend_attachment{}; + blend_attachment.blendEnable = VK_TRUE; + blend_attachment.srcColorBlendFactor = VK_BLEND_FACTOR_SRC_ALPHA; + blend_attachment.dstColorBlendFactor = VK_BLEND_FACTOR_ONE_MINUS_SRC_ALPHA; + blend_attachment.colorBlendOp = VK_BLEND_OP_ADD; + blend_attachment.srcAlphaBlendFactor = VK_BLEND_FACTOR_ONE; + blend_attachment.dstAlphaBlendFactor = VK_BLEND_FACTOR_ZERO; + blend_attachment.alphaBlendOp = VK_BLEND_OP_ADD; + blend_attachment.colorWriteMask = VK_COLOR_COMPONENT_R_BIT | VK_COLOR_COMPONENT_G_BIT | VK_COLOR_COMPONENT_B_BIT | VK_COLOR_COMPONENT_A_BIT; + + VkPipelineColorBlendStateCreateInfo color_blend_state{VK_STRUCTURE_TYPE_PIPELINE_COLOR_BLEND_STATE_CREATE_INFO}; + color_blend_state.attachmentCount = 1; + color_blend_state.pAttachments = &blend_attachment; + + VkDynamicState dynamic_states[] = {VK_DYNAMIC_STATE_VIEWPORT, VK_DYNAMIC_STATE_SCISSOR}; + VkPipelineDynamicStateCreateInfo dynamic_state{VK_STRUCTURE_TYPE_PIPELINE_DYNAMIC_STATE_CREATE_INFO}; + dynamic_state.dynamicStateCount = 2; + dynamic_state.pDynamicStates = dynamic_states; + + VkPipelineRenderingCreateInfo rendering_info{VK_STRUCTURE_TYPE_PIPELINE_RENDERING_CREATE_INFO}; + rendering_info.colorAttachmentCount = 1; + rendering_info.pColorAttachmentFormats = &surface_format.format; + + VkGraphicsPipelineCreateInfo pipeline_info{VK_STRUCTURE_TYPE_GRAPHICS_PIPELINE_CREATE_INFO}; + pipeline_info.pNext = &rendering_info; + pipeline_info.stageCount = 2; + pipeline_info.pStages = stages; + pipeline_info.pVertexInputState = &vertex_input_state; + pipeline_info.pInputAssemblyState = &input_assembly_state; + pipeline_info.pViewportState = &viewport_state; + pipeline_info.pRasterizationState = &rasterization_state; + pipeline_info.pMultisampleState = &multisample_state; + pipeline_info.pColorBlendState = &color_blend_state; + pipeline_info.pDynamicState = &dynamic_state; + pipeline_info.layout = pipeline_layout; + + VK_CHECK(vkCreateGraphicsPipelines(get_device_handle(), VK_NULL_HANDLE, 1, &pipeline_info, nullptr, &pipeline)); + + vkDestroyShaderModule(get_device_handle(), vert_module, nullptr); + vkDestroyShaderModule(get_device_handle(), frag_module, nullptr); +} + +/** + * @brief Allocates per-frame resources (fence, acquire semaphore, command pool, command buffer). + * + * @param frame The PerFrame structure to initialize. + */ +void SwapchainPresentTiming::create_frame_resources(PerFrame &frame) +{ + VkFenceCreateInfo fence_info{VK_STRUCTURE_TYPE_FENCE_CREATE_INFO}; + fence_info.flags = VK_FENCE_CREATE_SIGNALED_BIT; + VK_CHECK(vkCreateFence(get_device_handle(), &fence_info, nullptr, &frame.render_fence)); + + VkSemaphoreCreateInfo sem_info{VK_STRUCTURE_TYPE_SEMAPHORE_CREATE_INFO}; + VK_CHECK(vkCreateSemaphore(get_device_handle(), &sem_info, nullptr, &frame.acquire_semaphore)); + + VkCommandPoolCreateInfo cmd_pool_info{VK_STRUCTURE_TYPE_COMMAND_POOL_CREATE_INFO}; + cmd_pool_info.flags = VK_COMMAND_POOL_CREATE_TRANSIENT_BIT; + cmd_pool_info.queueFamilyIndex = queue->get_family_index(); + VK_CHECK(vkCreateCommandPool(get_device_handle(), &cmd_pool_info, nullptr, &frame.command_pool)); + + VkCommandBufferAllocateInfo cmd_buf_info{VK_STRUCTURE_TYPE_COMMAND_BUFFER_ALLOCATE_INFO}; + cmd_buf_info.commandPool = frame.command_pool; + cmd_buf_info.level = VK_COMMAND_BUFFER_LEVEL_PRIMARY; + cmd_buf_info.commandBufferCount = 1; + VK_CHECK(vkAllocateCommandBuffers(get_device_handle(), &cmd_buf_info, &frame.command_buffer)); +} + +void SwapchainPresentTiming::destroy_frame_resources(PerFrame &frame) +{ + vkDestroyFence(get_device_handle(), frame.render_fence, nullptr); + vkDestroySemaphore(get_device_handle(), frame.acquire_semaphore, nullptr); + vkDestroyCommandPool(get_device_handle(), frame.command_pool, nullptr); +} + +void SwapchainPresentTiming::init_swapchain() +{ + VkSwapchainKHR old_swapchain = swapchain; + + VkSwapchainCreateInfoKHR info{VK_STRUCTURE_TYPE_SWAPCHAIN_CREATE_INFO_KHR}; + info.flags = VK_SWAPCHAIN_CREATE_PRESENT_TIMING_BIT_EXT | VK_SWAPCHAIN_CREATE_PRESENT_ID_2_BIT_KHR; + info.surface = get_surface(); + info.minImageCount = desired_swapchain_images; + info.imageFormat = surface_format.format; + info.imageColorSpace = surface_format.colorSpace; + info.imageExtent = swapchain_extents; + info.imageArrayLayers = 1; + info.imageUsage = VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT; + info.imageSharingMode = VK_SHARING_MODE_EXCLUSIVE; + info.preTransform = VK_SURFACE_TRANSFORM_IDENTITY_BIT_KHR; + info.compositeAlpha = composite; + info.presentMode = present_mode; + info.clipped = true; + info.oldSwapchain = old_swapchain; + + LOGI("Creating new swapchain"); + VK_CHECK(vkCreateSwapchainKHR(get_device_handle(), &info, nullptr, &swapchain)); + + if (old_swapchain != VK_NULL_HANDLE) + { + get_device().wait_idle(); + vkDestroySwapchainKHR(get_device_handle(), old_swapchain, nullptr); + } + + uint32_t image_count = 0; + VK_CHECK(vkGetSwapchainImagesKHR(get_device_handle(), swapchain, &image_count, nullptr)); + + std::vector images(image_count); + VK_CHECK(vkGetSwapchainImagesKHR(get_device_handle(), swapchain, &image_count, images.data())); + + for (auto &img : swapchain_images) + { + if (img.image_view != VK_NULL_HANDLE) + { + vkDestroyImageView(get_device_handle(), img.image_view, nullptr); + } + if (img.render_semaphore != VK_NULL_HANDLE) + { + vkDestroySemaphore(get_device_handle(), img.render_semaphore, nullptr); + } + } + + swapchain_images.clear(); + swapchain_images.resize(image_count); + + for (uint32_t index = 0; index < image_count; ++index) + { + init_swapchain_image(swapchain_images[index], images[index]); + } + + // Initialize present timing utilities. + // + // First, query the swapchain time domain properties to select an appropriate time domain. + select_swapchain_time_domain(); + + // Query the swapchain timing properties to get the refresh cycle duration. Note + // this value may not be available on all platforms yet before presenting a few times. + query_swapchain_timing_properties(); + + // Clear the frame timing history in case we are reusing a swapchain. + invalidate_timing_history(); + + // Finally, compute a target present duration based on the current timing properties. + select_target_present_duration(); + + // Set a queue size for the swapchain to hold the present timing data. We'll poll results + // every frame, but give it a decent buffer for results to be consistently available. + VK_CHECK(vkSetSwapchainPresentTimingQueueSizeEXT(get_device_handle(), swapchain, history_buffer_size)); +} + +void SwapchainPresentTiming::init_swapchain_image(PerSwapchainImage &perImage, VkImage image) +{ + perImage.image = image; + + VkImageViewCreateInfo view_info{VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO}; + + view_info.viewType = VK_IMAGE_VIEW_TYPE_2D; + view_info.format = surface_format.format; + view_info.image = image; + view_info.subresourceRange.levelCount = 1; + view_info.subresourceRange.layerCount = 1; + view_info.subresourceRange.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT; + view_info.components.r = VK_COMPONENT_SWIZZLE_R; + view_info.components.g = VK_COMPONENT_SWIZZLE_G; + view_info.components.b = VK_COMPONENT_SWIZZLE_B; + view_info.components.a = VK_COMPONENT_SWIZZLE_A; + + VK_CHECK(vkCreateImageView(get_device_handle(), &view_info, nullptr, &perImage.image_view)); + + VkSemaphoreCreateInfo sem_info{VK_STRUCTURE_TYPE_SEMAPHORE_CREATE_INFO}; + VK_CHECK(vkCreateSemaphore(get_device_handle(), &sem_info, nullptr, &perImage.render_semaphore)); +} + +bool SwapchainPresentTiming::recreate_swapchain() +{ + VkSurfaceCapabilitiesKHR surface_properties; + VK_CHECK(vkGetPhysicalDeviceSurfaceCapabilitiesKHR(get_gpu_handle(), get_surface(), &surface_properties)); + + // Only rebuild the swapchain if the dimensions have changed + if (surface_properties.currentExtent.width == swapchain_extents.width && + surface_properties.currentExtent.height == swapchain_extents.height) + { + return false; + } + + if (surface_properties.currentExtent.width == 0xFFFFFFFF) + { + swapchain_extents = VkExtent2D{400, 300}; + } + else + { + swapchain_extents = surface_properties.currentExtent; + } + + init_swapchain(); + return true; +} + +void SwapchainPresentTiming::setup_frame() +{ + frame_index = (frame_index + 1) % frame_resources.size(); + PerFrame &frame = frame_resources[frame_index]; + + // Wait for frame N-2 to finish before starting recording of frame N so we can reuse resources. + vkWaitForFences(get_device_handle(), 1, &frame.render_fence, true, UINT64_MAX); + vkResetFences(get_device_handle(), 1, &frame.render_fence); + + vkResetCommandPool(get_device_handle(), frame.command_pool, 0); + + // Collect presentation timings to update the display time + query_past_presentation_timing(); +} + +/** + * @brief Records and submits rendering commands for the given swapchain image. + * + * @param index Swapchain image index to render into. + * @param delta_time Elapsed time since last frame (used when present timing is disabled). + * + * @return Target present time to pass to present_image (0 if present timing disabled). + */ +uint64_t SwapchainPresentTiming::render(uint32_t index, float delta_time) +{ + uint64_t present_time = 0; + float animation_time = 0.f; + PerFrame &frame = frame_resources[frame_index]; + + // Compute the animation time by adding the appropriate multiple of our target + // present duration to the last known display time. + // + // `present_time` will be returned and used to set the present timing info. + if (can_use_present_timing) + { + const uint64_t id_delta = present_id - display_time_present_id; + const uint64_t time_offset = target_present_duration * id_delta; + + // Subtract display_time_ns_base for floating-point accuracy only; do not use + // this base-adjusted value as an absolute targetTime for the presentation engine. + animation_time = (float) ((display_time_ns - display_time_ns_base + time_offset) / (uint64_t) 1000000) / 1000.f; + + if (can_present_at_absolute_time) + { + present_time = display_time_ns ? display_time_ns + time_offset : 0; + } + else + { + present_time = target_present_duration; + } + } + else + { + animation_time = cpu_time; + } + + VkCommandBufferBeginInfo begin_info{VK_STRUCTURE_TYPE_COMMAND_BUFFER_BEGIN_INFO}; + begin_info.flags = VK_COMMAND_BUFFER_USAGE_ONE_TIME_SUBMIT_BIT; + VK_CHECK(vkBeginCommandBuffer(frame.command_buffer, &begin_info)); + + VkImageMemoryBarrier barrier{VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER}; + barrier.srcAccessMask = 0; + barrier.dstAccessMask = VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT; + barrier.oldLayout = VK_IMAGE_LAYOUT_UNDEFINED; + barrier.newLayout = VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL; + barrier.srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED; + barrier.dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED; + barrier.image = swapchain_images[index].image; + barrier.subresourceRange.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT; + barrier.subresourceRange.baseMipLevel = 0; + barrier.subresourceRange.levelCount = 1; + barrier.subresourceRange.baseArrayLayer = 0; + barrier.subresourceRange.layerCount = 1; + + vkCmdPipelineBarrier(frame.command_buffer, VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT, VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT, 0, 0, nullptr, 0, nullptr, 1, &barrier); + + VkClearValue clear_values[1] = {}; + clear_values[0].color = {{0.01f, 0.01f, 0.01f, 1.0f}}; + + VkRenderingAttachmentInfo attachment_info{VK_STRUCTURE_TYPE_RENDERING_ATTACHMENT_INFO}; + attachment_info.imageView = swapchain_images[index].image_view; + attachment_info.imageLayout = VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL; + attachment_info.loadOp = VK_ATTACHMENT_LOAD_OP_CLEAR; + attachment_info.storeOp = VK_ATTACHMENT_STORE_OP_STORE; + attachment_info.clearValue = clear_values[0]; + + VkRenderingInfo rendering_info{VK_STRUCTURE_TYPE_RENDERING_INFO}; + rendering_info.renderArea.extent = swapchain_extents; + rendering_info.layerCount = 1; + rendering_info.colorAttachmentCount = 1; + rendering_info.pColorAttachments = &attachment_info; + + vkCmdBeginRendering(frame.command_buffer, &rendering_info); + vkCmdBindPipeline(frame.command_buffer, VK_PIPELINE_BIND_POINT_GRAPHICS, pipeline); + + VkViewport viewport{}; + viewport.x = 0.0f; + viewport.y = 0.0f; + viewport.width = (float) swapchain_extents.width; + viewport.height = (float) swapchain_extents.height; + viewport.minDepth = 0.0f; + viewport.maxDepth = 1.0f; + + VkRect2D scissor{}; + scissor.offset = {0, 0}; + scissor.extent = swapchain_extents; + + vkCmdSetViewport(frame.command_buffer, 0, 1, &viewport); + vkCmdSetScissor(frame.command_buffer, 0, 1, &scissor); + + // Linearly interpolate horizontal position over 1 second, so any stutter should + // be easily perceptible + const float pos_x = (animation_time - floorf(animation_time)) * 3.0f - 1.5f; + + PushConstants push_constants; + push_constants.resolution = {(float) swapchain_extents.width, (float) swapchain_extents.height}; + push_constants.position = {pos_x, 0.f, 0.0f}; + push_constants.color = can_use_present_timing ? glm::vec3{0.2f, 0.7f, 0.3f} : glm::vec3{0.6f, 0.4f, 0.2f}; + vkCmdPushConstants(frame.command_buffer, pipeline_layout, VK_SHADER_STAGE_FRAGMENT_BIT, 0, sizeof(PushConstants), &push_constants); + vkCmdDraw(frame.command_buffer, 3, 1, 0, 0); + + vkCmdEndRendering(frame.command_buffer); + + barrier.srcAccessMask = VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT; + barrier.dstAccessMask = 0; + barrier.oldLayout = VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL; + barrier.newLayout = VK_IMAGE_LAYOUT_PRESENT_SRC_KHR; + + vkCmdPipelineBarrier(frame.command_buffer, VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT, VK_PIPELINE_STAGE_BOTTOM_OF_PIPE_BIT, 0, 0, nullptr, 0, nullptr, 1, &barrier); + + VK_CHECK(vkEndCommandBuffer(frame.command_buffer)); + + VkPipelineStageFlags wait_stage{VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT}; + + VkSubmitInfo info{VK_STRUCTURE_TYPE_SUBMIT_INFO}; + info.commandBufferCount = 1; + info.pCommandBuffers = &frame.command_buffer; + info.waitSemaphoreCount = 1; + info.pWaitSemaphores = &frame.acquire_semaphore; + info.pWaitDstStageMask = &wait_stage; + info.signalSemaphoreCount = 1; + info.pSignalSemaphores = &swapchain_images[index].render_semaphore; + + VK_CHECK(vkQueueSubmit(queue->get_handle(), 1, &info, frame.render_fence)); + + return present_time; +} + +static VkResult ignore_suboptimal_due_to_rotation(VkResult result) +{ + // Because preTransform is not respected in this sample, VK_SUBOPTIMAL_KHR is returned if + // the device is rotated. Handling preTransform optimally is out of scope for this sample, + // so VK_SUBOPTIMAL_KHR is ignored in that case. + // + // Note that on Android VK_SUBOPTIMAL_KHR is only returned when there is a mismatch between + // the device rotation and the specified preTransform. +#if defined(ANDROID) + if (result == VK_SUBOPTIMAL_KHR) + { + result = VK_SUCCESS; + } +#endif + return result; +} + +VkResult SwapchainPresentTiming::acquire_next_image(uint32_t *index) +{ + PerFrame &frame = frame_resources[frame_index]; + VkResult result = vkAcquireNextImageKHR(get_device_handle(), swapchain, UINT64_MAX, frame.acquire_semaphore, VK_NULL_HANDLE, index); + + if (result != VK_SUCCESS && result != VK_SUBOPTIMAL_KHR) + { + return result; + } + + return ignore_suboptimal_due_to_rotation(result); +} + +/** + * @brief Fills VkPresentTimingInfoEXT with target time, time domain, and present stage queries. + * + * @param timing_info Timing info structure that gets pNext-chained for vkQueuePresentKHR + * @param present_time Target present time or duration, depending on device/surface support. + */ +void SwapchainPresentTiming::set_present_timing_info(VkPresentTimingInfoEXT &timing_info, uint64_t present_time) +{ + timing_info.targetTime = present_time; + timing_info.flags = VK_PRESENT_TIMING_INFO_PRESENT_AT_NEAREST_REFRESH_CYCLE_BIT_EXT; + timing_info.timeDomainId = time_domain.time_domain_id; + timing_info.presentStageQueries = display_present_stage | VK_PRESENT_STAGE_QUEUE_OPERATIONS_END_BIT_EXT | VK_PRESENT_STAGE_REQUEST_DEQUEUED_BIT_EXT; + timing_info.targetTimeDomainPresentStage = display_present_stage; + + if (!can_present_at_absolute_time) + { + timing_info.flags |= VK_PRESENT_TIMING_INFO_PRESENT_AT_RELATIVE_TIME_BIT_EXT; + } +} + +/** + * @brief Presents an image to the swapchain. + * + * @param index The swapchain image index previously obtained from @ref acquire_next_image. + * + * @returns Vulkan result of vkQueuePresentKHR, ignoring VK_SUBOPTIMAL_KHR + */ +VkResult SwapchainPresentTiming::present_image(uint32_t index, uint64_t present_time) +{ + VkPresentInfoKHR present{VK_STRUCTURE_TYPE_PRESENT_INFO_KHR}; + present.swapchainCount = 1; + present.pSwapchains = &swapchain; + present.pImageIndices = &index; + present.waitSemaphoreCount = 1; + present.pWaitSemaphores = &swapchain_images[index].render_semaphore; + + VkPresentId2KHR present_id_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}; + + present_id_info.swapchainCount = 1; + present_id_info.pPresentIds = &present_id; + present.pNext = &present_id_info; + + if (can_use_present_timing) + { + set_present_timing_info(timing_info, present_time); + timings_info.swapchainCount = 1; + timings_info.pTimingInfos = &timing_info; + present_id_info.pNext = &timings_info; + } + + VkResult result = vkQueuePresentKHR(queue->get_handle(), &present); + if (result == VK_ERROR_PRESENT_TIMING_QUEUE_FULL_EXT) + { + LOGI("Present timing queue is full, presenting {} without requesting timing information", present_id); + timing_info.presentStageQueries = 0; + result = vkQueuePresentKHR(queue->get_handle(), &present); + } + + present_id++; + + return ignore_suboptimal_due_to_rotation(result); +} + +VkPhysicalDevice SwapchainPresentTiming::get_gpu_handle() +{ + return get_device().get_gpu().get_handle(); +} + +VkDevice SwapchainPresentTiming::get_device_handle() +{ + if (!has_device()) + { + return VK_NULL_HANDLE; + } + return get_device().get_handle(); +} + +uint32_t SwapchainPresentTiming::get_api_version() const +{ + return VK_API_VERSION_1_3; +} + +void SwapchainPresentTiming::request_instance_extensions(std::unordered_map &requested_extensions) const +{ + vkb::VulkanSampleC::request_instance_extensions(requested_extensions); + requested_extensions[VK_KHR_GET_SURFACE_CAPABILITIES_2_EXTENSION_NAME] = vkb::RequestMode::Required; +} + +SwapchainPresentTiming::SwapchainPresentTiming() +{ + // VK_EXT_present_timing dependencies + add_device_extension(VK_KHR_PRESENT_ID_2_EXTENSION_NAME); + add_device_extension(VK_EXT_PRESENT_TIMING_EXTENSION_NAME); + add_device_extension(VK_KHR_CALIBRATED_TIMESTAMPS_EXTENSION_NAME); + + for (auto &timing : past_presentation_timings) + { + timing.sType = VK_STRUCTURE_TYPE_PAST_PRESENTATION_TIMING_EXT; + timing.pNext = nullptr; + timing.pPresentStages = new VkPresentStageTimeEXT[max_present_stages]; + timing.presentStageCount = max_present_stages; + } +} + +SwapchainPresentTiming::~SwapchainPresentTiming() +{ + if (get_device_handle() == VK_NULL_HANDLE) + { + return; + } + + // Wait for device to be idle and clean up everything. + vkDeviceWaitIdle(get_device_handle()); + + for (auto &timing : past_presentation_timings) + { + delete[] timing.pPresentStages; + } + + for (PerFrame &frame : frame_resources) + { + destroy_frame_resources(frame); + } + + if (pipeline != VK_NULL_HANDLE) + { + vkDestroyPipeline(get_device_handle(), pipeline, nullptr); + } + + if (pipeline_layout != VK_NULL_HANDLE) + { + vkDestroyPipelineLayout(get_device_handle(), pipeline_layout, nullptr); + } + + for (auto &img : swapchain_images) + { + if (img.image_view != VK_NULL_HANDLE) + { + vkDestroyImageView(get_device_handle(), img.image_view, nullptr); + } + if (img.render_semaphore != VK_NULL_HANDLE) + { + vkDestroySemaphore(get_device_handle(), img.render_semaphore, nullptr); + } + } + + if (swapchain != VK_NULL_HANDLE) + { + vkDestroySwapchainKHR(get_device_handle(), swapchain, nullptr); + } +} + +void SwapchainPresentTiming::request_gpu_features(vkb::core::PhysicalDeviceC &gpu) +{ + REQUEST_REQUIRED_FEATURE(gpu, VkPhysicalDevicePresentTimingFeaturesEXT, presentTiming); + REQUEST_REQUIRED_FEATURE(gpu, VkPhysicalDevicePresentId2FeaturesKHR, presentId2); + + // Not technically required but makes the reduces the amount of boilerplate + REQUEST_REQUIRED_FEATURE(gpu, VkPhysicalDeviceDynamicRenderingFeatures, dynamicRendering); + + bool presentAtAbsoluteTime = REQUEST_OPTIONAL_FEATURE(gpu, VkPhysicalDevicePresentTimingFeaturesEXT, presentAtAbsoluteTime); + bool presentAtRelativeTime = REQUEST_OPTIONAL_FEATURE(gpu, VkPhysicalDevicePresentTimingFeaturesEXT, presentAtRelativeTime); + + if (!presentAtAbsoluteTime && !presentAtRelativeTime) + { + throw std::runtime_error("Requested required feature is not supported"); + } +} + +void SwapchainPresentTiming::create_render_context() +{ + get_queue(); + select_surface_format(); + query_surface_capabilities(); + create_pipeline(); + init_swapchain(); +} + +/** + * @brief Prepares per-frame resources for all frames in the ring. + */ +void SwapchainPresentTiming::prepare_render_context() +{ + for (PerFrame &frame : frame_resources) + { + create_frame_resources(frame); + } +} + +/** + * @brief Main update loop: acquire image, setup frame, render, present; handles swapchain recreation on out-of-date. + * @param delta_time Time since last frame. + */ +void SwapchainPresentTiming::update(float delta_time) +{ + uint64_t next_present_time = 0; + + fps_timer += delta_time; + cpu_time += delta_time; + + if (fps_timer > 1.0f) + { + LOGI("FPS: {} - target {}", static_cast(present_id - fps_last_logged_frame_number) / fps_timer, + target_present_duration ? 1'000'000'000 / target_present_duration : 0); + fps_timer -= 1.0f; + fps_last_logged_frame_number = present_id; + } + + setup_frame(); + + uint32_t index; + VkResult res = acquire_next_image(&index); + + if (res == VK_SUBOPTIMAL_KHR || res == VK_ERROR_OUT_OF_DATE_KHR) + { + recreate_swapchain(); + res = acquire_next_image(&index); + } + if (res != VK_SUBOPTIMAL_KHR) + { + VK_CHECK(res); + } + + next_present_time = render(index, delta_time); + + res = present_image(index, next_present_time); + + if (res == VK_SUBOPTIMAL_KHR || res == VK_ERROR_OUT_OF_DATE_KHR) + { + recreate_swapchain(); + } + else + { + VK_CHECK(res); + } +} + +bool SwapchainPresentTiming::resize(const uint32_t, const uint32_t) +{ + if (get_device_handle() == VK_NULL_HANDLE) + { + return false; + } + + return recreate_swapchain(); +} + +void SwapchainPresentTiming::input_event(const vkb::InputEvent &input_event) +{ + if (input_event.get_source() != vkb::EventSource::Keyboard) + { + return; + } + + const auto &key_button = static_cast(input_event); + if (key_button.get_action() != vkb::KeyAction::Up) + { + return; + } + + switch (key_button.get_code()) + { + case vkb::KeyCode::P: + can_use_present_timing = !can_use_present_timing; + LOGI("can_use_present_timing: {}", can_use_present_timing); + break; + default: + break; + } +} + +std::unique_ptr create_swapchain_present_timing() +{ + return std::make_unique(); +} diff --git a/samples/api/swapchain_present_timing/swapchain_present_timing.h b/samples/api/swapchain_present_timing/swapchain_present_timing.h new file mode 100644 index 0000000000..0ad20b8e45 --- /dev/null +++ b/samples/api/swapchain_present_timing/swapchain_present_timing.h @@ -0,0 +1,183 @@ +/* Copyright (c) 2026, NVIDIA CORPORATION + * + * SPDX-License-Identifier: Apache-2.0 + * + * Licensed under the Apache License, Version 2.0 the "License"; + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#pragma once + +#include "common/vk_common.h" +#include "platform/application.h" +#include "vulkan_sample.h" + +/** + * @brief A sample that implements frame pacing using time-based presentation. + */ +class SwapchainPresentTiming : public vkb::VulkanSampleC +{ + /** + * @brief Size of the frame data history. Only used as an example here. + */ + static constexpr size_t history_buffer_size = 64; + + /** + * @brief Maximum number of present stages we ever request. + */ + static constexpr size_t max_present_stages = 4; + + /** + * @brief A swapchain time domain is a combination of a VkTimeDomainKHR enum and an id which + * is used to differentiate between multiple swapchain-local opaque time domains. + */ + struct SwapchainTimeDomain + { + VkTimeDomainKHR time_domain = VK_TIME_DOMAIN_MAX_ENUM_KHR; + uint64_t time_domain_id = 0; + }; + + /** + * @brief Timing results for a given present + */ + struct FrameTimingData + { + uint64_t present_id = 0; + uint64_t target_time = 0; + uint64_t present_ready_time = 0; + uint64_t present_dequeued_time = 0; + uint64_t present_display_time = 0; + }; + + /** + * @brief Per-frame render data. + */ + struct PerFrame + { + VkFence render_fence = VK_NULL_HANDLE; + VkSemaphore acquire_semaphore = VK_NULL_HANDLE; + VkCommandPool command_pool = VK_NULL_HANDLE; + VkCommandBuffer command_buffer = VK_NULL_HANDLE; + }; + + /** + * @brief Per swapchain image resources. + */ + struct PerSwapchainImage + { + VkImage image = VK_NULL_HANDLE; + VkImageView image_view = VK_NULL_HANDLE; + VkSemaphore render_semaphore = VK_NULL_HANDLE; + }; + + /** + * @brief Simple push constants to draw an animated shape in a fragment shader. + */ + struct PushConstants + { + glm::vec2 resolution; + alignas(16) glm::vec3 position; + alignas(16) glm::vec3 color; + }; + + public: + SwapchainPresentTiming(); + ~SwapchainPresentTiming() override; + + void create_render_context() override; + void prepare_render_context() override; + void update(float delta_time) override; + bool resize(uint32_t width, uint32_t height) override; + void input_event(const vkb::InputEvent &input_event) override; + + private: + /// Submission and present queue. + const vkb::Queue *queue = nullptr; + VkPipelineLayout pipeline_layout = VK_NULL_HANDLE; + VkPipeline pipeline = VK_NULL_HANDLE; + + /// Surface data. + VkSurfaceFormatKHR surface_format = {}; + VkExtent2D swapchain_extents = {}; + VkCompositeAlphaFlagBitsKHR composite = VK_COMPOSITE_ALPHA_OPAQUE_BIT_KHR; + uint32_t desired_swapchain_images = 0; + bool can_use_present_timing = false; + bool can_present_at_absolute_time = false; + VkPresentStageFlagsEXT display_present_stage = 0; + + /// The swapchain. + VkSwapchainKHR swapchain = VK_NULL_HANDLE; + VkPresentModeKHR present_mode = VK_PRESENT_MODE_FIFO_KHR; + std::vector swapchain_images; + + /// Present timing utilities + VkSwapchainTimingPropertiesEXT timing_properties = {}; + uint64_t timing_properties_counter = 0; + SwapchainTimeDomain time_domain = {}; + uint64_t time_domains_counter = 0; + + /// Present infos + uint64_t present_id = 1; + uint64_t target_present_duration = 0; + + /// Per frame resources + std::array frame_resources; + size_t frame_index = 0; + + /// Buffer used to query past presentation timing. + std::array past_presentation_timings; + + /// Frame timing history. This is not directly used and is meant as an example. + std::array timing_history; + size_t timing_history_size = 0; + + /// FPS log. + float fps_timer = 0; + uint32_t fps_last_logged_frame_number = 0; + + /// Time tracking + float cpu_time = 0.0f; + uint64_t display_time_ns_base = 0; + uint64_t display_time_ns = 0; + uint64_t display_time_present_id = 0; + + uint32_t get_api_version() const override; + void request_gpu_features(vkb::core::PhysicalDeviceC &gpu) override; + void request_instance_extensions(std::unordered_map &requested_extensions) const override; + + void select_surface_format(); + void query_surface_capabilities(); + void query_swapchain_timing_properties(); + void select_swapchain_time_domain(); + void select_target_present_duration(); + void invalidate_timing_history(); + void query_past_presentation_timing(); + void set_present_timing_info(VkPresentTimingInfoEXT &timing_info, uint64_t present_time); + + void create_pipeline(); + void create_frame_resources(PerFrame &frame); + void destroy_frame_resources(PerFrame &frame); + void init_swapchain(); + void init_swapchain_image(PerSwapchainImage &perImage, VkImage image); + bool recreate_swapchain(); + + void setup_frame(); + uint64_t render(uint32_t index, float delta_time); + VkResult acquire_next_image(uint32_t *index); + VkResult present_image(uint32_t index, uint64_t present_time); + + void get_queue(); + VkPhysicalDevice get_gpu_handle(); + VkDevice get_device_handle(); +}; + +std::unique_ptr create_swapchain_present_timing(); diff --git a/shaders/swapchain_present_timing/glsl/circle.frag b/shaders/swapchain_present_timing/glsl/circle.frag new file mode 100644 index 0000000000..64d1c8fc67 --- /dev/null +++ b/shaders/swapchain_present_timing/glsl/circle.frag @@ -0,0 +1,46 @@ +/* Copyright (c) 2026, NVIDIA CORPORATION + * + * SPDX-License-Identifier: Apache-2.0 + * + * Licensed under the Apache License, Version 2.0 the "License"; + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#version 450 + +layout (location = 0) out vec4 outColor; + +// Push constants for window resolution, camera, and time +layout(push_constant) uniform PushConstants { + vec2 resolution; + vec3 position; + vec3 color; +} pushConstants; + +void main() +{ + vec2 uv = (gl_FragCoord.xy - 0.5 * pushConstants.resolution) / pushConstants.resolution.y; + + // Circle parameters + vec2 pos = pushConstants.position.xy; + float radius = 0.2; + + // 2D SDF for circle + float dist = length(uv - pos) - radius; + + if (dist > 0.0) + { + discard; + } + + outColor = vec4(pushConstants.color, 1.0); +} diff --git a/shaders/swapchain_present_timing/glsl/circle.frag.spv b/shaders/swapchain_present_timing/glsl/circle.frag.spv new file mode 100644 index 0000000000000000000000000000000000000000..3e8f9288ff559a56123a599ae84227b8640b9944 GIT binary patch literal 1712 zcmZ9K$!-%t5QaOp#|ha(Hr6mn*h0VrNWu~l$5>zvmTbfWSeC~MBN>n7nTf;=i3=wl zh$Cls9uNYF|FgT1mA1-N_1E9kwM;g%Id06DnKCn`Y=&pSWP})D`GKxf-&PB85)^j# zc3{k!?0^Vo&WxL!H1fU~b_q;No=N7#1J?txvdU`AH(kn^NmHpdDz#dr`m)jL^%`;V zq0?@3!fv|}g#C6a2~XSbCPsK+x7A65QGzGN=)0cYIHxz#Q)4{UP8}zm#;bnwq#Q;4 zK<)rvkglikyK>ZxlV&%G<>&PS=k(ig)Jc;tQa28#*IpEdLloQ_@ad@)b)vpBZ!W?+ zlbgCWhWEwEnJwAfzR_{4nq6VhM^@kwdo?1eZr zk4eA}tWPgs+w$SklTMNQ)C$J?YqmFXgCpcTFU_9mo1FN}9c)?tlC&>zZ{ROWINV>6 zhD%=fmj}51bf^jciiG>{uS&zGF5={4hAw_xHgQ+qZQ0~@eDod3kMGqZ*YhL!oFz?i zSDJU8O4GNS6+Zg`bMGF=X7(=6k!)i=7WXwPaOTe8&MzUJG&5pPu4m_O2%}DXYTi2I z^IZ}@RGgY-Br(u>S@Rl-h~JNMyVkbr*?2mhkQ*#9SsU6M^+VkJ$G z+4Ft6T%=DU#)$Lv1V&I=Gwl15TzT`+#VFqB-$=+^A$jv^<%MNc!z+IoWEC$Y@ zPjWFoV%W@%9lKud%H}QS-;)jB^;wV&2Yg!H(9eAdxnD?#yFPbhlb^fr9!TJV^EP_| zbNq+0!M=*mx3wn$bqBzSj zCp$qh-GDjgq`N+@`+n4XuUn*<$Ri00Z^CPxW1N9U&&0zAV^7O^0XSP?fa9;qYs$8y zYhv+&#N=9vXJo<96~(h24}Hl~n;F2L#hCkm?M42=fAM9-)TiH?Ed7{|`qyMzQiSuw z*QL~3SA0VjKG=pl_LFLmLz!z`mOGMXF5>VXp3k)*453fjfx=fQzU{N$UZ(htk3;^9rNGx8uE-ml*zERd_1>2mW?j*#H0l literal 0 HcmV?d00001 diff --git a/shaders/swapchain_present_timing/hlsl/circle.frag.hlsl b/shaders/swapchain_present_timing/hlsl/circle.frag.hlsl new file mode 100644 index 0000000000..7384b2f042 --- /dev/null +++ b/shaders/swapchain_present_timing/hlsl/circle.frag.hlsl @@ -0,0 +1,49 @@ +/* Copyright (c) 2026, NVIDIA CORPORATION + * + * SPDX-License-Identifier: Apache-2.0 + * + * Licensed under the Apache License, Version 2.0 the "License"; + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +struct PSInput +{ + float4 position : SV_POSITION; +}; + +// Push constants for window resolution, camera, and time +struct PushConstants +{ + float2 resolution; + float3 position; + float3 color; +}; +[[vk::push_constant]] PushConstants pushConstants; + +float4 main(PSInput input) : SV_TARGET +{ + float2 uv = (input.position.xy - 0.5 * pushConstants.resolution) / pushConstants.resolution.y; + + // Circle parameters + float2 pos = pushConstants.position.xy; + float radius = 0.2; + + // 2D SDF for circle + float dist = length(uv - pos) - radius; + + if (dist > 0.0) + { + discard; + } + + return float4(pushConstants.color, 1.0); +} diff --git a/shaders/swapchain_present_timing/hlsl/circle.frag.spv b/shaders/swapchain_present_timing/hlsl/circle.frag.spv new file mode 100644 index 0000000000000000000000000000000000000000..739fba5ff13845e1c64db9e7a71ffaa989f42b45 GIT binary patch literal 1344 zcmY+D+fGwa5QZ0p76mLICy!XfGg?uBm>40|n0nE~D#TkultdC_lkV2UI}&fa@_~E? zeIAX`#P8c(%fL$h&dfiv=CEdWu(UWF!axY28rJyrOomdyjD|`fo2|`OJ@3A&udFWX zuZBTI;TsRbVF9HKW=yG zo3Gz&HC{EJZ56(uWAikb#+gRJIM8;!dEj{->#8sM!%3Vy%1s>M>`$%+n(K_9?zPHv>{3&ctim#$uJNcb2gu{(F^4qgH z^lMK>+qamHGaDl>YQ&sIddFRRXDZshm;Py_{mE<3AkC*eMWtEf0?Zsz-Iy~-b6QLL zY_#RH&qdoFrpa+0`3T-#D$%EHZEFnxZEM??+!`HQ&sjaz|Aln{`IspCy9kVZ!ItdB z{mTiDc^l-h-%G&S?#}!D-uwJlVzd`P4YD6ts_0ZnM>bYAm{t5{~@!wT$*7S-5LCTeGg~22jpMI+V>-e_|H9v ioV}*FmB_jKHoJA#tHArl>-T@LPOpA1`u@_>L+}sxJyLl9 literal 0 HcmV?d00001 diff --git a/shaders/swapchain_present_timing/hlsl/fullscreen_triangle.vert.hlsl b/shaders/swapchain_present_timing/hlsl/fullscreen_triangle.vert.hlsl new file mode 100644 index 0000000000..1a3577d1bd --- /dev/null +++ b/shaders/swapchain_present_timing/hlsl/fullscreen_triangle.vert.hlsl @@ -0,0 +1,34 @@ +/* Copyright (c) 2026, NVIDIA CORPORATION + * + * SPDX-License-Identifier: Apache-2.0 + * + * Licensed under the Apache License, Version 2.0 the "License"; + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +struct VSOutput +{ + float4 position : SV_POSITION; +}; + +VSOutput main(uint vertexID : SV_VertexID) +{ + VSOutput output; + + // Generate a fullscreen triangle using vertex ID + // This technique generates a triangle that covers the entire screen + // without needing any vertex buffer input + float2 uv = float2((vertexID << 1) & 2, vertexID & 2); + output.position = float4(uv * 2.0 - 1.0, 0.0, 1.0); + + return output; +} diff --git a/shaders/swapchain_present_timing/hlsl/fullscreen_triangle.vert.spv b/shaders/swapchain_present_timing/hlsl/fullscreen_triangle.vert.spv new file mode 100644 index 0000000000000000000000000000000000000000..dbdb52e54797d33776cf9ee0c3a497ac7f402c4b GIT binary patch literal 656 zcmY+AJ5B>p3`Nb%gg+r6Kf*7A(g8I>LIgV6umtERKv@D3J*z@uOEgHFGkH$}M~;23 zul+nOi|Ja#N<_3{!oSvyA~9uyt?mXg%>3o?`PIC@Rh0Z%#3zVWRR1*hV0Byg87{U* ztPQ{wu@+$G2|LfXGF~Nj&X8_5sc~!O>o|6o@kRsRZb#HN$<^3JrVb(+uBqjyU)D?U(GK&YEW5H*@xgH)wsQ zhui 0.0) + { + discard; + } + + return float4(pushConstants.color, 1.0); +} diff --git a/shaders/swapchain_present_timing/slang/circle.frag.spv b/shaders/swapchain_present_timing/slang/circle.frag.spv new file mode 100644 index 0000000000000000000000000000000000000000..aac60c414455803a0739bb01e0abb8c40cda3eba GIT binary patch literal 1296 zcmYk6+iFum6o#icHQL&PJ$W{5JygYLZ7qV>qF$sI0V6&@32mW)HoIi+2;K?ac;y56 z4Ej8ZDENIldmt13tTq2S%~~@_xwJkRDia}ujj+SFYd(~M%v7j_yU14iL%W$~N6qc$ zPqoj4vLg4H4fRk#FM+deuP;9jw!jpa!&loRDD#msca^vqWM`P3yiNLP*6nBM0Y$dA z@)|X2Yj+Kf)8urR^%Am2-W=yi+8eR*)!g4&hskL&KvrQr%lUYXoG*u)$l*tQ%3)2P zBt@@;d+0~KG()hj=Y1Uav%#lM((7lP?x1^i5c|}Byzdr!`xCREMQ(f0XP<1bpLO=G z$k~hB+!$w!TmzWbdENr91u`15noo2z&t-IH+yE>1*OBhMn+D$(k;cetpGIo8j(g8L zx+Ck1&Z>{RFo);60PJIl{pFk=<$YA_7vNm_FCz6fk9PB40j#~~Z%>{}|Jz9Ytrz|8 zqyPNT-d*IR8)N)KbXUyt7~NiC+@{9xdC$Dh(0sA>{*t}3i0{z7efdtT?OuQ9ZOjg{ z@O8=J=({ATgi zP7<%-pPr3NrvCL@fvvapnq6XNh@B_ai~YKEJ9DGnFrMEho`S2e0rgG1xWsy#Gx!R# zuL94dSl&Eq>pU*@Q~eUL^28YGFF3(tycqk4p7kjhnPFGV>oNQkDqnlAm4w+={wcW9T~TX-SI}R`2|eQx)*wU z^CBkyNsYLhIJNmoTrctJ^GKgs3^4gzvrO!)|1~RM`4Mw`kN*$* Date: Tue, 5 May 2026 11:46:14 +0200 Subject: [PATCH 2/3] Address review comments - Fix "times domains" typo in README - Add missing blank lines before bullet lists in README - Rename desired_swapchain_images to desired_swapchain_image_count - Simplify can_use_present_timing boolean assignment --- .../api/swapchain_present_timing/README.adoc | 4 +++- .../swapchain_present_timing.cpp | 19 ++++++------------- .../swapchain_present_timing.h | 14 +++++++------- 3 files changed, 16 insertions(+), 21 deletions(-) diff --git a/samples/api/swapchain_present_timing/README.adoc b/samples/api/swapchain_present_timing/README.adoc index 73547351a7..13e8a57125 100644 --- a/samples/api/swapchain_present_timing/README.adoc +++ b/samples/api/swapchain_present_timing/README.adoc @@ -32,6 +32,7 @@ Press 'P' on the keyboard to toggle the use of present timing feature. When pres == Extension Features VK_EXT_present_timing exposes 3 features at the physical device level: + * `presentTiming` is required for the extension to be exposed, and allows the application to query past presentation timings. * `presentAtAbsoluteTime` and `presentAtRelativeTime` allow applications to precisely control the time or duration of a presentation request. @@ -46,6 +47,7 @@ After creating a swapchain with a capable `VkSurfaceKHR` and the `VK_SWAPCHAIN_C === Timing Properties `VkSwapchainTimingPropertiesEXT` exposes a `refreshDuration` and a `refreshInterval` value. These two fields put together describe the behavior of the presentation engine: + * If both values are equal, the presentation engine is operating in a fixed refresh rate mode (FRR), and the value indicates the length of a refresh cycle in nanoseconds. * If `refreshInterval` is `UINT64_MAX`, it means variable refresh rate (VRR) is active, and `refreshDuration` is the minimum duration of a refresh cycle achievable (i.e. the maximum framerate). * If both values are different (but non-zero), then the presentation engine is operating in FRR mode, but with the ability to adjust its refresh duration by a factor of `refreshInterval` nanoseconds, sometimes referred to as Adaptive Refresh Rate (ARR). @@ -55,7 +57,7 @@ In this example which has trivial rendering, we simply use the `refreshDuration` === Time Domains -The time values are all expressed in a time domain chosen by the application among a list of candidates exposed by the swapchain. `VK_EXT_present_timing` introduces new opaque times domains that are local to a given swapchain: `VK_TIME_DOMAIN_PRESENT_STAGE_LOCAL_EXT`, which all implementations must support, and `VK_TIME_DOMAIN_SWAPCHAIN_LOCAL_EXT`. +The time values are all expressed in a time domain chosen by the application among a list of candidates exposed by the swapchain. `VK_EXT_present_timing` introduces new opaque time domains that are local to a given swapchain: `VK_TIME_DOMAIN_PRESENT_STAGE_LOCAL_EXT`, which all implementations must support, and `VK_TIME_DOMAIN_SWAPCHAIN_LOCAL_EXT`. Because these are opaque time domains, it is possible for an implementation to expose more than one of the same kind, for example when a window is moved from one display to another. For this reason, time domains are also assigned a unique id by the implementation. diff --git a/samples/api/swapchain_present_timing/swapchain_present_timing.cpp b/samples/api/swapchain_present_timing/swapchain_present_timing.cpp index b6730753e2..e543c63708 100644 --- a/samples/api/swapchain_present_timing/swapchain_present_timing.cpp +++ b/samples/api/swapchain_present_timing/swapchain_present_timing.cpp @@ -73,10 +73,10 @@ void SwapchainPresentTiming::query_surface_capabilities() } // Do triple-buffering when possible. This is clamped to the min and max image count limits. - desired_swapchain_images = std::max(surface_capabilities.surfaceCapabilities.minImageCount, 3u); + desired_swapchain_image_count = std::max(surface_capabilities.surfaceCapabilities.minImageCount, 3u); if (surface_capabilities.surfaceCapabilities.maxImageCount > 0) { - desired_swapchain_images = std::min(desired_swapchain_images, surface_capabilities.surfaceCapabilities.maxImageCount); + desired_swapchain_image_count = std::min(desired_swapchain_image_count, surface_capabilities.surfaceCapabilities.maxImageCount); } // Find a supported composite type. @@ -120,16 +120,9 @@ void SwapchainPresentTiming::query_surface_capabilities() // We'll calculate the animation time based on the display_present_stage's time measurements, so // don't try to use present timing if we don't have one. - if (display_present_stage && - (present_timing_capabilities.presentAtRelativeTimeSupported || - present_timing_capabilities.presentAtAbsoluteTimeSupported)) - { - can_use_present_timing = true; - } - else - { - can_use_present_timing = false; - } + can_use_present_timing = display_present_stage && + (present_timing_capabilities.presentAtRelativeTimeSupported || + present_timing_capabilities.presentAtAbsoluteTimeSupported); } /** @@ -434,7 +427,7 @@ void SwapchainPresentTiming::init_swapchain() VkSwapchainCreateInfoKHR info{VK_STRUCTURE_TYPE_SWAPCHAIN_CREATE_INFO_KHR}; info.flags = VK_SWAPCHAIN_CREATE_PRESENT_TIMING_BIT_EXT | VK_SWAPCHAIN_CREATE_PRESENT_ID_2_BIT_KHR; info.surface = get_surface(); - info.minImageCount = desired_swapchain_images; + info.minImageCount = desired_swapchain_image_count; info.imageFormat = surface_format.format; info.imageColorSpace = surface_format.colorSpace; info.imageExtent = swapchain_extents; diff --git a/samples/api/swapchain_present_timing/swapchain_present_timing.h b/samples/api/swapchain_present_timing/swapchain_present_timing.h index 0ad20b8e45..faaf76cd04 100644 --- a/samples/api/swapchain_present_timing/swapchain_present_timing.h +++ b/samples/api/swapchain_present_timing/swapchain_present_timing.h @@ -106,13 +106,13 @@ class SwapchainPresentTiming : public vkb::VulkanSampleC VkPipeline pipeline = VK_NULL_HANDLE; /// Surface data. - VkSurfaceFormatKHR surface_format = {}; - VkExtent2D swapchain_extents = {}; - VkCompositeAlphaFlagBitsKHR composite = VK_COMPOSITE_ALPHA_OPAQUE_BIT_KHR; - uint32_t desired_swapchain_images = 0; - bool can_use_present_timing = false; - bool can_present_at_absolute_time = false; - VkPresentStageFlagsEXT display_present_stage = 0; + VkSurfaceFormatKHR surface_format = {}; + VkExtent2D swapchain_extents = {}; + VkCompositeAlphaFlagBitsKHR composite = VK_COMPOSITE_ALPHA_OPAQUE_BIT_KHR; + uint32_t desired_swapchain_image_count = 0; + bool can_use_present_timing = false; + bool can_present_at_absolute_time = false; + VkPresentStageFlagsEXT display_present_stage = 0; /// The swapchain. VkSwapchainKHR swapchain = VK_NULL_HANDLE; From a521b2c0d8b8f42916d3808d39332faed7096206 Mon Sep 17 00:00:00 2001 From: Lionel Duc Date: Tue, 5 May 2026 12:18:28 +0200 Subject: [PATCH 3/3] Remove double-assignment of time domains / timing properties counters --- .../api/swapchain_present_timing/swapchain_present_timing.cpp | 3 --- 1 file changed, 3 deletions(-) diff --git a/samples/api/swapchain_present_timing/swapchain_present_timing.cpp b/samples/api/swapchain_present_timing/swapchain_present_timing.cpp index e543c63708..b345f22046 100644 --- a/samples/api/swapchain_present_timing/swapchain_present_timing.cpp +++ b/samples/api/swapchain_present_timing/swapchain_present_timing.cpp @@ -280,7 +280,6 @@ void SwapchainPresentTiming::query_past_presentation_timing() { query_swapchain_timing_properties(); select_target_present_duration(); - timing_properties_counter = past_presentation_properties.timingPropertiesCounter; } if (past_presentation_properties.timeDomainsCounter != time_domains_counter) @@ -294,8 +293,6 @@ void SwapchainPresentTiming::query_past_presentation_timing() { invalidate_timing_history(); } - - time_domains_counter = past_presentation_properties.timeDomainsCounter; } }