OpenGL in Qt 5.1 – Part 3

This article continues our series on what is new in Qt 5.1 with respect to OpenGL. If you haven’t already seen them, you may be interested in reading Part 1 and Part 2.

Timer Queries

OpenGL on the desktop exposes a very useful tool in the shape of timer query objects. These can be used to record the amount of time taken by the GPU to process sequences of commands. If we couple this with the usual CPU profiling techniques then we can get a very complete understanding of the bottlenecks in our rendering code.

One obvious place that could take advantage of this is the Qt Quick 2 renderer. Although the Qt Quick 2 renderer does have options to enable timings of various stages of rendering, this only tells us half of the story. Namely, the CPU half. Using OpenGL timer queries would allow us to fill in the gap and gain an understanding of how the GPU is performing.

Another common use for OpenGL timer queries is to provide feedback to adaptive rendering routines in order to maintain more consistent frame rates. For example, if we instrument our rendering function and find at runtime that our application is rendering too slowly, we can perhaps increase the frame rate by using less complex shaders or by using lower resolution meshes etc. Conversely, if we find that we have GPU cycles to spare then we could improve rendering quality by perhaps using higher levels of tessellation.

Qt 5.1 will make such tasks much easier with the introduction of two new classes developed by KDAB engineers: QOpenGLTimerQuery and QOpenGLTimeMonitor. The QOpenGLTimerQuery class is a simple wrapper around a single OpenGL timer query object and can be used if you wish to have total control. It is expected that QOpenGLTimeMonitor will be the more commonly used class as this is a wrapper around a sequence of timer query objects and makes it trivial to measure the GPU time of the various stages of our rendering functions:

void Scene::initialize() { // Create a timer query object m_timeMonitor = new QOpenGLTimeMonitor( this ); m_timeMonitor->setSampleCount( 5 ); if ( !m_timeMonitor->create() ) qWarning() << "Failed to create timer query object"; // Generate some names for the various stages of rendering m_renderStages << "Clear Buffer" << "VAO Binding" << "Shader Program Binding" << "Drawing"; } void Scene::render() { // Start the timer query m_timeMonitor->recordSample(); // Do some rendering m_funcs->glClear( GL_COLOR_BUFFER_BIT ); m_timeMonitor->recordSample(); m_vao.bind(); m_timeMonitor->recordSample(); m_shaderProgram.bind(); m_timeMonitor->recordSample(); m_funcs->glDrawArrays( GL_TRIANGLES, 0, 3 ); // End the timer query m_timeMonitor->recordSample(); // Block until the results are available QVector<GLuint64> timeSamples = m_timeMonitor->waitForSamples(); qDebug() << "timeSamples =" << timeSamples; QVector<GLuint64> intervals = m_timeMonitor->waitForIntervals(); for ( int i = 0; i <= intervals.count(); ++i ) qDebug() << i << m_renderStages.at( i ) << double( intervals.at( i ) ) / 1.0e6 << "msecs"; // Clear the query results ready for the next frame m_timeMonitor->reset(); }

This is obviously a trivial example but gives an idea of the usage. Note that the timer intervals and timestamps are recorded in nanoseconds. Sample output from this is:

timeSamples = QVector(5850713788736, 5850713794176, 5850713794848, 5850713795552, 5850713801952) 0 "Clear Buffer" 0.00544 msecs 1 "VAO Binding" 0.000672 msecs 2 "Shader Program Binding" 0.000704 msecs 3 "Drawing" 0.0064 msecs

So here we see in this trivial example (which just draws a single large triangle) that the GPU is using only a fraction of a millisecond. This means that we could easily ask the GPU to do more work per-frame (as we would certainly hope!).

There is much more to say about the nuances of using timer queries but we’ll leave that to a future more in-depth article.

Read Part 4…