summaryrefslogtreecommitdiffstats
path: root/android/experimental/LOAndroid/app/src/main/java/org/mozilla/gecko/gfx/GLController.java
blob: be6118894e6a8a45ead1777cd28b17af38d0d61c (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
 * This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */

package org.mozilla.gecko.gfx;

//import org.mozilla.gecko.GeckoAppShell;
//import org.mozilla.gecko.GeckoEvent;
//import org.mozilla.gecko.GeckoThread;
//import org.mozilla.gecko.mozglue.generatorannotations.WrapElementForJNI;
import org.libreoffice.LOKitShell;
import org.mozilla.gecko.util.ThreadUtils;

import android.util.Log;

import javax.microedition.khronos.egl.EGL10;
import javax.microedition.khronos.egl.EGLConfig;
import javax.microedition.khronos.egl.EGLContext;
import javax.microedition.khronos.egl.EGLDisplay;
import javax.microedition.khronos.egl.EGLSurface;

/**
 * EGLPreloadingThread is purely a preloading optimization, not something
 * we rely on for anything else than performance. We will be initializing
 * EGL in GLController::initEGL() when we need it, but having EGL initialization
 * already previously done by EGLPreloadingThread::run() will make it much
 * faster for GLController to do again.
 *
 * For example, here are some timings recorded on two devices:
 *
 * Device                 | EGLPreloadingThread::run() | GLController::initEGL()
 * -----------------------+----------------------------+------------------------
 * Nexus S (Android 2.3)  | ~ 80 ms                    | < 0.1 ms
 * Nexus 10 (Android 4.3) | ~ 35 ms                    | < 0.1 ms
 */
class EGLPreloadingThread extends Thread
{
    private static final String LOGTAG = "EGLPreloadingThread";
    private EGL10 mEGL;
    private EGLDisplay mEGLDisplay;

    public EGLPreloadingThread()
    {
    }

    @Override
    public void run()
    {
        mEGL = (EGL10)EGLContext.getEGL();
        mEGLDisplay = mEGL.eglGetDisplay(EGL10.EGL_DEFAULT_DISPLAY);
        if (mEGLDisplay == EGL10.EGL_NO_DISPLAY) {
            Log.w(LOGTAG, "Can't get EGL display!");
            return;
        }

        int[] returnedVersion = new int[2];
        if (!mEGL.eglInitialize(mEGLDisplay, returnedVersion)) {
            Log.w(LOGTAG, "eglInitialize failed");
            return;
        }
    }
}

/**
 * This class is a singleton that tracks EGL and compositor things over
 * the lifetime of Fennec running.
 * We only ever create one C++ compositor over Fennec's lifetime, but
 * most of the Java-side objects (e.g. LayerView, GeckoLayerClient,
 * LayerRenderer) can all get destroyed and re-created if the GeckoApp
 * activity is destroyed. This GLController is never destroyed, so that
 * the mCompositorCreated field and other state variables are always
 * accurate.
 */
public class GLController {
    private static final int EGL_CONTEXT_CLIENT_VERSION = 0x3098;
    private static final String LOGTAG = "GeckoGLController";

    private static GLController sInstance;

    private LayerView mView;
    private boolean mServerSurfaceValid;
    private int mWidth, mHeight;

    /* This is written by the compositor thread (while the UI thread
     * is blocked on it) and read by the UI thread. */
    private volatile boolean mCompositorCreated;

    private EGL10 mEGL;
    private EGLDisplay mEGLDisplay;
    private EGLConfig mEGLConfig;
    private EGLPreloadingThread mEGLPreloadingThread;
    private EGLSurface mEGLSurfaceForCompositor;

    private static final int LOCAL_EGL_OPENGL_ES2_BIT = 4;

    private static final int[] CONFIG_SPEC_16BPP = {
        EGL10.EGL_RED_SIZE, 5,
        EGL10.EGL_GREEN_SIZE, 6,
        EGL10.EGL_BLUE_SIZE, 5,
        EGL10.EGL_SURFACE_TYPE, EGL10.EGL_WINDOW_BIT,
        EGL10.EGL_RENDERABLE_TYPE, LOCAL_EGL_OPENGL_ES2_BIT,
        EGL10.EGL_NONE
    };

    private static final int[] CONFIG_SPEC_24BPP = {
        EGL10.EGL_RED_SIZE, 8,
        EGL10.EGL_GREEN_SIZE, 8,
        EGL10.EGL_BLUE_SIZE, 8,
        EGL10.EGL_SURFACE_TYPE, EGL10.EGL_WINDOW_BIT,
        EGL10.EGL_RENDERABLE_TYPE, LOCAL_EGL_OPENGL_ES2_BIT,
        EGL10.EGL_NONE
    };

    private GLController() {
        mEGLPreloadingThread = new EGLPreloadingThread();
        mEGLPreloadingThread.start();
    }

    static GLController getInstance(LayerView view) {
        if (sInstance == null) {
            sInstance = new GLController();
        }
        sInstance.mView = view;
        return sInstance;
    }

    synchronized void serverSurfaceDestroyed() {
        ThreadUtils.assertOnUiThread();
        Log.w(LOGTAG, "GLController::serverSurfaceDestroyed() with mCompositorCreated=" + mCompositorCreated);

        mServerSurfaceValid = false;

        if (mEGLSurfaceForCompositor != null) {
          mEGL.eglDestroySurface(mEGLDisplay, mEGLSurfaceForCompositor);
          mEGLSurfaceForCompositor = null;
        }

        // We need to coordinate with Gecko when pausing composition, to ensure
        // that Gecko never executes a draw event while the compositor is paused.
        // This is sent synchronously to make sure that we don't attempt to use
        // any outstanding Surfaces after we call this (such as from a
        // serverSurfaceDestroyed notification), and to make sure that any in-flight
        // Gecko draw events have been processed.  When this returns, composition is
        // definitely paused -- it'll synchronize with the Gecko event loop, which
        // in turn will synchronize with the compositor thread.
        if (mCompositorCreated) {
            //GeckoAppShell.sendEventToGeckoSync(GeckoEvent.createCompositorPauseEvent());
        }
        Log.w(LOGTAG, "done GLController::serverSurfaceDestroyed()");
    }

    synchronized void serverSurfaceChanged(int newWidth, int newHeight) {
        ThreadUtils.assertOnUiThread();
        Log.w(LOGTAG, "GLController::serverSurfaceChanged(" + newWidth + ", " + newHeight + ")");

        mWidth = newWidth;
        mHeight = newHeight;
        mServerSurfaceValid = true;

        // we defer to a runnable the task of updating the compositor, because this is going to
        // call back into createEGLSurfaceForCompositor, which will try to create an EGLSurface
        // against mView, which we suspect might fail if called too early. By posting this to
        // mView, we hope to ensure that it is deferred until mView is actually "ready" for some
        // sense of "ready".
        mView.post(new Runnable() {
            @Override
            public void run() {
                updateCompositor();
            }
        });
    }

    void updateCompositor() {
        ThreadUtils.assertOnUiThread();
        Log.w(LOGTAG, "GLController::updateCompositor with mCompositorCreated=" + mCompositorCreated);

        if (mCompositorCreated) {
            // If the compositor has already been created, just resume it instead. We don't need
            // to block here because if the surface is destroyed before the compositor grabs it,
            // we can handle that gracefully (i.e. the compositor will remain paused).
            resumeCompositor(mWidth, mHeight);
            Log.w(LOGTAG, "done GLController::updateCompositor with compositor resume");
            return;
        }

        if (!AttemptPreallocateEGLSurfaceForCompositor()) {
            return;
        }

        // Only try to create the compositor if we have a valid surface and gecko is up. When these
        // two conditions are satisfied, we can be relatively sure that the compositor creation will
        // happen without needing to block anywhere. Do it with a sync gecko event so that the
        // android doesn't have a chance to destroy our surface in between.
        /*if (GeckoThread.checkLaunchState(GeckoThread.LaunchState.GeckoRunning)) {
            GeckoAppShell.sendEventToGeckoSync(GeckoEvent.createCompositorCreateEvent(mWidth, mHeight));
        }*/
        Log.w(LOGTAG, "done GLController::updateCompositor");
    }

    void compositorCreated() {
        Log.w(LOGTAG, "GLController::compositorCreated");
        // This is invoked on the compositor thread, while the java UI thread
        // is blocked on the gecko sync event in updateCompositor() above
        mCompositorCreated = true;
    }

    public boolean isServerSurfaceValid() {
        return mServerSurfaceValid;
    }

    public boolean isCompositorCreated() {
        return mCompositorCreated;
    }

    private void initEGL() {
        if (mEGL != null) {
            return;
        }

        // This join() should not be necessary, but makes this code a bit easier to think about.
        // The EGLPreloadingThread should long be done by now, and even if it's not,
        // it shouldn't be a problem to be initalizing EGL from two different threads.
        // Still, having this join() here means that we don't have to wonder about what
        // kind of caveats might exist with EGL initialization reentrancy on various drivers.
        try {
            mEGLPreloadingThread.join();
        } catch (InterruptedException e) {
            Log.w(LOGTAG, "EGLPreloadingThread interrupted", e);
        }

        mEGL = (EGL10)EGLContext.getEGL();

        mEGLDisplay = mEGL.eglGetDisplay(EGL10.EGL_DEFAULT_DISPLAY);
        if (mEGLDisplay == EGL10.EGL_NO_DISPLAY) {
            Log.w(LOGTAG, "Can't get EGL display!");
            return;
        }

        // while calling eglInitialize here should not be necessary as it was already called
        // by the EGLPreloadingThread, it really doesn't cost much to call it again here,
        // and makes this code easier to think about: EGLPreloadingThread is only a
        // preloading optimization, not something we rely on for anything else.
        //
        // Also note that while calling eglInitialize isn't necessary on Android 4.x
        // (at least Android's HardwareRenderer does it for us already), it is necessary
        // on Android 2.x.
        int[] returnedVersion = new int[2];
        if (!mEGL.eglInitialize(mEGLDisplay, returnedVersion)) {
            Log.w(LOGTAG, "eglInitialize failed");
            return;
        }

        mEGLConfig = chooseConfig();
    }

    private EGLConfig chooseConfig() {
        int[] desiredConfig;
        int rSize, gSize, bSize;
        int[] numConfigs = new int[1];

        switch (/*GeckoAppShell*/LOKitShell.getScreenDepth()) {
        case 24:
            desiredConfig = CONFIG_SPEC_24BPP;
            rSize = gSize = bSize = 8;
            break;
        case 16:
        default:
            desiredConfig = CONFIG_SPEC_16BPP;
            rSize = 5; gSize = 6; bSize = 5;
            break;
        }

        if (!mEGL.eglChooseConfig(mEGLDisplay, desiredConfig, null, 0, numConfigs) ||
                numConfigs[0] <= 0) {
            throw new GLControllerException("No available EGL configurations " +
                                            getEGLError());
        }

        EGLConfig[] configs = new EGLConfig[numConfigs[0]];
        if (!mEGL.eglChooseConfig(mEGLDisplay, desiredConfig, configs, numConfigs[0], numConfigs)) {
            throw new GLControllerException("No EGL configuration for that specification " +
                                            getEGLError());
        }

        // Select the first configuration that matches the screen depth.
        int[] red = new int[1], green = new int[1], blue = new int[1];
        for (EGLConfig config : configs) {
            mEGL.eglGetConfigAttrib(mEGLDisplay, config, EGL10.EGL_RED_SIZE, red);
            mEGL.eglGetConfigAttrib(mEGLDisplay, config, EGL10.EGL_GREEN_SIZE, green);
            mEGL.eglGetConfigAttrib(mEGLDisplay, config, EGL10.EGL_BLUE_SIZE, blue);
            if (red[0] == rSize && green[0] == gSize && blue[0] == bSize) {
                return config;
            }
        }

        throw new GLControllerException("No suitable EGL configuration found");
    }

    private synchronized boolean AttemptPreallocateEGLSurfaceForCompositor() {
        if (mEGLSurfaceForCompositor == null) {
            initEGL();
            try {
                mEGLSurfaceForCompositor = mEGL.eglCreateWindowSurface(mEGLDisplay, mEGLConfig, mView.getNativeWindow(), null);
                // In failure cases, eglCreateWindowSurface should return EGL_NO_SURFACE.
                // We currently normalize this to null, and compare to null in all our checks.
                if (mEGLSurfaceForCompositor == EGL10.EGL_NO_SURFACE) {
                    mEGLSurfaceForCompositor = null;
                }
            } catch (Exception e) {
                Log.e(LOGTAG, "eglCreateWindowSurface threw", e);
            }
        }
        if (mEGLSurfaceForCompositor == null) {
            Log.w(LOGTAG, "eglCreateWindowSurface returned no surface!");
        }
        return mEGLSurfaceForCompositor != null;
    }

   // @WrapElementForJNI(allowMultithread = true, stubName = "CreateEGLSurfaceForCompositorWrapper")
    private synchronized EGLSurface createEGLSurfaceForCompositor() {
        AttemptPreallocateEGLSurfaceForCompositor();
        EGLSurface result = mEGLSurfaceForCompositor;
        mEGLSurfaceForCompositor = null;
        return result;
    }

    private String getEGLError() {
        return "Error " + (mEGL == null ? "(no mEGL)" : mEGL.eglGetError());
    }

    void resumeCompositor(int width, int height) {
        Log.w(LOGTAG, "GLController::resumeCompositor(" + width + ", " + height + ") and mCompositorCreated=" + mCompositorCreated);
        // Asking Gecko to resume the compositor takes too long (see
        // https://bugzilla.mozilla.org/show_bug.cgi?id=735230#c23), so we
        // resume the compositor directly. We still need to inform Gecko about
        // the compositor resuming, so that Gecko knows that it can now draw.
        // It is important to not notify Gecko until after the compositor has
        // been resumed, otherwise Gecko may send updates that get dropped.
        if (mCompositorCreated) {
            //GeckoAppShell.scheduleResumeComposition(width, height);
            //GeckoAppShell.sendEventToGecko(GeckoEvent.createCompositorResumeEvent());
        }
        Log.w(LOGTAG, "done GLController::resumeCompositor");
    }

    public static class GLControllerException extends RuntimeException {
        public static final long serialVersionUID = 1L;

        GLControllerException(String e) {
            super(e);
        }
    }
}