Créer une application avec WebGPU

François Beaufort
François Beaufort

Publié le 20 juillet 2023, dernière mise à jour le 27 août 2025

Pour les développeurs Web, WebGPU est une API graphique Web qui fournit un accès unifié et rapide aux GPU. WebGPU expose les capacités matérielles modernes et permet d'effectuer des opérations de rendu et de calcul sur un GPU, comme Direct3D 12, Metal et Vulkan.

Bien que cette histoire soit vraie, elle est incomplète. WebGPU est le fruit d'un effort de collaboration, y compris de grandes entreprises telles qu'Apple, Google, Intel, Mozilla et Microsoft. Parmi eux, certains ont réalisé que WebGPU pouvait être plus qu'une API JavaScript, mais une API graphique multiplate-forme pour les développeurs de différents écosystèmes, autres que le Web.

Pour répondre au cas d'utilisation principal, une API JavaScript a été introduite dans Chrome 113. Cependant, un autre projet important a été développé en parallèle : l'API C webgpu.h. Ce fichier d'en-tête C liste toutes les procédures et structures de données WebGPU disponibles. Il sert de couche d'abstraction matérielle indépendante de la plate-forme, ce qui vous permet de créer des applications spécifiques à une plate-forme en fournissant une interface cohérente sur différentes plates-formes.

Dans ce document, vous allez apprendre à écrire une petite application C++ à l'aide de WebGPU, qui s'exécute à la fois sur le Web et sur des plates-formes spécifiques. Attention spoiler : vous obtiendrez le même triangle rouge que celui qui s'affiche dans une fenêtre de navigateur et une fenêtre de bureau, avec des ajustements minimes de votre code.

Capture d'écran d'un triangle rouge alimenté par WebGPU dans une fenêtre de navigateur et une fenêtre de bureau sur macOS.
Le même triangle optimisé par WebGPU dans une fenêtre de navigateur et une fenêtre de bureau.

Fonctionnement

Pour voir l'application terminée, consultez le dépôt WebGPU cross-platform app.

L'application est un exemple minimaliste en C++ qui montre comment utiliser WebGPU pour créer des applications de bureau et Web à partir d'un seul codebase. En arrière-plan, il utilise webgpu.h de WebGPU comme couche d'abstraction matérielle indépendante de la plate-forme via un wrapper C++ appelé webgpu_cpp.h.

Sur le Web, l'application est conçue avec emdawnwebgpu (Emscripten Dawn WebGPU), qui comporte des liaisons implémentant webgpu.h au-dessus de l'API JavaScript. Sur des plates-formes spécifiques telles que macOS ou Windows, ce projet peut être compilé avec Dawn, l'implémentation WebGPU multiplate-forme de Chromium. Il convient de mentionner que wgpu-native, une implémentation Rust de webgpu.h, existe également, mais n'est pas utilisée dans ce document.

Commencer

Pour commencer, vous avez besoin d'un compilateur C++ et de CMake pour gérer les builds multiplates-formes de manière standard. Dans un dossier dédié, créez un fichier source main.cpp et un fichier de compilation CMakeLists.txt.

Le fichier main.cpp doit contenir une fonction main() vide pour le moment.

int main() {}

Le fichier CMakeLists.txt contient des informations de base sur le projet. La dernière ligne indique que le nom de l'exécutable est "app" et que son code source est 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")

Exécutez cmake -B build pour créer des fichiers de compilation dans un sous-dossier "build/" et cmake --build build pour compiler l'application et générer le fichier exécutable.

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

# Run the app.
$ ./build/app

L'application s'exécute, mais il n'y a pas encore de sortie, car vous avez besoin d'un moyen de dessiner des éléments à l'écran.

Obtenir Dawn

Pour dessiner votre triangle, vous pouvez profiter de Dawn, l'implémentation WebGPU multiplate-forme de Chromium. Cela inclut la bibliothèque C++ GLFW pour dessiner à l'écran. Pour télécharger Dawn, vous pouvez l'ajouter en tant que sous-module Git à votre dépôt. Les commandes suivantes le récupèrent dans un sous-dossier "dawn/".

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

Ensuite, ajoutez les éléments suivants au fichier CMakeLists.txt :

  • L'option CMake DAWN_FETCH_DEPENDENCIES récupère toutes les dépendances Dawn.
  • L'option CMake DAWN_BUILD_MONOLITHIC_LIBRARY regroupe tous les composants Dawn dans une seule bibliothèque.
  • Le sous-dossier dawn/ est inclus dans la cible.
  • Votre application dépendra des cibles webgpu_dawn, webgpu_glfw et glfw afin que vous puissiez les utiliser ultérieurement dans le fichier main.cpp.

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)

Ouvrir une fenêtre

Maintenant que Dawn est disponible, utilisez GLFW pour dessiner des éléments à l'écran. Cette bibliothèque incluse dans webgpu_glfw pour plus de commodité vous permet d'écrire du code indépendant de la plate-forme pour la gestion des fenêtres.

Pour ouvrir une fenêtre nommée "WebGPU window" avec une résolution de 512 x 512, mettez à jour le fichier main.cpp comme indiqué ci-dessous. Notez que glfwWindowHint() est utilisé ici pour demander l'initialisation d'aucune API graphique particulière.

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

Si vous recompilez l'application et l'exécutez comme avant, une fenêtre vide s'affiche. Vous faites des progrès !

