Chapter 15. OpenGL ES and EGL on Handheld Platforms

By now, you should be familiar with the details of OpenGL ES 2.0 and EGL 1.3. In the final chapter, we divert ourselves a bit from the details of the APIs to talk about programming with OpenGL ES 2.0 and EGL in the real world. There are a diverse set of handheld platforms in the market that pose some interesting issues and challenges when developing applications for OpenGL ES 2.0. Here, we seek to cover some of those issues by discussing these handheld platform issues:

  • C++ portability.

  • OpenKODE.

  • Platform-specific shader binaries.

  • Targeting extensions.

Handheld Platforms Overview

Knowing OpenGL ES 2.0 and EGL 1.3 is a critical step to writing games and applications for handheld platforms. However, a big part of the challenge in targeting handheld devices is coping with the diversity of platforms. One of the biggest issues today in the handheld market is the fragmentation in development capabilities and environments available on handheld platforms. Let’s start by taking a look at some of the biggest platforms out there today.

  • Nokia—Series 60 on Symbian.

  • Qualcomm—BREW.

  • Microsoft—Windows Mobile.

  • Embedded Linux.

  • Sony Ericsson—UIQ on Symbian.

In addition to operating systems (OSs), a wide variety of CPUs are in use. Most of the architectures are based on the ARM processor family, which supports a wide variety of features. Some CPUs support floating-point natively, whereas others do not. Targeting ARM means you need to be cognizant of aligning data to 32-bit boundaries and potentially providing your own fast floating-point emulation library (or using fixed-point math).

Some of the OSs—Windows Mobile and Embedded Linux in particular—provide the most straightforward and familiar development environment for Windows/Linux developers. For example, Microsoft provides an embedded version of Visual C++ that contains much of the functionality of the desktop Windows version. In addition, a subset of the Win32 API is available, making portability easier.

Other of the OSs—Symbian and BREW in particular—are quite different than what PC and console developers are used to. One needs to be very careful about which C++ features are used, not having writeable static global variables, managing memory, and a host of other issues. The issue with writable static global variables is that some handheld OSs store applications as dynamic link libraries (DLLs) and the static data end up being in read-only memory (ROM) and therefore cannot be written to. In addition to code issues, Symbian, for example, provides an entirely new toolchain called Carbide based on the Eclipse IDE. This means learning to use a new debugger, new project files (called MMP files in Symbian), and a set of new OS APIs.

Online Resources

Given the wide array of handheld platforms, we thought it would be useful to give a quick guide to where to get information to start developing for each of the platforms. If you plan to target Nokia devices, you will want to visit http://forum.nokia.com, where you can freely download the Series 60 Software Development Kit (SDK) along with a wide array of documentation. As of this writing, Nokia already provides support in its SDK for OpenGL ES 1.1 so you can look at the existing OpenGL ES samples to get a feel for how you will develop OpenGL ES 2.0 applications. Developers targeting Symbian on Sony Ericsson devices using UIQ can find information at http://developer.sonyericsson.com.

Likewise, the Qualcomm BREW SDK is available from http://brew.qualcomm.com and supports OpenGL ES 1.0 plus extensions. As of this writing, OpenGL ES 2.0 is not yet supported. However, just as with the Series 60 SDK, you can begin developing applications for BREW and start learning about the platform portability issues. Qualcomm has already announced plans to support OpenGL ES 2.0 in its forthcoming MSM7850 with the LT graphics core.

For developers looking to get started with Windows Mobile, Microsoft hosts a Windows Mobile Developer Center at http://msdn2.microsoft.com/enus/windowsmobile/default.aspx. If you already have Microsoft Visual Studio 2005, you can download the relevant Windows Mobile 6 SDK and develop directly in Visual Studio 2005. A large number of devices support Windows Mobile 6, such as the Moto Q, Palm Treo 750, Pantech Duo, HTC Touch, and many more. As of yet, there are no devices supporting OpenGL ES 2.0, but we can expect such devices in the future.

Finally, for developers looking to get started with Embedded Linux, check out www.linuxdevices.com. There were several Embedded Linux devices that entered the market in 2007, such as the Nokia N800 Internet tablet. For developers wanting to develop OpenGL ES 2.0 applications under regular Linux (not embedded), Imagination Technologies has released its OpenGL ES 2.0 wrapper for Linux. This can be downloaded from www.powervrinsider.com.

