Loupe

// Build 2014 : Direct3D 12 API preview

Une belle session de Max MacMullen, réservée aux barbus (avec un premier slide qui annonce la couleur : cette session ne s’adresse qu’aux développeurs ayant une bonne expérience de développement DirectX 11).

Peu de démo, mais un discours très clair sur les buts que s’est fixé l’équipe de développement de Direct3D en terme de performances (à priori, d’autres features non directement liées aux performances seront présentées dans de futures talks).

La présentation s’est donc focalisée sur un thème principal : atteindre le même degré d’efficacité avec Direct3D sur le PC, que ce que l’on peut faire avec une console au matériel dédié. Et pour ceci, Microsoft met le paquet sur 2 aspects cruciaux : l’efficacité du cpu (combien d’instructions sont nécessaires pour soumettre des commandes aux GPU), et le parallélisme (comment profiter au mieux des processeurs multicores dans le contexte d’un moteur de rendu 3D avec Direct3D).

Efficacité du CPU

Pipeline State Objects (PSO)

Une des actions qui demande le plus de ressources CPU avec DirectX11, est la gestion des états de la pipeline de rendu. En effet, pour chaque étape de la pipeline de rendu (input assembler, vertex shader, rasterizer, pixel shader, etc.), Direct3D 11 expose de getters et setters pour spécifier comment le GPU doit se comporter (c’est ce qui nous permet de spécifier quel shader utiliser à telle ou telle étape du rendu, définir comment l’alpha Blending doit se comporter, décrire le layout de nos vertex buffers etc.). Chaque paramètre d’état de chaque étape de la pipeline est tracké indépendament par le driver, et au moment du “Draw”, celui-ci doit se charger d’envoyer les données ayant été modifiées au GPU pour que la pipeline soit prête à effectuer le rendu.

Ce tracking est particulièrement couteux, d’autant que le changement d’états est quelque chose se produisant très fréquemment. D’après les métriques récoltées par Microsoft, dans les jeux les plus modernes, on peut facilement atteindre les centaines, voir milliers de changement d’état par frame ! La partie la plus couteuse de cette opération consistant à enregistrer les modifications apportées à ces paramettre, et à générer les buffers à envoyer au gpu pour préparer la pipeline de rendu. L’envoi de ces buffers au GPU étant lui relativement anecdotique. A noter que dans la très grande majorité des GPUs actuels, un même “state” Direct3D aura finalement un impact à plusieurs endroit dans la pipeline de rendu spécifique à la carte graphique : par exemple quand on spécifie quel Pixel Shader utiliser, au niveau du GPU cela va non seulement vouloir dire que le bytecode de ce shader sera exécuté à cette étape de la pipeline, mais aussi que telle ou telle transformation de données doit avoir lieux en sortie du Rasterize pour que le Pixel Shader soit capable de travailler. On imagine donc bien que le driver a effectivement du boulot à faire pour identifier l’état général de la pipeline de rendu complet au moment de l’appel à la méthode Draw!

Pour éviter le tracking individuel de tous ces paramètres et la génération des buffers d’états au moment du draw, DirectX12 propose la notion de Pipeline State Objects. Le principe est très simple : plutôt que d’appeler les setter des états de chaque étape de la pipeline de rendu indépendamment, on crée un Pipeline State Object, sur lequel on va spécifier l’état de toutes les étapes de la pipeline, et le driver génère une fois pour toute les buffers correspondant aux données de configuration de la pipeline pour le rendu d’un objet particulier. Quand on va vouloir configurer la pipeline de rendu pour dessiner un objet, plutôt que de le faire propriété par propriété, on indiquera simplement à Direct3D quel Pipeline State Object utiliser. Cela simplifie ENORMEMENT le travail du driver qui n’aura plus de tracking du tout à faire pour dynamiquement générer ses buffers : tout aura été pré-calculé à l’avance, on envoie juste un buffer au GPU !

Resource descriptor Heaps and Tables

