Ekstensi pengaturan kecepatan frame Vulkan

Pacing frame sangat penting untuk memberikan pengalaman bermain game yang lancar. Pacing frame memastikan bahwa frame ditampilkan pada interval reguler, sehingga meminimalkan stutter dan latensi input. Meskipun library Android Frame Pacing (Swappy) adalah solusi tingkat tinggi yang direkomendasikan untuk sebagian besar game, kontrol tingkat rendah tersedia melalui ekstensi Vulkan.

Gunakan ekstensi pengaturan kecepatan frame Vulkan berikut untuk mendapatkan kontrol yang presisi atas presentasi frame:

  • VK_GOOGLE_display_timing: Memungkinkan penjadwalan frame untuk ditampilkan pada waktu tertentu dan membuat kueri waktu presentasi sebelumnya untuk menyesuaikan loop rendering
  • VK_EXT_present_timing: Ekstensi baru yang distandardisasi yang memberikan masukan pengaturan waktu yang komprehensif untuk permintaan presentasi, yang diperkenalkan di Android 17 (level API 37) dan yang lebih tinggi

Ekstensi VK_GOOGLE_display_timing lebih lama dan didukung di berbagai perangkat Android. Namun, VK_EXT_present_timing lebih disukai saat menargetkan perangkat yang lebih baru karena menawarkan lebih banyak fitur dan informasi waktu yang lebih mendetail.

VK_GOOGLE_display_timing

Ekstensi VK_GOOGLE_display_timing menyediakan cara bagi aplikasi untuk:

  1. Membuat kueri durasi siklus refresh layar
  2. Tentukan waktu presentasi yang dipilih untuk setiap frame
  3. Kueri waktu presentasi sebenarnya dari frame sebelumnya untuk menerapkan loop masukan

Ekstensi ini berguna untuk game yang menerapkan algoritma pengaturan kecepatan frame sendiri, bukan menggunakan Swappy.

Mengaktifkan ekstensi

Untuk menggunakan VK_GOOGLE_display_timing, aktifkan saat membuat perangkat Vulkan. Sebelum mengaktifkan ekstensi, pastikan ekstensi didukung oleh perangkat fisik:

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

Kueri durasi refresh layar

Anda dapat membuat kueri durasi refresh layar yang terkait dengan swapchain menggunakan vkGetRefreshCycleDurationGOOGLE:

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

Menjadwalkan presentasi frame

Untuk menentukan kapan frame harus ditampilkan, lampirkan struktur VkPresentTimesInfoGOOGLE ke rantai pNext dari VkPresentInfoKHR saat memanggil 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);

desiredPresentTime harus berupa stempel waktu dari clock monotonik sistem (CLOCK_MONOTONIC). Hitung waktu target ini berdasarkan kecepatan refresh layar dan waktu presentasi sebenarnya dari frame sebelumnya. Di Android, desiredPresentTime 0 atau stempel waktu apa pun yang lebih dari 1 detik di masa mendatang akan diabaikan.

VK_GOOGLE_display_timing mengasumsikan semua stempel waktu berasal dari jam yang sama. Di Android, semua stempel waktu yang relevan dengan VK_GOOGLE_display_timing dan VK_EXT_present_timing menggunakan CLOCK_MONOTONIC. (Meskipun VK_EXT_present_timing mendukung beberapa domain waktu, penggunaan clock yang berbeda tidak diperlukan di Android).

Mengueri waktu presentasi sebelumnya

Untuk menyesuaikan loop pengaturan frame, gunakan vkGetPastPresentationTimingGOOGLE untuk mengueri kapan frame sebelumnya benar-benar ditampilkan:

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

Dengan membandingkan actualPresentTime dengan desiredPresentTime, Anda dapat menentukan apakah frame tiba terlalu awal atau terlalu lambat dan menyesuaikan loop rendering Anda dengan tepat.

Contoh kode

Untuk contoh lengkap cara mengintegrasikan VK_GOOGLE_display_timing ke dalam perender Vulkan, lihat demo kubus di repositori Android Game SDK:

Demo Vulkan Cube dengan Pengaturan Waktu Tampilan

Demo ini menunjukkan cara mengaktifkan ekstensi, menghitung waktu presentasi target, dan memproses masukan dari pengaturan waktu presentasi sebelumnya untuk mempertahankan kecepatan frame yang stabil.


VK_EXT_present_timing

