Von WebGL zu WebGPU

François Beaufort
François Beaufort

Als WebGL-Entwickler sind Sie vielleicht sowohl eingeschüchtert als auch begeistert, WebGPU zu verwenden, den Nachfolger von WebGL, der die Fortschritte moderner Grafik-APIs ins Web bringt.

Es ist beruhigend zu wissen, dass WebGL und WebGPU viele grundlegende Konzepte gemeinsam haben. Mit beiden APIs können Sie kleine Programme, sogenannte Shader, auf der GPU ausführen. WebGL unterstützt Vertex- und Fragment-Shader, während WebGPU auch Compute-Shader unterstützt. WebGL verwendet die OpenGL Shading Language (GLSL), während WebGPU die WebGPU Shading Language (WGSL) verwendet. Obwohl sich die beiden Sprachen unterscheiden, sind die zugrunde liegenden Konzepte größtenteils identisch.

In diesem Artikel werden einige Unterschiede zwischen WebGL und WebGPU hervorgehoben, um Ihnen den Einstieg zu erleichtern.

Globaler Status

WebGL hat viele globale Status. Einige Einstellungen gelten für alle Rendering-Vorgänge, z. B. welche Texturen und Puffer gebunden sind. Sie legen diesen globalen Status durch Aufrufen verschiedener API-Funktionen fest. Er bleibt in Kraft, bis Sie ihn ändern. Der globale Status in WebGL ist eine wichtige Fehlerquelle, da es leicht passieren kann, dass eine globale Einstellung nicht geändert wird. Außerdem erschwert der globale Status die gemeinsame Nutzung von Code, da Entwickler darauf achten müssen, den globalen Status nicht versehentlich so zu ändern, dass sich dies auf andere Teile des Codes auswirkt.

WebGPU ist eine zustandslose API und verwaltet keinen globalen Status. Stattdessen wird das Konzept einer Pipeline verwendet, um den gesamten Rendering-Status zu kapseln, der in WebGL global war. Eine Pipeline enthält Informationen dazu, welche Mischung, Topologie und Attribute verwendet werden sollen. Eine Pipeline ist unveränderlich. Wenn Sie einige Einstellungen ändern möchten, müssen Sie eine weitere Pipeline erstellen. WebGPU verwendet auch Befehlscoder, um Befehle zusammenzufassen und in der Reihenfolge auszuführen, in der sie aufgezeichnet wurden. Dies ist beispielsweise beim Shadow Mapping nützlich, da die Anwendung in einem einzigen Durchlauf über die Objekte mehrere Befehlsstreams aufzeichnen kann, einen für jede Schattenkarte des Lichts.

Zusammenfassend lässt sich sagen, dass das globale Statusmodell von WebGL die Entwicklung robuster, zusammensetzbarer Bibliotheken und Anwendungen erschwert und anfällig gemacht hat. WebGPU hat die Menge an Status, die Entwickler beim Senden von Befehlen an die GPU im Blick behalten mussten, erheblich reduziert.

Nicht mehr synchronisieren

Auf GPUs ist es in der Regel ineffizient, Befehle synchron zu senden und auf sie zu warten, da dies die Pipeline leeren und Blasen verursachen kann. Dies gilt insbesondere für WebGPU und WebGL, die eine Architektur mit mehreren Prozessen verwenden, bei der der GPU-Treiber in einem separaten Prozess von JavaScript ausgeführt wird.

In WebGL erfordert der Aufruf von gl.getError() beispielsweise einen synchronen IPC vom JavaScript-Prozess zum GPU-Prozess und zurück. Dies kann zu einer Blase auf der CPU-Seite führen, da die beiden Prozesse kommunizieren.

Um diese Blasen zu vermeiden, ist WebGPU vollständig asynchron konzipiert. Das Fehlermodell und alle anderen Vorgänge werden asynchron ausgeführt. Wenn Sie beispielsweise eine Textur erstellen, wird der Vorgang sofort als erfolgreich angezeigt, auch wenn die Textur tatsächlich einen Fehler enthält. Sie können den Fehler nur asynchron erkennen. Durch dieses Design wird die prozessübergreifende Kommunikation entkoppelt und die Leistung von Anwendungen verbessert.

Compute-Shader

Compute-Shader sind Programme, die auf der GPU ausgeführt werden, um allgemeine Berechnungen durchzuführen. Sie sind nur in WebGPU, nicht in WebGL verfügbar.