Un autre aspect assez couteux pour les drivers sous Direct3D11, est la binding de ressources. Il s’agit de l’étape ou on va indiquer à la Pipeline, quelles textures / vertex buffers / index buffers / etc. utiliser à chaque étape de la pipeline de rendu. Pour effectuer cela, sous DirectX11, pour chaque ressource à binder, un resource descriptor est envoyé au GPU (un resource descriptor étant une structure contenant des métadonnées sur la ressource – format des données, taille, nombre de mips, …- et l’adresse des données de cette resource en mémoire vidéo).

Sous DirectX12, on va plutôt déclarer des tableaux de descriptors en mémoire vidéo, et on va simplement indiquer au gpu quel tableau utiliser, ainsi qu’un range de resource à binder. Ainsi si un Pixel Shader donné utilise 5 textures, plutôt que de faire 5 appels à Direct3D avec chacun un Resource Descriptor, on va allouer une fois pour tous ces descriptors dans un tableau en mémoire vidéo, et on indiquera à Direct3D que l’on veut utiliser ce tableau de resources. La encore, on gagne pas mal en efficacité !

Command Bundles

Dernier aspect abordé à propos de l’efficacité du CPU, les command bundles sont une avancée qui à mon sens ressemble un peu au Pipeline State Object, à la différence prêt qu’au lieu de regrouper l’ensemble des états de la pipeline désirés dans un objet unique, cette fois-ci on va pouvoir enregistrer la séquence de commandes à exécuter pour effectuer le rendu de tel ou tel objet, et la ré-exécuter plus tard. Cela vas nous permettre pour chaque objet composant une scène d’enregistrer à la première frame la séquence de commande à envoyer au GPU pour le dessiner, et pour chaque frame suivante, on changera simplement le contenu des Constant Buffer utilisés (pour modifier la position de la caméra, l’état d’une animation squelettique etc.), et on ré-exécutera simplement la même liste de commande, plutôt que de devoir traduire à chaque frame les appels DirectX en commandes spécifiques au gpu. Cela est très simple et très efficace (petite démo à l’occasion montrant une consommation CPU 2 fois moindre sur une même scène extrait de 3D mark quand cette feature est utilisée).

Petit truc pas mal aussi, à leur réexécution, on peut aussi spécifier quel Resource Descriptor Table utiliser : si plusieurs objets de notre scène se dessinent de la même manière, mais en utilisant des textures / modèles différents, on pourra utiliser le même Command Bundle pour effectuer leur rendu !

Parallélisme

Du côté du parallélisme, il y’a une nouveauté assez fondamentale : l’ImmediateContext n’existe plus ! En DirectX12 tous les DeviceContext sont deferred, et poussent leur command lists (qu’ils soient générés dynamiquement ou soumis sous la forme de Command Bundles) dans une Command Queue commune de manière asynchrone. Bien sûr cela veut aussi dire qu’il n’y a plus de fonctionnalités réservées à l’ImmediateContext (vu qu’il n’existe plus !). Cela veut aussi dire, que la génération de Pipeline State Object et de Command Bundles, ainsi que l’allocation de resource descriptor heaps peut se faire de manière parallèle ! On va donc pouvoir exploiter chaque coeur au maximum lors du rendu 3d.

Une autre barrière est aussi tombée : on pourra désormais modifier les resource dynamiques de manière parallèle (au paravent, seul un thread pouvait avoir accès au contenu d’un buffer dynamique à un moment donné, ce qui limitait les possibilité de parallélisme).

 

Conclusion

Cette session m’a énormément plu, tant du point de vue technique (la session fut très très riche et pointue à ce niveau là), que sur la prestation du speaker : le discours était clair, et on avait l’impression de retracer l’histoire de “l’équipe Direct3D à la recherche du moindre truc qu’on pourrait optimiser du côté du CPU”. Une session à regarder donc par tous les développeurs de jeux et applications 3D !

Ces billets pourraient aussi vous intéresser

Vous nous direz ?!

Commentaires

comments powered by Disqus