Wiki

Glimpsing the Third Dimension

by Trond Kjernеsen

Qt's QGL module makes it easy to integrate OpenGL[1] rendering into Qt applications. In this article we will demonstrate how to create a custom QGLContext that can make use of implementation-specific features not covered by the QGL abstraction. We will also present a simple example of a multiplatform multithreaded OpenGL Qt application.

Creating a Custom QGLContext

Due to differences in "GL" (OpenGL) implementations and the features they support it may be necessary to subclass QGLContext to obtain more control over how the format of a GL context is chosen. Our first example demonstrates how to create a GL context that has a 32-bit depth buffer, if one is available.

QGLContext has a set of platform-specific functions that query and choose a context format based on the QGLFormat that has been set for a particular widget. These functions are:

  • choosePixelFormat() on Windows
  • chooseVisual() on X11
  • chooseMacVisual() on Mac OS X
All you need to do is reimplement each of these functions in a subclass for the platforms that you are interested in. Below we present a sample multiplatform implementation.

The common class definition:

#include <qapplication.h>
#include <qgl.h>
#if defined(Q_WS_X11)
#include <GL/glx.h>
#endif
#if defined(Q_WS_MAC)
#include <agl.h>
#endif
 
class CustomContext : public QGLContext
{
public:
    CustomContext(const QGLFormat &fmt, QPaintDevice *dev)
        : QGLContext(fmt, dev) {}
 
protected:
#if defined(Q_WS_WIN)
    int choosePixelFormat(void *p, HDC hdc);
#elif defined(Q_WS_X11)
    void *chooseVisual();
#elif defined(Q_WS_MAC)
    void *chooseMacVisual(GDHandle gdev);
#endif
};

The Windows specific function implementation:

#if defined(Q_WS_WIN)
int CustomContext::choosePixelFormat(void *p, HDC pdc)
{
    PIXELFORMATDESCRIPTOR *pfd = (PIXELFORMATDESCRIPTOR *)p;
    int pfiMax = DescribePixelFormat(pdc, 0, 0, NULL);
    int pfi;
    for (pfi = 1; pfi <= pfiMax; pfi++) {
        DescribePixelFormat(pdc, pfi, sizeof(PIXELFORMATDESCRIPTOR), pfd);
        if (pfd->cDepthBits == 32)
            return pfi;
}
    pfi = QGLContext::choosePixelFormat(pfd, pdc);
    qWarning("32-bit depth unavailable: using %d bits", pfd->cDepthBits);
    return pfi;
}
#endif

In the Windows implementation, we loop through all the available pixel formats until we find one that has a 32-bit depth buffer. In a real-world application we would probably have some other criteria that we'd check for as well. If a 32-bit depth buffer can't be found we fall back to the default QGLContext implementation.

Next, the X11 implementation:

#if defined(Q_WS_X11)
void *CustomContext::chooseVisual()
{
    GLint attribs[] = {GLX_RGBA, GLX_DEPTH_SIZE, 32, None};
    XVisualInfo *vis = glXChooseVisual(device()->x11Display(), device()->x11Screen(),
				   attribs);
    if (vis) {
        GLint depth = 0;
        glXGetConfig(device()->x11Display(), vis, GLX_DEPTH_SIZE, &depth);
        if (depth != 32)
            qWarning("32-bit depth unavailable: using %d bits", depth);
        return vis;
    } 
    return QGLContext::chooseVisual();
}
#endif

Under X11 we can request a visual that supports a 32-bit depth buffer directly. But even if the glXChooseVisual() function succeeds we're not guaranteed to have obtained a visual that has the buffer depth we requested. glXChooseVisual() will return the visual that best meets the specification we pass in with the attribs array. If no visual can be obtained at all we fall back to the default implementation.

Finally, the Mac OS X implementation:

#if defined(Q_WS_MAC)
void *CustomContext::chooseMacVisual(GDHandle gdev)
{
    GLint attribs[] = {AGL_ALL_RENDERERS, AGL_RGBA, AGL_DEPTH_SIZE, 32, AGL_NONE};
    AGLPixelFormat fmt = aglChoosePixelFormat(NULL, 0, attribs);
    if (fmt) {
        GLint depth;
        aglDescribePixelFormat(fmt, AGL_DEPTH_SIZE, &depth);
        if (depth != 32)
            qWarning("32-bit depth unavailable: using %d bits", depth);
        return fmt;
    }
    return QGLContext::chooseMacVisual(gdev);
}
#endif

The aglChoosePixelFormat() function is similar to X11's glXChooseVisual(): It returns a pixel format that most closely matches our specification. If it doesn't return anything we fall back to the default implementation.

The context can be used by setting it on a QGLWidget using the QGLWidget::setContext() call. The setContext() function is not documented in Qt's public API because it won't work as expected in all situations on all platforms. It does work if the widget has not yet been shown. In Qt 3.2, QGLWidget is due to feature a new constructor that takes a QGLContext parameter: This should make setContext() redundant. Setting the new context can be done like this:

MyGLWidget gl;
CustomContext *cx = new CustomContext(gl.format(), &gl);
gl.setContext(cx);
gl.show();

Note that the custom context is created on the heap. The QGLWidget takes ownership of the CustomContext pointer and destroys it when the GL widget itself is destroyed, in the usual Qt way.

Writing Multithreaded GL Applications

It is perfectly possible to write multiplatform multithreaded GL applications with Qt. In this section we present a simple program that demonstrates how to create threads that render into GL widgets created in the main Qt GUI thread.

Firstly we must ensure that Qt is configured with thread support and that we're using a thread-safe GL library. The GL libraries that ship with recent versions of Windows and Mac OS X are thread-safe, but under Unix/X11 this might not always be the case, so you should check this.

An additional problem under X11 is that Xlib (and therefore GLX) is not inherently thread-safe; doing Xlib calls in two different threads simultaneously will usually result in a crash. The GLX functions that the QGL module calls (e.g. for switching GL contexts, or for doing a buffer swap) also make Xlib calls which means that these calls must be protected in some way under X11. The simple solution is to call XInitThreads() before creating the QApplication object in your program. XInitThreads() must be the first Xlib call made in an application for it to work reliably. This will in effect make Xlib thread-safe. [2]

XInitThreads() is quite a recent addition to Xlib, so not all implementations support the call. It is possible to work around this by serializing the Xlib calls yourself using the Qt library mutex. This would involve adding qApp->lock() and qApp->unlock() calls around the code in the rendering loop, as well as changing how the GLWidget and GLThread are destroyed: You would have to make sure that the GLThread is finished before destroying the GLWidget, for example by posting a custom event to the GLWidget just before the thread finishes.

In Qt you must not create widgets outside of the main thread since the QObject based classes (including QGLWidget) are neither reentrant nor thread-safe. The solution is to create the widget in the main GUI thread and let the rendering threads do the actual rendering (i.e. OpenGL calls).

One way to do this is to create a QGLWidget subclass that contains a QThread subclass with functions to start and stop the rendering.

Below we present an example of this approach with a thread continuously rendering a spinning tetrahedron. But the rendering could easily have been made to react to the QGLWidget paint events instead. Our example begins with a QThread subclass that we'll call GLThread. Here's the class definition:

class GLThread : public QThread
{
public:
    GLThread(GLWidget *glWidget);
    void resizeViewport(const QSize &size);
    void run();
    void stop();
 
private:
    bool doRendering;
    bool doResize;
    int w;
    int h;
    int rotAngle;
    GLWidget *glw;
};

And here's the implementation:

GLThread::GLThread(GLWidget *gl) 
    : QThread(), glw(gl)
{
    doRendering = true;
    doResize = false;
}
 
void GLThread::stop()
{
    doRendering = false;
}
 
void GLThread::resizeViewport(const QSize &size)
{
    w = size.width();
    h = size.height();
    doResize = true;
}    
 
void GLThread::run()
{
    srand(QTime::currentTime().msec());
    rotAngle = rand() % 360;
 
    glw->makeCurrent();
    glMatrixMode(GL_PROJECTION);
    glLoadIdentity();        
    glOrtho(-5.0, 5.0, -5.0, 5.0, 1.0, 100.0);
    glMatrixMode(GL_MODELVIEW);
    glViewport(0, 0, 200, 200);
    glClearColor(0.0, 0.0, 0.0, 1.0);
    glShadeModel(GL_SMOOTH);
    glEnable(GL_DEPTH_TEST);
 
    while (doRendering) {
        if (doResize) {
            glViewport(0, 0, w, h);
            doResize = false;
        }
        // Rendering code goes here
        glw->swapBuffers();
        msleep(40);
    }
}

The stop() function will stop the rendering and terminate the thread. And the resizeViewport() function can be called by the main thread to notify the rendering thread that the widget size has changed to make the thread resize the GL viewport accordingly.

The heart of the GLThread is its run() function where the GL rendering takes place. The makeCurrent() call makes the widget's GL context current in this thread. Since we swap the buffers ourselves we must call swapBuffers(). We're allowed to do these QGLWidget calls since they resolve into platform specific GLX, AGL, or WGL calls and don't generate any Qt events. This would have caused problems under X11 if we hadn't made Xlib thread-safe. We've omitted the rendering code itself since it isn't relevant. Before looping we put the thread to sleep for a little while with the msleep() call, so that the threads won't compete and congest the system with rendering calls. The msleep() call also indirectly sets the speed of the rendering.

The GLWidget class:

class GLWidget : public QGLWidget
{
public:
    GLWidget(QWidget *parent);
    void startRendering();    
    void stopRendering();
 
protected:
    void resizeEvent(QResizeEvent *evt);
    void paintEvent(QPaintEvent *);
    void closeEvent(QCloseEvent *evt);
 
