Bouw een app met WebGPU

François Beaufort
François Beaufort

Gepubliceerd: 20 juli 2023, Laatst bijgewerkt: 27 augustus 2025

Voor webontwikkelaars is WebGPU een webgraphics-API die uniforme en snelle toegang tot GPU's biedt. WebGPU maakt moderne hardwaremogelijkheden beschikbaar en maakt rendering en berekeningen op een GPU mogelijk, vergelijkbaar met Direct3D 12, Metal en Vulkan.

Hoewel dat waar is, is dat verhaal onvolledig. WebGPU is het resultaat van een gezamenlijke inspanning van grote bedrijven zoals Apple, Google, Intel, Mozilla en Microsoft. Sommigen van hen realiseerden zich dat WebGPU meer kon zijn dan een Javascript API, maar een platformonafhankelijke grafische API voor ontwikkelaars in verschillende ecosystemen, behalve het web.

Om aan de primaire use case te voldoen, werd in Chrome 113 een JavaScript API geïntroduceerd . Daarnaast is er echter ook een ander belangrijk project ontwikkeld: de webgpu.h C API. Dit C-headerbestand bevat een overzicht van alle beschikbare procedures en datastructuren van WebGPU. Het dient als een platformonafhankelijke hardware-abstractielaag, waarmee u platformspecifieke applicaties kunt bouwen door een consistente interface op verschillende platforms te bieden.

In dit document leer je hoe je een kleine C++-app schrijft met WebGPU die zowel op het web als op specifieke platforms draait. Spoiler alert: je krijgt dezelfde rode driehoek te zien die in een browservenster en een desktopvenster verschijnt, met minimale aanpassingen aan je codebase.

Schermafbeelding van een rode driehoek aangestuurd door WebGPU in een browservenster en een bureaubladvenster op macOS.
Dezelfde driehoek, aangestuurd door WebGPU, in een browservenster en een desktopvenster.

Hoe werkt het?

Bekijk de voltooide applicatie in de WebGPU cross-platform app repository.

De app is een minimalistisch C++-voorbeeld dat laat zien hoe je WebGPU kunt gebruiken om desktop- en webapps te bouwen vanuit één codebase. Onder de motorkap gebruikt het WebGPU's webgpu.h als een platformonafhankelijke hardware-abstractielaag via een C++-wrapper genaamd webgpu_cpp.h .

Op het web is de app gebouwd met emdawnwebgpu (Emscripten Dawn WebGPU), dat bindingen heeft die webgpu.h implementeren bovenop de JavaScript API. Op specifieke platforms zoals macOS of Windows kan dit project gebouwd worden met Dawn , Chromium's cross-platform WebGPU-implementatie. Het is vermeldenswaard dat wgpu-native , een Rust-implementatie van webgpu.h, ook bestaat, maar niet in dit document wordt gebruikt.

Aan de slag

Om te beginnen heb je een C++-compiler en CMake nodig om platformonafhankelijke builds op een standaardmanier af te handelen. Maak in een speciale map een bronbestand main.cpp en een buildbestand CMakeLists.txt .

Het bestand main.cpp zou voorlopig een lege main() functie moeten bevatten.

int main() {}

Het bestand CMakeLists.txt bevat basisinformatie over het project. De laatste regel specificeert de naam van het uitvoerbare bestand: "app" en de broncode: main.cpp .

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")

Voer cmake -B build uit om buildbestanden in de submap "build/" te maken en cmake --build build om de app daadwerkelijk te bouwen en het uitvoerbare bestand te genereren.

# Build the app with CMake.
$ cmake -B build && cmake --build build

# Run the app.
$ ./build/app

De app werkt, maar er is nog geen resultaat. Je moet namelijk iets op het scherm kunnen tekenen.

Krijg Dawn

Om je driehoek te tekenen, kun je Dawn gebruiken, Chromium's cross-platform WebGPU-implementatie. Deze bevat de GLFW C++-bibliotheek voor tekenen op het scherm. Je kunt Dawn downloaden door het als een Git-submodule aan je repository toe te voegen. De volgende opdrachten halen het op in een submap "dawn/".

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

Voeg vervolgens het volgende toe aan het bestand CMakeLists.txt :

  • Met de CMake DAWN_FETCH_DEPENDENCIES optie worden alle Dawn-afhankelijkheden opgehaald.
  • De CMake optie DAWN_BUILD_MONOLITHIC_LIBRARY bundelt alle Dawn-componenten in één bibliotheek.
  • De map dawn/ sub is opgenomen in het doel.
  • Uw app is afhankelijk van de doelen webgpu_dawn , webgpu_glfw en glfw , zodat u deze later in het bestand main.cpp kunt gebruiken.

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)

Open een raam

Nu Dawn beschikbaar is, kunt u GLFW gebruiken om dingen op het scherm te tekenen. Deze bibliotheek, die voor uw gemak is opgenomen in webgpu_glfw , stelt u in staat om platformonafhankelijke code te schrijven voor vensterbeheer.

Om een ​​venster met de naam "WebGPU-venster" te openen met een resolutie van 512x512, werkt u het bestand main.cpp bij zoals hieronder. Merk op dat glfwWindowHint() hier wordt gebruikt om geen specifieke grafische API-initialisatie aan te vragen.

#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();
}

Als je de app opnieuw bouwt en uitvoert zoals voorheen, krijg je een leeg venster. Je boekt vooruitgang!

Schermafbeelding van een leeg macOS-venster.
Een leeg raam.

GPU-apparaat ophalen

