Расширения синхронизации кадров Vulkan

Настройка частоты кадров имеет решающее значение для обеспечения плавной игры. Она гарантирует отображение кадров через равные промежутки времени, минимизируя рывки и задержку ввода. Хотя библиотека Android Frame Pacing (Swappy) является рекомендуемым высокоуровневым решением для большинства игр, низкоуровневое управление доступно через расширения Vulkan.

Для точного управления отображением кадров используйте следующие расширения Vulkan для регулировки частоты кадров:

  • VK_GOOGLE_display_timing : Позволяет планировать показ кадров в определенное время и запрашивать данные о времени показа в прошлом для корректировки цикла рендеринга.
  • VK_EXT_present_timing : Новое стандартизированное расширение, предоставляющее исчерпывающую информацию о времени выполнения запросов на презентацию, представленное в Android 17 (уровень API 37) и выше.

Расширение VK_GOOGLE_display_timing является более старым и поддерживается на более широком спектре устройств Android. Однако для более новых устройств предпочтительнее использовать VK_EXT_present_timing , поскольку оно предлагает больше функций и более подробную информацию о времени отображения.

VK_GOOGLE_display_timing

Расширение VK_GOOGLE_display_timing предоставляет приложениям возможность:

  1. Запрос продолжительности цикла обновления дисплея.
  2. Укажите желаемое время показа для каждого кадра.
  3. Для реализации обратной связи запросите фактическое время отображения предыдущих кадров.

Это расширение полезно для игр, которые используют собственный алгоритм синхронизации кадров вместо Swappy.

Включите расширение

Чтобы использовать VK_GOOGLE_display_timing , включите его при создании устройства Vulkan. Перед включением расширения убедитесь, что оно поддерживается физическим устройством:

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

Продолжительность обновления отображения запроса

Вы можете запросить продолжительность обновления отображения, связанного с цепочкой обменов, используя vkGetRefreshCycleDurationGOOGLE :

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

Презентация графика

Чтобы указать, когда следует отображать кадр, при вызове vkQueuePresentKHR добавьте структуру VkPresentTimesInfoGOOGLE к цепочке pNext объекта VkPresentInfoKHR :

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 должен представлять собой метку времени из монотонных часов системы ( CLOCK_MONOTONIC ). Целевое время рассчитывается на основе частоты обновления дисплея и фактического времени отображения предыдущих кадров. На Android значение desiredPresentTime , равное 0, или любая метка времени, отстоящая более чем на 1 секунду в будущем, игнорируется.

VK_GOOGLE_display_timing предполагает, что все метки времени исходят из одних и тех же часов. На Android все метки времени, относящиеся как к VK_GOOGLE_display_timing , так и VK_EXT_present_timing используют CLOCK_MONOTONIC . (Хотя VK_EXT_present_timing поддерживает несколько временных диапазонов, использование разных часов на Android не является обязательным).

Запрос времени проведения предыдущих презентаций

Чтобы настроить цикл синхронизации кадров, используйте vkGetPastPresentationTimingGOOGLE для запроса фактического времени отображения предыдущих кадров:

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

Сравнивая actualPresentTime с desiredPresentTime , вы можете определить, поступают ли кадры слишком рано или слишком поздно, и соответствующим образом скорректировать цикл рендеринга.

Пример кода

Полный рабочий пример интеграции VK_GOOGLE_display_timing в рендерер Vulkan можно найти в демонстрации куба в репозитории Android Game SDK:

Демонстрация Vulkan Cube с отображением таймингов

В этой демонстрации показано, как включить расширение, рассчитать целевое время показа и обработать данные о времени предыдущих показов для поддержания стабильной частоты кадров.


VK_EXT_present_timing

Расширение VK_EXT_present_timing представленное в Vulkan и поддерживаемое в Android 17 и выше, представляет собой стандартизированный и более надежный способ получения подробной информации о отображении кадров. Оно заменяет и расширяет концепции, представленные в VK_GOOGLE_display_timing .

Основные преимущества VK_EXT_present_timing включают в себя:

  • Стандартизированный API : часть официального набора расширений Khronos Vulkan.
  • Подробные запросы по этапам : позволяют запрашивать временные метки на определенных этапах конвейера обработки изображений (например, когда кадр был извлечен из очереди, когда первый пиксель был отправлен на экран и когда первый пиксель стал видимым).
  • Поддержка временных доменов : Поддерживает различные временные домены (например, системное время, время графического процессора) и позволяет калибровать их. Временной домен представляет собой конкретный источник тактового сигнала или временную базу, используемую для измерения временных меток. На Android все соответствующие временные метки используют CLOCK_MONOTONIC , поэтому использование нескольких временных доменов не требуется.
  • Интеграция с VK_KHR_present_id2 : использует стандартизированный идентификатор VkPresentId2KHR для идентификации запросов на презентацию.

Включите расширение

Для использования VK_EXT_present_timing необходимо включить его и его предварительный параметр VK_KHR_present_id2 при создании устройства. Также следует проверить наличие поддержки физических функций устройства:

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

Если хотя бы одна из этих проверок функций не пройдена ( presentTiming или presentId2 имеют значение false), устройство или драйвер не поддерживают VK_EXT_present_timing или его необходимые параметры. В этом случае ваше приложение не сможет использовать VK_EXT_present_timing и должно будет вернуться к VK_GOOGLE_display_timing (если поддерживается) или использовать механизмы синхронизации кадров по умолчанию, такие как Swappy.

Включите отображение текущего времени в свопчейне.

При создании цепочки обменов явно включите отображение времени присутствия, установив флаг 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);

Сопоставьте существующие идентификаторы

При презентации свяжите каждому кадру уникальный идентификатор, используя VkPresentId2KHR (часть расширения 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);

Запрос свойств синхронизации цепочки обменов

Запрос параметров синхронизации цепочки обменов, таких как длительность обновления, осуществляется с помощью функции 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

Запросы поддерживают временные домены

Как отмечалось ранее, временная область представляет собой конкретный источник тактового сигнала или временную базу. Хотя VK_EXT_present_timing поддерживает несколько временных областей и позволяет калибровать их, использование разных тактовых сигналов на Android не требуется, поскольку все соответствующие метки времени используют CLOCK_MONOTONIC . Запросите список поддерживаемых временных областей для вашего свопчейна:

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

Запросить целевое время презентации

Чтобы запросить отображение кадра в определенное время, добавьте структуру VkPresentTimingInfoEXT к вашей 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;

Как отмечалось ранее, на Android targetTime , равное 0, или любая целевая метка времени, отстоящая более чем на 1 секунду в будущем, игнорируется.

Запрос информации о времени проведения предыдущих презентаций.

Для получения подробной информации о времени проведения прошлых презентаций сначала настройте размер очереди времени с помощью функции vkSetSwapchainPresentTimingQueueSizeEXT , а затем получите данные о времени с помощью функции 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
    }
}

Пример кода

Полный пример интеграции и тесты на соответствие стандарту VK_EXT_present_timing см. в тестах deqp (Draw Elements Quality Program) в репозитории Vulkan CTS (Compatibility Test Suite):

Vulkan CTS представляет тесты времени выполнения.

Эти тесты демонстрируют, как настроить цепочки обмена для текущего времени, установить целевое время и проверить точность сообщаемых временных меток представления в различных временных диапазонах.