/* * Copyright (C) 2008 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package android.opengl; import java.io.Writer; import java.util.ArrayList; import javax.microedition.khronos.egl.EGL10; import javax.microedition.khronos.egl.EGL11; import javax.microedition.khronos.egl.EGLConfig; import javax.microedition.khronos.egl.EGLContext; import javax.microedition.khronos.egl.EGLDisplay; import javax.microedition.khronos.egl.EGLSurface; import javax.microedition.khronos.opengles.GL; import javax.microedition.khronos.opengles.GL10; import android.content.Context; import android.content.pm.ConfigurationInfo; import android.graphics.PixelFormat; import android.os.SystemProperties; import android.util.AttributeSet; import android.util.Log; import android.view.SurfaceHolder; import android.view.SurfaceView; /** * An implementation of SurfaceView that uses the dedicated surface for * displaying OpenGL rendering. *

* A GLSurfaceView provides the following features: *

*

* *

Using GLSurfaceView

*

* Typically you use GLSurfaceView by subclassing it and overriding one or more of the * View system input event methods. If your application does not need to override event * methods then GLSurfaceView can be used as-is. For the most part * GLSurfaceView behavior is customized by calling "set" methods rather than by subclassing. * For example, unlike a regular View, drawing is delegated to a separate Renderer object which * is registered with the GLSurfaceView * using the {@link #setRenderer(Renderer)} call. *

*

Initializing GLSurfaceView

* All you have to do to initialize a GLSurfaceView is call {@link #setRenderer(Renderer)}. * However, if desired, you can modify the default behavior of GLSurfaceView by calling one or * more of these methods before calling setRenderer: * *

*

Specifying the android.view.Surface

* By default GLSurfaceView will create a PixelFormat.RGB_565 format surface. If a translucent * surface is required, call getHolder().setFormat(PixelFormat.TRANSLUCENT). * The exact format of a TRANSLUCENT surface is device dependent, but it will be * a 32-bit-per-pixel surface with 8 bits per component. *

*

Choosing an EGL Configuration

* A given Android device may support multiple EGLConfig rendering configurations. * The available configurations may differ in how may channels of data are present, as * well as how many bits are allocated to each channel. Therefore, the first thing * GLSurfaceView has to do when starting to render is choose what EGLConfig to use. *

* By default GLSurfaceView chooses a EGLConfig that has an RGB_656 pixel format, * with at least a 16-bit depth buffer and no stencil. *

* If you would prefer a different EGLConfig * you can override the default behavior by calling one of the * setEGLConfigChooser methods. *

*

Debug Behavior

* You can optionally modify the behavior of GLSurfaceView by calling * one or more of the debugging methods {@link #setDebugFlags(int)}, * and {@link #setGLWrapper}. These methods may be called before and/or after setRenderer, but * typically they are called before setRenderer so that they take effect immediately. *

*

Setting a Renderer

* Finally, you must call {@link #setRenderer} to register a {@link Renderer}. * The renderer is * responsible for doing the actual OpenGL rendering. *

*

Rendering Mode

* Once the renderer is set, you can control whether the renderer draws * continuously or on-demand by calling * {@link #setRenderMode}. The default is continuous rendering. *

*

Activity Life-cycle

* A GLSurfaceView must be notified when the activity is paused and resumed. GLSurfaceView clients * are required to call {@link #onPause()} when the activity pauses and * {@link #onResume()} when the activity resumes. These calls allow GLSurfaceView to * pause and resume the rendering thread, and also allow GLSurfaceView to release and recreate * the OpenGL display. *

*

Handling events

*

* To handle an event you will typically subclass GLSurfaceView and override the * appropriate method, just as you would with any other View. However, when handling * the event, you may need to communicate with the Renderer object * that's running in the rendering thread. You can do this using any * standard Java cross-thread communication mechanism. In addition, * one relatively easy way to communicate with your renderer is * to call * {@link #queueEvent(Runnable)}. For example: *

 * class MyGLSurfaceView extends GLSurfaceView {
 *
 *     private MyRenderer mMyRenderer;
 *
 *     public void start() {
 *         mMyRenderer = ...;
 *         setRenderer(mMyRenderer);
 *     }
 *
 *     public boolean onKeyDown(int keyCode, KeyEvent event) {
 *         if (keyCode == KeyEvent.KEYCODE_DPAD_CENTER) {
 *             queueEvent(new Runnable() {
 *                 // This method will be called on the rendering
 *                 // thread:
 *                 public void run() {
 *                     mMyRenderer.handleDpadCenter();
 *                 }});
 *             return true;
 *         }
 *         return super.onKeyDown(keyCode, event);
 *     }
 * }
 * 
* */ public class GLSurfaceView extends SurfaceView implements SurfaceHolder.Callback { private final static boolean LOG_THREADS = false; private final static boolean LOG_PAUSE_RESUME = false; private final static boolean LOG_SURFACE = false; private final static boolean LOG_RENDERER = false; private final static boolean LOG_RENDERER_DRAW_FRAME = false; private final static boolean LOG_EGL = false; // Work-around for bug 2263168 private final static boolean DRAW_TWICE_AFTER_SIZE_CHANGED = true; /** * The renderer only renders * when the surface is created, or when {@link #requestRender} is called. * * @see #getRenderMode() * @see #setRenderMode(int) * @see #requestRender() */ public final static int RENDERMODE_WHEN_DIRTY = 0; /** * The renderer is called * continuously to re-render the scene. * * @see #getRenderMode() * @see #setRenderMode(int) */ public final static int RENDERMODE_CONTINUOUSLY = 1; /** * Check glError() after every GL call and throw an exception if glError indicates * that an error has occurred. This can be used to help track down which OpenGL ES call * is causing an error. * * @see #getDebugFlags * @see #setDebugFlags */ public final static int DEBUG_CHECK_GL_ERROR = 1; /** * Log GL calls to the system log at "verbose" level with tag "GLSurfaceView". * * @see #getDebugFlags * @see #setDebugFlags */ public final static int DEBUG_LOG_GL_CALLS = 2; /** * Standard View constructor. In order to render something, you * must call {@link #setRenderer} to register a renderer. */ public GLSurfaceView(Context context) { super(context); init(); } /** * Standard View constructor. In order to render something, you * must call {@link #setRenderer} to register a renderer. */ public GLSurfaceView(Context context, AttributeSet attrs) { super(context, attrs); init(); } private void init() { // Install a SurfaceHolder.Callback so we get notified when the // underlying surface is created and destroyed SurfaceHolder holder = getHolder(); holder.addCallback(this); // setFormat is done by SurfaceView in SDK 2.3 and newer. Uncomment // this statement if back-porting to 2.2 or older: // holder.setFormat(PixelFormat.RGB_565); // // setType is not needed for SDK 2.0 or newer. Uncomment this // statement if back-porting this code to older SDKs. // holder.setType(SurfaceHolder.SURFACE_TYPE_GPU); } /** * Set the glWrapper. If the glWrapper is not null, its * {@link GLWrapper#wrap(GL)} method is called * whenever a surface is created. A GLWrapper can be used to wrap * the GL object that's passed to the renderer. Wrapping a GL * object enables examining and modifying the behavior of the * GL calls made by the renderer. *

* Wrapping is typically used for debugging purposes. *

* The default value is null. * @param glWrapper the new GLWrapper */ public void setGLWrapper(GLWrapper glWrapper) { mGLWrapper = glWrapper; } /** * Set the debug flags to a new value. The value is * constructed by OR-together zero or more * of the DEBUG_CHECK_* constants. The debug flags take effect * whenever a surface is created. The default value is zero. * @param debugFlags the new debug flags * @see #DEBUG_CHECK_GL_ERROR * @see #DEBUG_LOG_GL_CALLS */ public void setDebugFlags(int debugFlags) { mDebugFlags = debugFlags; } /** * Get the current value of the debug flags. * @return the current value of the debug flags. */ public int getDebugFlags() { return mDebugFlags; } /** * Set the renderer associated with this view. Also starts the thread that * will call the renderer, which in turn causes the rendering to start. *

This method should be called once and only once in the life-cycle of * a GLSurfaceView. *

The following GLSurfaceView methods can only be called before * setRenderer is called: *

*

* The following GLSurfaceView methods can only be called after * setRenderer is called: *

* * @param renderer the renderer to use to perform OpenGL drawing. */ public void setRenderer(Renderer renderer) { checkRenderThreadState(); if (mEGLConfigChooser == null) { mEGLConfigChooser = new SimpleEGLConfigChooser(true); } if (mEGLContextFactory == null) { mEGLContextFactory = new DefaultContextFactory(); } if (mEGLWindowSurfaceFactory == null) { mEGLWindowSurfaceFactory = new DefaultWindowSurfaceFactory(); } mGLThread = new GLThread(renderer); mGLThread.start(); } /** * Install a custom EGLContextFactory. *

If this method is * called, it must be called before {@link #setRenderer(Renderer)} * is called. *

* If this method is not called, then by default * a context will be created with no shared context and * with a null attribute list. */ public void setEGLContextFactory(EGLContextFactory factory) { checkRenderThreadState(); mEGLContextFactory = factory; } /** * Install a custom EGLWindowSurfaceFactory. *

If this method is * called, it must be called before {@link #setRenderer(Renderer)} * is called. *

* If this method is not called, then by default * a window surface will be created with a null attribute list. */ public void setEGLWindowSurfaceFactory(EGLWindowSurfaceFactory factory) { checkRenderThreadState(); mEGLWindowSurfaceFactory = factory; } /** * Install a custom EGLConfigChooser. *

If this method is * called, it must be called before {@link #setRenderer(Renderer)} * is called. *

* If no setEGLConfigChooser method is called, then by default the * view will choose an EGLConfig that is compatible with the current * android.view.Surface, with a depth buffer depth of * at least 16 bits. * @param configChooser */ public void setEGLConfigChooser(EGLConfigChooser configChooser) { checkRenderThreadState(); mEGLConfigChooser = configChooser; } /** * Install a config chooser which will choose a config * as close to 16-bit RGB as possible, with or without an optional depth * buffer as close to 16-bits as possible. *

If this method is * called, it must be called before {@link #setRenderer(Renderer)} * is called. *

* If no setEGLConfigChooser method is called, then by default the * view will choose an RGB_565 surface with a depth buffer depth of * at least 16 bits. * * @param needDepth */ public void setEGLConfigChooser(boolean needDepth) { setEGLConfigChooser(new SimpleEGLConfigChooser(needDepth)); } /** * Install a config chooser which will choose a config * with at least the specified depthSize and stencilSize, * and exactly the specified redSize, greenSize, blueSize and alphaSize. *

If this method is * called, it must be called before {@link #setRenderer(Renderer)} * is called. *

* If no setEGLConfigChooser method is called, then by default the * view will choose an RGB_565 surface with a depth buffer depth of * at least 16 bits. * */ public void setEGLConfigChooser(int redSize, int greenSize, int blueSize, int alphaSize, int depthSize, int stencilSize) { setEGLConfigChooser(new ComponentSizeChooser(redSize, greenSize, blueSize, alphaSize, depthSize, stencilSize)); } /** * Inform the default EGLContextFactory and default EGLConfigChooser * which EGLContext client version to pick. *

Use this method to create an OpenGL ES 2.0-compatible context. * Example: *

     *     public MyView(Context context) {
     *         super(context);
     *         setEGLContextClientVersion(2); // Pick an OpenGL ES 2.0 context.
     *         setRenderer(new MyRenderer());
     *     }
     * 
*

Note: Activities which require OpenGL ES 2.0 should indicate this by * setting @lt;uses-feature android:glEsVersion="0x00020000" /> in the activity's * AndroidManifest.xml file. *

If this method is called, it must be called before {@link #setRenderer(Renderer)} * is called. *

This method only affects the behavior of the default EGLContexFactory and the * default EGLConfigChooser. If * {@link #setEGLContextFactory(EGLContextFactory)} has been called, then the supplied * EGLContextFactory is responsible for creating an OpenGL ES 2.0-compatible context. * If * {@link #setEGLConfigChooser(EGLConfigChooser)} has been called, then the supplied * EGLConfigChooser is responsible for choosing an OpenGL ES 2.0-compatible config. * @param version The EGLContext client version to choose. Use 2 for OpenGL ES 2.0 */ public void setEGLContextClientVersion(int version) { checkRenderThreadState(); mEGLContextClientVersion = version; } /** * Set the rendering mode. When renderMode is * RENDERMODE_CONTINUOUSLY, the renderer is called * repeatedly to re-render the scene. When renderMode * is RENDERMODE_WHEN_DIRTY, the renderer only rendered when the surface * is created, or when {@link #requestRender} is called. Defaults to RENDERMODE_CONTINUOUSLY. *

* Using RENDERMODE_WHEN_DIRTY can improve battery life and overall system performance * by allowing the GPU and CPU to idle when the view does not need to be updated. *

* This method can only be called after {@link #setRenderer(Renderer)} * * @param renderMode one of the RENDERMODE_X constants * @see #RENDERMODE_CONTINUOUSLY * @see #RENDERMODE_WHEN_DIRTY */ public void setRenderMode(int renderMode) { mGLThread.setRenderMode(renderMode); } /** * Get the current rendering mode. May be called * from any thread. Must not be called before a renderer has been set. * @return the current rendering mode. * @see #RENDERMODE_CONTINUOUSLY * @see #RENDERMODE_WHEN_DIRTY */ public int getRenderMode() { return mGLThread.getRenderMode(); } /** * Request that the renderer render a frame. * This method is typically used when the render mode has been set to * {@link #RENDERMODE_WHEN_DIRTY}, so that frames are only rendered on demand. * May be called * from any thread. Must not be called before a renderer has been set. */ public void requestRender() { mGLThread.requestRender(); } /** * This method is part of the SurfaceHolder.Callback interface, and is * not normally called or subclassed by clients of GLSurfaceView. */ public void surfaceCreated(SurfaceHolder holder) { mGLThread.surfaceCreated(); } /** * This method is part of the SurfaceHolder.Callback interface, and is * not normally called or subclassed by clients of GLSurfaceView. */ public void surfaceDestroyed(SurfaceHolder holder) { // Surface will be destroyed when we return mGLThread.surfaceDestroyed(); } /** * This method is part of the SurfaceHolder.Callback interface, and is * not normally called or subclassed by clients of GLSurfaceView. */ public void surfaceChanged(SurfaceHolder holder, int format, int w, int h) { mGLThread.onWindowResize(w, h); } /** * Inform the view that the activity is paused. The owner of this view must * call this method when the activity is paused. Calling this method will * pause the rendering thread. * Must not be called before a renderer has been set. */ public void onPause() { mGLThread.onPause(); } /** * Inform the view that the activity is resumed. The owner of this view must * call this method when the activity is resumed. Calling this method will * recreate the OpenGL display and resume the rendering * thread. * Must not be called before a renderer has been set. */ public void onResume() { mGLThread.onResume(); } /** * Queue a runnable to be run on the GL rendering thread. This can be used * to communicate with the Renderer on the rendering thread. * Must not be called before a renderer has been set. * @param r the runnable to be run on the GL rendering thread. */ public void queueEvent(Runnable r) { mGLThread.queueEvent(r); } /** * This method is used as part of the View class and is not normally * called or subclassed by clients of GLSurfaceView. * Must not be called before a renderer has been set. */ @Override protected void onDetachedFromWindow() { super.onDetachedFromWindow(); mGLThread.requestExitAndWait(); } // ---------------------------------------------------------------------- /** * An interface used to wrap a GL interface. *

Typically * used for implementing debugging and tracing on top of the default * GL interface. You would typically use this by creating your own class * that implemented all the GL methods by delegating to another GL instance. * Then you could add your own behavior before or after calling the * delegate. All the GLWrapper would do was instantiate and return the * wrapper GL instance: *

     * class MyGLWrapper implements GLWrapper {
     *     GL wrap(GL gl) {
     *         return new MyGLImplementation(gl);
     *     }
     *     static class MyGLImplementation implements GL,GL10,GL11,... {
     *         ...
     *     }
     * }
     * 
* @see #setGLWrapper(GLWrapper) */ public interface GLWrapper { /** * Wraps a gl interface in another gl interface. * @param gl a GL interface that is to be wrapped. * @return either the input argument or another GL object that wraps the input argument. */ GL wrap(GL gl); } /** * A generic renderer interface. *

* The renderer is responsible for making OpenGL calls to render a frame. *

* GLSurfaceView clients typically create their own classes that implement * this interface, and then call {@link GLSurfaceView#setRenderer} to * register the renderer with the GLSurfaceView. *

*

Threading

* The renderer will be called on a separate thread, so that rendering * performance is decoupled from the UI thread. Clients typically need to * communicate with the renderer from the UI thread, because that's where * input events are received. Clients can communicate using any of the * standard Java techniques for cross-thread communication, or they can * use the {@link GLSurfaceView#queueEvent(Runnable)} convenience method. *

*

EGL Context Lost

* There are situations where the EGL rendering context will be lost. This * typically happens when device wakes up after going to sleep. When * the EGL context is lost, all OpenGL resources (such as textures) that are * associated with that context will be automatically deleted. In order to * keep rendering correctly, a renderer must recreate any lost resources * that it still needs. The {@link #onSurfaceCreated(GL10, EGLConfig)} method * is a convenient place to do this. * * * @see #setRenderer(Renderer) */ public interface Renderer { /** * Called when the surface is created or recreated. *

* Called when the rendering thread * starts and whenever the EGL context is lost. The EGL context will typically * be lost when the Android device awakes after going to sleep. *

* Since this method is called at the beginning of rendering, as well as * every time the EGL context is lost, this method is a convenient place to put * code to create resources that need to be created when the rendering * starts, and that need to be recreated when the EGL context is lost. * Textures are an example of a resource that you might want to create * here. *

* Note that when the EGL context is lost, all OpenGL resources associated * with that context will be automatically deleted. You do not need to call * the corresponding "glDelete" methods such as glDeleteTextures to * manually delete these lost resources. *

* @param gl the GL interface. Use instanceof to * test if the interface supports GL11 or higher interfaces. * @param config the EGLConfig of the created surface. Can be used * to create matching pbuffers. */ void onSurfaceCreated(GL10 gl, EGLConfig config); /** * Called when the surface changed size. *

* Called after the surface is created and whenever * the OpenGL ES surface size changes. *

* Typically you will set your viewport here. If your camera * is fixed then you could also set your projection matrix here: *

         * void onSurfaceChanged(GL10 gl, int width, int height) {
         *     gl.glViewport(0, 0, width, height);
         *     // for a fixed camera, set the projection too
         *     float ratio = (float) width / height;
         *     gl.glMatrixMode(GL10.GL_PROJECTION);
         *     gl.glLoadIdentity();
         *     gl.glFrustumf(-ratio, ratio, -1, 1, 1, 10);
         * }
         * 
* @param gl the GL interface. Use instanceof to * test if the interface supports GL11 or higher interfaces. * @param width * @param height */ void onSurfaceChanged(GL10 gl, int width, int height); /** * Called to draw the current frame. *

* This method is responsible for drawing the current frame. *

* The implementation of this method typically looks like this: *

         * void onDrawFrame(GL10 gl) {
         *     gl.glClear(GL10.GL_COLOR_BUFFER_BIT | GL10.GL_DEPTH_BUFFER_BIT);
         *     //... other gl calls to render the scene ...
         * }
         * 
* @param gl the GL interface. Use instanceof to * test if the interface supports GL11 or higher interfaces. */ void onDrawFrame(GL10 gl); } /** * An interface for customizing the eglCreateContext and eglDestroyContext calls. *

* This interface must be implemented by clients wishing to call * {@link GLSurfaceView#setEGLContextFactory(EGLContextFactory)} */ public interface EGLContextFactory { EGLContext createContext(EGL10 egl, EGLDisplay display, EGLConfig eglConfig); void destroyContext(EGL10 egl, EGLDisplay display, EGLContext context); } private class DefaultContextFactory implements EGLContextFactory { private int EGL_CONTEXT_CLIENT_VERSION = 0x3098; public EGLContext createContext(EGL10 egl, EGLDisplay display, EGLConfig config) { int[] attrib_list = {EGL_CONTEXT_CLIENT_VERSION, mEGLContextClientVersion, EGL10.EGL_NONE }; return egl.eglCreateContext(display, config, EGL10.EGL_NO_CONTEXT, mEGLContextClientVersion != 0 ? attrib_list : null); } public void destroyContext(EGL10 egl, EGLDisplay display, EGLContext context) { if (!egl.eglDestroyContext(display, context)) { Log.e("DefaultContextFactory", "display:" + display + " context: " + context); if (LOG_THREADS) { Log.i("DefaultContextFactory", "tid=" + Thread.currentThread().getId()); } throw new RuntimeException("eglDestroyContext failed: " + EGLLogWrapper.getErrorString(egl.eglGetError())); } } } /** * An interface for customizing the eglCreateWindowSurface and eglDestroySurface calls. *

* This interface must be implemented by clients wishing to call * {@link GLSurfaceView#setEGLWindowSurfaceFactory(EGLWindowSurfaceFactory)} */ public interface EGLWindowSurfaceFactory { EGLSurface createWindowSurface(EGL10 egl, EGLDisplay display, EGLConfig config, Object nativeWindow); void destroySurface(EGL10 egl, EGLDisplay display, EGLSurface surface); } private static class DefaultWindowSurfaceFactory implements EGLWindowSurfaceFactory { public EGLSurface createWindowSurface(EGL10 egl, EGLDisplay display, EGLConfig config, Object nativeWindow) { return egl.eglCreateWindowSurface(display, config, nativeWindow, null); } public void destroySurface(EGL10 egl, EGLDisplay display, EGLSurface surface) { egl.eglDestroySurface(display, surface); } } /** * An interface for choosing an EGLConfig configuration from a list of * potential configurations. *

* This interface must be implemented by clients wishing to call * {@link GLSurfaceView#setEGLConfigChooser(EGLConfigChooser)} */ public interface EGLConfigChooser { /** * Choose a configuration from the list. Implementors typically * implement this method by calling * {@link EGL10#eglChooseConfig} and iterating through the results. Please consult the * EGL specification available from The Khronos Group to learn how to call eglChooseConfig. * @param egl the EGL10 for the current display. * @param display the current display. * @return the chosen configuration. */ EGLConfig chooseConfig(EGL10 egl, EGLDisplay display); } private abstract class BaseConfigChooser implements EGLConfigChooser { public BaseConfigChooser(int[] configSpec) { mConfigSpec = filterConfigSpec(configSpec); } public EGLConfig chooseConfig(EGL10 egl, EGLDisplay display) { int[] num_config = new int[1]; if (!egl.eglChooseConfig(display, mConfigSpec, null, 0, num_config)) { throw new IllegalArgumentException("eglChooseConfig failed"); } int numConfigs = num_config[0]; if (numConfigs <= 0) { throw new IllegalArgumentException( "No configs match configSpec"); } EGLConfig[] configs = new EGLConfig[numConfigs]; if (!egl.eglChooseConfig(display, mConfigSpec, configs, numConfigs, num_config)) { throw new IllegalArgumentException("eglChooseConfig#2 failed"); } EGLConfig config = chooseConfig(egl, display, configs); if (config == null) { throw new IllegalArgumentException("No config chosen"); } return config; } abstract EGLConfig chooseConfig(EGL10 egl, EGLDisplay display, EGLConfig[] configs); protected int[] mConfigSpec; private int[] filterConfigSpec(int[] configSpec) { if (mEGLContextClientVersion != 2) { return configSpec; } /* We know none of the subclasses define EGL_RENDERABLE_TYPE. * And we know the configSpec is well formed. */ int len = configSpec.length; int[] newConfigSpec = new int[len + 2]; System.arraycopy(configSpec, 0, newConfigSpec, 0, len-1); newConfigSpec[len-1] = EGL10.EGL_RENDERABLE_TYPE; newConfigSpec[len] = 4; /* EGL_OPENGL_ES2_BIT */ newConfigSpec[len+1] = EGL10.EGL_NONE; return newConfigSpec; } } /** * Choose a configuration with exactly the specified r,g,b,a sizes, * and at least the specified depth and stencil sizes. */ private class ComponentSizeChooser extends BaseConfigChooser { public ComponentSizeChooser(int redSize, int greenSize, int blueSize, int alphaSize, int depthSize, int stencilSize) { super(new int[] { EGL10.EGL_RED_SIZE, redSize, EGL10.EGL_GREEN_SIZE, greenSize, EGL10.EGL_BLUE_SIZE, blueSize, EGL10.EGL_ALPHA_SIZE, alphaSize, EGL10.EGL_DEPTH_SIZE, depthSize, EGL10.EGL_STENCIL_SIZE, stencilSize, EGL10.EGL_NONE}); mValue = new int[1]; mRedSize = redSize; mGreenSize = greenSize; mBlueSize = blueSize; mAlphaSize = alphaSize; mDepthSize = depthSize; mStencilSize = stencilSize; } @Override public EGLConfig chooseConfig(EGL10 egl, EGLDisplay display, EGLConfig[] configs) { for (EGLConfig config : configs) { int d = findConfigAttrib(egl, display, config, EGL10.EGL_DEPTH_SIZE, 0); int s = findConfigAttrib(egl, display, config, EGL10.EGL_STENCIL_SIZE, 0); if ((d >= mDepthSize) && (s >= mStencilSize)) { int r = findConfigAttrib(egl, display, config, EGL10.EGL_RED_SIZE, 0); int g = findConfigAttrib(egl, display, config, EGL10.EGL_GREEN_SIZE, 0); int b = findConfigAttrib(egl, display, config, EGL10.EGL_BLUE_SIZE, 0); int a = findConfigAttrib(egl, display, config, EGL10.EGL_ALPHA_SIZE, 0); if ((r == mRedSize) && (g == mGreenSize) && (b == mBlueSize) && (a == mAlphaSize)) { return config; } } } return null; } private int findConfigAttrib(EGL10 egl, EGLDisplay display, EGLConfig config, int attribute, int defaultValue) { if (egl.eglGetConfigAttrib(display, config, attribute, mValue)) { return mValue[0]; } return defaultValue; } private int[] mValue; // Subclasses can adjust these values: protected int mRedSize; protected int mGreenSize; protected int mBlueSize; protected int mAlphaSize; protected int mDepthSize; protected int mStencilSize; } /** * This class will choose a RGB_565 surface with * or without a depth buffer. * */ private class SimpleEGLConfigChooser extends ComponentSizeChooser { public SimpleEGLConfigChooser(boolean withDepthBuffer) { super(5, 6, 5, 0, withDepthBuffer ? 16 : 0, 0); } } /** * An EGL helper class. */ private class EglHelper { public EglHelper() { } /** * Initialize EGL for a given configuration spec. * @param configSpec */ public void start() { if (LOG_EGL) { Log.w("EglHelper", "start() tid=" + Thread.currentThread().getId()); } /* * Get an EGL instance */ mEgl = (EGL10) EGLContext.getEGL(); /* * Get to the default display. */ mEglDisplay = mEgl.eglGetDisplay(EGL10.EGL_DEFAULT_DISPLAY); if (mEglDisplay == EGL10.EGL_NO_DISPLAY) { throw new RuntimeException("eglGetDisplay failed"); } /* * We can now initialize EGL for that display */ int[] version = new int[2]; if(!mEgl.eglInitialize(mEglDisplay, version)) { throw new RuntimeException("eglInitialize failed"); } mEglConfig = mEGLConfigChooser.chooseConfig(mEgl, mEglDisplay); /* * Create an EGL context. We want to do this as rarely as we can, because an * EGL context is a somewhat heavy object. */ mEglContext = mEGLContextFactory.createContext(mEgl, mEglDisplay, mEglConfig); if (mEglContext == null || mEglContext == EGL10.EGL_NO_CONTEXT) { mEglContext = null; throwEglException("createContext"); } if (LOG_EGL) { Log.w("EglHelper", "createContext " + mEglContext + " tid=" + Thread.currentThread().getId()); } mEglSurface = null; } /* * React to the creation of a new surface by creating and returning an * OpenGL interface that renders to that surface. */ public GL createSurface(SurfaceHolder holder) { if (LOG_EGL) { Log.w("EglHelper", "createSurface() tid=" + Thread.currentThread().getId()); } /* * Check preconditions. */ if (mEgl == null) { throw new RuntimeException("egl not initialized"); } if (mEglDisplay == null) { throw new RuntimeException("eglDisplay not initialized"); } if (mEglConfig == null) { throw new RuntimeException("mEglConfig not initialized"); } /* * The window size has changed, so we need to create a new * surface. */ if (mEglSurface != null && mEglSurface != EGL10.EGL_NO_SURFACE) { /* * Unbind and destroy the old EGL surface, if * there is one. */ mEgl.eglMakeCurrent(mEglDisplay, EGL10.EGL_NO_SURFACE, EGL10.EGL_NO_SURFACE, EGL10.EGL_NO_CONTEXT); mEGLWindowSurfaceFactory.destroySurface(mEgl, mEglDisplay, mEglSurface); } /* * Create an EGL surface we can render into. */ mEglSurface = mEGLWindowSurfaceFactory.createWindowSurface(mEgl, mEglDisplay, mEglConfig, holder); if (mEglSurface == null || mEglSurface == EGL10.EGL_NO_SURFACE) { int error = mEgl.eglGetError(); if (error == EGL10.EGL_BAD_NATIVE_WINDOW) { Log.e("EglHelper", "createWindowSurface returned EGL_BAD_NATIVE_WINDOW."); return null; } throwEglException("createWindowSurface", error); } /* * Before we can issue GL commands, we need to make sure * the context is current and bound to a surface. */ if (!mEgl.eglMakeCurrent(mEglDisplay, mEglSurface, mEglSurface, mEglContext)) { throwEglException("eglMakeCurrent"); } GL gl = mEglContext.getGL(); if (mGLWrapper != null) { gl = mGLWrapper.wrap(gl); } if ((mDebugFlags & (DEBUG_CHECK_GL_ERROR | DEBUG_LOG_GL_CALLS)) != 0) { int configFlags = 0; Writer log = null; if ((mDebugFlags & DEBUG_CHECK_GL_ERROR) != 0) { configFlags |= GLDebugHelper.CONFIG_CHECK_GL_ERROR; } if ((mDebugFlags & DEBUG_LOG_GL_CALLS) != 0) { log = new LogWriter(); } gl = GLDebugHelper.wrap(gl, configFlags, log); } return gl; } /** * Display the current render surface. * @return false if the context has been lost. */ public boolean swap() { if (! mEgl.eglSwapBuffers(mEglDisplay, mEglSurface)) { /* * Check for EGL_CONTEXT_LOST, which means the context * and all associated data were lost (For instance because * the device went to sleep). We need to sleep until we * get a new surface. */ int error = mEgl.eglGetError(); switch(error) { case EGL11.EGL_CONTEXT_LOST: return false; case EGL10.EGL_BAD_NATIVE_WINDOW: // The native window is bad, probably because the // window manager has closed it. Ignore this error, // on the expectation that the application will be closed soon. Log.e("EglHelper", "eglSwapBuffers returned EGL_BAD_NATIVE_WINDOW. tid=" + Thread.currentThread().getId()); break; default: throwEglException("eglSwapBuffers", error); } } return true; } public void destroySurface() { if (LOG_EGL) { Log.w("EglHelper", "destroySurface() tid=" + Thread.currentThread().getId()); } if (mEglSurface != null && mEglSurface != EGL10.EGL_NO_SURFACE) { mEgl.eglMakeCurrent(mEglDisplay, EGL10.EGL_NO_SURFACE, EGL10.EGL_NO_SURFACE, EGL10.EGL_NO_CONTEXT); mEGLWindowSurfaceFactory.destroySurface(mEgl, mEglDisplay, mEglSurface); mEglSurface = null; } } public void finish() { if (LOG_EGL) { Log.w("EglHelper", "finish() tid=" + Thread.currentThread().getId()); } if (mEglContext != null) { mEGLContextFactory.destroyContext(mEgl, mEglDisplay, mEglContext); mEglContext = null; } if (mEglDisplay != null) { mEgl.eglTerminate(mEglDisplay); mEglDisplay = null; } } private void throwEglException(String function) { throwEglException(function, mEgl.eglGetError()); } private void throwEglException(String function, int error) { String message = function + " failed: " + EGLLogWrapper.getErrorString(error); if (LOG_THREADS) { Log.e("EglHelper", "throwEglException tid=" + Thread.currentThread().getId() + " " + message); } throw new RuntimeException(message); } EGL10 mEgl; EGLDisplay mEglDisplay; EGLSurface mEglSurface; EGLConfig mEglConfig; EGLContext mEglContext; } /** * A generic GL Thread. Takes care of initializing EGL and GL. Delegates * to a Renderer instance to do the actual drawing. Can be configured to * render continuously or on request. * * All potentially blocking synchronization is done through the * sGLThreadManager object. This avoids multiple-lock ordering issues. * */ class GLThread extends Thread { GLThread(Renderer renderer) { super(); mWidth = 0; mHeight = 0; mRequestRender = true; mRenderMode = RENDERMODE_CONTINUOUSLY; mRenderer = renderer; } @Override public void run() { setName("GLThread " + getId()); if (LOG_THREADS) { Log.i("GLThread", "starting tid=" + getId()); } try { guardedRun(); } catch (InterruptedException e) { // fall thru and exit normally } finally { sGLThreadManager.threadExiting(this); } } /* * This private method should only be called inside a * synchronized(sGLThreadManager) block. */ private void stopEglSurfaceLocked() { if (mHaveEglSurface) { mHaveEglSurface = false; mEglHelper.destroySurface(); } } /* * This private method should only be called inside a * synchronized(sGLThreadManager) block. */ private void stopEglContextLocked() { if (mHaveEglContext) { mEglHelper.finish(); mHaveEglContext = false; sGLThreadManager.releaseEglContextLocked(this); } } private void guardedRun() throws InterruptedException { mEglHelper = new EglHelper(); mHaveEglContext = false; mHaveEglSurface = false; try { GL10 gl = null; boolean createEglContext = false; boolean createEglSurface = false; boolean lostEglContext = false; boolean sizeChanged = false; boolean wantRenderNotification = false; boolean doRenderNotification = false; boolean askedToReleaseEglContext = false; int w = 0; int h = 0; Runnable event = null; while (true) { synchronized (sGLThreadManager) { while (true) { if (mShouldExit) { return; } if (! mEventQueue.isEmpty()) { event = mEventQueue.remove(0); break; } // Update the pause state. if (mPaused != mRequestPaused) { mPaused = mRequestPaused; sGLThreadManager.notifyAll(); if (LOG_PAUSE_RESUME) { Log.i("GLThread", "mPaused is now " + mPaused + " tid=" + getId()); } } // Do we need to give up the EGL context? if (mShouldReleaseEglContext) { if (LOG_SURFACE) { Log.i("GLThread", "releasing EGL context because asked to tid=" + getId()); } stopEglSurfaceLocked(); stopEglContextLocked(); mShouldReleaseEglContext = false; askedToReleaseEglContext = true; } // Have we lost the EGL context? if (lostEglContext) { stopEglSurfaceLocked(); stopEglContextLocked(); lostEglContext = false; } // Do we need to release the EGL surface? if (mHaveEglSurface && mPaused) { if (LOG_SURFACE) { Log.i("GLThread", "releasing EGL surface because paused tid=" + getId()); } stopEglSurfaceLocked(); if (sGLThreadManager.shouldReleaseEGLContextWhenPausing()) { stopEglContextLocked(); if (LOG_SURFACE) { Log.i("GLThread", "releasing EGL context because paused tid=" + getId()); } } if (sGLThreadManager.shouldTerminateEGLWhenPausing()) { mEglHelper.finish(); if (LOG_SURFACE) { Log.i("GLThread", "terminating EGL because paused tid=" + getId()); } } } // Have we lost the surface view surface? if ((! mHasSurface) && (! mWaitingForSurface)) { if (LOG_SURFACE) { Log.i("GLThread", "noticed surfaceView surface lost tid=" + getId()); } if (mHaveEglSurface) { stopEglSurfaceLocked(); } mWaitingForSurface = true; sGLThreadManager.notifyAll(); } // Have we acquired the surface view surface? if (mHasSurface && mWaitingForSurface) { if (LOG_SURFACE) { Log.i("GLThread", "noticed surfaceView surface acquired tid=" + getId()); } mWaitingForSurface = false; sGLThreadManager.notifyAll(); } if (doRenderNotification) { if (LOG_SURFACE) { Log.i("GLThread", "sending render notification tid=" + getId()); } wantRenderNotification = false; doRenderNotification = false; mRenderComplete = true; sGLThreadManager.notifyAll(); } // Ready to draw? if (readyToDraw()) { // If we don't have an EGL context, try to acquire one. if (! mHaveEglContext) { if (askedToReleaseEglContext) { askedToReleaseEglContext = false; } else if (sGLThreadManager.tryAcquireEglContextLocked(this)) { try { mEglHelper.start(); } catch (RuntimeException t) { sGLThreadManager.releaseEglContextLocked(this); throw t; } mHaveEglContext = true; createEglContext = true; sGLThreadManager.notifyAll(); } } if (mHaveEglContext && !mHaveEglSurface) { mHaveEglSurface = true; createEglSurface = true; sizeChanged = true; } if (mHaveEglSurface) { if (mSizeChanged) { sizeChanged = true; w = mWidth; h = mHeight; wantRenderNotification = true; if (LOG_SURFACE) { Log.i("GLThread", "noticing that we want render notification tid=" + getId()); } if (DRAW_TWICE_AFTER_SIZE_CHANGED) { // We keep mRequestRender true so that we draw twice after the size changes. // (Once because of mSizeChanged, the second time because of mRequestRender.) // This forces the updated graphics onto the screen. } else { mRequestRender = false; } mSizeChanged = false; } else { mRequestRender = false; } sGLThreadManager.notifyAll(); break; } } // By design, this is the only place in a GLThread thread where we wait(). if (LOG_THREADS) { Log.i("GLThread", "waiting tid=" + getId() + " mHaveEglContext: " + mHaveEglContext + " mHaveEglSurface: " + mHaveEglSurface + " mPaused: " + mPaused + " mHasSurface: " + mHasSurface + " mWaitingForSurface: " + mWaitingForSurface + " mWidth: " + mWidth + " mHeight: " + mHeight + " mRequestRender: " + mRequestRender + " mRenderMode: " + mRenderMode); } sGLThreadManager.wait(); } } // end of synchronized(sGLThreadManager) if (event != null) { event.run(); event = null; continue; } if (createEglSurface) { if (LOG_SURFACE) { Log.w("GLThread", "egl createSurface"); } gl = (GL10) mEglHelper.createSurface(getHolder()); if (gl == null) { // Couldn't create a surface. Quit quietly. break; } sGLThreadManager.checkGLDriver(gl); createEglSurface = false; } if (createEglContext) { if (LOG_RENDERER) { Log.w("GLThread", "onSurfaceCreated"); } mRenderer.onSurfaceCreated(gl, mEglHelper.mEglConfig); createEglContext = false; } if (sizeChanged) { if (LOG_RENDERER) { Log.w("GLThread", "onSurfaceChanged(" + w + ", " + h + ")"); } mRenderer.onSurfaceChanged(gl, w, h); sizeChanged = false; } if (LOG_RENDERER_DRAW_FRAME) { Log.w("GLThread", "onDrawFrame tid=" + getId()); } mRenderer.onDrawFrame(gl); if (!mEglHelper.swap()) { if (LOG_SURFACE) { Log.i("GLThread", "egl context lost tid=" + getId()); } lostEglContext = true; } if (wantRenderNotification) { doRenderNotification = true; } } } finally { /* * clean-up everything... */ synchronized (sGLThreadManager) { stopEglSurfaceLocked(); stopEglContextLocked(); } } } public boolean ableToDraw() { return mHaveEglContext && mHaveEglSurface && readyToDraw(); } private boolean readyToDraw() { return (!mPaused) && mHasSurface && (mWidth > 0) && (mHeight > 0) && (mRequestRender || (mRenderMode == RENDERMODE_CONTINUOUSLY)); } public void setRenderMode(int renderMode) { if ( !((RENDERMODE_WHEN_DIRTY <= renderMode) && (renderMode <= RENDERMODE_CONTINUOUSLY)) ) { throw new IllegalArgumentException("renderMode"); } synchronized(sGLThreadManager) { mRenderMode = renderMode; sGLThreadManager.notifyAll(); } } public int getRenderMode() { synchronized(sGLThreadManager) { return mRenderMode; } } public void requestRender() { synchronized(sGLThreadManager) { mRequestRender = true; sGLThreadManager.notifyAll(); } } public void surfaceCreated() { synchronized(sGLThreadManager) { if (LOG_THREADS) { Log.i("GLThread", "surfaceCreated tid=" + getId()); } mHasSurface = true; sGLThreadManager.notifyAll(); while((mWaitingForSurface) && (!mExited)) { try { sGLThreadManager.wait(); } catch (InterruptedException e) { Thread.currentThread().interrupt(); } } } } public void surfaceDestroyed() { synchronized(sGLThreadManager) { if (LOG_THREADS) { Log.i("GLThread", "surfaceDestroyed tid=" + getId()); } mHasSurface = false; sGLThreadManager.notifyAll(); while((!mWaitingForSurface) && (!mExited)) { try { sGLThreadManager.wait(); } catch (InterruptedException e) { Thread.currentThread().interrupt(); } } } } public void onPause() { synchronized (sGLThreadManager) { if (LOG_PAUSE_RESUME) { Log.i("GLThread", "onPause tid=" + getId()); } mRequestPaused = true; sGLThreadManager.notifyAll(); while ((! mExited) && (! mPaused)) { if (LOG_PAUSE_RESUME) { Log.i("Main thread", "onPause waiting for mPaused."); } try { sGLThreadManager.wait(); } catch (InterruptedException ex) { Thread.currentThread().interrupt(); } } } } public void onResume() { synchronized (sGLThreadManager) { if (LOG_PAUSE_RESUME) { Log.i("GLThread", "onResume tid=" + getId()); } mRequestPaused = false; mRequestRender = true; mRenderComplete = false; sGLThreadManager.notifyAll(); while ((! mExited) && mPaused && (!mRenderComplete)) { if (LOG_PAUSE_RESUME) { Log.i("Main thread", "onResume waiting for !mPaused."); } try { sGLThreadManager.wait(); } catch (InterruptedException ex) { Thread.currentThread().interrupt(); } } } } public void onWindowResize(int w, int h) { synchronized (sGLThreadManager) { mWidth = w; mHeight = h; mSizeChanged = true; mRequestRender = true; mRenderComplete = false; sGLThreadManager.notifyAll(); // Wait for thread to react to resize and render a frame while (! mExited && !mPaused && !mRenderComplete && (mGLThread != null && mGLThread.ableToDraw())) { if (LOG_SURFACE) { Log.i("Main thread", "onWindowResize waiting for render complete from tid=" + mGLThread.getId()); } try { sGLThreadManager.wait(); } catch (InterruptedException ex) { Thread.currentThread().interrupt(); } } } } public void requestExitAndWait() { // don't call this from GLThread thread or it is a guaranteed // deadlock! synchronized(sGLThreadManager) { mShouldExit = true; sGLThreadManager.notifyAll(); while (! mExited) { try { sGLThreadManager.wait(); } catch (InterruptedException ex) { Thread.currentThread().interrupt(); } } } } public void requestReleaseEglContextLocked() { mShouldReleaseEglContext = true; sGLThreadManager.notifyAll(); } /** * Queue an "event" to be run on the GL rendering thread. * @param r the runnable to be run on the GL rendering thread. */ public void queueEvent(Runnable r) { if (r == null) { throw new IllegalArgumentException("r must not be null"); } synchronized(sGLThreadManager) { mEventQueue.add(r); sGLThreadManager.notifyAll(); } } // Once the thread is started, all accesses to the following member // variables are protected by the sGLThreadManager monitor private boolean mShouldExit; private boolean mExited; private boolean mRequestPaused; private boolean mPaused; private boolean mHasSurface; private boolean mWaitingForSurface; private boolean mHaveEglContext; private boolean mHaveEglSurface; private boolean mShouldReleaseEglContext; private int mWidth; private int mHeight; private int mRenderMode; private boolean mRequestRender; private boolean mRenderComplete; private ArrayList mEventQueue = new ArrayList(); // End of member variables protected by the sGLThreadManager monitor. private Renderer mRenderer; private EglHelper mEglHelper; } static class LogWriter extends Writer { @Override public void close() { flushBuilder(); } @Override public void flush() { flushBuilder(); } @Override public void write(char[] buf, int offset, int count) { for(int i = 0; i < count; i++) { char c = buf[offset + i]; if ( c == '\n') { flushBuilder(); } else { mBuilder.append(c); } } } private void flushBuilder() { if (mBuilder.length() > 0) { Log.v("GLSurfaceView", mBuilder.toString()); mBuilder.delete(0, mBuilder.length()); } } private StringBuilder mBuilder = new StringBuilder(); } private void checkRenderThreadState() { if (mGLThread != null) { throw new IllegalStateException( "setRenderer has already been called for this instance."); } } private static class GLThreadManager { private static String TAG = "GLThreadManager"; public synchronized void threadExiting(GLThread thread) { if (LOG_THREADS) { Log.i("GLThread", "exiting tid=" + thread.getId()); } thread.mExited = true; if (mEglOwner == thread) { mEglOwner = null; } notifyAll(); } /* * Tries once to acquire the right to use an EGL * context. Does not block. Requires that we are already * in the sGLThreadManager monitor when this is called. * * @return true if the right to use an EGL context was acquired. */ public boolean tryAcquireEglContextLocked(GLThread thread) { if (mEglOwner == thread || mEglOwner == null) { mEglOwner = thread; notifyAll(); return true; } checkGLESVersion(); if (mMultipleGLESContextsAllowed) { return true; } // Notify the owning thread that it should release the context. // TODO: implement a fairness policy. Currently // if the owning thread is drawing continuously it will just // reacquire the EGL context. if (mEglOwner != null) { mEglOwner.requestReleaseEglContextLocked(); } return false; } /* * Releases the EGL context. Requires that we are already in the * sGLThreadManager monitor when this is called. */ public void releaseEglContextLocked(GLThread thread) { if (mEglOwner == thread) { mEglOwner = null; } notifyAll(); } public synchronized boolean shouldReleaseEGLContextWhenPausing() { // Release the EGL context when pausing even if // the hardware supports multiple EGL contexts. // Otherwise the device could run out of EGL contexts. return true; } public synchronized boolean shouldTerminateEGLWhenPausing() { checkGLESVersion(); return !mMultipleGLESContextsAllowed; } public synchronized void checkGLDriver(GL10 gl) { if (! mGLESDriverCheckComplete) { checkGLESVersion(); if (mGLESVersion < kGLES_20) { String renderer = gl.glGetString(GL10.GL_RENDERER); mMultipleGLESContextsAllowed = ! renderer.startsWith(kMSM7K_RENDERER_PREFIX); if (LOG_SURFACE) { Log.w(TAG, "checkGLDriver renderer = \"" + renderer + "\" multipleContextsAllowed = " + mMultipleGLESContextsAllowed); } notifyAll(); } mGLESDriverCheckComplete = true; } } private void checkGLESVersion() { if (! mGLESVersionCheckComplete) { mGLESVersion = SystemProperties.getInt( "ro.opengles.version", ConfigurationInfo.GL_ES_VERSION_UNDEFINED); if (mGLESVersion >= kGLES_20) { mMultipleGLESContextsAllowed = true; } if (LOG_SURFACE) { Log.w(TAG, "checkGLESVersion mGLESVersion =" + " " + mGLESVersion + " mMultipleGLESContextsAllowed = " + mMultipleGLESContextsAllowed); } mGLESVersionCheckComplete = true; } } private boolean mGLESVersionCheckComplete; private int mGLESVersion; private boolean mGLESDriverCheckComplete; private boolean mMultipleGLESContextsAllowed; private static final int kGLES_20 = 0x20000; private static final String kMSM7K_RENDERER_PREFIX = "Q3Dimension MSM7500 "; private GLThread mEglOwner; } private static final GLThreadManager sGLThreadManager = new GLThreadManager(); private boolean mSizeChanged = true; private GLThread mGLThread; private EGLConfigChooser mEGLConfigChooser; private EGLContextFactory mEGLContextFactory; private EGLWindowSurfaceFactory mEGLWindowSurfaceFactory; private GLWrapper mGLWrapper; private int mDebugFlags; private int mEGLContextClientVersion; }