Shaders en temps réel: mes erreurs de débutant pour éviter les saccades
Dans une œuvre réactive, le public ne distingue pas toujours la cause — compilation, surcharge du processeur graphique, attente du processeur central, synchronisation avec un capteur ou simple variation du pipeline — mais il perçoit immédiatement la rupture. L’image semble accrocher, le mouvement perd sa continuité, et l’interaction paraît moins fiable.
C’est là que l’optimisation des shaders en temps réel pour l’art numérique cesse d’être une affaire de micro-élégance dans le code. Il faut comprendre à quel moment le travail est demandé au GPU, combien de fois il est répété, quelles données circulent entre les étapes du pipeline et ce que le pilote compile réellement. Une saccade ne vient pas nécessairement d’un seul shader, ni même du GPU seul: elle peut apparaître à l’intersection de l’application, du pilote, du matériel et de la manière dont les ressources sont préparées.
Mes premières erreurs ont surtout consisté à optimiser ce qui était facile à lire plutôt que ce qui était réellement coûteux. J’ai déplacé des calculs entre les étapes du pipeline sans mesurer leur fréquence, remplacé des divisions par des multiplications en supposant que le résultat serait strictement identique, et accordé trop de confiance au nettoyage automatique du compilateur. Ces réflexes sont compréhensibles. Ils deviennent problématiques dès qu’ils remplacent une observation concrète de la scène.
Le piège de la compilation à la volée: comprendre les saccades
Quand une œuvre numérique compile des shaders ou prépare des variantes pendant l’exécution, le problème n’est pas seulement le temps nécessaire pour produire le code machine. Le pilote peut aussi devoir créer ou finaliser un état de pipeline, charger des ressources, configurer des formats de rendu ou attendre que plusieurs éléments soient disponibles avant de lancer le dessin. Si cette préparation intervient au moment exact où le public attend une nouvelle image, le coût devient visible.
À 60 images par seconde, une image dispose d’environ 16,7 millisecondes pour être produite. Une opération ponctuelle qui dépasse ce budget ne provoque pas automatiquement une saccade perceptible: tout dépend de la file de commandes, de la synchronisation et de la capacité du système à absorber le retard. Mais lorsqu’une compilation bloque le chemin critique du rendu, elle peut retarder plusieurs images successives. Le résultat se manifeste alors par une pause courte, parfois plus gênante qu’une baisse régulière de la fréquence d’affichage.
La cause peut être l’application, le pilote ou le matériel. Une application mal préparée peut demander un état inédit au mauvais moment. Un pilote peut effectuer une compilation ou une validation supplémentaire que le programme ne voit pas directement. Une carte graphique peut manquer de bande passante, de mémoire disponible ou de capacité de calcul. Dans une installation d’art numérique, il faut aussi compter avec le casque, la caméra, le suivi de position, les surfaces de capture et les sorties vidéo. Réduire toutes les saccades à une seule erreur de programmation conduit à chercher au mauvais endroit.
D’où vient la recompilation
Les API modernes comme Vulkan, DirectX 12 et Metal donnent davantage de responsabilité à l’application que les API plus anciennes. Au lieu de laisser une partie importante de la préparation se faire implicitement lors du dessin, elles demandent souvent de décrire plus explicitement les états du pipeline. Selon l’API, cela passe notamment par des objets de pipeline ou des structures équivalentes réunissant le programme utilisé, les formats de sortie, le mélange des couleurs, la profondeur, le test de stencil, la rastérisation et d’autres paramètres.
Dans ce contexte, deux shaders identiques peuvent encore correspondre à des états différents si le format de la cible ou l’état de profondeur change. Une permutation activée par une directive de compilation peut également produire un autre binaire. Le nombre de variantes augmente vite dès que l’œuvre propose plusieurs niveaux de qualité, un mode stéréoscopique, des matériaux distincts ou des options activables en direct.
L’erreur de débutant consiste à penser qu’un shader compilé est nécessairement prêt à être utilisé dans toutes les situations. Il faut plutôt distinguer plusieurs niveaux de préparation:
- le texte du shader, écrit en GLSL, HLSL ou dans un langage équivalent;
- le module compilé pour une architecture donnée;
- la variante correspondant à un ensemble de définitions ou de paramètres;
- l’état de pipeline qui associe ce programme aux autres réglages du rendu;
- les ressources effectivement disponibles au moment du dessin.
Préparer le premier niveau ne garantit pas que les suivants seront disponibles sans coût pendant l’exposition.
Trois règles de précompilation opérationnelle
La première consiste à inventorier les variantes réellement utilisées. Il ne s’agit pas de générer toutes les combinaisons imaginables par prudence: une telle stratégie peut produire un catalogue impossible à maintenir. Il faut plutôt repérer les permutations qui correspondent à des situations visibles dans l’œuvre: mode de rendu, qualité, matériau, présence d’une texture, stéréoscopie ou type de surface.
La deuxième consiste à construire les états du pipeline avant la première image interactive. Cela peut avoir lieu au lancement, dans une salle d’attente, pendant une transition prévue par la scénographie ou lors d’un écran de chargement. Le point important est de ne pas laisser une nouvelle combinaison apparaître au milieu d’un geste du visiteur. Si certaines variantes ne peuvent être connues à l’avance, l’application doit au moins prévoir leur apparition et éviter de la placer sur le chemin critique.
La troisième consiste à mettre en place un cache avec une stratégie d’invalidation claire. Un cache n’est pas une boîte magique: il dépend souvent du pilote, du matériel, de la version de l’application et des paramètres de compilation. Une mise à jour peut le rendre inutilisable ou modifier les temps de préparation. Pour une œuvre exposée sur plusieurs machines, il faut donc tester le démarrage réel sur les configurations retenues, et non seulement sur l’ordinateur de développement.
Préparer les variantes avant l’interaction transforme une cause imprévisible de saccade en une étape de production que l’on peut observer, tester et documenter.
Le préchargement fait partie du média de l’œuvre au même titre que les textures, les modèles et les fichiers audio. Il mérite une place dans le calendrier de production, mais aussi dans la procédure d’installation. Une installation qui fonctionne sur la machine de l’artiste peut encore rencontrer un état inédit sur le matériel du lieu d’exposition.
Arbitrage entre vertex et fragment shaders: où placer vos calculs
Le deuxième levier de l’optimisation des shaders en temps réel pour l’art numérique consiste à regarder combien de fois un calcul est exécuté. Le vertex shader travaille sur les sommets. Le fragment shader, lui, intervient sur les fragments générés par la rastérisation, souvent à une fréquence bien plus élevée lorsque les surfaces occupent une grande partie de l’écran.
La comparaison n’est pas aussi simple que « le vertex shader est rapide, le fragment shader est lent ». Un maillage très dense peut rendre le traitement des sommets coûteux. À l’inverse, un fragment shader léger peut parfaitement convenir à une surface plein écran. Ce qui compte est le produit entre le coût d’une opération et le nombre d’éléments auxquels elle s’applique.
Un calcul qui dépend seulement de la position du sommet, du temps global ou d’un paramètre uniforme n’a pas forcément besoin d’être répété pour chaque pixel. Il peut parfois être effectué avant la rastérisation, puis transmis au fragment shader par interpolation. À l’inverse, un calcul lié à la texture, à la normale finale, à la visibilité ou à une dérivée d’écran doit généralement rester au niveau du fragment.
Répartition typique des calculs
| Type de calcul | Emplacement souvent pertinent | Point de vigilance |
|---|---|---|
| Déformation d’un maillage à partir de la position et du temps | Vertex shader | La densité du maillage doit représenter correctement la déformation |
| Transformation objet, monde et vue | Vertex shader | Les grandes scènes peuvent exiger une attention particulière à la précision |
| Couleur dépendant seulement du sommet | Vertex shader puis interpolation | L’interpolation peut lisser un détail que l’on voulait conserver |
| Bruit procédural appliqué à chaque pixel | Fragment shader | Son coût dépend de la résolution et du nombre de couches |
| Éclairage dépendant de la normale et de la position finales | Fragment shader | Le calcul par sommet peut produire des facettes ou des variations trop lisses |
| Composition de couches et mélange avec le fond | Fragment shader | Les formats de cible et la bande passante comptent aussi |
| Dérivées d’écran pour le filtrage ou les niveaux de texture | Fragment shader | Elles ne sont pas toujours disponibles ou pertinentes en amont |
| Génération d’une normale à partir d’une déformation de sommet | Vertex shader ou étape dédiée | Il faut conserver la cohérence entre géométrie et éclairage |
Pour une œuvre générative, la question se pose souvent autour du bruit, des champs de distance et des fonctions trigonométriques. Déplacer un bruit procédural du fragment shader vers le vertex shader peut réduire le nombre d’évaluations, mais le résultat ne sera plus le même: le motif sera interpolé entre les sommets. Cette différence peut être une perte, ou au contraire devenir une esthétique intéressante. L’optimisation n’est donc pas toujours une soustraction. Elle peut modifier la matière visuelle de l’œuvre.
Gouraud, Phong et les compromis visibles
Un éclairage calculé au niveau des sommets puis interpolé est moins coûteux par pixel qu’un éclairage recalculé pour chaque fragment, mais il peut manquer des variations fines. Les reflets spéculaires, les arêtes brillantes et certaines transitions rapides ont besoin d’informations plus locales. Un calcul au niveau du fragment conserve cette finesse au prix d’une répétition plus importante.
Le bon arbitrage dépend de la géométrie et du mouvement. Sur un maillage dense, un éclairage par sommet peut être suffisamment précis. Sur une surface peu subdivisée, l’interpolation risque de faire disparaître un reflet ou de produire une transition artificiellement uniforme. À l’inverse, un éclairage complet par pixel n’est pas automatiquement justifié pour une modulation diffuse très douce.
Une méthode utile consiste à produire deux versions visuellement comparables, puis à les profiler dans la scène réelle. Il faut observer la caméra utilisée par le public, la résolution de sortie, l’occlusion, le nombre d’instances et les mouvements effectifs. Un shader peut sembler rapide dans une petite fenêtre de prévisualisation et devenir dominant dès que l’œuvre passe en plein écran ou en stéréoscopie.
Le calcul le moins cher n’est pas toujours celui que l’on déplace: c’est celui que l’on évite de répéter sans raison.
Il faut aussi surveiller les interpolants. Ajouter une nouvelle normale, une coordonnée ou une couleur à transmettre du vertex shader au fragment shader peut augmenter la quantité de données transportées et la pression sur les registres. Là encore, le profilage permet de vérifier si le déplacement d’un calcul produit réellement un bénéfice sur le matériel visé.
Précision numérique et registres: l’impact du 16-bit sur la fluidité
Les GPU modernes proposent plusieurs niveaux de précision selon l’API et l’architecture. On rencontre notamment float, half, mediump, highp ou min16float, mais ces mots ne garantissent pas partout un comportement identique. Sur certains matériels, une précision réduite peut améliorer l’utilisation des registres ou la bande passante. Sur d’autres, le compilateur peut conserver une précision plus élevée, ou le gain peut être trop faible pour apparaître dans la mesure finale.
Le 16-bit est donc un outil, pas une promesse universelle. Réduire la taille des valeurs intermédiaires peut permettre à davantage de groupes de travail de rester actifs, limiter les transferts et alléger la consommation de mémoire. Mais une variable qui semble anodine peut aussi influencer une normalisation, une accumulation de petites valeurs ou une comparaison proche d’un seuil. L’image ne se dégrade pas forcément de façon spectaculaire; elle peut simplement devenir instable sur certaines distances, certaines rotations ou certains niveaux de zoom.
Comparatif des précisions courantes
| Type ou qualificatif | Taille généralement visée | Usage possible | Risque principal |
|---|---|---|---|
float / highp | 32 bits | Positions, profondeur, transformations, accumulations sensibles | Pression accrue sur les registres et la bande passante |
half / mediump | 16 bits lorsque le matériel le permet | Couleurs, intensités, certaines normales et coordonnées locales | Plage et précision insuffisantes dans les cas limites |
min16float en HLSL | Précision minimale demandée | Valeurs dont la précision exacte n’est pas critique | Le matériel peut choisir une représentation plus large |
La plage théorique d’un type ne suffit pas à déterminer son adéquation. Une position locale peut rester dans une petite plage tout en nécessitant une grande précision si elle est utilisée dans une soustraction entre deux valeurs proches. À l’inverse, une couleur peut tolérer une précision moindre, surtout lorsque le bruit visuel ou le traitement de postproduction masque les écarts.
Quand essayer une précision réduite
Les couleurs, certaines intensités lumineuses et des coordonnées locales constituent souvent de bons candidats. Il faut toutefois tester les gradients, les surfaces très éloignées, les animations lentes et les effets qui accumulent de petites variations au fil du temps. Les erreurs qui ne sont pas visibles sur une image fixe peuvent devenir évidentes lorsqu’elles se déplacent.
Les positions de grande scène et les calculs de profondeur demandent davantage de prudence. Une scène d’installation peut sembler petite à l’écran tout en utilisant des coordonnées mondiales éloignées de l’origine. Dans ce cas, passer trop tôt en 16-bit peut provoquer du scintillement, des déplacements irréguliers ou du z-fighting. Les grands UV répétés, les intersections et les calculs de dérivées sont également de mauvais endroits pour appliquer une réduction de précision sans vérification.
La bonne démarche est de réduire la précision par famille de variables, puis de comparer:
- la stabilité de l’image lors des mouvements de caméra;
- les contours et les détails spéculaires;
- les transitions de profondeur;
- la cohérence entre le GPU de développement et celui du lieu d’exposition;
- le temps du shader, la pression sur les registres et l’occupation observée dans le profilage.
Le mot-clé half ne doit pas devenir une décoration ajoutée à toutes les déclarations. Il doit exprimer une décision: cette donnée n’a pas besoin de davantage de précision dans ce calcul précis, sur cette scène et avec ce matériel.
Mathématiques optimisées: remplacer la division par la multiplication
La division peut être plus coûteuse qu’une multiplication, mais le remplacement mécanique de x / y par x * (1.0 / y) n’est pas une règle universelle. Les compilateurs savent souvent transformer certaines expressions, déplacer un calcul invariant ou utiliser une instruction spécialisée. Avant de modifier le code, il faut donc vérifier le résultat produit par le compilateur et mesurer le shader obtenu.
L’idée reste utile lorsque le même diviseur est réutilisé plusieurs fois. Calculer une valeur inverse une seule fois peut éviter de répéter une opération, surtout si le dénominateur est uniforme pour l’ensemble du dessin ou constant pendant une passe. Mais l’inverse et la division ne sont pas nécessairement exactement équivalents en virgule flottante. L’ordre des opérations, les arrondis, les valeurs subnormales, les zéros et les valeurs très proches de zéro peuvent produire des écarts.
La transformation doit donc être traitée comme une approximation contrôlée, pas comme une identité parfaite. Il faut examiner les cas limites et vérifier que l’écart reste acceptable pour l’œuvre. Dans certains effets, un petit changement est invisible. Dans d’autres, il modifie la position d’un seuil, la luminosité d’un contour ou la stabilité d’un motif animé.
Cas d’application typiques en art numérique
- Normalisation de vecteurs. Calculer une longueur puis réutiliser son inverse peut être pertinent lorsque la même valeur intervient dans plusieurs composantes. Il faut protéger le cas du vecteur nul ou presque nul.
- Coordonnées polaires. Une expression comme
r = length(p)suivie dep / rpeut être réécrite avec une valeur inverse, mais la gestion derproche de zéro doit rester explicite. - Mélanges répétés. Dans une accumulation de couches, un poids préparé une fois peut éviter de refaire le même calcul à chaque composante.
- Paramètres uniformes. Si une valeur ne change pas entre les fragments d’un dessin, son inverse peut parfois être calculé en amont par l’application ou dans une étape moins répétée.
- Effets atmosphériques. Il ne suffit pas de remplacer une division par une multiplication: l’expression globale, la fonction exponentielle et la plage des profondeurs déterminent le coût réel.
Là encore, le gain dépend de l’architecture. Sur un GPU, une opération isolée ne mérite pas toujours une réécriture. Le coût peut être dominé par les accès aux textures, la divergence, les dépendances entre instructions ou la résolution de la cible. Une modification mathématique doit être conservée seulement si elle apporte un bénéfice mesurable sans dégrader l’image.
En virgule flottante, une forme plus courte n’est pas forcément une forme équivalente. L’optimisation commence par la mesure de l’écart, pas par la confiance dans l’expression.
Une autre erreur consiste à réécrire tout un shader à partir d’une seule opération supposée coûteuse. Les compilateurs modernes simplifient déjà de nombreuses expressions. Une inspection du code intermédiaire ou du rapport du compilateur peut révéler que la division a été remplacée automatiquement, tandis que le véritable coût se trouve dans un échantillonnage de texture ou une branche divergente.
Gestion du code mort et des branchements: nettoyer le pipeline
Les compilateurs éliminent souvent une partie du code mort: variables inutilisées, calculs dont le résultat ne rejoint aucune sortie observable ou chemins rendus impossibles par des constantes. Cette optimisation est précieuse, mais elle ne justifie pas de laisser le shader dans un état confus. Le code mort peut compliquer la lecture, masquer une fonctionnalité qui ne fonctionne plus et rendre plus difficile l’interprétation des captures de profilage.
Une variable uniforme qui n’est finalement pas utilisée peut notamment disparaître du programme compilé. Dans certaines API, sa recherche renverra alors une valeur indiquant qu’elle n’existe pas dans l’interface active. Ce comportement est normal, mais il peut surprendre une application qui suppose que chaque déclaration du shader correspond encore à une ressource accessible. Le nettoyage du code doit donc être accompagné d’une gestion correcte des interfaces entre l’application et le shader.
Les branchements conditionnels sont un autre terrain de fausses certitudes. Un if n’est pas automatiquement mauvais. Si une condition est identique pour les éléments exécutés ensemble — par exemple un paramètre uniforme pour un dessin — le matériel peut suivre un seul chemin. En revanche, lorsque les fragments voisins prennent des décisions différentes, le groupe d’exécution peut devoir parcourir plusieurs chemins. Le coût dépend alors de la divergence, de la longueur de chaque branche et du nombre de pixels concernés.
Ce que j’ai appris à vérifier
- La nature de la condition. Est-elle uniforme pour tout le dessin, liée au sommet ou différente pour chaque fragment?
- La longueur des branches. Une divergence entre deux chemins très courts n’a pas le même poids qu’une divergence entre deux effets complexes.
- La fréquence de la décision. Une branche évaluée sur une petite surface n’a pas le même impact qu’une condition exécutée sur toute la cible.
- La présence d’accès aux textures. Une branche peut éviter un échantillonnage coûteux, et sa suppression peut alors empirer la situation.
- Le comportement du compilateur. Une condition constante peut déjà être supprimée, tandis qu’une expression apparemment simple peut rester dynamique.
- La lisibilité après optimisation. Un shader légèrement plus long mais compréhensible vaut parfois mieux qu’une expression compacte impossible à maintenir.
Les fonctions sans branchement comme step, mix et smoothstep peuvent être utiles lorsque la condition varie fortement d’un fragment à l’autre. Elles ne sont toutefois pas automatiquement plus rapides. Remplacer un if par une expression qui calcule les deux résultats avant de les mélanger peut faire davantage de travail, notamment si chaque résultat implique des textures ou des fonctions complexes.
La meilleure pratique consiste à comparer des versions équivalentes dans une capture de frame. RenderDoc, PIX, Xcode GPU Capture, NVIDIA Nsight ou AMD Radeon GPU Profiler peuvent aider à relier le temps observé à une passe, à un dessin ou à une famille de shaders. Le nom de l’outil importe moins que la question posée: quelle partie du pipeline est réellement en attente, et dans quelles conditions?
Débogage des shaders interactifs: mesurer avant de déplacer
Les erreurs de performance les plus coûteuses viennent souvent d’une intuition plausible mais non vérifiée. On repère un mod, un length, une boucle ou une division, puis on décide que cette instruction est responsable de la saccade. Elle peut effectivement contribuer au temps de calcul, mais elle peut aussi être négligeable face à une lecture de texture, à une surimpression, à une résolution trop élevée ou à une synchronisation avec le processeur central.
Un profilage utile commence par une scène représentative. Il faut capturer le moment où l’œuvre est réellement sollicitée: mouvement de caméra, apparition de particules, changement de matériau, suivi d’un visiteur ou activation d’un mode de réalité augmentée. Une image immobile et dépouillée ne permet pas toujours de voir le problème.
La capture peut ensuite être lue selon plusieurs axes:
- le temps total de la trame;
- la part prise par chaque passe;
- le nombre de dessins et d’instances;
- la résolution de chaque cible intermédiaire;
- les lectures et écritures de textures;
- les changements d’état et de pipeline;
- la pression sur les registres et l’occupation;
- les attentes entre le processeur central et le processeur graphique.
Quelques outils et leur usage
| Outil | Environnement principal | Ce qu’il permet d’observer |
|---|---|---|
| RenderDoc | Vulkan, D3D12, OpenGL et autres contextes compatibles | Capture d’images, dessins, ressources et états |
| PIX | Windows et DirectX 12 | Chronologie, événements GPU et analyse des états de pipeline |
| Xcode GPU Capture | macOS, iOS et Metal | Passes Metal, ressources et coût sur les appareils Apple |
| NVIDIA Nsight | GPU NVIDIA | Occupation, groupes d’exécution et détails d’ordonnancement |
| AMD Radeon GPU Profiler | GPU AMD | Occupation, compteurs matériels et goulets d’étranglement |
Le profilage ne sert pas seulement à trouver une instruction lente. Il permet de distinguer un problème de calcul d’un problème de préparation. Si le GPU est peu occupé mais que la trame attend le processeur central, réécrire le fragment shader ne résoudra rien. Si le temps augmente avec la résolution alors que le nombre de sommets reste stable, la charge se trouve probablement du côté des fragments ou des passes plein écran. Si le temps varie au premier affichage d’un matériau, la compilation ou la création de ressources mérite une enquête.
Pour le débogage des shaders d’un art interactif, je garde une règle simple: modifier une famille de calculs à la fois, mesurer avant et après, puis vérifier l’image dans les cas qui comptent. Une optimisation qui accélère une scène fixe mais dégrade une animation lente n’est pas nécessairement un progrès. Une réduction visible sur une carte graphique peut disparaître sur une autre, surtout lorsque les pilotes ne choisissent pas les mêmes instructions ou la même représentation pour les types réduits.
Une discipline de production plutôt qu’une collection d’astuces
L’optimisation des shaders en temps réel pour l’art numérique ne repose pas sur une formule unique. Elle combine la préparation des variantes, la répartition des calculs entre sommets et fragments, le choix prudent de la précision, la simplification arithmétique et le nettoyage des branchements. Ces leviers se répondent: déplacer un calcul peut augmenter le nombre d’interpolants; réduire la précision peut modifier une branche; supprimer une branche peut entraîner le calcul de deux chemins au lieu d’un.
Pour une œuvre destinée à un musée, une galerie, un festival ou un espace public, le pipeline doit être pensé avec les conditions d’exposition en tête. Il faut connaître les résolutions réellement utilisées, les modes stéréoscopiques éventuels, les mouvements de caméra, les capteurs connectés et les variantes de matériel. Les meilleures pratiques de performance en GLSL ne sont pas des interdits absolus. Elles servent à formuler de bonnes hypothèses, à savoir où regarder et à éviter les réécritures sans preuve.
La précompilation réduit le risque de découvrir un état inédit pendant l’interaction. Le profilage indique si le problème vient des fragments, des sommets, de la bande passante ou de la synchronisation. La précision réduite peut libérer des ressources, mais seulement lorsque l’image conserve sa stabilité. Le remplacement d’une division peut aider lorsque l’inverse est réutilisé, sans garantir une égalité parfaite du résultat. Les branchements peuvent être conservés lorsqu’ils évitent un travail inutile, puis remplacés lorsque la divergence devient dominante.
Une œuvre fluide n’est donc pas une œuvre dont chaque shader a été rendu artificiellement minimal. C’est une œuvre dont les coûts sont connus dans les situations où le public la rencontre. Le code reste lisible, les variantes sont préparées, les choix visuels sont assumés et les gains sont vérifiés sur le matériel qui fera réellement tourner l’installation. C’est cette méthode, plus que n’importe quelle astuce isolée, qui réduit la latence des shaders GLSL et rend une œuvre interactive plus stable, plus transmissible et plus durable.




