- Un développeur ayant de l’expérience avec OpenGL a appris Vulkan pour la première fois et a créé en environ 3 mois deux petites démos de jeux ainsi qu’un moteur réutilisable, EDBR
- Plutôt que de concevoir dès le départ un moteur généraliste, il a d’abord créé un petit jeu, puis n’a transformé en moteur que les parties nécessaires, réduisant ainsi la surconception et le bike-shedding
- Le moteur fait 19k LoC, avec 6,7k LoC de code graphique et 2k LoC d’abstraction légère de Vulkan ; il inclut le compute skinning, les CSM, le shading PBR, le MSAA, les post FX et le rendu d’UI
vk-bootstrap, Vulkan Memory Allocator,volk,VK_KHR_dynamic_rendering, les constantes push, les adresses de périphérique de buffers et les descriptors bindless réduisent le boilerplate Vulkan et l’usage des descriptor sets- Vulkan a apporté la suppression de l’état global, de meilleures erreurs de validation, le débogage des shaders dans RenderDoc et une cohérence entre GPU et OS, mais la synchronisation explicite reste à gérer directement
Le moteur EDBR basé sur Vulkan, créé en 3 mois
- EDBR (Elias Daler’s Bikeshed Engine) est parti d’un projet d’apprentissage de Vulkan, puis a évolué en un petit moteur réutilisable pour des projets ultérieurs
- Le code du moteur et des jeux est publié dans le dépôt GitHub
- Au moment de la rédaction, la taille du code est la suivante
- Moteur lui-même : 19k LoC
- Code lié au graphisme : 6,7k LoC
- Abstraction légère de Vulkan : 2k LoC
- Jeu de chat en 3D : 4,6k LoC
- Jeu de plateformes en 2D : 1,2k LoC
- Moteur lui-même : 19k LoC
- Une partie du code non graphique, comme la gestion des entrées et le système audio, a été reprise d’un ancien moteur ; le graphisme et plusieurs systèmes essentiels ont été réécrits
- L’auteur estime qu’il était plus adapté de réécrire que d’essayer d’insérer Vulkan dans l’abstraction OpenGL existante
Parcours d’apprentissage, de la programmation graphique à Vulkan
- Si l’on débute en programmation graphique, commencer par OpenGL plutôt que Vulkan permet plus facilement d’éviter d’être submergé par la complexité
- Comme objectif minimal, il recommande d’afficher à l’écran un modèle texturé et d’implémenter un éclairage Blinn-Phong simple
- Le shadow mapping de base aide à apprendre à rendre une scène depuis un autre point de vue vers une autre cible de rendu, ainsi qu’à échantillonner une depth texture
- Ressources recommandées pour apprendre OpenGL
- Pour apprendre Vulkan, vkguide a été le plus utile ; si l’on débute, il conseille de le suivre en entier, tout en notant qu’un jeu simple n’a pas forcément besoin d’emblée de la complexité du « GPU driven rendering »
- La Vulkan Lecture Series de TU Wien couvre les bases de Vulkan, et le cours sur la synchronisation est particulièrement utile
- À l’issue du premier mois d’apprentissage, il avait implémenté les fonctionnalités suivantes
- Chargement de modèles glTF
- Compute skinning
- Frustum culling
- Shadow mapping et cascaded shadow maps
Raisons du choix de Vulkan et comparaison avec WebGPU
- L’objectif était un petit jeu 3D desktop centré sur Windows et Linux ; préférant les technologies open source et les standards ouverts, il a choisi entre OpenGL et Vulkan
- OpenGL est suffisant pour de petits jeux, mais il juge son avenir incertain, car il est peu probable qu’une nouvelle version sorte et il est deprecated sur macOS
- Il a aussi étudié WebGPU, mais a relevé les limites suivantes
- Pas encore stable, avec peu de tutoriels et d’exemples
- La syntaxe de WGSL lui plaisait moins que celle de GLSL
- Sur desktop, il s’apparente plutôt à un wrapper au-dessus de DirectX, Vulkan et Metal, ce qui rend les captures RenderDoc différentes selon la plateforme, et les appels WebGPU ne correspondent pas 1:1 aux appels des API natives
- Absence de bindless textures et de push constants
- WebGPU présente aussi des avantages évidents
- De meilleures erreurs de validation qu’OpenGL/WebGL et pas d’état global
- Des similitudes avec Vulkan, utiles avant d’apprendre Vulkan
- Moins de boilerplate que Vulkan pour afficher quelque chose à l’écran
- Pas besoin de gérer directement la synchronisation explicite
- Possibilité d’exécuter des jeux dans le navigateur
Flux de rendu d’une frame
- Une frame est divisée en plusieurs étapes, chacune implémentée sous forme de pipeline ou de passe
- À l’étape de skinning, les modèles avec skeletal animation sont traités par un compute shader
- L’entrée est un mesh non skinné et des joint matrices
- La sortie est un vertex buffer à utiliser dans les étapes de rendu suivantes
- Les étapes ultérieures peuvent ensuite traiter de manière similaire les mesh statiques et les mesh skinnés
- L’étape CSM effectue le cascaded shadow mapping avec une depth texture 4096x4096 et 3 slices
- À l’étape geometry + shading, le modèle est dessiné et le shading utilise la shadow map et les informations de lumière
- Le modèle PBR est presque identique à la méthode décrite dans Physically Based Rendering in Filament
- Le fragment shader calcule, dans un même draw call, toutes les lumières qui affectent le mesh concerné
- Tout est dessiné dans une texture multi-sampled, puis résolu
- Le depth resolve est géré manuellement avec un fragment shader
- Il parcourt tous les fragments de la multi-sample depth texture et écrit la valeur minimale dans une depth texture non-MS
- L’étape post FX n’applique actuellement que le depth fog ; le tone mapping et le bloom devraient aussi être traités à cette étape par la suite
- À l’étape UI, l’interface de dialogue est dessinée et traitée en un seul draw call
Bibliothèques pour réduire le boilerplate Vulkan
vk-bootstrapréduit le boilerplate d’initialisation de Vulkan, comme le choix du physical device et la création de la swapchain- Ce n’est pas un wrapper complet de toutes les fonctions Vulkan ; il agit surtout sur la phase d’initialisation
- Vulkan Memory Allocator évite d’avoir à gérer directement les allocations mémoire Vulkan
volksimplifie le chargement des extension functions- Par exemple, il permet d’utiliser des fonctions d’extension comme
vkSetDebugUtilsObjectNameEXTsans stocker soi-même leur pointeur
- Par exemple, il permet d’utiliser des fonctions d’extension comme
- La classe
GfxDeviceregroupe les objets et fonctionnalités Vulkan fréquemment utilisés- Initialisation du contexte Vulkan
- Création et gestion de la swapchain
beginFrameetendFrame- Création d’images et chargement de textures
- Création de buffers
- Gestion du bindless descriptor set
GfxDevice.cppcompte 714 lignes au moment de la rédaction ; passer un seul objet plutôt que transmettreVkDevice,VkQueue,VmaAllocator, etc. à de nombreux endroits s’avère pratique
Build des shaders et stratégie pour éviter les descriptor sets
- Le langage de shaders choisi est GLSL, en raison de l’expérience avec OpenGL
- Les shaders sont compilés en SPIR-V à l’étape de build, et non à l’exécution
- Le code de chargement des shaders à l’exécution devient plus simple
- Aucune dépendance à un shader compiler à l’exécution
- Les erreurs de shaders sont détectées à l’étape de build
glslcpermet de spécifier leDEPFILEde CMake, ce qui permet de recompiler automatiquement les fichiers concernés quand un include de shader change- Dans Vulkan, les uniforms doivent être regroupés en descriptor sets, ce qui rend la transmission des données plus complexe qu’avec OpenGL
- Dans cette implémentation, l’usage des descriptor sets est fortement réduit
- Un seul descriptor set global est utilisé pour les textures et samplers bindless
- Le reste est majoritairement transmis via des push constants
- Les adresses de buffers sont passées en push constants à l’aide de buffer device address
Classe pipeline et dynamic rendering
- Les étapes de rendu sont séparées en classes pipeline comme
PostFXPipeline - Chaque pipeline a généralement les rôles suivants
init: chargement des shaders, initialisation deVkPipelineetVkPipelineLayoutcleanup: nettoyage du pipeline et du layoutdraw: recevoir les entrées nécessaires à chaque frame et effectuer les draw calls
drawsuppose être appelé entrevkCmdBeginRenderingetvkCmdEndRendering- Le pipeline ne se préoccupe pas de savoir sur quelle texture le render pass effectue le rendu ; c’est l’appelant qui décide de la render target
VK_KHR_dynamic_renderingest utilisé partout, sans render pass ni subpass Vulkan- Il a été entendu que les render pass et subpass sont plus efficaces sur les GPU tile-based, mais le support mobile n’est pas envisagé pour l’instant
- Le dynamic rendering rend l’implémentation beaucoup plus simple
Utilisation de PVP, BDA et des descripteurs bindless
- Un seul type de vertex est utilisé pour tous les meshes
- Avec le programmable vertex pulling, on peut éviter de définir les formats de vertex comme les VAO d’OpenGL ou
VkVertexInputBindingDescriptionetVkVertexInputAttributeDescriptionde Vulkan - Avec buffer device address, il est possible de transmettre l’adresse du buffer en push constants sans binder le vertex buffer dans un descriptor set
- Le layout
scalarest utilisé pour les push constants et les buffers- Plus simple à gérer pour l’alignement que
std430, il se manipule presque comme une structure C++ - Il réduit le besoin de membres de padding dans les structures C++
- Plus simple à gérer pour l’alignement que
- Les descripteurs bindless sont utilisés en plaçant des tableaux de textures et de samplers dans un grand descriptor set
- Lorsqu’une nouvelle texture est chargée, elle est placée dans le tableau
textureset son index est utilisé comme identifiant de texture bindless - L’identifiant de texture est transmis au shader via les push constants
- Lorsqu’une nouvelle texture est chargée, elle est placée dans le tableau
- Les samplers sont séparés des images ; les samplers communs sont créés au démarrage et placés dans le tableau
samplers - Les identifiants de textures bindless sont aussi utilisés dans le material buffer
- Seul l’ID du matériau est transmis en push constants, et le fragment shader consulte le material buffer
- L’accès aux textures est possible avec un seul entier par matériau, sans descriptor set volumineux
- Comme ressource de référence sur les textures bindless, Vulkan Bindless Texture est recommandé
Données dynamiques uploadées à chaque frame
- Les données qui doivent être transférées du CPU vers le GPU à chaque frame sont gérées en préallouant un grand tableau et en le remplissant depuis l’index 0 à chaque frame
- Par exemple, toutes les matrices de joints sont stockées dans un grand tableau de
mat4, et l’index de départ est transmis en push constants pour chaque skinned mesh - Il existe deux approches
- Avoir N buffers côté GPU et alterner selon les frames-in-flight
- N’avoir qu’un seul buffer côté GPU et N staging buffers côté CPU
- Dans la plupart des cas, la première approche est recommandée
- Elle consomme davantage de mémoire GPU, mais ne nécessite pas de synchronisation manuelle
- Si la mémoire GPU doit être économisée, la deuxième approche peut être utile
- Aucune différence de performances notable n’a été observée entre les deux approches, mais un écart peut apparaître si de très grands volumes de données sont uploadés à chaque frame
Cleanup et synchronisation
- Le pattern de deletion queue de vkguide n’a pas été très utile dans le moteur de l’auteur
- Parce qu’il n’alloue ni ne détruit de nouveaux objets Vulkan à chaque frame
- Le cleanup basé sur les destructeurs C++ n’était pas pratique non plus
- Il faut des wrapper classes, des move constructors et des move assignments, ce qui augmente la complexité
- Il existe un risque de supprimer accidentellement un objet encore utilisé au milieu d’une frame lors de la destruction d’un wrapper
- Actuellement, les objets Vulkan sont nettoyés à un seul endroit en appelant explicitement la fonction
cleanup- Il est facile d’oublier un appel, mais à la fermeture, les erreurs de validation Vulkan et les assertions VMA signalent les cleanups manquants
- La synchronization Vulkan est difficile et doit être gérée explicitement
- OpenGL et WebGPU gèrent à la place la synchronisation nécessaire aux lectures de textures ou de buffers
- Avec Vulkan, il faut insérer soi-même des barrières pour éviter les data races
- Actuellement, les draws sont divisés en passes et pipelines, et des barrières sont insérées manuellement entre eux
- Par exemple, une barrière est placée après qu’une passe de skinning en compute shader a écrit les vertex data et avant qu’une passe de shadow mapping ne les lise
- Cela pourrait être automatisé avec un render graph, mais la synchronisation manuelle convient pour l’instant
- La synchronization validation layer de
vkconfigaide à trouver les erreurs de synchronisation
Sprite, skinning et séparation game/renderer
- Avec les textures bindless, il est facile de dessiner de nombreux sprites en un seul draw call, sans vertex buffer
- Le vertex shader des sprites génère les coordonnées des vertices du quad et les UV avec
gl_VertexIndex - Tous les draw calls de sprites sont regroupés sous forme de
SpriteDrawCommanddansSpriteDrawBuffer- transformation
- plage d’UV
- couleur
- ID de texture
- ID de shader
- Le draw call réel est de la forme
vkCmdDraw(cmd, 6, spriteDrawCommands.size(), 0, 0)- 6 vertices par sprite
- autant d’instances que de sprites
- Le renderer de sprites peut dessiner 10 000 sprites en 315 microsecondes
- Le compute skinning prend les vertices d’entrée et les matrices de joints d’un mesh avec skeletal animation pour générer un skinned vertex buffer
- Même trois chats partageant le même mesh peuvent avoir des animations différentes
- Un output vertex buffer est nécessaire pour chaque instance de mesh
- La game logic et le renderer sont séparés par des draw commands
- La game logic utilise entt
- Le renderer ne connaît ni les entities ni les game objects ; il ne manipule que les lights, les scene parameters et les mesh draw commands
MeshDrawCommandcontientmeshId, la matrice de transformation, la bounding sphere, un pointeur de skinned mesh, l’index de départ des matrices de joints et l’indication de projection d’ombre
Chargement de scènes, UI, Dear ImGui
- L’auteur n’a pas créé son propre level editor : il utilise Blender et exporte en glTF
- Écrire soi-même un level editor pouvant prendre des mois, voire des années, cela permet de gagner du temps
- La création des prefabs et la forme physique sont spécifiées via le nom des nodes
- Exemple : dans
Interact.Sphere.Diary,Interactavant le premier point est le nom du prefab Spheresert au système physique à créer un corps physique sphériqueCapsuleetBoxpeuvent aussi être utilisés ; en leur absence, la forme physique est générée à partir des vertices du mesh
- Exemple : dans
- Les modèles complexes ne sont pas intégrés directement dans le glTF du niveau : ils sont placés sous forme d’objets
Empty->Arrows, puis nommés par exempleCat.NearStore- Cela crée le prefab
Catet lui associe le tagNearStore
- Cela crée le prefab
- Les prefabs sont écrits en JSON et peuvent inclure des glTF externes ainsi que des informations de mouvement et de physique
- Le système d’UI s’inspire de l’API UI de Roblox
- origin
- taille relative
- position relative
offsetPosition,offsetSize- taille fixe
- taille basée sur le contenu d’un label ou d’une image
- La taille des éléments d’UI est calculée récursivement, puis leur position est calculée, et ils sont dessinés dans l’ordre parent puis enfants
- Dear ImGui est utilisé pour les outils de développement et de debug
- Comme Dear ImGui s’affichait mal avec un framebuffer sRGB, l’auteur a écrit son propre backend Dear ImGui
- Il n’a écrit que la partie rendering ; la gestion des input events, du clipboard et des autres interactions logique/OS reste prise en charge par le backend SDL Dear ImGui de base
- Les avantages du backend maison sont les suivants
- il prend en charge les bindless texture IDs, ce qui permet de dessiner une image avec
ImGui::Image(bindlessTextureId, ...) - les images linéaires et non linéaires peuvent être dessinées correctement en transmettant le format
- il peut être initialisé et manipulé de la même manière que le reste du code Vulkan du moteur
- il prend en charge les bindless texture IDs, ce qui permet de dessiner une image avec
Bibliothèques utilisées et effets du passage à Vulkan
- Pour la physique, Jolt Physics est utilisé
- Il sert principalement à la collision resolution et aux déplacements basiques des personnages
JPH::CharacterVirtualgère bien les déplacements de personnage de base
- Pour l’ECS, entt est utilisé
- Utiliser une bibliothèque externe plutôt qu’un ECS maison réduit le code à maintenir
- Pour l’audio, openal-soft, libogg et libvorbis sont utilisés
- Pour le profiling, Tracy est utilisé
- Il aide à vérifier quelles parties du code consomment réellement très peu de temps, afin d’éviter le bike-shedding inutile
- Les gains obtenus en passant à Vulkan sont les suivants
- l’absence d’état global à la OpenGL rend l’abstraction plus facile
- il y a moins besoin de
shader.bind()à la OpenGL, de state tracker ou de RAII magique - les erreurs de validation sont plus riches qu’avec OpenGL
- RenderDoc permet de déboguer directement les vertex shaders et fragment shaders
- les différences de comportement entre GPU et OS sont moins marquées qu’avec OpenGL
- il devient possible d’explorer à l’avenir d’autres shading languages comme Slang ou Shady
- chaque aspect du pipeline graphique peut être contrôlé plus finement
Travaux à venir
- Les travaux prévus sont les suivants
- prise en charge des polices en signed-distance field
- chargement de nombreuses images et génération parallèle des mipmaps
- bloom
- brouillard volumétrique
- animation blending
- render graph
- ambient occlusion
- finalisation du jeu
- Apprendre Vulkan a été difficile, mais pas autant que prévu, et a permis de mieux comprendre la programmation graphique et les API modernes
1 commentaires
Avis de Hacker News
Le minimalisme est très efficace
Je suis allé dans la direction opposée, et j’en souffre beaucoup. Je développe un client de métavers en Rust, et là encore, sur un autre écran, il affiche une scène où un avatar traverse en tram une immense ville steampunk. Avant de publier une nouvelle prérelease, je le fais tourner pendant 12 heures d’affilée
J’ai mis WGPU et Rend3 par-dessus Vulkan, et Rend3 fournit une API propre où l’on crée des meshes, des textures 2D et des objets, puis les objets référencent les meshes et les textures et apparaissent à l’écran. Le comptage de références de Rust maintient les liens en place, donc l’écriture est intuitive
Mais quand les couches s’empilent, les problèmes apparaissent. WGPU essaie de prendre en charge les navigateurs web, Vulkan, Metal, DX11 (récemment supprimé), DX12, Android et même OpenGL, ce qui exige une grosse équipe de développement et rend les changements difficiles. L’API WGPU elle-même ressemble globalement à Vulkan, donc il faut gérer soi-même l’allocation de mémoire GPU et la synchronisation
WGPU souffre d’un problème de plus petit dénominateur commun. Certaines plateformes ne prennent pas en charge certaines fonctionnalités, et WGPU ne sait pas gérer sans interférence la mise à jour concurrente de la mémoire GPU depuis plusieurs threads, que Vulkan prend en charge. Les jeux ou clients qui manipulent de grands mondes ont besoin de cette capacité pour envoyer du contenu au GPU sans tuer le framerate. Comme les contraintes de concurrence varient selon les plateformes, les conflits de verrous peuvent aussi faire chuter fortement les performances
Rend3 essayait de jouer le rôle de code de liaison raisonnable pour gérer la synchronisation et l’allocation, mais la synchronisation, en particulier, est difficile à résoudre de manière générale. Le frustum culling apporte un gros gain de performances, mais l’occlusion culling s’est révélé perdant à cause de son coût de calcul. La gestion de la transparence est aussi pénible, car elle nécessite un tri par profondeur. Comme je manipule des mondes avec beaucoup de fenêtres, les objets transparents, permettant de regarder dehors comme dedans, sont indispensables
Les gens de la stack 3D Rust semblent agacés que je les pousse sans arrêt depuis trois ans à corriger la stack. Tout le monde est bénévole, et Vulkan est maintenu parce qu’il y a de l’argent et une base d’utilisateurs. Rend3 a récemment été abandonné par son créateur, donc je dois maintenant mettre les mains dedans pour le corriger. Les gens qui construisent des choses complexes avec WGPU sont rares ; la plupart font des jeux 2D qui auraient aussi bien pu être faits en Flash, ou de simples scènes 3D statiques. Les projets commerciaux continuent d’utiliser Unity ou UE5
Passer directement à Vulkan serait aussi un gros changement, puisqu’il faudrait écrire soi-même la synchronisation, l’allocation, le frustum culling et la gestion de la transparence
À noter que Vulkano, un wrapper au-dessus de Vulkan et Metal, souffre lui aussi du problème du plus petit dénominateur commun. Vulkan et Metal prennent tous deux en charge la mise à jour concurrente des assets GPU, mais Vulkano ne l’autorise pas. Bien sûr, Apple gère encore les choses différemment
https://gpuweb.github.io/gpuweb/explainer/#multithreading
https://github.com/gpuweb/gpuweb/issues/354
OpenGL n’a jamais pris en charge les threads non plus, donc l’utilisation d’OpenGL ne peut pas permettre cela
Il y a un peu plus d’un an, j’ai essayé d’apprendre Vulkan, mais je n’ai aucune envie d’y retoucher. C’est vraiment pénible de mettre OpenGL au rebut pour le remplacer par quelque chose qui rend même les tâches simples absurdement difficiles. Par exemple, un simple cube en rotation demande déjà plusieurs centaines de lignes de code
OpenGL n’était pas facile non plus, mais une personne ordinaire pouvait tout de même en apprendre les bases en relativement peu de temps. On pouvait acheter un manuel d’introduction à la programmation graphique dans une grande librairie et afficher un rendu de base en un après-midi ou deux. Vulkan est peut-être meilleur sur certains aspects, mais s’attendre à l’apprendre rapidement n’est pas réaliste
Imaginez qu’avec l’arrivée des dernières puces Intel/ARM/AMD, on ne puisse plus utiliser C ou C++ et qu’on nous dise : « nous arrêtons la prise en charge des langages de haut niveau, désormais vous devez écrire uniquement en assembleur ; vous aurez plus de contrôle, donc ce sera plus rapide ». Évidemment, on trouverait ça absurde
On dit souvent que Vulkan ressemble davantage à une « API GPU » qu’à une API graphique de haut niveau. Vu comme ça, sa complexité n’a rien d’étonnant, et le domaine lui-même est difficile
Plutôt que l’analogie avec la suppression des langages de haut niveau au profit de l’assembleur sur les CPU modernes, c’est plus proche de la différence entre du C/C++ monothread et du C/C++ multithread. La complexité augmente fortement, ça explose ou les performances empirent si on ne sait pas ce qu’on fait, mais c’est aussi une voie pratique pour avancer
OpenGL peut en grande partie être implémenté au-dessus de Vulkan. C’est dommage que le standard OpenGL ne soit plus vraiment développé activement, mais rien n’est éternel
Le problème, c’est qu’OpenGL ne correspond plus très bien au matériel actuel, si bien qu’utilisé naïvement, il est très inefficace en termes de performances. Même avec OpenGL, quand on va jusqu’aux techniques qui éliminent la charge côté driver, Vulkan n’est pas tellement plus difficile
Les puces Intel/ARM/AMD actuelles ne prennent pas non plus directement en charge C ou C++. On utilise déjà soit l’assembleur, soit des outils tiers qui traduisent depuis C/C++. L’objectif de Vulkan est aussi de fournir une interface standard de bas niveau vers le GPU, sur laquelle on peut ajouter des abstractions plus agréables à utiliser
https://github.com/google/angle
Plusieurs téléphones intègrent désormais ANGLE comme unique prise en charge d’OpenGL au-dessus d’un driver Vulkan
Si vous voulez une API moderne, relativement simple et portable, utilisez WebGPU via wgpu en Rust ou dawn en C++
Je pense que Vulkan est excellent, mais son objectif est d’exploiter au maximum les fonctions GPU avancées. Quand on utilise ces fonctions avancées, il peut aussi offrir de meilleures performances qu’OpenGL
Si l’on ne vise pas des techniques de rendu avancées, OpenGL me semble généralement être la voie recommandée
Il existe encore beaucoup de jeux 2D, low-poly ou au style PS1, et ces jeux n’ont pas besoin de Vulkan
Vulkan illustre le fait que l’industrie du jeu AAA s’est orientée vers la qualité du rendu et l’apparence visuelle. Les studios AAA justifient leurs budgets par des moteurs et des contenus très sophistiqués, mais le marché des jeux 2D/low-poly grandit parce que les joueurs se lassent et réalisent qu’ils veulent du gameplay plutôt que des graphismes
En tant que développeur de jeu, on voudra sans doute se concentrer sur le gameplay et les fonctionnalités plutôt que sur la qualité du rendu
Il n’y a pas d’état global, on peut choisir le GPU à utiliser à l’exécution, et la gestion des erreurs d’OpenGL est épouvantable. Il y a des couches de validation, les illustrations officielles sont chouettes, et la documentation est excellente. Téléverser des données vers le GPU de manière asynchrone depuis un second thread est aussi possible de façon plus raisonnable, et il y a des fonctions GPU avancées comme les mesh shaders ou RTX
Les consommateurs rejetteraient-ils un jeu indépendant aux graphismes de niveau AAA ? Probablement pas. S’il y en a peu, c’est parce qu’ils sont difficiles à rentabiliser, et parce qu’il existe un marché suffisamment large qui accepte des graphismes plus stylisés et de moindre fidélité
En même temps, il donne accès à des fonctions modernes comme les compute shaders, impossibles avec WebGL, et il n’accumule pas autant d’héritage qu’OpenGL avec plusieurs façons de faire la même chose. Son principal avantage est d’être nouveau, mais c’est aussi pour cette raison qu’il y a beaucoup moins de tutoriels, ce qui est un inconvénient assez sérieux
Les frameworks et les moteurs sont innombrables, mais la plupart sont inachevés, et tous ont des avis très tranchés sur la structure du code. Du genre : « créez un arbre de scène contenant des callbacks. Non, écrivez des classes et des objets d’entités et de composants. Attendez, maintenant tout est en mode immédiat, fonctionnel et sans état »
Ajoutez à cela le bazar des plateformes. Avec les notifications push des jeux mobiles, les achats intégrés, la signature obligatoire via Xcode, c’est le chaos complet. Il y a une raison pour laquelle Unity a des parts de marché, et ce n’est pas parce que c’est un excellent logiciel. Le multiplateforme qui dépasse la simple webview tape-à-l’œil reste extrêmement douloureux
Si l’on doit continuer à écrire du code spécifique à chaque GPU/driver, la couche d’abstraction ne sert à rien. Il vaut mieux séparer les drivers de bas niveau des bibliothèques de haut niveau
Cet article contient beaucoup de bons conseils. Celui qui m’a particulièrement marqué est « n’implémentez pas ce dont vous n’avez pas besoin maintenant ».
C’est un point sur lequel je me bats constamment avec des programmeurs juniors qui ont déjà quelques années d’expérience, mais sont encore en progression. Ils s’attachent souvent aux « bonnes pratiques » et aux nouveaux outils tape-à-l’œil à la mode, mais ont du mal à partir du problème à résoudre et à se concentrer sur le strict minimum nécessaire pour le résoudre.
Dans une organisation moyenne ou grande, ce n’est généralement pas le cas. En général, on passe à la tâche suivante et on a rarement le temps d’y revenir. C’est regrettable, mais c’est pour cela qu’il faut bien faire les choses dès le départ et réduire les occasions de créer des bugs ou des effets de bord.
Dans trop de codebases, la précipitation pour ajouter de nouvelles fonctionnalités a transformé le code en champ de mines, au point que le moindre petit changement obligeait à vérifier manuellement toutes les fonctionnalités et à garder en tête le contexte de toute l’application.
D’après mon expérience, quand de nouvelles exigences arrivent plus tard, la solution générique conçue auparavant se révèle totalement inadaptée et il faut de toute façon la refaire. Il faut résoudre le problème qu’on a sous les yeux, pas un problème futur inconnu.
Il n’est donc pas étonnant que les gens essaient d’écrire eux-mêmes quelque chose dès qu’ils en ont l’occasion. Ce genre de corvée de plomberie numérique épuise, et pour rester sain d’esprit un peu plus longtemps, on a envie d’intercaler de temps en temps quelque chose de plus amusant.
Le site semble être tombé sous l’afflux de connexions, donc je laisse un cache : https://web.archive.org/web/20240606103630/https://edw.is/le...
Excellent article. J’ai appris Vulkan par moi-même pour créer un moteur de visualisation de données scientifiques : https://datoviz.org/ C’est encore assez expérimental, et une nouvelle version devrait sortir bientôt.
J’avais déjà des connaissances en OpenGL, mais apprendre Vulkan a été vraiment difficile. Il y a cinq ans, les ressources d’apprentissage n’étaient pas très bonnes non plus. J’ai tout de même relevé le défi, et c’était très amusant.
Il m’a fallu des mois pour comprendre le rôle des dizaines d’abstractions, et au passage j’ai aussi écrit un petit wrapper pour rendre Vulkan un peu moins pénible à utiliser : https://datoviz.org/api/vklite/
Ce wrapper ne prend en charge qu’une partie des fonctionnalités les plus nécessaires à la visualisation scientifique.
Obtenir de meilleures performances qu’OpenGL avec Vulkan n’est pas simple. Dans les pilotes Vulkan, il manque environ 20 000 lignes de code que les pilotes OpenGL fournissaient pour configurer à votre place le pipeline de rendu et les render targets.
Ce code existe déjà dans les pilotes OpenGL, et certaines des meilleures personnes du secteur l’optimisent depuis plus de vingt ans.
Donc assembler naïvement par-dessus Vulkan l’équivalent de ce qu’OpenGL fournissait par défaut ne donne pas magiquement de bonnes performances. Il faut davantage de travail, et les vrais problèmes commencent à s’accumuler, comme placer les bons fences et les bonnes primitives de synchronisation.
Ce n’est que lorsque vous savez réellement ce que vous faites, et que vous pouvez exécuter le rendu avec un bon parallélisme et une synchronisation correcte, que vous pouvez espérer tirer un avantage de performance de Vulkan.
En tant que développeur amateur, j’utilise OpenGL ES3 pour sa simplicité. C’est déjà largement suffisant pour moi, et j’ai des choses plus urgentes à faire que d’écrire ces pénibles descripteurs descripteurs descripteurs de sommets Vulkan.
Pour référence, mon moteur est ici : https://github.com/ensisoft/detonator
J’ai entendu dire que Vulkan autorise désormais les textures bindless, donc le bazar des descripteurs devrait être moins horrible qu’avant.
Vulkan est séduisant, mais il a un coût initial élevé que je n’ai pas envie de payer.
Ensuite, on pourrait remplacer progressivement cette bibliothèque par des routines optimisées pour ses cas d’usage.
C’est une bonne chose qu’il y ait davantage de ressources sur Vulkan, mais celle-ci souffre du même problème que toutes les ressources Vulkan que j’ai vues dès qu’il s’agit d’afficher quelque chose à l’écran
Avant même de montrer un cas simple, elles introduisent toutes une couche d’abstraction supplémentaire au-dessus de Vulkan. On conseille toujours d’utiliser vk-bootstrap, volk, vma ou d’autres bibliothèques
Je ne sais pas s’il existe une seule ressource qui montre un exemple de gestion de la mémoire manuelle. On a l’impression que les seules options sont d’utiliser vma ou de se plonger directement dans la spécification. Est-ce vraiment trop demander qu’un exemple le plus basique possible, sans aucune bibliothèque ajoutée en dehors du Vulkan SDK lui-même ?
La plupart des jeux ont grosso modo trois durées de vie : permanente/au démarrage, par niveau, et par frame
Ces durées de vie étant imbriquées, on peut déjà aller assez loin avec un seul allocateur de pile. À la fin d’une frame ou d’un niveau, il suffit de revenir à la position de départ
Il existe des schémas plus complexes, mais cette approche est déjà très utile, et peut servir aussi bien côté CPU que côté GPU
De ce point de vue, l’usage massif de bibliothèques par-dessus est tout à fait normal. C’est comme le fait qu’aujourd’hui on n’écrit pas généralement du logiciel directement contre les appels système
Quand on compare les consignes d’initialisation Vulkan de quelqu’un avec le code du dépôt du Khronos Group, puis qu’on lit la spécification Vulkan 1.3 et qu’on se rend compte que, pour faire quelque chose, il faut lire la spécification dans le désordre plutôt que de manière séquentielle, il devient évident que c’est un échec
Ils ont échoué. Même selon d’autres critères, c’est un mauvais travail. C’est juste que les spécialistes ne s’en plaignent pas trop, parce qu’on peut le faire une fois puis l’oublier en grande partie
J’ai laissé, dans un autre commentaire de ce fil, un bout de code source annoté avec les chapitres et sections de la spécification. C’est une implémentation générique utilisable avec SDL et autres
À l’heure où j’écris, l’approche standard consiste à utiliser VMA et Volk, inclus dans le Vulkan SDK officiel. Rien que cela en dit assez long sur l’état actuel
Pendant des années, j’ai essayé d’apprendre Vulkan par intermittence. Avant, je connaissais assez bien OpenGL ES 2 et 3
L’une des difficultés a été de comprendre comment l’utiliser dans un vrai moteur, et pas seulement dans des exemples. Beaucoup d’exemples allouent juste exactement ce qu’il faut, ou bien en allouent des centaines pour être sûrs de ne jamais en manquer
Quand j’ai appris DirectX, le MiniEngine de Microsoft m’a aidé, parce qu’il contenait des choses comme un DescriptorAllocator pour gérer l’allocation de descripteurs sans être trop complexe. Je me demande s’il existe quelque chose de similaire pour Vulkan
Une autre difficulté est de savoir comment créer de bonnes abstractions pour les matériaux, les meshes, l’ordre de rendu, etc. Y a-t-il de bons moteurs ou frameworks à étudier pour dépasser le stade des tutoriels ?
Pour l’allocation des descriptor sets, un seul schéma me paraît logique : supposer que les pools ont une durée de vie courte et qu’ils seront nombreux. Si l’allocation depuis le pool courant échoue, on crée un nouveau pool, sans maintenir soi-même un compteur du nombre de descripteurs. Le standard autorise toutes sortes de comportements de pool au-delà du simple comptage. Il suffit de jeter les anciens pools une fois terminé le dernier command buffer qui les référence
Les pipeline barriers et les layouts d’images sont vraiment pénibles. Mieux vaut les abstraire derrière une couche qui suit la dernière utilisation et le dernier format de chaque ressource, puis ajoute les barrières nécessaires. Cela peut devenir complexe, mais cela en vaut la peine dès qu’on a des situations plus sophistiquées, comme des passes optionnelles ou un ordre de passes modifiable
Les meshes, matériaux et l’ordre de rendu sont difficiles à résumer dans un seul commentaire HN, et dépendent fortement du choix de l’algorithme de rendu. À mon avis, l’effort énorme nécessaire pour construire correctement une solution très généralisée n’en vaut pas la peine
Pour les lecteurs curieux qui se demandent ce qu’il faut pour écrire un “Hello, Triangle!” avec Vulkan 1.3, voici un exemple : https://github.com/Planimeter/game-engine-3d/blob/main/src/g...
L’initialisation de Vulkan et la gestion de base de la swapchain sont très verbeuses, mais une fois qu’on l’a fait une fois, puis qu’on crée ensuite des abstractions pratiques pour la création et la gestion des pipelines, cela devient beaucoup plus agréable