Window surface
Introduction
Vulkan ignore la plateforme sur laquelle il opère et ne peut donc pas directement établir d'interface avec le
gestionnaire de fenêtres. Pour créer une interface permettant de présenter les rendus à l'écran, nous devons utiliser
l'extension WSI (Window System Integration). Nous verrons dans ce chapitre l'extension VK_KHR_surface
, l'une des
extensions du WSI. Nous pourrons ainsi obtenir l'objet VkSurfaceKHR
, qui est un type abstrait de surface sur
lequel nous pourrons effectuer des rendus. Cette surface sera en lien avec la fenêtre que nous avons créée grâce à GLFW.
L'extension VK_KHR_surface
, qui se charge au niveau de l'instance, a déjà été ajoutée, car elle fait partie des
extensions retournées par la fonction glfwGetRequiredInstanceExtensions
. Les autres fonctions WSI que nous verrons
dans les prochains chapitres feront aussi partie des extensions retournées par cette fonction.
La surface de fenêtre doit être créée juste après l'instance car elle peut influencer le choix du physical device. Nous ne nous intéressons à ce sujet que maintenant car il fait partie du grand ensemble que nous abordons et qu'en parler plus tôt aurait été confus. Il est important de noter que cette surface est complètement optionnelle, et vous pouvez l'ignorer si vous voulez effectuer du rendu off-screen ou du calculus. Vulkan vous offre ces possibilités sans vous demander de recourir à des astuces comme créer une fenêtre invisible, là où d'autres APIs le demandaient (cf OpenGL).
Création de la window surface
Commencez par ajouter un membre donnée surface
sous le messager.
VkSurfaceKHR surface;
Bien que l'utilisation d'un objet VkSurfaceKHR
soit indépendant de la plateforme, sa création ne l'est pas.
Celle-ci requiert par exemple des références à HWND
et à HMODULE
sous Windows. C'est pourquoi il existe des
extensions spécifiques à la plateforme, dont par exemple VK_KHR_win32_surface
sous Windows, mais celles-ci sont
aussi évaluées par GLFW et intégrées dans les extensions retournées par la fonction glfwGetRequiredInstanceExtensions
.
Nous allons voir l'exemple de la création de la surface sous Windows, même si nous n'utiliserons pas cette méthode.
Il est en effet contre-productif d'utiliser une librairie comme GLFW et un API comme Vulkan pour se retrouver à écrire
du code spécifique à la plateforme. La fonction de GLFW glfwCreateWindowSurface
permet de gérer les différences de
plateforme. Cet exemple ne servira ainsi qu'à présenter le travail de bas niveau, dont la connaissance est toujours
utile à une bonne utilisation de Vulkan.
Une window surface est un objet Vulkan comme un autre et nécessite donc de remplir une structure, ici
VkWin32SurfaceCreateInfoKHR
. Elle possède deux paramètres importants : hwnd
et hinstance
. Ce sont les références
à la fenêtre et au processus courant.
VkWin32SurfaceCreateInfoKHR createInfo{};
createInfo.sType = VK_STRUCTURE_TYPE_WIN32_SURFACE_CREATE_INFO_KHR;
createInfo.hwnd = glfwGetWin32Window(window);
createInfo.hinstance = GetModuleHandle(nullptr);
Nous pouvons extraire HWND
de la fenêtre à l'aide de la fonction glfwGetWin32Window
. La fonction
GetModuleHandle
fournit une référence au HINSTANCE
du thread courant.
La surface peut maintenant être crée avec vkCreateWin32SurfaceKHR
. Cette fonction prend en paramètre une instance, des
détails sur la création de la surface, l'allocateur optionnel et la variable dans laquelle placer la référence. Bien que
cette fonction fasse partie d'une extension, elle est si communément utilisée qu'elle est chargée par défaut par Vulkan.
Nous n'avons ainsi pas à la charger à la main :
if (vkCreateWin32SurfaceKHR(instance, &createInfo, nullptr, &surface) != VK_SUCCESS) {
throw std::runtime_error("échec de la creation d'une window surface!");
}
Ce processus est similaire pour Linux, où la fonction vkCreateXcbSurfaceKHR
requiert la fenêtre et une connexion à
XCB comme paramètres pour X11.
La fonction glfwCreateWindowSurface
implémente donc tout cela pour nous et utilise le code correspondant à la bonne
plateforme. Nous devons maintenant l'intégrer à notre programme. Ajoutez la fonction createSurface
et appelez-la
dans initVulkan
après la création de l'instance et du messager :
void initVulkan() {
createInstance();
setupDebugMessenger();
createSurface();
pickPhysicalDevice();
createLogicalDevice();
}
void createSurface() {
}
L'appel à la fonction fournie par GLFW ne prend que quelques paramètres au lieu d'une structure, ce qui rend le tout très simple :
void createSurface() {
if (glfwCreateWindowSurface(instance, window, nullptr, &surface) != VK_SUCCESS) {
throw std::runtime_error("échec de la création de la window surface!");
}
}
Les paramètres sont l'instance, le pointeur sur la fenêtre, l'allocateur optionnel et un pointeur sur une variable de
type VkSurfaceKHR
. GLFW ne fournit aucune fonction pour détruire cette surface mais nous pouvons le faire
nous-mêmes avec une simple fonction Vulkan :
void cleanup() {
...
vkDestroySurfaceKHR(instance, surface, nullptr);
vkDestroyInstance(instance, nullptr);
...
}
Détruisez bien la surface avant l'instance.
Demander le support pour la présentation
Bien que l'implémentation de Vulkan supporte le WSI, il est possible que d'autres éléments du système ne le supportent
pas. Nous devons donc allonger isDeviceSuitable
pour s'assurer que le logical device puisse présenter les
rendus à la surface que nous avons créée. La présentation est spécifique aux queues families, ce qui signifie que
nous devons en fait trouver une queue family supportant cette présentation.
Il est possible que les queue families supportant les commandes d'affichage et celles supportant les commandes de
présentation ne soient pas les mêmes, nous devons donc considérer que ces deux queues sont différentes. En fait, les
spécificités des queues families diffèrent majoritairement entre les vendeurs, et assez peu entre les modèles d'une même
série. Nous devons donc étendre la structure QueueFamilyIndices
:
struct QueueFamilyIndices {
std::optional<uint32_t> graphicsFamily;
std::optional<uint32_t> presentFamily;
bool isComplete() {
return graphicsFamily.has_value() && presentFamily.has_value();
}
};
Nous devons ensuite modifier la fonction findQueueFamilies
pour qu'elle cherche une queue family pouvant supporter
les commandes de présentation. La fonction qui nous sera utile pour cela est vkGetPhysicalDeviceSurfaceSupportKHR
.
Elle possède quatre paramètres, le physical device, un indice de queue family, la surface et un booléen. Appelez-la
depuis la même boucle que pour VK_QUEUE_GRAPHICS_BIT
:
VkBool32 presentSupport = false;
vkGetPhysicalDeviceSurfaceSupportKHR(device, i, surface, &presentSupport);
Vérifiez simplement la valeur du booléen et stockez la queue dans la structure si elle est intéressante :
if (presentSupport) {
indices.presentFamily = i;
}
Il est très probable que ces deux queue families soient en fait les mêmes, mais nous les traiterons comme si elles étaient différentes pour une meilleure compatibilité. Vous pouvez cependant ajouter un alorithme préférant des queues combinées pour améliorer légèrement les performances.
Création de la queue de présentation (presentation queue)
Il nous reste à modifier la création du logical device pour extraire de celui-ci la référence à une presentation queue
VkQueue
. Ajoutez un membre donnée pour cette référence :
VkQueue presentQueue;
Nous avons besoin de plusieurs structures VkDeviceQueueCreateInfo
, une pour chaque queue family. Une manière de
gérer ces structures est d'utiliser un set
contenant tous les indices des queues et un vector
pour les structures :
#include <set>
...
QueueFamilyIndices indices = findQueueFamilies(physicalDevice);
std::vector<VkDeviceQueueCreateInfo> queueCreateInfos;
std::set<uint32_t> uniqueQueueFamilies = {indices.graphicsFamily.value(), indices.presentFamily.value()};
float queuePriority = 1.0f;
for (uint32_t queueFamily : uniqueQueueFamilies) {
VkDeviceQueueCreateInfo queueCreateInfo{};
queueCreateInfo.sType = VK_STRUCTURE_TYPE_DEVICE_QUEUE_CREATE_INFO;
queueCreateInfo.queueFamilyIndex = queueFamily;
queueCreateInfo.queueCount = 1;
queueCreateInfo.pQueuePriorities = &queuePriority;
queueCreateInfos.push_back(queueCreateInfo);
}
Puis modifiez VkDeviceCreateInfo
pour qu'il pointe sur le contenu du vector :
createInfo.queueCreateInfoCount = static_cast<uint32_t>(queueCreateInfos.size());
createInfo.pQueueCreateInfos = queueCreateInfos.data();
Si les queues sont les mêmes, nous n'avons besoin de les indiquer qu'une seule fois, ce dont le set s'assure. Ajoutez enfin un appel pour récupérer les queue families :
vkGetDeviceQueue(device, indices.presentFamily.value(), 0, &presentQueue);
Si les queues sont les mêmes, les variables contenant les références contiennent la même valeur. Dans le prochain chapitre nous nous intéresserons aux swap chain, et verrons comment elle permet de présenter les rendus à l'écran.