Sane replacement for GLSurfaceView

I’ve been asked several times about my implementation of OpenGL ES on top of Android’s SurfaceView so I’m posting it here for easier access 🙂 And maybe someone finds it useful by googling.

A few words about why did I bother in the first place? First, I wanted to control the main game loop by myself and don’t rely on GLSurfaceView’s callbacks. Doing it this way allowed me to have a much better code architecture and is more robust as I can *really* control when and what it does. My version does the same thing in 200 lines of code instead of 1350 and can actually be understood by human beings. That brings us to the second reason: I was experiencing weird bugs and had to debug my GL rendering code. Tracing the issue I ended up at EGL level. To this day I don’t know what was really the problem as GLSurfaceView is an abomination. It doesn’t have a proper separation of responsibilities in its thirteen internal classes, the logic is horribly convoluted… let’s say initially I started to refactor it into something understandable but after a few hours just gave up and wrote my own implementation. In a single class. Simple and working one. Nuff said 🙂

public class GLSurface extends SurfaceView implements SurfaceHolder.Callback {

	public GLSurface(Context context) {
		this(context, null);
	}
	public GLSurface(Context context, GLSurfaceListener listener) {
		this(context, null, listener, 5, 6, 5, 0, 16, 0);
	}
	public GLSurface(Context context, AttributeSet attrs, GLSurfaceListener listener,
			int rBits, int gBits, int bBits, int aBits, int dBits, int sBits) {
		super(context, attrs);
		this.rBits = rBits;
		this.gBits = gBits;
		this.bBits = bBits;
		this.aBits = aBits;
		this.dBits = dBits;
		this.sBits = sBits;
		getHolder().addCallback(this);
		this.listener = listener;
	}

	private GLSurfaceListener listener = null;
	public void setGLSurfaceListener(GLSurfaceListener listener) {
		this.listener = listener;
	}

	private boolean hasSurface;
	@Override
	public void surfaceCreated(SurfaceHolder holder) {
		hasSurface = true;
	}
	@Override
	public void surfaceDestroyed(SurfaceHolder holder) {
		hasSurface = false;
		while (gl != null) {
		}
	}

	private int width = 0;
	public int getGLWidth() {
		return width;
	}
	private int height = 0;
	public int getGLHeight() {
		return height;
	}

	private boolean surfaceChanged = false;
	@Override
	public void surfaceChanged(SurfaceHolder holder, int format, int w, int h) {
		width = w;
		height = h;
		surfaceChanged = true;
	}

	private boolean paused = false;
	public boolean isPaused() {
		return paused;
	}
	public void onPause() {
		paused = true;
		if (listener != null) {
			listener.onPause();
		}
	}
	public void onResume() {
		paused = false;
		if (listener != null) {
			listener.onResume();
		}
	}

	private boolean alive = true;
	public boolean isAlive() {
		return alive;
	}
	@Override
	protected void onDetachedFromWindow() {
		super.onDetachedFromWindow();
		alive = false;
	}

