Frame Pacing-Bibliothek Teil des Android Game Development Kit.
Die Android Frame Pacing-Bibliothek, auch bekannt als Swappy, ist Teil der AGDK-Bibliotheken. Es sorgt für reibungsloses Rendering und korrektes Frame-Pacing bei OpenGL- und Vulkan-Spielen auf Android. In diesem Dokument wird das Frame-Pacing definiert, es werden Situationen beschrieben, in denen Frame-Pacing erforderlich ist, und es wird gezeigt, wie die Bibliothek diese Situationen behandelt. Wenn Sie direkt mit der Implementierung der Frame-Pacing-Funktion in Ihrem Spiel beginnen möchten, finden Sie hier die nächsten Schritte.
Hintergrund
Frame Pacing ist die Synchronisierung des Logik- und Rendering-Loops eines Spiels mit dem Anzeigesubsystem eines Betriebssystems und der zugrunde liegenden Anzeigehardware. Das Android-Display-Subsystem wurde so konzipiert, dass visuelle Artefakte (Tearing) vermieden werden, die auftreten können, wenn die Display-Hardware während eines Updates zu einem neuen Frame wechselt. Um diese Artefakte zu vermeiden, führt das Display-Subsystem Folgendes aus:
- Frames werden intern gepuffert
- Erkennt verspätete Frame-Einreichungen
- Wiederholt die Anzeige vergangener Frames, wenn verspätete Frames erkannt werden
Ein Spiel informiert SurfaceFlinger, den Compositor im Display-Subsystem, dass alle für einen Frame erforderlichen Zeichenaufrufe gesendet wurden (durch Aufrufen von eglSwapBuffers
oder vkQueuePresentKHR
). SurfaceFlinger signalisiert die Verfügbarkeit eines Frames an die Displayhardware über einen Latch. Die Displayhardware zeigt dann den angegebenen Frame an. Die Displayhardware taktet mit einer konstanten Rate, z. B. 60 Hz. Wenn kein neuer Frame verfügbar ist, wenn die Hardware einen benötigt, wird der vorherige Frame noch einmal angezeigt.
Inkonsistente Frame-Zeiten treten häufig auf, wenn ein Spiel-Render-Loop mit einer anderen Rate als die native Displayhardware gerendert wird. Wenn ein Spiel, das mit 30 FPS ausgeführt wird, auf einem Gerät gerendert wird, das nativ 60 FPS unterstützt, wird im Render-Loop des Spiels nicht erkannt, dass ein wiederholter Frame zusätzliche 16 Millisekunden auf dem Bildschirm bleibt. Diese Diskrepanz führt in der Regel zu erheblichen Unregelmäßigkeiten bei den Frame-Zeiten, z. B. 49 Millisekunden, 16 Millisekunden, 33 Millisekunden. Übermäßig komplexe Szenen verschärfen dieses Problem noch, da sie zu fehlenden Frames führen.
Nicht optimale Lösungen
Die folgenden Lösungen für das Frame-Pacing wurden in der Vergangenheit in Spielen eingesetzt und führen in der Regel zu inkonsistenten Frame-Zeiten und einer erhöhten Eingabelatenz.
Frames so schnell wie möglich über die Rendering-API senden
Bei diesem Ansatz ist ein Spiel an die variable SurfaceFlinger-Aktivität gebunden und es kommt zu einer zusätzlichen Frame-Latenz. Die Display-Pipeline enthält eine Warteschlange mit Frames, in der Regel mit einer Größe von 2, die sich füllt, wenn das Spiel versucht, Frames zu schnell zu präsentieren. Da kein Platz mehr in der Warteschlange ist, wird die Spielschleife (oder zumindest der Rendering-Thread) durch einen OpenGL- oder Vulkan-Aufruf blockiert. Das Spiel muss dann warten, bis die Displayhardware einen Frame anzeigt. Dieser Rückstau synchronisiert die beiden Komponenten. Diese Situation wird als Puffer-Stuffing oder Warteschlangen-Stuffing bezeichnet. Der Renderingprozess erkennt nicht, was passiert, sodass die Inkonsistenz der Framerate zunimmt. Wenn das Spiel die Eingabe vor dem Frame erfasst, verschlechtert sich die Eingabelatenz.
Android Choreographer allein verwenden
Spiele verwenden Android Choreographer ebenfalls zur Synchronisierung. Diese Komponente ist in Java ab API 16 und in C++ ab API 24 verfügbar und liefert regelmäßige Ticks mit derselben Frequenz wie das Display-Subsystem. Es gibt jedoch immer noch Feinheiten, wann dieser Tick im Verhältnis zum tatsächlichen Hardware-VSYNC ausgeliefert wird. Diese Offsets variieren je nach Gerät. Bei langen Frames kann es weiterhin zu Buffer-Stuffing kommen.
Vorteile der Frame Pacing-Bibliothek
Die Frame Pacing-Bibliothek verwendet Android Choreographer für die Synchronisierung und kümmert sich um die Variabilität bei der Tick-Übermittlung. Dabei werden Präsentationszeitstempel verwendet, um sicherzustellen, dass Frames zur richtigen Zeit präsentiert werden, und Synchronisations-Fences, um das Füllen des Puffers zu vermeiden. Die Bibliothek verwendet den NDK Choreographer, sofern verfügbar, und greift andernfalls auf den Java Choreographer zurück.
Die Bibliothek unterstützt mehrere Aktualisierungsraten, sofern sie vom Gerät unterstützt werden. Dadurch hat ein Spiel mehr Flexibilität bei der Darstellung eines Frames. Bei einem Gerät, das sowohl eine Aktualisierungsrate von 60 Hz als auch von 90 Hz unterstützt, kann ein Spiel, das keine 60 Bilder pro Sekunde erzeugen kann, beispielsweise auf 45 fps statt auf 30 fps fallen, um flüssig zu bleiben. Die Bibliothek erkennt die erwartete Framerate des Spiels und passt die Zeiten für die Frame-Präsentation automatisch an. Die Frame Pacing-Bibliothek verbessert auch die Akkulaufzeit, da unnötige Display-Updates vermieden werden. Wenn ein Spiel beispielsweise mit 60 FPS gerendert wird, das Display aber mit 120 Hz aktualisiert wird, wird der Bildschirm für jeden Frame zweimal aktualisiert. Die Frame Pacing-Bibliothek verhindert dies, indem sie die Aktualisierungsrate auf den Wert festlegt, der vom Gerät unterstützt wird und der Ziel-Frame-Rate am nächsten kommt.
Funktionsweise
In den folgenden Abschnitten wird beschrieben, wie die Frame Pacing-Bibliothek mit langen und kurzen Frames umgeht, um ein korrektes Frame Pacing zu erreichen.
Korrekte Frame-Pacing bei 30 Hz
Wenn auf einem 60‑Hz-Gerät mit 30 Hz gerendert wird, ist die ideale Situation auf Android in Abbildung 1 dargestellt. SurfaceFlinger übernimmt neue Grafikpuffer, sofern vorhanden (NB im Diagramm steht für „kein Puffer vorhanden“ und der vorherige wird wiederholt).
Abbildung 1: Ideales Frame-Pacing bei 30 Hz auf einem Gerät mit 60 Hz.
Kurze Game-Frames führen zu Ruckeln
Auf den meisten modernen Geräten verlassen sich Game-Engines auf den Plattform-Choreografen, der Ticks liefert, um das Senden von Frames zu steuern. Es besteht jedoch weiterhin die Möglichkeit für ein schlechtes Frame-Pacing aufgrund kurzer Frames, wie in Abbildung 2 zu sehen ist. Kurze Frames, gefolgt von langen Frames, werden vom Spieler als Ruckeln wahrgenommen.
Abbildung 2: Der kurze Spiel-Frame C führt dazu, dass in Frame B nur ein Frame angezeigt wird, gefolgt von mehreren C-Frames.
Die Frame Pacing-Bibliothek löst dieses Problem mithilfe von Präsentationszeitstempeln. Die Bibliothek verwendet die Erweiterungen für den Präsentationszeitstempel EGL_ANDROID_presentation_time
und VK_GOOGLE_display_timing
, damit Frames nicht zu früh präsentiert werden, wie in Abbildung 3 zu sehen ist.
Abbildung 3: Spielbild B wird zweimal präsentiert, um eine flüssigere Darstellung zu ermöglichen.
Lange Frames führen zu Ruckeln und Latenz
Wenn die Arbeitslast für die Anzeige länger dauert als die Arbeitslast der Anwendung, werden zusätzliche Frames in eine Warteschlange eingefügt. Das führt wieder zu Rucklern und kann aufgrund von Buffer-Stuffing (siehe Abbildung 4) auch zu einem zusätzlichen Frame an Latenz führen. Durch die Bibliothek werden sowohl das Ruckeln als auch der zusätzliche Frame der Latenz entfernt.
Abbildung 4: Langer Frame B führt zu einem falschen Pacing für zwei Frames – A und B
Die Bibliothek löst dieses Problem, indem sie Synchronisations-Fences (EGL_KHR_fence_sync
und VkFence
) verwendet, um Wartezeiten in die Anwendung einzufügen, die es der Display-Pipeline ermöglichen, aufzuholen, anstatt dass sich ein Rückstau aufbaut. In Frame A wird weiterhin ein zusätzlicher Frame angezeigt, Frame B wird jetzt aber korrekt dargestellt (siehe Abbildung 5).
Abbildung 5: Die Frames C und D warten auf die Präsentation.
Unterstützte Betriebsarten
Sie können die Frame Pacing-Bibliothek für einen der folgenden drei Modi konfigurieren:
- Automatikmodus aus + Pipeline
- Automatikmodus + Pipeline
- Automatikmodus + automatischer Pipelinemodus (Pipeline/Nicht-Pipeline)
Empfohlener Modus
Sie können mit dem automatischen Modus und den Pipeline-Modi experimentieren. Beginnen Sie jedoch damit, sie zu deaktivieren und Folgendes nach der Initialisierung von Swappy einzufügen:
swappyAutoSwapInterval(false);
swappyAutoPipelineMode(false);
swappyEnableStats(false);
swappySwapIntervalNS(1000000000L/yourPreferredFrameRateInHz);
Pipelined-Modus
Zum Koordinieren von Engine-Arbeitslasten verwendet die Bibliothek in der Regel ein Pipelining-Modell, das die CPU- und GPU-Arbeitslasten über VSYNC-Grenzen hinweg trennt.
Abbildung 6 Pipelining-Modus.
Nicht-Pipeline-Modus
Im Allgemeinen führt dieser Ansatz zu einer geringeren, besser vorhersagbaren Latenz bei der Eingabe über den Bildschirm. Wenn ein Spiel eine sehr niedrige Frame-Zeit hat, können sowohl die CPU- als auch die GPU-Arbeitslasten in ein einzelnes Swap-Intervall passen. In diesem Fall würde ein nicht gepipelinetes Verfahren tatsächlich eine geringere Latenz des Eingabebildschirms bieten.
Abbildung 7. Nicht-Pipeline-Modus.
Automatischer Modus
Die meisten Spiele wissen nicht, wie das Swap-Intervall ausgewählt werden soll. Das ist die Dauer, für die jedes Frame präsentiert wird (z. B. 33,3 ms für 30 Hz). Auf einigen Geräten kann ein Spiel mit 60 FPS gerendert werden, auf anderen muss die Framerate möglicherweise auf einen niedrigeren Wert gesenkt werden. Im automatischen Modus werden CPU- und GPU-Zeiten gemessen, um Folgendes zu ermöglichen:
- Swap-Intervalle automatisch auswählen: Bei Spielen, die in einigen Szenen 30 Hz und in anderen 60 Hz liefern, kann die Bibliothek dieses Intervall dynamisch anpassen.
- Pipelining für ultraschnelle Frames deaktivieren: Sorgt in allen Fällen für eine optimale Eingabebildschirm-Latenz.
Mehrere Aktualisierungsraten
Geräte, die mehrere Aktualisierungsraten unterstützen, bieten mehr Flexibilität bei der Auswahl eines Swap-Intervalls, das flüssig aussieht:
- Auf Geräten mit 60 Hz: 60 fps / 30 fps / 20 fps
- Auf Geräten mit 60 Hz und 90 Hz: 90 fps / 60 fps / 45 fps / 30 fps
- Auf Geräten mit 60 Hz, 90 Hz und 120 Hz: 120 fps / 90 fps / 60 fps / 45 fps / 40 fps / 30 fps
Die Bibliothek wählt die Aktualisierungsrate aus, die am besten zur tatsächlichen Renderingdauer der Frames eines Spiels passt, um die visuelle Qualität zu verbessern.
Weitere Informationen zum Frame-Pacing bei mehreren Aktualisierungsraten finden Sie im Blogpost Rendering mit hoher Aktualisierungsrate unter Android.
Framestatistiken
Die Frame Pacing-Bibliothek bietet die folgenden Statistiken für Debugging- und Profiling-Zwecke:
- Ein Histogramm der Anzahl der Bildschirmaktualisierungen, die ein Frame nach Abschluss des Renderings in der Compositor-Warteschlange gewartet hat.
- Ein Histogramm der Anzahl der Bildschirmaktualisierungen zwischen der angeforderten Präsentationszeit und der tatsächlichen Präsentationszeit.
- Ein Histogramm der Anzahl der Bildschirmaktualisierungen zwischen zwei aufeinanderfolgenden Frames.
- Ein Histogramm der Anzahl der Bildschirmaktualisierungen, die zwischen dem Beginn der CPU-Arbeit für diesen Frame und der tatsächlichen aktuellen Zeit vergangen sind.
Nächster Schritt
In den folgenden Leitfäden erfahren Sie, wie Sie die Android Frame Pacing-Bibliothek in Ihr Spiel einbinden:
- Android Frame Pacing in Ihren OpenGL-Renderer einbinden
- Android Frame Pacing in den Vulkan-Renderer einbinden
Zusätzliche Ressourcen
- Mir 2 verbessert die Rendering-Leistung durch die Verwendung von Swappy und reduziert die Rate langsamer Sitzungen von 40% auf 10%.