vendredi 20 janvier 2017

Comment utiliser Vulkan à partir de Gtk3 sous GNU/Linux?

Salutations,

Je suis en train de me demander comment utiliser les librairies Vulkan directement dans Gtk.

Ce qui m'intéresse pour le moment, c'est Gtk3. Je sais que la "nouvelle" librairie possède des fonctionnalités intéressante pour ce que je cherche mais en ce moment, mon ordinateur ainsi que beaucoup d'entre vous ont un système basé sur Gtk3, donc il va falloir chercher un peu.


Donc, si on se réfère à la doc officielle du Vulkan, on peut remarque que cette librairie est prévue d'origine pour fonctionner avec Xlib et XBC. Il semblerait que la partie Xlib ne soit pas encore implantée correctement d'après divers articles lu ici et là sur le net. Je vais donc regarder de plus près ce qu'il y a lieu de faire.

Un rapide coup d'oeil sur le site www.khronos.org nous permet de trouver rapidement le "Vulkan 1.0 Quick Reference". À la neuvième page, on y apperçoit la rubrique "Window System Integration (WSI). Que du bonheur.... ou pas!

Diverses fonctions en rapport avec différents OS s'y trouvent. En ce qui concerne GNU/Linux, on voit vkCreateXcbSurfacesKHR(...) et vkCreateXlibSurfacesKHR(...), ainsi que les structures qui leur sont associées. Ces fonctions permettent de créer des "Surfaces" utilisable sous Vulkan.

En rapport avec nos deux possibilités sous X.org, il existe aussi les fonctionnalités de "questionnement": vkGetPhysicalDeviceXcbPresentationSupportKHR(…) et vkGetPhysicalDeviceXlibPresentationKHR(…). Ces 2 fonctions renvoie un booléen, on verra pourquoi plus tard quand j'aurai étudié la question.

Regardons de plus près les deux premières fonctions. Les 2 renvoient un VkResult, par contre elles ne prennent pas tout à fait les même choses en paramètres.

VkInstance Instance    comme premier paramètre des 2 fonctions. Ce qui veut dire qu'il faudra commencer par créer ce fameux VkInstance avant de vouloir interagir avec GTK.

En deuxième paramètre, il faut fournir un pointeur vers une structure d'information à la création de la surface. C'est ici que les choses changent un peu. En effet, la structure diffère légèrement suivant l'environnement sous lequel on veut travailler. Ce sera donc cette structure qui jouera un rôle primordiale dans la possibilité de créer ou non une surface qui nous servira à dessiner ce que nous avons envie.

En troisième paramètre, un pointeur vers un VkAllocationCallback, une espèce de pointeur vers une fonction d'allocation dynamique de mémoire, si j'ai bien tout compris.

Et comme dernier paramètre, il faut passer un pointeur vers un VkSurfaceKHR. C'est le pointeur vers la surface sur laquelle nous dessinerons plus tard.

Jusque maintenant, je n'ai rien de concret pour mettre en relation Vulkan et GTK, mais quelque chose me dit que je ne suis pas si loin de trouver un indice...

Regardons de plus près ces fameuses structures, retirons ce qui est commun au deux et analysons le reste.

Xlib a besoin de Display* dpy et de Window window
XCB a besoin de xcb_connection_t* connection et de xcb_window_t window

Dans les deux cas, c'est presque pareil, il ne reste plus qu'à aller voir notre GTK afin de savoir de quoi il a besoin.

Dans GTK3, il exite un GLArea, peut être que l'on pourrait trouver là dedans une fonctionnalité qui nous renverrais un pointeur vers une fenêtre de type Window de Xlib ou de type xcb_window_t de XCB. Dans ce GLArea, il existe une fonction get_window() qui renvoie un Gdk::Window. Gdk(Gnome Development Kit) c'est intéressent, mais a t'on le droit d'utiliser des fonctionnalités Gnome avec un autre gestionnaire de fenêtre, c'est en tout cas une des choses a tester. On trouve aussi dans GLArea une fonction get_screen() qui retourne un Gdk::Screen, ce qui pourrait aussi s'avérer utile me semble t'il. Dans la même lignée, il y a un get_display qui retourne un Gdk::Display. Et il est même possible de récupérer la fenêtre racine se trouvant derrière la fenêtre en cours avec get_root_window() et qui renvoie un Gdk::Window… Bref, ce fameux GLArea travaille étroitement avec la librairie Gdk. get_pointer permettrait quand à lui de récupérer la position de la souris...

Avant de rentrer plus en profondeur dans ce GLArea, je vais aller voir si je ne vois pas autre chose d'intéressent ailleurs.

