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.hardware.SyncFenceCompat
import androidx.graphics.opengl.egl.EGLManager
import androidx.graphics.opengl.egl.EGLSpec
import androidx.opengl.EGLExt
import java.util.concurrent.atomic.AtomicBoolean
import androidx.opengl.EGLExt.Companion.EGL_ANDROID_NATIVE_FENCE_SYNC
import androidx.opengl.EGLExt.Companion.EGL_KHR_FENCE_SYNC

/**
 * [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.
     */
    @RequiresApi(Build.VERSION_CODES.KITKAT)
    fun createSyncFence(eglSpec: EGLSpec): SyncFenceCompat?

    companion object {
        /**
         * [SyncStrategy] that will always create a [SyncFenceCompat] object
         */
        @JvmField
        val ALWAYS = object : SyncStrategy {
            @RequiresApi(Build.VERSION_CODES.KITKAT)
            override fun createSyncFence(eglSpec: EGLSpec): SyncFenceCompat? {
                return SyncFenceCompat.createNativeSyncFence()
            }
        }
    }
}