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 de commencer à utiliser WebGPU, le successeur de WebGL qui apporte 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 sommets et de fragments, tandis que WebGPU est également compatible avec les nuanceurs de calcul. WebGL utilise le langage de nuanceurs OpenGL (GLSL), tandis que WebGPU utilise le langage de nuanceurs WebGPU (WGSL). Bien que les deux langages soient différents, les concepts sous-jacents sont essentiellement 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 dispose d'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 diverses 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 de manière à affecter d'autres parties du code.

WebGPU est une API sans état et ne conserve pas d'état global. Il utilise plutôt le concept de pipeline pour encapsuler tout l'état de rendu qui était global dans WebGL. Un pipeline contient des informations telles que les mélanges, 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 commandes pour regrouper des commandes et les exécuter dans l'ordre dans lequel elles ont été enregistrées. Cela est utile dans le mappage d'ombres, par exemple, où, en une seule passe sur les objets, l'application peut enregistrer plusieurs flux de commandes, un pour chaque carte d'ombre de lumière.

Pour résumer, le modèle d'état global de WebGL rendait la création de bibliothèques et d'applications robustes et composables difficile et fragile. WebGPU a considérablement réduit la quantité d'état 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 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 du côté du processeur lorsque les deux processus communiquent.

Pour éviter ces bulles, WebGPU est conçu pour être complètement asymétrique. 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 réalité une erreur. Vous ne pouvez découvrir l'erreur qu'à l'aide d'une méthode asynchrone. Cette conception évite les bulles de communication inter-processus et offre aux applications des performances fiables.

Nuancheurs de calcul

Les nuanceurs de calcul sont des programmes exécutés 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. Pour en savoir plus sur le calcul GPU, consultez cet article détaillé 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 de processeur. WebGPU ne présente pas ces limites. Il est donc particulièrement adapté au 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 peut-être pas aux capacités matérielles de l'appareil physique, mais plutôt au dénominateur commun raisonnable et le plus bas de tous les GPU. En obligeant les développeurs à demander des limites d'appareil, 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 fourni des 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 effectuer un anticrénelage dans WebGPU, vous devez créer une texture multiéchantillonnage et y effectuer le rendu. Vous devez ensuite résoudre la texture multiéchantillon en une texture standard et dessiner 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. À l'inverse, WebGL ne peut créer qu'un seul contexte par canevas.

Découvrez la démo WebGPU Multiple Canvases.

À noter que le nombre de canevas WebGL par page est actuellement limité dans les navigateurs. 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émo.

Messages d'erreur utiles

WebGPU fournit une pile d'appels pour chaque message renvoyé par l'API. Vous pouvez ainsi voir rapidement où l'erreur s'est produite dans votre code, ce qui est utile pour déboguer et corriger les erreurs.

En plus de fournir une pile d'appels, 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 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 en GLSL et obtenir son emplacement à l'aide de gl.getUniformLocation(program, 'myUniform'). Cela est 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 un décalage d'octet ou un indice (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 MIP de niveau 0 d'une texture, puis appeler gl.generateMipmap(). WebGL génère ensuite tous les autres niveaux de MIP.

Dans WebGPU, vous devez générer vous-même des mipmap. Aucune fonction intégrée ne permet de le faire. 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 de stockage 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 plus flexibles que les tampons de variables uniformes.

  • Les données des tampons de stockage transmises aux nuanceurs peuvent être beaucoup plus importantes que les tampons uniformes. Bien que la spécification indique que les liaisons de tampons uniformes peuvent avoir une taille maximale de 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 en écriture et acceptent certaines opérations atomiques, tandis que les tampons uniformes ne sont en lecture seule. Cela permet d'implémenter de nouvelles classes d'algorithmes.

  • Les liaisons de tampons de stockage acceptent 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 pour les textures ce que les tampons de stockage sont pour les tampons uniformes. Elles sont plus flexibles que les textures standards et prennent en charge les écritures en accès aléatoire (et les lectures à l'avenir).

Modifications des tampons et des textures

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. 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 découpe Z s'étend de -1 à 1. Dans WebGPU, la plage de l'espace de découpe Z s'étend de 0 à 1. Cela signifie que les objets dont la valeur z est de 0 sont les plus proches de la caméra, tandis que ceux dont la valeur z est de 1 sont les plus éloignés.

Illustration des plages d'espace de découpe Z dans WebGL et WebGPU.
Plages d'espace de découpe Z dans WebGL et WebGPU.

WebGL utilise la convention OpenGL, où l'axe Y est vers le haut et l'axe Z vers l'observateur. WebGPU utilise la convention Metal, où l'axe Y est vers le bas et l'axe Z en dehors de l'écran. Notez que l'axe Y est orienté vers le bas dans les coordonnées du frame buffer, de la vue et des fragments/pixels. Dans l'espace de clip, l'axe Y est toujours orienté vers le haut, comme dans WebGL.

Remerciements

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

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