افزونه‌های تنظیم سرعت فریم ولکان

تنظیم سرعت فریم برای ارائه یک تجربه بازی روان بسیار مهم است. تنظیم سرعت فریم تضمین می‌کند که فریم‌ها در فواصل منظم نمایش داده شوند و پرش و تأخیر ورودی را به حداقل برسانند. در حالی که کتابخانه تنظیم سرعت فریم اندروید (Swappy) راه‌حل سطح بالای پیشنهادی برای اکثر بازی‌ها است، کنترل سطح پایین از طریق افزونه‌های Vulkan در دسترس است.

برای کنترل دقیق بر نمایش فریم، از افزونه‌های تنظیم سرعت فریم Vulkan زیر استفاده کنید:

  • VK_GOOGLE_display_timing : امکان زمان‌بندی ارائه فریم‌ها در زمان‌های خاص و پرس‌وجو از زمان‌های ارائه قبلی برای تنظیم حلقه رندر را فراهم می‌کند.
  • VK_EXT_present_timing : یک افزونه جدیدتر و استاندارد که بازخورد زمان‌بندی جامعی را برای درخواست‌های ارائه ارائه می‌دهد و در اندروید ۱۷ (سطح API ۳۷) و بالاتر معرفی شده است.

افزونه‌ی VK_GOOGLE_display_timing قدیمی‌تر است و در طیف وسیع‌تری از دستگاه‌های اندروید پشتیبانی می‌شود. با این حال، VK_EXT_present_timing هنگام هدف قرار دادن دستگاه‌های جدیدتر ترجیح داده می‌شود زیرا ویژگی‌های بیشتر و اطلاعات زمان‌بندی دقیق‌تری را ارائه می‌دهد.

زمان‌بندی نمایش VK_GOOGLE

افزونه‌ی 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);
}

مدت زمان تازه‌سازی نمایش پرس‌وجو

شما می‌توانید مدت زمان به‌روزرسانی نمایشگر مرتبط با یک swapchain را با استفاده از 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 ) باشد. این زمان هدف را بر اساس نرخ تازه‌سازی صفحه نمایش و زمان‌های واقعی ارائه فریم‌های گذشته محاسبه کنید. در اندروید، زمان desiredPresentTime 0 یا هر مهر زمانی بیش از 1 ثانیه در آینده نادیده گرفته می‌شود.

VK_GOOGLE_display_timing فرض می‌کند که همه مهرهای زمانی از یک ساعت سرچشمه می‌گیرند. در اندروید، همه مهرهای زمانی مربوط به هر دو VK_GOOGLE_display_timing و VK_EXT_present_timing CLOCK_MONOTONIC استفاده می‌کنند. (در حالی که VK_EXT_present_timing از چندین دامنه زمانی پشتیبانی می‌کند، استفاده از ساعت‌های مختلف در اندروید ضروری نیست).

زمان ارائه‌های گذشته را استعلام کنید

برای تنظیم حلقه‌ی سرعت فریم، از 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 مراجعه کنید:

نسخه آزمایشی مکعب ولکان با زمان‌بندی نمایش

این نسخه آزمایشی نشان می‌دهد که چگونه می‌توان این افزونه را فعال کرد، زمان‌های ارائه هدف را محاسبه کرد و بازخورد زمان‌بندی‌های ارائه‌های قبلی را برای حفظ نرخ فریم پایدار پردازش کرد.


VK_EXT_present_timing

افزونه‌ی VK_EXT_present_timing که در Vulkan معرفی شده و در اندروید ۱۷ و بالاتر پشتیبانی می‌شود، روشی استاندارد و قوی‌تر برای دریافت بازخورد دقیق در مورد نمایش فریم است. این افزونه جایگزین و گسترش‌دهنده‌ی مفاهیم VK_GOOGLE_display_timing است.

مزایای کلیدی VK_EXT_present_timing عبارتند از:

  • API استاندارد : بخشی از مجموعه افزونه‌های رسمی Khronos Vulkan
  • پرس‌وجوهای مرحله‌ای دقیق : امکان پرس‌وجوی مهرهای زمانی را در مراحل خاصی از فرآیند ارائه فراهم می‌کند (برای مثال، زمانی که فریم از صف خارج شد، زمانی که اولین پیکسل به نمایشگر ارسال شد و زمانی که اولین پیکسل قابل مشاهده شد).
  • پشتیبانی از دامنه زمانی : از دامنه‌های زمانی مختلف (مثلاً زمان سیستم، زمان GPU) پشتیبانی می‌کند و امکان کالیبراسیون بین آنها را فراهم می‌کند. یک دامنه زمانی نشان‌دهنده یک منبع ساعت یا پایه زمانی خاص است که برای اندازه‌گیری مهرهای زمانی استفاده می‌شود. در اندروید، همه مهرهای زمانی مربوطه 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، تکیه کند.

فعال کردن زمان‌بندی فعلی در swapchain

هنگام ایجاد swapchain، با تنظیم پرچم 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);

پرس و جو در مورد ویژگی‌های زمان‌بندی swapchain

با استفاده از vkGetSwapchainTimingPropertiesEXT ، ویژگی‌های زمان‌بندی swapchain، مانند مدت زمان به‌روزرسانی را جستجو کنید:

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 از چندین دامنه زمانی پشتیبانی می‌کند و امکان کالیبراسیون بین آنها را فراهم می‌کند، استفاده از ساعت‌های مختلف در اندروید ضروری نیست زیرا همه مهرهای زمانی مربوطه CLOCK_MONOTONIC استفاده می‌کنند. دامنه‌های زمانی پشتیبانی شده برای swapchain خود را جستجو کنید:

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;

همانطور که قبلاً اشاره شد، در اندروید، targetTime برابر با ۰ یا هر target timestamp بیش از ۱ ثانیه در آینده نادیده گرفته می‌شود.

زمان ارائه‌های گذشته را استعلام کنید

برای جستجوی اطلاعات دقیق زمان‌بندی برای ارائه‌های گذشته، ابتدا اندازه صف زمان‌بندی را با استفاده از 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 (برنامه‌ی کیفیت عناصر ترسیمی) در مخزن Vulkan CTS (مجموعه‌ی تست‌های سازگاری) مراجعه کنید:

ولکان CTS تست‌های زمان‌بندی را ارائه می‌دهد

این آزمایش‌ها نشان می‌دهند که چگونه می‌توان swapchainها را برای زمان‌بندی فعلی پیکربندی کرد، زمان‌های هدف را تعیین کرد و دقت مهرهای زمانی ارائه گزارش‌شده را در حوزه‌های زمانی مختلف تأیید کرد.