Bouw een app met WebGPU

François Beaufort
François Beaufort

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

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

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

In dit document leert u hoe u een kleine C++-app schrijft met behulp van WebGPU die zowel op internet als op specifieke platforms kan worden uitgevoerd. Spoiler alert: u krijgt dezelfde rode driehoek die verschijnt in een browservenster en een desktopvenster met minimale aanpassingen aan uw codebase.

Schermafbeelding van een rode driehoek mogelijk gemaakt door WebGPU in een browservenster en een desktopvenster op macOS.
Dezelfde driehoek aangedreven door WebGPU in een browservenster en een desktopvenster.

Hoe werkt het?

Om de voltooide applicatie te zien, ga naar de WebGPU platformonafhankelijke app- repository.

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

Op internet is de app gebouwd tegen Emscripten , die bindingen heeft die webgpu.h implementeren bovenop de JavaScript-API. Op specifieke platforms zoals macOS of Windows kan dit project worden gebouwd tegen Dawn , de platformonafhankelijke WebGPU-implementatie van Chromium. Het is de moeite waard om te vermelden dat wgpu-native , een Rust-implementatie van webgpu.h, ook bestaat, maar niet in dit document wordt gebruikt.

Begin

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

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

int main() {}

Het CMakeLists.txt -bestand bevat basisinformatie over het project. De laatste regel geeft aan dat de naam van het uitvoerbare bestand "app" is en dat de broncode main.cpp is.

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

Voer cmake -B build uit om build-bestanden te maken in een "build/" submap 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 uitvoer, omdat je een manier nodig hebt om dingen op het scherm te tekenen.

Neem Dawn

Om uw driehoek te tekenen, kunt u profiteren van Dawn , de platformonafhankelijke WebGPU-implementatie van Chromium. Dit omvat de GLFW C++-bibliotheek voor tekenen naar het scherm. Eén manier om Dawn te downloaden is door het als een git-submodule aan je repository toe te voegen. Met de volgende opdrachten wordt het opgehaald in een submap "dawn/".

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

Voeg vervolgens als volgt toe aan het bestand CMakeLists.txt :

  • De optie CMake DAWN_FETCH_DEPENDENCIES haalt alle Dawn-afhankelijkheden op.
  • De map dawn/ sub is opgenomen in het doel.
  • Uw app is afhankelijk van webgpu_cpp , webgpu_dawn en webgpu_glfw -doelen, zodat u deze later in het bestand main.cpp kunt gebruiken.
…
set(DAWN_FETCH_DEPENDENCIES ON)
add_subdirectory("dawn" EXCLUDE_FROM_ALL)
target_link_libraries(app PRIVATE webgpu_cpp webgpu_dawn webgpu_glfw)

Open een raam

Nu Dawn beschikbaar is, kun je GLFW gebruiken om dingen op het scherm te tekenen. Met deze bibliotheek die voor het gemak is opgenomen in webgpu_glfw , kunt u code schrijven die platformonafhankelijk is voor vensterbeheer.

Om een ​​venster met de naam "WebGPU-venster" te openen met een resolutie van 512x512, updatet u het bestand main.cpp 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();
}

Het opnieuw opbouwen van de app en het uitvoeren ervan zoals voorheen resulteert nu in een leeg venster. Je boekt vooruitgang!

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

GPU-apparaat downloaden

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

…
#include <webgpu/webgpu_cpp.h>

wgpu::Instance instance;
…

int main() {
  instance = wgpu::CreateInstance();
  Start();
}

Toegang tot de GPU is asynchroon vanwege de vorm van de JavaScript-API. Maak in C++ een hulpfunctie GetDevice() die een callback-functieargument gebruikt en dit aanroept met de resulterende wgpu::Device .

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

Voor eenvoudigere toegang declareert u een wgpu::Device variabele bovenaan het bestand main.cpp en werkt u de functie main() bij om GetDevice() aan te roepen en wijst u de resultaat-callback toe aan device voordat u Start() aanroept.

wgpu::Device device;
…

