Anwendung mit WebGPU erstellen

François Beaufort
François Beaufort

Veröffentlicht am 20. Juli 2023, zuletzt aktualisiert am 27. August 2025

Für Webentwickler ist WebGPU eine Webgrafik-API, die einheitlichen und schnellen Zugriff auf GPUs bietet. WebGPU macht moderne Hardwarefunktionen verfügbar und ermöglicht Rendering- und Rechenvorgänge auf einer GPU, ähnlich wie Direct3D 12, Metal und Vulkan.

Das stimmt zwar, aber die Geschichte ist unvollständig. WebGPU ist das Ergebnis einer Zusammenarbeit von großen Unternehmen wie Apple, Google, Intel, Mozilla und Microsoft. Einige erkannten, dass WebGPU mehr als nur eine JavaScript-API sein könnte, nämlich eine plattformübergreifende Grafik-API für Entwickler in verschiedenen Ökosystemen, die nicht nur für das Web gedacht ist.

Um den primären Anwendungsfall zu erfüllen, wurde in Chrome 113 eine JavaScript API eingeführt. Es wurde jedoch ein weiteres wichtiges Projekt entwickelt: die C-API webgpu.h. In dieser C-Headerdatei sind alle verfügbaren Prozeduren und Datenstrukturen von WebGPU aufgeführt. Sie dient als plattformunabhängige Hardwareabstraktionsschicht, mit der Sie plattformspezifische Anwendungen erstellen können, indem sie eine einheitliche Schnittstelle für verschiedene Plattformen bietet.

In diesem Dokument erfahren Sie, wie Sie eine kleine C++-Anwendung mit WebGPU schreiben, die sowohl im Web als auch auf bestimmten Plattformen ausgeführt wird. Spoiler: Sie erhalten dasselbe rote Dreieck, das in einem Browserfenster und einem Desktopfenster angezeigt wird, und müssen nur minimale Anpassungen an Ihrem Code vornehmen.

Screenshot eines roten Dreiecks, das von WebGPU gerendert wird, in einem Browserfenster und einem Desktopfenster unter macOS.
Dasselbe Dreieck, das von WebGPU in einem Browserfenster und einem Desktopfenster gerendert wird.

Wie funktioniert das?

Die fertige Anwendung finden Sie im Repository WebGPU cross-platform app.

Die App ist ein minimalistisches C++-Beispiel, das zeigt, wie WebGPU verwendet werden kann, um Desktop- und Web-Apps aus einer einzigen Codebasis zu erstellen. Intern wird webgpu.h von WebGPU als plattformunabhängige Hardwareabstraktionsschicht über einen C++-Wrapper namens webgpu_cpp.h verwendet.

Im Web wird die App mit emdawnwebgpu (Emscripten Dawn WebGPU) erstellt, das Bindungen enthält, die webgpu.h auf der JavaScript API implementieren. Auf bestimmten Plattformen wie macOS oder Windows kann dieses Projekt mit Dawn, der plattformübergreifenden WebGPU-Implementierung von Chromium, erstellt werden. Es gibt auch wgpu-native, eine Rust-Implementierung von webgpu.h, die in diesem Dokument jedoch nicht verwendet wird.

Jetzt starten

Zuerst benötigen Sie einen C++-Compiler und CMake, um plattformübergreifende Builds auf standardisierte Weise zu verarbeiten. Erstellen Sie in einem dedizierten 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 zum Projekt. In der letzten Zeile wird angegeben, dass der ausführbare Name „app“ ist und der Quellcode main.cpp lautet.

cmake_minimum_required(VERSION 3.22) # 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 in einem 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 eine Möglichkeit benötigen, Dinge auf dem Bildschirm darzustellen.

Dawn abrufen

Um das Dreieck zu zeichnen, können Sie Dawn verwenden, die plattformübergreifende WebGPU-Implementierung von Chromium. Dazu gehört die GLFW-C++-Bibliothek zum Zeichnen auf dem Bildschirm. Eine Möglichkeit, Dawn herunterzuladen, besteht darin, es als Git-Submodul zu Ihrem Repository hinzuzufügen. Mit den folgenden Befehlen wird sie in einem Unterordner „dawn/“ abgerufen.

$ git init
$ git submodule add https://dawn.googlesource.com/dawn