    GLThread glt;
};
 
GLWidget::GLWidget(QWidget *parent) 
    : QGLWidget(parent, "GLWidget", 0, WDestructiveClose),
      glt(this)
{ 
    setAutoBufferSwap(false);
    resize(320, 240);
}
 
void GLWidget::startRendering()
{
     glt.start();
}
 
void GLWidget::stopRendering()
{
    glt.stop();
    glt.wait();
}
 
void GLWidget::resizeEvent(QResizeEvent *evt)
{
    glt.resizeViewport(evt->size());
}
 
void GLWidget::paintEvent(QPaintEvent *)
{
    // Handled by the GLThread.
}
 
void GLWidget::closeEvent(QCloseEvent *evt)
{
    stopRendering();
    QGLWidget::closeEvent(evt);
}

The GLWidget subclass just acts as a placeholder for the GL window. Since the rendering thread is handling all the updates for the widget the paintEvent() is reimplemented to do nothing. We don't need to respond to paintEvent()s because we are rendering every 40 milliseconds. But, if the scene is static we would need to communicate the paint event to the rendering thread. Similarly, we don't resize the GL viewport in resizeEvent(); instead we notify the rendering thread that a resize is necessary and leave the thread to take care of it. And since we are handling buffer swapping ourselves we call setAutoBufferSwap(false) in the GLWidget constructor.

Now we'll put these classes together in an application. Our AppWindow class is a trivial QMainWindow subclass containing a QWorkspace. It has functions that can create a new rendering widget that renders in a separate thread, and that can close a widget and terminate its rendering thread.

class AppWindow: public QMainWindow
{
    Q_OBJECT
public:
    AppWindow();
 
protected:
    void closeEvent(QCloseEvent *evt);
 
private slots:
    void newThread();
    void killThread();
 
private:
    QWorkspace *ws;
};
 
AppWindow::AppWindow()
    : QMainWindow(0)
{
    QPopupMenu *menu = new QPopupMenu(this);
    menuBar()->insertItem("&Thread", menu);
    menu->insertItem("&New thread", this, SLOT(newThread()), CTRL+Key_N);
    menu->insertItem("&End current thread", this, SLOT(killThread()), CTRL+Key_K);
    menu->insertSeparator();
    menu->insertItem("E&xit", qApp, SLOT(quit()), CTRL+Key_Q);
 
    ws = new QWorkspace(this);
    setCentralWidget(ws);
}
 
void AppWindow::closeEvent(QCloseEvent *evt)
{
    QWidgetList windows = ws->windowList();
    for (int i = 0; i < int(windows.count()); ++i) {
        GLWidget *window = (GLWidget *)windows.at(i);
        window->stopRendering();
    }
    QMainWindow::closeEvent(evt);
}
 
void AppWindow::newThread()
{
    QWidgetList windows = ws->windowList();
    GLWidget *widget = new GLWidget(ws);
    widget->setCaption("Thread #" + QString::number(windows.count() + 1));
    widget->show();
    widget->startRendering();
}
 
void AppWindow::killThread()
{
    GLWidget *widget = (GLWidget *)ws->activeWindow();    
    if (widget) {
    widget>stopRendering();
        delete widget
    }
}

The AppWindow constructor builds a menu for creating and destroying GLWidget windows. If the user closes the application we stop the rendering in every window before passing on the close event. When the user creates a new thread we just create a new GLWidget and call startRendering(). And when the user stops a thread we call killThread() to stop the rendering; then we delete the widget.

GLThreads Application

#include <qapplication.h>
#include "appwindow.h"
#ifdef Q_WS_X11
#include <X11/Xlib.h>
#endif
 
int main(int argc, char *argv[])
{
#ifdef Q_WS_X11
    XInitThreads();
#endif
    QApplication app(argc, argv);
    AppWindow aw;
    app.setMainWidget(&aw);
    aw.show();
    return app.exec();
}

General Tips

If you do any rendering outside of the paintGL() function, you must always call makeCurrent() first; otherwise you might render on the wrong widget. And if you're using display lists and want to render pixmaps you must set up the display lists from within the initializeGL() function. This is necessary because a new GL context is created when rendering to a pixmap.

If you have problems building Qt with OpenGL under X11, first make sure that you have the OpenGL header and library files correctly installed. The configure script only looks in a few standard locations, and if the OpenGL headers are not found in any of them, configure's auto-detection will fail. Use configure's -v option to see what went wrong. You can add search paths with configure's -I and -L switches. You must also ensure that if your GL library is linked against the pthread library, Qt is also threaded; this can be done by configuring Qt with the -thread option.


[1] OpenGL is a trademark of Silicon Graphics Inc. in the United States and some other countries.
[2] We don't do this call internally in Qt/X11 because we've experienced lockups with certain X servers.


Copyright © 2003 Trolltech. Trademarks