Im Gegensatz zu Vertex- und Fragment-Shadern sind sie nicht auf die Grafikverarbeitung beschränkt und können für eine Vielzahl von Aufgaben verwendet werden, z. B. für maschinelles Lernen, physikalische Simulationen und wissenschaftliche Berechnungen. Compute-Shader werden parallel von Hunderten oder sogar Tausenden von Threads ausgeführt, was sie sehr effizient für die Verarbeitung großer Datasets macht. Weitere Informationen zu GPU-Berechnungen finden Sie in diesem ausführlichen Artikel zu WebGPU.

Verarbeitung von Videoframes

Die Verarbeitung von Videoframes mit JavaScript und WebAssembly hat einige Nachteile: die Kosten für das Kopieren der Daten vom GPU- in den CPU-Arbeitsspeicher und die begrenzte Parallelität, die mit Workern und CPU-Threads erreicht werden kann. WebGPU hat diese Einschränkungen nicht und eignet sich daher hervorragend für die Verarbeitung von Videoframes, da es eng in die WebCodecs API eingebunden ist.

Das folgende Code-Snippet zeigt, wie Sie einen VideoFrame als externe Textur in WebGPU importieren und verarbeiten. Demo ansehen

// Init WebGPU device and pipeline...
// Configure canvas context...
// Feed camera stream to video...

(function render() {
  const videoFrame = new VideoFrame(video);
  applyFilter(videoFrame);
  requestAnimationFrame(render);
})();

function applyFilter(videoFrame) {
  const texture = device.importExternalTexture({ source: videoFrame });
  const bindgroup = device.createBindGroup({
    layout: pipeline.getBindGroupLayout(0),
    entries: [{ binding: 0, resource: texture }],
  });
  // Finally, submit commands to GPU
}

Standardmäßige Anwendungsportabilität

Bei WebGPU müssen Sie limits anfordern. Standardmäßig gibt requestDevice() ein GPUDevice zurück, das möglicherweise nicht den Hardwarefunktionen des physischen Geräts entspricht, sondern einen angemessenen und niedrigsten gemeinsamen Nenner aller GPUs darstellt. Da Entwickler Geräteeinschränkungen anfordern müssen, wird durch WebGPU sichergestellt, dass Anwendungen auf möglichst vielen Geräten ausgeführt werden.

Umgang mit Leinwänden

WebGL verwaltet das Canvas automatisch, nachdem Sie einen WebGL-Kontext erstellt und Kontextattribute wie „alpha“, „antialias“, „colorSpace“, „depth“, „preserveDrawingBuffer“ oder „stencil“ angegeben haben.

Bei WebGPU müssen Sie das Canvas jedoch selbst verwalten. Um beispielsweise Antialiasing in WebGPU zu erreichen, erstellen Sie eine Multisample-Textur und rendern sie. Anschließend würden Sie die Multisample-Textur in eine reguläre Textur auflösen und diese Textur auf den Canvas zeichnen. Durch diese manuelle Verwaltung können Sie mit einem einzelnen GPUDevice-Objekt so viele Canvas ausgeben, wie Sie möchten. Mit WebGL kann dagegen nur ein Kontext pro Canvas erstellt werden.

WebGPU-Demo mit mehreren Canvas-Elementen

Nebenbei bemerkt: Browser haben derzeit ein Limit für die Anzahl der WebGL-Canvasse pro Seite. Zum Zeitpunkt der Erstellung dieses Dokuments können in Chrome und Safari nur bis zu 16 WebGL-Canvas gleichzeitig verwendet werden. In Firefox können bis zu 200 WebGL-Canvas erstellt werden. Andererseits gibt es keine Beschränkung für die Anzahl der WebGPU-Canvasse pro Seite.

Screenshot mit der maximalen Anzahl von WebGL-Canvas-Elementen in den Browsern Safari, Chrome und Firefox
Die maximale Anzahl von WebGL-Canvas in Safari, Chrome und Firefox (von links nach rechts) – Demo.

Hilfreiche Fehlermeldungen

WebGPU stellt für jede Nachricht, die von der API zurückgegeben wird, einen Aufrufstapel bereit. So können Sie schnell sehen, wo der Fehler in Ihrem Code aufgetreten ist, was beim Debuggen und Beheben von Fehlern hilfreich ist.

WebGPU-Fehlermeldungen enthalten nicht nur einen Callstack, sondern sind auch leicht verständlich und umsetzbar. Die Fehlermeldungen enthalten in der Regel eine Beschreibung des Fehlers und Vorschläge zur Behebung.

Mit WebGPU können Sie auch für jedes WebGPU-Objekt ein benutzerdefiniertes label bereitstellen. Dieses Label wird dann vom Browser in GPUError-Meldungen, Konsolenwarnungen und Browser-Entwicklertools verwendet.

