Anwendung mit WebGPU erstellen

François Beaufort
François Beaufort

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.

Screenshot eines roten Dreiecks mit WebGPU in einem Browserfenster und eines Desktop-Fensters unter macOS.
Dasselbe Dreieck mit WebGPU in einem Browserfenster und einem Desktop-Fenster.

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.

Mehr erfahren

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- und webgpu_glfw-Zielen ab, damit du sie später in der main.cpp-Datei verwenden kannst.
…
set(DAWN_FETCH_DEPENDENCIES ON)
add_subdirectory("dawn" EXCLUDE_FROM_ALL)
target_link_libraries(app PRIVATE webgpu_cpp webgpu_dawn 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!

Screenshot eines leeren macOS-Fensters.
Ein leeres Fenster.

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++ eine Hilfsfunktion GetDevice(), die ein Argument der Callback-Funktion annimmt und mit dem resultierenden wgpu::Device aufruft.

#include <iostream>
…

void GetDevice(void (*callback)(wgpu::Device)) {
  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);
        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);
            },
            userdata);
      },
      reinterpret_cast<void*>(callback));
}

Deklarieren Sie für einen einfacheren Zugriff eine wgpu::Device-Variable am Anfang der main.cpp-Datei und aktualisieren Sie die main()-Funktion so, dass GetDevice() aufgerufen und der Ergebnis-Callback vor dem Start()-Aufruf device zugewiesen wird.

wgpu::Device device;
…

int main() {
  instance = wgpu::CreateInstance();
  GetDevice([](wgpu::Device dev) {
    device = dev;
    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::SwapChain-Variable deklarieren. Rufen Sie direkt nach dem Erstellen des GLFW-Fensters in Start() die praktische wgpu::glfw::CreateSurfaceForWindow()-Funktion auf, um ein wgpu::Surface (ähnlich einem HTML-Canvas) zu erstellen, und verwenden Sie es zum Einrichten der Auslagerungskette, indem Sie die neue Hilfsfunktion SetupSwapChain() in InitGraphics() aufrufen. Außerdem müssen Sie swapChain.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::SwapChain swapChain;

void SetupSwapChain(wgpu::Surface surface) {
  wgpu::SwapChainDescriptor scDesc{
      .usage = wgpu::TextureUsage::RenderAttachment,
      .format = wgpu::TextureFormat::BGRA8Unorm,
      .width = kWidth,
      .height = kHeight,
      .presentMode = wgpu::PresentMode::Fifo};
  swapChain = device.CreateSwapChain(surface, &scDesc);
}

void InitGraphics(wgpu::Surface surface) {
  SetupSwapChain(surface);
}

void Render() {
  // TODO: Render a triangle using WebGPU.
}

void Start() {
  …
  wgpu::Surface surface =
      wgpu::glfw::CreateSurfaceForWindow(instance, window);

  InitGraphics(surface);

  while (!glfwWindowShouldClose(window)) {
    glfwPollEvents();
    Render();
    swapChain.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 = wgpu::TextureFormat::BGRA8Unorm};

  wgpu::FragmentState fragmentState{.module = shaderModule,
                                    .targetCount = 1,
                                    .targets = &colorTargetState};

  wgpu::RenderPipelineDescriptor descriptor{
      .vertex = {.module = shaderModule},
      .fragment = &fragmentState};
  pipeline = device.CreateRenderPipeline(&descriptor);
}

void InitGraphics(wgpu::Surface surface) {
  …
  CreateRenderPipeline();
}

Senden Sie abschließend Renderingbefehle an die GPU in der Funktion Render(), die jeden Frame aufrufen.

void Render() {
  wgpu::RenderPassColorAttachment attachment{
      .view = swapChain.GetCurrentTextureView(),
      .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.

Screenshot eines roten Dreiecks in einem macOS-Fenster.
Ein rotes Dreieck in einem Desktopfenster.

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 Ihre main.cpp-Datei nicht auf die webgpu/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 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};
  wgpu::Surface surface = instance.CreateSurface(&surfaceDesc);
#else
  wgpu::Surface surface =
      wgpu::glfw::CreateSurfaceForWindow(instance, window);
#endif

  InitGraphics(surface);

#if defined(__EMSCRIPTEN__)
  emscripten_set_main_loop(Render, 0, false);
#else
  while (!glfwWindowShouldClose(window)) {
    glfwPollEvents();
    Render();
    swapChain.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
Screenshot eines roten Dreiecks in einem Browserfenster.
Ein rotes Dreieck in einem Browserfenster.

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.

Ressourcen

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 verfasst.

Foto von Marc-Olivier Jodoin bei Unsplash.