Pour les développeurs Web, WebGPU est une API Web Graph qui fournit un accès unifié et rapide aux GPU. WebGPU expose des fonctionnalité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 vraie, cette histoire est incomplète. WebGPU est le résultat d'un effort collaboratif entre de grandes entreprises comme Apple, Google, Intel, Mozilla et Microsoft. Parmi eux, certains ont réalisé que WebGPU pouvait être bien plus qu'une API JavaScript, mais une API graphique multiplate-forme destinée aux développeurs d'é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 répertorie toutes les procédures et structures de données disponibles pour WebGPU. 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. Pour révéler l'intrigue, vous obtiendrez le même triangle rouge qui apparaît dans une fenêtre de navigateur et dans une fenêtre de bureau, avec un minimum d'ajustements sur votre codebase.
Comment ça fonctionne ?
Pour voir l'application finalisée, consultez le dépôt de l'application multiplate-forme 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 seul codebase. En arrière-plan, il utilise webgpu.h de WebGPU en tant que 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 bindings 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 multiplate-forme WebGPU de Chromium. Il est utile 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
.
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 spécifie que le nom du fichier exécutable est "app" et que 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 un sous-dossier "build/", puis 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 résultat, car vous avez besoin d'un moyen de dessiner des éléments à l'écran.
À l'aube
Pour dessiner votre triangle, vous pouvez utiliser Dawn, l'implémentation multiplate-forme WebGPU 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
Ajoutez ensuite les éléments suivants au fichier CMakeLists.txt
:
- 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
webgpu_cpp
,webgpu_dawn
,glfw
etwebgpu_glfw
pour que vous puissiez les utiliser ultérieurement dans le fichiermain.cpp
.
…
set(DAWN_FETCH_DEPENDENCIES ON)
add_subdirectory("dawn" EXCLUDE_FROM_ALL)
target_link_libraries(app PRIVATE webgpu_cpp webgpu_dawn glfw webgpu_glfw)
Ouvrir une fenêtre
Maintenant que Dawn est disponible, utilisez GLFW pour dessiner des éléments à l'écran. Incluse dans webgpu_glfw
pour plus de commodité, cette bibliothèque 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" (Fenêtre WebGPU) avec une résolution de 512 x 512, mettez à jour le fichier main.cpp
comme ci-dessous. Notez que glfwWindowHint()
est utilisé ici pour ne demander aucune initialisation particulière de l'API graphique.
#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 auparavant, une fenêtre vide s'affiche. Vous progressez !
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
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 y accéder plus facilement, déclarez 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
, puis appeler GetDevice()
et attribuer 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 de permutation n'est pas exposée dans l'API JavaScript, car le navigateur s'en charge. En C++, vous devez le créer manuellement. Là encore, 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), puis 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 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 les 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);
}
Si vous recompilez l'application avec CMake et que vous l'exécutez, le triangle rouge tant attendu apparaît dans une fenêtre. Faites une pause, vous l'avez bien mérité.
Compiler dans WebAssembly
Examinons 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 utilise des bindings pour implémenter 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 le seul élément que vous devez modifier.
set_target_properties
permet d'ajouter automatiquement l'extension de fichier "html" au fichier cible. En d'autres termes, vous allez générer un fichier "app.html".- L'option de lien vers l'application
USE_WEBGPU
est requise pour activer la prise en charge de WebGPU dans Emscripten. Sans ce nom, votre fichiermain.cpp
ne peut pas accéder au fichierwebgpu/webgpu_cpp.h
. - L'option de lien vers l'application
USE_GLFW
est également requise ici pour pouvoir 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 webgpu_cpp webgpu_dawn glfw webgpu_glfw)
endif()
Mettre à jour le code
Dans Emscripten, la création d'un wgpu::surface
nécessite un élément de 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 "when", appelez emscripten_set_main_loop(Render)
pour vous assurer que la fonction Render()
est appelée à un débit approprié, qui s'aligne correctement avec le navigateur et l'écran.
#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 emcmake
magique au début des 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
Étapes suivantes
Voici ce à 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, n'hésitez pas à nous faire part de vos suggestions et de vos questions sur les problèmes liés à WebGPU pour Emscripten et Dawn.
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++ en partant de zéro avec WebGPU, consultez la documentation de WebGPU pour C++ et la section Exemples Dawn Native WebGPU.
Si vous êtes intéressé par Rust, vous pouvez également explorer la bibliothèque graphique wgpu basée sur WebGPU. Accédez à la démo hello-triangle.
Remerciements
Cet article a été lu par Corentin Wallez, Kai Ninomiya et Rachel Andrew.
Photo de Marc-Olivier Jodoin sur Unsplash.