Capture d&#39;écran d&#39;une fenêtre macOS vide.
Une fenêtre vide.

Obtenir un appareil GPU

En JavaScript, navigator.gpu est votre point d'entrée pour accéder au GPU. En C++, vous devez créer manuellement une variable wgpu::Instance qui est utilisée dans le même but. Pour plus de commodité, déclarez instance en haut du fichier main.cpp et appelez wgpu::CreateInstance() dans 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();
}

Déclarez deux variables wgpu::Adapter et wgpu::Device en haut du fichier main.cpp. Mettez à jour la fonction Init() pour appeler instance.RequestAdapter() et attribuer son rappel de résultat à adapter, puis appelez adapter.RequestDevice() et attribuez son rappel de résultat à 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);
}

Dessiner un triangle

La chaîne d'échange n'est pas exposée dans l'API JavaScript, car le navigateur s'en charge. En C++, vous devez le créer manuellement. Encore une fois, pour plus de commodité, déclarez une variable wgpu::Surface en haut du fichier main.cpp. Juste après avoir créé la fenêtre GLFW dans Start(), appelez la fonction pratique wgpu::glfw::CreateSurfaceForWindow() pour créer un wgpu::Surface (semblable à un canevas HTML) et configurez-le en appelant la nouvelle fonction d'assistance ConfigureSurface() dans InitGraphics(). Vous devez également appeler surface.Present() pour présenter la texture suivante dans la boucle while. Cela n'a aucun effet visible, car aucun rendu n'est effectué pour le moment.

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

C'est le bon moment pour créer le pipeline de rendu avec le code ci-dessous. Pour faciliter l'accès, déclarez une variable wgpu::RenderPipeline en haut du fichier main.cpp et appelez la fonction d'assistance CreateRenderPipeline() dans 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();
}

Enfin, envoyez les commandes de rendu au GPU dans la fonction Render() appelée à chaque frame.

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

Si vous recompilez l'application avec CMake et que vous l'exécutez, le triangle rouge tant attendu s'affiche dans une fenêtre. Faites une pause, vous l'avez bien méritée.

Capture d&#39;écran d&#39;un triangle rouge dans une fenêtre macOS.
Triangle rouge dans une fenêtre de bureau.

Compiler vers WebAssembly

Examinons maintenant les modifications minimales requises pour ajuster votre base de code existante afin de dessiner ce triangle rouge dans une fenêtre de navigateur. Une fois encore, l'application est conçue pour emdawnwebgpu (Emscripten Dawn WebGPU), qui comporte des liaisons implémentant webgpu.h au-dessus de l'API JavaScript. Il utilise Emscripten, un outil permettant de compiler des programmes C/C++ en WebAssembly.

Mettre à jour les paramètres CMake

Une fois Emscripten installé, mettez à jour le fichier de compilation CMakeLists.txt comme suit. Le code mis en surbrillance est la seule chose que vous devez modifier.

  • set_target_properties est utilisé pour ajouter automatiquement l'extension de fichier "html" au fichier cible. En d'autres termes, vous allez générer un fichier "app.html".
  • La bibliothèque de liens cibles emdawnwebgpu_cpp permet la prise en charge de WebGPU dans Emscripten. Sans cela, votre fichier main.cpp ne peut pas accéder au fichier webgpu/webgpu_cpp.h.
  • L'option de lien d'application ASYNCIFY=1 permet au code C++ synchrone d'interagir avec le code JavaScript asynchrone.
  • L'option de lien d'application USE_GLFW=3 indique à Emscripten d'utiliser son implémentation JavaScript intégrée de l'API GLFW 3.
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()

Mettre à jour le code

Au lieu d'utiliser une boucle while, appelez emscripten_set_main_loop(Render) pour vous assurer que la fonction Render() est appelée à une fréquence fluide appropriée qui correspond correctement au navigateur et à l'écran.

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

Compiler l'application avec Emscripten

La seule modification nécessaire pour compiler l'application avec Emscripten consiste à ajouter le script shell emcmake magique aux commandes cmake. Cette fois, générez l'application dans un sous-dossier build-web et démarrez un serveur HTTP. Enfin, ouvrez votre navigateur et accédez à 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
Capture d&#39;écran d&#39;un triangle rouge dans une fenêtre de navigateur.
Triangle rouge dans une fenêtre de navigateur.

Étape suivante

Voici à quoi vous pouvez vous attendre à l'avenir :

  • Améliorations apportées à la stabilisation des API webgpu.h et webgpu_cpp.h.
  • Prise en charge initiale de Dawn pour Android et iOS.

En attendant, veuillez signaler les problèmes WebGPU pour Emscripten et les problèmes Dawn en y ajoutant vos suggestions et questions.

Ressources

N'hésitez pas à explorer le code source de cette application.

Si vous souhaitez en savoir plus sur la création d'applications 3D natives en C++ à partir de zéro avec WebGPU, consultez la documentation sur l'apprentissage de WebGPU pour C++ et les exemples Dawn Native WebGPU.

Si Rust vous intéresse, vous pouvez également explorer la bibliothèque graphique wgpu basée sur WebGPU. Consultez leur démo hello-triangle.

Remerciements

Cet article a été relu par Corentin Wallez, Kai Ninomiya et Rachel Andrew.

Photo de Marc-Olivier Jodoin sur Unsplash.