From efc776e4f9ff25989b9357536487c539c15bbf47 Mon Sep 17 00:00:00 2001 From: Ivo van Dongen Date: Mon, 2 Oct 2017 15:41:02 +0300 Subject: [android] texture view map renderer --- .../java/com/mapbox/mapboxsdk/maps/MapView.java | 4 + .../mapboxsdk/maps/renderer/MapRenderer.java | 3 + .../textureview/TextureViewMapRenderer.java | 96 +++++ .../textureview/TextureViewRenderThread.java | 442 +++++++++++++++++++++ 4 files changed, 545 insertions(+) create mode 100644 platform/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/maps/renderer/textureview/TextureViewMapRenderer.java create mode 100644 platform/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/maps/renderer/textureview/TextureViewRenderThread.java diff --git a/platform/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/maps/MapView.java b/platform/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/maps/MapView.java index 99813621ef..622cdd33a6 100644 --- a/platform/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/maps/MapView.java +++ b/platform/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/maps/MapView.java @@ -374,6 +374,10 @@ public class MapView extends FrameLayout { mapCallback.clearOnMapReadyCallbacks(); nativeMapView.destroy(); nativeMapView = null; + + if (mapRenderer != null) { + mapRenderer.onDestroy(); + } } @Override diff --git a/platform/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/maps/renderer/MapRenderer.java b/platform/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/maps/renderer/MapRenderer.java index a7c0fa3778..961438ed14 100644 --- a/platform/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/maps/renderer/MapRenderer.java +++ b/platform/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/maps/renderer/MapRenderer.java @@ -41,6 +41,9 @@ public abstract class MapRenderer implements MapRendererScheduler { // Implement if needed } + public void onDestroy() { + // Implement if needed + } public void setOnFpsChangedListener(MapboxMap.OnFpsChangedListener listener) { onFpsChangedListener = listener; diff --git a/platform/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/maps/renderer/textureview/TextureViewMapRenderer.java b/platform/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/maps/renderer/textureview/TextureViewMapRenderer.java new file mode 100644 index 0000000000..8cd724a828 --- /dev/null +++ b/platform/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/maps/renderer/textureview/TextureViewMapRenderer.java @@ -0,0 +1,96 @@ +package com.mapbox.mapboxsdk.maps.renderer.textureview; + +import android.content.Context; +import android.support.annotation.NonNull; +import android.view.TextureView; + +import com.mapbox.mapboxsdk.maps.renderer.MapRenderer; + +import javax.microedition.khronos.egl.EGLConfig; +import javax.microedition.khronos.opengles.GL10; + +/** + * The {@link TextureViewMapRenderer} encapsulates the GL thread and + * {@link TextureView} specifics to render the map. + * + * @see MapRenderer + */ +public class TextureViewMapRenderer extends MapRenderer { + private TextureViewRenderThread renderThread; + + /** + * Create a {@link MapRenderer} for the given {@link TextureView} + * + * @param context the current Context + * @param textureView the TextureView + */ + public TextureViewMapRenderer(@NonNull Context context, @NonNull TextureView textureView) { + super(context); + renderThread = new TextureViewRenderThread(textureView, this); + renderThread.start(); + } + + /** + * Overridden to provide package access + */ + @Override + protected void onSurfaceCreated(GL10 gl, EGLConfig config) { + super.onSurfaceCreated(gl, config); + } + + /** + * Overridden to provide package access + */ + @Override + protected void onSurfaceChanged(GL10 gl, int width, int height) { + super.onSurfaceChanged(gl, width, height); + } + + /** + * Overridden to provide package access + */ + @Override + protected void onDrawFrame(GL10 gl) { + super.onDrawFrame(gl); + } + + /** + * {@inheritDoc} + */ + @Override + public void requestRender() { + renderThread.requestRender(); + } + + /** + * {@inheritDoc} + */ + @Override + public void queueEvent(Runnable runnable) { + renderThread.queueEvent(runnable); + } + + /** + * {@inheritDoc} + */ + @Override + public void onPause() { + renderThread.onPause(); + } + + /** + * {@inheritDoc} + */ + @Override + public void onResume() { + renderThread.onResume(); + } + + /** + * {@inheritDoc} + */ + @Override + public void onDestroy() { + renderThread.onDestroy(); + } +} diff --git a/platform/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/maps/renderer/textureview/TextureViewRenderThread.java b/platform/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/maps/renderer/textureview/TextureViewRenderThread.java new file mode 100644 index 0000000000..3ed55b634a --- /dev/null +++ b/platform/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/maps/renderer/textureview/TextureViewRenderThread.java @@ -0,0 +1,442 @@ +package com.mapbox.mapboxsdk.maps.renderer.textureview; + +import android.graphics.SurfaceTexture; +import android.support.annotation.NonNull; +import android.support.annotation.UiThread; +import android.view.TextureView; + +import com.mapbox.mapboxsdk.maps.renderer.egl.EGLConfigChooser; + +import java.lang.ref.WeakReference; +import java.util.ArrayList; + +import javax.microedition.khronos.egl.EGL10; +import javax.microedition.khronos.egl.EGL11; +import javax.microedition.khronos.egl.EGLConfig; +import javax.microedition.khronos.egl.EGLContext; +import javax.microedition.khronos.egl.EGLDisplay; +import javax.microedition.khronos.egl.EGLSurface; +import javax.microedition.khronos.opengles.GL10; + +import timber.log.Timber; + +/** + * The render thread is responsible for managing the communication between the + * ui thread and the render thread it creates. Also, the EGL and GL contexts + * are managed from here. + */ +class TextureViewRenderThread extends Thread implements TextureView.SurfaceTextureListener { + + private final TextureViewMapRenderer mapRenderer; + private final EGLHolder eglHolder; + + // Lock used for synchronization + private final Object lock = new Object(); + + // Guarded by lock + private final ArrayList eventQueue = new ArrayList<>(); + private SurfaceTexture surface; + private int width; + private int height; + private boolean requestRender; + private boolean sizeChanged; + private boolean paused; + private boolean destroyContext; + private boolean destroySurface; + private boolean shouldExit; + private boolean exited; + + /** + * Create a render thread for the given TextureView / Maprenderer combination. + * + * @param textureView the TextureView + * @param mapRenderer the MapRenderer + */ + @UiThread + TextureViewRenderThread(@NonNull TextureView textureView, @NonNull TextureViewMapRenderer mapRenderer) { + textureView.setSurfaceTextureListener(this); + this.mapRenderer = mapRenderer; + this.eglHolder = new EGLHolder(new WeakReference<>(textureView)); + } + + // SurfaceTextureListener methods + + @UiThread + @Override + public void onSurfaceTextureAvailable(final SurfaceTexture surface, final int width, final int height) { + synchronized (lock) { + this.surface = surface; + this.width = width; + this.height = height; + this.requestRender = true; + lock.notifyAll(); + } + } + + @Override + @UiThread + public void onSurfaceTextureSizeChanged(SurfaceTexture surface, final int width, final int height) { + synchronized (lock) { + this.width = width; + this.height = height; + this.sizeChanged = true; + this.requestRender = true; + lock.notifyAll(); + } + } + + @Override + @UiThread + public boolean onSurfaceTextureDestroyed(SurfaceTexture surface) { + synchronized (lock) { + this.surface = null; + this.destroySurface = true; + this.requestRender = false; + lock.notifyAll(); + } + return true; + } + + @Override + @UiThread + public void onSurfaceTextureUpdated(SurfaceTexture surface) { + // Ignored + } + + // MapRenderer delegate methods + + /** + * May be called from any thread + */ + void requestRender() { + synchronized (lock) { + requestRender = true; + lock.notifyAll(); + } + } + + /** + * May be called from any thread + */ + void queueEvent(Runnable runnable) { + if (runnable == null) { + throw new IllegalArgumentException("runnable must not be null"); + } + synchronized (lock) { + eventQueue.add(runnable); + lock.notifyAll(); + } + } + + + @UiThread + void onPause() { + synchronized (lock) { + this.paused = true; + lock.notifyAll(); + } + } + + @UiThread + void onResume() { + synchronized (lock) { + this.paused = false; + lock.notifyAll(); + } + } + + + @UiThread + void onDestroy() { + synchronized (lock) { + this.shouldExit = true; + lock.notifyAll(); + + // Wait for the thread to exit + while (!this.exited) { + try { + lock.wait(); + } catch (InterruptedException ex) { + Thread.currentThread().interrupt(); + } + } + } + } + + // Thread implementation + + @Override + public void run() { + try { + + while (true) { + Runnable event = null; + boolean initializeEGL = false; + boolean recreateSurface = false; + int w = -1; + int h = -1; + + // Guarded block + synchronized (lock) { + while (true) { + + if (shouldExit) { + return; + } + + // If any events are scheduled, pop one for processing + if (!eventQueue.isEmpty()) { + event = eventQueue.remove(0); + break; + } + + if (destroySurface) { + eglHolder.destroySurface(); + destroySurface = false; + break; + } + + if (destroyContext) { + eglHolder.destroyContext(); + destroyContext = false; + break; + } + + if (surface != null && !paused && requestRender) { + + w = width; + h = height; + + // Initialize EGL if needed + if (eglHolder.eglContext == EGL10.EGL_NO_CONTEXT) { + initializeEGL = true; + break; + } + + // Check if the size has changed + if (sizeChanged) { + recreateSurface = true; + sizeChanged = false; + break; + } + + // Reset the request render flag now, so we can catch new requests + // while rendering + requestRender = false; + + // Break the guarded loop and continue to process + break; + } + + + // Wait until needed + lock.wait(); + + } // end guarded while loop + + } // end guarded block + + // Run event, if any + if (event != null) { + event.run(); + continue; + } + + GL10 gl = eglHolder.createGL(); + + // Initialize EGL + if (initializeEGL) { + eglHolder.prepare(); + eglHolder.createSurface(); + mapRenderer.onSurfaceCreated(gl, eglHolder.eglConfig); + mapRenderer.onSurfaceChanged(gl, w, h); + continue; + } + + // If the surface size has changed inform the map renderer. + if (recreateSurface) { + eglHolder.createSurface(); + mapRenderer.onSurfaceChanged(gl, w, h); + continue; + } + + // Don't continue without a surface + if (eglHolder.eglSurface == EGL10.EGL_NO_SURFACE) { + continue; + } + + // Time to render a frame + mapRenderer.onDrawFrame(gl); + + // Swap and check the result + int swapError = eglHolder.swap(); + switch (swapError) { + case EGL10.EGL_SUCCESS: + break; + case EGL11.EGL_CONTEXT_LOST: + Timber.w("Context lost. Waiting for re-aquire"); + synchronized (lock) { + surface = null; + destroySurface = true; + destroyContext = true; + } + break; + default: + Timber.w("eglSwapBuffer error: %s. Waiting or new surface", swapError); + // Probably lost the surface. Clear the current one and + // wait for a new one to be set + synchronized (lock) { + surface = null; + destroySurface = true; + } + } + + } + + } catch (InterruptedException err) { + // To be expected + } finally { + // Cleanup + eglHolder.cleanup(); + + // Signal we're done + synchronized (lock) { + this.exited = true; + lock.notifyAll(); + } + } + } + + /** + * Holds the EGL state and offers methods to mutate it. + */ + private static class EGLHolder { + private static final int EGL_CONTEXT_CLIENT_VERSION = 0x3098; + private final WeakReference textureViewWeakRef; + + private EGL10 egl; + private EGLConfig eglConfig; + private EGLDisplay eglDisplay = EGL10.EGL_NO_DISPLAY; + private EGLContext eglContext = EGL10.EGL_NO_CONTEXT; + private EGLSurface eglSurface = EGL10.EGL_NO_SURFACE; + + EGLHolder(WeakReference textureViewWeakRef) { + this.textureViewWeakRef = textureViewWeakRef; + } + + void prepare() { + this.egl = (EGL10) EGLContext.getEGL(); + + // Only re-initialize display when needed + if (eglDisplay == EGL10.EGL_NO_DISPLAY) { + this.eglDisplay = egl.eglGetDisplay(EGL10.EGL_DEFAULT_DISPLAY); + + if (eglDisplay == EGL10.EGL_NO_DISPLAY) { + throw new RuntimeException("eglGetDisplay failed"); + } + + int[] version = new int[2]; + if (!egl.eglInitialize(eglDisplay, version)) { + throw new RuntimeException("eglInitialize failed"); + } + } + + if (textureViewWeakRef == null) { + // No texture view present + eglConfig = null; + eglContext = EGL10.EGL_NO_CONTEXT; + } else /*if (eglContext == EGL10.EGL_NO_CONTEXT)*/ { + eglConfig = new EGLConfigChooser().chooseConfig(egl, eglDisplay); + int[] attrib_list = {EGL_CONTEXT_CLIENT_VERSION, 2, EGL10.EGL_NONE}; + eglContext = egl.eglCreateContext(eglDisplay, eglConfig, EGL10.EGL_NO_CONTEXT, attrib_list); + } + + if (eglContext == EGL10.EGL_NO_CONTEXT) { + throw new RuntimeException("createContext"); + } + } + + GL10 createGL() { + return (GL10) eglContext.getGL(); + } + + boolean createSurface() { + // The window size has changed, so we need to create a new surface. + destroySurface(); + + // Create an EGL surface we can render into. + TextureView view = textureViewWeakRef.get(); + if (view != null) { + int[] surfaceAttribs = {EGL10.EGL_NONE}; + eglSurface = egl.eglCreateWindowSurface(eglDisplay, eglConfig, view.getSurfaceTexture(), surfaceAttribs); + } else { + eglSurface = EGL10.EGL_NO_SURFACE; + } + + if (eglSurface == null || eglSurface == EGL10.EGL_NO_SURFACE) { + int error = egl.eglGetError(); + if (error == EGL10.EGL_BAD_NATIVE_WINDOW) { + Timber.e("createWindowSurface returned EGL_BAD_NATIVE_WINDOW."); + } + return false; + } + + return makeCurrent(); + } + + boolean makeCurrent() { + if (!egl.eglMakeCurrent(eglDisplay, eglSurface, eglSurface, eglContext)) { + // Could not make the context current, probably because the underlying + // SurfaceView surface has been destroyed. + Timber.w("eglMakeCurrent: %s", egl.eglGetError()); + return false; + } + + return true; + } + + int swap() { + if (!egl.eglSwapBuffers(eglDisplay, eglSurface)) { + return egl.eglGetError(); + } + return EGL10.EGL_SUCCESS; + } + + private void destroySurface() { + if (eglSurface == EGL10.EGL_NO_SURFACE) { + return; + } + + if (!egl.eglDestroySurface(eglDisplay, eglSurface)) { + Timber.e("Could not destroy egl surface. Display %s, Surface %s", eglDisplay, eglSurface); + throw new RuntimeException("eglDestroyContext: " + egl.eglGetError()); + } + } + + private void destroyContext() { + if (eglSurface == null || eglSurface == EGL10.EGL_NO_SURFACE) { + return; + } + + if (!egl.eglDestroyContext(eglDisplay, eglContext)) { + Timber.e("Could not destroy egl context. Display %s, Context %s", eglDisplay, eglContext); + throw new RuntimeException("eglDestroyContext: " + egl.eglGetError()); + } + } + + private void terminate() { + if (eglDisplay != EGL10.EGL_NO_DISPLAY) { + if (!egl.eglTerminate(eglDisplay)) { + Timber.w("Could not terminate egl. Display %s", eglDisplay); + } + eglDisplay = EGL10.EGL_NO_DISPLAY; + } + } + + void cleanup() { + destroySurface(); + destroyContext(); + terminate(); + } + } +} -- cgit v1.2.1