Von Namen zu Indexen

In WebGL sind viele Dinge über Namen miteinander verbunden. Sie können beispielsweise eine Uniform-Variable namens myUniform in GLSL deklarieren und ihren Speicherort mit gl.getUniformLocation(program, 'myUniform') abrufen. Das ist praktisch, da Sie einen Fehler erhalten, wenn Sie den Namen der Uniform-Variablen falsch eingeben.

In WebGPU hingegen ist alles vollständig über Byte-Offset oder Index (oft als location bezeichnet) verbunden. Es liegt in Ihrer Verantwortung, die Speicherorte für den Code in WGSL und JavaScript zu synchronisieren.

Mipmap-Generierung

In WebGL können Sie das Mip-Level 0 einer Textur erstellen und dann gl.generateMipmap() aufrufen. WebGL generiert dann alle anderen Mipmap-Ebenen für Sie.

In WebGPU müssen Sie Mipmaps selbst generieren. Dafür gibt es keine integrierte Funktion. Weitere Informationen zu dieser Entscheidung finden Sie in der Spezifikationsdiskussion. Sie können praktische Bibliotheken wie webgpu-utils verwenden, um Mipmaps zu generieren, oder selbst lernen, wie das geht.

Speicherpuffer und Speichertexturen

Einheitliche Puffer werden sowohl von WebGL als auch von WebGPU unterstützt und ermöglichen es Ihnen, konstante Parameter mit begrenzter Größe an Shader zu übergeben. Speicherpuffer, die Uniform-Puffern sehr ähnlich sind, werden nur von WebGPU unterstützt und sind leistungsfähiger und flexibler als Uniform-Puffer.

  • Speicherpuffer für Daten, die an Shader übergeben werden, können viel größer sein als Uniform-Puffer. Laut Spezifikation können Uniform-Puffer-Bindungen bis zu 64 KB groß sein (siehe maxUniformBufferBindingSize). Die maximale Größe einer Speicherpuffer-Bindung beträgt in WebGPU jedoch mindestens 128 MB (siehe maxStorageBufferBindingSize).

  • Speicherpuffer sind beschreibbar und unterstützen einige atomare Vorgänge, während einheitliche Puffer nur schreibgeschützt sind. Dadurch können neue Klassen von Algorithmen implementiert werden.

  • Speicherpufferbindungen unterstützen Arrays mit Laufzeitgröße für flexiblere Algorithmen, während die Größen von Uniform-Puffer-Arrays im Shader angegeben werden müssen.

Speicher-Textures werden nur in WebGPU unterstützt und sind für Texturen das, was Speicherpuffer für einheitliche Puffer sind. Sie sind flexibler als reguläre Texturen und unterstützen Schreibvorgänge mit Direktzugriff (und in Zukunft auch Lesevorgänge).

Puffer- und Texturänderungen

In WebGL können Sie einen Puffer oder eine Textur erstellen und dann die Größe jederzeit mit gl.bufferData() bzw. gl.texImage2D() ändern.

In WebGPU sind Puffer und Texturen unveränderlich. Das bedeutet, dass Sie Größe, Verwendung oder Format nach der Erstellung nicht mehr ändern können. Sie können nur die Inhalte ändern.

Unterschiede bei der Leerzeichenkonvention

In WebGL liegt der Z-Bereich des Clip-Space zwischen -1 und 1. In WebGPU liegt der Z-Clip-Space-Bereich zwischen 0 und 1. Das bedeutet, dass Objekte mit einem Z-Wert von 0 der Kamera am nächsten sind, während Objekte mit einem Z-Wert von 1 am weitesten entfernt sind.

Abbildung der Z-Clip-Space-Bereiche in WebGL und WebGPU.
Z-Clip-Space-Bereiche in WebGL und WebGPU.

WebGL verwendet die OpenGL-Konvention, bei der die Y-Achse nach oben und die Z-Achse in Richtung des Betrachters zeigt. WebGPU verwendet die Metal-Konvention, bei der die Y-Achse nach unten und die Z-Achse aus dem Bildschirm heraus zeigt. Die Richtung der Y-Achse ist in Framebuffer-, Viewport- und Fragment-/Pixelkoordinaten nach unten gerichtet. Im Clip-Space verläuft die Y-Achse weiterhin nach oben wie in WebGL.

Danksagungen

Vielen Dank an Corentin Wallez, Gregg Tavares, Stephen White, Ken Russell und Rachel Andrew für das Überprüfen dieses Artikels.

Außerdem empfehle ich WebGPUFundamentals.org, um mehr über die Unterschiede zwischen WebGPU und WebGL zu erfahren.