Extensiones de ritmo de fotogramas de Vulkan

El ritmo de fotogramas es fundamental para ofrecer una experiencia de juego fluida. El ritmo de fotogramas garantiza que los fotogramas se muestren en intervalos regulares, lo que minimiza la latencia de entrada y los tartamudeos. Si bien la biblioteca de Android Frame Pacing (Swappy) es la solución de alto nivel recomendada para la mayoría de los juegos, el control de bajo nivel está disponible a través de las extensiones de Vulkan.

Usa las siguientes extensiones de ritmo de fotogramas de Vulkan para lograr un control preciso sobre la presentación de fotogramas:

  • VK_GOOGLE_display_timing: Permite programar la presentación de fotogramas en momentos específicos y consultar los tiempos de presentación anteriores para ajustar el bucle de renderización.
  • VK_EXT_present_timing: Es una extensión más reciente y estandarizada que proporciona comentarios integrales sobre el tiempo para las solicitudes de presentación, y se introdujo en Android 17 (nivel de API 37) y versiones posteriores.

La extensión VK_GOOGLE_display_timing es más antigua y se admite en una mayor variedad de dispositivos Android. Sin embargo, se prefiere VK_EXT_present_timing cuando se segmentan dispositivos más nuevos porque ofrece más funciones y más información de sincronización detallada.

VK_GOOGLE_display_timing

La extensión VK_GOOGLE_display_timing proporciona una forma para que las aplicaciones hagan lo siguiente:

  1. Consultar la duración del ciclo de actualización de una pantalla
  2. Especifica un tiempo de presentación elegido para cada fotograma
  3. Consulta los tiempos de presentación reales de los fotogramas anteriores para implementar un bucle de retroalimentación.

Esta extensión es útil para los juegos que implementan su propio algoritmo de cadencia de fotogramas en lugar de usar Swappy.

Habilita la extensión

Para usar VK_GOOGLE_display_timing, habilítalo cuando crees el dispositivo Vulkan. Antes de habilitar la extensión, verifica que el dispositivo físico la admita:

// 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 la duración de la actualización de la pantalla

Puedes consultar la duración de actualización de la pantalla asociada a una cadena de intercambio con vkGetRefreshCycleDurationGOOGLE:

VkRefreshCycleDurationGOOGLE refreshCycle;
vkGetRefreshCycleDurationGOOGLE(device, swapchain, &refreshCycle);
// refreshCycle.refreshDuration is the duration in nanoseconds

Cómo programar la presentación de fotogramas

Para especificar cuándo se debe mostrar un fotograma, adjunta una estructura VkPresentTimesInfoGOOGLE a la cadena pNext de VkPresentInfoKHR cuando llames a 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);

El desiredPresentTime debe ser una marca de tiempo del reloj monotónico del sistema (CLOCK_MONOTONIC). Calcula este tiempo objetivo en función de la frecuencia de actualización de la pantalla y los tiempos de presentación reales de los fotogramas anteriores. En Android, se ignora un desiredPresentTime de 0 o cualquier marca de tiempo que sea más de 1 segundo en el futuro.

VK_GOOGLE_display_timing supone que todas las marcas de tiempo provienen del mismo reloj. En Android, todas las marcas de tiempo relevantes para VK_GOOGLE_display_timing y VK_EXT_present_timing usan CLOCK_MONOTONIC. (Si bien VK_EXT_present_timing admite varios dominios de tiempo, no es necesario usar diferentes relojes en Android).

Consultar horarios de presentaciones anteriores

Para ajustar el bucle de cadencia de fotogramas, usa vkGetPastPresentationTimingGOOGLE para consultar cuándo se mostraron realmente los fotogramas anteriores:

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
    }
}

Si comparas actualPresentTime con tu desiredPresentTime, puedes determinar si los fotogramas llegan demasiado temprano o demasiado tarde, y ajustar tu bucle de renderización en consecuencia.

Ejemplo de código

Para ver un ejemplo funcional completo de cómo integrar VK_GOOGLE_display_timing en un renderizador de Vulkan, consulta la demostración del cubo en el repositorio del SDK de juegos de Android:

Demostración de cubo de Vulkan con sincronización de pantalla

