來源請注明:http://www.cnblogs.com/vertexshader/articles/3563883.html
一、導言
在OpenGL應用程序開發者心中,最需要的一個功能莫過於是幫助應用程序開發者調試程序的功能模塊。過去OpenGL只簡單地提供了glGetError這個命令來簡單地獲取OpenGL產生的錯誤信息;而glGetError所提供的調試消息及其有限,只是簡單地返回了一個GLenum枚舉類型,開發者並不知道哪個GL命令出現了錯誤,而且必須對照着參考手冊去判斷出現錯誤的原因。OpenGL 4.3則提供了GL_ARB_debug_output擴展,引入了新的反饋機制來提供調試輸出功能,幫助應用程序開發者更加容易地獲取OpenGL調試消息,進而方便地去調試OpenGL程序,提升OpenGL程序的性能。
Debug Output |
|
列入核心的版本 |
4.3 |
核心ARB擴展 |
|
ARB擴展 |
|
供應商擴展 |
表格1: 調試輸出功能的核心版本和相關擴展。
二、描述
擴展GL_ARB_debug_output最初是由AMD引入的,目的是為了提供一個新的反饋機制,來幫助應用程序開發者獲取更多的調試消息,稱之為調試輸出(Debug Output)。這個擴展允許應用程序開發者提供一個可以把調試消息(Debug Message)返回到應用程序的調試回調函數(Debug Callback Function),來避免在檢查錯誤期間,在每個GL命令后面添加glGetError命令代碼的繁瑣過程——在GL命令執行的時候會生成調試消息,在這個之后GL會自己調用應用程序開發者所定義的回調函數。不過該功能也並非一定要定義一個調試回調函數,在沒有定義調試回調函數的時候,GL會將消息存儲在內部默認的調試消息日志(Debug Message Log)中,應用程序可以查詢這個調試消息日志來獲取調試消息。該擴展所提供的功能非常的詳細,例如錯誤的使用API、性能警告、未定義的行為、着色器編譯錯誤和鏈接錯誤,或者其他有用的信息。調試消息包含着消息源(Source)、類型(Type)和嚴重性級別(Severity)這些信息,調試輸出控制(Debug Output Control)可以把外部的應用程序的事件生成的消息添加到調試消息流中;調試輸出控制也可以在應用程序不需要調試消息的時候關閉調試消息。
1. 調試環境
不同級別的調試消息的產生,取決於渲染環境(Render Context)的創建。如果當前的渲染環境不是調試環境(Debug Context),也就是在創建環境時GL_CONTEXT_FLAGS沒有設置GL_CONTEXT_FLAG_DEBUG_BIT,那么GL不會產生任何的調試消息,不過使用調試消息所提供的命令不會產生任何的錯誤。應用程序可以通過參數為符號常量的GL_DEBUG_OUTPUT的glEnable和glDisable命令來開啟或者關閉調試輸出的功能。如果當前的渲染環境是調試環境(也就是在創建環境時GL_CONTEXT_FLAGS設置了GL_CONTEXT_FLAG_DEBUG_BIT),那么GL_DEBUG_OUTPUT的初始值是GL_TRUE;反之,其初始值為GL_FALSE。調試環境是使用窗口系統相關的API,比如WGL_ARB_create_context或者GLX_ARB_created_context中的wglCreateContextARB或者glxCreateContextARB,在創建渲染環境時指定的:
GLuint attrib[] = { #ifdef WIN32 WGL_CONTEXT_MAJOR_VERSION_ARB, 4, WGL_CONTEXT_MINOR_VERSION_ARB, 4, #ifdef DEBUG WGL_CONTEXT_FLAGS_ARB, WGL_CONTEXT_DEBUG_BIT_ARB, #endif // DEBUG #endif // WIN32 #ifdef __linux__ GLX_CONTEXT_MAJOR_VERSION_ARB, 4, GLX_CONTEXT_MINOR_VERSION_ARB, 4, #ifdef DEBUG GLX_CONTEXT_FLAGS_ARB, GLX_CONTEXT_DEBUG_BIT_ARB, #endif // DEBUG #endif // __linux__ 0 }
在調試環境中,當GL_DEBUG_OUTPUT關閉時,GL則不會產生任何的調試消息到調試回調函數或者調試消息日志中;當GL_DEBUG_OUTPUT開啟時則會啟用完全的調試輸出功能。在非調試環境中,如果GL_DEBUG_OUTPUT被關閉,那么調試輸出日志的級別則是取決於GL的實現,在一些情況下可能沒有任何的調試輸出。完全的調試輸出只在調試環境中被完整地支持。
2. 調試消息
一個調試會被其生成源、和源有關的類型,以及一個和這個類型有關的無符號整數ID獨一無二地標記。表格2展示了消息生成源的類型,而表格3則展示了消息的類型。每個信息源與其類型的對,有其消息自己的命名空間,在其中每個消息都會與一個ID相關聯,而這個命名空間下消息的ID的賦值則是依賴於實現的。在不同的源於類型對所擁有的命名空間中,ID的值可能會重復。所以想要唯一地識別一個消息,要通過源、類型和ID的組合來完成。
每個消息也分配了一個嚴重性級別(Severity Level),其大致描述了消息的重要程度。嚴重性級別的相關符號常量展示在表格4中。應用程序可以選擇關閉不同等級的嚴重性,來達到控制調試輸出量的目的。每條消息中有一條以null結尾的用於描述消息的字符串,而消息的內容也是依賴於實現。
每個消息字符串的長度,包括null結尾符,必須小於或者等於依賴於實現的常量GL_MAX_DEBUG_MESSAGE_LENGTH的值。每個消息都可以被關閉或者啟用,所有的消息缺省是開啟的並且其嚴重性級別是GL_DEBUG_SEVERITY_LOW。而消息的開啟或者關閉狀態則可以通過glDebugMessageControl來改變。
調試消息源 |
生成消息的對象 |
GL_DEBUG_SOURCE_API |
GL |
GL_DEBUG_SOURCE_SHADER_COMPILER |
GLSL編譯器或者其他着色語言的編譯器 |
GL_DEBUG_SOURCE_WINDOW_SYSTEM |
窗口系統,比如WGL或者GLX |
GL_DEBUG_SOURCE_THIRD_PARTY |
外部調試器或者第三方中間庫 |
GL_DEBUG_SOURCE_APPLICATION |
應用程序 |
GL_DEBUG_SOURCE_OTHER |
與前面列出都不相符的源 |
表格2: 調試消息源和其生成對象,每個消息都必須是由其中的一個源產生的。
調試輸出消息類型 |
產生消息的情況 |
GL_DEBUG_TYPE_ERROR |
生成一個錯誤的事件 |
GL_DEBUG_TYPE_PEPRECATED_BEHAVIOR |
被標記為棄用的行為 |
GL_DEBUG_TYPE_UNDEFINED_BEHAVIOR |
在規范中未定義的行為 |
GL_DEBUG_TYPE_PERFORMANCE |
依賴於實現的性能警告 |
GL_DEBUG_TYPE_PORTABILITY |
使用特定供應商提供的擴展或者着色器 |
GL_DEBUG_TYPE_MARKER |
命令流的注解(Annotation) |
GL_DEBUG_TYPE_PUSH_GROUP |
進入一個調試組(Debug Group) |
GL_DEBUG_TYPE_POP_GROUP |
離開一個調試組 |
GL_DEBUG_TYPE_OTHER |
與前面列出都不相符的類型 |
表格3: 調試輸出消息的類型和產生這些消息的情況。
嚴重性級別 |
顯示消息的例子 |
GL_DEBUG_SEVERITY_HIGH |
任何GL錯誤;危險的未定義行為;着色器編譯錯誤和鏈接錯誤 |
GL_DEBUG_SEVERITY_MEDIUM |
嚴重的性能警告;着色器編譯鏈接警告;使用棄用的行為 |
GL_DEBUG_SEVERITY_LOW |
冗余狀態改變所產生的性能警告;微不足道的棄用行為 |
GL_DEBUG_SEVERITY_NOTIFICATION |
沒有任何錯誤或者性能警告 |
表格4: 消息的嚴重性級別和產生這個級別的例子。
3. 調試消息回調
這個擴展也定義了一個回調函數的機制,來獲取調試消息而不必訪問默認的調試消息日志,應用程序可以通過向glDebugMessageCallback命令提供一個調試回調函數來接收所產生的調試信息,並且通過自定義的回調函數來處理這些消息:
void glDebugMessageCallback(DEBUGPROC callback, void *userParam);
參數<callback>則是回調函數的指針,其形式必須是:
void callback(GLenum source, GLenum type, GLuint id, GLenum serverity, GLsizei length, const GLchar *message, const GLvoid *userParam);
此外也必須聲明回調函數的類型為相關平台所定義的DEBUGPROC類型,否則會導致未定義行為。當前的調試環境中只會存在唯一的一個調試回調函數,重復的設置調試回調函數指針會重寫之前的回調函數指針;而將<callback>設置為null,則會清楚當前環境中所設定的回調函數指針,並且不再通過回調函數指針輸出消息,而是存放在默認的調試消息日志中。在不同的需求下,應用程序開發者可以定義不同的回調函數,變更渲染環境中的回調函數指針,來達到處理不同消息的目的。應用程序也可以通過<userParam>來放入自定義的數據,渲染環境會將<userParam>作為消息回調函數的參數添加進去。
當應用程序定義了一個用於接收調試消息輸出的回調函數之后,OpenGL實現會在處於開啟狀態的消息生成的時候調用這個回調函數,並且將消息的源、類型、ID、嚴重性級別、消息字符串的長度、消息字符串,以及用戶定義的數據分別填入這個消息回調函數的<source>、<type>、<id>、<serverity>、<length>、<message>和<userParam>中。值得注意的是,<message>的內存是屬於GL並且是由GL管理的,所以其指針僅限於回調函數內部使用。在回調函數中調用任何GL或者窗口系統的函數,也是未定義的行為,可能會導致應用程序的終止。如果應用程序關閉了GL_DEBUG_OUTPUT,那么GL根本不會調用這個callback函數。對於調試回調函數最簡單的實現如下:
void MyProc(GLenum source, GLenum type, GLuint id, GLenum serverity, GLsizei length, const GLchar *message, const GLvoid *userParam) { std::cout << “source: ” << ValueToString(source) << “; ” << “type: ” << ValueToString(type) << “; ” << “id: ” << id << “; ” << “severity: ” << ValueToString(serverity) << “; ” << “message: ” << message << std::endl; }
4. 調試消息日志
如果GL_DEBUG_CALLBACK_FUNCTION的值是null,那么調試消息則會存儲到GL內部的消息日志中去,而最大的消息數量則由GL_MAX_DEBUG_LOGGED_MESSAGES的值定義。而每個渲染環境只會存儲自己的調試消息日志,並且只會存儲在這個環境中執行命令所產的調試消息。如果消息日志已滿,那么之后生成的消息都會被丟棄,直到消息日志被清除。應用程序可以通過查詢GL_DEBUG_LOGGED_MESSAGES來獲取當前存儲在消息日志中的消息數量。可以通過查詢GL_DEBUG_NEXT_LOGGED_MESSAGE_LENGTH得到最早的消息的長度,並且可以通過glGetDebugMessageLog來獲取日志中的消息。如果GL_DEBUG_CALLBACK_FUNCTION的值不是null,那么任何生成的消息都不會存儲在消息日志中,而是通過調試回調函數來處理。如果GL_DEBUG_OUTPUT被關閉了,那么任何消息都不會添加到消息日志中。
5. 控制調試消息
應用程序可以通過glDebugMessageControl在激活的調試組中控制調試輸出量:
void glDebugMessageControl(GLenum source, GLenum type, GLenum severity, GLsizei count, const GLuint *ids, GLboolean enabled);
參數<enabled>是消息的開啟狀態,如果<enable>的值是GL_TRUE,那么所引用的消息則會被啟用;如果<enable>的值是GL_FALSE,那么所引用的消息則會被關閉。這個命令也可以處理全部的消息集,並且通過控制的開啟狀態達到消息過濾的目的:
- 如果<source>、<type>或者<severity>是GL_DONT_CARE,那么所有的源、所有的類型或者所有的嚴重性級別都將被引用;
- 如果<source>、<type>或者<severity>不是GL_DONT_CARE,那么和值匹配的源、類型或者嚴重性級別將被引用;
- 如果<count>大於0,那么<ids>是有<count>個特定的<source>和<type>組合的消息的數組。在這種情況下,<source>或者<type>不可以是GL_DONT_CARE,但是<serverity>必須是GL_DONT_CARE;<id>中不能識別的消息ID將被忽略。如果<count>是0,那么<ids>的值則會被忽略。
雖然消息通過其源和類型被分組到一個隱式的層次結構中,但是對於每個源或者類型或者嚴重性級別沒有顯式的啟用狀態;相反的,啟用狀態則是存儲在每個消息之中的。通過命令一次性關閉所有的消息,或者通過<source>、<type>和<id>來關閉所有的消息是沒有差異的。如果GL_DEBUG_OUTPUT被關閉,那么每個<source>、<type>或者<severity>所對應的消息也會被關閉。消息過濾的范例如下:
// Disabling events related to deprecated behaviour. glDebugMessageControlGL_DONT_CARE , GL_DEBUG_TYPE_DEPRECATED_BEHAVIOR, GL_DONT_CARE , 0, NULL , GL_FALSE ); // Enabling only two particular combinations of source , type and id. // Note that first we had to disable all events . GLuint id [2] = {1280 , 1282}; glDebugMessageControl(GL_DONT_CARE , GL_DONT_CARE , GL_DONT_CARE , 0, 0, FALSE); glDebugMessageControl(GL_DEBUG_SOURCE_API_, GL_DEBUG_TYPE_ERROR, GL_DONT_CARE , 2, id, GL_TRUE);
6. 外部生成的消息
為了支持應用程序或者第三方庫生成他們自己的消息,比如特定渲染系統事件的時間標記信息,可以使用如下函數插入調試消息:
void glDebugMessageInsert(GLenum source, GLenum type, GLuint id, GLenum severity, GLint length, const GLchar *buf);
其用法和之前的幾個函數一樣,其中字符串<buf>則代表了消息的內容,<length>則是字符串的長度,如果<length>的值是負數,則暗示<buf>是有null結尾符的。
// Insert a debug message to GL. glDebugMessageInsert(GL_DEBUG_SOURCE_APPLICATION, GL_DEBUG_TYPE_ERROR, 10000 , GL_DEBUG_SEVERITY_HIGH, -1, "Error: My Application Generated an Error!");
7. 調試組
有應用程序插入的或者由GL實現生成的消息都會先寫入激活的調試組中。在一個大型的渲染系統中有許多的子模塊,在每個模塊中對於調試消息的級別要求不盡然相同,應用程序開發者可能需要在某個模塊中設定一個級別,然后在這個模塊執行完畢之后把級別恢復到過去的狀態。相應的OpenGL提供了將調試組入棧,在處理消息之后,通過出棧恢復默認的調試組的功能。調試組可以嵌套在別的調試組內,但是不可以重疊。如果應用程序沒有設置入棧的調試組,那么當前所啟用的調試組是缺省調試組。
void glPushDebugGroup(GLenum source, GLuint id, GLsizei length, const GLchar *message);
上面的命令用來將一個調試組入棧,參數的使用方法和其他的類似,<source>和<id>則標識了一個消息,其類型是GL_DEBUG_TYPE_PUSH_GROUP,其嚴重性級別則是GL_DEBUG_SEVERITY_NOTIFICATION。GL會把該調試組放入棧頂,用於繼承控制之前處於棧頂部的調試輸出量控制。調試組是有嚴格的層次結構的,所以任何額外的調試輸出量的控制只會應用於當前激活的調試組或者棧頂部激活的調試組中。
void glPopDebugGroup(void);
上面的命令則是將一個調試組出棧,在出棧調試組后GL也會生成一個調試消息,而消息的源和ID則是通過glPopDebugGroup所設置的<source>和<id>,其類型是GL_DEBUG_TYPE_POP_GROUP,該符號常量與GL_DEBUG_TYPE_PUSH_GROUP共享相同的命名空間,嚴重性等級則是GL_DEBUG_SEVERITY_NOTIFICATION。在調試組出棧之后,調試輸出控制則又重新回歸到父級調試組中。
三、測試
本文提供了一個范例代碼來應用調試輸出的功能,該程序創建了調試渲染環境,並且設置了顯示所有的調試輸出消息,並且通過調試回調函數輸出到控制台:
glEnable(GL_DEBUG_OUTPUT); glEnable(GL_DEBUG_OUTPUT_SYNCHRONOUS); glDebugMessageCallback(&callback, NULL); glDebugMessageControlARB(GL_DONT_CARE, GL_DONT_CARE, GL_DONT_CARE, 0, NULL, GL_TRUE);
應用程序故意做了兩個生成錯誤的方法,一個是錯誤地使用了OpenGL的API,一個是編譯的GLSL的代碼存在着錯誤:
// Error code 1: Has not create buffer yet. glBindBuffer(GL_ARRAY_BUFFER, 1); // Error code 2: Compiled Shader Failed. std::string code = "#version 330\n" \ "void main(void)\n" \ "{\n" \ "gl_Position = ftransform()\n" \ "}"; const GLchar *ptr = code.c_str(); GLuint shader = glCreateShader(GL_VERTEX_SHADER); glShaderSource(shader, 1, &ptr, 0); glCompileShader(shader); GLuint prog = glCreateProgram(); glAttachShader(prog, shader); glLinkProgram(prog);
本次測試的平台屬性是Intel(R) Core(TM) i3-4130 CPU @ 3.40GHz 3.40 GHz,Nvidia GeForce GTX 650(Driver 334.89),DDR3 1600 4G, Microsoft Windows 7 SP1 x64,程序運行了之后產生如下調試消息:
source: DEBUG_SOURCE_API
type: DEBUG_TYPE_ERROR
id: 1282
severity: DEBUG_SEVERITY_HIGH
message:
GL_INVALID_OPERATION error generated. Buffer name does not refer to an buffer object generated by OpenGL.
source: DEBUG_SOURCE_API
type: DEBUG_TYPE_OTHER
id: 131216
severity: DEBUG_SEVERITY_LOW
message:
Program/shader state info: GLSL program 2 failed to link.
source: DEBUG_SOURCE_API
type: DEBUG_TYPE_OTHER
id: 131184
severity: DEBUG_SEVERITY_LOW
message:
Buffer Info:
Total VBO memory usage in the system:
memtype: SYSHEAP, 0 bytes Allocated, numAllocations: 0.
memtype: VID, 0 bytes Allocated, numAllocations: 0.
memtype: DMA_CACHED, 0 bytes Allocated, numAllocations: 0.
memtype: MALLOC, 0 bytes Allocated, numAllocations: 0.
memtype: PAGE_AND_MAPPED, 0 bytes Allocated, numAllocations: 0.
memtype: PAGED, 0 bytes Allocated, numAllocations: 0.
測試結果正確地顯示了當前的錯誤消息,以及更加明顯的詳細的錯誤提示,這將大大幫助應用程序開發者直接發現錯誤的原因,而不必要在每個GL命令后面添加glGetError命令並且查詢手冊尋找出錯的原因。
四、總結
1. 應用總結
GL_ARB_debug_output提供了一個新的機制來獲得調試信息,避免了過去繁瑣地設置glGetError並且查詢手冊的過程,在開發和調試OpenGL應用程序上有很大的幫助。在Direct3D11中也有相關的功能實現——ID3DInfoQueue接口,功能上如出一轍,可以在上層渲染系統接口抽象中抽象這部分的功能。應用程序可以通過預編譯宏來處理相關的調試輸出代碼,這樣在應用程序發布時能直接去掉調試輸出功能。