Apprenez OpenGL. Leçon 7.1 - Débogage

image La programmation graphique n'est pas seulement une source de plaisir, mais aussi de frustration lorsque quelque chose ne s'affiche pas comme prévu, ou que rien ne s'affiche à l'écran. Étant donné que la plupart de ce que nous faisons est lié à la manipulation des pixels, il peut être difficile de déterminer la cause de l'erreur lorsque quelque chose ne fonctionne pas comme il se doit. Le débogage de ce type d'erreur est plus difficile que le débogage des erreurs sur le CPU. Nous n'avons pas de console où nous pouvons afficher le texte, nous ne pouvons pas mettre un point d'arrêt dans le shader et nous ne pouvons pas simplement prendre et vérifier l'état du programme sur le GPU.


Dans ce didacticiel, nous vous présenterons certaines des méthodes et techniques de débogage de votre programme OpenGL. Le débogage dans OpenGL n'est pas si difficile, et l'apprentissage de quelques astuces sera certainement payant.



glGetError ()


Lorsque vous utilisez incorrectement OpenGL (par exemple, lorsque vous configurez un tampon et oubliez de le lier), OpenGL remarquera et créera un ou plusieurs indicateurs d'erreur personnalisés en arrière-plan. Nous pouvons suivre ces erreurs en appelant la fonction glGetError() , qui vérifie simplement l'ensemble d'indicateurs d'erreur et renvoie la valeur d'erreur si des erreurs se produisent.


 GLenum glGetError(); 

Cette fonction renvoie un indicateur d'erreur ou aucune erreur du tout. Liste des valeurs de retour:


DrapeauCodeLa description
GL_NO_ERROR0Aucune erreur générée depuis le dernier appel glGetError
GL_INVALID_ENUM1280Défini lorsqu'un paramètre d'énumération n'est pas valide
GL_INVALID_VALUE1281Définir lorsque la valeur n'est pas valide
GL_INVALID_OPERATION1282Définir lorsqu'une commande avec des paramètres spécifiés n'est pas valide
GL_STACK_OVERFLOW1283Elle est établie lorsque l'opération consistant à pousser des données sur la pile (push) provoque un débordement de pile.
GL_STACK_UNDERFLOW1284Elle est établie lorsque l'opération d'extraction des données de la pile (pop) se produit à partir du plus petit point de la pile.
GL_OUT_OF_MEMORY1285Défini lorsqu'une opération d'allocation de mémoire ne peut pas allouer suffisamment de mémoire.
GL_INVALID_FRAMEBUFFER_OPERATION1286Défini lors de la lecture / écriture vers / depuis un tampon de cadre qui n'est pas terminé

Dans la documentation des fonctions OpenGL, vous pouvez trouver des codes d'erreur générés par des fonctions mal utilisées. Par exemple, si vous consultez la documentation de la fonction glBindTexture() , vous pouvez trouver les codes d'erreur générés par cette fonction dans la section Erreurs.
Lorsque l'indicateur d'erreur est défini, aucun autre indicateur d'erreur n'est généré. De plus, lorsque glGetError est appelée, la fonction efface tous les drapeaux d'erreur (ou un seul sur un système distribué, voir ci-dessous). Cela signifie que si vous appelez glGetError une fois après chaque trame et obtenez une erreur, cela ne signifie pas que c'est la seule erreur et vous ne savez toujours pas où cette erreur s'est produite.


Notez que lorsque OpenGL fonctionne de manière distribuée, comme c'est souvent le cas sur les systèmes avec X11, d'autres erreurs peuvent être générées alors qu'elles ont des codes différents. L'appel de glGetError ensuite simplement l'un des indicateurs de code d'erreur au lieu de tous. Pour cette raison, ils recommandent d'appeler cette fonction en boucle.

 glBindTexture(GL_TEXTURE_2D, tex); std::cout << glGetError() << std::endl; //  0 ( ) glTexImage2D(GL_TEXTURE_3D, 0, GL_RGB, 512, 512, 0, GL_RGB, GL_UNSIGNED_BYTE, data); std::cout << glGetError() << std::endl; //  1280 ( ) glGenTextures(-5, textures); std::cout << glGetError() << std::endl; //  1281 (  std::cout << glGetError() << std::endl; //  0 ( ) 

