De WebGL vers WebGPU

François Beaufort
François Beaufort

En tant que développeur WebGL, vous pouvez être à la fois intimidé et enthousiaste à l'idée d'utiliser WebGPU, le successeur de WebGL qui permet d'intégrer les avancées des API graphiques modernes sur le Web.

Il est rassurant de savoir que WebGL et WebGPU partagent de nombreux concepts fondamentaux. Les deux API vous permettent d'exécuter de petits programmes appelés nuanceurs sur le GPU. WebGL est compatible avec les nuanceurs de sommet et de fragment, tandis que WebGPU prend également en charge les nuanceurs de calcul. WebGL utilise le langage d'ombrage OpenGL (GLSL), tandis que WebGPU utilise le langage d'ombrage WebGPU (WGSL). Bien que les deux langages soient différents, les concepts sous-jacents sont pour la plupart les mêmes.

Dans cette optique, cet article met en évidence certaines différences entre WebGL et WebGPU, et vous aider ainsi à démarrer.

État global

WebGL a un grand nombre d'états globaux. Certains paramètres s'appliquent à toutes les opérations de rendu, telles que les textures et les tampons. Cet état global est défini en appelant diverses fonctions de l'API et reste actif jusqu'à ce que vous le modifiiez. L'état global dans WebGL est une principale source d'erreurs, car il est facile d'oublier de modifier un paramètre global. De plus, l'état global rend le partage de code difficile, car les développeurs doivent veiller à ne pas modifier accidentellement l'état global d'une manière qui affecte d'autres parties du code.

WebGPU est une API sans état et ne maintient pas d'état global. Au lieu de cela, il utilise le concept de pipeline pour encapsuler l'ensemble de l'état de rendu global dans WebGL. Un pipeline contient des informations telles que la combinaison, la topologie et les attributs à utiliser. Un pipeline est immuable. Si vous souhaitez modifier certains paramètres, vous devez créer un autre pipeline. WebGPU utilise également des encodeurs de commande pour regrouper les commandes et les exécuter dans l'ordre dans lequel elles ont été enregistrées. Cela est utile pour les mappages d'ombres, par exemple où, en un seul passage sur les objets, l'application peut enregistrer plusieurs flux de commandes, un pour chaque carte d'ombres.

En résumé, comme le modèle d'état global de WebGL rendait difficile et fragile la création d'applications et de bibliothèques composables robustes, WebGPU a considérablement réduit la quantité d'états que les développeurs devaient suivre lorsqu'ils envoyaient des commandes au GPU.

Ne plus synchroniser

Sur les GPU, il est généralement inefficace d'envoyer des commandes et d'attendre celles-ci de manière synchrone, car cela peut vider le pipeline et provoquer des bulles. C'est particulièrement vrai dans WebGPU et WebGL, qui utilisent une architecture multiprocessus avec le pilote de GPU s'exécutant dans un processus distinct de JavaScript.

Dans WebGL, par exemple, l'appel de gl.getError() nécessite un IPC synchrone entre le processus JavaScript et le processus GPU, et inversement. Cela peut entraîner une bulle au niveau du processeur lorsque les deux processus communiquent.

Pour éviter ces bulles, WebGPU est conçu pour être entièrement asynchrone. Le modèle d'erreur et toutes les autres opérations se produisent de manière asynchrone. Par exemple, lorsque vous créez une texture, l'opération semble réussir immédiatement, même s'il s'agit d'une erreur. L'erreur ne peut être détectée que de manière asynchrone. Cette conception évite les bulles de communication entre les processus et assure des performances fiables aux applications.

Nuanceurs de calcul

Les nuanceurs de calcul sont des programmes qui s'exécutent sur le GPU pour effectuer des calculs à usage général. Ils ne sont disponibles que dans WebGPU, et non dans WebGL.

Contrairement aux nuanceurs de sommets et de fragments, ils ne se limitent pas au traitement graphique et peuvent être utilisés pour une grande variété de tâches, telles que le machine learning, la simulation physique et l'informatique scientifique. Les nuanceurs de calcul sont exécutés en parallèle par des centaines, voire des milliers de threads, ce qui les rend très efficaces pour traiter de grands ensembles de données. Pour en savoir plus sur le calcul GPU et obtenir plus de détails, consultez cet article complet sur les GPU Web.