On monte d'un cran, puisque toutes ces fonctions sont en fait des fonctions réimplémentée de la classe Gtk::Widget. Je viens d'y voir aussi un get_visual qui renvoie un Gdk::Visual.

Après avoir un peu décortiqué Gtk et Gdk, il est certain que ceux ci utilise Xlib et non XCB, ce qui est vraiment dommage puisqu'il parait que Vulkan aurait du mal à travailler avec Xlib. Vraisemblablement, il se pourrait que Xlib puisse être pris en charge actuellement étant donné que ce que j'ai pu lire à ce sujet était assez ancien. Tout ceci veut dire également qu'à partir de maintenant, je fais cavalier seul.

Rappelons nous de ce que nous avons besoin pour travailler avec Vulkan sous Xlib. Les deux choses particulières à Xlib qui nous sont nécessaires sont:

- Display* dpy
- Window window

Comment les obtenir?

Dans la documentation de gdk3, on y trouve deux macros qui remplisse ce rôle:
- gdk_x11_display_get_xdisplay() renvoie un Display* correspondant à un GdkDisplay*
- GDK_WINDOW_XID() qui renvoie le Xlib Window d'une fenêtre de type GdkWindow
La fonction équivalente étant Window gdk_x11_window_get_xid(GdkWindow* window);

Waouw! En voila du chemin parcouru…

Bon! Moi, ce qui m'intéresse, c'est de travailler en c++ et non en c. Je vais donc aller analyser gtkmm pour savoir si je peux trouver des données intéressantes. Je me rappelle qu'il est possible de récupérer des classes Gdk::Window et Gdk::Display, je vais donc chercher dans cette voie là.

Oui oui oui oui oui!

Un Gtk::Widget possède les 2 fonctions suivantes:
- Glib::RefPtr<Gdk::Window> get_window()
- Glib::RefPtr<Gdk::Display> get_display()

Ni Gdk::Window ni Gdk::Display ne permettent d'obtenir directement les ressources nécessaire. Heureusement, toutes deux ont la fonction gobj() qui renvoie respectivement un GdkWindow et un GdkDisplay, et donc au format du langage C, ce qui va nous permettre d'utiliser les fonctions que nous avons découvert quelques paragraphes plus haut.

En résumé, il faudra écrire dans notre programme quelque chose du genre:
-Display* dpy = gdk_x11_display_get_xdisplay(myWidget.get_display());
-Window window = gdk_x11_window_get_xid(myWidget.get_window());

La boucle est bouclée comme on dit chez nous…

Je suis déjà bien content du résultat théorique obtenu. Il est plus que temps de passer à la pratique.

La première chose à faire est de créé un nouveau projet gtkmm de base sans gestion ui, on va tout faire à la main comme des grands.

#include <gtkmm/main.h>
#include <gtkmm/window.h>

#include "config.h"

int main (int argc, char *argv[])
{
Gtk::Main kit(argc, argv);

Gtk::Window* main_win = new Gtk::Window (Gtk::WINDOW_TOPLEVEL);
main_win->set_title ("vulkanbasetutorial1");

if (main_win)
{
kit.run(*main_win);
}
return 0;
}

Voici le code minimaliste créé par Anjuta, à l'exeption des include, en effet, j'ai remplacé <gtkmm.h> par <gtkmm/main.h> et <gtkmm/window.h> (Pourquoi tout lier quand on peut ne prendre que ce qui est nécessaire.).

Pour ne pas trop se casser la tête pour le moment, nous allons simplement insérer nos 2 fonctions juste devant le if(main_win) et nous remplacerons myWidget par, je vous le donne en 1000, main_win lui même. Ben oui, main_win est un Gtk::Window qui, lui même est un Gtk::Widget.
On aura donc:

Display* dpy = gdk_x11_display_get_xdisplay(main_win->get_display()->gobj());
Window window = gdk_x11_window_get_xid(main_win->get_window()->gobj());

Attention toutefois, si la fenêtre n'est pas visible, cela veut dire que la fenêtre au niveau de Xlib n'existe pas, et donc il faut vérifier si elle est "réalisée"… Voici le code de test que j'ai réalisé, ce n'est pas construit très correctement mais ça l'est suffisamment pour l'apprentissage.

#include <gtkmm/main.h>
#include <gtkmm/window.h>

#include <gdk/gdkx.h>

#include <iostream>

#include "config.h"

Gtk::Window* main_win;

void onWindowRealized()
{
GdkWindow* gdkw = main_win->get_window()->gobj();
std::cout << "GdkWindow : " << gdkw << std::endl;
Window window = gdk_x11_window_get_xid(gdkw);
std::cout << "Window = " << window << std::endl;
}

