RPH - blog

Blog programisty

Generyczny wzorzez dekoratora i debugowanie OpenGL

Opublikowany: Czwartek 14.01.2016
0

Pomysł wpadł mi do głowy o godzinie 2 nad ranem. Pomimo tego że o 6:30 miał zadzwonić budzik, aby obudzić mnie do pracy, poświęciłem godzinę aby zamienić koncepcję w kawałek kodu. Ale może od początku, zacząłem się uczyć OpenGL. Niestety w moim programie cały czas występował błąd OpenGL: invalid enum. I nie mogłem go znaleźć.

Zastosowałem więc wzorzec dekoratora, aby po każdym wywołaniu funkcji OpenGL występowało sprawdzenie czy wystąpił błąd. Opracowane przeze mnie rozwiązanie jest na tyle ogólne, że bez problemu może zostać zastosowane do rozwiązania innych problemów.

TL;DR Jeżeli nie nie interesuje cię droga jaką dochodziłem do rozwiązania możesz od razu zobaczyć Zakończenie.

OpenGL

Obsługa błędów w OpenGL sprowadza się do wywołania jednej funkcji glGetError(), która zwraca kod błędu lub wartość oznaczającą brak błędu (GL_NO_ERROR). OpenGL przechowuje błędy na stosie, tak więc kilkukrotne wywołanie glGetError() aż do momentu gdy zwrócona wartość będzie równa GL_NO_ERROR, udostępni informacje o wszystkich błędach które wystąpiły, w kolejności odwrotnej do ich wystąpnienia.

Ponieważ funkcje OpenGL nie zwracają informacji o wystąpieniu błędu, a jedyną możliwością sprawdzenia jest glGetErrors(), które nie zwraca informacji o miejscu wystąpienia błędu, dobrą praktyką jest sprawdzanie błędów często, np po każdym narysowaniu ramki.

Najwygodniejszym rozwiązaniem byłoby robić to po każdym wywołaniu funkcji OpenGL, jednak byłoby to bardzo męczące, błędogenne, a dodatkowo potem należałoby to usunąć lub otoczyć dyrektywami preprocesora, aby w skończonym kodzie nie obciążać procesora zbędnymi instrukcjami.

Wzorzec dekoratora

Wzorzec dekoratora dodaje funkcjonalność do innego obiektu, w moim przypadku funkcji. Funkcję, która ma zostać udekorowana przekazuje się jako parametr dla dekoratora. Następnie dekorator może wykonać czynność przed wywołaniem funkcji, może zmienić parametry, zmienić wynik itp.

Dla zainteresowanych tematem odsyłam do Wikipedii: Dekorator (wzorzec projektowy)

Pierwsze rozwiązanie

Sprawdzanie błędów OpenGL

Do sprawdzenia czy wystąpił błąd służy funkcja, która przyjmuje zestaw parametrów:

  • function_name - nazwa wywoływanej funkcji;
  • vargs - lista parametrów;
  • file - plik źródłowy z makra __FILE__;
  • line - numer linii kodu z makra __LINE__.

Funkcja wygląda tak:

void check_opengl_errors(const std::string& function_name,
                         const std::string& vargs,
                         const std::string& file,
                         size_t line)
{
    GLenum error;
    static const std::map<GLenum, std::string> error_map =
    {
        {GL_INVALID_ENUM , "invalid enum"},
        {GL_INVALID_VALUE , "invalid value"},
        {GL_INVALID_OPERATION , "invalid operation"},
        {GL_STACK_OVERFLOW , "stack overflow"},
        {GL_STACK_UNDERFLOW , "stack underflow"},
        {GL_OUT_OF_MEMORY , "out of memory"},
        {GL_TABLE_TOO_LARGE , "table too large"}
    };

    while((error = glGetError()) != GL_NO_ERROR)
    {
        std::cerr << "-[ ERROR ]- OpenGL error after function call "
            << function_name
            << "(" << vargs << ") in "
            << file << ":" << line
            << "  [ " << (error_map.count(error) > 0 ? error_map.at(error) : "unknown")
            << " ] " << std::endl;
    }
}

Dekorator

Dekorator został zaimplementowany przy użyciu variadic templates oraz innych konstrukcji z języka C++11.

template<typename F, typename... Args>
auto gl_debug_decorator(const std::string& function_name,
                        const std::string& vargs,
                        const std::string& file,
                        size_t line,
                        F&& function,
                        Args&&... args)
    -> decltype(function(std::forward<Args>(args)...))
{
    auto result = function(std::forward<Args>(args)...);

    check_opengl_errors(function_name, vargs, file, line);

    return result;
}

