Qt5 OpenGL Part 4: Error Checking 3


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:

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.

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:

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):

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:

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:

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.)

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.

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:

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:

Obviously we need to include the new OpenGL headers.

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.

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:

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.

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:

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:

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.

View Code on GitHub

Cheers!


Leave a comment

Your email address will not be published. Required fields are marked *

3 thoughts on “Qt5 OpenGL Part 4: Error Checking

  • visual

    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