Une caractéristique distinctive de glGetError est qu'il permet de déterminer relativement facilement où une erreur peut se produire et de vérifier que OpenGL est utilisé correctement. Imaginons que vous ne dessiniez rien et que vous ne connaissiez pas la raison: le tampon de trame est mal réglé? Vous avez oublié de définir la texture? En appelant glGetError partout, vous pouvez rapidement déterminer où se produit la première erreur.
Par défaut, glGetError ne signale que le numéro d'erreur, ce qui n'est pas facile à comprendre tant que vous n'avez pas mémorisé les numéros de code. Il est souvent judicieux d'écrire une petite fonction pour aider à imprimer une chaîne d'erreur avec l'emplacement à partir duquel la fonction est appelée.


 GLenum glCheckError_(const char *file, int line) { GLenum errorCode; while ((errorCode = glGetError()) != GL_NO_ERROR) { std::string error; switch (errorCode) { case GL_INVALID_ENUM: error = "INVALID_ENUM"; break; case GL_INVALID_VALUE: error = "INVALID_VALUE"; break; case GL_INVALID_OPERATION: error = "INVALID_OPERATION"; break; case GL_STACK_OVERFLOW: error = "STACK_OVERFLOW"; break; case GL_STACK_UNDERFLOW: error = "STACK_UNDERFLOW"; break; case GL_OUT_OF_MEMORY: error = "OUT_OF_MEMORY"; break; case GL_INVALID_FRAMEBUFFER_OPERATION: error = "INVALID_FRAMEBUFFER_OPERATION"; break; } std::cout << error << " | " << file << " (" << line << ")" << std::endl; } return errorCode; } #define glCheckError() glCheckError_(__FILE__, __LINE__) 

Si vous décidez de faire plus d'appels à glCheckError , il sera utile de savoir où l'erreur s'est produite.


 glBindBuffer(GL_VERTEX_ARRAY, vbo); glCheckError(); 

Conclusion:



Une chose importante reste: il y a un bogue de longue date dans GLEW: glewInit() définit toujours l'indicateur GL_INVALID_ENUM . Pour résoudre ce problème, appelez simplement glGetError après glewInit pour effacer l'indicateur:


 glewInit(); glGetError(); 

glGetError n'aide pas beaucoup, car les informations renvoyées sont relativement simples, mais elles aident souvent à détecter les fautes de frappe ou à localiser l'endroit où l'erreur s'est produite. Il s'agit d'un outil de débogage simple mais efficace.


Sortie de débogage


L'outil est moins connu, mais plus utile que glCheckError , l'extension "debug output" d'OpenGL, qui était incluse dans le profil principal d'OpenGL 4.3. Avec cette extension, OpenGL enverra un message d'erreur à l'utilisateur avec les détails de l'erreur. Cette extension fournit non seulement plus d'informations, mais vous permet également de détecter les erreurs là où elles se produisent à l'aide du débogueur.


La sortie de débogage est incluse dans OpenGL à partir de la version 4.3, ce qui signifie que vous trouverez cette fonctionnalité sur n'importe quelle machine prenant en charge OpenGL 4.3 et supérieur. Si cette version n'est pas disponible, vous pouvez vérifier les extensions ARB_debug_output et AMD_debug_output . Il existe également des informations non vérifiées selon lesquelles la sortie de débogage n'est pas prise en charge sur OS X (l'auteur de l'original et le traducteur n'ont pas testé, veuillez en informer l'auteur de l'original ou à moi-même dans des messages privés via le mécanisme de correction d'erreurs, si vous trouvez une confirmation ou une réfutation de ce fait; UPD: Jeka178RUS a vérifié cela fait: hors de la boîte, la sortie de débogage ne fonctionne pas, il n'a pas vérifié les extensions).

Pour commencer à utiliser la sortie de débogage, nous devons demander le contexte de débogage OpenGL pendant le processus d'initialisation. Ce processus est différent sur différents systèmes de fenêtres, mais ici nous ne discuterons que de GLFW, mais à la fin de l'article dans la section "Matériaux supplémentaires", vous pouvez trouver des informations sur d'autres systèmes de fenêtres.


Déboguer la sortie dans GLFW


