Tốc độ khung hình là yếu tố quan trọng để mang lại trải nghiệm chơi trò chơi mượt mà. Tốc độ khung hình đảm bảo rằng các khung hình được hiển thị theo khoảng thời gian đều đặn, giảm thiểu tình trạng giật và độ trễ đầu vào. Mặc dù thư viện Android Frame Pacing (Swappy) là giải pháp cấp cao được đề xuất cho hầu hết các trò chơi, nhưng bạn có thể kiểm soát ở cấp thấp thông qua các tiện ích Vulkan.
Sử dụng các tiện ích tốc độ khung hình Vulkan sau đây để kiểm soát chính xác việc trình bày khung hình:
VK_GOOGLE_display_timing: Cho phép lên lịch trình bày khung hình vào những thời điểm cụ thể và truy vấn thời gian trình bày trước đây để điều chỉnh vòng lặp kết xuấtVK_EXT_present_timing: Một tiện ích mới hơn, được tiêu chuẩn hoá, cung cấp thông tin phản hồi toàn diện về thời gian cho các yêu cầu trình chiếu, được giới thiệu trong Android 17 (cấp độ API 37) trở lên
Tiện ích VK_GOOGLE_display_timing cũ hơn và được hỗ trợ trên nhiều thiết bị Android hơn. Tuy nhiên, bạn nên dùng VK_EXT_present_timing khi nhắm đến các thiết bị mới hơn vì API này cung cấp nhiều tính năng hơn và thông tin về thời gian chi tiết hơn.
VK_GOOGLE_display_timing
Tiện ích VK_GOOGLE_display_timing cung cấp cho các ứng dụng một cách để:
- Truy vấn thời lượng của chu kỳ làm mới màn hình
- Chỉ định thời gian trình chiếu đã chọn cho mỗi khung hình
- Truy vấn thời gian trình chiếu thực tế của các khung hình trước đây để triển khai một vòng lặp phản hồi
Tiện ích này hữu ích cho những trò chơi triển khai thuật toán điều chỉnh tốc độ khung hình của riêng mình thay vì sử dụng Swappy.
Bật tiện ích
Để sử dụng VK_GOOGLE_display_timing, hãy bật tính năng này khi tạo thiết bị Vulkan. Trước khi bật tiện ích, hãy xác minh rằng thiết bị thực hỗ trợ tiện ích đó:
// 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);
}
Truy vấn thời lượng làm mới màn hình
Bạn có thể truy vấn thời lượng làm mới của màn hình được liên kết với một swapchain bằng cách sử dụng vkGetRefreshCycleDurationGOOGLE:
VkRefreshCycleDurationGOOGLE refreshCycle;
vkGetRefreshCycleDurationGOOGLE(device, swapchain, &refreshCycle);
// refreshCycle.refreshDuration is the duration in nanoseconds
Lên lịch trình bày khung
Để chỉ định thời điểm hiển thị một khung hình, hãy đính kèm cấu trúc VkPresentTimesInfoGOOGLE vào chuỗi pNext của VkPresentInfoKHR khi gọi vkQueuePresentKHR:
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 phải là dấu thời gian từ đồng hồ đơn điệu của hệ thống (CLOCK_MONOTONIC). Hãy tính thời gian mục tiêu này dựa trên tốc độ làm mới của màn hình và thời gian trình chiếu thực tế của các khung hình trước. Trên Android, desiredPresentTime có giá trị 0 hoặc bất kỳ dấu thời gian nào lớn hơn 1 giây trong tương lai đều bị bỏ qua.
VK_GOOGLE_display_timing giả định rằng tất cả dấu thời gian đều bắt nguồn từ cùng một đồng hồ.
Trên Android, tất cả dấu thời gian liên quan đến cả VK_GOOGLE_display_timing và VK_EXT_present_timing đều sử dụng CLOCK_MONOTONIC. (Mặc dù VK_EXT_present_timing hỗ trợ nhiều miền thời gian, nhưng bạn không cần dùng nhiều đồng hồ trên Android).
Truy vấn thời gian trình bày trước đây
Để điều chỉnh vòng lặp nhịp độ khung hình, hãy dùng vkGetPastPresentationTimingGOOGLE để truy vấn thời điểm các khung hình trước đây thực sự được hiển thị:
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
}
}
Bằng cách so sánh actualPresentTime với desiredPresentTime, bạn có thể xác định xem các khung hình có đến quá sớm hay quá muộn hay không và điều chỉnh vòng lặp kết xuất cho phù hợp.
Ví dụ về mã
Để xem ví dụ đầy đủ về cách tích hợp VK_GOOGLE_display_timing vào một trình kết xuất Vulkan, hãy xem bản minh hoạ hình lập phương trong kho lưu trữ SDK cho trò chơi Android:
Bản minh hoạ Vulkan Cube có tính năng đo thời gian hiển thị
Bản minh hoạ này cho thấy cách bật tiện ích, tính toán thời gian trình chiếu mục tiêu và xử lý thông tin phản hồi từ thời gian trình chiếu trước đây để duy trì tốc độ khung hình ổn định.
VK_EXT_present_timing
Được giới thiệu trong Vulkan và được hỗ trợ trong Android 17 trở lên, tiện ích VK_EXT_present_timing là một cách tiêu chuẩn và mạnh mẽ hơn để nhận được thông tin phản hồi chi tiết về việc trình bày khung hình. Nó thay thế và mở rộng các khái niệm trong VK_GOOGLE_display_timing.
Các lợi thế chính của VK_EXT_present_timing bao gồm:
- API được chuẩn hoá: Nằm trong bộ tiện ích Vulkan chính thức của Khronos
- Truy vấn chi tiết theo giai đoạn: Cho phép truy vấn dấu thời gian ở các giai đoạn cụ thể của quy trình trình bày (ví dụ: khi khung hình được đưa vào hàng đợi, khi pixel đầu tiên được gửi đến màn hình và khi pixel đầu tiên xuất hiện)
- Hỗ trợ miền thời gian: Hỗ trợ nhiều miền thời gian (ví dụ: thời gian hệ thống, thời gian GPU) và cho phép hiệu chỉnh giữa các miền này. Miền thời gian biểu thị một nguồn đồng hồ hoặc cơ sở thời gian cụ thể được dùng để đo dấu thời gian.
Trên Android, tất cả dấu thời gian có liên quan đều sử dụng
CLOCK_MONOTONIC, nên bạn không cần sử dụng nhiều miền thời gian. - Tích hợp với
VK_KHR_present_id2: Sử dụng VkPresentId2KHR được chuẩn hoá để xác định các yêu cầu trình bày
Bật tiện ích
Để sử dụng VK_EXT_present_timing, bạn phải bật tính năng này và điều kiện tiên quyết của tính năng này là VK_KHR_present_id2 trong quá trình tạo thiết bị. Bạn cũng nên kiểm tra xem thiết bị thực có hỗ trợ các tính năng sau hay không:
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);
}
Nếu một trong hai quy trình kiểm tra tính năng này không thành công (presentTiming hoặc presentId2 là false), thì thiết bị hoặc trình điều khiển không hỗ trợ VK_EXT_present_timing hoặc điều kiện tiên quyết của VK_EXT_present_timing. Trong trường hợp này, ứng dụng của bạn không thể sử dụng VK_EXT_present_timing và nên quay lại VK_GOOGLE_display_timing (nếu được hỗ trợ) hoặc dựa vào các cơ chế điều chỉnh tốc độ khung hình mặc định, chẳng hạn như Swappy.
Bật tính năng hiện thời gian trên chuỗi hoán đổi
Khi tạo chuỗi hoán đổi, hãy bật rõ ràng tính năng định thời gian hiển thị bằng cách đặt cờ 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);
Mã nhận dạng cấp phó hiện tại
Khi trình bày, hãy liên kết một mã nhận dạng duy nhất với mỗi khung hình bằng cách sử dụng VkPresentId2KHR (một phần của tiện ích VK_KHR_present_id2):
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);
Truy vấn các thuộc tính thời gian của chuỗi hoán đổi
Truy vấn các thuộc tính thời gian của chuỗi hoán đổi, chẳng hạn như thời lượng làm mới, bằng cách sử dụng 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
Truy vấn các miền thời gian được hỗ trợ
]
Như đã đề cập trước đó, miền thời gian đại diện cho một nguồn đồng hồ hoặc cơ sở thời gian cụ thể.
Mặc dù VK_EXT_present_timing hỗ trợ nhiều miền thời gian và cho phép hiệu chỉnh giữa các miền này, nhưng bạn không cần dùng các đồng hồ khác nhau trên Android vì tất cả dấu thời gian có liên quan đều dùng CLOCK_MONOTONIC. Truy vấn các miền thời gian được hỗ trợ cho swapchain của bạn:
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);
Thời gian trình bày mục tiêu theo yêu cầu
Để yêu cầu một khung hình được trình bày vào một thời điểm cụ thể, hãy xâu chuỗi cấu trúc VkPresentTimingInfoEXT vào 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;
Như đã đề cập trước đó, trên Android, targetTime có giá trị 0 hoặc bất kỳ dấu thời gian mục tiêu nào lớn hơn 1 giây trong tương lai đều sẽ bị bỏ qua.
Truy vấn thời gian trình bày trước đây
Để truy vấn thông tin chi tiết về thời gian cho các bản trình bày trước đây, trước tiên, hãy định cấu hình kích thước hàng đợi thời gian bằng cách sử dụng vkSetSwapchainPresentTimingQueueSizeEXT, sau đó truy xuất thời gian bằng cách sử dụng 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
}
}
Ví dụ về mã
Để xem ví dụ đầy đủ về quy trình tích hợp và các bài kiểm thử tuân thủ cho VK_EXT_present_timing, hãy tham khảo các bài kiểm thử deqp (Chương trình chất lượng của các phần tử vẽ) trong kho lưu trữ CTS (Bộ kiểm tra tính tương thích) của Vulkan:
Các bài kiểm thử thời gian hiển thị CTS của Vulkan
Các bài kiểm thử này minh hoạ cách định cấu hình swapchain cho thời gian hiển thị, đặt thời gian mục tiêu và xác minh độ chính xác của dấu thời gian trình bày được báo cáo trên nhiều miền thời gian.