Créer une application avec WebGPU

François Beaufort
François Beaufort

Pour les développeurs Web, WebGPU est une API de graphisme Web qui offre un niveau de service unifié et rapide l'accès aux GPU. WebGPU présente des capacités matérielles modernes et permet le rendu et les opérations de calcul sur un GPU, comme Direct3D 12, Metal et Vulkan.

Bien que vrai, cette histoire est incomplète. WebGPU est le résultat d'une collaboration y compris de grandes entreprises comme Apple, Google, Intel, Mozilla et Microsoft. Parmi eux, certains se sont rendu compte que WebGPU pourrait être plus qu'une API JavaScript, mais qu'un service graphique multiplate-forme API pour les développeurs d'autres écosystèmes que le Web.

Pour répondre au cas d'utilisation principal, une API JavaScript a été introduite dans Chrome 113. Cependant, un autre élément important a été développé en parallèle: le webgpu.h C API. Ce fichier d’en-tête C répertorie toutes les procédures et structures de données disponibles de WebGPU. Il sert de couche d'abstraction matérielle indépendante de la plate-forme, ce qui permet de créer des applications propres à chaque plate-forme via 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. Pour révéler l'intrigue, vous verrez le même triangle rouge qui apparaît dans une fenêtre de navigateur et dans une fenêtre de bureau, avec des ajustements minimes au niveau de votre codebase.

<ph type="x-smartling-placeholder">
</ph> Capture d&#39;écran d&#39;un triangle rouge alimenté par WebGPU dans une fenêtre de navigateur et une fenêtre de bureau sur macOS. <ph type="x-smartling-placeholder">
</ph> Même triangle fourni par WebGPU dans une fenêtre de navigateur et un ordinateur de bureau.

Fonctionnement

Pour voir l'application terminée, consultez le dépôt d'applications multiplates-formes WebGPU.

L'application est un exemple C++ minimaliste qui montre comment utiliser WebGPU pour créer des applications Web et de bureau à partir d'un codebase unique. 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 basée sur Emscripten, qui utilise des liaisons pour implémenter webgpu.h en plus 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 wgpu-native, une implémentation Rust de webgpu.h, qui 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.

Pour le moment, le fichier main.cpp doit contenir une fonction main() vide.

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 son code source est 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")

Exécutez cmake -B build pour créer des fichiers de compilation dans une compilation et cmake --build build pour créer 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 aucune sortie n'est encore disponible, car vous avez besoin d'un moyen de dessiner des éléments à l'écran.

Aube

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

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

Ajoutez ensuite au fichier CMakeLists.txt comme suit:

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

set(DAWN_FETCH_DEPENDENCIES ON)
add_subdirectory("dawn" EXCLUDE_FROM_ALL)
target_link_libraries(app PRIVATE dawn::webgpu_dawn glfw webgpu_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.

Ouvrir une fenêtre intitulée "Fenêtre WebGPU" avec une résolution de 512 x 512, modifiez le fichier main.cpp comme indiqué ci-dessous. Notez que glfwWindowHint() est utilisé ici pour ne demander aucune initialisation d'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();
}

Recompiler l'application et l'exécuter comme précédemment entraîne une fenêtre vide. Vous faites des progrès !

<ph type="x-smartling-placeholder">
</ph> Capture d&#39;écran d&#39;une fenêtre macOS vide. <ph type="x-smartling-placeholder">
</ph> Une fenêtre vide.

Obtenir l'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 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 main().


#include <webgpu/webgpu_cpp.h>

wgpu::Instance instance;


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

En raison de la forme de l'API JavaScript, l'accès au GPU est asynchrone. En C++, créez deux fonctions d'assistance appelées GetAdapter() et GetDevice() qui renvoient respectivement une fonction de rappel avec wgpu::Adapter et wgpu::Device.

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

Pour un accès plus facile, déclarez les deux variables wgpu::Adapter et wgpu::Device en haut du fichier main.cpp. Mettez à jour la fonction main() pour appeler GetAdapter() et attribuer son rappel de résultat à adapter. Ensuite, appelez GetDevice() et attribuez son rappel de résultat à device avant d'appeler Start().

wgpu::Adapter adapter;
wgpu::Device device;


int main() {
  instance = wgpu::CreateInstance();
  GetAdapter([](wgpu::Adapter a) {
    adapter = a;
    GetDevice([](wgpu::Device d) {
      device = d;
      Start();
    });
  });
}

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 le configurer 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 encore en cours.

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

Le moment est venu de 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::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();
}

Enfin, envoyez des commandes de rendu au GPU dans la fonction Render() appelée chaque image.

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

Recompiler l'application avec CMake et l'exécuter maintenant génère le triangle rouge tant attendu dans une fenêtre. Faites une pause, vous l'avez bien mérité.

<ph type="x-smartling-placeholder">
</ph> Capture d&#39;écran d&#39;un triangle rouge dans une fenêtre macOS. <ph type="x-smartling-placeholder">
</ph> Triangle rouge dans une fenêtre d'ordinateur de bureau.

Compiler sur WebAssembly

Voyons maintenant les modifications minimales requises pour ajuster votre codebase existant afin de dessiner ce triangle rouge dans une fenêtre de navigateur. Là encore, l'application est basée sur Emscripten, un outil permettant de compiler des programmes C/C++ pour WebAssembly, qui comporte des liaisons qui implémentent webgpu.h en plus de l'API JavaScript.

Mettre à jour les paramètres CMake

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

  • set_target_properties permet d'ajouter automatiquement le code "html". l'extension de fichier au fichier cible. En d'autres termes, vous allez générer un fichier "app.html" .
  • L'option de lien d'application USE_WEBGPU est requise pour activer la prise en charge de WebGPU dans Emscripten. Sans lui, votre fichier main.cpp ne pourra pas accéder au fichier webgpu/webgpu_cpp.h.
  • L'option de lien d'application USE_GLFW est également requise ici pour que vous puissiez réutiliser votre code GLFW.
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()

Mettre à jour le code

Dans Emscripten, la création d'un wgpu::surface nécessite un élément canevas HTML. Pour cela, appelez instance.CreateSurface() et spécifiez le sélecteur #canvas pour qu'il corresponde à l'élément de canevas HTML approprié dans la page HTML générée par Emscripten.

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, alignée avec le navigateur et le moniteur.

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

Créer l'application avec Emscripten

La seule modification nécessaire pour compiler l'application avec Emscripten consiste à ajouter le script shell magique emcmake dans les 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
<ph type="x-smartling-placeholder">
</ph> 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 ce qui vous attend à 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 avec vos suggestions et vos 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 WebGPU pour C++ et les exemples de WebGPU natifs Dawn.

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

Remerciements

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

Photo de Marc-Olivier Jodoin sur Unsplash.