La demande de contextes de débogage dans GLFW est étonnamment simple: tout ce que vous devez faire est de donner à GLFW une indication que nous voulons un contexte qui prend en charge la sortie de débogage. Nous devons le faire avant d'appeler glfwCreateWindow :


 glfwWindowHint(GLFW_OPENGL_DEBUG_CONTEXT, GL_TRUE); 

Dès que nous avons initialisé GLFW, nous devrions avoir un contexte de débogage si nous utilisons OpenGL 4.3 ou supérieur, sinon nous devons tenter notre chance et espérons que le système pourra toujours créer un contexte de débogage. En cas d'échec, nous devons demander une sortie de débogage via le mécanisme d'extension OpenGL.


Le contexte de débogage OpenGL peut être plus lent que la normale, vous devez donc supprimer ou commenter cette ligne lorsque vous travaillez sur des optimisations ou avant la publication.

Pour vérifier le résultat de l'initialisation du contexte de débogage, il suffit d'exécuter le code suivant:


 GLint flags; glGetIntegerv(GL_CONTEXT_FLAGS, &flags); if (flags & GL_CONTEXT_FLAG_DEBUG_BIT) { //  } else { //   } 

Comment fonctionne la sortie de débogage? Nous passons une fonction de rappel à un gestionnaire de messages dans OpenGL (similaire aux rappels dans GLFW) et dans cette fonction, nous pouvons traiter les données OpenGL comme nous le souhaitons, dans notre cas, envoyer des messages d'erreur utiles à la console. Le prototype de cette fonction:


 void APIENTRY glDebugOutput(GLenum source, GLenum type, GLuint id, GLenum severity, GLsizei length, const GLchar *message, void *userParam); 

Notez que sur certains systèmes d'exploitation, le type du dernier paramètre peut être const void* .
Étant donné le grand ensemble de données dont nous disposons, nous pouvons créer un outil d'impression d'erreur utile, comme indiqué ci-dessous:


 void APIENTRY glDebugOutput(GLenum source, GLenum type, GLuint id, GLenum severity, GLsizei length, const GLchar *message, void *userParam) { // ignore non-significant error/warning codes if(id == 131169 || id == 131185 || id == 131218 || id == 131204) return; std::cout << "---------------" << std::endl; std::cout << "Debug message (" << id << "): " << message << std::endl; switch (source) { case GL_DEBUG_SOURCE_API: std::cout << "Source: API"; break; case GL_DEBUG_SOURCE_WINDOW_SYSTEM: std::cout << "Source: Window System"; break; case GL_DEBUG_SOURCE_SHADER_COMPILER: std::cout << "Source: Shader Compiler"; break; case GL_DEBUG_SOURCE_THIRD_PARTY: std::cout << "Source: Third Party"; break; case GL_DEBUG_SOURCE_APPLICATION: std::cout << "Source: Application"; break; case GL_DEBUG_SOURCE_OTHER: std::cout << "Source: Other"; break; } std::cout << std::endl; switch (type) { case GL_DEBUG_TYPE_ERROR: std::cout << "Type: Error"; break; case GL_DEBUG_TYPE_DEPRECATED_BEHAVIOR: std::cout << "Type: Deprecated Behaviour"; break; case GL_DEBUG_TYPE_UNDEFINED_BEHAVIOR: std::cout << "Type: Undefined Behaviour"; break; case GL_DEBUG_TYPE_PORTABILITY: std::cout << "Type: Portability"; break; case GL_DEBUG_TYPE_PERFORMANCE: std::cout << "Type: Performance"; break; case GL_DEBUG_TYPE_MARKER: std::cout << "Type: Marker"; break; case GL_DEBUG_TYPE_PUSH_GROUP: std::cout << "Type: Push Group"; break; case GL_DEBUG_TYPE_POP_GROUP: std::cout << "Type: Pop Group"; break; case GL_DEBUG_TYPE_OTHER: std::cout << "Type: Other"; break; } std::cout << std::endl; switch (severity) { case GL_DEBUG_SEVERITY_HIGH: std::cout << "Severity: high"; break; case GL_DEBUG_SEVERITY_MEDIUM: std::cout << "Severity: medium"; break; case GL_DEBUG_SEVERITY_LOW: std::cout << "Severity: low"; break; case GL_DEBUG_SEVERITY_NOTIFICATION: std::cout << "Severity: notification"; break; } std::cout << std::endl; std::cout << std::endl; } 

Lorsque l'extension détecte une erreur OpenGL, elle appelle cette fonction et nous pouvons imprimer une énorme quantité d'informations sur les erreurs. Notez que nous avons ignoré certaines erreurs, car elles sont inutiles (par exemple, 131185 dans les pilotes NVidia indique que le tampon a été créé avec succès).
Maintenant que nous avons le rappel souhaité, il est temps d'initialiser la sortie de débogage:


 if (flags & GL_CONTEXT_FLAG_DEBUG_BIT) { glEnable(GL_DEBUG_OUTPUT); glEnable(GL_DEBUG_OUTPUT_SYNCHRONOUS); glDebugMessageCallback(glDebugOutput, nullptr); glDebugMessageControl(GL_DONT_CARE, GL_DONT_CARE, GL_DONT_CARE, 0, nullptr, GL_TRUE); } 

Nous disons donc à OpenGL que nous voulons activer la sortie de débogage. L'appel à glEnable(GL_DEBUG_SYNCRHONOUS) indique à OpenGL que nous voulons un message d'erreur quand il vient de se produire.


Débogage du filtrage de sortie


Avec la fonction glDebugMessageControl vous pouvez sélectionner les types d'erreurs que vous souhaitez recevoir. Dans notre cas, nous obtenons toutes sortes d'erreurs. Si nous voulions uniquement les erreurs de l'API OpenGL, telles que Erreur et le niveau de signification élevé, nous écririons le code suivant:


 glDebugMessageControl(GL_DEBUG_SOURCE_API, GL_DEBUG_TYPE_ERROR, GL_DEBUG_SEVERITY_HIGH, 0, nullptr, GL_TRUE); 

Avec ce contexte de configuration et de débogage, chaque commande OpenGL incorrecte enverra beaucoup d'informations utiles:



Trouver la source de l'erreur via la pile d'appels


Une autre astuce avec la sortie de débogage est que vous pouvez établir relativement facilement l'emplacement exact de l'erreur dans votre code. En définissant un point d'arrêt dans la fonction DebugOutput sur le type d'erreur souhaité (ou au début de la fonction si vous souhaitez intercepter toutes les erreurs), le débogueur intercepte l'erreur et vous pouvez parcourir la pile des appels pour savoir où l'erreur s'est produite:



Cela nécessite une intervention manuelle, mais si vous savez à peu près ce que vous recherchez, il est extrêmement utile de déterminer rapidement quel appel est à l'origine de l'erreur.


Propres erreurs


En plus des erreurs de lecture, nous pouvons les envoyer au système de sortie de débogage à l'aide de glDebugMessageInsert :


 glDebugMessageInsert(GL_DEBUG_SOURCE_APPLICATION, GL_DEBUG_TYPE_ERROR, 0, GL_DEBUG_SEVERITY_MEDIUM, -1, "error message here"); 

Ceci est très utile si vous vous connectez à une autre application ou à un code OpenGL qui utilise un contexte de débogage. Les autres développeurs pourront découvrir rapidement toute erreur signalée qui se produit dans votre code OpenGL personnalisé.
En général, la sortie de débogage (si disponible) est très utile pour détecter rapidement les erreurs et vaut vraiment la peine d'être consacrée à l'optimisation, car elle économise un temps de développement important. Vous pouvez trouver une copie du code source ici en utilisant glGetError et une sortie de débogage. Il y a des erreurs, essayez de les corriger.


Sortie de débogage de shader


En ce qui concerne GLSL, nous n'avons pas accès à des fonctions comme glGetError ou la possibilité de parcourir le code par étapes dans le débogueur. Lorsque vous rencontrez un écran noir ou un affichage complètement incorrect, il peut être très difficile de comprendre ce qui se passe si le problème est dans le shader. Oui, les erreurs de compilation signalent des erreurs de syntaxe, mais la capture d'erreurs sémantiques est ce morceau.
L'une des méthodes couramment utilisées pour découvrir ce qui ne va pas avec un shader est d'envoyer toutes les variables pertinentes du programme de shader directement au canal de sortie du fragment shader. En sortant des variables de shader directement sur le canal de sortie avec la couleur, nous pouvons trouver des informations intéressantes en vérifiant l'image à la sortie. Par exemple, nous devons savoir si les normales sont correctes pour le modèle. Nous pouvons les envoyer (transformés ou non) du sommet au fragment shader, où nous dérivons les normales quelque chose comme ceci:
(remarque: pourquoi n'y a-t-il pas de mise en évidence de la syntaxe pour GLSL?)


 #version 330 core out vec4 FragColor; in vec3 Normal; [...] void main() { [...] FragColor.rgb = Normal; FragColor.a = 1.0f; } 

En sortant une variable non colorée sur le canal de sortie avec la couleur telle qu'elle est maintenant, nous pouvons rapidement vérifier la valeur de la variable. Si, par exemple, le résultat est un écran noir, il est clair que les normales sont incorrectement transférées aux shaders, et lorsqu'elles sont affichées, il est relativement facile de vérifier leur exactitude:



D'après les résultats visuels, nous pouvons voir que les normales sont correctes, car le côté droit de la combinaison est principalement rouge (ce qui signifie que les normales apparaissent approximativement dans la direction de l'axe x de rinçage) et que la face avant de la combinaison est colorée dans la direction de l'axe z positif (bleu).


Cette approche peut être étendue à toute variable que vous souhaitez tester. Chaque fois que vous êtes bloqué et supposez que l'erreur se trouve dans les shaders, essayez de dessiner des variables ou des résultats intermédiaires et découvrez dans quelle partie de l'algorithme il y a une erreur.


Compilateur de référence OpenGL GLSL


Chaque pilote vidéo a ses propres bizarreries. Par exemple, les pilotes NVIDIA adoucissent légèrement les exigences de la spécification et les pilotes AMD répondent mieux aux spécifications (ce qui est mieux, il me semble). Le problème est que les shaders fonctionnant sur une machine peuvent ne pas gagner d'argent sur une autre en raison des différences de pilotes.


Pendant plusieurs années d'expérience, vous pouvez apprendre toutes les différences entre les différents GPU, mais si vous voulez être sûr que vos shaders fonctionneront partout, vous pouvez vérifier votre code avec les spécifications officielles en utilisant le compilateur de référence GLSL . Vous pouvez télécharger le soi-disant validateur GLSL lang ici ( source ).


Avec ce programme, vous pouvez tester vos shaders en les passant comme 1er argument au programme. N'oubliez pas que le programme détermine le type de shader par extension:


  • .vert : vertex shader
  • .frag : fragment shader
  • .geom : shader géométrique
  • .tesc : .tesc contrôlant le shader
  • .tese : shader informatique de .tese
  • .comp : shader de calcul

L'exécution du programme est simple:


 glslangValidator shader.vert 

Notez que s'il n'y a pas d'erreur, le programme ne sortira rien. Sur un vertex shader cassé, la sortie ressemblera à:



Le programme ne montrera pas les différences entre les compilateurs GLSL d'AMD, NVidia ou Intel, et ne peut même pas signaler tous les bogues dans le shader, mais il vérifie au moins la conformité des shaders avec les normes.


Sortie du tampon de trame


Une autre méthode pour votre boîte à outils consiste à afficher le contenu du tampon de trame dans une partie spécifique de l'écran. Très probablement, vous utilisez souvent des tampons d'images, et comme toute la magie se produit dans les coulisses, il peut être difficile de déterminer ce qui se passe. La sortie du contenu du tampon de trame est une astuce utile pour vérifier que les choses sont correctes.


Notez que le contenu du tampon de cadre, comme expliqué ici, fonctionne avec des textures, pas avec des objets dans les tampons de dessin

En utilisant un shader simple qui dessine une seule texture, nous pouvons écrire une petite fonction qui dessine rapidement n'importe quelle texture dans le coin supérieur droit de l'écran:


 // vertex shader #version 330 core layout (location = 0) in vec2 position; layout (location = 1) in vec2 texCoords; out vec2 TexCoords; void main() { gl_Position = vec4(position, 0.0f, 1.0f); TexCoords = texCoords; } 

 //fragment shader #version 330 core out vec4 FragColor; in vec2 TexCoords; uniform sampler2D fboAttachment; void main() { FragColor = texture(fboAttachment, TexCoords); } 

 //main.cpp void DisplayFramebufferTexture(GLuint textureID) { if(!notInitialized) { // initialize shader and vao w/ NDC vertex coordinates at top-right of the screen [...] } glActiveTexture(GL_TEXTURE0); glUseProgram(shaderDisplayFBOOutput); glBindTexture(GL_TEXTURE_2D, textureID); glBindVertexArray(vaoDebugTexturedRect); glDrawArrays(GL_TRIANGLES, 0, 6); glBindVertexArray(0); glUseProgram(0); } int main() { [...] while (!glfwWindowShouldClose(window)) { [...] DisplayFramebufferTexture(fboAttachment0); glfwSwapBuffers(window); } } 

Cela vous donnera une petite fenêtre dans le coin de l'écran pour déboguer la sortie du tampon de trame. Il est utile, par exemple, lorsque vous essayez de vérifier l'exactitude des normales:



Vous pouvez également développer cette fonction afin qu'elle rende plus d'une texture. Il s'agit d'un moyen rapide d'obtenir une rétroaction continue de n'importe quoi dans les tampons de trame.


Programmes de débogage externes


Lorsque tout le reste échoue, il y a une autre astuce: utiliser des programmes tiers. Ils sont intégrés au pilote OpenGL et peuvent intercepter tous les appels OpenGL pour vous fournir de nombreuses données intéressantes sur votre application. Ces applications peuvent profiler l'utilisation des fonctions OpenGL, rechercher les goulots d'étranglement et surveiller les tampons de trame, les textures et la mémoire. Tout en travaillant sur du (gros) code, ces outils peuvent devenir inestimables.


J'ai énuméré plusieurs outils populaires. Essayez chacun et choisissez celui qui vous convient le mieux.


Renderderoc


RenderDoc est un bon outil de débogage séparé (entièrement ouvert ). Pour démarrer la capture, sélectionnez le fichier exécutable et le répertoire de travail. Votre application fonctionne comme d'habitude, et lorsque vous souhaitez regarder une seule image, vous autorisez RenderDoc à capturer plusieurs images de votre application. Parmi les trames capturées, vous pouvez afficher l'état du pipeline, toutes les commandes OpenGL, le stockage du tampon et les textures utilisées.



Codexl


CodeXL - Outil de débogage GPU, fonctionne comme une application autonome et un plugin pour Visual Studio. CodeXL Fournit beaucoup d'informations et est idéal pour le profilage d'applications graphiques. CodeXL fonctionne également sur les cartes graphiques de NVidia et Intel, mais sans prise en charge du débogage OpenCL.



Je n'utilisais pas beaucoup CodeXL, car RenderDoc me semblait plus facile, mais j'ai inclus CodeXL dans cette liste car il ressemble à un outil assez fiable et est principalement développé par l'un des plus grands fabricants de GPU.


NVIDIA Nsight


Nsight est un outil de débogage NUIDIA GPU populaire. Ce n'est pas seulement un plug-in pour Visual Studio et Eclipse, mais aussi une application distincte . Le plugin Nsight est une chose très utile pour les développeurs graphiques car il collecte de nombreuses statistiques en temps réel concernant l'utilisation du GPU et l'état image par image du GPU.


Au moment où vous lancez votre application via Visual Studio ou Eclipse à l'aide des commandes de débogage ou du profilage Nsight, elle démarre à l'intérieur de l'application elle-même. Une bonne chose dans Nsight: rendre un système GUI (GUI, interface utilisateur graphique) au-dessus d'une application en cours d'exécution, qui peut être utilisé pour collecter toutes sortes d'informations sur votre application en temps réel ou analyse image par image.



Nsight est un outil très utile qui, à mon avis, surpasse les outils ci-dessus, mais présente un sérieux inconvénient: il ne fonctionne que sur les cartes graphiques NVIDIA. Si vous utilisez des cartes graphiques NVIDIA et utilisez Visual Studio, alors Nsight vaut vraiment la peine d'être essayé.


, ( , VOGL APItrace ), , . , , () ( , ).



  • ? — Reto Koradi.
  • — Vallentin Source.

PS : Nous avons un télégramme conf pour la coordination des transferts. Si vous avez un sérieux désir d'aider à la traduction, alors vous êtes les bienvenus!

Source: https://habr.com/ru/post/fr462897/


All Articles