O ritmo de frames é essencial para oferecer uma experiência de jogo tranquila. O pacing de frames garante que os frames sejam exibidos em intervalos regulares, minimizando a latência de entrada e a instabilidade. Embora a biblioteca Android Frame Pacing (Swappy) seja a solução de alto nível recomendada para a maioria dos jogos, o controle de baixo nível está disponível por extensões do Vulkan.
Use as seguintes extensões de ritmo de frames do Vulkan para ter controle preciso sobre a apresentação de frames:
VK_GOOGLE_display_timing: permite agendar frames para serem apresentados em horários específicos e consultar horários de apresentação anteriores para ajustar o loop de renderização.VK_EXT_present_timing: uma extensão mais recente e padronizada que oferece feedback abrangente de tempo para solicitações de apresentação, introduzida no Android 17 (nível da API 37) e mais recente.
A extensão VK_GOOGLE_display_timing é mais antiga e compatível com uma variedade maior de dispositivos Android. No entanto, o VK_EXT_present_timing é preferível ao segmentar
dispositivos mais novos porque oferece mais recursos e informações de tempo
mais detalhadas.
VK_GOOGLE_display_timing
A extensão VK_GOOGLE_display_timing oferece uma maneira para os aplicativos:
- Consultar a duração do ciclo de atualização de uma tela
- Especifique um tempo de apresentação para cada frame
- Consultar os tempos de apresentação reais de frames anteriores para implementar um loop de feedback
Essa extensão é útil para jogos que implementam o próprio algoritmo de ajuste de taxa de frames em vez de usar o Swappy.
Ativar a extensão
Para usar VK_GOOGLE_display_timing, ative-o ao criar o dispositivo
Vulkan. Antes de ativar a extensão, verifique se ela é compatível com o dispositivo físico:
// 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);
}
Consultar a duração da atualização da tela
É possível consultar a duração da atualização da tela associada a uma cadeia de troca usando vkGetRefreshCycleDurationGOOGLE:
VkRefreshCycleDurationGOOGLE refreshCycle;
vkGetRefreshCycleDurationGOOGLE(device, swapchain, &refreshCycle);
// refreshCycle.refreshDuration is the duration in nanoseconds
Programar a apresentação de um frame
Para especificar quando um frame deve ser exibido, anexe uma estrutura VkPresentTimesInfoGOOGLE à cadeia pNext de VkPresentInfoKHR ao chamar 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);
O desiredPresentTime precisa ser um carimbo de data/hora do relógio
monotônico do sistema (CLOCK_MONOTONIC). Calcule esse tempo de destino com base na taxa de atualização da tela e nos tempos de apresentação reais de frames anteriores. No Android, um desiredPresentTime de 0 ou qualquer carimbo de data/hora mais de um segundo no futuro é ignorado.
O VK_GOOGLE_display_timing pressupõe que todos os carimbos de data/hora vêm do mesmo relógio. No Android, todos os carimbos de data/hora relevantes para VK_GOOGLE_display_timing e VK_EXT_present_timing usam CLOCK_MONOTONIC. Embora o VK_EXT_present_timing seja compatível com vários domínios de tempo, não é necessário usar relógios diferentes no Android.
Consultar horários de apresentações anteriores
Para ajustar seu loop de espaçamento de frames, use vkGetPastPresentationTimingGOOGLE para consultar quando os frames anteriores foram exibidos:
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
}
}
Ao comparar actualPresentTime com seu desiredPresentTime, você pode determinar se os frames estão chegando muito cedo ou muito tarde e ajustar o loop de renderização de acordo.
Exemplo de código
Para um exemplo completo de como integrar VK_GOOGLE_display_timing
a um renderizador Vulkan, consulte a demonstração de cubo no repositório
do SDK para jogos do Android:
Demonstração do cubo do Vulkan com ajuste de tempo da tela
Esta demonstração mostra como ativar a extensão, calcular os tempos de apresentação desejados e processar o feedback de tempos de apresentação anteriores para manter uma taxa de frames estável.
VK_EXT_present_timing
Introduzida no Vulkan e compatível com o Android 17 e versões mais recentes, a extensão
VK_EXT_present_timing é uma maneira padronizada e mais robusta de receber
feedback detalhado sobre a apresentação de frames. Ela substitui e expande os
conceitos em VK_GOOGLE_display_timing.
As principais vantagens do VK_EXT_present_timing incluem:
- API padronizada: parte do conjunto oficial de extensões Khronos Vulkan.
- Consultas detalhadas de etapas: permite consultar carimbos de data/hora em etapas específicas do pipeline de apresentação (por exemplo, quando o frame foi removido da fila, quando o primeiro pixel foi enviado para a tela e quando o primeiro pixel ficou visível).
- Suporte ao domínio de tempo: oferece suporte a diferentes domínios de tempo (por exemplo, tempo do sistema, tempo da GPU) e permite a calibragem entre eles. Um domínio de tempo representa uma origem de relógio ou uma base de tempo específica usada para medir carimbos de data/hora.
No Android, todos os carimbos de data/hora relevantes usam
CLOCK_MONOTONIC. Portanto, não é necessário usar vários domínios de tempo. - Integração com
VK_KHR_present_id2: usa o VkPresentId2KHR padronizado para identificar solicitações de apresentação.
Ativar a extensão
Para usar o VK_EXT_present_timing, é necessário ativar esse recurso e o pré-requisito dele, o
VK_KHR_present_id2, durante a criação do dispositivo. Também é necessário verificar se há suporte para
recursos de dispositivos físicos:
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 uma dessas verificações de recursos falhar (presentTiming ou presentId2 for falso), o dispositivo ou driver não vai oferecer suporte a VK_EXT_present_timing ou ao pré-requisito dele. Nesse caso, o aplicativo não pode usar VK_EXT_present_timing e precisa voltar para VK_GOOGLE_display_timing (se compatível) ou usar mecanismos padrão de ajuste de taxa de frames, como o Swappy.
Ativar o tempo de apresentação na cadeia de troca
Ao criar a cadeia de troca, ative explicitamente a apresentação de tempo definindo a 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);
Associar IDs presentes
Ao apresentar, associe um ID exclusivo a cada frame usando
VkPresentId2KHR (parte da extensão 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);
Consultar propriedades de tempo da cadeia de troca
Consulte propriedades de tempo da cadeia de troca, como duração da atualização, usando 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
Consultar domínios de tempo compatíveis
]
Como observado anteriormente, um domínio de tempo representa uma fonte de relógio ou base de tempo específica.
Embora o VK_EXT_present_timing seja compatível com vários domínios de tempo e permita a calibragem entre eles, não é necessário usar relógios diferentes no Android porque todos os carimbos de data/hora relevantes usam CLOCK_MONOTONIC. Consulte os domínios de tempo compatíveis para sua cadeia de troca:
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);
Solicitar horário de apresentação desejado
Para solicitar que um frame seja apresentado em um momento específico, encadeie uma estrutura VkPresentTimingInfoEXT ao seu 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;
Como observado anteriormente, no Android, um targetTime de 0 ou qualquer carimbo de data/hora de destino com mais de um segundo no futuro é ignorado.
Consultar horários de apresentações anteriores
Para consultar informações detalhadas de tempo de apresentações anteriores, primeiro configure o tamanho da fila de tempo usando vkSetSwapchainPresentTimingQueueSizeEXT e recupere os tempos usando 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
}
}
Exemplo de código
Para um exemplo de integração completo e testes de conformidade para
VK_EXT_present_timing, consulte os testes do deqp (Draw Elements Quality Program)
no repositório do conjunto de teste de compatibilidade (CTS) do Vulkan:
Testes de tempo de apresentação do CTS do Vulkan
Esses testes demonstram como configurar cadeias de troca para o tempo de apresentação, definir tempos de destino e verificar a precisão dos carimbos de data/hora de apresentação informados em diferentes domínios de tempo.