En esta demostración, se muestra cómo habilitar la extensión, calcular los tiempos de presentación objetivo y procesar los comentarios de los tiempos de presentación anteriores para mantener una velocidad de fotogramas estable.


VK_EXT_present_timing

La extensión VK_EXT_present_timing, que se introdujo en Vulkan y es compatible con Android 17 y versiones posteriores, es una forma estandarizada y más sólida de obtener comentarios detallados sobre la presentación de fotogramas. Reemplaza y amplía los conceptos de VK_GOOGLE_display_timing.

Entre las ventajas clave de VK_EXT_present_timing, se incluyen las siguientes:

  • API estandarizada: Forma parte del conjunto oficial de extensiones de Khronos Vulkan
  • Consultas detalladas de etapas: Permite consultar marcas de tiempo en etapas específicas de la canalización de presentación (por ejemplo, cuándo se quitó el fotograma de la cola, cuándo se envió el primer píxel a la pantalla y cuándo se hizo visible el primer píxel).
  • Compatibilidad con el dominio de tiempo: Admite diferentes dominios de tiempo (por ejemplo, hora del sistema, hora de la GPU) y permite la calibración entre ellos. Un dominio de tiempo representa una fuente de reloj o una base de tiempo específicas que se usan para medir marcas de tiempo. En Android, todas las marcas de tiempo pertinentes usan CLOCK_MONOTONIC, por lo que no es necesario utilizar varios dominios de tiempo.
  • Integración con VK_KHR_present_id2: Usa el VkPresentId2KHR estandarizado para identificar las solicitudes de presentación.

Habilita la extensión

Para usar VK_EXT_present_timing, debes habilitarlo y su requisito previo, VK_KHR_present_id2, durante la creación del dispositivo. También debes verificar la compatibilidad con las funciones del dispositivo físico:

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 falla alguna de estas verificaciones de funciones (presentTiming o presentId2 es falso), el dispositivo o el controlador no admiten VK_EXT_present_timing ni su requisito previo. En este caso, tu aplicación no puede usar VK_EXT_present_timing y debe recurrir a VK_GOOGLE_display_timing (si se admite) o depender de mecanismos predeterminados de cadencia de fotogramas, como Swappy.

Habilita la sincronización de la presentación en la cadena de intercambio

Cuando crees la cadena de intercambio, habilita explícitamente la sincronización de la presentación configurando la marca 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);

IDs de asociación presentes

Cuando presentes, asocia un ID único a cada fotograma con VkPresentId2KHR (parte de la extensión 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);

Consulta las propiedades de sincronización de la cadena de intercambio

Consulta las propiedades de sincronización de la cadena de intercambio, como la duración de la actualización, con 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 los dominios de tiempo admitidos

] Como se mencionó anteriormente, un dominio de tiempo representa una fuente de reloj o una base de tiempo específicas. Si bien VK_EXT_present_timing admite varios dominios de tiempo y permite la calibración entre ellos, no es necesario usar diferentes relojes en Android, ya que todas las marcas de tiempo relevantes usan CLOCK_MONOTONIC. Consulta los dominios de tiempo admitidos para tu cadena de intercambio:

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);

Hora de presentación objetivo de la solicitud

Para solicitar que se presente un fotograma en un momento específico, encadena una estructura VkPresentTimingInfoEXT a tu 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 se mencionó anteriormente, en Android, se ignora un targetTime de 0 o cualquier marca de tiempo de destino que sea más de 1 segundo en el futuro.

Consultar los tiempos de presentaciones anteriores

Para consultar información detallada sobre la sincronización de presentaciones anteriores, primero configura el tamaño de la cola de sincronización con vkSetSwapchainPresentTimingQueueSizeEXT y, luego, recupera los tiempos con 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
    }
}

Ejemplo de código

Para ver un ejemplo de integración completo y pruebas de conformidad para VK_EXT_present_timing, consulta las pruebas de deqp (Draw Elements Quality Program) en el repositorio de Conjunto de pruebas de compatibilidad (CTS) de Vulkan:

Pruebas de sincronización de presentación de CTS de Vulkan

Estas pruebas demuestran cómo configurar cadenas de intercambio para la sincronización de la presentación, establecer tiempos objetivo y verificar la precisión de las marcas de tiempo de presentación informadas en diferentes dominios de tiempo.