De WebGL vers WebGPU

François Beaufort
François Beaufort

En tant que développeur WebGL, vous êtes peut-être à la fois intimidé et enthousiaste à l'idée de commencer à utiliser WebGPU, le successeur de WebGL qui apporte les avancées des API graphiques modernes au Web.

Il est rassurant de savoir que WebGL et WebGPU partagent de nombreux concepts de base. Les deux API vous permettent d'exécuter de petits programmes appelés nuanceurs sur le GPU. WebGL est compatible avec les nuanceurs de sommets et de fragments, tandis que WebGPU est également compatible avec les nuanceurs de calcul. WebGL utilise le langage de nuanceur OpenGL (GLSL), tandis que WebGPU utilise le langage de nuanceur 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 pour vous aider à vous lancer.

État global

WebGL possède un grand nombre d'états globaux. Certains paramètres s'appliquent à toutes les opérations de rendu, comme les textures et les tampons liés. Vous définissez cet état global en appelant différentes fonctions d'API. Il reste en vigueur jusqu'à ce que vous le modifiiez. L'état global dans WebGL est une source majeure 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 conserve pas d'état global. Au lieu de cela, il utilise le concept de pipeline pour encapsuler tout l'état de rendu qui était global dans WebGL. Un pipeline contient des informations telles que les attributs, la topologie et le blending à 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 commandes pour regrouper les commandes et les exécuter dans l'ordre dans lequel elles ont été enregistrées. Cela est utile dans le mappage d'ombre, par exemple, où, en un seul passage sur les objets, l'application peut enregistrer plusieurs flux de commandes, un pour chaque carte d'ombre de lumière.

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

Synchronisation désactivée

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

Dans WebGL, par exemple, l'appel de gl.getError() nécessite un IPC synchrone du processus JavaScript au processus GPU et inversement. Cela peut entraîner une bulle côté 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 si la texture est en fait une erreur. Vous ne pouvez découvrir l'erreur qu'de manière asynchrone. Cette conception permet d'éviter les bulles de communication interprocessus et d'offrir 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. Elles ne sont disponibles que dans WebGPU, et non dans WebGL.

Contrairement aux nuanceurs de vertex et de fragment, ils ne sont pas limités 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 le calcul 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. En savoir plus sur le calcul GPU et cet article complet sur WebGPU

Traitement des images vidéo

Le traitement des images vidéo à l'aide de JavaScript et de WebAssembly présente certains inconvénients : le coût de la copie des données de la mémoire du 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 du processeur. WebGPU ne présente pas ces limites, ce qui en fait un excellent choix pour le traitement des images vidéo grâce à son intégration étroite avec l'API WebCodecs.

L'extrait de code suivant montre comment importer un VideoFrame en tant que texture externe dans WebGPU et 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 ne correspond pas forcément aux capacités matérielles de l'appareil physique, mais plutôt à un dénominateur commun raisonnable et minimal de tous les GPU. En exigeant des développeurs qu'ils demandent des limites d'appareils, WebGPU garantit que les applications s'exécuteront sur le plus grand nombre d'appareils possible.

Gestion du canevas

WebGL gère automatiquement le canevas après la création d'un contexte WebGL et la fourniture d'attributs de contexte tels que alpha, antialias, colorSpace, depth, preserveDrawingBuffer ou stencil.

En revanche, WebGPU vous oblige à gérer le canevas vous-même. Par exemple, pour obtenir l'anticrénelage dans WebGPU, vous devez créer une texture multisample et l'afficher. Vous résolvez ensuite la texture multisample en une texture normale et dessinez cette texture sur le canevas. Cette gestion manuelle vous permet de générer des sorties sur 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émonstration WebGPU avec plusieurs canevas.

Pour information, les navigateurs limitent actuellement le nombre de canevas WebGL par page. Au moment de la rédaction de cet article, Chrome et Safari ne peuvent utiliser que 16 canevas WebGL simultanément, tandis que 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) – démonstration.

Messages d'erreur utiles

WebGPU fournit une pile d'appels pour chaque message renvoyé par l'API. Cela signifie que vous pouvez rapidement voir où l'erreur s'est produite dans votre code, ce qui est utile pour le débogage et la correction des erreurs.

En plus de fournir une pile d'appels, les messages d'erreur WebGPU sont également faciles à comprendre et à résoudre. 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 GPUError, les avertissements de la console et les outils de développement du navigateur.

Des noms aux index

Dans WebGL, de nombreux éléments sont connecté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 utile, car une erreur s'affiche si vous saisissez mal le nom de la variable uniforme.

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

Génération de mipmaps

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

Dans WebGPU, vous devez générer vous-même les mipmaps. Il n'existe pas de fonction intégrée pour cela. Pour en savoir plus sur cette décision, consultez la discussion sur les spécifications. 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 et textures de stockage

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

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

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

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

Les textures de stockage ne sont compatibles qu'avec WebGPU. Elles sont aux textures ce que les tampons de stockage sont aux tampons uniformes. Elles sont plus flexibles que les textures classiques et prennent en charge les écritures à accès aléatoire (et les lectures à l'avenir).

Modifications de la mémoire 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(), respectivement.

Dans WebGPU, les tampons et les textures sont immuables. Cela signifie que vous ne pouvez pas modifier leur taille, leur utilisation ni leur format après leur création. Vous ne pouvez modifier que leur contenu.

Différences entre les conventions d'espace

Dans WebGL, la plage de l'espace de clipping Z est comprise entre -1 et 1. Dans WebGPU, la plage de l'espace de clipping Z est comprise entre 0 et 1. Cela signifie que les objets dont la valeur Z est égale à 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 clip Z dans WebGL et WebGPU.

WebGL utilise la convention OpenGL, où l'axe Y est orienté vers le haut et l'axe Z vers le spectateur. WebGPU utilise la convention Metal, où l'axe Y est orienté vers le bas et l'axe Z est orienté vers l'extérieur de l'écran. Notez que la direction de l'axe Y est vers le bas dans les coordonnées du framebuffer, de la fenêtre d'affichage et des fragments/pixels. Dans l'espace de clipping, la direction de l'axe Y est toujours vers le haut, comme dans WebGL.

Remerciements

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

Je vous recommande également WebGPUFundamentals.org pour en savoir plus sur les différences entre WebGPU et WebGL.