Le frame pacing est essentiel pour offrir une expérience de jeu fluide. Il permet de s'assurer que les frames sont affichées à intervalles réguliers, ce qui minimise les saccades et la latence d'entrée. Bien que la bibliothèque Android Frame Pacing (Swappy) soit la solution de haut niveau recommandée pour la plupart des jeux, un contrôle de bas niveau est disponible via les extensions Vulkan.
Utilisez les extensions de frame pacing Vulkan suivantes pour contrôler précisément la présentation des frames :
VK_GOOGLE_display_timing: permet de planifier la présentation des frames à des moments spécifiques et d'interroger les temps de présentation passés pour ajuster la boucle de rendu.VK_EXT_present_timing: extension plus récente et standardisée qui fournit des commentaires complets sur le timing pour les requêtes de présentation. Elle a été introduite dans Android 17 (niveau d'API 37) et versions ultérieures.
L'extension VK_GOOGLE_display_timing est plus ancienne et compatible avec un plus grand nombre d'appareils Android. Toutefois, VK_EXT_present_timing est préférable lorsque vous ciblez des appareils plus récents, car elle offre plus de fonctionnalités et des informations de timing plus détaillées.
VK_GOOGLE_display_timing
L'extension VK_GOOGLE_display_timing permet aux applications de :
- interroger la durée du cycle d'actualisation d'un écran ;
- spécifier un temps de présentation choisi pour chaque frame ;
- interroger les temps de présentation réels des frames passées pour implémenter une boucle de rétroaction.
Cette extension est utile pour les jeux qui implémentent leur propre algorithme de frame pacing au lieu d'utiliser Swappy.
Activer l'extension
Pour utiliser VK_GOOGLE_display_timing, activez-la lors de la création de l'appareil Vulkan. Avant d'activer l'extension, vérifiez qu'elle est compatible avec l'appareil physique :
// 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);
}
Interroger la durée d'actualisation de l'écran
Vous pouvez interroger la durée d'actualisation de l'écran associé à une chaîne d'échange à l'aide de vkGetRefreshCycleDurationGOOGLE :
VkRefreshCycleDurationGOOGLE refreshCycle;
vkGetRefreshCycleDurationGOOGLE(device, swapchain, &refreshCycle);
// refreshCycle.refreshDuration is the duration in nanoseconds
Planifier la présentation des frames
Pour spécifier le moment où une frame doit être affichée, associez une
structure VkPresentTimesInfoGOOGLE à la chaîne pNext de VkPresentInfoKHR
lorsque vous appelez 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);
Le desiredPresentTime doit être un code temporel de l'horloge monotone du système (CLOCK_MONOTONIC). Calculez ce délai cible en fonction de la fréquence d'actualisation de l'écran et des temps de présentation réels des frames passées. Sur Android, un desiredPresentTime de 0 ou tout code temporel supérieur à une seconde dans le futur est ignoré.
VK_GOOGLE_display_timing suppose que tous les codes temporels proviennent de la même horloge.
Sur Android, tous les codes temporels pertinents pour VK_GOOGLE_display_timing et VK_EXT_present_timing utilisent CLOCK_MONOTONIC. (Bien que VK_EXT_present_timing soit compatible avec plusieurs domaines temporels, l'utilisation de différentes horloges n'est pas nécessaire sur Android).
Interroger les temps de présentation passés
Pour ajuster votre boucle de frame pacing, utilisez vkGetPastPresentationTimingGOOGLE pour interroger le moment où les frames passées ont été réellement affichées :
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
}
}
En comparant actualPresentTime à votre desiredPresentTime, vous pouvez déterminer si les frames arrivent trop tôt ou trop tard, et ajuster votre boucle de rendu en conséquence.
Exemple de code
Pour obtenir un exemple de fonctionnement complet de l'intégration de VK_GOOGLE_display_timing dans un moteur de rendu Vulkan, consultez la démonstration du cube dans le dépôt SDK Android Game :
Démonstration du cube Vulkan avec le timing d'affichage
Cette démonstration explique comment activer l'extension, calculer les temps de présentation cibles et traiter les commentaires des timings de présentation passés pour maintenir une fréquence d'images stable.
VK_EXT_present_timing
Introduite dans Vulkan et compatible avec Android 17 et versions ultérieures, l'extension VK_EXT_present_timing est un moyen standardisé et plus robuste d'obtenir des commentaires détaillés sur la présentation des frames. Elle remplace et développe les concepts de VK_GOOGLE_display_timing.
Les principaux avantages de VK_EXT_present_timing sont les suivants :
- API standardisée : fait partie de l'ensemble d'extensions Khronos Vulkan officiel.
- Requêtes d'étape détaillées : permet d'interroger les codes temporels à des étapes spécifiques du pipeline de présentation (par exemple, lorsque la frame a été mise en file d'attente, lorsque le premier pixel a été envoyé à l'écran et lorsque le premier pixel est devenu visible).
- Prise en charge des domaines temporels : prend en charge différents domaines temporels (par exemple,
l'heure système, l'heure du GPU) et permet de les calibrer. Un domaine temporel représente une source d'horloge ou une base de temps spécifique utilisée pour mesurer les codes temporels.
Sur Android, tous les codes temporels pertinents utilisent
CLOCK_MONOTONIC. Il n'est donc pas nécessaire d'utiliser plusieurs domaines temporels. - Intégration à
VK_KHR_present_id2: utilise le VkPresentId2KHR standardisé pour identifier les requêtes de présentation.
Activer l'extension
Pour utiliser VK_EXT_present_timing, vous devez l'activer, ainsi que son prérequis, VK_KHR_present_id2, lors de la création de l'appareil. Vous devez également vérifier la compatibilité des fonctionnalités de l'appareil physique :
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);
}
Si l'une de ces vérifications de fonctionnalité échoue (presentTiming ou presentId2 est défini sur "false"), l'appareil ou le pilote n'est pas compatible avec VK_EXT_present_timing ni avec son prérequis. Dans ce cas, votre application ne peut pas utiliser VK_EXT_present_timing et doit revenir à VK_GOOGLE_display_timing (si elle est compatible) ou s'appuyer sur des mécanismes de frame pacing par défaut, tels que Swappy.
Activer le timing de présentation sur la chaîne d'échange
Lors de la création de la chaîne d'échange, activez explicitement le timing de présentation en définissant l'indicateur 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);
Associer des ID de présentation
Lors de la présentation, associez un ID unique à chaque frame à l'aide de VkPresentId2KHR (qui fait partie de l'extension 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);
Interroger les propriétés de timing de la chaîne d'échange
Interrogez les propriétés de timing de la chaîne d'échange, telles que la durée d'actualisation, à l'aide de 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
Interroger les domaines temporels compatibles
] Comme indiqué précédemment, un domaine temporel représente une source d'horloge ou une base de temps spécifique.
Bien que VK_EXT_present_timing soit compatible avec plusieurs domaines temporels et permette de les calibrer, l'utilisation de différentes horloges n'est pas nécessaire sur Android, car tous les codes temporels pertinents utilisent CLOCK_MONOTONIC. Interrogez les domaines temporels compatibles pour votre chaîne d'échange :
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);
Demander le temps de présentation cible
Pour demander qu'une frame soit présentée à un moment spécifique, enchaînez une
structure VkPresentTimingInfoEXT à votre 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;
Comme indiqué précédemment, sur Android, un targetTime de 0 ou tout code temporel cible supérieur à une seconde dans le futur est ignoré.
Interroger les timings de présentation passés
Pour interroger des informations de timing détaillées sur les présentations passées, configurez d'abord la taille de la file d'attente de timing à l'aide de vkSetSwapchainPresentTimingQueueSizeEXT, puis récupérez les timings à l'aide de 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
}
}
Exemple de code
Pour obtenir un exemple d'intégration complet et des tests de conformité pour VK_EXT_present_timing, consultez les tests deqp (Draw Elements Quality Program) dans le dépôt Vulkan CTS (Compatibility Test Suite) :
Tests de timing de présentation Vulkan CTS
Ces tests expliquent comment configurer des chaînes d'échange pour le timing de présentation, définir des délais cibles et vérifier l'exactitude des codes temporels de présentation signalés dans différents domaines temporels.