Cette publication est dédiée à la traduction de la section Dessiner un triangle, à savoir la sous-section Configuration, les chapitres Code de base et Instance.
Contenu
Code de base
Structure générale
Dans le chapitre précédent, nous vous avons montré comment créer un projet pour Vulkan, le configurer correctement et le tester à l'aide d'un extrait de code. Dans ce chapitre, nous allons commencer par les bases.
Considérez le code suivant:
#include <vulkan/vulkan.h>
#include <iostream>
#include <stdexcept>
#include <cstdlib>
class HelloTriangleApplication {
public:
void run() {
initVulkan();
mainLoop();
cleanup();
}
private:
void initVulkan() {
}
void mainLoop() {
}
void cleanup() {
}
};
int main() {
HelloTriangleApplication app;
try {
app.run();
} catch (const std::exception& e) {
std::cerr << e.what() << std::endl;
return EXIT_FAILURE;
}
return EXIT_SUCCESS;
}
Tout d'abord, nous incluons le fichier d'en-tête Vulkan du SDK LunarG. Fichiers d'en-tête
stdexcepts
et
iostream
sont utilisés pour la gestion et la distribution des erreurs. Le fichier d'en-tête
cstdlib
fournit des macros
EXIT_SUCCESS
et
EXIT_FAILURE
.
Le programme lui-même est enveloppé dans la classe HelloTriangleApplication, dans laquelle nous stockerons les objets Vulkan en tant que membres privés de la classe. Là, nous ajouterons également des fonctions pour initialiser chaque objet, appelé depuis la fonction
initVulkan
. Après cela, créons une boucle principale pour le rendu des images. Pour ce faire, remplissez une fonction
mainLoop
où la boucle sera exécutée jusqu'à ce que la fenêtre soit fermée. Après avoir fermé la fenêtre et quitté, les
mainLoop
ressources doivent être libérées. Pour ce faire, remplissez
cleanup
.
Si une erreur critique se produit pendant l'opération, nous lèverons une exception
std::runtime_error
qui sera interceptée dans la fonction
main
, et la description sera affichée dans
std::cerr
. L'une de ces erreurs peut être, par exemple, un message indiquant que l'extension requise n'est pas prise en charge. Pour gérer de nombreux types d'exceptions standard, nous en attrapons une plus générale
std::exception
.
Presque tous les chapitres suivants ajouteront de nouvelles fonctions appelées depuis
initVulkan
et de nouveaux objets Vulkan qui doivent être publiés à
cleanup
la fin du programme.
La gestion des ressources
Si les objets Vulkan ne sont plus nécessaires, ils doivent être détruits. C ++ vous permet de désallouer automatiquement les ressources à l'aide de RAII ou de pointeurs intelligents fournis par le fichier d'en-tête
<memory>
. Cependant, dans ce tutoriel, nous avons décidé d'écrire explicitement quand allouer et désallouer des objets Vulkan. Après tout, c'est la particularité du travail de Vulkan - décrire en détail chaque opération afin d'éviter d'éventuelles erreurs.
Après avoir lu le didacticiel, vous pouvez implémenter la gestion automatique des ressources en écrivant des classes C ++ qui reçoivent des objets Vulkan dans le constructeur et les désallouent dans le destructeur. Vous pouvez également implémenter votre propre déleter pour
std::unique_ptr
ou
std::shared_ptr
, en fonction de vos besoins. Le concept RAII est recommandé pour les programmes plus importants, mais il est utile d'en savoir plus à ce sujet.
Les objets Vulkan sont créés directement à l'aide d'une fonction telle que vkCreateXXX , ou alloués via un autre objet à l'aide d'une fonction telle que vkAllocateXXX . Après vous être assuré que l'objet n'est utilisé nulle part ailleurs, vous devez le détruire avec vkDestroyXXX ou vkFreeXXX . Les paramètres de ces caractéristiques varient généralement en fonction du type d'objet, mais il y a un paramètre commun:
pAllocator
. Il s'agit d'un paramètre facultatif qui vous permet d'utiliser des rappels pour l'allocation de mémoire personnalisée. Nous n'en aurons pas besoin dans le manuel, nous le passerons comme argument
nullptr
.
Intégration GLFW
Vulkan fonctionne bien sans créer de fenêtre lors de l'utilisation du rendu hors écran, mais bien mieux lorsque le résultat est visible à l'écran.
Tout d'abord, remplacez la ligne par ce qui
#include <vulkan/vulkan.h>
suit:
#define GLFW_INCLUDE_VULKAN #include <GLFW/glfw3.h>
Ajoutez une fonction
initWindow
et ajoutez son appel depuis la méthode
run
avant les autres appels. Nous utiliserons
initWindow
GLFW pour initialiser et créer une fenêtre.
void run() {
initWindow();
initVulkan();
mainLoop();
cleanup();
}
private:
void initWindow() {
}
Le tout premier appel à
initWindow
doit être une fonction
glfwInit()
qui initialise la bibliothèque GLFW. GLFW a été conçu à l'origine pour fonctionner avec OpenGL. Nous n'avons pas besoin d'un contexte OpenGL, alors indiquez que nous n'avons pas besoin de le créer à l'aide de l'appel suivant:
glfwWindowHint(GLFW_CLIENT_API, GLFW_NO_API);
Nous désactiverons temporairement la possibilité de redimensionner la fenêtre, car la gestion de cette situation nécessite un examen séparé:
glfwWindowHint(GLFW_RESIZABLE, GLFW_FALSE);
Il reste à créer une fenêtre. Pour ce faire, ajoutez un membre privé
GLFWwindow* window;
et initialisez la fenêtre avec:
window = glfwCreateWindow(800, 600, "Vulkan", nullptr, nullptr);
Les trois premiers paramètres définissent la largeur, la hauteur et le titre de la fenêtre. Le quatrième paramètre est facultatif, il permet de spécifier le moniteur sur lequel la fenêtre sera affichée. Le dernier paramètre est spécifique à OpenGL.
Ce serait bien d'utiliser des constantes pour la largeur et la hauteur de la fenêtre, car nous aurons besoin de ces valeurs ailleurs. Ajoutez les lignes suivantes avant la définition de classe
HelloTriangleApplication
:
const uint32_t WIDTH = 800;
const uint32_t HEIGHT = 600;
et remplacez l'appel pour créer une fenêtre avec
window = glfwCreateWindow(WIDTH, HEIGHT, "Vulkan", nullptr, nullptr);
Vous devriez avoir la fonction suivante
initWindow
:
void initWindow() {
glfwInit();
glfwWindowHint(GLFW_CLIENT_API, GLFW_NO_API);
glfwWindowHint(GLFW_RESIZABLE, GLFW_FALSE);
window = glfwCreateWindow(WIDTH, HEIGHT, "Vulkan", nullptr, nullptr);
}
Décrivons la boucle principale de la méthode
mainLoop
pour maintenir l'application en cours d'exécution jusqu'à la fermeture de la fenêtre:
void mainLoop() {
while (!glfwWindowShouldClose(window)) {
glfwPollEvents();
}
}
Ce code ne devrait pas soulever de questions. Il gère les événements tels que l'appui sur le bouton X avant que l'utilisateur ne ferme la fenêtre. Aussi à partir de cette boucle, nous allons appeler une fonction pour rendre des images individuelles.
Après avoir fermé la fenêtre, nous devons libérer des ressources et quitter GLFW. Tout d'abord, ajoutons au
cleanup
code suivant:
void cleanup() {
glfwDestroyWindow(window);
glfwTerminate();
}
En conséquence, après le démarrage du programme, vous verrez une fenêtre avec un nom
Vulkan
qui sera affiché jusqu'à ce que le programme soit fermé. Maintenant que nous avons un squelette pour travailler avec Vulkan, passons à la création de notre premier objet Vulkan!
Exemple
Instanciation
La première chose à faire est de créer une instance pour initialiser la bibliothèque. Une instance est le lien entre votre programme et la bibliothèque Vulkan, et pour la créer, vous devrez fournir au pilote des informations sur votre programme.
Ajoutez une méthode
createInstance
et appelez-la depuis une fonction
initVulkan
.
void initVulkan() { createInstance(); }
Ajoutez un membre d'instance à notre classe pour contenir un handle d'instance:
private:
VkInstance instance;
Maintenant, nous devons remplir une structure spéciale avec des informations sur le programme. Techniquement, les données sont facultatives, cependant, cela permettra au pilote d'obtenir des informations utiles pour optimiser le travail avec votre programme. Cette structure s'appelle
VkApplicationInfo
:
void createInstance() {
VkApplicationInfo appInfo{};
appInfo.sType = VK_STRUCTURE_TYPE_APPLICATION_INFO;
appInfo.pApplicationName = "Hello Triangle";
appInfo.applicationVersion = VK_MAKE_VERSION(1, 0, 0);
appInfo.pEngineName = "No Engine";
appInfo.engineVersion = VK_MAKE_VERSION(1, 0, 0);
appInfo.apiVersion = VK_API_VERSION_1_0;
}
Comme mentionné, de nombreuses structures dans Vulkan nécessitent une définition de type explicite dans le membre sType . De plus, cette structure, comme beaucoup d'autres, contient un élément
pNext
qui vous permet de fournir des informations pour les extensions. Nous utilisons l'initialisation de la valeur pour remplir la structure avec des zéros.
La plupart des informations de Vulkan passent par des structures, vous devez donc remplir une autre structure pour fournir suffisamment d'informations pour créer une instance. La structure suivante est requise, elle indique au pilote les extensions globales et les couches de validation que nous voulons utiliser. «Global» signifie que les extensions s'appliquent à l'ensemble du programme et non à un appareil spécifique.
VkInstanceCreateInfo createInfo{}; createInfo.sType = VK_STRUCTURE_TYPE_INSTANCE_CREATE_INFO; createInfo.pApplicationInfo = &appInfo;
Les deux premiers paramètres ne soulèvent aucune question. Les deux membres suivants définissent les extensions globales requises. Comme vous le savez déjà, l'API Vulkan est totalement indépendante de la plateforme. Cela signifie que vous avez besoin d'une extension pour interagir avec le système de fenêtrage. GLFW a une fonction intégrée pratique qui renvoie une liste d'extensions requises.
uint32_t glfwExtensionCount = 0;
const char** glfwExtensions;
glfwExtensions = glfwGetRequiredInstanceExtensions(&glfwExtensionCount);
createInfo.enabledExtensionCount = glfwExtensionCount;
createInfo.ppEnabledExtensionNames = glfwExtensions;
Les deux derniers membres de la structure définissent les couches de validation globale à inclure. Nous en parlerons plus en détail dans le chapitre suivant, alors laissez ces valeurs vides pour le moment.
createInfo.enabledLayerCount = 0;
Vous avez maintenant fait tout le nécessaire pour créer une instance. Passer un appel
vkCreateInstance
:
VkResult result = vkCreateInstance(&createInfo, nullptr, &instance);
En règle générale, les paramètres des fonctions de création d'objets sont dans cet ordre:
- Pointeur vers une structure avec les informations requises
- Pointeur vers l'allocateur personnalisé
- Pointeur vers la variable où le descripteur du nouvel objet sera écrit
Si tout est fait correctement, le descripteur d'instance sera stocké dans l' instance . Presque toutes les fonctions Vulkan renvoient une valeur VkResult , qui peut être soit un
VK_SUCCESS
code d'erreur. Nous n'avons pas besoin de stocker le résultat pour nous assurer que l'instance a été créée. Utilisons une simple vérification:
if (vkCreateInstance(&createInfo, nullptr, &instance) != VK_SUCCESS) {
throw std::runtime_error("failed to create instance!");
}
Exécutez maintenant le programme pour vérifier que l'instance a été créée avec succès.
Vérification des extensions prises en charge
Si nous regardons la documentation Vulkan , nous pouvons constater que l'un des codes d'erreur possibles est
VK_ERROR_EXTENSION_NOT_PRESENT
. Nous pouvons simplement spécifier les extensions requises et arrêter de travailler si elles ne sont pas prises en charge. Cela a du sens pour les extensions majeures comme l'interface du système de fenêtrage, mais que faire si nous voulons tester les fonctionnalités optionnelles?
Pour obtenir une liste des extensions prises en charge avant l'instanciation, utilisez la fonction vkEnumerateInstanceExtensionProperties... Le premier paramètre de la fonction est facultatif, il vous permet de filtrer les extensions par une couche de validation spécifique, nous allons donc le laisser vide pour le moment. En outre, la fonction nécessite un pointeur vers une variable où le nombre d'extensions sera écrit et un pointeur vers une zone mémoire où les informations les concernant doivent être écrites.
Pour allouer de la mémoire pour stocker les informations d'extension, vous devez d'abord connaître le nombre d'extensions. Laissez le dernier paramètre vide pour demander le nombre d'extensions:
uint32_t extensionCount = 0;
vkEnumerateInstanceExtensionProperties(nullptr, &extensionCount, nullptr);
Allouez un tableau pour stocker les informations d'extension (n'oubliez pas
include <vector>
):
std::vector<VkExtensionProperties> extensions(extensionCount);
Vous pouvez désormais demander des informations sur les extensions.
vkEnumerateInstanceExtensionProperties(nullptr, &extensionCount, extensions.data());
Chaque structure VkExtensionProperties contient le nom et la version de l'extension. Ils peuvent être listés avec une simple boucle for (
\t
voici l'onglet d'indentation):
std::cout << "available extensions:\n";
for (const auto& extension : extensions) {
std::cout << '\t' << extension.extensionName << '\n';
}
Vous pouvez ajouter ce code à une fonction
createInstance
pour plus d'informations sur le support Vulkan. Vous pouvez également essayer de créer une fonction qui vérifiera si toutes les extensions renvoyées par la fonction
glfwGetRequiredInstanceExtensions
sont incluses dans la liste des extensions prises en charge.
Nettoyage
VkInstance doit être détruit avant de fermer le programme. Cela peut être fait en
cleanup
utilisant la fonction VkDestroyInstance :
void cleanup() {
vkDestroyInstance(instance, nullptr);
glfwDestroyWindow(window);
glfwTerminate();
}
Les paramètres de la fonction vkDestroyInstance sont explicites. Comme mentionné dans le chapitre précédent, les fonctions d'allocation et de désallocation de Vulkan acceptent des pointeurs optionnels vers des allocateurs personnalisés que nous n'utilisons pas et que nous transmettons
nullptr
. Toutes les autres ressources Vulkan doivent être nettoyées avant que l'instance ne soit détruite.
Avant de passer à des étapes plus complexes, nous devons configurer les couches de validation pour faciliter le débogage.
Code C ++