1 /*
<lambda>null2  * Copyright 2023 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package androidx.hardware
18 
19 import android.hardware.HardwareBuffer
20 import android.os.Build
21 import android.util.Log
22 import androidx.annotation.RequiresApi
23 import java.util.concurrent.locks.ReentrantLock
24 import kotlin.concurrent.withLock
25 
26 @RequiresApi(Build.VERSION_CODES.O)
27 internal class BufferPool<T : BufferPool.BufferProvider>(val maxPoolSize: Int) {
28 
29     private val mPool: ArrayList<Entry<T>> = ArrayList()
30     private val mLock = ReentrantLock()
31     private val mCondition = mLock.newCondition()
32     private var mBuffersAvailable = 0
33     private var mIsClosed = false
34 
35     init {
36         if (maxPoolSize <= 0) {
37             throw IllegalArgumentException("Pool size must be at least 1")
38         }
39     }
40 
41     fun add(buffer: T) {
42         mPool.add(Entry(buffer))
43     }
44 
45     inline fun obtain(factory: () -> T): T {
46         mLock.withLock {
47             return obtainFromPool() ?: factory.invoke().also { buffer -> insert(buffer) }
48         }
49     }
50 
51     private fun insert(buffer: T) {
52         // Insert the buffer into the pool. If we are in single buffer mode mark this buffer
53         // as being available to support front buffered rendering use cases of simultaneous
54         // presentation and rendering
55         val singleBuffered = maxPoolSize == 1
56         if (singleBuffered) {
57             mBuffersAvailable++
58         }
59         mPool.add(Entry(buffer, isAvailable = singleBuffered))
60     }
61 
62     private fun obtainFromPool(): T? {
63         if (mIsClosed) {
64             throw IllegalStateException(
65                 "Attempt to obtain frame buffer from FrameBufferPool " +
66                     "that has already been closed"
67             )
68         }
69         val singleBuffered = maxPoolSize == 1
70         return if (singleBuffered) {
71             val entry = mPool.firstOrNull()?.also { entry -> entry.isAvailable = true }
72             entry?.bufferProvider
73         } else {
74             while (mBuffersAvailable == 0 && mPool.size >= maxPoolSize) {
75                 Log.w(
76                     TAG,
77                     "Waiting for buffer to become available, current allocation " +
78                         "count: ${mPool.size}"
79                 )
80                 mCondition.await()
81             }
82             val entry =
83                 mPool.findEntryWith(isAvailable, signaledFence)?.also { entry ->
84                     mBuffersAvailable--
85                     with(entry) {
86                         isAvailable = false
87                         releaseFence?.let { fence ->
88                             fence.awaitForever()
89                             fence.close()
90                         }
91                         bufferProvider
92                     }
93                 }
94             entry?.bufferProvider
95         }
96     }
97 
98     fun release(hardwareBuffer: HardwareBuffer, fence: SyncFenceCompat? = null) {
99         mLock.withLock {
100             val entry = mPool.find { entry -> entry.hardwareBuffer === hardwareBuffer }
101             if (entry != null) {
102                 // Only mark the entry as available if it was previously allocated
103                 // This protects against the potential for the same buffer to be released
104                 // multiple times into the same pool
105                 if (!entry.isAvailable) {
106                     if (!entry.hardwareBuffer.isClosed) {
107                         entry.releaseFence = fence
108                         entry.isAvailable = true
109                         mBuffersAvailable++
110                     } else {
111                         // The consumer closed the buffer before releasing it to the pool.
112                         // In this case remove the entry to allocate new buffers when requested.
113                         // Because HardwareBuffer instances can be managed by applications, we
114                         // should defend against this potential scenario.
115                         mPool.remove(entry)
116                     }
117                 }
118 
119                 if (!mIsClosed) {
120                     mCondition.signal()
121                 } else {
122                     // If a buffer is attempted to be released after the pool is closed
123                     // just remove it from the entries and release it
124                     hardwareBuffer.close()
125                     if (mBuffersAvailable == mPool.size) {
126                         mPool.clear()
127                     }
128                 }
129             } else if (!hardwareBuffer.isClosed) {
130                 // If the FrameBuffer is not previously closed and we don't own this, flag this as
131                 // an error as most likely this buffer was attempted to be returned to the wrong
132                 // pool
133                 throw IllegalArgumentException(
134                     "No entry associated with this framebuffer " +
135                         "instance. Was this frame buffer created from a different FrameBufferPool?"
136                 )
137             }
138         }
139     }
140 
141     /**
142      * Return the current pool allocation size. This will increase until the [maxPoolSize]. This
143      * count will decrease if a buffer is closed before it is returned to the pool as a closed
144      * buffer is no longer re-usable
145      */
146     val allocationCount: Int
147         get() = mPool.size
148 
149     val isClosed: Boolean
150         get() = mLock.withLock { mIsClosed }
151 
152     /**
153      * Invokes [BufferProvider.release] on all [Entry] instances currently available within the pool
154      * and clears the pool. This method is thread safe.
155      */
156     fun close() {
157         mLock.withLock {
158             if (!mIsClosed) {
159                 for (entry in mPool) {
160                     if (entry.isAvailable) {
161                         entry.releaseFence?.let { fence ->
162                             fence.awaitForever()
163                             fence.close()
164                         }
165                         entry.bufferProvider.release()
166                     }
167                 }
168                 if (mBuffersAvailable == mPool.size) {
169                     mPool.clear()
170                 }
171                 mIsClosed = true
172             }
173         }
174     }
175 
176     private data class Entry<T : BufferProvider>(
177         val bufferProvider: T,
178         var releaseFence: SyncFenceCompat? = null,
179         var isAvailable: Boolean = true
180     ) {
181         val hardwareBuffer: HardwareBuffer
182             get() = bufferProvider.hardwareBuffer
183     }
184 
185     interface BufferProvider {
186         val hardwareBuffer: HardwareBuffer
187 
188         fun release()
189     }
190 
191     internal companion object {
192         private const val TAG = "BufferPool"
193 
194         /**
195          * Predicate used to search for the first entry within the pool that is either null or has
196          * already signalled
197          */
198         private val signaledFence: (Entry<*>) -> Boolean = { entry ->
199             val fence = entry.releaseFence
200             fence == null || fence.getSignalTimeNanos() != SyncFenceCompat.SIGNAL_TIME_PENDING
201         }
202 
203         private val isAvailable: (Entry<*>) -> Boolean = { entry -> entry.isAvailable }
204 
205         /**
206          * Finds the first element within the ArrayList that satisfies both primary and secondary
207          * conditions. If no entries satisfy the secondary condition, this returns the first entry
208          * that satisfies the primary condition or null if no entries do.
209          */
210         internal fun <T> ArrayList<T>.findEntryWith(
211             primaryCondition: ((T) -> Boolean),
212             secondaryCondition: ((T) -> Boolean)
213         ): T? {
214             var fallback: T? = null
215             for (entry in this) {
216                 if (primaryCondition(entry)) {
217                     if (fallback == null) {
218                         fallback = entry
219                     }
220                     if (secondaryCondition(entry)) {
221                         return entry
222                     }
223                 }
224             }
225             // No elements satisfy the condition, return the entry that satisfies the primary
226             // condition if available
227             return fallback
228         }
229     }
230 }
231