Optimisations WebGPU : Réutilisation du tampon de commande et décalage dynamique du tampon uniforme

Cet article présente deux optimisations clés dans WebGPU : la réutilisation des tampons de commande de rendu (render command buffer) et l'utilisation du décalage dynamique (dynamic uniform buffer offset). Nous analyserons également les résultats du benchmark réalisé avec l'exemple Chrome->webgpu-samplers->animometre.

Optimisation : Réutilisation du tampon de commande de rendu

Problématique

Pour chaque frame, le processus de rendu suit ces étapes :

  • Création d'un tampon de commande
  • Démarrage d'un passage de rendu
  • Configuration des commandes de rendu
  • Fin du passage de rendu

Exemple de code :

return function frame() {
    ...
    const commandEncoder = device.createCommandEncoder();
    ...
    const passEncoder = commandEncoder.beginRenderPass(renderPassDescriptor);
    
    passEncoder.setPipeline(pipeline);
    passEncoder.setVertexBuffer(0, verticesBuffer);
    passEncoder.setBindGroup(0, uniformBindGroup1);
    passEncoder.draw(36, 1, 0, 0);
    
    passEncoder.endPass();
    ...
}

Ces opérations génèrent des coûts de répétition significatifs :

  • Coûts liés aux bindings JavaScript (conversino des objets descripteurs, vérification des limites, validation des données)
  • Coûts de création et d'encodage des commandes de rendu

Solution

WebGPU introduit les GPURenderBundle permettant d'enregistrer une séquence de commandes une fois pour toutes. Ces bundles peuvent être exécutés à chaque frame sans coût de ré-encodage.

Exemple de code optimisé (un seul bundle) :

function enregistrerPassageDeRendu(passEncoder) {
    passEncoder.setPipeline(pipeline);
    passEncoder.setVertexBuffer(0, verticesBuffer);

    passEncoder.setBindGroup(0, groupeLiaisonUniforme1);
    passEncoder.draw(36, 1, 0, 0);

    passEncoder.setBindGroup(0, groupeLiaisonUniforme2);
    passEncoder.draw(36, 1, 0, 0);
}

const encodeurBundle = device.createRenderBundleEncoder({
    colorFormats: [formatChaineEchange],
});
enregistrerPassageDeRendu(encodeurBundle);
const bundleRendu = encodeurBundle.finish();


return function frame(timestamp) {
    ...
    const commandEncoder = device.createCommandEncoder();
    ...
    const passEncoder = commandEncoder.beginRenderPass(renderPassDescriptor);

    passEncoder.executeBundles([bundleRendu]);

    passEncoder.endPass();
    ...
}

Analyse technique

Définitions clés :

interface GPUDevice : EventTarget {
   ...
   GPURenderBundleEncoder createRenderBundleEncoder(GPURenderBundleEncoderDescriptor descriptor);
   ...
}

dictionary GPURenderBundleEncoderDescriptor : GPUObjectDescriptorBase {
    required sequence<GPUTextureFormat> colorFormats;
    GPUTextureFormat depthStencilFormat;
    unsigned long sampleCount = 1;
};

Les formats de couche et profondeur/stencil doivent correspondre entre les bundles et leurs passages de rendu associés.

Optimisation : Décalage dynamique du tampon uniforme

Problématique

Chaque appel de dessin nécessite souvent des variablse uniformes distinctes, impliquant la création de groupes de liaison uniques par frame. Or, la création de ces groupes est coûteuse.

Solution

  1. Cache de groupes de liaison : Précréer tous les groupes et les utiliser directement
  2. Décalage dynamique : Utiliser un seul groupe avec des offsets multiples

Approche optimale :

// Configuration avec décalage dynamique
const layoutGroupeDynamique = device.createBindGroupLayout({
    bindings: [
        { binding: 0, visibility: GPUShaderStage.VERTEX, type: "uniform-buffer", hasDynamicOffset: true },
    ],
});

const groupeDynamique = device.createBindGroup({
    layout: layoutGroupeDynamique,
    bindings: [{
        binding: 0,
        resource: {
            buffer: tamponUniforme,
            offset: 0,
            size: 5 * Float32Array.BYTES_PER_ELEMENT,
        },
    }],
});

// Dans la boucle de rendu
const offsets = [0];
for (let i = 0; i < objets; ++i) {
    offsets[0] = i * tailleAlignee;
    passEncoder.setBindGroup(0, groupeDynamique, offsets);
    passEncoder.draw(3, 1, 0, 0);
}

Comparaison des performances

Résultats obtenus sur Mac Pro 2014 avec 40 000 triangles :

  • Bundle seul : Réduction de 98% du temps JS (14ms → 0.2ms), amélioration globale de 20%
  • Combinaison bundle+offset : Aucune différence notable
  • Offset seul : Augmentation de 60% du temps JS, mais impact global négligeable (+10%)

Conclusion

L'optimisation par décalage dynamique offre un bon compromis : code simplifié, faible impact CPU et gain GPU. L'utilisation des bundles reste recommandée pour les gains CPU significatifs, malgré un léger coût GPU supplémentaire.

Étiquettes: WebGPU Optimisation Benchmark Tampon de commande Décalage dynamique

Publié le 4 septembre à 22h33