blob: 98cd2bc5ede32e3301221883e0983cc9dd0db1be [file] [log] [blame]
/*
* 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.graphics.Canvas
import android.graphics.RenderNode
import android.hardware.HardwareBuffer
import android.os.Build
import android.util.Log
import android.view.SurfaceHolder
import android.view.SurfaceView
import androidx.annotation.RequiresApi
import androidx.annotation.WorkerThread
import androidx.graphics.MultiBufferedCanvasRenderer
import androidx.graphics.surface.SurfaceControlCompat
import androidx.hardware.SyncFenceCompat
import java.util.concurrent.CountDownLatch
import java.util.concurrent.Executors
/**
* Class responsible for supporting a "front buffered" rendering system. This allows for lower
* latency graphics by leveraging a combination of front buffered and multi buffered content layers.
* Active content is rendered first into the front buffered layer which is simultaneously being
* presented to the display. Periodically content is rendered into the multi buffered layer which
* will have more traditional latency guarantees, however, minimizes the impact of visual artifacts
* due to graphical tearing.
*
* @param surfaceView Target SurfaceView to act as the parent rendering layer for multi buffered
* content
* @param callback Callbacks used to render into front and multi buffered layers as well as
* configuring [SurfaceControlCompat.Transaction]s for controlling these layers in addition to
* other [SurfaceControlCompat] instances that must be updated atomically within the user
* interface. These callbacks are invoked on an internal rendering thread. The templated type
* here is consumer defined to represent the data structures to be consumed for rendering within
* [Callback.onDrawFrontBufferedLayer] and [Callback.onDrawMultiBufferedLayer] and are provided
* by the [CanvasFrontBufferedRenderer.renderFrontBufferedLayer] and
* [CanvasFrontBufferedRenderer.renderMultiBufferedLayer] methods.
*/
@RequiresApi(Build.VERSION_CODES.Q)
class CanvasFrontBufferedRenderer<T>(
private val surfaceView: SurfaceView,
private val callback: Callback<T>,
) {
/**
* Executor used to deliver callbacks for rendering as well as issuing surface control
* transactions
*/
private val mExecutor = Executors.newSingleThreadExecutor()
/**
* RenderNode used to draw the entire multi buffered scene
*/
private var mMultiBufferNode: RenderNode? = null
/**
* Renderer used to draw [mMultiBufferNode] into a [HardwareBuffer] that is used to configure
* the parent SurfaceControl that represents the multi-buffered scene
*/
private var mMultiBufferedCanvasRenderer: MultiBufferedCanvasRenderer? = null
/**
* Renderer used to draw the front buffer content into a HardwareBuffer instance that is
* preserved across frames
*/
private var mPersistedCanvasRenderer: SingleBufferedCanvasRenderer<T>? = null
/**
* [SurfaceControlCompat] used to configure buffers and visibility of the front buffered layer
*/
private var mFrontBufferSurfaceControl: SurfaceControlCompat? = null
/**
* [SurfaceControlCompat] used to configure buffers and visibility of the multi-buffered layer
*/
private var mParentSurfaceControl: SurfaceControlCompat? = null
/**
* Queue of parameters to be consumed in [Callback.onDrawFrontBufferedLayer] with the parameter
* provided in [renderFrontBufferedLayer]. When [commit] is invoked the collection is used
* to render the multi-buffered scene and is subsequently cleared
*/
private var mParams = ArrayList<T>()
/**
* Flag to determine if the [CanvasFrontBufferedRenderer] has previously been released. If this
* flag is true, then subsequent requests to [renderFrontBufferedLayer],
* [renderMultiBufferedLayer], [commit], and [release] are ignored.
*/
private var mIsReleased = false
/**
* Runnable executed on the GLThread to update [FrontBufferSyncStrategy.isVisible] as well
* as hide the SurfaceControl associated with the front buffered layer
*/
private val mCancelRunnable = Runnable {
mPersistedCanvasRenderer?.isVisible = false
mFrontBufferSurfaceControl?.let { frontBufferSurfaceControl ->
SurfaceControlCompat.Transaction()
.setVisibility(frontBufferSurfaceControl, false)
.commit()
}
}
private var inverse = BufferTransformHintResolver.UNKNOWN_TRANSFORM
private val mBufferTransform = BufferTransformer()
private val mParentLayerTransform = android.graphics.Matrix()
init {
surfaceView.holder.addCallback(object : SurfaceHolder.Callback2 {
private var mWidth = -1
private var mHeight = -1
private var transformHint = BufferTransformHintResolver.UNKNOWN_TRANSFORM
private val mTransformResolver = BufferTransformHintResolver()
override fun surfaceCreated(p0: SurfaceHolder) {
// NO-OP
}
override fun surfaceChanged(
holder: SurfaceHolder,
format: Int,
width: Int,
height: Int
) {
mWidth = width
mHeight = height
releaseInternal(true)
transformHint = mTransformResolver.getBufferTransformHint(surfaceView)
inverse = mBufferTransform.invertBufferTransform(transformHint)
mBufferTransform.computeTransform(width, height, inverse)
updateMatrixTransform(width.toFloat(), height.toFloat(), inverse)
mPersistedCanvasRenderer = SingleBufferedCanvasRenderer.create<T>(
width,
height,
mBufferTransform,
mExecutor,
object : SingleBufferedCanvasRenderer.RenderCallbacks<T> {
override fun render(canvas: Canvas, width: Int, height: Int, param: T) {
callback.onDrawFrontBufferedLayer(canvas, width, height, param)
}
@SuppressLint("WrongConstant")
override fun onBufferReady(
hardwareBuffer: HardwareBuffer,
syncFenceCompat: SyncFenceCompat?
) {
mPersistedCanvasRenderer?.isVisible = true
mFrontBufferSurfaceControl?.let { frontBufferSurfaceControl ->
val transaction = SurfaceControlCompat.Transaction()
.setLayer(frontBufferSurfaceControl, Integer.MAX_VALUE)
.setBuffer(
frontBufferSurfaceControl,
hardwareBuffer,
syncFenceCompat
)
.setVisibility(frontBufferSurfaceControl, true)
.reparent(frontBufferSurfaceControl, mParentSurfaceControl)
if (inverse != BufferTransformHintResolver.UNKNOWN_TRANSFORM) {
transaction.setBufferTransform(
frontBufferSurfaceControl,
inverse
)
}
callback.onFrontBufferedLayerRenderComplete(
frontBufferSurfaceControl, transaction)
transaction.commit()
syncFenceCompat?.close()
}
}
})
val parentSurfaceControl = SurfaceControlCompat.Builder()
.setParent(surfaceView)
.setName("MultiBufferedLayer")
.build()
.apply {
// SurfaceControl is not visible by default so make it visible right
// after creation
SurfaceControlCompat.Transaction()
.setVisibility(this, true)
.commit()
}
val multiBufferNode = RenderNode("MultiBufferNode").apply {
setPosition(0, 0, mBufferTransform.glWidth, mBufferTransform.glHeight)
mMultiBufferNode = this
}
mMultiBufferedCanvasRenderer = MultiBufferedCanvasRenderer(
multiBufferNode,
mBufferTransform.glWidth,
mBufferTransform.glHeight
)
mFrontBufferSurfaceControl = SurfaceControlCompat.Builder()
.setParent(parentSurfaceControl)
.setName("FrontBufferedLayer")
.build()
mParentSurfaceControl = parentSurfaceControl
}
override fun surfaceDestroyed(p0: SurfaceHolder) {
releaseInternal(true)
}
override fun surfaceRedrawNeeded(holder: SurfaceHolder) {
val latch = CountDownLatch(1)
surfaceRedrawNeededAsync(holder) {
latch.countDown()
}
latch.await()
}
override fun surfaceRedrawNeededAsync(
holder: SurfaceHolder,
drawingFinished: Runnable
) {
val renderer = mMultiBufferedCanvasRenderer
if (renderer != null) {
renderer.renderFrame(mExecutor) { buffer ->
setParentSurfaceControlBuffer(buffer, drawingFinished)
}
} else {
drawingFinished.run()
}
}
})
}
private inline fun RenderNode.record(block: (canvas: Canvas) -> Unit): RenderNode {
val canvas = beginRecording()
block(canvas)
endRecording()
return this
}
/**
* Render content to the front buffered layer providing optional parameters to be consumed in
* [Callback.onDrawFrontBufferedLayer].
* Additionally the parameter provided here will also be consumed in
* [Callback.onDrawMultiBufferedLayer]
* when the corresponding [commit] method is invoked, which will include all [param]s in each
* call made to this method up to the corresponding [commit] call.
*
* If this [CanvasFrontBufferedRenderer] has been released, that is [isValid] returns `false`,
* this call is ignored.
*
* @param param Optional parameter to be consumed when rendering content into the commit layer
*/
fun renderFrontBufferedLayer(param: T) {
if (isValid()) {
mParams.add(param)
mPersistedCanvasRenderer?.render(param)
} else {
Log.w(TAG, "Attempt to render to front buffered layer when " +
"CanvasFrontBufferedRenderer has been released"
)
}
}
/**
* Requests to render to the multi buffered layer. This schedules a call to
* [Callback.onDrawMultiBufferedLayer] with the parameters provided. If the front buffered
* layer is visible, this will hide this layer after rendering to the multi buffered layer
* is complete. This is equivalent to calling [CanvasFrontBufferedRenderer.renderFrontBufferedLayer]
* for each parameter provided in the collection followed by a single call to
* [CanvasFrontBufferedRenderer.commit]. This is useful for re-rendering the multi buffered
* scene when the corresponding Activity is being resumed from the background in which the
* contents should be re-drawn. Additionally this allows for applications to decide to
* dynamically render to either front or multi buffered layers.
*
* If this [CanvasFrontBufferedRenderer] has been released, that is [isValid] returns 'false',
* this call is ignored.
*
* @param params Parameters that to be consumed when rendering to the multi buffered layer.
* These parameters will be provided in the corresponding call to
* [Callback.onDrawMultiBufferedLayer]
*/
fun renderMultiBufferedLayer(params: Collection<T>) {
if (isValid()) {
mParams.addAll(params)
commit()
} else {
Log.w(TAG, "Attempt to render to the multi buffered layer when " +
"CanvasFrontBufferedRenderer has been released"
)
}
}
/**
* Determines whether or not the [CanvasFrontBufferedRenderer] is in a valid state. That is the
* [release] method has not been called.
* If this returns false, then subsequent calls to [renderFrontBufferedLayer],
* [renderMultiBufferedLayer], [commit], and [release] are ignored
*
* @return `true` if this [CanvasFrontBufferedRenderer] has been released, `false` otherwise
*/
fun isValid() = !mIsReleased
@SuppressLint("WrongConstant")
internal fun setParentSurfaceControlBuffer(
buffer: HardwareBuffer,
block: Runnable? = null
) {
val frontBufferSurfaceControl = mFrontBufferSurfaceControl
val parentSurfaceControl = mParentSurfaceControl
if (frontBufferSurfaceControl != null && parentSurfaceControl != null) {
mPersistedCanvasRenderer?.isVisible = false
val transaction = SurfaceControlCompat.Transaction()
.setVisibility(frontBufferSurfaceControl, false)
.setVisibility(parentSurfaceControl, true)
.setBuffer(parentSurfaceControl, buffer) {
buffer.close()
}
if (inverse != BufferTransformHintResolver.UNKNOWN_TRANSFORM) {
transaction.setBufferTransform(parentSurfaceControl, inverse)
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
val listener = if (block != null) {
object : SurfaceControlCompat.TransactionCommittedListener {
override fun onTransactionCommitted() {
mCommitListener.onTransactionCommitted()
block.run()
}
}
} else {
mCommitListener
}
transaction.addTransactionCommittedListener(mExecutor, listener)
} else {
if (block != null) {
mExecutor.execute {
mCommitRunnable.run()
block.run()
}
} else {
mExecutor.execute(mCommitRunnable)
}
}
callback.onMultiBufferedLayerRenderComplete(
frontBufferSurfaceControl, transaction)
transaction.commit()
}
}
/**
* Requests to render the entire scene to the multi buffered layer and schedules a call to
* [Callback.onDrawMultiBufferedLayer]. The parameters provided to
* [Callback.onDrawMultiBufferedLayer] will include each argument provided to every
* [renderFrontBufferedLayer] call since the last call to [commit] has been made. When rendering
* to the multi-buffered layer is complete, this synchronously hides the front buffer and
* updates the multi buffered layer.
*
* If this [CanvasFrontBufferedRenderer] has been released, that is [isValid] returns `false`,
* this call is ignored.
*/
fun commit() {
if (isValid()) {
mPersistedCanvasRenderer?.cancelPending()
val params = mParams
mParams = ArrayList<T>()
val width = surfaceView.width
val height = surfaceView.height
mExecutor.execute {
mMultiBufferNode?.record { canvas ->
canvas.save()
canvas.setMatrix(mParentLayerTransform)
callback.onDrawMultiBufferedLayer(canvas, width, height, params)
canvas.restore()
}
params.clear()
mMultiBufferedCanvasRenderer?.renderFrame(mExecutor) { buffer ->
setParentSurfaceControlBuffer(buffer)
}
}
} else {
Log.w(TAG, "Attempt to render to the multi buffered layer when " +
"CanvasFrontBufferedRenderer has been released"
)
}
}
internal fun updateMatrixTransform(width: Float, height: Float, transform: Int) {
mParentLayerTransform.apply {
when (transform) {
SurfaceControlCompat.BUFFER_TRANSFORM_ROTATE_90 -> {
setRotate(270f)
postTranslate(0f, width)
}
SurfaceControlCompat.BUFFER_TRANSFORM_ROTATE_180 -> {
setRotate(180f)
postTranslate(width, height)
}
SurfaceControlCompat.BUFFER_TRANSFORM_ROTATE_270 -> {
setRotate(90f)
postTranslate(height, 0f)
}
else -> {
reset()
}
}
}
}
/**
* Requests to cancel rendering and hides the front buffered layer.
* Unlike [commit], this does not schedule a call to render into the multi buffered layer. This
* is useful in palm rejection use cases, where some initial touch events might be processed
* before a corresponding cancel event is received indicating the touch gesture is coming
* from a palm rather than intentional user input. In the case where MotionEvent#getAction
* returns ACTION_CANCEL, this is to be invoked.
*
* If this [GLFrontBufferedRenderer] has been released, that is [isValid] returns `false`,
* this call is ignored.
*/
fun cancel() {
if (isValid()) {
mPersistedCanvasRenderer?.cancelPending()
mExecutor.execute(mCancelRunnable)
mPersistedCanvasRenderer?.clear()
} else {
Log.w(TAG, "Attempt to cancel rendering to front buffer after " +
"CanvasFrontBufferRenderer has been released")
}
}
private val mCommitListener = object : SurfaceControlCompat.TransactionCommittedListener {
override fun onTransactionCommitted() {
mPersistedCanvasRenderer?.clear()
}
}
private val mCommitRunnable = Runnable {
mPersistedCanvasRenderer?.clear()
}
internal fun releaseInternal(cancelPending: Boolean, releaseCallback: (() -> Unit)? = null) {
mPersistedCanvasRenderer?.release(cancelPending) {
mMultiBufferNode?.discardDisplayList()
mFrontBufferSurfaceControl?.release()
mParentSurfaceControl?.release()
mMultiBufferedCanvasRenderer?.release()
mMultiBufferNode = null
mFrontBufferSurfaceControl = null
mParentSurfaceControl = null
mPersistedCanvasRenderer = null
mMultiBufferedCanvasRenderer = null
releaseCallback?.invoke()
}
}
/**
* Releases the [CanvasFrontBufferedRenderer]. In process requests are ignored.
* If the [CanvasFrontBufferedRenderer] is already released, that is [isValid] returns `false`,
* this method does nothing.
*/
@JvmOverloads
fun release(cancelPending: Boolean, onReleaseComplete: (() -> Unit)? = null) {
if (!mIsReleased) {
releaseInternal(cancelPending) {
onReleaseComplete?.invoke()
mExecutor.shutdown()
}
mIsReleased = true
}
}
/**
* Provides callbacks for consumers to draw into the front and multi buffered layers as well as
* provide opportunities to synchronize [SurfaceControlCompat.Transaction]s to submit the layers
* to the hardware compositor.
*/
@JvmDefaultWithCompatibility
interface Callback<T> {
/**
* Callback invoked to render content into the front buffered layer with the specified
* parameters.
* @param canvas [Canvas] used to issue drawing instructions into the front buffered layer
* @param bufferWidth Width of the buffer that is being rendered into.
* @param bufferHeight Height of the buffer that is being rendered into.
* @param param optional parameter provided the corresponding
* [CanvasFrontBufferedRenderer.renderFrontBufferedLayer] method that triggered this request
* to render into the front buffered layer
*/
@WorkerThread
fun onDrawFrontBufferedLayer(
canvas: Canvas,
bufferWidth: Int,
bufferHeight: Int,
param: T
)
/**
* Callback invoked to render content into the front buffered layer with the specified
* parameters.
* @param canvas [Canvas] used to issue drawing instructions into the front buffered layer
* @param bufferWidth Width of the buffer that is being rendered into.
* @param bufferHeight Height of the buffer that is being rendered into.
* @param params optional parameter provided to render the entire scene into the multi
* buffered layer.
* This is a collection of all parameters provided in consecutive invocations to
* [CanvasFrontBufferedRenderer.renderFrontBufferedLayer] since the last call to
* [CanvasFrontBufferedRenderer.commit] has been made. After
* [CanvasFrontBufferedRenderer.commit] is invoked, this collection is cleared and new
* parameters are added on each subsequent call to
* [CanvasFrontBufferedRenderer.renderFrontBufferedLayer]
*/
@WorkerThread
fun onDrawMultiBufferedLayer(
canvas: Canvas,
bufferWidth: Int,
bufferHeight: Int,
params: Collection<T>
)
/**
* Optional callback invoked when rendering to the front buffered layer is complete but
* before the buffers are submitted to the hardware compositor.
* This provides consumers a mechanism for synchronizing the transaction with other
* [SurfaceControlCompat] objects that maybe rendered within the scene.
*
* @param frontBufferedLayerSurfaceControl Handle to the [SurfaceControlCompat] where the
* front buffered layer content is drawn. This can be used to configure various properties
* of the [SurfaceControlCompat] like z-ordering or visibility with the corresponding
* [SurfaceControlCompat.Transaction].
* @param transaction Current [SurfaceControlCompat.Transaction] to apply updated buffered
* content to the front buffered layer.
*/
@WorkerThread
fun onFrontBufferedLayerRenderComplete(
frontBufferedLayerSurfaceControl: SurfaceControlCompat,
transaction: SurfaceControlCompat.Transaction
) {
// Default implementation is a no-op
}
/**
* Optional callback invoked when rendering to the multi buffered layer is complete but
* before the buffers are submitted to the hardware compositor.
* This provides consumers a mechanism for synchronizing the transaction with other
* [SurfaceControlCompat] objects that maybe rendered within the scene.
*
* @param frontBufferedLayerSurfaceControl Handle to the [SurfaceControlCompat] where the
* front buffered layer content is drawn. This can be used to configure various properties
* of the [SurfaceControlCompat] like z-ordering or visibility with the corresponding
* [SurfaceControlCompat.Transaction].
* @param transaction Current [SurfaceControlCompat.Transaction] to apply updated buffered
* content to the multi buffered layer.
*/
@WorkerThread
fun onMultiBufferedLayerRenderComplete(
frontBufferedLayerSurfaceControl: SurfaceControlCompat,
transaction: SurfaceControlCompat.Transaction
) {
// Default implementation is a no-op
}
}
internal companion object {
internal const val TAG = "LowLatencyCanvas"
}
}