Anwendung mit WebGPU erstellen

François Beaufort
François Beaufort

Für Webentwickler ist WebGPU eine Webgrafik-API, die eine einheitliche und schnelle auf GPUs zugreifen können. WebGPU bietet moderne Hardwarefunktionen und ermöglicht Rendering und Rechenvorgängen auf einer GPU, ähnlich wie Direct3D 12, Metal und Vulkan.

Diese Geschichte ist zwar unvollständig. WebGPU ist das Ergebnis einer kollaborativen einschließlich großer Unternehmen wie Apple, Google, Intel, Mozilla und Microsoft Einige von ihnen haben dass WebGPU mehr als eine JavaScript API sein könnte, aber eine plattformübergreifende Grafik API für Entwickler in allen Plattformen – nicht nur im Web

Für den primären Anwendungsfall wurde eine JavaScript API in Chrome 113 eingeführt. Ein weiterer wichtiger Punkt zusammen mit dem Projekt entwickelt: webgpu.h C der API erstellen. In dieser C-Header-Datei sind alle verfügbaren Prozeduren und Datenstrukturen aufgeführt. von WebGPU. Es dient als plattformunabhängige Hardwareabstraktionsebene, können Sie plattformspezifische Anwendungen über eine einheitliche Benutzeroberfläche erstellen. auf verschiedenen Plattformen.

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. Achtung, Sie sehen das gleiche rote Dreieck, das in einem Browserfenster und einem Desktop-Fenster mit minimalen Anpassungen an der Codebasis angezeigt wird.

<ph type="x-smartling-placeholder">
</ph> Screenshot eines roten Dreiecks mit WebGPU in einem Browserfenster und einem Desktopfenster unter macOS. <ph type="x-smartling-placeholder">
</ph> Dasselbe Dreieck mit WebGPU in einem Browserfenster und einem Desktopfenster.

Wie funktioniert das?

Die fertige Anwendung finden Sie im Repository der plattformübergreifenden WebGPU-Apps.

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 wurde die Anwendung mit Emscripten erstellt, 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.

Erste Schritte

Zuerst benötigen Sie einen C++-Compiler und CMake, um plattformübergreifende Builds standardmäßig zu verarbeiten. Erstellen Sie in einem speziellen Ordner 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. Die letzte Zeile gibt den Namen der ausführbaren Datei an: "app". 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 in einem "build/" zu erstellen. Unterordner 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 Dinge auf dem Bildschirm zeichnen möchten.

Morgendämmerung

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, das Projekt Ihrem Repository als Git-Submodul hinzuzufügen. Die folgenden Befehle rufen ihn in der Morgenröte ab/ Unterordner.

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

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

  • Mit der CMake-Option DAWN_FETCH_DEPENDENCIES werden alle Dawn-Abhängigkeiten abgerufen.
  • Der Unterordner „dawn/“ ist im Ziel enthalten.
  • Deine App ist von dawn::webgpu_dawn-, glfw- und webgpu_glfw-Zielen abhängig, sodass du diese 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 dawn::webgpu_dawn glfw webgpu_glfw)

Fenster öffnen

Jetzt, da „Dawn“ verfügbar ist, kannst du 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.

So öffnen Sie ein Fenster mit dem Namen „WebGPU-Fenster“ einer Auflösung von 512 x 512, aktualisieren Sie die main.cpp-Datei wie unten dargestellt. 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 Anwendung neu erstellen und wie zuvor ausführen, bleibt das Fenster leer. Du machst Fortschritte!

<ph type="x-smartling-placeholder">
</ph> Screenshot eines leeren macOS-Fensters. <ph type="x-smartling-placeholder">
</ph> 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. Der Einfachheit halber sollten Sie instance oben in der Datei main.cpp deklarieren und wgpu::CreateInstance() innerhalb von main() aufrufen.


#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 wgpu::Adapter und 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));
}