Hängen Sie dann Folgendes an die Datei CMakeLists.txt an:

  • Mit der CMake-Option DAWN_FETCH_DEPENDENCIES werden alle Dawn-Abhängigkeiten abgerufen.
  • Mit der CMake-Option DAWN_BUILD_MONOLITHIC_LIBRARY werden alle Dawn-Komponenten in einer einzigen Bibliothek zusammengefasst.
  • Der Unterordner dawn/ ist im Ziel enthalten.
  • Ihre App hängt von den Zielen webgpu_dawn, webgpu_glfw und glfw ab, damit Sie sie später in der Datei main.cpp verwenden können.

set(DAWN_FETCH_DEPENDENCIES ON)
set(DAWN_BUILD_MONOLITHIC_LIBRARY STATIC)
add_subdirectory("dawn" EXCLUDE_FROM_ALL)

target_link_libraries(app PRIVATE webgpu_dawn webgpu_glfw glfw)

Fenster öffnen

Jetzt, da Dawn verfügbar ist, können Sie mit GLFW Elemente auf dem Bildschirm zeichnen. Diese Bibliothek ist in webgpu_glfw enthalten und ermöglicht es Ihnen, plattformunabhängigen Code für die Fensterverwaltung zu schreiben.

Wenn Sie ein Fenster mit dem Namen „WebGPU window“ und einer Auflösung von 512 × 512 öffnen möchten, aktualisieren Sie die Datei main.cpp wie unten beschrieben. Beachten Sie, dass glfwWindowHint() hier verwendet wird, um keine bestimmte Initialisierung der Grafik-API 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 jetzt 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 instance der Einfachheit halber oben in der Datei main.cpp und rufen Sie wgpu::CreateInstance() in Init() auf.

#include <webgpu/webgpu_cpp.h>


wgpu::Instance instance;


void Init() {
  static const auto kTimedWaitAny = wgpu::InstanceFeatureName::TimedWaitAny;
  wgpu::InstanceDescriptor instanceDesc{.requiredFeatureCount = 1,
                                        .requiredFeatures = &kTimedWaitAny};
  instance = wgpu::CreateInstance(&instanceDesc);
}

int main() {
  Init();
  Start();
}

Deklarieren Sie oben in der Datei main.cpp zwei Variablen: wgpu::Adapter und wgpu::Device. Aktualisieren Sie die Funktion Init() so, dass instance.RequestAdapter() aufgerufen und der zugehörige Ergebnis-Callback adapter zugewiesen wird. Rufen Sie dann adapter.RequestDevice() auf und weisen Sie den zugehörigen Ergebnis-Callback device zu.

#include <iostream>

#include <dawn/webgpu_cpp_print.h>


wgpu::Adapter adapter;
wgpu::Device device;


void Init() {
  

  wgpu::Future f1 = instance.RequestAdapter(
      nullptr, wgpu::CallbackMode::WaitAnyOnly,
      [](wgpu::RequestAdapterStatus status, wgpu::Adapter a,
         wgpu::StringView message) {
        if (status != wgpu::RequestAdapterStatus::Success) {
          std::cout << "RequestAdapter: " << message << "\n";
          exit(0);
        }
        adapter = std::move(a);
      });
  instance.WaitAny(f1, UINT64_MAX);

  wgpu::DeviceDescriptor desc{};
  desc.SetUncapturedErrorCallback([](const wgpu::Device&,
                                     wgpu::ErrorType errorType,
                                     wgpu::StringView message) {
    std::cout << "Error: " << errorType << " - message: " << message << "\n";
  });

  wgpu::Future f2 = adapter.RequestDevice(
      &desc, wgpu::CallbackMode::WaitAnyOnly,
      [](wgpu::RequestDeviceStatus status, wgpu::Device d,
         wgpu::StringView message) {
        if (status != wgpu::RequestDeviceStatus::Success) {
          std::cout << "RequestDevice: " << message << "\n";
          exit(0);
        }
        device = std::move(d);
      });
  instance.WaitAny(f2, UINT64_MAX);
}

Dreieck zeichnen

