קצב פריימים קבוע הוא קריטי לחוויית משחק חלקה. קצב פריימים קבוע מבטיח שהפריימים יוצגו במרווחי זמן קבועים, וכך מצמצם את הגימגום ואת זמן האחזור של הקלט. למרות שספריית קצב הפריימים הקבוע של Android (Swappy) היא הפתרון המומלץ ברמה גבוהה לרוב המשחקים, יש אפשרות לשליטה ברמה נמוכה באמצעות תוספים של Vulkan.
כדי להשיג שליטה מדויקת בהצגת הפריימים, אפשר להשתמש בתוספים הבאים של Vulkan frame pacing:
-
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);
}
משך הרענון של תצוגת השאילתה
אפשר לשלוח שאילתה לגבי משך הרענון של הצג שמשויך ל-swapchain באמצעות vkGetRefreshCycleDurationGOOGLE:
VkRefreshCycleDurationGOOGLE refreshCycle;
vkGetRefreshCycleDurationGOOGLE(device, swapchain, &refreshCycle);
// refreshCycle.refreshDuration is the duration in nanoseconds
תזמון הצגת הפריימים
כדי לציין מתי צריך להציג פריים, צריך לצרף מבנה VkPresentTimesInfoGOOGLE לשרשרת pNext של VkPresentInfoKHR כשקוראים ל-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 צריך להיות חותמת זמן מהשעון המונוטוני של המערכת (CLOCK_MONOTONIC). צריך לחשב את זמן היעד הזה על סמך קצב הרענון של המסך וזמני ההצגה בפועל של פריימים קודמים. ב-Android, ערך של desiredPresentTime ששווה ל-0 או חותמת זמן שמוגדרת יותר משנייה אחת בעתיד מתעלמים ממנה.
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, אפשר לעיין בהדגמה של קובייה במאגר SDK למשחק ב-Android:
הדגמה של קובייה ב-Vulkan עם תזמון של הצגת המודעה
בהדגמה הזו נראה איך להפעיל את התוסף, לחשב את זמני ההצגה של היעד ולעבד משוב מתיזמונים קודמים של הצגות כדי לשמור על קצב פריימים יציב.
VK_EXT_present_timing
התוסף VK_EXT_present_timing, שהוצג ב-Vulkan ונתמך ב-Android מגרסה 17 ואילך, הוא דרך סטנדרטית ואמינה יותר לקבלת משוב מפורט על הצגת מסגרות. הוא מחליף את המושגים ב-VK_GOOGLE_display_timing ומרחיב אותם.
היתרונות העיקריים של VK_EXT_present_timing:
- Standardized 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.
הפעלת תזמון ההצגה ב-swapchain
כשיוצרים את שרשרת ההחלפה, צריך להפעיל במפורש את התזמון של הפעולה הנוכחית על ידי הגדרת הדגל 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);
מזהים של משתתפים משויכים
כשמציגים, משייכים מזהה ייחודי לכל פריים באמצעות
VkPresentId2KHR (חלק מהתוסף 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);
מאפייני התזמון של שרשרת החלפה של שאילתה
כדי לשלוח שאילתה לגבי מאפייני התזמון של swapchain, כמו משך הרענון, משתמשים ב-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 או כל חותמת זמן של יעד שגדולה ביותר משנייה אחת בעתיד מתעלמים ממנה.
שאילתה לגבי תזמונים של מצגות קודמות
כדי לשלוח שאילתה לגבי מידע מפורט על תזמון של הצגות קודמות, קודם צריך להגדיר את גודל תור התזמון באמצעות 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, אפשר לעיין בבדיקות deqp (תוכנית איכות של רכיבי ציור) במאגר Vulkan CTS (חבילת בדיקות תאימות):
Vulkan CTS Present Timing Tests
הבדיקות האלה מדגימות איך להגדיר שרשראות החלפה לתזמון הצגה, להגדיר זמני יעד ולאמת את הדיוק של חותמות הזמן של ההצגה שדווחו בדומיינים שונים של זמן.