michael@0: /* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*- michael@0: * This Source Code Form is subject to the terms of the Mozilla Public michael@0: * License, v. 2.0. If a copy of the MPL was not distributed with this michael@0: * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ michael@0: michael@0: package org.mozilla.gecko.gfx; michael@0: michael@0: import org.mozilla.gecko.GeckoAppShell; michael@0: import org.mozilla.gecko.GeckoEvent; michael@0: import org.mozilla.gecko.GeckoThread; michael@0: import org.mozilla.gecko.mozglue.generatorannotations.WrapElementForJNI; michael@0: import org.mozilla.gecko.util.ThreadUtils; michael@0: michael@0: import android.util.Log; michael@0: michael@0: import javax.microedition.khronos.egl.EGL10; michael@0: import javax.microedition.khronos.egl.EGLConfig; michael@0: import javax.microedition.khronos.egl.EGLContext; michael@0: import javax.microedition.khronos.egl.EGLDisplay; michael@0: import javax.microedition.khronos.egl.EGLSurface; michael@0: michael@0: /** michael@0: * EGLPreloadingThread is purely a preloading optimization, not something michael@0: * we rely on for anything else than performance. We will be initializing michael@0: * EGL in GLController::initEGL() when we need it, but having EGL initialization michael@0: * already previously done by EGLPreloadingThread::run() will make it much michael@0: * faster for GLController to do again. michael@0: * michael@0: * For example, here are some timings recorded on two devices: michael@0: * michael@0: * Device | EGLPreloadingThread::run() | GLController::initEGL() michael@0: * -----------------------+----------------------------+------------------------ michael@0: * Nexus S (Android 2.3) | ~ 80 ms | < 0.1 ms michael@0: * Nexus 10 (Android 4.3) | ~ 35 ms | < 0.1 ms michael@0: */ michael@0: class EGLPreloadingThread extends Thread michael@0: { michael@0: private static final String LOGTAG = "EGLPreloadingThread"; michael@0: private EGL10 mEGL; michael@0: private EGLDisplay mEGLDisplay; michael@0: michael@0: public EGLPreloadingThread() michael@0: { michael@0: } michael@0: michael@0: @Override michael@0: public void run() michael@0: { michael@0: mEGL = (EGL10)EGLContext.getEGL(); michael@0: mEGLDisplay = mEGL.eglGetDisplay(EGL10.EGL_DEFAULT_DISPLAY); michael@0: if (mEGLDisplay == EGL10.EGL_NO_DISPLAY) { michael@0: Log.w(LOGTAG, "Can't get EGL display!"); michael@0: return; michael@0: } michael@0: michael@0: int[] returnedVersion = new int[2]; michael@0: if (!mEGL.eglInitialize(mEGLDisplay, returnedVersion)) { michael@0: Log.w(LOGTAG, "eglInitialize failed"); michael@0: return; michael@0: } michael@0: } michael@0: } michael@0: michael@0: /** michael@0: * This class is a singleton that tracks EGL and compositor things over michael@0: * the lifetime of Fennec running. michael@0: * We only ever create one C++ compositor over Fennec's lifetime, but michael@0: * most of the Java-side objects (e.g. LayerView, GeckoLayerClient, michael@0: * LayerRenderer) can all get destroyed and re-created if the GeckoApp michael@0: * activity is destroyed. This GLController is never destroyed, so that michael@0: * the mCompositorCreated field and other state variables are always michael@0: * accurate. michael@0: */ michael@0: public class GLController { michael@0: private static final int EGL_CONTEXT_CLIENT_VERSION = 0x3098; michael@0: private static final String LOGTAG = "GeckoGLController"; michael@0: michael@0: private static GLController sInstance; michael@0: michael@0: private LayerView mView; michael@0: private boolean mServerSurfaceValid; michael@0: private int mWidth, mHeight; michael@0: michael@0: /* This is written by the compositor thread (while the UI thread michael@0: * is blocked on it) and read by the UI thread. */ michael@0: private volatile boolean mCompositorCreated; michael@0: michael@0: private EGL10 mEGL; michael@0: private EGLDisplay mEGLDisplay; michael@0: private EGLConfig mEGLConfig; michael@0: private EGLPreloadingThread mEGLPreloadingThread; michael@0: private EGLSurface mEGLSurfaceForCompositor; michael@0: michael@0: private static final int LOCAL_EGL_OPENGL_ES2_BIT = 4; michael@0: michael@0: private static final int[] CONFIG_SPEC_16BPP = { michael@0: EGL10.EGL_RED_SIZE, 5, michael@0: EGL10.EGL_GREEN_SIZE, 6, michael@0: EGL10.EGL_BLUE_SIZE, 5, michael@0: EGL10.EGL_SURFACE_TYPE, EGL10.EGL_WINDOW_BIT, michael@0: EGL10.EGL_RENDERABLE_TYPE, LOCAL_EGL_OPENGL_ES2_BIT, michael@0: EGL10.EGL_NONE michael@0: }; michael@0: michael@0: private static final int[] CONFIG_SPEC_24BPP = { michael@0: EGL10.EGL_RED_SIZE, 8, michael@0: EGL10.EGL_GREEN_SIZE, 8, michael@0: EGL10.EGL_BLUE_SIZE, 8, michael@0: EGL10.EGL_SURFACE_TYPE, EGL10.EGL_WINDOW_BIT, michael@0: EGL10.EGL_RENDERABLE_TYPE, LOCAL_EGL_OPENGL_ES2_BIT, michael@0: EGL10.EGL_NONE michael@0: }; michael@0: michael@0: private GLController() { michael@0: mEGLPreloadingThread = new EGLPreloadingThread(); michael@0: mEGLPreloadingThread.start(); michael@0: } michael@0: michael@0: static GLController getInstance(LayerView view) { michael@0: if (sInstance == null) { michael@0: sInstance = new GLController(); michael@0: } michael@0: sInstance.mView = view; michael@0: return sInstance; michael@0: } michael@0: michael@0: synchronized void serverSurfaceDestroyed() { michael@0: ThreadUtils.assertOnUiThread(); michael@0: michael@0: mServerSurfaceValid = false; michael@0: michael@0: if (mEGLSurfaceForCompositor != null) { michael@0: mEGL.eglDestroySurface(mEGLDisplay, mEGLSurfaceForCompositor); michael@0: mEGLSurfaceForCompositor = null; michael@0: } michael@0: michael@0: // We need to coordinate with Gecko when pausing composition, to ensure michael@0: // that Gecko never executes a draw event while the compositor is paused. michael@0: // This is sent synchronously to make sure that we don't attempt to use michael@0: // any outstanding Surfaces after we call this (such as from a michael@0: // serverSurfaceDestroyed notification), and to make sure that any in-flight michael@0: // Gecko draw events have been processed. When this returns, composition is michael@0: // definitely paused -- it'll synchronize with the Gecko event loop, which michael@0: // in turn will synchronize with the compositor thread. michael@0: if (mCompositorCreated) { michael@0: GeckoAppShell.sendEventToGeckoSync(GeckoEvent.createCompositorPauseEvent()); michael@0: } michael@0: } michael@0: michael@0: synchronized void serverSurfaceChanged(int newWidth, int newHeight) { michael@0: ThreadUtils.assertOnUiThread(); michael@0: michael@0: mWidth = newWidth; michael@0: mHeight = newHeight; michael@0: mServerSurfaceValid = true; michael@0: michael@0: // we defer to a runnable the task of updating the compositor, because this is going to michael@0: // call back into createEGLSurfaceForCompositor, which will try to create an EGLSurface michael@0: // against mView, which we suspect might fail if called too early. By posting this to michael@0: // mView, we hope to ensure that it is deferred until mView is actually "ready" for some michael@0: // sense of "ready". michael@0: mView.post(new Runnable() { michael@0: @Override michael@0: public void run() { michael@0: updateCompositor(); michael@0: } michael@0: }); michael@0: } michael@0: michael@0: void updateCompositor() { michael@0: ThreadUtils.assertOnUiThread(); michael@0: michael@0: if (mCompositorCreated) { michael@0: // If the compositor has already been created, just resume it instead. We don't need michael@0: // to block here because if the surface is destroyed before the compositor grabs it, michael@0: // we can handle that gracefully (i.e. the compositor will remain paused). michael@0: resumeCompositor(mWidth, mHeight); michael@0: return; michael@0: } michael@0: michael@0: if (!AttemptPreallocateEGLSurfaceForCompositor()) { michael@0: return; michael@0: } michael@0: michael@0: // Only try to create the compositor if we have a valid surface and gecko is up. When these michael@0: // two conditions are satisfied, we can be relatively sure that the compositor creation will michael@0: // happen without needing to block anyhwere. Do it with a sync gecko event so that the michael@0: // android doesn't have a chance to destroy our surface in between. michael@0: if (GeckoThread.checkLaunchState(GeckoThread.LaunchState.GeckoRunning)) { michael@0: GeckoAppShell.sendEventToGeckoSync(GeckoEvent.createCompositorCreateEvent(mWidth, mHeight)); michael@0: } michael@0: } michael@0: michael@0: void compositorCreated() { michael@0: // This is invoked on the compositor thread, while the java UI thread michael@0: // is blocked on the gecko sync event in updateCompositor() above michael@0: mCompositorCreated = true; michael@0: } michael@0: michael@0: public boolean isServerSurfaceValid() { michael@0: return mServerSurfaceValid; michael@0: } michael@0: michael@0: public boolean isCompositorCreated() { michael@0: return mCompositorCreated; michael@0: } michael@0: michael@0: private void initEGL() { michael@0: if (mEGL != null) { michael@0: return; michael@0: } michael@0: michael@0: // This join() should not be necessary, but makes this code a bit easier to think about. michael@0: // The EGLPreloadingThread should long be done by now, and even if it's not, michael@0: // it shouldn't be a problem to be initalizing EGL from two different threads. michael@0: // Still, having this join() here means that we don't have to wonder about what michael@0: // kind of caveats might exist with EGL initialization reentrancy on various drivers. michael@0: try { michael@0: mEGLPreloadingThread.join(); michael@0: } catch (InterruptedException e) { michael@0: Log.w(LOGTAG, "EGLPreloadingThread interrupted", e); michael@0: } michael@0: michael@0: mEGL = (EGL10)EGLContext.getEGL(); michael@0: michael@0: mEGLDisplay = mEGL.eglGetDisplay(EGL10.EGL_DEFAULT_DISPLAY); michael@0: if (mEGLDisplay == EGL10.EGL_NO_DISPLAY) { michael@0: Log.w(LOGTAG, "Can't get EGL display!"); michael@0: return; michael@0: } michael@0: michael@0: // while calling eglInitialize here should not be necessary as it was already called michael@0: // by the EGLPreloadingThread, it really doesn't cost much to call it again here, michael@0: // and makes this code easier to think about: EGLPreloadingThread is only a michael@0: // preloading optimization, not something we rely on for anything else. michael@0: // michael@0: // Also note that while calling eglInitialize isn't necessary on Android 4.x michael@0: // (at least Android's HardwareRenderer does it for us already), it is necessary michael@0: // on Android 2.x. michael@0: int[] returnedVersion = new int[2]; michael@0: if (!mEGL.eglInitialize(mEGLDisplay, returnedVersion)) { michael@0: Log.w(LOGTAG, "eglInitialize failed"); michael@0: return; michael@0: } michael@0: michael@0: mEGLConfig = chooseConfig(); michael@0: } michael@0: michael@0: private EGLConfig chooseConfig() { michael@0: int[] desiredConfig; michael@0: int rSize, gSize, bSize; michael@0: int[] numConfigs = new int[1]; michael@0: michael@0: switch (GeckoAppShell.getScreenDepth()) { michael@0: case 24: michael@0: desiredConfig = CONFIG_SPEC_24BPP; michael@0: rSize = gSize = bSize = 8; michael@0: break; michael@0: case 16: michael@0: default: michael@0: desiredConfig = CONFIG_SPEC_16BPP; michael@0: rSize = 5; gSize = 6; bSize = 5; michael@0: break; michael@0: } michael@0: michael@0: if (!mEGL.eglChooseConfig(mEGLDisplay, desiredConfig, null, 0, numConfigs) || michael@0: numConfigs[0] <= 0) { michael@0: throw new GLControllerException("No available EGL configurations " + michael@0: getEGLError()); michael@0: } michael@0: michael@0: EGLConfig[] configs = new EGLConfig[numConfigs[0]]; michael@0: if (!mEGL.eglChooseConfig(mEGLDisplay, desiredConfig, configs, numConfigs[0], numConfigs)) { michael@0: throw new GLControllerException("No EGL configuration for that specification " + michael@0: getEGLError()); michael@0: } michael@0: michael@0: // Select the first configuration that matches the screen depth. michael@0: int[] red = new int[1], green = new int[1], blue = new int[1]; michael@0: for (EGLConfig config : configs) { michael@0: mEGL.eglGetConfigAttrib(mEGLDisplay, config, EGL10.EGL_RED_SIZE, red); michael@0: mEGL.eglGetConfigAttrib(mEGLDisplay, config, EGL10.EGL_GREEN_SIZE, green); michael@0: mEGL.eglGetConfigAttrib(mEGLDisplay, config, EGL10.EGL_BLUE_SIZE, blue); michael@0: if (red[0] == rSize && green[0] == gSize && blue[0] == bSize) { michael@0: return config; michael@0: } michael@0: } michael@0: michael@0: throw new GLControllerException("No suitable EGL configuration found"); michael@0: } michael@0: michael@0: private synchronized boolean AttemptPreallocateEGLSurfaceForCompositor() { michael@0: if (mEGLSurfaceForCompositor == null) { michael@0: initEGL(); michael@0: try { michael@0: mEGLSurfaceForCompositor = mEGL.eglCreateWindowSurface(mEGLDisplay, mEGLConfig, mView.getNativeWindow(), null); michael@0: // In failure cases, eglCreateWindowSurface should return EGL_NO_SURFACE. michael@0: // We currently normalize this to null, and compare to null in all our checks. michael@0: if (mEGLSurfaceForCompositor == EGL10.EGL_NO_SURFACE) { michael@0: mEGLSurfaceForCompositor = null; michael@0: } michael@0: } catch (Exception e) { michael@0: Log.e(LOGTAG, "eglCreateWindowSurface threw", e); michael@0: } michael@0: } michael@0: if (mEGLSurfaceForCompositor == null) { michael@0: Log.w(LOGTAG, "eglCreateWindowSurface returned no surface!"); michael@0: } michael@0: return mEGLSurfaceForCompositor != null; michael@0: } michael@0: michael@0: @WrapElementForJNI(allowMultithread = true, stubName = "CreateEGLSurfaceForCompositorWrapper") michael@0: private synchronized EGLSurface createEGLSurfaceForCompositor() { michael@0: AttemptPreallocateEGLSurfaceForCompositor(); michael@0: EGLSurface result = mEGLSurfaceForCompositor; michael@0: mEGLSurfaceForCompositor = null; michael@0: return result; michael@0: } michael@0: michael@0: private String getEGLError() { michael@0: return "Error " + (mEGL == null ? "(no mEGL)" : mEGL.eglGetError()); michael@0: } michael@0: michael@0: void resumeCompositor(int width, int height) { michael@0: // Asking Gecko to resume the compositor takes too long (see michael@0: // https://bugzilla.mozilla.org/show_bug.cgi?id=735230#c23), so we michael@0: // resume the compositor directly. We still need to inform Gecko about michael@0: // the compositor resuming, so that Gecko knows that it can now draw. michael@0: // It is important to not notify Gecko until after the compositor has michael@0: // been resumed, otherwise Gecko may send updates that get dropped. michael@0: if (mCompositorCreated) { michael@0: GeckoAppShell.scheduleResumeComposition(width, height); michael@0: GeckoAppShell.sendEventToGecko(GeckoEvent.createCompositorResumeEvent()); michael@0: } michael@0: } michael@0: michael@0: public static class GLControllerException extends RuntimeException { michael@0: public static final long serialVersionUID = 1L; michael@0: michael@0: GLControllerException(String e) { michael@0: super(e); michael@0: } michael@0: } michael@0: }