In JavaScript is navigator.gpu je toegangspunt voor toegang tot de GPU. In C++ moet je handmatig een variabele wgpu::Instance aanmaken die voor hetzelfde doel wordt gebruikt. Voor het gemak declareer je instance bovenaan het bestand main.cpp en roep je wgpu::CreateInstance() aan binnen Init() .

#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();
}

Declareer twee variabelen wgpu::Adapter en wgpu::Device bovenaan het bestand main.cpp . Werk de Init() -functie bij om instance.RequestAdapter() aan te roepen en de resulterende callback toe te wijzen aan adapter . Roep vervolgens adapter.RequestDevice() aan en wijs de resulterende callback toe aan device .

#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);
}

Teken een driehoek

De swap-keten wordt niet weergegeven in de JavaScript API, omdat de browser hiervoor zorgt. In C++ moet u deze handmatig aanmaken. Declareer voor het gemak wederom een wgpu::Surface variabele bovenaan het bestand main.cpp . Direct na het aanmaken van het GLFW-venster in Start() roept u de handige functie wgpu::glfw::CreateSurfaceForWindow() aan om een wgpu::Surface te maken (vergelijkbaar met een HTML-canvas) en configureert u deze door de nieuwe helperfunctie ConfigureSurface() aan te roepen in InitGraphics() . U moet ook surface.Present() aanroepen om de volgende textuur in de while-lus te presenteren. Dit heeft geen zichtbaar effect, omdat er nog geen rendering plaatsvindt.

#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();
  }
}

Dit is een goed moment om de renderpipeline te maken met de onderstaande code. Voor eenvoudigere toegang declareert u een variabele wgpu::RenderPipeline bovenaan het bestand main.cpp en roept u de helperfunctie CreateRenderPipeline() aan in InitGraphics() .

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();
}

Stuur ten slotte renderingopdrachten naar de GPU in de Render() functie, waarbij u elk frame aanspreekt.

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);
}

Het opnieuw opbouwen van de app met CMake en het uitvoeren ervan resulteert nu in de langverwachte rode driehoek in een venster! Neem even pauze, je hebt het verdiend.

Schermafbeelding van een rode driehoek in een macOS-venster.
Een rode driehoek in een bureaubladvenster.

Compileren naar WebAssembly

Laten we nu eens kijken naar de minimale wijzigingen die nodig zijn om je bestaande codebase aan te passen en deze rode driehoek in een browservenster te tekenen. De app is wederom gebouwd met emdawnwebgpu (Emscripten Dawn WebGPU), dat bindingen heeft die webgpu.h implementeren bovenop de JavaScript API. Het maakt gebruik van Emscripten , een tool voor het compileren van C/C++-programma's naar WebAssembly.

CMake-instellingen bijwerken

Zodra Emscripten is geïnstalleerd, werkt u het buildbestand CMakeLists.txt als volgt bij. De gemarkeerde code is het enige dat u hoeft te wijzigen.

  • set_target_properties wordt gebruikt om automatisch de bestandsextensie "html" aan het doelbestand toe te voegen. Met andere woorden: u genereert een "app.html"-bestand.
  • De emdawnwebgpu_cpp doellinkbibliotheek maakt WebGPU-ondersteuning in Emscripten mogelijk. Zonder deze bibliotheek heeft uw main.cpp bestand geen toegang tot het bestand webgpu/webgpu_cpp.h .
  • Met de app-koppelingsoptie ASYNCIFY=1 kan synchrone C++-code communiceren met asynchrone JavaScript.
  • Met de app-linkoptie USE_GLFW=3 krijgt Emscripten de opdracht om de ingebouwde JavaScript-implementatie van de GLFW 3 API te gebruiken.
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()

Werk de code bij

In plaats van een while-lus kunt u emscripten_set_main_loop(Render) aanroepen om er zeker van te zijn dat de Render() functie met de juiste snelheid wordt aangeroepen en goed is uitgelijnd met de browser en monitor.

#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
}

Bouw de app met Emscripten

De enige verandering die nodig is om de app met Emscripten te bouwen, is het toevoegen van het magische emcmake shell-script aan de cmake -opdrachten. Genereer de app deze keer in een submap build-web en start een HTTP-server. Open ten slotte je browser en ga naar build-web/app.html .

# Build the app with Emscripten.
$ emcmake cmake -B build-web && cmake --build build-web

# Start a HTTP server.
$ npx http-server
Schermafbeelding van een rode driehoek in een browservenster.
Een rode driehoek in een browservenster.

Wat volgt er?

Dit kunt u in de toekomst verwachten:

  • Verbeteringen in de stabilisatie van webgpu.h en webgpu_cpp.h API's.
  • Dawn biedt in eerste instantie ondersteuning voor Android en iOS.

In de tussentijd kunt u WebGPU-problemen voor Emscripten en Dawn-problemen melden met suggesties en vragen.

Bronnen

U kunt gerust de broncode van deze app verkennen.

Als u dieper wilt ingaan op het maken van native 3D-applicaties in C++ vanaf nul met WebGPU, bekijk dan Learn WebGPU voor C++-documentatie en Dawn Native WebGPU-voorbeelden .

Ben je geïnteresseerd in Rust? Bekijk dan ook de wgpu grafische bibliotheek gebaseerd op WebGPU. Bekijk hun hello-triangle demo.

Dankbetuigingen

Dit artikel is beoordeeld door Corentin Wallez , Kai Ninomiya en Rachel Andrew .

Foto door Marc-Olivier Jodoin op Unsplash .