Für Webentwickler ist WebGPU eine Webgrafik-API, die einheitlichen und schnellen Zugriff auf GPUs bietet. WebGPU bietet moderne Hardwarefunktionen und ermöglicht Rendering- und Berechnungsvorgänge auf einer GPU, ähnlich wie Direct3D 12, Metal und Vulkan.
Diese Geschichte ist zwar wahr, aber unvollständig. WebGPU ist das Ergebnis der Zusammenarbeit mit großen Unternehmen wie Apple, Google, Intel, Mozilla und Microsoft. Einige haben unter anderem erkannt, dass eine WebGPU mehr als eine JavaScript API, sondern eine plattformübergreifende Grafik-API für Entwickler in verschiedenen Umgebungen und nicht dem Web sein kann.
Für den primären Anwendungsfall wurde in Chrome 113 eine JavaScript API eingeführt. Zusätzlich wurde jedoch ein weiteres wichtiges Projekt entwickelt: die C API webgpu.h. Diese C-Header-Datei listet alle verfügbaren Verfahren und Datenstrukturen von WebGPU auf. Sie dient als plattformunabhängige Hardware-Abstraktionsebene, mit der Sie plattformspezifische Anwendungen erstellen können, indem Sie eine einheitliche Schnittstelle auf verschiedenen Plattformen bereitstellen.
In diesem Dokument erfahren Sie, wie Sie eine kleine C++ App mit WebGPU schreiben, die sowohl im Web als auch auf bestimmten Plattformen ausgeführt wird. Spoileralarm: Du siehst das gleiche rote Dreieck, das in einem Browserfenster und einem Desktop-Fenster angezeigt wird, mit minimalen Anpassungen deiner Codebasis.
Wie funktioniert die Kanalmitgliedschaft?
Die fertige Anwendung finden Sie im Repository der plattformübergreifenden WebGPU-App.
Die App ist ein minimalistisches C++-Beispiel, das zeigt, wie mit WebGPU Desktop- und Web-Apps aus einer einzigen Codebasis erstellt werden können. Intern verwendet webgpu.h von WebGPU als plattformunabhängige Hardware-Abstraktionsschicht über einen C++-Wrapper namens webgpu_cpp.h.
Im Web basiert die App auf Emscripten, das über Bindungen verfügt, die webgpu.h zusätzlich zur JavaScript API implementiert. Auf bestimmten Plattformen wie macOS oder Windows kann dieses Projekt auf der Grundlage von Dawn erstellt werden, der plattformübergreifenden WebGPU-Implementierung von Chromium. Erwähnenswert ist auch wgpu-native, eine Rust-Implementierung von webgpu.h, die in diesem Dokument aber nicht verwendet wird.
Erste Schritte
Zuerst benötigen Sie einen C++-Compiler und CMake, um plattformübergreifende Builds standardmäßig zu verarbeiten. Erstellen Sie in einem dafür vorgesehenen Ordner eine main.cpp
-Quelldatei und eine CMakeLists.txt
-Build-Datei.
Die Datei main.cpp
sollte vorerst eine leere main()
-Funktion enthalten.
int main() {}
Die Datei CMakeLists.txt
enthält grundlegende Informationen über das Projekt. Die letzte Zeile gibt den Namen der ausführbaren Datei „app“ und den Quellcode main.cpp
an.
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 Anwendung 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 Anwendung wird ausgeführt, aber es gibt noch keine Ausgabe, da Sie eine Möglichkeit benötigen, Dinge auf dem Bildschirm zu zeichnen.
Morgendämmerung
Zum Zeichnen Ihres Dreiecks können Sie Dawn nutzen, die plattformübergreifende WebGPU-Implementierung von Chromium. Dazu gehört auch eine GLFW-C++-Bibliothek zum Zeichnen auf dem Bildschirm. Sie können Dawn z. B. Ihrem Repository als Git-Submodul hinzufügen. Die folgenden Befehle rufen sie in den Unterordner „dawn/“ ab.
$ git init
$ git submodule add https://dawn.googlesource.com/dawn
Fügen Sie diese dann so an die Datei CMakeLists.txt
an:
- Mit der Option CMake
DAWN_FETCH_DEPENDENCIES
werden alle Dawn-Abhängigkeiten abgerufen. - Der Unterordner „
dawn/
“ ist im Ziel enthalten. - Deine App hängt von
webgpu_cpp
-,webgpu_dawn
-,glfw
- undwebgpu_glfw
-Zielen ab, damit du sie später in dermain.cpp
-Datei verwenden kannst.
…
set(DAWN_FETCH_DEPENDENCIES ON)
add_subdirectory("dawn" EXCLUDE_FROM_ALL)
target_link_libraries(app PRIVATE webgpu_cpp webgpu_dawn glfw webgpu_glfw)
Fenster öffnen
Jetzt, da „Dämmerung“ verfügbar ist, kannst du mit GLFW Dinge auf dem Bildschirm zeichnen. Mit dieser Bibliothek, die der Einfachheit halber in webgpu_glfw
enthalten ist, können Sie Code schreiben, der für die Fensterverwaltung plattformunabhängig ist.
Aktualisieren Sie die Datei main.cpp
wie unten beschrieben, um ein Fenster namens „WebGPU window“ mit einer Auflösung von 512 x 512 zu öffnen. Beachten Sie, dass glfwWindowHint()
hier verwendet wird, um keine bestimmte Grafik-API-Initialisierung anzufordern.
#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 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 der Einfachheit halber instance
am Anfang 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 Form der JavaScript API asynchron. Erstellen Sie in C++ zwei Hilfsfunktionen mit den Namen GetAdapter()
und GetDevice()
, die jeweils eine Callback-Funktion mit einem wgpu::Adapter
und einem 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 für einen einfacheren Zugriff die beiden Variablen wgpu::Adapter
und wgpu::Device
am Anfang der Datei main.cpp
. Aktualisieren Sie die main()
-Funktion so, dass GetAdapter()
aufgerufen wird 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 ist in der JavaScript API nicht verfügbar, da sie vom Browser verarbeitet wird. In C++ müssen Sie sie manuell erstellen. Nochmals der Einfachheit halber sollten Sie oben in der main.cpp
-Datei eine wgpu::Surface
-Variable deklarieren. Direkt nach dem Erstellen des GLFW-Fensters in Start()
rufen Sie die praktische wgpu::glfw::CreateSurfaceForWindow()
-Funktion auf, um ein wgpu::Surface
(ähnlich einem HTML-Canvas) zu erstellen und zu konfigurieren, indem Sie die neue Hilfsfunktion ConfigureSurface()
in InitGraphics()
aufrufen. Außerdem müssen Sie surface.Present()
aufrufen, um die nächste Textur in der while-Schleife zu präsentieren. Dies hat keinen sichtbaren Effekt, da noch kein Rendering stattfindet.
#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 folgenden Code zu erstellen. Deklarieren Sie für einen einfacheren Zugriff eine wgpu::RenderPipeline
-Variable am Anfang der main.cpp
-Datei 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 Funktion Render()
, die jeden Frame aufrufen.
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 erwartete rote Dreieck in einem Fenster angezeigt. Mach eine Pause – du hast es dir verdient.
In WebAssembly kompilieren
Werfen wir nun einen Blick auf die minimalen Änderungen, die erforderlich sind, um Ihre vorhandene Codebasis anzupassen, um dieses rote Dreieck in einem Browserfenster zu zeichnen. Die App basiert ebenfalls auf Emscripten, einem Tool zum Kompilieren von C/C++-Programmen in WebAssembly, das über Bindungen zusätzlich zur JavaScript API webgpu.h implementiert.
CMake-Einstellungen aktualisieren
Aktualisieren Sie nach der Installation von Emscripten die Build-Datei CMakeLists.txt
so.
Sie müssen lediglich den hervorgehobenen Code ändern.
set_target_properties
wird verwendet, um die Dateiendung „html“ automatisch zur Zieldatei hinzuzufügen. Sie generieren also die Datei „app.html“.- Die Option für den App-Link „
USE_WEBGPU
“ ist erforderlich, um die WebGPU-Unterstützung in Emscripten zu aktivieren. Andernfalls kann Ihremain.cpp
-Datei nicht auf diewebgpu/webgpu_cpp.h
-Datei zugreifen. - Die Option zum App-Link „
USE_GLFW
“ 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 webgpu_cpp webgpu_dawn glfw webgpu_glfw)
endif()
Code aktualisieren
In Emscripten ist zum Erstellen eines wgpu::surface
ein HTML-Canvas-Element erforderlich. Rufen Sie dazu instance.CreateSurface()
auf und geben Sie den #canvas
-Selektor an, der dem entsprechenden HTML-Canvas-Element in der von Emscripten generierten HTML-Seite entspricht.
Rufen Sie statt einer Dauerschleife emscripten_set_main_loop(Render)
auf, damit die Funktion Render()
mit einer flüssigen Geschwindigkeit aufgerufen wird, die korrekt an den Browser und Monitor angepasst ist.
#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 erforderlich ist, um die App mit Emscripten zu erstellen, besteht darin, den cmake
-Befehlen das magische emcmake
-Shell-Script voranzustellen. Generieren Sie die App dieses Mal im Unterordner build-web
und starten Sie 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
Folgendes erwartet Sie in Zukunft:
- Verbesserungen bei der Stabilisierung der APIs webgpu.h und webgpu_cpp.h
- Ursprüngliche Unterstützung für Android und iOS in der Morgendämmerung
Bitte melde uns in der Zwischenzeit bitte WebGPU-Probleme für Emscripten und Dawn-Probleme mit Vorschlägen und Fragen.
Weitere Informationen
Erkunden Sie den Quellcode dieser App.
Wenn Sie mehr darüber erfahren möchten, wie Sie native 3D-Anwendungen in C++ mit WebGPU von Grund auf neu erstellen können, lesen Sie die Informationen unter Learn WebGPU for C++ und Dawn Native WebGPU Examples.
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 veröffentlicht.
Foto von Marc-Olivier Jodoin bei Unsplash.