SingleBufferedCanvasRendererV29.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.graphics.Canvas
import android.graphics.SurfaceTexture
import android.hardware.HardwareBuffer
import android.opengl.GLES20
import android.opengl.Matrix
import android.os.Build
import android.util.Log
import androidx.annotation.RequiresApi
import androidx.graphics.lowlatency.FrontBufferUtils.Companion.obtainHardwareBufferUsageFlags
import androidx.graphics.opengl.FrameBuffer
import androidx.graphics.opengl.FrameBufferRenderer
import androidx.graphics.opengl.GLRenderer
import androidx.graphics.opengl.QuadTextureRenderer
import androidx.graphics.opengl.egl.EGLManager
import androidx.graphics.opengl.egl.EGLSpec
import androidx.hardware.SyncFenceCompat
import java.nio.IntBuffer
import java.util.concurrent.Executor
import java.util.concurrent.atomic.AtomicBoolean

@RequiresApi(Build.VERSION_CODES.Q)
internal class SingleBufferedCanvasRendererV29<T>(
    private val width: Int,
    private val height: Int,
    private val bufferTransformer: BufferTransformer,
    private val executor: Executor,
    private val callbacks: SingleBufferedCanvasRenderer.RenderCallbacks<T>,
) : SingleBufferedCanvasRenderer<T> {

    private val mProducer = TextureProducer<T>(
        width,
        height,
        object : TextureProducer.Callbacks<T> {
            override fun onTextureAvailable(texture: SurfaceTexture) {
                mSurfaceTexture = texture
                mFrameBufferTarget.requestRender()
            }

            override fun render(canvas: Canvas, width: Int, height: Int, param: T) {
                callbacks.render(canvas, width, height, param)
            }
        })

    /**
     * Source SurfaceTexture that the destination of content to be rendered from the provided
     * RenderNode
     */
    private var mSurfaceTexture: SurfaceTexture? = null

    /**
     * HardwareBuffer flags for front buffered rendering
     */
    private val mHardwareBufferUsageFlags = obtainHardwareBufferUsageFlags()

    // ---------- GLThread ------

    /**
     * [FrontBufferSyncStrategy] used for [FrameBufferRenderer] to conditionally decide
     * when to create a [SyncFenceCompat] for transaction calls.
     */
    private val mFrontBufferSyncStrategy = FrontBufferSyncStrategy(mHardwareBufferUsageFlags)

    /**
     * Shader that handles rendering a texture as a quad into the destination
     */
    private var mQuadRenderer: QuadTextureRenderer? = null

    /**
     * Texture id of the SurfaceTexture that is to be rendered
     */
    private var mTextureId: Int = -1

    /**
     * Scratch buffer used for gen/delete texture operations
     */
    private val buffer = IntArray(1)

    // ---------- GLThread ------

    private val mFrameBufferRenderer = FrameBufferRenderer(
        object : FrameBufferRenderer.RenderCallback {

            private val mMVPMatrix = FloatArray(16)
            private val mProjection = FloatArray(16)

            private fun obtainQuadRenderer(): QuadTextureRenderer =
                mQuadRenderer ?: QuadTextureRenderer().apply {
                    GLES20.glGenTextures(1, buffer, 0)
                    mTextureId = buffer[0]
                    mSurfaceTexture?.let { texture ->
                        texture.attachToGLContext(mTextureId)
                        setSurfaceTexture(texture)
                    }
                    mQuadRenderer = this
                }

            override fun obtainFrameBuffer(egl: EGLSpec): FrameBuffer {
                return mFrontBufferLayer ?: FrameBuffer(
                    egl,
                    HardwareBuffer.create(
                        bufferTransformer.glWidth,
                        bufferTransformer.glHeight,
                        HardwareBuffer.RGBA_8888,
                        1,
                        mHardwareBufferUsageFlags
                    )
                ).also { mFrontBufferLayer = it }
            }

            @RequiresApi(Build.VERSION_CODES.S)
            override fun onDraw(eglManager: EGLManager) {
                mSurfaceTexture?.let { texture ->
                    val bufferWidth = bufferTransformer.glWidth
                    val bufferHeight = bufferTransformer.glHeight
                    GLES20.glViewport(0, 0, bufferWidth, bufferHeight)
                    Matrix.orthoM(
                        mMVPMatrix,
                        0,
                        0f,
                        bufferWidth.toFloat(),
                        0f,
                        bufferHeight.toFloat(),
                        -1f,
                        1f
                    )

                    Matrix.multiplyMM(mProjection, 0, mMVPMatrix, 0, bufferTransformer.transform, 0)
                    // texture.updateTexImage is called within QuadTextureRenderer#draw
                    obtainQuadRenderer().draw(mProjection, width.toFloat(), height.toFloat())
                    texture.releaseTexImage()
                    mProducer.markTextureConsumed()
                }
            }

            override fun onDrawComplete(
                frameBuffer: FrameBuffer,
                syncFenceCompat: SyncFenceCompat?
            ) {
                if (forceFlush.get()) {
                    // See b/236394768. On some ANGLE versions, attempting to do a glClear + flush
                    // does not actually flush pixels to FBOs with HardwareBuffer attachments
                    // For testing purposes when verifying clear use cases, do a GPU readback
                    // to actually executed any pending clear operations to verify output
                    GLES20.glReadPixels(0, 0, 1, 1, GLES20.GL_RGBA, GLES20.GL_UNSIGNED_BYTE,
                        IntBuffer.wrap(IntArray(1)))
                }

                mProducer.execute {
                    callbacks.onBufferReady(frameBuffer.hardwareBuffer, syncFenceCompat)
                }
            }
        },
        mFrontBufferSyncStrategy
    )

    /**
     * [GLRenderer] used to render contents of the SurfaceTexture into a HardwareBuffer
     */
    private val mGLRenderer = GLRenderer().apply { start() }

    private var mFrameBufferTarget: GLRenderer.RenderTarget =
        mGLRenderer.createRenderTarget(width, height, mFrameBufferRenderer)

    /**
     * Flag to maintain visibility state on the main thread
     */
    private var mIsVisible = false

    /**
     * Flag to determine if the SingleBufferedCanvasRenderer instance has been released
     */
    private var mIsReleased = false

    override var isVisible: Boolean
        set(value) {
            mGLRenderer.execute {
                mFrontBufferSyncStrategy.isVisible = value
            }
            mIsVisible = value
        }
        get() = mFrontBufferSyncStrategy.isVisible

    private var mFrontBufferLayer: FrameBuffer? = null

    override fun render(param: T) {
        if (!mIsReleased) {
            mProducer.requestRender(param)
        } else {
            Log.w(TAG, "Attempt to render with CanvasRenderer that has already been released")
        }
    }

    override fun release(cancelPending: Boolean, onReleaseComplete: (() -> Unit)?) {
        if (!mIsReleased) {
            if (cancelPending) {
                mProducer.cancelPending()
            }
            mProducer.release(cancelPending) {
                // If the producer is torn down after all pending requests are completed
                // then there is nothing left for the render target to consume so
                // detach immediately
                mFrameBufferTarget.detach(true) {
                    // GL Thread
                    mQuadRenderer?.release()
                    if (mTextureId != -1) {
                        buffer[0] = mTextureId
                        GLES20.glDeleteTextures(1, buffer, 0)
                        mTextureId = -1
                    }
                }
                mGLRenderer.stop(true)
                onReleaseComplete?.invoke()
           }

            mIsReleased = true
        } else {
            Log.w(TAG, "Attempt to release CanvasRenderer that has already been released")
        }
    }

    private val mClearRunnable = Runnable {
        mFrameBufferRenderer.clear()
        mFrameBufferTarget.requestRender()
    }

    override fun cancelPending() {
        if (!mIsReleased) {
            mProducer.cancelPending()
        } else {
            Log.w(TAG, "Attempt to cancel pending requests when the CanvasRender has " +
                "already been released")
        }
    }

    override fun clear() {
        if (!mIsReleased) {
            mProducer.execute(mClearRunnable)
        } else {
            Log.w(TAG, "Attempt to clear contents when the CanvasRenderer has already " +
                "been released")
        }
    }

    // See: b/236394768. Some test emulator instances have not picked up the fix so
    // apply a workaround here for testing purposes
    internal val forceFlush = AtomicBoolean(false)

    private companion object {

        const val TAG = "SingleBufferedCanvasV29"
    }
}