FrameBufferPool.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.hardware.HardwareBuffer
import android.os.Build
import android.util.Log
import androidx.annotation.RequiresApi
import androidx.graphics.opengl.egl.EGLSpec
import androidx.hardware.SyncFenceCompat
import java.util.concurrent.locks.ReentrantLock
import kotlin.concurrent.withLock
/**
* Allocation pool used for the creation and reuse of [FrameBuffer] instances.
* This class is thread safe.
*/
@RequiresApi(Build.VERSION_CODES.O)
internal class FrameBufferPool(
/**
* Width of the [HardwareBuffer] objects to allocate if none are available in the pool
*/
private val width: Int,
/**
* Height of the [HardwareBuffer] objects to allocate if none are available in the pool
*/
private val height: Int,
/**
* Format of the [HardwareBuffer] objects to allocate if none are available in the pool
*/
private val format: Int,
/**
* Usage hint flag of the [HardwareBuffer] objects to allocate if none are available in the pool
*/
private val usage: Long,
/**
* Maximum size that the pool before additional requests to allocate buffers blocks until
* another [FrameBuffer] is released. Must be greater than 0.
*/
private val maxPoolSize: Int
) {
private data class FrameBufferEntry(
var frameBuffer: FrameBuffer,
var fence: SyncFenceCompat?,
var isAvailable: Boolean
)
private val mPool = ArrayList<FrameBufferEntry>()
private var mBuffersAvailable = 0
private val mLock = ReentrantLock()
private val mCondition = mLock.newCondition()
private var mIsClosed = false
init {
if (maxPoolSize <= 0) {
throw IllegalArgumentException("Pool size must be at least 1")
}
}
/**
* Return the current pool allocation size. This will increase until the [maxPoolSize].
* This count will decrease if a buffer is closed before it is returned to the pool as a
* closed buffer is no longer re-usable
*/
val allocationCount: Int
get() = mPool.size
/**
* Obtains a [FrameBuffer] instance. This will either return a [FrameBuffer] if one is
* available within the pool, or creates a new [FrameBuffer] instance if the number of
* outstanding [FrameBuffer] instances is less than [maxPoolSize]
*/
fun obtain(eglSpec: EGLSpec): FrameBuffer {
mLock.withLock {
if (mIsClosed) {
throw IllegalStateException("Attempt to obtain frame buffer from FrameBufferPool " +
"that has already been closed")
}
while (mBuffersAvailable == 0 && mPool.size >= maxPoolSize) {
Log.w(
TAG,
"Waiting for FrameBuffer to become available, current allocation " +
"count: ${mPool.size}"
)
mCondition.await()
}
val entry = mPool.findEntryWith(isAvailable, signaledFence)
return if (entry != null) {
mBuffersAvailable--
entry.isAvailable = false
entry.fence?.let { fence ->
fence.awaitForever()
fence.close()
}
entry.frameBuffer
} else {
FrameBuffer(
eglSpec,
HardwareBuffer.create(
width,
height,
format,
1,
usage
)
).also {
mPool.add(FrameBufferEntry(it, null, false))
}
}
}
}
/**
* Releases the given [FrameBuffer] back to the pool and signals all
* consumers that are currently waiting for a buffer to become available
* via [FrameBufferPool.obtain]
* This method is thread safe.
*/
fun release(frameBuffer: FrameBuffer, fence: SyncFenceCompat? = null) {
mLock.withLock {
val entry = mPool.find { entry -> entry.frameBuffer === frameBuffer }
if (entry != null) {
// Only mark the entry as available if it was previously allocated
// This protects against the potential for the same buffer to be released
// multiple times into the same pool
if (!entry.isAvailable) {
if (!entry.frameBuffer.isClosed) {
entry.fence = fence
entry.isAvailable = true
mBuffersAvailable++
} else {
// The consumer closed the buffer before releasing it to the pool.
// In this case remove the entry to allocate new buffers when requested.
// Because framebuffer instances can be managed by applications, we should
// defend against this potential scenario.
mPool.remove(entry)
}
}
if (!mIsClosed) {
mCondition.signal()
} else {
// If a buffer is attempted to be released after the pool is closed
// just remove it from the entries and release it
frameBuffer.close()
if (mBuffersAvailable == mPool.size) {
mPool.clear()
}
}
} else if (!frameBuffer.isClosed) {
// If the FrameBuffer is not previously closed and we don't own this, flag this as
// an error as most likely this buffer was attempted to be returned to the wrong
// pool
throw IllegalArgumentException("No entry associated with this framebuffer " +
"instance. Was this frame buffer created from a different FrameBufferPool?")
}
}
}
val isClosed: Boolean
get() = mLock.withLock { mIsClosed }
/**
* Invokes [FrameBuffer.close] on all [FrameBuffer] instances currently available within
* the pool and clears the pool.
* This method is thread safe.
*/
fun close() {
mLock.withLock {
if (!mIsClosed) {
for (entry in mPool) {
val frameBuffer = entry.frameBuffer
if (entry.isAvailable) {
entry.fence?.let { fence ->
fence.awaitForever()
fence.close()
}
frameBuffer.close()
}
}
if (mBuffersAvailable == mPool.size) {
mPool.clear()
}
mIsClosed = true
}
}
}
internal companion object {
private const val TAG = "FrameBufferPool"
/**
* Predicate used to search for the first entry within the pool that is either null
* or has already signalled
*/
private val signaledFence: (FrameBufferEntry) -> Boolean = { entry ->
val fence = entry.fence
fence == null || fence.getSignalTimeNanos() != SyncFenceCompat.SIGNAL_TIME_PENDING
}
private val isAvailable: (FrameBufferEntry) -> Boolean = { entry -> entry.isAvailable }
/**
* Finds the first element within the ArrayList that satisfies both primary and
* secondary conditions. If no entries satisfy the secondary condition, this returns
* the first entry that satisfies the primary condition or null if no entries do.
*/
internal fun <T> ArrayList<T>.findEntryWith(
primaryCondition: ((T) -> Boolean),
secondaryCondition: ((T) -> Boolean)
): T? {
var fallback: T? = null
for (entry in this) {
if (primaryCondition(entry)) {
if (fallback == null) {
fallback = entry
}
if (secondaryCondition(entry)) {
return entry
}
}
}
// No elements satisfy the condition, return the entry that satisfies the primary
// condition if available
return fallback
}
}
}