Making Qt and OpenSceneGraph play nice

Tags: programming, software

Published on
« Previous post: Building a FreeBSD NAS, part III: ZFS — Next post: Why the Heartbleed Bug does not … »

We have been using the powerful OpeneSceneGraph graphics toolkit for our internal software. The software initially started out with a GUI based on GTK, but in the middle of 2012, we decided to rewrite it, using Qt. Our rewrite was mostly prompted my portability concerns and code constraints, which required an almost complete overhaul of the architecture.

During the rewrite, I thought hard about different ways of combining Qt and OpenSceneGraph. Our application should have the possibility to render different scenes in different Qt widgets. This turned out to be harder than expected.

The initial attempt

Being a very impressionable fool, I initially looked at the examples that shipped with OSG. Their main examples all contained a QTimer class for managing updates. As I recall, the code was very similar to this:

connect( &timer, SIGNAL(timeout()), this, SLOT(update()) );
timer.start( 15 );

Looking back now, I see the glaringly obvious performance issue. Multiple timers force each widget to redraw multiple times per second, making any interaction with the GUI all but useless. Obviously, this was not the way to go.

Getting funky with threads

The next idea involved dreaded threads. I figured that by setting up each widget in its own render thread, GUI interactions still could work. This was not completely wrong, but more than 5 windows still made for unbearably large processing times. Reading OSG’s source code again proved fruitful, and I found out about on-demand rendering. By simply creating all viewers slightly differently, the threads suddenly worked better and took less processing time:

osgViewer::CompositeViewer* viewer = new osgViewer::CompositeViewer;
viewer->setRunFrameScheme( osgViewer::ViewerBase::ON_DEMAND );

At the surface, this solution seemed to perfectly. Things got strange, though, when we attempted to modify existing scene graphs. So I implemented a special update callback that enqueued nodes on each call. This helped and everything was fine again. Except for random crashes. And problems with some graphics cards. And some more random crashes.

I now know that the crashes we were getting are caused by us attempting to perform multi-threaded OpenGL rendering without attempting to perform a proper switching of graphics contexts. While Qt appears to have given some thought to this matter in more recent versions, OpenSceneGraph apparently did not. At least, the osgQt module that we used to connect both frameworks is not suitable for our particular requirements.

Getting smarter

A different solution had to be found. I took multiple steps back and thought about providing the “glue” between OpenSceneGraph and Qt on my own. Armed with the knowledge of my two previous failures, this was surprisingly easy.

At the core, I decided I wanted to use a QGLWidget for encapsulating the rendering stuff. Since I required the widget only to render on demand, I did not require any threaded redraw functionalities. Instead, I relied on the builtin event system by Qt.

I thus created a new widget that inherited from QGLWidget. The widget was to contain an instance of an osgViewer::GraphicsWindowEmbedded. Said graphics window provided its own graphics context which I could attach to any number of cameras. The cameras, in turn, could then be attached to some osgViewer::CompositeViewer instance. The constructor of the widget looked something like this:

OSGWidget::OSGWidget( QWidget* parent,
                      const QGLWidget* shareWidget,
                      Qt::WindowFlags f )
  : QGLWidget( parent,
               shareWidget,
               f ),
    graphicsWindow_( new osgViewer::GraphicsWindowEmbedded( this->x(), this->y(),
                                                            this->width(), this->height() ) ),
    viewer_( new osgViewer::CompositeViewer )
{
  osg::Camera* camera = new osg::Camera;
  camera->setViewport( 0, 0, this->width(), this->height() );
  camera->setClearColor( osg::Vec4( 0.f, 0.f, 1.f, 1.f ) );
  camera->setGraphicsContext( graphicsWindow_ );

  osgViewer::View* view = new osgViewer::View;
  view->setCamera( camera );
  view->setSceneData( ... );

  viewer_->addView( view );
  viewer_->addView( sideView );
  viewer_->setThreadingModel( osgViewer::CompositeViewer::SingleThreaded );

  this->setFocusPolicy( Qt::StrongFocus );
  this->setMinimumSize( 100, 100 );
}

The call to setFocusPolicy is required because I want the widget to receive all keyboard events, regardless of whether the widget has received focus by clicking into it, so that I can forward them to the graphics window:

void OSGWidget::keyReleaseEvent( QKeyEvent* event )
{
  QString keyString   = event->text();
  const char* keyData = keyString.toAscii().data();

  this->getEventQueue()->keyRelease( osgGA::GUIEventAdapter::KeySymbol( *keyData ) );
}

I had to implement similar event handling procedures for mouse move events, key press events, and so on. To ensure that repaints happen after user interactions, I overrode the generic event handler of the widget:

bool OSGWidget::event( QEvent* event )
{
  bool handled = QGLWidget::event( event );

  switch( event->type() )
  {
  case QEvent::KeyPress:
  case QEvent::KeyRelease:
  case QEvent::MouseButtonDblClick:
  case QEvent::MouseButtonPress:
  case QEvent::MouseButtonRelease:
  case QEvent::MouseMove:
    this->update();
    break;

  default:
    break;
  }

  return handled;
}

The only remaining “tricky” part is to handle resize events properly. Here, the viewport of each camera should be reset correctly. For a single camera, this is rather easy:

void OSGWidget::resizeGL( int width, int height )
{
  this->getEventQueue()->windowResize( this->x(), this->y(), width, height );
  graphicsWindow_->resized( this->x(), this->y(), width, height );

  std::vector<osg::Camera*> cameras;
  viewer_->getCameras( cameras );

  assert( cameras.size() == 1 );

  cameras.front()->setViewport( 0, 0, this->width(), this->height() );
}

Conclusion & code

So far, the new widgets are working perfectly. Even multiple cameras with different viewports did not prove to be a significant problem. So, after much toiling and teeth-gnashing, I conclude that this is a good way for making OpenSceneGraph and Qt behave.

If you want to grab the code as a starting point for your own projects, you are most welcome to do so. You can clone the git repository. The code is released under the “MIT Licence”.