Vulkan-Erweiterungen für die Frame-Abstimmung

Die Frame-Abstimmung ist entscheidend für ein reibungsloses Spielerlebnis. Sie sorgt dafür, dass Frames in regelmäßigen Abständen angezeigt werden, wodurch Ruckeln und Eingabelatenz minimiert werden. Die Android Frame Pacing Library (Swappy) ist die empfohlene Lösung auf hoher Ebene für die meisten Spiele. Über Vulkan-Erweiterungen ist jedoch auch eine Steuerung auf niedriger Ebene möglich.

Mit den folgenden Vulkan-Erweiterungen für die Frame-Abstimmung können Sie die Frame-Präsentation präzise steuern:

  • VK_GOOGLE_display_timing: Ermöglicht das Planen von Frames für die Präsentation zu bestimmten Zeiten und das Abfragen vergangener Präsentationszeiten, um die Rendering-Schleife anzupassen.
  • VK_EXT_present_timing: Eine neuere, standardisierte Erweiterung, die umfassendes Timing-Feedback für Präsentationsanfragen bietet. Sie wurde in Android 17 (API-Level 37) und höher eingeführt.

Die Erweiterung VK_GOOGLE_display_timing ist älter und wird auf einer größeren Anzahl von Android-Geräten unterstützt. VK_EXT_present_timing wird jedoch für neuere Geräte bevorzugt, da sie mehr Funktionen und detailliertere Timing-Informationen bietet.

VK_GOOGLE_display_timing

Mit der Erweiterung VK_GOOGLE_display_timing können Anwendungen Folgendes tun:

  1. Die Dauer des Aktualisierungszyklus eines Displays abfragen
  2. Eine ausgewählte Präsentationszeit für jeden Frame angeben
  3. Die tatsächlichen Präsentationszeiten vergangener Frames abfragen, um eine Feedbackschleife zu implementieren

Diese Erweiterung ist nützlich für Spiele, die ihren eigenen Frame-Abstimmungsalgorithmus implementieren, anstatt Swappy zu verwenden.

Erweiterung aktivieren

Wenn Sie VK_GOOGLE_display_timing verwenden möchten, aktivieren Sie sie beim Erstellen des Vulkan-Geräts. Prüfen Sie vor dem Aktivieren der Erweiterung, ob sie vom physischen Gerät unterstützt wird:

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

Dauer der Displayaktualisierung abfragen

Mit vkGetRefreshCycleDurationGOOGLE können Sie die Aktualisierungsdauer des Displays abfragen, das mit einer Swapchain verknüpft ist:

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

Frame-Präsentation planen

Wenn Sie angeben möchten, wann ein Frame angezeigt werden soll, fügen Sie der VkPresentTimesInfoGOOGLE-Struktur der pNextKette von VkPresentInfoKHR hinzu, wenn Sie vkQueuePresentKHRaufrufen:

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

Die desiredPresentTime sollte ein Zeitstempel aus der monotonen Uhr des Systems (CLOCK_MONOTONIC) sein. Berechnen Sie diese Zielzeit anhand der Aktualisierungsrate des Displays und der tatsächlichen Präsentationszeiten vergangener Frames. Unter Android wird eine desiredPresentTime von 0 oder ein Zeitstempel, der mehr als 1 Sekunde in der Zukunft liegt, ignoriert.

Bei VK_GOOGLE_display_timing wird davon ausgegangen, dass alle Zeitstempel von derselben Uhr stammen. Unter Android verwenden alle Zeitstempel, die sowohl für VK_GOOGLE_display_timing als auch für VK_EXT_present_timing relevant sind, CLOCK_MONOTONIC. (VK_EXT_present_timing unterstützt zwar mehrere Zeitbereiche, die Verwendung verschiedener Uhren ist unter Android jedoch nicht erforderlich.)

Vergangene Präsentationszeiten abfragen

Verwenden Sie vkGetPastPresentationTimingGOOGLE, um abzufragen, wann vergangene Frames tatsächlich angezeigt wurden, und so die Frame-Abstimmungsschleife anzupassen:

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

Wenn Sie actualPresentTime mit desiredPresentTime vergleichen, können Sie feststellen, ob Frames zu früh oder zu spät eintreffen, und die Rendering-Schleife entsprechend anpassen.

Codebeispiel

Ein vollständiges funktionierendes Beispiel für die Integration von VK_GOOGLE_display_timing in einen Vulkan-Renderer finden Sie in der Cube-Demo im Android Game SDK-Repository:

Vulkan Cube Demo with Display Timing

In dieser Demo wird gezeigt, wie Sie die Erweiterung aktivieren, Zielpräsentationszeiten berechnen und Feedback aus vergangenen Präsentationszeiten verarbeiten, um eine stabile Framerate aufrechtzuerhalten.


VK_EXT_present_timing

