FrameBufferRenderer.kt
/*
* Copyright 2022 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 androidx.graphics.opengl
import android.annotation.SuppressLint
import android.hardware.HardwareBuffer
import android.opengl.EGLConfig
import android.opengl.EGLSurface
import android.opengl.GLES20
import android.os.Build
import android.util.Log
import android.view.Surface
import androidx.annotation.RequiresApi
import androidx.graphics.opengl.egl.EGLManager
import androidx.graphics.opengl.egl.EGLSpec
import androidx.hardware.SyncFenceCompat
import androidx.opengl.EGLExt
import androidx.opengl.EGLExt.Companion.EGL_ANDROID_NATIVE_FENCE_SYNC
import androidx.opengl.EGLExt.Companion.EGL_KHR_FENCE_SYNC
import java.util.concurrent.atomic.AtomicBoolean
/**
* [GLRenderer.RenderCallback] implementation that renders content into a frame buffer object
* backed by a [HardwareBuffer] object
*
* @param frameBufferRendererCallbacks Callbacks to provide a [FrameBuffer] instance to render into,
* draw method to render into the [FrameBuffer] as well as a subsequent callback to consume the
* contents of the [FrameBuffer]
* @param syncStrategy [SyncStrategy] used to determine when a fence is to be created to gate on
* consumption of the [FrameBuffer] instance. This determines if a [SyncFenceCompat] instance is
* provided in the [RenderCallback.onDrawComplete] depending on the use case.
* For example for front buffered rendering scenarios, it is possible that no [SyncFenceCompat] is
* provided in order to reduce latency within the rendering pipeline.
*
* This API can be used to render content into a [HardwareBuffer] directly and convert that to a
* bitmap with the following code snippet:
*
* ```
* val glRenderer = GLRenderer().apply { start() }
* val callbacks = object : FrameBufferRenderer.RenderCallback {
*
* override fun obtainFrameBuffer(egl: EGLSpec): FrameBuffer =
* FrameBuffer(
* egl,
* HardwareBuffer.create(
* width,
* height,
* HardwareBuffer.RGBA_8888,
* 1,
* HardwareBuffer.USAGE_GPU_SAMPLED_IMAGE
* )
* )
*
* override fun onDraw(eglManager: EGLManager) {
* // GL code
* }
*
* override fun onDrawComplete(frameBuffer: FrameBuffer, syncFenceCompat: SyncFenceCompat?) {
* syncFenceCompat?.awaitForever()
* val bitmap = Bitmap.wrapHardwareBuffer(frameBuffer.hardwareBuffer,
* ColorSpace.get(ColorSpace.Named.LINEAR_SRGB))
* // bitmap operations
* }
* }
*
* glRenderer.createRenderTarget(width,height, FrameBufferRenderer(callbacks)).requestRender()
* ```
*/
@RequiresApi(Build.VERSION_CODES.O)
class FrameBufferRenderer(
private val frameBufferRendererCallbacks: RenderCallback,
@SuppressLint("ListenerLast") private val syncStrategy: SyncStrategy = SyncStrategy.ALWAYS
) : GLRenderer.RenderCallback {
private val mClear = AtomicBoolean(false)
override fun onSurfaceCreated(
spec: EGLSpec,
config: EGLConfig,
surface: Surface,
width: Int,
height: Int
): EGLSurface? = null
fun clear() {
mClear.set(true)
}
override fun onDrawFrame(eglManager: EGLManager) {
val egl = eglManager.eglSpec
val buffer = frameBufferRendererCallbacks.obtainFrameBuffer(egl)
var syncFenceCompat: SyncFenceCompat? = null
try {
buffer.makeCurrent()
if (mClear.getAndSet(false)) {
GLES20.glClearColor(0.0f, 0.0f, 0.0f, 0.0f)
GLES20.glClear(GLES20.GL_COLOR_BUFFER_BIT)
} else {
frameBufferRendererCallbacks.onDraw(eglManager)
}
syncFenceCompat = if (eglManager.supportsNativeAndroidFence()) {
syncStrategy.createSyncFence(egl)
} else if (eglManager.isExtensionSupported(EGL_KHR_FENCE_SYNC)) {
// In this case the device only supports EGL sync objects but not creation
// of native SyncFence objects from an EGLSync.
// This usually occurs in emulator/cuttlefish instances as well as ChromeOS devices
// running ARC++. In this case fallback onto creating a sync object and waiting
// on it instead.
// TODO b/256217036 block on another thread instead of waiting here
val syncKhr = egl.eglCreateSyncKHR(EGLExt.EGL_SYNC_FENCE_KHR, null)
if (syncKhr != null) {
GLES20.glFlush()
val status = egl.eglClientWaitSyncKHR(
syncKhr,
EGLExt.EGL_SYNC_FLUSH_COMMANDS_BIT_KHR,
EGLExt.EGL_FOREVER_KHR
)
if (status != EGLExt.EGL_CONDITION_SATISFIED_KHR) {
Log.w(TAG, "warning waiting on sync object: $status")
}
} else {
Log.w(TAG, "Unable to create EGLSync")
GLES20.glFinish()
}
null
} else {
Log.w(TAG, "Device does not support creation of any fences")
GLES20.glFinish()
null
}
} catch (exception: Exception) {
Log.w(TAG, "Error attempting to render to frame buffer: ${exception.message}")
} finally {
// At this point the HardwareBuffer has the contents of the GL rendering
// Create a surface Control transaction to dispatch this request
frameBufferRendererCallbacks.onDrawComplete(buffer, syncFenceCompat)
}
}
private fun EGLManager.supportsNativeAndroidFence(): Boolean =
isExtensionSupported(EGL_KHR_FENCE_SYNC) &&
isExtensionSupported(EGL_ANDROID_NATIVE_FENCE_SYNC)
/**
* Callbacks invoked to render content leveraging a [FrameBufferRenderer]
*/
interface RenderCallback {
/**
* Obtain a [FrameBuffer] to render content into. The [FrameBuffer] obtained here
* is expected to be managed by the consumer of [FrameBufferRenderer]. That is
* implementations of this API are expected to be maintaining a reference to the returned
* [FrameBuffer] here and calling [FrameBuffer.close] where appropriate as the instance
* will not be released by [FrameBufferRenderer].
*
* @param egl EGLSpec that is utilized within creation of the [FrameBuffer] object
*/
@SuppressLint("CallbackMethodName")
fun obtainFrameBuffer(egl: EGLSpec): FrameBuffer
/**
* Draw contents into the [HardwareBuffer]. Before this method is invoked the [FrameBuffer]
* instance returned in [obtainFrameBuffer] is made current
*/
fun onDraw(eglManager: EGLManager)
/**
* Callback when [onDraw] is complete and the contents of the draw
* are reflected in the corresponding [HardwareBuffer].
*
* @param frameBuffer [FrameBuffer] that content is rendered into. The frameBuffer
* should not be consumed unless the syncFenceCompat is signalled or the fence is null.
* This is the same [FrameBuffer] instance returned in [obtainFrameBuffer]
* @param syncFenceCompat [SyncFenceCompat] is used to determine when rendering
* is done in [onDraw] and reflected within the given frameBuffer.
*/
fun onDrawComplete(frameBuffer: FrameBuffer, syncFenceCompat: SyncFenceCompat?)
}
private companion object {
private const val TAG = "FrameBufferRenderer"
}
}
/**
* A strategy class for deciding how to utilize [SyncFenceCompat] within
* [FrameBufferRenderer.RenderCallback]. SyncStrategy provides default strategies for
* usage:
*
* [SyncStrategy.ALWAYS] will always create a [SyncFenceCompat] to pass into the render
* callbacks for [FrameBufferRenderer]
*/
interface SyncStrategy {
/**
* Conditionally generates a [SyncFenceCompat] based upon implementation.
*
* @param eglSpec an [EGLSpec] object to dictate the version of EGL and make EGL calls.
*/
fun createSyncFence(eglSpec: EGLSpec): SyncFenceCompat?
companion object {
/**
* [SyncStrategy] that will always create a [SyncFenceCompat] object
*/
@JvmField
val ALWAYS = object : SyncStrategy {
override fun createSyncFence(eglSpec: EGLSpec): SyncFenceCompat? {
return SyncFenceCompat.createNativeSyncFence()
}
}
}
}