Optimierung von WebGL

Aufgrund der zusätzlichen Validierung, die WebGL zur Gewährleistung der Websicherheit auferlegen muss, ist bekannt, dass der CPU-seitige Overhead beim Ausführen von WebGL-Anwendungen im Vergleich zu nativen OpenGL-Anwendungen höher ist. Aus diesem Grund können grafikintensive Anwendungen beim Zusammenspiel mit GL-Funktionen auf der CPU-Seite zu Engpässen führen. Für die beste Leistung sollte daher besondere Sorgfalt auf das Profiling und die Optimierung der GL-API-Nutzung in Anwendungen gelegt werden, die als WebGL-intensiv erwartet werden. Dieser Optimierungsleitfaden konzentriert sich auf verschiedene Techniken, die sich als nützlich zur Verbesserung der WebGL-Leistung erwiesen haben.

Es gibt so viele GL-Hardware- und Treiberhersteller sowie Betriebssystemkombinationen, dass es schwierig ist, einen spezifischen Optimierungsleitfaden zu erstellen. Einige Optimierungen, die auf bestimmten Hardware-/Treiber-/OS-Kombinationen effizient sind, haben bei Treibern eines anderen Herstellers keinen großen Unterschied gemacht. Glücklicherweise war es bisher eher selten, ein widersprüchliches Szenario zu finden, bei dem eine bestimmte Optimierung für einen Treiber zu einem Leistungsabfall bei Hardware eines anderen GPU-Herstellers geführt hätte. Meistens geschieht dies, weil eine bestimmte Funktion von einer bestimmten Hardware nicht unterstützt wird, was dazu führt, dass der Treiber auf Emulation zurückgreift. Zum Beispiel wurde in einem Fall festgestellt, dass ein nativer GL-Treiber angibt, Unterstützung für das ETC2-komprimierte Texturformat zu haben, obwohl die Grafikhardware dies nicht implementierte, und in einem anderen Fall wurde festgestellt, dass die Verwendung des Vertex-Shader-Primitive-Restart-Index dazu führen würde, dass der GL-Treiber auf das Ausführen von Vertex-Shadern in Software zurückfällt. Leider bieten OpenGL-Spezifikationen keine Möglichkeit für einen Treiber, diese Art von Leistungseinschränkungen zu melden, weshalb das Benchmarking über eine Vielzahl von Zielhardware nahezu notwendig ist, wenn GL optimiert wird. Es ist auch nützlich, die Webseitenkonsole des Browsers während der Ausführung genau zu beachten, da Browser zusätzliche Leistungswarnungen in den Konsolenprotokollen melden können.

Es sollte auch anerkannt werden, dass einige festgestellte Leistungsprobleme auf Ineffizienzen oder regelrechte Leistungsfehler in Browsern und ihren verwendeten Softwarebibliotheken zurückzuführen waren und nichts mit den zugrunde liegenden GL-Treibern oder der Websicherheit im Allgemeinen zu tun hatten. Bei der anfänglichen Arbeit an der Optimierung von Emscripten-portierten GL-Codebasen waren die meisten Browser mit ihren WebGL-Stacks ineffizient, aber das war zu erwarten, da es vor Emscripten und asm.js nicht einmal möglich war, so präzise GL-Leistungsvergleiche zwischen nativ und dem Web durchzuführen, so dass eine große Anzahl von leistungsrelevanten Problemen übersehen wurde. Dieser Aspekt hat sich stetig verbessert, da immer mehr Menschen WebGL mit großen Emscripten-Codebasen belasten, so dass einige der Punkte in diesem Leitfaden in Zukunft möglicherweise nicht mehr relevant sind. Wenn Sie diesen Leitfaden in Zukunft lesen und etwas finden, das in allen Fällen wie ein Nettoverlust erscheint, reichen Sie bitte einen Doc-PR zur Diskussion ein. Wenn ein bestimmtes GL-Zugriffsmuster im Web im Vergleich zu nativ um Größenordnungen langsamer ist, handelt es sich wahrscheinlich um einen Leistungsfehler.

Die folgende Liste mit Optimierungstipps zeigt verschiedene Situationen auf, die in der Praxis bekanntermaßen Auswirkungen haben, obwohl davon abgeraten wird, eine Optimierung blind durchzuführen, sondern den Profiler beim Experimentieren stets griffbereit zu halten.

Welchen GL-Modus ansteuern?

Emscripten ermöglicht das Anzielen verschiedener OpenGL- und OpenGL-ES-API-Varianten mit unterschiedlichen Linker-Flags.

