Anwendung mit WebGPU erstellen

François Beaufort
François Beaufort

Veröffentlicht: 20. Juli 2023, zuletzt aktualisiert: 17. Juni 2025

Für Webentwickler ist WebGPU eine Webgrafik-API, die 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, nämlich eine plattformübergreifende Grafik-API für Entwickler in verschiedenen Systemen, nicht nur im Web.

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. Sie dient als plattformunabhängige Hardwareabstraktionsschicht, mit der Sie plattformspezifische Anwendungen erstellen können, indem Sie eine einheitliche Schnittstelle für verschiedene Plattformen bereitstellen.

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.

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

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 Sie mit WebGPU Desktop- und Webanwendungen aus einer einzigen Codebasis erstellen. Intern verwendet es webgpu.h von WebGPU als plattformunabhängige Hardwareabstraktionsschicht über einen C++-Wrapper namens webgpu_cpp.h.

Im Web wird die App mit emdawnwebgpu (Emscripten Dawn WebGPU) erstellt, das Bindungen enthält, die webgpu.h über der JavaScript API implementieren. Auf bestimmten Plattformen wie macOS oder Windows kann dieses Projekt mit Dawn erstellt werden, der plattformübergreifenden WebGPU-Implementierung von Chromium. 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 standardmäßige Weise zu verarbeiten. Erstellen Sie in einem separaten Ordner eine main.cpp-Quelldatei und eine CMakeLists.txt-Builddatei.

Die main.cpp-Datei 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 Name der ausführbaren Datei „app“ und der Quellcode main.cpp ist.

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 können Sie Dawn verwenden, die plattformübergreifende WebGPU-Implementierung von Chromium. Dazu gehört die C++-Bibliothek GLFW zum Zeichnen auf dem Bildschirm. Eine Möglichkeit, Dawn herunterzuladen, 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 und webgpu_glfw ab, damit Sie sie später in der main.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. Diese Bibliothek ist der Einfachheit halber in webgpu_glfw enthalten und ermöglicht das Schreiben von Code, der plattformunabhängig für die Fensterverwaltung ist.

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!

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 eine wgpu::Instance-Variable manuell 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() {
  wgpu::InstanceDescriptor instanceDesc{
      .capabilities = {.timedWaitAnyEnable = true}};
  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 Callback für das Ergebnis adapter zugewiesen wird. Rufen Sie dann adapter.RequestDevice() auf und weisen Sie den Callback für das Ergebnis 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 Auslagerungskette wird in der JavaScript API nicht bereitgestellt, da der Browser dafür zuständig ist. In C++ müssen Sie sie manuell erstellen. Deklarieren Sie zur Vereinfachung noch einmal oben in der Datei main.cpp eine Variable wgpu::Surface. 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 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, die Renderpipeline mit dem folgenden Code 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::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 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.

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

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 emdawnwebgpu (Emscripten Dawn WebGPU) erstellt, das Bindungen enthält, die webgpu.h über der JavaScript API implementieren. Es verwendet Emscripten, ein Tool zum Kompilieren von C/C++-Programmen in WebAssembly.

CMake-Einstellungen aktualisieren

Nachdem Emscripten installiert ist, aktualisieren Sie die Build-Datei CMakeLists.txt so: Sie müssen nur den markierten 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 emdawnwebgpu_cpp-Zielverknüpfungsbibliothek ermöglicht die WebGPU-Unterstützung in Emscripten. Andernfalls kann die main.cpp-Datei nicht auf die webgpu/webgpu_cpp.h-Datei zugreifen.
  • Mit der Option ASYNCIFY=1 können Sie synchronen C++-Code mit asynchronem JavaScript interagieren lassen.
  • Mit der App-Link-Option USE_GLFW=3 weist Emscripten an, die integrierte JavaScript-Implementierung der GLFW 3 API zu verwenden.
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")

set(DAWN_FETCH_DEPENDENCIES ON)
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 dawn::webgpu_dawn glfw webgpu_glfw)
endif()

Code aktualisieren

Verwenden Sie anstelle einer While-Schleife emscripten_set_main_loop(Render), damit die Render()-Funktion mit einer angemessenen Geschwindigkeit 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 emcmake-Shell-Script voranzustellen. Generieren Sie die App diesmal in einem Unterordner von 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 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 Demo „Hallo Dreieck“ an.

Danksagung

Dieser Artikel wurde von Corentin Wallez, Kai Ninomiya und Rachel Andrew geprüft.

Foto von Marc-Olivier Jodoin auf Unsplash.