Il frame pacing è fondamentale per offrire un'esperienza di gioco fluida. Il frame pacing garantisce che i frame vengano visualizzati a intervalli regolari, riducendo al minimo i problemi di stuttering e la latenza di input. Sebbene la libreria Android Frame Pacing (Swappy) sia la soluzione di alto livello consigliata per la maggior parte dei giochi, il controllo di basso livello è disponibile tramite le estensioni Vulkan.
Utilizza le seguenti estensioni di Vulkan per il pacing dei frame per ottenere un controllo preciso sulla presentazione dei frame:
VK_GOOGLE_display_timing: consente di programmare la presentazione dei frame in orari specifici e di eseguire query sugli orari di presentazione passati per regolare il ciclo di renderingVK_EXT_present_timing: un'estensione più recente e standardizzata che fornisce un feedback completo sulla tempistica per le richieste di presentazione, introdotta in Android 17 (livello API 37) e versioni successive
L'estensione VK_GOOGLE_display_timing è meno recente e supportata su una gamma più ampia
di dispositivi Android. Tuttavia, VK_EXT_present_timing è preferibile per il targeting
di dispositivi più recenti perché offre più funzionalità e informazioni
più dettagliate sulla tempistica.
VK_GOOGLE_display_timing
L'estensione VK_GOOGLE_display_timing consente alle applicazioni di:
- Eseguire una query sulla durata del ciclo di aggiornamento di un display
- Specifica un tempo di presentazione scelto per ogni frame
- Esegui query sui tempi di presentazione effettivi dei frame precedenti per implementare un ciclo di feedback
Questa estensione è utile per i giochi che implementano il proprio algoritmo di pacing dei frame anziché utilizzare Swappy.
Attivare l'estensione
Per utilizzare VK_GOOGLE_display_timing, attivalo durante la creazione del dispositivo Vulkan. Prima di attivare l'estensione, verifica che sia supportata dal dispositivo fisico:
// 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);
}
Durata dell'aggiornamento della visualizzazione della query
Puoi eseguire query sulla durata dell'aggiornamento del display associato a una swapchain utilizzando vkGetRefreshCycleDurationGOOGLE:
VkRefreshCycleDurationGOOGLE refreshCycle;
vkGetRefreshCycleDurationGOOGLE(device, swapchain, &refreshCycle);
// refreshCycle.refreshDuration is the duration in nanoseconds
Programmare la presentazione dei fotogrammi
Per specificare quando deve essere visualizzato un frame, collega una struttura
VkPresentTimesInfoGOOGLE alla catena pNext di VkPresentInfoKHR
quando chiami 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 deve essere un timestamp dell'orologio
monotonico del sistema (CLOCK_MONOTONIC). Calcola questo tempo target in base alla frequenza di aggiornamento del display e ai tempi di presentazione effettivi dei fotogrammi precedenti. Su Android, un
desiredPresentTime pari a 0 o un timestamp futuro superiore a 1 secondo viene
ignorato.
VK_GOOGLE_display_timing presuppone che tutti i timestamp provengano dallo stesso orologio.
Su Android, tutti i timestamp pertinenti sia a VK_GOOGLE_display_timing sia a
VK_EXT_present_timing utilizzano CLOCK_MONOTONIC. (Sebbene VK_EXT_present_timing
supporti più domini temporali, l'utilizzo di orologi diversi non è necessario su
Android).
Query sugli orari delle presentazioni passate
Per regolare il ciclo di pacing dei frame, utilizza vkGetPastPresentationTimingGOOGLE per eseguire query su quando sono stati effettivamente visualizzati i frame precedenti:
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
}
}
Confrontando actualPresentTime con desiredPresentTime, puoi
determinare se i frame arrivano troppo presto o troppo tardi e regolare il ciclo di rendering
di conseguenza.
Codice di esempio
Per un esempio pratico completo di come integrare VK_GOOGLE_display_timing
in un renderer Vulkan, consulta la demo del cubo nel repository
dell'SDK Android Game:
Vulkan Cube Demo con Display Timing
Questa demo mostra come attivare l'estensione, calcolare i tempi di presentazione target ed elaborare il feedback dei tempi di presentazione passati per mantenere una frequenza fotogrammi stabile.
VK_EXT_present_timing
Introdotta in Vulkan e supportata in Android 17 e versioni successive, l'estensione
VK_EXT_present_timing è un modo standardizzato e più solido per ottenere
feedback dettagliati sulla presentazione dei frame. Sostituisce ed espande i concetti
in VK_GOOGLE_display_timing.
I principali vantaggi di VK_EXT_present_timing includono:
- API standardizzata: parte del set di estensioni Vulkan ufficiale di Khronos
- Query dettagliate sulle fasi: consente di eseguire query sui timestamp in fasi specifiche della pipeline di presentazione (ad esempio, quando il frame è stato rimosso dalla coda, quando il primo pixel è stato inviato al display e quando il primo pixel è diventato visibile)
- Supporto del dominio temporale: supporta diversi domini temporali (ad esempio, ora di sistema, ora della GPU) e consente la calibrazione tra loro. Un dominio temporale rappresenta una specifica origine di clock o base temporale utilizzata per misurare i timestamp. Su Android, tutti i timestamp pertinenti utilizzano
CLOCK_MONOTONIC, pertanto non è necessario utilizzare più domini temporali. - Integrazione con
VK_KHR_present_id2: utilizza VkPresentId2KHR standardizzato per identificare le richieste di presentazione
Attivare l'estensione
Per utilizzare VK_EXT_present_timing, devi attivarlo e il relativo prerequisito,
VK_KHR_present_id2, durante la creazione del dispositivo. Devi anche verificare il supporto delle funzionalità
del dispositivo fisico:
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);
}
Se uno di questi controlli delle funzionalità non va a buon fine (presentTiming o presentId2 è false), il dispositivo o il driver non supporta VK_EXT_present_timing o il relativo prerequisito. In questo caso, la tua applicazione non può utilizzare VK_EXT_present_timing e deve ricorrere a VK_GOOGLE_display_timing (se supportato) o affidarsi a meccanismi di pacing dei frame predefiniti, come Swappy.
Abilita la presentazione della sincronizzazione verticale sulla swapchain
Quando crei la swapchain, attiva esplicitamente la sincronizzazione della presentazione impostando il flag 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);
Associa ID presenti
Durante la presentazione, associa un ID univoco a ogni frame utilizzando
VkPresentId2KHR (parte dell'estensione 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);
Proprietà di temporizzazione della swapchain di query
Esegui query sulle proprietà di temporizzazione della swapchain, ad esempio la durata dell'aggiornamento, utilizzando 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
Eseguire query sui domini temporali supportati
]
Come indicato in precedenza, un dominio temporale rappresenta una base temporale o un'origine di clock specifica.
Sebbene VK_EXT_present_timing supporti più domini temporali e consenta
la calibrazione tra di loro, l'utilizzo di orologi diversi non è necessario su Android
perché tutti i timestamp pertinenti utilizzano CLOCK_MONOTONIC. Interroga i domini temporali supportati
per la tua swapchain:
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);
Richiedi l'orario di presentazione target
Per richiedere la presentazione di un frame in un momento specifico, collega una
struttura VkPresentTimingInfoEXT al tuo 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;
Come indicato in precedenza, su Android, un targetTime pari a 0 o qualsiasi timestamp di destinazione successivo di più di 1 secondo viene ignorato.
Query sui tempi di presentazione passati
Per eseguire query sulle informazioni dettagliate sui tempi delle presentazioni passate, configura prima le dimensioni della coda di temporizzazione utilizzando vkSetSwapchainPresentTimingQueueSizeEXT, e poi recupera le tempistiche utilizzando 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
}
}
Codice di esempio
Per un esempio di integrazione completo e test di conformità per
VK_EXT_present_timing, consulta i test deqp (Draw Elements Quality Program)
nel repository Vulkan CTS (Compatibility Test Suite):
Vulkan CTS Present Timing Tests
Questi test mostrano come configurare le swapchain per la sincronizzazione della presentazione, impostare i tempi target e verificare l'accuratezza dei timestamp di presentazione segnalati in diversi domini temporali.