VI. Tampons de variables uniformes▲
VI-A. Descripteur d’agencement et de tampon▲
VI-A-1. Introduction▲
Nous pouvons maintenant passer des attributs différents pour chaque sommet au vertex shader, mais qu’en est il des variables globales ? À partir de ce chapitre, nous allons effectuer un rendu 3D :. cela nécessite une matrice de modèle-vue-projection. Nous pouvons l’inclure comme données de sommet, mais c’est un énorme gâchis de mémoire. De plus, il faudrait mettre à jour le tampon de sommets à chaque fois que nous voulons modifier la transformation : c’est-à-dire à chaque image !
La solution fournie par Vulkan consiste à utiliser des descripteurs de ressource (resource descriptors). Un descripteur est un moyen pour les shaders d’obtenir un accès libre aux ressources telles que des tampons ou des images. Nous allons configurer un tampon qui contiendra les matrices de transformation. Le vertex shader pourra y accéder grâce à un descripteur. Leur mise en place se fait en trois parties :
- spécifier l’agencement du descripteur (descriptor layout) durant la création du pipeline ;
- allouer un ensemble de descripteurs (descriptor set) depuis un groupe de descripteurs (descriptor pool) ;
- lier l’ensemble de descripteurs durant les opérations de rendu.
L’agencement du descripteur indique le type de ressources auquel le pipeline pourra accéder. Cela fonctionne de manière similaire à ce que nous avons fait pour les attaches auxquelles la passe de rendu doit accéder. L’ensemble de descripteurs indique le tampon ou les images qui seront liées aux descripteurs. Cela fonctionne comme le tampon d’images qui spécifie les vues d’images à lier aux attaches de la passe de rendu. L’ensemble de descripteurs est ensuite lié aux commandes de rendu, tout comme les tampons de sommets et le tampon d’images.
Il existe plusieurs types de descripteurs, mais dans ce chapitre, nous travaillerons avec les tampons de variables uniformes (uniform buffer objects (UBO)). Nous verrons les autres types de descripteurs plus tard, mais le processus de mise en place est le même. Partons du principe que les données que nous voulons envoyer au vertex shader sont stockées dans une structure C comme suit :
struct
UniformBufferObject {
glm::
mat4 model;
glm::
mat4 view;
glm::
mat4 proj;
}
;
Nous devons copier les données dans un objet de type VkBuffer et y accéder par le biais d’un descripteur d’objet de tampon de variables uniformes dans le vertex shader :
layout
(
binding =
0
) uniform
UniformBufferObject {
mat4
model;
mat4
view;
mat4
proj;
}
ubo;
void
main
(
) {
gl_Position
=
ubo.proj *
ubo.view *
ubo.model *
vec4
(
inPosition, 0
.0
, 1
.0
);
fragColor =
inColor;
}
Nous allons mettre à jour les matrices de modèle, vue et projection à chaque image afin de faire tourner le rectangle dans une scène 3D.
VI-A-2. Vertex shader▲
Modifiez le vertex shader pour inclure les variables uniformes comme décrit plus haut. Je pars du principe que vous connaissez les transformations de modèle, vue et projection. Si ce n'est pas le cas, vous pouvez lire ce tutoriel.
#
version
450
#
extension
GL_ARB_separate_shader_objects : enable
layout
(
binding =
0
) uniform
UniformBufferObject {
mat4
model;
mat4
view;
mat4
proj;
}
ubo;
layout
(
location =
0
) in
vec2
inPosition;
layout
(
location =
1
) in
vec3
inColor;
layout
(
location =
0
) out
vec3
fragColor;
void
main
(
) {
gl_Position
=
ubo.proj *
ubo.view *
ubo.model *
vec4
(
inPosition, 0
.0
, 1
.0
);
fragColor =
inColor;
}
Notez que l'ordre des variables uniform, in et out n'a aucune importance. La directive binding est semblable à la directive location pour les attributs. Nous référençons ce binding dans l’agencement du descripteur. La ligne concernant la variable gl_Position a été modifiée pour utiliser les transformations dans le calcul permettant d’obtenir la position finale dans l’espace de coordonnées de découpage. Contrairement aux triangles 2D, le dernier composant de la coordonnée peut ne pas être 1. Par conséquent, une division aura bien lieu lors du passage aux coordonnées normalisées pour l’écran. Cette division de perspective permet de faire que les objets les plus proches sont plus gros que les objets au loin.
VI-A-3. Agencement de l’ensemble de descripteurs▲
La prochaine étape consiste à définir l'UBO côté C++. Nous devons aussi informer Vulkan que nous voulons l'utiliser dans le vertex shader.
struct
UniformBufferObject {
glm::
mat4 model;
glm::
mat4 view;
glm::
mat4 proj;
}
;
Nous pouvons faire correspondre parfaitement la déclaration C++ avec celle du shader grâce aux types fournis par GLM. Les données dans les matrices sont binairement compatibles avec ce qui est attendu par les shaders. Ainsi, nous pouvons utiliser la fonction memcpy() pour copier l’objet UniformBufferObject dans un VkBuffer.
Nous devons fournir des informations sur chacun des descripteurs utilisés par les shaders lors de la création du pipeline, tout comme nous le faisons pour chaque attribut de sommet. Évidemment, nous devons aussi spécifier leur indice pour correspondre à la directive binding. Nous allons mettre en place une fonction nommée createDescriptorSetLayout() ayant ce rôle. La fonction doit être appelée avant la création du pipeline.
void
initVulkan() {
...
createDescriptorSetLayout();
createGraphicsPipeline();
...
}
...
void
createDescriptorSetLayout() {
}
Chaque lien (binding) doit être décrit grâce à la structure de type VkDescriptorSetLayoutBinding.
void
createDescriptorSetLayout() {
VkDescriptorSetLayoutBinding uboLayoutBinding{}
;
uboLayoutBinding.binding =
0
;
uboLayoutBinding.descriptorType =
VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER;
uboLayoutBinding.descriptorCount =
1
;
}
Les deux premières propriétés indiquent le binding spécifié dans le shader et le type du descripteur, c’est-à-dire un tampon de variables uniformes. Il est possible que la variable du shader soit un tableau d'UBO. Pour ce cas, la propriété descriptorCount indique le nombre d’éléments dans le tableau. Cette possibilité pourrait être utilisée pour transmettre la transformation à appliquer à chaque os d’un squelette pour effectuer une animation. Notre transformation modèle, vue, projection ne consiste qu’en un objet UBO. Par conséquent, la valeur de la propriété descriptorCount est 1.
uboLayoutBinding.stageFlags =
VK_SHADER_STAGE_VERTEX_BIT;
Nous devons aussi indiquer à Vulkan dans quelles étapes programmables les descripteurs seront référencés. Le champ de bits stageFlags peut être une combinaison des valeurs VkShaderStageFlagBits ou la valeur VK_SHADER_STAGE_ALL_GRAPHICS. Nous utilisons ce descripteur uniquement dans le vertex shader.
uboLayoutBinding.pImmutableSamplers =
nullptr
; // Optionnel
La propriété pImmutableSamplers n'est utile que pour les descripteurs en rapport avec l’échantillonnage des images que nous verrons plus tard. Nous laissons donc la valeur par défaut.
Tous les liens de descripteurs sont ensuite combinés en un seul objet de type VkDescriptorSetLayout correspondant à l’agencement des descripteurs. Créez pour cela une nouvelle variable membre nommée pipelineLayout :
VkDescriptorSetLayout descriptorSetLayout;
VkPipelineLayout pipelineLayout;
Nous pouvons créer cet objet grâce à la fonction vkCreateDescriptorSetLayout(). Cette fonction prend en argument une structure de type VkDescriptorSetLayoutCreateInfo contenant un tableau avec les liens :
VkDescriptorSetLayoutCreateInfo layoutInfo{}
;
layoutInfo.sType =
VK_STRUCTURE_TYPE_DESCRIPTOR_SET_LAYOUT_CREATE_INFO;
layoutInfo.bindingCount =
1
;
layoutInfo.pBindings =
&
uboLayoutBinding;
if
(vkCreateDescriptorSetLayout(device, &
layoutInfo, nullptr
, &
descriptorSetLayout) !=
VK_SUCCESS) {
throw
std::
runtime_error("Échec lors de la création de l’agencement des descripteurs !"
);
}
Nous devons fournir cette structure à Vulkan durant la création du pipeline graphique afin que les shaders puissent utiliser les descripteurs. Les agencements des descripteurs sont spécifiés dans l’agencement du pipeline graphique. Modifiez la structure VkPipelineLayoutCreateInfo pour référencer le nouvel objet :
VkPipelineLayoutCreateInfo pipelineLayoutInfo =
{}
;
pipelineLayoutInfo.sType =
VK_STRUCTURE_TYPE_PIPELINE_LAYOUT_CREATE_INFO;
pipelineLayoutInfo.setLayoutCount =
1
;
pipelineLayoutInfo.pSetLayouts =
&
descriptorSetLayout;
Vous pouvez vous demander pourquoi il est possible de spécifier plusieurs agencements de descripteurs alors qu’un unique objet inclut toutes les liaisons. Nous allons revenir sur ce point plus tard, quand nous allons détailler les groupes de descripteurs et les ensembles de descripteurs.
L'objet doit persister tant que nous créons des pipelines graphiques, autrement dit, jusqu’à la fin du programme :
void
cleanup() {
cleanupSwapChain();
vkDestroyDescriptorSetLayout(device, descriptorSetLayout, nullptr
);
...
}
VI-A-4. Tampon de variables uniformes▲
Dans le prochain chapitre, nous allons spécifier le tampon contenant les données UBO pour le shader. Toutefois, nous devons d’abord créer un tampon. À chaque image, nous allons copier des données différentes dans le tampon, il est donc contre-productif d’utiliser un tampon intermédiaire. Cela ajouterait de la complexité et dégraderait les performances.
Nous avons besoin de plusieurs tampons, car nous pouvons traiter plusieurs rendus en parallèle et nous ne souhaitons pas mettre à jour un tampon qui est toujours en cours de lecture pour le rendu précédent. Nous pouvons soit en avoir un par rendu, soit un par image de la « swap chain ». Comme nous devons référencer un tampon de variables uniformes à partir du tampon de commandes qui lui-même est distinct pour chaque image de la « swap chain », il est donc logique d’avoir un tampon de variables uniformes pour chaque image.
Pour cela, créez les variables membres uniformBuffers et uniformBuffersMemory :
VkBuffer indexBuffer;
VkDeviceMemory indexBufferMemory;
std::
vector<
VkBuffer>
uniformBuffers;
std::
vector<
VkDeviceMemory>
uniformBuffersMemory;
Par ailleurs, créez une nouvelle fonction nommée createUniformBuffers() et appelez-la après la fonction createIndexBuffers(). Son but est d’allouer les tampons :
void
initVulkan() {
...
createVertexBuffer();
createIndexBuffer();
createUniformBuffers();
...
}
...
void
createUniformBuffers() {
VkDeviceSize bufferSize =
sizeof
(UniformBufferObject);
uniformBuffers.resize(swapChainImages.size());
uniformBuffersMemory.resize(swapChainImages.size());
for
(size_t i =
0
; i <
swapChainImages.size(); i++
) {
createBuffer(bufferSize, VK_BUFFER_USAGE_UNIFORM_BUFFER_BIT, VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT |
VK_MEMORY_PROPERTY_HOST_COHERENT_BIT, uniformBuffers[i], uniformBuffersMemory[i]);
}
}
Nous allons créer une autre fonction qui mettra à jour le tampon avec la nouvelle transformation, et ce, à chaque rendu. C’est pourquoi il n’y a pas d’appel à la fonction vkMapMemory() ici. Les données uniformes seront utilisées dans tous les rendus. Par conséquent, le tampon contenant les variables uniformes doit être détruit après l’arrêt du rendu. Comme le tampon dépend du nombre d’images fournies par la « swap chain », nous devons le modifier après la reconstruction de la « swap chain ». Nous allons donc faire le nettoyage dans la fonction cleanupSwapChain() :
void
cleanupSwapChain() {
...
for
(size_t i =
0
; i <
swapChainImages.size(); i++
) {
vkDestroyBuffer(device, uniformBuffers[i], nullptr
);
vkFreeMemory(device, uniformBuffersMemory[i], nullptr
);
}
}
Le tampon doit donc être recrée dans la fonction recreateSwapChain() :
void
recreateSwapChain() {
...
createFramebuffers();
createUniformBuffers();
createCommandBuffers();
}
VI-A-5. Mise à jour des variables uniformes▲
Créez une nouvelle fonction nommée updateUniformBuffer() et appelez-la dans la fonction drawFrame(), juste après avoir obtenu une image de la « swap chain » :
void
drawFrame() {
...
uint32_t imageIndex;
VkResult result =
vkAcquireNextImageKHR(device, swapChain, UINT64_MAX, imageAvailableSemaphores[currentFrame], VK_NULL_HANDLE, &
imageIndex);
...
updateUniformBuffer(imageIndex);
VkSubmitInfo submitInfo{}
;
submitInfo.sType =
VK_STRUCTURE_TYPE_SUBMIT_INFO;
...
}
...
void
updateUniformBuffer(uint32_t currentImage) {
}
Cette fonction génère une transformation à chaque rendu permettant de tourner le modèle 3D. Nous devons inclure deux nouveaux fichiers d’en-têtes pour implémenter cette fonctionnalité :
#define GLM_FORCE_RADIANS
#include
<glm/glm.hpp>
#include
<glm/gtc/matrix_transform.hpp>
#include
<chrono>
Le fichier d’en-tête <glm/gtc/matrix_transform.hpp> fournit des fonctions permettant de générer des matrices de transformations. Nous avons besoin des fonctions glm::rotate() pour la matrice du modèle, glm::lookAt() pour la matrice de vue et glm::perspective() pour la matrice de projection. La macro GLM_FORCE_RADIANS assure l’utilisation des radians pour les angles, notamment ceux passés en paramètre à la fonction glm::rotate().
La bibliothèque standard chrono fournit des fonctions liées à la mesure du temps. Nous allons l’utiliser pour implémenter une rotation fluide de 90 degrés par seconde, et ce, quel que soit le nombre d’images par seconde :
void
updateUniformBuffer(uint32_t currentImage) {
static
auto
startTime =
std::chrono::high_resolution_clock::
now();
auto
currentTime =
std::chrono::high_resolution_clock::
now();
float
time =
std::chrono::
duration<
float
, std::chrono::seconds::
period>
(currentTime -
startTime).count();
}
La fonction updateUniformBuffer() commence par la logique pour calculer le temps en secondes depuis le début du rendu.
Ensuite, nous définissons les matrices de modèle, vue et projection que nous stockons dans le tampon de variables uniformes. La matrice du modèle représente une simple rotation sur l’axe Z suivant la variable time :
UniformBufferObject ubo{}
;
ubo.model =
glm::
rotate(glm::
mat4(1.0
f), time *
glm::
radians(90.0
f), glm::
vec3(0.0
f, 0.0
f, 1.0
f));
La fonction glm::rotate() accepte en argument une matrice déjà existante, un angle de rotation et un axe de rotation. Le constructeur glm::mat4(1.0) crée une matrice identité. Avec la multiplication time * glm::radians(90.0f) l’objet tournera avec une vitesse de 90 degrés par seconde.
ubo.view =
glm::
lookAt(glm::
vec3(2.0
f, 2.0
f, 2.0
f), glm::
vec3(0.0
f, 0.0
f, 0.0
f), glm::
vec3(0.0
f, 0.0
f, 1.0
f));
J’ai décidé de placer la vue 45° au-dessus de l’objet. La fonction glm::lookAt prend en arguments la position de l'oeil, la direction du regard et l'axe servant de référence pour le haut.
ubo.proj =
glm::
perspective(glm::
radians(45.0
f), swapChainExtent.width /
(float
) swapChainExtent.height, 0.1
f, 10.0
f);
J'ai opté pour un champ de vision vertical de 45 degrés. Les autres paramètres de la fonction glm::perspective() sont le ratio et les plans proche et lointain. Il est important d'utiliser la zone d’échange de la « swap chain » en cours pour calculer le ratio, afin d'utiliser des valeurs à jour suite aux redimensionnements de la fenêtre.
ubo.proj[1
][1
] *=
-
1
;
La bibliothèque GLM a été initialement conçue pour OpenGL, qui utilise des coordonnées de découpage inversé pour l’axe Y. La manière la plus simple de compenser cela consiste à changer le signe de l'axe Y dans la matrice de projection. Si vous ne le faites pas, l’image sera retournée.
Maintenant que nous avons toutes les transformations, nous pouvons copier les données dans le tampon de variables uniformes. Pour ce faire, nous faisons comme pour les tampons de sommets, mais sans tampon intermédiaire :
void
*
data;
vkMapMemory(device, uniformBuffersMemory[currentImage], 0
, sizeof
(ubo), 0
, &
data);
memcpy(data, &
ubo, sizeof
(ubo));
vkUnmapMemory(device, uniformBuffersMemory[currentImage]);
Utiliser un UBO de cette manière n'est pas ce qu’il y a de plus efficace pour transmettre au shader des données fréquemment mises à jour. Le mieux est d’utiliser des constantes poussées (push constant) que nous aborderons dans un futur chapitre.
Dans le chapitre suivant, nous allons mettre en place les ensembles de descripteurs qui vont lier les objets de type VkBuffer aux descripteurs de tampon de variables uniformes afin que le shader puisse accéder aux transformations.
VI-B. Groupe de descripteurs et ensembles▲
VI-B-1. Introduction▲
L’agencement de descripteurs du chapitre précédent décrit le type des descripteurs que nous pouvons lier. Dans ce chapitre, nous allons créer un ensemble de descripteurs pour chaque ressource de type VkBuffer à lier au descripteur de tampon de variables uniformes.
VI-B-2. Groupe de descripteurs▲
Les ensembles de descripteurs ne peuvent pas être créés directement. Il faut les allouer depuis un groupe (pool), comme pour les tampons de commandes. Nous allons mettre en place une fonction nommée createDescriptorPool() pour configurer un groupe de descripteurs.
void
initVulkan() {
...
createUniformBuffers();
createDescriptorPool();
...
}
...
void
createDescriptorPool() {
}
Nous devons d'abord indiquer les types des descripteurs contenus dans le groupe ainsi que leur nombre. Pour cela, nous utilisons une structure du type VkDescriptorPoolSize :
VkDescriptorPoolSize poolSize{}
;
poolSize.type =
VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER;
poolSize.descriptorCount =
static_cast
<
uint32_t>
(swapChainImages.size());
Nous allons allouer un descripteur pour chaque image. Cette structure est référencée dans la structure principale VkDescriptorPoolCreateInfo.
VkDescriptorPoolCreateInfo poolInfo{}
;
poolInfo.sType =
VK_STRUCTURE_TYPE_DESCRIPTOR_POOL_CREATE_INFO;
poolInfo.poolSizeCount =
1
;
poolInfo.pPoolSizes =
&
poolSize;
En plus du nombre maximal de descripteurs, nous devons aussi spécifier le nombre maximum d’ensembles de descripteurs pouvant être alloués :
poolInfo.maxSets =
static_cast
<
uint32_t>
(swapChainImages.size());;
La structure possède un indicateur optionnel, similaire à celui des groupes de commandes, pour indiquer si des ensembles individuels de descripteurs peuvent être libérés ou non : VK_DESCRIPTOR_POOL_CREATE_FREE_DESCRIPTOR_SET_BIT. Nous n’allons pas toucher l’ensemble après sa création. Cet indicateur est donc inutile et nous laissons la valeur par défaut.
VkDescriptorPool descriptorPool;
...
if
(vkCreateDescriptorPool(device, &
poolInfo, nullptr
, &
descriptorPool) !=
VK_SUCCESS) {
throw
std::
runtime_error("Échec de création du groupe de descripteurs !"
);
}
Ajoutez une nouvelle variable membre pour stocker la référence au groupe de descripteurs et appelez la fonction vkCreateDescriptPool() pour créer le groupe. Le groupe doit être détruit lorsque la « swap chain » est reconstruite, car le groupe dépend du nombre d’images :
void
cleanupSwapChain() {
...
for
(size_t i =
0
; i <
swapChainImages.size(); i++
) {
vkDestroyBuffer(device, uniformBuffers[i], nullptr
);
vkFreeMemory(device, uniformBuffersMemory[i], nullptr
);
}
vkDestroyDescriptorPool(device, descriptorPool, nullptr
);
}
Le groupe doit être recréé dans la fonction recreateSwapChain() :
void
recreateSwapChain() {
...
createUniformBuffers();
createDescriptorPool();
createCommandBuffers();
}
VI-B-3. Ensemble de descripteurs▲
Nous pouvons maintenant allouer les ensembles de descripteurs. Ajoutez une fonction nommée createDescriptorSets() :
void
initVulkan() {
...
createDescriptorPool();
createDescriptorSets();
...
}
void
recreateSwapChain() {
...
createDescriptorPool();
createDescriptorSets();
...
}
...
void
createDescriptorSets() {
}
L'allocation de cette ressource est définie par la structure de type VkDescriptorSetAllocateInfo. Vous devez indiquer le groupe de descripteurs à partir duquel faire l’allocation, le nombre d’ensembles à créer et l’agencement de ceux-ci :
std::
vector<
VkDescriptorSetLayout>
layouts(swapChainImages.size(), descriptorSetLayout);
VkDescriptorSetAllocateInfo allocInfo{}
;
allocInfo.sType =
VK_STRUCTURE_TYPE_DESCRIPTOR_SET_ALLOCATE_INFO;
allocInfo.descriptorPool =
descriptorPool;
allocInfo.descriptorSetCount =
static_cast
<
uint32_t>
(swapChainImages.size());
allocInfo.pSetLayouts =
layouts.data();
Dans notre cas, nous créons autant d’ensembles de descripteurs qu'il y a d'images dans la « swap chain ». Ils auront tous le même agencement. Malheureusement, nous devons copier l’agencement plusieurs fois, car la fonction suivante prend un tableau ayant la même taille que le nombre d’ensembles à créer.
Ajoutez une variable membre pour stocker les références des ensembles et allouez-les avec la fonction vkAllocateDescriptorSets() :
VkDescriptorPool descriptorPool;
std::
vector<
VkDescriptorSet>
descriptorSets;
...
descriptorSets.resize(swapChainImages.size());
if
(vkAllocateDescriptorSets(device, &
allocInfo, descriptorSets.data()) !=
VK_SUCCESS) {
throw
std::
runtime_error("Échec lors de l’allocation des ensembles de descripteurs !"
);
}
Vous n’avez pas besoin de libérer les ensembles de descripteurs manuellement. Ils seront libérés lors de la destruction du groupe de descripteurs. L’appel à la fonction vkAllocateDescriptorSets() alloue les ensembles de descripteurs, chacun possédant un descripteur de tampon de variable uniforme.
Les ensembles de descripteurs sont alloués, mais les descripteurs à l’intérieur doivent être configurés. Nous allons ajouter une boucle pour définir la configuration de chaque descripteur :
for
(size_t i =
0
; i <
swapChainImages.size(); i++
) {
}
Les descripteurs référant à un tampon, comme c’est le cas pour notre descripteur de tampon de variables uniformes, sont configurés avec une structure de type VkDescriptorBufferInfo. La structure indique le tampon et la région dans ce tampon contenant les données pour le descripteur.
for
(size_t i =
0
; i <
swapChainImages.size(); i++
) {
VkDescriptorBufferInfo bufferInfo{}
;
bufferInfo.buffer =
uniformBuffers[i];
bufferInfo.offset =
0
;
bufferInfo.range =
sizeof
(UniformBufferObject);
}
Si vous écrasez l’intégralité du tampon, comme nous le faisons ici, il est aussi possible d’utiliser la valeur VK_WHOLE_SIZE pour la propriété range. La configuration des descripteurs se met à jour avec la fonction vkUpdateDescriptorSets(). Elle prend, en paramètre, un tableau contenant des instances de la structure VkWriteDescriptorSet.
VkWriteDescriptorSet descriptorWrite{}
;
descriptorWrite.sType =
VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET;
descriptorWrite.dstSet =
descriptorSets[i];
descriptorWrite.dstBinding =
0
;
descriptorWrite.dstArrayElement =
0
;
Les deux premières propriétés spécifient l’ensemble de descripteurs à mettre à jour et l'indice du lien auquel il correspond. Nous spécifions 0, car nous n’avons qu’une unique liaison de tampon de variables uniformes. Souvenez-vous que les descripteurs peuvent être des tableaux ; nous devons donc aussi indiquer l’indice du tableau à partir duquel nous voulons effectuer des modifications. Nous n’utilisons pas de tableau, donc nous spécifions 0.
descriptorWrite.descriptorType =
VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER;
descriptorWrite.descriptorCount =
1
;
Encore une fois, nous devons indiquer le type du descripteur. Il est possible de mettre à jour plusieurs descripteurs à la fois grâce à un tableau. La mise à jour commence à partir de l’index dstArrayElement. La propriété descriptCount indique le nombre d’éléments dans le tableau que nous voulons mettre à jour.
descriptorWrite.pBufferInfo =
&
bufferInfo;
descriptorWrite.pImageInfo =
nullptr
; // Optionnel
descriptorWrite.pTexelBufferView =
nullptr
; // Optionnel
Le dernier champ référence un tableau de descriptorCount éléments représentant les configurations des descripteurs. La propriété à utiliser parmi les trois dépend du type du descripteur. Le champ pBufferInfo s’utilise avec les descripteurs référençant un tampon de données, pImageInfo s’utilise avec des descripteurs référençant des données images et pTexelBufferView s’utilise avec des descripteurs référençant des vues de tampons. Notre descripteur repose sur les tampons, donc nous utilisons le champ pBufferInfo.
vkUpdateDescriptorSets(device, 1
, &
descriptorWrite, 0
, nullptr
);
Les mises à jour sont appliquées avec la fonction vkUpdateDescriptorSets(). La fonction accepte deux tableaux, un tableau contenant des instances du type VkWriteDesciptorSets et un second contenant des instances de type VkCopyDescriptorSet. Comme son nom l’indique, le deuxième permet de copier des descripteurs.
VI-B-4. Utiliser des ensembles de descripteurs▲
Nous devons maintenant modifier la fonction createCommandBuffers() pour lier le bon ensemble de descripteurs suivant l’image de la « swap chain » aux descripteurs présents dans le shader grâce à la fonction vkCmdBindDescriptorSets(). Cela doit être fait avant l’appel à la fonction vkCmdDrawIndexed().
vkCmdBindDescriptorSets(commandBuffers[i], VK_PIPELINE_BIND_POINT_GRAPHICS, pipelineLayout, 0
, 1
, &
descriptorSets[i], 0
, nullptr
);
vkCmdDrawIndexed(commandBuffers[i], static_cast
<
uint32_t>
(indices.size()), 1
, 0
, 0
, 0
);
Contrairement aux tampons de sommets ou d’indices, les ensembles de descripteurs ne sont pas spécifiques aux pipelines graphiques. Par conséquent, nous devons indiquer à quel pipeline lier nos ensembles de descripteurs. Le paramètre suivant correspond à l’agencement sur lequel les descripteurs reposent. Les trois paramètres suivants sont l’index du premier ensemble de descripteurs, le nombre d’ensembles à lier et le tableau des ensembles à lier. Nous allons revenir sur ce point dans un instant. Les deux derniers paramètres permettent de spécifier un tableau de décalage à utiliser pour les descripteurs dynamiques. Nous y reviendrons aussi dans un futur chapitre.
Si vous lancez le programme, vous verrez que rien ne s'affiche. Le problème est que l'inversion de la coordonnée Y dans la matrice de projection fait que les sommets sont dessinés dans le sens inverse des aiguilles d’une montre. Par conséquent, l’étape de suppression des faces, configurée pour supprimer les faces arrière (backface culling), supprime nos triangles. Allez dans la fonction createGraphicsPipeline() et modifiez le paramètre frontFace de la structure VkPipelineRasterizationStateCreateInfo pour corriger cela :
rasterizer.cullMode =
VK_CULL_MODE_BACK_BIT;
rasterizer.frontFace =
VK_FRONT_FACE_COUNTER_CLOCKWISE;
Maintenant, vous devriez voir ceci en lançant votre programme :
Le rectangle est maintenant un carré, car la matrice de projection corrige son aspect. La fonction updateUniformBuffer() gère le redimensionnement de la fenêtre, il n'est donc pas nécessaire de recréer les descripteurs dans la fonction recreateSwapChain().
VI-B-5. Alignement▲
Jusqu'à présent, nous avons ignoré si les données des structures C++ correspondent avec la définition des variables uniformes du shader. Cela semble évident d’utiliser les mêmes types dans les deux déclarations :
struct
UniformBufferObject {
glm::
mat4 model;
glm::
mat4 view;
glm::
mat4 proj;
}
;
layout(binding =
0
) uniform UniformBufferObject {
mat4 model;
mat4 view;
mat4 proj;
}
ubo;
Pourtant, ce n'est pas aussi simple. Essayez par exemple de modifier la structure et le shader comme suit :
struct
UniformBufferObject {
glm::
vec2 foo;
glm::
mat4 model;
glm::
mat4 view;
glm::
mat4 proj;
}
;
layout(binding =
0
) uniform UniformBufferObject {
vec2 foo;
mat4 model;
mat4 view;
mat4 proj;
}
ubo;
Recompilez les shaders et relancez le programme. Le carré coloré a disparu ! Cela se produit, car nous avons ignoré les alignements mémoire des structures.
Vulkan attend que vos structures soient alignées d’une certaine façon :
- les nombres scalaires doivent être alignés sur N (soit quatre octets pour les nombres flottant sur 32 bits) ;
- un vec2 doit être aligné sur 2N (huit octets) ;
- un vec3 ou vec4 doit être aligné sur 4N (16 octets) ;
- une structure imbriquée doit être alignée suivant la somme des alignements de ses membres arrondie sur le multiple de 16 supérieur ;
- une matrice mat4 doit avoir le même alignement qu’un vec4.
Vous pouvez trouver la liste complète des alignements dans la spécification.
Notre premier shader spécifiait trois champs de type mat4 et correspondait donc aux règles d’alignements sus-citées. Chaque variable de type mat4 s’aligne sur 4 octets, soit 4 * 4 * 4 = 64 octets. La matrice modèle se situe à l’octet 0, la matrice de vue à l’octet 64 et la matrice de projection à l’octet 128. Toutes les matrices ont un décalage multiple de 16 faisant que tout va bien.
La nouvelle structure débute avec une variable de type vec2 stockée sur 8 octets et provoquant donc un décalage de toutes les autres variables. La matrice modèle se situe à l’octet 8, la matrice vue à l’octet 72 et la matrice de projection à l’octet 136. Aucun des décalages n’est multiple de 16 dans ce cas. Pour corriger ce problème, vous pouvez utiliser le mot clef alignas introduit avec le C++11 :
struct
UniformBufferObject {
glm::
vec2 foo;
alignas
(16
) glm::
mat4 model;
glm::
mat4 view;
glm::
mat4 proj;
}
;
Si vous recompilez et relancez le programme vous devriez constater que le shader reçoit correctement les matrices.
Heureusement, il existe une méthode pour ne pas avoir à penser à ces problématiques d’alignement la plupart du temps. Nous pouvons définir la macro GLM_FORCE_DEFAULT_ALIGNED_GENTYPES avant d’inclure le fichier d’en-tête de GLM :
#define GLM_FORCE_RADIANS
#define GLM_FORCE_DEFAULT_ALIGNED_GENTYPES
#include
<glm/glm.hpp>
Ainsi, nous forçons la bibliothèque GLM à utiliser une version des types vec2 et mat4 ayant le même alignement mémoire que Vulkan. Si vous ajoutez cette macro, vous pouvez supprimer le mot clef alignas de la structure et votre programme fonctionnera toujours.
Malheureusement, cette méthode n’est pas suffisante si vous utilisez des structures imbriquées. Prenons l'exemple suivant :
struct
Foo {
glm::
vec2 v;
}
;
struct
UniformBufferObject {
Foo f1;
Foo f2;
}
;
Et pour le shader, utilisons :
struct
Foo {
vec2 v;
}
;
layout(binding =
0
) uniform UniformBufferObject {
Foo f1;
Foo f2;
}
ubo;
Dans ce cas, la variable f2 se situe à l’octet 8 alors qu’elle devrait être à l’octet 16, car c’est une structure imbriquée. Dans ce cas, vous devez indiquer vous-même l’alignement :
struct
UniformBufferObject {
Foo f1;
alignas
(16
) Foo f2;
}
;
À cause de ce genre de problème, il est préférable de toujours expliciter l’alignement. De cette manière, vous ne serez pas pris au dépourvu par des comportements étranges liés à des erreurs d’alignement.
struct
UniformBufferObject {
alignas
(16
) glm::
mat4 model;
alignas
(16
) glm::
mat4 view;
alignas
(16
) glm::
mat4 proj;
}
;
N’oubliez pas de recompiler le shader avec avoir supprimé le champ foo.
VI-B-6. Plusieurs ensembles de descripteurs▲
Comme nous avons pu le voir, certaines fonctions permettent de lier plusieurs ensembles de descripteurs à la fois. Vous devez spécifier un agencement de descripteur pour chaque ensemble lors de la création de l’agencement du pipeline. Les shaders peuvent référencer un ensemble spécifique de cette façon :
layout(set =
0
, binding =
0
) uniform UniformBufferObject {
... }
Vous pouvez utiliser cette fonctionnalité pour utiliser des descripteurs variant par objet et avoir des descripteurs référencés par plusieurs ensembles. Dans ce cas, vous évitez de relier la plupart de vos descripteurs lors des appels de rendu, ce qui peut être plus performant.