Command buffers
Les commandes Vulkan, comme les opérations d'affichage et de transfert mémoire, ne sont pas réalisées avec des appels de fonctions. Il faut pré-enregistrer toutes les opérations dans des command buffers. L'avantage est que vous pouvez préparer tout ce travail à l'avance et depuis plusieurs threads, puis vous contenter d'indiquer à Vulkan quel command buffer doit être exécuté. Cela réduit considérablement la bande passante entre le CPU et le GPU et améliore grandement les performances.
Command pools
Nous devons créer une command pool avant de pouvoir créer les command buffers. Les command pools gèrent la mémoire
utilisée par les buffers, et c'est de fait les command pools qui nous instancient les command buffers. Ajoutez un
nouveau membre donnée à la classe de type VkCommandPool
:
VkCommandPool commandPool;
Créez ensuite la fonction createCommandPool
et appelez-la depuis initVulkan
après la création du framebuffer.
void initVulkan() {
createInstance();
setupDebugMessenger();
createSurface();
pickPhysicalDevice();
createLogicalDevice();
createSwapChain();
createImageViews();
createRenderPass();
createGraphicsPipeline();
createFramebuffers();
createCommandPool();
}
...
void createCommandPool() {
}
La création d'une command pool ne nécessite que deux paramètres :
QueueFamilyIndices queueFamilyIndices = findQueueFamilies(physicalDevice);
VkCommandPoolCreateInfo poolInfo{};
poolInfo.sType = VK_STRUCTURE_TYPE_COMMAND_POOL_CREATE_INFO;
poolInfo.queueFamilyIndex = queueFamilyIndices.graphicsFamily.value();
poolInfo.flags = 0; // Optionel
Les commands buffers sont exécutés depuis une queue, comme la queue des graphismes et de présentation que nous avons récupérées. Une command pool ne peut allouer des command buffers compatibles qu'avec une seule famille de queues. Nous allons enregistrer des commandes d'affichage, c'est pourquoi nous avons récupéré une queue de graphismes.
Il existe deux valeurs acceptées par flags
pour les command pools :
-
VK_COMMAND_POOL_CREATE_TRANSIENT_BIT
: informe que les command buffers sont ré-enregistrés très souvent, ce qui peut inciter Vulkan (et donc le driver) à ne pas utiliser le même type d'allocation -
VK_COMMAND_POOL_CREATE_RESET_COMMAND_BUFFER_BIT
: permet aux command buffers d'être ré-enregistrés individuellement, ce que les autres configurations ne permettent pas
Nous n'enregistrerons les command buffers qu'une seule fois au début du programme, nous n'aurons donc pas besoin de ces fonctionnalités.
if (vkCreateCommandPool(device, &poolInfo, nullptr, &commandPool) != VK_SUCCESS) {
throw std::runtime_error("échec de la création d'une command pool!");
}
Terminez la création de la command pool à l'aide de la fonction vkCreateComandPool
. Elle ne comprend pas de
paramètre particulier. Les commandes seront utilisées tout au long du programme pour tout affichage, nous ne devons
donc la détruire que dans la fonction cleanup
:
void cleanup() {
vkDestroyCommandPool(device, commandPool, nullptr);
...
}
Allocation des command buffers
Nous pouvons maintenant allouer des command buffers et enregistrer les commandes d'affichage. Dans la mesure où l'une
des commandes consiste à lier un framebuffer nous devrons les enregistrer pour chacune des images de la swap chain.
Créez pour cela une liste de VkCommandBuffer
et stockez-la dans un membre donnée de la classe. Les command buffers
sont libérés avec la destruction de leur command pool, nous n'avons donc pas à faire ce travail.
std::vector<VkCommandBuffer> commandBuffers;
Commençons maintenant à travailler sur notre fonction createCommandBuffers
qui allouera et enregistrera les command
buffers pour chacune des images de la swap chain.
void initVulkan() {
createInstance();
setupDebugMessenger();
createSurface();
pickPhysicalDevice();
createLogicalDevice();
createSwapChain();
createImageViews();
createRenderPass();
createGraphicsPipeline();
createFramebuffers();
createCommandPool();
createCommandBuffers();
}
...
void createCommandBuffers() {
commandBuffers.resize(swapChainFramebuffers.size());
}
Les command buffers sont alloués par la fonction vkAllocateCommandBuffers
qui prend en paramètre une structure du
type VkCommandBufferAllocateInfo
. Cette structure spécifie la command pool et le nombre de buffers à allouer depuis
celle-ci :
VkCommandBufferAllocateInfo allocInfo{};
allocInfo.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_ALLOCATE_INFO;
allocInfo.commandPool = commandPool;
allocInfo.level = VK_COMMAND_BUFFER_LEVEL_PRIMARY;
allocInfo.commandBufferCount = (uint32_t) commandBuffers.size();
if (vkAllocateCommandBuffers(device, &allocInfo, commandBuffers.data()) != VK_SUCCESS) {
throw std::runtime_error("échec de l'allocation de command buffers!");
}
Les command buffers peuvent être primaires ou secondaires, ce que l'on indique avec le paramètre level
. Il peut
prendre les valeurs suivantes :
-
VK_COMMAND_BUFFER_LEVEL_PRIMARY
: peut être envoyé à une queue pour y être exécuté mais ne peut être appelé par d'autres command buffers -
VK_COMMAND_BUFFER_LEVEL_SECONDARY
: ne peut pas être directement émis à une queue mais peut être appelé par un autre command buffer
Nous n'utiliserons pas la fonctionnalité de command buffer secondaire ici. Sachez que le mécanisme de command buffer secondaire est à la base de la génération rapie de commandes d'affichage depuis plusieurs threads.
Début de l'enregistrement des commandes
Nous commençons l'enregistrement des commandes en appelant vkBeginCommandBuffer
. Cette fonction prend une petite
structure du type VkCommandBufferBeginInfo
en argument, permettant d'indiquer quelques détails sur l'utilisation du
command buffer.
for (size_t i = 0; i < commandBuffers.size(); i++) {
VkCommandBufferBeginInfo beginInfo{};
beginInfo.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_BEGIN_INFO;
beginInfo.flags = 0; // Optionnel
beginInfo.pInheritanceInfo = nullptr; // Optionel
if (vkBeginCommandBuffer(commandBuffers[i], &beginInfo) != VK_SUCCESS) {
throw std::runtime_error("erreur au début de l'enregistrement d'un command buffer!");
}
}
L'utilisation du command buffer s'indique avec le paramètre flags
, qui peut prendre les valeurs suivantes :
-
VK_COMMAND_BUFFER_USAGE_ONE_TIME_SUBMIT_BIT
: le command buffer sera ré-enregistré après son utilisation, donc invalidé une fois son exécution terminée -
VK_COMMAND_BUFFER_USAGE_RENDER_PASS_CONTINUE_BIT
: ce command buffer secondaire sera intégralement exécuté dans une unique render pass -
VK_COMMAND_BUFFER_USAGE_SIMULTANEOUS_USE_BIT
: le command buffer peut être ré-envoyé à la queue alors qu'il y est déjà et/ou est en cours d'exécution
Nous n'avons pas besoin de ces flags ici.
Le paramètre pInheritanceInfo
n'a de sens que pour les command buffers secondaires.
Il indique l'état à hériter de l'appel par le command buffer primaire.
Si un command buffer est déjà prêt un appel à vkBeginCommandBuffer
le regénèrera implicitement. Il n'est pas possible
d'enregistrer un command buffer en plusieurs fois.
Commencer une render pass
L'affichage commence par le lancement de la render pass réalisé par vkCmdBeginRenderPass
. La passe est configurée
à l'aide des paramètres remplis dans une structure de type VkRenderPassBeginInfo
.
VkRenderPassBeginInfo renderPassInfo{};
renderPassInfo.sType = VK_STRUCTURE_TYPE_RENDER_PASS_BEGIN_INFO;
renderPassInfo.renderPass = renderPass;
renderPassInfo.framebuffer = swapChainFramebuffers[i];
Ces premiers paramètres sont la render pass elle-même et les attachements à lui fournir. Nous avons créé un framebuffer pour chacune des images de la swap chain qui spécifient ces images comme attachements de couleur.
renderPassInfo.renderArea.offset = {0, 0};
renderPassInfo.renderArea.extent = swapChainExtent;
Les deux paramètres qui suivent définissent la taille de la zone de rendu. Cette zone de rendu définit où les chargements et stockages shaders se produiront. Les pixels hors de cette région auront une valeur non définie. Elle doit correspondre à la taille des attachements pour avoir une performance optimale.
VkClearValue clearColor = {{{0.0f, 0.0f, 0.0f, 1.0f}}};
renderPassInfo.clearValueCount = 1;
renderPassInfo.pClearValues = &clearColor;
Les deux derniers paramètres définissent les valeurs à utiliser pour remplacer le contenu (fonctionnalité que nous
avions activée avec VK_ATTACHMENT_LOAD_CLEAR
). J'ai utilisé un noir complètement opaque.
vkCmdBeginRenderPass(commandBuffers[i], &renderPassInfo, VK_SUBPASS_CONTENTS_INLINE);
La render pass peut maintenant commencer. Toutes les fonctions enregistrables se reconnaisent à leur préfixe vkCmd
.
Comme elles retournent toutes void
nous n'avons aucun moyen de gérer d'éventuelles erreurs avant d'avoir fini
l'enregistrement.
Le premier paramètre de chaque commande est toujours le command buffer qui stockera l'appel. Le second paramètre donne des détails sur la render pass à l'aide de la structure que nous avons préparée. Le dernier paramètre informe sur la provenance des commandes pendant l'exécution de la passe. Il peut prendre ces valeurs :
-
VK_SUBPASS_CONTENTS_INLINE
: les commandes de la render pass seront inclues directement dans le command buffer (qui est donc primaire) -
VK_SUBPASS_CONTENTS_SECONDARY_COMMAND_BUFFER
: les commandes de la render pass seront fournies par un ou plusieurs command buffers secondaires
Nous n'utiliserons pas de command buffer secondaire, nous devons donc fournir la première valeur à la fonction.
Commandes d'affichage basiques
Nous pouvons maintenant activer la pipeline graphique :
vkCmdBindPipeline(commandBuffers[i], VK_PIPELINE_BIND_POINT_GRAPHICS, graphicsPipeline);
Le second paramètre indique que la pipeline est bien une pipeline graphique et non de calcul. Nous avons fourni à Vulkan les opérations à exécuter avec la pipeline graphique et les attachements que le fragment shader devra utiliser. Il ne nous reste donc plus qu'à lui dire d'afficher un triangle :
vkCmdDraw(commandBuffers[i], 3, 1, 0, 0);
Le fonction vkCmdDraw
est assez ridicule quand on sait tout ce qu'elle implique, mais sa simplicité est due
à ce que tout a déjà été préparé en vue de ce moment tant attendu. Elle possède les paramètres suivants en plus du
command buffer concerné :
-
vertexCount
: même si nous n'avons pas de vertex buffer, nous avons techniquement trois vertices à dessiner -
instanceCount
: sert au rendu instancié (instanced rendering); indiquez1
si vous ne l'utilisez pas -
firstVertex
: utilisé comme décalage dans le vertex buffer et définit ainsi la valeur la plus basse pourglVertexIndex
-
firstInstance
: utilisé comme décalage pour l'instanced rendering et définit ainsi la valeur la plus basse pourgl_InstanceIndex
Finitions
La render pass peut ensuite être terminée :
vkCmdEndRenderPass(commandBuffers[i]);
Et nous avons fini l'enregistrement du command buffer :
if (vkEndCommandBuffer(commandBuffers[i]) != VK_SUCCESS) {
throw std::runtime_error("échec de l'enregistrement d'un command buffer!");
}
Dans le prochain chapitre nous écrirons le code pour la boucle principale. Elle récupérera une image de la swap chain, exécutera le bon command buffer et retournera l'image complète à la swap chain.