Conseils de profilage des performances pour les développeurs de jeux
Des performances fluides sont essentielles pour créer des expériences de jeu immersives pour les joueurs. En profilant et en affinant les performances de votre jeu pour un large éventail de plates-formes et d'appareils, vous pouvez élargir votre base de joueurs et augmenter vos chances de succès.
Cette page décrit un flux de travail de profilage général pour les développeurs de jeux. Il est extrait du livre électronique, Guide ultime pour profiler les jeux Unity,disponible en téléchargement gratuit. Le livre électronique a été créé par des experts Unity externes et internes en développement, profilage et optimisation de jeux.
Poursuivez votre lecture pour en savoir plus sur les objectifs utiles à définir avec le profilage, les goulots d'étranglement courants en matière de performances, tels que le fait d'être lié au CPU ou au GPU, et comment identifier et étudier ces situations plus en détail.
Mesurer la fréquence d'images de votre jeu en images par seconde (fps) n'est pas idéal pour offrir des expériences cohérentes à vos joueurs. Considérez le scénario simplifié suivant :
Pendant l'exécution, votre jeu restitue 59 images en 0,75 seconde. Cependant, le rendu de l’image suivante prend 0,25 seconde. La fréquence d'images moyenne délivrée de 60 ips semble bonne, mais en réalité, les joueurs remarqueront un effet de bégaiement puisque le rendu de la dernière image prend un quart de seconde.
C'est l'une des raisons pour lesquelles il est important de viser un budget temps spécifique par image. Cela vous donne un objectif solide à atteindre lors du profilage et de l’optimisation de votre jeu et, en fin de compte, cela crée une expérience plus fluide et plus cohérente pour vos joueurs.
Chaque image aura un budget de temps basé sur vos fps cibles. Une application ciblant 30 ips devrait toujours prendre moins de 33,33 ms par image (1 000 ms / 30 ips). De même, un objectif de 60 fps laisse 16,66 ms par image (1000 ms / 60 fps).
Vous pouvez dépasser ce budget lors de séquences non interactives, par exemple lors de l'affichage des menus de l'interface utilisateur ou du chargement de scènes, mais pas pendant le jeu. Même une seule image dépassant le budget d’image cible entraînera des problèmes.
remarque Une fréquence d’images constamment élevée dans les jeux VR est essentielle pour éviter de provoquer des nausées ou de l’inconfort chez les joueurs. Sans cela, vous risquez d'être rejeté par le détenteur de la plateforme lors de la certification de votre jeu.
Images par seconde: Une métrique trompeuse
Les joueurs mesurent couramment leurs performances à l'aide de la fréquence d'images ou d'images par seconde. Cependant, il est recommandé d’utiliser plutôt la durée d’image en millisecondes. Pour comprendre pourquoi, regardez le graphique ci-dessus des images par seconde en fonction du temps d'image.
Considérez ces chiffres :
1 000 ms/sec / 900 ips = 1,111 ms par image
1 000 ms/sec / 450 ips = 2,222 ms par image
1 000 ms/sec / 60 ips = 16,666 ms par image
1 000 ms/sec / 56,25 ips = 17,777 ms par image
Si votre application s'exécute à 900 ips, cela se traduit par un temps d'image de 1,111 millisecondes par image. À 450 ips, cela représente 2,222 millisecondes par image. Cela représente une différence de seulement 1,111 millisecondes par image, même si la fréquence d'images semble diminuer de moitié.
Si vous regardez les différences entre 60 ips et 56,25 ips, cela se traduit respectivement par 16,666 millisecondes par image et 17,777 millisecondes par image. Cela représente également 1,111 millisecondes supplémentaires par image, mais ici, la baisse de la fréquence d'images semble beaucoup moins dramatique en termes de pourcentage.
C'est pourquoi les développeurs utilisent le temps d'image moyen pour évaluer la vitesse du jeu plutôt que les fps.
Ne vous inquiétez pas des fps, sauf si vous descendez en dessous de votre fréquence d'images cible. Concentrez-vous sur le temps d'image pour mesurer la vitesse d'exécution de votre jeu, puis respectez votre budget d'images.
Lisez l'article original, « Fps par rapport au temps d'image de Robert Dunlop » pour plus d'informations.
Le contrôle thermique est l’un des domaines les plus importants à optimiser lors du développement d’applications pour appareils mobiles. Si le CPU ou le GPU passent trop de temps à fonctionner à plein régime en raison d’une conception inefficace, ces puces deviendront chaudes. Pour éviter d'endommager les puces (et potentiellement de brûler les mains du joueur !), le système d'exploitation réduira la vitesse d'horloge de l'appareil pour lui permettre de refroidir, provoquant un bégaiement d'image et une mauvaise expérience utilisateur. Cette réduction des performances est connue sous le nom de limitation thermique.
Des fréquences d'images plus élevées et une exécution de code accrue (ou des opérations d'accès à la DRAM) entraînent une consommation accrue de la batterie et une génération de chaleur. De mauvaises performances peuvent également supprimer des segments entiers d’appareils mobiles bas de gamme, ce qui peut conduire à des opportunités de marché manquées et, par conséquent, à une baisse des ventes.
Lorsque vous abordez le problème des thermiques, considérez le budget dont vous disposez comme un budget à l’échelle du système.
Combattez la limitation thermique et l’épuisement de la batterie en tirant parti d’une technique de profilage précoce pour optimiser votre jeu dès le début. Composez les paramètres de votre projet pour le matériel de votre plate-forme cible afin de lutter contre les problèmes thermiques et d'épuisement de la batterie.
Ajuster les budgets de trame sur mobile
Laisser un temps d'inactivité d'environ 35 % est la recommandation typique pour lutter contre les problèmes thermiques des appareils sur des durées de lecture prolongées. Cela donne aux puces mobiles le temps de refroidir et aide à éviter une décharge excessive de la batterie. En utilisant un temps de trame cible de 33,33 ms par image (pour 30 ips), un budget d'image typique pour les appareils mobiles sera d'environ 22 ms par image.
Le calcul ressemble à ceci : (1 000 ms / 30) * 0,65 = 21,66 ms
Pour atteindre 60 ips sur mobile en utilisant le même calcul, il faudrait un temps de trame cible de (1 000 ms / 60) * 0,65 = 10,83 ms. Ceci est difficile à réaliser sur de nombreux appareils mobiles et viderait la batterie deux fois plus vite que si l’on visait 30 ips. Pour ces raisons, la plupart des jeux mobiles ciblent 30 fps plutôt que 60. Utilisez Application.targetFrameRate pour contrôler ce paramètre et reportez-vous à la section « Définir un budget de trame » dans le livre électronique pour plus de détails sur la durée de trame.
La mise à l'échelle de fréquence sur les puces mobiles peut rendre difficile l'identification des allocations de budget de temps d'inactivité de votre trame lors du profilage. Vos améliorations et optimisations peuvent avoir un effet positif net, mais l'appareil mobile peut réduire sa fréquence et, par conséquent, fonctionner plus froidement. Utilisez des outils personnalisés tels que FTrace ou Perfetto pour surveiller les fréquences des puces mobiles, les temps d'inactivité et la mise à l'échelle avant et après les optimisations.
Tant que vous respectez votre budget total de temps d'image pour votre fps cible (33,33 ms pour 30 fps) et que vous voyez votre appareil fonctionner moins ou enregistrer des températures plus basses pour maintenir cette fréquence d'images, alors vous êtes sur la bonne voie.
Une autre raison d’ajouter une marge de manœuvre au budget de cadre sur les appareils mobiles est de tenir compte des fluctuations de température réelles. Par une journée chaude, un appareil mobile chauffe et a du mal à dissiper la chaleur, ce qui peut entraîner une limitation thermique et de mauvaises performances de jeu. Mettre de côté un pourcentage du budget cadre permettra d’éviter ce genre de scénarios.
L'accès à la DRAM est généralement une opération gourmande en énergie sur les appareils mobiles. Les conseils d'optimisation d'Arm pour le contenu graphique sur les appareils mobiles indiquent que l'accès à la mémoire LPDDR4 coûte environ 100 picojoules par octet.
Réduisez le nombre d’opérations d’accès à la mémoire par image en :
- Réduire la fréquence d'images
- Réduire la résolution d’affichage lorsque cela est possible
- Utilisation de maillages plus simples avec un nombre de sommets et une précision d'attribut réduits
- Utiliser la compression de texture et le mipmapping
Lorsque vous devez vous concentrer sur les appareils exploitant le matériel Arm ou Arm Mali, les outils Arm Mobile Studio (en particulier Streamline Performance Analyzer) incluent d'excellents compteurs de performances pour identifier les problèmes de bande passante mémoire. Les compteurs sont répertoriés et expliqués pour chaque génération de GPU Arm, par exemple Mali-G78. Notez que le profilage GPU Mobile Studio nécessite Arm Mali.
Établir des niveaux de matériel pour l'analyse comparative
En plus d'utiliser des outils de profilage spécifiques à la plate-forme, établissez des niveaux ou un appareil aux spécifications les plus basses pour chaque plate-forme et niveau de qualité que vous souhaitez prendre en charge, puis profilez et optimisez les performances pour chacune de ces spécifications.
À titre d'exemple, si vous ciblez des plates-formes mobiles, vous pouvez décider de prendre en charge trois niveaux avec des contrôles de qualité qui activent ou désactivent des fonctionnalités en fonction du matériel cible. Vous optimisez ensuite pour la spécification d’appareil la plus basse de chaque niveau. Comme autre exemple, si vous développez un jeu pour PlayStation 4 et PlayStation 5, assurez-vous d'avoir un profil sur les deux.
Pour un guide complet d'optimisation mobile, consultez Optimisez les performances de vos jeux mobiles. Cet e-book contient de nombreux trucs et astuces qui vous aideront à réduire la limitation thermique et à augmenter la durée de vie de la batterie des appareils mobiles exécutant vos jeux.
Une approche de haut en bas fonctionne bien lors du profilage, en commençant par le profilage profond désactivé. Utilisez cette approche de haut niveau pour collecter des données et prendre des notes sur les scénarios qui entraînent des allocations gérées indésirables ou une trop grande quantité de temps CPU dans vos zones de boucle de jeu principales.
Vous devrez d’abord rassembler les piles d’appels pour les marqueurs GC.Alloc. Si vous n'êtes pas familier avec ce processus, trouvez quelques trucs et astuces dans la section « Localisation des allocations de mémoire récurrentes sur la durée de vie de l'application » du Guide ultime de profilage des jeux Unity.
Si les piles d'appels signalées ne sont pas suffisamment détaillées pour retrouver la source des allocations ou d'autres ralentissements, vous pouvez alors effectuer une deuxième session de profilage avec le profilage approfondi activé afin de trouver la source des allocations.
Lorsque vous collectez des notes sur les « délinquants » temporels, assurez-vous de noter comment ils se comparent par rapport au reste de la trame. Cet impact relatif sera affecté par l’activation du profilage profond.
En savoir plus sur le profilage approfondi dans le Guide ultime du profilage des jeux Unity.
Profil précoce
Les meilleurs gains du profilage sont obtenus lorsque vous commencez tôt dans le cycle de vie de développement de votre projet.
Créez votre profil tôt et souvent afin que vous et votre équipe compreniez et mémorisiez une « signature de performance » pour le projet. Si les performances chutent, vous pourrez facilement repérer quand les choses tournent mal et remédier au problème.
Les résultats de profilage les plus précis proviennent toujours de l'exécution et du profilage des builds sur les appareils cibles, ainsi que de l'exploitation d'outils spécifiques à la plate-forme pour approfondir les caractéristiques matérielles de chaque plate-forme. Cette combinaison vous fournira une vue globale des performances des applications sur tous vos appareils cibles.
Téléchargez la version PDF imprimable de ce graphique ici.
Sur certaines plates-formes, il est facile de déterminer si votre application est liée au CPU ou au GPU. Par exemple, lors de l'exécution d'un jeu iOS à partir de Xcode, le panneau fps affiche un graphique à barres avec le temps total CPU et GPU afin que vous puissiez voir lequel est le plus élevé. Le temps CPU inclut le temps passé à attendre VSync, qui est toujours activé sur les appareils mobiles.
Cependant, sur certaines plates-formes, il peut être difficile d'obtenir des données de synchronisation GPU. Heureusement, Unity Profiler affiche suffisamment d'informations pour identifier l'emplacement des goulots d'étranglement en matière de performances. L'organigramme ci-dessus illustre le processus de profilage initial et les sections qui le suivent fournissent des informations détaillées sur chaque étape. Ils présentent également des captures Profiler de projets Unity réels pour illustrer le type de choses à rechercher.
Pour obtenir une image globale de toutes les activités du processeur, y compris lorsqu'il attend le GPU, utilisez la vue Chronologie dans le module CPU du Profiler. Familiarisez-vous avec les marqueurs courants du Profiler pour interpréter correctement les captures. Certains marqueurs du Profiler peuvent apparaître différemment en fonction de votre plate-forme cible, alors prenez le temps d'explorer les captures de votre jeu sur chacune de vos plates-formes cibles pour avoir une idée de ce à quoi ressemble une capture « normale » pour votre projet.
Les performances d'un projet sont limitées par la puce et/ou le thread qui prend le plus de temps. C'est le domaine sur lequel vous devez concentrer vos efforts d'optimisation. Par exemple, imaginez un jeu avec un budget de temps d'image cible de 33,33 ms et VSync activé :
- Si le temps de trame CPU (hors VSync) est de 25 ms et le temps GPU de 20 ms, pas de problème ! Vous êtes limité au processeur, mais tout respecte le budget, et l'optimisation des choses n'améliorera pas la fréquence d'images (à moins que vous n'obteniez à la fois le processeur et le GPU en dessous de 16,66 ms et que vous sautiez jusqu'à 60 ips).
- Si le temps de trame du processeur est de 40 ms et celui du GPU de 20 ms, vous êtes limité au processeur et devrez optimiser les performances du processeur. L'optimisation des performances du GPU n'aidera pas ; en fait, vous souhaiterez peut-être déplacer une partie du travail du processeur vers le GPU, par exemple en utilisant des shaders de calcul au lieu du code C# pour certaines choses, afin d'équilibrer les choses.
- Si le temps de trame du processeur est de 20 ms et celui du GPU de 40 ms, vous êtes limité au GPU et devez optimiser le travail du GPU.
- Si le CPU et le GPU sont tous deux à 40 ms, vous êtes lié par les deux et devrez les optimiser tous les deux en dessous de 33,33 ms pour atteindre 30 ips.
Consultez ces ressources qui explorent plus en détail le fait d'être lié au CPU ou au GPU :
Le profilage et l'optimisation de votre projet dès le début et souvent tout au long du développement vous aideront à garantir que tous les threads CPU de votre application et la durée globale du frame GPU respectent le budget de frame.
Ci-dessus, une image d'une capture Profiler d'un jeu mobile Unity développé par une équipe qui a effectué un profilage et une optimisation en continu. Le jeu cible 60 ips sur les téléphones mobiles haut de gamme et 30 ips sur les téléphones de spécifications moyennes/basse, comme celui de cette capture.
Notez que près de la moitié du temps sur l’image sélectionnée est occupée par le marqueur jaune WaitForTargetfps Profiler. L'application a défini Application.targetFrameRate sur 30 ips et VSync est activé. Le travail de traitement réel sur le thread principal se termine aux alentours de 19 ms, et le reste du temps est passé à attendre que le reste des 33,33 ms s'écoule avant de commencer la trame suivante. Bien que ce temps soit représenté par un marqueur Profiler, le thread principal du processeur est essentiellement inactif pendant ce temps, permettant au processeur de refroidir et d'utiliser un minimum d'énergie de la batterie.
Le marqueur à surveiller peut être différent sur d'autres plates-formes ou si VSync est désactivé. L'important est de vérifier si le thread principal s'exécute dans les limites de votre budget de trame ou exactement selon votre budget de trame, avec une sorte de marqueur qui indique que l'application attend VSync et si les autres threads ont du temps d'inactivité.
Le temps d'inactivité est représenté par des marqueurs Profiler gris ou jaunes. La capture d'écran ci-dessus montre que le thread de rendu est inactif dans Gfx.WaitForGfxCommandsFromMainThread, ce qui indique les moments où il a fini d'envoyer des appels de tirage au GPU sur une image et attend d'autres demandes d'appel de tirage du CPU sur la suivante. De même, bien que le thread Job Worker 0 passe du temps dans Canvas.GeometryJob, il est la plupart du temps inactif. Ce sont tous des signes d’une application qui respecte confortablement le budget cadre.
Si votre jeu respecte le budget cadre
Si vous respectez le budget cadre, y compris les ajustements apportés au budget pour tenir compte de l'utilisation de la batterie et de la limitation thermique, vous avez terminé le profilage des performances jusqu'à la prochaine fois – félicitations. Pensez à exécuter Memory Profiler pour vous assurer que l’application respecte également son budget de mémoire.
L'image ci-dessus montre un jeu fonctionnant confortablement dans le budget d'images d'environ 22 ms requis pour 30 ips. Notez le WaitForTargetfps remplissant le temps du thread principal jusqu'à VSync et les temps d'inactivité gris dans le thread de rendu et le thread de travail. Notez également que l'intervalle VBlank peut être observé en regardant les heures de fin de Gfx.Present image par image, et que vous pouvez établir une échelle de temps dans la zone Chronologie ou sur la règle de temps en haut pour mesurer de l'une d'elles à le suivant.
Si votre jeu ne respecte pas le budget du processeur, l’étape suivante consiste à rechercher quelle partie du processeur constitue le goulot d’étranglement – en d’autres termes, quel thread est le plus occupé. Le but du profilage est d'identifier les goulots d'étranglement comme cibles d'optimisation ; si vous vous fiez à des conjectures, vous pouvez finir par optimiser des parties du jeu qui ne constituent pas des goulots d'étranglement, ce qui entraînera peu ou pas d'amélioration des performances globales. Certaines « optimisations » pourraient même dégrader les performances globales de votre jeu.
Il est rare que la totalité de la charge de travail du processeur soit le goulot d'étranglement. Les processeurs modernes disposent d'un certain nombre de cœurs différents, capables d'effectuer des travaux de manière indépendante et simultanée. Différents threads peuvent s'exécuter sur chaque cœur de processeur. Une application Unity complète utilise une gamme de threads à des fins différentes, mais les threads les plus courants pour détecter des problèmes de performances sont :
- Le fil conducteur: C'est là que toutes les logiques/scripts du jeu effectuent leur travail par défaut et où la majorité du temps est consacrée aux fonctionnalités et aux systèmes tels que la physique, l'animation, l'interface utilisateur et le rendu.
- Le fil de rendu : Pendant le processus de rendu, le thread principal examine la scène et effectue l'élimination de la caméra, le tri en profondeur et le regroupement des appels de dessin, ce qui donne lieu à une liste d'éléments à restituer. Cette liste est transmise au thread de rendu, qui la traduit de la représentation interne indépendante de la plate-forme d'Unity en appels d'API graphiques spécifiques requis pour instruire le GPU sur une plate-forme particulière.
- Les fils de discussion du Job Worker : Les développeurs peuvent utiliser le système de tâches C# pour planifier l'exécution de certains types de travaux sur les threads de travail, ce qui réduit la charge de travail sur le thread principal. Certains systèmes et fonctionnalités d'Unity utilisent également le système de tâches, comme la physique, l'animation et le rendu.
Fil principal
L'image ci-dessus montre à quoi pourraient ressembler les choses dans un projet lié au fil principal. Ce projet fonctionne sur un Meta Quest 2, qui cible normalement des budgets d'images de 13,88 ms (72 ips) ou même de 8,33 ms (120 ips), car des fréquences d'images élevées sont importantes pour éviter le mal des transports dans les appareils VR . Cependant, même si ce jeu visait les 30 fps, force est de constater que ce projet est en difficulté.
Bien que le thread de rendu et les threads de travail ressemblent à l'exemple qui respecte le budget du frame, le thread principal est clairement occupé par le travail pendant toute la frame. Même en tenant compte de la petite quantité de surcharge du Profiler à la fin de la trame, le thread principal est occupé pendant plus de 45 ms, ce qui signifie que ce projet atteint des fréquences d'images inférieures à 22 ips. Il n'y a aucun marqueur qui montre le thread principal en attente de VSync ; c'est occupé pour tout le cadre.
La prochaine étape de l'enquête consiste à identifier les parties du cadre qui prennent le plus de temps et à comprendre pourquoi. Sur cette image, PostLateUpdate.FinishFrameRendering prend 16,23 ms, soit plus que le budget total de l'image. Une inspection plus approfondie révèle qu'il existe cinq instances d'un marqueur appelé Inl_RenderCameraStack, indiquant qu'il y a cinq caméras actives et rendant la scène. Étant donné que chaque caméra dans Unity appelle l'ensemble du pipeline de rendu, y compris l'élimination, le tri et le traitement par lots, la tâche la plus prioritaire de ce projet consiste à réduire le nombre de caméras actives, idéalement à une seule.
BehaviourUpdate, le marqueur qui englobe toutes les méthodes MonoBehaviour Update(), prend 7,27 ms, et les sections magenta de la chronologie indiquent où les scripts allouent la mémoire de tas gérée. Le passage à la vue Hiérarchie et le filtrage en tapant GC.Alloc dans la barre de recherche montrent que l'allocation de cette mémoire prend environ 0,33 ms dans cette trame. Cependant, il s'agit d'une mesure inexacte de l'impact des allocations de mémoire sur les performances de votre processeur.
Les marqueurs GC.Alloc ne sont pas réellement chronométrés en mesurant le temps entre un point de début et un point de fin. Pour réduire leur surcharge, ils sont enregistrés uniquement sous la forme de leur horodatage de début, plus la taille de leur allocation. Le Profiler leur attribue un minimum de temps pour s'assurer qu'ils sont visibles. L'allocation réelle peut prendre plus de temps, surtout si une nouvelle plage de mémoire doit être demandée au système. Pour voir l'impact plus clairement, placez des marqueurs Profiler autour du code qui effectue l'allocation, et dans le profilage approfondi, les écarts entre les échantillons GC.Alloc de couleur magenta dans la vue Chronologie fournissent une indication du temps qu'ils ont pu prendre.
De plus, l’allocation de nouvelle mémoire peut avoir des effets négatifs sur les performances qui sont plus difficiles à mesurer et à leur attribuer directement :
- Demander une nouvelle mémoire au système peut affecter le budget d'alimentation d'un appareil mobile, ce qui pourrait conduire le système à ralentir le CPU ou le GPU.
- La nouvelle mémoire doit probablement être chargée dans le cache L1 du processeur et repousse ainsi les lignes de cache existantes.
- Le garbage collection incrémentiel ou synchrone peut être déclenché directement ou avec un délai lorsque l'espace libre existant dans la mémoire gérée est finalement dépassé.
Au début de la trame, quatre instances de Physics.FixedUpdate totalisent 4,57 ms. Plus tard, LateBehaviourUpdate (appels à MonoBehaviour.LateUpdate()) prend 4 ms et les animateurs représentent environ 1 ms.
Pour garantir que ce projet atteigne le budget et le débit de trame souhaités, tous ces problèmes de threads principaux doivent être étudiés pour trouver des optimisations appropriées. Les gains de performances les plus importants seront réalisés en optimisant les choses qui prennent le plus de temps.
Les domaines suivants sont souvent des endroits fructueux à rechercher pour l'optimisation dans les projets liés au thread principal :
- Physique
- Mises à jour du script MonoBehaviour
- Répartition et/ou collecte des déchets
- Sélection et rendu de la caméra
- Mauvais regroupement des appels de tirage
- Mises à jour, mises en page et reconstructions de l'interface utilisateur
- Animation
Selon le problème que vous souhaitez étudier, d'autres outils peuvent également être utiles :
- Pour les scripts MonoBehaviour qui prennent beaucoup de temps mais ne vous montrent pas exactement pourquoi c'est le cas, ajoutez des marqueurs de profileur au code ou essayez le profilage approfondi pour voir la pile d'appels complète.
- Pour les scripts qui allouent de la mémoire gérée, activez les piles d'appels d'allocation pour voir exactement d'où proviennent les allocations. Vous pouvez également activer le profilage approfondi ou utiliser Project Auditor, qui affiche les problèmes de code filtrés par mémoire, afin que vous puissiez identifier toutes les lignes de code qui aboutissent à des allocations gérées.
- Utilisez Frame Debugger pour rechercher les causes d’un mauvais traitement par lots des appels de tirage.
Pour obtenir des conseils complets sur l’optimisation de votre jeu, téléchargez gratuitement ces guides experts Unity :
- Optimisez les performances de vos jeux mobiles
- Optimisez vos performances de jeu pour console et PC
La capture d'écran ci-dessus représente un projet lié par son fil de rendu. Il s'agit d'un jeu sur console avec un point de vue isométrique et un budget d'image cible de 33,33 ms.
La capture du Profiler montre qu'avant que le rendu puisse commencer sur l'image actuelle, le thread principal attend le thread de rendu, comme indiqué par Gfx.WaitForPresentOnGfxThreadmarker. Le thread de rendu soumet toujours les commandes d'appel draw de l'image précédente et n'est pas prêt à accepter de nouveaux appels draw depuis le thread principal ; le fil de rendu passe du temps dans Camera.Render.
Vous pouvez faire la différence entre les marqueurs relatifs à l'image actuelle et les marqueurs d'autres images, car ces derniers apparaissent plus sombres. Vous pouvez également voir qu'une fois que le thread principal est capable de continuer et de commencer à émettre des appels draw que le thread de rendu doit traiter, le thread de rendu prend plus de 100 ms pour traiter l'image actuelle, ce qui crée également un goulot d'étranglement lors de l'image suivante.
Une enquête plus approfondie a montré que ce jeu avait une configuration de rendu complexe, impliquant neuf caméras différentes et de nombreuses passes supplémentaires causées par des shaders de remplacement. Le jeu rendait également plus de 130 points lumineux en utilisant un chemin de rendu direct, qui peut ajouter plusieurs appels de dessin transparents supplémentaires pour chaque lumière. Au total, ces problèmes se sont combinés pour créer plus de 3 000 appels de tirage par image.
Voici les causes courantes à rechercher pour les projets dont le rendu est lié aux threads :
- Mauvais traitement par lots des appels de dessin, en particulier sur les anciennes API graphiques telles que OpenGL ou DirectX 11
- Trop de caméras. À moins que vous ne créiez un jeu multijoueur en écran partagé, il est probable que vous n'ayez qu'une seule caméra active.
- Mauvaise sélection, ce qui entraîne le tirage de trop de choses. Examinez les dimensions du tronc de votre appareil photo et éliminez les masques de calque. Pensez à activer l’élimination des occlusions. Peut-être même créez votre propre système d'élimination des occlusions simple, basé sur ce que vous savez sur la façon dont les objets sont disposés dans votre monde. Regardez combien d'objets projetant des ombres il y a dans la scène – l'élimination des ombres se produit dans une passe distincte de l'élimination « régulière ».
Le module Rendering Profiler affiche un aperçu du nombre de lots d'appels de tirage et d'appels SetPass à chaque image. Le meilleur outil pour rechercher les lots d'appels de dessin que votre thread de rendu envoie au GPU est le Frame Debugger.
Les projets liés par des threads CPU autres que les threads principaux ou de rendu ne sont pas si courants. Cependant, cela peut survenir si votre projet utilise la pile technologique orientée données (DOTS), en particulier si le travail est déplacé du thread principal vers des threads de travail à l'aide du système de tâches C#.
La capture vue ci-dessus provient du mode Lecture dans l'éditeur, montrant un projet DOTS exécutant une simulation de fluide de particules sur le processeur.
À première vue, cela semble être une réussite. Les threads de travail sont étroitement remplis de tâches compilées en rafale, ce qui indique qu'une grande quantité de travail a été déplacée du thread principal. Habituellement, c’est une bonne décision.
Cependant, dans ce cas, le temps de trame de 48,14 ms et le marqueur gris WaitForJobGroupID de 35,57 ms sur le thread principal sont des signes que tout ne va pas bien. WaitForJobGroupID indique que le thread principal a planifié l'exécution de tâches de manière asynchrone sur les threads de travail, mais il a besoin des résultats de ces tâches avant que les threads de travail aient fini de les exécuter. Les marqueurs bleus du profileur sous WaitForJobGroupID affichent le thread principal exécutant les tâches pendant qu'il attend, dans le but de garantir que les tâches se terminent plus tôt.
Bien que les tâches soient compilées en rafale, elles font encore beaucoup de travail. Peut-être que la structure de requête spatiale utilisée par ce projet pour trouver rapidement quelles particules sont proches les unes des autres devrait être optimisée ou remplacée par une structure plus efficace. Les tâches de requête spatiale peuvent également être planifiées pour la fin de la trame plutôt que pour le début, les résultats n'étant requis qu'au début de la trame suivante. Peut-être que ce projet essaie de simuler trop de particules. Une analyse plus approfondie du code des tâches est nécessaire pour trouver la solution, donc l'ajout de marqueurs Profiler plus fins peut aider à identifier leurs parties les plus lentes.
Les tâches de votre projet peuvent ne pas être aussi parallélisées que dans cet exemple. Peut-être que vous n'avez qu'un seul long travail en cours d'exécution dans un seul thread de travail. C'est très bien, à condition que le temps entre la planification du travail et le moment où il doit être terminé soit suffisamment long pour que le travail puisse s'exécuter. Si ce n'est pas le cas, vous verrez le thread principal se bloquer en attendant la fin du travail, comme dans la capture d'écran ci-dessus.
Les causes courantes des points de synchronisation et des goulots d'étranglement des threads de travail incluent :
- Les travaux ne sont pas compilés par le compilateur Burst
- Travaux de longue durée sur un seul thread de travail au lieu d'être parallélisés sur plusieurs threads de travail
- Temps insuffisant entre le moment dans la trame où un travail est planifié et le moment où le résultat est requis
- Plusieurs « points de synchronisation » dans un cadre, qui nécessitent que toutes les tâches soient terminées immédiatement
Vous pouvez utiliser la fonctionnalité Événements de flux dans la vue Chronologie du module CPU Usage Profiler pour déterminer quand les tâches sont planifiées et quand leurs résultats sont attendus par le thread principal. Pour plus d'informations sur l'écriture de code DOTS efficace, consultez le guide des meilleures pratiques DOTS .
Votre application est liée au GPU si le thread principal passe beaucoup de temps dans les marqueurs du profileur tels que Gfx.WaitForPresentOnGfxThread et que votre thread de rendu affiche simultanément des marqueurs tels que Gfx.PresentFrame ou <GraphicsAPIName>.WaitForLastPresent.
La capture suivante a été prise sur un Samsung Galaxy S7, à l'aide de l'API graphique Vulkan. Bien qu'une partie du temps passé dans Gfx.PresentFrame dans cet exemple puisse être liée à l'attente de VSync, la longueur extrême de ce marqueur de profileur indique que la majorité de ce temps est passée à attendre que le GPU termine le rendu de l'image précédente.
Dans ce jeu, certains événements de gameplay ont déclenché l'utilisation d'un shader qui a triplé le nombre d'appels draw rendus par le GPU. Les problèmes courants à étudier lors du profilage des performances du GPU incluent :
- Effets de post-traitement coûteux en plein écran, y compris des coupables courants comme l'occlusion ambiante et la floraison
- Shaders de fragments coûteux causés par :
- Logique de branchement
- Utiliser la précision flottante complète plutôt que la demi-précision
- Utilisation excessive de registres qui affectent l'occupation du front d'onde des GPU
- Overdraw dans la file d'attente de rendu transparent causé par une interface utilisateur, des systèmes de particules ou des effets de post-traitement inefficaces
- Résolutions d'écran excessivement élevées, telles que celles trouvées sur les écrans 4K ou les écrans Retina sur les appareils mobiles
- Microtriangles causés par une géométrie de maillage dense ou un manque de LOD, ce qui constitue un problème particulier sur les GPU mobiles mais peut également affecter les GPU des PC et des consoles.
- Manques de cache et gaspillage de bande passante mémoire GPU causés par des textures non compressées ou des textures haute résolution sans mipmaps
- Shaders de géométrie ou de tesselation, qui peuvent être exécutés plusieurs fois par image si les ombres dynamiques sont activées
Si votre application semble être liée au GPU, vous pouvez utiliser Frame Debugger comme moyen rapide de comprendre les lots d’appels de dessin envoyés au GPU. Cependant, cet outil ne peut présenter aucune information spécifique sur la synchronisation du GPU, mais uniquement sur la façon dont la scène globale est construite.
La meilleure façon d'étudier la cause des goulots d'étranglement du GPU est d'examiner une capture GPU à partir d'un profileur GPU approprié. L'outil que vous utilisez dépend du matériel cible et de l'API graphique choisie.
Téléchargez gratuitement l'e-book, Guide ultime pour profiler les jeux Unity, pour obtenir tous les conseils et bonnes pratiques.