C++ Portability

The first decision you need to make when developing your OpenGL ES 2.0 application is which language you will use. Many handheld developers choose to use plain vanilla C, and with good reason. C++ portability can be a significant issue because different platforms have varying levels of support for C++ features. The reason that many C++ features are not supported is because they can be burdensome for the implementation. Remember, we are working on handheld devices here, and conserving memory and power are significant goals of handheld OSs. However, this means the C++ features you might be accustomed to using are not available on handheld platforms.

For example, Symbian 8 does not support throwing C++ exceptions. This means that using exceptions in your C++ code would not be portable (Symbian does provide its own exception mechanism, but it is not the standard C++ way). This lack of exceptions also means that Symbian 8 does not support Standard Template Library (STL). To implement conformant STL, it is necessary to be able to throw exceptions. As a consequence, Symbian provides its own set of container classes that you can use. The long and short of this is that using STL might not be portable to certain platforms.

Another consequence of Symbian 8 not supporting C++ exceptions is that that the programmer must manually manage a cleanup stack. That is, to properly support the new operator failing in C++, one must be able to throw an exception. Instead, on Symbian, the programmer becomes responsible for managing the cleanup stack themselves and objects are created with two-phase construction. Having this sort of code in a portable engine is really not an option. As a consequence, some developers choose to write their own memory manager on Symbian. They allocate a block of memory at startup of a known size they will need and then manage all allocations themselves so that they do not have to litter their code with Symbian-specific cleanup code.

With all that said, a lot of the C++ portability issues were fixed in Symbian 9. However, the point here was to give you a flavor of the types of C++ limitations one finds on handheld platforms. There are a number of features you simply cannot be confident will be supported on all handheld devices. For example, features such as runtime type information, exceptions, multiple inheritance, and STL might not be present. To guarantee portability, you will want to restrict your use of C++. Or, like many developers, simply write your application in C.

If you choose to use C++, the following is a list of features that you should avoid to gain portability:

  • Runtime type information—For example, the use of dynamic_cast, which requires an implementation to know the runtime type of a class.

  • Exceptions—The standard C++ mechanism of try-catch is not supported on some handheld OSs.

  • Standard Template Library—Although STL provides many useful classes, it also requires exceptions that are not supported on all handheld OSs.

  • Multiple inheritance—Some handheld implementations of C++ do not allow classes that are derived from multiple classes.

  • Global data—Some handheld OSs store applications in ROM, and thus having static writeable data is not possible.

This list is not comprehensive, but represents things we have run into as being issues on handheld platforms. You might ask yourself what good a language standard (e.g., C++) is if vendors choose to create nonstandard implementations. There is a valid argument to be made that one should not claim to be supporting C++ without supporting such basic features of the language. In time, it probably will be the case that full C++ will be supported on embedded devices. However, for now, we must live with the choices that platform vendors have made and adjust our code accordingly.

OpenKODE

Aside from C++ inconsistencies across platforms, another major barrier to portability is the lack of common OS APIs. For example, features like input/output, file access, time, math, and events are handled differently on various operating systems. Dealing with these sorts of differences is old hat for seasoned game developers. Most portable game engines are written with abstraction layers for each of the various platforms. The portable portions of the code will not make calls into any OS-specific functions but rather use the abstraction layers.

The issue on handheld platforms is that writing your own abstraction layers has been made orders of magnitude more difficult by the large number of handheld platforms. Also, finding a common set of features across different OSs is very difficult for someone new to handheld platforms. Fortunately, the Khronos group recognized this as a significant barrier to the handheld ecosystem and has invented a new API to deal with it called OpenKODE.

The OpenKODE 1.0 specification was released in February 2008. OpenKODE provides a standard set of APIs (including OpenGL ES and EGL) to which an application can write in order to access functionality on the system. OpenKODE Core provides APIs for events, memory allocation, file access, input/output, math, network sockets, and more. To introduce you to the concept of using OpenKODE, we ported our Hello Triangle example program to use OpenKODE. We removed any calls to OS-specific functions and removed any dependencies on our ES application framework.

A code listing for the example that can be found in Chapter_15/Hello_Triangle_KD is shown in Example 15-1.

Example 15-1. Hello Triangle Using OpenKODE