int main (int argc, char *argv[])
{
Gtk::Main kit(argc, argv);

main_win = new Gtk::Window (Gtk::WINDOW_TOPLEVEL);
main_win->set_title ("vulkanbasetutorial1");

GdkDisplay* gdkd = main_win->get_display()->gobj();
std::cout << "GdkDisplay = " << gdkd << std::endl;
Display* dpy = gdk_x11_display_get_xdisplay(gdkd);
std::cout << "Display = " << dpy << std::endl;

main_win->signal_realize().connect(sigc::ptr_fun(&onWindowRealized));
if (main_win){
kit.run(*main_win);
}
return 0;
}

Et bien, cette fois, on a tout ce que nous avons besoin pour démarrer quelque chose plus en rapport avec le sujet de cet article. Etant donné que Vulkan a besoin que la fenêtre existe réellement, toutes les fonctions concernant la surface sera traité dans la fonction onWindowRealized(). En ce qui concerne l'initialisation de Vulkan, je vais pouvoir inclure cela dans mon main(). Je vais peut être devoir mettre d'autres variables en global mais je le répète, c'est juste pour les besoins de l'apprentissage.

Pour commencer, il faut que Vulkan sache que nous allons travailler avec lui. Il ne suffit pas bêtement d'ajouter un #include en début de fichier bien que cela soit nécessaire. Par ailleurs, dans mon système Archlinux, il n'y a pas de donnée concernant l'utilisation de pkg_config, les fichiers include se trouvent dans un répertoire vulkan, lui même dans un répertoire connu du système. En ce qui concerne les librairies objets, elles sont dans un répertoire également connu du système. Puisque j'ai décidé de travailler en c++, on devra donc écrire #include <vulkan/vulkan.hpp>. Pour le linker, un simple -lvulkan sera suffisant.

Ensuite, il va falloir initialiser Vulkan, et là, si j'ai bien compris, cela ne va pas être une partie de plaisir, quoique, je suis un peu maso sur les bords. La première partie de l'initialisation consiste à créer une instance. Pour se faire il existe une classe vk::Instance que je déclarerai en global sous le nom vki. L'avantage de la classe, c'est qu'au moment de sa destruction, elle exécutera automatiquement la fonction de libération de l'instance. Notre Instance a besoin fondamentalement d'une structure/classe nommée InstanceCreateInfo, cette classe regroupe toute une série de paramètres concernant l'application, les extensions ainsi que les layers nécessaires pour faire tourner notre programme.

Pour les paramètres de l'application, il existe également une classe/structure avec quelques informations non obligatoire pour le bon déroulement des choses, mais comme je suis un peu maso, et bien, je vais aussi m'en servir. Cette classe n'est autre que vk::ApplicationInfo. Dans le cadre de mon étude, je vais créer une instance de cette classe en global, ce qui me permettra d'y accéder plus tard. Son constructeur admet 5 paramètres mais je me contenterai des valeurs par défaut car j'ai l'intention d'initialiser les valeurs plus tard via les méthodes set qui me plaisent beaucoup. Ces méthodes renvoient systématiquement une référence vers la classe elle même, ce qui permet de chaîner les assignations et donc d'avoir une mise en forme très lisible. Je vais procéder de la même façon avec la classe vk::InstanceCreateInfo.

Maintenant, je vais créer une fonction pour l'initialisation de la librairie Vulkan et y regrouper tout ce que je viens d'énoncer.

void initializeVulkan()
{
vkApplicationInfo
.setPApplicationName("Vulkan Base Tutorial")
.setApplicationVersion(VK_MAKE_VERSION(0,0,1))
.setPEngineName("No Engine")
.setEngineVersion(VK_MAKE_VERSION(0,0,1))
.setApiVersion(VK_MAKE_VERSION(1,0,0));

vkInstanceCreateInfo
.setPApplicationInfo(&vkApplicationInfo);

vkInstance = vk::createInstance(vkInstanceCreateInfo); 
}

C'est très lisible comme ceci. J'aime assez bien ce concept de chaîner les instructions d'initialisation des attributs d'une classe.

Je commence à bien m'amuser ici…

Maintenant que l'instance est créée, il serait peut être intéressent de savoir quelles sont les extensions disponibles pour travailler.

Qu'est ce qu'on entend par extensions?

