Découvrir Vulkan et son architecture

Vulkan vous intéresse et pour démarrer, quoi de mieux que de découvrir comment est architecturée la bibliothèque.

8 commentaires Donner une note à l'article (5)

Article lu   fois.

L'auteur

Profil ProSite personnel

Liens sociaux

Viadeo Twitter Facebook Share on Google+   

I. Introduction

Vulkan est le nom de la nouvelle bibliothèque de hautes performances pour le GPU. Pour en savoir plus sur ce qu'est Vulkan, il est conseillé de lire la présentation réalisée en 2015.

Cet article a pour but de plonger un peu plus en profondeur dans l'architecture de Vulkan afin d'offrir un premier aperçu sur la façon dont a été pensée la nouvelle bibliothèque. En effet, par rapport à OpenGL, beaucoup de choses ont changé et cela, pour le meilleur.

II. Objectifs de Vulkan

Les objectifs de Vulkan sont clairs : se débarrasser de l'héritage d'OpenGL et permettre l'utilisation de la puissance totale de la machine. Pour cela, plusieurs choses ont été réalisées :

  • Vulkan ne possède pas de machine à états globale permettant ainsi l'utilisation de l'ensemble des threads CPU ;
  • les synchronisations CPU-GPU sont gérées par le développeur. Ainsi, le pilote n'a plus besoin de prendre de précautions pour éviter les race conditions ;
  • la mémoire est entièrement gérée par le développeur. Ainsi, le nombre d'allocations peut être diminué et le pilote ne fait pas d'hypothèse sur les besoins du développeur ;
  • la vérification d'erreurs a été fortement diminuée. Certes une mauvaise utilisation peut amener à un plantage. Par contre, le pilote n'utilise pas les performances pour faire des vérifications inutiles dans un produit final.

III. Objets Vulkan

La bibliothèque Vulkan introduit de nouveaux objets représentant au mieux l'architecture de la carte graphique :

  • Device : un « device » (périphérique en français) permet de récupérer les informations de la carte graphique et de créer la majorité des objets Vulkan ;
  • Heap : un périphérique embarque une quantité de mémoire limitée. C'est à partir du tas que vous pouvez allouer de la mémoire ;
  • Memory : mémoire pour les tampons et images. Ceux-ci sont liés aux objets mémoire suivant la préférence et les besoins du développeur. Il est possible d'allouer un grand bloc de mémoire, puis de le segmenter pour différentes ressources ;
  • Queue : une queue représente un fil d'opérations qui sera traité par la carte graphique. La carte graphique peut traiter plusieurs queues en parallèle, mais assure que les opérations dans une queue seront effectuées dans l'ordre ;
  • CommandBuffer : tampon de commandes dans lequel on insère des opérations affectant le rendu (définir les états, afficher des tampons de sommets, copier des tampons…) ;
  • CommandBufferPool : objet à partir duquel il est possible d'allouer les tampons de commandes et leurs contenus ;
  • Image : données formatées disposées en grille (textures, cible de rendu…). Elles sont équivalentes aux textures OpenGL ;
  • FrameBuffer : un ensemble d'images dans lesquelles le rendu est effectué. Le tampon de rendu doit correspondre à la configuration de la passe de rendu ;
  • RenderPass : encode le format des tampons de rendu attachés, le type de nettoyage, si on effectue des effets multipasses, les dépendances entre les passes… ;
  • Buffer : tampon de données brutes, linéaires tels les sommets, les index, les données uniformes. Équivalent aux tampons OpenGL ;
  • DescriptorSet : un ensemble de liaisons avec les entrées de shaders ;
  • DescriptorSetLayout : l'objet décrit quels liens sont compris dans le DescriptorSet et cela pour tous les shaders d'un rendu. C'est au développeur de s'assurer que les shaders (SPIR-V) ont une définition compatible avec le DescriptorSet ;
  • DescriptorPool : objet à partir duquel il est possible d'allouer les DescriptorSet ;
  • Pipeline : encode un état de rendu, tels les shaders utilisés, le test de profondeur, les opérations de fondu… ;
  • PipelineLayout : sachant qu'un Pipeline peut avoir plusieurs DescriptorSet, cet objet détermine quel DescriptorSet est utilisé avec quel numéro d'ensemble de liens :
  • Uniform Buffer Binding : équivalent à glBindBufferRange(GL_UNIFORM_BUFFER, dset.binding, dset.bufferOffset, dset.bufferSize) et directement encodé dans le DescriptorSet ;
  • Uniform Buffer Dynamic Binding : similaire aux Uniform Buffer Binding, mais permet de donner l'offset lors de son enregistrement dans le tampon de commandes ;
  • Push Constants : valeurs uniformes stockées dans le tampon de commandes pouvant être accédées globalement à partir des shaders. Similaire à glProgramEnvParameter.

IV. Commandes de rendu

Contrairement à OpenGL où les commandes de changement d'état et de rendu ont un effet immédiat, Vulkan propose des tampons dans lesquels les commandes sont placées, puis envoyées à la queue pour être traitées.

Image non disponible