Die Erweiterung VK_EXT_present_timing wurde in Vulkan eingeführt und wird in Android 17 und höher unterstützt. Sie bietet eine standardisierte und robustere Möglichkeit, detailliertes Feedback zur Frame-Präsentation zu erhalten. Sie ersetzt und erweitert die Konzepte in VK_GOOGLE_display_timing.

Zu den wichtigsten Vorteilen von VK_EXT_present_timing gehören:

  • Standardisierte API: Teil des offiziellen Khronos Vulkan-Erweiterungssatzes
  • Detaillierte Phasenabfragen: Ermöglicht das Abfragen von Zeitstempeln in bestimmten Phasen der Präsentationspipeline, z. B. wenn der Frame aus der Warteschlange entfernt wurde, wenn das erste Pixel an das Display gesendet wurde und wenn das erste Pixel sichtbar wurde.
  • Unterstützung für Zeitbereiche: Unterstützt verschiedene Zeitbereiche (z. B. Systemzeit, GPU-Zeit) und ermöglicht die Kalibrierung zwischen ihnen. Ein Zeitbereich stellt eine bestimmte Uhrquelle oder Zeitbasis dar, die zum Messen von Zeitstempeln verwendet wird. Unter Android verwenden alle relevanten Zeitstempel CLOCK_MONOTONIC, sodass die Verwendung mehrerer Zeitbereiche nicht erforderlich ist.
  • Integration mit VK_KHR_present_id2: Verwendet die standardisierte VkPresentId2KHR, um Präsentationsanfragen zu identifizieren.

Erweiterung aktivieren

Wenn Sie VK_EXT_present_timing verwenden möchten, müssen Sie sie und die Voraussetzung VK_KHR_present_id2 beim Erstellen des Geräts aktivieren. Prüfen Sie außerdem, ob die Funktionen vom physischen Gerät unterstützt werden:

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

Wenn eine dieser Funktionsprüfungen fehlschlägt (presentTiming oder presentId2 ist „false“), unterstützt das Gerät oder der Treiber VK_EXT_present_timing oder die Voraussetzung nicht. In diesem Fall kann Ihre Anwendung VK_EXT_present_timing nicht verwenden und sollte auf VK_GOOGLE_display_timing (falls unterstützt) oder auf Standardmechanismen für die Frame-Abstimmung wie Swappy zurückgreifen.

Präsentationszeit für die Swapchain aktivieren

Aktivieren Sie beim Erstellen der Swapchain explizit die Präsentationszeit, indem Sie das Flag VK_SWAPCHAIN_CREATE_PRESENT_TIMING_BIT_EXT festlegen:

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

Präsentations-IDs verknüpfen

Verknüpfen Sie beim Präsentieren mit jedem Frame eine eindeutige ID mit VkPresentId2KHR (Teil der Erweiterung 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);

Timing-Eigenschaften der Swapchain abfragen

Fragen Sie Timing-Eigenschaften der Swapchain wie die Aktualisierungsdauer mit vkGetSwapchainTimingPropertiesEXT ab:

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

Unterstützte Zeitbereiche abfragen

] Wie bereits erwähnt, stellt ein Zeitbereich eine bestimmte Uhrquelle oder Zeitbasis dar. VK_EXT_present_timing unterstützt zwar mehrere Zeitbereiche und ermöglicht die Kalibrierung zwischen ihnen, die Verwendung verschiedener Uhren ist unter Android jedoch nicht erforderlich, da alle relevanten Zeitstempel CLOCK_MONOTONIC verwenden. Fragen Sie die unterstützten Zeitbereiche für Ihre Swapchain ab:

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

Zielpräsentationszeit anfordern

Wenn Sie anfordern möchten, dass ein Frame zu einem bestimmten Zeitpunkt präsentiert wird, verketten Sie eine VkPresentTimingInfoEXT-Struktur mit Ihrem 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;

Wie bereits erwähnt, wird unter Android eine targetTime von 0 oder ein Zielzeitstempel, der mehr als 1 Sekunde in der Zukunft liegt, ignoriert.

Vergangene Präsentationszeiten abfragen

Wenn Sie detaillierte Timing-Informationen für vergangene Präsentationen abfragen möchten, konfigurieren Sie zuerst die Größe der Timing-Warteschlange mit vkSetSwapchainPresentTimingQueueSizeEXT, und rufen Sie dann die Zeitangaben mit vkGetPastPresentationTimingEXT ab:

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

Codebeispiel

Ein vollständiges Integrationsbeispiel und Konformitätstests für VK_EXT_present_timing finden Sie in den deqp-Tests (Draw Elements Quality Program) im Vulkan CTS-Repository (Compatibility Test Suite):

Vulkan CTS Present Timing Tests

In diesen Tests wird gezeigt, wie Sie Swapchains für die Präsentationszeit konfigurieren, Zielzeiten festlegen und die Genauigkeit der gemeldeten Präsentationszeitstempel in verschiedenen Zeitbereichen überprüfen.