RenderQueue.kt
/*
* Copyright 2023 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
import android.hardware.HardwareBuffer
import androidx.annotation.WorkerThread
import androidx.graphics.utils.HandlerThreadExecutor
import androidx.hardware.SyncFenceCompat
import java.util.concurrent.Executor
import java.util.concurrent.atomic.AtomicBoolean
/**
* Helper class to handle processing of event queues between the provided [HandlerThreadExecutor]
* and the [FrameProducer] which may be executing on different threads. This provides helper
* facilities to guarantee cancellation of requests and proper queueing of pending requests
* while the [FrameProducer] is in the middle of generating a frame.
*/
internal class RenderQueue(
private val handlerThread: HandlerThreadExecutor,
private val frameProducer: FrameProducer,
private val frameCallback: FrameCallback
) {
/**
* Callbacks invoked when new frames are produced or if a frame is generated for a request
* that has been cancelled.
*/
interface FrameCallback {
fun onFrameComplete(hardwareBuffer: HardwareBuffer, fence: SyncFenceCompat?)
fun onFrameCancelled(hardwareBuffer: HardwareBuffer, fence: SyncFenceCompat?)
}
/**
* Interface to represent a [FrameProducer] this can either be backed by a
* [android.graphics.HardwareRenderer] or [android.graphics.HardwareBufferRenderer] depending
* on the API level.
*/
interface FrameProducer {
fun renderFrame(
executor: Executor,
requestComplete: (HardwareBuffer, SyncFenceCompat?) -> Unit
)
}
/**
* Request to be executed by the [RenderQueue] this provides callbacks that are invoked
* when the request is initially queued as well as when to be executed before a frame is
* generated. This supports batching operations if the [FrameProducer] is busy.
*/
interface Request {
/**
* Callback invoked when the request is enqueued but before a frame is generated
*/
@WorkerThread
fun onEnqueued() {}
/**
* Callback invoked when the request is about to be processed as part of a the next
* frame
*/
@WorkerThread
fun execute()
/**
* Identifier for a request type to determine if the request can be batched
*/
val id: Int
}
/**
* Flag to determine if all pending requests should be cancelled
*/
private val mIsCancelling = AtomicBoolean(false)
/**
* Queue of pending requests that are executed whenever the [FrameProducer] is idle
*/
private val mRequests = ArrayDeque<Request>()
/**
* Determines if the [FrameProducer] is in the middle of rendering a frame.
* This is accessed on the underlying HandlerThread only
*/
private var mRequestPending = false
/**
* Callback invoked when the [RenderQueue] is to be released. This will be invoked when
* there are no more pending requests to process
*/
private var mReleaseCallback: (() -> Unit)? = null
/**
* Flag to determine if we are in the middle of releasing the [RenderQueue]
*/
private val mIsReleasing = AtomicBoolean(false)
/**
* Enqueues a request to be executed by the provided [FrameProducer]
*/
fun enqueue(request: Request) {
if (!mIsReleasing.get()) {
handlerThread.post(this) {
request.onEnqueued()
executeRequest(request)
}
}
}
/**
* Cancels all pending requests. If a frame is the in the middle of being rendered,
* [FrameCallback.onFrameCancelled] will be invoked upon completion
*/
fun cancelPending() {
if (!mIsReleasing.get()) {
mIsCancelling.set(true)
handlerThread.removeCallbacksAndMessages(this)
handlerThread.post(cancelRequestsRunnable)
}
}
/**
* Configures a release callback to be invoked. If there are no pending requests, this
* will get invoked immediately on the [HandlerThreadExecutor]. Otherwise the callback is
* preserved and invoked after there are no more pending requests.
* After this method is invoked, no subsequent requests will be processed and this [RenderQueue]
* instance can no longer be used.
*/
fun release(cancelPending: Boolean, onReleaseComplete: (() -> Unit)?) {
if (!mIsReleasing.get()) {
if (cancelPending) {
cancelPending()
}
handlerThread.post {
mReleaseCallback = onReleaseComplete
val pendingRequest = isPendingRequest()
if (!pendingRequest) {
executeReleaseCallback()
}
}
mIsReleasing.set(true)
}
}
/**
* Determines if there are any pending requests or a frame is waiting to be produced
*/
private fun isPendingRequest() = mRequestPending || mRequests.isNotEmpty()
/**
* Helper method that will execute a request on the [FrameProducer] if there is not a previous
* request pending. If there is a pending request, this will add it to an internal queue
* that will be dequeued when the request is completed.
*/
@WorkerThread
private fun executeRequest(request: Request) {
if (!mRequestPending) {
mRequestPending = true
request.execute()
frameProducer.renderFrame(handlerThread) { hardwareBuffer, syncFenceCompat ->
mRequestPending = false
if (!mIsCancelling.getAndSet(false)) {
frameCallback.onFrameComplete(hardwareBuffer, syncFenceCompat)
} else {
frameCallback.onFrameCancelled(hardwareBuffer, syncFenceCompat)
}
if (mRequests.isNotEmpty()) {
// Execute any pending requests that were queued while waiting for the
// previous frame to render
executeRequest(mRequests.removeFirst())
} else if (mIsReleasing.get()) {
executeReleaseCallback()
}
}
} else {
// If the last request matches the type that we are adding, then batch the request
// i.e. don't add it to the queue as the previous request will handle batching.
val pendingRequest = mRequests.lastOrNull()
if (pendingRequest == null || pendingRequest.id != request.id) {
mRequests.add(request)
}
}
}
/**
* Returns true if [release] has been invoked
*/
fun isReleased(): Boolean = mIsReleasing.get()
/**
* Invokes the release callback if one is previously configured and discards it
*/
private fun executeReleaseCallback() {
mReleaseCallback?.invoke()
mReleaseCallback = null
}
/**
* Runnable executed when requests are to be cancelled
*/
private val cancelRequestsRunnable = Runnable {
mRequests.clear()
// Only reset the cancel flag if there is no current frame render request pending
// Otherwise when the frame is completed we will update the flag in the corresponding
// callback
if (!mRequestPending) {
mIsCancelling.set(false)
}
}
}