WebGPU ist eine Webgrafik-API, die Webentwicklern einen einheitlichen und schnellen Zugriff auf GPUs bietet. WebGPU stellt moderne Hardwarefunktionen bereit und ermöglicht Rendering- und Berechnungsvorgänge auf einer GPU, ähnlich wie Direct3D 12, Metal und Vulkan.
Das ist zwar richtig, aber diese Geschichte ist unvollständig. WebGPU ist das Ergebnis einer Zusammenarbeit großer Unternehmen wie Apple, Google, Intel, Mozilla und Microsoft. Einige von ihnen erkannten, dass WebGPU mehr als eine JavaScript-API sein könnte, sondern eine plattformübergreifende Grafik-API für Entwickler in verschiedenen Systemen, die nicht dem Web zuzuordnen sind.
Um den primären Anwendungsfall zu erfüllen, wurde in Chrome 113 eine JavaScript API eingeführt. Daneben wurde jedoch noch ein weiteres wichtiges Projekt entwickelt: die C-API webgpu.h. In dieser C-Headerdatei sind alle verfügbaren Verfahren und Datenstrukturen von WebGPU aufgeführt. Es dient als plattformunabhängige Hardwareabstraktionsebene, mit der Sie durch die Bereitstellung einer einheitlichen Schnittstelle über verschiedene Plattformen plattformspezifische Anwendungen erstellen können.
In diesem Dokument erfahren Sie, wie Sie mit WebGPU eine kleine C++-Anwendung schreiben, die sowohl im Web als auch auf bestimmten Plattformen ausgeführt werden kann. Spoiler: Sie sehen dasselbe rote Dreieck, das in einem Browserfenster und einem Desktopfenster angezeigt wird, mit minimalen Anpassungen an Ihrer Codebasis.
Wie funktioniert das?
Die fertige Anwendung finden Sie im Repository der WebGPU-Plattformübergreifenden App.
Die App ist ein minimalistisches C++-Beispiel, das zeigt, wie mit WebGPU Desktop- und Web-Apps aus einer einzigen Codebasis erstellt werden. Intern verwendet es webgpu.h von WebGPU als plattformunabhängige Hardwareabstraktionsebene über einen C++-Wrapper namens webgpu_cpp.h.
Im Web basiert die Anwendung auf Emscripten, wobei Webgpu.h mithilfe von bindings zusätzlich zur JavaScript API implementiert wird. Auf bestimmten Plattformen wie macOS oder Windows kann dieses Projekt mit Dawn erstellt werden, der plattformübergreifenden WebGPU-Implementierung von Chromium. Erwähnenswert ist, dass wgpu-native, eine Rust-Implementierung von webgpu.h, ebenfalls vorhanden ist, aber in diesem Dokument nicht verwendet wird.
Jetzt starten
Zuerst benötigen Sie einen C++-Compiler und CMake, um plattformübergreifende Builds auf standardmäßige Weise zu verarbeiten. Erstellen Sie in einem separaten Ordner eine main.cpp
-Quelldatei und eine CMakeLists.txt
-Builddatei.
Die Datei main.cpp
sollte vorerst eine leere main()
-Funktion enthalten.
int main() {}
Die Datei CMakeLists.txt
enthält grundlegende Informationen zum Projekt. In der letzten Zeile wird „app“ als Name für die ausführbare Datei angegeben und der Quellcode lautet main.cpp
.
cmake_minimum_required(VERSION 3.13) # CMake version check
project(app) # Create project "app"
set(CMAKE_CXX_STANDARD 20) # Enable C++20 standard
add_executable(app "main.cpp")
Führen Sie cmake -B build
aus, um Build-Dateien im Unterordner „build/“ zu erstellen, und cmake --build build
, um die App zu erstellen und die ausführbare Datei zu generieren.
# Build the app with CMake.
$ cmake -B build && cmake --build build
# Run the app.
$ ./build/app
Die App wird ausgeführt, aber es gibt noch keine Ausgabe, da Sie etwas auf dem Bildschirm zeichnen müssen.
Get Dawn
Zum Zeichnen des Dreiecks kannst du Dawn nutzen, die plattformübergreifende WebGPU-Implementierung von Chromium. Dazu gehört die GLFW-C++-Bibliothek zum Zeichnen auf dem Bildschirm. Eine Möglichkeit zum Herunterladen von Dawn besteht darin, es Ihrem Repository als Git-Submodul hinzuzufügen. Mit den folgenden Befehlen wird es in den Unterordner „dawn/“ abgerufen.
$ git init
$ git submodule add https://dawn.googlesource.com/dawn
Fügen Sie dann der Datei CMakeLists.txt
Folgendes hinzu:
- Mit der CMake-Option
DAWN_FETCH_DEPENDENCIES
werden alle Dawn-Abhängigkeiten abgerufen. - Der Unterordner „
dawn/
“ ist im Ziel enthalten. - Ihre App hängt von den Zielen
dawn::webgpu_dawn
,glfw
undwebgpu_glfw
ab, damit Sie sie später in dermain.cpp
-Datei verwenden können.
…
set(DAWN_FETCH_DEPENDENCIES ON)
add_subdirectory("dawn" EXCLUDE_FROM_ALL)
target_link_libraries(app PRIVATE dawn::webgpu_dawn glfw webgpu_glfw)
Fenster öffnen
Jetzt, da Dawn verfügbar ist, können Sie mit GLFW Dinge auf dem Bildschirm zeichnen. Mit dieser in webgpu_glfw
enthaltenen Bibliothek können Sie plattformunabhängigen Code für die Fensterverwaltung schreiben.
Wenn Sie ein Fenster mit dem Namen „WebGPU-Fenster“ mit einer Auflösung von 512 × 512 öffnen möchten, aktualisieren Sie die Datei main.cpp
wie unten beschrieben. Hinweis: Mit glfwWindowHint()
wird hier keine bestimmte Initialisierung der Grafik-API angefordert.
#include <GLFW/glfw3.h>
const uint32_t kWidth = 512;
const uint32_t kHeight = 512;
void Start() {
if (!glfwInit()) {
return;
}
glfwWindowHint(GLFW_CLIENT_API, GLFW_NO_API);
GLFWwindow* window =
glfwCreateWindow(kWidth, kHeight, "WebGPU window", nullptr, nullptr);
while (!glfwWindowShouldClose(window)) {
glfwPollEvents();
// TODO: Render a triangle using WebGPU.
}
}
int main() {
Start();
}
Wenn Sie die App neu erstellen und wie zuvor ausführen, wird jetzt ein leeres Fenster angezeigt. Du machst Fortschritte!
GPU-Gerät abrufen
In JavaScript ist navigator.gpu
der Einstiegspunkt für den Zugriff auf die GPU. In C++ müssen Sie manuell eine wgpu::Instance
-Variable erstellen, die für denselben Zweck verwendet wird. Deklarieren Sie instance
der Einfachheit halber oben in der Datei main.cpp
und rufen Sie wgpu::CreateInstance()
in main()
auf.
…
#include <webgpu/webgpu_cpp.h>
wgpu::Instance instance;
…
int main() {
instance = wgpu::CreateInstance();
Start();
}
Der Zugriff auf die GPU erfolgt aufgrund der Struktur der JavaScript API asynchron. Erstellen Sie in C++ zwei Hilfsfunktionen namens GetAdapter()
und GetDevice()
, die jeweils eine Callback-Funktion mit einer wgpu::Adapter
und einer wgpu::Device
zurückgeben.
#include <iostream>
…
void GetAdapter(void (*callback)(wgpu::Adapter)) {
instance.RequestAdapter(
nullptr,
[](WGPURequestAdapterStatus status, WGPUAdapter cAdapter,
const char* message, void* userdata) {
if (status != WGPURequestAdapterStatus_Success) {
exit(0);
}
wgpu::Adapter adapter = wgpu::Adapter::Acquire(cAdapter);
reinterpret_cast<void (*)(wgpu::Adapter)>(userdata)(adapter);
}, reinterpret_cast<void*>(callback));
}
void GetDevice(void (*callback)(wgpu::Device)) {
adapter.RequestDevice(
nullptr,
[](WGPURequestDeviceStatus status, WGPUDevice cDevice,
const char* message, void* userdata) {
wgpu::Device device = wgpu::Device::Acquire(cDevice);
device.SetUncapturedErrorCallback(
[](WGPUErrorType type, const char* message, void* userdata) {
std::cout << "Error: " << type << " - message: " << message;
},
nullptr);
reinterpret_cast<void (*)(wgpu::Device)>(userdata)(device);
}, reinterpret_cast<void*>(callback));
}
Deklarieren Sie zum einfacheren Zugriff oben in der Datei main.cpp
zwei Variablen wgpu::Adapter
und wgpu::Device
. Aktualisieren Sie die Funktion main()
so, dass GetAdapter()
aufgerufen und der Ergebnis-Callback adapter
zugewiesen wird. Rufen Sie dann GetDevice()
auf und weisen Sie den Ergebnis-Callback device
zu, bevor Sie Start()
aufrufen.
wgpu::Adapter adapter;
wgpu::Device device;
…
int main() {
instance = wgpu::CreateInstance();
GetAdapter([](wgpu::Adapter a) {
adapter = a;
GetDevice([](wgpu::Device d) {
device = d;
Start();
});
});
}
Ein Dreieck zeichnen
Die Auslagerungskette wird in der JavaScript API nicht bereitgestellt, da sie vom Browser verwaltet wird. In C++ müssen Sie sie manuell erstellen. Auch hier sollten Sie der Einfachheit halber oben in der Datei main.cpp
eine Variable wgpu::Surface
deklarieren. Rufen Sie direkt nach dem Erstellen des GLFW-Fensters in Start()
die praktische Funktion wgpu::glfw::CreateSurfaceForWindow()
auf, um eine wgpu::Surface
(ähnlich wie ein HTML-Canvas) zu erstellen, und konfigurieren Sie sie durch Aufrufen der neuen Hilfsfunktion ConfigureSurface()
in InitGraphics()
. Außerdem müssen Sie surface.Present()
aufrufen, um die nächste Textur in der while-Schleife darzustellen. Das hat keine sichtbaren Auswirkungen, da noch kein Rendering erfolgt.
#include <webgpu/webgpu_glfw.h>
…
wgpu::Surface surface;
wgpu::TextureFormat format;
void ConfigureSurface() {
wgpu::SurfaceCapabilities capabilities;
surface.GetCapabilities(adapter, &capabilities);
format = capabilities.formats[0];
wgpu::SurfaceConfiguration config{
.device = device,
.format = format,
.width = kWidth,
.height = kHeight};
surface.Configure(&config);
}
void InitGraphics() {
ConfigureSurface();
}
void Render() {
// TODO: Render a triangle using WebGPU.
}
void Start() {
…
surface = wgpu::glfw::CreateSurfaceForWindow(instance, window);
InitGraphics();
while (!glfwWindowShouldClose(window)) {
glfwPollEvents();
Render();
surface.Present();
instance.ProcessEvents();
}
}
Jetzt ist ein guter Zeitpunkt, um die Rendering-Pipeline mit dem Code unten zu erstellen. Für einen einfacheren Zugriff deklarieren Sie oben in der main.cpp
-Datei eine wgpu::RenderPipeline
-Variable und rufen Sie die Hilfsfunktion CreateRenderPipeline()
in InitGraphics()
auf.
wgpu::RenderPipeline pipeline;
…
const char shaderCode[] = R"(
@vertex fn vertexMain(@builtin(vertex_index) i : u32) ->
@builtin(position) vec4f {
const pos = array(vec2f(0, 1), vec2f(-1, -1), vec2f(1, -1));
return vec4f(pos[i], 0, 1);
}
@fragment fn fragmentMain() -> @location(0) vec4f {
return vec4f(1, 0, 0, 1);
}
)";
void CreateRenderPipeline() {
wgpu::ShaderModuleWGSLDescriptor wgslDesc{};
wgslDesc.code = shaderCode;
wgpu::ShaderModuleDescriptor shaderModuleDescriptor{
.nextInChain = &wgslDesc};
wgpu::ShaderModule shaderModule =
device.CreateShaderModule(&shaderModuleDescriptor);
wgpu::ColorTargetState colorTargetState{.format = format};
wgpu::FragmentState fragmentState{.module = shaderModule,
.targetCount = 1,
.targets = &colorTargetState};
wgpu::RenderPipelineDescriptor descriptor{
.vertex = {.module = shaderModule},
.fragment = &fragmentState};
pipeline = device.CreateRenderPipeline(&descriptor);
}
void InitGraphics() {
…
CreateRenderPipeline();
}
Senden Sie abschließend Renderingbefehle an die GPU in der Render()
-Funktion, die jeden Frame aufgerufen wird.
void Render() {
wgpu::SurfaceTexture surfaceTexture;
surface.GetCurrentTexture(&surfaceTexture);
wgpu::RenderPassColorAttachment attachment{
.view = surfaceTexture.texture.CreateView(),
.loadOp = wgpu::LoadOp::Clear,
.storeOp = wgpu::StoreOp::Store};
wgpu::RenderPassDescriptor renderpass{.colorAttachmentCount = 1,
.colorAttachments = &attachment};
wgpu::CommandEncoder encoder = device.CreateCommandEncoder();
wgpu::RenderPassEncoder pass = encoder.BeginRenderPass(&renderpass);
pass.SetPipeline(pipeline);
pass.Draw(3);
pass.End();
wgpu::CommandBuffer commands = encoder.Finish();
device.GetQueue().Submit(1, &commands);
}
Wenn Sie die App mit CMake neu erstellen und ausführen, wird jetzt das lang erwartete rote Dreieck in einem Fenster angezeigt. Mach eine Pause – du hast es dir verdient.
In WebAssembly kompilieren
Sehen wir uns nun an, welche minimalen Änderungen erforderlich sind, um Ihre vorhandene Codebasis so anzupassen, dass dieses rote Dreieck in einem Browserfenster gezeichnet wird. Auch hier wird die App mit Emscripten erstellt, einem Tool zum Kompilieren von C/C++-Programmen in WebAssembly. Es hat Bindungen, die webgpu.h über der JavaScript API implementieren.
CMake-Einstellungen aktualisieren
Nachdem Emscripten installiert ist, aktualisieren Sie die Build-Datei CMakeLists.txt
so:
Sie müssen lediglich den hervorgehobenen Code ändern.
- Mit
set_target_properties
wird der Zieldatei automatisch die Dateiendung „html“ hinzugefügt. Mit anderen Worten: Sie generieren eine „app.html“-Datei. - Die Option „
USE_WEBGPU
“ für den App-Link ist erforderlich, um die WebGPU-Unterstützung in Emscripten zu aktivieren. Andernfalls kann die Dateimain.cpp
nicht auf die Dateiwebgpu/webgpu_cpp.h
zugreifen. - Die Option „
USE_GLFW
App-Link“ ist hier ebenfalls erforderlich, damit Sie Ihren GLFW-Code wiederverwenden können.
cmake_minimum_required(VERSION 3.13) # CMake version check
project(app) # Create project "app"
set(CMAKE_CXX_STANDARD 20) # Enable C++20 standard
add_executable(app "main.cpp")
if(EMSCRIPTEN)
set_target_properties(app PROPERTIES SUFFIX ".html")
target_link_options(app PRIVATE "-sUSE_WEBGPU=1" "-sUSE_GLFW=3")
else()
set(DAWN_FETCH_DEPENDENCIES ON)
add_subdirectory("dawn" EXCLUDE_FROM_ALL)
target_link_libraries(app PRIVATE dawn::webgpu_dawn glfw webgpu_glfw)
endif()
Code aktualisieren
In Emscripten ist für das Erstellen eines wgpu::surface
ein HTML-Canvas-Element erforderlich. Rufen Sie dazu instance.CreateSurface()
auf und geben Sie den #canvas
-Selektor an, um das entsprechende HTML-Canvas-Element auf der von Emscripten generierten HTML-Seite zu finden.
Rufen Sie anstelle einer while-Schleife emscripten_set_main_loop(Render)
auf, um dafür zu sorgen, dass die Render()
-Funktion mit einer geeigneten Rate aufgerufen wird, die dem Browser und Monitor korrekt entspricht.
#include <GLFW/glfw3.h>
#include <webgpu/webgpu_cpp.h>
#include <iostream>
#if defined(__EMSCRIPTEN__)
#include <emscripten/emscripten.h>
#else
#include <webgpu/webgpu_glfw.h>
#endif
void Start() {
if (!glfwInit()) {
return;
}
glfwWindowHint(GLFW_CLIENT_API, GLFW_NO_API);
GLFWwindow* window =
glfwCreateWindow(kWidth, kHeight, "WebGPU window", nullptr, nullptr);
#if defined(__EMSCRIPTEN__)
wgpu::SurfaceDescriptorFromCanvasHTMLSelector canvasDesc{};
canvasDesc.selector = "#canvas";
wgpu::SurfaceDescriptor surfaceDesc{.nextInChain = &canvasDesc};
surface = instance.CreateSurface(&surfaceDesc);
#else
surface = wgpu::glfw::CreateSurfaceForWindow(instance, window);
#endif
InitGraphics();
#if defined(__EMSCRIPTEN__)
emscripten_set_main_loop(Render, 0, false);
#else
while (!glfwWindowShouldClose(window)) {
glfwPollEvents();
Render();
surface.Present();
instance.ProcessEvents();
}
#endif
}
App mit Emscripten erstellen
Die einzige Änderung, die zum Erstellen der App mit Emscripten erforderlich ist, besteht darin, den cmake
-Befehlen das magische emcmake
-Shell-Skript voranzustellen. Generiere diesmal die Anwendung in einem build-web
-Unterordner und starte einen HTTP-Server. Öffnen Sie abschließend Ihren Browser und rufen Sie build-web/app.html
auf.
# Build the app with Emscripten.
$ emcmake cmake -B build-web && cmake --build build-web
# Start a HTTP server.
$ npx http-server
Nächste Schritte
Das erwartet Sie in Zukunft:
- Verbesserungen bei der Stabilisierung der APIs „webgpu.h“ und „webgpu_cpp.h“.
- Erste Unterstützung von Dawn für Android und iOS.
In der Zwischenzeit können Sie WebGPU-Probleme für Emscripten und Dawn-Probleme mit Vorschlägen und Fragen melden.
Ressourcen
Sie können sich den Quellcode dieser App ansehen.
Wenn Sie mehr über das Erstellen nativer 3D-Anwendungen in C++ mit WebGPU erfahren möchten, lesen Sie die Dokumentation zu WebGPU für C++ und die Beispiele für native WebGPU-Anwendungen mit Dawn.
Wenn Sie sich für Rust interessieren, können Sie sich auch die wgpu-Grafikbibliothek ansehen, die auf WebGPU basiert. Sehen Sie sich die hello-triangle-Demo an.
Danksagung
Dieser Artikel wurde von Corentin Wallez, Kai Ninomiya und Rachel Andrew geprüft.
Foto von Marc-Olivier Jodoin auf Unsplash.