Vulkan フレーム ペーシング拡張機能

スムーズなゲーム エクスペリエンスを提供するには、フレーム ペーシングが不可欠です。フレーム ペーシングにより、フレームが一定の間隔で表示されるため、カクつきや入力レイテンシが最小限に抑えられます。Android Frame Pacing ライブラリ(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);
}

ディスプレイの更新期間をクエリする

vkGetRefreshCycleDurationGOOGLE を使用して、スワップチェーンに関連付けられたディスプレイの更新期間をクエリできます。 vkGetRefreshCycleDurationGOOGLE

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

フレーム表示をスケジュール設定する

フレームを表示するタイミングを指定するには、 VkPresentTimesInfoGOOGLE 構造体を pNext チェーンの VkPresentInfoKHR when calling 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_timingVK_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
    }
}

actualPresentTimedesiredPresentTime と比較することで、フレームの到着が早すぎるか遅すぎるかを判断し、それに応じてレンダリング ループを調整できます。

サンプルコード

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: 公式の Khronos Vulkan 拡張機能セットの一部
  • 詳細なステージクエリ: 表示パイプラインの特定のステージ (フレームがデキューされたとき、最初のピクセルがディスプレイに送信されたとき、最初のピクセルが表示されたときなど)でタイムスタンプをクエリできます。
  • 時間ドメインのサポート: さまざまな時間ドメイン(たとえば、 システム時間、GPU 時間)をサポートし、それらの間でキャリブレーションを行うことができます。時間ドメインは、タイムスタンプの測定に使用される特定のクロックソースまたはタイムベースを表します。 Android では、関連するすべてのタイムスタンプで CLOCK_MONOTONIC が使用されるため、複数の時間ドメインを使用する必要はありません。
  • VK_KHR_present_id2 との統合: 標準化された VkPresentId2KHR を使用して表示リクエストを識別します。

拡張機能を有効にする

VK_EXT_present_timing を使用するには、デバイスの作成時に、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);

表示 ID を関連付ける

表示時に、VkPresentId2KHRVK_KHR_present_id2 拡張機能の一部)を使用して、各フレームに一意の ID を関連付けます。

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

スワップチェーンのタイミング プロパティをクエリする

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 は複数の時間ドメインをサポートし、それらの間でキャリブレーションを行うことができますが、関連するすべてのタイムスタンプで CLOCK_MONOTONIC が使用されるため、Android では異なるクロックを使用する必要はありません。スワップチェーンでサポートされている時間ドメインをクエリします。

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 の場合、または 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 の完全な統合例と適合性テストについては、Vulkan CTS(互換性テストスイート)リポジトリの deqp(Draw Elements Quality Program) テストをご覧ください。

Vulkan CTS 表示タイミング テスト

これらのテストでは、表示タイミング用にスワップチェーンを構成する方法、ターゲット時刻を設定する方法、さまざまな時間ドメインで報告された表示タイムスタンプの精度を確認する方法を示します。