int main() {
  instance = wgpu::CreateInstance();
  GetDevice([](wgpu::Device dev) {
    device = dev;
    Start();
  });
}

Teken een driehoek

De swapketen wordt niet weergegeven in de JavaScript-API, omdat de browser hiervoor zorgt. In C++ moet je het handmatig maken. Voor het gemak declareert u nogmaals een wgpu::SwapChain variabele bovenaan het bestand main.cpp . Net nadat u het GLFW-venster in Start() hebt gemaakt, roept u de handige functie wgpu::glfw::CreateSurfaceForWindow() aan om een wgpu::Surface te maken (vergelijkbaar met een HTML-canvas) en deze te gebruiken om de swap-keten in te stellen door de nieuwe aan te roepen helper SetupSwapChain() functie in InitGraphics() . Je moet ook swapChain.Present() aanroepen om de volgende textuur in de while-lus te presenteren. Dit heeft geen zichtbaar effect omdat er nog geen weergave plaatsvindt.

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

Dit is een goed moment om de renderpijplijn te maken met de onderstaande code. Voor eenvoudigere toegang declareert u een wgpu::RenderPipeline variabele 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::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();
}

Stuur ten slotte renderingopdrachten naar de GPU in de functie Render() die elk frame wordt genoemd.

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

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

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 uw bestaande codebase aan te passen om deze rode driehoek in een browservenster te tekenen. Nogmaals, de app is gebouwd tegen Emscripten , een tool voor het compileren van C/C++-programma's naar WebAssembly, die bindingen heeft die webgpu.h implementeren bovenop de JavaScript-API.

Update CMake-instellingen

Zodra Emscripten is geïnstalleerd, werkt u het CMakeLists.txt buildbestand 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 app-linkoptie USE_WEBGPU is vereist om WebGPU-ondersteuning in Emscripten in te schakelen. Zonder dit bestand heeft uw main.cpp -bestand geen toegang tot het bestand webgpu/webgpu_cpp.h .
  • Ook hier is de app-linkoptie USE_GLFW vereist, zodat u uw GLFW-code opnieuw kunt gebruiken.
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()

Werk de code bij

In Emscripten vereist het maken van een wgpu::surface een HTML-canvaselement. Roep hiervoor instance.CreateSurface() aan en specificeer de #canvas selector zodat deze overeenkomt met het juiste HTML-canvaselement op de HTML-pagina die door Emscripten is gegenereerd.

In plaats van een while-lus te gebruiken, roept u emscripten_set_main_loop(Render) aan om er zeker van te zijn dat de Render() functie met de juiste, vloeiende snelheid wordt aangeroepen, zodat deze goed aansluit bij de browser en de monitor.

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

Bouw de app met Emscripten

De enige verandering die nodig is om de app met Emscripten te bouwen, is door de cmake opdrachten vooraf te laten gaan door het magische emcmake shellscript. Genereer deze keer de app in een build-web submap en start een HTTP-server. Open ten slotte uw browser en bezoek 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
Screenshot van een rode driehoek in een browservenster.
Een rode driehoek in een browservenster.

Wat is het volgende

Dit is wat u in de toekomst kunt verwachten:

  • Verbeteringen in de stabilisatie van webgpu.h en webgpu_cpp.h API's.
  • Dawn initiële ondersteuning voor Android en iOS.

Dien in de tussentijd WebGPU-problemen voor Emscripten- en Dawn-problemen in met suggesties en vragen.

Bronnen

Voel je vrij om de broncode van deze app te verkennen.

Als je je verder wilt verdiepen in het helemaal opnieuw maken van native 3D-applicaties in C++ met WebGPU, bekijk dan de Learn WebGPU voor C++-documentatie en Dawn Native WebGPU-voorbeelden .

Als je geïnteresseerd bent in Rust, kun je ook de wgpu grafische bibliotheek verkennen op basis van WebGPU. Kijk eens naar hun hello-triangle- demo.

Dankbetuigingen

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

Foto door Marc-Olivier Jodoin op Unsplash .