Setting up Native OpenGL ES* on Android* Platforms Abstract This document details the necessary steps for creating a native OpenGL ES 2.0 graphics application on the Android platform. The process for setting up an OpenGL ES 2.0 application using the Java* API will be discussed followed by a conversion of the application to use native rendering inside a shared library. Note: In order to support the largest audience, no IDE will be used in this document. All project creation, maintenance, and deployment will be performed through the command line tool set provided by Google. Project Setup To create a default Android project, execute the following command at a command prompt. android create project --target <TID> \ --name <ProjName> \ --path <FolderPath> \ --activity <ActivityName> \ --package <Package> Note: The target ID will be different depending on how the Android SDK is configured. Retrieve a list of available targets by executing android list targets at a command prompt. The following settings will be used for this project: --name DemoProject --activity DemoProjectActivity --package demo.project The project generated by the Android utility is the default ”Hello World” project. Verify the project was initialized correctly by executing ant debug at a command prompt while in the project's root directory. Adding OpenGL ES Support Android exposes the OpenGL ES API directly to Java for application developers. The classes and methods used to setup OpenGL are used whether leveraging the Java API or performing native rendering. Therefore, the details of rendering a simple scene using the Java API will be discussed beforehand. Following this, the sample application will be modified to use native rendering. Removing the Title Bar When executing the Hello World application, the application title bar is visible during execution. This behavior is typically undesirable when programming OpenGL ES applications. This behavior may be disabled by adding the appropriate theme attribute of the activity tag inside the AndroidManifest.xml file. android:theme=”@android:style/Theme.NoTitleBar.Fullscreen” Alternatively, the following code can be executed inside the onCreate method of the activity class. requestWindowFeature(Window.FEATURE_NO_TITLE); getWindow().setFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN, WindowManager.LayoutParams.FLAG_FULLSCREEN); Replacing the Default View The Hello World project uses the overload of setContentView, which takes a resource identifier to the XML resource to inflate and set as the current activity view. This behavior must to be replaced with a view surface capable of rendering an OpenGL ES context. Android provides a Java class, GLSurfaceView, which encapsulates a rendering surface and EGL display. It's safe to remove the ``res/layout/main.xml'' file from the project because the view will be created programmatically. GLSurfaceView The two import methods for GLSurfaceView are setEGLContextClientVersion and setRenderer. The setEGLContextClientVersion method sets the ES version that should be used. For this project, OpenGL ES 2.0 is used. Rendering to the context is handled by a class instance that inherits from GLSurfaceView.Renderer. The renderer instance is set by calling the setRenderer method. The onPause and onResume methods of the Activity class must be overridden to call the respective methods on the GLSurfaceView class to pause/resume the rendering thread. Here is the complete DemoActivity class with a GLSurfaceView: public class DemoActivity extends Activity { private GLSurfaceView view; @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); view = new GLSurfaceView(); // Tell EGL to use a ES 2.0 Context view.setupEGLContextClientVersion(2); // Set the renderer view.setRenderer(new DemoProjectRenderer()); setContentView(view); } @Override protected void onPause() { super.onPause(); view.onPause(); } @Override protected void onResume() { super.onResume(); view.onResume(); } } GLSurfaceView.Renderer Classes inheriting from GLSurfaceView.Renderer are responsible for drawing to the OpenGL context. GLSurfaceView.Renderer exposes three abstract methods that must be implemented in the inherited classes: onSurfaceCreated, onSurfaceChanged, and onDrawFrame. When Android applications are paused or suspended, the OpenGL context and all the resources associated with it will be destroyed. Therefore, initialization and resource loading for the application should be handled in the onSurfaceCreated function. Note: A method, setPreserveEGLContextOnPause, exists that will tell EGL to try and preserve the context across pause/resume boundaries. However, be aware that this function relies on the hardware's support for multiple EGL contexts, so calling it will not guarantee the context will be preserved. The demo project will use the Java OpenGL ES API to clear the color buffer magenta. The GLSurfaceView.Renderer derived class for the demo project is listed below: class DemoProjectRenderer implements GLSurfaceView.Renderer { public void onDrawFrame(GL10 gl) { gl.glClear(gl.GL_COLOR_BUFFER_BIT); } public void onSurfaceChanged(GL10 gl, int width, int height) { gl.glViewport(0, 0, width, height); } public void onSurfaceCreated(GL10 gl, EGLConfig config) { gl.glClearColor(1.0f, 0.0f, 1.0f, 1.0f); } } AndroidManifest.xml The manifest file should specify the need for OpenGL ES 2 by adding the following XML element under the manifest element. <uses-feature android:glEsVersion=``0x00020000'' android:required=``true'' /> Moving to Native Rendering Rather than re-implementing portions of existing code to use the Java interfaces, it’s possible to leverage existing C/C++ code bases by calling native code from Java classes. Native code is compiled into a shared library and deployed with the APK file. All native code is placed under the jni folder inside the project root directory. JNI Basics Native code is exposed to Java classes via the JNI framework. For a more detailed account of the JNI framework, see http://docs.oracle.com/javase/7/docs/technotes/guides/jni/spec/jniTOC.html. The JNI naming convention must be followed for the loader to properly find native methods. All native methods: Must begin with the prefix Java_ Followed with the fully-qualified mangled class name Followed with the fully-qualified mangled method name For example, below is a simple native C function: void nativeLibraryFunction(int width, int height); To expose this method to Java code without requiring modifications to the existing function, include the JNI header and create a wrapper method that calls the existing native method. #include <jni.h> JNIEXPORT void JNICALL Java_demo_project_LibraryClass_LibraryFunctionWrapper(JNIEnv* env, jobject obj, jint width, jint height) { nativeLibraryFunction(width, height); } The above function assumes that the package for the project is demo.project and a Java class named LibraryClass will be used to expose the native function. The first parameter in all exported JNI functions is of type “JNIEnv*.” This parameter has several purposes, the most notable of which is allowing native code to make callbacks to Java code. This behavior will not be discussed here. For static functions, the jobject parameter is a reference to the calling Java class. The remaining parameter types are exposed from the JNI framework. For built-in C types the JNI framework handles the conversion automatically. For arrays and complex types consult the documentation. On the Java side, the shared library must be imported. This is achieved by a call to System.loadLibrary. This is typically done in a static initializer for the class. Additionally, the class must declare the interface for the exported methods. The methods are static and declared as native. class LibraryClass { static { System.loadLibrary("demo"); } public static native void LibraryFunctionWrapper(int width, int height); } The important part is that the LibraryClass name matches the name exposed by the exported native method. Also, the definition of the exported function needs to be marked as native. Here the library follows the convention for shared libraries on the target platform. For Unix-based (i.e., Android) targets, the library name is prefixed with “lib” and “.so” is appended. For Windows*-based targets, an extension of “dll” is appended. Note: For more complex function declarations, Java provides a utility, javah, that generates header definitions for native functions given a Java class file. Now the method can be called as a static Java method from anywhere in your code. For a concrete example DemoProject will now move to native rendering. Migrating the Initialization Code Create a C file inside the projects jni folder. Here the file will be “nativelibrary.c.” The file will use all native OpenGL ES calls. void InitializeOpenGL() { glClearColor(1.0f, 1.0f, 0.0f, 1.0f); } void resizeViewport(int newWidth, int newHeight) { glViewport(0, 0, newWidth, newHeight); } void renderFrame() { glClear(GL_COLOR_BUFFER_BIT); } Assume these methods already existed inside the project, or in a static library. To exercise these methods, JNI wrapper methods are created for each of the three methods. These methods go in another C file, jniExports.c. JNIEXPORT void JNICALL Java_demo_project_SomeClass_init(JNIEnv*) { InitializeOpenGL(); } JNIEXPORT void JNICALL Java_demo_project_SomeClass_resize(JNIEnv*, jint width, jint height) { resizeViewport(width, height); } JNIEXPORT void JNICALL Java_demo_project_SomeClass_render(JNIEnv*) { renderFrame(); } On the Java side the exported methods are declared in the library class. class LibraryClass { static { System.loadLibrary("demo"); } public static native void init(); public static native void resize(int width, int height); public static native void render(); } Now the GLSurfaceView.Renderer derived class is modified to call the native methods: class DemoProjectRenderer extends GLSurfaceView.Renderer { public void onDrawFrame(GL10 gl) { LibraryClass.render(); } public void onSurfaceChanged(GL10 gl, int width, int height) { LibraryClass.resize(width, height); } public void onSurfaceCreated(GL10 gl, EGLConfig config) { LibraryClass.init(); } } Configuring the Native Build Android.mk Inside the “jni” folder, a makefile, “Android.mk,” is created to instruct the NDK on how to compile native modules. An example Android.mk used in the particle sample project is listed below: # Tell Android where to locate source files # "my-dir" is a macro function which will return the path of the current directory(where Android.mk resides) LOCAL_PATH := $(call my-dir) # Clear contents of the LOCAL_* variables include $(CLEAR_VARS) # All the source files to include in this module LOCAL_SRC_FILES := nativeLibrary.c \ jniExports.c # The name of the module LOCAL_MODULE := libdemo # Compilation flags LOCAL_CFLAGS := -Werror # Static libraries to link with LOCAL_LDLIBS := -llog –landroid -lGLESv2 # Build the module include $(BUILD_SHARED_LIBRARY) Execute the ndk-build command from a command prompt to build the shared library. This step should be performed before building the Android application with ant. Reading Assets from the APK Accessing assets is one area of moving to native rendering that deserves special attention. The asset manager is passed from Java to the native code. For example, in the activities onCreate method, the following lines of code are added: @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); assetManager = getAssets(); LibraryClass.initializeAssetManager(assetManager); view = new GLSurfaceView(); // Tell EGL to use a ES 2.0 Context view.setupEGLContextClientVersion(2); // Set the renderer view.setRenderer(new DemoProjectRenderer()); setContentView(view); } JNIEXPORT void JNICALL Java_demo_project_LibraryClass_initializeAssetManager(JNIEnv* env, jobject obj, jobject assetManager) { AAssetManager* mgr = AAssetManager_fromJava(env, assetManager); } The AAssetManager_fromJava function is provided by the NDK to get a native pointer to an asset manager instance. Assets stored inside the APK are accessible via the AAssetManager API. For an example of reading files using the AAssetManager API, see util/file.c in the Particle Sample source code. Particle Sample Code The text above details a minimal set of steps for enabling a native OpenGL ES workload on Android platforms. For a more complete example, source code for a native OpenGL ES 2.0 particle system can be found at http://software.intel.com/en-us/articles/Android-code-samples. The portions of the source code relevant to the document will be mentioned below. ParticlesActivity.java This class houses the main application activity. It is responsible for creating the GLSurfaceView derived class, as well as passing the application’s asset manager to the native C code by calling createAssetManager. ParticlesView.java In the particle sample an explicit class is inherited from GLSurfaceView to set the OpenGL ES client version. Additionally, the surface render is implemented as an inner class. ParticlesLib.java This class imports the shared library and declares the three native methods used by the sample: init, step, and createAssetManager. Shared Library Code Code for the shared library used in the sample is found under the jni directory. The exported C function hooks are located in jni/gl_code.cpp. They expose the initialization, frame step, and asset creation functions. Implementations of these methods are found in jni/App.c. The particle system used in the sample can be found in jni/Particles.c and jni/Particles.h. The remaining files located in the sample are utility methods for math functions, simple geometry creation, and texture loading. This code is located under the jni/util directory. One file of particular note is a simple utility function for reading asset data, which can be found in util/file.c. About the Author Chris Kirkpatrick is a software applications engineer working in the Intel Software and Services Group supporting Intel graphics solutions in the Personal Form Factors division. He holds a B.Sc. in Computer Science from Oregon State University. Notices INFORMATION IN THIS DOCUMENT IS PROVIDED IN CONNECTION WITH INTEL PRODUCTS. NO LICENSE, EXPRESS OR IMPLIED, BY ESTOPPEL OR OTHERWISE, TO ANY INTELLECTUAL PROPERTY RIGHTS IS GRANTED BY THIS DOCUMENT. EXCEPT AS PROVIDED IN INTEL'S TERMS AND CONDITIONS OF SALE FOR SUCH PRODUCTS, INTEL ASSUMES NO LIABILITY WHATSOEVER AND INTEL DISCLAIMS ANY EXPRESS OR IMPLIED WARRANTY, RELATING TO SALE AND/OR USE OF INTEL PRODUCTS INCLUDING LIABILITY OR WARRANTIES RELATING TO FITNESS FOR A PARTICULAR PURPOSE, MERCHANTABILITY, OR INFRINGEMENT OF ANY PATENT, COPYRIGHT OR OTHER INTELLECTUAL PROPERTY RIGHT. UNLESS OTHERWISE AGREED IN WRITING BY INTEL, THE INTEL PRODUCTS ARE NOT DESIGNED NOR INTENDED FOR ANY APPLICATION IN WHICH THE FAILURE OF THE INTEL PRODUCT COULD CREATE A SITUATION WHERE PERSONAL INJURY OR DEATH MAY OCCUR. Intel may make changes to specifications and product descriptions at any time, without notice. Designers must not rely on the absence or characteristics of any features or instructions marked "reserved" or "undefined." Intel reserves these for future definition and shall have no responsibility whatsoever for conflicts or incompatibilities arising from future changes to them. The information here is subject to change without notice. Do not finalize a design with this information. The products described in this document may contain design defects or errors known as errata which may cause the product to deviate from published specifications. Current characterized errata are available on request. Contact your local Intel sales office or your distributor to obtain the latest specifications and before placing your product order. Copies of documents which have an order number and are referenced in this document, or other Intel literature, may be obtained by calling 1-800-548-4725, or go to: http://www.intel.com/design/literature.htm Software and workloads used in performance tests may have been optimized for performance only on Intel microprocessors. Performance tests, such as SYSmark and MobileMark, are measured using specific computer systems, components, software, operations, and functions. Any change to any of those factors may cause the results to vary. You should consult other information and performance tests to assist you in fully evaluating your contemplated purchases, including the performance of that product when combined with other products. Any software source code reprinted in this document is furnished under a software license and may only be used or copied in accordance with the terms of that license. Intel and the Intel logo are trademarks of Intel Corporation in the US and/or other countries. OpenGL is a registered trademark and the OpenGL ES logo is a trademark of Silicon Graphics Inc. used by permission by Khronos. Copyright © 2012 Intel Corporation. All rights reserved. *Other names and brands may be claimed as the property of others.