Von WebGL zu WebGPU

François Beaufort
François Beaufort

Als WebGL-Entwickler sind Sie möglicherweise sowohl eingeschüchtert als auch begeistert, WebGPU zu verwenden, den Nachfolger von WebGL, der die Fortschritte moderner Grafik-APIs im Web ermöglicht.

Es ist beruhigend zu wissen, dass WebGL und WebGPU viele Kernkonzepte 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 nutzt 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 größtenteils gleich.

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 Rendering-Vorgänge, beispielsweise welche Texturen und Puffer gebunden werden. Sie legen diesen globalen Status fest, indem Sie verschiedene API-Funktionen aufrufen. Er bleibt wirksam, 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 verwendende Kombination, Topologie und Attribute. Eine Pipeline ist unveränderlich. Wenn Sie einige Einstellungen ändern möchten, müssen Sie eine weitere Pipeline erstellen. WebGPU verwendet außerdem Command-Encoder, um Befehle zusammenzufassen und in der Reihenfolge auszuführen, in der sie aufgezeichnet wurden. Dies ist beispielsweise bei der Schattenzuordnung nützlich, bei der die Anwendung in einem einzigen Durchlauf über die Objekte mehrere Befehlsströme aufzeichnen kann, einen für die Schattenkarte jedes Lichts.

Zusammenfassend lässt sich sagen, dass das globale Zustandsmodell von WebGL das Erstellen robuster, zusammensetzbarer Bibliotheken und Anwendungen schwierig und fragil machte. WebGPU reduzierte also die Menge des Zustands, den Entwickler beim Senden von Befehlen an die GPU im Auge behalten mussten.

Nicht mehr synchronisieren

Bei GPUs ist es in der Regel ineffizient, Befehle 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 Multi-Prozess-Architektur verwenden, bei der der GPU-Treiber in einem von JavaScript getrennten Prozess ausgeführt wird.

In WebGL ist beispielsweise für den Aufruf von gl.getError() ein synchroner IPC vom JavaScript-Prozess zum GPU-Prozess und zurück erforderlich. Dies kann auf der CPU-Seite zu einer Blase führen, während die beiden Prozesse kommunizieren.

Um diese Blasen 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 blasenfrei ist und Anwendungen zuverlässige Leistung erzielen.

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 verfügbar, nicht in WebGL.

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, beispielsweise für maschinelles Lernen, physikalische Simulationen und wissenschaftliches Rechnen. Compute-Shader werden parallel von Hunderten oder sogar Tausenden von Threads ausgeführt, was sie bei der Verarbeitung großer Datensätze sehr effizient macht. In diesem ausführlichen Artikel zu WebGPU erfahren Sie mehr über GPU-Computing und weitere Details.

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 daher hervorragend für die Verarbeitung von Videoframes, da es eng in die WebCodecs eingebunden ist.

Das folgende Code-Snippet zeigt, 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äßige Portabilität von Anwendungen

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

Umgang mit Leinwand

WebGL verwaltet den Canvas automatisch, nachdem Sie einen WebGL-Kontext erstellt und Kontextattribute wie Alpha, Antialias, colorSpace, depth, keepDrawingBuffer oder Schablone angegeben haben.

Bei WebGPU hingegen müssen Sie den Canvas selbst verwalten. Um beispielsweise Antialiasing in einer WebGPU zu erreichen, müssen Sie eine Multisample-Textur erstellen und in diese rendern. Anschließend würden Sie die Multisample-Textur in eine normale Textur auflösen und diese Textur auf den Canvas zeichnen. 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-Canvases in den Browsern Safari, Chrome und Firefox
Die maximale Anzahl von WebGL-Canvases in Safari, Chrome und Firefox (von links nach rechts) – Demo.

Hilfreiche Fehlermeldungen

WebGPU bietet einen Aufrufstack für jede Nachricht, die von der API zurückgegeben wird. Das bedeutet, dass Sie schnell sehen können, wo der Fehler in Ihrem Code aufgetreten ist. Dies ist hilfreich beim Debugging und Beheben von Fehlern.

WebGPU-Fehlermeldungen bieten nicht nur einen Aufrufstack, sondern sind auch leicht verständlich und umsetzbar. Die Fehlermeldungen enthalten normalerweise eine Fehlerbeschreibung und Vorschläge zur Fehlerbehebung.

Mit WebGPU können Sie auch einen benutzerdefinierten label für jedes WebGPU-Objekt angeben. Dieses Label wird dann vom Browser in GPUError-Meldungen, Konsolenwarnungen und Browserentwicklertools 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. Dies ist hilfreich, wenn Sie eine Fehlermeldung erhalten, wenn Sie sich bei der Eingabe des Namens der einheitlichen Variablen vertippen.

Bei WebGPU hingegen ist alles vollständig durch Byte-Offset oder -Index (oft als Standort bezeichnet) verbunden. Sie sind dafür verantwortlich, die Speicherorte für den Code in WGSL und JavaScript zu synchronisieren.

Mipmap-Generierung

In WebGL können Sie den MiP einer Texturebene 0 erstellen und dann gl.generateMipmap() aufrufen. WebGL generiert dann alle anderen MiP-Levels für Sie.

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

Speicherpuffer und Speichertexturen

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. Speicherpuffer, die ähnlich aussehen wie einheitliche Zwischenspeicher, werden nur von WebGPU unterstützt und sind leistungsfähiger und flexibler als einheitliche Zwischenspeicher.

  • Speicherpufferdaten, die an Shader weitergegeben werden, können viel größer sein als einheitliche Zwischenspeicher. Die Spezifikation besagt, dass einheitliche Zwischenspeicherbindungen bis zu 64 KB groß sein können (siehe maxUniformBufferBindingSize). Die maximale Größe einer Speicherpufferbindung beträgt jedoch mindestens 128 MB in WebGPU (siehe maxStorageBufferBindingSize).

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

  • Speicherpufferbindungen unterstützen laufzeitgroße Arrays für flexiblere Algorithmen, während einheitliche Zwischenspeicher-Arraygrößen im Shader bereitgestellt werden müssen.

Speichertexturen werden nur in WebGPU unterstützt und sind für Texturen wie Speicherpuffer zu einheitlichen Zwischenspeichern. Sie sind flexibler als normale Texturen und unterstützen Schreib- und Lesevorgänge mit zufälligem Zugriff.

Änderungen an Puffern und Texturen

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

Bei 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 den Inhalt ändern.

Unterschiede bei der Raumkonvention

In WebGL liegt der Bereich für den Z-Clipbereich zwischen -1 und 1. Bei WebGPU liegt der Z-Clipbereich 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 von der Kamera entfernt sind.

Abbildung von Z-Clipbereich-Bereichen in WebGL und WebGPU
Z-Clipbereich-Bereiche in WebGL und WebGPU.

WebGL verwendet die OpenGL-Konvention, bei der die Y-Achse nach oben und die Z-Achse zum Viewer zeigt. WebGPU verwendet die Metal-Konvention, bei der die Y-Achse nach unten und die Z-Achse außerhalb des Bildschirms liegt. Beachten Sie, dass die Richtung der Y-Achse in der Framebuffer-Koordinate, der Viewport-Koordinate und der Fragment-/Pixel-Koordinate unten liegt. Im Clipspace ist die Richtung der Y-Achse weiterhin nach oben wie bei WebGL.

Danksagungen

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

Außerdem empfehle ich WebGPUFundamentals.org, einen detaillierten Einblick in die Unterschiede zwischen WebGPU und WebGL zu erhalten.