Même s'il peut y avoir un coût à la création lors de la configuration du tampon, la soumission à la queue est rapide.

Il est possible de construire et d'envoyer plusieurs tampons de commandes en parallèle. De plus, il est possible de les réutiliser. Cela s'avère très pratique pour redessiner une scène possédant plusieurs shadow-map, ou encore pour l'image de gauche et de droite pour un casque de réalité virtuelle. Cette réutilisation réduit l'utilisation du CPU et permet donc un rendu plus rapide.

Deux types de tampons de commandes existent : les tampons primaires et les tampons secondaires. Les tampons primaires contiennent toujours la configuration de la passe de rendu. Par contre, les autres informations peuvent être soit directement encodées, soit fournies par un tampon de commandes secondaire. Il n'y a pas d'héritage d'états entre les tampons. Seulement, le tampon secondaire utilise l'image active définie par le tampon primaire.

En conclusion, le tampon de commandes peut représenter une scène. Celle-ci peut toujours être animée sachant que les données telles que les matrices ou les sommets sont contenues dans des tampons séparés. En effet, le tampon de commandes référence ces données, mais ne les contient pas.

Image non disponible

V. Allocation mémoire

Dans Vulkan, l'allocation mémoire est entièrement laissée aux développeurs. Par exemple, le tampon de commandes et le DescriptorSet sera construit à partir d'une memory pool dédiée alors que les tampons et images seront alloués à partir du tas.

Image non disponible

L'objectif des memory pool est de permettre une allocation et libération sans verrou et sans surcoût de la part du système. Chaque thread pourra ainsi avoir sa memory pool et donc se réserver l'accès intégral à celle-ci. Il est ainsi possible d'utiliser un CommandBufferPool pour chaque image et une fois qu'une image est complétée, de réutiliser celui-ci pour une autre.

En laissant l'utilisateur gérer sa mémoire, il est aussi possible de veiller à la continuité de la mémoire, de partager la même zone, ou encore de fournir son propre allocateur de mémoire dynamique.

VI. Liaison de ressources

La liaison de ressources permet de connecter les entrées des shaders avec le reste des ressources que vous avez placées dans la mémoire de la carte graphique. Avec Vulkan les liens formés peuvent être rassemblés dans un groupe, appelé DescriptorSet.

Le DescriptorSet va être décrit par les DescriptorSetLayout : quelles ressources seront liées dans ce groupe. Ensuite, grâce aux PipelineLayout, le Pipeline saura quel ensemble utiliser pour le rendu.

Image non disponible

L'avantage de cette nouvelle architecture est de permettre une plus grande liberté, mais aussi une plus grande réutilisation au sein d'un rendu. Généralement, le rendu est implémenté comme suit :

 
Sélectionnez
// exemple des boucles typiques d'un rendu
pour chaque vue {
  lier les ressources de la vue          // caméra, environnement...
  pour chaque shader {
    lier le pipeline du shader
    lier les ressources du shader      // valeurs pour le shader
    pour chaque matériau {
      lier les ressources du matériau  // paramètres des matériaux et textures
      pour chaque objet {
        lier les ressources de l'objet  // transformations de l'objet
        dessiner l'objet
      }
    }
  }
}

Avec Vulkan, vous pouvez indiquer au pilote que les ressources liées à la vue sont communes à tous les shaders et partagent la même entrée. Ainsi, la bibliothèque n'a pas besoin de faire de vérification supplémentaire.

Ci-dessous, un exemple montrant que le DescriptorSet reste actif tant que l'entrée indiquée par le PipelineLayout est adéquate :

Image non disponible

Dans un autre cas d'école, l'application peut changer très fréquemment les données d'entrée du shader. Vulkan apporte aussi une solution à cela en permettant d'envoyer un large tampon de variables uniformes, mais pour lequel vous pouvez spécifier l'offset au moment de l'enregistrement dans le tampon de commandes. De plus, il existe des Push Constants, directement encodées dans le tampon de commandes et qui peuvent être considérées comme des variables globales à tous les shaders. Cette mémoire est certes très limitée, mais peut suffire pour enregistrer des matrices ou des index. L'interprétation est déterminée par le shader directement.

Image non disponible

VII. Ressources

VIII. Remerciements

Je remercie dragonjoker59 pour sa relecture et ClaudeLELOUP pour ses corrections orthographiques.

Vous avez aimé ce tutoriel ? Alors partagez-le en cliquant sur les boutons suivants : Viadeo Twitter Facebook Share on Google+   

En complément sur Developpez.com

  

Les sources présentées sur cette page sont libres de droits et vous pouvez les utiliser à votre convenance. Par contre, la page de présentation constitue une œuvre intellectuelle protégée par les droits d'auteur. Copyright © 2016 Alexandre Laurent. Aucune reproduction, même partielle, ne peut être faite de ce site et de l'ensemble de son contenu : textes, documents, images, etc. sans l'autorisation expresse de l'auteur. Sinon vous encourez selon la loi jusqu'à trois ans de prison et jusqu'à 300 000 € de dommages et intérêts.