Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Replace GLSurfaceView with GLRenderer and AndroidExternalSurface #1908

Draft
wants to merge 6 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions app/dependencies/releaseRuntimeClasspath.txt
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,7 @@ androidx.emoji2:emoji2-views-helper:1.4.0
androidx.emoji2:emoji2:1.4.0
androidx.exifinterface:exifinterface:1.3.7
androidx.fragment:fragment:1.5.4
androidx.graphics:graphics-core:1.0.2
androidx.graphics:graphics-path:1.0.1
androidx.graphics:graphics-shapes-android:1.0.1
androidx.graphics:graphics-shapes:1.0.1
Expand Down
3 changes: 3 additions & 0 deletions gradle/libs.versions.toml
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@ androidxCore = "1.16.0-alpha01"
androidxCoreSplashscreen = "1.2.0-alpha02"
#androidxDataStore
androidxDataStore = "1.1.2"
#androidxGraphics
androidxGraphics = "1.0.2"
#androidxLifecycle
androidxLifecycle = "2.9.0-alpha08"
#androidxLintGradle
Expand Down Expand Up @@ -149,6 +151,7 @@ androidx-core = { group = "androidx.core", name = "core-ktx", version.ref = "and
androidx-core-splashscreen = { group = "androidx.core", name = "core-splashscreen", version.ref = "androidxCoreSplashscreen" }
androidx-dataStore = { group = "androidx.datastore", name = "datastore", version.ref = "androidxDataStore" }
androidx-dataStore-core-okio = { group = "androidx.datastore", name = "datastore-core-okio", version.ref = "androidxDataStore" }
androidx-graphics-core = { group = "androidx.graphics", name = "graphics-core", version.ref = "androidxGraphics" }
androidx-lifecycle-process = { group = "androidx.lifecycle", name = "lifecycle-process", version.ref = "androidxLifecycle" }
androidx-lifecycle-runtime = { group = "androidx.lifecycle", name = "lifecycle-runtime-ktx", version.ref = "androidxLifecycle" }
androidx-lifecycle-runtime-compose = { group = "androidx.lifecycle", name = "lifecycle-runtime-compose", version.ref = "androidxLifecycle" }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -261,8 +261,10 @@ class GameOfLifeShape(

fun draw(mvpMatrix: FloatArray) {
GLES20.glUseProgram(program)
checkOpenGLError()

GLES20.glEnableVertexAttribArray(positionHandle)
checkOpenGLError()

GLES20.glVertexAttribPointer(
positionHandle,
Expand All @@ -272,15 +274,19 @@ class GameOfLifeShape(
vertexStride,
vertexBuffer,
)
checkOpenGLError()

GLES20.glUniformMatrix4fv(mvpMatrixHandle, 1, false, mvpMatrix, 0)
checkOpenGLError()
checkOpenGLFramebufferStatus()

GLES20.glDrawElements(
GLES20.GL_TRIANGLES,
drawOrder.size,
GLES20.GL_UNSIGNED_SHORT,
drawListBuffer,
)
checkOpenGLError()

GLES20.glDisableVertexAttribArray(positionHandle)
checkOpenGLError()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,16 @@ fun checkOpenGLError() {
}
}

/**
* Throws if the framebuffer is not complete.
*/
fun checkOpenGLFramebufferStatus() {
val status = GLES20.glCheckFramebufferStatus(GLES20.GL_FRAMEBUFFER)
if (status != GLES20.GL_FRAMEBUFFER_COMPLETE) {
error("OpenGL framebuffer error: $status")
}
}

/**
* Creates and compiles the given [shaderCode] of type [type].
*/
Expand Down
5 changes: 5 additions & 0 deletions ui-cells/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,10 @@ android {
configureGradleManagedDevices(setOf(FormFactor.Mobile), this)
}

ksp {
arg("skipPrivatePreviews", "true")
}

kotlin {
androidTarget()
jvm("desktop")
Expand Down Expand Up @@ -104,6 +108,7 @@ kotlin {
implementation(libs.androidx.compose.uiTooling)
implementation(libs.androidx.compose.uiUtil)
implementation(libs.androidx.core)
implementation(libs.androidx.graphics.core)
implementation(libs.androidx.lifecycle.runtime)
implementation(libs.androidx.poolingContainer)
implementation(libs.androidx.window)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,43 +17,41 @@
package com.alexvanyo.composelife.ui.cells

import android.app.ActivityManager
import android.opengl.EGLConfig
import android.opengl.EGLSurface
import android.opengl.GLES20
import android.opengl.GLSurfaceView
import android.opengl.Matrix
import android.view.Surface
import androidx.compose.foundation.AndroidExternalSurface
import androidx.compose.foundation.AndroidExternalSurfaceZOrder
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.runtime.Composable
import androidx.compose.runtime.RememberObserver
import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.rememberUpdatedState
import androidx.compose.runtime.snapshotFlow
import androidx.compose.ui.Modifier
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.viewinterop.AndroidView
import androidx.core.content.getSystemService
import androidx.core.view.isInvisible
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.compose.LocalLifecycleOwner
import androidx.lifecycle.repeatOnLifecycle
import androidx.graphics.opengl.GLRenderer
import androidx.graphics.opengl.egl.EGLManager
import androidx.graphics.opengl.egl.EGLSpec
import com.alexvanyo.composelife.model.CellWindow
import com.alexvanyo.composelife.model.GameOfLifeState
import com.alexvanyo.composelife.openglrenderer.GameOfLifeShape
import com.alexvanyo.composelife.openglrenderer.GameOfLifeShapeParameters
import com.alexvanyo.composelife.openglrenderer.checkOpenGLError
import com.alexvanyo.composelife.openglrenderer.checkOpenGLFramebufferStatus
import com.alexvanyo.composelife.preferences.CurrentShape
import com.alexvanyo.composelife.ui.mobile.ComposeLifeTheme
import com.alexvanyo.composelife.ui.util.LocalGhostElement
import kotlinx.coroutines.asCoroutineDispatcher
import kotlinx.coroutines.awaitCancellation
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.flow.flowOn
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.launch
import java.nio.IntBuffer
import java.util.concurrent.Executor
import javax.microedition.khronos.egl.EGLConfig
import javax.microedition.khronos.opengles.GL10

@Composable
fun openGLSupported(): Boolean {
Expand Down Expand Up @@ -92,32 +90,41 @@
buffer
}

val parameters = when (shape) {
is CurrentShape.RoundRectangle -> {
GameOfLifeShapeParameters.RoundRectangle(
cells = cellsBuffer,
aliveColor = aliveColor,
deadColor = deadColor,
cellWindowSize = cellWindow.size,
scaledCellPixelSize = scaledCellPixelSize,
pixelOffsetFromCenter = pixelOffsetFromCenter,
sizeFraction = shape.sizeFraction,
cornerFraction = shape.cornerFraction,
)
}
}

val lifecycleOwner = LocalLifecycleOwner.current
val coroutineScope = rememberCoroutineScope()
val parameters by rememberUpdatedState(
when (shape) {
is CurrentShape.RoundRectangle -> {
GameOfLifeShapeParameters.RoundRectangle(
cells = cellsBuffer,
aliveColor = aliveColor,
deadColor = deadColor,
cellWindowSize = cellWindow.size,
scaledCellPixelSize = scaledCellPixelSize,
pixelOffsetFromCenter = pixelOffsetFromCenter,
sizeFraction = shape.sizeFraction,
cornerFraction = shape.cornerFraction,

Check warning on line 104 in ui-cells/src/androidMain/kotlin/com/alexvanyo/composelife/ui/cells/OpenGLNonInteractableCells.kt

View check run for this annotation

Codecov / codecov/patch

ui-cells/src/androidMain/kotlin/com/alexvanyo/composelife/ui/cells/OpenGLNonInteractableCells.kt#L93-L104

Added lines #L93 - L104 were not covered by tests
)
}
},
)

val isGhostElement by rememberUpdatedState(LocalGhostElement.current)
val glRenderer = rememberGLRenderer()

Check warning on line 110 in ui-cells/src/androidMain/kotlin/com/alexvanyo/composelife/ui/cells/OpenGLNonInteractableCells.kt

View check run for this annotation

Codecov / codecov/patch

ui-cells/src/androidMain/kotlin/com/alexvanyo/composelife/ui/cells/OpenGLNonInteractableCells.kt#L110

Added line #L110 was not covered by tests

AndroidView(
factory = { context ->
object : GLSurfaceView(context) {
val parametersState = MutableStateFlow(parameters)
AndroidExternalSurface(
modifier = modifier,

Check warning on line 113 in ui-cells/src/androidMain/kotlin/com/alexvanyo/composelife/ui/cells/OpenGLNonInteractableCells.kt

View check run for this annotation

Codecov / codecov/patch

ui-cells/src/androidMain/kotlin/com/alexvanyo/composelife/ui/cells/OpenGLNonInteractableCells.kt#L113

Added line #L113 was not covered by tests
zOrder = if (inOverlay) {
AndroidExternalSurfaceZOrder.MediaOverlay

Check warning on line 115 in ui-cells/src/androidMain/kotlin/com/alexvanyo/composelife/ui/cells/OpenGLNonInteractableCells.kt

View check run for this annotation

Codecov / codecov/patch

ui-cells/src/androidMain/kotlin/com/alexvanyo/composelife/ui/cells/OpenGLNonInteractableCells.kt#L115

Added line #L115 was not covered by tests
} else {
AndroidExternalSurfaceZOrder.Behind

Check warning on line 117 in ui-cells/src/androidMain/kotlin/com/alexvanyo/composelife/ui/cells/OpenGLNonInteractableCells.kt

View check run for this annotation

Codecov / codecov/patch

ui-cells/src/androidMain/kotlin/com/alexvanyo/composelife/ui/cells/OpenGLNonInteractableCells.kt#L117

Added line #L117 was not covered by tests
},
) {
onSurface { surface, width, height ->
val renderTarget = glRenderer.attach(
surface,
width,
height,
object : GLRenderer.RenderCallback {

Check warning on line 125 in ui-cells/src/androidMain/kotlin/com/alexvanyo/composelife/ui/cells/OpenGLNonInteractableCells.kt

View check run for this annotation

Codecov / codecov/patch

ui-cells/src/androidMain/kotlin/com/alexvanyo/composelife/ui/cells/OpenGLNonInteractableCells.kt#L119-L125

Added lines #L119 - L125 were not covered by tests
private lateinit var gameOfLifeShape: GameOfLifeShape

val renderer = object : Renderer {
private val mvpMatrix = FloatArray(16)

init {
Expand All @@ -128,64 +135,102 @@
Matrix.multiplyMM(mvpMatrix, 0, projectionMatrix, 0, viewMatrix, 0)
}

lateinit var gameOfLifeShape: GameOfLifeShape
override fun onSurfaceCreated(
spec: EGLSpec,
config: EGLConfig,
surface: Surface,
width: Int,
height: Int,
): EGLSurface? =
super.onSurfaceCreated(spec, config, surface, width, height).also {
GLES20.glClearColor(0f, 0f, 0f, 0f)
GLES20.glViewport(0, 0, width, height)
gameOfLifeShape = GameOfLifeShape().apply {
setSize(width, height)
}
}

Check warning on line 151 in ui-cells/src/androidMain/kotlin/com/alexvanyo/composelife/ui/cells/OpenGLNonInteractableCells.kt

View check run for this annotation

Codecov / codecov/patch

ui-cells/src/androidMain/kotlin/com/alexvanyo/composelife/ui/cells/OpenGLNonInteractableCells.kt#L145-L151

Added lines #L145 - L151 were not covered by tests

override fun onSurfaceCreated(unused: GL10?, config: EGLConfig?) {
GLES20.glClearColor(0f, 0f, 0f, 0f)
gameOfLifeShape = GameOfLifeShape()
gameOfLifeShape.setScreenShapeParameters(parametersState.value)
override fun onDrawFrame(eglManager: EGLManager) {
gameOfLifeShape.setScreenShapeParameters(parameters)
gameOfLifeShape.draw(mvpMatrix)

Check warning on line 155 in ui-cells/src/androidMain/kotlin/com/alexvanyo/composelife/ui/cells/OpenGLNonInteractableCells.kt

View check run for this annotation

Codecov / codecov/patch

ui-cells/src/androidMain/kotlin/com/alexvanyo/composelife/ui/cells/OpenGLNonInteractableCells.kt#L154-L155

Added lines #L154 - L155 were not covered by tests
}
},
)

override fun onSurfaceChanged(unused: GL10?, width: Int, height: Int) {
GLES20.glViewport(0, 0, width, height)
gameOfLifeShape.setSize(width, height)
}
surface.onChanged { w, h ->
renderTarget.resize(w, h)
}

Check warning on line 162 in ui-cells/src/androidMain/kotlin/com/alexvanyo/composelife/ui/cells/OpenGLNonInteractableCells.kt

View check run for this annotation

Codecov / codecov/patch

ui-cells/src/androidMain/kotlin/com/alexvanyo/composelife/ui/cells/OpenGLNonInteractableCells.kt#L160-L162

Added lines #L160 - L162 were not covered by tests

override fun onDrawFrame(unused: GL10?) {
gameOfLifeShape.draw(mvpMatrix)
}
surface.onDestroyed {
glRenderer.detach(renderTarget, true)
}

Check warning on line 166 in ui-cells/src/androidMain/kotlin/com/alexvanyo/composelife/ui/cells/OpenGLNonInteractableCells.kt

View check run for this annotation

Codecov / codecov/patch

ui-cells/src/androidMain/kotlin/com/alexvanyo/composelife/ui/cells/OpenGLNonInteractableCells.kt#L164-L166

Added lines #L164 - L166 were not covered by tests

fun setParameters(parameters: GameOfLifeShapeParameters) {
if (::gameOfLifeShape.isInitialized) {
gameOfLifeShape.setScreenShapeParameters(parameters)
}
}
}
}.apply {
setEGLContextClientVersion(2)
setRenderer(renderer)
renderMode = GLSurfaceView.RENDERMODE_WHEN_DIRTY

val openGLExecutor = Executor(::queueEvent)
val openGLDispatcher = openGLExecutor.asCoroutineDispatcher()

coroutineScope.launch {
parametersState
.onEach(renderer::setParameters)
.onEach { requestRender() }
.flowOn(openGLDispatcher)
.collect()
snapshotFlow { parameters }

Check warning on line 168 in ui-cells/src/androidMain/kotlin/com/alexvanyo/composelife/ui/cells/OpenGLNonInteractableCells.kt

View check run for this annotation

Codecov / codecov/patch

ui-cells/src/androidMain/kotlin/com/alexvanyo/composelife/ui/cells/OpenGLNonInteractableCells.kt#L168

Added line #L168 was not covered by tests
.onEach {
renderTarget.requestRender()

Check warning on line 170 in ui-cells/src/androidMain/kotlin/com/alexvanyo/composelife/ui/cells/OpenGLNonInteractableCells.kt

View check run for this annotation

Codecov / codecov/patch

ui-cells/src/androidMain/kotlin/com/alexvanyo/composelife/ui/cells/OpenGLNonInteractableCells.kt#L170

Added line #L170 was not covered by tests
}
.collect()
}
}
}

Check warning on line 175 in ui-cells/src/androidMain/kotlin/com/alexvanyo/composelife/ui/cells/OpenGLNonInteractableCells.kt

View check run for this annotation

Codecov / codecov/patch

ui-cells/src/androidMain/kotlin/com/alexvanyo/composelife/ui/cells/OpenGLNonInteractableCells.kt#L172-L175

Added lines #L172 - L175 were not covered by tests

coroutineScope.launch {
lifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) {
onResume()
try {
awaitCancellation()
} finally {
onPause()
@Composable
fun rememberGLRenderer(): GLRenderer =
remember {
object : RememberObserver {
val glRenderer = GLRenderer()
override fun onAbandoned() = onForgotten()

Check warning on line 182 in ui-cells/src/androidMain/kotlin/com/alexvanyo/composelife/ui/cells/OpenGLNonInteractableCells.kt

View check run for this annotation

Codecov / codecov/patch

ui-cells/src/androidMain/kotlin/com/alexvanyo/composelife/ui/cells/OpenGLNonInteractableCells.kt#L179-L182

Added lines #L179 - L182 were not covered by tests
override fun onForgotten() {
glRenderer.stop(true)
}

Check warning on line 185 in ui-cells/src/androidMain/kotlin/com/alexvanyo/composelife/ui/cells/OpenGLNonInteractableCells.kt

View check run for this annotation

Codecov / codecov/patch

ui-cells/src/androidMain/kotlin/com/alexvanyo/composelife/ui/cells/OpenGLNonInteractableCells.kt#L184-L185

Added lines #L184 - L185 were not covered by tests
override fun onRemembered() {
glRenderer.start()
}
}

Check warning on line 189 in ui-cells/src/androidMain/kotlin/com/alexvanyo/composelife/ui/cells/OpenGLNonInteractableCells.kt

View check run for this annotation

Codecov / codecov/patch

ui-cells/src/androidMain/kotlin/com/alexvanyo/composelife/ui/cells/OpenGLNonInteractableCells.kt#L187-L189

Added lines #L187 - L189 were not covered by tests
}.glRenderer

@Preview
@Composable
private fun OpenGLRepro() {
val glRenderer = rememberGLRenderer()

Check warning on line 195 in ui-cells/src/androidMain/kotlin/com/alexvanyo/composelife/ui/cells/OpenGLNonInteractableCells.kt

View check run for this annotation

Codecov / codecov/patch

ui-cells/src/androidMain/kotlin/com/alexvanyo/composelife/ui/cells/OpenGLNonInteractableCells.kt#L195

Added line #L195 was not covered by tests

AndroidExternalSurface(
modifier = Modifier.fillMaxSize(),
) {

Check warning on line 199 in ui-cells/src/androidMain/kotlin/com/alexvanyo/composelife/ui/cells/OpenGLNonInteractableCells.kt

View check run for this annotation

Codecov / codecov/patch

ui-cells/src/androidMain/kotlin/com/alexvanyo/composelife/ui/cells/OpenGLNonInteractableCells.kt#L198-L199

Added lines #L198 - L199 were not covered by tests
onSurface { surface, width, height ->
val renderTarget = glRenderer.attach(
surface,
width,
height,
object : GLRenderer.RenderCallback {

Check warning on line 205 in ui-cells/src/androidMain/kotlin/com/alexvanyo/composelife/ui/cells/OpenGLNonInteractableCells.kt

View check run for this annotation

Codecov / codecov/patch

ui-cells/src/androidMain/kotlin/com/alexvanyo/composelife/ui/cells/OpenGLNonInteractableCells.kt#L201-L205

Added lines #L201 - L205 were not covered by tests
override fun onSurfaceCreated(
spec: EGLSpec,
config: EGLConfig,
surface: Surface,
width: Int,
height: Int,
): EGLSurface? =
super.onSurfaceCreated(spec, config, surface, width, height).also {
GLES20.glViewport(0, 0, width, height)

Check warning on line 214 in ui-cells/src/androidMain/kotlin/com/alexvanyo/composelife/ui/cells/OpenGLNonInteractableCells.kt

View check run for this annotation

Codecov / codecov/patch

ui-cells/src/androidMain/kotlin/com/alexvanyo/composelife/ui/cells/OpenGLNonInteractableCells.kt#L213-L214

Added lines #L213 - L214 were not covered by tests
}

override fun onDrawFrame(eglManager: EGLManager) {
checkOpenGLError()
checkOpenGLFramebufferStatus()

Check warning on line 219 in ui-cells/src/androidMain/kotlin/com/alexvanyo/composelife/ui/cells/OpenGLNonInteractableCells.kt

View check run for this annotation

Codecov / codecov/patch

ui-cells/src/androidMain/kotlin/com/alexvanyo/composelife/ui/cells/OpenGLNonInteractableCells.kt#L218-L219

Added lines #L218 - L219 were not covered by tests
}
}
},
)

surface.onChanged { w, h ->
renderTarget.resize(w, h)
renderTarget.requestRender()

Check warning on line 226 in ui-cells/src/androidMain/kotlin/com/alexvanyo/composelife/ui/cells/OpenGLNonInteractableCells.kt

View check run for this annotation

Codecov / codecov/patch

ui-cells/src/androidMain/kotlin/com/alexvanyo/composelife/ui/cells/OpenGLNonInteractableCells.kt#L224-L226

Added lines #L224 - L226 were not covered by tests
}
},
update = {
it.parametersState.value = parameters
it.setZOrderMediaOverlay(inOverlay)
// The GLSurfaceView may not handling animating in and out well with externally applied alphas.
// If we are in a ghost element, skip showing it.
it.isInvisible = isGhostElement
},
modifier = modifier,
)

surface.onDestroyed {
glRenderer.detach(renderTarget, true)
}

Check warning on line 231 in ui-cells/src/androidMain/kotlin/com/alexvanyo/composelife/ui/cells/OpenGLNonInteractableCells.kt

View check run for this annotation

Codecov / codecov/patch

ui-cells/src/androidMain/kotlin/com/alexvanyo/composelife/ui/cells/OpenGLNonInteractableCells.kt#L229-L231

Added lines #L229 - L231 were not covered by tests

renderTarget.requestRender()
}
}

Check warning on line 235 in ui-cells/src/androidMain/kotlin/com/alexvanyo/composelife/ui/cells/OpenGLNonInteractableCells.kt

View check run for this annotation

Codecov / codecov/patch

ui-cells/src/androidMain/kotlin/com/alexvanyo/composelife/ui/cells/OpenGLNonInteractableCells.kt#L233-L235

Added lines #L233 - L235 were not covered by tests
}
Loading