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.lowlatency

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.view.Surface
import androidx.annotation.RequiresApi
import androidx.graphics.opengl.GLRenderer
import androidx.graphics.opengl.egl.EGLManager
import androidx.graphics.opengl.egl.EGLSpec
import java.util.concurrent.atomic.AtomicBoolean

/**
 * [GLRenderer.RenderCallback] implementation that renders content into a frame buffer object
 * backed by a [HardwareBuffer] object
 */
@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 = syncStrategy.createSyncFence(egl)

            // 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)
        } finally {
            syncFenceCompat?.close()
        }
    }

    /**
     * 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
         * callers 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]
         */
        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.
         * @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?)
    }
}

/**
 * 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 eglSpec.createNativeSyncFence()
            }
        }
    }
}

/**
 * [SyncStrategy] implementation that optimizes for front buffered rendering use cases.
 * More specifically this attempts to avoid unnecessary synchronization overhead
 * wherever possible.
 *
 * This will always provide a fence if the corresponding layer transitions from
 * an invisible to a visible state. If the layer is already visible and front
 * buffer usage flags are support on the device, then no fence is provided. If this
 * flag is not supported, then a fence is created to ensure contents
 * are flushed to the single buffer.
 *
 * @param usageFlags usage flags that describe the [HardwareBuffer] that is used as the destination
 * for rendering content within [FrameBufferRenderer]. The usage flags can be obtained via
 * [HardwareBuffer.getUsage] or by passing in the same flags from [HardwareBuffer.create]
 */
class FrontBufferSyncStrategy(
    usageFlags: Long
) : SyncStrategy {
    private val supportsFrontBufferUsage = (usageFlags and HardwareBuffer.USAGE_FRONT_BUFFER) != 0L
    private var mFrontBufferVisible: Boolean = false

    /**
     * Tells whether the corresponding front buffer layer is visible in its current state or not.
     * Utilize this to dictate when a [SyncFenceCompat] will be created when using
     * [createSyncFence].
     */
    var isVisible
        get() = mFrontBufferVisible
        set(visibility) {
            mFrontBufferVisible = visibility
        }

    /**
     * Creates a [SyncFenceCompat] based on various conditions.
     * If the layer is changing from invisible to visible, a fence is provided.
     * If the layer is already visible and front buffer usage flag is supported on the device, then
     * no fence is provided.
     * If front buffer usage is not supported, then a fence is created and destroyed to flush
     * contents to screen.
     */
    @RequiresApi(Build.VERSION_CODES.KITKAT)
    override fun createSyncFence(eglSpec: EGLSpec): SyncFenceCompat? {
        return if (!isVisible) {
            eglSpec.createNativeSyncFence()
        } else if (supportsFrontBufferUsage) {
            return null
        } else {
            val fence = eglSpec.createNativeSyncFence()
            fence.close()
            return null
        }
    }
}