summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorTrevor McGuire <trevormcguire@google.com>2024-03-29 13:52:05 -0700
committerGitHub <noreply@github.com>2024-03-29 13:52:05 -0700
commit96d1cc4bd2216d10e4751d2e272e6e2dba2ab00e (patch)
treec89668dc271ba4640d5e1a01ffb7ccf632be75e8
parent7ee29775a78c3e6a0ef362f7784e6f0fca79bf47 (diff)
downloadjetpack-camera-app-96d1cc4bd2216d10e4751d2e272e6e2dba2ab00e.tar.gz
Migrate shader copy effect to androidx-graphics-core (#151)
* Modify EmptySurfaceProcessor to work with HDR Hacks in a GLES3 shader to allow for copying 10-bit pixels. * Migrate to using AndroidX graphics for effects processor This effects processor uses the graphics-core library's GLRenderer to perform rendering rather than CameraX's internal OpenGLRenderer. This renderer can handle SDR and 10-bit HLG dynamic ranges. * Rename EmptySurfaceProcessor to CopyingSurfaceProcessor * Factor out shader copy into own class Factors out the shader copying code into its own class, ShaderCopy. This will allow us to create separate implementations to perform the copy that do not just use shaders. * Hook up SingleStreamForcingEffect Add SingleStreamForcingEffect when the capture mode is set to SINGLE_STREAM. * Move graphics-core to versions toml * Apply spotless * Use property delegation to remove most duplicated code from EGLSpecV14ES3 * Add link in comment to issue tracker issue for later cleanup
-rw-r--r--domain/camera/build.gradle.kts3
-rw-r--r--domain/camera/src/main/java/com/google/jetpackcamera/domain/camera/CameraXCameraUseCase.kt16
-rw-r--r--domain/camera/src/main/java/com/google/jetpackcamera/domain/camera/EmptySurfaceProcessor.kt143
-rw-r--r--domain/camera/src/main/java/com/google/jetpackcamera/domain/camera/effects/CopyingSurfaceProcessor.kt361
-rw-r--r--domain/camera/src/main/java/com/google/jetpackcamera/domain/camera/effects/EGLSpecV14ES3.kt46
-rw-r--r--domain/camera/src/main/java/com/google/jetpackcamera/domain/camera/effects/ShaderCopy.kt450
-rw-r--r--domain/camera/src/main/java/com/google/jetpackcamera/domain/camera/effects/SingleSurfaceForcingEffect.kt (renamed from domain/camera/src/main/java/com/google/jetpackcamera/domain/camera/SingleSurfaceForcingEffect.kt)23
-rw-r--r--gradle/libs.versions.toml2
8 files changed, 881 insertions, 163 deletions
diff --git a/domain/camera/build.gradle.kts b/domain/camera/build.gradle.kts
index aafcae7..a26d9be 100644
--- a/domain/camera/build.gradle.kts
+++ b/domain/camera/build.gradle.kts
@@ -76,6 +76,9 @@ dependencies {
// Tracing
implementation("androidx.tracing:tracing-ktx:1.2.0")
+ // Graphics libraries
+ implementation(libs.androidx.graphics.core)
+
// Project dependencies
implementation(project(":data:settings"))
implementation(project(":core:common"))
diff --git a/domain/camera/src/main/java/com/google/jetpackcamera/domain/camera/CameraXCameraUseCase.kt b/domain/camera/src/main/java/com/google/jetpackcamera/domain/camera/CameraXCameraUseCase.kt
index 29982e5..5d82f0f 100644
--- a/domain/camera/src/main/java/com/google/jetpackcamera/domain/camera/CameraXCameraUseCase.kt
+++ b/domain/camera/src/main/java/com/google/jetpackcamera/domain/camera/CameraXCameraUseCase.kt
@@ -24,6 +24,7 @@ import android.provider.MediaStore
import android.util.Log
import android.util.Range
import android.view.Display
+import androidx.camera.core.CameraEffect
import androidx.camera.core.CameraInfo
import androidx.camera.core.CameraSelector
import androidx.camera.core.ImageCapture
@@ -43,6 +44,7 @@ import androidx.camera.video.VideoCapture
import androidx.concurrent.futures.await
import androidx.core.content.ContextCompat
import com.google.jetpackcamera.domain.camera.CameraUseCase.ScreenFlashEvent.Type
+import com.google.jetpackcamera.domain.camera.effects.SingleSurfaceForcingEffect
import com.google.jetpackcamera.settings.SettingsRepository
import com.google.jetpackcamera.settings.model.AspectRatio
import com.google.jetpackcamera.settings.model.CameraAppSettings
@@ -221,7 +223,11 @@ constructor(
val useCaseGroup = createUseCaseGroup(
sessionSettings,
initialTransientSettings,
- supportedStabilizationModes
+ supportedStabilizationModes,
+ effect = when (sessionSettings.captureMode) {
+ CaptureMode.SINGLE_STREAM -> SingleSurfaceForcingEffect(coroutineScope)
+ CaptureMode.MULTI_STREAM -> null
+ }
)
var prevTransientSettings = initialTransientSettings
@@ -502,7 +508,8 @@ constructor(
private fun createUseCaseGroup(
sessionSettings: PerpetualSessionSettings,
initialTransientSettings: TransientSessionSettings,
- supportedStabilizationModes: List<SupportedStabilizationMode>
+ supportedStabilizationModes: List<SupportedStabilizationMode>,
+ effect: CameraEffect? = null
): UseCaseGroup {
val previewUseCase = createPreviewUseCase(sessionSettings, supportedStabilizationModes)
videoCaptureUseCase = createVideoUseCase(sessionSettings, supportedStabilizationModes)
@@ -523,9 +530,8 @@ constructor(
addUseCase(imageCaptureUseCase)
addUseCase(videoCaptureUseCase)
- if (sessionSettings.captureMode == CaptureMode.SINGLE_STREAM) {
- addEffect(SingleSurfaceForcingEffect())
- }
+ effect?.let { addEffect(it) }
+
captureMode = sessionSettings.captureMode
}.build()
}
diff --git a/domain/camera/src/main/java/com/google/jetpackcamera/domain/camera/EmptySurfaceProcessor.kt b/domain/camera/src/main/java/com/google/jetpackcamera/domain/camera/EmptySurfaceProcessor.kt
deleted file mode 100644
index 9263f80..0000000
--- a/domain/camera/src/main/java/com/google/jetpackcamera/domain/camera/EmptySurfaceProcessor.kt
+++ /dev/null
@@ -1,143 +0,0 @@
-/*
- * Copyright (C) 2023 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package com.google.jetpackcamera.domain.camera
-
-import android.annotation.SuppressLint
-import android.graphics.SurfaceTexture
-import android.os.Handler
-import android.os.HandlerThread
-import android.view.Surface
-import androidx.camera.core.DynamicRange
-import androidx.camera.core.SurfaceOutput
-import androidx.camera.core.SurfaceProcessor
-import androidx.camera.core.SurfaceRequest
-import androidx.camera.core.impl.utils.executor.CameraXExecutors.newHandlerExecutor
-import androidx.camera.core.processing.OpenGlRenderer
-import androidx.camera.core.processing.ShaderProvider
-import java.util.concurrent.Executor
-
-private const val GL_THREAD_NAME = "EmptySurfaceProcessor"
-
-/**
- * This is a [SurfaceProcessor] that passes on the same content from the input
- * surface to the output surface. Used to make a copies of surfaces.
- */
-@SuppressLint("RestrictedApi")
-class EmptySurfaceProcessor : SurfaceProcessor {
- private val glThread: HandlerThread = HandlerThread(GL_THREAD_NAME)
- private var glHandler: Handler
- var glExecutor: Executor
- private set
-
- // Members below are only accessed on GL thread.
- private val glRenderer: OpenGlRenderer = OpenGlRenderer()
- private val outputSurfaces: MutableMap<SurfaceOutput, Surface> = mutableMapOf()
- private val textureTransform: FloatArray = FloatArray(16)
- private val surfaceTransform: FloatArray = FloatArray(16)
- private var isReleased = false
-
- init {
- glThread.start()
- glHandler = Handler(glThread.looper)
- glExecutor = newHandlerExecutor(glHandler)
- glExecutor.execute {
- glRenderer.init(
- DynamicRange.SDR,
- ShaderProvider.DEFAULT
- )
- }
- }
-
- override fun onInputSurface(surfaceRequest: SurfaceRequest) {
- checkGlThread()
- if (isReleased) {
- surfaceRequest.willNotProvideSurface()
- return
- }
- val surfaceTexture = SurfaceTexture(glRenderer.textureName)
- surfaceTexture.setDefaultBufferSize(
- surfaceRequest.resolution.width,
- surfaceRequest.resolution.height
- )
- val surface = Surface(surfaceTexture)
- surfaceRequest.provideSurface(surface, glExecutor) {
- surfaceTexture.setOnFrameAvailableListener(null)
- surfaceTexture.release()
- surface.release()
- }
- surfaceTexture.setOnFrameAvailableListener({
- checkGlThread()
- if (!isReleased) {
- surfaceTexture.updateTexImage()
- surfaceTexture.getTransformMatrix(textureTransform)
- outputSurfaces.forEach { (surfaceOutput, surface) ->
- run {
- surfaceOutput.updateTransformMatrix(surfaceTransform, textureTransform)
- glRenderer.render(surfaceTexture.timestamp, surfaceTransform, surface)
- }
- }
- }
- }, glHandler)
- }
-
- override fun onOutputSurface(surfaceOutput: SurfaceOutput) {
- checkGlThread()
- if (isReleased) {
- surfaceOutput.close()
- return
- }
- val surface =
- surfaceOutput.getSurface(glExecutor) {
- surfaceOutput.close()
- outputSurfaces.remove(surfaceOutput)?.let { removedSurface ->
- glRenderer.unregisterOutputSurface(removedSurface)
- }
- }
- glRenderer.registerOutputSurface(surface)
- outputSurfaces[surfaceOutput] = surface
- }
-
- /**
- * Releases associated resources.
- *
- * Closes output surfaces.
- * Releases the [OpenGlRenderer].
- * Quits the GL HandlerThread.
- */
- fun release() {
- glExecutor.execute {
- releaseInternal()
- }
- }
-
- private fun releaseInternal() {
- checkGlThread()
- if (!isReleased) {
- // Once release is called, we can stop sending frame to output surfaces.
- for (surfaceOutput in outputSurfaces.keys) {
- surfaceOutput.close()
- }
- outputSurfaces.clear()
- glRenderer.release()
- glThread.quitSafely()
- isReleased = true
- }
- }
-
- private fun checkGlThread() {
- check(GL_THREAD_NAME == Thread.currentThread().name)
- }
-}
diff --git a/domain/camera/src/main/java/com/google/jetpackcamera/domain/camera/effects/CopyingSurfaceProcessor.kt b/domain/camera/src/main/java/com/google/jetpackcamera/domain/camera/effects/CopyingSurfaceProcessor.kt
new file mode 100644
index 0000000..6f626c1
--- /dev/null
+++ b/domain/camera/src/main/java/com/google/jetpackcamera/domain/camera/effects/CopyingSurfaceProcessor.kt
@@ -0,0 +1,361 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.jetpackcamera.domain.camera.effects
+
+import android.graphics.SurfaceTexture
+import android.opengl.EGL14
+import android.opengl.EGLConfig
+import android.opengl.EGLExt
+import android.opengl.EGLSurface
+import android.util.Size
+import android.view.Surface
+import androidx.camera.core.SurfaceOutput
+import androidx.camera.core.SurfaceProcessor
+import androidx.camera.core.SurfaceRequest
+import androidx.graphics.opengl.GLRenderer
+import androidx.graphics.opengl.egl.EGLManager
+import androidx.graphics.opengl.egl.EGLSpec
+import com.google.jetpackcamera.core.common.RefCounted
+import kotlin.coroutines.coroutineContext
+import kotlinx.coroutines.CompletableDeferred
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.CoroutineStart
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.Job
+import kotlinx.coroutines.Runnable
+import kotlinx.coroutines.SupervisorJob
+import kotlinx.coroutines.async
+import kotlinx.coroutines.cancel
+import kotlinx.coroutines.coroutineScope
+import kotlinx.coroutines.ensureActive
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.collectLatest
+import kotlinx.coroutines.flow.filterNot
+import kotlinx.coroutines.flow.filterNotNull
+import kotlinx.coroutines.flow.onCompletion
+import kotlinx.coroutines.flow.update
+import kotlinx.coroutines.launch
+
+private const val TIMESTAMP_UNINITIALIZED = -1L
+
+/**
+ * This is a [SurfaceProcessor] that passes on the same content from the input
+ * surface to the output surface. Used to make a copies of surfaces.
+ */
+class CopyingSurfaceProcessor(coroutineScope: CoroutineScope) : SurfaceProcessor {
+
+ private val inputSurfaceFlow = MutableStateFlow<SurfaceRequestScope?>(null)
+ private val outputSurfaceFlow = MutableStateFlow<SurfaceOutputScope?>(null)
+
+ init {
+ coroutineScope.launch(start = CoroutineStart.UNDISPATCHED) {
+ inputSurfaceFlow
+ .filterNotNull()
+ .collectLatest { surfaceRequestScope ->
+ surfaceRequestScope.withSurfaceRequest { surfaceRequest ->
+
+ val renderCallbacks = ShaderCopy(surfaceRequest.dynamicRange)
+ renderCallbacks.renderWithSurfaceRequest(surfaceRequest)
+ }
+ }
+ }
+ }
+
+ private suspend fun RenderCallbacks.renderWithSurfaceRequest(surfaceRequest: SurfaceRequest) =
+ coroutineScope inputScope@{
+ var currentTimestamp = TIMESTAMP_UNINITIALIZED
+ val surfaceTextureRef = RefCounted<SurfaceTexture> {
+ it.release()
+ }
+ val textureTransform = FloatArray(16)
+
+ val frameUpdateFlow = MutableStateFlow(0)
+
+ val initializeCallback = object : GLRenderer.EGLContextCallback {
+
+ override fun onEGLContextCreated(eglManager: EGLManager) {
+ initRenderer()
+
+ val surfaceTex = createSurfaceTexture(
+ surfaceRequest.resolution.width,
+ surfaceRequest.resolution.height
+ )
+
+ // Initialize the reference counted surface texture
+ surfaceTextureRef.initialize(surfaceTex)
+
+ surfaceTex.setOnFrameAvailableListener {
+ // Increment frame counter
+ frameUpdateFlow.update { it + 1 }
+ }
+
+ val inputSurface = Surface(surfaceTex)
+ surfaceRequest.provideSurface(inputSurface, Runnable::run) { result ->
+ inputSurface.release()
+ surfaceTextureRef.release()
+ this@inputScope.cancel(
+ "Input surface no longer receiving frames: $result"
+ )
+ }
+ }
+
+ override fun onEGLContextDestroyed(eglManager: EGLManager) {
+ // no-op
+ }
+ }
+
+ val glRenderer = GLRenderer(
+ eglSpecFactory = provideEGLSpec,
+ eglConfigFactory = initConfig
+ )
+ glRenderer.registerEGLContextCallback(initializeCallback)
+ glRenderer.start(glThreadName)
+
+ val inputRenderTarget = glRenderer.createRenderTarget(
+ surfaceRequest.resolution.width,
+ surfaceRequest.resolution.height,
+ object : GLRenderer.RenderCallback {
+
+ override fun onDrawFrame(eglManager: EGLManager) {
+ surfaceTextureRef.acquire()?.also {
+ try {
+ currentTimestamp =
+ if (currentTimestamp == TIMESTAMP_UNINITIALIZED) {
+ // Don't perform any updates on first draw,
+ // we're only setting up the context.
+ 0
+ } else {
+ it.updateTexImage()
+ it.getTransformMatrix(textureTransform)
+ it.timestamp
+ }
+ } finally {
+ surfaceTextureRef.release()
+ }
+ }
+ }
+ }
+ )
+
+ // Create the context and initialize the input. This will call RenderTarget.onDrawFrame,
+ // but we won't actually update the frame since this triggers adding the frame callback.
+ // All subsequent updates will then happen through frameUpdateFlow.
+ // This should be updated when https://issuetracker.google.com/331968279 is resolved.
+ inputRenderTarget.requestRender()
+
+ // Connect the onConnectToInput callback with the onDisconnectFromInput
+ // Should only be called on worker thread
+ var connectedToInput = false
+
+ // Should only be called on worker thread
+ val onConnectToInput: () -> Boolean = {
+ connectedToInput = surfaceTextureRef.acquire() != null
+ connectedToInput
+ }
+
+ // Should only be called on worker thread
+ val onDisconnectFromInput: () -> Unit = {
+ if (connectedToInput) {
+ surfaceTextureRef.release()
+ connectedToInput = false
+ }
+ }
+
+ // Wait for output surfaces
+ outputSurfaceFlow
+ .onCompletion {
+ glRenderer.stop(cancelPending = false)
+ glRenderer.unregisterEGLContextCallback(initializeCallback)
+ }.filterNotNull()
+ .collectLatest { surfaceOutputScope ->
+ surfaceOutputScope.withSurfaceOutput { refCountedSurface,
+ size,
+ updateTransformMatrix ->
+ // If we can't acquire the surface, then the surface output is already
+ // closed, so we'll return and wait for the next output surface.
+ val outputSurface =
+ refCountedSurface.acquire() ?: return@withSurfaceOutput
+
+ val surfaceTransform = FloatArray(16)
+ val outputRenderTarget = glRenderer.attach(
+ outputSurface,
+ size.width,
+ size.height,
+ object : GLRenderer.RenderCallback {
+
+ override fun onSurfaceCreated(
+ spec: EGLSpec,
+ config: EGLConfig,
+ surface: Surface,
+ width: Int,
+ height: Int
+ ): EGLSurface? {
+ return if (onConnectToInput()) {
+ createOutputSurface(spec, config, surface, width, height)
+ } else {
+ null
+ }
+ }
+
+ override fun onDrawFrame(eglManager: EGLManager) {
+ val currentDrawSurface = eglManager.currentDrawSurface
+ if (currentDrawSurface != eglManager.defaultSurface) {
+ updateTransformMatrix(
+ surfaceTransform,
+ textureTransform
+ )
+
+ drawFrame(
+ size.width,
+ size.height,
+ surfaceTransform
+ )
+
+ // Set timestamp
+ val display =
+ EGL14.eglGetDisplay(EGL14.EGL_DEFAULT_DISPLAY)
+ EGLExt.eglPresentationTimeANDROID(
+ display,
+ eglManager.currentDrawSurface,
+ currentTimestamp
+ )
+ }
+ }
+ }
+ )
+
+ frameUpdateFlow
+ .onCompletion {
+ outputRenderTarget.detach(cancelPending = false) {
+ onDisconnectFromInput()
+ refCountedSurface.release()
+ }
+ }.filterNot { it == 0 } // Don't attempt render on frame count 0
+ .collectLatest {
+ inputRenderTarget.requestRender()
+ outputRenderTarget.requestRender()
+ }
+ }
+ }
+ }
+
+ override fun onInputSurface(surfaceRequest: SurfaceRequest) {
+ val newScope = SurfaceRequestScope(surfaceRequest)
+ inputSurfaceFlow.update { old ->
+ old?.cancel("New SurfaceRequest received.")
+ newScope
+ }
+ }
+
+ override fun onOutputSurface(surfaceOutput: SurfaceOutput) {
+ val newScope = SurfaceOutputScope(surfaceOutput)
+ outputSurfaceFlow.update { old ->
+ old?.cancel("New SurfaceOutput received.")
+ newScope
+ }
+ }
+}
+
+interface RenderCallbacks {
+ val glThreadName: String
+ val provideEGLSpec: () -> EGLSpec
+ val initConfig: EGLManager.() -> EGLConfig
+ val initRenderer: () -> Unit
+ val createSurfaceTexture: (width: Int, height: Int) -> SurfaceTexture
+ val createOutputSurface: (
+ eglSpec: EGLSpec,
+ config: EGLConfig,
+ surface: Surface,
+ width: Int,
+ height: Int
+ ) -> EGLSurface
+ val drawFrame: (outputWidth: Int, outputHeight: Int, surfaceTransform: FloatArray) -> Unit
+}
+
+private class SurfaceOutputScope(val surfaceOutput: SurfaceOutput) {
+ private val surfaceLifecycleJob = SupervisorJob()
+ private val refCountedSurface = RefCounted<Surface>(onRelease = {
+ surfaceOutput.close()
+ }).apply {
+ // Ensure we don't release until after `initialize` has completed by deferring
+ // the release.
+ val deferredRelease = CompletableDeferred<Unit>()
+ initialize(
+ surfaceOutput.getSurface(Runnable::run) {
+ deferredRelease.complete(Unit)
+ }
+ )
+ CoroutineScope(Dispatchers.Unconfined).launch {
+ deferredRelease.await()
+ surfaceLifecycleJob.cancel("SurfaceOutput close requested.")
+ this@apply.release()
+ }
+ }
+
+ suspend fun <R> withSurfaceOutput(
+ block: suspend CoroutineScope.(
+ surface: RefCounted<Surface>,
+ surfaceSize: Size,
+ updateTransformMatrix: (updated: FloatArray, original: FloatArray) -> Unit
+ ) -> R
+ ): R {
+ return CoroutineScope(coroutineContext + Job(surfaceLifecycleJob)).async(
+ start = CoroutineStart.UNDISPATCHED
+ ) {
+ ensureActive()
+ block(
+ refCountedSurface,
+ surfaceOutput.size,
+ surfaceOutput::updateTransformMatrix
+ )
+ }.await()
+ }
+
+ fun cancel(message: String? = null) {
+ message?.apply { surfaceLifecycleJob.cancel(message) } ?: surfaceLifecycleJob.cancel()
+ }
+}
+
+private class SurfaceRequestScope(private val surfaceRequest: SurfaceRequest) {
+ private val requestLifecycleJob = SupervisorJob()
+
+ init {
+ surfaceRequest.addRequestCancellationListener(Runnable::run) {
+ requestLifecycleJob.cancel("SurfaceRequest cancelled.")
+ }
+ }
+
+ suspend fun <R> withSurfaceRequest(
+ block: suspend CoroutineScope.(
+ surfaceRequest: SurfaceRequest
+ ) -> R
+ ): R {
+ return CoroutineScope(coroutineContext + Job(requestLifecycleJob)).async(
+ start = CoroutineStart.UNDISPATCHED
+ ) {
+ ensureActive()
+ block(surfaceRequest)
+ }.await()
+ }
+
+ fun cancel(message: String? = null) {
+ message?.apply { requestLifecycleJob.cancel(message) } ?: requestLifecycleJob.cancel()
+ // Attempt to tell frame producer we will not provide a surface. This may fail (silently)
+ // if surface was already provided or the producer has cancelled the request, in which
+ // case we don't have to do anything.
+ surfaceRequest.willNotProvideSurface()
+ }
+}
diff --git a/domain/camera/src/main/java/com/google/jetpackcamera/domain/camera/effects/EGLSpecV14ES3.kt b/domain/camera/src/main/java/com/google/jetpackcamera/domain/camera/effects/EGLSpecV14ES3.kt
new file mode 100644
index 0000000..97918ed
--- /dev/null
+++ b/domain/camera/src/main/java/com/google/jetpackcamera/domain/camera/effects/EGLSpecV14ES3.kt
@@ -0,0 +1,46 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.jetpackcamera.domain.camera.effects
+
+import android.opengl.EGL14
+import android.opengl.EGLConfig
+import android.opengl.EGLContext
+import androidx.graphics.opengl.egl.EGLSpec
+
+val EGLSpec.Companion.V14ES3: EGLSpec
+ get() = object : EGLSpec by V14 {
+
+ private val contextAttributes = intArrayOf(
+ // GLES VERSION 3
+ EGL14.EGL_CONTEXT_CLIENT_VERSION,
+ 3,
+ // HWUI provides the ability to configure a context priority as well but that only
+ // seems to be configured on SystemUIApplication. This might be useful for
+ // front buffer rendering situations for performance.
+ EGL14.EGL_NONE
+ )
+
+ override fun eglCreateContext(config: EGLConfig): EGLContext {
+ return EGL14.eglCreateContext(
+ EGL14.eglGetDisplay(EGL14.EGL_DEFAULT_DISPLAY),
+ config,
+ // not creating from a shared context
+ EGL14.EGL_NO_CONTEXT,
+ contextAttributes,
+ 0
+ )
+ }
+ }
diff --git a/domain/camera/src/main/java/com/google/jetpackcamera/domain/camera/effects/ShaderCopy.kt b/domain/camera/src/main/java/com/google/jetpackcamera/domain/camera/effects/ShaderCopy.kt
new file mode 100644
index 0000000..5bb9812
--- /dev/null
+++ b/domain/camera/src/main/java/com/google/jetpackcamera/domain/camera/effects/ShaderCopy.kt
@@ -0,0 +1,450 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.jetpackcamera.domain.camera.effects
+
+import android.graphics.SurfaceTexture
+import android.opengl.EGL14
+import android.opengl.EGLConfig
+import android.opengl.EGLExt
+import android.opengl.GLES11Ext
+import android.opengl.GLES20
+import android.util.Log
+import android.view.Surface
+import androidx.annotation.WorkerThread
+import androidx.camera.core.DynamicRange
+import androidx.graphics.opengl.egl.EGLConfigAttributes
+import androidx.graphics.opengl.egl.EGLManager
+import androidx.graphics.opengl.egl.EGLSpec
+import java.nio.ByteBuffer
+import java.nio.ByteOrder
+import java.nio.FloatBuffer
+
+class ShaderCopy(private val dynamicRange: DynamicRange) : RenderCallbacks {
+
+ // Called on worker thread only
+ private var externalTextureId: Int = -1
+ private var programHandle = -1
+ private var texMatrixLoc = -1
+ private var positionLoc = -1
+ private var texCoordLoc = -1
+ private val use10bitPipeline: Boolean
+ get() = dynamicRange.bitDepth == DynamicRange.BIT_DEPTH_10_BIT
+
+ override val glThreadName: String
+ get() = TAG
+
+ override val provideEGLSpec: () -> EGLSpec
+ get() = { if (use10bitPipeline) EGLSpec.V14ES3 else EGLSpec.V14 }
+
+ override val initConfig: EGLManager.() -> EGLConfig
+ get() = {
+ checkNotNull(
+ loadConfig(
+ EGLConfigAttributes {
+ if (use10bitPipeline) {
+ TEN_BIT_REQUIRED_EGL_EXTENSIONS.forEach {
+ check(isExtensionSupported(it)) {
+ "Required extension for 10-bit HDR is not " +
+ "supported: $it"
+ }
+ }
+ include(EGLConfigAttributes.RGBA_1010102)
+ EGL14.EGL_RENDERABLE_TYPE to
+ EGLExt.EGL_OPENGL_ES3_BIT_KHR
+ EGL14.EGL_SURFACE_TYPE to
+ (EGL14.EGL_WINDOW_BIT or EGL14.EGL_PBUFFER_BIT)
+ } else {
+ include(EGLConfigAttributes.RGBA_8888)
+ }
+ }
+ )
+ ) {
+ "Unable to select EGLConfig"
+ }
+ }
+
+ override val initRenderer: () -> Unit
+ get() = {
+ createProgram(
+ if (use10bitPipeline) {
+ TEN_BIT_VERTEX_SHADER
+ } else {
+ DEFAULT_VERTEX_SHADER
+ },
+ if (use10bitPipeline) {
+ TEN_BIT_FRAGMENT_SHADER
+ } else {
+ DEFAULT_FRAGMENT_SHADER
+ }
+ )
+ loadLocations()
+ createTexture()
+ useAndConfigureProgram()
+ }
+
+ override val createSurfaceTexture
+ get() = { width: Int, height: Int ->
+ SurfaceTexture(externalTextureId).apply {
+ setDefaultBufferSize(width, height)
+ }
+ }
+
+ override val createOutputSurface
+ get() = { eglSpec: EGLSpec,
+ config: EGLConfig,
+ surface: Surface,
+ _: Int,
+ _: Int ->
+ eglSpec.eglCreateWindowSurface(
+ config,
+ surface,
+ EGLConfigAttributes {
+ if (use10bitPipeline) {
+ EGL_GL_COLORSPACE_KHR to EGL_GL_COLORSPACE_BT2020_HLG_EXT
+ }
+ }
+ )
+ }
+
+ override val drawFrame
+ get() = { outputWidth: Int,
+ outputHeight: Int,
+ surfaceTransform: FloatArray ->
+ GLES20.glViewport(
+ 0,
+ 0,
+ outputWidth,
+ outputHeight
+ )
+ GLES20.glScissor(
+ 0,
+ 0,
+ outputWidth,
+ outputHeight
+ )
+
+ GLES20.glUniformMatrix4fv(
+ texMatrixLoc,
+ /*count=*/
+ 1,
+ /*transpose=*/
+ false,
+ surfaceTransform,
+ /*offset=*/
+ 0
+ )
+ checkGlErrorOrThrow("glUniformMatrix4fv")
+
+ // Draw the rect.
+ GLES20.glDrawArrays(
+ GLES20.GL_TRIANGLE_STRIP,
+ /*firstVertex=*/
+ 0,
+ /*vertexCount=*/
+ 4
+ )
+ checkGlErrorOrThrow("glDrawArrays")
+ }
+
+ @WorkerThread
+ fun createTexture() {
+ checkGlThread()
+ val textures = IntArray(1)
+ GLES20.glGenTextures(1, textures, 0)
+ checkGlErrorOrThrow("glGenTextures")
+ val texId = textures[0]
+ GLES20.glBindTexture(GLES11Ext.GL_TEXTURE_EXTERNAL_OES, texId)
+ checkGlErrorOrThrow("glBindTexture $texId")
+ GLES20.glTexParameterf(
+ GLES11Ext.GL_TEXTURE_EXTERNAL_OES,
+ GLES20.GL_TEXTURE_MIN_FILTER,
+ GLES20.GL_NEAREST.toFloat()
+ )
+ GLES20.glTexParameterf(
+ GLES11Ext.GL_TEXTURE_EXTERNAL_OES,
+ GLES20.GL_TEXTURE_MAG_FILTER,
+ GLES20.GL_LINEAR.toFloat()
+ )
+ GLES20.glTexParameteri(
+ GLES11Ext.GL_TEXTURE_EXTERNAL_OES,
+ GLES20.GL_TEXTURE_WRAP_S,
+ GLES20.GL_CLAMP_TO_EDGE
+ )
+ GLES20.glTexParameteri(
+ GLES11Ext.GL_TEXTURE_EXTERNAL_OES,
+ GLES20.GL_TEXTURE_WRAP_T,
+ GLES20.GL_CLAMP_TO_EDGE
+ )
+ checkGlErrorOrThrow("glTexParameter")
+ externalTextureId = texId
+ }
+
+ @WorkerThread
+ fun useAndConfigureProgram() {
+ checkGlThread()
+ // Select the program.
+ GLES20.glUseProgram(programHandle)
+ checkGlErrorOrThrow("glUseProgram")
+
+ // Set the texture.
+ GLES20.glActiveTexture(GLES20.GL_TEXTURE0)
+ GLES20.glBindTexture(GLES11Ext.GL_TEXTURE_EXTERNAL_OES, externalTextureId)
+
+ // Enable the "aPosition" vertex attribute.
+ GLES20.glEnableVertexAttribArray(positionLoc)
+ checkGlErrorOrThrow("glEnableVertexAttribArray")
+
+ // Connect vertexBuffer to "aPosition".
+ val coordsPerVertex = 2
+ val vertexStride = 0
+ GLES20.glVertexAttribPointer(
+ positionLoc,
+ coordsPerVertex,
+ GLES20.GL_FLOAT,
+ /*normalized=*/
+ false,
+ vertexStride,
+ VERTEX_BUF
+ )
+ checkGlErrorOrThrow("glVertexAttribPointer")
+
+ // Enable the "aTextureCoord" vertex attribute.
+ GLES20.glEnableVertexAttribArray(texCoordLoc)
+ checkGlErrorOrThrow("glEnableVertexAttribArray")
+
+ // Connect texBuffer to "aTextureCoord".
+ val coordsPerTex = 2
+ val texStride = 0
+ GLES20.glVertexAttribPointer(
+ texCoordLoc,
+ coordsPerTex,
+ GLES20.GL_FLOAT,
+ /*normalized=*/
+ false,
+ texStride,
+ TEX_BUF
+ )
+ checkGlErrorOrThrow("glVertexAttribPointer")
+ }
+
+ @WorkerThread
+ private fun createProgram(vertShader: String, fragShader: String) {
+ checkGlThread()
+ var vertexShader = -1
+ var fragmentShader = -1
+ var program = -1
+ try {
+ fragmentShader = loadShader(
+ GLES20.GL_FRAGMENT_SHADER,
+ fragShader
+ )
+ vertexShader = loadShader(
+ GLES20.GL_VERTEX_SHADER,
+ vertShader
+ )
+ program = GLES20.glCreateProgram()
+ checkGlErrorOrThrow("glCreateProgram")
+ GLES20.glAttachShader(program, vertexShader)
+ checkGlErrorOrThrow("glAttachShader")
+ GLES20.glAttachShader(program, fragmentShader)
+ checkGlErrorOrThrow("glAttachShader")
+ GLES20.glLinkProgram(program)
+ val linkStatus = IntArray(1)
+ GLES20.glGetProgramiv(
+ program,
+ GLES20.GL_LINK_STATUS,
+ linkStatus,
+ /*offset=*/
+ 0
+ )
+ check(linkStatus[0] == GLES20.GL_TRUE) {
+ "Could not link program: " + GLES20.glGetProgramInfoLog(
+ program
+ )
+ }
+ programHandle = program
+ } catch (e: Exception) {
+ if (vertexShader != -1) {
+ GLES20.glDeleteShader(vertexShader)
+ }
+ if (fragmentShader != -1) {
+ GLES20.glDeleteShader(fragmentShader)
+ }
+ if (program != -1) {
+ GLES20.glDeleteProgram(program)
+ }
+ throw e
+ }
+ }
+
+ @WorkerThread
+ private fun loadLocations() {
+ checkGlThread()
+ positionLoc = GLES20.glGetAttribLocation(programHandle, "aPosition")
+ checkLocationOrThrow(positionLoc, "aPosition")
+ texCoordLoc = GLES20.glGetAttribLocation(programHandle, "aTextureCoord")
+ checkLocationOrThrow(texCoordLoc, "aTextureCoord")
+ texMatrixLoc = GLES20.glGetUniformLocation(programHandle, "uTexMatrix")
+ checkLocationOrThrow(texMatrixLoc, "uTexMatrix")
+ }
+
+ @WorkerThread
+ private fun loadShader(shaderType: Int, source: String): Int {
+ checkGlThread()
+ val shader = GLES20.glCreateShader(shaderType)
+ checkGlErrorOrThrow("glCreateShader type=$shaderType")
+ GLES20.glShaderSource(shader, source)
+ GLES20.glCompileShader(shader)
+ val compiled = IntArray(1)
+ GLES20.glGetShaderiv(
+ shader,
+ GLES20.GL_COMPILE_STATUS,
+ compiled,
+ /*offset=*/
+ 0
+ )
+ check(compiled[0] == GLES20.GL_TRUE) {
+ Log.w(TAG, "Could not compile shader: $source")
+ try {
+ return@check "Could not compile shader type " +
+ "$shaderType: ${GLES20.glGetShaderInfoLog(shader)}"
+ } finally {
+ GLES20.glDeleteShader(shader)
+ }
+ }
+ return shader
+ }
+
+ @WorkerThread
+ private fun checkGlErrorOrThrow(op: String) {
+ val error = GLES20.glGetError()
+ check(error == GLES20.GL_NO_ERROR) { op + ": GL error 0x" + Integer.toHexString(error) }
+ }
+
+ private fun checkLocationOrThrow(location: Int, label: String) {
+ check(location >= 0) { "Unable to locate '$label' in program" }
+ }
+
+ companion object {
+ private const val SIZEOF_FLOAT = 4
+
+ private val VERTEX_BUF = floatArrayOf(
+ // 0 bottom left
+ -1.0f,
+ -1.0f,
+ // 1 bottom right
+ 1.0f,
+ -1.0f,
+ // 2 top left
+ -1.0f,
+ 1.0f,
+ // 3 top right
+ 1.0f,
+ 1.0f
+ ).toBuffer()
+
+ private val TEX_BUF = floatArrayOf(
+ // 0 bottom left
+ 0.0f,
+ 0.0f,
+ // 1 bottom right
+ 1.0f,
+ 0.0f,
+ // 2 top left
+ 0.0f,
+ 1.0f,
+ // 3 top right
+ 1.0f,
+ 1.0f
+ ).toBuffer()
+
+ private const val TAG = "ShaderCopy"
+ private const val GL_THREAD_NAME = TAG
+
+ private const val VAR_TEXTURE_COORD = "vTextureCoord"
+ private val DEFAULT_VERTEX_SHADER =
+ """
+ uniform mat4 uTexMatrix;
+ attribute vec4 aPosition;
+ attribute vec4 aTextureCoord;
+ varying vec2 $VAR_TEXTURE_COORD;
+ void main() {
+ gl_Position = aPosition;
+ $VAR_TEXTURE_COORD = (uTexMatrix * aTextureCoord).xy;
+ }
+ """.trimIndent()
+
+ private val TEN_BIT_VERTEX_SHADER =
+ """
+ #version 300 es
+ in vec4 aPosition;
+ in vec4 aTextureCoord;
+ uniform mat4 uTexMatrix;
+ out vec2 $VAR_TEXTURE_COORD;
+ void main() {
+ gl_Position = aPosition;
+ $VAR_TEXTURE_COORD = (uTexMatrix * aTextureCoord).xy;
+ }
+ """.trimIndent()
+
+ private const val VAR_TEXTURE = "sTexture"
+ private val DEFAULT_FRAGMENT_SHADER =
+ """
+ #extension GL_OES_EGL_image_external : require
+ precision mediump float;
+ varying vec2 $VAR_TEXTURE_COORD;
+ uniform samplerExternalOES $VAR_TEXTURE;
+ void main() {
+ gl_FragColor = texture2D($VAR_TEXTURE, $VAR_TEXTURE_COORD);
+ }
+ """.trimIndent()
+
+ private val TEN_BIT_FRAGMENT_SHADER =
+ """
+ #version 300 es
+ #extension GL_EXT_YUV_target : require
+ precision mediump float;
+ uniform __samplerExternal2DY2YEXT $VAR_TEXTURE;
+ in vec2 $VAR_TEXTURE_COORD;
+ layout (yuv) out vec3 outColor;
+
+ void main() {
+ outColor = texture($VAR_TEXTURE, $VAR_TEXTURE_COORD).xyz;
+ }
+ """.trimIndent()
+
+ private const val EGL_GL_COLORSPACE_KHR = 0x309D
+ private const val EGL_GL_COLORSPACE_BT2020_HLG_EXT = 0x3540
+
+ private val TEN_BIT_REQUIRED_EGL_EXTENSIONS = listOf(
+ "EGL_EXT_gl_colorspace_bt2020_hlg",
+ "EGL_EXT_yuv_surface"
+ )
+
+ private fun FloatArray.toBuffer(): FloatBuffer {
+ val bb = ByteBuffer.allocateDirect(size * SIZEOF_FLOAT)
+ bb.order(ByteOrder.nativeOrder())
+ val fb = bb.asFloatBuffer()
+ fb.put(this)
+ fb.position(0)
+ return fb
+ }
+
+ private fun checkGlThread() {
+ check(GL_THREAD_NAME == Thread.currentThread().name)
+ }
+ }
+}
diff --git a/domain/camera/src/main/java/com/google/jetpackcamera/domain/camera/SingleSurfaceForcingEffect.kt b/domain/camera/src/main/java/com/google/jetpackcamera/domain/camera/effects/SingleSurfaceForcingEffect.kt
index 09c691d..6057b89 100644
--- a/domain/camera/src/main/java/com/google/jetpackcamera/domain/camera/SingleSurfaceForcingEffect.kt
+++ b/domain/camera/src/main/java/com/google/jetpackcamera/domain/camera/effects/SingleSurfaceForcingEffect.kt
@@ -1,5 +1,5 @@
/*
- * Copyright (C) 2023 The Android Open Source Project
+ * Copyright (C) 2024 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -13,14 +13,13 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-package com.google.jetpackcamera.domain.camera
+package com.google.jetpackcamera.domain.camera.effects
import androidx.camera.core.CameraEffect
+import kotlinx.coroutines.CoroutineScope
private const val TARGETS =
- CameraEffect.PREVIEW or CameraEffect.VIDEO_CAPTURE or CameraEffect.IMAGE_CAPTURE
-
-private val emptySurfaceProcessor = EmptySurfaceProcessor()
+ CameraEffect.PREVIEW or CameraEffect.VIDEO_CAPTURE
/**
* [CameraEffect] that applies a no-op effect.
@@ -30,15 +29,9 @@ private val emptySurfaceProcessor = EmptySurfaceProcessor()
*
* Used as a workaround to force the above 3 use cases to use a single camera stream.
*/
-class SingleSurfaceForcingEffect : CameraEffect(
+class SingleSurfaceForcingEffect(coroutineScope: CoroutineScope) : CameraEffect(
TARGETS,
- emptySurfaceProcessor.glExecutor,
- emptySurfaceProcessor,
+ Runnable::run,
+ CopyingSurfaceProcessor(coroutineScope),
{}
-) {
- // TODO(b/304547401): Invoke this to release the processor properly
- @SuppressWarnings("unused")
- fun release() {
- emptySurfaceProcessor.release()
- }
-}
+)
diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml
index aa7998f..8096d91 100644
--- a/gradle/libs.versions.toml
+++ b/gradle/libs.versions.toml
@@ -6,6 +6,7 @@ androidxActivityCompose = "1.8.2"
androidxAppCompat = "1.6.1"
androidxCore = "1.12.0"
androidxCoreKtx = "1.12.0"
+androidxGraphicsCore = "1.0.0-beta01"
androidxLifecycle = "2.7.0"
androidxTracing = "1.2.0"
atomicfu = "0.23.2"
@@ -44,6 +45,7 @@ androidx-core = { module = "androidx.core:core", version.ref = "androidxCore" }
androidx-core-ktx = { module = "androidx.core:core-ktx", version.ref = "androidxCoreKtx" }
androidx-datastore = { module = "androidx.datastore:datastore", version.ref = "datastore" }
androidx-espresso-core = { module = "androidx.test.espresso:espresso-core", version.ref = "espressoCore" }
+androidx-graphics-core = { module = "androidx.graphics:graphics-core", version.ref = "androidxGraphicsCore" }
androidx-junit = { module = "androidx.test.ext:junit", version.ref = "androidJunit" }
androidx-lifecycle-viewmodel-compose = { module = "androidx.lifecycle:lifecycle-viewmodel-compose", version.ref = "androidxLifecycle" }
androidx-lifecycle-runtime-compose = { module = "androidx.lifecycle:lifecycle-runtime-compose", version.ref = "androidxLifecycle" }