Vulkan 影格使用速度擴充功能

影格放送速度對於提供流暢的遊戲體驗至關重要。影格放送速度可確保影格以固定間隔顯示,盡量減少延遲和輸入延遲。雖然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」擴充功能可讓應用程式:

  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 結構體附加至 VkPresentInfoKHRpNext 鏈結:

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

如果這兩項功能檢查有任何一項失敗 (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 的關聯

呈現時,請使用 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) 測試:

Vulkan CTS Present Timing 測試

這些測試示範如何設定交換鏈的顯示時間、設定目標時間,以及驗證不同時間網域中回報的顯示時間戳記是否準確。