Une extension au sens de Vulkan est une fonctionnalité, tout simplement, et donc, il faut contrôler que ce qu'on a besoin soit bien accessible pour notre GPU. Donc, au moment de créer notre instance, il faut indiquer à Vulkan quelles extensions nous allons utiliser, ce qui nous aidera à choisir un GPU si le système en possède plusieurs. La liste de ces extensions sont à inclure dans la classe vk::InstanceCreateInfo. Dans la version C de Vulkan il existe une fonction vkEnumerateInstanceExtensionProperties(…), avant de regarder là dedans, je vais aller voir s'il n'y a pas une classe qui va me permettre de faire la même chose plus facilement.

Je n'ai rien vu dans la classe Instance ni dans la classe InstanceCreateInfo. Allons voir s'il n'y a pas une classe nommée Extension par exemple…

Il existe bien une classe nommée ExtensionProperties, mais ce n'est pas elle qui va "énumérer" les extensions possible, c'est juste un contereur pour une Extension que l'on connait déjà.

Je vais donc aller voir si je ne vois pas une fonction dans le namespace vk comme pour la création d'une Instance.

Bingo! Cela existe en effet et cela s'appelle: vk::enumerateInstanceExtensionProperties … qui l'eu cru…? Tout comme pour l'Instance, il en existe deux versions. Une qui donne un Result en retour et l'autre qui lance une exception en cas d'erreur ou bien renvoie un vecteur de ExtensionProperties. Je vous avoue que la deuxième solution me satisfait au plus haut point. Voici le prototype:

ResultValueType<std::vector<ExtensionProperties,Allocator>>::type enumerateInstanceExtensionProperties (Optional< const std::string > layerName=nullptr)

Ça a l'air bien compliqué comme cela mais il n'en est rien. Il suffit de déclarer une variable du type std::vector<ExtensionProperties> et puis d'appeler la fonction sans même être obligé d'écrire quelque chose entre les parenthèses, et voila que toutes les extensions de la version de votre Vulkan possible sont énumérées. Que demander de plus au peuple?

Ce que je vais donc faire, c'est créer une variable globale:
std::vector<vk::ExtensionProperties> vkextensionPropertiess;

Chacun de ces ExtensionProperties est composé du nom de l'extension et de sa version. Pour le fun, nous pouvons lister à l'écran toutes les extensions:

std::cout << "Extensions disponible: " << std::endl;
for (const auto& extension : vkextensionPropertiess)
{
  std::cout << "  " << extension.extensionName;
}

En ce qui me concerne, voici la liste de ce que je reçois:

VK_KHR_surface
VK_KHR_xcb_surface
VK_KHR_xlib_surface
VK_KHR_wayland_surface
VK_EXT_debug_report

Il y a seulement 5 extensions disponible, une première de création de surface générale car avec Vulkan, il est possible de travailler sur une surface non visible à l'écran, 3 autres surfaces qui correspondent à 3 façons de travailler sous GNU/Linux, dans notre cas, c'est Xlib qui nous intéresse et en dernier, je crois que ça dit bien ce que cela veut dire.

Et dans ma fonction d'initialisation je vais faire appel à cette fameuse fonction. Pour le moment, je ne m'occupe pas trop des try/catch mais il va falloir que j'y pense sérieusement, et même très sérieusement, heureusement que c'est juste pour faire des tests… Quand le travail que je réalise ici sera terminé, je pourrai m'occuper sérieusement du tutoriel, qui, j'en suis certain vous fait impatience.

J'ai bien avancé aujourd'hui, je suis assez content de moi! Je vais maintenant m'occuper des LayerProperties, encore une de ces choses étrange venant tout droit de la planète Vulkan…

Par défaut, l'API Vulkan ne fait aucun contrôle de ce que l'on peut lui envoyer, vous êtes donc libre de lui envoyer un fichier son alors qu'il s'attend à recevoir une liste de course. Pas très pratique direz vous… En fait, c'est à nous, programmeur, de savoir faire la part des choses, et de s'assurer que ce que nous exécutons est bel et bien sans erreur. Avec l'API Vulkan, une erreur est vite arrivée, et peut s'avérer fatale pour votre session. Bah! Vous direz que dans le pire des cas, on reboot l'ordi et puis on recommence, mais je ne sais pas si l'utilisateur serait très content, surtout s'il était justement en train d'écrire un article pour son blog…

Heureusement, Vulkan est plus fort que cela et viens avec toute une panoplie de surcouches (vk::LayerProperties) qui, elles, sont capable de vérifier votre bon travail (ou pas). Quel est l’intérêt de tout ça? Et bien, il va nous être possible d'avoir une compilation conditionnelle, et donc d'inclure ou non ces layers en fonction de ce que l'on fait (Release/Debug). Voila, c'est génial! Il y a du #ifdef dans l'air. Je n'aime pas trop la compilation conditionnelle, mais dans ce cas ci, je ne suis pas trop dérangé.

