Rozszerzenia tempa klatek interfejsu Vulkan

Synchronizacja klatek ma kluczowe znaczenie dla płynności rozgrywki. Synchronizacja klatek zapewnia wyświetlanie klatek w regularnych odstępach czasu, co minimalizuje zacinanie się obrazu i opóźnienia wejściowe. Biblioteka Android Frame Pacing (Swappy) to zalecane rozwiązanie wysokiego poziomu dla większości gier, ale sterowanie niskiego poziomu jest dostępne za pomocą rozszerzeń Vulkan.

Aby uzyskać precyzyjną kontrolę nad prezentacją klatek, użyj tych rozszerzeń tempa klatek Vulkan:

  • VK_GOOGLE_display_timing: umożliwia planowanie wyświetlania klatek w określonych momentach i wykonywanie zapytań o poprzednie czasy wyświetlania w celu dostosowania pętli renderowania.
  • VK_EXT_present_timing: nowsze, standardowe rozszerzenie, które zapewnia kompleksowe informacje o czasie w przypadku żądań prezentacji. Zostało wprowadzone w Androidzie 17 (poziom API 37) i nowszych wersjach.

Rozszerzenie VK_GOOGLE_display_timing jest starsze i obsługiwane na większej liczbie urządzeń z Androidem. Jednak w przypadku kierowania na nowsze urządzenia preferowana jest wartość VK_EXT_present_timing, ponieważ oferuje więcej funkcji i bardziej szczegółowe informacje o czasie.

VK_GOOGLE_display_timing

Rozszerzenie VK_GOOGLE_display_timing umożliwia aplikacjom:

  1. Sprawdzanie czasu trwania cyklu odświeżania wyświetlacza
  2. Określ wybrany czas wyświetlania każdej klatki.
  3. Wykonywanie zapytań o rzeczywiste czasy wyświetlania poprzednich klatek w celu wdrożenia pętli informacji zwrotnej

To rozszerzenie jest przydatne w przypadku gier, które implementują własny algorytm synchronizacji klatek zamiast korzystać z Swappy.

Włącz rozszerzenie

Aby używać VK_GOOGLE_display_timing, włącz tę funkcję podczas tworzenia urządzenia Vulkan. Zanim włączysz rozszerzenie, sprawdź, czy jest ono obsługiwane przez urządzenie fizyczne:

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

Sprawdzanie czasu odświeżania wyświetlacza

Możesz sprawdzić czas odświeżania wyświetlacza powiązanego z łańcuchem wymiany za pomocą funkcji vkGetRefreshCycleDurationGOOGLE:

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

Planowanie prezentacji ramek

Aby określić, kiedy ma być wyświetlana ramka, dołącz strukturę VkPresentTimesInfoGOOGLE do łańcucha pNext struktury VkPresentInfoKHR podczas wywoływania funkcji 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);

Wartość desiredPresentTime powinna być sygnaturą czasową z zegara monotonicznego systemu (CLOCK_MONOTONIC). Oblicz tę docelową sygnaturę czasową na podstawie częstotliwości odświeżania wyświetlacza i rzeczywistych czasów wyświetlania poprzednich klatek. Na Androidzie wartość desiredPresentTime = 0 lub dowolna sygnatura czasowa w przyszłości o ponad 1 sekundę jest ignorowana.

VK_GOOGLE_display_timing zakłada, że wszystkie sygnatury czasowe pochodzą z tego samego zegara. Na Androidzie wszystkie sygnatury czasowe dotyczące zarówno VK_GOOGLE_display_timing, jak i VK_EXT_present_timing używają CLOCK_MONOTONIC. (Chociaż VK_EXT_present_timing obsługuje wiele domen czasowych, używanie różnych zegarów nie jest konieczne na Androidzie).

Zapytanie o poprzednie czasy prezentacji

Aby dostosować pętlę tempa klatek, użyj funkcji vkGetPastPresentationTimingGOOGLE, aby sprawdzić, kiedy były wyświetlane poprzednie klatki:

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

Porównując actualPresentTimedesiredPresentTime, możesz określić, czy klatki docierają za wcześnie czy za późno, i odpowiednio dostosować pętlę renderowania.

Przykładowy kod

Kompletny przykład integracji VK_GOOGLE_display_timing z renderem Vulkan znajdziesz w demonstracji sześcianu w repozytorium pakietu SDK do gier na Androida:

Prezentacja Vulkan Cube z pomiarem czasu wyświetlania

Ta prezentacja pokazuje, jak włączyć rozszerzenie, obliczyć docelowe czasy prezentacji i przetwarzać opinie na temat poprzednich czasów prezentacji, aby utrzymać stabilną liczbę klatek na sekundę.


