Von WebGL zu WebGPU

François Beaufort
François Beaufort

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

Es ist beruhigend zu wissen, dass WebGL und WebGPU viele gemeinsame Kernkonzepte 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 die beiden Sprachen unterschiedlich sind, sind die zugrunde liegenden Konzepte weitgehend 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 Renderingvorgänge, z. B. welche Texturen und Buffers gebunden sind. Sie legen diesen globalen Status durch Aufrufen verschiedener API-Funktionen fest. Er bleibt so lange in Kraft, bis Sie ihn ändern. Der globale Status in WebGL ist eine wichtige Fehlerquelle, da es leicht zu vergessen ist, eine globale Einstellung zu ändern. Außerdem erschwert der globale Status die Codefreigabe, 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 wie die zu verwendenden Überblendungen, Topologien und Attribute. Eine Pipeline ist unveränderlich. Wenn Sie einige Einstellungen ändern möchten, müssen Sie eine weitere Pipeline erstellen. WebGPU verwendet auch Befehls-Encoder, um Befehle zu bündeln und in der Reihenfolge auszuführen, in der sie aufgezeichnet wurden. Dies ist beispielsweise bei der Schattenprojektion nützlich, bei der die Anwendung bei einem einzigen Durchlauf über die Objekte mehrere Befehlsstreams aufzeichnen kann, einen für die Schattenkarte jedes Lichts.

Zusammenfassend lässt sich sagen, dass das globale Statusmodell von WebGL das Erstellen robuster, kombinierbarer Bibliotheken und Anwendungen schwierig und anfällig machte. WebGPU reduzierte hingegen erheblich die Menge an Status, die Entwickler im Blick behalten mussten, während sie Befehle an die GPU sendeten.

Synchronisierung beenden

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

In WebGL ist beispielsweise für den Aufruf von gl.getError() eine synchrone IPC vom JavaScript-Prozess zum GPU-Prozess und zurück erforderlich. Dies kann zu einer Verzögerung auf der CPU-Seite führen, wenn die beiden Prozesse miteinander kommunizieren.

Um diese Probleme zu vermeiden, ist WebGPU vollständig asynchron. Das Fehlermodell und alle anderen Vorgänge werden asynchron ausgeführt. Wenn Sie beispielsweise eine Textur erstellen, scheint der Vorgang sofort erfolgreich zu sein, auch wenn die Textur tatsächlich einen Fehler enthält. Sie können den Fehler nur asynchron erkennen. Dieses Design sorgt dafür, dass die prozessübergreifende Kommunikation reibungslos abläuft und Anwendungen eine zuverlässige Leistung bieten.

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 wie maschinelles Lernen, Physiksimulation und wissenschaftliches Computing verwendet werden. Compute Shader werden parallel von Hunderten oder sogar Tausenden von Threads ausgeführt, was sie für die Verarbeitung großer Datensätze sehr effizient macht. Weitere Informationen zu GPU-Computing finden Sie in diesem ausführlichen Artikel zu WebGPU.

Videoframe-Verarbeitung

Die Verarbeitung von Videoframes mit JavaScript und WebAssembly hat einige Nachteile: die Kosten für das Kopieren der Daten aus dem GPU-Speicher in den CPU-Speicher und die begrenzte Parallelität, die mit Workern und CPU-Threads erreicht werden kann. WebGPU hat diese Einschränkungen nicht und eignet sich dank der engen Integration in die WebCodecs hervorragend für die Verarbeitung von Videoframes.

Im folgenden Code-Snippet wird gezeigt, wie ein VideoFrame als externe Textur in WebGPU importiert und verarbeitet wird. Hier können Sie die Demo ausprobieren.

// 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äßig Anwendungsportabilität

WebGPU zwingt Sie, limits anzufordern. Standardmäßig gibt requestDevice() ein GPUDevice zurück, das möglicherweise nicht den Hardwarefunktionen des physischen Geräts entspricht, sondern einen angemessenen und kleinsten gemeinsamen Nenner aller GPUs. Da Entwickler Gerätelimits anfordern müssen, sorgt WebGPU dafür, dass Anwendungen auf möglichst vielen Geräten ausgeführt werden.

Canvas-Verarbeitung

WebGL verwaltet den 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 den Canvas hingegen selbst verwalten. Wenn Sie beispielsweise Anti-Aliasing in WebGPU erreichen möchten, erstellen Sie eine Multisample-Textur und rendern sie. Anschließend lösen Sie die Multisample-Textur in eine normale Textur auf und zeichnen diese Textur auf den Canvas. Mit dieser manuellen Verwaltung können Sie von einem einzelnen GPUDevice-Objekt auf beliebig viele Canvases ausgeben. Mit WebGL kann dagegen nur ein Kontext pro Canvas erstellt werden.

Sehen Sie sich die Demo für mehrere Canvasse mit WebGPU an.

Die Anzahl der WebGL-Canvasse pro Seite ist in Browsern derzeit begrenzt. Derzeit können in Chrome und Safari nur bis zu 16 WebGL-Canvas gleichzeitig verwendet werden. In Firefox können bis zu 200 erstellt werden. Die Anzahl der WebGPU-Canvas pro Seite ist hingegen unbegrenzt.

Screenshot mit der maximalen Anzahl von WebGL-Canvas in den Browsern Safari, Chrome und Firefox
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. Das ist hilfreich beim Debuggen und Beheben von Fehlern.

Neben einem Aufrufstapel enthalten WebGPU-Fehlermeldungen auch leicht verständliche und umsetzbare Informationen. Die Fehlermeldungen enthalten in der Regel eine Beschreibung des Fehlers und Vorschläge zur Fehlerbehebung.

Mit WebGPU können Sie für jedes WebGPU-Objekt auch eine benutzerdefinierte label angeben. Dieses Label wird dann vom Browser in GPUError-Nachrichten, Konsolenw warnungen und Browser-Entwicklertools verwendet.

Von Namen zu Indexen

In WebGL sind viele Dinge durch Namen verbunden. Sie können beispielsweise eine einheitliche 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 einheitlichen Variablen falsch eingeben.

In WebGPU hingegen ist alles über einen Byte-Offset oder Index (oft als Speicherort bezeichnet) verbunden. Sie sind dafür verantwortlich, die Code-Standorte in WGSL und JavaScript synchron zu halten.

Mipmap-Generierung

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

In WebGPU müssen Sie Mipmaps selbst generieren. Es gibt keine integrierte Funktion, um dies zu tun. Weitere Informationen zur Entscheidung finden Sie in der Diskussion zur Spezifikation. Sie können praktische Bibliotheken wie webgpu-utils verwenden, um Mipmaps zu generieren, oder selber lernen.

Speicherpuffer und Speichertextur

Uniform-Buffers werden sowohl von WebGL als auch von WebGPU unterstützt und ermöglichen es, konstante Parameter mit begrenzter Größe an Shader zu übergeben. Speicher- bzw. Arbeitsspeicher-Buffer ähneln sehr den einheitlichen Buffers, werden aber nur von WebGPU unterstützt. Sie sind leistungsfähiger und flexibler als einheitliche Buffers.

  • Speicher- und Uniform-Buffers können sehr unterschiedlich groß sein. Laut der Spezifikation können Bindungen für einheitliche Buffers eine Größe von bis zu 64 KB haben (siehe maxUniformBufferBindingSize). In WebGPU beträgt die maximale Größe einer Speicherbufferbindung jedoch mindestens 128 MB (siehe maxStorageBufferBindingSize).

  • Speicher- und einheitliche Puffer sind schreibbar und unterstützen einige atomare Vorgänge. Einheitliche Puffer sind nur schreibgeschützt. So können neue Algorithmen implementiert werden.

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

Speichertextur werden nur in WebGPU unterstützt und sind für Texturen das, was Speicher- und Uniform-Buffer für Vertex- und Index-Buffer sind. Sie sind flexibler als normale Texturen und unterstützen Schreibvorgänge (und in Zukunft auch Lesevorgänge) per Zufallszugriff.

Änderungen an Puffer und Textur

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 Buffers und Texturen unveränderlich. Das bedeutet, dass Sie Größe, Verwendung oder Format nach dem Erstellen nicht mehr ändern können. Sie können nur den Inhalt ändern.

Unterschiede bei den Konventionen für Leerzeichen

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

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

WebGL verwendet die OpenGL-Konvention, bei der die Y-Achse nach oben und die Z-Achse zum Betrachter zeigt. WebGPU verwendet die Metal-Konvention, bei der die Y-Achse nach unten und die Z-Achse außerhalb des Bildschirms zeigt. Beachten Sie, dass die Y-Achse in Framebuffer-, Ansichts- und Fragment-/Pixelkoordinaten nach unten zeigt. Im Clipbereich ist die Y-Achse wie in WebGL nach oben ausgerichtet.

Danksagungen

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

Ich empfehle auch WebGPUFundamentals.org, um mehr über die Unterschiede zwischen WebGPU und WebGL zu erfahren.