Avant d'en arriver là, je vais essayer d'énumérer la liste des vk::LayerProperties disponible dans ma version de l'API Vulkan. Après avoir fait la liste des extensions, je pense que cela va aller comme sur des roulettes…

Je ne vous ferai pas l'affront de vous présenter du code ici, vous devriez être capable de le faire vous même. Il suffit de recopier la partie Extension et de remplacer ce mot par le mot Layer partout partout partout. Et puis ça marche du premier coup.

Simple mais efficace.

Parmis tous les layers disponible, il y en a un qui nous intéresse particulièrement, c'est VK_LAYER_LUNARG_standard_validation. Si, comme moi, vous avez afficher à l'écran la liste des layers, alors vous avez certainement dû le voir dans la liste. Si ce n'est pas le cas, c'est que vous n'avez pas la bonne version du Vulkan SDK, allez télécharger la dernière de LUNARG, disponible sur le site de Khronos.

Maintenant que j'ai la liste des layers, et que je connais celui qui m'intéresse, il va falloir tester si ce layer existe bel et bien dans notre liste. Pour ce faire, j'ai déjà une belle boucle qui affiche la liste, il suffit de lui ajouter une fonctionnalité de test, et pourquoi pas créer une fonction qui fera le test. Cette idée me plait beaucoup, d'autant plus qu'il faudra très certainement réaliser la même chose avec les Extension, si je ne m'abuse. Enfin, je m'écarte un peu de ce qui m'intéresse pour le moment.

En ce qui concerne la fonction, il faudrait qu'elle puisse traiter deux listes. La première représente tous les layers que je viens de récupérer, la seconde n'est autre que la liste des layers nécessaire au bon fonctionnement de mon programme.

Une fois de plus, le site de vulkan-tutorial.com m'est d'un grand secours. Tout y est expliqué en détail, mais pour la version C.

Magnifique, ça marche très bien! À la prochaine étape, je vais pouvoir donner les informations de layers à ma classe d'information de création d'instance, sans oublier qu'il faut que je bloque la création de l'instance dans le cas où je serai en mode debug et que les validation layers ne soient pas accessible, même si je sais que je l'ai déjà.

Donc, comme je l'écrivais ci dessus, maintenant, je vais déterminer quels layers vont être utilisé dans mon programme. Cette sélection se fera en fonction du type de compilation que je vais produire (debug ou release). Ainsi, grâce à un simple booléen qui prendra un état ou l'autre en fonction du mode de compilation, je vais pouvoir contrôler tout le programme. Inclus dans les spécifications du c++, il existe un #define NDEBUG qui signifie que la compilation n'est pas en mode debug. Avec une simple série #ifdef #else #endif, je vais pouvoir donner une valeur ou une autre à un booléen. Facile!

Petite (grande) rectification, NDEBUG n'est pas du tout automatique, je m'en suis rendu compte quand j'ai voulu faire une compilation "Release", le flag NDEBUG ne s'est pas mis tout seul. Trop beau pour être vrai. Par contre, c'est à nous à placer -DDEBUG ou -DNDEBUG dans la commande de compilation ou bien dans le CPPFLAGS, et tout de suite tout fonctionne correctement.

C'est bien tout ça mais j'ai vérifier que les fonctionnalités existent, mais je ne les ai pas encore utilisées. Pour cela, je vais avoir besoin d'une extension que je me souviens d'avoir vu passer dans la liste que j'avais récupéré, c'est le VK_EXT_debug_report. Celui que je ne savais pas trop à quoi il pouvait bien servir. C'est donc le moment d'apprendre son fonctionnement…

D'abord, si j'utilise le debug, et donc les validations layers alors il faut ajouter cette fameuse extension à la liste des extensions utile à notre programme, ensuite, il va falloir tester si elle existe dans la liste des extensions disponibles et réagir en fonction du résultat, quoique, elle devrait implicitement exister en même temps que le layer correspondant. Le principe ressemble très fort à ce que j'ai réalisé avec les layers précédemment. C'est parti!

Je vais éditer ce sujet au fur et à mesure de mes découvertes. Si vous connaissez quelque chose à propos de ceci, n'hésitez pas à le mettre en commentaire. Ça pourrait m'aider ainsi que ceux qui lisent cet article.

Merci.

A bientôt.

Ali Bébert.

Aucun commentaire:

Enregistrer un commentaire

N'hésitez pas à laisser un commentaire, de plus les Backlink sont activés ;)