VK_EXT_present_timing

Wprowadzone w Vulkanie i obsługiwane w Androidzie 17 i nowszych wersjach rozszerzenie VK_EXT_present_timing to standardowy, bardziej niezawodny sposób uzyskiwania szczegółowych informacji o wyświetlaniu klatek. Zastępuje i rozwija koncepcje opisane w VK_GOOGLE_display_timing.

Najważniejsze zalety VK_EXT_present_timing:

  • Standardized API: część oficjalnego zestawu rozszerzeń Khronos Vulkan.
  • Szczegółowe zapytania dotyczące etapów: umożliwiają sprawdzanie sygnatur czasowych na poszczególnych etapach potoku prezentacji (np. kiedy ramka została usunięta z kolejki, kiedy pierwszy piksel został wysłany na wyświetlacz i kiedy pierwszy piksel stał się widoczny).
  • Obsługa domen czasowych: obsługuje różne domeny czasowe (np. czas systemowy, czas GPU) i umożliwia ich kalibrację. Domena czasu reprezentuje konkretne źródło zegara lub bazę czasu używaną do pomiaru sygnatur czasowych. Na urządzeniach z Androidem wszystkie odpowiednie sygnatury czasowe używają formatu CLOCK_MONOTONIC, więc korzystanie z wielu domen czasowych nie jest konieczne.
  • Integracja z VK_KHR_present_id2: do identyfikowania żądań prezentacji używa standardowego identyfikatora VkPresentId2KHR.

Włącz rozszerzenie

Aby korzystać z usługi VK_EXT_present_timing, musisz włączyć ją i jej wymaganie wstępne, VK_KHR_present_id2, podczas tworzenia urządzenia. Sprawdź też, czy urządzenie fizyczne obsługuje te funkcje:

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

Jeśli którykolwiek z tych testów funkcji zakończy się niepowodzeniem (presentTiming lub presentId2 ma wartość false), urządzenie lub sterownik nie obsługuje VK_EXT_present_timing ani jego wymagań wstępnych. W takim przypadku aplikacja nie może używać VK_EXT_present_timing i powinna wrócić do VK_GOOGLE_display_timing (jeśli jest obsługiwane) lub korzystać z domyślnych mechanizmów synchronizacji klatek, takich jak Swappy.

Włącz prezentowanie czasu w łańcuchu wymiany

Podczas tworzenia łańcucha wymiany jawnie włącz czas prezentacji, ustawiając 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);

Powiązywanie identyfikatorów

Podczas prezentacji przypisz unikalny identyfikator do każdej ramki za pomocą VkPresentId2KHR (część rozszerzenia 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);

Właściwości czasu łańcucha wymiany zapytań

Pobierz właściwości czasu łańcucha wymiany, takie jak czas odświeżania, za pomocą funkcji 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

Wysyłanie zapytań do obsługiwanych domen czasu

]Jak wspomnieliśmy wcześniej, domena czasowa reprezentuje konkretne źródło zegara lub bazę czasu. Chociaż VK_EXT_present_timing obsługuje wiele domen czasowych i umożliwia kalibrację między nimi, korzystanie z różnych zegarów nie jest konieczne na Androidzie, ponieważ wszystkie odpowiednie sygnatury czasowe używają CLOCK_MONOTONIC. Wyślij zapytanie do obsługiwanych domen czasu dla łańcucha wymiany:

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

Żądanie docelowego czasu prezentacji

Aby zażądać wyświetlenia klatki w określonym czasie, połącz strukturę VkPresentTimingInfoEXTVkPresentInfoKHR.

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;

Jak wspomnieliśmy wcześniej, w Androidzie wartość targetTime równa 0 lub dowolna docelowa sygnatura czasowa w przyszłości o ponad sekundę jest ignorowana.

Sprawdzanie wcześniejszych czasów prezentacji

Aby wysłać zapytanie o szczegółowe informacje o czasie wyświetlania poprzednich prezentacji, najpierw skonfiguruj rozmiar kolejki czasu za pomocą funkcji vkSetSwapchainPresentTimingQueueSizeEXT, a potem pobierz czasy za pomocą funkcji 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
    }
}

Przykładowy kod

Pełny przykład integracji i testy zgodności dla VK_EXT_present_timing znajdziesz w deqp (Draw Elements Quality Program) w repozytorium Vulkan CTS (Compatibility Test Suite):

Testy czasu prezentacji w pakiecie Vulkan CTS

Te testy pokazują, jak skonfigurować łańcuchy wymiany pod kątem czasu prezentacji, ustawić czasy docelowe i sprawdzić dokładność zgłaszanych sygnatur czasowych prezentacji w różnych domenach czasowych.