Die Swap Chain wird in der JavaScript API nicht verfügbar gemacht, da der Browser sich darum kümmert. In C++ müssen Sie sie manuell erstellen. Deklarieren Sie der Einfachheit halber noch einmal eine wgpu::Surface-Variable oben in der Datei main.cpp. Rufen Sie direkt nach dem Erstellen des GLFW-Fensters in Start() die praktische Funktion wgpu::glfw::CreateSurfaceForWindow() auf, um ein wgpu::Surface (ähnlich einem HTML-Canvas) zu erstellen und es 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. 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,
                                    .presentMode = wgpu::PresentMode::Fifo};
  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. Für einen einfacheren Zugriff deklarieren Sie oben in der Datei main.cpp 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::ShaderSourceWGSL wgsl{{.code = shaderCode}};

  wgpu::ShaderModuleDescriptor shaderModuleDescriptor{.nextInChain = &wgsl};
  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 schließlich in der Funktion Render(), die in jedem Frame aufgerufen wird, Rendering-Befehle an die GPU.

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 endlich das lang ersehnte 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

Sehen wir uns nun die minimalen Änderungen an, die erforderlich sind, um das rote Dreieck in einem Browserfenster zu zeichnen. Die App wird wieder mit emdawnwebgpu (Emscripten Dawn WebGPU) erstellt, das Bindungen enthält, die webgpu.h auf der JavaScript API implementieren. Dazu wird Emscripten verwendet, ein Tool zum Kompilieren von C/C++-Programmen in WebAssembly.

CMake-Einstellungen aktualisieren

Nachdem Emscripten installiert wurde, aktualisieren Sie die Build-Datei CMakeLists.txt so: Der markierte Code ist das Einzige, was Sie ändern müssen.

  • Mit set_target_properties wird der Zieldatei automatisch die Dateiendung „html“ hinzugefügt. Mit anderen Worten: Sie generieren eine Datei namens „app.html“.
  • Die emdawnwebgpu_cpp-Ziellinkbibliothek ermöglicht die WebGPU-Unterstützung in Emscripten. Ohne sie kann über Ihre main.cpp-Datei nicht auf die webgpu/webgpu_cpp.h-Datei zugegriffen werden.
  • Mit der Option für ASYNCIFY=1-App-Links kann synchroner C++-Code mit asynchronem JavaScript interagieren.
  • Die App-Link-Option USE_GLFW=3 weist Emscripten an, die integrierte JavaScript-Implementierung der GLFW 3 API zu verwenden.
cmake_minimum_required(VERSION 3.22) # CMake version check
project(app)                         # Create project "app"
set(CMAKE_CXX_STANDARD 20)           # Enable C++20 standard

add_executable(app "main.cpp")

set(DAWN_FETCH_DEPENDENCIES ON)
set(DAWN_BUILD_MONOLITHIC_LIBRARY STATIC)
add_subdirectory("dawn" EXCLUDE_FROM_ALL)

if(EMSCRIPTEN)
  set_target_properties(app PROPERTIES SUFFIX ".html")
  target_link_libraries(app PRIVATE emdawnwebgpu_cpp webgpu_glfw)
  target_link_options(app PRIVATE "-sASYNCIFY=1" "-sUSE_GLFW=3")
else()
  target_link_libraries(app PRIVATE webgpu_dawn webgpu_glfw glfw)
endif()

Code aktualisieren

Rufen Sie anstelle einer while-Schleife emscripten_set_main_loop(Render) auf, damit die Render()-Funktion mit einer angemessenen Rate aufgerufen wird, die mit dem Browser und dem Monitor übereinstimmt.

#include <iostream>

#include <GLFW/glfw3.h>
#if defined(__EMSCRIPTEN__)
#include <emscripten/emscripten.h>
#endif
#include <dawn/webgpu_cpp_print.h>
#include <webgpu/webgpu_cpp.h>
#include <webgpu/webgpu_glfw.h>
void Start() {
  
#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 Shell-Skript emcmake voranzustellen. Generieren Sie die App dieses Mal in einem 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

Das erwartet Sie in Zukunft:

  • Verbesserungen bei der Stabilisierung der APIs webgpu.h und webgpu_cpp.h.
  • Erste Unterstützung 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 die Erstellung nativer 3D-Anwendungen in C++ mit WebGPU erfahren möchten, sehen Sie sich die Dokumentation zu WebGPU für C++ und die Dawn Native WebGPU-Beispiele an.

Wenn Sie sich für Rust interessieren, können Sie sich auch die auf WebGPU basierende Grafikbibliothek wgpu ansehen. 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.