Standardmäßig, wenn keine speziellen GL-bezogenen Linker-Flags ausgewählt werden, zielt Emscripten auf die WebGL 1 API ab, auf die der Benutzercode durch Einbinden von OpenGL ES 2.0 Headern in C/C++-Code zugreift (#include <GLES2/gl2.h> und #include <GLES2/gl2ext.h>). Dieser Modus funktioniert wie GLES 2, mit der Ausnahme, dass eine Reihe von WebGL-spezifischen Änderungen und Einschränkungen angewendet werden. Für eine nahezu vollständige Referenz der Unterschiede zwischen WebGL 1 und OpenGL ES 2 siehe WebGL 1 Spezifikation: Unterschiede zwischen WebGL und OpenGL ES 2.0.

  • Wenn Ihre Anwendung Geometrie aus dem Client-seitigen Speicher rendert, muss sie mit dem Linker-Flag -sFULL_ES2 gebaut werden. Dieser Modus ist praktisch, um das Portieren neuer Codebasen zu erleichtern, jedoch unterstützt WebGL selbst das Rendern aus dem Client-seitigen Speicher nicht, daher wird diese Funktion emuliert. Für die beste Leistung verwenden Sie stattdessen VBOs und bauen Sie ohne das Linker-Flag -sFULL_ES2.

  • Wenn Ihre Anwendung alte Desktop-OpenGL-APIs ansteuert, funktioniert sie möglicherweise, wenn sie mit dem Flag -sLEGACY_GL_EMULATION erstellt wird. Erwarten Sie jedoch in diesem Modus, selbst wenn es funktioniert, keine gute Leistung. Wenn die Anwendung in diesem Modus langsam ist und nur eine feste Pipeline und überhaupt keine Shader verwendet, ist es auch möglich, -sLEGACY_GL_EMULATION mit dem Linker-Flag -sGL_FFP_ONLY zu kombinieren, um zu versuchen, etwas Leistung zurückzugewinnen. Im Allgemeinen wird jedoch empfohlen, den Aufwand zu betreiben, die Anwendung stattdessen auf WebGL 1/OpenGL ES 2 zu portieren.

  • Wenn Sie OpenGL ES 3 ansteuern und aus dem clientseitigen Speicher rendern müssen oder die glMapBuffer*() API benötigen, übergeben Sie das Linker-Flag -sFULL_ES3, um diese Funktionen zu emulieren, die das Core WebGL 2 nicht besitzt. Es wird erwartet, dass diese Emulation die Leistung beeinträchtigt, daher wird stattdessen die Verwendung von VBOs empfohlen.

  • Auch wenn Ihre Anwendung keine WebGL 2/OpenGL ES 3-Funktionen benötigt, sollten Sie in Erwägung ziehen, die Anwendung für WebGL 2 zu portieren, da die JavaScript-seitige Leistung in WebGL 2 so optimiert wurde, dass kein temporärer Müll erzeugt wird, was eine solide Geschwindigkeitsverbesserung von 3-7% sowie eine Reduzierung potenzieller Ruckler zur Renderzeit zur Folge hatte. Um diese Optimierungen zu aktivieren, erstellen Sie mit dem Linker-Flag -sMAX_WEBGL_VERSION=2 und stellen Sie sicher, dass Sie zur GL-Startzeit einen WebGL 2-Kontext erstellen (OpenGL ES 3-Kontext, wenn EGL verwendet wird).

Wie man WebGL profiliert

Für die Messung der GL-Leistung stehen verschiedene Tools zur Verfügung. Im Allgemeinen wird hier empfohlen, dass Entwickler sich nicht nur auf die Suche nach browserspezifischen Profiling-Tools beschränken, sondern in der Praxis haben sich native Profiler als ebenso gut, wenn nicht sogar besser erwiesen. Der einzige Nachteil bei der Verwendung eines nativen Profilers ist, dass ein intimes Wissen darüber, wie WebGL im Browser implementiert ist, entscheidend sein kann, oder es könnte sonst schwierig sein, die Aufrufströme zur GPU zu verstehen.

  • Um einen Überblick darüber zu erhalten, wie viel Zeit in den verschiedenen WebGL-Einstiegspunkten verbracht wird, verwenden Sie Firefox mit seinem Gecko Profiler Add-on. Dieses Profiling-Tool kann Zeitdaten über den gesamten Stack des ausgeführten Codes anzeigen: handgeschriebenes JavaScript, asm.js/WebAssembly und nativen Firefox C/C++ Browser-Code, was es im Vergleich zu anderen Profiling-Tools sehr wertvoll macht.

  • Nützliche native Tools zur CPU-Overhead-Profilierung sind AMD CodeXL, Intel VTune Amplifier und macOS Instruments. Wenn das Browser-Leistungs-Profiling-Tool darauf hindeutet, dass ein großer Teil der Zeit in den Browser-Einstiegspunkten selbst verbracht wird, können diese Tools nützlich sein, um Hotspots zu lokalisieren. Bei der Verwendung nativer CPU-Profiling-Tools ist es jedoch notwendig, den Browser-Code manuell aus dem Quellcode zu erstellen, um Symbolinformationsdaten (z. B. .pdb-Dateien unter Windows) zu erhalten, die diese Tools lokal nachschlagen. Beim Debuggen von Firefox auf diese Weise ist es hilfreich, die Multiprozess-Architektur in Firefox zu deaktivieren, um Traces zu erhalten, die den Inhaltsprozess im selben Thread wie der Browser selbst ausführen. Navigieren Sie im Firefox-Browser zur Seite about:config und setzen Sie die Präferenz browser.tabs.remote.autostart.2 auf false und starten Sie den Browser neu.

  • Zum Debuggen von GPU-seitigen API-Aufruf-Traces können NVidia Nsight, Intel Graphics Performance Analyzers, Visual Studio Graphics Debugger und AMD CodeXL nützliche Tools sein. Unter Windows kann Firefox entweder OpenGL oder Direct3D zum Rendern von WebGL-Inhalten verwenden. Direct3D ist die Standardeinstellung, aber z. B. AMD CodeXL verfolgt nur OpenGL-Aufrufströme. Um AMD CodeXL zum Verfolgen von WebGL-API-Aufrufen in Firefox zu verwenden, navigieren Sie im Browser zu about:config und setzen Sie die Präferenz webgl.disable-angle auf true und laden Sie die Seite neu.

Redundante Aufrufe vermeiden

In WebGL hat jeder einzelne GL-Funktionsaufruf einen gewissen Overhead, selbst scheinbar einfache und nahezu funktionslose. Dies liegt daran, dass WebGL-Implementierungen jeden Aufruf validieren müssen, da die zugrunde liegenden nativen OpenGL-Spezifikationen keine Sicherheitsgarantien bieten, auf die im Web vertraut werden könnte. Zusätzlich erzeugt jeder WebGL-Aufruf auf der asm.js/WebAssembly-Seite einen FFI-Übergang (einen Sprung zwischen der Ausführung von Code im asm.js-Kontext und der Ausführung von Code im nativen C++-Kontext des Browsers), der einen etwas höheren Overhead als ein regulärer Funktionsaufruf innerhalb von asm.js/WebAssembly hat. Daher ist es im Web für die CPU-seitige Leistung im Allgemeinen am besten, die Anzahl der Aufrufe an WebGL zu minimieren. Die folgenden Tipps können hier angewendet werden.

  • Optimieren Sie den Renderer und die Eingabekomponenten auf hoher Ebene, um redundante Aufrufe zu vermeiden. Gestalten Sie das Design bei Bedarf neu, damit der Renderer besser erkennen kann, welche Art von Zustandsänderungen relevant sind und welche nicht benötigt werden. Die beste Art von Cache ist eine, die unnötig ist. Wenn der High-Level-Renderer also den GL-Aufrufstrom schlank halten kann, führt dies zu den schnellsten Ergebnissen. In Fällen, in denen dies schwierig zu erreichen ist, können jedoch einige Arten von Caching auf niedrigerer Ebene effektiv sein, die weiter unten diskutiert werden.

  • Speichern Sie den GL-Zustand im Renderer-Code zwischen und vermeiden Sie redundante Aufrufe, um denselben Zustand mehrmals zu setzen, wenn er sich nicht geändert hat. Zum Beispiel konfigurieren einige Engines möglicherweise blind vor jedem Draw Call die Tiefenprüfung oder Alpha-Blending-Modi neu oder setzen das Shader-Programm für jeden Aufruf zurück.

  • Vermeiden Sie alle Arten von Renderer-Mustern, die den GL nach bestimmten Operationen auf einen bestimmten "Grundzustand" zurücksetzen. Häufig anzutreffende Vorkommen sind glBindBuffer(GL_ARRAY_BUFFER, 0), glUseProgram(0) oder for(i in 0 -> max_attributes) glDisableVertexAttribArray(i); nach jedem Draw Call, um zu einer bekannten festen Konfiguration zurückzukehren. Ändern Sie stattdessen nur den GL-Zustand, der beim Übergang von einem Draw Call zum nächsten benötigt wird.

  • Erwägen Sie, den GL-Zustand nur dann verzögert zu setzen, wenn er wirksam werden muss. Zum Beispiel in dem folgenden Aufrufstrom

    // First draw
    glBindBuffer(...);
    glVertexAttribPointer(...);
    glActiveTexture(0);
    glBindTexture(GL_TEXTURE_2D, texture1);
    glActiveTexture(1);
    glBindTexture(GL_TEXTURE_2D, texture2);
    glDrawArrays(...);
    
    // Second draw (back-to-back)
    glBindBuffer(...);
    glVertexAttribPointer(...);
    glActiveTexture(0); // (*)
    glBindTexture(GL_TEXTURE_2D, texture1); // (*)
    glActiveTexture(1); // (*)
    glBindTexture(GL_TEXTURE_2D, texture2); // (*)
    glDrawArrays(...);
    

Alle vier mit einem Sternchen markierten API-Aufrufe sind redundant, aber ein einfacher Status-Cache reicht nicht aus, um dies zu erkennen. Ein trägerer Status-Cache-Mechanismus kann diese Arten von Änderungen erkennen. Bei der Implementierung von tiefgreifend trägen Status-Caches wird jedoch empfohlen, dies erst nach Vorliegen von Profiling-Daten zu tun, um die Optimierung zu motivieren, da das Anwenden träger Caching-Techniken auf den gesamten GL-Zustand vor dem Rendern aus anderen Gründen ebenfalls kostspielig werden kann und Leistung verschwendet werden kann, wenn der Renderer bereits gut darin ist, das erneute Senden redundanter Aufrufe zu vermeiden. Die richtige Menge an Caching kann ein wenig Abstimmung erfordern, um das Gleichgewicht zu finden.

Eine gute Faustregel ist, dass ein Renderer, der redundante Zustandsaufrufe von vornherein durch ein High-Level-Design vermeidet, im Allgemeinen effizienter ist als einer, der stark auf Zustands-Caching auf niedriger Ebene angewiesen ist.

Techniken zur Minimierung von API-Aufrufen

Neben dem Entfernen von API-Aufrufen, die eindeutig redundant sind, ist es auch gut, darauf zu achten, wie Zustandsänderungen mit anderen Techniken minimiert werden können. Die folgende Checkliste bietet einige Möglichkeiten.

  • Beim Rendern auf Offscreen-Renderziele verwenden Sie mehrere FBOs, sodass das Umschalten der Renderziele nur einen einzigen glBindFramebuffer()-Aufruf erfordert. Dies vermeidet, dass in jedem Frame mehrere Aufrufe ausgeführt werden müssen, um den FBO-Zustand zu setzen.

  • Vermeiden Sie das Mutieren des FBO-Zustands, sondern bevorzugen Sie das Einrichten mehrerer unveränderlicher/statischer FBOs, die ihren Zustand nicht ändern. Das Ändern des FBO-Zustands führt zu einer erneuten Validierung dieser FBO-Kombination im Browser, aber unveränderliche FBOs müssen nur einmal bei der Erstellung validiert werden.

  • Verwenden Sie VAOs, wann immer möglich, um das Aufrufen mehrerer GL-Funktionen zum Einrichten von Vertex-Attributen für das Rendern zu vermeiden.

  • Fassen Sie glUniform*-Aufrufe zu Arrays von Uniforms zusammen und aktualisieren Sie diese in einem glUniform4fv()-Array-Aufruf, anstatt glUniform4f() mehrmals aufzurufen, um jeden einzeln zu aktualisieren. Oder noch besser, verwenden Sie Uniform Buffer Objects in WebGL 2.

  • Rufen Sie glGetUniformLocation() nicht zur Renderzeit auf, sondern fragen Sie die Locations einmal pro Shader-Programm beim Start ab und cachen Sie sie.

  • Verwenden Sie, wann immer anwendbar, Instanz-Rendering.

  • Erwägen Sie, mehrere Texturen in einer zu atlasieren, um bessere Möglichkeiten zum Geometrie-Batching und Instancing zu ermöglichen.

  • Erwägen Sie, Renderables aggressiver auszusortieren als auf nativen GL-Plattformen, wenn sie nicht bereits so eng wie möglich sind.

GPU-CPU-Synchronisationspunkte vermeiden

Der wichtigste Aspekt einer effizienten GPU-Nutzung ist sicherzustellen, dass die CPU während der Renderzeit niemals auf die GPU warten muss und umgekehrt. Diese Art von Blockaden erzeugt extrem kostspielige CPU-GPU-Synchronisationspunkte, die zu einer schlechten Auslastung beider Ressourcen führen. Im Allgemeinen kann ein Hinweis auf ein solches Szenario durch die Beobachtung der Gesamt-GPU- und CPU-Auslastungsraten erkannt werden. Wenn ein GPU-Profiler behauptet, dass die GPU für große Teile der Zeit im Leerlauf ist, aber ein CPU-Profiler behauptet, dass die CPU wiederum im Leerlauf ist oder dass bestimmte GL-Funktionen sehr lange dauern, deutet dies darauf hin, dass Frames nicht effizient an die GPU übermittelt werden, sondern GPU-CPU-Synchronisation(en) irgendwo während der Draw-Call-Übermittlung auftreten. Leider bieten OpenGL-Spezifikationen keine Leistungsgarantien dafür, welche GL-Aufrufe zu einer Blockade führen können, achten Sie also auf das folgende Verhalten und experimentieren Sie, indem Sie diese ändern und die Auswirkungen neu profilieren.

  • Vermeiden Sie das Erstellen neuer GL-Ressourcen zur Renderzeit. Dies bedeutet, dass Aufrufe von glGen*() und glCreate*() Funktionen (glGenTextures(), glGenBuffers(), glCreateShader() usw.) zur Renderzeit optimiert werden. Wenn neue Ressourcen benötigt werden, versuchen Sie, sie einige Frames vor dem Rendern zu erstellen und hochzuladen.

  • Löschen Sie ebenso keine GL-Ressourcen, die gerade gerendert wurden. Die Funktionen glDelete*() können einen vollständigen Pipeline-Flush auslösen, wenn der Treiber feststellt, dass Ressourcen in Gebrauch sind. Es ist besser, Ressourcen nur zur Ladezeit zu löschen.

  • Rufen Sie niemals glGetError() oder glCheckFramebufferStatus() zur Renderzeit auf. Diese Funktionen sollten darauf beschränkt sein, nur zur Ladezeit überprüft zu werden, da beide einen vollständigen Pipeline-Sync ausführen können.

  • Rufen Sie analog dazu keine der glGet*() API-Funktionen zur Renderzeit auf, sondern fragen Sie sie zur Start- und Ladezeit ab und greifen Sie zur Renderzeit auf zwischengespeicherte Ergebnisse zurück.

  • Versuchen Sie, das Kompilieren von Shadern zur Renderzeit zu vermeiden, sowohl glCompileShader() als auch glLinkProgram() können extrem langsam sein.

  • Rufen Sie glReadPixels() nicht auf, um Texturinhalte zur Renderzeit in den Hauptspeicher zurückzukopieren. Falls erforderlich, verwenden Sie stattdessen den WebGL 2 GL_PIXEL_PACK_BUFFER Bindungsziel, um eine GPU-Oberfläche zuerst auf ein Offscreen-Ziel zu kopieren, und erst später mit glReadPixels() die Inhalte dieser Oberfläche zurück in den Hauptspeicher zu kopieren.

GPU-Treiber-freundliches Speicherzugriffsverhalten

Die Übertragung von Speicher zwischen CPU und GPU ist eine häufige Ursache für GL-Leistungsprobleme. Dies liegt daran, dass das Erstellen neuer GL-Ressourcen langsam sein kann und das Hoch- oder Herunterladen von Daten die CPU blockieren kann, wenn die Daten nicht bereit sind oder wenn eine alte Version der Daten noch benötigt wird, bevor sie mit einer neuen Version überschrieben werden können.

  • Bevorzugen Sie verschachtelte Vertex-Daten in einem einzelnen VBO gegenüber mehreren VBOs, die planare Attribute enthalten. Dies verbessert das Verhalten des GPU-Vertex-Caches und vermeidet mehrere redundante glBindBuffer()-Aufrufe beim Einrichten von Vertex-Attribut-Pointern für das Rendern.

  • Vermeiden Sie Aufrufe von glBufferData() oder glTexImage2D/3D(), um den Inhalt eines Puffers oder einer Textur zur Laufzeit zu ändern. Wenn Sie die Größe dynamischer VBOs vergrößern oder verkleinern, verwenden Sie geometrische Array-Wachstumssemantiken im Stil von std::vector, um das ständige Ändern der Größe in jedem Frame zu vermeiden.

  • Bevorzugen Sie das Aufrufen von glBufferSubData() und glTexSubImage2D/3D() beim Aktualisieren von Puffertexturdaten, auch wenn sich der gesamte Inhalt der Textur oder des Puffers ändert. Wenn die Größe eines Puffers schrumpfen würde, erstellen Sie den Speicher nicht eifrig neu, sondern ignorieren Sie einfach die überschüssige Größe.

  • Für dynamische Vertex-Pufferdaten sollten Sie ein Double- oder sogar Triple-Buffering von VBOs in jedem Frame in Betracht ziehen, um das Hochladen eines noch in Verwendung befindlichen VBOs zu vermeiden. Bevorzugen Sie die Verwendung von GL_DYNAMIC Vertex-Puffern gegenüber GL_STREAM.

Wenn die GPU der Engpass ist

Nachdem verifiziert wurde, dass keine CPU-GPU-Pipeline-Synchronisierungsblasen auftreten und das Rendering immer noch GPU-gebunden ist, können die folgenden Optimierungen nützlich sein.

  • Mehrere additive Beleuchtungs-Draw-Passes von Geometrie in einem Forward-Lighting-Renderer können einfach zu implementieren sein, aber die Menge der dabei generierten GL-API-Aufrufe kann zu kostspielig sein. In solchen Fällen sollten Sie in Betracht ziehen, mehrere Lichtbeiträge in einem Shader-Pass zu berechnen, selbst wenn dies zu No-Op-Arithmetikoperationen in Shadern führen würde, wenn einige Objekte nicht von bestimmten Lichtern betroffen sind.

  • Verwenden Sie die niedrigstmögliche Fragment-Shader-Genauigkeit, wenn dies ausreicht (lowp). Optimieren Sie Shader im Vorfeld zur Offline-Authoring-Zeit aggressiv; erwarten Sie nicht, dass der GPU-GLSL-Treiber Optimierungen im laufenden Betrieb vornimmt. Dies ist besonders wichtig für mobile GPU-Treiber.

  • Sortieren Sie Renderables zuerst nach dem Ziel-FBO, dann nach dem Shader-Programm und drittens, um alle anderen notwendigen GL-Zustandsänderungen zu minimieren oder um Überzeichnung zu minimieren, je nachdem, ob das Programm CPU- oder GPU-gebunden ist. Dies hilft Tile-basierten Renderern. Rufen Sie WebGL 2 glDiscardFramebuffer() auf, wenn der Inhalt eines FBO nicht mehr benötigt wird.

  • Verwenden Sie einen GPU-Profiler oder implementieren Sie benutzerdefinierte Fragment-Shader, die dabei helfen können, die Überzeichnung der gerenderten Szene zu profilieren. Eine große Überzeichnung erzeugt nicht nur zusätzliche Arbeit, sondern die sequentiellen Abhängigkeiten beim Rendern in dieselben Speicherblöcke verlangsamen das parallele Rendern. Wenn Sie eine 3D-Szene mit aktiviertem Tiefenpuffer rendern, sollten Sie die Szene von vorne nach hinten sortieren, um Überzeichnung und redundante Füllbandbreite pro Pixel zu minimieren. Wenn Sie sehr komplexe Fragment-Shader in einer 3D-Szene verwenden, sollten Sie einen Tiefen-Prepass durchführen, um die Anzahl der tatsächlich gerasterten Farbfragmente auf ein absolutes Minimum zu reduzieren.

Ladezeiten optimieren und andere Best Practices

Schließlich haben sich eine Reihe von verschiedenen Optimierungen als wirksam erwiesen.

  • Im Web kann man im Allgemeinen nicht erwarten, welche komprimierten Texturformate verfügbar sein werden. Erstellen Sie Texturen in mehreren komprimierten Texturpaketen, z. B. eines pro Format, und laden Sie zur Laufzeit das entsprechende herunter, um übermäßige Downloads zu minimieren. Speichern Sie Texturen und andere Assets in IndexedDB, um ein erneutes Herunterladen bei späteren Ausführungen zu vermeiden. Das Emscripten-Linker-Flag -sGL_PREINITIALIZED_CONTEXT kann bei der Erstellung einer HTML-Shell-Seite helfen, die solche Texturformatprüfungen im Voraus durchführt.

  • Erwägen Sie, Shader parallel zum Herunterladen anderer Assets zu kompilieren. Dies kann dazu beitragen, langsame Shader-Kompilierungszeiten zu verbergen.

  • Prüfen Sie frühzeitig im Seitenladevorgang, ob der Browser des Benutzers WebGL unterstützt, bevor Sie eine große Menge an Assets herunterladen. Es kann für den Benutzer frustrierend sein, mehrere Megabyte an Assets herunterladen zu müssen, nur um danach eine Fehlermeldung zu erhalten, dass WebGL nach dem Warten nicht verfügbar ist.

  • Überprüfen Sie den Fehlergrund des WebGL-Kontextes, wenn die WebGL-Initialisierung fehlschlägt, mithilfe des "webglcontextcreationerror"-Callbacks. Browser können im Kontext-Erstellungs-Fehlerbehandler gute Diagnosen geben, um die Ursache zu ermitteln.

  • Achten Sie genau auf die sichtbare Größe des Canvas (die CSS-Pixelgröße des DOM-Elements) im Vergleich zur physikalischen Renderzielgröße des initialisierten WebGL-Kontextes auf dem Canvas und stellen Sie sicher, dass diese beiden übereinstimmen, um pixelgenaue 1:1-Inhalte zu rendern.

  • Überprüfen Sie die Kontext-Erstellung mit dem Flag failIfMajorPerformanceCaveat, um zu erkennen, wann auf Software gerendert wird, und reduzieren Sie in solchen Fällen die Grafikqualität.

  • Stellen Sie sicher, dass Sie den WebGL-Kontext nur mit der minimalen Anzahl der benötigten Funktionen initialisieren. WebGL-Kontexterstellungsparameter umfassen Unterstützung für Alpha, Tiefe, Schablone und MSAA, und meistens wird z. B. die Unterstützung für Alpha-Blending des Canvas vor dem HTML-Seitenhintergrund nicht benötigt und sollte deaktiviert werden.

  • Vermeiden Sie die Verwendung der *glGetProcAddress() API-Funktionen. Emscripten bietet statische Verknüpfung zu allen GL-API-Funktionen, auch für alle WebGL-Erweiterungen. Die *glGetProcAddress() API wird nur zur Kompatibilität bereitgestellt, um das Portieren bestehenden Codes zu erleichtern, aber der Zugriff auf WebGL über dynamisch erhaltene Funktionszeiger ist merklich langsamer als direkte Funktionsaufrufe, aufgrund zusätzlicher Sicherheitsvalidierungen von Funktionszeigern, die das dynamische Dispatching in asm.js/WebAssembly durchführen muss. Da Emscripten alle GL-Einstiegspunkte statisch verknüpft bereitstellt, wird empfohlen, dies für die beste Leistung zu nutzen.

  • Verwenden Sie immer requestAnimationFrame()-Schleifen, um Animationen zu rendern, anstatt der setTimeout()-API. Dies bietet die flüssigste Planung der Animations-Ticks.

Migration zu WebGL 2

Im Vergleich zu WebGL 1 bietet die neue WebGL 2 API im Wesentlichen kostenlose API-Optimierungen, die einfach durch das Anzielen von WebGL 2 aktiviert werden. Dieser Geschwindigkeitszuwachs ergibt sich aus der Tatsache, dass die WebGL 2 API aus der Perspektive der JavaScript-Bindungen überarbeitet wurde und es nun möglich ist, WebGL zu verwenden, ohne temporäre Objekte zu allozieren, die den JavaScript-Garbage-Collector belasten würden. Diese neuen Einstiegspunkte passen besser zu asm.js- und WebAssembly-Anwendungen und machen die WebGL API etwas schlanker in der Anwendung. Als Fallstudie führte die Aktualisierung der Unreal Engine 4 auf WebGL 2, ohne weitere Engine-Modifikationen, zu einer um 7% schnelleren Durchsatzleistung.

Aufgrund dieser Quelle kostenloser Leistung wird allen Entwicklern dringend empfohlen, auf WebGL 2 umzusteigen, auch wenn keine weiteren WebGL 2-Funktionen benötigt werden, wenn die Leistung ein Anliegen ist. WebGL 2 ist ab Firefox 51 und Chrome 58 verfügbar (siehe #4945). Siehe auch die Tabelle caniuse: WebGL 2. Mit etwas Sorgfalt ist es möglich, gleichzeitig sowohl WebGL 1 als auch WebGL 2 APIs anzusteuern und die beste Leistung zu nutzen, wenn verfügbar, aber bei weniger kompatiblen GPUs elegant zurückzufallen.

Bei der Arbeit mit diesen beiden Spezifikationen ist es gut, sich daran zu erinnern, dass WebGL 1 auf der OpenGL ES 2.0 Spezifikation basiert und WebGL 2 auf der OpenGL ES 3.0 Spezifikation.

Die Migration zu WebGL 2 wird dadurch etwas erschwert, dass WebGL, genau wie OpenGL ES, keine abwärtskompatible API ist. Das heißt, WebGL 1/OpenGL ES 2-Anwendungen funktionieren im Allgemeinen nicht einfach durch die Initialisierung einer neueren Version des GL-Kontextes, um auf WebGL 2/OpenGL ES 3.0 ausgeführt zu werden. Der Grund dafür ist, dass zwischen den beiden Versionen eine Reihe von Änderungen eingeführt wurden, die die Abwärtskompatibilität brechen. Diese Änderungen sind jedoch eher oberflächlicher/kosmetischer Natur als funktional, und funktional umfasst WebGL2/OpenGL ES 3.0 alle Funktionen, die in WebGL 1/OpenGL ES 2 existieren. Nur die Art und Weise, wie die verschiedenen API-Funktionen aufgerufen werden, hat sich geändert.

Um von WebGL 1 auf WebGL 2 zu migrieren, beachten Sie die folgende Liste bekannter Abwärtsinkompatibilitäten.

  • In WebGL 2 wurden eine Reihe von WebGL 1.0-Erweiterungen in die Kern-WebGL 2 API integriert, und diese Erweiterungen werden beim Abfragen der Liste verschiedener WebGL-Erweiterungen nicht mehr als existent beworben. Zum Beispiel wird das Vorhandensein von Instanz-Rendering in WebGL 1 durch die Erweiterung ANGLE_instanced_arrays bereitgestellt, aber dies ist eine Kernfunktion von WebGL 2 und wird daher nicht mehr in der Liste der GL-Erweiterungen aufgeführt. Wenn in einer Anwendung gleichzeitig WebGL 1 und WebGL 2 angesprochen werden, denken Sie daran, sowohl die Erweiterung als auch die Kernkontext-Versionsnummer zu überprüfen, wenn Sie das Vorhandensein einer Funktion feststellen.

  • Ein Nebeneffekt des oben Genannten ist, dass sich die spezifischen Funktionsnamen, die für die Funktion aufgerufen werden müssen, geändert haben, als die Funktionalität in den Kern integriert wurde, d.h. in WebGL1/GLES 2-Kontexten würde man die Funktion glDrawBuffersEXT() aufrufen, aber mit WebGL2/GLES 3.0 sollte man stattdessen die ungesuffixte Funktion glDrawBuffers() aufrufen.

  • Die vollständige Liste der WebGL 1-Erweiterungen, die in die Kernspezifikation von WebGL 2 aufgenommen wurden, ist

    ANGLE_instanced_arrays
    EXT_blend_minmax
    EXT_color_buffer_half_float
    EXT_frag_depth
    EXT_sRGB
    EXT_shader_texture_lod
    OES_element_index_uint
    OES_standard_derivatives
    OES_texture_float
    OES_texture_half_float
    OES_texture_half_float_linear
    OES_vertex_array_object
    WEBGL_color_buffer_float
    WEBGL_depth_texture
    WEBGL_draw_buffers
    

Diese Erweiterungen wurden ohne funktionale Änderungen übernommen, sodass bei der Initialisierung eines WebGL2/GLES 3.0-Kontextes diese direkt verwendet werden können, ohne das Vorhandensein einer Erweiterung zu prüfen.

  • Eine bemerkenswerte Ergänzung ist, dass WebGL 2 ein neues GLSL-Shader-Sprachformat eingeführt hat. In WebGL 1 erstellt man Shader in OpenGL ES Shading Language, Version 1.00, unter Verwendung des Versions-Pragmas #version 100 im Shader-Code. WebGL 2 führte eine neue Shader-Sprachversion ein, The OpenGL ES Shading Language, Version 3.00, die durch die Pragma-Direktive #version 300 es im Shader-Code gekennzeichnet ist.

  • In WebGL 2/GLES 3.0 kann man entweder WebGL 1/GLES 2 #version 100-Shader weiterverwenden oder zu WebGL 2/GLES 3.0 #version 300 es-Shadern migrieren. Beachten Sie jedoch, dass WebGL 2 eine abwärtskompatible Inkompatibilität aufweist, dass die WebGL-Erweiterungen OES_standard_derivatives und EXT_shader_texture_lod in #version 100-Shadern nicht mehr verfügbar sind, da diese Funktionen nicht mehr als Erweiterungen vorhanden sind. #version 100-Shader, die diese Erweiterungen verwenden, müssen stattdessen in das #version 300 es-Format umgeschrieben werden. Emscripten bietet das Linker-Flag -sWEBGL2_BACKWARDS_COMPATIBILITY_EMULATION, das eine String-Suchen-Ersetzen-basierte automatische Migration von #version 100-Shadern in das #version 300 es-Format durchführt, wenn eine dieser Erweiterungen erkannt wird, um diesen Bruch in der Abwärtskompatibilität zu verbergen.

  • In WebGL 2/GLES 3.0 haben sich eine Reihe von Texturformat-Enums für Texturformate geändert, die durch Erweiterungen eingeführt wurden. Es ist nicht mehr möglich, sogenannte unsized Texturformate aus WebGL 1/GLES 2-Erweiterungen zu verwenden, sondern stattdessen müssen die neuen dimensionierten Varianten der Formate für das Feld internalFormat verwendet werden. Zum Beispiel ist es anstatt einer Textur mit format=GL_DEPTH_COMPONENT, type=GL_UNSIGNED_INT, internalFormat=GL_DEPTH_COMPONENT erforderlich, die Größe im Feld internalFormat anzugeben, d.h. format=GL_DEPTH_COMPONENT, type=GL_UNSIGNED_INT, internalFormat=GL_DEPTH_COMPONENT24.

  • Eine besondere Falle bei WebGL 2/GLES 3.0-Texturformaten ist, dass sich der Enum-Wert für den Half-Float (float16)-Texturtyp änderte, als die WebGL 1/GLES 2-Erweiterung OES_texture_half_float in die Kernspezifikation von WebGL 2/GLES 3.0 aufgenommen wurde. In WebGL1/GLES 2 wurden Half-Floats durch den Wert GL_HALF_FLOAT_OES=0x8d61 bezeichnet, aber in WebGL2/GLES 3.0 wird der Enum-Wert GL_HALF_FLOAT=0x140b verwendet, im Gegensatz zu anderen Texturtyp-Erweiterungen, bei denen die Aufnahme in die Kernspezifikation im Allgemeinen den Wert des verwendeten Enums beibehielt.

Um die gleichzeitige Ausrichtung auf WebGL1/GLES 2- und WebGL2/GLES 3.0-Kontexte zu erleichtern, bietet Emscripten insgesamt das Linker-Flag -sWEBGL2_BACKWARDS_COMPATIBILITY_EMULATION, das die oben genannten Unterschiede durch automatisch erkannte Migrationen verbirgt, um vorhandenen WebGL 1-Inhalten zu ermöglichen, transparent auch WebGL 2 für den kostenlosen Geschwindigkeitszuwachs zu nutzen, den es bietet.

Wenn Sie einen fehlenden Punkt in dieser Emulation finden oder Kommentare zur Verbesserung dieses Leitfadens haben, senden Sie bitte Feedback an den Emscripten Bug Tracker.