II. Aperçu de Vulkan▲
Ce chapitre commencera par introduire Vulkan et la raison de sa création. Nous nous intéresserons ensuite aux éléments requis pour afficher un premier triangle. Cela vous donnera une vue d'ensemble pour mieux replacer les futurs chapitres dans leur contexte. Nous conclurons sur la structure de Vulkan et la manière dont la bibliothèque est communément utilisée.
II-A. Origine de Vulkan▲
Comme pour les bibliothèques précédentes, Vulkan est conçue comme une abstraction, multiplateforme, des GPU. Le problème avec la plupart de ces bibliothèques est qu'elles furent créées à une époque où le matériel graphique était limité à des fonctionnalités prédéfinies configurables. Les développeurs devaient fournir les données des sommets dans un format standardisé et étaient ainsi à la merci des constructeurs pour les options d'éclairage et d’ombrage.
Au fil des évolutions des cartes graphiques, elles offrirent de plus en plus de fonctionnalités programmables. Ces fonctionnalités devaient être intégrées tant bien que mal aux bibliothèques existantes. Ceci résulta en une abstraction peu pratique où le pilote devait deviner l'intention du développeur et faire correspondre le programme aux architectures modernes. C'est pour cela qu’il existe des mises à jour de pilotes visant à améliorer les performances dans les jeux et même quelquefois, de manière significative. À cause de la complexité de ces pilotes, les développeurs doivent gérer les différences de comportement entre les fabricants dont notamment la syntaxe acceptée dans les shaders. En plus des nouvelles fonctionnalités, cette dernière décennie a vu un nombre grandissant d’appareils mobiles ayant de puissantes puces graphiques. Ces GPU mobiles ont des architectures différentes, pensées pour fonctionner avec des fortes contraintes d’énergie et d’espace. On peut citer le rendu en tuiles, qui offrirait de meilleures performances si les développeurs avaient un meilleur contrôle sur la fonctionnalité. Une autre limitation provenant de l’âge de ces bibliothèques est le manque de support du multithread créant un goulot d’étranglement sur le CPU.
Vulkan résout ces problèmes en ayant été conçu pour les architectures modernes. Elle réduit le travail du pilote en permettant au développeur d’expliciter ses objectifs grâce à une bibliothèque plus prolixe. Aussi elle permet à plusieurs threads de créer et d’envoyer des commandes en parallèle. Elle supprime les différences lors de la compilation des shaders en imposant un format de code intermédiaire (bytecode) interprété par un compilateur officiel. Enfin, la bibliothèque prend en compte le caractère générique des cartes graphiques et permet d’accéder aux fonctionnalités génériques et graphiques par le biais d’une unique API.
II-B. Le nécessaire pour afficher un triangle▲
Nous allons maintenant nous intéresser aux étapes nécessaires à l’affichage d’un triangle dans un programme Vulkan correctement conçu. Tous les concepts ici évoqués seront développés dans les prochains chapitres. Le but ici est simplement de vous donner une vue d’ensemble du processus afin que ce soit plus clair pour la suite.
II-B-1. Étape 1 - Instance et sélection d’un périphérique physique▲
Une application commence par initialiser la bibliothèque à l’aide d’une VkInstance. Une instance est créée en décrivant votre application et les extensions que vous comptez utiliser. Après avoir créé votre instance, vous pouvez demander l’accès au matériel compatible avec Vulkan et ainsi sélectionner un ou plusieurs VkPhysicalDevice à utiliser. Vous pouvez récupérer des informations telles que la taille de la VRAM ou les fonctionnalités offertes par le périphérique sélectionné et ainsi choisir de travailler avec un matériel adapté pour votre application.
II-B-2. Étape 2 – Périphérique logique et familles de queues▲
Après avoir sélectionné le matériel adéquat, vous devez créer un VkDevice (périphérique logique) au travers duquel vous allez définir les VkPhysicalDeviceFeatures que vous utiliserez. Par exemple, l’affichage multifenêtre ou le support des nombres flottants sur 64 bits. Vous devrez également spécifier quelles vkQueueFamilies (famille de queues) vous utiliserez. La plupart des opérations, comme les commandes d’affichage et les opérations mémoire, sont exécutées de manière asynchrone en les envoyant à une VkQueue. Ces queues sont créées à partir d’une famille de queues. Chaque famille supporte uniquement un sous-ensemble d’opérations. Il pourrait par exemple y avoir des familles différentes pour les graphismes, le calcul et les opérations mémoire. L’existence des familles peut aussi être utilisée comme critère pour la sélection d’un périphérique physique. Il est possible qu’un périphérique supportant Vulkan ne possède aucune fonctionnalité graphique. Toutefois, une carte graphique supportant Vulkan devrait nous offrir le support des opérations qui nous intéressent.
II-B-3. Étape 3 – Surface d’affichage et swap chain▲
À moins que vous ne soyez intéressé que par le rendu hors écran, vous devrez créer une fenêtre pour y afficher votre rendu. Les fenêtres peuvent être créées avec les bibliothèques spécifiques aux différentes plateformes ou avec des bibliothèques telles que GLFW et SDL. Ce tutoriel repose sur GLFW, mais nous verrons cela dans le prochain chapitre.
Nous avons besoin de deux composants pour afficher quoi que ce soit : une surface (VkSurfaceKHR) et une « swap chain » (VkSwapchainKHR). Remarquez le suffixe « KHR », qui indique que ces fonctionnalités font partie d’une extension. La bibliothèque en elle-même est totalement agnostique de la plateforme, nous devons donc utiliser l’extension standard WSI (Window System Interface) pour interagir avec le gestionnaire de fenêtres. La surface est une abstraction multiplateforme de la fenêtre sur laquelle réaliser l’affichage. Elle est généralement créée ) partir d’une référence à une fenêtre native, par exemple un HWND sur Windows. Heureusement, la bibliothèque GLFW possède une fonction permettant de gérer tous les détails spécifiques à la plateforme pour nous.
La « swap chain » est une collection de cibles de rendu. Son but principal est d’assurer que l’image sur laquelle nous travaillons n’est pas celle utilisée par l’écran. C’est important pour s’assurer que l’image affichée est complète. Chaque fois que nous voudrons afficher une image, nous devrons demander à la swap chain de nous fournir une image dans laquelle dessiner. Une fois le rendu effectué, l’image est rendue à la « swap chain » qui la donnera à l’écran au moment voulu. Le nombre de cibles et les conditions d’affichage de l’image finale dépendent du mode de présentation. Les modes les plus communs utilisent deux ou trois tampons (« double buffering » ou « triple buffering »). Nous détaillerons tout cela dans le chapitre dédié à la « swap chain »Swap chain.
Certaines plateformes permettent d'effectuer un rendu directement à l'écran sans passer par un gestionnaire de fenêtres grâce aux extensions VK_KHR_display et VK_KHR_display_swapchain. Celles-ci permettent de créer une surface représentant l’intégralité de l’écran et peuvent être utilisées pour implémenter votre propre gestionnaire de fenêtres.
II-B-4. Étape 4 – Vue d’image et tampons d’image▲
Pour dessiner sur une image provenant de la « swap chain », nous devons l'encapsuler dans une VkImageView (vue d’image ou « image view ») et un VkFramebuffer. Une vue sur une image référence la partie d’une image à utiliser et un tampon d’image référence les vues qui seront utilisées pour les cibles de couleur, de profondeur ou de stencil. Dans la mesure où il peut y avoir de nombreuses images dans la « swap chain », nous créerons en amont les vues et les tampons d’image pour chacune d’entre elles, puis sélectionnerons celles qui nous conviennent au moment de l’affichage.
II-B-5. Étape 5 – Passes de rendu▲
La passe de rendu décrit le type des images utilisé lors des opérations de rendu, comment elles sont utilisées et comment leur contenu doit être traité. Pour l’affichage d’un triangle, nous indiquerons à Vulkan que nous utilisons une seule image pour la couleur et que nous voulons qu’elle soit remplie d’une couleur opaque avant l’affichage. Là où la passe de rendu décrit seulement le type des images, un tampon d’image (VkFramebuffer) lie les images appropriées à la passe.
II-B-6. Étape 6 - Le pipeline graphique▲
Le pipeline graphique est configuré lors de la création d’un VkPipeline. Il décrit les éléments paramétrables de la carte graphique, comme la taille du viewport, les opérations réalisées sur le tampon de profondeur (depth buffer) et les étapes programmables à l’aide de VkShaderModule. Ces derniers sont créés à partir du code intermédiaire généré à partir des shaders. Le pilote doit également être informé des cibles du rendu utilisées dans le pipeline, ce que nous lui donnons en référençant la passe de rendu.
L’une des particularités les plus importantes de Vulkan est que la quasi-totalité de la configuration des étapes doit être réalisée à l’avance. Cela implique que si vous voulez changer un shader ou la disposition des données des sommets alors la totalité du pipeline doit être recréée. Vous aurez donc probablement de nombreux VkPipeline correspondant à toutes les combinaisons dont votre programme aura besoin. Seules quelques configurations basiques peuvent être changées de manière dynamique, comme le viewport ou la couleur de fond. Les états doivent aussi être définis explicitement : il n’y a par exemple pas de fonction de mélange (blending) par défaut.
La bonne nouvelle est que toute cette anticipation est comparable à une compilation en avance contrairement à une compilation juste à temps. Le pilote a plus d’opportunités d’optimisation et les performances à l’exécution sont prédictibles, car tous les changements d’état sont explicites.
II-B-7. Étape 7 – Groupe de commandes et tampons de commandes▲
Comme dit plus haut, de nombreuses opérations telles que les opérations de rendu que nous souhaitons exécuter doivent être envoyées à une queue. Ces opérations doivent d’abord être enregistrées dans un tampon de commandes (VkCommandBuffer) avant de pouvoir être envoyées. Ces tampons de commandes sont alloués à partir d’une VkCommandPool associée à une famille de queues spécifique. Pour afficher un simple triangle nous devrons enregistrer un tampon de commandes avec les opérations suivantes :
- commencer la passe de rendu ;
- lier le pipeline graphique ;
- afficher trois sommets ;
- terminer la passe de rendu.
Sachant que l’image dans le tampon d’image dépend de l’image fournie par la swap chain, nous devons préparer un tampon de commandes pour chaque image possible et choisir la bonne à l’affichage. Nous pourrions enregistrer un tampon de commandes à chaque image, mais ce n’est pas aussi efficace.
II-B-8. Étape 8 - Boucle principale▲
Maintenant que nous avons inscrit les commandes graphiques dans un tampon de commandes, la boucle principale est simple. D’abord, nous acquérons une image fournie par la « swap chain » en utilisant vkAcquireNextImageKHR. Nous sélectionnons ensuite le tampon de commandes adéquat pour cette image et le postons à la queue avec vkQueueSubmit. Enfin, nous retournons l’image à la « swap chain » pour sa présentation à l’écran à l’aide de vkQueuePresentKHR.
Les opérations envoyées à la queue sont exécutées de manière asynchrone. Nous devons donc utiliser des mécanismes de synchronisation tels que des sémaphores pour nous assurer que les opérations sont exécutées dans l’ordre voulu. L’exécution du tampon de commandes pour l’affichage doit être configurée pour attendre la fin de l’acquisition de l’image, sinon nous pourrions dessiner sur une image en cours d’utilisation pour l’affichage. L’appel à vkQueuePresentKHR doit aussi attendre que le rendu soit terminé. Pour cela, nous utilisons un deuxième sémaphore déclenché après la fin du rendu.
II-B-9. Résumé▲
Ce rapide tour devrait vous donner une compréhension basique du travail que nous aurons à fournir pour afficher notre premier triangle. Un véritable programme contient plus d’étapes comme allouer des tampons de sommets, créer les tampons de variables uniformes et envoyer à la carte graphique les images pour les textures, mais nous verrons cela dans des chapitres suivants. Nous allons commencer simplement, car Vulkan a une courbe d’apprentissage abrupte. Notez que nous allons « tricher » en écrivant les coordonnées du triangle directement dans un shader au lieu d’utiliser un tampon de sommets. En effet, la gestion d’un tampon de sommets nécessite quelques familiarités avec les tampons de commandes.
En résumé, pour afficher un triangle, nous devons :
- créer une VkInstance ;
- sélectionner une carte graphique compatible (VkPhysicalDevice) ;
- créer un VkDevice et une VkQueue pour le rendu et la présentation ;
- créer une fenêtre, une surface associée à la fenêtre et une « swap chain » ;
- associer les images de la « swap chain » aux VkImageView ;
- créer la passe de rendu spécifiant les cibles de rendu et leur utilisation ;
- créer des tampons d’image pour ces passes ;
- générer le pipeline graphique ;
- allouer et enregistrer un tampon de commandes contenant les commandes de rendu pour toutes les images de la « swap chain » ;
- dessiner sur les tampons en acquérant une image, puis en soumettant le bon tampon de commandes et en renvoyant l’image à la « swap chain ».
Cela fait beaucoup d’étapes, cependant le but de chacune d’entre elles sera expliqué clairement et simplement dans les chapitres suivants. Si vous êtes confus quant à l’intérêt d’une étape dans le programme entier, référez-vous à ce premier chapitre.
II-C. Concepts de la bibliothèque▲
Ce chapitre va conclure en survolant la structure de la bibliothèque à un plus bas niveau.
II-C-1. Conventions▲
Toutes les fonctions, les énumérations et les structures de Vulkan sont définies dans le fichier d’en-têtes vulkan.h, inclus dans le SDK Vulkan développé par LunarG. Nous verrons comment l’installer dans le prochain chapitre.
Les fonctions sont préfixées par ‘vk’, les types comme les énumérations et les structures par ‘Vk’ et les macros par ‘VK_’. La bibliothèque utilise massivement les structures pour la création d’objets plutôt que de passer des arguments à des fonctions. Par exemple la création d’objets suit généralement le schéma suivant :
VkXXXCreateInfo createInfo =
{}
;
createInfo.sType =
VK_STRUCTURE_TYPE_XXX_CREATE_INFO;
createInfo.pNext =
nullptr
;
createInfo.foo =
...;
createInfo.bar =
...;
VkXXX object;
if
(vkCreateXXX(&
createInfo, nullptr
, &
object) !=
VK_SUCCESS) {
std::
cerr <<
"failed to create object"
<<
std::
endl;
return
false
;
}
De nombreuses structures imposent que l’on spécifie explicitement leur type dans le membre sType. Le membre pNext peut pointer vers une extension de la structure et sera toujours nullptr dans ce tutoriel. Les fonctions qui créent ou détruisent les objets ont un paramètre appelé VkAllocationCallbacks, qui vous permet de spécifier un allocateur. Nous le mettrons également à nullptr.
La plupart des fonctions retournent un VkResult, qui peut être soit VK_SUCCESS soit un code d’erreur. La spécification décrit quelles erreurs sont retournées par chaque fonction et ce qu’elles signifient.
II-C-2. Couches de validation▲
Vulkan est pensé pour la haute performance et pour un travail minimal pour le pilote. Par conséquent, il inclut, par défaut, très peu de gestion d’erreurs et de système de débogage. Le pilote crashera beaucoup plus souvent qu’il ne retournera de code d’erreur si vous faites quelque chose d’incorrect. Pire, il peut fonctionner sur votre carte graphique, mais pas sur une autre.
Cependant, Vulkan vous permet d’activer des vérifications précises à l’aide d’une fonctionnalité nommée couches de validation (validation layers). Ces couches consistent en du code s’insérant entre la bibliothèque et le pilote et permettent de lancer des analyses de mémoire et de relever les défauts. Vous pouvez les activer pendant le développement et les désactiver lors de la mise en production de votre code. Une fois désactivées, il n’y aura aucune conséquence sur la performance. N’importe qui peut écrire ses couches de validation, mais le SDK de LunarG fournit un ensemble de validations que nous allons utiliser dans ce tutoriel. Vous aurez cependant à écrire vos propres fonctions de callback pour récupérer les messages de débogage provenant des couches.
Du fait que Vulkan est explicite pour chaque opération et grâce à l’extensivité des couches de validation, il est plus facile de comprendre pourquoi l’écran est noir qu’avec OpenGL ou Direct3D !
Il reste une dernière étape avant de commencer à coder : mettre en place l’environnement de développementEnvironnement de développement.