إضافات Vulkan لتحديد عدد اللقطات في الثانية

يُعدّ ضبط سرعة عرض اللقطات أمرًا بالغ الأهمية لتقديم تجربة لعب سلسة. يضمن معدّل عرض اللقطات عرض اللقطات على فترات منتظمة، ما يقلّل من التقطّع وزمن استجابة الإدخال. على الرغم من أنّ مكتبة Android Frame Pacing (Swappy) هي الحلّ المقترَح عالي المستوى لمعظم الألعاب، إلا أنّ التحكّم المنخفض المستوى متاح من خلال إضافات Vulkan.

استخدِم إضافات Vulkan التالية لتحديد سرعة عرض اللقطات والتحكّم بدقة في عرض اللقطات:

  • VK_GOOGLE_display_timing: تتيح جدولة عرض اللقطات في أوقات معيّنة والاستعلام عن أوقات العرض السابقة لتعديل حلقة العرض
  • VK_EXT_present_timing: إضافة أحدث وموحّدة تقدّم ملاحظات شاملة حول التوقيت لطلبات العرض، وتم طرحها في Android 17 (مستوى واجهة برمجة التطبيقات 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

جدولة عرض اللقطات

لتحديد وقت عرض إطار، أرفِق بنية VkPresentTimesInfoGOOGLE بسلسلة pNext من VkPresentInfoKHR عند استدعاء 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 طابعًا زمنيًا من ساعة النظام الرتيبة (CLOCK_MONOTONIC). احسب هذا الوقت المستهدف استنادًا إلى معدّل تجديد الشاشة وأوقات العرض الفعلية للإطارات السابقة. على Android، يتم تجاهل desiredPresentTime بقيمة 0 أو أي طابع زمني يزيد عن ثانية واحدة في المستقبل.

يفترض 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 مع توقيت العرض

يوضّح هذا العرض التوضيحي كيفية تفعيل الإضافة واحتساب أوقات العرض المستهدَفة ومعالجة الملاحظات الواردة من توقيتات العرض السابقة للحفاظ على عدد اللقطات في الثانية ثابت.


VK_EXT_present_timing

تم طرح الإضافة VK_EXT_present_timing في Vulkan، وهي متوافقة مع الإصدار 17 من نظام التشغيل Android والإصدارات الأحدث. وتوفّر هذه الإضافة طريقة موحّدة وأكثر فعالية للحصول على ملاحظات تفصيلية حول عرض اللقطات، كما أنّها تحلّ محل المفاهيم الواردة في VK_GOOGLE_display_timing وتوسّع نطاقها.

تشمل المزايا الرئيسية لـ VK_EXT_present_timing ما يلي:

  • واجهة برمجة التطبيقات الموحّدة: جزء من مجموعة إضافات 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);

Query swapchain timing properties

يمكنك طلب خصائص توقيت سلسلة التبديل، مثل مدة إعادة التحميل، باستخدام 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 أو أي طابع زمني مستهدف يزيد عن ثانية واحدة في المستقبل.

الاستعلام عن أوقات العرض التقديمي السابقة

للاستعلام عن معلومات مفصّلة حول توقيت العروض التقديمية السابقة، عليك أولاً ضبط حجم قائمة انتظار التوقيت باستخدام 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) الخاصة بـ Vulkan

توضّح هذه الاختبارات كيفية ضبط سلاسل التبديل لتحديد توقيت العرض، وتحديد الأوقات المستهدَفة، والتحقّق من دقة الطوابع الزمنية للعرض المُسجّلة على مستوى نطاقات زمنية مختلفة.