Grafische Programmierung ist nicht nur eine Quelle des Spaßes, sondern auch der Frustration, wenn etwas nicht wie beabsichtigt angezeigt wird oder überhaupt nichts auf dem Bildschirm angezeigt wird. Angesichts der Tatsache, dass das meiste, was wir tun, mit der Manipulation von Pixeln zusammenhängt, kann es schwierig sein, die Ursache des Fehlers herauszufinden, wenn etwas nicht so funktioniert, wie es sollte. Das Debuggen dieser Art von Fehler ist schwieriger als das Debuggen von Fehlern auf der CPU. Wir haben keine Konsole, auf der wir den Text ausgeben können, wir können keinen Haltepunkt in den Shader setzen und wir können nicht einfach den Status des Programms auf der GPU übernehmen und überprüfen.
In diesem Tutorial werden wir Ihnen einige der Debugging-Methoden und -Techniken für Ihr OpenGL-Programm vorstellen. Das Debuggen in OpenGL ist nicht so schwierig, und das Erlernen einiger Tricks wird sich definitiv auszahlen.
InhaltTeil 3. Laden Sie 3D-Modelle herunter Teil 4. Erweiterte OpenGL-Funktionen Teil 5. Erweiterte Beleuchtung glGetError ()
Wenn Sie OpenGL falsch verwenden (z. B. wenn Sie einen Puffer einrichten und vergessen, ihn zu binden), bemerkt OpenGL hinter den Kulissen ein oder mehrere benutzerdefinierte Fehlerflags und erstellt diese. Wir können diese Fehler verfolgen, indem glGetError()
Funktion glGetError()
aufrufen, die einfach die gesetzten glGetError()
überprüft und den Fehlerwert zurückgibt, wenn Fehler auftreten.
GLenum glGetError();
Diese Funktion gibt ein Fehlerflag oder gar keinen Fehler zurück. Liste der Rückgabewerte:
In der Dokumentation zu OpenGL-Funktionen finden Sie Fehlercodes, die von falsch verwendeten Funktionen generiert werden. Wenn Sie sich beispielsweise die Dokumentation zur Funktion glBindTexture()
, finden Sie die von dieser Funktion generierten Fehlercodes im Abschnitt Fehler.
Wenn das Fehlerflag gesetzt ist, werden keine weiteren Fehlerflags generiert. Wenn glGetError
aufgerufen wird, löscht die Funktion außerdem alle glGetError
(oder nur eines auf einem verteilten System, siehe unten). Dies bedeutet, dass wenn Sie glGetError
nach jedem Frame einmal aufrufen und einen Fehler erhalten, dies nicht bedeutet, dass dies der einzige Fehler ist und Sie immer noch nicht wissen, wo dieser Fehler aufgetreten ist.
Beachten Sie, dass, wenn OpenGL auf verteilte Weise arbeitet, wie dies häufig bei Systemen mit X11 der Fall ist, andere Fehler generiert werden können, wenn sie unterschiedliche Codes haben. glGetError
Sie glGetError
wird einfach nur eines der Fehlercode-Flags anstelle von allen glGetError
. Aus diesem Grund empfehlen sie, diese Funktion in einer Schleife aufzurufen.
glBindTexture(GL_TEXTURE_2D, tex); std::cout << glGetError() << std::endl;
Eine Besonderheit von glGetError
besteht darin, dass es relativ einfach ist, festzustellen, wo Fehler auftreten können, und zu überprüfen, ob OpenGL korrekt verwendet wird. Angenommen, Sie zeichnen nichts und wissen nicht, was der Grund ist: Der Frame-Puffer ist falsch eingestellt? Vergessen, die Textur einzustellen? Wenn Sie glGetError
überall aufrufen, können Sie schnell herausfinden, wo der erste Fehler auftritt.
Standardmäßig meldet glGetError
nur die Fehlernummer, die erst dann leicht zu verstehen ist, wenn Sie sich die Codenummern merken. Oft ist es sinnvoll, eine kleine Funktion zu schreiben, um eine Fehlerzeichenfolge zusammen mit der Position zu drucken, von der aus die Funktion aufgerufen wird.
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__)
Wenn Sie weitere Aufrufe von glCheckError
tätigen glCheckError
, ist es hilfreich zu wissen, wo der Fehler aufgetreten ist.
glBindBuffer(GL_VERTEX_ARRAY, vbo); glCheckError();
Fazit:

Eine wichtige Sache bleibt: Es gibt einen langjährigen Fehler in GLEW: glewInit()
setzt immer das Flag GL_INVALID_ENUM
. Um dies zu beheben, rufen glGetError
einfach glGetError
nach glewInit
auf, um das Flag zu löschen:
glewInit(); glGetError();
glGetError
hilft nicht viel, da die zurückgegebenen Informationen relativ einfach sind, aber es hilft oft, Tippfehler zu erkennen oder die Stelle zu ermitteln, an der der Fehler aufgetreten ist. Dies ist ein einfaches, aber effektives Debugging-Tool.
Debug-Ausgabe
Das Tool ist weniger bekannt, aber nützlicher als glCheckError
, die OpenGL-Erweiterung "Debug-Ausgabe", die im OpenGL 4.3- glCheckError
enthalten war. Mit dieser Erweiterung sendet OpenGL eine Fehlermeldung mit den Details des Fehlers an den Benutzer. Diese Erweiterung bietet nicht nur weitere Informationen, sondern ermöglicht es Ihnen auch, Fehler mit dem Debugger abzufangen, wo sie auftreten.
Die Debug-Ausgabe ist in OpenGL ab Version 4.3 enthalten. Dies bedeutet, dass Sie diese Funktionalität auf jedem Computer finden, der OpenGL 4.3 und höher unterstützt. Wenn diese Version nicht verfügbar ist, können Sie die Erweiterungen ARB_debug_output
und AMD_debug_output
. Es gibt auch nicht überprüfte Informationen darüber, dass die Debugging-Ausgabe unter OS X nicht unterstützt wird (der Autor des Originals und der Übersetzer haben nicht getestet. Bitte informieren Sie den Autor des Originals oder mich in privaten Nachrichten über den Fehlerkorrekturmechanismus, wenn Sie eine Bestätigung oder Widerlegung dieser Tatsache finden. UPD: Jeka178RUS hat dies überprüft Tatsache: Standardmäßig funktioniert die Debug-Ausgabe nicht, er hat die Erweiterungen nicht überprüft.
Um die Debug-Ausgabe verwenden zu können, müssen wir während des Initialisierungsprozesses den OpenGL-Debug-Kontext anfordern. Dieser Prozess ist auf verschiedenen Fenstersystemen unterschiedlich, aber hier werden wir nur GLFW diskutieren, aber am Ende des Artikels im Abschnitt "Zusätzliche Materialien" finden Sie Informationen zu anderen Fenstersystemen.
Debug-Ausgabe in GLFW
Das Anfordern von Debugging-Kontexten in GLFW ist überraschend einfach: Sie müssen GLFW lediglich einen Hinweis geben, dass wir einen Kontext wünschen, der die Debugging-Ausgabe unterstützt. Wir müssen dies tun, bevor glfwCreateWindow
aufrufen:
glfwWindowHint(GLFW_OPENGL_DEBUG_CONTEXT, GL_TRUE);
Sobald wir GLFW initialisiert haben, sollten wir einen Debugging-Kontext haben, wenn wir OpenGL 4.3 oder höher verwenden. Andernfalls müssen wir unser Glück versuchen und hoffen, dass das System weiterhin einen Debugging-Kontext erstellen kann. Im Fehlerfall müssen wir die Debug-Ausgabe über den OpenGL-Erweiterungsmechanismus anfordern.
Der OpenGL-Debugging-Kontext kann langsamer als normal sein. Sie sollten diese Zeile daher entfernen oder auskommentieren, während Sie an Optimierungen arbeiten oder vor der Veröffentlichung.
Um das Ergebnis der Initialisierung des Debug-Kontexts zu überprüfen, reicht es aus, den folgenden Code auszuführen:
GLint flags; glGetIntegerv(GL_CONTEXT_FLAGS, &flags); if (flags & GL_CONTEXT_FLAG_DEBUG_BIT) {
Wie funktioniert die Debug-Ausgabe? Wir übergeben eine Rückruffunktion an einen Nachrichtenhandler in OpenGL (ähnlich wie Rückrufe in GLFW) und können in dieser Funktion OpenGL-Daten nach Belieben verarbeiten und in unserem Fall nützliche Fehlermeldungen an die Konsole senden. Der Prototyp dieser Funktion:
void APIENTRY glDebugOutput(GLenum source, GLenum type, GLuint id, GLenum severity, GLsizei length, const GLchar *message, void *userParam);
Beachten Sie, dass unter einigen Betriebssystemen der Typ des letzten Parameters möglicherweise const void*
.
Angesichts des großen Datensatzes können wir ein nützliches Fehlerdruckwerkzeug erstellen, wie unten gezeigt:
void APIENTRY glDebugOutput(GLenum source, GLenum type, GLuint id, GLenum severity, GLsizei length, const GLchar *message, void *userParam) {
Wenn die Erweiterung einen OpenGL-Fehler erkennt, ruft sie diese Funktion auf und wir können eine große Menge an Fehlerinformationen drucken. Beachten Sie, dass wir einige Fehler ignoriert haben, da sie unbrauchbar sind (z. B. 131185 in den NVidia-Treibern zeigt an, dass der Puffer erfolgreich erstellt wurde).
Nachdem wir den gewünschten Rückruf erhalten haben, ist es Zeit, die Debug-Ausgabe zu initialisieren:
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); }
Deshalb teilen wir OpenGL mit, dass wir die Debug-Ausgabe aktivieren möchten. Der Aufruf von glEnable(GL_DEBUG_SYNCRHONOUS)
teilt OpenGL mit, dass eine Fehlermeldung glEnable(GL_DEBUG_SYNCRHONOUS)
werden soll, wenn dies gerade passiert ist.
Debug-Ausgabefilterung
Mit der Funktion glDebugMessageControl
können Sie die Arten von Fehlern auswählen, die Sie erhalten möchten. In unserem Fall erhalten wir alle Arten von Fehlern. Wenn wir nur die OpenGL-API-Fehler wie Fehler und das Signifikanzniveau Hoch haben wollten, würden wir den folgenden Code schreiben:
glDebugMessageControl(GL_DEBUG_SOURCE_API, GL_DEBUG_TYPE_ERROR, GL_DEBUG_SEVERITY_HIGH, 0, nullptr, GL_TRUE);
In diesem Konfigurations- und Debugging-Kontext sendet jeder falsche OpenGL-Befehl viele nützliche Informationen:

Suchen Sie die Fehlerquelle über den Aufrufstapel
Ein weiterer Trick beim Debuggen der Ausgabe besteht darin, dass Sie den genauen Ort des Fehlers in Ihrem Code relativ einfach ermitteln können. Durch Festlegen eines Haltepunkts in der DebugOutput
Funktion für den gewünschten Fehlertyp (oder am Anfang der Funktion, wenn Sie alle Fehler abfangen möchten) erkennt der Debugger den Fehler und Sie können im Aufrufstapel navigieren, um herauszufinden, wo der Fehler aufgetreten ist:

Dies erfordert einige manuelle Eingriffe. Wenn Sie jedoch ungefähr wissen, wonach Sie suchen, ist es unglaublich nützlich, schnell festzustellen, welcher Anruf den Fehler verursacht.
Eigene Fehler
Zusammen mit Lesefehlern können wir sie mit glDebugMessageInsert
an das Debug-Ausgabesystem glDebugMessageInsert
:
glDebugMessageInsert(GL_DEBUG_SOURCE_APPLICATION, GL_DEBUG_TYPE_ERROR, 0, GL_DEBUG_SEVERITY_MEDIUM, -1, "error message here");
Dies ist sehr nützlich, wenn Sie eine Verbindung zu einer anderen Anwendung oder einem anderen OpenGL-Code herstellen, der einen Debugging-Kontext verwendet. Andere Entwickler können schnell alle gemeldeten Fehler in Ihrem benutzerdefinierten OpenGL-Code herausfinden.
Im Allgemeinen ist das Debuggen der Ausgabe (falls verfügbar) sehr nützlich, um Fehler schnell zu erkennen, und es lohnt sich auf jeden Fall, sich für die Optimierung einzusetzen, da dies erhebliche Entwicklungszeit spart. Eine Kopie des Quellcodes finden Sie hier mit glGetError
und Debug-Ausgabe. Es gibt Fehler, versuchen Sie sie zu beheben.
Shader-Debug-Ausgabe
Wenn es um GLSL geht, haben wir keinen Zugriff auf Funktionen wie glGetError
oder die Möglichkeit, den Code glGetError
im Debugger glGetError
. Wenn Sie auf einen schwarzen Bildschirm oder eine völlig falsche Anzeige stoßen, kann es sehr schwierig sein zu verstehen, was passiert, wenn das Problem im Shader liegt. Ja, Kompilierungsfehler melden Syntaxfehler, aber das Abfangen semantischer Fehler ist dieses Lied.
Eine der häufig verwendeten Methoden, um herauszufinden, was mit einem Shader nicht stimmt, besteht darin, alle relevanten Variablen im Shader-Programm direkt an den Ausgabekanal des Fragment-Shaders zu senden. Indem wir Shader-Variablen mit Farbe direkt auf den Ausgabekanal ausgeben, können wir interessante Informationen herausfinden, indem wir das Bild am Ausgang überprüfen. Zum Beispiel müssen wir herausfinden, ob die Normalen für das Modell korrekt sind. Wir können sie (transformiert oder nicht) vom Scheitelpunkt an den Fragment-Shader senden, wo wir die Normalen wie folgt ableiten:
(Hinweis: Warum gibt es keine Syntaxhervorhebung für GLSL?)
#version 330 core out vec4 FragColor; in vec3 Normal; [...] void main() { [...] FragColor.rgb = Normal; FragColor.a = 1.0f; }
Durch die Ausgabe einer nicht farbigen Variablen an den Ausgabekanal mit der aktuellen Farbe können wir den Wert der Variablen schnell überprüfen. Wenn das Ergebnis beispielsweise ein schwarzer Bildschirm ist, ist es klar, dass die Normalen falsch auf die Shader übertragen werden, und wenn sie angezeigt werden, ist es relativ einfach, sie auf Richtigkeit zu überprüfen:

Aus den visuellen Ergebnissen können wir erkennen, dass die Normalen korrekt sind, da die rechte Seite des Anzugs überwiegend rot ist (was bedeutet, dass die Normalen ungefähr in Richtung der x-Spülachse angezeigt werden) und auch die Vorderseite des Anzugs in Richtung der positiven z-Achse (blau) gefärbt ist.
Dieser Ansatz kann auf jede Variable erweitert werden, die Sie testen möchten. Versuchen Sie jedes Mal, wenn Sie nicht weiterkommen und davon ausgehen, dass der Fehler in den Shadern liegt, einige Variablen oder Zwischenergebnisse zu zeichnen und herauszufinden, in welchem Teil des Algorithmus ein Fehler vorliegt.
OpenGL GLSL Referenzcompiler
Jeder Grafiktreiber hat seine eigenen Macken. Zum Beispiel machen NVIDIA-Treiber die Anforderungen der Spezifikation etwas weicher, und AMD-Treiber erfüllen die Spezifikationen besser (was meiner Meinung nach besser ist). Das Problem ist, dass Shader, die auf einem Computer ausgeführt werden, aufgrund unterschiedlicher Treiber möglicherweise kein Geld auf einem anderen Computer verdienen.
Nach mehrjähriger Erfahrung können Sie alle Unterschiede zwischen verschiedenen GPUs kennenlernen. Wenn Sie jedoch sicherstellen möchten, dass Ihre Shader überall funktionieren, können Sie Ihren Code mithilfe des GLSL-Referenz-Compilers anhand der offiziellen Spezifikation überprüfen. Den sogenannten GLSL lang validator können Sie hier herunterladen ( Quelle ).
Mit diesem Programm können Sie Ihre Shader testen, indem Sie sie als erstes Argument an das Programm übergeben. Denken Sie daran, dass das Programm den Shadertyp durch Erweiterung bestimmt:
.vert
: Vertex-Shader.frag
: Fragment-Shader.geom
: geometrischer Shader.tesc
: Shader zur Steuerung der Tessellation.tese
: Tessellation Computing Shader.comp
: Compute Shader
Das Programm auszuführen ist einfach:
glslangValidator shader.vert
Beachten Sie, dass das Programm nichts ausgibt, wenn keine Fehler vorliegen. Bei einem fehlerhaften Vertex-Shader sieht die Ausgabe folgendermaßen aus:

Das Programm zeigt nicht die Unterschiede zwischen den GLSL-Compilern von AMD, NVidia oder Intel an und kann nicht einmal alle Fehler im Shader melden, überprüft jedoch zumindest die Shader auf Übereinstimmung mit den Standards.
Bildpufferausgabe
Eine andere Methode für Ihr Toolkit besteht darin, den Inhalt des Bildpuffers in einem bestimmten Teil des Bildschirms anzuzeigen. Höchstwahrscheinlich verwenden Sie häufig Framebuffer, und da die ganze Magie hinter den Kulissen geschieht, kann es schwierig sein, festzustellen, was passiert. Die Ausgabe des Inhalts des Bildpuffers ist ein nützlicher Trick, um die Richtigkeit zu überprüfen.
Beachten Sie, dass der Inhalt des Bildpuffers, wie hier erläutert, mit Texturen und nicht mit Objekten in den Zeichenpuffern funktioniert
Mit einem einfachen Shader, der eine einzelne Textur zeichnet, können wir eine kleine Funktion schreiben, die schnell jede Textur in der oberen rechten Ecke des Bildschirms zeichnet:
Dadurch erhalten Sie in der Ecke des Bildschirms ein kleines Fenster zum Debuggen der Ausgabe des Bildpuffers. Dies ist beispielsweise nützlich, wenn Sie versuchen, die Richtigkeit von Normalen zu überprüfen:

Sie können diese Funktion auch so erweitern, dass mehr als eine Textur gerendert wird. Dies ist eine schnelle Möglichkeit, um kontinuierliches Feedback von allen Elementen in Frame-Puffern zu erhalten.
Externe Debugger-Programme
Wenn alles andere fehlschlägt, gibt es noch einen Trick: Programme von Drittanbietern zu verwenden. Sie sind in den OpenGL-Treiber integriert und können alle OpenGL-Aufrufe abfangen, um Ihnen viele interessante Daten über Ihre Anwendung zu liefern. Diese Anwendungen können die Verwendung von OpenGL-Funktionen profilieren, nach Engpässen suchen und Frame-Puffer, Texturen und Speicher überwachen. Während der Arbeit an (großem) Code können diese Tools von unschätzbarem Wert sein.
Ich habe einige beliebte Tools aufgelistet. Probieren Sie jeden aus und wählen Sie den, der am besten zu Ihnen passt.
Renderderoc
RenderDoc ist ein gutes (vollständig geöffnetes ) separates Debugging-Tool. Wählen Sie zum Starten der Erfassung die ausführbare Datei und das Arbeitsverzeichnis aus. Ihre Anwendung funktioniert wie gewohnt. Wenn Sie ein einzelnes Bild anzeigen möchten, können Sie mit RenderDoc mehrere Bilder Ihrer Anwendung erfassen. Unter den erfassten Frames können Sie den Status der Pipeline, alle OpenGL-Befehle, den Pufferspeicher und die verwendeten Texturen anzeigen.

Codexl
CodeXL - GPU-Debugging-Tool, funktioniert als eigenständige Anwendung und Plugin für Visual Studio. CodeXL Bietet viele Informationen und eignet sich hervorragend zum Profilieren grafischer Anwendungen. CodeXL läuft auch auf Grafikkarten von NVidia und Intel, jedoch ohne OpenCL-Debugging-Unterstützung.

Ich habe CodeXL nicht viel verwendet, weil mir RenderDoc einfacher erschien, aber ich habe CodeXL in diese Liste aufgenommen, weil es wie ein ziemlich zuverlässiges Tool aussieht und hauptsächlich von einem der größten Hersteller von GPUs entwickelt wird.
NVIDIA Nsight
Nsight ist ein beliebtes NUIDIA GPU-Debugging-Tool. Es ist nicht nur ein Plug-In für Visual Studio und Eclipse, sondern auch eine separate Anwendung . Das Nsight-Plugin ist eine sehr nützliche Sache für Grafikentwickler, da es viele Echtzeitstatistiken zur GPU-Nutzung und zum Frame-für-Frame-Status der GPU sammelt.
Sobald Sie Ihre Anwendung über Visual Studio oder Eclipse mit den Debug-Befehlen oder der Nsight-Profilerstellung starten, wird sie in der Anwendung selbst gestartet. Eine gute Sache in Nsight: Rendern eines GUI-Systems (GUI, grafische Benutzeroberfläche) über einer laufenden Anwendung, mit dem alle Arten von Informationen über Ihre Anwendung in Echtzeit oder Frame-für-Frame-Analyse gesammelt werden können.

Nsight ist ein sehr nützliches Tool, das meiner Meinung nach die oben genannten Tools übertrifft, aber einen schwerwiegenden Nachteil hat: Es funktioniert nur auf NVIDIA-Grafikkarten. Wenn Sie NVIDIA-Grafikkarten und Visual Studio verwenden, ist Nsight auf jeden Fall einen Versuch wert.
, ( , VOGL APItrace ), , . , , () ( , ).
Zusätzliche Materialien
- ? — Reto Koradi.
- — Vallentin Source.
PS : - . , !