ส่วนขยายการควบคุมอัตราเฟรมของ Vulkan

การเว้นระยะเฟรมเป็นสิ่งสำคัญในการมอบประสบการณ์การเล่นเกมที่ราบรื่น การเว้นระยะเฟรมช่วยให้มั่นใจได้ว่า เฟรมจะแสดงในช่วงเวลาปกติ ซึ่งจะช่วยลดอาการกระตุกและเวลาในการตอบสนองต่ออินพุต แม้ว่าไลบรารีการเว้นระยะเฟรมของ Android (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);
}

ระยะเวลาการรีเฟรชการแสดงผลของคำค้นหา

คุณสามารถค้นหาระยะเวลารีเฟรชของจอแสดงผลที่เชื่อมโยงกับ Swapchain ได้โดยใช้ 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 หรือการประทับเวลาใดๆ ที่มากกว่า 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 พร้อมการกำหนดเวลาการแสดงผล

การสาธิตนี้แสดงวิธีเปิดใช้ส่วนขยาย คำนวณเวลาเป้าหมายในการนำเสนอ และประมวลผลความคิดเห็นจากเวลาในการนำเสนอที่ผ่านมาเพื่อรักษาอัตราเฟรมที่เสถียร


VK_EXT_present_timing

เปิดตัวใน Vulkan และรองรับใน Android 17 ขึ้นไป ส่วนขยาย VK_EXT_present_timing เป็นวิธีที่ได้มาตรฐานและมีประสิทธิภาพมากขึ้นในการรับ ความคิดเห็นโดยละเอียดเกี่ยวกับการนำเสนอเฟรม โดยจะมาแทนที่และขยายแนวคิดใน VK_GOOGLE_display_timing

ข้อดีหลักๆ ของ VK_EXT_present_timing มีดังนี้

  • API มาตรฐาน: ส่วนหนึ่งของชุดส่วนขยาย Vulkan อย่างเป็นทางการของ Khronos
  • การค้นหาระดับละเอียด: อนุญาตให้ค้นหาการประทับเวลาในขั้นตอนที่เฉพาะเจาะจง ของไปป์ไลน์การนำเสนอ (เช่น เมื่อมีการนำเฟรมออกจากคิว เมื่อ ส่งพิกเซลแรกไปยังจอแสดงผล และเมื่อพิกเซลแรกปรากฏ ให้เห็น)
  • รองรับโดเมนเวลา: รองรับโดเมนเวลาต่างๆ (เช่น เวลาของระบบ เวลาของ GPU) และอนุญาตให้ปรับเทียบระหว่างโดเมนเวลาเหล่านั้น โดเมนเวลา แสดงถึงแหล่งที่มาของนาฬิกาหรือฐานเวลาที่เฉพาะเจาะจงซึ่งใช้ในการวัดการประทับเวลา ใน 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 เป็นเท็จ) แสดงว่าอุปกรณ์หรือไดรเวอร์ไม่รองรับ 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

สอบถามพร็อพเพอร์ตี้การกำหนดเวลาของ Swapchain เช่น ระยะเวลาการรีเฟรช โดยใช้ 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 ค้นหาโดเมนเวลาที่รองรับสำหรับ 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);

ขอเวลาเป้าหมายในการนำเสนอ

หากต้องการขอให้แสดงเฟรมในเวลาที่เฉพาะเจาะจง ให้เชื่อมต่อโครงสร้าง a 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 (โปรแกรมคุณภาพขององค์ประกอบการวาด) ในที่เก็บ Vulkan CTS (ชุดเครื่องมือทดสอบความเข้ากันได้)

การทดสอบเวลาในการนำเสนอของ CTS สำหรับ Vulkan

การทดสอบเหล่านี้แสดงให้เห็นวิธีกำหนดค่า Swapchain สำหรับเวลาปัจจุบัน ตั้งค่าเวลาเป้าหมาย และยืนยันความถูกต้องของไทม์สแตมป์การนำเสนอที่รายงานในโดเมนเวลาต่างๆ