WebGPU : calculer un tableau et dessiner un triangle dans le navigateur
WebGPU permet à une page web d'utiliser le processeur graphique (GPU) pour dessiner, mais aussi pour effectuer des calculs parallèles généralistes, par exemple traiter les pixels d'une image ou mettre à jour les particules d'une simulation. JavaScript prépare les données et soumet le travail ; de petits programmes écrits en WebGPU Shading Language (WGSL) s'exécutent sur le GPU. On les appelle des shaders, mais un shader de calcul n'a besoin ni de couleurs, ni d'image, ni de canevas. La présentation de WebGPU sur MDN distingue les rôles des pipelines de calcul et de rendu.
Ce guide envoie [1, 2, 3, 4, 5] au GPU, récupère [2, 4, 6, 8, 10], puis dessine un triangle dans une page séparée. Il faut comprendre les tableaux JavaScript et async/await, et disposer d'un navigateur, d'un éditeur de texte et d'un serveur statique local. Aucun outil d'assemblage JavaScript, dépendance npm, compte, installation CUDA ou service cloud n'est nécessaire.
Pour obtenir d'abord un résultat, commencez par « Exécuter le calcul du tableau en local », puis revenez au modèle des ressources. Les deux exemples sont des fichiers HTML autonomes, sans modification des composants du site.
Quel travail confier au GPU ?
Le processeur central (CPU) convient à la logique de contrôle à faible latence, aux branchements complexes et aux tâches dont les étapes dépendent fortement les unes des autres. Les CPU disposent aussi de plusieurs cœurs et d'opérations vectorielles : résumer la distinction à « CPU séquentiel, GPU parallèle » serait trompeur. Le GPU est intéressant lorsque de nombreuses unités d'exécution peuvent appliquer des opérations semblables à beaucoup d'éléments. Leur utilisation effective dépend du volume de données, des accès mémoire et des dépendances de l'algorithme.
Multiplier chaque élément d'un tableau par deux n'introduit aucune dépendance entre les éléments : les opérations peuvent s'exécuter en parallèle. Mais créer le périphérique, compiler les shaders, construire le pipeline, transférer les données, planifier le travail et récupérer le résultat demande aussi du travail. Pour cinq multiplications, JavaScript ordinaire est généralement plus approprié. Cet exemple démontre un flux de données correct, pas une accélération.
La présentation de WebGPU par Chrome explique son rapport aux API GPU natives modernes. Choisir une API de navigateur ou un écosystème de calcul natif dépend d'abord du lieu d'exécution du programme, pas d'un classement de performances sans mesures.
Vérifier les capacités réelles avant les versions du navigateur
WebGPU exige un contexte sécurisé. Il s'agit généralement de HTTPS ; pour le développement, http://localhost:8000 et http://127.0.0.1:8000 sont aussi des origines locales potentiellement dignes de confiance. Une adresse HTTP ordinaire du réseau local n'équivaut pas à localhost. Ce guide utilise un serveur local plutôt que l'ouverture d'un document file:// par double-clic.
Vérifiez dans cet ordre :
window.isSecureContextvaut-il vrai ?navigator.gpuexiste-t-il ?requestAdapter()renvoie-t-il un adapter non nul ?adapter.requestDevice()se termine-t-il avec succès ?- Les
device.featuresetdevice.limitsobtenus répondent-ils aux besoins du travail ?
L'adapter est le point d'accès aux capacités GPU sélectionné par le navigateur. Un résultat null signifie qu'aucun accès utilisable n'a été obtenu ; il ne suffit pas à diagnostiquer un pilote défectueux. La configuration du navigateur, l'environnement d'exécution, le matériel et les pilotes, les capacités demandées et les ressources disponibles peuvent intervenir. Interceptez aussi l'échec de création du périphérique : la présence d'une propriété d'API ne prouve pas que l'initialisation a réussi. Les explications d'initialisation de MDN suivent cette démarche.
Ce que les annonces officielles établissent
Voici des jalons de disponibilité documentés dans les sources consultées le 2026-09-12, et non un tableau actuel couvrant tous les appareils :
- La présentation de Chrome indique une première prise en charge dans Chrome 113 sur ChromeOS avec Vulkan, Windows avec Direct3D 12 et macOS. Pour Android, elle cite Chrome 121 sur Android 12 ou ultérieur, avec des GPU Qualcomm ou ARM.
- La même présentation mentionne Firefox 141 sur Windows et le jalon Safari 26. Ces indications ne prouvent pas la disponibilité sur toutes les plateformes Firefox ni sur tous les systèmes et appareils Apple.
- La formulation « bientôt disponible » pour Linux dans cette présentation est ancienne. La mise à jour officielle de Chrome 144 décrit un déploiement prudent sur Linux, en commençant par les GPU Intel Gen12 et ultérieurs, avec Vulkan pour WebGPU. Ce compte rendu de déploiement ne signifie pas que tous les GPU Linux sont pris en charge.
Ne déduisez pas les capacités de votre machine de cette liste historique, et ne faites pas de l'activation d'options expérimentales ou de l'affaiblissement de la sécurité la solution par défaut. Les vérifications à l'exécution déterminent si la page actuelle peut utiliser WebGPU. Un produit destiné à des utilisateurs doit aussi être testé sur les navigateurs, systèmes et appareils visés.
Fonctionnalités facultatives et limites
adapter.features et adapter.limits décrivent les capacités que l'on peut demander ; device.features et device.limits décrivent celles qu'expose le périphérique créé. Vérifiez les fonctionnalités facultatives avant de les inclure dans requiredFeatures. Utilisez requiredLimits pour demander des limites prises en charge seulement lorsque c'est nécessaire. Des exigences non satisfaites peuvent faire échouer la création du périphérique ; consultez le contrat de requestDevice().
Ne demandez pas toutes les fonctionnalités et toutes les limites maximales. Cet exemple n'exige aucune fonctionnalité facultative. Quand les données augmentent, vérifiez notamment la taille des tampons, des liaisons de stockage, des groupes de travail et des dispatchs. Toutes les limites ne suivent pas la règle « plus grand est mieux » : les capacités maximales et les contraintes d'alignement des décalages n'ont pas le même sens de comparaison.
Les objets rencontrés dans le code
L'exemple suit cet ordre : allouer → transférer → lier → enregistrer le calcul → terminer la passe → enregistrer la copie → soumettre → attendre la récupération → libérer. JavaScript n'appelle pas une fonction pour chaque élément : un dispatch lance de nombreuses invocations WGSL sur le GPU. L'API WebGPU détaille les contrats de ces objets.
Exécuter le calcul du tableau en local
Enregistrez tout le contenu suivant dans index.html, dans un nouveau répertoire d'apprentissage. Conservez l'extension .html, sans enregistrer accidentellement .html.txt. Les messages d'état et d'erreur de l'exemple restent en anglais pour faciliter leur rapprochement avec la documentation de l'API.
Le programme transfère l'entrée dans un tampon de stockage, double les valeurs sur place, puis copie le résultat dans un tampon intermédiaire réservé à la lecture par le CPU. Ce tampon, appelé staging buffer, sert au transfert, pas à un deuxième calcul. En cas d'échec, la page identifie explicitement le résultat de repli CPU au lieu de le présenter comme un résultat GPU.
<!doctype html>
<html lang="en">
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<title>WebGPU: double an array</title>
<h1>WebGPU compute</h1>
<pre id="output" role="status">Starting…</pre>
<script type="module">
const output = document.querySelector('#output');
const input = new Float32Array([1, 2, 3, 4, 5]);
const WORKGROUP_SIZE = 64;
async function main() {
let device;
let storage;
let staging;
let scopeOpen = false;
let asyncFailure = null;
try {
if (!window.isSecureContext) {
throw new Error('Use HTTPS or a localhost static server.');
}
if (!navigator.gpu) {
throw new Error('WebGPU is unavailable in this browser/context.');
}
const adapter = await navigator.gpu.requestAdapter();
if (!adapter) {
throw new Error('No usable WebGPU adapter was returned.');
}
device = await adapter.requestDevice();
device.lost.then(info => {
if (info.reason !== 'destroyed') {
asyncFailure = new Error(`GPU device lost: ${info.message}`);
output.textContent = asyncFailure.message;
}
});
device.addEventListener('uncapturederror', event => {
asyncFailure = new Error(event.error.message);
output.textContent = `GPU error: ${event.error.message}`;
});
device.pushErrorScope('validation');
scopeOpen = true;
storage = device.createBuffer({
label: 'In-place float data',
size: input.byteLength,
usage: GPUBufferUsage.STORAGE |
GPUBufferUsage.COPY_DST |
GPUBufferUsage.COPY_SRC,
});
staging = device.createBuffer({
label: 'CPU readback',
size: input.byteLength,
usage: GPUBufferUsage.COPY_DST | GPUBufferUsage.MAP_READ,
});
device.queue.writeBuffer(storage, 0, input);
const module = device.createShaderModule({
label: 'Double each float',
code: `
@group(0) @binding(0)
var<storage, read_write> values: array<f32>;
@compute @workgroup_size(${WORKGROUP_SIZE})
fn main(@builtin(global_invocation_id) id: vec3<u32>) {
let i = id.x;
if (i >= arrayLength(&values)) {
return;
}
values[i] = values[i] * 2.0;
}
`,
});
const diagnostics = await module.getCompilationInfo();
const shaderErrors = diagnostics.messages.filter(m => m.type === 'error');
if (shaderErrors.length) {
throw new Error(shaderErrors.map(m =>
`${m.lineNum}:${m.linePos} ${m.message}`).join('\n'));
}
const pipeline = await device.createComputePipelineAsync({
layout: 'auto',
compute: {module, entryPoint: 'main'},
});
const bindings = device.createBindGroup({
layout: pipeline.getBindGroupLayout(0),
entries: [{binding: 0, resource: {buffer: storage}}],
});
const encoder = device.createCommandEncoder();
const pass = encoder.beginComputePass();
pass.setPipeline(pipeline);
pass.setBindGroup(0, bindings);
pass.dispatchWorkgroups(Math.ceil(input.length / WORKGROUP_SIZE));
pass.end();
encoder.copyBufferToBuffer(storage, 0, staging, 0, input.byteLength);
device.queue.submit([encoder.finish()]);
const scopeResult = device.popErrorScope();
scopeOpen = false;
const validationError = await scopeResult;
if (validationError) throw new Error(validationError.message);
await staging.mapAsync(GPUMapMode.READ);
// Copy before unmap: the mapped ArrayBuffer is detached by unmap().
const result = new Float32Array(staging.getMappedRange().slice(0));
staging.unmap();
if (asyncFailure) throw asyncFailure;
const correct = result.every((value, i) => value === input[i] * 2);
if (!correct) throw new Error('Result verification failed.');
output.textContent =
`Input: ${JSON.stringify(Array.from(input))}\n` +
`GPU: ${JSON.stringify(Array.from(result))}\nVerified.`;
} catch (error) {
// Explicit educational fallback, never presented as a GPU result.
const cpu = input.map(value => value * 2);
output.textContent =
`WebGPU did not complete: ${error.message ?? String(error)}\n` +
`CPU fallback: ${JSON.stringify(Array.from(cpu))}`;
} finally {
if (scopeOpen && device) {
try { await device.popErrorScope(); } catch { /* Original error shown. */ }
}
if (staging?.mapState === 'mapped') staging.unmap();
staging?.destroy();
storage?.destroy();
// This is a one-shot demo. A real app normally keeps its device alive.
device?.destroy();
}
}
main();
</script>
</html>
Ouvrez un terminal dans ce répertoire. Si Python 3 est déjà installé, exécutez :
python3 -m http.server 8000 --bind 127.0.0.1
Sous Windows, une installation Python existante peut plutôt utiliser py -3 -m http.server 8000 --bind 127.0.0.1. Un autre serveur statique déjà disponible convient aussi ; cet exemple ne nécessite aucune nouvelle dépendance. Limitez l'écoute à l'interface de bouclage pour ne pas exposer involontairement le répertoire au réseau local, et n'y placez que les fichiers de l'exercice.
Ouvrez http://127.0.0.1:8000/. Une exécution réussie doit afficher :
Input: [1,2,3,4,5]
GPU: [2,4,6,8,10]
Verified.
Si le port est occupé, remplacez 8000 par 8001 dans la commande et dans l'adresse. Si une liste de fichiers apparaît, vérifiez le nom du fichier et le répertoire depuis lequel le serveur a été lancé. Arrêtez le serveur avec Ctrl+C à la fin.
Si la page affiche WebGPU did not complete, le message suivant indique l'échec ; CPU fallback signifie seulement que JavaScript ordinaire a produit un résultat. Vérifiez d'abord l'adresse, le contexte sécurisé, le navigateur et l'initialisation du périphérique. Supprimer la gestion des erreurs ne répare pas un environnement indisponible.
Pourquoi créer et lire les tampons de cette manière
Chaque élément de Float32Array occupe 4 octets, soit 20 octets pour cinq éléments. Le array<f32> WGSL utilise ici la même disposition contiguë. La liaison 0 en JavaScript correspond à @group(0) @binding(0) en WGSL. layout: 'auto' simplifie l'apprentissage d'un pipeline, mais un groupe de liaisons créé à partir d'une disposition automatique n'est pas nécessairement réutilisable avec des pipelines sans rapport.
Le tampon de stockage a trois usages distincts : STORAGE autorise l'accès du shader, COPY_DST permet le transfert par queue.writeBuffer(), et COPY_SRC permet de copier le résultat vers l'extérieur. Le tampon de lecture n'utilise que COPY_DST | MAP_READ. Les règles des tampons WebGPU interdisent de combiner directement MAP_READ et STORAGE sur un même tampon.
Une fois mapAsync() terminé, le CPU peut accéder à la plage mappée. Ici, le décalage est de 0 et la taille de 20 octets : ils respectent les contraintes d'un décalage multiple de 8 et d'une taille multiple de 4. Copiez le contenu avant unmap(), car la fin du mappage invalide la vue d'origine.
Le retour de submit() ne signifie pas que le GPU a terminé. Attendre mapAsync() suffit pour cette lecture ; ajouter queue.onSubmittedWorkDone() juste avant n'est pas nécessaire. Le chemin normal lit avant de libérer les tampons ; le chemin d'échec abandonne la tâche et libère les ressources. Cette page à usage ponctuel détruit son périphérique à la fin. Une application réelle réutilise généralement un périphérique plutôt que d'en créer un à chaque lot.
Taille du groupe de travail et nombre de groupes
@workgroup_size(64) déclare 64 invocations suivant X par groupe de travail. dispatchWorkgroups(1) lance un de ces groupes, pas un seul élément du tableau. Cinq éléments produisent donc les indices 0–63 ; les indices 5–63 doivent quitter le shader lors du contrôle des bornes.
Pour 1 000 éléments, Math.ceil(1000 / 64) donne 16 groupes, soit 1 024 invocations ; les 24 dernières quittent le shader. La taille 64 est un choix pédagogique, pas un optimum universel. Vérifiez maxComputeWorkgroupSizeX, maxComputeInvocationsPerWorkgroup et maxComputeWorkgroupsPerDimension avant de dimensionner des tâches plus grandes.
Chaque invocation écrit uniquement son propre élément : aucune barrière de synchronisation n'est nécessaire. Conservez une entrée non vide ; une fonction générale doit traiter les tableaux vides sur le CPU et vérifier les tailles en octets et les limites de liaison avant l'allocation. L'égalité exacte convient au double de ces petits nombres entiers représentés en flottants ; les calculs flottants généraux exigent des tolérances adaptées au problème.
Comme premier exercice, remplacez l'opération par values[i] = values[i] + 3.0. Remplacez aussi le doublement par l'ajout de trois dans la vérification JavaScript et le repli CPU. Le résultat attendu devient [4,5,6,7,8]. Modifier seulement le shader fera échouer la vérification, ce qui est précisément le rôle d'un résultat de référence.
Dessiner ensuite un triangle
Un pipeline de calcul traite des données ; un pipeline de rendu écrit aussi dans une image cible. Le fichier triangle.html suivant est une autre page autonome complète. Enregistrez-le dans le même répertoire, puis ouvrez http://127.0.0.1:8000/triangle.html. Il dessine une fois et redessine lors du redimensionnement de la fenêtre, sans animation continue ni récupération des pixels de chaque image par le CPU.
<!doctype html>
<html lang="en">
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<title>WebGPU triangle</title>
<style>
canvas { display: block; width: min(90vw, 640px); height: 360px; }
</style>
<h1>WebGPU triangle</h1>
<pre id="status" role="status">Starting…</pre>
<canvas id="canvas" aria-label="A colored triangle on a dark background">
This example requires canvas support.
</canvas>
<script type="module">
const status = document.querySelector('#status');
const canvas = document.querySelector('#canvas');
let device;
let context;
let frame = 0;
let stopped = false;
let listening = false;
let scopeOpen = false;
function stop() {
stopped = true;
cancelAnimationFrame(frame);
if (listening) window.removeEventListener('resize', scheduleDraw);
context?.unconfigure();
device?.destroy();
}
function fail(error) {
status.textContent = `Cannot render with WebGPU: ${error.message ?? error}`;
stop();
}
let draw = () => {};
function scheduleDraw() {
cancelAnimationFrame(frame);
frame = requestAnimationFrame(() => {
if (!stopped) {
try { draw(); } catch (error) { fail(error); }
}
});
}
window.addEventListener('pagehide', stop, {once: true});
async function main() {
try {
if (!window.isSecureContext || !navigator.gpu) {
throw new Error('Use a supporting browser over HTTPS or localhost.');
}
const adapter = await navigator.gpu.requestAdapter();
if (!adapter) throw new Error('No usable GPU adapter.');
device = await adapter.requestDevice();
if (stopped) { device.destroy(); return; }
device.lost.then(info => {
if (info.reason !== 'destroyed') fail(new Error(`Device lost: ${info.message}`));
});
device.addEventListener('uncapturederror', event => fail(event.error));
context = canvas.getContext('webgpu');
if (!context) throw new Error('No WebGPU canvas context.');
const format = navigator.gpu.getPreferredCanvasFormat();
device.pushErrorScope('validation');
scopeOpen = true;
context.configure({device, format, alphaMode: 'opaque'});
const module = device.createShaderModule({code: `
struct VertexOutput {
@builtin(position) position: vec4<f32>,
@location(0) color: vec3<f32>,
}
@vertex
fn vs(@builtin(vertex_index) index: u32) -> VertexOutput {
let positions = array<vec2<f32>, 3>(
vec2<f32>(0.0, 0.7),
vec2<f32>(-0.7, -0.7),
vec2<f32>(0.7, -0.7)
);
let colors = array<vec3<f32>, 3>(
vec3<f32>(1.0, 0.2, 0.2),
vec3<f32>(0.2, 1.0, 0.2),
vec3<f32>(0.2, 0.4, 1.0)
);
var result: VertexOutput;
result.position = vec4<f32>(positions[index], 0.0, 1.0);
result.color = colors[index];
return result;
}
@fragment
fn fs(input: VertexOutput) -> @location(0) vec4<f32> {
return vec4<f32>(input.color, 1.0);
}
`});
const pipeline = await device.createRenderPipelineAsync({
layout: 'auto',
vertex: {module, entryPoint: 'vs'},
fragment: {module, entryPoint: 'fs', targets: [{format}]},
primitive: {topology: 'triangle-list'},
});
const scopeResult = device.popErrorScope();
scopeOpen = false;
const error = await scopeResult;
if (error) throw new Error(error.message);
if (stopped) return;
draw = () => {
const ratio = window.devicePixelRatio || 1;
const rect = canvas.getBoundingClientRect();
const limit = device.limits.maxTextureDimension2D;
const desiredWidth = Math.max(1, Math.round(rect.width * ratio));
const desiredHeight = Math.max(1, Math.round(rect.height * ratio));
const scale = Math.min(1, limit / desiredWidth, limit / desiredHeight);
const width = Math.max(1, Math.floor(desiredWidth * scale));
const height = Math.max(1, Math.floor(desiredHeight * scale));
if (canvas.width !== width) canvas.width = width;
if (canvas.height !== height) canvas.height = height;
const encoder = device.createCommandEncoder();
const pass = encoder.beginRenderPass({
colorAttachments: [{
view: context.getCurrentTexture().createView(),
clearValue: {r: 0.03, g: 0.04, b: 0.08, a: 1},
loadOp: 'clear',
storeOp: 'store',
}],
});
pass.setPipeline(pipeline);
pass.draw(3);
pass.end();
device.queue.submit([encoder.finish()]);
};
window.addEventListener('resize', scheduleDraw);
listening = true;
draw();
status.textContent = 'Triangle submitted. Resize the window to redraw.';
} catch (error) {
fail(error);
} finally {
if (scopeOpen && device) {
try { await device.popErrorScope(); } catch { /* Failure shown above. */ }
}
}
}
main();
</script>
</html>
L'image attendue est un triangle coloré sur fond sombre. Triangle submitted signifie que les commandes ont été soumises ; ce message ne prouve pas à lui seul que la présentation à l'écran a réussi. Sans GPU utilisable, la page affiche une erreur claire. Cet exemple de rendu n'implémente pas de solution de repli WebGL.
Sommets, fragments et canevas
Le shader de sommets vs utilise vertex_index pour générer les positions et couleurs de trois sommets. Les positions sont exprimées dans l'espace de découpage (clip space) ; ici, avec w = 1, les valeurs de X et Y allant de -1 à 1 couvrent la zone cible. Pour rendre la géométrie facile à suivre, les trois points sont directement dans le shader : aucun tampon de sommets ni groupe de liaisons n'est nécessaire.
La rastérisation produit des fragments dans la zone couverte par le triangle et interpole les couleurs des sommets. Le shader de fragments fs renvoie la couleur à cet emplacement. Un fragment est une contribution candidate au rendu, pas nécessairement un pixel final de l'écran. draw(3) fait traiter trois sommets par l'étape de sommets, et triangle-list les assemble en triangle.
getPreferredCanvasFormat() renvoie le format de canevas recommandé pour le système actuel. targets: [{format}] dans le pipeline doit correspondre au canevas ; ne figez pas le format d'un ordinateur particulier. Appelez à nouveau getCurrentTexture() à chaque dessin, au lieu de conserver indéfiniment une texture de présentation. La spécification WebGPU du canevas décrit sa configuration et la durée de vie des textures de présentation.
Les dimensions CSS déterminent la mise en page ; canvas.width et canvas.height déterminent les dimensions réelles en pixels du support de dessin. Le code les multiplie par devicePixelRatio, puis les réduit proportionnellement si nécessaire pour respecter maxTextureDimension2D. loadOp: 'clear' et clearValue effacent le fond ; storeOp: 'store' conserve le résultat pour la présentation.
Cette page fixe n'écoute que le redimensionnement de la fenêtre. Un canevas intégré dans une mise en page complexe doit aussi gérer les changements de taille de l'élément et de densité de pixels. L'exemple libère son périphérique lors de pagehide ; une restauration par le cache de navigation précédent/suivant exige une réinitialisation, ou simplement un rechargement pendant l'exercice. Ce cycle de vie ponctuel ne constitue pas un cadre complet de composant.
Disposition des données : accorder les tableaux JavaScript avec WGSL
Le tableau de scalaires du premier exemple ne nécessite aucun remplissage supplémentaire, mais les coordonnées tridimensionnelles révèlent souvent des erreurs de disposition. La spécification WGSL de l'alignement et de la taille définit les dispositions suivantes pour les éléments en stockage, en octets :
La taille est l'espace occupé par la valeur ; l'alignement contraint son adresse de départ ; le pas est la distance entre les débuts de deux éléments consécutifs. Le pas d'un tableau est arrondi vers le haut selon l'alignement de l'élément. Chaque point de array<vec3<f32>> doit donc occuper quatre emplacements flottants côté hôte, et non trois :
const points = new Float32Array([
1, 2, 3, 0, // x, y, z, padding
4, 5, 6, 0,
]);
Le quatrième emplacement est du remplissage, pas une quatrième composante du vec3 WGSL. Une structure comprenant deux membres vec3f les place aux décalages 0 et 16, pour une taille totale de 32 ; consultez les exemples de disposition des tableaux WGSL. L'espace d'adressage uniform impose des règles supplémentaires : la disposition storage ne se réutilise pas automatiquement pour les uniforms.
L'alignement des membres de structure, le pas du tableau, l'alignement des décalages de liaison et celui des plages mappées sont des contraintes différentes. En cas de valeurs étranges ou d'échec de validation, vérifiez la disposition et la plage réellement liée en octets, pas seulement le nombre d'éléments. Le calcul numérique doit aussi tenir compte de la différence de précision entre f32 en WGSL et le Number par défaut de JavaScript.
Gérer synchronisation et erreurs avant d'augmenter la charge
Une barrière de groupe synchronise les invocations participantes de ce groupe ; elle ne peut pas réunir tous les groupes de travail en un point commun. Les algorithmes qui nécessitent des communications entre groupes se divisent généralement en plusieurs dispatchs ordonnés. Si vous ajoutez workgroupBarrier(), analysez le flux de contrôle avant de conserver un retour anticipé pour certaines invocations alors que les autres atteignent la barrière. WGSL impose des règles d'uniformité à ce flux ; voir l'analyse d'uniformité WGSL.
Ne gardez pas un tampon mappé tout en demandant au GPU d'utiliser ce même tampon. Pour des traitements répétés, réutilisez pipelines et ressources, gardez les données intermédiaires sur le GPU et ne récupérez que ce dont le CPU a besoin. Plusieurs tampons intermédiaires permettent des ordonnancements plus avancés avec chevauchement, mais ne sont pas nécessaires au bon fonctionnement du premier exemple.
Les portées d'erreur pushErrorScope() / popErrorScope() capturent les erreurs GPU asynchrones de la catégorie choisie ; popErrorScope() renvoie la première erreur correspondante ou null. Les exemples ne capturent ainsi que validation, pas toutes les catégories comme le manque de mémoire ou les erreurs internes. Un écouteur uncapturederror reçoit les erreurs non capturées par une portée. Un simple try/catch JavaScript ne remplace pas ces mécanismes ; voir les explications de MDN sur les erreurs.
device.lost peut se résoudre même juste après la création du périphérique. La récupération ne peut pas réutiliser ses anciens tampons, textures, groupes de liaisons ou pipelines. Un appel volontaire à device.destroy() déclenche aussi la notification de perte. Le code ignore donc reason === 'destroyed' pour ne pas confondre nettoyage normal et demande de récupération.
Détruisez les tampons et textures que l'application possède lorsqu'ils ne servent plus, et terminez les mappages. Les pipelines et groupes de liaisons n'ont pas de méthode .destroy() générique : libérez leurs références. Le navigateur gère les textures de présentation courantes du canevas ; ce ne sont pas des textures permanentes allouées par l'application.
Déterminer si le GPU est réellement plus rapide
Commencez par une référence CPU correcte et des tailles d'entrée réalistes. Distinguez au moins deux questions : combien de temps une première visite de la page met à produire un résultat, et combien de temps prend un lot lorsque le périphérique, les pipelines et les tampons sont réutilisés.
- Mesurez séparément l'initialisation du périphérique et la création des shaders et pipelines, sans les confondre avec l'exécution à chaud.
- Utilisez les mêmes bornes de mesure pour une comparaison de bout en bout : transferts nécessaires, calcul, copies, attentes, récupération et traitement du résultat.
- Chronométrer uniquement
queue.submit()mesure surtout la soumission. Pour savoir quand JavaScript dispose du résultat du premier exemple, mesurez jusqu'à la fin deawait mapAsync()et de la copie du résultat. - Répétez les mesures et indiquez leur variabilité, la taille des données, le navigateur, le système et les conditions GPU. Une observation ne démontre pas une accélération générale.
- Placez les vérifications de correction de façon appropriée et précisez les tolérances flottantes des tâches numériques. N'omettez pas des transferts indispensables pour améliorer le temps annoncé.
Ces conseils découlent de la séquence de soumission et de récupération, sans garantir un gain de vitesse. La fonctionnalité facultative timestamp-query permet une analyse temporelle GPU plus fine, mais exige une vérification et une demande de prise en charge distinctes ; elle ne remplace pas une mesure de bout en bout. Calculer puis afficher directement sans lecture CPU constitue une autre charge utile : mesurez-la séparément d'un traitement renvoyant chaque résultat à JavaScript.
Applications adaptées et exercices suivants
Filtres d'image, mises à jour de nombreuses particules, tableaux numériques structurés, certains noyaux de simulation et inférence d'apprentissage automatique dans le navigateur méritent une évaluation avec WebGPU. Ils peuvent offrir assez de travail indépendant pour amortir la planification et les transferts. Ce sont des pistes d'application, pas des accélérations mesurées. Pour l'apprentissage automatique, il faut aussi vérifier le framework, les opérateurs et les navigateurs visés ; pour le calcul scientifique, vérifier d'abord la précision, les dépendances algorithmiques, la mémoire et la validation des résultats.
Les petits tableaux, opérations DOM, états d'interface, logiques fortement séquentielles et allers-retours permanents de petits résultats conviennent généralement mieux à JavaScript ordinaire ou à des workers CPU. Envisagez une application native ou un serveur si les capacités du navigateur, sa mémoire ou la couverture des appareils clients ne conviennent pas. WebGPU ne manipule pas directement le DOM et ne contourne pas le bac à sable du navigateur.
Site Lab propose des expériences existantes pour comparer les approches : la dispersion par un prisme utilise un rendu WebGL par lots avec p5.js, la Terre ASCII montre la transformation d'une scène Three.js en caractères, et l'attracteur de Rössler relie intégration numérique et présentation d'une trajectoire. Ce sont des références d'apprentissage, pas des implémentations déjà converties à WebGPU.
Essayez d'étendre le tableau à des mises à jour de particules indépendantes, puis faites consommer directement les résultats du calcul par le rendu pour éviter une récupération à chaque image. En comparant le modèle de Rössler, conservez les dépendances temporelles : l'étape suivante d'une trajectoire exige le résultat précédent, donc ses pas de temps ne peuvent pas être simplement lancés indépendamment. Plusieurs trajectoires avec des conditions initiales différentes peuvent en revanche offrir un autre niveau de parallélisme. D'autres outils de développement et de projet figurent dans Outils et méthodes de travail.