Diperkenalkan di Vulkan dan didukung di Android 17 dan yang lebih tinggi, ekstensi VK_EXT_present_timing adalah cara yang lebih andal dan standar untuk mendapatkan masukan mendetail tentang presentasi frame. Bagian ini menggantikan dan memperluas konsep dalam VK_GOOGLE_display_timing.

Keuntungan utama VK_EXT_present_timing meliputi:

  • API Standar: Bagian dari set ekstensi Vulkan Khronos resmi
  • Kueri Tahap Mendetail: Memungkinkan kueri stempel waktu pada tahap tertentu pipeline presentasi (misalnya, saat frame dikeluarkan dari antrean, saat piksel pertama dikirim ke layar, dan saat piksel pertama menjadi terlihat)
  • Dukungan Domain Waktu: Mendukung domain waktu yang berbeda (misalnya, waktu sistem, waktu GPU) dan memungkinkan kalibrasi di antara keduanya. Domain waktu mewakili sumber clock atau basis waktu tertentu yang digunakan untuk mengukur stempel waktu. Di Android, semua stempel waktu yang relevan menggunakan CLOCK_MONOTONIC, sehingga tidak perlu menggunakan beberapa domain waktu.
  • Integrasi dengan VK_KHR_present_id2: Menggunakan VkPresentId2KHR yang telah distandardisasi untuk mengidentifikasi permintaan presentasi

Mengaktifkan ekstensi

Untuk menggunakan VK_EXT_present_timing, Anda harus mengaktifkannya dan prasyaratnya, VK_KHR_present_id2, selama pembuatan perangkat. Anda juga harus memeriksa dukungan fitur perangkat fisik:

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

Jika salah satu pemeriksaan fitur ini gagal (presentTiming atau presentId2 salah), perangkat atau driver tidak mendukung VK_EXT_present_timing atau prasyaratnya. Dalam hal ini, aplikasi Anda tidak dapat menggunakan VK_EXT_present_timing dan harus melakukan penggantian ke VK_GOOGLE_display_timing (jika didukung) atau mengandalkan mekanisme kecepatan frame default, seperti Swappy.

Mengaktifkan pengaturan waktu present di swapchain

Saat membuat swapchain, aktifkan waktu presentasi secara eksplisit dengan menyetel tanda 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);

ID yang terkait

Saat menampilkan, kaitkan ID unik dengan setiap frame menggunakan VkPresentId2KHR (bagian dari ekstensi 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);

Mengkueri properti pengaturan waktu swapchain

Mengkueri properti pengaturan waktu swapchain, seperti durasi refresh, menggunakan 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

Kueri domain waktu yang didukung

] Seperti yang disebutkan sebelumnya, domain waktu merepresentasikan sumber clock atau basis waktu tertentu. Meskipun VK_EXT_present_timing mendukung beberapa domain waktu dan memungkinkan kalibrasi di antaranya, penggunaan jam yang berbeda tidak diperlukan di Android karena semua stempel waktu yang relevan menggunakan CLOCK_MONOTONIC. Kueri domain waktu yang didukung untuk swapchain Anda:

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

Meminta waktu presentasi target

Untuk meminta agar frame ditampilkan pada waktu tertentu, hubungkan struktur VkPresentTimingInfoEXT ke VkPresentInfoKHR Anda.

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;

Seperti yang disebutkan sebelumnya, di Android, targetTime 0 atau stempel waktu target yang lebih dari 1 detik di masa mendatang akan diabaikan.

Mengueri waktu presentasi sebelumnya

Untuk membuat kueri informasi pengaturan waktu mendetail untuk presentasi sebelumnya, pertama-tama konfigurasi ukuran antrean pengaturan waktu menggunakan vkSetSwapchainPresentTimingQueueSizeEXT, lalu ambil pengaturan waktu menggunakan 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
    }
}

Contoh kode

Untuk contoh integrasi lengkap dan pengujian kepatuhan untuk VK_EXT_present_timing, lihat pengujian deqp (Draw Elements Quality Program) di repositori Vulkan CTS (Compatibility Test Suite):

Pengujian Waktu Presentasi CTS Vulkan

Pengujian ini menunjukkan cara mengonfigurasi swapchain untuk pengaturan waktu presentasi, menetapkan waktu target, dan memverifikasi akurasi stempel waktu presentasi yang dilaporkan di berbagai domain waktu.