Für einen leichteren Zugriff deklarieren Sie oben in der Datei main.cpp die beiden Variablen wgpu::Adapter und wgpu::Device. Aktualisieren Sie die Funktion main(), um GetAdapter() aufzurufen, und weisen Sie ihren Ergebnis-Callback adapter zu. 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 Austauschkette wird in der JavaScript API nicht offengelegt, da der Browser dies übernimmt. 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. Direkt nach dem Erstellen des GLFW-Fensters in Start() rufen Sie die praktische Funktion wgpu::glfw::CreateSurfaceForWindow() auf, um ein wgpu::Surface (ähnlich einem HTML-Canvas) zu erstellen, und konfigurieren Sie es, 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 darzustellen. Dies hat keine sichtbaren Auswirkungen, 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 Code unten zu erstellen. Für einen leichteren Zugriff deklarieren Sie oben in der Datei main.cpp eine wgpu::RenderPipeline-Variable und rufen 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 zum Schluss Renderingbefehle an die GPU in der Funktion Render(), 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, sehen Sie jetzt das lang erwartete rote Dreieck in einem Fenster. Mach eine Pause – du hast es dir verdient.

<ph type="x-smartling-placeholder">
</ph> Screenshot eines roten Dreiecks in einem macOS-Fenster. <ph type="x-smartling-placeholder">
</ph> Ein rotes Dreieck in einem Desktop-Fenster.

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. Auch hier basiert die App auf Emscripten, einem Tool zum Kompilieren von C/C++-Programmen in WebAssembly, dessen bindings webgpu.h auf der JavaScript API implementieren.

CMake-Einstellungen aktualisieren

Sobald Emscripten installiert ist, aktualisieren Sie die CMakeLists.txt-Build-Datei so. Sie müssen lediglich den hervorgehobenen Code ändern.

  • Mit set_target_properties wird „html“ automatisch hinzugefügt Dateiendung der Zieldatei hinzu. Sie generieren also eine Datei namens "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 Datei main.cpp nicht auf die Datei webgpu/webgpu_cpp.h zugreifen.
  • Die Option für den App-Link USE_GLFW ist hier auch 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

Zum Erstellen eines wgpu::surface in Emscripten ist ein HTML-Canvas-Element erforderlich. Rufen Sie dazu instance.CreateSurface() auf und geben Sie den #canvas-Selektor an, damit das entsprechende HTML-Canvas-Element auf der von Emscripten generierten HTML-Seite übereinstimmt.

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. Generieren Sie diesmal die Anwendung in einem build-web-Unterordner und starten Sie einen HTTP-Server. Öffnen Sie anschließ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
<ph type="x-smartling-placeholder">
</ph> Screenshot eines roten Dreiecks in einem Browserfenster.
. Ein rotes Dreieck in einem Browserfenster.

Nächste Schritte

Künftige Änderungen:

  • Verbesserungen bei der Stabilisierung der APIs webgpu.h und webgpu_cpp.h.
  • Anfängliche Unterstützung für Android und iOS

Bitte melde uns in der Zwischenzeit WebGPU-Probleme für Emscripten und Dawn-Probleme mit Vorschlägen und Fragen.

Ressourcen

Sehen Sie sich auch den Quellcode dieser App an.

Wenn Sie mehr darüber erfahren möchten, wie Sie native 3D-Anwendungen in C++ mit WebGPU von Grund auf neu erstellen, lesen Sie die Dokumentation zu WebGPU in der C++-Dokumentation und Dawn Native WebGPU-Beispiele.

Wenn Sie sich für Rust interessieren, können Sie auch die auf WebGPU basierende wgpu-Grafikbibliothek verwenden. Sehen Sie sich die hello-triangle-Demo an.

Danksagung

Dieser Artikel wurde von Corentin Wallez, Kai Ninomiya und Rachel Andrew gelesen.

Foto von Marc-Olivier Jodoin auf Unsplash