Traitement des frames de la vidéo

Le traitement des images vidéo à l'aide de JavaScript et WebAssembly présente certains inconvénients: le coût de copie des données de la mémoire GPU vers la mémoire du processeur, et le parallélisme limité qui peut être obtenu avec les nœuds de calcul et les threads de processeur. WebGPU ne présente pas ces limites, ce qui le rend particulièrement adapté au traitement des images vidéo grâce à son intégration étroite à l'API WebCodecs.

L'extrait de code suivant montre comment importer un élément VideoFrame en tant que texture externe dans WebGPU et comment le traiter. Vous pouvez essayer cette démonstration.

// Init WebGPU device and pipeline...
// Configure canvas context...
// Feed camera stream to video...

(function render() {
  const videoFrame = new VideoFrame(video);
  applyFilter(videoFrame);
  requestAnimationFrame(render);
})();

function applyFilter(videoFrame) {
  const texture = device.importExternalTexture({ source: videoFrame });
  const bindgroup = device.createBindGroup({
    layout: pipeline.getBindGroupLayout(0),
    entries: [{ binding: 0, resource: texture }],
  });
  // Finally, submit commands to GPU
}

Portabilité des applications par défaut

WebGPU vous oblige à demander limits. Par défaut, requestDevice() renvoie un GPUDevice qui peut ne pas correspondre aux capacités matérielles de l'appareil physique, mais plutôt un plus petit dénominateur commun raisonnable de tous les GPU. En obligeant les développeurs à demander des limites d'appareils, WebGPU garantit que les applications s'exécutent sur autant d'appareils que possible.

Gestion du canevas

WebGL gère automatiquement le canevas une fois que vous avez créé un contexte WebGL et fournit des attributs de contexte tels que alpha, antialias, colorSpace, depth, objetsDrawingBuffer ou pochoir.

En revanche, WebGPU nécessite que vous gériez vous-même le canevas. Par exemple, pour obtenir un anticrénelage dans WebGPU, vous devez créer une texture de plusieurs échantillons et y effectuer le rendu. Vous devez ensuite résoudre la texture à plusieurs échantillons en une texture standard, puis dessiner cette texture sur le canevas. Cette gestion manuelle vous permet d'afficher autant de canevas que vous le souhaitez à partir d'un seul objet GPUDevice. En revanche, WebGL ne peut créer qu'un seul contexte par canevas.

Consultez la démo de WebGPU Multiple Canvases.

Par ailleurs, le nombre de canevas WebGL par page est actuellement limité dans les navigateurs. À l'heure où nous écrivons ces lignes, Chrome et Safari ne peuvent utiliser que jusqu'à 16 canevas WebGL simultanément. Firefox peut en créer jusqu'à 200. En revanche, le nombre de canevas WebGPU par page n'est pas limité.

Capture d'écran montrant le nombre maximal de canevas WebGL dans les navigateurs Safari, Chrome et Firefox
Nombre maximal de canevas WebGL dans Safari, Chrome et Firefox (de gauche à droite) - demo

Messages d'erreur utiles

WebGPU fournit une pile d'appel pour chaque message renvoyé par l'API. Cela vous permet de voir rapidement où l'erreur s'est produite dans le code, ce qui est utile pour déboguer et corriger les erreurs.

En plus de fournir une pile d'appel, les messages d'erreur WebGPU sont également faciles à comprendre et à exploiter. Les messages d'erreur incluent généralement une description de l'erreur et des suggestions pour la corriger.

WebGPU vous permet également de fournir un label personnalisé pour chaque objet WebGPU. Ce libellé est ensuite utilisé par le navigateur dans les messages d'erreur GPU, les avertissements de la console et les outils pour les développeurs du navigateur.

Des noms aux index

Dans WebGL, beaucoup d'éléments sont reliés par des noms. Par exemple, vous pouvez déclarer une variable uniforme appelée myUniform dans GLSL et obtenir son emplacement à l'aide de gl.getUniformLocation(program, 'myUniform'). Cela s'avère pratique, car vous obtenez une erreur si vous avez mal saisi le nom de la variable uniforme.

