Vous ne remarquez pas le GPU quand il fonctionne bien. Les images sont fluides, les ventilateurs restent discrets, et personne dans votre organisation ne se transforme
en « expert en graphisme » cinq minutes avant une démo. Puis vous livrez une nouvelle build et les performances s’effondrent, une mise à jour du pilote transforme votre éclairage en disco,
ou la première partie après un patch saccade comme un tableau de stockage en cours de reconstruction sous charge. C’est le moment où vous vous rappelez : le graphisme moderne, c’est du logiciel.
Les shaders programmables n’ont pas seulement apporté des effets jolis. Ils ont déplacé le rendu d’un ensemble fixe d’astuces matérielles vers un modèle d’exécution généraliste
avec compilateurs, caches, chaînes d’outils et modes de panne qui ressemblent étonnamment à ceux de tout autre système de production. Si vous traitez les shaders comme des « assets artistiques »,
vous finirez par avoir un incident. Si vous les traitez comme du code qui s’exécute sur une plateforme distribuée, définie par le fournisseur et compilée à la volée, vous pouvez conserver
vos budgets de temps de frame et votre santé mentale.
Le point d’inflexion : du fixed‑function au programmable
Les GPU fixed‑function étaient des appareils dédiés. Vous leur donniez des sommets et des textures ; ils appliquaient une séquence connue de transformations, d’éclairage et de mélange.
Vous pouviez choisir des options (brouillard activé/désactivé, quelques stages de texture, un modèle d’éclairage), mais vous ne pouviez pas réécrire le pipeline. Cette époque apportait
une certaine stabilité : si le rendu était incorrect, c’était probablement vos maths ou vos assets, pas votre « programme ».
Les shaders programmables ont transformé le pipeline en environnement d’exécution. Au lieu de choisir dans un menu, vous écrivez des programmes — d’abord pour les sommets,
puis pour les fragments/pixels, puis pour la géométrie, la tessellation, le compute, les mesh shaders, et diverses variantes modernes. Le GPU est devenu une machine massivement parallèle
qui exécute votre code avec des contraintes qui ressemblent à un croisement entre systèmes embarqués et informatique distribuée : différents fournisseurs, différents
compilateurs, comportements subtils non définis, et des falaises de performance qui peuvent apparaître ou disparaître avec une mise à jour de pilote.
Le changement culturel importe autant que le changement technique. Une fois que les shaders sont devenus du code, le « graphisme » a cessé d’être un problème purement artistique
et est devenu un problème d’ingénierie opérationnelle. Vous avez maintenant :
- Systèmes de build qui compilent les shaders (souvent plusieurs fois, pour plusieurs cibles).
- Caches qui stockent des variantes compilées et peuvent devenir obsolètes, corrompus ou gigantesques.
- Chemins de compilation au runtime qui provoquent des saccades, glitches ou timeouts.
- Comportements et bugs spécifiques aux fournisseurs que vous devez atténuer sans réécrire le monde.
- Budgets de performance qui se comportent comme des SLO : une cible de temps de frame manquée est une panne visible par l’utilisateur.
Voici la vérité opérationnelle : si vous ne pouvez pas expliquer où passe votre temps de frame, vous ne contrôlez pas votre produit. Les shaders programmables vous donnent
les leviers pour le contrôler — mais aussi suffisamment de corde pour tisser un hamac en macramé et ensuite en tomber.
Le pipeline shader, vu comme un SRE
Pensez chaque stage de shader comme un service avec un budget de latence
Dans les systèmes de production, vous allouez des budgets : temps CPU, E/S, mémoire, profondeur de file. En rendu, votre « requête » est une frame, et votre SLO est un temps de frame stable
(par exemple 16,6 ms pour 60 Hz, 8,3 ms pour 120 Hz). Chaque stage du pipeline consomme du budget : traitement des sommets, rastérisation, shading de fragments, mélange,
post‑traitement, présentation. Lorsque vous ajoutez des stages programmables, vous ajoutez des services avec du code que vous pouvez changer fréquemment — et chaque changement est une régression
potentielle.
Le temps de frame n’est pas un nombre unique ; c’est un chemin critique. Votre file GPU peut être bloquée par un shader qui s’exécute longtemps, un overdraw excessif, une passe gourmande en bande passante,
ou des points de synchronisation (barrières, readbacks, attente de fences). « GPU bound » est une réponse comme « la base de données est lente » : techniquement vraie, opérationnellement inutile.
Les shaders sont des artefacts compilés avec un risque de déploiement
Les shaders ne s’exécutent pas en tant que code source de haut niveau. Ils sont compilés en représentations intermédiaires puis en code machine. Selon l’API et la plateforme, la compilation peut être :
- Hors ligne (ahead‑of‑time), intégrée dans la build.
- En ligne (JIT), compilée à l’installation ou au premier usage.
- Hybride, où vous expédiez un intermédiaire (comme SPIR‑V) mais les pilotes l’optimisent et le transforment encore.
Chacun de ces modèles a un coût ops. La compilation hors ligne déplace les échecs vers la CI et réduit les saccades au runtime, mais augmente la complexité de build et le nombre d’artefacts.
Le JIT réduit la taille du build et peut utiliser le meilleur compilateur du pilote, mais introduit des saccades au premier usage et fait apparaître des échecs sur la machine du client,
où vous avez la pire observabilité.
Permutations de shaders : le problème de systèmes distribués que vous n’avez pas demandé
Un seul « shader » est rarement un seul programme. Les moteurs réels génèrent des permutations basées sur :
- Fonctionnalités du matériau (normal map, clear coat, subsurface, emissive, etc.).
- Chemins d’éclairage (forward vs deferred, qualité des ombres, nombre de lumières).
- Capacités de la plateforme (précision, opérations wave, formats de texture).
- Options du pipeline de rendu (MSAA, HDR, VR, variantes de temporal AA).
Multipliez le tout et vous obtenez des milliers de variantes. Si vous ne gérez pas activement les permutations, vos temps de compilation explosent, vos caches se déréglent, et vous livrez
une build « correcte sur les machines dev » mais qui saccade pour les joueurs parce que le cache shader est froid. C’est l’équivalent graphique de déployer une architecture microservices parce que
vous vouliez juste un nouveau bouton sur la page d’accueil.
Une citation à garder sur un pense‑bête
L’espoir n’est pas une stratégie.
— General Gordon R. Sullivan
Les shaders récompensent l’ingénierie basée sur l’espoir : « Le compilateur l’optimisera », « Le pilote le mettra en cache », « Ça doit être OK ». Ça marche jusqu’au moment où ça ne marche plus,
et alors votre canal d’incident se remplit de captures d’écran d’éclairages fondus et de graphiques de temps de frame en forme de montagnes.
Faits et contexte historique intéressants (court et concret)
- Le fixed‑function a duré plus longtemps qu’on ne s’en souvient. Les premiers GPU grand public proposaient un pipeline que l’on pouvait configurer mais pas réécrire ; la créativité venait à force de contournements.
- Les vertex shaders programmables sont arrivés avant les pixel shaders programmables. Transform & lighting ont été la première grande victoire « programmable » parce qu’ils correspondaient aux forces du matériel.
- Les premiers pixel shaders étaient fortement contraints. Les comptes d’instructions et la pression sur les registres étaient des limites strictes ; on apprenait à compter les ops comme les ingénieurs stockage comptent les IOPS.
- Les langages shader n’étaient pas juste une commodité. Ils visaient la portabilité et les outils — se libérer de l’assembleur propre à chaque fournisseur pour entrer dans un format compréhensible par les compilateurs.
- Les architectures « unified shader » ont changé l’ordonnancement. Quand les GPU sont passés d’unités vertex/pixel séparées à des cœurs unifiés, la performance a cessé d’être une simple histoire « lié aux vertex vs lié aux pixels ».
- Les compute shaders ont estompé la frontière entre graphisme et GPGPU. Une fois qu’on a un stage compute général, on commence à faire du culling, du travail de physique et des post‑effets en tant que charges compute.
- La compilation shader est entrée dans la pile pilote. Cela a fait des mises à jour une variable de performance, ce qui est une manière polie de dire : votre build peut devenir plus lente sans que vous changiez de code.
- Les représentations intermédiaires sont devenues une stratégie. Expédier quelque chose comme SPIR‑V vise à standardiser les entrées, mais les pilotes ont toujours le dernier mot sur le code machine final.
- Les pipelines modernes ajoutent de nouveaux stages programmables. Tessellation, mesh shaders, ray tracing shaders — chacun apporte du pouvoir et de nouveaux modes de panne.
Ce qui casse en vrai : modes de panne prévisibles
1) Des saccades de compilation déguisées en « latence réseau »
Classique : un joueur tourne un coin et voit un nouvel effet, et le jeu saccade. Le réseau est blâmé. Les serveurs sont blâmés. Quelqu’un ouvre un ticket contre le matchmaking. Pendant ce temps,
le vrai problème est qu’une variante de shader a été compilée au premier usage sur le client. Si vous ne pré‑chauffez pas les caches ou n’expédiez pas d’artefacts compilés, vous externalisez
la latence au pire moment possible : quand l’utilisateur interagit activement.
2) Explosions de permutations qui font silencieusement un DoS de votre build et de votre cache
Les fonctionnalités shader sont addictives. Une define de plus, une branche de plus, un toggle de qualité de plus. Vous « ajoutez juste ça » jusqu’à atteindre des dizaines de milliers de variantes.
Ensuite, vos temps de CI doublent, les artistes arrêtent d’itérer parce que les builds sont lents, et votre cache runtime devient une décharge. Les permutations ne sont pas gratuites ; c’est un
problème de planification de capacité.
3) Bugs de précision : « ça marche sur ma GPU » avec des maths au ras des pâquerettes
Différents GPU et pilotes diffèrent dans le comportement en virgule flottante, le traitement des denormales, les opérations fusionnées et les valeurs par défaut de précision. Si votre shader
dépend d’un comportement numérique limite — surtout en half precision — vous pouvez obtenir du banding, du scintillement, des NaN ou des écrans noirs sur un sous‑ensemble de matériel.
4) Overdraw et bande passante : les tueurs silencieux
L’ALU du shader attire toute l’attention parce que ça ressemble à du « code ». Mais beaucoup de goulots réels sont de la bande passante (fetchs de textures, écritures de targets) et de l’overdraw
(shader des pixels qui seront réécrits). Vous pouvez écrire le BRDF le plus mignon du monde et vous faire battre par une passe plein écran qui lit quatre textures en 4K et écrit deux targets HDR.
Votre GPU n’est pas philosophe ; c’est un chariot élévateur.
5) Erreurs de synchronisation : des bulles GPU que vous avez vous‑même créées
Barrières, transitions de ressources et readbacks peuvent sérialiser le travail. Le résultat est un GPU « occupé » mais peu productif. C’est l’équivalent rendu d’une charge stockage qui passe
la moitié de son temps à attendre des flush parce que quelqu’un a mis fsync dans une boucle chaude.
6) Régressions des compilateurs pilotes : votre code stable, leur backend changeant
Les pilotes évoluent. Les compilateurs shader évoluent. Le même shader de haut niveau peut se compiler en code machine différent après une mise à jour. Parfois c’est plus rapide.
Parfois c’est plus lent. Parfois ça se compile mal. C’est pourquoi le déploiement des shaders nécessite de l’observabilité et des garde‑fous, pas de l’intuition.
Blague #1 : Un compilateur de shader est comme un chat — s’il aime votre code, il cassera quand même quelque chose juste pour vous regarder réagir.
Playbook de diagnostic rapide (quoi vérifier en priorité)
Premier : déterminer si vous êtes CPU‑bound, GPU‑bound ou sync‑bound
- Vérifiez l’utilisation et les fréquences GPU. Une utilisation élevée du GPU avec des fréquences stables suggère un lien GPU ; une utilisation faible avec des saccades suggère des stalls de synchro ou une famine CPU.
- Vérifiez la répartition du temps de frame. Si le temps CPU est faible mais que le temps GPU est élevé, vous êtes GPU‑bound. Si les deux montent, cherchez des stalls et des synchronisations.
- Recherchez des pics périodiques. Des pics réguliers (toutes les quelques secondes) indiquent souvent une compilation shader, du streaming d’assets ou du garbage collection.
Second : classer le goulot GPU
- ALU‑bound : maths lourdes, éclairage complexe, trop d’instructions, branches divergentes.
- Bande passante / textures : nombreuses lectures de textures, cache misses, grands render targets, passes en haute résolution.
- Overdraw : nombreuses couches transparentes, particules, effets plein écran, pixels shader plusieurs fois.
- Goulot fixed‑function : rastérisation, blending, résolutions MSAA, complexité de profondeur, saturation des ROP.
Troisième : isoler la passe ou le matériau en cause
- Capturez une frame GPU et triez les draws par coût (temps ou samples).
- Désactivez les passes systématiquement : ombres, SSAO, bloom, réflexions, volumétriques.
- Forcez un matériau « plat » pour voir si le problème est lié au contenu (matériaux) ou au pipeline (post, éclairage, resolves).
Quatrième : confirmer le comportement de compilation et de cache
- Vérifiez les taux de hits du cache shader (logs du moteur, répertoires de cache du pilote, stats du pipeline cache).
- Recherchez les événements de compilation au runtime au premier usage.
- Vérifiez que votre pipeline cache est correctement versionné et n’est pas invalidé à chaque build.
Cinquième : régressions et sécurité de déploiement
- Bisectez les changements de shader par commit ou par feature flag.
- Validez sur plusieurs fournisseurs et plusieurs versions de pilotes.
- Déployez avec des garde‑fous : toggles, modes sûrs et télémétrie.
Tâches pratiques : commandes, sorties et décisions (12+)
Voici le type de commandes que vous exécutez quand vous déboguez des problèmes liés aux shaders sur des postes Linux ou des bancs de test.
L’objectif n’est pas de se faire passer pour un ingénieur pilote. L’objectif est de prendre rapidement une décision : CPU vs GPU, compilation vs runtime, santé du cache,
et où instrumenter ensuite.
Task 1: Identify the GPU and driver in use
cr0x@server:~$ lspci -nn | grep -E "VGA|3D"
01:00.0 VGA compatible controller [0300]: NVIDIA Corporation GA104 [GeForce RTX 3070] [10de:2484] (rev a1)
Ce que ça signifie : Vous connaissez le fournisseur/le device. C’est votre première clé de partition pour « seulement sur certaines machines ».
Décision : Reproduisez sur au moins un autre fournisseur si possible ; ne faites pas confiance à une seule famille GPU pour représenter le « PC ».
Task 2: Confirm the loaded kernel driver module
cr0x@server:~$ lsmod | grep -E "amdgpu|i915|nvidia"
nvidia_drm 73728 4
nvidia_modeset 1236992 8 nvidia_drm
nvidia 59387904 457 nvidia_modeset
Ce que ça signifie : La pile de pilotes est active ; utile pour confirmer que vous ne vous exécutez pas accidentellement sur une solution de repli.
Décision : Si vous attendez un pilote (ex. amdgpu) et que vous en voyez un autre ou aucun, arrêtez‑vous : vos données de test sont invalides.
Task 3: Check OpenGL renderer and version (easy sanity check)
cr0x@server:~$ glxinfo -B | sed -n '1,20p'
name of display: :0
display: :0 screen: 0
direct rendering: Yes
Extended renderer info (GLX_MESA_query_renderer):
Vendor: NVIDIA Corporation (0x10de)
Device: NVIDIA GeForce RTX 3070/PCIe/SSE2 (0x2484)
Version: 535.154.05
OpenGL vendor string: NVIDIA Corporation
OpenGL renderer string: NVIDIA GeForce RTX 3070/PCIe/SSE2
OpenGL core profile version string: 4.6.0 NVIDIA 535.154.05
Ce que ça signifie : Confirme le rendu direct et la version du pilote user‑space. Si c’est incorrect, tout le reste est du bruit.
Décision : Enregistrez ceci dans votre template de rapport de bug. Si vous ne pouvez pas reproduire avec la même chaîne renderer, traitez‑le comme un incident différent.
Task 4: Check Vulkan device and driver (if your engine uses Vulkan)
cr0x@server:~$ vulkaninfo --summary | sed -n '1,80p'
Vulkan Instance Version: 1.3.280
Devices:
========
GPU0:
apiVersion = 1.3.280
driverVersion = 535.154.5
vendorID = 0x10de
deviceID = 0x2484
deviceType = DISCRETE_GPU
deviceName = NVIDIA GeForce RTX 3070
Ce que ça signifie : Confirme le chemin Vulkan et la version du pilote ; crucial pour le comportement du pipeline cache et les toolchains SPIR‑V.
Décision : Si une régression corrèle avec des changements de driverVersion, reproduisez sur une version plus ancienne/plus récente avant de réécrire les shaders.
Task 5: Watch GPU utilization live (is it actually busy?)
cr0x@server:~$ nvidia-smi dmon -s u -d 1
# gpu sm mem enc dec mclk pclk
# Idx % % % % MHz MHz
0 97 61 0 0 7001 1905
0 98 62 0 0 7001 1905
Ce que ça signifie : Un SM% élevé suggère un shader/compute lourd. Un mem% élevé suggère une pression bande passante.
Décision : Si SM% est bas mais que le temps de frame est élevé, suspectez des stalls de synchronisation, un goulot CPU, ou une attente au niveau pilote.
Task 6: Identify which process is using the GPU
cr0x@server:~$ nvidia-smi --query-compute-apps=pid,process_name,used_gpu_memory --format=csv
pid, process_name, used_gpu_memory [MiB]
23144, game-client, 6123
Ce que ça signifie : Confirme que le bon binaire est mesuré ; aide à éviter l’erreur « ah merde, je mesurais le launcher ».
Décision : Si plusieurs processus se disputent le GPU, isolez‑les : les chiffres de performance ne sont pas comparables sous contention.
Task 7: Inspect shader cache directories (size and churn)
cr0x@server:~$ du -sh ~/.cache/*shader* 2>/dev/null | sort -h
128M /home/cr0x/.cache/nvidia/GLCache
1.9G /home/cr0x/.cache/mesa_shader_cache
Ce que ça signifie : Des caches volumineux peuvent être normaux, mais une croissance soudaine après un patch suggère un gonflement des permutations ou une invalidation.
Décision : Si le cache croît de façon dramatique par build, versionnez correctement votre pipeline cache et réduisez les permutations.
Task 8: Check whether your app is recompiling shaders at runtime (log grep)
cr0x@server:~$ grep -E "Compiling shader|Pipeline cache miss|PSO compile" -n /var/log/game-client.log | tail -n 8
18422 Compiling shader variant: Material=Water, Perm=HDR+SSR+Foam
18423 PSO compile: vkCreateGraphicsPipelines took 47 ms
18424 Pipeline cache miss: key=0x6f2a...
Ce que ça signifie : Preuve de compilation au runtime et de création coûteuse de pipeline. Ces 47 ms, c’est une saccade perceptible.
Décision : Ajoutez des étapes de précompilation/préchauffage pour les permutations chaudes connues ; évitez de créer des pipelines sur le thread de rendu pendant le jeu.
Task 9: Monitor CPU frequency and throttling (stutters that look like GPU issues)
cr0x@server:~$ sudo turbostat --Summary --quiet --interval 1 | head -n 5
Avg_MHz Busy% Bzy_MHz TSC_MHz IRQ
4120 38.5 4710 2800 21430
1180 12.1 2870 2800 9050
Ce que ça signifie : Si Avg_MHz s’effondre pendant les saccades, votre « régression GPU » peut être de la gestion d’énergie CPU ou des limites thermiques.
Décision : Retestez avec le governor performance, vérifiez le refroidissement et retirez la throttling CPU de l’équation expérimentale.
Task 10: Check present mode / compositor interference (frame pacing problems)
cr0x@server:~$ echo $XDG_SESSION_TYPE
wayland
Ce que ça signifie : Les différences Wayland/X11 peuvent modifier le pacing des frames et le comportement des outils de capture.
Décision : Si le pacing est incohérent seulement sous un compositeur, testez en session plein écran dédiée ou dans un autre type de session.
Task 11: Track GPU memory pressure (evictions cause spikes)
cr0x@server:~$ nvidia-smi --query-gpu=memory.total,memory.used,memory.free --format=csv
memory.total [MiB], memory.used [MiB], memory.free [MiB]
8192 MiB, 7940 MiB, 252 MiB
Ce que ça signifie : Vous êtes proche du seuil. Quand la VRAM est serrée, le pilote peut paginer ou évincer des ressources, produisant des saccades et des coûts imprévisibles.
Décision : Réduisez la taille des render targets, limitez la résidence des textures, coupez les pipelines à permutations lourdes, ou implémentez des limites de streaming.
Task 12: Confirm the engine is using the expected shader backend (config check)
cr0x@server:~$ grep -E "rhi=|renderer=|shader_backend=" -n /etc/game-client.conf
12 renderer=vulkan
13 shader_backend=spirv
Ce que ça signifie : Un backend inadéquat (fallback OpenGL, chemin de compilateur différent) change les performances et la correction.
Décision : Si un bug n’apparaît que sur un backend, vous avez isolé le rayon d’impact et pouvez livrer une mitigation ciblée.
Task 13: Measure per-process CPU time and context switches (sync-bound clue)
cr0x@server:~$ pidstat -w -p $(pgrep -n game-client) 1 3
Linux 6.5.0 (server) 01/13/2026 _x86_64_ (16 CPU)
12:14:01 UID PID cswch/s nvcswch/s Command
12:14:02 1000 23144 1250.00 210.00 game-client
12:14:03 1000 23144 1180.00 190.00 game-client
Ce que ça signifie : Des context switches involontaires élevés peuvent indiquer contention, attentes bloquantes ou synchronisation du pilote.
Décision : Si nvcswch/s monte pendant les saccades, inspectez les points de synchronisation et les threads de compilation en arrière‑plan.
Task 14: Detect shader-related crashes via kernel logs (GPU reset / hang)
cr0x@server:~$ sudo dmesg -T | tail -n 12
[Mon Jan 13 12:22:09 2026] NVRM: Xid (PCI:0000:01:00): 13, Graphics Exception: Shader Program Error
[Mon Jan 13 12:22:09 2026] NVRM: Xid (PCI:0000:01:00): 31, Ch 0000007b, engmask 00000101, intr 10000000
Ce que ça signifie : Le GPU a signalé une faute cohérente avec un problème de shader/programmation ou un bug pilote. Ce n’est pas « juste un crash ».
Décision : Reproduisez avec des couches de validation / builds debug et simplifiez le shader ; testez aussi des versions alternatives de pilotes.
Task 15: Compare shader artifact counts between builds (permutation control)
cr0x@server:~$ find /opt/game/shaders/ -type f -name "*.spv" | wc -l
18422
Ce que ça signifie : Un saut de comptage entre builds est un fort indicateur d’explosion de permutations.
Décision : Bloquez les merges qui augmentent le nombre d’artefacts shader au‑delà d’un seuil ; exigez une justification et un test de perf.
Trois micro‑histoires d’entreprise depuis le pays du « ça marchait sur ma GPU »
Mini‑histoire 1 : L’incident causé par une mauvaise hypothèse
Une équipe produit de taille moyenne a livré une nouvelle passe d’éclairage « premium ». Elle était contrôlée par un simple toggle : High/Ultra l’activait, Medium le désactivait.
QA avait validé le toggle. Les performances semblaient acceptables. Tout le monde est passé à autre chose.
L’incident a commencé un lundi après qu’une mise à jour pilote routinière a été déployée sur les machines d’entreprise. Soudain, les canaux de support ont été saturés de rapports :
« Écrans noirs aléatoires après alt‑tab », « UI qui scintille », « seulement sur les laptops », « seulement parfois ». L’ingénierie a fait la danse habituelle — réinstaller, vider les caches,
blâmer Windows, blâmer le compositeur, blâmer la phase de la lune.
La mauvaise hypothèse : ils croyaient que « si ça compile, ça tourne ». En réalité, la nouvelle passe incluait une variante de shader qui compilait correctement mais déclenchait
un comportement indéfini sur une combinaison pilote/compilateur spécifique lorsqu’une define rarement utilisée était activée (ça n’arrivait que lorsque l’overlay UI était actif et que l’HDR était activé).
Ils n’avaient pas testé cette permutation parce qu’elle ne faisait pas partie de leurs scènes de test « standard ».
La correction n’a pas été glamour. Ils ont créé une matrice de permutations pour la couverture de tests, ajouté des vérifications de validation runtime (gardes NaN en debug), et construit
une petite suite de scènes « bizarres mais réelles » qui activaient overlays, échelles de résolution étranges et combinaisons HDR. Ils ont aussi ajouté une soupape de sécurité : si la passe échoue
à la validation ou déclenche des erreurs GPU répétées, elle se désactive automatiquement et génère une empreinte.
La leçon : la compilation est l’admission dans l’immeuble, pas la preuve que vous n’allumerez pas la cuisine.
Mini‑histoire 2 : L’optimisation qui a tourné au fiasco
Une autre équipe chassait un pic de coût GPU dans une scène à forte végétation. Le profiling montrait que le fragment shading était coûteux, et un ingénieur senior a proposé une optimisation :
empaqueter plusieurs paramètres de matériau dans moins de textures et utiliser des calculs en half precision. Moins de bande passante, moins de registres, shading plus rapide. Sur le papier,
c’était le genre de changement dont on se vanterait dans une revue performance.
Le changement a été livré derrière un flag et avait l’air excellent sur les GPU phare des devs. Le temps de frame s’est amélioré modestement. Puis c’est arrivé sur un parc plus large et les bizarreries
ont commencé : reflets scintillants, TAA instable, pixels noirs occasionnels en mouvement. Les performances ont aussi empiré sur certaines cartes AMD.
Le retour de manivelle était double. D’abord, la half precision a réduit la stabilité numérique dans leur chemin de reconstruction normal. Des valeurs « assez bonnes » sont devenues « assez mauvaises pour casser l’historique TAA ».
Ensuite, le schéma d’empaquetage a augmenté la divergence des échantillonnages de texture : lectures dépendantes plus nombreuses, accès moins friendly au cache, et latences plus élevées sur des architectures spécifiques.
Le shader est devenu « plus petit » mais moins cohérent.
Ils l’ont rollbacké, puis réintroduit avec des garde‑fous : garder la pleine précision dans les parties qui alimentent la reprojection temporelle ; n’utiliser la half precision que dans les lobes moins sensibles ;
et vérifier les performances par fournisseur. Ils ont aussi ajouté un test de correction visuelle qui comparait les frames sur un chemin caméra déterministe, parce que « ça a l’air OK » subjectivement n’est pas un test.
Leçon : optimiser des shaders, c’est comme ajuster un index de base de données — on peut gagner beaucoup, mais aussi optimiser pour le benchmark et pénaliser la réalité.
Mini‑histoire 3 : La pratique ennuyeuse mais correcte qui a sauvé la mise
Une équipe plateforme avait une politique qui semblait douloureusement conservatrice : chaque changement de shader devait inclure un petit fichier metadata décrivant l’impact attendu sur le nombre de permutations,
et chaque build produisait un manifeste diffable des artefacts shader. Les gens se plaignaient. Ça avait l’air bureaucratique.
Un cycle de release, une fonctionnalité apparemment inoffensive a ajouté un nouveau mot‑clé matériau. Les artistes ont commencé à l’utiliser partout, parce que ça améliorait le rendu.
Le mot‑clé interagissait avec trois autres toggles, et le nombre de permutations a silencieusement explosé. Les temps de build ont commencé à grimper. Les rapports de saccades runtime ont augmenté,
mais assez lentement pour qu’aucun incident unique n’apparaisse.
La pratique ennuyeuse a joué son rôle : le diff du manifeste shader a montré une augmentation significative des variantes compilées liée à ce mot‑clé. Parce que c’était tracé comme artefact de première classe,
l’équipe plateforme a pu le signaler sans débat sur les impressions. Ils ont mis le déploiement en pause, refactorisé le mot‑clé en branche runtime où il était sûr, et introduit une politique de tiering :
si un mot‑clé crée trop de variantes, il doit être restreint à certains matériaux ou placé derrière un niveau de qualité.
Cela a évité un incident plus grave : une scène marketing de dernière minute aurait forcé la compilation à froid de milliers de variantes au premier lancement. À la place,
la build a été livrée avec des permutations contrôlées et des entrées de cache préchauffées pour le chemin de démo connu. Personne en dehors de l’ingénierie n’a rien remarqué — signe de succès.
Blague #2 : Le shader le plus fiable est celui qui n’existe pas, ce qui est aussi ma méthode pour les réunions.
Erreurs courantes : symptôme → cause racine → correctif
Stutter à la première apparition d’un effet
Symptôme : Pics de temps de frame quand un nouveau matériau/effet apparaît ; ensuite fluide.
Cause racine : Compilation runtime de shader ou création de pipeline (cache froid), souvent sur le thread de rendu.
Correctif : Précompiler les permutations connues ; préchauffer les caches pendant le chargement ; déplacer la création de pipeline vers une compilation asynchrone avec fallback ; expédier des pipeline caches versionnés.
Régression de perf seulement sur un fournisseur GPU
Symptôme : NVIDIA OK, AMD chute (ou vice versa) après un changement shader.
Cause racine : Sensibilité spécifique à l’architecture : branches divergentes, pression sur les registres, motifs d’accès aux textures, ou régression du compilateur pilote.
Correctif : Profiler par fournisseur ; réduire la divergence ; simplifier les lectures dépendantes de textures ; essayer d’autres formes de code ; maintenir des contournements spécifiques par fournisseur uniquement quand mesurés.
Pixels scintillants ou étincelants en mouvement
Symptôme : Instabilité temporelle, « fireflies », ou scintillement pire lors des mouvements de caméra.
Cause racine : Problèmes de précision (half floats), NaN/Inf, normales instables, ou maths non déterministes alimentant les filtres temporels.
Correctif : Ajouter des clamps NaN en debug ; conserver la pleine précision sur les chemins critiques ; stabiliser les entrées (normaliser en sécurité) ; auditer divisions et racines carrées ; ajouter un epsilon si nécessaire.
Banding dans les dégradés ou l’éclairage
Symptôme : Des dégradés lisses deviennent des marches, surtout dans le brouillard, le ciel ou les zones peu éclairées.
Cause racine : Stockage ou maths en basse précision (buffers 8 bits, half precision), ou absence de dithering.
Correctif : Utiliser des formats de plus haute précision où nécessaire ; appliquer du dithering ; éviter de quantifier trop tôt l’éclairage intermédiaire.
Utilisation GPU faible mais temps de frame élevé
Symptôme : Le GPU semble sous‑utilisé, pourtant les frames sont lentes ou incohérentes.
Cause racine : Stalls de synchronisation : attente de fences GPU/CPU, readbacks, sérialisation due aux barrières, ou le CPU qui n’alimente pas le GPU.
Correctif : Retirer les readbacks des chemins chauds ; double/triple buffering ; déplacer le travail hors du thread principal ; réduire les barrières inutiles ; mesurer les temps de soumission et d’attente des queues.
Les particules transparentes écrasent les performances de façon inattendue
Symptôme : Pics de temps de frame avec de la fumée, UI ou scènes à forte densité de particules.
Cause racine : Overdraw et shaders fragment coûteux ; le blending empêche les optimisations early‑z.
Correctif : Réduire le nombre de couches de particules ; trier et batcher ; utiliser des shaders moins coûteux pour les particules lointaines ; ajouter une passe de profondeur préliminaire ou une profondeur approximative ; envisager les masques plutôt que le blending si acceptable.
Temps de build explosent après « encore une fonctionnalité »
Symptôme : La compilation shader en CI passe de quelques minutes à « va prendre le temps du déjeuner ».
Cause racine : Explosion de permutations due aux combinaisons de fonctionnalités et mots‑clé matériaux.
Correctif : Suivre les comptes de permutations ; plafonner les mots‑clé ; consolider les fonctionnalités ; déplacer certains toggles vers des branches runtime ; précompiler le code partagé ; exiger une justification perf/compile pour les nouvelles defines.
Crash ou reset GPU sur des scènes spécifiques
Symptôme : Reset pilote, device lost, ou entrées kernel log référant des erreurs shader.
Cause racine : Accès ressource invalide, comportement indéfini, shaders trop longs déclenchant des watchdogs, ou bugs pilote atteints par des motifs de code spécifiques.
Correctif : Utiliser des couches de validation ; simplifier le shader ; éviter les index hors limites ; réduire la complexité des boucles ; ajouter des vérifications robustes de bornes ; tester d’autres pilotes et désactiver la fonctionnalité comme mitigation.
Checklists / plan étape par étape pour une livraison fiable des shaders
1) Traitez les shaders comme du code avec des gates CI
- Compiler en CI pour toutes les cibles que vous livrez. Faites échouer les builds sur des warnings que vous comprenez, pas sur des warnings ignorés.
- Exporter un manifeste d’artefacts shader. Compter variantes, tailles et hashes ; diff par commit.
- Suivre le temps de compilation comme métrique. Si ça tend à la hausse, c’est de la dette technique avec intérêt.
- Exécuter un petit test de rendu déterministe. Chemin caméra fixe, seed fixe, capturer des frames ; comparer aux baselines pour les déviations importantes.
2) Contrôler les permutations intentionnellement
- Faire justifier chaque define. Si une feature crée beaucoup de variantes, poussez‑la dans une branche runtime ou restreignez‑la par tiers.
- Séparer « convenience artistique » et « réalité runtime ». La flexibilité d’authoring est excellente ; livrer 20k variants ne l’est pas.
- Versionner correctement les clés de cache. Les changements de codegen shader, d’options de compilateur ou d’état de rendu doivent invalider proprement les anciens caches — pas au hasard.
3) Préchauffer ce qui compte, pas tout
- Identifier les chemins chauds. Écran titre, première match, effets d’armes courants, chaîne post la plus utilisée.
- Précompiler ou précréer les pipelines pour ces chemins. Faites‑le pendant le chargement ou en tâche de fond avec indicateur de progression.
- Ne bloquez pas le thread de rendu sur la compilation. Si nécessaire, utilisez un shader de fallback peu coûteux et swappez quand prêt.
4) Observabilité : rendre les problèmes shader mesurables
- Loggez les événements de compilation de pipeline avec durées. Traitez >5 ms comme suspect, >20 ms comme incident dans les scènes interactives.
- Enregistrez l’empreinte GPU/driver. Vendor, device ID, version pilote, backend API, et toggles clés.
- Capturez des histogrammes de temps de frame, pas seulement des moyennes. Les utilisateurs ressentent les pics p99, pas votre FPS moyen.
- Gardez des feature flags pour les passes risquées. Si une régression pilote apparaît, vous avez besoin d’un kill switch qui ne nécessite pas une rebuild.
5) Discipline opérationnelle face au chaos des pilotes
- Maintenez une petite matrice de compatibilité. Minimum deux fournisseurs, au moins un pilote ancien et un pilote récent.
- Documentez les versions de pilotes connues comme mauvaises. Pas en folklore — liez‑les à la télémétrie et à des scènes reproductibles.
- Privilégiez des formes de code stables plutôt que des astuces intelligentes. Le compilateur GPU est puissant, mais ce n’est pas votre coéquipier.
FAQ
1) Qu’est‑ce qu’un shader programmable exactement ?
Un petit programme exécuté sur le GPU dans le cadre du rendu (ou du compute). Au lieu d’un éclairage et d’un texturing fixed‑function, vous définissez comment les sommets sont
transformés et comment les pixels sont ombrés, souvent avec accès aux textures et buffers.
2) Pourquoi les shaders programmables ont‑ils changé l’ingénierie de production ?
Parce qu’ils ont introduit compilation, mise en cache et comportements spécifiques à la plateforme dans le chemin de rendu. Vous déployez maintenant du code qui passe par des toolchains
fournisseurs que vous ne contrôlez pas, avec des risques de latence et de correction qui apparaissent au runtime.
3) Vertex vs fragment shader : quelle différence opérationnelle ?
Les vertex shaders évoluent avec le nombre de sommets ; les fragment shaders évoluent avec le nombre de pixels (et l’overdraw). Si votre problème apparaît en haute résolution ou avec beaucoup
de transparence, suspectez le coût fragment. Si ça apparaît avec de la géométrie dense quel que soit la résolution, suspectez le coût vertex ou le traitement géométrique.
4) Pourquoi les shaders saccadent‑ils la première fois que je vois un effet ?
Parce que quelque chose a compilé ou créé un pipeline state object à la demande. La première apparition déclenche la compilation, et vous payez ce coût sur le chemin critique.
La solution est le préchauffage, le caching, ou le déplacement de la compilation hors du thread de rendu avec des fallbacks.
5) Expédier du SPIR‑V (ou autre intermédiaire) équivaut‑il à expédier des « shaders compilés » ?
Pas tout à fait. Un intermédiaire peut réduire la variabilité et améliorer les outils, mais les pilotes compilent/optimisent souvent encore en code machine final. Vous devez toujours
gérer les coûts de création de pipeline et le comportement du cache.
6) Comment savoir si je suis limité par la bande passante ou par l’ALU ?
Utilisez des profileurs GPU et des compteurs quand disponibles, mais vous pouvez aussi faire des expériences simples : réduire la résolution (les coûts liés à la bande passante/fragments devraient chuter),
réduire les lectures de textures, ou simplifier les maths. Si la mise à l’échelle de la résolution change à peine les performances, vous pouvez être limité par les vertex/CPU/synchronisation.
7) Les shaders « sans branche » sont‑ils toujours plus rapides ?
Non. Supprimer des branches peut augmenter le nombre d’instructions et la pression sur les registres, ce qui peut réduire l’occupation et nuire aux performances. Le bon choix dépend de l’architecture
et doit être mesuré sur des GPU représentatifs.
8) Devons‑nous toujours utiliser la half precision pour la vitesse ?
Seulement là où c’est sûr. La half precision peut être excellente pour certaines valeurs intermédiaires, mais elle peut aussi introduire des instabilités dans les systèmes d’éclairage et temporels.
Utilisez‑la de façon chirurgicale, pas systématique, et testez à la fois la performance et la correction.
9) Quelle est l’erreur de déploiement de shader la plus commune en entreprise ?
Traiter les changements de shader comme des « mises à jour de contenu » plutôt que comme des déploiements de code : pas de gates CI, pas de diff de manifeste, pas de versionnage de cache discipliné,
et pas de télémétrie pour les événements de compilation runtime.
10) Si les pilotes peuvent changer les performances, l’optimisation est‑elle inutile ?
L’optimisation reste importante, mais elle doit être liée à la mesure et protégée par des tests de régression. De plus, des formes de code stables et des motifs d’accès prévisibles
survivent généralement mieux aux variations des pilotes que des astuces trop « intelligentes ».
Prochaines étapes pratiques
Si vous êtes responsable d’une pile de rendu en production — jeu, visualisation, UI, tout ce qui est GPU‑intensif — traitez les shaders comme le code critique qu’ils sont.
Pas parce que c’est intellectuellement satisfaisant, mais parce que ça réduit les incidents.
- Ajoutez un manifeste d’artefacts shader à votre build. Comptez les variantes et differez‑les par changement. Détectez tôt les explosions de permutations.
- Instrumentez la compilation runtime et la création de pipeline. Loggez durée, stage et clé shader ; alertez sur les pics p95/p99 du temps de frame après les releases.
- Établissez un plan de « préchauffage » des chemins chauds. Identifiez les 5 premières minutes de comportement utilisateur typique et assurez‑vous que les shaders/pipelines pertinents sont prêts.
- Construisez une matrice de compatibilité minimale. Plusieurs fournisseurs, plusieurs versions de pilotes, et une scène de test déterministe difficile à biaiser.
- Créez des kill switches pour les passes risquées. Si une régression pilote survient, vous voulez une mitigation aujourd’hui, pas après une rebuild et une recertification.
Les shaders programmables sont l’une des meilleures choses arrivées au graphisme. Ils rappellent aussi que le « graphisme » n’est pas un domaine isolé.
C’est du calcul, de la compilation, du caching et des budgets de latence — juste avec des captures d’écran plus jolies quand tout est bien fait.