Dekorator przyjmuje takie same argumenty jak check_opengl_errors oraz funkcję która ma zostać wywołana i parametry które mają zostać jej przekazane. Dekorator wywołuje funkcję, zapamiętuje wartość jaką zwróciła, a następnie wywołuje check_opengl_errors w celu sprawdzenia błędów, ostatecznie zwraca przechowywaną wartość.

Uzupełnieniem dekoratora jest makro, które automatycznie uzupełnia 4 pierwsze parametry:

#define gl_debug_call(func, ...) \
    gl_debug_decorator(#func, #__VA_ARGS__,__FILE__, __LINE__, func, __VA_ARGS__)

Wersja druga dekoratora

Zaimplementowany wcześniej dekorator nie jest wystarczająco generyczny. Gdy dekorowana funkcja nie będzie zwracała wartości wtedy kompilator zakończy kompilację z błędem. Jest tak dlatego, ponieważ w przypadku gdy funkcja 'zwraca' typ void wtedy następuje próba zadeklarowania zmiennej result o typie void co nie może się udać. Należy więc napisać drugą wersję dekoratora specjalnie dla funkcji nie zwracającej wartości. Do tego celu można wykorzystać technikę SFINAE. Technika polega na zasadzie, że jeżeli kompilator przy próbie podstawienia parametrów do szablonu napotka na błąd przechodzi dalej i szuka innej pasującej deklaracji szablonu.

Bardziej zainteresowanych odsyłam tu: SFINAE oraz do innych informacji w sieci.

Po zastosowaniu SFINAE szablon dekoratora wygląda tak:

//wersja dla funkcji zwracających wartość
template<typename F, typename... Args>
auto gl_debug_decorator(const std::string& function_name,
                        const std::string& vargs,
                        const std::string& file,
                        size_t line,
                        F&& function,
                        Args&&... args)
    -> typename std::enable_if<!std::is_void<
                                    decltype(function(std::forward<Args>(args)...))>::value, 
                               decltype(function(std::forward<Args>(args)...)) >::type
{
    auto result = function(std::forward<Args>(args)...);

    check_opengl_errors(function_name, vargs, file, line);

    return result;
}

//wersja dla funkcji nie zwracających wartości
template<typename F, typename... Args>
auto gl_debug_decorator(const std::string& function_name,
                        const std::string& vargs,
                        const std::string& file,
                        size_t line,
                        F&& function,
                        Args&&... args)
    -> typename std::enable_if<std::is_void< /* różnica jest tu!!!  brak wykrzynika*/
                                    decltype(function(std::forward<Args>(args)...))>::value, 
                               void>::type
{
    function(std::forward<Args>(args)...);

    check_opengl_errors(function_name, vargs, file, line);
}

Trzecia wersja dekoratora

Makro gl_call jest to makro o zmiennej liczbie parametrów, inaczej variadic macro. Tak zadeklarowane makro wymusza aby zostały podane co najmniej dwa argumenty. Uniemożliwia to dekorowanie funkcji które nie przyjmują argumentów. Łatwym sposobem na poradzenie sobie z problemem jest skorzystanie z ##__VA_ARGS__ (więcej informacji: https://gcc.gnu.org/onlinedocs/cpp/Variadic-Macros.html).

Można więc zadeklarować tak:

#define gl_debug_call(func, ...) \
    gl_debug_decorator(#func, #__VA_ARGS__,__FILE__, __LINE__, func, ##__VA_ARGS__)

I to zostanie skompilowane bez błędów. Jednak ponieważ standard C++11 określa, że dla parametru ... musi zostać podany co najmniej jeden argument, kompilator GCC wyświetli ostrzeżenie:

 warning: ISO C++11 requires at least one argument for the "..." in a variadic macro

Aby poradzić sobie z tym problemem skorzystałem z gotowego rozwiązania: http://stackoverflow.com/a/5589364/3965277.

Ostatecznie całość kodu wygląda tak:

#define ARGS_HEAD_HELPER(first_arg, ...) first_arg
#define ARGS_HEAD(...) ARGS_HEAD_HELPER(__VA_ARGS__ , garbage)

#define ARGS_HEAD_NAME_HELPER(first, ...) #first
#define ARGS_HEAD_NAME(...) ARGS_HEAD_NAME_HELPER(__VA_ARGS__, garbage)

#define ARGS_TAIL_HELPER_10TH_ARG(a1,a2,a3,a4,a5,a6,a7,a8,a9,a10,...) a10
#define ARGS_TAIL_HELPER_NUM(...) \
    ARGS_TAIL_HELPER_10TH_ARG(__VA_ARGS__, MANY, MANY, MANY, \
        MANY, MANY, MANY, MANY, MANY, ONE, garbage)
#define ARGS_TAIL_HELPER_ONE(first)
#define ARGS_TAIL_HELPER_MANY(first, ...) , __VA_ARGS__
#define ARGS_TAIL_HELPER_2(suffix, ...) ARGS_TAIL_HELPER_##suffix(__VA_ARGS__)
#define ARGS_TAIL_HELPER(suffix, ...) ARGS_TAIL_HELPER_2(suffix, __VA_ARGS__)
#define ARGS_TAIL(...) ARGS_TAIL_HELPER(ARGS_TAIL_HELPER_NUM(__VA_ARGS__), __VA_ARGS__)


#define ARGS_TAIL_NAME_HELPER_10TH_ARG(a1,a2,a3,a4,a5,a6,a7,a8,a9,a10,...) a10
#define ARGS_TAIL_NAME_HELPER_NUM(...) \
    ARGS_TAIL_NAME_HELPER_10TH_ARG(__VA_ARGS__, MANY, MANY, MANY, \
        MANY, MANY, MANY, MANY, MANY, ONE, garbage)
#define ARGS_TAIL_NAME_HELPER_ONE(first) ""
#define ARGS_TAIL_NAME_HELPER_MANY(first, ...)  #__VA_ARGS__
#define ARGS_TAIL_NAME_HELPER_2(suffix, ...) ARGS_TAIL_NAME_HELPER_##suffix(__VA_ARGS__)
#define ARGS_TAIL_NAME_HELPER(suffix, ...) ARGS_TAIL_NAME_HELPER_2(suffix, __VA_ARGS__)
#define ARGS_TAIL_NAME(...) \
    ARGS_TAIL_NAME_HELPER(ARGS_TAIL_NAME_HELPER_NUM(__VA_ARGS__), __VA_ARGS__)

#define gl_debug_call(...) \
    gl_call_wrapper(ARGS_HEAD_NAME(__VA_ARGS__), ARGS_TAIL_NAME(__VA_ARGS__), \
    __FILE__,__LINE__,ARGS_HEAD(__VA_ARGS__) ARGS_TAIL(__VA_ARGS__))

Problemy z NULL

OpenGL jest interfejsem dla języka C, który to nie ma tak silnej kontroli typów jak C++. Do tego fragmenty kodów z kursów OpenGL, nawet w języku C++, zawierają niepoprawny kod. Tam gdzie funkcja OpenGL wymaga wskaźnika przekazywany jest NULL lub 0. Dla dekoratora parametr ten nie jest odpowiedni, przez to kod poniżej nie skompiluje się:

gl_debug_call(glVertexAttribPointer, 0, 3, GL_FLOAT, GL_FALSE, 0, 0);

Aby to poprawić należy używać nullptr:

gl_debug_call(glVertexAttribPointer, 0, 3, GL_FLOAT, GL_FALSE, 0, nullptr);

Zakończenie

Używając wzorca dekoratora można prawie automatycznie znaleźć miejsce w którym wystąpił błąd. W moim kodzie wyglądało to tak:

//init glew
glewExperimental = GL_TRUE;
GLenum glew_init_res = gl_debug_call(glewInit);

Nie spodziewałem się aby błąd wystąpił w tym miejscu, ale jednak po uruchomieniu zobaczyłem:

-[ ERROR ]- OpenGL error after function call glewInit() in src/app.cpp:45  [ invalid enum ]

Jest to znane zachowanie, opisane tu: https://www.opengl.org/wiki/OpenGL_Loading_Library#GLEW_.28OpenGL_Extension_Wrangler.29. Tak więc przynajmniej nie ja popełniłem błąd :).

Implementacja dekoratora jest na tyle ogólna, że może zostać użyta w innych projektach, nie tylko dla OpenGL.

Całość kodu do gotowego zastosowania (powtórzenie tego co było wcześniej zebrane do kupy):

void check_opengl_errors(const std::string& function_name,
                         const std::string& vargs,
                         const std::string& file,
                         size_t line)
{
    GLenum error;
    static const std::map<GLenum, std::string> error_map =
    {
        {GL_INVALID_ENUM , "invalid enum"},
        {GL_INVALID_VALUE , "invalid value"},
        {GL_INVALID_OPERATION , "invalid operation"},
        {GL_STACK_OVERFLOW , "stack overflow"},
        {GL_STACK_UNDERFLOW , "stack underflow"},
        {GL_OUT_OF_MEMORY , "out of memory"},
        {GL_TABLE_TOO_LARGE , "table too large"}
    };

    while((error = glGetError()) != GL_NO_ERROR)
    {
        std::cerr << "-[ ERROR ]- OpenGL error after function call "
            << function_name
            << "(" << vargs << ") in "
            << file << ":" << line
            << "  [ " << (error_map.count(error) > 0 ? error_map.at(error) : "unknown")
            << " ] " << std::endl;
    }
}

//wersja dla funkcji zwracających wartość
template<typename F, typename... Args>
auto gl_debug_decorator(const std::string& function_name,
                        const std::string& vargs,
                        const std::string& file,
                        size_t line,
                        F&& function,
                        Args&&... args)
    -> typename std::enable_if<!std::is_void<
                                    decltype(function(std::forward<Args>(args)...))>::value, 
                               decltype(function(std::forward<Args>(args)...)) >::type
{
    auto result = function(std::forward<Args>(args)...);

    check_opengl_errors(function_name, vargs, file, line);

    return result;
}

//wersja dla funkcji nie zwracających wartości
template<typename F, typename... Args>
auto gl_debug_decorator(const std::string& function_name,
                        const std::string& vargs,
                        const std::string& file,
                        size_t line,
                        F&& function,
                        Args&&... args)
    -> typename std::enable_if<std::is_void< /* różnica jest tu!!!  brak wykrzynika*/
                                    decltype(function(std::forward<Args>(args)...))>::value, 
                               void>::type
{
    function(std::forward<Args>(args)...);

    check_opengl_errors(function_name, vargs, file, line);
}

#define ARGS_HEAD_HELPER(first_arg, ...) first_arg
#define ARGS_HEAD(...) ARGS_HEAD_HELPER(__VA_ARGS__ , garbage)

#define ARGS_HEAD_NAME_HELPER(first, ...) #first
#define ARGS_HEAD_NAME(...) ARGS_HEAD_NAME_HELPER(__VA_ARGS__, garbage)

#define ARGS_TAIL_HELPER_10TH_ARG(a1,a2,a3,a4,a5,a6,a7,a8,a9,a10,...) a10
#define ARGS_TAIL_HELPER_NUM(...) \
    ARGS_TAIL_HELPER_10TH_ARG(__VA_ARGS__, MANY, MANY, MANY, \
        MANY, MANY, MANY, MANY, MANY, ONE, garbage)
#define ARGS_TAIL_HELPER_ONE(first)
#define ARGS_TAIL_HELPER_MANY(first, ...) , __VA_ARGS__
#define ARGS_TAIL_HELPER_2(suffix, ...) ARGS_TAIL_HELPER_##suffix(__VA_ARGS__)
#define ARGS_TAIL_HELPER(suffix, ...) ARGS_TAIL_HELPER_2(suffix, __VA_ARGS__)
#define ARGS_TAIL(...) ARGS_TAIL_HELPER(ARGS_TAIL_HELPER_NUM(__VA_ARGS__), __VA_ARGS__)

#define ARGS_TAIL_NAME_HELPER_10TH_ARG(a1,a2,a3,a4,a5,a6,a7,a8,a9,a10,...) a10
#define ARGS_TAIL_NAME_HELPER_NUM(...) \
    ARGS_TAIL_NAME_HELPER_10TH_ARG(__VA_ARGS__, MANY, MANY, MANY, \
        MANY, MANY, MANY, MANY, MANY, ONE, garbage)
#define ARGS_TAIL_NAME_HELPER_ONE(first) ""
#define ARGS_TAIL_NAME_HELPER_MANY(first, ...)  #__VA_ARGS__
#define ARGS_TAIL_NAME_HELPER_2(suffix, ...) ARGS_TAIL_NAME_HELPER_##suffix(__VA_ARGS__)
#define ARGS_TAIL_NAME_HELPER(suffix, ...) ARGS_TAIL_NAME_HELPER_2(suffix, __VA_ARGS__)
#define ARGS_TAIL_NAME(...) \
    ARGS_TAIL_NAME_HELPER(ARGS_TAIL_NAME_HELPER_NUM(__VA_ARGS__), __VA_ARGS__)

#define gl_debug_call(...) \
    gl_call_wrapper(ARGS_HEAD_NAME(__VA_ARGS__), ARGS_TAIL_NAME(__VA_ARGS__), \
    __FILE__,__LINE__,ARGS_HEAD(__VA_ARGS__) ARGS_TAIL(__VA_ARGS__))

Nie ma jeszcze komentarzy

Zostaw komentarz