From e47e7ebaca9858201f605f47f7473b003136550a Mon Sep 17 00:00:00 2001 From: Peng Liu Date: Thu, 29 Jul 2021 17:14:45 +0300 Subject: [PATCH 01/13] Base. --- .../testapp/auto/car/CarCameraController.kt | 14 +- .../maps/testapp/auto/car/MapSession.kt | 27 +- .../customlayer/ExampleCustomLayer.kt | 4 + .../extension/androidauto/BackgroundWidget.kt | 196 ++++++++ .../androidauto/BaseWidgetRenderer.kt | 450 ++++++++++++++++++ .../extension/androidauto/CompassWidget.kt | 13 + .../maps/extension/androidauto/LogoWidget.kt | 13 + .../extension/androidauto/MapboxCarUtils.kt | 2 + .../extension/androidauto/WidgetPosition.kt | 8 + .../java/com/mapbox/maps/MapControllable.kt | 7 + .../java/com/mapbox/maps/MapController.kt | 5 + .../main/java/com/mapbox/maps/MapSurface.kt | 12 +- sdk/src/main/java/com/mapbox/maps/MapView.kt | 9 + .../maps/renderer/MapboxRenderThread.kt | 14 + .../com/mapbox/maps/renderer/SurfaceUtils.kt | 26 + .../java/com/mapbox/maps/renderer/Widget.kt | 35 ++ 16 files changed, 829 insertions(+), 6 deletions(-) create mode 100644 extension-androidauto/src/main/java/com/mapbox/maps/extension/androidauto/BackgroundWidget.kt create mode 100644 extension-androidauto/src/main/java/com/mapbox/maps/extension/androidauto/BaseWidgetRenderer.kt create mode 100644 extension-androidauto/src/main/java/com/mapbox/maps/extension/androidauto/CompassWidget.kt create mode 100644 extension-androidauto/src/main/java/com/mapbox/maps/extension/androidauto/LogoWidget.kt create mode 100644 extension-androidauto/src/main/java/com/mapbox/maps/extension/androidauto/WidgetPosition.kt create mode 100644 sdk/src/main/java/com/mapbox/maps/renderer/SurfaceUtils.kt create mode 100644 sdk/src/main/java/com/mapbox/maps/renderer/Widget.kt diff --git a/android-auto-app/src/main/java/com/mapbox/maps/testapp/auto/car/CarCameraController.kt b/android-auto-app/src/main/java/com/mapbox/maps/testapp/auto/car/CarCameraController.kt index 920efa62d3..95de402314 100644 --- a/android-auto-app/src/main/java/com/mapbox/maps/testapp/auto/car/CarCameraController.kt +++ b/android-auto-app/src/main/java/com/mapbox/maps/testapp/auto/car/CarCameraController.kt @@ -6,12 +6,13 @@ import com.mapbox.maps.MapSurface import com.mapbox.maps.dsl.cameraOptions import com.mapbox.maps.extension.androidauto.OnMapScrollListener import com.mapbox.maps.plugin.animation.camera +import com.mapbox.maps.plugin.locationcomponent.OnIndicatorBearingChangedListener import com.mapbox.maps.plugin.locationcomponent.OnIndicatorPositionChangedListener /** * Controller class to handle map camera changes. */ -class CarCameraController : OnIndicatorPositionChangedListener, OnMapScrollListener { +class CarCameraController : OnIndicatorPositionChangedListener, OnIndicatorBearingChangedListener, OnMapScrollListener { private var lastGpsLocation: Point = HELSINKI private var isTrackingPuck = true @@ -46,6 +47,16 @@ class CarCameraController : OnIndicatorPositionChangedListener, OnMapScrollListe } } + override fun onIndicatorBearingChanged(bearing: Double) { + if (isTrackingPuck) { + surface.getMapboxMap().setCamera( + cameraOptions { + bearing(bearing) + } + ) + } + } + override fun onMapScroll() { dismissTracking() } @@ -86,4 +97,5 @@ class CarCameraController : OnIndicatorPositionChangedListener, OnMapScrollListe private const val INITIAL_PITCH = 75.0 private const val TAG = "CarCameraController" } + } \ No newline at end of file diff --git a/android-auto-app/src/main/java/com/mapbox/maps/testapp/auto/car/MapSession.kt b/android-auto-app/src/main/java/com/mapbox/maps/testapp/auto/car/MapSession.kt index f93341809a..3871f84d30 100644 --- a/android-auto-app/src/main/java/com/mapbox/maps/testapp/auto/car/MapSession.kt +++ b/android-auto-app/src/main/java/com/mapbox/maps/testapp/auto/car/MapSession.kt @@ -4,12 +4,14 @@ import android.Manifest.permission.ACCESS_FINE_LOCATION import android.content.Intent import android.content.pm.PackageManager.PERMISSION_GRANTED import android.content.res.Configuration -import androidx.car.app.Screen -import androidx.car.app.ScreenManager -import androidx.car.app.Session +import android.graphics.BitmapFactory +import android.util.Log +import androidx.car.app.* import com.mapbox.maps.EdgeInsets import com.mapbox.maps.MapSurface import com.mapbox.maps.Style +import com.mapbox.maps.extension.androidauto.CompassWidget +import com.mapbox.maps.extension.androidauto.LogoWidget import com.mapbox.maps.extension.androidauto.initMapSurface import com.mapbox.maps.extension.style.layers.generated.skyLayer import com.mapbox.maps.extension.style.layers.properties.generated.SkyType @@ -17,6 +19,7 @@ import com.mapbox.maps.extension.style.sources.generated.rasterDemSource import com.mapbox.maps.extension.style.style import com.mapbox.maps.extension.style.terrain.generated.terrain import com.mapbox.maps.plugin.locationcomponent.location +import com.mapbox.maps.renderer.Widget import com.mapbox.maps.testapp.auto.R /** @@ -25,10 +28,15 @@ import com.mapbox.maps.testapp.auto.R class MapSession : Session() { private lateinit var mapSurface: MapSurface private val carCameraController = CarCameraController() + private val widgetList = mutableListOf() override fun onCreateScreen(intent: Intent): Screen { val mapScreen = MapScreen(carContext) - initMapSurface(scrollListener = carCameraController) { surface -> + widgetList.add(LogoWidget(carContext)) + widgetList.add(CompassWidget(carContext)) + initMapSurface( + scrollListener = carCameraController, + ) { surface -> mapSurface = surface carCameraController.init( mapSurface, @@ -42,6 +50,16 @@ class MapSession : Session() { mapScreen.setMapCameraController(carCameraController) loadStyle(surface) initLocationComponent(surface) + widgetList.forEach { surface.addWidget(it) } + surface.getMapboxMap().apply { + addOnCameraChangeListener { + widgetList.filterIsInstance().forEach { + it.rotate( + this.cameraState.bearing.toFloat().also { Log.e("testtest", it.toString()) } + ) + } + } + } } return if (carContext.checkSelfPermission(ACCESS_FINE_LOCATION) != PERMISSION_GRANTED) { carContext.getCarService(ScreenManager::class.java).push(mapScreen) @@ -73,6 +91,7 @@ class MapSession : Session() { locationPuck = CarLocationPuck.duckLocationPuckLowZoom enabled = true addOnIndicatorPositionChangedListener(carCameraController) + addOnIndicatorBearingChangedListener(carCameraController) } } diff --git a/app/src/main/java/com/mapbox/maps/testapp/examples/customlayer/ExampleCustomLayer.kt b/app/src/main/java/com/mapbox/maps/testapp/examples/customlayer/ExampleCustomLayer.kt index ba63832716..d15fb9086f 100644 --- a/app/src/main/java/com/mapbox/maps/testapp/examples/customlayer/ExampleCustomLayer.kt +++ b/app/src/main/java/com/mapbox/maps/testapp/examples/customlayer/ExampleCustomLayer.kt @@ -142,6 +142,10 @@ class ExampleCustomLayer : CustomLayerHost { -1.0f, 1.0f, 1.0f, 1.0f ) + // -0.8f, -0.8f, +// -1.0f, -0.8f, +// -0.8f, -1.0f, +// -1.0f, -1.0f private val VERTEX_COUNT = BACKGROUND_COORDINATES.size / COORDS_PER_VERTEX // Set color with red, green, blue and alpha (opacity) values diff --git a/extension-androidauto/src/main/java/com/mapbox/maps/extension/androidauto/BackgroundWidget.kt b/extension-androidauto/src/main/java/com/mapbox/maps/extension/androidauto/BackgroundWidget.kt new file mode 100644 index 0000000000..d936e88396 --- /dev/null +++ b/extension-androidauto/src/main/java/com/mapbox/maps/extension/androidauto/BackgroundWidget.kt @@ -0,0 +1,196 @@ +package com.mapbox.maps.extension.androidauto + +import android.opengl.GLES20 +import com.mapbox.common.Logger +import com.mapbox.maps.renderer.Widget +import java.nio.ByteBuffer +import java.nio.ByteOrder +import java.nio.FloatBuffer + +class BackgroundWidget : Widget() { + private var program = 0 + private var positionHandle = 0 + private var colorHandle = 0 + private var vertexShader = 0 + private var fragmentShader = 0 + + private val vertexBuffer: FloatBuffer by lazy { + // initialize vertex byte buffer for shape coordinates + // (number of coordinate values * 4 bytes per float) + ByteBuffer.allocateDirect(BACKGROUND_COORDINATES.size * BYTES_PER_FLOAT).run { + // use the device hardware's native byte order + order(ByteOrder.nativeOrder()) + + // create a floating point buffer from the ByteBuffer + asFloatBuffer().apply { + // add the coordinates to the FloatBuffer + put(BACKGROUND_COORDINATES) + // set the buffer to read the first coordinate + rewind() + } + } + } + + override fun initialize() { + val maxAttrib = IntArray(1) + GLES20.glGetIntegerv(GLES20.GL_MAX_VERTEX_ATTRIBS, maxAttrib, 0) + Logger.d(TAG, "Max vertex attributes: ${maxAttrib[0]}") + + // load and compile shaders + vertexShader = loadShader( + GLES20.GL_VERTEX_SHADER, + VERTEX_SHADER_CODE + ).also { checkCompileStatus(it) } + + fragmentShader = loadShader( + GLES20.GL_FRAGMENT_SHADER, + FRAGMENT_SHADER_CODE + ).also { checkCompileStatus(it) } + + // create empty OpenGL ES Program + program = GLES20.glCreateProgram().also { + checkError("glCreateProgram") + // add the vertex shader to program + GLES20.glAttachShader(it, vertexShader).also { checkError("glAttachShader") } + + // add the fragment shader to program + GLES20.glAttachShader(it, fragmentShader).also { checkError("glAttachShader") } + + // creates OpenGL ES program executables + GLES20.glLinkProgram(it).also { + checkError("glLinkProgram") + } + } + + // get handle to vertex shader's vPosition member + positionHandle = + GLES20.glGetAttribLocation(program, "a_pos").also { checkError("glGetAttribLocation") } + + // get handle to fragment shader's vColor member + colorHandle = GLES20.glGetUniformLocation(program, "fill_color") + .also { checkError("glGetUniformLocation") } + } + + override fun render() { + super.render() + if (program != 0) { + // Add program to OpenGL ES environment + GLES20.glUseProgram(program).also { checkError("glUseProgram") } + + GLES20.glBindBuffer(GLES20.GL_ARRAY_BUFFER, positionHandle) + + // Enable a handle to the vertices + GLES20.glEnableVertexAttribArray(positionHandle) + .also { checkError("glEnableVertexAttribArray") } + + // Prepare the coordinate data + GLES20.glVertexAttribPointer( + positionHandle, COORDS_PER_VERTEX, + GLES20.GL_FLOAT, false, + VERTEX_STRIDE, vertexBuffer + ).also { checkError("glVertexAttribPointer") } + + // Set color for drawing the background + GLES20.glUniform4fv(colorHandle, 1, color, 0).also { checkError("glUniform4fv") } + + // Draw the background + GLES20.glDrawArrays(GLES20.GL_TRIANGLE_STRIP, 0, VERTEX_COUNT) + .also { checkError("glDrawArrays") } + } + } + + fun contextLost() { + Logger.w(TAG, "contextLost") + program = 0 + } + + override fun deinitialize() { + if (program != 0) { + // Disable vertex array + GLES20.glDisableVertexAttribArray(positionHandle) + GLES20.glDetachShader(program, vertexShader) + GLES20.glDetachShader(program, fragmentShader) + GLES20.glDeleteShader(vertexShader) + GLES20.glDeleteShader(fragmentShader) + GLES20.glDeleteProgram(program) + program = 0 + } + } + + private fun checkCompileStatus(shader: Int) { + if (BuildConfig.DEBUG) { + val isCompiled = IntArray(1) + GLES20.glGetShaderiv(shader, GLES20.GL_COMPILE_STATUS, isCompiled, 0) + if (isCompiled[0] == GLES20.GL_FALSE) { + val infoLog = GLES20.glGetShaderInfoLog(program) + throw RuntimeException("checkCompileStatus error: $infoLog") + } + } + } + + companion object { + private const val TAG = "ExampleCustomLayer" + + // number of coordinates per vertex in this array + private const val COORDS_PER_VERTEX = 2 + private const val BYTES_PER_FLOAT = 4 + private const val VERTEX_STRIDE = COORDS_PER_VERTEX * BYTES_PER_FLOAT // 4 bytes per vertex + private val BACKGROUND_COORDINATES = floatArrayOf( // in counterclockwise order: +// -1.0f, -1.0f, +// 1.0f, -1.0f, +// -1.0f, 1.0f, +// 1.0f, 1.0f + -0.8f, -0.8f, + -1.0f, -0.8f, + -0.8f, -1.0f, + -1.0f, -1.0f + ) + + private val VERTEX_COUNT = BACKGROUND_COORDINATES.size / COORDS_PER_VERTEX + + // Set color with red, green, blue and alpha (opacity) values + var color = floatArrayOf(0.0f, 1.0f, 0.0f, 1.0f) + + private val VERTEX_SHADER_CODE = """ + attribute vec2 a_pos; + void main() { + gl_Position = vec4(a_pos, 0.0, 1.0); + } + """.trimIndent() + + private val FRAGMENT_SHADER_CODE = """ + precision mediump float; + uniform vec4 fill_color; + void main() { + gl_FragColor = fill_color; + } + """.trimIndent() + + private fun loadShader(type: Int, shaderCode: String): Int { + // create a vertex shader type (GLES20.GL_VERTEX_SHADER) + // or a fragment shader type (GLES20.GL_FRAGMENT_SHADER) + return GLES20.glCreateShader(type).also { shader -> + + // add the source code to the shader and compile it + GLES20.glShaderSource(shader, shaderCode) + GLES20.glCompileShader(shader) + } + } + + private fun checkError(cmd: String? = null) { + if (BuildConfig.DEBUG) { + when (val error = GLES20.glGetError()) { + GLES20.GL_NO_ERROR -> { + Logger.d(TAG, "$cmd -> no error") + } + GLES20.GL_INVALID_ENUM -> throw RuntimeException("$cmd -> error in gl: GL_INVALID_ENUM") + GLES20.GL_INVALID_VALUE -> throw RuntimeException("$cmd -> error in gl: GL_INVALID_VALUE") + GLES20.GL_INVALID_OPERATION -> throw RuntimeException("$cmd -> error in gl: GL_INVALID_OPERATION") + GLES20.GL_INVALID_FRAMEBUFFER_OPERATION -> throw RuntimeException("$cmd -> error in gl: GL_INVALID_FRAMEBUFFER_OPERATION") + GLES20.GL_OUT_OF_MEMORY -> throw RuntimeException("$cmd -> error in gl: GL_OUT_OF_MEMORY") + else -> throw RuntimeException("$cmd -> error in gl: $error") + } + } + } + } +} \ No newline at end of file diff --git a/extension-androidauto/src/main/java/com/mapbox/maps/extension/androidauto/BaseWidgetRenderer.kt b/extension-androidauto/src/main/java/com/mapbox/maps/extension/androidauto/BaseWidgetRenderer.kt new file mode 100644 index 0000000000..3a46182d70 --- /dev/null +++ b/extension-androidauto/src/main/java/com/mapbox/maps/extension/androidauto/BaseWidgetRenderer.kt @@ -0,0 +1,450 @@ +package com.mapbox.maps.extension.androidauto + +import android.graphics.Bitmap +import android.opengl.GLES20 +import android.opengl.GLUtils +import android.opengl.Matrix +import android.util.Log +import com.mapbox.common.Logger +import com.mapbox.maps.BuildConfig +import com.mapbox.maps.renderer.Widget +import java.nio.ByteBuffer +import java.nio.ByteOrder +import java.nio.FloatBuffer + +open class BaseWidgetRenderer( + private var bitmap: Bitmap, + private val position: WidgetPosition = WidgetPosition.BOTTOM_LEFT, + private val marginLeft: Float = 0f, + private val marginTop: Float = 0f, + private val marginRight: Float = 0f, + private val marginBottom: Float = 0f, +) : Widget() { + private var program = 0 + private var vertexPositionHandle = 0 + private var texturePositionHandle = 0 + private var textureHandle = 0 + private var screenMatrixHandle = 0 + private var rotationMatrixHandle = 0 + private var translateMatrixHandle = 0 + private val textures = intArrayOf(0) + + private var vertexShader = 0 + private var fragmentShader = 0 + + private var heightOffset: Float = 0f + private var widthOffset: Float = 0f + + private lateinit var screenMatrixData: FloatArray + private lateinit var screenMatrixBuffer: FloatBuffer + + private val rotationMatrixData = FloatArray(16).apply { + Matrix.setIdentityM(this, 0) + } + + private val translateMatrix = FloatArray(16).apply { + Matrix.setIdentityM(this, 0) + } + +// private lateinit var rotationMatrixBuffer : FloatBuffer + + private fun getMatrixBuffer(matrixData: FloatArray): FloatBuffer { + return ByteBuffer.allocateDirect(matrixData.size * BYTES_PER_FLOAT).run { + // use the device hardware's native byte order + order(ByteOrder.nativeOrder()) + + // create a floating point buffer from the ByteBuffer + asFloatBuffer().apply { + // add the coordinates to the FloatBuffer + put(matrixData) + // set the buffer to read the first coordinate + rewind() + } + } + } + + private lateinit var vertexPositionData: FloatArray + private lateinit var vertexPositionBuffer: FloatBuffer + + init { + Matrix.setIdentityM(rotationMatrixData, 0) + } + + private val texturePositionData = floatArrayOf( + 0f, 0f, //Texture coordinate for V1 + 0f, 1f, + 1f, 0f, + 1f, 1f + ) + private val texturePositionBuffer: FloatBuffer by lazy { + getMatrixBuffer(texturePositionData) + } + + override fun onSizeChanged(width: Int, height: Int) { + super.onSizeChanged(width, height) + Log.e(TAG, "bitmap size: ${bitmap.width}, ${bitmap.height}") + Log.e(TAG, "screen size: ${width}, ${height}") + heightOffset = when (position) { + WidgetPosition.BOTTOM_LEFT -> height.toFloat() - bitmap.height.toFloat() / 2f - marginBottom + WidgetPosition.BOTTOM_RIGHT -> height.toFloat() - bitmap.height.toFloat() / 2f - marginBottom + WidgetPosition.TOP_LEFT -> marginTop + bitmap.height.toFloat() / 2f + WidgetPosition.TOP_RIGHT -> marginTop + bitmap.height.toFloat() / 2f + } + widthOffset = when (position) { + WidgetPosition.TOP_RIGHT -> width.toFloat() - bitmap.width.toFloat() / 2f - marginRight + WidgetPosition.BOTTOM_RIGHT -> width.toFloat() - bitmap.width.toFloat() / 2f - marginRight + WidgetPosition.TOP_LEFT -> marginLeft + bitmap.width.toFloat() / 2f + WidgetPosition.BOTTOM_LEFT -> marginLeft + bitmap.width.toFloat() / 2f + } + + Matrix.translateM( + translateMatrix, + 0, + widthOffset, + heightOffset, + 0f + ) + + // The uScreen matrix + // + // First of all, the only coordinate system that OpenGL understands + // put the center of the screen at the 0,0 position. The maximum value of + // the X axis is 1 (rightmost part of the screen) and the minimum is -1 + // (leftmost part of the screen). The same thing goes for the Y axis, + // where 1 is the top of the screen and -1 the bottom. + // + // However, when you're doing a 2d application you often need to think in 'pixels' + // (or something like that). If you have a 300x300 screen, you want to see the center + // at 150,150 not 0,0! + // + // The solution to this 'problem' is to multiply a matrix with your position to + // another matrix that will convert 'your' coordinates to the one OpenGL expects. + // There's no magic in this, only a bit of math. Try to multiply the uScreen matrix + // to the 150,150 position in a sheet of paper and look at the results. + // + // IMPORTANT: When trying to calculate the matrix on paper, you should treat the + // uScreen ROWS as COLUMNS and vice versa. This happens because OpenGL expect the + // matrix values ordered in a more efficient way, that unfortunately is different + // from the mathematical notation :( + screenMatrixData = floatArrayOf( + 2f / width.toFloat(), 0f, 0f, 0f, + 0f, -2f / height.toFloat(), 0f, 0f, + 0f, 0f, 0f, 0f, + -1f, 1f, 0f, 1f + ) + + // initialize vertex byte buffer for shape coordinates + // (number of coordinate values * 4 bytes per float) + screenMatrixBuffer = ByteBuffer.allocateDirect(screenMatrixData.size * BYTES_PER_FLOAT).run { + // use the device hardware's native byte order + order(ByteOrder.nativeOrder()) + + // create a floating point buffer from the ByteBuffer + asFloatBuffer().apply { + // add the coordinates to the FloatBuffer + put(screenMatrixData) + // set the buffer to read the first coordinate + rewind() + } + } + + vertexPositionData = floatArrayOf( + -bitmap.width.toFloat() / 2f, -bitmap.height.toFloat() / 2f, + -bitmap.width.toFloat() / 2f, bitmap.height.toFloat() / 2f, + bitmap.width.toFloat() / 2f, -bitmap.height.toFloat() / 2f, + bitmap.width.toFloat() / 2f, bitmap.height.toFloat() / 2f, + ) + + // initialize vertex byte buffer for shape coordinates + // (number of coordinate values * 4 bytes per float) + vertexPositionBuffer = + ByteBuffer.allocateDirect(vertexPositionData.size * BYTES_PER_FLOAT).run { + // use the device hardware's native byte order + order(ByteOrder.nativeOrder()) + + // create a floating point buffer from the ByteBuffer + asFloatBuffer().apply { + // add the coordinates to the FloatBuffer + put(vertexPositionData) + // set the buffer to read the first coordinate + rewind() + } + } + } + + override fun initialize() { + val maxAttrib = IntArray(1) + GLES20.glGetIntegerv(GLES20.GL_MAX_VERTEX_ATTRIBS, maxAttrib, 0) + Logger.e(TAG, "Max vertex attributes: ${maxAttrib[0]}") + + // load and compile shaders + vertexShader = loadShader( + GLES20.GL_VERTEX_SHADER, + VERTEX_SHADER_CODE + ).also { checkCompileStatus(it) } + + fragmentShader = loadShader( + GLES20.GL_FRAGMENT_SHADER, + FRAGMENT_SHADER_CODE + ).also { checkCompileStatus(it) } + + // create empty OpenGL ES Program + program = GLES20.glCreateProgram().also { + checkError("glCreateProgram") + // add the vertex shader to program + GLES20.glAttachShader(it, vertexShader).also { checkError("glAttachShader") } + + // add the fragment shader to program + GLES20.glAttachShader(it, fragmentShader).also { checkError("glAttachShader") } + + // creates OpenGL ES program executables + GLES20.glLinkProgram(it).also { + checkError("glLinkProgram") + } + } + + // get handle to screen matrix(fragment shader's uScreen member) + screenMatrixHandle = + GLES20.glGetUniformLocation(program, "uScreen").also { checkError("glGetAttribLocation") } + + // get handle to rotation matrix(fragment shader's uRotation member) + rotationMatrixHandle = + GLES20.glGetUniformLocation(program, "uRotation").also { checkError("glGetAttribLocation") } + + translateMatrixHandle = + GLES20.glGetUniformLocation(program, "uTranslate") + .also { checkError("glGetAttribLocation") } + + // get handle to vertex shader's vPosition member + vertexPositionHandle = + GLES20.glGetAttribLocation(program, "vPosition").also { checkError("glGetAttribLocation") } + + // get handle to texture coordinate(vertex shader's vCoordinate member) + texturePositionHandle = + GLES20.glGetAttribLocation(program, "vCoordinate") + .also { checkError("glGetAttribLocation") } + + // get handle to fragment shader's vColor member + textureHandle = + GLES20.glGetUniformLocation(program, "vTexture").also { checkError("glGetAttribLocation") } + } + + override fun render() { + super.render() + if (program != 0) { + // Add program to OpenGL ES environment + GLES20.glUseProgram(program).also { + checkError("glUseProgram") + } + + GLES20.glUniformMatrix4fv( + screenMatrixHandle, + screenMatrixBuffer.limit() / screenMatrixData.size, + false, + screenMatrixBuffer + ) + + val rotationBuffer = getMatrixBuffer(rotationMatrixData) + GLES20.glUniformMatrix4fv( + rotationMatrixHandle, + rotationBuffer.limit() / rotationMatrixData.size, + false, + rotationBuffer + ) + + val translateBuffer = getMatrixBuffer(translateMatrix) + GLES20.glUniformMatrix4fv( + translateMatrixHandle, + rotationBuffer.limit() / translateMatrix.size, + false, + translateBuffer + ) + + createTexture() + + // Activate the first texture (GL_TEXTURE0) and bind it to our handle + GLES20.glActiveTexture(GLES20.GL_TEXTURE0) + GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, textures[0]) + + // Textures + GLES20.glUniform1i(textureHandle, 0) + + // set the viewport and a fixed, white background + GLES20.glClearColor(1f, 1f, 1f, 1f) + + // Enable a handle to the vertices + GLES20.glEnableVertexAttribArray(vertexPositionHandle) + .also { checkError("glEnableVertexAttribArray") } + + // Prepare the vertex coordinate data + GLES20.glVertexAttribPointer( + vertexPositionHandle, COORDS_PER_VERTEX, + GLES20.GL_FLOAT, false, + VERTEX_STRIDE, vertexPositionBuffer + ).also { checkError("glVertexAttribPointer") } + + // Enable a handle to the tex position + GLES20.glEnableVertexAttribArray(texturePositionHandle) + .also { checkError("glEnableVertexAttribArray") } + + // Prepare the texture coordinate data + GLES20.glVertexAttribPointer( + texturePositionHandle, COORDS_PER_VERTEX, + GLES20.GL_FLOAT, false, + VERTEX_STRIDE, texturePositionBuffer + ).also { checkError("glVertexAttribPointer") } + + // Draw the background + GLES20.glDrawArrays(GLES20.GL_TRIANGLE_STRIP, 0, VERTEX_COUNT) + .also { checkError("glDrawArrays") } + + + // Clearing + GLES20.glDisableVertexAttribArray(vertexPositionHandle) + GLES20.glDisableVertexAttribArray(texturePositionHandle) + GLES20.glBindBuffer(GLES20.GL_ARRAY_BUFFER, 0) + GLES20.glUseProgram(0) + } + } + + fun contextLost() { + Logger.w(TAG, "contextLost") + program = 0 + } + + override fun deinitialize() { + if (program != 0) { + // Disable vertex array + GLES20.glDisableVertexAttribArray(vertexPositionHandle) + GLES20.glDetachShader(program, vertexShader) + GLES20.glDetachShader(program, fragmentShader) + GLES20.glDeleteShader(vertexShader) + GLES20.glDeleteShader(fragmentShader) + GLES20.glDeleteTextures(textures.size, textures, 0) + GLES20.glDeleteProgram(program) + program = 0 + } + } + + private fun checkCompileStatus(shader: Int) { + if (BuildConfig.DEBUG) { + val isCompiled = IntArray(1) + GLES20.glGetShaderiv(shader, GLES20.GL_COMPILE_STATUS, isCompiled, 0) + if (isCompiled[0] == GLES20.GL_FALSE) { + val infoLog = GLES20.glGetShaderInfoLog(program) + throw RuntimeException("checkCompileStatus error: $infoLog") + } + } + } + + private fun createTexture() { + if (!bitmap.isRecycled) { + // generate texture + GLES20.glGenTextures(1, textures, 0) + // generate texture + GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, textures[0]) + // set the color filter is reduced pixel color closest to the coordinates of a pixel in texture drawn as required + GLES20.glTexParameterf( + GLES20.GL_TEXTURE_2D, + GLES20.GL_TEXTURE_MIN_FILTER, + GLES20.GL_NEAREST.toFloat() + ) + // set the amplification filter using texture coordinates to the nearest number of colors, obtained by the weighted averaging algorithm requires pixel color drawn + GLES20.glTexParameterf( + GLES20.GL_TEXTURE_2D, + GLES20.GL_TEXTURE_MAG_FILTER, + GLES20.GL_LINEAR.toFloat() + ) + // Set the circumferential direction S, texture coordinates taken to [1 / 2n, 1-1 / 2n]. Will never lead to integration and border + GLES20.glTexParameterf( + GLES20.GL_TEXTURE_2D, + GLES20.GL_TEXTURE_WRAP_S, + GLES20.GL_CLAMP_TO_EDGE.toFloat() + ) + // Set the circumferential direction T, taken to texture coordinates [1 / 2n, 1-1 / 2n]. Will never lead to integration and border + GLES20.glTexParameterf( + GLES20.GL_TEXTURE_2D, + GLES20.GL_TEXTURE_WRAP_T, + GLES20.GL_CLAMP_TO_EDGE.toFloat() + ) + // The parameters specified above, generates a 2D texture + GLUtils.texImage2D(GLES20.GL_TEXTURE_2D, 0, bitmap, 0) + bitmap.recycle() + } + } + + fun updateBitmap(bitmap: Bitmap) { + this.bitmap = bitmap + onSizeChanged(width, height) + } + + fun rotate(bearing: Float) { + Matrix.setIdentityM(rotationMatrixData, 0) + Matrix.rotateM(rotationMatrixData, 0, bearing, 0f, 0f, 1f) + } + + companion object { + private const val TAG = "testtest" + + // number of coordinates per vertex in this array + private const val COORDS_PER_VERTEX = 2 + private const val BYTES_PER_FLOAT = 4 + private const val VERTEX_STRIDE = COORDS_PER_VERTEX * BYTES_PER_FLOAT // 4 bytes per vertex + private const val VERTEX_COUNT = 4 // 4 vertex in total + + private val VERTEX_SHADER_CODE = """ + uniform mat4 uScreen; + uniform mat4 uTranslate; + uniform mat4 uRotation; + attribute vec2 vPosition; + attribute vec2 vCoordinate; + varying vec2 aCoordinate; + void main() { + aCoordinate = vCoordinate; + gl_Position = uScreen * uTranslate * uRotation * vec4(vPosition, 0.0, 1.0); + } + """.trimIndent() + + private val FRAGMENT_SHADER_CODE = """ + precision mediump float; + uniform sampler2D vTexture; + varying vec2 aCoordinate; + void main() { + gl_FragColor = texture2D(vTexture, aCoordinate); + } + """.trimIndent() + + private fun loadShader(type: Int, shaderCode: String): Int { + // create a vertex shader type (GLES20.GL_VERTEX_SHADER) + // or a fragment shader type (GLES20.GL_FRAGMENT_SHADER) + return GLES20.glCreateShader(type).also { shader -> + + // add the source code to the shader and compile it + GLES20.glShaderSource(shader, shaderCode) + GLES20.glCompileShader(shader) + } + } + + private fun checkError(cmd: String? = null) { + if (BuildConfig.DEBUG) { + when (val error = GLES20.glGetError()) { + GLES20.GL_NO_ERROR -> { + Logger.d(TAG, "$cmd -> no error") + } + GLES20.GL_INVALID_ENUM -> Logger.e(TAG, "$cmd -> error in gl: GL_INVALID_ENUM") + GLES20.GL_INVALID_VALUE -> Logger.e(TAG, "$cmd -> error in gl: GL_INVALID_VALUE") + GLES20.GL_INVALID_OPERATION -> Logger.e( + TAG, + "$cmd -> error in gl: GL_INVALID_OPERATION" + ) + GLES20.GL_INVALID_FRAMEBUFFER_OPERATION -> Logger.e( + TAG, + "$cmd -> error in gl: GL_INVALID_FRAMEBUFFER_OPERATION" + ) + GLES20.GL_OUT_OF_MEMORY -> Logger.e(TAG, "$cmd -> error in gl: GL_OUT_OF_MEMORY") + else -> Logger.e(TAG, "$cmd -> error in gl: $error") + } + } + } + } +} \ No newline at end of file diff --git a/extension-androidauto/src/main/java/com/mapbox/maps/extension/androidauto/CompassWidget.kt b/extension-androidauto/src/main/java/com/mapbox/maps/extension/androidauto/CompassWidget.kt new file mode 100644 index 0000000000..92219cbebf --- /dev/null +++ b/extension-androidauto/src/main/java/com/mapbox/maps/extension/androidauto/CompassWidget.kt @@ -0,0 +1,13 @@ +package com.mapbox.maps.extension.androidauto + +import android.content.Context +import android.graphics.BitmapFactory + +class CompassWidget(context: Context) : BaseWidgetRenderer( + bitmap = BitmapFactory.decodeResource(context.resources, R.drawable.mapbox_compass_icon), + position = WidgetPosition.TOP_RIGHT, + marginBottom = 10f, + marginLeft = 10f, + marginRight = 20f, + marginTop = 30f +) \ No newline at end of file diff --git a/extension-androidauto/src/main/java/com/mapbox/maps/extension/androidauto/LogoWidget.kt b/extension-androidauto/src/main/java/com/mapbox/maps/extension/androidauto/LogoWidget.kt new file mode 100644 index 0000000000..9d70c94c72 --- /dev/null +++ b/extension-androidauto/src/main/java/com/mapbox/maps/extension/androidauto/LogoWidget.kt @@ -0,0 +1,13 @@ +package com.mapbox.maps.extension.androidauto + +import android.content.Context +import android.graphics.BitmapFactory + +class LogoWidget(context: Context) : BaseWidgetRenderer( + bitmap = BitmapFactory.decodeResource(context.resources, R.drawable.mapbox_logo_icon), + position = WidgetPosition.BOTTOM_LEFT, + marginBottom = 20f, + marginLeft = 20f, + marginRight = 20f, + marginTop = 20f +) \ No newline at end of file diff --git a/extension-androidauto/src/main/java/com/mapbox/maps/extension/androidauto/MapboxCarUtils.kt b/extension-androidauto/src/main/java/com/mapbox/maps/extension/androidauto/MapboxCarUtils.kt index b1a1a02ad6..cd58c491b2 100644 --- a/extension-androidauto/src/main/java/com/mapbox/maps/extension/androidauto/MapboxCarUtils.kt +++ b/extension-androidauto/src/main/java/com/mapbox/maps/extension/androidauto/MapboxCarUtils.kt @@ -57,11 +57,13 @@ fun Session.initMapSurface( mapInitOptions: MapInitOptions = MapInitOptions(carContext), scrollListener: OnMapScrollListener? = null, scaleListener: OnMapScaleListener? = null, + callback: SurfaceCallback? = null, mapSurfaceReadyCallback: MapSurfaceReadyCallback ) { var mapSurface: MapSurface? = null val surfaceCallback: SurfaceCallback = object : SurfaceCallback { override fun onSurfaceAvailable(surfaceContainer: SurfaceContainer) { + callback?.onSurfaceAvailable(surfaceContainer) synchronized(this) { Logger.i(TAG, "Surface available $surfaceContainer") surfaceContainer.surface?.let { surface -> diff --git a/extension-androidauto/src/main/java/com/mapbox/maps/extension/androidauto/WidgetPosition.kt b/extension-androidauto/src/main/java/com/mapbox/maps/extension/androidauto/WidgetPosition.kt new file mode 100644 index 0000000000..326275f9ae --- /dev/null +++ b/extension-androidauto/src/main/java/com/mapbox/maps/extension/androidauto/WidgetPosition.kt @@ -0,0 +1,8 @@ +package com.mapbox.maps.extension.androidauto + +enum class WidgetPosition { + TOP_LEFT, + TOP_RIGHT, + BOTTOM_LEFT, + BOTTOM_RIGHT +} \ No newline at end of file diff --git a/sdk/src/main/java/com/mapbox/maps/MapControllable.kt b/sdk/src/main/java/com/mapbox/maps/MapControllable.kt index ec5a74b4b1..4ca49d7593 100644 --- a/sdk/src/main/java/com/mapbox/maps/MapControllable.kt +++ b/sdk/src/main/java/com/mapbox/maps/MapControllable.kt @@ -3,6 +3,7 @@ package com.mapbox.maps import android.graphics.Bitmap import android.view.MotionEvent import com.mapbox.maps.renderer.OnFpsChangedListener +import com.mapbox.maps.renderer.Widget /** * MapControllable interface is the gateway for public API to talk to the internal map controller. @@ -73,4 +74,10 @@ interface MapControllable : MapboxLifecycleObserver { * Set [OnFpsChangedListener] to get map rendering FPS. */ fun setOnFpsChangedListener(listener: OnFpsChangedListener) + + /** + * Add static image widget to the map. + */ + @MapboxExperimental + fun addWidget(widget: Widget) } \ No newline at end of file diff --git a/sdk/src/main/java/com/mapbox/maps/MapController.kt b/sdk/src/main/java/com/mapbox/maps/MapController.kt index eec807afe9..ce13cafe1f 100644 --- a/sdk/src/main/java/com/mapbox/maps/MapController.kt +++ b/sdk/src/main/java/com/mapbox/maps/MapController.kt @@ -41,6 +41,7 @@ import com.mapbox.maps.plugin.scalebar.ScaleBarPluginImpl import com.mapbox.maps.plugin.viewport.ViewportPluginImpl import com.mapbox.maps.renderer.MapboxRenderer import com.mapbox.maps.renderer.OnFpsChangedListener +import com.mapbox.maps.renderer.Widget import java.lang.ref.WeakReference internal class MapController : MapPluginProviderDelegate, MapControllable { @@ -205,6 +206,10 @@ internal class MapController : MapPluginProviderDelegate, MapControllable { renderer.setOnFpsChangedListener(listener) } + override fun addWidget(widget: Widget) { + renderer.renderThread.addWidget(widget) + } + // // Telemetry // diff --git a/sdk/src/main/java/com/mapbox/maps/MapSurface.kt b/sdk/src/main/java/com/mapbox/maps/MapSurface.kt index 2d50e5e4f8..679c73c955 100644 --- a/sdk/src/main/java/com/mapbox/maps/MapSurface.kt +++ b/sdk/src/main/java/com/mapbox/maps/MapSurface.kt @@ -8,6 +8,7 @@ import com.mapbox.maps.plugin.MapPlugin import com.mapbox.maps.plugin.delegates.MapPluginProviderDelegate import com.mapbox.maps.renderer.MapboxSurfaceRenderer import com.mapbox.maps.renderer.OnFpsChangedListener +import com.mapbox.maps.renderer.Widget /** * A [MapSurface] provides an embeddable map interface. @@ -27,7 +28,7 @@ import com.mapbox.maps.renderer.OnFpsChangedListener */ class MapSurface @JvmOverloads constructor( context: Context, - private val surface: Surface, + val surface: Surface, mapInitOptions: MapInitOptions = MapInitOptions(context) ) : MapPluginProviderDelegate, MapControllable { @@ -178,6 +179,15 @@ class MapSurface @JvmOverloads constructor( mapController.onLowMemory() } + /** + * Add static image widget to the map. + */ + @MapboxExperimental + override fun addWidget(widget: Widget) { + mapController.addWidget(widget) + } + + /** * Get the plugin instance. * diff --git a/sdk/src/main/java/com/mapbox/maps/MapView.kt b/sdk/src/main/java/com/mapbox/maps/MapView.kt index c02233a3ff..22e71281c8 100644 --- a/sdk/src/main/java/com/mapbox/maps/MapView.kt +++ b/sdk/src/main/java/com/mapbox/maps/MapView.kt @@ -19,6 +19,7 @@ import com.mapbox.maps.plugin.delegates.MapPluginProviderDelegate import com.mapbox.maps.renderer.MapboxSurfaceHolderRenderer import com.mapbox.maps.renderer.MapboxTextureViewRenderer import com.mapbox.maps.renderer.OnFpsChangedListener +import com.mapbox.maps.renderer.Widget import com.mapbox.maps.renderer.egl.EGLCore import com.mapbox.maps.viewannotation.ViewAnnotationManager @@ -309,6 +310,14 @@ open class MapView : FrameLayout, MapPluginProviderDelegate, MapControllable { mapController.setOnFpsChangedListener(listener) } + /** + * Add static image widget to the map. + */ + @MapboxExperimental + override fun addWidget(widget: Widget) { + mapController.addWidget(widget) + } + /** * Interface for getting snapshot result [Bitmap]. */ diff --git a/sdk/src/main/java/com/mapbox/maps/renderer/MapboxRenderThread.kt b/sdk/src/main/java/com/mapbox/maps/renderer/MapboxRenderThread.kt index e83392ee6a..202aad0b8e 100644 --- a/sdk/src/main/java/com/mapbox/maps/renderer/MapboxRenderThread.kt +++ b/sdk/src/main/java/com/mapbox/maps/renderer/MapboxRenderThread.kt @@ -11,6 +11,7 @@ import com.mapbox.common.Logger import com.mapbox.maps.renderer.egl.EGLCore import java.util.LinkedList import java.util.concurrent.ConcurrentLinkedQueue +import java.util.concurrent.CopyOnWriteArrayList import java.util.concurrent.locks.ReentrantLock import javax.microedition.khronos.egl.EGL10 import javax.microedition.khronos.egl.EGL11 @@ -39,6 +40,8 @@ internal class MapboxRenderThread : Choreographer.FrameCallback { @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE) internal val nonRenderEventQueue = ConcurrentLinkedQueue() + private val widgetList = CopyOnWriteArrayList() + private var surface: Surface? = null @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE) internal var eglSurface: EGLSurface @@ -205,6 +208,7 @@ internal class MapboxRenderThread : Choreographer.FrameCallback { width = width, height = height ) + widgetList.forEach { it.onSizeChanged(width, height) } sizeChanged = false } } @@ -224,6 +228,10 @@ internal class MapboxRenderThread : Choreographer.FrameCallback { // it makes sense to execute them after drawing a map but before swapping buffers // **note** this queue also holds snapshot tasks drainQueue(renderEventQueue) + // render all the widgets + widgetList.forEach { + it.render() + } when (val swapStatus = eglCore.swapBuffers(eglSurface)) { EGL10.EGL_SUCCESS -> {} EGL11.EGL_CONTEXT_LOST -> { @@ -336,6 +344,11 @@ internal class MapboxRenderThread : Choreographer.FrameCallback { } } + fun addWidget(widget: Widget) { + widget.onSizeChanged(this.width, this.height) + widgetList.add(widget) + } + @WorkerThread internal fun processAndroidSurface(surface: Surface, width: Int, height: Int) { if (this.surface != surface) { @@ -347,6 +360,7 @@ internal class MapboxRenderThread : Choreographer.FrameCallback { } this.width = width this.height = height + widgetList.forEach { it.onSizeChanged(width, height) } renderEventQueue.removeAll { it.eventType == EventType.SDK } nonRenderEventQueue.removeAll { it.eventType == EventType.SDK } // we do not want to clear render events scheduled by user diff --git a/sdk/src/main/java/com/mapbox/maps/renderer/SurfaceUtils.kt b/sdk/src/main/java/com/mapbox/maps/renderer/SurfaceUtils.kt new file mode 100644 index 0000000000..9aeccbe3d0 --- /dev/null +++ b/sdk/src/main/java/com/mapbox/maps/renderer/SurfaceUtils.kt @@ -0,0 +1,26 @@ +package com.mapbox.maps.renderer + +import android.graphics.Bitmap +import android.graphics.Rect +import android.util.Log +import android.view.Surface +import java.lang.Exception + +fun Surface.drawWidget(bitmap: Bitmap) { + if (!isValid) { + // Surface is not available, or has been destroyed, skip this frame. + Log.e("testtest", "Surface is not available, or has been destroyed, skip this frame.") + return + } + val height = bitmap.height + val width = bitmap.width + try { + this.lockCanvas(Rect(0, 0, width, height))?.let { canvas -> + Log.e("testtest", "lockCanvas done") + canvas.drawBitmap(bitmap, 0f, 0f, null) + this.unlockCanvasAndPost(canvas) + } + } catch (e:Exception) { + Log.e("testtest", e.toString()) + } +} \ No newline at end of file diff --git a/sdk/src/main/java/com/mapbox/maps/renderer/Widget.kt b/sdk/src/main/java/com/mapbox/maps/renderer/Widget.kt new file mode 100644 index 0000000000..55aca59af3 --- /dev/null +++ b/sdk/src/main/java/com/mapbox/maps/renderer/Widget.kt @@ -0,0 +1,35 @@ +package com.mapbox.maps.renderer + +import android.opengl.GLES20 +import android.util.Log +import androidx.annotation.CallSuper + +abstract class Widget { + + private var isInit = false + var width = 0 + var height = 0 + + @CallSuper + open fun onSizeChanged(width: Int, height: Int) { + Log.e("testtest", "onSize changed: $width, $height") + this.width = width + this.height = height + } + + abstract fun initialize() + + // TODO make params customizable + @CallSuper + open fun render() { + if (!isInit) { + isInit = true + initialize() + } + GLES20.glUseProgram(0) + GLES20.glBindBuffer(GLES20.GL_ARRAY_BUFFER, 0) + GLES20.glBindBuffer(GLES20.GL_ELEMENT_ARRAY_BUFFER, 0) + } + + abstract fun deinitialize() +} \ No newline at end of file From 07589762633db3577dea50f113bf8878baca5c41 Mon Sep 17 00:00:00 2001 From: yunik Date: Tue, 11 Jan 2022 15:03:52 +0200 Subject: [PATCH 02/13] Draw widgets with separate GL context to a texture, then draw this texture on top of the map. --- .../testapp/auto/car/CarCameraController.kt | 1 - .../maps/testapp/auto/car/MapSession.kt | 20 +- .../customlayer/ExampleCustomLayer.kt | 4 - .../extension/androidauto/BackgroundWidget.kt | 196 -------- .../androidauto/BaseWidgetRenderer.kt | 450 ------------------ .../extension/androidauto/CompassWidget.kt | 15 +- .../maps/extension/androidauto/LogoWidget.kt | 15 +- .../extension/androidauto/MapboxCarUtils.kt | 6 - .../extension/androidauto/WidgetPosition.kt | 8 - .../java/com/mapbox/maps/MapControllable.kt | 2 +- .../java/com/mapbox/maps/MapController.kt | 2 +- .../main/java/com/mapbox/maps/MapSurface.kt | 3 +- sdk/src/main/java/com/mapbox/maps/MapView.kt | 2 +- .../maps/renderer/MapboxRenderThread.kt | 55 ++- .../mapbox/maps/renderer/MapboxRenderer.kt | 1 + .../maps/renderer/MapboxSurfaceRenderer.kt | 11 + .../renderer/MapboxTextureViewRenderer.kt | 13 + .../maps/renderer/MapboxWidgetRenderer.kt | 171 +++++++ .../com/mapbox/maps/renderer/SurfaceUtils.kt | 26 - .../java/com/mapbox/maps/renderer/Widget.kt | 35 -- .../com/mapbox/maps/renderer/egl/EGLCore.kt | 26 +- .../com/mapbox/maps/renderer/gl/GlUtils.kt | 56 +++ .../maps/renderer/gl/TextureRenderer.kt | 157 ++++++ .../maps/renderer/widget/BitmapWidget.kt | 34 ++ .../renderer/widget/BitmapWidgetRenderer.kt | 323 +++++++++++++ .../com/mapbox/maps/renderer/widget/Widget.kt | 13 + .../maps/renderer/widget/WidgetPosition.kt | 21 + .../maps/renderer/widget/WidgetRenderer.kt | 10 + sdk/src/test/java/com/mapbox/TestUtils.kt | 35 ++ .../maps/renderer/MapboxRenderThreadTest.kt | 134 +++++- 30 files changed, 1064 insertions(+), 781 deletions(-) delete mode 100644 extension-androidauto/src/main/java/com/mapbox/maps/extension/androidauto/BackgroundWidget.kt delete mode 100644 extension-androidauto/src/main/java/com/mapbox/maps/extension/androidauto/BaseWidgetRenderer.kt delete mode 100644 extension-androidauto/src/main/java/com/mapbox/maps/extension/androidauto/WidgetPosition.kt create mode 100644 sdk/src/main/java/com/mapbox/maps/renderer/MapboxWidgetRenderer.kt delete mode 100644 sdk/src/main/java/com/mapbox/maps/renderer/SurfaceUtils.kt delete mode 100644 sdk/src/main/java/com/mapbox/maps/renderer/Widget.kt create mode 100644 sdk/src/main/java/com/mapbox/maps/renderer/gl/GlUtils.kt create mode 100644 sdk/src/main/java/com/mapbox/maps/renderer/gl/TextureRenderer.kt create mode 100644 sdk/src/main/java/com/mapbox/maps/renderer/widget/BitmapWidget.kt create mode 100644 sdk/src/main/java/com/mapbox/maps/renderer/widget/BitmapWidgetRenderer.kt create mode 100644 sdk/src/main/java/com/mapbox/maps/renderer/widget/Widget.kt create mode 100644 sdk/src/main/java/com/mapbox/maps/renderer/widget/WidgetPosition.kt create mode 100644 sdk/src/main/java/com/mapbox/maps/renderer/widget/WidgetRenderer.kt create mode 100644 sdk/src/test/java/com/mapbox/TestUtils.kt diff --git a/android-auto-app/src/main/java/com/mapbox/maps/testapp/auto/car/CarCameraController.kt b/android-auto-app/src/main/java/com/mapbox/maps/testapp/auto/car/CarCameraController.kt index 95de402314..9c4e79a768 100644 --- a/android-auto-app/src/main/java/com/mapbox/maps/testapp/auto/car/CarCameraController.kt +++ b/android-auto-app/src/main/java/com/mapbox/maps/testapp/auto/car/CarCameraController.kt @@ -97,5 +97,4 @@ class CarCameraController : OnIndicatorPositionChangedListener, OnIndicatorBeari private const val INITIAL_PITCH = 75.0 private const val TAG = "CarCameraController" } - } \ No newline at end of file diff --git a/android-auto-app/src/main/java/com/mapbox/maps/testapp/auto/car/MapSession.kt b/android-auto-app/src/main/java/com/mapbox/maps/testapp/auto/car/MapSession.kt index 3871f84d30..1184309a4e 100644 --- a/android-auto-app/src/main/java/com/mapbox/maps/testapp/auto/car/MapSession.kt +++ b/android-auto-app/src/main/java/com/mapbox/maps/testapp/auto/car/MapSession.kt @@ -4,8 +4,6 @@ import android.Manifest.permission.ACCESS_FINE_LOCATION import android.content.Intent import android.content.pm.PackageManager.PERMISSION_GRANTED import android.content.res.Configuration -import android.graphics.BitmapFactory -import android.util.Log import androidx.car.app.* import com.mapbox.maps.EdgeInsets import com.mapbox.maps.MapSurface @@ -19,7 +17,6 @@ import com.mapbox.maps.extension.style.sources.generated.rasterDemSource import com.mapbox.maps.extension.style.style import com.mapbox.maps.extension.style.terrain.generated.terrain import com.mapbox.maps.plugin.locationcomponent.location -import com.mapbox.maps.renderer.Widget import com.mapbox.maps.testapp.auto.R /** @@ -28,15 +25,15 @@ import com.mapbox.maps.testapp.auto.R class MapSession : Session() { private lateinit var mapSurface: MapSurface private val carCameraController = CarCameraController() - private val widgetList = mutableListOf() override fun onCreateScreen(intent: Intent): Screen { val mapScreen = MapScreen(carContext) - widgetList.add(LogoWidget(carContext)) - widgetList.add(CompassWidget(carContext)) initMapSurface( scrollListener = carCameraController, ) { surface -> + val logo = LogoWidget(carContext) + val compass = CompassWidget(carContext) + mapSurface = surface carCameraController.init( mapSurface, @@ -50,14 +47,13 @@ class MapSession : Session() { mapScreen.setMapCameraController(carCameraController) loadStyle(surface) initLocationComponent(surface) - widgetList.forEach { surface.addWidget(it) } + + surface.addWidget(logo) + surface.addWidget(compass) + surface.getMapboxMap().apply { addOnCameraChangeListener { - widgetList.filterIsInstance().forEach { - it.rotate( - this.cameraState.bearing.toFloat().also { Log.e("testtest", it.toString()) } - ) - } + compass.rotate(this.cameraState.bearing.toFloat()) } } } diff --git a/app/src/main/java/com/mapbox/maps/testapp/examples/customlayer/ExampleCustomLayer.kt b/app/src/main/java/com/mapbox/maps/testapp/examples/customlayer/ExampleCustomLayer.kt index d15fb9086f..ba63832716 100644 --- a/app/src/main/java/com/mapbox/maps/testapp/examples/customlayer/ExampleCustomLayer.kt +++ b/app/src/main/java/com/mapbox/maps/testapp/examples/customlayer/ExampleCustomLayer.kt @@ -142,10 +142,6 @@ class ExampleCustomLayer : CustomLayerHost { -1.0f, 1.0f, 1.0f, 1.0f ) - // -0.8f, -0.8f, -// -1.0f, -0.8f, -// -0.8f, -1.0f, -// -1.0f, -1.0f private val VERTEX_COUNT = BACKGROUND_COORDINATES.size / COORDS_PER_VERTEX // Set color with red, green, blue and alpha (opacity) values diff --git a/extension-androidauto/src/main/java/com/mapbox/maps/extension/androidauto/BackgroundWidget.kt b/extension-androidauto/src/main/java/com/mapbox/maps/extension/androidauto/BackgroundWidget.kt deleted file mode 100644 index d936e88396..0000000000 --- a/extension-androidauto/src/main/java/com/mapbox/maps/extension/androidauto/BackgroundWidget.kt +++ /dev/null @@ -1,196 +0,0 @@ -package com.mapbox.maps.extension.androidauto - -import android.opengl.GLES20 -import com.mapbox.common.Logger -import com.mapbox.maps.renderer.Widget -import java.nio.ByteBuffer -import java.nio.ByteOrder -import java.nio.FloatBuffer - -class BackgroundWidget : Widget() { - private var program = 0 - private var positionHandle = 0 - private var colorHandle = 0 - private var vertexShader = 0 - private var fragmentShader = 0 - - private val vertexBuffer: FloatBuffer by lazy { - // initialize vertex byte buffer for shape coordinates - // (number of coordinate values * 4 bytes per float) - ByteBuffer.allocateDirect(BACKGROUND_COORDINATES.size * BYTES_PER_FLOAT).run { - // use the device hardware's native byte order - order(ByteOrder.nativeOrder()) - - // create a floating point buffer from the ByteBuffer - asFloatBuffer().apply { - // add the coordinates to the FloatBuffer - put(BACKGROUND_COORDINATES) - // set the buffer to read the first coordinate - rewind() - } - } - } - - override fun initialize() { - val maxAttrib = IntArray(1) - GLES20.glGetIntegerv(GLES20.GL_MAX_VERTEX_ATTRIBS, maxAttrib, 0) - Logger.d(TAG, "Max vertex attributes: ${maxAttrib[0]}") - - // load and compile shaders - vertexShader = loadShader( - GLES20.GL_VERTEX_SHADER, - VERTEX_SHADER_CODE - ).also { checkCompileStatus(it) } - - fragmentShader = loadShader( - GLES20.GL_FRAGMENT_SHADER, - FRAGMENT_SHADER_CODE - ).also { checkCompileStatus(it) } - - // create empty OpenGL ES Program - program = GLES20.glCreateProgram().also { - checkError("glCreateProgram") - // add the vertex shader to program - GLES20.glAttachShader(it, vertexShader).also { checkError("glAttachShader") } - - // add the fragment shader to program - GLES20.glAttachShader(it, fragmentShader).also { checkError("glAttachShader") } - - // creates OpenGL ES program executables - GLES20.glLinkProgram(it).also { - checkError("glLinkProgram") - } - } - - // get handle to vertex shader's vPosition member - positionHandle = - GLES20.glGetAttribLocation(program, "a_pos").also { checkError("glGetAttribLocation") } - - // get handle to fragment shader's vColor member - colorHandle = GLES20.glGetUniformLocation(program, "fill_color") - .also { checkError("glGetUniformLocation") } - } - - override fun render() { - super.render() - if (program != 0) { - // Add program to OpenGL ES environment - GLES20.glUseProgram(program).also { checkError("glUseProgram") } - - GLES20.glBindBuffer(GLES20.GL_ARRAY_BUFFER, positionHandle) - - // Enable a handle to the vertices - GLES20.glEnableVertexAttribArray(positionHandle) - .also { checkError("glEnableVertexAttribArray") } - - // Prepare the coordinate data - GLES20.glVertexAttribPointer( - positionHandle, COORDS_PER_VERTEX, - GLES20.GL_FLOAT, false, - VERTEX_STRIDE, vertexBuffer - ).also { checkError("glVertexAttribPointer") } - - // Set color for drawing the background - GLES20.glUniform4fv(colorHandle, 1, color, 0).also { checkError("glUniform4fv") } - - // Draw the background - GLES20.glDrawArrays(GLES20.GL_TRIANGLE_STRIP, 0, VERTEX_COUNT) - .also { checkError("glDrawArrays") } - } - } - - fun contextLost() { - Logger.w(TAG, "contextLost") - program = 0 - } - - override fun deinitialize() { - if (program != 0) { - // Disable vertex array - GLES20.glDisableVertexAttribArray(positionHandle) - GLES20.glDetachShader(program, vertexShader) - GLES20.glDetachShader(program, fragmentShader) - GLES20.glDeleteShader(vertexShader) - GLES20.glDeleteShader(fragmentShader) - GLES20.glDeleteProgram(program) - program = 0 - } - } - - private fun checkCompileStatus(shader: Int) { - if (BuildConfig.DEBUG) { - val isCompiled = IntArray(1) - GLES20.glGetShaderiv(shader, GLES20.GL_COMPILE_STATUS, isCompiled, 0) - if (isCompiled[0] == GLES20.GL_FALSE) { - val infoLog = GLES20.glGetShaderInfoLog(program) - throw RuntimeException("checkCompileStatus error: $infoLog") - } - } - } - - companion object { - private const val TAG = "ExampleCustomLayer" - - // number of coordinates per vertex in this array - private const val COORDS_PER_VERTEX = 2 - private const val BYTES_PER_FLOAT = 4 - private const val VERTEX_STRIDE = COORDS_PER_VERTEX * BYTES_PER_FLOAT // 4 bytes per vertex - private val BACKGROUND_COORDINATES = floatArrayOf( // in counterclockwise order: -// -1.0f, -1.0f, -// 1.0f, -1.0f, -// -1.0f, 1.0f, -// 1.0f, 1.0f - -0.8f, -0.8f, - -1.0f, -0.8f, - -0.8f, -1.0f, - -1.0f, -1.0f - ) - - private val VERTEX_COUNT = BACKGROUND_COORDINATES.size / COORDS_PER_VERTEX - - // Set color with red, green, blue and alpha (opacity) values - var color = floatArrayOf(0.0f, 1.0f, 0.0f, 1.0f) - - private val VERTEX_SHADER_CODE = """ - attribute vec2 a_pos; - void main() { - gl_Position = vec4(a_pos, 0.0, 1.0); - } - """.trimIndent() - - private val FRAGMENT_SHADER_CODE = """ - precision mediump float; - uniform vec4 fill_color; - void main() { - gl_FragColor = fill_color; - } - """.trimIndent() - - private fun loadShader(type: Int, shaderCode: String): Int { - // create a vertex shader type (GLES20.GL_VERTEX_SHADER) - // or a fragment shader type (GLES20.GL_FRAGMENT_SHADER) - return GLES20.glCreateShader(type).also { shader -> - - // add the source code to the shader and compile it - GLES20.glShaderSource(shader, shaderCode) - GLES20.glCompileShader(shader) - } - } - - private fun checkError(cmd: String? = null) { - if (BuildConfig.DEBUG) { - when (val error = GLES20.glGetError()) { - GLES20.GL_NO_ERROR -> { - Logger.d(TAG, "$cmd -> no error") - } - GLES20.GL_INVALID_ENUM -> throw RuntimeException("$cmd -> error in gl: GL_INVALID_ENUM") - GLES20.GL_INVALID_VALUE -> throw RuntimeException("$cmd -> error in gl: GL_INVALID_VALUE") - GLES20.GL_INVALID_OPERATION -> throw RuntimeException("$cmd -> error in gl: GL_INVALID_OPERATION") - GLES20.GL_INVALID_FRAMEBUFFER_OPERATION -> throw RuntimeException("$cmd -> error in gl: GL_INVALID_FRAMEBUFFER_OPERATION") - GLES20.GL_OUT_OF_MEMORY -> throw RuntimeException("$cmd -> error in gl: GL_OUT_OF_MEMORY") - else -> throw RuntimeException("$cmd -> error in gl: $error") - } - } - } - } -} \ No newline at end of file diff --git a/extension-androidauto/src/main/java/com/mapbox/maps/extension/androidauto/BaseWidgetRenderer.kt b/extension-androidauto/src/main/java/com/mapbox/maps/extension/androidauto/BaseWidgetRenderer.kt deleted file mode 100644 index 3a46182d70..0000000000 --- a/extension-androidauto/src/main/java/com/mapbox/maps/extension/androidauto/BaseWidgetRenderer.kt +++ /dev/null @@ -1,450 +0,0 @@ -package com.mapbox.maps.extension.androidauto - -import android.graphics.Bitmap -import android.opengl.GLES20 -import android.opengl.GLUtils -import android.opengl.Matrix -import android.util.Log -import com.mapbox.common.Logger -import com.mapbox.maps.BuildConfig -import com.mapbox.maps.renderer.Widget -import java.nio.ByteBuffer -import java.nio.ByteOrder -import java.nio.FloatBuffer - -open class BaseWidgetRenderer( - private var bitmap: Bitmap, - private val position: WidgetPosition = WidgetPosition.BOTTOM_LEFT, - private val marginLeft: Float = 0f, - private val marginTop: Float = 0f, - private val marginRight: Float = 0f, - private val marginBottom: Float = 0f, -) : Widget() { - private var program = 0 - private var vertexPositionHandle = 0 - private var texturePositionHandle = 0 - private var textureHandle = 0 - private var screenMatrixHandle = 0 - private var rotationMatrixHandle = 0 - private var translateMatrixHandle = 0 - private val textures = intArrayOf(0) - - private var vertexShader = 0 - private var fragmentShader = 0 - - private var heightOffset: Float = 0f - private var widthOffset: Float = 0f - - private lateinit var screenMatrixData: FloatArray - private lateinit var screenMatrixBuffer: FloatBuffer - - private val rotationMatrixData = FloatArray(16).apply { - Matrix.setIdentityM(this, 0) - } - - private val translateMatrix = FloatArray(16).apply { - Matrix.setIdentityM(this, 0) - } - -// private lateinit var rotationMatrixBuffer : FloatBuffer - - private fun getMatrixBuffer(matrixData: FloatArray): FloatBuffer { - return ByteBuffer.allocateDirect(matrixData.size * BYTES_PER_FLOAT).run { - // use the device hardware's native byte order - order(ByteOrder.nativeOrder()) - - // create a floating point buffer from the ByteBuffer - asFloatBuffer().apply { - // add the coordinates to the FloatBuffer - put(matrixData) - // set the buffer to read the first coordinate - rewind() - } - } - } - - private lateinit var vertexPositionData: FloatArray - private lateinit var vertexPositionBuffer: FloatBuffer - - init { - Matrix.setIdentityM(rotationMatrixData, 0) - } - - private val texturePositionData = floatArrayOf( - 0f, 0f, //Texture coordinate for V1 - 0f, 1f, - 1f, 0f, - 1f, 1f - ) - private val texturePositionBuffer: FloatBuffer by lazy { - getMatrixBuffer(texturePositionData) - } - - override fun onSizeChanged(width: Int, height: Int) { - super.onSizeChanged(width, height) - Log.e(TAG, "bitmap size: ${bitmap.width}, ${bitmap.height}") - Log.e(TAG, "screen size: ${width}, ${height}") - heightOffset = when (position) { - WidgetPosition.BOTTOM_LEFT -> height.toFloat() - bitmap.height.toFloat() / 2f - marginBottom - WidgetPosition.BOTTOM_RIGHT -> height.toFloat() - bitmap.height.toFloat() / 2f - marginBottom - WidgetPosition.TOP_LEFT -> marginTop + bitmap.height.toFloat() / 2f - WidgetPosition.TOP_RIGHT -> marginTop + bitmap.height.toFloat() / 2f - } - widthOffset = when (position) { - WidgetPosition.TOP_RIGHT -> width.toFloat() - bitmap.width.toFloat() / 2f - marginRight - WidgetPosition.BOTTOM_RIGHT -> width.toFloat() - bitmap.width.toFloat() / 2f - marginRight - WidgetPosition.TOP_LEFT -> marginLeft + bitmap.width.toFloat() / 2f - WidgetPosition.BOTTOM_LEFT -> marginLeft + bitmap.width.toFloat() / 2f - } - - Matrix.translateM( - translateMatrix, - 0, - widthOffset, - heightOffset, - 0f - ) - - // The uScreen matrix - // - // First of all, the only coordinate system that OpenGL understands - // put the center of the screen at the 0,0 position. The maximum value of - // the X axis is 1 (rightmost part of the screen) and the minimum is -1 - // (leftmost part of the screen). The same thing goes for the Y axis, - // where 1 is the top of the screen and -1 the bottom. - // - // However, when you're doing a 2d application you often need to think in 'pixels' - // (or something like that). If you have a 300x300 screen, you want to see the center - // at 150,150 not 0,0! - // - // The solution to this 'problem' is to multiply a matrix with your position to - // another matrix that will convert 'your' coordinates to the one OpenGL expects. - // There's no magic in this, only a bit of math. Try to multiply the uScreen matrix - // to the 150,150 position in a sheet of paper and look at the results. - // - // IMPORTANT: When trying to calculate the matrix on paper, you should treat the - // uScreen ROWS as COLUMNS and vice versa. This happens because OpenGL expect the - // matrix values ordered in a more efficient way, that unfortunately is different - // from the mathematical notation :( - screenMatrixData = floatArrayOf( - 2f / width.toFloat(), 0f, 0f, 0f, - 0f, -2f / height.toFloat(), 0f, 0f, - 0f, 0f, 0f, 0f, - -1f, 1f, 0f, 1f - ) - - // initialize vertex byte buffer for shape coordinates - // (number of coordinate values * 4 bytes per float) - screenMatrixBuffer = ByteBuffer.allocateDirect(screenMatrixData.size * BYTES_PER_FLOAT).run { - // use the device hardware's native byte order - order(ByteOrder.nativeOrder()) - - // create a floating point buffer from the ByteBuffer - asFloatBuffer().apply { - // add the coordinates to the FloatBuffer - put(screenMatrixData) - // set the buffer to read the first coordinate - rewind() - } - } - - vertexPositionData = floatArrayOf( - -bitmap.width.toFloat() / 2f, -bitmap.height.toFloat() / 2f, - -bitmap.width.toFloat() / 2f, bitmap.height.toFloat() / 2f, - bitmap.width.toFloat() / 2f, -bitmap.height.toFloat() / 2f, - bitmap.width.toFloat() / 2f, bitmap.height.toFloat() / 2f, - ) - - // initialize vertex byte buffer for shape coordinates - // (number of coordinate values * 4 bytes per float) - vertexPositionBuffer = - ByteBuffer.allocateDirect(vertexPositionData.size * BYTES_PER_FLOAT).run { - // use the device hardware's native byte order - order(ByteOrder.nativeOrder()) - - // create a floating point buffer from the ByteBuffer - asFloatBuffer().apply { - // add the coordinates to the FloatBuffer - put(vertexPositionData) - // set the buffer to read the first coordinate - rewind() - } - } - } - - override fun initialize() { - val maxAttrib = IntArray(1) - GLES20.glGetIntegerv(GLES20.GL_MAX_VERTEX_ATTRIBS, maxAttrib, 0) - Logger.e(TAG, "Max vertex attributes: ${maxAttrib[0]}") - - // load and compile shaders - vertexShader = loadShader( - GLES20.GL_VERTEX_SHADER, - VERTEX_SHADER_CODE - ).also { checkCompileStatus(it) } - - fragmentShader = loadShader( - GLES20.GL_FRAGMENT_SHADER, - FRAGMENT_SHADER_CODE - ).also { checkCompileStatus(it) } - - // create empty OpenGL ES Program - program = GLES20.glCreateProgram().also { - checkError("glCreateProgram") - // add the vertex shader to program - GLES20.glAttachShader(it, vertexShader).also { checkError("glAttachShader") } - - // add the fragment shader to program - GLES20.glAttachShader(it, fragmentShader).also { checkError("glAttachShader") } - - // creates OpenGL ES program executables - GLES20.glLinkProgram(it).also { - checkError("glLinkProgram") - } - } - - // get handle to screen matrix(fragment shader's uScreen member) - screenMatrixHandle = - GLES20.glGetUniformLocation(program, "uScreen").also { checkError("glGetAttribLocation") } - - // get handle to rotation matrix(fragment shader's uRotation member) - rotationMatrixHandle = - GLES20.glGetUniformLocation(program, "uRotation").also { checkError("glGetAttribLocation") } - - translateMatrixHandle = - GLES20.glGetUniformLocation(program, "uTranslate") - .also { checkError("glGetAttribLocation") } - - // get handle to vertex shader's vPosition member - vertexPositionHandle = - GLES20.glGetAttribLocation(program, "vPosition").also { checkError("glGetAttribLocation") } - - // get handle to texture coordinate(vertex shader's vCoordinate member) - texturePositionHandle = - GLES20.glGetAttribLocation(program, "vCoordinate") - .also { checkError("glGetAttribLocation") } - - // get handle to fragment shader's vColor member - textureHandle = - GLES20.glGetUniformLocation(program, "vTexture").also { checkError("glGetAttribLocation") } - } - - override fun render() { - super.render() - if (program != 0) { - // Add program to OpenGL ES environment - GLES20.glUseProgram(program).also { - checkError("glUseProgram") - } - - GLES20.glUniformMatrix4fv( - screenMatrixHandle, - screenMatrixBuffer.limit() / screenMatrixData.size, - false, - screenMatrixBuffer - ) - - val rotationBuffer = getMatrixBuffer(rotationMatrixData) - GLES20.glUniformMatrix4fv( - rotationMatrixHandle, - rotationBuffer.limit() / rotationMatrixData.size, - false, - rotationBuffer - ) - - val translateBuffer = getMatrixBuffer(translateMatrix) - GLES20.glUniformMatrix4fv( - translateMatrixHandle, - rotationBuffer.limit() / translateMatrix.size, - false, - translateBuffer - ) - - createTexture() - - // Activate the first texture (GL_TEXTURE0) and bind it to our handle - GLES20.glActiveTexture(GLES20.GL_TEXTURE0) - GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, textures[0]) - - // Textures - GLES20.glUniform1i(textureHandle, 0) - - // set the viewport and a fixed, white background - GLES20.glClearColor(1f, 1f, 1f, 1f) - - // Enable a handle to the vertices - GLES20.glEnableVertexAttribArray(vertexPositionHandle) - .also { checkError("glEnableVertexAttribArray") } - - // Prepare the vertex coordinate data - GLES20.glVertexAttribPointer( - vertexPositionHandle, COORDS_PER_VERTEX, - GLES20.GL_FLOAT, false, - VERTEX_STRIDE, vertexPositionBuffer - ).also { checkError("glVertexAttribPointer") } - - // Enable a handle to the tex position - GLES20.glEnableVertexAttribArray(texturePositionHandle) - .also { checkError("glEnableVertexAttribArray") } - - // Prepare the texture coordinate data - GLES20.glVertexAttribPointer( - texturePositionHandle, COORDS_PER_VERTEX, - GLES20.GL_FLOAT, false, - VERTEX_STRIDE, texturePositionBuffer - ).also { checkError("glVertexAttribPointer") } - - // Draw the background - GLES20.glDrawArrays(GLES20.GL_TRIANGLE_STRIP, 0, VERTEX_COUNT) - .also { checkError("glDrawArrays") } - - - // Clearing - GLES20.glDisableVertexAttribArray(vertexPositionHandle) - GLES20.glDisableVertexAttribArray(texturePositionHandle) - GLES20.glBindBuffer(GLES20.GL_ARRAY_BUFFER, 0) - GLES20.glUseProgram(0) - } - } - - fun contextLost() { - Logger.w(TAG, "contextLost") - program = 0 - } - - override fun deinitialize() { - if (program != 0) { - // Disable vertex array - GLES20.glDisableVertexAttribArray(vertexPositionHandle) - GLES20.glDetachShader(program, vertexShader) - GLES20.glDetachShader(program, fragmentShader) - GLES20.glDeleteShader(vertexShader) - GLES20.glDeleteShader(fragmentShader) - GLES20.glDeleteTextures(textures.size, textures, 0) - GLES20.glDeleteProgram(program) - program = 0 - } - } - - private fun checkCompileStatus(shader: Int) { - if (BuildConfig.DEBUG) { - val isCompiled = IntArray(1) - GLES20.glGetShaderiv(shader, GLES20.GL_COMPILE_STATUS, isCompiled, 0) - if (isCompiled[0] == GLES20.GL_FALSE) { - val infoLog = GLES20.glGetShaderInfoLog(program) - throw RuntimeException("checkCompileStatus error: $infoLog") - } - } - } - - private fun createTexture() { - if (!bitmap.isRecycled) { - // generate texture - GLES20.glGenTextures(1, textures, 0) - // generate texture - GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, textures[0]) - // set the color filter is reduced pixel color closest to the coordinates of a pixel in texture drawn as required - GLES20.glTexParameterf( - GLES20.GL_TEXTURE_2D, - GLES20.GL_TEXTURE_MIN_FILTER, - GLES20.GL_NEAREST.toFloat() - ) - // set the amplification filter using texture coordinates to the nearest number of colors, obtained by the weighted averaging algorithm requires pixel color drawn - GLES20.glTexParameterf( - GLES20.GL_TEXTURE_2D, - GLES20.GL_TEXTURE_MAG_FILTER, - GLES20.GL_LINEAR.toFloat() - ) - // Set the circumferential direction S, texture coordinates taken to [1 / 2n, 1-1 / 2n]. Will never lead to integration and border - GLES20.glTexParameterf( - GLES20.GL_TEXTURE_2D, - GLES20.GL_TEXTURE_WRAP_S, - GLES20.GL_CLAMP_TO_EDGE.toFloat() - ) - // Set the circumferential direction T, taken to texture coordinates [1 / 2n, 1-1 / 2n]. Will never lead to integration and border - GLES20.glTexParameterf( - GLES20.GL_TEXTURE_2D, - GLES20.GL_TEXTURE_WRAP_T, - GLES20.GL_CLAMP_TO_EDGE.toFloat() - ) - // The parameters specified above, generates a 2D texture - GLUtils.texImage2D(GLES20.GL_TEXTURE_2D, 0, bitmap, 0) - bitmap.recycle() - } - } - - fun updateBitmap(bitmap: Bitmap) { - this.bitmap = bitmap - onSizeChanged(width, height) - } - - fun rotate(bearing: Float) { - Matrix.setIdentityM(rotationMatrixData, 0) - Matrix.rotateM(rotationMatrixData, 0, bearing, 0f, 0f, 1f) - } - - companion object { - private const val TAG = "testtest" - - // number of coordinates per vertex in this array - private const val COORDS_PER_VERTEX = 2 - private const val BYTES_PER_FLOAT = 4 - private const val VERTEX_STRIDE = COORDS_PER_VERTEX * BYTES_PER_FLOAT // 4 bytes per vertex - private const val VERTEX_COUNT = 4 // 4 vertex in total - - private val VERTEX_SHADER_CODE = """ - uniform mat4 uScreen; - uniform mat4 uTranslate; - uniform mat4 uRotation; - attribute vec2 vPosition; - attribute vec2 vCoordinate; - varying vec2 aCoordinate; - void main() { - aCoordinate = vCoordinate; - gl_Position = uScreen * uTranslate * uRotation * vec4(vPosition, 0.0, 1.0); - } - """.trimIndent() - - private val FRAGMENT_SHADER_CODE = """ - precision mediump float; - uniform sampler2D vTexture; - varying vec2 aCoordinate; - void main() { - gl_FragColor = texture2D(vTexture, aCoordinate); - } - """.trimIndent() - - private fun loadShader(type: Int, shaderCode: String): Int { - // create a vertex shader type (GLES20.GL_VERTEX_SHADER) - // or a fragment shader type (GLES20.GL_FRAGMENT_SHADER) - return GLES20.glCreateShader(type).also { shader -> - - // add the source code to the shader and compile it - GLES20.glShaderSource(shader, shaderCode) - GLES20.glCompileShader(shader) - } - } - - private fun checkError(cmd: String? = null) { - if (BuildConfig.DEBUG) { - when (val error = GLES20.glGetError()) { - GLES20.GL_NO_ERROR -> { - Logger.d(TAG, "$cmd -> no error") - } - GLES20.GL_INVALID_ENUM -> Logger.e(TAG, "$cmd -> error in gl: GL_INVALID_ENUM") - GLES20.GL_INVALID_VALUE -> Logger.e(TAG, "$cmd -> error in gl: GL_INVALID_VALUE") - GLES20.GL_INVALID_OPERATION -> Logger.e( - TAG, - "$cmd -> error in gl: GL_INVALID_OPERATION" - ) - GLES20.GL_INVALID_FRAMEBUFFER_OPERATION -> Logger.e( - TAG, - "$cmd -> error in gl: GL_INVALID_FRAMEBUFFER_OPERATION" - ) - GLES20.GL_OUT_OF_MEMORY -> Logger.e(TAG, "$cmd -> error in gl: GL_OUT_OF_MEMORY") - else -> Logger.e(TAG, "$cmd -> error in gl: $error") - } - } - } - } -} \ No newline at end of file diff --git a/extension-androidauto/src/main/java/com/mapbox/maps/extension/androidauto/CompassWidget.kt b/extension-androidauto/src/main/java/com/mapbox/maps/extension/androidauto/CompassWidget.kt index 92219cbebf..aff4b972d3 100644 --- a/extension-androidauto/src/main/java/com/mapbox/maps/extension/androidauto/CompassWidget.kt +++ b/extension-androidauto/src/main/java/com/mapbox/maps/extension/androidauto/CompassWidget.kt @@ -2,12 +2,15 @@ package com.mapbox.maps.extension.androidauto import android.content.Context import android.graphics.BitmapFactory +import com.mapbox.maps.renderer.widget.BitmapWidget +import com.mapbox.maps.renderer.widget.WidgetPosition -class CompassWidget(context: Context) : BaseWidgetRenderer( +class CompassWidget(context: Context) : BitmapWidget( bitmap = BitmapFactory.decodeResource(context.resources, R.drawable.mapbox_compass_icon), - position = WidgetPosition.TOP_RIGHT, - marginBottom = 10f, - marginLeft = 10f, - marginRight = 20f, - marginTop = 30f + position = WidgetPosition( + horizontal = WidgetPosition.Horizontal.LEFT, + vertical = WidgetPosition.Vertical.CENTER, + ), + marginX = 20f, + marginY = 20f ) \ No newline at end of file diff --git a/extension-androidauto/src/main/java/com/mapbox/maps/extension/androidauto/LogoWidget.kt b/extension-androidauto/src/main/java/com/mapbox/maps/extension/androidauto/LogoWidget.kt index 9d70c94c72..2bf042a29c 100644 --- a/extension-androidauto/src/main/java/com/mapbox/maps/extension/androidauto/LogoWidget.kt +++ b/extension-androidauto/src/main/java/com/mapbox/maps/extension/androidauto/LogoWidget.kt @@ -2,12 +2,15 @@ package com.mapbox.maps.extension.androidauto import android.content.Context import android.graphics.BitmapFactory +import com.mapbox.maps.renderer.widget.BitmapWidget +import com.mapbox.maps.renderer.widget.WidgetPosition -class LogoWidget(context: Context) : BaseWidgetRenderer( +class LogoWidget(context: Context) : BitmapWidget( bitmap = BitmapFactory.decodeResource(context.resources, R.drawable.mapbox_logo_icon), - position = WidgetPosition.BOTTOM_LEFT, - marginBottom = 20f, - marginLeft = 20f, - marginRight = 20f, - marginTop = 20f + position = WidgetPosition( + horizontal = WidgetPosition.Horizontal.LEFT, + vertical = WidgetPosition.Vertical.BOTTOM, + ), + marginX = 20f, + marginY = 20f, ) \ No newline at end of file diff --git a/extension-androidauto/src/main/java/com/mapbox/maps/extension/androidauto/MapboxCarUtils.kt b/extension-androidauto/src/main/java/com/mapbox/maps/extension/androidauto/MapboxCarUtils.kt index cd58c491b2..f19e9c3817 100644 --- a/extension-androidauto/src/main/java/com/mapbox/maps/extension/androidauto/MapboxCarUtils.kt +++ b/extension-androidauto/src/main/java/com/mapbox/maps/extension/androidauto/MapboxCarUtils.kt @@ -121,7 +121,6 @@ fun Session.initMapSurface( lifecycle.addObserver( object : DefaultLifecycleObserver { override fun onCreate(owner: LifecycleOwner) { - Logger.i(TAG, "SurfaceRenderer created") synchronized(this) { carContext.getCarService(AppManager::class.java) .setSurfaceCallback(surfaceCallback) @@ -129,21 +128,18 @@ fun Session.initMapSurface( } override fun onStart(owner: LifecycleOwner) { - Logger.i(TAG, "onStart") synchronized(this) { mapSurface?.onStart() } } override fun onStop(owner: LifecycleOwner) { - Logger.i(TAG, "onStop") synchronized(this) { mapSurface?.onStop() } } override fun onDestroy(owner: LifecycleOwner) { - Logger.i(TAG, "onDestroy") synchronized(this) { mapSurface?.onDestroy() } @@ -153,7 +149,6 @@ fun Session.initMapSurface( } private fun MapSurface.onScroll(distanceX: Float, distanceY: Float) { - Logger.i(TAG, "handleScroll $distanceX, $distanceY") synchronized(this) { val centerScreen = ScreenCoordinate(0.0, 0.0) getMapboxMap().apply { @@ -172,7 +167,6 @@ private fun MapSurface.onScroll(distanceX: Float, distanceY: Float) { } private fun MapSurface.onScale(focusX: Float, focusY: Float, scaleFactor: Float) { - Logger.i(TAG, "handleScale $focusX, $focusY. $scaleFactor") synchronized(this) { val cameraState = getMapboxMap().cameraState Logger.i(TAG, "setting zoom ${cameraState.zoom * scaleFactor}") diff --git a/extension-androidauto/src/main/java/com/mapbox/maps/extension/androidauto/WidgetPosition.kt b/extension-androidauto/src/main/java/com/mapbox/maps/extension/androidauto/WidgetPosition.kt deleted file mode 100644 index 326275f9ae..0000000000 --- a/extension-androidauto/src/main/java/com/mapbox/maps/extension/androidauto/WidgetPosition.kt +++ /dev/null @@ -1,8 +0,0 @@ -package com.mapbox.maps.extension.androidauto - -enum class WidgetPosition { - TOP_LEFT, - TOP_RIGHT, - BOTTOM_LEFT, - BOTTOM_RIGHT -} \ No newline at end of file diff --git a/sdk/src/main/java/com/mapbox/maps/MapControllable.kt b/sdk/src/main/java/com/mapbox/maps/MapControllable.kt index 4ca49d7593..7462fa7d3f 100644 --- a/sdk/src/main/java/com/mapbox/maps/MapControllable.kt +++ b/sdk/src/main/java/com/mapbox/maps/MapControllable.kt @@ -3,7 +3,7 @@ package com.mapbox.maps import android.graphics.Bitmap import android.view.MotionEvent import com.mapbox.maps.renderer.OnFpsChangedListener -import com.mapbox.maps.renderer.Widget +import com.mapbox.maps.renderer.widget.Widget /** * MapControllable interface is the gateway for public API to talk to the internal map controller. diff --git a/sdk/src/main/java/com/mapbox/maps/MapController.kt b/sdk/src/main/java/com/mapbox/maps/MapController.kt index ce13cafe1f..f6bb75dc1c 100644 --- a/sdk/src/main/java/com/mapbox/maps/MapController.kt +++ b/sdk/src/main/java/com/mapbox/maps/MapController.kt @@ -41,7 +41,7 @@ import com.mapbox.maps.plugin.scalebar.ScaleBarPluginImpl import com.mapbox.maps.plugin.viewport.ViewportPluginImpl import com.mapbox.maps.renderer.MapboxRenderer import com.mapbox.maps.renderer.OnFpsChangedListener -import com.mapbox.maps.renderer.Widget +import com.mapbox.maps.renderer.widget.Widget import java.lang.ref.WeakReference internal class MapController : MapPluginProviderDelegate, MapControllable { diff --git a/sdk/src/main/java/com/mapbox/maps/MapSurface.kt b/sdk/src/main/java/com/mapbox/maps/MapSurface.kt index 679c73c955..43ffa530f9 100644 --- a/sdk/src/main/java/com/mapbox/maps/MapSurface.kt +++ b/sdk/src/main/java/com/mapbox/maps/MapSurface.kt @@ -8,7 +8,7 @@ import com.mapbox.maps.plugin.MapPlugin import com.mapbox.maps.plugin.delegates.MapPluginProviderDelegate import com.mapbox.maps.renderer.MapboxSurfaceRenderer import com.mapbox.maps.renderer.OnFpsChangedListener -import com.mapbox.maps.renderer.Widget +import com.mapbox.maps.renderer.widget.Widget /** * A [MapSurface] provides an embeddable map interface. @@ -187,7 +187,6 @@ class MapSurface @JvmOverloads constructor( mapController.addWidget(widget) } - /** * Get the plugin instance. * diff --git a/sdk/src/main/java/com/mapbox/maps/MapView.kt b/sdk/src/main/java/com/mapbox/maps/MapView.kt index 22e71281c8..d0157ea2a6 100644 --- a/sdk/src/main/java/com/mapbox/maps/MapView.kt +++ b/sdk/src/main/java/com/mapbox/maps/MapView.kt @@ -19,7 +19,7 @@ import com.mapbox.maps.plugin.delegates.MapPluginProviderDelegate import com.mapbox.maps.renderer.MapboxSurfaceHolderRenderer import com.mapbox.maps.renderer.MapboxTextureViewRenderer import com.mapbox.maps.renderer.OnFpsChangedListener -import com.mapbox.maps.renderer.Widget +import com.mapbox.maps.renderer.widget.Widget import com.mapbox.maps.renderer.egl.EGLCore import com.mapbox.maps.viewannotation.ViewAnnotationManager diff --git a/sdk/src/main/java/com/mapbox/maps/renderer/MapboxRenderThread.kt b/sdk/src/main/java/com/mapbox/maps/renderer/MapboxRenderThread.kt index 202aad0b8e..6816af907f 100644 --- a/sdk/src/main/java/com/mapbox/maps/renderer/MapboxRenderThread.kt +++ b/sdk/src/main/java/com/mapbox/maps/renderer/MapboxRenderThread.kt @@ -1,5 +1,6 @@ package com.mapbox.maps.renderer +import android.opengl.GLES20 import android.os.SystemClock import android.view.Choreographer import android.view.Surface @@ -9,9 +10,10 @@ import androidx.annotation.VisibleForTesting import androidx.annotation.WorkerThread import com.mapbox.common.Logger import com.mapbox.maps.renderer.egl.EGLCore +import com.mapbox.maps.renderer.gl.TextureRenderer +import com.mapbox.maps.renderer.widget.Widget import java.util.LinkedList import java.util.concurrent.ConcurrentLinkedQueue -import java.util.concurrent.CopyOnWriteArrayList import java.util.concurrent.locks.ReentrantLock import javax.microedition.khronos.egl.EGL10 import javax.microedition.khronos.egl.EGL11 @@ -40,14 +42,15 @@ internal class MapboxRenderThread : Choreographer.FrameCallback { @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE) internal val nonRenderEventQueue = ConcurrentLinkedQueue() - private val widgetList = CopyOnWriteArrayList() - private var surface: Surface? = null @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE) internal var eglSurface: EGLSurface private var width: Int = 0 private var height: Int = 0 + private val widgetRenderer: MapboxWidgetRenderer + private val widgetTextureRenderer: TextureRenderer + @Volatile @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE) internal var renderTimeNs = 0L @@ -65,6 +68,7 @@ internal class MapboxRenderThread : Choreographer.FrameCallback { * We track moment when native renderer is prepared. */ private var renderCreated = false + private var widgetRenderCreated = false /** * We track moment when EGL context is created and associated with current Android surface. @@ -87,26 +91,33 @@ internal class MapboxRenderThread : Choreographer.FrameCallback { constructor( mapboxRenderer: MapboxRenderer, + mapboxWidgetRenderer: MapboxWidgetRenderer, translucentSurface: Boolean, antialiasingSampleCount: Int, ) { this.translucentSurface = translucentSurface this.mapboxRenderer = mapboxRenderer + this.widgetRenderer = mapboxWidgetRenderer this.eglCore = EGLCore(translucentSurface, antialiasingSampleCount) this.eglSurface = eglCore.eglNoSurface + this.widgetTextureRenderer = TextureRenderer() renderHandlerThread = RenderHandlerThread().apply { start() } } @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE) constructor( mapboxRenderer: MapboxRenderer, + mapboxWidgetRenderer: MapboxWidgetRenderer, handlerThread: RenderHandlerThread, - eglCore: EGLCore + eglCore: EGLCore, + widgetTextureRenderer: TextureRenderer, ) { this.translucentSurface = false this.mapboxRenderer = mapboxRenderer + this.widgetRenderer = mapboxWidgetRenderer this.renderHandlerThread = handlerThread this.eglCore = eglCore + this.widgetTextureRenderer = widgetTextureRenderer this.eglSurface = eglCore.eglNoSurface } @@ -144,6 +155,10 @@ internal class MapboxRenderThread : Choreographer.FrameCallback { ) renderCreated = true } + if (!widgetRenderCreated) { + widgetRenderer.onSharedContext(eglCore.eglContext) + widgetRenderCreated = true + } return true } } @@ -204,11 +219,8 @@ internal class MapboxRenderThread : Choreographer.FrameCallback { private fun checkSurfaceSizeChanged() { if (sizeChanged) { - mapboxRenderer.onSurfaceChanged( - width = width, - height = height - ) - widgetList.forEach { it.onSizeChanged(width, height) } + mapboxRenderer.onSurfaceChanged(width = width, height = height) + widgetRenderer.onSurfaceChanged(width = width, height = height) sizeChanged = false } } @@ -223,15 +235,23 @@ internal class MapboxRenderThread : Choreographer.FrameCallback { postPrepareRenderFrame() return } + + if (widgetRenderer.needRender) { + eglCore.makeNothingCurrent() + widgetRenderer.updateTexture() + eglCore.makeCurrent(eglSurface) + } + mapboxRenderer.render() + + if (widgetRenderer.getTextureId() != 0) { + widgetTextureRenderer.render(widgetRenderer.getTextureId()) + } + // assuming render event queue holds user's runnables with OpenGL ES commands // it makes sense to execute them after drawing a map but before swapping buffers // **note** this queue also holds snapshot tasks drainQueue(renderEventQueue) - // render all the widgets - widgetList.forEach { - it.render() - } when (val swapStatus = eglCore.swapBuffers(eglSurface)) { EGL10.EGL_SUCCESS -> {} EGL11.EGL_CONTEXT_LOST -> { @@ -267,9 +287,11 @@ internal class MapboxRenderThread : Choreographer.FrameCallback { } private fun releaseEglSurface() { + widgetTextureRenderer.release() eglCore.releaseSurface(eglSurface) eglContextCreated = false eglSurface = eglCore.eglNoSurface + widgetRenderer.release() } private fun releaseAll() { @@ -279,6 +301,7 @@ internal class MapboxRenderThread : Choreographer.FrameCallback { renderCreated = false releaseEgl() surface?.release() + widgetRenderer.release() } private fun prepareRenderFrame(creatingSurface: Boolean = false) { @@ -345,8 +368,7 @@ internal class MapboxRenderThread : Choreographer.FrameCallback { } fun addWidget(widget: Widget) { - widget.onSizeChanged(this.width, this.height) - widgetList.add(widget) + widgetRenderer.addWidget(widget) } @WorkerThread @@ -360,7 +382,7 @@ internal class MapboxRenderThread : Choreographer.FrameCallback { } this.width = width this.height = height - widgetList.forEach { it.onSizeChanged(width, height) } + widgetRenderer.onSurfaceChanged(width = width, height = height) renderEventQueue.removeAll { it.eventType == EventType.SDK } nonRenderEventQueue.removeAll { it.eventType == EventType.SDK } // we do not want to clear render events scheduled by user @@ -499,6 +521,7 @@ internal class MapboxRenderThread : Choreographer.FrameCallback { companion object { private const val TAG = "Mbgl-RenderThread" + private val ONE_SECOND_NS = 10.0.pow(9.0).toLong() private val ONE_MILLISECOND_NS = 10.0.pow(6.0).toLong() diff --git a/sdk/src/main/java/com/mapbox/maps/renderer/MapboxRenderer.kt b/sdk/src/main/java/com/mapbox/maps/renderer/MapboxRenderer.kt index 54699196f7..a1ad1f7d55 100644 --- a/sdk/src/main/java/com/mapbox/maps/renderer/MapboxRenderer.kt +++ b/sdk/src/main/java/com/mapbox/maps/renderer/MapboxRenderer.kt @@ -20,6 +20,7 @@ internal abstract class MapboxRenderer : MapClient { @VisibleForTesting(otherwise = VisibleForTesting.PROTECTED) internal lateinit var renderThread: MapboxRenderThread + internal abstract val widgetRenderer: MapboxWidgetRenderer @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE) internal var map: MapInterface? = null diff --git a/sdk/src/main/java/com/mapbox/maps/renderer/MapboxSurfaceRenderer.kt b/sdk/src/main/java/com/mapbox/maps/renderer/MapboxSurfaceRenderer.kt index 5d92511607..feb5662ae5 100644 --- a/sdk/src/main/java/com/mapbox/maps/renderer/MapboxSurfaceRenderer.kt +++ b/sdk/src/main/java/com/mapbox/maps/renderer/MapboxSurfaceRenderer.kt @@ -7,9 +7,16 @@ internal open class MapboxSurfaceRenderer : MapboxRenderer { private var createSurface = false + override val widgetRenderer: MapboxWidgetRenderer + constructor(antialiasingSampleCount: Int) { + widgetRenderer = MapboxWidgetRenderer( + translucentSurface = false, + antialiasingSampleCount = antialiasingSampleCount, + ) renderThread = MapboxRenderThread( mapboxRenderer = this, + mapboxWidgetRenderer = widgetRenderer, translucentSurface = false, antialiasingSampleCount = antialiasingSampleCount, ) @@ -17,6 +24,10 @@ internal open class MapboxSurfaceRenderer : MapboxRenderer { @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE) internal constructor(renderThread: MapboxRenderThread) { + widgetRenderer = MapboxWidgetRenderer( + translucentSurface = false, + antialiasingSampleCount = 1, + ) this.renderThread = renderThread } diff --git a/sdk/src/main/java/com/mapbox/maps/renderer/MapboxTextureViewRenderer.kt b/sdk/src/main/java/com/mapbox/maps/renderer/MapboxTextureViewRenderer.kt index cbf7bf8375..975318d8d8 100644 --- a/sdk/src/main/java/com/mapbox/maps/renderer/MapboxTextureViewRenderer.kt +++ b/sdk/src/main/java/com/mapbox/maps/renderer/MapboxTextureViewRenderer.kt @@ -7,9 +7,17 @@ import androidx.annotation.VisibleForTesting internal class MapboxTextureViewRenderer : MapboxRenderer, TextureView.SurfaceTextureListener { + override val widgetRenderer: MapboxWidgetRenderer + constructor(textureView: TextureView, antialiasingSampleCount: Int) { + val widgetRenderer = MapboxWidgetRenderer( + translucentSurface = false, + antialiasingSampleCount = antialiasingSampleCount, + ) + this.widgetRenderer = widgetRenderer renderThread = MapboxRenderThread( mapboxRenderer = this, + mapboxWidgetRenderer = widgetRenderer, translucentSurface = true, antialiasingSampleCount = antialiasingSampleCount, ) @@ -21,6 +29,11 @@ internal class MapboxTextureViewRenderer : MapboxRenderer, TextureView.SurfaceTe @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE) internal constructor(renderThread: MapboxRenderThread) { + val widgetRenderer = MapboxWidgetRenderer( + translucentSurface = false, + antialiasingSampleCount = 1, + ) + this.widgetRenderer = widgetRenderer this.renderThread = renderThread } diff --git a/sdk/src/main/java/com/mapbox/maps/renderer/MapboxWidgetRenderer.kt b/sdk/src/main/java/com/mapbox/maps/renderer/MapboxWidgetRenderer.kt new file mode 100644 index 0000000000..3653db6e52 --- /dev/null +++ b/sdk/src/main/java/com/mapbox/maps/renderer/MapboxWidgetRenderer.kt @@ -0,0 +1,171 @@ +package com.mapbox.maps.renderer + +import android.opengl.GLES20 +import com.mapbox.common.Logger +import com.mapbox.maps.MapboxExperimental +import com.mapbox.maps.renderer.egl.EGLCore +import com.mapbox.maps.renderer.widget.Widget +import java.util.concurrent.CopyOnWriteArraySet +import javax.microedition.khronos.egl.EGLContext +import javax.microedition.khronos.egl.EGLSurface + +@MapboxExperimental +internal class MapboxWidgetRenderer( + private val translucentSurface: Boolean, + private val antialiasingSampleCount: Int, +) { + private var widgetEglPrepared = false + private lateinit var widgetEglSurface: EGLSurface + private lateinit var widgetEglCore: EGLCore + + private val textures = intArrayOf(0) + private val framebuffers = intArrayOf(0) + + private val widgets = CopyOnWriteArraySet() + + private var width = 0 + private var height = 0 + + fun getTextureId() = textures[0] + + val needRender: Boolean + get() = widgets.any { it.renderer.needRender } + + fun onSharedContext(sharedContext: EGLContext) { + if (widgetEglPrepared) { + TODO("New shared context while previous is still alive!") + } + widgetEglCore = EGLCore( + translucentSurface = translucentSurface, + antialiasingSampleCount = antialiasingSampleCount, + sharedContext = sharedContext, + ) + widgetEglSurface = widgetEglCore.eglNoSurface + } + + fun onSurfaceChanged(width: Int, height: Int) { + // TODO createOffscreenSurface with new dimensions + this.width = width + this.height = height + widgets.forEach { it.renderer.onSurfaceChanged(width, height) } + } + + private fun textureToFramebuffer() { + if (textures[0] != 0) { + GLES20.glDeleteTextures(textures.size, textures, 0) + } + GLES20.glGenTextures(1, textures, 0) + + GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, textures[0]) + GLES20.glTexParameterf( + GLES20.GL_TEXTURE_2D, + GLES20.GL_TEXTURE_MAG_FILTER, + GLES20.GL_LINEAR.toFloat() + ) + GLES20.glTexParameterf( + GLES20.GL_TEXTURE_2D, + GLES20.GL_TEXTURE_MIN_FILTER, + GLES20.GL_LINEAR.toFloat() + ) + GLES20.glTexParameterf( + GLES20.GL_TEXTURE_2D, + GLES20.GL_TEXTURE_WRAP_S, + GLES20.GL_CLAMP_TO_EDGE.toFloat() + ) + GLES20.glTexParameterf( + GLES20.GL_TEXTURE_2D, + GLES20.GL_TEXTURE_WRAP_T, + GLES20.GL_CLAMP_TO_EDGE.toFloat() + ) + GLES20.glTexImage2D( + GLES20.GL_TEXTURE_2D, + 0, + GLES20.GL_RGBA, + width, + height, + 0, + GLES20.GL_RGBA, + GLES20.GL_UNSIGNED_BYTE, + null + ) + GLES20.glFramebufferTexture2D( + GLES20.GL_FRAMEBUFFER, GLES20.GL_COLOR_ATTACHMENT0, + GLES20.GL_TEXTURE_2D, textures[0], 0 + ) + GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, 0) + } + + fun release() { + if (widgetEglPrepared) { + widgetEglPrepared = false + + if (widgetEglSurface != widgetEglCore.eglNoSurface) { + widgetEglCore.makeCurrent(widgetEglSurface) + + GLES20.glDeleteFramebuffers(framebuffers.size, framebuffers, 0) + GLES20.glDeleteTextures(textures.size, textures, 0) + widgets.forEach { + it.renderer.release() + } + widgets.clear() + + widgetEglCore.releaseSurface(widgetEglSurface) + + widgetEglSurface = widgetEglCore.eglNoSurface + } + + widgetEglCore.release() + } + } + + fun updateTexture() { + if (needRender) { + checkEgl() + widgetEglCore.makeCurrent(widgetEglSurface) + GLES20.glBindFramebuffer(GLES20.GL_FRAMEBUFFER, framebuffers[0]) + textureToFramebuffer() + widgets.forEach { + it.renderer.render() + } + GLES20.glBindFramebuffer(GLES20.GL_FRAMEBUFFER, 0) + widgetEglCore.makeNothingCurrent() + } + } + + private fun checkEgl() { + if (widgetEglPrepared && widgetEglSurface != widgetEglCore.eglNoSurface) { + return + } + + if (!widgetEglPrepared) { + widgetEglPrepared = widgetEglCore.prepareEgl() + if (!widgetEglPrepared) { + Logger.e(TAG, "Widget EGL was not configured, please check logs above.") + return + } + } + + if (widgetEglSurface == widgetEglCore.eglNoSurface) { + widgetEglSurface = widgetEglCore.createOffscreenSurface(width = width, height = height) + if (widgetEglSurface == widgetEglCore.eglNoSurface) { + return + } + } + + widgetEglCore.makeCurrent(widgetEglSurface) + + GLES20.glGenFramebuffers(1, framebuffers, 0) + GLES20.glBindFramebuffer(GLES20.GL_FRAMEBUFFER, framebuffers[0]) + } + + fun addWidget(widget: Widget) { + widget.renderer.onSurfaceChanged(width, height) + widgets.add(widget) + } + + fun removeWidget(widget: Widget) = widgets.remove(widget) + + companion object { + private const val TAG: String = "MapboxWidgetRenderer" + } +} diff --git a/sdk/src/main/java/com/mapbox/maps/renderer/SurfaceUtils.kt b/sdk/src/main/java/com/mapbox/maps/renderer/SurfaceUtils.kt deleted file mode 100644 index 9aeccbe3d0..0000000000 --- a/sdk/src/main/java/com/mapbox/maps/renderer/SurfaceUtils.kt +++ /dev/null @@ -1,26 +0,0 @@ -package com.mapbox.maps.renderer - -import android.graphics.Bitmap -import android.graphics.Rect -import android.util.Log -import android.view.Surface -import java.lang.Exception - -fun Surface.drawWidget(bitmap: Bitmap) { - if (!isValid) { - // Surface is not available, or has been destroyed, skip this frame. - Log.e("testtest", "Surface is not available, or has been destroyed, skip this frame.") - return - } - val height = bitmap.height - val width = bitmap.width - try { - this.lockCanvas(Rect(0, 0, width, height))?.let { canvas -> - Log.e("testtest", "lockCanvas done") - canvas.drawBitmap(bitmap, 0f, 0f, null) - this.unlockCanvasAndPost(canvas) - } - } catch (e:Exception) { - Log.e("testtest", e.toString()) - } -} \ No newline at end of file diff --git a/sdk/src/main/java/com/mapbox/maps/renderer/Widget.kt b/sdk/src/main/java/com/mapbox/maps/renderer/Widget.kt deleted file mode 100644 index 55aca59af3..0000000000 --- a/sdk/src/main/java/com/mapbox/maps/renderer/Widget.kt +++ /dev/null @@ -1,35 +0,0 @@ -package com.mapbox.maps.renderer - -import android.opengl.GLES20 -import android.util.Log -import androidx.annotation.CallSuper - -abstract class Widget { - - private var isInit = false - var width = 0 - var height = 0 - - @CallSuper - open fun onSizeChanged(width: Int, height: Int) { - Log.e("testtest", "onSize changed: $width, $height") - this.width = width - this.height = height - } - - abstract fun initialize() - - // TODO make params customizable - @CallSuper - open fun render() { - if (!isInit) { - isInit = true - initialize() - } - GLES20.glUseProgram(0) - GLES20.glBindBuffer(GLES20.GL_ARRAY_BUFFER, 0) - GLES20.glBindBuffer(GLES20.GL_ELEMENT_ARRAY_BUFFER, 0) - } - - abstract fun deinitialize() -} \ No newline at end of file diff --git a/sdk/src/main/java/com/mapbox/maps/renderer/egl/EGLCore.kt b/sdk/src/main/java/com/mapbox/maps/renderer/egl/EGLCore.kt index 50569d4232..d1601d7667 100644 --- a/sdk/src/main/java/com/mapbox/maps/renderer/egl/EGLCore.kt +++ b/sdk/src/main/java/com/mapbox/maps/renderer/egl/EGLCore.kt @@ -14,11 +14,12 @@ import javax.microedition.khronos.egl.* internal class EGLCore( private val translucentSurface: Boolean, private val antialiasingSampleCount: Int, + private val sharedContext: EGLContext = EGL10.EGL_NO_CONTEXT, ) { private lateinit var egl: EGL10 private lateinit var eglConfig: EGLConfig private var eglDisplay: EGLDisplay = EGL10.EGL_NO_DISPLAY - private var eglContext: EGLContext = EGL10.EGL_NO_CONTEXT + internal var eglContext: EGLContext = EGL10.EGL_NO_CONTEXT internal val eglNoSurface: EGLSurface = EGL10.EGL_NO_SURFACE @@ -45,7 +46,7 @@ internal class EGLCore( val context = egl.eglCreateContext( eglDisplay, eglConfig, - EGL10.EGL_NO_CONTEXT, + sharedContext, intArrayOf(EGL_CONTEXT_CLIENT_VERSION, 2, EGL10.EGL_NONE) ) val contextCreated = checkEglErrorNoException("eglCreateContext") @@ -113,6 +114,25 @@ internal class EGLCore( return eglSurface } + /** + * Creates an EGL surface associated with an offscreen buffer. + */ + fun createOffscreenSurface(width: Int, height: Int): EGLSurface { + val surfaceAttribs = + intArrayOf(EGL10.EGL_WIDTH, width, EGL10.EGL_HEIGHT, height, EGL10.EGL_NONE) + val eglSurface = egl.eglCreatePbufferSurface( + eglDisplay, + eglConfig, + surfaceAttribs + ) + checkEglErrorNoException("eglCreatePbufferSurface") + if (eglSurface == null) { + Logger.e(TAG, "Offscreen surface was null") + return eglNoSurface + } + return eglSurface + } + /** * Makes no context current. */ @@ -145,7 +165,7 @@ internal class EGLCore( } /** - * Calls eglSwapBuffers. Use this to "publish" the current frame. + * Calls eglSwapBuffers. Use this to "publish" the current frame. */ fun swapBuffers(eglSurface: EGLSurface): Int { val swapStatus = egl.eglSwapBuffers(eglDisplay, eglSurface) diff --git a/sdk/src/main/java/com/mapbox/maps/renderer/gl/GlUtils.kt b/sdk/src/main/java/com/mapbox/maps/renderer/gl/GlUtils.kt new file mode 100644 index 0000000000..f7434b48b8 --- /dev/null +++ b/sdk/src/main/java/com/mapbox/maps/renderer/gl/GlUtils.kt @@ -0,0 +1,56 @@ +package com.mapbox.maps.renderer.gl + +import android.opengl.GLES20 +import android.opengl.Matrix +import com.mapbox.maps.BuildConfig +import java.nio.ByteBuffer +import java.nio.ByteOrder +import java.nio.FloatBuffer + +internal object GlUtils { + + fun FloatArray.toFloatBuffer(): FloatBuffer = ByteBuffer.allocateDirect(size * 4) + .order(ByteOrder.nativeOrder()) + .asFloatBuffer().also { + it.put(this@toFloatBuffer) + it.rewind() + } + + /** + * @param type GLES20.GL_VERTEX_SHADER or GLES20.GL_FRAGMENT_SHADER + */ + fun loadShader(type: Int, shaderCode: String) = + GLES20.glCreateShader(type).also { shader -> + GLES20.glShaderSource(shader, shaderCode) + GLES20.glCompileShader(shader) + } + + fun checkError(cmd: String? = null) { + if (BuildConfig.DEBUG) { + when (val error = GLES20.glGetError()) { + GLES20.GL_NO_ERROR -> {} + else -> throw java.lang.RuntimeException("$cmd - error in GL : ${when (error) { + GLES20.GL_INVALID_ENUM -> "GL_INVALID_ENUM" + GLES20.GL_INVALID_VALUE -> "GL_INVALID_VALUE" + GLES20.GL_INVALID_OPERATION -> "GL_INVALID_OPERATION" + GLES20.GL_INVALID_FRAMEBUFFER_OPERATION -> "GL_INVALID_FRAMEBUFFER_OPERATION" + GLES20.GL_OUT_OF_MEMORY -> "GL_OUT_OF_MEMORY" + else -> error + }}") + } + } + } + + fun checkCompileStatus(shader: Int) { + if (BuildConfig.DEBUG) { + val isCompiled = IntArray(1) + GLES20.glGetShaderiv(shader, GLES20.GL_COMPILE_STATUS, isCompiled, 0) + if (isCompiled[0] == GLES20.GL_FALSE) { + val infoLog = GLES20.glGetShaderInfoLog(shader) + throw RuntimeException("checkCompileStatus error: $infoLog") + } + } + } + + fun getIdentityMatrix(): FloatArray = FloatArray(16).also { Matrix.setIdentityM(it, 0) } +} \ No newline at end of file diff --git a/sdk/src/main/java/com/mapbox/maps/renderer/gl/TextureRenderer.kt b/sdk/src/main/java/com/mapbox/maps/renderer/gl/TextureRenderer.kt new file mode 100644 index 0000000000..a4b33a3846 --- /dev/null +++ b/sdk/src/main/java/com/mapbox/maps/renderer/gl/TextureRenderer.kt @@ -0,0 +1,157 @@ +package com.mapbox.maps.renderer.gl + +import android.opengl.GLES20 +import com.mapbox.maps.renderer.gl.GlUtils.toFloatBuffer + +internal class TextureRenderer( + private val depth: Float = 0f +) { + + private var program: Int = 0 + private var attributePosition: Int = 0 + private var attributeTextureCoord: Int = 0 + private var uniformTexture: Int = 0 + private var vbo: IntArray = IntArray(2) + + fun prepare() { + setupVbo( + vertexArray = floatArrayOf( + -1f, -1f, depth, + 1f, -1f, depth, + -1f, 1f, depth, + 1f, 1f, depth, + ), + textureArray = floatArrayOf( + 0f, 0f, + 1f, 0f, + 0f, 1f, + 1f, 1f, + ) + ) + + val vertexShader = GlUtils.loadShader( + GLES20.GL_VERTEX_SHADER, + VERTEX_SHADER_CODE + ).also(GlUtils::checkCompileStatus) + + val fragmentShader = GlUtils.loadShader( + GLES20.GL_FRAGMENT_SHADER, + FRAGMENT_SHADER_CODE + ).also(GlUtils::checkCompileStatus) + + program = GLES20.glCreateProgram().also { + GlUtils.checkError("glCreateProgram") + GLES20.glAttachShader(it, vertexShader) + GlUtils.checkError("glAttachShader") + + GLES20.glAttachShader(it, fragmentShader) + GlUtils.checkError("glAttachShader") + + GLES20.glLinkProgram(it) + GlUtils.checkError("glLinkProgram") + } + attributePosition = GLES20.glGetAttribLocation(program, "aPosition") + attributeTextureCoord = GLES20.glGetAttribLocation(program, "aTexCoord") + + uniformTexture = GLES20.glGetUniformLocation(program, "uTexture") + } + + fun render(textureID: Int) { + if (program == 0) { + prepare() + } + // Reset to guarantee widgets are drawn on top of map + GLES20.glDisable(GLES20.GL_STENCIL_TEST) + + GLES20.glUseProgram(program) + GLES20.glBindBuffer(GLES20.GL_ARRAY_BUFFER, vbo[0]) + GLES20.glVertexAttribPointer( + attributePosition, + COORDS_PER_VERTEX, + GLES20.GL_FLOAT, + false, + VERTEX_STRIDE, + 0 + ) + GLES20.glEnableVertexAttribArray(attributePosition) + GLES20.glBindBuffer(GLES20.GL_ARRAY_BUFFER, vbo[1]) + GLES20.glVertexAttribPointer( + attributeTextureCoord, + COORDS_PER_TEX, + GLES20.GL_FLOAT, + false, + TEX_STRIDE, + 0 + ) + GLES20.glEnableVertexAttribArray(attributeTextureCoord) + + GLES20.glActiveTexture(GLES20.GL_TEXTURE0) + GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, textureID) + GLES20.glUniform1i(uniformTexture, 0) + + GLES20.glDrawArrays(GLES20.GL_TRIANGLE_STRIP, 0, VERTEX_COUNT) + + GLES20.glDisableVertexAttribArray(attributePosition) + GLES20.glDisableVertexAttribArray(attributeTextureCoord) + GLES20.glBindBuffer(GLES20.GL_ARRAY_BUFFER, 0) + GLES20.glUseProgram(0) + } + + fun release() { + if (program != 0) { + GLES20.glDeleteBuffers(vbo.size, vbo, 0) + GLES20.glDeleteProgram(program) + program = 0 + } + } + + private fun setupVbo(vertexArray: FloatArray, textureArray: FloatArray) { + GLES20.glGenBuffers(2, vbo, 0) + GLES20.glBindBuffer(GLES20.GL_ARRAY_BUFFER, vbo[0]) + GLES20.glBufferData( + GLES20.GL_ARRAY_BUFFER, + vertexArray.size * 4, + vertexArray.toFloatBuffer(), + GLES20.GL_STATIC_DRAW + ) + GLES20.glBindBuffer(GLES20.GL_ARRAY_BUFFER, vbo[1]) + GLES20.glBufferData( + GLES20.GL_ARRAY_BUFFER, + textureArray.size * 4, + textureArray.toFloatBuffer(), + GLES20.GL_STATIC_DRAW + ) + GLES20.glBindBuffer(GLES20.GL_ARRAY_BUFFER, 0) + } + + companion object { + private const val COORDS_PER_VERTEX = 3 + private const val COORDS_PER_TEX = 2 + private const val BYTES_PER_FLOAT = 4 + private const val VERTEX_STRIDE = COORDS_PER_VERTEX * BYTES_PER_FLOAT + private const val TEX_STRIDE = COORDS_PER_TEX * BYTES_PER_FLOAT + private const val VERTEX_COUNT = 4 + + private val VERTEX_SHADER_CODE = """ + uniform mat4 uTexMatrix; + attribute vec4 aPosition; + attribute vec2 aTexCoord; + varying vec2 vTexCoord; + void main() + { + gl_Position = aPosition; + vTexCoord = aTexCoord; + } + """.trimIndent() + + private val FRAGMENT_SHADER_CODE = """ + precision mediump float; + varying vec2 vTexCoord; + uniform sampler2D uTexture; + void main() + { + gl_FragColor = texture2D(uTexture, vTexCoord); + } + """.trimIndent() + } +} diff --git a/sdk/src/main/java/com/mapbox/maps/renderer/widget/BitmapWidget.kt b/sdk/src/main/java/com/mapbox/maps/renderer/widget/BitmapWidget.kt new file mode 100644 index 0000000000..cfef775db5 --- /dev/null +++ b/sdk/src/main/java/com/mapbox/maps/renderer/widget/BitmapWidget.kt @@ -0,0 +1,34 @@ +package com.mapbox.maps.renderer.widget + +import android.graphics.Bitmap +import com.mapbox.maps.MapboxExperimental + +@MapboxExperimental +open class BitmapWidget( + bitmap: Bitmap, + position: WidgetPosition = WidgetPosition( + vertical = WidgetPosition.Vertical.TOP, + horizontal = WidgetPosition.Horizontal.LEFT, + ), + marginX: Float = 0f, + marginY: Float = 0f, +) : Widget() { + override val renderer = BitmapWidgetRendererImpl( + bitmap = bitmap, + position = position, + marginX = marginX, + marginY = marginY, + ) + + override fun updateBitmap(bitmap: Bitmap) { + renderer.updateBitmap(bitmap) + } + + override fun translate(translateX: Float, translateY: Float) { + renderer.translate(translateX = translateX, translateY = translateY) + } + + override fun rotate(angleDegrees: Float) { + renderer.rotate(angleDegrees = angleDegrees) + } +} diff --git a/sdk/src/main/java/com/mapbox/maps/renderer/widget/BitmapWidgetRenderer.kt b/sdk/src/main/java/com/mapbox/maps/renderer/widget/BitmapWidgetRenderer.kt new file mode 100644 index 0000000000..9e22d4e62e --- /dev/null +++ b/sdk/src/main/java/com/mapbox/maps/renderer/widget/BitmapWidgetRenderer.kt @@ -0,0 +1,323 @@ +package com.mapbox.maps.renderer.widget + +import android.graphics.Bitmap +import android.opengl.GLES20 +import android.opengl.GLUtils +import android.opengl.Matrix +import com.mapbox.maps.renderer.gl.GlUtils +import com.mapbox.maps.renderer.gl.GlUtils.toFloatBuffer +import java.nio.FloatBuffer + +internal class BitmapWidgetRendererImpl( + private var bitmap: Bitmap, + private val position: WidgetPosition, + private val marginX: Float, + private val marginY: Float, +) : WidgetRenderer { + + private var bitmapWidth = bitmap.width + private var bitmapHeight = bitmap.height + + private var surfaceWidth = 0 + private var surfaceHeight = 0 + + private var program = 0 + private var vertexShader = 0 + private var fragmentShader = 0 + + private var attributeVertexPosition = 0 + private var attributeTexturePosition = 0 + private var uniformTexture = 0 + private var uniformMvpMatrix = 0 + private val textures = intArrayOf(0) + + private var screenMatrix = FloatArray(16) + private var translateRotate = FloatArray(16) + private val rotationMatrix = GlUtils.getIdentityMatrix() + private val translateMatrix = GlUtils.getIdentityMatrix() + private val mvpMatrix = GlUtils.getIdentityMatrix() + private val mvpMatrixBuffer = mvpMatrix.toFloatBuffer() + + private var updateMatrix: Boolean = true + + private val vertexPositionBuffer = FloatArray(8).toFloatBuffer() + private val texturePositionBuffer = floatArrayOf( + 0f, 0f, + 0f, 1f, + 1f, 0f, + 1f, 1f + ).toFloatBuffer() + + override var needRender: Boolean = true + + private fun FloatArray.put(vararg values: Float) { + values.forEachIndexed { index, value -> + this[index] = value + } + } + + private fun FloatBuffer.put(vararg values: Float) { + rewind() + values.forEach { value -> + this.put(value) + } + rewind() + } + + override fun onSurfaceChanged(width: Int, height: Int) { + surfaceWidth = width + surfaceHeight = height + + // transforms from (0,0) - (width, height) in screen pixels + // to (-1, -1) - (1, 1) for GL + screenMatrix.put( + 2f / width, 0f, 0f, 0f, + 0f, -2f / height, 0f, 0f, + 0f, 0f, 0f, 0f, + -1f, 1f, 0f, 1f + ) + + Matrix.translateM( + translateMatrix, + 0, + leftX(), + topY(), + 0f + ) + + updateVertexBuffer() + + updateMatrix = true + needRender = true + } + + private fun updateVertexBuffer() { + // in pixels, (-bitmapWidth / 2, -bitmapHeight/2) - (bitmapWidth / 2, bitmapHeight/2) + vertexPositionBuffer.put( + -bitmapWidth / 2f, -bitmapHeight / 2f, + -bitmapWidth / 2f, bitmapHeight / 2f, + bitmapWidth / 2f, -bitmapHeight / 2f, + bitmapWidth / 2f, bitmapHeight / 2f, + ) + } + + private fun topY() = when (position.vertical) { + WidgetPosition.Vertical.BOTTOM -> surfaceHeight.toFloat() - bitmapHeight.toFloat() / 2f - marginY + WidgetPosition.Vertical.CENTER -> surfaceHeight.toFloat() / 2 - bitmapHeight.toFloat() / 2f + marginY + WidgetPosition.Vertical.TOP -> marginY + bitmapHeight.toFloat() / 2f + } + + private fun leftX() = when (position.horizontal) { + WidgetPosition.Horizontal.LEFT -> marginX + bitmapWidth.toFloat() / 2f + WidgetPosition.Horizontal.CENTER -> surfaceWidth.toFloat() / 2 - bitmapWidth.toFloat() / 2f + marginX + WidgetPosition.Horizontal.RIGHT -> surfaceWidth.toFloat() - bitmapWidth.toFloat() / 2f - marginX + } + + override fun prepare() { + val maxAttrib = IntArray(1) + GLES20.glGetIntegerv(GLES20.GL_MAX_VERTEX_ATTRIBS, maxAttrib, 0) + + vertexShader = GlUtils.loadShader( + GLES20.GL_VERTEX_SHADER, + VERTEX_SHADER_CODE + ).also(GlUtils::checkCompileStatus) + + fragmentShader = GlUtils.loadShader( + GLES20.GL_FRAGMENT_SHADER, + FRAGMENT_SHADER_CODE + ).also(GlUtils::checkCompileStatus) + + program = GLES20.glCreateProgram().also { program -> + GlUtils.checkError("glCreateProgram") + + GLES20.glAttachShader(program, vertexShader) + GlUtils.checkError("glAttachShader") + + GLES20.glAttachShader(program, fragmentShader) + GlUtils.checkError("glAttachShader") + + GLES20.glLinkProgram(program) + GlUtils.checkError("glLinkProgram") + } + + uniformMvpMatrix = + GLES20.glGetUniformLocation(program, "uMvpMatrix") + GlUtils.checkError("glGetUniformLocation") + + attributeVertexPosition = + GLES20.glGetAttribLocation(program, "aPosition") + GlUtils.checkError("glGetAttribLocation") + + attributeTexturePosition = + GLES20.glGetAttribLocation(program, "aCoordinate") + GlUtils.checkError("glGetAttribLocation") + + uniformTexture = + GLES20.glGetUniformLocation(program, "uTexture") + GlUtils.checkError("glGetUniformLocation") + + needRender = true + } + + override fun render() { + if (program == 0) { + prepare() + } + GLES20.glUseProgram(program) + GlUtils.checkError("glUseProgram") + + if (updateMatrix) { + Matrix.setIdentityM(mvpMatrix, 0) + + Matrix.multiplyMM(translateRotate, 0, translateMatrix, 0, rotationMatrix, 0) + Matrix.multiplyMM(mvpMatrix, 0, screenMatrix, 0, translateRotate, 0) + + mvpMatrixBuffer.rewind() + mvpMatrixBuffer.put(mvpMatrix) + mvpMatrixBuffer.rewind() + + updateMatrix = false + } + + GLES20.glUniformMatrix4fv(uniformMvpMatrix, 1, false, mvpMatrixBuffer) + + textureFromBitmap() + + GLES20.glActiveTexture(GLES20.GL_TEXTURE0) + GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, textures[0]) + + GLES20.glUniform1i(uniformTexture, 0) + + GLES20.glClearColor(1f, 1f, 1f, 1f) + + GLES20.glEnableVertexAttribArray(attributeVertexPosition) + GlUtils.checkError("glEnableVertexAttribArray") + + GLES20.glVertexAttribPointer( + attributeVertexPosition, COORDS_PER_VERTEX, + GLES20.GL_FLOAT, false, + VERTEX_STRIDE, vertexPositionBuffer + ) + GlUtils.checkError("glVertexAttribPointer") + + GLES20.glEnableVertexAttribArray(attributeTexturePosition) + GlUtils.checkError("glEnableVertexAttribArray") + + GLES20.glVertexAttribPointer( + attributeTexturePosition, COORDS_PER_VERTEX, + GLES20.GL_FLOAT, false, + VERTEX_STRIDE, texturePositionBuffer + ) + GlUtils.checkError("glVertexAttribPointer") + + GLES20.glDrawArrays(GLES20.GL_TRIANGLE_STRIP, 0, VERTEX_COUNT) + GlUtils.checkError("glDrawArrays") + + GLES20.glDisableVertexAttribArray(attributeVertexPosition) + GLES20.glDisableVertexAttribArray(attributeTexturePosition) + GLES20.glBindBuffer(GLES20.GL_ARRAY_BUFFER, 0) + GLES20.glUseProgram(0) + + needRender = false + } + + override fun release() { + if (program != 0) { + GLES20.glDisableVertexAttribArray(attributeVertexPosition) + GLES20.glDetachShader(program, vertexShader) + GLES20.glDetachShader(program, fragmentShader) + GLES20.glDeleteShader(vertexShader) + GLES20.glDeleteShader(fragmentShader) + GLES20.glDeleteTextures(textures.size, textures, 0) + GLES20.glDeleteProgram(program) + program = 0 + } + needRender = false + } + + private fun textureFromBitmap() { + if (!bitmap.isRecycled) { + GLES20.glGenTextures(1, textures, 0) + GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, textures[0]) + GLES20.glTexParameterf( + GLES20.GL_TEXTURE_2D, + GLES20.GL_TEXTURE_MIN_FILTER, + GLES20.GL_NEAREST.toFloat() + ) + GLES20.glTexParameterf( + GLES20.GL_TEXTURE_2D, + GLES20.GL_TEXTURE_MAG_FILTER, + GLES20.GL_LINEAR.toFloat() + ) + GLES20.glTexParameterf( + GLES20.GL_TEXTURE_2D, + GLES20.GL_TEXTURE_WRAP_S, + GLES20.GL_CLAMP_TO_EDGE.toFloat() + ) + GLES20.glTexParameterf( + GLES20.GL_TEXTURE_2D, + GLES20.GL_TEXTURE_WRAP_T, + GLES20.GL_CLAMP_TO_EDGE.toFloat() + ) + GLUtils.texImage2D(GLES20.GL_TEXTURE_2D, 0, bitmap, 0) + bitmap.recycle() + } + } + + fun updateBitmap(bitmap: Bitmap) { + this.bitmap = bitmap + this.bitmapWidth = bitmap.width + this.bitmapHeight = bitmap.height + updateVertexBuffer() + updateMatrix = true + needRender = true + } + + fun rotate(angleDegrees: Float) { + Matrix.setIdentityM(rotationMatrix, 0) + Matrix.setRotateM(rotationMatrix, 0, angleDegrees, 0f, 0f, 1f) + updateMatrix = true + needRender = true + } + + fun translate(translateX: Float, translateY: Float) { + Matrix.setIdentityM(translateMatrix, 0) + Matrix.translateM( + translateMatrix, + 0, + leftX() + translateX, + topY() + translateY, + 0f + ) + + updateMatrix = true + needRender = true + } + + companion object { + private const val COORDS_PER_VERTEX = 2 + private const val BYTES_PER_FLOAT = 4 + private const val VERTEX_STRIDE = COORDS_PER_VERTEX * BYTES_PER_FLOAT + private const val VERTEX_COUNT = 4 + + private val VERTEX_SHADER_CODE = """ + uniform mat4 uMvpMatrix; + attribute vec2 aPosition; + attribute vec2 aCoordinate; + varying vec2 vCoordinate; + void main() { + vCoordinate = aCoordinate; + gl_Position = uMvpMatrix * vec4(aPosition, 0.0, 1.0); + } + """.trimIndent() + + private val FRAGMENT_SHADER_CODE = """ + precision mediump float; + uniform sampler2D uTexture; + varying vec2 vCoordinate; + void main() { + gl_FragColor = texture2D(uTexture, vCoordinate); + } + """.trimIndent() + } +} \ No newline at end of file diff --git a/sdk/src/main/java/com/mapbox/maps/renderer/widget/Widget.kt b/sdk/src/main/java/com/mapbox/maps/renderer/widget/Widget.kt new file mode 100644 index 0000000000..a14b99b770 --- /dev/null +++ b/sdk/src/main/java/com/mapbox/maps/renderer/widget/Widget.kt @@ -0,0 +1,13 @@ +package com.mapbox.maps.renderer.widget + +import android.graphics.Bitmap +import com.mapbox.maps.MapboxExperimental + +@MapboxExperimental +abstract class Widget { + internal abstract val renderer : WidgetRenderer + + abstract fun updateBitmap(bitmap: Bitmap) + abstract fun translate(translateX: Float, translateY: Float) + abstract fun rotate(angleDegrees: Float) +} \ No newline at end of file diff --git a/sdk/src/main/java/com/mapbox/maps/renderer/widget/WidgetPosition.kt b/sdk/src/main/java/com/mapbox/maps/renderer/widget/WidgetPosition.kt new file mode 100644 index 0000000000..6f1f3c720f --- /dev/null +++ b/sdk/src/main/java/com/mapbox/maps/renderer/widget/WidgetPosition.kt @@ -0,0 +1,21 @@ +package com.mapbox.maps.renderer.widget + +import com.mapbox.maps.MapboxExperimental + +@MapboxExperimental +class WidgetPosition( + val horizontal: Horizontal, + val vertical: Vertical, +) { + enum class Horizontal { + LEFT, + CENTER, + RIGHT, + } + + enum class Vertical { + TOP, + CENTER, + BOTTOM, + } +} diff --git a/sdk/src/main/java/com/mapbox/maps/renderer/widget/WidgetRenderer.kt b/sdk/src/main/java/com/mapbox/maps/renderer/widget/WidgetRenderer.kt new file mode 100644 index 0000000000..865163ded0 --- /dev/null +++ b/sdk/src/main/java/com/mapbox/maps/renderer/widget/WidgetRenderer.kt @@ -0,0 +1,10 @@ +package com.mapbox.maps.renderer.widget + +internal interface WidgetRenderer { + val needRender: Boolean + + fun onSurfaceChanged(width: Int, height: Int) + fun prepare() + fun render() + fun release() +} diff --git a/sdk/src/test/java/com/mapbox/TestUtils.kt b/sdk/src/test/java/com/mapbox/TestUtils.kt new file mode 100644 index 0000000000..c668cb53f2 --- /dev/null +++ b/sdk/src/test/java/com/mapbox/TestUtils.kt @@ -0,0 +1,35 @@ +package com.mapbox + +import io.mockk.MockKVerificationScope +import io.mockk.Ordering +import io.mockk.verify + +internal fun verifyNo( + ordering: Ordering = Ordering.UNORDERED, + inverse: Boolean = false, + timeout: Long = 0, + verifyBlock: MockKVerificationScope.() -> Unit +) = verify( + ordering, + inverse, + 1, + Int.MAX_VALUE, + exactly = 0, + timeout, + verifyBlock +) + +internal fun verifyOnce( + ordering: Ordering = Ordering.UNORDERED, + inverse: Boolean = false, + timeout: Long = 0, + verifyBlock: MockKVerificationScope.() -> Unit +) = verify( + ordering, + inverse, + 1, + Int.MAX_VALUE, + exactly = 1, + timeout, + verifyBlock +) diff --git a/sdk/src/test/java/com/mapbox/maps/renderer/MapboxRenderThreadTest.kt b/sdk/src/test/java/com/mapbox/maps/renderer/MapboxRenderThreadTest.kt index 7b2da78bfa..683114dae7 100644 --- a/sdk/src/test/java/com/mapbox/maps/renderer/MapboxRenderThreadTest.kt +++ b/sdk/src/test/java/com/mapbox/maps/renderer/MapboxRenderThreadTest.kt @@ -4,6 +4,9 @@ import android.view.Surface import com.mapbox.common.ShadowLogger import com.mapbox.maps.renderer.MapboxRenderThread.Companion.RETRY_DELAY_MS import com.mapbox.maps.renderer.egl.EGLCore +import com.mapbox.maps.renderer.gl.TextureRenderer +import com.mapbox.verifyNo +import com.mapbox.verifyOnce import io.mockk.* import org.junit.After import org.junit.Assert.assertEquals @@ -18,6 +21,7 @@ import java.util.concurrent.CountDownLatch import java.util.concurrent.TimeUnit import java.util.concurrent.TimeoutException import javax.microedition.khronos.egl.EGL10 +import javax.microedition.khronos.egl.EGLSurface @RunWith(RobolectricTestRunner::class) @Config(shadows = [ShadowLogger::class]) @@ -26,9 +30,11 @@ class MapboxRenderThreadTest { private lateinit var mapboxRenderThread: MapboxRenderThread private lateinit var mapboxRenderer: MapboxRenderer + private lateinit var mapboxWidgetRenderer: MapboxWidgetRenderer private lateinit var eglCore: EGLCore private lateinit var renderHandlerThread: RenderHandlerThread - private val waitTime = 200L + private lateinit var textureRenderer: TextureRenderer + private val waitTime = 300L private fun mockValidSurface(): Surface { val surface = mockk() @@ -53,11 +59,18 @@ class MapboxRenderThreadTest { mapboxRenderer = mockk(relaxUnitFun = true) eglCore = mockk(relaxUnitFun = true) every { eglCore.eglNoSurface } returns mockk() + every { eglCore.swapBuffers(any()) } returns EGL10.EGL_SUCCESS + mapboxWidgetRenderer = mockk(relaxUnitFun = true) + every { mapboxWidgetRenderer.getTextureId() } returns 0 + every { mapboxWidgetRenderer.needRender } returns false renderHandlerThread = RenderHandlerThread() + textureRenderer = mockk(relaxed = true) mapboxRenderThread = MapboxRenderThread( mapboxRenderer, + mapboxWidgetRenderer, renderHandlerThread, - eglCore + eglCore, + textureRenderer, ).apply { renderHandlerThread.start() } @@ -93,7 +106,7 @@ class MapboxRenderThreadTest { every { eglCore.createWindowSurface(any()) } returns mockk(relaxed = true) mapboxRenderThread.onSurfaceCreated(surface, 1, 1) latch.await(waitTime, TimeUnit.MILLISECONDS) - verify(exactly = 0) { + verifyNo { mapboxRenderer.createRenderer() mapboxRenderer.onSurfaceChanged(1, 1) } @@ -179,7 +192,7 @@ class MapboxRenderThreadTest { eglCore.swapBuffers(any()) } // make EGL context current only once when creating surface - verify(exactly = 1) { + verifyOnce { eglCore.makeNothingCurrent() } } @@ -475,8 +488,10 @@ class MapboxRenderThreadTest { mapboxRenderer = mockk(relaxUnitFun = true) mapboxRenderThread = MapboxRenderThread( mapboxRenderer, + mapboxWidgetRenderer, renderHandlerThread, - eglCore + eglCore, + textureRenderer, ).apply { renderHandlerThread.start() } @@ -497,8 +512,10 @@ class MapboxRenderThreadTest { mapboxRenderer = mockk(relaxUnitFun = true) mapboxRenderThread = MapboxRenderThread( mapboxRenderer, + mapboxWidgetRenderer, renderHandlerThread, - eglCore + eglCore, + textureRenderer, ).apply { renderHandlerThread.start() } @@ -528,4 +545,107 @@ class MapboxRenderThreadTest { eglCore.swapBuffers(any()) } } -} \ No newline at end of file + + @Test + fun onDrawDoesNotRenderWidgets() { + mockValidSurface() + every { mapboxWidgetRenderer.needRender } returns false + every { mapboxWidgetRenderer.getTextureId() } returns 0 + Shadows.shadowOf(renderHandlerThread.handler?.looper).pause() + mapboxRenderThread.queueRenderEvent(MapboxRenderer.renderEventSdk) + mapboxRenderThread.queueRenderEvent(MapboxRenderer.renderEventSdk) + Shadows.shadowOf(renderHandlerThread.handler?.looper).idle() + + verifyNo { + mapboxWidgetRenderer.updateTexture() + } + verifyNo { + textureRenderer.render(any()) + } + } + + @Test + fun onDrawRendersWidgets() { + mockValidSurface() + val textureId = 1 + every { mapboxWidgetRenderer.needRender } returns true + every { mapboxWidgetRenderer.getTextureId() } returns textureId + Shadows.shadowOf(renderHandlerThread.handler?.looper).pause() + mapboxRenderThread.queueRenderEvent(MapboxRenderer.renderEventSdk) + Shadows.shadowOf(renderHandlerThread.handler?.looper).idle() + mapboxRenderThread.queueRenderEvent(MapboxRenderer.renderEventSdk) + Shadows.shadowOf(renderHandlerThread.handler?.looper).idle() + + verify(exactly = 2) { + mapboxWidgetRenderer.updateTexture() + } + verify(exactly = 2) { + textureRenderer.render(textureId) + } + } + + @Test + fun onSurfaceCreatedInitsWidgetRender() { + val latch = CountDownLatch(1) + every { mapboxRenderer.createRenderer() } answers { latch.countDown() } + mockValidSurface() + if (!latch.await(waitTime, TimeUnit.MILLISECONDS)) { + throw TimeoutException() + } + verifyOnce { + mapboxWidgetRenderer.onSharedContext(eglCore.eglContext) + } + } + + @Test + fun onEglCoreFailDoesntInitWidgetRender() { + val surface = mockk() + mapboxRenderThread.onSurfaceCreated(surface, 1, 1) + val latch = CountDownLatch(1) + latch.await(waitTime, TimeUnit.MILLISECONDS) + verifyNo { + mapboxWidgetRenderer.onSharedContext(any()) + } + } + + @Test + fun onInvalidSurfaceDoesntInitWidgetRender() { + val surface = mockk() + every { surface.isValid } returns false + mapboxRenderThread.onSurfaceCreated(surface, 1, 1) + val latch = CountDownLatch(1) + latch.await(waitTime, TimeUnit.MILLISECONDS) + verifyNo { + mapboxWidgetRenderer.onSharedContext(any()) + } + } + + @Test + fun onInvalidEglWindowSurfaceDoesntInitWidgetRender() { + val latch = CountDownLatch(1) + val surface = mockk() + val noSurface = mockk() + + every { surface.isValid } returns true + every { eglCore.createWindowSurface(any()) } returns noSurface + every { eglCore.eglNoSurface } returns noSurface + + mapboxRenderThread = MapboxRenderThread( + mapboxRenderer, + mapboxWidgetRenderer, + renderHandlerThread, + eglCore, + textureRenderer, + ).apply { + renderHandlerThread.start() + } + + mapboxRenderThread.onSurfaceCreated(surface, 1, 1) + + latch.await(waitTime, TimeUnit.MILLISECONDS) + + verifyNo { + mapboxWidgetRenderer.onSharedContext(any()) + } + } +} From 33dfdb14c703134b0c0a7b0c4baa06cac1613c44 Mon Sep 17 00:00:00 2001 From: yunik Date: Tue, 8 Feb 2022 21:37:19 +0200 Subject: [PATCH 03/13] More review fixes. --- .../java/com/mapbox/maps/MapControllable.kt | 10 +- .../java/com/mapbox/maps/MapController.kt | 2 + .../main/java/com/mapbox/maps/MapSurface.kt | 10 +- sdk/src/main/java/com/mapbox/maps/MapView.kt | 10 +- .../maps/renderer/MapboxRenderThread.kt | 39 +++--- .../maps/renderer/MapboxSurfaceRenderer.kt | 2 - .../renderer/MapboxTextureViewRenderer.kt | 2 - .../maps/renderer/MapboxWidgetRenderer.kt | 116 +++++++++++------- .../com/mapbox/maps/renderer/gl/GlUtils.kt | 14 +++ .../maps/renderer/gl/TextureRenderer.kt | 22 ++-- .../maps/renderer/widget/BitmapWidget.kt | 2 +- .../renderer/widget/BitmapWidgetRenderer.kt | 16 +-- .../maps/renderer/MapboxRenderThreadTest.kt | 6 +- 13 files changed, 159 insertions(+), 92 deletions(-) diff --git a/sdk/src/main/java/com/mapbox/maps/MapControllable.kt b/sdk/src/main/java/com/mapbox/maps/MapControllable.kt index 7462fa7d3f..0c73269424 100644 --- a/sdk/src/main/java/com/mapbox/maps/MapControllable.kt +++ b/sdk/src/main/java/com/mapbox/maps/MapControllable.kt @@ -76,8 +76,16 @@ interface MapControllable : MapboxLifecycleObserver { fun setOnFpsChangedListener(listener: OnFpsChangedListener) /** - * Add static image widget to the map. + * Add [Widget] to the map. */ @MapboxExperimental fun addWidget(widget: Widget) + + /** + * Remove [Widget] from the map. + * + * @return true if widget was removed + */ + @MapboxExperimental + fun removeWidget(widget: Widget) : Boolean } \ No newline at end of file diff --git a/sdk/src/main/java/com/mapbox/maps/MapController.kt b/sdk/src/main/java/com/mapbox/maps/MapController.kt index f6bb75dc1c..9993bc2db5 100644 --- a/sdk/src/main/java/com/mapbox/maps/MapController.kt +++ b/sdk/src/main/java/com/mapbox/maps/MapController.kt @@ -210,6 +210,8 @@ internal class MapController : MapPluginProviderDelegate, MapControllable { renderer.renderThread.addWidget(widget) } + override fun removeWidget(widget: Widget) = renderer.renderThread.removeWidget(widget) + // // Telemetry // diff --git a/sdk/src/main/java/com/mapbox/maps/MapSurface.kt b/sdk/src/main/java/com/mapbox/maps/MapSurface.kt index 43ffa530f9..ef8b5cbebd 100644 --- a/sdk/src/main/java/com/mapbox/maps/MapSurface.kt +++ b/sdk/src/main/java/com/mapbox/maps/MapSurface.kt @@ -180,13 +180,21 @@ class MapSurface @JvmOverloads constructor( } /** - * Add static image widget to the map. + * Add [Widget] to the map. */ @MapboxExperimental override fun addWidget(widget: Widget) { mapController.addWidget(widget) } + /** + * Remove [Widget] from the map. + * + * @return true if widget was removed + */ + @MapboxExperimental + override fun removeWidget(widget: Widget) = mapController.removeWidget(widget) + /** * Get the plugin instance. * diff --git a/sdk/src/main/java/com/mapbox/maps/MapView.kt b/sdk/src/main/java/com/mapbox/maps/MapView.kt index d0157ea2a6..9afb00a99d 100644 --- a/sdk/src/main/java/com/mapbox/maps/MapView.kt +++ b/sdk/src/main/java/com/mapbox/maps/MapView.kt @@ -311,13 +311,21 @@ open class MapView : FrameLayout, MapPluginProviderDelegate, MapControllable { } /** - * Add static image widget to the map. + * Add [Widget] to the map. */ @MapboxExperimental override fun addWidget(widget: Widget) { mapController.addWidget(widget) } + /** + * Remove [Widget] from the map. + * + * @return true if widget was removed + */ + @MapboxExperimental + override fun removeWidget(widget: Widget) = mapController.removeWidget(widget) + /** * Interface for getting snapshot result [Bitmap]. */ diff --git a/sdk/src/main/java/com/mapbox/maps/renderer/MapboxRenderThread.kt b/sdk/src/main/java/com/mapbox/maps/renderer/MapboxRenderThread.kt index 6816af907f..1bf54778fd 100644 --- a/sdk/src/main/java/com/mapbox/maps/renderer/MapboxRenderThread.kt +++ b/sdk/src/main/java/com/mapbox/maps/renderer/MapboxRenderThread.kt @@ -1,6 +1,5 @@ package com.mapbox.maps.renderer -import android.opengl.GLES20 import android.os.SystemClock import android.view.Choreographer import android.view.Surface @@ -49,6 +48,7 @@ internal class MapboxRenderThread : Choreographer.FrameCallback { private var height: Int = 0 private val widgetRenderer: MapboxWidgetRenderer + private var widgetRenderCreated = false private val widgetTextureRenderer: TextureRenderer @Volatile @@ -68,7 +68,6 @@ internal class MapboxRenderThread : Choreographer.FrameCallback { * We track moment when native renderer is prepared. */ private var renderCreated = false - private var widgetRenderCreated = false /** * We track moment when EGL context is created and associated with current Android surface. @@ -155,10 +154,7 @@ internal class MapboxRenderThread : Choreographer.FrameCallback { ) renderCreated = true } - if (!widgetRenderCreated) { - widgetRenderer.onSharedContext(eglCore.eglContext) - widgetRenderCreated = true - } + return true } } @@ -225,6 +221,13 @@ internal class MapboxRenderThread : Choreographer.FrameCallback { } } + private fun checkWidgetRender() { + if (eglPrepared && !widgetRenderCreated && widgetRenderer.hasWidgets()) { + widgetRenderer.onSharedContext(eglCore.eglContext) + widgetRenderCreated = true + } + } + private fun draw() { val renderTimeNsCopy = renderTimeNs val currentTimeNs = SystemClock.elapsedRealtimeNanos() @@ -236,16 +239,19 @@ internal class MapboxRenderThread : Choreographer.FrameCallback { return } - if (widgetRenderer.needRender) { - eglCore.makeNothingCurrent() - widgetRenderer.updateTexture() - eglCore.makeCurrent(eglSurface) - } + if (widgetRenderer.hasWidgets()) { + if (widgetRenderer.needRender) { + widgetRenderer.updateTexture() + eglCore.makeCurrent(eglSurface) + } - mapboxRenderer.render() + mapboxRenderer.render() - if (widgetRenderer.getTextureId() != 0) { - widgetTextureRenderer.render(widgetRenderer.getTextureId()) + if (widgetRenderer.hasTexture()) { + widgetTextureRenderer.render(widgetRenderer.getTexture()) + } + } else { + mapboxRenderer.render() } // assuming render event queue holds user's runnables with OpenGL ES commands @@ -291,6 +297,7 @@ internal class MapboxRenderThread : Choreographer.FrameCallback { eglCore.releaseSurface(eglSurface) eglContextCreated = false eglSurface = eglCore.eglNoSurface + widgetRenderCreated = false widgetRenderer.release() } @@ -301,7 +308,6 @@ internal class MapboxRenderThread : Choreographer.FrameCallback { renderCreated = false releaseEgl() surface?.release() - widgetRenderer.release() } private fun prepareRenderFrame(creatingSurface: Boolean = false) { @@ -327,6 +333,7 @@ internal class MapboxRenderThread : Choreographer.FrameCallback { return } } + checkWidgetRender() checkSurfaceSizeChanged() Choreographer.getInstance().postFrameCallback(this) awaitingNextVsync = true @@ -371,6 +378,8 @@ internal class MapboxRenderThread : Choreographer.FrameCallback { widgetRenderer.addWidget(widget) } + fun removeWidget(widget: Widget) = widgetRenderer.removeWidget(widget) + @WorkerThread internal fun processAndroidSurface(surface: Surface, width: Int, height: Int) { if (this.surface != surface) { diff --git a/sdk/src/main/java/com/mapbox/maps/renderer/MapboxSurfaceRenderer.kt b/sdk/src/main/java/com/mapbox/maps/renderer/MapboxSurfaceRenderer.kt index feb5662ae5..246c1eb1aa 100644 --- a/sdk/src/main/java/com/mapbox/maps/renderer/MapboxSurfaceRenderer.kt +++ b/sdk/src/main/java/com/mapbox/maps/renderer/MapboxSurfaceRenderer.kt @@ -11,7 +11,6 @@ internal open class MapboxSurfaceRenderer : MapboxRenderer { constructor(antialiasingSampleCount: Int) { widgetRenderer = MapboxWidgetRenderer( - translucentSurface = false, antialiasingSampleCount = antialiasingSampleCount, ) renderThread = MapboxRenderThread( @@ -25,7 +24,6 @@ internal open class MapboxSurfaceRenderer : MapboxRenderer { @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE) internal constructor(renderThread: MapboxRenderThread) { widgetRenderer = MapboxWidgetRenderer( - translucentSurface = false, antialiasingSampleCount = 1, ) this.renderThread = renderThread diff --git a/sdk/src/main/java/com/mapbox/maps/renderer/MapboxTextureViewRenderer.kt b/sdk/src/main/java/com/mapbox/maps/renderer/MapboxTextureViewRenderer.kt index 975318d8d8..668b55cf14 100644 --- a/sdk/src/main/java/com/mapbox/maps/renderer/MapboxTextureViewRenderer.kt +++ b/sdk/src/main/java/com/mapbox/maps/renderer/MapboxTextureViewRenderer.kt @@ -11,7 +11,6 @@ internal class MapboxTextureViewRenderer : MapboxRenderer, TextureView.SurfaceTe constructor(textureView: TextureView, antialiasingSampleCount: Int) { val widgetRenderer = MapboxWidgetRenderer( - translucentSurface = false, antialiasingSampleCount = antialiasingSampleCount, ) this.widgetRenderer = widgetRenderer @@ -30,7 +29,6 @@ internal class MapboxTextureViewRenderer : MapboxRenderer, TextureView.SurfaceTe @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE) internal constructor(renderThread: MapboxRenderThread) { val widgetRenderer = MapboxWidgetRenderer( - translucentSurface = false, antialiasingSampleCount = 1, ) this.widgetRenderer = widgetRenderer diff --git a/sdk/src/main/java/com/mapbox/maps/renderer/MapboxWidgetRenderer.kt b/sdk/src/main/java/com/mapbox/maps/renderer/MapboxWidgetRenderer.kt index 3653db6e52..44a99597e3 100644 --- a/sdk/src/main/java/com/mapbox/maps/renderer/MapboxWidgetRenderer.kt +++ b/sdk/src/main/java/com/mapbox/maps/renderer/MapboxWidgetRenderer.kt @@ -11,12 +11,12 @@ import javax.microedition.khronos.egl.EGLSurface @MapboxExperimental internal class MapboxWidgetRenderer( - private val translucentSurface: Boolean, private val antialiasingSampleCount: Int, ) { - private var widgetEglPrepared = false - private lateinit var widgetEglSurface: EGLSurface - private lateinit var widgetEglCore: EGLCore + private var eglCore: EGLCore? = null + private var eglPrepared = false + private var eglSurface: EGLSurface? = null + private var sizeChanged = false private val textures = intArrayOf(0) private val framebuffers = intArrayOf(0) @@ -26,31 +26,34 @@ internal class MapboxWidgetRenderer( private var width = 0 private var height = 0 - fun getTextureId() = textures[0] - val needRender: Boolean get() = widgets.any { it.renderer.needRender } + fun hasWidgets() = widgets.isNotEmpty() + + fun hasTexture() = textures[0] != 0 + + fun getTexture() = textures[0] + fun onSharedContext(sharedContext: EGLContext) { - if (widgetEglPrepared) { - TODO("New shared context while previous is still alive!") + if (eglPrepared) { + release() } - widgetEglCore = EGLCore( - translucentSurface = translucentSurface, + eglCore = EGLCore( + translucentSurface = false, antialiasingSampleCount = antialiasingSampleCount, sharedContext = sharedContext, ) - widgetEglSurface = widgetEglCore.eglNoSurface } fun onSurfaceChanged(width: Int, height: Int) { - // TODO createOffscreenSurface with new dimensions + sizeChanged = true this.width = width this.height = height widgets.forEach { it.renderer.onSurfaceChanged(width, height) } } - private fun textureToFramebuffer() { + private fun attachTexture() { if (textures[0] != 0) { GLES20.glDeleteTextures(textures.size, textures, 0) } @@ -96,11 +99,11 @@ internal class MapboxWidgetRenderer( } fun release() { - if (widgetEglPrepared) { - widgetEglPrepared = false - - if (widgetEglSurface != widgetEglCore.eglNoSurface) { - widgetEglCore.makeCurrent(widgetEglSurface) + val eglCore = this.eglCore + val eglSurface = this.eglSurface + if (eglCore != null) { + if (eglSurface != null && eglSurface != eglCore.eglNoSurface) { + eglCore.makeCurrent(eglSurface) GLES20.glDeleteFramebuffers(framebuffers.size, framebuffers, 0) GLES20.glDeleteTextures(textures.size, textures, 0) @@ -109,53 +112,84 @@ internal class MapboxWidgetRenderer( } widgets.clear() - widgetEglCore.releaseSurface(widgetEglSurface) - - widgetEglSurface = widgetEglCore.eglNoSurface + eglCore.releaseSurface(eglSurface) } - widgetEglCore.release() + eglCore.release() } + this.eglSurface = null + this.eglCore = null } fun updateTexture() { if (needRender) { + checkSizeChanged() checkEgl() - widgetEglCore.makeCurrent(widgetEglSurface) - GLES20.glBindFramebuffer(GLES20.GL_FRAMEBUFFER, framebuffers[0]) - textureToFramebuffer() - widgets.forEach { - it.renderer.render() + val eglCore = this.eglCore + val eglSurface = this.eglSurface + if (eglCore != null && eglSurface != null && eglSurface != eglCore.eglNoSurface) { + eglCore.makeCurrent(eglSurface) + bindFramebuffer() + attachTexture() + widgets.forEach { + it.renderer.render() + } + unbindFramebuffer() + } + } + } + + private fun checkSizeChanged() { + if (sizeChanged) { + val eglCore = this.eglCore + val eglSurface = this.eglSurface + if (eglCore != null && eglSurface != null && eglSurface != eglCore.eglNoSurface) { + eglCore.releaseSurface(eglSurface) + this.eglSurface = eglCore.eglNoSurface } - GLES20.glBindFramebuffer(GLES20.GL_FRAMEBUFFER, 0) - widgetEglCore.makeNothingCurrent() + + sizeChanged = false } } + private fun unbindFramebuffer() { + GLES20.glBindFramebuffer(GLES20.GL_FRAMEBUFFER, 0) + } + + private fun bindFramebuffer() { + if (framebuffers[0] == 0) { + GLES20.glGenFramebuffers(1, framebuffers, 0) + } + GLES20.glBindFramebuffer(GLES20.GL_FRAMEBUFFER, framebuffers[0]) + } + private fun checkEgl() { - if (widgetEglPrepared && widgetEglSurface != widgetEglCore.eglNoSurface) { + val eglSurface = this.eglSurface + val eglCore = this.eglCore + + if (eglCore == null) { + Logger.e(TAG, "Cannot prepare egl, eglCore has not been initialized yet.") + return + } + if (eglSurface != null && eglSurface != eglCore.eglNoSurface) { return } - if (!widgetEglPrepared) { - widgetEglPrepared = widgetEglCore.prepareEgl() - if (!widgetEglPrepared) { + if (!eglPrepared) { + eglPrepared = eglCore.prepareEgl() + if (!eglPrepared) { Logger.e(TAG, "Widget EGL was not configured, please check logs above.") return } } - if (widgetEglSurface == widgetEglCore.eglNoSurface) { - widgetEglSurface = widgetEglCore.createOffscreenSurface(width = width, height = height) - if (widgetEglSurface == widgetEglCore.eglNoSurface) { + if (eglSurface == null || eglSurface == eglCore.eglNoSurface) { + this.eglSurface = eglCore.createOffscreenSurface(width = width, height = height) + if (eglSurface == eglCore.eglNoSurface) { + Logger.e(TAG, "Widget offscreen surface was not configured, please check logs above.") return } } - - widgetEglCore.makeCurrent(widgetEglSurface) - - GLES20.glGenFramebuffers(1, framebuffers, 0) - GLES20.glBindFramebuffer(GLES20.GL_FRAMEBUFFER, framebuffers[0]) } fun addWidget(widget: Widget) { diff --git a/sdk/src/main/java/com/mapbox/maps/renderer/gl/GlUtils.kt b/sdk/src/main/java/com/mapbox/maps/renderer/gl/GlUtils.kt index f7434b48b8..7ed9b47074 100644 --- a/sdk/src/main/java/com/mapbox/maps/renderer/gl/GlUtils.kt +++ b/sdk/src/main/java/com/mapbox/maps/renderer/gl/GlUtils.kt @@ -9,6 +9,20 @@ import java.nio.FloatBuffer internal object GlUtils { + fun FloatArray.put(vararg values: Float) { + values.forEachIndexed { index, value -> + this[index] = value + } + } + + fun FloatBuffer.put(vararg values: Float) { + rewind() + values.forEach { value -> + this.put(value) + } + rewind() + } + fun FloatArray.toFloatBuffer(): FloatBuffer = ByteBuffer.allocateDirect(size * 4) .order(ByteOrder.nativeOrder()) .asFloatBuffer().also { diff --git a/sdk/src/main/java/com/mapbox/maps/renderer/gl/TextureRenderer.kt b/sdk/src/main/java/com/mapbox/maps/renderer/gl/TextureRenderer.kt index a4b33a3846..f0de88c0db 100644 --- a/sdk/src/main/java/com/mapbox/maps/renderer/gl/TextureRenderer.kt +++ b/sdk/src/main/java/com/mapbox/maps/renderer/gl/TextureRenderer.kt @@ -124,16 +124,16 @@ internal class TextureRenderer( GLES20.glBindBuffer(GLES20.GL_ARRAY_BUFFER, 0) } - companion object { - private const val COORDS_PER_VERTEX = 3 - private const val COORDS_PER_TEX = 2 - private const val BYTES_PER_FLOAT = 4 - private const val VERTEX_STRIDE = COORDS_PER_VERTEX * BYTES_PER_FLOAT - private const val TEX_STRIDE = COORDS_PER_TEX * BYTES_PER_FLOAT - private const val VERTEX_COUNT = 4 - - private val VERTEX_SHADER_CODE = """ - uniform mat4 uTexMatrix; + private companion object { + const val COORDS_PER_VERTEX = 3 + const val COORDS_PER_TEX = 2 + const val BYTES_PER_FLOAT = 4 + const val VERTEX_STRIDE = COORDS_PER_VERTEX * BYTES_PER_FLOAT + const val TEX_STRIDE = COORDS_PER_TEX * BYTES_PER_FLOAT + const val VERTEX_COUNT = 4 + + val VERTEX_SHADER_CODE = """ + precision highp float; attribute vec4 aPosition; attribute vec2 aTexCoord; varying vec2 vTexCoord; @@ -144,7 +144,7 @@ internal class TextureRenderer( } """.trimIndent() - private val FRAGMENT_SHADER_CODE = """ + val FRAGMENT_SHADER_CODE = """ precision mediump float; varying vec2 vTexCoord; uniform sampler2D uTexture; diff --git a/sdk/src/main/java/com/mapbox/maps/renderer/widget/BitmapWidget.kt b/sdk/src/main/java/com/mapbox/maps/renderer/widget/BitmapWidget.kt index cfef775db5..c4a3efc370 100644 --- a/sdk/src/main/java/com/mapbox/maps/renderer/widget/BitmapWidget.kt +++ b/sdk/src/main/java/com/mapbox/maps/renderer/widget/BitmapWidget.kt @@ -4,7 +4,7 @@ import android.graphics.Bitmap import com.mapbox.maps.MapboxExperimental @MapboxExperimental -open class BitmapWidget( +open class BitmapWidget @JvmOverloads constructor( bitmap: Bitmap, position: WidgetPosition = WidgetPosition( vertical = WidgetPosition.Vertical.TOP, diff --git a/sdk/src/main/java/com/mapbox/maps/renderer/widget/BitmapWidgetRenderer.kt b/sdk/src/main/java/com/mapbox/maps/renderer/widget/BitmapWidgetRenderer.kt index 9e22d4e62e..75cb946127 100644 --- a/sdk/src/main/java/com/mapbox/maps/renderer/widget/BitmapWidgetRenderer.kt +++ b/sdk/src/main/java/com/mapbox/maps/renderer/widget/BitmapWidgetRenderer.kt @@ -5,6 +5,7 @@ import android.opengl.GLES20 import android.opengl.GLUtils import android.opengl.Matrix import com.mapbox.maps.renderer.gl.GlUtils +import com.mapbox.maps.renderer.gl.GlUtils.put import com.mapbox.maps.renderer.gl.GlUtils.toFloatBuffer import java.nio.FloatBuffer @@ -50,20 +51,6 @@ internal class BitmapWidgetRendererImpl( override var needRender: Boolean = true - private fun FloatArray.put(vararg values: Float) { - values.forEachIndexed { index, value -> - this[index] = value - } - } - - private fun FloatBuffer.put(vararg values: Float) { - rewind() - values.forEach { value -> - this.put(value) - } - rewind() - } - override fun onSurfaceChanged(width: Int, height: Int) { surfaceWidth = width surfaceHeight = height @@ -301,6 +288,7 @@ internal class BitmapWidgetRendererImpl( private const val VERTEX_COUNT = 4 private val VERTEX_SHADER_CODE = """ + precision highp float; uniform mat4 uMvpMatrix; attribute vec2 aPosition; attribute vec2 aCoordinate; diff --git a/sdk/src/test/java/com/mapbox/maps/renderer/MapboxRenderThreadTest.kt b/sdk/src/test/java/com/mapbox/maps/renderer/MapboxRenderThreadTest.kt index 683114dae7..a26446a5ff 100644 --- a/sdk/src/test/java/com/mapbox/maps/renderer/MapboxRenderThreadTest.kt +++ b/sdk/src/test/java/com/mapbox/maps/renderer/MapboxRenderThreadTest.kt @@ -61,7 +61,7 @@ class MapboxRenderThreadTest { every { eglCore.eglNoSurface } returns mockk() every { eglCore.swapBuffers(any()) } returns EGL10.EGL_SUCCESS mapboxWidgetRenderer = mockk(relaxUnitFun = true) - every { mapboxWidgetRenderer.getTextureId() } returns 0 + every { mapboxWidgetRenderer.getTexture() } returns 0 every { mapboxWidgetRenderer.needRender } returns false renderHandlerThread = RenderHandlerThread() textureRenderer = mockk(relaxed = true) @@ -550,7 +550,7 @@ class MapboxRenderThreadTest { fun onDrawDoesNotRenderWidgets() { mockValidSurface() every { mapboxWidgetRenderer.needRender } returns false - every { mapboxWidgetRenderer.getTextureId() } returns 0 + every { mapboxWidgetRenderer.getTexture() } returns 0 Shadows.shadowOf(renderHandlerThread.handler?.looper).pause() mapboxRenderThread.queueRenderEvent(MapboxRenderer.renderEventSdk) mapboxRenderThread.queueRenderEvent(MapboxRenderer.renderEventSdk) @@ -569,7 +569,7 @@ class MapboxRenderThreadTest { mockValidSurface() val textureId = 1 every { mapboxWidgetRenderer.needRender } returns true - every { mapboxWidgetRenderer.getTextureId() } returns textureId + every { mapboxWidgetRenderer.getTexture() } returns textureId Shadows.shadowOf(renderHandlerThread.handler?.looper).pause() mapboxRenderThread.queueRenderEvent(MapboxRenderer.renderEventSdk) Shadows.shadowOf(renderHandlerThread.handler?.looper).idle() From a462188e5e8f12e1621be0e0a6a5cdf0d3f9a2b0 Mon Sep 17 00:00:00 2001 From: yunik Date: Wed, 9 Feb 2022 13:18:28 +0200 Subject: [PATCH 04/13] More review fixes. --- .../maps/testapp/auto/car/MapSession.kt | 10 ++++-- .../extension/androidauto/CompassWidget.kt | 26 ++++++++++---- .../maps/extension/androidauto/LogoWidget.kt | 22 +++++++++--- .../maps/renderer/MapboxWidgetRenderer.kt | 5 ++- .../maps/renderer/widget/BitmapWidget.kt | 21 ++++++++--- .../renderer/widget/BitmapWidgetRenderer.kt | 36 +++++++++---------- .../com/mapbox/maps/renderer/widget/Widget.kt | 12 +++++-- .../maps/renderer/widget/WidgetRenderer.kt | 3 ++ 8 files changed, 92 insertions(+), 43 deletions(-) diff --git a/android-auto-app/src/main/java/com/mapbox/maps/testapp/auto/car/MapSession.kt b/android-auto-app/src/main/java/com/mapbox/maps/testapp/auto/car/MapSession.kt index 1184309a4e..2e71aa9dc5 100644 --- a/android-auto-app/src/main/java/com/mapbox/maps/testapp/auto/car/MapSession.kt +++ b/android-auto-app/src/main/java/com/mapbox/maps/testapp/auto/car/MapSession.kt @@ -4,6 +4,8 @@ import android.Manifest.permission.ACCESS_FINE_LOCATION import android.content.Intent import android.content.pm.PackageManager.PERMISSION_GRANTED import android.content.res.Configuration +import android.os.Handler +import android.os.Looper import androidx.car.app.* import com.mapbox.maps.EdgeInsets import com.mapbox.maps.MapSurface @@ -32,7 +34,11 @@ class MapSession : Session() { scrollListener = carCameraController, ) { surface -> val logo = LogoWidget(carContext) - val compass = CompassWidget(carContext) + val compass = CompassWidget( + carContext, + marginX = 26f, + marginY = 120f, + ) mapSurface = surface carCameraController.init( @@ -53,7 +59,7 @@ class MapSession : Session() { surface.getMapboxMap().apply { addOnCameraChangeListener { - compass.rotate(this.cameraState.bearing.toFloat()) + compass.setRotation(this.cameraState.bearing.toFloat()) } } } diff --git a/extension-androidauto/src/main/java/com/mapbox/maps/extension/androidauto/CompassWidget.kt b/extension-androidauto/src/main/java/com/mapbox/maps/extension/androidauto/CompassWidget.kt index aff4b972d3..ec976f9c74 100644 --- a/extension-androidauto/src/main/java/com/mapbox/maps/extension/androidauto/CompassWidget.kt +++ b/extension-androidauto/src/main/java/com/mapbox/maps/extension/androidauto/CompassWidget.kt @@ -5,12 +5,24 @@ import android.graphics.BitmapFactory import com.mapbox.maps.renderer.widget.BitmapWidget import com.mapbox.maps.renderer.widget.WidgetPosition -class CompassWidget(context: Context) : BitmapWidget( - bitmap = BitmapFactory.decodeResource(context.resources, R.drawable.mapbox_compass_icon), - position = WidgetPosition( - horizontal = WidgetPosition.Horizontal.LEFT, - vertical = WidgetPosition.Vertical.CENTER, +/** + * Widget shows compass. Positioned in the top right corner by default. + * + * @param position position of compass + * @param marginX horizontal margin in pixels + * @param marginY vertical margin in pixels + */ +class CompassWidget( + context: Context, + position: WidgetPosition = WidgetPosition( + horizontal = WidgetPosition.Horizontal.RIGHT, + vertical = WidgetPosition.Vertical.TOP, ), - marginX = 20f, - marginY = 20f + marginX: Float = 20f, + marginY: Float = 20f, +) : BitmapWidget( + bitmap = BitmapFactory.decodeResource(context.resources, R.drawable.mapbox_compass_icon), + position = position, + marginX = marginX, + marginY = marginY, ) \ No newline at end of file diff --git a/extension-androidauto/src/main/java/com/mapbox/maps/extension/androidauto/LogoWidget.kt b/extension-androidauto/src/main/java/com/mapbox/maps/extension/androidauto/LogoWidget.kt index 2bf042a29c..81d4f6ef55 100644 --- a/extension-androidauto/src/main/java/com/mapbox/maps/extension/androidauto/LogoWidget.kt +++ b/extension-androidauto/src/main/java/com/mapbox/maps/extension/androidauto/LogoWidget.kt @@ -5,12 +5,24 @@ import android.graphics.BitmapFactory import com.mapbox.maps.renderer.widget.BitmapWidget import com.mapbox.maps.renderer.widget.WidgetPosition -class LogoWidget(context: Context) : BitmapWidget( - bitmap = BitmapFactory.decodeResource(context.resources, R.drawable.mapbox_logo_icon), - position = WidgetPosition( +/** + * Widget shows compass. Positioned in the bottom left corner by default. + * + * @param position position of logo + * @param marginX horizontal margin in pixels + * @param marginY vertical margin in pixels + */ +class LogoWidget( + context: Context, + position: WidgetPosition = WidgetPosition( horizontal = WidgetPosition.Horizontal.LEFT, vertical = WidgetPosition.Vertical.BOTTOM, ), - marginX = 20f, - marginY = 20f, + marginX: Float = 20f, + marginY: Float = 20f, +) : BitmapWidget( + bitmap = BitmapFactory.decodeResource(context.resources, R.drawable.mapbox_logo_icon), + position = position, + marginX = marginX, + marginY = marginY, ) \ No newline at end of file diff --git a/sdk/src/main/java/com/mapbox/maps/renderer/MapboxWidgetRenderer.kt b/sdk/src/main/java/com/mapbox/maps/renderer/MapboxWidgetRenderer.kt index 44a99597e3..7c5e171de9 100644 --- a/sdk/src/main/java/com/mapbox/maps/renderer/MapboxWidgetRenderer.kt +++ b/sdk/src/main/java/com/mapbox/maps/renderer/MapboxWidgetRenderer.kt @@ -9,7 +9,6 @@ import java.util.concurrent.CopyOnWriteArraySet import javax.microedition.khronos.egl.EGLContext import javax.microedition.khronos.egl.EGLSurface -@MapboxExperimental internal class MapboxWidgetRenderer( private val antialiasingSampleCount: Int, ) { @@ -199,7 +198,7 @@ internal class MapboxWidgetRenderer( fun removeWidget(widget: Widget) = widgets.remove(widget) - companion object { - private const val TAG: String = "MapboxWidgetRenderer" + private companion object { + const val TAG: String = "MapboxWidgetRenderer" } } diff --git a/sdk/src/main/java/com/mapbox/maps/renderer/widget/BitmapWidget.kt b/sdk/src/main/java/com/mapbox/maps/renderer/widget/BitmapWidget.kt index c4a3efc370..8ab694678c 100644 --- a/sdk/src/main/java/com/mapbox/maps/renderer/widget/BitmapWidget.kt +++ b/sdk/src/main/java/com/mapbox/maps/renderer/widget/BitmapWidget.kt @@ -3,6 +3,14 @@ package com.mapbox.maps.renderer.widget import android.graphics.Bitmap import com.mapbox.maps.MapboxExperimental +/** + * Widget displaying bitmap within specified position and margins. + * + * @param bitmap bitmap used to draw widget + * @param position position of widget + * @param marginX horizontal margin in pixels + * @param marginY vertical margin in pixels + */ @MapboxExperimental open class BitmapWidget @JvmOverloads constructor( bitmap: Bitmap, @@ -20,15 +28,18 @@ open class BitmapWidget @JvmOverloads constructor( marginY = marginY, ) - override fun updateBitmap(bitmap: Bitmap) { + /** + * Update bitmap widget uses. + */ + fun updateBitmap(bitmap: Bitmap) { renderer.updateBitmap(bitmap) } - override fun translate(translateX: Float, translateY: Float) { - renderer.translate(translateX = translateX, translateY = translateY) + override fun setTranslation(translationX: Float, translationY: Float) { + renderer.setTranslation(translationX = translationX, translationY = translationY) } - override fun rotate(angleDegrees: Float) { - renderer.rotate(angleDegrees = angleDegrees) + override fun setRotation(angleDegrees: Float) { + renderer.setRotation(angleDegrees = angleDegrees) } } diff --git a/sdk/src/main/java/com/mapbox/maps/renderer/widget/BitmapWidgetRenderer.kt b/sdk/src/main/java/com/mapbox/maps/renderer/widget/BitmapWidgetRenderer.kt index 75cb946127..ec12a53c2a 100644 --- a/sdk/src/main/java/com/mapbox/maps/renderer/widget/BitmapWidgetRenderer.kt +++ b/sdk/src/main/java/com/mapbox/maps/renderer/widget/BitmapWidgetRenderer.kt @@ -7,17 +7,16 @@ import android.opengl.Matrix import com.mapbox.maps.renderer.gl.GlUtils import com.mapbox.maps.renderer.gl.GlUtils.put import com.mapbox.maps.renderer.gl.GlUtils.toFloatBuffer -import java.nio.FloatBuffer internal class BitmapWidgetRendererImpl( - private var bitmap: Bitmap, + private var bitmap: Bitmap?, private val position: WidgetPosition, private val marginX: Float, private val marginY: Float, ) : WidgetRenderer { - private var bitmapWidth = bitmap.width - private var bitmapHeight = bitmap.height + private var bitmapWidth = bitmap?.width ?: 0 + private var bitmapHeight = bitmap?.height ?: 0 private var surfaceWidth = 0 private var surfaceHeight = 0 @@ -223,7 +222,7 @@ internal class BitmapWidgetRendererImpl( } private fun textureFromBitmap() { - if (!bitmap.isRecycled) { + bitmap?.let { GLES20.glGenTextures(1, textures, 0) GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, textures[0]) GLES20.glTexParameterf( @@ -246,8 +245,9 @@ internal class BitmapWidgetRendererImpl( GLES20.GL_TEXTURE_WRAP_T, GLES20.GL_CLAMP_TO_EDGE.toFloat() ) - GLUtils.texImage2D(GLES20.GL_TEXTURE_2D, 0, bitmap, 0) - bitmap.recycle() + GLUtils.texImage2D(GLES20.GL_TEXTURE_2D, 0, it, 0) + + bitmap = null } } @@ -260,20 +260,20 @@ internal class BitmapWidgetRendererImpl( needRender = true } - fun rotate(angleDegrees: Float) { + override fun setRotation(angleDegrees: Float) { Matrix.setIdentityM(rotationMatrix, 0) Matrix.setRotateM(rotationMatrix, 0, angleDegrees, 0f, 0f, 1f) updateMatrix = true needRender = true } - fun translate(translateX: Float, translateY: Float) { + override fun setTranslation(translationX: Float, translationY: Float) { Matrix.setIdentityM(translateMatrix, 0) Matrix.translateM( translateMatrix, 0, - leftX() + translateX, - topY() + translateY, + leftX() + translationX, + topY() + translationY, 0f ) @@ -281,13 +281,13 @@ internal class BitmapWidgetRendererImpl( needRender = true } - companion object { - private const val COORDS_PER_VERTEX = 2 - private const val BYTES_PER_FLOAT = 4 - private const val VERTEX_STRIDE = COORDS_PER_VERTEX * BYTES_PER_FLOAT - private const val VERTEX_COUNT = 4 + private companion object { + const val COORDS_PER_VERTEX = 2 + const val BYTES_PER_FLOAT = 4 + const val VERTEX_STRIDE = COORDS_PER_VERTEX * BYTES_PER_FLOAT + const val VERTEX_COUNT = 4 - private val VERTEX_SHADER_CODE = """ + val VERTEX_SHADER_CODE = """ precision highp float; uniform mat4 uMvpMatrix; attribute vec2 aPosition; @@ -299,7 +299,7 @@ internal class BitmapWidgetRendererImpl( } """.trimIndent() - private val FRAGMENT_SHADER_CODE = """ + val FRAGMENT_SHADER_CODE = """ precision mediump float; uniform sampler2D uTexture; varying vec2 vCoordinate; diff --git a/sdk/src/main/java/com/mapbox/maps/renderer/widget/Widget.kt b/sdk/src/main/java/com/mapbox/maps/renderer/widget/Widget.kt index a14b99b770..3a8b924d2a 100644 --- a/sdk/src/main/java/com/mapbox/maps/renderer/widget/Widget.kt +++ b/sdk/src/main/java/com/mapbox/maps/renderer/widget/Widget.kt @@ -7,7 +7,13 @@ import com.mapbox.maps.MapboxExperimental abstract class Widget { internal abstract val renderer : WidgetRenderer - abstract fun updateBitmap(bitmap: Bitmap) - abstract fun translate(translateX: Float, translateY: Float) - abstract fun rotate(angleDegrees: Float) + /** + * Set absolute translation of widget in pixels. + */ + abstract fun setTranslation(translateX: Float, translateY: Float) + + /** + * Set absolute rotation of widget in angles. + */ + abstract fun setRotation(angleDegrees: Float) } \ No newline at end of file diff --git a/sdk/src/main/java/com/mapbox/maps/renderer/widget/WidgetRenderer.kt b/sdk/src/main/java/com/mapbox/maps/renderer/widget/WidgetRenderer.kt index 865163ded0..2a1f44e67c 100644 --- a/sdk/src/main/java/com/mapbox/maps/renderer/widget/WidgetRenderer.kt +++ b/sdk/src/main/java/com/mapbox/maps/renderer/widget/WidgetRenderer.kt @@ -7,4 +7,7 @@ internal interface WidgetRenderer { fun prepare() fun render() fun release() + + fun setRotation(angleDegrees: Float) + fun setTranslation(translationX: Float, translationY: Float) } From 6c40e3ed2ed18243c22e42dfde2df254f967965a Mon Sep 17 00:00:00 2001 From: yunik Date: Wed, 9 Feb 2022 18:35:47 +0200 Subject: [PATCH 05/13] More review fixes. --- .../java/com/mapbox/maps/renderer/MapboxWidgetRenderer.kt | 7 +++---- .../main/java/com/mapbox/maps/renderer/widget/Widget.kt | 6 ++++-- .../java/com/mapbox/maps/renderer/widget/WidgetPosition.kt | 3 +++ 3 files changed, 10 insertions(+), 6 deletions(-) diff --git a/sdk/src/main/java/com/mapbox/maps/renderer/MapboxWidgetRenderer.kt b/sdk/src/main/java/com/mapbox/maps/renderer/MapboxWidgetRenderer.kt index 7c5e171de9..b3c6952499 100644 --- a/sdk/src/main/java/com/mapbox/maps/renderer/MapboxWidgetRenderer.kt +++ b/sdk/src/main/java/com/mapbox/maps/renderer/MapboxWidgetRenderer.kt @@ -2,7 +2,6 @@ package com.mapbox.maps.renderer import android.opengl.GLES20 import com.mapbox.common.Logger -import com.mapbox.maps.MapboxExperimental import com.mapbox.maps.renderer.egl.EGLCore import com.mapbox.maps.renderer.widget.Widget import java.util.concurrent.CopyOnWriteArraySet @@ -20,12 +19,12 @@ internal class MapboxWidgetRenderer( private val textures = intArrayOf(0) private val framebuffers = intArrayOf(0) - private val widgets = CopyOnWriteArraySet() + private val widgets = mutableListOf() private var width = 0 private var height = 0 - val needRender: Boolean + val needTextureUpdate: Boolean get() = widgets.any { it.renderer.needRender } fun hasWidgets() = widgets.isNotEmpty() @@ -121,7 +120,7 @@ internal class MapboxWidgetRenderer( } fun updateTexture() { - if (needRender) { + if (needTextureUpdate) { checkSizeChanged() checkEgl() val eglCore = this.eglCore diff --git a/sdk/src/main/java/com/mapbox/maps/renderer/widget/Widget.kt b/sdk/src/main/java/com/mapbox/maps/renderer/widget/Widget.kt index 3a8b924d2a..de792d176b 100644 --- a/sdk/src/main/java/com/mapbox/maps/renderer/widget/Widget.kt +++ b/sdk/src/main/java/com/mapbox/maps/renderer/widget/Widget.kt @@ -1,11 +1,13 @@ package com.mapbox.maps.renderer.widget -import android.graphics.Bitmap import com.mapbox.maps.MapboxExperimental +/** + * Base class for widgets displayed on top of the map. + */ @MapboxExperimental abstract class Widget { - internal abstract val renderer : WidgetRenderer + internal abstract val renderer: WidgetRenderer /** * Set absolute translation of widget in pixels. diff --git a/sdk/src/main/java/com/mapbox/maps/renderer/widget/WidgetPosition.kt b/sdk/src/main/java/com/mapbox/maps/renderer/widget/WidgetPosition.kt index 6f1f3c720f..528b7ca930 100644 --- a/sdk/src/main/java/com/mapbox/maps/renderer/widget/WidgetPosition.kt +++ b/sdk/src/main/java/com/mapbox/maps/renderer/widget/WidgetPosition.kt @@ -2,6 +2,9 @@ package com.mapbox.maps.renderer.widget import com.mapbox.maps.MapboxExperimental +/** + * Specifies widget position relative to the screen. + */ @MapboxExperimental class WidgetPosition( val horizontal: Horizontal, From d9b0d1ea5664f119437bfd1b6291844a52adeb61 Mon Sep 17 00:00:00 2001 From: yunik Date: Wed, 9 Feb 2022 18:36:01 +0200 Subject: [PATCH 06/13] Updated tests. --- sdk/src/test/java/com/mapbox/TestUtils.kt | 39 +- .../maps/renderer/MapboxRenderThreadTest.kt | 508 +++++++++--------- 2 files changed, 262 insertions(+), 285 deletions(-) diff --git a/sdk/src/test/java/com/mapbox/TestUtils.kt b/sdk/src/test/java/com/mapbox/TestUtils.kt index c668cb53f2..1a24d4ee3d 100644 --- a/sdk/src/test/java/com/mapbox/TestUtils.kt +++ b/sdk/src/test/java/com/mapbox/TestUtils.kt @@ -1,35 +1,40 @@ package com.mapbox -import io.mockk.MockKVerificationScope -import io.mockk.Ordering -import io.mockk.verify +import io.mockk.* +import java.util.concurrent.CountDownLatch +import java.util.concurrent.TimeUnit +import java.util.concurrent.TimeoutException internal fun verifyNo( ordering: Ordering = Ordering.UNORDERED, - inverse: Boolean = false, timeout: Long = 0, verifyBlock: MockKVerificationScope.() -> Unit ) = verify( - ordering, - inverse, - 1, - Int.MAX_VALUE, + ordering = ordering, exactly = 0, - timeout, - verifyBlock + timeout = timeout, + verifyBlock = verifyBlock, ) internal fun verifyOnce( ordering: Ordering = Ordering.UNORDERED, - inverse: Boolean = false, timeout: Long = 0, verifyBlock: MockKVerificationScope.() -> Unit ) = verify( - ordering, - inverse, - 1, - Int.MAX_VALUE, + ordering = ordering, exactly = 1, - timeout, - verifyBlock + timeout = timeout, + verifyBlock = verifyBlock, ) + +internal fun waitZeroCounter(startCounter: Int = 1, timeoutMillis: Int = 1000, runnable: CountDownLatch.() -> Unit) { + val countDownLatch = CountDownLatch(startCounter) + runnable(countDownLatch) + if (!countDownLatch.await(timeoutMillis.toLong(), TimeUnit.MILLISECONDS)) { + throw TimeoutException("Test had failed, counter is not zero but $startCounter after $timeoutMillis milliseconds!") + } +} + +internal fun CountDownLatch.countDownEvery(stubBlock: MockKMatcherScope.() -> Unit) { + every(stubBlock).answers { countDown() } +} diff --git a/sdk/src/test/java/com/mapbox/maps/renderer/MapboxRenderThreadTest.kt b/sdk/src/test/java/com/mapbox/maps/renderer/MapboxRenderThreadTest.kt index a26446a5ff..8f3c9b7e3c 100644 --- a/sdk/src/test/java/com/mapbox/maps/renderer/MapboxRenderThreadTest.kt +++ b/sdk/src/test/java/com/mapbox/maps/renderer/MapboxRenderThreadTest.kt @@ -2,15 +2,17 @@ package com.mapbox.maps.renderer import android.view.Surface import com.mapbox.common.ShadowLogger +import com.mapbox.countDownEvery import com.mapbox.maps.renderer.MapboxRenderThread.Companion.RETRY_DELAY_MS import com.mapbox.maps.renderer.egl.EGLCore import com.mapbox.maps.renderer.gl.TextureRenderer import com.mapbox.verifyNo import com.mapbox.verifyOnce +import com.mapbox.waitZeroCounter import io.mockk.* import org.junit.After import org.junit.Assert.assertEquals -import org.junit.Before +import org.junit.Assert.assertFalse import org.junit.Test import org.junit.runner.RunWith import org.robolectric.RobolectricTestRunner @@ -19,9 +21,7 @@ import org.robolectric.annotation.Config import org.robolectric.annotation.LooperMode import java.util.concurrent.CountDownLatch import java.util.concurrent.TimeUnit -import java.util.concurrent.TimeoutException import javax.microedition.khronos.egl.EGL10 -import javax.microedition.khronos.egl.EGLSurface @RunWith(RobolectricTestRunner::class) @Config(shadows = [ShadowLogger::class]) @@ -34,63 +34,74 @@ class MapboxRenderThreadTest { private lateinit var eglCore: EGLCore private lateinit var renderHandlerThread: RenderHandlerThread private lateinit var textureRenderer: TextureRenderer - private val waitTime = 300L + private lateinit var surface: Surface - private fun mockValidSurface(): Surface { - val surface = mockk() + private fun initRenderThread(mapboxRenderer: MapboxRenderer = mockk(relaxUnitFun = true)) { + this.mapboxRenderer = mapboxRenderer + mockEglCore() + mockWidgetRenderer() + renderHandlerThread = RenderHandlerThread() + textureRenderer = mockk(relaxed = true) + mapboxRenderThread = MapboxRenderThread( + mapboxRenderer, + mapboxWidgetRenderer, + renderHandlerThread, + eglCore, + textureRenderer, + ) + renderHandlerThread.start() + } + + private fun mockSurface() { + surface = mockk() every { surface.isValid } returns true every { surface.release() } just Runs + } + + private fun mockWidgetRenderer() { + mapboxWidgetRenderer = mockk(relaxUnitFun = true) + every { mapboxWidgetRenderer.getTexture() } returns 0 + every { mapboxWidgetRenderer.hasTexture() } returns false + every { mapboxWidgetRenderer.needTextureUpdate } returns false + every { mapboxWidgetRenderer.hasWidgets() } returns false + } + + private fun mockEglCore() { + eglCore = mockk(relaxUnitFun = true) + every { eglCore.eglNoSurface } returns mockk() + every { eglCore.eglContext } returns mockk() every { eglCore.prepareEgl() } returns true every { eglCore.createWindowSurface(any()) } returns mockk(relaxed = true) every { eglCore.makeNothingCurrent() } returns true every { eglCore.makeCurrent(any()) } returns true every { eglCore.swapBuffers(any()) } returns EGL10.EGL_SUCCESS + } + + private fun provideValidSurface() { + mockSurface() mapboxRenderThread.onSurfaceCreated(surface, 1, 1) - Shadows.shadowOf(renderHandlerThread.handler?.looper).idle() - return surface + idleHandler() } private fun mockCountdownRunnable(latch: CountDownLatch) = mockk(relaxUnitFun = true).also { every { it.run() } answers { latch.countDown() } } - @Before - fun setUp() { - mapboxRenderer = mockk(relaxUnitFun = true) - eglCore = mockk(relaxUnitFun = true) - every { eglCore.eglNoSurface } returns mockk() - every { eglCore.swapBuffers(any()) } returns EGL10.EGL_SUCCESS - mapboxWidgetRenderer = mockk(relaxUnitFun = true) - every { mapboxWidgetRenderer.getTexture() } returns 0 - every { mapboxWidgetRenderer.needRender } returns false - renderHandlerThread = RenderHandlerThread() - textureRenderer = mockk(relaxed = true) - mapboxRenderThread = MapboxRenderThread( - mapboxRenderer, - mapboxWidgetRenderer, - renderHandlerThread, - eglCore, - textureRenderer, - ).apply { - renderHandlerThread.start() - } - } + private fun pauseHandler() = Shadows.shadowOf(renderHandlerThread.handler?.looper).pause() + + private fun idleHandler() = Shadows.shadowOf(renderHandlerThread.handler?.looper).idle() @After fun cleanup() { - clearAllMocks() renderHandlerThread.stop() + clearAllMocks() } @Test fun onSurfaceCreatedTest() { - val latch = CountDownLatch(1) - every { mapboxRenderer.createRenderer() } answers { latch.countDown() } - mockValidSurface() - if (!latch.await(waitTime, TimeUnit.MILLISECONDS)) { - throw TimeoutException() - } - verify { + initRenderThread() + provideValidSurface() + verifyOnce { mapboxRenderer.createRenderer() eglCore.makeNothingCurrent() mapboxRenderer.onSurfaceChanged(1, 1) @@ -98,32 +109,25 @@ class MapboxRenderThreadTest { } @Test - fun onSurfaceCreatedNotNativeSupportedTest() { - val latch = CountDownLatch(1) - val surface = mockk() - every { surface.isValid } returns true -// every { eglCore.eglStatusSuccess } returns false - every { eglCore.createWindowSurface(any()) } returns mockk(relaxed = true) - mapboxRenderThread.onSurfaceCreated(surface, 1, 1) - latch.await(waitTime, TimeUnit.MILLISECONDS) + fun onInvalidEglSurfaceNotCreateRenderer() { + initRenderThread() + every { eglCore.createWindowSurface(any()) } returns eglCore.eglNoSurface + provideValidSurface() verifyNo { mapboxRenderer.createRenderer() mapboxRenderer.onSurfaceChanged(1, 1) } } + @Test - fun onSurfaceSizeChangedIndeedTest() { - val latch = CountDownLatch(1) - every { mapboxRenderer.createRenderer() } answers { latch.countDown() } - mockValidSurface() - Shadows.shadowOf(renderHandlerThread.handler?.looper).pause() + fun onSurfaceSizeChangedTest() { + initRenderThread() + provideValidSurface() + pauseHandler() mapboxRenderThread.onSurfaceSizeChanged(2, 2) - Shadows.shadowOf(renderHandlerThread.handler?.looper).idle() - if (!latch.await(waitTime, TimeUnit.MILLISECONDS)) { - throw TimeoutException() - } - verify { + idleHandler() + verifyOnce { mapboxRenderer.createRenderer() mapboxRenderer.onSurfaceChanged(1, 1) mapboxRenderer.onSurfaceChanged(2, 2) @@ -132,16 +136,12 @@ class MapboxRenderThreadTest { @Test fun onSurfaceSizeChangedSameSizeTest() { - val latch = CountDownLatch(1) - every { mapboxRenderer.createRenderer() } answers { latch.countDown() } - mockValidSurface() - Shadows.shadowOf(renderHandlerThread.handler?.looper).pause() + initRenderThread() + provideValidSurface() + pauseHandler() mapboxRenderThread.onSurfaceSizeChanged(1, 1) - Shadows.shadowOf(renderHandlerThread.handler?.looper).idle() - if (!latch.await(waitTime, TimeUnit.MILLISECONDS)) { - throw TimeoutException() - } - verify { + idleHandler() + verifyOnce { mapboxRenderer.createRenderer() mapboxRenderer.onSurfaceChanged(1, 1) } @@ -149,44 +149,41 @@ class MapboxRenderThreadTest { @Test fun onSurfaceWithActivityDestroyedAfterSurfaceTest() { - val latch = CountDownLatch(1) - every { mapboxRenderer.destroyRenderer() } answers { latch.countDown() } - mockValidSurface() - mapboxRenderThread.onSurfaceDestroyed() - mapboxRenderThread.destroy() - if (!latch.await(waitTime, TimeUnit.MILLISECONDS)) { - throw TimeoutException() + initRenderThread() + provideValidSurface() + waitZeroCounter { + countDownEvery { mapboxRenderer.destroyRenderer() } + mapboxRenderThread.onSurfaceDestroyed() + mapboxRenderThread.destroy() } - verify(exactly = 1) { eglCore.release() } - verify { mapboxRenderer.destroyRenderer() } - assert(!renderHandlerThread.started) + verifyOnce { eglCore.release() } + verifyOnce { mapboxRenderer.destroyRenderer() } + assertFalse(renderHandlerThread.started) } @Test fun onSurfaceWithActivityDestroyedBeforeSurfaceTest() { - val latch = CountDownLatch(1) - every { mapboxRenderer.destroyRenderer() } answers { latch.countDown() } - mockValidSurface() - mapboxRenderThread.destroy() - mapboxRenderThread.onSurfaceDestroyed() - if (!latch.await(waitTime, TimeUnit.MILLISECONDS)) { - throw TimeoutException() + initRenderThread() + provideValidSurface() + waitZeroCounter { + countDownEvery { mapboxRenderer.destroyRenderer() } + mapboxRenderThread.destroy() + mapboxRenderThread.onSurfaceDestroyed() } - verify(exactly = 1) { eglCore.release() } - verify(exactly = 1) { mapboxRenderer.destroyRenderer() } - assert(!renderHandlerThread.started) + verifyOnce { eglCore.release() } + verifyOnce { mapboxRenderer.destroyRenderer() } + assertFalse(renderHandlerThread.started) } @Test fun onDrawFrameSeparateRequestRender() { - val latch = CountDownLatch(1) - mockValidSurface() - Shadows.shadowOf(renderHandlerThread.handler?.looper).pause() + initRenderThread() + provideValidSurface() + pauseHandler() mapboxRenderThread.queueRenderEvent(MapboxRenderer.renderEventSdk) - Shadows.shadowOf(renderHandlerThread.handler?.looper).idle() + idleHandler() mapboxRenderThread.queueRenderEvent(MapboxRenderer.renderEventSdk) - Shadows.shadowOf(renderHandlerThread.handler?.looper).idle() - latch.await(waitTime, TimeUnit.MILLISECONDS) + idleHandler() // one swap buffer for surface creation, two for not squashed render requests verify(exactly = 3) { eglCore.swapBuffers(any()) @@ -199,13 +196,12 @@ class MapboxRenderThreadTest { @Test fun onDrawFrameSquashedRequestRender() { - val latch = CountDownLatch(1) - mockValidSurface() - Shadows.shadowOf(renderHandlerThread.handler?.looper).pause() + initRenderThread() + provideValidSurface() + pauseHandler() mapboxRenderThread.queueRenderEvent(MapboxRenderer.renderEventSdk) mapboxRenderThread.queueRenderEvent(MapboxRenderer.renderEventSdk) - Shadows.shadowOf(renderHandlerThread.handler?.looper).idle() - latch.await(waitTime, TimeUnit.MILLISECONDS) + idleHandler() // one swap buffer for surface creation, 2 render requests squash in one swap buffers call verify(exactly = 2) { eglCore.swapBuffers(any()) @@ -214,22 +210,22 @@ class MapboxRenderThreadTest { @Test fun setMaximumFpsTest() { + initRenderThread() mapboxRenderThread.setMaximumFps(30) assert(mapboxRenderThread.renderTimeNs == 33333333L) } @Test fun pauseTest() { - val latch = CountDownLatch(1) - mockValidSurface() - Shadows.shadowOf(renderHandlerThread.handler?.looper).pause() + initRenderThread() + provideValidSurface() + pauseHandler() mapboxRenderThread.queueRenderEvent(MapboxRenderer.renderEventSdk) - Shadows.shadowOf(renderHandlerThread.handler?.looper).idle() + idleHandler() mapboxRenderThread.pause() - Shadows.shadowOf(renderHandlerThread.handler?.looper).idle() + idleHandler() mapboxRenderThread.queueRenderEvent(MapboxRenderer.renderEventSdk) - Shadows.shadowOf(renderHandlerThread.handler?.looper).idle() - latch.await(waitTime, TimeUnit.MILLISECONDS) + idleHandler() // one swap buffer for surface creation, one request render after pause is omitted verify(exactly = 2) { eglCore.swapBuffers(any()) @@ -238,22 +234,21 @@ class MapboxRenderThreadTest { @Test fun resumeTestWithRequestRenderAtPause() { - val latch = CountDownLatch(1) - mockValidSurface() - Shadows.shadowOf(renderHandlerThread.handler?.looper).pause() + initRenderThread() + provideValidSurface() + pauseHandler() mapboxRenderThread.queueRenderEvent(MapboxRenderer.renderEventSdk) - Shadows.shadowOf(renderHandlerThread.handler?.looper).idle() + idleHandler() mapboxRenderThread.pause() - Shadows.shadowOf(renderHandlerThread.handler?.looper).idle() + idleHandler() mapboxRenderThread.queueRenderEvent(MapboxRenderer.renderEventSdk) - Shadows.shadowOf(renderHandlerThread.handler?.looper).idle() + idleHandler() mapboxRenderThread.queueRenderEvent(MapboxRenderer.renderEventSdk) - Shadows.shadowOf(renderHandlerThread.handler?.looper).idle() + idleHandler() mapboxRenderThread.resume() - Shadows.shadowOf(renderHandlerThread.handler?.looper).idle() + idleHandler() mapboxRenderThread.queueRenderEvent(MapboxRenderer.renderEventSdk) - Shadows.shadowOf(renderHandlerThread.handler?.looper).idle() - latch.await(waitTime, TimeUnit.MILLISECONDS) + idleHandler() // render requests after pause do not swap buffer, we do it on resume if needed once verify(exactly = 4) { eglCore.swapBuffers(any()) @@ -262,18 +257,17 @@ class MapboxRenderThreadTest { @Test fun resumeTestWithoutRequestRenderAtPause() { - val latch = CountDownLatch(1) - mockValidSurface() - Shadows.shadowOf(renderHandlerThread.handler?.looper).pause() + initRenderThread() + provideValidSurface() + pauseHandler() mapboxRenderThread.queueRenderEvent(MapboxRenderer.renderEventSdk) - Shadows.shadowOf(renderHandlerThread.handler?.looper).idle() + idleHandler() mapboxRenderThread.pause() - Shadows.shadowOf(renderHandlerThread.handler?.looper).idle() + idleHandler() mapboxRenderThread.resume() - Shadows.shadowOf(renderHandlerThread.handler?.looper).idle() + idleHandler() mapboxRenderThread.queueRenderEvent(MapboxRenderer.renderEventSdk) - Shadows.shadowOf(renderHandlerThread.handler?.looper).idle() - latch.await(waitTime, TimeUnit.MILLISECONDS) + idleHandler() // we always do extra render call on resume verify(exactly = 4) { eglCore.swapBuffers(any()) @@ -282,16 +276,17 @@ class MapboxRenderThreadTest { @Test fun destroyTest() { + initRenderThread() mapboxRenderThread.destroy() - assert(!renderHandlerThread.started) + assertFalse(renderHandlerThread.started) } @Test fun queueRenderEventTest() { - val latch = CountDownLatch(1) - mockValidSurface() - val runnable = mockCountdownRunnable(latch) - Shadows.shadowOf(renderHandlerThread.handler?.looper).pause() + initRenderThread() + provideValidSurface() + val runnable = mockk(relaxUnitFun = true) + pauseHandler() mapboxRenderThread.queueRenderEvent( RenderEvent( runnable, @@ -300,12 +295,9 @@ class MapboxRenderThreadTest { ) ) assertEquals(1, mapboxRenderThread.renderEventQueue.size) - Shadows.shadowOf(renderHandlerThread.handler?.looper).idle() - if (!latch.await(waitTime, TimeUnit.MILLISECONDS)) { - throw TimeoutException() - } + idleHandler() assert(mapboxRenderThread.renderEventQueue.isEmpty()) - verify { runnable.run() } + verifyOnce { runnable.run() } // one swap buffer from surface creation, one for custom event verify(exactly = 2) { eglCore.swapBuffers(any()) @@ -314,10 +306,11 @@ class MapboxRenderThreadTest { @Test fun queueSdkNonRenderEventTestNoVsync() { - mockValidSurface() + initRenderThread() + provideValidSurface() val runnable = mockk(relaxUnitFun = true) mapboxRenderThread.awaitingNextVsync = false - Shadows.shadowOf(renderHandlerThread.handler?.looper).pause() + pauseHandler() mapboxRenderThread.queueRenderEvent( RenderEvent( runnable, @@ -328,20 +321,21 @@ class MapboxRenderThreadTest { // we do not add non-render event to the queue assert(mapboxRenderThread.nonRenderEventQueue.isEmpty()) // do not schedule any render requests explicitly - Shadows.shadowOf(renderHandlerThread.handler?.looper).idle() - verify { runnable.run() } + idleHandler() + verifyOnce { runnable.run() } // one swap buffer from surface creation only - verify(exactly = 1) { + verifyOnce { eglCore.swapBuffers(any()) } } @Test fun queueSdkNonRenderEventTestWithVsync() { - mockValidSurface() + initRenderThread() + provideValidSurface() val runnable = mockk(relaxUnitFun = true) mapboxRenderThread.awaitingNextVsync = true - Shadows.shadowOf(renderHandlerThread.handler?.looper).pause() + pauseHandler() mapboxRenderThread.queueRenderEvent( RenderEvent( runnable, @@ -352,27 +346,28 @@ class MapboxRenderThreadTest { // we add to the queue assert(mapboxRenderThread.nonRenderEventQueue.size == 1) // do not schedule any render requests explicitly - Shadows.shadowOf(renderHandlerThread.handler?.looper).idle() + idleHandler() // without explicit render event runnable should not be executed - verify(exactly = 0) { runnable.run() } + verifyNo { runnable.run() } mapboxRenderThread.awaitingNextVsync = false - Shadows.shadowOf(renderHandlerThread.handler?.looper).pause() + pauseHandler() // schedule render request mapboxRenderThread.queueRenderEvent(MapboxRenderer.renderEventSdk) - Shadows.shadowOf(renderHandlerThread.handler?.looper).idle() + idleHandler() assert(mapboxRenderThread.nonRenderEventQueue.isEmpty()) // one swap buffer from surface creation + one for render request verify(exactly = 2) { eglCore.swapBuffers(any()) } - verify(exactly = 1) { runnable.run() } + verifyOnce { runnable.run() } } @Test fun queueUserNonRenderEventLoosingSurfaceTest() { - val surface = mockValidSurface() + initRenderThread() + provideValidSurface() val runnable = mockk(relaxUnitFun = true) - Shadows.shadowOf(renderHandlerThread.handler?.looper).pause() + pauseHandler() mapboxRenderThread.queueRenderEvent( RenderEvent( runnable, @@ -382,23 +377,24 @@ class MapboxRenderThreadTest { ) // simulate render thread is not fully prepared, e.g. EGL context is lost mapboxRenderThread.eglContextCreated = false - Shadows.shadowOf(renderHandlerThread.handler?.looper).idle() - verify(exactly = 0) { runnable.run() } - Shadows.shadowOf(renderHandlerThread.handler?.looper).pause() + idleHandler() + verifyNo { runnable.run() } + pauseHandler() // simulate render thread is fully prepared again mapboxRenderThread.eglContextCreated = true mapboxRenderThread.processAndroidSurface(surface, 1, 1) // taking into account we try to reschedule event with some delay Shadows.shadowOf(renderHandlerThread.handler?.looper).idleFor(RETRY_DELAY_MS, TimeUnit.MILLISECONDS) // user's runnable is executed when thread is fully prepared again - verify(exactly = 1) { runnable.run() } + verifyOnce { runnable.run() } } @Test fun queueSdkNonRenderEventLoosingSurfaceTest() { - val surface = mockValidSurface() + initRenderThread() + provideValidSurface() val runnable = mockk(relaxUnitFun = true) - Shadows.shadowOf(renderHandlerThread.handler?.looper).pause() + pauseHandler() mapboxRenderThread.queueRenderEvent( RenderEvent( runnable, @@ -408,34 +404,30 @@ class MapboxRenderThreadTest { ) // simulate render thread is not fully prepared, e.g. EGL context is lost mapboxRenderThread.eglContextCreated = false - Shadows.shadowOf(renderHandlerThread.handler?.looper).idle() - verify(exactly = 0) { runnable.run() } - Shadows.shadowOf(renderHandlerThread.handler?.looper).pause() + idleHandler() + verifyNo { runnable.run() } + pauseHandler() // simulate render thread is fully prepared again mapboxRenderThread.eglContextCreated = true mapboxRenderThread.processAndroidSurface(surface, 1, 1) // taking into account we try to reschedule event with some delay Shadows.shadowOf(renderHandlerThread.handler?.looper).idleFor(RETRY_DELAY_MS, TimeUnit.MILLISECONDS) // SDK's task is not executed with new surface - verify(exactly = 0) { runnable.run() } + verifyNo { runnable.run() } } @Test fun fpsListenerTest() { - val latch = CountDownLatch(2) + initRenderThread() val listener = mockk(relaxUnitFun = true) - every { listener.onFpsChanged(any()) } answers { latch.countDown() } mapboxRenderThread.fpsChangedListener = listener - mockValidSurface() - Shadows.shadowOf(renderHandlerThread.handler?.looper).pause() + provideValidSurface() + pauseHandler() mapboxRenderThread.queueRenderEvent(MapboxRenderer.renderEventSdk) mapboxRenderThread.queueRenderEvent(MapboxRenderer.renderEventSdk) - Shadows.shadowOf(renderHandlerThread.handler?.looper).idle() + idleHandler() mapboxRenderThread.queueRenderEvent(MapboxRenderer.renderEventSdk) - Shadows.shadowOf(renderHandlerThread.handler?.looper).idle() - if (!latch.await(waitTime, TimeUnit.MILLISECONDS)) { - throw TimeoutException() - } + idleHandler() verify(exactly = 2) { listener.onFpsChanged(any()) } @@ -443,37 +435,36 @@ class MapboxRenderThreadTest { @Test fun surfaceCreatedCalledBeforeActivityStartTest() { - val latch = CountDownLatch(1) - every { mapboxRenderer.createRenderer() } answers { latch.countDown() } + initRenderThread() mapboxRenderThread.paused = true - mockValidSurface() - if (!latch.await(waitTime, TimeUnit.MILLISECONDS)) { - throw TimeoutException() - } - verify(exactly = 1) { mapboxRenderer.createRenderer() } + provideValidSurface() + verifyOnce { mapboxRenderer.createRenderer() } // EGL should be fully prepared - verify(exactly = 0) { eglCore.releaseSurface(any()) } + verifyNo { eglCore.releaseSurface(any()) } } @Test fun snapshotsAreTakenAfterDrawAndBeforeSwapBuffers() { - val latch = CountDownLatch(3) + initRenderThread() + provideValidSurface() - mockValidSurface() + lateinit var runnable: Runnable + lateinit var runnable2: Runnable + lateinit var runnable3: Runnable + waitZeroCounter(startCounter = 3) { + runnable = mockCountdownRunnable(this) + runnable2 = mockCountdownRunnable(this) + runnable3 = mockCountdownRunnable(this) - val runnable = mockCountdownRunnable(latch) - val runnable2 = mockCountdownRunnable(latch) - val runnable3 = mockCountdownRunnable(latch) - Shadows.shadowOf(renderHandlerThread.handler?.looper).pause() + pauseHandler() - mapboxRenderThread.queueRenderEvent(RenderEvent(runnable, true, EventType.SDK)) - mapboxRenderThread.queueRenderEvent(RenderEvent(runnable2, true, EventType.SDK)) - mapboxRenderThread.queueRenderEvent(RenderEvent(runnable3, true, EventType.SDK)) + mapboxRenderThread.queueRenderEvent(RenderEvent(runnable, true, EventType.SDK)) + mapboxRenderThread.queueRenderEvent(RenderEvent(runnable2, true, EventType.SDK)) + mapboxRenderThread.queueRenderEvent(RenderEvent(runnable3, true, EventType.SDK)) - Shadows.shadowOf(renderHandlerThread.handler?.looper).idle() - if (!latch.await(waitTime, TimeUnit.MILLISECONDS)) { - throw TimeoutException() + idleHandler() } + verifyOrder { mapboxRenderer.render() runnable.run() @@ -485,61 +476,42 @@ class MapboxRenderThreadTest { @Test fun onSurfaceDestroyedWithRenderCallAfterTestSurfaceView() { - mapboxRenderer = mockk(relaxUnitFun = true) - mapboxRenderThread = MapboxRenderThread( - mapboxRenderer, - mapboxWidgetRenderer, - renderHandlerThread, - eglCore, - textureRenderer, - ).apply { - renderHandlerThread.start() - } - mockValidSurface() + initRenderThread(mockk(relaxUnitFun = true)) + provideValidSurface() mapboxRenderThread.onSurfaceDestroyed() - Shadows.shadowOf(renderHandlerThread.handler?.looper).idle() + idleHandler() mapboxRenderThread.queueRenderEvent(MapboxRenderer.renderEventSdk) - Shadows.shadowOf(renderHandlerThread.handler?.looper).idle() + idleHandler() // we do not destroy native renderer if it's stop and not destroy - verify(exactly = 0) { mapboxRenderer.destroyRenderer() } + verifyNo { mapboxRenderer.destroyRenderer() } // we clear only EGLSurface but not all EGL - verify(exactly = 1) { eglCore.releaseSurface(any()) } - verify(exactly = 0) { eglCore.release() } + verifyOnce { eglCore.releaseSurface(any()) } + verifyNo { eglCore.release() } } @Test fun onSurfaceDestroyedWithRenderCallAfterTestTextureView() { - mapboxRenderer = mockk(relaxUnitFun = true) - mapboxRenderThread = MapboxRenderThread( - mapboxRenderer, - mapboxWidgetRenderer, - renderHandlerThread, - eglCore, - textureRenderer, - ).apply { - renderHandlerThread.start() - } - val latch = CountDownLatch(1) - every { mapboxRenderer.destroyRenderer() } answers { latch.countDown() } - mockValidSurface() + initRenderThread(mockk(relaxUnitFun = true)) + provideValidSurface() mapboxRenderThread.onSurfaceDestroyed() - Shadows.shadowOf(renderHandlerThread.handler?.looper).idle() + idleHandler() mapboxRenderThread.queueRenderEvent(MapboxRenderer.renderEventSdk) - Shadows.shadowOf(renderHandlerThread.handler?.looper).idle() + idleHandler() // we do destroy native renderer if it's stop (for texture renderer) - verify(exactly = 1) { mapboxRenderer.destroyRenderer() } + verifyOnce { mapboxRenderer.destroyRenderer() } // we clear all EGL - verify(exactly = 1) { eglCore.releaseSurface(any()) } - verify(exactly = 1) { eglCore.release() } + verifyOnce { eglCore.releaseSurface(any()) } + verifyOnce { eglCore.release() } } @Test fun renderWithMaxFpsSet() { - mockValidSurface() + initRenderThread() + provideValidSurface() mapboxRenderThread.setMaximumFps(15) - Shadows.shadowOf(renderHandlerThread.handler?.looper).pause() + pauseHandler() mapboxRenderThread.queueRenderEvent(MapboxRenderer.renderEventSdk) - Shadows.shadowOf(renderHandlerThread.handler?.looper).idle() + idleHandler() // 1 swap when creating surface + 1 for request render call verify(exactly = 2) { eglCore.swapBuffers(any()) @@ -548,14 +520,15 @@ class MapboxRenderThreadTest { @Test fun onDrawDoesNotRenderWidgets() { - mockValidSurface() - every { mapboxWidgetRenderer.needRender } returns false + initRenderThread() + provideValidSurface() + every { mapboxWidgetRenderer.needTextureUpdate } returns false every { mapboxWidgetRenderer.getTexture() } returns 0 - Shadows.shadowOf(renderHandlerThread.handler?.looper).pause() + pauseHandler() mapboxRenderThread.queueRenderEvent(MapboxRenderer.renderEventSdk) + idleHandler() mapboxRenderThread.queueRenderEvent(MapboxRenderer.renderEventSdk) - Shadows.shadowOf(renderHandlerThread.handler?.looper).idle() - + idleHandler() verifyNo { mapboxWidgetRenderer.updateTexture() } @@ -566,16 +539,18 @@ class MapboxRenderThreadTest { @Test fun onDrawRendersWidgets() { - mockValidSurface() + initRenderThread() + provideValidSurface() val textureId = 1 - every { mapboxWidgetRenderer.needRender } returns true + every { mapboxWidgetRenderer.needTextureUpdate } returns true + every { mapboxWidgetRenderer.hasWidgets() } returns true + every { mapboxWidgetRenderer.hasTexture() } returns true every { mapboxWidgetRenderer.getTexture() } returns textureId - Shadows.shadowOf(renderHandlerThread.handler?.looper).pause() + pauseHandler() mapboxRenderThread.queueRenderEvent(MapboxRenderer.renderEventSdk) - Shadows.shadowOf(renderHandlerThread.handler?.looper).idle() + idleHandler() mapboxRenderThread.queueRenderEvent(MapboxRenderer.renderEventSdk) - Shadows.shadowOf(renderHandlerThread.handler?.looper).idle() - + idleHandler() verify(exactly = 2) { mapboxWidgetRenderer.updateTexture() } @@ -585,67 +560,64 @@ class MapboxRenderThreadTest { } @Test - fun onSurfaceCreatedInitsWidgetRender() { - val latch = CountDownLatch(1) - every { mapboxRenderer.createRenderer() } answers { latch.countDown() } - mockValidSurface() - if (!latch.await(waitTime, TimeUnit.MILLISECONDS)) { - throw TimeoutException() - } + fun onSurfaceCreatedWidgetsInitWidgetRender() { + initRenderThread() + every { mapboxWidgetRenderer.hasWidgets() } returns true + provideValidSurface() verifyOnce { + mapboxWidgetRenderer.onSharedContext(any()) + } + } + + @Test + fun onSurfaceCreatedNoWidgetsNotInitWidgetRender() { + initRenderThread() + every { mapboxWidgetRenderer.hasWidgets() } returns false + provideValidSurface() + verifyNo { mapboxWidgetRenderer.onSharedContext(eglCore.eglContext) } } @Test - fun onEglCoreFailDoesntInitWidgetRender() { - val surface = mockk() - mapboxRenderThread.onSurfaceCreated(surface, 1, 1) - val latch = CountDownLatch(1) - latch.await(waitTime, TimeUnit.MILLISECONDS) + fun onEglCorePrepareFailNotInitWidgetRender() { + initRenderThread() + every { eglCore.prepareEgl() } returns false + provideValidSurface() verifyNo { mapboxWidgetRenderer.onSharedContext(any()) } } @Test - fun onInvalidSurfaceDoesntInitWidgetRender() { + fun onInvalidSurfaceNotInitWidgetRender() { + initRenderThread() val surface = mockk() every { surface.isValid } returns false mapboxRenderThread.onSurfaceCreated(surface, 1, 1) - val latch = CountDownLatch(1) - latch.await(waitTime, TimeUnit.MILLISECONDS) + idleHandler() verifyNo { mapboxWidgetRenderer.onSharedContext(any()) } } @Test - fun onInvalidEglWindowSurfaceDoesntInitWidgetRender() { - val latch = CountDownLatch(1) - val surface = mockk() - val noSurface = mockk() - - every { surface.isValid } returns true - every { eglCore.createWindowSurface(any()) } returns noSurface - every { eglCore.eglNoSurface } returns noSurface - - mapboxRenderThread = MapboxRenderThread( - mapboxRenderer, - mapboxWidgetRenderer, - renderHandlerThread, - eglCore, - textureRenderer, - ).apply { - renderHandlerThread.start() + fun onInvalidEglSurfaceNotInitWidgetRender() { + initRenderThread() + every { eglCore.createWindowSurface(any()) } returns eglCore.eglNoSurface + provideValidSurface() + verifyNo { + mapboxWidgetRenderer.onSharedContext(any()) } + } - mapboxRenderThread.onSurfaceCreated(surface, 1, 1) - - latch.await(waitTime, TimeUnit.MILLISECONDS) - + @Test + fun onMakeCurrentErrorNotInitWidgetRender() { + initRenderThread() + every { eglCore.makeCurrent(any()) } returns false + provideValidSurface() verifyNo { mapboxWidgetRenderer.onSharedContext(any()) } } -} +} \ No newline at end of file From c156d312ec1f5353b71ef3b7f1688c2018e046e8 Mon Sep 17 00:00:00 2001 From: yunik Date: Wed, 9 Feb 2022 18:39:12 +0200 Subject: [PATCH 07/13] More review fixes. --- sdk/src/main/java/com/mapbox/maps/MapSurface.kt | 2 +- sdk/src/main/java/com/mapbox/maps/MapView.kt | 2 +- .../main/java/com/mapbox/maps/renderer/MapboxRenderThread.kt | 3 +-- .../main/java/com/mapbox/maps/renderer/gl/TextureRenderer.kt | 4 ++-- 4 files changed, 5 insertions(+), 6 deletions(-) diff --git a/sdk/src/main/java/com/mapbox/maps/MapSurface.kt b/sdk/src/main/java/com/mapbox/maps/MapSurface.kt index ef8b5cbebd..c304ea0517 100644 --- a/sdk/src/main/java/com/mapbox/maps/MapSurface.kt +++ b/sdk/src/main/java/com/mapbox/maps/MapSurface.kt @@ -190,7 +190,7 @@ class MapSurface @JvmOverloads constructor( /** * Remove [Widget] from the map. * - * @return true if widget was removed + * @return true if widget was present and removed, false otherwise */ @MapboxExperimental override fun removeWidget(widget: Widget) = mapController.removeWidget(widget) diff --git a/sdk/src/main/java/com/mapbox/maps/MapView.kt b/sdk/src/main/java/com/mapbox/maps/MapView.kt index 9afb00a99d..5e29885606 100644 --- a/sdk/src/main/java/com/mapbox/maps/MapView.kt +++ b/sdk/src/main/java/com/mapbox/maps/MapView.kt @@ -321,7 +321,7 @@ open class MapView : FrameLayout, MapPluginProviderDelegate, MapControllable { /** * Remove [Widget] from the map. * - * @return true if widget was removed + * @return true if widget was present and removed, false otherwise */ @MapboxExperimental override fun removeWidget(widget: Widget) = mapController.removeWidget(widget) diff --git a/sdk/src/main/java/com/mapbox/maps/renderer/MapboxRenderThread.kt b/sdk/src/main/java/com/mapbox/maps/renderer/MapboxRenderThread.kt index 1bf54778fd..479cd722d3 100644 --- a/sdk/src/main/java/com/mapbox/maps/renderer/MapboxRenderThread.kt +++ b/sdk/src/main/java/com/mapbox/maps/renderer/MapboxRenderThread.kt @@ -154,7 +154,6 @@ internal class MapboxRenderThread : Choreographer.FrameCallback { ) renderCreated = true } - return true } } @@ -240,7 +239,7 @@ internal class MapboxRenderThread : Choreographer.FrameCallback { } if (widgetRenderer.hasWidgets()) { - if (widgetRenderer.needRender) { + if (widgetRenderer.needTextureUpdate) { widgetRenderer.updateTexture() eglCore.makeCurrent(eglSurface) } diff --git a/sdk/src/main/java/com/mapbox/maps/renderer/gl/TextureRenderer.kt b/sdk/src/main/java/com/mapbox/maps/renderer/gl/TextureRenderer.kt index f0de88c0db..bb7e49a37c 100644 --- a/sdk/src/main/java/com/mapbox/maps/renderer/gl/TextureRenderer.kt +++ b/sdk/src/main/java/com/mapbox/maps/renderer/gl/TextureRenderer.kt @@ -110,14 +110,14 @@ internal class TextureRenderer( GLES20.glBindBuffer(GLES20.GL_ARRAY_BUFFER, vbo[0]) GLES20.glBufferData( GLES20.GL_ARRAY_BUFFER, - vertexArray.size * 4, + vertexArray.size * BYTES_PER_FLOAT, vertexArray.toFloatBuffer(), GLES20.GL_STATIC_DRAW ) GLES20.glBindBuffer(GLES20.GL_ARRAY_BUFFER, vbo[1]) GLES20.glBufferData( GLES20.GL_ARRAY_BUFFER, - textureArray.size * 4, + textureArray.size * BYTES_PER_FLOAT, textureArray.toFloatBuffer(), GLES20.GL_STATIC_DRAW ) From 341958eb8c3120c05b4f2ab20609ba3b49c82f45 Mon Sep 17 00:00:00 2001 From: yunik Date: Wed, 9 Feb 2022 19:31:20 +0200 Subject: [PATCH 08/13] Update tests. --- .../maps/renderer/MapboxRenderThreadTest.kt | 80 ++++++++++++++++--- 1 file changed, 67 insertions(+), 13 deletions(-) diff --git a/sdk/src/test/java/com/mapbox/maps/renderer/MapboxRenderThreadTest.kt b/sdk/src/test/java/com/mapbox/maps/renderer/MapboxRenderThreadTest.kt index 8f3c9b7e3c..a934c7ba8f 100644 --- a/sdk/src/test/java/com/mapbox/maps/renderer/MapboxRenderThreadTest.kt +++ b/sdk/src/test/java/com/mapbox/maps/renderer/MapboxRenderThreadTest.kt @@ -119,6 +119,40 @@ class MapboxRenderThreadTest { } } + @Test + fun onInvalidSurfaceNotInitNativeRenderer() { + initRenderThread() + val surface = mockk() + every { surface.isValid } returns false + mapboxRenderThread.onSurfaceCreated(surface, 1, 1) + idleHandler() + verifyNo { + mapboxRenderer.createRenderer() + mapboxRenderer.onSurfaceChanged(1, 1) + } + } + + @Test + fun onEglCorePrepareFailNotInitNativeRenderer() { + initRenderThread() + every { eglCore.prepareEgl() } returns false + provideValidSurface() + verifyNo { + mapboxRenderer.createRenderer() + mapboxRenderer.onSurfaceChanged(1, 1) + } + } + + @Test + fun onMakeCurrentErrorNotInitNativeRenderer() { + initRenderThread() + every { eglCore.makeCurrent(any()) } returns false + provideValidSurface() + verifyNo { + mapboxRenderer.createRenderer() + mapboxRenderer.onSurfaceChanged(1, 1) + } + } @Test fun onSurfaceSizeChangedTest() { @@ -482,11 +516,13 @@ class MapboxRenderThreadTest { idleHandler() mapboxRenderThread.queueRenderEvent(MapboxRenderer.renderEventSdk) idleHandler() - // we do not destroy native renderer if it's stop and not destroy - verifyNo { mapboxRenderer.destroyRenderer() } + verifyNo { + // we do not destroy native renderer if it's stop and not destroy + mapboxRenderer.destroyRenderer() + eglCore.release() + } // we clear only EGLSurface but not all EGL verifyOnce { eglCore.releaseSurface(any()) } - verifyNo { eglCore.release() } } @Test @@ -497,11 +533,14 @@ class MapboxRenderThreadTest { idleHandler() mapboxRenderThread.queueRenderEvent(MapboxRenderer.renderEventSdk) idleHandler() - // we do destroy native renderer if it's stop (for texture renderer) - verifyOnce { mapboxRenderer.destroyRenderer() } - // we clear all EGL - verifyOnce { eglCore.releaseSurface(any()) } - verifyOnce { eglCore.release() } + + verifyOnce { + // we do destroy native renderer if it's stop (for texture renderer) + mapboxRenderer.destroyRenderer() + // we clear all EGL + eglCore.releaseSurface(any()) + eglCore.release() + } } @Test @@ -531,8 +570,6 @@ class MapboxRenderThreadTest { idleHandler() verifyNo { mapboxWidgetRenderer.updateTexture() - } - verifyNo { textureRenderer.render(any()) } } @@ -553,8 +590,25 @@ class MapboxRenderThreadTest { idleHandler() verify(exactly = 2) { mapboxWidgetRenderer.updateTexture() + textureRenderer.render(textureId) } - verify(exactly = 2) { + } + + @Test + fun onDrawRendersWidgetsBeforeMap() { + initRenderThread() + provideValidSurface() + val textureId = 1 + every { mapboxWidgetRenderer.needTextureUpdate } returns true + every { mapboxWidgetRenderer.hasWidgets() } returns true + every { mapboxWidgetRenderer.hasTexture() } returns true + every { mapboxWidgetRenderer.getTexture() } returns textureId + pauseHandler() + mapboxRenderThread.queueRenderEvent(MapboxRenderer.renderEventSdk) + idleHandler() + verifyOrder { + mapboxWidgetRenderer.updateTexture() + mapboxRenderer.render() textureRenderer.render(textureId) } } @@ -565,7 +619,7 @@ class MapboxRenderThreadTest { every { mapboxWidgetRenderer.hasWidgets() } returns true provideValidSurface() verifyOnce { - mapboxWidgetRenderer.onSharedContext(any()) + mapboxWidgetRenderer.onSharedContext(eglCore.eglContext) } } @@ -575,7 +629,7 @@ class MapboxRenderThreadTest { every { mapboxWidgetRenderer.hasWidgets() } returns false provideValidSurface() verifyNo { - mapboxWidgetRenderer.onSharedContext(eglCore.eglContext) + mapboxWidgetRenderer.onSharedContext(any()) } } From 30fc8283d62be08a0c5fdd3c28781b5ef2062e62 Mon Sep 17 00:00:00 2001 From: yunik Date: Thu, 10 Feb 2022 15:11:13 +0200 Subject: [PATCH 09/13] ktlint fixes. --- .../maps/testapp/auto/car/MapSession.kt | 2 -- .../java/com/mapbox/maps/MapControllable.kt | 2 +- sdk/src/main/java/com/mapbox/maps/MapView.kt | 2 +- .../maps/renderer/MapboxWidgetRenderer.kt | 3 +- .../com/mapbox/maps/renderer/gl/GlUtils.kt | 18 ++++++------ .../maps/renderer/gl/TextureRenderer.kt | 2 +- .../maps/renderer/widget/BitmapWidget.kt | 4 +-- .../renderer/widget/BitmapWidgetRenderer.kt | 2 +- .../maps/renderer/widget/WidgetPosition.kt | 2 +- .../maps/renderer/widget/WidgetRenderer.kt | 2 +- sdk/src/test/java/com/mapbox/TestUtils.kt | 2 +- .../maps/renderer/MapboxRenderThreadTest.kt | 28 +++++++++---------- 12 files changed, 34 insertions(+), 35 deletions(-) diff --git a/android-auto-app/src/main/java/com/mapbox/maps/testapp/auto/car/MapSession.kt b/android-auto-app/src/main/java/com/mapbox/maps/testapp/auto/car/MapSession.kt index 2e71aa9dc5..a24fa5b0cf 100644 --- a/android-auto-app/src/main/java/com/mapbox/maps/testapp/auto/car/MapSession.kt +++ b/android-auto-app/src/main/java/com/mapbox/maps/testapp/auto/car/MapSession.kt @@ -4,8 +4,6 @@ import android.Manifest.permission.ACCESS_FINE_LOCATION import android.content.Intent import android.content.pm.PackageManager.PERMISSION_GRANTED import android.content.res.Configuration -import android.os.Handler -import android.os.Looper import androidx.car.app.* import com.mapbox.maps.EdgeInsets import com.mapbox.maps.MapSurface diff --git a/sdk/src/main/java/com/mapbox/maps/MapControllable.kt b/sdk/src/main/java/com/mapbox/maps/MapControllable.kt index 0c73269424..d73e7d1426 100644 --- a/sdk/src/main/java/com/mapbox/maps/MapControllable.kt +++ b/sdk/src/main/java/com/mapbox/maps/MapControllable.kt @@ -87,5 +87,5 @@ interface MapControllable : MapboxLifecycleObserver { * @return true if widget was removed */ @MapboxExperimental - fun removeWidget(widget: Widget) : Boolean + fun removeWidget(widget: Widget): Boolean } \ No newline at end of file diff --git a/sdk/src/main/java/com/mapbox/maps/MapView.kt b/sdk/src/main/java/com/mapbox/maps/MapView.kt index 5e29885606..29be22cde8 100644 --- a/sdk/src/main/java/com/mapbox/maps/MapView.kt +++ b/sdk/src/main/java/com/mapbox/maps/MapView.kt @@ -19,8 +19,8 @@ import com.mapbox.maps.plugin.delegates.MapPluginProviderDelegate import com.mapbox.maps.renderer.MapboxSurfaceHolderRenderer import com.mapbox.maps.renderer.MapboxTextureViewRenderer import com.mapbox.maps.renderer.OnFpsChangedListener -import com.mapbox.maps.renderer.widget.Widget import com.mapbox.maps.renderer.egl.EGLCore +import com.mapbox.maps.renderer.widget.Widget import com.mapbox.maps.viewannotation.ViewAnnotationManager /** diff --git a/sdk/src/main/java/com/mapbox/maps/renderer/MapboxWidgetRenderer.kt b/sdk/src/main/java/com/mapbox/maps/renderer/MapboxWidgetRenderer.kt index b3c6952499..7371c9e241 100644 --- a/sdk/src/main/java/com/mapbox/maps/renderer/MapboxWidgetRenderer.kt +++ b/sdk/src/main/java/com/mapbox/maps/renderer/MapboxWidgetRenderer.kt @@ -4,7 +4,6 @@ import android.opengl.GLES20 import com.mapbox.common.Logger import com.mapbox.maps.renderer.egl.EGLCore import com.mapbox.maps.renderer.widget.Widget -import java.util.concurrent.CopyOnWriteArraySet import javax.microedition.khronos.egl.EGLContext import javax.microedition.khronos.egl.EGLSurface @@ -200,4 +199,4 @@ internal class MapboxWidgetRenderer( private companion object { const val TAG: String = "MapboxWidgetRenderer" } -} +} \ No newline at end of file diff --git a/sdk/src/main/java/com/mapbox/maps/renderer/gl/GlUtils.kt b/sdk/src/main/java/com/mapbox/maps/renderer/gl/GlUtils.kt index 7ed9b47074..7e83629282 100644 --- a/sdk/src/main/java/com/mapbox/maps/renderer/gl/GlUtils.kt +++ b/sdk/src/main/java/com/mapbox/maps/renderer/gl/GlUtils.kt @@ -43,14 +43,16 @@ internal object GlUtils { if (BuildConfig.DEBUG) { when (val error = GLES20.glGetError()) { GLES20.GL_NO_ERROR -> {} - else -> throw java.lang.RuntimeException("$cmd - error in GL : ${when (error) { - GLES20.GL_INVALID_ENUM -> "GL_INVALID_ENUM" - GLES20.GL_INVALID_VALUE -> "GL_INVALID_VALUE" - GLES20.GL_INVALID_OPERATION -> "GL_INVALID_OPERATION" - GLES20.GL_INVALID_FRAMEBUFFER_OPERATION -> "GL_INVALID_FRAMEBUFFER_OPERATION" - GLES20.GL_OUT_OF_MEMORY -> "GL_OUT_OF_MEMORY" - else -> error - }}") + else -> throw java.lang.RuntimeException( + "$cmd - error in GL : ${when (error) { + GLES20.GL_INVALID_ENUM -> "GL_INVALID_ENUM" + GLES20.GL_INVALID_VALUE -> "GL_INVALID_VALUE" + GLES20.GL_INVALID_OPERATION -> "GL_INVALID_OPERATION" + GLES20.GL_INVALID_FRAMEBUFFER_OPERATION -> "GL_INVALID_FRAMEBUFFER_OPERATION" + GLES20.GL_OUT_OF_MEMORY -> "GL_OUT_OF_MEMORY" + else -> error + }}" + ) } } } diff --git a/sdk/src/main/java/com/mapbox/maps/renderer/gl/TextureRenderer.kt b/sdk/src/main/java/com/mapbox/maps/renderer/gl/TextureRenderer.kt index bb7e49a37c..2d44bf2511 100644 --- a/sdk/src/main/java/com/mapbox/maps/renderer/gl/TextureRenderer.kt +++ b/sdk/src/main/java/com/mapbox/maps/renderer/gl/TextureRenderer.kt @@ -154,4 +154,4 @@ internal class TextureRenderer( } """.trimIndent() } -} +} \ No newline at end of file diff --git a/sdk/src/main/java/com/mapbox/maps/renderer/widget/BitmapWidget.kt b/sdk/src/main/java/com/mapbox/maps/renderer/widget/BitmapWidget.kt index 8ab694678c..ac24d4f65b 100644 --- a/sdk/src/main/java/com/mapbox/maps/renderer/widget/BitmapWidget.kt +++ b/sdk/src/main/java/com/mapbox/maps/renderer/widget/BitmapWidget.kt @@ -21,7 +21,7 @@ open class BitmapWidget @JvmOverloads constructor( marginX: Float = 0f, marginY: Float = 0f, ) : Widget() { - override val renderer = BitmapWidgetRendererImpl( + override val renderer = BitmapWidgetRenderer( bitmap = bitmap, position = position, marginX = marginX, @@ -42,4 +42,4 @@ open class BitmapWidget @JvmOverloads constructor( override fun setRotation(angleDegrees: Float) { renderer.setRotation(angleDegrees = angleDegrees) } -} +} \ No newline at end of file diff --git a/sdk/src/main/java/com/mapbox/maps/renderer/widget/BitmapWidgetRenderer.kt b/sdk/src/main/java/com/mapbox/maps/renderer/widget/BitmapWidgetRenderer.kt index ec12a53c2a..9d647b8695 100644 --- a/sdk/src/main/java/com/mapbox/maps/renderer/widget/BitmapWidgetRenderer.kt +++ b/sdk/src/main/java/com/mapbox/maps/renderer/widget/BitmapWidgetRenderer.kt @@ -8,7 +8,7 @@ import com.mapbox.maps.renderer.gl.GlUtils import com.mapbox.maps.renderer.gl.GlUtils.put import com.mapbox.maps.renderer.gl.GlUtils.toFloatBuffer -internal class BitmapWidgetRendererImpl( +internal class BitmapWidgetRenderer( private var bitmap: Bitmap?, private val position: WidgetPosition, private val marginX: Float, diff --git a/sdk/src/main/java/com/mapbox/maps/renderer/widget/WidgetPosition.kt b/sdk/src/main/java/com/mapbox/maps/renderer/widget/WidgetPosition.kt index 528b7ca930..127d01cb6d 100644 --- a/sdk/src/main/java/com/mapbox/maps/renderer/widget/WidgetPosition.kt +++ b/sdk/src/main/java/com/mapbox/maps/renderer/widget/WidgetPosition.kt @@ -21,4 +21,4 @@ class WidgetPosition( CENTER, BOTTOM, } -} +} \ No newline at end of file diff --git a/sdk/src/main/java/com/mapbox/maps/renderer/widget/WidgetRenderer.kt b/sdk/src/main/java/com/mapbox/maps/renderer/widget/WidgetRenderer.kt index 2a1f44e67c..f0bac97087 100644 --- a/sdk/src/main/java/com/mapbox/maps/renderer/widget/WidgetRenderer.kt +++ b/sdk/src/main/java/com/mapbox/maps/renderer/widget/WidgetRenderer.kt @@ -10,4 +10,4 @@ internal interface WidgetRenderer { fun setRotation(angleDegrees: Float) fun setTranslation(translationX: Float, translationY: Float) -} +} \ No newline at end of file diff --git a/sdk/src/test/java/com/mapbox/TestUtils.kt b/sdk/src/test/java/com/mapbox/TestUtils.kt index 1a24d4ee3d..444bd17178 100644 --- a/sdk/src/test/java/com/mapbox/TestUtils.kt +++ b/sdk/src/test/java/com/mapbox/TestUtils.kt @@ -37,4 +37,4 @@ internal fun waitZeroCounter(startCounter: Int = 1, timeoutMillis: Int = 1000, r internal fun CountDownLatch.countDownEvery(stubBlock: MockKMatcherScope.() -> Unit) { every(stubBlock).answers { countDown() } -} +} \ No newline at end of file diff --git a/sdk/src/test/java/com/mapbox/maps/renderer/MapboxRenderThreadTest.kt b/sdk/src/test/java/com/mapbox/maps/renderer/MapboxRenderThreadTest.kt index a934c7ba8f..73a5c05a78 100644 --- a/sdk/src/test/java/com/mapbox/maps/renderer/MapboxRenderThreadTest.kt +++ b/sdk/src/test/java/com/mapbox/maps/renderer/MapboxRenderThreadTest.kt @@ -60,10 +60,10 @@ class MapboxRenderThreadTest { private fun mockWidgetRenderer() { mapboxWidgetRenderer = mockk(relaxUnitFun = true) - every { mapboxWidgetRenderer.getTexture() } returns 0 - every { mapboxWidgetRenderer.hasTexture() } returns false - every { mapboxWidgetRenderer.needTextureUpdate } returns false - every { mapboxWidgetRenderer.hasWidgets() } returns false + every { mapboxWidgetRenderer.getTexture() } returns 0 + every { mapboxWidgetRenderer.hasTexture() } returns false + every { mapboxWidgetRenderer.needTextureUpdate } returns false + every { mapboxWidgetRenderer.hasWidgets() } returns false } private fun mockEglCore() { @@ -561,8 +561,8 @@ class MapboxRenderThreadTest { fun onDrawDoesNotRenderWidgets() { initRenderThread() provideValidSurface() - every { mapboxWidgetRenderer.needTextureUpdate } returns false - every { mapboxWidgetRenderer.getTexture() } returns 0 + every { mapboxWidgetRenderer.needTextureUpdate } returns false + every { mapboxWidgetRenderer.getTexture() } returns 0 pauseHandler() mapboxRenderThread.queueRenderEvent(MapboxRenderer.renderEventSdk) idleHandler() @@ -579,10 +579,10 @@ class MapboxRenderThreadTest { initRenderThread() provideValidSurface() val textureId = 1 - every { mapboxWidgetRenderer.needTextureUpdate } returns true - every { mapboxWidgetRenderer.hasWidgets() } returns true - every { mapboxWidgetRenderer.hasTexture() } returns true - every { mapboxWidgetRenderer.getTexture() } returns textureId + every { mapboxWidgetRenderer.needTextureUpdate } returns true + every { mapboxWidgetRenderer.hasWidgets() } returns true + every { mapboxWidgetRenderer.hasTexture() } returns true + every { mapboxWidgetRenderer.getTexture() } returns textureId pauseHandler() mapboxRenderThread.queueRenderEvent(MapboxRenderer.renderEventSdk) idleHandler() @@ -599,10 +599,10 @@ class MapboxRenderThreadTest { initRenderThread() provideValidSurface() val textureId = 1 - every { mapboxWidgetRenderer.needTextureUpdate } returns true - every { mapboxWidgetRenderer.hasWidgets() } returns true - every { mapboxWidgetRenderer.hasTexture() } returns true - every { mapboxWidgetRenderer.getTexture() } returns textureId + every { mapboxWidgetRenderer.needTextureUpdate } returns true + every { mapboxWidgetRenderer.hasWidgets() } returns true + every { mapboxWidgetRenderer.hasTexture() } returns true + every { mapboxWidgetRenderer.getTexture() } returns textureId pauseHandler() mapboxRenderThread.queueRenderEvent(MapboxRenderer.renderEventSdk) idleHandler() From 48c17935378444c79e71c3808dd2f7ae4cdda04b Mon Sep 17 00:00:00 2001 From: yunik Date: Thu, 10 Feb 2022 18:08:29 +0200 Subject: [PATCH 10/13] Review fixes. --- .../maps/extension/androidauto/CompassWidget.kt | 2 ++ .../maps/extension/androidauto/LogoWidget.kt | 4 +++- .../main/java/com/mapbox/maps/MapControllable.kt | 2 +- .../mapbox/maps/renderer/MapboxRenderThread.kt | 2 +- .../mapbox/maps/renderer/MapboxWidgetRenderer.kt | 2 +- .../maps/renderer/widget/BitmapWidgetRenderer.kt | 15 +++++++++++---- .../maps/renderer/MapboxRenderThreadTest.kt | 12 ++++++------ 7 files changed, 25 insertions(+), 14 deletions(-) diff --git a/extension-androidauto/src/main/java/com/mapbox/maps/extension/androidauto/CompassWidget.kt b/extension-androidauto/src/main/java/com/mapbox/maps/extension/androidauto/CompassWidget.kt index ec976f9c74..25338337d0 100644 --- a/extension-androidauto/src/main/java/com/mapbox/maps/extension/androidauto/CompassWidget.kt +++ b/extension-androidauto/src/main/java/com/mapbox/maps/extension/androidauto/CompassWidget.kt @@ -2,6 +2,7 @@ package com.mapbox.maps.extension.androidauto import android.content.Context import android.graphics.BitmapFactory +import com.mapbox.maps.MapboxExperimental import com.mapbox.maps.renderer.widget.BitmapWidget import com.mapbox.maps.renderer.widget.WidgetPosition @@ -12,6 +13,7 @@ import com.mapbox.maps.renderer.widget.WidgetPosition * @param marginX horizontal margin in pixels * @param marginY vertical margin in pixels */ +@MapboxExperimental class CompassWidget( context: Context, position: WidgetPosition = WidgetPosition( diff --git a/extension-androidauto/src/main/java/com/mapbox/maps/extension/androidauto/LogoWidget.kt b/extension-androidauto/src/main/java/com/mapbox/maps/extension/androidauto/LogoWidget.kt index 81d4f6ef55..f4bdbcf39b 100644 --- a/extension-androidauto/src/main/java/com/mapbox/maps/extension/androidauto/LogoWidget.kt +++ b/extension-androidauto/src/main/java/com/mapbox/maps/extension/androidauto/LogoWidget.kt @@ -2,6 +2,7 @@ package com.mapbox.maps.extension.androidauto import android.content.Context import android.graphics.BitmapFactory +import com.mapbox.maps.MapboxExperimental import com.mapbox.maps.renderer.widget.BitmapWidget import com.mapbox.maps.renderer.widget.WidgetPosition @@ -12,7 +13,8 @@ import com.mapbox.maps.renderer.widget.WidgetPosition * @param marginX horizontal margin in pixels * @param marginY vertical margin in pixels */ -class LogoWidget( +@MapboxExperimental +class LogoWidget constructor( context: Context, position: WidgetPosition = WidgetPosition( horizontal = WidgetPosition.Horizontal.LEFT, diff --git a/sdk/src/main/java/com/mapbox/maps/MapControllable.kt b/sdk/src/main/java/com/mapbox/maps/MapControllable.kt index d73e7d1426..ac1c8abc6e 100644 --- a/sdk/src/main/java/com/mapbox/maps/MapControllable.kt +++ b/sdk/src/main/java/com/mapbox/maps/MapControllable.kt @@ -84,7 +84,7 @@ interface MapControllable : MapboxLifecycleObserver { /** * Remove [Widget] from the map. * - * @return true if widget was removed + * @return true if widget was present and removed, false otherwise */ @MapboxExperimental fun removeWidget(widget: Widget): Boolean diff --git a/sdk/src/main/java/com/mapbox/maps/renderer/MapboxRenderThread.kt b/sdk/src/main/java/com/mapbox/maps/renderer/MapboxRenderThread.kt index 479cd722d3..f02a552b55 100644 --- a/sdk/src/main/java/com/mapbox/maps/renderer/MapboxRenderThread.kt +++ b/sdk/src/main/java/com/mapbox/maps/renderer/MapboxRenderThread.kt @@ -222,7 +222,7 @@ internal class MapboxRenderThread : Choreographer.FrameCallback { private fun checkWidgetRender() { if (eglPrepared && !widgetRenderCreated && widgetRenderer.hasWidgets()) { - widgetRenderer.onSharedContext(eglCore.eglContext) + widgetRenderer.setSharedContext(eglCore.eglContext) widgetRenderCreated = true } } diff --git a/sdk/src/main/java/com/mapbox/maps/renderer/MapboxWidgetRenderer.kt b/sdk/src/main/java/com/mapbox/maps/renderer/MapboxWidgetRenderer.kt index 7371c9e241..237f986d23 100644 --- a/sdk/src/main/java/com/mapbox/maps/renderer/MapboxWidgetRenderer.kt +++ b/sdk/src/main/java/com/mapbox/maps/renderer/MapboxWidgetRenderer.kt @@ -32,7 +32,7 @@ internal class MapboxWidgetRenderer( fun getTexture() = textures[0] - fun onSharedContext(sharedContext: EGLContext) { + fun setSharedContext(sharedContext: EGLContext) { if (eglPrepared) { release() } diff --git a/sdk/src/main/java/com/mapbox/maps/renderer/widget/BitmapWidgetRenderer.kt b/sdk/src/main/java/com/mapbox/maps/renderer/widget/BitmapWidgetRenderer.kt index 9d647b8695..4381847571 100644 --- a/sdk/src/main/java/com/mapbox/maps/renderer/widget/BitmapWidgetRenderer.kt +++ b/sdk/src/main/java/com/mapbox/maps/renderer/widget/BitmapWidgetRenderer.kt @@ -9,6 +9,7 @@ import com.mapbox.maps.renderer.gl.GlUtils.put import com.mapbox.maps.renderer.gl.GlUtils.toFloatBuffer internal class BitmapWidgetRenderer( + @Volatile private var bitmap: Bitmap?, private val position: WidgetPosition, private val marginX: Float, @@ -167,9 +168,8 @@ internal class BitmapWidgetRenderer( GLES20.glUniformMatrix4fv(uniformMvpMatrix, 1, false, mvpMatrixBuffer) - textureFromBitmap() + textureFromBitmapIfChanged() - GLES20.glActiveTexture(GLES20.GL_TEXTURE0) GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, textures[0]) GLES20.glUniform1i(uniformTexture, 0) @@ -202,6 +202,7 @@ internal class BitmapWidgetRenderer( GLES20.glDisableVertexAttribArray(attributeVertexPosition) GLES20.glDisableVertexAttribArray(attributeTexturePosition) GLES20.glBindBuffer(GLES20.GL_ARRAY_BUFFER, 0) + GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, 0) GLES20.glUseProgram(0) needRender = false @@ -221,9 +222,14 @@ internal class BitmapWidgetRenderer( needRender = false } - private fun textureFromBitmap() { + /** + * Updates texture from bitmap once and nullifies bitmap. + */ + private fun textureFromBitmapIfChanged() { bitmap?.let { - GLES20.glGenTextures(1, textures, 0) + if (textures[0] == 0) { + GLES20.glGenTextures(1, textures, 0) + } GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, textures[0]) GLES20.glTexParameterf( GLES20.GL_TEXTURE_2D, @@ -246,6 +252,7 @@ internal class BitmapWidgetRenderer( GLES20.GL_CLAMP_TO_EDGE.toFloat() ) GLUtils.texImage2D(GLES20.GL_TEXTURE_2D, 0, it, 0) + GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, 0) bitmap = null } diff --git a/sdk/src/test/java/com/mapbox/maps/renderer/MapboxRenderThreadTest.kt b/sdk/src/test/java/com/mapbox/maps/renderer/MapboxRenderThreadTest.kt index 73a5c05a78..6c091e34fb 100644 --- a/sdk/src/test/java/com/mapbox/maps/renderer/MapboxRenderThreadTest.kt +++ b/sdk/src/test/java/com/mapbox/maps/renderer/MapboxRenderThreadTest.kt @@ -619,7 +619,7 @@ class MapboxRenderThreadTest { every { mapboxWidgetRenderer.hasWidgets() } returns true provideValidSurface() verifyOnce { - mapboxWidgetRenderer.onSharedContext(eglCore.eglContext) + mapboxWidgetRenderer.setSharedContext(eglCore.eglContext) } } @@ -629,7 +629,7 @@ class MapboxRenderThreadTest { every { mapboxWidgetRenderer.hasWidgets() } returns false provideValidSurface() verifyNo { - mapboxWidgetRenderer.onSharedContext(any()) + mapboxWidgetRenderer.setSharedContext(any()) } } @@ -639,7 +639,7 @@ class MapboxRenderThreadTest { every { eglCore.prepareEgl() } returns false provideValidSurface() verifyNo { - mapboxWidgetRenderer.onSharedContext(any()) + mapboxWidgetRenderer.setSharedContext(any()) } } @@ -651,7 +651,7 @@ class MapboxRenderThreadTest { mapboxRenderThread.onSurfaceCreated(surface, 1, 1) idleHandler() verifyNo { - mapboxWidgetRenderer.onSharedContext(any()) + mapboxWidgetRenderer.setSharedContext(any()) } } @@ -661,7 +661,7 @@ class MapboxRenderThreadTest { every { eglCore.createWindowSurface(any()) } returns eglCore.eglNoSurface provideValidSurface() verifyNo { - mapboxWidgetRenderer.onSharedContext(any()) + mapboxWidgetRenderer.setSharedContext(any()) } } @@ -671,7 +671,7 @@ class MapboxRenderThreadTest { every { eglCore.makeCurrent(any()) } returns false provideValidSurface() verifyNo { - mapboxWidgetRenderer.onSharedContext(any()) + mapboxWidgetRenderer.setSharedContext(any()) } } } \ No newline at end of file From a3937ffd2fd3e0091714d26d26d4468c8a504361 Mon Sep 17 00:00:00 2001 From: yunik Date: Thu, 10 Feb 2022 22:00:10 +0200 Subject: [PATCH 11/13] Update changelog. --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8d9559c1a3..479c191cce 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,7 @@ Mapbox welcomes participation and contributions from everyone. ## Features ✨ and improvements 🏁 * Add accuracy radius support for LocationComponent. ([#1016](https://github.com/mapbox/mapbox-maps-android/pull/1016)) +* Add support for custom widgets rendered on top of the map. ([#1036](https://github.com/mapbox/mapbox-maps-android/pull/1036)) # 10.4.0-beta.1 From a77a9449b0c169aed691acfd0e196dbce8950895 Mon Sep 17 00:00:00 2001 From: yunik Date: Fri, 11 Feb 2022 16:09:39 +0100 Subject: [PATCH 12/13] Fixed test. --- .../java/com/mapbox/maps/renderer/MapboxRenderThreadTest.kt | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/sdk/src/test/java/com/mapbox/maps/renderer/MapboxRenderThreadTest.kt b/sdk/src/test/java/com/mapbox/maps/renderer/MapboxRenderThreadTest.kt index 6c091e34fb..24d4ea1b9a 100644 --- a/sdk/src/test/java/com/mapbox/maps/renderer/MapboxRenderThreadTest.kt +++ b/sdk/src/test/java/com/mapbox/maps/renderer/MapboxRenderThreadTest.kt @@ -22,6 +22,7 @@ import org.robolectric.annotation.LooperMode import java.util.concurrent.CountDownLatch import java.util.concurrent.TimeUnit import javax.microedition.khronos.egl.EGL10 +import javax.microedition.khronos.egl.EGLContext @RunWith(RobolectricTestRunner::class) @Config(shadows = [ShadowLogger::class]) @@ -616,10 +617,12 @@ class MapboxRenderThreadTest { @Test fun onSurfaceCreatedWidgetsInitWidgetRender() { initRenderThread() + val eglContext = mockk() + every { eglCore.eglContext } returns eglContext every { mapboxWidgetRenderer.hasWidgets() } returns true provideValidSurface() verifyOnce { - mapboxWidgetRenderer.setSharedContext(eglCore.eglContext) + mapboxWidgetRenderer.setSharedContext(eglContext) } } From 16ec27d7e76dcb00b1177bf3a4a601da376a5cc3 Mon Sep 17 00:00:00 2001 From: yunik Date: Fri, 11 Feb 2022 17:30:53 +0100 Subject: [PATCH 13/13] Update public api files. --- .../api/extension-androidauto.api | 13 ++++- sdk/api/sdk.api | 47 +++++++++++++++++++ 2 files changed, 59 insertions(+), 1 deletion(-) diff --git a/extension-androidauto/api/extension-androidauto.api b/extension-androidauto/api/extension-androidauto.api index 61b9785e04..1b52d6918b 100644 --- a/extension-androidauto/api/extension-androidauto.api +++ b/extension-androidauto/api/extension-androidauto.api @@ -7,6 +7,16 @@ public final class com/mapbox/maps/extension/androidauto/BuildConfig { public fun ()V } +public final class com/mapbox/maps/extension/androidauto/CompassWidget : com/mapbox/maps/renderer/widget/BitmapWidget { + public fun (Landroid/content/Context;Lcom/mapbox/maps/renderer/widget/WidgetPosition;FF)V + public synthetic fun (Landroid/content/Context;Lcom/mapbox/maps/renderer/widget/WidgetPosition;FFILkotlin/jvm/internal/DefaultConstructorMarker;)V +} + +public final class com/mapbox/maps/extension/androidauto/LogoWidget : com/mapbox/maps/renderer/widget/BitmapWidget { + public fun (Landroid/content/Context;Lcom/mapbox/maps/renderer/widget/WidgetPosition;FF)V + public synthetic fun (Landroid/content/Context;Lcom/mapbox/maps/renderer/widget/WidgetPosition;FFILkotlin/jvm/internal/DefaultConstructorMarker;)V +} + public abstract interface class com/mapbox/maps/extension/androidauto/MapSurfaceReadyCallback { public abstract fun onMapSurfaceReady (Lcom/mapbox/maps/MapSurface;)V } @@ -14,9 +24,10 @@ public abstract interface class com/mapbox/maps/extension/androidauto/MapSurface public final class com/mapbox/maps/extension/androidauto/MapboxCarUtilsKt { public static final fun initMapSurface (Landroidx/car/app/Session;Lcom/mapbox/maps/MapInitOptions;Lcom/mapbox/maps/extension/androidauto/MapSurfaceReadyCallback;)V public static final fun initMapSurface (Landroidx/car/app/Session;Lcom/mapbox/maps/MapInitOptions;Lcom/mapbox/maps/extension/androidauto/OnMapScrollListener;Lcom/mapbox/maps/extension/androidauto/MapSurfaceReadyCallback;)V + public static final fun initMapSurface (Landroidx/car/app/Session;Lcom/mapbox/maps/MapInitOptions;Lcom/mapbox/maps/extension/androidauto/OnMapScrollListener;Lcom/mapbox/maps/extension/androidauto/OnMapScaleListener;Landroidx/car/app/SurfaceCallback;Lcom/mapbox/maps/extension/androidauto/MapSurfaceReadyCallback;)V public static final fun initMapSurface (Landroidx/car/app/Session;Lcom/mapbox/maps/MapInitOptions;Lcom/mapbox/maps/extension/androidauto/OnMapScrollListener;Lcom/mapbox/maps/extension/androidauto/OnMapScaleListener;Lcom/mapbox/maps/extension/androidauto/MapSurfaceReadyCallback;)V public static final fun initMapSurface (Landroidx/car/app/Session;Lcom/mapbox/maps/extension/androidauto/MapSurfaceReadyCallback;)V - public static synthetic fun initMapSurface$default (Landroidx/car/app/Session;Lcom/mapbox/maps/MapInitOptions;Lcom/mapbox/maps/extension/androidauto/OnMapScrollListener;Lcom/mapbox/maps/extension/androidauto/OnMapScaleListener;Lcom/mapbox/maps/extension/androidauto/MapSurfaceReadyCallback;ILjava/lang/Object;)V + public static synthetic fun initMapSurface$default (Landroidx/car/app/Session;Lcom/mapbox/maps/MapInitOptions;Lcom/mapbox/maps/extension/androidauto/OnMapScrollListener;Lcom/mapbox/maps/extension/androidauto/OnMapScaleListener;Landroidx/car/app/SurfaceCallback;Lcom/mapbox/maps/extension/androidauto/MapSurfaceReadyCallback;ILjava/lang/Object;)V } public abstract interface class com/mapbox/maps/extension/androidauto/OnMapScaleListener { diff --git a/sdk/api/sdk.api b/sdk/api/sdk.api index 3cf2e6716f..ca4eae6783 100644 --- a/sdk/api/sdk.api +++ b/sdk/api/sdk.api @@ -9,11 +9,13 @@ public final class com/mapbox/maps/BuildConfig { } public abstract interface class com/mapbox/maps/MapControllable : com/mapbox/maps/MapboxLifecycleObserver { + public abstract fun addWidget (Lcom/mapbox/maps/renderer/widget/Widget;)V public abstract fun getMapboxMap ()Lcom/mapbox/maps/MapboxMap; public abstract fun onGenericMotionEvent (Landroid/view/MotionEvent;)Z public abstract fun onSizeChanged (II)V public abstract fun onTouchEvent (Landroid/view/MotionEvent;)Z public abstract fun queueEvent (Ljava/lang/Runnable;Z)V + public abstract fun removeWidget (Lcom/mapbox/maps/renderer/widget/Widget;)Z public abstract fun setMaximumFps (I)V public abstract fun setOnFpsChangedListener (Lcom/mapbox/maps/renderer/OnFpsChangedListener;)V public abstract fun snapshot ()Landroid/graphics/Bitmap; @@ -83,8 +85,10 @@ public final class com/mapbox/maps/MapSurface : com/mapbox/maps/MapControllable, public fun (Landroid/content/Context;Landroid/view/Surface;)V public fun (Landroid/content/Context;Landroid/view/Surface;Lcom/mapbox/maps/MapInitOptions;)V public synthetic fun (Landroid/content/Context;Landroid/view/Surface;Lcom/mapbox/maps/MapInitOptions;ILkotlin/jvm/internal/DefaultConstructorMarker;)V + public fun addWidget (Lcom/mapbox/maps/renderer/widget/Widget;)V public fun getMapboxMap ()Lcom/mapbox/maps/MapboxMap; public fun getPlugin (Ljava/lang/String;)Lcom/mapbox/maps/plugin/MapPlugin; + public final fun getSurface ()Landroid/view/Surface; public fun onDestroy ()V public fun onGenericMotionEvent (Landroid/view/MotionEvent;)Z public fun onLowMemory ()V @@ -93,6 +97,7 @@ public final class com/mapbox/maps/MapSurface : com/mapbox/maps/MapControllable, public fun onStop ()V public fun onTouchEvent (Landroid/view/MotionEvent;)Z public fun queueEvent (Ljava/lang/Runnable;Z)V + public fun removeWidget (Lcom/mapbox/maps/renderer/widget/Widget;)Z public fun setMaximumFps (I)V public fun setOnFpsChangedListener (Lcom/mapbox/maps/renderer/OnFpsChangedListener;)V public fun snapshot ()Landroid/graphics/Bitmap; @@ -109,6 +114,7 @@ public class com/mapbox/maps/MapView : android/widget/FrameLayout, com/mapbox/ma public fun (Landroid/content/Context;Landroid/util/AttributeSet;I)V public fun (Landroid/content/Context;Lcom/mapbox/maps/MapInitOptions;)V public synthetic fun (Landroid/content/Context;Lcom/mapbox/maps/MapInitOptions;ILkotlin/jvm/internal/DefaultConstructorMarker;)V + public fun addWidget (Lcom/mapbox/maps/renderer/widget/Widget;)V public final fun createPlugin (Lcom/mapbox/maps/plugin/Plugin;)V public fun getMapboxMap ()Lcom/mapbox/maps/MapboxMap; public fun getPlugin (Ljava/lang/String;)Lcom/mapbox/maps/plugin/MapPlugin; @@ -124,6 +130,7 @@ public class com/mapbox/maps/MapView : android/widget/FrameLayout, com/mapbox/ma public fun onStop ()V public fun onTouchEvent (Landroid/view/MotionEvent;)Z public fun queueEvent (Ljava/lang/Runnable;Z)V + public fun removeWidget (Lcom/mapbox/maps/renderer/widget/Widget;)Z public fun setMaximumFps (I)V public fun setOnFpsChangedListener (Lcom/mapbox/maps/renderer/OnFpsChangedListener;)V public fun snapshot ()Landroid/graphics/Bitmap; @@ -496,6 +503,46 @@ public final class com/mapbox/maps/renderer/RenderHandlerThread$sam$i$java_lang_ public final synthetic fun run ()V } +public class com/mapbox/maps/renderer/widget/BitmapWidget : com/mapbox/maps/renderer/widget/Widget { + public fun (Landroid/graphics/Bitmap;)V + public fun (Landroid/graphics/Bitmap;Lcom/mapbox/maps/renderer/widget/WidgetPosition;)V + public fun (Landroid/graphics/Bitmap;Lcom/mapbox/maps/renderer/widget/WidgetPosition;F)V + public fun (Landroid/graphics/Bitmap;Lcom/mapbox/maps/renderer/widget/WidgetPosition;FF)V + public synthetic fun (Landroid/graphics/Bitmap;Lcom/mapbox/maps/renderer/widget/WidgetPosition;FFILkotlin/jvm/internal/DefaultConstructorMarker;)V + public synthetic fun getRenderer$sdk_release ()Lcom/mapbox/maps/renderer/widget/WidgetRenderer; + public fun setRotation (F)V + public fun setTranslation (FF)V + public final fun updateBitmap (Landroid/graphics/Bitmap;)V +} + +public abstract class com/mapbox/maps/renderer/widget/Widget { + public fun ()V + public abstract fun setRotation (F)V + public abstract fun setTranslation (FF)V +} + +public final class com/mapbox/maps/renderer/widget/WidgetPosition { + public fun (Lcom/mapbox/maps/renderer/widget/WidgetPosition$Horizontal;Lcom/mapbox/maps/renderer/widget/WidgetPosition$Vertical;)V + public final fun getHorizontal ()Lcom/mapbox/maps/renderer/widget/WidgetPosition$Horizontal; + public final fun getVertical ()Lcom/mapbox/maps/renderer/widget/WidgetPosition$Vertical; +} + +public final class com/mapbox/maps/renderer/widget/WidgetPosition$Horizontal : java/lang/Enum { + public static final field CENTER Lcom/mapbox/maps/renderer/widget/WidgetPosition$Horizontal; + public static final field LEFT Lcom/mapbox/maps/renderer/widget/WidgetPosition$Horizontal; + public static final field RIGHT Lcom/mapbox/maps/renderer/widget/WidgetPosition$Horizontal; + public static fun valueOf (Ljava/lang/String;)Lcom/mapbox/maps/renderer/widget/WidgetPosition$Horizontal; + public static fun values ()[Lcom/mapbox/maps/renderer/widget/WidgetPosition$Horizontal; +} + +public final class com/mapbox/maps/renderer/widget/WidgetPosition$Vertical : java/lang/Enum { + public static final field BOTTOM Lcom/mapbox/maps/renderer/widget/WidgetPosition$Vertical; + public static final field CENTER Lcom/mapbox/maps/renderer/widget/WidgetPosition$Vertical; + public static final field TOP Lcom/mapbox/maps/renderer/widget/WidgetPosition$Vertical; + public static fun valueOf (Ljava/lang/String;)Lcom/mapbox/maps/renderer/widget/WidgetPosition$Vertical; + public static fun values ()[Lcom/mapbox/maps/renderer/widget/WidgetPosition$Vertical; +} + public abstract interface class com/mapbox/maps/viewannotation/ViewAnnotationManager { public abstract fun addViewAnnotation (ILcom/mapbox/maps/ViewAnnotationOptions;)Landroid/view/View; public abstract fun addViewAnnotation (ILcom/mapbox/maps/ViewAnnotationOptions;Landroidx/asynclayoutinflater/view/AsyncLayoutInflater;Lkotlin/jvm/functions/Function1;)V