1 /* <lambda>null2 * Copyright 2022 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.graphics.opengl 18 19 import android.opengl.EGL14 20 import android.opengl.EGLConfig 21 import android.opengl.EGLSurface 22 import android.os.Handler 23 import android.os.HandlerThread 24 import android.util.Log 25 import android.view.Surface 26 import androidx.annotation.AnyThread 27 import androidx.annotation.WorkerThread 28 import androidx.graphics.opengl.GLRenderer.EGLContextCallback 29 import androidx.graphics.opengl.GLRenderer.RenderCallback 30 import androidx.graphics.opengl.egl.EGLManager 31 import androidx.graphics.opengl.egl.EGLSpec 32 import androidx.graphics.utils.post 33 import java.util.concurrent.atomic.AtomicBoolean 34 35 /** 36 * Thread responsible for management of EGL dependencies, setup and teardown of EGLSurface instances 37 * as well as delivering callbacks to draw a frame 38 */ 39 internal class GLThread( 40 name: String = "GLThread", 41 private val mEglSpecFactory: () -> EGLSpec, 42 private val mEglConfigFactory: EGLManager.() -> EGLConfig, 43 ) : HandlerThread(name) { 44 45 // Accessed on internal HandlerThread 46 private val mIsTearingDown = AtomicBoolean(false) 47 private var mEglManager: EGLManager? = null 48 private val mSurfaceSessions = HashMap<Int, SurfaceSession>() 49 private var mHandler: Handler? = null 50 private val mEglContextCallback = HashSet<EGLContextCallback>() 51 52 override fun start() { 53 super.start() 54 mHandler = 55 Handler(looper).apply { 56 // Create an EGLContext right after starting in order to have one ready on a call to 57 // GLRenderer#execute 58 post { obtainEGLManager() } 59 } 60 } 61 62 /** 63 * Adds the given [android.view.Surface] to be managed by the GLThread. A corresponding 64 * [EGLSurface] is created on the GLThread as well as a callback for rendering into the surface 65 * through [RenderCallback]. 66 * 67 * @param surface intended to be be rendered into on the GLThread 68 * @param width Desired width of the [surface] 69 * @param height Desired height of the [surface] 70 * @param renderer callbacks used to create a corresponding [EGLSurface] from the given surface 71 * as well as render content into the created [EGLSurface] 72 * @return Identifier used for subsequent requests to communicate with the provided Surface (ex. 73 * [requestRender] or [detachSurface] 74 */ 75 @AnyThread 76 fun attachSurface( 77 token: Int, 78 surface: Surface?, 79 width: Int, 80 height: Int, 81 renderer: RenderCallback 82 ) { 83 withHandler { 84 post(token) { 85 attachSurfaceSessionInternal( 86 SurfaceSession(token, surface, renderer).apply { 87 this.width = width 88 this.height = height 89 } 90 ) 91 } 92 } 93 } 94 95 @AnyThread 96 fun resizeSurface(token: Int, width: Int, height: Int, callback: Runnable? = null) { 97 withHandler { 98 post(token) { 99 resizeSurfaceSessionInternal(token, width, height) 100 requestRenderInternal(token) 101 callback?.run() 102 } 103 } 104 } 105 106 @AnyThread 107 fun addEGLCallbacks(callbacks: ArrayList<EGLContextCallback>) { 108 withHandler { 109 post { 110 mEglContextCallback.addAll(callbacks) 111 mEglManager?.let { 112 for (callback in callbacks) { 113 callback.onEGLContextCreated(it) 114 } 115 } 116 } 117 } 118 } 119 120 @AnyThread 121 fun addEGLCallback(callbacks: EGLContextCallback) { 122 withHandler { 123 post { 124 mEglContextCallback.add(callbacks) 125 // If EGL dependencies are already initialized, immediately invoke 126 // the added callback 127 mEglManager?.let { callbacks.onEGLContextCreated(it) } 128 } 129 } 130 } 131 132 @AnyThread 133 fun removeEGLCallback(callbacks: EGLContextCallback) { 134 withHandler { post { mEglContextCallback.remove(callbacks) } } 135 } 136 137 @AnyThread fun execute(runnable: Runnable) = withHandler { post(runnable) } 138 139 /** 140 * Removes the corresponding [android.view.Surface] from management of the GLThread. This 141 * destroys the EGLSurface associated with this surface and subsequent requests to render into 142 * the surface with the provided token are ignored. Any queued request to render to the 143 * corresponding [SurfaceSession] that has not started yet is cancelled. However, if this is 144 * invoked in the middle of the frame being rendered, it will continue to process the current 145 * frame. 146 */ 147 @AnyThread 148 fun detachSurface(token: Int, cancelPending: Boolean, callback: Runnable?) { 149 log("dispatching request to detach surface w/ token: $token") 150 withHandler { 151 if (cancelPending) { 152 removeCallbacksAndMessages(token) 153 } 154 post(token) { detachSurfaceSessionInternal(token, callback) } 155 } 156 } 157 158 /** 159 * Cancel all pending requests to all currently managed [SurfaceSession] instances, destroy all 160 * EGLSurfaces, teardown EGLManager and quit this thread 161 */ 162 @AnyThread 163 fun tearDown(cancelPending: Boolean, callback: Runnable?) { 164 withHandler { 165 if (cancelPending) { 166 removeCallbacksAndMessages(null) 167 } 168 post { releaseResourcesInternalAndQuit(callback) } 169 mIsTearingDown.set(true) 170 } 171 } 172 173 /** 174 * Mark the corresponding surface session with the given token as dirty to schedule a call to 175 * [RenderCallback#onDrawFrame]. If there is already a queued request to render into the 176 * provided surface with the specified token, this request is ignored. 177 */ 178 @AnyThread 179 fun requestRender(token: Int, callback: Runnable? = null) { 180 log("dispatching request to render for token: $token") 181 withHandler { 182 post(token) { 183 requestRenderInternal(token) 184 callback?.run() 185 } 186 } 187 } 188 189 /** 190 * Lazily creates an [EGLManager] instance from the given [mEglSpecFactory] used to determine 191 * the configuration. This result is cached across calls unless [tearDown] has been called. 192 */ 193 @WorkerThread 194 fun obtainEGLManager(): EGLManager = 195 mEglManager 196 ?: EGLManager(mEglSpecFactory.invoke()).also { 197 it.initialize() 198 val config = mEglConfigFactory.invoke(it) 199 it.createContext(config) 200 for (callback in mEglContextCallback) { 201 callback.onEGLContextCreated(it) 202 } 203 mEglManager = it 204 } 205 206 @WorkerThread 207 private fun disposeSurfaceSession(session: SurfaceSession) { 208 val eglSurface = session.eglSurface 209 if (eglSurface != null && eglSurface != EGL14.EGL_NO_SURFACE) { 210 obtainEGLManager().eglSpec.eglDestroySurface(eglSurface) 211 session.eglSurface = null 212 } 213 } 214 215 /** 216 * Helper method to obtain the cached EGLSurface for the given [SurfaceSession], creating one if 217 * it does not previously exist 218 */ 219 @WorkerThread 220 private fun obtainEGLSurfaceForSession(session: SurfaceSession): EGLSurface? { 221 return if (session.eglSurface != null) { 222 session.eglSurface 223 } else { 224 createEGLSurfaceForSession( 225 session.surface, 226 session.width, 227 session.height, 228 session.surfaceRenderer 229 ) 230 .also { session.eglSurface = it } 231 } 232 } 233 234 /** 235 * Helper method to create the corresponding EGLSurface from the [SurfaceSession] instance 236 * Consumers are expected to teardown the previously existing EGLSurface instance if it exists 237 */ 238 @WorkerThread 239 private fun createEGLSurfaceForSession( 240 surface: Surface?, 241 width: Int, 242 height: Int, 243 surfaceRenderer: RenderCallback 244 ): EGLSurface? { 245 with(obtainEGLManager()) { 246 return if (surface != null) { 247 surfaceRenderer.onSurfaceCreated( 248 eglSpec, 249 // Successful creation of EGLManager ensures non null EGLConfig 250 eglConfig!!, 251 surface, 252 width, 253 height 254 ) 255 } else { 256 null 257 } 258 } 259 } 260 261 @WorkerThread 262 private fun releaseResourcesInternalAndQuit(callback: Runnable?) { 263 val eglManager = obtainEGLManager() 264 for (session in mSurfaceSessions) { 265 disposeSurfaceSession(session.value) 266 } 267 callback?.run() 268 mSurfaceSessions.clear() 269 for (eglCallback in mEglContextCallback) { 270 eglCallback.onEGLContextDestroyed(eglManager) 271 } 272 mEglContextCallback.clear() 273 eglManager.release() 274 mEglManager = null 275 quit() 276 } 277 278 @WorkerThread 279 private fun requestRenderInternal(token: Int) { 280 log("requesting render for token: $token") 281 mSurfaceSessions[token]?.let { surfaceSession -> 282 val eglManager = obtainEGLManager() 283 val eglSurface = obtainEGLSurfaceForSession(surfaceSession) 284 if (eglSurface != null) { 285 eglManager.makeCurrent(eglSurface) 286 } else { 287 eglManager.makeCurrent(eglManager.defaultSurface) 288 } 289 290 val width = surfaceSession.width 291 val height = surfaceSession.height 292 if (width > 0 && height > 0) { 293 surfaceSession.surfaceRenderer.onDrawFrame(eglManager) 294 } 295 296 if (eglSurface != null) { 297 eglManager.swapAndFlushBuffers() 298 } 299 } 300 } 301 302 @WorkerThread 303 private fun attachSurfaceSessionInternal(surfaceSession: SurfaceSession) { 304 mSurfaceSessions[surfaceSession.surfaceToken] = surfaceSession 305 } 306 307 @WorkerThread 308 private fun resizeSurfaceSessionInternal(token: Int, width: Int, height: Int) { 309 mSurfaceSessions[token]?.let { surfaceSession -> 310 surfaceSession.apply { 311 this.width = width 312 this.height = height 313 } 314 disposeSurfaceSession(surfaceSession) 315 obtainEGLSurfaceForSession(surfaceSession) 316 } 317 } 318 319 @WorkerThread 320 private fun detachSurfaceSessionInternal(token: Int, callback: Runnable?) { 321 val session = mSurfaceSessions.remove(token) 322 if (session != null) { 323 disposeSurfaceSession(session) 324 } 325 callback?.run() 326 } 327 328 /** 329 * Helper method that issues a callback on the handler instance for this thread ensuring proper 330 * nullability checks are handled. This assumes that that [GLRenderer.start] has been called 331 * before attempts to interact with the corresponding Handler are made with this method 332 */ 333 private inline fun withHandler(block: Handler.() -> Unit) { 334 val handler = 335 mHandler ?: throw IllegalStateException("Did you forget to call GLThread.start()?") 336 if (!mIsTearingDown.get()) { 337 block(handler) 338 } 339 } 340 341 companion object { 342 343 private const val DEBUG = true 344 private const val TAG = "GLThread" 345 346 internal fun log(msg: String) { 347 if (DEBUG) { 348 Log.v(TAG, msg) 349 } 350 } 351 } 352 353 private class SurfaceSession( 354 /** 355 * Identifier used to lookup the mapping of this surface session. Consumers are expected to 356 * provide this identifier to operate on the corresponding surface to either request a frame 357 * be rendered or to remove this Surface 358 */ 359 val surfaceToken: Int, 360 361 /** 362 * Target surface to render into. Can be null for situations where GL is used to render into 363 * a frame buffer object provided from an AHardwareBuffer instance. In these cases the 364 * actual surface is never used. 365 */ 366 val surface: Surface?, 367 368 /** 369 * Callback used to create an EGLSurface from the provided surface as well as render content 370 * to the surface 371 */ 372 val surfaceRenderer: RenderCallback 373 ) { 374 /** 375 * Lazily created + cached [EGLSurface] after [RenderCallback.onSurfaceCreated] is invoked. 376 * This is only modified on the backing thread 377 */ 378 var eglSurface: EGLSurface? = null 379 380 /** Target width of the [surface]. This is only modified on the backing thread */ 381 var width: Int = 0 382 383 /** Target height of the [surface]. This is only modified on the backing thread */ 384 var height: Int = 0 385 } 386 } 387