	private GL10 gl = null;
	public GL10 gl() {
		return gl;
	}
	private final int rBits;
	private final int gBits;
	private final int bBits;
	private final int aBits;
	private final int dBits;
	private final int sBits;
	private EGL10 egl;
	private EGLDisplay eglDisplay;
	private EGLSurface eglSurface;
	private EGLConfig eglConfig;
	private EGLContext eglContext;
	private void createSurface(SurfaceHolder holder) {
		if (gl != null) {
			throw new RuntimeException("Surface already initialized");
		}

		egl = (EGL10) EGLContext.getEGL();
		eglDisplay = egl.eglGetDisplay(EGL10.EGL_DEFAULT_DISPLAY);
		egl.eglInitialize(eglDisplay, null);

		int[] numConfigs = new int[1];
		egl.eglChooseConfig(eglDisplay, new int[] { EGL10.EGL_NONE }, null, 0, numConfigs);
		EGLConfig[] configs = new EGLConfig[numConfigs[0]];
		egl.eglChooseConfig(eglDisplay, new int[] { EGL10.EGL_NONE }, configs, numConfigs[0], numConfigs);
		int bestDiff = Integer.MAX_VALUE;
		int[] v = new int[1];
		for (EGLConfig c : configs) {
			int diff = 0;
			egl.eglGetConfigAttrib(eglDisplay, c, EGL10.EGL_RED_SIZE,     v); diff += Math.abs(rBits - v[0]);
			egl.eglGetConfigAttrib(eglDisplay, c, EGL10.EGL_GREEN_SIZE,   v); diff += Math.abs(gBits - v[0]);
			egl.eglGetConfigAttrib(eglDisplay, c, EGL10.EGL_BLUE_SIZE,    v); diff += Math.abs(bBits - v[0]);
			egl.eglGetConfigAttrib(eglDisplay, c, EGL10.EGL_ALPHA_SIZE,   v); diff += Math.abs(aBits - v[0]);
			// depth and stencil values are more important than colour depth
			egl.eglGetConfigAttrib(eglDisplay, c, EGL10.EGL_DEPTH_SIZE,   v); diff += Math.abs(dBits - v[0]) * 10;
			egl.eglGetConfigAttrib(eglDisplay, c, EGL10.EGL_STENCIL_SIZE, v); diff += Math.abs(sBits - v[0]) * 100;
			if (diff < bestDiff) {
				bestDiff = diff;
				eglConfig = c;
			}
		}

		eglContext = egl.eglCreateContext(eglDisplay, eglConfig, EGL10.EGL_NO_CONTEXT, null);
		if (eglContext == null || eglContext == EGL10.EGL_NO_CONTEXT) {
			throw new RuntimeException("Unable to create EGL context");
		}

		eglSurface = egl.eglCreateWindowSurface(eglDisplay, eglConfig, holder, null);
		if (eglSurface == null || eglSurface == EGL10.EGL_NO_SURFACE) {
			throw new RuntimeException("Unable to create EGL surface");
		}
		if (!egl.eglMakeCurrent(eglDisplay, eglSurface, eglSurface, eglContext)) {
			throw new RuntimeException("Unable to make EGL current");
		}

		gl = (GL10)eglContext.getGL();
	}
	private  void destroySurface() {
		if (gl == null) {
			return;
		}

		if (eglSurface != null && eglSurface != EGL10.EGL_NO_SURFACE) {
			egl.eglMakeCurrent(eglDisplay, EGL10.EGL_NO_SURFACE, EGL10.EGL_NO_SURFACE, EGL10.EGL_NO_CONTEXT);
			egl.eglDestroySurface(eglDisplay, eglSurface);
			eglSurface = null;
		}
		if (eglContext != null) {
			egl.eglDestroyContext(eglDisplay, eglContext);
			eglContext = null;
		}
		if (eglDisplay != null) {
			egl.eglTerminate(eglDisplay);
			eglDisplay = null;
		}

		gl = null;
	}
	public boolean canDraw() {
		if (!alive || width <= 0 || height <= 0) {
			return false;
		}
		if (isPaused() || !hasSurface) {
			if (listener != null) {
				listener.onSurfaceDestroyed();
			}
			destroySurface();
			return false;
		}
		return true;
	}
	public boolean bind() {
		if (!canDraw()) {
			return false;
		}
		if (gl == null) {
			createSurface(getHolder());
			if (listener != null) {
				listener.onSurfaceCreated(width, height);
			}
		}
		if (surfaceChanged) {
			if (listener != null) {
				listener.onSurfaceChanged(width, height);
			}
			surfaceChanged = false;
		}

		return true;
	}
	public void release() {
		egl.eglSwapBuffers(eglDisplay, eglSurface);
	}
}

public interface GLSurfaceListener {
	void onSurfaceCreated(int width, int height);
	void onResume();
	void onSurfaceChanged(int width, int height);
	void onPause();
	void onSurfaceDestroyed();
}
Advertisements

Tags: , , , ,

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s


%d bloggers like this: