
Hallo allerseits!
In der Welt gibt es eine große Anzahl von Anwendungen auf OpenGL, und es scheint, dass Apple dem nicht ganz zustimmt. Ab iOS 12 und MacOS Mojave ist OpenGL veraltet. Wir haben Apple Metal in MAPS.ME integriert und sind bereit, unsere Erfahrungen und Ergebnisse zu teilen. Wir werden Ihnen sagen, wie unsere Grafik-Engine überarbeitet wurde, mit welchen Schwierigkeiten wir konfrontiert waren und vor allem, wie viele FPS wir jetzt haben.
Jeder, der interessiert ist oder erwägt, der Grafik-Engine Apple Metal-Unterstützung hinzuzufügen, ist bei cat willkommen.
Problem
Unsere Grafik-Engine wurde plattformübergreifend konzipiert. Da OpenGL tatsächlich die einzige plattformübergreifende Grafik-API für die für uns interessanten Plattformen (iOS, Android, MacOS und Linux) ist, haben wir sie als Basis ausgewählt. Wir haben keine zusätzliche Abstraktionsebene erstellt, die die für OpenGL spezifischen Funktionen verbirgt, aber glücklicherweise das Potenzial für deren Implementierung gelassen.
Mit dem Aufkommen der neuen Generation von Grafik-APIs Apple Metal und Vulkan haben wir natürlich die Möglichkeit ihres Auftretens in unserer Anwendung in Betracht gezogen. Wir wurden jedoch durch Folgendes gestoppt:
- Vulkan konnte nur unter Android und Linux funktionieren, und Apple Metal konnte nur unter iOS und MacOS funktionieren. Wir wollten nicht die plattformübergreifende Funktionalität auf der Ebene der Grafik-API verlieren. Dies würde die Entwicklungs- und Debugging-Prozesse erschweren und den Arbeitsaufwand erhöhen.
- Eine Anwendung auf Apple Metal kann (übrigens bis jetzt) nicht auf einem iOS-Simulator erstellt und ausgeführt werden, was auch unsere Entwicklung erschweren und es uns nicht ermöglichen würde, OpenGL vollständig loszuwerden.
- Das Qt-Framework, mit dem wir interne Tools erstellen, unterstützt nur OpenGL ( Vulkan wird jetzt unterstützt ).
- Apple Metal hatte und hat keine C ++ - API, was uns zwingen würde, Abstraktionen nicht nur für die Laufzeit, sondern auch für die Erstellungsphase der Anwendung zu erstellen, wenn ein Teil der Engine in Objective-C ++ kompiliert wird und der andere, wesentlich größere, in C ++.
- Wir waren nicht bereit, eine separate Engine oder einen separaten Code-Zweig speziell für iOS zu erstellen.
- Die Implementierung wurde in der Arbeit eines Grafikentwicklers mindestens sechs Monate lang evaluiert.
Als Apple im Frühjahr 2018 die Übertragung von OpenGL in den veralteten Status ankündigte, wurde klar, dass eine Verschiebung nicht mehr möglich war und die oben genannten Probleme auf die eine oder andere Weise gelöst werden mussten. Darüber hinaus haben wir lange daran gearbeitet, sowohl die Anwendungsgeschwindigkeit als auch den Stromverbrauch zu optimieren, und Apple Metal schien in der Lage zu sein, zu helfen.
Entscheidungsauswahl
Fast sofort bemerkten wir
MoltenVK . Dieses Framework emuliert die Vulkan-API mithilfe von Apple Metal und der Quellcode wurde kürzlich geöffnet. Die Verwendung von MoltenVK würde es anscheinend ermöglichen, OpenGL durch Vulkan zu ersetzen und sich überhaupt nicht mit einer separaten Apple Metal-Integration zu befassen. Darüber hinaus haben Qt-Entwickler die
separate Unterstützung für das Rendern auf Apple Metal zugunsten von MoltenVK abgelehnt. Wir wurden jedoch gestoppt:
- die Notwendigkeit, Android-Geräte zu unterstützen, auf denen Vulkan nicht verfügbar ist;
- die Unfähigkeit, auf dem iOS-Simulator ohne Fallback auf OpenGL zu starten;
- die Unfähigkeit, Apple-Tools zum Debuggen, Profilieren und Vorkompilieren von Shadern zu verwenden, da MoltenVK Echtzeit-Shader für Apple Metal aus SPIR-V- oder GLSL-Quellcodes generiert;
- die Notwendigkeit, auf Updates und Fehlerbehebungen von MoltenVK zu warten, wenn neue Versionen von Metal veröffentlicht werden;
- die Unmöglichkeit einer subtilen Optimierung, die spezifisch für Metall ist, aber nicht spezifisch oder nicht existent für Vulkan.
Es stellte sich heraus, dass wir OpenGL speichern müssen, was bedeutet, dass wir nicht darauf verzichten können, die Engine von der grafischen API zu abstrahieren. Apple Metal, OpenGL ES und in Zukunft Vulkan werden verwendet, um unabhängige interne Komponenten der Grafik-Engine zu erstellen, die vollständig austauschbar sind. OpenGL spielt die Rolle der Fallback-Option, wenn Metal oder Vulkan aus dem einen oder anderen Grund nicht verfügbar sind.
Der Umsetzungsplan lautete wie folgt:
- Refactoring der Grafik-Engine zur Zusammenfassung der verwendeten Grafik-API.
- Rendern Sie für die iOS-Version der App auf Apple Metal.
- Erstellen Sie geeignete Benchmarks für die Rendergeschwindigkeit und den Stromverbrauch, um festzustellen, ob moderne Grafik-APIs auf niedrigerer Ebene dem Produkt zugute kommen können.
Hauptunterschiede zwischen OpenGL und Metal
Um zu verstehen, wie die grafische API abstrahiert wird, müssen wir zunächst die wichtigsten konzeptionellen Unterschiede zwischen OpenGL und Metal ermitteln.
- Es wird angenommen, und nicht ohne Grund, dass Metal eine untergeordnete API ist. Dies bedeutet jedoch nicht, dass Sie in Assembler schreiben oder die Rasterisierung selbst implementieren müssen. Metal kann als Low-Level-API bezeichnet werden, da es eine sehr kleine Anzahl impliziter Aktionen ausführt, dh fast alle Aktionen müssen selbst in den Programmierer geschrieben werden. OpenGL führt viele Dinge implizit aus, angefangen bei der Unterstützung eines impliziten Verweises auf einen OpenGL-Kontext bis hin zur Verknüpfung dieses Kontexts mit dem Stream, in dem er erstellt wurde.
- In Metal "keine" Echtzeitvalidierung von Teams. Im Debug-Modus gibt es natürlich eine Validierung, die viel besser ist als in vielen anderen APIs, was hauptsächlich auf die enge Integration mit Xcode zurückzuführen ist. Wenn das Programm jedoch an den Benutzer gesendet wird, erfolgt keine Validierung mehr. Das Programm stürzt einfach beim ersten Fehler ab. Natürlich stürzt OpenGL nur in den extremsten Fällen ab. Die häufigste Vorgehensweise: Den Fehler ignorieren und weiterarbeiten.
- Metal kann Shader vorkompilieren und daraus Bibliotheken erstellen. In OpenGL werden Shader im Verlauf des Programms aus dem Quellcode kompiliert, da dies für die spezifische Implementierung von OpenGL auf niedriger Ebene auf einem bestimmten Gerät verantwortlich ist. Unterschiede und / oder Fehler bei der Implementierung von Shader-Compilern führen manchmal zu fantastischen Fehlern, insbesondere auf Android-Geräten chinesischer Marken.
- OpenGL nutzt aktiv die Zustandsmaschine, die fast jeder Funktion Nebenwirkungen hinzufügt. Daher sind OpenGL-Funktionen keine reinen Funktionen, und die Reihenfolge und der Anrufverlauf sind häufig wichtig. Metall verwendet Zustände nicht implizit und bewahrt sie nicht länger auf, als für das Rendern erforderlich ist. Zustände existieren als vorgefertigte und fehlgeschlagene Objekte.
Graphic Engine Refactoring und Einbettung von Metall
Der Prozess der Überarbeitung der Grafik-Engine bestand im Wesentlichen darin, die beste Lösung zu finden, um die OpenGL-Funktionen zu entfernen, die unsere Engine aktiv verwendet hat. Das Einbetten von Metall, beginnend von einer der Stufen, verlief parallel.
- Wie bereits erwähnt, verfügt die OpenGL-API über eine implizite Entität, die als Kontext bezeichnet wird. Der Kontext ist einem bestimmten Thread zugeordnet, und die in diesem Thread aufgerufene OpenGL-Funktion findet und verwendet diesen Kontext. Metal, Vulkan (ja, und andere APIs, zum Beispiel Direct3D) funktionieren nicht auf diese Weise, sie haben ähnliche explizite Objekte, die als Gerät oder Instanz bezeichnet werden. Der Benutzer selbst erstellt diese Objekte und ist für deren Übertragung auf verschiedene Subsysteme verantwortlich. Über diese Objekte werden alle Aufrufe von Grafikbefehlen ausgeführt.
Wir haben unser abstraktes Objekt als grafischen Kontext bezeichnet. Im Fall von OpenGL werden die Aufrufe von OpenGL-Befehlen einfach dekoriert, und im Fall von Metal enthält es die MTLDevice-Root-Schnittstelle, über die die Metal-Befehle aufgerufen werden.
Natürlich musste ich dieses Objekt (und da wir Multithread-Rendering haben, dann sogar mehrere solcher Objekte) auf alle Subsysteme verteilen.
Wir haben die Erstellung von Warteschlangen für Befehle, Encoder und deren Verwaltung im grafischen Kontext versteckt, um keine Entitäten zu verbreiten, die in OpenGL einfach nicht vorhanden sind. - Die Aussicht auf das Verschwinden der Validierung von Grafikbefehlen auf Benutzergeräten hat uns nicht offen gefallen. Eine breite Palette von Geräten und Betriebssystemversionen konnte von unserer QS-Abteilung nicht vollständig abgedeckt werden. Daher mussten wir detaillierte Protokolle hinzufügen, bei denen wir zuvor einen aussagekräftigen Fehler von der grafischen API erhalten hatten. Natürlich wurde diese Validierung nur an potenziell gefährlichen und kritischen Stellen der Grafik-Engine hinzugefügt, da das Abdecken der gesamten Engine mit einem Diagnosecode praktisch unmöglich und im Allgemeinen leistungsschädlich ist. Die neue Realität ist, dass Benutzertests und das Debuggen von Protokollen zumindest in Bezug auf das Rendern der Vergangenheit angehören.
- Unser bisheriges Shader-System war für das Refactoring ungeeignet, ich musste es komplett neu schreiben. Hier geht es nicht nur um die Vorkompilierung von Shadern und deren Validierung in der Phase der Projektmontage. OpenGL verwendet die sogenannten einheitlichen Variablen, um Parameter an Shader zu übergeben. Die strukturierte Datenübertragung ist nur mit OpenGL ES 3.0 verfügbar. Da wir OpenGL ES 2.0 weiterhin unterstützen, haben wir diese Methode einfach nicht verwendet. Metal brachte uns dazu, Datenstrukturen zu verwenden, um Parameter zu übergeben, und für OpenGL mussten wir Strukturfelder auf einheitliche Variablen abbilden. Außerdem musste ich jeden der Shader in der Metal Shading Language neu schreiben.
- Bei der Verwendung von Zustandsobjekten mussten wir einen Trick machen. In OpenGL werden in der Regel alle Zustände unmittelbar vor dem Rendern festgelegt, und in Metal sollte dies ein zuvor erstelltes und validiertes Objekt sein. Unsere Engine verwendete offensichtlich den OpenGL-Ansatz, und das Refactoring mit der vorläufigen Erstellung von Statusobjekten war vergleichbar mit einem vollständigen Umschreiben der Engine. Um diesen Knoten zu schneiden, haben wir einen Statuscache im grafischen Kontext erstellt. Wenn zum ersten Mal eine eindeutige Kombination von Statusparametern generiert wird, wird ein Statusobjekt in Metal erstellt und im Cache abgelegt. Zum zweiten und nachfolgenden Mal wird das Objekt einfach aus dem Cache abgerufen. Dies funktioniert in unseren Karten, da die Anzahl der verschiedenen Kombinationen von Zustandsparametern nicht zu groß ist (ca. 20-30). Für eine komplexe Spielgrafik-Engine ist diese Methode kaum geeignet.
Infolgedessen konnten wir nach etwa 5 Monaten Arbeit MAPS.ME zum ersten Mal mit vollständigem Rendering auf Apple Metal starten. Es war Zeit herauszufinden, was passiert war.
Testen der Rendergeschwindigkeit
Experimentelle Technik
Wir haben im Experiment verschiedene Generationen von Apple-Geräten verwendet. Alle von ihnen wurden auf iOS 12 aktualisiert. Das gleiche Benutzerskript wurde für die All-Map-Navigation (Verschieben und Skalieren) ausgeführt. Das Skript wurde so geschrieben, dass bei jedem Start auf jedem Gerät eine nahezu vollständige Identität der Prozesse in der Anwendung gewährleistet ist. Als Teststandort haben wir das Gebiet von Los Angeles ausgewählt - eines der am stärksten belasteten Gebiete in MAPS.ME.
Zuerst wurde das Skript mit Rendering unter OpenGL ES 3.0 ausgeführt, dann auf demselben Gerät mit Rendering unter Apple Metal. Zwischen den Starts wurde die Anwendung vollständig aus dem Speicher entladen.
Folgende Indikatoren wurden gemessen:
- FPS (Bilder pro Sekunde) für das gesamte Bild;
- FPS für den Teil des Frames, der nur mit dem Rendern beschäftigt ist, ausgenommen Datenaufbereitung und andere Frame-für-Frame-Operationen;
- Der Prozentsatz langsamer Bilder (größer als ~ 30 ms), d.h. diejenigen, die das menschliche Auge als Idioten wahrnehmen kann.
Bei der Messung von FPS wurde das Zeichnen direkt auf dem Bildschirm des Geräts ausgeschlossen, da die vertikale Synchronisation mit der Bildwiederholfrequenz des Bildschirms keine zuverlässigen Ergebnisse ermöglicht. Daher wurde der Rahmen in die Textur im Speicher gezeichnet. Um die CPU und die GPU zu synchronisieren, verwendete OpenGL einen zusätzlichen Aufruf von
glFinish
, während Apple Metal
waitUntilCompleted
für
MTLFrameCommandBuffer
.
| iPhone 6s | | iPhone 7+ | | iPhone 8 | |
---|
| Opengl | Metall | Opengl | Metall | Opengl | Metall |
---|
Fps | 106 | 160 | 159 | 221 | 196 | 298 |
FPS (nur Rendern) | 157 | 596 | 247 | 597 | 271 | 833 |
Bruchteil langsamer Bilder (<30 fps) | 4,13% | 1,25% | 5,45% | 0,76% | 1,5% | 0,29% |
| iPhone X. | | iPad Pro 12.9 ' | |
---|
| Opengl | Metall | Opengl | Metall |
---|
Fps | 145 | 210 | 104 | 137 |
FPS (nur Rendern) | 248 | 705 | 147 | 463 |
Bruchteil langsamer Bilder (<30 fps) | 0,15% | 0,15% | 17,52% | 4,46% |
| iPhone 6s | iPhone 7+ | iPhone 8 | iPhone X. | iPad Pro 12.9 ' |
---|
Beschleunigung des Rahmens auf Metall (N-mal) | 1,5 | 1,39 | 1,52 | 1.45 | 1.32 |
Beschleunigung des Renderns auf Metall (N-mal) | 3,78 | 2.41 | 3,07 | 2.84 | 3.15 |
Verbesserung in langsamen Frames (N-mal) | 3.3 | 7.17 | 5.17 | 1 | 3.93 |
Ergebnisanalyse
Im Durchschnitt betrug der Frame-Performance-Gewinn mit Apple Metal 43%. Der Mindestwert ist auf dem iPad Pro 12,9 'festgelegt - 32%, der Höchstwert - 52% auf dem iPhone 8. Es besteht eine Abhängigkeit: Je niedriger die Bildschirmauflösung, desto mehr übertrifft Apple Metal OpenGL ES 3.0.
Wenn wir den Teil des Frames bewerten, der direkt für das Rendern verantwortlich ist, hat sich die durchschnittliche Rendergeschwindigkeit auf Apple Metal dreimal erhöht. Dies weist auf eine deutlich bessere Organisation und damit auf die Effizienz der Apple Metal API im Vergleich zu OpenGL ES 3.0 hin.
Die Anzahl der langsamen Frames (mehr als ~ 30 ms) auf Apple Metal wurde um das Vierfache reduziert. Dies bedeutet, dass die Wahrnehmung von Animationen und das Bewegen auf der Karte reibungsloser geworden ist. Das schlechteste Ergebnis wurde auf dem iPad Pro 12.9 'mit einer Auflösung von 2732 x 2048 Pixel aufgezeichnet: OpenGL ES 3.0 liefert etwa 17,5% langsame Bilder, während Apple Metal nur 4,5% liefert.
Leistungsprüfung
Experimentelle Technik
Der Stromverbrauch wurde auf dem iPhone 8 unter iOS 12 getestet. Das gleiche Benutzerszenario wurde ausgeführt - Kartennavigation (Verschieben und Skalieren) für 1 Stunde. Das Skript wurde so geschrieben, dass bei jedem Start eine nahezu vollständige Identität der Prozesse innerhalb der Anwendung gewährleistet ist. Das Gebiet von Los Angeles wurde ebenfalls als Teststandort ausgewählt.
Wir haben den folgenden Ansatz zur Messung des Energieverbrauchs verwendet. Das Gerät ist nicht an das Laden angeschlossen. In den Einstellungen des Entwicklers ist die Energieprotokollierung aktiviert. Vor Beginn des Experiments ist das Gerät vollständig aufgeladen. Das Experiment endet am Ende des Skripts. Am Ende des Experiments wurde der Zustand der Batterieladung aufgezeichnet und die Stromverbrauchsprotokolle wurden in das Dienstprogramm zum Profilieren der Batterie in Xcode importiert. Wir haben aufgezeichnet, wie viel der Ladung für die GPU ausgegeben wurde. Zusätzlich haben wir hier das Rendering zusätzlich gewichtet, indem wir die Anzeige des U-Bahn-Schemas und das Vollbild-Antialiasing einbezogen haben.
Die Bildschirmhelligkeit änderte sich nicht in allen Fällen. Es wurden keine anderen Prozesse außer system und MAPS.ME ausgeführt. Der Flugzeugmodus wurde eingeschaltet, Wi-Fi und GPS wurden ausgeschaltet. Zusätzlich wurden mehrere Kontrollmessungen durchgeführt.
Als Ergebnis wurde für jeden der Indikatoren ein Vergleich von Metall mit OpenGL gebildet, und dann wurden die Verhältnisse gemittelt, um eine aggregierte Schätzung zu erhalten.
| Opengl | Metall | Gewinn |
---|
Batterie entladen | 32% | 28% | 12,5% |
Profilierung der Batterieverwendung in Xcode | 1,95% | 1,83% | 6,16% |
Ergebnisanalyse
Im Durchschnitt hat sich der Stromverbrauch der Rendering-Version auf Apple Metal leicht verbessert. Der Stromverbrauch unserer GPU-Anwendung ist mit 2% nicht sehr stark betroffen, da MAPS.ME im Hinblick auf die Verwendung der GPU nicht als hoch belastet bezeichnet werden kann. Ein kleiner Gewinn wird wahrscheinlich durch die Reduzierung der Rechenkosten bei der Erstellung von Anweisungen für die GPU auf der CPU erzielt, die mit Hilfe von Profiling-Tools leider nicht unterschieden werden können.
Zusammenfassung
Das Einbetten von Metall hat uns 5 Monate Entwicklungszeit gekostet. Zwei Entwickler haben dies jedoch fast immer nacheinander getan. Offensichtlich haben wir beim Rendern deutlich gewonnen, beim Energieverbrauch ein wenig. Darüber hinaus hatten wir die Möglichkeit, neue Grafik-APIs, insbesondere Vulkan, mit viel weniger Aufwand einzubetten. Die Grafik-Engine wurde fast vollständig „aussortiert“. Infolgedessen haben wir einige alte Fehler und Leistungsprobleme gefunden und behoben.
Auf die Frage, ob unser Projekt wirklich auf Apple Metal gerendert werden muss, sind wir bereit, dies zu bejahen. Es ist nicht so sehr, dass wir Innovation lieben oder dass Apple OpenGL endgültig aufgeben kann. Es ist erst 2018 und OpenGL erschien im fernen 1997, es ist höchste Zeit, den nächsten Schritt zu tun.
PS Bis wir die Funktion auf allen iOS-Geräten gestartet haben. Um es manuell zu aktivieren, geben Sie
?metal
in die Suchleiste ein und starten Sie die Anwendung neu.
?gl
Befehl
?gl
und starten Sie die Anwendung neu, um das Rendering an OpenGL zurückzugeben.
PPS MAPS.ME ist ein Open-Source-Projekt. Sie können den Quellcode auf
Github lesen .