/*
* 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.graphics.SurfaceTexture
import android.opengl.EGL14
import android.opengl.EGLConfig
import android.opengl.EGLSurface
import android.view.Surface
import android.view.SurfaceHolder
import android.view.SurfaceView
import android.view.TextureView
import androidx.annotation.WorkerThread
import androidx.graphics.opengl.egl.EGLConfigAttributes
import androidx.graphics.opengl.egl.EGLManager
import androidx.graphics.opengl.egl.EGLSpec
import java.util.concurrent.CountDownLatch
import java.util.concurrent.atomic.AtomicInteger
/**
* Class responsible for coordination of requests to render into surfaces using OpenGL.
* This creates a backing thread to handle EGL dependencies and draw leveraging OpenGL across
* multiple [android.view.Surface] instances that can be attached and detached throughout
* the lifecycle of an application. Usage of this class is recommended to be done on the UI thread.
*
* @param eglSpecFactory Callback invoked to determine the EGL spec version to use
* for EGL management. This is invoked on the GL Thread
* @param eglConfigFactory Callback invoked to determine the appropriate EGLConfig used
* to create the EGL context. This is invoked on the GL Thread
*/
// GL is the industry standard for referencing OpenGL vs Gl (lowercase l)
@Suppress("AcronymName")
class GLRenderer(
eglSpecFactory: () -> EGLSpec = { EGLSpec.V14 },
eglConfigFactory: EGLManager.() -> EGLConfig = {
// 8 bit channels should always be supported
loadConfig(EGLConfigAttributes.RGBA_8888)
?: throw IllegalStateException("Unable to obtain config for 8 bit EGL " +
"configuration")
}
) {
/**
* Factory method to determine which [EGLSpec] the underlying [EGLManager] implementation uses
*/
private val mEglSpecFactory: () -> EGLSpec = eglSpecFactory
/**
* Factory method used to create the corresponding EGLConfig used to create the EGLRenderer used
* by [EGLManager]
*/
private val mEglConfigFactory: EGLManager.() -> EGLConfig = eglConfigFactory
/**
* GLThread used to manage EGL dependencies, create EGLSurfaces and draw content
*/
private var mGLThread: GLThread? = null
/**
* Collection of [RenderTarget] instances that are managed by the GLRenderer
*/
private val mRenderTargets = ArrayList<RenderTarget>()
/**
* Collection of callbacks to be invoked when the EGL dependencies are initialized
* or torn down
*/
private val mEglContextCallback = HashSet<EGLContextCallback>()
/**
* Removes the corresponding [RenderTarget] from management of the GLThread.
* This destroys the EGLSurface associated with this surface and subsequent requests
* to render into the surface with the provided token are ignored.
*
* If the [cancelPending] flag is set to true, any queued request
* to render that has not started yet is cancelled. However, if this is invoked in the
* middle of the frame being rendered, it will continue to process the current frame.
*
* Additionally if this flag is false, all pending requests to render will be processed
* before the [RenderTarget] is detached.
*
* Note the detach operation will only occur if the GLRenderer is started, that is if
* [isRunning] returns true. Otherwise this is a no-op. GLRenderer will automatically detach all
* [RenderTarget] instances as part of its teardown process.
*/
@JvmOverloads
fun detach(
target: RenderTarget,
cancelPending: Boolean,
@WorkerThread onDetachComplete: ((RenderTarget) -> Unit)? = null
) {
if (mRenderTargets.contains(target)) {
mGLThread?.detachSurface(target.token, cancelPending) {
// WorkerThread
target.release()
target.onDetach.invoke()
onDetachComplete?.invoke(target)
}
mRenderTargets.remove(target)
}
}
/**
* Determines if the GLThread has been started. That is [start] has been invoked
* on this GLRenderer instance without a corresponding call to [stop].
*/
fun isRunning(): Boolean = mGLThread != null
/**
* Starts the GLThread. After this method is called, consumers can attempt
* to attach [android.view.Surface] instances through [attach] as well as
* schedule content to be drawn through [requestRender]
*
* @param name Optional name to provide to the GLThread
*
* @throws IllegalStateException if EGLConfig with desired attributes cannot be created
*/
@JvmOverloads
fun start(
name: String = "GLThread",
) {
if (mGLThread == null) {
GLThread.log("starting thread...")
mGLThread = GLThread(
name,
mEglSpecFactory,
mEglConfigFactory
).apply {
start()
if (!mEglContextCallback.isEmpty()) {
// Add a copy of the current collection as new entries to mEglContextCallback
// could be mistakenly added multiple times.
this.addEGLCallbacks(ArrayList<EGLContextCallback>(mEglContextCallback))
}
}
}
}
/**
* Mark the corresponding surface session with the given token as dirty
* to schedule a call to [RenderCallback#onDrawFrame].
* If there is already a queued request to render into the provided surface with
* the specified token, this request is ignored.
*
* Note the render operation will only occur if the GLRenderer is started, that is if
* [isRunning] returns true. Otherwise this is a no-op.
*
* @param target RenderTarget to be re-rendered
* @param onRenderComplete Optional callback invoked on the backing thread after the frame has
* been rendered.
*/
@JvmOverloads
fun requestRender(target: RenderTarget, onRenderComplete: ((RenderTarget) -> Unit)? = null) {
val token = target.token
val callbackRunnable = if (onRenderComplete != null) {
Runnable {
onRenderComplete.invoke(target)
}
} else {
null
}
mGLThread?.requestRender(token, callbackRunnable)
}
/**
* Resize the corresponding surface associated with the RenderTarget to the specified
* width and height and re-render. This will destroy the EGLSurface created by
* [RenderCallback.onSurfaceCreated] and invoke it again with the updated dimensions.
* An optional callback is invoked on the backing thread after the resize operation
* is complete.
*
* Note the resize operation will only occur if the GLRenderer is started, that is if
* [isRunning] returns true. Otherwise this is a no-op.
*
* @param target RenderTarget to be resized
* @param width Updated width of the corresponding surface
* @param height Updated height of the corresponding surface
* @param onResizeComplete Optional callback invoked on the backing thread when the resize
* operation is complete
*/
@JvmOverloads
fun resize(
target: RenderTarget,
width: Int,
height: Int,
onResizeComplete: ((RenderTarget) -> Unit)? = null
) {
val token = target.token
val callbackRunnable = if (onResizeComplete != null) {
Runnable {
onResizeComplete.invoke(target)
}
} else {
null
}
mGLThread?.resizeSurface(token, width, height, callbackRunnable)
}
/**
* Queue a [Runnable] to be executed on the GL rendering thread. Note it is important that this
* [Runnable] does not block otherwise it can stall the GL thread.
*
* @param runnable Runnable to be executed
*/
fun execute(runnable: Runnable) {
mGLThread?.execute(runnable)
}
/**
* Stop the corresponding GL thread. This destroys all EGLSurfaces as well
* as any other EGL dependencies. All queued requests that have not been processed
* yet are cancelled.
*
* Note the stop operation will only occur if the GLRenderer was previously started, that is
* [isRunning] returns true. Otherwise this is a no-op.
*
* @param cancelPending If true all pending requests and cancelled and the backing thread is
* torn down immediately. If false, all pending requests are processed first before tearing
* down the backing thread. Subsequent requests made after this call are ignored.
* @param onStop Optional callback invoked on the backing thread after it is torn down.
*/
@JvmOverloads
fun stop(cancelPending: Boolean, onStop: ((GLRenderer) -> Unit)? = null) {
GLThread.log("stopping thread...")
// Make a copy of the render targets to call cleanup operations on to avoid potential
// concurrency issues.
// This method will clear the existing collection and we do not want to potentially tear
// down a target that was attached after a subsequent call to start if the tear down
// callback execution is delayed if previously pending requests have not been cancelled
// (i.e. cancelPending is false)
val renderTargets = ArrayList(mRenderTargets)
mGLThread?.tearDown(cancelPending) {
// No need to call target.detach as this callback is invoked after
// the dependencies are cleaned up
for (target in renderTargets) {
target.release()
target.onDetach.invoke()
}
onStop?.invoke(this@GLRenderer)
}
mGLThread = null
mRenderTargets.clear()
}
/**
* Add an [EGLContextCallback] to receive callbacks for construction and
* destruction of EGL dependencies.
*
* These callbacks are invoked on the backing thread.
*/
@Suppress("AcronymName")
fun registerEGLContextCallback(callback: EGLContextCallback) {
mEglContextCallback.add(callback)
mGLThread?.addEGLCallback(callback)
}
/**
* Remove [EGLContextCallback] to no longer receive callbacks for construction and
* destruction of EGL dependencies.
*
* These callbacks are invoked on the backing thread
*/
@Suppress("AcronymName")
fun unregisterEGLContextCallback(callback: EGLContextCallback) {
mEglContextCallback.remove(callback)
mGLThread?.removeEGLCallback(callback)
}
/**
* Callbacks invoked when the GL dependencies are created and destroyed.
* These are logical places to setup and tear down any dependencies that are used
* for drawing content within a frame (ex. compiling shaders)
*/
@Suppress("AcronymName")
interface EGLContextCallback {
/**
* Callback invoked on the backing thread after EGL dependencies are initialized.
* This is guaranteed to be invoked before any instance of
* [RenderCallback.onSurfaceCreated] is called.
* This will be invoked lazily before the first request to [GLRenderer.requestRender]
*/
// Suppressing CallbackMethodName due to b/238939160
@Suppress("AcronymName", "CallbackMethodName")
@WorkerThread
fun onEGLContextCreated(eglManager: EGLManager)
/**
* Callback invoked on the backing thread before EGL dependencies are about to be torn down.
* This is invoked after [GLRenderer.stop] is processed.
*/
// Suppressing CallbackMethodName due to b/238939160
@Suppress("AcronymName", "CallbackMethodName")
@WorkerThread
fun onEGLContextDestroyed(eglManager: EGLManager)
}
@JvmDefaultWithCompatibility
/**
* Interface used for creating an [EGLSurface] with a user defined configuration
* from the provided surface as well as a callback used to render content into the surface
* for a given frame
*/
interface RenderCallback {
/**
* Used to create a corresponding [EGLSurface] from the provided
* [android.view.Surface] instance. This enables consumers to configure
* the corresponding [EGLSurface] they wish to render into.
* The [EGLSurface] created here is guaranteed to be the current surface
* before [onDrawFrame] is called. That is, implementations of onDrawFrame
* do not need to call eglMakeCurrent on this [EGLSurface].
*
* This method is invoked on the GL thread.
*
* The default implementation will create a window surface with EGL_WIDTH and EGL_HEIGHT
* set to [width] and [height] respectively.
* Implementations can override this method to provide additional [EGLConfigAttributes]
* for this surface (ex. [EGL14.EGL_SINGLE_BUFFER].
*
* Implementations can return null to indicate the default surface should be used.
* This is helpful in situations where content is to be rendered within a frame buffer
* object instead of to an [EGLSurface]
*
* @param spec EGLSpec used to create the corresponding EGLSurface
* @param config EGLConfig used to create the corresponding EGLSurface
* @param surface [android.view.Surface] used to create an EGLSurface from
* @param width Desired width of the surface to create
* @param height Desired height of the surface to create
*/
@WorkerThread
fun onSurfaceCreated(
spec: EGLSpec,
config: EGLConfig,
surface: Surface,
width: Int,
height: Int
): EGLSurface? =
// Always default to creating an EGL window surface
// Despite having access to the width and height here, do not explicitly
// pass in EGLConfigAttributes specifying the EGL_WIDTH and EGL_HEIGHT parameters
// as those are not accepted parameters for eglCreateWindowSurface but they are
// for other EGL Surface factory methods such as eglCreatePBufferSurface
// See accepted parameters here:
// https://www.khronos.org/registry/EGL/sdk/docs/man/html/eglCreateWindowSurface.xhtml
// and here
// https://www.khronos.org/registry/EGL/sdk/docs/man/html/eglCreatePbufferSurface.xhtml
spec.eglCreateWindowSurface(config, surface, null)
/**
* Callback used to issue OpenGL drawing commands into the [EGLSurface]
* created in [onSurfaceCreated]. This [EGLSurface] is guaranteed to
* be current before this callback is invoked and [EGLManager.swapAndFlushBuffers]
* will be invoked afterwards. If additional scratch [EGLSurface]s are used
* here it is up to the implementation of this method to ensure that the proper
* surfaces are made current and the appropriate swap buffers call is made
*
* This method is invoked on the backing thread
*
* @param eglManager Handle to EGL dependencies
*/
@WorkerThread
fun onDrawFrame(eglManager: EGLManager)
}
/**
* Adds the [android.view.Surface] to be managed by the GLThread.
* A corresponding [EGLSurface] is created on the GLThread as well as a callback
* for rendering into the surface through [RenderCallback].
* Unlike the other [attach] methods that consume a [SurfaceView] or [TextureView],
* this method does not handle any lifecycle callbacks associated with the target surface.
* Therefore it is up to the consumer to properly setup/teardown resources associated with
* this surface.
*
* @param surface Target surface to be managed by the backing thread
* @param width Desired width of the [surface]
* @param height Desired height of the [surface]
* @param renderer Callbacks used to create a corresponding [EGLSurface] from the
* given surface as well as render content into the created [EGLSurface]
* @return [RenderTarget] used for subsequent requests to communicate
* with the provided Surface (ex. [requestRender] or [detach]).
*
* @throws IllegalStateException If this method was called when the GLThread has not started
* (i.e. start has not been called)
*/
fun attach(surface: Surface, width: Int, height: Int, renderer: RenderCallback): RenderTarget {
val thread = mGLThread
if (thread != null) {
val token = sToken.getAndIncrement()
thread.attachSurface(token, surface, width, height, renderer)
return RenderTarget(token, this).also { mRenderTargets.add(it) }
} else {
throw IllegalStateException("GLThread not started, did you forget to call start?")
}
}
/**
* Creates a new [RenderTarget] without a corresponding [android.view.Surface].
* This avoids creation of an [EGLSurface] which is useful in scenarios where only
* rendering to a frame buffer object is required.
*
* @param width Desired width of the [RenderTarget]
* @param height Desired height of the [RenderTarget]
* @param renderer Callbacks used to issue OpenGL commands to the [RenderTarget]
* @return [RenderTarget] used for subsequent requests to render through
* [RenderTarget.requestRender] or to remove itself from the [GLRenderer] through
* [RenderTarget.detach]
*
* @throws IllegalStateException If this method was called when the GLThread has not started
* (i.e. start has not been called)
*/
fun createRenderTarget(width: Int, height: Int, renderer: RenderCallback): RenderTarget {
val thread = mGLThread
if (thread != null) {
val token = sToken.getAndIncrement()
thread.attachSurface(
token,
null,
width,
height,
renderer
)
return RenderTarget(token, this).also { mRenderTargets.add(it) }
} else {
throw IllegalStateException("GLThread not started, did you forget to call start?")
}
}
/**
* Adds the [android.view.Surface] provided by the given [SurfaceView] to be managed by the
* backing thread.
*
* A corresponding [EGLSurface] is created on the GLThread as well as a callback
* for rendering into the surface through [RenderCallback].
*
* This method automatically configures a [SurfaceHolder.Callback] used to attach the
* [android.view.Surface] when the underlying [SurfaceHolder] that contains the surface is
* available. Similarly this surface will be detached from [GLRenderer] when the surface provided
* by the [SurfaceView] is destroyed (i.e. [SurfaceHolder.Callback.surfaceDestroyed] is called.
*
* If the [android.view.Surface] is already available by the time this method is invoked,
* it is attached synchronously.
*
* @param surfaceView SurfaceView that provides the surface to be rendered by the backing thread
* @param renderer callbacks used to create a corresponding [EGLSurface] from the
* given surface as well as render content into the created [EGLSurface]
* @return [RenderTarget] used for subsequent requests to communicate
* with the provided Surface (ex. [requestRender] or [detach]).
*
* @throws IllegalStateException If this method was called when the GLThread has not started
* (i.e. start has not been called)
*/
fun attach(surfaceView: SurfaceView, renderer: RenderCallback): RenderTarget {
val thread = mGLThread
if (thread != null) {
val token = sToken.getAndIncrement()
val holder = surfaceView.holder
val callback = object : SurfaceHolder.Callback2 {
var isAttached = false
/**
* Optional condition that maybe used if we are issuing a blocking call to render
* in [SurfaceHolder.Callback2.surfaceRedrawNeeded]
* In this case we need to signal the condition of either the request to render
* has completed, or if the RenderTarget has been detached and the pending
* render request is cancelled.
*/
@Volatile var renderLatch: CountDownLatch? = null
/**
* [CountDownLatch] used when issuing a blocking call to
* [SurfaceHolder.Callback.surfaceDestroyed]
* In this case we need to signal the condition of either the request to detach
* has completed in case the GLRenderer has been forcefully stopped via
* [GLRenderer.stop] with the cancel pending flag set to true.
*/
val detachLatch: CountDownLatch = CountDownLatch(1)
val renderTarget = RenderTarget(token, this@GLRenderer) @WorkerThread {
isAttached = false
// SurfaceHolder.add/remove callback is thread safe
holder.removeCallback(this)
// Countdown in case we have been detached while waiting for a render
// to be completed
renderLatch?.countDown()
detachLatch.countDown()
}
override fun surfaceRedrawNeeded(p0: SurfaceHolder) {
// If the [RenderTarget] has already been detached then skip rendering
if (detachLatch.count > 0) {
val latch = CountDownLatch(1).also { renderLatch = it }
// Request a render and block until the rendering is complete
// surfaceRedrawNeeded is invoked on older API levels and is replaced with
// surfaceRedrawNeededAsync for newer API levels which is non-blocking
renderTarget.requestRender @WorkerThread {
latch.countDown()
}
latch.await()
renderLatch = null
}
}
override fun surfaceRedrawNeededAsync(
holder: SurfaceHolder,
drawingFinished: Runnable
) {
renderTarget.requestRender {
drawingFinished.run()
}
}
override fun surfaceCreated(holder: SurfaceHolder) {
// NO-OP wait until surfaceChanged which is guaranteed to be called and also
// provides the appropriate width height of the surface
}
override fun surfaceChanged(
holder: SurfaceHolder,
format: Int,
width: Int,
height: Int
) {
if (!isAttached) {
thread.attachSurface(token, holder.surface, width, height, renderer)
isAttached = true
} else {
renderTarget.resize(width, height)
}
renderTarget.requestRender()
}
override fun surfaceDestroyed(holder: SurfaceHolder) {
// Issue a request to detech the [RenderTarget]. Even if it was
// previously detached this request is a no-op and the corresponding
// [CountDownLatch] will signal when the [RenderTarget] detachment is complete
// or instantaneously if it was already detached
renderTarget.detach(true)
detachLatch.await()
}
}
holder.addCallback(callback)
if (holder.surface != null && holder.surface.isValid) {
thread.attachSurface(
token,
holder.surface,
surfaceView.width,
surfaceView.height,
renderer
)
}
mRenderTargets.add(callback.renderTarget)
return callback.renderTarget
} else {
throw IllegalStateException("GLThread not started, did you forget to call start?")
}
}
/**
* Adds the [android.view.Surface] provided by the given [TextureView] to be managed by the
* backing thread.
*
* A corresponding [EGLSurface] is created on the GLThread as well as a callback
* for rendering into the surface through [RenderCallback].
*
* This method automatically configures a [TextureView.SurfaceTextureListener] used to create a
* [android.view.Surface] when the underlying [SurfaceTexture] is available.
* Similarly this surface will be detached from [GLRenderer] if the underlying [SurfaceTexture]
* is destroyed (i.e. [TextureView.SurfaceTextureListener.onSurfaceTextureDestroyed] is called.
*
* If the [SurfaceTexture] is already available by the time this method is called, then it is
* attached synchronously.
*
* @param textureView TextureView that provides the surface to be rendered into on the GLThread
* @param renderer callbacks used to create a corresponding [EGLSurface] from the
* given surface as well as render content into the created [EGLSurface]
* @return [RenderTarget] used for subsequent requests to communicate
* with the provided Surface (ex. [requestRender] or [detach]).
*
* @throws IllegalStateException If this method was called when the GLThread has not started
* (i.e. start has not been called)
*/
fun attach(textureView: TextureView, renderer: RenderCallback): RenderTarget {
val thread = mGLThread
if (thread != null) {
val token = sToken.getAndIncrement()
val detachLatch = CountDownLatch(1)
val renderTarget = RenderTarget(token, this) @WorkerThread {
textureView.handler?.post {
textureView.surfaceTextureListener = null
}
detachLatch.countDown()
}
textureView.surfaceTextureListener = object : TextureView.SurfaceTextureListener {
override fun onSurfaceTextureAvailable(
surfaceTexture: SurfaceTexture,
width: Int,
height: Int
) {
thread.attachSurface(token, Surface(surfaceTexture), width, height, renderer)
}
override fun onSurfaceTextureSizeChanged(
texture: SurfaceTexture,
width: Int,
height: Int
) {
renderTarget.resize(width, height)
renderTarget.requestRender()
}
override fun onSurfaceTextureDestroyed(p0: SurfaceTexture): Boolean {
// Issue a request to detech the [RenderTarget]. Even if it was
// previously detached this request is a no-op and the corresponding
// [CountDownLatch] will signal when the [RenderTarget] detachment is complete
// or instantaneously if it was already detached
renderTarget.detach(true)
detachLatch.await()
return true
}
override fun onSurfaceTextureUpdated(p0: SurfaceTexture) {
// NO-OP
}
}
if (textureView.isAvailable) {
thread.attachSurface(
token,
Surface(textureView.surfaceTexture),
textureView.width,
textureView.height,
renderer
)
}
mRenderTargets.add(renderTarget)
return renderTarget
} else {
throw IllegalStateException("GLThread not started, did you forget to call start?")
}
}
/**
* Handle to a [android.view.Surface] that is given to [GLRenderer] to handle
* rendering.
*/
class RenderTarget internal constructor(
internal val token: Int,
glManager: GLRenderer,
@WorkerThread internal val onDetach: () -> Unit = {}
) {
@Volatile
private var mManager: GLRenderer? = glManager
internal fun release() {
mManager = null
}
/**
* Request that this [RenderTarget] should have its contents redrawn.
* This consumes an optional callback that is invoked on the backing thread when
* the rendering is completed.
*
* Note the render operation will only occur if the RenderTarget is attached, that is
* [isAttached] returns true. If the [RenderTarget] is detached or the [GLRenderer] that
* created this RenderTarget is stopped, this is a no-op.
*
* @param onRenderComplete Optional callback called on the backing thread when
* rendering is finished
*/
@JvmOverloads
fun requestRender(@WorkerThread onRenderComplete: ((RenderTarget) -> Unit)? = null) {
mManager?.requestRender(this@RenderTarget, onRenderComplete)
}
/**
* Determines if the current RenderTarget is attached to GLRenderer.
* This is true until [detach] has been called. If the RenderTarget is no longer
* in an attached state (i.e. this returns false). Subsequent calls to [requestRender]
* will be ignored.
*/
fun isAttached(): Boolean = mManager != null
/**
* Resize the RenderTarget to the specified width and height.
* This will destroy the EGLSurface created by [RenderCallback.onSurfaceCreated]
* and invoke it again with the updated dimensions.
* An optional callback is invoked on the backing thread after the resize operation
* is complete
*
* Note the resize operation will only occur if the RenderTarget is attached, that is
* [isAttached] returns true. If the [RenderTarget] is detached or the [GLRenderer] that
* created this RenderTarget is stopped, this is a no-op.
*
* @param width New target width to resize the RenderTarget
* @param height New target height to resize the RenderTarget
* @param onResizeComplete Optional callback invoked after the resize is complete
*/
@JvmOverloads
fun resize(
width: Int,
height: Int,
@WorkerThread onResizeComplete: ((RenderTarget) -> Unit)? = null
) {
mManager?.resize(this, width, height, onResizeComplete)
}
/**
* Removes the corresponding [RenderTarget] from management of the GLThread.
* This destroys the EGLSurface associated with this surface and subsequent requests
* to render into the surface with the provided token are ignored.
*
* If the [cancelPending] flag is set to true, any queued request
* to render that has not started yet is cancelled. However, if this is invoked in the
* middle of the frame being rendered, it will continue to process the current frame.
*
* Additionally if this flag is false, all pending requests to render will be processed
* before the [RenderTarget] is detached.
*
* This is a convenience method around [GLRenderer.detach]
*
* Note the detach operation will only occur if the RenderTarget is attached, that is
* [isAttached] returns true. If the [RenderTarget] is detached or the [GLRenderer] that
* created this RenderTarget is stopped, this is a no-op.
*/
@JvmOverloads
fun detach(cancelPending: Boolean, onDetachComplete: ((RenderTarget) -> Unit)? = null) {
mManager?.detach(this, cancelPending, onDetachComplete)
}
}
companion object {
/**
* Counter used to issue unique identifiers for surfaces that are managed by GLRenderer
*/
private val sToken = AtomicInteger()
}
}