WebGPU API
Limited availability
This feature is not Baseline because it does not work in some of the most widely-used browsers.
Experimentell: Dies ist eine experimentelle Technologie
Überprüfen Sie die Browser-Kompatibilitätstabelle sorgfältig vor der Verwendung auf produktiven Webseiten.
Sicherer Kontext: Diese Funktion ist nur in sicheren Kontexten (HTTPS) in einigen oder allen unterstützenden Browsern verfügbar.
Die WebGPU API ermöglicht es Webentwicklerinnen und -entwicklern, die GPU (Graphics Processing Unit) des zugrunde liegenden Systems zu nutzen, um Hochleistungsberechnungen durchzuführen und komplexe Bilder zu zeichnen, die im Browser gerendert werden können.
WebGPU ist der Nachfolger von WebGL, bietet eine bessere Kompatibilität mit modernen GPUs, Unterstützung für allgemeine GPU-Berechnungen, schnellere Operationen und Zugriff auf fortschrittlichere GPU-Funktionen.
Konzepte und Verwendung
Es ist fair zu sagen, dass WebGL die Möglichkeiten des Web in Bezug auf Grafikfähigkeiten revolutioniert hat, nachdem es um 2011 erstmals erschienen ist. WebGL ist ein JavaScript-Port der OpenGL ES 2.0 Grafikbibliothek, die es Webseiten ermöglicht, Rendering-Berechnungen direkt an die GPU des Geräts zur Verarbeitung mit sehr hohen Geschwindigkeiten zu übergeben und das Ergebnis in einem <canvas>
Element zu rendern.
WebGL und die GLSL Sprache, die zum Schreiben von WebGL-Shader-Code verwendet wird, sind komplex, sodass mehrere WebGL-Bibliotheken erstellt wurden, um WebGL-Apps einfacher schreiben zu können: Beliebte Beispiele sind Three.js, Babylon.js und PlayCanvas. Entwicklerinnen und Entwickler haben diese Tools genutzt, um immersive webbasierte 3D-Spiele, Musikvideos, Trainings- und Modellierungstools, VR- und AR-Erfahrungen und mehr zu bauen.
WebGL hat jedoch einige grundlegende Probleme, die angegangen werden mussten:
- Seit der Veröffentlichung von WebGL ist eine neue Generation nativer GPU-APIs erschienen - die beliebtesten sind Microsofts Direct3D 12, Apples Metal und The Khronos Group's Vulkan - die eine Vielzahl neuer Funktionen bieten. Es sind keine weiteren Updates für OpenGL (und damit WebGL) geplant, sodass es keine dieser neuen Funktionen erhalten wird. WebGPU hingegen wird künftig neue Funktionen hinzugefügt bekommen.
- WebGL basiert vollständig auf der Verwendung von Grafikzeichnung und deren Rendering auf einer Canvas. Es kann allgemeine GPU-Berechnungen (GPGPU) nicht sehr gut verarbeiten. GPGPU-Berechnungen werden für viele verschiedene Anwendungsfälle, zum Beispiel auf Maschinenlernmodellen basierende, immer wichtiger.
- 3D-Grafikanwendungen werden zunehmend anspruchsvoller, sowohl in Bezug auf die Anzahl der Objekte, die gleichzeitig gerendert werden sollen, als auch auf die Verwendung neuer Rendering-Funktionen.
WebGPU löst diese Probleme, indem es eine aktualisierte allgemeine Architektur bietet, die mit modernen GPU-APIs kompatibel ist und sich "webbiger" anfühlt. Es unterstützt Grafikrendering, hat aber auch erstklassige Unterstützung für GPGPU-Berechnungen. Das Rendering einzelner Objekte ist auf der CPU-Seite deutlich günstiger, und es unterstützt moderne GPU-Rendering-Funktionen wie berechnungsbasierte Partikel und Post-Processing-Filter wie Farbeffekte, Schärfen und Tiefenschärfen-Simulation. Darüber hinaus kann es teure Berechnungen wie Culling und Transformation von geskinnten Modellen direkt auf der GPU verarbeiten.
Allgemeines Modell
Zwischen einer Geräte-GPU und einem Browser, der die WebGPU API ausführt, gibt es mehrere Abstraktionsschichten. Es ist nützlich, diese zu verstehen, wenn Sie beginnen, WebGPU zu lernen:
-
Physische Geräte haben GPUs. Die meisten Geräte haben nur eine GPU, aber einige haben mehr als eine. Verschiedene GPU-Typen sind verfügbar:
- Integrierte GPUs, die sich auf demselben Board wie die CPU befinden und deren Speicher teilen.
- Dedizierte GPUs, die auf ihrem eigenen Board unabhängig von der CPU leben.
- Software-"GPUs", implementiert auf der CPU.
Hinweis: Das obige Diagramm geht von einem Gerät mit nur einer GPU aus.
-
Eine native GPU-API, die Teil des Betriebssystems ist (z.B. Metal auf macOS), ist eine Programmierschnittstelle, die es nativen Anwendungen ermöglicht, die Fähigkeiten der GPU zu nutzen. API-Anweisungen werden über einen Treiber an die GPU gesendet (und Antworten empfangen). Es ist möglich, dass ein System mehrere native OS-APIs und -Treiber zur Kommunikation mit der GPU hat, obwohl das obige Diagramm von einem Gerät mit nur einer nativen API/einem Treiber ausgeht.
-
Eine Browser-WebGPU-Implementierung übernimmt die Kommunikation mit der GPU über einen nativen GPU-API-Treiber. Ein WebGPU-Adapter stellt effektiv eine physische GPU und einen Treiber dar, der im zugrunde liegenden System in Ihrem Code verfügbar ist.
-
Ein logisches Gerät ist eine Abstraktion, über die eine einzelne Web-App in einer kapselnden Weise auf GPU-Funktionen zugreifen kann. Logische Geräte müssen Multiplexing-Funktionen bereitstellen. Eine GPU des physischen Geräts wird von vielen Anwendungen und Prozessen gleichzeitig genutzt, möglicherweise auch von vielen Web-Apps. Jede Web-App muss isolierten Zugang zu WebGPU erhalten können, aus Sicherheits- und logischen Gründen.
Zugriff auf ein Gerät
Ein logisches Gerät — dargestellt durch eine GPUDevice
Objektinstanz — ist die Basis, von der aus eine Web-App Zugriff auf alle WebGPU-Funktionen erhält. Der Zugriff auf ein Gerät erfolgt wie folgt:
- Die
Navigator.gpu
Eigenschaft (oderWorkerNavigator.gpu
, wenn Sie WebGPU-Funktionen innerhalb eines Workers verwenden) gibt dasGPU
Objekt für den aktuellen Kontext zurück. - Sie greifen über die
GPU.requestAdapter()
Methode auf einen Adapter zu. Diese Methode akzeptiert ein optionales Einstellungsobjekt, das es Ihnen ermöglicht, z.B. einen leistungsstarken oder energiearmen Adapter anzufordern. Wenn dies nicht eingeschlossen ist, stellt das Gerät Zugriff auf den Standardadapter zur Verfügung, der für die meisten Zwecke ausreichend ist. - Ein Gerät kann über
GPUAdapter.requestDevice()
angefordert werden. Diese Methode akzeptiert ebenfalls ein Optionsobjekt (ein sogenannter Deskriptor), das dazu verwendet werden kann, die genauen Funktionen und Grenzen zu spezifizieren, die das logische Gerät haben soll. Wenn dies nicht enthalten ist, wird das gelieferte Gerät über eine vernünftige allgemeine Spezifikation verfügen, die für die meisten Zwecke ausreichend ist.
Mit einigen Funktionsprüfungen zusammengefügt, könnte der oben beschriebene Prozess wie folgt erreicht werden:
async function init() {
if (!navigator.gpu) {
throw Error("WebGPU not supported.");
}
const adapter = await navigator.gpu.requestAdapter();
if (!adapter) {
throw Error("Couldn't request WebGPU adapter.");
}
const device = await adapter.requestDevice();
//...
}
Pipelines und Shader: Struktur einer WebGPU-App
Eine Pipeline ist eine logische Struktur, die programmierbare Phasen enthält, die abgeschlossen werden, um die Arbeit Ihres Programms zu erledigen. Derzeit kann WebGPU zwei Typen von Pipelines verarbeiten:
-
Eine Render-Pipeline rendert Grafiken, typischerweise in ein
<canvas>
Element, aber sie könnte auch Grafiken im Offscreen rendern. Sie hat zwei Hauptstadien:-
Ein Vertex-Stadium, in dem ein Vertex-Shader Positionierungsdaten entgegennimmt, die in die GPU eingespeist werden, und diese verwendet, um eine Reihe von Vertices im 3D-Raum zu positionieren, indem spezifizierte Effekte wie Rotation, Translation oder Perspektive angewendet werden. Die Vertices werden dann zu Primitiven wie Dreiecken (dem grundlegenden Baustein von gerenderten Grafiken) zusammengesetzt und von der GPU rasterisiert, um herauszufinden, welche Pixel jedes auf der Zeichnungs-Canvas abdecken soll.
-
Ein Fragment-Stadium, in dem ein Fragment-Shader die Farbe jedes vom Vertex-Shader produzierten Primitivs abgedeckten Pixels berechnet. Diese Berechnungen verwenden häufig Eingaben wie Bilder (in Form von Texturen), die Oberflächendetails bereitstellen, und die Position und Farbe virtueller Lichter.
-
-
Eine Compute-Pipeline ist für allgemeine Berechnungen. Eine Compute-Pipeline enthält ein einzelnes Compute-Stadium, in dem ein Compute-Shader allgemeine Daten entgegennimmt, diese parallel über eine bestimmte Anzahl von Arbeitsgruppen verarbeitet und das Ergebnis dann in einem oder mehreren Puffern zurückgibt. Die Puffer können jedes beliebige Datentyp enthalten.
Die oben erwähnten Shader sind Anweisungssets, die von der GPU verarbeitet werden. WebGPU-Shader werden in einer Niedrig-Level-Sprache geschrieben, die Rust-ähnlich ist - WebGPU Shader Language (WGSL).
Es gibt mehrere Möglichkeiten, wie Sie eine WebGPU-App strukturieren könnten, aber der Prozess wird wahrscheinlich die folgenden Schritte umfassen:
- Shader-Module erstellen: Schreiben Sie Ihren Shader-Code in WGSL und verpacken Sie ihn in einem oder mehreren Shader-Modulen.
- Canvas-Kontext abrufen und konfigurieren: Holen Sie sich den
webgpu
Kontext eines<canvas>
Elements und konfigurieren Sie ihn, um Informationen darüber zu erhalten, welche Grafiken von Ihrem GPU-logischen Gerät gerendert werden sollen. Dieser Schritt ist nicht erforderlich, wenn Ihre App keine grafische Ausgabe hat, wie z.B. eine, die nur Compute-Pipelines verwendet. - Ressourcen erstellen, die Ihre Daten enthalten: Die Daten, die Sie von Ihren Pipelines verarbeiten lassen möchten, müssen in GPU-Puffern oder Texturen gespeichert werden, um von Ihrer App darauf zugegriffen zu werden.
- Pipelines erstellen: Definieren Sie Pipeline-Deskriptoren, die die gewünschten Pipelines im Detail beschreiben, einschließlich der erforderlichen Datenstruktur, Bindungen, Shader und Ressourcenlayouts, und erstellen Sie daraus Pipelines. Unsere grundlegenden Demos enthalten nur eine einzige Pipeline, aber nicht triviale Apps enthalten normalerweise mehrere Pipelines für verschiedene Zwecke.
- Ausführen eines Berechnungs-/Rendering-Passes: Dies umfasst eine Reihe von Unterschritten:
- Erstellen Sie einen Command-Encoder, der eine Reihe von Befehlen enkodieren kann, die an die GPU übergeben werden, um ausgeführt zu werden.
- Erstellen Sie ein Pass Encoder Objekt, auf dem Berechnungs-/Renderbefehle ausgegeben werden.
- Führen Sie Befehle aus, um die zu verwendenden Pipelines anzugeben, aus welchem Puffer(n) die erforderlichen Daten abgerufen werden sollen, wie viele Zeichenoperationen ausgeführt werden sollen (im Fall von Render Pipelines) usw.
- Finalisieren Sie die Befehlsliste und kapseln Sie sie in einem Command-Buffer.
- Übermitteln Sie den Command-Buffer über die Befehlsschlange des logischen Geräts an die GPU.
In den folgenden Abschnitten werden wir ein grundlegendes Demo einer Render-Pipeline untersuchen, um Ihnen die Möglichkeit zu geben, zu erkunden, was dafür erforderlich ist. Später werden wir auch ein grundlegendes Compute-Pipeline Beispiel untersuchen und darauf eingehen, wie es sich von der Render-Pipeline unterscheidet.
Grundlegende Render-Pipeline
In unserem grundlegenden Render-Demo geben wir einem <canvas>
Element einen durchgehend blauen Hintergrund und zeichnen ein Dreieck darauf.
Shader-Module erstellen
Wir verwenden den folgenden Shader-Code. Die Vertex-Shader-Phase (@vertex
Block) akzeptiert einen Teil von Daten, der eine Position und eine Farbe enthält, positioniert den Vertex gemäß der angegebenen Position, interpoliert die Farbe und gibt die Daten an die Fragment-Shader-Phase weiter. Die Fragment-Shader-Phase (@fragment
Block) akzeptiert die Daten aus der Vertex-Shader-Phase und färbt den Vertex gemäß der angegebenen Farbe.
const shaders = `
struct VertexOut {
@builtin(position) position : vec4f,
@location(0) color : vec4f
}
@vertex
fn vertex_main(@location(0) position: vec4f,
@location(1) color: vec4f) -> VertexOut
{
var output : VertexOut;
output.position = position;
output.color = color;
return output;
}
@fragment
fn fragment_main(fragData: VertexOut) -> @location(0) vec4f
{
return fragData.color;
}
`;
Hinweis:
In unseren Demos speichern wir unseren Shader-Code in einem Template-Literal, aber Sie können ihn überall speichern, von wo aus er einfach als Text abgerufen und in Ihr WebGPU-Programm eingespeist werden kann. Beispielsweise eine andere übliche Praxis besteht darin, Shader in einem <script>
Element zu speichern und den Inhalt mit Node.textContent
abzurufen. Der richtige Mime-Typ für WGSL ist text/wgsl
.
Um Ihren Shader-Code in WebGPU verfügbar zu machen, müssen Sie ihn in ein GPUShaderModule
über einen GPUDevice.createShaderModule()
Aufruf setzen, indem Sie Ihren Shader-Code als Eigenschaft in einem Deskriptorobjekt übergeben. Beispielsweise:
const shaderModule = device.createShaderModule({
code: shaders,
});
Canvas-Kontext abrufen und konfigurieren
In einer Render-Pipeline müssen wir einen Ort angeben, an dem die Grafiken gerendert werden sollen. In diesem Fall holen wir eine Referenz auf ein Onscreen-<canvas>
Element und rufen dann HTMLCanvasElement.getContext()
mit dem Parameter webgpu
auf, um den GPU-Kontext (eine GPUCanvasContext
Instanz) zurückzugeben.
Von dort aus konfigurieren wir den Kontext mit einem Aufruf von GPUCanvasContext.configure()
, indem wir ihm ein Optionsobjekt übergeben, das das GPUDevice
enthält, von dem die Rendering-Informationen stammen werden, das Format, das die Texturen haben werden, und den Alpha-Modus, der beim Rendern von halbtransparenten Texturen verwendet werden soll.
const canvas = document.querySelector("#gpuCanvas");
const context = canvas.getContext("webgpu");
context.configure({
device: device,
format: navigator.gpu.getPreferredCanvasFormat(),
alphaMode: "premultiplied",
});
Hinweis:
Die beste Praxis zur Bestimmung des Texturformats besteht darin, die GPU.getPreferredCanvasFormat()
Methode zu verwenden; diese wählt das effizienteste Format (entweder bgra8unorm
oder rgba8unorm
) für das Gerät des Benutzers aus.
Erstellen eines Puffers und Schreiben unserer Dreieck-Daten darin
Als Nächstes versehen wir unser WebGPU-Programm mit unseren Daten in einer für es nutzbaren Form. Unsere Daten werden zunächst in einem Float32Array
bereitgestellt, das 8 Datenpunkte für jeden Dreiecks-Vertex enthält — X, Y, Z, W für die Position und R, G, B, A für die Farbe.
const vertices = new Float32Array([
0.0, 0.6, 0, 1, 1, 0, 0, 1, -0.5, -0.6, 0, 1, 0, 1, 0, 1, 0.5, -0.6, 0, 1, 0,
0, 1, 1,
]);
Allerdings haben wir hier ein Problem. Wir müssen unsere Daten in einen GPUBuffer
bekommen. Im Hintergrund wird dieser Puffer direkt in einem sehr eng mit den Kernen der GPU integrierten Speicher gespeichert, um die gewünschte Hochleistungsverarbeitung zu ermöglichen. Als Nebeneffekt kann dieser Speicher von Prozessen, die im Hostsystem laufen, wie dem Browser, nicht zugegriffen werden.
Der GPUBuffer
wird über einen Aufruf von GPUDevice.createBuffer()
erstellt. Wir geben ihm eine Größe gleich der Länge des vertices
Arrays, damit es alle Daten enthalten kann, und VERTEX
und COPY_DST
Nutzungs-Flags, um anzugeben, dass der Puffer als Vertex-Puffer und Ziel von Kopiervorgängen verwendet wird.
const vertexBuffer = device.createBuffer({
size: vertices.byteLength, // make it big enough to store vertices in
usage: GPUBufferUsage.VERTEX | GPUBufferUsage.COPY_DST,
});
Wir könnten den Zugriff auf unsere Daten im GPUBuffer
mit einem Mapping-Vorgang erledigen, wie wir es im Compute-Pipeline Beispiel verwenden, um Daten von der GPU zurück zu JavaScript zu lesen. In diesem Fall verwenden wir jedoch die praktische GPUQueue.writeBuffer()
Convenience-Methode, die als Parameter den Puffer, in den geschrieben werden soll, die Datenquelle, von der geschrieben werden soll, einen Offset-Wert für jeden und die Größe der zu schreibenden Daten entgegennimmt (wir haben die gesamte Länge des Arrays angegeben). Der Browser berechnet dann den effizientesten Weg, die Daten zu schreiben.
device.queue.writeBuffer(vertexBuffer, 0, vertices, 0, vertices.length);
Definieren und Erstellen der Render-Pipeline
Jetzt, wo wir unsere Daten in einem Puffer haben, ist der nächste Teil der Einrichtung, tatsächlich unsere Pipeline zu erstellen, die bereit für das Rendering verwendet zu werden.
Zunächst erstellen wir ein Objekt, das das erforderliche Layout unserer Vertex-Daten beschreibt. Dies beschreibt perfekt, was wir zuvor in unserem vertices
Array und der Vertex-Shader-Phase gesehen haben - jeder Vertex hat Positions- und Farbdaten. Beide sind im float32x4
Format formatiert (das dem WGSL vec4<f32>
Typ entspricht), und die Farbdaten beginnen bei einem Offset von 16 Bytes in jedem Vertex. arrayStride
gibt die Schrittweite an, die Anzahl der Bytes, aus der jeder Vertex besteht, und stepMode
gibt an, dass die Daten pro Vertex abgerufen werden sollen.
const vertexBuffers = [
{
attributes: [
{
shaderLocation: 0, // position
offset: 0,
format: "float32x4",
},
{
shaderLocation: 1, // color
offset: 16,
format: "float32x4",
},
],
arrayStride: 32,
stepMode: "vertex",
},
];
Als Nächstes erstellen wir ein Deskriptorobjekt, das die Konfiguration unserer Render-Pipeline-Stadien spezifiziert. Für beide Shader-Phasen spezifizieren wir das GPUShaderModule
, in dem der relevante Code gefunden werden kann (shaderModule
), und den Namen der Funktion, die als Einstiegspunkt für jede Phase dient.
Darüber hinaus geben wir im Fall der Vertex-Shader-Phase unser vertexBuffers
Objekt zu, um den erwarteten Zustand unserer Vertex-Daten bereitzustellen. Und im Fall der Fragment-Shader-Phase stellen wir ein Array von Farbezielzuständen bereit, die das spezifizierte Rendering-Format angeben (dies entspricht dem Format, das zuvor in unserer Canvas-Kontextkonfiguration spezifiziert wurde).
Wir spezifizieren auch einen primitive
Zustand, der in diesem Fall nur den Typ des zu zeichnenden Primitivs angibt, und ein layout
von auto
. Die layout
Eigenschaft definiert das Layout (Struktur, Zweck und Typ) aller GPU-Ressourcen (Puffer, Texturen usw.), die während der Ausführung der Pipeline verwendet werden. In komplexeren Apps würde dies die Form eines GPUPipelineLayout
Objekts annehmen, das mit GPUDevice.createPipelineLayout()
erstellt wird (Sie können ein Beispiel in unserer Basic Compute Pipeline sehen), das der GPU ermöglicht, herauszufinden, wie die Pipeline im Voraus am effizientesten ausgeführt werden kann. Hier jedoch geben wir den auto
Wert an, der die Pipeline dazu veranlasst, auf der Grundlage aller in den Shader-Code definierten Bindungen ein implizites Bindungsgruppenlayout zu generieren.
const pipelineDescriptor = {
vertex: {
module: shaderModule,
entryPoint: "vertex_main",
buffers: vertexBuffers,
},
fragment: {
module: shaderModule,
entryPoint: "fragment_main",
targets: [
{
format: navigator.gpu.getPreferredCanvasFormat(),
},
],
},
primitive: {
topology: "triangle-list",
},
layout: "auto",
};
Schließlich können wir eine GPURenderPipeline
basierend auf unserem pipelineDescriptor
Objekt erstellen, indem wir es als Parameter in einen GPUDevice.createRenderPipeline()
Methodenaufruf übergeben.
const renderPipeline = device.createRenderPipeline(pipelineDescriptor);
Ausführen eines Rendering-Passes
Nun, da die gesamte Einrichtung abgeschlossen ist, können wir tatsächlich einen Rendering-Pass ausführen und etwas auf unser <canvas>
zeichnen. Um Befehle zu kodieren, die später an die GPU ausgegeben werden sollen, müssen Sie eine GPUCommandEncoder
Instanz erstellen, was durch einen GPUDevice.createCommandEncoder()
Aufruf durchgeführt wird.
const commandEncoder = device.createCommandEncoder();
Als Nächstes beginnen wir den Rendering-Pass, indem wir eine GPURenderPassEncoder
Instanz mit einem GPUCommandEncoder.beginRenderPass()
Aufruf erstellen. Diese Methode nimmt ein Deskriptorobjekt als Parameter, dessen einzig zwingende Eigenschaft ein colorAttachments
Array ist. In diesem Fall spezifizieren wir:
- Eine Texturansicht zum Rendern; wir erstellen eine neue Ansicht vom
<canvas>
übercontext.getCurrentTexture().createView()
. - Dass die Ansicht beim Laden gelöscht und auf eine angegebene Farbe gesetzt werden soll, bevor irgendeine Zeichnung stattfindet. Dies bewirkt den blauen Hintergrund hinter dem Dreieck.
- Dass der Wert des aktuellen Rendering-Passes für diesen Farbezielanhang gespeichert werden soll.
const clearColor = { r: 0.0, g: 0.5, b: 1.0, a: 1.0 };
const renderPassDescriptor = {
colorAttachments: [
{
clearValue: clearColor,
loadOp: "clear",
storeOp: "store",
view: context.getCurrentTexture().createView(),
},
],
};
const passEncoder = commandEncoder.beginRenderPass(renderPassDescriptor);
Nun können wir Methoden des Rendering-Pass-Encoders aufrufen, um unser Dreieck zu zeichnen:
GPURenderPassEncoder.setPipeline()
wird mit unseremrenderPipeline
Objekt als Parameter aufgerufen, um die zu verwendende Pipeline für den Rendering-Pass zu spezifizieren.GPURenderPassEncoder.setVertexBuffer()
wird mit unseremvertexBuffer
Objekt als Parameter aufgerufen, um als Datenquelle zu dienen, die an die Pipeline zum Rendern übergeben wird. Der erste Parameter ist der Slot, um den Vertex-Puffer für den Index des Elements imvertexBuffers
Array zu setzen, das das Layout dieses Puffers beschreibt.GPURenderPassEncoder.draw()
setzt das Zeichnen in Bewegung. Es gibt Daten für drei Vertices in unseremvertexBuffer
, daher setzen wir einen Vertex-Zählwert von3
, um sie alle zu zeichnen.
passEncoder.setPipeline(renderPipeline);
passEncoder.setVertexBuffer(0, vertexBuffer);
passEncoder.draw(3);
Um die Sequenz der Befehle abzuschließen und an die GPU auszugeben, sind drei weitere Schritte erforderlich.
- Wir rufen die
GPURenderPassEncoder.end()
Methode auf, um das Ende der Render-Pass-Befehlsliste zu signalisieren. - Wir rufen die
GPUCommandEncoder.finish()
Methode auf, um die Aufzeichnung der ausgegebenen Befehlssequenz abzuschließen und sie in einGPUCommandBuffer
Objektinstanz zu kapseln. - Wir übermitteln den
GPUCommandBuffer
an die Befehlswarteschlange des Geräts (dargestellt durch eineGPUQueue
Instanz), um an die GPU gesendet zu werden. Die Warteschlange des Geräts ist über dieGPUDevice.queue
Eigenschaft verfügbar, und ein Array vonGPUCommandBuffer
Instanzen kann der Warteschlange über einenGPUQueue.submit()
Aufruf hinzugefügt werden.
Diese drei Schritte können über die folgenden zwei Zeilen erreicht werden:
passEncoder.end();
device.queue.submit([commandEncoder.finish()]);
Grundlegende Compute Pipeline
In unserem grundlegenden Compute-Demo lassen wir die GPU einige Werte berechnen, die in einem Ausgabepuffer gespeichert werden, kopieren die Daten in einen stagingBuffer und mappen diesen stagingBuffer, damit die Daten ausgelesen und in der Konsole ausgegeben werden können.
Die App folgt einer ähnlichen Struktur wie das grundlegende Rendering-Demo. Wir erstellen eine GPUDevice
Referenz wie zuvor und kapseln unseren Shader-Code in ein GPUShaderModule
durch einen GPUDevice.createShaderModule()
Aufruf. Der Unterschied hier ist, dass unser Shader-Code nur eine Shader-Phase hat, eine @compute
Phase:
// Define global buffer size
const BUFFER_SIZE = 1000;
const shader = `
@group(0) @binding(0)
var<storage, read_write> output: array<f32>;
@compute @workgroup_size(64)
fn main(
@builtin(global_invocation_id)
global_id : vec3u,
@builtin(local_invocation_id)
local_id : vec3u,
) {
// Avoid accessing the buffer out of bounds
if (global_id.x >= ${BUFFER_SIZE}) {
return;
}
output[global_id.x] =
f32(global_id.x) * 1000. + f32(local_id.x);
}
`;
Erstellen von Puffern zur Handhabung unserer Daten
In diesem Beispiel erstellen wir zwei GPUBuffer
Instanzen zur Handhabung unserer Daten, einen output
Puffer, um die GPU-Berechnungsergebnisse mit hoher Geschwindigkeit zu schreiben, und einen stagingBuffer
, zu dem wir den Inhalt von output
kopieren werden und der gemappt werden kann, damit JavaScript die Werte zugreifen kann.
output
wird als Speicherpuffer spezifiziert, der die Quelle eines Kopiervorgangs sein wird.stagingBuffer
wird als Puffer spezifiziert, der zum Lesen durch JavaScript gemappt werden kann und das Ziel eines Kopiervorgangs sein wird.
const output = device.createBuffer({
size: BUFFER_SIZE,
usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_SRC,
});
const stagingBuffer = device.createBuffer({
size: BUFFER_SIZE,
usage: GPUBufferUsage.MAP_READ | GPUBufferUsage.COPY_DST,
});
Erstellen eines Bindungsgruppenlayouts
Wenn die Pipeline erstellt wird, spezifizieren wir eine Bindungsgruppe, die für die Pipeline verwendet werden soll. Dies beinhaltet zuerst das Erstellen eines GPUBindGroupLayout
(durch einen Aufruf von GPUDevice.createBindGroupLayout()
), das die Struktur und den Zweck von GPU-Ressourcen wie Puffern definiert, die in dieser Pipeline verwendet werden. Dieses Layout wird als Vorlage für Bindungsgruppen verwendet. In diesem Fall geben wir der Pipeline Zugriff auf einen einzigen Speicherpuffer, der an den Bindungsslot 0 gebunden ist (dies entspricht der relevanten Bindungsnummer in unserem Shader-Code — @binding(0)
), nutzbar im Computationsstadium der Pipeline, und mit dem Zweck des Puffers als storage
.
const bindGroupLayout = device.createBindGroupLayout({
entries: [
{
binding: 0,
visibility: GPUShaderStage.COMPUTE,
buffer: {
type: "storage",
},
},
],
});
Als nächstes erstellen wir eine GPUBindGroup
durch einen Aufruf von GPUDevice.createBindGroup()
. Wir übergeben diesem Methodenaufruf ein Deskriptorobjekt, das das Bindungsgruppenlayout spezifiziert, auf dem diese Bindungsgruppe basieren soll, und die Details der Variablen, die an den im Layout definierten Slot gebunden werden sollen. In diesem Fall deklarieren wir Bindung 0 und spezifizieren, dass der zuvor definierte output
Puffer daran gebunden werden soll.
const bindGroup = device.createBindGroup({
layout: bindGroupLayout,
entries: [
{
binding: 0,
resource: {
buffer: output,
},
},
],
});
Hinweis:
Sie könnten ein implizites Layout abrufen, um es beim Erstellen einer Bindungsgruppe zu nutzen, indem Sie die GPUComputePipeline.getBindGroupLayout()
Methode aufrufen. Es gibt auch eine Version für Render-Pipelines: siehe GPURenderPipeline.getBindGroupLayout()
.
Erstellen einer Compute-Pipeline
Mit all dem oben genannten können wir nun eine Compute-Pipeline erstellen, indem wir GPUDevice.createComputePipeline()
aufrufen und ein Pipelinesdeskriptorobjekt übergeben. Dies funktioniert ähnlich wie das Erstellen einer Render-Pipeline. Wir beschreiben den Compute-Shader, indem wir angeben, in welchem Modul der Code zu finden ist und was der Einstiegspunkt ist. Wir spezifizieren auch ein layout
für die Pipeline, indem wir ein Layout basierend auf dem zuvor definierten bindGroupLayout
über einen GPUDevice.createPipelineLayout()
Aufruf erstellen.
const computePipeline = device.createComputePipeline({
layout: device.createPipelineLayout({
bindGroupLayouts: [bindGroupLayout],
}),
compute: {
module: shaderModule,
entryPoint: "main",
},
});
Ein Unterschied hier zum Layout der Render-Pipeline besteht darin, dass wir keinen Primitivtyp angeben, da wir nichts zeichnen.
Ausführen eines Compute-Passes
Das Ausführen eines Compute-Passes ist in der Struktur dem Ausführen eines Rendering-Passes ähnlich, mit einigen unterschiedlichen Befehlen. Zum Start wird der Pass-Encoder mit GPUCommandEncoder.beginComputePass()
erstellt.
Beim Ausgeben der Befehle spezifizieren wir die Pipeline, um sie in der gleichen Weise wie zuvor zu verwenden, indem wir GPUComputePassEncoder.setPipeline()
verwenden. Wir verwenden jedoch GPUComputePassEncoder.setBindGroup()
, um anzugeben, dass wir unsere bindGroup
verwenden möchten, um die Daten zur Verwendung bei der Berechnung anzugeben, und GPUComputePassEncoder.dispatchWorkgroups()
, um die Anzahl der GPU-Arbeitsgruppen anzugeben, die zur Durchführung der Berechnungen verwendet werden sollen.
Anschließend signalisieren wir das Ende der Render-Pass-Befehlsliste mit GPURenderPassEncoder.end()
.
passEncoder.setPipeline(computePipeline);
passEncoder.setBindGroup(0, bindGroup);
passEncoder.dispatchWorkgroups(Math.ceil(BUFFER_SIZE / 64));
passEncoder.end();
Ergebnisse an JavaScript zurücklesen
Bevor wir die kodierten Befehle zur Ausführung an die GPU übermitteln, indem wir GPUQueue.submit()
verwenden, kopieren wir die Inhalte des output
Puffers auf den stagingBuffer
Puffer mithilfe von GPUCommandEncoder.copyBufferToBuffer()
.
// Copy output buffer to staging buffer
commandEncoder.copyBufferToBuffer(
output,
0, // Source offset
stagingBuffer,
0, // Destination offset
BUFFER_SIZE,
);
// End frame by passing array of command buffers to command queue for execution
device.queue.submit([commandEncoder.finish()]);
Sobald die Ausgabedaten im stagingBuffer
verfügbar sind, verwenden wir die GPUBuffer.mapAsync()
Methode, um die Daten in den Zwischen-Speicher zu mappen, eine Referenz auf den gemappten Bereich zu holen, indem wir GPUBuffer.getMappedRange()
verwenden, die Daten in JavaScript kopieren und sie dann an die Konsole ausgeben. Wir entmappen den stagingBuffer
, sobald wir damit fertig sind.
// map staging buffer to read results back to JS
await stagingBuffer.mapAsync(
GPUMapMode.READ,
0, // Offset
BUFFER_SIZE, // Length
);
const copyArrayBuffer = stagingBuffer.getMappedRange(0, BUFFER_SIZE);
const data = copyArrayBuffer.slice();
stagingBuffer.unmap();
console.log(new Float32Array(data));
GPU Fehlerbehandlung
WebGPU-Aufrufe werden asynchron im GPU-Prozess validiert. Wenn Fehler gefunden werden, wird der problematische Aufruf auf der GPU-Seite als ungültig markiert. Wenn ein weiterer Aufruf gemacht wird, der sich auf den Rückgabewert eines ungültig gemachten Aufrufs stützt, wird dieses Objekt auch als ungültig markiert, und so weiter. Aus diesem Grund werden Fehler in WebGPU als "ansteckend" bezeichnet.
Jede GPUDevice
Instanz unterhält ihren eigenen Fehlerbereichs-Stack. Dieser Stack ist zunächst leer, aber Sie können einen Fehlerbereich setzen, indem Sie GPUDevice.pushErrorScope()
aufrufen, um Fehler eines bestimmten Typs zu erfassen.
Sobald Sie mit dem Erfassen von Fehlern fertig sind, können Sie die Erfassung beenden, indem Sie GPUDevice.popErrorScope()
aufrufen. Dies entfernt den Bereich vom Stack und gibt ein Promise
zurück, das sich zu einem Objekt auflöst (GPUInternalError
, GPUOutOfMemoryError
oder GPUValidationError
), das den ersten in dem Bereich erfassten Fehler beschreibt, oder null
, wenn keine Fehler erfasst wurden.
Wir haben versucht, nützliche Informationen bereitzustellen, um Ihnen zu helfen zu verstehen, warum Fehler in Ihrem WebGPU-Code auftreten, in "Validierung"-Abschnitten, wo es angebracht ist, die Kriterien auflisten, die erfüllt werden müssen, um Fehler zu vermeiden. Siehe beispielsweise den GPUDevice.createBindGroup()
Validierungsabschnitt. Einige dieser Informationen sind komplex; anstatt die Spezifikation zu wiederholen, haben wir uns entschieden, nur Fehlerkriterien aufzulisten, die:
- Nicht offensichtlich sind, beispielsweise Kombinationen von Deskriptoreigenschaften, die Validierungsfehler erzeugen. Es hat keinen Sinn, Ihnen zu sagen sicherzustellen, dass der richtige Struktur des Deskriptorobjekts verwendet wird. Das ist sowohl offensichtlich als auch vage.
- Entwicklerkontrolliert sind. Einige der Fehlerkriterien basieren rein auf internen und sind nicht wirklich für Web-Entwickler relevant.
Weitere Informationen zur Fehlerbehandlung in WebGPU finden Sie im Erklärer — siehe Objekt-Gültigkeit und zerstört-heit und Errors. WebGPU Error Handling Best Practices bieten nützliche praxisnahe Beispiele und Ratschläge.
Hinweis:
Die historische Methode zur Fehlerbehandlung in WebGL besteht darin, eine getError()
Methode bereitzustellen, um Fehlerinformationen zurückzugeben. Dies ist problematisch, da es Fehler synchron zurückgibt, was schlecht für die Leistung ist - jeder Aufruf erfordert eine Rundreise zur GPU und erfordert, dass alle zuvor ausgegebenen Operationen abgeschlossen werden. Sein Zustandsmodell ist auch flach, was bedeutet, dass Fehler zwischen nicht zusammenhängendem Code durchsickern können. Die Erschaffer von WebGPU waren entschlossen, dies zu verbessern.
Schnittstellen
Einstiegspunkt für die API
-
Der Einstiegspunkt für die API — gibt das
GPU
Objekt für den aktuellen Kontext zurück. GPU
-
Der Startpunkt zur Nutzung von WebGPU. Es kann verwendet werden, um einen
GPUAdapter
zurückzugeben. GPUAdapter
-
Stellt einen GPU-Adapter dar. Von hier aus können Sie ein
GPUDevice
, Adapterinformationen, Funktionen und Grenzen anfordern. GPUAdapterInfo
-
Enthält Identifikationsinformationen über einen Adapter.
Konfigurieren von GPUDevices
GPUDevice
-
Stellt ein logisches GPU-Gerät dar. Dies ist die Hauptschnittstelle, über die der Großteil der WebGPU-Funktionalität zugegriffen wird.
GPUSupportedFeatures
-
Ein setlike Objekt, das zusätzliche Funktionalitäten beschreibt, die von einem
GPUAdapter
oderGPUDevice
unterstützt werden. GPUSupportedLimits
-
Beschreibt die Grenzen, die von einem
GPUAdapter
oderGPUDevice
unterstützt werden.
Konfigurieren eines Rendering <canvas>
HTMLCanvasElement.getContext()
— der"webgpu"
contextType
-
Der Aufruf von
getContext()
mit dem"webgpu"
contextType
gibt einGPUCanvasContext
Objektinstanz zurück, das dann mitGPUCanvasContext.configure()
konfiguriert werden kann. GPUCanvasContext
-
Stellt den WebGPU-Rendering-Kontext eines
<canvas>
Elements dar.
Repräsentation von Pipeline-Ressourcen
GPUBuffer
-
Stellt einen Speicherblock dar, der zur Speicherung von Rohdaten für GPU-Operationen verwendet werden kann.
GPUExternalTexture
-
Ein Wrapper-Objekt, das ein Snapshot von einem
HTMLVideoElement
enthält, das als Textur in GPU-Rendering-Operationen verwendet werden kann. GPUSampler
-
Kontrolliert, wie Shader Texturressourcendaten transformieren und filtern.
GPUShaderModule
-
Eine Referenz auf ein internes Shader-Modulobjekt, ein Container für WGSL Shader-Code, der der GPU zur Ausführung durch eine Pipeline eingespeist werden kann.
GPUTexture
-
Ein Container, der verwendet wird, um 1D-, 2D- oder 3D-Datenarrays zu speichern, wie Bilder, die in GPU-Rendering-Operationen verwendet werden.
GPUTextureView
-
Eine Ansicht auf einige Teilmengen der Textur-Subressourcen, die durch eine bestimmte
GPUTexture
definiert sind.
Repräsentation von Pipelines
GPUBindGroup
-
Basierend auf einem
GPUBindGroupLayout
, definiert eineGPUBindGroup
eine Gruppe von Ressourcen, die zusammen gebunden werden und wie diese Ressourcen in Shader-Phasen genutzt werden. GPUBindGroupLayout
-
Definiert die Struktur und den Zweck verwandter GPU-Ressourcen wie Puffer, die in einer Pipeline verwendet werden, und wird als Vorlage beim Erstellen von
GPUBindGroup
s verwendet. GPUComputePipeline
-
Kontrolliert die Compute-Shader-Phase und kann in einem
GPUComputePassEncoder
verwendet werden. GPUPipelineLayout
-
Definiert die
GPUBindGroupLayout
s, die von einer Pipeline verwendet werden.GPUBindGroup
s, die während der Befehlskodierung mit der Pipeline verwendet werden, müssen kompatibleGPUBindGroupLayout
s haben. GPURenderPipeline
-
Kontrolliert die Vertex- und Fragment-Shader-Phasen und kann in einem
GPURenderPassEncoder
oderGPURenderBundleEncoder
verwendet werden.
Kodierung und Einreichung von Befehlen zur GPU
GPUCommandBuffer
-
Stellt eine aufgezeichnete Liste von GPU-Befehlen dar, die zur Ausführung an eine
GPUQueue
übermittelt werden können. GPUCommandEncoder
-
Stellt einen Befehl-Encoder dar, der zum Enkodieren von Befehlen verwendet wird, die an die GPU ausgestellt werden sollen.
GPUComputePassEncoder
-
Encodiert Befehle, die sich auf die Kontrolle der Compute-Shader-Phase beziehen, wie sie von einer
GPUComputePipeline
ausgestellt werden. Teil der Gesamtaktivität des Enkodierens einesGPUCommandEncoder
. GPUQueue
-
kontrolliert die Ausführung von kodierten Befehlen auf der GPU.
GPURenderBundle
-
Ein Container für voraufgezeichnete Befehlsbündel (siehe
GPURenderBundleEncoder
). GPURenderBundleEncoder
-
Wird verwendet, um Befehlsbündel vorab aufzuzeichnen. Diese können in
GPURenderPassEncoder
s über dieexecuteBundles()
Methode beliebig oft wieder verwendet werden. GPURenderPassEncoder
-
Encodiert Befehle, die sich auf die Kontrolle der Vertex- und Fragment-Shader-Phasen beziehen, wie sie von einer
GPURenderPipeline
ausgestellt werden. Teil der Gesamtaktivität des Enkodierens einesGPUCommandEncoder
.
Ausführen von Abfragen in Rendering-Pässen
GPUQuerySet
-
Wird verwendet, um die Ergebnisse von Abfragen bei Pässen, wie Okklusions- oder Zeitstempelabfragen, aufzuzeichnen.
Debugging von Fehlern
GPUCompilationInfo
-
Ein Array von
GPUCompilationMessage
Objekten, generiert vom GPU-Shader-Modul-Compiler, um Probleme mit Shader-Code zu diagnostizieren. GPUCompilationMessage
-
Stellt eine einzelne Informations-, Warnungs- oder Fehlermeldung dar, die vom GPU-Shader-Modul-Compiler generiert wurde.
GPUDeviceLostInfo
-
Wird zurückgegeben, wenn das
GPUDevice.lost
Promise
aufgelöst wird und Informationen darüber gibt, warum das Gerät verloren ging. GPUError
-
Die Basisschnittstelle für Fehler, die von
GPUDevice.popErrorScope
und demuncapturederror
Event sichtbar gemacht werden. GPUInternalError
-
Eine der Arten von Fehlern, die von
GPUDevice.popErrorScope
und demGPUDevice
uncapturederror
Event sichtbar gemacht werden. Zeigt an, dass eine Operation aus system- oder implementierungsspezifischen Gründen fehlgeschlagen ist, selbst wenn alle Validierungsanforderungen erfüllt wurden. GPUOutOfMemoryError
-
Eine der Arten von Fehlern, die von
GPUDevice.popErrorScope
und demGPUDevice
uncapturederror
Event sichtbar gemacht werden. Zeigt an, dass nicht genügend freier Speicher vorhanden war, um die angeforderte Operation abzuschließen. GPUPipelineError
-
Beschreibt ein Pipeline-Versagen. Der Wert, den man erhält, wenn ein von einem
GPUDevice.createComputePipelineAsync()
oderGPUDevice.createRenderPipelineAsync()
zurückgegebenesPromise
zurückgewiesen wird. GPUUncapturedErrorEvent
-
Der Ereignisobjekttyp für das
GPUDevice
uncapturederror
Event. GPUValidationError
-
Eine der Arten von Fehlern, die von
GPUDevice.popErrorScope
und demGPUDevice
uncapturederror
Event sichtbar gemacht werden. Beschreibt einen Anwendungsfehler, der darauf hinweist, dass eine Operation die Validierungseinschränkungen der WebGPU API nicht erfüllt hat.
Sicherheitsanforderungen
Die gesamte API ist nur in einem sicheren Kontext verfügbar.
Beispiele
Spezifikationen
Specification |
---|
WebGPU # gpu-interface |
Browser-Kompatibilität
BCD tables only load in the browser