En revanche, dans WebGPU, tout est entièrement connecté par un décalage d'octets ou un index (souvent appelé emplacement). Il est de votre responsabilité de synchroniser les emplacements du code dans WGSL et JavaScript.

Génération des mipmaps

Dans WebGL, vous pouvez créer le niveau 0 mip d'une texture, puis appeler gl.generateMipmap(). WebGL génère alors tous les autres niveaux de mip.

Dans WebGPU, vous devez générer vous-même des mipmaps. Aucune fonction intégrée n'est disponible pour effectuer cette opération. Consultez la discussion sur les spécifications pour en savoir plus sur la décision. Vous pouvez utiliser des bibliothèques pratiques telles que webgpu-utils pour générer des mipmaps ou apprendre à le faire vous-même.

Tampons de stockage et textures de stockage

Les tampons uniformes sont compatibles avec WebGL et WebGPU, et vous permettent de transmettre des paramètres constants de taille limitée aux nuanceurs. Les tampons de stockage, qui ressemblent beaucoup à des tampons uniformes, ne sont pris en charge que par WebGPU et sont plus puissants et plus flexibles que les tampons uniformes.

  • Les données des tampons de stockage transmises aux nuanceurs peuvent être beaucoup plus volumineuses que les tampons uniformes. Bien que la spécification indique que les liaisons de tampons uniformes peuvent atteindre 64 Ko (voir maxUniformBufferBindingSize) , la taille maximale d'une liaison de tampon de stockage est d'au moins 128 Mo en WebGPU (voir maxStorageBufferBindingSize).

  • Les tampons de stockage sont accessibles en écriture et prennent en charge certaines opérations atomiques, tandis que les tampons uniformes ne sont qu'en lecture seule. Cela permet d'implémenter de nouvelles classes d'algorithmes.

  • Les liaisons de tampons de stockage prennent en charge les tableaux de taille d'exécution pour des algorithmes plus flexibles, tandis que des tailles de tableaux de tampons uniformes doivent être fournies dans le nuanceur.

Les textures de stockage ne sont compatibles qu'avec les GPU Web. Elles correspondent aux textures ce que les tampons de stockage sont et aux tampons uniformes. Elles sont plus flexibles que les textures classiques et acceptent les écritures aléatoires (et les lectures à l'avenir).

Modifications du tampon et de la texture

Dans WebGL, vous pouvez créer un tampon ou une texture, puis modifier sa taille à tout moment avec gl.bufferData() et gl.texImage2D(), par exemple.

Dans WebGPU, les tampons et les textures sont immuables. Une fois créés, vous ne pouvez plus modifier leur taille, leur utilisation ni leur format. Vous pouvez uniquement modifier leur contenu.

Différences entre les conventions spatiales

Dans WebGL, la plage d'espace de rognage Z est comprise entre -1 et 1. Dans WebGPU, la plage d'espace de rognage Z est comprise entre 0 et 1. Autrement dit, les objets dont la valeur z est définie sur 0 sont les plus proches de la caméra, tandis que ceux dont la valeur z est égale à 1 sont les plus éloignés.

Illustration des plages d'espace de clip Z dans WebGL et WebGPU.
Plages d'espace de rognage Z dans WebGL et WebGPU.

WebGL utilise la convention OpenGL, où l'axe Y est orienté vers le haut et l'axe Z vers la visionneuse. WebGPU utilise la convention Metal, où l'axe Y est bas et l'axe Z hors de l'écran. Notez que la direction de l'axe Y est abaissée au niveau des coordonnées du framebuffer, de la fenêtre d'affichage et des coordonnées de fragment/pixel. Dans l'espace de rognage, la direction de l'axe Y est toujours en haut, comme dans WebGL.

Remerciements

Merci à Corentin Wallez, Gregg Tavares, Stephen White, Ken Russell et Rachel Andrew pour avoir consulté cet article.

Je vous recommande également le site WebGPUFundamentals.org pour découvrir en détail les différences entre WebGPU et WebGL.