I've been using GLSurfaceView since it was introduced in Android 1.5 and I was a little let down to find that the new Live Wallpaper APIs didn't include anything like that. I like the design because it makes it very easy to quickly start working in OpenGL. Without it, there is quite a bit of tedious initialization and thread management code that isn't necessary for the vast majority of apps. Fortunately, for my first live wallpaper (Live Waterpaper), I adapted the GLSurfaceView's code and created a GLWallpaperService with a GLEngine which takes a Renderer and does the job for me.
This is very similar in design to how things work with GLSurfaceView, except the GLWallpaperService does nothing more than create Engines and I have to move around a few of the fields for the GLThread management and configurations. Any event handling code you have should go into your GLEngine subclass and can be passed directly to your Renderer, which can be an exact port of an existing Renderer, with the exception of specifying the different package name on the interface.
I submitted this code to be part of the standard Android API. Hopefully it will make it in.
There is a lot of overlap with this code and GLSurfaceView, perhaps 99%.
So how do you use this? Extend GLWallpaperService and in your subclass, declare an inner class that extends GLEngine. Override onCreateEngine() to return your GLEngine subclass. In your GLEngine subclass, instantiate your Renderer, configure it and set it using setRenderer(Renderer) and setRenderMode(int). Your engine can handle touch events if you call setTouchEventsEnabled(true) and can also handle sensor data and preference changes. Everything else works exactly like it did with GLSurfaceView.
Enjoy!
Update 4/6/2010 - There were some issues with the original code posted and the Droid 2.1 update that was rolled out recently. Just get the latest code that is posted here and it should work fine.
File android.opengl.GLWallpaperService.java
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.opengl.BaseConfigChooser.ComponentSizeChooser;
import android.opengl.BaseConfigChooser.SimpleEGLConfigChooser;
import android.service.wallpaper.WallpaperService;
import android.util.Log;
import android.view.SurfaceHolder;
public class GLWallpaperService extends WallpaperService {
private static final String TAG = "GLWallpaperService";
@Override
public Engine onCreateEngine() {
return new GLEngine();
}
public class GLEngine extends Engine {
public final static int RENDERMODE_WHEN_DIRTY = 0;
public final static int RENDERMODE_CONTINUOUSLY = 1;
private GLThread mGLThread;
private EGLConfigChooser mEGLConfigChooser;
private EGLContextFactory mEGLContextFactory;
private EGLWindowSurfaceFactory mEGLWindowSurfaceFactory;
private GLWrapper mGLWrapper;
private int mDebugFlags;
public GLEngine() {
super();
}
@Override
public void onVisibilityChanged(boolean visible) {
if (visible) {
onResume();
} else {
onPause();
}
super.onVisibilityChanged(visible);
}
@Override
public void onCreate(SurfaceHolder surfaceHolder) {
super.onCreate(surfaceHolder);
// Log.d(TAG, "GLEngine.onCreate()");
}
@Override
public void onDestroy() {
super.onDestroy();
// Log.d(TAG, "GLEngine.onDestroy()");
mGLThread.requestExitAndWait();
}
@Override
public void onSurfaceChanged(SurfaceHolder holder, int format, int width, int height) {
// Log.d(TAG, "onSurfaceChanged()");
mGLThread.onWindowResize(width, height);
super.onSurfaceChanged(holder, format, width, height);
}
@Override
public void onSurfaceCreated(SurfaceHolder holder) {
Log.d(TAG, "onSurfaceCreated()");
mGLThread.surfaceCreated(holder);
super.onSurfaceCreated(holder);
}
@Override
public void onSurfaceDestroyed(SurfaceHolder holder) {
Log.d(TAG, "onSurfaceDestroyed()");
mGLThread.surfaceDestroyed();
super.onSurfaceDestroyed(holder);
}
/**
* An EGL helper class.
*/
public void setGLWrapper(GLWrapper glWrapper) {
mGLWrapper = glWrapper;
}
public void setDebugFlags(int debugFlags) {
mDebugFlags = debugFlags;
}
public int getDebugFlags() {
return mDebugFlags;
}
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, mEGLConfigChooser, mEGLContextFactory, mEGLWindowSurfaceFactory, mGLWrapper);
mGLThread.start();
}
public void setEGLContextFactory(EGLContextFactory factory) {
checkRenderThreadState();
mEGLContextFactory = factory;
}
public void setEGLWindowSurfaceFactory(EGLWindowSurfaceFactory factory) {
checkRenderThreadState();
mEGLWindowSurfaceFactory = factory;
}
public void setEGLConfigChooser(EGLConfigChooser configChooser) {
checkRenderThreadState();
mEGLConfigChooser = configChooser;
}
public void setEGLConfigChooser(boolean needDepth) {
setEGLConfigChooser(new SimpleEGLConfigChooser(needDepth));
}
public void setEGLConfigChooser(int redSize, int greenSize, int blueSize, int alphaSize, int depthSize,
int stencilSize) {
setEGLConfigChooser(new ComponentSizeChooser(redSize, greenSize, blueSize, alphaSize, depthSize,
stencilSize));
}
public void setRenderMode(int renderMode) {
mGLThread.setRenderMode(renderMode);
}
public int getRenderMode() {
return mGLThread.getRenderMode();
}
public void requestRender() {
mGLThread.requestRender();
}
public void onPause() {
mGLThread.onPause();
}
public void onResume() {
mGLThread.onResume();
}
public void queueEvent(Runnable r) {
mGLThread.queueEvent(r);
}
private void checkRenderThreadState() {
if (mGLThread != null) {
throw new IllegalStateException("setRenderer has already been called for this instance.");
}
}
}
public interface Renderer {
public void onSurfaceCreated(GL10 gl, EGLConfig config);
public void onSurfaceChanged(GL10 gl, int width, int height);
public void onDrawFrame(GL10 gl);
}
}
class LogWriter extends Writer {
private StringBuilder mBuilder = new StringBuilder();
@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());
}
}
}
// ----------------------------------------------------------------------
/**
* An interface for customizing the eglCreateContext and eglDestroyContext calls.
*
* This interface must be implemented by clients wishing to call
* {@link GLWallpaperService#setEGLContextFactory(EGLContextFactory)}
*/
interface EGLContextFactory {
EGLContext createContext(EGL10 egl, EGLDisplay display, EGLConfig eglConfig);
void destroyContext(EGL10 egl, EGLDisplay display, EGLContext context);
}
class DefaultContextFactory implements EGLContextFactory {
public EGLContext createContext(EGL10 egl, EGLDisplay display, EGLConfig config) {
return egl.eglCreateContext(display, config, EGL10.EGL_NO_CONTEXT, null);
}
public void destroyContext(EGL10 egl, EGLDisplay display, EGLContext context) {
egl.eglDestroyContext(display, context);
}
}
/**
* An interface for customizing the eglCreateWindowSurface and eglDestroySurface calls.
*
* This interface must be implemented by clients wishing to call
* {@link GLWallpaperService#setEGLWindowSurfaceFactory(EGLWindowSurfaceFactory)}
*/
interface EGLWindowSurfaceFactory {
EGLSurface createWindowSurface(EGL10 egl, EGLDisplay display, EGLConfig config, Object nativeWindow);
void destroySurface(EGL10 egl, EGLDisplay display, EGLSurface surface);
}
class DefaultWindowSurfaceFactory implements EGLWindowSurfaceFactory {
public EGLSurface createWindowSurface(EGL10 egl, EGLDisplay
display, EGLConfig config, Object nativeWindow) {
// this is a bit of a hack to work around Droid init problems - if you don't have this, it'll get hung up on orientation changes
EGLSurface eglSurface = null;
while (eglSurface == null) {
try {
eglSurface = egl.eglCreateWindowSurface(display,
config, nativeWindow, null);
} catch (Throwable t) {
} finally {
if (eglSurface == null) {
try {
Thread.sleep(10);
} catch (InterruptedException t) {
}
}
}
}
return eglSurface;
}
public void destroySurface(EGL10 egl, EGLDisplay display, EGLSurface surface) {
egl.eglDestroySurface(display, surface);
}
}
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);
}
class EglHelper {
private EGL10 mEgl;
private EGLDisplay mEglDisplay;
private EGLSurface mEglSurface;
private EGLContext mEglContext;
EGLConfig mEglConfig;
private EGLConfigChooser mEGLConfigChooser;
private EGLContextFactory mEGLContextFactory;
private EGLWindowSurfaceFactory mEGLWindowSurfaceFactory;
private GLWrapper mGLWrapper;
public EglHelper(EGLConfigChooser chooser, EGLContextFactory contextFactory,
EGLWindowSurfaceFactory surfaceFactory, GLWrapper wrapper) {
this.mEGLConfigChooser = chooser;
this.mEGLContextFactory = contextFactory;
this.mEGLWindowSurfaceFactory = surfaceFactory;
this.mGLWrapper = wrapper;
}
/**
* Initialize EGL for a given configuration spec.
*
* @param configSpec
*/
public void start() {
Log.d("EglHelper" + instanceId, "start()");
if (mEgl == null) {
Log.d("EglHelper" + instanceId, "getting new EGL");
/*
* Get an EGL instance
*/
mEgl = (EGL10) EGLContext.getEGL();
} else {
Log.d("EglHelper" + instanceId, "reusing EGL");
}
if (mEglDisplay == null) {
Log.d("EglHelper" + instanceId, "getting new display");
/*
* Get to the default display.
*/
mEglDisplay = mEgl.eglGetDisplay(EGL10.EGL_DEFAULT_DISPLAY);
} else {
Log.d("EglHelper" + instanceId, "reusing display");
}
if (mEglConfig == null) {
Log.d("EglHelper" + instanceId, "getting new config");
/*
* We can now initialize EGL for that display
*/
int[] version = new int[2];
mEgl.eglInitialize(mEglDisplay, version);
mEglConfig = mEGLConfigChooser.chooseConfig(mEgl, mEglDisplay);
} else {
Log.d("EglHelper" + instanceId, "reusing config");
}
if (mEglContext == null) {
Log.d("EglHelper" + instanceId, "creating new context");
/*
* Create an OpenGL ES context. This must be done only once, an OpenGL context is a somewhat heavy object.
*/
mEglContext = mEGLContextFactory.createContext(mEgl, mEglDisplay, mEglConfig);
if (mEglContext == null || mEglContext == EGL10.EGL_NO_CONTEXT) {
throw new RuntimeException("createContext failed");
}
} else {
Log.d("EglHelper" + instanceId, "reusing context");
}
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) {
/*
* 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) {
throw new RuntimeException("createWindowSurface failed");
}
/*
* 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)) {
throw new RuntimeException("eglMakeCurrent failed.");
}
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() {
mEgl.eglSwapBuffers(mEglDisplay, mEglSurface);
/*
* Always 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.
*/
return mEgl.eglGetError() != EGL11.EGL_CONTEXT_LOST;
}
public void destroySurface() {
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 (mEglContext != null) {
mEGLContextFactory.destroyContext(mEgl, mEglDisplay, mEglContext);
mEglContext = null;
}
if (mEglDisplay != null) {
mEgl.eglTerminate(mEglDisplay);
mEglDisplay = null;
}
}
}
class GLThread extends Thread {
private final static boolean LOG_THREADS = false;
public final static int DEBUG_CHECK_GL_ERROR = 1;
public final static int DEBUG_LOG_GL_CALLS = 2;
private final GLThreadManager sGLThreadManager = new GLThreadManager();
private GLThread mEglOwner;
private EGLConfigChooser mEGLConfigChooser;
private EGLContextFactory mEGLContextFactory;
private EGLWindowSurfaceFactory mEGLWindowSurfaceFactory;
private GLWrapper mGLWrapper;
public SurfaceHolder mHolder;
private boolean mSizeChanged = true;
// Once the thread is started, all accesses to the following member
// variables are protected by the sGLThreadManager monitor
public boolean mDone;
private boolean mPaused;
private boolean mHasSurface;
private boolean mWaitingForSurface;
private boolean mHaveEgl;
private int mWidth;
private int mHeight;
private int mRenderMode;
private boolean mRequestRender;
private boolean mEventsWaiting;
// End of member variables protected by the sGLThreadManager monitor.
private GLWallpaperService.Renderer mRenderer;
private ArrayList
private EglHelper mEglHelper;
GLThread(GLWallpaperService.Renderer renderer, EGLConfigChooser chooser, EGLContextFactory contextFactory,
EGLWindowSurfaceFactory surfaceFactory, GLWrapper wrapper) {
super();
mDone = false;
mWidth = 0;
mHeight = 0;
mRequestRender = true;
mRenderMode = GLWallpaperService.GLEngine.RENDERMODE_CONTINUOUSLY;
mRenderer = renderer;
this.mEGLConfigChooser = chooser;
this.mEGLContextFactory = contextFactory;
this.mEGLWindowSurfaceFactory = surfaceFactory;
this.mGLWrapper = wrapper;
}
@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 stopEglLocked() {
if (mHaveEgl) {
mHaveEgl = false;
mEglHelper.destroySurface();
sGLThreadManager.releaseEglSurface(this);
}
}
private void guardedRun() throws InterruptedException {
mEglHelper = new EglHelper(mEGLConfigChooser, mEGLContextFactory, mEGLWindowSurfaceFactory, mGLWrapper);
try {
GL10 gl = null;
boolean tellRendererSurfaceCreated = true;
boolean tellRendererSurfaceChanged = true;
/*
* This is our main activity thread's loop, we go until asked to quit.
*/
while (!isDone()) {
/*
* Update the asynchronous state (window size)
*/
int w = 0;
int h = 0;
boolean changed = false;
boolean needStart = false;
boolean eventsWaiting = false;
synchronized (sGLThreadManager) {
while (true) {
// Manage acquiring and releasing the SurfaceView
// surface and the EGL surface.
if (mPaused) {
stopEglLocked();
}
if (!mHasSurface) {
if (!mWaitingForSurface) {
stopEglLocked();
mWaitingForSurface = true;
sGLThreadManager.notifyAll();
}
} else {
if (!mHaveEgl) {
if (sGLThreadManager.tryAcquireEglSurface(this)) {
mHaveEgl = true;
mEglHelper.start();
mRequestRender = true;
needStart = true;
}
}
}
// Check if we need to wait. If not, update any state
// that needs to be updated, copy any state that
// needs to be copied, and use "break" to exit the
// wait loop.
if (mDone) {
return;
}
if (mEventsWaiting) {
eventsWaiting = true;
mEventsWaiting = false;
break;
}
if ((!mPaused) && mHasSurface && mHaveEgl && (mWidth > 0) && (mHeight > 0)
&& (mRequestRender || (mRenderMode == GLWallpaperService.GLEngine.RENDERMODE_CONTINUOUSLY))) {
changed = mSizeChanged;
w = mWidth;
h = mHeight;
mSizeChanged = false;
mRequestRender = false;
if (mHasSurface && mWaitingForSurface) {
changed = true;
mWaitingForSurface = false;
sGLThreadManager.notifyAll();
}
break;
}
// By design, this is the only place where we wait().
if (LOG_THREADS) {
Log.i("GLThread", "waiting tid=" + getId());
}
sGLThreadManager.wait();
}
} // end of synchronized(sGLThreadManager)
/*
* Handle queued events
*/
if (eventsWaiting) {
Runnable r;
while ((r = getEvent()) != null) {
r.run();
if (isDone()) {
return;
}
}
// Go back and see if we need to wait to render.
continue;
}
if (needStart) {
tellRendererSurfaceCreated = true;
changed = true;
}
if (changed) {
gl = (GL10) mEglHelper.createSurface(mHolder);
tellRendererSurfaceChanged = true;
}
if (tellRendererSurfaceCreated) {
mRenderer.onSurfaceCreated(gl, mEglHelper.mEglConfig);
tellRendererSurfaceCreated = false;
}
if (tellRendererSurfaceChanged) {
mRenderer.onSurfaceChanged(gl, w, h);
tellRendererSurfaceChanged = false;
}
if ((w > 0) && (h > 0)) {
/* draw a frame here */
mRenderer.onDrawFrame(gl);
/*
* Once we're done with GL, we need to call swapBuffers() to instruct the system to display the
* rendered frame
*/
mEglHelper.swap();
}
}
} finally {
/*
* clean-up everything...
*/
synchronized (sGLThreadManager) {
stopEglLocked();
mEglHelper.finish();
}
}
}
private boolean isDone() {
synchronized (sGLThreadManager) {
return mDone;
}
}
public void setRenderMode(int renderMode) {
if (!((GLWallpaperService.GLEngine.RENDERMODE_WHEN_DIRTY <= renderMode) && (renderMode <= GLWallpaperService.GLEngine.RENDERMODE_CONTINUOUSLY))) {
throw new IllegalArgumentException("renderMode");
}
synchronized (sGLThreadManager) {
mRenderMode = renderMode;
if (renderMode == GLWallpaperService.GLEngine.RENDERMODE_CONTINUOUSLY) {
sGLThreadManager.notifyAll();
}
}
}
public int getRenderMode() {
synchronized (sGLThreadManager) {
return mRenderMode;
}
}
public void requestRender() {
synchronized (sGLThreadManager) {
mRequestRender = true;
sGLThreadManager.notifyAll();
}
}
public void surfaceCreated(SurfaceHolder holder) {
mHolder = holder;
synchronized (sGLThreadManager) {
if (LOG_THREADS) {
Log.i("GLThread", "surfaceCreated tid=" + getId());
}
mHasSurface = true;
sGLThreadManager.notifyAll();
}
}
public void surfaceDestroyed() {
synchronized (sGLThreadManager) {
if (LOG_THREADS) {
Log.i("GLThread", "surfaceDestroyed tid=" + getId());
}
mHasSurface = false;
sGLThreadManager.notifyAll();
while (!mWaitingForSurface && isAlive() && !mDone) {
try {
sGLThreadManager.wait();
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
}
}
public void onPause() {
synchronized (sGLThreadManager) {
mPaused = true;
sGLThreadManager.notifyAll();
}
}
public void onResume() {
synchronized (sGLThreadManager) {
mPaused = false;
mRequestRender = true;
sGLThreadManager.notifyAll();
}
}
public void onWindowResize(int w, int h) {
synchronized (sGLThreadManager) {
mWidth = w;
mHeight = h;
mSizeChanged = true;
sGLThreadManager.notifyAll();
}
}
public void requestExitAndWait() {
// don't call this from GLThread thread or it is a guaranteed
// deadlock!
synchronized (sGLThreadManager) {
mDone = true;
sGLThreadManager.notifyAll();
}
try {
join();
} catch (InterruptedException ex) {
Thread.currentThread().interrupt();
}
}
/**
* 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) {
synchronized (this) {
mEventQueue.add(r);
synchronized (sGLThreadManager) {
mEventsWaiting = true;
sGLThreadManager.notifyAll();
}
}
}
private Runnable getEvent() {
synchronized (this) {
if (mEventQueue.size() > 0) {
return mEventQueue.remove(0);
}
}
return null;
}
private class GLThreadManager {
public synchronized void threadExiting(GLThread thread) {
if (LOG_THREADS) {
Log.i("GLThread", "exiting tid=" + thread.getId());
}
thread.mDone = true;
if (mEglOwner == thread) {
mEglOwner = null;
}
notifyAll();
}
/*
* Tries once to acquire the right to use an EGL surface. Does not block.
*
* @return true if the right to use an EGL surface was acquired.
*/
public synchronized boolean tryAcquireEglSurface(GLThread thread) {
if (mEglOwner == thread || mEglOwner == null) {
mEglOwner = thread;
notifyAll();
return true;
}
return false;
}
public synchronized void releaseEglSurface(GLThread thread) {
if (mEglOwner == thread) {
mEglOwner = null;
}
notifyAll();
}
}
}
interface EGLConfigChooser {
EGLConfig chooseConfig(EGL10 egl, EGLDisplay display);
}
abstract class BaseConfigChooser implements EGLConfigChooser {
public BaseConfigChooser(int[] configSpec) {
mConfigSpec = configSpec;
}
public EGLConfig chooseConfig(EGL10 egl, EGLDisplay display) {
int[] num_config = new int[1];
egl.eglChooseConfig(display, mConfigSpec, null, 0, num_config);
int numConfigs = num_config[0];
if (numConfigs <= 0) {
throw new IllegalArgumentException("No configs match configSpec");
}
EGLConfig[] configs = new EGLConfig[numConfigs];
egl.eglChooseConfig(display, mConfigSpec, configs, numConfigs, num_config);
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;
public static 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) {
EGLConfig closestConfig = null;
int closestDistance = 1000;
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);
int distance = Math.abs(r - mRedSize) + Math.abs(g - mGreenSize) + Math.abs(b - mBlueSize)
+ Math.abs(a - mAlphaSize);
if (distance < closestDistance) {
closestDistance = distance;
closestConfig = config;
}
}
}
return closestConfig;
}
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 supported surface as close to RGB565 as possible, with or without a depth buffer.
*
*/
public static class SimpleEGLConfigChooser extends ComponentSizeChooser {
public SimpleEGLConfigChooser(boolean withDepthBuffer) {
super(4, 4, 4, 0, withDepthBuffer ? 16 : 0, 0);
// Adjust target values. This way we'll accept a 4444 or
// 555 buffer if there's no 565 buffer available.
mRedSize = 5;
mGreenSize = 6;
mBlueSize = 5;
}
}
}
And here's the basic template for implementing your wallpaper service:
public Engine onCreateEngine() {
MyEngine engine = new MyEngine();
return engine;
}
// prefs and sensor interface is optional, just showing that this is where you do all of that - everything that would normally be in an activity is in here.
class MyEngine extends GLEngine implements SharedPreferences.OnSharedPreferenceChangeListener, SensorEventListener {
MyRenderer renderer;
public MyEngine() {
super();
// handle prefs, other initialization
renderer = new MyRenderer();
setRenderer(renderer);
setRenderMode(RENDERMODE_WHEN_DIRTY);
}
public void onDestroy() {
super.onDestroy();
if (renderer != null) {
renderer.release(); // assuming yours has this method - it should!
}
renderer = null;
}
52 Comments
Post a comment here or discuss this and other topics in the forumsSample Project (very basic)
I've created a very basic project using your code that works on the Nexus One (just draws a square). After some messing around with this code and the Live Earth Wallpaper source, I wanted to make something that was as basic as possible that didn't result in a force close.
Also almost all the OpenGL code has been directly copied from the awesome tutorials at:
http://blog.jayway.com/2010/01/01/opengl-es-tutorial-for-android-%E2%80%....
Is there any way I can post a zip file or something that people can use? Since I'm an OpenGL newb, I had a lot of trouble getting to this stage, but I'm sure there are other people that are a bit unclear as to how to implement your code.
Thanks!
cannot convert from object to runnable
At this point:
private Runnable getEvent() {
synchronized (this) {
if (mEventQueue.size() > 0) {
return mEventQueue.remove(0);
}
}
return null;
}
I get an error about not being able to convert from object to runnable. Should this be cast here or should I define the ArrayList as runnable?
Is there a download link or
Is there a download link or should it be copy/pasted from above? Also, what is the instanceId variable? I don't see it declared anywhere, Eclipse is giving me errors. Sorry for the newb questions!
The instanceId was just a
The instanceId was just a field incremented by a static field so I could see how many different instances were created and used. You can comment those lines out - they are just for debugging.
License
Robert,
Hate to be an ass, but what is the license for your code here? Would you allow an enterprising young developer to put it in their code base for a paid application for example?
cheers
Peter
This code is derived from an
This code is derived from an old version of GLSurfaceView in the Android project. Use it like you would use any part of Android for your applications :)
Where is this project at?
I am working on an app that would incorporate a live wallpaper and am trying to decide if I can use OpenGL or not.
As it stands, this is buggy on either the Droid or Nexus One right? Are there any other alternatives out there for using OpenGL in the live wallpaper?
Thanks
Justin
I'm currently not happy with
I'm currently not happy with how this is working. There is a bug on Android 2.1 and 2.2 now that causes processes to completely lockup and this code has been modified in a bad way to work around it. I'm waiting for that to be fixed before moving forward with this.
Textures
Hi Robert,
Thanks for this great code sample!
Any idea why the Moto Droid has problems displaying textures using this GLSurfaceView? It works great on the Nexus with 2.1, but not on the droid. And now I'm getting weird results with the Nexus running 2.2 (FroYo).
On the nexus running 2.1 it can render textures fine, on the nexus running FroYo I get coloured squares, and on the moto droid running 2.1 I get a black screen (no texture drawn).
Cheers Mate!
Nilz.
Are you sure you're giving it
Are you sure you're giving it power-of-two textures? You'll get some weird results if you don't :)
Textures
Yep, I'm definitely using power of two textures. But like I said, works fine on Nexus with 2.1, only coloured squares on the Nexus 2.2, and black screen on the Droid running 2.1.
Have you tried using this code with textures on the Nexus with FroYo?
Cheers,
Nilz.
I haven't tested in on Froyo
I haven't tested in on Froyo but I'd have a hard time believing that this code would cause what you're seeing. This code does very normal EGL init stuff. If you're getting a scene rendered at all, it's working. If there are problems with that scene, it's something you'll need to debug in your loading/rendering.
I don't get a black screen on droid on this - I get a normal rendering context and things work fine, which is why I'm thinking you've got a problem elsewhere.
Live Wallpaper not showing on Market
I recently uploaded a Live Wallpaper to the Android Market. It shows up for download on the Nexus One, but not on the Droid.
I tried all difference settings for supports-screens (even removing it altogether) thinking it was related, but still have not seen it show up in the Android Market on a Moto Droid w/ the OTA 2.1 update.
Can you please provide your AndroidManifest.xml file for review? I really appreciate any help.
Here is the most recent version I posted:
<?xml version="1.0" encoding="utf-8"?>
There's a bug with the Droid's feature list
Odds are the problem is you're doing everything correctly. Basically, I've found that if you have your manifest tagged as using the Live Wallpaper feature, meaning this is in your manifest:
uses-feature android:name="android.software.live_wallpaper"
The Droid will filter it out of the Marketplace. This is incorrect, as the Droid obviously supports Live Wallpapers just fine. I've posted a few threads on this trying to get response from Motorola, and sort of did here. Hopefully they'll fix this, because right now it seems to be a choice between losing half the available userbase, or confusing people with the Eris/Moment/Hero now that they're using 2.1.
I actually don't recommend
I actually don't recommend that you use my code right now. It can cause memory leaks if you don't release all of your vram on configuration changes. It's also not totally stable. I apologize for this. When I get some more time, I'll look into stabilizing it on the Droid and on the Nexus One together, assuming that's possible.
Doesn't work for me
Doesn't work for me -- the live wallpaper shows up OK in preview, but after choosing the wallpaper and returning to the homescreen, only about two frames of the new live wallpaper are shown before the whole live wallpaper turns blue (i.e. the launcher icons are shown over the top of a plain blue background). When I exit I get a segv (I'm calling NDK code, specifically the San Angeles demo)...
Yeah this is currently having
Yeah this is currently having some issues. It was working fine on the N1 but I started making changes to get it stable on Droid 2.1 and now everything is a mess. I apologize. I'm very busy with the release of a new game but I will take another day soon to work on it.
Live Waterpaper leaks memory
Hi Robert,
thank you for the code.
Today I implemented my first live wallpaper with the GLWallpaperService.
After I got it working I observed two things:
1.) My wallpaper leadked memory.
2.) There was a forced close every now and then when initializing the wallpaper.
I tracked down the memory leak: The mistake I made was this:
In onSurfaceChanged(GL10 gl, int width, int height) in my renderer I _always_ allocated new textures as I do it in my activities, but it seems in wallpapers the render context is not always lost - only the surface. This means the old textures are still allocated.
After I fixed it I thought I try your wallpaper and indeed it seems to have the same memory leak.
How to reproduce:
0.) Activate your Waterpaper
1.) Go to Settings->Applications->Services. Check how much memory the waterpaper service is using.
2.) Return to the home screen.
3.) Open the "phone application" or any other app.
4.) Return to the home screen by pressing the "back" button
5.) Repeat step 3 and 4 (very quickly) ten times
6.) Check memory usage of the wallpaper service
7.) GOTO 3
Currently your Waterpapaer is using 33 MBytes on my phone.
How I solved it, in case it is really the same memory leak as I have had it:
In my render class the code looks now like this:
public void onSurfaceCreated(GL10 gl, EGLConfig config) {
if (old_gl != gl) {
NativeBridge.appGFXInit();
old_gl = gl;
}
}
... just check if the render context has changed, if not don't allocate any new textures (and VBOs)
The other problem:
Every now and then my wallpaper showed a "force close" dialog when initializing. The very stange thing was: The wallpaper apeared behind the "force close" dialog and was running properly!
I tracked down the problem to this code in "public GL createSurface(SurfaceHolder holder)":
if (!mEgl.eglMakeCurrent(mEglDisplay, mEglSurface, mEglSurface,
mEglContext)) {
throw new RuntimeException("eglMakeCurrent failed.");
}
The execption was thrown every now and then during initialization. But well, the wallpaper worked behind the force close dialog...
So I just commented out the throw, the wallpaper runs fine even if the call fails.
Now my wallpaper seems to run fine. I don't know any method how to crash it (orientation change works fine, too) and memory usage is constant.
Thanks again for your code, you propbably saved me days of boring work. I'm still no java expert, so such code snippets and examples helps me a lot!
Kind Regards,
Ralf
Thank you for debugging that!
Thank you for debugging that! We've been back and forth a bit trying to get stability with this code on both the Nexus One and the Droid. The current issue is that the Nexus One leaks memory if you keep the context/display but don't keep the surface... Did you test fully on both the N1 and Droid?
Only N1
I only have a Nexus One. So, it's only tested on this device.
Ok. To give you a brief
Ok. To give you a brief history, the code was originally tested on the N1 and working correctly. Droid 2.1 came out and didn't work right so now I'm trying to get it working correctly on both. I think we need to add a method in like onContextChanged() which can be used to manage video memory.
What tool are you using to check memory usage, btw?
Re: Ok. To give you a brief
onContextChanged doesn't sound bad. But after adding this Renderer is no longer the same as GLSurfaceView.Renderer... I think that's not a real problem.
Memory checking on the phone? I check in: Settings->Applications->Services or use the process manager of "Astro" a file manager available in the market.
But mainly I'm just using the Task Manager of Windows... The apps I'm currently devlope are written with the NDK and are designed to be cross-platform from the begining. So I 95% code in Visual Studio and if I think there might be a leak I use the memory debug funtions of the windows runtime.
But in cases like this where the bug is not in the code which is shared by both platforms I have to use the tools available on the phone.
Issues on Motorola Droid w/ 2.1 update
I've made use of this for a couple of live wallpapers now (and really appreciate your implementation, believe me). With the official Droid 2.1 update being pushed out I'm sanity checking things on a Droid, however, and have run into a couple stability problems. Usually these stem from sliding the keyboard in and out a few times, forcing the wallpaper to recreate itself between landscape/portrait. I'll sit down tonight and see if i can provide some better callstacks, but wanted to mention them here as frankly I'm not sure what's going on in a few of these places, and am hoping for your advice... and I know you've got some wallpapers on the market you'd probably want to check as well. =P
The big one that I've seen happen several times is an onDestroy call preceding a drawFrame call. This occurs upon hitting "set as wallpaper" within the preview window. The wallpaper is recreated, it hits onSurfaceCreated/onSurfaceChanged, I take a moment loading a few images. A split second later (before it's even done with the image reading) I get an onDestroy call, and dutifully wipe out things as needed. A split second after that, I get a call to drawFrame (which, obviously, has nothing to work with anymore, as it just got unloaded).
In addition to this, a couple of times I got a crash in chooseConfig,which ends up in findConfigAttrib on GLWallpaperService line 1056. It's a nullPointerException, if I remember right. I really should've saved a callstack of that one.
I've also had a couple of cases where (clicking the keyboard in and out) it just stops doing things altogether, eventually ending up with an endless spew of "waitForCondition(ReallocateCondition) timed out" lines in logCat.
(Sorry this got so long!)
I'm seeing some problems as
I'm seeing some problems as well. I have been waiting for my Droid to get the official update so I can test and solve them. Perhaps I'll do an early update and hammer this out.
Understood
Anything I can do to help, just say the word. I've got a Droid with the official 2.1 update (There's a link on www.androidandme.com with a zip, if you don't want to wait for it OTA). I'll be taking a look myself, but admittedly don't understand what's going on under the hood very well currently, so it'll be a bit of a crapshoot, especially considering attaching a debugger to a live wallpaper still eludes me.
Debugging in Eclipse for 2.1 on a livewallpaper
Did you ever manage to work out how to debug a live wallpaper Jeremy?
I cant do it on the emulator OR thro my phone (HTC desire)
Thanks for any help
Sanjay
Debug a live wallpaper with breakpoints
Here is how you debug a live wallpaper (which is a service)... I have tested this and it works:
http://www.helloandroid.com/tutorials/how-debug-service
I've tracked the problem down
I've tracked the problem down to a black hole call during egl.eglCreateWindowSurface(display, config, nativeWindow, null);
I put in extremely verbose logging and it looks like everything is happening correctly. I'm not sure why that call is hanging. I don't believe it should ever hang like that and that leads me to believe that there may be a problem with either the driver or some underlying egl code. There's not much I can do about that right now, unfortunately. I'm just going to wait for the powers that be to confirm or deny the issue and take it from there.
OpenGL Android drivers
I've seen a few issues like that on one of my other projects (a game called Synergy) as well... if you put the phone to sleep and restart it several times, sometimes it'll come back to a black screen. It doesn't pop up the force close window or anything, just sits there being black. After (similarly) putting output into all the outerlying functions I found that when this happens, none of them ever get called, when at the very least you'd expect GLSurfaceView's onCreate or onRestoreInstanceState to get hit.
I've never seen any behavior like this on any of my non OpenGL projects thus far, sleep/restore with those has always been rock solid.
Jeremy, a hack has been added
Jeremy, a hack has been added in DefaultWindowSurfaceFactory which stabilizes the orientation changes on the Droid:
public EGLSurface createWindowSurface(EGL10 egl, EGLDisplay
display, EGLConfig config, Object nativeWindow) {
EGLSurface eglSurface = null;
while (eglSurface == null) {
try {
eglSurface = egl.eglCreateWindowSurface(display,
config, nativeWindow, null);
} catch (Throwable t) {
} finally {
if (eglSurface == null) {
try {
Thread.sleep(10);
} catch (InterruptedException t) {
}
}
}
}
return eglSurface;
}
Post new comment