Vulkan 帧同步扩展

帧同步对于提供流畅的游戏体验至关重要。帧同步可确保以固定的时间间隔显示帧,从而最大限度地减少卡顿和输入延迟。虽然 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 扩展为应用提供了一种方式,以便:

  1. 查询显示屏的刷新周期时长
  2. 为每个帧指定所选的呈现时间
  3. 查询过去帧的实际呈现时间,以实现反馈循环

对于实现自己的帧同步算法而不是使用 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_timingVK_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
    }
}

通过将 actualPresentTimedesiredPresentTime 进行比较,您可以确定帧是过早还是过晚到达,并相应地调整渲染循环。

代码示例

如需查看如何将 VK_GOOGLE_display_timing 集成到 Vulkan 渲染器的完整工作示例,请参阅 Android Game SDK 代码库中的立方体演示

Vulkan 立方体演示(带显示屏时间)

此演示展示了如何启用扩展、计算目标呈现时间,以及处理过去呈现时间的反馈以保持稳定的帧速率。


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);
}

如果其中任何一项功能检查失败(presentTimingpresentId2 为 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

呈现时,使用 VkPresentId2KHRVK_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(绘制元素质量计划) 测试:

Vulkan CTS 呈现时间测试

这些测试展示了如何为呈现时间配置交换链、设置目标时间,以及验证不同时间网域中报告的呈现时间戳的准确性。