Things are starting to get interesting. We have a colorful 3D cube that rotates, and we can move around the environment with the mouse and keyboard to look at it. Before we go on, it’s about time we start covering ourselves in case of OpenGL errors / warnings. In this section, we are going to create classes which wrap our QOpenGL* classes to report errors through Qt’s event system. The only real bummer about the next few tutorials is that they don’t involve any visual changes to our application.
QEvent
In Qt, it’s preferable to connect things via signals and slots – for everything else (read: when you cannot connect) there are QEvents. We can opt to create QEvents when we don’t have particular signals that we can connect to at our current level of abstraction. For example, if I want to be able to report some information to any kind of QObject, but I can’t instruct that every QObject has a particular kind of signal – I would use a QEvent.
In more modern languages, we can use other methods of determining functionality – such as C#’s interface keyword. But since C++ lacks such constructs, we have to do things a little differently.
The common way of creating a custom QEvent is to have a static function which will identify the QEvent by a unique type id, generated via QEvent::registerEventType() . After that, we can pack our custom QEvent with as much information as possible – and then send them using QCoreApplication::sendEvent() or QCoreApplication::postEvent() .
Macro Preprocessor
A sad fact of the code we’re about to write is that it’s strewn with duplicate code. We can make this a little easier on ourselves with macros – though honestly I don’t recommend it. Even though I don’t recommend it, we are going to experiment a little bit and head down that path. Realistically speaking, if you’re working on a major product, you will really want to have exhausted all other options before arriving at the need for a macro.
For those of you who don’t understand the issue with such constructs, I advise you think about Window’s stamp over their own headers to handle ANSI (A) and UNICODE 16LE (W). At the time, it made sense to simply have a mode “enabled” for Windows to handle UNICODE 16LE character encoding for multilingual support – the problem was the underlying types were different. So what do we do? Do we tell all developers to go through and change their code and call *A or *W versions of the functions explicitly? That would make many developers mad. UNIX-based OSes had the same difficult choice to make, except they chose to go the UTF-8 route, which still uses a char for the underlying content, the encoding is just different. What Microsoft decided they would do is stamp macros over the all of WINAPI32 to call these functions for you. So a call to MessageBox(…) will be preprocessed to MessageBoxW(…) if UNICODE is defined, and MessageBoxA(…) if it is not. If you aren’t familiar with encoding at all, I recommend an article written by Joel Spolsky called: The Absolute Minimum Every Software Developer Absolutely, Positively Must Know About Unicode and Character Sets (No Excuses!).
I’m getting off-track though – my point is that macros are serious business. This tutorial alone has been rewritten 3 times because I keep having second thoughts about what ideas I want you – the reader – to take away from it. In the end, I really need to just commit and move on (VCS puns, zing!). Just when you are designing your software keep this in mind, and really think if there isn’t a better solution than to stamp macros everywhere wildly.
Error Checking
Really, our usage of the above two topics is going to be fun. This is the kind of stuff you wake up thinking “man is this even possible?” and then you accomplish it. It’s no wondrous feat, it’s just something fun, and saves us from littering return checks throughout our code. And our code is so simple it will not likely bring anything to fruition currently – but it will stop us dead in our tracks if we mess up later.
1. Create our OpenGLError class.
Since we don’t have a guaranteed signal for any given QObject, we will be raising QEvents for OpenGLErrors. For some types of QOpenGL* classes, signals would have worked – but not all of them.
openglerror.h:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 |
#ifndef OPENGLERROR_H #define OPENGLERROR_H #include <QEvent> #include <QStack> class OpenGLError : public QEvent { public: /***************************************************************************** * Different Possible Error Types ****************************************************************************/ enum FunctionType { bind, read, create, unmap, link, addShader, addShaderFromSourceCode, addShaderFromSourceFile }; enum ObjectType { QOpenGLBuffer, QOpenGLShaderProgram, QOpenGLVertexArrayObject }; /***************************************************************************** * Event Methods ****************************************************************************/ // Constructors / Destructors OpenGLError(char const *callerName, char const *fnName, ObjectType objType, FunctionType fnType); virtual ~OpenGLError(); // Accessing Types char const *functionName() const; char const *callerName() const; ObjectType objectType() const; FunctionType functionType() const; // Static Access static QEvent::Type type(); static bool sendEvent(OpenGLError *event); static void pushErrorHandler(QObject *obj); static void popErrorHandler(); private: char const *m_functionName; char const *m_callerName; ObjectType m_objectType; FunctionType m_functionType; static QStack<QObject*> m_errorHandler; }; // Inline Functions inline OpenGLError::OpenGLError(char const *callerName, char const *fnName, ObjectType objType, FunctionType fnType) : QEvent(type()), m_functionName(fnName), m_callerName(callerName), m_objectType(objType), m_functionType(fnType) {} inline OpenGLError::~OpenGLError() {} inline char const* OpenGLError::functionName() const { return m_functionName; } inline char const* OpenGLError::callerName() const { return m_callerName; } inline OpenGLError::ObjectType OpenGLError::objectType() const { return m_objectType; } inline OpenGLError::FunctionType OpenGLError::functionType() const { return m_functionType; } #endif // OPENGLERROR_H |
We’re using ObjectType and FunctionType enum for just some extra information that we can use in a switch-case if we need. The rest is is just some helpful information to let us know what kind of error event took place. Unfortuntely, we don’t get any more information than this. It would be nice to get a filename and line number, but it’s not easy without macro stamping all of the source code. We are already doing some pretty intrusive stuff with the macro preprocessor later on, so we want to keep our damage to a minimum – so we’re going to avoid that.
As you can also imagine, we’re introducing another singleton-esque object here. Not the best design, but for our purposes this should be acceptable.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 |
#include "openglerror.h" #include <QObject> #include <QCoreApplication> #include <QDebug> /******************************************************************************* * OpenGLError static types ******************************************************************************/ QStack<QObject*> OpenGLError::m_errorHandler; /******************************************************************************* * OpenGLError methods ******************************************************************************/ QEvent::Type OpenGLError::type() { static QEvent::Type customEventType = static_cast<QEvent::Type>(QEvent::registerEventType()); return customEventType; } bool OpenGLError::sendEvent(OpenGLError *event) { if (m_errorHandler.empty()) { qWarning("Set OpenGLError::setErrorHandler() before calling any GL_DEBUG OpenGL functions!"); return false; } return QCoreApplication::sendEvent(m_errorHandler.top(), event); } void OpenGLError::pushErrorHandler(QObject *obj) { #ifdef GL_DEBUG qDebug() << "GL_DEBUG Error Handler (" << obj << ")"; #endif // GL_DEBUG m_errorHandler.push(obj); } void OpenGLError::popErrorHandler() { m_errorHandler.pop(); } |
Whenever we create a custom QEvent, we have to register it with QEvent::registerEventType(). This allows Qt to manage custom QEvents for us, so that event ids don’t conflict with one another.
We also have a helper function that can ask QCoreApplication to sendEvents to the currently assigned error handler. The reason we’re calling sendEvent, and not postEvent is because we want the event to be handled immediately. Realize that sendEvents will immediately handle an event, whereas postEvent will queue it to be handled later.
2. Create a macros header.
In order to make our lives easier, we’re going to have the preprocessor wrap most of the functionality for us – and thus send the OpenGLErrors. We’ll still have to tell the preprocessor what things we want it to wrap, but it will be far less typing.
macros.h:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 |
#ifndef MACROS_H #define MACROS_H // Append two preprocessor arguments together #define _APPEND(a,b) a##b #define APPEND(a,b) _APPEND(a,b) // Count number of arguments #define _NARGS(_1,_2,_3,_4,_5,_6,_7,_8,_9,_10,N,...) N #define NARGS(...) APPEND(_NARGS(__VA_ARGS__,10,9,8,7,6,5,4,3,2,1),) // Stringize Token #define _STR(x) #x #define STR(x) _STR(x) // Get specific value from __VA_ARGS__ #define PGET_0(e0,...) e0 #define PGET_1(e0,e1,...) e1 #define PGET_2(e0,e1,e2,...) e2 #define PGET_3(e0,e1,e2,e3,...) e3 #define PGET_4(e0,e1,e2,e3,e4,...) e4 #define PGET_5(e0,e1,e2,e3,e4,e5,...) e5 #define PGET_6(e0,e1,e2,e3,e4,e5,e6,...) e6 #define PGET_7(e0,e1,e2,e3,e4,e5,e6,e7,...) e7 #define PGET_8(e0,e1,e2,e3,e4,e5,e6,e7,e8,...) e8 #define PGET_9(e0,e1,e2,e3,e4,e5,e6,e7,e8,e9,...) e9 #ifdef WIN32 # define PGET_N(n,...) PGET_I(n,__VA_ARGS__) # define PGET_I(n,...) APPEND(APPEND(PGET_,n)(__VA_ARGS__),) #else # define PGET_N(n,...) APPEND(PGET_,n)(__VA_ARGS__) #endif // Declare variables using comma separator #define PDECL_1(e0,...) e0 v0 #define PDECL_2(e0,e1,...) e0 v0, e1 v1 #define PDECL_3(e0,e1,e2,...) e0 v0, e1 v1, e2 v2 #define PDECL_4(e0,e1,e2,e3,...) e0 v0, e1 v1, e2 v2, e3 v3 #define PDECL_5(e0,e1,e2,e3,e4,...) e0 v0, e1 v1, e2 v2, e3 v3, e4 v4 #define PDECL_6(e0,e1,e2,e3,e4,e5,...) e0 v0, e1 v1, e2 v2, e3 v3, e4 v4, e5 v5 #define PDECL_7(e0,e1,e2,e3,e4,e5,e6,...) e0 v0, e1 v1, e2 v2, e3 v3, e4 v4, e5 v5, e6 v6 #define PDECL_8(e0,e1,e2,e3,e4,e5,e6,e7,...) e0 v0, e1 v1, e2 v2, e3 v3, e4 v4, e5 v5, e6 v6, e7 v7 #define PDECL_9(e0,e1,e2,e3,e4,e5,e6,e7,e8,...) e0 v0, e1 v1, e2 v2, e3 v3, e4 v4, e5 v5, e6 v6, e7 v7, e8 v8 #define PDECL_10(e0,e1,e2,e3,e4,e5,e6,e7,e8,e9,...) e0 v0, e1 v1, e2 v2, e3 v3, e4 v4, e5 v5, e6 v6, e7 v7, e8 v8, e9 v9 #ifdef WIN32 # define PDECL_N(n,...) PDECL_I(n,__VA_ARGS__) # define PDECL_I(n,...) APPEND(APPEND(PDECL_,n)(__VA_ARGS__),) #else # define PDECL_N(n,...) APPEND(PDECL_,n)(__VA_ARGS__) #endif // Pass variables using comma separator #define PCALL_1(...) v0 #define PCALL_2(...) v0, v1 #define PCALL_3(...) v0, v1, v2 #define PCALL_4(...) v0, v1, v2, v3 #define PCALL_5(...) v0, v1, v2, v3, v4 #define PCALL_6(...) v0, v1, v2, v3, v4, v5 #define PCALL_7(...) v0, v1, v2, v3, v4, v5, v6 #define PCALL_8(...) v0, v1, v2, v3, v4, v5, v6, v7 #define PCALL_9(...) v0, v1, v2, v3, v4, v5, v6, v7, v8 #define PCALL_10(...) v0, v1, v2, v3, v4, v5, v6, v7, v8, v9 #ifdef WIN32 # define PCALL_N(n,...) PCALL_I(n,__VA_ARGS__) # define PCALL_I(n,...) APPEND(APPEND(PCALL_,n)(__VA_ARGS__),) #else # define PCALL_N(n,...) APPEND(PCALL_,n)(__VA_ARGS__) #endif #endif // MACROS_H |
The above macros are common when abusing the preprocessor. It should be noted that this is an ugly abuse of such tools, and in general should be avoided (have I stressed this enough?). But sometimes such hacks can save ourselves hours of time, and make future additions to the code a little easier. Let’s talk about what each macro can do.
APPEND will take two parameters and put them together. Sometimes this can solve delay-problems by forcing the preprocessor to delay it’s expansion of the surrounding information another cycle. But alternatively it’s just nicer to look at APPEND(a,b) instead of a##b everywhere, it’s a little more readable.
NARGS is a little more complicated. This is a limited implementation which will count the number of arguments passed into the macro. To understand how this one works, let’s run through an example (I’ve simplified it to 5 values):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
NARGS(paramA,paramB,paramC) // Preprocessor pass 1 --> APPEND(_NARGS(__VA_ARGS__,5,4,3,2,1),) // Preprocessor pass 2 --> _NARGS(paramA,paramB,paramC,5,4,3,2,1) // Preprocessor pass 3 --> 3 // Note: The jump from _NARGS -> 3 is a little much, // let's take a detailed look at that call. OURS :_NARGS(paramA,paramB,paramC, 5, 4, 3, ...) DEFINES:_NARGS( _1, _2, _3, _4, _5, N, ...) N // See how N lines up with 3 when we have 3 arguments? // This pattern remains true with paramN where N >= 1 and N <= 10. |
As you might have noticed, NARGS has the unfortunate side-effect of not being able to work with 0 arguments. At some level, we will always have some comma that pushes at least the 1 into place of N. There is no good compile-time way around this that I could determine.
You also might notice that we’re APPENDING for no reason – well this is to fix a problem that is present with the MSVC12 preprocessor that analyzes __VA_ARGS__ as one argument passed into a macro expansion. In this case, the extra APPEND doesn’t harm gcc or clang compilation – but other examples aren’t compatible like NARGS is, so you may see some ifndef here or there.
STR will turn some preprocessor token into a string. Again, this can usually be done with #param, but this is a little more readable.
PGET allows us to get the Nth value from a list of __VA_ARGS__, this is useful for getting a specific value from a __VA_ARGS__ list. Originally I made this recursive, but gcc and clang seemed to have some trouble with that. (We can’t win, here.)
PDECL / PCALL allows us to either declare multiple variables in a comma-separated list, or access multiple variables in a comma-separated list. (Depending on if you call with PDecl or PCall). Keep in mind, the same issues are in place for not being able to identify when there are 0 arguments. This also suffered from the recursive macro issues under gcc and clang for my original implementation.
3. Create openglcommon.h header.
Now that we have our common, global macro tools – we’re going to make some more specific ones for our OpenGL usage, and because of this we should put them someplace else as well. We’re going to have several of these headers, and we will only be adding more – so I ended up putting them under a new folder called “OpenGL”. If you want, you can put them there and then add the following to QMake:
1 |
INCLUDEPATH += $$PWD/OpenGL |
This will include the OpenGL folder, so we can just directly include the files. I also opted to make some extension-less headers that call into the real headers.
So for example, for openglcommon.h, I have another file called OpenGLCommon, and it has the following:
1 |
#include "openglcommon.h" |
It’s up to you if you want to do this. I found it convenient. (I also did this with OpenGLError, as well as moving it into the OpenGL folder.)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 |
#ifndef OPENGLCOMMON_H #define OPENGLCOMMON_H #include "macros.h" #include <OpenGLError> // Report error statement #define GL_REPORT_STMT(type,func,...) { OpenGLError ev(STR(type), STR(func), OpenGLError::type, OpenGLError::func); OpenGLError::sendEvent(&ev); } #ifdef WIN32 # define GL_REPORT(type,func,...) APPEND(GL_REPORT_STMT(type,func,__VA_ARGS__),) #else # define GL_REPORT(type,func,...) GL_REPORT_STMT(type,func,__VA_ARGS__) #endif // Accept declarations #define GL_DECL_1(fn) inline bool fn() #define GL_DECL_2(fn,t0,...) inline bool fn(PDECL_1(t0)) #define GL_DECL_3(fn,t0,t1,...) inline bool fn(PDECL_2(t0,t1)) #define GL_DECL_4(fn,t0,t1,t2,...) inline bool fn(PDECL_3(t0,t1,t2)) #define GL_DECL_5(fn,t0,t1,t2,t3,...) inline bool fn(PDECL_4(t0,t1,t2,t3)) #define GL_DECL_6(fn,t0,t1,t2,t3,t4,...) inline bool fn(PDECL_5(t0,t1,t2,t3,t4)) #define GL_DECL_7(fn,t0,t1,t2,t3,t4,t5,...) inline bool fn(PDECL_6(t0,t1,t2,t3,t4,t5)) #define GL_DECL_8(fn,t0,t1,t2,t3,t4,t5,t6,...) inline bool fn(PDECL_7(t0,t1,t2,t3,t4,t5,t6)) #define GL_DECL_9(fn,t0,t1,t2,t3,t4,t5,t6,t7,...) inline bool fn(PDECL_8(t0,t1,t2,t3,t4,t5,t6,t7)) #define GL_DECL_10(fn,t0,t1,t2,t3,t4,t5,t6,t7,t8,...) inline bool fn(PDECL_9(t0,t1,t2,t3,t4,t5,t6,t7,t8)) #define GL_DECL_11(fn,t0,t1,t2,t3,t4,t5,t6,t7,t8,t9,...) inline bool fn(PDECL_10(t0,t1,t2,t3,t4,t5,t6,t7,t8,t9)) // Accept callings #define GL_CALL_1(caller,fn) caller::fn() #define GL_CALL_2(caller,fn,...) caller::fn(PCALL_1()) #define GL_CALL_3(caller,fn,...) caller::fn(PCALL_2()) #define GL_CALL_4(caller,fn,...) caller::fn(PCALL_3()) #define GL_CALL_5(caller,fn,...) caller::fn(PCALL_4()) #define GL_CALL_6(caller,fn,...) caller::fn(PCALL_5()) #define GL_CALL_7(caller,fn,...) caller::fn(PCALL_6()) #define GL_CALL_8(caller,fn,...) caller::fn(PCALL_7()) #define GL_CALL_9(caller,fn,...) caller::fn(PCALL_8()) #define GL_CALL_10(caller,fn,...) caller::fn(PCALL_9()) #define GL_CALL_11(caller,fn,...) caller::fn(PCALL_10()) // Platform-specific entry points #ifdef WIN32 # define GL_DECL(...) GL_DECL_I(NARGS(__VA_ARGS__),__VA_ARGS__) # define GL_DECL_I(n,...) APPEND(APPEND(GL_DECL_,n)(__VA_ARGS__),) # define GL_CALL(caller,...) GL_CALL_I(NARGS(__VA_ARGS__),caller,__VA_ARGS__) # define GL_CALL_I(n,caller,...) APPEND(APPEND(GL_CALL_,n)(caller,__VA_ARGS__),) #else # define GL_DECL(...) APPEND(GL_DECL_,NARGS(__VA_ARGS__)(__VA_ARGS__)) # define GL_CALL(caller,...) APPEND(GL_CALL_,NARGS(__VA_ARGS__)(caller,__VA_ARGS__)) #endif // Entry points #ifdef GL_DEBUG # define GL_CHECK(caller,...) GL_DECL(__VA_ARGS__) { if(!GL_CALL(caller,__VA_ARGS__)) { GL_REPORT(caller,__VA_ARGS__); return false; } return true; } #else # define GL_CHECK(caller,...) #endif // GL_DEBUG #endif // OPENGLCOMMON_H |
This header is just some more macros.
GL_REPORT is a macro which will fire a OpenGLError event, and fill it with the proper information.
GL_DECL / GL_CALL are macros which will either declare a function, or call the underlying function.
GL_CHECK is the macro we care most about, it declares and defines the function with it’s validity check, calling GL_REPORT if it fails.
You also might notice that GL_CHECK is only defined to do something when GL_DEBUG is defined. We’re going to add that as a custom definition, but in general this will allow us to turn off and on this error checking.
4. Implement all OpenGL classes you care about.
So now that we have all our macro tools, we simply need to define the classes which utilize them:
- openglbuffer.h
- openglshaderprogram.h
- openglvertexarrayobject.h
I’ll quickly show each file, and explain – in general – what these do.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
#ifndef OPENGLBUFFER_H #define OPENGLBUFFER_H #include <OpenGLCommon> #include <QOpenGLBuffer> // Register to check OpenGLBuffer class OpenGLBufferChecked : public QOpenGLBuffer { public: GL_CHECK(QOpenGLBuffer,bind); GL_CHECK(QOpenGLBuffer,create); GL_CHECK(QOpenGLBuffer,unmap); GL_CHECK(QOpenGLBuffer,read,int,void*,int); }; // Final class class OpenGLBuffer : public OpenGLBufferChecked { // Intentionally Empty }; #endif // OPENGLBUFFER_H |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 |
#ifndef OPENGLSHADERPROGRAM_H #define OPENGLSHADERPROGRAM_H #include <OpenGLCommon> #include <QOpenGLShaderProgram> // Register to check OpenGLShaderProgram class OpenGLShaderProgramChecked : public QOpenGLShaderProgram { public: explicit OpenGLShaderProgramChecked(QObject *parent = 0) : QOpenGLShaderProgram(parent) {} GL_CHECK(QOpenGLShaderProgram,addShader,QOpenGLShader*); GL_CHECK(QOpenGLShaderProgram,addShaderFromSourceCode,QOpenGLShader::ShaderType,const char*); GL_CHECK(QOpenGLShaderProgram,addShaderFromSourceCode,QOpenGLShader::ShaderType,const QByteArray&); GL_CHECK(QOpenGLShaderProgram,addShaderFromSourceCode,QOpenGLShader::ShaderType,const QString&); GL_CHECK(QOpenGLShaderProgram,addShaderFromSourceFile,QOpenGLShader::ShaderType,const QString&); GL_CHECK(QOpenGLShaderProgram,bind); GL_CHECK(QOpenGLShaderProgram,create); GL_CHECK(QOpenGLShaderProgram,link); }; // Final class class OpenGLShaderProgram : public OpenGLShaderProgramChecked { public: explicit OpenGLShaderProgram(QObject *parent = 0) : OpenGLShaderProgramChecked(parent) {} }; #endif // OPENGLSHADERPROGRAM_H |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
#ifndef OPENGLVERTEXARRAYOBJECT_H #define OPENGLVERTEXARRAYOBJECT_H #include <OpenGLCommon> #include <QOpenGLVertexArrayObject> // Register to check OpenGLVertexArrayObject class OpenGLVertexArrayObjectChecked : public QOpenGLVertexArrayObject { public: explicit OpenGLVertexArrayObjectChecked(QObject *parent = 0) : QOpenGLVertexArrayObject(parent) {} GL_CHECK(QOpenGLVertexArrayObject,create); }; // Final class class OpenGLVertexArrayObject : public OpenGLVertexArrayObjectChecked { public: explicit OpenGLVertexArrayObject(QObject *parent = 0) : OpenGLVertexArrayObjectChecked(parent) {} }; #endif // OPENGLVERTEXARRAYOBJECT_H |
All these files do, is define some OpenGL*Checked classes which derive from QOpenGL*, where we use the GL_CHECK() macro, and then we in the end derive publicly into OpenGL* class. This effectively wraps our OpenGL types, yet still makes unchecked functions available through inheritance.
You can see why I might have wanted to spend the time to introduce some macros at this point. If I hadn’t done this, for every function I would have had to define it, inline it, and run through the whole procedure of creating a OpenGLError, etc. At some level I’ll want macros to help me out here – the real question is where do you introduce that?
I also took advantage of the fact the NARGS can only return values >= 1, we always at least supply a function name, so we just include that in our NARGS check, and we can get an accurate portrayal of whether or not the function has 0~9 parameters. This way we don’t have to have different macro calls for whether or not there are parameters.
I’ve opted to leave the construction of classes very transparent. This is probably for the best. My original implementation hid even that from you, but I really didn’t like how that was hiding information from me. It made it hard to tell what my magical macros were doing. I think this is a good middle ground.
5. Modify the Window class.
Now we need to actually handle these OpenGLError events, as well as use our inherited OpenGL* classes.
window.h:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 |
#ifndef WINDOW_H #define WINDOW_H #include <QOpenGLWindow> #include <QOpenGLFunctions> #include <QMatrix4x4> #include "transform3d.h" #include "camera3d.h" // OpenGL Classes class OpenGLError; class OpenGLShaderProgram; #include <OpenGLBuffer> #include <OpenGLVertexArrayObject> class Window : public QOpenGLWindow, protected QOpenGLFunctions { Q_OBJECT // OpenGL Events public: Window(); ~Window(); void initializeGL(); void resizeGL(int width, int height); void paintGL(); void teardownGL(); protected slots: void update(); protected: bool event(QEvent *event); void errorEventGL(OpenGLError *event); void keyPressEvent(QKeyEvent *event); void keyReleaseEvent(QKeyEvent *event); void mousePressEvent(QMouseEvent *event); void mouseReleaseEvent(QMouseEvent *event); private: // OpenGL State Information OpenGLBuffer m_vertex; OpenGLVertexArrayObject m_object; OpenGLShaderProgram *m_program; // Shader Information int u_modelToWorld; int u_worldToCamera; int u_cameraToView; QMatrix4x4 m_projection; Camera3D m_camera; Transform3D m_transform; // Private Helpers void printVersionInformation(); }; #endif // WINDOW_H |
event() is a virtual function that we can override to tell our Window class to handle our new custom events. And we’ll finally need a destructor to tell OpenGLError to pop our error handler. Also notice that our QOpenGL* classes have lost the Q? This is how we’re injecting our code into our OpenGL calls.
window.cpp:
1 2 3 4 5 6 7 8 9 10 11 12 |
#include "window.h" #include <QDebug> #include <QString> #include <QKeyEvent> #include "vertex.h" #include "input.h" // OpenGL Includes #include <OpenGLError> #include <OpenGLShaderProgram> // ... |
Obviously we need to include the new OpenGL headers.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
// ... Window::Window() { m_transform.translate(0.0f, 0.0f, -5.0f); OpenGLError::pushErrorHandler(this); } Window::~Window() { makeCurrent(); teardownGL(); OpenGLError::popErrorHandler(); } // ... |
The constructor will now push the error handler, and the destructor will pop it. This is really there for completion-sake. I don’t think having a stack error handler will prove useful in many scenarios – but it may catch some dangling gl calls.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
// ... bool Window::event(QEvent *e) { if (e->type() == OpenGLError::type()) { errorEventGL(static_cast<OpenGLError*>(e)); return true; } return QOpenGLWindow::event(e); } void Window::errorEventGL(OpenGLError *event) { qFatal("%s::%s => Returned an error!", event->callerName(), event->functionName()); } // ... |
And lastly for Window, we must check for our custom event type, and handle it! Every other kind of event we will pass down to QOpenGLWindow to handle like normal.
Also, don’t forget to update the allocations to allocate OpenGL classes instead of QOpenGL classes:
1 2 3 4 5 |
// ... m_program = new OpenGLShaderProgram(this); // ... |
I believe OpenGLShaderProgram is the only one you have to worry about.
6. Update the QMake project file
Now we’re going to set our own option for CONFIG. From what I’ve gathered, it’s difficult to make a custom configuration using QMake project files (outside of the default Debug and Release). Otherwise you’d really want a new configuration for this.
1 2 3 4 5 6 7 8 9 10 |
# ... CONFIG += gl_debug CONFIG(debug,debug|release) { CONFIG(gl_debug) { DEFINES += GL_DEBUG } } # ... |
What this says is that we’re using our custom gl_debug options, and when debug (or debug|release) is active, AND if we have gl_debug active, add the define GL_DEBUG. You may have seen this littered throughout our code today.
What this does is for Debug, we will be using our class abstractions and checking return values. For Release, we will not be.
Sure enough, running under Debug yields:
1 |
GL_DEBUG Error Handler ( Window(0xXXXXXXX) ) |
Try the same code under Release, and nothing prints.
So how can we test our code? Cause a gl* call to fail! For example, try linking before adding any source code to our OpenGLShaderProgram object.
Under Debug I get:
1 |
OpenGLShaderProgram::link => Returned an error! |
Complete with a debug dialog window and everything!
Summary
In this tutorial we learned the following.
- What the QEvent class is, and when you would want to prefer it to SIGNALs and SLOTs.
- How to subclass a QEvent to form your own custom QEvents.
- Leveraging the preprocessor to wrap our OpenGL code is possible, but should be seriously considered before attempting.
- How to alter the Window class to make it handle our new OpenGLError QEvents.
C:\Users\zxb\Desktop\ErrorCheck\window.cpp:93: error: invalid use of incomplete type ‘class OpenGLShaderProgram’
m_program = new OpenGLShaderProgram(this);
^
======error=========
win10 64bit
Qt 5.7
If you’re getting this error, you are not properly including the file which defined OpenGLShaderProgram. (I’m not getting this error with “tutorial-series” branch from my git repo, more information would be helpful.)
thanks! It’s my error, I found it.