Extensões de ritmo de frames do Vulkan

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:

  1. Consultar a duração do ciclo de atualização de uma tela
  2. Especifique um tempo de apresentação para cada frame
  3. 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.