1/*
2 * Copyright (C) 2013 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 *      http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17package com.android.photos.views;
18
19import android.content.Context;
20import android.graphics.SurfaceTexture;
21import android.opengl.GLSurfaceView.Renderer;
22import android.opengl.GLUtils;
23import android.util.Log;
24import android.view.TextureView;
25import android.view.TextureView.SurfaceTextureListener;
26
27import javax.microedition.khronos.egl.EGL10;
28import javax.microedition.khronos.egl.EGLConfig;
29import javax.microedition.khronos.egl.EGLContext;
30import javax.microedition.khronos.egl.EGLDisplay;
31import javax.microedition.khronos.egl.EGLSurface;
32import javax.microedition.khronos.opengles.GL10;
33
34/**
35 * A TextureView that supports blocking rendering for synchronous drawing
36 */
37public class BlockingGLTextureView extends TextureView
38        implements SurfaceTextureListener {
39
40    private RenderThread mRenderThread;
41
42    public BlockingGLTextureView(Context context) {
43        super(context);
44        setSurfaceTextureListener(this);
45    }
46
47    public void setRenderer(Renderer renderer) {
48        if (mRenderThread != null) {
49            throw new IllegalArgumentException("Renderer already set");
50        }
51        mRenderThread = new RenderThread(renderer);
52    }
53
54    public void render() {
55        mRenderThread.render();
56    }
57
58    public void destroy() {
59        if (mRenderThread != null) {
60            mRenderThread.finish();
61            mRenderThread = null;
62        }
63    }
64
65    @Override
66    public void onSurfaceTextureAvailable(SurfaceTexture surface, int width,
67            int height) {
68        mRenderThread.setSurface(surface);
69        mRenderThread.setSize(width, height);
70    }
71
72    @Override
73    public void onSurfaceTextureSizeChanged(SurfaceTexture surface, int width,
74            int height) {
75        mRenderThread.setSize(width, height);
76    }
77
78    @Override
79    public boolean onSurfaceTextureDestroyed(SurfaceTexture surface) {
80        if (mRenderThread != null) {
81            mRenderThread.setSurface(null);
82        }
83        return false;
84    }
85
86    @Override
87    public void onSurfaceTextureUpdated(SurfaceTexture surface) {
88    }
89
90    @Override
91    protected void finalize() throws Throwable {
92        try {
93            destroy();
94        } catch (Throwable t) {
95            // Ignore
96        }
97        super.finalize();
98    }
99
100    /**
101     * An EGL helper class.
102     */
103
104    private static class EglHelper {
105        private static final int EGL_CONTEXT_CLIENT_VERSION = 0x3098;
106        private static final int EGL_OPENGL_ES2_BIT = 4;
107
108        EGL10 mEgl;
109        EGLDisplay mEglDisplay;
110        EGLSurface mEglSurface;
111        EGLConfig mEglConfig;
112        EGLContext mEglContext;
113
114        private EGLConfig chooseEglConfig() {
115            int[] configsCount = new int[1];
116            EGLConfig[] configs = new EGLConfig[1];
117            int[] configSpec = getConfig();
118            if (!mEgl.eglChooseConfig(mEglDisplay, configSpec, configs, 1, configsCount)) {
119                throw new IllegalArgumentException("eglChooseConfig failed " +
120                        GLUtils.getEGLErrorString(mEgl.eglGetError()));
121            } else if (configsCount[0] > 0) {
122                return configs[0];
123            }
124            return null;
125        }
126
127        private static int[] getConfig() {
128            return new int[] {
129                    EGL10.EGL_RENDERABLE_TYPE, EGL_OPENGL_ES2_BIT,
130                    EGL10.EGL_RED_SIZE, 8,
131                    EGL10.EGL_GREEN_SIZE, 8,
132                    EGL10.EGL_BLUE_SIZE, 8,
133                    EGL10.EGL_ALPHA_SIZE, 8,
134                    EGL10.EGL_DEPTH_SIZE, 0,
135                    EGL10.EGL_STENCIL_SIZE, 0,
136                    EGL10.EGL_NONE
137            };
138        }
139
140        EGLContext createContext(EGL10 egl, EGLDisplay eglDisplay, EGLConfig eglConfig) {
141            int[] attribList = { EGL_CONTEXT_CLIENT_VERSION, 2, EGL10.EGL_NONE };
142            return egl.eglCreateContext(eglDisplay, eglConfig, EGL10.EGL_NO_CONTEXT, attribList);
143        }
144
145        /**
146         * Initialize EGL for a given configuration spec.
147         */
148        public void start() {
149            /*
150             * Get an EGL instance
151             */
152            mEgl = (EGL10) EGLContext.getEGL();
153
154            /*
155             * Get to the default display.
156             */
157            mEglDisplay = mEgl.eglGetDisplay(EGL10.EGL_DEFAULT_DISPLAY);
158
159            if (mEglDisplay == EGL10.EGL_NO_DISPLAY) {
160                throw new RuntimeException("eglGetDisplay failed");
161            }
162
163            /*
164             * We can now initialize EGL for that display
165             */
166            int[] version = new int[2];
167            if (!mEgl.eglInitialize(mEglDisplay, version)) {
168                throw new RuntimeException("eglInitialize failed");
169            }
170            mEglConfig = chooseEglConfig();
171
172            /*
173            * Create an EGL context. We want to do this as rarely as we can, because an
174            * EGL context is a somewhat heavy object.
175            */
176            mEglContext = createContext(mEgl, mEglDisplay, mEglConfig);
177
178            if (mEglContext == null || mEglContext == EGL10.EGL_NO_CONTEXT) {
179                mEglContext = null;
180                throwEglException("createContext");
181            }
182
183            mEglSurface = null;
184        }
185
186        /**
187         * Create an egl surface for the current SurfaceTexture surface. If a surface
188         * already exists, destroy it before creating the new surface.
189         *
190         * @return true if the surface was created successfully.
191         */
192        public boolean createSurface(SurfaceTexture surface) {
193            /*
194             * Check preconditions.
195             */
196            if (mEgl == null) {
197                throw new RuntimeException("egl not initialized");
198            }
199            if (mEglDisplay == null) {
200                throw new RuntimeException("eglDisplay not initialized");
201            }
202            if (mEglConfig == null) {
203                throw new RuntimeException("mEglConfig not initialized");
204            }
205
206            /*
207             *  The window size has changed, so we need to create a new
208             *  surface.
209             */
210            destroySurfaceImp();
211
212            /*
213             * Create an EGL surface we can render into.
214             */
215            if (surface != null) {
216                mEglSurface = mEgl.eglCreateWindowSurface(mEglDisplay, mEglConfig, surface, null);
217            } else {
218                mEglSurface = null;
219            }
220
221            if (mEglSurface == null || mEglSurface == EGL10.EGL_NO_SURFACE) {
222                int error = mEgl.eglGetError();
223                if (error == EGL10.EGL_BAD_NATIVE_WINDOW) {
224                    Log.e("EglHelper", "createWindowSurface returned EGL_BAD_NATIVE_WINDOW.");
225                }
226                return false;
227            }
228
229            /*
230             * Before we can issue GL commands, we need to make sure
231             * the context is current and bound to a surface.
232             */
233            if (!mEgl.eglMakeCurrent(mEglDisplay, mEglSurface, mEglSurface, mEglContext)) {
234                /*
235                 * Could not make the context current, probably because the underlying
236                 * SurfaceView surface has been destroyed.
237                 */
238                logEglErrorAsWarning("EGLHelper", "eglMakeCurrent", mEgl.eglGetError());
239                return false;
240            }
241
242            return true;
243        }
244
245        /**
246         * Create a GL object for the current EGL context.
247         */
248        public GL10 createGL() {
249            return (GL10) mEglContext.getGL();
250        }
251
252        /**
253         * Display the current render surface.
254         * @return the EGL error code from eglSwapBuffers.
255         */
256        public int swap() {
257            if (!mEgl.eglSwapBuffers(mEglDisplay, mEglSurface)) {
258                return mEgl.eglGetError();
259            }
260            return EGL10.EGL_SUCCESS;
261        }
262
263        public void destroySurface() {
264            destroySurfaceImp();
265        }
266
267        private void destroySurfaceImp() {
268            if (mEglSurface != null && mEglSurface != EGL10.EGL_NO_SURFACE) {
269                mEgl.eglMakeCurrent(mEglDisplay, EGL10.EGL_NO_SURFACE,
270                        EGL10.EGL_NO_SURFACE,
271                        EGL10.EGL_NO_CONTEXT);
272                mEgl.eglDestroySurface(mEglDisplay, mEglSurface);
273                mEglSurface = null;
274            }
275        }
276
277        public void finish() {
278            if (mEglContext != null) {
279                mEgl.eglDestroyContext(mEglDisplay, mEglContext);
280                mEglContext = null;
281            }
282            if (mEglDisplay != null) {
283                mEgl.eglTerminate(mEglDisplay);
284                mEglDisplay = null;
285            }
286        }
287
288        private void throwEglException(String function) {
289            throwEglException(function, mEgl.eglGetError());
290        }
291
292        public static void throwEglException(String function, int error) {
293            String message = formatEglError(function, error);
294            throw new RuntimeException(message);
295        }
296
297        public static void logEglErrorAsWarning(String tag, String function, int error) {
298            Log.w(tag, formatEglError(function, error));
299        }
300
301        public static String formatEglError(String function, int error) {
302            return function + " failed: " + error;
303        }
304
305    }
306
307    private static class RenderThread extends Thread {
308        private static final int INVALID = -1;
309        private static final int RENDER = 1;
310        private static final int CHANGE_SURFACE = 2;
311        private static final int RESIZE_SURFACE = 3;
312        private static final int FINISH = 4;
313
314        private EglHelper mEglHelper = new EglHelper();
315
316        private Object mLock = new Object();
317        private int mExecMsgId = INVALID;
318        private SurfaceTexture mSurface;
319        private Renderer mRenderer;
320        private int mWidth, mHeight;
321
322        private boolean mFinished = false;
323        private GL10 mGL;
324
325        public RenderThread(Renderer renderer) {
326            super("RenderThread");
327            mRenderer = renderer;
328            start();
329        }
330
331        private void checkRenderer() {
332            if (mRenderer == null) {
333                throw new IllegalArgumentException("Renderer is null!");
334            }
335        }
336
337        private void checkSurface() {
338            if (mSurface == null) {
339                throw new IllegalArgumentException("surface is null!");
340            }
341        }
342
343        public void setSurface(SurfaceTexture surface) {
344            // If the surface is null we're being torn down, don't need a
345            // renderer then
346            if (surface != null) {
347                checkRenderer();
348            }
349            mSurface = surface;
350            exec(CHANGE_SURFACE);
351        }
352
353        public void setSize(int width, int height) {
354            checkRenderer();
355            checkSurface();
356            mWidth = width;
357            mHeight = height;
358            exec(RESIZE_SURFACE);
359        }
360
361        public void render() {
362            checkRenderer();
363            if (mSurface != null) {
364                exec(RENDER);
365                mSurface.updateTexImage();
366            }
367        }
368
369        public void finish() {
370            mSurface = null;
371            exec(FINISH);
372            try {
373                join();
374            } catch (InterruptedException e) {
375                // Ignore
376            }
377        }
378
379        private void exec(int msgid) {
380            synchronized (mLock) {
381                if (mExecMsgId != INVALID) {
382                    throw new IllegalArgumentException(
383                            "Message already set - multithreaded access?");
384                }
385                mExecMsgId = msgid;
386                mLock.notify();
387                try {
388                    mLock.wait();
389                } catch (InterruptedException e) {
390                    // Ignore
391                }
392            }
393        }
394
395        private void handleMessageLocked(int what) {
396            switch (what) {
397            case CHANGE_SURFACE:
398                if (mEglHelper.createSurface(mSurface)) {
399                    mGL = mEglHelper.createGL();
400                    mRenderer.onSurfaceCreated(mGL, mEglHelper.mEglConfig);
401                }
402                break;
403            case RESIZE_SURFACE:
404                mRenderer.onSurfaceChanged(mGL, mWidth, mHeight);
405                break;
406            case RENDER:
407                mRenderer.onDrawFrame(mGL);
408                mEglHelper.swap();
409                break;
410            case FINISH:
411                mEglHelper.destroySurface();
412                mEglHelper.finish();
413                mFinished = true;
414                break;
415            }
416        }
417
418        @Override
419        public void run() {
420            synchronized (mLock) {
421                mEglHelper.start();
422                while (!mFinished) {
423                    while (mExecMsgId == INVALID) {
424                        try {
425                            mLock.wait();
426                        } catch (InterruptedException e) {
427                            // Ignore
428                        }
429                    }
430                    handleMessageLocked(mExecMsgId);
431                    mExecMsgId = INVALID;
432                    mLock.notify();
433                }
434                mExecMsgId = FINISH;
435            }
436        }
437    }
438}
439