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 einen vielen globalen 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 leicht vergessen wird, 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 erfordert der Aufruf von gl.getError() beispielsweise eine synchrone IPC vom JavaScript-Prozess zum GPU-Prozess und zurück. Dies kann auf der CPU-Seite zu einer Blase führen, während die beiden Prozesse 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 eigentlich ein Fehler ist. 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 Berechnungen für allgemeine Zwecke 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 wissenschaftliche Berechnungen 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.

Videoframes verarbeiten

Die Verarbeitung von Videoframes mit JavaScript und WebAssembly hat einige Nachteile: die Kosten für das Kopieren der Daten vom GPU- 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 API 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. Sie können diese 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 so vielen Geräten wie möglich 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 hingegen müssen Sie den Canvas selbst verwalten. Wenn Sie beispielsweise Antialiasing in einer WebGPU erzielen möchten, müssen Sie eine Multisample-Textur erstellen und in diese rendern. Anschließend lösen Sie die Multisample-Textur in eine normale Textur auf und zeichnen diese Textur auf den Canvas. Dank dieser manuellen Verwaltung können Sie mit einem einzigen GPUDevice-Objekt auf beliebig viele Canvases drucken. Im Gegensatz dazu kann WebGL nur einen Kontext pro Canvas erstellen.

Sehen Sie sich die WebGPU Multiple Canvas-Demo an.

Übrigens: In Browsern ist die Anzahl der WebGL-Canvases pro Seite derzeit eingeschränkt. Zum Zeitpunkt der Erstellung dieses Dokuments können Chrome und Safari nur bis zu 16 WebGL-Canvases gleichzeitig verwenden. Firefox kann bis zu 200 davon erstellen. Andererseits gibt es keine Begrenzung für die Anzahl der WebGPU-Canvases pro Seite.

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. Beispielsweise können Sie eine einheitliche Variable namens myUniform in GLSL deklarieren und ihren Standort 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-Levels 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

Einheitliche Puffer werden sowohl von WebGL als auch von WebGPU unterstützt und ermöglichen es Ihnen, konstante Parameter von begrenzter Größe an Shader weiterzugeben. 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.

  • Speicherpufferdaten, die an Shader weitergegeben werden, können viel größer sein als einheitliche Zwischenspeicher. 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 Zwischenspeicher oder eine Textur erstellen und dann ihre 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 der Erstellung 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. Bei 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 Clip-Raum 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.