#include <KD/kd.h>
#include <EGL/egl.h>
#include <GLES2/gl2.h>
typedef struct
{
   // Handle to a program object
   GLuint programObject;

   // EGL handles
   EGLDisplay eglDisplay;
   EGLContext eglContext;
   EGLSurface eglSurface;

} UserData;

///
// Create a shader object, load the shader source, and
// compile the shader.
//
GLuint LoadShader(GLenum type, const char *shaderSrc) {
   GLuint shader;
   GLint compiled;

   // Create the shader object
   shader = glCreateShader(type);

   if(shader == 0)
      return 0;

   // Load the shader source
   glShaderSource(shader, 1, &shaderSrc, NULL);

   // Compile the shader
   glCompileShader(shader);

   // Check the compile status
   glGetShaderiv(shader, GL_COMPILE_STATUS, &compiled);

   if(!compiled)
   {
      GLint infoLen = 0;

      glGetShaderiv(shader, GL_INFO_LOG_LENGTH, &infoLen);

      if(infoLen > 1)
      {
         char* infoLog = kdMalloc(sizeof(char) * infoLen);

         glGetShaderInfoLog(shader, infoLen, NULL, infoLog);
         kdLogMessage(infoLog);

         kdFree(infoLog);
      }

      glDeleteShader(shader);
      return 0;
   }

   return shader;

}

///
// Initialize the shader and program object
// 
int Init(UserData *userData) {
   GLbyte vShaderStr[] =
      "attribute vec4 vPosition;   
"
      "void main()                 
"
      "{                           
"
      "   gl_Position = vPosition; 
"
      "}                           
";

   GLbyte fShaderStr[] =
      "precision mediump float;
"
      "void main()                                 
"
      "{                                           
"
      "  gl_FragColor = vec4(1.0, 0.0, 0.0, 1.0);  
"
      "}                                           
";

   GLuint vertexShader;
   GLuint fragmentShader;
   GLuint programObject;
   GLint linked;

   // Load the vertex/fragment shaders
   vertexShader = LoadShader(GL_VERTEX_SHADER, vShaderStr);
   fragmentShader = LoadShader(GL_FRAGMENT_SHADER, fShaderStr);

   // Create the program object
   programObject = glCreateProgram();

   if(programObject == 0)
      return 0;

   glAttachShader(programObject, vertexShader);
   glAttachShader(programObject, fragmentShader);

   // Bind vPosition to attribute 0
   glBindAttribLocation(programObject, 0, "vPosition");
    // Link the program
   glLinkProgram(programObject);

   // Check the link status
   glGetProgramiv(programObject, GL_LINK_STATUS, &linked);

   if(!linked)
   {
      GLint infoLen = 0;

      glGetProgramiv(programObject, GL_INFO_LOG_LENGTH, &infoLen);

      if(infoLen > 1)
      {
         char* infoLog = kdMalloc(sizeof(char) * infoLen);

         glGetProgramInfoLog(programObject, infoLen, NULL, infoLog);
         kdLogMessage(infoLog);

         kdFree(infoLog);
       }

       glDeleteProgram(programObject);
       return FALSE;
    }

    // Store the program object
    userData->programObject = programObject;

    glClearColor(0.0f, 0.0f, 0.0f, 0.0f);
    return TRUE;
}

///
// Draw a triangle using the shader pair created in Init()
// 
void Draw(UserData *userData) {
   GLfloat vVertices[] = {  0.0f,  0.5f, 0.0f,
                           -0.5f, -0.5f, 0.0f,
                            0.5f, -0.5f, 0.0f };

   // Set the viewport
   glViewport(0, 0, 320, 240);

   // Clear the color buffer
   glClear(GL_COLOR_BUFFER_BIT);

   // Use the program object
   glUseProgram(userData->programObject);
   // Load the vertex data
   glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 0, vVertices);
   glEnableVertexAttribArray(0);

   glDrawArrays(GL_TRIANGLES, 0, 3);

   eglSwapBuffers(userData->eglDisplay, userData->eglSurface); }



///
// InitEGLContext()
//
// Initialize an EGL rendering context and all associated elements
//
EGLBoolean InitEGLContext(UserData *userData,
                          KDWindow *window,
                          EGLConfig config) {
   EGLContext context;
   EGLSurface surface;
   EGLint contextAttribs[] = { EGL_CONTEXT_CLIENT_VERSION, 2,
                               EGL_NONE, EGL_NONE };

   // Get native window handle
   EGLNativeWindowType hWnd;
   if(kdRealizeWindow(window, &hWnd) != 0)
   {
       return EGL_FALSE;
   }
   surface = eglCreateWindowSurface(userData->eglDisplay, config,
                                    hWnd, NULL);
   if(surface == EGL_NO_SURFACE)
   {
      return EGL_FALSE;
   }

   // Create a GL context
   context = eglCreateContext(userData->eglDisplay, config,
                              EGL_NO_CONTEXT, contextAttribs );
   if(context == EGL_NO_CONTEXT)
   {
      return EGL_FALSE;
   }

   // Make the context current
   if(!eglMakeCurrent(userData->eglDisplay, surface, surface,
                                context))
   {
      return EGL_FALSE;
   }
   userData->eglContext = context;
   userData->eglSurface = surface;

   return EGL_TRUE;
}

///
// kdMain()
//
// Main function for OpenKODE application
//
KDint kdMain(KDint argc, const KDchar *const *argv) {
   EGLint attribList[] =
   {
       EGL_RED_SIZE,        8,
       EGL_GREEN_SIZE,      8,
       EGL_BLUE_SIZE,       8,
       EGL_ALPHA_SIZE,     EGL_DONT_CARE,
       EGL_DEPTH_SIZE,     EGL_DONT_CARE,
       EGL_STENCIL_SIZE,   EGL_DONT_CARE,
       EGL_NONE
   };
   EGLint majorVersion,
      minorVersion;
   UserData userData;
   EGLint numConfigs;
   EGLConfig config;
   KDWindow *window = KD_NULL;

   userData.eglDisplay = eglGetDisplay(EGL_DEFAULT_DISPLAY);

   // Initialize EGL
   if(!eglInitialize(userData.eglDisplay, &majorVersion,
                     &minorVersion) )
   {
      return EGL_FALSE;
   }

   // Get configs
   if(!eglGetConfigs(userData.eglDisplay, NULL, 0, &numConfigs))
   {
      return EGL_FALSE;
   }

   // Choose config
   if(!eglChooseConfig(userData.eglDisplay, attribList, &config,
      1, &numConfigs))
   {
       return EGL_FALSE;
   }


   // Use OpenKODE to create a Window
   window = kdCreateWindow(userData.eglDisplay, config, KD_NULL);
   if(!window)
      kdExit(0);

   if(!InitEGLContext(&userData, window, config))
      kdExit(0);

   if(!Init(&userData))
      kdExit(0);

   // Main Loop
   while(1)
   {
      // Wait for an event
      const KDEvent *evt = kdWaitEvent(0);
      if ( evt )
      {
         // Exit app
         if(evt->type == KD_EVENT_WINDOW_CLOSE)
            break;
      }

      // Draw frame
      Draw(&userData);
    }

    // EGL clean up
    eglMakeCurrent(0, 0, 0, 0);
    eglDestroySurface(userData.eglDisplay, userData.eglSurface);
    eglDestroyContext(userData.eglDisplay, userData.eglContext);

    // Destroy the window
    kdDestroyWindow(window);

    return 0;
}

The functions that are part of OpenKODE Core are preceded by the kd prefix. The main function of an OpenKODE application is named kdMain(). You will notice that this example uses OpenKODE functions for all of the window creation and setup. EGL is used for creating and initializing the OpenGL ES rendering surface in the window. In addition, the example uses OpenKODE functions for memory allocation (kdMalloc() and kdFree()) and logging of messages (kdLogMessage()).

This example was purposefully simple just to show that by using OpenKODE we were able to remove any OS-specific calls from our sample application. There are many more functions available in OpenKODE. Had we chosen to do so, the entire application framework we wrote could have been layered on OpenKODE to provide more portability. A full treatment of the OpenKODE core API would warrant a book unto itself so we are just barely scratching the surface here. If you are interested in using OpenKODE in your application, you can go to www.khronos.org/ to download the specification.

Platform-Specific Shader Binaries

