影格放送速度對於提供流暢的遊戲體驗至關重要。影格放送速度可確保影格以固定間隔顯示,盡量減少延遲和輸入延遲。雖然Android 影格放送速度程式庫 (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 結構體附加至 VkPresentInfoKHR 的 pNext 鏈結:
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 上,系統會忽略 0 的 desiredPresentTime 或任何超過 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 存放區中的立方體試用版:
Vulkan Cube Demo with Display Timing
本示範說明如何啟用擴充功能、計算目標呈現時間,以及處理過去呈現時間的回饋,以維持穩定的影格率。
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 (Compatibility Test Suite) 存放區中的 deqp (Draw Elements Quality Program) 測試:
這些測試示範如何設定交換鏈的顯示時間、設定目標時間,以及驗證不同時間網域中回報的顯示時間戳記是否準確。