Android off-screen rendering using EGL pixelbuffers and OpenGL ES 2.0

Posted at 08 Dec 2014
Tags: android, opengl, egl, ndk, gpgpu

Setting up an Android project for OpenGL ES 2.0 on-screen graphics rendering is quite easy using GLSurfaceView. However, if you want to do off-screen rendering (like rendering to a texture), you need to go the long the way by manually setting up an EGL context and creating an EGL pixelbuffer (“pbuffer”) surface to render to. By this, OpenGL runs completely in the background without any visible surface or view, and will render to a pixelbuffer which you can later read back to main memory, for example by using glReadPixels(). This is the fundament for OpenGL-driven GPGPU experiments on Android devices. This small posts explains how to do that.


I’ll explain it using native C code and the Android NDK, because that’s how I’ve implemented it in a recent project. However, EGL wrapper classes for the SDK are also available and the interface is almost the same. See this anddev.org forum post for an example on how to use the EGL SDK classes. Notice that this forum post is from 2010 and sets up the EGL context to use legacy OpenGL ES 1.x. I will show how to set up an OpenGL ES 2.0 context.

To access the EGL interface in native code, you need to include its C headers from EGL/egl.h and link to the EGL library by adding -lEGL to your LOCAL_LDLIBS makefile variable. Then specify some global state variables:

static EGLConfig eglConf;
static EGLSurface eglSurface;
static EGLContext eglCtx;
static EGLDisplay eglDisp;

We can then implement a setup function that takes the size of the pixelbuffer surface as parameters as follows:

void setupEGL(int w, int h) {
    // EGL config attributes
    const EGLint confAttr[] = {
            EGL_RENDERABLE_TYPE, EGL_OPENGL_ES2_BIT,    // very important!
            EGL_SURFACE_TYPE, EGL_PBUFFER_BIT,          // we will create a pixelbuffer surface
            EGL_RED_SIZE,   8,
            EGL_GREEN_SIZE, 8,
            EGL_BLUE_SIZE,  8,
            EGL_ALPHA_SIZE, 8,     // if you need the alpha channel
            EGL_DEPTH_SIZE, 16,    // if you need the depth buffer
            EGL_NONE
    };

    // EGL context attributes
    const EGLint ctxAttr[] = {
            EGL_CONTEXT_CLIENT_VERSION, 2,              // very important!
            EGL_NONE
    };

    // surface attributes
    // the surface size is set to the input frame size
    const EGLint surfaceAttr[] = {
             EGL_WIDTH, w,
             EGL_HEIGHT, h,
             EGL_NONE
    };

    EGLint eglMajVers, eglMinVers;
    EGLint numConfigs;

    eglDisp = eglGetDisplay(EGL_DEFAULT_DISPLAY);
    eglInitialize(eglDisp, &eglMajVers, &eglMinVers));

    LOGI("EGL init with version %d.%d", eglMajVers, eglMinVers);

    // choose the first config, i.e. best config
    eglChooseConfig(eglDisp, confAttr, &eglConf, 1, &numConfigs));

    eglCtx = eglCreateContext(eglDisp, eglConf, EGL_NO_CONTEXT, ctxAttr);

    // create a pixelbuffer surface
    eglSurface = eglCreatePbufferSurface(eglDisp, eglConf, surfaceAttr);    

    eglMakeCurrent(eglDisp, eglSurface, eglSurface, eglCtx));
}

It is absolutely necessary to set the EGL_RENDERABLE_TYPE and EGL_CONTEXT_CLIENT_VERSION to use OpenGL ES 2.0, otherwise you will notice that calling glCompileShader() and other ES 2.0 functions will fail. Furthermore, you should check for errors after each egl...() call. I just omitted here it so the example code is not too bloated.

You should also create a shutdown function to release the EGL objects again:

void shutdownEGL() {
    eglMakeCurrent(eglDisp, EGL_NO_SURFACE, EGL_NO_SURFACE, EGL_NO_CONTEXT);
    eglDestroyContext(eglDisp, eglCtx);
    eglDestroySurface(eglDisp, eglSurface);
    eglTerminate(eglDisp);

    eglDisp = EGL_NO_DISPLAY;
    eglSurface = EGL_NO_SURFACE;
    eglCtx = EGL_NO_CONTEXT;
}

Call these functions at the beginning and end of your activity life-cycle, respectively. During application run-time you can now use the gl...() rendering functions to render some graphics off-screen into the pixelbuffer. You might use glReadPixels() to copy the rendered image data to a ByteBuffer. You can then construct an Android Bitmap from it by using Bitmap.copyPixelsFromBuffer().

Notice that it is very important that all calls to egl...() or gl...() functions happen in the same thread! When you called setupEGL() from the main thread, then all your texture handling, graphics rendering, etc. should also be executed in the main thread.

If you spotted a mistake or want to comment on this post, please contact me: post(-at-)mkonrad(-dot-)net.
← “Library for GPGPU on mobile and embedded systems: *ogles_gpgpu*
View all posts
Resolving the 'unresolved inclusion' error in Eclipse CDT for standard library headers of the Android NDK” →