mobile/android/base/gfx/GLController.java

changeset 0
6474c204b198
     1.1 --- /dev/null	Thu Jan 01 00:00:00 1970 +0000
     1.2 +++ b/mobile/android/base/gfx/GLController.java	Wed Dec 31 06:09:35 2014 +0100
     1.3 @@ -0,0 +1,344 @@
     1.4 +/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
     1.5 + * This Source Code Form is subject to the terms of the Mozilla Public
     1.6 + * License, v. 2.0. If a copy of the MPL was not distributed with this
     1.7 + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
     1.8 +
     1.9 +package org.mozilla.gecko.gfx;
    1.10 +
    1.11 +import org.mozilla.gecko.GeckoAppShell;
    1.12 +import org.mozilla.gecko.GeckoEvent;
    1.13 +import org.mozilla.gecko.GeckoThread;
    1.14 +import org.mozilla.gecko.mozglue.generatorannotations.WrapElementForJNI;
    1.15 +import org.mozilla.gecko.util.ThreadUtils;
    1.16 +
    1.17 +import android.util.Log;
    1.18 +
    1.19 +import javax.microedition.khronos.egl.EGL10;
    1.20 +import javax.microedition.khronos.egl.EGLConfig;
    1.21 +import javax.microedition.khronos.egl.EGLContext;
    1.22 +import javax.microedition.khronos.egl.EGLDisplay;
    1.23 +import javax.microedition.khronos.egl.EGLSurface;
    1.24 +
    1.25 +/**
    1.26 + * EGLPreloadingThread is purely a preloading optimization, not something
    1.27 + * we rely on for anything else than performance. We will be initializing
    1.28 + * EGL in GLController::initEGL() when we need it, but having EGL initialization
    1.29 + * already previously done by EGLPreloadingThread::run() will make it much
    1.30 + * faster for GLController to do again.
    1.31 + *
    1.32 + * For example, here are some timings recorded on two devices:
    1.33 + *
    1.34 + * Device                 | EGLPreloadingThread::run() | GLController::initEGL()
    1.35 + * -----------------------+----------------------------+------------------------
    1.36 + * Nexus S (Android 2.3)  | ~ 80 ms                    | < 0.1 ms
    1.37 + * Nexus 10 (Android 4.3) | ~ 35 ms                    | < 0.1 ms
    1.38 + */
    1.39 +class EGLPreloadingThread extends Thread
    1.40 +{
    1.41 +    private static final String LOGTAG = "EGLPreloadingThread";
    1.42 +    private EGL10 mEGL;
    1.43 +    private EGLDisplay mEGLDisplay;
    1.44 +
    1.45 +    public EGLPreloadingThread()
    1.46 +    {
    1.47 +    }
    1.48 +
    1.49 +    @Override
    1.50 +    public void run()
    1.51 +    {
    1.52 +        mEGL = (EGL10)EGLContext.getEGL();
    1.53 +        mEGLDisplay = mEGL.eglGetDisplay(EGL10.EGL_DEFAULT_DISPLAY);
    1.54 +        if (mEGLDisplay == EGL10.EGL_NO_DISPLAY) {
    1.55 +            Log.w(LOGTAG, "Can't get EGL display!");
    1.56 +            return;
    1.57 +        }
    1.58 +
    1.59 +        int[] returnedVersion = new int[2];
    1.60 +        if (!mEGL.eglInitialize(mEGLDisplay, returnedVersion)) {
    1.61 +            Log.w(LOGTAG, "eglInitialize failed");
    1.62 +            return;
    1.63 +        }
    1.64 +    }
    1.65 +}
    1.66 +
    1.67 +/**
    1.68 + * This class is a singleton that tracks EGL and compositor things over
    1.69 + * the lifetime of Fennec running.
    1.70 + * We only ever create one C++ compositor over Fennec's lifetime, but
    1.71 + * most of the Java-side objects (e.g. LayerView, GeckoLayerClient,
    1.72 + * LayerRenderer) can all get destroyed and re-created if the GeckoApp
    1.73 + * activity is destroyed. This GLController is never destroyed, so that
    1.74 + * the mCompositorCreated field and other state variables are always
    1.75 + * accurate.
    1.76 + */
    1.77 +public class GLController {
    1.78 +    private static final int EGL_CONTEXT_CLIENT_VERSION = 0x3098;
    1.79 +    private static final String LOGTAG = "GeckoGLController";
    1.80 +
    1.81 +    private static GLController sInstance;
    1.82 +
    1.83 +    private LayerView mView;
    1.84 +    private boolean mServerSurfaceValid;
    1.85 +    private int mWidth, mHeight;
    1.86 +
    1.87 +    /* This is written by the compositor thread (while the UI thread
    1.88 +     * is blocked on it) and read by the UI thread. */
    1.89 +    private volatile boolean mCompositorCreated;
    1.90 +
    1.91 +    private EGL10 mEGL;
    1.92 +    private EGLDisplay mEGLDisplay;
    1.93 +    private EGLConfig mEGLConfig;
    1.94 +    private EGLPreloadingThread mEGLPreloadingThread;
    1.95 +    private EGLSurface mEGLSurfaceForCompositor;
    1.96 +
    1.97 +    private static final int LOCAL_EGL_OPENGL_ES2_BIT = 4;
    1.98 +
    1.99 +    private static final int[] CONFIG_SPEC_16BPP = {
   1.100 +        EGL10.EGL_RED_SIZE, 5,
   1.101 +        EGL10.EGL_GREEN_SIZE, 6,
   1.102 +        EGL10.EGL_BLUE_SIZE, 5,
   1.103 +        EGL10.EGL_SURFACE_TYPE, EGL10.EGL_WINDOW_BIT,
   1.104 +        EGL10.EGL_RENDERABLE_TYPE, LOCAL_EGL_OPENGL_ES2_BIT,
   1.105 +        EGL10.EGL_NONE
   1.106 +    };
   1.107 +
   1.108 +    private static final int[] CONFIG_SPEC_24BPP = {
   1.109 +        EGL10.EGL_RED_SIZE, 8,
   1.110 +        EGL10.EGL_GREEN_SIZE, 8,
   1.111 +        EGL10.EGL_BLUE_SIZE, 8,
   1.112 +        EGL10.EGL_SURFACE_TYPE, EGL10.EGL_WINDOW_BIT,
   1.113 +        EGL10.EGL_RENDERABLE_TYPE, LOCAL_EGL_OPENGL_ES2_BIT,
   1.114 +        EGL10.EGL_NONE
   1.115 +    };
   1.116 +
   1.117 +    private GLController() {
   1.118 +        mEGLPreloadingThread = new EGLPreloadingThread();
   1.119 +        mEGLPreloadingThread.start();
   1.120 +    }
   1.121 +
   1.122 +    static GLController getInstance(LayerView view) {
   1.123 +        if (sInstance == null) {
   1.124 +            sInstance = new GLController();
   1.125 +        }
   1.126 +        sInstance.mView = view;
   1.127 +        return sInstance;
   1.128 +    }
   1.129 +
   1.130 +    synchronized void serverSurfaceDestroyed() {
   1.131 +        ThreadUtils.assertOnUiThread();
   1.132 +
   1.133 +        mServerSurfaceValid = false;
   1.134 +
   1.135 +        if (mEGLSurfaceForCompositor != null) {
   1.136 +          mEGL.eglDestroySurface(mEGLDisplay, mEGLSurfaceForCompositor);
   1.137 +          mEGLSurfaceForCompositor = null;
   1.138 +        }
   1.139 +
   1.140 +        // We need to coordinate with Gecko when pausing composition, to ensure
   1.141 +        // that Gecko never executes a draw event while the compositor is paused.
   1.142 +        // This is sent synchronously to make sure that we don't attempt to use
   1.143 +        // any outstanding Surfaces after we call this (such as from a
   1.144 +        // serverSurfaceDestroyed notification), and to make sure that any in-flight
   1.145 +        // Gecko draw events have been processed.  When this returns, composition is
   1.146 +        // definitely paused -- it'll synchronize with the Gecko event loop, which
   1.147 +        // in turn will synchronize with the compositor thread.
   1.148 +        if (mCompositorCreated) {
   1.149 +            GeckoAppShell.sendEventToGeckoSync(GeckoEvent.createCompositorPauseEvent());
   1.150 +        }
   1.151 +    }
   1.152 +
   1.153 +    synchronized void serverSurfaceChanged(int newWidth, int newHeight) {
   1.154 +        ThreadUtils.assertOnUiThread();
   1.155 +
   1.156 +        mWidth = newWidth;
   1.157 +        mHeight = newHeight;
   1.158 +        mServerSurfaceValid = true;
   1.159 +
   1.160 +        // we defer to a runnable the task of updating the compositor, because this is going to
   1.161 +        // call back into createEGLSurfaceForCompositor, which will try to create an EGLSurface
   1.162 +        // against mView, which we suspect might fail if called too early. By posting this to
   1.163 +        // mView, we hope to ensure that it is deferred until mView is actually "ready" for some
   1.164 +        // sense of "ready".
   1.165 +        mView.post(new Runnable() {
   1.166 +            @Override
   1.167 +            public void run() {
   1.168 +                updateCompositor();
   1.169 +            }
   1.170 +        });
   1.171 +    }
   1.172 +
   1.173 +    void updateCompositor() {
   1.174 +        ThreadUtils.assertOnUiThread();
   1.175 +
   1.176 +        if (mCompositorCreated) {
   1.177 +            // If the compositor has already been created, just resume it instead. We don't need
   1.178 +            // to block here because if the surface is destroyed before the compositor grabs it,
   1.179 +            // we can handle that gracefully (i.e. the compositor will remain paused).
   1.180 +            resumeCompositor(mWidth, mHeight);
   1.181 +            return;
   1.182 +        }
   1.183 +
   1.184 +        if (!AttemptPreallocateEGLSurfaceForCompositor()) {
   1.185 +            return;
   1.186 +        }
   1.187 +
   1.188 +        // Only try to create the compositor if we have a valid surface and gecko is up. When these
   1.189 +        // two conditions are satisfied, we can be relatively sure that the compositor creation will
   1.190 +        // happen without needing to block anyhwere. Do it with a sync gecko event so that the
   1.191 +        // android doesn't have a chance to destroy our surface in between.
   1.192 +        if (GeckoThread.checkLaunchState(GeckoThread.LaunchState.GeckoRunning)) {
   1.193 +            GeckoAppShell.sendEventToGeckoSync(GeckoEvent.createCompositorCreateEvent(mWidth, mHeight));
   1.194 +        }
   1.195 +    }
   1.196 +
   1.197 +    void compositorCreated() {
   1.198 +        // This is invoked on the compositor thread, while the java UI thread
   1.199 +        // is blocked on the gecko sync event in updateCompositor() above
   1.200 +        mCompositorCreated = true;
   1.201 +    }
   1.202 +
   1.203 +    public boolean isServerSurfaceValid() {
   1.204 +        return mServerSurfaceValid;
   1.205 +    }
   1.206 +
   1.207 +    public boolean isCompositorCreated() {
   1.208 +        return mCompositorCreated;
   1.209 +    }
   1.210 +
   1.211 +    private void initEGL() {
   1.212 +        if (mEGL != null) {
   1.213 +            return;
   1.214 +        }
   1.215 +
   1.216 +        // This join() should not be necessary, but makes this code a bit easier to think about.
   1.217 +        // The EGLPreloadingThread should long be done by now, and even if it's not,
   1.218 +        // it shouldn't be a problem to be initalizing EGL from two different threads.
   1.219 +        // Still, having this join() here means that we don't have to wonder about what
   1.220 +        // kind of caveats might exist with EGL initialization reentrancy on various drivers.
   1.221 +        try {
   1.222 +            mEGLPreloadingThread.join();
   1.223 +        } catch (InterruptedException e) {
   1.224 +            Log.w(LOGTAG, "EGLPreloadingThread interrupted", e);
   1.225 +        }
   1.226 +
   1.227 +        mEGL = (EGL10)EGLContext.getEGL();
   1.228 +
   1.229 +        mEGLDisplay = mEGL.eglGetDisplay(EGL10.EGL_DEFAULT_DISPLAY);
   1.230 +        if (mEGLDisplay == EGL10.EGL_NO_DISPLAY) {
   1.231 +            Log.w(LOGTAG, "Can't get EGL display!");
   1.232 +            return;
   1.233 +        }
   1.234 +
   1.235 +        // while calling eglInitialize here should not be necessary as it was already called
   1.236 +        // by the EGLPreloadingThread, it really doesn't cost much to call it again here,
   1.237 +        // and makes this code easier to think about: EGLPreloadingThread is only a
   1.238 +        // preloading optimization, not something we rely on for anything else.
   1.239 +        //
   1.240 +        // Also note that while calling eglInitialize isn't necessary on Android 4.x
   1.241 +        // (at least Android's HardwareRenderer does it for us already), it is necessary
   1.242 +        // on Android 2.x.
   1.243 +        int[] returnedVersion = new int[2];
   1.244 +        if (!mEGL.eglInitialize(mEGLDisplay, returnedVersion)) {
   1.245 +            Log.w(LOGTAG, "eglInitialize failed");
   1.246 +            return;
   1.247 +        }
   1.248 +
   1.249 +        mEGLConfig = chooseConfig();
   1.250 +    }
   1.251 +
   1.252 +    private EGLConfig chooseConfig() {
   1.253 +        int[] desiredConfig;
   1.254 +        int rSize, gSize, bSize;
   1.255 +        int[] numConfigs = new int[1];
   1.256 +
   1.257 +        switch (GeckoAppShell.getScreenDepth()) {
   1.258 +        case 24:
   1.259 +            desiredConfig = CONFIG_SPEC_24BPP;
   1.260 +            rSize = gSize = bSize = 8;
   1.261 +            break;
   1.262 +        case 16:
   1.263 +        default:
   1.264 +            desiredConfig = CONFIG_SPEC_16BPP;
   1.265 +            rSize = 5; gSize = 6; bSize = 5;
   1.266 +            break;
   1.267 +        }
   1.268 +
   1.269 +        if (!mEGL.eglChooseConfig(mEGLDisplay, desiredConfig, null, 0, numConfigs) ||
   1.270 +                numConfigs[0] <= 0) {
   1.271 +            throw new GLControllerException("No available EGL configurations " +
   1.272 +                                            getEGLError());
   1.273 +        }
   1.274 +
   1.275 +        EGLConfig[] configs = new EGLConfig[numConfigs[0]];
   1.276 +        if (!mEGL.eglChooseConfig(mEGLDisplay, desiredConfig, configs, numConfigs[0], numConfigs)) {
   1.277 +            throw new GLControllerException("No EGL configuration for that specification " +
   1.278 +                                            getEGLError());
   1.279 +        }
   1.280 +
   1.281 +        // Select the first configuration that matches the screen depth.
   1.282 +        int[] red = new int[1], green = new int[1], blue = new int[1];
   1.283 +        for (EGLConfig config : configs) {
   1.284 +            mEGL.eglGetConfigAttrib(mEGLDisplay, config, EGL10.EGL_RED_SIZE, red);
   1.285 +            mEGL.eglGetConfigAttrib(mEGLDisplay, config, EGL10.EGL_GREEN_SIZE, green);
   1.286 +            mEGL.eglGetConfigAttrib(mEGLDisplay, config, EGL10.EGL_BLUE_SIZE, blue);
   1.287 +            if (red[0] == rSize && green[0] == gSize && blue[0] == bSize) {
   1.288 +                return config;
   1.289 +            }
   1.290 +        }
   1.291 +
   1.292 +        throw new GLControllerException("No suitable EGL configuration found");
   1.293 +    }
   1.294 +
   1.295 +    private synchronized boolean AttemptPreallocateEGLSurfaceForCompositor() {
   1.296 +        if (mEGLSurfaceForCompositor == null) {
   1.297 +            initEGL();
   1.298 +            try {
   1.299 +                mEGLSurfaceForCompositor = mEGL.eglCreateWindowSurface(mEGLDisplay, mEGLConfig, mView.getNativeWindow(), null);
   1.300 +                // In failure cases, eglCreateWindowSurface should return EGL_NO_SURFACE.
   1.301 +                // We currently normalize this to null, and compare to null in all our checks.
   1.302 +                if (mEGLSurfaceForCompositor == EGL10.EGL_NO_SURFACE) {
   1.303 +                    mEGLSurfaceForCompositor = null;
   1.304 +                }
   1.305 +            } catch (Exception e) {
   1.306 +                Log.e(LOGTAG, "eglCreateWindowSurface threw", e);
   1.307 +            }
   1.308 +        }
   1.309 +        if (mEGLSurfaceForCompositor == null) {
   1.310 +            Log.w(LOGTAG, "eglCreateWindowSurface returned no surface!");
   1.311 +        }
   1.312 +        return mEGLSurfaceForCompositor != null;
   1.313 +    }
   1.314 +
   1.315 +    @WrapElementForJNI(allowMultithread = true, stubName = "CreateEGLSurfaceForCompositorWrapper")
   1.316 +    private synchronized EGLSurface createEGLSurfaceForCompositor() {
   1.317 +        AttemptPreallocateEGLSurfaceForCompositor();
   1.318 +        EGLSurface result = mEGLSurfaceForCompositor;
   1.319 +        mEGLSurfaceForCompositor = null;
   1.320 +        return result;
   1.321 +    }
   1.322 +
   1.323 +    private String getEGLError() {
   1.324 +        return "Error " + (mEGL == null ? "(no mEGL)" : mEGL.eglGetError());
   1.325 +    }
   1.326 +
   1.327 +    void resumeCompositor(int width, int height) {
   1.328 +        // Asking Gecko to resume the compositor takes too long (see
   1.329 +        // https://bugzilla.mozilla.org/show_bug.cgi?id=735230#c23), so we
   1.330 +        // resume the compositor directly. We still need to inform Gecko about
   1.331 +        // the compositor resuming, so that Gecko knows that it can now draw.
   1.332 +        // It is important to not notify Gecko until after the compositor has
   1.333 +        // been resumed, otherwise Gecko may send updates that get dropped.
   1.334 +        if (mCompositorCreated) {
   1.335 +            GeckoAppShell.scheduleResumeComposition(width, height);
   1.336 +            GeckoAppShell.sendEventToGecko(GeckoEvent.createCompositorResumeEvent());
   1.337 +        }
   1.338 +    }
   1.339 +
   1.340 +    public static class GLControllerException extends RuntimeException {
   1.341 +        public static final long serialVersionUID = 1L;
   1.342 +
   1.343 +        GLControllerException(String e) {
   1.344 +            super(e);
   1.345 +        }
   1.346 +    }
   1.347 +}

mercurial