MeeGo 1.2 Harmattan Developer Documentation Develop for the Nokia N9

OpenGL ES 2.0 interface with gaming features.

This API adds to Qt a "pure" OpenGL ES 2.0 interface with specific functionalities to aid in game development and game porting. Over a standard OpenGL ES interface in Qt this API adds:

  • OpenGL ES 2.0 API across all these targets (meaning: OpenGL ES API is emulated on top of OpenGL for desktop targets): Fremantle, MeeGo, Symbian^3 devices, Desktop Linux, Mac OSX, Windows
  • Support for HW scaling on platforms that support it
  • Debugging features
  • Automatic handling (if enabled) of graphics memory issues on Symbian^3

API is fairly self-explanatory and covers mainly only OpenGL ES initialization. Other additional features are controlled via compiler defines and C++ namespace based API wrapping.

IMPORTANT NOTE: in order for the debug functionalities to work, OpenGL ES headers MUST NOT be included directly from anywhere in your code and all code calling OpenGL ES must be C++ (wrappers use namespaces). Include this header only. It includes the GL function prototypes.

C++ namespace wrapping

This API uses C++ namespace wrapping to enable some of the additional functionality without having developers to change their GL code. GL headers coming with this API have C++ preprocessor tricks that define GL functions either in global namespace or in namespace 'qgamegl2wrapper'. When application developer includes the headers, the resulting code will call some GL functions direct to GL driver, while others go to the wrapper. Which functions get routed to wrapper, depend on enabled functionality.

In addition to defining GL functions on two different namespaces, the GL API has a default parameter in the initialization function that is used to pass in the "wrapper mode". This all is done automatically in the header files (depending on what defines are enabled) and developer doesn't have to worry about it.

In desktop builds, some of the wrapping is always enabled so that "GL ES" to "GL" emulation can be implemented. This emulation is not intended for FULL emulation, but rather enable content to be ported easily across. For this reason some behaviour may differ slightly. Especially if vendor specific extensions are in use, changes are that there is no implementation of those extensions in the emulator layer.

Debug / wrapper features

See <qgameopenglescommon.h> header file for more precise explanations, defines are listed here for quick reference:

  • QGAMEOPENGLES_LOG_TO_FILE routes LOGging to file [ TODO: control of logging ... ]
  • QGAMEOPENGLES_OPENGLES_SHADOWING enables VBO + texture shadowing functionality [ Needed for Symbian^3 devices ]
  • QGAMEOPENGLES_TRACK_GRAM_USAGE tracks graphics memory usage
  • QGAMEOPENGLES_GL_ERROR_WARNINGS reports errors to LOG file on GL errors
  • QGAMEOPENGLES_GL_ERROR_ASSERTS asserts (breaks to a stop-n-go breakpoint) on GL errors
  • QGAMEOPENGLES_DRAW_CALL_DEBUGGING does multitude of 'higher level' GL error checks (VBO out of bounds accesses etc.)
  • QGAMEOPENGLES_FULL_DEBUG combines all debugging flags
  • QGAMEOPENGLES_CALCULATE_STATS keeps track of statistics [ NOT IMPLEMENTED YET ! ]
  • QGAMEOPENGLES_PERFORMANCE_WARNINGS warns about bad practices (vs. performance) [ NOT IMPLEMENTED YET ! ]
  • QGAMEOPENGLES_SHADER_VALIDATOR validates shader for ultimate portability against GLSL ES spec [ NOT IMPLEMENTED YET ! ]

Debug features include things such as checking for out-of-bounds accesses of VBO's (GL error doesn't flag them, behaviour UNDEFINED as per specification), reporting shader compiler errors, etc. Generally the intent of the debugging functionality to let developer debug GL related error cases (where error is not only GL errors, but, e.g., hitting conditions that are UNDEFINED by specification) exactly where the error happened.

HW scaling

Rendering surface is initialized with a 'screen configuration'. Screen configuration includes a render resolution. With this mechanism game developer can choose a resolution it wants to render into with OpenGL ES. This API automatically scales the render result to the screen. There are two reasons why you might want to do this: easy porting of existing assets (that were developed for different resolution) or increasing frame rate.