In addition to code portability, one of the other issues OpenGL ES 2.0 developers are going to have to tackle is the building and distribution of shader binaries. As you will recall, OpenGL ES 2.0 provides a mechanism whereby an application can provide its shaders in a precompiled binary format. This will be desirable on some platforms for a number of reasons. First, it will very likely reduce load times for your application because the driver will not need to do compilation of shaders. Second, offline shader compilers might be able to do a better job of optimization because they do not have to run under the same memory and time constraints as an online compiler. Third, some implementations of OpenGL ES 2.0 will support only shader binaries, which will mean that you will have to compile your shaders offline.

There are a few issues as a developer of an OpenGL ES 2.0 application you need to consider for targeting multiple platforms with shader binaries. First, it must be understood that shader binaries are inherently non-portable. The binaries may be tied to the device, GPU, or even the driver or OS revision on which your application will run. The binary shader format is defined to be opaque. That is, device vendors are free to store binary shaders in any non-portable format they wish. In general, it is likely that vendors will store the shaders in their final hardware binary.

The second issue is that generation of binary shaders does not have any defined mechanism in the API standard. Each device or GPU vendor is free to build its own tool suite for generating binary shaders and it will be incumbent on you to make sure your engine can support each vendor’s toolchain. For example, AMD provides a Windows executable called MakeBinaryShader.exe, which compiles text shaders into binary. AMD also provides a library-based interface, BinaryShader.lib, which provides functions that you can call into to compile your shaders directly from within an application. Imagination Technologies also provides its own binary shader compilation tool called PVRUniSCo along with an editor called PVRUniSCo Editor. You can be sure that other vendors will come along with their own binary compilation tools as well.

A third issue with binary shaders is that the vendor extension that defines the shader binaries might put restrictions on how those binaries can be used. For instance, the shader compiler binaries might only be optimal (or possibly only even functional) when used with a specific OpenGL ES state vector. As an example, it’s possible that a vendor can define that the shader compilation process accepts input defining that the binary will be used only with fragment blending or multisampling, and the binary might not be valid if these features are not enabled. These restrictions are left to the vendor to define, so application developers are encouraged to consult the vendor-specific extensions defining a given shader binary format.

Your engine will need to support a preprocessing path whereby you can choose one of the device vendors’ tools and store a binary shader package to go along with the app. There is one alternative that AMD (and likely other vendors) will offer in its OpenGL ES 2.0 implementation. AMD will provide an extension whereby an application can call into the driver to retrieve a compiled program binary. In other words, an application will load and compile shaders online in the driver and then it can request the final compiled binary back from the driver and store it out to a file. This binary is in the same format as the binary provided by the offline compilation tool. The advantage to this approach is that an application can compile its shaders on first run (or at install time) using the driver and then read back the shader binaries and store them directly on the device. You should be prepared to handle this type of binary shader compilation path in your engine as well.

Targeting Extensions

Throughout the book, we have introduced you to a number of extensions for features such as 3D textures, ETC, depth textures, and derivatives. The extensions we covered in the book were all Khronos-ratified extensions that will be supported by multiple vendors. That said, not all vendors will support these extensions, which is why they are not part of the standard. You will also encounter extensions that are specific to a single vendor. For example, both AMD and Imagination Technologies provide their own extensions for texture compression formats supported by their hardware. To fully take advantage of the underlying hardware, it will often be useful to use such extensions in your engine.

To maintain portability across OpenGL ES devices, you must have proper fallback paths in your engine. For any extension you use, you must check that the extension string is present in the GL_EXTENSIONS string. For extensions that modify the shading language, you must use the #extension mechanism to explicitly enable them. If an extension is not present, you will need to write fallback paths. For example, if you use some extension for a compressed texture format, you will need to retain the ability to load an uncompressed version of the texture if the extension does not exit. If you use the 3D texture extension, you will need to provide a fallback texture and shader path to substitute for the effect.

If you want to guarantee portability across platforms and are unable to write fallback paths for specific extensions, you should avoid using them. Given the level of platform fragmentation that already exists in terms of OS and device capabilities, you do not want to make the problem worse by making your engine use OpenGL ES 2.0 in a non-portable way. The good news here is that the rules for writing portable OpenGL ES 2.0 applications are really no different than for desktop OpenGL. Checking for extensions and writing fallback paths is a fact of life you need to live with if you want to harness the latest features offered by OpenGL ES 2.0 implementations.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset