帧同步对于提供流畅的游戏体验至关重要。帧同步可确保以固定的时间间隔显示帧,从而最大限度地减少卡顿和输入延迟。虽然 Android Frame Pacing 库 (Swappy) 是大多数游戏的 推荐高级解决方案,但您可以通过 Vulkan 扩展实现 低级别控制。
使用以下 Vulkan 帧同步扩展,可以精确控制帧呈现:
VK_GOOGLE_display_timing:允许安排在特定时间呈现帧,并查询过去的呈现时间以调整渲染循环VK_EXT_present_timing:一种较新的标准化扩展,可为呈现请求提供全面的时间反馈,在 Android 17(API 级别 37)及更高版本中引入
VK_GOOGLE_display_timing 扩展较旧,并且受更多 Android 设备支持。不过,在面向较新设备时,最好使用
VK_EXT_present_timing,因为它提供了更多功能和更详细的时间信息。
VK_GOOGLE_display_timing
VK_GOOGLE_display_timing 扩展为应用提供了一种方式,以便:
- 查询显示屏的刷新周期时长
- 为每个帧指定所选的呈现时间
- 查询过去帧的实际呈现时间,以实现反馈循环
对于实现自己的帧同步算法而不是使用 Swappy 的游戏,此扩展非常有用。
启用扩展
如需使用 VK_GOOGLE_display_timing,请在创建 Vulkan 设备时启用它。在启用扩展之前,请验证实体设备是否支持该扩展:
// Check for extension support
uint32_t extensionCount;
vkEnumerateDeviceExtensionProperties(physicalDevice, nullptr, &extensionCount, nullptr);
std::vector<VkExtensionProperties> availableExtensions(extensionCount);
vkEnumerateDeviceExtensionProperties(physicalDevice, nullptr, &extensionCount, availableExtensions.data());
bool supported = false;
for (const auto& ext : availableExtensions) {
if (strcmp(ext.extensionName, VK_GOOGLE_DISPLAY_TIMING_EXTENSION_NAME) == 0) {
supported = true;
break;
}
}
if (supported) {
// Add to your enabled extensions list when calling vkCreateDevice
enabledDeviceExtensions.push_back(VK_GOOGLE_DISPLAY_TIMING_EXTENSION_NAME);
}
查询显示屏刷新时长
您可以使用 vkGetRefreshCycleDurationGOOGLE 查询与交换链关联的显示屏的刷新时长 :
VkRefreshCycleDurationGOOGLE refreshCycle;
vkGetRefreshCycleDurationGOOGLE(device, swapchain, &refreshCycle);
// refreshCycle.refreshDuration is the duration in nanoseconds
安排帧呈现
如需指定应何时显示帧,请在调用 vkQueuePresentKHR 时,将
VkPresentTimesInfoGOOGLE 结构附加到 pNext 链的 VkPresentInfoKHR
:
VkPresentTimeGOOGLE presentTime = {};
presentTime.presentID = frameIndex; // Unique ID for this frame
presentTime.desiredPresentTime = targetTimeNs; // Target time in nanoseconds (CLOCK_MONOTONIC)
VkPresentTimesInfoGOOGLE presentTimesInfo = {};
presentTimesInfo.sType = VK_STRUCTURE_TYPE_PRESENT_TIMES_INFO_GOOGLE;
presentTimesInfo.swapchainCount = 1;
presentTimesInfo.pTimes = &presentTime;
VkPresentInfoKHR presentInfo = {};
presentInfo.sType = VK_STRUCTURE_TYPE_PRESENT_INFO_KHR;
presentInfo.pNext = &presentTimesInfo;
// ... populate other presentInfo fields ...
vkQueuePresentKHR(queue, &presentInfo);
desiredPresentTime 应该是系统单调时钟 (CLOCK_MONOTONIC) 中的时间戳。根据显示屏的刷新率和过去帧的实际呈现时间计算此目标时间。在
Android 上,系统会忽略 desiredPresentTime 为 0 或任何时间戳超过 1 秒的未来时间。
VK_GOOGLE_display_timing 假定所有时间戳都源自同一时钟。
在 Android 上,与
VK_GOOGLE_display_timing 和 VK_EXT_present_timing 相关的所有时间戳都使用
CLOCK_MONOTONIC。(虽然 VK_EXT_present_timing 支持多个时间网域,但在 Android 上没有必要使用不同的时钟)。
查询过去的呈现时间
如需调整帧同步循环,请使用 vkGetPastPresentationTimingGOOGLE 查询过去帧的实际显示时间:
uint32_t timingCount = 0;
// Query the number of available timings
vkGetPastPresentationTimingGOOGLE(device, swapchain, &timingCount, nullptr);
if (timingCount > 0) {
std::vector<VkPastPresentationTimingGOOGLE> presentationTimings(timingCount);
vkGetPastPresentationTimingGOOGLE(device, swapchain, &timingCount, presentationTimings.data());
for (const auto& timing : presentationTimings) {
// Use timing information to adjust your pacing algorithm
// timing.presentID identifies the frame
// timing.actualPresentTime is when the frame was displayed (nanoseconds)
// timing.earliestPresentTime is the earliest the frame could have been displayed
// timing.presentMargin is the slack time between GPU completion and presentation
}
}
通过将 actualPresentTime 与 desiredPresentTime 进行比较,您可以确定帧是过早还是过晚到达,并相应地调整渲染循环。
代码示例
如需查看如何将 VK_GOOGLE_display_timing 集成到 Vulkan 渲染器的完整工作示例,请参阅 Android Game SDK 代码库中的立方体演示 :
此演示展示了如何启用扩展、计算目标呈现时间,以及处理过去呈现时间的反馈以保持稳定的帧速率。
VK_EXT_present_timing
VK_EXT_present_timing 扩展在 Vulkan 中引入,并在 Android 17 及更高版本中受支持,是一种标准化的更可靠的方式,可获取有关帧呈现的详细反馈。它取代并扩展了 VK_GOOGLE_display_timing 中的概念。
VK_EXT_present_timing 的主要优势包括:
- 标准化 API:属于官方 Khronos Vulkan 扩展集
- 详细的阶段查询:允许在呈现流水线的特定阶段查询时间戳(例如,帧何时出列、第一个像素何时发送到显示屏,以及第一个像素何时可见)
- 时间网域支持:支持不同的时间网域(例如,
系统时间、GPU 时间),并允许在它们之间进行校准。时间网域表示用于衡量时间戳的特定时钟源或时间基准。
在 Android 上,所有相关时间戳都使用
CLOCK_MONOTONIC,因此没有必要使用多个时间网域。 - 与
VK_KHR_present_id2集成:使用标准化的 VkPresentId2KHR 来标识呈现请求
启用扩展
如需使用 VK_EXT_present_timing,您必须在设备创建期间启用它及其前提条件 VK_KHR_present_id2。您还应检查实体设备功能支持:
VkPhysicalDevicePresentId2FeaturesKHR presentId2Features = {};
presentId2Features.sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_PRESENT_ID_2_FEATURES_KHR;
VkPhysicalDevicePresentTimingFeaturesEXT presentTimingFeatures = {};
presentTimingFeatures.sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_PRESENT_TIMING_FEATURES_EXT;
presentTimingFeatures.pNext = &presentId2Features;
VkPhysicalDeviceFeatures2 features2 = {};
features2.sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_FEATURES_2;
features2.pNext = &presentTimingFeatures;
vkGetPhysicalDeviceFeatures2(physicalDevice, &features2);
if (presentTimingFeatures.presentTiming && presentId2Features.presentId2) {
// Enable VK_EXT_present_timing and VK_KHR_present_id2
enabledDeviceExtensions.push_back(VK_EXT_PRESENT_TIMING_EXTENSION_NAME);
enabledDeviceExtensions.push_back(VK_KHR_PRESENT_ID_2_EXTENSION_NAME);
}
如果其中任何一项功能检查失败(presentTiming 或 presentId2 为 false),则设备或驱动程序不支持
VK_EXT_present_timing 或其前提条件。在这种情况下,您的应用无法使用 VK_EXT_present_timing,应回退到 VK_GOOGLE_display_timing(如果受支持)或依赖于默认帧同步机制,例如 Swappy。
在交换链上启用呈现时间
创建交换链时,通过设置 VK_SWAPCHAIN_CREATE_PRESENT_TIMING_BIT_EXT 标志来明确启用呈现时间:
VkSwapchainCreateInfoKHR swapchainCreateInfo = {};
swapchainCreateInfo.sType = VK_STRUCTURE_TYPE_SWAPCHAIN_CREATE_INFO_KHR;
swapchainCreateInfo.flags = VK_SWAPCHAIN_CREATE_PRESENT_TIMING_BIT_EXT;
// ... populate other fields ...
vkCreateSwapchainKHR(device, &swapchainCreateInfo, nullptr, &swapchain);
关联呈现 ID
呈现时,使用 VkPresentId2KHR(VK_KHR_present_id2 扩展的一部分)将唯一 ID 与每个帧相关联:
uint64_t presentId = frameIndex; // Unique, monotonically increasing ID
VkPresentId2KHR presentIdInfo = {};
presentIdInfo.sType = VK_STRUCTURE_TYPE_PRESENT_ID_2_KHR;
presentIdInfo.swapchainCount = 1;
presentIdInfo.pPresentIds = &presentId;
VkPresentInfoKHR presentInfo = {};
presentInfo.sType = VK_STRUCTURE_TYPE_PRESENT_INFO_KHR;
presentInfo.pNext = &presentIdInfo;
// ... populate other presentInfo fields ...
vkQueuePresentKHR(queue, &presentInfo);
查询交换链时间属性
使用 vkGetSwapchainTimingPropertiesEXT查询交换链时间属性,例如刷新时长:
VkSwapchainTimingPropertiesEXT timingProperties = {};
timingProperties.sType = VK_STRUCTURE_TYPE_SWAPCHAIN_TIMING_PROPERTIES_EXT;
uint64_t propertiesCounter = 0;
vkGetSwapchainTimingPropertiesEXT(device, swapchain, &timingProperties, &propertiesCounter);
// timingProperties.refreshDuration is the duration in nanoseconds
查询支持的时间网域
] 如前所述,时间网域表示特定的时钟源或时间基准。
虽然 VK_EXT_present_timing
支持多个时间网域并允许在它们之间进行校准,但在 Android 上没有必要使用不同的时钟,因为所有相关时间戳都使用
CLOCK_MONOTONIC。查询交换链支持的时间网域:
VkSwapchainTimeDomainPropertiesEXT timeDomainProps = {};
timeDomainProps.sType = VK_STRUCTURE_TYPE_SWAPCHAIN_TIME_DOMAIN_PROPERTIES_EXT;
// Query the count first
vkGetSwapchainTimeDomainPropertiesEXT(device, swapchain, &timeDomainProps, nullptr);
std::vector<VkTimeDomainKHR> timeDomains(timeDomainProps.timeDomainCount);
std::vector<uint64_t> timeDomainIds(timeDomainProps.timeDomainCount);
timeDomainProps.pTimeDomains = timeDomains.data();
timeDomainProps.pTimeDomainIds = timeDomainIds.data();
// Populate the data
vkGetSwapchainTimeDomainPropertiesEXT(device, swapchain, &timeDomainProps, nullptr);
请求目标呈现时间
如需请求在特定时间呈现帧,请将
VkPresentTimingInfoEXT结构链接到VkPresentInfoKHR。
VkPresentTimingInfoEXT timingInfo = {};
timingInfo.sType = VK_STRUCTURE_TYPE_PRESENT_TIMING_INFO_EXT;
timingInfo.flags = VK_PRESENT_TIMING_INFO_PRESENT_AT_RELATIVE_TIME_BIT_EXT; // Or absolute if supported
timingInfo.targetTime = targetTime; // Time value
timingInfo.timeDomainId = timeDomainIds[0]; // Use a supported time domain ID
timingInfo.presentStageQueries = VK_PRESENT_STAGE_IMAGE_FIRST_PIXEL_VISIBLE_BIT_EXT;
VkPresentTimingsInfoEXT presentTimingsInfo = {};
presentTimingsInfo.sType = VK_STRUCTURE_TYPE_PRESENT_TIMINGS_INFO_EXT;
presentTimingsInfo.swapchainCount = 1;
presentTimingsInfo.pTimingInfos = &timingInfo;
// Chain to VkPresentId2KHR
presentIdInfo.pNext = &presentTimingsInfo;
如前所述,在 Android 上,系统会忽略 targetTime 为 0 或任何目标时间戳超过 1 秒的未来时间。
查询过去的呈现时间
如需查询过去呈现的详细时间信息,请先 使用 vkSetSwapchainPresentTimingQueueSizeEXT 配置时间队列大小, 然后使用 vkGetPastPresentationTimingEXT 检索时间:
// Set the size of the timing queue (do this during initialization)
vkSetSwapchainPresentTimingQueueSizeEXT(device, swapchain, 10); // Keep last 10 frames
// ... later in your frame loop ...
VkPastPresentationTimingInfoEXT pastTimingInfo = {};
pastTimingInfo.sType = VK_STRUCTURE_TYPE_PAST_PRESENTATION_TIMING_INFO_EXT;
pastTimingInfo.swapchain = swapchain;
VkPastPresentationTimingPropertiesEXT pastProperties = {};
pastProperties.sType = VK_STRUCTURE_TYPE_PAST_PRESENTATION_TIMING_PROPERTIES_EXT;
// First query to get the count of available timings
vkGetPastPresentationTimingEXT(device, &pastTimingInfo, &pastProperties);
if (pastProperties.presentationTimingCount > 0) {
std::vector<VkPastPresentationTimingEXT> timings(pastProperties.presentationTimingCount);
pastProperties.pPresentationTimings = timings.data();
// Populate the timings
vkGetPastPresentationTimingEXT(device, &pastTimingInfo, &pastProperties);
for (const auto& timing : timings) {
// timing.presentId identifies the frame (matches the presentId you set)
// timing.targetTime is the requested target time
// If you requested stage queries, you can inspect timing.pPresentStages
}
}
代码示例
如需查看 VK_EXT_present_timing 的完整集成示例和一致性测试,请参阅 Vulkan CTS(兼容性测试套件)代码库中的
deqp(绘制元素质量计划) 测试:
这些测试展示了如何为呈现时间配置交换链、设置目标时间,以及验证不同时间网域中报告的呈现时间戳的准确性。