Not all platforms have support for zero-cost HW scaling, so the results may vary from platform to another as to what to expect. For platforms that have zero-cost HW scaling (e.g., Nokia MeeGo devices), the performance impact of switching to rendering in 640x360 resolution instead of the native 854x480 is pretty much equivalent of performance on a device that has otherwise exactly same SW & HW but has a display panel of 640x360. Usually this would mean roughly 1.8x increase in performance/frame rate (resolution ratio).

On devices that don't have any kind of HW scaling enablers at all, rendering is directed to an off-screen render surface (EGL pixmap or pbuffer) and a bilinear filtered scaling blit is issued to scale the graphics to the full screen window. On most HW this approach doesn't produce big performance benefits. It is provided for making the porting easier.

Event handling, Touch Coordinate mapping & integration with Qt event loop

Since this API onws the underlying GL widget / OS window, normal Qt C++ inheritance can't be used for event handling using this API. To handle events, this API exposes the QWidget by returning it through the API (GetWidget()) so that developers can install event filter ( GetWidget()->installEventFilter(myeventfilter); ) to the widget and get access to the events delivered to the widget. See the example code for reference.

Because HW scaling is controlled by the API (and also in case of S60, the switching of the orientation modes), the best information about how the touch coordinates should be mapped is in this API itself. To facilitate mapping of the touch coordinates (expressed in current system specific orientation in full screen resolution) to the desired surface resolution. If you have, e.g., 640x360 render surface, typically you want touch coordinates between [0,0] - [639, 359]. API to call for this is "MapTouchCoordinate".

For Symbian^3 platform also, it is IMPORTANT that developer implements "symbianEventFilter" and passes the Symbian events to this API via "handleSymbianEvent". If this is not done, the graphics memory handling will not work correctly.

While it would be technically feasible to totally own the QApplication inside this API implementation and "hide" all of this OS specific stuff, decision was made to keep the existing Qt application structure in place as much as possible.

Stop-n-go breakpoints

When the debugging features are enabled and the program hits an error condition a "stop-n-go" breakpoint is executed. Since there is no reliable way of inserting a SW breakpoint that the debugger would stop to without errors (debugger doesn't know about that breakpoint as it is not inserted through the UI). For making debugging possible at the exact state when the error occured, this API has a function for registering a callback function that will be called when error occurs. The typical usage pattern is to create a simple function that does nothing (perhaps has a volatile expression in it to avoid optimization) and a breakpoint is insterted to that function. Callback can be set by calling to RegisterAssert(). See also the supplied example on the usage.

Sometimes the debugger doesn't have enough information to decode the stack trace to figure out where exactly in game code the error occured. To get around this, you need to step in debugger through some assembly code to get back to the game code. Usually 5-6 presses on F10 (step over) is enough to get back to the offending code line and the surrounding state can be examined.

Simple Example: main.cpp
 #include <QtGui/QApplication>
 #include <QtGui/QMainWindow>
 #include "qgameopengles2test.h"

 #if defined(Q_WS_X11)
 #include <X11/Xlib.h>
 #endif

 int main(int argc, char *argv[])
 {
 #if defined(Q_WS_X11)
    XInitThreads();
 #endif

  QCoreApplication::setAttribute(Qt::AA_NativeWindows, true);
  QCoreApplication::setAttribute(Qt::AA_ImmediateWidgetCreation, true);

  QTestApplication app(argc, argv);
  return app.exec();
 }
Simple Example: qgameopengles2test.cpp
 #include <QTimer>
 #include <QtGui/QMouseEvent>
 #include <QtGui>
 #include <QtGui/QTouchEvent>

 #include "qgameopengles2test.h"
 #include "qgameopengles2.h"

 QGameOpenGLES2 *gles2;
 OurEventFilter *filter;
 QTimer         *timer;

 #define SURFACE_WIDTH  640
 #define SURFACE_HEIGHT 360

 // INSERT breakpoint here to debug OpenGL ES error cases
 //
 // This function needs to be registered with the RegisterAssert through the
 // QGameOpenGLES2 API.
 //
 // NOTE: to get back to your program to find out where this call was made,
 //       use "step over" to get to your program (stack unwind).
 //
 // See qgameopenglescommon.h for definitions on debug modes. For full debug,
 // add:
 //           DEFINES += QGAMEOPENGLES_FULL_DEBUG
 //
 // To your QMAKE project file.
 //
 // NOTE: for handling the Symbian graphics memory issues automatically, enable
 //       also QGAMEOPENGLES_SHADOWING

 void assertfunc(char *str)
 {
     volatile char *t = str;
     t[0] = t[0];
 }

 //    With QGameOpenGLES2 you need to receive the Qt events through the
 // EventFilter mechanism of Qt.

 int posx = 0;
 int posy = 0;

 bool OurEventFilter::eventFilter(QObject *obj, QEvent *event)
 {
     switch(event->type())
     {
         case(QEvent::TouchEnd):
         {
             QTouchEvent *t = static_cast<QTouchEvent*>(event);
             QTouchEvent::TouchPoint p = t->touchPoints()[0];
             QPoint pp = QPoint((int)(p.pos().x()), (int)(p.pos().y()));
             gles2->MapTouchCoordinate(pp);
             posx = pp.x(); posy = pp.y();

             if((posx < 30) && (posy < 30)) exit(0);
             return true;
         }
         case(QEvent::MouseButtonPress):
         {
             QMouseEvent *tt = static_cast<QMouseEvent*>(event);
             QPoint p = tt->pos();
             gles2->MapTouchCoordinate(p);
             posx = p.x(); posy = p.y();

             if((posx < 30) && (posy < 30)) exit(0);
             return true;
         }
         default:
          // standard event processing
          return obj->eventFilter(obj, event);
      }
 }

 QTestApplication::QTestApplication(int &argc, char **argv, int version) :
         QApplication(argc, argv, version)
 {
         int i;

         // Create API instance
         gles2 = QGameOpenGLES2::Create(this);

         // Scan through render configurations and find the one we need
         int rcount = gles2->RenderConfigurationCount();
         int rconf = -1;
         for(i=0;i<rcount;i++)
         {
             QGameOpenGLES2::RenderConfig config;
             gles2->RenderConfiguration(i, config);

             if((config.mAAQuality   == QGameOpenGLES2::EANTI_ALIASING_NONE)  &&
                (config.mColorFormat == QGameOpenGLES2::ECOLOR_FORMAT_RGB565) &&
                (config.mDepthSize   == QGameOpenGLES2::EDEPTH_MINIMUM_16BITS)&&
                (config.mStencilSize == QGameOpenGLES2::ESTENCIL_NONE))
             {
                 rconf = i;
                 break;
             }
         }
         if(rconf == -1) { qDebug("Render configuration not found!"); exit(0); }

         // Scan through screen configurations and find the one we need
         int scount = gles2->ScreenConfigurationCount();
         int sconf = -1;
         for(i=0;i<scount;i++)
         {
             QGameOpenGLES2::ScreenConfig config;
             gles2->ScreenConfiguration(i, config);

             if((config.mHeightInPixels == SURFACE_HEIGHT) &&
                (config.mWidthInPixels  == SURFACE_WIDTH))
             {
                 sconf = i;
                 break;
             }
         }
         if(sconf == -1) { qDebug("Screen configuration not found!"); exit(0); }

         // Initialize the API with render and screen config
         gles2->Initialize(rconf, sconf);

         // Register the function to call to for GL debug asserts
         gles2->RegisterAssert(assertfunc);

         // Install our event filter to the widget to receive events
         filter = new OurEventFilter();
         gles2->GetWidget()->installEventFilter(filter);

         // Set window title
         gles2->GetWidget()->setWindowTitle(tr("ES test"));

         // Setup a idle callback to run the gameloop from. NOTE: while we are in
         // the idle callback, no events are processed etc. (event loop of Qt is
         // blocked)
         timer = new QTimer(this);
         timer->setInterval(0);
         timer->setSingleShot(false);
         timer->start();
         QObject::connect(timer, SIGNAL(timeout()), this, SLOT(idletimer()));

         // Indicate that we want active mode to be on (we own the screen)
         gles2->SetActiveMode(true);
 }


 QTestApplication::~QTestApplication()
 {
 }

 // IMPORTANT:
 //
 //   For Symbian targets we need to pass Symbian events to the QGameOpenGLES2
 //   API so that it can handle graphics memory usage correctly

 #if defined(Q_WS_S60)
 bool QTestApplication::symbianEventFilter( const QSymbianEvent *event)
 {
     return gles2->handleSymbianEvent(event);
 }
 #endif

 int frame = 0;

 void QTestApplication::idletimer()
 {
     // GL rendering code (frame loop)
     glClearColor(((float)(frame&255))/255, 0, 0, 1);
     glClear(GL_COLOR_BUFFER_BIT);
     glEnable(GL_SCISSOR_TEST);
     glClearColor(1.f, 1.f, 1.f, 1.f);
     glScissor(0,0,40,40);
     glClear(GL_COLOR_BUFFER_BIT);
     glClearColor(1.f, 0.f, 1.f, 1.f);
     glScissor(60,60,40,40);
     glClear(GL_COLOR_BUFFER_BIT);
     glClearColor(0.f, 1.f, 0.f, 1.f);
     glScissor(posx-15,SURFACE_HEIGHT-posy-15,30,30);
     glClear(GL_COLOR_BUFFER_BIT);
     glDisable(GL_SCISSOR_TEST);

     // When frame hits 250, we on purpose call glEnable(GL_TEXTURE_2D)
     // to set off the GL debug feature (glEnable for GL_TEXTURE_2D is
     // invalid for OpenGL ES 2.0).
     if(frame == 250)
     {
         glClearColor(2,3,4,5);
         glEnable(GL_TEXTURE_2D);
         glClearColor(4,5,6,7);
     }

     // Finally we swap buffers to get the contents to display
     gles2->SwapBuffers();

     frame++;
 }
Simple Example: qgameopengles2test.h
 #ifndef QGAMEOPENGLES2TEST_H
 #define QGAMEOPENGLES2TEST_H

 #include <QWidget>
 #include <QApplication>

 class QTestApplication : public QApplication
 {
     Q_OBJECT

 public:
     QTestApplication(int &argc, char **argv, int = QT_VERSION);
     ~QTestApplication();

 public slots:
     void idletimer();


 // For Symbian targets we need to pass Symbian events to the QGameOpenGLES2 API
 // so that it can handle graphics memory usage correctly
 #if defined(Q_WS_S60)
 public:
     virtual bool symbianEventFilter( const QSymbianEvent *event);
 #endif

 };

 class OurEventFilter : public QObject
 {
      Q_OBJECT
 protected:
    bool eventFilter(QObject *obj, QEvent *event);
 };

 #endif // QGAMEOPENGLES2TEST_H
Simple Example: qgameopengles2test.pro
 LIBS += -lQGameOpenGLES2

 QT   += core gui
 QT   -= opengl
 LIBS += -lGLESv2 -lEGL

 TARGET = QGameOpenGLES2Test
 TEMPLATE = app

 SOURCES += main.cpp qgameopengles2test.cpp

 HEADERS  += qgameopengles2test.h

 INCLUDEPATH += ../inc_release

 # DEBUGGING ENABLERS (modify if needed.. disable stuff for release builds!!)
 #DEFINES += QGAMEOPENGLES_FULL_DEBUG

 # This needs to be enabled for "auto handling" of graphics memory issue on S60
 # (keeps shadow copies of textures and VBOs on the RAM)
 symbian: DEFINES += QGAMEOPENGLES_SHADOWING

 target.path = /usr/local/bin
 INSTALLS += target

 symbian {
     TARGET.UID3 = 0xeb48d878
     TARGET.EPOCSTACKSIZE = 0x14000
     TARGET.EPOCHEAPSIZE = 0x020000 0x1800000      # we need to have enough space for storing the SHADOWED textures..
 }

Copyright (C) 2010-2011 Nokia Corporation
Nokia Proprietary
MeeGo 1.2 Harmattan API